handler.go 35 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180
  1. package googlereader // import "miniflux.app/googlereader"
  2. import (
  3. "errors"
  4. "fmt"
  5. "net/http"
  6. "net/http/httputil"
  7. "strconv"
  8. "strings"
  9. "time"
  10. "github.com/gorilla/mux"
  11. "miniflux.app/config"
  12. "miniflux.app/http/request"
  13. "miniflux.app/http/response/json"
  14. "miniflux.app/http/route"
  15. "miniflux.app/integration"
  16. "miniflux.app/logger"
  17. "miniflux.app/model"
  18. mff "miniflux.app/reader/handler"
  19. mfs "miniflux.app/reader/subscription"
  20. "miniflux.app/storage"
  21. "miniflux.app/validator"
  22. )
  23. type handler struct {
  24. store *storage.Storage
  25. router *mux.Router
  26. }
  27. const (
  28. // StreamPrefix is the prefix for astreams (read/starred/reading list and so on)
  29. StreamPrefix = "user/-/state/com.google/"
  30. // UserStreamPrefix is the user specific prefix for streams (read/starred/reading list and so on)
  31. UserStreamPrefix = "user/%d/state/com.google/"
  32. // LabelPrefix is the prefix for a label stream
  33. LabelPrefix = "user/-/label/"
  34. // UserLabelPrefix is the user specific prefix prefix for a label stream
  35. UserLabelPrefix = "user/%d/label/"
  36. // FeedPrefix is the prefix for a feed stream
  37. FeedPrefix = "feed/"
  38. // Read is the suffix for read stream
  39. Read = "read"
  40. // Starred is the suffix for starred stream
  41. Starred = "starred"
  42. // ReadingList is the suffix for reading list stream
  43. ReadingList = "reading-list"
  44. // KeptUnread is the suffix for kept unread stream
  45. KeptUnread = "kept-unread"
  46. // Broadcast is the suffix for broadcast stream
  47. Broadcast = "broadcast"
  48. // BroadcastFriends is the suffix for broadcast friends stream
  49. BroadcastFriends = "broadcast-friends"
  50. // Like is the suffix for like stream
  51. Like = "like"
  52. // EntryIDLong is the long entry id representation
  53. EntryIDLong = "tag:google.com,2005:reader/item/%016x"
  54. )
  55. const (
  56. // ParamItemIDs - name of the parameter with the item ids
  57. ParamItemIDs = "i"
  58. // ParamStreamID - name of the parameter containing the stream to be included
  59. ParamStreamID = "s"
  60. // ParamStreamExcludes - name of the parameter containing streams to be excluded
  61. ParamStreamExcludes = "xt"
  62. // ParamStreamFilters - name of the parameter containing streams to be included
  63. ParamStreamFilters = "it"
  64. // ParamStreamMaxItems - name of the parameter containing number of items per page/max items returned
  65. ParamStreamMaxItems = "n"
  66. // ParamStreamOrder - name of the parameter containing the sort criteria
  67. ParamStreamOrder = "r"
  68. // ParamStreamStartTime - name of the parameter containing epoch timestamp, filtering items older than
  69. ParamStreamStartTime = "ot"
  70. // ParamStreamStopTime - name of the parameter containing epoch timestamp, filtering items newer than
  71. ParamStreamStopTime = "nt"
  72. // ParamTagsRemove - name of the parameter containing tags (streams) to be removed
  73. ParamTagsRemove = "r"
  74. // ParamTagsAdd - name of the parameter containing tags (streams) to be added
  75. ParamTagsAdd = "a"
  76. // ParamSubscribeAction - name of the parameter indicating the action to take for subscription/edit
  77. ParamSubscribeAction = "ac"
  78. // ParamTitle - name of the parameter for the title of the subscription
  79. ParamTitle = "t"
  80. // ParamQuickAdd - name of the parameter for a URL being quick subscribed to
  81. ParamQuickAdd = "quickadd"
  82. // ParamDestination - name fo the parameter for the new name of a tag
  83. ParamDestination = "dest"
  84. )
  85. // StreamType represents the possible stream types
  86. type StreamType int
  87. const (
  88. // NoStream - no stream type
  89. NoStream StreamType = iota
  90. // ReadStream - read stream type
  91. ReadStream
  92. // StarredStream - starred stream type
  93. StarredStream
  94. // ReadingListStream - reading list stream type
  95. ReadingListStream
  96. // KeptUnreadStream - kept unread stream type
  97. KeptUnreadStream
  98. // BroadcastStream - broadcast stream type
  99. BroadcastStream
  100. // BroadcastFriendsStream - broadcast friends stream type
  101. BroadcastFriendsStream
  102. // LabelStream - label stream type
  103. LabelStream
  104. // FeedStream - feed stream type
  105. FeedStream
  106. // LikeStream - like stream type
  107. LikeStream
  108. )
  109. // Stream defines a stream type and its id
  110. type Stream struct {
  111. Type StreamType
  112. ID string
  113. }
  114. // RequestModifiers are the parsed request parameters
  115. type RequestModifiers struct {
  116. ExcludeTargets []Stream
  117. FilterTargets []Stream
  118. Streams []Stream
  119. Count int
  120. SortDirection string
  121. StartTime int64
  122. StopTime int64
  123. ContinuationToken string
  124. UserID int64
  125. }
  126. func (st StreamType) String() string {
  127. switch st {
  128. case NoStream:
  129. return "NoStream"
  130. case ReadStream:
  131. return "ReadStream"
  132. case StarredStream:
  133. return "StarredStream"
  134. case ReadingListStream:
  135. return "ReadingListStream"
  136. case KeptUnreadStream:
  137. return "KeptUnreadStream"
  138. case BroadcastStream:
  139. return "BroadcastStream"
  140. case BroadcastFriendsStream:
  141. return "BroadcastFriendsStream"
  142. case LabelStream:
  143. return "LabelStream"
  144. case FeedStream:
  145. return "FeedStream"
  146. case LikeStream:
  147. return "LikeStream"
  148. default:
  149. return st.String()
  150. }
  151. }
  152. func (s Stream) String() string {
  153. return fmt.Sprintf("%v - '%s'", s.Type, s.ID)
  154. }
  155. func (r RequestModifiers) String() string {
  156. result := fmt.Sprintf("UserID: %d\n", r.UserID)
  157. result += fmt.Sprintf("Streams: %d\n", len(r.Streams))
  158. for _, s := range r.Streams {
  159. result += fmt.Sprintf(" %v\n", s)
  160. }
  161. result += fmt.Sprintf("Exclusions: %d\n", len(r.ExcludeTargets))
  162. for _, s := range r.ExcludeTargets {
  163. result += fmt.Sprintf(" %v\n", s)
  164. }
  165. result += fmt.Sprintf("Filter: %d\n", len(r.FilterTargets))
  166. for _, s := range r.FilterTargets {
  167. result += fmt.Sprintf(" %v\n", s)
  168. }
  169. result += fmt.Sprintf("Count: %d\n", r.Count)
  170. result += fmt.Sprintf("Sort Direction: %s\n", r.SortDirection)
  171. result += fmt.Sprintf("Continuation Token: %s\n", r.ContinuationToken)
  172. result += fmt.Sprintf("Start Time: %d\n", r.StartTime)
  173. result += fmt.Sprintf("Stop Time: %d\n", r.StopTime)
  174. return result
  175. }
  176. // Serve handles Google Reader API calls.
  177. func Serve(router *mux.Router, store *storage.Storage) {
  178. handler := &handler{store, router}
  179. middleware := newMiddleware(store)
  180. router.HandleFunc("/accounts/ClientLogin", middleware.clientLogin).Methods(http.MethodPost).Name("ClientLogin")
  181. sr := router.PathPrefix("/reader/api/0").Subrouter()
  182. sr.Use(middleware.handleCORS)
  183. sr.Use(middleware.apiKeyAuth)
  184. sr.Methods(http.MethodOptions)
  185. sr.HandleFunc("/token", middleware.token).Methods(http.MethodGet).Name("Token")
  186. sr.HandleFunc("/edit-tag", handler.editTag).Methods(http.MethodPost).Name("EditTag")
  187. sr.HandleFunc("/rename-tag", handler.renameTag).Methods(http.MethodPost).Name("Rename Tag")
  188. sr.HandleFunc("/disable-tag", handler.disableTag).Methods(http.MethodPost).Name("Disable Tag")
  189. sr.HandleFunc("/tag/list", handler.tagList).Methods(http.MethodGet).Name("TagList")
  190. sr.HandleFunc("/user-info", handler.userInfo).Methods(http.MethodGet).Name("UserInfo")
  191. sr.HandleFunc("/subscription/list", handler.subscriptionList).Methods(http.MethodGet).Name("SubscriptonList")
  192. sr.HandleFunc("/subscription/edit", handler.editSubscription).Methods(http.MethodPost).Name("SubscriptionEdit")
  193. sr.HandleFunc("/subscription/quickadd", handler.quickAdd).Methods(http.MethodPost).Name("QuickAdd")
  194. sr.HandleFunc("/stream/items/ids", handler.streamItemIDs).Methods(http.MethodGet).Name("StreamItemIDs")
  195. sr.HandleFunc("/stream/items/contents", handler.streamItemContents).Methods(http.MethodPost).Name("StreamItemsContents")
  196. sr.PathPrefix("/").HandlerFunc(handler.serve).Methods(http.MethodPost, http.MethodGet).Name("GoogleReaderApiEndpoint")
  197. }
  198. func getStreamFilterModifiers(r *http.Request) (RequestModifiers, error) {
  199. userID := request.UserID(r)
  200. result := RequestModifiers{
  201. SortDirection: "desc",
  202. UserID: userID,
  203. }
  204. streamOrder := request.QueryStringParam(r, ParamStreamOrder, "d")
  205. if streamOrder == "o" {
  206. result.SortDirection = "asc"
  207. }
  208. var err error
  209. result.Streams, err = getStreams(request.QueryStringParamList(r, ParamStreamID), userID)
  210. if err != nil {
  211. return RequestModifiers{}, err
  212. }
  213. result.ExcludeTargets, err = getStreams(request.QueryStringParamList(r, ParamStreamExcludes), userID)
  214. if err != nil {
  215. return RequestModifiers{}, err
  216. }
  217. result.FilterTargets, err = getStreams(request.QueryStringParamList(r, ParamStreamFilters), userID)
  218. if err != nil {
  219. return RequestModifiers{}, err
  220. }
  221. result.Count = request.QueryIntParam(r, ParamStreamMaxItems, 0)
  222. result.StartTime = int64(request.QueryIntParam(r, ParamStreamStartTime, 0))
  223. result.StopTime = int64(request.QueryIntParam(r, ParamStreamStopTime, 0))
  224. return result, nil
  225. }
  226. func getStream(streamID string, userID int64) (Stream, error) {
  227. if strings.HasPrefix(streamID, FeedPrefix) {
  228. return Stream{Type: FeedStream, ID: strings.TrimPrefix(streamID, FeedPrefix)}, nil
  229. } else if strings.HasPrefix(streamID, fmt.Sprintf(UserStreamPrefix, userID)) || strings.HasPrefix(streamID, StreamPrefix) {
  230. id := strings.TrimPrefix(streamID, fmt.Sprintf(UserStreamPrefix, userID))
  231. id = strings.TrimPrefix(id, StreamPrefix)
  232. switch id {
  233. case Read:
  234. return Stream{ReadStream, ""}, nil
  235. case Starred:
  236. return Stream{StarredStream, ""}, nil
  237. case ReadingList:
  238. return Stream{ReadingListStream, ""}, nil
  239. case KeptUnread:
  240. return Stream{KeptUnreadStream, ""}, nil
  241. case Broadcast:
  242. return Stream{BroadcastStream, ""}, nil
  243. case BroadcastFriends:
  244. return Stream{BroadcastFriendsStream, ""}, nil
  245. case Like:
  246. return Stream{LikeStream, ""}, nil
  247. default:
  248. err := fmt.Errorf("uknown stream with id: %s", id)
  249. return Stream{NoStream, ""}, err
  250. }
  251. } else if strings.HasPrefix(streamID, fmt.Sprintf(UserLabelPrefix, userID)) || strings.HasPrefix(streamID, LabelPrefix) {
  252. id := strings.TrimPrefix(streamID, fmt.Sprintf(UserLabelPrefix, userID))
  253. id = strings.TrimPrefix(id, LabelPrefix)
  254. return Stream{LabelStream, id}, nil
  255. } else if streamID == "" {
  256. return Stream{NoStream, ""}, nil
  257. }
  258. err := fmt.Errorf("uknown stream type: %s", streamID)
  259. return Stream{NoStream, ""}, err
  260. }
  261. func getStreams(streamIDs []string, userID int64) ([]Stream, error) {
  262. streams := make([]Stream, 0)
  263. for _, streamID := range streamIDs {
  264. stream, err := getStream(streamID, userID)
  265. if err != nil {
  266. return []Stream{}, err
  267. }
  268. streams = append(streams, stream)
  269. }
  270. return streams, nil
  271. }
  272. func checkAndSimplifyTags(addTags []Stream, removeTags []Stream) (map[StreamType]bool, error) {
  273. tags := make(map[StreamType]bool)
  274. for _, s := range addTags {
  275. switch s.Type {
  276. case ReadStream:
  277. if _, ok := tags[ReadStream]; ok {
  278. return nil, fmt.Errorf(KeptUnread + " and " + Read + " should not be supplied simultaneously")
  279. }
  280. tags[ReadStream] = true
  281. case KeptUnreadStream:
  282. if _, ok := tags[ReadStream]; ok {
  283. return nil, fmt.Errorf(KeptUnread + " and " + Read + " should not be supplied simultaneously")
  284. }
  285. tags[ReadStream] = false
  286. case StarredStream:
  287. tags[StarredStream] = true
  288. case BroadcastStream, LikeStream:
  289. logger.Info("Broadcast & Like tags are not implemented!")
  290. default:
  291. return nil, fmt.Errorf("unsupported tag type: %s", s.Type)
  292. }
  293. }
  294. for _, s := range removeTags {
  295. switch s.Type {
  296. case ReadStream:
  297. if _, ok := tags[ReadStream]; ok {
  298. return nil, fmt.Errorf(KeptUnread + " and " + Read + " should not be supplied simultaneously")
  299. }
  300. tags[ReadStream] = false
  301. case KeptUnreadStream:
  302. if _, ok := tags[ReadStream]; ok {
  303. return nil, fmt.Errorf(KeptUnread + " and " + Read + " should not be supplied simultaneously")
  304. }
  305. tags[ReadStream] = true
  306. case StarredStream:
  307. if _, ok := tags[StarredStream]; ok {
  308. return nil, fmt.Errorf(Starred + " should not be supplied for add and remove simultaneously")
  309. }
  310. tags[StarredStream] = false
  311. case BroadcastStream, LikeStream:
  312. logger.Info("Broadcast & Like tags are not implemented!")
  313. default:
  314. return nil, fmt.Errorf("unsupported tag type: %s", s.Type)
  315. }
  316. }
  317. return tags, nil
  318. }
  319. func getItemIDs(r *http.Request) ([]int64, error) {
  320. items := r.Form[ParamItemIDs]
  321. if len(items) == 0 {
  322. return nil, fmt.Errorf("no items requested")
  323. }
  324. itemIDs := make([]int64, len(items))
  325. for i, item := range items {
  326. var itemID int64
  327. _, err := fmt.Sscanf(item, EntryIDLong, &itemID)
  328. if err != nil {
  329. itemID, err = strconv.ParseInt(item, 16, 64)
  330. if err != nil {
  331. return nil, fmt.Errorf("could not parse item: %v", item)
  332. }
  333. }
  334. itemIDs[i] = itemID
  335. }
  336. return itemIDs, nil
  337. }
  338. func checkOutputFormat(w http.ResponseWriter, r *http.Request) error {
  339. var output string
  340. if r.Method == http.MethodPost {
  341. err := r.ParseForm()
  342. if err != nil {
  343. return err
  344. }
  345. output = r.Form.Get("output")
  346. } else {
  347. output = request.QueryStringParam(r, "output", "")
  348. }
  349. if output != "json" {
  350. err := fmt.Errorf("output only as json supported")
  351. return err
  352. }
  353. return nil
  354. }
  355. func (h *handler) editTag(w http.ResponseWriter, r *http.Request) {
  356. userID := request.UserID(r)
  357. clientIP := request.ClientIP(r)
  358. logger.Info("[Reader][/edit-tag][ClientIP=%s] Incoming Request for userID #%d", clientIP, userID)
  359. err := r.ParseForm()
  360. if err != nil {
  361. logger.Error("[Reader][/edit-tag] [ClientIP=%s] %v", clientIP, err)
  362. json.ServerError(w, r, err)
  363. return
  364. }
  365. addTags, err := getStreams(r.PostForm[ParamTagsAdd], userID)
  366. if err != nil {
  367. logger.Error("[Reader][/edit-tag] [ClientIP=%s] %v", clientIP, err)
  368. json.ServerError(w, r, err)
  369. return
  370. }
  371. removeTags, err := getStreams(r.PostForm[ParamTagsRemove], userID)
  372. if err != nil {
  373. logger.Error("[Reader][/edit-tag] [ClientIP=%s] %v", clientIP, err)
  374. json.ServerError(w, r, err)
  375. return
  376. }
  377. if len(addTags) == 0 && len(removeTags) == 0 {
  378. err = fmt.Errorf("add or/and remove tags should be supllied")
  379. logger.Error("[Reader][/edit-tag] [ClientIP=%s] ", clientIP, err)
  380. json.ServerError(w, r, err)
  381. return
  382. }
  383. tags, err := checkAndSimplifyTags(addTags, removeTags)
  384. if err != nil {
  385. logger.Error("[Reader][/edit-tag] [ClientIP=%s] %v", clientIP, err)
  386. json.ServerError(w, r, err)
  387. return
  388. }
  389. itemIDs, err := getItemIDs(r)
  390. if err != nil {
  391. logger.Error("[Reader][/edit-tag] [ClientIP=%s] %v", clientIP, err)
  392. json.ServerError(w, r, err)
  393. return
  394. }
  395. logger.Debug("[Reader][/edit-tag] [ClientIP=%s] itemIDs: %v", clientIP, itemIDs)
  396. logger.Debug("[Reader][/edit-tag] [ClientIP=%s] tags: %v", clientIP, tags)
  397. builder := h.store.NewEntryQueryBuilder(userID)
  398. builder.WithEntryIDs(itemIDs)
  399. builder.WithoutStatus(model.EntryStatusRemoved)
  400. entries, err := builder.GetEntries()
  401. if err != nil {
  402. logger.Error("[Reader][/edit-tag] [ClientIP=%s] %v", clientIP, err)
  403. json.ServerError(w, r, err)
  404. return
  405. }
  406. n := 0
  407. readEntryIDs := make([]int64, 0)
  408. unreadEntryIDs := make([]int64, 0)
  409. starredEntryIDs := make([]int64, 0)
  410. unstarredEntryIDs := make([]int64, 0)
  411. for _, entry := range entries {
  412. if read, exists := tags[ReadStream]; exists {
  413. if read && entry.Status == model.EntryStatusUnread {
  414. readEntryIDs = append(readEntryIDs, entry.ID)
  415. } else if entry.Status == model.EntryStatusRead {
  416. unreadEntryIDs = append(unreadEntryIDs, entry.ID)
  417. }
  418. }
  419. if starred, exists := tags[StarredStream]; exists {
  420. if starred && !entry.Starred {
  421. starredEntryIDs = append(starredEntryIDs, entry.ID)
  422. // filter the original array
  423. entries[n] = entry
  424. n++
  425. } else if entry.Starred {
  426. unstarredEntryIDs = append(unstarredEntryIDs, entry.ID)
  427. }
  428. }
  429. }
  430. entries = entries[:n]
  431. if len(readEntryIDs) > 0 {
  432. err = h.store.SetEntriesStatus(userID, readEntryIDs, model.EntryStatusRead)
  433. if err != nil {
  434. logger.Error("[Reader][/edit-tag] [ClientIP=%s] %v", clientIP, err)
  435. json.ServerError(w, r, err)
  436. return
  437. }
  438. }
  439. if len(unreadEntryIDs) > 0 {
  440. err = h.store.SetEntriesStatus(userID, unreadEntryIDs, model.EntryStatusUnread)
  441. if err != nil {
  442. logger.Error("[Reader][/edit-tag] [ClientIP=%s] %v", clientIP, err)
  443. json.ServerError(w, r, err)
  444. return
  445. }
  446. }
  447. if len(unstarredEntryIDs) > 0 {
  448. err = h.store.SetEntriesBookmarkedState(userID, unstarredEntryIDs, true)
  449. if err != nil {
  450. logger.Error("[Reader][/edit-tag] [ClientIP=%s] %v", clientIP, err)
  451. json.ServerError(w, r, err)
  452. return
  453. }
  454. }
  455. if len(starredEntryIDs) > 0 {
  456. err = h.store.SetEntriesBookmarkedState(userID, starredEntryIDs, true)
  457. if err != nil {
  458. logger.Error("[Reader][/edit-tag] [ClientIP=%s] %v", clientIP, err)
  459. json.ServerError(w, r, err)
  460. return
  461. }
  462. }
  463. if len(entries) > 0 {
  464. settings, err := h.store.Integration(userID)
  465. if err != nil {
  466. logger.Error("[Reader][/edit-tag] [ClientIP=%s] %v", clientIP, err)
  467. json.ServerError(w, r, err)
  468. return
  469. }
  470. for _, entry := range entries {
  471. e := entry
  472. go func() {
  473. integration.SendEntry(e, settings)
  474. }()
  475. }
  476. }
  477. OK(w, r)
  478. }
  479. func (h *handler) quickAdd(w http.ResponseWriter, r *http.Request) {
  480. userID := request.UserID(r)
  481. clientIP := request.ClientIP(r)
  482. logger.Info("[Reader][/subscription/quickadd][ClientIP=%s] Incoming Request for userID #%d", clientIP, userID)
  483. err := r.ParseForm()
  484. if err != nil {
  485. logger.Error("[Reader][/subscription/quickadd] [ClientIP=%s] %v", clientIP, err)
  486. json.BadRequest(w, r, err)
  487. return
  488. }
  489. url := r.Form.Get(ParamQuickAdd)
  490. if !validator.IsValidURL(url) {
  491. json.BadRequest(w, r, fmt.Errorf("invalid URL: %s", url))
  492. return
  493. }
  494. subscriptions, s_err := mfs.FindSubscriptions(url, "", "", "", "", false, false)
  495. if s_err != nil {
  496. json.ServerError(w, r, s_err)
  497. return
  498. }
  499. if len(subscriptions) == 0 {
  500. json.OK(w, r, quickAddResponse{
  501. NumResults: 0,
  502. })
  503. return
  504. }
  505. toSubscribe := Stream{FeedStream, subscriptions[0].URL}
  506. category := Stream{NoStream, ""}
  507. newFeed, err := subscribe(toSubscribe, category, "", h.store, userID)
  508. if err != nil {
  509. json.ServerError(w, r, err)
  510. return
  511. }
  512. json.OK(w, r, quickAddResponse{
  513. NumResults: 1,
  514. Query: newFeed.FeedURL,
  515. StreamID: fmt.Sprintf(FeedPrefix+"%d", newFeed.ID),
  516. StreamName: newFeed.Title,
  517. })
  518. }
  519. func getFeed(stream Stream, store *storage.Storage, userID int64) (*model.Feed, error) {
  520. feedID, err := strconv.ParseInt(stream.ID, 10, 64)
  521. if err != nil {
  522. return nil, err
  523. }
  524. return store.FeedByID(userID, feedID)
  525. }
  526. func getOrCreateCategory(category Stream, store *storage.Storage, userID int64) (*model.Category, error) {
  527. if category.ID == "" {
  528. return store.FirstCategory(userID)
  529. } else if store.CategoryTitleExists(userID, category.ID) {
  530. return store.CategoryByTitle(userID, category.ID)
  531. } else {
  532. catRequest := model.CategoryRequest{
  533. Title: category.ID,
  534. }
  535. return store.CreateCategory(userID, &catRequest)
  536. }
  537. }
  538. func subscribe(newFeed Stream, category Stream, title string, store *storage.Storage, userID int64) (*model.Feed, error) {
  539. destCategory, err := getOrCreateCategory(category, store, userID)
  540. if err != nil {
  541. return nil, err
  542. }
  543. feedRequest := model.FeedCreationRequest{
  544. FeedURL: newFeed.ID,
  545. CategoryID: destCategory.ID,
  546. }
  547. verr := validator.ValidateFeedCreation(store, userID, &feedRequest)
  548. if verr != nil {
  549. return nil, verr.Error()
  550. }
  551. created, err := mff.CreateFeed(store, userID, &feedRequest)
  552. if err != nil {
  553. return nil, err
  554. }
  555. if title != "" {
  556. feedModification := model.FeedModificationRequest{
  557. Title: &title,
  558. }
  559. feedModification.Patch(created)
  560. if err := store.UpdateFeed(created); err != nil {
  561. return nil, err
  562. }
  563. }
  564. return created, nil
  565. }
  566. func unsubscribe(streams []Stream, store *storage.Storage, userID int64) error {
  567. for _, stream := range streams {
  568. feedID, err := strconv.ParseInt(stream.ID, 10, 64)
  569. if err != nil {
  570. return err
  571. }
  572. err = store.RemoveFeed(userID, feedID)
  573. if err != nil {
  574. return err
  575. }
  576. }
  577. return nil
  578. }
  579. func rename(stream Stream, title string, store *storage.Storage, userID int64) error {
  580. if title == "" {
  581. return errors.New("empty title")
  582. }
  583. feed, err := getFeed(stream, store, userID)
  584. if err != nil {
  585. return err
  586. }
  587. feedModification := model.FeedModificationRequest{
  588. Title: &title,
  589. }
  590. feedModification.Patch(feed)
  591. return store.UpdateFeed(feed)
  592. }
  593. func move(stream Stream, destination Stream, store *storage.Storage, userID int64) error {
  594. feed, err := getFeed(stream, store, userID)
  595. if err != nil {
  596. return err
  597. }
  598. category, err := getOrCreateCategory(destination, store, userID)
  599. if err != nil {
  600. return err
  601. }
  602. feedModification := model.FeedModificationRequest{
  603. CategoryID: &category.ID,
  604. }
  605. feedModification.Patch(feed)
  606. return store.UpdateFeed(feed)
  607. }
  608. func (h *handler) editSubscription(w http.ResponseWriter, r *http.Request) {
  609. userID := request.UserID(r)
  610. clientIP := request.ClientIP(r)
  611. logger.Info("[Reader][/subscription/edit][ClientIP=%s] Incoming Request for userID #%d", clientIP, userID)
  612. err := r.ParseForm()
  613. if err != nil {
  614. logger.Error("[Reader][/subscription/edit] [ClientIP=%s] %v", clientIP, err)
  615. json.BadRequest(w, r, err)
  616. return
  617. }
  618. streamIds, err := getStreams(r.Form[ParamStreamID], userID)
  619. if err != nil || len(streamIds) == 0 {
  620. json.BadRequest(w, r, errors.New("no valid stream IDs provided"))
  621. return
  622. }
  623. newLabel, err := getStream(r.Form.Get(ParamTagsAdd), userID)
  624. if err != nil {
  625. json.BadRequest(w, r, fmt.Errorf("invalid data in %s", ParamTagsAdd))
  626. return
  627. }
  628. title := r.Form.Get(ParamTitle)
  629. action := r.Form.Get(ParamSubscribeAction)
  630. switch action {
  631. case "subscribe":
  632. _, err := subscribe(streamIds[0], newLabel, title, h.store, userID)
  633. if err != nil {
  634. json.ServerError(w, r, err)
  635. return
  636. }
  637. case "unsubscribe":
  638. err := unsubscribe(streamIds, h.store, userID)
  639. if err != nil {
  640. json.ServerError(w, r, err)
  641. return
  642. }
  643. case "edit":
  644. if title != "" {
  645. err := rename(streamIds[0], title, h.store, userID)
  646. if err != nil {
  647. json.ServerError(w, r, err)
  648. return
  649. }
  650. } else {
  651. if newLabel.Type != LabelStream {
  652. json.BadRequest(w, r, errors.New("destination must be a label"))
  653. return
  654. }
  655. err := move(streamIds[0], newLabel, h.store, userID)
  656. if err != nil {
  657. json.ServerError(w, r, err)
  658. return
  659. }
  660. }
  661. default:
  662. json.ServerError(w, r, fmt.Errorf("unrecognized action %s", action))
  663. return
  664. }
  665. OK(w, r)
  666. }
  667. func (h *handler) streamItemContents(w http.ResponseWriter, r *http.Request) {
  668. userID := request.UserID(r)
  669. clientIP := request.ClientIP(r)
  670. logger.Info("[Reader][/stream/items/contents][ClientIP=%s] Incoming Request for userID #%d", clientIP, userID)
  671. if err := checkOutputFormat(w, r); err != nil {
  672. logger.Error("[Reader][/stream/items/contents] [ClientIP=%s] %v", clientIP, err)
  673. json.ServerError(w, r, err)
  674. return
  675. }
  676. err := r.ParseForm()
  677. if err != nil {
  678. logger.Error("[Reader][/stream/items/contents] [ClientIP=%s] %v", clientIP, err)
  679. json.ServerError(w, r, err)
  680. return
  681. }
  682. var user *model.User
  683. if user, err = h.store.UserByID(userID); err != nil {
  684. logger.Error("[Reader][/stream/items/contents] [ClientIP=%s] %v", clientIP, err)
  685. json.ServerError(w, r, err)
  686. return
  687. }
  688. requestModifiers, err := getStreamFilterModifiers(r)
  689. if err != nil {
  690. logger.Error("[Reader][/stream/items/contents] [ClientIP=%s] %v", clientIP, err)
  691. json.ServerError(w, r, err)
  692. return
  693. }
  694. userReadingList := fmt.Sprintf(UserStreamPrefix, userID) + ReadingList
  695. userRead := fmt.Sprintf(UserStreamPrefix, userID) + Read
  696. userStarred := fmt.Sprintf(UserStreamPrefix, userID) + Starred
  697. itemIDs, err := getItemIDs(r)
  698. if err != nil {
  699. logger.Error("[Reader][/stream/items/contents] [ClientIP=%s] %v", clientIP, err)
  700. json.ServerError(w, r, err)
  701. return
  702. }
  703. logger.Debug("[Reader][/stream/items/contents] [ClientIP=%s] itemIDs: %v", clientIP, itemIDs)
  704. builder := h.store.NewEntryQueryBuilder(userID)
  705. builder.WithoutStatus(model.EntryStatusRemoved)
  706. builder.WithEntryIDs(itemIDs)
  707. builder.WithOrder(model.DefaultSortingOrder)
  708. builder.WithDirection(requestModifiers.SortDirection)
  709. entries, err := builder.GetEntries()
  710. if err != nil {
  711. json.ServerError(w, r, err)
  712. return
  713. }
  714. if len(entries) == 0 {
  715. err = fmt.Errorf("no items returned from the database")
  716. logger.Error("[Reader][/stream/items/contents] [ClientIP=%s] %v", clientIP, err)
  717. json.ServerError(w, r, err)
  718. return
  719. }
  720. result := streamContentItems{
  721. Direction: "ltr",
  722. ID: fmt.Sprintf("feed/%d", entries[0].FeedID),
  723. Title: entries[0].Feed.Title,
  724. Alternate: []contentHREFType{
  725. {
  726. HREF: entries[0].Feed.SiteURL,
  727. Type: "text/html",
  728. },
  729. },
  730. Updated: time.Now().Unix(),
  731. Self: []contentHREF{
  732. {
  733. HREF: config.Opts.BaseURL() + route.Path(h.router, "StreamItemsContents"),
  734. },
  735. },
  736. Author: user.Username,
  737. }
  738. contentItems := make([]contentItem, len(entries))
  739. for i, entry := range entries {
  740. enclosures := make([]contentItemEnclosure, len(entry.Enclosures))
  741. for _, enclosure := range entry.Enclosures {
  742. enclosures = append(enclosures, contentItemEnclosure{URL: enclosure.URL, Type: enclosure.MimeType})
  743. }
  744. categories := make([]string, 0)
  745. categories = append(categories, userReadingList)
  746. if entry.Feed.Category.Title != "" {
  747. categories = append(categories, fmt.Sprintf(UserLabelPrefix, userID)+entry.Feed.Category.Title)
  748. }
  749. if entry.Starred {
  750. categories = append(categories, userRead)
  751. }
  752. if entry.Starred {
  753. categories = append(categories, userStarred)
  754. }
  755. contentItems[i] = contentItem{
  756. ID: fmt.Sprintf(EntryIDLong, entry.ID),
  757. Title: entry.Title,
  758. Author: entry.Author,
  759. TimestampUsec: fmt.Sprintf("%d", entry.Date.UnixNano()/(int64(time.Microsecond)/int64(time.Nanosecond))),
  760. CrawlTimeMsec: fmt.Sprintf("%d", entry.Date.UnixNano()/(int64(time.Microsecond)/int64(time.Nanosecond))),
  761. Published: entry.Date.Unix(),
  762. Updated: entry.Date.Unix(),
  763. Categories: categories,
  764. Canonical: []contentHREF{
  765. {
  766. HREF: entry.URL,
  767. },
  768. },
  769. Alternate: []contentHREFType{
  770. {
  771. HREF: entry.URL,
  772. Type: "text/html",
  773. },
  774. },
  775. Content: contentItemContent{
  776. Direction: "ltr",
  777. Content: entry.Content,
  778. },
  779. Summary: contentItemContent{
  780. Direction: "ltr",
  781. Content: entry.Content,
  782. },
  783. Origin: contentItemOrigin{
  784. StreamID: fmt.Sprintf("feed/%d", entry.FeedID),
  785. Title: entry.Feed.Title,
  786. HTMLUrl: entry.Feed.SiteURL,
  787. },
  788. Enclosure: enclosures,
  789. }
  790. }
  791. result.Items = contentItems
  792. json.OK(w, r, result)
  793. }
  794. func (h *handler) disableTag(w http.ResponseWriter, r *http.Request) {
  795. userID := request.UserID(r)
  796. clientIP := request.ClientIP(r)
  797. logger.Info("[Reader][/disable-tag][ClientIP=%s] Incoming Request for userID #%d", clientIP, userID)
  798. err := r.ParseForm()
  799. if err != nil {
  800. logger.Error("[Reader][/disable-tag] [ClientIP=%s] %v", clientIP, err)
  801. json.BadRequest(w, r, err)
  802. return
  803. }
  804. streams, err := getStreams(r.Form[ParamStreamID], userID)
  805. if err != nil {
  806. json.BadRequest(w, r, fmt.Errorf("invalid data in %s", ParamStreamID))
  807. return
  808. }
  809. titles := make([]string, len(streams))
  810. for i, stream := range streams {
  811. if stream.Type != LabelStream {
  812. json.BadRequest(w, r, errors.New("only labels are supported"))
  813. return
  814. }
  815. titles[i] = stream.ID
  816. }
  817. err = h.store.RemoveAndReplaceCategoriesByName(userID, titles)
  818. if err != nil {
  819. json.ServerError(w, r, err)
  820. return
  821. }
  822. OK(w, r)
  823. }
  824. func (h *handler) renameTag(w http.ResponseWriter, r *http.Request) {
  825. userID := request.UserID(r)
  826. clientIP := request.ClientIP(r)
  827. logger.Info("[Reader][/rename-tag][ClientIP=%s] Incoming Request for userID #%d", clientIP, userID)
  828. err := r.ParseForm()
  829. if err != nil {
  830. logger.Error("[Reader][/rename-tag] [ClientIP=%s] %v", clientIP, err)
  831. json.BadRequest(w, r, err)
  832. return
  833. }
  834. source, err := getStream(r.Form.Get(ParamStreamID), userID)
  835. if err != nil {
  836. json.BadRequest(w, r, fmt.Errorf("invalid data in %s", ParamStreamID))
  837. return
  838. }
  839. destination, err := getStream(r.Form.Get(ParamDestination), userID)
  840. if err != nil {
  841. json.BadRequest(w, r, fmt.Errorf("invalid data in %s", ParamDestination))
  842. return
  843. }
  844. if source.Type != LabelStream || destination.Type != LabelStream {
  845. json.BadRequest(w, r, errors.New("only labels supported"))
  846. return
  847. }
  848. if destination.ID == "" {
  849. json.BadRequest(w, r, errors.New("empty destination name"))
  850. return
  851. }
  852. category, err := h.store.CategoryByTitle(userID, source.ID)
  853. if err != nil {
  854. json.ServerError(w, r, err)
  855. return
  856. }
  857. if category == nil {
  858. json.NotFound(w, r)
  859. return
  860. }
  861. categoryRequest := model.CategoryRequest{
  862. Title: destination.ID,
  863. }
  864. verr := validator.ValidateCategoryModification(h.store, userID, category.ID, &categoryRequest)
  865. if verr != nil {
  866. json.BadRequest(w, r, verr.Error())
  867. return
  868. }
  869. categoryRequest.Patch(category)
  870. err = h.store.UpdateCategory(category)
  871. if err != nil {
  872. json.ServerError(w, r, err)
  873. return
  874. }
  875. OK(w, r)
  876. }
  877. func (h *handler) tagList(w http.ResponseWriter, r *http.Request) {
  878. userID := request.UserID(r)
  879. clientIP := request.ClientIP(r)
  880. logger.Info("[Reader][tags/list][ClientIP=%s] Incoming Request for userID #%d", clientIP, userID)
  881. if err := checkOutputFormat(w, r); err != nil {
  882. logger.Error("[Reader][OutputFormat] %v", err)
  883. json.BadRequest(w, r, err)
  884. return
  885. }
  886. var result tagsResponse
  887. categories, err := h.store.Categories(userID)
  888. if err != nil {
  889. json.ServerError(w, r, err)
  890. return
  891. }
  892. result.Tags = make([]subscriptionCategory, 0)
  893. result.Tags = append(result.Tags, subscriptionCategory{
  894. ID: fmt.Sprintf(UserStreamPrefix, userID) + Starred,
  895. })
  896. for _, category := range categories {
  897. result.Tags = append(result.Tags, subscriptionCategory{
  898. ID: fmt.Sprintf(UserLabelPrefix, userID) + category.Title,
  899. Label: category.Title,
  900. Type: "folder",
  901. })
  902. }
  903. json.OK(w, r, result)
  904. }
  905. func (h *handler) subscriptionList(w http.ResponseWriter, r *http.Request) {
  906. userID := request.UserID(r)
  907. clientIP := request.ClientIP(r)
  908. logger.Info("[Reader][/subscription/list][ClientIP=%s] Incoming Request for userID #%d", clientIP, userID)
  909. if err := checkOutputFormat(w, r); err != nil {
  910. logger.Error("[Reader][/subscription/list] [ClientIP=%s] %v", clientIP, err)
  911. json.ServerError(w, r, err)
  912. return
  913. }
  914. var result subscriptionsResponse
  915. feeds, err := h.store.Feeds(userID)
  916. if err != nil {
  917. json.ServerError(w, r, err)
  918. return
  919. }
  920. result.Subscriptions = make([]subscription, 0)
  921. for _, feed := range feeds {
  922. result.Subscriptions = append(result.Subscriptions, subscription{
  923. ID: fmt.Sprintf(FeedPrefix+"%d", feed.ID),
  924. Title: feed.Title,
  925. URL: feed.FeedURL,
  926. Categories: []subscriptionCategory{{fmt.Sprintf(UserLabelPrefix, userID) + feed.Category.Title, feed.Category.Title, "folder"}},
  927. HTMLURL: feed.SiteURL,
  928. IconURL: "", //TODO Icons are only base64 encode in DB yet
  929. })
  930. }
  931. json.OK(w, r, result)
  932. }
  933. func (h *handler) serve(w http.ResponseWriter, r *http.Request) {
  934. clientIP := request.ClientIP(r)
  935. dump, _ := httputil.DumpRequest(r, true)
  936. logger.Info("[Reader][UNKNOWN] [ClientIP=%s] URL: %s", clientIP, dump)
  937. logger.Error("Call to Google Reader API not implemented yet!!")
  938. json.OK(w, r, []string{})
  939. }
  940. func (h *handler) userInfo(w http.ResponseWriter, r *http.Request) {
  941. clientIP := request.ClientIP(r)
  942. logger.Info("[Reader][UserInfo] [ClientIP=%s] Sending", clientIP)
  943. if err := checkOutputFormat(w, r); err != nil {
  944. logger.Error("[Reader][/user-info] [ClientIP=%s] %v", clientIP, err)
  945. json.ServerError(w, r, err)
  946. return
  947. }
  948. user, err := h.store.UserByID(request.UserID(r))
  949. if err != nil {
  950. logger.Error("[Reader][/user-info] [ClientIP=%s] %v", clientIP, err)
  951. json.ServerError(w, r, err)
  952. return
  953. }
  954. userInfo := userInfo{UserID: fmt.Sprint(user.ID), UserName: user.Username, UserProfileID: fmt.Sprint(user.ID), UserEmail: user.Username}
  955. json.OK(w, r, userInfo)
  956. }
  957. func (h *handler) streamItemIDs(w http.ResponseWriter, r *http.Request) {
  958. userID := request.UserID(r)
  959. clientIP := request.ClientIP(r)
  960. logger.Debug("[Reader][/stream/items/ids][ClientIP=%s] Incoming Request for userID #%d", clientIP, userID)
  961. if err := checkOutputFormat(w, r); err != nil {
  962. err := fmt.Errorf("output only as json supported")
  963. logger.Error("[Reader][/stream/items/ids] [ClientIP=%s] %v", clientIP, err)
  964. json.ServerError(w, r, err)
  965. return
  966. }
  967. rm, err := getStreamFilterModifiers(r)
  968. if err != nil {
  969. json.ServerError(w, r, err)
  970. return
  971. }
  972. logger.Info("Request Modifiers: %v", rm)
  973. if len(rm.Streams) != 1 {
  974. err := fmt.Errorf("only one stream type expected")
  975. logger.Error("[Reader][/stream/items/ids] [ClientIP=%s] %v", clientIP, err)
  976. json.ServerError(w, r, err)
  977. return
  978. }
  979. switch rm.Streams[0].Type {
  980. case ReadingListStream:
  981. h.handleReadingListStream(w, r, rm)
  982. case StarredStream:
  983. h.handleStarredStream(w, r, rm)
  984. case ReadStream:
  985. h.handleReadStream(w, r, rm)
  986. default:
  987. dump, _ := httputil.DumpRequest(r, true)
  988. logger.Info("[Reader][/stream/items/ids] [ClientIP=%s] Unknown Stream: %s", clientIP, dump)
  989. err := fmt.Errorf("unknown stream type")
  990. logger.Error("[Reader][/stream/items/ids] [ClientIP=%s] %v", clientIP, err)
  991. json.ServerError(w, r, err)
  992. return
  993. }
  994. }
  995. func (h *handler) handleReadingListStream(w http.ResponseWriter, r *http.Request, rm RequestModifiers) {
  996. clientIP := request.ClientIP(r)
  997. builder := h.store.NewEntryQueryBuilder(rm.UserID)
  998. for _, s := range rm.ExcludeTargets {
  999. switch s.Type {
  1000. case ReadStream:
  1001. builder.WithStatus(model.EntryStatusUnread)
  1002. default:
  1003. logger.Info("[Reader][ReadingListStreamIDs][ClientIP=%s] xt filter type: %#v", clientIP, s)
  1004. }
  1005. }
  1006. builder.WithLimit(rm.Count)
  1007. builder.WithOrder(model.DefaultSortingOrder)
  1008. builder.WithDirection(rm.SortDirection)
  1009. rawEntryIDs, err := builder.GetEntryIDs()
  1010. if err != nil {
  1011. logger.Error("[Reader][/stream/items/ids#reading-list] [ClientIP=%s] %v", clientIP, err)
  1012. json.ServerError(w, r, err)
  1013. return
  1014. }
  1015. var itemRefs = make([]itemRef, 0)
  1016. for _, entryID := range rawEntryIDs {
  1017. formattedID := strconv.FormatInt(entryID, 10)
  1018. itemRefs = append(itemRefs, itemRef{ID: formattedID})
  1019. }
  1020. json.OK(w, r, streamIDResponse{itemRefs})
  1021. }
  1022. func (h *handler) handleStarredStream(w http.ResponseWriter, r *http.Request, rm RequestModifiers) {
  1023. clientIP := request.ClientIP(r)
  1024. builder := h.store.NewEntryQueryBuilder(rm.UserID)
  1025. builder.WithStarred()
  1026. builder.WithLimit(rm.Count)
  1027. builder.WithOrder(model.DefaultSortingOrder)
  1028. builder.WithDirection(rm.SortDirection)
  1029. rawEntryIDs, err := builder.GetEntryIDs()
  1030. if err != nil {
  1031. logger.Error("[Reader][/stream/items/ids#starred] [ClientIP=%s] %v", clientIP, err)
  1032. json.ServerError(w, r, err)
  1033. return
  1034. }
  1035. var itemRefs = make([]itemRef, 0)
  1036. for _, entryID := range rawEntryIDs {
  1037. formattedID := strconv.FormatInt(entryID, 10)
  1038. itemRefs = append(itemRefs, itemRef{ID: formattedID})
  1039. }
  1040. json.OK(w, r, streamIDResponse{itemRefs})
  1041. }
  1042. func (h *handler) handleReadStream(w http.ResponseWriter, r *http.Request, rm RequestModifiers) {
  1043. clientIP := request.ClientIP(r)
  1044. builder := h.store.NewEntryQueryBuilder(rm.UserID)
  1045. builder.WithStatus(model.EntryStatusRead)
  1046. builder.WithOrder(model.DefaultSortingOrder)
  1047. builder.WithDirection(rm.SortDirection)
  1048. if rm.StartTime > 0 {
  1049. builder.AfterDate(time.Unix(rm.StartTime, 0))
  1050. }
  1051. if rm.StopTime > 0 {
  1052. builder.BeforeDate(time.Unix(rm.StopTime, 0))
  1053. }
  1054. rawEntryIDs, err := builder.GetEntryIDs()
  1055. if err != nil {
  1056. logger.Error("[Reader][/stream/items/ids#read] [ClientIP=%s] %v", clientIP, err)
  1057. json.ServerError(w, r, err)
  1058. return
  1059. }
  1060. var itemRefs = make([]itemRef, 0)
  1061. for _, entryID := range rawEntryIDs {
  1062. formattedID := strconv.FormatInt(entryID, 10)
  1063. itemRefs = append(itemRefs, itemRef{ID: formattedID})
  1064. }
  1065. json.OK(w, r, streamIDResponse{itemRefs})
  1066. }