locate.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. package foodgroup
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "time"
  7. "github.com/mk6i/open-oscar-server/state"
  8. "github.com/mk6i/open-oscar-server/wire"
  9. )
  10. // omitCaps is the map of to filter out of the client's capability list
  11. // because they are not currently supported by the server.
  12. var omitCaps = map[[16]byte]bool{
  13. wire.CapGames: true, // games
  14. wire.CapSupportICQ: true, // ICQ inter-op
  15. wire.CapVoiceChat: true, // voice chat
  16. }
  17. // NewLocateService creates a new instance of LocateService.
  18. func NewLocateService(
  19. bartItemManager BARTItemManager,
  20. messageRelayer MessageRelayer,
  21. profileManager ProfileManager,
  22. relationshipFetcher RelationshipFetcher,
  23. sessionRetriever SessionRetriever,
  24. userManager UserManager,
  25. ) LocateService {
  26. return LocateService{
  27. buddyBroadcaster: newBuddyNotifier(bartItemManager, relationshipFetcher, messageRelayer, sessionRetriever),
  28. messageRelayer: messageRelayer,
  29. relationshipFetcher: relationshipFetcher,
  30. profileManager: profileManager,
  31. sessionRetriever: sessionRetriever,
  32. userManager: userManager,
  33. }
  34. }
  35. // LocateService provides functionality for the Locate food group, which is
  36. // responsible for user profiles, user info lookups, directory information, and
  37. // keyword lookups.
  38. type LocateService struct {
  39. buddyBroadcaster buddyBroadcaster
  40. messageRelayer MessageRelayer
  41. relationshipFetcher RelationshipFetcher
  42. profileManager ProfileManager
  43. sessionRetriever SessionRetriever
  44. userManager UserManager
  45. }
  46. // RightsQuery returns SNAC wire.LocateRightsReply, which contains Locate food
  47. // group settings for the current user.
  48. func (s LocateService) RightsQuery(_ context.Context, inFrame wire.SNACFrame) wire.SNACMessage {
  49. return wire.SNACMessage{
  50. Frame: wire.SNACFrame{
  51. FoodGroup: wire.Locate,
  52. SubGroup: wire.LocateRightsReply,
  53. RequestID: inFrame.RequestID,
  54. },
  55. Body: wire.SNAC_0x02_0x03_LocateRightsReply{
  56. TLVRestBlock: wire.TLVRestBlock{
  57. TLVList: wire.TLVList{
  58. // these are arbitrary values--AIM clients seem to perform
  59. // OK with them
  60. wire.NewTLVBE(wire.LocateTLVTagsRightsMaxSigLen, uint16(1000)),
  61. wire.NewTLVBE(wire.LocateTLVTagsRightsMaxCapabilitiesLen, uint16(1000)),
  62. wire.NewTLVBE(wire.LocateTLVTagsRightsMaxFindByEmailList, uint16(1000)),
  63. wire.NewTLVBE(wire.LocateTLVTagsRightsMaxCertsLen, uint16(1000)),
  64. wire.NewTLVBE(wire.LocateTLVTagsRightsMaxMaxShortCapabilities, uint16(1000)),
  65. },
  66. },
  67. },
  68. }
  69. }
  70. // SetInfo sets the user's profile, away message or capabilities.
  71. func (s LocateService) SetInfo(ctx context.Context, instance *state.SessionInstance, inBody wire.SNAC_0x02_0x04_LocateSetInfo) error {
  72. // update profile
  73. if profileText, hasProfile := inBody.String(wire.LocateTLVTagsInfoSigData); hasProfile {
  74. mime, _ := inBody.String(wire.LocateTLVTagsInfoSigMime)
  75. profile := state.UserProfile{
  76. ProfileText: profileText,
  77. MIMEType: mime,
  78. UpdateTime: time.Now(),
  79. }
  80. // set the server-side profile
  81. if instance.KerberosAuth() || inBody.HasTag(wire.LocateTLVTagsInfoSupportHostSig) {
  82. // normally, the SupportHostSig TLV indicates that the profile should
  83. // be stored server-side. however, some AIM 6 clients expect server-side
  84. // profiles but do not send this TLV. in order to cover all bases, just
  85. // save the profile for all kerberos-based clients.
  86. if err := s.profileManager.SetProfile(ctx, instance.IdentScreenName(), profile); err != nil {
  87. return err
  88. }
  89. for _, _instance := range instance.Session().Instances() {
  90. if _instance.KerberosAuth() {
  91. // update all instances that do server-side profile storage
  92. _instance.SetProfile(profile)
  93. }
  94. }
  95. s.messageRelayer.RelayToOtherInstances(ctx, instance, wire.SNACMessage{
  96. Frame: wire.SNACFrame{
  97. FoodGroup: wire.OService,
  98. SubGroup: wire.OServiceUserInfoUpdate,
  99. },
  100. Body: newOServiceUserInfoUpdate(instance),
  101. })
  102. } else {
  103. // set the client-side profile
  104. instance.SetProfile(profile)
  105. }
  106. }
  107. // broadcast away message change to buddies
  108. if awayMsg, hasAwayMsg := inBody.String(wire.LocateTLVTagsInfoUnavailableData); hasAwayMsg {
  109. if awayMsg != "" {
  110. instance.SetUserInfoFlag(wire.OServiceUserFlagUnavailable)
  111. } else {
  112. instance.ClearUserInfoFlag(wire.OServiceUserFlagUnavailable)
  113. }
  114. instance.SetAwayMessage(awayMsg)
  115. if instance.SignonComplete() {
  116. if err := s.buddyBroadcaster.BroadcastBuddyArrived(ctx, instance.IdentScreenName(), instance.Session().TLVUserInfo()); err != nil {
  117. return err
  118. }
  119. }
  120. }
  121. // update client capabilities (buddy icon, chat, etc...)
  122. if b, hasCaps := inBody.Bytes(wire.LocateTLVTagsInfoCapabilities); hasCaps {
  123. if len(b)%16 != 0 {
  124. return errors.New("capability list must be array of 16-byte values")
  125. }
  126. var caps [][16]byte
  127. for i := 0; i < len(b); i += 16 {
  128. var c [16]byte
  129. copy(c[:], b[i:i+16])
  130. if _, found := omitCaps[c]; found {
  131. continue
  132. }
  133. caps = append(caps, c)
  134. }
  135. instance.SetCaps(caps)
  136. if instance.SignonComplete() {
  137. if err := s.buddyBroadcaster.BroadcastBuddyArrived(ctx, instance.IdentScreenName(), instance.Session().TLVUserInfo()); err != nil {
  138. return err
  139. }
  140. }
  141. }
  142. return nil
  143. }
  144. func newLocateErr(requestID uint32, errCode uint16) wire.SNACMessage {
  145. return wire.SNACMessage{
  146. Frame: wire.SNACFrame{
  147. FoodGroup: wire.Locate,
  148. SubGroup: wire.LocateErr,
  149. RequestID: requestID,
  150. },
  151. Body: wire.SNACError{
  152. Code: errCode,
  153. },
  154. }
  155. }
  156. // UserInfoQuery fetches display information about an arbitrary user (not the
  157. // current user). It returns wire.LocateUserInfoReply, which contains the
  158. // profile, if requested, and/or the away message, if requested.
  159. func (s LocateService) UserInfoQuery(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x02_0x05_LocateUserInfoQuery) (wire.SNACMessage, error) {
  160. lookupSN := state.NewIdentScreenName(inBody.ScreenName)
  161. var lookupSess *state.Session
  162. if lookupSN == instance.IdentScreenName() {
  163. // looking up own profile
  164. lookupSess = instance.Session()
  165. } else {
  166. rel, err := s.relationshipFetcher.Relationship(ctx, instance.IdentScreenName(), lookupSN)
  167. if err != nil {
  168. return wire.SNACMessage{}, err
  169. }
  170. if rel.YouBlock || rel.BlocksYou {
  171. return newLocateErr(inFrame.RequestID, wire.ErrorCodeNotLoggedOn), nil
  172. }
  173. lookupSess = s.sessionRetriever.RetrieveSession(lookupSN)
  174. if lookupSess == nil {
  175. // user is offline
  176. return newLocateErr(inFrame.RequestID, wire.ErrorCodeNotLoggedOn), nil
  177. }
  178. }
  179. var list wire.TLVList
  180. if inBody.RequestProfile() {
  181. prof := lookupSess.Profile()
  182. // if looking up own profile, return this instance's profile for consistency
  183. if instance.IdentScreenName() == lookupSN {
  184. prof = instance.Profile()
  185. }
  186. list.AppendList([]wire.TLV{
  187. wire.NewTLVBE(wire.LocateTLVTagsInfoSigMime, prof.MIMEType),
  188. wire.NewTLVBE(wire.LocateTLVTagsInfoSigData, prof.ProfileText),
  189. })
  190. }
  191. if inBody.RequestAwayMessage() && lookupSess.Away() {
  192. list.AppendList([]wire.TLV{
  193. wire.NewTLVBE(wire.LocateTLVTagsInfoUnavailableMime, `text/aolrtf; charset="us-ascii"`),
  194. wire.NewTLVBE(wire.LocateTLVTagsInfoUnavailableData, lookupSess.AwayMessage()),
  195. })
  196. }
  197. return wire.SNACMessage{
  198. Frame: wire.SNACFrame{
  199. FoodGroup: wire.Locate,
  200. SubGroup: wire.LocateUserInfoReply,
  201. RequestID: inFrame.RequestID,
  202. },
  203. Body: wire.SNAC_0x02_0x06_LocateUserInfoReply{
  204. TLVUserInfo: lookupSess.TLVUserInfo(),
  205. LocateInfo: wire.TLVRestBlock{
  206. TLVList: list,
  207. },
  208. },
  209. }, nil
  210. }
  211. // SetDirInfo sets directory information for current user (first name, last
  212. // name, etc).
  213. func (s LocateService) SetDirInfo(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x02_0x09_LocateSetDirInfo) (wire.SNACMessage, error) {
  214. info := newAIMNameAndAddrFromTLVList(inBody.TLVList)
  215. if err := s.profileManager.SetDirectoryInfo(ctx, instance.IdentScreenName(), info); err != nil {
  216. return wire.SNACMessage{}, err
  217. }
  218. return wire.SNACMessage{
  219. Frame: wire.SNACFrame{
  220. FoodGroup: wire.Locate,
  221. SubGroup: wire.LocateSetDirReply,
  222. RequestID: inFrame.RequestID,
  223. },
  224. Body: wire.SNAC_0x02_0x0A_LocateSetDirReply{
  225. Result: 1,
  226. },
  227. }, nil
  228. }
  229. // SetKeywordInfo sets profile keywords and interests. This method does nothing
  230. // and exists to placate the AIM client. It returns wire.LocateSetKeywordReply
  231. // with a canned success message.
  232. func (s LocateService) SetKeywordInfo(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x02_0x0F_LocateSetKeywordInfo) (wire.SNACMessage, error) {
  233. var keywords [5]string
  234. i := 0
  235. for _, tlv := range inBody.TLVList {
  236. if tlv.Tag != wire.ODirTLVInterest {
  237. continue
  238. }
  239. keywords[i] = string(tlv.Value)
  240. i++
  241. if i == len(keywords) {
  242. break
  243. }
  244. }
  245. if err := s.profileManager.SetKeywords(ctx, instance.IdentScreenName(), keywords); err != nil {
  246. return wire.SNACMessage{}, fmt.Errorf("SetKeywords: %w", err)
  247. }
  248. return wire.SNACMessage{
  249. Frame: wire.SNACFrame{
  250. FoodGroup: wire.Locate,
  251. SubGroup: wire.LocateSetKeywordReply,
  252. RequestID: inFrame.RequestID,
  253. },
  254. Body: wire.SNAC_0x02_0x10_LocateSetKeywordReply{
  255. Unknown: 1,
  256. },
  257. }, nil
  258. }
  259. // DirInfo returns directory information for a user.
  260. func (s LocateService) DirInfo(ctx context.Context, inFrame wire.SNACFrame, inBody wire.SNAC_0x02_0x0B_LocateGetDirInfo) (wire.SNACMessage, error) {
  261. reply := wire.SNAC_0x02_0x0C_LocateGetDirReply{
  262. Status: wire.LocateGetDirReplyOK,
  263. TLVBlock: wire.TLVBlock{
  264. TLVList: wire.TLVList{},
  265. },
  266. }
  267. user, err := s.profileManager.User(ctx, state.NewIdentScreenName(inBody.ScreenName))
  268. if err != nil {
  269. return wire.SNACMessage{}, fmt.Errorf("User: %w", err)
  270. }
  271. if user != nil {
  272. reply.Append(wire.NewTLVBE(wire.ODirTLVFirstName, user.AIMDirectoryInfo.FirstName))
  273. reply.Append(wire.NewTLVBE(wire.ODirTLVLastName, user.AIMDirectoryInfo.LastName))
  274. reply.Append(wire.NewTLVBE(wire.ODirTLVMiddleName, user.AIMDirectoryInfo.MiddleName))
  275. reply.Append(wire.NewTLVBE(wire.ODirTLVMaidenName, user.AIMDirectoryInfo.MaidenName))
  276. reply.Append(wire.NewTLVBE(wire.ODirTLVCountry, user.AIMDirectoryInfo.Country))
  277. reply.Append(wire.NewTLVBE(wire.ODirTLVState, user.AIMDirectoryInfo.State))
  278. reply.Append(wire.NewTLVBE(wire.ODirTLVCity, user.AIMDirectoryInfo.City))
  279. reply.Append(wire.NewTLVBE(wire.ODirTLVNickName, user.AIMDirectoryInfo.NickName))
  280. reply.Append(wire.NewTLVBE(wire.ODirTLVZIP, user.AIMDirectoryInfo.ZIPCode))
  281. reply.Append(wire.NewTLVBE(wire.ODirTLVAddress, user.AIMDirectoryInfo.Address))
  282. }
  283. return wire.SNACMessage{
  284. Frame: wire.SNACFrame{
  285. FoodGroup: wire.Locate,
  286. SubGroup: wire.LocateGetDirReply,
  287. RequestID: inFrame.RequestID,
  288. },
  289. Body: reply,
  290. }, nil
  291. }