odir.go 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. package foodgroup
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "log/slog"
  7. "github.com/mk6i/open-oscar-server/state"
  8. "github.com/mk6i/open-oscar-server/wire"
  9. )
  10. // NewODirService creates a new instance of ODirService.
  11. func NewODirService(logger *slog.Logger, profileManager ProfileManager) ODirService {
  12. return ODirService{
  13. logger: logger,
  14. profileManager: profileManager,
  15. }
  16. }
  17. // ODirService provides functionality for the ODir food group, which
  18. // provides functionality for searching the user directory.
  19. type ODirService struct {
  20. logger *slog.Logger
  21. profileManager ProfileManager
  22. }
  23. // InfoQuery searches the user directory based on the query type: name/address,
  24. // email, or interest. It dispatches the request to the appropriate search
  25. // method and returns the search results or an error. The search type is
  26. // determined by the presence of certain TLVs:
  27. //
  28. // - wire.ODirTLVEmailAddress: Search by email.
  29. // - wire.ODirTLVInterest: Search by interest keyword.
  30. // - wire.ODirTLVFirstName or wire.ODirTLVLastName: Search by name and address.
  31. // First name or last name must be required to search by name and address.
  32. //
  33. // AIM 5.x sends wire.ODirTLVSearchType to specify the search type. This TLV is
  34. // ignored in order to be backwards compatible with older versions that do not
  35. // send it. It doesn't appear to make a difference, since AIM 5.x sends the
  36. // same TLV types for each search type.
  37. func (s ODirService) InfoQuery(ctx context.Context, inFrame wire.SNACFrame, inBody wire.SNAC_0x0F_0x02_InfoQuery) (wire.SNACMessage, error) {
  38. response := wire.SNACMessage{
  39. Frame: wire.SNACFrame{
  40. FoodGroup: wire.ODir,
  41. SubGroup: wire.ODirInfoReply,
  42. RequestID: inFrame.RequestID,
  43. },
  44. }
  45. // search by email address
  46. if email, hasEmail := inBody.String(wire.ODirTLVEmailAddress); hasEmail {
  47. foundUser, err := s.profileManager.FindByAIMEmail(ctx, email)
  48. if err != nil {
  49. if errors.Is(err, state.ErrNoUser) {
  50. response.Body = s.searchResponse(nil)
  51. return response, nil
  52. }
  53. return wire.SNACMessage{}, fmt.Errorf("FindByAIMEmail: %w", err)
  54. }
  55. response.Body = s.searchResponse([]state.User{foundUser})
  56. return response, nil
  57. }
  58. // search by interest keyword
  59. if interest, hasInterest := inBody.String(wire.ODirTLVInterest); hasInterest {
  60. foundUsers, err := s.profileManager.FindByAIMKeyword(ctx, interest)
  61. if err != nil {
  62. return wire.SNACMessage{}, fmt.Errorf("FindByAIMKeyword: %w", err)
  63. }
  64. response.Body = s.searchResponse(foundUsers)
  65. return response, nil
  66. }
  67. // search by name and address
  68. if inBody.HasTag(wire.ODirTLVFirstName) || inBody.HasTag(wire.ODirTLVLastName) {
  69. foundUsers, err := s.profileManager.FindByAIMNameAndAddr(ctx, newAIMNameAndAddrFromTLVList(inBody.TLVList))
  70. if err != nil {
  71. return wire.SNACMessage{}, fmt.Errorf("FindByAIMNameAndAddr: %w", err)
  72. }
  73. response.Body = s.searchResponse(foundUsers)
  74. return response, nil
  75. }
  76. // no suitable combination of search TLVs found
  77. response.Body = wire.SNAC_0x0F_0x03_InfoReply{
  78. Status: wire.ODirSearchResponseNameMissing,
  79. }
  80. return response, nil
  81. }
  82. // KeywordListQuery returns a list of keywords that can be searched in the user
  83. // directory.
  84. func (s ODirService) KeywordListQuery(ctx context.Context, inFrame wire.SNACFrame) (wire.SNACMessage, error) {
  85. interests, err := s.profileManager.InterestList(ctx)
  86. if err != nil {
  87. return wire.SNACMessage{}, fmt.Errorf("InterestList: %w", err)
  88. }
  89. return wire.SNACMessage{
  90. Frame: wire.SNACFrame{
  91. FoodGroup: wire.ODir,
  92. SubGroup: wire.ODirKeywordListReply,
  93. RequestID: inFrame.RequestID,
  94. },
  95. Body: wire.SNAC_0x0F_0x04_KeywordListReply{
  96. Status: 0x01,
  97. Interests: interests,
  98. },
  99. }, nil
  100. }
  101. // searchResponse constructs the SNAC reply based on the users found during the
  102. // search.
  103. func (s ODirService) searchResponse(foundUsers []state.User) wire.SNAC_0x0F_0x03_InfoReply {
  104. body := wire.SNAC_0x0F_0x03_InfoReply{
  105. Status: wire.ODirSearchResponseOK,
  106. }
  107. for _, res := range foundUsers {
  108. body.Results.List = append(body.Results.List, wire.TLVBlock{
  109. TLVList: wire.TLVList{
  110. wire.NewTLVBE(wire.ODirTLVFirstName, res.AIMDirectoryInfo.FirstName),
  111. wire.NewTLVBE(wire.ODirTLVLastName, res.AIMDirectoryInfo.LastName),
  112. wire.NewTLVBE(wire.ODirTLVState, res.AIMDirectoryInfo.State),
  113. wire.NewTLVBE(wire.ODirTLVCity, res.AIMDirectoryInfo.City),
  114. wire.NewTLVBE(wire.ODirTLVCountry, res.AIMDirectoryInfo.Country),
  115. wire.NewTLVBE(wire.ODirTLVScreenName, res.DisplayScreenName.String()),
  116. },
  117. })
  118. }
  119. return body
  120. }
  121. // newAIMNameAndAddrFromTLVList constructs an AIMNameAndAddr structure from the
  122. // TLV list containing user directory fields like first name, last name, etc.
  123. func newAIMNameAndAddrFromTLVList(tlvList wire.TLVList) state.AIMNameAndAddr {
  124. a := state.AIMNameAndAddr{}
  125. if firstName, hasFirstName := tlvList.String(wire.ODirTLVFirstName); hasFirstName {
  126. a.FirstName = firstName
  127. }
  128. if lastName, hasLastName := tlvList.String(wire.ODirTLVLastName); hasLastName {
  129. a.LastName = lastName
  130. }
  131. if middleName, hasMiddleName := tlvList.String(wire.ODirTLVMiddleName); hasMiddleName {
  132. a.MiddleName = middleName
  133. }
  134. if maidenName, hasMaidenName := tlvList.String(wire.ODirTLVMaidenName); hasMaidenName {
  135. a.MaidenName = maidenName
  136. }
  137. if country, hasCountry := tlvList.String(wire.ODirTLVCountry); hasCountry {
  138. a.Country = country
  139. }
  140. if st, hasState := tlvList.String(wire.ODirTLVState); hasState {
  141. a.State = st
  142. }
  143. if city, hasCity := tlvList.String(wire.ODirTLVCity); hasCity {
  144. a.City = city
  145. }
  146. if nickName, hasNickName := tlvList.String(wire.ODirTLVNickName); hasNickName {
  147. a.NickName = nickName
  148. }
  149. if zipCode, hasZIPCode := tlvList.String(wire.ODirTLVZIP); hasZIPCode {
  150. a.ZIPCode = zipCode
  151. }
  152. if address, hasAddress := tlvList.String(wire.ODirTLVAddress); hasAddress {
  153. a.Address = address
  154. }
  155. return a
  156. }