소스 검색

implement group chat

* chat, wip

* consistently asks for create room

* SendAndReceiveCreateRoom leads to receiveAndSendServiceRequest

* routeChatNav not implemented :)

* chat invite works

* provide for separate connections for bos and chat

* chat works

* add snacWriter to TLV writer

* implement chat registry

* everything works except for who is in the room

* online list almost working...

* status quo...

* fix bugs

* almost there

* don't pass FLAP references, race condition!

* cleanup

* handle users exiting from chat

* refactoring

* refactoring

* refactoring

* refactoring

* refactoring
Mike 2 년 전
부모
커밋
fcb1b405fb
12개의 변경된 파일1088개의 추가작업 그리고 125개의 파일을 삭제
  1. 91 12
      cmd/main.go
  2. 3 3
      oscar/bucp.go
  3. 4 4
      oscar/buddy.go
  4. 338 0
      oscar/chat.go
  5. 305 10
      oscar/chat_nav.go
  6. 12 12
      oscar/feedbag.go
  7. 26 19
      oscar/icbm.go
  8. 7 7
      oscar/locate.go
  9. 79 25
      oscar/oservice.go
  10. 2 2
      oscar/pd.go
  11. 68 29
      oscar/protocol.go
  12. 153 2
      oscar/session.go

+ 91 - 12
cmd/main.go

@@ -13,13 +13,16 @@ const testFile string = "/Users/mike/dev/goaim/aim.db"
 
 func main() {
 
-	sm := oscar.NewSessionManager()
 	fm, err := oscar.NewFeedbagStore(testFile)
 	if err != nil {
 		log.Fatal(err)
 	}
 
-	go listenBOS(sm, fm)
+	sm := oscar.NewSessionManager()
+	cr := oscar.NewChatRegistry()
+
+	go listenBOS(sm, fm, cr)
+	go listenChat(fm, cr)
 
 	listener, err := net.Listen("tcp", ":5190")
 	if err != nil {
@@ -31,14 +34,12 @@ func main() {
 	fmt.Println("Server is listening on port 5190")
 
 	for {
-		// Accept incoming connections
 		conn, err := listener.Accept()
 		if err != nil {
 			log.Println(err)
 			continue
 		}
 
-		// Handle connection in a separate goroutine
 		go handleAuthConnection(sm, fm, conn)
 	}
 }
@@ -54,19 +55,33 @@ func webServer(ch chan string) {
 	}
 }
 
-func listenBOS(sm *oscar.SessionManager, fm *oscar.FeedbagStore) {
-	// Listen on TCP port 5190
+func listenBOS(sm *oscar.SessionManager, fm *oscar.FeedbagStore, cr *oscar.ChatRegistry) {
 	listener, err := net.Listen("tcp", ":5191")
 	if err != nil {
 		log.Fatal(err)
 	}
+	defer listener.Close()
+
+	fmt.Println("Server is listening on port 5191")
 
-	ch := make(chan string, 1)
-	go webServer(ch)
+	for {
+		conn, err := listener.Accept()
+		if err != nil {
+			log.Println(err)
+			continue
+		}
+		go handleBOSConnection(sm, fm, cr, conn)
+	}
+}
 
+func listenChat(fm *oscar.FeedbagStore, cr *oscar.ChatRegistry) {
+	listener, err := net.Listen("tcp", ":5192")
+	if err != nil {
+		log.Fatal(err)
+	}
 	defer listener.Close()
 
-	fmt.Println("Server is listening on port 5191")
+	fmt.Println("Server is listening on port 5192")
 
 	for {
 		// Accept incoming connections
@@ -75,7 +90,7 @@ func listenBOS(sm *oscar.SessionManager, fm *oscar.FeedbagStore) {
 			log.Println(err)
 			continue
 		}
-		go handleBOSConnection(sm, fm, conn)
+		go handleChatConnection(fm, cr, conn)
 	}
 }
 
@@ -106,8 +121,28 @@ func handleAuthConnection(sm *oscar.SessionManager, fm *oscar.FeedbagStore, conn
 	}
 }
 
-func handleBOSConnection(sm *oscar.SessionManager, fm *oscar.FeedbagStore, conn net.Conn) {
-	if err := oscar.ReadBos(sm, fm, conn); err != nil && err != io.EOF {
+func handleBOSConnection(sm *oscar.SessionManager, fm *oscar.FeedbagStore, cr *oscar.ChatRegistry, conn net.Conn) {
+	defer conn.Close()
+
+	sess, seq, err := oscar.VerifyLogin(sm, conn)
+	if err != nil {
+		fmt.Printf("user disconnected with error: %s\n", err.Error())
+		return
+	}
+
+	defer oscar.Signout(sess, sm, fm)
+
+	onClientReady := func(sess *oscar.Session, sm *oscar.SessionManager, r io.Reader, w io.Writer, sequence *uint32) error {
+		err := oscar.NotifyArrival(sess, sm, fm)
+		if err != nil {
+			return err
+		}
+
+		return oscar.GetOnlineBuddies(w, sess, sm, fm, sequence)
+	}
+
+	foodGroups := []uint16{0x0001, 0x0002, 0x0003, 0x0004, 0x0009, 0x0013, 0x000D}
+	if err := oscar.ReadBos(onClientReady, sess, seq, sm, fm, cr, conn, foodGroups); err != nil && err != io.EOF {
 		if err != io.EOF {
 			fmt.Printf("user disconnected with error: %s\n", err.Error())
 		} else {
@@ -115,3 +150,47 @@ func handleBOSConnection(sm *oscar.SessionManager, fm *oscar.FeedbagStore, conn
 		}
 	}
 }
+
+func handleChatConnection(fm *oscar.FeedbagStore, cr *oscar.ChatRegistry, conn net.Conn) {
+	defer conn.Close()
+
+	cookie, seq, err := oscar.VerifyChatLogin(conn)
+	if err != nil {
+		fmt.Printf("user disconnected with error: %s\n", err.Error())
+		return
+	}
+
+	room, err := cr.Retrieve(string(cookie.Cookie))
+
+	chatSess, found := room.SessionManager.Retrieve(cookie.SessID)
+	if !found {
+		fmt.Printf("unable to find user for session: %s\n", cookie.SessID)
+		return
+	}
+	defer exitChat(chatSess, room, cr)
+
+	foodGroups := []uint16{0x0001, 0x0002, 0x0003, 0x0004, 0x0009, 0x0013, 0x000D, 0x000E}
+
+	onClientReady := func(sess *oscar.Session, sm *oscar.SessionManager, r io.Reader, w io.Writer, sequence *uint32) error {
+		if err := oscar.SendChatRoomInfoUpdate(room, w, sequence); err != nil {
+			return err
+		}
+		oscar.AlertUserJoined(sess, sm)
+		return oscar.SetOnlineChatUsers(sm, w, sequence)
+	}
+
+	if err := oscar.ReadBos(onClientReady, chatSess, seq, room.SessionManager, fm, cr, conn, foodGroups); err != nil && err != io.EOF {
+		if err != io.EOF {
+			fmt.Printf("user disconnected with error: %s\n", err.Error())
+		} else {
+			fmt.Println("user disconnected")
+		}
+	}
+}
+
+func exitChat(chatSess *oscar.Session, room oscar.ChatRoom, cr *oscar.ChatRegistry) {
+	oscar.AlertUserLeft(chatSess, room.SessionManager)
+	chatSess.Close()
+	room.SessionManager.Remove(chatSess)
+	cr.MaybeRemoveRoom(room.ID)
+}

+ 3 - 3
oscar/bucp.go

@@ -19,7 +19,7 @@ const (
 	BUCPRegistrationImageRequest        = 0x000C
 )
 
-func routeBUCP(flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func routeBUCP(flap flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	switch snac.subGroup {
 	case BUCPErr:
 		panic("not implemented")
@@ -67,7 +67,7 @@ func (s *snacBUCPChallengeResponse) write(w io.Writer) error {
 }
 
 func ReceiveAndSendAuthChallenge(s *Session, r io.Reader, w io.Writer, sequence *uint32) error {
-	flap := &flapFrame{}
+	flap := flapFrame{}
 	if err := flap.read(r); err != nil {
 		return err
 	}
@@ -125,7 +125,7 @@ func (s *snacBUCPLoginRequest) read(r io.Reader) error {
 }
 
 func ReceiveAndSendBUCPLoginRequest(sess *Session, fm *FeedbagStore, r io.Reader, w io.Writer, sequence *uint32) error {
-	flap := &flapFrame{}
+	flap := flapFrame{}
 	if err := flap.read(r); err != nil {
 		return err
 	}

+ 4 - 4
oscar/buddy.go

@@ -22,7 +22,7 @@ const (
 	BuddyDelTempBuddies             = 0x0010
 )
 
-func routeBuddy(flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func routeBuddy(flap flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 
 	switch snac.subGroup {
 	case BuddyErr:
@@ -63,7 +63,7 @@ func (s *snacBuddyRights) read(r io.Reader) error {
 	})
 }
 
-func SendAndReceiveBuddyRights(flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func SendAndReceiveBuddyRights(flap flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("sendAndReceiveBuddyRights read SNAC frame: %+v\n", snac)
 
 	snacPayloadIn := &snacBuddyRights{}
@@ -155,7 +155,7 @@ func NotifyArrival(sess *Session, sm *SessionManager, fm *FeedbagStore) error {
 	sessions := sm.RetrieveByScreenNames(screenNames)
 
 	for _, adjSess := range sessions {
-		flap := &flapFrame{
+		flap := flapFrame{
 			startMarker: 42,
 			frameType:   2,
 		}
@@ -190,7 +190,7 @@ func NotifyDeparture(sess *Session, sm *SessionManager, fm *FeedbagStore) error
 
 	for _, adjSess := range sessions {
 		adjSess.SendMessage(&XMessage{
-			flap: &flapFrame{
+			flap: flapFrame{
 				startMarker: 42,
 				frameType:   2,
 			},

+ 338 - 0
oscar/chat.go

@@ -0,0 +1,338 @@
+package oscar
+
+import (
+	"encoding/binary"
+	"fmt"
+	"io"
+	"reflect"
+)
+
+const (
+	ChatErr                uint16 = 0x0001
+	ChatRoomInfoUpdate            = 0x0002
+	ChatUsersJoined               = 0x0003
+	ChatUsersLeft                 = 0x0004
+	ChatChannelMsgTohost          = 0x0005
+	ChatChannelMsgToclient        = 0x0006
+	ChatEvilRequest               = 0x0007
+	ChatEvilReply                 = 0x0008
+	ChatClientErr                 = 0x0009
+	ChatPauseRoomReq              = 0x000A
+	ChatPauseRoomAck              = 0x000B
+	ChatResumeRoom                = 0x000C
+	ChatShowMyRow                 = 0x000D
+	ChatShowRowByUsername         = 0x000E
+	ChatShowRowByNumber           = 0x000F
+	ChatShowRowByName             = 0x0010
+	ChatRowInfo                   = 0x0011
+	ChatListRows                  = 0x0012
+	ChatRowListInfo               = 0x0013
+	ChatMoreRows                  = 0x0014
+	ChatMoveToRow                 = 0x0015
+	ChatToggleChat                = 0x0016
+	ChatSendQuestion              = 0x0017
+	ChatSendComment               = 0x0018
+	ChatTallyVote                 = 0x0019
+	ChatAcceptBid                 = 0x001A
+	ChatSendInvite                = 0x001B
+	ChatDeclineInvite             = 0x001C
+	ChatAcceptInvite              = 0x001D
+	ChatNotifyMessage             = 0x001E
+	ChatGotoRow                   = 0x001F
+	ChatStageUserJoin             = 0x0020
+	ChatStageUserLeft             = 0x0021
+	ChatUnnamedSnac22             = 0x0022
+	ChatClose                     = 0x0023
+	ChatUserBan                   = 0x0024
+	ChatUserUnban                 = 0x0025
+	ChatJoined                    = 0x0026
+	ChatUnnamedSnac27             = 0x0027
+	ChatUnnamedSnac28             = 0x0028
+	ChatUnnamedSnac29             = 0x0029
+	ChatRoomInfoOwner             = 0x0030
+)
+
+func routeChat(cr *ChatRegistry, sess *Session, sm *SessionManager, flap flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+	switch snac.subGroup {
+	case ChatErr:
+		panic("not implemented")
+	case ChatRoomInfoUpdate:
+		panic("not implemented")
+	case ChatUsersJoined:
+		panic("not implemented")
+	case ChatUsersLeft:
+		panic("not implemented")
+	case ChatChannelMsgTohost:
+		return SendAndReceiveChatChannelMsgTohost(sess, sm, flap, snac, r, w, sequence)
+	case ChatChannelMsgToclient:
+		panic("not implemented")
+	case ChatEvilRequest:
+		panic("not implemented")
+	case ChatEvilReply:
+		panic("not implemented")
+	case ChatClientErr:
+		panic("not implemented")
+	case ChatPauseRoomReq:
+		panic("not implemented")
+	case ChatPauseRoomAck:
+		panic("not implemented")
+	case ChatResumeRoom:
+		panic("not implemented")
+	case ChatShowMyRow:
+		panic("not implemented")
+	case ChatShowRowByUsername:
+		panic("not implemented")
+	case ChatShowRowByNumber:
+		panic("not implemented")
+	case ChatShowRowByName:
+		panic("not implemented")
+	case ChatRowInfo:
+		panic("not implemented")
+	case ChatListRows:
+		panic("not implemented")
+	case ChatRowListInfo:
+		panic("not implemented")
+	case ChatMoreRows:
+		panic("not implemented")
+	case ChatMoveToRow:
+		panic("not implemented")
+	case ChatToggleChat:
+		panic("not implemented")
+	case ChatSendQuestion:
+		panic("not implemented")
+	case ChatSendComment:
+		panic("not implemented")
+	case ChatTallyVote:
+		panic("not implemented")
+	case ChatAcceptBid:
+		panic("not implemented")
+	case ChatSendInvite:
+		panic("not implemented")
+	case ChatDeclineInvite:
+		panic("not implemented")
+	case ChatAcceptInvite:
+		panic("not implemented")
+	case ChatNotifyMessage:
+		panic("not implemented")
+	case ChatGotoRow:
+		panic("not implemented")
+	case ChatStageUserJoin:
+		panic("not implemented")
+	case ChatStageUserLeft:
+		panic("not implemented")
+	case ChatUnnamedSnac22:
+		panic("not implemented")
+	case ChatClose:
+		panic("not implemented")
+	case ChatUserBan:
+		panic("not implemented")
+	case ChatUserUnban:
+		panic("not implemented")
+	case ChatJoined:
+		panic("not implemented")
+	case ChatUnnamedSnac27:
+		panic("not implemented")
+	case ChatUnnamedSnac28:
+		panic("not implemented")
+	case ChatUnnamedSnac29:
+		panic("not implemented")
+	case ChatRoomInfoOwner:
+		panic("not implemented")
+	}
+	return nil
+}
+
+type snacChatMessage struct {
+	cookie  uint64
+	channel uint16
+	TLVPayload
+}
+
+func (s *snacChatMessage) read(r io.Reader) error {
+	if err := binary.Read(r, binary.BigEndian, &s.cookie); err != nil {
+		return err
+	}
+	if err := binary.Read(r, binary.BigEndian, &s.channel); err != nil {
+		return err
+	}
+	return s.TLVPayload.read(r, map[uint16]reflect.Kind{
+		0x01: reflect.Slice,
+		0x06: reflect.Slice,
+		0x05: reflect.Slice,
+	})
+}
+
+func (s *snacChatMessage) write(w io.Writer) error {
+	if err := binary.Write(w, binary.BigEndian, s.cookie); err != nil {
+		return err
+	}
+	if err := binary.Write(w, binary.BigEndian, s.channel); err != nil {
+		return err
+	}
+	return s.TLVPayload.write(w)
+}
+
+type senders []*snacSenderInfo
+
+func (s senders) write(w io.Writer) error {
+	for _, sender := range s {
+		if err := sender.write(w); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+type snacSenderInfo struct {
+	screenName   string
+	warningLevel uint16
+	TLVPayload
+}
+
+func (f *snacSenderInfo) write(w io.Writer) error {
+	if err := binary.Write(w, binary.BigEndian, uint8(len(f.screenName))); err != nil {
+		return err
+	}
+	if err := binary.Write(w, binary.BigEndian, []byte(f.screenName)); err != nil {
+		return err
+	}
+	if err := binary.Write(w, binary.BigEndian, f.warningLevel); err != nil {
+		return err
+	}
+	if err := binary.Write(w, binary.BigEndian, uint16(len(f.TLVs))); err != nil {
+		return err
+	}
+	return f.TLVPayload.write(w)
+}
+
+func SendAndReceiveChatChannelMsgTohost(sess *Session, sm *SessionManager, flap flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+	fmt.Printf("SendAndReceiveChatChannelMsgTohost read SNAC frame: %+v\n", snac)
+
+	snacPayloadIn := snacChatMessage{}
+	if err := snacPayloadIn.read(r); err != nil {
+		return err
+	}
+
+	snacFrameOut := snacFrame{
+		foodGroup: CHAT,
+		subGroup:  ChatChannelMsgToclient,
+	}
+	snacPayloadOut := &snacChatMessage{
+		cookie:     snacPayloadIn.cookie,
+		channel:    snacPayloadIn.channel,
+		TLVPayload: TLVPayload{snacPayloadIn.TLVs},
+	}
+
+	snacPayloadOut.addTLV(&TLV{
+		tType: 0x03,
+		val: &snacSenderInfo{
+			screenName:   sess.ScreenName,
+			warningLevel: sess.GetWarning(),
+			TLVPayload:   TLVPayload{sess.GetUserInfo()},
+		},
+	})
+
+	// send message to all the participants except sender
+	sm.BroadcastExcept(sess, &XMessage{
+		flap:      flap,
+		snacFrame: snacFrameOut,
+		snacOut:   snacPayloadOut,
+	})
+
+	if _, ackMsg := snacPayloadIn.getTLV(0x06); !ackMsg {
+		return nil
+	}
+
+	// reflect the message back to the sender
+	return writeOutSNAC(snac, flap, snacFrameOut, snacPayloadOut, sequence, w)
+}
+
+func SetOnlineChatUsers(sm *SessionManager, w io.Writer, sequence *uint32) error {
+	flap := flapFrame{
+		startMarker: 42,
+		frameType:   2,
+	}
+	snacFrameOut := snacFrame{
+		foodGroup: CHAT,
+		subGroup:  ChatUsersJoined,
+	}
+	snacPayloadOut := senders{}
+
+	sessions := sm.All()
+
+	for _, uSess := range sessions {
+		if !uSess.Ready() {
+			continue
+		}
+		snacPayloadOut = append(snacPayloadOut, &snacSenderInfo{
+			screenName:   uSess.ScreenName,
+			warningLevel: uSess.GetWarning(),
+			TLVPayload: TLVPayload{
+				TLVs: uSess.GetUserInfo(),
+			},
+		})
+	}
+
+	return writeOutSNAC(nil, flap, snacFrameOut, snacPayloadOut, sequence, w)
+}
+
+func AlertUserJoined(sess *Session, sm *SessionManager) {
+	sm.BroadcastExcept(sess, &XMessage{
+		flap: flapFrame{
+			startMarker: 42,
+			frameType:   2,
+		},
+		snacFrame: snacFrame{
+			foodGroup: CHAT,
+			subGroup:  ChatUsersJoined,
+		},
+		snacOut: &snacSenderInfo{
+			screenName:   sess.ScreenName,
+			warningLevel: sess.GetWarning(),
+			TLVPayload: TLVPayload{
+				TLVs: sess.GetUserInfo(),
+			},
+		},
+	})
+}
+
+func AlertUserLeft(sess *Session, sm *SessionManager) {
+	sm.BroadcastExcept(sess, &XMessage{
+		flap: flapFrame{
+			startMarker: 42,
+			frameType:   2,
+		},
+		snacFrame: snacFrame{
+			foodGroup: CHAT,
+			subGroup:  ChatUsersLeft,
+		},
+		snacOut: &snacSenderInfo{
+			screenName:   sess.ScreenName,
+			warningLevel: sess.GetWarning(),
+			TLVPayload: TLVPayload{
+				TLVs: sess.GetUserInfo(),
+			},
+		},
+	})
+}
+
+func SendChatRoomInfoUpdate(room ChatRoom, w io.Writer, sequence *uint32) error {
+	flap := flapFrame{
+		startMarker: 42,
+		frameType:   2,
+	}
+	snacFrameOut := snacFrame{
+		foodGroup: CHAT,
+		subGroup:  ChatRoomInfoUpdate,
+	}
+	snacPayloadOut := &snacCreateRoom{
+		exchange:       4,
+		cookie:         []byte(room.ID),
+		instanceNumber: 100,
+		detailLevel:    2,
+		TLVPayload: TLVPayload{
+			TLVs: room.TLVList(),
+		},
+	}
+	return writeOutSNAC(nil, flap, snacFrameOut, snacPayloadOut, sequence, w)
+}

+ 305 - 10
oscar/chat_nav.go

@@ -1,8 +1,13 @@
 package oscar
 
 import (
+	"encoding/binary"
+	"errors"
 	"fmt"
+	"github.com/google/uuid"
 	"io"
+	"reflect"
+	"time"
 )
 
 const (
@@ -17,7 +22,7 @@ const (
 	ChatNavNavInfo                    = 0x0009
 )
 
-func routeChatNav(flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func routeChatNav(sess *Session, cr *ChatRegistry, flap flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	switch snac.subGroup {
 	case ChatNavErr:
 		panic("not implemented")
@@ -26,7 +31,7 @@ func routeChatNav(flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, se
 	case ChatNavRequestExchangeInfo:
 		panic("not implemented")
 	case ChatNavRequestRoomInfo:
-		panic("not implemented")
+		return SendAndReceiveChatNav(cr, flap, snac, r, w, sequence)
 	case ChatNavRequestMoreRoomInfo:
 		panic("not implemented")
 	case ChatNavRequestOccupantList:
@@ -34,29 +39,319 @@ func routeChatNav(flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, se
 	case ChatNavSearchForRoom:
 		panic("not implemented")
 	case ChatNavCreateRoom:
-		panic("not implemented")
+		return SendAndReceiveCreateRoom(sess, cr, flap, snac, r, w, sequence)
 	case ChatNavNavInfo:
 		panic("not implemented")
 	}
 	return nil
 }
 
-func SendAndReceiveNextChatRights(flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+type ChatCookie struct {
+	Cookie []byte
+	SessID string
+}
+
+func (s *ChatCookie) read(r io.Reader) error {
+	var l uint16
+	if err := binary.Read(r, binary.BigEndian, &l); err != nil {
+		return err
+	}
+	s.Cookie = make([]byte, l)
+	if _, err := r.Read(s.Cookie); err != nil {
+		return err
+	}
+	if err := binary.Read(r, binary.BigEndian, &l); err != nil {
+		return err
+	}
+	buf := make([]byte, l)
+	if _, err := r.Read(buf); err != nil {
+		return err
+	}
+	s.SessID = string(buf)
+	return nil
+}
+
+func (s *ChatCookie) write(w io.Writer) error {
+	if err := binary.Write(w, binary.BigEndian, uint16(len(s.Cookie))); err != nil {
+		return err
+	}
+	if err := binary.Write(w, binary.BigEndian, s.Cookie); err != nil {
+		return err
+	}
+	if err := binary.Write(w, binary.BigEndian, uint16(len(s.SessID))); err != nil {
+		return err
+	}
+	if err := binary.Write(w, binary.BigEndian, []byte(s.SessID)); err != nil {
+		return err
+	}
+	return nil
+}
+
+type exchangeInfo struct {
+	identifier uint16
+	TLVPayload
+}
+
+func (s *exchangeInfo) write(w io.Writer) error {
+	if err := binary.Write(w, binary.BigEndian, s.identifier); err != nil {
+		return err
+	}
+	if err := binary.Write(w, binary.BigEndian, uint16(len(s.TLVs))); err != nil {
+		return err
+	}
+	return s.TLVPayload.write(w)
+}
+
+func SendAndReceiveNextChatRights(flap flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("sendAndReceiveNextChatRights read SNAC frame: %+v\n", snac)
 
-	snacPayload := &snacFrame{}
-	if err := snacPayload.read(r); err != nil {
+	snacFrameOut := snacFrame{
+		foodGroup: CHAT_NAV,
+		subGroup:  ChatNavNavInfo,
+	}
+
+	snacPayloadOut := &TLVPayload{
+		TLVs: []*TLV{
+			{
+				tType: 0x02,
+				val:   uint8(10),
+			},
+			{
+				tType: 0x03,
+				val: &exchangeInfo{
+					identifier: 4,
+					TLVPayload: TLVPayload{
+						TLVs: []*TLV{
+							{
+								tType: 0x0002,
+								val:   uint16(0x0010),
+							},
+							{
+								tType: 0x00c9,
+								val:   uint16(15),
+							},
+							{
+								tType: 0x00d3,
+								val:   "default exchange",
+							},
+							{
+								tType: 0x00d5,
+								val:   uint8(2),
+							},
+							{
+								tType: 0xd6,
+								val:   "us-ascii",
+							},
+							{
+								tType: 0xd7,
+								val:   "en",
+							},
+							{
+								tType: 0xd8,
+								val:   "us-ascii",
+							},
+							{
+								tType: 0xd9,
+								val:   "en",
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	return writeOutSNAC(snac, flap, snacFrameOut, snacPayloadOut, sequence, w)
+}
+
+type snacCreateRoom struct {
+	exchange       uint16
+	cookie         []byte
+	instanceNumber uint16
+	detailLevel    uint8
+	TLVPayload
+}
+
+func (s *snacCreateRoom) read(r io.Reader) error {
+	if err := binary.Read(r, binary.BigEndian, &s.exchange); err != nil {
+		return err
+	}
+	var l uint8
+	if err := binary.Read(r, binary.BigEndian, &l); err != nil {
+		return err
+	}
+	s.cookie = make([]byte, l)
+	if _, err := r.Read(s.cookie); err != nil {
+		return err
+	}
+	if err := binary.Read(r, binary.BigEndian, &s.instanceNumber); err != nil {
+		return err
+	}
+	if err := binary.Read(r, binary.BigEndian, &s.detailLevel); err != nil {
+		return err
+	}
+
+	var tlvCount uint16
+	if err := binary.Read(r, binary.BigEndian, &tlvCount); err != nil {
+		return err
+	}
+
+	return s.TLVPayload.read(r, map[uint16]reflect.Kind{
+		0x00d3: reflect.String, // name
+		0x00d6: reflect.String, // charset
+		0x00d7: reflect.String, // lang
+	})
+}
+
+func (s *snacCreateRoom) write(w io.Writer) error {
+	if err := binary.Write(w, binary.BigEndian, s.exchange); err != nil {
+		return err
+	}
+	if err := binary.Write(w, binary.BigEndian, uint8(len(s.cookie))); err != nil {
+		return err
+	}
+	if err := binary.Write(w, binary.BigEndian, s.cookie); err != nil {
+		return err
+	}
+	if err := binary.Write(w, binary.BigEndian, s.instanceNumber); err != nil {
+		return err
+	}
+	if err := binary.Write(w, binary.BigEndian, s.detailLevel); err != nil {
+		return err
+	}
+	if err := binary.Write(w, binary.BigEndian, uint16(len(s.TLVs))); err != nil {
+		return err
+	}
+	return s.TLVPayload.write(w)
+}
+
+func SendAndReceiveCreateRoom(sess *Session, cr *ChatRegistry, flap flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+	fmt.Printf("SendAndReceiveCreateRoom read SNAC frame: %+v\n", snac)
+
+	snacPayloadIn := &snacCreateRoom{}
+	if err := snacPayloadIn.read(r); err != nil {
 		return err
 	}
 
-	fmt.Printf("sendAndReceiveNextChatRights read SNAC payload: %+v\n", snac)
+	name, hasName := snacPayloadIn.getString(0x00d3)
+	if !hasName {
+		return errors.New("unable to find chat name")
+	}
+
+	room := ChatRoom{
+		ID:             uuid.New().String(),
+		SessionManager: NewSessionManager(),
+		CreateTime:     time.Now(),
+		Name:           name,
+	}
+	cr.Register(room)
+
+	// add user to chat room
+	room.SessionManager.NewSessionWithSN(sess.ID, sess.ScreenName)
 
 	snacFrameOut := snacFrame{
-		foodGroup: 0x0D,
-		subGroup:  0x09,
+		foodGroup: CHAT_NAV,
+		subGroup:  ChatNavNavInfo,
+	}
+	snacPayloadOut := &TLVPayload{
+		TLVs: []*TLV{
+			{
+				tType: 0x04,
+				val: &snacCreateRoom{
+					exchange:       4,
+					cookie:         []byte(room.ID),
+					instanceNumber: 100,
+					detailLevel:    2,
+					TLVPayload: TLVPayload{
+						TLVs: room.TLVList(),
+					},
+				},
+			},
+		},
 	}
+
+	return writeOutSNAC(snac, flap, snacFrameOut, snacPayloadOut, sequence, w)
+}
+
+type roomInfoOService struct {
+	exchange       uint16
+	cookie         []byte
+	instanceNumber uint16
+}
+
+func (s *roomInfoOService) read(r io.Reader) error {
+	if err := binary.Read(r, binary.BigEndian, &s.exchange); err != nil {
+		return err
+	}
+	var l uint8
+	if err := binary.Read(r, binary.BigEndian, &l); err != nil {
+		return err
+	}
+	s.cookie = make([]byte, l)
+	if _, err := r.Read(s.cookie); err != nil {
+		return err
+	}
+	return binary.Read(r, binary.BigEndian, &s.instanceNumber)
+}
+
+type roomInfo struct {
+	exchange       uint16
+	cookie         []byte
+	instanceNumber uint16
+	detailLevel    uint8
+}
+
+func (s *roomInfo) read(r io.Reader) error {
+	if err := binary.Read(r, binary.BigEndian, &s.exchange); err != nil {
+		return err
+	}
+	var l uint8
+	if err := binary.Read(r, binary.BigEndian, &l); err != nil {
+		return err
+	}
+	s.cookie = make([]byte, l)
+	if _, err := r.Read(s.cookie); err != nil {
+		return err
+	}
+	if err := binary.Read(r, binary.BigEndian, &s.instanceNumber); err != nil {
+		return err
+	}
+	return binary.Read(r, binary.BigEndian, &s.detailLevel)
+}
+
+func SendAndReceiveChatNav(cr *ChatRegistry, flap flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+	fmt.Printf("SendAndReceiveChatNav read SNAC frame: %+v\n", snac)
+
+	snacPayloadIn := &roomInfo{}
+	if err := snacPayloadIn.read(r); err != nil {
+		return err
+	}
+
+	room, err := cr.Retrieve(string(snacPayloadIn.cookie))
+	if err != nil {
+		return err
+	}
+
+	snacFrameOut := snacFrame{
+		foodGroup: CHAT_NAV,
+		subGroup:  ChatNavNavInfo,
+	}
+
 	snacPayloadOut := &TLVPayload{
-		TLVs: []*TLV{},
+		TLVs: []*TLV{
+			{
+				tType: 0x04,
+				val: &snacCreateRoom{
+					exchange:       4,
+					cookie:         []byte(room.ID),
+					instanceNumber: 100,
+					detailLevel:    2,
+					TLVPayload: TLVPayload{
+						TLVs: room.TLVList(),
+					},
+				},
+			},
+		},
 	}
 
 	return writeOutSNAC(snac, flap, snacFrameOut, snacPayloadOut, sequence, w)

+ 12 - 12
oscar/feedbag.go

@@ -146,7 +146,7 @@ const (
 	FeedbagClassIdMin                     = 0x0400
 )
 
-func routeFeedbag(sm *SessionManager, sess *Session, fm *FeedbagStore, flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func routeFeedbag(sm *SessionManager, sess *Session, fm *FeedbagStore, flap flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	switch snac.subGroup {
 	case FeedbagErr:
 		panic("not implemented")
@@ -231,7 +231,7 @@ func (s *payloadFeedbagRightsQuery) read(r io.Reader) error {
 	})
 }
 
-func SendAndReceiveFeedbagRightsQuery(flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func SendAndReceiveFeedbagRightsQuery(flap flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("sendAndReceiveFeedbagRightsQuery read SNAC frame: %+v\n", snac)
 
 	snacPayloadIn := &payloadFeedbagRightsQuery{}
@@ -413,7 +413,7 @@ func (s *snacFeedbagQuery) write(w io.Writer) error {
 	return binary.Write(w, binary.BigEndian, s.lastUpdate)
 }
 
-func ReceiveAndSendFeedbagQuery(sess *Session, fm *FeedbagStore, flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func ReceiveAndSendFeedbagQuery(sess *Session, fm *FeedbagStore, flap flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("receiveAndSendFeedbagQuery read SNAC frame: %+v\n", snac)
 
 	fb, err := fm.Retrieve(sess.ScreenName)
@@ -462,7 +462,7 @@ func (s *snacQueryIfModified) read(r io.Reader) error {
 	return binary.Read(r, binary.BigEndian, &s.count)
 }
 
-func ReceiveAndSendFeedbagQueryIfModified(sess *Session, fm *FeedbagStore, flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func ReceiveAndSendFeedbagQueryIfModified(sess *Session, fm *FeedbagStore, flap flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("ReceiveAndSendFeedbagQueryIfModified read SNAC frame: %+v\n", snac)
 
 	snacPayload := &snacQueryIfModified{}
@@ -520,7 +520,7 @@ func (s *snacFeedbagStatusReply) write(w io.Writer) error {
 	return binary.Write(w, binary.BigEndian, s.results)
 }
 
-func ReceiveInsertItem(sm *SessionManager, sess *Session, fm *FeedbagStore, flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func ReceiveInsertItem(sm *SessionManager, sess *Session, fm *FeedbagStore, flap flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("ReceiveInsertItem read SNAC frame: %+v\n", snac)
 
 	snacPayloadOut := &snacFeedbagStatusReply{}
@@ -564,7 +564,7 @@ func ReceiveInsertItem(sm *SessionManager, sess *Session, fm *FeedbagStore, flap
 				return err
 			}
 			buddySess.SendMessage(&XMessage{
-				flap: &flapFrame{
+				flap: flapFrame{
 					startMarker: 42,
 					frameType:   2,
 				},
@@ -578,7 +578,7 @@ func ReceiveInsertItem(sm *SessionManager, sess *Session, fm *FeedbagStore, flap
 				},
 			})
 			sess.SendMessage(&XMessage{
-				flap: &flapFrame{
+				flap: flapFrame{
 					startMarker: 42,
 					frameType:   2,
 				},
@@ -598,7 +598,7 @@ func ReceiveInsertItem(sm *SessionManager, sess *Session, fm *FeedbagStore, flap
 	return GetOnlineBuddies(w, sess, sm, fm, sequence)
 }
 
-func ReceiveUpdateItem(sm *SessionManager, sess *Session, fm *FeedbagStore, flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func ReceiveUpdateItem(sm *SessionManager, sess *Session, fm *FeedbagStore, flap flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("ReceiveUpdateItem read SNAC frame: %+v\n", snac)
 
 	var items []*feedbagItem
@@ -638,7 +638,7 @@ func ReceiveUpdateItem(sm *SessionManager, sess *Session, fm *FeedbagStore, flap
 	return GetOnlineBuddies(w, sess, sm, fm, sequence)
 }
 
-func ReceiveDeleteItem(sm *SessionManager, sess *Session, fm *FeedbagStore, flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func ReceiveDeleteItem(sm *SessionManager, sess *Session, fm *FeedbagStore, flap flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("ReceiveUpdateItem read SNAC frame: %+v\n", snac)
 
 	var items []*feedbagItem
@@ -685,7 +685,7 @@ func ReceiveDeleteItem(sm *SessionManager, sess *Session, fm *FeedbagStore, flap
 	return writeOutSNAC(snac, flap, snacFrameOut, snacPayloadOut, sequence, w)
 }
 
-func ReceiveFeedbagStartCluster(flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func ReceiveFeedbagStartCluster(flap flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("ReceiveFeedbagStartCluster read SNAC frame: %+v\n", snac)
 
 	tlv := &TLVPayload{}
@@ -696,12 +696,12 @@ func ReceiveFeedbagStartCluster(flap *flapFrame, snac *snacFrame, r io.Reader, w
 	return nil
 }
 
-func ReceiveFeedbagEndCluster(flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func ReceiveFeedbagEndCluster(flap flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("receiveFeedbagEndCluster read SNAC frame: %+v\n", snac)
 	return nil
 }
 
-func ReceiveUse(flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func ReceiveUse(flap flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("ReceiveUse read SNAC frame: %+v\n", snac)
 	return nil
 }

+ 26 - 19
oscar/icbm.go

@@ -33,7 +33,7 @@ const (
 	ICBMSinReply                  = 0x0017
 )
 
-func routeICBM(sm *SessionManager, fm *FeedbagStore, sess *Session, flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func routeICBM(sm *SessionManager, fm *FeedbagStore, sess *Session, flap flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	switch snac.subGroup {
 	case ICBMErr:
 		panic("not implemented")
@@ -140,7 +140,7 @@ func (s *snacParameterResponse) write(w io.Writer) error {
 	return nil
 }
 
-func SendAndReceiveICBMParameterReply(flap *flapFrame, snac *snacFrame, _ io.Reader, w io.Writer, sequence *uint32) error {
+func SendAndReceiveICBMParameterReply(flap flapFrame, snac *snacFrame, _ io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("sendAndReceiveICBMParameterReply read SNAC frame: %+v\n", snac)
 
 	snacFrameOut := snacFrame{
@@ -188,6 +188,7 @@ func (s *snacMessageToHost) read(r io.Reader) error {
 		0x02: reflect.Slice,
 		0x03: reflect.Slice,
 		0x04: reflect.Slice,
+		0x05: reflect.Slice,
 	})
 }
 
@@ -213,7 +214,7 @@ func (f *snacHostAck) write(w io.Writer) error {
 	return nil
 }
 
-func SendAndReceiveChannelMsgTohost(sm *SessionManager, fm *FeedbagStore, sess *Session, flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func SendAndReceiveChannelMsgTohost(sm *SessionManager, fm *FeedbagStore, sess *Session, flap flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("SendAndReceiveChannelMsgTohost read SNAC frame: %+v\n", snac)
 
 	snacPayloadIn := &snacMessageToHost{}
@@ -254,15 +255,10 @@ func SendAndReceiveChannelMsgTohost(sm *SessionManager, fm *FeedbagStore, sess *
 		return err
 	}
 
-	messagePayload, found := snacPayloadIn.TLVPayload.getSlice(0x02)
-	if !found {
-		return errors.New("unable to find message data tlv")
-	}
-
 	_, requestedConfirmation := snacPayloadIn.TLVPayload.getSlice(0x03)
 
 	mm := &XMessage{
-		flap: &flapFrame{
+		flap: flapFrame{
 			startMarker: 42,
 			frameType:   2,
 		},
@@ -281,14 +277,25 @@ func SendAndReceiveChannelMsgTohost(sm *SessionManager, fm *FeedbagStore, sess *
 						tType: 0x0B,
 						val:   []byte{},
 					},
-					{
-						tType: 0x02,
-						val:   messagePayload,
-					},
 				},
 			},
 		},
 	}
+	if messagePayload, found := snacPayloadIn.TLVPayload.getSlice(0x02); found {
+		mm.snacOut.(*snacClientIM).TLVs = append(mm.snacOut.(*snacClientIM).TLVs,
+			&TLV{
+				tType: 0x02,
+				val:   messagePayload,
+			})
+	}
+
+	if messagePayload, found := snacPayloadIn.TLVPayload.getSlice(0x05); found {
+		mm.snacOut.(*snacClientIM).TLVs = append(mm.snacOut.(*snacClientIM).TLVs,
+			&TLV{
+				tType: 0x05,
+				val:   messagePayload,
+			})
+	}
 
 	// todo: add append to TLVPayload?
 	if t, hasAutoResp := snacPayloadIn.getTLV(0x04); hasAutoResp {
@@ -314,7 +321,7 @@ func SendAndReceiveChannelMsgTohost(sm *SessionManager, fm *FeedbagStore, sess *
 	}
 }
 
-func ReceiveAddParameters(flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func ReceiveAddParameters(flap flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("ReceiveAddParameters read SNAC frame: %+v\n", snac)
 
 	snacPayload := &snacParameterRequest{}
@@ -448,7 +455,7 @@ func (s *snacClientErr) read(r io.Reader) error {
 	return err
 }
 
-func ReceiveClientErr(flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func ReceiveClientErr(flap flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("ReceiveClientErr read SNAC frame: %+v\n", snac)
 
 	snacPayload := &snacClientErr{}
@@ -504,7 +511,7 @@ func (s *sncClientEvent) write(w io.Writer) error {
 	return binary.Write(w, binary.BigEndian, s.event)
 }
 
-func SendAndReceiveClientEvent(sm *SessionManager, fm *FeedbagStore, sess *Session, flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func SendAndReceiveClientEvent(sm *SessionManager, fm *FeedbagStore, sess *Session, flap flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("SendAndReceiveClientEvent read SNAC frame: %+v\n", snac)
 
 	snacPayloadIn := &sncClientEvent{}
@@ -528,7 +535,7 @@ func SendAndReceiveClientEvent(sm *SessionManager, fm *FeedbagStore, sess *Sessi
 	snacPayloadIn.screenName = sess.ScreenName
 
 	session.SendMessage(&XMessage{
-		flap: &flapFrame{
+		flap: flapFrame{
 			startMarker: 42,
 			frameType:   2,
 		},
@@ -592,7 +599,7 @@ const (
 	evilDeltaAnon = uint16(30)
 )
 
-func SendAndReceiveEvilRequest(sm *SessionManager, fm *FeedbagStore, sess *Session, flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func SendAndReceiveEvilRequest(sm *SessionManager, fm *FeedbagStore, sess *Session, flap flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("SendAndReceiveEvilRequest read SNAC frame: %+v\n", snac)
 
 	snacPayloadIn := &snacEvilRequest{}
@@ -640,7 +647,7 @@ func SendAndReceiveEvilRequest(sm *SessionManager, fm *FeedbagStore, sess *Sessi
 	}
 
 	mm := &XMessage{
-		flap: &flapFrame{
+		flap: flapFrame{
 			startMarker: 42,
 			frameType:   2,
 		},

+ 7 - 7
oscar/locate.go

@@ -32,7 +32,7 @@ const (
 	LocateUserInfoQuery2              = 0x0015
 )
 
-func routeLocate(sess *Session, sm *SessionManager, fm *FeedbagStore, flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func routeLocate(sess *Session, sm *SessionManager, fm *FeedbagStore, flap flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	switch snac.subGroup {
 	case LocateErr:
 		panic("not implemented")
@@ -83,7 +83,7 @@ func (s *snacLocateRightsReply) write(w io.Writer) error {
 	return s.TLVPayload.write(w)
 }
 
-func SendAndReceiveLocateRights(flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func SendAndReceiveLocateRights(flap flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("sendAndReceiveLocateRights read SNAC frame: %+v\n", snac)
 
 	snacFrameOut := snacFrame{
@@ -134,7 +134,7 @@ var (
 	LocateTlvTagsInfoHtmlInfoType    = uint16(0x0D)
 )
 
-func ReceiveSetInfo(sess *Session, sm *SessionManager, fm *FeedbagStore, flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func ReceiveSetInfo(sess *Session, sm *SessionManager, fm *FeedbagStore, flap flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("ReceiveSetInfo read SNAC frame: %+v\n", snac)
 
 	snacPayload := &TLVPayload{}
@@ -197,7 +197,7 @@ func (s *snacDirInfo) read(r io.Reader) error {
 	return nil
 }
 
-func ReceiveLocateGetDirInfo(flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func ReceiveLocateGetDirInfo(flap flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("ReceiveLocateGetDirInfo read SNAC frame: %+v\n", snac)
 
 	snacPayload := &snacDirInfo{}
@@ -261,7 +261,7 @@ func (f *snacUserInfoReply) write(w io.Writer) error {
 	return f.awayMessage.write(w)
 }
 
-func SendAndReceiveUserInfoQuery2(sess *Session, sm *SessionManager, fm *FeedbagStore, flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func SendAndReceiveUserInfoQuery2(sess *Session, sm *SessionManager, fm *FeedbagStore, flap flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("SendAndReceiveUserInfoQuery2 read SNAC frame: %+v\n", snac)
 
 	snacPayloadIn := &snacUserInfoQuery2{}
@@ -385,7 +385,7 @@ func (s *snacSetDirInfoReply) write(w io.Writer) error {
 	return binary.Write(w, binary.BigEndian, s.result)
 }
 
-func SendAndReceiveSetDirInfo(flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func SendAndReceiveSetDirInfo(flap flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("SendAndReceiveSetDirInfo read SNAC frame: %+v\n", snac)
 
 	snacPayloadIn := &snacSetDirInfo{}
@@ -421,7 +421,7 @@ func (s *snacSetKeywordInfoReply) write(w io.Writer) error {
 	return binary.Write(w, binary.BigEndian, s.unknown)
 }
 
-func SendAndReceiveSetKeywordInfo(flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func SendAndReceiveSetKeywordInfo(flap flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("SendAndReceiveSetKeywordInfo read SNAC frame: %+v\n", snac)
 
 	snacPayloadIn := &snacSetKeywordInfo{}

+ 79 - 25
oscar/oservice.go

@@ -7,6 +7,7 @@ import (
 	"fmt"
 	"io"
 	"reflect"
+	"sync"
 	"time"
 )
 
@@ -48,16 +49,18 @@ const (
 	OServiceBartReply2               = 0x0023
 )
 
-func routeOService(sm *SessionManager, fm *FeedbagStore, sess *Session, flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func routeOService(ready OnReadyCB, wg *sync.WaitGroup, cr *ChatRegistry, sm *SessionManager, fm *FeedbagStore, sess *Session, flap flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	switch snac.subGroup {
 	case OServiceErr:
 		panic("not implemented")
 	case OServiceClientOnline:
-		return ReceiveClientOnline(sess, sm, fm, flap, snac, r, w, sequence)
+		wg.Done()
+		sess.SetReady()
+		return ReceiveClientOnline(ready, sess, sm, flap, snac, r, w, sequence)
 	case OServiceHostOnline:
 		panic("not implemented")
 	case OServiceServiceRequest:
-		return ReceiveAndSendServiceRequest(sess, flap, snac, r, w, sequence)
+		return ReceiveAndSendServiceRequest(cr, sess, flap, snac, r, w, sequence)
 	case OServiceRateParamsQuery:
 		return ReceiveAndSendServiceRateParams(flap, snac, r, w, sequence)
 	case OServiceRateParamsSubAdd:
@@ -145,7 +148,7 @@ func (s *snac01_03) write(w io.Writer) error {
 	return nil
 }
 
-func WriteOServiceHostOnline(rw io.ReadWriter, sequence *uint32) error {
+func WriteOServiceHostOnline(foodGroups []uint16, rw io.ReadWriter, sequence *uint32) error {
 	fmt.Println("writeOServiceHostOnline...")
 
 	snac := &snac01_03{
@@ -153,9 +156,7 @@ func WriteOServiceHostOnline(rw io.ReadWriter, sequence *uint32) error {
 			foodGroup: 0x01,
 			subGroup:  0x03,
 		},
-		foodGroups: []uint16{
-			0x0001, 0x0002, 0x0003, 0x0004, 0x0009, 0x0013,
-		},
+		foodGroups: foodGroups,
 	}
 
 	fmt.Printf("writeOServiceHostOnline SNAC: %+v\n", snac)
@@ -165,7 +166,7 @@ func WriteOServiceHostOnline(rw io.ReadWriter, sequence *uint32) error {
 		return err
 	}
 
-	flap := &flapFrame{
+	flap := flapFrame{
 		startMarker:   42,
 		frameType:     2,
 		sequence:      uint16(*sequence),
@@ -216,7 +217,7 @@ func (s *snacVersions) write(w io.Writer) error {
 	return nil
 }
 
-func ReceiveAndSendHostVersions(flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func ReceiveAndSendHostVersions(flap flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("receiveAndSendHostVersions read SNAC frame: %+v\n", snac)
 
 	snacPayload := &snacVersions{
@@ -318,7 +319,7 @@ func (s *snacOServiceRateParamsReply) write(w io.Writer) error {
 	return nil
 }
 
-func ReceiveAndSendServiceRateParams(flap *flapFrame, snac *snacFrame, _ io.Reader, w io.Writer, sequence *uint32) error {
+func ReceiveAndSendServiceRateParams(flap flapFrame, snac *snacFrame, _ io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("receiveAndSendServiceRateParams read SNAC frame: %+v\n", snac)
 
 	snacFrameOut := snacFrame{
@@ -389,7 +390,7 @@ func (s *snacOServiceUserInfoUpdate) write(w io.Writer) error {
 	return s.TLVPayload.write(w)
 }
 
-func ReceiveAndSendServiceRequestSelfInfo(sess *Session, flap *flapFrame, snac *snacFrame, _ io.Reader, w io.Writer, sequence *uint32) error {
+func ReceiveAndSendServiceRequestSelfInfo(sess *Session, flap flapFrame, snac *snacFrame, _ io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("receiveAndSendServiceRequestSelfInfo read SNAC frame: %+v\n", snac)
 
 	snacFrameOut := snacFrame{
@@ -407,7 +408,7 @@ func ReceiveAndSendServiceRequestSelfInfo(sess *Session, flap *flapFrame, snac *
 	return writeOutSNAC(snac, flap, snacFrameOut, snacPayloadOut, sequence, w)
 }
 
-func ReceiveRateParamsSubAdd(flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func ReceiveRateParamsSubAdd(flap flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("receiveRateParamsSubAdd read SNAC frame: %+v\n", snac)
 
 	snacPayload := &TLVPayload{}
@@ -441,7 +442,9 @@ func (c *clientVersion) read(r io.Reader) error {
 	return binary.Read(r, binary.BigEndian, &c.toolVersion)
 }
 
-func ReceiveClientOnline(sess *Session, sm *SessionManager, fm *FeedbagStore, flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+type OnReadyCB func(sess *Session, sm *SessionManager, r io.Reader, w io.Writer, sequence *uint32) error
+
+func ReceiveClientOnline(onReadyCB OnReadyCB, sess *Session, sm *SessionManager, flap flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("receiveClientOnline read SNAC frame: %+v\n", snac)
 
 	b := make([]byte, flap.payloadLength-10)
@@ -459,12 +462,7 @@ func ReceiveClientOnline(sess *Session, sm *SessionManager, fm *FeedbagStore, fl
 		fmt.Printf("ReceiveClientOnline read SNAC client messageType: %+v\n", item)
 	}
 
-	err := NotifyArrival(sess, sm, fm)
-	if err != nil {
-		return err
-	}
-
-	return GetOnlineBuddies(w, sess, sm, fm, sequence)
+	return onReadyCB(sess, sm, r, w, sequence)
 }
 
 func GetOnlineBuddies(w io.Writer, sess *Session, sm *SessionManager, fm *FeedbagStore, sequence *uint32) error {
@@ -486,7 +484,7 @@ func GetOnlineBuddies(w io.Writer, sess *Session, sm *SessionManager, fm *Feedba
 			continue
 		}
 
-		flap := &flapFrame{
+		flap := flapFrame{
 			startMarker: 42,
 			frameType:   2,
 		}
@@ -509,7 +507,7 @@ func GetOnlineBuddies(w io.Writer, sess *Session, sm *SessionManager, fm *Feedba
 	return nil
 }
 
-func ReceiveSetUserInfoFields(sess *Session, sm *SessionManager, fm *FeedbagStore, flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func ReceiveSetUserInfoFields(sess *Session, sm *SessionManager, fm *FeedbagStore, flap flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("receiveSetUserInfoFields read SNAC frame: %+v\n", snac)
 
 	snacPayload := &TLVPayload{}
@@ -561,7 +559,7 @@ func (s *snacIdleNotification) read(r io.Reader) error {
 	return binary.Read(r, binary.BigEndian, &s.idleTime)
 }
 
-func ReceiveIdleNotification(sess *Session, sm *SessionManager, fm *FeedbagStore, flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func ReceiveIdleNotification(sess *Session, sm *SessionManager, fm *FeedbagStore, flap flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("receiveIdleNotification read SNAC frame: %+v\n", snac)
 
 	snacPayload := &snacIdleNotification{}
@@ -587,7 +585,9 @@ func (s *snacServiceRequest) read(r io.Reader) error {
 	if err := binary.Read(r, binary.BigEndian, &s.foodGroup); err != nil {
 		return err
 	}
-	return s.TLVPayload.read(r, map[uint16]reflect.Kind{})
+	return s.TLVPayload.read(r, map[uint16]reflect.Kind{
+		0x01: reflect.Slice,
+	})
 }
 
 const (
@@ -598,12 +598,12 @@ const (
 	OserviceTlvTagsSslState             = 0x8E
 )
 
-func ReceiveAndSendServiceRequest(sess *Session, flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func ReceiveAndSendServiceRequest(cr *ChatRegistry, sess *Session, flap flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("receiveAndSendServiceRequest read SNAC frame: %+v\n", snac)
 
 	snacPayload := &snacServiceRequest{}
 	if err := snacPayload.read(r); err != nil {
-		return nil
+		return err
 	}
 
 	fmt.Printf("receiveAndSendServiceRequest read SNAC body: %+v\n", snacPayload)
@@ -617,6 +617,60 @@ func ReceiveAndSendServiceRequest(sess *Session, flap *flapFrame, snac *snacFram
 		code: 0x06,
 	}
 
+	if snacPayload.foodGroup == CHAT {
+		roomMeta, ok := snacPayload.getTLV(0x01)
+		if !ok {
+			return errors.New("missing room info")
+		}
+
+		roomSnac := &roomInfoOService{}
+		if err := roomSnac.read(bytes.NewBuffer(roomMeta.val.([]byte))); err != nil {
+			return err
+		}
+
+		cookie := &ChatCookie{
+			Cookie: roomSnac.cookie,
+			SessID: sess.ID,
+		}
+		buf := &bytes.Buffer{}
+		if err := cookie.write(buf); err != nil {
+			return err
+		}
+
+		room, err := cr.Retrieve(string(roomSnac.cookie))
+		if err != nil {
+			return err
+		}
+
+		room.SessionManager.NewSessionWithSN(sess.ID, sess.ScreenName)
+
+		snacPayloadOut := &TLVPayload{
+			TLVs: []*TLV{
+				{
+					tType: OserviceTlvTagsReconnectHere,
+					val:   "192.168.64.1:5192",
+				},
+				{
+					tType: OserviceTlvTagsLoginCookie,
+					val:   buf.Bytes(),
+				},
+				{
+					tType: OserviceTlvTagsGroupId,
+					val:   snacPayload.foodGroup,
+				},
+				{
+					tType: OserviceTlvTagsSslCertname,
+					val:   "",
+				},
+				{
+					tType: OserviceTlvTagsSslState,
+					val:   uint8(0x00),
+				},
+			},
+		}
+		return writeOutSNAC(snac, flap, snacFrameOut, snacPayloadOut, sequence, w)
+	}
+
 	return writeOutSNAC(snac, flap, snacFrameOut, snacPayloadOut, sequence, w)
 }
 

+ 2 - 2
oscar/pd.go

@@ -19,7 +19,7 @@ const (
 	PDDelTempPermitListEntries        = 0x000B
 )
 
-func routePD(flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func routePD(flap flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	switch snac.subGroup {
 	case PDErr:
 		panic("not implemented")
@@ -46,7 +46,7 @@ func routePD(flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequenc
 	return nil
 }
 
-func SendAndReceivePDRightsQuery(flap *flapFrame, snac *snacFrame, _ io.Reader, w io.Writer, sequence *uint32) error {
+func SendAndReceivePDRightsQuery(flap flapFrame, snac *snacFrame, _ io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("sendAndReceivePDRightsQuery read SNAC frame: %+v\n", snac)
 
 	snacFrameOut := snacFrame{

+ 68 - 29
oscar/protocol.go

@@ -5,8 +5,10 @@ import (
 	"encoding/binary"
 	"errors"
 	"fmt"
+	"github.com/google/uuid"
 	"io"
 	"reflect"
+	"sync"
 )
 
 const (
@@ -52,6 +54,10 @@ const (
 	ErrorTagsErrorInfoData  = 0x2A
 )
 
+var (
+	CapChat, _ = uuid.MustParse("748F2420-6287-11D1-8222-444553540000").MarshalBinary()
+)
+
 type snacError struct {
 	code uint16
 	TLVPayload
@@ -141,10 +147,15 @@ type snacWriter interface {
 	write(w io.Writer) error
 }
 
+// todo make type TLVPayload TLVs []*TLV
 type TLVPayload struct {
 	TLVs []*TLV
 }
 
+func (s *TLVPayload) addTLV(tlv *TLV) {
+	s.TLVs = append(s.TLVs, tlv)
+}
+
 func (s *TLVPayload) read(r io.Reader, lookup map[uint16]reflect.Kind) error {
 	for {
 		// todo, don't like this extra alloc when we're EOF
@@ -257,6 +268,13 @@ func (t *TLV) write(w io.Writer) error {
 		}
 		valLen = uint16(buf.Len())
 		val = buf.Bytes()
+	case snacWriter:
+		buf := &bytes.Buffer{}
+		if err := val.(snacWriter).write(buf); err != nil {
+			return err
+		}
+		valLen = uint16(buf.Len())
+		val = buf.Bytes()
 	}
 
 	if err := binary.Write(w, binary.BigEndian, valLen); err != nil {
@@ -348,7 +366,7 @@ func (f *flapSignonFrame) read(r io.Reader) error {
 	}
 
 	lookup := map[uint16]reflect.Kind{
-		0x06: reflect.String,
+		0x06: reflect.Slice,
 		0x4A: reflect.Uint8,
 	}
 
@@ -397,26 +415,48 @@ func SendAndReceiveSignonFrame(rw io.ReadWriter, sequence *uint32) (*flapSignonF
 	return flap, nil
 }
 
-func VerifyLogin(sm *SessionManager, rw io.ReadWriter, sequence *uint32) (*Session, error) {
+func VerifyLogin(sm *SessionManager, rw io.ReadWriter) (*Session, uint32, error) {
+	seq := uint32(100)
 	fmt.Println("VerifyLogin...")
 
-	flap, err := SendAndReceiveSignonFrame(rw, sequence)
+	flap, err := SendAndReceiveSignonFrame(rw, &seq)
 	if err != nil {
-		return nil, err
+		return nil, 0, err
 	}
 
 	var ok bool
-	ID, ok := flap.getString(OserviceTlvTagsLoginCookie)
+	ID, ok := flap.getSlice(OserviceTlvTagsLoginCookie)
+	if !ok {
+		return nil, 0, errors.New("unable to get session ID from payload")
+	}
+
+	sess, ok := sm.Retrieve(string(ID))
 	if !ok {
-		return nil, errors.New("unable to get session ID from payload")
+		return nil, 0, fmt.Errorf("unable to find session by ID %s", ID)
+	}
+
+	return sess, seq, nil
+}
+
+func VerifyChatLogin(rw io.ReadWriter) (*ChatCookie, uint32, error) {
+	seq := uint32(100)
+	fmt.Println("VerifyChatLogin...")
+
+	flap, err := SendAndReceiveSignonFrame(rw, &seq)
+	if err != nil {
+		return nil, 0, err
 	}
 
-	sess, ok := sm.Retrieve(ID)
+	var ok bool
+	buf, ok := flap.getSlice(OserviceTlvTagsLoginCookie)
 	if !ok {
-		return nil, fmt.Errorf("unable to find session by ID %s", ID)
+		return nil, 0, errors.New("unable to get session ID from payload")
 	}
 
-	return sess, nil
+	cookie := &ChatCookie{}
+	err = cookie.read(bytes.NewBuffer(buf))
+
+	return cookie, seq, err
 }
 
 const (
@@ -447,7 +487,7 @@ const (
 )
 
 type IncomingMessage struct {
-	flap *flapFrame
+	flap flapFrame
 	snac *snacFrame
 	buf  io.Reader
 }
@@ -455,7 +495,7 @@ type IncomingMessage struct {
 type XMessage struct {
 	// todo: this should only take values, not pointers, in order to avoid race
 	// conditions
-	flap      *flapFrame
+	flap      flapFrame
 	snacFrame snacFrame
 	snacOut   snacWriter
 }
@@ -473,7 +513,7 @@ func readIncomingRequests(rw io.Reader, msCh chan IncomingMessage, errCh chan er
 	defer close(errCh)
 
 	for {
-		flap := &flapFrame{}
+		flap := flapFrame{}
 		if err := flap.read(rw); err != nil {
 			errCh <- err
 			return
@@ -518,7 +558,7 @@ func readIncomingRequests(rw io.Reader, msCh chan IncomingMessage, errCh chan er
 	}
 }
 
-func signout(sess *Session, sm *SessionManager, fm *FeedbagStore) {
+func Signout(sess *Session, sm *SessionManager, fm *FeedbagStore) {
 	if err := NotifyDeparture(sess, sm, fm); err != nil {
 		fmt.Printf("error notifying departure: %s", err.Error())
 	}
@@ -526,17 +566,8 @@ func signout(sess *Session, sm *SessionManager, fm *FeedbagStore) {
 	sm.Remove(sess)
 }
 
-func ReadBos(sm *SessionManager, fm *FeedbagStore, rwc io.ReadWriteCloser) error {
-	defer rwc.Close()
-
-	seq := uint32(100)
-	sess, err := VerifyLogin(sm, rwc, &seq)
-	if err != nil {
-		return err
-	}
-	defer signout(sess, sm, fm)
-
-	if err := WriteOServiceHostOnline(rwc, &seq); err != nil {
+func ReadBos(ready OnReadyCB, sess *Session, seq uint32, sm *SessionManager, fm *FeedbagStore, cr *ChatRegistry, rwc io.ReadWriter, foodGroups []uint16) error {
+	if err := WriteOServiceHostOnline(foodGroups, rwc, &seq); err != nil {
 		return err
 	}
 
@@ -545,13 +576,17 @@ func ReadBos(sm *SessionManager, fm *FeedbagStore, rwc io.ReadWriteCloser) error
 	errCh := make(chan error, 1)
 	go readIncomingRequests(rwc, msgCh, errCh)
 
+	wg := sync.WaitGroup{}
+	wg.Add(1)
+
 	for {
 		select {
 		case m := <-msgCh:
-			if err := routeIncomingRequests(sm, sess, fm, rwc, &seq, m.snac, m.flap, m.buf); err != nil {
+			if err := routeIncomingRequests(ready, &wg, sm, sess, fm, cr, rwc, &seq, m.snac, m.flap, m.buf); err != nil {
 				return err
 			}
 		case m := <-sess.RecvMessage():
+			wg.Wait()
 			if err := writeOutSNAC(nil, m.flap, m.snacFrame, m.snacOut, &seq, rwc); err != nil {
 				return err
 			}
@@ -561,10 +596,10 @@ func ReadBos(sm *SessionManager, fm *FeedbagStore, rwc io.ReadWriteCloser) error
 	}
 }
 
-func routeIncomingRequests(sm *SessionManager, sess *Session, fm *FeedbagStore, rw io.ReadWriter, sequence *uint32, snac *snacFrame, flap *flapFrame, buf io.Reader) error {
+func routeIncomingRequests(ready OnReadyCB, wg *sync.WaitGroup, sm *SessionManager, sess *Session, fm *FeedbagStore, cr *ChatRegistry, rw io.ReadWriter, sequence *uint32, snac *snacFrame, flap flapFrame, buf io.Reader) error {
 	switch snac.foodGroup {
 	case OSERVICE:
-		if err := routeOService(sm, fm, sess, flap, snac, buf, rw, sequence); err != nil {
+		if err := routeOService(ready, wg, cr, sm, fm, sess, flap, snac, buf, rw, sequence); err != nil {
 			return err
 		}
 	case LOCATE:
@@ -584,7 +619,7 @@ func routeIncomingRequests(sm *SessionManager, sess *Session, fm *FeedbagStore,
 			return err
 		}
 	case CHAT_NAV:
-		if err := routeChatNav(flap, snac, buf, rw, sequence); err != nil {
+		if err := routeChatNav(sess, cr, flap, snac, buf, rw, sequence); err != nil {
 			return err
 		}
 	case FEEDBAG:
@@ -595,6 +630,10 @@ func routeIncomingRequests(sm *SessionManager, sess *Session, fm *FeedbagStore,
 		if err := routeBUCP(flap, snac, buf, rw, sequence); err != nil {
 			return err
 		}
+	case CHAT:
+		if err := routeChat(cr, sess, sm, flap, snac, buf, rw, sequence); err != nil {
+			return err
+		}
 	default:
 		panic(fmt.Sprintf("unsupported food group: %d", snac.foodGroup))
 	}
@@ -602,7 +641,7 @@ func routeIncomingRequests(sm *SessionManager, sess *Session, fm *FeedbagStore,
 	return nil
 }
 
-func writeOutSNAC(originSnac *snacFrame, flap *flapFrame, snacFrame snacFrame, snacOut snacWriter, sequence *uint32, w io.Writer) error {
+func writeOutSNAC(originSnac *snacFrame, flap flapFrame, snacFrame snacFrame, snacOut snacWriter, sequence *uint32, w io.Writer) error {
 	if originSnac != nil {
 		snacFrame.requestID = originSnac.requestID
 	}

+ 153 - 2
oscar/session.go

@@ -22,6 +22,19 @@ type Session struct {
 	invisible   bool
 	idle        bool
 	idleTime    time.Time
+	ready       bool
+}
+
+func (s *Session) SetReady() {
+	s.Mutex.RLock()
+	defer s.Mutex.RUnlock()
+	s.ready = true
+}
+
+func (s *Session) Ready() bool {
+	s.Mutex.RLock()
+	defer s.Mutex.RUnlock()
+	return s.ready
 }
 
 func (s *Session) IncreaseWarning(incr uint16) {
@@ -116,6 +129,15 @@ func (s *Session) GetUserInfo() []*TLV {
 	}
 	tlvs = append(tlvs, idle)
 
+	// capabilities
+	caps := &TLV{
+		tType: 0x0D,
+		val:   []byte{},
+	}
+	// chat capability
+	caps.val = append(caps.val.([]byte), CapChat...)
+	tlvs = append(tlvs, caps)
+
 	return tlvs
 }
 
@@ -154,6 +176,41 @@ func NewSessionManager() *SessionManager {
 	}
 }
 
+func (s *SessionManager) Broadcast(msg *XMessage) {
+	s.mapMutex.RLock()
+	defer s.mapMutex.RUnlock()
+	for _, sess := range s.store {
+		sess.SendMessage(msg)
+	}
+}
+
+func (s *SessionManager) Empty() bool {
+	s.mapMutex.RLock()
+	defer s.mapMutex.RUnlock()
+	return len(s.store) == 0
+}
+
+func (s *SessionManager) All() []*Session {
+	s.mapMutex.RLock()
+	defer s.mapMutex.RUnlock()
+	var sessions []*Session
+	for _, sess := range s.store {
+		sessions = append(sessions, sess)
+	}
+	return sessions
+}
+
+func (s *SessionManager) BroadcastExcept(except *Session, msg *XMessage) {
+	s.mapMutex.RLock()
+	defer s.mapMutex.RUnlock()
+	for _, sess := range s.store {
+		if sess == except {
+			continue
+		}
+		sess.SendMessage(msg)
+	}
+}
+
 func (s *SessionManager) Retrieve(ID string) (*Session, bool) {
 	s.mapMutex.RLock()
 	defer s.mapMutex.RUnlock()
@@ -187,8 +244,8 @@ func (s *SessionManager) RetrieveByScreenNames(screenNames []string) []*Session
 }
 
 func (s *SessionManager) NewSession() (*Session, error) {
-	s.mapMutex.RLock()
-	defer s.mapMutex.RUnlock()
+	s.mapMutex.Lock()
+	defer s.mapMutex.Unlock()
 	id, err := uuid.NewUUID()
 	if err != nil {
 		return nil, err
@@ -203,8 +260,102 @@ func (s *SessionManager) NewSession() (*Session, error) {
 	return sess, nil
 }
 
+func (s *SessionManager) NewSessionWithSN(sessID string, screenName string) *Session {
+	s.mapMutex.Lock()
+	defer s.mapMutex.Unlock()
+	sess := &Session{
+		ID: sessID,
+		// todo what if client is unresponsive and blocks other messages?
+		// idea: make channel big enough to handle backlog, disconnect user
+		// if the queue fills up
+		msgCh:      make(chan *XMessage, 1),
+		stopCh:     make(chan struct{}),
+		SignonTime: time.Now(),
+		ScreenName: screenName,
+	}
+	s.store[sess.ID] = sess
+	return sess
+}
+
 func (s *SessionManager) Remove(sess *Session) {
 	s.mapMutex.Lock()
 	defer s.mapMutex.Unlock()
 	delete(s.store, sess.ID)
 }
+
+type ChatRoom struct {
+	ID             string
+	SessionManager *SessionManager
+	CreateTime     time.Time
+	Name           string
+}
+
+func (c ChatRoom) TLVList() []*TLV {
+	return []*TLV{
+		{
+			tType: 0x00c9,
+			val:   uint16(15),
+		},
+		{
+			tType: 0x00ca,
+			val:   uint32(c.CreateTime.Unix()),
+		},
+		{
+			tType: 0x00d1,
+			val:   uint16(1024),
+		},
+		{
+			tType: 0x00d2,
+			val:   uint16(100),
+		},
+		{
+			tType: 0x00d5,
+			val:   uint8(2),
+		},
+		{
+			tType: 0x006a,
+			val:   c.Name,
+		},
+		{
+			tType: 0x00d3,
+			val:   c.Name,
+		},
+	}
+}
+
+type ChatRegistry struct {
+	store    map[string]ChatRoom
+	mapMutex sync.RWMutex
+}
+
+func NewChatRegistry() *ChatRegistry {
+	return &ChatRegistry{
+		store: make(map[string]ChatRoom),
+	}
+}
+
+func (c *ChatRegistry) Register(room ChatRoom) {
+	c.mapMutex.Lock()
+	defer c.mapMutex.Unlock()
+	c.store[room.ID] = room
+}
+
+func (c *ChatRegistry) Retrieve(chatID string) (ChatRoom, error) {
+	c.mapMutex.RLock()
+	defer c.mapMutex.RUnlock()
+	sm, found := c.store[chatID]
+	if !found {
+		return sm, errors.New("unable to find session manager for chat")
+	}
+	return sm, nil
+}
+
+func (c *ChatRegistry) MaybeRemoveRoom(chatID string) {
+	c.mapMutex.Lock()
+	defer c.mapMutex.Unlock()
+
+	room, found := c.store[chatID]
+	if found && room.SessionManager.Empty() {
+		delete(c.store, chatID)
+	}
+}