bart.go 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. package foodgroup
  2. import (
  3. "bytes"
  4. "context"
  5. "crypto/md5"
  6. "errors"
  7. "fmt"
  8. "log/slog"
  9. "github.com/mk6i/open-oscar-server/state"
  10. "github.com/mk6i/open-oscar-server/wire"
  11. )
  12. // blankGIF is a blank, transparent 50x50p GIF that takes the place of a
  13. // cleared buddy icon.
  14. var blankGIF = []byte{
  15. 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x32, 0x00, 0x32, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
  16. 0x00, 0x00, 0x00, 0x21, 0xf9, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x00,
  17. 0x32, 0x00, 0x32, 0x00, 0x00, 0x02, 0x02, 0x44, 0x01, 0x00, 0x3b,
  18. }
  19. func NewBARTService(
  20. logger *slog.Logger,
  21. bartItemManager BARTItemManager,
  22. messageRelayer MessageRelayer,
  23. relationshipFetcher RelationshipFetcher,
  24. sessionRetriever SessionRetriever,
  25. ) BARTService {
  26. return BARTService{
  27. bartItemManager: bartItemManager,
  28. buddyUpdateBroadcaster: newBuddyNotifier(bartItemManager, relationshipFetcher, messageRelayer, sessionRetriever),
  29. messageRelayer: messageRelayer,
  30. logger: logger,
  31. }
  32. }
  33. type BARTService struct {
  34. bartItemManager BARTItemManager
  35. buddyUpdateBroadcaster buddyBroadcaster
  36. messageRelayer MessageRelayer
  37. logger *slog.Logger
  38. }
  39. func (s BARTService) UpsertItem(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x10_0x02_BARTUploadQuery) (wire.SNACMessage, error) {
  40. h := md5.New()
  41. if _, err := h.Write(inBody.Data); err != nil {
  42. return wire.SNACMessage{}, err
  43. }
  44. hash := h.Sum(nil)
  45. if err := s.bartItemManager.InsertBARTItem(ctx, hash, inBody.Data, inBody.Type); err != nil {
  46. if !errors.Is(err, state.ErrBARTItemExists) {
  47. return wire.SNACMessage{}, fmt.Errorf("failed to insert BART item: %w", err)
  48. }
  49. }
  50. s.logger.DebugContext(ctx, "successfully uploaded BART item", "hash", fmt.Sprintf("%x", hash))
  51. bartID, hasIcon := instance.Session().BuddyIcon()
  52. if hasIcon && bytes.Equal(hash, bartID.Hash) {
  53. // unset unknown flag
  54. bartID.Flags ^= wire.BARTFlagsUnknown
  55. instance.Session().SetBuddyIcon(bartID)
  56. s.messageRelayer.RelayToScreenName(ctx, instance.IdentScreenName(), wire.SNACMessage{
  57. Frame: wire.SNACFrame{
  58. FoodGroup: wire.OService,
  59. SubGroup: wire.OServiceUserInfoUpdate,
  60. },
  61. Body: newOServiceUserInfoUpdate(instance),
  62. })
  63. if err := s.buddyUpdateBroadcaster.BroadcastBuddyArrived(ctx, instance.IdentScreenName(), instance.Session().TLVUserInfo()); err != nil {
  64. return wire.SNACMessage{}, err
  65. }
  66. } else {
  67. bartID = wire.BARTID{
  68. Type: inBody.Type,
  69. BARTInfo: wire.BARTInfo{
  70. Flags: wire.BARTFlagsCustom,
  71. Hash: hash,
  72. },
  73. }
  74. }
  75. return wire.SNACMessage{
  76. Frame: wire.SNACFrame{
  77. FoodGroup: wire.BART,
  78. SubGroup: wire.BARTUploadReply,
  79. RequestID: inFrame.RequestID,
  80. },
  81. Body: wire.SNAC_0x10_0x03_BARTUploadReply{
  82. Code: wire.BARTReplyCodesSuccess,
  83. ID: bartID,
  84. },
  85. }, nil
  86. }
  87. // RetrieveItem fetches a BART item from the data store. The item is selected
  88. // based on inBody.Hash. It's unclear what effect inBody.Flags is supposed to
  89. // have on the request.
  90. func (s BARTService) RetrieveItem(ctx context.Context, inFrame wire.SNACFrame, inBody wire.SNAC_0x10_0x04_BARTDownloadQuery) (wire.SNACMessage, error) {
  91. var item []byte
  92. if inBody.HasClearIconHash() {
  93. item = blankGIF
  94. } else {
  95. var err error
  96. if item, err = s.bartItemManager.BARTItem(ctx, inBody.Hash); err != nil {
  97. return wire.SNACMessage{}, err
  98. }
  99. }
  100. // todo... how to reply if requested item doesn't exist
  101. return wire.SNACMessage{
  102. Frame: wire.SNACFrame{
  103. FoodGroup: wire.BART,
  104. SubGroup: wire.BARTDownloadReply,
  105. RequestID: inFrame.RequestID,
  106. },
  107. Body: wire.SNAC_0x10_0x05_BARTDownloadReply{
  108. ScreenName: inBody.ScreenName,
  109. BARTID: inBody.BARTID,
  110. Data: item,
  111. },
  112. }, nil
  113. }
  114. // RetrieveItemV2 implements the SNAC handler for SNAC(0x10,0x06) BARTDownload2Query.
  115. func (s BARTService) RetrieveItemV2(ctx context.Context, inFrame wire.SNACFrame, inBody wire.SNAC_0x10_0x06_BARTDownload2Query) ([]wire.SNACMessage, error) {
  116. var result []wire.SNACMessage
  117. for _, id := range inBody.IDs {
  118. var item []byte
  119. if id.Type == wire.BARTTypesBuddyIcon && id.HasClearIconHash() {
  120. item = blankGIF
  121. } else {
  122. var err error
  123. if item, err = s.bartItemManager.BARTItem(ctx, id.Hash); err != nil {
  124. return nil, err
  125. }
  126. }
  127. result = append(result, wire.SNACMessage{
  128. Frame: wire.SNACFrame{
  129. FoodGroup: wire.BART,
  130. SubGroup: wire.BARTDownload2Reply,
  131. RequestID: inFrame.RequestID,
  132. },
  133. Body: wire.SNAC_0x10_0x07_BARTDownload2Reply{
  134. ScreenName: inBody.ScreenName,
  135. ReplyID: wire.BartQueryReplyID{
  136. QueryID: id,
  137. Code: 0x00, // found
  138. ReplyID: wire.BARTID{
  139. Type: id.Type,
  140. BARTInfo: id.BARTInfo,
  141. },
  142. },
  143. Data: item,
  144. },
  145. })
  146. }
  147. return result, nil
  148. }