adapters.go 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. package webapi
  2. import (
  3. "bytes"
  4. "context"
  5. "crypto/rand"
  6. "encoding/binary"
  7. "time"
  8. "github.com/mk6i/retro-aim-server/server/webapi/types"
  9. "github.com/mk6i/retro-aim-server/state"
  10. "github.com/mk6i/retro-aim-server/wire"
  11. )
  12. // FeedbagAdapter wraps SQLiteUserStore to implement FeedbagRetriever and FeedbagManager interfaces
  13. type FeedbagAdapter struct {
  14. Store *state.SQLiteUserStore
  15. }
  16. // RetrieveFeedbag implements FeedbagRetriever interface
  17. func (f *FeedbagAdapter) RetrieveFeedbag(ctx context.Context, screenName state.IdentScreenName) ([]wire.FeedbagItem, error) {
  18. return f.Store.Feedbag(ctx, screenName)
  19. }
  20. // RelationshipsByUser implements FeedbagRetriever interface
  21. // Returns the list of users who have this user in their buddy list
  22. func (f *FeedbagAdapter) RelationshipsByUser(ctx context.Context, screenName state.IdentScreenName) ([]state.IdentScreenName, error) {
  23. // Get all relationships where this user is involved
  24. relationships, err := f.Store.AllRelationships(ctx, screenName, nil)
  25. if err != nil {
  26. return nil, err
  27. }
  28. // Extract unique screen names from relationships
  29. uniqueUsers := make(map[state.IdentScreenName]bool)
  30. for _, rel := range relationships {
  31. // Add the user from the relationship
  32. uniqueUsers[rel.User] = true
  33. }
  34. // Convert map to slice
  35. users := make([]state.IdentScreenName, 0, len(uniqueUsers))
  36. for user := range uniqueUsers {
  37. users = append(users, user)
  38. }
  39. return users, nil
  40. }
  41. // InsertItem implements FeedbagManager interface
  42. func (f *FeedbagAdapter) InsertItem(ctx context.Context, screenName state.IdentScreenName, item wire.FeedbagItem) error {
  43. // Use FeedbagUpsert to insert a new item
  44. return f.Store.FeedbagUpsert(ctx, screenName, []wire.FeedbagItem{item})
  45. }
  46. // UpdateItem implements FeedbagManager interface
  47. func (f *FeedbagAdapter) UpdateItem(ctx context.Context, screenName state.IdentScreenName, item wire.FeedbagItem) error {
  48. // Use FeedbagUpsert to update an existing item
  49. return f.Store.FeedbagUpsert(ctx, screenName, []wire.FeedbagItem{item})
  50. }
  51. // DeleteItem implements FeedbagManager interface
  52. func (f *FeedbagAdapter) DeleteItem(ctx context.Context, screenName state.IdentScreenName, item wire.FeedbagItem) error {
  53. // Use FeedbagDelete to remove an item
  54. return f.Store.FeedbagDelete(ctx, screenName, []wire.FeedbagItem{item})
  55. }
  56. // Message Conversion Functions
  57. // WebAPIToICBM converts a Web API message to OSCAR ICBM format
  58. func WebAPIToICBM(sender state.IdentScreenName, recipient string, message string, autoResponse bool) (wire.SNAC_0x04_0x06_ICBMChannelMsgToHost, error) {
  59. // Generate message cookie
  60. var cookie [8]byte
  61. if _, err := rand.Read(cookie[:]); err != nil {
  62. return wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{}, err
  63. }
  64. cookieUint64 := binary.BigEndian.Uint64(cookie[:])
  65. // Create ICBM fragment list for the message
  66. frags, err := wire.ICBMFragmentList(message)
  67. if err != nil {
  68. return wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{}, err
  69. }
  70. // Marshal the fragments
  71. buf := &bytes.Buffer{}
  72. for _, frag := range frags {
  73. if err := wire.MarshalBE(frag, buf); err != nil {
  74. return wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{}, err
  75. }
  76. }
  77. // Build ICBM message
  78. icbmMsg := wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{
  79. Cookie: cookieUint64,
  80. ChannelID: wire.ICBMChannelIM,
  81. ScreenName: recipient,
  82. TLVRestBlock: wire.TLVRestBlock{
  83. TLVList: wire.TLVList{
  84. wire.NewTLVBE(wire.ICBMTLVAOLIMData, buf.Bytes()),
  85. },
  86. },
  87. }
  88. // Add auto-response flag if applicable
  89. if autoResponse {
  90. icbmMsg.Append(wire.NewTLVBE(wire.ICBMTLVAutoResponse, []byte{}))
  91. }
  92. return icbmMsg, nil
  93. }
  94. // ICBMToWebAPIEvent converts an incoming ICBM message to a WebAPI event
  95. func ICBMToWebAPIEvent(icbm wire.SNAC_0x04_0x07_ICBMChannelMsgToClient) (types.Event, error) {
  96. // Extract message text
  97. var messageText string
  98. var autoResponse bool
  99. // Check for AOL IM data
  100. if msgData, hasMsg := icbm.Bytes(wire.ICBMTLVAOLIMData); hasMsg {
  101. msgText, err := wire.UnmarshalICBMMessageText(msgData)
  102. if err == nil {
  103. messageText = msgText
  104. }
  105. }
  106. // Check for auto-response flag
  107. if _, hasAutoResp := icbm.Bytes(wire.ICBMTLVAutoResponse); hasAutoResp {
  108. autoResponse = true
  109. }
  110. // Extract sender screen name from TLVUserInfo
  111. senderScreenName := ""
  112. if icbm.TLVUserInfo.ScreenName != "" {
  113. senderScreenName = icbm.TLVUserInfo.ScreenName
  114. }
  115. // Create WebAPI event
  116. event := types.Event{
  117. Type: types.EventTypeIM,
  118. Timestamp: time.Now().Unix(),
  119. Data: types.IMEvent{
  120. From: senderScreenName,
  121. Message: messageText,
  122. Timestamp: float64(time.Now().Unix()),
  123. AutoResp: autoResponse,
  124. },
  125. }
  126. return event, nil
  127. }
  128. // TypingNotificationToWebAPIEvent converts an OSCAR typing notification to a WebAPI event
  129. func TypingNotificationToWebAPIEvent(notification wire.SNAC_0x04_0x14_ICBMClientEvent) types.Event {
  130. typing := false
  131. switch notification.Event {
  132. case 0x0002: // Typing started
  133. typing = true
  134. case 0x0001: // Typing stopped
  135. typing = false
  136. }
  137. return types.Event{
  138. Type: types.EventTypeTyping,
  139. Timestamp: time.Now().Unix(),
  140. Data: types.TypingEvent{
  141. From: notification.ScreenName,
  142. Typing: typing,
  143. },
  144. }
  145. }
  146. // PresenceUpdateToWebAPIEvent converts OSCAR buddy arrival/departure to WebAPI event
  147. func PresenceUpdateToWebAPIEvent(screenName string, online bool, awayMsg string, statusBitmask uint32) types.Event {
  148. stateStr := "offline"
  149. if online {
  150. stateStr = "online"
  151. if statusBitmask&wire.OServiceUserStatusAway != 0 {
  152. stateStr = "away"
  153. } else if statusBitmask&wire.OServiceUserStatusDND != 0 {
  154. stateStr = "dnd"
  155. } else if statusBitmask&wire.OServiceUserStatusInvisible != 0 {
  156. stateStr = "invisible"
  157. }
  158. }
  159. return types.Event{
  160. Type: types.EventTypePresence,
  161. Timestamp: time.Now().Unix(),
  162. Data: types.PresenceEvent{
  163. AimID: screenName,
  164. State: stateStr,
  165. AwayMsg: awayMsg,
  166. },
  167. }
  168. }