chat_nav.go 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  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. var defaultExchangeCfg = wire.TLVBlock{
  11. TLVList: wire.TLVList{
  12. wire.NewTLVBE(wire.ChatRoomTLVMaxConcurrentRooms, uint8(10)),
  13. wire.NewTLVBE(wire.ChatRoomTLVClassPerms, uint16(0x0010)),
  14. wire.NewTLVBE(wire.ChatRoomTLVMaxNameLen, uint16(100)),
  15. wire.NewTLVBE(wire.ChatRoomTLVFlags, uint16(15)),
  16. wire.NewTLVBE(wire.ChatRoomTLVNavCreatePerms, uint8(2)),
  17. wire.NewTLVBE(wire.ChatRoomTLVCharSet1, "us-ascii"),
  18. wire.NewTLVBE(wire.ChatRoomTLVLang1, "en"),
  19. wire.NewTLVBE(wire.ChatRoomTLVCharSet2, "us-ascii"),
  20. wire.NewTLVBE(wire.ChatRoomTLVLang2, "en"),
  21. },
  22. }
  23. var (
  24. errChatNavRoomNameMissing = errors.New("unable to find chat name in TLV payload")
  25. errChatNavRoomCreateFailed = errors.New("unable to create chat room")
  26. errChatNavRetrieveFailed = errors.New("unable to retrieve chat room chat room")
  27. errChatNavMismatchedExchange = errors.New("chat room exchange does not match requested exchange")
  28. )
  29. // NewChatNavService creates a new instance of NewChatNavService.
  30. func NewChatNavService(logger *slog.Logger, chatRoomManager ChatRoomRegistry) *ChatNavService {
  31. return &ChatNavService{
  32. logger: logger,
  33. chatRoomManager: chatRoomManager,
  34. }
  35. }
  36. // ChatNavService provides functionality for the ChatNav food group, which
  37. // handles chat room creation and serving chat room metadata.
  38. type ChatNavService struct {
  39. logger *slog.Logger
  40. chatRoomManager ChatRoomRegistry
  41. }
  42. // RequestChatRights returns SNAC wire.ChatNavNavInfo, which contains chat
  43. // navigation service parameters and limits.
  44. func (s ChatNavService) RequestChatRights(_ context.Context, inFrame wire.SNACFrame) wire.SNACMessage {
  45. return wire.SNACMessage{
  46. Frame: wire.SNACFrame{
  47. FoodGroup: wire.ChatNav,
  48. SubGroup: wire.ChatNavNavInfo,
  49. RequestID: inFrame.RequestID,
  50. },
  51. Body: wire.SNAC_0x0D_0x09_ChatNavNavInfo{
  52. TLVRestBlock: wire.TLVRestBlock{
  53. TLVList: wire.TLVList{
  54. wire.NewTLVBE(wire.ChatNavTLVMaxConcurrentRooms, uint8(10)),
  55. wire.NewTLVBE(wire.ChatNavTLVExchangeInfo, wire.SNAC_0x0D_0x09_TLVExchangeInfo{
  56. Identifier: state.PrivateExchange,
  57. TLVBlock: defaultExchangeCfg,
  58. }),
  59. wire.NewTLVBE(wire.ChatNavTLVExchangeInfo, wire.SNAC_0x0D_0x09_TLVExchangeInfo{
  60. Identifier: state.PublicExchange,
  61. TLVBlock: defaultExchangeCfg,
  62. }),
  63. },
  64. },
  65. },
  66. }
  67. }
  68. // CreateRoom creates and returns a chat room or returns an existing chat
  69. // room. It returns SNAC wire.ChatNavNavInfo, which contains metadata for the
  70. // chat room.
  71. func (s ChatNavService) CreateRoom(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate) (wire.SNACMessage, error) {
  72. if err := validateExchange(inBody.Exchange); err != nil {
  73. s.logger.Debug("error validating exchange: " + err.Error())
  74. return sendChatNavErrorSNAC(inFrame, wire.ErrorCodeNotSupportedByHost)
  75. }
  76. if inBody.Cookie != "create" {
  77. s.logger.Info("got a non-create cookie", "value", inBody.Cookie)
  78. }
  79. name, hasName := inBody.String(wire.ChatRoomTLVRoomName)
  80. if !hasName {
  81. return wire.SNACMessage{}, errChatNavRoomNameMissing
  82. }
  83. // todo call ChatRoomByName and CreateChatRoom in a txn
  84. room, err := s.chatRoomManager.ChatRoomByName(ctx, inBody.Exchange, name)
  85. switch {
  86. case errors.Is(err, state.ErrChatRoomNotFound):
  87. if inBody.Exchange == state.PublicExchange {
  88. s.logger.Debug(fmt.Sprintf("public chat room not found: %s:%d", name, inBody.Exchange))
  89. return sendChatNavErrorSNAC(inFrame, wire.ErrorCodeNoMatch)
  90. }
  91. room = state.NewChatRoom(name, instance.IdentScreenName(), inBody.Exchange)
  92. if err := s.chatRoomManager.CreateChatRoom(ctx, &room); err != nil {
  93. return wire.SNACMessage{}, fmt.Errorf("%w: %w", errChatNavRoomCreateFailed, err)
  94. }
  95. case err != nil:
  96. return wire.SNACMessage{}, fmt.Errorf("%w: %w", errChatNavRetrieveFailed, err)
  97. }
  98. return wire.SNACMessage{
  99. Frame: wire.SNACFrame{
  100. FoodGroup: wire.ChatNav,
  101. SubGroup: wire.ChatNavNavInfo,
  102. RequestID: inFrame.RequestID,
  103. },
  104. Body: wire.SNAC_0x0D_0x09_ChatNavNavInfo{
  105. TLVRestBlock: wire.TLVRestBlock{
  106. TLVList: wire.TLVList{
  107. wire.NewTLVBE(wire.ChatNavTLVRoomInfo, wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
  108. Cookie: room.Cookie(),
  109. Exchange: room.Exchange(),
  110. DetailLevel: room.DetailLevel(),
  111. InstanceNumber: room.InstanceNumber(),
  112. TLVBlock: wire.TLVBlock{
  113. TLVList: room.TLVList(),
  114. },
  115. }),
  116. },
  117. },
  118. },
  119. }, nil
  120. }
  121. // RequestRoomInfo returns wire.ChatNavNavInfo, which contains metadata for
  122. // the chat room specified in the inFrame.hmacCookie.
  123. func (s ChatNavService) RequestRoomInfo(ctx context.Context, inFrame wire.SNACFrame, inBody wire.SNAC_0x0D_0x04_ChatNavRequestRoomInfo) (wire.SNACMessage, error) {
  124. if err := validateExchange(inBody.Exchange); err != nil {
  125. s.logger.Debug("error validating exchange: " + err.Error())
  126. return sendChatNavErrorSNAC(inFrame, wire.ErrorCodeNotSupportedByHost)
  127. }
  128. room, err := s.chatRoomManager.ChatRoomByCookie(ctx, inBody.Cookie)
  129. if err != nil {
  130. return wire.SNACMessage{}, fmt.Errorf("%w: %w", state.ErrChatRoomNotFound, err)
  131. }
  132. if room.Exchange() != inBody.Exchange {
  133. return wire.SNACMessage{}, errChatNavMismatchedExchange
  134. }
  135. return wire.SNACMessage{
  136. Frame: wire.SNACFrame{
  137. FoodGroup: wire.ChatNav,
  138. SubGroup: wire.ChatNavNavInfo,
  139. RequestID: inFrame.RequestID,
  140. },
  141. Body: wire.SNAC_0x0D_0x09_ChatNavNavInfo{
  142. TLVRestBlock: wire.TLVRestBlock{
  143. TLVList: wire.TLVList{
  144. wire.NewTLVBE(wire.ChatNavTLVRoomInfo, wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
  145. Cookie: room.Cookie(),
  146. Exchange: room.Exchange(),
  147. DetailLevel: room.DetailLevel(),
  148. InstanceNumber: room.InstanceNumber(),
  149. TLVBlock: wire.TLVBlock{
  150. TLVList: room.TLVList(),
  151. },
  152. }),
  153. },
  154. },
  155. },
  156. }, nil
  157. }
  158. func (s ChatNavService) ExchangeInfo(_ context.Context, inFrame wire.SNACFrame, inBody wire.SNAC_0x0D_0x03_ChatNavRequestExchangeInfo) (wire.SNACMessage, error) {
  159. if err := validateExchange(inBody.Exchange); err != nil {
  160. s.logger.Debug("error validating exchange: " + err.Error())
  161. return sendChatNavErrorSNAC(inFrame, wire.ErrorCodeNotSupportedByHost)
  162. }
  163. return wire.SNACMessage{
  164. Frame: wire.SNACFrame{
  165. FoodGroup: wire.ChatNav,
  166. SubGroup: wire.ChatNavNavInfo,
  167. RequestID: inFrame.RequestID,
  168. },
  169. Body: wire.SNAC_0x0D_0x09_ChatNavNavInfo{
  170. TLVRestBlock: wire.TLVRestBlock{
  171. TLVList: wire.TLVList{
  172. wire.NewTLVBE(wire.ChatNavTLVMaxConcurrentRooms, uint8(10)),
  173. wire.NewTLVBE(wire.ChatNavTLVExchangeInfo, wire.SNAC_0x0D_0x09_TLVExchangeInfo{
  174. Identifier: inBody.Exchange,
  175. TLVBlock: defaultExchangeCfg,
  176. }),
  177. },
  178. },
  179. },
  180. }, nil
  181. }
  182. // sendChatNavErrorSNAC returns a ChatNavErr SNAC and logs an error for the operator
  183. func sendChatNavErrorSNAC(inFrame wire.SNACFrame, errorCode uint16) (wire.SNACMessage, error) {
  184. return wire.SNACMessage{
  185. Frame: wire.SNACFrame{
  186. FoodGroup: wire.ChatNav,
  187. SubGroup: wire.ChatNavErr,
  188. RequestID: inFrame.RequestID,
  189. },
  190. Body: wire.SNACError{
  191. Code: errorCode,
  192. },
  193. }, nil
  194. }
  195. func validateExchange(exchange uint16) error {
  196. if exchange != state.PrivateExchange && exchange != state.PublicExchange {
  197. return fmt.Errorf("only exchanges %d and %d are supported", state.PrivateExchange, state.PublicExchange)
  198. }
  199. return nil
  200. }