$ git clone http://tcclient.ion.nu/tc_client.git
commit 75dd8cfa138cde86dbdb5139e15b735e5b980282
Author: Alicia <...>
Date:   Tue Apr 7 06:49:01 2015 +0200

    Version 0.16

diff --git a/ChangeLog b/ChangeLog
index 6008164..dd44c65 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,11 @@
+0.16:
+Get and print the channel topic (contributed by Jade)
+irchack: fork and keep accepting connections.
+irchack: translate IRC's "\x01ACTION stuff\x01" (/me) into "*stuff*" for tc_client.
+modbot: add a 100ms delay between the lines of !help to prevent throttling.
+modbot: handle manual /mbs and /mbc commands (remove from queue, mark as good)
+Added a /help command to list commands handled by tc_client at runtime.
+irchack: use USER and PASS IRC commands to get the tinychat account to login as.
 0.15:
 Fixed a bug introduced with the new argument handling in 0.14
 Prompt for password if a username is given with -u/--user but no password (-p/--pass) is given.
diff --git a/Makefile b/Makefile
index 23c0816..1a130b8 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-VERSION=0.15
+VERSION=0.16
 CFLAGS=-g3 -Wall $(shell curl-config --cflags)
 LIBS=-g3 $(shell curl-config --libs)
 
diff --git a/README b/README
index 7987c95..4c84504 100644
--- a/README
+++ b/README
@@ -1,16 +1,25 @@
-Some notes about tc_client in its current state (things that should be fixed):
-*there is no real user interface, to send a message you just type it and it might get cut off by incoming messages, it'll be ugly. There is however the irchack program that lets you use an IRC client.
-*PMs can be sent by /msg <nickname> <message> and replies appear similarly.
+tc_client is a very primitive application, it doesn't implement a user interface on its own,
+you could use it stand-alone but it will be ugly and if someone says something while you are typing a message your input-line will get cut off.
+Better interfaces can be implemented as wrappers that launch tc_client with stdin and stdout as pipes.
+
+Commands supported by tc_client:
+/msg <nickname> <message> = send a PM, incoming PMs look similar
+/color <on/off>           = enable/disable showing colors with ANSI escapes
+/color <0-15>             = set the color of your outgoing messages
+/colors                   = list the available colors
+/nick <newnick>           = change nickname
 
 Some things that will probably never change:
 *tc_client can't view people's webcams or listen to mics
 *tc_client can't stream/broadcast your webcam/mic
-*tc_client probably won't ever play youtube videos (although you can see when one is launched and get the video ID because it is sent as a message and can then open it yourself)
+*tc_client itself won't play youtube videos, but applications that rely on tc_client can interpret the /mbs, /mbsk and /mbc commands
 
 Current commands sent by the TC servers that tc_client doesn't know how to handle:
 notice
-topic
 joinsdone
 avons (list of people currently on cam)
 pros
-oper
+
+Included applications that rely on tc_client (type 'make utils' to build):
+*irchack = a minimal IRC server that translates between IRC and tc_client's commands
+*modbot  = a bot that handles youtube video requests with a queue and an approval system to keep inappropriate videos from being played. Supports the following commands: !help, !request, !queue (show queue status), for mods only: !playnext (try playing next in queue without marking it yet), !approve, !badvid (Note: modbot depends on youtube-dl to find out the length of videos)
diff --git a/client.c b/client.c
index c8cb173..bd724fb 100644
--- a/client.c
+++ b/client.c
@@ -348,7 +348,18 @@ int main(int argc, char** argv)
       int privlen;
       if(buf[0]=='/') // Got a command
       {
-        if(!strncmp((char*)buf, "/color", 6) && (!buf[6]||buf[6]==' '))
+        if(!strcmp((char*)buf, "/help"))
+        {
+          printf("/help           = print this help text\n"
+                 "/color <0-15>   = pick color of your messages\n"
+                 "/color <on/off> = turn on/off showing others' colors with ANSI codes\n"
+                 "/color          = see your current color\n"
+                 "/colors         = list the available colors and their numbers\n"
+                 "/nick <newnick> = changes your nickname\n"
+                 "/msg <to> <msg> = send a private message\n");
+          fflush(stdout);
+        }
+        else if(!strncmp((char*)buf, "/color", 6) && (!buf[6]||buf[6]==' '))
         {
           if(buf[6]) // Color specified
           {
@@ -562,6 +573,12 @@ int main(int argc, char** argv)
         printf("Nick is already in use.\n");
         fflush(stdout);
       }
+      // Room topic
+      else if(amfin->itemcount>2 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "topic") && amfin->items[2].type==AMF_STRING && strlen(amfin->items[2].string.string) > 0)
+      {
+        printf("Room topic: %s\n", amfin->items[2].string.string);
+        fflush(stdout);
+      }
       // else{printf("Unknown command...\n"); printamf(amfin);} // (Debugging)
       amf_free(amfin);
     }
diff --git a/colors.c b/colors.c
index 1114e67..6812d67 100644
--- a/colors.c
+++ b/colors.c
@@ -18,7 +18,7 @@
 #include "colors.h"
 
 // Sorted like rainbows
-const char* colors[]={
+const char* colors[]={ // The 16 colors accepted by the flash client
   "#821615,en",
   "#c53332,en",
   "#a08f23,en",
@@ -37,7 +37,7 @@ const char* colors[]={
   "#b9807f,en"
 };
 
-const char* termcolors[]={
+const char* termcolors[]={ // Equivalent color codes for ANSI escape sequences
   "31",
   "31;1",
   "33",
diff --git a/irchack.c b/irchack.c
index e376f0b..26a67b9 100644
--- a/irchack.c
+++ b/irchack.c
@@ -23,6 +23,7 @@
 #include <stdio.h>
 #include <sys/socket.h>
 #include <ctype.h>
+#include <signal.h>
 
 #ifdef __ANDROID__
 // Android has no dprintf, so we make our own
@@ -97,7 +98,7 @@ int findcolor_ansi(char* irc, char** end)
   return -1;
 }
 
-extern char session(int sock, const char* nick, const char* channel, const char* pass);
+extern char session(int sock, const char* nick, const char* channel, const char* pass, const char* acc_user, const char* acc_pass);
 
 int main(int argc, char** argv)
 {
@@ -111,24 +112,43 @@ int main(int argc, char** argv)
   if(bind(lsock, (struct sockaddr*)&addr, sizeof(addr))){perror("bind"); return 1;}
   listen(lsock, 1);
   printf("Done! Open an IRC client and connect to localhost on port %i\n", port);
+  signal(SIGCHLD, SIG_IGN);
   int sock;
   while((sock=accept(lsock, 0, 0))>-1)
   {
+    if(fork()){continue;}
     char buf[2048];
     char* nick=0;
     char* channel=0;
     char* pass=0;
+    char* acc_user=0;
+    char* acc_pass=0;
     int len;
     while(1)
     {
       len=0;
+      int r;
       while(len<2047)
       {
-        if(read(sock, &buf[len], 1)!=1 || buf[len]=='\r' || buf[len]=='\n'){break;}
+        if((r=read(sock, &buf[len], 1))!=1 || buf[len]=='\r' || buf[len]=='\n'){break;}
         ++len;
       }
+      if(r!=1){break;}
       buf[len]=0;
-      if(!strncmp(buf, "NICK ", 5))
+      if(!strncmp(buf, "USER ", 5))
+      {
+        acc_user=&buf[5];
+        char* end=strchr(acc_user, ' ');
+        if(end){end[0]=0;}
+        acc_user=strdup(acc_user);
+      }
+      else if(!strncmp(buf, "PASS ", 5))
+      {
+        acc_pass=&buf[5];
+        if(acc_pass[0]==':'){acc_pass=&acc_pass[1];}
+        acc_pass=strdup(acc_pass);
+      }
+      else if(!strncmp(buf, "NICK ", 5))
       {
         char* newnick=&buf[5];
         if(newnick[0]==':'){newnick=&nick[1];}
@@ -156,7 +176,7 @@ int main(int argc, char** argv)
         }
         if(channel[0]=='#'){channel=&channel[1];}
         channel=strdup(channel);
-        if(!session(sock, nick, channel, pass)){break;}
+        if(!session(sock, nick, channel, pass, acc_user, acc_pass)){break;}
       }
       else if(!strncmp(buf, "PING ", 5))
       {
@@ -164,12 +184,13 @@ int main(int argc, char** argv)
       }
     }
     shutdown(sock, SHUT_RDWR);
+    _exit(0);
   }
   close(lsock);
   return 0;
 }
 
-char session(int sock, const char* nick, const char* channel, const char* pass)
+char session(int sock, const char* nick, const char* channel, const char* pass, const char* acc_user, const char* acc_pass)
 {
   printf("Nick: %s\n", nick);
   printf("Channel: %s\n", channel);
@@ -184,7 +205,12 @@ char session(int sock, const char* nick, const char* channel, const char* pass)
     close(tc_out[0]);
     dup2(tc_in[0], 0);
     dup2(tc_out[1], 1);
-    execl("./tc_client", "./tc_client", channel, nick, pass, (char*)0);
+    if(acc_user && acc_pass)
+    {
+      execl("./tc_client", "./tc_client", "-u", acc_user, "-p", acc_pass, channel, nick, pass, (char*)0);
+    }else{
+      execl("./tc_client", "./tc_client", channel, nick, pass, (char*)0);
+    }
     perror("Failed to exec tc_client");
     _exit(1);
   }
@@ -234,6 +260,11 @@ printf("Got from tc_client: '%s'\n", buf);
         dprintf(sock, ":irchack 366 %s #%s :End of /NAMES list.\n", nick, channel);
         joins=0;
       }
+      if(!strncmp(buf, "Room topic: ", 12))
+      {
+        dprintf(sock, ":irchack 332 %s #%s :%s\n", nick, channel, &buf[12]);
+        continue;
+      }
       if(!strcmp(buf, "Password required"))
       {
         dprintf(sock, ":irchack 475 %s :Cannot join %s without the correct password\n", channel, channel);
@@ -317,12 +348,13 @@ printf("Got from tc_client: '%s'\n", buf);
     {
       pfd[1].revents=0;
       len=0;
+      int r;
       while(len<2047)
       {
-        if(read(sock, &buf[len], 1)!=1 || buf[len]=='\r' || buf[len]=='\n'){break;}
+        if((r=read(sock, &buf[len], 1))!=1 || buf[len]=='\r' || buf[len]=='\n'){break;}
         ++len;
       }
-      if(len<=0){continue;}
+      if(r!=1){break;}
       buf[len]=0;
 printf("Got from IRC client: '%s'\n", buf);
       if(!strncmp(buf, "PRIVMSG ", 8))
@@ -341,6 +373,13 @@ printf("Got from IRC client: '%s'\n", buf);
           if(c!=-1){dprintf(tc_in[1], "/color %i\n", c);}
           memmove(color, end, strlen(end)+1);
         }
+        if(!strncmp(msg, "\x01""ACTION ", 8)) // Translate '/me'
+        {
+          msg=&msg[7];
+          msg[0]='*';
+          char* end=strchr(msg, '\x01');
+          if(end){end[0]='*';}
+        }
         if(target[0]=='#' && !strcmp(&target[1], channel))
         {
           dprintf(tc_in[1], "%s\n", msg);
diff --git a/modbot.c b/modbot.c
index 69a54b4..d375590 100644
--- a/modbot.c
+++ b/modbot.c
@@ -157,13 +157,8 @@ void say(const char* pm, const char* fmt, ...)
   write(tc_client, buf, strlen(buf));
 }
 
-void playnextvid()
+unsigned int getduration(const char* vid)
 {
-  playing=queue.items[0];
-  --queue.itemcount;
-  memmove(queue.items, &queue.items[1], sizeof(char*)*queue.itemcount);
-  say(0, "/mbs youTube %s 0\n", playing);
-  // Find out the video's length and schedule an alarm for then
   int out[2];
   pipe(out);
   if(!fork())
@@ -172,15 +167,15 @@ void playnextvid()
     dup2(out[1], 1);
     close(2); // Ignore youtube-dl errors/warnings
     write(1, ":", 1);
-    execlp("youtube-dl", "youtube-dl", "--get-duration", playing, (char*)0);
+    execlp("youtube-dl", "youtube-dl", "--get-duration", vid, (char*)0);
     perror("execlp(youtube-dl)");
     _exit(1);
   }
-  close(out[1]);
   wait(0);
+  close(out[1]);
   char timebuf[128];
   int len=read(out[0], timebuf, 127);
-  if(len<1){alarm(60); return;}
+  if(len<1){return 60;} // If using youtube-dl fails, assume all videos are 1 minute long
   timebuf[len]=0;
   close(out[0]);
   // youtube-dl prints it out in hh:mm:ss format, convert it to plain seconds
@@ -196,11 +191,21 @@ void playnextvid()
   // Days
   sep=strrchr(timebuf, ':');
   if(sep){sep[0]=0; len+=atoi(&sep[1])*24;}
-//  printf("Estimated video length to %u sec\n", len);
-  alarm(len);
+  return len;
 }
 
 char waitskip=0;
+void playnextvid()
+{
+  waitskip=0;
+  playing=queue.items[0];
+  --queue.itemcount;
+  memmove(queue.items, &queue.items[1], sizeof(char*)*queue.itemcount);
+  say(0, "/mbs youTube %s 0\n", playing);
+  // Find out the video's length and schedule an alarm for then
+  alarm(getduration(playing));
+}
+
 void playnext(int x)
 {
   free(playing);
@@ -365,13 +370,21 @@ int main(int argc, char** argv)
         else if(!strcmp(msg, "!help"))
         {
           say(nick, "The following commands can be used:\n");
+          usleep(100000);
           say(nick, "!request <link> = request a video to be played\n");
+          usleep(100000);
           say(nick, "!queue          = get the number of songs in queue and which (if any) need to  be approved\n");
+          usleep(100000);
           say(nick, "Mod commands:\n"); // TODO: don't bother filling non-mods' chats with these?
+          usleep(100000);
           say(nick, "!playnext       = play the next video in queue without approving it (to see if it's ok)\n");
+          usleep(100000);
           say(nick, "!approve        = mark the currently playing video, or if none is playing the next in queue\n");
+          usleep(100000);
           say(nick, "!approve <link> = mark the specified video as okay \n");
+          usleep(100000);
           say(nick, "!badvid         = stop playing the current video and mark it as bad\n");
+          usleep(100000);
           say(nick, "!badvid <link>  = mark the specified video as bad, preventing it from ever being queued again\n");
         }
         else if(list_contains(&mods, nick)) // Mods-only commands
@@ -417,6 +430,18 @@ int main(int argc, char** argv)
             list_save(&badvids, "badvids.txt");
             if(vid==playing){say(0, "/mbc youTube\n"); playnext(0);}
           }
+          else if(!strncmp(msg, "/mbs youTube ", 13))
+          {
+            // Someone manually started a video, mark that video as good, remove it from queue, and set an alarm for when it's modbot's turn to play stuff again
+            char* vid=&msg[13];
+            char* end=strchr(vid, ' ');
+            if(end){end[0]=0;}
+            list_del(&queue, vid);
+            list_add(&goodvids, vid);
+            list_save(&goodvids, "goodvids.txt");
+            alarm(getduration(vid));
+          }
+          else if(!strcmp(msg, "/mbc youTube")){playnext(0);} // TODO: handle /mbsk (seek) too?
         }
       }else{ // Actions
         if(!strncmp(space, " changed nickname to ", 21))