oservice.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  1. package handler
  2. import (
  3. "bytes"
  4. "context"
  5. "errors"
  6. "fmt"
  7. "time"
  8. "github.com/mk6i/retro-aim-server/config"
  9. "github.com/mk6i/retro-aim-server/oscar"
  10. "github.com/mk6i/retro-aim-server/server"
  11. "github.com/mk6i/retro-aim-server/state"
  12. )
  13. // NewOServiceService creates a new instance of OServiceService.
  14. func NewOServiceService(cfg config.Config, messageRelayer MessageRelayer, feedbagManager FeedbagManager) *OServiceService {
  15. return &OServiceService{cfg: cfg, messageRelayer: messageRelayer, feedbagManager: feedbagManager}
  16. }
  17. // OServiceService provides handlers for the OService food group.
  18. type OServiceService struct {
  19. cfg config.Config
  20. feedbagManager FeedbagManager
  21. messageRelayer MessageRelayer
  22. }
  23. // ClientVersionsHandler informs the server what food group versions the client
  24. // supports and returns to the client what food group versions it supports.
  25. // This method simply regurgitates versions supplied by the client in inBody
  26. // back to the client in a OServiceHostVersions SNAC. The server doesn't
  27. // attempt to accommodate any particular food group version. The server
  28. // implicitly accommodates any food group version for Windows AIM clients 5.x.
  29. // It returns SNAC oscar.OServiceHostVersions containing the server's supported
  30. // food group versions.
  31. func (s OServiceService) ClientVersionsHandler(_ context.Context, frame oscar.SNACFrame, inBody oscar.SNAC_0x01_0x17_OServiceClientVersions) oscar.SNACMessage {
  32. return oscar.SNACMessage{
  33. Frame: oscar.SNACFrame{
  34. FoodGroup: oscar.OService,
  35. SubGroup: oscar.OServiceHostVersions,
  36. RequestID: frame.RequestID,
  37. },
  38. Body: oscar.SNAC_0x01_0x18_OServiceHostVersions{
  39. Versions: inBody.Versions,
  40. },
  41. }
  42. }
  43. // RateParamsQueryHandler returns SNAC rate limits.
  44. // The purpose of this method is to provide information about rate limits that
  45. // can be enforced on the server side. The response consists of two main parts:
  46. // rate classes and rate groups. Rate classes define limits based on specific
  47. // parameters, while rate groups associate these limits with relevant SNAC
  48. // types.
  49. //
  50. // Note: The current implementation does not enforce server-side rate limiting.
  51. // Instead, the provided values inform the client about the recommended
  52. // client-side rate limits.
  53. //
  54. // It returns SNAC oscar.OServiceRateParamsReply containing rate limits for
  55. // sending Instant Messages (IMs) and chat messages. More refined limits may be
  56. // added in the future if/when server rate limiting is implemented.
  57. func (s OServiceService) RateParamsQueryHandler(_ context.Context, inFrame oscar.SNACFrame) oscar.SNACMessage {
  58. return oscar.SNACMessage{
  59. Frame: oscar.SNACFrame{
  60. FoodGroup: oscar.OService,
  61. SubGroup: oscar.OServiceRateParamsReply,
  62. RequestID: inFrame.RequestID,
  63. },
  64. Body: oscar.SNAC_0x01_0x07_OServiceRateParamsReply{
  65. RateClasses: []struct {
  66. ID uint16
  67. WindowSize uint32
  68. ClearLevel uint32
  69. AlertLevel uint32
  70. LimitLevel uint32
  71. DisconnectLevel uint32
  72. CurrentLevel uint32
  73. MaxLevel uint32
  74. LastTime uint32
  75. CurrentState uint8
  76. }{
  77. {
  78. ID: 0x01,
  79. WindowSize: 0x0050,
  80. ClearLevel: 0x09C4,
  81. AlertLevel: 0x07D0,
  82. LimitLevel: 0x05DC,
  83. DisconnectLevel: 0x0320,
  84. CurrentLevel: 0x0D69,
  85. MaxLevel: 0x1770,
  86. LastTime: 0x0000,
  87. CurrentState: 0x0,
  88. },
  89. },
  90. RateGroups: []struct {
  91. ID uint16
  92. Pairs []struct {
  93. FoodGroup uint16
  94. SubGroup uint16
  95. } `count_prefix:"uint16"`
  96. }{
  97. {
  98. ID: 1,
  99. Pairs: []struct {
  100. FoodGroup uint16
  101. SubGroup uint16
  102. }{
  103. {
  104. FoodGroup: oscar.Buddy,
  105. SubGroup: oscar.BuddyRightsQuery,
  106. },
  107. {
  108. FoodGroup: oscar.Chat,
  109. SubGroup: oscar.ChatChannelMsgToHost,
  110. },
  111. {
  112. FoodGroup: oscar.ChatNav,
  113. SubGroup: oscar.ChatNavRequestChatRights,
  114. },
  115. {
  116. FoodGroup: oscar.ChatNav,
  117. SubGroup: oscar.ChatNavRequestRoomInfo,
  118. },
  119. {
  120. FoodGroup: oscar.ChatNav,
  121. SubGroup: oscar.ChatNavCreateRoom,
  122. },
  123. {
  124. FoodGroup: oscar.Feedbag,
  125. SubGroup: oscar.FeedbagRightsQuery,
  126. },
  127. {
  128. FoodGroup: oscar.Feedbag,
  129. SubGroup: oscar.FeedbagQuery,
  130. },
  131. {
  132. FoodGroup: oscar.Feedbag,
  133. SubGroup: oscar.FeedbagQueryIfModified,
  134. },
  135. {
  136. FoodGroup: oscar.Feedbag,
  137. SubGroup: oscar.FeedbagUse,
  138. },
  139. {
  140. FoodGroup: oscar.Feedbag,
  141. SubGroup: oscar.FeedbagInsertItem,
  142. },
  143. {
  144. FoodGroup: oscar.Feedbag,
  145. SubGroup: oscar.FeedbagUpdateItem,
  146. },
  147. {
  148. FoodGroup: oscar.Feedbag,
  149. SubGroup: oscar.FeedbagDeleteItem,
  150. },
  151. {
  152. FoodGroup: oscar.Feedbag,
  153. SubGroup: oscar.FeedbagStartCluster,
  154. },
  155. {
  156. FoodGroup: oscar.Feedbag,
  157. SubGroup: oscar.FeedbagEndCluster,
  158. },
  159. {
  160. FoodGroup: oscar.ICBM,
  161. SubGroup: oscar.ICBMAddParameters,
  162. },
  163. {
  164. FoodGroup: oscar.ICBM,
  165. SubGroup: oscar.ICBMParameterQuery,
  166. },
  167. {
  168. FoodGroup: oscar.ICBM,
  169. SubGroup: oscar.ICBMChannelMsgToHost,
  170. },
  171. {
  172. FoodGroup: oscar.ICBM,
  173. SubGroup: oscar.ICBMEvilRequest,
  174. },
  175. {
  176. FoodGroup: oscar.ICBM,
  177. SubGroup: oscar.ICBMClientErr,
  178. },
  179. {
  180. FoodGroup: oscar.ICBM,
  181. SubGroup: oscar.ICBMClientEvent,
  182. },
  183. {
  184. FoodGroup: oscar.Locate,
  185. SubGroup: oscar.LocateRightsQuery,
  186. },
  187. {
  188. FoodGroup: oscar.Locate,
  189. SubGroup: oscar.LocateSetInfo,
  190. },
  191. {
  192. FoodGroup: oscar.Locate,
  193. SubGroup: oscar.LocateSetDirInfo,
  194. },
  195. {
  196. FoodGroup: oscar.Locate,
  197. SubGroup: oscar.LocateGetDirInfo,
  198. },
  199. {
  200. FoodGroup: oscar.Locate,
  201. SubGroup: oscar.LocateSetKeywordInfo,
  202. },
  203. {
  204. FoodGroup: oscar.Locate,
  205. SubGroup: oscar.LocateUserInfoQuery2,
  206. },
  207. {
  208. FoodGroup: oscar.OService,
  209. SubGroup: oscar.OServiceServiceRequest,
  210. },
  211. {
  212. FoodGroup: oscar.OService,
  213. SubGroup: oscar.OServiceClientOnline,
  214. },
  215. {
  216. FoodGroup: oscar.OService,
  217. SubGroup: oscar.OServiceRateParamsQuery,
  218. },
  219. {
  220. FoodGroup: oscar.OService,
  221. SubGroup: oscar.OServiceRateParamsSubAdd,
  222. },
  223. {
  224. FoodGroup: oscar.OService,
  225. SubGroup: oscar.OServiceUserInfoQuery,
  226. },
  227. {
  228. FoodGroup: oscar.OService,
  229. SubGroup: oscar.OServiceIdleNotification,
  230. },
  231. {
  232. FoodGroup: oscar.OService,
  233. SubGroup: oscar.OServiceClientVersions,
  234. },
  235. {
  236. FoodGroup: oscar.OService,
  237. SubGroup: oscar.OServiceSetUserInfoFields,
  238. },
  239. },
  240. },
  241. },
  242. },
  243. }
  244. }
  245. // UserInfoQueryHandler returns SNAC oscar.OServiceUserInfoUpdate containing
  246. // the user's info.
  247. func (s OServiceService) UserInfoQueryHandler(_ context.Context, sess *state.Session, inFrame oscar.SNACFrame) oscar.SNACMessage {
  248. return oscar.SNACMessage{
  249. Frame: oscar.SNACFrame{
  250. FoodGroup: oscar.OService,
  251. SubGroup: oscar.OServiceUserInfoUpdate,
  252. RequestID: inFrame.RequestID,
  253. },
  254. Body: oscar.SNAC_0x01_0x0F_OServiceUserInfoUpdate{
  255. TLVUserInfo: sess.TLVUserInfo(),
  256. },
  257. }
  258. }
  259. // SetUserInfoFieldsHandler sets the user's visibility status to visible or invisible.
  260. // The visibility status is set according to the inFrame TLV entry under key
  261. // oscar.OServiceUserInfoStatus. If the value is 0x0000, set invisible. If set
  262. // to 0x0100, set invisible. Else, return an error for any other value.
  263. // It returns SNAC oscar.OServiceUserInfoUpdate containing the user's info.
  264. func (s OServiceService) SetUserInfoFieldsHandler(ctx context.Context, sess *state.Session, inFrame oscar.SNACFrame, inBody oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields) (oscar.SNACMessage, error) {
  265. if status, hasStatus := inBody.Uint32(oscar.OServiceUserInfoStatus); hasStatus {
  266. switch status {
  267. case 0x0000:
  268. sess.SetInvisible(false)
  269. if err := broadcastArrival(ctx, sess, s.messageRelayer, s.feedbagManager); err != nil {
  270. return oscar.SNACMessage{}, err
  271. }
  272. case 0x0100:
  273. sess.SetInvisible(true)
  274. if err := broadcastDeparture(ctx, sess, s.messageRelayer, s.feedbagManager); err != nil {
  275. return oscar.SNACMessage{}, err
  276. }
  277. default:
  278. return oscar.SNACMessage{}, fmt.Errorf("don't know what to do with status %d", status)
  279. }
  280. }
  281. return oscar.SNACMessage{
  282. Frame: oscar.SNACFrame{
  283. FoodGroup: oscar.OService,
  284. SubGroup: oscar.OServiceUserInfoUpdate,
  285. RequestID: inFrame.RequestID,
  286. },
  287. Body: oscar.SNAC_0x01_0x0F_OServiceUserInfoUpdate{
  288. TLVUserInfo: sess.TLVUserInfo(),
  289. },
  290. }, nil
  291. }
  292. // IdleNotificationHandler sets the user idle time.
  293. // Set session idle time to the value of bodyIn.IdleTime. Return a user arrival
  294. // message to all users who have this user on their buddy list.
  295. func (s OServiceService) IdleNotificationHandler(ctx context.Context, sess *state.Session, bodyIn oscar.SNAC_0x01_0x11_OServiceIdleNotification) error {
  296. if bodyIn.IdleTime == 0 {
  297. sess.UnsetIdle()
  298. } else {
  299. sess.SetIdle(time.Duration(bodyIn.IdleTime) * time.Second)
  300. }
  301. return broadcastArrival(ctx, sess, s.messageRelayer, s.feedbagManager)
  302. }
  303. // RateParamsSubAddHandler exists to capture the SNAC input in unit tests to
  304. // verify it's correctly unmarshalled.
  305. func (s OServiceService) RateParamsSubAddHandler(context.Context, oscar.SNAC_0x01_0x08_OServiceRateParamsSubAdd) {
  306. }
  307. // NewOServiceServiceForBOS creates a new instance of OServiceServiceForBOS.
  308. func NewOServiceServiceForBOS(oserviceService OServiceService, cr *state.ChatRegistry) *OServiceServiceForBOS {
  309. return &OServiceServiceForBOS{
  310. OServiceService: oserviceService,
  311. chatRegistry: cr,
  312. }
  313. }
  314. // OServiceServiceForBOS contains handlers for the OService food group for the
  315. // BOS service.
  316. type OServiceServiceForBOS struct {
  317. OServiceService
  318. chatRegistry *state.ChatRegistry
  319. }
  320. // ServiceRequestHandler configures food group settings for the current user.
  321. // This method only provides services for the Chat food group; return
  322. // server.ErrUnsupportedSubGroup for any other food group. When the chat food
  323. // group is specified in inFrame, add user to the chat room specified by TLV
  324. // 0x01.
  325. // It returns SNAC oscar.OServiceServiceResponse containing metadata the client
  326. // needs to connect to the chat service and join the chat room.
  327. func (s OServiceServiceForBOS) ServiceRequestHandler(_ context.Context, sess *state.Session, inFrame oscar.SNACFrame, inBody oscar.SNAC_0x01_0x04_OServiceServiceRequest) (oscar.SNACMessage, error) {
  328. if inBody.FoodGroup != oscar.Chat {
  329. return oscar.SNACMessage{}, server.ErrUnsupportedSubGroup
  330. }
  331. roomMeta, ok := inBody.Slice(0x01)
  332. if !ok {
  333. return oscar.SNACMessage{}, errors.New("missing room info")
  334. }
  335. roomSnac := oscar.SNAC_0x01_0x04_TLVRoomInfo{}
  336. if err := oscar.Unmarshal(&roomSnac, bytes.NewBuffer(roomMeta)); err != nil {
  337. return oscar.SNACMessage{}, err
  338. }
  339. room, chatSessMgr, err := s.chatRegistry.Retrieve(string(roomSnac.Cookie))
  340. if err != nil {
  341. return oscar.SNACMessage{}, server.ErrUnsupportedSubGroup
  342. }
  343. chatSessMgr.(SessionManager).AddSession(sess.ID(), sess.ScreenName())
  344. return oscar.SNACMessage{
  345. Frame: oscar.SNACFrame{
  346. FoodGroup: oscar.OService,
  347. SubGroup: oscar.OServiceServiceResponse,
  348. RequestID: inFrame.RequestID,
  349. },
  350. Body: oscar.SNAC_0x01_0x05_OServiceServiceResponse{
  351. TLVRestBlock: oscar.TLVRestBlock{
  352. TLVList: oscar.TLVList{
  353. oscar.NewTLV(oscar.OServiceTLVTagsReconnectHere, config.Address(s.cfg.OSCARHost, s.cfg.ChatPort)),
  354. oscar.NewTLV(oscar.OServiceTLVTagsLoginCookie, server.ChatCookie{
  355. Cookie: []byte(room.Cookie),
  356. SessID: sess.ID(),
  357. }),
  358. oscar.NewTLV(oscar.OServiceTLVTagsGroupID, oscar.Chat),
  359. oscar.NewTLV(oscar.OServiceTLVTagsSSLCertName, ""),
  360. oscar.NewTLV(oscar.OServiceTLVTagsSSLState, uint8(0x00)),
  361. },
  362. },
  363. },
  364. }, nil
  365. }
  366. // WriteOServiceHostOnline initiates the BOS protocol sequence.
  367. // It returns SNAC oscar.OServiceHostOnline containing the list food groups
  368. // supported by the BOS service.
  369. func (s OServiceServiceForBOS) WriteOServiceHostOnline() oscar.SNACMessage {
  370. return oscar.SNACMessage{
  371. Frame: oscar.SNACFrame{
  372. FoodGroup: oscar.OService,
  373. SubGroup: oscar.OServiceHostOnline,
  374. },
  375. Body: oscar.SNAC_0x01_0x03_OServiceHostOnline{
  376. FoodGroups: []uint16{
  377. oscar.Alert,
  378. oscar.Buddy,
  379. oscar.ChatNav,
  380. oscar.Feedbag,
  381. oscar.ICBM,
  382. oscar.Locate,
  383. oscar.OService,
  384. },
  385. },
  386. }
  387. }
  388. // ClientOnlineHandler runs when the current user is ready to join.
  389. // It performs the following sequence of actions:
  390. // - Announce current user's arrival to users who have the current user on
  391. // their buddy list.
  392. // - Send current user its buddy list
  393. func (s OServiceServiceForBOS) ClientOnlineHandler(ctx context.Context, _ oscar.SNAC_0x01_0x02_OServiceClientOnline, sess *state.Session) error {
  394. if err := broadcastArrival(ctx, sess, s.messageRelayer, s.feedbagManager); err != nil {
  395. return err
  396. }
  397. buddies, err := s.feedbagManager.Buddies(sess.ScreenName())
  398. if err != nil {
  399. return err
  400. }
  401. for _, screenName := range buddies {
  402. buddy := s.messageRelayer.RetrieveByScreenName(screenName)
  403. if buddy == nil || buddy.Invisible() {
  404. continue
  405. }
  406. unicastArrival(ctx, buddy, sess, s.messageRelayer)
  407. }
  408. return nil
  409. }
  410. // NewOServiceServiceForChat creates a new instance of OServiceServiceForChat.
  411. func NewOServiceServiceForChat(oserviceService OServiceService, chatRegistry *state.ChatRegistry) *OServiceServiceForChat {
  412. return &OServiceServiceForChat{
  413. OServiceService: oserviceService,
  414. chatRegistry: chatRegistry,
  415. }
  416. }
  417. // OServiceServiceForChat contains handlers for the OService food group for the
  418. // Chat service.
  419. type OServiceServiceForChat struct {
  420. OServiceService
  421. chatRegistry *state.ChatRegistry
  422. }
  423. // WriteOServiceHostOnline initiates the Chat protocol sequence.
  424. // It returns SNAC oscar.OServiceHostOnline containing the list of food groups
  425. // supported by the Chat service.
  426. func (s OServiceServiceForChat) WriteOServiceHostOnline() oscar.SNACMessage {
  427. return oscar.SNACMessage{
  428. Frame: oscar.SNACFrame{
  429. FoodGroup: oscar.OService,
  430. SubGroup: oscar.OServiceHostOnline,
  431. },
  432. Body: oscar.SNAC_0x01_0x03_OServiceHostOnline{
  433. FoodGroups: []uint16{
  434. oscar.OService,
  435. oscar.Chat,
  436. },
  437. },
  438. }
  439. }
  440. // ClientOnlineHandler runs when the current user is ready to join the chat.
  441. // Trigger the following actions:
  442. // - Send current user the chat room metadata
  443. // - Announce current user's arrival to other chat room participants
  444. // - Send current user the chat room participant list
  445. func (s OServiceServiceForChat) ClientOnlineHandler(ctx context.Context, sess *state.Session, chatID string) error {
  446. room, chatSessMgr, err := s.chatRegistry.Retrieve(chatID)
  447. if err != nil {
  448. return err
  449. }
  450. sendChatRoomInfoUpdate(ctx, sess, chatSessMgr.(ChatMessageRelayer), room)
  451. alertUserJoined(ctx, sess, chatSessMgr.(ChatMessageRelayer))
  452. setOnlineChatUsers(ctx, sess, chatSessMgr.(ChatMessageRelayer))
  453. return nil
  454. }