buddylist.go 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. package handlers
  2. import (
  3. "context"
  4. "log/slog"
  5. "net/http"
  6. "strings"
  7. "github.com/mk6i/retro-aim-server/server/webapi/types"
  8. "github.com/mk6i/retro-aim-server/state"
  9. "github.com/mk6i/retro-aim-server/wire"
  10. )
  11. // BuddyListHandler handles Web AIM API buddy list management endpoints.
  12. type BuddyListHandler struct {
  13. SessionManager *state.WebAPISessionManager
  14. FeedbagManager FeedbagManager
  15. Logger *slog.Logger
  16. }
  17. // FeedbagManager provides methods to manage buddy lists.
  18. type FeedbagManager interface {
  19. RetrieveFeedbag(ctx context.Context, screenName state.IdentScreenName) ([]wire.FeedbagItem, error)
  20. InsertItem(ctx context.Context, screenName state.IdentScreenName, item wire.FeedbagItem) error
  21. UpdateItem(ctx context.Context, screenName state.IdentScreenName, item wire.FeedbagItem) error
  22. DeleteItem(ctx context.Context, screenName state.IdentScreenName, item wire.FeedbagItem) error
  23. }
  24. // AddBuddy handles GET /buddylist/addBuddy requests.
  25. func (h *BuddyListHandler) AddBuddy(w http.ResponseWriter, r *http.Request) {
  26. ctx := r.Context()
  27. // Get session ID from parameters
  28. aimsid := r.URL.Query().Get("aimsid")
  29. if aimsid == "" {
  30. h.sendError(w, http.StatusBadRequest, "missing aimsid parameter")
  31. return
  32. }
  33. // Get session
  34. session, err := h.SessionManager.GetSession(r.Context(), aimsid)
  35. if err != nil {
  36. if err == state.ErrNoWebAPISession {
  37. h.sendError(w, http.StatusNotFound, "session not found")
  38. } else if err == state.ErrWebAPISessionExpired {
  39. h.sendError(w, http.StatusGone, "session expired")
  40. } else {
  41. h.sendError(w, http.StatusInternalServerError, "internal server error")
  42. }
  43. return
  44. }
  45. // Touch the session
  46. h.SessionManager.TouchSession(r.Context(), aimsid)
  47. // Get buddy and group parameters
  48. buddyName := strings.TrimSpace(r.URL.Query().Get("buddy"))
  49. groupName := strings.TrimSpace(r.URL.Query().Get("group"))
  50. if buddyName == "" {
  51. h.sendError(w, http.StatusBadRequest, "missing buddy parameter")
  52. return
  53. }
  54. if groupName == "" {
  55. groupName = "Buddies" // Default group
  56. }
  57. // Add buddy to feedbag
  58. resultCode, buddyInfo := h.addBuddyToFeedbag(ctx, session.ScreenName.IdentScreenName(), buddyName, groupName)
  59. // Prepare response
  60. responseData := map[string]interface{}{
  61. "resultCode": resultCode,
  62. }
  63. if resultCode == "success" {
  64. responseData["buddyInfo"] = buddyInfo
  65. }
  66. resp := BaseResponse{}
  67. resp.Response.StatusCode = 200
  68. resp.Response.StatusText = "OK"
  69. resp.Response.Data = responseData
  70. SendResponse(w, r, resp, h.Logger)
  71. if resultCode == "success" && session.EventQueue != nil {
  72. // Push buddy list update event to the session's event queue
  73. event := types.BuddyListEvent{
  74. Action: "add",
  75. Buddy: buddyInfo,
  76. Group: groupName,
  77. }
  78. session.EventQueue.Push(types.EventTypeBuddyList, event)
  79. }
  80. h.Logger.InfoContext(ctx, "buddy added",
  81. "aimsid", aimsid,
  82. "buddy", buddyName,
  83. "group", groupName,
  84. "result", resultCode,
  85. )
  86. }
  87. // addBuddyToFeedbag adds a buddy to the user's feedbag.
  88. func (h *BuddyListHandler) addBuddyToFeedbag(ctx context.Context, screenName state.IdentScreenName, buddyName, groupName string) (string, *BuddyPresenceInfo) {
  89. // Retrieve current feedbag
  90. items, err := h.FeedbagManager.RetrieveFeedbag(ctx, screenName)
  91. if err != nil {
  92. h.Logger.ErrorContext(ctx, "failed to retrieve feedbag", "err", err.Error())
  93. return "error", nil
  94. }
  95. // Check if buddy already exists
  96. for _, item := range items {
  97. if item.ClassID == wire.FeedbagClassIdBuddy && item.Name == buddyName {
  98. // Buddy already exists
  99. return "alreadyExists", nil
  100. }
  101. }
  102. // Find or create the group
  103. var groupID uint16
  104. groupFound := false
  105. maxGroupID := uint16(0)
  106. for _, item := range items {
  107. if item.ClassID == wire.FeedbagClassIdGroup {
  108. if item.ItemID > maxGroupID {
  109. maxGroupID = item.ItemID
  110. }
  111. // Check group name
  112. if item.Name == groupName {
  113. groupID = item.ItemID
  114. groupFound = true
  115. }
  116. }
  117. }
  118. // If group doesn't exist, create it
  119. if !groupFound {
  120. groupID = maxGroupID + 1
  121. groupItem := wire.FeedbagItem{
  122. ItemID: groupID,
  123. ClassID: wire.FeedbagClassIdGroup,
  124. Name: groupName,
  125. GroupID: 0,
  126. TLVLBlock: wire.TLVLBlock{},
  127. }
  128. if err := h.FeedbagManager.InsertItem(ctx, screenName, groupItem); err != nil {
  129. h.Logger.ErrorContext(ctx, "failed to create group", "err", err.Error())
  130. return "error", nil
  131. }
  132. }
  133. // Find next available item ID for buddy
  134. maxBuddyID := uint16(0)
  135. for _, item := range items {
  136. if item.ClassID == wire.FeedbagClassIdBuddy && item.ItemID > maxBuddyID {
  137. maxBuddyID = item.ItemID
  138. }
  139. }
  140. // Create buddy item
  141. buddyItem := wire.FeedbagItem{
  142. ItemID: maxBuddyID + 1,
  143. ClassID: wire.FeedbagClassIdBuddy,
  144. Name: buddyName,
  145. GroupID: groupID,
  146. TLVLBlock: wire.TLVLBlock{},
  147. }
  148. // Insert buddy into feedbag
  149. if err := h.FeedbagManager.InsertItem(ctx, screenName, buddyItem); err != nil {
  150. h.Logger.ErrorContext(ctx, "failed to add buddy", "err", err.Error())
  151. return "error", nil
  152. }
  153. // Get current presence for the buddy
  154. buddyInfo := &BuddyPresenceInfo{
  155. AimID: buddyName,
  156. State: "offline", // Default to offline
  157. UserType: "aim",
  158. }
  159. // TODO: Check actual presence status and update buddyInfo accordingly
  160. return "success", buddyInfo
  161. }
  162. // sendError is a convenience method that wraps the common SendError function.
  163. func (h *BuddyListHandler) sendError(w http.ResponseWriter, statusCode int, message string) {
  164. SendError(w, statusCode, message)
  165. }