$ git clone http://tcclient.ion.nu/tc_client.git
commit b44bfc1b10b95840bc08b2feb6fc12341093566f
Author: Alicia <...>
Date:   Thu Dec 29 18:16:31 2016 +0100

    Added some basic support for kageshi (using the -s/--site option)

diff --git a/ChangeLog b/ChangeLog
index 4a4583d..c7f68ed 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,6 @@
 0.42:
 Improved RTMP compatibility by responding to RTMP ping requests and always starting new chunk streams with a format 0 packet.
+Added some basic support for kageshi (using the -s/--site option)
 0.41.1:
 Use tinychat.com instead of apl.tinychat.com (works around SSL/TLS issues on windows)
 0.41:
diff --git a/Makefile b/Makefile
index e9412f6..8e5979f 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-VERSION=0.41.1
+VERSION=0.42pre
 CFLAGS=-g3 -Wall -I. $(shell curl-config --cflags)
 LDFLAGS=-g3
 PREFIX=/usr/local
@@ -11,7 +11,7 @@ else
  @echo 'Run ./configure first, make sure tc_client-gtk is enabled.'
   endif
 endif
-OBJ=client.o amfparser.o rtmp.o numlist.o amfwriter.o idlist.o colors.o endianutils.o media.o utilities/compat.o
+OBJ=client.o amfparser.o rtmp.o numlist.o amfwriter.o idlist.o colors.o endianutils.o media.o tinychat.o kageshi.o utilities/compat.o
 IRCHACK_OBJ=utilities/irchack/irchack.o utilities/compat.o
 MODBOT_OBJ=utilities/modbot/modbot.o utilities/list.o utilities/modbot/queue.o utilities/compat.o
 CAMVIEWER_OBJ=utilities/camviewer/camviewer.o utilities/compat.o utilities/compat_av.o libcamera.a
diff --git a/amfparser.h b/amfparser.h
index 7244445..e2313e5 100644
--- a/amfparser.h
+++ b/amfparser.h
@@ -14,6 +14,8 @@
     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/>.
 */
+#ifndef AMFPARSER_H
+#define AMFPARSER_H
 enum
 {
   AMF_NUMBER,
@@ -68,3 +70,4 @@ extern void amf_free(struct amf* amf);
 extern struct amfitem* amf_getobjmember(struct amfobject* obj, const char* name);
 
 extern void printamf(struct amf* amf);
+#endif
diff --git a/amfwriter.h b/amfwriter.h
index 62f735e..a709306 100644
--- a/amfwriter.h
+++ b/amfwriter.h
@@ -14,6 +14,8 @@
     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 "rtmp.h"
 
 extern void amfinit(struct rtmp* msg, unsigned int streamid);
 extern void amfnum(struct rtmp* msg, double v);
diff --git a/client.c b/client.c
index e0d574c..cf36e2b 100644
--- a/client.c
+++ b/client.c
@@ -36,9 +36,20 @@
 #include "media.h"
 #include "amfwriter.h"
 #include "utilities/compat.h"
+#include "client.h"
 
-const char* sitearg="tinychat";
-const char* bpassword=0;
+struct command
+{
+  const char* command;
+  void(*callback)(struct amf*,int);
+  unsigned int minargs;
+};
+static struct command* commands=0;
+static unsigned int commandcount=0;
+
+char greenroom=0;
+char* channel=0;
+char* nickname=0;
 
 struct writebuf
 {
@@ -46,17 +57,6 @@ struct writebuf
   int len;
 };
 
-void b_read(int sock, void* buf, size_t len)
-{
-  while(len>0)
-  {
-    size_t r=read(sock, buf, len);
-    if(r<1){return;}
-    len-=r;
-    buf+=r;
-  }
-}
-
 size_t writehttp(char* ptr, size_t size, size_t nmemb, void* x)
 {
   struct writebuf* data=x;
@@ -68,10 +68,10 @@ size_t writehttp(char* ptr, size_t size, size_t nmemb, void* x)
   return size;
 }
 
-CURL* curl=0;
 const char* cookiefile="";
 char* http_get(const char* url, const char* post)
 {
+  static CURL* curl=0;
   if(!curl){curl=curl_easy_init();}
   if(!curl){return 0;}
   curl_easy_setopt(curl, CURLOPT_URL, url);
@@ -89,160 +89,6 @@ char* http_get(const char* url, const char* post)
   return writebuf.buf; // should be free()d when no longer needed
 }
 
-char* gethost(char *channel, char *password)
-{
-  int urllen;
-  if(password)
-  {
-    urllen=strlen("http://tinychat.com/api/find.room/?site=&password=0")+strlen(channel)+strlen(sitearg)+strlen(password);
-  }else{
-    urllen=strlen("http://tinychat.com/api/find.room/?site=0")+strlen(channel)+strlen(sitearg);
-  }
-  char url[urllen];
-  if(password)
-  {
-    sprintf(url, "http://tinychat.com/api/find.room/%s?site=%s&password=%s", channel, sitearg, password);
-  }else{
-    sprintf(url, "http://tinychat.com/api/find.room/%s?site=%s", channel, sitearg);
-  }
-  char* response=http_get(url, 0);
-  if(!response){exit(-1);}
-  //response contains result='(OK|RES)|PW' (latter means a password is required)
-  char* result=strstr(response, "result='");
-  if(!result){printf("No result\n"); exit(-1);}
-  char* bpass=strstr(response, " bpassword='");
-  result+=strlen("result='");
-  // Handle the result value
-  if(!strncmp(result, "PW", 2)){printf("Password required\n"); exit(-1);}
-  if(strncmp(result, "OK", 2) && strncmp(result, "RES", 3)){printf("Result not OK\n"); exit(-1);}
-  // Find and extract the server responsible for this channel
-  char* rtmp=strstr(response, "rtmp='rtmp://");
-  if(!rtmp){printf("No rtmp found.\n"); exit(-1);}
-  rtmp+=strlen("rtmp='rtmp://");
-  int len;
-  for(len=0; rtmp[len] && rtmp[len]!='/'; ++len);
-  char* host=strndup(rtmp, len);
-  // Check if this is a greenroom channel
-  if(strstr(response, "greenroom=\"1\""))
-  {
-    printf("Channel has greenroom\n");
-    fflush(stdout);
-  }
-  char* end;
-  if(bpass && (end=strchr(&bpass[12], '\'')))
-  {
-    end[0]=0;
-    bpassword=strdup(&bpass[12]);
-    printf("Authenticated to broadcast\n");
-  }
-  free(response);
-  return host;
-}
-
-char* getkey(int id, const char* channel)
-{
-  char url[snprintf(0,0, "http://tinychat.com/api/captcha/check.php?guest%%5Fid=%i&room=%s%%5E%s", id, sitearg, channel)+1];
-  sprintf(url, "http://tinychat.com/api/captcha/check.php?guest%%5Fid=%i&room=%s%%5E%s", id, sitearg, channel);
-  char* response=http_get(url, 0);
-  char* key=strstr(response, "\"key\":\"");
-
-  if(!key){return 0;}
-  key+=7;
-  char* keyend=strchr(key, '"');
-  if(!keyend){return 0;}
-  char* backslash;
-  while((backslash=strchr(key, '\\')))
-  {
-    memmove(backslash, &backslash[1], strlen(backslash));
-    --keyend;
-  }
-  key=strndup(key, keyend-key);
-  free(response);
-  return key;
-}
-
-char* getcookie(const char* channel)
-{
-  unsigned long long now=time(0);
-  char url[strlen("http://tinychat.com/cauth?t=&room=0")+snprintf(0,0, "%llu", now)+strlen(channel)];
-  sprintf(url, "http://tinychat.com/cauth?t=%llu&room=%s", now, channel);
-  char* response=http_get(url, 0);
-  if(strstr(response, "\"lurker\":1"))
-  {
-    printf("Captcha not completed, you will not be able to send any messages or broadcast\n");
-  }
-  char* cookie=strstr(response, "\"cookie\":\"");
-
-  if(!cookie){return 0;}
-  cookie+=10;
-  char* end=strchr(cookie, '"');
-  if(!end){return 0;}
-  cookie=strndup(cookie, end-cookie);
-  free(response);
-  return cookie;
-}
-
-char* getbroadcastkey(const char* channel, const char* nick, const char* bpassword)
-{
-  unsigned int id=idlist_get(nick);
-  char url[snprintf(0,0, "http://tinychat.com/api/broadcast.pw?name=%s&site=%s&nick=%s&id=%u%s%s", channel, sitearg, nick, id, bpassword?"&password=":"", bpassword?bpassword:"")+1];
-  sprintf(url, "http://tinychat.com/api/broadcast.pw?name=%s&site=%s&nick=%s&id=%u%s%s", channel, sitearg, nick, id, bpassword?"&password=":"", bpassword?bpassword:"");
-  char* response=http_get(url, 0);
-  if(strstr(response, " result='PW'")){free(response); return 0;}
-  char* key=strstr(response, " token='");
-
-  if(!key){free(response); return 0;}
-  key+=8;
-  char* keyend=strchr(key, '\'');
-  if(!keyend){free(response); return 0;}
-  key=strndup(key, keyend-key);
-  free(response);
-  return key;
-}
-
-char* getmodkey(const char* user, const char* pass, const char* channel, char* loggedin)
-{
-  // TODO: if possible, do this in a neater way than digging the key out from an HTML page.
-  if(!user||!pass){return 0;}
-  char post[strlen("form_sent=1&username=&password=&next=http://tinychat.com/0")+strlen(user)+strlen(pass)+strlen(channel)];
-  sprintf(post, "form_sent=1&username=%s&password=%s&next=http://tinychat.com/%s", user, pass, channel);
-  char* response=http_get("http://tinychat.com/login", post);
-  char* key=strstr(response, "autoop: \"");
-  if(key)
-  {
-    key=&key[9];
-    char* end=strchr(key, '"');
-    if(end)
-    {
-      end[0]=0;
-      key=strdup(key);
-    }else{key=0;}
-  }
-  if(strstr(response, "<div class='name'")){*loggedin=1;}
-  free(response);
-  return key;
-}
-
-void getcaptcha(void)
-{
-  char* url="http://tinychat.com/cauth/captcha";
-  char* page=http_get(url, 0);
-  char* token=strstr(page, "\"token\":\"");
-  if(token)
-  {
-    token=&token[9];
-    char* end=strchr(token, '"');
-    if(end)
-    {
-      end[0]=0;
-      printf("Captcha: http://tinychat.com/cauth/recaptcha?token=%s\n", token);
-      fflush(stdout);
-      fgetc(stdin);
-    }
-  }
-  free(page);
-}
-
 char timestampbuf[8];
 char* timestamp()
 {
@@ -253,47 +99,46 @@ char* timestamp()
   return timestampbuf;
 }
 
-char checknick(const char* nick) // Returns zero if the nick is valid, otherwise returning the character that failed the check
+int connectto(const char* host, const char* port)
 {
-  unsigned int i;
-  for(i=0; nick[i]; ++i)
+  struct addrinfo* res;
+  getaddrinfo(host, port, 0, &res);
+  int sock=socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
+  if(connect(sock, res->ai_addr, res->ai_addrlen))
   {
-    if(!strchr("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-^[]{}`\\|", nick[i])){return nick[i];}
+    perror("Failed to connect");
+    freeaddrinfo(res);
+    return 1;
   }
-  return 0;
+  freeaddrinfo(res);
+  int i=1;
+  setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &i, sizeof(i));
+  return sock;
 }
 
-char* getprivfield(char* nick)
+void registercmd(const char* command, void(*callback)(struct amf*,int), unsigned int minargs)
 {
-  unsigned int id;
-  unsigned int privlen;
-  for(privlen=0; nick[privlen]&&nick[privlen]!=' '; ++privlen);
-  id=idlist_get(nick);
-  if(id<0)
-  {
-    nick[privlen]=0;
-    printf("No such nick: %s\n", nick);
-    fflush(stdout);
-    return 0;
-  }
-  char* priv=malloc(snprintf(0, 0, "n%i-", id)+privlen+1);
-  sprintf(priv, "n%i-", id); // 'n' for not-broadcasting
-  strncat(priv, nick, privlen);
-  return priv;
+  ++commandcount;
+  commands=realloc(commands, sizeof(struct command)*commandcount);
+  commands[commandcount-1].command=strdup(command);
+  commands[commandcount-1].callback=callback;
+  commands[commandcount-1].minargs=minargs;
 }
 
-void usage(const char* me)
+static void usage(const char* me)
 {
   printf("Usage: %s [options] <channelname> <nickname> [channelpassword]\n"
          "Options include:\n"
-         "-h, --help           Show this help text and exit\n"
+         "-h, --help           Show this help text and exit.\n"
          "-v, --version        Show the program version and exit.\n"
          "-u, --user <user>    Username of tinychat account to use.\n"
          "-p, --pass <pass>    Password of tinychat account to use.\n"
          "-c, --color <value>  Color to use in chat.\n"
+         "-s, --site <site>    Site to connect to.\n"
          "    --hexcolors      Print hex colors instead of ANSI color codes.\n"
          "    --cookies <file> File to store cookies in (not having to solve\n"
          "                       the captchas every time)\n"
+         "    --greenroom      Join a channel's greenroom.\n"
 #ifdef RTMP_DEBUG
          "    --rtmplog <file> Write RTMP input to file.\n"
 #endif
@@ -303,13 +148,15 @@ void usage(const char* me)
 extern int rtmplog;
 #endif
 
+extern int init_tinychat(const char* chanpass, const char* username, const char* userpass, struct site* site);
+extern int init_kageshi(const char* chanpass, const char* username, const char* userpass, struct site* site);
+extern int init_kageshicam(struct site* site);
 int main(int argc, char** argv)
 {
-  char* channel=0;
-  char* nickname=0;
   char* password=0;
   char* account_user=0;
   char* account_pass=0;
+  const char* sitestr=0;
   int i;
   for(i=1; i<argc; ++i)
   {
@@ -334,6 +181,11 @@ int main(int argc, char** argv)
       ++i;
       currentcolor=atoi(argv[i]);
     }
+    else if(!strcmp(argv[i], "-s")||!strcmp(argv[i], "--site"))
+    {
+      ++i;
+      sitestr=argv[i];
+    }
     else if(!strcmp(argv[i], "--cookies"))
     {
       ++i;
@@ -343,17 +195,22 @@ int main(int argc, char** argv)
     else if(!strcmp(argv[i], "--rtmplog")){++i; rtmplog=open(argv[i], O_WRONLY|O_CREAT|O_TRUNC, 0600); if(rtmplog<0){perror("rtmplog: open");}}
 #endif
     else if(!strcmp(argv[i], "--hexcolors")){hexcolors=1;}
-    else if(!strcmp(argv[i], "--greenroom")){sitearg="greenroom";}
+    else if(!strcmp(argv[i], "--greenroom")){greenroom=1;}
     else if(!channel){channel=argv[i];}
     else if(!nickname){nickname=strdup(argv[i]);}
     else if(!password){password=argv[i];}
   }
   // Check for required arguments
   if(!channel||!nickname){usage(argv[0]); return 1;}
-  char badchar;
-  if((badchar=checknick(nickname)))
+  if(sitestr &&
+     strcmp(sitestr, "tinychat") &&
+     strcmp(sitestr, "kageshi") &&
+     strcmp(sitestr, "kageshicam"))
   {
-    printf("'%c' is not allowed in nicknames.\n", badchar);
+    printf("Unknown site '%s'. Currently supported sites:\n"
+           "tinychat\n"
+           "kageshi\n"
+           "kageshicam (server/camname/numkey as channel)\n", sitestr);
     return 1;
   }
   setlocale(LC_ALL, "");
@@ -376,120 +233,45 @@ int main(int argc, char** argv)
       if(account_pass[i]=='\n'||account_pass[i]=='\r'){account_pass[i]=0; break;}
     }
   }
-  char loggedin=0;
-  // Log in if user account is specified
-  char* modkey=getmodkey(account_user, account_pass, channel, &loggedin);
-  char* server=gethost(channel, password);
-  struct addrinfo* res;
-  // Separate IP/domain and port
-  char* port=strchr(server, ':');
-  if(!port){return 3;}
-  port[0]=0;
-  ++port;
-  // Connect
-  getaddrinfo(server, port, 0, &res);
-  int sock=socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
-  if(connect(sock, res->ai_addr, res->ai_addrlen))
+
+  // Accept URL as "channel" and pick site accordingly (in addition to --site)
+  char* domain;
+  if((domain=strstr(channel, "://")))
   {
-    perror("Failed to connect");
-    freeaddrinfo(res);
-    return 1;
+    domain=&domain[3];
+    if(!strncmp(domain, "www.", 4)){domain=&domain[4];}
+    if(!strncmp(domain, "tinychat.com/", 13))
+    {
+      sitestr="tinychat";
+      channel=&domain[13];
+    }
+    else if(!strncmp(domain, "kageshi.com/rooms/", 18))
+    {
+      sitestr="kageshi";
+      channel=&domain[18];
+    }
   }
-  freeaddrinfo(res);
-  i=1;
-  setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &i, sizeof(i));
+  struct site site;
+  int sock;
+  if(!sitestr || !strcmp(sitestr, "tinychat"))
+  {
+    sock=init_tinychat(password, account_user, account_pass, &site);
+  }
+  else if(!strcmp(sitestr, "kageshi"))
+  {
+    sock=init_kageshi(password, account_user, account_pass, &site);
+  }
+  else if(!strcmp(sitestr, "kageshicam"))
+  {
+    sock=init_kageshicam(&site);
+  }
+  free(account_pass);
   int random=open("/dev/urandom", O_RDONLY);
-  // RTMP handshake
-  unsigned char handshake[1536];
-  read(random, handshake, 1536);
   if(currentcolor>=COLORCOUNT)
   {
     read(random, &currentcolor, sizeof(currentcolor));
   }
   close(random);
-  write(sock, "\x03", 1); // Send 0x03 and 1536 bytes of random junk
-  write(sock, handshake, 1536);
-  read(sock, handshake, 1); // Receive server's 0x03+junk
-  b_read(sock, handshake, 1536);
-  write(sock, handshake, 1536); // Send server's junk back
-  b_read(sock, handshake, 1536); // Read our junk back, we don't bother checking that it's the same
-  printf("Handshake complete\n");
-  struct rtmp rtmp={0,0,0,0,0};
-  if(!loggedin){free(account_pass); account_user=0; account_pass=0;}
-  getcaptcha();
-  char* cookie=getcookie(channel);
-  // Send connect request
-  struct rtmp amf;
-  amfinit(&amf, 3);
-  amfstring(&amf, "connect");
-  amfnum(&amf, 0);
-  amfobjstart(&amf);
-    amfobjitem(&amf, "app");
-    amfstring(&amf, "tinyconf");
-
-    amfobjitem(&amf, "flashVer");
-    amfstring(&amf, "no flash");
-
-    amfobjitem(&amf, "swfUrl");
-    amfstring(&amf, "no flash");
-
-    amfobjitem(&amf, "tcUrl");
-    char str[strlen("rtmp://:/tinyconf0")+strlen(server)+strlen(port)];
-    sprintf(str, "rtmp://%s:%s/tinyconf", server, port);
-    amfstring(&amf, str);
-
-    amfobjitem(&amf, "fpad");
-    amfbool(&amf, 0);
-
-    amfobjitem(&amf, "capabilities");
-    amfnum(&amf, 0);
-
-    amfobjitem(&amf, "audioCodecs");
-    amfnum(&amf, 0);
-
-    amfobjitem(&amf, "videoCodecs");
-    amfnum(&amf, 0);
-
-    amfobjitem(&amf, "videoFunction");
-    amfnum(&amf, 0);
-
-    amfobjitem(&amf, "pageUrl");
-    char pageurl[strlen("http://tinychat.com/0") + strlen(channel)];
-    sprintf(pageurl, "http://tinychat.com/%s", channel);
-    amfstring(&amf, pageurl);
-
-    amfobjitem(&amf, "objectEncoding");
-    amfnum(&amf, 0);
-  amfobjend(&amf);
-  amfobjstart(&amf);
-    amfobjitem(&amf, "version");
-    amfstring(&amf, "Desktop");
-
-    amfobjitem(&amf, "type");
-    amfstring(&amf, "default"); // This item is called roomtype in the same HTTP response that gives us the server (IP+port) to connect to, but "default" seems to work fine too.
-
-    amfobjitem(&amf, "account");
-    amfstring(&amf, account_user?account_user:"");
-
-    amfobjitem(&amf, "prefix");
-    amfstring(&amf, sitearg);
-
-    amfobjitem(&amf, "room");
-    amfstring(&amf, channel);
-
-    if(modkey)
-    {
-      amfobjitem(&amf, "autoop");
-      amfstring(&amf, modkey);
-    }
-    amfobjitem(&amf, "cookie");
-    amfstring(&amf, cookie);
-  amfobjend(&amf);
-  amfsend(&amf, sock);
-  free(modkey);
-  free(cookie);
-
-  char* unban=0;
   struct pollfd pfd[2];
   pfd[0].fd=0;
   pfd[0].events=POLLIN;
@@ -497,6 +279,7 @@ int main(int argc, char** argv)
   pfd[1].fd=sock;
   pfd[1].events=POLLIN;
   pfd[1].revents=0;
+  struct rtmp rtmp={0,0,0,0,0};
   while(1)
   {
     // Poll for input, very crude chat UI
@@ -517,7 +300,6 @@ int main(int argc, char** argv)
       while(len>0 && (buf[len-1]=='\n'||buf[len-1]=='\r')){--len;}
       if(!len){continue;} // Don't send empty lines
       buf[len]=0;
-      char* privfield=0;
       if(buf[0]=='/') // Got a command
       {
         if(!strcmp(buf, "/help"))
@@ -577,93 +359,47 @@ int main(int argc, char** argv)
         }
         else if(!strncmp(buf, "/nick ", 6))
         {
-          if((badchar=checknick(&buf[6])))
-          {
-            printf("'%c' is not allowed in nicknames.\n", badchar);
-            continue;
-          }
-          amfinit(&amf, 3);
-          amfstring(&amf, "nick");
-          amfnum(&amf, 0);
-          amfnull(&amf);
-          amfstring(&amf, &buf[6]);
-          amfsend(&amf, sock);
+          site.nick(sock, &buf[6]);
           continue;
         }
         else if(!strncmp(buf, "/msg ", 5))
         {
-          privfield=getprivfield(&buf[5]);
-          if(!privfield){continue;}
-        }
-        else if(!strncmp(buf, "/priv ", 6))
-        {
-          char* end=strchr(&buf[6], ' ');
-          if(!end){continue;}
-          privfield=getprivfield(&buf[6]);
-          if(!privfield){continue;}
-          len=strlen(&end[1]);
-          memmove(buf, &end[1], len+1);
+          char* msg=strchr(&buf[5], ' ');
+          if(msg)
+          {
+            msg[0]=0;
+            site.sendpm(sock, &msg[1], &buf[5]);
+            continue;
+          }
         }
         else if(!strncmp(buf, "/opencam ", 9))
         {
-          stream_start(&buf[9], sock);
+          site.opencam(sock, &buf[9]);
           continue;
         }
         else if(!strncmp(buf, "/closecam ", 10))
         {
-          stream_stopvideo(sock, idlist_get(&buf[10]));
+          site.closecam(sock, &buf[10]);
           continue;
         }
         else if(!strncmp(buf, "/close ", 7)) // Stop someone's cam/mic broadcast
         {
-          char nick[strlen(&buf[7])+1];
-          strcpy(nick, &buf[7]);
-          amfinit(&amf, 2);
-          amfstring(&amf, "owner_run");
-          amfnum(&amf, 0);
-          amfnull(&amf);
-          sprintf(buf, "_close%s", nick);
-          amfstring(&amf, buf);
-          amfsend(&amf, sock);
-          len=sprintf(buf, "closed: %s", nick);
+          site.mod_close(sock, &buf[7]);
+          continue;
         }
         else if(!strncmp(buf, "/ban ", 5)) // Ban someone
         {
-          char nick[strlen(&buf[5])+1];
-          strcpy(nick, &buf[5]);
-          amfinit(&amf, 3);
-          amfstring(&amf, "owner_run");
-          amfnum(&amf, 0);
-          amfnull(&amf);
-          sprintf(buf, "notice%s%%20was%%20banned%%20by%%20%s%%20(%s)", nick, nickname, account_user);
-          amfstring(&amf, buf);
-          amfsend(&amf, sock);
-          printf("%s %s was banned by %s (%s)\n", timestamp(), nick, nickname, account_user);
-          // kick (this does the actual banning)
-          amfinit(&amf, 3);
-          amfstring(&amf, "kick");
-          amfnum(&amf, 0);
-          amfnull(&amf);
-          amfstring(&amf, nick);
-          sprintf(buf, "%i", idlist_get(nick));
-          amfstring(&amf, buf);
-          amfsend(&amf, sock);
+          site.mod_ban(sock, &buf[5]);
           continue;
         }
-        else if(!strcmp(buf, "/banlist") || !strncmp(buf, "/forgive ", 9))
+        else if(!strcmp(buf, "/banlist"))
         {
-          free(unban);
-          if(buf[1]=='f') // forgive
-          {
-            unban=strdup(&buf[9]);
-          }else{
-            unban=0;
-          }
-          amfinit(&amf, 3);
-          amfstring(&amf, "banlist");
-          amfnum(&amf, 0);
-          amfnull(&amf);
-          amfsend(&amf, sock);
+          site.mod_banlist(sock);
+          continue;
+        }
+        else if(!strncmp(buf, "/forgive ", 9))
+        {
+          site.mod_unban(sock, &buf[9]);
           continue;
         }
         else if(!strcmp(buf, "/names"))
@@ -679,42 +415,22 @@ int main(int argc, char** argv)
         }
         else if(!strcmp(buf, "/mute"))
         {
-          amfinit(&amf, 3);
-          amfstring(&amf, "owner_run");
-          amfnum(&amf, 0);
-          amfnull(&amf);
-          amfstring(&amf, "mute");
-          amfsend(&amf, sock);
+          site.mod_mute(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);
+          site.mod_push2talk(sock);
           continue;
         }
         else if(!strcmp(buf, "/camup"))
         {
-          // Retrieve and send the key for broadcasting access
-          char* key=getbroadcastkey(channel, nickname, bpassword);
-          amfinit(&amf, 3);
-          amfstring(&amf, "bauth");
-          amfnum(&amf, 0);
-          amfnull(&amf);
-          amfstring(&amf, key);
-          amfsend(&amf, sock);
-          free(key);
-          // Initiate stream
-          streamout_start(idlist_get(nickname), sock);
+          site.camup(sock);
           continue;
         }
         else if(!strcmp(buf, "/camdown"))
         {
-          stream_stopvideo(sock, idlist_get(nickname));
+          site.camdown(sock);
           continue;
         }
         else if(!strncmp(buf, "/video ", 7)) // Send video data
@@ -735,13 +451,7 @@ int main(int argc, char** argv)
         }
         else if(!strncmp(buf, "/topic ", 7))
         {
-          amfinit(&amf, 3);
-          amfstring(&amf, "topic");
-          amfnum(&amf, 0);
-          amfnull(&amf);
-          amfstring(&amf, &buf[7]);
-          amfstring(&amf, "");
-          amfsend(&amf, sock);
+          site.mod_topic(sock, &buf[7]);
           continue;
         }
         else if(!strncmp(buf, "/whois ", 7)) // Get account username
@@ -763,10 +473,8 @@ int main(int argc, char** argv)
         }
         else if(!strncmp(buf, "/allow ", 7))
         {
-          if(!bpassword){continue;}
-          privfield=getprivfield(&buf[7]);
-          if(!privfield){continue;}
-          len=sprintf(buf, "/allowbroadcast %s", bpassword);
+          site.mod_allowbroadcast(sock, &buf[7]);
+          continue;
         }
         else if(!strncmp(buf, "/getnick ", 9))
         {
@@ -776,32 +484,7 @@ int main(int argc, char** argv)
           continue;
         }
       }
-      char* msg=tonumlist(buf, len);
-      amfinit(&amf, 3);
-      amfstring(&amf, "privmsg");
-      amfnum(&amf, 0);
-      amfnull(&amf);
-      amfstring(&amf, msg);
-      amfstring(&amf, colors[currentcolor%COLORCOUNT]);
-      // For PMs, add a string like "n<numeric ID>-<nick>" to make the server only send it to the intended recipient
-      if(privfield)
-      {
-        amfstring(&amf, privfield);
-        // And one in case they're broadcasting
-        privfield[0]='b'; // 'b' for broadcasting
-        struct rtmp bamf;
-        amfinit(&bamf, 3);
-        amfstring(&bamf, "privmsg");
-        amfnum(&bamf, 0);
-        amfnull(&bamf);
-        amfstring(&bamf, msg);
-        amfstring(&bamf, colors[currentcolor%COLORCOUNT]);
-        amfstring(&bamf, privfield);
-        amfsend(&bamf, sock);
-        free(privfield);
-      }
-      amfsend(&amf, sock);
-      free(msg);
+      site.sendmessage(sock, buf);
       continue;
     }
     // Got data from server
@@ -813,328 +496,16 @@ int main(int argc, char** argv)
     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); fflush(stdout); continue;}
     struct amf* amfin=amf_parse(rtmp.buf, rtmp.length);
-    if(amfin->itemcount>0 && amfin->items[0].type==AMF_STRING)
-    {
-//      printf("Got command: '%s'\n", amfin->items[0].string.string);
-      if(!strcmp(amfin->items[0].string.string, "_error"))
-        printamf(amfin);
-    }
-    // Items for privmsg: 0=cmd, 2=channel, 3=msg, 4=color/lang, 5=nick
-    if(amfin->itemcount>5 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "privmsg") && amfin->items[3].type==AMF_STRING && amfin->items[4].type==AMF_STRING && amfin->items[5].type==AMF_STRING)
-    {
-      size_t len;
-      char* msg=fromnumlist(amfin->items[3].string.string, &len);
-      const char* color=color_start(amfin->items[4].string.string);
-      char* line=msg;
-      while(line)
-      {
-        // Handle multi-line messages
-        char* nextline=0;
-        unsigned int linelen;
-        for(linelen=0; &line[linelen]<&msg[len]; ++linelen)
-        {
-          if(line[linelen]=='\r' || line[linelen]=='\n'){nextline=&line[linelen+1]; break;}
-        }
-        printf("%s %s%s: ", timestamp(), color, amfin->items[5].string.string);
-        fwrite(line, linelen, 1, stdout);
-        printf("%s\n", color_end());
-        line=nextline;
-      }
-      char* response=0;
-      if(!strncmp(msg, "/allowbroadcast ", 16))
-      {
-        if(getbroadcastkey(channel, nickname, &msg[16])) // Validate password
-        {
-          free((void*)bpassword);
-          bpassword=strdup(&msg[16]);
-          printf("Authenticated to broadcast\n");
-        }
-      }
-      else if(!strcmp(msg, "/version"))
-      {
-        response=tonumlist("/version tc_client-" VERSION, strlen(VERSION)+19);
-      }
-      if(response)
-      {
-        // Send our command reponse with a privacy field
-        amfinit(&amf, 3);
-        amfstring(&amf, "privmsg");
-        amfnum(&amf, 0);
-        amfnull(&amf);
-        amfstring(&amf, response);
-        amfstring(&amf, "#0,en");
-        int id=idlist_get(amfin->items[5].string.string);
-        char priv[snprintf(0, 0, "n%i-%s", id, amfin->items[5].string.string)+1];
-        sprintf(priv, "n%i-%s", id, amfin->items[5].string.string);
-        amfstring(&amf, priv);
-        amfsend(&amf, sock);
-        // And again in case they're broadcasting
-        amfinit(&amf, 3);
-        amfstring(&amf, "privmsg");
-        amfnum(&amf, 0);
-        amfnull(&amf);
-        amfstring(&amf, response);
-        amfstring(&amf, "#0,en");
-        priv[0]='b';
-        amfstring(&amf, priv);
-        amfsend(&amf, sock);
-        free(response);
-      }
-      free(msg);
-      fflush(stdout);
-    }
-    // users on channel entry.  there's also a "joinsdone" command for some reason...
-    else if(amfin->itemcount>2 && amfin->items[0].type==AMF_STRING && (amf_comparestrings_c(&amfin->items[0].string, "joins") || amf_comparestrings_c(&amfin->items[0].string, "join") || amf_comparestrings_c(&amfin->items[0].string, "registered")))
-    {
-      if(amf_comparestrings_c(&amfin->items[0].string, "joins"))
-      {
-        printf("Currently online: ");
-      }else{
-        printf("%s ", timestamp());
-      }
-      int i;
-      for(i = 2; i < amfin->itemcount; ++i)
-      {
-        if(amfin->items[i].type==AMF_OBJECT)
-        {
-          struct amfitem* item=amf_getobjmember(&amfin->items[i].object, "id");
-          if(item->type!=AMF_NUMBER){continue;}
-          int id=item->number;
-          item=amf_getobjmember(&amfin->items[i].object, "nick");
-          if(item->type!=AMF_STRING){continue;}
-          const char* nick=item->string.string;
-          item=amf_getobjmember(&amfin->items[i].object, "account");
-          if(item->type!=AMF_STRING){continue;}
-          const char* account=(item->string.string[0]?item->string.string:0);
-          item=amf_getobjmember(&amfin->items[i].object, "mod");
-          if(item->type!=AMF_BOOL){continue;}
-          char mod=item->boolean;
-          idlist_add(id, nick, account, mod);
-          printf("%s%s", (i==2?"":", "), nick);
-          if(amf_comparestrings_c(&amfin->items[0].string, "registered"))
-          {
-            // Tell the server how often to let us know it got our packets
-            struct rtmp setbw={
-              .type=RTMP_SERVER_BW,
-              .chunkid=2,
-              .length=4,
-              .msgid=0,
-              .buf="\x00\x00\x40\x00" // Every 0x4000 bytes
-            };
-            rtmp_send(sock, &setbw);
-            char* key=getkey(id, channel);
-            curl_easy_cleanup(curl); // At this point we should be done with HTTP requests
-            curl=0;
-            if(!key){printf("Failed to get channel key\n"); return 1;}
-            amfinit(&amf, 3);
-            amfstring(&amf, "cauth");
-            amfnum(&amf, 0);
-            amfnull(&amf); // Means nothing but is apparently critically important for cauth at least
-            amfstring(&amf, key);
-            amfsend(&amf, sock);
-            free(key);
-            // Set the given nickname
-            amfinit(&amf, 3);
-            amfstring(&amf, "nick");
-            amfnum(&amf, 0);
-            amfnull(&amf);
-            amfstring(&amf, nickname);
-            amfsend(&amf, sock);
-            // Keep what the server gave us, just in case
-            free(nickname);
-            nickname=strdup(nick);
-          }
-        }
-      }
-      if(amf_comparestrings_c(&amfin->items[0].string, "joins"))
-      {
-        printf("\n");
-        for(i=0; i<idlistlen; ++i)
-        {
-          if(idlist[i].op){printf("%s is a moderator.\n", idlist[i].name);}
-        }
-        printf("Currently on cam: ");
-        char first=1;
-        for(i = 2; i < amfin->itemcount; ++i)
-        {
-          if(amfin->items[i].type==AMF_OBJECT)
-          {
-            struct amfitem* item=amf_getobjmember(&amfin->items[i].object, "bf");
-            if(item->type!=AMF_BOOL || !item->boolean){continue;}
-            item=amf_getobjmember(&amfin->items[i].object, "nick");
-            if(item->type!=AMF_STRING){continue;}
-            printf("%s%s", (first?"":", "), item->string.string);
-            first=0;
-          }
-        }
-        printf("\n");
-      }else{
-        printf(" entered the channel\n");
-        if(idlist[idlistlen-1].op){printf("%s is a moderator.\n", idlist[idlistlen-1].name);}
-        if(amf_comparestrings_c(&amfin->items[0].string, "registered"))
-        {
-          printf("Connection ID: %i\n", idlist[0].id);
-        }
-      }
-      fflush(stdout);
-    }
-    // quit/part
-    else if(amfin->itemcount>2 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "quit") && amfin->items[2].type==AMF_STRING)
-    {
-      idlist_remove(amfin->items[2].string.string);
-      printf("%s %s left the channel\n", timestamp(), amfin->items[2].string.string);
-      fflush(stdout);
-    }
-    // nick
-    else if(amfin->itemcount==5 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "nick") && amfin->items[2].type==AMF_STRING && amfin->items[3].type==AMF_STRING)
+    for(i=0; i<commandcount; ++i)
     {
-      if(!strcmp(amfin->items[2].string.string, nickname)) // Successfully changed our own nickname
+      if(amfin->itemcount>=commands[i].minargs && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, commands[i].command))
       {
-        free(nickname);
-        nickname=strdup(amfin->items[3].string.string);
-      }
-      idlist_rename(amfin->items[2].string.string, amfin->items[3].string.string);
-      printf("%s %s changed nickname to %s\n", timestamp(), amfin->items[2].string.string, amfin->items[3].string.string);
-      fflush(stdout);
-    }
-    // kick
-    else if(amfin->itemcount==4 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "kick") && amfin->items[2].type==AMF_STRING)
-    {
-      if(atoi(amfin->items[2].string.string) == idlist_get(nickname))
-      {
-        printf("%s You have been kicked out\n", timestamp());
-        fflush(stdout);
-      }
-    }
-    // banned
-    else if(amfin->itemcount==2 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "banned"))
-    {
-      printf("%s You are banned from %s\n", timestamp(), channel);
-      fflush(stdout);
-      // When banned and reconnecting, tinychat doesn't disconnect us itself, we need to disconnect
-      close(sock);
-      return 1; // Getting banned is a failure, right?
-    }
-    // from_owner: notices, mute, push2talk, closing cams
-    else if(amfin->itemcount==3 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "from_owner") && amfin->items[2].type==AMF_STRING)
-    {
-      if(!strncmp("notice", amfin->items[2].string.string, 6))
-      {
-        char* notice=strdup(&amfin->items[2].string.string[6]);
-        // replace "%20" with spaces
-        char* space;
-        while((space=strstr(notice, "%20")))
-        {
-          memmove(space, &space[2], strlen(&space[2])+1);
-          space[0]=' ';
-        }
-        printf("%s %s\n", timestamp(), notice);
+        commands[i].callback(amfin, sock);
         fflush(stdout);
-      }
-      else if(!strcmp("mute", amfin->items[2].string.string))
-      {
-        printf("%s Non-moderators have been temporarily muted.\n", timestamp());
-        fflush(stdout);
-      }
-      else if(!strcmp("push2talk", amfin->items[2].string.string))
-      {
-        printf("%s Push to talk request has been sent to non-moderators.\n", timestamp());
-        fflush(stdout);
-      }
-      else if(!strncmp("_close", amfin->items[2].string.string, 6) && !strcmp(&amfin->items[2].string.string[6], nickname))
-      {
-        printf("Outgoing media stream was closed\n");
-        fflush(stdout);
-        stream_stopvideo(sock, idlist_get(nickname));
-      }
-    }
-    // nickinuse, the nick we wanted to change to is already taken
-    else if(amfin->itemcount>0 && amfin->items[0].type==AMF_STRING &&  amf_comparestrings_c(&amfin->items[0].string, "nickinuse"))
-    {
-      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);
-    }
-    // Get list of banned users
-    else if(amfin->itemcount>0 && amfin->items[0].type==AMF_STRING &&  amf_comparestrings_c(&amfin->items[0].string, "banlist"))
-    {
-      unsigned int i;
-      if(unban) // This is not a response to /banlist but to /forgive
-      {
-        for(i=2; i+1<amfin->itemcount; i+=2)
-        {
-          if(amfin->items[i].type!=AMF_STRING || amfin->items[i+1].type!=AMF_STRING){break;}
-          if(!strcmp(amfin->items[i+1].string.string, unban) || !strcmp(amfin->items[i].string.string, unban))
-          {
-            amfinit(&amf, 3);
-            amfstring(&amf, "forgive");
-            amfnum(&amf, 0);
-            amfnull(&amf);
-            amfstring(&amf, amfin->items[i].string.string);
-            amfsend(&amf, sock);
-          }
-          // If the nickname isn't found in the banlist we assume it's an ID
-        }
-        continue;
-      }
-      printf("Banned users:\n");
-      printf("ID         Nickname\n");
-      for(i=2; i+1<amfin->itemcount; i+=2)
-      {
-        if(amfin->items[i].type!=AMF_STRING || amfin->items[i+1].type!=AMF_STRING){break;}
-        unsigned int len=printf("%s", amfin->items[i].string.string);
-        for(;len<10; ++len){printf(" ");}
-        printf(" %s\n", amfin->items[i+1].string.string);
-      }
-      printf("Use /forgive <ID/nick> to unban someone\n");
-      fflush(stdout);
-    }
-    else if(amfin->itemcount>4 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "notice") && amfin->items[2].type==AMF_STRING && amf_comparestrings_c(&amfin->items[2].string, "avon"))
-    {
-      if(amfin->items[4].type==AMF_STRING)
-      {
-        printf("%s cammed up\n", amfin->items[4].string.string);
-        fflush(stdout);
-      }
-    }
-    // Handle results for various requests, haven't seen much of a pattern to it, always successful?
-    else if(amfin->itemcount>0 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "_result"))
-    {
-      // Creating a new stream worked, now play media (cam/mic) on it (if that's what the result was for)
-      stream_play(amfin, sock);
-    }
-    else if(amfin->itemcount>0 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "onStatus"))
-    {
-      stream_handlestatus(amfin, sock);
-    }
-    else if(amfin->itemcount>2 && amfin->items[0].type==AMF_STRING && amfin->items[2].type==AMF_OBJECT && amf_comparestrings_c(&amfin->items[0].string, "account"))
-    {
-      struct amfitem* id=amf_getobjmember(&amfin->items[2].object, "id");
-      struct amfitem* account=amf_getobjmember(&amfin->items[2].object, "account");
-      if(id && account && id->type==AMF_NUMBER && account->type==AMF_STRING)
-      {
-        for(i=0; i<idlistlen; ++i)
-        {
-          if(id->number==idlist[i].id)
-          {
-            if(strcmp(account->string.string, "$noinfo"))
-            {
-              printf("%s is logged in as %s\n", idlist[i].name, account->string.string);
-            }else{
-              printf("%s is not logged in\n", idlist[i].name);
-            }
-            fflush(stdout);
-            break;
-          }
-        }
+        break;
       }
     }
-    // else{printf("Unknown command...\n"); printamf(amfin);} // (Debugging)
+    // if(i==commandcount){printf("Unknown command...\n"); printamf(amfin);} // (Debugging)
     amf_free(amfin);
   }
   free(rtmp.buf);
diff --git a/client.h b/client.h
new file mode 100644
index 0000000..1c3f312
--- /dev/null
+++ b/client.h
@@ -0,0 +1,27 @@
+#include "amfparser.h"
+struct site
+{
+  void(*sendmessage)(int sock, const char* buf);
+  void(*sendpm)(int sock, const char* buf, const char* recipient);
+  void(*nick)(int sock, const char* newnick);
+  void(*mod_close)(int sock, const char* nick);
+  void(*mod_ban)(int sock, const char* nick);
+  void(*mod_banlist)(int sock);
+  void(*mod_unban)(int sock, const char* nick);
+  void(*mod_mute)(int sock);
+  void(*mod_push2talk)(int sock);
+  void(*mod_topic)(int sock, const char* newtopic);
+  void(*mod_allowbroadcast)(int sock, const char* nick);
+  void(*opencam)(int sock, const char* arg);
+  void(*closecam)(int sock, const char* arg);
+  void(*camup)(int sock);
+  void(*camdown)(int sock);
+};
+
+extern char greenroom;
+extern char* channel;
+extern char* nickname;
+extern char* http_get(const char* url, const char* post);
+extern char* timestamp();
+extern int connectto(const char* host, const char* port);
+extern void registercmd(const char* command, void(*callback)(struct amf*,int), unsigned int minargs);
diff --git a/colors.c b/colors.c
index 8b8a489..d29d3d4 100644
--- a/colors.c
+++ b/colors.c
@@ -65,7 +65,7 @@ const char* resolvecolor(const char* tc_color)
   int i;
   for(i=0; i<COLORCOUNT; ++i)
   {
-    if(!strcmp(colors[i], tc_color)){return termcolors[i];}
+    if(!strncmp(colors[i], tc_color, 7)){return termcolors[i];}
   }
   return "0";
 }
diff --git a/configure b/configure
index 2e5b8d3..e47dc2d 100755
--- a/configure
+++ b/configure
@@ -244,6 +244,11 @@ if ! testbuild 'prctl' 'prctl(PR_SET_PDEATHSIG, SIGHUP);return 0;' 'sys/prctl.h
   echo '#define NO_PRCTL 1' >> config.h
 fi
 
+if testpkgconfig glib-2.0 GLIB; then
+  echo 'LIBS+=$(GLIB_LIBS)' >> config.mk
+  echo 'CFLAGS+=$(GLIB_CFLAGS) -DHAVE_GLIB=1' >> config.mk
+fi
+
 printf 'Checking if we have a working poll()... '
 echo '#include <poll.h>' > polltest.c
 echo '#include <sys/socket.h>' >> polltest.c
@@ -259,11 +264,7 @@ if ./polltest 2> /dev/null; then
   echo yes
 else
   echo no
-  if testpkgconfig glib-2.0 GLIB; then
-    echo '#define POLL_BROKEN_OR_MISSING 1' >> config.h
-    echo 'LIBS+=$(GLIB_LIBS)' >> config.mk
-    echo 'CFLAGS+=$(GLIB_CFLAGS)' >> config.mk
-  fi
+  echo '#define POLL_BROKEN_OR_MISSING 1' >> config.h
 fi
 rm -f polltest.c polltest
 
diff --git a/kageshi.c b/kageshi.c
new file mode 100644
index 0000000..5e4b307
--- /dev/null
+++ b/kageshi.c
@@ -0,0 +1,469 @@
+/*
+    tc_client, a simple non-flash client for tinychat(.com)
+    Copyright (C) 2016  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 <stdio.h>
+#include <string.h>
+#ifdef HAVE_GLIB
+#include <glib.h>
+#endif
+#include "media.h"
+#include "amfwriter.h"
+#include "rtmp.h"
+#include "client.h"
+#include "idlist.h"
+#include "colors.h"
+
+static char* joinkey=0;
+static unsigned int joinkeynum;
+
+static void joindata(struct amf* amfin, int sock)
+{
+  if(amfin->items[6].type!=AMF_STRING || amfin->items[12].type!=AMF_NUMBER){return;}
+  joinkey=strdup(amfin->items[6].string.string);
+  joinkeynum=amfin->items[12].number;
+  struct rtmp amf;
+  amfinit(&amf, 3);
+  amfstring(&amf, "connectionOK");
+  amfnum(&amf, 0);
+  amfnull(&amf);
+  amfnum(&amf, joinkeynum);
+  amfstring(&amf, joinkey); // Channel ID or something?
+  amfstring(&amf, nickname);
+  amfstring(&amf, ""); // Unidentified number string, doesn't seem to matter at all
+  amfsend(&amf, sock);
+}
+
+static void joinuser(struct amf* amfin, int sock)
+{
+  if(amfin->items[2].type==AMF_STRING)
+  {
+    printf("%s %s entered the channel\n", timestamp(), amfin->items[2].string.string);
+  }
+}
+
+static void receivepublicmsg(struct amf* amfin, int sock)
+{
+  if(amfin->items[3].type!=AMF_STRING || amfin->items[4].type!=AMF_STRING || amfin->items[5].type!=AMF_STRING){return;}
+  if(!strcmp(amfin->items[3].string.string, nickname)){return;} // It's the message we just sent
+  const char* color=color_start(amfin->items[5].string.string);
+  printf("%s %s%s: %s%s\n", timestamp(), color, amfin->items[3].string.string, amfin->items[4].string.string, color_end());
+}
+
+static void removeuser(struct amf* amfin, int sock)
+{
+  if(amfin->items[2].type!=AMF_STRING){return;}
+  idlist_remove(amfin->items[2].string.string);
+  printf("%s %s left the channel\n", timestamp(), amfin->items[2].string.string);
+}
+
+static void senduserlist(struct amf* amfin, int sock)
+{
+  if(amfin->items[2].type!=AMF_STRING || amfin->items[2].string.string[1]!='"'){return;}
+  printf("Currently online: ");
+// "u" seems to be timestamp of status update
+// "l" seems to be timestamp of connection
+// "m" seems to be a status message (showed up as "BRB" once)
+  char* nick=&amfin->items[2].string.string[2];
+  char first=1;
+  while(nick)
+  {
+    if(first){first=0;}else{printf(", "); nick=&nick[3];}
+    char* end=strchr(nick, '"');
+    if(!end){break;}
+    end[0]=0;
+    printf("%s", nick);
+    idlist_add(0, nick, 0, 0);
+    nick=strstr(&end[1], "\",\"");
+  }
+  printf("\n");
+}
+
+/*
+Unknown command...
+amf: 0x1eca070
+amf->itemcount: 3
+String: 'camList'
+Number: 5.000000
+String: '{"1":"{\"camno\":1,\"server\":\"<IP>\",\"server_name\":\"<name>\",\"server_location\":\"<location>\",\"camname\":\"<camid>\",\"username\":\"<nickname/username>\"}","2":"{\"camno\":2,\"server\":\"<IP>\",\"server_name\":\"<name>\",\"server_location\":\"<location>\",\"camname\":\"<camid>\",\"username\":\"<nickname/username>\"}","3":etc...}'
+*/
+
+static void startcam(struct amf* amfin, int sock)
+{
+  if(amfin->items[3].type!=AMF_STRING || amfin->items[4].type!=AMF_STRING || amfin->items[5].type!=AMF_STRING){return;}
+  printf("%s cammed up on %s/%s/%u as %s\n", amfin->items[4].string.string, amfin->items[5].string.string, channel, joinkeynum, amfin->items[3].string.string);
+}
+
+static void stopcam(struct amf* amfin, int sock)
+{
+  if(amfin->items[3].type!=AMF_STRING || amfin->items[4].type!=AMF_STRING){return;}
+  printf("%s cammed down\n", amfin->items[4].string.string);
+// TODO: Send a VideoEnd notice?
+}
+
+static void pmreceive(struct amf* amfin, int sock)
+{
+  if(amfin->items[2].type!=AMF_STRING || amfin->items[3].type!=AMF_STRING || amfin->items[4].type!=AMF_STRING || amfin->items[5].type!=AMF_STRING){return;}
+  if(!strcmp(amfin->items[2].string.string, nickname)){return;} // It's the message we just sent
+  const char* color=color_start(amfin->items[5].string.string);
+  printf("%s %s%s: /msg %s %s%s\n", timestamp(), color, amfin->items[2].string.string, amfin->items[3].string.string, amfin->items[4].string.string, color_end());
+}
+
+static void typingpm(struct amf* amfin, int sock)
+{
+  if(amfin->items[2].type!=AMF_STRING){return;}
+  printf("%s is typing a PM\n", amfin->items[2].string.string);
+}
+
+static void kicked(struct amf* amfin, int sock)
+{
+  if(amfin->items[2].type!=AMF_STRING || amfin->items[3].type!=AMF_STRING || amfin->items[7].type!=AMF_STRING){return;}
+  printf("%s %s was kicked by %s (%s)\n", timestamp(), amfin->items[3].string.string, amfin->items[2].string.string, amfin->items[7].string.string);
+  idlist_remove(amfin->items[3].string.string);
+  printf("%s %s left the channel\n", timestamp(), amfin->items[3].string.string);
+}
+
+static void result2(struct amf* amfin, int sock)
+{
+  if(amfin->items[2].type!=AMF_OBJECT){return;}
+  struct amfitem* desc=amf_getobjmember(&amfin->items[2].object, "description");
+  if(desc->type!=AMF_STRING){return;}
+  if(!strcmp(desc->string.string, "{\"param\":\"\",\"reject\":\"0006\"}"))
+  {
+    printf("Wrong username/password\n");
+  }
+  else if(!strcmp(desc->string.string, "{\"param\":\"\",\"reject\":\"0008\"}"))
+  {
+    printf("Password required\n");
+  }
+  else if(!strcmp(desc->string.string, "{\"param\":\"\",\"reject\":\"0012\"}"))
+  {
+    printf("Account required\n");
+  }
+  else if(!strcmp(desc->string.string, "{\"param\":\"\",\"reject\":\"0003\"}"))
+  {
+    printf("Nickname unavailable\n");
+  }
+  else if(!strcmp(desc->string.string, "{\"param\":\"\",\"reject\":\"0005\"}"))
+  {
+    printf("Nickname too short\n");
+  }
+}
+
+static void sendmessage(int sock, const char* buf)
+{
+  char color[8];
+  memcpy(color, colors[currentcolor%COLORCOUNT], 7);
+  color[7]=0;
+  if(color[5]==',') // That one stupid tinychat color
+  {
+    strcpy(color, "#00a990");
+  }
+  struct rtmp amf;
+  amfinit(&amf, 3);
+  amfstring(&amf, "send_public");
+  amfnum(&amf, 0);
+  amfnull(&amf);
+  amfnum(&amf, joinkeynum);
+  amfstring(&amf, joinkey);
+  amfstring(&amf, nickname); // Spoofable?
+  amfstring(&amf, buf);
+  amfstring(&amf, "0");
+  amfstring(&amf, color);
+  amfstring(&amf, "1");
+  amfnum(&amf, 1);
+  amfsend(&amf, sock);
+}
+
+/* Not used yet
+static void sendpmtyping(int sock, const char* nick)
+{
+  struct rtmp amf;
+  amfinit(&amf, 3);
+  amfstring(&amf, "typing_pm");
+  amfnum(&amf, 0);
+  amfnull(&amf);
+  amfnum(&amf, joinkeynum);
+  amfstring(&amf, joinkey);
+  amfstring(&amf, nickname); // Spoofable?
+  amfstring(&amf, nick);
+  amfstring(&amf, "1"); // 0 for not typing anymore? (or 2? from observations)
+  amfsend(&amf, sock);
+}
+*/
+
+static void sendpm(int sock, const char* buf, const char* nick)
+{
+  char color[8];
+  memcpy(color, colors[currentcolor%COLORCOUNT], 7);
+  color[7]=0;
+  if(color[5]==',') // That one stupid tinychat color
+  {
+    strcpy(color, "#00a990");
+  }
+  struct rtmp amf;
+  amfinit(&amf, 3);
+  amfstring(&amf, "pm");
+  amfnum(&amf, 0);
+  amfnull(&amf);
+  amfnum(&amf, joinkeynum);
+  amfstring(&amf, joinkey);
+  amfstring(&amf, nickname);
+  amfstring(&amf, nick);
+  amfstring(&amf, buf);
+  amfstring(&amf, color);
+  amfstring(&amf, "1");
+  amfnum(&amf, 1);
+  amfsend(&amf, sock);
+}
+
+static void notimplemented()
+{
+  printf("The requested feature is either not supported by kageshi or not yet implemented in tc_client's kageshi support\n");
+}
+
+int init_kageshi(const char* chanpass, const char* username, const char* userpass, struct site* site)
+{
+#ifndef HAVE_GLIB
+  if(username){printf("Account support requires glib (for md5sums)\n");}
+  username=0;
+#endif
+  registercmd("joinData", joindata, 13);
+  registercmd("joinuser", joinuser, 3);
+  registercmd("receivePublicMsg", receivepublicmsg, 6);
+  registercmd("removeuser", removeuser, 3);
+  registercmd("sendUserList", senduserlist, 3);
+  registercmd("startCam", startcam, 6);
+  registercmd("stopCam", stopcam, 5);
+  registercmd("pmReceive", pmreceive, 6);
+  registercmd("typingPM", typingpm, 3);
+  registercmd("kicked", kicked, 8);
+  registercmd("_result", result2, 3);
+  site->sendmessage=sendmessage;
+  site->sendpm=sendpm;
+  site->nick=notimplemented;
+  site->mod_close=notimplemented;
+  site->mod_ban=notimplemented;
+  site->mod_banlist=notimplemented;
+  site->mod_unban=notimplemented;
+  site->mod_mute=notimplemented;
+  site->mod_push2talk=notimplemented;
+  site->mod_topic=notimplemented;
+  site->mod_allowbroadcast=notimplemented;
+  site->opencam=notimplemented;
+  site->closecam=notimplemented;
+  site->camup=notimplemented;
+  site->camdown=notimplemented;
+  int sock=connectto("107.191.96.85", "1935");
+  rtmp_handshake(sock);
+  // Send connect request
+  struct rtmp amf;
+  amfinit(&amf, 3);
+  amfstring(&amf, "connect");
+  amfnum(&amf, username?1:0);
+  amfobjstart(&amf);
+    amfobjitem(&amf, "app");
+    {char str[strlen("chat/0")+strlen(channel)];
+    sprintf(str, "chat/%s", channel);
+    amfstring(&amf, str);}
+
+    amfobjitem(&amf, "flashVer");
+    amfstring(&amf, "no flash");
+
+    amfobjitem(&amf, "swfUrl");
+    amfstring(&amf, "no flash");
+
+    amfobjitem(&amf, "tcUrl");
+    {char str[strlen("rtmp://107.191.96.85:1935/chat/0")+strlen(channel)];
+    sprintf(str, "rtmp://107.191.96.85:1935/chat/%s", channel);
+    amfstring(&amf, str);}
+
+    amfobjitem(&amf, "fpad");
+    amfbool(&amf, 0);
+
+    amfobjitem(&amf, "capabilities");
+    amfnum(&amf, 0);
+
+    amfobjitem(&amf, "audioCodecs");
+    amfnum(&amf, 0);
+
+    amfobjitem(&amf, "videoCodecs");
+    amfnum(&amf, 0);
+
+    amfobjitem(&amf, "videoFunction");
+    amfnum(&amf, 0);
+
+    amfobjitem(&amf, "pageUrl");
+    char pageurl[strlen("https://kageshi.com/rooms/0") + strlen(channel)];
+    sprintf(pageurl, "https://kageshi.com/rooms/%s", channel);
+    amfstring(&amf, pageurl);
+
+    amfobjitem(&amf, "objectEncoding");
+    amfnum(&amf, 0);
+  amfobjend(&amf);
+  amfstring(&amf, "connect");
+  amfstring(&amf, "");
+  amfstring(&amf, username?"":nickname);
+  if(username)
+  {
+  #ifdef HAVE_GLIB
+    GChecksum* md5=g_checksum_new(G_CHECKSUM_MD5);
+    g_checksum_update(md5, (void*)userpass, strlen(userpass));
+    amfstring(&amf, username);
+    amfstring(&amf, g_checksum_get_string(md5));
+    g_checksum_free(md5);
+  #endif
+  }else{
+    amfstring(&amf, "");
+    amfstring(&amf, "");
+  }
+  amfstring(&amf, channel);
+  // Not sure what these are, they don't seem to do anything, but need to be in the right format. kinda looks like HMAC
+  amfstring(&amf, "00000000-0000-0000-0000-000000000000-00000000-0000-0000-0000-000000000000");
+  amfstring(&amf, "00000000-0000-0000-0000-000000000000-00000000-0000-0000-0000-000000000000");
+  amfstring(&amf, "0.31");
+  amfstring(&amf, chanpass?chanpass:"");
+  amfstring(&amf, "");
+  amfbool(&amf, !!chanpass);
+  amfsend(&amf, sock);
+  return sock;
+}
+
+static void result(struct amf* amfin, int sock)
+{
+  // Handle results for various requests, haven't seen much of a pattern to it, always successful?
+  // Creating a new stream worked, now play media (cam/mic) on it (if that's what the result was for)
+  stream_play(amfin, sock);
+  // Tell them to actually send us media
+  unsigned int i;
+  for(i=0; i<streamcount; ++i)
+  {
+    if(streams[i].streamid==amfin->items[2].number)
+    {
+      struct rtmp amf;
+      amfinit(&amf, streams[i].streamid*6+2);
+      amfstring(&amf, "receiveVideo");
+      amfnum(&amf, 0);
+      amfnull(&amf);
+      amfbool(&amf, 1);
+      amfsend(&amf, sock);
+      amfinit(&amf, streams[i].streamid*6+2);
+      amfstring(&amf, "receiveAudio");
+      amfnum(&amf, 0);
+      amfnull(&amf);
+      amfbool(&amf, 1);
+      amfsend(&amf, sock);
+    }
+  }
+}
+
+static void opencam(int sock, const char* camname)
+{
+  stream_start(camname, camname, sock);
+}
+
+static void closecam(int sock, const char* camname)
+{
+  stream_stopvideo(sock, camname);
+}
+
+static void notimplemented2()
+{
+  printf("The requested feature is either not supported by kageshi cam sessions or not yet implemented in tc_client's kageshi cam support\n");
+}
+
+int init_kageshicam(struct site* site)
+{
+  // Separate server from channel name
+  char server[strlen(channel)+1];
+  strcpy(server, channel);
+  char* channelname=strchr(server, '/');
+  if(!channelname){return -1;} // TODO: Useful error
+  channelname[0]=0;
+  channelname=&channelname[1];
+  // Separate the channel name from the numeric key
+  char* key=strchr(channelname, '/');
+  if(!key){return -1;} // TODO: Useful error
+  key[0]=0;
+  joinkeynum=strtoul(&key[1], 0, 0);
+
+  registercmd("_result", result, 3);
+  site->opencam=opencam;
+  site->closecam=closecam;
+  site->sendmessage=notimplemented2;
+  site->sendpm=notimplemented2;
+  site->nick=notimplemented2;
+  site->mod_close=notimplemented2;
+  site->mod_ban=notimplemented2;
+  site->mod_banlist=notimplemented2;
+  site->mod_unban=notimplemented2;
+  site->mod_mute=notimplemented2;
+  site->mod_push2talk=notimplemented2;
+  site->mod_topic=notimplemented2;
+  site->mod_allowbroadcast=notimplemented2;
+  site->camup=notimplemented2;
+  site->camdown=notimplemented2;
+  int sock=connectto(server, "1935");
+  rtmp_handshake(sock);
+  // Send connect request
+  struct rtmp amf;
+  amfinit(&amf, 3);
+  amfstring(&amf, "connect");
+  amfnum(&amf, 0);
+  amfobjstart(&amf);
+    amfobjitem(&amf, "app");
+    {char str[snprintf(0,0,"CamServer/%u", joinkeynum)+1];
+    sprintf(str, "CamServer/%u", joinkeynum);
+    amfstring(&amf, str);}
+
+    amfobjitem(&amf, "flashVer");
+    amfstring(&amf, "no flash");
+
+    amfobjitem(&amf, "swfUrl");
+    amfstring(&amf, "no flash");
+
+    amfobjitem(&amf, "tcUrl");
+    {char str[snprintf(0,0,"rtmp://%s:1935/CamServer/%u", server, joinkeynum)+1];
+    sprintf(str, "rtmp://%s:1935/CamServer/%u", server, joinkeynum);
+    amfstring(&amf, str);}
+
+    amfobjitem(&amf, "fpad");
+    amfbool(&amf, 0);
+
+    amfobjitem(&amf, "capabilities");
+    amfnum(&amf, 0);
+
+    amfobjitem(&amf, "audioCodecs");
+    amfnum(&amf, 0);
+
+    amfobjitem(&amf, "videoCodecs");
+    amfnum(&amf, 0);
+
+    amfobjitem(&amf, "videoFunction");
+    amfnum(&amf, 0);
+
+    amfobjitem(&amf, "pageUrl");
+    char pageurl[strlen("http://kageshi.com/rooms/0") + strlen(channelname)];
+    sprintf(pageurl, "http://kageshi.com/rooms/%s", channelname);
+    amfstring(&amf, pageurl);
+
+    amfobjitem(&amf, "objectEncoding");
+    amfnum(&amf, 0);
+  amfobjend(&amf);
+  amfstring(&amf, "connect");
+  amfstring(&amf, "0.1");
+  amfsend(&amf, sock);
+  return sock;
+}
diff --git a/media.c b/media.c
index 0776545..255dc2d 100644
--- a/media.c
+++ b/media.c
@@ -36,14 +36,13 @@ char stream_idtaken(unsigned int id)
   return 0;
 }
 
-void stream_start(const char* nick, int sock) // called upon privmsg "/opencam ..."
+void stream_start(const char* nick, const char* camid, int sock) // called upon privmsg "/opencam ..."
 {
-  unsigned int userid=idlist_get(nick);
   unsigned int streamid=1;
   while(stream_idtaken(streamid)){++streamid;}
   ++streamcount;
   streams=realloc(streams, sizeof(struct stream)*streamcount);
-  streams[streamcount-1].userid=userid;
+  streams[streamcount-1].camid=strdup(camid);
   streams[streamcount-1].streamid=streamid;
   streams[streamcount-1].outgoing=0;
   struct rtmp amf;
@@ -52,17 +51,17 @@ void stream_start(const char* nick, int sock) // called upon privmsg "/opencam .
   amfnum(&amf, streamid+1);
   amfnull(&amf);
   amfsend(&amf, sock);
-  printf("Starting media stream for %s (%u)\n", nick, userid);
+  printf("Starting media stream for %s (%s)\n", nick, camid);
   fflush(stdout);
 }
 
-void streamout_start(unsigned int id, int sock) // called upon privmsg "/camup"
+void streamout_start(const char* id, int sock) // called upon privmsg "/camup"
 {
   unsigned int streamid=1;
   while(stream_idtaken(streamid)){++streamid;}
   ++streamcount;
   streams=realloc(streams, sizeof(struct stream)*streamcount);
-  streams[streamcount-1].userid=id;
+  streams[streamcount-1].camid=id;
   streams[streamcount-1].streamid=streamid;
   streams[streamcount-1].outgoing=1;
   struct rtmp amf;
@@ -83,13 +82,11 @@ void stream_play(struct amf* amf, int sock) // called upon _result
     if(streams[i].streamid==amf->items[2].number)
     {
       struct rtmp amf;
-      amfinit(&amf, 8);
+      amfinit(&amf, streams[i].streamid*6+2);
       amfstring(&amf, streams[i].outgoing?"publish":"play");
       amfnum(&amf, 0);
       amfnull(&amf);
-      char camid[snprintf(0,0,"%u0", streams[i].userid)];
-      sprintf(camid, "%u", streams[i].userid);
-      amfstring(&amf, camid);
+      amfstring(&amf, streams[i].camid);
       if(streams[i].outgoing){amfstring(&amf, "live");}
       amf.msgid=le32(streams[i].streamid);
       amfsend(&amf, sock);
@@ -118,9 +115,9 @@ void stream_handledata(struct rtmp* rtmp)
 // fprintf(stderr, "Chunk: chunkid: %u, streamid: %u, userid: %u\n", rtmp->chunkid, rtmp->msgid, streams[i].userid);
     if(rtmp->type==RTMP_VIDEO)
     {
-      printf("Video: %u %u\n", streams[i].userid, rtmp->length);
+      printf("Video: %s %u\n", streams[i].camid, rtmp->length);
     }else if(rtmp->type==RTMP_AUDIO){
-      printf("Audio: %u %u\n", streams[i].userid, rtmp->length);
+      printf("Audio: %s %u\n", streams[i].camid, rtmp->length);
     }
     fwrite(rtmp->buf, rtmp->length, 1, stdout);
     fflush(stdout);
@@ -140,9 +137,8 @@ void stream_handlestatus(struct amf* amf, int sock)
   if(code->type!=AMF_STRING || details->type!=AMF_STRING){return;}
   if(!strcmp(code->string.string, "NetStream.Play.Stop"))
   {
-    unsigned int id=strtoul(details->string.string, 0, 0);
-    printf("VideoEnd: %u\n", id);
-    stream_stopvideo(sock, id);
+    printf("VideoEnd: %s\n", details->string.string);
+    stream_stopvideo(sock, details->string.string);
   }
 }
 
@@ -165,12 +161,12 @@ void stream_sendframe(int sock, void* buf, size_t len, unsigned char type)
   }
 }
 
-void stream_stopvideo(int sock, unsigned int id)
+void stream_stopvideo(int sock, const char* id)
 {
   unsigned int i;
   for(i=0; i<streamcount; ++i)
   {
-    if(streams[i].userid==id)
+    if(!strcmp(streams[i].camid, id))
     {
       struct rtmp amf;
       // Close the stream
@@ -187,6 +183,7 @@ void stream_stopvideo(int sock, unsigned int id)
       amfnull(&amf);
       amfnum(&amf, streams[i].streamid);
       amfsend(&amf, sock);
+      free((void*)streams[i].camid);
       // Remove from list of streams
       --streamcount;
       memmove(&streams[i], &streams[i+1], sizeof(struct stream)*(streamcount-i));
diff --git a/media.h b/media.h
index 329cb21..18c4f67 100644
--- a/media.h
+++ b/media.h
@@ -19,18 +19,18 @@
 struct stream
 {
   unsigned int streamid;
-  unsigned int userid;
+  const char* camid;
   char outgoing;
 };
 
 extern struct stream* streams;
 extern unsigned int streamcount;
 
-extern void stream_start(const char* nick, int sock); // called upon privmsg "/opencam ..."
-extern void streamout_start(unsigned int id, int sock); // called upon privmsg "/camup"
+extern void stream_start(const char* nick, const char* camid, int sock); // called upon privmsg "/opencam ..."
+extern void streamout_start(const char* id, int sock); // called upon privmsg "/camup"
 extern void stream_play(struct amf* amf, int sock); // called upon _result
 extern void stream_handledata(struct rtmp* rtmp);
 extern void stream_handlestatus(struct amf* amf, int sock);
 extern void stream_sendframe(int sock, void* buf, size_t len, unsigned char type);
-extern void stream_stopvideo(int sock, unsigned int id);
+extern void stream_stopvideo(int sock, const char* id);
 extern void setallowsnapshots(int sock, char v);
diff --git a/testbuilds.sh b/testbuilds.sh
index 925f9a6..b6d473d 100755
--- a/testbuilds.sh
+++ b/testbuilds.sh
@@ -4,7 +4,7 @@ rm -rf testbuilds
 mkdir -p testbuilds
 cd testbuilds
 
-printf "Without mic support, with gtk+-2.x, without RTMP_DEBUG, without youtube support: "
+printf "Without mic support, with gtk+-2.x, without RTMP_DEBUG, without youtube support, with glib: "
 res="broken"
 while true; do
   ../configure > /dev/null 2> /dev/null || break
@@ -14,6 +14,7 @@ while true; do
   if ! grep -q 'GTK_LIBS=.*-lgtk-[^- ]*-2' config.mk; then res="gtk+-2.x not found, can't test"; break; fi
   if ! grep -q '^AVCODEC_LIBS' config.mk; then res="libavcodec not found, can't test"; break; fi
   if ! grep -q '^SWSCALE_LIBS' config.mk; then res="libswscale not found, can't test"; break; fi
+  if ! grep -q '^GLIB_LIBS' config.mk; then res="glib not found, can't test"; break; fi
   sed -i -e '/^AO_LIBS/d' config.mk
   sed -i -e '/^AVFORMAT_LIBS/d' config.mk
   echo 'CFLAGS+=-Werror' >> config.mk
@@ -31,7 +32,7 @@ mv camviewer camviewer.gtk2 > /dev/null 2> /dev/null
 mv tc_client-gtk tc_client-gtk.gtk2 > /dev/null 2> /dev/null
 
 make clean > /dev/null 2> /dev/null
-printf "With mic support, with gtk+-3.x, with RTMP_DEBUG, with youtube support: "
+printf "With mic support, with gtk+-3.x, with RTMP_DEBUG, with youtube support, without glib: "
 res="broken"
 while true; do
   ../configure > /dev/null 2> /dev/null || break
@@ -41,6 +42,7 @@ while true; do
   if ! grep -q '^AVCODEC_LIBS' config.mk; then res="libavcodec not found, can't test"; break; fi
   if ! grep -q '^SWSCALE_LIBS' config.mk; then res="libswscale not found, can't test"; break; fi
   if ! grep -q '^AVFORMAT_LIBS' config.mk; then res="libavformat not found, can't test"; break; fi
+  sed -i -e '/GLIB_/d' config.mk
   echo 'CFLAGS+=-DRTMP_DEBUG=1 -Werror' >> config.mk
   if ! make utils > /dev/null 2> /dev/null; then
     sed -i -e 's/-Werror[^ ]*//g' config.mk
diff --git a/tinychat.c b/tinychat.c
new file mode 100644
index 0000000..7ea0f29
--- /dev/null
+++ b/tinychat.c
@@ -0,0 +1,806 @@
+/*
+    tc_client, a simple non-flash client for tinychat(.com)
+    Copyright (C) 2014-2016  alicia@ion.nu
+    Copyright (C) 2014-2015  Jade Lea
+
+    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 <unistd.h>
+#include <string.h>
+#include <stdio.h>
+#include <fcntl.h>
+#include <time.h>
+#include "client.h"
+#include "idlist.h"
+#include "rtmp.h"
+#include "colors.h"
+#include "amfwriter.h"
+#include "numlist.h"
+#include "media.h"
+#define sitearg (greenroom?"greenroom":"tinychat")
+static const char* bpassword=0;
+static char* unban=0;
+
+static char* gethost(const char *channel, const char *password)
+{
+  int urllen;
+  if(password)
+  {
+    urllen=strlen("http://tinychat.com/api/find.room/?site=&password=0")+strlen(channel)+strlen(sitearg)+strlen(password);
+  }else{
+    urllen=strlen("http://tinychat.com/api/find.room/?site=0")+strlen(channel)+strlen(sitearg);
+  }
+  char url[urllen];
+  if(password)
+  {
+    sprintf(url, "http://tinychat.com/api/find.room/%s?site=%s&password=%s", channel, sitearg, password);
+  }else{
+    sprintf(url, "http://tinychat.com/api/find.room/%s?site=%s", channel, sitearg);
+  }
+  char* response=http_get(url, 0);
+  if(!response){exit(-1);}
+  //response contains result='(OK|RES)|PW' (latter means a password is required)
+  char* result=strstr(response, "result='");
+  if(!result){printf("No result\n"); exit(-1);}
+  char* bpass=strstr(response, " bpassword='");
+  result+=strlen("result='");
+  // Handle the result value
+  if(!strncmp(result, "PW", 2)){printf("Password required\n"); exit(-1);}
+  if(strncmp(result, "OK", 2) && strncmp(result, "RES", 3)){printf("Result not OK\n"); exit(-1);}
+  // Find and extract the server responsible for this channel
+  char* rtmp=strstr(response, "rtmp='rtmp://");
+  if(!rtmp){printf("No rtmp found.\n"); exit(-1);}
+  rtmp+=strlen("rtmp='rtmp://");
+  int len;
+  for(len=0; rtmp[len] && rtmp[len]!='/'; ++len);
+  char* host=strndup(rtmp, len);
+  // Check if this is a greenroom channel
+  if(strstr(response, "greenroom=\"1\""))
+  {
+    printf("Channel has greenroom\n");
+  }
+  char* end;
+  if(bpass && (end=strchr(&bpass[12], '\'')))
+  {
+    end[0]=0;
+    bpassword=strdup(&bpass[12]);
+    printf("Authenticated to broadcast\n");
+  }
+  free(response);
+  return host;
+}
+
+static char* getkey(int id, const char* channel)
+{
+  char url[snprintf(0,0, "http://tinychat.com/api/captcha/check.php?guest%%5Fid=%i&room=%s%%5E%s", id, sitearg, channel)+1];
+  sprintf(url, "http://tinychat.com/api/captcha/check.php?guest%%5Fid=%i&room=%s%%5E%s", id, sitearg, channel);
+  char* response=http_get(url, 0);
+  char* key=strstr(response, "\"key\":\"");
+
+  if(!key){return 0;}
+  key+=7;
+  char* keyend=strchr(key, '"');
+  if(!keyend){return 0;}
+  char* backslash;
+  while((backslash=strchr(key, '\\')))
+  {
+    memmove(backslash, &backslash[1], strlen(backslash));
+    --keyend;
+  }
+  key=strndup(key, keyend-key);
+  free(response);
+  return key;
+}
+
+static void getcaptcha(void)
+{
+  char* url="http://tinychat.com/cauth/captcha";
+  char* page=http_get(url, 0);
+  char* token=strstr(page, "\"token\":\"");
+  if(token)
+  {
+    token=&token[9];
+    char* end=strchr(token, '"');
+    if(end)
+    {
+      end[0]=0;
+      printf("Captcha: http://tinychat.com/cauth/recaptcha?token=%s\n", token);
+      fflush(stdout);
+      fgetc(stdin);
+    }
+  }
+  free(page);
+}
+
+static char* getcookie(const char* channel)
+{
+  unsigned long long now=time(0);
+  char url[strlen("http://tinychat.com/cauth?t=&room=0")+snprintf(0,0, "%llu", now)+strlen(channel)];
+  sprintf(url, "http://tinychat.com/cauth?t=%llu&room=%s", now, channel);
+  char* response=http_get(url, 0);
+  if(strstr(response, "\"lurker\":1"))
+  {
+    printf("Captcha not completed, you will not be able to send any messages or broadcast\n");
+  }
+  char* cookie=strstr(response, "\"cookie\":\"");
+
+  if(!cookie){return 0;}
+  cookie+=10;
+  char* end=strchr(cookie, '"');
+  if(!end){return 0;}
+  cookie=strndup(cookie, end-cookie);
+  free(response);
+  return cookie;
+}
+
+static char* getbroadcastkey(const char* channel, const char* nick, const char* bpassword)
+{
+  unsigned int id=idlist_get(nick);
+  char url[snprintf(0,0, "http://tinychat.com/api/broadcast.pw?name=%s&site=%s&nick=%s&id=%u%s%s", channel, sitearg, nick, id, bpassword?"&password=":"", bpassword?bpassword:"")+1];
+  sprintf(url, "http://tinychat.com/api/broadcast.pw?name=%s&site=%s&nick=%s&id=%u%s%s", channel, sitearg, nick, id, bpassword?"&password=":"", bpassword?bpassword:"");
+  char* response=http_get(url, 0);
+  if(strstr(response, " result='PW'")){free(response); return 0;}
+  char* key=strstr(response, " token='");
+
+  if(!key){free(response); return 0;}
+  key+=8;
+  char* keyend=strchr(key, '\'');
+  if(!keyend){free(response); return 0;}
+  key=strndup(key, keyend-key);
+  free(response);
+  return key;
+}
+
+static char* getmodkey(const char* user, const char* pass, const char* channel, char* loggedin)
+{
+  // TODO: if possible, do this in a neater way than digging the key out from an HTML page.
+  if(!user||!pass){return 0;}
+  char post[strlen("form_sent=1&username=&password=&next=http://tinychat.com/0")+strlen(user)+strlen(pass)+strlen(channel)];
+  sprintf(post, "form_sent=1&username=%s&password=%s&next=http://tinychat.com/%s", user, pass, channel);
+  char* response=http_get("http://tinychat.com/login", post);
+  char* key=strstr(response, "autoop: \"");
+  if(key)
+  {
+    key=&key[9];
+    char* end=strchr(key, '"');
+    if(end)
+    {
+      end[0]=0;
+      key=strdup(key);
+    }else{key=0;}
+  }
+  if(strstr(response, "<div class='name'")){*loggedin=1;}
+  free(response);
+  return key;
+}
+
+static char checknick(const char* nick) // Returns zero if the nick is valid, otherwise returning the character that failed the check
+{
+  unsigned int i;
+  for(i=0; nick[i]; ++i)
+  {
+    if(!strchr("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-^[]{}`\\|", nick[i])){return nick[i];}
+  }
+  return 0;
+}
+
+static void sendmessage_priv(int sock, const char* buf, const char* priv)
+{
+  int id=priv?idlist_get(priv):0;
+  char privfield[snprintf(0, 0, "n%i-%s", id, priv?priv:"")+1];
+  sprintf(privfield, "n%i-%s", id, priv?priv:"");
+
+  struct rtmp amf;
+  char* msg=tonumlist((char*)buf, strlen(buf));
+  amfinit(&amf, 3);
+  amfstring(&amf, "privmsg");
+  amfnum(&amf, 0);
+  amfnull(&amf);
+  amfstring(&amf, msg);
+  amfstring(&amf, colors[currentcolor%COLORCOUNT]);
+  // For PMs, add a string like "n<numeric ID>-<nick>" to make the server only send it to the intended recipient
+  if(priv)
+  {
+    amfstring(&amf, privfield);
+    // And one in case they're broadcasting
+    privfield[0]='b'; // 'b' for broadcasting
+    struct rtmp bamf;
+    amfinit(&bamf, 3);
+    amfstring(&bamf, "privmsg");
+    amfnum(&bamf, 0);
+    amfnull(&bamf);
+    amfstring(&bamf, msg);
+    amfstring(&bamf, colors[currentcolor%COLORCOUNT]);
+    amfstring(&bamf, privfield);
+    amfsend(&bamf, sock);
+  }
+  amfsend(&amf, sock);
+  free(msg);
+}
+
+static void sendmessage(int sock, const char* buf)
+{
+  if(!strncmp(buf, "/priv ", 6))
+  {
+    const char* msg=strchr(&buf[6], ' ');
+    if(msg)
+    {
+      char priv[msg-&buf[6]+1];
+      memcpy(priv, &buf[6], msg-&buf[6]);
+      priv[msg-&buf[6]]=0;
+      sendmessage_priv(sock, &msg[1], priv);
+      return;
+    }
+  }
+  sendmessage_priv(sock, buf, 0);
+}
+
+static void sendpm(int sock, const char* buf, const char* recipient)
+{
+  char msg[strlen("/msg  0")+strlen(recipient)+strlen(buf)];
+  sprintf(msg, "/msg %s %s", recipient, buf);
+  sendmessage_priv(sock, msg, recipient);
+}
+
+static void changenick(int sock, const char* newnick)
+{
+  char badchar;
+  if((badchar=checknick(newnick)))
+  {
+    printf("'%c' is not allowed in nicknames.\n", badchar);
+    return;
+  }
+  struct rtmp amf;
+  amfinit(&amf, 3);
+  amfstring(&amf, "nick");
+  amfnum(&amf, 0);
+  amfnull(&amf);
+  amfstring(&amf, newnick);
+  amfsend(&amf, sock);
+}
+
+static void joinreg(struct amf* amfin, int sock)
+{
+  if(amf_comparestrings_c(&amfin->items[0].string, "joins"))
+  {
+    printf("Currently online: ");
+  }else{
+    printf("%s ", timestamp());
+  }
+  int i;
+  for(i = 2; i < amfin->itemcount; ++i)
+  {
+    if(amfin->items[i].type==AMF_OBJECT)
+    {
+      struct amfitem* item=amf_getobjmember(&amfin->items[i].object, "id");
+      if(item->type!=AMF_NUMBER){continue;}
+      int id=item->number;
+      item=amf_getobjmember(&amfin->items[i].object, "nick");
+      if(item->type!=AMF_STRING){continue;}
+      const char* nick=item->string.string;
+      item=amf_getobjmember(&amfin->items[i].object, "account");
+      if(item->type!=AMF_STRING){continue;}
+      const char* account=(item->string.string[0]?item->string.string:0);
+      item=amf_getobjmember(&amfin->items[i].object, "mod");
+      if(item->type!=AMF_BOOL){continue;}
+      char mod=item->boolean;
+      idlist_add(id, nick, account, mod);
+      printf("%s%s", (i==2?"":", "), nick);
+      if(amf_comparestrings_c(&amfin->items[0].string, "registered"))
+      {
+        // Tell the server how often to let us know it got our packets
+        struct rtmp setbw={
+          .type=RTMP_SERVER_BW,
+          .chunkid=2,
+          .length=4,
+          .msgid=0,
+          .buf="\x00\x00\x40\x00" // Every 0x4000 bytes
+        };
+        rtmp_send(sock, &setbw);
+        char* key=getkey(id, channel);
+        if(!key){printf("Failed to get channel key\n"); exit(1);}
+        struct rtmp amf;
+        amfinit(&amf, 3);
+        amfstring(&amf, "cauth");
+        amfnum(&amf, 0);
+        amfnull(&amf); // Means nothing but is apparently critically important for cauth at least
+        amfstring(&amf, key);
+        amfsend(&amf, sock);
+        free(key);
+        // Set the given nickname
+        changenick(sock, nickname);
+        // Keep what the server gave us, just in case
+        free(nickname);
+        nickname=strdup(nick);
+      }
+    }
+  }
+  if(amf_comparestrings_c(&amfin->items[0].string, "joins"))
+  {
+    printf("\n");
+    for(i=0; i<idlistlen; ++i)
+    {
+      if(idlist[i].op){printf("%s is a moderator.\n", idlist[i].name);}
+    }
+    printf("Currently on cam: ");
+    char first=1;
+    for(i = 2; i < amfin->itemcount; ++i)
+    {
+      if(amfin->items[i].type==AMF_OBJECT)
+      {
+        struct amfitem* item=amf_getobjmember(&amfin->items[i].object, "bf");
+        if(item->type!=AMF_BOOL || !item->boolean){continue;}
+        item=amf_getobjmember(&amfin->items[i].object, "nick");
+        if(item->type!=AMF_STRING){continue;}
+        printf("%s%s", (first?"":", "), item->string.string);
+        first=0;
+      }
+    }
+    printf("\n");
+  }else{
+    printf(" entered the channel\n");
+    if(idlist[idlistlen-1].op){printf("%s is a moderator.\n", idlist[idlistlen-1].name);}
+    if(amf_comparestrings_c(&amfin->items[0].string, "registered"))
+    {
+      printf("Connection ID: %i\n", idlist[0].id);
+    }
+  }
+}
+
+static void privmsg(struct amf* amfin, int sock)
+{
+  if(amfin->items[3].type!=AMF_STRING || amfin->items[4].type!=AMF_STRING || amfin->items[5].type!=AMF_STRING){return;}
+  size_t len;
+  char* msg=fromnumlist(amfin->items[3].string.string, &len);
+  const char* color=color_start(amfin->items[4].string.string);
+  char* line=msg;
+  while(line)
+  {
+    // Handle multi-line messages
+    char* nextline=0;
+    unsigned int linelen;
+    for(linelen=0; &line[linelen]<&msg[len]; ++linelen)
+    {
+      if(line[linelen]=='\r' || line[linelen]=='\n'){nextline=&line[linelen+1]; break;}
+    }
+    printf("%s %s%s: ", timestamp(), color, amfin->items[5].string.string);
+    fwrite(line, linelen, 1, stdout);
+    printf("%s\n", color_end());
+    line=nextline;
+  }
+  if(!strncmp(msg, "/allowbroadcast ", 16))
+  {
+    if(getbroadcastkey(channel, nickname, &msg[16])) // Validate password
+    {
+      free((void*)bpassword);
+      bpassword=strdup(&msg[16]);
+      printf("Authenticated to broadcast\n");
+    }
+  }
+  else if(!strcmp(msg, "/version"))
+  {
+    sendmessage_priv(sock, "/version tc_client-" VERSION, amfin->items[5].string.string);
+  }
+  free(msg);
+}
+
+static void userquit(struct amf* amfin, int sock)
+{
+  // quit/part
+  if(amfin->items[2].type!=AMF_STRING){return;}
+  idlist_remove(amfin->items[2].string.string);
+  printf("%s %s left the channel\n", timestamp(), amfin->items[2].string.string);
+}
+
+static void usernick(struct amf* amfin, int sock)
+{
+  // nick
+  if(amfin->items[2].type!=AMF_STRING || amfin->items[3].type!=AMF_STRING){return;}
+  if(!strcmp(amfin->items[2].string.string, nickname)) // Successfully changed our own nickname
+  {
+    free(nickname);
+    nickname=strdup(amfin->items[3].string.string);
+  }
+  idlist_rename(amfin->items[2].string.string, amfin->items[3].string.string);
+  printf("%s %s changed nickname to %s\n", timestamp(), amfin->items[2].string.string, amfin->items[3].string.string);
+}
+
+static void userkick(struct amf* amfin, int sock)
+{
+  // kick
+  if(amfin->items[2].type!=AMF_STRING){return;}
+  if(atoi(amfin->items[2].string.string) == idlist_get(nickname))
+  {
+    printf("%s You have been kicked out\n", timestamp());
+  }
+}
+
+static void banned(struct amf* amfin, int sock)
+{
+  // banned
+  printf("%s You are banned from %s\n", timestamp(), channel);
+  exit(1); // Getting banned is a failure, right?
+}
+
+static void closecam(int sock, const char* nick)
+{
+  unsigned int userid=idlist_get(nick);
+  char camid[snprintf(0,0,"%u", userid)+1];
+  sprintf(camid, "%u", userid);
+  stream_stopvideo(sock, camid);
+}
+
+static void fromowner(struct amf* amfin, int sock)
+{
+  // from_owner: notices, mute, push2talk, closing cams
+  if(amfin->items[2].type!=AMF_STRING){return;}
+  if(!strncmp("notice", amfin->items[2].string.string, 6))
+  {
+    char* notice=strdup(&amfin->items[2].string.string[6]);
+    // replace "%20" with spaces
+    char* space;
+    while((space=strstr(notice, "%20")))
+    {
+      memmove(space, &space[2], strlen(&space[2])+1);
+      space[0]=' ';
+    }
+    printf("%s %s\n", timestamp(), notice);
+  }
+  else if(!strcmp("mute", amfin->items[2].string.string))
+  {
+    printf("%s Non-moderators have been temporarily muted.\n", timestamp());
+  }
+  else if(!strcmp("push2talk", amfin->items[2].string.string))
+  {
+    printf("%s Push to talk request has been sent to non-moderators.\n", timestamp());
+  }
+  else if(!strncmp("_close", amfin->items[2].string.string, 6) && !strcmp(&amfin->items[2].string.string[6], nickname))
+  {
+    printf("Outgoing media stream was closed\n");
+    closecam(sock, nickname);
+  }
+}
+
+static void nickinuse(struct amf* amfin, int sock)
+{
+  // nickinuse, the nick we wanted to change to is already taken
+  printf("Nick is already in use.\n");
+}
+
+static void channeltopic(struct amf* amfin, int sock)
+{
+  // Room topic
+  if(amfin->items[2].type!=AMF_STRING || !amfin->items[2].string.length){return;}
+  printf("Room topic: %s\n", amfin->items[2].string.string);
+}
+
+static void banlist(struct amf* amfin, int sock)
+{
+  // Get list of banned users
+  unsigned int i;
+  if(unban) // This is not a response to /banlist but to /forgive
+  {
+    // If the nickname isn't found in the banlist we assume it's an ID
+    const char* id=unban;
+    for(i=2; i+1<amfin->itemcount; i+=2)
+    {
+      if(amfin->items[i].type!=AMF_STRING || amfin->items[i+1].type!=AMF_STRING){break;}
+      if(!strcmp(amfin->items[i+1].string.string, unban) || !strcmp(amfin->items[i].string.string, unban))
+      {
+        id=amfin->items[i].string.string;
+      }
+    }
+    struct rtmp amf;
+    amfinit(&amf, 3);
+    amfstring(&amf, "forgive");
+    amfnum(&amf, 0);
+    amfnull(&amf);
+    amfstring(&amf, id);
+    amfsend(&amf, sock);
+    free(unban);
+    unban=0;
+    return;
+  }
+  printf("Banned users:\n");
+  printf("ID         Nickname\n");
+  for(i=2; i+1<amfin->itemcount; i+=2)
+  {
+    if(amfin->items[i].type!=AMF_STRING || amfin->items[i+1].type!=AMF_STRING){break;}
+    unsigned int len=printf("%s", amfin->items[i].string.string);
+    for(;len<10; ++len){printf(" ");}
+    printf(" %s\n", amfin->items[i+1].string.string);
+  }
+  printf("Use /forgive <ID/nick> to unban someone\n");
+}
+
+static void notice(struct amf* amfin, int sock)
+{
+  if(amfin->items[2].type!=AMF_STRING || !amf_comparestrings_c(&amfin->items[2].string, "avon") || amfin->items[4].type!=AMF_STRING){return;}
+  printf("%s cammed up\n", amfin->items[4].string.string);
+}
+
+static void result(struct amf* amfin, int sock)
+{
+  // Handle results for various requests, haven't seen much of a pattern to it, always successful?
+  // Creating a new stream worked, now play media (cam/mic) on it (if that's what the result was for)
+  stream_play(amfin, sock);
+}
+
+static void onstatus(struct amf* amfin, int sock)
+{
+  stream_handlestatus(amfin, sock);
+}
+
+static void mod_close(int sock, const char* nick)
+{
+  struct rtmp amf;
+  amfinit(&amf, 2);
+  amfstring(&amf, "owner_run");
+  amfnum(&amf, 0);
+  amfnull(&amf);
+  char buf[strlen("closed: 0")+strlen(nick)];
+  sprintf(buf, "_close%s", nick);
+  amfstring(&amf, buf);
+  amfsend(&amf, sock);
+  sprintf(buf, "closed: %s", nick);
+  sendmessage(sock, buf);
+}
+
+static void mod_ban(int sock, const char* nick)
+{
+  struct rtmp amf;
+  amfinit(&amf, 3);
+  amfstring(&amf, "owner_run");
+  amfnum(&amf, 0);
+  amfnull(&amf);
+//  char buf[strlen("notice%20was%20banned%20by%20%20()0")+strlen(nick)+strlen(nickname)+strlen(account_user)];
+//  sprintf(buf, "notice%s%%20was%%20banned%%20by%%20%s%%20(%s)", nick, nickname, account_user);
+  char buf[strlen("notice%20was%20banned%20by%200")+strlen(nick)+strlen(nickname)];
+  sprintf(buf, "notice%s%%20was%%20banned%%20by%%20%s", nick, nickname);
+  amfstring(&amf, buf);
+  amfsend(&amf, sock);
+//  printf("%s %s was banned by %s (%s)\n", timestamp(), nick, nickname, account_user);
+  printf("%s %s was banned by %s\n", timestamp(), nick, nickname);
+  // kick (this does the actual banning)
+  amfinit(&amf, 3);
+  amfstring(&amf, "kick");
+  amfnum(&amf, 0);
+  amfnull(&amf);
+  amfstring(&amf, nick);
+  sprintf(buf, "%i", idlist_get(nick));
+  amfstring(&amf, buf);
+  amfsend(&amf, sock);
+}
+
+static void mod_banlist(int sock)
+{
+  struct rtmp amf;
+  amfinit(&amf, 3);
+  amfstring(&amf, "banlist");
+  amfnum(&amf, 0);
+  amfnull(&amf);
+  amfsend(&amf, sock);
+}
+
+static void mod_unban(int sock, const char* nick)
+{
+  free(unban);
+  unban=strdup(nick);
+  mod_banlist(sock);
+}
+
+static void mod_mute(int sock)
+{
+  struct rtmp amf;
+  amfinit(&amf, 3);
+  amfstring(&amf, "owner_run");
+  amfnum(&amf, 0);
+  amfnull(&amf);
+  amfstring(&amf, "mute");
+  amfsend(&amf, sock);
+}
+
+static void mod_push2talk(int sock)
+{
+  struct rtmp amf;
+  amfinit(&amf, 3);
+  amfstring(&amf, "owner_run");
+  amfnum(&amf, 0);
+  amfnull(&amf);
+  amfstring(&amf, "push2talk");
+  amfsend(&amf, sock);
+}
+
+static void mod_topic(int sock, const char* newtopic)
+{
+  struct rtmp amf;
+  amfinit(&amf, 3);
+  amfstring(&amf, "topic");
+  amfnum(&amf, 0);
+  amfnull(&amf);
+  amfstring(&amf, newtopic);
+  amfstring(&amf, "");
+  amfsend(&amf, sock);
+}
+
+static void mod_allowbroadcast(int sock, const char* nick)
+{
+  if(!bpassword){return;}
+  char buf[strlen("/allowbroadcast 0")+strlen(bpassword)];
+  sprintf(buf, "/allowbroadcast %s", bpassword);
+  sendmessage_priv(sock, buf, nick);
+}
+
+static void camup(int sock)
+{
+  // Retrieve and send the key for broadcasting access
+  char* key=getbroadcastkey(channel, nickname, bpassword);
+  struct rtmp amf;
+  amfinit(&amf, 3);
+  amfstring(&amf, "bauth");
+  amfnum(&amf, 0);
+  amfnull(&amf);
+  amfstring(&amf, key);
+  amfsend(&amf, sock);
+  free(key);
+  // Initiate stream
+  unsigned int userid=idlist_get(nickname);
+  char camid[snprintf(0,0,"%u", userid)+1];
+  sprintf(camid, "%u", userid);
+  streamout_start(camid, sock);
+}
+
+static void camdown(int sock)
+{
+  unsigned int userid=idlist_get(nickname);
+  char camid[snprintf(0,0,"%u", userid)+1];
+  sprintf(camid, "%u", userid);
+  stream_stopvideo(sock, camid);
+}
+
+static void opencam(int sock, const char* nick)
+{
+  unsigned int userid=idlist_get(nick);
+  char camid[snprintf(0,0,"%u", userid)+1];
+  sprintf(camid, "%u", userid);
+  stream_start(nick, camid, sock);
+}
+
+int init_tinychat(const char* chanpass, const char* username, const char* userpass, struct site* site)
+{
+  char badchar;
+  if((badchar=checknick(nickname)))
+  {
+    printf("'%c' is not allowed in nicknames.\n", badchar);
+    return -1;
+  }
+  registercmd("privmsg", privmsg, 6);
+  registercmd("join", joinreg, 3);
+  registercmd("nick", usernick, 5);
+  registercmd("quit", userquit, 3);
+  registercmd("notice", notice, 5);
+  registercmd("from_owner", fromowner, 3);
+  registercmd("nickinuse", nickinuse, 1);
+  registercmd("banlist", banlist, 1);
+  registercmd("topic", channeltopic, 3);
+  registercmd("joins", joinreg, 3);
+  registercmd("registered", joinreg, 3);
+  registercmd("onStatus", onstatus, 1);
+  registercmd("_result", result, 3);
+  registercmd("kick", userkick, 4);
+  registercmd("banned", banned, 2);
+  site->sendmessage=sendmessage;
+  site->sendpm=sendpm;
+  site->nick=changenick;
+  site->mod_close=mod_close;
+  site->mod_ban=mod_ban;
+  site->mod_banlist=mod_banlist;
+  site->mod_unban=mod_unban;
+  site->mod_mute=mod_mute;
+  site->mod_push2talk=mod_push2talk;
+  site->mod_topic=mod_topic;
+  site->mod_allowbroadcast=mod_allowbroadcast;
+  site->opencam=opencam;
+  site->closecam=closecam;
+  site->camup=camup;
+  site->camdown=camdown;
+  char loggedin=0;
+  // Log in if user account is specified
+  char* modkey=getmodkey(username, userpass, channel, &loggedin);
+  char* server=gethost(channel, chanpass);
+  // Separate IP/domain and port
+  char* port=strchr(server, ':');
+  if(!port){return 3;}
+  port[0]=0;
+  ++port;
+  int sock=connectto(server, port);
+
+  rtmp_handshake(sock);
+  if(!loggedin){username=0; userpass=0;}
+  getcaptcha();
+  char* cookie=getcookie(channel);
+  // Send connect request
+  struct rtmp amf;
+  amfinit(&amf, 3);
+  amfstring(&amf, "connect");
+  amfnum(&amf, 0);
+  amfobjstart(&amf);
+    amfobjitem(&amf, "app");
+    {char str[strlen("chat/0")+strlen(channel)];
+    sprintf(str, "chat/%s", channel);
+    amfstring(&amf, str);}
+
+    amfobjitem(&amf, "flashVer");
+    amfstring(&amf, "no flash");
+
+    amfobjitem(&amf, "swfUrl");
+    amfstring(&amf, "no flash");
+
+    amfobjitem(&amf, "tcUrl");
+    {char str[strlen("rtmp://:/tinyconf0")+strlen(server)+strlen(port)+strlen(channel)];
+    sprintf(str, "rtmp://%s:%s/chat/%s", server, port, channel);
+    amfstring(&amf, str);}
+
+    amfobjitem(&amf, "fpad");
+    amfbool(&amf, 0);
+
+    amfobjitem(&amf, "capabilities");
+    amfnum(&amf, 0);
+
+    amfobjitem(&amf, "audioCodecs");
+    amfnum(&amf, 0);
+
+    amfobjitem(&amf, "videoCodecs");
+    amfnum(&amf, 0);
+
+    amfobjitem(&amf, "videoFunction");
+    amfnum(&amf, 0);
+
+    amfobjitem(&amf, "pageUrl");
+    char pageurl[strlen("http://tinychat.com/rooms/0") + strlen(channel)];
+    sprintf(pageurl, "http://tinychat.com/rooms/%s", channel);
+    amfstring(&amf, pageurl);
+
+    amfobjitem(&amf, "objectEncoding");
+    amfnum(&amf, 0);
+  amfobjend(&amf);
+  amfobjstart(&amf);
+    amfobjitem(&amf, "version");
+    amfstring(&amf, "Desktop");
+
+    amfobjitem(&amf, "type");
+    amfstring(&amf, "default"); // This item is called roomtype in the same HTTP response that gives us the server (IP+port) to connect to, but "default" seems to work fine too.
+
+    amfobjitem(&amf, "account");
+    amfstring(&amf, username?username:"");
+
+    amfobjitem(&amf, "prefix");
+    amfstring(&amf, sitearg);
+
+    amfobjitem(&amf, "room");
+    amfstring(&amf, channel);
+
+    if(modkey)
+    {
+      amfobjitem(&amf, "autoop");
+      amfstring(&amf, modkey);
+    }
+    amfobjitem(&amf, "cookie");
+    amfstring(&amf, cookie);
+  amfobjend(&amf);
+  amfsend(&amf, sock);
+  free(modkey);
+  free(cookie);
+  return sock;
+}
diff --git a/utilities/gtk/camviewer.c b/utilities/gtk/camviewer.c
index c91a67b..64ab1cc 100644
--- a/utilities/gtk/camviewer.c
+++ b/utilities/gtk/camviewer.c
@@ -255,6 +255,7 @@ gboolean handledata(GIOChannel* iochannel, GIOCondition condition, gpointer x)
     if(config_get_bool("disablesnapshots")){write(tc_client_in[1], "/disablesnapshots\n", 18);}
     write(tc_client_in[1], "/color\n", 7); // Check which random color tc_client picked
     unsigned int length=strlen(&buf[15]);
+    free(nickname);
     nickname=malloc(length+strlen("guest-")+1);
     sprintf(nickname, "guest-%s", &(buf[15]));
     if(hasgreenroom){greenroom_join(&buf[15]);}
@@ -895,6 +896,8 @@ void startsession(GtkButton* button, void* x)
   gtk_widget_hide(GTK_WIDGET(gtk_builder_get_object(gui, "channelpasswordwindow")));
   GtkWidget* mainwindow=GTK_WIDGET(gtk_builder_get_object(gui, "main"));
   const char* nick=gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(gui, "cc_nick")));
+  free(nickname);
+  nickname=strdup(nick);
   channel=gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(gui, "cc_channel")));
   const char* chanpass=gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(gui, "channelpassword")));
   if(!chanpass[0]){chanpass=gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(gui, "cc_password")));}