├── jjirc.png ├── config.h ├── Makefile ├── LICENSE ├── extra ├── jji ├── screenshot.sh ├── jjcatchup └── jjp-simple ├── jjp ├── jjd.c ├── README.md └── jjc /jjirc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronNGi/jj/HEAD/jjirc.png -------------------------------------------------------------------------------- /config.h: -------------------------------------------------------------------------------- 1 | /* Directory used when the IRC_DIR var is empty */ 2 | #define DEFAULT_DIR "." 3 | 4 | /* Host used when IRC_SERVER var is empty */ 5 | #define DEFAULT_HOST "irc.libera.chat" 6 | 7 | /* Port used when IRC_PORT var is empty */ 8 | #define DEFAULT_PORT "6667" 9 | 10 | /* Command used as parser when IRC_CLIENT is empty */ 11 | #define DEFAULT_CMD "jjc" 12 | 13 | /* The name of the FIFO */ 14 | #define FIFO_NAME "in" 15 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PREFIX = /usr 2 | INCS = -I. -I/usr/include 3 | LIBS = -L/usr/lib -lc -lsocket 4 | 5 | CC = cc 6 | CPPFLAGS = -D_POSIX_C_SOURCE=200809L 7 | CFLAGS += -pedantic -Wall ${INCS} ${CPPFLAGS} 8 | LDFLAGS = -s ${LIBS} 9 | 10 | SRC = jjd.c 11 | OBJS = ${SRC:.c=.o} 12 | 13 | all: jjd 14 | 15 | %.o: %.c 16 | @echo CC $< 17 | $(CC) -c $(CFLAGS) $< 18 | 19 | jjd: $(OBJS) 20 | @echo CC -o $@ 21 | $(CC) -o $@ $(OBJS) $(LDFLAGS) 22 | 23 | clean: 24 | @echo cleaning 25 | rm -f *.o jjd 26 | 27 | install: all 28 | @echo installing executable files to ${DESTDIR}${PREFIX}/bin 29 | @install -Dm 755 jjd ${DESTDIR}${PREFIX}/bin/jjd 30 | @install -Dm 755 jjc ${DESTDIR}${PREFIX}/bin/jjc 31 | @install -Dm 755 jjp ${DESTDIR}${PREFIX}/bin/jjp 32 | 33 | uninstall: 34 | @echo removing executable files from ${DESTDIR}${PREFIX}/bin 35 | @rm -f ${DESTDIR}${PREFIX}/bin/jjd 36 | @rm -f ${DESTDIR}${PREFIX}/bin/jjc 37 | @rm -f ${DESTDIR}${PREFIX}/bin/jjp 38 | 39 | 40 | .PHONY: all clean install uninstall 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT/X Consortium License 2 | 3 | © 2005-2017 Anselm R Garbe 4 | © 2008-2009 Jeroen Schot 5 | © 2007-2009 Kris Maglione 6 | © 2005 Nico Golde 7 | © 2020 Aaron Gießelmann 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a 10 | copy of this software and associated documentation files (the "Software"), 11 | to deal in the Software without restriction, including without limitation 12 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 13 | and/or sell copies of the Software, and to permit persons to whom the 14 | Software is furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in 17 | all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 22 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /extra/jji: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | # 3 | # Send stdin to a channel. 4 | # 5 | # Usage: jji []... 6 | # 7 | # Commands can be executed by prefixing them with a '/'. Typing two 8 | # slashes allows sending of messages with a slash in the beginning. 9 | # The channel can be changed using `/channel `. Omitting the 10 | # channel argument, switches to the previously set channel. The shorter 11 | # `/chan`or `/c` can also be used. `/` selects channels from the 12 | # argument list. 13 | 14 | arg0=${0##*/} 15 | fifo=${IRC_DIR:-$HOME/irc}/${IRC_HOST:-irc.libera.chat}/in 16 | 17 | ! [ "$1" ] && { 18 | printf '%s: missing channel argument\n' "$arg0" >&2 19 | exit 1 20 | } 21 | 22 | if ! [ -p "$fifo" ] || ! [ -w "$fifo" ]; then 23 | printf '%s: not a named pipe or not writeable: %s\n' \ 24 | "$arg0" "$fifo" >&2 25 | exit 1 26 | fi 27 | 28 | chan_by_number() { 29 | [ "$1" -ge $# ] && 30 | return 31 | shift "$1" 32 | last_chan=$chan 33 | chan=$1 34 | } 35 | 36 | chan=$1 37 | last_chan=${2:-$chan} 38 | 39 | prompt() { [ -t 0 ] && printf '\033[37m%s\033[0m> ' "$chan" >&2 || :; } 40 | 41 | while prompt "$chan" && IFS= read -r line || [ -n "$line" ]; do 42 | case $line in 43 | /[0-9]|/[0-9][0-9]|/[0-9]\ *|/[0-9][0-9]\ *) 44 | num=${line#?} 45 | num=${num%% *} 46 | chan_by_number "$num" "$@" 47 | continue 48 | 49 | ;; 50 | 51 | /channel\ *[!\ ]*|/chan\ *[!\ ]*|/c\ *[!\ ]*) 52 | # Strip trailing whitespace. 53 | line=${line%%${line##*[! ]}} 54 | 55 | [ "${line##* }" != "$chan" ] && { 56 | last_chan=$chan 57 | chan=${line##* } 58 | } 59 | continue 60 | ;; 61 | 62 | /channel|/chan|/c|/channel\ *|/chan\ *|/c\ *|/) 63 | tmp=$chan 64 | chan=$last_chan 65 | last_chan=$tmp 66 | continue 67 | ;; 68 | 69 | "") 70 | continue 71 | ;; 72 | 73 | /[!/]*) line=${line#?} ;; 74 | *) line="msg $chan ${line#/}" ;; 75 | esac 76 | printf %s\\n "$line" 77 | done >"$fifo" 78 | -------------------------------------------------------------------------------- /extra/screenshot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Creates the example screenshot for README.md. 4 | # Dependencies: st, xdotool, imagemagick. 5 | 6 | jjp=../jjp 7 | outfile=../jjirc.png 8 | spacing=3 9 | cols=80 10 | rows=24 11 | 12 | realcols=$((cols + 4 * spacing)) 13 | realrows=$((rows + 2 * spacing)) 14 | 15 | [ -z "$JJ_SCROT" ] && { 16 | JJ_SCROT=1 exec st -g "${realcols}x${realrows}+0+0" -e "$0" "$@" 17 | } 18 | 19 | # Clear screen and hide cursor. 20 | printf '\033[H\033[J\033[?25l' 21 | 22 | { 23 | cat <<-EOF 24 | ==> irc/irc.test.org/#test.log <== 25 | 1601460000 <-> Channel opened 26 | 1601460001 <-> jjuser [~user@host] has joined #test 27 | 1601460121 * this is us 28 | 1601460121 * successive messages from the same user are grouped 29 | 1601460180 Hello from another user 30 | 1601460180 different nicks have unique colors 31 | 1601503200 the date is shown when it changed 32 | 1601503200 ! jjuser: this is how it looks when your nick is mentioned 33 | 1601503200 (highlight) 34 | 1601503200 <-> alice1 [~alice@host] has changed nick to alice 35 | 36 | ==> irc/irc.test.org/#anotherchan.log <== 37 | 1601506533 * say cheese for the screenshot 38 | 1601506593 a says cheese 39 | EOF 40 | } \ 41 | | awk -f "$jjp" \ 42 | | tail -n"-$((rows-3))" \ 43 | | awk -vspacing="$spacing" ' 44 | function repeat(char, amount, str) { 45 | str = sprintf("%" amount "s", "") 46 | gsub(/ /, char, str) 47 | return str 48 | } 49 | BEGIN { 50 | printf "%s", repeat("\n", spacing) 51 | pre = repeat(" ", spacing * 2) 52 | } { 53 | gsub(/\a/, "") 54 | printf "%s%s\n", pre, $0 55 | } END { 56 | # Pane border. 57 | printf "%s\033[38;5;8m%s\033[0m\n", 58 | pre, repeat("─", 80) 59 | 60 | # Input. 61 | printf "%s\033[37m%s\033[0m> %s\033[42m \033[0m\n", 62 | pre, "#test", "Lorem ipsum" 63 | 64 | # Window list. 65 | printf "%s 1 \033[30;47m 2 \033[0m \033[30;41m 3 \033[0m\n", 66 | repeat(" ", spacing * 2 + 80 / 2 - 6) 67 | } 68 | ' 69 | printf 'Press enter to take a screenshot of this window or press Ctrl-C to cancel' 70 | read || exit 71 | 72 | # Move cursor up one line and clear to end of screen. 73 | printf '\033[1A\033[K' 74 | 75 | import -window "$(xdotool getwindowfocus -f)" "$outfile" 76 | 77 | printf 'Done\nPress enter to close this window' 78 | read 79 | -------------------------------------------------------------------------------- /extra/jjcatchup: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | arg0=${0##*/} 4 | help_text="Usage: $arg0 [-a] [-A] [-h] [-y ] [-m ] 5 | [-d ] [-H ] [-M ] []... 6 | 7 | Catch up on jj IRC logs starting from a relative time back 8 | 9 | -a Show all messages instead of filtering status messages 10 | -A Show almost all messages (includes important messages) 11 | -y Start n years back 12 | -m Start n months back 13 | -d Start n days back 14 | -H Start n hours back 15 | -M Start n minutes back 16 | -h Display this help text and exit 17 | 18 | The final starting point in the logs, is the sum of all options. E.g. 19 | $arg0 -d 1 -d 1 -H 8 \#chan.log # 2 days and 8 hours. If no 20 | starting point is set, 1 hour will be used. If no file arguments are 21 | given, $arg0 tries to use relevant log files from \$IRC_DIR." 22 | 23 | # Based on retail(1) by josuah. 24 | awkscript='BEGIN { 25 | # Important status messages have a type suffix after "<->". 26 | filter = "^.{10} <->" (All ? " " : "") 27 | 28 | for (i = 1; i < ARGC; i++) 29 | files[ARGV[i]] = "" 30 | 31 | for (f in files) 32 | if ((getline files[f] 0) { 36 | for (f in files) 37 | if (!(min in files) || files[f] < files[min]) 38 | min = f 39 | 40 | if (files[min] >= start && 41 | (all || files[min] !~ filter)) 42 | { 43 | if (last != min) { 44 | print nl "==> " min " <==" 45 | nl = "\n" 46 | last = min 47 | } 48 | print(files[min]) 49 | } 50 | 51 | if ((getline files[min] &2 92 | printf '%s: %s\n\n' "$arg0" "$*" 93 | } 94 | printf '%s\n' "$help_text" 95 | exit ${1:+1} 96 | } 97 | 98 | parse_options() { 99 | while getopts ":Aahy:m:d:H:M:" _opt; do 100 | case $_opt in 101 | :) usage "option '$OPTARG' requires a parameter" ;; 102 | \?) usage "unrecognized option '$OPTARG'" ;; 103 | h) help_flag=1 ;; 104 | a) all=1 ;; 105 | A) All=1 ;; 106 | *) 107 | case $OPTARG in *[!0-9]*|"") 108 | usage "Invalid number '$OPTARG'" ;; 109 | esac 110 | ;; 111 | esac 112 | 113 | case $_opt in 114 | y) duration=$((OPTARG * 31536000 + duration)) ;; 115 | m) duration=$((OPTARG * 2592000 + duration)) ;; 116 | d) duration=$((OPTARG * 86400 + duration)) ;; 117 | H) duration=$((OPTARG * 3600 + duration)) ;; 118 | M) duration=$((OPTARG * 60 + duration)) ;; 119 | esac 120 | done 121 | } 122 | 123 | main "$@" 124 | -------------------------------------------------------------------------------- /extra/jjp-simple: -------------------------------------------------------------------------------- 1 | #!/usr/bin/awk -f 2 | 3 | function isleap(year) 4 | { 5 | return (year % 4 == 0) \ 6 | && (year % 100 != 0) \ 7 | || (year % 400 == 0) 8 | } 9 | 10 | function mdays(mon, year) 11 | { 12 | return (mon == 2) \ 13 | ? (28 + isleap(year)) \ 14 | : (30 + (mon + (mon > 7)) % 2) 15 | } 16 | 17 | # Split the time in seconds since epoch into a table, with fields named 18 | # as with gmtime(3): tm["year"], tm["mon"], tm["mday"], tm["hour"], 19 | # tm["min"], tm["sec"]. 20 | function gmtime(sec, tm) 21 | { 22 | tm["year"]=1970 23 | while (sec >= (s = 86400 * (365 + isleap(tm["year"])))) { 24 | tm["year"]++ 25 | sec-=s 26 | } 27 | 28 | tm["mon"]=1 29 | while (sec >= (s = 86400 * mdays(tm["mon"], tm["year"]))) { 30 | tm["mon"]++ 31 | sec-=s 32 | } 33 | 34 | tm["mday"]=1 35 | while (sec >= (s = 86400)) { 36 | tm["mday"]++ 37 | sec-=s 38 | } 39 | 40 | tm["hour"]=0 41 | while (sec >= 3600) { 42 | tm["hour"]++ 43 | sec-=3600 44 | } 45 | 46 | tm["min"]=0 47 | while (sec >= 60) { 48 | tm["min"]++ 49 | sec-=60 50 | } 51 | 52 | tm["sec"]=sec 53 | } 54 | 55 | function tz( cmd, date, h, m) 56 | { 57 | cmd="date +%z 2>/dev/null" 58 | cmd | getline date 59 | close(cmd) 60 | 61 | h=substr(date, 2, 2) 62 | m=substr(date, 4, 2) 63 | tzoffset=substr(date, 1, 1) (h * 3600 + m * 60) 64 | } 65 | 66 | function print_header(text) 67 | { 68 | printf "%s\033[37m%s\033[0m", 69 | (NR != 1 ? "\n\n" : ""), text 70 | } 71 | 72 | BEGIN { 73 | FS=" " 74 | tz() 75 | } !NF { 76 | next 77 | } $1 == "==>" { # Handle tail's "==> filename <==" headers. 78 | gsub(/(^==> |(\.log)? <==$)/, "") 79 | gsub("/+", "/") 80 | 81 | if ((n=split($0, arr, "/")) >= 3) 82 | $0=arr[n] " (" arr[n-2] ")" 83 | 84 | delete arr 85 | print_header("== " $0 " ==") 86 | next 87 | } { 88 | if (NF < 2) { 89 | printf "\n%s", $0 90 | fflush() 91 | next 92 | } 93 | 94 | gmtime($1 + tzoffset, tm) 95 | date=sprintf("%04d/%02d/%02d", tm["year"], tm["mon"], tm["mday"]) 96 | time=sprintf("%02d:%02d", tm["hour"], tm["min"]) 97 | 98 | # Display date only when it actually changed. 99 | if (lastdate != date) { 100 | lastdate=date 101 | print_header("-- " date " --") 102 | } 103 | 104 | nickend=index($2, ">") 105 | type=substr($2, nickend+1) 106 | msg=substr($0, index($0, ">") + 2 + length(type)) 107 | nick=substr($2, 2, nickend-2) 108 | 109 | # Set the date color for highlights. 110 | if (index(type, "!")) { 111 | printf "\a" 112 | dcolor="33" 113 | } else { 114 | dcolor="38;5;8" 115 | } 116 | 117 | # When JJ_SERVERLOG is set, print messages in the default color. 118 | if (ENVIRON["JJ_SERVERLOG"] != "") { 119 | printf "\n\033[%sm%s\033[0m %s", dcolor, time, msg 120 | fflush() 121 | next 122 | } 123 | 124 | # Status message. 125 | if (nick == "-") { 126 | printf "\n\033[%sm%s \033[%sm%s\033[0m", 127 | dcolor, time, "38;5;8", msg 128 | fflush() 129 | next 130 | } 131 | 132 | isme=index(type, "*") 133 | 134 | # Alternating nick colors for better readability. 135 | if (!isme && lastnick != nick) { 136 | alt=!alt 137 | lastnick=nick 138 | } 139 | 140 | if (isme) 141 | ncolor="37" 142 | else if (alt) 143 | ncolor="31" 144 | else 145 | ncolor="0" 146 | 147 | # Handle ACTION. 148 | if (index(type, "a")) { 149 | msg=nick " " msg 150 | nick="*" 151 | } 152 | 153 | printf "\n\033[%sm%s\033[0m \033[%sm%10s\033[0m: %s", 154 | dcolor, time, ncolor, nick, msg 155 | fflush() 156 | } END { print "" } 157 | -------------------------------------------------------------------------------- /jjp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/awk -f 2 | 3 | BEGIN { 4 | no_color = "NO_COLOR" in ENVIRON 5 | 6 | if (!no_color) { 7 | color["red"] = sprintf("\033[31m") 8 | color["white"] = sprintf("\033[37m") 9 | color["default"] = sprintf("\033[m") 10 | color["dark_gray"] = sprintf("\033[38;5;8m") 11 | color["reset"] = sprintf("\033[m") 12 | 13 | color["header"] = color["default"] 14 | color["time"] = color["dark_gray"] 15 | color["time_highlight"] = color["red"] 16 | color["others_msg"] = color["default"] 17 | color["self_nick"] = color["white"] 18 | color["self_msg"] = color["default"] 19 | color["status_nick"] = color["dark_gray"] 20 | color["status_msg"] = color["dark_gray"] 21 | color["status_msg_srv"] = color["default"] 22 | } 23 | } 24 | 25 | function isleap(year) 26 | { 27 | return (year % 4 == 0) \ 28 | && (year % 100 != 0) \ 29 | || (year % 400 == 0) 30 | } 31 | 32 | function mdays(mon, year) 33 | { 34 | return (mon == 2) \ 35 | ? (28 + isleap(year)) \ 36 | : (30 + (mon + (mon > 7)) % 2) 37 | } 38 | 39 | # Split the time in seconds since epoch into a table, with fields named 40 | # as with gmtime(3): tm["year"], tm["mon"], tm["mday"], tm["hour"], 41 | # tm["min"], tm["sec"]. 42 | function gmtime(sec, tm) 43 | { 44 | tm["year"] = 1970 45 | while (sec >= (s = 86400 * (365 + isleap(tm["year"])))) { 46 | tm["year"]++ 47 | sec -= s 48 | } 49 | 50 | tm["mon"] = 1 51 | while (sec >= (s = 86400 * mdays(tm["mon"], tm["year"]))) { 52 | tm["mon"]++ 53 | sec -= s 54 | } 55 | 56 | tm["mday"] = 1 57 | while (sec >= (s = 86400)) { 58 | tm["mday"]++ 59 | sec -= s 60 | } 61 | 62 | tm["hour"] = 0 63 | while (sec >= 3600) { 64 | tm["hour"]++ 65 | sec -= 3600 66 | } 67 | 68 | tm["min"] = 0 69 | while (sec >= 60) { 70 | tm["min"]++ 71 | sec -= 60 72 | } 73 | 74 | tm["sec"] = sec 75 | } 76 | 77 | function tz( cmd, date, h, m) 78 | { 79 | cmd = "date +%z 2>/dev/null" 80 | cmd | getline date 81 | close(cmd) 82 | 83 | h = substr(date, 2, 2) 84 | m = substr(date, 4, 2) 85 | tzoffset = substr(date, 1, 1) (h * 3600 + m * 60) 86 | } 87 | 88 | function print_header(text) 89 | { 90 | printf "%s%s%s%s", (NR != 1 ? "\n\n" : ""), 91 | color["header"], text, color["reset"] 92 | } 93 | 94 | BEGIN { 95 | FS = " " 96 | tz() 97 | 98 | for (i = 32; i < 127; i++) 99 | char_to_num[sprintf("%c", i)] = i 100 | } !NF { 101 | next 102 | } $1 == "==>" { # Handle tail's "==> filename <==" headers. 103 | # Prevent message groupings from spanning over headers. 104 | lastnick = "" 105 | 106 | gsub("(^==> (.*/)?| <==$)", "") 107 | print_header("== " $0 " ==") 108 | 109 | next 110 | } { 111 | if (NF < 2) { 112 | printf "\n%s", $0 113 | lastnick = "" 114 | fflush("/dev/stdout") 115 | next 116 | } 117 | 118 | gmtime($1 + tzoffset, tm) 119 | date = sprintf("%04d/%02d/%02d", tm["year"], tm["mon"], tm["mday"]) 120 | time = sprintf("%02d:%02d", tm["hour"], tm["min"]) 121 | 122 | # Display date only when it actually changed. 123 | if (lastdate != date) { 124 | lastnick = "" 125 | lastdate = date 126 | print_header("-- " date " --") 127 | } 128 | 129 | nickend = index($2, ">") 130 | type = substr($2, nickend+1) 131 | msg = substr($0, index($0, ">") + 2 + length(type)) 132 | nick = substr($2, 2, nickend-2) 133 | 134 | # Set the date color for highlights. 135 | if (index(type, "!")) { 136 | printf "\a" 137 | time_color = color["time_highlight"] 138 | } else { 139 | time_color = color["time"] 140 | } 141 | 142 | # When JJ_SERVERLOG is set, print messages without nick and in a 143 | # different color. 144 | if (ENVIRON["JJ_SERVERLOG"] != "") { 145 | printf "\n%s%s%s %s%s", 146 | time_color, time, color["status_msg_srv"], 147 | msg, color["reset"] 148 | fflush("/dev/stdout") 149 | next 150 | } 151 | 152 | if (!no_color) { 153 | # Status message. 154 | if (nick == "-") { 155 | nick_color = color["status_nick"] 156 | msg_color = color["status_msg"] 157 | 158 | # My message. 159 | } else if (index(type, "*")) { 160 | nick_color = color["self_nick"] 161 | msg_color = color["self_msg"] 162 | 163 | # Other message. 164 | } else { 165 | # Calculate nick color if necessary. 166 | if ((nick_color = nick_colors[nick]) == "") { 167 | i = 1 168 | sum = 0 169 | while (char = substr(nick, i++, 1)) 170 | sum = sum + char_to_num[char] 171 | 172 | nick_color = nick_colors[nick] = \ 173 | sprintf("\033[38;5;%dm", sum % 232) 174 | } 175 | msg_color = color["others_msg"] 176 | } 177 | } 178 | 179 | # Message grouping. 180 | if (lastnick != nick) { 181 | print "" 182 | lastnick = nick 183 | } else if (nick != "-") 184 | nick = "" 185 | 186 | # Handle ACTION. 187 | if (index(type, "a")) { 188 | msg = (length(nick) ? nick : lastnick) " " msg 189 | nick = "*" 190 | } 191 | 192 | printf "\n%s%s%s %s%10s%s %s%s%s", 193 | time_color, time, color["reset"], 194 | nick_color, nick, color["reset"], 195 | msg_color, msg, color["reset"] 196 | fflush("/dev/stdout") 197 | } END { print "" } 198 | -------------------------------------------------------------------------------- /jjd.c: -------------------------------------------------------------------------------- 1 | /* See LICENSE file for license details. */ 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #define _WITH_DPRINTF 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | #include "config.h" 24 | 25 | #define max(a,b) ((a) < (b) ? (b) : (a)) 26 | #define IRC_MSG_MAX 512 27 | 28 | static const char *prog; 29 | static const int *pipe_fd; 30 | 31 | #if defined(__sun) && defined(__SVR4) 32 | int 33 | dprintf(int fd, const char *restrict format, ...) 34 | { 35 | // As of January 2022, illumos does not have dprintf 36 | // yet (see man printf). Provide a wrapper: 37 | va_list ap; 38 | FILE *f = fdopen(fd, "w"); 39 | 40 | if (!f) { 41 | return -1; 42 | } 43 | 44 | va_start(ap, format); 45 | int result = fprintf(f, format, ap); 46 | va_end(ap); 47 | 48 | return result; 49 | } 50 | #endif 51 | 52 | static void 53 | die(const char *fmt, ...) 54 | { 55 | int sverr = errno; 56 | va_list ap; 57 | 58 | fprintf(stderr, "%s: error: ", prog); 59 | 60 | va_start(ap, fmt); 61 | vfprintf(stderr, fmt, ap); 62 | va_end(ap); 63 | 64 | if (fmt[0] && fmt[strlen(fmt) - 1] == ':') 65 | fprintf(stderr, " %s", strerror(sverr)); 66 | 67 | fprintf(stderr, "\n"); 68 | exit(1); 69 | } 70 | 71 | static int 72 | dial(const char *host, const char *port) 73 | { 74 | int fd = -1; 75 | struct addrinfo *res, *r, hints; 76 | 77 | memset(&hints, 0, sizeof hints); 78 | hints.ai_family = AF_UNSPEC; 79 | hints.ai_socktype = SOCK_STREAM; 80 | 81 | if (getaddrinfo(host, port, &hints, &res)) 82 | die("cannot resolve '%s:%s':", host, port); 83 | 84 | for (r = res; r; r = r->ai_next) { 85 | fd = socket(r->ai_family, r->ai_socktype, r->ai_protocol); 86 | if (fd < 0) 87 | continue; 88 | 89 | if (connect(fd, r->ai_addr, r->ai_addrlen) == 0) 90 | break; 91 | 92 | close(fd); 93 | } 94 | freeaddrinfo(res); 95 | 96 | if (r == NULL) 97 | die("cannot connect to '%s:%s'", host, port); 98 | 99 | return fd; 100 | } 101 | 102 | static int 103 | read_line(FILE *fp, char *buf, size_t bufsize) 104 | { 105 | int c; 106 | size_t i = 0; 107 | 108 | if (bufsize < 2) /* must have room for 1 character and \0 */ 109 | abort(); 110 | 111 | while ((c = fgetc(fp)) != EOF) { 112 | if (c == '\n') { 113 | /* take out \n or \r\n */ 114 | if (i && buf[i - 1] == '\r') 115 | --i; 116 | buf[i] = '\0'; 117 | return 0; 118 | } 119 | /* leave room for \0 */ 120 | if (i < bufsize - 1) 121 | buf[i++] = c; 122 | } 123 | return -1; /* EOF or error */ 124 | } 125 | 126 | static void 127 | input_from_socket(FILE *fp) 128 | { 129 | char buf[IRC_MSG_MAX]; 130 | 131 | if (read_line(fp, buf, sizeof buf)) 132 | die("remote host closed the connection"); 133 | 134 | if (dprintf(*pipe_fd, "i %ju %s\n", (uintmax_t)time(NULL), buf) < 0) 135 | die("cannot write to client:"); 136 | 137 | } 138 | 139 | static void 140 | input_from_fifo(FILE *fp) 141 | { 142 | char buf[IRC_MSG_MAX]; 143 | 144 | if (read_line(fp, buf, sizeof buf)) 145 | die("failed reading fifo:"); 146 | 147 | if (dprintf(*pipe_fd, "u %ju %s\n", (uintmax_t)time(NULL), buf) < 0) 148 | die("cannot write to client:"); 149 | } 150 | 151 | static void 152 | handle_sig_child(int sig) 153 | { 154 | if (sig != SIGCHLD) 155 | abort(); 156 | 157 | die("child died\n"); 158 | } 159 | 160 | static void 161 | handle_sig_usr1(int sig) 162 | { 163 | if (dprintf(*pipe_fd, "s %ju SIGUSR1\n", (uintmax_t)time(NULL)) < 0) 164 | die("cannot write to client:"); 165 | } 166 | 167 | /* fifo is opened read and write, so there is never EOF */ 168 | static int 169 | fifo_setup(const char *dir, const char *host) 170 | { 171 | const char *fifoname = FIFO_NAME; 172 | char *path; 173 | int fd; 174 | 175 | /* +3 for //\0 */ 176 | path = malloc(strlen(dir) + strlen(host) + strlen(fifoname) + 3); 177 | if (path == NULL) 178 | die("malloc:"); 179 | 180 | sprintf(path, "%s/%s", dir, host); 181 | 182 | if (mkdir(path, 0777) == -1 && errno != EEXIST) 183 | die("cannot create directory '%s':", path); 184 | 185 | sprintf(path, "%s/%s/%s", dir, host, fifoname); 186 | 187 | if (mkfifo(path, 0600) == -1 && errno != EEXIST) 188 | die("cannot create fifo '%s':", path); 189 | 190 | fd = open(path, O_RDWR | O_NONBLOCK); 191 | if (fd == -1) 192 | die("cannot open fifo '%s':", path); 193 | 194 | free(path); 195 | 196 | return (fd); 197 | } 198 | 199 | static const char * 200 | get_username() 201 | { 202 | struct passwd *pw = getpwuid(geteuid()); 203 | if (pw == NULL) 204 | die("cannot get username:"); 205 | 206 | return pw->pw_name; 207 | } 208 | 209 | static const char * 210 | set_var(const char *name, const char *def) 211 | { 212 | if (setenv(name, def, 0) == -1) 213 | die("cannot set environment variable '%s':", name); 214 | 215 | return getenv(name); 216 | } 217 | 218 | int 219 | main(int argc, char **argv) 220 | { 221 | int sock_in, sock_out, fifo_fd; 222 | FILE *sock_fp, *fifo_fp; 223 | int child_pipe[2]; 224 | time_t trespond; 225 | fd_set rdset; 226 | sigset_t masked, not_masked; 227 | struct sigaction sa; 228 | 229 | prog = argc ? argv[0] : "jjd"; 230 | 231 | /* Signals will be caught sequentially, 232 | * never interrupting each other. */ 233 | 234 | sigemptyset(&masked); 235 | sigaddset(&masked, SIGCHLD); 236 | sigaddset(&masked, SIGUSR1); 237 | 238 | /* Enter safe region and get "vulnerable" sigset. */ 239 | 240 | sigprocmask(SIG_BLOCK, &masked, ¬_masked); 241 | 242 | memset(&sa, 0, sizeof (sa)); 243 | sa.sa_handler = handle_sig_child; 244 | sa.sa_flags = SA_RESTART | SA_NOCLDSTOP; 245 | sa.sa_mask = masked; 246 | sigaction(SIGCHLD, &sa, NULL); 247 | 248 | memset(&sa, 0, sizeof (sa)); 249 | sa.sa_handler = handle_sig_usr1; 250 | sa.sa_flags = SA_RESTART; 251 | sa.sa_mask = masked; 252 | sigaction(SIGUSR1, &sa, NULL); 253 | 254 | const char *ircdir = set_var("IRC_DIR", DEFAULT_DIR); 255 | const char *host = set_var("IRC_HOST", DEFAULT_HOST); 256 | const char *port = set_var("IRC_PORT", DEFAULT_PORT); 257 | const char *cmd = set_var("IRC_CLIENT", DEFAULT_CMD); 258 | const char *nick = set_var("IRC_NICK", get_username()); 259 | set_var("IRC_USER", nick); 260 | set_var("IRC_REALNAME", nick); 261 | 262 | fifo_fd = fifo_setup(ircdir, host); 263 | 264 | /* stdin, stdout or stderr was closed. too funny. */ 265 | if (fifo_fd <= 2) 266 | abort(); 267 | 268 | if (pipe(child_pipe)) 269 | die("pipe:"); 270 | 271 | if (getenv("PROTO") == 0) { 272 | /* Dies if cannot connect. */ 273 | sock_in = sock_out = dial(host, port); 274 | } else { 275 | /* UCSPI sockets */ 276 | sock_in = 6; 277 | sock_out = 7; 278 | } 279 | 280 | pid_t child_pid = fork(); 281 | if (child_pid < 0) 282 | die("fork:"); 283 | 284 | if (child_pid == 0) { 285 | dup2(child_pipe[0], 0); /* stdin */ 286 | dup2(sock_out, 1); /* stdout */ 287 | 288 | close(child_pipe[0]); 289 | close(child_pipe[1]); 290 | close(sock_in); 291 | close(sock_out); 292 | close(fifo_fd); 293 | 294 | execlp(cmd, cmd, NULL); 295 | die("execlp '%s':", cmd); 296 | } 297 | close(child_pipe[0]); 298 | pipe_fd = &child_pipe[1]; 299 | 300 | trespond = time(NULL); 301 | 302 | FD_ZERO(&rdset); 303 | 304 | sock_fp = fdopen(sock_in, "r"); 305 | if (sock_fp == NULL) 306 | die("could not setup buffering on input socket fd%d", sock_in); 307 | 308 | fifo_fp = fdopen(fifo_fd, "r"); 309 | if (fifo_fp == NULL) 310 | die("could not setup buffering on fifo fd%d", fifo_fd); 311 | 312 | for (;;) { 313 | int n; 314 | struct timespec tv; 315 | 316 | tv.tv_sec = 120; 317 | tv.tv_nsec = 0; 318 | 319 | FD_SET(sock_in, &rdset); 320 | FD_SET(fifo_fd, &rdset); 321 | 322 | n = pselect(max(sock_in, fifo_fd) + 1, &rdset, 323 | NULL, NULL, &tv, ¬_masked); 324 | 325 | if (n < 0) { 326 | if (errno == EINTR) 327 | continue; 328 | die("select:"); 329 | } 330 | if (n == 0) { 331 | if (time(NULL) - trespond >= 300) 332 | die("shutting down: ping timeout"); 333 | 334 | dprintf(sock_out, "PING %s\r\n", host); 335 | continue; 336 | } 337 | 338 | if (FD_ISSET(sock_in, &rdset)) { 339 | trespond = time(NULL); 340 | input_from_socket(sock_fp); 341 | } 342 | 343 | if (FD_ISSET(fifo_fd, &rdset)) { 344 | input_from_fifo(fifo_fp); 345 | } 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | jj irc 2 | ========================================================================== 3 | 4 | A file-based IRC client. 5 | 6 | ![Image of jjp sample output]( 7 | https://raw.githubusercontent.com/aaronNGi/jj/master/jjirc.png) 8 | 9 | 10 | Index 11 | -------------------------------------------------------------------------- 12 | 13 | * [Concepts](#concepts) 14 | * [Dependencies](#dependencies) 15 | * [Installation](#installation) 16 | * [Usage](#usage) 17 | * [Directory Structure](#directory-structure) 18 | * [Input Commands](#input-commands) 19 | * [Environment Variables](#environment-variables) 20 | * [Log Format](#log-format) 21 | * [Log Rotation](#log-rotation) 22 | * [Hooks](#hooks) 23 | * [Examples](#examples) 24 | * [Community](#community) 25 | 26 | 27 | Concepts 28 | -------------------------------------------------------------------------- 29 | 30 | jj irc is an evolution of the [ii(1)][ii homepage] IRC client. It is a 31 | suite of multiple smaller programs which together form a full-fledged 32 | IRC client. It consists of the following three (interchangeable) core 33 | components: 34 | 35 | 1. `jjd` - The daemon. It does the bare minimum like connecting to the 36 | IRC server and reading user input from a named pipe (fifo). It 37 | spawns a child and forwards all user and IRC messages to it. 38 | Text printed by the child is directly forwarded to the IRC 39 | server. Written in C. 40 | 2. `jjc` - The client. Gets spawned as child of `jjd` and handles the 41 | more typical IRC client things. Written in AWK. 42 | 3. `jjp` - Pretty prints log files from disk or stdin. Written in AWK. 43 | 44 | jj tries to stay true to the UNIX philosophy, which says: 45 | 46 | > Write programs that do one thing and do it well. Write programs to work 47 | > together. Write programs to handle text streams, because that is a 48 | > universal interface. 49 | 50 | The IRC output is saved in various log files, user input is read via a 51 | named pipe and certain events are handled by external tools. 52 | 53 | Log files *being* the IRC client makes it possible to use the systems 54 | text mangling utilities on them (think `grep(1)`) and it automatically 55 | adds a nice separation between the back-end (`jjq(1)`) and front-end. The 56 | front-end can easily be stopped and restarted without affecting the 57 | back-end. Building a front-end could be as easy as `tail -f` and some 58 | `tmux(1)` usage. Or it could be a complex log parser, which colors 59 | messages, right aligns nicknames and more. Want to filter out join 60 | messages? Use `grep(1)` in the pipeline. Scrollback and search? `tmux(1)`! 61 | 62 | Reading input via a named pipe makes it possible to script the input side 63 | of the front-end. For instance, want to use your editor to write and send 64 | IRC messages? No problem, you can do that! 65 | 66 | Common tasks, e.g. auto-joining channels after connecting to an IRC 67 | server, are not handled by `jjc(1)` itself, but instead are delegated to 68 | external programs (usually shell scripts). That gives the user a lot of 69 | freedom and power in terms of scriptability. To stay with the channel 70 | auto-join example: After successfully connecting, `jjc(1)` runs 71 | `irc_on_connect` if it's in `PATH`. That program can of course be written 72 | in your favorite languange. It could check the current host and, 73 | depending on server, join different channels, auth with services etc. The 74 | `irc_on_highlight` script could be used to send desktop/push 75 | notifications. The `irc_on_join` script could be used to automatically 76 | open new windows in `tmux(1)`, whenever a channel is joined. See 77 | [Hooks](#hooks) for more details. 78 | 79 | 80 | Dependencies 81 | -------------------------------------------------------------------------- 82 | 83 | Nothing but a C compiler and `awk(1)`. 84 | 85 | *Supported AWK implementations are gawk, nawk, mawk & busybox awk. The 86 | original awk described in "The AWK Programming Language" is not working.* 87 | 88 | 89 | Installation 90 | -------------------------------------------------------------------------- 91 | 92 | As root user: 93 | 94 | ```shell 95 | make 96 | make install 97 | ``` 98 | 99 | or as unpriviliged user: 100 | 101 | ```shell 102 | make 103 | make PREFIX=~/.local install 104 | PATH=~/.local/bin:$PATH 105 | ``` 106 | 107 | 108 | Usage 109 | -------------------------------------------------------------------------- 110 | 111 | None of the programs have any options and instead are controlled entirely 112 | by environment variables. To change the defaults, it makes sense to add 113 | the desired changes to `~/.profile` or similar, so that they don't have to 114 | be entered manually for every `jjd(1)` invocation. For the full list of 115 | variables, see the [Environment Variables](#environment-variables) 116 | section. 117 | 118 | ### Connecting to a Server 119 | 120 | To connect to an IRC server, simply run: 121 | 122 | ```shell 123 | jjd 124 | ``` 125 | 126 | Or: 127 | 128 | ```shell 129 | IRC_HOST=irc.rizon.net jjd 130 | ``` 131 | 132 | By default `jjd(1)` connects to irc.libera.chat, using the current user as 133 | nickname and creates the directory "irc.libera.chat" in the current 134 | working directory. Located in that directory will be the various log files 135 | and the named pipe for input. For more information, see [Directory 136 | Structure](#directory-structure) and [Input Commands](#input-commands). 137 | 138 | ### Printing a Log 139 | 140 | To save some typing, change into the directory of that new connection: 141 | 142 | ```shell 143 | cd irc.libera.chat 144 | ``` 145 | 146 | To display the IRC output, the most basic would be to simply run: 147 | 148 | ```shell 149 | cat server.log 150 | ``` 151 | 152 | `cat` could also be replaced with `jjp` for prettier output. 153 | 154 | ### Joining And Sending to a Channel 155 | 156 | To join a channel and say something in it, run: 157 | 158 | ```shell 159 | echo 'join #jj' >in 160 | ``` 161 | 162 | And then: 163 | 164 | ```shell 165 | echo 'msg #jj Hello, World!' >in 166 | ``` 167 | 168 | The output of that channel can then be read from `channels/\#jj.log`. 169 | 170 | Because nobody wants to type that full command for every message, a simple 171 | loop can make this more convenient: 172 | 173 | ```shell 174 | while IFS= read -r line; do printf 'msg #jj %s\n' "$line"; done 175 | ``` 176 | 177 | See [Examples](#examples) for a more elaborate version of that input-loop. 178 | 179 | 180 | Directory Structure 181 | -------------------------------------------------------------------------- 182 | 183 | The following shows a typical channel structure tree created by jj irc. 184 | 185 | ``` 186 | irc.libera.chat/ 187 | ├── channels/ 188 | │ ├── #jj.log 189 | │ └── nickserv.log 190 | ├── in 191 | ├── motd 192 | └── server.log 193 | ``` 194 | 195 | `server.log` is where all non-channel specific messages go. Instead of 196 | spamming `server.log` with the servers message of the day, every time the 197 | user connects, it is instead written to the file called `motd`. The file 198 | named `in` is a named pipe, used for sending messages to the IRC server. 199 | The `channels` directory contains log files of channels and private 200 | messages. 201 | 202 | > **Note:** The directories and the in-fifo are created by `jjd(1)`, while 203 | > all the logfiles are created by `jjc(1)`. 204 | 205 | 206 | Input Commands 207 | -------------------------------------------------------------------------- 208 | 209 | These are the input commands supported by `jjc(1)`. All commands are 210 | case-insensitive. "target" is a channel or a nickname. Parameters in 211 | square brackets are optional. 212 | 213 | | Command | Parameter 1 | Parameter 2 | Parameter 3 | Description | 214 | |------------|------------------------|----------------------------|------------------|-------------| 215 | | `action` | target | message | n/a | Send an action message to a user or channel. 216 | | `me` | target | message | n/a | An alias for `action`. 217 | | `away` | [away text] | n/a | n/a | Mark yourself as away. Without parameters it unsets away. 218 | | `invite` | nickname | channel | n/a | Invite a user to a channel. 219 | | `join` | channel1[,channel2]... | [password1[,password2]...] | n/a | Join channels. 220 | | `kick` | channel | nickname | [reason] | Kick a user from a channel. 221 | | `list` | n/a | n/a | n/a | Print a list of all currently open channels (including private message channels). 222 | | `ls` | n/a | n/a | n/a | An alias for `list`. 223 | | `message` | target | [message] | n/a | Send a message to a channel or user. When messaging a user, the message text can be omitted to create a private message channel. 224 | | `msg` | target | [message] | n/a | An alias for `message`. 225 | | `mode` | target | [mode] | [mode parameter] | Set various user or channel modes. `mode #channel +b` requests the channels banlist. `mode #channel` requests the current mode. 226 | | `names` | channel | n/a | n/a | Request the names listing of a channel. 227 | | `nick` | nickname | n/a | n/a | Change your nickname. 228 | | `notice` | target | [message] | n/a | Send a notice to a channel or user. The same rules as with `message` apply. 229 | | `part` | target | [reason] | n/a | Leave a channel or close a private message channel. 230 | | `quit` | [reason] | n/a | n/a | Disconnect from the server and quit jj. 231 | | `topic` | channel | n/a | n/a | Request the topic of a channel. 232 | | `topicset` | channel | [topic] | n/a | Set the topic of a channel. Omitting the second parameter removes the channel topic. 233 | | `whois` | nickname | n/a | n/a | Request the whois information of a user. 234 | 235 | Additionally, the `raw` command can be used to send IRC commands not 236 | supported by `jjc(1)`. 237 | 238 | > **Note:** Using `raw` to message or notice a channel or user results in 239 | > the message not being printed locally (to the log file). It 240 | > could be used to auth with services to prevent passwords from 241 | > being written to the logs. 242 | 243 | 244 | Environment Variables 245 | -------------------------------------------------------------------------- 246 | 247 | ### Settings 248 | 249 | | Name | Description | Default | 250 | |----------------|-----------------------------------------------------------------------------------|-------------------------| 251 | | `IRC_CLIENT` | The program spawned as child of `jjd(1)` which handles all user and IRC messages. | `jjc` | 252 | | `IRC_DIR` | Where to store the per-host directories. | `.` (current directory) | 253 | | `IRC_HOST` | The IRC host to connect to. | `irc.libera.chat` | 254 | | `IRC_NICK` | The Nickname to use. | `$USER` | 255 | | `IRC_PASSWORD` | The server password. | unset | 256 | | `IRC_PORT` | Connect using this port. | `6667` | 257 | | `IRC_REALNAME` | The real name to use. Can be seen in `whois`. | `$USER` | 258 | | `IRC_USER` | The user name to use. | `$USER` | 259 | 260 | ### Special Information for Hooks 261 | 262 | When [Hooks](#hooks) are being run, the following additional environment 263 | variables are also available to the called program: 264 | 265 | | Name | Description | 266 | |-------------------|-------------| 267 | | `IRC_ME` | Our nickname. 268 | | `IRC_NETWORK` | The networks official name as supplied by the server. Like "QuakeNet". 269 | | `IRC_TEXT` | The text of a message. Or if the event is a kick, part, or quit, it would contain the reason. If applicable, empty otherwise. 270 | | `IRC_WHERE` | In which channel the event happened. If applicable, empty otherwise. 271 | | `IRC_WHO` | Who triggered this hook, e.g. the nickname of the message author. If applicable, empty otherwise. 272 | | `IRC_CASEMAPPING` | The servers casemapping. For rfc1459 for example, its value would be `][\~A-Z }{\|^a-z`, which can be split on space and then used as arguments for `tr(1)`, to properly casefold a string. 273 | | `IRC_AWAY` | 1 when we are marked away, empty otherwise. 274 | 275 | ### Misc 276 | 277 | | Name | Description | 278 | |----------------|-------------| 279 | | `JJ_SERVERLOG` | When not empty, `jjp(1)` will print status messages using the default color. 280 | | `JJ_DEBUG` | When not emtpy, `jjc(1)` will print debug output. 281 | | `NO_COLOR` | Prevent `jjp(1)` from printing colors. 282 | 283 | 284 | Log Format 285 | -------------------------------------------------------------------------- 286 | 287 | The general log format is: 288 | 289 | ``` 290 | 1579093317 n! Hello, World! 291 | ``` 292 | 293 | The "1579093317" is the seconds since epoch. It can be converted to the 294 | current timezone and a readable format, which `jjc(1)` does automatically. 295 | The second part is the nickname of the message author with a suffix 296 | indicating message types. The "Hello, World!" is the actual body of the 297 | message. 298 | 299 | `jjc(1)` uses the following message type indicators: 300 | 301 | * `*` - This is us, we are the author of this message. 302 | * `!` - Important information in server.log or our nick is mentioned in 303 | this message. 304 | * `n` - This message is a notice. 305 | * `::` - A channel message send only to users with a certain 306 | status in that channel (@%+ etc). 307 | * `c` - A CTCP message. 308 | * `a` - an ACTION message. 309 | 310 | > **Note:** For messages without an author (server messages), a single 311 | > dash is used as nickname. 312 | 313 | 314 | Log Rotation 315 | -------------------------------------------------------------------------- 316 | 317 | When `jjd(1)` receives a `SIGUSR1`, it will send `s SIGUSR1` 318 | to `$IRC_CLIENT`. By default, `jjc(1)` will close all its open log files 319 | when receiving that message. That way, any new messages after that will 320 | automatically reopen the log files at their original location, completing 321 | the log rotation process. 322 | 323 | Hooks 324 | -------------------------------------------------------------------------- 325 | 326 | Certain events can trigger the execution of external programs. Those 327 | programs have to be executable and in `PATH` and they are run with an 328 | altered environment (See: [Environment 329 | Variables](#environment-variables)). 330 | 331 | The following programs are supported: 332 | 333 | | Name | Trigger | 334 | |-------------------|-------------------------------------------------| 335 | | `irc_on_query` | A private message channel is created. | 336 | | `irc_on_connect` | Succesfully connecting to the server. | 337 | | `irc_on_ctcp` | Receiving a CTCP message. | 338 | | `irc_on_highlight`| Own nick is mentioned in a message. | 339 | | `irc_on_invite` | Being invited to join a channel. | 340 | | `irc_on_join` | A channel or private message channel is joined. | 341 | | `irc_on_kick` | We got kicked from a channel. | 342 | | `irc_on_part` | A channel is parted. | 343 | 344 | > **Note:** When a private message contains our nick but also caused the 345 | > creation of a channel, instead of executing both, 346 | > `irc_on_query` *and* `irc_on_highlight`, only the former is 347 | > triggered. Unlike `irc_on_join`, which is always triggered, 348 | > `irc_on_query` is not triggered when *we* caused the channel 349 | > creation by messaging another user. 350 | 351 | 352 | Examples 353 | -------------------------------------------------------------------------- 354 | 355 | ### Securing the Connection 356 | 357 | Using TLS via [s6-networking][s6 networking] utilities 358 | 359 | ```shell 360 | s6-tlsclient irc.libera.chat 6697 jjd 361 | ``` 362 | 363 | Or using SSL via netcat 364 | 365 | ```shell 366 | ncat -vv --ssl --ssl-verify -c 'exec 6<&0 7>&1; PROTO=lol jjd /dev/tty' irc.libera.chat 6697 367 | ``` 368 | 369 | ### Automatic Reconnection 370 | 371 | ```shell 372 | while :; do jjd; sleep 5; done 373 | ``` 374 | 375 | ### Watching Logs 376 | 377 | ```shell 378 | tail -fn100 "$IRC_DIR/$IRC_HOST/channels/#channel.log" | jjp 379 | ``` 380 | 381 | ### Ignoring Certain Nicks 382 | 383 | ```shell 384 | tail -fn100 "$IRC_DIR/$IRC_HOST/channels/#channel.log" | 385 | grep -iv '^.\{10\} ' | jjp 386 | ``` 387 | 388 | ### Printing Last n User Messages 389 | 390 | ```shell 391 | tac "$IRC_DIR/$IRC_HOST/channels/#channel.log" | 392 | grep -m10 -v '^.\{10\} <->' | 393 | tac | jjp 394 | ``` 395 | 396 | ### A Simple Input Method 397 | 398 | See [jji](extra/jji) in [extra](extra). 399 | 400 | ### A Sample irc_on_connect 401 | 402 | ```sh 403 | #!/bin/sh -e 404 | 405 | fifo=$IRC_DIR/$IRC_HOST/in 406 | [ -p "$fifo" ] && [ -w "$fifo" ] || 407 | exit 1 408 | 409 | if [ "$IRC_HOST" = irc.libera.chat ]; then 410 | printf 'raw PRIVMSG NickServ :IDENTIFY jilles foo\n' >"$fifo" 411 | sleep .5 412 | printf 'join #jj\n' >"$fifo" 413 | fi 414 | ``` 415 | 416 | 417 | Community 418 | -------------------------------------------------------------------------- 419 | 420 | ### Contributions/Scripts 421 | 422 | * [panica][panica github] - Thin tmux wrapper around jj 423 | 424 | ### IRC 425 | 426 | Join `#jj` on irc.libera.chat 427 | 428 | [ii homepage]: https://tools.suckless.org/ii/ 429 | [s6 networking]: https://skarnet.org/software/s6-networking/ 430 | [panica github]: https://github.com/guysv/panica 431 | -------------------------------------------------------------------------------- /jjc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/awk -f 2 | # 3 | # This script parses irc messages and user input and maintains logs of all 4 | # channels and private messages. 5 | # Messages are read from stdin. Messages prefixed with "u " are 6 | # considered user input, while "i " denotes irc messages. Messages 7 | # prefixed with "s " are signals. Processed user input is printed to 8 | # stdout. 9 | 10 | BEGIN { 11 | # The individual parts of an irc messages are separated by 12 | # space(s), so we set awks field separator accordingly. 13 | FS = " " 14 | 15 | # The casemap array holds the various casemapping flavors. It 16 | # also maps upper to lowercase characters. We map A-Z to a-z 17 | # here, while the rest of the characters, of a certain 18 | # casemapping, are set while parsing the ISUPPORT messages in 19 | # on_isupport(). 20 | # The uppercase_range and lowercase_range variables also get 21 | # complimented depending on the servers casemapping. They get 22 | # used when checking whether or not a string contains any 23 | # characters which would need folding. 24 | casemap["ascii"] = "A-Z a-z" 25 | casemap["rfc1459"] = "][\\~A-Z }{|^a-z" 26 | casemap["strict"] = "][\\A-Z }{|a-z" 27 | casemap["strict_rfc1459"] = casemap["strict"] 28 | casemap["rfc1459_strict"] = casemap["strict"] 29 | casemap["uppercase_range"] = "A-Z" 30 | casemap["lowercase_range"] = "a-z" 31 | # A-Z -> a-z 32 | for (i = 65; i < 91; i++) 33 | casemap[sprintf("%c", i)] = sprintf("%c", i + 32) 34 | i = "" 35 | 36 | # General settings and connection related state. 37 | irc["connected"] = 0 38 | irc["away"] = 0 39 | irc["nick"] = ENVIRON["IRC_NICK"] 40 | irc["nick_lc"] = irc_tolower(irc["nick"]) 41 | irc["user"] = ENVIRON["IRC_USER"] 42 | irc["name"] = ENVIRON["IRC_REALNAME"] 43 | irc["password"] = ENVIRON["IRC_PASSWORD"] 44 | irc["host"] = ENVIRON["IRC_HOST"] 45 | irc["network"] = irc["host"] 46 | irc["logdir"] = ENVIRON["IRC_DIR"] "/" irc["host"] 47 | irc["motdfile"] = irc["logdir"] "/motd" 48 | 49 | # Initialize the channels array. It stores all joined channels and 50 | # their users. 51 | split("", channels) 52 | 53 | # The command array maps an event to a command name. That name, 54 | # appended to "prefix", is then used as command when calling 55 | # an external event script via ext_command(). 56 | command["prefix"] = "irc_on_" 57 | command["add"] = "query" 58 | command["connect"] = "connect" 59 | command["ctcp"] = "ctcp" 60 | command["highlight"] = "highlight" 61 | command["invite"] = "invite" 62 | command["join"] = "join" 63 | command["kick"] = "kick" 64 | command["part"] = "part" 65 | 66 | # The support array stores information obtained from the ISUPPORT 67 | # messages and IRCv3 capabilities. 68 | support["casemapping"] = "ascii" 69 | support["chantypes"] = "#&" 70 | support["prefix"] = "@+" 71 | support["statusmsg"] = "" 72 | 73 | # Try to create the "channels" directory while also creating all 74 | # parent directories as needed. 75 | cmd = "exec mkdir -p -- \"$IRC_DIR/$IRC_HOST/channels\" >&2" 76 | if (system(cmd) != 0) 77 | exit(1) 78 | cmd = "" 79 | 80 | # Initiate the irc login sequence which sends the PASS, NICK and 81 | # USER messages to the server. 82 | login(irc["nick"], irc["user"], irc["name"], irc["password"]) 83 | } 84 | 85 | # Ignore malformed lines. 86 | !/^[isu] [0-9]+ ./ { next } 87 | 88 | # Our parent process receives the raw irc messages and reads user input. 89 | # Before sending it to us, the read lines are marked with a leading "u" 90 | # and "i" for user and irc respectively. Signals received by our parent 91 | # are send as text prefixed with "s". 92 | /^u / { 93 | debug_usr() 94 | 95 | # Ignore all user input until we are fully connected to the irc 96 | # server, which is the case when all relevant ISUPPORT messages 97 | # have been parsed. 98 | if (!irc["connected"]) 99 | next 100 | 101 | handle_user_input() 102 | 103 | # All following code is specific to irc messages, so let's skip 104 | # it. 105 | next 106 | } 107 | 108 | function handle_user_input() 109 | { 110 | time = $2 111 | 112 | # Commands are case-insensitive. 113 | cmd = tolower($3) 114 | 115 | # Having saved the command, we can now remove everything up to the 116 | # actual parameters. 117 | sub(/^u [0-9]+ +[^ ]+ ?/, "") 118 | 119 | if (cmd == "") { print_out("Missing command") } 120 | else if (cmd == "action") { cmd_msg("ACTION") } 121 | else if (cmd == "me") { cmd_msg("ACTION") } 122 | else if (cmd == "away") { cmd_generic("AWAY", 0, 0) } 123 | else if (cmd == "invite") { cmd_generic("INVITE", 2, 2) } 124 | else if (cmd == "join") { cmd_generic("JOIN", 1, 2) } 125 | else if (cmd == "kick") { cmd_generic("KICK", 2, 0, 3) } 126 | else if (cmd == "list") { cmd_list() } 127 | else if (cmd == "ls") { cmd_list() } 128 | else if (cmd == "message") { cmd_msg("PRIVMSG") } 129 | else if (cmd == "msg") { cmd_msg("PRIVMSG") } 130 | else if (cmd == "notice") { cmd_msg("NOTICE") } 131 | else if (cmd == "names") { cmd_generic("NAMES", 1, 1) } 132 | else if (cmd == "nick") { cmd_generic("NICK", 1, 1) } 133 | else if (cmd == "mode") { cmd_generic("MODE", 1) } 134 | else if (cmd == "part") { cmd_part() } 135 | else if (cmd == "quit") { cmd_quit() } 136 | else if (cmd == "raw") { cmd_raw() } 137 | else if (cmd == "topic") { cmd_generic("TOPIC", 1, 1) } 138 | else if (cmd == "topicset") { cmd_generic("TOPIC", 1, 0, 2) } 139 | else if (cmd == "whois") { cmd_generic("WHOIS", 1, 0) } 140 | else { print_out("Unknown command: " cmd) } 141 | } 142 | 143 | /^s / { 144 | time = $2 145 | signal = $3 146 | 147 | # Reopen logs at their original location after log rotation. 148 | if (signal == "SIGUSR1") { 149 | print_out("Reopening all log files due to SIGUSR1") 150 | close_all() 151 | } 152 | next 153 | } 154 | 155 | # Before actual handling of the various message types, the raw irc 156 | # messages get parsed here. 157 | /^i / { 158 | parse_irc_input() 159 | debug_irc() 160 | } 161 | 162 | # Dispatch the handling of parsed messages to separate functions. 163 | cmd == "JOIN" { on_join(); next } 164 | cmd == "QUIT" { on_quit(); next } 165 | cmd == "PART" { on_part(); next } 166 | cmd == "MODE" { on_mode(); next } 167 | cmd == "PRIVMSG" { on_msg(); next } 168 | cmd == "NICK" { on_nick(); next } 169 | cmd == "NOTICE" { on_msg(); next } 170 | cmd == "TOPIC" { on_topic(); next } 171 | cmd == "KICK" { on_kick(); next } 172 | cmd == "PING" { on_ping(); next } 173 | cmd == "PONG" { next } 174 | cmd ~ /^33[23]$/ { on_topicinfo(); next } 175 | cmd == "353" { on_names(); next } 176 | cmd == "366" { on_names(); next } 177 | cmd == "INVITE" { on_invite(); next } 178 | cmd == "301" { on_whois(); next } 179 | cmd ~ /^31[1234789]$/ { on_whois(); next } 180 | cmd ~ /^30[56]$/ { on_away(); next } 181 | cmd == "221" { on_modereply(); next } 182 | cmd ~ /^32[49]$/ { on_modereply(); next } 183 | cmd == "341" { on_inviting(); next } 184 | cmd == "367" { on_banlist(); next } 185 | cmd == "401" { on_nosuchnick(); next } 186 | cmd == "433" { on_nickinuse(); next } 187 | cmd == "432" { on_erroneous(); next } 188 | cmd ~ /^37[256]$/ { on_motd(); next } 189 | cmd == "001" { on_welcome(); next } 190 | cmd == "004" { on_myinfo(); next } 191 | cmd == "005" { on_isupport(); next } 192 | cmd == "CAP" { on_cap(); next } 193 | { on_generic(); next } 194 | 195 | # Split irc messages into the global arrays "tag", "source" and "param". 196 | # The irc command (name or numerical) is stored in the global variable 197 | # "cmd". All arrays have a corresponding variable holding the number of 198 | # array elements. They are called ntag, nsource and nparam. 199 | function parse_irc_input( field, i, line, has_trailing, trailing) 200 | { 201 | time = $2 202 | 203 | # Skip the "i