|
|
@@ -12,7 +12,6 @@ import (
|
|
|
|
|
|
"github.com/google/uuid"
|
|
|
|
|
|
- "github.com/mk6i/open-oscar-server/state"
|
|
|
"github.com/mk6i/open-oscar-server/wire"
|
|
|
)
|
|
|
|
|
|
@@ -24,67 +23,129 @@ var (
|
|
|
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.SessionInstance, chatRegistry *ChatRegistry, ch chan<- []string) error {
|
|
|
+// OServiceRateParamsChange codes the OSCAR server sends when one of the user's
|
|
|
+// rate classes becomes limited or clears.
|
|
|
+const (
|
|
|
+ rateParamsChangeLimited uint16 = 3
|
|
|
+ rateParamsChangeClear uint16 = 4
|
|
|
+)
|
|
|
+
|
|
|
+// RecvBOS routes SNACs from the BOS server, replies and server-initiated
|
|
|
+// messages alike, to their corresponding TOC handlers. It ignores any SNAC for
|
|
|
+// which there is no TOC response.
|
|
|
+func (s OSCARProxy) RecvBOS(ctx context.Context, me *tocConn) error {
|
|
|
+ for _, snac := range me.backlog {
|
|
|
+ s.recvBOSSNAC(ctx, me, snac)
|
|
|
+ }
|
|
|
+ me.backlog = nil
|
|
|
+
|
|
|
for {
|
|
|
select {
|
|
|
case <-ctx.Done():
|
|
|
- me.CloseInstance()
|
|
|
+ me.bos.Close()
|
|
|
return nil
|
|
|
- case <-me.Closed():
|
|
|
+ case <-me.bos.Closed():
|
|
|
return errDisconnect
|
|
|
- case snac := <-me.ReceiveMessage():
|
|
|
- switch v := snac.Body.(type) {
|
|
|
- case wire.SNAC_0x03_0x0B_BuddyArrived:
|
|
|
- sendOrCancel(ctx, ch, s.UpdateBuddyArrival(v, me))
|
|
|
- case wire.SNAC_0x03_0x0C_BuddyDeparted:
|
|
|
- sendOrCancel(ctx, ch, s.UpdateBuddyDeparted(v, me))
|
|
|
- case wire.SNAC_0x04_0x07_ICBMChannelMsgToClient:
|
|
|
- sendOrCancel(ctx, ch, s.IMIn(ctx, chatRegistry, me, v))
|
|
|
- case wire.SNAC_0x01_0x10_OServiceEvilNotification:
|
|
|
- sendOrCancel(ctx, ch, s.Eviled(v))
|
|
|
- case wire.SNAC_0x04_0x14_ICBMClientEvent:
|
|
|
- if me.IsTOC2() {
|
|
|
- sendOrCancel(ctx, ch, s.ClientEvent(v))
|
|
|
- }
|
|
|
- case wire.SNAC_0x13_0x09_FeedbagUpdateItem:
|
|
|
- if me.IsTOC2() {
|
|
|
- sendOrCancel(ctx, ch, s.Inserted2(ctx, me, v))
|
|
|
- }
|
|
|
- case wire.SNAC_0x13_0x0A_FeedbagDeleteItem:
|
|
|
- if me.IsTOC2() {
|
|
|
- sendOrCancel(ctx, ch, s.Deleted2(ctx, me, v))
|
|
|
- }
|
|
|
+ case snac := <-me.bos.Messages():
|
|
|
+ s.recvBOSSNAC(ctx, me, snac)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
- 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)))
|
|
|
- }
|
|
|
+func (s OSCARProxy) recvBOSSNAC(ctx context.Context, me *tocConn, snac wire.SNACMessage) {
|
|
|
+ var msg []string
|
|
|
+
|
|
|
+ switch v := snac.Body.(type) {
|
|
|
+ case wire.SNAC_0x03_0x0B_BuddyArrived:
|
|
|
+ msg = s.UpdateBuddyArrival(v, me)
|
|
|
+ case wire.SNAC_0x03_0x0C_BuddyDeparted:
|
|
|
+ msg = s.UpdateBuddyDeparted(v, me)
|
|
|
+ case wire.SNAC_0x04_0x07_ICBMChannelMsgToClient:
|
|
|
+ msg = s.IMIn(ctx, me, v)
|
|
|
+ case wire.SNAC_0x01_0x10_OServiceEvilNotification:
|
|
|
+ msg = s.Eviled(v)
|
|
|
+ case wire.SNAC_0x01_0x0A_OServiceRateParamsChange:
|
|
|
+ msg = s.RateParamsChange(v)
|
|
|
+ if v.Code == rateParamsChangeClear && me.feedbag.hasPending() {
|
|
|
+ // the server may have dropped feedbag changes without replying
|
|
|
+ // while limited
|
|
|
+ msg = s.reloadFeedbag(ctx, me)
|
|
|
+ }
|
|
|
+ case wire.SNAC_0x04_0x14_ICBMClientEvent:
|
|
|
+ if me.IsTOC2() {
|
|
|
+ msg = s.ClientEvent(v)
|
|
|
+ }
|
|
|
+ case wire.SNAC_0x13_0x08_FeedbagInsertItem:
|
|
|
+ if me.IsTOC2() {
|
|
|
+ me.feedbag.applyRemote(false, v.Items)
|
|
|
+ msg = s.Inserted2(me, v.Items)
|
|
|
+ }
|
|
|
+ case wire.SNAC_0x13_0x09_FeedbagUpdateItem:
|
|
|
+ if me.IsTOC2() {
|
|
|
+ me.feedbag.applyRemote(false, v.Items)
|
|
|
+ msg = s.Inserted2(me, v.Items)
|
|
|
+ }
|
|
|
+ case wire.SNAC_0x13_0x0A_FeedbagDeleteItem:
|
|
|
+ if me.IsTOC2() {
|
|
|
+ msg = s.Deleted2(me, v)
|
|
|
+ me.feedbag.applyRemote(true, v.Items)
|
|
|
}
|
|
|
+ case wire.SNACError:
|
|
|
+ msg = s.SNACError(ctx, me, snac.Frame, v)
|
|
|
+ case wire.SNAC_0x02_0x06_LocateUserInfoReply:
|
|
|
+ msg = s.UserInfoReply(me, snac.Frame, v)
|
|
|
+ case wire.SNAC_0x07_0x05_AdminChangeReply:
|
|
|
+ msg = s.AdminChangeReply(ctx, me, snac.Frame, v)
|
|
|
+ case wire.SNAC_0x0D_0x09_ChatNavNavInfo:
|
|
|
+ msg = s.ChatNavInfo(ctx, me, snac.Frame, v)
|
|
|
+ case wire.SNAC_0x01_0x05_OServiceServiceResponse:
|
|
|
+ msg = s.ChatServiceResponse(ctx, me, snac.Frame, v)
|
|
|
+ case wire.SNAC_0x13_0x0E_FeedbagStatus:
|
|
|
+ s.FeedbagStatus(me, snac.Frame, v)
|
|
|
+ case wire.SNAC_0x13_0x06_FeedbagReply:
|
|
|
+ s.FeedbagReply(me, snac.Frame, v)
|
|
|
+ case wire.SNAC_0x04_0x09_ICBMEvilReply:
|
|
|
+ // the warning succeeded and there's nothing to tell the client
|
|
|
+ me.cache.take(snac.Frame)
|
|
|
+ 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)))
|
|
|
+ }
|
|
|
+
|
|
|
+ if len(msg) > 0 {
|
|
|
+ sendOrCancel(ctx, me.toCh, msg)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// 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.SessionInstance, chatID int, ch chan<- []string) {
|
|
|
+// RecvChat routes SNACs from a chat room connection to their corresponding
|
|
|
+// TOC handlers. It ignores any SNAC for which there is no TOC response.
|
|
|
+func (s OSCARProxy) RecvChat(ctx context.Context, me *tocConn, conn oscarConn, chatID int) {
|
|
|
for {
|
|
|
select {
|
|
|
case <-ctx.Done():
|
|
|
+ conn.Close()
|
|
|
return
|
|
|
- case <-me.Closed():
|
|
|
+ case <-conn.Closed():
|
|
|
return
|
|
|
- case snac := <-me.ReceiveMessage():
|
|
|
+ case snac := <-conn.Messages():
|
|
|
switch v := snac.Body.(type) {
|
|
|
case wire.SNAC_0x0E_0x04_ChatUsersLeft:
|
|
|
- sendOrCancel(ctx, ch, s.ChatUpdateBuddyLeft(v, chatID))
|
|
|
+ sendOrCancel(ctx, me.toCh, s.ChatUpdateBuddyLeft(v, chatID))
|
|
|
case wire.SNAC_0x0E_0x03_ChatUsersJoined:
|
|
|
- sendOrCancel(ctx, ch, s.ChatUpdateBuddyArrived(v, chatID))
|
|
|
+ sendOrCancel(ctx, me.toCh, s.ChatUpdateBuddyArrived(v, chatID))
|
|
|
case wire.SNAC_0x0E_0x06_ChatChannelMsgToClient:
|
|
|
- sendOrCancel(ctx, ch, s.ChatIn(ctx, me, v, chatID))
|
|
|
+ // a reflected message answers the user's own toc_chat_send
|
|
|
+ me.cache.take(snac.Frame)
|
|
|
+ sendOrCancel(ctx, me.toCh, s.ChatIn(ctx, me, v, chatID))
|
|
|
+ case wire.SNAC_0x01_0x0A_OServiceRateParamsChange:
|
|
|
+ if msg := s.RateParamsChange(v); len(msg) > 0 {
|
|
|
+ sendOrCancel(ctx, me.toCh, msg)
|
|
|
+ }
|
|
|
+ case wire.SNACError:
|
|
|
+ if msg := s.SNACError(ctx, me, snac.Frame, v); len(msg) > 0 {
|
|
|
+ sendOrCancel(ctx, me.toCh, msg)
|
|
|
+ }
|
|
|
default:
|
|
|
s.Logger.DebugContext(ctx, fmt.Sprintf("unsupported snac. foodgroup: %s subgroup: %s",
|
|
|
wire.FoodGroupName(snac.Frame.FoodGroup),
|
|
|
@@ -94,6 +155,191 @@ func (s OSCARProxy) RecvChat(ctx context.Context, me *state.SessionInstance, cha
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// RateParamsChange tells the client that it's being rate limited when one of
|
|
|
+// its rate classes becomes limited, since the OSCAR server drops rate-limited
|
|
|
+// requests without replying.
|
|
|
+func (s OSCARProxy) RateParamsChange(snac wire.SNAC_0x01_0x0A_OServiceRateParamsChange) []string {
|
|
|
+ if snac.Code == rateParamsChangeLimited {
|
|
|
+ return []string{rateLimitExceededErr}
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+// SNACError handles an error reply from the OSCAR server according to the
|
|
|
+// request that caused it.
|
|
|
+func (s OSCARProxy) SNACError(ctx context.Context, me *tocConn, frame wire.SNACFrame, snac wire.SNACError) []string {
|
|
|
+ req, ok := me.cache.take(frame)
|
|
|
+ if !ok {
|
|
|
+ s.Logger.DebugContext(ctx, "received SNAC error for unknown request",
|
|
|
+ "foodgroup", wire.FoodGroupName(frame.FoodGroup), "code", snac.Code)
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ switch data := req.data.(type) {
|
|
|
+ case getStatusReqCtx:
|
|
|
+ if snac.Code == wire.ErrorCodeNotLoggedOn {
|
|
|
+ return []string{fmt.Sprintf("ERROR:%s:%s", wire.TOCErrorGeneralUserNotAvailable, data.screenName)}
|
|
|
+ }
|
|
|
+ return s.runtimeErr(ctx, fmt.Errorf("LocateUserInfoQuery error code: %d", snac.Code))
|
|
|
+ case evilReqCtx:
|
|
|
+ s.Logger.InfoContext(ctx, "unable to warn user", "code", snac.Code)
|
|
|
+ return []string{"ERROR:" + wire.TOCErrorGeneralWarningUserNotAvailable}
|
|
|
+ case feedbagChangeReqCtx:
|
|
|
+ s.Logger.DebugContext(ctx, "feedbag change failed", "code", snac.Code)
|
|
|
+ me.feedbag.discard(frame.RequestID)
|
|
|
+ return nil
|
|
|
+ case chatJoinReqCtx, chatAcceptReqCtx, chatServiceReqCtx:
|
|
|
+ return s.runtimeErr(ctx, fmt.Errorf("unable to join chat room, %s error code: %d",
|
|
|
+ wire.SubGroupName(req.frame.FoodGroup, req.frame.SubGroup), snac.Code))
|
|
|
+ default:
|
|
|
+ s.Logger.DebugContext(ctx, "received SNAC error",
|
|
|
+ "request", wire.SubGroupName(req.frame.FoodGroup, req.frame.SubGroup), "code", snac.Code)
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// FeedbagStatus confirms the items of a feedbag change that the server
|
|
|
+// accepted.
|
|
|
+func (s OSCARProxy) FeedbagStatus(me *tocConn, frame wire.SNACFrame, snac wire.SNAC_0x13_0x0E_FeedbagStatus) {
|
|
|
+ me.cache.take(frame)
|
|
|
+ me.feedbag.confirm(frame.RequestID, snac.Results)
|
|
|
+}
|
|
|
+
|
|
|
+// FeedbagReply finishes reloading the feedbag mirror.
|
|
|
+func (s OSCARProxy) FeedbagReply(me *tocConn, frame wire.SNACFrame, snac wire.SNAC_0x13_0x06_FeedbagReply) {
|
|
|
+ req, ok := me.cache.take(frame)
|
|
|
+ if !ok {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if data, ok := req.data.(feedbagReloadReqCtx); ok {
|
|
|
+ me.feedbag.reload(data.marker, snac.Items)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// UserInfoReply handles the user info reply to toc_get_status, which is sent
|
|
|
+// to the client as UPDATE_BUDDY.
|
|
|
+func (s OSCARProxy) UserInfoReply(me *tocConn, frame wire.SNACFrame, snac wire.SNAC_0x02_0x06_LocateUserInfoReply) []string {
|
|
|
+ me.cache.take(frame)
|
|
|
+ return []string{userInfoToUpdateBuddy(snac.TLVUserInfo, me)}
|
|
|
+}
|
|
|
+
|
|
|
+// AdminChangeReply handles the reply to toc_change_passwd, sent as
|
|
|
+// ADMIN_PASSWD_STATUS, or to toc_format_nickname, sent as ADMIN_NICK_STATUS
|
|
|
+// and NICK.
|
|
|
+func (s OSCARProxy) AdminChangeReply(ctx context.Context, me *tocConn, frame wire.SNACFrame, snac wire.SNAC_0x07_0x05_AdminChangeReply) []string {
|
|
|
+ req, ok := me.cache.take(frame)
|
|
|
+ if !ok {
|
|
|
+ s.Logger.DebugContext(ctx, "received admin change reply for unknown request")
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ code, hasErr := snac.Uint16BE(wire.AdminTLVErrorCode)
|
|
|
+
|
|
|
+ switch req.data.(type) {
|
|
|
+ case changePasswdReqCtx:
|
|
|
+ if hasErr {
|
|
|
+ switch code {
|
|
|
+ case wire.AdminInfoErrorInvalidPasswordLength:
|
|
|
+ return []string{"ERROR:" + wire.TOCErrorAdminInvalidInput}
|
|
|
+ case wire.AdminInfoErrorValidatePassword:
|
|
|
+ return []string{"ERROR:" + wire.TOCErrorAuthIncorrectNickOrPassword}
|
|
|
+ default:
|
|
|
+ return []string{"ERROR:" + wire.TOCErrorAdminProcessingRequest}
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return []string{"ADMIN_PASSWD_STATUS:0"}
|
|
|
+ case formatNickReqCtx:
|
|
|
+ if hasErr {
|
|
|
+ switch code {
|
|
|
+ case wire.AdminInfoErrorInvalidNickNameLength, wire.AdminInfoErrorInvalidNickName:
|
|
|
+ return []string{"ERROR:" + wire.TOCErrorAdminInvalidInput}
|
|
|
+ default:
|
|
|
+ return []string{"ERROR:" + wire.TOCErrorAdminProcessingRequest}
|
|
|
+ }
|
|
|
+ }
|
|
|
+ val, hasVal := snac.String(wire.AdminTLVScreenNameFormatted)
|
|
|
+ if !hasVal {
|
|
|
+ return s.runtimeErr(ctx, fmt.Errorf("AdminChangeReply: missing AdminTLVScreenNameFormatted %v", snac))
|
|
|
+ }
|
|
|
+ return []string{"ADMIN_NICK_STATUS:0", "NICK:" + val}
|
|
|
+ default:
|
|
|
+ return s.runtimeErr(ctx, fmt.Errorf("AdminChangeReply: unexpected request %v", req.frame))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// ChatNavInfo continues toc_chat_join and toc_chat_accept once the chat room
|
|
|
+// info arrives, by requesting a connection to the room.
|
|
|
+func (s OSCARProxy) ChatNavInfo(ctx context.Context, me *tocConn, frame wire.SNACFrame, snac wire.SNAC_0x0D_0x09_ChatNavNavInfo) []string {
|
|
|
+ req, ok := me.cache.take(frame)
|
|
|
+ if !ok {
|
|
|
+ s.Logger.DebugContext(ctx, "received chat nav info for unknown request")
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ b, hasInfo := snac.Bytes(wire.ChatNavTLVRoomInfo)
|
|
|
+ if !hasInfo {
|
|
|
+ return s.runtimeErr(ctx, errors.New("ChatNavNavInfo: missing wire.ChatNavTLVRoomInfo"))
|
|
|
+ }
|
|
|
+
|
|
|
+ roomInfo := wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{}
|
|
|
+ if err := wire.UnmarshalBE(&roomInfo, bytes.NewReader(b)); err != nil {
|
|
|
+ return s.runtimeErr(ctx, fmt.Errorf("wire.UnmarshalBE: %w", err))
|
|
|
+ }
|
|
|
+ roomName, hasName := roomInfo.String(wire.ChatRoomTLVRoomName)
|
|
|
+ if !hasName {
|
|
|
+ return s.runtimeErr(ctx, errors.New("roomInfo.String: missing wire.ChatRoomTLVRoomName"))
|
|
|
+ }
|
|
|
+
|
|
|
+ switch data := req.data.(type) {
|
|
|
+ case chatJoinReqCtx:
|
|
|
+ chatID := me.chats.Add(wire.ICBMRoomInfo{
|
|
|
+ Exchange: roomInfo.Exchange,
|
|
|
+ Cookie: roomInfo.Cookie,
|
|
|
+ Instance: roomInfo.InstanceNumber,
|
|
|
+ })
|
|
|
+ return s.requestChatService(ctx, me, chatID, roomName, roomInfo.Cookie)
|
|
|
+ case chatAcceptReqCtx:
|
|
|
+ return s.requestChatService(ctx, me, data.chatID, roomName, roomInfo.Cookie)
|
|
|
+ default:
|
|
|
+ return s.runtimeErr(ctx, fmt.Errorf("ChatNavNavInfo: unexpected request %v", req.frame))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// ChatServiceResponse finishes toc_chat_join and toc_chat_accept by
|
|
|
+// connecting to the chat room. It sends CHAT_JOIN before relaying anything
|
|
|
+// from the room, so that the client knows the room before its first
|
|
|
+// CHAT_UPDATE_BUDDY.
|
|
|
+func (s OSCARProxy) ChatServiceResponse(ctx context.Context, me *tocConn, frame wire.SNACFrame, snac wire.SNAC_0x01_0x05_OServiceServiceResponse) []string {
|
|
|
+ req, ok := me.cache.take(frame)
|
|
|
+ if !ok {
|
|
|
+ s.Logger.DebugContext(ctx, "received service response for unknown request")
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+ data, ok := req.data.(chatServiceReqCtx)
|
|
|
+ if !ok {
|
|
|
+ return s.runtimeErr(ctx, fmt.Errorf("OServiceServiceResponse: unexpected request %v", req.frame))
|
|
|
+ }
|
|
|
+
|
|
|
+ loginCookie, hasCookie := snac.Bytes(wire.OServiceTLVTagsLoginCookie)
|
|
|
+ if !hasCookie {
|
|
|
+ return s.runtimeErr(ctx, errors.New("OServiceServiceResponse: missing wire.OServiceTLVTagsLoginCookie"))
|
|
|
+ }
|
|
|
+
|
|
|
+ conn, err := s.connectChat(ctx, me, loginCookie)
|
|
|
+ if err != nil {
|
|
|
+ return s.runtimeErr(ctx, fmt.Errorf("connectChat: %w", err))
|
|
|
+ }
|
|
|
+ me.chats.RegisterConn(data.chatID, conn)
|
|
|
+
|
|
|
+ sendOrCancel(ctx, me.toCh, []string{fmt.Sprintf("CHAT_JOIN:%d:%s", data.chatID, data.roomName)})
|
|
|
+
|
|
|
+ me.goAsync(func() error {
|
|
|
+ s.RecvChat(ctx, me, conn, data.chatID)
|
|
|
+ return nil
|
|
|
+ })
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
// ChatIn handles the CHAT_IN and ENC_CHAT_IN TOC commands.
|
|
|
//
|
|
|
// From the TiK documentation:
|
|
|
@@ -107,7 +353,7 @@ func (s OSCARProxy) RecvChat(ctx context.Context, me *state.SessionInstance, cha
|
|
|
//
|
|
|
// Command syntax: CHAT_IN:<Chat Room Id>:<Source User>:<Whisper? T/F>:<Message>
|
|
|
// Command syntax: CHAT_IN_ENC:<chatroom id>:<user>:<whisper T/F>:<???>:en:<message>
|
|
|
-func (s OSCARProxy) ChatIn(ctx context.Context, me *state.SessionInstance, snac wire.SNAC_0x0E_0x06_ChatChannelMsgToClient, chatID int) []string {
|
|
|
+func (s OSCARProxy) ChatIn(ctx context.Context, me *tocConn, 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"))
|
|
|
@@ -204,12 +450,12 @@ func (s OSCARProxy) Eviled(snac wire.SNAC_0x01_0x10_OServiceEvilNotification) []
|
|
|
// Command syntax: IM_IN:<Source User>:<Auto Response T/F?>:<Message>
|
|
|
// Command syntax: IM_IN2:<Source User>:<Auto Response T/F?>:<Whisper?>:<Message>
|
|
|
// Command syntax: IM_IN_ENC2:<User>:<Auto>:<???>:<???>:<User Class>:<???>:<???>:<Language>:<Message>
|
|
|
-func (s OSCARProxy) IMIn(ctx context.Context, chatRegistry *ChatRegistry, me *state.SessionInstance, snac wire.SNAC_0x04_0x07_ICBMChannelMsgToClient) []string {
|
|
|
+func (s OSCARProxy) IMIn(ctx context.Context, me *tocConn, snac wire.SNAC_0x04_0x07_ICBMChannelMsgToClient) []string {
|
|
|
switch snac.ChannelID {
|
|
|
case wire.ICBMChannelIM:
|
|
|
return []string{s.convertICBMInstantMsg(ctx, me, snac)}
|
|
|
case wire.ICBMChannelRendezvous:
|
|
|
- return []string{s.convertICBMRendezvous(ctx, chatRegistry, snac)}
|
|
|
+ return []string{s.convertICBMRendezvous(ctx, me.chats, snac)}
|
|
|
default:
|
|
|
s.Logger.DebugContext(ctx, "received unsupported ICBM channel message", "channel_id", snac.ChannelID)
|
|
|
return []string{}
|
|
|
@@ -217,7 +463,7 @@ func (s OSCARProxy) IMIn(ctx context.Context, chatRegistry *ChatRegistry, me *st
|
|
|
}
|
|
|
|
|
|
// convertICBMInstantMsg converts an ICBM instant message SNAC to a TOC IM_IN or TOC2 IM_IN2, or TOC2Enhanced IM_IN_ENC2 response.
|
|
|
-func (s OSCARProxy) convertICBMInstantMsg(ctx context.Context, me *state.SessionInstance, snac wire.SNAC_0x04_0x07_ICBMChannelMsgToClient) string {
|
|
|
+func (s OSCARProxy) convertICBMInstantMsg(ctx context.Context, me *tocConn, snac wire.SNAC_0x04_0x07_ICBMChannelMsgToClient) string {
|
|
|
buf, ok := snac.Bytes(wire.ICBMTLVAOLIMData)
|
|
|
if !ok {
|
|
|
return s.runtimeErr(ctx, errors.New("TLVRestBlock.Bytes: missing wire.ICBMTLVAOLIMData"))[0]
|
|
|
@@ -353,7 +599,7 @@ func (s OSCARProxy) convertICBMRendezvous(ctx context.Context, chatRegistry *Cha
|
|
|
//
|
|
|
// For TOC2 this sends UPDATE_BUDDY2 with the same fields (plus a trailing field). When
|
|
|
// the buddy has capabilities, BUDDY_CAPS2 is also sent (see userInfoToBuddyCaps).
|
|
|
-func (s OSCARProxy) UpdateBuddyArrival(snac wire.SNAC_0x03_0x0B_BuddyArrived, me *state.SessionInstance) []string {
|
|
|
+func (s OSCARProxy) UpdateBuddyArrival(snac wire.SNAC_0x03_0x0B_BuddyArrived, me *tocConn) []string {
|
|
|
msgs := []string{userInfoToUpdateBuddy(snac.TLVUserInfo, me)}
|
|
|
if caps := userInfoToBuddyCaps(snac.TLVUserInfo, me, s.Logger); caps != "" {
|
|
|
msgs = append(msgs, caps)
|
|
|
@@ -383,7 +629,7 @@ func (s OSCARProxy) UpdateBuddyArrival(snac wire.SNAC_0x03_0x0B_BuddyArrived, me
|
|
|
//
|
|
|
// Command syntax: UPDATE_BUDDY:<Buddy User>:<Online? T/F>:<Evil Amount>:<Signon Time>:<IdleTime>:<UC>
|
|
|
// TOC2 uses UPDATE_BUDDY2 with the same fields.
|
|
|
-func (s OSCARProxy) UpdateBuddyDeparted(snac wire.SNAC_0x03_0x0C_BuddyDeparted, me *state.SessionInstance) []string {
|
|
|
+func (s OSCARProxy) UpdateBuddyDeparted(snac wire.SNAC_0x03_0x0C_BuddyDeparted, me *tocConn) []string {
|
|
|
if me.IsTOC2() {
|
|
|
return []string{fmt.Sprintf("UPDATE_BUDDY2:%s:F:0:0:0: :", snac.ScreenName)}
|
|
|
}
|
|
|
@@ -409,32 +655,13 @@ func (s OSCARProxy) UpdateBuddyDeparted(snac wire.SNAC_0x03_0x0C_BuddyDeparted,
|
|
|
// INSERTED2:p:<username>
|
|
|
// Somebody has been added to the permit list.
|
|
|
//
|
|
|
-// Inserted2 is invoked when this session receives FeedbagUpdateItem (e.g. list
|
|
|
-// modified from another client). The feedbag is queried when adding buddies to
|
|
|
-// resolve GroupID to group name; buddy alias comes from TLV.
|
|
|
-func (s OSCARProxy) Inserted2(ctx context.Context, me *state.SessionInstance, snac wire.SNAC_0x13_0x09_FeedbagUpdateItem) []string {
|
|
|
+// Inserted2 is invoked when this session receives FeedbagInsertItem or
|
|
|
+// FeedbagUpdateItem (e.g. list modified from another client). Group names come
|
|
|
+// from the feedbag mirror; buddy alias comes from TLV.
|
|
|
+func (s OSCARProxy) Inserted2(me *tocConn, items []wire.FeedbagItem) []string {
|
|
|
var out []string
|
|
|
- groupNameByID := make(map[uint16]string)
|
|
|
- hasBuddy := false
|
|
|
- for _, item := range snac.Items {
|
|
|
- if item.ClassID == wire.FeedbagClassIdBuddy {
|
|
|
- hasBuddy = true
|
|
|
- break
|
|
|
- }
|
|
|
- }
|
|
|
- if hasBuddy {
|
|
|
- fb, err := s.FeedbagManager.Feedbag(ctx, me.IdentScreenName())
|
|
|
- if err != nil {
|
|
|
- s.Logger.DebugContext(ctx, "Inserted2: feedbag lookup failed", "err", err)
|
|
|
- return nil
|
|
|
- }
|
|
|
- for _, item := range fb {
|
|
|
- if item.ClassID == wire.FeedbagClassIdGroup {
|
|
|
- groupNameByID[item.GroupID] = item.Name
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- for _, item := range snac.Items {
|
|
|
+ groupNameByID := me.feedbag.groupNames()
|
|
|
+ for _, item := range items {
|
|
|
switch item.ClassID {
|
|
|
case wire.FeedbagClassIdGroup:
|
|
|
out = append(out, fmt.Sprintf("INSERTED2:g:%s", item.Name))
|
|
|
@@ -477,30 +704,11 @@ func (s OSCARProxy) Inserted2(ctx context.Context, me *state.SessionInstance, sn
|
|
|
// A user has been removed from the permit list.
|
|
|
//
|
|
|
// Deleted2 is invoked when this session receives FeedbagDeleteItem (e.g. list
|
|
|
-// modified from another client). The feedbag is queried when deleting buddies to
|
|
|
-// resolve GroupID to group name.
|
|
|
-func (s OSCARProxy) Deleted2(ctx context.Context, me *state.SessionInstance, snac wire.SNAC_0x13_0x0A_FeedbagDeleteItem) []string {
|
|
|
+// modified from another client), before the items leave the feedbag mirror, so
|
|
|
+// that deleted buddies' group names can still be found there.
|
|
|
+func (s OSCARProxy) Deleted2(me *tocConn, snac wire.SNAC_0x13_0x0A_FeedbagDeleteItem) []string {
|
|
|
var out []string
|
|
|
- groupNameByID := make(map[uint16]string)
|
|
|
- hasBuddy := false
|
|
|
- for _, item := range snac.Items {
|
|
|
- if item.ClassID == wire.FeedbagClassIdBuddy {
|
|
|
- hasBuddy = true
|
|
|
- break
|
|
|
- }
|
|
|
- }
|
|
|
- if hasBuddy {
|
|
|
- fb, err := s.FeedbagManager.Feedbag(ctx, me.IdentScreenName())
|
|
|
- if err != nil {
|
|
|
- s.Logger.DebugContext(ctx, "Deleted2: feedbag lookup failed", "err", err)
|
|
|
- return nil
|
|
|
- }
|
|
|
- for _, item := range fb {
|
|
|
- if item.ClassID == wire.FeedbagClassIdGroup {
|
|
|
- groupNameByID[item.GroupID] = item.Name
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+ groupNameByID := me.feedbag.groupNames()
|
|
|
for _, item := range snac.Items {
|
|
|
switch item.ClassID {
|
|
|
case wire.FeedbagClassIdGroup:
|
|
|
@@ -574,7 +782,7 @@ func sendOrCancel(ctx context.Context, ch chan<- []string, msg []string) {
|
|
|
|
|
|
// userInfoToUpdateBuddy creates an UPDATE_BUDDY or UPDATE_BUDDY2 server reply from a User
|
|
|
// Info TLV.
|
|
|
-func userInfoToUpdateBuddy(snac wire.TLVUserInfo, me *state.SessionInstance) string {
|
|
|
+func userInfoToUpdateBuddy(snac wire.TLVUserInfo, me *tocConn) string {
|
|
|
online, _ := snac.Uint32BE(wire.OServiceUserInfoSignonTOD)
|
|
|
idle, _ := snac.Uint16BE(wire.OServiceUserInfoIdleTime)
|
|
|
|
|
|
@@ -603,7 +811,7 @@ func hasFlag[T ~uint16 | ~uint8](bitmask, flag T) bool {
|
|
|
// Direct Connect, you will get sent both packets.
|
|
|
//
|
|
|
// Format: BUDDY_CAPS2:<User>:<Cap1>,<Cap2>,...
|
|
|
-func userInfoToBuddyCaps(snac wire.TLVUserInfo, me *state.SessionInstance, logger *slog.Logger) string {
|
|
|
+func userInfoToBuddyCaps(snac wire.TLVUserInfo, me *tocConn, logger *slog.Logger) string {
|
|
|
if !me.IsTOC2() {
|
|
|
return ""
|
|
|
}
|