4
0

buddy.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  1. package foodgroup
  2. import (
  3. "context"
  4. "fmt"
  5. "github.com/mk6i/open-oscar-server/state"
  6. "github.com/mk6i/open-oscar-server/wire"
  7. )
  8. // NewBuddyService creates a new instance of BuddyService.
  9. func NewBuddyService(
  10. messageRelayer MessageRelayer,
  11. clientSideBuddyListManager ClientSideBuddyListManager,
  12. relationshipFetcher RelationshipFetcher,
  13. sessionRetriever SessionRetriever,
  14. bartItemManager BARTItemManager,
  15. contactPreAuthorizer ContactPreAuthorizer,
  16. ) *BuddyService {
  17. return &BuddyService{
  18. buddyBroadcaster: newBuddyNotifier(bartItemManager, relationshipFetcher, messageRelayer, sessionRetriever),
  19. clientSideBuddyListManager: clientSideBuddyListManager,
  20. contactPreAuthorizer: contactPreAuthorizer,
  21. }
  22. }
  23. // BuddyService provides functionality for the Buddy food group.
  24. type BuddyService struct {
  25. clientSideBuddyListManager ClientSideBuddyListManager
  26. buddyBroadcaster buddyBroadcaster
  27. contactPreAuthorizer ContactPreAuthorizer
  28. }
  29. // RightsQuery returns buddy list service parameters.
  30. func (s BuddyService) RightsQuery(_ context.Context, frameIn wire.SNACFrame) wire.SNACMessage {
  31. return wire.SNACMessage{
  32. Frame: wire.SNACFrame{
  33. FoodGroup: wire.Buddy,
  34. SubGroup: wire.BuddyRightsReply,
  35. RequestID: frameIn.RequestID,
  36. },
  37. Body: wire.SNAC_0x03_0x03_BuddyRightsReply{
  38. TLVRestBlock: wire.TLVRestBlock{
  39. TLVList: wire.TLVList{
  40. wire.NewTLVBE(wire.BuddyTLVTagsParmMaxBuddies, uint16(100)),
  41. wire.NewTLVBE(wire.BuddyTLVTagsParmMaxWatchers, uint16(100)),
  42. wire.NewTLVBE(wire.BuddyTLVTagsParmMaxIcqBroad, uint16(100)),
  43. wire.NewTLVBE(wire.BuddyTLVTagsParmMaxTempBuddies, uint16(100)),
  44. },
  45. },
  46. },
  47. }
  48. }
  49. // AddBuddies adds buddies to my client-side buddy list.
  50. // It returns a single SNAC(03,0x0A) listing buddies rejected for lack of
  51. // pre-authorization (ICQ UIN contacts only), or nil when none were rejected.
  52. func (s BuddyService) AddBuddies(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x03_0x04_BuddyAddBuddies) (*wire.SNACMessage, error) {
  53. var rejected []struct {
  54. ScreenName string `oscar:"len_prefix=uint8"`
  55. }
  56. var added []state.IdentScreenName
  57. for _, entry := range inBody.Buddies {
  58. them := state.NewIdentScreenName(entry.ScreenName)
  59. if them.UIN() != 0 {
  60. if instance.IdentScreenName().UIN() == 0 {
  61. // AIM may not add ICQ UIN buddies on the client-side buddy path; only the
  62. // feedbag route supports ICQ interop for AIM with proper authorization.
  63. // Otherwise an AIM session could subscribe to buddy presence ("snoop" on ICQ
  64. // status) without completing the authorization flow.
  65. rejected = append(rejected, struct {
  66. ScreenName string `oscar:"len_prefix=uint8"`
  67. }{ScreenName: entry.ScreenName})
  68. continue
  69. }
  70. blocked, err := s.contactPreAuthorizer.RequiresAuthorization(ctx, them, instance.IdentScreenName())
  71. if err != nil {
  72. return nil, err
  73. }
  74. if blocked {
  75. rejected = append(rejected, struct {
  76. ScreenName string `oscar:"len_prefix=uint8"`
  77. }{ScreenName: entry.ScreenName})
  78. continue
  79. }
  80. }
  81. if err := s.clientSideBuddyListManager.AddBuddy(ctx, instance.IdentScreenName(), them); err != nil {
  82. return nil, err
  83. }
  84. added = append(added, them)
  85. }
  86. instance.SetContactsInit()
  87. if !instance.SignonComplete() {
  88. // client has not completed sign-on sequence, so any arrival
  89. // messages sent at this point would be ignored by the client.
  90. return nil, nil
  91. }
  92. if len(added) > 0 {
  93. if err := s.buddyBroadcaster.BroadcastVisibility(ctx, instance, added, true); err != nil {
  94. return nil, fmt.Errorf("buddyBroadcaster.BroadcastVisibility: %w", err)
  95. }
  96. }
  97. if len(rejected) > 0 {
  98. return &wire.SNACMessage{
  99. Frame: wire.SNACFrame{
  100. FoodGroup: wire.Buddy,
  101. SubGroup: wire.BuddyRejectNotification,
  102. RequestID: inFrame.RequestID,
  103. },
  104. Body: wire.SNAC_0x03_0x0A_BuddyRejectNotification{Buddies: rejected},
  105. }, nil
  106. }
  107. return nil, nil
  108. }
  109. // DelBuddies deletes buddies from my client-side buddy list.
  110. func (s BuddyService) DelBuddies(ctx context.Context, instance *state.SessionInstance, inBody wire.SNAC_0x03_0x05_BuddyDelBuddies) error {
  111. var toNotify []state.IdentScreenName
  112. for _, entry := range inBody.Buddies {
  113. sn := state.NewIdentScreenName(entry.ScreenName)
  114. if err := s.clientSideBuddyListManager.RemoveBuddy(ctx, instance.IdentScreenName(), sn); err != nil {
  115. return err
  116. }
  117. toNotify = append(toNotify, sn)
  118. }
  119. if err := s.buddyBroadcaster.BroadcastVisibility(ctx, instance, toNotify, true); err != nil {
  120. return fmt.Errorf("buddyBroadcaster.BroadcastVisibility: %w", err)
  121. }
  122. return nil
  123. }
  124. // AddTempBuddies adds temporary buddies to the user's buddy list that persist
  125. // for the duration of the user's session.
  126. func (s BuddyService) AddTempBuddies(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x03_0x0F_BuddyAddTempBuddies) (*wire.SNACMessage, error) {
  127. var b wire.SNAC_0x03_0x04_BuddyAddBuddies
  128. for _, buddy := range inBody.Buddies {
  129. b.Buddies = append(b.Buddies, struct {
  130. ScreenName string `oscar:"len_prefix=uint8"`
  131. }{ScreenName: buddy.ScreenName})
  132. }
  133. return s.AddBuddies(ctx, instance, inFrame, b)
  134. }
  135. // DelTempBuddies deletes temporary buddies from the user's buddy list.
  136. func (s BuddyService) DelTempBuddies(ctx context.Context, instance *state.SessionInstance, inBody wire.SNAC_0x03_0x10_BuddyDelTempBuddies) error {
  137. var b wire.SNAC_0x03_0x05_BuddyDelBuddies
  138. for _, buddy := range inBody.Buddies {
  139. b.Buddies = append(b.Buddies, struct {
  140. ScreenName string `oscar:"len_prefix=uint8"`
  141. }{ScreenName: buddy.ScreenName})
  142. }
  143. return s.DelBuddies(ctx, instance, b)
  144. }
  145. // BroadcastBuddyArrived broadcasts buddy arrival with custom user info (implements DepartureNotifier)
  146. func (s BuddyService) BroadcastBuddyArrived(ctx context.Context, screenName state.IdentScreenName, userInfo wire.TLVUserInfo) error {
  147. return s.buddyBroadcaster.BroadcastBuddyArrived(ctx, screenName, userInfo)
  148. }
  149. func (s BuddyService) BroadcastBuddyDeparted(ctx context.Context, screenName state.IdentScreenName) error {
  150. return s.buddyBroadcaster.BroadcastBuddyDeparted(ctx, screenName)
  151. }
  152. func (s BuddyService) BroadcastVisibility(ctx context.Context, you *state.SessionInstance, filter []state.IdentScreenName, doSendDepartures bool) error {
  153. return s.buddyBroadcaster.BroadcastVisibility(ctx, you, filter, doSendDepartures)
  154. }
  155. func newBuddyNotifier(
  156. bartItemManager BARTItemManager,
  157. relationshipFetcher RelationshipFetcher,
  158. messageRelayer MessageRelayer,
  159. sessionRetriever SessionRetriever,
  160. ) buddyNotifier {
  161. return buddyNotifier{
  162. bartItemManager: bartItemManager,
  163. relationshipFetcher: relationshipFetcher,
  164. messageRelayer: messageRelayer,
  165. sessionRetriever: sessionRetriever,
  166. }
  167. }
  168. // buddyNotifier centralizes logic for sending buddy arrival and departure
  169. // notifications.
  170. type buddyNotifier struct {
  171. bartItemManager BARTItemManager
  172. relationshipFetcher RelationshipFetcher
  173. messageRelayer MessageRelayer
  174. sessionRetriever SessionRetriever
  175. }
  176. // BroadcastBuddyArrived sends the latest user info to the user's adjacent users.
  177. // While updates are sent via the wire.BuddyArrived SNAC, the message is not
  178. // only used to indicate the user coming online. It can also notify changes to
  179. // buddy icons, warning levels, invisibility status, etc.
  180. func (s buddyNotifier) BroadcastBuddyArrived(ctx context.Context, screenName state.IdentScreenName, userInfo wire.TLVUserInfo) error {
  181. if userInfo.IsInvisible() {
  182. return nil
  183. }
  184. users, err := s.relationshipFetcher.AllRelationships(ctx, screenName, nil)
  185. if err != nil {
  186. return err
  187. }
  188. var recipients []state.IdentScreenName
  189. for _, user := range users {
  190. if user.YouBlock || user.BlocksYou || !user.IsOnTheirList {
  191. continue
  192. }
  193. recipients = append(recipients, user.User)
  194. }
  195. s.messageRelayer.RelayToScreenNames(ctx, recipients, wire.SNACMessage{
  196. Frame: wire.SNACFrame{
  197. FoodGroup: wire.Buddy,
  198. SubGroup: wire.BuddyArrived,
  199. RequestID: wire.ReqIDFromServer,
  200. },
  201. Body: wire.SNAC_0x03_0x0B_BuddyArrived{
  202. TLVUserInfo: userInfo,
  203. },
  204. })
  205. return nil
  206. }
  207. func (s buddyNotifier) BroadcastBuddyDeparted(ctx context.Context, screenName state.IdentScreenName) error {
  208. users, err := s.relationshipFetcher.AllRelationships(ctx, screenName, nil)
  209. if err != nil {
  210. return err
  211. }
  212. var recipients []state.IdentScreenName
  213. for _, user := range users {
  214. if user.YouBlock || user.BlocksYou || !user.IsOnTheirList {
  215. continue
  216. }
  217. recipients = append(recipients, user.User)
  218. }
  219. s.messageRelayer.RelayToScreenNames(ctx, recipients, wire.SNACMessage{
  220. Frame: wire.SNACFrame{
  221. FoodGroup: wire.Buddy,
  222. SubGroup: wire.BuddyDeparted,
  223. RequestID: wire.ReqIDFromServer,
  224. },
  225. Body: wire.SNAC_0x03_0x0C_BuddyDeparted{
  226. TLVUserInfo: wire.TLVUserInfo{
  227. // don't include the TLV block, otherwise the AIM client fails
  228. // to process the block event
  229. ScreenName: screenName.String(),
  230. WarningLevel: 0,
  231. TLVBlock: wire.TLVBlock{
  232. TLVList: wire.TLVList{
  233. // this TLV needs to be set in order for departure
  234. // events to work in ICQ
  235. wire.NewTLVBE(wire.OServiceUserInfoUserFlags, uint16(0)),
  236. },
  237. },
  238. },
  239. },
  240. })
  241. return nil
  242. }
  243. // BroadcastVisibility sends you and related users arrival/departure
  244. // notifications that reflect your buddy list and privacy preferences.
  245. //
  246. // Behavior:
  247. // - Sends you arrival notifications for users on your buddy list that I do
  248. // not block.
  249. // - Sends arrival notifications to users that you block who have you on
  250. // their buddy lists.
  251. // - Sends you departure notifications for users on your buddy list that you
  252. // block (if doSendDepartures is true).
  253. // - Sends departure notifications to users that you block who have you on
  254. // their buddy lists (if doSendDepartures is true).
  255. // - Don't send notifications for any user that blocks you.
  256. //
  257. // This method is called when your visibility settings change, ensuring that
  258. // all relevant users are notified of your arrival or departure status.
  259. func (s buddyNotifier) BroadcastVisibility(
  260. ctx context.Context,
  261. you *state.SessionInstance,
  262. filter []state.IdentScreenName,
  263. doSendDepartures bool,
  264. ) error {
  265. relationships, err := s.relationshipFetcher.AllRelationships(ctx, you.IdentScreenName(), filter)
  266. if err != nil {
  267. return fmt.Errorf("retrieving relationships: %w", err)
  268. }
  269. yourTLVInfo := you.Session().TLVUserInfo()
  270. for _, relationship := range relationships {
  271. if relationship.BlocksYou {
  272. continue // they block you, don't send them notifications
  273. }
  274. theirSess := s.sessionRetriever.RetrieveSession(relationship.User)
  275. if theirSess == nil {
  276. continue // they are offline
  277. }
  278. if !relationship.YouBlock {
  279. if relationship.IsOnTheirList {
  280. // tell them you're online
  281. s.unicastBuddyArrived(ctx, yourTLVInfo, theirSess.IdentScreenName())
  282. }
  283. if relationship.IsOnYourList {
  284. theirInfo := theirSess.TLVUserInfo()
  285. // tell you they're online
  286. s.unicastBuddyArrived(ctx, theirInfo, you.IdentScreenName())
  287. }
  288. } else if relationship.YouBlock && doSendDepartures {
  289. if relationship.IsOnTheirList {
  290. // tell them you're offline
  291. s.unicastBuddyDeparted(ctx, you.Session(), theirSess.IdentScreenName())
  292. }
  293. if relationship.IsOnYourList {
  294. // tell you they're offline
  295. s.unicastBuddyDeparted(ctx, theirSess, you.IdentScreenName())
  296. }
  297. }
  298. }
  299. return nil
  300. }
  301. func (s buddyNotifier) unicastBuddyDeparted(ctx context.Context, from *state.Session, to state.IdentScreenName) {
  302. s.messageRelayer.RelayToScreenName(ctx, to, wire.SNACMessage{
  303. Frame: wire.SNACFrame{
  304. FoodGroup: wire.Buddy,
  305. SubGroup: wire.BuddyDeparted,
  306. RequestID: wire.ReqIDFromServer,
  307. },
  308. Body: wire.SNAC_0x03_0x0C_BuddyDeparted{
  309. TLVUserInfo: wire.TLVUserInfo{
  310. // don't include the TLV block, otherwise the AIM client fails
  311. // to process the block event
  312. ScreenName: from.IdentScreenName().String(),
  313. WarningLevel: from.Warning(),
  314. },
  315. },
  316. })
  317. }
  318. // unicastBuddyArrived sends the latest user info to a particular user.
  319. // While updates are sent via the wire.BuddyArrived SNAC, the message is not
  320. // only used to indicate the user coming online. It can also notify changes to
  321. // buddy icons, warning levels, invisibility status, etc.
  322. func (s buddyNotifier) unicastBuddyArrived(ctx context.Context, userInfo wire.TLVUserInfo, to state.IdentScreenName) {
  323. if userInfo.IsInvisible() {
  324. return
  325. }
  326. s.messageRelayer.RelayToScreenName(ctx, to, wire.SNACMessage{
  327. Frame: wire.SNACFrame{
  328. FoodGroup: wire.Buddy,
  329. SubGroup: wire.BuddyArrived,
  330. RequestID: wire.ReqIDFromServer,
  331. },
  332. Body: wire.SNAC_0x03_0x0B_BuddyArrived{
  333. TLVUserInfo: userInfo,
  334. },
  335. })
  336. }