| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381 |
- package foodgroup
- import (
- "context"
- "fmt"
- "github.com/mk6i/open-oscar-server/state"
- "github.com/mk6i/open-oscar-server/wire"
- )
- // NewBuddyService creates a new instance of BuddyService.
- func NewBuddyService(
- messageRelayer MessageRelayer,
- clientSideBuddyListManager ClientSideBuddyListManager,
- relationshipFetcher RelationshipFetcher,
- sessionRetriever SessionRetriever,
- bartItemManager BARTItemManager,
- contactPreAuthorizer ContactPreAuthorizer,
- ) *BuddyService {
- return &BuddyService{
- buddyBroadcaster: newBuddyNotifier(bartItemManager, relationshipFetcher, messageRelayer, sessionRetriever),
- clientSideBuddyListManager: clientSideBuddyListManager,
- contactPreAuthorizer: contactPreAuthorizer,
- }
- }
- // BuddyService provides functionality for the Buddy food group.
- type BuddyService struct {
- clientSideBuddyListManager ClientSideBuddyListManager
- buddyBroadcaster buddyBroadcaster
- contactPreAuthorizer ContactPreAuthorizer
- }
- // RightsQuery returns buddy list service parameters.
- func (s BuddyService) RightsQuery(_ context.Context, frameIn wire.SNACFrame) wire.SNACMessage {
- return wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.Buddy,
- SubGroup: wire.BuddyRightsReply,
- RequestID: frameIn.RequestID,
- },
- Body: wire.SNAC_0x03_0x03_BuddyRightsReply{
- TLVRestBlock: wire.TLVRestBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.BuddyTLVTagsParmMaxBuddies, uint16(100)),
- wire.NewTLVBE(wire.BuddyTLVTagsParmMaxWatchers, uint16(100)),
- wire.NewTLVBE(wire.BuddyTLVTagsParmMaxIcqBroad, uint16(100)),
- wire.NewTLVBE(wire.BuddyTLVTagsParmMaxTempBuddies, uint16(100)),
- },
- },
- },
- }
- }
- // AddBuddies adds buddies to my client-side buddy list.
- // It returns a single SNAC(03,0x0A) listing buddies rejected for lack of
- // pre-authorization (ICQ UIN contacts only), or nil when none were rejected.
- func (s BuddyService) AddBuddies(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x03_0x04_BuddyAddBuddies) (*wire.SNACMessage, error) {
- var rejected []struct {
- ScreenName string `oscar:"len_prefix=uint8"`
- }
- var added []state.IdentScreenName
- for _, entry := range inBody.Buddies {
- them := state.NewIdentScreenName(entry.ScreenName)
- if them.UIN() != 0 {
- if instance.IdentScreenName().UIN() == 0 {
- // AIM may not add ICQ UIN buddies on the client-side buddy path; only the
- // feedbag route supports ICQ interop for AIM with proper authorization.
- // Otherwise an AIM session could subscribe to buddy presence ("snoop" on ICQ
- // status) without completing the authorization flow.
- rejected = append(rejected, struct {
- ScreenName string `oscar:"len_prefix=uint8"`
- }{ScreenName: entry.ScreenName})
- continue
- }
- blocked, err := s.contactPreAuthorizer.RequiresAuthorization(ctx, them, instance.IdentScreenName())
- if err != nil {
- return nil, err
- }
- if blocked {
- rejected = append(rejected, struct {
- ScreenName string `oscar:"len_prefix=uint8"`
- }{ScreenName: entry.ScreenName})
- continue
- }
- }
- if err := s.clientSideBuddyListManager.AddBuddy(ctx, instance.IdentScreenName(), them); err != nil {
- return nil, err
- }
- added = append(added, them)
- }
- instance.SetContactsInit()
- if !instance.SignonComplete() {
- // client has not completed sign-on sequence, so any arrival
- // messages sent at this point would be ignored by the client.
- return nil, nil
- }
- if len(added) > 0 {
- if err := s.buddyBroadcaster.BroadcastVisibility(ctx, instance, added, true); err != nil {
- return nil, fmt.Errorf("buddyBroadcaster.BroadcastVisibility: %w", err)
- }
- }
- if len(rejected) > 0 {
- return &wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.Buddy,
- SubGroup: wire.BuddyRejectNotification,
- RequestID: inFrame.RequestID,
- },
- Body: wire.SNAC_0x03_0x0A_BuddyRejectNotification{Buddies: rejected},
- }, nil
- }
- return nil, nil
- }
- // DelBuddies deletes buddies from my client-side buddy list.
- func (s BuddyService) DelBuddies(ctx context.Context, instance *state.SessionInstance, inBody wire.SNAC_0x03_0x05_BuddyDelBuddies) error {
- var toNotify []state.IdentScreenName
- for _, entry := range inBody.Buddies {
- sn := state.NewIdentScreenName(entry.ScreenName)
- if err := s.clientSideBuddyListManager.RemoveBuddy(ctx, instance.IdentScreenName(), sn); err != nil {
- return err
- }
- toNotify = append(toNotify, sn)
- }
- if err := s.buddyBroadcaster.BroadcastVisibility(ctx, instance, toNotify, true); err != nil {
- return fmt.Errorf("buddyBroadcaster.BroadcastVisibility: %w", err)
- }
- return nil
- }
- // AddTempBuddies adds temporary buddies to the user's buddy list that persist
- // for the duration of the user's session.
- func (s BuddyService) AddTempBuddies(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x03_0x0F_BuddyAddTempBuddies) (*wire.SNACMessage, error) {
- var b wire.SNAC_0x03_0x04_BuddyAddBuddies
- for _, buddy := range inBody.Buddies {
- b.Buddies = append(b.Buddies, struct {
- ScreenName string `oscar:"len_prefix=uint8"`
- }{ScreenName: buddy.ScreenName})
- }
- return s.AddBuddies(ctx, instance, inFrame, b)
- }
- // DelTempBuddies deletes temporary buddies from the user's buddy list.
- func (s BuddyService) DelTempBuddies(ctx context.Context, instance *state.SessionInstance, inBody wire.SNAC_0x03_0x10_BuddyDelTempBuddies) error {
- var b wire.SNAC_0x03_0x05_BuddyDelBuddies
- for _, buddy := range inBody.Buddies {
- b.Buddies = append(b.Buddies, struct {
- ScreenName string `oscar:"len_prefix=uint8"`
- }{ScreenName: buddy.ScreenName})
- }
- return s.DelBuddies(ctx, instance, b)
- }
- // BroadcastBuddyArrived broadcasts buddy arrival with custom user info (implements DepartureNotifier)
- func (s BuddyService) BroadcastBuddyArrived(ctx context.Context, screenName state.IdentScreenName, userInfo wire.TLVUserInfo) error {
- return s.buddyBroadcaster.BroadcastBuddyArrived(ctx, screenName, userInfo)
- }
- func (s BuddyService) BroadcastBuddyDeparted(ctx context.Context, screenName state.IdentScreenName) error {
- return s.buddyBroadcaster.BroadcastBuddyDeparted(ctx, screenName)
- }
- func (s BuddyService) BroadcastVisibility(ctx context.Context, you *state.SessionInstance, filter []state.IdentScreenName, doSendDepartures bool) error {
- return s.buddyBroadcaster.BroadcastVisibility(ctx, you, filter, doSendDepartures)
- }
- func newBuddyNotifier(
- bartItemManager BARTItemManager,
- relationshipFetcher RelationshipFetcher,
- messageRelayer MessageRelayer,
- sessionRetriever SessionRetriever,
- ) buddyNotifier {
- return buddyNotifier{
- bartItemManager: bartItemManager,
- relationshipFetcher: relationshipFetcher,
- messageRelayer: messageRelayer,
- sessionRetriever: sessionRetriever,
- }
- }
- // buddyNotifier centralizes logic for sending buddy arrival and departure
- // notifications.
- type buddyNotifier struct {
- bartItemManager BARTItemManager
- relationshipFetcher RelationshipFetcher
- messageRelayer MessageRelayer
- sessionRetriever SessionRetriever
- }
- // BroadcastBuddyArrived sends the latest user info to the user's adjacent users.
- // While updates are sent via the wire.BuddyArrived SNAC, the message is not
- // only used to indicate the user coming online. It can also notify changes to
- // buddy icons, warning levels, invisibility status, etc.
- func (s buddyNotifier) BroadcastBuddyArrived(ctx context.Context, screenName state.IdentScreenName, userInfo wire.TLVUserInfo) error {
- if userInfo.IsInvisible() {
- return nil
- }
- users, err := s.relationshipFetcher.AllRelationships(ctx, screenName, nil)
- if err != nil {
- return err
- }
- var recipients []state.IdentScreenName
- for _, user := range users {
- if user.YouBlock || user.BlocksYou || !user.IsOnTheirList {
- continue
- }
- recipients = append(recipients, user.User)
- }
- s.messageRelayer.RelayToScreenNames(ctx, recipients, wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.Buddy,
- SubGroup: wire.BuddyArrived,
- RequestID: wire.ReqIDFromServer,
- },
- Body: wire.SNAC_0x03_0x0B_BuddyArrived{
- TLVUserInfo: userInfo,
- },
- })
- return nil
- }
- func (s buddyNotifier) BroadcastBuddyDeparted(ctx context.Context, screenName state.IdentScreenName) error {
- users, err := s.relationshipFetcher.AllRelationships(ctx, screenName, nil)
- if err != nil {
- return err
- }
- var recipients []state.IdentScreenName
- for _, user := range users {
- if user.YouBlock || user.BlocksYou || !user.IsOnTheirList {
- continue
- }
- recipients = append(recipients, user.User)
- }
- s.messageRelayer.RelayToScreenNames(ctx, recipients, wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.Buddy,
- SubGroup: wire.BuddyDeparted,
- RequestID: wire.ReqIDFromServer,
- },
- Body: wire.SNAC_0x03_0x0C_BuddyDeparted{
- TLVUserInfo: wire.TLVUserInfo{
- // don't include the TLV block, otherwise the AIM client fails
- // to process the block event
- ScreenName: screenName.String(),
- WarningLevel: 0,
- TLVBlock: wire.TLVBlock{
- TLVList: wire.TLVList{
- // this TLV needs to be set in order for departure
- // events to work in ICQ
- wire.NewTLVBE(wire.OServiceUserInfoUserFlags, uint16(0)),
- },
- },
- },
- },
- })
- return nil
- }
- // BroadcastVisibility sends you and related users arrival/departure
- // notifications that reflect your buddy list and privacy preferences.
- //
- // Behavior:
- // - Sends you arrival notifications for users on your buddy list that I do
- // not block.
- // - Sends arrival notifications to users that you block who have you on
- // their buddy lists.
- // - Sends you departure notifications for users on your buddy list that you
- // block (if doSendDepartures is true).
- // - Sends departure notifications to users that you block who have you on
- // their buddy lists (if doSendDepartures is true).
- // - Don't send notifications for any user that blocks you.
- //
- // This method is called when your visibility settings change, ensuring that
- // all relevant users are notified of your arrival or departure status.
- func (s buddyNotifier) BroadcastVisibility(
- ctx context.Context,
- you *state.SessionInstance,
- filter []state.IdentScreenName,
- doSendDepartures bool,
- ) error {
- relationships, err := s.relationshipFetcher.AllRelationships(ctx, you.IdentScreenName(), filter)
- if err != nil {
- return fmt.Errorf("retrieving relationships: %w", err)
- }
- yourTLVInfo := you.Session().TLVUserInfo()
- for _, relationship := range relationships {
- if relationship.BlocksYou {
- continue // they block you, don't send them notifications
- }
- theirSess := s.sessionRetriever.RetrieveSession(relationship.User)
- if theirSess == nil {
- continue // they are offline
- }
- if !relationship.YouBlock {
- if relationship.IsOnTheirList {
- // tell them you're online
- s.unicastBuddyArrived(ctx, yourTLVInfo, theirSess.IdentScreenName())
- }
- if relationship.IsOnYourList {
- theirInfo := theirSess.TLVUserInfo()
- // tell you they're online
- s.unicastBuddyArrived(ctx, theirInfo, you.IdentScreenName())
- }
- } else if relationship.YouBlock && doSendDepartures {
- if relationship.IsOnTheirList {
- // tell them you're offline
- s.unicastBuddyDeparted(ctx, you.Session(), theirSess.IdentScreenName())
- }
- if relationship.IsOnYourList {
- // tell you they're offline
- s.unicastBuddyDeparted(ctx, theirSess, you.IdentScreenName())
- }
- }
- }
- return nil
- }
- func (s buddyNotifier) unicastBuddyDeparted(ctx context.Context, from *state.Session, to state.IdentScreenName) {
- s.messageRelayer.RelayToScreenName(ctx, to, wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.Buddy,
- SubGroup: wire.BuddyDeparted,
- RequestID: wire.ReqIDFromServer,
- },
- Body: wire.SNAC_0x03_0x0C_BuddyDeparted{
- TLVUserInfo: wire.TLVUserInfo{
- // don't include the TLV block, otherwise the AIM client fails
- // to process the block event
- ScreenName: from.IdentScreenName().String(),
- WarningLevel: from.Warning(),
- },
- },
- })
- }
- // unicastBuddyArrived sends the latest user info to a particular user.
- // While updates are sent via the wire.BuddyArrived SNAC, the message is not
- // only used to indicate the user coming online. It can also notify changes to
- // buddy icons, warning levels, invisibility status, etc.
- func (s buddyNotifier) unicastBuddyArrived(ctx context.Context, userInfo wire.TLVUserInfo, to state.IdentScreenName) {
- if userInfo.IsInvisible() {
- return
- }
- s.messageRelayer.RelayToScreenName(ctx, to, wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.Buddy,
- SubGroup: wire.BuddyArrived,
- RequestID: wire.ReqIDFromServer,
- },
- Body: wire.SNAC_0x03_0x0B_BuddyArrived{
- TLVUserInfo: userInfo,
- },
- })
- }
|