cmd_server.go 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. package toc
  2. import (
  3. "bytes"
  4. "context"
  5. "errors"
  6. "fmt"
  7. "strings"
  8. "github.com/mk6i/retro-aim-server/state"
  9. "github.com/mk6i/retro-aim-server/wire"
  10. )
  11. var (
  12. cmdInternalSvcErr = "ERROR:989:internal server error"
  13. errDisconnect = errors.New("got booted by another session")
  14. )
  15. // RecvBOS routes incoming SNAC messages from the BOS server to their
  16. // corresponding TOC handlers. It ignores any SNAC messages for which there is
  17. // no TOC response.
  18. func (s OSCARProxy) RecvBOS(ctx context.Context, me *state.Session, chatRegistry *ChatRegistry, ch chan<- []byte) error {
  19. for {
  20. select {
  21. case <-ctx.Done():
  22. return nil
  23. case <-me.Closed():
  24. return errDisconnect
  25. case snac := <-me.ReceiveMessage():
  26. switch v := snac.Body.(type) {
  27. case wire.SNAC_0x03_0x0B_BuddyArrived:
  28. sendOrCancel(ctx, ch, s.UpdateBuddyArrival(v))
  29. case wire.SNAC_0x03_0x0C_BuddyDeparted:
  30. sendOrCancel(ctx, ch, s.UpdateBuddyDeparted(v))
  31. case wire.SNAC_0x04_0x07_ICBMChannelMsgToClient:
  32. sendOrCancel(ctx, ch, s.IMIn(ctx, chatRegistry, v))
  33. case wire.SNAC_0x01_0x10_OServiceEvilNotification:
  34. sendOrCancel(ctx, ch, s.Eviled(v))
  35. default:
  36. s.Logger.DebugContext(ctx, fmt.Sprintf("unsupported snac. foodgroup: %s subgroup: %s",
  37. wire.FoodGroupName(snac.Frame.FoodGroup),
  38. wire.SubGroupName(snac.Frame.FoodGroup, snac.Frame.SubGroup)))
  39. }
  40. }
  41. }
  42. }
  43. // RecvChat routes incoming SNAC messages from the chat server to their
  44. // corresponding TOC handlers. It ignores any SNAC messages for which there is
  45. // no TOC response.
  46. func (s OSCARProxy) RecvChat(ctx context.Context, me *state.Session, chatID int, ch chan<- []byte) {
  47. for {
  48. select {
  49. case <-ctx.Done():
  50. return
  51. case <-me.Closed():
  52. return
  53. case snac := <-me.ReceiveMessage():
  54. switch v := snac.Body.(type) {
  55. case wire.SNAC_0x0E_0x04_ChatUsersLeft:
  56. sendOrCancel(ctx, ch, s.ChatUpdateBuddyLeft(v, chatID))
  57. case wire.SNAC_0x0E_0x03_ChatUsersJoined:
  58. sendOrCancel(ctx, ch, s.ChatUpdateBuddyArrived(v, chatID))
  59. case wire.SNAC_0x0E_0x06_ChatChannelMsgToClient:
  60. sendOrCancel(ctx, ch, s.ChatIn(ctx, v, chatID))
  61. default:
  62. s.Logger.DebugContext(ctx, fmt.Sprintf("unsupported snac. foodgroup: %s subgroup: %s",
  63. wire.FoodGroupName(snac.Frame.FoodGroup),
  64. wire.SubGroupName(snac.Frame.FoodGroup, snac.Frame.SubGroup)))
  65. }
  66. }
  67. }
  68. }
  69. // ChatIn handles the CHAT_IN TOC command.
  70. //
  71. // From the TiK documentation:
  72. //
  73. // A chat message was sent in a chat room.
  74. //
  75. // Command syntax: CHAT_IN:<Chat Room Id>:<Source User>:<Whisper? T/F>:<Message>
  76. func (s OSCARProxy) ChatIn(ctx context.Context, snac wire.SNAC_0x0E_0x06_ChatChannelMsgToClient, chatID int) string {
  77. b, ok := snac.Bytes(wire.ChatTLVSenderInformation)
  78. if !ok {
  79. return s.runtimeErr(ctx, errors.New("snac.Bytes: missing wire.ChatTLVSenderInformation"))
  80. }
  81. u := wire.TLVUserInfo{}
  82. err := wire.UnmarshalBE(&u, bytes.NewReader(b))
  83. if err != nil {
  84. return s.runtimeErr(ctx, fmt.Errorf("wire.UnmarshalBE: %w", err))
  85. }
  86. b, ok = snac.Bytes(wire.ChatTLVMessageInfo)
  87. if !ok {
  88. return s.runtimeErr(ctx, errors.New("snac.Bytes: missing wire.ChatTLVMessageInfo"))
  89. }
  90. text, err := wire.UnmarshalChatMessageText(b)
  91. if err != nil {
  92. return s.runtimeErr(ctx, fmt.Errorf("wire.UnmarshalChatMessageText: %w", err))
  93. }
  94. return fmt.Sprintf("CHAT_IN:%d:%s:F:%s", chatID, u.ScreenName, text)
  95. }
  96. // ChatUpdateBuddyArrived handles the CHAT_UPDATE_BUDDY TOC command for chat
  97. // room arrival events.
  98. //
  99. // From the TiK documentation:
  100. //
  101. // This one command handles arrival/departs from a chat room. The very first
  102. // message of this type for each chat room contains the users already in the
  103. // room.
  104. //
  105. // Command syntax: CHAT_UPDATE_BUDDY:<Chat Room Id>:<Inside? T/F>:<User 1>:<User 2>...
  106. func (s OSCARProxy) ChatUpdateBuddyArrived(snac wire.SNAC_0x0E_0x03_ChatUsersJoined, chatID int) string {
  107. users := make([]string, 0, len(snac.Users))
  108. for _, u := range snac.Users {
  109. users = append(users, u.ScreenName)
  110. }
  111. return fmt.Sprintf("CHAT_UPDATE_BUDDY:%d:T:%s", chatID, strings.Join(users, ":"))
  112. }
  113. // ChatUpdateBuddyLeft handles the CHAT_UPDATE_BUDDY TOC command for chat
  114. // room departure events.
  115. //
  116. // From the TiK documentation:
  117. //
  118. // This one command handles arrival/departs from a chat room. The very first
  119. // message of this type for each chat room contains the users already in the
  120. // room.
  121. //
  122. // Command syntax: CHAT_UPDATE_BUDDY:<Chat Room Id>:<Inside? T/F>:<User 1>:<User 2>...
  123. func (s OSCARProxy) ChatUpdateBuddyLeft(snac wire.SNAC_0x0E_0x04_ChatUsersLeft, chatID int) string {
  124. users := make([]string, 0, len(snac.Users))
  125. for _, u := range snac.Users {
  126. users = append(users, u.ScreenName)
  127. }
  128. return fmt.Sprintf("CHAT_UPDATE_BUDDY:%d:F:%s", chatID, strings.Join(users, ":"))
  129. }
  130. // Eviled handles the EVILED TOC command.
  131. //
  132. // From the TiK documentation:
  133. //
  134. // The user was just eviled.
  135. //
  136. // Command syntax: EVILED:<new evil>:<name of eviler, blank if anonymous>
  137. func (s OSCARProxy) Eviled(snac wire.SNAC_0x01_0x10_OServiceEvilNotification) string {
  138. warning := fmt.Sprintf("%d", snac.NewEvil/10)
  139. who := ""
  140. if snac.Snitcher != nil {
  141. who = snac.Snitcher.ScreenName
  142. }
  143. return fmt.Sprintf("EVILED:%s:%s", warning, who)
  144. }
  145. // IMIn handles the IM_IN TOC command.
  146. //
  147. // From the TiK documentation:
  148. //
  149. // Receive an IM from someone. Everything after the third colon is the
  150. // incoming message, including other colons.
  151. //
  152. // Command syntax: IM_IN:<Source User>:<Auto Response T/F?>:<Message>
  153. func (s OSCARProxy) IMIn(ctx context.Context, chatRegistry *ChatRegistry, snac wire.SNAC_0x04_0x07_ICBMChannelMsgToClient) string {
  154. if snac.ChannelID == wire.ICBMChannelRendezvous {
  155. rdinfo, has := snac.TLVRestBlock.Bytes(wire.ICBMTLVData)
  156. if !has {
  157. return s.runtimeErr(ctx, errors.New("TLVRestBlock.Bytes: missing rendezvous block"))
  158. }
  159. frag := wire.ICBMCh2Fragment{}
  160. if err := wire.UnmarshalBE(&frag, bytes.NewReader(rdinfo)); err != nil {
  161. return s.runtimeErr(ctx, fmt.Errorf("wire.UnmarshalBE: %w", err))
  162. }
  163. prompt, ok := frag.Bytes(wire.ICBMRdvTLVTagsInvitation)
  164. if !ok {
  165. return s.runtimeErr(ctx, errors.New("frag.Bytes: missing chat invite prompt"))
  166. }
  167. svcData, ok := frag.Bytes(wire.ICBMRdvTLVTagsSvcData)
  168. if !ok || svcData == nil {
  169. return s.runtimeErr(ctx, errors.New("frag.Bytes: missing room info"))
  170. }
  171. roomInfo := wire.ICBMRoomInfo{}
  172. if err := wire.UnmarshalBE(&roomInfo, bytes.NewReader(svcData)); err != nil {
  173. return s.runtimeErr(ctx, fmt.Errorf("wire.UnmarshalBE: %w", err))
  174. }
  175. cookie := strings.Split(roomInfo.Cookie, "-") // make this safe
  176. if len(cookie) < 3 {
  177. return s.runtimeErr(ctx, errors.New("roomInfo.Cookie: malformed cookie, could not get room name"))
  178. }
  179. roomName := cookie[2]
  180. chatID := chatRegistry.Add(roomInfo)
  181. return fmt.Sprintf("CHAT_INVITE:%s:%d:%s:%s", roomName, chatID, snac.ScreenName, prompt)
  182. }
  183. buf, ok := snac.TLVRestBlock.Bytes(wire.ICBMTLVAOLIMData)
  184. if !ok {
  185. return s.runtimeErr(ctx, errors.New("TLVRestBlock.Bytes: missing wire.ICBMTLVAOLIMData"))
  186. }
  187. txt, err := wire.UnmarshalICBMMessageText(buf)
  188. if err != nil {
  189. return s.runtimeErr(ctx, fmt.Errorf("wire.UnmarshalICBMMessageText: %w", err))
  190. }
  191. autoResp := "F"
  192. if _, isAutoReply := snac.TLVRestBlock.Bytes(wire.ICBMTLVAutoResponse); isAutoReply {
  193. autoResp = "T"
  194. }
  195. return fmt.Sprintf("IM_IN:%s:%s:%s", snac.ScreenName, autoResp, txt)
  196. }
  197. // UpdateBuddyArrival handles the UPDATE_BUDDY TOC command for buddy arrival events.
  198. //
  199. // From the TiK documentation:
  200. //
  201. // This one command handles arrival/depart/updates. Evil Amount is a percentage, Signon Time is UNIX epoc, idle time is in minutes, UC (User Class) is a two/three character string.
  202. // - uc[0]
  203. // - ' ' - Ignore
  204. // - 'A' - On AOL
  205. // - uc[1]
  206. // - ' ' - Ignore
  207. // - 'A' - Oscar Admin
  208. // - 'U' - Oscar Unconfirmed
  209. // - 'O' - Oscar Normal
  210. // - uc[2]
  211. // - '\0' - Ignore
  212. // - ' ' - Ignore
  213. // - 'U' - The user has set their unavailable flag.
  214. //
  215. // Command syntax: UPDATE_BUDDY:<Buddy User>:<Online? T/F>:<Evil Amount>:<Signon Time>:<IdleTime>:<UC>
  216. func (s OSCARProxy) UpdateBuddyArrival(snac wire.SNAC_0x03_0x0B_BuddyArrived) string {
  217. online, _ := snac.Uint32BE(wire.OServiceUserInfoSignonTOD)
  218. idle, _ := snac.Uint16BE(wire.OServiceUserInfoIdleTime)
  219. uc := [3]string{" ", "O", " "}
  220. if snac.IsAway() {
  221. uc[2] = "U"
  222. }
  223. warning := fmt.Sprintf("%d", snac.WarningLevel/10)
  224. class := strings.Join(uc[:], "")
  225. return fmt.Sprintf("UPDATE_BUDDY:%s:%s:%s:%d:%d:%s", snac.ScreenName, "T", warning, online, idle, class)
  226. }
  227. // UpdateBuddyDeparted handles the UPDATE_BUDDY TOC command for buddy departure events.
  228. //
  229. // From the TiK documentation:
  230. //
  231. // This one command handles arrival/depart/updates. Evil Amount is a
  232. // percentage, Signon Time is UNIX epoc, idle time is in minutes, UC (User
  233. // Class) is a two/three character string.
  234. // - uc[0]
  235. // - ' ' - Ignore
  236. // - 'A' - On AOL
  237. // - uc[1]
  238. // - ' ' - Ignore
  239. // - 'A' - Oscar Admin
  240. // - 'U' - Oscar Unconfirmed
  241. // - 'O' - Oscar Normal
  242. // - uc[2]
  243. // - '\0' - Ignore
  244. // - ' ' - Ignore
  245. // - 'U' - The user has set their unavailable flag.
  246. //
  247. // Command syntax: UPDATE_BUDDY:<Buddy User>:<Online? T/F>:<Evil Amount>:<Signon Time>:<IdleTime>:<UC>
  248. func (s OSCARProxy) UpdateBuddyDeparted(snac wire.SNAC_0x03_0x0C_BuddyDeparted) string {
  249. return fmt.Sprintf("UPDATE_BUDDY:%s:F:0:0:0: ", snac.ScreenName)
  250. }
  251. func sendOrCancel(ctx context.Context, ch chan<- []byte, msg string) {
  252. select {
  253. case <-ctx.Done():
  254. return
  255. case ch <- []byte(msg):
  256. return
  257. }
  258. }