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

    Version 0.23

diff --git a/ChangeLog b/ChangeLog
index 424a400..1c9f9f1 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,15 @@
+0.23:
+Added the moderator commands /mute and /push2talk (contributed by Jade)
+Set tc_client as a dependency of utils.
+Implemented RTMP message type 1 (set chunk size)
+Writing audio data the same way video data is written.
+camviewer: fixed a bug where joining a channel where no one is on cam resulted in trying to view the cam for "", leading to an empty GtkImage in the row of cams. 
+camviewer: added labels under each image with people's nicknames.
+Added a basic ncurses utility called cursedchat.
+modbot: for manual /mbc, remove the video from goodvids.
+modbot: fixed a bug that caused a segfault when using !badvid if nothing was playing.
+modbot: added a response for !wrongrequest without an argument when no request is found.
+modbot: renamed !whorequested to !requestedby for the sake of immature minds.
 0.22:
 Cleaned up media.c/h which I forgot to clean up before releasing 0.21.
 Detect when someone cams down.
@@ -53,7 +65,7 @@ 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)
+modbot: handle manual /mbs and /mbc commands (remove from queue, mark as good for /mbs)
 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:
diff --git a/Makefile b/Makefile
index f943ec4..e780924 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-VERSION=0.22
+VERSION=0.23
 CFLAGS=-g3 -Wall $(shell curl-config --cflags)
 LIBS=-g3 $(shell curl-config --libs)
 ifneq ($(wildcard config.mk),)
@@ -8,6 +8,7 @@ OBJ=client.o amfparser.o rtmp.o numlist.o amfwriter.o idlist.o colors.o endian.o
 IRCHACK_OBJ=utilities/irchack/irchack.o
 MODBOT_OBJ=utilities/modbot/modbot.o utilities/modbot/list.o utilities/modbot/queue.o
 CAMVIEWER_OBJ=utilities/camviewer/camviewer.o
+CURSEDCHAT_OBJ=utilities/cursedchat/cursedchat.o
 UTILS=irchack modbot
 ifdef GTK_LIBS
 ifdef AVCODEC_LIBS
@@ -19,11 +20,17 @@ endif
 endif
 endif
 endif
+ifdef CURSES_LIBS
+ifdef READLINE_LIBS
+  UTILS+=cursedchat
+  CFLAGS+=$(CURSES_CFLAGS)
+endif
+endif
 
 tc_client: $(OBJ)
  $(CC) $(LDFLAGS) $^ $(LIBS) -o $@
 
-utils: $(UTILS)
+utils: $(UTILS) tc_client
 
 irchack: $(IRCHACK_OBJ)
  $(CC) $(LDFLAGS) $^ $(LIBS) -o $@
@@ -34,8 +41,11 @@ modbot: $(MODBOT_OBJ)
 camviewer: $(CAMVIEWER_OBJ)
  $(CC) $(LDFLAGS) $^ $(LIBS) $(GTK_LIBS) $(AVCODEC_LIBS) $(AVUTIL_LIBS) $(SWSCALE_LIBS) -o $@
 
+cursedchat: $(CURSEDCHAT_OBJ)
+ $(CC) $(LDFLAGS) $^ $(LIBS) $(READLINE_LIBS) $(CURSES_LIBS) -o $@
+
 clean:
- rm -f $(OBJ) $(IRCHACK_OBJ) $(MODBOT_OBJ) $(CAMVIEWER_OBJ) tc_client irchack modbot camviewer
+ rm -f $(OBJ) $(IRCHACK_OBJ) $(MODBOT_OBJ) $(CAMVIEWER_OBJ) $(CURSEDCHAT_OBJ) tc_client irchack modbot camviewer cursedchat
 
 tarball:
- tar -cJf tc_client-$(VERSION).tar.xz --transform='s|^|tc_client-$(VERSION)/|' Makefile client.c amfparser.c rtmp.c numlist.c amfwriter.c idlist.c colors.c endian.c media.c amfparser.h rtmp.h numlist.h amfwriter.h idlist.h colors.h endian.h media.h LICENSE README ChangeLog crossbuild.sh utilities/irchack/irchack.c utilities/modbot/modbot.c utilities/modbot/list.c utilities/modbot/list.h utilities/modbot/queue.c utilities/modbot/queue.h utilities/camviewer/camviewer.c configure
+ tar -cJf tc_client-$(VERSION).tar.xz --transform='s|^|tc_client-$(VERSION)/|' Makefile client.c amfparser.c rtmp.c numlist.c amfwriter.c idlist.c colors.c endian.c media.c amfparser.h rtmp.h numlist.h amfwriter.h idlist.h colors.h endian.h media.h LICENSE README ChangeLog crossbuild.sh utilities/irchack/irchack.c utilities/modbot/modbot.c utilities/modbot/list.c utilities/modbot/list.h utilities/modbot/queue.c utilities/modbot/queue.h utilities/camviewer/camviewer.c utilities/cursedchat/cursedchat.c configure
diff --git a/amfparser.c b/amfparser.c
index 35e9a76..c9829ef 100644
--- a/amfparser.c
+++ b/amfparser.c
@@ -1,6 +1,6 @@
 /*
     tc_client, a simple non-flash client for tinychat(.com)
-    Copyright (C) 2014  alicia@ion.nu
+    Copyright (C) 2014-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
diff --git a/amfparser.h b/amfparser.h
index 96d9a1c..7244445 100644
--- a/amfparser.h
+++ b/amfparser.h
@@ -1,6 +1,6 @@
 /*
     tc_client, a simple non-flash client for tinychat(.com)
-    Copyright (C) 2014  alicia@ion.nu
+    Copyright (C) 2014-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
diff --git a/client.c b/client.c
index e7f8824..9cd746d 100644
--- a/client.c
+++ b/client.c
@@ -226,6 +226,7 @@ int main(int argc, char** argv)
     if(!strcmp(argv[i], "-h")||!strcmp(argv[i], "--help")){usage(argv[0]); return 0;}
     else if(!strcmp(argv[i], "-u")||!strcmp(argv[i], "--user"))
     {
+      if(i+1==argc){continue;}
       ++i;
       account_user=argv[i];
       unsigned int j;
@@ -233,6 +234,7 @@ int main(int argc, char** argv)
     }
     else if(!strcmp(argv[i], "-p")||!strcmp(argv[i], "--pass"))
     {
+      if(i+1==argc){continue;}
       ++i;
       account_pass=strdup(argv[i]);
     }
@@ -401,7 +403,9 @@ int main(int argc, char** argv)
                  "/ban <nick>     = ban someone\n"
                  "/banlist        = list who is banned\n"
                  "/forgive <nick/ID> = unban someone\n"
-                 "/names          = list everyone that is online\n");
+                 "/names          = list everyone that is online\n"
+                 "/mute           = temporarily mutes all non-moderator broadcasts\n"
+                 "/push2talk      = puts all non-operators in push2talk mode\n");
           fflush(stdout);
         }
         else if(!strncmp(buf, "/color", 6) && (!buf[6]||buf[6]==' '))
@@ -522,6 +526,26 @@ int main(int argc, char** argv)
           fflush(stdout);
           continue;
         }
+        else if(!strcmp(buf, "/mute"))
+        {
+          amfinit(&amf, 3);
+          amfstring(&amf, "owner_run");
+          amfnum(&amf, 0);
+          amfnull(&amf);
+          amfstring(&amf, "mute");
+          amfsend(&amf, sock);
+          continue;
+        }
+        else if(!strcmp(buf, "/push2talk"))
+        {
+          amfinit(&amf, 3);
+          amfstring(&amf, "owner_run");
+          amfnum(&amf, 0);
+          amfnull(&amf);
+          amfstring(&amf, "push2talk");
+          amfsend(&amf, sock);
+          continue;
+        }
       }
       char* msg=tonumlist(buf, len);
       amfinit(&amf, 3);
@@ -546,7 +570,7 @@ int main(int argc, char** argv)
     char rtmpres=rtmp_get(sock, &rtmp);
     if(!rtmpres){printf("Server disconnected\n"); break;}
     if(rtmpres==2){continue;} // Not disconnected, but we didn't get a complete chunk yet either
-    if(rtmp.type==RTMP_VIDEO){stream_handledata(&rtmp); continue;}
+    if(rtmp.type==RTMP_VIDEO || rtmp.type==RTMP_AUDIO){stream_handledata(&rtmp); continue;}
     if(rtmp.type!=RTMP_AMF0){printf("Got RTMP type 0x%x\n", rtmp.type); continue;}
     struct amf* amfin=amf_parse(rtmp.buf, rtmp.length);
     if(amfin->itemcount>0 && amfin->items[0].type==AMF_STRING)
@@ -690,6 +714,16 @@ int main(int argc, char** argv)
         printf("%s %s\n", timestamp(), notice);
         fflush(stdout);
       }
+      else if(!strncmp("mute", amfin->items[2].string.string, 4))
+      {
+        printf("%s Non-moderators have been temporarily muted.\n", timestamp());
+        fflush(stdout);
+      }
+      else if(!strncmp("push2talk", amfin->items[2].string.string, 9))
+      {
+        printf("%s Push to talk request has been sent to non-moderators.\n", timestamp());
+        fflush(stdout);
+      }
     }
     // oper, identifies mods
     else if(amfin->itemcount==4 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "oper") && amfin->items[3].type==AMF_STRING)
diff --git a/configure b/configure
index 955fecc..4d62ba9 100755
--- a/configure
+++ b/configure
@@ -87,4 +87,26 @@ else
   echo no
 fi
 
+printf 'Checking for ncurses... '
+libs="`ncursesw5-config --libs 2> /dev/null || ncurses5-config --libs 2> /dev/null`"
+if [ "x$libs" != "x" ]; then
+  echo "CURSES_LIBS=${libs}" >> config.mk
+  echo "CURSES_CFLAGS=`ncursesw5-config --cflags 2> /dev/null || ncurses5-config --cflags 2> /dev/null`" >> config.mk
+  echo yes
+else
+  echo no
+fi
+
+printf 'Checking for readline... '
+echo '#include <stdio.h>' > readlinetest.c
+echo '#include <readline/readline.h>' >> readlinetest.c
+echo 'int main(){rl_initialize();return 0;}' >> readlinetest.c
+if "$CC" readlinetest.c -lreadline ${libs} -o readlinetest > /dev/null 2> /dev/null; then
+  echo "READLINE_LIBS=-lreadline" >> config.mk
+  echo yes
+else
+  echo no
+fi
+rm -f readlinetest.c readlinetest
+
 echo Done
diff --git a/media.c b/media.c
index 18157be..a4e7bf3 100644
--- a/media.c
+++ b/media.c
@@ -82,7 +82,12 @@ void stream_handledata(struct rtmp* rtmp)
   {
     if(streams[i].streamid!=rtmp->msgid){continue;}
 // fprintf(stderr, "Chunk: chunkid: %u, streamid: %u, userid: %u\n", rtmp->chunkid, rtmp->msgid, streams[i].userid);
-    printf("Video: %u %u\n", streams[i].userid, rtmp->length);
+    if(rtmp->type==RTMP_VIDEO)
+    {
+      printf("Video: %u %u\n", streams[i].userid, rtmp->length);
+    }else if(rtmp->type==RTMP_AUDIO){
+      printf("Audio: %u %u\n", streams[i].userid, rtmp->length);
+    }
     fwrite(rtmp->buf, rtmp->length, 1, stdout);
     fflush(stdout);
     return;
@@ -107,8 +112,7 @@ void stream_handlestatus(struct amf* amf)
       if(streams[i].userid==id)
       {
         printf("VideoEnd: %u\n", streams[i].userid);
-// TODO: remove stream from array
-// TODO: destroyStream or something?
+        // Note: not removing it from the list because tinychat doesn't seem to handle reusing stream IDs
         return;
       }
     }
diff --git a/rtmp.c b/rtmp.c
index ab2c29f..443a484 100644
--- a/rtmp.c
+++ b/rtmp.c
@@ -33,6 +33,7 @@ struct chunk
 };
 struct chunk* chunks=0;
 unsigned int chunkcount=0;
+unsigned int chunksize_in=128;
 
 struct chunk* chunk_get(unsigned int id)
 {
@@ -99,7 +100,7 @@ char rtmp_get(int sock, struct rtmp* rtmp)
     chunk->buf=malloc(chunk->length);
     chunk->pos=0;
   }
-  unsigned int rsize=((chunk->length-chunk->pos>127)?128:(chunk->length-chunk->pos));
+  unsigned int rsize=((chunk->length-chunk->pos>=chunksize_in)?chunksize_in:(chunk->length-chunk->pos));
   while(rsize>0)
   {
     size_t r=read(sock, chunk->buf+chunk->pos, rsize);
@@ -110,6 +111,11 @@ char rtmp_get(int sock, struct rtmp* rtmp)
   }
   if(chunk->pos==chunk->length)
   {
+    if(chunk->type==RTMP_SET_PACKET_SIZE)
+    {
+      memcpy(&chunksize_in, chunk->buf, sizeof(unsigned int));
+//      printf("Server set chunk size to %u (packet size: %u)\n", chunksize_in, chunk->length);
+    }
 // printf("Got chunk: chunkid=%u, type=%u, length=%u, streamid=%u\n", chunk->id, chunk->type, chunk->length, chunk->streamid);
     rtmp->type=chunk->type;
     rtmp->chunkid=chunk->id;
diff --git a/utilities/camviewer/camviewer.c b/utilities/camviewer/camviewer.c
index dca0f35..2b25607 100644
--- a/utilities/camviewer/camviewer.c
+++ b/utilities/camviewer/camviewer.c
@@ -21,7 +21,17 @@
 #include <gtk/gtk.h>
 
 #if GTK_MAJOR_VERSION==2
-  #define gtk_box_new(orientation, spacing) gtk_hbox_new(1, spacing)
+  #define GTK_ORIENTATION_HORIZONTAL 0
+  #define GTK_ORIENTATION_VERTICAL 1
+  GtkWidget* gtk_box_new(int vertical, int spacing)
+  {
+    if(vertical)
+    {
+      return gtk_vbox_new(1, spacing);
+    }else{
+      return gtk_hbox_new(1, spacing);
+    }
+  }
 #endif
 
 struct camera
@@ -32,6 +42,7 @@ struct camera
   AVCodecContext* ctx;
   char* id;
   char* nick;
+  GtkWidget* box; // holds label and cam
 };
 
 struct viddata
@@ -64,6 +75,7 @@ gboolean handledata(GIOChannel* channel, GIOCondition condition, gpointer datap)
     {
       char* user=&next[2];
       next=strstr(user, ", ");
+      if(!user[0]){continue;}
       if(next){next[0]=0;}
       dprintf(tc_client_in[1], "/opencam %s\n", user);
     }
@@ -97,8 +109,11 @@ gboolean handledata(GIOChannel* channel, GIOCondition condition, gpointer datap)
     cam->ctx=avcodec_alloc_context3(data->decoder);
     avcodec_open2(cam->ctx, data->decoder, 0);
     cam->cam=gtk_image_new();
-    gtk_box_pack_start(GTK_BOX(data->box), cam->cam, 0, 0, 0);
-    gtk_widget_show(cam->cam);
+    cam->box=gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+    gtk_box_pack_start(GTK_BOX(cam->box), cam->cam, 0, 0, 0);
+    gtk_box_pack_start(GTK_BOX(cam->box), gtk_label_new(cam->nick), 0, 0, 0);
+    gtk_box_pack_start(GTK_BOX(data->box), cam->box, 0, 0, 0);
+    gtk_widget_show_all(cam->box);
     return 1;
   }
   if(!strncmp(buf, "VideoEnd: ", 10))
@@ -107,7 +122,7 @@ gboolean handledata(GIOChannel* channel, GIOCondition condition, gpointer datap)
     {
       if(!strcmp(data->cams[i].id, &buf[10]))
       {
-        gtk_widget_destroy(data->cams[i].cam);
+        gtk_widget_destroy(data->cams[i].box);
         av_frame_free(&data->cams[i].frame);
         avcodec_free_context(&data->cams[i].ctx);
         free(data->cams[i].id);
@@ -119,6 +134,21 @@ gboolean handledata(GIOChannel* channel, GIOCondition condition, gpointer datap)
     }
     return 1;
   }
+  if(!strncmp(buf, "Audio: ", 7))
+  {
+    char* sizestr=strchr(&buf[7], ' ');
+    if(!sizestr){return 1;}
+    unsigned int size=strtoul(&sizestr[1], 0, 0);
+    unsigned char frameinfo;
+    read(tc_client[0], &frameinfo, 1);
+    unsigned char buf[size];
+    unsigned int pos=0;
+    while(pos<size)
+    {
+      pos+=read(tc_client[0], &buf[pos], size-pos);
+    }
+// TODO: decode and send to a sound lib (libao)
+  }
   if(strncmp(buf, "Video: ", 7)){printf("Got '%s'\n", buf); return 1;} // Ignore anything else that isn't video
   char* sizestr=strchr(&buf[7], ' ');
   if(!sizestr){return 1;}
diff --git a/utilities/cursedchat/cursedchat.c b/utilities/cursedchat/cursedchat.c
new file mode 100644
index 0000000..5da726a
--- /dev/null
+++ b/utilities/cursedchat/cursedchat.c
@@ -0,0 +1,278 @@
+/*
+    cursedchat, a simple curses interface for tc_client
+    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 <unistd.h>
+#include <stdlib.h>
+#include <poll.h>
+#include <signal.h>
+#include <sys/ioctl.h>
+#include <locale.h>
+#include <curses.h>
+#include <readline/readline.h>
+
+WINDOW* topic;
+WINDOW* chat;
+WINDOW* input;
+int to_app;
+
+// Translate ANSI escape codes to curses commands and write the text to a window
+void waddansi(WINDOW* w, char* str)
+{
+  while(str[0])
+  {
+    char* esc=strstr(str, "\x1b[");
+    if(esc==str)
+    {
+      str=&str[2];
+      while(str[0]!='m')
+      {
+        if(str[0]=='3'&&str[1]!='m') // Color
+        {
+          unsigned int c=strtoul(&str[1], &str, 10);
+          wattron(w, COLOR_PAIR(c+1));
+        }
+        else if(str[0]=='1') // Bold
+        {
+          wattron(w, A_BOLD);
+          str=&str[1];
+        }
+        else if(str[0]=='0') // Reset
+        {
+          wattroff(w, COLOR_PAIR(1));
+          wattroff(w, A_BOLD);
+          str=&str[1];
+        }
+        else{str=&str[1];}
+      }
+      str=&str[1];
+      continue;
+    }
+    if(esc)
+    {
+      waddnstr(w, str, esc-str);
+      str=esc;
+    }else{
+      waddstr(w, str);
+      return;
+    }
+  }
+}
+
+void gotline(char* line)
+{
+  if(!line){close(to_app); return;} // TODO: handle EOF on stdin better?
+// TODO: handle commands (/pm, /help addition)
+
+  write(to_app, line, strlen(line));
+  write(to_app, "\n", 1);
+// TODO: grab user's nick for this
+  wprintw(chat, "\n%s: %s", "You", line);
+  wrefresh(chat);
+}
+
+unsigned int bytestochars(const char* buf, unsigned int buflen, unsigned int bytes)
+{
+  unsigned int pos=0;
+  unsigned int i;
+  for(i=0; i<bytes; ++pos)
+  {
+    i+=mbtowc(0,&buf[i],buflen-i);
+  }
+  return pos;
+}
+
+unsigned int charstobytes(const char* buf, unsigned int buflen, unsigned int chars)
+{
+  unsigned int pos;
+  unsigned int i=0;
+  for(pos=0; pos<chars; ++pos)
+  {
+    i+=mbtowc(0,&buf[i],buflen-i);
+  }
+  return i;
+}
+
+int escinput(int a, int byte)
+{
+  char buf[4];
+  read(0, buf, 2);
+  buf[2]=0;
+  if(!strcmp(buf, "[A")||!strcmp(buf, "OA")){return 0;} // TODO: history?
+  if(!strcmp(buf, "[B")||!strcmp(buf, "OB")){return 0;} // TODO: history?
+  if(!strcmp(buf, "[C")||!strcmp(buf, "OC")){rl_forward(1,27);return 0;}
+  if(!strcmp(buf, "[D")||!strcmp(buf, "OD")){rl_backward(1,27);return 0;}
+  if(!strcmp(buf, "[H")||!strcmp(buf, "OH")){rl_beg_of_line(1,27);return 0;}
+  if(!strcmp(buf, "[F")||!strcmp(buf, "OF")){rl_end_of_line(1,27);return 0;}
+  if(!strcmp(buf, "[3")&&read(0, buf, 1)&&buf[0]=='~'){rl_delete(1,27);return 0;}
+  if(!strcmp(buf, "[5"))
+  {
+    read(0, buf, 1);
+    wprintw(chat, "\nTODO: handle Page up");
+// wscrl(chat, chat->_maxy/2);
+    wrefresh(chat);
+    return 0;
+  }
+  if(!strcmp(buf, "[6"))
+  {
+    read(0, buf, 1);
+    wprintw(chat, "\nTODO: handle Page down");
+// wscrl(chat, -chat->_maxy/2);
+    wrefresh(chat);
+    return 0;
+  }
+//  wprintw(chat, "\nbuf: %s", buf);
+  return 0;
+}
+
+void drawinput()
+{
+  werase(input);
+  unsigned int pos=bytestochars(rl_line_buffer, rl_end, rl_point);
+
+  waddstr(input, "> ");
+  int cursor_row=(pos+2)/COLS;
+  int end_row=(rl_end+2)/COLS;
+  // Figure out how much of the buffer to print to not scroll past the cursor
+  unsigned int eol=charstobytes(rl_line_buffer, rl_end, (cursor_row+2)*COLS-3); // -2 for cursor, -1 to avoid wrapping
+  waddnstr(input, rl_line_buffer, eol);
+
+  wmove(input, cursor_row==end_row && cursor_row>0, (pos+2)%COLS); // +2 for prompt
+  wrefresh(input);
+}
+
+void resizechat(int sig)
+{
+  struct winsize size;
+  ioctl(0, TIOCGWINSZ, &size);
+  if(size.ws_row<3){return;} // Too small, would result in negative numbers breaking the chat window
+  resize_term(size.ws_row, size.ws_col);
+  wresize(topic, 1, COLS);
+  wresize(chat, LINES-3, COLS);
+  wresize(input, 2, COLS);
+  mvwin(input, LINES-2, 0);
+  redrawwin(chat);
+  redrawwin(topic);
+  redrawwin(input);
+  wrefresh(chat);
+  wrefresh(topic);
+//  wrefresh(input);
+  drawinput();
+}
+
+int main(int argc, char** argv)
+{
+  if(argc<3){execv("./tc_client", argv); return 1;}
+  setlocale(LC_ALL, "");
+  WINDOW* w=initscr();
+  signal(SIGWINCH, resizechat);
+  start_color();
+  cbreak();
+  noecho();
+  keypad(w, 1);
+  topic=newwin(1, COLS, 0, 0);
+  init_pair(1, COLOR_WHITE, COLOR_BLUE);
+
+  // Define colors mapped to ANSI color codes (at least the ones tc_client uses)
+  init_pair(2, COLOR_RED, 0);
+  init_pair(3, COLOR_GREEN, 0);
+  init_pair(4, COLOR_YELLOW, 0);
+  init_pair(5, COLOR_BLUE, 0);
+  init_pair(6, COLOR_MAGENTA, 0);
+  init_pair(7, COLOR_CYAN, 0);
+
+  wbkgd(topic, COLOR_PAIR(1)|' ');
+  chat=newwin(LINES-3, COLS, 1, 0);
+  scrollok(chat, 1);
+  input=newwin(2, COLS, LINES-2, 0);
+  scrollok(input, 1);
+  rl_initialize();
+  rl_callback_handler_install(0, gotline);
+  rl_bind_key('\x1b', escinput);
+  wprintw(input, "> ");
+  wrefresh(topic);
+  wrefresh(input);
+  int app_in[2];
+  int app_out[2];
+  pipe(app_in);
+  pipe(app_out);
+  if(!fork())
+  {
+    close(app_in[1]);
+    close(app_out[0]);
+    dup2(app_in[0],0);
+    dup2(app_out[1],1);
+    argv[0]="./tc_client";
+    execv("./tc_client", argv);
+    _exit(1);
+  }
+  close(app_in[0]);
+  close(app_out[1]);
+  to_app=app_in[1];
+  struct pollfd p[2]={{.fd=0, .events=POLLIN, .revents=0},
+                      {.fd=app_out[0], .events=POLLIN, .revents=0}};
+  while(1)
+  {
+    poll(p, 2, -1);
+    if(p[1].revents) // Getting data from tc_client
+    {
+      p[1].revents=0;
+      char buf[1024];
+      size_t len=0;
+      while(len<1023)
+      {
+        if(read(app_out[0], &buf[len], 1)!=1){len=-1; break;}
+        if(buf[len]=='\r'||buf[len]=='\n'){break;}
+        ++len;
+      }
+      if(len==-1){break;} // Bad read
+      buf[len]=0;
+      if(!strncmp(buf, "Room topic: ", 12))
+      {
+        werase(topic);
+        waddstr(topic, &buf[12]);
+        wrefresh(topic);
+      }
+      waddstr(chat, "\n");
+      waddansi(chat, buf);
+      wrefresh(chat);
+      wrefresh(input);
+      continue;
+    }
+    if(!p[0].revents){continue;}
+    p[0].revents=0;
+    rl_callback_read_char();
+    drawinput();
+#if 0
+// TODO: move this into a function and also call it when the terminal is resized
+    werase(input);
+    unsigned int pos=bytestochars(rl_line_buffer, rl_end, rl_point);
+
+    waddstr(input, "> ");
+    int cursor_row=(pos+2)/COLS;
+    int end_row=(rl_end+2)/COLS;
+    // Figure out how much of the buffer to print to not scroll past the cursor
+//    unsigned int eol=cursor_row;// (pos+2)/COLS;
+    unsigned int eol=charstobytes(rl_line_buffer, rl_end, (cursor_row+2)*COLS-3); // -2 for cursor, -1 to avoid wrapping
+    waddnstr(input, rl_line_buffer, eol);
+
+    wmove(input, cursor_row==end_row && cursor_row>0, (pos+2)%COLS); // +2 for prompt
+    wrefresh(input);
+#endif
+  }
+  rl_callback_handler_remove();
+  endwin();
+  return 0;
+}
diff --git a/utilities/modbot/modbot.c b/utilities/modbot/modbot.c
index 90eec60..54a3ac5 100644
--- a/utilities/modbot/modbot.c
+++ b/utilities/modbot/modbot.c
@@ -127,7 +127,7 @@ void playnext(int x)
   {
     if(!waitskip)
     {
-      say(0, "Next video (http://youtube.com/watch?v=%s) is not yet approved by mods\n", queue.items[0].video);
+      say(0, "Next video (%s, %s) is not yet approved by mods\n", queue.items[0].video, queue.items[0].title);
       unsigned int i;
       for(i=1; i<queue.itemcount; ++i)
       {
@@ -304,6 +304,10 @@ int main(int argc, char** argv)
               break;
             }
           }
+          if(i==queue.itemcount)
+          {
+            say(pm, "I can't find any request by you, sorry\n");
+          }
         }
         else if(!strncmp(msg, "!wrongrequest ", 14))
         {
@@ -354,7 +358,7 @@ int main(int argc, char** argv)
             say(pm, "%u videos in queue\n", queue.itemcount);
           }
         }
-        else if(!strcmp(msg, "!whorequested"))
+        else if(!strcmp(msg, "!requestedby"))
         {
           if(!playing){say(pm, "Nothing is playing\n");}
           else{say(pm, "%s requested %s\n", requester, playing);}
@@ -391,6 +395,8 @@ int main(int argc, char** argv)
           usleep(500000);
           say(nick, "!wrongrequest   = undo the last request you made\n");
           usleep(500000);
+          say(nick, "!requestedby    = see who requested the current video\n");
+          usleep(500000);
           say(nick, "You can also just play videos manually/the good old way and they will be marked as good.\n");
         }
         else if(list_contains(&mods, nick)) // Mods-only commands
@@ -476,13 +482,15 @@ int main(int argc, char** argv)
             {
               getvidinfo(&space[1], "--get-id", vid, 256);
             }else{strncpy(vid, playing, 1023); vid[1023]=0;}
-            if(!vid[0]){say(pm, "Video not found, sorry\n");}
+            if(!vid[0]){say(pm, "Video not found, sorry\n"); continue;}
             queue_del(&queue, vid);
             list_del(&goodvids, vid);
             list_add(&badvids, vid);
             list_save(&goodvids, "goodvids.txt");
             list_save(&badvids, "badvids.txt");
-            if(!strcmp(vid, playing)){say(0, "/mbc youTube\n"); playnext(0);}
+            if(playing && !strcmp(vid, playing)){say(0, "/mbc youTube\n");}
+            say(pm, "Marked '%s' as bad, it will not be allowed into the queue again. You can reverse this by !approve'ing the video by ID/link/name\n", vid);
+            playnext(0);
           }
           else if(!strcmp(msg, "!skip") || !strncmp(msg, "!skip ", 6))
           {
@@ -511,7 +519,12 @@ int main(int argc, char** argv)
             alarm(getduration(playing)-pos);
             started=time(0)-pos;
           }
-          else if(!strcmp(msg, "/mbc youTube")){playnext(0);} // Video cancelled
+          else if(!strcmp(msg, "/mbc youTube")) // Video cancelled
+          {
+            list_del(&goodvids, playing); // manual /mbc is often used when !badvid should be used, so at least remove it from the list of approved videos
+            list_save(&goodvids, "goodvids.txt");
+            playnext(0);
+          }
           else if(!strncmp(msg, "/mbsk youTube ", 14)) // Seeking
           {
             unsigned int pos=strtol(&msg[14], 0, 0)/1000;