$ git clone https://ion.nu/git/relaybot
commit 5916bb749a44aab9524c4e28fed8d0a3a4bf7f2d
Author: Alicia <...>
Date: Mon Mar 27 23:02:14 2017 +0200
Initial commit
diff --git a/API.doc b/API.doc
new file mode 100644
index 0000000..c44fc84
--- /dev/null
+++ b/API.doc
@@ -0,0 +1,19 @@
+Variables:
+EXECDIR Home of relaybot.sh and all the modules + static data
+DATADIR Home of things like messages to relay
+
+Functions:
+addcmd <command> <function>
+delcmd <command> <function>
+addeventhandler <event> <function>
+deleventhandler <event> <function>
+handleevent <event> <data...>
+toggle_cammode <mode>
+
+Command functions are called as <function> <from> <parameters>
+
+Events commonly pass along the following parameters:
+chatmsg <msg> <from> (<color>, if applicable>)
+join <nickname>
+part <nickname>
+camup <nickname>
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..b01846c
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,5 @@
+CFLAGS=$(shell pkg-config --cflags gdk-pixbuf-2.0 libavcodec libswscale libavutil)
+LIBS=$(shell pkg-config --libs gdk-pixbuf-2.0 libavcodec libswscale libavutil)
+
+camutil: camutil.c
+ $(CC) $(CFLAGS) $^ $(LIBS) -o $@
diff --git a/camutil.c b/camutil.c
new file mode 100644
index 0000000..0c905e7
--- /dev/null
+++ b/camutil.c
@@ -0,0 +1,165 @@
+/*
+ camutil, a utility to give relaybot a face (or something)
+ Copyright (C) 2015 alicia@ion.nu
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, version 3 of the License.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <signal.h>
+#include <gdk-pixbuf/gdk-pixbuf.h>
+#include <libavcodec/avcodec.h>
+#include <sys/prctl.h>
+#include <libswscale/swscale.h>
+#if LIBAVUTIL_VERSION_MAJOR>50 || (LIBAVUTIL_VERSION_MAJOR==50 && LIBAVUTIL_VERSION_MINOR>37)
+ #include <libavutil/imgutils.h>
+#else
+ #include <libavcore/imgutils.h>
+#endif
+
+int running=1;
+
+void stoprunning(int x)
+{
+ running=0;
+}
+
+void fullwrite(int fd, void* buf, int len)
+{
+ while(len>0)
+ {
+ int wrote=write(fd, buf, len);
+ if(wrote<1){return;}
+ len-=wrote;
+ buf+=wrote;
+ }
+}
+
+void fixpixels(GdkPixbuf* img)
+{
+// printf("Channels: %i\n", gdk_pixbuf_get_n_channels(img));
+ if(gdk_pixbuf_get_n_channels(img)==4)
+ {
+ unsigned char* pixels=gdk_pixbuf_get_pixels(img);
+ unsigned int i;
+ unsigned int pixelcount=gdk_pixbuf_get_width(img)*gdk_pixbuf_get_height(img);
+ for(i=0; i<pixelcount; ++i)
+ {
+ memmove(&pixels[i*3], &pixels[i*4], 3);
+ }
+ }
+}
+
+int main(int argc, char** argv)
+{
+ signal(SIGUSR1, stoprunning);
+ avcodec_register_all();
+ AVCodec* vencoder=avcodec_find_encoder(AV_CODEC_ID_FLV1);
+ unsigned int delay=500000;
+ // Set up camera
+ AVCodecContext* ctx=avcodec_alloc_context3(vencoder);
+ unsigned int i;
+ GdkPixbuf* img;
+ GdkPixbufAnimation* anim;
+ void** images=0;
+ unsigned int imgcount=0;
+ for(i=1; i<argc; ++i)
+ {
+ anim=gdk_pixbuf_animation_new_from_file(argv[i], 0);
+ if(!anim){continue;}
+ if(gdk_pixbuf_animation_is_static_image(anim)) // Not sure if this is necessary or if it'd just be a single frame anyway
+ {
+ ++imgcount;
+ images=realloc(images, sizeof(void*)*imgcount);
+ img=gdk_pixbuf_animation_get_static_image(anim);
+ fixpixels(img);
+ images[imgcount-1]=gdk_pixbuf_get_pixels(img);
+ }else{
+ GTimeVal atime={0,0};
+ GdkPixbufAnimationIter* iter=gdk_pixbuf_animation_get_iter(anim, &atime);
+ GdkPixbuf* firstimg=gdk_pixbuf_animation_iter_get_pixbuf(iter);
+ ++imgcount;
+ images=realloc(images, sizeof(void*)*imgcount);
+ img=gdk_pixbuf_copy(firstimg);
+ fixpixels(img);
+ images[imgcount-1]=gdk_pixbuf_get_pixels(img);
+ while(1)
+ {
+ if(gdk_pixbuf_animation_iter_advance(iter, &atime))
+ {
+ img=gdk_pixbuf_animation_iter_get_pixbuf(iter);
+ if(img==firstimg){break;}
+ img=gdk_pixbuf_copy(img);
+ fixpixels(img);
+ }
+ ++imgcount;
+ images=realloc(images, sizeof(void*)*imgcount);
+ images[imgcount-1]=gdk_pixbuf_get_pixels(img);
+ g_time_val_add(&atime, 100000);
+ }
+ }
+ }
+ ctx->width=gdk_pixbuf_get_width(img);
+ ctx->height=gdk_pixbuf_get_height(img);
+ ctx->pix_fmt=PIX_FMT_YUV420P;
+ ctx->time_base.num=1;
+ ctx->time_base.den=10;
+ avcodec_open2(ctx, vencoder, 0);
+ AVFrame* frame=av_frame_alloc();
+ frame->format=PIX_FMT_RGB24;
+ frame->width=ctx->width;
+ frame->height=ctx->height;
+ av_image_alloc(frame->data, frame->linesize, ctx->width, ctx->height, frame->format, 1);
+ AVPacket packet;
+ packet.buf=0;
+ packet.data=0;
+ packet.size=0;
+ packet.dts=AV_NOPTS_VALUE;
+ packet.pts=AV_NOPTS_VALUE;
+
+ // Set up frame for conversion from the camera's format to a format the encoder can use
+ AVFrame* dstframe=av_frame_alloc();
+ dstframe->format=ctx->pix_fmt;
+ dstframe->width=ctx->width;
+ dstframe->height=ctx->height;
+ av_image_alloc(dstframe->data, dstframe->linesize, ctx->width, ctx->height, ctx->pix_fmt, 1);
+
+ struct SwsContext* swsctx=sws_getContext(frame->width, frame->height, PIX_FMT_RGB24, frame->width, frame->height, AV_PIX_FMT_YUV420P, 0, 0, 0, 0);
+
+ i=0;
+ while(running)
+ {
+ usleep(delay);
+ if(delay>100000){delay-=50000;}
+ ++i;
+ if(i>=imgcount){i=0;}
+ frame->data[0]=images[i];
+ int gotpacket;
+ sws_scale(swsctx, (const uint8_t*const*)frame->data, frame->linesize, 0, frame->height, dstframe->data, dstframe->linesize);
+ av_init_packet(&packet);
+ packet.data=0;
+ packet.size=0;
+ avcodec_encode_video2(ctx, &packet, dstframe, &gotpacket);
+ unsigned char frameinfo=0x22; // Note: differentiating between keyframes and non-keyframes seems to break stuff, so let's just go with all being interframes (1=keyframe, 2=interframe, 3=disposable interframe)
+ dprintf(1, "/video %i\n", packet.size+1);
+ fullwrite(1, &frameinfo, 1);
+ fullwrite(1, packet.data, packet.size);
+
+ av_free_packet(&packet);
+ }
+ sws_freeContext(swsctx);
+ return 1;
+}
diff --git a/modules/activitystats.sh b/modules/activitystats.sh
new file mode 100755
index 0000000..9b05de2
--- /dev/null
+++ b/modules/activitystats.sh
@@ -0,0 +1,13 @@
+#!/bin/sh
+if [ -z "$irclog" ]; then echo 'ERROR: the activitystats module requires $irclog to be defined' >&2; fi
+
+activitystats()
+{
+ days="`grep -c '^--- Day changed ' "$irclog"`"
+ hour="`date +%H`"
+ msgcount="`grep -c "^${hour}:[0-9][0-9] <" "$irclog"`"
+ perminute="`expr "${msgcount}000" / "$days" / 60`"
+ say "This hour of day gets `decimals "$perminute" 3` messages per minute on average"
+}
+
+addcmd '!activitystats' activitystats
diff --git a/modules/camface.sh b/modules/camface.sh
new file mode 100755
index 0000000..4fb12dc
--- /dev/null
+++ b/modules/camface.sh
@@ -0,0 +1,26 @@
+#!/bin/sh
+if [ "x${protocol}" != "xtc_client" ]; then echo 'WARNING: the camface module is unlikely to make sense on other protocols than tinychat/tc_client' >&2; fi
+camface()
+{
+ if toggle_cammode face; then
+ "${EXECDIR}/camutil" \
+"${EXECDIR}/relaybotface/relaybot1.png" \
+"${EXECDIR}/relaybotface/relaybot1.png" \
+"${EXECDIR}/relaybotface/relaybot2.png" \
+"${EXECDIR}/relaybotface/relaybot2.png" \
+"${EXECDIR}/relaybotface/relaybot3.png" \
+"${EXECDIR}/relaybotface/relaybot3.png" \
+"${EXECDIR}/relaybotface/relaybot4.png" \
+"${EXECDIR}/relaybotface/relaybot4.png" \
+"${EXECDIR}/relaybotface/relaybot4.png" \
+"${EXECDIR}/relaybotface/relaybot4.png" \
+"${EXECDIR}/relaybotface/relaybot3.png" \
+"${EXECDIR}/relaybotface/relaybot3.png" \
+"${EXECDIR}/relaybotface/relaybot2.png" \
+"${EXECDIR}/relaybotface/relaybot2.png" \
+"${EXECDIR}/relaybotface/relaybot1.png" \
+"${EXECDIR}/relaybotface/relaybot1.png" &
+ fi
+}
+
+addcmd '!cam' camface
diff --git a/modules/conversation.sh b/modules/conversation.sh
new file mode 100755
index 0000000..658e337
--- /dev/null
+++ b/modules/conversation.sh
@@ -0,0 +1,46 @@
+#!/bin/false
+getspecies()
+{
+ if echo "$species" | grep -q "^${1}="; then
+ echo "$species" | sed -n -e "s/^${1}=//p"
+ else
+ echo 'human'
+ fi
+}
+
+conversation()
+{
+ chatmsg="$1"
+ from="$2"
+
+ if echo "$chatmsg" | grep -i -q '\(thx\|tha*nks*\|thanks* *y*o*u\)[,.]* *\(relay\|bots*\|relaybots*\)[!,.]*$'; then
+ say "you're welcome `getspecies "$from"`"
+
+ elif echo "$chatmsg" | grep -q '^ *good *\(relay\|\)bot *$'; then
+ say "good `getspecies "$from"`"
+
+ elif echo "$chatmsg" | grep -i -q '^\(hee*yy*a*\|hii*\|hee*ll*oo*\)\(\| there\),*\(\| fellow\) \(homos\|homosexuals\|everyone*\|y*o*u *all\|y*o*u *guys\|everybody\|bots\|relaybot\|all\|yall\|y.all\|lesbos\|ladies\|friends\|girls\|gays\)\([ \.!,].*\|\)[ ~]*$'; then
+ sayonce "Hey ${from}"
+
+ elif echo "$chatmsg" | grep -i -q '^\(good *night\|good *nite\|g.night\|g.nite\|nights*\|night night\|nite nite\),* \(everyone*\|y*o*u *all\|y*o*u *guys\|everybody\|bots\|relaybot\|all\|yall\|y.all\|lesbos\|ladies\)\([ \.!,].*\|\)$'; then
+ sayonce "Good night ${from}"
+
+ elif echo "$chatmsg" | grep -i -q '^\(good *morning*\|good *morn\|g.morning*\|g.morn\|morning\),* \(everyone*\|y*o*u *all\|y*o*u *guys\|everybody\|bots\|relaybot\|all\|yall\|y.all\|lesbos\|ladies\)\([ \.!,].*\|\)$'; then
+ sayonce "Good morning ${from}"
+
+ elif echo "$chatmsg" | grep -i -q '^\(<3\|<3\|I love\|I love you\|I luv\|I luv u\),* \(everyone*\|y*o*u *all\|y*o*u *guys\|everybody\|bots\|relaybot\|yall\|y.all\)\([ \.!,].*\|\)$' || echo "$chatmsg" | grep -i -x -q ' *relaybot[:,]* *\(<3\|I love you\) *'; then
+ sayonce "<3 ${from}"
+
+ elif (echo "$chatmsg" | grep -i -q '\(relay\|\)bots*\b' && echo "$chatmsg" | grep -i -q '^\(.* \|\)how old ') || echo "$chatmsg" | grep -i -q "^\(\|.* \)how old \(is\|are\) \(everyone*\|y*o*u *all\|y*o*u *guys\|y'*all\)"; then
+ sec="`date +%s`"
+ sec="`expr "$sec" - 1429999200`"
+ weeks="`expr "$sec" / 60 / 60 / 24 / 7`"
+ say "${weeks} weeks" # TODO: Count years and months instead?
+
+ else
+ return 1
+ fi
+ return 0
+}
+
+addeventhandler chatmsg conversation
diff --git a/modules/conversion.sh b/modules/conversion.sh
new file mode 100755
index 0000000..75847f3
--- /dev/null
+++ b/modules/conversion.sh
@@ -0,0 +1,97 @@
+#!/bin/sh
+conversion_get_feet="s/'.*//;s/.*\"//"
+conversion_get_inch="s/\".*//;s/.*'//"
+imperial_to_metric()
+{
+ input="$2"
+ if ! echo "$input" | grep -q "['\"]"; then input="${input}\""; fi
+ feet="`echo "$input" | sed -e "$conversion_get_feet"`"
+ inches="`echo "$input" | sed -e "$conversion_get_inch"`"
+ if [ "x${feet}" = "x" ]; then feet=0; fi
+ if [ "x${inches}" = "x" ]; then inches=0; fi
+ decimals="`countdecimals "$inches"`"
+ inches="`echo "$inches" | sed -e 's/\.//'`"
+ feet="`tenpow "$feet" "$decimals"`"
+ inches="`expr "$feet" '*' 12 + "$inches"`"
+ decimals="`expr "$decimals" + 2`"
+ centimeters="`expr "$inches" '*' 254`"
+ centimeters="`decimals "$centimeters" "$decimals"`"
+ if [ "x${centimeters}" = "x" ]; then return; fi
+ say "${centimeters} cm"
+}
+
+metric_to_imperial()
+{
+ input="`echo "$2" | tr -d '[:alpha:]'`"
+ decimals="`countdecimals "$input"`"
+ input="`echo "$input" | sed -e 's/\.//'`"
+ if echo "$2" | grep -i -q '[0-9]km$'; then
+ input="${input}00000"
+ elif echo "$2" | grep -i -q '[0-9]m$'; then
+ input="${input}00"
+ fi
+ inch="`tenpow 254 "$decimals"`"
+ inches="`expr "$input" '*' 100000 / "$inch"`"
+ if [ "x${inches}" = "x" ]; then return; fi
+ feet="`expr "$inches" '/' 12000`"
+ inches="`expr "$inches" '%' 12000`"
+ inches="`decimals "$inches" 3`"
+ say "${feet}'${inches}\""
+}
+
+celsius_to_fahrenheit()
+{
+ c="`echo "$2" | sed -e 's|[/ .]||g'`"
+ decimals="`echo "$2" | sed -n -e 's/^.*\.//p' | tr -d '\r\n' | wc -c`"
+ dec='.'
+ for x in `seq "$decimals"`; do dec="${dec}."; done
+ zero="`echo "$dec" | tr . 0`"
+ expr "$c" '*' 90 / 5 + "32${zero}" | sed -e "s/${dec}$/\.&°F/" | sayx
+}
+
+fahrenheit_to_celsius()
+{
+ f="`echo "$2" | sed -e 's|[/ .]||g'`"
+ decimals="`echo "$2" | sed -n -e 's/^.*\.//p' | tr -d '\r\n' | wc -c`"
+ dec=''
+ for x in `seq "$decimals"`; do dec="${dec}."; done
+ zero="`echo "$dec" | tr . 0`"
+ expr '(' "$f" - "32${zero}" ')' '*' 50 / 9 | sed -e "s/${dec}.$/\\.&°C/" | sayx
+}
+
+gallons_to_liters()
+{
+ g="`echo "$2" | sed -e 's|[/ .]||g'`"
+ expr "$g" '*' 379 | sed -e 's/..$/\.& liters/' | sayx
+}
+
+lbs_to_kg()
+{
+ decimals="`countdecimals "$2"`"
+ lbs="`echo "$2" | sed -e 's|[/ .]||g'`"
+ decimals="`expr "$decimals" + 5`"
+ kg="`expr "$lbs" '*' 45359`"
+ kg="`decimals "$kg" "$decimals"`"
+ say "${kg} kg"
+}
+
+kg_to_lbs()
+{
+ decimals="`countdecimals "$2"`"
+ kg="`echo "$2" | sed -e 's|[/ .]||g'`"
+ decimals="`expr "$decimals" + 5`"
+ lbs="`expr "$kg" '*' 220462`"
+ lbs="`decimals "$lbs" "$decimals"`"
+ say "${lbs} lbs"
+}
+
+addcmd '!imperial' metric_to_imperial
+addcmd '!metric' imperial_to_metric
+addcmd '!f' celsius_to_fahrenheit
+addcmd '!F' celsius_to_fahrenheit
+addcmd '!c' fahrenheit_to_celsius
+addcmd '!C' fahrenheit_to_celsius
+addcmd '!l' gallons_to_liters
+addcmd '!L' gallons_to_liters
+addcmd '!kg' lbs_to_kg
+addcmd '!lbs' kg_to_lbs
diff --git a/modules/countdown.sh b/modules/countdown.sh
new file mode 100755
index 0000000..ffe5c73
--- /dev/null
+++ b/modules/countdown.sh
@@ -0,0 +1,63 @@
+#!/bin/sh
+countdown_cmd()
+{
+ now="`date +%s`"
+ target="`date -d "$countdownto" +%s`"
+ diff="`expr "$target" - "$now"`"
+ if [ "$diff" -lt 0 ]; then say "The target time has been reached"; return; fi
+ days="`expr "$diff" / 86400`"
+ diff="`expr "$diff" '%' 86400`"
+ hours="`expr "$diff" / 3600`"
+ diff="`expr "$diff" '%' 3600`"
+ minutes="`expr "$diff" / 60`"
+ output=''
+ if [ "$days" != "0" ]; then
+ output="${days} day"
+ if [ "$days" != "1" ]; then output="${output}s"; fi
+ fi
+ if [ "$hours" != "0" ]; then
+ if [ -n "$output" ]; then output="${output}, "; fi
+ output="${output}${hours} hour"
+ if [ "$hours" != "1" ]; then output="${output}s"; fi
+ fi
+ if [ "$minutes" != "0" ]; then
+ if [ -n "$output" ]; then output="${output}, "; fi
+ output="${output}${minutes} minute"
+ if [ "$minutes" != "1" ]; then output="${output}s"; fi
+ fi
+ say "$output"
+}
+
+# Just a copy of the above, except for the / in the output
+countdown_cmd_secret()
+{
+ now="`date +%s`"
+ target="`date -d "$countdownto" +%s`"
+ diff="`expr "$target" - "$now"`"
+ if [ "$diff" -lt 0 ]; then echo "/The target time has been reached"; return; fi
+ days="`expr "$diff" / 86400`"
+ diff="`expr "$diff" '%' 86400`"
+ hours="`expr "$diff" / 3600`"
+ diff="`expr "$diff" '%' 3600`"
+ minutes="`expr "$diff" / 60`"
+ output=''
+ if [ "$days" != "0" ]; then
+ output="${days} day"
+ if [ "$days" != "1" ]; then output="${output}s"; fi
+ fi
+ if [ "$hours" != "0" ]; then
+ if [ -n "$output" ]; then output="${output}, "; fi
+ output="${output}${hours} hour"
+ if [ "$hours" != "1" ]; then output="${output}s"; fi
+ fi
+ if [ "$minutes" != "0" ]; then
+ if [ -n "$output" ]; then output="${output}, "; fi
+ output="${output}${minutes} minute"
+ if [ "$minutes" != "1" ]; then output="${output}s"; fi
+ fi
+ echo "/ $output"
+}
+
+addcmd '!countdown' countdown_cmd
+addcmd '/!countdown' countdown_cmd_secret
+if [ -z "$countdownto" ]; then echo 'WARNING: the countdown module requires $countdownto to be set to a date' >&2; fi
diff --git a/modules/gitreport.sh b/modules/gitreport.sh
new file mode 100644
index 0000000..41baaca
--- /dev/null
+++ b/modules/gitreport.sh
@@ -0,0 +1,27 @@
+#!/bin/sh
+gitreport="`date +%s | xargs expr 30 +`"
+mkdir -p "${DATADIR}/gitreport"
+gitreport_check()
+{
+ if [ "`date +%s`" -gt "$gitreport" ]; then
+ gitreport="`date +%s | xargs expr 300 +`"
+ for repo in ${gitreport_repos}; do
+ fsrepo="`echo "$repo" | sed -e 's/[^-a-zA-Z0-9]/_/g'`"
+ commit="`curl -s "${repo}/refs/heads/master"`"
+ oldcommit="`cat "${DATADIR}/gitreport/${fsrepo}" 2> /dev/null`"
+ if [ -z "$oldcommit" ]; then echo "$commit" > "${DATADIR}/gitreport/${fsrepo}"; return; fi
+ if [ -z "$commit" ]; then continue; fi
+ if [ "x${commit}" = "x${oldcommit}" ]; then continue; fi
+ echo "$commit" > "${DATADIR}/gitreport/${fsrepo}"
+ object="`echo "$commit" | sed -e 's|^..|&/|'`"
+ msg="`(printf '\037\213\010\000\026\022\360\127'; curl -s "${repo}/objects/${object}") | gzip -d 2> /dev/null | sed -e '1,/^$/d'`"
+ if [ -z "$msg" ]; then continue; fi
+ say "New commit in `basename "$repo"`: ${msg}"
+ done
+ fi
+}
+addeventhandler chatmsg gitreport_check
+addeventhandler join gitreport_check
+addeventhandler part gitreport_check
+addeventhandler ping gitreport_check
+if [ -z "$gitreport_repos" ]; then echo 'WARNING: the gitreport module requires $gitreport_repos to be set to a list of git repository URLs' >&2; fi
diff --git a/modules/imgcam.sh b/modules/imgcam.sh
new file mode 100755
index 0000000..7a8d0da
--- /dev/null
+++ b/modules/imgcam.sh
@@ -0,0 +1,22 @@
+#!/bin/sh
+if [ "x${protocol}" != "xtc_client" ]; then echo 'WARNING: the imgcam module is unlikely to make sense on other protocols than tinychat/tc_client' >&2; fi
+imgcam()
+{
+ if toggle_cammode img; then
+ img="$2"
+ if ! wget -O img.img "$img"; then
+ cammode=''
+ echo '/camdown'
+ continue
+ fi
+ if echo "$img" | grep -q '\.gif$'; then
+ convert img.img -coalesce -resize 320x -alpha remove -alpha off img.gif
+ "${EXECDIR}/camutil" img.gif &
+ else
+ convert img.img -resize 320x -alpha remove -alpha off img.png
+ "${EXECDIR}/camutil" img.png &
+ fi
+ fi
+}
+
+addcmd '!img' imgcam
diff --git a/modules/magnifier.sh b/modules/magnifier.sh
new file mode 100755
index 0000000..07931c4
--- /dev/null
+++ b/modules/magnifier.sh
@@ -0,0 +1,22 @@
+#!/bin/sh
+if [ "x${protocol}" != "xtc_client" ]; then echo 'WARNING: the magnifier module is unlikely to make sense on other protocols than tinychat/tc_client' >&2; fi
+magnifier_toggle()
+{
+ deleventhandler chatmsg magnifier
+ if toggle_cammode magnifier; then
+ addeventhandler chatmsg magnifier
+ magnifier
+ fi
+}
+
+magnifier()
+{
+ chatmsg="$1"
+ from="$2"
+ if [ "x${cammode}" != 'xmagnifier' ]; then return; fi
+ convert -font /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf -size 320x240 -background '#ffffff' -fill "#000000" "caption:${from}: ${chatmsg}" /tmp/magnifier.png
+ pkill -x camutil
+ "${EXECDIR}/camutil" /tmp/magnifier.png &
+}
+
+addcmd '!magnifier' magnifier_toggle
diff --git a/modules/neverhaveiever.sh b/modules/neverhaveiever.sh
new file mode 100755
index 0000000..7d7a658
--- /dev/null
+++ b/modules/neverhaveiever.sh
@@ -0,0 +1,102 @@
+#!/bin/sh
+if [ "x${protocol}" != "xtc_client" ]; then echo 'WARNING: the neverhaveiever module is unlikely to make sense on other protocols than tinychat/tc_client' >&2; fi
+neverhaveiever_join()
+{
+ if [ "x${cammode}" != "xneverhaveiever" ]; then return; fi
+ from="$1"
+ if [ -n "$2" ]; then return; fi
+ if echo "$neverhaveiever" | grep -q "^${from}: "; then
+ say "${from}: you're already in the game"
+ elif [ "x${neverhaveiever}" != "x" ] && echo "$neverhaveiever" | grep -v -q ": ${neverhaveieverstartnum}\(\| (next)\)$"; then
+ say "${from}: the game has already started, join next round instead"
+ else
+ if [ "x${neverhaveiever}" = "x" ]; then
+ neverhaveiever="${from}: ${neverhaveieverstartnum} (next)"
+ else
+ neverhaveiever="${neverhaveiever}
+${from}: ${neverhaveieverstartnum}"
+ fi
+ convert -font /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf -size 320x240 -background '#ffffff' -fill "#000000" "label:${neverhaveiever}" /tmp/neverhaveiever.png
+ pkill -x camutil
+ "${EXECDIR}/camutil" /tmp/neverhaveiever.png &
+ fi
+}
+
+neverhaveiever_quit()
+{
+ if [ "x${cammode}" != "xneverhaveiever" ]; then return; fi
+ from="$1"
+ neverhaveiever="`echo "$neverhaveiever" | sed -e "/^${from}: /d"`"
+ if [ "x${neverhaveiever}" = "x" ]; then # No users left = stop
+ toggle_cammode neverhaveiever
+ elif [ "`echo "$neverhaveiever" | wc -l`" = "1" ]; then # Win by default
+ say "Winner: `echo "$neverhaveiever" | sed -e 's/: .*//'`"
+ toggle_cammode neverhaveiever
+ else
+ convert -font /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf -size 320x240 -background '#ffffff' -fill "#000000" "label:${neverhaveiever}" /tmp/neverhaveiever.png
+ pkill -x camutil
+ "${EXECDIR}/camutil" /tmp/neverhaveiever.png &
+ fi
+}
+
+neverhaveiever_losepoint()
+{
+ if [ "x${cammode}" != "xneverhaveiever" ]; then return; fi
+ from="$1"
+ if [ -n "$2" ]; then return; fi
+ points="`echo "$neverhaveiever" | sed -n -e "s/ (next)//; s/^${from}: //p"`"
+ if [ "x${points}" = "x" ]; then
+ say "${from}: you don't seem to have joined the game"
+ else
+ points="`expr "$points" - 1`"
+ neverhaveiever="`echo "$neverhaveiever" | sed -e "s/^${from}: [0-9]*/${from}: ${points}/"`"
+ convert -font /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf -size 320x240 -background '#ffffff' -fill "#000000" "label:${neverhaveiever}" /tmp/neverhaveiever.png
+ pkill -x camutil
+ "${EXECDIR}/camutil" /tmp/neverhaveiever.png &
+ neverhaveiever="`echo "$neverhaveiever" | sed -e "/^.*: 0/d"`"
+ if [ "`echo "$neverhaveiever" | wc -l`" = "1" ]; then
+ say "Winner: `echo "$neverhaveiever" | sed -e 's/: .*//'`"
+ toggle_cammode neverhaveiever
+ fi
+ fi
+}
+
+neverhaveiever_addnext='n;s/$/ (next)/'
+neverhaveiever_next()
+{
+ if [ "x${cammode}" != "xneverhaveiever" ]; then return; fi
+ from="$1"
+ if ! echo "$2" | grep -qi '^have I ever'; then return; fi
+ neverhaveiever="`echo "$neverhaveiever" | sed -e "s/ (next)//; /^${from}: /{${neverhaveiever_addnext};}"`"
+ convert -font /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf -size 320x240 -background '#ffffff' -fill "#000000" "label:${neverhaveiever}" /tmp/neverhaveiever.png
+ pkill -x camutil
+ "${EXECDIR}/camutil" /tmp/neverhaveiever.png &
+}
+
+neverhaveiever_toggle()
+{
+ delcmd '-1' neverhaveiever_losepoint
+ delcmd '!join' neverhaveiever_join
+ delcmd '!quit' neverhaveiever_quit
+ delcmd 'never' neverhaveiever_next
+ delcmd 'Never' neverhaveiever_next
+ delcmd 'NEVER' neverhaveiever_next
+ if toggle_cammode neverhaveiever; then
+ neverhaveieverstartnum="`echo "$2" | sed -e 's/ //g'`"
+ if [ -z "$neverhaveieverstartnum" ]; then neverhaveieverstartnum=5; fi
+ convert -font /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf -size 320x240 -background '#ffffff' -fill "#000000" "caption:Type !join to join never-have-I-ever, type '-1' when losing a point, !quit to quit" /tmp/neverhaveiever.png
+ "${EXECDIR}/camutil" /tmp/neverhaveiever.png &
+ neverhaveiever=''
+ addcmd '-1' neverhaveiever_losepoint
+ addcmd '!join' neverhaveiever_join
+ addcmd '!quit' neverhaveiever_quit
+ addcmd 'never' neverhaveiever_next
+ addcmd 'Never' neverhaveiever_next
+ addcmd 'NEVER' neverhaveiever_next
+ fi
+}
+
+addcmd '!neverhaveiever' neverhaveiever_toggle
+addcmd '!neverhaveIever' neverhaveiever_toggle
+addcmd '!NeverHaveIEver' neverhaveiever_toggle
+addcmd '!NEVERHAVEIEVER' neverhaveiever_toggle
diff --git a/modules/relay.sh b/modules/relay.sh
new file mode 100755
index 0000000..2a0f8c8
--- /dev/null
+++ b/modules/relay.sh
@@ -0,0 +1,80 @@
+#!/bin/sh
+active=''
+mkdir -p "${DATADIR}/relaymsgs"
+relay_msg()
+{
+ from="$2"
+ # New relay format, potentially supporting nick aliasing
+ first=true
+ for msg in `grep -rli "^ to: ${from}\$" "${DATADIR}/relaymsgs" | sort -n`; do
+ if "$first"; then
+ sed -n -e "s/^\\([^ :]*\\):/${from}: Message from \1:/p" "$msg" | sayx
+ first=false
+ else
+ sed -n -e "s/^\\([^ :]*\\):/From \1:/p" "$msg" | sayx
+ fi
+ rm -f "$msg"
+ sleep 0.8
+ done
+ active="`echo "$active" | grep -v "^${from}: "`
+${from}: `date +%s | xargs expr 300 +`"
+}
+
+relay_relay()
+{
+ from="$1"
+ tolistplus="`echo "$2" | sed -e 's/ .*//;s|+| |g'`" # Get recipient(s) and separate by +
+ actualtolist=''
+ for tolist in ${tolistplus}; do
+ tolist="`echo "$tolist" | sed -e 's|/| |g'`"
+ for to in ${tolist}; do
+ mintime="`echo "$active" | grep "^${to}: " | sed -e 's/.*: //'`"
+ echo "mintime: ${mintime}, now: `date +%s`" >&2
+ if [ "$mintime" -gt "`date +%s`" ] 2> /dev/null; then
+ say "${to} looks active to me, won't relay" priv "$from"
+ continue 2
+ fi
+ if [ "`grep -l "^ to: ${to}\$" "${DATADIR}/relaymsgs"/* | wc -l`" -gt 5 ]; then
+ say "${from}: ${to}'s inbox is full (restricting to 6 messages to prevent getting autobanned)"
+ continue 2
+ fi
+ done
+ if [ "x${actualtolist}" = "x" ]; then
+ actualtolist="`echo "$tolist" | tr ' ' '/'`"
+ else
+ actualtolist="${actualtolist}+`echo "$tolist" | tr ' ' '/'`"
+ fi
+ msg="`echo "$2" | sed -e "s/^[^ ]* //"`"
+ echo "Relaying message from '${from}' to '${tolist}': '${msg}'" >&2
+ id="`date +%s`"
+ while [ -e "${DATADIR}/relaymsgs/${id}" ]; do id="`expr "$id" + 1`"; done
+ for to in ${tolist}; do
+ echo " to: ${to}" >> "${DATADIR}/relaymsgs/${id}"
+ done
+ echo "${from}: ${msg}" >> "${DATADIR}/relaymsgs/${id}"
+ done
+ if [ "x${actualtolist}" != "x" ]; then
+ say "Will relay '${msg}' to ${actualtolist}" priv "$from"
+ fi
+}
+
+relay_relayed()
+{
+ from="$1"
+ to="$2"
+ if grep -l "^ to: ${to}\$" "${DATADIR}/relaymsgs"/* | xargs --no-run-if-empty grep "^${from}: " | grep -q .; then
+ say "There are messages from ${from} to ${to} which have not yet been delivered"
+ else
+ say "All messages from ${from} to ${to} have been delivered :)"
+ fi
+}
+
+relay_part()
+{
+ active="`echo "$active" | grep -v "^${1}: "`"
+}
+
+addeventhandler chatmsg relay_msg
+addeventhandler part relay_part
+addcmd '!relay' relay_relay
+addcmd '!relayed' relay_relayed
diff --git a/modules/reminder.sh b/modules/reminder.sh
new file mode 100755
index 0000000..e1446bf
--- /dev/null
+++ b/modules/reminder.sh
@@ -0,0 +1,81 @@
+#!/bin/false
+wordtonum='s/\ba\b/1/g;
+s/\ban\b/1/ig;
+s/\bone\b/1/ig;
+s/\btwo\b/2/ig;
+s/\bthree\b/3/ig;
+s/\bfour\b/4/ig;
+s/\bfive\b/5/ig;
+s/\bsix\b/6/ig;
+s/\bseven\b/7/ig;
+s/\beight\b/8/ig;
+s/\bnine\b/9/ig;
+s/\bten\b/10/ig;
+s/\beleven\b/11/ig;
+s/\bfifteen\b/15/ig;
+s/\btwenty\b/20/ig;
+s/\bthirty\b/30/ig;
+s/\bfourty\b/40/ig;
+s/\bfourtyfive\b/45/ig;
+s/\bfifty\b/50/ig;
+s/\band\b/+/ig;
+s/,/+/g;
+s/\btomorrow\b/+1day/ig;
+s/\bnext day\b/+1day/ig;
+s/\bnext week\b/+7days/ig;
+s/\bnext month\b/+1month/ig;
+'
+reversepronouns='s/\bmy\b/yourXX<tmp>XX/ig; s/\bme\b/youXX<tmp>XX/ig; s/\byour\b/my/ig; s/\byou\b/me/ig; s/XX<tmp>XX//g'
+
+reminder()
+{
+ chatmsg="$1"
+ from="$2"
+ if echo "$chatmsg" | grep -qi '^\(relaybot: *\|relaybot, *\|\)remind me to .* \(in\|at\|on\) '; then
+ inat="`echo "$chatmsg" | sed -e 's/^\(relaybot: *\|relaybot, *\|\)remind me to .* \(in\|at\|on\) .*/\2/i'`"
+ desttime="`echo "$chatmsg" | sed -e "s/^\(relaybot: *\|relaybot, *\|\)remind me to .* \(in\|at\|on\) //i; ${wordtonum}"`"
+ remindmsg="`echo "$chatmsg" | sed -e 's/^\(relaybot: *\|relaybot, *\|\)remind me to \(.*\) \(in\|at\|on\) .*/\2/i'`"
+ elif echo "$chatmsg" | grep -qi '^\(relaybot: *\|relaybot, *\|\)remind me \(in\|at\|on\) .* to .'; then
+ inat="`echo "$chatmsg" | sed -e 's/^\(relaybot: *\|relaybot, *\|\)remind me \(in\|at\|on\) .* to ./\2/i'`"
+ desttime="`echo "$chatmsg" | sed -e "s/^\(relaybot: *\|relaybot, *\|\)remind me \(in\|at\|on\) .* to .//i; ${wordtonum}"`"
+ remindmsg="`echo "$chatmsg" | sed -e 's/^\(relaybot: *\|relaybot, *\|\)remind me \(in\|at\|on\) .* to \(.*\)/\3/i'`"
+ else
+ return
+ fi
+ remindmsg="`echo "$remindmsg" | sed -e "$reversepronouns"`"
+ if [ "$inat" = "in" ]; then desttime="+${desttime}"; fi # TODO: what about "remind me in <month/year>?"
+ if date -d "$desttime" > /dev/null 2> /dev/null; then
+ desttime="`date -d "$desttime" +%s`"
+ diff="`date +%s`"
+ diff="`expr "$desttime" - "$diff"`"
+ if [ "$diff" -lt 1 ]; then return 1; fi
+ if [ "$diff" -gt 30 ]; then
+ say "ok ${from}, I will remind you"
+ fi
+ if [ "$diff" -lt 86400 ]; then # Less than 24 hours, volatile but precise reminder
+ (
+ sleep "$diff"
+ say "${from}: remember to ${remindmsg}"
+ ) &
+ else # 24 hours or more, less precise but persists across reloads/reconnects
+ mkdir -p "${DATADIR}/reminders"
+ id="`date +%s`"
+ while [ -e "${DATADIR}/reminders/${id}" ]; do id="`expr "$id" + 1`"; done
+ echo "Time: ${desttime}" > "${DATADIR}/reminders/${id}"
+ echo "For: ${from}" >> "${DATADIR}/reminders/${id}"
+ echo "Message: ${remindmsg}" >> "${DATADIR}/reminders/${id}"
+ fi
+ return 0
+ fi
+ for x in `grep -rl "^For: ${from}$" "${DATADIR}/reminders" 2> /dev/null`; do
+ desttime="`sed -n -e 's/^Time: //p' "$remindmsg"`"
+ if [ "$desttime" -gt "`date +%s`" ]; then continue; fi
+ msg="`sed -n -e 's/^Message: //p' "$x"`"
+ say "${from}: remember to ${msg}"
+ rm -f "$x"
+ break # Only one reminder per message
+ done
+ return 1
+}
+
+addeventhandler chatmsg reminder
diff --git a/modules/tc_versioncheck.sh b/modules/tc_versioncheck.sh
new file mode 100755
index 0000000..ef34b8b
--- /dev/null
+++ b/modules/tc_versioncheck.sh
@@ -0,0 +1,16 @@
+#!/bin/false
+versioncheck="`date +%s | xargs expr 3600 +`"
+versioncheck_check()
+{
+ if [ "`date +%s`" -gt "$versioncheck" ]; then
+ version="`curl -s 'https://tinychat.com/embed/chat.js' | sed -n -e '/swfName =/{s/.*swfName = "Tinychat-//;s/\.swf".*//;p;q;}'`"
+ if [ "$version" != "`cat "${DATADIR}/flashclient.version"`" ]; then
+ if [ -e "${DATADIR}/flashclient.version" ]; then # Don't announce on the first check
+ say "Flash client version changed to ${version}"
+ fi
+ echo "$version" > "${DATADIR}/flashclient.version"
+ fi
+ versioncheck="`date +%s | xargs expr 3600 +`"
+ fi
+}
+addeventhandler chatmsg versioncheck_check
diff --git a/modules/time.sh b/modules/time.sh
new file mode 100755
index 0000000..3474b17
--- /dev/null
+++ b/modules/time.sh
@@ -0,0 +1,25 @@
+#!/bin/sh
+gettimeat()
+{
+ timezone="`echo "$2" | sed -e 's/ /_/g'`"
+ timezone="`find /usr/share/zoneinfo -follow -type f -iname "$timezone" | head -n 1`"
+ if [ "x${timezone}" = "x" ]; then
+ say "Unknown timezone"
+ else
+ TZ="$timezone" date '+%H:%M %Z' | sayx
+ fi
+}
+
+gettimeatsecret()
+{
+ timezone="`echo "$2" | sed -e 's/ /_/g'`"
+ timezone="`find /usr/share/zoneinfo -follow -type f -iname "$timezone" | head -n 1`"
+ if [ "x${timezone}" = "x" ]; then
+ echo "/Unknown timezone"
+ else
+ TZ="$timezone" date '+%H:%M %Z' | sed -e 's|^|/|'
+ fi
+}
+
+addcmd '!time' gettimeat
+addcmd '/!time' gettimeatsecret
diff --git a/modules/topic.sh b/modules/topic.sh
new file mode 100755
index 0000000..1e548db
--- /dev/null
+++ b/modules/topic.sh
@@ -0,0 +1,32 @@
+#!/bin/sh
+topic='{UNSET}'
+
+topic_changed()
+{
+ if [ "$topic" = "{UNSET}" ]; then
+ echo "Topic (join): ${1}" >&2
+ else
+ echo "Topic (new): ${1}" >&2
+ say "Topic changed: ${1}"
+ fi
+ topic="$1"
+}
+
+topic_save()
+{
+ echo "$topic" > relaybot.reload.topic
+}
+topic_load()
+{
+ topic="`cat relaybot.reload.topic`"
+}
+
+topic_get()
+{
+ say "Topic: ${topic}"
+}
+
+addeventhandler topic topic_changed
+addeventhandler reload_before topic_save
+addeventhandler reload_after topic_load
+addcmd '!topic' topic_get
diff --git a/modules/videoutils.sh b/modules/videoutils.sh
new file mode 100755
index 0000000..67d5f42
--- /dev/null
+++ b/modules/videoutils.sh
@@ -0,0 +1,26 @@
+#!/bin/sh
+if [ "x${protocol}" != "xtc_client" ]; then echo 'WARNING: the videoutils module is unlikely to make sense on other protocols than tinychat/tc_client' >&2; fi
+videoutils_mbs()
+{
+ if ! echo "$2" | grep -q '^youTube '; then return; fi
+ vid="`echo "$2" | sed -e 's|^youTube ||; s/ .*//'`"
+ bans="`curl --connect-timeout 10 -s "http://polsy.org.uk/stuff/ytrestrict.cgi?ytid=${vid}" | sed -n -e 's/.*<td>.*<td>.. - //p'`"
+ if [ "`echo "$bans" | wc -l`" -gt 5 ]; then
+ bans="`echo "$bans" | wc -l` countries, http://polsy.org.uk/stuff/ytrestrict.cgi?ytid=${vid}"
+ else
+ bans="`echo "$bans" | sed -e 's/$/, /' | tr -d '\n' | sed -e 's/, $//; s/, \([^,]*\)$/ and \1/'`"
+ fi
+# echo "Bans for ${vid}: ${bans}" >&2
+ if [ "x${bans}" != "x" -a "x${bans}" != "xGermany" ]; then # Germany bans everything, so ignore it if it's just them
+ say "Note: this video is blocked in ${bans}"
+ fi
+ # If a name wasn't given, find and announce it
+ if echo "$2" | grep -q '^youTube [^ ]* 0$'; then
+ title="`youtube-dl --get-title -- "$vid"`"
+ if [ -n "$title" ]; then
+ echo "/video: ${title}"
+ fi
+ fi
+}
+
+addcmd '/mbs' videoutils_mbs
diff --git a/modules/whoismycrush.sh b/modules/whoismycrush.sh
new file mode 100755
index 0000000..cd4530f
--- /dev/null
+++ b/modules/whoismycrush.sh
@@ -0,0 +1,25 @@
+#!/bin/false
+guesscrush()
+{
+ nick="$1"
+ lines=50000
+ tail -n "$lines" "${DATADIR}/relaybot.chat" | sed -e 's/:.*//' | grep -B 1 -F -x -- "$nick" | grep -v -x -F -- "$nick" | grep -v -- -- | sort | uniq -c | sort -n | tail -n 1 | sed -e 's/.* //' | grep .
+}
+
+whoismycrush()
+{
+ from="$1"
+ x="`echo "$from" | sed -e 's/[0-9]*$//'`"
+ crush="`echo "$crushes" | sed -n -e "s/^${x}=//p"`"
+ if [ -z "${crush}" ]; then
+ crush="`guesscrush "$from" || echo "I don't know, sorry"`"
+ else
+ sleep 1 # Some artificial delay
+ fi
+ if echo "$saidlast" | grep -q '^[^ ]*: your crush is probably\.\.\. '; then
+ say "${from}: ${crush}"
+ else
+ say "${from}: your crush is probably... ${crush}"
+ fi
+}
+addcmd '!whoismycrush' whoismycrush
diff --git a/protocols/irc.sh b/protocols/irc.sh
new file mode 100644
index 0000000..a70960d
--- /dev/null
+++ b/protocols/irc.sh
@@ -0,0 +1,39 @@
+#!/bin/sh
+if [ -z "$port" ]; then port=6667; fi
+protocol_cmd="`which nc` '${host}' '${port}'"
+
+handleline()
+{
+ line="`echo "$line" | tr -d '\r\n'`"
+echo "Got: ${line}" >&2
+ name="`echo "$line" | sed -e 's/^://;s/!.*//'`"
+ if echo "$line" | grep -q '^:[^ ]* 332 '; then
+ handleevent topic "`echo "$line" | sed -e 's/^:[^:]*://'`"
+ elif echo "$line" | grep -q '^:[^ ]* PRIVMSG '; then
+ color="`echo "$line" | sed -n -e 's/.*\([0-9][0-9]\?\).*/\1/p'`"
+ msg="`echo "$line" | sed -e 's/^:[^:]*://; s/[0-9][0-9]\?//g'`"
+ handleevent chatmsg "$msg" "$name" "$color"
+ elif echo "$line" | grep -q '^:[^ ]* JOIN '; then
+ handleevent join "$name"
+ elif echo "$line" | grep -q '^:[^ ]* \(QUIT\|PART\) '; then
+ handleevent part "$name"
+ elif echo "$line" | grep -q '^:[^ ]* 001 '; then
+ echo "JOIN ${channel}"
+ elif echo "$line" | grep -q '^PING '; then
+ echo "$line" | sed -e 's/PING/PONG/'
+ handleevent ping
+ fi
+}
+
+say()
+{
+ saidlast="$1"
+ echo "PRIVMSG ${channel} :${1}"
+ echo "relaybot: $1" >> "${DATADIR}/relaybot.chat"
+}
+
+protocol_init()
+{
+ echo 'USER relaybot relaybot relaybot relaybot'
+ echo "NICK ${nickname}"
+}
diff --git a/protocols/stdio.sh b/protocols/stdio.sh
new file mode 100644
index 0000000..518672f
--- /dev/null
+++ b/protocols/stdio.sh
@@ -0,0 +1,21 @@
+#!/bin/sh
+protocol_cmd="run_with_PIPE_as_argv1"
+if [ -n "$cookiejar" ]; then protocol_cmd="${protocol_cmd} --cookies '${cookiejar}'"; fi
+if [ -n "$textcolor" ]; then protocol_cmd="${protocol_cmd} -c '${textcolor}'"; fi
+
+handleline()
+{
+ handleevent chatmsg "$1" "user"
+}
+
+say()
+{
+ saidlast="$1"
+ echo "$1"
+ echo "relaybot: $1" >> "${DATADIR}/relaybot.chat"
+}
+
+protocol_init()
+{
+ true # Nothing to initialize
+}
diff --git a/protocols/tc_client.sh b/protocols/tc_client.sh
new file mode 100755
index 0000000..22acca8
--- /dev/null
+++ b/protocols/tc_client.sh
@@ -0,0 +1,59 @@
+#!/bin/sh
+protocol_cmd="`which tc_client` --hexcolors ${channel} ${nickname} ${password}"
+if [ -n "$cookiejar" ]; then protocol_cmd="${protocol_cmd} --cookies '${cookiejar}'"; fi
+if [ -n "$textcolor" ]; then protocol_cmd="${protocol_cmd} -c '${textcolor}'"; fi
+
+handleline()
+{
+ line="$1"
+
+ if echo "$line" | grep -q '^\[[0-2][0-9]:[0-6][0-9]\] ([^)]*)[^:]*: '; then
+ from="`echo "$line" | sed -e "s/^[^ ]* ([^)]*)\([^:]*\):.*/\1/"`"
+ chatmsg="`echo "$line" | sed -e 's/^\[[0-2][0-9]:[0-6][0-9]\] ([^)]*)[^:]*: //'`"
+ color="`echo "$line" | sed -e "s/^[^ ]* (//; s/[,)].*//"`"
+ handleevent chatmsg "$chatmsg" "$from" "$color"
+ elif echo "$line" | grep -q '^Room topic: '; then
+ handleevent topic "`echo "$line" | sed -e 's/^Room topic: //'`"
+ elif echo "$line" | grep -q '^[^ ]* cammed up$'; then
+ handleevent camup "`echo "$line" | sed -e 's/ .*//'`"
+ elif echo "$line" | grep -q '^guest-[0-9]* is logged in as '; then
+ name="`echo "$line" | sed -e 's/ .*//'`"
+ acc="`echo "$line" | sed -e 's|^guest-[0-9]* is logged in as ||'`"
+ handleevent account "$name" "$acc"
+ elif echo "$line" | grep -q '^\[[0-2][0-9]:[0-6][0-9]\] [^ ]* entered the channel$'; then
+ name="`echo "$line" | sed -e 's/^\[[0-2][0-9]:[0-6][0-9]\] //;s/ .*//'`"
+ handleevent join "$name"
+ echo "/whois ${name}" # Used by multiple modules
+ elif echo "$line" | grep -q '^\[[0-2][0-9]:[0-6][0-9]\] [^:]* changed nickname to '; then
+ from="`echo "$line" | sed -e 's/^\[[0-2][0-9]:[0-6][0-9]\] //; s/ changed nickname to .*//'`"
+ nick="`echo "$line" | sed -e 's/^\[[0-2][0-9]:[0-6][0-9]\] [^ ]* changed nickname to //'`"
+ handleevent nick "$from" "$nick"
+ elif echo "$line" | grep -q '^\[[0-2][0-9]:[0-6][0-9]\] [^:]* left the channel'; then
+ nick="`echo "$line" | sed -e 's/^\[[0-2][0-9]:[0-6][0-9]\] //; s/ .*//'`"
+ handleevent part "$nick"
+ elif echo "$line" | grep -q '^[^ ]* is a moderator.'; then # TODO: expand into proper mod tracking?
+ handleevent mod "`echo "$line" | sed -e 's/ .*//'`"
+ elif [ "x${line}" = "xProbably cut off:" ]; then
+ handleevent probablycutoff
+ elif echo "$line" | grep '^Captcha:' >&2; then
+ read x < /dev/tty
+ echo done >&2
+ echo
+ fi
+}
+
+say()
+{
+ if [ "x${2}" = "xpriv" ]; then
+ echo "/priv ${3} ${1}"
+ return
+ fi
+ saidlast="$1"
+ echo "$1"
+ echo "relaybot: $1" >> "${DATADIR}/relaybot.chat"
+}
+
+protocol_init()
+{
+ true # Nothing to initialize
+}
diff --git a/relaybot.conf b/relaybot.conf
new file mode 100755
index 0000000..b9739df
--- /dev/null
+++ b/relaybot.conf
@@ -0,0 +1,30 @@
+#!/bin/sh
+# Sample configuration file
+
+irclog='/home/user/irclogs/tc_client_irchack/#relaybotchannel.log'
+protocol='tc_client'
+nickname='relaybot'
+channel='relaybotchannel'
+password='channelpassword'
+cookiejar='cookies' # For fewer captchas
+textcolor=12 # Purple
+DATADIR='/home/user/relaybot_data'
+countdownto='Apr 25 23:00:00 CEST 2025' # when relaybot turns 10
+gitreport_repos='https://ion.nu/git/relaybot.git'
+modules='
+tc_versioncheck
+conversation
+reminder
+conversion
+neverhaveiever
+time
+topic
+relay
+camface
+imgcam
+magnifier
+activitystats
+countdown
+gitreport
+videoutils
+whoismycrush'
diff --git a/relaybot.sh b/relaybot.sh
new file mode 100755
index 0000000..0bcffc7
--- /dev/null
+++ b/relaybot.sh
@@ -0,0 +1,143 @@
+#!/bin/sh
+# TODO: accept path to configuration file as $1, make PIPE an environment variable instead?
+# Or parse arguments normally, -r for reload, -b for the bot part, non-flag for configuration file
+EXECDIR="`realpath "$0" | xargs dirname`" # Home of this script and all the modules + static data
+DATADIR="." # Home of things like messages to relay
+. ./relaybot.conf
+. "${EXECDIR}/protocols/${protocol}.sh"
+if [ "x${1}" != "xPIPE" ]; then
+ (
+ echo '[Net'
+ echo "$protocol_cmd"
+ echo ']' '[Bot' "$0" PIPE ']' '{Net:1>Bot:0}' '{Bot:1>Net:0}'
+ ) | xargs pipexec -k
+fi
+echo '(Re-)loaded' >&2
+chatcommands=''
+addcmd()
+{
+ chatcommands="${chatcommands}
+${1}=${2}"
+}
+delcmd()
+{
+ chatcommands="`echo "$chatcommands" | sed -e "/^${1}=${2}\$/d"`"
+}
+eventhandlers=''
+addeventhandler()
+{
+# echo "Adding event handler '${2}' for '${1}'" >&2
+ eventhandlers="${eventhandlers}
+${1}=${2}"
+}
+deleventhandler()
+{
+ eventhandlers="`echo "$eventhandlers" | sed -e "/^${1}=${2}\$/d"`"
+}
+handleevent()
+{
+# echo "Handling event '${1}'" >&2
+ for x in `echo "$eventhandlers" | sed -n -e "s/^${1}=//p"`; do
+# echo "Handler: ${x}" >&2
+ "$x" "$2" "$3" "$4"
+ done
+}
+protocol_init
+for module in ${modules}; do . "${EXECDIR}/modules/${module}.sh"; done
+# TODO: move this into a module for cam features to depend on?
+cammode=''
+toggle_cammode()
+{
+ if [ "x$cammode" = "x${1}" ]; then
+ cammode=''
+ pkill -x camutil
+ echo '/camdown'
+ return 1
+ else
+ if [ "x$cammode" != "x" ]; then
+ pkill -x camutil
+ else
+ echo '/camup'
+ fi
+ cammode="$1"
+ return 0
+ fi
+}
+if [ "x${2}" = "xreload" ]; then
+ cammode="`cat relaybot.reload.cammode`"
+ handleevent reload_after
+ rm -f relaybot.reload.*
+fi
+saidlast=''
+sayx()
+{
+ read x
+ say "$x"
+}
+sayonce()
+{
+ if [ "x${saidlast}" != "x${1}" ]; then say "$1"; saidlast="$1"; fi
+}
+decimals()
+{
+ value="$1"
+ decimalcount="$2"
+ decimalpoints=''
+ for x in `seq "$decimalcount"`; do
+ value="0${value}"
+ decimalpoints="${decimalpoints}[0-9]"
+ done
+ echo "$value" | sed -e "s/${decimalpoints}\$/.&/;s/^0*//;s/0*\$//;s/\\.\$//"
+}
+countdecimals()
+{
+ echo -n "$1" | sed -n -e 's/.*\.//p' | wc -c
+}
+tenpow()
+{
+ echo -n "$1"
+ seq "$2" | sed -e 's/.*/0/' | tr -d '\n'
+}
+
+commandhandler()
+{
+ chatmsg="$1"
+ from="$2"
+ potentialcmd="`echo "$chatmsg" | sed -e 's/ .*//'`"
+ for commandbinding in ${chatcommands}; do
+ chatcommand="`echo "$commandbinding" | sed -e 's/=.*//'`"
+ if [ "x${potentialcmd}" = "x${chatcommand}" ]; then
+ commandcallback="`echo "$commandbinding" | sed -e 's/.*=//'`"
+ "$commandcallback" "$from" "`echo "$chatmsg" | sed -e 's/^[^ ]*//; s/^ //'`"
+ fi
+ done
+}
+addeventhandler chatmsg commandhandler
+
+do_reload()
+{
+ # TODO: cleaner implementation of restriction/authorization so this can be called as a command
+ # TODO: callbacks for modules to save and restore state cleanly (events done, need handlers)
+ if ! sh -n "$0"; then echo "/priv ${from} Syntax error, won't reload"; continue; fi
+ echo "$cammode" > relaybot.reload.cammode
+ handleevent reload_before
+ exec "$0" PIPE reload
+}
+
+logchat()
+{
+ msg="$1"
+ from="$2"
+ echo "${from}: ${msg}" >> "${DATADIR}/relaybot.chat"
+}
+addeventhandler chatmsg logchat
+
+while true; do
+ line=''
+ if ! read -r line; then
+ if [ ! -e relaybot.say ]; then break; fi
+ fi
+ if [ "x${line}" = "x" ]; then continue; fi
+ echo "Got line '${line}'" >> "${DATADIR}/relaybot.log" # TODO: clean up references to this obsolete thing (camspammers module)
+ handleline "$line"
+done
diff --git a/relaybotface/relaybot.xcf b/relaybotface/relaybot.xcf
new file mode 100644
index 0000000..f822850
Binary files /dev/null and b/relaybotface/relaybot.xcf differ
diff --git a/relaybotface/relaybot1.png b/relaybotface/relaybot1.png
new file mode 100644
index 0000000..9c4194e
Binary files /dev/null and b/relaybotface/relaybot1.png differ
diff --git a/relaybotface/relaybot2.png b/relaybotface/relaybot2.png
new file mode 100644
index 0000000..039d0f5
Binary files /dev/null and b/relaybotface/relaybot2.png differ
diff --git a/relaybotface/relaybot3.png b/relaybotface/relaybot3.png
new file mode 100644
index 0000000..9f98852
Binary files /dev/null and b/relaybotface/relaybot3.png differ
diff --git a/relaybotface/relaybot4.png b/relaybotface/relaybot4.png
new file mode 100644
index 0000000..b3acc41
Binary files /dev/null and b/relaybotface/relaybot4.png differ