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

    Version 0.20

diff --git a/ChangeLog b/ChangeLog
index cd2abe0..4662717 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,10 @@
+0.20:
+Fixed amf0 reading of numbers (type 0)
+Added support for sending format 0 RTMP packets (which include the msgid field)
+Handle "/userinfo $request", responding with the account name if logged in and otherwise "tc_client" (which is not a valid tinychat account name but the flash client will show it anyway)
+Added a /priv command, mostly for use by modbot.
+Improved the way RTMP payloads are read from the stream.
+modbot: when someone joins, send a /mbs (start video) command with the current position in the video, but only to that user.
 0.19:
 Got unicode characters working, as it turns out tinychat doesn't use ISO-8859-1 but UTF-16 (which shares character codes with ISO-8859-1 below 256)
 Enable keepalive on the TCP socket.
diff --git a/Makefile b/Makefile
index 105b5d1..b364d79 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-VERSION=0.19
+VERSION=0.20
 CFLAGS=-g3 -Wall $(shell curl-config --cflags)
 LIBS=-g3 $(shell curl-config --libs)
 ifneq ($(wildcard config.mk),)
diff --git a/README b/README
index 4c84504..73890b7 100644
--- a/README
+++ b/README
@@ -8,6 +8,7 @@ Commands supported by tc_client:
 /color <0-15>             = set the color of your outgoing messages
 /colors                   = list the available colors
 /nick <newnick>           = change nickname
+/help                     = list these commands at runtime
 
 Some things that will probably never change:
 *tc_client can't view people's webcams or listen to mics
diff --git a/amfparser.c b/amfparser.c
index 3e0cf1b..8dfaf51 100644
--- a/amfparser.c
+++ b/amfparser.c
@@ -97,6 +97,8 @@ struct amf* amf_parse(const unsigned char* buf, int len)
         item=amf_newitem(amf);
       item->type=AMF_NUMBER;
       memcpy(&item->number, buf, sizeof(double));
+      unsigned long long* endian=(void*)&item->number;
+      *endian=be64(*endian);
       buf=&buf[sizeof(double)];
       break;
     case 1:
diff --git a/amfwriter.c b/amfwriter.c
index 97876ec..76d06d2 100644
--- a/amfwriter.c
+++ b/amfwriter.c
@@ -27,11 +27,14 @@ void amfinit(struct rtmp* msg, unsigned int streamid)
   msg->type=RTMP_AMF0;
   msg->streamid=streamid;
   msg->length=0;
+  msg->msgid=0;
   msg->buf=0;
 }
 
 void amfnum(struct rtmp* msg, double v)
 {
+  unsigned long long* endian=(void*)&v;
+  *endian=be64(*endian);
   int offset=msg->length;
   msg->length+=1+sizeof(double);
   msg->buf=realloc(msg->buf, msg->length);
diff --git a/client.c b/client.c
index 7df618d..f2e8bcc 100644
--- a/client.c
+++ b/client.c
@@ -141,7 +141,7 @@ char* getkey(char* id, char* channel)
   return key;
 }
 
-char* getmodkey(const char* user, const char* pass, const char* channel)
+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;}
@@ -159,6 +159,7 @@ char* getmodkey(const char* user, const char* pass, const char* channel)
       key=strdup(key);
     }else{key=0;}
   }
+  if(strstr(response, "<div class='name'")){*loggedin=1;}
   free(response);
   return key;
 }
@@ -183,6 +184,25 @@ char checknick(const char* nick) // Returns zero if the nick is valid, otherwise
   return 0;
 }
 
+char* getprivfield(char* nick)
+{
+  unsigned int id;
+  unsigned int privlen;
+  for(privlen=0; nick[privlen]&&nick[privlen]!=' '; ++privlen);
+  id=idlist_get((char*)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);
+  strncat(priv, nick, privlen);
+  return priv;
+}
+
 void usage(const char* me)
 {
   printf("Usage: %s [options] <channelname> <nickname> [channelpassword]\n"
@@ -214,7 +234,7 @@ int main(int argc, char** argv)
     else if(!strcmp(argv[i], "-p")||!strcmp(argv[i], "--pass"))
     {
       ++i;
-      account_pass=argv[i];
+      account_pass=strdup(argv[i]);
     }
     else if(!channel){channel=argv[i];}
     else if(!nickname){nickname=strdup(argv[i]);}
@@ -273,9 +293,11 @@ int main(int argc, char** argv)
   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};
+  struct rtmp rtmp={0,0,0,0,0};
   // Handshake complete, log in (if user account is specified)
-  char* modkey=getmodkey(account_user, account_pass, channel);
+  char loggedin=0;
+  char* modkey=getmodkey(account_user, account_pass, channel, &loggedin);
+  if(!loggedin){free(account_pass); account_user=0; account_pass=0;}
   // Send connect request
   struct rtmp amf;
   amfinit(&amf, 3);
@@ -354,8 +376,7 @@ 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;
-      int privfield=-1;
-      int privlen;
+      char* privfield=0;
       if(buf[0]=='/') // Got a command
       {
         if(!strcmp((char*)buf, "/help"))
@@ -410,16 +431,32 @@ int main(int argc, char** argv)
         }
         else if(!strncmp((char*)buf, "/msg ", 5))
         {
-          for(privlen=0; buf[5+privlen]&&buf[5+privlen]!=' '; ++privlen);
-          privfield=idlist_get((char*)&buf[5]);
-          if(privfield<0)
-          {
-            buf[5+privlen]=0;
-            printf("No such nick: %s\n", &buf[5]);
-            fflush(stdout);
-            continue;
-          }
+          privfield=getprivfield((char*)&buf[5]);
+          if(!privfield){continue;}
+        }
+        else if(!strncmp((char*)buf, "/priv ", 6))
+        {
+          char* end=strchr((char*)&buf[6], ' ');
+          if(!end){continue;}
+          privfield=getprivfield((char*)&buf[6]);
+          if(!privfield){continue;}
+          len=strlen(&end[1]);
+          memmove(buf, &end[1], len+1);
         }
+/* While we can get the server to send us video data, we don't know how to handle the data yet.
+        else if(!strncmp((char*)buf, "/cam ", 5))
+        {
+          unsigned int id=idlist_get((char*)&buf[5]);
+          camid=malloc(128);
+          sprintf(camid, "%u", id);
+          amfinit(&amf, 3);
+          amfstring(&amf, "createStream");
+          amfnum(&amf, 2);
+          amfnull(&amf);
+          amfsend(&amf, sock);
+          continue;
+        }
+*/
       }
       char* msg=tonumlist((char*)buf, len);
       amfinit(&amf, 3);
@@ -429,12 +466,10 @@ int main(int argc, char** argv)
       amfstring(&amf, msg);
       amfstring(&amf, colors[currentcolor%16]);
       // For PMs, add a string like "n<numeric ID>-<nick>" to make the server only send it to the intended recipient
-      if(privfield>-1)
+      if(privfield)
       {
-        char priv[snprintf(0, 0, "n%i-", privfield)+privlen+1];
-        sprintf(priv, "n%i-", privfield);
-        strncat(priv, (char*)&buf[5], privlen);
-        amfstring(&amf, priv);
+        amfstring(&amf, privfield);
+        free(privfield);
       }
       amfsend(&amf, sock);
       free(msg);
@@ -445,6 +480,13 @@ int main(int argc, char** argv)
     // Read the RTMP stream and handle AMF0 packets
     if(rtmp_get(sock, &rtmp))
     {
+/* Getting video/cam data, but we don't know how to make use of it yet
+      if(rtmp.type==RTMP_VIDEO)
+      {
+        write(vidf, rtmp.buf, rtmp.length);
+        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)
@@ -483,6 +525,31 @@ int main(int argc, char** argv)
         printf("%s \x1b[%sm%s: ", timestamp(), color, amfin->items[5].string.string);
         fwrite(msg, len, 1, stdout);
         printf("\x1b[0m\n");
+        if(len==18 && !strncmp(msg, "/userinfo $request", 18))
+        {
+          char* msg;
+          if(account_user)
+          {
+            unsigned int len=strlen("/userinfo \n0")+strlen(account_user);
+            char buf[len+1];
+            sprintf(buf, "/userinfo %s\n", account_user);
+            msg=tonumlist(buf, len);
+          }else{
+            msg=tonumlist("/userinfo tc_client\n", 20); // TODO: include version number?
+          }
+          amfinit(&amf, 3);
+          amfstring(&amf, "privmsg");
+          amfnum(&amf, 0);
+          amfnull(&amf);
+          amfstring(&amf, msg);
+          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);
+          free(msg);
+        }
         free(msg);
         fflush(stdout);
       }
@@ -590,6 +657,22 @@ int main(int argc, char** argv)
         printf("Room topic: %s\n", amfin->items[2].string.string);
         fflush(stdout);
       }
+/* More not yet usable cam code, response to trying to open a new stream
+      else if(amfin->itemcount>0 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "_result"))
+      {
+        printamf(amfin);
+        if(camid)
+        {
+          amfinit(&amf, 8);
+          amfstring(&amf, "play");
+          amfnum(&amf, 0);
+          amfnull(&amf);
+          amfstring(&amf, camid);
+          amf.msgid=le32(1);
+          amfsend(&amf, sock);
+        }
+      }
+*/
       // else{printf("Unknown command...\n"); printamf(amfin);} // (Debugging)
       amf_free(amfin);
     }
diff --git a/endian.c b/endian.c
index c327ab4..17f9a2c 100644
--- a/endian.c
+++ b/endian.c
@@ -16,6 +16,22 @@
 */
 #include "endian.h"
 
+unsigned long long be64(unsigned long long in)
+{
+#if(__BYTE_ORDER__==__ORDER_LITTLE_ENDIAN__)
+  return ((in&0xff)<<56) |
+         ((in&0xff00)<<40) |
+         ((in&0xff0000)<<24) |
+         ((in&0xff000000)<<8) |
+         ((in&0xff00000000)>>8) |
+         ((in&0xff0000000000)>>24) |
+         ((in&0xff000000000000)>>40) |
+         ((in&0xff00000000000000)>>56);
+#else
+  return in;
+#endif
+}
+
 unsigned long be32(unsigned long in)
 {
 #if(__BYTE_ORDER__==__ORDER_LITTLE_ENDIAN__)
diff --git a/endian.h b/endian.h
index 790416d..1f87c60 100644
--- a/endian.h
+++ b/endian.h
@@ -14,6 +14,7 @@
     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/>.
 */
+extern unsigned long long be64(unsigned long long in);
 extern unsigned long be32(unsigned long in);
 extern unsigned long le32(unsigned long in);
 extern unsigned short be16(unsigned short in);
diff --git a/modbot.c b/modbot.c
index d6b8dde..5cffbb9 100644
--- a/modbot.c
+++ b/modbot.c
@@ -24,6 +24,7 @@
 #include <signal.h>
 #include <sys/wait.h>
 #include <stdarg.h>
+#include <time.h>
 
 struct list
 {
@@ -36,6 +37,7 @@ struct list queue={0,0};
 struct list goodvids={0,0}; // pre-approved videos
 struct list badvids={0,0}; // not allowed, essentially banned
 char* playing=0;
+time_t started=0;
 int tc_client;
 
 void list_del(struct list* list, const char* item)
@@ -222,6 +224,7 @@ void playnextvid()
   say(0, "/mbs youTube %s 0\n", playing);
   // Find out the video's length and schedule an alarm for then
   alarm(getduration(playing));
+  started=time(0);
 }
 
 void playnext(int x)
@@ -444,7 +447,7 @@ int main(int argc, char** argv)
           {
             if(playing)
             {
-              if(list_contains(&goodvids, playing) && !list_contains(&badvids, playing)){say(pm, "'%s' is already approved, use !approve <ID> to approve another video\n", playing); continue;}
+              if(list_contains(&goodvids, playing) && !list_contains(&badvids, playing)){say(pm, "'%s' is already approved, use !approve <ID> to approve another video (or 'next' instead of an ID to approve the next not-yet-approved video in queue)\n", playing); continue;}
               list_add(&goodvids, playing);
               list_del(&badvids, playing);
               list_save(&goodvids, "goodvids.txt");
@@ -527,14 +530,18 @@ int main(int argc, char** argv)
             list_save(&goodvids, "goodvids.txt");
             free(playing);
             playing=strdup(vid);
-            alarm(getduration(vid));
+            unsigned int pos=(end?(strtol(&end[1], 0, 0)/1000):0);
+            alarm(getduration(playing)-pos);
+            started=time(0)-pos;
           }
           else if(!strcmp(msg, "/mbc youTube")){playnext(0);} // Video cancelled
           else if(!strncmp(msg, "/mbsk youTube ", 14)) // Seeking
           {
             unsigned int pos=strtol(&msg[14], 0, 0)/1000;
             alarm(getduration(playing)-pos);
+            started=time(0)-pos;
           }
+          // TODO: handle /mbpa (pause) and /mbpl (resume play)
         }
       }else{ // Actions
         if(!strncmp(space, " changed nickname to ", 21))
@@ -556,6 +563,14 @@ int main(int argc, char** argv)
           }
           continue;
         }
+        else if(!strcmp(space, " entered the channel")) // Newcomer, inform about the currently playing video
+        {
+          if(playing)
+          {
+            space[0]=0;
+            say(0, "/priv %s /mbs youTube %s %u\n", nick, playing, (time(0)-started)*1000);
+          }
+        }
       }
     }
   }
diff --git a/rtmp.c b/rtmp.c
index 34261ee..a1f5ac8 100644
--- a/rtmp.c
+++ b/rtmp.c
@@ -31,6 +31,7 @@ char rtmp_get(int sock, struct rtmp* rtmp)
   if(read(sock, &x, 1)<1){return 0;}
   unsigned int streamid=x&0x3f;
   unsigned int fmt=(x&0xc0)>>6;
+  unsigned int msgid=0;
   // Handle extended stream IDs
   if(streamid<2) // (0=1 extra byte, 1=2 extra bytes)
   {
@@ -52,8 +53,7 @@ char rtmp_get(int sock, struct rtmp* rtmp)
       read(sock, &type, sizeof(type));
       if(fmt<1)
       {
-        // Message ID (is this ever used?)
-        unsigned int msgid;
+        // Message ID
         read(sock, &msgid, sizeof(msgid));
       }
     }
@@ -67,16 +67,19 @@ char rtmp_get(int sock, struct rtmp* rtmp)
 
   rtmp->type=type;
   rtmp->streamid=streamid;
-  rtmp->length=length+length/128;
+  rtmp->length=length;
+  rtmp->msgid=le32(msgid);
   free(rtmp->buf);
   rtmp->buf=malloc(rtmp->length);
-  read(sock, rtmp->buf, rtmp->length);
-  int i;
-  for(i=128; i<rtmp->length; i+=128)
+  size_t pos=0;
+  size_t w;
+  // Only read up to 128 bytes at a time and discard the (garbage/RTMP continuation header) bytes in between
+  while(pos<length)
   {
-    --rtmp->length;
-    // printf("Dropping garbage byte %x\n", (unsigned int)((unsigned char*)rtmp->buf)[i]&0xff);
-    memmove(rtmp->buf+i, rtmp->buf+i+1, rtmp->length-i);
+    w=read(sock, rtmp->buf+pos, ((length-pos>127)?128:(length-pos)));
+    if(w<1){break;}
+    pos+=w;
+    if(length-pos>0){read(sock, &w, 1);} // Skip junk once every 128 bytes
   }
   return 1;
 }
@@ -84,7 +87,8 @@ char rtmp_get(int sock, struct rtmp* rtmp)
 void rtmp_send(int sock, struct rtmp* rtmp)
 {
   // Header format and stream ID
-  unsigned char basicheader=(rtmp->streamid<64?rtmp->streamid:(rtmp->streamid<256?0:1)) | (1<<6);
+  unsigned int fmt=(rtmp->msgid?0:1);
+  unsigned char basicheader=(rtmp->streamid<64?rtmp->streamid:(rtmp->streamid<256?0:1)) | (fmt<<6);
   write(sock, &basicheader, sizeof(basicheader));
   if(rtmp->streamid>=64) // Handle large stream IDs
   {
@@ -105,7 +109,10 @@ void rtmp_send(int sock, struct rtmp* rtmp)
   write(sock, ((void*)&x)+1, 3);
   // Type
   write(sock, &rtmp->type, sizeof(rtmp->type));
-  // TODO: is there a situation where message IDs are needed? (format 0 header)
+  if(fmt<1) // Send message ID if there is one (that isn't 0)
+  {
+    write(sock, &rtmp->msgid, sizeof(rtmp->msgid));
+  }
   // Send 128 bytes at a time separated by 0xc3 (because apparently that's something RTMP requires)
   void* pos=rtmp->buf;
   unsigned int len=rtmp->length;
diff --git a/rtmp.h b/rtmp.h
index 3326286..f18b2ce 100644
--- a/rtmp.h
+++ b/rtmp.h
@@ -29,6 +29,7 @@ struct rtmp
   unsigned char type;
   unsigned int streamid;
   unsigned int length;
+  unsigned int msgid;
   void* buf;
 };