xtraz.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. package wire
  2. import (
  3. "encoding/hex"
  4. "encoding/xml"
  5. "errors"
  6. "fmt"
  7. "html"
  8. "math"
  9. "strconv"
  10. "strings"
  11. "github.com/google/uuid"
  12. )
  13. const (
  14. XtrazFuncInvitation uint16 = 0x0001 // chat invitation
  15. XtrazFuncData uint16 = 0x0002 // greeting cards, custom data
  16. XtrazFuncUserRemove uint16 = 0x0004 // user removal notification
  17. XtrazFuncNotify uint16 = 0x0008 // XStatus notifications
  18. )
  19. const (
  20. XStatusAngry uint8 = 1
  21. XStatusDuck uint8 = 2
  22. XStatusTired uint8 = 3
  23. XStatusParty uint8 = 4
  24. XStatusBeer uint8 = 5
  25. XStatusThinking uint8 = 6
  26. XStatusEating uint8 = 7
  27. XStatusTV uint8 = 8
  28. XStatusFriends uint8 = 9
  29. XStatusCoffee uint8 = 10
  30. XStatusMusic uint8 = 11
  31. XStatusBusiness uint8 = 12
  32. XStatusCamera uint8 = 13
  33. XStatusFunny uint8 = 14
  34. XStatusPhone uint8 = 15
  35. XStatusGames uint8 = 16
  36. XStatusCollege uint8 = 17
  37. XStatusShopping uint8 = 18
  38. XStatusSick uint8 = 19
  39. XStatusSleeping uint8 = 20
  40. XStatusSurfing uint8 = 21
  41. XStatusInternet uint8 = 22
  42. XStatusEngineering uint8 = 23
  43. XStatusTyping uint8 = 24
  44. XStatusPPC uint8 = 25
  45. XStatusMobile uint8 = 26
  46. XStatusLove uint8 = 27
  47. XStatusSearching uint8 = 28
  48. XStatusEvil uint8 = 29
  49. XStatusDepression uint8 = 30
  50. XStatusParty2 uint8 = 31
  51. XStatusCoffee2 uint8 = 32
  52. )
  53. // UnmangleXtrazXML decodes the HTML entity encoded XML used in Xtraz messages.
  54. // Xtraz uses HTML entity encoding for transport: < > & "
  55. func UnmangleXtrazXML(mangled string) string {
  56. return html.UnescapeString(mangled)
  57. }
  58. // MangleXtrazXML encodes XML for Xtraz transport using HTML entities.
  59. func MangleXtrazXML(plain string) string {
  60. return html.EscapeString(plain)
  61. }
  62. // XtrazNotifyRequest represents a parsed Xtraz notification request (<N> type).
  63. type XtrazNotifyRequest struct {
  64. PluginID string
  65. ServiceID string
  66. RequestID string
  67. TransID string // transaction ID
  68. SenderID string // sender's UIN
  69. }
  70. // XtrazNotifyResponse represents a parsed Xtraz notification response (<NR> type).
  71. type XtrazNotifyResponse struct {
  72. UIN string
  73. Index uint8
  74. Title string
  75. Message string
  76. }
  77. // xmlNotifyRequest is the internal XML structure for parsing <N> requests.
  78. type xmlNotifyRequest struct {
  79. XMLName xml.Name `xml:"N"`
  80. Query struct {
  81. PluginID string `xml:"PluginID"`
  82. } `xml:"QUERY"`
  83. Notify struct {
  84. Srv struct {
  85. ID string `xml:"id"`
  86. Req struct {
  87. ID string `xml:"id"`
  88. Trans string `xml:"trans"`
  89. SenderID string `xml:"senderId"`
  90. } `xml:"req"`
  91. } `xml:"srv"`
  92. } `xml:"NOTIFY"`
  93. }
  94. // xmlNotifyResponseRoot is the internal XML structure for the <Root> element in responses.
  95. type xmlNotifyResponseRoot struct {
  96. UIN string `xml:"uin"`
  97. Index uint8 `xml:"index"`
  98. Title string `xml:"title"`
  99. Desc string `xml:"desc"`
  100. }
  101. // ParseXtrazNotifyRequest parses an Xtraz notification request from XML.
  102. // The input is expected to be unmangled.
  103. func ParseXtrazNotifyRequest(xmlData []byte) (*XtrazNotifyRequest, error) {
  104. var req xmlNotifyRequest
  105. if err := xml.Unmarshal(xmlData, &req); err != nil {
  106. return nil, err
  107. }
  108. return &XtrazNotifyRequest{
  109. PluginID: req.Query.PluginID,
  110. ServiceID: req.Notify.Srv.ID,
  111. RequestID: req.Notify.Srv.Req.ID,
  112. TransID: req.Notify.Srv.Req.Trans,
  113. SenderID: req.Notify.Srv.Req.SenderID,
  114. }, nil
  115. }
  116. // ErrXtrazRootNotFound is returned when the Root element is not found in an Xtraz response.
  117. var ErrXtrazRootNotFound = errors.New("xtraz: Root element not found in response")
  118. // ParseXtrazNotifyResponse parses an Xtraz notification response from XML.
  119. // The input should be unmangled.
  120. func ParseXtrazNotifyResponse(xmlData []byte) (*XtrazNotifyResponse, error) {
  121. // The response XML has a nested structure, we need to extract the Root element
  122. xmlStr := string(xmlData)
  123. rootStart := strings.Index(xmlStr, "<Root>")
  124. rootEnd := strings.Index(xmlStr, "</Root>")
  125. if rootStart == -1 || rootEnd == -1 {
  126. return nil, ErrXtrazRootNotFound
  127. }
  128. rootXML := xmlStr[rootStart : rootEnd+len("</Root>")]
  129. var root xmlNotifyResponseRoot
  130. if err := xml.Unmarshal([]byte(rootXML), &root); err != nil {
  131. return nil, err
  132. }
  133. return &XtrazNotifyResponse{
  134. UIN: root.UIN,
  135. Index: root.Index,
  136. Title: root.Title,
  137. Message: root.Desc,
  138. }, nil
  139. }
  140. // BuildXtrazNotifyResponse builds an Xtraz notification response XML string.
  141. func BuildXtrazNotifyResponse(uin string, index uint8, title, message string) string {
  142. xmlStr := `<NR><RES><ret event="OnRemoteNotification"><srv><id></id>` +
  143. `<val srv_id="cAwaySrv"><Root><CASXtraSetAwayMessage></CASXtraSetAwayMessage>` +
  144. `<uin>` + uin + `</uin>` +
  145. `<index>` + strconv.Itoa(int(index)) + `</index>` +
  146. `<title>` + MangleXtrazXML(title) + `</title>` +
  147. `<desc>` + MangleXtrazXML(message) + `</desc>` +
  148. `</Root></val></srv></ret></RES></NR>`
  149. return MangleXtrazXML(xmlStr)
  150. }
  151. // BuildXtrazNotifyRequest builds an Xtraz notification request XML string.
  152. func BuildXtrazNotifyRequest(senderUIN string) string {
  153. xml := `<N><QUERY><PluginID>srvMng</PluginID></QUERY>` +
  154. `<NOTIFY><srv><id>cAwaySrv</id>` +
  155. `<req><id>AwayStat</id><trans>1</trans>` +
  156. `<senderId>` + senderUIN + `</senderId></req></srv></NOTIFY></N>`
  157. return MangleXtrazXML(xml)
  158. }
  159. // XStatus mood capability IDs, advertised alongside the regular capabilities to
  160. // indicate which mood icon a client is displaying.
  161. var (
  162. // CapXStatusThinking is the UUID for the "thinking" mood
  163. CapXStatusThinking = uuid.MustParse("3FB0BD36-AF3B-4A60-9EEF-CF190F6A5A7F")
  164. // CapXStatusBusy is the UUID for the "busy" mood
  165. CapXStatusBusy = uuid.MustParse("488E1489-8ACA-4A08-82AA-77CE7A165208")
  166. // CapXStatusShopping is the UUID for the "shopping" mood
  167. CapXStatusShopping = uuid.MustParse("63627337-A03F-49FF-80E5-F709CDE0A4EE")
  168. // CapXStatusTypingAlt is an alternate UUID for the "typing" mood that ICQ 6
  169. // does not set for its typewriter icon
  170. CapXStatusTypingAlt = uuid.MustParse("634F6BD8-ADD2-4AA1-AAB9-115BC26D05A1")
  171. // CapXStatusQuestion is the UUID for the "question" mood
  172. CapXStatusQuestion = uuid.MustParse("631436FF-3F8A-40D0-A5CB-7B66E051B364")
  173. // CapXStatusAngry is the UUID for the "angry" mood
  174. CapXStatusAngry = uuid.MustParse("01D8D7EE-AC3B-492A-A58D-D3D877E66B92")
  175. // CapXStatusPlate is the UUID for the "plate" (eating) mood
  176. CapXStatusPlate = uuid.MustParse("F8E8D7B2-82C4-4142-90F8-10C6CE0A89A6")
  177. // CapXStatusCinema is the UUID for the "cinema" mood
  178. CapXStatusCinema = uuid.MustParse("107A9A18-1232-4DA4-B6CD-0879DB780F09")
  179. // CapXStatusSick is the UUID for the "sick" mood
  180. CapXStatusSick = uuid.MustParse("1F7A4071-BF3B-4E60-BC32-4C5787B04CF1")
  181. // CapXStatusTyping is the UUID for the "typing" mood
  182. CapXStatusTyping = uuid.MustParse("2CE0E4E5-7C64-4370-9C3A-7A1CE878A7DC")
  183. // CapXStatusSuit is the UUID for the "suit" mood
  184. CapXStatusSuit = uuid.MustParse("B70867F5-3825-4327-A1FF-CF4CC1939797")
  185. // CapXStatusBathing is the UUID for the "bathing" mood
  186. CapXStatusBathing = uuid.MustParse("5A581EA1-E580-430C-A06F-612298B7E4C7")
  187. // CapXStatusTV is the UUID for the "tv" mood
  188. CapXStatusTV = uuid.MustParse("80537DE2-A467-4A76-B354-6DFD075F5EC6")
  189. // CapXStatusExcited is the UUID for the "excited" mood
  190. CapXStatusExcited = uuid.MustParse("6F493098-4F7C-4AFF-A276-34A03BCEAEA7")
  191. // CapXStatusSleeping is the UUID for the "sleeping" mood
  192. CapXStatusSleeping = uuid.MustParse("785E8C48-40D3-4C65-886F-04CF3F3F43DF")
  193. // CapXStatusHiptop is the UUID for the "hiptop" mood
  194. CapXStatusHiptop = uuid.MustParse("101117C9-A3B0-40F9-81AC-49E159FBD5D4")
  195. // CapXStatusInLove is the UUID for the "in love" mood
  196. CapXStatusInLove = uuid.MustParse("DDCF0EA9-7195-4048-A9C6-413206D6F280")
  197. // CapXStatusSleepy is the UUID for the "sleepy" mood
  198. CapXStatusSleepy = uuid.MustParse("83C9B78E-77E7-4378-B2C5-FB6CFCC35BEC")
  199. // CapXStatusMeeting is the UUID for the "meeting" mood
  200. CapXStatusMeeting = uuid.MustParse("F18AB52E-DC57-491D-99DC-6444502457AF")
  201. // CapXStatusPhone is the UUID for the "phone" mood
  202. CapXStatusPhone = uuid.MustParse("1292E550-1B64-4F66-B206-B29AF378E48D")
  203. // CapXStatusSurfing is the UUID for the "surfing" mood
  204. CapXStatusSurfing = uuid.MustParse("A6ED557E-6BF7-44D4-A5D4-D2E7D95CE81F")
  205. // CapXStatusMobile is the UUID for the "mobile" mood
  206. CapXStatusMobile = uuid.MustParse("160C60BB-DD44-43F3-9140-050F00E6C009")
  207. // CapXStatusSearch is the UUID for the "search" mood
  208. CapXStatusSearch = uuid.MustParse("D4E2B0BA-334E-4FA5-98D0-117DBF4D3CC8")
  209. // CapXStatusParty is the UUID for the "party" mood
  210. CapXStatusParty = uuid.MustParse("E601E41C-3373-4BD1-BC06-811D6C323D81")
  211. // CapXStatusCoffee is the UUID for the "coffee" mood
  212. CapXStatusCoffee = uuid.MustParse("1B78AE31-FA0B-4D38-93D1-997EEEAFB218")
  213. // CapXStatusConsole is the UUID for the "console" (gaming) mood
  214. CapXStatusConsole = uuid.MustParse("D4A611D0-8F01-4EC0-9223-C5B6BEC6CCF0")
  215. // CapXStatusInternet is the UUID for the "internet" mood
  216. CapXStatusInternet = uuid.MustParse("12D07E3E-F885-489E-8E97-A72A6551E58D")
  217. // CapXStatusCigarette is the UUID for the "cigarette" mood
  218. CapXStatusCigarette = uuid.MustParse("6443C6AF-2260-4517-B58C-D7DF8E290352")
  219. // CapXStatusWriting is the UUID for the "writing" mood
  220. CapXStatusWriting = uuid.MustParse("0072D908-4AD1-43DD-9199-6F026966026F")
  221. // CapXStatusBeer is the UUID for the "beer" mood
  222. CapXStatusBeer = uuid.MustParse("8C50DBAE-81ED-4786-ACCA-16CC3213C7B7")
  223. // CapXStatusMusic is the UUID for the "music" mood
  224. CapXStatusMusic = uuid.MustParse("61BEE0DD-8BDD-475D-8DEE-5F4BAACF19A7")
  225. // CapXStatusStudying is the UUID for the "studying" mood
  226. CapXStatusStudying = uuid.MustParse("609D52F8-A29A-49A6-B2A0-2524C5E9D260")
  227. // CapXStatusWorking is the UUID for the "working" mood
  228. CapXStatusWorking = uuid.MustParse("BA74DB3E-9E24-434B-87B6-2F6B8DFEE50F")
  229. // CapXStatusRestroom is the UUID for the "restroom" mood
  230. CapXStatusRestroom = uuid.MustParse("16F5B76F-A9D2-4035-8CC5-C084703C98FA")
  231. )
  232. // A mood is a capability UUID ICQ clients advertise; Web API clients name the
  233. // same moods with a token string.
  234. // Invented capabilities for the four moods ICQ never assigned a UUID to, so at
  235. // least clients of this server resolve them. Namespaced under "MOOD" in ASCII
  236. // and ending in the mood number, they cannot collide with a genuine capability.
  237. var (
  238. CapMoodHavingFun = uuid.MustParse("4D4F4F44-0000-0000-0000-00000000000D")
  239. CapMoodLove = uuid.MustParse("4D4F4F44-0000-0000-0000-00000000003D")
  240. CapMoodWeekend = uuid.MustParse("4D4F4F44-0000-0000-0000-000000000042")
  241. CapMoodOnTheWay = uuid.MustParse("4D4F4F44-0000-0000-0000-000000000053")
  242. )
  243. // Mood is one ICQ XStatus mood, in both of the forms it travels in.
  244. type Mood struct {
  245. ID string // the token Web API clients exchange, e.g. "0icqmood6"
  246. Cap uuid.UUID // the capability OSCAR clients advertise
  247. }
  248. // Moods lists every mood in the order ICQ web clients enumerate them. Where two
  249. // moods share a capability, the one listed first is its canonical name.
  250. var Moods = []Mood{
  251. {ID: "0icqmood0", Cap: CapXStatusShopping}, // shopping
  252. {ID: "0icqmood1", Cap: CapXStatusBathing}, // bathing
  253. {ID: "0icqmood2", Cap: CapXStatusSleepy}, // tired
  254. {ID: "0icqmood3", Cap: CapXStatusParty}, // party
  255. {ID: "0icqmood4", Cap: CapXStatusBeer}, // beer
  256. {ID: "0icqmood5", Cap: CapXStatusThinking}, // thinking
  257. {ID: "0icqmood6", Cap: CapXStatusPlate}, // eating
  258. {ID: "0icqmood7", Cap: CapXStatusTV}, // tv
  259. {ID: "0icqmood8", Cap: CapXStatusMeeting}, // friends
  260. {ID: "0icqmood9", Cap: CapXStatusCoffee}, // coffee
  261. {ID: "0icqmood10", Cap: CapXStatusMusic}, // music
  262. {ID: "0icqmood11", Cap: CapXStatusSuit}, // business
  263. {ID: "0icqmood12", Cap: CapXStatusCinema}, // cinema
  264. {ID: "0icqmood13", Cap: CapMoodHavingFun}, // having fun
  265. {ID: "0icqmood14", Cap: CapXStatusPhone}, // phone
  266. {ID: "0icqmood81", Cap: CapXStatusConsole}, // gamepad
  267. {ID: "0icqmood16", Cap: CapXStatusStudying}, // studying
  268. {ID: "0icqmood17", Cap: CapXStatusSick}, // sick
  269. {ID: "0icqmood70", Cap: CapXStatusSleeping}, // sleeping
  270. {ID: "0icqmood19", Cap: CapXStatusSurfing}, // surfing
  271. {ID: "0icqmood20", Cap: CapXStatusInternet}, // internet
  272. {ID: "0icqmood21", Cap: CapXStatusWorking}, // working
  273. {ID: "0icqmood22", Cap: CapXStatusTyping}, // typing
  274. {ID: "0icqmood23", Cap: CapXStatusAngry}, // angry
  275. {ID: "0icqmood66", Cap: CapMoodWeekend}, // weekend
  276. {ID: "0icqmood15", Cap: CapXStatusConsole}, // PSP
  277. {ID: "0icqmood71", Cap: CapXStatusMobile}, // on mobile
  278. {ID: "0icqmood18", Cap: CapXStatusSleeping}, // fall asleep
  279. {ID: "0icqmood68", Cap: CapXStatusRestroom}, // WC
  280. {ID: "0icqmood77", Cap: CapXStatusQuestion}, // confused
  281. {ID: "0icqmood83", Cap: CapMoodOnTheWay}, // on the way
  282. {ID: "0icqmood61", Cap: CapMoodLove}, // love
  283. {ID: "0icqmood76", Cap: CapXStatusInLove}, // in love
  284. {ID: "0icqmood85", Cap: CapXStatusSearch}, // searching
  285. {ID: "0icqmood84", Cap: CapXStatusWriting}, // diary
  286. }
  287. var (
  288. moodsByID = indexMoodsByID()
  289. moodsByCap = indexMoodsByCap()
  290. )
  291. func indexMoodsByID() map[string]Mood {
  292. index := make(map[string]Mood, len(Moods))
  293. for _, m := range Moods {
  294. index[m.ID] = m
  295. }
  296. return index
  297. }
  298. func indexMoodsByCap() map[uuid.UUID]Mood {
  299. index := make(map[uuid.UUID]Mood, len(Moods))
  300. for _, m := range Moods {
  301. if _, taken := index[m.Cap]; !taken {
  302. index[m.Cap] = m
  303. }
  304. }
  305. return index
  306. }
  307. // MoodByID returns the mood named by a Web API mood token.
  308. func MoodByID(id string) (Mood, bool) {
  309. m, ok := moodsByID[id]
  310. return m, ok
  311. }
  312. // MoodByCap returns the mood advertised by a capability UUID.
  313. func MoodByCap(c [16]byte) (Mood, bool) {
  314. m, ok := moodsByCap[uuid.UUID(c)]
  315. return m, ok
  316. }
  317. // IsMoodCap reports whether c is a known mood capability.
  318. func IsMoodCap(c [16]byte) bool {
  319. _, ok := MoodByCap(c)
  320. return ok
  321. }
  322. // MoodIconID encodes a mood token for the id parameter of a mood icon URL: hex
  323. // of a two-byte big-endian length followed by the token, which is how ICQ
  324. // clients read it. An unencodable token yields "", which clients read as no mood.
  325. func MoodIconID(id string) string {
  326. if id == "" || len(id) > math.MaxUint16 {
  327. return ""
  328. }
  329. return fmt.Sprintf("%04x%s", len(id), hex.EncodeToString([]byte(id)))
  330. }