cmd_server.go 9.4 KB

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