93 Commits

Author SHA1 Message Date
11a6c7662e Update NEWS, bump release 2016-03-27 17:17:15 +02:00
dc71af9c31 degesch: fix two minor issues
- completely new unread markers could be created in active buffers
 - control characters confused word wrapping
2016-03-26 20:59:37 +01:00
f964495d1a degesch: don't wrap lines in pager 2016-03-26 16:41:55 +01:00
550a0419a6 degesch: detect //TRANSLIT support, use cp1252
Now BSDs should have it enabled as well.
2016-03-26 14:27:59 +01:00
9b12c830d1 degesch: remap goto-activity to M-a
weechat appears to use this key binding, so let's not reinvent it.
2016-03-26 13:11:28 +01:00
1e24d1d1b8 degesch: add partial matching /buffer goto 2016-03-26 13:00:10 +01:00
6292114c76 degesch: append message count in /buffer listing 2016-03-26 05:15:11 +01:00
e646afe5ae degesch: retain the unseen marker
Don't erase the message counts immediately.

Also make the marker visible in the pager.
2016-03-26 04:55:46 +01:00
410bcdcd78 degesch: phase 1 of word wrapping implementation 2016-03-26 04:52:05 +01:00
62962dc7ac Fix Travis CI notifications 2016-03-14 20:41:07 +01:00
a83ef111c8 Fix git commit tracking 2016-03-13 17:07:04 +01:00
90842c23a2 kike: fix daemonization on *BSD
Bump liberty.
2016-03-13 16:44:58 +01:00
1c9de9291b degesch: cleanup, fix no-tty mode FWIW
Removed the no color mode that couldn't even be enabled.

Not sure why we still support running without a proper terminal
but let's at least not make it crash for now.
2016-03-13 16:44:06 +01:00
e11ca7cc00 Use TMPDIR as a fallback if set 2016-03-13 00:59:28 +01:00
df395f32e5 Update NEWS, README 2016-03-12 23:02:59 +01:00
f96fa66168 degesch: add a --format switch 2016-03-12 14:28:17 +01:00
781a37c152 Don't link kike, ZyklonB against degesch libs 2016-03-10 22:27:09 +01:00
5a197162bf Fix manpage generation
The manpages could end up in a wrong directory.
2016-03-10 20:26:35 +01:00
d70f156a20 Update README 2016-03-10 20:25:06 +01:00
42d88f87f5 degesch: add unbound commands for buffer movement 2016-03-10 00:07:59 +01:00
a1c4a1ef3a degesch: fix binding to our own fns from inputrc 2016-03-10 00:06:28 +01:00
dc248b8840 degesch: add goto activity and highlight 2016-03-08 22:29:40 +01:00
09c7d9a65d degesch: fix mIRC color parsing 2016-03-08 01:59:51 +01:00
0f1fd2eb3a Update NEWS, cleanup 2016-03-07 23:43:47 +01:00
696273558e degesch: rewrite input layer
Now with less #ifdefs.
2016-03-07 22:52:56 +01:00
584d2f0295 degesch: use libffi to unify input callbacks
And fuck you both, Readline and Editline.
2016-03-06 18:12:12 +01:00
3304b718aa Try to use version information from git 2016-03-06 03:52:39 +01:00
10bdf90fe2 Don't force override compile flags 2016-03-06 03:50:35 +01:00
17804fa49b degesch: fix +=/-= to null config items 2016-03-05 19:15:40 +01:00
4b10ea7ab0 factoids: safer DB writes 2016-02-29 03:15:44 +01:00
fb0b0c4cf0 factoids: allow querying definitions by number 2016-02-29 03:11:33 +01:00
f492592735 factoids: look for duplicates case-insensitively 2016-02-29 02:50:53 +01:00
6190733079 degesch: mark a problem 2016-02-29 02:50:41 +01:00
676e6c20fa ZyklonB: add a factoids plugin 2016-02-27 22:29:19 +01:00
ed20322e5e ZyklonB: run plugins in a special work directory
Also small refactoring.
2016-02-20 00:37:57 +01:00
a275f9636c ZyklonB: revisit error handling 2016-02-20 00:01:54 +01:00
056e0a4765 Resolve tls_ca_{file,path} relative to config dir 2016-02-19 23:46:44 +01:00
798ed73a8c ZyklonB: fix segfault on total SOCKS failure 2016-02-12 04:11:33 +01:00
7be995f74a ZyklonB: avoid infinite reexec loops 2016-02-12 04:05:09 +01:00
06b03d336e degesch: fix segfault on sent messages w/o buffer 2016-02-12 04:04:35 +01:00
11519ee860 degesch: update screen size when terminal resumed
Apparently we don't receive the events when we give up the terminal.
2016-02-10 23:02:33 +01:00
03d5b27398 degesch: use mkstemp() with a safe umask 2016-02-09 13:52:56 +01:00
3315b16f79 degesch: log messages from /quote and plugins
That is, parse back all output messages and log based on that.
2016-02-09 05:10:41 +01:00
0c19a384f1 Fix typos 2016-02-09 04:50:51 +01:00
333ad2c981 degesch: allow changing the list of used CAPs
Bump liberty.
2016-02-01 21:57:43 +01:00
a850ee45f1 degesch: optimize buffer memory usage
We have approximately 5 formatter_items per buffer_line.  Let's assume
we're on a 64-bit machine.  Then there were (5 * 2) + 3 useless pointers
(104 bytes) as well as 5 * (4 + 4) = 40 bytes of wasted space because
of needless padding.  That's 144 bytes already.  Compared to that, this
change adds 16 bytes of overhead for an array sentinel, i.e. 128B less.

With a limit of 1000 lines per buffer, we've saved ~128kB per buffer
on completely useless data, and code complexity stays roughly the same.

All in all, memory usage for buffers should be about 50% lower.
2016-01-31 21:43:23 +01:00
10a264ec3d kike: add support for IRCv3.2 server-time 2016-01-31 21:43:23 +01:00
2ec6258ff3 last-fm.lua: don't use empty album names 2016-01-18 01:28:53 +01:00
f57664ddd0 degesch: add an assertion
It should never fail with current code.
2016-01-18 00:59:43 +01:00
773d14e740 degesch: disable TLS compression 2016-01-18 00:45:20 +01:00
221ae03b5c degesch: Lua: fix memory leak on load failure 2016-01-17 22:57:16 +01:00
588a696c68 degesch: lesser heap fragmentation 2016-01-17 22:15:48 +01:00
6db40c4503 Bump liberty 2016-01-17 04:43:43 +01:00
f070523085 Bump liberty 2016-01-16 06:30:08 +01:00
dac5c9df6d kike: more cleanup 2016-01-16 06:30:08 +01:00
ced2a57cfc kike: allow messages before protocol establishment
We can just queue them.
2016-01-16 06:30:08 +01:00
f36d66b0cb kike: asynchronous address resolution
As well as some refactoring and cleanup.
2016-01-16 06:30:08 +01:00
fdeb550ee0 degesch: fix backlog limit
It was effectively infinite.
2016-01-15 22:11:05 +01:00
c4a18ec8a7 degesch: fix and simplify screen handling
Now with less madness.
2016-01-15 05:40:20 +01:00
d0db1a6cdc degesch: enforce fullscreen buffers
Probably long overdue.

Now we actually look like something resembling a regular IRC client.
2016-01-15 05:12:03 +01:00
9333081178 degesch: option for fullscreen buffers 2016-01-15 05:09:42 +01:00
b7c9e8ca23 degesch: make backlog limit configurable 2016-01-15 05:09:42 +01:00
f39e2a4bc8 degesch: Lua: add autocomplete hooks 2016-01-15 02:39:10 +01:00
91f3bd60df degesch: Lua: finish the last-fm plugin 2016-01-14 04:13:03 +01:00
56858a97dd degesch: Lua: allow simulating user input
Also added UTF-8 validation to buffer:log() while I'm at it.
2016-01-14 03:34:29 +01:00
331d1842b9 Bump liberty, shuffle some code 2016-01-14 03:26:02 +01:00
19b09a8cec degesch: add a last-fm "now playing" plugin 2016-01-09 10:27:01 +01:00
32f719dec7 degesch: Lua: pass hostname to on_connected 2016-01-09 06:19:54 +01:00
0b92e9210c degesch: Lua: set sockets to nonblocking 2016-01-09 05:47:24 +01:00
092e9b5101 Bump liberty 2016-01-09 05:27:45 +01:00
faa0c989f8 degesch: Lua: actually allow filtering out input 2016-01-09 05:05:46 +01:00
53e72dd12d degesch: Lua: provide a traceback on load error 2016-01-09 05:01:50 +01:00
83c14ba264 degesch: Lua: fix plugin configuration names 2016-01-09 05:01:50 +01:00
64143a5957 degesch: Lua: fix luaL_ref() usage 2016-01-09 05:01:50 +01:00
aca153f575 degesch: Lua: fix configuration loading
Not the cleanest solution but it has to do for now.
2016-01-09 05:01:50 +01:00
79f46752d4 degesch: make sure newlines are output correctly 2016-01-08 08:40:40 +01:00
2a180ee084 degesch: Lua: finish implementation of connection 2016-01-07 22:49:53 +01:00
6754c59890 degesch: Lua: avoid resource leak
If a connector's on_success callback fails, we need to destroy the connection.
2016-01-07 22:49:53 +01:00
376bbea249 Factor out socket_io_try_{read,write}()
To be reused in Lua connection API.
2016-01-07 22:49:53 +01:00
a5ac0d24b8 degesch: fix handling of input editor death 2016-01-07 22:49:53 +01:00
cabab5f351 Fix a memory leak in SOCKS connector 2016-01-07 22:49:49 +01:00
1d3910fd8e degesch: fix switching of buffers by command
Readline used to erase the new buffer's contents.

Defer processing.
2016-01-07 22:49:49 +01:00
a259e96405 degesch: Lua: fix a resource leak 2016-01-06 00:23:54 +01:00
a7be2bf160 degesch: refactor Lua
And fix handling of nil returns from filter callbacks.
2016-01-05 23:19:28 +01:00
e1c7b8dcaf degesch: Lua: halfplement a connector wrapper
You can't do anything reasonable with the socket now.
2016-01-05 22:12:22 +01:00
00a1bdc707 Fix build of tests 2016-01-05 21:51:07 +01:00
e9b39a1ef7 degesch: Lua: allow arbitrary userdata properties 2016-01-04 23:14:38 +01:00
a227060383 degesch: Lua: use references for hook callbacks
Don't associate the callback with the full userdata object,
we'll need this for something else.
2016-01-04 22:24:05 +01:00
4832a99461 degesch: add basic autocomplete for /topic 2016-01-04 22:06:29 +01:00
0092c34568 Cleanup 2016-01-04 01:15:42 +01:00
aeb047260f Bump liberty, enable TLS SNI
Involves some rewrites to fit the new APIs.

SNI has been implemented Mostly just because we can, I don't think it's
widely in use and kike doesn't support this feature of the protocol either.
2016-01-04 01:12:42 +01:00
28fec6d4a6 ZyklonB: fix tls_ca_{path,file} config. options 2016-01-01 02:00:02 +01:00
1a73f1f1d7 degesch: fix a memory leak under libedit 2016-01-01 02:00:02 +01:00
12 changed files with 3626 additions and 1676 deletions

View File

@@ -3,7 +3,7 @@ dist: trusty
language: c
notifications:
irc:
channels: "anathema.irc.so#anathema"
channels: "irc.janouch.name#dev"
use_notice: true
skip_join: true
env:
@@ -32,7 +32,7 @@ before_install:
- sudo apt-get update -qq
install:
- sudo apt-get install -y libncursesw5-dev libreadline-dev libedit-dev
liblua5.3-dev help2man expect
liblua5.3-dev libffi-dev help2man expect
before_script:
- mkdir build
- cd build

View File

@@ -8,35 +8,79 @@ option (WANT_LIBEDIT "Use BSD libedit for the UI" OFF)
# Moar warnings
if ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUC)
# -Wunused-function is pretty annoying here, as everything is static
set (CMAKE_C_FLAGS "-std=c99 -Wall -Wextra -Wno-unused-function")
set (CMAKE_C_FLAGS
"${CMAKE_C_FLAGS} -std=c99 -Wall -Wextra -Wno-unused-function")
endif ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUC)
# Version
set (project_VERSION_MAJOR "0")
set (project_VERSION_MINOR "9")
set (project_VERSION_PATCH "2")
set (project_version "0.9.3")
set (project_VERSION "${project_VERSION_MAJOR}")
set (project_VERSION "${project_VERSION}.${project_VERSION_MINOR}")
set (project_VERSION "${project_VERSION}.${project_VERSION_PATCH}")
# Try to append commit ID if it follows a version tag. It might be nicer if
# we could also detect dirty worktrees but that's very hard to get right.
find_package (Git)
set (git_head "${PROJECT_SOURCE_DIR}/.git/HEAD")
if (GIT_FOUND AND EXISTS "${git_head}")
configure_file ("${git_head}" git-head.tag COPYONLY)
file (READ "${git_head}" git_head_content)
if (git_head_content MATCHES "^ref: ([^\r\n]+)")
set (git_ref "${PROJECT_SOURCE_DIR}/.git/${CMAKE_MATCH_1}")
if (EXISTS "${git_ref}")
configure_file ("${git_ref}" git-ref.tag COPYONLY)
endif (EXISTS "${git_ref}")
endif (git_head_content MATCHES "^ref: ([^\r\n]+)")
execute_process (COMMAND ${GIT_EXECUTABLE} describe --tags --match v*
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
RESULT_VARIABLE git_describe_result
OUTPUT_VARIABLE git_describe OUTPUT_STRIP_TRAILING_WHITESPACE)
if (NOT git_describe_result)
string (REGEX REPLACE "^v" "" project_version "${git_describe}")
endif (NOT git_describe_result)
endif (GIT_FOUND AND EXISTS "${git_head}")
# Dashes make filenames confusing and upset packaging software
string (REPLACE "-" "+" project_version_safe "${project_version}")
# Dependencies
find_package (Curses)
set (CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/liberty/cmake)
include (AddThreads)
find_package (PkgConfig REQUIRED)
pkg_check_modules (libssl REQUIRED libssl libcrypto)
pkg_check_modules (ncursesw ncursesw)
list (APPEND project_libraries ${libssl_LIBRARIES})
include_directories (${libssl_INCLUDE_DIRS})
link_directories (${libssl_LIBRARY_DIRS})
if ("${CMAKE_SYSTEM_NAME}" MATCHES "BSD")
include_directories(/usr/local/include)
link_directories(/usr/local/lib)
include_directories (/usr/local/include)
link_directories (/usr/local/lib)
# Need this for SIGWINCH in FreeBSD and OpenBSD respectively;
# our POSIX version macros make it undefined
add_definitions (-D__BSD_VISIBLE=1 -D_BSD_SOURCE=1)
endif ("${CMAKE_SYSTEM_NAME}" MATCHES "BSD")
list (APPEND project_libraries ${libssl_LIBRARIES})
include_directories (${libssl_INCLUDE_DIRS})
link_directories (${libssl_LIBRARY_DIRS})
# -lrt is only for glibc < 2.17
# -liconv may or may not be a part of libc
foreach (extra iconv rt)
find_library (extra_lib_${extra} ${extra})
if (extra_lib_${extra})
list (APPEND project_libraries ${extra_lib_${extra}})
endif (extra_lib_${extra})
endforeach (extra)
include (CheckCSourceRuns)
set (CMAKE_REQUIRED_LIBRARIES ${project_libraries})
get_property (CMAKE_REQUIRED_INCLUDES
DIRECTORY "${PROJECT_SOURCE_DIR}" PROPERTY INCLUDE_DIRECTORIES)
CHECK_C_SOURCE_RUNS ("#include <iconv.h>
int main () { return iconv_open (\"UTF-8//TRANSLIT\", \"ISO-8859-1\")
== (iconv_t) -1; }" ICONV_ACCEPTS_TRANSLIT)
# Dependencies for degesch
pkg_check_modules (libffi REQUIRED libffi)
list (APPEND degesch_libraries ${libffi_LIBRARIES})
include_directories (${libffi_INCLUDE_DIRS})
link_directories (${libffi_LIBRARY_DIRS})
# FIXME: other Lua versions may be acceptable, don't know yet
pkg_search_module (lua lua53 lua5.3 lua-5.3 lua>=5.3)
@@ -47,26 +91,18 @@ if (WITH_LUA)
message (FATAL_ERROR "Lua library not found")
endif (NOT lua_FOUND)
list (APPEND project_libraries ${lua_LIBRARIES})
list (APPEND degesch_libraries ${lua_LIBRARIES})
include_directories (${lua_INCLUDE_DIRS})
link_directories (${lua_LIBRARY_DIRS})
endif (WITH_LUA)
# -lpthread is only there for debugging (gdb & errno)
# -lrt is only for glibc < 2.17
# -liconv may or may not be a part of libc
foreach (extra iconv rt pthread)
find_library (extra_lib_${extra} ${extra})
if (extra_lib_${extra})
list (APPEND project_libraries ${extra})
endif (extra_lib_${extra})
endforeach (extra)
find_package (Curses)
pkg_check_modules (ncursesw ncursesw)
if (ncursesw_FOUND)
list (APPEND project_libraries ${ncursesw_LIBRARIES})
list (APPEND degesch_libraries ${ncursesw_LIBRARIES})
include_directories (${ncursesw_INCLUDE_DIRS})
elseif (CURSES_FOUND)
list (APPEND project_libraries ${CURSES_LIBRARY})
list (APPEND degesch_libraries ${CURSES_LIBRARY})
include_directories (${CURSES_INCLUDE_DIR})
else (CURSES_FOUND)
message (SEND_ERROR "Curses not found")
@@ -78,13 +114,13 @@ elseif (WANT_READLINE)
# OpenBSD's default readline is too old
if ("${CMAKE_SYSTEM_NAME}" MATCHES "OpenBSD")
include_directories (/usr/local/include/ereadline)
list (APPEND project_libraries ereadline)
list (APPEND degesch_libraries ereadline)
else ("${CMAKE_SYSTEM_NAME}" MATCHES "OpenBSD")
list (APPEND project_libraries readline)
list (APPEND degesch_libraries readline)
endif ("${CMAKE_SYSTEM_NAME}" MATCHES "OpenBSD")
elseif (WANT_LIBEDIT)
pkg_check_modules (libedit REQUIRED libedit)
list (APPEND project_libraries ${libedit_LIBRARIES})
list (APPEND degesch_libraries ${libedit_LIBRARIES})
include_directories (${libedit_INCLUDE_DIRS})
endif ((WANT_READLINE AND WANT_LIBEDIT) OR (NOT WANT_READLINE AND NOT WANT_LIBEDIT))
@@ -114,24 +150,31 @@ set_source_files_properties (${PROJECT_BINARY_DIR}/kike-replies.c
# Build
add_executable (zyklonb zyklonb.c ${common_sources} ${common_headers})
target_link_libraries (zyklonb ${project_libraries})
add_threads (zyklonb)
add_executable (degesch degesch.c kike-replies.c
${common_sources} ${common_headers})
target_link_libraries (degesch ${project_libraries})
target_link_libraries (degesch ${project_libraries} ${degesch_libraries})
add_threads (degesch)
add_executable (kike kike.c kike-replies.c ${common_sources} ${common_headers})
target_link_libraries (kike ${project_libraries})
add_threads (kike)
# Tests
function (make_tests_for target_name)
get_target_property (sources ${target_name} SOURCES)
get_target_property (libraries ${target_name} LINK_LIBRARIES)
get_target_property (options ${target_name} COMPILE_OPTIONS)
set (test test-${target_name})
add_executable (${test} ${sources})
target_link_libraries (${test} ${libraries})
set_target_properties (${test} PROPERTIES
COMPILE_DEFINITIONS TESTING
COMPILE_OPTIONS "${options}")
add_test (NAME ${test} COMMAND ${test})
set_target_properties (${test} PROPERTIES COMPILE_DEFINITIONS TESTING)
endfunction (make_tests_for)
include (CTest)
@@ -180,26 +223,26 @@ endforeach (page)
add_custom_target (docs ALL DEPENDS ${project_MAN_PAGES})
foreach (page ${project_MAN_PAGES})
string (REGEX MATCH "\\.([0-9])" manpage_suffix "${page}")
string (REGEX MATCH "\\.([0-9])$" manpage_suffix "${page}")
install (FILES "${page}"
DESTINATION "${CMAKE_INSTALL_MANDIR}/man${CMAKE_MATCH_1}")
endforeach (page)
# CPack
set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "Experimental IRC client, daemon and bot")
set (CPACK_PACKAGE_VERSION ${project_VERSION})
set (CPACK_PACKAGE_VERSION "${project_version_safe}")
set (CPACK_PACKAGE_VENDOR "Premysl Janouch")
set (CPACK_PACKAGE_CONTACT "Přemysl Janouch <p.janouch@gmail.com>")
set (CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE")
set (CPACK_GENERATOR "TGZ;ZIP")
set (CPACK_PACKAGE_FILE_NAME
"${PROJECT_NAME}-${project_VERSION}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}")
set (CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}-${project_VERSION}")
"${PROJECT_NAME}-${project_version_safe}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}")
set (CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}-${project_version_safe}")
set (CPACK_SOURCE_GENERATOR "TGZ;ZIP")
set (CPACK_SOURCE_IGNORE_FILES "/\\\\.git;/build;/CMakeLists.txt.user")
set (CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${project_VERSION}")
set (CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${project_version_safe}")
set (CPACK_SET_DESTDIR TRUE)
include (CPack)

40
NEWS
View File

@@ -1,3 +1,43 @@
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
* degesch: implemented word wrapping in buffers
* degesch: added autocomplete for /topic
* degesch: Lua API was improved and extended
* degesch: added a basic last.fm "now playing" plugin
* degesch: backlog limit was made configurable
* degesch: allow changing the list of IRC capabilities to use if available
* degesch: optimize buffer memory usage
* degesch: 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
* degesch: added --format for previewing things like MOTD files
* degesch: added /buffer goto supporting case insensitive partial matches
* kike: add support for IRCv3.2 server-time
* ZyklonB: plugins now run in a dedicated data directory
* ZyklonB: added a factoids plugin
* Remote addresses are now resolved asynchronously
* Various bugfixes
0.9.2 (2015-12-31)
* degesch: added rudimentary support for Lua scripting

View File

@@ -10,7 +10,7 @@ All of them have these potentially interesting properties:
- full IPv6 support
- TLS support, including client certificates
- minimal dependencies
- lean on dependencies (with the exception of 'degesch')
- compact and arguably easy to hack on
- permissive license
@@ -63,12 +63,21 @@ the rest of the package.
It survives crashes, server disconnects and timeouts, and also has native SOCKS
support (even though socksify can add that easily to any program).
Packages
--------
Regular releases are sporadic. git master should be stable enough. You can get
a package with the latest development version from Archlinux's AUR, or from
openSUSE Build Service for the rest of mainstream distributions. Consult the
list of repositories and their respective links at:
https://build.opensuse.org/project/repositories/home:pjanouch:git
Building
--------
Build dependencies: CMake, pkg-config, help2man, awk, sh, liberty (included) +
Runtime dependencies: openssl, curses (degesch),
readline >= 6.0 or libedit >= 2013-07-12 (degesch),
lua >= 5.3 (degesch, optional)
Runtime dependencies: openssl +
Additionally for degesch: curses, libffi, lua >= 5.3 (optional),
readline >= 6.0 or libedit >= 2013-07-12
$ git clone --recursive https://github.com/pjanouch/uirc3.git
$ mkdir uirc3/build
@@ -89,8 +98,8 @@ Or you can try telling CMake to make a package for you. For Debian it is:
Note that for versions of CMake before 2.8.9, you need to prefix `cpack` with
`fakeroot` or file ownership will end up wrong.
Running
-------
Usage
-----
'degesch' has in-program configuration. Just run it and read the instructions.
For the rest you might want to generate a configuration file:
@@ -118,11 +127,29 @@ 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:
....
# Preload with system-wide settings
$include /etc/inputrc
# Make M-left and M-right reorder buffers
$if degesch
"\e\e[C": move-buffer-right
"\e\e[D": move-buffer-left
$endif
....
Consult the source code and the GNU Readline manual for a list of available
functions. Also refer to the latter for the exact syntax of this file.
Beware that you can easily break the program if you're not careful.
Contributing and Support
------------------------
Use this project's GitHub to report any bugs, request features, or submit pull
requests. If you want to discuss this project, or maybe just hang out with
the developer, feel free to join me at irc://anathema.irc.so, channel #anathema.
the developer, feel free to join me at irc://irc.janouch.name, channel #dev.
Disclaimer
----------

115
common.c
View File

@@ -18,6 +18,7 @@
*/
#define LIBERTY_WANT_SSL
#define LIBERTY_WANT_ASYNC
#define LIBERTY_WANT_POLLER
#define LIBERTY_WANT_PROTO_IRC
@@ -40,9 +41,6 @@
return 0; \
BLOCK_END
#define CONTAINER_OF(pointer, type, member) \
(type *) ((char *) pointer - offsetof (type, member))
// --- To be moved to liberty --------------------------------------------------
static ssize_t
@@ -54,6 +52,21 @@ str_vector_find (const struct str_vector *v, const char *s)
return -1;
}
static time_t
unixtime_msec (long *msec)
{
#ifdef _POSIX_TIMERS
struct timespec tp;
hard_assert (clock_gettime (CLOCK_REALTIME, &tp) != -1);
*msec = tp.tv_nsec / 1000000;
#else // ! _POSIX_TIMERS
struct timeval tp;
hard_assert (gettimeofday (&tp, NULL) != -1);
*msec = tp.tv_usec / 1000;
#endif // ! _POSIX_TIMERS
return tp.tv_sec;
}
/// This differs from the non-unique version in that we expect the filename
/// to be something like a pattern for mkstemp(), so the resulting path can
/// reside in a system-wide directory with no risk of a conflict.
@@ -64,8 +77,11 @@ resolve_relative_runtime_unique_filename (const char *filename)
str_init (&path);
const char *runtime_dir = getenv ("XDG_RUNTIME_DIR");
const char *tmpdir = getenv ("TMPDIR");
if (runtime_dir && *runtime_dir == '/')
str_append (&path, runtime_dir);
else if (tmpdir && *tmpdir == '/')
str_append (&path, tmpdir);
else
str_append (&path, "/tmp");
str_append_printf (&path, "/%s/%s", PROGRAM_NAME, filename);
@@ -97,6 +113,74 @@ xwrite (int fd, const char *data, size_t len, struct error **e)
return true;
}
// --- Simple network I/O ------------------------------------------------------
// TODO: move to liberty and remove from dwmstatus.c as well
#define SOCKET_IO_OVERFLOW (8 << 20) ///< How large a read buffer can be
enum socket_io_result
{
SOCKET_IO_OK, ///< Completed successfully
SOCKET_IO_EOF, ///< Connection shut down by peer
SOCKET_IO_ERROR ///< Connection error
};
static enum socket_io_result
socket_io_try_read (int socket_fd, struct str *rb, struct error **e)
{
// We allow buffering of a fair amount of data, however within reason,
// so that it's not so easy to flood us and cause an allocation failure
ssize_t n_read;
while (rb->len < SOCKET_IO_OVERFLOW)
{
str_ensure_space (rb, 4096);
n_read = recv (socket_fd, rb->str + rb->len,
rb->alloc - rb->len - 1 /* null byte */, 0);
if (n_read > 0)
{
rb->str[rb->len += n_read] = '\0';
continue;
}
if (n_read == 0)
return SOCKET_IO_EOF;
if (errno == EAGAIN)
return SOCKET_IO_OK;
if (errno == EINTR)
continue;
error_set (e, "%s", strerror (errno));
return SOCKET_IO_ERROR;
}
return SOCKET_IO_OK;
}
static enum socket_io_result
socket_io_try_write (int socket_fd, struct str *wb, struct error **e)
{
ssize_t n_written;
while (wb->len)
{
n_written = send (socket_fd, wb->str, wb->len, 0);
if (n_written >= 0)
{
str_remove_slice (wb, 0, n_written);
continue;
}
if (errno == EAGAIN)
return SOCKET_IO_OK;
if (errno == EINTR)
continue;
error_set (e, "%s", strerror (errno));
return SOCKET_IO_ERROR;
}
return SOCKET_IO_OK;
}
// --- Logging -----------------------------------------------------------------
static void
@@ -223,7 +307,7 @@ struct socks_connector
// You may destroy the connector object in these two main callbacks:
/// Connection has been successfully established
void (*on_connected) (void *user_data, int socket);
void (*on_connected) (void *user_data, int socket, const char *hostname);
/// Failed to establish a connection to either target
void (*on_failure) (void *user_data);
@@ -308,7 +392,6 @@ socks_4a_start (struct socks_connector *self)
}
struct str *wb = &self->write_buffer;
str_init (wb);
str_pack_u8 (wb, 4); // version
str_pack_u8 (wb, 1); // connect
@@ -594,9 +677,11 @@ socks_connector_on_timeout (struct socks_connector *self)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
socks_connector_on_connected (void *user_data, int socket_fd)
socks_connector_on_connected
(void *user_data, int socket_fd, const char *hostname)
{
set_blocking (socket_fd, false);
(void) hostname;
struct socks_connector *self = user_data;
self->socket_fd = socket_fd;
@@ -658,20 +743,8 @@ socks_connector_start (struct socks_connector *self)
connector->on_error = socks_connector_on_error;
connector->on_failure = socks_connector_on_failure;
struct error *e = NULL;
if (!connector_add_target (connector, self->hostname, self->service, &e))
{
if (self->on_error)
self->on_error (self->user_data, e->message);
error_free (e);
socks_connector_destroy_connector (self);
socks_connector_fail (self);
return;
}
connector_add_target (connector, self->hostname, self->service);
poller_timer_set (&self->timeout, 60 * 1000);
connector_step (connector);
self->done = false;
self->bound_port = 0;
@@ -762,8 +835,10 @@ socks_connector_on_ready
int fd = self->socket_fd;
self->socket_fd = -1;
struct socks_target *target = self->targets_iter;
set_blocking (fd, true);
self->on_connected (self->user_data, fd);
self->on_connected (self->user_data, fd, target->address_str);
}
else
// We've failed this target, let's try to move on

View File

@@ -1,11 +1,13 @@
#ifndef CONFIG_H
#define CONFIG_H
#define PROGRAM_VERSION "${project_VERSION}"
#define PROGRAM_VERSION "${project_version}"
#define ZYKLONB_PLUGIN_DIR "${CMAKE_INSTALL_PREFIX}/${zyklonb_plugin_dir}"
#cmakedefine HAVE_READLINE
#cmakedefine HAVE_EDITLINE
#cmakedefine HAVE_LUA
#cmakedefine01 ICONV_ACCEPTS_TRANSLIT
#endif // ! CONFIG_H

4040
degesch.c

File diff suppressed because it is too large Load Diff

277
kike.c
View File

@@ -1,7 +1,7 @@
/*
* kike.c: the experimental IRC daemon
*
* Copyright (c) 2014 - 2015, Přemysl Janouch <p.janouch@gmail.com>
* Copyright (c) 2014 - 2016, Přemysl Janouch <p.janouch@gmail.com>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
@@ -307,7 +307,8 @@ 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_USERHOST_IN_NAMES = (1 << 3),
IRC_CAP_SERVER_TIME = (1 << 4)
};
struct client
@@ -330,17 +331,17 @@ struct client
struct poller_timer timeout_timer; ///< Connection seems to be dead
struct poller_timer kill_timer; ///< Hard kill timeout
bool initialized; ///< Has any data been received yet?
bool cap_negotiating; ///< Negotiating capabilities
bool registered; ///< The user has registered
bool closing_link; ///< Closing link
bool half_closed; ///< Closing link: conn. is half-closed
unsigned long cap_version; ///< CAP protocol version
unsigned caps_enabled; ///< Enabled capabilities
bool ssl_rx_want_tx; ///< SSL_read() wants to write
bool ssl_tx_want_rx; ///< SSL_write() wants to read
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
@@ -349,20 +350,23 @@ struct client
char *realname; ///< IRC realname (e-mail)
char *hostname; ///< Hostname shown to the network
char *address; ///< Full address including port
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 void
client_init (struct client *self)
static struct client *
client_new (void)
{
memset (self, 0, sizeof *self);
struct client *self = xcalloc (1, sizeof *self);
self->socket_fd = -1;
str_init (&self->read_buffer);
str_init (&self->write_buffer);
@@ -371,10 +375,11 @@ client_init (struct client *self)
flood_detector_init (&self->antiflood, 10, 20);
str_map_init (&self->invites);
self->invites.key_xfrm = irc_strxfrm;
return self;
}
static void
client_free (struct client *self)
client_destroy (struct client *self)
{
if (!soft_assert (self->socket_fd == -1))
xclose (self->socket_fd);
@@ -389,10 +394,16 @@ client_free (struct client *self)
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);
@@ -819,12 +830,25 @@ client_get_mode (struct client *self)
static void
client_send_str (struct client *c, const struct str *s)
{
hard_assert (c->initialized && !c->closing_link);
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,
s->len > IRC_MAX_MESSAGE_LENGTH ? IRC_MAX_MESSAGE_LENGTH : s->len);
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.
@@ -891,9 +915,46 @@ client_unregister (struct client *c, const char *reason)
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;
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;
@@ -910,35 +971,6 @@ client_close_link (struct client *c, const char *reason)
client_set_kill_timer (c);
}
static void
client_kill (struct client *c, const char *reason)
{
client_unregister (c, reason ? reason : "Client exited");
struct server_context *ctx = c->ctx;
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_event.closed = true;
poller_fd_reset (&c->socket_event);
client_cancel_timers (c);
print_debug ("closed connection to %s (%s)",
c->address, reason ? reason : "");
c->socket_fd = -1;
client_free (c);
LIST_UNLINK (ctx->clients, c);
ctx->n_clients--;
free (c);
irc_try_finish_quit (ctx);
}
static bool
client_in_mask_list (const struct client *c, const struct str_vector *mask)
{
@@ -991,6 +1023,7 @@ 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
@@ -1002,9 +1035,8 @@ client_set_timer (struct client *c,
}
static void
on_client_kill_timer (void *user_data)
on_client_kill_timer (struct client *c)
{
struct client *c = user_data;
hard_assert (!c->initialized || c->closing_link);
client_kill (c, NULL);
}
@@ -1016,9 +1048,8 @@ client_set_kill_timer (struct client *c)
}
static void
on_client_timeout_timer (void *user_data)
on_client_timeout_timer (struct client *c)
{
struct client *c = user_data;
char *reason = xstrdup_printf
("Ping timeout: >%u seconds", c->ctx->ping_interval);
client_close_link (c, reason);
@@ -1026,9 +1057,8 @@ on_client_timeout_timer (void *user_data)
}
static void
on_client_ping_timer (void *user_data)
on_client_ping_timer (struct client *c)
{
struct client *c = user_data;
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);
@@ -1235,6 +1265,7 @@ irc_cap_table[] =
{ 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
@@ -1247,7 +1278,7 @@ irc_handle_cap_ls (struct client *c, struct irc_cap_args *a)
c->cap_negotiating = true;
client_send (c, ":%s CAP %s LS :multi-prefix invite-notify echo-message"
" userhost-in-names", c->ctx->server_name, a->target);
" userhost-in-names server-time", c->ctx->server_name, a->target);
}
static void
@@ -3211,6 +3242,8 @@ irc_try_write_tls (struct client *c)
return true;
}
// -----------------------------------------------------------------------------
static bool
irc_autodetect_tls (struct client *c)
{
@@ -3281,6 +3314,8 @@ error_ssl_1:
return false;
}
// -----------------------------------------------------------------------------
static void
on_client_ready (const struct pollfd *pfd, void *user_data)
{
@@ -3336,6 +3371,7 @@ on_client_ready (const struct pollfd *pfd, void *user_data)
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)
{
@@ -3346,7 +3382,7 @@ client_update_poller (struct client *c, const struct pollfd *pfd)
if (c->ssl_rx_want_tx) new_events &= ~POLLIN;
if (c->ssl_tx_want_rx) new_events &= ~POLLOUT;
}
else if (c->write_buffer.len)
else if (c->initialized && c->write_buffer.len)
new_events |= POLLOUT;
hard_assert (new_events != 0);
@@ -3354,6 +3390,43 @@ client_update_poller (struct client *c, const struct pollfd *pfd)
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
{
free (c->hostname);
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)
{
@@ -3378,6 +3451,15 @@ irc_try_fetch_client (struct server_context *ctx, int listen_fd)
return false;
}
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");
@@ -3387,20 +3469,16 @@ irc_try_fetch_client (struct server_context *ctx, int listen_fd)
char host[NI_MAXHOST] = "unknown", port[NI_MAXSERV] = "unknown";
int err = getnameinfo ((struct sockaddr *) &peer, peer_len,
host, sizeof host, port, sizeof port, NI_NUMERICSERV);
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);
print_debug ("accepted connection from %s", address);
struct client *c = xmalloc (sizeof *c);
client_init (c);
struct client *c = client_new ();
c->ctx = ctx;
c->opened = time (NULL);
c->socket_fd = fd;
c->hostname = xstrdup (host);
c->address = address;
c->port = xstrdup (port);
c->last_active = time (NULL);
LIST_PREPEND (ctx->clients, c);
ctx->n_clients++;
@@ -3410,27 +3488,29 @@ irc_try_fetch_client (struct server_context *ctx, int listen_fd)
c->socket_event.user_data = c;
poller_timer_init (&c->kill_timer, &c->ctx->poller);
c->kill_timer.dispatcher = on_client_kill_timer;
c->kill_timer.dispatcher = (poller_timer_fn) on_client_kill_timer;
c->kill_timer.user_data = c;
poller_timer_init (&c->timeout_timer, &c->ctx->poller);
c->timeout_timer.dispatcher = on_client_timeout_timer;
c->timeout_timer.dispatcher = (poller_timer_fn) on_client_timeout_timer;
c->timeout_timer.user_data = c;
poller_timer_init (&c->ping_timer, &c->ctx->poller);
c->ping_timer.dispatcher = on_client_ping_timer;
c->ping_timer.dispatcher = (poller_timer_fn) on_client_ping_timer;
c->ping_timer.user_data = c;
// 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);
// 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;
set_blocking (fd, false);
client_update_poller (c, NULL);
client_set_kill_timer (c);
poller_timer_init (&c->gni_timer, &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;
}
@@ -3718,51 +3798,6 @@ irc_initialize_server_name (struct server_context *ctx, struct error **e)
return true;
}
static bool
lock_pid_file (const char *path, struct error **e)
{
// When using XDG_RUNTIME_DIR, the file needs to either have its
// access time bumped every 6 hours, or have the sticky bit set
int fd = open (path, O_RDWR | O_CREAT,
S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH /* 644 */ | S_ISVTX /* sticky */);
if (fd < 0)
{
error_set (e, "can't open `%s': %s", path, strerror (errno));
return false;
}
struct flock lock =
{
.l_type = F_WRLCK,
.l_start = 0,
.l_whence = SEEK_SET,
.l_len = 0,
};
if (fcntl (fd, F_SETLK, &lock))
{
error_set (e, "can't lock `%s': %s", path, strerror (errno));
xclose (fd);
return false;
}
struct str pid;
str_init (&pid);
str_append_printf (&pid, "%ld", (long) getpid ());
if (ftruncate (fd, 0)
|| write (fd, pid.str, pid.len) != (ssize_t) pid.len)
{
error_set (e, "can't write to `%s': %s", path, strerror (errno));
xclose (fd);
return false;
}
str_free (&pid);
// Intentionally not closing the file descriptor; it must stay alive
// for the entire life of the application
return true;
}
static bool
irc_lock_pid_file (struct server_context *ctx, struct error **e)
{
@@ -3771,7 +3806,7 @@ irc_lock_pid_file (struct server_context *ctx, struct error **e)
return true;
char *resolved = resolve_filename (path, resolve_relative_runtime_filename);
bool result = lock_pid_file (resolved, e);
bool result = lock_pid_file (resolved, e) != -1;
free (resolved);
return result;
}
@@ -3953,6 +3988,8 @@ daemonize (struct server_context *ctx)
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

Submodule liberty updated: f6d74544f8...365aed456e

180
plugins/degesch/last-fm.lua Normal file
View File

@@ -0,0 +1,180 @@
--
-- 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 Janouch <p.janouch@gmail.com>
--
-- Permission to use, copy, modify, and/or distribute this software for any
-- purpose with or without fee is hereby granted, provided that the above
-- copyright notice and this permission notice appear in all copies.
--
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
-- SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
-- OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
--
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
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.abort () end
running = degesch.connect ("ws.audioscrobbler.com", 80, {
on_success = function (c, host)
on_connected (buffer, c, host, action)
running = nil
end,
on_error = function (e)
report_error (buffer, e)
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)

177
plugins/zyklonb/factoids Executable file
View File

@@ -0,0 +1,177 @@
#!/usr/bin/env perl
#
# ZyklonB factoids plugin
#
# Copyright 2016 Přemysl Janouch <p.janouch@gmail.com>
# 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 <STDIN>)->{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: <name> = <definition>")
unless $input =~ /^([^=]+?)(?:\s+(\d+))?\s*=\s*(.+?)\s*$/;
my ($name, $number, $definition) = ($1, $2, $3);
return &$respond("trailing numbers in names are disallowed")
if defined $2;
$db{$name} = [] unless exists $db{$name};
my $entries = $db{$name};
return &$respond("duplicate definition")
if grep { lc $_ eq lc $definition } @$entries;
push @$entries, $definition;
&$respond("saved as #${\scalar @$entries}");
db_save $db_path, \%db;
}
sub check_number {
my ($respond, $name, $number) = @_;
my $entries = $db{$name};
if ($number > @$entries) {
&$respond(qq/"$name" has only ${\scalar @$entries} definitions/);
} elsif (not $number) {
&$respond("number must not be zero");
} else {
return 1;
}
return 0;
}
sub forget {
my ($respond, $input) = @_;
return &$respond("usage: <name> <number>")
unless $input =~ /^([^=]+?)\s+(\d+)\s*$/;
my ($name, $number) = ($1, int($2));
return &$respond(qq/"$name" is undefined/)
unless exists $db{$name};
my $entries = $db{$name};
return unless check_number $respond, $name, $number;
splice @$entries, --$number, 1;
&$respond("forgotten");
db_save $db_path, \%db;
}
sub whatis {
my ($respond, $input) = @_;
return &$respond("usage: <name> [<number>]")
unless $input =~ /^([^=]+?)(?:\s+(\d+))?\s*$/;
my ($name, $number) = ($1, $2);
return &$respond(qq/"$name" is undefined/)
unless exists $db{$name};
my $entries = $db{$name};
if (defined $number) {
return unless check_number $respond, $name, $number;
&$respond(qq/"$name" is #$number $entries->[$number - 1]/);
} else {
my $i = 1;
my $definition = join ", ", map { "#${\$i++} $_" } @{$entries};
&$respond(qq/"$name" is $definition/);
}
}
sub wildcard {
my ($respond, $input) = @_;
$input =~ /=/ ? learn(@_) : whatis(@_);
}
my %commands = (
'learn' => \&learn,
'forget' => \&forget,
'whatis' => \&whatis,
'??' => \&wildcard,
);
# --- Input loop ---------------------------------------------------------------
while (my $line = <STDIN>) {
my %msg = %{parse $line};
my @args = @{$msg{args}};
# This plugin only bothers to respond to PRIVMSG messages
next unless $msg{command} eq 'PRIVMSG' and @args >= 2
and my ($cmd, $input) = $args[1] =~ /^$config{prefix}(\S+)\s*(.*)/;
# So far the only reaction is a PRIVMSG back to the sender, so all the
# handlers need is a response callback and all arguments to the command
my ($target => $quote) = ($args[0] =~ /^[#+&!]/)
? ($args[0] => "$msg{nick}: ") : ($msg{nick} => '');
# Wrap all responses so that there's space for our prefix in the message
my $respond = sub {
local ($Text::Wrap::columns, $Text::Wrap::unexpand) = 400, 0;
my $start = "PRIVMSG $target :$quote";
print for split "\n", wrap $start, $start, shift;
};
&{$commands{$cmd}}($respond, $input) if exists($commands{$cmd});
}

245
zyklonb.c
View File

@@ -1,7 +1,7 @@
/*
* zyklonb.c: the experimental IRC bot
*
* Copyright (c) 2014 - 2015, Přemysl Janouch <p.janouch@gmail.com>
* Copyright (c) 2014 - 2016, Přemysl Janouch <p.janouch@gmail.com>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
@@ -310,8 +310,52 @@ irc_get_boolean_from_config
if (set_boolean_if_valid (value, str))
return true;
error_set (e, "invalid configuration value for `%s'", name);
return false;
FAIL ("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;
FAIL ("%s: %s", "failed to set locations for the CA certificate bundle",
ERR_reason_error_string (ERR_get_error ()));
}
if (!SSL_CTX_set_default_verify_paths (ssl_ctx))
FAIL ("%s: %s", "couldn't load the default CA certificate bundle",
ERR_reason_error_string (ERR_get_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
@@ -326,31 +370,9 @@ irc_initialize_ssl_ctx (struct bot_context *ctx, struct error **e)
SSL_CTX_set_verify (ctx->ssl_ctx,
verify ? SSL_VERIFY_PEER : SSL_VERIFY_NONE, NULL);
const char *ca_file = str_map_find (&ctx->config, "ca_file");
const char *ca_path = str_map_find (&ctx->config, "ca_path");
struct error *error = NULL;
if (ca_file || ca_path)
if (!irc_initialize_ca (ctx, &error))
{
if (SSL_CTX_load_verify_locations (ctx->ssl_ctx, ca_file, ca_path))
return true;
error_set (&error, "%s: %s",
"failed to set locations for the CA certificate bundle",
ERR_reason_error_string (ERR_get_error ()));
goto ca_error;
}
if (!SSL_CTX_set_default_verify_paths (ctx->ssl_ctx))
{
error_set (&error, "%s: %s",
"couldn't load the default CA certificate bundle",
ERR_reason_error_string (ERR_get_error ()));
goto ca_error;
}
return true;
ca_error:
if (verify)
{
error_propagate (e, error);
@@ -360,6 +382,7 @@ ca_error:
// Only inform the user if we're not actually verifying
print_warning ("%s", error->message);
error_free (error);
}
return true;
}
@@ -419,8 +442,7 @@ error_ssl_1:
// multiple errors on the OpenSSL stack.
if (!error_info)
error_info = ERR_error_string (ERR_get_error (), NULL);
error_set (e, "%s: %s", "could not initialize TLS", error_info);
return false;
FAIL ("%s: %s", "could not initialize TLS", error_info);
}
static bool
@@ -433,11 +455,8 @@ irc_establish_connection (struct bot_context *ctx,
int err = getaddrinfo (host, port, &gai_hints, &gai_result);
if (err)
{
error_set (e, "%s: %s: %s",
"connection failed", "getaddrinfo", gai_strerror (err));
return false;
}
FAIL ("%s: %s: %s", "connection failed",
"getaddrinfo", gai_strerror (err));
int sockfd;
for (gai_iter = gai_result; gai_iter; gai_iter = gai_iter->ai_next)
@@ -478,10 +497,7 @@ irc_establish_connection (struct bot_context *ctx,
freeaddrinfo (gai_result);
if (!gai_iter)
{
error_set (e, "connection failed");
return false;
}
FAIL ("connection failed");
ctx->irc_fd = sockfd;
return true;
@@ -636,6 +652,9 @@ recovery_handler (int signum, siginfo_t *info, void *context)
"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
@@ -1002,87 +1021,78 @@ is_valid_plugin_name (const char *name)
return true;
}
static bool
plugin_load (struct bot_context *ctx, const char *name, struct error **e)
static struct plugin *
plugin_launch (struct bot_context *ctx, const char *name, struct error **e)
{
const char *plugin_dir = str_map_find (&ctx->config, "plugin_dir");
if (!plugin_dir)
{
error_set (e, "plugin directory not set");
return false;
}
if (!is_valid_plugin_name (name))
{
error_set (e, "invalid plugin name");
return false;
}
if (str_map_find (&ctx->plugins_by_name, name))
{
error_set (e, "the plugin has already been loaded");
return false;
}
FAIL ("plugin directory not set");
int stdin_pipe[2];
if (pipe (stdin_pipe) == -1)
{
error_set (e, "%s: %s: %s",
"failed to load the plugin", "pipe", strerror (errno));
goto fail_1;
}
FAIL ("%s: %s", "pipe", strerror (errno));
int stdout_pipe[2];
if (pipe (stdout_pipe) == -1)
{
error_set (e, "%s: %s: %s",
"failed to load the plugin", "pipe", strerror (errno));
goto fail_2;
error_set (e, "%s: %s", "pipe", strerror (errno));
goto fail_1;
}
struct str work_dir;
str_init (&work_dir);
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: %s",
"failed to load the plugin", "fork", strerror (errno));
goto fail_3;
error_set (e, "%s: %s", "fork", strerror (errno));
goto fail_2;
}
if (pid == 0)
{
// Redirect the child's stdin and stdout to the pipes
hard_assert (dup2 (stdin_pipe[0], STDIN_FILENO) != -1);
hard_assert (dup2 (stdout_pipe[1], STDOUT_FILENO) != -1);
xclose (stdin_pipe[0]);
xclose (stdout_pipe[1]);
struct str pathname;
str_init (&pathname);
str_append (&pathname, plugin_dir);
str_append_c (&pathname, '/');
str_append (&pathname, name);
// Restore some of the signal handling
signal (SIGPIPE, SIG_DFL);
char *const argv[] = { pathname.str, 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));
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]);
set_blocking (stdout_pipe[0], false);
set_blocking (stdin_pipe[1], false);
// Restore some of the signal handling
signal (SIGPIPE, SIG_DFL);
char *argv[] = { xstrdup_printf ("%s/%s", plugin_dir, name), 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);
xclose (stdin_pipe[0]);
xclose (stdout_pipe[1]);
struct plugin *plugin = xmalloc (sizeof *plugin);
plugin_init (plugin);
@@ -1091,6 +1101,32 @@ plugin_load (struct bot_context *ctx, const char *name, struct error **e)
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]);
return NULL;
}
static bool
plugin_load (struct bot_context *ctx, const char *name, struct error **e)
{
if (!is_valid_plugin_name (name))
FAIL ("invalid plugin name");
if (str_map_find (&ctx->plugins_by_name, name))
FAIL ("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);
poller_fd_init (&plugin->read_event, &ctx->poller, plugin->read_fd);
plugin->read_event.dispatcher = (poller_fd_fn) on_plugin_readable;
@@ -1105,15 +1141,6 @@ plugin_load (struct bot_context *ctx, const char *name, struct error **e)
poller_fd_set (&plugin->read_event, POLLIN);
return true;
fail_3:
xclose (stdout_pipe[0]);
xclose (stdout_pipe[1]);
fail_2:
xclose (stdin_pipe[0]);
xclose (stdin_pipe[1]);
fail_1:
return false;
}
static bool
@@ -1122,10 +1149,7 @@ 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)
{
error_set (e, "no such plugin is loaded");
return false;
}
FAIL ("no such plugin is loaded");
plugin_zombify (plugin);
@@ -1663,8 +1687,10 @@ struct irc_socks_data
};
static void
irc_on_socks_connected (void *user_data, int socket)
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;
@@ -1722,6 +1748,8 @@ irc_establish_connection_socks (struct bot_context *ctx,
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);
@@ -1750,10 +1778,7 @@ irc_connect (struct bot_context *ctx, struct error **e)
// 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)
{
error_set (e, "no hostname specified in configuration");
return false;
}
FAIL ("no hostname specified in configuration");
bool use_tls;
if (!irc_get_boolean_from_config (ctx, "tls", &use_tls, e))
@@ -1798,11 +1823,7 @@ parse_config (struct bot_context *ctx, struct error **e)
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))
{
error_set (e, "invalid configuration value for `%s'",
"reconnect_delay");
return false;
}
FAIL ("invalid configuration value for `%s'", "reconnect_delay");
hard_assert (!ctx->admin_re);
const char *admin = str_map_find (&ctx->config, "admin");