feedbag.go 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. package handler
  2. import (
  3. "context"
  4. "time"
  5. "github.com/mk6i/retro-aim-server/oscar"
  6. "github.com/mk6i/retro-aim-server/state"
  7. )
  8. // NewFeedbagService creates a new instance of FeedbagService.
  9. func NewFeedbagService(messageRelayer MessageRelayer, feedbagManager FeedbagManager) FeedbagService {
  10. return FeedbagService{
  11. messageRelayer: messageRelayer,
  12. feedbagManager: feedbagManager,
  13. }
  14. }
  15. // FeedbagService provides handlers for the Feedbag food group.
  16. type FeedbagService struct {
  17. messageRelayer MessageRelayer
  18. feedbagManager FeedbagManager
  19. }
  20. // RightsQueryHandler returns SNAC oscar.FeedbagRightsReply, which contains
  21. // Feedbag food group settings for the current user. The values within the SNAC
  22. // are not well understood but seem to make the AIM client happy.
  23. func (s FeedbagService) RightsQueryHandler(_ context.Context, inFrame oscar.SNACFrame) oscar.SNACMessage {
  24. return oscar.SNACMessage{
  25. Frame: oscar.SNACFrame{
  26. FoodGroup: oscar.Feedbag,
  27. SubGroup: oscar.FeedbagRightsReply,
  28. RequestID: inFrame.RequestID,
  29. },
  30. Body: oscar.SNAC_0x13_0x03_FeedbagRightsReply{
  31. TLVRestBlock: oscar.TLVRestBlock{
  32. TLVList: oscar.TLVList{
  33. oscar.NewTLV(oscar.FeedbagRightsMaxItemAttrs, uint16(200)),
  34. oscar.NewTLV(oscar.FeedbagRightsMaxItemsByClass, []uint16{
  35. 0x3D, // max num of contacts
  36. 0x3D, // max num of groups
  37. 0x64, // max visible contacts
  38. 0x64, // max invisible contacts
  39. 0x01, // max vis/invis bitmasks
  40. 0x01, // max presense info fields
  41. 0x32, // limit for item type 06
  42. 0x00, // limit for item type 07
  43. 0x00, // limit for item type 08
  44. 0x03, // limit for item type 09
  45. 0x00, // limit for item type 0a
  46. 0x00, // limit for item type 0b
  47. 0x00, // limit for item type 0c
  48. 0x80, // limit for item type 0d
  49. 0xFF, // max ignore list entries
  50. 0x14, // limit for item type 0f
  51. 0xC8, // limit for item 10
  52. 0x01, // limit for item 11
  53. 0x00, // limit for item 12
  54. 0x01, // limit for item 13
  55. 0x00, // limit for item 14
  56. }),
  57. oscar.NewTLV(oscar.FeedbagRightsMaxClientItems, uint16(200)),
  58. oscar.NewTLV(oscar.FeedbagRightsMaxItemNameLen, uint16(200)),
  59. oscar.NewTLV(oscar.FeedbagRightsMaxRecentBuddies, uint16(200)),
  60. oscar.NewTLV(oscar.FeedbagRightsInteractionBuddies, uint16(200)),
  61. oscar.NewTLV(oscar.FeedbagRightsInteractionHalfLife, uint16(200)),
  62. oscar.NewTLV(oscar.FeedbagRightsInteractionMaxScore, uint16(200)),
  63. oscar.NewTLV(oscar.FeedbagRightsMaxBuddiesPerGroup, uint16(200)),
  64. oscar.NewTLV(oscar.FeedbagRightsMaxMegaBots, uint16(200)),
  65. oscar.NewTLV(oscar.FeedbagRightsMaxSmartGroups, uint16(100)),
  66. },
  67. },
  68. },
  69. }
  70. }
  71. // QueryHandler fetches the user's feedbag (aka buddy list). It returns
  72. // oscar.FeedbagReply, which contains feedbag entries.
  73. func (s FeedbagService) QueryHandler(_ context.Context, sess *state.Session, inFrame oscar.SNACFrame) (oscar.SNACMessage, error) {
  74. fb, err := s.feedbagManager.Feedbag(sess.ScreenName())
  75. if err != nil {
  76. return oscar.SNACMessage{}, err
  77. }
  78. lm := time.UnixMilli(0)
  79. if len(fb) > 0 {
  80. lm, err = s.feedbagManager.FeedbagLastModified(sess.ScreenName())
  81. if err != nil {
  82. return oscar.SNACMessage{}, err
  83. }
  84. }
  85. return oscar.SNACMessage{
  86. Frame: oscar.SNACFrame{
  87. FoodGroup: oscar.Feedbag,
  88. SubGroup: oscar.FeedbagReply,
  89. RequestID: inFrame.RequestID,
  90. },
  91. Body: oscar.SNAC_0x13_0x06_FeedbagReply{
  92. Version: 0,
  93. Items: fb,
  94. LastUpdate: uint32(lm.Unix()),
  95. },
  96. }, nil
  97. }
  98. // QueryIfModifiedHandler fetches the user's feedbag (aka buddy list). It
  99. // returns oscar.FeedbagReplyNotModified if the feedbag was last modified
  100. // before inBody.LastUpdate, else return oscar.FeedbagReply, which contains
  101. // feedbag entries.
  102. func (s FeedbagService) QueryIfModifiedHandler(_ context.Context, sess *state.Session, inFrame oscar.SNACFrame, inBody oscar.SNAC_0x13_0x05_FeedbagQueryIfModified) (oscar.SNACMessage, error) {
  103. fb, err := s.feedbagManager.Feedbag(sess.ScreenName())
  104. if err != nil {
  105. return oscar.SNACMessage{}, err
  106. }
  107. lm := time.UnixMilli(0)
  108. if len(fb) > 0 {
  109. lm, err = s.feedbagManager.FeedbagLastModified(sess.ScreenName())
  110. if err != nil {
  111. return oscar.SNACMessage{}, err
  112. }
  113. if lm.Before(time.Unix(int64(inBody.LastUpdate), 0)) {
  114. return oscar.SNACMessage{
  115. Frame: oscar.SNACFrame{
  116. FoodGroup: oscar.Feedbag,
  117. SubGroup: oscar.FeedbagReplyNotModified,
  118. RequestID: inFrame.RequestID,
  119. },
  120. Body: oscar.SNAC_0x13_0x05_FeedbagQueryIfModified{
  121. LastUpdate: uint32(lm.Unix()),
  122. Count: uint8(len(fb)),
  123. },
  124. }, nil
  125. }
  126. }
  127. return oscar.SNACMessage{
  128. Frame: oscar.SNACFrame{
  129. FoodGroup: oscar.Feedbag,
  130. SubGroup: oscar.FeedbagReply,
  131. RequestID: inFrame.RequestID,
  132. },
  133. Body: oscar.SNAC_0x13_0x06_FeedbagReply{
  134. Version: 0,
  135. Items: fb,
  136. LastUpdate: uint32(lm.Unix()),
  137. },
  138. }, nil
  139. }
  140. // InsertItemHandler adds items to the user's feedbag (aka buddy list). Sends
  141. // user buddy arrival notifications for each online & visible buddy added to
  142. // the feedbag. Sends a buddy departure notification to blocked buddies if
  143. // current user is visible. It returns oscar.FeedbagStatus, which contains
  144. // insert confirmation.
  145. func (s FeedbagService) InsertItemHandler(ctx context.Context, sess *state.Session, inFrame oscar.SNACFrame, inBody oscar.SNAC_0x13_0x08_FeedbagInsertItem) (oscar.SNACMessage, error) {
  146. for _, item := range inBody.Items {
  147. // don't let users block themselves, it causes the AIM client to go
  148. // into a weird state.
  149. if item.ClassID == 3 && item.Name == sess.ScreenName() {
  150. return oscar.SNACMessage{
  151. Frame: oscar.SNACFrame{
  152. FoodGroup: oscar.Feedbag,
  153. SubGroup: oscar.FeedbagErr,
  154. RequestID: inFrame.RequestID,
  155. },
  156. Body: oscar.SNACError{
  157. Code: oscar.ErrorCodeNotSupportedByHost,
  158. },
  159. }, nil
  160. }
  161. }
  162. if err := s.feedbagManager.FeedbagUpsert(sess.ScreenName(), inBody.Items); err != nil {
  163. return oscar.SNACMessage{}, nil
  164. }
  165. for _, item := range inBody.Items {
  166. switch item.ClassID {
  167. case oscar.FeedbagClassIdBuddy, oscar.FeedbagClassIDPermit: // add new buddy
  168. buddy := s.messageRelayer.RetrieveByScreenName(item.Name)
  169. if buddy == nil || buddy.Invisible() {
  170. continue
  171. }
  172. unicastArrival(ctx, buddy, sess, s.messageRelayer)
  173. case oscar.FeedbagClassIDDeny: // block buddy
  174. if sess.Invisible() {
  175. continue // user's offline, don't send departure notification
  176. }
  177. blockedSess := s.messageRelayer.RetrieveByScreenName(item.Name)
  178. if blockedSess == nil {
  179. continue // blocked buddy is offline, nothing to do here
  180. }
  181. // alert blocked buddy that current user is offline
  182. unicastDeparture(ctx, sess, blockedSess, s.messageRelayer)
  183. }
  184. }
  185. snacPayloadOut := oscar.SNAC_0x13_0x0E_FeedbagStatus{}
  186. for range inBody.Items {
  187. snacPayloadOut.Results = append(snacPayloadOut.Results, 0x0000)
  188. }
  189. return oscar.SNACMessage{
  190. Frame: oscar.SNACFrame{
  191. FoodGroup: oscar.Feedbag,
  192. SubGroup: oscar.FeedbagStatus,
  193. RequestID: inFrame.RequestID,
  194. },
  195. Body: snacPayloadOut,
  196. }, nil
  197. }
  198. // UpdateItemHandler updates items in the user's feedbag (aka buddy list).
  199. // Sends user buddy arrival notifications for each online & visible buddy added
  200. // to the feedbag. It returns oscar.FeedbagStatus, which contains update
  201. // confirmation.
  202. func (s FeedbagService) UpdateItemHandler(ctx context.Context, sess *state.Session, inFrame oscar.SNACFrame, inBody oscar.SNAC_0x13_0x09_FeedbagUpdateItem) (oscar.SNACMessage, error) {
  203. if err := s.feedbagManager.FeedbagUpsert(sess.ScreenName(), inBody.Items); err != nil {
  204. return oscar.SNACMessage{}, nil
  205. }
  206. for _, item := range inBody.Items {
  207. switch item.ClassID {
  208. case oscar.FeedbagClassIdBuddy, oscar.FeedbagClassIDPermit:
  209. buddy := s.messageRelayer.RetrieveByScreenName(item.Name)
  210. if buddy == nil || buddy.Invisible() {
  211. continue
  212. }
  213. unicastArrival(ctx, buddy, sess, s.messageRelayer)
  214. }
  215. }
  216. snacPayloadOut := oscar.SNAC_0x13_0x0E_FeedbagStatus{}
  217. for range inBody.Items {
  218. snacPayloadOut.Results = append(snacPayloadOut.Results, 0x0000)
  219. }
  220. return oscar.SNACMessage{
  221. Frame: oscar.SNACFrame{
  222. FoodGroup: oscar.Feedbag,
  223. SubGroup: oscar.FeedbagStatus,
  224. RequestID: inFrame.RequestID,
  225. },
  226. Body: snacPayloadOut,
  227. }, nil
  228. }
  229. // DeleteItemHandler removes items from feedbag (aka buddy list). Sends user
  230. // buddy arrival notifications for each online & visible buddy added to
  231. // the feedbag. Sends buddy arrival notifications to each unblocked buddy if
  232. // current user is visible. It returns oscar.FeedbagStatus, which contains update
  233. // confirmation.
  234. func (s FeedbagService) DeleteItemHandler(ctx context.Context, sess *state.Session, inFrame oscar.SNACFrame, inBody oscar.SNAC_0x13_0x0A_FeedbagDeleteItem) (oscar.SNACMessage, error) {
  235. if err := s.feedbagManager.FeedbagDelete(sess.ScreenName(), inBody.Items); err != nil {
  236. return oscar.SNACMessage{}, err
  237. }
  238. for _, item := range inBody.Items {
  239. if item.ClassID == oscar.FeedbagClassIDDeny {
  240. unblockedSess := s.messageRelayer.RetrieveByScreenName(item.Name)
  241. if unblockedSess == nil {
  242. continue // unblocked user is offline, nothing to do here
  243. }
  244. if !sess.Invisible() {
  245. // alert unblocked user that current user is online
  246. unicastArrival(ctx, sess, unblockedSess, s.messageRelayer)
  247. }
  248. if !unblockedSess.Invisible() {
  249. // alert current user that unblocked user is online
  250. unicastArrival(ctx, unblockedSess, sess, s.messageRelayer)
  251. }
  252. }
  253. }
  254. snacPayloadOut := oscar.SNAC_0x13_0x0E_FeedbagStatus{}
  255. for range inBody.Items {
  256. snacPayloadOut.Results = append(snacPayloadOut.Results, 0x0000) // success by default
  257. }
  258. return oscar.SNACMessage{
  259. Frame: oscar.SNACFrame{
  260. FoodGroup: oscar.Feedbag,
  261. SubGroup: oscar.FeedbagStatus,
  262. RequestID: inFrame.RequestID,
  263. },
  264. Body: snacPayloadOut,
  265. }, nil
  266. }
  267. // StartClusterHandler exists to capture the SNAC input in unit tests to verify
  268. // it's correctly unmarshalled.
  269. func (s FeedbagService) StartClusterHandler(context.Context, oscar.SNACFrame, oscar.SNAC_0x13_0x11_FeedbagStartCluster) {
  270. }