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",
- "[ | ]",
- handle_command_help, 0 },
- { "quit", "Quit the program",
- "[]",
- handle_command_quit, 0 },
- { "buffer", "Manage buffers",
- " | list | clear | move | goto | close []",
- handle_command_buffer, 0 },
- { "set", "Manage configuration",
- "[]",
- handle_command_set, 0 },
- { "save", "Save configuration",
- NULL,
- handle_command_save, 0 },
- { "plugin", "Manage plugins",
- "list | load | unload ",
- handle_command_plugin, 0 },
-
- { "alias", "List or set aliases",
- "[ ]",
- handle_command_alias, 0 },
- { "unalias", "Unset aliases",
- "...",
- handle_command_unalias, 0 },
-
- { "msg", "Send message to a nick or channel",
- " ",
- handle_command_msg, HANDLER_SERVER | HANDLER_NEEDS_REG },
- { "query", "Send a private message to a nick",
- " ",
- handle_command_query, HANDLER_SERVER | HANDLER_NEEDS_REG },
- { "notice", "Send notice to a nick or channel",
- " ",
- handle_command_notice, HANDLER_SERVER | HANDLER_NEEDS_REG },
- { "squery", "Send a message to a service",
- " ",
- handle_command_squery, HANDLER_SERVER | HANDLER_NEEDS_REG },
- { "ctcp", "Send a CTCP query",
- " ",
- handle_command_ctcp, HANDLER_SERVER | HANDLER_NEEDS_REG },
- { "me", "Send a CTCP action",
- "",
- handle_command_me, HANDLER_SERVER | HANDLER_NEEDS_REG },
-
- { "join", "Join channels",
- "[[,...]] [[,...]]",
- handle_command_join, HANDLER_SERVER },
- { "part", "Leave channels",
- "[[,...]] []",
- handle_command_part, HANDLER_SERVER },
- { "cycle", "Rejoin channels",
- "[[,...]] []",
- handle_command_cycle, HANDLER_SERVER },
-
- { "op", "Give channel operator status",
- "...",
- handle_command_op, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
- { "deop", "Remove channel operator status",
- "[...]",
- handle_command_deop, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
- { "voice", "Give voice",
- "...",
- handle_command_voice, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
- { "devoice", "Remove voice",
- "[...]",
- handle_command_devoice, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
-
- { "mode", "Change mode",
- "[] [...]",
- handle_command_mode, HANDLER_SERVER },
- { "topic", "Change topic",
- "[] []",
- handle_command_topic, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
- { "kick", "Kick user from channel",
- "[] []",
- handle_command_kick, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
- { "kickban", "Kick and ban user from channel",
- "[] []",
- handle_command_kickban, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
- { "ban", "Ban user from channel",
- "[] [...]",
- handle_command_ban, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
- { "unban", "Unban user from channel",
- "[] ...",
- handle_command_unban, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
- { "invite", "Invite user to channel",
- "... []",
- handle_command_invite, HANDLER_SERVER | HANDLER_CHANNEL_LAST },
-
- { "server", "Manage servers",
- "list | add | remove | rename ",
- handle_command_server, 0 },
- { "connect", "Connect to the server",
- "[]",
- handle_command_connect, 0 },
- { "disconnect", "Disconnect from the server",
- "[ []]",
- handle_command_disconnect, 0 },
-
- { "list", "List channels and their topic",
- "[[,...]] []",
- handle_command_list, HANDLER_SERVER },
- { "names", "List users on channel",
- "[[,...]]",
- handle_command_names, HANDLER_SERVER },
- { "who", "List users",
- "[ [o]]",
- handle_command_who, HANDLER_SERVER },
- { "whois", "Get user information",
- "[] ",
- handle_command_whois, HANDLER_SERVER },
- { "whowas", "Get user information",
- " [ []]",
- handle_command_whowas, HANDLER_SERVER },
-
- { "motd", "Get the Message of The Day",
- "[]",
- handle_command_motd, HANDLER_SERVER },
- { "oper", "Authenticate as an IRC operator",
- " ",
- handle_command_oper, HANDLER_SERVER },
- { "kill", "Kick another user from the server",
- " ",
- handle_command_kill, HANDLER_SERVER },
- { "stats", "Query server statistics",
- "[ []]",
- handle_command_stats, HANDLER_SERVER },
- { "away", "Set away status",
- "[]",
- handle_command_away, HANDLER_SERVER },
- { "nick", "Change current nick",
- "",
- handle_command_nick, HANDLER_SERVER },
- { "quote", "Send a raw command to the server",
- "",
- handle_command_quote, HANDLER_SERVER },
-};
-
-static bool
-try_handle_command_help_option (struct app_context *ctx, const char *name)
-{
- struct config_item *item =
- config_item_get (ctx->config.root, name, NULL);
- if (!item)
- return false;
-
- struct config_schema *schema = item->schema;
- if (!schema)
- {
- log_global_error (ctx, "#s: #s", "Option not recognized", name);
- return true;
- }
-
- log_global_indent (ctx, "");
- log_global_indent (ctx, "Option \"#s\":", name);
- log_global_indent (ctx, " Description: #s",
- schema->comment ? schema->comment : "(none)");
- log_global_indent (ctx, " Type: #s", config_item_type_name (schema->type));
- log_global_indent (ctx, " Default: #s",
- schema->default_ ? schema->default_ : "null");
-
- struct str tmp = str_make ();
- config_item_write (item, false, &tmp);
- log_global_indent (ctx, " Current value: #s", tmp.str);
- str_free (&tmp);
- return true;
-}
-
-static bool
-show_command_list (struct app_context *ctx)
-{
- log_global_indent (ctx, "");
- log_global_indent (ctx, "Commands:");
-
- int longest = 0;
- for (size_t i = 0; i < N_ELEMENTS (g_command_handlers); i++)
- {
- int len = strlen (g_command_handlers[i].name);
- longest = MAX (longest, len);
- }
- for (size_t i = 0; i < N_ELEMENTS (g_command_handlers); i++)
- {
- struct command_handler *handler = &g_command_handlers[i];
- log_global_indent (ctx, " #&s", xstrdup_printf
- ("%-*s %s", longest, handler->name, handler->description));
- }
- return true;
-}
-
-static bool
-show_command_help (struct app_context *ctx, struct command_handler *handler)
-{
- log_global_indent (ctx, "");
- log_global_indent (ctx, "/#s: #s", handler->name, handler->description);
- log_global_indent (ctx, " Arguments: #s",
- handler->usage ? handler->usage : "(none)");
- return true;
-}
-
-static bool
-handle_command_help (struct handler_args *a)
-{
- struct app_context *ctx = a->ctx;
- if (!*a->arguments)
- return show_command_list (ctx);
-
- const char *word = cut_word (&a->arguments);
-
- const char *command = word + (*word == '/');
- for (size_t i = 0; i < N_ELEMENTS (g_command_handlers); i++)
- {
- struct command_handler *handler = &g_command_handlers[i];
- if (!strcasecmp_ascii (command, handler->name))
- return show_command_help (ctx, handler);
- }
-
- if (try_handle_command_help_option (ctx, word))
- return true;
-
- if (str_map_find (get_aliases_config (ctx), command))
- log_global_status (ctx, "/#s is an alias", command);
- else
- log_global_error (ctx, "#s: #s", "No such command or option", word);
- return true;
-}
-
-static void
-init_user_command_map (struct str_map *map)
-{
- *map = str_map_make (NULL);
- map->key_xfrm = tolower_ascii_strxfrm;
-
- for (size_t i = 0; i < N_ELEMENTS (g_command_handlers); i++)
- {
- struct command_handler *handler = &g_command_handlers[i];
- str_map_set (map, handler->name, handler);
- }
-}
-
-static bool
-process_user_command (struct app_context *ctx, struct buffer *buffer,
- const char *command_name, char *input)
-{
- static bool initialized = false;
- static struct str_map map;
- if (!initialized)
- {
- init_user_command_map (&map);
- initialized = true;
- }
-
- if (try_handle_buffer_goto (ctx, command_name))
- return true;
-
- struct handler_args args =
- {
- .ctx = ctx,
- .buffer = buffer,
- .arguments = input,
- };
-
- struct command_handler *handler;
- if (!(handler = str_map_find (&map, command_name)))
- return false;
- hard_assert (handler->flags == 0 || (handler->flags & HANDLER_SERVER));
-
- if ((handler->flags & HANDLER_SERVER)
- && args.buffer->type == BUFFER_GLOBAL)
- log_global_error (ctx, "/#s: #s",
- command_name, "can't do this from a global buffer");
- else if ((handler->flags & HANDLER_SERVER)
- && !irc_is_connected ((args.s = args.buffer->server)))
- log_server_error (args.s, args.s->buffer, "Not connected");
- else if ((handler->flags & HANDLER_NEEDS_REG)
- && args.s->state != IRC_REGISTERED)
- log_server_error (args.s, args.s->buffer, "Not registered");
- else if (((handler->flags & HANDLER_CHANNEL_FIRST)
- && !(args.channel_name =
- try_get_channel (&args, maybe_cut_word)))
- || ((handler->flags & HANDLER_CHANNEL_LAST)
- && !(args.channel_name =
- try_get_channel (&args, maybe_cut_word_from_end))))
- log_server_error (args.s, args.buffer, "/#s: #s", command_name,
- "no channel name given and this buffer is not a channel");
- else if (!handler->handler (&args))
- log_global_error (ctx,
- "#s: /#s #s", "Usage", handler->name, handler->usage);
- return true;
-}
-
-static const char *
-expand_alias_escape (const char *p, const char *arguments, struct str *output)
-{
- struct strv words = strv_make ();
- cstr_split (arguments, " ", true, &words);
-
- // TODO: eventually also add support for argument ranges
- // - Can use ${0}, ${0:}, ${:0}, ${1:-1} with strtol, dispose of $1 syntax
- // (default aliases don't use numeric arguments).
- // - Start numbering from zero, since we'd have to figure out what to do
- // in case we encounter a zero if we keep the current approach.
- // - Ignore the sequence altogether if no closing '}' can be found,
- // or if the internal format doesn't fit the above syntax.
- if (*p >= '1' && *p <= '9')
- {
- size_t offset = *p - '1';
- if (offset < words.len)
- str_append (output, words.vector[offset]);
- }
- else if (*p == '*')
- str_append (output, arguments);
- else if (strchr ("$;", *p))
- str_append_c (output, *p);
- else
- str_append_printf (output, "$%c", *p);
-
- strv_free (&words);
- return ++p;
-}
-
-static void
-expand_alias_definition (const char *definition, const char *arguments,
- struct strv *commands)
-{
- struct str expanded = str_make ();
- bool escape = false;
- for (const char *p = definition; *p; p++)
- {
- if (escape)
- {
- p = expand_alias_escape (p, arguments, &expanded) - 1;
- escape = false;
- }
- else if (*p == ';')
- {
- strv_append_owned (commands, str_steal (&expanded));
- expanded = str_make ();
- }
- else if (*p == '$' && p[1])
- escape = true;
- else
- str_append_c (&expanded, *p);
- }
- strv_append_owned (commands, str_steal (&expanded));
-}
-
-static bool
-expand_alias (struct app_context *ctx,
- const char *alias_name, char *input, struct strv *commands)
-{
- struct config_item *entry =
- str_map_find (get_aliases_config (ctx), alias_name);
- if (!entry)
- return false;
-
- if (!config_item_type_is_string (entry->type))
- {
- log_global_error (ctx, "Error executing `/#s': #s",
- alias_name, "alias definition is not a string");
- return false;
- }
-
- expand_alias_definition (entry->value.string.str, input, commands);
- return true;
-}
-
-// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-static void
-send_message_to_target (struct server *s,
- const char *target, char *message, struct buffer *buffer)
-{
- if (!irc_is_connected (s))
- log_server_error (s, buffer, "Not connected");
- else
- SEND_AUTOSPLIT_PRIVMSG (s, target, message);
-}
-
-static void
-send_message_to_buffer (struct app_context *ctx, struct buffer *buffer,
- char *message)
-{
- hard_assert (buffer != NULL);
-
- switch (buffer->type)
- {
- case BUFFER_CHANNEL:
- send_message_to_target (buffer->server,
- buffer->channel->name, message, buffer);
- break;
- case BUFFER_PM:
- send_message_to_target (buffer->server,
- buffer->user->nickname, message, buffer);
- break;
- default:
- log_full (ctx, NULL, buffer, BUFFER_LINE_ERROR,
- "This buffer is not a channel");
- }
-}
-
-static bool
-process_alias (struct app_context *ctx, struct buffer *buffer,
- struct strv *commands, int level)
-{
- for (size_t i = 0; i < commands->len; i++)
- log_global_debug (ctx, "Alias expanded to: ###d: \"#s\"",
- (int) i, commands->vector[i]);
- for (size_t i = 0; i < commands->len; i++)
- if (!process_input_utf8 (ctx, buffer, commands->vector[i], ++level))
- return false;
- return true;
-}
-
-static bool
-process_input_utf8_posthook (struct app_context *ctx, struct buffer *buffer,
- char *input, int alias_level)
-{
- if (*input != '/' || *++input == '/')
- {
- send_message_to_buffer (ctx, buffer, input);
- return true;
- }
-
- char *name = cut_word (&input);
- if (process_user_command (ctx, buffer, name, input))
- return true;
-
- struct strv commands = strv_make ();
- bool result = false;
- if (!expand_alias (ctx, name, input, &commands))
- log_global_error (ctx, "#s: /#s", "No such command or alias", name);
- else if (alias_level != 0)
- log_global_error (ctx, "#s: /#s", "Aliases can't nest", name);
- else
- result = process_alias (ctx, buffer, &commands, alias_level);
-
- strv_free (&commands);
- return result;
-}
-
-static char *
-process_input_hooks (struct app_context *ctx, struct buffer *buffer,
- char *input)
-{
- uint64_t hash = siphash_wrapper (input, strlen (input));
- LIST_FOR_EACH (struct hook, iter, ctx->input_hooks)
- {
- struct input_hook *hook = (struct input_hook *) iter;
- if (!(input = hook->filter (hook, buffer, input)))
- {
- log_global_debug (ctx, "Input thrown away by hook");
- return NULL;
- }
-
- uint64_t new_hash = siphash_wrapper (input, strlen (input));
- if (new_hash != hash)
- log_global_debug (ctx, "Input transformed to \"#s\"#r", input);
- hash = new_hash;
- }
- return input;
-}
-
-static bool
-process_input_utf8 (struct app_context *ctx, struct buffer *buffer,
- const char *input, int alias_level)
-{
- // Note that this also gets called on expanded aliases,
- // which might or might not be desirable (we can forward "alias_level")
- char *processed = process_input_hooks (ctx, buffer, xstrdup (input));
- bool result = !processed
- || process_input_utf8_posthook (ctx, buffer, processed, alias_level);
- free (processed);
- return result;
-}
-
-static void
-process_input (struct app_context *ctx, char *user_input)
-{
- char *input;
- if (!(input = iconv_xstrdup (ctx->term_to_utf8, user_input, -1, NULL)))
- print_error ("character conversion failed for: %s", "user input");
- else
- {
- struct strv lines = strv_make ();
-
- // XXX: this interprets commands in pasted text
- cstr_split (input, "\r\n", false, &lines);
- for (size_t i = 0; i < lines.len; i++)
- (void) process_input_utf8 (ctx,
- ctx->current_buffer, lines.vector[i], 0);
-
- strv_free (&lines);
- }
- free (input);
-}
-
-// --- Word completion ---------------------------------------------------------
-
-// The amount of crap that goes into this is truly insane.
-// It's mostly because of Editline's total ignorance of this task.
-
-static void
-completion_init (struct completion *self)
-{
- memset (self, 0, sizeof *self);
-}
-
-static void
-completion_free (struct completion *self)
-{
- free (self->line);
- free (self->words);
-}
-
-static void
-completion_add_word (struct completion *self, size_t start, size_t end)
-{
- if (!self->words)
- self->words = xcalloc ((self->words_alloc = 4), sizeof *self->words);
- if (self->words_len == self->words_alloc)
- self->words = xreallocarray (self->words,
- (self->words_alloc <<= 1), sizeof *self->words);
- self->words[self->words_len++] = (struct completion_word) { start, end };
-}
-
-static void
-completion_parse (struct completion *self, const char *line, size_t len)
-{
- self->line = xstrndup (line, len);
-
- // The first and the last word may be empty
- const char *s = self->line;
- while (true)
- {
- const char *start = s;
- size_t word_len = strcspn (s, WORD_BREAKING_CHARS);
- const char *end = start + word_len;
- s = end + strspn (end, WORD_BREAKING_CHARS);
-
- completion_add_word (self, start - self->line, end - self->line);
- if (s == end)
- break;
- }
-}
-
-static void
-completion_locate (struct completion *self, size_t offset)
-{
- size_t i = 0;
- for (; i < self->words_len; i++)
- if (self->words[i].start > offset)
- break;
- self->location = i - 1;
-}
-
-static char *
-completion_word (struct completion *self, int word)
-{
- hard_assert (word >= 0 && word < (int) self->words_len);
- return xstrndup (self->line + self->words[word].start,
- self->words[word].end - self->words[word].start);
-}
-
-// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-// XXX: this isn't completely right because Unicode, but let's keep it simple.
-// At worst it will stop before a combining mark, or fail to compare
-// non-ASCII identifiers case-insensitively.
-
-static size_t
-utf8_common_prefix (const char **vector, size_t len)
-{
- size_t prefix = 0;
- if (!vector || !len)
- return 0;
-
- struct utf8_iter a[len];
- for (size_t i = 0; i < len; i++)
- a[i] = utf8_iter_make (vector[i]);
-
- size_t ch_len;
- int32_t ch;
- while ((ch = utf8_iter_next (&a[0], &ch_len)) >= 0)
- {
- for (size_t i = 1; i < len; i++)
- {
- int32_t other = utf8_iter_next (&a[i], NULL);
- if (ch == other)
- continue;
- // Not bothering with lowercasing non-ASCII
- if (ch >= 0x80 || other >= 0x80
- || tolower_ascii (ch) != tolower_ascii (other))
- return prefix;
- }
- prefix += ch_len;
- }
- return prefix;
-}
-
-static void
-complete_command (struct app_context *ctx, struct completion *data,
- const char *word, struct strv *output)
-{
- (void) data;
-
- const char *prefix = "";
- if (*word == '/')
- {
- word++;
- prefix = "/";
- }
-
- size_t word_len = strlen (word);
- for (size_t i = 0; i < N_ELEMENTS (g_command_handlers); i++)
- {
- struct command_handler *handler = &g_command_handlers[i];
- if (!strncasecmp_ascii (word, handler->name, word_len))
- strv_append_owned (output,
- xstrdup_printf ("%s%s", prefix, handler->name));
- }
-
- struct str_map_iter iter = str_map_iter_make (get_aliases_config (ctx));
- struct config_item *alias;
- while ((alias = str_map_iter_next (&iter)))
- {
- if (!strncasecmp_ascii (word, iter.link->key, word_len))
- strv_append_owned (output,
- xstrdup_printf ("%s%s", prefix, iter.link->key));
- }
-}
-
-static void
-complete_option (struct app_context *ctx, struct completion *data,
- const char *word, struct strv *output)
-{
- (void) data;
-
- struct strv options = strv_make ();
- config_dump (ctx->config.root, &options);
- strv_sort (&options);
-
- // Wildcard expansion is an interesting side-effect
- char *mask = xstrdup_printf ("%s*", word);
- for (size_t i = 0; i < options.len; i++)
- {
- char *key = cstr_cut_until (options.vector[i], " ");
- if (!fnmatch (mask, key, 0))
- strv_append_owned (output, key);
- else
- free (key);
- }
- free (mask);
- strv_free (&options);
-}
-
-static void
-complete_set_value (struct config_item *item, const char *word,
- struct strv *output)
-{
- struct str serialized = str_make ();
- config_item_write (item, false, &serialized);
- if (!strncmp (serialized.str, word, strlen (word)))
- strv_append_owned (output, str_steal (&serialized));
- else
- str_free (&serialized);
-}
-
-static void
-complete_set_value_array (struct config_item *item, const char *word,
- struct strv *output)
-{
- if (!item->schema || item->schema->type != CONFIG_ITEM_STRING_ARRAY)
- return;
-
- struct strv items = strv_make ();
- cstr_split (item->value.string.str, ",", false, &items);
- for (size_t i = 0; i < items.len; i++)
- {
- struct str wrapped = str_make (), serialized = str_make ();
- str_append (&wrapped, items.vector[i]);
- config_item_write_string (&serialized, &wrapped);
- str_free (&wrapped);
-
- if (!strncmp (serialized.str, word, strlen (word)))
- strv_append_owned (output, str_steal (&serialized));
- else
- str_free (&serialized);
- }
- strv_free (&items);
-}
-
-static void
-complete_set (struct app_context *ctx, struct completion *data,
- const char *word, struct strv *output)
-{
- if (data->location == 1)
- {
- complete_option (ctx, data, word, output);
- return;
- }
- if (data->location != 3)
- return;
-
- char *key = completion_word (data, 1);
- struct config_item *item = config_item_get (ctx->config.root, key, NULL);
- if (item)
- {
- char *op = completion_word (data, 2);
- if (!strcmp (op, "-=")) complete_set_value_array (item, word, output);
- if (!strcmp (op, "=")) complete_set_value (item, word, output);
- free (op);
- }
- free (key);
-}
-
-static void
-complete_topic (struct app_context *ctx, struct completion *data,
- const char *word, struct strv *output)
-{
- (void) data;
-
- // TODO: make it work in other server-related buffers, too, i.e. when we're
- // completing the third word and the second word is a known channel name
- struct buffer *buffer = ctx->current_buffer;
- if (buffer->type != BUFFER_CHANNEL)
- return;
-
- const char *topic = buffer->channel->topic;
- if (topic && !strncasecmp_ascii (word, topic, strlen (word)))
- {
- // We must prepend the channel name if the topic itself starts
- // with something that could be regarded as a channel name
- strv_append_owned (output, irc_is_channel (buffer->server, topic)
- ? xstrdup_printf ("%s %s", buffer->channel->name, topic)
- : xstrdup (topic));
- }
-}
-
-static void
-complete_nicknames (struct app_context *ctx, struct completion *data,
- const char *word, struct strv *output)
-{
- struct buffer *buffer = ctx->current_buffer;
- if (buffer->type == BUFFER_SERVER)
- {
- struct user *self_user = buffer->server->irc_user;
- if (self_user)
- strv_append (output, self_user->nickname);
- }
- if (buffer->type != BUFFER_CHANNEL)
- return;
-
- size_t word_len = strlen (word);
- LIST_FOR_EACH (struct channel_user, iter, buffer->channel->users)
- {
- const char *nickname = iter->user->nickname;
- if (irc_server_strncmp (buffer->server, word, nickname, word_len))
- continue;
- strv_append_owned (output, data->location == 0
- ? xstrdup_printf ("%s:", nickname)
- : xstrdup (nickname));
- }
-}
-
-static char **
-complete_word (struct app_context *ctx, struct completion *data,
- const char *word)
-{
- char *initial = completion_word (data, 0);
-
- // Start with a placeholder for the longest common prefix
- struct strv words = strv_make ();
- strv_append_owned (&words, NULL);
-
- if (data->location == 0 && *initial == '/')
- complete_command (ctx, data, word, &words);
- else if (data->location >= 1 && !strcmp (initial, "/set"))
- complete_set (ctx, data, word, &words);
- else if (data->location == 1 && !strcmp (initial, "/help"))
- {
- complete_command (ctx, data, word, &words);
- complete_option (ctx, data, word, &words);
- }
- else if (data->location == 1 && !strcmp (initial, "/topic"))
- {
- complete_topic (ctx, data, word, &words);
- complete_nicknames (ctx, data, word, &words);
- }
- else
- complete_nicknames (ctx, data, word, &words);
-
- cstr_set (&initial, NULL);
- LIST_FOR_EACH (struct hook, iter, ctx->completion_hooks)
- {
- struct completion_hook *hook = (struct completion_hook *) iter;
- hook->complete (hook, data, word, &words);
- }
-
- if (words.len == 1)
- {
- // Nothing matched
- strv_free (&words);
- return NULL;
- }
-
- if (words.len == 2)
- {
- words.vector[0] = words.vector[1];
- words.vector[1] = NULL;
- }
- else
- {
- size_t prefix = utf8_common_prefix
- ((const char **) words.vector + 1, words.len - 1);
- if (!prefix)
- words.vector[0] = xstrdup (word);
- else
- words.vector[0] = xstrndup (words.vector[1], prefix);
- }
- return words.vector;
-}
-
-// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-/// A special wrapper for iconv_xstrdup() that also fixes indexes into the
-/// original string to point to the right location in the output.
-/// Thanks, Readline! Without you I would have never needed to deal with this.
-static char *
-locale_to_utf8 (struct app_context *ctx, const char *locale,
- int *indexes[], size_t n_indexes)
-{
- mbstate_t state;
- memset (&state, 0, sizeof state);
-
- size_t remaining = strlen (locale) + 1;
- const char *p = locale;
-
- // Reset the shift state, FWIW
- (void) iconv (ctx->term_to_utf8, NULL, NULL, NULL, NULL);
-
- bool fixed[n_indexes];
- memset (fixed, 0, sizeof fixed);
-
- struct str utf8 = str_make ();
- while (true)
- {
- size_t len = mbrlen (p, remaining, &state);
-
- // Incomplete multibyte character or illegal sequence (probably)
- if (len == (size_t) -2
- || len == (size_t) -1)
- {
- str_free (&utf8);
- return NULL;
- }
-
- // Convert indexes into the multibyte string to UTF-8
- for (size_t i = 0; i < n_indexes; i++)
- if (!fixed[i] && *indexes[i] <= p - locale)
- {
- *indexes[i] = utf8.len;
- fixed[i] = true;
- }
-
- // End of string
- if (!len)
- break;
-
- // EINVAL (incomplete sequence) should never happen and
- // EILSEQ neither because we've already checked for that with mbrlen().
- // E2BIG is what iconv_xstrdup solves. This must succeed.
- size_t ch_len;
- char *ch = iconv_xstrdup (ctx->term_to_utf8, (char *) p, len, &ch_len);
- hard_assert (ch != NULL);
- str_append_data (&utf8, ch, ch_len);
- free (ch);
-
- p += len;
- remaining -= len;
- }
- return str_steal (&utf8);
-}
-
-static void
-utf8_vector_to_locale (struct app_context *ctx, char **vector)
-{
- for (; *vector; vector++)
- {
- char *converted = iconv_xstrdup
- (ctx->term_from_utf8, *vector, -1, NULL);
- if (!soft_assert (converted))
- converted = xstrdup ("");
-
- cstr_set (vector, converted);
- }
-}
-
-// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-/// Takes a line in locale-specific encoding and position of a word to complete,
-/// returns a vector of matches in locale-specific encoding.
-static char **
-make_completions (struct app_context *ctx, char *line, int start, int end)
-{
- int *fixes[] = { &start, &end };
- char *line_utf8 = locale_to_utf8 (ctx, line, fixes, N_ELEMENTS (fixes));
- if (!line_utf8)
- return NULL;
-
- hard_assert (start >= 0 && end >= 0 && start <= end);
-
- struct completion c;
- completion_init (&c);
- completion_parse (&c, line, strlen (line));
- completion_locate (&c, start);
- char *word = xstrndup (line + start, end - start);
- char **completions = complete_word (ctx, &c, word);
- free (word);
- completion_free (&c);
-
- if (completions)
- utf8_vector_to_locale (ctx, completions);
-
- free (line_utf8);
- return completions;
-}
-
-// --- Common code for user actions --------------------------------------------
-
-static void
-toggle_bracketed_paste (bool enable)
-{
- fprintf (stdout, "\x1b[?2004%c", "lh"[enable]);
- fflush (stdout);
-}
-
-static void
-suspend_terminal (struct app_context *ctx)
-{
- // Terminal can get suspended by both backlog helper and SIGTSTP handling
- if (ctx->terminal_suspended++ > 0)
- return;
-
- toggle_bracketed_paste (false);
- CALL (ctx->input, hide);
- poller_fd_reset (&ctx->tty_event);
-
- CALL_ (ctx->input, prepare, false);
-}
-
-static void
-resume_terminal (struct app_context *ctx)
-{
- if (--ctx->terminal_suspended > 0)
- return;
-
- update_screen_size ();
- CALL_ (ctx->input, prepare, true);
- CALL (ctx->input, on_tty_resized);
-
- toggle_bracketed_paste (true);
- // In theory we could just print all unseen messages but this is safer
- buffer_print_backlog (ctx, ctx->current_buffer);
- // Now it's safe to process any user input
- poller_fd_set (&ctx->tty_event, POLLIN);
- CALL (ctx->input, show);
-}
-
-static pid_t
-spawn_helper_child (struct app_context *ctx)
-{
- suspend_terminal (ctx);
- pid_t child = fork ();
- switch (child)
- {
- case -1:
- {
- int saved_errno = errno;
- resume_terminal (ctx);
- errno = saved_errno;
- break;
- }
- case 0:
- // Put the child in a new foreground process group
- hard_assert (setpgid (0, 0) != -1);
- hard_assert (tcsetpgrp (STDOUT_FILENO, getpgid (0)) != -1);
- break;
- default:
- // Make sure of it in the parent as well before continuing
- (void) setpgid (child, child);
- }
- return child;
-}
-
-static void
-redraw_screen (struct app_context *ctx)
-{
- // If by some circumstance we had the wrong idea
- CALL (ctx->input, on_tty_resized);
- update_screen_size ();
-
- CALL (ctx->input, hide);
- buffer_print_backlog (ctx, ctx->current_buffer);
- CALL (ctx->input, show);
-}
-
-// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-static bool
-dump_input_to_file (struct app_context *ctx, char *template, struct error **e)
-{
- mode_t mask = umask (S_IXUSR | S_IRWXG | S_IRWXO);
- int fd = mkstemp (template);
- (void) umask (mask);
-
- if (fd < 0)
- return error_set (e, "%s", strerror (errno));
-
- char *input = CALL (ctx->input, get_line);
- bool success = xwrite (fd, input, strlen (input), e);
- free (input);
-
- if (!success)
- (void) unlink (template);
-
- xclose (fd);
- return success;
-}
-
-static char *
-try_dump_input_to_file (struct app_context *ctx)
-{
- char *template = resolve_filename
- ("input.XXXXXX", resolve_relative_runtime_template);
-
- struct error *e = NULL;
- if (dump_input_to_file (ctx, template, &e))
- return template;
-
- log_global_error (ctx, "#s: #s",
- "Failed to create a temporary file for editing", e->message);
- error_free (e);
- free (template);
- return NULL;
-}
-
-static bool
-on_edit_input (int count, int key, void *user_data)
-{
- (void) count;
- (void) key;
- struct app_context *ctx = user_data;
-
- char *filename;
- if (!(filename = try_dump_input_to_file (ctx)))
- return false;
-
- const char *command;
- if (!(command = getenv ("VISUAL"))
- && !(command = getenv ("EDITOR")))
- command = "vi";
-
- hard_assert (!ctx->running_editor);
- switch (spawn_helper_child (ctx))
- {
- case 0:
- execlp (command, command, filename, NULL);
- print_error ("%s: %s",
- "Failed to launch editor", strerror (errno));
- _exit (EXIT_FAILURE);
- case -1:
- log_global_error (ctx, "#s: #l",
- "Failed to launch editor", strerror (errno));
- free (filename);
- break;
- default:
- ctx->running_editor = true;
- ctx->editor_filename = filename;
- }
- return true;
-}
-
-static void
-input_editor_process (struct app_context *ctx)
-{
- struct str input = str_make ();
- struct error *e = NULL;
- if (!read_file (ctx->editor_filename, &input, &e))
- {
- log_global_error (ctx, "#s: #s", "Input editing failed", e->message);
- error_free (e);
- }
- else
- CALL (ctx->input, clear_line);
-
- if (!CALL_ (ctx->input, insert, input.str))
- log_global_error (ctx, "#s: #s", "Input editing failed",
- "could not re-insert the modified text");
-
- str_free (&input);
-}
-
-static void
-input_editor_cleanup (struct app_context *ctx)
-{
- if (unlink (ctx->editor_filename))
- log_global_error (ctx, "Could not unlink `#s': #l",
- ctx->editor_filename, strerror (errno));
-
- cstr_set (&ctx->editor_filename, NULL);
- ctx->running_editor = false;
-}
-
-// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-static void
-launch_backlog_helper (struct app_context *ctx, int backlog_fd)
-{
- hard_assert (!ctx->running_backlog_helper);
- switch (spawn_helper_child (ctx))
- {
- case 0:
- dup2 (backlog_fd, STDIN_FILENO);
- execl ("/bin/sh", "/bin/sh", "-c", get_config_string
- (ctx->config.root, "behaviour.backlog_helper"), NULL);
- print_error ("%s: %s",
- "Failed to launch backlog helper", strerror (errno));
- _exit (EXIT_FAILURE);
- case -1:
- log_global_error (ctx, "#s: #l",
- "Failed to launch backlog helper", strerror (errno));
- break;
- default:
- ctx->running_backlog_helper = true;
- }
-}
-
-static bool
-display_backlog (struct app_context *ctx, int flush_opts)
-{
- FILE *backlog = tmpfile ();
- if (!backlog)
- {
- log_global_error (ctx, "#s: #l",
- "Failed to create a temporary file", strerror (errno));
- return false;
- }
-
- if (!get_config_boolean (ctx->config.root,
- "behaviour.backlog_helper_strip_formatting"))
- flush_opts |= FLUSH_OPT_RAW;
-
- struct buffer *buffer = ctx->current_buffer;
- int until_marker =
- (int) buffer->lines_count - (int) buffer->new_messages_count;
- for (struct buffer_line *line = buffer->lines; line; line = line->next)
- {
- if (until_marker-- == 0
- && buffer->new_messages_count != buffer->lines_count)
- buffer_print_read_marker (ctx, backlog, flush_opts);
- if (buffer_line_will_show_up (buffer, line))
- buffer_line_write_to_backlog (ctx, line, backlog, flush_opts);
- }
-
- // So that it is obvious if the last line in the buffer is not from today
- buffer_update_time (ctx, time (NULL), backlog, flush_opts);
-
- rewind (backlog);
- set_cloexec (fileno (backlog));
- launch_backlog_helper (ctx, fileno (backlog));
- fclose (backlog);
- return true;
-}
-
-static bool
-on_display_backlog (int count, int key, void *user_data)
-{
- (void) count;
- (void) key;
- return display_backlog (user_data, 0);
-}
-
-static bool
-on_display_backlog_nowrap (int count, int key, void *user_data)
-{
- (void) count;
- (void) key;
- return display_backlog (user_data, FLUSH_OPT_NOWRAP);
-}
-
-static bool
-on_display_full_log (int count, int key, void *user_data)
-{
- (void) count;
- (void) key;
- struct app_context *ctx = user_data;
-
- char *path = buffer_get_log_path (ctx->current_buffer);
- FILE *full_log = fopen (path, "rb");
- free (path);
-
- if (!full_log)
- {
- log_global_error (ctx, "Failed to open log file for #s: #l",
- ctx->current_buffer->name, strerror (errno));
- return false;
- }
-
- if (ctx->current_buffer->log_file)
- // The regular flush will log any error eventually
- (void) fflush (ctx->current_buffer->log_file);
-
- set_cloexec (fileno (full_log));
- launch_backlog_helper (ctx, fileno (full_log));
- fclose (full_log);
- return true;
-}
-
-static bool
-on_toggle_unimportant (int count, int key, void *user_data)
-{
- (void) count;
- (void) key;
- struct app_context *ctx = user_data;
- ctx->current_buffer->hide_unimportant ^= true;
- buffer_print_backlog (ctx, ctx->current_buffer);
- return true;
-}
-
-// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-static bool
-on_goto_buffer (int count, int key, void *user_data)
-{
- (void) count;
- struct app_context *ctx = user_data;
-
- int n = key - '0';
- if (n < 0 || n > 9)
- return false;
-
- // There's no buffer zero
- if (n == 0)
- n = 10;
-
- if (!ctx->last_buffer || buffer_get_index (ctx, ctx->current_buffer) != n)
- return buffer_goto (ctx, n);
-
- // Fast switching between two buffers
- buffer_activate (ctx, ctx->last_buffer);
- return true;
-}
-
-static bool
-on_previous_buffer (int count, int key, void *user_data)
-{
- (void) key;
- buffer_activate (user_data, buffer_previous (user_data, count));
- return true;
-}
-
-static bool
-on_next_buffer (int count, int key, void *user_data)
-{
- (void) key;
- buffer_activate (user_data, buffer_next (user_data, count));
- return true;
-}
-
-static bool
-on_switch_buffer (int count, int key, void *user_data)
-{
- (void) count;
- (void) key;
- struct app_context *ctx = user_data;
-
- if (!ctx->last_buffer)
- return false;
- buffer_activate (ctx, ctx->last_buffer);
- return true;
-}
-
-static bool
-on_goto_highlight (int count, int key, void *user_data)
-{
- (void) count;
- (void) key;
-
- struct app_context *ctx = user_data;
- struct buffer *iter = ctx->current_buffer;;
- do
- {
- if (!(iter = iter->next))
- iter = ctx->buffers;
- if (iter == ctx->current_buffer)
- return false;
- }
- while (!iter->highlighted);
- buffer_activate (ctx, iter);
- return true;
-}
-
-static bool
-on_goto_activity (int count, int key, void *user_data)
-{
- (void) count;
- (void) key;
-
- struct app_context *ctx = user_data;
- struct buffer *iter = ctx->current_buffer;
- do
- {
- if (!(iter = iter->next))
- iter = ctx->buffers;
- if (iter == ctx->current_buffer)
- return false;
- }
- while (iter->new_messages_count == iter->new_unimportant_count);
- buffer_activate (ctx, iter);
- return true;
-}
-
-static bool
-on_move_buffer_left (int count, int key, void *user_data)
-{
- (void) key;
-
- struct app_context *ctx = user_data;
- int total = buffer_count (ctx);
- int n = buffer_get_index (ctx, ctx->current_buffer) - count;
- buffer_move (ctx, ctx->current_buffer, n <= 0
- ? (total + n % total)
- : ((n - 1) % total + 1));
- return true;
-}
-
-static bool
-on_move_buffer_right (int count, int key, void *user_data)
-{
- return on_move_buffer_left (-count, key, user_data);
-}
-
-static bool
-on_redraw_screen (int count, int key, void *user_data)
-{
- (void) count;
- (void) key;
-
- redraw_screen (user_data);
- return true;
-}
-
-static bool
-on_insert_attribute (int count, int key, void *user_data)
-{
- (void) count;
- (void) key;
-
- struct app_context *ctx = user_data;
- ctx->awaiting_mirc_escape = true;
- return true;
-}
-
-static bool
-on_start_paste_mode (int count, int key, void *user_data)
-{
- (void) count;
- (void) key;
-
- struct app_context *ctx = user_data;
- ctx->in_bracketed_paste = true;
- return true;
-}
-
-static void
-input_add_functions (void *user_data)
-{
- struct app_context *ctx = user_data;
-#define XX(...) CALL_ (ctx->input, register_fn, __VA_ARGS__, ctx);
- XX ("previous-buffer", "Previous buffer", on_previous_buffer)
- XX ("next-buffer", "Next buffer", on_next_buffer)
- XX ("goto-buffer", "Go to buffer", on_goto_buffer)
- XX ("switch-buffer", "Switch buffer", on_switch_buffer)
- XX ("goto-highlight", "Go to highlight", on_goto_highlight)
- XX ("goto-activity", "Go to activity", on_goto_activity)
- XX ("move-buffer-left", "Move buffer left", on_move_buffer_left)
- XX ("move-buffer-right", "Move buffer right", on_move_buffer_right)
- XX ("display-backlog", "Show backlog", on_display_backlog)
- XX ("display-backlog-nw", "Non-wrapped log", on_display_backlog_nowrap)
- XX ("display-full-log", "Show full log", on_display_full_log)
- XX ("toggle-unimportant", "Toggle junk msgs", on_toggle_unimportant)
- XX ("edit-input", "Edit input", on_edit_input)
- XX ("redraw-screen", "Redraw screen", on_redraw_screen)
- XX ("insert-attribute", "mIRC formatting", on_insert_attribute)
- XX ("start-paste-mode", "Bracketed paste", on_start_paste_mode)
-#undef XX
-}
-
-static void
-bind_common_keys (struct app_context *ctx)
-{
- struct input *self = ctx->input;
- CALL_ (self, bind_control, 'p', "previous-buffer");
- CALL_ (self, bind_control, 'n', "next-buffer");
-
- // Redefine M-0 through M-9 to switch buffers
- for (int i = 0; i <= 9; i++)
- CALL_ (self, bind_meta, '0' + i, "goto-buffer");
-
- CALL_ (self, bind_meta, '\t', "switch-buffer");
- CALL_ (self, bind_meta, '!', "goto-highlight");
- CALL_ (self, bind_meta, 'a', "goto-activity");
- CALL_ (self, bind_meta, 'm', "insert-attribute");
- CALL_ (self, bind_meta, 'h', "display-full-log");
- CALL_ (self, bind_meta, 'H', "toggle-unimportant");
- CALL_ (self, bind_meta, 'e', "edit-input");
-
- if (key_f5) CALL_ (self, bind, key_f5, "previous-buffer");
- if (key_f6) CALL_ (self, bind, key_f6, "next-buffer");
- if (key_ppage) CALL_ (self, bind, key_ppage, "display-backlog");
-
- if (clear_screen)
- CALL_ (self, bind_control, 'l', "redraw-screen");
-
- CALL_ (self, bind, "\x1b[200~", "start-paste-mode");
-}
-
-// --- GNU Readline user actions -----------------------------------------------
-
-#ifdef HAVE_READLINE
-
-static int
-on_readline_return (int count, int key)
-{
- (void) count;
- (void) key;
-
- // Let readline pass the line to our input handler
- rl_done = 1;
-
- struct app_context *ctx = g_ctx;
- struct input_rl *self = (struct input_rl *) ctx->input;
-
- // Hide the line, don't redisplay it
- CALL (ctx->input, hide);
- input_rl__restore (self);
- return 0;
-}
-
-static void
-on_readline_input (char *line)
-{
- struct app_context *ctx = g_ctx;
- struct input_rl *self = (struct input_rl *) ctx->input;
-
- if (line)
- {
- if (*line)
- add_history (line);
-
- // readline always erases the input line after returning from here,
- // but we don't want that to happen if the command to be executed
- // would switch the buffer (we'd keep the already executed command in
- // the old buffer and delete any input restored from the new buffer)
- strv_append_owned (&ctx->pending_input, line);
- poller_idle_set (&ctx->input_event);
- }
- else
- {
- // Prevent readline from showing the prompt twice for w/e reason
- CALL (ctx->input, hide);
- input_rl__restore (self);
-
- CALL (ctx->input, ding);
- }
-
- if (self->active)
- // Readline automatically redisplays it
- self->prompt_shown = 1;
-}
-
-static char **
-app_readline_completion (const char *text, int start, int end)
-{
- // We will reconstruct that ourselves
- (void) text;
-
- // Don't iterate over filenames and stuff
- rl_attempted_completion_over = true;
-
- return make_completions (g_ctx, rl_line_buffer, start, end);
-}
-
-static int
-app_readline_init (void)
-{
- struct app_context *ctx = g_ctx;
- struct input *self = ctx->input;
-
- // XXX: maybe use rl_make_bare_keymap() and start from there;
- // our dear user could potentionally rig things up in a way that might
- // result in some funny unspecified behaviour
-
- // For vi mode, enabling "show-mode-in-prompt" is recommended as there is
- // no easy way to indicate mode changes otherwise.
-
- rl_add_defun ("send-line", on_readline_return, -1);
- bind_common_keys (ctx);
-
- // Move native history commands
- CALL_ (self, bind_meta, 'p', "previous-history");
- CALL_ (self, bind_meta, 'n', "next-history");
-
- // We need to hide the prompt and input first
- rl_bind_key (RETURN, rl_named_function ("send-line"));
- CALL_ (self, bind_control, 'j', "send-line");
-
- rl_variable_bind ("completion-ignore-case", "on");
- rl_bind_key (TAB, rl_named_function ("menu-complete"));
- if (key_btab)
- CALL_ (self, bind, key_btab, "menu-complete-backward");
- return 0;
-}
-
-#endif // HAVE_READLINE
-
-// --- BSD Editline user actions -----------------------------------------------
-
-#ifdef HAVE_EDITLINE
-
-static unsigned char
-on_editline_complete (EditLine *editline, int key)
-{
- (void) key;
- (void) editline;
-
- struct app_context *ctx = g_ctx;
-
- // First prepare what Readline would have normally done for us...
- const LineInfo *info_mb = el_line (editline);
- int len = info_mb->lastchar - info_mb->buffer;
- int point = info_mb->cursor - info_mb->buffer;
- char *copy = xstrndup (info_mb->buffer, len);
-
- // XXX: possibly incorrect wrt. shift state encodings
- int el_start = point, el_end = point;
- while (el_start && !strchr (WORD_BREAKING_CHARS, copy[el_start - 1]))
- el_start--;
-
- char **completions = make_completions (ctx, copy, el_start, el_end);
-
- // XXX: possibly incorrect wrt. shift state encodings
- copy[el_end] = '\0';
- int el_len = mbstowcs (NULL, copy + el_start, 0);
- free (copy);
-
- if (!completions)
- return CC_REFRESH_BEEP;
-
- // Remove the original word
- el_wdeletestr (editline, el_len);
-
- // Insert the best match instead
- el_insertstr (editline, completions[0]);
- bool only_match = !completions[1];
- for (char **p = completions; *p; p++)
- free (*p);
- free (completions);
-
- // I'm not sure if Readline's menu-complete can at all be implemented
- // with Editline. Spamming the terminal with possible completions
- // probably isn't what the user wants and we have no way of detecting
- // what the last executed handler was.
- if (!only_match)
- return CC_REFRESH_BEEP;
-
- // But if there actually is just one match, finish the word
- el_insertstr (editline, " ");
- return CC_REFRESH;
-}
-
-static unsigned char
-on_editline_return (EditLine *editline, int key)
-{
- (void) key;
- struct app_context *ctx = g_ctx;
- struct input_el *self = (struct input_el *) ctx->input;
-
- const LineInfoW *info = el_wline (editline);
- int len = info->lastchar - info->buffer;
- int point = info->cursor - info->buffer;
-
- wchar_t *line = calloc (sizeof *info->buffer, len + 1);
- memcpy (line, info->buffer, sizeof *info->buffer * len);
-
- // XXX: Editline seems to remember its position in history,
- // so it's not going to work as you'd expect it to
- if (*line)
- {
- HistEventW ev;
- history_w (self->current->history, &ev, H_ENTER, line);
- print_debug ("history: %d %ls", ev.num, ev.str);
- }
- free (line);
-
- // process_input() expects a multibyte string
- const LineInfo *info_mb = el_line (editline);
- strv_append_owned (&ctx->pending_input,
- xstrndup (info_mb->buffer, info_mb->lastchar - info_mb->buffer));
- poller_idle_set (&ctx->input_event);
-
- el_cursor (editline, len - point);
- el_wdeletestr (editline, len);
- return CC_REFRESH;
-}
-
-static void
-app_editline_init (struct input_el *self)
-{
- // el_set() leaks memory in 20150325 and other versions, we need wchar_t
- el_wset (self->editline, EL_ADDFN,
- L"send-line", L"Send line", on_editline_return);
- el_wset (self->editline, EL_ADDFN,
- L"complete", L"Complete word", on_editline_complete);
-
- struct input *input = &self->super;
- input->add_functions (input->user_data);
- bind_common_keys (g_ctx);
-
- // Move native history commands
- CALL_ (input, bind_meta, 'p', "ed-prev-history");
- CALL_ (input, bind_meta, 'n', "ed-next-history");
-
- // No, editline, it's not supposed to kill the entire line
- CALL_ (input, bind_control, 'w', "ed-delete-prev-word");
- // Just what are you doing?
- CALL_ (input, bind_control, 'u', "vi-kill-line-prev");
-
- // We need to hide the prompt and input first
- CALL_ (input, bind, "\n", "send-line");
-
- CALL_ (input, bind_control, 'i', "complete");
-
- // Source the user's defaults file
- el_source (self->editline, NULL);
-}
-
-#endif // HAVE_EDITLINE
-
-// --- Configuration loading ---------------------------------------------------
-
-static const char *g_first_time_help[] =
-{
- "",
- "\x02Welcome to degesch!",
- "",
- "To get a list of all commands, type \x02/help\x02. To obtain",
- "more information on a command or option, simply add it as",
- "a parameter, e.g. \x02/help set\x02 or \x02/help behaviour.logging\x02.",
- "",
- "To switch between buffers, press \x02"
- "F5/Ctrl-P\x02 or \x02" "F6/Ctrl-N\x02.",
- "",
- "Finally, adding a network is as simple as:",
- " - \x02/server add freenode\x02",
- " - \x02/set servers.freenode.addresses = \"chat.freenode.net\"\x02",
- " - \x02/connect freenode\x02",
- "",
- "That should be enough to get you started. Have fun!",
- ""
-};
-
-static void
-show_first_time_help (struct app_context *ctx)
-{
- for (size_t i = 0; i < N_ELEMENTS (g_first_time_help); i++)
- log_global_indent (ctx, "#m", g_first_time_help[i]);
-}
-
-const char *g_default_aliases[][2] =
-{
- { "c", "/buffer clear" }, { "close", "/buffer close" },
- { "j", "/join $*" }, { "p", "/part $*" },
- { "k", "/kick $*" }, { "kb", "/kickban $*" },
- { "m", "/msg $*" }, { "q", "/query $*" },
- { "n", "/names $*" }, { "t", "/topic $*" },
- { "w", "/who $*" }, { "wi", "/whois $*" },
- { "ww", "/whowas $*" },
-};
-
-static void
-load_default_aliases (struct app_context *ctx)
-{
- struct str_map *aliases = get_aliases_config (ctx);
- for (size_t i = 0; i < N_ELEMENTS (g_default_aliases); i++)
- {
- const char **pair = g_default_aliases[i];
- str_map_set (aliases, pair[0], config_item_string_from_cstr (pair[1]));
- }
-}
-
-static void
-load_configuration (struct app_context *ctx)
-{
- // In theory, we could ensure that only one instance is running by locking
- // the configuration file and ensuring here that it exists. This is
- // however brittle, as it may be unlinked without the application noticing.
-
- struct config_item *root = NULL;
- struct error *e = NULL;
-
- char *filename = resolve_filename
- (PROGRAM_NAME ".conf", resolve_relative_config_filename);
- if (filename)
- root = config_read_from_file (filename, &e);
- else
- log_global_error (ctx, "Configuration file not found");
- free (filename);
-
- if (e)
- {
- log_global_error (ctx, "Cannot load configuration: #s", e->message);
- log_global_error (ctx,
- "Please either fix the configuration file or remove it");
- error_free (e);
- exit (EXIT_FAILURE);
- }
-
- if (root)
- {
- config_load (&ctx->config, root);
- log_global_status (ctx, "Configuration loaded");
- }
- else
- {
- show_first_time_help (ctx);
- load_default_aliases (ctx);
- }
-}
-
-// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-static void
-load_servers (struct app_context *ctx)
-{
- struct str_map_iter iter = str_map_iter_make (get_servers_config (ctx));
-
- struct config_item *subtree;
- while ((subtree = str_map_iter_next (&iter)))
- {
- const char *name = iter.link->key;
- const char *err;
- if (subtree->type != CONFIG_ITEM_OBJECT)
- log_global_error (ctx, "Error in configuration: "
- "ignoring server `#s' as it's not an object", name);
- else if ((err = check_server_name_for_addition (ctx, name)))
- log_global_error (ctx, "Cannot load server `#s': #s", name, err);
- else
- server_add (ctx, name, subtree);
- }
-}
-
-// --- Signals -----------------------------------------------------------------
-
-static int g_signal_pipe[2]; ///< A pipe used to signal... signals
-
-/// Program termination has been requested by a signal
-static volatile sig_atomic_t g_termination_requested;
-/// The window has changed in size
-static volatile sig_atomic_t g_winch_received;
-
-static void
-postpone_signal_handling (char id)
-{
- int original_errno = errno;
- if (write (g_signal_pipe[1], &id, 1) == -1)
- soft_assert (errno == EAGAIN);
- errno = original_errno;
-}
-
-static void
-signal_superhandler (int signum)
-{
- switch (signum)
- {
- case SIGWINCH:
- g_winch_received = true;
- postpone_signal_handling ('w');
- break;
- case SIGINT:
- case SIGTERM:
- g_termination_requested = true;
- postpone_signal_handling ('t');
- break;
- case SIGCHLD:
- postpone_signal_handling ('c');
- break;
- case SIGTSTP:
- postpone_signal_handling ('s');
- break;
- default:
- hard_assert (!"unhandled signal");
- }
-}
-
-static void
-setup_signal_handlers (void)
-{
- if (pipe (g_signal_pipe) == -1)
- exit_fatal ("%s: %s", "pipe", strerror (errno));
-
- set_cloexec (g_signal_pipe[0]);
- set_cloexec (g_signal_pipe[1]);
-
- // So that the pipe cannot overflow; it would make write() block within
- // the signal handler, which is something we really don't want to happen.
- // The same holds true for read().
- set_blocking (g_signal_pipe[0], false);
- set_blocking (g_signal_pipe[1], false);
-
- signal (SIGPIPE, SIG_IGN);
-
- // So that we can write to the terminal while we're running a backlog
- // helper. This is also inherited by the child so that it doesn't stop
- // when it calls tcsetpgrp().
- signal (SIGTTOU, SIG_IGN);
-
- struct sigaction sa;
- sa.sa_flags = SA_RESTART;
- sa.sa_handler = signal_superhandler;
- sigemptyset (&sa.sa_mask);
-
- if (sigaction (SIGWINCH, &sa, NULL) == -1
- || sigaction (SIGINT, &sa, NULL) == -1
- || sigaction (SIGTERM, &sa, NULL) == -1
- || sigaction (SIGTSTP, &sa, NULL) == -1
- || sigaction (SIGCHLD, &sa, NULL) == -1)
- exit_fatal ("sigaction: %s", strerror (errno));
-}
-
-// --- I/O event handlers ------------------------------------------------------
-
-static bool
-try_reap_child (struct app_context *ctx)
-{
- int status;
- pid_t zombie = waitpid (-1, &status, WNOHANG | WUNTRACED);
-
- if (zombie == -1)
- {
- if (errno == ECHILD) return false;
- if (errno == EINTR) return true;
- exit_fatal ("%s: %s", "waitpid", strerror (errno));
- }
- if (!zombie)
- return false;
-
- if (WIFSTOPPED (status))
- {
- // We could also send SIGCONT but what's the point
- print_debug ("a child has been stopped, killing its process group");
- kill (-zombie, SIGKILL);
- return true;
- }
-
- if (ctx->running_backlog_helper)
- ctx->running_backlog_helper = false;
- else if (!ctx->running_editor)
- {
- log_global_debug (ctx, "An unknown child has died");
- return true;
- }
-
- hard_assert (tcsetpgrp (STDOUT_FILENO, getpgid (0)) != -1);
- resume_terminal (ctx);
-
- if (WIFSIGNALED (status))
- log_global_error (ctx,
- "Child died from signal #d", WTERMSIG (status));
- else if (WIFEXITED (status) && WEXITSTATUS (status) != 0)
- log_global_error (ctx,
- "Child returned status #d", WEXITSTATUS (status));
- else if (ctx->running_editor)
- input_editor_process (ctx);
-
- if (ctx->running_editor)
- input_editor_cleanup (ctx);
- return true;
-}
-
-static void
-on_signal_pipe_readable (const struct pollfd *fd, struct app_context *ctx)
-{
- char id = 0;
- (void) read (fd->fd, &id, 1);
-
- // Stop ourselves cleanly, even if it makes little sense to do this
- if (id == 's')
- {
- suspend_terminal (ctx);
- kill (getpid (), SIGSTOP);
- g_winch_received = true;
- resume_terminal (ctx);
- }
-
- // Reap all dead children (since the signal pipe may overflow etc. we run
- // waitpid() in a loop to return all the zombies it knows about).
- while (try_reap_child (ctx))
- ;
-
- if (g_termination_requested)
- {
- g_termination_requested = false;
- request_quit (ctx, NULL);
- }
- if (g_winch_received)
- {
- g_winch_received = false;
- redraw_screen (ctx);
- }
-}
-
-static void
-process_mirc_escape (const struct pollfd *fd, struct app_context *ctx)
-{
- // There's no other way with libedit, as both el_getc() in a function
- // handler and CC_ARGHACK would block execution
- struct str *buf = &ctx->input_buffer;
- str_reserve (buf, 1);
- if (read (fd->fd, buf->str + buf->len, 1) != 1)
- goto error;
- buf->str[++buf->len] = '\0';
-
- // XXX: I think this should be global and shared with Readline/libedit
- mbstate_t state;
- memset (&state, 0, sizeof state);
-
- size_t len = mbrlen (buf->str, buf->len, &state);
-
- // Illegal sequence
- if (len == (size_t) -1)
- goto error;
-
- // Incomplete multibyte character
- if (len == (size_t) -2)
- return;
-
- if (buf->len != 1)
- goto error;
- switch (buf->str[0])
- {
- case 'b' ^ 96:
- case 'b': CALL_ (ctx->input, insert, "\x02"); break;
- case 'c': CALL_ (ctx->input, insert, "\x03"); break;
- case 'i' ^ 96:
- case 'i':
- case ']': CALL_ (ctx->input, insert, "\x1d"); break;
- case 'x' ^ 96:
- case 'x':
- case '^': CALL_ (ctx->input, insert, "\x1e"); break;
- case 'u' ^ 96:
- case 'u':
- case '_': CALL_ (ctx->input, insert, "\x1f"); break;
- case 'v': CALL_ (ctx->input, insert, "\x16"); break;
- case 'o': CALL_ (ctx->input, insert, "\x0f"); break;
-
- default:
- goto error;
- }
- goto done;
-
-error:
- CALL (ctx->input, ding);
-done:
- str_reset (buf);
- ctx->awaiting_mirc_escape = false;
-}
-
-#define BRACKETED_PASTE_LIMIT 102400 ///< How much text can be pasted
-
-static void
-process_bracketed_paste (const struct pollfd *fd, struct app_context *ctx)
-{
- struct str *buf = &ctx->input_buffer;
- str_reserve (buf, 1);
- if (read (fd->fd, buf->str + buf->len, 1) != 1)
- goto error;
- buf->str[++buf->len] = '\0';
-
- static const char stop_mark[] = "\x1b[201~";
- static const size_t stop_mark_len = sizeof stop_mark - 1;
- if (buf->len < stop_mark_len)
- return;
-
- size_t text_len = buf->len - stop_mark_len;
- if (memcmp (buf->str + text_len, stop_mark, stop_mark_len))
- return;
-
- // Avoid endless flooding of the buffer
- if (text_len > BRACKETED_PASTE_LIMIT)
- log_global_error (ctx, "Paste trimmed to #d bytes",
- (int) (text_len = BRACKETED_PASTE_LIMIT));
-
- buf->str[text_len] = '\0';
- if (CALL_ (ctx->input, insert, buf->str))
- goto done;
-
-error:
- CALL (ctx->input, ding);
- log_global_error (ctx, "Paste failed");
-done:
- str_reset (buf);
- ctx->in_bracketed_paste = false;
-}
-
-static void
-reset_autoaway (struct app_context *ctx)
-{
- // Stop the last one if it's been disabled altogether in the meantime
- poller_timer_reset (&ctx->autoaway_tmr);
-
- // Unset any automated statuses that are active right at this moment
- struct str_map_iter iter = str_map_iter_make (&ctx->servers);
- struct server *s;
- while ((s = str_map_iter_next (&iter)))
- {
- if (s->autoaway_active
- && s->irc_user
- && s->irc_user->away)
- irc_send (s, "AWAY");
-
- s->autoaway_active = false;
- }
-
- // And potentially start a new auto-away timer
- int64_t delay = get_config_integer
- (ctx->config.root, "behaviour.autoaway_delay");
- if (delay)
- poller_timer_set (&ctx->autoaway_tmr, delay * 1000);
-}
-
-static void
-on_autoaway_timer (struct app_context *ctx)
-{
- // An empty message would unset any away status, so let's ignore that
- const char *message = get_config_string
- (ctx->config.root, "behaviour.autoaway_message");
- if (!message || !*message)
- return;
-
- struct str_map_iter iter = str_map_iter_make (&ctx->servers);
- struct server *s;
- while ((s = str_map_iter_next (&iter)))
- {
- // If the user has already been marked as away,
- // don't override his current away status
- if (s->irc_user
- && s->irc_user->away)
- continue;
-
- irc_send (s, "AWAY :%s", message);
- s->autoaway_active = true;
- }
-}
-
-static void
-on_tty_readable (const struct pollfd *fd, struct app_context *ctx)
-{
- if (fd->revents & ~(POLLIN | POLLHUP | POLLERR))
- print_debug ("fd %d: unexpected revents: %d", fd->fd, fd->revents);
-
- if (ctx->awaiting_mirc_escape)
- process_mirc_escape (fd, ctx);
- else if (ctx->in_bracketed_paste)
- process_bracketed_paste (fd, ctx);
- else if (!ctx->quitting)
- CALL (ctx->input, on_tty_readable);
-
- // User activity detected, stop current auto-away and start anew;
- // since they might have just changed the settings, do this last
- reset_autoaway (ctx);
-}
-
-static void
-rearm_flush_timer (struct app_context *ctx)
-{
- poller_timer_set (&ctx->flush_timer, 60 * 1000);
-}
-
-static void
-on_flush_timer (struct app_context *ctx)
-{
- // I guess we don't need to do anything more complicated
- fflush (NULL);
-
- // It would be a bit problematic to handle it properly, so do this at least
- LIST_FOR_EACH (struct buffer, buffer, ctx->buffers)
- {
- if (!buffer->log_file || !ferror (buffer->log_file))
- continue;
-
- // Might be a transient error such as running out of disk space,
- // keep notifying of the problem until it disappears
- clearerr (buffer->log_file);
- log_global (ctx, BUFFER_LINE_ERROR | BUFFER_LINE_SKIP_FILE,
- "Log write failure detected for #s", buffer->name);
- }
-
-#ifdef LOMEM
- // Lua should normally be reasonable and collect garbage when needed,
- // though we can try to push it. This is a reasonable place.
- LIST_FOR_EACH (struct plugin, iter, ctx->plugins)
- if (iter->vtable->gc)
- iter->vtable->gc (iter);
-#endif // LOMEM
-
- rearm_flush_timer (ctx);
-}
-
-static void
-rearm_date_change_timer (struct app_context *ctx)
-{
- struct tm tm_;
- const time_t now = time (NULL);
- if (!soft_assert (localtime_r (&now, &tm_)))
- return;
-
- tm_.tm_sec = tm_.tm_min = tm_.tm_hour = 0;
- tm_.tm_mday++;
- tm_.tm_isdst = -1;
-
- const time_t midnight = mktime (&tm_);
- if (!soft_assert (midnight != (time_t) -1))
- return;
- poller_timer_set (&ctx->date_chg_tmr, (midnight - now) * 1000);
-}
-
-static void
-on_date_change_timer (struct app_context *ctx)
-{
- if (ctx->terminal_suspended <= 0)
- {
- CALL (ctx->input, hide);
- buffer_update_time (ctx, time (NULL), stdout, 0);
- CALL (ctx->input, show);
- }
- rearm_date_change_timer (ctx);
-}
-
-static void
-on_pending_input (struct app_context *ctx)
-{
- poller_idle_reset (&ctx->input_event);
- for (size_t i = 0; i < ctx->pending_input.len; i++)
- process_input (ctx, ctx->pending_input.vector[i]);
- strv_reset (&ctx->pending_input);
-}
-
-static void
-init_poller_events (struct app_context *ctx)
-{
- ctx->signal_event = poller_fd_make (&ctx->poller, g_signal_pipe[0]);
- ctx->signal_event.dispatcher = (poller_fd_fn) on_signal_pipe_readable;
- ctx->signal_event.user_data = ctx;
- poller_fd_set (&ctx->signal_event, POLLIN);
-
- ctx->tty_event = poller_fd_make (&ctx->poller, STDIN_FILENO);
- ctx->tty_event.dispatcher = (poller_fd_fn) on_tty_readable;
- ctx->tty_event.user_data = ctx;
- poller_fd_set (&ctx->tty_event, POLLIN);
-
- ctx->flush_timer = poller_timer_make (&ctx->poller);
- ctx->flush_timer.dispatcher = (poller_timer_fn) on_flush_timer;
- ctx->flush_timer.user_data = ctx;
- rearm_flush_timer (ctx);
-
- ctx->date_chg_tmr = poller_timer_make (&ctx->poller);
- ctx->date_chg_tmr.dispatcher = (poller_timer_fn) on_date_change_timer;
- ctx->date_chg_tmr.user_data = ctx;
- rearm_date_change_timer (ctx);
-
- ctx->autoaway_tmr = poller_timer_make (&ctx->poller);
- ctx->autoaway_tmr.dispatcher = (poller_timer_fn) on_autoaway_timer;
- ctx->autoaway_tmr.user_data = ctx;
-
- ctx->prompt_event = poller_idle_make (&ctx->poller);
- ctx->prompt_event.dispatcher = (poller_idle_fn) on_refresh_prompt;
- ctx->prompt_event.user_data = ctx;
-
- ctx->input_event = poller_idle_make (&ctx->poller);
- ctx->input_event.dispatcher = (poller_idle_fn) on_pending_input;
- ctx->input_event.user_data = ctx;
-}
-
-// --- Tests -------------------------------------------------------------------
-
-// The application is quite monolithic and can only be partially unit-tested.
-// Locale-, terminal- and filesystem-dependent tests are also somewhat tricky.
-
-#ifdef TESTING
-
-static struct config_schema g_config_test[] =
-{
- { .name = "foo", .type = CONFIG_ITEM_BOOLEAN, .default_ = "off" },
- { .name = "bar", .type = CONFIG_ITEM_INTEGER, .default_ = "1" },
- { .name = "foobar", .type = CONFIG_ITEM_STRING, .default_ = "\"x\\x01\"" },
- {}
-};
-
-static void
-test_config (void)
-{
- struct config_item *foo = config_item_object ();
- config_schema_apply_to_object (g_config_test, foo, NULL);
- struct config_item *root = config_item_object ();
- str_map_set (&root->value.object, "top", foo);
-
- struct strv v = strv_make ();
- dump_matching_options (root, "*foo*", &v);
- hard_assert (v.len == 2);
- hard_assert (!strcmp (v.vector[0], "top.foo = off"));
- hard_assert (!strcmp (v.vector[1], "top.foobar = \"x\\x01\""));
- strv_free (&v);
-
- config_item_destroy (root);
-}
-
-// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-static void
-test_aliases (void)
-{
- struct strv v = strv_make ();
- expand_alias_definition ("/foo; /bar $* $$$;;;$1$2$3$4", "foo bar baz", &v);
- hard_assert (v.len == 4);
- hard_assert (!strcmp (v.vector[0], "/foo"));
- hard_assert (!strcmp (v.vector[1], " /bar foo bar baz $;"));
- hard_assert (!strcmp (v.vector[2], ""));
- hard_assert (!strcmp (v.vector[3], "foobarbaz"));
- strv_free (&v);
-}
-
-static void
-test_wrapping (void)
-{
- static const char *message = " foo bar foobar fóóbárbáz";
- static const char *split[] =
- { " foo", "bar", "foob", "ar", "fó", "ób", "árb", "áz" };
-
- struct strv v = strv_make ();
- hard_assert (wrap_message (message, 4, &v, NULL));
- hard_assert (v.len == N_ELEMENTS (split));
- for (size_t i = 0; i < N_ELEMENTS (split); i++)
- hard_assert (!strcmp (v.vector[i], split[i]));
- strv_free (&v);
-}
-
-static void
-test_utf8 (void)
-{
- static const char *a[] = { "fřoo", "Fřooř", "fřOOŘ" };
- hard_assert (utf8_common_prefix (a, N_ELEMENTS (a)) == 5);
-
- char *cut_off = xstrdup ("ё\xD0");
- irc_sanitize_cut_off_utf8 (&cut_off);
- hard_assert (!strcmp (cut_off, "ё\xEF\xBF\xBD"));
- free (cut_off);
-}
-
-int
-main (int argc, char *argv[])
-{
- struct test test;
- test_init (&test, argc, argv);
- test_add_simple (&test, "/config", NULL, test_config);
- test_add_simple (&test, "/aliases", NULL, test_aliases);
- test_add_simple (&test, "/wrapping", NULL, test_wrapping);
- test_add_simple (&test, "/utf8", NULL, test_utf8);
- return test_run (&test);
-}
-
-#define main main_shadowed
-#endif // TESTING
-
-// --- Main program ------------------------------------------------------------
-
-static const char *g_logo[] =
-{
- " __ __ ",
- " __/ /___________________/ / ",
- " / / , / / , / __/ __/ _ \\ ",
- "/ / / __/ / / __/_ / /_/ // / ",
- "\\__/\\__/_ /\\__/___/\\__/_//_/ " PROGRAM_VERSION,
- " /___/",
- ""
-};
-
-static void
-show_logo (struct app_context *ctx)
-{
- for (size_t i = 0; i < N_ELEMENTS (g_logo); i++)
- log_global_indent (ctx, "#m", g_logo[i]);
-}
-
-static void
-format_input_and_die (struct app_context *ctx)
-{
- char buf[513];
- while (fgets (buf, sizeof buf, stdin))
- {
- struct formatter f = formatter_make (ctx, NULL);
- formatter_add (&f, "#m", buf);
- formatter_flush (&f, stdout, FLUSH_OPT_NOWRAP);
- formatter_free (&f);
- }
- exit (EXIT_SUCCESS);
-}
-
-int
-main (int argc, char *argv[])
-{
- // We include a generated file from kike including this array we don't use;
- // let's just keep it there and silence the compiler warning instead
- (void) g_default_replies;
-
- static const struct opt opts[] =
- {
- { 'h', "help", NULL, 0, "display this help and exit" },
- { 'V', "version", NULL, 0, "output version information and exit" },
- // This is mostly intended for previewing formatted MOTD files
- { 'f', "format", NULL, OPT_LONG_ONLY, "format IRC text from stdin" },
- { 0, NULL, NULL, 0, NULL }
- };
-
- struct opt_handler oh =
- opt_handler_make (argc, argv, opts, NULL, "Terminal-based IRC client.");
- bool format_mode = false;
-
- int c;
- while ((c = opt_handler_get (&oh)) != -1)
- switch (c)
- {
- case 'h':
- opt_handler_usage (&oh, stdout);
- exit (EXIT_SUCCESS);
- case 'V':
- printf (PROGRAM_NAME " " PROGRAM_VERSION "\n");
- exit (EXIT_SUCCESS);
- case 'f':
- format_mode = true;
- break;
- default:
- print_error ("wrong options");
- opt_handler_usage (&oh, stderr);
- exit (EXIT_FAILURE);
- }
- if (optind != argc)
- {
- opt_handler_usage (&oh, stderr);
- exit (EXIT_FAILURE);
- }
- opt_handler_free (&oh);
-
- // We only need to convert to and from the terminal encoding
- setlocale (LC_CTYPE, "");
-
- struct app_context ctx;
- app_context_init (&ctx);
- g_ctx = &ctx;
-
- init_openssl ();
-
- // Bootstrap configuration, so that we can access schema items at all
- register_config_modules (&ctx);
- config_load (&ctx.config, config_item_object ());
-
- // The following part is a bit brittle because of interdependencies
- init_colors (&ctx);
- if (format_mode) format_input_and_die (&ctx);
- init_global_buffer (&ctx);
- show_logo (&ctx);
- setup_signal_handlers ();
- init_poller_events (&ctx);
- load_configuration (&ctx);
-
- // At this moment we can safely call any "on_change" callbacks
- config_schema_call_changed (ctx.config.root);
-
- // Initialize input so that we can switch to new buffers
- on_refresh_prompt (&ctx);
- ctx.input->add_functions = input_add_functions;
- CALL_ (ctx.input, start, argv[0]);
- toggle_bracketed_paste (true);
- reset_autoaway (&ctx);
-
- // Finally, we juice the configuration for some servers to create
- load_plugins (&ctx);
- load_servers (&ctx);
-
- ctx.polling = true;
- while (ctx.polling)
- poller_run (&ctx.poller);
-
- CALL (ctx.input, stop);
-
- if (get_config_boolean (ctx.config.root, "behaviour.save_on_quit"))
- save_configuration (&ctx);
-
- app_context_free (&ctx);
- toggle_bracketed_paste (false);
- free_terminal ();
- return EXIT_SUCCESS;
-}
diff --git a/degesch.png b/degesch.png
deleted file mode 100644
index 068ddb6..0000000
Binary files a/degesch.png and /dev/null differ
diff --git a/kike-gen-replies.sh b/kike-gen-replies.sh
deleted file mode 100755
index 004da2b..0000000
--- a/kike-gen-replies.sh
+++ /dev/null
@@ -1,28 +0,0 @@
-#!/bin/sh
-LC_ALL=C exec awk '
- BEGIN {
- # The message catalog is a by-product
- msg = "kike.msg"
- print "$quote \"" > msg;
- print "$set 1" > msg;
- }
- /^[0-9]+ *IRC_(ERR|RPL)_[A-Z]+ *".*"$/ {
- match($0, /".*"/);
- ids[$1] = $2;
- texts[$2] = substr($0, RSTART, RLENGTH);
- print $1 " " texts[$2] > msg
- }
- END {
- printf("enum\n{")
- for (i in ids) {
- if (seen_first)
- printf(",")
- seen_first = 1
- printf("\n\t%s = %s", ids[i], i)
- }
- print "\n};\n"
- print "static const char *g_default_replies[] =\n{"
- for (i in ids)
- print "\t[" ids[i] "] = " texts[ids[i]] ","
- print "};"
- }'
diff --git a/kike-replies b/kike-replies
deleted file mode 100644
index fc8d6df..0000000
--- a/kike-replies
+++ /dev/null
@@ -1,93 +0,0 @@
-1 IRC_RPL_WELCOME ":Welcome to the Internet Relay Network %s!%s@%s"
-2 IRC_RPL_YOURHOST ":Your host is %s, running version %s"
-3 IRC_RPL_CREATED ":This server was created %s"
-4 IRC_RPL_MYINFO "%s %s %s %s"
-5 IRC_RPL_ISUPPORT "%s :are supported by this server"
-211 IRC_RPL_STATSLINKINFO "%s %zu %zu %zu %zu %zu %lld"
-212 IRC_RPL_STATSCOMMANDS "%s %zu %zu %zu"
-219 IRC_RPL_ENDOFSTATS "%c :End of STATS report"
-221 IRC_RPL_UMODEIS "+%s"
-242 IRC_RPL_STATSUPTIME ":Server Up %d days %d:%02d:%02d"
-251 IRC_RPL_LUSERCLIENT ":There are %d users and %d services on %d servers"
-252 IRC_RPL_LUSEROP "%d :operator(s) online"
-253 IRC_RPL_LUSERUNKNOWN "%d :unknown connection(s)"
-254 IRC_RPL_LUSERCHANNELS "%d :channels formed"
-255 IRC_RPL_LUSERME ":I have %d clients and %d servers"
-301 IRC_RPL_AWAY "%s :%s"
-302 IRC_RPL_USERHOST ":%s"
-303 IRC_RPL_ISON ":%s"
-305 IRC_RPL_UNAWAY ":You are no longer marked as being away"
-306 IRC_RPL_NOWAWAY ":You have been marked as being away"
-311 IRC_RPL_WHOISUSER "%s %s %s * :%s"
-312 IRC_RPL_WHOISSERVER "%s %s :%s"
-313 IRC_RPL_WHOISOPERATOR "%s :is an IRC operator"
-314 IRC_RPL_WHOWASUSER "%s %s %s * :%s"
-315 IRC_RPL_ENDOFWHO "%s :End of WHO list"
-317 IRC_RPL_WHOISIDLE "%s %d :seconds idle"
-318 IRC_RPL_ENDOFWHOIS "%s :End of WHOIS list"
-319 IRC_RPL_WHOISCHANNELS "%s :%s"
-322 IRC_RPL_LIST "%s %d :%s"
-323 IRC_RPL_LISTEND ":End of LIST"
-324 IRC_RPL_CHANNELMODEIS "%s +%s"
-329 IRC_RPL_CREATIONTIME "%s %lld"
-331 IRC_RPL_NOTOPIC "%s :No topic is set"
-332 IRC_RPL_TOPIC "%s :%s"
-333 IRC_RPL_TOPICWHOTIME "%s %s %lld"
-341 IRC_RPL_INVITING "%s %s"
-346 IRC_RPL_INVITELIST "%s %s"
-347 IRC_RPL_ENDOFINVITELIST "%s :End of channel invite list"
-348 IRC_RPL_EXCEPTLIST "%s %s"
-349 IRC_RPL_ENDOFEXCEPTLIST "%s :End of channel exception list"
-351 IRC_RPL_VERSION "%s.%d %s :%s"
-352 IRC_RPL_WHOREPLY "%s %s %s %s %s %s :%d %s"
-353 IRC_RPL_NAMREPLY "%c %s :%s"
-364 IRC_RPL_LINKS "%s %s :%d %s"
-365 IRC_RPL_ENDOFLINKS "%s :End of LINKS list"
-366 IRC_RPL_ENDOFNAMES "%s :End of NAMES list"
-367 IRC_RPL_BANLIST "%s %s"
-368 IRC_RPL_ENDOFBANLIST "%s :End of channel ban list"
-369 IRC_RPL_ENDOFWHOWAS "%s :End of WHOWAS"
-372 IRC_RPL_MOTD ":- %s"
-375 IRC_RPL_MOTDSTART ":- %s Message of the day - "
-376 IRC_RPL_ENDOFMOTD ":End of MOTD command"
-391 IRC_RPL_TIME "%s :%s"
-401 IRC_ERR_NOSUCHNICK "%s :No such nick/channel"
-402 IRC_ERR_NOSUCHSERVER "%s :No such server"
-403 IRC_ERR_NOSUCHCHANNEL "%s :No such channel"
-404 IRC_ERR_CANNOTSENDTOCHAN "%s :Cannot send to channel"
-406 IRC_ERR_WASNOSUCHNICK "%s :There was no such nickname"
-409 IRC_ERR_NOORIGIN ":No origin specified"
-410 IRC_ERR_INVALIDCAPCMD "%s :%s"
-411 IRC_ERR_NORECIPIENT ":No recipient given (%s)"
-412 IRC_ERR_NOTEXTTOSEND ":No text to send"
-421 IRC_ERR_UNKNOWNCOMMAND "%s: Unknown command"
-422 IRC_ERR_NOMOTD ":MOTD File is missing"
-423 IRC_ERR_NOADMININFO "%s :No administrative info available"
-431 IRC_ERR_NONICKNAMEGIVEN ":No nickname given"
-432 IRC_ERR_ERRONEOUSNICKNAME "%s :Erroneous nickname"
-433 IRC_ERR_NICKNAMEINUSE "%s :Nickname is already in use"
-441 IRC_ERR_USERNOTINCHANNEL "%s %s :They aren't on that channel"
-442 IRC_ERR_NOTONCHANNEL "%s :You're not on that channel"
-443 IRC_ERR_USERONCHANNEL "%s %s :is already on channel"
-445 IRC_ERR_SUMMONDISABLED ":SUMMON has been disabled"
-446 IRC_ERR_USERSDISABLED ":USERS has been disabled"
-451 IRC_ERR_NOTREGISTERED ":You have not registered"
-461 IRC_ERR_NEEDMOREPARAMS "%s :Not enough parameters"
-462 IRC_ERR_ALREADYREGISTERED ":Unauthorized command (already registered)"
-467 IRC_ERR_KEYSET "%s :Channel key already set"
-471 IRC_ERR_CHANNELISFULL "%s :Cannot join channel (+l)"
-472 IRC_ERR_UNKNOWNMODE "%c :is unknown mode char to me for %s"
-473 IRC_ERR_INVITEONLYCHAN "%s :Cannot join channel (+i)"
-474 IRC_ERR_BANNEDFROMCHAN "%s :Cannot join channel (+b)"
-475 IRC_ERR_BADCHANNELKEY "%s :Cannot join channel (+k)"
-476 IRC_ERR_BADCHANMASK "%s :Bad Channel Mask"
-481 IRC_ERR_NOPRIVILEGES ":Permission Denied- You're not an IRC operator"
-482 IRC_ERR_CHANOPRIVSNEEDED "%s :You're not channel operator"
-501 IRC_ERR_UMODEUNKNOWNFLAG ":Unknown MODE flag"
-502 IRC_ERR_USERSDONTMATCH ":Cannot change mode for other users"
-902 IRC_ERR_NICKLOCKED ":You must use a nick assigned to you"
-903 IRC_RPL_SASLSUCCESS ":SASL authentication successful"
-904 IRC_ERR_SASLFAIL ":SASL authentication failed"
-905 IRC_ERR_SASLTOOLONG ":SASL message too long"
-906 IRC_ERR_SASLABORTED ":SASL authentication aborted"
-907 IRC_ERR_SASLALREADY ":You have already authenticated using SASL"
diff --git a/kike.adoc b/kike.adoc
deleted file mode 100644
index 0807c78..0000000
--- a/kike.adoc
+++ /dev/null
@@ -1,53 +0,0 @@
-kike(1)
-=======
-:doctype: manpage
-:manmanual: uirc3 Manual
-:mansource: uirc3 {release-version}
-
-Name
-----
-kike - IRC daemon
-
-Synopsis
---------
-*kike* [_OPTION_]...
-
-Description
------------
-*kike* is a basic IRC daemon for single-server networks, suitable for testing
-and private use. When run without a configuration file, it will start listening
-on the standard port 6667 and the "any" address.
-
-Options
--------
-*-d*, *--debug*::
- Do not daemonize, print more information on the standard error stream
- to help debug various issues.
-
-*-h*, *--help*::
- Display a help message and exit.
-
-*-V*, *--version*::
- Output version information and exit.
-
-*--write-default-cfg*[**=**__PATH__]::
- Write a configuration file with defaults, show its path and exit.
-+
-The file will be appropriately commented.
-+
-When no _PATH_ is specified, it will be created in the user's home directory,
-contrary to what you might expect from a server.
-
-Files
------
-*kike* follows the XDG Base Directory Specification.
-
-_~/.config/kike/kike.conf_::
-_/etc/xdg/kike/kike.conf_::
- The daemon's configuration file. Use the *--write-default-cfg* option
- to create a new one for editing.
-
-Reporting bugs
---------------
-Use https://git.janouch.name/p/uirc3 to report bugs, request features,
-or submit pull requests.
diff --git a/kike.c b/kike.c
deleted file mode 100644
index e805e00..0000000
--- a/kike.c
+++ /dev/null
@@ -1,4079 +0,0 @@
-/*
- * kike.c: an IRC daemon
- *
- * Copyright (c) 2014 - 2020, 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.
- *
- */
-
-#include "config.h"
-#define PROGRAM_NAME "kike"
-
-#define WANT_SYSLOG_LOGGING
-#include "common.c"
-#include "kike-replies.c"
-#include
-
-enum { PIPE_READ, PIPE_WRITE };
-
-// FIXME: don't use time_t to compute time deltas
-
-// --- Configuration (application-specific) ------------------------------------
-
-// Just get rid of the crappiest ciphers available by default
-#define DEFAULT_CIPHERS "DEFAULT:!MEDIUM:!LOW"
-
-static struct simple_config_item g_config_table[] =
-{
- { "pid_file", NULL, "Path or name of the PID file" },
- { "server_name", NULL, "Server name" },
- { "server_info", "My server", "Brief server description" },
- { "motd", NULL, "MOTD filename" },
- { "catalog", NULL, "catgets localization catalog" },
-
- { "bind_host", NULL, "Address of the IRC server" },
- { "bind_port", "6667", "Port of the IRC server" },
- { "tls_cert", NULL, "Server TLS certificate (PEM)" },
- { "tls_key", NULL, "Server TLS private key (PEM)" },
- { "tls_ciphers", DEFAULT_CIPHERS, "OpenSSL cipher list" },
-
- { "operators", NULL, "IRCop TLS client cert. SHA-1 fingerprints" },
-
- { "max_connections", "0", "Global connection limit" },
- { "ping_interval", "180", "Interval between PINGs (sec)" },
- { NULL, NULL, NULL }
-};
-
-// --- Signals -----------------------------------------------------------------
-
-static int g_signal_pipe[2]; ///< A pipe used to signal... signals
-
-/// Program termination has been requested by a signal
-static volatile sig_atomic_t g_termination_requested;
-
-static void
-sigterm_handler (int signum)
-{
- (void) signum;
-
- g_termination_requested = true;
-
- int original_errno = errno;
- if (write (g_signal_pipe[1], "t", 1) == -1)
- soft_assert (errno == EAGAIN);
- errno = original_errno;
-}
-
-static void
-setup_signal_handlers (void)
-{
- if (pipe (g_signal_pipe) == -1)
- exit_fatal ("%s: %s", "pipe", strerror (errno));
-
- set_cloexec (g_signal_pipe[0]);
- set_cloexec (g_signal_pipe[1]);
-
- // So that the pipe cannot overflow; it would make write() block within
- // the signal handler, which is something we really don't want to happen.
- // The same holds true for read().
- set_blocking (g_signal_pipe[0], false);
- set_blocking (g_signal_pipe[1], false);
-
- signal (SIGPIPE, SIG_IGN);
-
- struct sigaction sa;
- sa.sa_flags = SA_RESTART;
- sigemptyset (&sa.sa_mask);
- sa.sa_handler = sigterm_handler;
- if (sigaction (SIGINT, &sa, NULL) == -1
- || sigaction (SIGTERM, &sa, NULL) == -1)
- exit_fatal ("%s: %s", "sigaction", strerror (errno));
-}
-
-// --- Rate limiter ------------------------------------------------------------
-
-struct flood_detector
-{
- unsigned interval; ///< Interval for the limit
- unsigned limit; ///< Maximum number of events allowed
-
- time_t *timestamps; ///< Timestamps of last events
- unsigned pos; ///< Index of the oldest event
-};
-
-static void
-flood_detector_init (struct flood_detector *self,
- unsigned interval, unsigned limit)
-{
- self->interval = interval;
- self->limit = limit;
- self->timestamps = xcalloc (limit + 1, sizeof *self->timestamps);
- self->pos = 0;
-}
-
-static void
-flood_detector_free (struct flood_detector *self)
-{
- free (self->timestamps);
-}
-
-static bool
-flood_detector_check (struct flood_detector *self)
-{
- time_t now = time (NULL);
- self->timestamps[self->pos++] = now;
- if (self->pos > self->limit)
- self->pos = 0;
-
- time_t begin = now - self->interval;
- size_t count = 0;
- for (size_t i = 0; i <= self->limit; i++)
- if (self->timestamps[i] >= begin)
- count++;
- return count <= self->limit;
-}
-
-// --- IRC token validation ----------------------------------------------------
-
-// Use the enum only if applicable and a simple boolean isn't sufficient.
-
-enum validation_result
-{
- VALIDATION_OK,
- VALIDATION_ERROR_EMPTY,
- VALIDATION_ERROR_TOO_LONG,
- VALIDATION_ERROR_INVALID
-};
-
-// Everything as per RFC 2812
-#define IRC_MAX_NICKNAME 9
-#define IRC_MAX_HOSTNAME 63
-#define IRC_MAX_CHANNEL_NAME 50
-#define IRC_MAX_MESSAGE_LENGTH 510
-
-static bool
-irc_regex_match (const char *regex, const char *s)
-{
- static struct str_map cache;
- static bool initialized;
-
- if (!initialized)
- {
- cache = regex_cache_make ();
- initialized = true;
- }
-
- struct error *e = NULL;
- bool result = regex_cache_match (&cache, regex,
- REG_EXTENDED | REG_NOSUB, s, &e);
- hard_assert (!e);
- return result;
-}
-
-static const char *
-irc_validate_to_str (enum validation_result result)
-{
- switch (result)
- {
- case VALIDATION_OK: return "success";
- case VALIDATION_ERROR_EMPTY: return "the value is empty";
- case VALIDATION_ERROR_INVALID: return "invalid format";
- case VALIDATION_ERROR_TOO_LONG: return "the value is too long";
- default: abort ();
- }
-}
-
-// Anything to keep it as short as possible
-// "shortname" from RFC 2812 doesn't work how its author thought it would.
-#define SN "[0-9A-Za-z](-*[0-9A-Za-z])*"
-#define N4 "[0-9]{1,3}"
-#define N6 "[0-9ABCDEFabcdef]{1,}"
-
-#define LE "A-Za-z"
-#define SP "][\\\\`_^{|}"
-
-static enum validation_result
-irc_validate_hostname (const char *hostname)
-{
- if (!*hostname)
- return VALIDATION_ERROR_EMPTY;
- if (!irc_regex_match ("^" SN "(\\." SN ")*$", hostname))
- return VALIDATION_ERROR_INVALID;
- if (strlen (hostname) > IRC_MAX_HOSTNAME)
- return VALIDATION_ERROR_TOO_LONG;
- return VALIDATION_OK;
-}
-
-static bool
-irc_is_valid_hostaddr (const char *hostaddr)
-{
- if (irc_regex_match ("^" N4 "\\." N4 "\\." N4 "\\." N4 "$", hostaddr)
- || irc_regex_match ("^" N6 ":" N6 ":" N6 ":" N6 ":"
- N6 ":" N6 ":" N6 ":" N6 "$", hostaddr)
- || irc_regex_match ("^0:0:0:0:0:(0|[Ff]{4}):"
- N4 "\\." N4 "\\." N4 "\\." N4 "$", hostaddr))
- return true;
- return false;
-}
-
-// TODO: we should actually use this, though what should we do on failure?
-static bool
-irc_is_valid_host (const char *host)
-{
- return irc_validate_hostname (host) == VALIDATION_OK
- || irc_is_valid_hostaddr (host);
-}
-
-// TODO: currently, we are almost encoding-agnostic (strings just need to be
-// ASCII-compatible). We should at least have an option to enforce a specific
-// encoding, such as UTF-8. Note that with Unicode we should not allow all
-// character clasess and exclude the likes of \pM with the goal of enforcing
-// NFC-normalized identifiers--utf8proc is a good candidate library to handle
-// the categorization and validation.
-
-static bool
-irc_is_valid_user (const char *user)
-{
- return irc_regex_match ("^[^\r\n @]+$", user);
-}
-
-static bool
-irc_validate_nickname (const char *nickname)
-{
- if (!*nickname)
- return VALIDATION_ERROR_EMPTY;
- if (!irc_regex_match ("^[" SP LE "][" SP LE "0-9-]*$", nickname))
- return VALIDATION_ERROR_INVALID;
- if (strlen (nickname) > IRC_MAX_NICKNAME)
- return VALIDATION_ERROR_TOO_LONG;
- return VALIDATION_OK;
-}
-
-static enum validation_result
-irc_validate_channel_name (const char *channel_name)
-{
- if (!*channel_name)
- return VALIDATION_ERROR_EMPTY;
- if (*channel_name != '#' || strpbrk (channel_name, "\7\r\n ,:"))
- return VALIDATION_ERROR_INVALID;
- if (strlen (channel_name) > IRC_MAX_CHANNEL_NAME)
- return VALIDATION_ERROR_TOO_LONG;
- return VALIDATION_OK;
-}
-
-static bool
-irc_is_valid_key (const char *key)
-{
- // XXX: should be 7-bit as well but whatever
- return irc_regex_match ("^[^\r\n\f\t\v ]{1,23}$", key);
-}
-
-#undef SN
-#undef N4
-#undef N6
-
-#undef LE
-#undef SP
-
-static bool
-irc_is_valid_user_mask (const char *mask)
-{
- return irc_regex_match ("^[^!@]+![^!@]+@[^@!]+$", mask);
-}
-
-static bool
-irc_is_valid_fingerprint (const char *fp)
-{
- return irc_regex_match ("^[a-fA-F0-9]{40}$", fp);
-}
-
-// --- Clients (equals users) --------------------------------------------------
-
-#define IRC_SUPPORTED_USER_MODES "aiwros"
-
-enum
-{
- IRC_USER_MODE_INVISIBLE = (1 << 0),
- IRC_USER_MODE_RX_WALLOPS = (1 << 1),
- IRC_USER_MODE_RESTRICTED = (1 << 2),
- IRC_USER_MODE_OPERATOR = (1 << 3),
- IRC_USER_MODE_RX_SERVER_NOTICES = (1 << 4)
-};
-
-enum
-{
- IRC_CAP_MULTI_PREFIX = (1 << 0),
- IRC_CAP_INVITE_NOTIFY = (1 << 1),
- IRC_CAP_ECHO_MESSAGE = (1 << 2),
- IRC_CAP_USERHOST_IN_NAMES = (1 << 3),
- IRC_CAP_SERVER_TIME = (1 << 4)
-};
-
-struct client
-{
- LIST_HEADER (struct client)
- struct server_context *ctx; ///< Server context
-
- time_t opened; ///< When the connection was opened
- size_t n_sent_messages; ///< Number of sent messages total
- size_t sent_bytes; ///< Number of sent bytes total
- size_t n_received_messages; ///< Number of received messages total
- size_t received_bytes; ///< Number of received bytes total
-
- int socket_fd; ///< The TCP socket
- struct str read_buffer; ///< Unprocessed input
- struct str write_buffer; ///< Output yet to be sent out
-
- struct poller_fd socket_event; ///< The socket can be read/written to
- struct poller_timer ping_timer; ///< We should send a ping
- struct poller_timer timeout_timer; ///< Connection seems to be dead
- struct poller_timer kill_timer; ///< Hard kill timeout
-
- unsigned long cap_version; ///< CAP protocol version
- unsigned caps_enabled; ///< Enabled capabilities
-
- unsigned initialized : 1; ///< Has any data been received yet?
- unsigned cap_negotiating : 1; ///< Negotiating capabilities
- unsigned registered : 1; ///< The user has registered
- unsigned closing_link : 1; ///< Closing link
- unsigned half_closed : 1; ///< Closing link: conn. is half-closed
-
- unsigned ssl_rx_want_tx : 1; ///< SSL_read() wants to write
- unsigned ssl_tx_want_rx : 1; ///< SSL_write() wants to read
- SSL *ssl; ///< SSL connection
- char *ssl_cert_fingerprint; ///< Client certificate fingerprint
-
- char *nickname; ///< IRC nickname (main identifier)
- char *username; ///< IRC username
- char *realname; ///< IRC realname (e-mail)
-
- char *hostname; ///< Hostname shown to the network
- char *port; ///< Port of the peer as a string
- char *address; ///< Full address
-
- unsigned mode; ///< User's mode
- char *away_message; ///< Away message
- time_t last_active; ///< Last PRIVMSG, to get idle time
- struct str_map invites; ///< Channel invitations by operators
- struct flood_detector antiflood; ///< Flood detector
-
- struct async_getnameinfo *gni; ///< Backwards DNS resolution
- struct poller_timer gni_timer; ///< Backwards DNS resolution timeout
-};
-
-static struct client *
-client_new (void)
-{
- struct client *self = xcalloc (1, sizeof *self);
- self->socket_fd = -1;
- self->read_buffer = str_make ();
- self->write_buffer = str_make ();
- self->cap_version = 301;
- // TODO: make this configurable and more fine-grained
- flood_detector_init (&self->antiflood, 10, 20);
- self->invites = str_map_make (NULL);
- self->invites.key_xfrm = irc_strxfrm;
- return self;
-}
-
-static void
-client_destroy (struct client *self)
-{
- if (!soft_assert (self->socket_fd == -1))
- xclose (self->socket_fd);
- if (self->ssl)
- SSL_free (self->ssl);
-
- str_free (&self->read_buffer);
- str_free (&self->write_buffer);
- free (self->ssl_cert_fingerprint);
-
- free (self->nickname);
- free (self->username);
- free (self->realname);
-
- free (self->hostname);
- free (self->port);
- free (self->address);
-
- free (self->away_message);
- flood_detector_free (&self->antiflood);
- str_map_free (&self->invites);
-
- if (self->gni)
- async_cancel (&self->gni->async);
- free (self);
-}
-
-static void client_close_link (struct client *c, const char *reason);
-static void client_kill (struct client *c, const char *reason);
-static void client_send (struct client *, const char *, ...)
- ATTRIBUTE_PRINTF (2, 3);
-static void client_cancel_timers (struct client *);
-static void client_set_kill_timer (struct client *);
-static void client_update_poller (struct client *, const struct pollfd *);
-
-// --- Channels ----------------------------------------------------------------
-
-#define IRC_SUPPORTED_CHAN_MODES "ov" "beI" "imnqpst" "kl"
-
-enum
-{
- IRC_CHAN_MODE_INVITE_ONLY = (1 << 0),
- IRC_CHAN_MODE_MODERATED = (1 << 1),
- IRC_CHAN_MODE_NO_OUTSIDE_MSGS = (1 << 2),
- IRC_CHAN_MODE_QUIET = (1 << 3),
- IRC_CHAN_MODE_PRIVATE = (1 << 4),
- IRC_CHAN_MODE_SECRET = (1 << 5),
- IRC_CHAN_MODE_PROTECTED_TOPIC = (1 << 6),
-
- IRC_CHAN_MODE_OPERATOR = (1 << 7),
- IRC_CHAN_MODE_VOICE = (1 << 8)
-};
-
-struct channel_user
-{
- LIST_HEADER (struct channel_user)
-
- unsigned modes;
- struct client *c;
-};
-
-struct channel
-{
- struct server_context *ctx; ///< Server context
-
- char *name; ///< Channel name
- unsigned modes; ///< Channel modes
- char *key; ///< Channel key
- long user_limit; ///< User limit or -1
- time_t created; ///< Creation time
-
- char *topic; ///< Channel topic
- char *topic_who; ///< Who set the topic
- time_t topic_time; ///< When the topic was set
-
- struct channel_user *users; ///< Channel users
-
- struct strv ban_list; ///< Ban list
- struct strv exception_list; ///< Exceptions from bans
- struct strv invite_list; ///< Exceptions from +I
-};
-
-static struct channel *
-channel_new (void)
-{
- struct channel *self = xcalloc (1, sizeof *self);
-
- self->user_limit = -1;
- self->topic = xstrdup ("");
-
- self->ban_list = strv_make ();
- self->exception_list = strv_make ();
- self->invite_list = strv_make ();
- return self;
-}
-
-static void
-channel_delete (struct channel *self)
-{
- free (self->name);
- free (self->key);
- free (self->topic);
- free (self->topic_who);
-
- struct channel_user *link, *tmp;
- for (link = self->users; link; link = tmp)
- {
- tmp = link->next;
- free (link);
- }
-
- strv_free (&self->ban_list);
- strv_free (&self->exception_list);
- strv_free (&self->invite_list);
-
- free (self);
-}
-
-static char *
-channel_get_mode (struct channel *self, bool disclose_secrets)
-{
- struct str mode = str_make ();
- unsigned m = self->modes;
- if (m & IRC_CHAN_MODE_INVITE_ONLY) str_append_c (&mode, 'i');
- if (m & IRC_CHAN_MODE_MODERATED) str_append_c (&mode, 'm');
- if (m & IRC_CHAN_MODE_NO_OUTSIDE_MSGS) str_append_c (&mode, 'n');
- if (m & IRC_CHAN_MODE_QUIET) str_append_c (&mode, 'q');
- if (m & IRC_CHAN_MODE_PRIVATE) str_append_c (&mode, 'p');
- if (m & IRC_CHAN_MODE_SECRET) str_append_c (&mode, 's');
- if (m & IRC_CHAN_MODE_PROTECTED_TOPIC) str_append_c (&mode, 't');
-
- if (self->user_limit != -1) str_append_c (&mode, 'l');
- if (self->key) str_append_c (&mode, 'k');
-
- // XXX: is it correct to split it? Try it on an existing implementation.
- if (disclose_secrets)
- {
- if (self->user_limit != -1)
- str_append_printf (&mode, " %ld", self->user_limit);
- if (self->key)
- str_append_printf (&mode, " %s", self->key);
- }
- return str_steal (&mode);
-}
-
-static struct channel_user *
-channel_get_user (const struct channel *chan, const struct client *c)
-{
- for (struct channel_user *iter = chan->users; iter; iter = iter->next)
- if (iter->c == c)
- return iter;
- return NULL;
-}
-
-static struct channel_user *
-channel_add_user (struct channel *chan, struct client *c)
-{
- struct channel_user *link = xcalloc (1, sizeof *link);
- link->c = c;
- LIST_PREPEND (chan->users, link);
- return link;
-}
-
-static void
-channel_remove_user (struct channel *chan, struct channel_user *user)
-{
- LIST_UNLINK (chan->users, user);
- free (user);
-}
-
-static size_t
-channel_user_count (const struct channel *chan)
-{
- size_t result = 0;
- for (struct channel_user *iter = chan->users; iter; iter = iter->next)
- result++;
- return result;
-}
-
-// --- IRC server context ------------------------------------------------------
-
-struct whowas_info
-{
- char *nickname; ///< IRC nickname
- char *username; ///< IRC username
- char *realname; ///< IRC realname
- char *hostname; ///< Hostname shown to the network
-};
-
-struct whowas_info *
-whowas_info_new (struct client *c)
-{
- struct whowas_info *self = xmalloc (sizeof *self);
- self->nickname = xstrdup (c->nickname);
- self->username = xstrdup (c->username);
- self->realname = xstrdup (c->realname);
- self->hostname = xstrdup (c->hostname);
- return self;
-}
-
-static void
-whowas_info_destroy (struct whowas_info *self)
-{
- free (self->nickname);
- free (self->username);
- free (self->realname);
- free (self->hostname);
- free (self);
-}
-
-// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-struct irc_command
-{
- const char *name;
- bool requires_registration;
- void (*handler) (const struct irc_message *, struct client *);
-
- size_t n_received; ///< Number of commands received
- size_t bytes_received; ///< Number of bytes received total
-};
-
-struct server_context
-{
- int *listen_fds; ///< Listening socket FD's
- struct poller_fd *listen_events; ///< New connections available
- size_t listen_len; ///< Number of listening sockets
- size_t listen_alloc; ///< How many we've allocated
-
- time_t started; ///< When has the server been started
-
- SSL_CTX *ssl_ctx; ///< SSL context
- struct client *clients; ///< Clients
- unsigned n_clients; ///< Current number of connections
-
- struct str_map users; ///< Maps nicknames to clients
- struct str_map channels; ///< Maps channel names to data
- struct str_map handlers; ///< Message handlers
- struct str_map cap_handlers; ///< CAP message handlers
-
- struct str_map whowas; ///< WHOWAS registry
-
- struct poller poller; ///< Manages polled description
- struct poller_timer quit_timer; ///< Quit timeout timer
- bool quitting; ///< User requested quitting
- bool polling; ///< The event loop is running
-
- struct poller_fd signal_event; ///< Got a signal
-
- struct str_map config; ///< Server configuration
- char *server_name; ///< Our server name
- unsigned ping_interval; ///< Ping interval in seconds
- unsigned max_connections; ///< Max. connections allowed or 0
- struct strv motd; ///< MOTD (none if empty)
- nl_catd catalog; ///< Message catalog for server msgs
- struct str_map operators; ///< TLS cert. fingerprints for IRCops
-};
-
-static void
-on_irc_quit_timeout (void *user_data)
-{
- struct server_context *ctx = user_data;
- struct client *iter, *next;
- for (iter = ctx->clients; iter; iter = next)
- {
- next = iter->next;
- // irc_initiate_quit() has already unregistered the client
- client_kill (iter, "Shutting down");
- }
-}
-
-static void
-server_context_init (struct server_context *self)
-{
- memset (self, 0, sizeof *self);
-
- self->users = str_map_make (NULL);
- self->users.key_xfrm = irc_strxfrm;
- self->channels = str_map_make ((str_map_free_fn) channel_delete);
- self->channels.key_xfrm = irc_strxfrm;
- self->handlers = str_map_make (NULL);
- self->handlers.key_xfrm = irc_strxfrm;
- self->cap_handlers = str_map_make (NULL);
- self->cap_handlers.key_xfrm = irc_strxfrm;
-
- self->whowas = str_map_make ((str_map_free_fn) whowas_info_destroy);
- self->whowas.key_xfrm = irc_strxfrm;
-
- poller_init (&self->poller);
- self->quit_timer = poller_timer_make (&self->poller);
- self->quit_timer.dispatcher = on_irc_quit_timeout;
- self->quit_timer.user_data = self;
-
- self->config = str_map_make (free);
- simple_config_load_defaults (&self->config, g_config_table);
-
- self->motd = strv_make ();
- self->catalog = (nl_catd) -1;
- self->operators = str_map_make (NULL);
- // The regular irc_strxfrm() is sufficient for fingerprints
- self->operators.key_xfrm = irc_strxfrm;
-}
-
-static void
-server_context_free (struct server_context *self)
-{
- str_map_free (&self->config);
-
- for (size_t i = 0; i < self->listen_len; i++)
- {
- poller_fd_reset (&self->listen_events[i]);
- xclose (self->listen_fds[i]);
- }
- free (self->listen_fds);
- free (self->listen_events);
-
- hard_assert (!self->clients);
- if (self->ssl_ctx)
- SSL_CTX_free (self->ssl_ctx);
-
- free (self->server_name);
- str_map_free (&self->users);
- str_map_free (&self->channels);
- str_map_free (&self->handlers);
- str_map_free (&self->cap_handlers);
- str_map_free (&self->whowas);
- poller_free (&self->poller);
-
- strv_free (&self->motd);
- if (self->catalog != (nl_catd) -1)
- catclose (self->catalog);
- str_map_free (&self->operators);
-}
-
-static const char *
-irc_get_text (struct server_context *ctx, int id, const char *def)
-{
- if (!soft_assert (def != NULL))
- def = "";
- if (ctx->catalog == (nl_catd) -1)
- return def;
- return catgets (ctx->catalog, 1, id, def);
-}
-
-static void
-irc_try_finish_quit (struct server_context *ctx)
-{
- if (!ctx->n_clients && ctx->quitting)
- {
- poller_timer_reset (&ctx->quit_timer);
- ctx->polling = false;
- }
-}
-
-static void
-irc_initiate_quit (struct server_context *ctx)
-{
- print_status ("shutting down");
-
- for (size_t i = 0; i < ctx->listen_len; i++)
- {
- poller_fd_reset (&ctx->listen_events[i]);
- xclose (ctx->listen_fds[i]);
- }
- ctx->listen_len = 0;
-
- for (struct client *iter = ctx->clients; iter; iter = iter->next)
- if (!iter->closing_link)
- client_close_link (iter, "Shutting down");
-
- ctx->quitting = true;
- poller_timer_set (&ctx->quit_timer, 5000);
- irc_try_finish_quit (ctx);
-}
-
-static struct channel *
-irc_channel_create (struct server_context *ctx, const char *name)
-{
- struct channel *chan = channel_new ();
- chan->ctx = ctx;
- chan->name = xstrdup (name);
- chan->created = time (NULL);
- str_map_set (&ctx->channels, name, chan);
- return chan;
-}
-
-static void
-irc_channel_destroy_if_empty (struct server_context *ctx, struct channel *chan)
-{
- if (!chan->users)
- str_map_set (&ctx->channels, chan->name, NULL);
-}
-
-static void
-irc_send_to_roommates (struct client *c, const char *message)
-{
- struct str_map targets = str_map_make (NULL);
- targets.key_xfrm = irc_strxfrm;
-
- struct str_map_iter iter = str_map_iter_make (&c->ctx->channels);
- struct channel *chan;
- while ((chan = str_map_iter_next (&iter)))
- {
- if (chan->modes & IRC_CHAN_MODE_QUIET
- || !channel_get_user (chan, c))
- continue;
- for (struct channel_user *iter = chan->users; iter; iter = iter->next)
- str_map_set (&targets, iter->c->nickname, iter->c);
- }
-
- iter = str_map_iter_make (&targets);
- struct client *target;
- while ((target = str_map_iter_next (&iter)))
- if (target != c)
- client_send (target, "%s", message);
- str_map_free (&targets);
-}
-
-// --- Clients (continued) -----------------------------------------------------
-
-static void
-client_mode_to_str (unsigned m, struct str *out)
-{
- if (m & IRC_USER_MODE_INVISIBLE) str_append_c (out, 'i');
- if (m & IRC_USER_MODE_RX_WALLOPS) str_append_c (out, 'w');
- if (m & IRC_USER_MODE_RESTRICTED) str_append_c (out, 'r');
- if (m & IRC_USER_MODE_OPERATOR) str_append_c (out, 'o');
- if (m & IRC_USER_MODE_RX_SERVER_NOTICES) str_append_c (out, 's');
-}
-
-static char *
-client_get_mode (struct client *self)
-{
- struct str mode = str_make ();
- if (self->away_message)
- str_append_c (&mode, 'a');
- client_mode_to_str (self->mode, &mode);
- return str_steal (&mode);
-}
-
-static void
-client_send_str (struct client *c, const struct str *s)
-{
- hard_assert (!c->closing_link);
-
- size_t old_sendq = c->write_buffer.len;
-
- // So far there's only one message tag we use, so we can do it simple;
- // note that a 1024-character limit applies to messages with tags on
- if (c->caps_enabled & IRC_CAP_SERVER_TIME)
- {
- long milliseconds; char buf[32]; struct tm tm;
- time_t now = unixtime_msec (&milliseconds);
- if (soft_assert (strftime (buf, sizeof buf,
- "%Y-%m-%dT%T", gmtime_r (&now, &tm))))
- str_append_printf (&c->write_buffer,
- "@time=%s.%03ldZ ", buf, milliseconds);
- }
-
- // TODO: kill the connection above some "SendQ" threshold (careful!)
- str_append_data (&c->write_buffer, s->str,
- MIN (s->len, IRC_MAX_MESSAGE_LENGTH));
- str_append (&c->write_buffer, "\r\n");
- // XXX: we might want to move this elsewhere, so that it doesn't get called
- // as often; it's going to cause a lot of syscalls with epoll.
- client_update_poller (c, NULL);
-
- // Technically we haven't sent it yet but that's a minor detail
- c->n_sent_messages++;
- c->sent_bytes += c->write_buffer.len - old_sendq;
-}
-
-static void
-client_send (struct client *c, const char *format, ...)
-{
- struct str tmp = str_make ();
-
- va_list ap;
- va_start (ap, format);
- str_append_vprintf (&tmp, format, ap);
- va_end (ap);
-
- client_send_str (c, &tmp);
- str_free (&tmp);
-}
-
-static void
-client_add_to_whowas (struct client *c)
-{
- // Only keeping one entry for each nickname
- // TODO: make sure this list doesn't get too long, for example by
- // putting them in a linked list ordered by time
- str_map_set (&c->ctx->whowas, c->nickname, whowas_info_new (c));
-}
-
-static void
-client_unregister (struct client *c, const char *reason)
-{
- if (!c->registered)
- return;
-
- char *message = xstrdup_printf (":%s!%s@%s QUIT :%s",
- c->nickname, c->username, c->hostname, reason);
- irc_send_to_roommates (c, message);
- free (message);
-
- struct str_map_unset_iter iter =
- str_map_unset_iter_make (&c->ctx->channels);
- struct channel *chan;
- while ((chan = str_map_unset_iter_next (&iter)))
- {
- struct channel_user *user;
- if (!(user = channel_get_user (chan, c)))
- continue;
- channel_remove_user (chan, user);
- irc_channel_destroy_if_empty (c->ctx, chan);
- }
- str_map_unset_iter_free (&iter);
-
- client_add_to_whowas (c);
-
- str_map_set (&c->ctx->users, c->nickname, NULL);
- cstr_set (&c->nickname, NULL);
- c->registered = false;
-}
-
-static void
-client_kill (struct client *c, const char *reason)
-{
- struct server_context *ctx = c->ctx;
- client_unregister (c, reason ? reason : "Client exited");
-
- if (c->address)
- // Only log the event if address resolution has finished
- print_debug ("closed connection to %s (%s)", c->address,
- reason ? reason : "");
-
- if (c->ssl)
- // Note that we might have already called this once, but that is fine
- (void) SSL_shutdown (c->ssl);
-
- xclose (c->socket_fd);
- c->socket_fd = -1;
-
- // We don't fork any children, this is okay
- c->socket_event.closed = true;
- poller_fd_reset (&c->socket_event);
- client_cancel_timers (c);
-
- LIST_UNLINK (ctx->clients, c);
- ctx->n_clients--;
- client_destroy (c);
-
- irc_try_finish_quit (ctx);
-}
-
-static void
-client_close_link (struct client *c, const char *reason)
-{
- // Let's just cut the connection, the client can try again later.
- // We also want to avoid accidentally setting poller events before
- // address resolution has finished.
- if (!c->initialized)
- {
- client_kill (c, reason);
- return;
- }
- if (!soft_assert (!c->closing_link))
- return;
-
- // We push an `ERROR' message to the write buffer and let the poller send
- // it, with some arbitrary timeout. The `closing_link' state makes sure
- // that a/ we ignore any successive messages, and b/ that the connection
- // is killed after the write buffer is transferred and emptied.
- client_send (c, "ERROR :Closing Link: %s[%s] (%s)",
- c->nickname ? c->nickname : "*",
- c->hostname /* TODO host IP? */, reason);
- c->closing_link = true;
-
- client_unregister (c, reason);
- client_set_kill_timer (c);
-}
-
-static bool
-client_in_mask_list (const struct client *c, const struct strv *mask)
-{
- char *client = xstrdup_printf ("%s!%s@%s",
- c->nickname, c->username, c->hostname);
- bool result = false;
- for (size_t i = 0; i < mask->len; i++)
- if (!irc_fnmatch (mask->vector[i], client))
- {
- result = true;
- break;
- }
- free (client);
- return result;
-}
-
-static char *
-client_get_ssl_cert_fingerprint (struct client *c)
-{
- if (!c->ssl)
- return NULL;
-
- X509 *peer_cert = SSL_get_peer_certificate (c->ssl);
- if (!peer_cert)
- return NULL;
-
- int cert_len = i2d_X509 (peer_cert, NULL);
- if (cert_len < 0)
- return NULL;
-
- unsigned char cert[cert_len], *p = cert;
- if (i2d_X509 (peer_cert, &p) < 0)
- return NULL;
-
- unsigned char hash[SHA_DIGEST_LENGTH];
- SHA1 (cert, cert_len, hash);
-
- struct str fingerprint = str_make ();
- for (size_t i = 0; i < sizeof hash; i++)
- str_append_printf (&fingerprint, "%02x", hash[i]);
- return str_steal (&fingerprint);
-}
-
-// --- Timers ------------------------------------------------------------------
-
-static void
-client_cancel_timers (struct client *c)
-{
- poller_timer_reset (&c->kill_timer);
- poller_timer_reset (&c->timeout_timer);
- poller_timer_reset (&c->ping_timer);
- poller_timer_reset (&c->gni_timer);
-}
-
-static void
-client_set_timer (struct client *c,
- struct poller_timer *timer, unsigned interval)
-{
- client_cancel_timers (c);
- poller_timer_set (timer, interval * 1000);
-}
-
-static void
-on_client_kill_timer (struct client *c)
-{
- hard_assert (!c->initialized || c->closing_link);
- client_kill (c, NULL);
-}
-
-static void
-client_set_kill_timer (struct client *c)
-{
- client_set_timer (c, &c->kill_timer, c->ctx->ping_interval);
-}
-
-static void
-on_client_timeout_timer (struct client *c)
-{
- char *reason = xstrdup_printf
- ("Ping timeout: >%u seconds", c->ctx->ping_interval);
- client_close_link (c, reason);
- free (reason);
-}
-
-static void
-on_client_ping_timer (struct client *c)
-{
- hard_assert (!c->closing_link);
- client_send (c, "PING :%s", c->ctx->server_name);
- client_set_timer (c, &c->timeout_timer, c->ctx->ping_interval);
-}
-
-static void
-client_set_ping_timer (struct client *c)
-{
- client_set_timer (c, &c->ping_timer, c->ctx->ping_interval);
-}
-
-// --- IRC command handling ----------------------------------------------------
-
-static void
-irc_make_reply (struct client *c, int id, va_list ap, struct str *output)
-{
- str_append_printf (output, ":%s %03d %s ",
- c->ctx->server_name, id, c->nickname ? c->nickname : "*");
- str_append_vprintf (output,
- irc_get_text (c->ctx, id, g_default_replies[id]), ap);
-}
-
-// XXX: this way we cannot typecheck the arguments, so we must be careful
-static void
-irc_send_reply (struct client *c, int id, ...)
-{
- struct str reply = str_make ();
-
- va_list ap;
- va_start (ap, id);
- irc_make_reply (c, id, ap, &reply);
- va_end (ap);
-
- client_send_str (c, &reply);
- str_free (&reply);
-}
-
-/// Send a space-separated list of words across as many replies as needed
-static void
-irc_send_reply_vector (struct client *c, int id, char **items, ...)
-{
- struct str common = str_make ();
-
- va_list ap;
- va_start (ap, items);
- irc_make_reply (c, id, ap, &common);
- va_end (ap);
-
- // We always send at least one message (there might be a client that
- // expects us to send this message at least once)
- do
- {
- struct str reply = str_make ();
- str_append_str (&reply, &common);
-
- // If not even a single item fits in the limit (which may happen,
- // in theory) it just gets cropped. We could also skip it.
- if (*items)
- str_append (&reply, *items++);
-
- // Append as many items as fits in a single message
- while (*items
- && reply.len + 1 + strlen (*items) <= IRC_MAX_MESSAGE_LENGTH)
- str_append_printf (&reply, " %s", *items++);
-
- client_send_str (c, &reply);
- str_free (&reply);
- }
- while (*items);
- str_free (&common);
-}
-
-#define RETURN_WITH_REPLY(c, ...) \
- BLOCK_START \
- irc_send_reply ((c), __VA_ARGS__); \
- return; \
- BLOCK_END
-
-static void
-irc_send_motd (struct client *c)
-{
- struct server_context *ctx = c->ctx;
- if (!ctx->motd.len)
- RETURN_WITH_REPLY (c, IRC_ERR_NOMOTD);
-
- irc_send_reply (c, IRC_RPL_MOTDSTART, ctx->server_name);
- for (size_t i = 0; i < ctx->motd.len; i++)
- irc_send_reply (c, IRC_RPL_MOTD, ctx->motd.vector[i]);
- irc_send_reply (c, IRC_RPL_ENDOFMOTD);
-}
-
-static void
-irc_send_lusers (struct client *c)
-{
- int n_users = 0, n_services = 0, n_opers = 0, n_unknown = 0;
- for (struct client *link = c->ctx->clients; link; link = link->next)
- {
- if (link->registered)
- n_users++;
- else
- n_unknown++;
- if (link->mode & IRC_USER_MODE_OPERATOR)
- n_opers++;
- }
-
- int n_channels = 0;
- struct str_map_iter iter = str_map_iter_make (&c->ctx->channels);
- struct channel *chan;
- while ((chan = str_map_iter_next (&iter)))
- if (!(chan->modes & IRC_CHAN_MODE_SECRET)
- || channel_get_user (chan, c))
- n_channels++;
-
- irc_send_reply (c, IRC_RPL_LUSERCLIENT,
- n_users, n_services, 1 /* servers total */);
- if (n_opers)
- irc_send_reply (c, IRC_RPL_LUSEROP, n_opers);
- if (n_unknown)
- irc_send_reply (c, IRC_RPL_LUSERUNKNOWN, n_unknown);
- if (n_channels)
- irc_send_reply (c, IRC_RPL_LUSERCHANNELS, n_channels);
- irc_send_reply (c, IRC_RPL_LUSERME,
- n_users + n_services + n_unknown, 0 /* peer servers */);
-}
-
-static bool
-irc_is_this_me (struct server_context *ctx, const char *target)
-{
- // Target servers can also be matched by their users
- return !irc_fnmatch (target, ctx->server_name)
- || str_map_find (&ctx->users, target);
-}
-
-static void
-irc_send_isupport (struct client *c)
-{
- // Only # channels, +e supported, +I supported, unlimited arguments to MODE
- irc_send_reply (c, IRC_RPL_ISUPPORT, "CHANTYPES=# EXCEPTS INVEX MODES"
- " TARGMAX=WHOIS:,LIST:,NAMES:,PRIVMSG:1,NOTICE:1,KICK:"
- " NICKLEN=" XSTRINGIFY (IRC_MAX_NICKNAME)
- " CHANNELLEN=" XSTRINGIFY (IRC_MAX_CHANNEL_NAME));
-}
-
-static void
-irc_try_finish_registration (struct client *c)
-{
- struct server_context *ctx = c->ctx;
- if (!c->nickname || !c->username || !c->realname)
- return;
- if (c->registered || c->cap_negotiating)
- return;
-
- c->registered = true;
- irc_send_reply (c, IRC_RPL_WELCOME, c->nickname, c->username, c->hostname);
-
- irc_send_reply (c, IRC_RPL_YOURHOST, ctx->server_name, PROGRAM_VERSION);
- // The purpose of this message eludes me
- irc_send_reply (c, IRC_RPL_CREATED, __DATE__);
- irc_send_reply (c, IRC_RPL_MYINFO, ctx->server_name, PROGRAM_VERSION,
- IRC_SUPPORTED_USER_MODES, IRC_SUPPORTED_CHAN_MODES);
-
- irc_send_isupport (c);
- irc_send_lusers (c);
- irc_send_motd (c);
-
- char *mode = client_get_mode (c);
- if (*mode)
- client_send (c, ":%s MODE %s :+%s", c->nickname, c->nickname, mode);
- free (mode);
-
- hard_assert (c->ssl_cert_fingerprint == NULL);
- if ((c->ssl_cert_fingerprint = client_get_ssl_cert_fingerprint (c)))
- client_send (c, ":%s NOTICE %s :"
- "Your TLS client certificate fingerprint is %s",
- ctx->server_name, c->nickname, c->ssl_cert_fingerprint);
-
- str_map_set (&ctx->whowas, c->nickname, NULL);
-}
-
-// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-// IRCv3 capability negotiation. See http://ircv3.org for details.
-
-struct irc_cap_args
-{
- const char *subcommand; ///< The subcommand being processed
- const char *full_params; ///< Whole parameter string
- struct strv params; ///< Split parameters
- const char *target; ///< Target parameter for replies
-};
-
-static struct
-{
- unsigned flag; ///< Flag
- const char *name; ///< Name of the capability
-}
-irc_cap_table[] =
-{
- { IRC_CAP_MULTI_PREFIX, "multi-prefix" },
- { IRC_CAP_INVITE_NOTIFY, "invite-notify" },
- { IRC_CAP_ECHO_MESSAGE, "echo-message" },
- { IRC_CAP_USERHOST_IN_NAMES, "userhost-in-names" },
- { IRC_CAP_SERVER_TIME, "server-time" },
-};
-
-static void
-irc_handle_cap_ls (struct client *c, struct irc_cap_args *a)
-{
- if (a->params.len == 1
- && !xstrtoul (&c->cap_version, a->params.vector[0], 10))
- irc_send_reply (c, IRC_ERR_INVALIDCAPCMD,
- a->subcommand, "Ignoring invalid protocol version number");
-
- c->cap_negotiating = true;
- client_send (c, ":%s CAP %s LS :multi-prefix invite-notify echo-message"
- " userhost-in-names server-time", c->ctx->server_name, a->target);
-}
-
-static void
-irc_handle_cap_list (struct client *c, struct irc_cap_args *a)
-{
- struct strv caps = strv_make ();
- for (size_t i = 0; i < N_ELEMENTS (irc_cap_table); i++)
- if (c->caps_enabled & irc_cap_table[i].flag)
- strv_append (&caps, irc_cap_table[i].name);
-
- char *caps_str = strv_join (&caps, " ");
- strv_free (&caps);
- client_send (c, ":%s CAP %s LIST :%s",
- c->ctx->server_name, a->target, caps_str);
- free (caps_str);
-}
-
-static unsigned
-irc_decode_capability (const char *name)
-{
- for (size_t i = 0; i < N_ELEMENTS (irc_cap_table); i++)
- if (!strcmp (irc_cap_table[i].name, name))
- return irc_cap_table[i].flag;
- return 0;
-}
-
-static void
-irc_handle_cap_req (struct client *c, struct irc_cap_args *a)
-{
- c->cap_negotiating = true;
-
- unsigned new_caps = c->caps_enabled;
- bool success = true;
- for (size_t i = 0; i < a->params.len; i++)
- {
- bool removing = false;
- const char *name = a->params.vector[i];
- if (*name == '-')
- {
- removing = true;
- name++;
- }
-
- unsigned cap;
- if (!(cap = irc_decode_capability (name)))
- success = false;
- else if (removing)
- new_caps &= ~cap;
- else
- new_caps |= cap;
- }
-
- if (success)
- {
- c->caps_enabled = new_caps;
- client_send (c, ":%s CAP %s ACK :%s",
- c->ctx->server_name, a->target, a->full_params);
- }
- else
- client_send (c, ":%s CAP %s NAK :%s",
- c->ctx->server_name, a->target, a->full_params);
-}
-
-static void
-irc_handle_cap_ack (struct client *c, struct irc_cap_args *a)
-{
- if (a->params.len)
- irc_send_reply (c, IRC_ERR_INVALIDCAPCMD,
- a->subcommand, "No acknowledgable capabilities supported");
-}
-
-static void
-irc_handle_cap_end (struct client *c, struct irc_cap_args *a)
-{
- (void) a;
-
- c->cap_negotiating = false;
- irc_try_finish_registration (c);
-}
-
-// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-struct irc_cap_command
-{
- const char *name;
- void (*handler) (struct client *, struct irc_cap_args *);
-};
-
-static void
-irc_register_cap_handlers (struct server_context *ctx)
-{
- static const struct irc_cap_command cap_handlers[] =
- {
- { "LS", irc_handle_cap_ls },
- { "LIST", irc_handle_cap_list },
- { "REQ", irc_handle_cap_req },
- { "ACK", irc_handle_cap_ack },
- { "END", irc_handle_cap_end },
- };
-
- for (size_t i = 0; i < N_ELEMENTS (cap_handlers); i++)
- {
- const struct irc_cap_command *cmd = &cap_handlers[i];
- str_map_set (&ctx->cap_handlers, cmd->name, (void *) cmd);
- }
-}
-
-static void
-irc_handle_cap (const struct irc_message *msg, struct client *c)
-{
- if (msg->params.len < 1)
- RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
-
- struct irc_cap_args args;
- args.target = c->nickname ? c->nickname : "*";
- args.subcommand = msg->params.vector[0];
- args.full_params = "";
- args.params = strv_make ();
-
- if (msg->params.len > 1)
- {
- args.full_params = msg->params.vector[1];
- cstr_split (args.full_params, " ", true, &args.params);
- }
-
- struct irc_cap_command *cmd =
- str_map_find (&c->ctx->cap_handlers, args.subcommand);
- if (!cmd)
- irc_send_reply (c, IRC_ERR_INVALIDCAPCMD,
- args.subcommand, "Invalid CAP subcommand");
- else
- cmd->handler (c, &args);
-
- strv_free (&args.params);
-}
-
-static void
-irc_handle_pass (const struct irc_message *msg, struct client *c)
-{
- if (c->registered)
- irc_send_reply (c, IRC_ERR_ALREADYREGISTERED);
- else if (msg->params.len < 1)
- irc_send_reply (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
-
- // We have TLS client certificates for this purpose; ignoring
-}
-
-static void
-irc_handle_nick (const struct irc_message *msg, struct client *c)
-{
- struct server_context *ctx = c->ctx;
-
- if (msg->params.len < 1)
- RETURN_WITH_REPLY (c, IRC_ERR_NONICKNAMEGIVEN);
-
- const char *nickname = msg->params.vector[0];
- if (irc_validate_nickname (nickname) != VALIDATION_OK)
- RETURN_WITH_REPLY (c, IRC_ERR_ERRONEOUSNICKNAME, nickname);
-
- struct client *client = str_map_find (&ctx->users, nickname);
- if (client && client != c)
- RETURN_WITH_REPLY (c, IRC_ERR_NICKNAMEINUSE, nickname);
-
- // Nothing to do here, let's not annoy roommates
- if (c->nickname && !strcmp (c->nickname, nickname))
- return;
-
- if (c->registered)
- {
- client_add_to_whowas (c);
-
- char *message = xstrdup_printf (":%s!%s@%s NICK :%s",
- c->nickname, c->username, c->hostname, nickname);
- irc_send_to_roommates (c, message);
- client_send (c, "%s", message);
- free (message);
- }
-
- // Release the old nickname and allocate a new one
- if (c->nickname)
- str_map_set (&ctx->users, c->nickname, NULL);
-
- cstr_set (&c->nickname, xstrdup (nickname));
- str_map_set (&ctx->users, nickname, c);
-
- irc_try_finish_registration (c);
-}
-
-static void
-irc_handle_user (const struct irc_message *msg, struct client *c)
-{
- if (c->registered)
- RETURN_WITH_REPLY (c, IRC_ERR_ALREADYREGISTERED);
- if (msg->params.len < 4)
- RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
-
- const char *username = msg->params.vector[0];
- const char *mode = msg->params.vector[1];
- const char *realname = msg->params.vector[3];
-
- // Unfortunately the protocol doesn't give us any means of rejecting it
- if (!irc_is_valid_user (username))
- username = "xxx";
-
- cstr_set (&c->username, xstrdup (username));
- cstr_set (&c->realname, xstrdup (realname));
- c->mode = 0;
-
- unsigned long m;
- if (xstrtoul (&m, mode, 10))
- {
- if (m & 4) c->mode |= IRC_USER_MODE_RX_WALLOPS;
- if (m & 8) c->mode |= IRC_USER_MODE_INVISIBLE;
- }
-
- irc_try_finish_registration (c);
-}
-
-static void
-irc_handle_userhost (const struct irc_message *msg, struct client *c)
-{
- if (msg->params.len < 1)
- RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
-
- struct str reply = str_make ();
- for (size_t i = 0; i < 5 && i < msg->params.len; i++)
- {
- const char *nick = msg->params.vector[i];
- struct client *target = str_map_find (&c->ctx->users, nick);
- if (!target)
- continue;
-
- if (i)
- str_append_c (&reply, ' ');
- str_append (&reply, nick);
- if (target->mode & IRC_USER_MODE_OPERATOR)
- str_append_c (&reply, '*');
- str_append_printf (&reply, "=%c%s@%s",
- target->away_message ? '-' : '+',
- target->username, target->hostname);
- }
- irc_send_reply (c, IRC_RPL_USERHOST, reply.str);
- str_free (&reply);
-}
-
-static void
-irc_handle_lusers (const struct irc_message *msg, struct client *c)
-{
- if (msg->params.len > 1 && !irc_is_this_me (c->ctx, msg->params.vector[1]))
- irc_send_reply (c, IRC_ERR_NOSUCHSERVER, msg->params.vector[1]);
- else
- irc_send_lusers (c);
-}
-
-static void
-irc_handle_motd (const struct irc_message *msg, struct client *c)
-{
- if (msg->params.len > 0 && !irc_is_this_me (c->ctx, msg->params.vector[0]))
- irc_send_reply (c, IRC_ERR_NOSUCHSERVER, msg->params.vector[0]);
- else
- irc_send_motd (c);
-}
-
-static void
-irc_handle_ping (const struct irc_message *msg, struct client *c)
-{
- // XXX: the RFC is pretty incomprehensible about the exact usage
- if (msg->params.len > 1 && !irc_is_this_me (c->ctx, msg->params.vector[1]))
- irc_send_reply (c, IRC_ERR_NOSUCHSERVER, msg->params.vector[1]);
- else if (msg->params.len < 1)
- irc_send_reply (c, IRC_ERR_NOORIGIN);
- else
- client_send (c, ":%s PONG :%s",
- c->ctx->server_name, msg->params.vector[0]);
-}
-
-static void
-irc_handle_pong (const struct irc_message *msg, struct client *c)
-{
- // We are the only server, so we don't have to care too much
- if (msg->params.len < 1)
- irc_send_reply (c, IRC_ERR_NOORIGIN);
- else
- // Set a new timer to send another PING
- client_set_ping_timer (c);
-}
-
-static void
-irc_handle_quit (const struct irc_message *msg, struct client *c)
-{
- char *reason = xstrdup_printf ("Quit: %s",
- msg->params.len > 0 ? msg->params.vector[0] : c->nickname);
- client_close_link (c, reason);
- free (reason);
-}
-
-static void
-irc_handle_time (const struct irc_message *msg, struct client *c)
-{
- if (msg->params.len > 0 && !irc_is_this_me (c->ctx, msg->params.vector[0]))
- RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHSERVER, msg->params.vector[0]);
-
- char buf[32] = "";
- time_t now = time (NULL);
- struct tm tm;
- strftime (buf, sizeof buf, "%a %b %d %Y %T", localtime_r (&now, &tm));
- irc_send_reply (c, IRC_RPL_TIME, c->ctx->server_name, buf);
-}
-
-static void
-irc_handle_version (const struct irc_message *msg, struct client *c)
-{
- if (msg->params.len > 0 && !irc_is_this_me (c->ctx, msg->params.vector[0]))
- RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHSERVER, msg->params.vector[0]);
-
- irc_send_reply (c, IRC_RPL_VERSION, PROGRAM_VERSION, g_debug_mode,
- c->ctx->server_name, PROGRAM_NAME " " PROGRAM_VERSION);
- irc_send_isupport (c);
-}
-
-static void
-irc_channel_multicast (struct channel *chan, const char *message,
- struct client *except)
-{
- for (struct channel_user *iter = chan->users; iter; iter = iter->next)
- if (iter->c != except)
- client_send (iter->c, "%s", message);
-}
-
-static bool
-irc_modify_mode (unsigned *mask, unsigned mode, bool add)
-{
- unsigned orig = *mask;
- if (add)
- *mask |= mode;
- else
- *mask &= ~mode;
- return *mask != orig;
-}
-
-static void
-irc_update_user_mode (struct client *c, unsigned new_mode)
-{
- unsigned old_mode = c->mode;
- c->mode = new_mode;
-
- unsigned added = new_mode & ~old_mode;
- unsigned removed = old_mode & ~new_mode;
-
- struct str diff = str_make ();
- if (added)
- {
- str_append_c (&diff, '+');
- client_mode_to_str (added, &diff);
- }
- if (removed)
- {
- str_append_c (&diff, '-');
- client_mode_to_str (removed, &diff);
- }
-
- if (diff.len)
- client_send (c, ":%s MODE %s :%s",
- c->nickname, c->nickname, diff.str);
- str_free (&diff);
-}
-
-static void
-irc_handle_user_mode_change (struct client *c, const char *mode_string)
-{
- unsigned new_mode = c->mode;
- bool adding = true;
-
- while (*mode_string)
- switch (*mode_string++)
- {
- case '+': adding = true; break;
- case '-': adding = false; break;
-
- case 'a':
- // Ignore, the client should use AWAY
- break;
- case 'i':
- irc_modify_mode (&new_mode, IRC_USER_MODE_INVISIBLE, adding);
- break;
- case 'w':
- irc_modify_mode (&new_mode, IRC_USER_MODE_RX_WALLOPS, adding);
- break;
- case 'r':
- // It's not possible to un-restrict yourself
- if (adding)
- new_mode |= IRC_USER_MODE_RESTRICTED;
- break;
- case 'o':
- if (!adding)
- new_mode &= ~IRC_USER_MODE_OPERATOR;
- else if (c->ssl_cert_fingerprint
- && str_map_find (&c->ctx->operators, c->ssl_cert_fingerprint))
- new_mode |= IRC_USER_MODE_OPERATOR;
- else
- client_send (c, ":%s NOTICE %s :Either you're not using an TLS"
- " client certificate, or the fingerprint doesn't match",
- c->ctx->server_name, c->nickname);
- break;
- case 's':
- irc_modify_mode (&new_mode, IRC_USER_MODE_RX_SERVER_NOTICES, adding);
- break;
- default:
- RETURN_WITH_REPLY (c, IRC_ERR_UMODEUNKNOWNFLAG);
- }
- irc_update_user_mode (c, new_mode);
-}
-
-static void
-irc_send_channel_list (struct client *c, const char *channel_name,
- const struct strv *list, int reply, int end_reply)
-{
- for (size_t i = 0; i < list->len; i++)
- irc_send_reply (c, reply, channel_name, list->vector[i]);
- irc_send_reply (c, end_reply, channel_name);
-}
-
-static char *
-irc_check_expand_user_mask (const char *mask)
-{
- struct str result = str_make ();
- str_append (&result, mask);
-
- // Make sure it is a complete mask
- if (!strchr (result.str, '!'))
- str_append (&result, "!*");
- if (!strchr (result.str, '@'))
- str_append (&result, "@*");
-
- // And validate whatever the result is
- if (!irc_is_valid_user_mask (result.str))
- {
- str_free (&result);
- return NULL;
- }
- return str_steal (&result);
-}
-
-// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-// Channel MODE command handling. This is by far the worst command to implement
-// from the whole RFC; don't blame me if it doesn't work exactly as expected.
-
-struct mode_processor
-{
- // Inputs to set after initialization:
-
- char **params; ///< Mode string parameters
-
- struct client *c; ///< Who does the changes
- struct channel *channel; ///< The channel we're modifying
- struct channel_user *user; ///< Presence of the client in the chan
-
- // Internals:
-
- bool adding; ///< Currently adding modes
- char mode_char; ///< Currently processed mode char
-
- struct str added; ///< Added modes
- struct str removed; ///< Removed modes
-
- struct strv added_params; ///< Params for added modes
- struct strv removed_params; ///< Params for removed modes
-
- struct str *output; ///< "added" or "removed"
- struct strv *output_params; ///< Similarly for "*_params"
-};
-
-static struct mode_processor
-mode_processor_make (void)
-{
- return (struct mode_processor)
- {
- .added = str_make (), .added_params = strv_make (),
- .removed = str_make (), .removed_params = strv_make (),
- };
-}
-
-static void
-mode_processor_free (struct mode_processor *self)
-{
- str_free (&self->added);
- str_free (&self->removed);
-
- strv_free (&self->added_params);
- strv_free (&self->removed_params);
-}
-
-static const char *
-mode_processor_next_param (struct mode_processor *self)
-{
- if (!*self->params)
- return NULL;
- return *self->params++;
-}
-
-static bool
-mode_processor_check_operator (struct mode_processor *self)
-{
- if ((self->user && (self->user->modes & IRC_CHAN_MODE_OPERATOR))
- || (self->c->mode & IRC_USER_MODE_OPERATOR))
- return true;
-
- irc_send_reply (self->c, IRC_ERR_CHANOPRIVSNEEDED, self->channel->name);
- return false;
-}
-
-static void
-mode_processor_do_user (struct mode_processor *self, int mode)
-{
- const char *target = mode_processor_next_param (self);
- if (!mode_processor_check_operator (self) || !target)
- return;
-
- struct client *client;
- struct channel_user *target_user;
- if (!(client = str_map_find (&self->c->ctx->users, target)))
- irc_send_reply (self->c, IRC_ERR_NOSUCHNICK, target);
- else if (!(target_user = channel_get_user (self->channel, client)))
- irc_send_reply (self->c, IRC_ERR_USERNOTINCHANNEL,
- target, self->channel->name);
- else if (irc_modify_mode (&target_user->modes, mode, self->adding))
- {
- str_append_c (self->output, self->mode_char);
- strv_append (self->output_params, client->nickname);
- }
-}
-
-static bool
-mode_processor_do_chan (struct mode_processor *self, int mode)
-{
- if (!mode_processor_check_operator (self)
- || !irc_modify_mode (&self->channel->modes, mode, self->adding))
- return false;
-
- str_append_c (self->output, self->mode_char);
- return true;
-}
-
-static void
-mode_processor_do_chan_remove
- (struct mode_processor *self, char mode_char, int mode)
-{
- if (self->adding
- && irc_modify_mode (&self->channel->modes, mode, false))
- str_append_c (&self->removed, mode_char);
-}
-
-static void
-mode_processor_do_list (struct mode_processor *self,
- struct strv *list, int list_msg, int end_msg)
-{
- const char *target = mode_processor_next_param (self);
- if (!target)
- {
- if (self->adding)
- irc_send_channel_list (self->c, self->channel->name,
- list, list_msg, end_msg);
- return;
- }
-
- if (!mode_processor_check_operator (self))
- return;
-
- char *mask = irc_check_expand_user_mask (target);
- if (!mask)
- return;
-
- size_t i;
- for (i = 0; i < list->len; i++)
- if (!irc_strcmp (list->vector[i], mask))
- break;
-
- bool found = i != list->len;
- if (found != self->adding)
- {
- if (self->adding)
- strv_append (list, mask);
- else
- strv_remove (list, i);
-
- str_append_c (self->output, self->mode_char);
- strv_append (self->output_params, mask);
- }
- free (mask);
-}
-
-static void
-mode_processor_do_key (struct mode_processor *self)
-{
- const char *target = mode_processor_next_param (self);
- if (!mode_processor_check_operator (self) || !target)
- return;
-
- if (!self->adding)
- {
- if (!self->channel->key || irc_strcmp (target, self->channel->key))
- return;
-
- str_append_c (&self->removed, self->mode_char);
- strv_append (&self->removed_params, self->channel->key);
- cstr_set (&self->channel->key, NULL);
- }
- else if (!irc_is_valid_key (target))
- // TODO: we should notify the user somehow
- return;
- else if (self->channel->key)
- irc_send_reply (self->c, IRC_ERR_KEYSET, self->channel->name);
- else
- {
- self->channel->key = xstrdup (target);
- str_append_c (&self->added, self->mode_char);
- strv_append (&self->added_params, self->channel->key);
- }
-}
-
-static void
-mode_processor_do_limit (struct mode_processor *self)
-{
- if (!mode_processor_check_operator (self))
- return;
-
- const char *target;
- if (!self->adding)
- {
- if (self->channel->user_limit == -1)
- return;
-
- self->channel->user_limit = -1;
- str_append_c (&self->removed, self->mode_char);
- }
- else if ((target = mode_processor_next_param (self)))
- {
- unsigned long x;
- if (xstrtoul (&x, target, 10) && x > 0 && x <= LONG_MAX)
- {
- self->channel->user_limit = x;
- str_append_c (&self->added, self->mode_char);
- strv_append (&self->added_params, target);
- }
- }
-}
-
-static bool
-mode_processor_step (struct mode_processor *self, char mode_char)
-{
- switch ((self->mode_char = mode_char))
- {
- case '+':
- self->adding = true;
- self->output = &self->added;
- self->output_params = &self->added_params;
- break;
- case '-':
- self->adding = false;
- self->output = &self->removed;
- self->output_params = &self->removed_params;
- break;
-
-#define USER(mode) mode_processor_do_user (self, (mode))
-#define CHAN(mode) mode_processor_do_chan (self, (mode))
-
- case 'o': USER (IRC_CHAN_MODE_OPERATOR); break;
- case 'v': USER (IRC_CHAN_MODE_VOICE); break;
-
- case 'i': CHAN (IRC_CHAN_MODE_INVITE_ONLY); break;
- case 'm': CHAN (IRC_CHAN_MODE_MODERATED); break;
- case 'n': CHAN (IRC_CHAN_MODE_NO_OUTSIDE_MSGS); break;
- case 'q': CHAN (IRC_CHAN_MODE_QUIET); break;
- case 't': CHAN (IRC_CHAN_MODE_PROTECTED_TOPIC); break;
-
- case 'p':
- if (CHAN (IRC_CHAN_MODE_PRIVATE))
- mode_processor_do_chan_remove (self, 's', IRC_CHAN_MODE_SECRET);
- break;
- case 's':
- if (CHAN (IRC_CHAN_MODE_SECRET))
- mode_processor_do_chan_remove (self, 'p', IRC_CHAN_MODE_PRIVATE);
- break;
-
-#undef USER
-#undef CHAN
-
- case 'b':
- mode_processor_do_list (self, &self->channel->ban_list,
- IRC_RPL_BANLIST, IRC_RPL_ENDOFBANLIST);
- break;
- case 'e':
- mode_processor_do_list (self, &self->channel->exception_list,
- IRC_RPL_EXCEPTLIST, IRC_RPL_ENDOFEXCEPTLIST);
- break;
- case 'I':
- mode_processor_do_list (self, &self->channel->invite_list,
- IRC_RPL_INVITELIST, IRC_RPL_ENDOFINVITELIST);
- break;
-
- case 'k':
- mode_processor_do_key (self);
- break;
- case 'l':
- mode_processor_do_limit (self);
- break;
-
- default:
- // It's not safe to continue, results could be undesired
- irc_send_reply (self->c, IRC_ERR_UNKNOWNMODE,
- mode_char, self->channel->name);
- return false;
- }
- return true;
-}
-
-static void
-irc_handle_chan_mode_change
- (struct client *c, struct channel *chan, char *params[])
-{
- struct mode_processor p = mode_processor_make ();
- p.params = params;
- p.channel = chan;
- p.c = c;
- p.user = channel_get_user (chan, c);
-
- const char *mode_string;
- while ((mode_string = mode_processor_next_param (&p)))
- {
- mode_processor_step (&p, '+');
- while (*mode_string)
- if (!mode_processor_step (&p, *mode_string++))
- goto done_processing;
- }
-
- // TODO: limit to three changes with parameter per command
-done_processing:
- if (p.added.len || p.removed.len)
- {
- struct str message = str_make ();
- str_append_printf (&message, ":%s!%s@%s MODE %s ",
- p.c->nickname, p.c->username, p.c->hostname,
- p.channel->name);
- if (p.added.len)
- str_append_printf (&message, "+%s", p.added.str);
- if (p.removed.len)
- str_append_printf (&message, "-%s", p.removed.str);
- for (size_t i = 0; i < p.added_params.len; i++)
- str_append_printf (&message, " %s", p.added_params.vector[i]);
- for (size_t i = 0; i < p.removed_params.len; i++)
- str_append_printf (&message, " %s", p.removed_params.vector[i]);
- irc_channel_multicast (p.channel, message.str, NULL);
- str_free (&message);
- }
- mode_processor_free (&p);
-}
-
-// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-static void
-irc_handle_mode (const struct irc_message *msg, struct client *c)
-{
- if (msg->params.len < 1)
- RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
-
- const char *target = msg->params.vector[0];
- struct client *client = str_map_find (&c->ctx->users, target);
- struct channel *chan = str_map_find (&c->ctx->channels, target);
-
- if (client)
- {
- if (irc_strcmp (target, c->nickname))
- RETURN_WITH_REPLY (c, IRC_ERR_USERSDONTMATCH);
-
- if (msg->params.len < 2)
- {
- char *mode = client_get_mode (client);
- irc_send_reply (c, IRC_RPL_UMODEIS, mode);
- free (mode);
- }
- else
- irc_handle_user_mode_change (c, msg->params.vector[1]);
- }
- else if (chan)
- {
- if (msg->params.len < 2)
- {
- char *mode = channel_get_mode (chan, channel_get_user (chan, c));
- irc_send_reply (c, IRC_RPL_CHANNELMODEIS, target, mode);
- irc_send_reply (c, IRC_RPL_CREATIONTIME,
- target, (long long) chan->created);
- free (mode);
- }
- else
- irc_handle_chan_mode_change (c, chan, &msg->params.vector[1]);
- }
- else
- irc_send_reply (c, IRC_ERR_NOSUCHNICK, target);
-}
-
-static void
-irc_handle_user_message (const struct irc_message *msg, struct client *c,
- const char *command, bool allow_away_reply)
-{
- if (msg->params.len < 1)
- RETURN_WITH_REPLY (c, IRC_ERR_NORECIPIENT, msg->command);
- if (msg->params.len < 2 || !*msg->params.vector[1])
- RETURN_WITH_REPLY (c, IRC_ERR_NOTEXTTOSEND);
-
- const char *target = msg->params.vector[0];
- const char *text = msg->params.vector[1];
- struct client *client = str_map_find (&c->ctx->users, target);
- if (client)
- {
- client_send (client, ":%s!%s@%s %s %s :%s",
- c->nickname, c->username, c->hostname, command, target, text);
- if (allow_away_reply && client->away_message)
- irc_send_reply (c, IRC_RPL_AWAY, target, client->away_message);
-
- // Acknowledging a message from the client to itself would be silly
- if (client != c && (c->caps_enabled & IRC_CAP_ECHO_MESSAGE))
- client_send (c, ":%s!%s@%s %s %s :%s",
- c->nickname, c->username, c->hostname, command, target, text);
- return;
- }
-
- struct channel *chan = str_map_find (&c->ctx->channels, target);
- if (chan)
- {
- struct channel_user *user = channel_get_user (chan, c);
- if ((chan->modes & IRC_CHAN_MODE_NO_OUTSIDE_MSGS) && !user)
- RETURN_WITH_REPLY (c, IRC_ERR_CANNOTSENDTOCHAN, target);
- if ((chan->modes & IRC_CHAN_MODE_MODERATED) && (!user ||
- !(user->modes & (IRC_CHAN_MODE_VOICE | IRC_CHAN_MODE_OPERATOR))))
- RETURN_WITH_REPLY (c, IRC_ERR_CANNOTSENDTOCHAN, target);
- if (client_in_mask_list (c, &chan->ban_list)
- && !client_in_mask_list (c, &chan->exception_list))
- RETURN_WITH_REPLY (c, IRC_ERR_CANNOTSENDTOCHAN, target);
-
- char *message = xstrdup_printf (":%s!%s@%s %s %s :%s",
- c->nickname, c->username, c->hostname, command, target, text);
- irc_channel_multicast (chan, message,
- (c->caps_enabled & IRC_CAP_ECHO_MESSAGE) ? NULL : c);
- free (message);
- return;
- }
-
- irc_send_reply (c, IRC_ERR_NOSUCHNICK, target);
-}
-
-static void
-irc_handle_privmsg (const struct irc_message *msg, struct client *c)
-{
- irc_handle_user_message (msg, c, "PRIVMSG", true);
- // Let's not care too much about success or failure
- c->last_active = time (NULL);
-}
-
-static void
-irc_handle_notice (const struct irc_message *msg, struct client *c)
-{
- irc_handle_user_message (msg, c, "NOTICE", false);
-}
-
-static void
-irc_send_rpl_list (struct client *c, const struct channel *chan)
-{
- int visible = 0;
- for (struct channel_user *user = chan->users;
- user; user = user->next)
- // XXX: maybe we should skip IRC_USER_MODE_INVISIBLE
- visible++;
-
- irc_send_reply (c, IRC_RPL_LIST, chan->name, visible, chan->topic);
-}
-
-static void
-irc_handle_list (const struct irc_message *msg, struct client *c)
-{
- if (msg->params.len > 1 && !irc_is_this_me (c->ctx, msg->params.vector[1]))
- RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHSERVER, msg->params.vector[1]);
-
- struct channel *chan;
- if (msg->params.len == 0)
- {
- struct str_map_iter iter = str_map_iter_make (&c->ctx->channels);
- while ((chan = str_map_iter_next (&iter)))
- if (!(chan->modes & (IRC_CHAN_MODE_PRIVATE | IRC_CHAN_MODE_SECRET))
- || channel_get_user (chan, c))
- irc_send_rpl_list (c, chan);
- }
- else
- {
- struct strv channels = strv_make ();
- cstr_split (msg->params.vector[0], ",", true, &channels);
- for (size_t i = 0; i < channels.len; i++)
- if ((chan = str_map_find (&c->ctx->channels, channels.vector[i]))
- && (!(chan->modes & IRC_CHAN_MODE_SECRET)
- || channel_get_user (chan, c)))
- irc_send_rpl_list (c, chan);
- strv_free (&channels);
- }
- irc_send_reply (c, IRC_RPL_LISTEND);
-}
-
-static void
-irc_append_prefixes (struct client *c, struct channel_user *user,
- struct str *output)
-{
- struct str prefixes = str_make ();
- if (user->modes & IRC_CHAN_MODE_OPERATOR) str_append_c (&prefixes, '@');
- if (user->modes & IRC_CHAN_MODE_VOICE) str_append_c (&prefixes, '+');
-
- if (prefixes.len)
- {
- if (c->caps_enabled & IRC_CAP_MULTI_PREFIX)
- str_append (output, prefixes.str);
- else
- str_append_c (output, prefixes.str[0]);
- }
- str_free (&prefixes);
-}
-
-static char *
-irc_make_rpl_namreply_item
- (struct client *c, struct client *target, struct channel_user *user)
-{
- struct str result = str_make ();
-
- if (user)
- irc_append_prefixes (c, user, &result);
-
- str_append (&result, target->nickname);
- if (c->caps_enabled & IRC_CAP_USERHOST_IN_NAMES)
- str_append_printf (&result,
- "!%s@%s", target->username, target->hostname);
- return str_steal (&result);
-}
-
-static void
-irc_send_rpl_namreply (struct client *c, const struct channel *chan,
- struct str_map *used_nicks)
-{
- char type = '=';
- if (chan->modes & IRC_CHAN_MODE_SECRET)
- type = '@';
- else if (chan->modes & IRC_CHAN_MODE_PRIVATE)
- type = '*';
-
- bool on_channel = channel_get_user (chan, c);
- struct strv nicks = strv_make ();
- for (struct channel_user *iter = chan->users; iter; iter = iter->next)
- {
- if (!on_channel && (iter->c->mode & IRC_USER_MODE_INVISIBLE))
- continue;
- if (used_nicks)
- str_map_set (used_nicks, iter->c->nickname, (void *) 1);
- strv_append_owned (&nicks,
- irc_make_rpl_namreply_item (c, iter->c, iter));
- }
-
- irc_send_reply_vector (c, IRC_RPL_NAMREPLY,
- nicks.vector, type, chan->name, "");
- strv_free (&nicks);
-}
-
-static void
-irc_send_disassociated_names (struct client *c, struct str_map *used)
-{
- struct strv nicks = strv_make ();
- struct str_map_iter iter = str_map_iter_make (&c->ctx->users);
- struct client *target;
- while ((target = str_map_iter_next (&iter)))
- {
- if ((target->mode & IRC_USER_MODE_INVISIBLE)
- || str_map_find (used, target->nickname))
- continue;
- strv_append_owned (&nicks,
- irc_make_rpl_namreply_item (c, target, NULL));
- }
-
- if (nicks.len)
- irc_send_reply_vector (c, IRC_RPL_NAMREPLY,
- nicks.vector, '*', "*", "");
- strv_free (&nicks);
-}
-
-static void
-irc_handle_names (const struct irc_message *msg, struct client *c)
-{
- if (msg->params.len > 1 && !irc_is_this_me (c->ctx, msg->params.vector[1]))
- RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHSERVER, msg->params.vector[1]);
-
- struct channel *chan;
- if (msg->params.len == 0)
- {
- struct str_map used = str_map_make (NULL);
- used.key_xfrm = irc_strxfrm;
-
- struct str_map_iter iter = str_map_iter_make (&c->ctx->channels);
- while ((chan = str_map_iter_next (&iter)))
- if (!(chan->modes & (IRC_CHAN_MODE_PRIVATE | IRC_CHAN_MODE_SECRET))
- || channel_get_user (chan, c))
- irc_send_rpl_namreply (c, chan, &used);
-
- // Also send all visible users we haven't listed yet
- irc_send_disassociated_names (c, &used);
- str_map_free (&used);
-
- irc_send_reply (c, IRC_RPL_ENDOFNAMES, "*");
- }
- else
- {
- struct strv channels = strv_make ();
- cstr_split (msg->params.vector[0], ",", true, &channels);
- for (size_t i = 0; i < channels.len; i++)
- if ((chan = str_map_find (&c->ctx->channels, channels.vector[i]))
- && (!(chan->modes & IRC_CHAN_MODE_SECRET)
- || channel_get_user (chan, c)))
- {
- irc_send_rpl_namreply (c, chan, NULL);
- irc_send_reply (c, IRC_RPL_ENDOFNAMES, channels.vector[i]);
- }
- strv_free (&channels);
- }
-}
-
-static void
-irc_send_rpl_whoreply (struct client *c, const struct channel *chan,
- const struct client *target)
-{
- struct str chars = str_make ();
- str_append_c (&chars, target->away_message ? 'G' : 'H');
- if (target->mode & IRC_USER_MODE_OPERATOR)
- str_append_c (&chars, '*');
-
- struct channel_user *user;
- if (chan && (user = channel_get_user (chan, target)))
- irc_append_prefixes (c, user, &chars);
-
- irc_send_reply (c, IRC_RPL_WHOREPLY, chan ? chan->name : "*",
- target->username, target->hostname, target->ctx->server_name,
- target->nickname, chars.str, 0 /* hop count */, target->realname);
- str_free (&chars);
-}
-
-static void
-irc_match_send_rpl_whoreply (struct client *c, struct client *target,
- const char *mask)
-{
- bool is_roommate = false;
- struct str_map_iter iter = str_map_iter_make (&c->ctx->channels);
- struct channel *chan;
- while ((chan = str_map_iter_next (&iter)))
- if (channel_get_user (chan, target) && channel_get_user (chan, c))
- {
- is_roommate = true;
- break;
- }
- if ((target->mode & IRC_USER_MODE_INVISIBLE) && !is_roommate)
- return;
-
- if (irc_fnmatch (mask, target->hostname)
- && irc_fnmatch (mask, target->nickname)
- && irc_fnmatch (mask, target->realname)
- && irc_fnmatch (mask, c->ctx->server_name))
- return;
-
- // Try to find a channel they're on that's visible to us
- struct channel *user_chan = NULL;
- iter = str_map_iter_make (&c->ctx->channels);
- while ((chan = str_map_iter_next (&iter)))
- if (channel_get_user (chan, target)
- && (!(chan->modes & (IRC_CHAN_MODE_PRIVATE | IRC_CHAN_MODE_SECRET))
- || channel_get_user (chan, c)))
- {
- user_chan = chan;
- break;
- }
- irc_send_rpl_whoreply (c, user_chan, target);
-}
-
-static void
-irc_handle_who (const struct irc_message *msg, struct client *c)
-{
- bool only_ops = msg->params.len > 1 && !strcmp (msg->params.vector[1], "o");
-
- const char *shown_mask = msg->params.vector[0], *used_mask;
- if (!shown_mask)
- used_mask = shown_mask = "*";
- else if (!strcmp (shown_mask, "0"))
- used_mask = "*";
- else
- used_mask = shown_mask;
-
- struct channel *chan;
- if ((chan = str_map_find (&c->ctx->channels, used_mask)))
- {
- bool on_chan = !!channel_get_user (chan, c);
- if (on_chan || !(chan->modes & IRC_CHAN_MODE_SECRET))
- for (struct channel_user *iter = chan->users;
- iter; iter = iter->next)
- {
- if ((on_chan || !(iter->c->mode & IRC_USER_MODE_INVISIBLE))
- && (!only_ops || (iter->c->mode & IRC_USER_MODE_OPERATOR)))
- irc_send_rpl_whoreply (c, chan, iter->c);
- }
- }
- else
- {
- struct str_map_iter iter = str_map_iter_make (&c->ctx->users);
- struct client *target;
- while ((target = str_map_iter_next (&iter)))
- if (!only_ops || (target->mode & IRC_USER_MODE_OPERATOR))
- irc_match_send_rpl_whoreply (c, target, used_mask);
- }
- irc_send_reply (c, IRC_RPL_ENDOFWHO, shown_mask);
-}
-
-static void
-irc_send_whois_reply (struct client *c, const struct client *target)
-{
- const char *nick = target->nickname;
- irc_send_reply (c, IRC_RPL_WHOISUSER, nick,
- target->username, target->hostname, target->realname);
- irc_send_reply (c, IRC_RPL_WHOISSERVER, nick, target->ctx->server_name,
- str_map_find (&c->ctx->config, "server_info"));
- if (target->mode & IRC_USER_MODE_OPERATOR)
- irc_send_reply (c, IRC_RPL_WHOISOPERATOR, nick);
- irc_send_reply (c, IRC_RPL_WHOISIDLE, nick,
- (int) (time (NULL) - target->last_active));
- if (target->away_message)
- irc_send_reply (c, IRC_RPL_AWAY, nick, target->away_message);
-
- struct strv channels = strv_make ();
-
- struct str_map_iter iter = str_map_iter_make (&c->ctx->channels);
- struct channel *chan;
- struct channel_user *channel_user;
- while ((chan = str_map_iter_next (&iter)))
- if ((channel_user = channel_get_user (chan, target))
- && (!(chan->modes & (IRC_CHAN_MODE_PRIVATE | IRC_CHAN_MODE_SECRET))
- || channel_get_user (chan, c)))
- {
- struct str item = str_make ();
- if (channel_user->modes & IRC_CHAN_MODE_OPERATOR)
- str_append_c (&item, '@');
- else if (channel_user->modes & IRC_CHAN_MODE_VOICE)
- str_append_c (&item, '+');
- str_append (&item, chan->name);
- strv_append_owned (&channels, str_steal (&item));
- }
-
- irc_send_reply_vector (c, IRC_RPL_WHOISCHANNELS, channels.vector, nick, "");
- strv_free (&channels);
-
- irc_send_reply (c, IRC_RPL_ENDOFWHOIS, nick);
-}
-
-static void
-irc_handle_whois (const struct irc_message *msg, struct client *c)
-{
- if (msg->params.len < 1)
- RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
- if (msg->params.len > 1 && !irc_is_this_me (c->ctx, msg->params.vector[0]))
- RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHSERVER, msg->params.vector[0]);
-
- struct strv masks = strv_make ();
- const char *masks_str = msg->params.vector[msg->params.len > 1];
- cstr_split (masks_str, ",", true, &masks);
- for (size_t i = 0; i < masks.len; i++)
- {
- const char *mask = masks.vector[i];
- struct client *target;
- if (!strpbrk (mask, "*?"))
- {
- if (!(target = str_map_find (&c->ctx->users, mask)))
- irc_send_reply (c, IRC_ERR_NOSUCHNICK, mask);
- else
- irc_send_whois_reply (c, target);
- }
- else
- {
- struct str_map_iter iter = str_map_iter_make (&c->ctx->users);
- bool found = false;
- while ((target = str_map_iter_next (&iter)))
- if (!irc_fnmatch (mask, target->nickname))
- {
- irc_send_whois_reply (c, target);
- found = true;
- }
- if (!found)
- irc_send_reply (c, IRC_ERR_NOSUCHNICK, mask);
- }
- }
- strv_free (&masks);
-}
-
-static void
-irc_handle_whowas (const struct irc_message *msg, struct client *c)
-{
- if (msg->params.len < 1)
- RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
- if (msg->params.len > 2 && !irc_is_this_me (c->ctx, msg->params.vector[2]))
- RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHSERVER, msg->params.vector[2]);
- // The "count" parameter is ignored, we only store one entry for a nick
-
- struct strv nicks = strv_make ();
- cstr_split (msg->params.vector[0], ",", true, &nicks);
-
- for (size_t i = 0; i < nicks.len; i++)
- {
- const char *nick = nicks.vector[i];
- struct whowas_info *info = str_map_find (&c->ctx->whowas, nick);
- if (!info)
- irc_send_reply (c, IRC_ERR_WASNOSUCHNICK, nick);
- else
- {
- irc_send_reply (c, IRC_RPL_WHOWASUSER, nick,
- info->username, info->hostname, info->realname);
- irc_send_reply (c, IRC_RPL_WHOISSERVER, nick, c->ctx->server_name,
- str_map_find (&c->ctx->config, "server_info"));
- }
- irc_send_reply (c, IRC_RPL_ENDOFWHOWAS, nick);
- }
- strv_free (&nicks);
-}
-
-static void
-irc_send_rpl_topic (struct client *c, struct channel *chan)
-{
- if (!*chan->topic)
- irc_send_reply (c, IRC_RPL_NOTOPIC, chan->name);
- else
- {
- irc_send_reply (c, IRC_RPL_TOPIC, chan->name, chan->topic);
- irc_send_reply (c, IRC_RPL_TOPICWHOTIME,
- chan->name, chan->topic_who, (long long) chan->topic_time);
- }
-}
-
-static void
-irc_handle_topic (const struct irc_message *msg, struct client *c)
-{
- if (msg->params.len < 1)
- RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
-
- const char *target = msg->params.vector[0];
- struct channel *chan = str_map_find (&c->ctx->channels, target);
- if (!chan)
- RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHCHANNEL, target);
-
- if (msg->params.len < 2)
- {
- irc_send_rpl_topic (c, chan);
- return;
- }
-
- struct channel_user *user = channel_get_user (chan, c);
- if (!user)
- RETURN_WITH_REPLY (c, IRC_ERR_NOTONCHANNEL, target);
-
- if ((chan->modes & IRC_CHAN_MODE_PROTECTED_TOPIC)
- && !(user->modes & IRC_CHAN_MODE_OPERATOR))
- RETURN_WITH_REPLY (c, IRC_ERR_CHANOPRIVSNEEDED, target);
-
- cstr_set (&chan->topic, xstrdup (msg->params.vector[1]));
- cstr_set (&chan->topic_who, xstrdup_printf
- ("%s!%s@%s", c->nickname, c->username, c->hostname));
- chan->topic_time = time (NULL);
-
- char *message = xstrdup_printf (":%s!%s@%s TOPIC %s :%s",
- c->nickname, c->username, c->hostname, target, chan->topic);
- irc_channel_multicast (chan, message, NULL);
- free (message);
-}
-
-static void
-irc_try_part (struct client *c, const char *channel_name, const char *reason)
-{
- if (!reason)
- reason = c->nickname;
-
- struct channel *chan;
- if (!(chan = str_map_find (&c->ctx->channels, channel_name)))
- RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHCHANNEL, channel_name);
-
- struct channel_user *user;
- if (!(user = channel_get_user (chan, c)))
- RETURN_WITH_REPLY (c, IRC_ERR_NOTONCHANNEL, channel_name);
-
- char *message = xstrdup_printf (":%s!%s@%s PART %s :%s",
- c->nickname, c->username, c->hostname, channel_name, reason);
- if (!(chan->modes & IRC_CHAN_MODE_QUIET))
- irc_channel_multicast (chan, message, NULL);
- else
- client_send (c, "%s", message);
- free (message);
-
- channel_remove_user (chan, user);
- irc_channel_destroy_if_empty (c->ctx, chan);
-}
-
-static void
-irc_part_all_channels (struct client *c)
-{
- // We have to be careful here, the channel might get destroyed
- struct str_map_unset_iter iter =
- str_map_unset_iter_make (&c->ctx->channels);
-
- struct channel *chan;
- while ((chan = str_map_unset_iter_next (&iter)))
- if (channel_get_user (chan, c))
- irc_try_part (c, chan->name, NULL);
- str_map_unset_iter_free (&iter);
-}
-
-static void
-irc_handle_part (const struct irc_message *msg, struct client *c)
-{
- if (msg->params.len < 1)
- RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
-
- const char *reason = msg->params.len > 1 ? msg->params.vector[1] : NULL;
- struct strv channels = strv_make ();
- cstr_split (msg->params.vector[0], ",", true, &channels);
- for (size_t i = 0; i < channels.len; i++)
- irc_try_part (c, channels.vector[i], reason);
- strv_free (&channels);
-}
-
-static void
-irc_try_kick (struct client *c, const char *channel_name, const char *nick,
- const char *reason)
-{
- struct channel *chan;
- if (!(chan = str_map_find (&c->ctx->channels, channel_name)))
- RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHCHANNEL, channel_name);
-
- struct channel_user *user;
- if (!(user = channel_get_user (chan, c)))
- RETURN_WITH_REPLY (c, IRC_ERR_NOTONCHANNEL, channel_name);
- if (!(user->modes & IRC_CHAN_MODE_OPERATOR))
- RETURN_WITH_REPLY (c, IRC_ERR_CHANOPRIVSNEEDED, channel_name);
-
- struct client *client;
- if (!(client = str_map_find (&c->ctx->users, nick))
- || !(user = channel_get_user (chan, client)))
- RETURN_WITH_REPLY (c, IRC_ERR_USERNOTINCHANNEL, nick, channel_name);
-
- char *message = xstrdup_printf (":%s!%s@%s KICK %s %s :%s",
- c->nickname, c->username, c->hostname, channel_name, nick, reason);
- if (!(chan->modes & IRC_CHAN_MODE_QUIET))
- irc_channel_multicast (chan, message, NULL);
- else
- client_send (c, "%s", message);
- free (message);
-
- channel_remove_user (chan, user);
- irc_channel_destroy_if_empty (c->ctx, chan);
-}
-
-static void
-irc_handle_kick (const struct irc_message *msg, struct client *c)
-{
- if (msg->params.len < 2)
- RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
-
- const char *reason = c->nickname;
- if (msg->params.len > 2)
- reason = msg->params.vector[2];
-
- struct strv channels = strv_make ();
- cstr_split (msg->params.vector[0], ",", true, &channels);
- struct strv users = strv_make ();
- cstr_split (msg->params.vector[1], ",", true, &users);
-
- if (channels.len == 1)
- for (size_t i = 0; i < users.len; i++)
- irc_try_kick (c, channels.vector[0], users.vector[i], reason);
- else
- for (size_t i = 0; i < channels.len && i < users.len; i++)
- irc_try_kick (c, channels.vector[i], users.vector[i], reason);
-
- strv_free (&channels);
- strv_free (&users);
-}
-
-static void
-irc_send_invite_notifications
- (struct channel *chan, struct client *c, struct client *target)
-{
- for (struct channel_user *iter = chan->users; iter; iter = iter->next)
- if (iter->c != target && iter->c->caps_enabled & IRC_CAP_INVITE_NOTIFY)
- client_send (iter->c, ":%s!%s@%s INVITE %s %s",
- c->nickname, c->username, c->hostname,
- target->nickname, chan->name);
-}
-
-static void
-irc_handle_invite (const struct irc_message *msg, struct client *c)
-{
- if (msg->params.len < 2)
- RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
-
- const char *target = msg->params.vector[0];
- const char *channel_name = msg->params.vector[1];
-
- struct client *client = str_map_find (&c->ctx->users, target);
- if (!client)
- RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHNICK, target);
-
- struct channel *chan = str_map_find (&c->ctx->channels, channel_name);
- if (chan)
- {
- struct channel_user *inviting_user;
- if (!(inviting_user = channel_get_user (chan, c)))
- RETURN_WITH_REPLY (c, IRC_ERR_NOTONCHANNEL, channel_name);
- if (channel_get_user (chan, client))
- RETURN_WITH_REPLY (c, IRC_ERR_USERONCHANNEL, target, channel_name);
-
- if ((inviting_user->modes & IRC_CHAN_MODE_OPERATOR))
- str_map_set (&client->invites, channel_name, (void *) 1);
- else if ((chan->modes & IRC_CHAN_MODE_INVITE_ONLY))
- RETURN_WITH_REPLY (c, IRC_ERR_CHANOPRIVSNEEDED, channel_name);
-
- // It's not specified when and how we should send out invite-notify
- if (chan->modes & IRC_CHAN_MODE_INVITE_ONLY)
- irc_send_invite_notifications (chan, c, client);
- }
-
- client_send (client, ":%s!%s@%s INVITE %s %s",
- c->nickname, c->username, c->hostname, client->nickname, channel_name);
- if (client->away_message)
- irc_send_reply (c, IRC_RPL_AWAY,
- client->nickname, client->away_message);
- irc_send_reply (c, IRC_RPL_INVITING, client->nickname, channel_name);
-}
-
-static void
-irc_try_join (struct client *c, const char *channel_name, const char *key)
-{
- struct channel *chan = str_map_find (&c->ctx->channels, channel_name);
- unsigned user_mode = 0;
- if (!chan)
- {
- if (irc_validate_channel_name (channel_name) != VALIDATION_OK)
- RETURN_WITH_REPLY (c, IRC_ERR_BADCHANMASK, channel_name);
- chan = irc_channel_create (c->ctx, channel_name);
- user_mode = IRC_CHAN_MODE_OPERATOR;
- }
- else if (channel_get_user (chan, c))
- return;
-
- bool invited_by_chanop = !!str_map_find (&c->invites, channel_name);
- if ((chan->modes & IRC_CHAN_MODE_INVITE_ONLY)
- && !client_in_mask_list (c, &chan->invite_list)
- && !invited_by_chanop)
- RETURN_WITH_REPLY (c, IRC_ERR_INVITEONLYCHAN, channel_name);
- if (chan->key && (!key || strcmp (key, chan->key)))
- RETURN_WITH_REPLY (c, IRC_ERR_BADCHANNELKEY, channel_name);
- if (chan->user_limit != -1
- && channel_user_count (chan) >= (size_t) chan->user_limit)
- RETURN_WITH_REPLY (c, IRC_ERR_CHANNELISFULL, channel_name);
- if (client_in_mask_list (c, &chan->ban_list)
- && !client_in_mask_list (c, &chan->exception_list)
- && !invited_by_chanop)
- RETURN_WITH_REPLY (c, IRC_ERR_BANNEDFROMCHAN, channel_name);
-
- // Destroy any invitation as there's no other way to get rid of it
- str_map_set (&c->invites, channel_name, NULL);
-
- channel_add_user (chan, c)->modes = user_mode;
-
- char *message = xstrdup_printf (":%s!%s@%s JOIN %s",
- c->nickname, c->username, c->hostname, channel_name);
- if (!(chan->modes & IRC_CHAN_MODE_QUIET))
- irc_channel_multicast (chan, message, NULL);
- else
- client_send (c, "%s", message);
- free (message);
-
- irc_send_rpl_topic (c, chan);
- irc_send_rpl_namreply (c, chan, NULL);
- irc_send_reply (c, IRC_RPL_ENDOFNAMES, chan->name);
-}
-
-static void
-irc_handle_join (const struct irc_message *msg, struct client *c)
-{
- if (msg->params.len < 1)
- RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
-
- if (!strcmp (msg->params.vector[0], "0"))
- {
- irc_part_all_channels (c);
- return;
- }
-
- struct strv channels = strv_make ();
- cstr_split (msg->params.vector[0], ",", true, &channels);
- struct strv keys = strv_make ();
- if (msg->params.len > 1)
- cstr_split (msg->params.vector[1], ",", true, &keys);
-
- for (size_t i = 0; i < channels.len; i++)
- irc_try_join (c, channels.vector[i],
- i < keys.len ? keys.vector[i] : NULL);
-
- strv_free (&channels);
- strv_free (&keys);
-}
-
-static void
-irc_handle_summon (const struct irc_message *msg, struct client *c)
-{
- (void) msg;
- irc_send_reply (c, IRC_ERR_SUMMONDISABLED);
-}
-
-static void
-irc_handle_users (const struct irc_message *msg, struct client *c)
-{
- (void) msg;
- irc_send_reply (c, IRC_ERR_USERSDISABLED);
-}
-
-static void
-irc_handle_away (const struct irc_message *msg, struct client *c)
-{
- if (msg->params.len < 1)
- {
- cstr_set (&c->away_message, NULL);
- irc_send_reply (c, IRC_RPL_UNAWAY);
- }
- else
- {
- cstr_set (&c->away_message, xstrdup (msg->params.vector[0]));
- irc_send_reply (c, IRC_RPL_NOWAWAY);
- }
-}
-
-static void
-irc_handle_ison (const struct irc_message *msg, struct client *c)
-{
- if (msg->params.len < 1)
- RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
-
- struct str result = str_make ();
- const char *nick;
- if (str_map_find (&c->ctx->users, (nick = msg->params.vector[0])))
- str_append (&result, nick);
- for (size_t i = 1; i < msg->params.len; i++)
- if (str_map_find (&c->ctx->users, (nick = msg->params.vector[i])))
- str_append_printf (&result, " %s", nick);
-
- irc_send_reply (c, IRC_RPL_ISON, result.str);
- str_free (&result);
-}
-
-static void
-irc_handle_admin (const struct irc_message *msg, struct client *c)
-{
- if (msg->params.len > 0 && !irc_is_this_me (c->ctx, msg->params.vector[0]))
- RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHSERVER, msg->params.vector[0]);
- irc_send_reply (c, IRC_ERR_NOADMININFO, c->ctx->server_name);
-}
-
-static void
-irc_handle_stats_links (struct client *c, const struct irc_message *msg)
-{
- // There is only an "l" query in RFC 2812 but we cannot link,
- // so instead we provide the "L" query giving information for all users
- const char *filter = NULL;
- if (msg->params.len > 1)
- filter = msg->params.vector[1];
-
- for (struct client *iter = c->ctx->clients; iter; iter = iter->next)
- {
- if (filter && irc_strcmp (iter->nickname, filter))
- continue;
- irc_send_reply (c, IRC_RPL_STATSLINKINFO,
- iter->address, // linkname
- iter->write_buffer.len, // sendq
- iter->n_sent_messages, iter->sent_bytes / 1024,
- iter->n_received_messages, iter->received_bytes / 1024,
- (long long) (time (NULL) - iter->opened));
- }
-}
-
-static void
-irc_handle_stats_commands (struct client *c)
-{
- struct str_map_iter iter = str_map_iter_make (&c->ctx->handlers);
- struct irc_command *handler;
- while ((handler = str_map_iter_next (&iter)))
- {
- if (!handler->n_received)
- continue;
- irc_send_reply (c, IRC_RPL_STATSCOMMANDS, handler->name,
- handler->n_received, handler->bytes_received, (size_t) 0);
- }
-}
-
-static void
-irc_handle_stats_uptime (struct client *c)
-{
- time_t uptime = time (NULL) - c->ctx->started;
-
- int days = uptime / 60 / 60 / 24;
- int hours = (uptime % (60 * 60 * 24)) / 60 / 60;
- int mins = (uptime % (60 * 60)) / 60;
- int secs = uptime % 60;
-
- irc_send_reply (c, IRC_RPL_STATSUPTIME, days, hours, mins, secs);
-}
-
-static void
-irc_handle_stats (const struct irc_message *msg, struct client *c)
-{
- char query = 0;
- if (msg->params.len > 0)
- query = *msg->params.vector[0];
- if (msg->params.len > 1 && !irc_is_this_me (c->ctx, msg->params.vector[1]))
- RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHSERVER, msg->params.vector[1]);
- if (!(c->mode & IRC_USER_MODE_OPERATOR))
- RETURN_WITH_REPLY (c, IRC_ERR_NOPRIVILEGES);
-
- switch (query)
- {
- case 'L': irc_handle_stats_links (c, msg); break;
- case 'm': irc_handle_stats_commands (c); break;
- case 'u': irc_handle_stats_uptime (c); break;
- }
-
- irc_send_reply (c, IRC_RPL_ENDOFSTATS, query ? query : '*');
-}
-
-static void
-irc_handle_links (const struct irc_message *msg, struct client *c)
-{
- if (msg->params.len > 1 && !irc_is_this_me (c->ctx, msg->params.vector[0]))
- RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHSERVER, msg->params.vector[0]);
-
- const char *mask = "*";
- if (msg->params.len > 0)
- mask = msg->params.vector[msg->params.len > 1];
-
- if (!irc_fnmatch (mask, c->ctx->server_name))
- irc_send_reply (c, IRC_RPL_LINKS, mask,
- c->ctx->server_name, 0 /* hop count */,
- str_map_find (&c->ctx->config, "server_info"));
- irc_send_reply (c, IRC_RPL_ENDOFLINKS, mask);
-}
-
-static void
-irc_handle_kill (const struct irc_message *msg, struct client *c)
-{
- if (msg->params.len < 2)
- RETURN_WITH_REPLY (c, IRC_ERR_NEEDMOREPARAMS, msg->command);
- if (!(c->mode & IRC_USER_MODE_OPERATOR))
- RETURN_WITH_REPLY (c, IRC_ERR_NOPRIVILEGES);
-
- struct client *target;
- if (!(target = str_map_find (&c->ctx->users, msg->params.vector[0])))
- RETURN_WITH_REPLY (c, IRC_ERR_NOSUCHNICK, msg->params.vector[0]);
-
- client_send (target, ":%s!%s@%s KILL %s :%s",
- c->nickname, c->username, c->hostname,
- target->nickname, msg->params.vector[1]);
-
- char *reason = xstrdup_printf ("Killed by %s: %s",
- c->nickname, msg->params.vector[1]);
- client_close_link (target, reason);
- free (reason);
-}
-
-static void
-irc_handle_die (const struct irc_message *msg, struct client *c)
-{
- (void) msg;
-
- if (!(c->mode & IRC_USER_MODE_OPERATOR))
- RETURN_WITH_REPLY (c, IRC_ERR_NOPRIVILEGES);
- if (!c->ctx->quitting)
- irc_initiate_quit (c->ctx);
-}
-
-// -----------------------------------------------------------------------------
-
-static void
-irc_register_handlers (struct server_context *ctx)
-{
- // TODO: add an index for IRC_ERR_NOSUCHSERVER validation?
- // TODO: add a minimal parameter count?
- // TODO: add a field for oper-only commands?
- static struct irc_command message_handlers[] =
- {
- { "CAP", false, irc_handle_cap, 0, 0 },
- { "PASS", false, irc_handle_pass, 0, 0 },
- { "NICK", false, irc_handle_nick, 0, 0 },
- { "USER", false, irc_handle_user, 0, 0 },
-
- { "USERHOST", true, irc_handle_userhost, 0, 0 },
- { "LUSERS", true, irc_handle_lusers, 0, 0 },
- { "MOTD", true, irc_handle_motd, 0, 0 },
- { "PING", true, irc_handle_ping, 0, 0 },
- { "PONG", false, irc_handle_pong, 0, 0 },
- { "QUIT", false, irc_handle_quit, 0, 0 },
- { "TIME", true, irc_handle_time, 0, 0 },
- { "VERSION", true, irc_handle_version, 0, 0 },
- { "USERS", true, irc_handle_users, 0, 0 },
- { "SUMMON", true, irc_handle_summon, 0, 0 },
- { "AWAY", true, irc_handle_away, 0, 0 },
- { "ADMIN", true, irc_handle_admin, 0, 0 },
- { "STATS", true, irc_handle_stats, 0, 0 },
- { "LINKS", true, irc_handle_links, 0, 0 },
-
- { "MODE", true, irc_handle_mode, 0, 0 },
- { "PRIVMSG", true, irc_handle_privmsg, 0, 0 },
- { "NOTICE", true, irc_handle_notice, 0, 0 },
- { "JOIN", true, irc_handle_join, 0, 0 },
- { "PART", true, irc_handle_part, 0, 0 },
- { "KICK", true, irc_handle_kick, 0, 0 },
- { "INVITE", true, irc_handle_invite, 0, 0 },
- { "TOPIC", true, irc_handle_topic, 0, 0 },
- { "LIST", true, irc_handle_list, 0, 0 },
- { "NAMES", true, irc_handle_names, 0, 0 },
- { "WHO", true, irc_handle_who, 0, 0 },
- { "WHOIS", true, irc_handle_whois, 0, 0 },
- { "WHOWAS", true, irc_handle_whowas, 0, 0 },
- { "ISON", true, irc_handle_ison, 0, 0 },
-
- { "KILL", true, irc_handle_kill, 0, 0 },
- { "DIE", true, irc_handle_die, 0, 0 },
- };
-
- for (size_t i = 0; i < N_ELEMENTS (message_handlers); i++)
- {
- const struct irc_command *cmd = &message_handlers[i];
- str_map_set (&ctx->handlers, cmd->name, (void *) cmd);
- }
-}
-
-static void
-irc_process_message (const struct irc_message *msg,
- const char *raw, void *user_data)
-{
- struct client *c = user_data;
- if (c->closing_link)
- return;
-
- c->n_received_messages++;
- c->received_bytes += strlen (raw) + 2;
-
- if (!flood_detector_check (&c->antiflood))
- {
- client_close_link (c, "Excess flood");
- return;
- }
-
- struct irc_command *cmd = str_map_find (&c->ctx->handlers, msg->command);
- if (!cmd)
- irc_send_reply (c, IRC_ERR_UNKNOWNCOMMAND, msg->command);
- else
- {
- cmd->n_received++;
- cmd->bytes_received += strlen (raw) + 2;
-
- if (cmd->requires_registration && !c->registered)
- irc_send_reply (c, IRC_ERR_NOTREGISTERED);
- else
- cmd->handler (msg, c);
- }
-}
-
-// --- Network I/O -------------------------------------------------------------
-
-static bool
-irc_try_read (struct client *c)
-{
- struct str *buf = &c->read_buffer;
- ssize_t n_read;
-
- while (true)
- {
- str_reserve (buf, 512);
- n_read = read (c->socket_fd, buf->str + buf->len,
- buf->alloc - buf->len - 1 /* null byte */);
-
- if (n_read > 0)
- {
- buf->str[buf->len += n_read] = '\0';
- // TODO: discard characters above the 512 character limit
- irc_process_buffer (buf, irc_process_message, c);
- continue;
- }
- if (n_read == 0)
- {
- client_kill (c, NULL);
- return false;
- }
-
- if (errno == EAGAIN)
- return true;
- if (errno == EINTR)
- continue;
-
- print_debug ("%s: %s: %s", __func__, "read", strerror (errno));
- client_kill (c, strerror (errno));
- return false;
- }
-}
-
-static bool
-irc_try_read_tls (struct client *c)
-{
- if (c->ssl_tx_want_rx)
- return true;
-
- struct str *buf = &c->read_buffer;
- c->ssl_rx_want_tx = false;
- while (true)
- {
- str_reserve (buf, 512);
- ERR_clear_error ();
- int n_read = SSL_read (c->ssl, buf->str + buf->len,
- buf->alloc - buf->len - 1 /* null byte */);
-
- const char *error_info = NULL;
- switch (xssl_get_error (c->ssl, n_read, &error_info))
- {
- case SSL_ERROR_NONE:
- buf->str[buf->len += n_read] = '\0';
- // TODO: discard characters above the 512 character limit
- irc_process_buffer (buf, irc_process_message, c);
- continue;
- case SSL_ERROR_ZERO_RETURN:
- client_kill (c, NULL);
- return false;
- case SSL_ERROR_WANT_READ:
- return true;
- case SSL_ERROR_WANT_WRITE:
- c->ssl_rx_want_tx = true;
- return true;
- case XSSL_ERROR_TRY_AGAIN:
- continue;
- default:
- print_debug ("%s: %s: %s", __func__, "SSL_read", error_info);
- client_kill (c, error_info);
- return false;
- }
- }
-}
-
-static bool
-irc_try_write (struct client *c)
-{
- struct str *buf = &c->write_buffer;
- ssize_t n_written;
-
- while (buf->len)
- {
- n_written = write (c->socket_fd, buf->str, buf->len);
- if (n_written >= 0)
- {
- str_remove_slice (buf, 0, n_written);
- continue;
- }
-
- if (errno == EAGAIN)
- return true;
- if (errno == EINTR)
- continue;
-
- print_debug ("%s: %s: %s", __func__, "write", strerror (errno));
- client_kill (c, strerror (errno));
- return false;
- }
- return true;
-}
-
-static bool
-irc_try_write_tls (struct client *c)
-{
- if (c->ssl_rx_want_tx)
- return true;
-
- struct str *buf = &c->write_buffer;
- c->ssl_tx_want_rx = false;
- while (buf->len)
- {
- ERR_clear_error ();
- int n_written = SSL_write (c->ssl, buf->str, buf->len);
-
- const char *error_info = NULL;
- switch (xssl_get_error (c->ssl, n_written, &error_info))
- {
- case SSL_ERROR_NONE:
- str_remove_slice (buf, 0, n_written);
- continue;
- case SSL_ERROR_ZERO_RETURN:
- client_kill (c, NULL);
- return false;
- case SSL_ERROR_WANT_WRITE:
- return true;
- case SSL_ERROR_WANT_READ:
- c->ssl_tx_want_rx = true;
- return true;
- case XSSL_ERROR_TRY_AGAIN:
- continue;
- default:
- print_debug ("%s: %s: %s", __func__, "SSL_write", error_info);
- client_kill (c, error_info);
- return false;
- }
- }
- return true;
-}
-
-// -----------------------------------------------------------------------------
-
-static bool
-irc_autodetect_tls (struct client *c)
-{
- // Trivial SSL/TLS autodetection. The first block of data returned by
- // recv() must be at least three bytes long for this to work reliably,
- // but that should not pose a problem in practice.
- //
- // SSL2: 1xxx xxxx | xxxx xxxx | <1>
- // (message length) (client hello)
- // SSL3/TLS: <22> | <3> | xxxx xxxx
- // (handshake)| (protocol version)
- //
- // Such byte sequences should never occur at the beginning of regular IRC
- // communication, which usually begins with USER/NICK/PASS/SERVICE.
-
- char buf[3];
-start:
- switch (recv (c->socket_fd, buf, sizeof buf, MSG_PEEK))
- {
- case 3:
- if ((buf[0] & 0x80) && buf[2] == 1)
- return true;
- // Fall-through
- case 2:
- if (buf[0] == 22 && buf[1] == 3)
- return true;
- break;
- case 1:
- if (buf[0] == 22)
- return true;
- break;
- case 0:
- break;
- default:
- if (errno == EINTR)
- goto start;
- }
- return false;
-}
-
-static bool
-client_initialize_tls (struct client *c)
-{
- const char *error_info = NULL;
- if (!c->ctx->ssl_ctx)
- {
- error_info = "TLS support disabled";
- goto error_ssl_1;
- }
-
- ERR_clear_error ();
-
- c->ssl = SSL_new (c->ctx->ssl_ctx);
- if (!c->ssl)
- goto error_ssl_2;
- if (!SSL_set_fd (c->ssl, c->socket_fd))
- goto error_ssl_3;
-
- SSL_set_accept_state (c->ssl);
- return true;
-
-error_ssl_3:
- SSL_free (c->ssl);
- c->ssl = NULL;
-error_ssl_2:
- error_info = xerr_describe_error ();
-error_ssl_1:
- print_debug ("could not initialize TLS for %s: %s", c->address, error_info);
- return false;
-}
-
-// -----------------------------------------------------------------------------
-
-static void
-on_client_ready (const struct pollfd *pfd, void *user_data)
-{
- struct client *c = user_data;
- if (!c->initialized)
- {
- hard_assert (pfd->events == POLLIN);
- if (irc_autodetect_tls (c) && !client_initialize_tls (c))
- {
- client_kill (c, NULL);
- return;
- }
- c->initialized = true;
- client_set_ping_timer (c);
- }
-
- if (c->ssl)
- {
- // Reads may want to write, writes may want to read, poll() may
- // return unexpected things in `revents'... let's try both
- if (!irc_try_read_tls (c) || !irc_try_write_tls (c))
- return;
- }
- else if (!irc_try_read (c) || !irc_try_write (c))
- return;
-
- client_update_poller (c, pfd);
-
- // The purpose of the `closing_link' state is to transfer the `ERROR'
- if (c->closing_link && !c->half_closed && !c->write_buffer.len)
- {
- // To make sure the client has received our ERROR message, we must
- // first half-close the connection, otherwise it could happen that they
- // receive a RST from our TCP stack first when we receive further data
-
- // We only send the "close notify" alert if libssl can write to the
- // socket at this moment. All the other data has been already written,
- // though, and the client will receive a TCP half-close as usual, so
- // it's not that important if the alert actually gets through.
- if (c->ssl)
- (void) SSL_shutdown (c->ssl);
-
- // Either the shutdown succeeds, in which case we set a flag so that
- // we don't retry this action and wait until we get an EOF, or it fails
- // and we just kill the client straight away
- if (!shutdown (c->socket_fd, SHUT_WR))
- c->half_closed = true;
- else
- client_kill (c, NULL);
- }
-}
-
-static void
-client_update_poller (struct client *c, const struct pollfd *pfd)
-{
- // We must not poll for writing when the connection hasn't been initialized
- int new_events = POLLIN;
- if (c->ssl)
- {
- if (c->write_buffer.len || c->ssl_rx_want_tx)
- new_events |= POLLOUT;
-
- // While we're waiting for an opposite event, we ignore the original
- if (c->ssl_rx_want_tx) new_events &= ~POLLIN;
- if (c->ssl_tx_want_rx) new_events &= ~POLLOUT;
- }
- else if (c->initialized && c->write_buffer.len)
- new_events |= POLLOUT;
-
- hard_assert (new_events != 0);
- if (!pfd || pfd->events != new_events)
- poller_fd_set (&c->socket_event, new_events);
-}
-
-static void
-client_finish_connection (struct client *c)
-{
- c->gni = NULL;
-
- c->address = format_host_port_pair (c->hostname, c->port);
- print_debug ("accepted connection from %s", c->address);
-
- client_update_poller (c, NULL);
- client_set_kill_timer (c);
-}
-
-static void
-on_client_gni_resolved (int result, char *host, char *port, void *user_data)
-{
- struct client *c = user_data;
-
- if (result)
- print_debug ("%s: %s", "getnameinfo", gai_strerror (result));
- else
- {
- cstr_set (&c->hostname, xstrdup (host));
- (void) port;
- }
-
- poller_timer_reset (&c->gni_timer);
- client_finish_connection (c);
-}
-
-static void
-on_client_gni_timer (struct client *c)
-{
- async_cancel (&c->gni->async);
- client_finish_connection (c);
-}
-
-static bool
-irc_try_fetch_client (struct server_context *ctx, int listen_fd)
-{
- // XXX: `struct sockaddr_storage' is not the most portable thing
- struct sockaddr_storage peer;
- socklen_t peer_len = sizeof peer;
-
- int fd = accept (listen_fd, (struct sockaddr *) &peer, &peer_len);
- if (fd == -1)
- {
- if (errno == EAGAIN || errno == EWOULDBLOCK)
- return false;
- if (errno == EINTR)
- return true;
-
- if (errno == EBADF
- || errno == EINVAL
- || errno == ENOTSOCK
- || errno == EOPNOTSUPP)
- print_fatal ("%s: %s", "accept", strerror (errno));
-
- // OS kernels may return a wide range of unforeseeable errors.
- // Assuming that they're either transient or caused by
- // a connection that we've just extracted from the queue.
- print_warning ("%s: %s", "accept", strerror (errno));
- return true;
- }
-
- hard_assert (peer_len <= sizeof peer);
- set_blocking (fd, false);
-
- // A little bit questionable once the traffic gets high enough (IMO),
- // but it reduces silly latencies that we don't need because we already
- // do buffer our output
- int yes = 1;
- soft_assert (setsockopt (fd, IPPROTO_TCP, TCP_NODELAY,
- &yes, sizeof yes) != -1);
-
- if (ctx->max_connections != 0 && ctx->n_clients >= ctx->max_connections)
- {
- print_debug ("connection limit reached, refusing connection");
- close (fd);
- return true;
- }
-
- char host[NI_MAXHOST] = "unknown", port[NI_MAXSERV] = "unknown";
- int err = getnameinfo ((struct sockaddr *) &peer, peer_len,
- host, sizeof host, port, sizeof port, NI_NUMERICHOST | NI_NUMERICSERV);
- if (err)
- print_debug ("%s: %s", "getnameinfo", gai_strerror (err));
-
- struct client *c = client_new ();
- c->ctx = ctx;
- c->opened = time (NULL);
- c->socket_fd = fd;
- c->hostname = xstrdup (host);
- c->port = xstrdup (port);
- c->last_active = time (NULL);
- LIST_PREPEND (ctx->clients, c);
- ctx->n_clients++;
-
- c->socket_event = poller_fd_make (&c->ctx->poller, c->socket_fd);
- c->socket_event.dispatcher = (poller_fd_fn) on_client_ready;
- c->socket_event.user_data = c;
-
- c->kill_timer = poller_timer_make (&c->ctx->poller);
- c->kill_timer.dispatcher = (poller_timer_fn) on_client_kill_timer;
- c->kill_timer.user_data = c;
-
- c->timeout_timer = poller_timer_make (&c->ctx->poller);
- c->timeout_timer.dispatcher = (poller_timer_fn) on_client_timeout_timer;
- c->timeout_timer.user_data = c;
-
- c->ping_timer = poller_timer_make (&c->ctx->poller);
- c->ping_timer.dispatcher = (poller_timer_fn) on_client_ping_timer;
- c->ping_timer.user_data = c;
-
- // Resolve the client's hostname first; this is a blocking operation that
- // depends on the network, so run it asynchronously with some timeout
- c->gni = async_getnameinfo (&ctx->poller.common.async,
- (const struct sockaddr *) &peer, peer_len, NI_NUMERICSERV);
- c->gni->dispatcher = on_client_gni_resolved;
- c->gni->user_data = c;
-
- c->gni_timer = poller_timer_make (&c->ctx->poller);
- c->gni_timer.dispatcher = (poller_timer_fn) on_client_gni_timer;
- c->gni_timer.user_data = c;
-
- poller_timer_set (&c->gni_timer, 5000);
- return true;
-}
-
-static void
-on_irc_client_available (const struct pollfd *pfd, void *user_data)
-{
- struct server_context *ctx = user_data;
- while (irc_try_fetch_client (ctx, pfd->fd))
- ;
-}
-
-// --- Application setup -------------------------------------------------------
-
-static int
-irc_ssl_verify_callback (int verify_ok, X509_STORE_CTX *ctx)
-{
- (void) verify_ok;
- (void) ctx;
-
- // RFC 5246: "If the client has sent a certificate with signing ability,
- // a digitally-signed CertificateVerify message is sent to explicitly
- // verify possession of the private key in the certificate."
- //
- // The handshake will fail if the client doesn't have a matching private
- // key, see OpenSSL's tls_process_cert_verify(), and the CertificateVerify
- // message cannot be skipped (except for a case where it doesn't matter).
- // Thus we're fine checking just the cryptographic hash of the certificate.
-
- // We only want to provide additional privileges based on the client's
- // certificate, so let's not terminate the connection because of a failure
- // (especially since self-signed certificates are likely to be used).
- return 1;
-}
-
-static void
-irc_ssl_info_callback (const SSL *ssl, int where, int ret)
-{
- // For debugging only; provides us with the most important information
-
- struct str s = str_make ();
- if (where & SSL_CB_LOOP)
- str_append_printf (&s, "loop (%s) ",
- SSL_state_string_long (ssl));
- if (where & SSL_CB_EXIT)
- str_append_printf (&s, "exit (%d in %s) ", ret,
- SSL_state_string_long (ssl));
-
- if (where & SSL_CB_READ) str_append (&s, "read ");
- if (where & SSL_CB_WRITE) str_append (&s, "write ");
-
- if (where & SSL_CB_ALERT)
- str_append_printf (&s, "alert (%s: %s) ",
- SSL_alert_type_string_long (ret),
- SSL_alert_desc_string_long (ret));
-
- if (where & SSL_CB_HANDSHAKE_START) str_append (&s, "handshake start ");
- if (where & SSL_CB_HANDSHAKE_DONE) str_append (&s, "handshake done ");
-
- print_debug ("ssl <%p> %s", ssl, s.str);
- str_free (&s);
-}
-
-static bool
-irc_initialize_ssl_ctx (struct server_context *ctx,
- const char *cert_path, const char *key_path, struct error **e)
-{
- ERR_clear_error ();
-
- ctx->ssl_ctx = SSL_CTX_new (SSLv23_server_method ());
- if (!ctx->ssl_ctx)
- {
- error_set (e, "%s: %s", "could not initialize TLS",
- xerr_describe_error ());
- return false;
- }
- SSL_CTX_set_verify (ctx->ssl_ctx,
- SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE, irc_ssl_verify_callback);
-
- if (g_debug_mode)
- SSL_CTX_set_info_callback (ctx->ssl_ctx, irc_ssl_info_callback);
-
- const unsigned char session_id_context[SSL_MAX_SSL_SESSION_ID_LENGTH]
- = PROGRAM_NAME;
- (void) SSL_CTX_set_session_id_context (ctx->ssl_ctx,
- session_id_context, sizeof session_id_context);
-
- // IRC is not particularly reconnect-heavy, prefer forward secrecy
- SSL_CTX_set_session_cache_mode (ctx->ssl_ctx, SSL_SESS_CACHE_OFF);
-
- // Gah, spare me your awkward semantics, I just want to push data!
- SSL_CTX_set_mode (ctx->ssl_ctx,
- SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER | SSL_MODE_ENABLE_PARTIAL_WRITE);
-
- // Disable deprecated protocols (see RFC 7568)
- SSL_CTX_set_options (ctx->ssl_ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
-
- // XXX: perhaps we should read the files ourselves for better messages
- const char *ciphers = str_map_find (&ctx->config, "tls_ciphers");
- if (!SSL_CTX_set_cipher_list (ctx->ssl_ctx, ciphers))
- error_set (e, "failed to select any cipher from the cipher list");
- else if (!SSL_CTX_use_certificate_chain_file (ctx->ssl_ctx, cert_path))
- error_set (e, "%s: %s", "setting the TLS certificate failed",
- xerr_describe_error ());
- else if (!SSL_CTX_use_PrivateKey_file
- (ctx->ssl_ctx, key_path, SSL_FILETYPE_PEM))
- error_set (e, "%s: %s", "setting the TLS private key failed",
- xerr_describe_error ());
- else
- // TODO: SSL_CTX_check_private_key()? It has probably already been
- // checked by SSL_CTX_use_PrivateKey_file() above.
- return true;
-
- SSL_CTX_free (ctx->ssl_ctx);
- ctx->ssl_ctx = NULL;
- return false;
-}
-
-static bool
-irc_initialize_tls (struct server_context *ctx, struct error **e)
-{
- const char *tls_cert = str_map_find (&ctx->config, "tls_cert");
- const char *tls_key = str_map_find (&ctx->config, "tls_key");
-
- // Only try to enable SSL support if the user configures it; it is not
- // a failure if no one has requested it.
- if (!tls_cert && !tls_key)
- return true;
-
- if (!tls_cert)
- error_set (e, "no TLS certificate set");
- else if (!tls_key)
- error_set (e, "no TLS private key set");
- if (!tls_cert || !tls_key)
- return false;
-
- bool result = false;
-
- char *cert_path = resolve_filename
- (tls_cert, resolve_relative_config_filename);
- char *key_path = resolve_filename
- (tls_key, resolve_relative_config_filename);
- if (!cert_path)
- error_set (e, "%s: %s", "cannot open file", tls_cert);
- else if (!key_path)
- error_set (e, "%s: %s", "cannot open file", tls_key);
- else
- result = irc_initialize_ssl_ctx (ctx, cert_path, key_path, e);
-
- free (cert_path);
- free (key_path);
- return result;
-}
-
-static bool
-irc_initialize_catalog (struct server_context *ctx, struct error **e)
-{
- hard_assert (ctx->catalog == (nl_catd) -1);
- const char *catalog = str_map_find (&ctx->config, "catalog");
- if (!catalog)
- return true;
-
- char *path = resolve_filename (catalog, resolve_relative_config_filename);
- if (!path)
- {
- error_set (e, "%s: %s", "cannot open file", catalog);
- return false;
- }
- ctx->catalog = catopen (path, NL_CAT_LOCALE);
- free (path);
-
- if (ctx->catalog == (nl_catd) -1)
- {
- error_set (e, "%s: %s",
- "failed reading the message catalog file", strerror (errno));
- return false;
- }
- return true;
-}
-
-static bool
-irc_initialize_motd (struct server_context *ctx, struct error **e)
-{
- hard_assert (ctx->motd.len == 0);
- const char *motd = str_map_find (&ctx->config, "motd");
- if (!motd)
- return true;
-
- char *path = resolve_filename (motd, resolve_relative_config_filename);
- if (!path)
- {
- error_set (e, "%s: %s", "cannot open file", motd);
- return false;
- }
- FILE *fp = fopen (path, "r");
- free (path);
-
- if (!fp)
- {
- error_set (e, "%s: %s",
- "failed reading the MOTD file", strerror (errno));
- return false;
- }
-
- struct str line = str_make ();
- while (read_line (fp, &line))
- strv_append_owned (&ctx->motd, str_steal (&line));
- str_free (&line);
-
- fclose (fp);
- return true;
-}
-
-static bool
-irc_parse_config_unsigned (const char *name, const char *value, unsigned *out,
- unsigned long min, unsigned long max, struct error **e)
-{
- unsigned long ul;
- hard_assert (value != NULL);
- if (!xstrtoul (&ul, value, 10) || ul > max || ul < min)
- {
- error_set (e, "invalid configuration value for `%s': %s",
- name, "the number is invalid or out of range");
- return false;
- }
- *out = ul;
- return true;
-}
-
-/// This function handles values that require validation before their first use,
-/// or some kind of a transformation (such as conversion to an integer) needs
-/// to be done before they can be used directly.
-static bool
-irc_parse_config (struct server_context *ctx, struct error **e)
-{
-#define PARSE_UNSIGNED(name, min, max) \
- irc_parse_config_unsigned (#name, str_map_find (&ctx->config, #name), \
- &ctx->name, min, max, e)
-
- if (!PARSE_UNSIGNED (ping_interval, 1, UINT_MAX)
- || !PARSE_UNSIGNED (max_connections, 0, UINT_MAX))
- return false;
-
- bool result = true;
- struct strv fingerprints = strv_make ();
- const char *operators = str_map_find (&ctx->config, "operators");
- if (operators)
- cstr_split (operators, ",", true, &fingerprints);
- for (size_t i = 0; i < fingerprints.len; i++)
- {
- const char *key = fingerprints.vector[i];
- if (!irc_is_valid_fingerprint (key))
- {
- error_set (e, "invalid configuration value for `%s': %s",
- "operators", "invalid fingerprint value");
- result = false;
- break;
- }
- str_map_set (&ctx->operators, key, (void *) 1);
- }
- strv_free (&fingerprints);
- return result;
-}
-
-static bool
-irc_initialize_server_name (struct server_context *ctx, struct error **e)
-{
- enum validation_result res;
- const char *server_name = str_map_find (&ctx->config, "server_name");
- if (server_name)
- {
- res = irc_validate_hostname (server_name);
- if (res != VALIDATION_OK)
- {
- error_set (e, "invalid configuration value for `%s': %s",
- "server_name", irc_validate_to_str (res));
- return false;
- }
- ctx->server_name = xstrdup (server_name);
- }
- else
- {
- long host_name_max = sysconf (_SC_HOST_NAME_MAX);
- if (host_name_max <= 0)
- host_name_max = _POSIX_HOST_NAME_MAX;
-
- char hostname[host_name_max + 1];
- if (gethostname (hostname, sizeof hostname))
- {
- error_set (e, "%s: %s",
- "getting the hostname failed", strerror (errno));
- return false;
- }
- res = irc_validate_hostname (hostname);
- if (res != VALIDATION_OK)
- {
- error_set (e,
- "`%s' is not set and the hostname (`%s') cannot be used: %s",
- "server_name", hostname, irc_validate_to_str (res));
- return false;
- }
- ctx->server_name = xstrdup (hostname);
- }
- return true;
-}
-
-static bool
-irc_lock_pid_file (struct server_context *ctx, struct error **e)
-{
- const char *path = str_map_find (&ctx->config, "pid_file");
- if (!path)
- return true;
-
- char *resolved = resolve_filename (path, resolve_relative_runtime_filename);
- bool result = lock_pid_file (resolved, e) != -1;
- free (resolved);
- return result;
-}
-
-static int
-irc_listen (struct addrinfo *gai_iter)
-{
- int fd = socket (gai_iter->ai_family,
- gai_iter->ai_socktype, gai_iter->ai_protocol);
- if (fd == -1)
- return -1;
- set_cloexec (fd);
-
- int yes = 1;
- soft_assert (setsockopt (fd, SOL_SOCKET, SO_KEEPALIVE,
- &yes, sizeof yes) != -1);
- soft_assert (setsockopt (fd, SOL_SOCKET, SO_REUSEADDR,
- &yes, sizeof yes) != -1);
-
-#if defined SOL_IPV6 && defined IPV6_V6ONLY
- // Make NULL always bind to both IPv4 and IPv6, irrespectively of the order
- // of results; only INADDR6_ANY seems to be affected by this
- if (gai_iter->ai_family == AF_INET6)
- soft_assert (setsockopt (fd, SOL_IPV6, IPV6_V6ONLY,
- &yes, sizeof yes) != -1);
-#endif
-
- char host[NI_MAXHOST], port[NI_MAXSERV];
- host[0] = port[0] = '\0';
- int err = getnameinfo (gai_iter->ai_addr, gai_iter->ai_addrlen,
- host, sizeof host, port, sizeof port,
- NI_NUMERICHOST | NI_NUMERICSERV);
- if (err)
- print_debug ("%s: %s", "getnameinfo", gai_strerror (err));
-
- char *address = format_host_port_pair (host, port);
- if (bind (fd, gai_iter->ai_addr, gai_iter->ai_addrlen))
- print_error ("bind to %s failed: %s", address, strerror (errno));
- else if (listen (fd, 16 /* arbitrary number */))
- print_error ("listen on %s failed: %s", address, strerror (errno));
- else
- {
- print_status ("listening on %s", address);
- free (address);
- return fd;
- }
-
- free (address);
- xclose (fd);
- return -1;
-}
-
-static void
-irc_listen_resolve (struct server_context *ctx,
- const char *host, const char *port, struct addrinfo *gai_hints)
-{
- struct addrinfo *gai_result, *gai_iter;
- int err = getaddrinfo (host, port, gai_hints, &gai_result);
- if (err)
- {
- char *address = format_host_port_pair (host, port);
- print_error ("bind to %s failed: %s: %s",
- address, "getaddrinfo", gai_strerror (err));
- free (address);
- return;
- }
-
- int fd;
- for (gai_iter = gai_result; gai_iter; gai_iter = gai_iter->ai_next)
- {
- if (ctx->listen_len == ctx->listen_alloc)
- break;
-
- if ((fd = irc_listen (gai_iter)) == -1)
- continue;
- set_blocking (fd, false);
-
- struct poller_fd *event = &ctx->listen_events[ctx->listen_len];
- *event = poller_fd_make (&ctx->poller, fd);
- event->dispatcher = (poller_fd_fn) on_irc_client_available;
- event->user_data = ctx;
-
- ctx->listen_fds[ctx->listen_len++] = fd;
- poller_fd_set (event, POLLIN);
- }
- freeaddrinfo (gai_result);
-}
-
-static bool
-irc_setup_listen_fds (struct server_context *ctx, struct error **e)
-{
- const char *bind_host = str_map_find (&ctx->config, "bind_host");
- const char *bind_port = str_map_find (&ctx->config, "bind_port");
- hard_assert (bind_port != NULL); // We have a default value for this
-
- struct addrinfo gai_hints;
- memset (&gai_hints, 0, sizeof gai_hints);
-
- gai_hints.ai_socktype = SOCK_STREAM;
- gai_hints.ai_flags = AI_PASSIVE;
-
- struct strv ports = strv_make ();
- cstr_split (bind_port, ",", true, &ports);
-
- // For C and simplicity's sake let's assume that the host will resolve
- // to at most two different addresses: IPv4 and IPv6 in case it is NULL
- ctx->listen_alloc = ports.len * 2;
-
- ctx->listen_fds =
- xcalloc (ctx->listen_alloc, sizeof *ctx->listen_fds);
- ctx->listen_events =
- xcalloc (ctx->listen_alloc, sizeof *ctx->listen_events);
- for (size_t i = 0; i < ports.len; i++)
- irc_listen_resolve (ctx, bind_host, ports.vector[i], &gai_hints);
- strv_free (&ports);
-
- if (!ctx->listen_len)
- {
- error_set (e, "%s: %s",
- "network setup failed", "no ports to listen on");
- return false;
- }
- return true;
-}
-
-// --- Main --------------------------------------------------------------------
-
-static void
-on_signal_pipe_readable (const struct pollfd *fd, struct server_context *ctx)
-{
- char dummy;
- (void) read (fd->fd, &dummy, 1);
-
- if (g_termination_requested && !ctx->quitting)
- irc_initiate_quit (ctx);
-}
-
-static void
-daemonize (struct server_context *ctx)
-{
- print_status ("daemonizing...");
-
- if (chdir ("/"))
- exit_fatal ("%s: %s", "chdir", strerror (errno));
-
- // Because of systemd, we need to exit the parent process _after_ writing
- // a PID file, otherwise our grandchild would receive a SIGTERM
- int sync_pipe[2];
- if (pipe (sync_pipe))
- exit_fatal ("%s: %s", "pipe", strerror (errno));
-
- pid_t pid;
- if ((pid = fork ()) < 0)
- exit_fatal ("%s: %s", "fork", strerror (errno));
- else if (pid)
- {
- // Wait until all write ends of the pipe are closed, which can mean
- // either success or failure, we don't need to care
- xclose (sync_pipe[PIPE_WRITE]);
-
- char dummy;
- if (read (sync_pipe[PIPE_READ], &dummy, 1) < 0)
- exit_fatal ("%s: %s", "read", strerror (errno));
-
- exit (EXIT_SUCCESS);
- }
-
- setsid ();
- signal (SIGHUP, SIG_IGN);
-
- if ((pid = fork ()) < 0)
- exit_fatal ("%s: %s", "fork", strerror (errno));
- else if (pid)
- exit (EXIT_SUCCESS);
-
- openlog (PROGRAM_NAME, LOG_NDELAY | LOG_NOWAIT | LOG_PID, 0);
- g_log_message_real = log_message_syslog;
-
- // Write the PID file (if so configured) and get rid of the pipe, so that
- // the read() in our grandparent finally returns zero (no write ends)
- struct error *e = NULL;
- if (!irc_lock_pid_file (ctx, &e))
- exit_fatal ("%s", e->message);
-
- xclose (sync_pipe[PIPE_READ]);
- xclose (sync_pipe[PIPE_WRITE]);
-
- // XXX: we may close our own descriptors this way, crippling ourselves;
- // there is no real guarantee that we will start with all three
- // descriptors open. In theory we could try to enumerate the descriptors
- // at the start of main().
- for (int i = 0; i < 3; i++)
- xclose (i);
-
- int tty = open ("/dev/null", O_RDWR);
- if (tty != 0 || dup (0) != 1 || dup (0) != 2)
- exit_fatal ("failed to reopen FD's: %s", strerror (errno));
-
- poller_post_fork (&ctx->poller);
-}
-
-int
-main (int argc, char *argv[])
-{
- // Need to call this first as all string maps depend on it
- siphash_wrapper_randomize ();
-
- static const struct opt opts[] =
- {
- { 'd', "debug", NULL, 0, "run in debug mode (do not daemonize)" },
- { 'h', "help", NULL, 0, "display this help and exit" },
- { 'V', "version", NULL, 0, "output version information and exit" },
- { 'w', "write-default-cfg", "FILENAME",
- OPT_OPTIONAL_ARG | OPT_LONG_ONLY,
- "write a default configuration file and exit" },
- { 0, NULL, NULL, 0, NULL }
- };
-
- struct opt_handler oh =
- opt_handler_make (argc, argv, opts, NULL, "IRC daemon.");
-
- int c;
- while ((c = opt_handler_get (&oh)) != -1)
- switch (c)
- {
- case 'd':
- g_debug_mode = true;
- break;
- case 'h':
- opt_handler_usage (&oh, stdout);
- exit (EXIT_SUCCESS);
- case 'V':
- printf (PROGRAM_NAME " " PROGRAM_VERSION "\n");
- exit (EXIT_SUCCESS);
- case 'w':
- call_simple_config_write_default (optarg, g_config_table);
- exit (EXIT_SUCCESS);
- default:
- print_error ("wrong options");
- opt_handler_usage (&oh, stderr);
- exit (EXIT_FAILURE);
- }
-
- opt_handler_free (&oh);
-
- print_status (PROGRAM_NAME " " PROGRAM_VERSION " starting");
- setup_signal_handlers ();
- init_openssl ();
-
- struct server_context ctx;
- server_context_init (&ctx);
- ctx.started = time (NULL);
- irc_register_handlers (&ctx);
- irc_register_cap_handlers (&ctx);
-
- struct error *e = NULL;
- if (!simple_config_update_from_file (&ctx.config, &e))
- {
- print_error ("error loading configuration: %s", e->message);
- error_free (e);
- exit (EXIT_FAILURE);
- }
-
- ctx.signal_event = poller_fd_make (&ctx.poller, g_signal_pipe[0]);
- ctx.signal_event.dispatcher = (poller_fd_fn) on_signal_pipe_readable;
- ctx.signal_event.user_data = &ctx;
- poller_fd_set (&ctx.signal_event, POLLIN);
-
- if (!irc_initialize_tls (&ctx, &e)
- || !irc_initialize_server_name (&ctx, &e)
- || !irc_initialize_motd (&ctx, &e)
- || !irc_initialize_catalog (&ctx, &e)
- || !irc_parse_config (&ctx, &e)
- || !irc_setup_listen_fds (&ctx, &e))
- exit_fatal ("%s", e->message);
-
- if (!g_debug_mode)
- daemonize (&ctx);
- else if (!irc_lock_pid_file (&ctx, &e))
- exit_fatal ("%s", e->message);
-
-#if OpenBSD >= 201605
- // This won't be as simple once we decide to implement REHASH
- if (pledge ("stdio inet dns", NULL))
- exit_fatal ("%s: %s", "pledge", strerror (errno));
-#endif
-
- ctx.polling = true;
- while (ctx.polling)
- poller_run (&ctx.poller);
-
- server_context_free (&ctx);
- return EXIT_SUCCESS;
-}
diff --git a/plugins/degesch/auto-rejoin.lua b/plugins/degesch/auto-rejoin.lua
deleted file mode 100644
index ce82213..0000000
--- a/plugins/degesch/auto-rejoin.lua
+++ /dev/null
@@ -1,48 +0,0 @@
---
--- auto-rejoin.lua: join back automatically when someone kicks you
---
--- Copyright (c) 2016, 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.
---
-
-local timeout
-degesch.setup_config {
- timeout = {
- type = "integer",
- comment = "auto rejoin timeout",
- default = "0",
-
- on_change = function (v)
- timeout = v
- end,
- validate = function (v)
- if v < 0 then error ("timeout must not be negative", 0) end
- end,
- },
-}
-
-async, await = degesch.async, coroutine.yield
-degesch.hook_irc (function (hook, server, line)
- local msg = degesch.parse (line)
- if msg.command ~= "KICK" then return line end
-
- local who = msg.prefix:match ("^[^!]*")
- local channel, whom = table.unpack (msg.params)
- if who ~= whom and whom == server.user.nickname then
- async.go (function ()
- await (async.timer_ms (timeout * 1000))
- server:send ("JOIN " .. channel)
- end)
- end
- return line
-end)
diff --git a/plugins/degesch/censor.lua b/plugins/degesch/censor.lua
deleted file mode 100644
index cb76c23..0000000
--- a/plugins/degesch/censor.lua
+++ /dev/null
@@ -1,90 +0,0 @@
---
--- censor.lua: black out certain users' messages
---
--- Copyright (c) 2016 - 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.
---
-
-local to_pattern = function (mask)
- if not mask:match ("!") then mask = mask .. "!*" end
- if not mask:match ("@") then mask = mask .. "@*" end
-
- -- That is, * acts like a wildcard, otherwise everything is escaped
- return "^" .. mask:gsub ("[%^%$%(%)%%%.%[%]%+%-%?]", "%%%0")
- :gsub ("%*", ".*") .. "$"
-end
-
-local patterns = {}
-local read_masks = function (v)
- patterns = {}
- local add = function (who, where)
- local channels = patterns[who] or {}
- table.insert (channels, where)
- patterns[who] = channels
- end
- for item in v:lower ():gmatch ("[^,]+") do
- local who, where = item:match ("^([^/]+)/*(.*)")
- if who then add (to_pattern (who), where == "" or where) end
- end
-end
-
-local quote
-degesch.setup_config {
- masks = {
- type = "string_array",
- default = "\"\"",
- comment = "user masks (optionally \"/#channel\") to censor",
- on_change = read_masks
- },
- quote = {
- type = "string",
- default = "\"\\x0301,01\"",
- comment = "formatting prefix for censored messages",
- on_change = function (v) quote = v end
- },
-}
-
-local decolor = function (text)
- local rebuilt, last = {""}, 1
- for start in text:gmatch ('()\x03') do
- table.insert (rebuilt, text:sub (last, start - 1))
- local sub = text:sub (start + 1)
- last = start + (sub:match ('^%d%d?,%d%d?()') or sub:match ('^%d?%d?()'))
- end
- return table.concat (rebuilt) .. text:sub (last)
-end
-
-local censor = function (line)
- -- Taking a shortcut to avoid lengthy message reassembly
- local start, text = line:match ("^(.- PRIVMSG .- :)(.*)$")
- local ctcp, rest = text:match ("^(\x01%g+ )(.*)")
- text = ctcp and ctcp .. quote .. decolor (rest) or quote .. decolor (text)
- return start .. text
-end
-
-degesch.hook_irc (function (hook, server, line)
- local msg = degesch.parse (line)
- if msg.command ~= "PRIVMSG" then return line end
-
- local channel = msg.params[1]:lower ()
- for who, where in pairs (patterns) do
- if msg.prefix:lower ():match (who) then
- for _, x in pairs (where) do
- if x == true or x == channel then
- return censor (line)
- end
- end
- end
- end
- return line
-end)
diff --git a/plugins/degesch/fancy-prompt.lua b/plugins/degesch/fancy-prompt.lua
deleted file mode 100644
index 93fe67c..0000000
--- a/plugins/degesch/fancy-prompt.lua
+++ /dev/null
@@ -1,105 +0,0 @@
---
--- fancy-prompt.lua: the fancy multiline prompt you probably want
---
--- Copyright (c) 2016, 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.
---
--- Beware that it is a hack and only goes about 90% of the way, which is why
--- this functionality is only available as a plugin in the first place
--- (well, and also for customizability).
---
--- The biggest problem is that the way we work with Readline is incompatible
--- with multiline prompts, and normal newlines just don't work. This is being
--- circumvented by using an overflowing single-line prompt with a specially
--- crafted character in the rightmost column that prevents the bar's background
--- from spilling all over the last line.
---
--- There is also a problem with C-r search rendering not clearing out the
--- background but to really fix that mode, we'd have to fully reimplement it
--- since its alternative prompt very often gets overriden by accident anyway.
-
-degesch.hook_prompt (function (hook)
- local current = degesch.current_buffer
- local chan = current.channel
- local s = current.server
-
- local bg_color = "255"
- local current_n = 0
- local active = ""
- for i, buffer in ipairs (degesch.buffers) do
- if buffer == current then
- current_n = i
- elseif buffer.new_messages_count ~= buffer.new_unimportant_count then
- if active ~= "" then active = active .. "," end
- if buffer.highlighted then
- active = active .. "!"
- bg_color = "224"
- end
- active = active .. i
- end
- end
- if active ~= "" then active = "(" .. active .. ")" end
- local x = current_n .. ":" .. current.name
- if chan and chan.users_len ~= 0 then
- local params = ""
- for mode, param in pairs (chan.param_modes) do
- params = params .. " +" .. mode .. " " .. param
- end
- local modes = chan.no_param_modes .. params:sub (3)
- if modes ~= "" then x = x .. "(+" .. modes .. ")" end
- x = x .. "{" .. chan.users_len .. "}"
- end
- if current.hide_unimportant then x = x .. "" end
-
- local lines, cols = degesch.get_screen_size ()
- x = x .. " " .. active .. string.rep (" ", cols)
-
- -- Readline 7.0.003 seems to be broken and completely corrupts the prompt.
- -- However 8.0.004 seems to be fine with these, as is libedit 20191231-3.1.
- --x = x:gsub("[\128-\255]", "?")
-
- -- Cut off extra characters and apply formatting, including the hack.
- -- FIXME: this doesn't count with full-width or zero-width characters.
- -- We might want to export wcwidth() above term_from_utf8 somehow.
- local overflow = utf8.offset (x, cols - 1)
- if overflow then x = x:sub (1, overflow) end
- x = "\x01\x1b[0;4;1;38;5;16m\x1b[48;5;" .. bg_color .. "m\x02" ..
- x .. "\x01\x1b[0;4;1;7;38;5;" .. bg_color .. "m\x02 \x01\x1b[0;1m\x02"
-
- local user_prefix = function (chan, user)
- for i, chan_user in ipairs (chan.users) do
- if chan_user.user == user then return chan_user.prefixes end
- end
- return ""
- end
- if s then
- x = x .. "["
- local state = s.state
- if state == "disconnected" or state == "connecting" then
- x = x .. "(" .. state .. ")"
- elseif state ~= "registered" then
- x = x .. "(unregistered)"
- else
- local user, modes = s.user, s.user_mode
- if chan then x = x .. user_prefix (chan, user) end
- x = x .. user.nickname
- if modes ~= "" then x = x .. "(" .. modes .. ")" end
- end
- x = x .. "] "
- else
- -- There needs to be at least one character so that the cursor
- -- doesn't get damaged by our hack in that last column
- x = x .. "> "
- end
- return x
-end)
diff --git a/plugins/degesch/last-fm.lua b/plugins/degesch/last-fm.lua
deleted file mode 100644
index 6ade80d..0000000
--- a/plugins/degesch/last-fm.lua
+++ /dev/null
@@ -1,178 +0,0 @@
---
--- last-fm.lua: "now playing" feature using the last.fm API
---
--- Dependencies: lua-cjson (from luarocks e.g.)
---
--- I call this style closure-oriented programming
---
--- Copyright (c) 2016, Přemysl Eric Janouch
---
--- Permission to use, copy, modify, and/or distribute this software for any
--- purpose with or without fee is hereby granted.
---
--- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
--- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
--- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
--- SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
--- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
--- OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
--- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
---
-
-local cjson = require "cjson"
-
--- Setup configuration to load last.fm API credentials from
-local user, api_key
-degesch.setup_config {
- user = {
- type = "string",
- comment = "last.fm username",
- on_change = function (v) user = v end
- },
- api_key = {
- type = "string",
- comment = "last.fm API key",
- on_change = function (v) api_key = v end
- },
-}
-
--- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
--- Generic error reporting
-local report_error = function (buffer, error)
- buffer:log ("last-fm error: " .. error)
-end
-
--- Process data return by the server and extract the now playing song
-local process = function (buffer, data, action)
- -- There's no reasonable Lua package to parse HTTP that I could find
- local s, e, v, status, message = string.find (data, "(%S+) (%S+) .+\r\n")
- if not s then return "server returned unexpected data" end
- if status ~= "200" then return status .. " " .. message end
-
- local s, e = string.find (data, "\r\n\r\n")
- if not s then return "server returned unexpected data" end
-
- local parser = cjson.new ()
- data = parser.decode (string.sub (data, e + 1))
- if not data.recenttracks or not data.recenttracks.track then
- return "invalid response" end
-
- -- Need to make some sense of the XML automatically converted to JSON
- local text_of = function (node)
- if type (node) ~= "table" then return node end
- return node["#text"] ~= "" and node["#text"] or nil
- end
-
- local name, artist, album
- for i, track in ipairs (data.recenttracks.track) do
- if track["@attr"] and track["@attr"].nowplaying then
- if track.name then name = text_of (track.name) end
- if track.artist then artist = text_of (track.artist) end
- if track.album then album = text_of (track.album) end
- end
- end
-
- if not name then
- action (false)
- else
- local np = "\"" .. name .. "\""
- if artist then np = np .. " by " .. artist end
- if album then np = np .. " from " .. album end
- action (np)
- end
-end
-
--- Set up the connection and make the request
-local on_connected = function (buffer, c, host, action)
- -- Buffer data in the connection object
- c.data = ""
- c.on_data = function (data)
- c.data = c.data .. data
- end
-
- -- And process it after we receive everything
- c.on_eof = function ()
- error = process (buffer, c.data, action)
- if error then report_error (buffer, error) end
- c:close ()
- end
- c.on_error = function (e)
- report_error (buffer, e)
- end
-
- -- Make the unencrypted HTTP request
- local url = "/2.0/?method=user.getrecenttracks&user=" .. user ..
- "&limit=1&api_key=" .. api_key .. "&format=json"
- c:send ("GET " .. url .. " HTTP/1.1\r\n")
- c:send ("User-agent: last-fm.lua\r\n")
- c:send ("Host: " .. host .. "\r\n")
- c:send ("Connection: close\r\n")
- c:send ("\r\n")
-end
-
--- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
--- Avoid establishing more than one connection at a time
-local running
-
--- Initiate a connection to last.fm servers
-async, await = degesch.async, coroutine.yield
-local make_request = function (buffer, action)
- if not user or not api_key then
- report_error (buffer, "configuration is incomplete")
- return
- end
-
- if running then running:cancel () end
- running = async.go (function ()
- local c, host, e = await (async.dial ("ws.audioscrobbler.com", 80))
- if e then
- report_error (buffer, e)
- else
- on_connected (buffer, c, host, action)
- end
- running = nil
- end)
-end
-
--- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-local now_playing
-
-local tell_song = function (buffer)
- if now_playing == nil then
- buffer:log ("last-fm: I don't know what you're listening to")
- elseif not now_playing then
- buffer:log ("last-fm: not playing anything right now")
- else
- buffer:log ("last-fm: now playing: " .. now_playing)
- end
-end
-
-local send_song = function (buffer)
- if not now_playing then
- tell_song (buffer)
- else
- buffer:execute ("/me is listening to " .. now_playing)
- end
-end
-
--- Hook input to simulate new commands
-degesch.hook_input (function (hook, buffer, input)
- if input == "/np" then
- make_request (buffer, function (np)
- now_playing = np
- send_song (buffer)
- end)
- elseif input == "/np?" then
- make_request (buffer, function (np)
- now_playing = np
- tell_song (buffer)
- end)
- elseif input == "/np!" then
- send_song (buffer)
- else
- return input
- end
-end)
diff --git a/plugins/degesch/ping-timeout.lua b/plugins/degesch/ping-timeout.lua
deleted file mode 100644
index 6444c0a..0000000
--- a/plugins/degesch/ping-timeout.lua
+++ /dev/null
@@ -1,32 +0,0 @@
---
--- ping-timeout.lua: ping timeout readability enhancement plugin
---
--- Copyright (c) 2015 - 2016, 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.
---
-
-degesch.hook_irc (function (hook, server, line)
- local msg = degesch.parse (line)
- local start, timeout = line:match ("^(.* :Ping timeout:) (%d+) seconds$")
- if msg.command ~= "QUIT" or not start then
- return line
- end
-
- local minutes = timeout // 60
- if minutes == 0 then
- return line
- end
-
- local seconds = timeout % 60
- return ("%s %d minutes, %d seconds"):format (start, minutes, seconds)
-end)
diff --git a/plugins/degesch/prime.lua b/plugins/degesch/prime.lua
deleted file mode 100644
index 420124f..0000000
--- a/plugins/degesch/prime.lua
+++ /dev/null
@@ -1,68 +0,0 @@
---
--- prime.lua: highlight prime numbers in messages
---
--- Copyright (c) 2020, 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.
---
-
-local smallest, highlight = 0, "\x1f"
-degesch.setup_config {
- smallest = {
- type = "integer",
- default = "0",
- comment = "smallest number to scan for primality",
- on_change = function (v) smallest = math.max (v, 2) end
- },
- highlight = {
- type = "string",
- default = "\"\\x1f\"",
- comment = "the attribute to use for highlights",
- on_change = function (v) highlight = v end
- },
-}
-
--- The prime test is actually very fast, so there is no DoS concern
-local do_intercolour = function (text)
- return tostring (text:gsub ("%f[%w_]%d+", function (n)
- if tonumber (n) < smallest then return nil end
- for i = 2, n ^ (1 / 2) do if (n % i) == 0 then return nil end end
- return highlight .. n .. highlight
- end))
-end
-
-local do_interlink = function (text)
- local rebuilt, last = {""}, 1
- for start in text:gmatch ('()\x03') do
- table.insert (rebuilt, do_intercolour (text:sub (last, start - 1)))
- local sub = text:sub (start + 1)
- last = start + (sub:match ('^%d%d?,%d%d?()') or sub:match ('^%d?%d?()'))
- table.insert (rebuilt, text:sub (start, last - 1))
- end
- return table.concat (rebuilt) .. do_intercolour (text:sub (last))
-end
-
-local do_message = function (text)
- local rebuilt, last = {""}, 1
- for run, link, endpos in text:gmatch ('(.-)(%f[%g]https?://%g+)()') do
- last = endpos
- table.insert (rebuilt, do_interlink (run) .. link)
- end
- return table.concat (rebuilt) .. do_interlink (text:sub (last))
-end
-
--- XXX: sadly it won't typically highlight primes in our own messages,
--- unless IRCv3 echo-message is on
-degesch.hook_irc (function (hook, server, line)
- local start, message = line:match ("^(.- PRIVMSG .- :)(.*)$")
- return message and start .. do_message (message) or line
-end)
diff --git a/plugins/degesch/slack.lua b/plugins/degesch/slack.lua
deleted file mode 100644
index dcddb3c..0000000
--- a/plugins/degesch/slack.lua
+++ /dev/null
@@ -1,147 +0,0 @@
---
--- slack.lua: try to fix up UX when using the Slack IRC gateway
---
--- Copyright (c) 2017, 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.
---
-
-local servers = {}
-local read_servers = function (v)
- servers = {}
- for name in v:lower ():gmatch "[^,]+" do
- servers[name] = true
- end
-end
-
--- This is a reverse list of Slack's automatic emoji, noseless forms
-local unemojify, emoji, emoji_default = false, {}, {
- heart = "<3",
- broken_heart = "3",
- sunglasses = "8)",
- anguished = "D:",
- cry = ":'(",
- monkey_face = ":o)",
- kiss = ":*",
- smiley = "=)",
- smile = ":D",
- wink = ";)",
- laughing = ":>",
- neutral_face = ":|",
- open_mouth = ":o",
- angry = ">:(",
- slightly_smiling_face = ":)",
- disappointed = ":(",
- confused = ":/",
- stuck_out_tongue = ":p",
- stuck_out_tongue_winking_eye = ";p",
-}
-local load_emoji = function (extra)
- emoji = {}
- for k, v in pairs (emoji_default) do emoji[k] = v end
- for k, v in extra:gmatch "([^,]+) ([^,]+)" do emoji[k] = v end
-end
-
-degesch.setup_config {
- servers = {
- type = "string_array",
- default = "\"\"",
- comment = "list of server names that are Slack IRC gateways",
- on_change = read_servers
- },
- unemojify = {
- type = "boolean",
- default = "true",
- comment = "convert emoji to normal ASCII emoticons",
- on_change = function (v) unemojify = v end
- },
- extra_emoji = {
- type = "string_array",
- default = "\"grinning :)),joy :'),innocent o:),persevere >_<\"",
- comment = "overrides or extra emoji for unemojify",
- on_change = function (v) load_emoji (v) end
- }
-}
-
--- We can handle external messages about what we've supposedly sent just fine,
--- so let's get rid of that "[username] some message sent from the web UI" crap
-degesch.hook_irc (function (hook, server, line)
- local msg, us = degesch.parse (line), server.user
- if not servers[server.name] or msg.command ~= "PRIVMSG" or not us
- or msg.params[1]:lower () ~= us.nickname:lower () then return line end
-
- -- Taking a shortcut to avoid lengthy message reassembly
- local quoted_nick = us.nickname:gsub ("[%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%0")
- local text = line:match ("^.- PRIVMSG .- :%[" .. quoted_nick .. "%] (.*)$")
- if not text then return line end
- return ":" .. us.nickname .. "!" .. server.irc_user_host .. " PRIVMSG "
- .. msg.prefix:match "^[^!@]*" .. " :" .. text
-end)
-
--- Unfuck emoji and :nick!nick@irc.tinyspeck.com MODE #channel +v nick : active
-degesch.hook_irc (function (hook, server, line)
- if not servers[server.name] then return line end
- if unemojify then
- local start, text = line:match ("^(.- PRIVMSG .- :)(.*)$")
- if start then return start .. text:gsub (":([a-z_]+):", function (name)
- if emoji[name] then return emoji[name] end
- return ":" .. name .. ":"
- end) end
- end
- return line:gsub ("^(:%S+ MODE .+) : .*", "%1")
-end)
-
--- The gateway simply ignores the NAMES command altogether
-degesch.hook_input (function (hook, buffer, input)
- if not buffer.channel or not servers[buffer.server.name]
- or not input:match "^/names%s*$" then return input end
-
- local users = buffer.channel.users
- table.sort (users, function (a, b)
- if a.prefixes > b.prefixes then return true end
- if a.prefixes < b.prefixes then return false end
- return a.user.nickname < b.user.nickname
- end)
-
- local names = "Users on " .. buffer.channel.name .. ":"
- for i, chan_user in ipairs (users) do
- names = names .. " " .. chan_user.prefixes .. chan_user.user.nickname
- end
- buffer:log (names)
-end)
-
-degesch.hook_completion (function (hook, data, word)
- local chan = degesch.current_buffer.channel
- local server = degesch.current_buffer.server
- if not chan or not servers[server.name] then return end
-
- -- In /commands there is typically no desire at all to add the at sign
- if data.location == 1 and data.words[1]:match "^/" then return end
-
- -- Handle both when the at sign is already there and when it is not
- local needle = word:gsub ("^@", ""):lower ()
-
- local t = {}
- local try = function (name)
- if data.location == 0 then name = name .. ":" end
- if name:sub (1, #needle):lower () == needle then
- table.insert (t, "@" .. name)
- end
- end
- for _, chan_user in ipairs (chan.users) do
- try (chan_user.user.nickname)
- end
- for _, special in ipairs { "channel", "here" } do
- try (special)
- end
- return t
-end)
diff --git a/plugins/degesch/thin-cursor.lua b/plugins/degesch/thin-cursor.lua
deleted file mode 100644
index d0fbf38..0000000
--- a/plugins/degesch/thin-cursor.lua
+++ /dev/null
@@ -1,27 +0,0 @@
---
--- thin-cursor.lua: set a thin cursor
---
--- Copyright (c) 2016, 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.
---
--- If tmux doesn't work, add the following to its configuration:
--- set -as terminal-overrides ',*:Ss=\E[%p1%d q:Se=\E[2 q'
--- Change the "2" as per http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
-
-local out = io.output ()
-out:write ("\x1b[6 q"):flush ()
-
--- By registering a global variable, we get notified about plugin unload
-x = setmetatable ({}, { __gc = function ()
- out:write ("\x1b[2 q"):flush ()
-end })
diff --git a/plugins/degesch/utm-filter.lua b/plugins/degesch/utm-filter.lua
deleted file mode 100644
index 63f85e3..0000000
--- a/plugins/degesch/utm-filter.lua
+++ /dev/null
@@ -1,62 +0,0 @@
---
--- utm-filter.lua: filter out Google Analytics bullshit from URLs
---
--- Copyright (c) 2015, 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 list of useless URL parameters that don't affect page function
-local banned = {
- gclid = 1,
-
- utm_source = 1,
- utm_medium = 1,
- utm_term = 1,
- utm_content = 1,
- utm_campaign = 1,
-}
-
--- Go through a parameter list and throw out any banned elements
-local do_args = function (args)
- local filtered = {}
- for part in args:gmatch ("[^&]+") do
- if not banned[part:match ("^[^=]*")] then
- table.insert (filtered, part)
- end
- end
- return table.concat (filtered, "&")
-end
-
--- Filter parameters in both the query and the fragment part of an URL
-local do_single_url = function (url)
- return url:gsub ('^([^?#]*)%?([^#]*)', function (start, query)
- local clean = do_args (query)
- return #clean > 0 and start .. "?" .. clean or start
- end, 1):gsub ('^([^#]*)#(.*)', function (start, fragment)
- local clean = do_args (fragment)
- return #clean > 0 and start .. "#" .. clean or start
- end, 1)
-end
-
-local do_text = function (text)
- return text:gsub ('%f[%g]https?://%g+', do_single_url)
-end
-
-degesch.hook_irc (function (hook, server, line)
- local start, message = line:match ("^(.* :)(.*)$")
- return message and start .. do_text (message) or line
-end)
-
-degesch.hook_input (function (hook, buffer, input)
- return do_text (input)
-end)
diff --git a/plugins/xB/calc b/plugins/xB/calc
new file mode 100755
index 0000000..e67244b
--- /dev/null
+++ b/plugins/xB/calc
@@ -0,0 +1,241 @@
+#!/usr/bin/env guile
+
+ xB calc plugin, basic Scheme evaluator
+
+ Copyright 2016 Přemysl Eric Janouch
+ See the file LICENSE for licensing information.
+
+!#
+
+(import (rnrs (6)))
+(use-modules ((rnrs) :version (6)))
+
+; --- Message parsing ----------------------------------------------------------
+
+(define-record-type message (fields prefix command params))
+(define (parse-message line)
+ (let f ([parts '()] [chars (string->list line)])
+ (define (take-word w chars)
+ (if (or (null? chars) (eqv? (car chars) #\x20))
+ (f (cons (list->string (reverse w)) parts)
+ (if (null? chars) chars (cdr chars)))
+ (take-word (cons (car chars) w) (cdr chars))))
+ (if (null? chars)
+ (let ([data (reverse parts)])
+ (when (< (length data) 2)
+ (error 'parse-message "invalid message"))
+ (make-message (car data) (cadr data) (cddr data)))
+ (if (null? parts)
+ (if (eqv? (car chars) #\:)
+ (take-word '() (cdr chars))
+ (f (cons #f parts) chars))
+ (if (eqv? (car chars) #\:)
+ (f (cons (list->string (cdr chars)) parts) '())
+ (take-word '() chars))))))
+
+; --- Utilities ----------------------------------------------------------------
+
+(define (display-exception e port)
+ (define (puts . x)
+ (for-all (lambda (a) (display a port)) x)
+ (newline port))
+
+ (define (record-fields rec)
+ (let* ([rtd (record-rtd rec)]
+ [v (record-type-field-names rtd)]
+ [len (vector-length v)])
+ (map (lambda (k i) (cons k ((record-accessor rtd i) rec)))
+ (vector->list v)
+ (let c ([i len] [ls '()])
+ (if (= i 0) ls (c (- i 1) (cons (- i 1) ls)))))))
+
+ (puts "Caught " (record-type-name (record-rtd e)))
+ (for-all
+ (lambda (subtype)
+ (puts " " (record-type-name (record-rtd subtype)))
+ (for-all
+ (lambda (field) (puts " " (car field) ": " (cdr field)))
+ (record-fields subtype)))
+ (simple-conditions e)))
+
+; XXX - we have to work around Guile's lack of proper eol-style support
+(define xc (make-transcoder (latin-1-codec) 'lf 'replace))
+(define irc-input-port (transcoded-port (standard-input-port) xc))
+(define irc-output-port (transcoded-port (standard-output-port) xc))
+
+(define (send . message)
+ (for-all (lambda (x) (display x irc-output-port)) message)
+ (display #\return irc-output-port)
+ (newline irc-output-port)
+ (flush-output-port irc-output-port))
+
+(define (get-line-crlf port)
+ (define line (get-line port))
+ (if (eof-object? line) line
+ (let ([len (string-length line)])
+ (if (and (> len 0) (eqv? (string-ref line (- len 1)) #\return))
+ (substring line 0 (- len 1)) line))))
+
+(define (get-config name)
+ (send "ZYKLONB get_config :" name)
+ (car (message-params (parse-message (get-line-crlf irc-input-port)))))
+
+(define (extract-nick prefix)
+ (do ([i 0 (+ i 1)] [len (string-length prefix)])
+ ([or (= i len) (char=? #\! (string-ref prefix i))]
+ [substring prefix 0 i])))
+
+(define (string-after s start)
+ (let ([s-len (string-length s)] [with-len (string-length start)])
+ (and (>= s-len with-len)
+ (string=? (substring s 0 with-len) start)
+ (substring s with-len s-len))))
+
+; --- Calculator ---------------------------------------------------------------
+
+; Evaluator derived from the example in The Scheme Programming Language.
+;
+; Even though EVAL with a carefully crafted environment would also do a good
+; job at sandboxing, it would probably be impossible to limit execution time...
+
+(define (env-new formals actuals env)
+ (cond [(null? formals) env]
+ [(symbol? formals) (cons (cons formals actuals) env)]
+ [else (cons (cons (car formals) (car actuals))
+ (env-new (cdr formals) (cdr actuals) env))]))
+(define (env-lookup var env) (cdr (assq var env)))
+(define (env-assign var val env) (set-cdr! (assq var env) val))
+
+(define (check-reductions r)
+ (if (= (car r) 0)
+ (error 'check-reductions "reduction limit exceeded")
+ (set-car! r (- (car r) 1))))
+
+; TODO - think about implementing more syntactical constructs,
+; however there's not much point in having anything else in a calculator...
+(define (exec expr r env)
+ (check-reductions r)
+ (cond [(symbol? expr) (env-lookup expr env)]
+ [(pair? expr)
+ (case (car expr)
+ [(quote) (cadr expr)]
+ [(lambda) (lambda vals
+ (let ([env (env-new (cadr expr) vals env)])
+ (let loop ([exprs (cddr expr)])
+ (if (null? (cdr exprs))
+ (exec (car exprs) r env)
+ (begin (exec (car exprs) r env)
+ (loop (cdr exprs)))))))]
+ [(if) (if (exec (cadr expr) r env)
+ (exec (caddr expr) r env)
+ (exec (cadddr expr) r env))]
+ [(set!) (env-assign (cadr expr) (exec (caddr expr) r env) env)]
+ [else (apply (exec (car expr) r env)
+ (map (lambda (x) (exec x r env)) (cdr expr)))])]
+ [else expr]))
+
+(define-syntax forward
+ (syntax-rules ()
+ [(_) '()]
+ [(_ a b ...) (cons (cons (quote a) a) (forward b ...))]))
+
+; ...which can't prevent me from simply importing most of the standard library
+(define base-library
+ (forward
+ ; Equivalence, procedure predicate, booleans
+ eqv? eq? equal? procedure? boolean? boolean=? not
+ ; numbers, numerical input and output
+ number? complex? real? rational? integer? exact? inexact? exact inexact
+ real-valued? rational-valued? integer-valued? number->string string->number
+ ; Arithmetic
+ = < > <= >= zero? positive? negative? odd? even? finite? infinite? nan?
+ min max + * - / abs div-and-mod div mod div0-and-mod0 div0 mod0
+ gcd lcm numerator denominator floor ceiling truncate round
+ rationalize exp log sin cos tan asin acos atan sqrt expt
+ make-rectangular make-polar real-part imag-part magnitude angle
+ ; Pairs and lists
+ map for-each cons car cdr caar cadr cdar cddr
+ caaar caadr cadar caddr cdaar cdadr cddar cdddr
+ caaaar caaadr caadar caaddr cadaar cadadr caddar cadddr
+ cdaaar cdaadr cdadar cdaddr cddaar cddadr cdddar cddddr
+ pair? null? list? list length append reverse list-tail list-ref
+ ; Symbols
+ symbol? symbol=? symbol->string string->symbol
+ ; Characters
+ char? char=? char char>? char<=? char>=? char->integer integer->char
+ ; Strings; XXX - omitted make-string - can cause OOM
+ string? string=? string string>? string<=? string>=?
+ string string-length string-ref substring
+ string-append string->list list->string string-for-each string-copy
+ ; Vectors; XXX - omitted make-vector - can cause OOM
+ vector? vector vector-length vector-ref vector-set!
+ vector->list list->vector vector-fill! vector-map vector-for-each
+ ; Control features
+ apply call/cc values call-with-values dynamic-wind))
+(define extended-library
+ (forward
+ char-upcase char-downcase char-titlecase char-foldcase
+ char-ci=? char-ci char-ci>? char-ci<=? char-ci>=?
+ char-alphabetic? char-numeric? char-whitespace?
+ char-upper-case? char-lower-case? char-title-case?
+ string-upcase string-downcase string-titlecase string-foldcase
+ string-ci=? string-ci string-ci>? string-ci<=? string-ci>=?
+ find for-all exists filter partition fold-left fold-right
+ remp remove remv remq memp member memv memq assp assoc assv assq cons*
+ list-sort vector-sort vector-sort!
+ bitwise-not bitwise-and bitwise-ior bitwise-xor bitwise-if
+ bitwise-bit-count bitwise-length bitwise-first-bit-set bitwise-bit-set?
+ bitwise-copy-bit bitwise-bit-field bitwise-copy-bit-field
+ bitwise-arithmetic-shift bitwise-rotate-bit-field bitwise-reverse-bit-field
+ bitwise-arithmetic-shift-left bitwise-arithmetic-shift-right
+ set-car! set-cdr! string-set! string-fill!))
+(define (interpret expr)
+ (exec expr '(2000) (append base-library extended-library)))
+
+; We could show something a bit nicer but it would be quite Guile-specific
+(define (error-string e)
+ (map (lambda (x) (string-append " " (symbol->string x)))
+ (filter (lambda (x) (not (member x '(&who &message &irritants &guile))))
+ (map (lambda (x) (record-type-name (record-rtd x)))
+ (simple-conditions e)))))
+
+(define (calc input respond)
+ (define (stringify x)
+ (call-with-string-output-port (lambda (port) (write x port))))
+ (guard (e [else (display-exception e (current-error-port))
+ (apply respond "caught" (error-string e))])
+ (let* ([input (open-string-input-port input)]
+ [data (let loop ()
+ (define datum (get-datum input))
+ (if (eof-object? datum) '() (cons datum (loop))))])
+ (call-with-values
+ (lambda () (interpret (list (append '(lambda ()) data))))
+ (lambda message
+ (for-all (lambda (x) (respond (stringify x))) message))))))
+
+; --- Main loop ----------------------------------------------------------------
+
+(define prefix (get-config "prefix"))
+(send "ZYKLONB register")
+
+(define (process msg)
+ (when (string-ci=? (message-command msg) "PRIVMSG")
+ (let* ([nick (extract-nick (message-prefix msg))]
+ [target (car (message-params msg))]
+ [response-begin
+ (apply string-append "PRIVMSG "
+ (if (memv (string-ref target 0) (string->list "#&!+"))
+ `(,target " :" ,nick ": ") `(,nick " :")))]
+ [respond (lambda args (apply send response-begin args))]
+ [text (cadr (message-params msg))]
+ [input (or (string-after text (string-append prefix "calc "))
+ (string-after text (string-append prefix "= ")))])
+ (when input (calc input respond)))))
+
+(let main-loop ()
+ (define line (get-line-crlf irc-input-port))
+ (unless (eof-object? line)
+ (guard (e [else (display-exception e (current-error-port))])
+ (unless (string=? "" line)
+ (process (parse-message line))))
+ (main-loop)))
diff --git a/plugins/xB/coin b/plugins/xB/coin
new file mode 100755
index 0000000..14cabb5
--- /dev/null
+++ b/plugins/xB/coin
@@ -0,0 +1,128 @@
+#!/usr/bin/env tclsh
+#
+# xB coin plugin, random number-based utilities
+#
+# Copyright 2012, 2014 Přemysl Eric Janouch
+# See the file LICENSE for licensing information.
+#
+
+# This is a terrible excuse for a programming language and I feel dirty.
+
+proc parse {line} {
+ global msg
+ unset -nocomplain msg
+
+ if [regexp {^:([^ ]*) *(.*)} $line -> prefix rest] {
+ set msg(prefix) $prefix
+ set line $rest
+ }
+ if [regexp {^([^ ]*) *(.*)} $line -> command rest] {
+ set msg(command) $command
+ set line $rest
+ }
+ while {1} {
+ set line [string trimleft $line " "]
+ set i [string first " " $line]
+ if {$i == -1} { set i [string length $line] }
+ if {$i == 0} { break }
+
+ if {[string index $line 0] == ":"} {
+ lappend msg(param) [string range $line 1 end]
+ break
+ }
+ lappend msg(param) [string range $line 0 [expr $i - 1]]
+ set line [string range $line $i end]
+ }
+}
+
+proc get_config {key} {
+ global msg
+ puts "ZYKLONB get_config :$key"
+ gets stdin line
+ parse $line
+ return [lindex $msg(param) 0]
+}
+
+proc pmrespond {text} {
+ global ctx
+ global ctx_quote
+ puts "PRIVMSG $ctx :$ctx_quote$text"
+}
+
+fconfigure stdin -translation crlf -encoding iso8859-1
+fconfigure stdout -translation crlf -encoding iso8859-1
+
+set prefix [get_config prefix]
+puts "ZYKLONB register"
+
+set eightball [list \
+ "It is certain" \
+ "It is decidedly so" \
+ "Without a doubt" \
+ "Yes - definitely" \
+ "You may rely on it" \
+ "As I see it, yes" \
+ "Most likely" \
+ "Outlook good" \
+ "Yes" \
+ "Signs point to yes" \
+ "Reply hazy, try again" \
+ "Ask again later" \
+ "Better not tell you now" \
+ "Cannot predict now" \
+ "Concentrate and ask again" \
+ "Don't count on it" \
+ "My reply is no" \
+ "My sources say no" \
+ "Outlook not so good" \
+ "Very doubtful"]
+
+while {[gets stdin line] != -1} {
+ parse $line
+
+ if {! [info exists msg(prefix)] || ! [info exists msg(command)]
+ || $msg(command) != "PRIVMSG" || ! [info exists msg(param)]
+ || [llength $msg(param)] < 2} { continue }
+
+ regexp {^[^!]*} $msg(prefix) ctx
+ if [regexp {^[#&+!]} [lindex $msg(param) 0]] {
+ set ctx_quote "$ctx: "
+ set ctx [lindex $msg(param) 0]
+ } else { set ctx_quote "" }
+
+ set input [lindex $msg(param) 1]
+ set first_chars [string range $input 0 \
+ [expr [string length $prefix] - 1]]
+ if {$first_chars != $prefix} { continue }
+ set input [string range $input [string length $prefix] end]
+
+ if {$input == "coin"} {
+ if {rand() < 0.5} {
+ pmrespond "Heads."
+ } else {
+ pmrespond "Tails."
+ }
+ } elseif {[regexp {^dice( +|$)(.*)} $input -> _ args]} {
+ if {! [string is integer -strict $args] || $args <= 0} {
+ pmrespond "Invalid or missing number."
+ } else {
+ pmrespond [expr {int($args * rand()) + 1}]
+ }
+ } elseif {[regexp {^(choose|\?)( +|$)(.*)} $input -> _ _ args]} {
+ if {$args == ""} {
+ pmrespond "Nothing to choose from."
+ } else {
+ set c [split $args ",|"]
+ pmrespond [string trim [lindex $c \
+ [expr {int([llength $c] * rand())}]]]
+ }
+ } elseif {[regexp {^eightball( +|$)(.*)} $input -> _ args]} {
+ if {$args == ""} {
+ pmrespond "You should, you know, ask something."
+ } else {
+ pmrespond [lindex $eightball \
+ [expr {int([llength $eightball] * rand())}]].
+ }
+ }
+}
+
diff --git a/plugins/xB/eval b/plugins/xB/eval
new file mode 100755
index 0000000..24e4050
--- /dev/null
+++ b/plugins/xB/eval
@@ -0,0 +1,312 @@
+#!/usr/bin/awk -f
+#
+# xB eval plugin, LISP-like expression evaluator
+#
+# Copyright 2013, 2014 Přemysl Eric Janouch
+# See the file LICENSE for licensing information.
+#
+
+BEGIN \
+{
+ RS = "\r"
+ ORS = "\r\n"
+ IGNORECASE = 1
+ srand()
+
+ prefix = get_config("prefix")
+
+ print "ZYKLONB register"
+ fflush("")
+
+ # All functions have to be in this particular array
+ min_args["int"] = 1
+ min_args["+"] = 1
+ min_args["-"] = 1
+ min_args["*"] = 1
+ min_args["/"] = 1
+ min_args["%"] = 1
+ min_args["^"] = 1
+ min_args["**"] = 1
+ min_args["exp"] = 1
+ min_args["sin"] = 1
+ min_args["cos"] = 1
+ min_args["atan2"] = 2
+ min_args["log"] = 1
+ min_args["rand"] = 0
+ min_args["sqrt"] = 1
+
+ min_args["pi"] = 0
+ min_args["e"] = 0
+
+ min_args["min"] = 1
+ min_args["max"] = 1
+
+ # Whereas here their presence is only optional
+ max_args["int"] = 1
+ max_args["sin"] = 1
+ max_args["cos"] = 1
+ max_args["atan2"] = 2
+ max_args["log"] = 1
+ max_args["rand"] = 0
+ max_args["sqrt"] = 1
+
+ max_args["pi"] = 0
+ max_args["e"] = 0
+}
+
+{
+ parse($0)
+}
+
+msg_command == "PRIVMSG" \
+{
+ # Context = either channel or user nickname
+ match(msg_prefix, /^[^!]+/)
+ ctx = substr(msg_prefix, RSTART, RLENGTH)
+ if (msg_param[0] ~ /^[#&!+]/)
+ {
+ ctx_quote = ctx ": "
+ ctx = msg_param[0]
+ }
+ else
+ ctx_quote = ""
+
+
+ if (substr(msg_param[1], 1, length(prefix)) == prefix)
+ {
+ keyword = "eval"
+ text = substr(msg_param[1], 1 + length(prefix))
+ if (match(text, "^" keyword "([^A-Za-z0-9].*|$)"))
+ process_request(substr(text, 1 + length(keyword)))
+ }
+}
+
+{
+ fflush("")
+}
+
+function pmrespond (text)
+{
+ print "PRIVMSG " ctx " :" ctx_quote text
+}
+
+function process_request (input, res, x)
+{
+ delete funs
+ delete accumulator
+ delete n_args
+
+ res = ""
+ fun_top = 0
+ funs[0] = ""
+ accumulator[0] = 0
+ n_args[0] = 0
+
+ if (match(input, "^[ \t]*"))
+ input = substr(input, RLENGTH + 1)
+ if (input == "")
+ res = "expression missing"
+
+ while (res == "" && input != "") {
+ if (match(input, "^-?[0-9]+\\.?[0-9]*")) {
+ x = substr(input, RSTART, RLENGTH)
+ input = substr(input, RLENGTH + 1)
+
+ match(input, "^ *")
+ input = substr(input, RLENGTH + 1)
+
+ res = process_argument(x)
+ } else if (match(input, "^[(]([^ ()]+)")) {
+ x = substr(input, RSTART + 1, RLENGTH - 1)
+ input = substr(input, RLENGTH + 1)
+
+ match(input, "^ *")
+ input = substr(input, RLENGTH + 1)
+
+ if (!(x in min_args)) {
+ res = "undefined function '" x "'"
+ } else {
+ fun_top++
+ funs[fun_top] = x
+ accumulator[fun_top] = 636363
+ n_args[fun_top] = 0
+ }
+ } else if (match(input, "^[)] *")) {
+ input = substr(input, RLENGTH + 1)
+ res = process_end()
+ } else
+ res = "invalid input at '" substr(input, 1, 10) "...'"
+ }
+
+ if (res == "") {
+ if (fun_top != 0)
+ res = "unclosed '" funs[fun_top] "'"
+ else if (n_args[0] != 1)
+ res = "internal error, expected one result" \
+ ", got " n_args[0] " instead"
+ }
+
+ if (res == "")
+ pmrespond(accumulator[0])
+ else
+ pmrespond(res)
+}
+
+function process_argument (arg)
+{
+ if (fun_top == 0) {
+ if (n_args[0]++ != 0)
+ return "too many results, I only expect one"
+
+ accumulator[0] = arg
+ return ""
+ }
+
+ fun = funs[fun_top]
+ if (fun in max_args && max_args[fun] <= n_args[fun_top])
+ return "too many operands for " fun
+
+ if (fun == "int") {
+ accumulator[fun_top] = int(arg)
+ } else if (fun == "+") {
+ if (n_args[fun_top] == 0)
+ accumulator[fun_top] = arg
+ else
+ accumulator[fun_top] += arg
+ } else if (fun == "-") {
+ if (n_args[fun_top] == 0)
+ accumulator[fun_top] = arg
+ else
+ accumulator[fun_top] -= arg
+ } else if (fun == "*") {
+ if (n_args[fun_top] == 0)
+ accumulator[fun_top] = arg
+ else
+ accumulator[fun_top] *= arg
+ } else if (fun == "/") {
+ if (n_args[fun_top] == 0)
+ accumulator[fun_top] = arg
+ else if (arg == 0)
+ return "division by zero"
+ else
+ accumulator[fun_top] /= arg
+ } else if (fun == "%") {
+ if (n_args[fun_top] == 0)
+ accumulator[fun_top] = arg
+ else if (arg == 0)
+ return "division by zero"
+ else
+ accumulator[fun_top] %= arg
+ } else if (fun == "^" || fun == "**" || fun == "exp") {
+ if (n_args[fun_top] == 0)
+ accumulator[fun_top] = arg
+ else
+ accumulator[fun_top] ^= arg
+ } else if (fun == "sin") {
+ accumulator[fun_top] = sin(arg)
+ } else if (fun == "cos") {
+ accumulator[fun_top] = cos(arg)
+ } else if (fun == "atan2") {
+ if (n_args[fun_top] == 0)
+ accumulator[fun_top] = arg
+ else
+ accumulator[fun_top] = atan2(accumulator[fun_top], arg)
+ } else if (fun == "log") {
+ accumulator[fun_top] = log(arg)
+ } else if (fun == "rand") {
+ # Just for completeness, execution never gets here
+ } else if (fun == "sqrt") {
+ accumulator[fun_top] = sqrt(arg)
+ } else if (fun == "min") {
+ if (n_args[fun_top] == 0)
+ accumulator[fun_top] = arg
+ else if (accumulator[fun_top] > arg)
+ accumulator[fun_top] = arg
+ } else if (fun == "max") {
+ if (n_args[fun_top] == 0)
+ accumulator[fun_top] = arg
+ else if (accumulator[fun_top] < arg)
+ accumulator[fun_top] = arg
+ } else
+ return "internal error, unhandled operands for " fun
+
+ n_args[fun_top]++
+ return ""
+}
+
+function process_end ()
+{
+ if (fun_top <= 0)
+ return "extraneous ')'"
+
+ fun = funs[fun_top]
+ if (!(fun in min_args))
+ return "internal error, unhandled ')' for '" fun "'"
+ if (min_args[fun] > n_args[fun_top])
+ return "not enough operands for '" fun "'"
+
+ # There's no 'init' function to do it in
+ if (fun == "rand")
+ accumulator[fun_top] = rand()
+ else if (fun == "pi")
+ accumulator[fun_top] = 3.141592653589793
+ else if (fun == "e")
+ accumulator[fun_top] = 2.718281828459045
+
+ return process_argument(accumulator[fun_top--])
+}
+
+function get_config (key)
+{
+ print "ZYKLONB get_config :" key
+ fflush("")
+
+ getline
+ parse($0)
+ return msg_param[0]
+}
+
+function parse (line, s, n, id, token)
+{
+ s = 1
+ id = 0
+
+ # NAWK only uses the first character of RS
+ if (line ~ /^\n/)
+ line = substr(line, 2)
+
+ msg_prefix = ""
+ msg_command = ""
+ delete msg_param
+
+ n = match(substr(line, s), / |$/)
+ while (n)
+ {
+ token = substr(line, s, n - 1)
+ if (token ~ /^:/)
+ {
+ if (s == 1)
+ msg_prefix = substr(token, 2)
+ else
+ {
+ msg_param[id] = substr(line, s + 1)
+ break
+ }
+ }
+ else if (!msg_command)
+ msg_command = toupper(token)
+ else
+ msg_param[id++] = token
+
+ s = s + n
+ n = index(substr(line, s), " ")
+
+ if (!n)
+ {
+ n = length(substr(line, s)) + 1
+ if (n == 1)
+ break;
+ }
+ }
+}
+
diff --git a/plugins/xB/factoids b/plugins/xB/factoids
new file mode 100755
index 0000000..9e9a7b4
--- /dev/null
+++ b/plugins/xB/factoids
@@ -0,0 +1,177 @@
+#!/usr/bin/env perl
+#
+# xB factoids plugin
+#
+# Copyright 2016 Přemysl Eric Janouch
+# See the file LICENSE for licensing information.
+#
+
+use strict;
+use warnings;
+use Text::Wrap;
+
+# --- IRC protocol -------------------------------------------------------------
+
+binmode STDIN; select STDIN; $| = 1; $/ = "\r\n";
+binmode STDOUT; select STDOUT; $| = 1; $\ = "\r\n";
+
+sub parse ($) {
+ chomp (my $line = shift);
+ return undef unless my ($nick, $user, $host, $command, $args) = ($line =~
+ qr/^(?::([^! ]*)(?:!([^@]*)@([^ ]*))? +)?([^ ]+)(?: +(.*))?$/o);
+ return {nick => $nick, user => $user, host => $host, command => $command,
+ args => defined $args ? [$args =~ /:?((?<=:).*|[^ ]+) */og] : []};
+}
+
+sub bot_print {
+ print "ZYKLONB print :${\shift}";
+}
+
+# --- Initialization -----------------------------------------------------------
+
+my %config;
+for my $name (qw(prefix)) {
+ print "ZYKLONB get_config :$name";
+ $config{$name} = (parse )->{args}->[0];
+}
+
+print "ZYKLONB register";
+
+# --- Database -----------------------------------------------------------------
+# Simple map of (factoid_name => [definitions]); all factoids are separated
+# by newlines and definitions by carriage returns. Both disallowed in IRC.
+
+sub db_load {
+ local $/ = "\n";
+ my ($path) = @_;
+ open my $db, "<", $path or return {};
+
+ my %entries;
+ while (<$db>) {
+ chomp;
+ my @defs = split "\r";
+ $entries{shift @defs} = \@defs;
+ }
+ \%entries
+}
+
+sub db_save {
+ local $\ = "\n";
+ my ($path, $ref) = @_;
+ my $path_new = "$path.new";
+ open my $db, ">", $path_new or die "db save failed: $!";
+
+ my %entries = %$ref;
+ print $db join "\r", ($_, @{$entries{$_}}) for keys %entries;
+ close $db;
+ rename $path_new, $path or die "db save failed: $!";
+}
+
+# --- Factoids -----------------------------------------------------------------
+
+my $db_path = 'factoids.db';
+my %db = %{db_load $db_path};
+
+sub learn {
+ my ($respond, $input) = @_;
+ return &$respond("usage: = ")
+ unless $input =~ /^([^=]+?)(?:\s+(\d+))?\s*=\s*(.+?)\s*$/;
+
+ my ($name, $number, $definition) = ($1, $2, $3);
+ return &$respond("trailing numbers in names are disallowed")
+ if defined $2;
+ $db{$name} = [] unless exists $db{$name};
+
+ my $entries = $db{$name};
+ return &$respond("duplicate definition")
+ if grep { lc $_ eq lc $definition } @$entries;
+
+ push @$entries, $definition;
+ &$respond("saved as #${\scalar @$entries}");
+ db_save $db_path, \%db;
+}
+
+sub check_number {
+ my ($respond, $name, $number) = @_;
+ my $entries = $db{$name};
+ if ($number > @$entries) {
+ &$respond(qq/"$name" has only ${\scalar @$entries} definitions/);
+ } elsif (not $number) {
+ &$respond("number must not be zero");
+ } else {
+ return 1;
+ }
+ return 0;
+}
+
+sub forget {
+ my ($respond, $input) = @_;
+ return &$respond("usage: ")
+ unless $input =~ /^([^=]+?)\s+(\d+)\s*$/;
+
+ my ($name, $number) = ($1, int($2));
+ return &$respond(qq/"$name" is undefined/)
+ unless exists $db{$name};
+
+ my $entries = $db{$name};
+ return unless check_number $respond, $name, $number;
+
+ splice @$entries, --$number, 1;
+ &$respond("forgotten");
+ db_save $db_path, \%db;
+}
+
+sub whatis {
+ my ($respond, $input) = @_;
+ return &$respond("usage: []")
+ unless $input =~ /^([^=]+?)(?:\s+(\d+))?\s*$/;
+
+ my ($name, $number) = ($1, $2);
+ return &$respond(qq/"$name" is undefined/)
+ unless exists $db{$name};
+
+ my $entries = $db{$name};
+ if (defined $number) {
+ return unless check_number $respond, $name, $number;
+ &$respond(qq/"$name" is #$number $entries->[$number - 1]/);
+ } else {
+ my $i = 1;
+ my $definition = join ", ", map { "#${\$i++} $_" } @{$entries};
+ &$respond(qq/"$name" is $definition/);
+ }
+}
+
+sub wildcard {
+ my ($respond, $input) = @_;
+ $input =~ /=/ ? learn(@_) : whatis(@_);
+}
+
+my %commands = (
+ 'learn' => \&learn,
+ 'forget' => \&forget,
+ 'whatis' => \&whatis,
+ '??' => \&wildcard,
+);
+
+# --- Input loop ---------------------------------------------------------------
+
+while (my $line = ) {
+ my %msg = %{parse $line};
+ my @args = @{$msg{args}};
+
+ # This plugin only bothers to respond to PRIVMSG messages
+ next unless $msg{command} eq 'PRIVMSG' and @args >= 2
+ and my ($cmd, $input) = $args[1] =~ /^$config{prefix}(\S+)\s*(.*)/;
+
+ # So far the only reaction is a PRIVMSG back to the sender, so all the
+ # handlers need is a response callback and all arguments to the command
+ my ($target => $quote) = ($args[0] =~ /^[#+&!]/)
+ ? ($args[0] => "$msg{nick}: ") : ($msg{nick} => '');
+ # Wrap all responses so that there's space for our prefix in the message
+ my $respond = sub {
+ local ($Text::Wrap::columns, $Text::Wrap::unexpand) = 400, 0;
+ my $start = "PRIVMSG $target :$quote";
+ print for split "\n", wrap $start, $start, shift;
+ };
+ &{$commands{$cmd}}($respond, $input) if exists($commands{$cmd});
+}
diff --git a/plugins/xB/pomodoro b/plugins/xB/pomodoro
new file mode 100755
index 0000000..08b87cb
--- /dev/null
+++ b/plugins/xB/pomodoro
@@ -0,0 +1,502 @@
+#!/usr/bin/env ruby
+# coding: utf-8
+#
+# xB pomodoro plugin
+#
+# Copyright 2015 Přemysl Eric Janouch
+# See the file LICENSE for licensing information.
+#
+
+# --- Simple event loop --------------------------------------------------------
+
+# This is more or less a straight-forward port of my C event loop. It's a bit
+# unfortunate that I really have to implement all this in order to get some
+# basic asynchronicity but at least I get to exercise my Ruby.
+
+class TimerEvent
+ attr_accessor :index, :when, :callback
+
+ def initialize (callback)
+ raise ArgumentError unless callback.is_a? Proc
+
+ @index = nil
+ @when = nil
+ @callback = callback
+ end
+
+ def active?
+ @index != nil
+ end
+
+ def until
+ return @when - Time.new
+ end
+end
+
+class IOEvent
+ READ = 1 << 0
+ WRITE = 1 << 1
+
+ attr_accessor :read_index, :write_index, :io, :callback
+
+ def initialize (io, callback)
+ raise ArgumentError unless callback.is_a? Proc
+
+ @read_index = nil
+ @write_index = nil
+ @io = io
+ @callback = callback
+ end
+end
+
+class EventLoop
+ def initialize
+ @running = false
+ @timers = []
+ @readers = []
+ @writers = []
+ @io_to_event = {}
+ end
+
+ def set_timer (timer, timeout)
+ raise ArgumentError unless timer.is_a? TimerEvent
+
+ timer.when = Time.now + timeout
+ if timer.index
+ heapify_down timer.index
+ heapify_up timer.index
+ else
+ timer.index = @timers.size
+ @timers.push timer
+ heapify_up timer.index
+ end
+ end
+
+ def reset_timer (timer)
+ raise ArgumentError unless timer.is_a? TimerEvent
+ remove_timer_at timer.index if timer.index
+ end
+
+ def set_io (io_event, events)
+ raise ArgumentError unless io_event.is_a? IOEvent
+ raise ArgumentError unless events.is_a? Numeric
+
+ reset_io io_event
+
+ @io_to_event[io_event.io] = io_event
+ if events & IOEvent::READ
+ io_event.read_index = @readers.size
+ @readers.push io_event.io
+ end
+ if events & IOEvent::WRITE
+ io_event.read_index = @writers.size
+ @writers.push io_event.io
+ end
+ end
+
+ def reset_io (io_event)
+ raise ArgumentError unless io_event.is_a? IOEvent
+
+ @readers.delete_at io_event.read_index if io_event.read_index
+ @writers.delete_at io_event.write_index if io_event.write_index
+
+ io_event.read_index = nil
+ io_event.write_index = nil
+
+ @io_to_event.delete io_event.io
+ end
+
+ def run
+ @running = true
+ while @running do one_iteration end
+ end
+
+ def quit
+ @running = false
+ end
+
+private
+ def one_iteration
+ rs, ws, = IO.select @readers, @writers, [], nearest_timeout
+ dispatch_timers
+ (Array(rs) | Array(ws)).each do |io|
+ @io_to_event[io].callback.call io
+ end
+ end
+
+ def dispatch_timers
+ now = Time.new
+ while not @timers.empty? and @timers[0].when <= now do
+ @timers[0].callback.call
+ remove_timer_at 0
+ end
+ end
+
+ def nearest_timeout
+ return nil if @timers.empty?
+ timeout = @timers[0].until
+ if timeout < 0 then 0 else timeout end
+ end
+
+ def remove_timer_at (index)
+ @timers[index].index = nil
+ moved = @timers.pop
+ return if index == @timers.size
+
+ @timers[index] = moved
+ @timers[index].index = index
+ heapify_down index
+ end
+
+ def swap_timers (a, b)
+ @timers[a], @timers[b] = @timers[b], @timers[a]
+ @timers[a].index = a
+ @timers[b].index = b
+ end
+
+ def heapify_up (index)
+ while index != 0 do
+ parent = (index - 1) / 2
+ break if @timers[parent].when <= @timers[index].when
+ swap_timers index, parent
+ index = parent
+ end
+ end
+
+ def heapify_down (index)
+ loop do
+ parent = index
+ left = 2 * index + 1
+ right = 2 * index + 2
+
+ lowest = parent
+ lowest = left if left < @timers.size and
+ @timers[left] .when < @timers[lowest].when
+ lowest = right if right < @timers.size and
+ @timers[right].when < @timers[lowest].when
+ break if parent == lowest
+
+ swap_timers lowest, parent
+ index = lowest
+ end
+ end
+end
+
+# --- IRC protocol -------------------------------------------------------------
+
+$stdin.set_encoding 'ASCII-8BIT'
+$stdout.set_encoding 'ASCII-8BIT'
+
+$stdin.sync = true
+$stdout.sync = true
+
+$/ = "\r\n"
+$\ = "\r\n"
+
+RE_MSG = /(?::([^! ]*)(?:!([^@]*)@([^ ]*))? +)?([^ ]+)(?: +(.*))?$/
+RE_ARGS = /:?((?<=:).*|[^ ]+) */
+
+def parse (line)
+ m = line.match RE_MSG
+ return nil if not m
+
+ nick, user, host, command, args = *m.captures
+ args = if args then args.scan(RE_ARGS).flatten else [] end
+ [nick, user, host, command, args]
+end
+
+def bot_print (what)
+ print "ZYKLONB print :#{what}"
+end
+
+# --- Initialization -----------------------------------------------------------
+
+# We can only read in configuration from here so far
+# To read it from anywhere else, it has to be done asynchronously
+$config = {}
+[:prefix].each do |name|
+ print "ZYKLONB get_config :#{name}"
+ _, _, _, _, args = *parse($stdin.gets.chomp)
+ $config[name] = args[0]
+end
+
+print "ZYKLONB register"
+
+# --- Plugin logic -------------------------------------------------------------
+
+# FIXME: this needs a major refactor as it doesn't make much sense at all
+
+class MessageMeta < Struct.new(:nick, :user, :host, :channel, :ctx, :quote)
+ def respond (message)
+ print "PRIVMSG #{ctx} :#{quote}#{message}"
+ end
+end
+
+class Context
+ attr_accessor :nick, :ctx
+
+ def initialize (meta)
+ @nick = meta.nick
+ @ctx = meta.ctx
+ end
+
+ def == (other)
+ self.class == other.class \
+ and other.nick == @nick \
+ and other.ctx == @ctx
+ end
+
+ alias eql? ==
+
+ def hash
+ @nick.hash ^ @ctx.hash
+ end
+end
+
+class PomodoroTimer
+ def initialize (context)
+ @ctx = context.ctx
+ @nicks = [context.nick]
+
+ @timer_work = TimerEvent.new(lambda { on_work })
+ @timer_rest = TimerEvent.new(lambda { on_rest })
+
+ on_work
+ end
+
+ def inform (message)
+ # FIXME: it tells the nick even in PM's
+ quote = "#{@nicks.join(" ")}: "
+ print "PRIVMSG #{@ctx} :#{quote}#{message}"
+ end
+
+ def on_work
+ inform "work now!"
+ $loop.set_timer @timer_rest, 25 * 60
+ end
+
+ def on_rest
+ inform "rest now!"
+ $loop.set_timer @timer_work, 5 * 60
+ end
+
+ def join (meta)
+ return if @nicks.include? meta.nick
+
+ meta.respond "you have joined their pomodoro"
+ @nicks |= [meta.nick]
+ end
+
+ def part (meta, requested)
+ return if not @nicks.include? meta.nick
+
+ if requested
+ meta.respond "you have stopped your pomodoro"
+ end
+
+ @nicks -= [meta.nick]
+ if @nicks.empty?
+ $loop.reset_timer @timer_work
+ $loop.reset_timer @timer_rest
+ end
+ end
+
+ def status (meta)
+ return if not @nicks.include? meta.nick
+
+ if @timer_rest.active?
+ till = @timer_rest.until
+ meta.respond "working, #{(till / 60).to_i} minutes, " +
+ "#{(till % 60).to_i} seconds until rest"
+ end
+ if @timer_work.active?
+ till = @timer_work.until
+ meta.respond "resting, #{(till / 60).to_i} minutes, " +
+ "#{(till % 60).to_i} seconds until work"
+ end
+ end
+end
+
+class Pomodoro
+ KEYWORD = "pomodoro"
+
+ def initialize
+ @timers = {}
+ end
+
+ def on_help (meta, args)
+ meta.respond "usage: #{KEYWORD} { start | stop | join | status }"
+ end
+
+ def on_start (meta, args)
+ if args.size != 0
+ meta.respond "usage: #{KEYWORD} start"
+ return
+ end
+
+ context = Context.new meta
+ if @timers[context]
+ meta.respond "you already have a timer running here"
+ else
+ @timers[context] = PomodoroTimer.new meta
+ end
+ end
+
+ def on_join (meta, args)
+ if args.size != 1
+ meta.respond "usage: #{KEYWORD} join "
+ return
+ end
+
+ context = Context.new meta
+ if @timers[context]
+ meta.respond "you already have a timer running here"
+ return
+ end
+
+ joined_context = Context.new meta
+ joined_context.nick = args.shift
+ timer = @timers[joined_context]
+ if not timer
+ meta.respond "that person doesn't have a timer here"
+ else
+ timer.join meta
+ @timers[context] = timer
+ end
+ end
+
+ def on_stop (meta, args)
+ if args.size != 0
+ meta.respond "usage: #{KEYWORD} stop"
+ return
+ end
+
+ context = Context.new meta
+ timer = @timers[context]
+ if not timer
+ meta.respond "you don't have a timer running here"
+ else
+ timer.part meta, true
+ @timers.delete context
+ end
+ end
+
+ def on_status (meta, args)
+ if args.size != 0
+ meta.respond "usage: #{KEYWORD} status"
+ return
+ end
+
+ timer = @timers[Context.new meta]
+ if not timer
+ meta.respond "you don't have a timer running here"
+ else
+ timer.status meta
+ end
+ end
+
+ def process_command (meta, msg)
+ args = msg.split
+ return if args.shift != KEYWORD
+
+ method = "on_#{args.shift}"
+ send method, meta, args if respond_to? method
+ end
+
+ def on_server_nick (meta, command, args)
+ # TODO: either handle this properly...
+ happened = false
+ @timers.keys.each do |key|
+ next if key.nick != meta.nick
+ @timers[key].part meta, false
+ @timers.delete key
+ happened = true
+ end
+ if happened
+ # TODO: ...or at least inform the user via his new nick
+ end
+ end
+
+ def on_server_part (meta, command, args)
+ # TODO: instead of cancelling the user's pomodoros, either redirect
+ # them to PM's and later upon rejoining undo the redirection...
+ context = Context.new(meta)
+ context.ctx = meta.channel
+ if @timers.include? context
+ # TODO: ...or at least inform the user about the cancellation
+ @timers[context].part meta, false
+ @timers.delete context
+ end
+ end
+
+ def on_server_quit (meta, command, args)
+ @timers.keys.each do |key|
+ next if key.nick != meta.nick
+ @timers[key].part meta, false
+ @timers.delete key
+ end
+ end
+
+ def process (meta, command, args)
+ method = "on_server_#{command.downcase}"
+ send method, meta, command, args if respond_to? method
+ end
+end
+
+# --- IRC message processing ---------------------------------------------------
+
+$handlers = [Pomodoro.new]
+def process_line (line)
+ msg = parse line
+ return if not msg
+
+ nick, user, host, command, args = *msg
+
+ context = nick
+ quote = ""
+ channel = nil
+
+ if args.size >= 1 and args[0].start_with? ?#, ?+, ?&, ?!
+ case command
+ when "PRIVMSG", "NOTICE", "JOIN"
+ context = args[0]
+ quote = "#{nick}: "
+ channel = args[0]
+ when "PART"
+ channel = args[0]
+ end
+ end
+
+ # Handle any IRC message
+ meta = MessageMeta.new(nick, user, host, channel, context, quote).freeze
+ $handlers.each do |handler|
+ handler.process meta, command, args
+ end
+
+ # Handle pre-processed bot commands
+ if command == 'PRIVMSG' and args.size >= 2
+ msg = args[1]
+ return unless msg.start_with? $config[:prefix]
+ $handlers.each do |handler|
+ handler.process_command meta, msg[$config[:prefix].size..-1]
+ end
+ end
+end
+
+buffer = ""
+stdin_io = IOEvent.new($stdin, lambda do |io|
+ begin
+ buffer << io.read_nonblock(4096)
+ lines = buffer.split $/, -1
+ buffer = lines.pop
+ lines.each { |line| process_line line }
+ rescue EOFError
+ $loop.quit
+ rescue IO::WaitReadable
+ # Ignore
+ end
+end)
+
+$loop = EventLoop.new
+$loop.set_io stdin_io, IOEvent::READ
+$loop.run
diff --git a/plugins/xB/script b/plugins/xB/script
new file mode 100755
index 0000000..948e7e5
--- /dev/null
+++ b/plugins/xB/script
@@ -0,0 +1,2310 @@
+#!/usr/bin/tcc -run -lm
+//
+// xB scripting plugin, using a custom stack-based language
+//
+// Copyright 2014 Přemysl Eric Janouch
+// See the file LICENSE for licensing information.
+//
+// Just compile this file as usual (sans #!) if you don't feel like using TCC.
+// It is a very basic and portable C99 application. It's not supposed to be
+// very sophisticated, for it'd get extremely big.
+//
+// The main influences of the language were Factor and Joy, stripped of all
+// even barely complex stuff. In its current state, it's only really useful as
+// a calculator but it's got great potential for extending.
+//
+// If you don't like something, just change it; this is just an experiment.
+//
+// NOTE: it is relatively easy to abuse. Be careful.
+//
+
+#define _XOPEN_SOURCE 500
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define ADDRESS_SPACE_LIMIT (100 * 1024 * 1024)
+#include
+
+#if defined __GNUC__
+#define ATTRIBUTE_PRINTF(x, y) __attribute__ ((format (printf, x, y)))
+#else // ! __GNUC__
+#define ATTRIBUTE_PRINTF(x, y)
+#endif // ! __GNUC__
+
+#define N_ELEMENTS(a) (sizeof (a) / sizeof ((a)[0]))
+
+// --- Utilities ---------------------------------------------------------------
+
+static char *strdup_printf (const char *format, ...) ATTRIBUTE_PRINTF (1, 2);
+
+static char *
+strdup_vprintf (const char *format, va_list ap)
+{
+ va_list aq;
+ va_copy (aq, ap);
+ int size = vsnprintf (NULL, 0, format, aq);
+ va_end (aq);
+ if (size < 0)
+ return NULL;
+
+ char buf[size + 1];
+ size = vsnprintf (buf, sizeof buf, format, ap);
+ if (size < 0)
+ return NULL;
+
+ return strdup (buf);
+}
+
+static char *
+strdup_printf (const char *format, ...)
+{
+ va_list ap;
+ va_start (ap, format);
+ char *result = strdup_vprintf (format, ap);
+ va_end (ap);
+ return result;
+}
+
+// --- Generic buffer ----------------------------------------------------------
+
+struct buffer
+{
+ char *s; ///< Buffer data
+ size_t alloc; ///< Number of bytes allocated
+ size_t len; ///< Number of bytes used
+ bool memory_failure; ///< Memory allocation failed
+};
+
+#define BUFFER_INITIALIZER { NULL, 0, 0, false }
+
+static bool
+buffer_append (struct buffer *self, const void *s, size_t n)
+{
+ if (self->memory_failure)
+ return false;
+
+ if (!self->s)
+ self->s = malloc (self->alloc = 8);
+ while (self->len + n > self->alloc)
+ self->s = realloc (self->s, self->alloc <<= 1);
+
+ if (!self->s)
+ {
+ self->memory_failure = true;
+ return false;
+ }
+
+ memcpy (self->s + self->len, s, n);
+ self->len += n;
+ return true;
+}
+
+inline static bool
+buffer_append_c (struct buffer *self, char c)
+{
+ return buffer_append (self, &c, 1);
+}
+
+// --- Data types --------------------------------------------------------------
+
+enum item_type
+{
+ ITEM_STRING,
+ ITEM_WORD,
+ ITEM_INTEGER,
+ ITEM_FLOAT,
+ ITEM_LIST
+};
+
+struct item
+{
+#define ITEM_HEADER \
+ enum item_type type; /**< The type of this object */ \
+ struct item *next; /**< Next item on the list/stack */
+
+ ITEM_HEADER
+};
+
+struct item_string
+{
+ ITEM_HEADER
+ size_t len; ///< Length of the string (sans '\0')
+ char value[]; ///< The null-terminated string value
+};
+
+#define get_string(item) \
+ (assert ((item)->type == ITEM_STRING), \
+ ((struct item_string *)(item))->value)
+
+/// It looks like a string but it doesn't quack like a string
+#define item_word item_string
+
+#define get_word(item) \
+ (assert ((item)->type == ITEM_WORD), \
+ ((struct item_word *)(item))->value)
+
+struct item_integer
+{
+ ITEM_HEADER
+ long long value; ///< The integer value
+};
+
+#define get_integer(item) \
+ (assert ((item)->type == ITEM_INTEGER), \
+ ((struct item_integer *)(item))->value)
+
+struct item_float
+{
+ ITEM_HEADER
+ long double value; ///< The floating point value
+};
+
+#define get_float(item) \
+ (assert ((item)->type == ITEM_FLOAT), \
+ ((struct item_float *)(item))->value)
+
+struct item_list
+{
+ ITEM_HEADER
+ struct item *head; ///< The head of the list
+};
+
+#define get_list(item) \
+ (assert ((item)->type == ITEM_LIST), \
+ ((struct item_list *)(item))->head)
+
+#define set_list(item, head_) \
+ (assert ((item)->type == ITEM_LIST), \
+ item_free_list (((struct item_list *)(item))->head), \
+ ((struct item_list *)(item))->head = (head_))
+
+const char *
+item_type_to_str (enum item_type type)
+{
+ switch (type)
+ {
+ case ITEM_STRING: return "string";
+ case ITEM_WORD: return "word";
+ case ITEM_INTEGER: return "integer";
+ case ITEM_FLOAT: return "float";
+ case ITEM_LIST: return "list";
+ }
+ abort ();
+}
+
+// --- Item management ---------------------------------------------------------
+
+static void item_free_list (struct item *);
+static struct item *new_clone_list (const struct item *);
+
+static void
+item_free (struct item *item)
+{
+ if (item->type == ITEM_LIST)
+ item_free_list (get_list (item));
+ free (item);
+}
+
+static void
+item_free_list (struct item *item)
+{
+ while (item)
+ {
+ struct item *link = item;
+ item = item->next;
+ item_free (link);
+ }
+}
+
+static struct item *
+new_clone (const struct item *item)
+{
+ size_t size;
+ switch (item->type)
+ {
+ case ITEM_STRING:
+ case ITEM_WORD:
+ {
+ const struct item_string *x = (const struct item_string *) item;
+ size = sizeof *x + x->len + 1;
+ break;
+ }
+ case ITEM_INTEGER: size = sizeof (struct item_integer); break;
+ case ITEM_FLOAT: size = sizeof (struct item_float); break;
+ case ITEM_LIST: size = sizeof (struct item_list); break;
+ }
+
+ struct item *clone = malloc (size);
+ if (!clone)
+ return NULL;
+
+ memcpy (clone, item, size);
+ if (item->type == ITEM_LIST)
+ {
+ struct item_list *x = (struct item_list *) clone;
+ if (x->head && !(x->head = new_clone_list (x->head)))
+ {
+ free (clone);
+ return NULL;
+ }
+ }
+ clone->next = NULL;
+ return clone;
+}
+
+static struct item *
+new_clone_list (const struct item *item)
+{
+ struct item *head = NULL, *clone;
+ for (struct item **out = &head; item; item = item->next)
+ {
+ if (!(clone = *out = new_clone (item)))
+ {
+ item_free_list (head);
+ return NULL;
+ }
+ clone->next = NULL;
+ out = &clone->next;
+ }
+ return head;
+}
+
+static struct item *
+new_string (const char *s, ssize_t len)
+{
+ if (len < 0)
+ len = strlen (s);
+
+ struct item_string *item = calloc (1, sizeof *item + len + 1);
+ if (!item)
+ return NULL;
+
+ item->type = ITEM_STRING;
+ item->len = len;
+ memcpy (item->value, s, len);
+ item->value[len] = '\0';
+ return (struct item *) item;
+}
+
+static struct item *
+new_word (const char *s, ssize_t len)
+{
+ struct item *item = new_string (s, len);
+ if (!item)
+ return NULL;
+
+ item->type = ITEM_WORD;
+ return item;
+}
+
+static struct item *
+new_integer (long long value)
+{
+ struct item_integer *item = calloc (1, sizeof *item);
+ if (!item)
+ return NULL;
+
+ item->type = ITEM_INTEGER;
+ item->value = value;
+ return (struct item *) item;
+}
+
+static struct item *
+new_float (long double value)
+{
+ struct item_float *item = calloc (1, sizeof *item);
+ if (!item)
+ return NULL;
+
+ item->type = ITEM_FLOAT;
+ item->value = value;
+ return (struct item *) item;
+}
+
+static struct item *
+new_list (struct item *head)
+{
+ struct item_list *item = calloc (1, sizeof *item);
+ if (!item)
+ return NULL;
+
+ item->type = ITEM_LIST;
+ item->head = head;
+ return (struct item *) item;
+}
+
+// --- Parsing -----------------------------------------------------------------
+
+#define PARSE_ERROR_TABLE(XX) \
+ XX( OK, NULL ) \
+ XX( EOF, "unexpected end of input" ) \
+ XX( INVALID_HEXA_ESCAPE, "invalid hexadecimal escape sequence" ) \
+ XX( INVALID_ESCAPE, "unrecognized escape sequence" ) \
+ XX( MEMORY, "memory allocation failure" ) \
+ XX( FLOAT_RANGE, "floating point value out of range" ) \
+ XX( INTEGER_RANGE, "integer out of range" ) \
+ XX( INVALID_INPUT, "invalid input" ) \
+ XX( UNEXPECTED_INPUT, "unexpected input" )
+
+enum tokenizer_error
+{
+#define XX(x, y) PARSE_ERROR_ ## x,
+ PARSE_ERROR_TABLE (XX)
+#undef XX
+ PARSE_ERROR_COUNT
+};
+
+struct tokenizer
+{
+ const char *cursor;
+ enum tokenizer_error error;
+};
+
+static bool
+decode_hexa_escape (struct tokenizer *self, struct buffer *buf)
+{
+ int i;
+ char c, code = 0;
+
+ for (i = 0; i < 2; i++)
+ {
+ c = tolower (*self->cursor);
+ if (c >= '0' && c <= '9')
+ code = (code << 4) | (c - '0');
+ else if (c >= 'a' && c <= 'f')
+ code = (code << 4) | (c - 'a' + 10);
+ else
+ break;
+
+ self->cursor++;
+ }
+
+ if (!i)
+ return false;
+
+ buffer_append_c (buf, code);
+ return true;
+}
+
+static bool
+decode_octal_escape (struct tokenizer *self, struct buffer *buf)
+{
+ int i;
+ char c, code = 0;
+
+ for (i = 0; i < 3; i++)
+ {
+ c = *self->cursor;
+ if (c < '0' || c > '7')
+ break;
+
+ code = (code << 3) | (c - '0');
+ self->cursor++;
+ }
+
+ if (!i)
+ return false;
+
+ buffer_append_c (buf, code);
+ return true;
+}
+
+static bool
+decode_escape_sequence (struct tokenizer *self, struct buffer *buf)
+{
+ // Support some basic escape sequences from the C language
+ char c;
+ switch ((c = *self->cursor))
+ {
+ case '\0':
+ self->error = PARSE_ERROR_EOF;
+ return false;
+ case 'x':
+ case 'X':
+ self->cursor++;
+ if (decode_hexa_escape (self, buf))
+ return true;
+
+ self->error = PARSE_ERROR_INVALID_HEXA_ESCAPE;
+ return false;
+ default:
+ if (decode_octal_escape (self, buf))
+ return true;
+
+ self->cursor++;
+ const char *from = "abfnrtv\"\\", *to = "\a\b\f\n\r\t\v\"\\", *x;
+ if ((x = strchr (from, c)))
+ {
+ buffer_append_c (buf, to[x - from]);
+ return true;
+ }
+
+ self->error = PARSE_ERROR_INVALID_ESCAPE;
+ return false;
+ }
+}
+
+static struct item *
+parse_string (struct tokenizer *self)
+{
+ struct buffer buf = BUFFER_INITIALIZER;
+ struct item *item = NULL;
+ char c;
+
+ while (true)
+ switch ((c = *self->cursor++))
+ {
+ case '\0':
+ self->cursor--;
+ self->error = PARSE_ERROR_EOF;
+ goto end;
+ case '"':
+ if (buf.memory_failure
+ || !(item = new_string (buf.s, buf.len)))
+ self->error = PARSE_ERROR_MEMORY;
+ goto end;
+ case '\\':
+ if (decode_escape_sequence (self, &buf))
+ break;
+ goto end;
+ default:
+ buffer_append_c (&buf, c);
+ }
+
+end:
+ free (buf.s);
+ return item;
+}
+
+static struct item *
+try_parse_number (struct tokenizer *self)
+{
+ // These two standard library functions can digest a lot of various inputs,
+ // including NaN and +/- infinity. That may get a bit confusing.
+ char *float_end;
+ errno = 0;
+ long double float_value = strtold (self->cursor, &float_end);
+ int float_errno = errno;
+
+ char *int_end;
+ errno = 0;
+ long long int_value = strtoll (self->cursor, &int_end, 10);
+ int int_errno = errno;
+
+ // If they both fail, then this is most probably not a number.
+ if (float_end == int_end && float_end == self->cursor)
+ return NULL;
+
+ // Only use the floating point result if it parses more characters:
+ struct item *item;
+ if (float_end > int_end)
+ {
+ if (float_errno == ERANGE)
+ {
+ self->error = PARSE_ERROR_FLOAT_RANGE;
+ return NULL;
+ }
+ self->cursor = float_end;
+ if (!(item = new_float (float_value)))
+ self->error = PARSE_ERROR_MEMORY;
+ return item;
+ }
+ else
+ {
+ if (int_errno == ERANGE)
+ {
+ self->error = PARSE_ERROR_INTEGER_RANGE;
+ return NULL;
+ }
+ self->cursor = int_end;
+ if (!(item = new_integer (int_value)))
+ self->error = PARSE_ERROR_MEMORY;
+ return item;
+ }
+}
+
+static struct item *
+parse_word (struct tokenizer *self)
+{
+ struct buffer buf = BUFFER_INITIALIZER;
+ struct item *item = NULL;
+ char c;
+
+ // Here we accept almost anything that doesn't break the grammar
+ while (!strchr (" []\"", (c = *self->cursor++)) && (unsigned char) c > ' ')
+ buffer_append_c (&buf, c);
+ self->cursor--;
+
+ if (buf.memory_failure)
+ self->error = PARSE_ERROR_MEMORY;
+ else if (!buf.len)
+ self->error = PARSE_ERROR_INVALID_INPUT;
+ else if (!(item = new_word (buf.s, buf.len)))
+ self->error = PARSE_ERROR_MEMORY;
+
+ free (buf.s);
+ return item;
+}
+
+static struct item *parse_item_list (struct tokenizer *);
+
+static struct item *
+parse_list (struct tokenizer *self)
+{
+ struct item *list = parse_item_list (self);
+ if (self->error)
+ {
+ assert (list == NULL);
+ return NULL;
+ }
+ if (!*self->cursor)
+ {
+ self->error = PARSE_ERROR_EOF;
+ item_free_list (list);
+ return NULL;
+ }
+ assert (*self->cursor == ']');
+ self->cursor++;
+ return new_list (list);
+}
+
+static struct item *
+parse_item (struct tokenizer *self)
+{
+ char c;
+ switch ((c = *self->cursor++))
+ {
+ case '[': return parse_list (self);
+ case '"': return parse_string (self);
+ default:;
+ }
+
+ self->cursor--;
+ struct item *item = try_parse_number (self);
+ if (!item && !self->error)
+ item = parse_word (self);
+ return item;
+}
+
+static struct item *
+parse_item_list (struct tokenizer *self)
+{
+ struct item *head = NULL;
+ struct item **tail = &head;
+
+ char c;
+ bool expected = true;
+ while ((c = *self->cursor) && c != ']')
+ {
+ if (isspace (c))
+ {
+ self->cursor++;
+ expected = true;
+ continue;
+ }
+ else if (!expected)
+ {
+ self->error = PARSE_ERROR_UNEXPECTED_INPUT;
+ goto fail;
+ }
+
+ if (!(*tail = parse_item (self)))
+ goto fail;
+ tail = &(*tail)->next;
+ expected = false;
+ }
+ return head;
+
+fail:
+ item_free_list (head);
+ return NULL;
+}
+
+static struct item *
+parse (const char *s, const char **error)
+{
+ struct tokenizer self = { .cursor = s, .error = PARSE_ERROR_OK };
+ struct item *list = parse_item_list (&self);
+ if (!self.error && *self.cursor != '\0')
+ {
+ self.error = PARSE_ERROR_UNEXPECTED_INPUT;
+ item_free_list (list);
+ list = NULL;
+ }
+
+#define XX(x, y) y,
+ static const char *strings[PARSE_ERROR_COUNT] =
+ { PARSE_ERROR_TABLE (XX) };
+#undef XX
+
+ static char error_buf[128];
+ if (self.error && error)
+ {
+ snprintf (error_buf, sizeof error_buf, "at character %d: %s",
+ (int) (self.cursor - s) + 1, strings[self.error]);
+ *error = error_buf;
+ }
+ return list;
+}
+
+// --- Runtime -----------------------------------------------------------------
+
+// TODO: try to think of a _simple_ way to do preemptive multitasking
+
+struct context
+{
+ struct item *stack; ///< The current top of the stack
+ size_t stack_size; ///< Number of items on the stack
+
+ size_t reduction_count; ///< # of function calls so far
+ size_t reduction_limit; ///< The hard limit on function calls
+
+ char *error; ///< Error information
+ bool error_is_fatal; ///< Whether the error can be catched
+ bool memory_failure; ///< Memory allocation failure
+
+ void *user_data; ///< User data
+};
+
+/// Internal handler for a function
+typedef bool (*handler_fn) (struct context *);
+
+struct fn
+{
+ struct fn *next; ///< The next link in the chain
+
+ handler_fn handler; ///< Internal C handler, or NULL
+ struct item *script; ///< Alternatively runtime code
+ char name[]; ///< The name of the function
+};
+
+struct fn *g_functions; ///< Maps words to functions
+
+static void
+context_init (struct context *ctx)
+{
+ ctx->stack = NULL;
+ ctx->stack_size = 0;
+
+ ctx->reduction_count = 0;
+ ctx->reduction_limit = 2000;
+
+ ctx->error = NULL;
+ ctx->error_is_fatal = false;
+ ctx->memory_failure = false;
+
+ ctx->user_data = NULL;
+}
+
+static void
+context_free (struct context *ctx)
+{
+ item_free_list (ctx->stack);
+ ctx->stack = NULL;
+
+ free (ctx->error);
+ ctx->error = NULL;
+}
+
+static bool
+set_error (struct context *ctx, const char *format, ...)
+{
+ free (ctx->error);
+
+ va_list ap;
+ va_start (ap, format);
+ ctx->error = strdup_vprintf (format, ap);
+ va_end (ap);
+
+ if (!ctx->error)
+ ctx->memory_failure = true;
+ return false;
+}
+
+static bool
+push (struct context *ctx, struct item *item)
+{
+ // The `item' is typically a result from new_(), thus when it is null,
+ // that function must have failed. This is a shortcut for convenience.
+ if (!item)
+ {
+ ctx->memory_failure = true;
+ return false;
+ }
+
+ assert (item->next == NULL);
+ item->next = ctx->stack;
+ ctx->stack = item;
+ ctx->stack_size++;
+ return true;
+}
+
+static bool
+bump_reductions (struct context *ctx)
+{
+ if (++ctx->reduction_count >= ctx->reduction_limit)
+ {
+ ctx->error_is_fatal = true;
+ return set_error (ctx, "reduction limit reached");
+ }
+ return true;
+}
+
+static bool execute (struct context *, struct item *);
+
+static bool
+call_function (struct context *ctx, const char *name)
+{
+ struct fn *iter;
+ for (iter = g_functions; iter; iter = iter->next)
+ if (!strcmp (name, iter->name))
+ goto found;
+ return set_error (ctx, "unknown function: %s", name);
+
+found:
+ if (!bump_reductions (ctx))
+ return false;
+
+ if (iter->handler
+ ? iter->handler (ctx)
+ : execute (ctx, iter->script))
+ return true;
+
+ // In this case, `error' is NULL
+ if (ctx->memory_failure)
+ return false;
+
+ // This creates some form of a stack trace
+ char *tmp = ctx->error;
+ ctx->error = NULL;
+ set_error (ctx, "%s -> %s", name, tmp);
+ free (tmp);
+ return false;
+}
+
+static void
+free_function (struct fn *fn)
+{
+ item_free_list (fn->script);
+ free (fn);
+}
+
+static void
+unregister_function (const char *name)
+{
+ for (struct fn **iter = &g_functions; *iter; iter = &(*iter)->next)
+ if (!strcmp ((*iter)->name, name))
+ {
+ struct fn *tmp = *iter;
+ *iter = tmp->next;
+ free_function (tmp);
+ break;
+ }
+}
+
+static struct fn *
+prepend_new_fn (const char *name)
+{
+ struct fn *fn = calloc (1, sizeof *fn + strlen (name) + 1);
+ if (!fn)
+ return NULL;
+
+ strcpy (fn->name, name);
+ fn->next = g_functions;
+ return g_functions = fn;
+}
+
+static bool
+register_handler (const char *name, handler_fn handler)
+{
+ unregister_function (name);
+ struct fn *fn = prepend_new_fn (name);
+ if (!fn)
+ return false;
+ fn->handler = handler;
+ return true;
+}
+
+static bool
+register_script (const char *name, struct item *script)
+{
+ unregister_function (name);
+ struct fn *fn = prepend_new_fn (name);
+ if (!fn)
+ return false;
+ fn->script = script;
+ return true;
+}
+
+static bool
+execute (struct context *ctx, struct item *script)
+{
+ for (; script; script = script->next)
+ {
+ if (script->type != ITEM_WORD)
+ {
+ if (!bump_reductions (ctx)
+ || !push (ctx, new_clone (script)))
+ return false;
+ }
+ else if (!call_function (ctx, get_word (script)))
+ return false;
+ }
+ return true;
+}
+
+// --- Runtime library ---------------------------------------------------------
+
+#define defn(name) static bool name (struct context *ctx)
+
+#define check_stack(n) \
+ if (ctx->stack_size < n) { \
+ set_error (ctx, "stack underflow"); \
+ return 0; \
+ }
+
+inline static bool
+check_stack_safe (struct context *ctx, size_t n)
+{
+ check_stack (n);
+ return true;
+}
+
+static bool
+check_type (struct context *ctx, const void *item_, enum item_type type)
+{
+ const struct item *item = item_;
+ if (item->type == type)
+ return true;
+
+ return set_error (ctx, "invalid type: expected `%s', got `%s'",
+ item_type_to_str (type), item_type_to_str (item->type));
+}
+
+static struct item *
+pop (struct context *ctx)
+{
+ check_stack (1);
+ struct item *top = ctx->stack;
+ ctx->stack = top->next;
+ top->next = NULL;
+ ctx->stack_size--;
+ return top;
+}
+
+// - - Types - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+#define defn_is_type(name, item_type) \
+ defn (fn_is_##name) { \
+ check_stack (1); \
+ struct item *top = pop (ctx); \
+ push (ctx, new_integer (top->type == (item_type))); \
+ item_free (top); \
+ return true; \
+ }
+
+defn_is_type (string, ITEM_STRING)
+defn_is_type (word, ITEM_WORD)
+defn_is_type (integer, ITEM_INTEGER)
+defn_is_type (float, ITEM_FLOAT)
+defn_is_type (list, ITEM_LIST)
+
+defn (fn_to_string)
+{
+ check_stack (1);
+ struct item *item = pop (ctx);
+ char *value;
+
+ switch (item->type)
+ {
+ case ITEM_WORD:
+ item->type = ITEM_STRING;
+ case ITEM_STRING:
+ return push (ctx, item);
+
+ case ITEM_FLOAT:
+ value = strdup_printf ("%Lf", get_float (item));
+ break;
+ case ITEM_INTEGER:
+ value = strdup_printf ("%lld", get_integer (item));
+ break;
+
+ default:
+ set_error (ctx, "cannot convert `%s' to `%s'",
+ item_type_to_str (item->type), item_type_to_str (ITEM_STRING));
+ item_free (item);
+ return false;
+ }
+
+ item_free (item);
+ if (!value)
+ {
+ ctx->memory_failure = true;
+ return false;
+ }
+
+ item = new_string (value, -1);
+ free (value);
+ return push (ctx, item);
+}
+
+defn (fn_to_integer)
+{
+ check_stack (1);
+ struct item *item = pop (ctx);
+ long long value;
+
+ switch (item->type)
+ {
+ case ITEM_INTEGER:
+ return push (ctx, item);
+ case ITEM_FLOAT:
+ value = get_float (item);
+ break;
+
+ case ITEM_STRING:
+ {
+ char *end;
+ const char *s = get_string (item);
+ value = strtoll (s, &end, 10);
+ if (end != s && *s == '\0')
+ break;
+
+ item_free (item);
+ return set_error (ctx, "integer conversion error");
+ }
+
+ default:
+ set_error (ctx, "cannot convert `%s' to `%s'",
+ item_type_to_str (item->type), item_type_to_str (ITEM_INTEGER));
+ item_free (item);
+ return false;
+ }
+
+ item_free (item);
+ return push (ctx, new_integer (value));
+}
+
+defn (fn_to_float)
+{
+ check_stack (1);
+ struct item *item = pop (ctx);
+ long double value;
+
+ switch (item->type)
+ {
+ case ITEM_FLOAT:
+ return push (ctx, item);
+ case ITEM_INTEGER:
+ value = get_integer (item);
+ break;
+
+ case ITEM_STRING:
+ {
+ char *end;
+ const char *s = get_string (item);
+ value = strtold (s, &end);
+ if (end != s && *s == '\0')
+ break;
+
+ item_free (item);
+ return set_error (ctx, "float conversion error");
+ }
+
+ default:
+ set_error (ctx, "cannot convert `%s' to `%s'",
+ item_type_to_str (item->type), item_type_to_str (ITEM_FLOAT));
+ item_free (item);
+ return false;
+ }
+
+ item_free (item);
+ return push (ctx, new_float (value));
+}
+
+// - - Miscellaneous - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+defn (fn_length)
+{
+ check_stack (1);
+ struct item *item = pop (ctx);
+ bool success = true;
+ switch (item->type)
+ {
+ case ITEM_STRING:
+ success = push (ctx, new_integer (((struct item_string *) item)->len));
+ break;
+ case ITEM_LIST:
+ {
+ long long length = 0;
+ struct item *iter;
+ for (iter = get_list (item); iter; iter = iter->next)
+ length++;
+ success = push (ctx, new_integer (length));
+ break;
+ }
+ default:
+ success = set_error (ctx, "invalid type");
+ }
+ item_free (item);
+ return success;
+}
+
+// - - Stack operations - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+defn (fn_dup)
+{
+ check_stack (1);
+ return push (ctx, new_clone (ctx->stack));
+}
+
+defn (fn_drop)
+{
+ check_stack (1);
+ item_free (pop (ctx));
+ return true;
+}
+
+defn (fn_swap)
+{
+ check_stack (2);
+ struct item *second = pop (ctx), *first = pop (ctx);
+ return push (ctx, second) && push (ctx, first);
+}
+
+defn (fn_call)
+{
+ check_stack (1);
+ struct item *script = pop (ctx);
+ bool success = check_type (ctx, script, ITEM_LIST)
+ && execute (ctx, get_list (script));
+ item_free (script);
+ return success;
+}
+
+defn (fn_dip)
+{
+ check_stack (2);
+ struct item *script = pop (ctx);
+ struct item *item = pop (ctx);
+ bool success = check_type (ctx, script, ITEM_LIST)
+ && execute (ctx, get_list (script));
+ item_free (script);
+ if (!success)
+ {
+ item_free (item);
+ return false;
+ }
+ return push (ctx, item);
+}
+
+defn (fn_unit)
+{
+ check_stack (1);
+ struct item *item = pop (ctx);
+ return push (ctx, new_list (item));
+}
+
+defn (fn_cons)
+{
+ check_stack (2);
+ struct item *list = pop (ctx);
+ struct item *item = pop (ctx);
+ if (!check_type (ctx, list, ITEM_LIST))
+ {
+ item_free (list);
+ item_free (item);
+ return false;
+ }
+ item->next = get_list (list);
+ ((struct item_list *) list)->head = item;
+ return push (ctx, list);
+}
+
+defn (fn_cat)
+{
+ check_stack (2);
+ struct item *scnd = pop (ctx);
+ struct item *frst = pop (ctx);
+ if (!check_type (ctx, frst, ITEM_LIST)
+ || !check_type (ctx, scnd, ITEM_LIST))
+ {
+ item_free (frst);
+ item_free (scnd);
+ return false;
+ }
+
+ // XXX: we shouldn't have to do this in O(n)
+ struct item **tail = &((struct item_list *) frst)->head;
+ while (*tail)
+ tail = &(*tail)->next;
+ *tail = get_list (scnd);
+
+ ((struct item_list *) scnd)->head = NULL;
+ item_free (scnd);
+ return push (ctx, frst);
+}
+
+defn (fn_uncons)
+{
+ check_stack (1);
+ struct item *list = pop (ctx);
+ if (!check_type (ctx, list, ITEM_LIST))
+ goto fail;
+ struct item *first = get_list (list);
+ if (!first)
+ {
+ set_error (ctx, "list is empty");
+ goto fail;
+ }
+ ((struct item_list *) list)->head = first->next;
+ first->next = NULL;
+ return push (ctx, first) && push (ctx, list);
+fail:
+ item_free (list);
+ return false;
+}
+
+// - - Logical - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static bool
+to_boolean (struct context *ctx, struct item *item, bool *ok)
+{
+ switch (item->type)
+ {
+ case ITEM_STRING:
+ return *get_string (item) != '\0';
+ case ITEM_INTEGER:
+ return get_integer (item) != 0;
+ case ITEM_FLOAT:
+ return get_float (item) != 0.;
+ default:
+ return (*ok = set_error (ctx, "cannot convert `%s' to boolean",
+ item_type_to_str (item->type)));
+ }
+}
+
+defn (fn_not)
+{
+ check_stack (1);
+ struct item *item = pop (ctx);
+ bool ok = true;
+ bool result = !to_boolean (ctx, item, &ok);
+ item_free (item);
+ return ok && push (ctx, new_integer (result));
+}
+
+defn (fn_and)
+{
+ check_stack (2);
+ struct item *op1 = pop (ctx);
+ struct item *op2 = pop (ctx);
+ bool ok = true;
+ bool result = to_boolean (ctx, op1, &ok) && to_boolean (ctx, op2, &ok);
+ item_free (op1);
+ item_free (op2);
+ return ok && push (ctx, new_integer (result));
+}
+
+defn (fn_or)
+{
+ check_stack (2);
+ struct item *op1 = pop (ctx);
+ struct item *op2 = pop (ctx);
+ bool ok = true;
+ bool result = to_boolean (ctx, op1, &ok)
+ || !ok || to_boolean (ctx, op2, &ok);
+ item_free (op1);
+ item_free (op2);
+ return ok && push (ctx, new_integer (result));
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+defn (fn_if)
+{
+ check_stack (3);
+ struct item *else_ = pop (ctx);
+ struct item *then_ = pop (ctx);
+ struct item *cond_ = pop (ctx);
+
+ bool ok = true;
+ bool condition = to_boolean (ctx, cond_, &ok);
+ item_free (cond_);
+
+ bool success = false;
+ if (ok
+ && check_type (ctx, then_, ITEM_LIST)
+ && check_type (ctx, else_, ITEM_LIST))
+ success = execute (ctx, condition
+ ? get_list (then_)
+ : get_list (else_));
+
+ item_free (then_);
+ item_free (else_);
+ return success;
+}
+
+defn (fn_try)
+{
+ check_stack (2);
+ struct item *catch = pop (ctx);
+ struct item *try = pop (ctx);
+ bool success = false;
+ if (!check_type (ctx, try, ITEM_LIST)
+ || !check_type (ctx, catch, ITEM_LIST))
+ goto fail;
+
+ if (!execute (ctx, get_list (try)))
+ {
+ if (ctx->memory_failure || ctx->error_is_fatal)
+ goto fail;
+
+ success = push (ctx, new_string (ctx->error, -1));
+ free (ctx->error);
+ ctx->error = NULL;
+
+ if (success)
+ success = execute (ctx, get_list (catch));
+ }
+
+fail:
+ item_free (try);
+ item_free (catch);
+ return success;
+}
+
+defn (fn_map)
+{
+ check_stack (2);
+ struct item *fn = pop (ctx);
+ struct item *list = pop (ctx);
+ if (!check_type (ctx, fn, ITEM_LIST)
+ || !check_type (ctx, list, ITEM_LIST))
+ {
+ item_free (fn);
+ item_free (list);
+ return false;
+ }
+
+ bool success = false;
+ struct item *result = NULL, **tail = &result;
+ for (struct item *iter = get_list (list); iter; iter = iter->next)
+ {
+ if (!push (ctx, new_clone (iter))
+ || !execute (ctx, get_list (fn))
+ || !check_stack_safe (ctx, 1))
+ goto fail;
+
+ struct item *item = pop (ctx);
+ *tail = item;
+ tail = &item->next;
+ }
+ success = true;
+
+fail:
+ set_list (list, result);
+ item_free (fn);
+ if (!success)
+ {
+ item_free (list);
+ return false;
+ }
+ return push (ctx, list);
+}
+
+defn (fn_filter)
+{
+ check_stack (2);
+ struct item *fn = pop (ctx);
+ struct item *list = pop (ctx);
+ if (!check_type (ctx, fn, ITEM_LIST)
+ || !check_type (ctx, list, ITEM_LIST))
+ {
+ item_free (fn);
+ item_free (list);
+ return false;
+ }
+
+ bool success = false;
+ bool ok = true;
+ struct item *result = NULL, **tail = &result;
+ for (struct item *iter = get_list (list); iter; iter = iter->next)
+ {
+ if (!push (ctx, new_clone (iter))
+ || !execute (ctx, get_list (fn))
+ || !check_stack_safe (ctx, 1))
+ goto fail;
+
+ struct item *item = pop (ctx);
+ bool survived = to_boolean (ctx, item, &ok);
+ item_free (item);
+ if (!ok)
+ goto fail;
+ if (!survived)
+ continue;
+
+ if (!(item = new_clone (iter)))
+ goto fail;
+ *tail = item;
+ tail = &item->next;
+ }
+ success = true;
+
+fail:
+ set_list (list, result);
+ item_free (fn);
+ if (!success)
+ {
+ item_free (list);
+ return false;
+ }
+ return push (ctx, list);
+}
+
+defn (fn_fold)
+{
+ check_stack (3);
+ struct item *op = pop (ctx);
+ struct item *null = pop (ctx);
+ struct item *list = pop (ctx);
+ bool success = false;
+ if (!check_type (ctx, op, ITEM_LIST)
+ || !check_type (ctx, list, ITEM_LIST))
+ {
+ item_free (null);
+ goto fail;
+ }
+
+ push (ctx, null);
+ for (struct item *iter = get_list (list); iter; iter = iter->next)
+ if (!push (ctx, new_clone (iter))
+ || !execute (ctx, get_list (op)))
+ goto fail;
+ success = true;
+
+fail:
+ item_free (op);
+ item_free (list);
+ return success;
+}
+
+defn (fn_each)
+{
+ check_stack (2);
+ struct item *op = pop (ctx);
+ struct item *list = pop (ctx);
+ bool success = false;
+ if (!check_type (ctx, op, ITEM_LIST)
+ || !check_type (ctx, list, ITEM_LIST))
+ goto fail;
+
+ for (struct item *iter = get_list (list); iter; iter = iter->next)
+ if (!push (ctx, new_clone (iter))
+ || !execute (ctx, get_list (op)))
+ goto fail;
+ success = true;
+
+fail:
+ item_free (op);
+ item_free (list);
+ return success;
+}
+
+// - - Arithmetic - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+// XXX: why not a `struct item_string *` argument?
+static bool
+push_repeated_string (struct context *ctx, struct item *op1, struct item *op2)
+{
+ struct item_string *string = (struct item_string *) op1;
+ struct item_integer *repeat = (struct item_integer *) op2;
+ assert (string->type == ITEM_STRING);
+ assert (repeat->type == ITEM_INTEGER);
+
+ if (repeat->value < 0)
+ return set_error (ctx, "cannot multiply a string by a negative value");
+
+ char *buf = NULL;
+ size_t len = string->len * repeat->value;
+ if (len < string->len && repeat->value != 0)
+ goto allocation_fail;
+
+ buf = malloc (len);
+ if (!buf)
+ goto allocation_fail;
+
+ for (size_t i = 0; i < len; i += string->len)
+ memcpy (buf + i, string->value, string->len);
+ struct item *item = new_string (buf, len);
+ free (buf);
+ return push (ctx, item);
+
+allocation_fail:
+ ctx->memory_failure = true;
+ return false;
+}
+
+defn (fn_times)
+{
+ check_stack (2);
+ struct item *op2 = pop (ctx);
+ struct item *op1 = pop (ctx);
+
+ bool ok;
+ if (op1->type == ITEM_INTEGER && op2->type == ITEM_INTEGER)
+ ok = push (ctx, new_integer (get_integer (op1) * get_integer (op2)));
+ else if (op1->type == ITEM_INTEGER && op2->type == ITEM_FLOAT)
+ ok = push (ctx, new_float (get_integer (op1) * get_float (op2)));
+ else if (op1->type == ITEM_FLOAT && op2->type == ITEM_FLOAT)
+ ok = push (ctx, new_float (get_float (op1) * get_float (op2)));
+ else if (op1->type == ITEM_FLOAT && op2->type == ITEM_INTEGER)
+ ok = push (ctx, new_float (get_float (op1) * get_integer (op2)));
+ else if (op1->type == ITEM_INTEGER && op2->type == ITEM_STRING)
+ ok = push_repeated_string (ctx, op2, op1);
+ else if (op1->type == ITEM_STRING && op2->type == ITEM_INTEGER)
+ ok = push_repeated_string (ctx, op1, op2);
+ else
+ ok = set_error (ctx, "cannot multiply `%s' and `%s'",
+ item_type_to_str (op1->type), item_type_to_str (op2->type));
+
+ item_free (op1);
+ item_free (op2);
+ return ok;
+}
+
+defn (fn_pow)
+{
+ check_stack (2);
+ struct item *op2 = pop (ctx);
+ struct item *op1 = pop (ctx);
+
+ bool ok;
+ if (op1->type == ITEM_INTEGER && op2->type == ITEM_INTEGER)
+ // TODO: implement this properly, outputting an integer
+ ok = push (ctx, new_float (powl (get_integer (op1), get_integer (op2))));
+ else if (op1->type == ITEM_INTEGER && op2->type == ITEM_FLOAT)
+ ok = push (ctx, new_float (powl (get_integer (op1), get_float (op2))));
+ else if (op1->type == ITEM_FLOAT && op2->type == ITEM_FLOAT)
+ ok = push (ctx, new_float (powl (get_float (op1), get_float (op2))));
+ else if (op1->type == ITEM_FLOAT && op2->type == ITEM_INTEGER)
+ ok = push (ctx, new_float (powl (get_float (op1), get_integer (op2))));
+ else
+ ok = set_error (ctx, "cannot exponentiate `%s' and `%s'",
+ item_type_to_str (op1->type), item_type_to_str (op2->type));
+
+ item_free (op1);
+ item_free (op2);
+ return ok;
+}
+
+defn (fn_div)
+{
+ check_stack (2);
+ struct item *op2 = pop (ctx);
+ struct item *op1 = pop (ctx);
+
+ bool ok;
+ if (op1->type == ITEM_INTEGER && op2->type == ITEM_INTEGER)
+ {
+ if (get_integer (op2) == 0)
+ ok = set_error (ctx, "division by zero");
+ else
+ ok = push (ctx, new_integer (get_integer (op1) / get_integer (op2)));
+ }
+ else if (op1->type == ITEM_INTEGER && op2->type == ITEM_FLOAT)
+ ok = push (ctx, new_float (get_integer (op1) / get_float (op2)));
+ else if (op1->type == ITEM_FLOAT && op2->type == ITEM_FLOAT)
+ ok = push (ctx, new_float (get_float (op1) / get_float (op2)));
+ else if (op1->type == ITEM_FLOAT && op2->type == ITEM_INTEGER)
+ ok = push (ctx, new_float (get_float (op1) / get_integer (op2)));
+ else
+ ok = set_error (ctx, "cannot divide `%s' and `%s'",
+ item_type_to_str (op1->type), item_type_to_str (op2->type));
+
+ item_free (op1);
+ item_free (op2);
+ return ok;
+}
+
+defn (fn_mod)
+{
+ check_stack (2);
+ struct item *op2 = pop (ctx);
+ struct item *op1 = pop (ctx);
+
+ bool ok;
+ if (op1->type == ITEM_INTEGER && op2->type == ITEM_INTEGER)
+ {
+ if (get_integer (op2) == 0)
+ ok = set_error (ctx, "division by zero");
+ else
+ ok = push (ctx, new_integer (get_integer (op1) % get_integer (op2)));
+ }
+ else if (op1->type == ITEM_INTEGER && op2->type == ITEM_FLOAT)
+ ok = push (ctx, new_float (fmodl (get_integer (op1), get_float (op2))));
+ else if (op1->type == ITEM_FLOAT && op2->type == ITEM_FLOAT)
+ ok = push (ctx, new_float (fmodl (get_float (op1), get_float (op2))));
+ else if (op1->type == ITEM_FLOAT && op2->type == ITEM_INTEGER)
+ ok = push (ctx, new_float (fmodl (get_float (op1), get_integer (op2))));
+ else
+ ok = set_error (ctx, "cannot divide `%s' and `%s'",
+ item_type_to_str (op1->type), item_type_to_str (op2->type));
+
+ item_free (op1);
+ item_free (op2);
+ return ok;
+}
+
+static bool
+push_concatenated_string (struct context *ctx,
+ struct item *op1, struct item *op2)
+{
+ struct item_string *s1 = (struct item_string *) op1;
+ struct item_string *s2 = (struct item_string *) op2;
+ assert (s1->type == ITEM_STRING);
+ assert (s2->type == ITEM_STRING);
+
+ char *buf = NULL;
+ size_t len = s1->len + s2->len;
+ if (len < s1->len || len < s2->len)
+ goto allocation_fail;
+
+ buf = malloc (len);
+ if (!buf)
+ goto allocation_fail;
+
+ memcpy (buf, s1->value, s1->len);
+ memcpy (buf + s1->len, s2->value, s2->len);
+ struct item *item = new_string (buf, len);
+ free (buf);
+ return push (ctx, item);
+
+allocation_fail:
+ ctx->memory_failure = true;
+ return false;
+
+}
+
+defn (fn_plus)
+{
+ check_stack (2);
+ struct item *op2 = pop (ctx);
+ struct item *op1 = pop (ctx);
+
+ bool ok;
+ if (op1->type == ITEM_INTEGER && op2->type == ITEM_INTEGER)
+ ok = push (ctx, new_integer (get_integer (op1) + get_integer (op2)));
+ else if (op1->type == ITEM_INTEGER && op2->type == ITEM_FLOAT)
+ ok = push (ctx, new_float (get_integer (op1) + get_float (op2)));
+ else if (op1->type == ITEM_FLOAT && op2->type == ITEM_FLOAT)
+ ok = push (ctx, new_float (get_float (op1) + get_float (op2)));
+ else if (op1->type == ITEM_FLOAT && op2->type == ITEM_INTEGER)
+ ok = push (ctx, new_float (get_float (op1) + get_integer (op2)));
+ else if (op1->type == ITEM_STRING && op2->type == ITEM_STRING)
+ ok = push_concatenated_string (ctx, op1, op2);
+ else
+ ok = set_error (ctx, "cannot add `%s' and `%s'",
+ item_type_to_str (op1->type), item_type_to_str (op2->type));
+
+ item_free (op1);
+ item_free (op2);
+ return ok;
+}
+
+defn (fn_minus)
+{
+ check_stack (2);
+ struct item *op2 = pop (ctx);
+ struct item *op1 = pop (ctx);
+
+ bool ok;
+ if (op1->type == ITEM_INTEGER && op2->type == ITEM_INTEGER)
+ ok = push (ctx, new_integer (get_integer (op1) - get_integer (op2)));
+ else if (op1->type == ITEM_INTEGER && op2->type == ITEM_FLOAT)
+ ok = push (ctx, new_float (get_integer (op1) - get_float (op2)));
+ else if (op1->type == ITEM_FLOAT && op2->type == ITEM_FLOAT)
+ ok = push (ctx, new_float (get_float (op1) - get_float (op2)));
+ else if (op1->type == ITEM_FLOAT && op2->type == ITEM_INTEGER)
+ ok = push (ctx, new_float (get_float (op1) - get_integer (op2)));
+ else
+ ok = set_error (ctx, "cannot subtract `%s' and `%s'",
+ item_type_to_str (op1->type), item_type_to_str (op2->type));
+
+ item_free (op1);
+ item_free (op2);
+ return ok;
+}
+
+// - - Comparison - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static int
+compare_strings (struct item_string *s1, struct item_string *s2)
+{
+ // XXX: not entirely correct wrt. null bytes
+ size_t len = (s1->len < s2->len ? s1->len : s2->len) + 1;
+ return memcmp (s1->value, s2->value, len);
+}
+
+static bool compare_lists (struct item *, struct item *);
+
+static bool
+compare_list_items (struct item *op1, struct item *op2)
+{
+ if (op1->type != op2->type)
+ return false;
+
+ switch (op1->type)
+ {
+ case ITEM_STRING:
+ case ITEM_WORD:
+ return !compare_strings ((struct item_string *) op1,
+ (struct item_string *) op2);
+ case ITEM_FLOAT:
+ return get_float (op1) == get_float (op2);
+ case ITEM_INTEGER:
+ return get_integer (op1) == get_integer (op2);
+ case ITEM_LIST:
+ return compare_lists (get_list (op1), get_list (op2));
+ }
+ abort ();
+}
+
+static bool
+compare_lists (struct item *op1, struct item *op2)
+{
+ while (op1 && op2)
+ {
+ if (!compare_list_items (op1, op2))
+ return false;
+
+ op1 = op1->next;
+ op2 = op2->next;
+ }
+ return !op1 && !op2;
+}
+
+defn (fn_eq)
+{
+ check_stack (2);
+ struct item *op2 = pop (ctx);
+ struct item *op1 = pop (ctx);
+
+ bool ok;
+ if (op1->type == ITEM_INTEGER && op2->type == ITEM_INTEGER)
+ ok = push (ctx, new_integer (get_integer (op1) == get_integer (op2)));
+ else if (op1->type == ITEM_INTEGER && op2->type == ITEM_FLOAT)
+ ok = push (ctx, new_integer (get_integer (op1) == get_float (op2)));
+ else if (op1->type == ITEM_FLOAT && op2->type == ITEM_FLOAT)
+ ok = push (ctx, new_integer (get_float (op1) == get_float (op2)));
+ else if (op1->type == ITEM_FLOAT && op2->type == ITEM_INTEGER)
+ ok = push (ctx, new_integer (get_float (op1) == get_integer (op2)));
+ else if (op1->type == ITEM_LIST && op2->type == ITEM_LIST)
+ ok = push (ctx, new_integer (compare_lists
+ (get_list (op1), get_list (op2))));
+ else if (op1->type == ITEM_STRING && op2->type == ITEM_STRING)
+ ok = push (ctx, new_integer (compare_strings
+ ((struct item_string *)(op1), (struct item_string *)(op2)) == 0));
+ else
+ ok = set_error (ctx, "cannot compare `%s' and `%s'",
+ item_type_to_str (op1->type), item_type_to_str (op2->type));
+
+ item_free (op1);
+ item_free (op2);
+ return ok;
+}
+
+defn (fn_lt)
+{
+ check_stack (2);
+ struct item *op2 = pop (ctx);
+ struct item *op1 = pop (ctx);
+
+ bool ok;
+ if (op1->type == ITEM_INTEGER && op2->type == ITEM_INTEGER)
+ ok = push (ctx, new_integer (get_integer (op1) < get_integer (op2)));
+ else if (op1->type == ITEM_INTEGER && op2->type == ITEM_FLOAT)
+ ok = push (ctx, new_integer (get_integer (op1) < get_float (op2)));
+ else if (op1->type == ITEM_FLOAT && op2->type == ITEM_FLOAT)
+ ok = push (ctx, new_integer (get_float (op1) < get_float (op2)));
+ else if (op1->type == ITEM_FLOAT && op2->type == ITEM_INTEGER)
+ ok = push (ctx, new_integer (get_float (op1) < get_integer (op2)));
+ else if (op1->type == ITEM_STRING && op2->type == ITEM_STRING)
+ ok = push (ctx, new_integer (compare_strings
+ ((struct item_string *)(op1), (struct item_string *)(op2)) < 0));
+ else
+ ok = set_error (ctx, "cannot compare `%s' and `%s'",
+ item_type_to_str (op1->type), item_type_to_str (op2->type));
+
+ item_free (op1);
+ item_free (op2);
+ return ok;
+}
+
+// - - Utilities - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+defn (fn_rand)
+{
+ return push (ctx, new_float ((long double) rand ()
+ / ((long double) RAND_MAX + 1)));
+}
+
+defn (fn_time)
+{
+ return push (ctx, new_integer (time (NULL)));
+}
+
+// XXX: this is a bit too constrained; combines strftime() with gmtime()
+defn (fn_strftime)
+{
+ check_stack (2);
+ struct item *format = pop (ctx);
+ struct item *time_ = pop (ctx);
+ bool success = false;
+ if (!check_type (ctx, time_, ITEM_INTEGER)
+ || !check_type (ctx, format, ITEM_STRING))
+ goto fail;
+
+ if (get_integer (time_) < 0)
+ {
+ set_error (ctx, "invalid time value");
+ goto fail;
+ }
+
+ char buf[128];
+ time_t time__ = get_integer (time_);
+ struct tm tm;
+ gmtime_r (&time__, &tm);
+ buf[strftime (buf, sizeof buf, get_string (format), &tm)] = '\0';
+ success = push (ctx, new_string (buf, -1));
+
+fail:
+ item_free (time_);
+ item_free (format);
+ return success;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void item_list_to_str (const struct item *, struct buffer *);
+
+static void
+string_to_str (const struct item_string *string, struct buffer *buf)
+{
+ buffer_append_c (buf, '"');
+ for (size_t i = 0; i < string->len; i++)
+ {
+ char c = string->value[i];
+ if (c == '\n') buffer_append (buf, "\\n", 2);
+ else if (c == '\r') buffer_append (buf, "\\r", 2);
+ else if (c == '\t') buffer_append (buf, "\\t", 2);
+ else if (!isprint (c))
+ {
+ char tmp[8];
+ snprintf (tmp, sizeof tmp, "\\x%02x", (unsigned char) c);
+ buffer_append (buf, tmp, strlen (tmp));
+ }
+ else if (c == '\\') buffer_append (buf, "\\\\", 2);
+ else if (c == '"') buffer_append (buf, "\\\"", 2);
+ else buffer_append_c (buf, c);
+ }
+ buffer_append_c (buf, '"');
+}
+
+static void
+item_to_str (const struct item *item, struct buffer *buf)
+{
+ switch (item->type)
+ {
+ char *x;
+ case ITEM_STRING:
+ string_to_str ((struct item_string *) item, buf);
+ break;
+ case ITEM_WORD:
+ {
+ struct item_word *word = (struct item_word *) item;
+ buffer_append (buf, word->value, word->len);
+ break;
+ }
+ case ITEM_INTEGER:
+ if (!(x = strdup_printf ("%lld", get_integer (item))))
+ goto alloc_failure;
+ buffer_append (buf, x, strlen (x));
+ free (x);
+ break;
+ case ITEM_FLOAT:
+ if (!(x = strdup_printf ("%Lf", get_float (item))))
+ goto alloc_failure;
+ buffer_append (buf, x, strlen (x));
+ free (x);
+ break;
+ case ITEM_LIST:
+ buffer_append_c (buf, '[');
+ item_list_to_str (get_list (item), buf);
+ buffer_append_c (buf, ']');
+ break;
+ }
+ return;
+
+alloc_failure:
+ // This is a bit hackish but it simplifies stuff
+ buf->memory_failure = true;
+ free (buf->s);
+ buf->s = NULL;
+}
+
+static void
+item_list_to_str (const struct item *script, struct buffer *buf)
+{
+ if (!script)
+ return;
+
+ item_to_str (script, buf);
+ while ((script = script->next))
+ {
+ buffer_append_c (buf, ' ');
+ item_to_str (script, buf);
+ }
+}
+
+// --- IRC protocol ------------------------------------------------------------
+
+struct message
+{
+ char *prefix; ///< Message prefix
+ char *command; ///< IRC command
+ char *params[16]; ///< Command parameters (0-terminated)
+ size_t n_params; ///< Number of parameters present
+};
+
+inline static char *
+cut_word (char **s)
+{
+ char *start = *s, *end = *s + strcspn (*s, " ");
+ *s = end + strspn (end, " ");
+ *end = '\0';
+ return start;
+}
+
+static bool
+parse_message (char *s, struct message *msg)
+{
+ memset (msg, 0, sizeof *msg);
+
+ // Ignore IRC 3.2 message tags, if present
+ if (*s == '@')
+ {
+ s += strcspn (s, " ");
+ s += strspn (s, " ");
+ }
+
+ // Prefix
+ if (*s == ':')
+ msg->prefix = cut_word (&s) + 1;
+
+ // Command
+ if (!*(msg->command = cut_word (&s)))
+ return false;
+
+ // Parameters
+ while (*s)
+ {
+ size_t n = msg->n_params++;
+ if (msg->n_params >= N_ELEMENTS (msg->params))
+ return false;
+ if (*s == ':')
+ {
+ msg->params[n] = ++s;
+ break;
+ }
+ msg->params[n] = cut_word (&s);
+ }
+ return true;
+}
+
+static struct message *
+read_message (void)
+{
+ static bool discard = false;
+ static char buf[1025];
+ static struct message msg;
+
+ bool discard_this;
+ do
+ {
+ if (!fgets (buf, sizeof buf, stdin))
+ return NULL;
+ size_t len = strlen (buf);
+
+ // Just to be on the safe side, if the line overflows our buffer,
+ // ignore everything up until the next line.
+ discard_this = discard;
+ if (len >= 2 && !strcmp (buf + len - 2, "\r\n"))
+ {
+ buf[len -= 2] = '\0';
+ discard = false;
+ }
+ else
+ discard = true;
+ }
+ // Invalid messages are silently ignored
+ while (discard_this || !parse_message (buf, &msg));
+ return &msg;
+}
+
+// --- Interfacing with the bot ------------------------------------------------
+
+#define BOT_PRINT "ZYKLONB print :script: "
+
+static const char *
+get_config (const char *key)
+{
+ printf ("ZYKLONB get_config :%s\r\n", key);
+ struct message *msg = read_message ();
+ if (!msg || msg->n_params <= 0)
+ exit (EXIT_FAILURE);
+ return msg->params[0];
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+// TODO: implement more functions; try to avoid writing them in C
+
+static bool
+init_runtime_library_scripts (void)
+{
+ bool ok = true;
+
+ // It's much cheaper (and more fun) to define functions in terms of other
+ // ones. The "unit tests" serve a secondary purpose of showing the usage.
+ struct script
+ {
+ const char *name; ///< Name of the function
+ const char *definition; ///< The defining script
+ const char *unit_test; ///< Trivial unit test, must return 1
+ }
+ scripts[] =
+ {
+ { "nip", "swap drop", "1 2 nip 2 =" },
+ { "over", "[dup] dip swap", "1 2 over nip nip 1 =" },
+ { "swons", "swap cons", "[2] 1 swons [1 2] =" },
+ { "first", "uncons drop", "[1 2 3] first 1 =" },
+ { "rest", "uncons swap drop", "[1 2 3] rest [2 3] =" },
+ { "reverse", "[] swap [swap cons] each", "[1 2] reverse [2 1] =" },
+ { "curry", "cons", "1 2 [+] curry call 3 =" },
+
+ { "xor", "not swap not + 1 =", "1 1 xor 0 =" },
+ { "min", "over over < [drop] [nip] if", "1 2 min 1 =" },
+ { "max", "over over > [drop] [nip] if", "1 2 max 2 =" },
+
+ { "all?", "[and] cat 1 swap fold", "[3 4 5] [> 3] all? 0 =" },
+ { "any?", "[or] cat 0 swap fold", "[3 4 5] [> 3] any? 1 =" },
+
+ { ">", "swap <", "1 2 > 0 =" },
+ { "!=", "= not", "1 2 != 1 =" },
+ { "<=", "> not", "1 2 <= 1 =" },
+ { ">=", "< not", "1 2 >= 0 =" },
+
+ // XXX: this is a bit crazy and does not work with an empty list
+ { "join", "[uncons] dip swap [[dup] dip swap [+ +] dip] each drop",
+ "[1 2 3] [>string] map \" -> \" join \"1 -> 2 -> 3\" =" },
+ };
+
+ for (size_t i = 0; i < N_ELEMENTS (scripts); i++)
+ {
+ const char *error = NULL;
+ struct item *script = parse (scripts[i].definition, &error);
+ if (error)
+ {
+ printf (BOT_PRINT "error parsing internal script `%s': %s\r\n",
+ scripts[i].definition, error);
+ ok = false;
+ }
+ else
+ ok &= register_script (scripts[i].name, script);
+ }
+
+ struct context ctx;
+ for (size_t i = 0; i < N_ELEMENTS (scripts); i++)
+ {
+ const char *error = NULL;
+ struct item *script = parse (scripts[i].unit_test, &error);
+ if (error)
+ {
+ printf (BOT_PRINT "error parsing unit test for `%s': %s\r\n",
+ scripts[i].name, error);
+ ok = false;
+ continue;
+ }
+ context_init (&ctx);
+ execute (&ctx, script);
+ item_free_list (script);
+
+ const char *failure = NULL;
+ if (ctx.memory_failure)
+ failure = "memory allocation failure";
+ else if (ctx.error)
+ failure = ctx.error;
+ else if (ctx.stack_size != 1)
+ failure = "too many results on the stack";
+ else if (ctx.stack->type != ITEM_INTEGER)
+ failure = "result is not an integer";
+ else if (get_integer (ctx.stack) != 1)
+ failure = "wrong test result";
+ if (failure)
+ {
+ printf (BOT_PRINT "error executing unit test for `%s': %s\r\n",
+ scripts[i].name, failure);
+ ok = false;
+ }
+ context_free (&ctx);
+ }
+ return ok;
+}
+
+static bool
+init_runtime_library (void)
+{
+ bool ok = true;
+
+ // Type detection
+ ok &= register_handler ("string?", fn_is_string);
+ ok &= register_handler ("word?", fn_is_word);
+ ok &= register_handler ("integer?", fn_is_integer);
+ ok &= register_handler ("float?", fn_is_float);
+ ok &= register_handler ("list?", fn_is_list);
+
+ // Type conversion
+ ok &= register_handler (">string", fn_to_string);
+ ok &= register_handler (">integer", fn_to_integer);
+ ok &= register_handler (">float", fn_to_float);
+
+ // Miscellaneous
+ ok &= register_handler ("length", fn_length);
+
+ // Basic stack manipulation
+ ok &= register_handler ("dup", fn_dup);
+ ok &= register_handler ("drop", fn_drop);
+ ok &= register_handler ("swap", fn_swap);
+
+ // Calling stuff
+ ok &= register_handler ("call", fn_call);
+ ok &= register_handler ("dip", fn_dip);
+
+ // Control flow
+ ok &= register_handler ("if", fn_if);
+ ok &= register_handler ("try", fn_try);
+
+ // List processing
+ ok &= register_handler ("map", fn_map);
+ ok &= register_handler ("filter", fn_filter);
+ ok &= register_handler ("fold", fn_fold);
+ ok &= register_handler ("each", fn_each);
+
+ // List manipulation
+ ok &= register_handler ("unit", fn_unit);
+ ok &= register_handler ("cons", fn_cons);
+ ok &= register_handler ("cat", fn_cat);
+ ok &= register_handler ("uncons", fn_uncons);
+
+ // Arithmetic operations
+ ok &= register_handler ("+", fn_plus);
+ ok &= register_handler ("-", fn_minus);
+ ok &= register_handler ("*", fn_times);
+ ok &= register_handler ("^", fn_pow);
+ ok &= register_handler ("/", fn_div);
+ ok &= register_handler ("%", fn_mod);
+
+ // Comparison
+ ok &= register_handler ("=", fn_eq);
+ ok &= register_handler ("<", fn_lt);
+
+ // Logical operations
+ ok &= register_handler ("not", fn_not);
+ ok &= register_handler ("and", fn_and);
+ ok &= register_handler ("or", fn_or);
+
+ // Utilities
+ ok &= register_handler ("rand", fn_rand);
+ ok &= register_handler ("time", fn_time);
+ ok &= register_handler ("strftime", fn_strftime);
+
+ ok &= init_runtime_library_scripts ();
+ return ok;
+}
+
+static void
+free_runtime_library (void)
+{
+ struct fn *next, *iter;
+ for (iter = g_functions; iter; iter = next)
+ {
+ next = iter->next;
+ free_function (iter);
+ }
+}
+
+// --- Function database -------------------------------------------------------
+
+// TODO: a global variable storing the various procedures (db)
+// XXX: defining procedures would ideally need some kind of an ACL
+
+static void
+read_db (void)
+{
+ // TODO
+}
+
+static void
+write_db (void)
+{
+ // TODO
+}
+
+// --- Main --------------------------------------------------------------------
+
+static char *g_prefix;
+
+struct user_info
+{
+ char *ctx; ///< Context: channel or user
+ char *ctx_quote; ///< Reply quotation
+};
+
+defn (fn_dot)
+{
+ check_stack (1);
+ struct item *item = pop (ctx);
+ struct user_info *info = ctx->user_data;
+
+ struct buffer buf = BUFFER_INITIALIZER;
+ item_to_str (item, &buf);
+ item_free (item);
+ buffer_append_c (&buf, '\0');
+ if (buf.memory_failure)
+ {
+ ctx->memory_failure = true;
+ return false;
+ }
+
+ if (buf.len > 255)
+ buf.s[255] = '\0';
+
+ printf ("PRIVMSG %s :%s%s\r\n", info->ctx, info->ctx_quote, buf.s);
+ free (buf.s);
+ return true;
+}
+
+static void
+process_message (struct message *msg)
+{
+ if (!msg->prefix
+ || strcasecmp (msg->command, "PRIVMSG")
+ || msg->n_params < 2)
+ return;
+ char *line = msg->params[1];
+
+ // Filter out only our commands
+ size_t prefix_len = strlen (g_prefix);
+ if (strncmp (line, g_prefix, prefix_len))
+ return;
+ line += prefix_len;
+
+ char *command = cut_word (&line);
+ if (strcasecmp (command, "script"))
+ return;
+
+ // Retrieve information on how to respond back
+ char *msg_ctx = msg->prefix, *x;
+ if ((x = strchr (msg_ctx, '!')))
+ *x = '\0';
+
+ char *msg_ctx_quote;
+ if (strchr ("#+&!", *msg->params[0]))
+ {
+ msg_ctx_quote = strdup_printf ("%s: ", msg_ctx);
+ msg_ctx = msg->params[0];
+ }
+ else
+ msg_ctx_quote = strdup ("");
+
+ if (!msg_ctx_quote)
+ {
+ printf (BOT_PRINT "%s\r\n", "memory allocation failure");
+ return;
+ }
+
+ struct user_info info;
+ info.ctx = msg_ctx;
+ info.ctx_quote = msg_ctx_quote;
+
+ // Finally parse and execute the macro
+ const char *error = NULL;
+ struct item *script = parse (line, &error);
+ if (error)
+ {
+ printf ("PRIVMSG %s :%s%s: %s\r\n",
+ msg_ctx, msg_ctx_quote, "parse error", error);
+ goto end;
+ }
+
+ struct context ctx;
+ context_init (&ctx);
+ ctx.user_data = &info;
+ execute (&ctx, script);
+ item_free_list (script);
+
+ const char *failure = NULL;
+ if (ctx.memory_failure)
+ failure = "memory allocation failure";
+ else if (ctx.error)
+ failure = ctx.error;
+ if (failure)
+ printf ("PRIVMSG %s :%s%s: %s\r\n",
+ msg_ctx, msg_ctx_quote, "runtime error", failure);
+ context_free (&ctx);
+end:
+ free (msg_ctx_quote);
+}
+
+int
+main (int argc, char *argv[])
+{
+ freopen (NULL, "rb", stdin); setvbuf (stdin, NULL, _IOLBF, BUFSIZ);
+ freopen (NULL, "wb", stdout); setvbuf (stdout, NULL, _IOLBF, BUFSIZ);
+
+ struct rlimit limit =
+ {
+ .rlim_cur = ADDRESS_SPACE_LIMIT,
+ .rlim_max = ADDRESS_SPACE_LIMIT
+ };
+
+ // Lower the memory limits to something sensible to prevent abuse
+ (void) setrlimit (RLIMIT_AS, &limit);
+
+ read_db ();
+ if (!init_runtime_library ()
+ || !register_handler (".", fn_dot))
+ printf (BOT_PRINT "%s\r\n", "runtime library initialization failed");
+
+ g_prefix = strdup (get_config ("prefix"));
+ printf ("ZYKLONB register\r\n");
+ struct message *msg;
+ while ((msg = read_message ()))
+ process_message (msg);
+
+ free_runtime_library ();
+ free (g_prefix);
+ return 0;
+}
+
diff --git a/plugins/xB/seen b/plugins/xB/seen
new file mode 100755
index 0000000..da20972
--- /dev/null
+++ b/plugins/xB/seen
@@ -0,0 +1,160 @@
+#!/usr/bin/env lua
+--
+-- xB seen plugin
+--
+-- Copyright 2016 Přemysl Eric Janouch
+-- See the file LICENSE for licensing information.
+--
+
+function parse (line)
+ local msg = { params = {} }
+ line = line:match ("[^\r]*")
+ for start, word in line:gmatch ("()([^ ]+)") do
+ local colon = word:match ("^:(.*)")
+ if start == 1 and colon then
+ msg.prefix = colon
+ elseif not msg.command then
+ msg.command = word
+ elseif colon then
+ table.insert (msg.params, line:sub (start + 1))
+ break
+ elseif start ~= #line then
+ table.insert (msg.params, word)
+ end
+ end
+ return msg
+end
+
+function get_config (name)
+ io.write ("ZYKLONB get_config :", name, "\r\n")
+ return parse (io.read ()).params[1]
+end
+
+-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+io.output ():setvbuf ('line')
+local prefix = get_config ('prefix')
+io.write ("ZYKLONB register\r\n")
+
+local db = {}
+local db_filename = "seen.db"
+local db_garbage = 0
+
+function remember (who, where, when, what)
+ if not db[who] then db[who] = {} end
+ if db[who][where] then db_garbage = db_garbage + 1 end
+ db[who][where] = { tonumber (when), what }
+end
+
+-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+local db_file, e = io.open (db_filename, "a+")
+if not db_file then error ("cannot open database: " .. e, 0) end
+
+function db_store (who, where, when, what)
+ db_file:write (string.format
+ (":%s %s %s %s :%s\n", who, "PRIVMSG", where, when, what))
+end
+
+function db_compact ()
+ db_file:close ()
+
+ -- Unfortunately, default Lua doesn't have anything like mkstemp()
+ local db_tmpname = db_filename .. "." .. os.time ()
+ db_file, e = io.open (db_tmpname, "a+")
+ if not db_file then error ("cannot save database: " .. e, 0) end
+
+ for who, places in pairs (db) do
+ for where, data in pairs (places) do
+ db_store (who, where, data[1], data[2])
+ end
+ end
+ db_file:flush ()
+
+ local ok, e = os.rename (db_tmpname, db_filename)
+ if not ok then error ("cannot save database: " .. e, 0) end
+ db_garbage = 0
+end
+
+for line in db_file:lines () do
+ local msg = parse (line)
+ remember (msg.prefix, table.unpack (msg.params))
+end
+
+-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+function seen (who, where, args)
+ local respond = function (...)
+ local privmsg = function (target, ...)
+ io.write ("PRIVMSG ", target, " :", table.concat { ... }, "\r\n")
+ end
+ if where:match ("^[#&!+]") then
+ privmsg (where, who, ": ", ...)
+ else
+ privmsg (who, ...)
+ end
+ end
+
+ local whom, e, garbage = args:match ("^(%S+)()%s*(.*)")
+ if not whom or #garbage ~= 0 then
+ return respond ("usage: ")
+ elseif who:lower () == whom:lower () then
+ return respond ("I can see you right now.")
+ end
+
+ local top = {}
+ -- That is, * acts like a wildcard, otherwise everything is escaped
+ local pattern = "^" .. whom:gsub ("[%^%$%(%)%%%.%[%]%+%-%?]", "%%%0")
+ :gsub ("%*", ".*"):lower () .. "$"
+ for name, places in pairs (db) do
+ if places[where] and name:lower ():match (pattern) then
+ local when, what = table.unpack (places[where])
+ table.insert (top, { name = name, when = when, what = what })
+ end
+ end
+ if #top == 0 then
+ return respond ("I have not seen \x02" .. whom .. "\x02 here.")
+ end
+
+ -- Get all matching nicknames ordered from the most recently active
+ -- and make the list case insensitive (remove older duplicates)
+ table.sort (top, function (a, b) return a.when > b.when end)
+ for i = #top, 2, -1 do
+ if top[i - 1].name:lower () == top[i].name:lower () then
+ table.remove (top, i)
+ end
+ end
+
+ -- Hopefully the formatting mess will disrupt highlights in clients
+ for i = 1, math.min (#top, 3) do
+ local name = top[i].name:gsub ("^.", "%0\x02\x02")
+ respond (string.format ("\x02%s\x02 -> %s -> %s",
+ name, os.date ("%c", top[i].when), top[i].what))
+ end
+end
+
+function handle (msg)
+ local who = msg.prefix:match ("^[^!@]*")
+ local where, what = table.unpack (msg.params)
+ local when = os.time ()
+
+ local what_log = what:gsub ("^\x01ACTION", "*"):gsub ("\x01$", "")
+ remember (who, where, when, what_log)
+ db_store (who, where, when, what_log)
+
+ -- Comment out to reduce both disk load and reliability
+ db_file:flush ()
+
+ if db_garbage > 5000 then db_compact () end
+
+ if what:sub (1, #prefix) == prefix then
+ local command = what:sub (#prefix + 1)
+ local name, e = command:match ("^(%S+)%s*()")
+ if name == 'seen' then seen (who, where, command:sub (e)) end
+ end
+end
+
+for line in io.lines () do
+ local msg = parse (line)
+ if msg.command == "PRIVMSG" then handle (msg) end
+end
diff --git a/plugins/xB/seen-import-xC.pl b/plugins/xB/seen-import-xC.pl
new file mode 100755
index 0000000..db706a0
--- /dev/null
+++ b/plugins/xB/seen-import-xC.pl
@@ -0,0 +1,39 @@
+#!/usr/bin/env perl
+# Creates a database for the "seen" plugin from logs for xC.
+# The results may not be completely accurate but are good for jumpstarting.
+# Usage: ./seen-import-xC.pl LOG-FILE... > seen.db
+
+use strict;
+use warnings;
+use File::Basename;
+use Time::Piece;
+
+my $db = {};
+for (@ARGV) {
+ my $where = (basename($_) =~ /\.(.*).log/)[0];
+ unless ($where) {
+ print STDERR "Invalid filename: $_\n";
+ next;
+ }
+
+ open my $fh, '<', $_ or die "Failed to open log file: $!";
+ while (<$fh>) {
+ my ($when, $who, $who_action, $what) =
+ /^(.{19}) (?:<[~&@%+]*(.*?)>| \* (\S+)) (.*)/;
+ next unless $when;
+
+ if ($who_action) {
+ $who = $who_action;
+ $what = "* $what";
+ }
+ $db->{$who}->{$where} =
+ [Time::Piece->strptime($when, "%Y-%m-%d %T")->epoch, $what];
+ }
+}
+
+while (my ($who, $places) = each %$db) {
+ while (my ($where, $data) = each %$places) {
+ my ($when, $what) = @$data;
+ print ":$who PRIVMSG $where $when :$what\n";
+ }
+}
diff --git a/plugins/xB/youtube b/plugins/xB/youtube
new file mode 100755
index 0000000..0bf0c1e
--- /dev/null
+++ b/plugins/xB/youtube
@@ -0,0 +1,111 @@
+#!/usr/bin/env python3
+#
+# xB YouTube plugin, displaying info about YouTube links
+#
+# Copyright 2014 - 2015, Přemysl Eric Janouch
+# See the file LICENSE for licensing information.
+#
+
+import sys
+import io
+import re
+import json
+import urllib.request
+
+class Plugin:
+ re_msg = re.compile ('(?::([^! ]*)(?:!([^@]*)@([^ ]*))? +)?'
+ '([^ ]+)(?: +(.*))?\r\n$')
+ re_args = re.compile (':?((?<=:).*|[^ ]+) *')
+
+ def parse (self, line):
+ m = self.re_msg.match (line)
+ if m is None:
+ return None
+
+ (nick, user, host, command, args) = m.groups ()
+ args = [] if args is None else self.re_args.findall (args)
+ return (nick, user, host, command, args)
+
+ def get_config (self, key):
+ print ("ZYKLONB get_config :%s" % key)
+ (_, _, _, _, args) = self.parse (sys.stdin.readline ())
+ return args[0]
+
+ def bot_print (self, what):
+ print ('ZYKLONB print :%s' % what)
+
+class YouTube (Plugin):
+ re_videos = [re.compile (x) for x in [
+ r'youtube\.[a-z]+/[^ ]*[&?]v=([-\w]+)',
+ r'youtube\.[a-z]+/v/([-\w]+)',
+ r'youtu\.be/([-\w]+)'
+ ]]
+ re_playlists = [re.compile (x) for x in [
+ r'youtube\.[a-z]+/playlist[&?][^ ]*(?<=&|\?)list=([-\w]+)',
+ ]]
+
+ def print_info (self, channel, url, cb):
+ try:
+ data = json.loads (urllib.request.urlopen
+ (url, None, 30).read ().decode ('utf-8'))
+
+ for line in map (lambda x: "YouTube: " + cb (x), data['items']):
+ print ("PRIVMSG %s :%s" % (channel,
+ line.encode ('utf-8').decode ('iso8859-1')))
+
+ except Exception as err:
+ self.bot_print ('youtube: %s' % (err))
+
+ def print_video_info (self, channel, video_id):
+ url = 'https://www.googleapis.com/youtube/v3/' \
+ + 'videos?id=%s&key=%s&part=snippet,contentDetails,statistics' \
+ % (video_id, self.youtube_api_key)
+ self.print_info (channel, url, lambda x: "%s | %s | %sx" % (
+ x['snippet']['title'],
+ x['contentDetails']['duration'][2:].lower (),
+ x['statistics']['viewCount']))
+
+ def print_playlist_info (self, channel, playlist_id):
+ url = 'https://www.googleapis.com/youtube/v3/' \
+ + 'playlists?id=%s&key=%s&part=snippet,contentDetails' \
+ % (playlist_id, self.youtube_api_key)
+ self.print_info (channel, url, lambda x: "%s | %d videos" % (
+ x['snippet']['title'],
+ x['contentDetails']['itemCount']))
+
+ def process_line (self, line):
+ msg = self.parse (line)
+ if msg is None:
+ return
+
+ (nick, user, host, command, args) = msg
+ if command != 'PRIVMSG' or len (args) < 2:
+ return
+
+ ctx = args[0]
+ if not ctx.startswith (('#', '+', '&', '!')):
+ ctx = nick
+
+ for regex in self.re_videos:
+ for i in regex.findall (args[1]):
+ self.print_video_info (ctx, i)
+ for regex in self.re_playlists:
+ for i in regex.findall (args[1]):
+ self.print_playlist_info (ctx, i)
+
+ def run (self):
+ self.youtube_api_key = self.get_config ('youtube_api_key')
+ if self.youtube_api_key == "":
+ self.bot_print ("youtube: missing `youtube_api_key'")
+
+ print ("ZYKLONB register")
+
+ for line in sys.stdin:
+ self.process_line (line)
+
+sys.stdin = io.TextIOWrapper (sys.__stdin__.buffer,
+ encoding = 'iso8859-1', newline = '\r\n', line_buffering = True)
+sys.stdout = io.TextIOWrapper (sys.__stdout__.buffer,
+ encoding = 'iso8859-1', newline = '\r\n', line_buffering = True)
+
+YouTube ().run ()
diff --git a/plugins/xC/auto-rejoin.lua b/plugins/xC/auto-rejoin.lua
new file mode 100644
index 0000000..f42fb2e
--- /dev/null
+++ b/plugins/xC/auto-rejoin.lua
@@ -0,0 +1,48 @@
+--
+-- auto-rejoin.lua: join back automatically when someone kicks you
+--
+-- Copyright (c) 2016, Přemysl Eric Janouch
+--
+-- Permission to use, copy, modify, and/or distribute this software for any
+-- purpose with or without fee is hereby granted.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+-- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+-- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+-- SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+-- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+-- OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+--
+
+local timeout
+xC.setup_config {
+ timeout = {
+ type = "integer",
+ comment = "auto rejoin timeout",
+ default = "0",
+
+ on_change = function (v)
+ timeout = v
+ end,
+ validate = function (v)
+ if v < 0 then error ("timeout must not be negative", 0) end
+ end,
+ },
+}
+
+async, await = xC.async, coroutine.yield
+xC.hook_irc (function (hook, server, line)
+ local msg = xC.parse (line)
+ if msg.command ~= "KICK" then return line end
+
+ local who = msg.prefix:match ("^[^!]*")
+ local channel, whom = table.unpack (msg.params)
+ if who ~= whom and whom == server.user.nickname then
+ async.go (function ()
+ await (async.timer_ms (timeout * 1000))
+ server:send ("JOIN " .. channel)
+ end)
+ end
+ return line
+end)
diff --git a/plugins/xC/censor.lua b/plugins/xC/censor.lua
new file mode 100644
index 0000000..49cab5b
--- /dev/null
+++ b/plugins/xC/censor.lua
@@ -0,0 +1,90 @@
+--
+-- censor.lua: black out certain users' messages
+--
+-- Copyright (c) 2016 - 2021, Přemysl Eric Janouch
+--
+-- Permission to use, copy, modify, and/or distribute this software for any
+-- purpose with or without fee is hereby granted.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+-- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+-- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+-- SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+-- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+-- OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+--
+
+local to_pattern = function (mask)
+ if not mask:match ("!") then mask = mask .. "!*" end
+ if not mask:match ("@") then mask = mask .. "@*" end
+
+ -- That is, * acts like a wildcard, otherwise everything is escaped
+ return "^" .. mask:gsub ("[%^%$%(%)%%%.%[%]%+%-%?]", "%%%0")
+ :gsub ("%*", ".*") .. "$"
+end
+
+local patterns = {}
+local read_masks = function (v)
+ patterns = {}
+ local add = function (who, where)
+ local channels = patterns[who] or {}
+ table.insert (channels, where)
+ patterns[who] = channels
+ end
+ for item in v:lower ():gmatch ("[^,]+") do
+ local who, where = item:match ("^([^/]+)/*(.*)")
+ if who then add (to_pattern (who), where == "" or where) end
+ end
+end
+
+local quote
+xC.setup_config {
+ masks = {
+ type = "string_array",
+ default = "\"\"",
+ comment = "user masks (optionally \"/#channel\") to censor",
+ on_change = read_masks
+ },
+ quote = {
+ type = "string",
+ default = "\"\\x0301,01\"",
+ comment = "formatting prefix for censored messages",
+ on_change = function (v) quote = v end
+ },
+}
+
+local decolor = function (text)
+ local rebuilt, last = {""}, 1
+ for start in text:gmatch ('()\x03') do
+ table.insert (rebuilt, text:sub (last, start - 1))
+ local sub = text:sub (start + 1)
+ last = start + (sub:match ('^%d%d?,%d%d?()') or sub:match ('^%d?%d?()'))
+ end
+ return table.concat (rebuilt) .. text:sub (last)
+end
+
+local censor = function (line)
+ -- Taking a shortcut to avoid lengthy message reassembly
+ local start, text = line:match ("^(.- PRIVMSG .- :)(.*)$")
+ local ctcp, rest = text:match ("^(\x01%g+ )(.*)")
+ text = ctcp and ctcp .. quote .. decolor (rest) or quote .. decolor (text)
+ return start .. text
+end
+
+xC.hook_irc (function (hook, server, line)
+ local msg = xC.parse (line)
+ if msg.command ~= "PRIVMSG" then return line end
+
+ local channel = msg.params[1]:lower ()
+ for who, where in pairs (patterns) do
+ if msg.prefix:lower ():match (who) then
+ for _, x in pairs (where) do
+ if x == true or x == channel then
+ return censor (line)
+ end
+ end
+ end
+ end
+ return line
+end)
diff --git a/plugins/xC/fancy-prompt.lua b/plugins/xC/fancy-prompt.lua
new file mode 100644
index 0000000..8ec697a
--- /dev/null
+++ b/plugins/xC/fancy-prompt.lua
@@ -0,0 +1,105 @@
+--
+-- fancy-prompt.lua: the fancy multiline prompt you probably want
+--
+-- Copyright (c) 2016, 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.
+--
+-- Beware that it is a hack and only goes about 90% of the way, which is why
+-- this functionality is only available as a plugin in the first place
+-- (well, and also for customizability).
+--
+-- The biggest problem is that the way we work with Readline is incompatible
+-- with multiline prompts, and normal newlines just don't work. This is being
+-- circumvented by using an overflowing single-line prompt with a specially
+-- crafted character in the rightmost column that prevents the bar's background
+-- from spilling all over the last line.
+--
+-- There is also a problem with C-r search rendering not clearing out the
+-- background but to really fix that mode, we'd have to fully reimplement it
+-- since its alternative prompt very often gets overriden by accident anyway.
+
+xC.hook_prompt (function (hook)
+ local current = xC.current_buffer
+ local chan = current.channel
+ local s = current.server
+
+ local bg_color = "255"
+ local current_n = 0
+ local active = ""
+ for i, buffer in ipairs (xC.buffers) do
+ if buffer == current then
+ current_n = i
+ elseif buffer.new_messages_count ~= buffer.new_unimportant_count then
+ if active ~= "" then active = active .. "," end
+ if buffer.highlighted then
+ active = active .. "!"
+ bg_color = "224"
+ end
+ active = active .. i
+ end
+ end
+ if active ~= "" then active = "(" .. active .. ")" end
+ local x = current_n .. ":" .. current.name
+ if chan and chan.users_len ~= 0 then
+ local params = ""
+ for mode, param in pairs (chan.param_modes) do
+ params = params .. " +" .. mode .. " " .. param
+ end
+ local modes = chan.no_param_modes .. params:sub (3)
+ if modes ~= "" then x = x .. "(+" .. modes .. ")" end
+ x = x .. "{" .. chan.users_len .. "}"
+ end
+ if current.hide_unimportant then x = x .. "" end
+
+ local lines, cols = xC.get_screen_size ()
+ x = x .. " " .. active .. string.rep (" ", cols)
+
+ -- Readline 7.0.003 seems to be broken and completely corrupts the prompt.
+ -- However 8.0.004 seems to be fine with these, as is libedit 20191231-3.1.
+ --x = x:gsub("[\128-\255]", "?")
+
+ -- Cut off extra characters and apply formatting, including the hack.
+ -- FIXME: this doesn't count with full-width or zero-width characters.
+ -- We might want to export wcwidth() above term_from_utf8 somehow.
+ local overflow = utf8.offset (x, cols - 1)
+ if overflow then x = x:sub (1, overflow) end
+ x = "\x01\x1b[0;4;1;38;5;16m\x1b[48;5;" .. bg_color .. "m\x02" ..
+ x .. "\x01\x1b[0;4;1;7;38;5;" .. bg_color .. "m\x02 \x01\x1b[0;1m\x02"
+
+ local user_prefix = function (chan, user)
+ for i, chan_user in ipairs (chan.users) do
+ if chan_user.user == user then return chan_user.prefixes end
+ end
+ return ""
+ end
+ if s then
+ x = x .. "["
+ local state = s.state
+ if state == "disconnected" or state == "connecting" then
+ x = x .. "(" .. state .. ")"
+ elseif state ~= "registered" then
+ x = x .. "(unregistered)"
+ else
+ local user, modes = s.user, s.user_mode
+ if chan then x = x .. user_prefix (chan, user) end
+ x = x .. user.nickname
+ if modes ~= "" then x = x .. "(" .. modes .. ")" end
+ end
+ x = x .. "] "
+ else
+ -- There needs to be at least one character so that the cursor
+ -- doesn't get damaged by our hack in that last column
+ x = x .. "> "
+ end
+ return x
+end)
diff --git a/plugins/xC/last-fm.lua b/plugins/xC/last-fm.lua
new file mode 100644
index 0000000..3bdfed2
--- /dev/null
+++ b/plugins/xC/last-fm.lua
@@ -0,0 +1,178 @@
+--
+-- last-fm.lua: "now playing" feature using the last.fm API
+--
+-- Dependencies: lua-cjson (from luarocks e.g.)
+--
+-- I call this style closure-oriented programming
+--
+-- Copyright (c) 2016, Přemysl Eric Janouch
+--
+-- Permission to use, copy, modify, and/or distribute this software for any
+-- purpose with or without fee is hereby granted.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+-- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+-- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+-- SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+-- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+-- OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+--
+
+local cjson = require "cjson"
+
+-- Setup configuration to load last.fm API credentials from
+local user, api_key
+xC.setup_config {
+ user = {
+ type = "string",
+ comment = "last.fm username",
+ on_change = function (v) user = v end
+ },
+ api_key = {
+ type = "string",
+ comment = "last.fm API key",
+ on_change = function (v) api_key = v end
+ },
+}
+
+-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+-- Generic error reporting
+local report_error = function (buffer, error)
+ buffer:log ("last-fm error: " .. error)
+end
+
+-- Process data return by the server and extract the now playing song
+local process = function (buffer, data, action)
+ -- There's no reasonable Lua package to parse HTTP that I could find
+ local s, e, v, status, message = string.find (data, "(%S+) (%S+) .+\r\n")
+ if not s then return "server returned unexpected data" end
+ if status ~= "200" then return status .. " " .. message end
+
+ local s, e = string.find (data, "\r\n\r\n")
+ if not s then return "server returned unexpected data" end
+
+ local parser = cjson.new ()
+ data = parser.decode (string.sub (data, e + 1))
+ if not data.recenttracks or not data.recenttracks.track then
+ return "invalid response" end
+
+ -- Need to make some sense of the XML automatically converted to JSON
+ local text_of = function (node)
+ if type (node) ~= "table" then return node end
+ return node["#text"] ~= "" and node["#text"] or nil
+ end
+
+ local name, artist, album
+ for i, track in ipairs (data.recenttracks.track) do
+ if track["@attr"] and track["@attr"].nowplaying then
+ if track.name then name = text_of (track.name) end
+ if track.artist then artist = text_of (track.artist) end
+ if track.album then album = text_of (track.album) end
+ end
+ end
+
+ if not name then
+ action (false)
+ else
+ local np = "\"" .. name .. "\""
+ if artist then np = np .. " by " .. artist end
+ if album then np = np .. " from " .. album end
+ action (np)
+ end
+end
+
+-- Set up the connection and make the request
+local on_connected = function (buffer, c, host, action)
+ -- Buffer data in the connection object
+ c.data = ""
+ c.on_data = function (data)
+ c.data = c.data .. data
+ end
+
+ -- And process it after we receive everything
+ c.on_eof = function ()
+ error = process (buffer, c.data, action)
+ if error then report_error (buffer, error) end
+ c:close ()
+ end
+ c.on_error = function (e)
+ report_error (buffer, e)
+ end
+
+ -- Make the unencrypted HTTP request
+ local url = "/2.0/?method=user.getrecenttracks&user=" .. user ..
+ "&limit=1&api_key=" .. api_key .. "&format=json"
+ c:send ("GET " .. url .. " HTTP/1.1\r\n")
+ c:send ("User-agent: last-fm.lua\r\n")
+ c:send ("Host: " .. host .. "\r\n")
+ c:send ("Connection: close\r\n")
+ c:send ("\r\n")
+end
+
+-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+-- Avoid establishing more than one connection at a time
+local running
+
+-- Initiate a connection to last.fm servers
+async, await = xC.async, coroutine.yield
+local make_request = function (buffer, action)
+ if not user or not api_key then
+ report_error (buffer, "configuration is incomplete")
+ return
+ end
+
+ if running then running:cancel () end
+ running = async.go (function ()
+ local c, host, e = await (async.dial ("ws.audioscrobbler.com", 80))
+ if e then
+ report_error (buffer, e)
+ else
+ on_connected (buffer, c, host, action)
+ end
+ running = nil
+ end)
+end
+
+-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+local now_playing
+
+local tell_song = function (buffer)
+ if now_playing == nil then
+ buffer:log ("last-fm: I don't know what you're listening to")
+ elseif not now_playing then
+ buffer:log ("last-fm: not playing anything right now")
+ else
+ buffer:log ("last-fm: now playing: " .. now_playing)
+ end
+end
+
+local send_song = function (buffer)
+ if not now_playing then
+ tell_song (buffer)
+ else
+ buffer:execute ("/me is listening to " .. now_playing)
+ end
+end
+
+-- Hook input to simulate new commands
+xC.hook_input (function (hook, buffer, input)
+ if input == "/np" then
+ make_request (buffer, function (np)
+ now_playing = np
+ send_song (buffer)
+ end)
+ elseif input == "/np?" then
+ make_request (buffer, function (np)
+ now_playing = np
+ tell_song (buffer)
+ end)
+ elseif input == "/np!" then
+ send_song (buffer)
+ else
+ return input
+ end
+end)
diff --git a/plugins/xC/ping-timeout.lua b/plugins/xC/ping-timeout.lua
new file mode 100644
index 0000000..c455c57
--- /dev/null
+++ b/plugins/xC/ping-timeout.lua
@@ -0,0 +1,32 @@
+--
+-- ping-timeout.lua: ping timeout readability enhancement plugin
+--
+-- Copyright (c) 2015 - 2016, Přemysl Eric Janouch
+--
+-- Permission to use, copy, modify, and/or distribute this software for any
+-- purpose with or without fee is hereby granted.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+-- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+-- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+-- SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+-- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+-- OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+--
+
+xC.hook_irc (function (hook, server, line)
+ local msg = xC.parse (line)
+ local start, timeout = line:match ("^(.* :Ping timeout:) (%d+) seconds$")
+ if msg.command ~= "QUIT" or not start then
+ return line
+ end
+
+ local minutes = timeout // 60
+ if minutes == 0 then
+ return line
+ end
+
+ local seconds = timeout % 60
+ return ("%s %d minutes, %d seconds"):format (start, minutes, seconds)
+end)
diff --git a/plugins/xC/prime.lua b/plugins/xC/prime.lua
new file mode 100644
index 0000000..23740ee
--- /dev/null
+++ b/plugins/xC/prime.lua
@@ -0,0 +1,68 @@
+--
+-- prime.lua: highlight prime numbers in messages
+--
+-- Copyright (c) 2020, Přemysl Eric Janouch
+--
+-- Permission to use, copy, modify, and/or distribute this software for any
+-- purpose with or without fee is hereby granted.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+-- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+-- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+-- SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+-- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+-- OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+--
+
+local smallest, highlight = 0, "\x1f"
+xC.setup_config {
+ smallest = {
+ type = "integer",
+ default = "0",
+ comment = "smallest number to scan for primality",
+ on_change = function (v) smallest = math.max (v, 2) end
+ },
+ highlight = {
+ type = "string",
+ default = "\"\\x1f\"",
+ comment = "the attribute to use for highlights",
+ on_change = function (v) highlight = v end
+ },
+}
+
+-- The prime test is actually very fast, so there is no DoS concern
+local do_intercolour = function (text)
+ return tostring (text:gsub ("%f[%w_]%d+", function (n)
+ if tonumber (n) < smallest then return nil end
+ for i = 2, n ^ (1 / 2) do if (n % i) == 0 then return nil end end
+ return highlight .. n .. highlight
+ end))
+end
+
+local do_interlink = function (text)
+ local rebuilt, last = {""}, 1
+ for start in text:gmatch ('()\x03') do
+ table.insert (rebuilt, do_intercolour (text:sub (last, start - 1)))
+ local sub = text:sub (start + 1)
+ last = start + (sub:match ('^%d%d?,%d%d?()') or sub:match ('^%d?%d?()'))
+ table.insert (rebuilt, text:sub (start, last - 1))
+ end
+ return table.concat (rebuilt) .. do_intercolour (text:sub (last))
+end
+
+local do_message = function (text)
+ local rebuilt, last = {""}, 1
+ for run, link, endpos in text:gmatch ('(.-)(%f[%g]https?://%g+)()') do
+ last = endpos
+ table.insert (rebuilt, do_interlink (run) .. link)
+ end
+ return table.concat (rebuilt) .. do_interlink (text:sub (last))
+end
+
+-- XXX: sadly it won't typically highlight primes in our own messages,
+-- unless IRCv3 echo-message is on
+xC.hook_irc (function (hook, server, line)
+ local start, message = line:match ("^(.- PRIVMSG .- :)(.*)$")
+ return message and start .. do_message (message) or line
+end)
diff --git a/plugins/xC/slack.lua b/plugins/xC/slack.lua
new file mode 100644
index 0000000..c1a08de
--- /dev/null
+++ b/plugins/xC/slack.lua
@@ -0,0 +1,147 @@
+--
+-- slack.lua: try to fix up UX when using the Slack IRC gateway
+--
+-- Copyright (c) 2017, Přemysl Eric Janouch
+--
+-- Permission to use, copy, modify, and/or distribute this software for any
+-- purpose with or without fee is hereby granted.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+-- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+-- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+-- SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+-- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+-- OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+--
+
+local servers = {}
+local read_servers = function (v)
+ servers = {}
+ for name in v:lower ():gmatch "[^,]+" do
+ servers[name] = true
+ end
+end
+
+-- This is a reverse list of Slack's automatic emoji, noseless forms
+local unemojify, emoji, emoji_default = false, {}, {
+ heart = "<3",
+ broken_heart = "3",
+ sunglasses = "8)",
+ anguished = "D:",
+ cry = ":'(",
+ monkey_face = ":o)",
+ kiss = ":*",
+ smiley = "=)",
+ smile = ":D",
+ wink = ";)",
+ laughing = ":>",
+ neutral_face = ":|",
+ open_mouth = ":o",
+ angry = ">:(",
+ slightly_smiling_face = ":)",
+ disappointed = ":(",
+ confused = ":/",
+ stuck_out_tongue = ":p",
+ stuck_out_tongue_winking_eye = ";p",
+}
+local load_emoji = function (extra)
+ emoji = {}
+ for k, v in pairs (emoji_default) do emoji[k] = v end
+ for k, v in extra:gmatch "([^,]+) ([^,]+)" do emoji[k] = v end
+end
+
+xC.setup_config {
+ servers = {
+ type = "string_array",
+ default = "\"\"",
+ comment = "list of server names that are Slack IRC gateways",
+ on_change = read_servers
+ },
+ unemojify = {
+ type = "boolean",
+ default = "true",
+ comment = "convert emoji to normal ASCII emoticons",
+ on_change = function (v) unemojify = v end
+ },
+ extra_emoji = {
+ type = "string_array",
+ default = "\"grinning :)),joy :'),innocent o:),persevere >_<\"",
+ comment = "overrides or extra emoji for unemojify",
+ on_change = function (v) load_emoji (v) end
+ }
+}
+
+-- We can handle external messages about what we've supposedly sent just fine,
+-- so let's get rid of that "[username] some message sent from the web UI" crap
+xC.hook_irc (function (hook, server, line)
+ local msg, us = xC.parse (line), server.user
+ if not servers[server.name] or msg.command ~= "PRIVMSG" or not us
+ or msg.params[1]:lower () ~= us.nickname:lower () then return line end
+
+ -- Taking a shortcut to avoid lengthy message reassembly
+ local quoted_nick = us.nickname:gsub ("[%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%0")
+ local text = line:match ("^.- PRIVMSG .- :%[" .. quoted_nick .. "%] (.*)$")
+ if not text then return line end
+ return ":" .. us.nickname .. "!" .. server.irc_user_host .. " PRIVMSG "
+ .. msg.prefix:match "^[^!@]*" .. " :" .. text
+end)
+
+-- Unfuck emoji and :nick!nick@irc.tinyspeck.com MODE #channel +v nick : active
+xC.hook_irc (function (hook, server, line)
+ if not servers[server.name] then return line end
+ if unemojify then
+ local start, text = line:match ("^(.- PRIVMSG .- :)(.*)$")
+ if start then return start .. text:gsub (":([a-z_]+):", function (name)
+ if emoji[name] then return emoji[name] end
+ return ":" .. name .. ":"
+ end) end
+ end
+ return line:gsub ("^(:%S+ MODE .+) : .*", "%1")
+end)
+
+-- The gateway simply ignores the NAMES command altogether
+xC.hook_input (function (hook, buffer, input)
+ if not buffer.channel or not servers[buffer.server.name]
+ or not input:match "^/names%s*$" then return input end
+
+ local users = buffer.channel.users
+ table.sort (users, function (a, b)
+ if a.prefixes > b.prefixes then return true end
+ if a.prefixes < b.prefixes then return false end
+ return a.user.nickname < b.user.nickname
+ end)
+
+ local names = "Users on " .. buffer.channel.name .. ":"
+ for i, chan_user in ipairs (users) do
+ names = names .. " " .. chan_user.prefixes .. chan_user.user.nickname
+ end
+ buffer:log (names)
+end)
+
+xC.hook_completion (function (hook, data, word)
+ local chan = xC.current_buffer.channel
+ local server = xC.current_buffer.server
+ if not chan or not servers[server.name] then return end
+
+ -- In /commands there is typically no desire at all to add the at sign
+ if data.location == 1 and data.words[1]:match "^/" then return end
+
+ -- Handle both when the at sign is already there and when it is not
+ local needle = word:gsub ("^@", ""):lower ()
+
+ local t = {}
+ local try = function (name)
+ if data.location == 0 then name = name .. ":" end
+ if name:sub (1, #needle):lower () == needle then
+ table.insert (t, "@" .. name)
+ end
+ end
+ for _, chan_user in ipairs (chan.users) do
+ try (chan_user.user.nickname)
+ end
+ for _, special in ipairs { "channel", "here" } do
+ try (special)
+ end
+ return t
+end)
diff --git a/plugins/xC/thin-cursor.lua b/plugins/xC/thin-cursor.lua
new file mode 100644
index 0000000..d0fbf38
--- /dev/null
+++ b/plugins/xC/thin-cursor.lua
@@ -0,0 +1,27 @@
+--
+-- thin-cursor.lua: set a thin cursor
+--
+-- Copyright (c) 2016, Přemysl Eric Janouch
+--
+-- Permission to use, copy, modify, and/or distribute this software for any
+-- purpose with or without fee is hereby granted.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+-- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+-- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+-- SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+-- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+-- OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+--
+-- If tmux doesn't work, add the following to its configuration:
+-- set -as terminal-overrides ',*:Ss=\E[%p1%d q:Se=\E[2 q'
+-- Change the "2" as per http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
+
+local out = io.output ()
+out:write ("\x1b[6 q"):flush ()
+
+-- By registering a global variable, we get notified about plugin unload
+x = setmetatable ({}, { __gc = function ()
+ out:write ("\x1b[2 q"):flush ()
+end })
diff --git a/plugins/xC/utm-filter.lua b/plugins/xC/utm-filter.lua
new file mode 100644
index 0000000..82c4522
--- /dev/null
+++ b/plugins/xC/utm-filter.lua
@@ -0,0 +1,62 @@
+--
+-- utm-filter.lua: filter out Google Analytics bullshit from URLs
+--
+-- Copyright (c) 2015, 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 list of useless URL parameters that don't affect page function
+local banned = {
+ gclid = 1,
+
+ utm_source = 1,
+ utm_medium = 1,
+ utm_term = 1,
+ utm_content = 1,
+ utm_campaign = 1,
+}
+
+-- Go through a parameter list and throw out any banned elements
+local do_args = function (args)
+ local filtered = {}
+ for part in args:gmatch ("[^&]+") do
+ if not banned[part:match ("^[^=]*")] then
+ table.insert (filtered, part)
+ end
+ end
+ return table.concat (filtered, "&")
+end
+
+-- Filter parameters in both the query and the fragment part of an URL
+local do_single_url = function (url)
+ return url:gsub ('^([^?#]*)%?([^#]*)', function (start, query)
+ local clean = do_args (query)
+ return #clean > 0 and start .. "?" .. clean or start
+ end, 1):gsub ('^([^#]*)#(.*)', function (start, fragment)
+ local clean = do_args (fragment)
+ return #clean > 0 and start .. "#" .. clean or start
+ end, 1)
+end
+
+local do_text = function (text)
+ return text:gsub ('%f[%g]https?://%g+', do_single_url)
+end
+
+xC.hook_irc (function (hook, server, line)
+ local start, message = line:match ("^(.* :)(.*)$")
+ return message and start .. do_text (message) or line
+end)
+
+xC.hook_input (function (hook, buffer, input)
+ return do_text (input)
+end)
diff --git a/plugins/zyklonb/calc b/plugins/zyklonb/calc
deleted file mode 100755
index 8e36357..0000000
--- a/plugins/zyklonb/calc
+++ /dev/null
@@ -1,241 +0,0 @@
-#!/usr/bin/env guile
-
- ZyklonB calc plugin, basic Scheme evaluator
-
- Copyright 2016 Přemysl Eric Janouch
- See the file LICENSE for licensing information.
-
-!#
-
-(import (rnrs (6)))
-(use-modules ((rnrs) :version (6)))
-
-; --- Message parsing ----------------------------------------------------------
-
-(define-record-type message (fields prefix command params))
-(define (parse-message line)
- (let f ([parts '()] [chars (string->list line)])
- (define (take-word w chars)
- (if (or (null? chars) (eqv? (car chars) #\x20))
- (f (cons (list->string (reverse w)) parts)
- (if (null? chars) chars (cdr chars)))
- (take-word (cons (car chars) w) (cdr chars))))
- (if (null? chars)
- (let ([data (reverse parts)])
- (when (< (length data) 2)
- (error 'parse-message "invalid message"))
- (make-message (car data) (cadr data) (cddr data)))
- (if (null? parts)
- (if (eqv? (car chars) #\:)
- (take-word '() (cdr chars))
- (f (cons #f parts) chars))
- (if (eqv? (car chars) #\:)
- (f (cons (list->string (cdr chars)) parts) '())
- (take-word '() chars))))))
-
-; --- Utilities ----------------------------------------------------------------
-
-(define (display-exception e port)
- (define (puts . x)
- (for-all (lambda (a) (display a port)) x)
- (newline port))
-
- (define (record-fields rec)
- (let* ([rtd (record-rtd rec)]
- [v (record-type-field-names rtd)]
- [len (vector-length v)])
- (map (lambda (k i) (cons k ((record-accessor rtd i) rec)))
- (vector->list v)
- (let c ([i len] [ls '()])
- (if (= i 0) ls (c (- i 1) (cons (- i 1) ls)))))))
-
- (puts "Caught " (record-type-name (record-rtd e)))
- (for-all
- (lambda (subtype)
- (puts " " (record-type-name (record-rtd subtype)))
- (for-all
- (lambda (field) (puts " " (car field) ": " (cdr field)))
- (record-fields subtype)))
- (simple-conditions e)))
-
-; XXX - we have to work around Guile's lack of proper eol-style support
-(define xc (make-transcoder (latin-1-codec) 'lf 'replace))
-(define irc-input-port (transcoded-port (standard-input-port) xc))
-(define irc-output-port (transcoded-port (standard-output-port) xc))
-
-(define (send . message)
- (for-all (lambda (x) (display x irc-output-port)) message)
- (display #\return irc-output-port)
- (newline irc-output-port)
- (flush-output-port irc-output-port))
-
-(define (get-line-crlf port)
- (define line (get-line port))
- (if (eof-object? line) line
- (let ([len (string-length line)])
- (if (and (> len 0) (eqv? (string-ref line (- len 1)) #\return))
- (substring line 0 (- len 1)) line))))
-
-(define (get-config name)
- (send "ZYKLONB get_config :" name)
- (car (message-params (parse-message (get-line-crlf irc-input-port)))))
-
-(define (extract-nick prefix)
- (do ([i 0 (+ i 1)] [len (string-length prefix)])
- ([or (= i len) (char=? #\! (string-ref prefix i))]
- [substring prefix 0 i])))
-
-(define (string-after s start)
- (let ([s-len (string-length s)] [with-len (string-length start)])
- (and (>= s-len with-len)
- (string=? (substring s 0 with-len) start)
- (substring s with-len s-len))))
-
-; --- Calculator ---------------------------------------------------------------
-
-; Evaluator derived from the example in The Scheme Programming Language.
-;
-; Even though EVAL with a carefully crafted environment would also do a good
-; job at sandboxing, it would probably be impossible to limit execution time...
-
-(define (env-new formals actuals env)
- (cond [(null? formals) env]
- [(symbol? formals) (cons (cons formals actuals) env)]
- [else (cons (cons (car formals) (car actuals))
- (env-new (cdr formals) (cdr actuals) env))]))
-(define (env-lookup var env) (cdr (assq var env)))
-(define (env-assign var val env) (set-cdr! (assq var env) val))
-
-(define (check-reductions r)
- (if (= (car r) 0)
- (error 'check-reductions "reduction limit exceeded")
- (set-car! r (- (car r) 1))))
-
-; TODO - think about implementing more syntactical constructs,
-; however there's not much point in having anything else in a calculator...
-(define (exec expr r env)
- (check-reductions r)
- (cond [(symbol? expr) (env-lookup expr env)]
- [(pair? expr)
- (case (car expr)
- [(quote) (cadr expr)]
- [(lambda) (lambda vals
- (let ([env (env-new (cadr expr) vals env)])
- (let loop ([exprs (cddr expr)])
- (if (null? (cdr exprs))
- (exec (car exprs) r env)
- (begin (exec (car exprs) r env)
- (loop (cdr exprs)))))))]
- [(if) (if (exec (cadr expr) r env)
- (exec (caddr expr) r env)
- (exec (cadddr expr) r env))]
- [(set!) (env-assign (cadr expr) (exec (caddr expr) r env) env)]
- [else (apply (exec (car expr) r env)
- (map (lambda (x) (exec x r env)) (cdr expr)))])]
- [else expr]))
-
-(define-syntax forward
- (syntax-rules ()
- [(_) '()]
- [(_ a b ...) (cons (cons (quote a) a) (forward b ...))]))
-
-; ...which can't prevent me from simply importing most of the standard library
-(define base-library
- (forward
- ; Equivalence, procedure predicate, booleans
- eqv? eq? equal? procedure? boolean? boolean=? not
- ; numbers, numerical input and output
- number? complex? real? rational? integer? exact? inexact? exact inexact
- real-valued? rational-valued? integer-valued? number->string string->number
- ; Arithmetic
- = < > <= >= zero? positive? negative? odd? even? finite? infinite? nan?
- min max + * - / abs div-and-mod div mod div0-and-mod0 div0 mod0
- gcd lcm numerator denominator floor ceiling truncate round
- rationalize exp log sin cos tan asin acos atan sqrt expt
- make-rectangular make-polar real-part imag-part magnitude angle
- ; Pairs and lists
- map for-each cons car cdr caar cadr cdar cddr
- caaar caadr cadar caddr cdaar cdadr cddar cdddr
- caaaar caaadr caadar caaddr cadaar cadadr caddar cadddr
- cdaaar cdaadr cdadar cdaddr cddaar cddadr cdddar cddddr
- pair? null? list? list length append reverse list-tail list-ref
- ; Symbols
- symbol? symbol=? symbol->string string->symbol
- ; Characters
- char? char=? char char>? char<=? char>=? char->integer integer->char
- ; Strings; XXX - omitted make-string - can cause OOM
- string? string=? string string>? string<=? string>=?
- string string-length string-ref substring
- string-append string->list list->string string-for-each string-copy
- ; Vectors; XXX - omitted make-vector - can cause OOM
- vector? vector vector-length vector-ref vector-set!
- vector->list list->vector vector-fill! vector-map vector-for-each
- ; Control features
- apply call/cc values call-with-values dynamic-wind))
-(define extended-library
- (forward
- char-upcase char-downcase char-titlecase char-foldcase
- char-ci=? char-ci char-ci>? char-ci<=? char-ci>=?
- char-alphabetic? char-numeric? char-whitespace?
- char-upper-case? char-lower-case? char-title-case?
- string-upcase string-downcase string-titlecase string-foldcase
- string-ci=? string-ci string-ci>? string-ci<=? string-ci>=?
- find for-all exists filter partition fold-left fold-right
- remp remove remv remq memp member memv memq assp assoc assv assq cons*
- list-sort vector-sort vector-sort!
- bitwise-not bitwise-and bitwise-ior bitwise-xor bitwise-if
- bitwise-bit-count bitwise-length bitwise-first-bit-set bitwise-bit-set?
- bitwise-copy-bit bitwise-bit-field bitwise-copy-bit-field
- bitwise-arithmetic-shift bitwise-rotate-bit-field bitwise-reverse-bit-field
- bitwise-arithmetic-shift-left bitwise-arithmetic-shift-right
- set-car! set-cdr! string-set! string-fill!))
-(define (interpret expr)
- (exec expr '(2000) (append base-library extended-library)))
-
-; We could show something a bit nicer but it would be quite Guile-specific
-(define (error-string e)
- (map (lambda (x) (string-append " " (symbol->string x)))
- (filter (lambda (x) (not (member x '(&who &message &irritants &guile))))
- (map (lambda (x) (record-type-name (record-rtd x)))
- (simple-conditions e)))))
-
-(define (calc input respond)
- (define (stringify x)
- (call-with-string-output-port (lambda (port) (write x port))))
- (guard (e [else (display-exception e (current-error-port))
- (apply respond "caught" (error-string e))])
- (let* ([input (open-string-input-port input)]
- [data (let loop ()
- (define datum (get-datum input))
- (if (eof-object? datum) '() (cons datum (loop))))])
- (call-with-values
- (lambda () (interpret (list (append '(lambda ()) data))))
- (lambda message
- (for-all (lambda (x) (respond (stringify x))) message))))))
-
-; --- Main loop ----------------------------------------------------------------
-
-(define prefix (get-config "prefix"))
-(send "ZYKLONB register")
-
-(define (process msg)
- (when (string-ci=? (message-command msg) "PRIVMSG")
- (let* ([nick (extract-nick (message-prefix msg))]
- [target (car (message-params msg))]
- [response-begin
- (apply string-append "PRIVMSG "
- (if (memv (string-ref target 0) (string->list "#&!+"))
- `(,target " :" ,nick ": ") `(,nick " :")))]
- [respond (lambda args (apply send response-begin args))]
- [text (cadr (message-params msg))]
- [input (or (string-after text (string-append prefix "calc "))
- (string-after text (string-append prefix "= ")))])
- (when input (calc input respond)))))
-
-(let main-loop ()
- (define line (get-line-crlf irc-input-port))
- (unless (eof-object? line)
- (guard (e [else (display-exception e (current-error-port))])
- (unless (string=? "" line)
- (process (parse-message line))))
- (main-loop)))
diff --git a/plugins/zyklonb/coin b/plugins/zyklonb/coin
deleted file mode 100755
index 7dfe923..0000000
--- a/plugins/zyklonb/coin
+++ /dev/null
@@ -1,128 +0,0 @@
-#!/usr/bin/env tclsh
-#
-# ZyklonB coin plugin, random number-based utilities
-#
-# Copyright 2012, 2014 Přemysl Eric Janouch
-# See the file LICENSE for licensing information.
-#
-
-# This is a terrible excuse for a programming language and I feel dirty.
-
-proc parse {line} {
- global msg
- unset -nocomplain msg
-
- if [regexp {^:([^ ]*) *(.*)} $line -> prefix rest] {
- set msg(prefix) $prefix
- set line $rest
- }
- if [regexp {^([^ ]*) *(.*)} $line -> command rest] {
- set msg(command) $command
- set line $rest
- }
- while {1} {
- set line [string trimleft $line " "]
- set i [string first " " $line]
- if {$i == -1} { set i [string length $line] }
- if {$i == 0} { break }
-
- if {[string index $line 0] == ":"} {
- lappend msg(param) [string range $line 1 end]
- break
- }
- lappend msg(param) [string range $line 0 [expr $i - 1]]
- set line [string range $line $i end]
- }
-}
-
-proc get_config {key} {
- global msg
- puts "ZYKLONB get_config :$key"
- gets stdin line
- parse $line
- return [lindex $msg(param) 0]
-}
-
-proc pmrespond {text} {
- global ctx
- global ctx_quote
- puts "PRIVMSG $ctx :$ctx_quote$text"
-}
-
-fconfigure stdin -translation crlf -encoding iso8859-1
-fconfigure stdout -translation crlf -encoding iso8859-1
-
-set prefix [get_config prefix]
-puts "ZYKLONB register"
-
-set eightball [list \
- "It is certain" \
- "It is decidedly so" \
- "Without a doubt" \
- "Yes - definitely" \
- "You may rely on it" \
- "As I see it, yes" \
- "Most likely" \
- "Outlook good" \
- "Yes" \
- "Signs point to yes" \
- "Reply hazy, try again" \
- "Ask again later" \
- "Better not tell you now" \
- "Cannot predict now" \
- "Concentrate and ask again" \
- "Don't count on it" \
- "My reply is no" \
- "My sources say no" \
- "Outlook not so good" \
- "Very doubtful"]
-
-while {[gets stdin line] != -1} {
- parse $line
-
- if {! [info exists msg(prefix)] || ! [info exists msg(command)]
- || $msg(command) != "PRIVMSG" || ! [info exists msg(param)]
- || [llength $msg(param)] < 2} { continue }
-
- regexp {^[^!]*} $msg(prefix) ctx
- if [regexp {^[#&+!]} [lindex $msg(param) 0]] {
- set ctx_quote "$ctx: "
- set ctx [lindex $msg(param) 0]
- } else { set ctx_quote "" }
-
- set input [lindex $msg(param) 1]
- set first_chars [string range $input 0 \
- [expr [string length $prefix] - 1]]
- if {$first_chars != $prefix} { continue }
- set input [string range $input [string length $prefix] end]
-
- if {$input == "coin"} {
- if {rand() < 0.5} {
- pmrespond "Heads."
- } else {
- pmrespond "Tails."
- }
- } elseif {[regexp {^dice( +|$)(.*)} $input -> _ args]} {
- if {! [string is integer -strict $args] || $args <= 0} {
- pmrespond "Invalid or missing number."
- } else {
- pmrespond [expr {int($args * rand()) + 1}]
- }
- } elseif {[regexp {^(choose|\?)( +|$)(.*)} $input -> _ _ args]} {
- if {$args == ""} {
- pmrespond "Nothing to choose from."
- } else {
- set c [split $args ",|"]
- pmrespond [string trim [lindex $c \
- [expr {int([llength $c] * rand())}]]]
- }
- } elseif {[regexp {^eightball( +|$)(.*)} $input -> _ args]} {
- if {$args == ""} {
- pmrespond "You should, you know, ask something."
- } else {
- pmrespond [lindex $eightball \
- [expr {int([llength $eightball] * rand())}]].
- }
- }
-}
-
diff --git a/plugins/zyklonb/eval b/plugins/zyklonb/eval
deleted file mode 100755
index ccc7f0a..0000000
--- a/plugins/zyklonb/eval
+++ /dev/null
@@ -1,312 +0,0 @@
-#!/usr/bin/awk -f
-#
-# ZyklonB eval plugin, LISP-like expression evaluator
-#
-# Copyright 2013, 2014 Přemysl Eric Janouch
-# See the file LICENSE for licensing information.
-#
-
-BEGIN \
-{
- RS = "\r"
- ORS = "\r\n"
- IGNORECASE = 1
- srand()
-
- prefix = get_config("prefix")
-
- print "ZYKLONB register"
- fflush("")
-
- # All functions have to be in this particular array
- min_args["int"] = 1
- min_args["+"] = 1
- min_args["-"] = 1
- min_args["*"] = 1
- min_args["/"] = 1
- min_args["%"] = 1
- min_args["^"] = 1
- min_args["**"] = 1
- min_args["exp"] = 1
- min_args["sin"] = 1
- min_args["cos"] = 1
- min_args["atan2"] = 2
- min_args["log"] = 1
- min_args["rand"] = 0
- min_args["sqrt"] = 1
-
- min_args["pi"] = 0
- min_args["e"] = 0
-
- min_args["min"] = 1
- min_args["max"] = 1
-
- # Whereas here their presence is only optional
- max_args["int"] = 1
- max_args["sin"] = 1
- max_args["cos"] = 1
- max_args["atan2"] = 2
- max_args["log"] = 1
- max_args["rand"] = 0
- max_args["sqrt"] = 1
-
- max_args["pi"] = 0
- max_args["e"] = 0
-}
-
-{
- parse($0)
-}
-
-msg_command == "PRIVMSG" \
-{
- # Context = either channel or user nickname
- match(msg_prefix, /^[^!]+/)
- ctx = substr(msg_prefix, RSTART, RLENGTH)
- if (msg_param[0] ~ /^[#&!+]/)
- {
- ctx_quote = ctx ": "
- ctx = msg_param[0]
- }
- else
- ctx_quote = ""
-
-
- if (substr(msg_param[1], 1, length(prefix)) == prefix)
- {
- keyword = "eval"
- text = substr(msg_param[1], 1 + length(prefix))
- if (match(text, "^" keyword "([^A-Za-z0-9].*|$)"))
- process_request(substr(text, 1 + length(keyword)))
- }
-}
-
-{
- fflush("")
-}
-
-function pmrespond (text)
-{
- print "PRIVMSG " ctx " :" ctx_quote text
-}
-
-function process_request (input, res, x)
-{
- delete funs
- delete accumulator
- delete n_args
-
- res = ""
- fun_top = 0
- funs[0] = ""
- accumulator[0] = 0
- n_args[0] = 0
-
- if (match(input, "^[ \t]*"))
- input = substr(input, RLENGTH + 1)
- if (input == "")
- res = "expression missing"
-
- while (res == "" && input != "") {
- if (match(input, "^-?[0-9]+\\.?[0-9]*")) {
- x = substr(input, RSTART, RLENGTH)
- input = substr(input, RLENGTH + 1)
-
- match(input, "^ *")
- input = substr(input, RLENGTH + 1)
-
- res = process_argument(x)
- } else if (match(input, "^[(]([^ ()]+)")) {
- x = substr(input, RSTART + 1, RLENGTH - 1)
- input = substr(input, RLENGTH + 1)
-
- match(input, "^ *")
- input = substr(input, RLENGTH + 1)
-
- if (!(x in min_args)) {
- res = "undefined function '" x "'"
- } else {
- fun_top++
- funs[fun_top] = x
- accumulator[fun_top] = 636363
- n_args[fun_top] = 0
- }
- } else if (match(input, "^[)] *")) {
- input = substr(input, RLENGTH + 1)
- res = process_end()
- } else
- res = "invalid input at '" substr(input, 1, 10) "...'"
- }
-
- if (res == "") {
- if (fun_top != 0)
- res = "unclosed '" funs[fun_top] "'"
- else if (n_args[0] != 1)
- res = "internal error, expected one result" \
- ", got " n_args[0] " instead"
- }
-
- if (res == "")
- pmrespond(accumulator[0])
- else
- pmrespond(res)
-}
-
-function process_argument (arg)
-{
- if (fun_top == 0) {
- if (n_args[0]++ != 0)
- return "too many results, I only expect one"
-
- accumulator[0] = arg
- return ""
- }
-
- fun = funs[fun_top]
- if (fun in max_args && max_args[fun] <= n_args[fun_top])
- return "too many operands for " fun
-
- if (fun == "int") {
- accumulator[fun_top] = int(arg)
- } else if (fun == "+") {
- if (n_args[fun_top] == 0)
- accumulator[fun_top] = arg
- else
- accumulator[fun_top] += arg
- } else if (fun == "-") {
- if (n_args[fun_top] == 0)
- accumulator[fun_top] = arg
- else
- accumulator[fun_top] -= arg
- } else if (fun == "*") {
- if (n_args[fun_top] == 0)
- accumulator[fun_top] = arg
- else
- accumulator[fun_top] *= arg
- } else if (fun == "/") {
- if (n_args[fun_top] == 0)
- accumulator[fun_top] = arg
- else if (arg == 0)
- return "division by zero"
- else
- accumulator[fun_top] /= arg
- } else if (fun == "%") {
- if (n_args[fun_top] == 0)
- accumulator[fun_top] = arg
- else if (arg == 0)
- return "division by zero"
- else
- accumulator[fun_top] %= arg
- } else if (fun == "^" || fun == "**" || fun == "exp") {
- if (n_args[fun_top] == 0)
- accumulator[fun_top] = arg
- else
- accumulator[fun_top] ^= arg
- } else if (fun == "sin") {
- accumulator[fun_top] = sin(arg)
- } else if (fun == "cos") {
- accumulator[fun_top] = cos(arg)
- } else if (fun == "atan2") {
- if (n_args[fun_top] == 0)
- accumulator[fun_top] = arg
- else
- accumulator[fun_top] = atan2(accumulator[fun_top], arg)
- } else if (fun == "log") {
- accumulator[fun_top] = log(arg)
- } else if (fun == "rand") {
- # Just for completeness, execution never gets here
- } else if (fun == "sqrt") {
- accumulator[fun_top] = sqrt(arg)
- } else if (fun == "min") {
- if (n_args[fun_top] == 0)
- accumulator[fun_top] = arg
- else if (accumulator[fun_top] > arg)
- accumulator[fun_top] = arg
- } else if (fun == "max") {
- if (n_args[fun_top] == 0)
- accumulator[fun_top] = arg
- else if (accumulator[fun_top] < arg)
- accumulator[fun_top] = arg
- } else
- return "internal error, unhandled operands for " fun
-
- n_args[fun_top]++
- return ""
-}
-
-function process_end ()
-{
- if (fun_top <= 0)
- return "extraneous ')'"
-
- fun = funs[fun_top]
- if (!(fun in min_args))
- return "internal error, unhandled ')' for '" fun "'"
- if (min_args[fun] > n_args[fun_top])
- return "not enough operands for '" fun "'"
-
- # There's no 'init' function to do it in
- if (fun == "rand")
- accumulator[fun_top] = rand()
- else if (fun == "pi")
- accumulator[fun_top] = 3.141592653589793
- else if (fun == "e")
- accumulator[fun_top] = 2.718281828459045
-
- return process_argument(accumulator[fun_top--])
-}
-
-function get_config (key)
-{
- print "ZYKLONB get_config :" key
- fflush("")
-
- getline
- parse($0)
- return msg_param[0]
-}
-
-function parse (line, s, n, id, token)
-{
- s = 1
- id = 0
-
- # NAWK only uses the first character of RS
- if (line ~ /^\n/)
- line = substr(line, 2)
-
- msg_prefix = ""
- msg_command = ""
- delete msg_param
-
- n = match(substr(line, s), / |$/)
- while (n)
- {
- token = substr(line, s, n - 1)
- if (token ~ /^:/)
- {
- if (s == 1)
- msg_prefix = substr(token, 2)
- else
- {
- msg_param[id] = substr(line, s + 1)
- break
- }
- }
- else if (!msg_command)
- msg_command = toupper(token)
- else
- msg_param[id++] = token
-
- s = s + n
- n = index(substr(line, s), " ")
-
- if (!n)
- {
- n = length(substr(line, s)) + 1
- if (n == 1)
- break;
- }
- }
-}
-
diff --git a/plugins/zyklonb/factoids b/plugins/zyklonb/factoids
deleted file mode 100755
index 431600c..0000000
--- a/plugins/zyklonb/factoids
+++ /dev/null
@@ -1,177 +0,0 @@
-#!/usr/bin/env perl
-#
-# ZyklonB factoids plugin
-#
-# Copyright 2016 Přemysl Eric Janouch
-# See the file LICENSE for licensing information.
-#
-
-use strict;
-use warnings;
-use Text::Wrap;
-
-# --- IRC protocol -------------------------------------------------------------
-
-binmode STDIN; select STDIN; $| = 1; $/ = "\r\n";
-binmode STDOUT; select STDOUT; $| = 1; $\ = "\r\n";
-
-sub parse ($) {
- chomp (my $line = shift);
- return undef unless my ($nick, $user, $host, $command, $args) = ($line =~
- qr/^(?::([^! ]*)(?:!([^@]*)@([^ ]*))? +)?([^ ]+)(?: +(.*))?$/o);
- return {nick => $nick, user => $user, host => $host, command => $command,
- args => defined $args ? [$args =~ /:?((?<=:).*|[^ ]+) */og] : []};
-}
-
-sub bot_print {
- print "ZYKLONB print :${\shift}";
-}
-
-# --- Initialization -----------------------------------------------------------
-
-my %config;
-for my $name (qw(prefix)) {
- print "ZYKLONB get_config :$name";
- $config{$name} = (parse )->{args}->[0];
-}
-
-print "ZYKLONB register";
-
-# --- Database -----------------------------------------------------------------
-# Simple map of (factoid_name => [definitions]); all factoids are separated
-# by newlines and definitions by carriage returns. Both disallowed in IRC.
-
-sub db_load {
- local $/ = "\n";
- my ($path) = @_;
- open my $db, "<", $path or return {};
-
- my %entries;
- while (<$db>) {
- chomp;
- my @defs = split "\r";
- $entries{shift @defs} = \@defs;
- }
- \%entries
-}
-
-sub db_save {
- local $\ = "\n";
- my ($path, $ref) = @_;
- my $path_new = "$path.new";
- open my $db, ">", $path_new or die "db save failed: $!";
-
- my %entries = %$ref;
- print $db join "\r", ($_, @{$entries{$_}}) for keys %entries;
- close $db;
- rename $path_new, $path or die "db save failed: $!";
-}
-
-# --- Factoids -----------------------------------------------------------------
-
-my $db_path = 'factoids.db';
-my %db = %{db_load $db_path};
-
-sub learn {
- my ($respond, $input) = @_;
- return &$respond("usage: = ")
- unless $input =~ /^([^=]+?)(?:\s+(\d+))?\s*=\s*(.+?)\s*$/;
-
- my ($name, $number, $definition) = ($1, $2, $3);
- return &$respond("trailing numbers in names are disallowed")
- if defined $2;
- $db{$name} = [] unless exists $db{$name};
-
- my $entries = $db{$name};
- return &$respond("duplicate definition")
- if grep { lc $_ eq lc $definition } @$entries;
-
- push @$entries, $definition;
- &$respond("saved as #${\scalar @$entries}");
- db_save $db_path, \%db;
-}
-
-sub check_number {
- my ($respond, $name, $number) = @_;
- my $entries = $db{$name};
- if ($number > @$entries) {
- &$respond(qq/"$name" has only ${\scalar @$entries} definitions/);
- } elsif (not $number) {
- &$respond("number must not be zero");
- } else {
- return 1;
- }
- return 0;
-}
-
-sub forget {
- my ($respond, $input) = @_;
- return &$respond("usage: ")
- unless $input =~ /^([^=]+?)\s+(\d+)\s*$/;
-
- my ($name, $number) = ($1, int($2));
- return &$respond(qq/"$name" is undefined/)
- unless exists $db{$name};
-
- my $entries = $db{$name};
- return unless check_number $respond, $name, $number;
-
- splice @$entries, --$number, 1;
- &$respond("forgotten");
- db_save $db_path, \%db;
-}
-
-sub whatis {
- my ($respond, $input) = @_;
- return &$respond("usage: []")
- unless $input =~ /^([^=]+?)(?:\s+(\d+))?\s*$/;
-
- my ($name, $number) = ($1, $2);
- return &$respond(qq/"$name" is undefined/)
- unless exists $db{$name};
-
- my $entries = $db{$name};
- if (defined $number) {
- return unless check_number $respond, $name, $number;
- &$respond(qq/"$name" is #$number $entries->[$number - 1]/);
- } else {
- my $i = 1;
- my $definition = join ", ", map { "#${\$i++} $_" } @{$entries};
- &$respond(qq/"$name" is $definition/);
- }
-}
-
-sub wildcard {
- my ($respond, $input) = @_;
- $input =~ /=/ ? learn(@_) : whatis(@_);
-}
-
-my %commands = (
- 'learn' => \&learn,
- 'forget' => \&forget,
- 'whatis' => \&whatis,
- '??' => \&wildcard,
-);
-
-# --- Input loop ---------------------------------------------------------------
-
-while (my $line = ) {
- my %msg = %{parse $line};
- my @args = @{$msg{args}};
-
- # This plugin only bothers to respond to PRIVMSG messages
- next unless $msg{command} eq 'PRIVMSG' and @args >= 2
- and my ($cmd, $input) = $args[1] =~ /^$config{prefix}(\S+)\s*(.*)/;
-
- # So far the only reaction is a PRIVMSG back to the sender, so all the
- # handlers need is a response callback and all arguments to the command
- my ($target => $quote) = ($args[0] =~ /^[#+&!]/)
- ? ($args[0] => "$msg{nick}: ") : ($msg{nick} => '');
- # Wrap all responses so that there's space for our prefix in the message
- my $respond = sub {
- local ($Text::Wrap::columns, $Text::Wrap::unexpand) = 400, 0;
- my $start = "PRIVMSG $target :$quote";
- print for split "\n", wrap $start, $start, shift;
- };
- &{$commands{$cmd}}($respond, $input) if exists($commands{$cmd});
-}
diff --git a/plugins/zyklonb/pomodoro b/plugins/zyklonb/pomodoro
deleted file mode 100755
index 2bb6531..0000000
--- a/plugins/zyklonb/pomodoro
+++ /dev/null
@@ -1,502 +0,0 @@
-#!/usr/bin/env ruby
-# coding: utf-8
-#
-# ZyklonB pomodoro plugin
-#
-# Copyright 2015 Přemysl Eric Janouch
-# See the file LICENSE for licensing information.
-#
-
-# --- Simple event loop --------------------------------------------------------
-
-# This is more or less a straight-forward port of my C event loop. It's a bit
-# unfortunate that I really have to implement all this in order to get some
-# basic asynchronicity but at least I get to exercise my Ruby.
-
-class TimerEvent
- attr_accessor :index, :when, :callback
-
- def initialize (callback)
- raise ArgumentError unless callback.is_a? Proc
-
- @index = nil
- @when = nil
- @callback = callback
- end
-
- def active?
- @index != nil
- end
-
- def until
- return @when - Time.new
- end
-end
-
-class IOEvent
- READ = 1 << 0
- WRITE = 1 << 1
-
- attr_accessor :read_index, :write_index, :io, :callback
-
- def initialize (io, callback)
- raise ArgumentError unless callback.is_a? Proc
-
- @read_index = nil
- @write_index = nil
- @io = io
- @callback = callback
- end
-end
-
-class EventLoop
- def initialize
- @running = false
- @timers = []
- @readers = []
- @writers = []
- @io_to_event = {}
- end
-
- def set_timer (timer, timeout)
- raise ArgumentError unless timer.is_a? TimerEvent
-
- timer.when = Time.now + timeout
- if timer.index
- heapify_down timer.index
- heapify_up timer.index
- else
- timer.index = @timers.size
- @timers.push timer
- heapify_up timer.index
- end
- end
-
- def reset_timer (timer)
- raise ArgumentError unless timer.is_a? TimerEvent
- remove_timer_at timer.index if timer.index
- end
-
- def set_io (io_event, events)
- raise ArgumentError unless io_event.is_a? IOEvent
- raise ArgumentError unless events.is_a? Numeric
-
- reset_io io_event
-
- @io_to_event[io_event.io] = io_event
- if events & IOEvent::READ
- io_event.read_index = @readers.size
- @readers.push io_event.io
- end
- if events & IOEvent::WRITE
- io_event.read_index = @writers.size
- @writers.push io_event.io
- end
- end
-
- def reset_io (io_event)
- raise ArgumentError unless io_event.is_a? IOEvent
-
- @readers.delete_at io_event.read_index if io_event.read_index
- @writers.delete_at io_event.write_index if io_event.write_index
-
- io_event.read_index = nil
- io_event.write_index = nil
-
- @io_to_event.delete io_event.io
- end
-
- def run
- @running = true
- while @running do one_iteration end
- end
-
- def quit
- @running = false
- end
-
-private
- def one_iteration
- rs, ws, = IO.select @readers, @writers, [], nearest_timeout
- dispatch_timers
- (Array(rs) | Array(ws)).each do |io|
- @io_to_event[io].callback.call io
- end
- end
-
- def dispatch_timers
- now = Time.new
- while not @timers.empty? and @timers[0].when <= now do
- @timers[0].callback.call
- remove_timer_at 0
- end
- end
-
- def nearest_timeout
- return nil if @timers.empty?
- timeout = @timers[0].until
- if timeout < 0 then 0 else timeout end
- end
-
- def remove_timer_at (index)
- @timers[index].index = nil
- moved = @timers.pop
- return if index == @timers.size
-
- @timers[index] = moved
- @timers[index].index = index
- heapify_down index
- end
-
- def swap_timers (a, b)
- @timers[a], @timers[b] = @timers[b], @timers[a]
- @timers[a].index = a
- @timers[b].index = b
- end
-
- def heapify_up (index)
- while index != 0 do
- parent = (index - 1) / 2
- break if @timers[parent].when <= @timers[index].when
- swap_timers index, parent
- index = parent
- end
- end
-
- def heapify_down (index)
- loop do
- parent = index
- left = 2 * index + 1
- right = 2 * index + 2
-
- lowest = parent
- lowest = left if left < @timers.size and
- @timers[left] .when < @timers[lowest].when
- lowest = right if right < @timers.size and
- @timers[right].when < @timers[lowest].when
- break if parent == lowest
-
- swap_timers lowest, parent
- index = lowest
- end
- end
-end
-
-# --- IRC protocol -------------------------------------------------------------
-
-$stdin.set_encoding 'ASCII-8BIT'
-$stdout.set_encoding 'ASCII-8BIT'
-
-$stdin.sync = true
-$stdout.sync = true
-
-$/ = "\r\n"
-$\ = "\r\n"
-
-RE_MSG = /(?::([^! ]*)(?:!([^@]*)@([^ ]*))? +)?([^ ]+)(?: +(.*))?$/
-RE_ARGS = /:?((?<=:).*|[^ ]+) */
-
-def parse (line)
- m = line.match RE_MSG
- return nil if not m
-
- nick, user, host, command, args = *m.captures
- args = if args then args.scan(RE_ARGS).flatten else [] end
- [nick, user, host, command, args]
-end
-
-def bot_print (what)
- print "ZYKLONB print :#{what}"
-end
-
-# --- Initialization -----------------------------------------------------------
-
-# We can only read in configuration from here so far
-# To read it from anywhere else, it has to be done asynchronously
-$config = {}
-[:prefix].each do |name|
- print "ZYKLONB get_config :#{name}"
- _, _, _, _, args = *parse($stdin.gets.chomp)
- $config[name] = args[0]
-end
-
-print "ZYKLONB register"
-
-# --- Plugin logic -------------------------------------------------------------
-
-# FIXME: this needs a major refactor as it doesn't make much sense at all
-
-class MessageMeta < Struct.new(:nick, :user, :host, :channel, :ctx, :quote)
- def respond (message)
- print "PRIVMSG #{ctx} :#{quote}#{message}"
- end
-end
-
-class Context
- attr_accessor :nick, :ctx
-
- def initialize (meta)
- @nick = meta.nick
- @ctx = meta.ctx
- end
-
- def == (other)
- self.class == other.class \
- and other.nick == @nick \
- and other.ctx == @ctx
- end
-
- alias eql? ==
-
- def hash
- @nick.hash ^ @ctx.hash
- end
-end
-
-class PomodoroTimer
- def initialize (context)
- @ctx = context.ctx
- @nicks = [context.nick]
-
- @timer_work = TimerEvent.new(lambda { on_work })
- @timer_rest = TimerEvent.new(lambda { on_rest })
-
- on_work
- end
-
- def inform (message)
- # FIXME: it tells the nick even in PM's
- quote = "#{@nicks.join(" ")}: "
- print "PRIVMSG #{@ctx} :#{quote}#{message}"
- end
-
- def on_work
- inform "work now!"
- $loop.set_timer @timer_rest, 25 * 60
- end
-
- def on_rest
- inform "rest now!"
- $loop.set_timer @timer_work, 5 * 60
- end
-
- def join (meta)
- return if @nicks.include? meta.nick
-
- meta.respond "you have joined their pomodoro"
- @nicks |= [meta.nick]
- end
-
- def part (meta, requested)
- return if not @nicks.include? meta.nick
-
- if requested
- meta.respond "you have stopped your pomodoro"
- end
-
- @nicks -= [meta.nick]
- if @nicks.empty?
- $loop.reset_timer @timer_work
- $loop.reset_timer @timer_rest
- end
- end
-
- def status (meta)
- return if not @nicks.include? meta.nick
-
- if @timer_rest.active?
- till = @timer_rest.until
- meta.respond "working, #{(till / 60).to_i} minutes, " +
- "#{(till % 60).to_i} seconds until rest"
- end
- if @timer_work.active?
- till = @timer_work.until
- meta.respond "resting, #{(till / 60).to_i} minutes, " +
- "#{(till % 60).to_i} seconds until work"
- end
- end
-end
-
-class Pomodoro
- KEYWORD = "pomodoro"
-
- def initialize
- @timers = {}
- end
-
- def on_help (meta, args)
- meta.respond "usage: #{KEYWORD} { start | stop | join | status }"
- end
-
- def on_start (meta, args)
- if args.size != 0
- meta.respond "usage: #{KEYWORD} start"
- return
- end
-
- context = Context.new meta
- if @timers[context]
- meta.respond "you already have a timer running here"
- else
- @timers[context] = PomodoroTimer.new meta
- end
- end
-
- def on_join (meta, args)
- if args.size != 1
- meta.respond "usage: #{KEYWORD} join "
- return
- end
-
- context = Context.new meta
- if @timers[context]
- meta.respond "you already have a timer running here"
- return
- end
-
- joined_context = Context.new meta
- joined_context.nick = args.shift
- timer = @timers[joined_context]
- if not timer
- meta.respond "that person doesn't have a timer here"
- else
- timer.join meta
- @timers[context] = timer
- end
- end
-
- def on_stop (meta, args)
- if args.size != 0
- meta.respond "usage: #{KEYWORD} stop"
- return
- end
-
- context = Context.new meta
- timer = @timers[context]
- if not timer
- meta.respond "you don't have a timer running here"
- else
- timer.part meta, true
- @timers.delete context
- end
- end
-
- def on_status (meta, args)
- if args.size != 0
- meta.respond "usage: #{KEYWORD} status"
- return
- end
-
- timer = @timers[Context.new meta]
- if not timer
- meta.respond "you don't have a timer running here"
- else
- timer.status meta
- end
- end
-
- def process_command (meta, msg)
- args = msg.split
- return if args.shift != KEYWORD
-
- method = "on_#{args.shift}"
- send method, meta, args if respond_to? method
- end
-
- def on_server_nick (meta, command, args)
- # TODO: either handle this properly...
- happened = false
- @timers.keys.each do |key|
- next if key.nick != meta.nick
- @timers[key].part meta, false
- @timers.delete key
- happened = true
- end
- if happened
- # TODO: ...or at least inform the user via his new nick
- end
- end
-
- def on_server_part (meta, command, args)
- # TODO: instead of cancelling the user's pomodoros, either redirect
- # them to PM's and later upon rejoining undo the redirection...
- context = Context.new(meta)
- context.ctx = meta.channel
- if @timers.include? context
- # TODO: ...or at least inform the user about the cancellation
- @timers[context].part meta, false
- @timers.delete context
- end
- end
-
- def on_server_quit (meta, command, args)
- @timers.keys.each do |key|
- next if key.nick != meta.nick
- @timers[key].part meta, false
- @timers.delete key
- end
- end
-
- def process (meta, command, args)
- method = "on_server_#{command.downcase}"
- send method, meta, command, args if respond_to? method
- end
-end
-
-# --- IRC message processing ---------------------------------------------------
-
-$handlers = [Pomodoro.new]
-def process_line (line)
- msg = parse line
- return if not msg
-
- nick, user, host, command, args = *msg
-
- context = nick
- quote = ""
- channel = nil
-
- if args.size >= 1 and args[0].start_with? ?#, ?+, ?&, ?!
- case command
- when "PRIVMSG", "NOTICE", "JOIN"
- context = args[0]
- quote = "#{nick}: "
- channel = args[0]
- when "PART"
- channel = args[0]
- end
- end
-
- # Handle any IRC message
- meta = MessageMeta.new(nick, user, host, channel, context, quote).freeze
- $handlers.each do |handler|
- handler.process meta, command, args
- end
-
- # Handle pre-processed bot commands
- if command == 'PRIVMSG' and args.size >= 2
- msg = args[1]
- return unless msg.start_with? $config[:prefix]
- $handlers.each do |handler|
- handler.process_command meta, msg[$config[:prefix].size..-1]
- end
- end
-end
-
-buffer = ""
-stdin_io = IOEvent.new($stdin, lambda do |io|
- begin
- buffer << io.read_nonblock(4096)
- lines = buffer.split $/, -1
- buffer = lines.pop
- lines.each { |line| process_line line }
- rescue EOFError
- $loop.quit
- rescue IO::WaitReadable
- # Ignore
- end
-end)
-
-$loop = EventLoop.new
-$loop.set_io stdin_io, IOEvent::READ
-$loop.run
diff --git a/plugins/zyklonb/script b/plugins/zyklonb/script
deleted file mode 100755
index c19b8c5..0000000
--- a/plugins/zyklonb/script
+++ /dev/null
@@ -1,2310 +0,0 @@
-#!/usr/bin/tcc -run -lm
-//
-// ZyklonB scripting plugin, using a custom stack-based language
-//
-// Copyright 2014 Přemysl Eric Janouch
-// See the file LICENSE for licensing information.
-//
-// Just compile this file as usual (sans #!) if you don't feel like using TCC.
-// It is a very basic and portable C99 application. It's not supposed to be
-// very sophisticated, for it'd get extremely big.
-//
-// The main influences of the language were Factor and Joy, stripped of all
-// even barely complex stuff. In its current state, it's only really useful as
-// a calculator but it's got great potential for extending.
-//
-// If you don't like something, just change it; this is just an experiment.
-//
-// NOTE: it is relatively easy to abuse. Be careful.
-//
-
-#define _XOPEN_SOURCE 500
-
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-#define ADDRESS_SPACE_LIMIT (100 * 1024 * 1024)
-#include
-
-#if defined __GNUC__
-#define ATTRIBUTE_PRINTF(x, y) __attribute__ ((format (printf, x, y)))
-#else // ! __GNUC__
-#define ATTRIBUTE_PRINTF(x, y)
-#endif // ! __GNUC__
-
-#define N_ELEMENTS(a) (sizeof (a) / sizeof ((a)[0]))
-
-// --- Utilities ---------------------------------------------------------------
-
-static char *strdup_printf (const char *format, ...) ATTRIBUTE_PRINTF (1, 2);
-
-static char *
-strdup_vprintf (const char *format, va_list ap)
-{
- va_list aq;
- va_copy (aq, ap);
- int size = vsnprintf (NULL, 0, format, aq);
- va_end (aq);
- if (size < 0)
- return NULL;
-
- char buf[size + 1];
- size = vsnprintf (buf, sizeof buf, format, ap);
- if (size < 0)
- return NULL;
-
- return strdup (buf);
-}
-
-static char *
-strdup_printf (const char *format, ...)
-{
- va_list ap;
- va_start (ap, format);
- char *result = strdup_vprintf (format, ap);
- va_end (ap);
- return result;
-}
-
-// --- Generic buffer ----------------------------------------------------------
-
-struct buffer
-{
- char *s; ///< Buffer data
- size_t alloc; ///< Number of bytes allocated
- size_t len; ///< Number of bytes used
- bool memory_failure; ///< Memory allocation failed
-};
-
-#define BUFFER_INITIALIZER { NULL, 0, 0, false }
-
-static bool
-buffer_append (struct buffer *self, const void *s, size_t n)
-{
- if (self->memory_failure)
- return false;
-
- if (!self->s)
- self->s = malloc (self->alloc = 8);
- while (self->len + n > self->alloc)
- self->s = realloc (self->s, self->alloc <<= 1);
-
- if (!self->s)
- {
- self->memory_failure = true;
- return false;
- }
-
- memcpy (self->s + self->len, s, n);
- self->len += n;
- return true;
-}
-
-inline static bool
-buffer_append_c (struct buffer *self, char c)
-{
- return buffer_append (self, &c, 1);
-}
-
-// --- Data types --------------------------------------------------------------
-
-enum item_type
-{
- ITEM_STRING,
- ITEM_WORD,
- ITEM_INTEGER,
- ITEM_FLOAT,
- ITEM_LIST
-};
-
-struct item
-{
-#define ITEM_HEADER \
- enum item_type type; /**< The type of this object */ \
- struct item *next; /**< Next item on the list/stack */
-
- ITEM_HEADER
-};
-
-struct item_string
-{
- ITEM_HEADER
- size_t len; ///< Length of the string (sans '\0')
- char value[]; ///< The null-terminated string value
-};
-
-#define get_string(item) \
- (assert ((item)->type == ITEM_STRING), \
- ((struct item_string *)(item))->value)
-
-/// It looks like a string but it doesn't quack like a string
-#define item_word item_string
-
-#define get_word(item) \
- (assert ((item)->type == ITEM_WORD), \
- ((struct item_word *)(item))->value)
-
-struct item_integer
-{
- ITEM_HEADER
- long long value; ///< The integer value
-};
-
-#define get_integer(item) \
- (assert ((item)->type == ITEM_INTEGER), \
- ((struct item_integer *)(item))->value)
-
-struct item_float
-{
- ITEM_HEADER
- long double value; ///< The floating point value
-};
-
-#define get_float(item) \
- (assert ((item)->type == ITEM_FLOAT), \
- ((struct item_float *)(item))->value)
-
-struct item_list
-{
- ITEM_HEADER
- struct item *head; ///< The head of the list
-};
-
-#define get_list(item) \
- (assert ((item)->type == ITEM_LIST), \
- ((struct item_list *)(item))->head)
-
-#define set_list(item, head_) \
- (assert ((item)->type == ITEM_LIST), \
- item_free_list (((struct item_list *)(item))->head), \
- ((struct item_list *)(item))->head = (head_))
-
-const char *
-item_type_to_str (enum item_type type)
-{
- switch (type)
- {
- case ITEM_STRING: return "string";
- case ITEM_WORD: return "word";
- case ITEM_INTEGER: return "integer";
- case ITEM_FLOAT: return "float";
- case ITEM_LIST: return "list";
- }
- abort ();
-}
-
-// --- Item management ---------------------------------------------------------
-
-static void item_free_list (struct item *);
-static struct item *new_clone_list (const struct item *);
-
-static void
-item_free (struct item *item)
-{
- if (item->type == ITEM_LIST)
- item_free_list (get_list (item));
- free (item);
-}
-
-static void
-item_free_list (struct item *item)
-{
- while (item)
- {
- struct item *link = item;
- item = item->next;
- item_free (link);
- }
-}
-
-static struct item *
-new_clone (const struct item *item)
-{
- size_t size;
- switch (item->type)
- {
- case ITEM_STRING:
- case ITEM_WORD:
- {
- const struct item_string *x = (const struct item_string *) item;
- size = sizeof *x + x->len + 1;
- break;
- }
- case ITEM_INTEGER: size = sizeof (struct item_integer); break;
- case ITEM_FLOAT: size = sizeof (struct item_float); break;
- case ITEM_LIST: size = sizeof (struct item_list); break;
- }
-
- struct item *clone = malloc (size);
- if (!clone)
- return NULL;
-
- memcpy (clone, item, size);
- if (item->type == ITEM_LIST)
- {
- struct item_list *x = (struct item_list *) clone;
- if (x->head && !(x->head = new_clone_list (x->head)))
- {
- free (clone);
- return NULL;
- }
- }
- clone->next = NULL;
- return clone;
-}
-
-static struct item *
-new_clone_list (const struct item *item)
-{
- struct item *head = NULL, *clone;
- for (struct item **out = &head; item; item = item->next)
- {
- if (!(clone = *out = new_clone (item)))
- {
- item_free_list (head);
- return NULL;
- }
- clone->next = NULL;
- out = &clone->next;
- }
- return head;
-}
-
-static struct item *
-new_string (const char *s, ssize_t len)
-{
- if (len < 0)
- len = strlen (s);
-
- struct item_string *item = calloc (1, sizeof *item + len + 1);
- if (!item)
- return NULL;
-
- item->type = ITEM_STRING;
- item->len = len;
- memcpy (item->value, s, len);
- item->value[len] = '\0';
- return (struct item *) item;
-}
-
-static struct item *
-new_word (const char *s, ssize_t len)
-{
- struct item *item = new_string (s, len);
- if (!item)
- return NULL;
-
- item->type = ITEM_WORD;
- return item;
-}
-
-static struct item *
-new_integer (long long value)
-{
- struct item_integer *item = calloc (1, sizeof *item);
- if (!item)
- return NULL;
-
- item->type = ITEM_INTEGER;
- item->value = value;
- return (struct item *) item;
-}
-
-static struct item *
-new_float (long double value)
-{
- struct item_float *item = calloc (1, sizeof *item);
- if (!item)
- return NULL;
-
- item->type = ITEM_FLOAT;
- item->value = value;
- return (struct item *) item;
-}
-
-static struct item *
-new_list (struct item *head)
-{
- struct item_list *item = calloc (1, sizeof *item);
- if (!item)
- return NULL;
-
- item->type = ITEM_LIST;
- item->head = head;
- return (struct item *) item;
-}
-
-// --- Parsing -----------------------------------------------------------------
-
-#define PARSE_ERROR_TABLE(XX) \
- XX( OK, NULL ) \
- XX( EOF, "unexpected end of input" ) \
- XX( INVALID_HEXA_ESCAPE, "invalid hexadecimal escape sequence" ) \
- XX( INVALID_ESCAPE, "unrecognized escape sequence" ) \
- XX( MEMORY, "memory allocation failure" ) \
- XX( FLOAT_RANGE, "floating point value out of range" ) \
- XX( INTEGER_RANGE, "integer out of range" ) \
- XX( INVALID_INPUT, "invalid input" ) \
- XX( UNEXPECTED_INPUT, "unexpected input" )
-
-enum tokenizer_error
-{
-#define XX(x, y) PARSE_ERROR_ ## x,
- PARSE_ERROR_TABLE (XX)
-#undef XX
- PARSE_ERROR_COUNT
-};
-
-struct tokenizer
-{
- const char *cursor;
- enum tokenizer_error error;
-};
-
-static bool
-decode_hexa_escape (struct tokenizer *self, struct buffer *buf)
-{
- int i;
- char c, code = 0;
-
- for (i = 0; i < 2; i++)
- {
- c = tolower (*self->cursor);
- if (c >= '0' && c <= '9')
- code = (code << 4) | (c - '0');
- else if (c >= 'a' && c <= 'f')
- code = (code << 4) | (c - 'a' + 10);
- else
- break;
-
- self->cursor++;
- }
-
- if (!i)
- return false;
-
- buffer_append_c (buf, code);
- return true;
-}
-
-static bool
-decode_octal_escape (struct tokenizer *self, struct buffer *buf)
-{
- int i;
- char c, code = 0;
-
- for (i = 0; i < 3; i++)
- {
- c = *self->cursor;
- if (c < '0' || c > '7')
- break;
-
- code = (code << 3) | (c - '0');
- self->cursor++;
- }
-
- if (!i)
- return false;
-
- buffer_append_c (buf, code);
- return true;
-}
-
-static bool
-decode_escape_sequence (struct tokenizer *self, struct buffer *buf)
-{
- // Support some basic escape sequences from the C language
- char c;
- switch ((c = *self->cursor))
- {
- case '\0':
- self->error = PARSE_ERROR_EOF;
- return false;
- case 'x':
- case 'X':
- self->cursor++;
- if (decode_hexa_escape (self, buf))
- return true;
-
- self->error = PARSE_ERROR_INVALID_HEXA_ESCAPE;
- return false;
- default:
- if (decode_octal_escape (self, buf))
- return true;
-
- self->cursor++;
- const char *from = "abfnrtv\"\\", *to = "\a\b\f\n\r\t\v\"\\", *x;
- if ((x = strchr (from, c)))
- {
- buffer_append_c (buf, to[x - from]);
- return true;
- }
-
- self->error = PARSE_ERROR_INVALID_ESCAPE;
- return false;
- }
-}
-
-static struct item *
-parse_string (struct tokenizer *self)
-{
- struct buffer buf = BUFFER_INITIALIZER;
- struct item *item = NULL;
- char c;
-
- while (true)
- switch ((c = *self->cursor++))
- {
- case '\0':
- self->cursor--;
- self->error = PARSE_ERROR_EOF;
- goto end;
- case '"':
- if (buf.memory_failure
- || !(item = new_string (buf.s, buf.len)))
- self->error = PARSE_ERROR_MEMORY;
- goto end;
- case '\\':
- if (decode_escape_sequence (self, &buf))
- break;
- goto end;
- default:
- buffer_append_c (&buf, c);
- }
-
-end:
- free (buf.s);
- return item;
-}
-
-static struct item *
-try_parse_number (struct tokenizer *self)
-{
- // These two standard library functions can digest a lot of various inputs,
- // including NaN and +/- infinity. That may get a bit confusing.
- char *float_end;
- errno = 0;
- long double float_value = strtold (self->cursor, &float_end);
- int float_errno = errno;
-
- char *int_end;
- errno = 0;
- long long int_value = strtoll (self->cursor, &int_end, 10);
- int int_errno = errno;
-
- // If they both fail, then this is most probably not a number.
- if (float_end == int_end && float_end == self->cursor)
- return NULL;
-
- // Only use the floating point result if it parses more characters:
- struct item *item;
- if (float_end > int_end)
- {
- if (float_errno == ERANGE)
- {
- self->error = PARSE_ERROR_FLOAT_RANGE;
- return NULL;
- }
- self->cursor = float_end;
- if (!(item = new_float (float_value)))
- self->error = PARSE_ERROR_MEMORY;
- return item;
- }
- else
- {
- if (int_errno == ERANGE)
- {
- self->error = PARSE_ERROR_INTEGER_RANGE;
- return NULL;
- }
- self->cursor = int_end;
- if (!(item = new_integer (int_value)))
- self->error = PARSE_ERROR_MEMORY;
- return item;
- }
-}
-
-static struct item *
-parse_word (struct tokenizer *self)
-{
- struct buffer buf = BUFFER_INITIALIZER;
- struct item *item = NULL;
- char c;
-
- // Here we accept almost anything that doesn't break the grammar
- while (!strchr (" []\"", (c = *self->cursor++)) && (unsigned char) c > ' ')
- buffer_append_c (&buf, c);
- self->cursor--;
-
- if (buf.memory_failure)
- self->error = PARSE_ERROR_MEMORY;
- else if (!buf.len)
- self->error = PARSE_ERROR_INVALID_INPUT;
- else if (!(item = new_word (buf.s, buf.len)))
- self->error = PARSE_ERROR_MEMORY;
-
- free (buf.s);
- return item;
-}
-
-static struct item *parse_item_list (struct tokenizer *);
-
-static struct item *
-parse_list (struct tokenizer *self)
-{
- struct item *list = parse_item_list (self);
- if (self->error)
- {
- assert (list == NULL);
- return NULL;
- }
- if (!*self->cursor)
- {
- self->error = PARSE_ERROR_EOF;
- item_free_list (list);
- return NULL;
- }
- assert (*self->cursor == ']');
- self->cursor++;
- return new_list (list);
-}
-
-static struct item *
-parse_item (struct tokenizer *self)
-{
- char c;
- switch ((c = *self->cursor++))
- {
- case '[': return parse_list (self);
- case '"': return parse_string (self);
- default:;
- }
-
- self->cursor--;
- struct item *item = try_parse_number (self);
- if (!item && !self->error)
- item = parse_word (self);
- return item;
-}
-
-static struct item *
-parse_item_list (struct tokenizer *self)
-{
- struct item *head = NULL;
- struct item **tail = &head;
-
- char c;
- bool expected = true;
- while ((c = *self->cursor) && c != ']')
- {
- if (isspace (c))
- {
- self->cursor++;
- expected = true;
- continue;
- }
- else if (!expected)
- {
- self->error = PARSE_ERROR_UNEXPECTED_INPUT;
- goto fail;
- }
-
- if (!(*tail = parse_item (self)))
- goto fail;
- tail = &(*tail)->next;
- expected = false;
- }
- return head;
-
-fail:
- item_free_list (head);
- return NULL;
-}
-
-static struct item *
-parse (const char *s, const char **error)
-{
- struct tokenizer self = { .cursor = s, .error = PARSE_ERROR_OK };
- struct item *list = parse_item_list (&self);
- if (!self.error && *self.cursor != '\0')
- {
- self.error = PARSE_ERROR_UNEXPECTED_INPUT;
- item_free_list (list);
- list = NULL;
- }
-
-#define XX(x, y) y,
- static const char *strings[PARSE_ERROR_COUNT] =
- { PARSE_ERROR_TABLE (XX) };
-#undef XX
-
- static char error_buf[128];
- if (self.error && error)
- {
- snprintf (error_buf, sizeof error_buf, "at character %d: %s",
- (int) (self.cursor - s) + 1, strings[self.error]);
- *error = error_buf;
- }
- return list;
-}
-
-// --- Runtime -----------------------------------------------------------------
-
-// TODO: try to think of a _simple_ way to do preemptive multitasking
-
-struct context
-{
- struct item *stack; ///< The current top of the stack
- size_t stack_size; ///< Number of items on the stack
-
- size_t reduction_count; ///< # of function calls so far
- size_t reduction_limit; ///< The hard limit on function calls
-
- char *error; ///< Error information
- bool error_is_fatal; ///< Whether the error can be catched
- bool memory_failure; ///< Memory allocation failure
-
- void *user_data; ///< User data
-};
-
-/// Internal handler for a function
-typedef bool (*handler_fn) (struct context *);
-
-struct fn
-{
- struct fn *next; ///< The next link in the chain
-
- handler_fn handler; ///< Internal C handler, or NULL
- struct item *script; ///< Alternatively runtime code
- char name[]; ///< The name of the function
-};
-
-struct fn *g_functions; ///< Maps words to functions
-
-static void
-context_init (struct context *ctx)
-{
- ctx->stack = NULL;
- ctx->stack_size = 0;
-
- ctx->reduction_count = 0;
- ctx->reduction_limit = 2000;
-
- ctx->error = NULL;
- ctx->error_is_fatal = false;
- ctx->memory_failure = false;
-
- ctx->user_data = NULL;
-}
-
-static void
-context_free (struct context *ctx)
-{
- item_free_list (ctx->stack);
- ctx->stack = NULL;
-
- free (ctx->error);
- ctx->error = NULL;
-}
-
-static bool
-set_error (struct context *ctx, const char *format, ...)
-{
- free (ctx->error);
-
- va_list ap;
- va_start (ap, format);
- ctx->error = strdup_vprintf (format, ap);
- va_end (ap);
-
- if (!ctx->error)
- ctx->memory_failure = true;
- return false;
-}
-
-static bool
-push (struct context *ctx, struct item *item)
-{
- // The `item' is typically a result from new_(), thus when it is null,
- // that function must have failed. This is a shortcut for convenience.
- if (!item)
- {
- ctx->memory_failure = true;
- return false;
- }
-
- assert (item->next == NULL);
- item->next = ctx->stack;
- ctx->stack = item;
- ctx->stack_size++;
- return true;
-}
-
-static bool
-bump_reductions (struct context *ctx)
-{
- if (++ctx->reduction_count >= ctx->reduction_limit)
- {
- ctx->error_is_fatal = true;
- return set_error (ctx, "reduction limit reached");
- }
- return true;
-}
-
-static bool execute (struct context *, struct item *);
-
-static bool
-call_function (struct context *ctx, const char *name)
-{
- struct fn *iter;
- for (iter = g_functions; iter; iter = iter->next)
- if (!strcmp (name, iter->name))
- goto found;
- return set_error (ctx, "unknown function: %s", name);
-
-found:
- if (!bump_reductions (ctx))
- return false;
-
- if (iter->handler
- ? iter->handler (ctx)
- : execute (ctx, iter->script))
- return true;
-
- // In this case, `error' is NULL
- if (ctx->memory_failure)
- return false;
-
- // This creates some form of a stack trace
- char *tmp = ctx->error;
- ctx->error = NULL;
- set_error (ctx, "%s -> %s", name, tmp);
- free (tmp);
- return false;
-}
-
-static void
-free_function (struct fn *fn)
-{
- item_free_list (fn->script);
- free (fn);
-}
-
-static void
-unregister_function (const char *name)
-{
- for (struct fn **iter = &g_functions; *iter; iter = &(*iter)->next)
- if (!strcmp ((*iter)->name, name))
- {
- struct fn *tmp = *iter;
- *iter = tmp->next;
- free_function (tmp);
- break;
- }
-}
-
-static struct fn *
-prepend_new_fn (const char *name)
-{
- struct fn *fn = calloc (1, sizeof *fn + strlen (name) + 1);
- if (!fn)
- return NULL;
-
- strcpy (fn->name, name);
- fn->next = g_functions;
- return g_functions = fn;
-}
-
-static bool
-register_handler (const char *name, handler_fn handler)
-{
- unregister_function (name);
- struct fn *fn = prepend_new_fn (name);
- if (!fn)
- return false;
- fn->handler = handler;
- return true;
-}
-
-static bool
-register_script (const char *name, struct item *script)
-{
- unregister_function (name);
- struct fn *fn = prepend_new_fn (name);
- if (!fn)
- return false;
- fn->script = script;
- return true;
-}
-
-static bool
-execute (struct context *ctx, struct item *script)
-{
- for (; script; script = script->next)
- {
- if (script->type != ITEM_WORD)
- {
- if (!bump_reductions (ctx)
- || !push (ctx, new_clone (script)))
- return false;
- }
- else if (!call_function (ctx, get_word (script)))
- return false;
- }
- return true;
-}
-
-// --- Runtime library ---------------------------------------------------------
-
-#define defn(name) static bool name (struct context *ctx)
-
-#define check_stack(n) \
- if (ctx->stack_size < n) { \
- set_error (ctx, "stack underflow"); \
- return 0; \
- }
-
-inline static bool
-check_stack_safe (struct context *ctx, size_t n)
-{
- check_stack (n);
- return true;
-}
-
-static bool
-check_type (struct context *ctx, const void *item_, enum item_type type)
-{
- const struct item *item = item_;
- if (item->type == type)
- return true;
-
- return set_error (ctx, "invalid type: expected `%s', got `%s'",
- item_type_to_str (type), item_type_to_str (item->type));
-}
-
-static struct item *
-pop (struct context *ctx)
-{
- check_stack (1);
- struct item *top = ctx->stack;
- ctx->stack = top->next;
- top->next = NULL;
- ctx->stack_size--;
- return top;
-}
-
-// - - Types - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-#define defn_is_type(name, item_type) \
- defn (fn_is_##name) { \
- check_stack (1); \
- struct item *top = pop (ctx); \
- push (ctx, new_integer (top->type == (item_type))); \
- item_free (top); \
- return true; \
- }
-
-defn_is_type (string, ITEM_STRING)
-defn_is_type (word, ITEM_WORD)
-defn_is_type (integer, ITEM_INTEGER)
-defn_is_type (float, ITEM_FLOAT)
-defn_is_type (list, ITEM_LIST)
-
-defn (fn_to_string)
-{
- check_stack (1);
- struct item *item = pop (ctx);
- char *value;
-
- switch (item->type)
- {
- case ITEM_WORD:
- item->type = ITEM_STRING;
- case ITEM_STRING:
- return push (ctx, item);
-
- case ITEM_FLOAT:
- value = strdup_printf ("%Lf", get_float (item));
- break;
- case ITEM_INTEGER:
- value = strdup_printf ("%lld", get_integer (item));
- break;
-
- default:
- set_error (ctx, "cannot convert `%s' to `%s'",
- item_type_to_str (item->type), item_type_to_str (ITEM_STRING));
- item_free (item);
- return false;
- }
-
- item_free (item);
- if (!value)
- {
- ctx->memory_failure = true;
- return false;
- }
-
- item = new_string (value, -1);
- free (value);
- return push (ctx, item);
-}
-
-defn (fn_to_integer)
-{
- check_stack (1);
- struct item *item = pop (ctx);
- long long value;
-
- switch (item->type)
- {
- case ITEM_INTEGER:
- return push (ctx, item);
- case ITEM_FLOAT:
- value = get_float (item);
- break;
-
- case ITEM_STRING:
- {
- char *end;
- const char *s = get_string (item);
- value = strtoll (s, &end, 10);
- if (end != s && *s == '\0')
- break;
-
- item_free (item);
- return set_error (ctx, "integer conversion error");
- }
-
- default:
- set_error (ctx, "cannot convert `%s' to `%s'",
- item_type_to_str (item->type), item_type_to_str (ITEM_INTEGER));
- item_free (item);
- return false;
- }
-
- item_free (item);
- return push (ctx, new_integer (value));
-}
-
-defn (fn_to_float)
-{
- check_stack (1);
- struct item *item = pop (ctx);
- long double value;
-
- switch (item->type)
- {
- case ITEM_FLOAT:
- return push (ctx, item);
- case ITEM_INTEGER:
- value = get_integer (item);
- break;
-
- case ITEM_STRING:
- {
- char *end;
- const char *s = get_string (item);
- value = strtold (s, &end);
- if (end != s && *s == '\0')
- break;
-
- item_free (item);
- return set_error (ctx, "float conversion error");
- }
-
- default:
- set_error (ctx, "cannot convert `%s' to `%s'",
- item_type_to_str (item->type), item_type_to_str (ITEM_FLOAT));
- item_free (item);
- return false;
- }
-
- item_free (item);
- return push (ctx, new_float (value));
-}
-
-// - - Miscellaneous - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-defn (fn_length)
-{
- check_stack (1);
- struct item *item = pop (ctx);
- bool success = true;
- switch (item->type)
- {
- case ITEM_STRING:
- success = push (ctx, new_integer (((struct item_string *) item)->len));
- break;
- case ITEM_LIST:
- {
- long long length = 0;
- struct item *iter;
- for (iter = get_list (item); iter; iter = iter->next)
- length++;
- success = push (ctx, new_integer (length));
- break;
- }
- default:
- success = set_error (ctx, "invalid type");
- }
- item_free (item);
- return success;
-}
-
-// - - Stack operations - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-defn (fn_dup)
-{
- check_stack (1);
- return push (ctx, new_clone (ctx->stack));
-}
-
-defn (fn_drop)
-{
- check_stack (1);
- item_free (pop (ctx));
- return true;
-}
-
-defn (fn_swap)
-{
- check_stack (2);
- struct item *second = pop (ctx), *first = pop (ctx);
- return push (ctx, second) && push (ctx, first);
-}
-
-defn (fn_call)
-{
- check_stack (1);
- struct item *script = pop (ctx);
- bool success = check_type (ctx, script, ITEM_LIST)
- && execute (ctx, get_list (script));
- item_free (script);
- return success;
-}
-
-defn (fn_dip)
-{
- check_stack (2);
- struct item *script = pop (ctx);
- struct item *item = pop (ctx);
- bool success = check_type (ctx, script, ITEM_LIST)
- && execute (ctx, get_list (script));
- item_free (script);
- if (!success)
- {
- item_free (item);
- return false;
- }
- return push (ctx, item);
-}
-
-defn (fn_unit)
-{
- check_stack (1);
- struct item *item = pop (ctx);
- return push (ctx, new_list (item));
-}
-
-defn (fn_cons)
-{
- check_stack (2);
- struct item *list = pop (ctx);
- struct item *item = pop (ctx);
- if (!check_type (ctx, list, ITEM_LIST))
- {
- item_free (list);
- item_free (item);
- return false;
- }
- item->next = get_list (list);
- ((struct item_list *) list)->head = item;
- return push (ctx, list);
-}
-
-defn (fn_cat)
-{
- check_stack (2);
- struct item *scnd = pop (ctx);
- struct item *frst = pop (ctx);
- if (!check_type (ctx, frst, ITEM_LIST)
- || !check_type (ctx, scnd, ITEM_LIST))
- {
- item_free (frst);
- item_free (scnd);
- return false;
- }
-
- // XXX: we shouldn't have to do this in O(n)
- struct item **tail = &((struct item_list *) frst)->head;
- while (*tail)
- tail = &(*tail)->next;
- *tail = get_list (scnd);
-
- ((struct item_list *) scnd)->head = NULL;
- item_free (scnd);
- return push (ctx, frst);
-}
-
-defn (fn_uncons)
-{
- check_stack (1);
- struct item *list = pop (ctx);
- if (!check_type (ctx, list, ITEM_LIST))
- goto fail;
- struct item *first = get_list (list);
- if (!first)
- {
- set_error (ctx, "list is empty");
- goto fail;
- }
- ((struct item_list *) list)->head = first->next;
- first->next = NULL;
- return push (ctx, first) && push (ctx, list);
-fail:
- item_free (list);
- return false;
-}
-
-// - - Logical - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-static bool
-to_boolean (struct context *ctx, struct item *item, bool *ok)
-{
- switch (item->type)
- {
- case ITEM_STRING:
- return *get_string (item) != '\0';
- case ITEM_INTEGER:
- return get_integer (item) != 0;
- case ITEM_FLOAT:
- return get_float (item) != 0.;
- default:
- return (*ok = set_error (ctx, "cannot convert `%s' to boolean",
- item_type_to_str (item->type)));
- }
-}
-
-defn (fn_not)
-{
- check_stack (1);
- struct item *item = pop (ctx);
- bool ok = true;
- bool result = !to_boolean (ctx, item, &ok);
- item_free (item);
- return ok && push (ctx, new_integer (result));
-}
-
-defn (fn_and)
-{
- check_stack (2);
- struct item *op1 = pop (ctx);
- struct item *op2 = pop (ctx);
- bool ok = true;
- bool result = to_boolean (ctx, op1, &ok) && to_boolean (ctx, op2, &ok);
- item_free (op1);
- item_free (op2);
- return ok && push (ctx, new_integer (result));
-}
-
-defn (fn_or)
-{
- check_stack (2);
- struct item *op1 = pop (ctx);
- struct item *op2 = pop (ctx);
- bool ok = true;
- bool result = to_boolean (ctx, op1, &ok)
- || !ok || to_boolean (ctx, op2, &ok);
- item_free (op1);
- item_free (op2);
- return ok && push (ctx, new_integer (result));
-}
-
-// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-defn (fn_if)
-{
- check_stack (3);
- struct item *else_ = pop (ctx);
- struct item *then_ = pop (ctx);
- struct item *cond_ = pop (ctx);
-
- bool ok = true;
- bool condition = to_boolean (ctx, cond_, &ok);
- item_free (cond_);
-
- bool success = false;
- if (ok
- && check_type (ctx, then_, ITEM_LIST)
- && check_type (ctx, else_, ITEM_LIST))
- success = execute (ctx, condition
- ? get_list (then_)
- : get_list (else_));
-
- item_free (then_);
- item_free (else_);
- return success;
-}
-
-defn (fn_try)
-{
- check_stack (2);
- struct item *catch = pop (ctx);
- struct item *try = pop (ctx);
- bool success = false;
- if (!check_type (ctx, try, ITEM_LIST)
- || !check_type (ctx, catch, ITEM_LIST))
- goto fail;
-
- if (!execute (ctx, get_list (try)))
- {
- if (ctx->memory_failure || ctx->error_is_fatal)
- goto fail;
-
- success = push (ctx, new_string (ctx->error, -1));
- free (ctx->error);
- ctx->error = NULL;
-
- if (success)
- success = execute (ctx, get_list (catch));
- }
-
-fail:
- item_free (try);
- item_free (catch);
- return success;
-}
-
-defn (fn_map)
-{
- check_stack (2);
- struct item *fn = pop (ctx);
- struct item *list = pop (ctx);
- if (!check_type (ctx, fn, ITEM_LIST)
- || !check_type (ctx, list, ITEM_LIST))
- {
- item_free (fn);
- item_free (list);
- return false;
- }
-
- bool success = false;
- struct item *result = NULL, **tail = &result;
- for (struct item *iter = get_list (list); iter; iter = iter->next)
- {
- if (!push (ctx, new_clone (iter))
- || !execute (ctx, get_list (fn))
- || !check_stack_safe (ctx, 1))
- goto fail;
-
- struct item *item = pop (ctx);
- *tail = item;
- tail = &item->next;
- }
- success = true;
-
-fail:
- set_list (list, result);
- item_free (fn);
- if (!success)
- {
- item_free (list);
- return false;
- }
- return push (ctx, list);
-}
-
-defn (fn_filter)
-{
- check_stack (2);
- struct item *fn = pop (ctx);
- struct item *list = pop (ctx);
- if (!check_type (ctx, fn, ITEM_LIST)
- || !check_type (ctx, list, ITEM_LIST))
- {
- item_free (fn);
- item_free (list);
- return false;
- }
-
- bool success = false;
- bool ok = true;
- struct item *result = NULL, **tail = &result;
- for (struct item *iter = get_list (list); iter; iter = iter->next)
- {
- if (!push (ctx, new_clone (iter))
- || !execute (ctx, get_list (fn))
- || !check_stack_safe (ctx, 1))
- goto fail;
-
- struct item *item = pop (ctx);
- bool survived = to_boolean (ctx, item, &ok);
- item_free (item);
- if (!ok)
- goto fail;
- if (!survived)
- continue;
-
- if (!(item = new_clone (iter)))
- goto fail;
- *tail = item;
- tail = &item->next;
- }
- success = true;
-
-fail:
- set_list (list, result);
- item_free (fn);
- if (!success)
- {
- item_free (list);
- return false;
- }
- return push (ctx, list);
-}
-
-defn (fn_fold)
-{
- check_stack (3);
- struct item *op = pop (ctx);
- struct item *null = pop (ctx);
- struct item *list = pop (ctx);
- bool success = false;
- if (!check_type (ctx, op, ITEM_LIST)
- || !check_type (ctx, list, ITEM_LIST))
- {
- item_free (null);
- goto fail;
- }
-
- push (ctx, null);
- for (struct item *iter = get_list (list); iter; iter = iter->next)
- if (!push (ctx, new_clone (iter))
- || !execute (ctx, get_list (op)))
- goto fail;
- success = true;
-
-fail:
- item_free (op);
- item_free (list);
- return success;
-}
-
-defn (fn_each)
-{
- check_stack (2);
- struct item *op = pop (ctx);
- struct item *list = pop (ctx);
- bool success = false;
- if (!check_type (ctx, op, ITEM_LIST)
- || !check_type (ctx, list, ITEM_LIST))
- goto fail;
-
- for (struct item *iter = get_list (list); iter; iter = iter->next)
- if (!push (ctx, new_clone (iter))
- || !execute (ctx, get_list (op)))
- goto fail;
- success = true;
-
-fail:
- item_free (op);
- item_free (list);
- return success;
-}
-
-// - - Arithmetic - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-// XXX: why not a `struct item_string *` argument?
-static bool
-push_repeated_string (struct context *ctx, struct item *op1, struct item *op2)
-{
- struct item_string *string = (struct item_string *) op1;
- struct item_integer *repeat = (struct item_integer *) op2;
- assert (string->type == ITEM_STRING);
- assert (repeat->type == ITEM_INTEGER);
-
- if (repeat->value < 0)
- return set_error (ctx, "cannot multiply a string by a negative value");
-
- char *buf = NULL;
- size_t len = string->len * repeat->value;
- if (len < string->len && repeat->value != 0)
- goto allocation_fail;
-
- buf = malloc (len);
- if (!buf)
- goto allocation_fail;
-
- for (size_t i = 0; i < len; i += string->len)
- memcpy (buf + i, string->value, string->len);
- struct item *item = new_string (buf, len);
- free (buf);
- return push (ctx, item);
-
-allocation_fail:
- ctx->memory_failure = true;
- return false;
-}
-
-defn (fn_times)
-{
- check_stack (2);
- struct item *op2 = pop (ctx);
- struct item *op1 = pop (ctx);
-
- bool ok;
- if (op1->type == ITEM_INTEGER && op2->type == ITEM_INTEGER)
- ok = push (ctx, new_integer (get_integer (op1) * get_integer (op2)));
- else if (op1->type == ITEM_INTEGER && op2->type == ITEM_FLOAT)
- ok = push (ctx, new_float (get_integer (op1) * get_float (op2)));
- else if (op1->type == ITEM_FLOAT && op2->type == ITEM_FLOAT)
- ok = push (ctx, new_float (get_float (op1) * get_float (op2)));
- else if (op1->type == ITEM_FLOAT && op2->type == ITEM_INTEGER)
- ok = push (ctx, new_float (get_float (op1) * get_integer (op2)));
- else if (op1->type == ITEM_INTEGER && op2->type == ITEM_STRING)
- ok = push_repeated_string (ctx, op2, op1);
- else if (op1->type == ITEM_STRING && op2->type == ITEM_INTEGER)
- ok = push_repeated_string (ctx, op1, op2);
- else
- ok = set_error (ctx, "cannot multiply `%s' and `%s'",
- item_type_to_str (op1->type), item_type_to_str (op2->type));
-
- item_free (op1);
- item_free (op2);
- return ok;
-}
-
-defn (fn_pow)
-{
- check_stack (2);
- struct item *op2 = pop (ctx);
- struct item *op1 = pop (ctx);
-
- bool ok;
- if (op1->type == ITEM_INTEGER && op2->type == ITEM_INTEGER)
- // TODO: implement this properly, outputting an integer
- ok = push (ctx, new_float (powl (get_integer (op1), get_integer (op2))));
- else if (op1->type == ITEM_INTEGER && op2->type == ITEM_FLOAT)
- ok = push (ctx, new_float (powl (get_integer (op1), get_float (op2))));
- else if (op1->type == ITEM_FLOAT && op2->type == ITEM_FLOAT)
- ok = push (ctx, new_float (powl (get_float (op1), get_float (op2))));
- else if (op1->type == ITEM_FLOAT && op2->type == ITEM_INTEGER)
- ok = push (ctx, new_float (powl (get_float (op1), get_integer (op2))));
- else
- ok = set_error (ctx, "cannot exponentiate `%s' and `%s'",
- item_type_to_str (op1->type), item_type_to_str (op2->type));
-
- item_free (op1);
- item_free (op2);
- return ok;
-}
-
-defn (fn_div)
-{
- check_stack (2);
- struct item *op2 = pop (ctx);
- struct item *op1 = pop (ctx);
-
- bool ok;
- if (op1->type == ITEM_INTEGER && op2->type == ITEM_INTEGER)
- {
- if (get_integer (op2) == 0)
- ok = set_error (ctx, "division by zero");
- else
- ok = push (ctx, new_integer (get_integer (op1) / get_integer (op2)));
- }
- else if (op1->type == ITEM_INTEGER && op2->type == ITEM_FLOAT)
- ok = push (ctx, new_float (get_integer (op1) / get_float (op2)));
- else if (op1->type == ITEM_FLOAT && op2->type == ITEM_FLOAT)
- ok = push (ctx, new_float (get_float (op1) / get_float (op2)));
- else if (op1->type == ITEM_FLOAT && op2->type == ITEM_INTEGER)
- ok = push (ctx, new_float (get_float (op1) / get_integer (op2)));
- else
- ok = set_error (ctx, "cannot divide `%s' and `%s'",
- item_type_to_str (op1->type), item_type_to_str (op2->type));
-
- item_free (op1);
- item_free (op2);
- return ok;
-}
-
-defn (fn_mod)
-{
- check_stack (2);
- struct item *op2 = pop (ctx);
- struct item *op1 = pop (ctx);
-
- bool ok;
- if (op1->type == ITEM_INTEGER && op2->type == ITEM_INTEGER)
- {
- if (get_integer (op2) == 0)
- ok = set_error (ctx, "division by zero");
- else
- ok = push (ctx, new_integer (get_integer (op1) % get_integer (op2)));
- }
- else if (op1->type == ITEM_INTEGER && op2->type == ITEM_FLOAT)
- ok = push (ctx, new_float (fmodl (get_integer (op1), get_float (op2))));
- else if (op1->type == ITEM_FLOAT && op2->type == ITEM_FLOAT)
- ok = push (ctx, new_float (fmodl (get_float (op1), get_float (op2))));
- else if (op1->type == ITEM_FLOAT && op2->type == ITEM_INTEGER)
- ok = push (ctx, new_float (fmodl (get_float (op1), get_integer (op2))));
- else
- ok = set_error (ctx, "cannot divide `%s' and `%s'",
- item_type_to_str (op1->type), item_type_to_str (op2->type));
-
- item_free (op1);
- item_free (op2);
- return ok;
-}
-
-static bool
-push_concatenated_string (struct context *ctx,
- struct item *op1, struct item *op2)
-{
- struct item_string *s1 = (struct item_string *) op1;
- struct item_string *s2 = (struct item_string *) op2;
- assert (s1->type == ITEM_STRING);
- assert (s2->type == ITEM_STRING);
-
- char *buf = NULL;
- size_t len = s1->len + s2->len;
- if (len < s1->len || len < s2->len)
- goto allocation_fail;
-
- buf = malloc (len);
- if (!buf)
- goto allocation_fail;
-
- memcpy (buf, s1->value, s1->len);
- memcpy (buf + s1->len, s2->value, s2->len);
- struct item *item = new_string (buf, len);
- free (buf);
- return push (ctx, item);
-
-allocation_fail:
- ctx->memory_failure = true;
- return false;
-
-}
-
-defn (fn_plus)
-{
- check_stack (2);
- struct item *op2 = pop (ctx);
- struct item *op1 = pop (ctx);
-
- bool ok;
- if (op1->type == ITEM_INTEGER && op2->type == ITEM_INTEGER)
- ok = push (ctx, new_integer (get_integer (op1) + get_integer (op2)));
- else if (op1->type == ITEM_INTEGER && op2->type == ITEM_FLOAT)
- ok = push (ctx, new_float (get_integer (op1) + get_float (op2)));
- else if (op1->type == ITEM_FLOAT && op2->type == ITEM_FLOAT)
- ok = push (ctx, new_float (get_float (op1) + get_float (op2)));
- else if (op1->type == ITEM_FLOAT && op2->type == ITEM_INTEGER)
- ok = push (ctx, new_float (get_float (op1) + get_integer (op2)));
- else if (op1->type == ITEM_STRING && op2->type == ITEM_STRING)
- ok = push_concatenated_string (ctx, op1, op2);
- else
- ok = set_error (ctx, "cannot add `%s' and `%s'",
- item_type_to_str (op1->type), item_type_to_str (op2->type));
-
- item_free (op1);
- item_free (op2);
- return ok;
-}
-
-defn (fn_minus)
-{
- check_stack (2);
- struct item *op2 = pop (ctx);
- struct item *op1 = pop (ctx);
-
- bool ok;
- if (op1->type == ITEM_INTEGER && op2->type == ITEM_INTEGER)
- ok = push (ctx, new_integer (get_integer (op1) - get_integer (op2)));
- else if (op1->type == ITEM_INTEGER && op2->type == ITEM_FLOAT)
- ok = push (ctx, new_float (get_integer (op1) - get_float (op2)));
- else if (op1->type == ITEM_FLOAT && op2->type == ITEM_FLOAT)
- ok = push (ctx, new_float (get_float (op1) - get_float (op2)));
- else if (op1->type == ITEM_FLOAT && op2->type == ITEM_INTEGER)
- ok = push (ctx, new_float (get_float (op1) - get_integer (op2)));
- else
- ok = set_error (ctx, "cannot subtract `%s' and `%s'",
- item_type_to_str (op1->type), item_type_to_str (op2->type));
-
- item_free (op1);
- item_free (op2);
- return ok;
-}
-
-// - - Comparison - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-static int
-compare_strings (struct item_string *s1, struct item_string *s2)
-{
- // XXX: not entirely correct wrt. null bytes
- size_t len = (s1->len < s2->len ? s1->len : s2->len) + 1;
- return memcmp (s1->value, s2->value, len);
-}
-
-static bool compare_lists (struct item *, struct item *);
-
-static bool
-compare_list_items (struct item *op1, struct item *op2)
-{
- if (op1->type != op2->type)
- return false;
-
- switch (op1->type)
- {
- case ITEM_STRING:
- case ITEM_WORD:
- return !compare_strings ((struct item_string *) op1,
- (struct item_string *) op2);
- case ITEM_FLOAT:
- return get_float (op1) == get_float (op2);
- case ITEM_INTEGER:
- return get_integer (op1) == get_integer (op2);
- case ITEM_LIST:
- return compare_lists (get_list (op1), get_list (op2));
- }
- abort ();
-}
-
-static bool
-compare_lists (struct item *op1, struct item *op2)
-{
- while (op1 && op2)
- {
- if (!compare_list_items (op1, op2))
- return false;
-
- op1 = op1->next;
- op2 = op2->next;
- }
- return !op1 && !op2;
-}
-
-defn (fn_eq)
-{
- check_stack (2);
- struct item *op2 = pop (ctx);
- struct item *op1 = pop (ctx);
-
- bool ok;
- if (op1->type == ITEM_INTEGER && op2->type == ITEM_INTEGER)
- ok = push (ctx, new_integer (get_integer (op1) == get_integer (op2)));
- else if (op1->type == ITEM_INTEGER && op2->type == ITEM_FLOAT)
- ok = push (ctx, new_integer (get_integer (op1) == get_float (op2)));
- else if (op1->type == ITEM_FLOAT && op2->type == ITEM_FLOAT)
- ok = push (ctx, new_integer (get_float (op1) == get_float (op2)));
- else if (op1->type == ITEM_FLOAT && op2->type == ITEM_INTEGER)
- ok = push (ctx, new_integer (get_float (op1) == get_integer (op2)));
- else if (op1->type == ITEM_LIST && op2->type == ITEM_LIST)
- ok = push (ctx, new_integer (compare_lists
- (get_list (op1), get_list (op2))));
- else if (op1->type == ITEM_STRING && op2->type == ITEM_STRING)
- ok = push (ctx, new_integer (compare_strings
- ((struct item_string *)(op1), (struct item_string *)(op2)) == 0));
- else
- ok = set_error (ctx, "cannot compare `%s' and `%s'",
- item_type_to_str (op1->type), item_type_to_str (op2->type));
-
- item_free (op1);
- item_free (op2);
- return ok;
-}
-
-defn (fn_lt)
-{
- check_stack (2);
- struct item *op2 = pop (ctx);
- struct item *op1 = pop (ctx);
-
- bool ok;
- if (op1->type == ITEM_INTEGER && op2->type == ITEM_INTEGER)
- ok = push (ctx, new_integer (get_integer (op1) < get_integer (op2)));
- else if (op1->type == ITEM_INTEGER && op2->type == ITEM_FLOAT)
- ok = push (ctx, new_integer (get_integer (op1) < get_float (op2)));
- else if (op1->type == ITEM_FLOAT && op2->type == ITEM_FLOAT)
- ok = push (ctx, new_integer (get_float (op1) < get_float (op2)));
- else if (op1->type == ITEM_FLOAT && op2->type == ITEM_INTEGER)
- ok = push (ctx, new_integer (get_float (op1) < get_integer (op2)));
- else if (op1->type == ITEM_STRING && op2->type == ITEM_STRING)
- ok = push (ctx, new_integer (compare_strings
- ((struct item_string *)(op1), (struct item_string *)(op2)) < 0));
- else
- ok = set_error (ctx, "cannot compare `%s' and `%s'",
- item_type_to_str (op1->type), item_type_to_str (op2->type));
-
- item_free (op1);
- item_free (op2);
- return ok;
-}
-
-// - - Utilities - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-defn (fn_rand)
-{
- return push (ctx, new_float ((long double) rand ()
- / ((long double) RAND_MAX + 1)));
-}
-
-defn (fn_time)
-{
- return push (ctx, new_integer (time (NULL)));
-}
-
-// XXX: this is a bit too constrained; combines strftime() with gmtime()
-defn (fn_strftime)
-{
- check_stack (2);
- struct item *format = pop (ctx);
- struct item *time_ = pop (ctx);
- bool success = false;
- if (!check_type (ctx, time_, ITEM_INTEGER)
- || !check_type (ctx, format, ITEM_STRING))
- goto fail;
-
- if (get_integer (time_) < 0)
- {
- set_error (ctx, "invalid time value");
- goto fail;
- }
-
- char buf[128];
- time_t time__ = get_integer (time_);
- struct tm tm;
- gmtime_r (&time__, &tm);
- buf[strftime (buf, sizeof buf, get_string (format), &tm)] = '\0';
- success = push (ctx, new_string (buf, -1));
-
-fail:
- item_free (time_);
- item_free (format);
- return success;
-}
-
-// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-static void item_list_to_str (const struct item *, struct buffer *);
-
-static void
-string_to_str (const struct item_string *string, struct buffer *buf)
-{
- buffer_append_c (buf, '"');
- for (size_t i = 0; i < string->len; i++)
- {
- char c = string->value[i];
- if (c == '\n') buffer_append (buf, "\\n", 2);
- else if (c == '\r') buffer_append (buf, "\\r", 2);
- else if (c == '\t') buffer_append (buf, "\\t", 2);
- else if (!isprint (c))
- {
- char tmp[8];
- snprintf (tmp, sizeof tmp, "\\x%02x", (unsigned char) c);
- buffer_append (buf, tmp, strlen (tmp));
- }
- else if (c == '\\') buffer_append (buf, "\\\\", 2);
- else if (c == '"') buffer_append (buf, "\\\"", 2);
- else buffer_append_c (buf, c);
- }
- buffer_append_c (buf, '"');
-}
-
-static void
-item_to_str (const struct item *item, struct buffer *buf)
-{
- switch (item->type)
- {
- char *x;
- case ITEM_STRING:
- string_to_str ((struct item_string *) item, buf);
- break;
- case ITEM_WORD:
- {
- struct item_word *word = (struct item_word *) item;
- buffer_append (buf, word->value, word->len);
- break;
- }
- case ITEM_INTEGER:
- if (!(x = strdup_printf ("%lld", get_integer (item))))
- goto alloc_failure;
- buffer_append (buf, x, strlen (x));
- free (x);
- break;
- case ITEM_FLOAT:
- if (!(x = strdup_printf ("%Lf", get_float (item))))
- goto alloc_failure;
- buffer_append (buf, x, strlen (x));
- free (x);
- break;
- case ITEM_LIST:
- buffer_append_c (buf, '[');
- item_list_to_str (get_list (item), buf);
- buffer_append_c (buf, ']');
- break;
- }
- return;
-
-alloc_failure:
- // This is a bit hackish but it simplifies stuff
- buf->memory_failure = true;
- free (buf->s);
- buf->s = NULL;
-}
-
-static void
-item_list_to_str (const struct item *script, struct buffer *buf)
-{
- if (!script)
- return;
-
- item_to_str (script, buf);
- while ((script = script->next))
- {
- buffer_append_c (buf, ' ');
- item_to_str (script, buf);
- }
-}
-
-// --- IRC protocol ------------------------------------------------------------
-
-struct message
-{
- char *prefix; ///< Message prefix
- char *command; ///< IRC command
- char *params[16]; ///< Command parameters (0-terminated)
- size_t n_params; ///< Number of parameters present
-};
-
-inline static char *
-cut_word (char **s)
-{
- char *start = *s, *end = *s + strcspn (*s, " ");
- *s = end + strspn (end, " ");
- *end = '\0';
- return start;
-}
-
-static bool
-parse_message (char *s, struct message *msg)
-{
- memset (msg, 0, sizeof *msg);
-
- // Ignore IRC 3.2 message tags, if present
- if (*s == '@')
- {
- s += strcspn (s, " ");
- s += strspn (s, " ");
- }
-
- // Prefix
- if (*s == ':')
- msg->prefix = cut_word (&s) + 1;
-
- // Command
- if (!*(msg->command = cut_word (&s)))
- return false;
-
- // Parameters
- while (*s)
- {
- size_t n = msg->n_params++;
- if (msg->n_params >= N_ELEMENTS (msg->params))
- return false;
- if (*s == ':')
- {
- msg->params[n] = ++s;
- break;
- }
- msg->params[n] = cut_word (&s);
- }
- return true;
-}
-
-static struct message *
-read_message (void)
-{
- static bool discard = false;
- static char buf[1025];
- static struct message msg;
-
- bool discard_this;
- do
- {
- if (!fgets (buf, sizeof buf, stdin))
- return NULL;
- size_t len = strlen (buf);
-
- // Just to be on the safe side, if the line overflows our buffer,
- // ignore everything up until the next line.
- discard_this = discard;
- if (len >= 2 && !strcmp (buf + len - 2, "\r\n"))
- {
- buf[len -= 2] = '\0';
- discard = false;
- }
- else
- discard = true;
- }
- // Invalid messages are silently ignored
- while (discard_this || !parse_message (buf, &msg));
- return &msg;
-}
-
-// --- Interfacing with the bot ------------------------------------------------
-
-#define BOT_PRINT "ZYKLONB print :script: "
-
-static const char *
-get_config (const char *key)
-{
- printf ("ZYKLONB get_config :%s\r\n", key);
- struct message *msg = read_message ();
- if (!msg || msg->n_params <= 0)
- exit (EXIT_FAILURE);
- return msg->params[0];
-}
-
-// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-// TODO: implement more functions; try to avoid writing them in C
-
-static bool
-init_runtime_library_scripts (void)
-{
- bool ok = true;
-
- // It's much cheaper (and more fun) to define functions in terms of other
- // ones. The "unit tests" serve a secondary purpose of showing the usage.
- struct script
- {
- const char *name; ///< Name of the function
- const char *definition; ///< The defining script
- const char *unit_test; ///< Trivial unit test, must return 1
- }
- scripts[] =
- {
- { "nip", "swap drop", "1 2 nip 2 =" },
- { "over", "[dup] dip swap", "1 2 over nip nip 1 =" },
- { "swons", "swap cons", "[2] 1 swons [1 2] =" },
- { "first", "uncons drop", "[1 2 3] first 1 =" },
- { "rest", "uncons swap drop", "[1 2 3] rest [2 3] =" },
- { "reverse", "[] swap [swap cons] each", "[1 2] reverse [2 1] =" },
- { "curry", "cons", "1 2 [+] curry call 3 =" },
-
- { "xor", "not swap not + 1 =", "1 1 xor 0 =" },
- { "min", "over over < [drop] [nip] if", "1 2 min 1 =" },
- { "max", "over over > [drop] [nip] if", "1 2 max 2 =" },
-
- { "all?", "[and] cat 1 swap fold", "[3 4 5] [> 3] all? 0 =" },
- { "any?", "[or] cat 0 swap fold", "[3 4 5] [> 3] any? 1 =" },
-
- { ">", "swap <", "1 2 > 0 =" },
- { "!=", "= not", "1 2 != 1 =" },
- { "<=", "> not", "1 2 <= 1 =" },
- { ">=", "< not", "1 2 >= 0 =" },
-
- // XXX: this is a bit crazy and does not work with an empty list
- { "join", "[uncons] dip swap [[dup] dip swap [+ +] dip] each drop",
- "[1 2 3] [>string] map \" -> \" join \"1 -> 2 -> 3\" =" },
- };
-
- for (size_t i = 0; i < N_ELEMENTS (scripts); i++)
- {
- const char *error = NULL;
- struct item *script = parse (scripts[i].definition, &error);
- if (error)
- {
- printf (BOT_PRINT "error parsing internal script `%s': %s\r\n",
- scripts[i].definition, error);
- ok = false;
- }
- else
- ok &= register_script (scripts[i].name, script);
- }
-
- struct context ctx;
- for (size_t i = 0; i < N_ELEMENTS (scripts); i++)
- {
- const char *error = NULL;
- struct item *script = parse (scripts[i].unit_test, &error);
- if (error)
- {
- printf (BOT_PRINT "error parsing unit test for `%s': %s\r\n",
- scripts[i].name, error);
- ok = false;
- continue;
- }
- context_init (&ctx);
- execute (&ctx, script);
- item_free_list (script);
-
- const char *failure = NULL;
- if (ctx.memory_failure)
- failure = "memory allocation failure";
- else if (ctx.error)
- failure = ctx.error;
- else if (ctx.stack_size != 1)
- failure = "too many results on the stack";
- else if (ctx.stack->type != ITEM_INTEGER)
- failure = "result is not an integer";
- else if (get_integer (ctx.stack) != 1)
- failure = "wrong test result";
- if (failure)
- {
- printf (BOT_PRINT "error executing unit test for `%s': %s\r\n",
- scripts[i].name, failure);
- ok = false;
- }
- context_free (&ctx);
- }
- return ok;
-}
-
-static bool
-init_runtime_library (void)
-{
- bool ok = true;
-
- // Type detection
- ok &= register_handler ("string?", fn_is_string);
- ok &= register_handler ("word?", fn_is_word);
- ok &= register_handler ("integer?", fn_is_integer);
- ok &= register_handler ("float?", fn_is_float);
- ok &= register_handler ("list?", fn_is_list);
-
- // Type conversion
- ok &= register_handler (">string", fn_to_string);
- ok &= register_handler (">integer", fn_to_integer);
- ok &= register_handler (">float", fn_to_float);
-
- // Miscellaneous
- ok &= register_handler ("length", fn_length);
-
- // Basic stack manipulation
- ok &= register_handler ("dup", fn_dup);
- ok &= register_handler ("drop", fn_drop);
- ok &= register_handler ("swap", fn_swap);
-
- // Calling stuff
- ok &= register_handler ("call", fn_call);
- ok &= register_handler ("dip", fn_dip);
-
- // Control flow
- ok &= register_handler ("if", fn_if);
- ok &= register_handler ("try", fn_try);
-
- // List processing
- ok &= register_handler ("map", fn_map);
- ok &= register_handler ("filter", fn_filter);
- ok &= register_handler ("fold", fn_fold);
- ok &= register_handler ("each", fn_each);
-
- // List manipulation
- ok &= register_handler ("unit", fn_unit);
- ok &= register_handler ("cons", fn_cons);
- ok &= register_handler ("cat", fn_cat);
- ok &= register_handler ("uncons", fn_uncons);
-
- // Arithmetic operations
- ok &= register_handler ("+", fn_plus);
- ok &= register_handler ("-", fn_minus);
- ok &= register_handler ("*", fn_times);
- ok &= register_handler ("^", fn_pow);
- ok &= register_handler ("/", fn_div);
- ok &= register_handler ("%", fn_mod);
-
- // Comparison
- ok &= register_handler ("=", fn_eq);
- ok &= register_handler ("<", fn_lt);
-
- // Logical operations
- ok &= register_handler ("not", fn_not);
- ok &= register_handler ("and", fn_and);
- ok &= register_handler ("or", fn_or);
-
- // Utilities
- ok &= register_handler ("rand", fn_rand);
- ok &= register_handler ("time", fn_time);
- ok &= register_handler ("strftime", fn_strftime);
-
- ok &= init_runtime_library_scripts ();
- return ok;
-}
-
-static void
-free_runtime_library (void)
-{
- struct fn *next, *iter;
- for (iter = g_functions; iter; iter = next)
- {
- next = iter->next;
- free_function (iter);
- }
-}
-
-// --- Function database -------------------------------------------------------
-
-// TODO: a global variable storing the various procedures (db)
-// XXX: defining procedures would ideally need some kind of an ACL
-
-static void
-read_db (void)
-{
- // TODO
-}
-
-static void
-write_db (void)
-{
- // TODO
-}
-
-// --- Main --------------------------------------------------------------------
-
-static char *g_prefix;
-
-struct user_info
-{
- char *ctx; ///< Context: channel or user
- char *ctx_quote; ///< Reply quotation
-};
-
-defn (fn_dot)
-{
- check_stack (1);
- struct item *item = pop (ctx);
- struct user_info *info = ctx->user_data;
-
- struct buffer buf = BUFFER_INITIALIZER;
- item_to_str (item, &buf);
- item_free (item);
- buffer_append_c (&buf, '\0');
- if (buf.memory_failure)
- {
- ctx->memory_failure = true;
- return false;
- }
-
- if (buf.len > 255)
- buf.s[255] = '\0';
-
- printf ("PRIVMSG %s :%s%s\r\n", info->ctx, info->ctx_quote, buf.s);
- free (buf.s);
- return true;
-}
-
-static void
-process_message (struct message *msg)
-{
- if (!msg->prefix
- || strcasecmp (msg->command, "PRIVMSG")
- || msg->n_params < 2)
- return;
- char *line = msg->params[1];
-
- // Filter out only our commands
- size_t prefix_len = strlen (g_prefix);
- if (strncmp (line, g_prefix, prefix_len))
- return;
- line += prefix_len;
-
- char *command = cut_word (&line);
- if (strcasecmp (command, "script"))
- return;
-
- // Retrieve information on how to respond back
- char *msg_ctx = msg->prefix, *x;
- if ((x = strchr (msg_ctx, '!')))
- *x = '\0';
-
- char *msg_ctx_quote;
- if (strchr ("#+&!", *msg->params[0]))
- {
- msg_ctx_quote = strdup_printf ("%s: ", msg_ctx);
- msg_ctx = msg->params[0];
- }
- else
- msg_ctx_quote = strdup ("");
-
- if (!msg_ctx_quote)
- {
- printf (BOT_PRINT "%s\r\n", "memory allocation failure");
- return;
- }
-
- struct user_info info;
- info.ctx = msg_ctx;
- info.ctx_quote = msg_ctx_quote;
-
- // Finally parse and execute the macro
- const char *error = NULL;
- struct item *script = parse (line, &error);
- if (error)
- {
- printf ("PRIVMSG %s :%s%s: %s\r\n",
- msg_ctx, msg_ctx_quote, "parse error", error);
- goto end;
- }
-
- struct context ctx;
- context_init (&ctx);
- ctx.user_data = &info;
- execute (&ctx, script);
- item_free_list (script);
-
- const char *failure = NULL;
- if (ctx.memory_failure)
- failure = "memory allocation failure";
- else if (ctx.error)
- failure = ctx.error;
- if (failure)
- printf ("PRIVMSG %s :%s%s: %s\r\n",
- msg_ctx, msg_ctx_quote, "runtime error", failure);
- context_free (&ctx);
-end:
- free (msg_ctx_quote);
-}
-
-int
-main (int argc, char *argv[])
-{
- freopen (NULL, "rb", stdin); setvbuf (stdin, NULL, _IOLBF, BUFSIZ);
- freopen (NULL, "wb", stdout); setvbuf (stdout, NULL, _IOLBF, BUFSIZ);
-
- struct rlimit limit =
- {
- .rlim_cur = ADDRESS_SPACE_LIMIT,
- .rlim_max = ADDRESS_SPACE_LIMIT
- };
-
- // Lower the memory limits to something sensible to prevent abuse
- (void) setrlimit (RLIMIT_AS, &limit);
-
- read_db ();
- if (!init_runtime_library ()
- || !register_handler (".", fn_dot))
- printf (BOT_PRINT "%s\r\n", "runtime library initialization failed");
-
- g_prefix = strdup (get_config ("prefix"));
- printf ("ZYKLONB register\r\n");
- struct message *msg;
- while ((msg = read_message ()))
- process_message (msg);
-
- free_runtime_library ();
- free (g_prefix);
- return 0;
-}
-
diff --git a/plugins/zyklonb/seen b/plugins/zyklonb/seen
deleted file mode 100755
index 8fc9c82..0000000
--- a/plugins/zyklonb/seen
+++ /dev/null
@@ -1,160 +0,0 @@
-#!/usr/bin/env lua
---
--- ZyklonB seen plugin
---
--- Copyright 2016 Přemysl Eric Janouch
--- See the file LICENSE for licensing information.
---
-
-function parse (line)
- local msg = { params = {} }
- line = line:match ("[^\r]*")
- for start, word in line:gmatch ("()([^ ]+)") do
- local colon = word:match ("^:(.*)")
- if start == 1 and colon then
- msg.prefix = colon
- elseif not msg.command then
- msg.command = word
- elseif colon then
- table.insert (msg.params, line:sub (start + 1))
- break
- elseif start ~= #line then
- table.insert (msg.params, word)
- end
- end
- return msg
-end
-
-function get_config (name)
- io.write ("ZYKLONB get_config :", name, "\r\n")
- return parse (io.read ()).params[1]
-end
-
--- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-io.output ():setvbuf ('line')
-local prefix = get_config ('prefix')
-io.write ("ZYKLONB register\r\n")
-
-local db = {}
-local db_filename = "seen.db"
-local db_garbage = 0
-
-function remember (who, where, when, what)
- if not db[who] then db[who] = {} end
- if db[who][where] then db_garbage = db_garbage + 1 end
- db[who][where] = { tonumber (when), what }
-end
-
--- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-local db_file, e = io.open (db_filename, "a+")
-if not db_file then error ("cannot open database: " .. e, 0) end
-
-function db_store (who, where, when, what)
- db_file:write (string.format
- (":%s %s %s %s :%s\n", who, "PRIVMSG", where, when, what))
-end
-
-function db_compact ()
- db_file:close ()
-
- -- Unfortunately, default Lua doesn't have anything like mkstemp()
- local db_tmpname = db_filename .. "." .. os.time ()
- db_file, e = io.open (db_tmpname, "a+")
- if not db_file then error ("cannot save database: " .. e, 0) end
-
- for who, places in pairs (db) do
- for where, data in pairs (places) do
- db_store (who, where, data[1], data[2])
- end
- end
- db_file:flush ()
-
- local ok, e = os.rename (db_tmpname, db_filename)
- if not ok then error ("cannot save database: " .. e, 0) end
- db_garbage = 0
-end
-
-for line in db_file:lines () do
- local msg = parse (line)
- remember (msg.prefix, table.unpack (msg.params))
-end
-
--- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-function seen (who, where, args)
- local respond = function (...)
- local privmsg = function (target, ...)
- io.write ("PRIVMSG ", target, " :", table.concat { ... }, "\r\n")
- end
- if where:match ("^[#&!+]") then
- privmsg (where, who, ": ", ...)
- else
- privmsg (who, ...)
- end
- end
-
- local whom, e, garbage = args:match ("^(%S+)()%s*(.*)")
- if not whom or #garbage ~= 0 then
- return respond ("usage: ")
- elseif who:lower () == whom:lower () then
- return respond ("I can see you right now.")
- end
-
- local top = {}
- -- That is, * acts like a wildcard, otherwise everything is escaped
- local pattern = "^" .. whom:gsub ("[%^%$%(%)%%%.%[%]%+%-%?]", "%%%0")
- :gsub ("%*", ".*"):lower () .. "$"
- for name, places in pairs (db) do
- if places[where] and name:lower ():match (pattern) then
- local when, what = table.unpack (places[where])
- table.insert (top, { name = name, when = when, what = what })
- end
- end
- if #top == 0 then
- return respond ("I have not seen \x02" .. whom .. "\x02 here.")
- end
-
- -- Get all matching nicknames ordered from the most recently active
- -- and make the list case insensitive (remove older duplicates)
- table.sort (top, function (a, b) return a.when > b.when end)
- for i = #top, 2, -1 do
- if top[i - 1].name:lower () == top[i].name:lower () then
- table.remove (top, i)
- end
- end
-
- -- Hopefully the formatting mess will disrupt highlights in clients
- for i = 1, math.min (#top, 3) do
- local name = top[i].name:gsub ("^.", "%0\x02\x02")
- respond (string.format ("\x02%s\x02 -> %s -> %s",
- name, os.date ("%c", top[i].when), top[i].what))
- end
-end
-
-function handle (msg)
- local who = msg.prefix:match ("^[^!@]*")
- local where, what = table.unpack (msg.params)
- local when = os.time ()
-
- local what_log = what:gsub ("^\x01ACTION", "*"):gsub ("\x01$", "")
- remember (who, where, when, what_log)
- db_store (who, where, when, what_log)
-
- -- Comment out to reduce both disk load and reliability
- db_file:flush ()
-
- if db_garbage > 5000 then db_compact () end
-
- if what:sub (1, #prefix) == prefix then
- local command = what:sub (#prefix + 1)
- local name, e = command:match ("^(%S+)%s*()")
- if name == 'seen' then seen (who, where, command:sub (e)) end
- end
-end
-
-for line in io.lines () do
- local msg = parse (line)
- if msg.command == "PRIVMSG" then handle (msg) end
-end
diff --git a/plugins/zyklonb/seen-import-degesch.pl b/plugins/zyklonb/seen-import-degesch.pl
deleted file mode 100755
index ddef6be..0000000
--- a/plugins/zyklonb/seen-import-degesch.pl
+++ /dev/null
@@ -1,39 +0,0 @@
-#!/usr/bin/env perl
-# Creates a database for the "seen" plugin from logs for degesch.
-# The results may not be completely accurate but are good for jumpstarting.
-# Usage: ./seen-import-degesch.pl LOG-FILE... > seen.db
-
-use strict;
-use warnings;
-use File::Basename;
-use Time::Piece;
-
-my $db = {};
-for (@ARGV) {
- my $where = (basename($_) =~ /\.(.*).log/)[0];
- unless ($where) {
- print STDERR "Invalid filename: $_\n";
- next;
- }
-
- open my $fh, '<', $_ or die "Failed to open log file: $!";
- while (<$fh>) {
- my ($when, $who, $who_action, $what) =
- /^(.{19}) (?:<[~&@%+]*(.*?)>| \* (\S+)) (.*)/;
- next unless $when;
-
- if ($who_action) {
- $who = $who_action;
- $what = "* $what";
- }
- $db->{$who}->{$where} =
- [Time::Piece->strptime($when, "%Y-%m-%d %T")->epoch, $what];
- }
-}
-
-while (my ($who, $places) = each %$db) {
- while (my ($where, $data) = each %$places) {
- my ($when, $what) = @$data;
- print ":$who PRIVMSG $where $when :$what\n";
- }
-}
diff --git a/plugins/zyklonb/youtube b/plugins/zyklonb/youtube
deleted file mode 100755
index 53b86d8..0000000
--- a/plugins/zyklonb/youtube
+++ /dev/null
@@ -1,111 +0,0 @@
-#!/usr/bin/env python3
-#
-# ZyklonB YouTube plugin, displaying info about YouTube links
-#
-# Copyright 2014 - 2015, Přemysl Eric Janouch
-# See the file LICENSE for licensing information.
-#
-
-import sys
-import io
-import re
-import json
-import urllib.request
-
-class Plugin:
- re_msg = re.compile ('(?::([^! ]*)(?:!([^@]*)@([^ ]*))? +)?'
- '([^ ]+)(?: +(.*))?\r\n$')
- re_args = re.compile (':?((?<=:).*|[^ ]+) *')
-
- def parse (self, line):
- m = self.re_msg.match (line)
- if m is None:
- return None
-
- (nick, user, host, command, args) = m.groups ()
- args = [] if args is None else self.re_args.findall (args)
- return (nick, user, host, command, args)
-
- def get_config (self, key):
- print ("ZYKLONB get_config :%s" % key)
- (_, _, _, _, args) = self.parse (sys.stdin.readline ())
- return args[0]
-
- def bot_print (self, what):
- print ('ZYKLONB print :%s' % what)
-
-class YouTube (Plugin):
- re_videos = [re.compile (x) for x in [
- r'youtube\.[a-z]+/[^ ]*[&?]v=([-\w]+)',
- r'youtube\.[a-z]+/v/([-\w]+)',
- r'youtu\.be/([-\w]+)'
- ]]
- re_playlists = [re.compile (x) for x in [
- r'youtube\.[a-z]+/playlist[&?][^ ]*(?<=&|\?)list=([-\w]+)',
- ]]
-
- def print_info (self, channel, url, cb):
- try:
- data = json.loads (urllib.request.urlopen
- (url, None, 30).read ().decode ('utf-8'))
-
- for line in map (lambda x: "YouTube: " + cb (x), data['items']):
- print ("PRIVMSG %s :%s" % (channel,
- line.encode ('utf-8').decode ('iso8859-1')))
-
- except Exception as err:
- self.bot_print ('youtube: %s' % (err))
-
- def print_video_info (self, channel, video_id):
- url = 'https://www.googleapis.com/youtube/v3/' \
- + 'videos?id=%s&key=%s&part=snippet,contentDetails,statistics' \
- % (video_id, self.youtube_api_key)
- self.print_info (channel, url, lambda x: "%s | %s | %sx" % (
- x['snippet']['title'],
- x['contentDetails']['duration'][2:].lower (),
- x['statistics']['viewCount']))
-
- def print_playlist_info (self, channel, playlist_id):
- url = 'https://www.googleapis.com/youtube/v3/' \
- + 'playlists?id=%s&key=%s&part=snippet,contentDetails' \
- % (playlist_id, self.youtube_api_key)
- self.print_info (channel, url, lambda x: "%s | %d videos" % (
- x['snippet']['title'],
- x['contentDetails']['itemCount']))
-
- def process_line (self, line):
- msg = self.parse (line)
- if msg is None:
- return
-
- (nick, user, host, command, args) = msg
- if command != 'PRIVMSG' or len (args) < 2:
- return
-
- ctx = args[0]
- if not ctx.startswith (('#', '+', '&', '!')):
- ctx = nick
-
- for regex in self.re_videos:
- for i in regex.findall (args[1]):
- self.print_video_info (ctx, i)
- for regex in self.re_playlists:
- for i in regex.findall (args[1]):
- self.print_playlist_info (ctx, i)
-
- def run (self):
- self.youtube_api_key = self.get_config ('youtube_api_key')
- if self.youtube_api_key == "":
- self.bot_print ("youtube: missing `youtube_api_key'")
-
- print ("ZYKLONB register")
-
- for line in sys.stdin:
- self.process_line (line)
-
-sys.stdin = io.TextIOWrapper (sys.__stdin__.buffer,
- encoding = 'iso8859-1', newline = '\r\n', line_buffering = True)
-sys.stdout = io.TextIOWrapper (sys.__stdout__.buffer,
- encoding = 'iso8859-1', newline = '\r\n', line_buffering = True)
-
-YouTube ().run ()
diff --git a/test b/test
index bf4d697..2ba55b4 100755
--- a/test
+++ b/test
@@ -2,13 +2,13 @@
# Very basic end-to-end testing for CI
# Run the daemon to test against
-system ./kike --write-default-cfg
-spawn ./kike -d
+system ./xD --write-default-cfg
+spawn ./xD -d
# 10 seconds is a bit too much
set timeout 5
-spawn ./degesch
+spawn ./xC
# Fuck this Tcl shit, I want the exit code
expect_after {
diff --git a/test-nick-colors b/test-nick-colors
index dcc112f..b6c97cc 100755
--- a/test-nick-colors
+++ b/test-nick-colors
@@ -9,7 +9,7 @@ export example=$(
#define N_ELEMENTS(a) (sizeof (a) / sizeof ((a)[0]))
$(perl -0777 -ne 'print $& if /^.*?\nfilter_color(?s:.*?)^}$/m' \
- "$(dirname "$0")"/degesch.c)
+ "$(dirname "$0")"/xC.c)
void main () {
size_t len = 0;
diff --git a/test-static b/test-static
index 0c22b0d..85d7f4f 100755
--- a/test-static
+++ b/test-static
@@ -1,7 +1,7 @@
#!/bin/sh
# We don't use printf's percent notation with our custom logging mechanism,
# so the compiler cannot check it for us like it usually does
-perl -n0777 - "$(dirname "$0")"/degesch.c <<-'END'
+perl -n0777 - "$(dirname "$0")"/xC.c <<-'END'
while (/\blog_[^ ]+\s*\([^"()]*"[^"]*%[^%][^"]*"/gm) {
my ($p, $m) = ($`, $&);
printf "$ARGV:%d: suspicious log format string: %s...\n",
diff --git a/xB.adoc b/xB.adoc
new file mode 100644
index 0000000..91f7703
--- /dev/null
+++ b/xB.adoc
@@ -0,0 +1,104 @@
+xB(1)
+=====
+:doctype: manpage
+:manmanual: uirc3 Manual
+:mansource: uirc3 {release-version}
+
+Name
+----
+xB - modular IRC bot
+
+Synopsis
+--------
+*xB* [_OPTION_]...
+
+Description
+-----------
+*xB* is a modular IRC bot with a programming language-agnostic plugin
+architecture based on co-processes.
+
+Options
+-------
+*-d*, *--debug*::
+ Print more information to help debug various issues.
+
+*-h*, *--help*::
+ Display a help message and exit.
+
+*-V*, *--version*::
+ Output version information and exit.
+
+*--write-default-cfg*[**=**__PATH__]::
+ Write a configuration file with defaults, show its path and exit.
++
+The file will be appropriately commented.
+
+Commands
+--------
+The bot accepts the following commands when they either appear quoted by the
+*prefix* string on a channel or unquoted as a private message sent directly
+to the bot, on the condition that the sending user matches the *admin*
+regular expression or that it is left unset:
+
+*quote* [_message_]::
+ Forwards the message to the IRC server as-is.
+*quit* [_reason_]::
+ Quits the IRC server, with an optional reason string.
+*status*::
+ Sends back a report about its state and all loaded plugins.
+*load* _plugin_[, _plugin_]...::
+ Tries to load the given plugins.
+*unload* _plugin_[, _plugin_]...::
+ Tries to unload the given plugins.
+*reload* _plugin_[, _plugin_]...::
+ The same as *unload* immediately followed by *load*.
+
+Plugins
+-------
+Plugins communicate with the bot over their standard input and output streams
+using the IRC protocol. (Caveat: the standard C library doesn't automatically
+flush FILE streams for pipes on newlines.) A special *ZYKLONB* command is
+introduced for RPC, with the following subcommands:
+
+*ZYKLONB get_config* _key_::
+ Request the value of the given configuration option. If no such option
+ exists, the value will be empty. The response will be delivered in
+ the following format:
++
+```
+ZYKLONB :value
+```
++
+This is particularly useful for retrieving the *prefix* string.
+
+*ZYKLONB print* _message_::
+ Make the bot print the _message_ on its standard output.
+
+*ZYKLONB register*::
+ Once a plugin issues this command, it will start receiving all of the bot's
+ incoming IRC traffic, which includes data from the initialization period.
+
+All other commands will be forwarded directly to the IRC server.
+
+Files
+-----
+*xB* follows the XDG Base Directory Specification.
+
+_~/.config/xB/xB.conf_::
+ The bot's configuration file. Use the *--write-default-cfg* option
+ to create a new one for editing.
+
+_~/.local/share/xB/_::
+ The initial working directory for plugins, in which they may create private
+ databases or other files as needed.
+
+_~/.local/share/xB/plugins/_::
+_/usr/local/share/xB/plugins/_::
+_/usr/share/xB/plugins/_::
+ Plugins are searched for in these directories, in order, unless
+ the *plugin_dir* configuration option overrides this.
+
+Reporting bugs
+--------------
+Use https://git.janouch.name/p/uirc3 to report bugs, request features,
+or submit pull requests.
diff --git a/xB.c b/xB.c
new file mode 100644
index 0000000..9e36040
--- /dev/null
+++ b/xB.c
@@ -0,0 +1,2063 @@
+/*
+ * xB.c: a modular IRC bot
+ *
+ * Copyright (c) 2014 - 2020, 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.
+ *
+ */
+
+#include "config.h"
+#define PROGRAM_NAME "xB"
+
+#include "common.c"
+
+// --- Configuration (application-specific) ------------------------------------
+
+static struct simple_config_item g_config_table[] =
+{
+ { "nickname", "xB", "IRC nickname" },
+ { "username", "bot", "IRC user name" },
+ { "realname", "xB IRC bot", "IRC real name/e-mail" },
+
+ { "irc_host", NULL, "Address of the IRC server" },
+ { "irc_port", "6667", "Port of the IRC server" },
+ { "tls", "off", "Whether to use TLS" },
+ { "tls_cert", NULL, "Client TLS certificate (PEM)" },
+ { "tls_verify", "on", "Whether to verify certificates" },
+ { "tls_ca_file", NULL, "OpenSSL CA bundle file" },
+ { "tls_ca_path", NULL, "OpenSSL CA bundle path" },
+ { "autojoin", NULL, "Channels to join on start" },
+ { "reconnect", "on", "Whether to reconnect on error" },
+ { "reconnect_delay", "5", "Time between reconnecting" },
+
+ { "socks_host", NULL, "Address of a SOCKS 4a/5 proxy" },
+ { "socks_port", "1080", "SOCKS port number" },
+ { "socks_username", NULL, "SOCKS auth. username" },
+ { "socks_password", NULL, "SOCKS auth. password" },
+
+ { "prefix", ":", "The prefix for bot commands" },
+ { "admin", NULL, "Host mask for administrators" },
+ { "plugins", NULL, "The plugins to load on startup" },
+ { "plugin_dir", NULL, "Plugin search path override" },
+ { "recover", "on", "Whether to re-launch on crash" },
+
+ { NULL, NULL, NULL }
+};
+
+// --- Application data --------------------------------------------------------
+
+struct plugin
+{
+ LIST_HEADER (struct plugin)
+ struct bot_context *ctx; ///< Parent context
+
+ char *name; ///< Plugin identifier
+ pid_t pid; ///< PID of the plugin process
+
+ bool is_zombie; ///< Whether the child is a zombie
+ bool initialized; ///< Ready to exchange IRC messages
+ struct str queued_output; ///< Output queued up until initialized
+
+ // Since we're doing non-blocking I/O, we need to queue up data so that
+ // we don't stall on plugins unnecessarily.
+
+ int read_fd; ///< The read end of the comm. pipe
+ int write_fd; ///< The write end of the comm. pipe
+
+ struct poller_fd read_event; ///< Read FD event
+ struct poller_fd write_event; ///< Write FD event
+
+ struct str read_buffer; ///< Unprocessed input
+ struct str write_buffer; ///< Output yet to be sent out
+};
+
+static struct plugin *
+plugin_new (void)
+{
+ struct plugin *self = xcalloc (1, sizeof *self);
+ self->pid = -1;
+ self->queued_output = str_make ();
+
+ self->read_fd = -1;
+ self->read_buffer = str_make ();
+ self->write_fd = -1;
+ self->write_buffer = str_make ();
+ return self;
+}
+
+static void
+plugin_destroy (struct plugin *self)
+{
+ soft_assert (self->pid == -1);
+ free (self->name);
+
+ str_free (&self->read_buffer);
+ if (!soft_assert (self->read_fd == -1))
+ xclose (self->read_fd);
+
+ str_free (&self->write_buffer);
+ if (!soft_assert (self->write_fd == -1))
+ xclose (self->write_fd);
+
+ if (!self->initialized)
+ str_free (&self->queued_output);
+
+ free (self);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+struct bot_context
+{
+ struct str_map config; ///< User configuration
+ regex_t *admin_re; ///< Regex to match our administrator
+ bool reconnect; ///< Whether to reconnect on conn. fail.
+ unsigned long reconnect_delay; ///< Reconnect delay in seconds
+
+ int irc_fd; ///< Socket FD of the server
+ struct str read_buffer; ///< Input yet to be processed
+ struct poller_fd irc_event; ///< IRC FD event
+ bool irc_registered; ///< Whether we may send messages now
+
+ struct poller_fd signal_event; ///< Signal FD event
+ struct poller_timer ping_tmr; ///< We should send a ping
+ struct poller_timer timeout_tmr; ///< Connection seems to be dead
+ struct poller_timer reconnect_tmr; ///< We should reconnect now
+
+ SSL_CTX *ssl_ctx; ///< SSL context
+ SSL *ssl; ///< SSL connection
+
+ struct plugin *plugins; ///< Linked list of plugins
+ struct str_map plugins_by_name; ///< Indexes @em plugins by their name
+
+ struct poller poller; ///< Manages polled descriptors
+ bool quitting; ///< User requested quitting
+ bool polling; ///< The event loop is running
+};
+
+static void on_irc_ping_timeout (void *user_data);
+static void on_irc_timeout (void *user_data);
+static void on_irc_reconnect_timeout (void *user_data);
+
+static void
+bot_context_init (struct bot_context *self)
+{
+ self->config = str_map_make (free);
+ simple_config_load_defaults (&self->config, g_config_table);
+ self->admin_re = NULL;
+
+ self->irc_fd = -1;
+ self->read_buffer = str_make ();
+ self->irc_registered = false;
+
+ self->ssl = NULL;
+ self->ssl_ctx = NULL;
+
+ self->plugins = NULL;
+ self->plugins_by_name = str_map_make (NULL);
+
+ poller_init (&self->poller);
+ self->quitting = false;
+ self->polling = false;
+
+ self->timeout_tmr = poller_timer_make (&self->poller);
+ self->timeout_tmr.dispatcher = on_irc_timeout;
+ self->timeout_tmr.user_data = self;
+
+ self->ping_tmr = poller_timer_make (&self->poller);
+ self->ping_tmr.dispatcher = on_irc_ping_timeout;
+ self->ping_tmr.user_data = self;
+
+ self->reconnect_tmr = poller_timer_make (&self->poller);
+ self->reconnect_tmr.dispatcher = on_irc_reconnect_timeout;
+ self->reconnect_tmr.user_data = self;
+}
+
+static void
+bot_context_free (struct bot_context *self)
+{
+ str_map_free (&self->config);
+ if (self->admin_re)
+ regex_free (self->admin_re);
+ str_free (&self->read_buffer);
+
+ // TODO: terminate the plugins properly before this is called
+ LIST_FOR_EACH (struct plugin, link, self->plugins)
+ plugin_destroy (link);
+
+ if (self->irc_fd != -1)
+ {
+ poller_fd_reset (&self->irc_event);
+ xclose (self->irc_fd);
+ }
+ if (self->ssl)
+ SSL_free (self->ssl);
+ if (self->ssl_ctx)
+ SSL_CTX_free (self->ssl_ctx);
+
+ str_map_free (&self->plugins_by_name);
+ poller_free (&self->poller);
+}
+
+static void
+irc_shutdown (struct bot_context *ctx)
+{
+ // TODO: set a timer after which we cut the connection?
+ // Generally non-critical
+ if (ctx->ssl)
+ soft_assert (SSL_shutdown (ctx->ssl) != -1);
+ else
+ soft_assert (shutdown (ctx->irc_fd, SHUT_WR) == 0);
+}
+
+static void
+try_finish_quit (struct bot_context *ctx)
+{
+ if (ctx->quitting && ctx->irc_fd == -1 && !ctx->plugins)
+ ctx->polling = false;
+}
+
+static bool plugin_zombify (struct plugin *);
+
+static void
+initiate_quit (struct bot_context *ctx)
+{
+ // Initiate bringing down of the two things that block our shutdown:
+ // a/ the IRC socket, b/ our child processes:
+
+ for (struct plugin *plugin = ctx->plugins;
+ plugin; plugin = plugin->next)
+ plugin_zombify (plugin);
+ if (ctx->irc_fd != -1)
+ irc_shutdown (ctx);
+
+ ctx->quitting = true;
+ try_finish_quit (ctx);
+}
+
+static bool irc_send (struct bot_context *ctx,
+ const char *format, ...) ATTRIBUTE_PRINTF (2, 3);
+
+static bool
+irc_send (struct bot_context *ctx, const char *format, ...)
+{
+ va_list ap;
+
+ if (g_debug_mode)
+ {
+ fputs ("[IRC] <== \"", stderr);
+ va_start (ap, format);
+ vfprintf (stderr, format, ap);
+ va_end (ap);
+ fputs ("\"\n", stderr);
+ }
+
+ if (!soft_assert (ctx->irc_fd != -1))
+ return false;
+
+ va_start (ap, format);
+ struct str str = str_make ();
+ str_append_vprintf (&str, format, ap);
+ str_append (&str, "\r\n");
+ va_end (ap);
+
+ bool result = true;
+ if (ctx->ssl)
+ {
+ // TODO: call SSL_get_error() to detect if a clean shutdown has occured
+ ERR_clear_error ();
+ if (SSL_write (ctx->ssl, str.str, str.len) != (int) str.len)
+ {
+ print_debug ("%s: %s: %s", __func__, "SSL_write",
+ xerr_describe_error ());
+ result = false;
+ }
+ }
+ else if (write (ctx->irc_fd, str.str, str.len) != (ssize_t) str.len)
+ {
+ print_debug ("%s: %s: %s", __func__, "write", strerror (errno));
+ result = false;
+ }
+
+ str_free (&str);
+ return result;
+}
+
+static bool
+irc_get_boolean_from_config
+ (struct bot_context *ctx, const char *name, bool *value, struct error **e)
+{
+ const char *str = str_map_find (&ctx->config, name);
+ hard_assert (str != NULL);
+
+ if (set_boolean_if_valid (value, str))
+ return true;
+
+ return error_set (e, "invalid configuration value for `%s'", name);
+}
+
+static bool
+irc_initialize_ca_set (SSL_CTX *ssl_ctx, const char *file, const char *path,
+ struct error **e)
+{
+ ERR_clear_error ();
+
+ if (file || path)
+ {
+ if (SSL_CTX_load_verify_locations (ssl_ctx, file, path))
+ return true;
+
+ return error_set (e, "%s: %s",
+ "failed to set locations for the CA certificate bundle",
+ xerr_describe_error ());
+ }
+
+ if (!SSL_CTX_set_default_verify_paths (ssl_ctx))
+ return error_set (e, "%s: %s",
+ "couldn't load the default CA certificate bundle",
+ xerr_describe_error ());
+ return true;
+}
+
+static bool
+irc_initialize_ca (struct bot_context *ctx, struct error **e)
+{
+ const char *ca_file = str_map_find (&ctx->config, "tls_ca_file");
+ const char *ca_path = str_map_find (&ctx->config, "tls_ca_path");
+
+ char *full_file = ca_file
+ ? resolve_filename (ca_file, resolve_relative_config_filename) : NULL;
+ char *full_path = ca_path
+ ? resolve_filename (ca_path, resolve_relative_config_filename) : NULL;
+
+ bool ok = false;
+ if (ca_file && !full_file)
+ error_set (e, "couldn't find the CA bundle file");
+ else if (ca_path && !full_path)
+ error_set (e, "couldn't find the CA bundle path");
+ else
+ ok = irc_initialize_ca_set (ctx->ssl_ctx, full_file, full_path, e);
+
+ free (full_file);
+ free (full_path);
+ return ok;
+}
+
+static bool
+irc_initialize_ssl_ctx (struct bot_context *ctx, struct error **e)
+{
+ // Disable deprecated protocols (see RFC 7568)
+ SSL_CTX_set_options (ctx->ssl_ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
+
+ bool verify;
+ if (!irc_get_boolean_from_config (ctx, "tls_verify", &verify, e))
+ return false;
+ SSL_CTX_set_verify (ctx->ssl_ctx,
+ verify ? SSL_VERIFY_PEER : SSL_VERIFY_NONE, NULL);
+
+ struct error *error = NULL;
+ if (!irc_initialize_ca (ctx, &error))
+ {
+ if (verify)
+ {
+ error_propagate (e, error);
+ return false;
+ }
+
+ // Only inform the user if we're not actually verifying
+ print_warning ("%s", error->message);
+ error_free (error);
+ }
+ return true;
+}
+
+static bool
+irc_initialize_tls (struct bot_context *ctx, struct error **e)
+{
+ const char *error_info = NULL;
+ ctx->ssl_ctx = SSL_CTX_new (SSLv23_client_method ());
+ if (!ctx->ssl_ctx)
+ goto error_ssl_1;
+ if (!irc_initialize_ssl_ctx (ctx, e))
+ goto error_ssl_2;
+
+ ctx->ssl = SSL_new (ctx->ssl_ctx);
+ if (!ctx->ssl)
+ goto error_ssl_2;
+
+ const char *tls_cert = str_map_find (&ctx->config, "tls_cert");
+ if (tls_cert)
+ {
+ char *path = resolve_filename
+ (tls_cert, resolve_relative_config_filename);
+ if (!path)
+ print_error ("%s: %s", "cannot open file", tls_cert);
+ // XXX: perhaps we should read the file ourselves for better messages
+ else if (!SSL_use_certificate_file (ctx->ssl, path, SSL_FILETYPE_PEM)
+ || !SSL_use_PrivateKey_file (ctx->ssl, path, SSL_FILETYPE_PEM))
+ print_error ("%s: %s", "setting the TLS client certificate failed",
+ xerr_describe_error ());
+ free (path);
+ }
+
+ SSL_set_connect_state (ctx->ssl);
+ if (!SSL_set_fd (ctx->ssl, ctx->irc_fd))
+ goto error_ssl_3;
+ // Avoid SSL_write() returning SSL_ERROR_WANT_READ
+ SSL_set_mode (ctx->ssl, SSL_MODE_AUTO_RETRY);
+
+ switch (xssl_get_error (ctx->ssl, SSL_connect (ctx->ssl), &error_info))
+ {
+ case SSL_ERROR_NONE:
+ return true;
+ case SSL_ERROR_ZERO_RETURN:
+ error_info = "server closed the connection";
+ default:
+ break;
+ }
+
+error_ssl_3:
+ SSL_free (ctx->ssl);
+ ctx->ssl = NULL;
+error_ssl_2:
+ SSL_CTX_free (ctx->ssl_ctx);
+ ctx->ssl_ctx = NULL;
+error_ssl_1:
+ if (!error_info)
+ error_info = xerr_describe_error ();
+ return error_set (e, "%s: %s", "could not initialize TLS", error_info);
+}
+
+static bool
+irc_establish_connection (struct bot_context *ctx,
+ const char *host, const char *port, struct error **e)
+{
+ struct addrinfo gai_hints, *gai_result, *gai_iter;
+ memset (&gai_hints, 0, sizeof gai_hints);
+ gai_hints.ai_socktype = SOCK_STREAM;
+
+ int err = getaddrinfo (host, port, &gai_hints, &gai_result);
+ if (err)
+ return error_set (e, "%s: %s: %s", "connection failed",
+ "getaddrinfo", gai_strerror (err));
+
+ int sockfd;
+ for (gai_iter = gai_result; gai_iter; gai_iter = gai_iter->ai_next)
+ {
+ sockfd = socket (gai_iter->ai_family,
+ gai_iter->ai_socktype, gai_iter->ai_protocol);
+ if (sockfd == -1)
+ continue;
+ set_cloexec (sockfd);
+
+ int yes = 1;
+ soft_assert (setsockopt (sockfd, SOL_SOCKET, SO_KEEPALIVE,
+ &yes, sizeof yes) != -1);
+
+ const char *real_host = host;
+
+ // Let's try to resolve the address back into a real hostname;
+ // we don't really need this, so we can let it quietly fail
+ char buf[NI_MAXHOST];
+ err = getnameinfo (gai_iter->ai_addr, gai_iter->ai_addrlen,
+ buf, sizeof buf, NULL, 0, 0);
+ if (err)
+ print_debug ("%s: %s", "getnameinfo", gai_strerror (err));
+ else
+ real_host = buf;
+
+ // XXX: we shouldn't mix these statuses with `struct error'; choose 1!
+ char *address = format_host_port_pair (real_host, port);
+ print_status ("connecting to %s...", address);
+ free (address);
+
+ if (!connect (sockfd, gai_iter->ai_addr, gai_iter->ai_addrlen))
+ break;
+
+ xclose (sockfd);
+ }
+
+ freeaddrinfo (gai_result);
+
+ if (!gai_iter)
+ return error_set (e, "connection failed");
+
+ ctx->irc_fd = sockfd;
+ return true;
+}
+
+// --- Signals -----------------------------------------------------------------
+
+static int g_signal_pipe[2]; ///< A pipe used to signal... signals
+
+static struct strv
+ g_original_argv, ///< Original program arguments
+ g_recovery_env; ///< Environment for re-exec recovery
+
+/// Program termination has been requested by a signal
+static volatile sig_atomic_t g_termination_requested;
+
+/// Points to startup reason location within `g_recovery_environment'
+static char **g_startup_reason_location;
+/// The environment variable used to pass the startup reason when re-executing
+static const char g_startup_reason_str[] = "STARTUP_REASON";
+
+static void
+sigchld_handler (int signum)
+{
+ (void) signum;
+
+ int original_errno = errno;
+ // Just so that the read end of the pipe wakes up the poller.
+ // NOTE: Linux has signalfd() and eventfd(), and the BSD's have kqueue.
+ // All of them are better than this approach, although platform-specific.
+ if (write (g_signal_pipe[1], "c", 1) == -1)
+ soft_assert (errno == EAGAIN);
+ errno = original_errno;
+}
+
+static void
+sigterm_handler (int signum)
+{
+ (void) signum;
+
+ g_termination_requested = true;
+
+ int original_errno = errno;
+ if (write (g_signal_pipe[1], "t", 1) == -1)
+ soft_assert (errno == EAGAIN);
+ errno = original_errno;
+}
+
+static void
+setup_signal_handlers (void)
+{
+ if (pipe (g_signal_pipe) == -1)
+ exit_fatal ("%s: %s", "pipe", strerror (errno));
+
+ set_cloexec (g_signal_pipe[0]);
+ set_cloexec (g_signal_pipe[1]);
+
+ // So that the pipe cannot overflow; it would make write() block within
+ // the signal handler, which is something we really don't want to happen.
+ // The same holds true for read().
+ set_blocking (g_signal_pipe[0], false);
+ set_blocking (g_signal_pipe[1], false);
+
+ struct sigaction sa;
+ sa.sa_flags = SA_RESTART;
+ sa.sa_handler = sigchld_handler;
+ sigemptyset (&sa.sa_mask);
+
+ if (sigaction (SIGCHLD, &sa, NULL) == -1)
+ exit_fatal ("sigaction: %s", strerror (errno));
+
+ signal (SIGPIPE, SIG_IGN);
+
+ sa.sa_handler = sigterm_handler;
+ if (sigaction (SIGINT, &sa, NULL) == -1
+ || sigaction (SIGTERM, &sa, NULL) == -1)
+ exit_fatal ("sigaction: %s", strerror (errno));
+}
+
+static void
+translate_signal_info (int no, const char **name, int code, const char **reason)
+{
+ if (code == SI_USER) *reason = "signal sent by kill()";
+ if (code == SI_QUEUE) *reason = "signal sent by sigqueue()";
+
+ switch (no)
+ {
+ case SIGILL:
+ *name = "SIGILL";
+ if (code == ILL_ILLOPC) *reason = "illegal opcode";
+ if (code == ILL_ILLOPN) *reason = "illegal operand";
+ if (code == ILL_ILLADR) *reason = "illegal addressing mode";
+ if (code == ILL_ILLTRP) *reason = "illegal trap";
+ if (code == ILL_PRVOPC) *reason = "privileged opcode";
+ if (code == ILL_PRVREG) *reason = "privileged register";
+ if (code == ILL_COPROC) *reason = "coprocessor error";
+ if (code == ILL_BADSTK) *reason = "internal stack error";
+ break;
+ case SIGFPE:
+ *name = "SIGFPE";
+ if (code == FPE_INTDIV) *reason = "integer divide by zero";
+ if (code == FPE_INTOVF) *reason = "integer overflow";
+ if (code == FPE_FLTDIV) *reason = "floating-point divide by zero";
+ if (code == FPE_FLTOVF) *reason = "floating-point overflow";
+ if (code == FPE_FLTUND) *reason = "floating-point underflow";
+ if (code == FPE_FLTRES) *reason = "floating-point inexact result";
+ if (code == FPE_FLTINV) *reason = "invalid floating-point operation";
+ if (code == FPE_FLTSUB) *reason = "subscript out of range";
+ break;
+ case SIGSEGV:
+ *name = "SIGSEGV";
+ if (code == SEGV_MAPERR)
+ *reason = "address not mapped to object";
+ if (code == SEGV_ACCERR)
+ *reason = "invalid permissions for mapped object";
+ break;
+ case SIGBUS:
+ *name = "SIGBUS";
+ if (code == BUS_ADRALN) *reason = "invalid address alignment";
+ if (code == BUS_ADRERR) *reason = "nonexistent physical address";
+ if (code == BUS_OBJERR) *reason = "object-specific hardware error";
+ break;
+ default:
+ *name = NULL;
+ }
+}
+
+static void
+recovery_handler (int signum, siginfo_t *info, void *context)
+{
+ (void) context;
+
+ // TODO: maybe try to force a core dump like this: if (fork() == 0) return;
+ // TODO: maybe we could even send "\r\nQUIT :reason\r\n" to the server. >_>
+ // As long as we're not connected via TLS, that is.
+
+ const char *signal_name = NULL, *reason = NULL;
+ translate_signal_info (signum, &signal_name, info->si_code, &reason);
+
+ char buf[128], numbuf[8];
+ if (!signal_name)
+ {
+ snprintf (numbuf, sizeof numbuf, "%d", signum);
+ signal_name = numbuf;
+ }
+
+ if (reason)
+ snprintf (buf, sizeof buf, "%s=%s: %s: %s", g_startup_reason_str,
+ "signal received", signal_name, reason);
+ else
+ snprintf (buf, sizeof buf, "%s=%s: %s", g_startup_reason_str,
+ "signal received", signal_name);
+ *g_startup_reason_location = buf;
+
+ // Avoid annoying resource intensive infinite loops by sleeping for a bit
+ (void) sleep (1);
+
+ // TODO: maybe pregenerate the path, see the following for some other ways
+ // that would be illegal to do from within a signal handler:
+ // http://stackoverflow.com/a/1024937
+ // http://stackoverflow.com/q/799679
+ // Especially if we change the current working directory in the program.
+ //
+ // Note that I can just overwrite g_orig_argv[0].
+
+ // NOTE: our children will read EOF on the read ends of their pipes as a
+ // a result of O_CLOEXEC. That should be enough to make them terminate.
+
+ char **argv = g_original_argv.vector, **argp = g_recovery_env.vector;
+ execve ("/proc/self/exe", argv, argp); // Linux
+ execve ("/proc/curproc/file", argv, argp); // BSD
+ execve ("/proc/curproc/exe", argv, argp); // BSD
+ execve ("/proc/self/path/a.out", argv, argp); // Solaris
+ execve (argv[0], argv, argp); // unreliable fallback
+
+ // Let's just crash
+ perror ("execve");
+ signal (signum, SIG_DFL);
+ raise (signum);
+}
+
+static void
+prepare_recovery_environment (void)
+{
+ g_recovery_env = strv_make ();
+ strv_append_vector (&g_recovery_env, environ);
+
+ // Prepare a location within the environment where we will put the startup
+ // (or maybe rather restart) reason in case of an irrecoverable error.
+ char **iter;
+ for (iter = g_recovery_env.vector; *iter; iter++)
+ {
+ const size_t len = sizeof g_startup_reason_str - 1;
+ if (!strncmp (*iter, g_startup_reason_str, len) && (*iter)[len] == '=')
+ break;
+ }
+
+ if (*iter)
+ g_startup_reason_location = iter;
+ else
+ {
+ g_startup_reason_location = g_recovery_env.vector + g_recovery_env.len;
+ strv_append (&g_recovery_env, "");
+ }
+}
+
+static bool
+setup_recovery_handler (struct bot_context *ctx, struct error **e)
+{
+ bool recover;
+ if (!irc_get_boolean_from_config (ctx, "recover", &recover, e))
+ return false;
+ if (!recover)
+ return true;
+
+ // Make sure these signals aren't blocked, otherwise we would be unable
+ // to handle them, making the critical conditions fatal.
+ sigset_t mask;
+ sigemptyset (&mask);
+ sigaddset (&mask, SIGSEGV);
+ sigaddset (&mask, SIGBUS);
+ sigaddset (&mask, SIGFPE);
+ sigaddset (&mask, SIGILL);
+ sigprocmask (SIG_UNBLOCK, &mask, NULL);
+
+ struct sigaction sa;
+ sa.sa_flags = SA_SIGINFO;
+ sa.sa_sigaction = recovery_handler;
+ sigemptyset (&sa.sa_mask);
+
+ prepare_recovery_environment ();
+
+ // TODO: also handle SIGABRT... or avoid doing abort() in the first place?
+ if (sigaction (SIGSEGV, &sa, NULL) == -1
+ || sigaction (SIGBUS, &sa, NULL) == -1
+ || sigaction (SIGFPE, &sa, NULL) == -1
+ || sigaction (SIGILL, &sa, NULL) == -1)
+ print_error ("sigaction: %s", strerror (errno));
+ return true;
+}
+
+// --- Plugins -----------------------------------------------------------------
+
+/// The name of the special IRC command for interprocess communication
+static const char *plugin_ipc_command = "ZYKLONB";
+
+static struct plugin *
+plugin_find_by_pid (struct bot_context *ctx, pid_t pid)
+{
+ struct plugin *iter;
+ for (iter = ctx->plugins; iter; iter = iter->next)
+ if (iter->pid == pid)
+ return iter;
+ return NULL;
+}
+
+static bool
+plugin_zombify (struct plugin *plugin)
+{
+ if (plugin->is_zombie)
+ return false;
+
+ // FIXME: make sure that we don't remove entries from the poller while we
+ // still may have stuff to read; maybe just check that the read pipe is
+ // empty before closing it... and then on EOF check if `pid == -1' and
+ // only then dispose of it (it'd be best to simulate that both of these
+ // cases may happen).
+ poller_fd_reset (&plugin->write_event);
+
+ // TODO: try to flush the write buffer (non-blocking)?
+
+ // The plugin should terminate itself after it receives EOF.
+ xclose (plugin->write_fd);
+ plugin->write_fd = -1;
+
+ // Make it a pseudo-anonymous zombie. In this state we process any
+ // remaining commands it attempts to send to us before it finally dies.
+ str_map_set (&plugin->ctx->plugins_by_name, plugin->name, NULL);
+ plugin->is_zombie = true;
+
+ // TODO: wait a few seconds and then send SIGKILL to the plugin
+ return true;
+}
+
+static void
+on_plugin_writable (const struct pollfd *fd, struct plugin *plugin)
+{
+ struct str *buf = &plugin->write_buffer;
+ size_t written_total = 0;
+
+ if (fd->revents & ~(POLLOUT | POLLHUP | POLLERR))
+ print_debug ("fd %d: unexpected revents: %d", fd->fd, fd->revents);
+
+ while (written_total != buf->len)
+ {
+ ssize_t n_written = write (fd->fd, buf->str + written_total,
+ buf->len - written_total);
+
+ if (n_written < 0)
+ {
+ if (errno == EAGAIN)
+ break;
+ if (errno == EINTR)
+ continue;
+
+ soft_assert (errno == EPIPE);
+ // Zombies shouldn't get dispatched for writability
+ hard_assert (!plugin->is_zombie);
+
+ print_debug ("%s: %s", "write", strerror (errno));
+ print_error ("failure on writing to plugin `%s',"
+ " therefore I'm unloading it", plugin->name);
+ plugin_zombify (plugin);
+ break;
+ }
+
+ // This may be equivalent to EAGAIN on some implementations
+ if (n_written == 0)
+ break;
+
+ written_total += n_written;
+ }
+
+ if (written_total != 0)
+ str_remove_slice (buf, 0, written_total);
+
+ if (buf->len == 0)
+ // Everything has been written, there's no need to end up in here again
+ poller_fd_reset (&plugin->write_event);
+}
+
+static void
+plugin_queue_write (struct plugin *plugin)
+{
+ if (plugin->is_zombie)
+ return;
+
+ // Don't let the write buffer grow indefinitely. If there's a ton of data
+ // waiting to be processed by the plugin, it usually means there's something
+ // wrong with it (such as someone stopping the process).
+ if (plugin->write_buffer.len >= (1 << 20))
+ {
+ print_warning ("plugin `%s' does not seem to process messages fast"
+ " enough, I'm unloading it", plugin->name);
+ plugin_zombify (plugin);
+ return;
+ }
+ poller_fd_set (&plugin->write_event, POLLOUT);
+}
+
+static void
+plugin_send (struct plugin *plugin, const char *format, ...)
+ ATTRIBUTE_PRINTF (2, 3);
+
+static void
+plugin_send (struct plugin *plugin, const char *format, ...)
+{
+ va_list ap;
+
+ if (g_debug_mode)
+ {
+ fprintf (stderr, "[%s] <-- \"", plugin->name);
+ va_start (ap, format);
+ vfprintf (stderr, format, ap);
+ va_end (ap);
+ fputs ("\"\n", stderr);
+ }
+
+ va_start (ap, format);
+ str_append_vprintf (&plugin->write_buffer, format, ap);
+ va_end (ap);
+ str_append (&plugin->write_buffer, "\r\n");
+
+ plugin_queue_write (plugin);
+}
+
+static void
+plugin_process_ipc (struct plugin *plugin, const struct irc_message *msg)
+{
+ // Replies are sent in the order in which they came in, so there's
+ // no need to attach a special identifier to them. It might be
+ // desirable in some cases, though.
+
+ if (msg->params.len < 1)
+ return;
+
+ const char *command = msg->params.vector[0];
+ if (!plugin->initialized && !strcasecmp (command, "register"))
+ {
+ // Register for relaying of IRC traffic
+ plugin->initialized = true;
+
+ // Flush any queued up traffic here. The point of queuing it in
+ // the first place is so that we don't have to wait for plugin
+ // initialization during startup.
+ //
+ // Note that if we start filtering data coming to the plugins e.g.
+ // based on what it tells us upon registration, we might need to
+ // filter `queued_output' as well.
+ str_append_str (&plugin->write_buffer, &plugin->queued_output);
+ str_free (&plugin->queued_output);
+
+ // NOTE: this may trigger the buffer length check
+ plugin_queue_write (plugin);
+ }
+ else if (!strcasecmp (command, "get_config"))
+ {
+ if (msg->params.len < 2)
+ return;
+
+ const char *value =
+ str_map_find (&plugin->ctx->config, msg->params.vector[1]);
+ // TODO: escape the value (although there's no need to ATM)
+ plugin_send (plugin, "%s :%s",
+ plugin_ipc_command, value ? value : "");
+ }
+ else if (!strcasecmp (command, "print"))
+ {
+ if (msg->params.len < 2)
+ return;
+
+ printf ("%s\n", msg->params.vector[1]);
+ }
+}
+
+static void
+plugin_process_message (const struct irc_message *msg,
+ const char *raw, void *user_data)
+{
+ struct plugin *plugin = user_data;
+ struct bot_context *ctx = plugin->ctx;
+
+ if (g_debug_mode)
+ fprintf (stderr, "[%s] --> \"%s\"\n", plugin->name, raw);
+
+ if (!strcasecmp (msg->command, plugin_ipc_command))
+ plugin_process_ipc (plugin, msg);
+ else if (plugin->initialized && ctx->irc_registered)
+ {
+ // Pass everything else through to the IRC server
+ // XXX: when the server isn't ready yet, these messages get silently
+ // discarded, which shouldn't pose a problem most of the time.
+ // Perhaps we could send a "connected" notification on `register'
+ // if `irc_ready' is true, or after it becomes true later, so that
+ // plugins know when to start sending unprovoked IRC messages.
+ // XXX: another case is when the connection gets interrupted and the
+ // plugin tries to send something back while we're reconnecting.
+ // For that we might set up a global buffer that gets flushed out
+ // after `irc_ready' becomes true. Note that there is always some
+ // chance of messages getting lost without us even noticing it.
+ irc_send (ctx, "%s", raw);
+ }
+}
+
+static void
+on_plugin_readable (const struct pollfd *fd, struct plugin *plugin)
+{
+ if (fd->revents & ~(POLLIN | POLLHUP | POLLERR))
+ print_debug ("fd %d: unexpected revents: %d", fd->fd, fd->revents);
+
+ // TODO: see if I can reuse irc_fill_read_buffer()
+ struct str *buf = &plugin->read_buffer;
+ while (true)
+ {
+ str_reserve (buf, 512 + 1);
+ ssize_t n_read = read (fd->fd, buf->str + buf->len,
+ buf->alloc - buf->len - 1);
+
+ if (n_read < 0)
+ {
+ if (errno == EAGAIN)
+ break;
+ if (soft_assert (errno == EINTR))
+ continue;
+
+ if (!plugin->is_zombie)
+ {
+ print_error ("failure on reading from plugin `%s',"
+ " therefore I'm unloading it", plugin->name);
+ plugin_zombify (plugin);
+ }
+ return;
+ }
+
+ // EOF; hopefully it will die soon (maybe it already has)
+ if (n_read == 0)
+ break;
+
+ buf->str[buf->len += n_read] = '\0';
+ if (buf->len >= (1 << 20))
+ {
+ // XXX: this isn't really the best flood prevention mechanism,
+ // but it wasn't even supposed to be one.
+ if (plugin->is_zombie)
+ {
+ print_error ("a zombie of plugin `%s' is trying to flood us,"
+ " therefore I'm killing it", plugin->name);
+ kill (plugin->pid, SIGKILL);
+ }
+ else
+ {
+ print_error ("plugin `%s' seems to spew out data frantically,"
+ " therefore I'm unloading it", plugin->name);
+ plugin_zombify (plugin);
+ }
+ return;
+ }
+ }
+
+ irc_process_buffer (buf, plugin_process_message, plugin);
+}
+
+static bool
+is_valid_plugin_name (const char *name)
+{
+ if (!*name)
+ return false;
+ for (const char *p = name; *p; p++)
+ if (!isgraph (*p) || *p == '/')
+ return false;
+ return true;
+}
+
+static char *
+plugin_resolve_relative_filename (const char *filename)
+{
+ struct strv paths = strv_make ();
+ get_xdg_data_dirs (&paths);
+ char *result = resolve_relative_filename_generic
+ (&paths, PROGRAM_NAME "/plugins/", filename);
+ strv_free (&paths);
+ return result;
+}
+
+static struct plugin *
+plugin_launch (struct bot_context *ctx, const char *name, struct error **e)
+{
+ char *path = NULL;
+ const char *plugin_dir = str_map_find (&ctx->config, "plugin_dir");
+ if (plugin_dir)
+ {
+ // resolve_relative_filename_generic() won't accept relative paths,
+ // so just keep the old behaviour and expect the file to exist.
+ // We could use resolve_filename() on "plugin_dir" with paths=getcwd().
+ path = xstrdup_printf ("%s/%s", plugin_dir, name);
+ }
+ else if (!(path = plugin_resolve_relative_filename (name)))
+ {
+ error_set (e, "plugin not found");
+ goto fail_0;
+ }
+
+ int stdin_pipe[2];
+ if (pipe (stdin_pipe) == -1)
+ {
+ error_set (e, "%s: %s", "pipe", strerror (errno));
+ goto fail_0;
+ }
+
+ int stdout_pipe[2];
+ if (pipe (stdout_pipe) == -1)
+ {
+ error_set (e, "%s: %s", "pipe", strerror (errno));
+ goto fail_1;
+ }
+
+ struct str work_dir = str_make ();
+ get_xdg_home_dir (&work_dir, "XDG_DATA_HOME", ".local/share");
+ str_append_printf (&work_dir, "/%s", PROGRAM_NAME);
+
+ if (!mkdir_with_parents (work_dir.str, e))
+ goto fail_2;
+
+ set_cloexec (stdin_pipe[1]);
+ set_cloexec (stdout_pipe[0]);
+
+ pid_t pid = fork ();
+ if (pid == -1)
+ {
+ error_set (e, "%s: %s", "fork", strerror (errno));
+ goto fail_2;
+ }
+
+ if (pid == 0)
+ {
+ // Redirect the child's stdin and stdout to the pipes
+ if (dup2 (stdin_pipe[0], STDIN_FILENO) == -1
+ || dup2 (stdout_pipe[1], STDOUT_FILENO) == -1)
+ {
+ print_error ("%s: %s: %s", "failed to load the plugin",
+ "dup2", strerror (errno));
+ _exit (EXIT_FAILURE);
+ }
+ if (chdir (work_dir.str))
+ {
+ print_error ("%s: %s: %s", "failed to load the plugin",
+ "chdir", strerror (errno));
+ _exit (EXIT_FAILURE);
+ }
+
+ xclose (stdin_pipe[0]);
+ xclose (stdout_pipe[1]);
+
+ // Restore some of the signal handling
+ signal (SIGPIPE, SIG_DFL);
+
+ char *argv[] = { path, NULL };
+ execve (argv[0], argv, environ);
+
+ // We will collect the failure later via SIGCHLD
+ print_error ("%s: %s: %s", "failed to load the plugin",
+ "exec", strerror (errno));
+ _exit (EXIT_FAILURE);
+ }
+
+ str_free (&work_dir);
+ free (path);
+
+ xclose (stdin_pipe[0]);
+ xclose (stdout_pipe[1]);
+
+ struct plugin *plugin = plugin_new ();
+ plugin->ctx = ctx;
+ plugin->pid = pid;
+ plugin->name = xstrdup (name);
+ plugin->read_fd = stdout_pipe[0];
+ plugin->write_fd = stdin_pipe[1];
+ return plugin;
+
+fail_2:
+ str_free (&work_dir);
+ xclose (stdout_pipe[0]);
+ xclose (stdout_pipe[1]);
+fail_1:
+ xclose (stdin_pipe[0]);
+ xclose (stdin_pipe[1]);
+fail_0:
+ free (path);
+ return NULL;
+}
+
+static bool
+plugin_load (struct bot_context *ctx, const char *name, struct error **e)
+{
+ if (!is_valid_plugin_name (name))
+ return error_set (e, "invalid plugin name");
+ if (str_map_find (&ctx->plugins_by_name, name))
+ return error_set (e, "the plugin has already been loaded");
+
+ struct plugin *plugin;
+ if (!(plugin = plugin_launch (ctx, name, e)))
+ return false;
+
+ set_blocking (plugin->read_fd, false);
+ set_blocking (plugin->write_fd, false);
+
+ plugin->read_event = poller_fd_make (&ctx->poller, plugin->read_fd);
+ plugin->read_event.dispatcher = (poller_fd_fn) on_plugin_readable;
+ plugin->read_event.user_data = plugin;
+
+ plugin->write_event = poller_fd_make (&ctx->poller, plugin->write_fd);
+ plugin->write_event.dispatcher = (poller_fd_fn) on_plugin_writable;
+ plugin->write_event.user_data = plugin;
+
+ LIST_PREPEND (ctx->plugins, plugin);
+ str_map_set (&ctx->plugins_by_name, name, plugin);
+
+ poller_fd_set (&plugin->read_event, POLLIN);
+ return true;
+}
+
+static bool
+plugin_unload (struct bot_context *ctx, const char *name, struct error **e)
+{
+ struct plugin *plugin = str_map_find (&ctx->plugins_by_name, name);
+
+ if (!plugin)
+ return error_set (e, "no such plugin is loaded");
+
+ plugin_zombify (plugin);
+
+ // TODO: add a `kill zombies' command to forcefully get rid of processes
+ // that do not understand the request.
+ return true;
+}
+
+static void
+plugin_load_all_from_config (struct bot_context *ctx)
+{
+ const char *plugin_list = str_map_find (&ctx->config, "plugins");
+ if (!plugin_list)
+ return;
+
+ struct strv plugins = strv_make ();
+ cstr_split (plugin_list, ",", true, &plugins);
+ for (size_t i = 0; i < plugins.len; i++)
+ {
+ char *name = cstr_strip_in_place (plugins.vector[i], " ");
+
+ struct error *e = NULL;
+ if (!plugin_load (ctx, name, &e))
+ {
+ print_error ("plugin `%s' failed to load: %s", name, e->message);
+ error_free (e);
+ }
+ }
+
+ strv_free (&plugins);
+}
+
+// --- Main program ------------------------------------------------------------
+
+static bool
+parse_bot_command (const char *s, const char *command, const char **following)
+{
+ size_t command_len = strlen (command);
+ if (strncasecmp (s, command, command_len))
+ return false;
+ s += command_len;
+
+ // Expect a word boundary, so that we don't respond to invalid things
+ if (isalnum (*s))
+ return false;
+
+ // Ignore any initial spaces; the rest is the command's argument
+ while (isblank (*s))
+ s++;
+ *following = s;
+ return true;
+}
+
+static void
+split_bot_command_argument_list (const char *arguments, struct strv *out)
+{
+ cstr_split (arguments, ",", true, out);
+ for (size_t i = 0; i < out->len; )
+ {
+ if (!*cstr_strip_in_place (out->vector[i], " \t"))
+ strv_remove (out, i);
+ else
+ i++;
+ }
+}
+
+static bool
+is_private_message (const struct irc_message *msg)
+{
+ hard_assert (msg->params.len);
+ return !strchr ("#&+!", *msg->params.vector[0]);
+}
+
+static bool
+is_sent_by_admin (struct bot_context *ctx, const struct irc_message *msg)
+{
+ // No administrator set -> everyone is an administrator
+ if (!ctx->admin_re)
+ return true;
+ return regexec (ctx->admin_re, msg->prefix, 0, NULL, 0) != REG_NOMATCH;
+}
+
+static void respond_to_user (struct bot_context *ctx, const struct
+ irc_message *msg, const char *format, ...) ATTRIBUTE_PRINTF (3, 4);
+
+static void
+respond_to_user (struct bot_context *ctx, const struct irc_message *msg,
+ const char *format, ...)
+{
+ if (!soft_assert (msg->prefix && msg->params.len))
+ return;
+
+ char nick[strcspn (msg->prefix, "!") + 1];
+ strncpy (nick, msg->prefix, sizeof nick - 1);
+ nick[sizeof nick - 1] = '\0';
+
+ va_list ap;
+ struct str text = str_make ();
+ va_start (ap, format);
+ str_append_vprintf (&text, format, ap);
+ va_end (ap);
+
+ if (is_private_message (msg))
+ irc_send (ctx, "PRIVMSG %s :%s", nick, text.str);
+ else
+ irc_send (ctx, "PRIVMSG %s :%s: %s",
+ msg->params.vector[0], nick, text.str);
+
+ str_free (&text);
+}
+
+static void
+process_plugin_load (struct bot_context *ctx,
+ const struct irc_message *msg, const char *name)
+{
+ struct error *e = NULL;
+ if (plugin_load (ctx, name, &e))
+ respond_to_user (ctx, msg, "plugin `%s' queued for loading", name);
+ else
+ {
+ respond_to_user (ctx, msg, "plugin `%s' could not be loaded: %s",
+ name, e->message);
+ error_free (e);
+ }
+}
+
+static void
+process_plugin_unload (struct bot_context *ctx,
+ const struct irc_message *msg, const char *name)
+{
+ struct error *e = NULL;
+ if (plugin_unload (ctx, name, &e))
+ respond_to_user (ctx, msg, "plugin `%s' unloaded", name);
+ else
+ {
+ respond_to_user (ctx, msg, "plugin `%s' could not be unloaded: %s",
+ name, e->message);
+ error_free (e);
+ }
+}
+
+static void
+process_plugin_reload (struct bot_context *ctx,
+ const struct irc_message *msg, const char *name)
+{
+ // XXX: we might want to wait until the plugin terminates before we try
+ // to reload it (so that it can save its configuration or whatever)
+
+ // So far the only error that can occur is that the plugin hasn't been
+ // loaded, which in this case doesn't really matter.
+ plugin_unload (ctx, name, NULL);
+
+ process_plugin_load (ctx, msg, name);
+}
+
+static char *
+make_status_report (struct bot_context *ctx)
+{
+ struct str report = str_make ();
+ const char *reason = getenv (g_startup_reason_str);
+ if (!reason)
+ reason = "launched normally";
+ str_append_printf (&report, "\x02startup reason:\x0f %s", reason);
+
+ size_t zombies = 0;
+ const char *prepend = "; \x02plugins:\x0f ";
+ for (struct plugin *plugin = ctx->plugins; plugin; plugin = plugin->next)
+ {
+ if (plugin->is_zombie)
+ zombies++;
+ else
+ {
+ str_append_printf (&report, "%s%s", prepend, plugin->name);
+ prepend = ", ";
+ }
+ }
+ if (!ctx->plugins)
+ str_append_printf (&report, "%s\x02none\x0f", prepend);
+
+ str_append_printf (&report, "; \x02zombies:\x0f %zu", zombies);
+ return str_steal (&report);
+}
+
+static void
+process_privmsg (struct bot_context *ctx, const struct irc_message *msg)
+{
+ if (!is_sent_by_admin (ctx, msg))
+ return;
+ if (msg->params.len < 2)
+ return;
+
+ const char *prefix = str_map_find (&ctx->config, "prefix");
+ hard_assert (prefix != NULL); // We have a default value for this
+
+ // For us to recognize the command, it has to start with the prefix,
+ // with the exception of PM's sent directly to us.
+ const char *text = msg->params.vector[1];
+ if (!strncmp (text, prefix, strlen (prefix)))
+ text += strlen (prefix);
+ else if (!is_private_message (msg))
+ return;
+
+ const char *following;
+ struct strv list = strv_make ();
+
+ if (parse_bot_command (text, "quote", &following))
+ // This seems to replace tons of random stupid commands
+ irc_send (ctx, "%s", following);
+ else if (parse_bot_command (text, "quit", &following))
+ {
+ // We actually need this command (instead of just `quote') because we
+ // could try to reconnect to the server automatically otherwise.
+ if (*following)
+ irc_send (ctx, "QUIT :%s", following);
+ else
+ irc_send (ctx, "QUIT");
+ initiate_quit (ctx);
+ }
+ else if (parse_bot_command (text, "status", &following))
+ {
+ char *report = make_status_report (ctx);
+ respond_to_user (ctx, msg, "%s", report);
+ free (report);
+ }
+ else if (parse_bot_command (text, "load", &following))
+ {
+ split_bot_command_argument_list (following, &list);
+ for (size_t i = 0; i < list.len; i++)
+ process_plugin_load (ctx, msg, list.vector[i]);
+ }
+ else if (parse_bot_command (text, "reload", &following))
+ {
+ split_bot_command_argument_list (following, &list);
+ for (size_t i = 0; i < list.len; i++)
+ process_plugin_reload (ctx, msg, list.vector[i]);
+ }
+ else if (parse_bot_command (text, "unload", &following))
+ {
+ split_bot_command_argument_list (following, &list);
+ for (size_t i = 0; i < list.len; i++)
+ process_plugin_unload (ctx, msg, list.vector[i]);
+ }
+
+ strv_free (&list);
+}
+
+static void
+irc_forward_message_to_plugins (struct bot_context *ctx, const char *raw)
+{
+ // For consistency with plugin_process_message()
+ if (!ctx->irc_registered)
+ return;
+
+ for (struct plugin *plugin = ctx->plugins;
+ plugin; plugin = plugin->next)
+ {
+ if (plugin->is_zombie)
+ continue;
+
+ if (plugin->initialized)
+ plugin_send (plugin, "%s", raw);
+ else
+ // TODO: make sure that this buffer doesn't get too large either
+ str_append_printf (&plugin->queued_output, "%s\r\n", raw);
+ }
+}
+
+static void
+irc_process_message (const struct irc_message *msg,
+ const char *raw, void *user_data)
+{
+ struct bot_context *ctx = user_data;
+ if (g_debug_mode)
+ fprintf (stderr, "[%s] ==> \"%s\"\n", "IRC", raw);
+
+ // This should be as minimal as possible, I don't want to have the whole bot
+ // written in C, especially when I have this overengineered plugin system.
+ // Therefore the very basic functionality only.
+ //
+ // I should probably even rip out the autojoin...
+
+ irc_forward_message_to_plugins (ctx, raw);
+
+ if (!strcasecmp (msg->command, "PING"))
+ {
+ if (msg->params.len)
+ irc_send (ctx, "PONG :%s", msg->params.vector[0]);
+ else
+ irc_send (ctx, "PONG");
+ }
+ else if (!ctx->irc_registered && !strcasecmp (msg->command, "001"))
+ {
+ print_status ("successfully connected");
+ ctx->irc_registered = true;
+
+ const char *autojoin = str_map_find (&ctx->config, "autojoin");
+ if (autojoin)
+ irc_send (ctx, "JOIN :%s", autojoin);
+ }
+ else if (!strcasecmp (msg->command, "PRIVMSG"))
+ process_privmsg (ctx, msg);
+}
+
+enum irc_read_result
+{
+ IRC_READ_OK, ///< Some data were read successfully
+ IRC_READ_EOF, ///< The server has closed connection
+ IRC_READ_AGAIN, ///< No more data at the moment
+ IRC_READ_ERROR ///< General connection failure
+};
+
+static enum irc_read_result
+irc_fill_read_buffer_tls (struct bot_context *ctx, struct str *buf)
+{
+ int n_read;
+start:
+ ERR_clear_error ();
+ n_read = SSL_read (ctx->ssl, buf->str + buf->len,
+ buf->alloc - buf->len - 1 /* null byte */);
+
+ const char *error_info = NULL;
+ switch (xssl_get_error (ctx->ssl, n_read, &error_info))
+ {
+ case SSL_ERROR_NONE:
+ buf->str[buf->len += n_read] = '\0';
+ return IRC_READ_OK;
+ case SSL_ERROR_ZERO_RETURN:
+ return IRC_READ_EOF;
+ case SSL_ERROR_WANT_READ:
+ return IRC_READ_AGAIN;
+ case SSL_ERROR_WANT_WRITE:
+ {
+ // Let it finish the handshake as we don't poll for writability;
+ // any errors are to be collected by SSL_read() in the next iteration
+ struct pollfd pfd = { .fd = ctx->irc_fd, .events = POLLOUT };
+ soft_assert (poll (&pfd, 1, 0) > 0);
+ goto start;
+ }
+ case XSSL_ERROR_TRY_AGAIN:
+ goto start;
+ default:
+ print_debug ("%s: %s: %s", __func__, "SSL_read", error_info);
+ return IRC_READ_ERROR;
+ }
+}
+
+static enum irc_read_result
+irc_fill_read_buffer (struct bot_context *ctx, struct str *buf)
+{
+ ssize_t n_read;
+start:
+ n_read = recv (ctx->irc_fd, buf->str + buf->len,
+ buf->alloc - buf->len - 1 /* null byte */, 0);
+
+ if (n_read > 0)
+ {
+ buf->str[buf->len += n_read] = '\0';
+ return IRC_READ_OK;
+ }
+ if (n_read == 0)
+ return IRC_READ_EOF;
+
+ if (errno == EAGAIN)
+ return IRC_READ_AGAIN;
+ if (errno == EINTR)
+ goto start;
+
+ print_debug ("%s: %s: %s", __func__, "recv", strerror (errno));
+ return IRC_READ_ERROR;
+}
+
+static bool irc_connect (struct bot_context *, struct error **);
+static void irc_queue_reconnect (struct bot_context *);
+
+static void
+irc_cancel_timers (struct bot_context *ctx)
+{
+ poller_timer_reset (&ctx->timeout_tmr);
+ poller_timer_reset (&ctx->ping_tmr);
+ poller_timer_reset (&ctx->reconnect_tmr);
+}
+
+static void
+on_irc_reconnect_timeout (void *user_data)
+{
+ struct bot_context *ctx = user_data;
+
+ struct error *e = NULL;
+ if (irc_connect (ctx, &e))
+ {
+ // TODO: inform plugins about the new connection
+ return;
+ }
+
+ print_error ("%s", e->message);
+ error_free (e);
+ irc_queue_reconnect (ctx);
+}
+
+static void
+irc_queue_reconnect (struct bot_context *ctx)
+{
+ hard_assert (ctx->irc_fd == -1);
+ print_status ("trying to reconnect in %ld seconds...",
+ ctx->reconnect_delay);
+ poller_timer_set (&ctx->reconnect_tmr, ctx->reconnect_delay * 1000);
+}
+
+static void
+on_irc_disconnected (struct bot_context *ctx)
+{
+ // Get rid of the dead socket and related things
+ if (ctx->ssl)
+ {
+ SSL_free (ctx->ssl);
+ ctx->ssl = NULL;
+ SSL_CTX_free (ctx->ssl_ctx);
+ ctx->ssl_ctx = NULL;
+ }
+
+ poller_fd_reset (&ctx->irc_event);
+ xclose (ctx->irc_fd);
+ ctx->irc_fd = -1;
+ ctx->irc_registered = false;
+
+ // TODO: inform plugins about the disconnect event
+
+ // All of our timers have lost their meaning now
+ irc_cancel_timers (ctx);
+
+ if (ctx->quitting)
+ try_finish_quit (ctx);
+ else if (!ctx->reconnect)
+ initiate_quit (ctx);
+ else
+ irc_queue_reconnect (ctx);
+}
+
+static void
+on_irc_ping_timeout (void *user_data)
+{
+ struct bot_context *ctx = user_data;
+ print_error ("connection timeout");
+ on_irc_disconnected (ctx);
+}
+
+static void
+on_irc_timeout (void *user_data)
+{
+ // Provoke a response from the server
+ struct bot_context *ctx = user_data;
+ irc_send (ctx, "PING :%s",
+ (char *) str_map_find (&ctx->config, "nickname"));
+}
+
+static void
+irc_reset_connection_timeouts (struct bot_context *ctx)
+{
+ irc_cancel_timers (ctx);
+ poller_timer_set (&ctx->timeout_tmr, 3 * 60 * 1000);
+ poller_timer_set (&ctx->ping_tmr, (3 * 60 + 30) * 1000);
+}
+
+static void
+on_irc_readable (const struct pollfd *fd, struct bot_context *ctx)
+{
+ if (fd->revents & ~(POLLIN | POLLHUP | POLLERR))
+ print_debug ("fd %d: unexpected revents: %d", fd->fd, fd->revents);
+
+ (void) set_blocking (ctx->irc_fd, false);
+
+ struct str *buf = &ctx->read_buffer;
+ enum irc_read_result (*fill_buffer)(struct bot_context *, struct str *)
+ = ctx->ssl
+ ? irc_fill_read_buffer_tls
+ : irc_fill_read_buffer;
+ bool disconnected = false;
+ while (true)
+ {
+ str_reserve (buf, 512);
+ switch (fill_buffer (ctx, buf))
+ {
+ case IRC_READ_AGAIN:
+ goto end;
+ case IRC_READ_ERROR:
+ print_error ("reading from the IRC server failed");
+ disconnected = true;
+ goto end;
+ case IRC_READ_EOF:
+ print_status ("the IRC server closed the connection");
+ disconnected = true;
+ goto end;
+ case IRC_READ_OK:
+ break;
+ }
+
+ if (buf->len >= (1 << 20))
+ {
+ print_error ("the IRC server seems to spew out data frantically");
+ irc_shutdown (ctx);
+ goto end;
+ }
+ }
+end:
+ (void) set_blocking (ctx->irc_fd, true);
+ irc_process_buffer (buf, irc_process_message, ctx);
+
+ if (disconnected)
+ on_irc_disconnected (ctx);
+ else
+ irc_reset_connection_timeouts (ctx);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+// The bot is currently mostly synchronous (which also makes it shorter),
+// however our current SOCKS code is not, hence we must wrap it.
+
+struct irc_socks_data
+{
+ struct bot_context *ctx; ///< Bot context
+ struct poller inner_poller; ///< Special inner poller
+ bool polling; ///< Inner poller is no longer needed
+ struct socks_connector connector; ///< SOCKS connector
+ bool succeeded; ///< Were we successful in connecting?
+};
+
+static void
+irc_on_socks_connected (void *user_data, int socket, const char *hostname)
+{
+ (void) hostname;
+
+ struct irc_socks_data *data = user_data;
+ data->ctx->irc_fd = socket;
+ data->succeeded = true;
+ data->polling = false;
+}
+
+static void
+irc_on_socks_failure (void *user_data)
+{
+ struct irc_socks_data *data = user_data;
+ data->succeeded = false;
+ data->polling = false;
+}
+
+static void
+irc_on_socks_connecting (void *user_data,
+ const char *address, const char *via, const char *version)
+{
+ (void) user_data;
+ print_status ("connecting to %s via %s (%s)...", address, via, version);
+}
+
+static void
+irc_on_socks_error (void *user_data, const char *error)
+{
+ (void) user_data;
+ print_error ("%s: %s", "SOCKS connection failed", error);
+}
+
+static bool
+irc_establish_connection_socks (struct bot_context *ctx,
+ const char *socks_host, const char *socks_port,
+ const char *host, const char *service, struct error **e)
+{
+ struct irc_socks_data data;
+ struct poller *poller = &data.inner_poller;
+ struct socks_connector *connector = &data.connector;
+
+ data.ctx = ctx;
+ poller_init (poller);
+ data.polling = true;
+ socks_connector_init (connector, poller);
+ data.succeeded = false;
+
+ connector->on_connected = irc_on_socks_connected;
+ connector->on_connecting = irc_on_socks_connecting;
+ connector->on_error = irc_on_socks_error;
+ connector->on_failure = irc_on_socks_failure;
+ connector->user_data = &data;
+
+ if (socks_connector_add_target (connector, host, service, e))
+ {
+ socks_connector_run (connector, socks_host, socks_port,
+ str_map_find (&ctx->config, "socks_username"),
+ str_map_find (&ctx->config, "socks_password"));
+ while (data.polling)
+ poller_run (poller);
+ if (!data.succeeded)
+ error_set (e, "connection failed");
+ }
+
+ socks_connector_free (connector);
+ poller_free (poller);
+ return data.succeeded;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static bool
+irc_connect (struct bot_context *ctx, struct error **e)
+{
+ const char *irc_host = str_map_find (&ctx->config, "irc_host");
+ const char *irc_port = str_map_find (&ctx->config, "irc_port");
+ const char *socks_host = str_map_find (&ctx->config, "socks_host");
+ const char *socks_port = str_map_find (&ctx->config, "socks_port");
+
+ const char *nickname = str_map_find (&ctx->config, "nickname");
+ const char *username = str_map_find (&ctx->config, "username");
+ const char *realname = str_map_find (&ctx->config, "realname");
+
+ // We have a default value for these
+ hard_assert (irc_port && socks_port);
+ hard_assert (nickname && username && realname);
+
+ // TODO: again, get rid of `struct error' in here. The question is: how
+ // do we tell our caller that he should not try to reconnect?
+ if (!irc_host)
+ return error_set (e, "no hostname specified in configuration");
+
+ bool use_tls;
+ if (!irc_get_boolean_from_config (ctx, "tls", &use_tls, e))
+ return false;
+
+ bool connected = socks_host
+ ? irc_establish_connection_socks (ctx,
+ socks_host, socks_port, irc_host, irc_port, e)
+ : irc_establish_connection (ctx, irc_host, irc_port, e);
+ if (!connected)
+ return false;
+
+ if (use_tls && !irc_initialize_tls (ctx, e))
+ {
+ xclose (ctx->irc_fd);
+ ctx->irc_fd = -1;
+ return false;
+ }
+ print_status ("connection established");
+
+ ctx->irc_event = poller_fd_make (&ctx->poller, ctx->irc_fd);
+ ctx->irc_event.dispatcher = (poller_fd_fn) on_irc_readable;
+ ctx->irc_event.user_data = ctx;
+
+ // TODO: in exec try: 1/ set blocking, 2/ setsockopt() SO_LINGER,
+ // (struct linger) { .l_onoff = true; .l_linger = 1 /* 1s should do */; }
+ // 3/ /* O_CLOEXEC */ But only if the QUIT message proves unreliable.
+ poller_fd_set (&ctx->irc_event, POLLIN);
+ irc_reset_connection_timeouts (ctx);
+
+ irc_send (ctx, "NICK %s", nickname);
+ irc_send (ctx, "USER %s 8 * :%s", username, realname);
+ return true;
+}
+
+static bool
+parse_config (struct bot_context *ctx, struct error **e)
+{
+ if (!irc_get_boolean_from_config (ctx, "reconnect", &ctx->reconnect, e))
+ return false;
+
+ const char *delay_str = str_map_find (&ctx->config, "reconnect_delay");
+ hard_assert (delay_str != NULL); // We have a default value for this
+ if (!xstrtoul (&ctx->reconnect_delay, delay_str, 10))
+ {
+ return error_set (e,
+ "invalid configuration value for `%s'", "reconnect_delay");
+ }
+
+ hard_assert (!ctx->admin_re);
+ const char *admin = str_map_find (&ctx->config, "admin");
+ if (!admin)
+ return true;
+
+ struct error *error = NULL;
+ ctx->admin_re = regex_compile (admin, REG_EXTENDED | REG_NOSUB, &error);
+ if (!error)
+ return true;
+
+ error_set (e, "invalid configuration value for `%s': %s",
+ "admin", error->message);
+ error_free (error);
+ return false;
+}
+
+static void
+on_plugin_death (struct plugin *plugin, int status)
+{
+ struct bot_context *ctx = plugin->ctx;
+
+ // TODO: callbacks on children death, so that we may tell the user
+ // "plugin `name' died"; use `status'
+ if (!plugin->is_zombie && WIFSIGNALED (status))
+ {
+ const char *notes = "";
+#ifdef WCOREDUMP
+ if (WCOREDUMP (status))
+ notes = " (core dumped)";
+#endif
+ print_warning ("Plugin `%s' died from signal %d%s",
+ plugin->name, WTERMSIG (status), notes);
+ }
+
+ // Let's go through the zombie state to simplify things a bit
+ // TODO: might not be a completely bad idea to restart the plugin
+ plugin_zombify (plugin);
+
+ plugin->pid = -1;
+
+ // In theory we could close `read_fd', set `read_event->closed' to true
+ // and expect epoll to no longer return events for the descriptor, as
+ // all the pipe ends should be closed by then (the child is dead, so its
+ // pipe FDs have been closed [assuming it hasn't forked without closing
+ // the descriptors, which would be evil], and we would have closed all
+ // of our FDs for this pipe as well). In practice that doesn't work.
+ poller_fd_reset (&plugin->read_event);
+
+ xclose (plugin->read_fd);
+ plugin->read_fd = -1;
+
+ LIST_UNLINK (ctx->plugins, plugin);
+ plugin_destroy (plugin);
+
+ // Living child processes block us from quitting
+ try_finish_quit (ctx);
+}
+
+static bool
+try_reap_plugin (struct bot_context *ctx)
+{
+ int status;
+ pid_t zombie = waitpid (-1, &status, WNOHANG);
+
+ if (zombie == -1)
+ {
+ // No children to wait on
+ if (errno == ECHILD)
+ return false;
+
+ hard_assert (errno == EINTR);
+ return true;
+ }
+
+ if (zombie == 0)
+ return false;
+
+ struct plugin *plugin = plugin_find_by_pid (ctx, zombie);
+ // XXX: re-exec if something has died that we don't recognize?
+ if (soft_assert (plugin != NULL))
+ on_plugin_death (plugin, status);
+ return true;
+}
+
+static void
+kill_all_zombies (struct bot_context *ctx)
+{
+ for (struct plugin *plugin = ctx->plugins; plugin; plugin = plugin->next)
+ {
+ if (!plugin->is_zombie)
+ continue;
+
+ print_status ("forcefully killing a zombie of `%s' (PID %d)",
+ plugin->name, (int) plugin->pid);
+ kill (plugin->pid, SIGKILL);
+ }
+}
+
+static void
+on_signal_pipe_readable (const struct pollfd *fd, struct bot_context *ctx)
+{
+ char dummy;
+ (void) read (fd->fd, &dummy, 1);
+
+ if (g_termination_requested)
+ {
+ g_termination_requested = false;
+ if (!ctx->quitting)
+ {
+ // There may be a timer set to reconnect to the server
+ irc_cancel_timers (ctx);
+
+ if (ctx->irc_fd != -1)
+ irc_send (ctx, "QUIT :Terminated by signal");
+ initiate_quit (ctx);
+ }
+ else
+ // Disregard proper termination, just kill all the children
+ kill_all_zombies (ctx);
+ }
+
+ // Reap all dead children (since the signal pipe may overflow etc. we run
+ // waitpid() in a loop to return all the zombies it knows about).
+ while (try_reap_plugin (ctx))
+ ;
+}
+
+int
+main (int argc, char *argv[])
+{
+ g_original_argv = strv_make ();
+ strv_append_vector (&g_original_argv, argv);
+
+ static const struct opt opts[] =
+ {
+ { 'd', "debug", NULL, 0, "run in debug mode" },
+ { 'h', "help", NULL, 0, "display this help and exit" },
+ { 'V', "version", NULL, 0, "output version information and exit" },
+ { 'w', "write-default-cfg", "FILENAME",
+ OPT_OPTIONAL_ARG | OPT_LONG_ONLY,
+ "write a default configuration file and exit" },
+ { 0, NULL, NULL, 0, NULL }
+ };
+
+ struct opt_handler oh =
+ opt_handler_make (argc, argv, opts, NULL, "Modular IRC bot.");
+
+ int c;
+ while ((c = opt_handler_get (&oh)) != -1)
+ switch (c)
+ {
+ case 'd':
+ g_debug_mode = true;
+ break;
+ case 'h':
+ opt_handler_usage (&oh, stdout);
+ exit (EXIT_SUCCESS);
+ case 'V':
+ printf (PROGRAM_NAME " " PROGRAM_VERSION "\n");
+ exit (EXIT_SUCCESS);
+ case 'w':
+ call_simple_config_write_default (optarg, g_config_table);
+ exit (EXIT_SUCCESS);
+ default:
+ print_error ("wrong options");
+ opt_handler_usage (&oh, stderr);
+ exit (EXIT_FAILURE);
+ }
+
+ opt_handler_free (&oh);
+
+ print_status (PROGRAM_NAME " " PROGRAM_VERSION " starting");
+ setup_signal_handlers ();
+ init_openssl ();
+
+ struct bot_context ctx;
+ bot_context_init (&ctx);
+
+ struct error *e = NULL;
+ if (!simple_config_update_from_file (&ctx.config, &e)
+ || !setup_recovery_handler (&ctx, &e))
+ {
+ print_error ("%s", e->message);
+ error_free (e);
+ exit (EXIT_FAILURE);
+ }
+
+ ctx.signal_event = poller_fd_make (&ctx.poller, g_signal_pipe[0]);
+ ctx.signal_event.dispatcher = (poller_fd_fn) on_signal_pipe_readable;
+ ctx.signal_event.user_data = &ctx;
+ poller_fd_set (&ctx.signal_event, POLLIN);
+
+#if OpenBSD >= 201605
+ // cpath is for creating the plugin home directory
+ if (pledge ("stdio rpath cpath inet proc exec", NULL))
+ exit_fatal ("%s: %s", "pledge", strerror (errno));
+#endif
+
+ plugin_load_all_from_config (&ctx);
+ if (!parse_config (&ctx, &e)
+ || !irc_connect (&ctx, &e))
+ {
+ print_error ("%s", e->message);
+ error_free (e);
+ exit (EXIT_FAILURE);
+ }
+
+ // TODO: clean re-exec support; to save the state I can either use argv,
+ // argp, or I can create a temporary file, unlink it and use the FD
+ // (mkstemp() on a `struct str' constructed from XDG_RUNTIME_DIR, TMPDIR
+ // or /tmp as a last resort + PROGRAM_NAME + ".XXXXXX" -> unlink();
+ // remember to use O_CREAT | O_EXCL). The state needs to be versioned.
+ // Unfortunately I cannot de/serialize SSL state.
+
+ ctx.polling = true;
+ while (ctx.polling)
+ poller_run (&ctx.poller);
+
+ bot_context_free (&ctx);
+ strv_free (&g_original_argv);
+ return EXIT_SUCCESS;
+}
+
diff --git a/xC.adoc b/xC.adoc
new file mode 100644
index 0000000..31c5b1d
--- /dev/null
+++ b/xC.adoc
@@ -0,0 +1,127 @@
+xC(1)
+=====
+:doctype: manpage
+:manmanual: uirc3 Manual
+:mansource: uirc3 {release-version}
+
+Name
+----
+xC - terminal-based IRC client
+
+Synopsis
+--------
+*xC* [_OPTION_]...
+
+Description
+-----------
+*xC* is a scriptable IRC client for the command line. On the first run it will
+welcome you with an introductory message. Should you ever get lost, use the
+*/help* command to obtain more information on commands or options.
+
+Options
+-------
+*-f*, *--format*::
+ Format IRC text from the standard input, converting colour sequences and
+ other formatting marks to ANSI codes retrieved from the *terminfo*(5)
+ database:
++
+```
+printf '\x02bold\x02\n' | xC -f
+```
++
+This feature may be used to preview server MOTD files.
+
+*-h*, *--help*::
+ Display a help message and exit.
+
+*-V*, *--version*::
+ Output version information and exit.
+
+Key bindings
+------------
+Most key bindings are inherited from the frontend in use, which is either GNU
+Readline or BSD editline. A few of them, however, are special to the IRC client
+or assume a different function. This is a list of all local overrides and
+their respective function names:
+
+*M-p*::
+ Go up in history for this buffer (normally mapped to *C-p*).
+*M-n*::
+ Go down in history for this buffer (normally mapped to *C-n*).
+*C-p*, *F5*: *previous-buffer*::
+ Switch to the previous buffer in order.
+*C-n*, *F6*: *next-buffer*::
+ Switch to the next buffer in order.
+*M-TAB*: *switch-buffer*::
+ Switch to the last buffer, i.e., the one you were in before.
+*M-0*, *M-1*, ..., *M-9*: *goto-buffer*::
+ Go to the N-th buffer (normally sets a repeat counter).
+ Since there is no buffer number zero, *M-0* goes to the tenth one.
+*M-!*: *goto-highlight*::
+ Go to the first following buffer with an unseen highlight.
+*M-a*: *goto-activity*::
+ Go to the first following buffer with unseen activity.
+*PageUp*: *display-backlog*::
+ Show the in-memory backlog for this buffer 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
+-----
+*xC* follows the XDG Base Directory Specification.
+
+_~/.config/xC/xC.conf_::
+ The program's configuration file. Preferrably use internal facilities, such
+ as the */set* command, to make changes in it.
+
+_~/.local/share/xC/logs/_::
+ When enabled by *behaviour.logging*, log files are stored here.
+
+_~/.local/share/xC/plugins/_::
+_/usr/local/share/xC/plugins/_::
+_/usr/share/xC/plugins/_::
+ Plugins are searched for in these directories, in order.
+
+Bugs
+----
+The editline (libedit) frontend 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/xC.c b/xC.c
new file mode 100644
index 0000000..438cf59
--- /dev/null
+++ b/xC.c
@@ -0,0 +1,14469 @@
+/*
+ * xC.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 "xC"
+
+#include "common.c"
+#include "xD-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 xC library as a singleton with "plugin" as an upvalue
+ // (mostly historical, but rather convenient)
+ luaL_newmetatable (L, lua_ctx_info.name);
+ lua_pushlightuserdata (L, plugin);
+ luaL_setfuncs (L, lua_plugin_library, 1);
+ lua_plugin_add_accessors (L, &lua_ctx_info);
+
+ // Add the asynchronous library underneath
+ lua_newtable (L);
+ lua_pushlightuserdata (L, plugin);
+ luaL_setfuncs (L, lua_async_library, 1);
+ lua_setfield (L, -2, "async");
+ lua_pop (L, 1);
+
+ lua_weak_push (L, plugin, ctx, &lua_ctx_info);
+ lua_setglobal (L, lua_ctx_info.name);
+
+ // Create metatables for our objects
+ lua_plugin_reg_meta (L, XLUA_HOOK_METATABLE, lua_hook_table);
+ lua_plugin_reg_weak (L, &lua_user_info, lua_user_table);
+ lua_plugin_reg_weak (L, &lua_channel_info, lua_channel_table);
+ lua_plugin_reg_weak (L, &lua_buffer_info, lua_buffer_table);
+ lua_plugin_reg_weak (L, &lua_server_info, lua_server_table);
+ lua_plugin_reg_meta (L, XLUA_SCHEMA_METATABLE, lua_schema_table);
+ lua_plugin_reg_meta (L, XLUA_CONNECTION_METATABLE, lua_connection_table);
+
+ lua_plugin_reg_meta (L, XLUA_TASK_METATABLE, lua_task_table);
+ lua_plugin_reg_meta (L, XLUA_WCHANNEL_METATABLE, lua_wchannel_table);
+
+ struct error *error = NULL;
+ if (luaL_loadfile (L, filename))
+ error_set (e, "%s: %s", "Lua", lua_tostring (L, -1));
+ else if (!lua_plugin_call (plugin, 0, 0, &error))
+ {
+ error_set (e, "%s: %s", "Lua", error->message);
+ error_free (error);
+ }
+ else
+ return &plugin->super;
+
+ plugin_destroy (&plugin->super);
+ return NULL;
+}
+
+#endif // HAVE_LUA
+
+// --- Plugins -----------------------------------------------------------------
+
+typedef struct plugin *(*plugin_load_fn)
+ (struct app_context *ctx, const char *filename, struct error **e);
+
+// We can potentially add support for other scripting languages if so desired,
+// however this possibility is just a byproduct of abstraction
+static plugin_load_fn g_plugin_loaders[] =
+{
+#ifdef HAVE_LUA
+ lua_plugin_load,
+#endif // HAVE_LUA
+};
+
+static struct plugin *
+plugin_load_from_filename (struct app_context *ctx, const char *filename,
+ struct error **e)
+{
+ struct plugin *plugin = NULL;
+ struct error *error = NULL;
+ for (size_t i = 0; i < N_ELEMENTS (g_plugin_loaders); i++)
+ if ((plugin = g_plugin_loaders[i](ctx, filename, &error)) || error)
+ break;
+
+ if (error)
+ error_propagate (e, error);
+ else if (!plugin)
+ {
+ error_set (e, "no plugin handler for \"%s\"", filename);
+ return NULL;
+ }
+ return plugin;
+}
+
+static struct plugin *
+plugin_find (struct app_context *ctx, const char *name)
+{
+ LIST_FOR_EACH (struct plugin, iter, ctx->plugins)
+ if (!strcmp (name, iter->name))
+ return iter;
+ return NULL;
+}
+
+static char *
+plugin_resolve_relative_filename (const char *filename)
+{
+ struct strv paths = strv_make ();
+ get_xdg_data_dirs (&paths);
+ 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",
+ "[ | ]",
+ handle_command_help, 0 },
+ { "quit", "Quit the program",
+ "[]",
+ handle_command_quit, 0 },
+ { "buffer", "Manage buffers",
+ " | list | clear | move | goto | close []",
+ handle_command_buffer, 0 },
+ { "set", "Manage configuration",
+ "[]",
+ handle_command_set, 0 },
+ { "save", "Save configuration",
+ NULL,
+ handle_command_save, 0 },
+ { "plugin", "Manage plugins",
+ "list | load | unload ",
+ handle_command_plugin, 0 },
+
+ { "alias", "List or set aliases",
+ "[ ]",
+ handle_command_alias, 0 },
+ { "unalias", "Unset aliases",
+ "...",
+ handle_command_unalias, 0 },
+
+ { "msg", "Send message to a nick or channel",
+ " ",
+ handle_command_msg, HANDLER_SERVER | HANDLER_NEEDS_REG },
+ { "query", "Send a private message to a nick",
+ " ",
+ handle_command_query, HANDLER_SERVER | HANDLER_NEEDS_REG },
+ { "notice", "Send notice to a nick or channel",
+ " ",
+ handle_command_notice, HANDLER_SERVER | HANDLER_NEEDS_REG },
+ { "squery", "Send a message to a service",
+ " ",
+ handle_command_squery, HANDLER_SERVER | HANDLER_NEEDS_REG },
+ { "ctcp", "Send a CTCP query",
+ " ",
+ handle_command_ctcp, HANDLER_SERVER | HANDLER_NEEDS_REG },
+ { "me", "Send a CTCP action",
+ "",
+ handle_command_me, HANDLER_SERVER | HANDLER_NEEDS_REG },
+
+ { "join", "Join channels",
+ "[[,...]] [[,...]]",
+ handle_command_join, HANDLER_SERVER },
+ { "part", "Leave channels",
+ "[[,...]] []",
+ handle_command_part, HANDLER_SERVER },
+ { "cycle", "Rejoin channels",
+ "[[,...]] []",
+ handle_command_cycle, HANDLER_SERVER },
+
+ { "op", "Give channel operator status",
+ "...",
+ handle_command_op, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
+ { "deop", "Remove channel operator status",
+ "[...]",
+ handle_command_deop, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
+ { "voice", "Give voice",
+ "...",
+ handle_command_voice, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
+ { "devoice", "Remove voice",
+ "[...]",
+ handle_command_devoice, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
+
+ { "mode", "Change mode",
+ "[] [...]",
+ handle_command_mode, HANDLER_SERVER },
+ { "topic", "Change topic",
+ "[] []",
+ handle_command_topic, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
+ { "kick", "Kick user from channel",
+ "[] []",
+ handle_command_kick, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
+ { "kickban", "Kick and ban user from channel",
+ "[] []",
+ handle_command_kickban, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
+ { "ban", "Ban user from channel",
+ "[] [...]",
+ handle_command_ban, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
+ { "unban", "Unban user from channel",
+ "[] ...",
+ handle_command_unban, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
+ { "invite", "Invite user to channel",
+ "... []",
+ handle_command_invite, HANDLER_SERVER | HANDLER_CHANNEL_LAST },
+
+ { "server", "Manage servers",
+ "list | add | remove | rename