messaging.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. package handlers
  2. import (
  3. "context"
  4. "crypto/rand"
  5. "encoding/binary"
  6. "fmt"
  7. "log/slog"
  8. "net/http"
  9. "time"
  10. "github.com/mk6i/open-oscar-server/server/webapi/types"
  11. "github.com/mk6i/open-oscar-server/state"
  12. "github.com/mk6i/open-oscar-server/wire"
  13. )
  14. // ICBMService defines methods for ICBM operations
  15. type ICBMService interface {
  16. ChannelMsgToHost(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x04_0x06_ICBMChannelMsgToHost) (*wire.SNACMessage, error)
  17. ClientEvent(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x04_0x14_ICBMClientEvent) error
  18. }
  19. // MessagingHandler handles Web AIM API messaging endpoints
  20. type MessagingHandler struct {
  21. SessionManager *state.WebAPISessionManager
  22. ICBMService ICBMService
  23. Logger *slog.Logger
  24. }
  25. // queryOrFormParam returns a request parameter from the query string or, for POST
  26. // requests, from application/x-www-form-urlencoded body fields. The Web AIM client
  27. // sends t/offlineIM/etc. on the query string and puts message in the POST body.
  28. func queryOrFormParam(r *http.Request, key string) string {
  29. if v := r.URL.Query().Get(key); v != "" {
  30. return v
  31. }
  32. if r.Method == http.MethodPost {
  33. if err := r.ParseForm(); err == nil {
  34. return r.FormValue(key)
  35. }
  36. }
  37. return ""
  38. }
  39. // SendIM handles the /im/sendIM endpoint for sending instant messages
  40. func (h *MessagingHandler) SendIM(w http.ResponseWriter, r *http.Request) {
  41. ctx := r.Context()
  42. // Get session from aimsid
  43. aimsid := queryOrFormParam(r, "aimsid")
  44. if aimsid == "" {
  45. h.sendErrorResponse(w, http.StatusBadRequest, "missing required parameter: aimsid")
  46. return
  47. }
  48. sess, err := h.SessionManager.GetSession(r.Context(), aimsid)
  49. if err != nil {
  50. if err == state.ErrNoWebAPISession || err == state.ErrWebAPISessionExpired {
  51. h.sendErrorResponse(w, http.StatusUnauthorized, "invalid or expired session")
  52. } else {
  53. h.sendErrorResponse(w, http.StatusInternalServerError, "internal server error")
  54. }
  55. return
  56. }
  57. // Update session activity
  58. if err := h.SessionManager.TouchSession(r.Context(), aimsid); err != nil {
  59. h.Logger.WarnContext(ctx, "failed to touch session", "aimsid", aimsid, "error", err)
  60. }
  61. // Parse parameters
  62. recipient := queryOrFormParam(r, "t")
  63. if recipient == "" {
  64. h.sendErrorResponse(w, http.StatusBadRequest, "missing required parameter: t (recipient)")
  65. return
  66. }
  67. message := queryOrFormParam(r, "message")
  68. if message == "" {
  69. h.sendErrorResponse(w, http.StatusBadRequest, "missing required parameter: message")
  70. return
  71. }
  72. // Parse optional parameters
  73. autoResponse := queryOrFormParam(r, "autoResponse") == "1"
  74. //offlineIMParam := queryOrFormParam(r, "offlineIM") what is this for
  75. //offlineIM := offlineIMParam != "0" && offlineIMParam != "false" // default to true
  76. // Create recipient identifier
  77. recipientIdent := state.NewIdentScreenName(recipient)
  78. // Generate message cookie
  79. var cookie [8]byte
  80. if _, err := rand.Read(cookie[:]); err != nil {
  81. h.Logger.ErrorContext(ctx, "failed to generate message cookie", "error", err)
  82. h.sendErrorResponse(w, http.StatusInternalServerError, "internal server error")
  83. return
  84. }
  85. cookieUint64 := binary.BigEndian.Uint64(cookie[:])
  86. // Get sender's OSCAR session if available
  87. var senderInfo wire.TLVUserInfo
  88. if sess.OSCARSession != nil {
  89. senderInfo = sess.OSCARSession.Session().TLVUserInfo()
  90. } else {
  91. // Create minimal user info for web-only sessions
  92. senderInfo = wire.TLVUserInfo{
  93. ScreenName: sess.ScreenName.String(),
  94. WarningLevel: 0,
  95. }
  96. senderInfo.Append(wire.NewTLVBE(wire.OServiceUserInfoSignonTOD, uint32(sess.CreatedAt.Unix())))
  97. senderInfo.Append(wire.NewTLVBE(wire.OServiceUserInfoStatus, uint32(0x0000))) // online status
  98. }
  99. // Create message ID for response (UUID format like working implementation)
  100. // Using the cookie bytes to generate a UUID-like string
  101. messageID := fmt.Sprintf("%08x-%04x-%04x-%04x-%012x",
  102. binary.BigEndian.Uint32(cookie[:4]),
  103. binary.BigEndian.Uint16(cookie[4:6]),
  104. binary.BigEndian.Uint16(cookie[6:8]),
  105. binary.BigEndian.Uint16([]byte{0x80, 0x00}), // Version bits
  106. time.Now().UnixNano()&0xffffffffffff)
  107. now := float64(time.Now().Unix())
  108. nowSec := time.Now().Unix()
  109. sn := sess.ScreenName.String()
  110. sess.AddStoredIM(recipient, sn, message, messageID, nowSec)
  111. // Recipient is online, deliver message
  112. clientIM := wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{
  113. Cookie: cookieUint64,
  114. ChannelID: wire.ICBMChannelIM,
  115. ScreenName: recipient,
  116. TLVRestBlock: wire.TLVRestBlock{},
  117. }
  118. // Add message data
  119. frags, err := wire.ICBMFragmentList(message)
  120. if err != nil {
  121. h.sendErrorResponse(w, http.StatusInternalServerError, "failed to send message")
  122. return
  123. }
  124. clientIM.Append(wire.NewTLVBE(wire.ICBMTLVAOLIMData, frags))
  125. // Add auto-response flag if applicable
  126. if autoResponse {
  127. clientIM.Append(wire.NewTLVBE(wire.ICBMTLVAutoResponse, []byte{}))
  128. }
  129. frame := wire.SNACFrame{
  130. FoodGroup: wire.ICBM,
  131. SubGroup: wire.ICBMChannelMsgToHost,
  132. RequestID: wire.ReqIDFromServer,
  133. }
  134. resp, err := h.ICBMService.ChannelMsgToHost(r.Context(), sess.OSCARSession, frame, clientIM)
  135. if err != nil {
  136. h.sendErrorResponse(w, http.StatusInternalServerError, "failed to send message")
  137. return
  138. }
  139. if resp != nil {
  140. switch {
  141. case resp.Frame.FoodGroup == wire.ICBM && resp.Frame.SubGroup == wire.ICBMErr:
  142. if errSn, ok := resp.Body.(wire.SNACError); ok {
  143. switch errSn.Code {
  144. case wire.ErrorCodeNotLoggedOn:
  145. subCode, hasSubCode := errSn.Uint16BE(wire.ErrorTLVErrorSubcode)
  146. if hasSubCode {
  147. if subCode == wire.ICBMSubErrOfflineIMExceedMax {
  148. h.Logger.DebugContext(r.Context(), "user's offline messages full")
  149. }
  150. } else {
  151. h.Logger.DebugContext(r.Context(), "recipient offline")
  152. }
  153. return
  154. case wire.ErrorCodeInLocalPermitDeny:
  155. h.Logger.DebugContext(r.Context(), "you blocked this user")
  156. return
  157. }
  158. }
  159. case resp.Frame.FoodGroup == wire.ICBM && resp.Frame.SubGroup == wire.ICBMHostAck:
  160. h.Logger.DebugContext(r.Context(), "received host ack")
  161. }
  162. }
  163. // Queue IM event for the recipient's WebAPI session if they have one
  164. if recipientWebSession, err := h.SessionManager.GetSessionByUser(r.Context(), recipientIdent); err == nil && recipientWebSession != nil {
  165. recipientWebSession.AddStoredIM(sn, sn, message, messageID, nowSec)
  166. eventData := types.IMEvent{
  167. Source: types.UserInfo{
  168. AimID: sn,
  169. DisplayID: sn,
  170. UserType: "aim",
  171. State: "online",
  172. },
  173. Message: message,
  174. MsgID: messageID,
  175. Timestamp: now,
  176. AutoResp: autoResponse,
  177. }
  178. recipientWebSession.EventQueue.Push(types.EventTypeIM, eventData)
  179. if recipientWebSession.IsSubscribedTo("conversation") {
  180. recipientWebSession.EventQueue.Push(types.EventTypeConversation, types.ConversationEventData("update", []map[string]interface{}{
  181. types.ConversationEntry(sn, sn, message, messageID, sn, false, 1),
  182. }))
  183. }
  184. }
  185. h.pushSenderWebAPIEvents(sess, sn, recipient, message, messageID, now, autoResponse)
  186. h.Logger.DebugContext(ctx, "queued sentIM event for sender",
  187. "from", sess.ScreenName.String(),
  188. "to", recipient,
  189. "eventType", types.EventTypeSentIM,
  190. )
  191. h.Logger.DebugContext(ctx, "delivered instant message",
  192. "from", sess.ScreenName.String(),
  193. "to", recipient)
  194. // Send success response
  195. responseData := map[string]interface{}{
  196. "msgId": messageID,
  197. "state": "delivered",
  198. }
  199. response := BaseResponse{}
  200. response.Response.StatusCode = 200
  201. response.Response.StatusText = "OK"
  202. response.Response.Data = responseData
  203. SendResponse(w, r, response, h.Logger)
  204. }
  205. func (h *MessagingHandler) pushSenderWebAPIEvents(sess *state.WebAPISession, sender, recipient, message, messageID string, now float64, autoResponse bool) {
  206. senderEventData := types.SentIMEvent{
  207. Sender: types.UserInfo{
  208. AimID: sender,
  209. DisplayID: sender,
  210. UserType: "aim",
  211. },
  212. Dest: types.UserInfo{
  213. AimID: recipient,
  214. DisplayID: recipient,
  215. UserType: "aim",
  216. },
  217. Message: message,
  218. MsgID: messageID,
  219. Timestamp: now,
  220. AutoResp: autoResponse,
  221. }
  222. sess.EventQueue.Push(types.EventTypeSentIM, senderEventData)
  223. if sess.IsSubscribedTo("conversation") {
  224. sess.EventQueue.Push(types.EventTypeConversation, types.ConversationEventData("update", []map[string]interface{}{
  225. types.ConversationEntry(recipient, recipient, message, messageID, sender, true, 0),
  226. }))
  227. }
  228. }
  229. // sendErrorResponse sends an error response in Web AIM API format
  230. func (h *MessagingHandler) sendErrorResponse(w http.ResponseWriter, statusCode int, errorText string) {
  231. SendError(w, statusCode, errorText)
  232. }
  233. // SetTyping handles the /im/setTyping endpoint for typing indicators
  234. func (h *MessagingHandler) SetTyping(w http.ResponseWriter, r *http.Request) {
  235. ctx := r.Context()
  236. // Get session from aimsid
  237. aimsid := r.URL.Query().Get("aimsid")
  238. if aimsid == "" {
  239. h.sendErrorResponse(w, http.StatusBadRequest, "missing required parameter: aimsid")
  240. return
  241. }
  242. sess, err := h.SessionManager.GetSession(r.Context(), aimsid)
  243. if err != nil {
  244. if err == state.ErrNoWebAPISession || err == state.ErrWebAPISessionExpired {
  245. h.sendErrorResponse(w, http.StatusUnauthorized, "invalid or expired session")
  246. } else {
  247. h.sendErrorResponse(w, http.StatusInternalServerError, "internal server error")
  248. }
  249. return
  250. }
  251. // Update session activity
  252. if err := h.SessionManager.TouchSession(r.Context(), aimsid); err != nil {
  253. h.Logger.WarnContext(ctx, "failed to touch session", "aimsid", aimsid, "error", err)
  254. }
  255. // Parse parameters
  256. recipient := r.URL.Query().Get("t")
  257. if recipient == "" {
  258. h.sendErrorResponse(w, http.StatusBadRequest, "missing required parameter: t (recipient)")
  259. return
  260. }
  261. typingStatus := r.URL.Query().Get("typingStatus")
  262. if typingStatus == "" {
  263. typingStatus = "none"
  264. }
  265. var event uint16
  266. switch typingStatus {
  267. case "typing":
  268. event = 0x0002
  269. case "typed":
  270. event = 0x0001
  271. default:
  272. event = 0x0000
  273. }
  274. inBody := wire.SNAC_0x04_0x14_ICBMClientEvent{
  275. ChannelID: wire.ICBMChannelIM,
  276. ScreenName: recipient,
  277. Event: event,
  278. }
  279. if err := h.ICBMService.ClientEvent(ctx, sess.OSCARSession, wire.SNACFrame{}, inBody); err != nil {
  280. h.Logger.ErrorContext(ctx, "failed to send typing notification", "error", err)
  281. h.sendErrorResponse(w, http.StatusInternalServerError, "internal server error")
  282. return
  283. }
  284. h.sendSuccessResponse(w, r, nil)
  285. }
  286. // sendSuccessResponse sends a success response in Web AIM API format
  287. func (h *MessagingHandler) sendSuccessResponse(w http.ResponseWriter, r *http.Request, data interface{}) {
  288. response := BaseResponse{}
  289. response.Response.StatusCode = 200
  290. response.Response.StatusText = "OK"
  291. response.Response.Data = data
  292. SendResponse(w, r, response, h.Logger)
  293. }