瀏覽代碼

wire: make BuddyPref own buddy-pref defaults

Mike 2 周之前
父節點
當前提交
a3ae470d6d

+ 1 - 8
foodgroup/feedbag.go

@@ -1011,20 +1011,13 @@ func (s *FeedbagService) rejectContact(ctx context.Context, rejecter state.Ident
 func setSessionBuddyPrefs(items []wire.FeedbagItem, instance *state.SessionInstance) {
 	for _, item := range items {
 		if item.ClassID == wire.FeedbagClassIdBuddyPrefs {
-			_, wantsTyping := wire.BuddyPref(item.TLVList, wire.FeedbagBuddyPrefsDiscloseTyping)
+			wantsTyping := wire.BuddyPref(item.TLVList, wire.FeedbagBuddyPrefsDiscloseTyping)
 			instance.Session().SetTypingEventsEnabled(wantsTyping)
 			break
 		}
 	}
 }
 
-// feedbagBuddyPref returns whether preference prefNum is present in the user's
-// feedbag buddy-prefs bitmask (valid) and its boolean value. See wire.BuddyPref
-// for the bitmask layout.
-func feedbagBuddyPref(prefNum uint16, list wire.TLVList) (valid bool, value bool) {
-	return wire.BuddyPref(list, prefNum)
-}
-
 // ForwardICQAuthEvents converts ICQ channel-4 payloads to feedbag SNACs and
 // sends them to feedbag-enabled recipient.
 func (s *FeedbagService) ForwardICQAuthEvents(ctx context.Context, sender state.IdentScreenName, recipient state.IdentScreenName, authMsg wire.ICBMCh4Message) error {

+ 3 - 31
foodgroup/feedbag_test.go

@@ -3008,9 +3008,7 @@ func TestFeedbagService_Use(t *testing.T) {
 								{
 									ClassID: wire.FeedbagClassIdBuddyPrefs,
 									TLVLBlock: wire.TLVLBlock{
-										TLVList: wire.TLVList{
-											wire.NewTLVBE(wire.FeedbagAttributesBuddyPrefs, uint32(wire.SNACFlagsExtendedInfo)),
-										},
+										TLVList: wire.SetBuddyPref(nil, wire.FeedbagBuddyPrefsDiscloseTyping, false),
 									},
 								},
 							},
@@ -4429,7 +4427,6 @@ func TestFeedbagBuddyPref(t *testing.T) {
 		name      string
 		itemType  uint16
 		list      wire.TLVList
-		wantValid bool
 		wantValue bool
 	}{
 		{
@@ -4441,19 +4438,6 @@ func TestFeedbagBuddyPref(t *testing.T) {
 				{Tag: wire.FeedbagAttributesBuddyPrefs2Valid, Value: []byte{0, 0, 17}},
 				{Tag: wire.FeedbagAttributesBuddyPrefs2, Value: []byte{0, 0, 1}},
 			},
-			wantValid: true,
-			wantValue: false,
-		},
-		{
-			name:     "offline messages disabled",
-			itemType: wire.FeedbagBuddyPrefsAcceptOfflineIM,
-			list: wire.TLVList{
-				{Tag: wire.FeedbagAttributesBuddyPrefsValid, Value: []byte{0, 0, 24, 64}},
-				{Tag: wire.FeedbagAttributesBuddyPrefs, Value: []byte{0, 0, 24, 64}},
-				{Tag: wire.FeedbagAttributesBuddyPrefs2Valid, Value: []byte{0, 0, 17}},
-				{Tag: wire.FeedbagAttributesBuddyPrefs2, Value: []byte{0, 0, 1}},
-			},
-			wantValid: true,
 			wantValue: false,
 		},
 		{
@@ -4465,7 +4449,6 @@ func TestFeedbagBuddyPref(t *testing.T) {
 				{Tag: wire.FeedbagAttributesBuddyPrefs2Valid, Value: []byte{0, 0, 17, 0, 0}},
 				{Tag: wire.FeedbagAttributesBuddyPrefs2, Value: []byte{0, 0, 1, 0, 0}},
 			},
-			wantValid: true,
 			wantValue: false,
 		},
 		{
@@ -4477,11 +4460,10 @@ func TestFeedbagBuddyPref(t *testing.T) {
 				{Tag: wire.FeedbagAttributesBuddyPrefs2Valid, Value: []byte{0, 0, 17}},
 				{Tag: wire.FeedbagAttributesBuddyPrefs2, Value: []byte{0, 0, 17}},
 			},
-			wantValid: true,
 			wantValue: true,
 		},
 		{
-			name:     "offline messages enabled",
+			name:     "offline messages enabled, extra padding",
 			itemType: wire.FeedbagBuddyPrefsAcceptOfflineIM,
 			list: wire.TLVList{
 				{Tag: wire.FeedbagAttributesBuddyPrefsValid, Value: []byte{0, 0, 24, 64}},
@@ -4489,7 +4471,6 @@ func TestFeedbagBuddyPref(t *testing.T) {
 				{Tag: wire.FeedbagAttributesBuddyPrefs2Valid, Value: []byte{0, 0, 17, 0, 0}},
 				{Tag: wire.FeedbagAttributesBuddyPrefs2, Value: []byte{0, 0, 17, 0, 0}},
 			},
-			wantValid: true,
 			wantValue: true,
 		},
 		{
@@ -4501,7 +4482,6 @@ func TestFeedbagBuddyPref(t *testing.T) {
 				{Tag: wire.FeedbagAttributesBuddyPrefs2Valid, Value: []byte{0, 0, 17}},
 				{Tag: wire.FeedbagAttributesBuddyPrefs2, Value: []byte{0, 0, 17}},
 			},
-			wantValid: true,
 			wantValue: true,
 		},
 		{
@@ -4513,7 +4493,6 @@ func TestFeedbagBuddyPref(t *testing.T) {
 				{Tag: wire.FeedbagAttributesBuddyPrefs2Valid, Value: []byte{17}},
 				{Tag: wire.FeedbagAttributesBuddyPrefs2, Value: []byte{17}},
 			},
-			wantValid: true,
 			wantValue: true,
 		},
 		{
@@ -4525,7 +4504,6 @@ func TestFeedbagBuddyPref(t *testing.T) {
 				{Tag: wire.FeedbagAttributesBuddyPrefs2Valid, Value: []byte{0, 0, 17}},
 				{Tag: wire.FeedbagAttributesBuddyPrefs2, Value: []byte{0, 0, 17}},
 			},
-			wantValid: true,
 			wantValue: false,
 		},
 		{
@@ -4537,7 +4515,6 @@ func TestFeedbagBuddyPref(t *testing.T) {
 				{Tag: wire.FeedbagAttributesBuddyPrefs2Valid, Value: []byte{17}},
 				{Tag: wire.FeedbagAttributesBuddyPrefs2, Value: []byte{17}},
 			},
-			wantValid: true,
 			wantValue: false,
 		},
 		{
@@ -4549,7 +4526,6 @@ func TestFeedbagBuddyPref(t *testing.T) {
 				{Tag: wire.FeedbagAttributesBuddyPrefs2Valid, Value: []byte{0x80, 0x00, 0x00, 0x00}},
 				{Tag: wire.FeedbagAttributesBuddyPrefs2, Value: []byte{0x80, 0x00, 0x00, 0x00}},
 			},
-			wantValid: true,
 			wantValue: true,
 		},
 		{
@@ -4561,7 +4537,6 @@ func TestFeedbagBuddyPref(t *testing.T) {
 				{Tag: wire.FeedbagAttributesBuddyPrefs2Valid, Value: []byte{0x80, 0x00, 0x00, 0x00}},
 				{Tag: wire.FeedbagAttributesBuddyPrefs2, Value: []byte{0x80, 0x00, 0x00, 0x00}},
 			},
-			wantValid: true,
 			wantValue: true,
 		},
 		{
@@ -4573,16 +4548,13 @@ func TestFeedbagBuddyPref(t *testing.T) {
 				{Tag: wire.FeedbagAttributesBuddyPrefs2Valid, Value: []byte{0x40, 0x00, 0x00, 0x00}},
 				{Tag: wire.FeedbagAttributesBuddyPrefs2, Value: []byte{0x40, 0x00, 0x00, 0x00}},
 			},
-			wantValid: true,
 			wantValue: true,
 		},
 	}
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			valid, value := feedbagBuddyPref(tt.itemType, tt.list)
-			assert.Equal(t, tt.wantValid, valid)
-			assert.Equal(t, tt.wantValue, value)
+			assert.Equal(t, tt.wantValue, wire.BuddyPref(tt.list, tt.itemType))
 		})
 	}
 }

+ 5 - 10
foodgroup/icbm.go

@@ -253,16 +253,11 @@ func (s *ICBMService) canSendOfflineMessage(ctx context.Context, inBody wire.SNA
 
 	for _, item := range bag {
 		if item.ClassID == wire.FeedbagClassIdBuddyPrefs {
-			valid, ok := feedbagBuddyPref(wire.FeedbagBuddyPrefsAcceptOfflineIM, item.TLVList)
-			if !valid {
-				// user doesn't have an opt-out, so assume they can accept offline
-				// messages, because AIM 6.0+ clients accept offline messages
-				// by default. this preference did not exist prior to AIM 6, so
-				// retroactively assume it's OK for users who have never used
-				// capable clients to have offline messages stored for them.
-				return true, nil
-			}
-			return ok, nil // return the explicit preference
+			// wire.BuddyPref defaults AcceptOfflineIM to true when the bit is
+			// absent, matching AIM 6.0+ behavior: the preference did not exist
+			// prior to AIM 6, so users who never ran a capable client are
+			// assumed to accept stored offline messages.
+			return wire.BuddyPref(item.TLVList, wire.FeedbagBuddyPrefsAcceptOfflineIM), nil
 		}
 	}
 

+ 89 - 99
server/webapi/handlers/preference.go

@@ -20,100 +20,94 @@ type PreferenceHandler struct {
 	Logger         *slog.Logger
 }
 
-// buddyPref maps a Web AIM API preference to its OSCAR buddy-pref bit number
-// and the default value defined by the Web API spec.
-type buddyPref struct {
-	num uint16
-	def bool
-}
-
-// webBuddyPrefs maps Web AIM API preference names to OSCAR buddy prefs, which
-// are stored as a bitmask in the user's feedbag (see wire.BuddyPref). Pref
-// numbers 0x07, 0x13, and 0x17 are reserved/unused and intentionally absent.
-var webBuddyPrefs = map[string]buddyPref{
-	"displayLogin":                {wire.FeedbagBuddyPrefsDisplayLogin, true},
-	"displayEBuddy":               {wire.FeedbagBuddyPrefsDisplayEBuddy, true},
-	"playEnter":                   {wire.FeedbagBuddyPrefsPlayEnter, true},
-	"playExit":                    {wire.FeedbagBuddyPrefsPlayExit, true},
-	"viewIMTimestamps":            {wire.FeedbagBuddyPrefsViewIMStamp, true},
-	"viewSmilies":                 {wire.FeedbagBuddyPrefsViewSmileys, true},
-	"acceptIcons":                 {wire.FeedbagBuddyPrefsAcceptIcons, true},
-	"knockNonAOLIMs":              {wire.FeedbagBuddyPrefsKnockNonAOLIMs, true},
-	"knockNonListIMs":             {wire.FeedbagBuddyPrefsKnockNonListIMs, true},
-	"discloseIdle":                {wire.FeedbagBuddyPrefsDiscloseIdle, true},
-	"acceptCustomBart":            {wire.FeedbagBuddyPrefsAcceptCustomBart, false},
-	"acceptNonListBart":           {wire.FeedbagBuddyPrefsAcceptNonListBart, false},
-	"acceptBgs":                   {wire.FeedbagBuddyPrefsAcceptBgs, true},
-	"acceptChromes":               {wire.FeedbagBuddyPrefsAcceptChromes, true},
-	"acceptBLSounds":              {wire.FeedbagBuddyPrefsAcceptBLSounds, true},
-	"acceptIMsounds":              {wire.FeedbagBuddyPrefsAcceptIMSounds, true},
-	"noSeeRecentBuddies":          {wire.FeedbagBuddyPrefsNoSeeRecentBuddies, false},
-	"acceptSMSLegal":              {wire.FeedbagBuddyPrefsAcceptSMSLegal, false},
-	"enterDoesCRLF":               {wire.FeedbagBuddyPrefsEnterDoesCRLF, false},
-	"playIMSound":                 {wire.FeedbagBuddyPrefsPlayIMSound, true},
-	"discloseTyping":              {wire.FeedbagBuddyPrefsDiscloseTyping, true},
-	"acceptSuperIcons":            {wire.FeedbagBuddyPrefsAcceptSuperIcons, true},
-	"acceptBLRichText":            {wire.FeedbagBuddyPrefsAcceptBLRichText, true},
-	"reduceIMSound":               {wire.FeedbagBuddyPrefsReduceIMSound, true},
-	"confirmDirectIM":             {wire.FeedbagBuddyPrefsConfirmDirectIM, true},
-	"oneTabbedIMWindow":           {wire.FeedbagBuddyPrefsOneTabbedIMWindow, true},
-	"buddyInfoOnMouseover":        {wire.FeedbagBuddyPrefsBuddyInfoOnMouseover, true},
-	"discloseBuddyMatches":        {wire.FeedbagBuddyPrefsDiscloseBuddyMatches, true},
-	"catchIMs":                    {wire.FeedbagBuddyPrefsCatchIMs, false},
-	"showFriendlyName":            {wire.FeedbagBuddyPrefsShowFriendlyName, true},
-	"discloseRadio":               {wire.FeedbagBuddyPrefsDiscloseRadio, true},
-	"showCapabilities":            {wire.FeedbagBuddyPrefsShowCapabilities, true},
-	"showBuddyListFilter":         {wire.FeedbagBuddyPrefsShowBuddyListFilter, true},
-	"showAwayIdle":                {wire.FeedbagBuddyPrefsShowAwayIdle, true},
-	"showMobile":                  {wire.FeedbagBuddyPrefsShowMobile, true},
-	"sortBuddyList":               {wire.FeedbagBuddyPrefsSortBuddyList, false},
-	"catchIMsForClient":           {wire.FeedbagBuddyPrefsCatchIMsForClient, false},
-	"newMessageSmallNotification": {wire.FeedbagBuddyPrefsNewMessageSmallNotify, true},
-	"noFrequentBuddies":           {wire.FeedbagBuddyPrefsNoFrequentBuddies, false},
-	"blogAwayMessages":            {wire.FeedbagBuddyPrefsBlogAwayMessages, false},
-	"blogAIMSigMessages":          {wire.FeedbagBuddyPrefsBlogAIMSigMessages, false},
-	"blogNoComments":              {wire.FeedbagBuddyPrefsBlogNoComments, false},
-	"friendOfFriend":              {wire.FeedbagBuddyPrefsFriendOfFriend, false},
-	"friendGetContactList":        {wire.FeedbagBuddyPrefsFriendGetContactList, false},
-	"compadInit":                  {wire.FeedbagBuddyPrefsCompadInit, false},
-	"sendBuddyFeed":               {wire.FeedbagBuddyPrefsSendBuddyFeed, true},
-	"blkSendIMWhileAway":          {wire.FeedbagBuddyPrefsBlkSendIMWhileAway, false},
-	"showBuddyFeed":               {wire.FeedbagBuddyPrefsShowBuddyFeed, true},
-	"noSaveVanityInfo":            {wire.FeedbagBuddyPrefsNoSaveVanityInfo, false},
-	"acceptOffLineIM":             {wire.FeedbagBuddyPrefsAcceptOfflineIM, true},
-	"showGroups":                  {wire.FeedbagBuddyPrefsShowGroups, true},
-	"sortGroup":                   {wire.FeedbagBuddyPrefsSortGroup, true},
-	"showOffLineBuddies":          {wire.FeedbagBuddyPrefsShowOfflineBuddies, true},
-	"expandBuddies":               {wire.FeedbagBuddyPrefsExpandBuddies, false},
-	"thirdPartyFeeds":             {wire.FeedbagBuddyPrefsThirdPartyFeeds, false},
-	"notifyReceivedInvite":        {wire.FeedbagBuddyPrefsNotifyReceivedInvite, true},
-	"apfAutoAccept":               {wire.FeedbagBuddyPrefsApfAutoAccept, false},
-	"apfAutoAcceptBuddy":          {wire.FeedbagBuddyPrefsApfAutoAcceptBuddy, false},
-	"blockAwayMsgFeed":            {wire.FeedbagBuddyPrefsBlockAwayMsgFeed, false},
-	"blockAIMProfileFeed":         {wire.FeedbagBuddyPrefsBlockAIMProfileFeed, false},
-	"blockAIMPagesFeed":           {wire.FeedbagBuddyPrefsBlockAIMPagesFeed, false},
-	"blockJournalsFeed":           {wire.FeedbagBuddyPrefsBlockJournalsFeed, false},
-	"blockLocationFeed":           {wire.FeedbagBuddyPrefsBlockLocationFeed, false},
-	"blockStickiesFeed":           {wire.FeedbagBuddyPrefsBlockStickiesFeed, false},
-	"blockUncutFeed":              {wire.FeedbagBuddyPrefsBlockUncutFeed, false},
-	"blockLinksFeed":              {wire.FeedbagBuddyPrefsBlockLinksFeed, false},
-	"blockAIMBulletinFeed":        {wire.FeedbagBuddyPrefsBlockAIMBulletinFeed, false},
-	"saveStatusMsg":               {wire.FeedbagBuddyPrefsSaveStatusMsg, true},
+// webBuddyPrefs maps Web AIM API preference names to OSCAR buddy-pref bit
+// numbers, which are stored as a bitmask in the user's feedbag (see
+// wire.BuddyPref). Default values for absent prefs are owned by wire.BuddyPref,
+// not here.
+var webBuddyPrefs = map[string]uint16{
+	"displayLogin":                wire.FeedbagBuddyPrefsDisplayLogin,
+	"displayEBuddy":               wire.FeedbagBuddyPrefsDisplayEBuddy,
+	"playEnter":                   wire.FeedbagBuddyPrefsPlayEnter,
+	"playExit":                    wire.FeedbagBuddyPrefsPlayExit,
+	"viewIMTimestamps":            wire.FeedbagBuddyPrefsViewIMStamp,
+	"viewSmilies":                 wire.FeedbagBuddyPrefsViewSmileys,
+	"acceptIcons":                 wire.FeedbagBuddyPrefsAcceptIcons,
+	"knockNonAOLIMs":              wire.FeedbagBuddyPrefsKnockNonAOLIMs,
+	"knockNonListIMs":             wire.FeedbagBuddyPrefsKnockNonListIMs,
+	"discloseIdle":                wire.FeedbagBuddyPrefsDiscloseIdle,
+	"acceptCustomBart":            wire.FeedbagBuddyPrefsAcceptCustomBart,
+	"acceptNonListBart":           wire.FeedbagBuddyPrefsAcceptNonListBart,
+	"acceptBgs":                   wire.FeedbagBuddyPrefsAcceptBgs,
+	"acceptChromes":               wire.FeedbagBuddyPrefsAcceptChromes,
+	"acceptBLSounds":              wire.FeedbagBuddyPrefsAcceptBLSounds,
+	"acceptIMsounds":              wire.FeedbagBuddyPrefsAcceptIMSounds,
+	"noSeeRecentBuddies":          wire.FeedbagBuddyPrefsNoSeeRecentBuddies,
+	"acceptSMSLegal":              wire.FeedbagBuddyPrefsAcceptSMSLegal,
+	"enterDoesCRLF":               wire.FeedbagBuddyPrefsEnterDoesCRLF,
+	"playIMSound":                 wire.FeedbagBuddyPrefsPlayIMSound,
+	"discloseTyping":              wire.FeedbagBuddyPrefsDiscloseTyping,
+	"acceptSuperIcons":            wire.FeedbagBuddyPrefsAcceptSuperIcons,
+	"acceptBLRichText":            wire.FeedbagBuddyPrefsAcceptBLRichText,
+	"reduceIMSound":               wire.FeedbagBuddyPrefsReduceIMSound,
+	"confirmDirectIM":             wire.FeedbagBuddyPrefsConfirmDirectIM,
+	"oneTabbedIMWindow":           wire.FeedbagBuddyPrefsOneTabbedIMWindow,
+	"buddyInfoOnMouseover":        wire.FeedbagBuddyPrefsBuddyInfoOnMouseover,
+	"discloseBuddyMatches":        wire.FeedbagBuddyPrefsDiscloseBuddyMatches,
+	"catchIMs":                    wire.FeedbagBuddyPrefsCatchIMs,
+	"showFriendlyName":            wire.FeedbagBuddyPrefsShowFriendlyName,
+	"discloseRadio":               wire.FeedbagBuddyPrefsDiscloseRadio,
+	"showCapabilities":            wire.FeedbagBuddyPrefsShowCapabilities,
+	"showBuddyListFilter":         wire.FeedbagBuddyPrefsShowBuddyListFilter,
+	"showAwayIdle":                wire.FeedbagBuddyPrefsShowAwayIdle,
+	"showMobile":                  wire.FeedbagBuddyPrefsShowMobile,
+	"sortBuddyList":               wire.FeedbagBuddyPrefsSortBuddyList,
+	"catchIMsForClient":           wire.FeedbagBuddyPrefsCatchIMsForClient,
+	"newMessageSmallNotification": wire.FeedbagBuddyPrefsNewMessageSmallNotify,
+	"noFrequentBuddies":           wire.FeedbagBuddyPrefsNoFrequentBuddies,
+	"blogAwayMessages":            wire.FeedbagBuddyPrefsBlogAwayMessages,
+	"blogAIMSigMessages":          wire.FeedbagBuddyPrefsBlogAIMSigMessages,
+	"blogNoComments":              wire.FeedbagBuddyPrefsBlogNoComments,
+	"friendOfFriend":              wire.FeedbagBuddyPrefsFriendOfFriend,
+	"friendGetContactList":        wire.FeedbagBuddyPrefsFriendGetContactList,
+	"compadInit":                  wire.FeedbagBuddyPrefsCompadInit,
+	"sendBuddyFeed":               wire.FeedbagBuddyPrefsSendBuddyFeed,
+	"blkSendIMWhileAway":          wire.FeedbagBuddyPrefsBlkSendIMWhileAway,
+	"showBuddyFeed":               wire.FeedbagBuddyPrefsShowBuddyFeed,
+	"noSaveVanityInfo":            wire.FeedbagBuddyPrefsNoSaveVanityInfo,
+	"acceptOffLineIM":             wire.FeedbagBuddyPrefsAcceptOfflineIM,
+	"showGroups":                  wire.FeedbagBuddyPrefsShowGroups,
+	"sortGroup":                   wire.FeedbagBuddyPrefsSortGroup,
+	"showOffLineBuddies":          wire.FeedbagBuddyPrefsShowOfflineBuddies,
+	"expandBuddies":               wire.FeedbagBuddyPrefsExpandBuddies,
+	"thirdPartyFeeds":             wire.FeedbagBuddyPrefsThirdPartyFeeds,
+	"notifyReceivedInvite":        wire.FeedbagBuddyPrefsNotifyReceivedInvite,
+	"apfAutoAccept":               wire.FeedbagBuddyPrefsApfAutoAccept,
+	"apfAutoAcceptBuddy":          wire.FeedbagBuddyPrefsApfAutoAcceptBuddy,
+	"blockAwayMsgFeed":            wire.FeedbagBuddyPrefsBlockAwayMsgFeed,
+	"blockAIMProfileFeed":         wire.FeedbagBuddyPrefsBlockAIMProfileFeed,
+	"blockAIMPagesFeed":           wire.FeedbagBuddyPrefsBlockAIMPagesFeed,
+	"blockJournalsFeed":           wire.FeedbagBuddyPrefsBlockJournalsFeed,
+	"blockLocationFeed":           wire.FeedbagBuddyPrefsBlockLocationFeed,
+	"blockStickiesFeed":           wire.FeedbagBuddyPrefsBlockStickiesFeed,
+	"blockUncutFeed":              wire.FeedbagBuddyPrefsBlockUncutFeed,
+	"blockLinksFeed":              wire.FeedbagBuddyPrefsBlockLinksFeed,
+	"blockAIMBulletinFeed":        wire.FeedbagBuddyPrefsBlockAIMBulletinFeed,
+	"saveStatusMsg":               wire.FeedbagBuddyPrefsSaveStatusMsg,
 	// Not in the spec Preferences enum, but sent by the web client.
-	"apfNotifyReceivedInviteByEmail": {wire.FeedbagBuddyPrefsApfNotifyReceivedByEmail, false},
-	"showOfflineGrp":                 {wire.FeedbagBuddyPrefsShowOfflineGrp, true},
-	"offlineGrpCollapsed":            {wire.FeedbagBuddyPrefsOfflineGrpCollapsed, false},
-	"firstImSoundOnly":               {wire.FeedbagBuddyPrefsFirstIMSoundOnly, false},
-	"imblastInviteNotify":            {wire.FeedbagBuddyPrefsImblastInviteNotify, true},
+	"apfNotifyReceivedInviteByEmail": wire.FeedbagBuddyPrefsApfNotifyReceivedByEmail,
+	"showOfflineGrp":                 wire.FeedbagBuddyPrefsShowOfflineGrp,
+	"offlineGrpCollapsed":            wire.FeedbagBuddyPrefsOfflineGrpCollapsed,
+	"firstImSoundOnly":               wire.FeedbagBuddyPrefsFirstIMSoundOnly,
+	"imblastInviteNotify":            wire.FeedbagBuddyPrefsImblastInviteNotify,
 
 	// Web-client-only preferences with no OSCAR buddy-pref equivalent. OSCAR
 	// defines prefs through 0x4B, so we persist these in the same feedbag
 	// buddy-prefs bitmask at positions above that range; no real OSCAR client
 	// reads or writes these bits.
-	"viewIMsInBubbles":           {wire.FeedbagBuddyPrefsViewIMsInBubbles, true},
-	"viewIMTimestampsRelative":   {wire.FeedbagBuddyPrefsViewIMTimestampsRelative, false},
-	"globalOTR":                  {wire.FeedbagBuddyPrefsGlobalOTR, false},
-	"imblastInviteFromBuddyOnly": {wire.FeedbagBuddyPrefsImblastInviteFromBuddyOnly, false},
+	"viewIMsInBubbles":           wire.FeedbagBuddyPrefsViewIMsInBubbles,
+	"viewIMTimestampsRelative":   wire.FeedbagBuddyPrefsViewIMTimestampsRelative,
+	"globalOTR":                  wire.FeedbagBuddyPrefsGlobalOTR,
+	"imblastInviteFromBuddyOnly": wire.FeedbagBuddyPrefsImblastInviteFromBuddyOnly,
 }
 
 // PermitDenyData contains permit/deny list information.
@@ -170,7 +164,7 @@ func (h *PreferenceHandler) SetPreferences(w http.ResponseWriter, r *http.Reques
 			continue
 		}
 		on := parseBoolPref(val)
-		item.TLVList = wire.SetBuddyPref(item.TLVList, pref.num, on)
+		item.TLVList = wire.SetBuddyPref(item.TLVList, pref, on)
 		applied[name] = boolToPrefInt(on)
 	}
 
@@ -329,16 +323,12 @@ func effectiveBuddyPrefs(list wire.TLVList) map[string]interface{} {
 	return prefs
 }
 
-// effectivePrefValue returns the 0/1 value for pref, using the feedbag value
-// when present and the spec default otherwise. Values are emitted as numbers
-// (not "1"/"0" strings) because the web client evaluates them with JavaScript
-// truthiness/numeric comparisons, where the string "0" is truthy.
-func effectivePrefValue(list wire.TLVList, pref buddyPref) int {
-	valid, val := wire.BuddyPref(list, pref.num)
-	if !valid {
-		val = pref.def
-	}
-	return boolToPrefInt(val)
+// effectivePrefValue returns the 0/1 value for the buddy pref prefNum, deferring
+// to wire.BuddyPref for both the stored value and its default. Values are emitted
+// as numbers (not "1"/"0" strings) because the web client evaluates them with
+// JavaScript truthiness/numeric comparisons, where the string "0" is truthy.
+func effectivePrefValue(list wire.TLVList, prefNum uint16) int {
+	return boolToPrefInt(wire.BuddyPref(list, prefNum))
 }
 
 // parseBoolPref interprets a Web API preference query value as a boolean.

+ 9 - 9
server/webapi/handlers/preference_test.go

@@ -35,9 +35,11 @@ func TestPreferenceHandler_SetPreferences(t *testing.T) {
 	oscarInstance := state.NewSession().AddInstance()
 	sessionMgr, aimsid := createTestSessionManagerWithOSCAR("testuser", oscarInstance)
 
-	// Existing feedbag already has displayLogin (0x00) enabled; it must survive.
+	// Existing feedbag already has acceptCustomBart (0x0B, default false) enabled;
+	// it must survive the read-modify-write. Using a default-false pref means an
+	// observed true value can only come from the stored bit, not the default.
 	fs.On("Query", mock.Anything, oscarInstance, mock.Anything).
-		Return(buddyPrefsFeedbag(map[uint16]bool{0x00: true}), nil)
+		Return(buddyPrefsFeedbag(map[uint16]bool{wire.FeedbagBuddyPrefsAcceptCustomBart: true}), nil)
 
 	var upserted []wire.FeedbagItem
 	fs.On("UpsertItem", mock.Anything, oscarInstance, mock.Anything, mock.Anything).
@@ -59,14 +61,12 @@ func TestPreferenceHandler_SetPreferences(t *testing.T) {
 		item := upserted[0]
 		assert.Equal(t, wire.FeedbagClassIdBuddyPrefs, item.ClassID)
 
-		assertPref := func(num uint16, wantValid, wantValue bool) {
-			valid, value := wire.BuddyPref(item.TLVList, num)
-			assert.Equalf(t, wantValid, valid, "pref 0x%02x valid", num)
-			assert.Equalf(t, wantValue, value, "pref 0x%02x value", num)
+		assertPref := func(num uint16, want bool) {
+			assert.Equalf(t, want, wire.BuddyPref(item.TLVList, num), "pref 0x%02x", num)
 		}
-		assertPref(0x00, true, true)  // preserved
-		assertPref(0x15, true, false) // playIMSound off
-		assertPref(0x16, true, true)  // discloseTyping on
+		assertPref(wire.FeedbagBuddyPrefsAcceptCustomBart, true) // preserved (default false)
+		assertPref(0x15, false)                                  // playIMSound off (default true)
+		assertPref(0x16, true)                                   // discloseTyping on
 	}
 	fs.AssertExpectations(t)
 }

+ 104 - 4
wire/buddy_prefs.go

@@ -154,10 +154,110 @@ func buddyPrefBit(prefNum uint16, length int) (index int, mask byte) {
 	return index, byte(0x80) >> (offset % 8)
 }
 
-// BuddyPref reads preference prefNum from a feedbag buddy-prefs TLV list. valid
-// reports whether the preference is present in the bitmask; value is its boolean
-// value, meaningful only when valid is true.
-func BuddyPref(list TLVList, prefNum uint16) (valid, value bool) {
+// buddyPrefDefaults is the source of truth for the default value of each buddy
+// preference: the value BuddyPref returns when the preference bit is absent from
+// the feedbag bitmask.
+var buddyPrefDefaults = map[uint16]bool{
+	FeedbagBuddyPrefsDisplayLogin:          true,
+	FeedbagBuddyPrefsDisplayEBuddy:         true,
+	FeedbagBuddyPrefsPlayEnter:             true,
+	FeedbagBuddyPrefsPlayExit:              true,
+	FeedbagBuddyPrefsViewIMStamp:           true,
+	FeedbagBuddyPrefsViewSmileys:           true,
+	FeedbagBuddyPrefsAcceptIcons:           true,
+	FeedbagBuddyPrefsKnockNonAOLIMs:        true,
+	FeedbagBuddyPrefsKnockNonListIMs:       true,
+	FeedbagBuddyPrefsDiscloseIdle:          true,
+	FeedbagBuddyPrefsAcceptCustomBart:      false,
+	FeedbagBuddyPrefsAcceptNonListBart:     false,
+	FeedbagBuddyPrefsAcceptBgs:             true,
+	FeedbagBuddyPrefsAcceptChromes:         true,
+	FeedbagBuddyPrefsAcceptBLSounds:        true,
+	FeedbagBuddyPrefsAcceptIMSounds:        true,
+	FeedbagBuddyPrefsNoSeeRecentBuddies:    false,
+	FeedbagBuddyPrefsAcceptSMSLegal:        false,
+	FeedbagBuddyPrefsEnterDoesCRLF:         false,
+	FeedbagBuddyPrefsPlayIMSound:           true,
+	FeedbagBuddyPrefsDiscloseTyping:        true,
+	FeedbagBuddyPrefsAcceptSuperIcons:      true,
+	FeedbagBuddyPrefsAcceptBLRichText:      true,
+	FeedbagBuddyPrefsReduceIMSound:         true,
+	FeedbagBuddyPrefsConfirmDirectIM:       true,
+	FeedbagBuddyPrefsOneTabbedIMWindow:     true,
+	FeedbagBuddyPrefsBuddyInfoOnMouseover:  true,
+	FeedbagBuddyPrefsDiscloseBuddyMatches:  true,
+	FeedbagBuddyPrefsCatchIMs:              false,
+	FeedbagBuddyPrefsShowFriendlyName:      true,
+	FeedbagBuddyPrefsDiscloseRadio:         true,
+	FeedbagBuddyPrefsShowCapabilities:      true,
+	FeedbagBuddyPrefsShowBuddyListFilter:   true,
+	FeedbagBuddyPrefsShowAwayIdle:          true,
+	FeedbagBuddyPrefsShowMobile:            true,
+	FeedbagBuddyPrefsSortBuddyList:         false,
+	FeedbagBuddyPrefsCatchIMsForClient:     false,
+	FeedbagBuddyPrefsNewMessageSmallNotify: true,
+	FeedbagBuddyPrefsNoFrequentBuddies:     false,
+	FeedbagBuddyPrefsBlogAwayMessages:      false,
+	FeedbagBuddyPrefsBlogAIMSigMessages:    false,
+	FeedbagBuddyPrefsBlogNoComments:        false,
+	FeedbagBuddyPrefsFriendOfFriend:        false,
+	FeedbagBuddyPrefsFriendGetContactList:  false,
+	FeedbagBuddyPrefsCompadInit:            false,
+	FeedbagBuddyPrefsSendBuddyFeed:         true,
+	FeedbagBuddyPrefsBlkSendIMWhileAway:    false,
+	FeedbagBuddyPrefsShowBuddyFeed:         true,
+	FeedbagBuddyPrefsNoSaveVanityInfo:      false,
+	FeedbagBuddyPrefsAcceptOfflineIM:       true,
+	// ShowGroups deliberately departs from the OSCAR spec default (0). The web
+	// client keys group-header visibility off this pref and has no default of
+	// its own, so we default it on to keep headers visible.
+	FeedbagBuddyPrefsShowGroups:               true,
+	FeedbagBuddyPrefsSortGroup:                true,
+	FeedbagBuddyPrefsShowOfflineBuddies:       true,
+	FeedbagBuddyPrefsExpandBuddies:            false,
+	FeedbagBuddyPrefsThirdPartyFeeds:          false,
+	FeedbagBuddyPrefsNotifyReceivedInvite:     true,
+	FeedbagBuddyPrefsApfAutoAccept:            false,
+	FeedbagBuddyPrefsApfAutoAcceptBuddy:       false,
+	FeedbagBuddyPrefsBlockAwayMsgFeed:         false,
+	FeedbagBuddyPrefsBlockAIMProfileFeed:      false,
+	FeedbagBuddyPrefsBlockAIMPagesFeed:        false,
+	FeedbagBuddyPrefsBlockJournalsFeed:        false,
+	FeedbagBuddyPrefsBlockLocationFeed:        false,
+	FeedbagBuddyPrefsBlockStickiesFeed:        false,
+	FeedbagBuddyPrefsBlockUncutFeed:           false,
+	FeedbagBuddyPrefsBlockLinksFeed:           false,
+	FeedbagBuddyPrefsBlockAIMBulletinFeed:     false,
+	FeedbagBuddyPrefsSaveStatusMsg:            true,
+	FeedbagBuddyPrefsApfNotifyReceivedByEmail: false,
+	FeedbagBuddyPrefsShowOfflineGrp:           true,
+	FeedbagBuddyPrefsOfflineGrpCollapsed:      false,
+	FeedbagBuddyPrefsFirstIMSoundOnly:         false,
+	FeedbagBuddyPrefsImblastInviteNotify:      true,
+
+	// Web-client-only preferences with no OSCAR equivalent.
+	FeedbagBuddyPrefsViewIMsInBubbles:           true,
+	FeedbagBuddyPrefsViewIMTimestampsRelative:   false,
+	FeedbagBuddyPrefsGlobalOTR:                  false,
+	FeedbagBuddyPrefsImblastInviteFromBuddyOnly: false,
+}
+
+// BuddyPref returns the effective boolean value of preference prefNum in a
+// feedbag buddy-prefs TLV list: the stored value when the preference is present
+// in the bitmask, otherwise the preference's default (see buddyPrefDefaults).
+// Whether the preference is actually present ("valid") is an internal detail;
+// BuddyPref is the source of truth for default values.
+func BuddyPref(list TLVList, prefNum uint16) bool {
+	if valid, value := readBuddyPref(list, prefNum); valid {
+		return value
+	}
+	return buddyPrefDefaults[prefNum]
+}
+
+// readBuddyPref reads preference prefNum from a feedbag buddy-prefs TLV list.
+// valid reports whether the preference is present in the bitmask; value is its
+// boolean value, meaningful only when valid is true.
+func readBuddyPref(list TLVList, prefNum uint16) (valid, value bool) {
 	validTag, valueTag := buddyPrefTags(prefNum)
 
 	validBytes, ok := list.Bytes(validTag)

+ 24 - 26
wire/buddy_prefs_test.go

@@ -7,19 +7,28 @@ import (
 
 func TestBuddyPref(t *testing.T) {
 	tests := []struct {
-		name      string
-		prefNum   uint16
-		list      TLVList
-		wantValid bool
-		wantValue bool
+		name    string
+		prefNum uint16
+		list    TLVList
+		want    bool
 	}{
 		{
-			name:    "absent bitmask => not valid",
-			prefNum: 0x15,
+			// PlayIMSound (0x15) defaults to true when the bitmask is absent.
+			name:    "absent bitmask => default true",
+			prefNum: FeedbagBuddyPrefsPlayIMSound,
+			list:    TLVList{},
+			want:    true,
+		},
+		{
+			// AcceptCustomBart (0x0B) defaults to false when the bitmask is absent.
+			name:    "absent bitmask => default false",
+			prefNum: FeedbagBuddyPrefsAcceptCustomBart,
 			list:    TLVList{},
+			want:    false,
 		},
 		{
 			// pref 0x34 (=52) lives in BuddyPrefs2 at offset 19 (index 2, mask 0x10).
+			// Explicitly set to false, overriding the true default.
 			name:    "offline messages disabled",
 			prefNum: FeedbagBuddyPrefsAcceptOfflineIM,
 			list: TLVList{
@@ -28,8 +37,7 @@ func TestBuddyPref(t *testing.T) {
 				{Tag: FeedbagAttributesBuddyPrefs2Valid, Value: []byte{0, 0, 17}},
 				{Tag: FeedbagAttributesBuddyPrefs2, Value: []byte{0, 0, 1}},
 			},
-			wantValid: true,
-			wantValue: false,
+			want: false,
 		},
 		{
 			// pref 22 lives in BuddyPrefs at index 1, mask 0x40.
@@ -39,8 +47,7 @@ func TestBuddyPref(t *testing.T) {
 				{Tag: FeedbagAttributesBuddyPrefsValid, Value: []byte{0, 0x40, 0, 0}},
 				{Tag: FeedbagAttributesBuddyPrefs, Value: []byte{0, 0x40, 0, 0}},
 			},
-			wantValid: true,
-			wantValue: true,
+			want: true,
 		},
 		{
 			// prefs 32 and 33 both fall at offset 0 in BuddyPrefs2.
@@ -50,17 +57,14 @@ func TestBuddyPref(t *testing.T) {
 				{Tag: FeedbagAttributesBuddyPrefs2Valid, Value: []byte{0x80, 0, 0, 0}},
 				{Tag: FeedbagAttributesBuddyPrefs2, Value: []byte{0x80, 0, 0, 0}},
 			},
-			wantValid: true,
-			wantValue: true,
+			want: true,
 		},
 	}
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			valid, value := BuddyPref(tt.list, tt.prefNum)
-			if valid != tt.wantValid || value != tt.wantValue {
-				t.Fatalf("BuddyPref(%d) = (valid=%v, value=%v), want (valid=%v, value=%v)",
-					tt.prefNum, valid, value, tt.wantValid, tt.wantValue)
+			if got := BuddyPref(tt.list, tt.prefNum); got != tt.want {
+				t.Fatalf("BuddyPref(%d) = %v, want %v", tt.prefNum, got, tt.want)
 			}
 		})
 	}
@@ -72,11 +76,7 @@ func TestSetBuddyPref_RoundTrip(t *testing.T) {
 	for _, want := range []bool{true, false} {
 		for _, prefNum := range prefs {
 			list := SetBuddyPref(TLVList{}, prefNum, want)
-			valid, value := BuddyPref(list, prefNum)
-			if !valid {
-				t.Errorf("pref 0x%02x set to %v: not valid after set", prefNum, want)
-			}
-			if value != want {
+			if value := BuddyPref(list, prefNum); value != want {
 				t.Errorf("pref 0x%02x round-trip: got %v, want %v", prefNum, value, want)
 			}
 		}
@@ -98,10 +98,8 @@ func TestSetBuddyPref_PreservesOtherBits(t *testing.T) {
 		{0x16, true},
 		{0x34, true},
 	} {
-		valid, value := BuddyPref(list, tc.prefNum)
-		if !valid || value != tc.wantValue {
-			t.Errorf("pref 0x%02x = (valid=%v, value=%v), want (true, %v)",
-				tc.prefNum, valid, value, tc.wantValue)
+		if value := BuddyPref(list, tc.prefNum); value != tc.wantValue {
+			t.Errorf("pref 0x%02x = %v, want %v", tc.prefNum, value, tc.wantValue)
 		}
 	}
 }