|
|
@@ -130,6 +130,8 @@ type OSCARProxy struct {
|
|
|
PermitDenyService PermitDenyService
|
|
|
TOCConfigStore TOCConfigStore
|
|
|
SessionRetriever SessionRetriever
|
|
|
+ FeedbagService FeedbagService
|
|
|
+ FeedbagManager FeedbagManager
|
|
|
SNACRateLimits wire.SNACRateLimits
|
|
|
HTTPIPRateLimiter *IPRateLimiter
|
|
|
}
|
|
|
@@ -148,9 +150,9 @@ func (s OSCARProxy) RecvClientCmd(
|
|
|
sessBOS *state.SessionInstance,
|
|
|
chatRegistry *ChatRegistry,
|
|
|
payload []byte,
|
|
|
- toCh chan<- []byte,
|
|
|
+ toCh chan<- []string,
|
|
|
doAsync func(f func() error),
|
|
|
-) (reply string) {
|
|
|
+) (replies []string) {
|
|
|
|
|
|
cmd := payload
|
|
|
var args []byte
|
|
|
@@ -193,7 +195,7 @@ func (s OSCARProxy) RecvClientCmd(
|
|
|
return s.FormatNickname(ctx, sessBOS, args)
|
|
|
case "toc_chat_join", "toc_chat_accept":
|
|
|
var chatID int
|
|
|
- var msg string
|
|
|
+ var msg []string
|
|
|
|
|
|
if string(cmd) == "toc_chat_join" {
|
|
|
chatID, msg = s.ChatJoin(ctx, sessBOS, chatRegistry, args)
|
|
|
@@ -201,7 +203,7 @@ func (s OSCARProxy) RecvClientCmd(
|
|
|
chatID, msg = s.ChatAccept(ctx, sessBOS, chatRegistry, args)
|
|
|
}
|
|
|
|
|
|
- if msg == cmdInternalSvcErr {
|
|
|
+ if msg[0] == cmdInternalSvcErr {
|
|
|
return msg
|
|
|
}
|
|
|
|
|
|
@@ -236,10 +238,16 @@ func (s OSCARProxy) RecvClientCmd(
|
|
|
return s.RvousAccept(ctx, sessBOS, args)
|
|
|
case "toc_rvous_cancel":
|
|
|
return s.RvousCancel(ctx, sessBOS, args)
|
|
|
+ case "toc2_set_pdmode":
|
|
|
+ return s.SetPDMode(ctx, sessBOS, args)
|
|
|
+ case "toc2_send_im_enc":
|
|
|
+ return s.SendIMEnc(ctx, sessBOS, args)
|
|
|
+ case "toc2_remove_buddy":
|
|
|
+ return s.RemoveBuddy2(ctx, sessBOS, args)
|
|
|
}
|
|
|
|
|
|
s.Logger.ErrorContext(ctx, fmt.Sprintf("unsupported TOC command %s", cmd))
|
|
|
- return cmdInternalSvcErr
|
|
|
+ return []string{cmdInternalSvcErr}
|
|
|
}
|
|
|
|
|
|
// AddBuddy handles the toc_add_buddy TOC command.
|
|
|
@@ -249,7 +257,7 @@ func (s OSCARProxy) RecvClientCmd(
|
|
|
// Add buddies to your buddy list. This does not change your saved config.
|
|
|
//
|
|
|
// Command syntax: toc_add_buddy <Buddy User 1> [<Buddy User2> [<Buddy User 3> [...]]]
|
|
|
-func (s OSCARProxy) AddBuddy(ctx context.Context, me *state.SessionInstance, args []byte) string {
|
|
|
+func (s OSCARProxy) AddBuddy(ctx context.Context, me *state.SessionInstance, args []byte) []string {
|
|
|
if msg, isLimited := s.checkRateLimit(ctx, me, wire.Buddy, wire.BuddyAddBuddies); isLimited {
|
|
|
return msg
|
|
|
}
|
|
|
@@ -270,7 +278,7 @@ func (s OSCARProxy) AddBuddy(ctx context.Context, me *state.SessionInstance, arg
|
|
|
return s.runtimeErr(ctx, fmt.Errorf("BuddyService.AddBuddies: %w", err))
|
|
|
}
|
|
|
|
|
|
- return ""
|
|
|
+ return []string{}
|
|
|
}
|
|
|
|
|
|
// AddPermit handles the toc_add_permit TOC command.
|
|
|
@@ -283,7 +291,7 @@ func (s OSCARProxy) AddBuddy(ctx context.Context, me *state.SessionInstance, arg
|
|
|
// arguments does nothing and your permit list remains the same.
|
|
|
//
|
|
|
// Command syntax: toc_add_permit [ <User 1> [<User 2> [...]]]
|
|
|
-func (s OSCARProxy) AddPermit(ctx context.Context, me *state.SessionInstance, args []byte) string {
|
|
|
+func (s OSCARProxy) AddPermit(ctx context.Context, me *state.SessionInstance, args []byte) []string {
|
|
|
if msg, isLimited := s.checkRateLimit(ctx, me, wire.PermitDeny, wire.PermitDenyAddDenyListEntries); isLimited {
|
|
|
return msg
|
|
|
}
|
|
|
@@ -303,7 +311,7 @@ func (s OSCARProxy) AddPermit(ctx context.Context, me *state.SessionInstance, ar
|
|
|
if err := s.PermitDenyService.AddPermListEntries(ctx, me, snac); err != nil {
|
|
|
return s.runtimeErr(ctx, fmt.Errorf("PermitDenyService.AddPermListEntries: %w", err))
|
|
|
}
|
|
|
- return ""
|
|
|
+ return []string{}
|
|
|
}
|
|
|
|
|
|
// AddDeny handles the toc_add_deny TOC command.
|
|
|
@@ -316,7 +324,7 @@ func (s OSCARProxy) AddPermit(ctx context.Context, me *state.SessionInstance, ar
|
|
|
// does nothing and your deny list remains unchanged.
|
|
|
//
|
|
|
// Command syntax: toc_add_deny [ <User 1> [<User 2> [...]]]
|
|
|
-func (s OSCARProxy) AddDeny(ctx context.Context, me *state.SessionInstance, args []byte) string {
|
|
|
+func (s OSCARProxy) AddDeny(ctx context.Context, me *state.SessionInstance, args []byte) []string {
|
|
|
if msg, isLimited := s.checkRateLimit(ctx, me, wire.PermitDeny, wire.PermitDenyAddDenyListEntries); isLimited {
|
|
|
return msg
|
|
|
}
|
|
|
@@ -336,7 +344,7 @@ func (s OSCARProxy) AddDeny(ctx context.Context, me *state.SessionInstance, args
|
|
|
if err := s.PermitDenyService.AddDenyListEntries(ctx, me, snac); err != nil {
|
|
|
return s.runtimeErr(ctx, fmt.Errorf("PermitDenyService.AddDenyListEntries: %w", err))
|
|
|
}
|
|
|
- return ""
|
|
|
+ return []string{}
|
|
|
}
|
|
|
|
|
|
// ChangePassword handles the toc_change_passwd TOC command.
|
|
|
@@ -347,7 +355,7 @@ func (s OSCARProxy) AddDeny(ctx context.Context, me *state.SessionInstance, args
|
|
|
// sent back to the client.
|
|
|
//
|
|
|
// Command syntax: toc_change_passwd <existing_passwd> <new_passwd>
|
|
|
-func (s OSCARProxy) ChangePassword(ctx context.Context, me *state.SessionInstance, args []byte) string {
|
|
|
+func (s OSCARProxy) ChangePassword(ctx context.Context, me *state.SessionInstance, args []byte) []string {
|
|
|
if msg, isLimited := s.checkRateLimit(ctx, me, wire.Admin, wire.AdminInfoChangeRequest); isLimited {
|
|
|
return msg
|
|
|
}
|
|
|
@@ -383,15 +391,15 @@ func (s OSCARProxy) ChangePassword(ctx context.Context, me *state.SessionInstanc
|
|
|
if ok {
|
|
|
switch code {
|
|
|
case wire.AdminInfoErrorInvalidPasswordLength:
|
|
|
- return "ERROR:911"
|
|
|
+ return []string{"ERROR:" + wire.TOCErrorAdminInvalidInput}
|
|
|
case wire.AdminInfoErrorValidatePassword:
|
|
|
- return "ERROR:912"
|
|
|
+ return []string{"ERROR:" + wire.TOCErrorAdminInvalidAccount} // jgk: is this correct error code?
|
|
|
default:
|
|
|
- return "ERROR:913"
|
|
|
+ return []string{"ERROR:" + wire.TOCErrorAdminProcessingRequest}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- return "ADMIN_PASSWD_STATUS:0"
|
|
|
+ return []string{"ADMIN_PASSWD_STATUS:0"}
|
|
|
}
|
|
|
|
|
|
// ChatAccept handles the toc_chat_accept TOC command.
|
|
|
@@ -407,7 +415,7 @@ func (s OSCARProxy) ChatAccept(
|
|
|
me *state.SessionInstance,
|
|
|
chatRegistry *ChatRegistry,
|
|
|
args []byte,
|
|
|
-) (int, string) {
|
|
|
+) (int, []string) {
|
|
|
|
|
|
var chatIDStr string
|
|
|
|
|
|
@@ -510,7 +518,7 @@ func (s OSCARProxy) ChatAccept(
|
|
|
|
|
|
chatRegistry.RegisterSess(chatID, chatSess)
|
|
|
|
|
|
- return chatID, fmt.Sprintf("CHAT_JOIN:%d:%s", chatID, roomName)
|
|
|
+ return chatID, []string{fmt.Sprintf("CHAT_JOIN:%d:%s", chatID, roomName)}
|
|
|
}
|
|
|
|
|
|
// ChatInvite handles the toc_chat_invite TOC command.
|
|
|
@@ -521,7 +529,7 @@ func (s OSCARProxy) ChatAccept(
|
|
|
// Remember to quote and encode the invite message.
|
|
|
//
|
|
|
// Command syntax: toc_chat_invite <Chat Room ID> <Invite Msg> <buddy1> [<buddy2> [<buddy3> [...]]]
|
|
|
-func (s OSCARProxy) ChatInvite(ctx context.Context, me *state.SessionInstance, chatRegistry *ChatRegistry, args []byte) string {
|
|
|
+func (s OSCARProxy) ChatInvite(ctx context.Context, me *state.SessionInstance, chatRegistry *ChatRegistry, args []byte) []string {
|
|
|
var chatRoomIDStr, msg string
|
|
|
|
|
|
users, err := parseArgs(args, &chatRoomIDStr, &msg)
|
|
|
@@ -572,7 +580,7 @@ func (s OSCARProxy) ChatInvite(ctx context.Context, me *state.SessionInstance, c
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- return ""
|
|
|
+ return []string{}
|
|
|
}
|
|
|
|
|
|
// ChatJoin handles the toc_chat_join TOC command.
|
|
|
@@ -594,7 +602,7 @@ func (s OSCARProxy) ChatJoin(
|
|
|
me *state.SessionInstance,
|
|
|
chatRegistry *ChatRegistry,
|
|
|
args []byte,
|
|
|
-) (int, string) {
|
|
|
+) (int, []string) {
|
|
|
var exchangeStr, roomName string
|
|
|
|
|
|
if _, err := parseArgs(args, &exchangeStr, &roomName); err != nil {
|
|
|
@@ -699,7 +707,7 @@ func (s OSCARProxy) ChatJoin(
|
|
|
chatID := chatRegistry.Add(roomInfo)
|
|
|
chatRegistry.RegisterSess(chatID, chatSess)
|
|
|
|
|
|
- return chatID, fmt.Sprintf("CHAT_JOIN:%d:%s", chatID, roomName)
|
|
|
+ return chatID, []string{fmt.Sprintf("CHAT_JOIN:%d:%s", chatID, roomName)}
|
|
|
}
|
|
|
|
|
|
// ChatLeave handles the toc_chat_leave TOC command.
|
|
|
@@ -709,7 +717,7 @@ func (s OSCARProxy) ChatJoin(
|
|
|
// Leave the chat room.
|
|
|
//
|
|
|
// Command syntax: toc_chat_leave <Chat Room ID>
|
|
|
-func (s OSCARProxy) ChatLeave(ctx context.Context, chatRegistry *ChatRegistry, args []byte) string {
|
|
|
+func (s OSCARProxy) ChatLeave(ctx context.Context, chatRegistry *ChatRegistry, args []byte) []string {
|
|
|
var chatIDStr string
|
|
|
|
|
|
if _, err := parseArgs(args, &chatIDStr); err != nil {
|
|
|
@@ -732,7 +740,7 @@ func (s OSCARProxy) ChatLeave(ctx context.Context, chatRegistry *ChatRegistry, a
|
|
|
|
|
|
chatRegistry.RemoveSess(chatID)
|
|
|
|
|
|
- return fmt.Sprintf("CHAT_LEFT:%d", chatID)
|
|
|
+ return []string{fmt.Sprintf("CHAT_LEFT:%d", chatID)}
|
|
|
}
|
|
|
|
|
|
// ChatSend handles the toc_chat_send TOC command.
|
|
|
@@ -745,7 +753,7 @@ func (s OSCARProxy) ChatLeave(ctx context.Context, chatRegistry *ChatRegistry, a
|
|
|
// and encode the message.
|
|
|
//
|
|
|
// Command syntax: toc_chat_send <Chat Room ID> <Message>
|
|
|
-func (s OSCARProxy) ChatSend(ctx context.Context, chatRegistry *ChatRegistry, args []byte) string {
|
|
|
+func (s OSCARProxy) ChatSend(ctx context.Context, chatRegistry *ChatRegistry, args []byte) []string {
|
|
|
var chatIDStr, msg string
|
|
|
|
|
|
if _, err := parseArgs(args, &chatIDStr, &msg); err != nil {
|
|
|
@@ -813,7 +821,7 @@ func (s OSCARProxy) ChatSend(ctx context.Context, chatRegistry *ChatRegistry, ar
|
|
|
return s.runtimeErr(ctx, fmt.Errorf("wire.UnmarshalBE: %w", err))
|
|
|
}
|
|
|
|
|
|
- return fmt.Sprintf("CHAT_IN:%d:%s:F:%s", chatID, userInfo.ScreenName, reflectMsg)
|
|
|
+ return []string{fmt.Sprintf("CHAT_IN:%d:%s:F:%s", chatID, userInfo.ScreenName, reflectMsg)}
|
|
|
default:
|
|
|
return s.runtimeErr(ctx, errors.New("ChatService.ChannelMsgToHost: unexpected response"))
|
|
|
}
|
|
|
@@ -830,7 +838,7 @@ func (s OSCARProxy) ChatSend(ctx context.Context, chatRegistry *ChatRegistry, ar
|
|
|
// be displayed in the chat room UI.
|
|
|
//
|
|
|
// Command syntax: toc_chat_whisper <Chat Room ID> <dst_user> <Message>
|
|
|
-func (s OSCARProxy) ChatWhisper(ctx context.Context, chatRegistry *ChatRegistry, args []byte) string {
|
|
|
+func (s OSCARProxy) ChatWhisper(ctx context.Context, chatRegistry *ChatRegistry, args []byte) []string {
|
|
|
var chatIDStr, recip, msg string
|
|
|
|
|
|
if _, err := parseArgs(args, &chatIDStr, &recip, &msg); err != nil {
|
|
|
@@ -869,7 +877,7 @@ func (s OSCARProxy) ChatWhisper(ctx context.Context, chatRegistry *ChatRegistry,
|
|
|
return s.runtimeErr(ctx, fmt.Errorf("ChatService.ChannelMsgToHost: %w", err))
|
|
|
}
|
|
|
|
|
|
- return ""
|
|
|
+ return []string{}
|
|
|
}
|
|
|
|
|
|
// Evil handles the toc_evil TOC command.
|
|
|
@@ -881,8 +889,10 @@ func (s OSCARProxy) ChatWhisper(ctx context.Context, chatRegistry *ChatRegistry,
|
|
|
// people who have recently sent you ims. The higher someones evil level, the
|
|
|
// slower they can send message.
|
|
|
//
|
|
|
+// An ERROR message will be sent back to the client if the warning was unsuccessful.
|
|
|
+//
|
|
|
// Command syntax: toc_evil <User> <norm|anon>
|
|
|
-func (s OSCARProxy) Evil(ctx context.Context, me *state.SessionInstance, args []byte) string {
|
|
|
+func (s OSCARProxy) Evil(ctx context.Context, me *state.SessionInstance, args []byte) []string {
|
|
|
if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.ICBM, wire.ICBMEvilRequest); isLimited {
|
|
|
return errMsg
|
|
|
}
|
|
|
@@ -913,14 +923,13 @@ func (s OSCARProxy) Evil(ctx context.Context, me *state.SessionInstance, args []
|
|
|
|
|
|
switch v := response.Body.(type) {
|
|
|
case wire.SNAC_0x04_0x09_ICBMEvilReply:
|
|
|
- return ""
|
|
|
+ return []string{}
|
|
|
case wire.SNACError:
|
|
|
s.Logger.InfoContext(ctx, "unable to warn user", "code", v.Code)
|
|
|
+ return []string{"ERROR:" + wire.TOCErrorGeneralWarningUserNotAvailable}
|
|
|
default:
|
|
|
return s.runtimeErr(ctx, errors.New("unexpected response"))
|
|
|
}
|
|
|
-
|
|
|
- return ""
|
|
|
}
|
|
|
|
|
|
// FormatNickname handles the toc_format_nickname TOC command.
|
|
|
@@ -930,8 +939,13 @@ func (s OSCARProxy) Evil(ctx context.Context, me *state.SessionInstance, args []
|
|
|
// Reformat a user's nickname. An ADMIN_NICK_STATUS or ERROR message will be
|
|
|
// sent back to the client.
|
|
|
//
|
|
|
+// From the BizTOCSock documentation:
|
|
|
+//
|
|
|
+// [NICK is also sent with ADMIN_NICK_STATUS. This gets called [...] whenever
|
|
|
+// you do a format change.
|
|
|
+//
|
|
|
// Command syntax: toc_format_nickname <new_format>
|
|
|
-func (s OSCARProxy) FormatNickname(ctx context.Context, me *state.SessionInstance, args []byte) string {
|
|
|
+func (s OSCARProxy) FormatNickname(ctx context.Context, me *state.SessionInstance, args []byte) []string {
|
|
|
if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.Admin, wire.AdminInfoChangeRequest); isLimited {
|
|
|
return errMsg
|
|
|
}
|
|
|
@@ -967,13 +981,17 @@ func (s OSCARProxy) FormatNickname(ctx context.Context, me *state.SessionInstanc
|
|
|
if ok {
|
|
|
switch code {
|
|
|
case wire.AdminInfoErrorInvalidNickNameLength, wire.AdminInfoErrorInvalidNickName:
|
|
|
- return "ERROR:911"
|
|
|
+ return []string{"ERROR:" + wire.TOCErrorAdminInvalidInput}
|
|
|
default:
|
|
|
- return "ERROR:913"
|
|
|
+ return []string{"ERROR:" + wire.TOCErrorAdminProcessingRequest}
|
|
|
}
|
|
|
}
|
|
|
+ val, hasVal := replyBody.TLVBlock.String(wire.AdminTLVScreenNameFormatted)
|
|
|
+ if !hasVal {
|
|
|
+ return s.runtimeErr(ctx, fmt.Errorf("AdminService.InfoChangeRequest: missing AdminTLVScreenNameFormatted %v", replyBody))
|
|
|
+ }
|
|
|
|
|
|
- return "ADMIN_NICK_STATUS:0"
|
|
|
+ return []string{"ADMIN_NICK_STATUS:0", "NICK:" + val}
|
|
|
}
|
|
|
|
|
|
// GetDirSearchURL handles the toc_dir_search TOC command.
|
|
|
@@ -992,9 +1010,9 @@ func (s OSCARProxy) FormatNickname(ctx context.Context, me *state.SessionInstanc
|
|
|
// Returns either a GOTO_URL or ERROR msg.
|
|
|
//
|
|
|
// Command syntax: toc_dir_search <info information>
|
|
|
-func (s OSCARProxy) GetDirSearchURL(ctx context.Context, me *state.SessionInstance, args []byte) string {
|
|
|
+func (s OSCARProxy) GetDirSearchURL(ctx context.Context, me *state.SessionInstance, args []byte) []string {
|
|
|
if status := me.Session().EvaluateRateLimit(time.Now(), 1); status == wire.RateLimitStatusLimited {
|
|
|
- return rateLimitExceededErr
|
|
|
+ return []string{rateLimitExceededErr}
|
|
|
}
|
|
|
|
|
|
var info string
|
|
|
@@ -1040,7 +1058,7 @@ func (s OSCARProxy) GetDirSearchURL(ctx context.Context, me *state.SessionInstan
|
|
|
}
|
|
|
p.Add("cookie", cookie)
|
|
|
|
|
|
- return fmt.Sprintf("GOTO_URL:search results:dir_search?%s", p.Encode())
|
|
|
+ return []string{fmt.Sprintf("GOTO_URL:search results:dir_search?%s", p.Encode())}
|
|
|
}
|
|
|
|
|
|
// GetDirURL handles the toc_get_dir TOC command.
|
|
|
@@ -1050,9 +1068,9 @@ func (s OSCARProxy) GetDirSearchURL(ctx context.Context, me *state.SessionInstan
|
|
|
// Gets a user's dir info a GOTO_URL or ERROR message will be sent back to the client.
|
|
|
//
|
|
|
// Command syntax: toc_get_dir <username>
|
|
|
-func (s OSCARProxy) GetDirURL(ctx context.Context, me *state.SessionInstance, args []byte) string {
|
|
|
+func (s OSCARProxy) GetDirURL(ctx context.Context, me *state.SessionInstance, args []byte) []string {
|
|
|
if status := me.Session().EvaluateRateLimit(time.Now(), 1); status == wire.RateLimitStatusLimited {
|
|
|
- return rateLimitExceededErr
|
|
|
+ return []string{rateLimitExceededErr}
|
|
|
}
|
|
|
|
|
|
var user string
|
|
|
@@ -1070,7 +1088,7 @@ func (s OSCARProxy) GetDirURL(ctx context.Context, me *state.SessionInstance, ar
|
|
|
p.Add("cookie", cookie)
|
|
|
p.Add("user", user)
|
|
|
|
|
|
- return fmt.Sprintf("GOTO_URL:directory info:dir_info?%s", p.Encode())
|
|
|
+ return []string{fmt.Sprintf("GOTO_URL:directory info:dir_info?%s", p.Encode())}
|
|
|
}
|
|
|
|
|
|
// GetInfoURL handles the toc_get_info TOC command.
|
|
|
@@ -1080,9 +1098,9 @@ func (s OSCARProxy) GetDirURL(ctx context.Context, me *state.SessionInstance, ar
|
|
|
// Gets a user's info a GOTO_URL or ERROR message will be sent back to the client.
|
|
|
//
|
|
|
// Command syntax: toc_get_info <username>
|
|
|
-func (s OSCARProxy) GetInfoURL(ctx context.Context, me *state.SessionInstance, args []byte) string {
|
|
|
+func (s OSCARProxy) GetInfoURL(ctx context.Context, me *state.SessionInstance, args []byte) []string {
|
|
|
if status := me.Session().EvaluateRateLimit(time.Now(), 1); status == wire.RateLimitStatusLimited {
|
|
|
- return rateLimitExceededErr
|
|
|
+ return []string{rateLimitExceededErr}
|
|
|
}
|
|
|
|
|
|
var user string
|
|
|
@@ -1101,7 +1119,7 @@ func (s OSCARProxy) GetInfoURL(ctx context.Context, me *state.SessionInstance, a
|
|
|
p.Add("from", me.IdentScreenName().String())
|
|
|
p.Add("user", user)
|
|
|
|
|
|
- return fmt.Sprintf("GOTO_URL:profile:info?%s", p.Encode())
|
|
|
+ return []string{fmt.Sprintf("GOTO_URL:profile:info?%s", p.Encode())}
|
|
|
}
|
|
|
|
|
|
// GetStatus handles the toc_get_status TOC command.
|
|
|
@@ -1113,7 +1131,7 @@ func (s OSCARProxy) GetInfoURL(ctx context.Context, me *state.SessionInstance, a
|
|
|
// guy appears to be online.
|
|
|
//
|
|
|
// Command syntax: toc_get_status <screenname>
|
|
|
-func (s OSCARProxy) GetStatus(ctx context.Context, me *state.SessionInstance, args []byte) string {
|
|
|
+func (s OSCARProxy) GetStatus(ctx context.Context, me *state.SessionInstance, args []byte) []string {
|
|
|
if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.Locate, wire.LocateUserInfoQuery); isLimited {
|
|
|
return errMsg
|
|
|
}
|
|
|
@@ -1136,12 +1154,12 @@ func (s OSCARProxy) GetStatus(ctx context.Context, me *state.SessionInstance, ar
|
|
|
switch v := info.Body.(type) {
|
|
|
case wire.SNACError:
|
|
|
if v.Code == wire.ErrorCodeNotLoggedOn {
|
|
|
- return fmt.Sprintf("ERROR:901:%s", them)
|
|
|
+ return []string{fmt.Sprintf("ERROR:%s:%s", wire.TOCErrorGeneralUserNotAvailable, them)}
|
|
|
} else {
|
|
|
return s.runtimeErr(ctx, fmt.Errorf("LocateService.UserInfoQuery error code: %d", v.Code))
|
|
|
}
|
|
|
case wire.SNAC_0x02_0x06_LocateUserInfoReply:
|
|
|
- return userInfoToUpdateBuddy(v.TLVUserInfo)
|
|
|
+ return []string{userInfoToUpdateBuddy(v.TLVUserInfo, me)}
|
|
|
default:
|
|
|
return s.runtimeErr(ctx, fmt.Errorf("AdminService.InfoChangeRequest: unexpected response type %v", v))
|
|
|
}
|
|
|
@@ -1162,14 +1180,18 @@ func (s OSCARProxy) GetStatus(ctx context.Context, me *state.SessionInstance, ar
|
|
|
// implemented.
|
|
|
//
|
|
|
// Command syntax: toc_init_done
|
|
|
-func (s OSCARProxy) InitDone(ctx context.Context, instance *state.SessionInstance) string {
|
|
|
+func (s OSCARProxy) InitDone(ctx context.Context, instance *state.SessionInstance) []string {
|
|
|
+ err := s.FeedbagManager.UseFeedbag(ctx, instance.IdentScreenName())
|
|
|
+ if err != nil {
|
|
|
+ return s.runtimeErr(ctx, fmt.Errorf("FeedbagManager.UseFeedbag: %w", err))
|
|
|
+ }
|
|
|
if errMsg, isLimited := s.checkRateLimit(ctx, instance, wire.OService, wire.OServiceClientOnline); isLimited {
|
|
|
return errMsg
|
|
|
}
|
|
|
if err := s.OServiceService.ClientOnline(ctx, wire.BOS, wire.SNAC_0x01_0x02_OServiceClientOnline{}, instance); err != nil {
|
|
|
return s.runtimeErr(ctx, fmt.Errorf("OServiceServiceBOS.ClientOnliney: %w", err))
|
|
|
}
|
|
|
- return ""
|
|
|
+ return []string{}
|
|
|
}
|
|
|
|
|
|
// RemoveBuddy handles the toc_remove_buddy TOC command.
|
|
|
@@ -1179,7 +1201,7 @@ func (s OSCARProxy) InitDone(ctx context.Context, instance *state.SessionInstanc
|
|
|
// Remove buddies from your buddy list. This does not change your saved config.
|
|
|
//
|
|
|
// Command syntax: toc_remove_buddy <Buddy User 1> [<Buddy User2> [<Buddy User 3> [...]]]
|
|
|
-func (s OSCARProxy) RemoveBuddy(ctx context.Context, me *state.SessionInstance, args []byte) string {
|
|
|
+func (s OSCARProxy) RemoveBuddy(ctx context.Context, me *state.SessionInstance, args []byte) []string {
|
|
|
if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.Buddy, wire.BuddyDelBuddies); isLimited {
|
|
|
return errMsg
|
|
|
}
|
|
|
@@ -1199,7 +1221,7 @@ func (s OSCARProxy) RemoveBuddy(ctx context.Context, me *state.SessionInstance,
|
|
|
if err := s.BuddyService.DelBuddies(ctx, me, snac); err != nil {
|
|
|
return s.runtimeErr(ctx, fmt.Errorf("BuddyService.DelBuddies: %w", err))
|
|
|
}
|
|
|
- return ""
|
|
|
+ return []string{}
|
|
|
}
|
|
|
|
|
|
// RvousAccept handles the toc_rvous_accept TOC command.
|
|
|
@@ -1214,7 +1236,7 @@ func (s OSCARProxy) RemoveBuddy(ctx context.Context, me *state.SessionInstance,
|
|
|
// passed in the TiK client, the reference implementation.
|
|
|
//
|
|
|
// Command syntax: toc_rvous_accept <nick> <cookie> <service>
|
|
|
-func (s OSCARProxy) RvousAccept(ctx context.Context, me *state.SessionInstance, args []byte) string {
|
|
|
+func (s OSCARProxy) RvousAccept(ctx context.Context, me *state.SessionInstance, args []byte) []string {
|
|
|
if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.ICBM, wire.ICBMChannelMsgToHost); isLimited {
|
|
|
return errMsg
|
|
|
}
|
|
|
@@ -1256,7 +1278,7 @@ func (s OSCARProxy) RvousAccept(ctx context.Context, me *state.SessionInstance,
|
|
|
return s.runtimeErr(ctx, fmt.Errorf("ICBMService.ChannelMsgToHost: %w", err))
|
|
|
}
|
|
|
|
|
|
- return ""
|
|
|
+ return []string{}
|
|
|
}
|
|
|
|
|
|
// RvousCancel handles the toc_rvous_cancel TOC command.
|
|
|
@@ -1271,7 +1293,7 @@ func (s OSCARProxy) RvousAccept(ctx context.Context, me *state.SessionInstance,
|
|
|
// passed in the TiK client, the reference implementation.
|
|
|
//
|
|
|
// Command syntax: toc_rvous_cancel <nick> <cookie> <service>
|
|
|
-func (s OSCARProxy) RvousCancel(ctx context.Context, me *state.SessionInstance, args []byte) string {
|
|
|
+func (s OSCARProxy) RvousCancel(ctx context.Context, me *state.SessionInstance, args []byte) []string {
|
|
|
if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.ICBM, wire.ICBMChannelMsgToHost); isLimited {
|
|
|
return errMsg
|
|
|
}
|
|
|
@@ -1318,7 +1340,115 @@ func (s OSCARProxy) RvousCancel(ctx context.Context, me *state.SessionInstance,
|
|
|
return s.runtimeErr(ctx, fmt.Errorf("ICBMService.ChannelMsgToHost: %w", err))
|
|
|
}
|
|
|
|
|
|
- return ""
|
|
|
+ return []string{}
|
|
|
+}
|
|
|
+
|
|
|
+// SetPDMode handles the toc2_set_pdmode TOC2 command.
|
|
|
+//
|
|
|
+
|
|
|
+// Command syntax: toc2_set_pdmode <mode>
|
|
|
+func (s OSCARProxy) SetPDMode(ctx context.Context, me *state.SessionInstance, args []byte) []string {
|
|
|
+ if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.OService, wire.OServiceIdleNotification); isLimited {
|
|
|
+ return errMsg
|
|
|
+ }
|
|
|
+
|
|
|
+ var pdModeStr string
|
|
|
+
|
|
|
+ if _, err := parseArgs(args, &pdModeStr); err != nil {
|
|
|
+ return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
|
|
|
+ }
|
|
|
+
|
|
|
+ mode, err := strconv.Atoi(pdModeStr)
|
|
|
+ if err != nil {
|
|
|
+ return s.runtimeErr(ctx, fmt.Errorf("strconv.Atoi: %w", err))
|
|
|
+ }
|
|
|
+
|
|
|
+ if mode < 0 || mode > 4 {
|
|
|
+ return s.runtimeErr(ctx, errors.New("invalid pd mode specified"))
|
|
|
+ }
|
|
|
+
|
|
|
+ snac := wire.SNAC_0x13_0x09_FeedbagUpdateItem{
|
|
|
+ Items: []wire.FeedbagItem{
|
|
|
+ {
|
|
|
+ ClassID: wire.FeedbagClassIdPdinfo,
|
|
|
+ TLVLBlock: wire.TLVLBlock{
|
|
|
+ TLVList: wire.TLVList{
|
|
|
+ wire.NewTLVBE(wire.FeedbagAttributesPdMode, uint8(mode)),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+ if _, err := s.FeedbagService.UpsertItem(ctx, me, wire.SNACFrame{}, snac.Items); err != nil {
|
|
|
+ return s.runtimeErr(ctx, fmt.Errorf("FeedbagManager.UpsertItem: %w", err))
|
|
|
+ }
|
|
|
+
|
|
|
+ return []string{}
|
|
|
+}
|
|
|
+
|
|
|
+// RemoveBuddy2 handles the toc2_remove_buddy command.
|
|
|
+//
|
|
|
+// From the TOC2 docs by Jeffrey Rosen
|
|
|
+//
|
|
|
+// You can remove multiple names in the same group using the syntax <screenname> <screenname> <group>.
|
|
|
+//
|
|
|
+// toc2_remove_buddy <screenname> [screenname] ... [screenname] <group>
|
|
|
+
|
|
|
+func (s OSCARProxy) RemoveBuddy2(ctx context.Context, me *state.SessionInstance, args []byte) []string {
|
|
|
+ if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.Locate, wire.LocateSetInfo); isLimited {
|
|
|
+ return errMsg
|
|
|
+ }
|
|
|
+
|
|
|
+ params, err := parseArgs(args)
|
|
|
+ if err != nil {
|
|
|
+ return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
|
|
|
+ }
|
|
|
+ if len(params) < 2 {
|
|
|
+ return s.runtimeErr(ctx, fmt.Errorf("misssing params"))
|
|
|
+ }
|
|
|
+ return []string{}
|
|
|
+}
|
|
|
+
|
|
|
+// SendIMEnc handles the toc2_send_im_enc TOC2 command. The Destination user, Message, and Auto are extracted and
|
|
|
+// the regular SendIM handler is called
|
|
|
+//
|
|
|
+// From the BlueTOC source code:
|
|
|
+//
|
|
|
+// Sends an encoded instant message to a user
|
|
|
+// Internally, this uses the "TOC3" encoded message format
|
|
|
+// This encoded message version supports a few more variables as well as encoding
|
|
|
+//
|
|
|
+// Command syntax: toc2_send_im_enc <Destination user> "F" <Encoding> <Language> <Message> [auto]
|
|
|
+func (s OSCARProxy) SendIMEnc(ctx context.Context, sender *state.SessionInstance, args []byte) []string {
|
|
|
+ var recip, msg string
|
|
|
+ autoReply := []string{}
|
|
|
+
|
|
|
+ parsedArgs, err := parseArgs(args)
|
|
|
+ if err != nil || len(parsedArgs) < 5 {
|
|
|
+ return s.runtimeErr(ctx, fmt.Errorf("SendIMEnc: invalid args: %w", err))
|
|
|
+ }
|
|
|
+ recip = parsedArgs[0]
|
|
|
+ msg = parsedArgs[4]
|
|
|
+ if len(parsedArgs) > 5 {
|
|
|
+ autoReply = append(autoReply, parsedArgs[5])
|
|
|
+ }
|
|
|
+
|
|
|
+ // Manually build args byte slice for SendIM: <Destination User> <Message> [auto]
|
|
|
+ sendIMArgs := []string{recip, msg}
|
|
|
+ if len(autoReply) > 0 && autoReply[0] == "auto" {
|
|
|
+ sendIMArgs = append(sendIMArgs, "auto")
|
|
|
+ }
|
|
|
+ // Join arguments with spaces and convert to []byte
|
|
|
+ sendIMArgsStr := ""
|
|
|
+ for i, v := range sendIMArgs {
|
|
|
+ if i > 0 {
|
|
|
+ sendIMArgsStr += " "
|
|
|
+ }
|
|
|
+ sendIMArgsStr += v
|
|
|
+ }
|
|
|
+ sendIMArgsBytes := []byte(sendIMArgsStr)
|
|
|
+ return s.SendIM(ctx, sender, sendIMArgsBytes)
|
|
|
}
|
|
|
|
|
|
// SendIM handles the toc_send_im TOC command.
|
|
|
@@ -1330,7 +1460,7 @@ func (s OSCARProxy) RvousCancel(ctx context.Context, me *state.SessionInstance,
|
|
|
// flag will be turned on for the IM.
|
|
|
//
|
|
|
// Command syntax: toc_send_im <Destination User> <Message> [auto]
|
|
|
-func (s OSCARProxy) SendIM(ctx context.Context, sender *state.SessionInstance, args []byte) string {
|
|
|
+func (s OSCARProxy) SendIM(ctx context.Context, sender *state.SessionInstance, args []byte) []string {
|
|
|
if msg, isLimited := s.checkRateLimit(ctx, sender, wire.ICBM, wire.ICBMChannelMsgToHost); isLimited {
|
|
|
return msg
|
|
|
}
|
|
|
@@ -1369,10 +1499,10 @@ func (s OSCARProxy) SendIM(ctx context.Context, sender *state.SessionInstance, a
|
|
|
return s.runtimeErr(ctx, fmt.Errorf("ICBMService.ChannelMsgToHost: %w", err))
|
|
|
}
|
|
|
|
|
|
- return ""
|
|
|
+ return []string{}
|
|
|
}
|
|
|
|
|
|
-// SetAway handles the toc_chat_join TOC command.
|
|
|
+// SetAway handles the toc_set_away TOC command.
|
|
|
//
|
|
|
// From the TiK documentation:
|
|
|
//
|
|
|
@@ -1382,7 +1512,7 @@ func (s OSCARProxy) SendIM(ctx context.Context, sender *state.SessionInstance, a
|
|
|
// information.
|
|
|
//
|
|
|
// Command syntax: toc_set_away [<away message>]
|
|
|
-func (s OSCARProxy) SetAway(ctx context.Context, me *state.SessionInstance, args []byte) string {
|
|
|
+func (s OSCARProxy) SetAway(ctx context.Context, me *state.SessionInstance, args []byte) []string {
|
|
|
if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.Locate, wire.LocateSetInfo); isLimited {
|
|
|
return errMsg
|
|
|
}
|
|
|
@@ -1409,7 +1539,7 @@ func (s OSCARProxy) SetAway(ctx context.Context, me *state.SessionInstance, args
|
|
|
return s.runtimeErr(ctx, fmt.Errorf("LocateService.SetInfo: %w", err))
|
|
|
}
|
|
|
|
|
|
- return ""
|
|
|
+ return []string{}
|
|
|
}
|
|
|
|
|
|
// SetCaps handles the toc_set_caps TOC command.
|
|
|
@@ -1424,7 +1554,7 @@ func (s OSCARProxy) SetAway(ctx context.Context, me *state.SessionInstance, args
|
|
|
// chat.
|
|
|
//
|
|
|
// Command syntax: toc_set_caps [ <Capability 1> [<Capability 2> [...]]]
|
|
|
-func (s OSCARProxy) SetCaps(ctx context.Context, me *state.SessionInstance, args []byte) string {
|
|
|
+func (s OSCARProxy) SetCaps(ctx context.Context, me *state.SessionInstance, args []byte) []string {
|
|
|
if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.Locate, wire.LocateSetInfo); isLimited {
|
|
|
return errMsg
|
|
|
}
|
|
|
@@ -1458,7 +1588,7 @@ func (s OSCARProxy) SetCaps(ctx context.Context, me *state.SessionInstance, args
|
|
|
return s.runtimeErr(ctx, fmt.Errorf("LocateService.SetInfo: %w", err))
|
|
|
}
|
|
|
|
|
|
- return ""
|
|
|
+ return []string{}
|
|
|
}
|
|
|
|
|
|
// SetConfig handles the toc_set_config TOC command.
|
|
|
@@ -1486,9 +1616,9 @@ func (s OSCARProxy) SetCaps(ctx context.Context, me *state.SessionInstance, args
|
|
|
// the config as received from the client.
|
|
|
//
|
|
|
// Command syntax: toc_set_config <Config Info>
|
|
|
-func (s OSCARProxy) SetConfig(ctx context.Context, me *state.SessionInstance, args []byte) string {
|
|
|
+func (s OSCARProxy) SetConfig(ctx context.Context, me *state.SessionInstance, args []byte) []string {
|
|
|
if status := me.Session().EvaluateRateLimit(time.Now(), 1); status == wire.RateLimitStatusLimited {
|
|
|
- return rateLimitExceededErr
|
|
|
+ return []string{rateLimitExceededErr}
|
|
|
}
|
|
|
|
|
|
// most TOC clients don't quote the config info argument, despite what the
|
|
|
@@ -1507,7 +1637,7 @@ func (s OSCARProxy) SetConfig(ctx context.Context, me *state.SessionInstance, ar
|
|
|
return s.runtimeErr(ctx, fmt.Errorf("TOCConfigStore.SaveTOCConfig: %w", err))
|
|
|
}
|
|
|
|
|
|
- return ""
|
|
|
+ return []string{}
|
|
|
}
|
|
|
|
|
|
// SetDir handles the toc_set_dir TOC command.
|
|
|
@@ -1525,7 +1655,7 @@ func (s OSCARProxy) SetConfig(ctx context.Context, me *state.SessionInstance, ar
|
|
|
// The fields "email" and "allow web searches" are ignored by this method.
|
|
|
//
|
|
|
// Command syntax: toc_set_dir <info information>
|
|
|
-func (s OSCARProxy) SetDir(ctx context.Context, me *state.SessionInstance, args []byte) string {
|
|
|
+func (s OSCARProxy) SetDir(ctx context.Context, me *state.SessionInstance, args []byte) []string {
|
|
|
if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.Locate, wire.LocateSetDirInfo); isLimited {
|
|
|
return errMsg
|
|
|
}
|
|
|
@@ -1565,7 +1695,7 @@ func (s OSCARProxy) SetDir(ctx context.Context, me *state.SessionInstance, args
|
|
|
return s.runtimeErr(ctx, fmt.Errorf("LocateService.SetDirInfo: %w", err))
|
|
|
}
|
|
|
|
|
|
- return ""
|
|
|
+ return []string{}
|
|
|
}
|
|
|
|
|
|
// SetIdle handles the toc_set_idle TOC command.
|
|
|
@@ -1578,7 +1708,7 @@ func (s OSCARProxy) SetDir(ctx context.Context, me *state.SessionInstance, args
|
|
|
// incrementing this number, so do not repeatedly call with new idle times.
|
|
|
//
|
|
|
// Command syntax: toc_set_idle <idle secs>
|
|
|
-func (s OSCARProxy) SetIdle(ctx context.Context, me *state.SessionInstance, args []byte) string {
|
|
|
+func (s OSCARProxy) SetIdle(ctx context.Context, me *state.SessionInstance, args []byte) []string {
|
|
|
if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.OService, wire.OServiceIdleNotification); isLimited {
|
|
|
return errMsg
|
|
|
}
|
|
|
@@ -1601,7 +1731,7 @@ func (s OSCARProxy) SetIdle(ctx context.Context, me *state.SessionInstance, args
|
|
|
return s.runtimeErr(ctx, fmt.Errorf("OServiceServiceBOS.IdleNotification: %w", err))
|
|
|
}
|
|
|
|
|
|
- return ""
|
|
|
+ return []string{}
|
|
|
}
|
|
|
|
|
|
// SetInfo handles the toc_set_info TOC command.
|
|
|
@@ -1611,7 +1741,7 @@ func (s OSCARProxy) SetIdle(ctx context.Context, me *state.SessionInstance, args
|
|
|
// Set the LOCATE user information. This is basic HTML. Remember to encode the info.
|
|
|
//
|
|
|
// Command syntax: toc_set_info <info information>
|
|
|
-func (s OSCARProxy) SetInfo(ctx context.Context, me *state.SessionInstance, args []byte) string {
|
|
|
+func (s OSCARProxy) SetInfo(ctx context.Context, me *state.SessionInstance, args []byte) []string {
|
|
|
if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.Locate, wire.LocateSetInfo); isLimited {
|
|
|
return errMsg
|
|
|
}
|
|
|
@@ -1634,10 +1764,10 @@ func (s OSCARProxy) SetInfo(ctx context.Context, me *state.SessionInstance, args
|
|
|
return s.runtimeErr(ctx, fmt.Errorf("LocateService.SetInfo: %w", err))
|
|
|
}
|
|
|
|
|
|
- return ""
|
|
|
+ return []string{}
|
|
|
}
|
|
|
|
|
|
-// Signon handles the toc_signon TOC command.
|
|
|
+// Signon handles the toc_signon and toc2_login TOC commands.
|
|
|
//
|
|
|
// From the TiK documentation:
|
|
|
//
|
|
|
@@ -1659,16 +1789,18 @@ func (s OSCARProxy) SetInfo(ctx context.Context, me *state.SessionInstance, args
|
|
|
// The Roasting String is Tic/Toc.
|
|
|
//
|
|
|
// Command syntax: toc_signon <authorizer host> <authorizer port> <User Name> <Password> <language> <version>
|
|
|
-func (s OSCARProxy) Signon(ctx context.Context, args []byte) (*state.SessionInstance, []string) {
|
|
|
+//
|
|
|
+// Command syntax: toc2_login <authorizer host> <authorizer port> <User Name> <Password> <language> <version> <unknown> <code>
|
|
|
+func (s OSCARProxy) Signon(ctx context.Context, args []byte, tocVersion state.TOCVersion) (*state.SessionInstance, []string) {
|
|
|
var userName, password string
|
|
|
|
|
|
if _, err := parseArgs(args, nil, nil, &userName, &password); err != nil {
|
|
|
- return nil, []string{s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))}
|
|
|
+ return nil, s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
|
|
|
}
|
|
|
|
|
|
passwordHash, err := hex.DecodeString(password[2:])
|
|
|
if err != nil {
|
|
|
- return nil, []string{s.runtimeErr(ctx, fmt.Errorf("hex.DecodeString: %w", err))}
|
|
|
+ return nil, s.runtimeErr(ctx, fmt.Errorf("hex.DecodeString: %w", err))
|
|
|
}
|
|
|
|
|
|
signonFrame := wire.FLAPSignonFrame{}
|
|
|
@@ -1677,17 +1809,17 @@ func (s OSCARProxy) Signon(ctx context.Context, args []byte) (*state.SessionInst
|
|
|
|
|
|
block, err := s.AuthService.FLAPLogin(ctx, signonFrame, "")
|
|
|
if err != nil {
|
|
|
- return nil, []string{s.runtimeErr(ctx, fmt.Errorf("AuthService.FLAPLogin: %w", err))}
|
|
|
+ return nil, s.runtimeErr(ctx, fmt.Errorf("AuthService.FLAPLogin: %w", err))
|
|
|
}
|
|
|
|
|
|
if block.HasTag(wire.LoginTLVTagsErrorSubcode) {
|
|
|
s.Logger.DebugContext(ctx, "login failed")
|
|
|
- return nil, []string{"ERROR:980"} // bad username/password
|
|
|
+ return nil, []string{"ERROR:" + wire.TOCErrorAuthIncorrectNickOrPassword}
|
|
|
}
|
|
|
|
|
|
authCookie, ok := block.Bytes(wire.OServiceTLVTagsLoginCookie)
|
|
|
if !ok {
|
|
|
- return nil, []string{s.runtimeErr(ctx, fmt.Errorf("unable to get session id from payload"))}
|
|
|
+ return nil, s.runtimeErr(ctx, fmt.Errorf("unable to get session id from payload"))
|
|
|
}
|
|
|
|
|
|
// todo: naming for cookie: login cookie, server cookie, or auth cookie?
|
|
|
@@ -1695,25 +1827,165 @@ func (s OSCARProxy) Signon(ctx context.Context, args []byte) (*state.SessionInst
|
|
|
|
|
|
sess, err := s.AuthService.RegisterBOSSession(ctx, serverCookie)
|
|
|
if err != nil {
|
|
|
- return nil, []string{s.runtimeErr(ctx, fmt.Errorf("AuthService.RegisterBOSSession: %w", err))}
|
|
|
+ return nil, s.runtimeErr(ctx, fmt.Errorf("AuthService.RegisterBOSSession: %w", err))
|
|
|
}
|
|
|
|
|
|
// set chat capability so that... tk
|
|
|
sess.SetCaps([][16]byte{wire.CapChat})
|
|
|
|
|
|
if err := s.BuddyListRegistry.RegisterBuddyList(ctx, sess.IdentScreenName()); err != nil {
|
|
|
- return nil, []string{s.runtimeErr(ctx, fmt.Errorf("BuddyListRegistry.RegisterBuddyList: %w", err))}
|
|
|
+ return nil, s.runtimeErr(ctx, fmt.Errorf("BuddyListRegistry.RegisterBuddyList: %w", err))
|
|
|
}
|
|
|
|
|
|
u, err := s.TOCConfigStore.User(ctx, sess.IdentScreenName())
|
|
|
if err != nil {
|
|
|
- return nil, []string{s.runtimeErr(ctx, fmt.Errorf("TOCConfigStore.User: %w", err))}
|
|
|
+ return nil, s.runtimeErr(ctx, fmt.Errorf("TOCConfigStore.User: %w", err))
|
|
|
}
|
|
|
if u == nil {
|
|
|
- return nil, []string{s.runtimeErr(ctx, fmt.Errorf("TOCConfigStore.User: user not found"))}
|
|
|
+ return nil, s.runtimeErr(ctx, fmt.Errorf("TOCConfigStore.User: user not found"))
|
|
|
+ }
|
|
|
+
|
|
|
+ if (tocVersion & state.SupportsTOC) != 0 {
|
|
|
+ return sess, []string{"SIGN_ON:TOC1.0", fmt.Sprintf("CONFIG:%s", u.TOCConfig), fmt.Sprintf("NICK:%s", sess.DisplayScreenName().String())}
|
|
|
+ }
|
|
|
+
|
|
|
+ fb, err := s.FeedbagManager.Feedbag(ctx, sess.IdentScreenName())
|
|
|
+ if err != nil {
|
|
|
+ return sess, s.runtimeErr(ctx, fmt.Errorf("FeedbagManager.Feedbag: %w", err))
|
|
|
+ }
|
|
|
+
|
|
|
+ signon := []string{"SIGN_ON:TOC2.0", fmt.Sprintf("NICK:%s", sess.DisplayScreenName().String())}
|
|
|
+ config, err := buildToc2Config(fb)
|
|
|
+ if err != nil {
|
|
|
+ return sess, s.runtimeErr(ctx, fmt.Errorf("buildToc2Config: %w", err))
|
|
|
+ }
|
|
|
+ return sess, append(signon, config...)
|
|
|
+}
|
|
|
+
|
|
|
+// buildToc2Config constructs configuration for CONFIG2
|
|
|
+func buildToc2Config(fb []wire.FeedbagItem) ([]string, error) {
|
|
|
+ config := []string{}
|
|
|
+
|
|
|
+ type buddy struct {
|
|
|
+ name string
|
|
|
+ alias string
|
|
|
+ note string
|
|
|
+ }
|
|
|
+ type group struct {
|
|
|
+ name string
|
|
|
+ buddies map[uint16]buddy
|
|
|
+ order []uint16
|
|
|
+ }
|
|
|
+ type feedbag struct {
|
|
|
+ order []uint16
|
|
|
+ groups map[uint16]group
|
|
|
+ }
|
|
|
+ buddylist := feedbag{
|
|
|
+ groups: make(map[uint16]group),
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, item := range fb {
|
|
|
+ if item.ClassID == wire.FeedbagClassIdGroup {
|
|
|
+ // root group contains the order of all other groups
|
|
|
+ if item.GroupID == 0 {
|
|
|
+ val, hasVal := item.Uint16SliceBE(wire.FeedbagAttributesOrder)
|
|
|
+ if !hasVal {
|
|
|
+ return []string{}, fmt.Errorf("root group missing order attribute")
|
|
|
+ }
|
|
|
+ buddylist.order = val
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ group := group{
|
|
|
+ name: item.Name,
|
|
|
+ buddies: make(map[uint16]buddy),
|
|
|
+ }
|
|
|
+ // order will be empty if the group is empty
|
|
|
+ val, _ := item.Uint16SliceBE(wire.FeedbagAttributesOrder)
|
|
|
+ group.order = val
|
|
|
+ buddylist.groups[item.GroupID] = group
|
|
|
+ }
|
|
|
+ if item.ClassID == wire.FeedbagClassIdBuddy {
|
|
|
+ buddy := buddy{
|
|
|
+ name: item.Name,
|
|
|
+ }
|
|
|
+ if val, hasVal := item.String(wire.FeedbagAttributesNote); hasVal {
|
|
|
+ buddy.note = val
|
|
|
+ }
|
|
|
+ if val, hasVal := item.String(wire.FeedbagAttributesAlias); hasVal {
|
|
|
+ buddy.alias = val
|
|
|
+ }
|
|
|
+ buddylist.groups[item.GroupID].buddies[item.ItemID] = buddy
|
|
|
+ }
|
|
|
+ if item.ClassID == wire.FeedbagClassIDDeny {
|
|
|
+ config = append(config, "d:"+item.Name+"\n")
|
|
|
+ }
|
|
|
+ if item.ClassID == wire.FeedbagClassIDPermit {
|
|
|
+ config = append(config, "p:"+item.Name+"\n")
|
|
|
+ }
|
|
|
+ if item.ClassID == wire.FeedbagClassIdPdinfo {
|
|
|
+ val, hasVal := item.Uint8(wire.FeedbagAttributesPdMode)
|
|
|
+ if hasVal {
|
|
|
+ config = append(config, fmt.Sprintf("m:%d\n", val))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, gid := range buddylist.order {
|
|
|
+ group := buddylist.groups[gid]
|
|
|
+ config = append(config, "g:"+group.name+"\n")
|
|
|
+
|
|
|
+ for _, bid := range group.order {
|
|
|
+ buddy := group.buddies[bid]
|
|
|
+ tmpLine := "b:" + buddy.name
|
|
|
+ if buddy.alias != "" {
|
|
|
+ tmpLine += ":" + buddy.alias
|
|
|
+ }
|
|
|
+ if buddy.note != "" {
|
|
|
+ tmpLine += ":::::" + buddy.note
|
|
|
+ }
|
|
|
+ config = append(config, tmpLine+"\n")
|
|
|
+ }
|
|
|
+ }
|
|
|
+ config = append(config, "done:\n")
|
|
|
+
|
|
|
+ // todo: these are documented in BizTOCSock as being sent in the CONFIG2 command,
|
|
|
+ // but are not currently implemented
|
|
|
+ // 25: - Recent user, followed by an unknown random or incrementing number
|
|
|
+ // M: - Unknown
|
|
|
+ // 20:, 26:, 29: - Unknown
|
|
|
+
|
|
|
+ return buildConfigCommands(config)
|
|
|
+}
|
|
|
+
|
|
|
+// buildConfigCommands takes the given input config and splits it to meet the TOC maximumm allowed
|
|
|
+// size of 8000 bytes. The 'CONFIG2:' command is prepended to each chunk.
|
|
|
+func buildConfigCommands(config []string) ([]string, error) {
|
|
|
+ const maxSize = 8000
|
|
|
+
|
|
|
+ var configChunks []string
|
|
|
+ var chunk strings.Builder
|
|
|
+
|
|
|
+ for _, line := range config {
|
|
|
+ if len(line) > maxSize {
|
|
|
+ // todo: for now just skip entries that are too large to fit into a single CONFIG2 command
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ if len(line)+chunk.Len() > maxSize {
|
|
|
+ configChunks = append(configChunks, "CONFIG2:"+chunk.String())
|
|
|
+ chunk.Reset()
|
|
|
+ }
|
|
|
+
|
|
|
+ _, err := chunk.WriteString(line)
|
|
|
+ if err != nil {
|
|
|
+ return []string{}, fmt.Errorf("buildConfigCommands chunk.WriteString: %w", err)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- return sess, []string{"SIGN_ON:TOC1.0", fmt.Sprintf("CONFIG:%s", u.TOCConfig)}
|
|
|
+ configChunks = append(configChunks, "CONFIG2:"+chunk.String())
|
|
|
+
|
|
|
+ return configChunks, nil
|
|
|
}
|
|
|
|
|
|
// Signout terminates a TOC session. It sends departure notifications to
|
|
|
@@ -1778,26 +2050,26 @@ func parseArgs(payload []byte, args ...*string) (varArgs []string, err error) {
|
|
|
|
|
|
// runtimeErr is a convenience function that logs an error and returns a TOC
|
|
|
// internal server error.
|
|
|
-func (s OSCARProxy) runtimeErr(ctx context.Context, err error) string {
|
|
|
+func (s OSCARProxy) runtimeErr(ctx context.Context, err error) []string {
|
|
|
s.Logger.ErrorContext(ctx, "internal service error", "err", err.Error())
|
|
|
- return cmdInternalSvcErr
|
|
|
+ return []string{cmdInternalSvcErr}
|
|
|
}
|
|
|
|
|
|
-func (s OSCARProxy) checkRateLimit(ctx context.Context, sender *state.SessionInstance, foodGroup uint16, subGroup uint16) (string, bool) {
|
|
|
+func (s OSCARProxy) checkRateLimit(ctx context.Context, sender *state.SessionInstance, foodGroup uint16, subGroup uint16) ([]string, bool) {
|
|
|
rateClassID, ok := s.SNACRateLimits.RateClassLookup(foodGroup, subGroup)
|
|
|
if !ok {
|
|
|
s.Logger.ErrorContext(ctx, "rate limit not found, allowing request through")
|
|
|
- return "", false
|
|
|
+ return []string{}, false
|
|
|
}
|
|
|
|
|
|
if status := sender.Session().EvaluateRateLimit(time.Now(), rateClassID); status == wire.RateLimitStatusLimited {
|
|
|
s.Logger.DebugContext(ctx, "(toc) rate limit exceeded, dropping SNAC",
|
|
|
"foodgroup", wire.FoodGroupName(foodGroup),
|
|
|
"subgroup", wire.SubGroupName(foodGroup, subGroup))
|
|
|
- return rateLimitExceededErr, true
|
|
|
+ return []string{rateLimitExceededErr}, true
|
|
|
}
|
|
|
|
|
|
- return "", false
|
|
|
+ return []string{}, false
|
|
|
}
|
|
|
|
|
|
// unescape removes escaping from the following TOC characters: \ { } ( ) [ ] $ "
|