package toc import ( "bytes" "context" "errors" "fmt" "strings" "github.com/mk6i/retro-aim-server/state" "github.com/mk6i/retro-aim-server/wire" ) var ( cmdInternalSvcErr = "ERROR:989:internal server error" errDisconnect = errors.New("got booted by another session") ) // RecvBOS routes incoming SNAC messages from the BOS server to their // corresponding TOC handlers. It ignores any SNAC messages for which there is // no TOC response. func (s OSCARProxy) RecvBOS(ctx context.Context, me *state.Session, chatRegistry *ChatRegistry, ch chan<- []byte) error { for { select { case <-ctx.Done(): return nil case <-me.Closed(): return errDisconnect case snac := <-me.ReceiveMessage(): switch v := snac.Body.(type) { case wire.SNAC_0x03_0x0B_BuddyArrived: sendOrCancel(ctx, ch, s.UpdateBuddyArrival(v)) case wire.SNAC_0x03_0x0C_BuddyDeparted: sendOrCancel(ctx, ch, s.UpdateBuddyDeparted(v)) case wire.SNAC_0x04_0x07_ICBMChannelMsgToClient: sendOrCancel(ctx, ch, s.IMIn(ctx, chatRegistry, v)) case wire.SNAC_0x01_0x10_OServiceEvilNotification: sendOrCancel(ctx, ch, s.Eviled(v)) default: s.Logger.DebugContext(ctx, fmt.Sprintf("unsupported snac. foodgroup: %s subgroup: %s", wire.FoodGroupName(snac.Frame.FoodGroup), wire.SubGroupName(snac.Frame.FoodGroup, snac.Frame.SubGroup))) } } } } // RecvChat routes incoming SNAC messages from the chat server to their // corresponding TOC handlers. It ignores any SNAC messages for which there is // no TOC response. func (s OSCARProxy) RecvChat(ctx context.Context, me *state.Session, chatID int, ch chan<- []byte) { for { select { case <-ctx.Done(): return case <-me.Closed(): return case snac := <-me.ReceiveMessage(): switch v := snac.Body.(type) { case wire.SNAC_0x0E_0x04_ChatUsersLeft: sendOrCancel(ctx, ch, s.ChatUpdateBuddyLeft(v, chatID)) case wire.SNAC_0x0E_0x03_ChatUsersJoined: sendOrCancel(ctx, ch, s.ChatUpdateBuddyArrived(v, chatID)) case wire.SNAC_0x0E_0x06_ChatChannelMsgToClient: sendOrCancel(ctx, ch, s.ChatIn(ctx, v, chatID)) default: s.Logger.DebugContext(ctx, fmt.Sprintf("unsupported snac. foodgroup: %s subgroup: %s", wire.FoodGroupName(snac.Frame.FoodGroup), wire.SubGroupName(snac.Frame.FoodGroup, snac.Frame.SubGroup))) } } } } // ChatIn handles the CHAT_IN TOC command. // // From the TiK documentation: // // A chat message was sent in a chat room. // // Command syntax: CHAT_IN:::: func (s OSCARProxy) ChatIn(ctx context.Context, snac wire.SNAC_0x0E_0x06_ChatChannelMsgToClient, chatID int) string { b, ok := snac.Bytes(wire.ChatTLVSenderInformation) if !ok { return s.runtimeErr(ctx, errors.New("snac.Bytes: missing wire.ChatTLVSenderInformation")) } u := wire.TLVUserInfo{} err := wire.UnmarshalBE(&u, bytes.NewReader(b)) if err != nil { return s.runtimeErr(ctx, fmt.Errorf("wire.UnmarshalBE: %w", err)) } b, ok = snac.Bytes(wire.ChatTLVMessageInfo) if !ok { return s.runtimeErr(ctx, errors.New("snac.Bytes: missing wire.ChatTLVMessageInfo")) } text, err := wire.UnmarshalChatMessageText(b) if err != nil { return s.runtimeErr(ctx, fmt.Errorf("wire.UnmarshalChatMessageText: %w", err)) } return fmt.Sprintf("CHAT_IN:%d:%s:F:%s", chatID, u.ScreenName, text) } // ChatUpdateBuddyArrived handles the CHAT_UPDATE_BUDDY TOC command for chat // room arrival events. // // From the TiK documentation: // // This one command handles arrival/departs from a chat room. The very first // message of this type for each chat room contains the users already in the // room. // // Command syntax: CHAT_UPDATE_BUDDY::::... func (s OSCARProxy) ChatUpdateBuddyArrived(snac wire.SNAC_0x0E_0x03_ChatUsersJoined, chatID int) string { users := make([]string, 0, len(snac.Users)) for _, u := range snac.Users { users = append(users, u.ScreenName) } return fmt.Sprintf("CHAT_UPDATE_BUDDY:%d:T:%s", chatID, strings.Join(users, ":")) } // ChatUpdateBuddyLeft handles the CHAT_UPDATE_BUDDY TOC command for chat // room departure events. // // From the TiK documentation: // // This one command handles arrival/departs from a chat room. The very first // message of this type for each chat room contains the users already in the // room. // // Command syntax: CHAT_UPDATE_BUDDY::::... func (s OSCARProxy) ChatUpdateBuddyLeft(snac wire.SNAC_0x0E_0x04_ChatUsersLeft, chatID int) string { users := make([]string, 0, len(snac.Users)) for _, u := range snac.Users { users = append(users, u.ScreenName) } return fmt.Sprintf("CHAT_UPDATE_BUDDY:%d:F:%s", chatID, strings.Join(users, ":")) } // Eviled handles the EVILED TOC command. // // From the TiK documentation: // // The user was just eviled. // // Command syntax: EVILED:: func (s OSCARProxy) Eviled(snac wire.SNAC_0x01_0x10_OServiceEvilNotification) string { warning := fmt.Sprintf("%d", snac.NewEvil/10) who := "" if snac.Snitcher != nil { who = snac.Snitcher.ScreenName } return fmt.Sprintf("EVILED:%s:%s", warning, who) } // IMIn handles the IM_IN TOC command. // // From the TiK documentation: // // Receive an IM from someone. Everything after the third colon is the // incoming message, including other colons. // // Command syntax: IM_IN::: func (s OSCARProxy) IMIn(ctx context.Context, chatRegistry *ChatRegistry, snac wire.SNAC_0x04_0x07_ICBMChannelMsgToClient) string { if snac.ChannelID == wire.ICBMChannelRendezvous { rdinfo, has := snac.TLVRestBlock.Bytes(wire.ICBMTLVData) if !has { return s.runtimeErr(ctx, errors.New("TLVRestBlock.Bytes: missing rendezvous block")) } frag := wire.ICBMCh2Fragment{} if err := wire.UnmarshalBE(&frag, bytes.NewReader(rdinfo)); err != nil { return s.runtimeErr(ctx, fmt.Errorf("wire.UnmarshalBE: %w", err)) } prompt, ok := frag.Bytes(wire.ICBMRdvTLVTagsInvitation) if !ok { return s.runtimeErr(ctx, errors.New("frag.Bytes: missing chat invite prompt")) } svcData, ok := frag.Bytes(wire.ICBMRdvTLVTagsSvcData) if !ok || svcData == nil { return s.runtimeErr(ctx, errors.New("frag.Bytes: missing room info")) } roomInfo := wire.ICBMRoomInfo{} if err := wire.UnmarshalBE(&roomInfo, bytes.NewReader(svcData)); err != nil { return s.runtimeErr(ctx, fmt.Errorf("wire.UnmarshalBE: %w", err)) } cookie := strings.Split(roomInfo.Cookie, "-") // make this safe if len(cookie) < 3 { return s.runtimeErr(ctx, errors.New("roomInfo.Cookie: malformed cookie, could not get room name")) } roomName := cookie[2] chatID := chatRegistry.Add(roomInfo) return fmt.Sprintf("CHAT_INVITE:%s:%d:%s:%s", roomName, chatID, snac.ScreenName, prompt) } buf, ok := snac.TLVRestBlock.Bytes(wire.ICBMTLVAOLIMData) if !ok { return s.runtimeErr(ctx, errors.New("TLVRestBlock.Bytes: missing wire.ICBMTLVAOLIMData")) } txt, err := wire.UnmarshalICBMMessageText(buf) if err != nil { return s.runtimeErr(ctx, fmt.Errorf("wire.UnmarshalICBMMessageText: %w", err)) } autoResp := "F" if _, isAutoReply := snac.TLVRestBlock.Bytes(wire.ICBMTLVAutoResponse); isAutoReply { autoResp = "T" } return fmt.Sprintf("IM_IN:%s:%s:%s", snac.ScreenName, autoResp, txt) } // UpdateBuddyArrival handles the UPDATE_BUDDY TOC command for buddy arrival events. // // From the TiK documentation: // // This one command handles arrival/depart/updates. Evil Amount is a percentage, Signon Time is UNIX epoc, idle time is in minutes, UC (User Class) is a two/three character string. // - uc[0] // - ' ' - Ignore // - 'A' - On AOL // - uc[1] // - ' ' - Ignore // - 'A' - Oscar Admin // - 'U' - Oscar Unconfirmed // - 'O' - Oscar Normal // - uc[2] // - '\0' - Ignore // - ' ' - Ignore // - 'U' - The user has set their unavailable flag. // // Command syntax: UPDATE_BUDDY:::::: func (s OSCARProxy) UpdateBuddyArrival(snac wire.SNAC_0x03_0x0B_BuddyArrived) string { online, _ := snac.Uint32BE(wire.OServiceUserInfoSignonTOD) idle, _ := snac.Uint16BE(wire.OServiceUserInfoIdleTime) uc := [3]string{" ", "O", " "} if snac.IsAway() { uc[2] = "U" } warning := fmt.Sprintf("%d", snac.WarningLevel/10) class := strings.Join(uc[:], "") return fmt.Sprintf("UPDATE_BUDDY:%s:%s:%s:%d:%d:%s", snac.ScreenName, "T", warning, online, idle, class) } // UpdateBuddyDeparted handles the UPDATE_BUDDY TOC command for buddy departure events. // // From the TiK documentation: // // This one command handles arrival/depart/updates. Evil Amount is a // percentage, Signon Time is UNIX epoc, idle time is in minutes, UC (User // Class) is a two/three character string. // - uc[0] // - ' ' - Ignore // - 'A' - On AOL // - uc[1] // - ' ' - Ignore // - 'A' - Oscar Admin // - 'U' - Oscar Unconfirmed // - 'O' - Oscar Normal // - uc[2] // - '\0' - Ignore // - ' ' - Ignore // - 'U' - The user has set their unavailable flag. // // Command syntax: UPDATE_BUDDY:::::: func (s OSCARProxy) UpdateBuddyDeparted(snac wire.SNAC_0x03_0x0C_BuddyDeparted) string { return fmt.Sprintf("UPDATE_BUDDY:%s:F:0:0:0: ", snac.ScreenName) } func sendOrCancel(ctx context.Context, ch chan<- []byte, msg string) { select { case <-ctx.Done(): return case ch <- []byte(msg): return } }