|
| 1 | +package generators |
| 2 | + |
| 3 | +import ( |
| 4 | + "encoding/json" |
| 5 | + "fmt" |
| 6 | + "github.com/gorilla/feeds" |
| 7 | + "golang.org/x/net/html" |
| 8 | + "net/http" |
| 9 | + "time" |
| 10 | +) |
| 11 | + |
| 12 | +type AppleSecurityGenerator struct { |
| 13 | + filterFunc ItemFilterFunc |
| 14 | + itemModFunc ItemModifierFunc |
| 15 | +} |
| 16 | +type Page struct { |
| 17 | + Props struct { |
| 18 | + PageProps struct { |
| 19 | + Blogs []BlogEntry `json:"blogs"` |
| 20 | + } `json:"pageProps"` |
| 21 | + } `json:"props"` |
| 22 | +} |
| 23 | + |
| 24 | +type BlogEntry struct { |
| 25 | + Id string `json:"id"` |
| 26 | + Slug string `json:"slug"` |
| 27 | + Title string `json:"title"` |
| 28 | + Description string `json:"description"` |
| 29 | + Date string `json:"date"` |
| 30 | +} |
| 31 | + |
| 32 | +func (e BlogEntry) Time() time.Time { |
| 33 | + t, _ := time.Parse("2006-01-02", e.Date) |
| 34 | + return t |
| 35 | +} |
| 36 | + |
| 37 | +func (e BlogEntry) Link() string { |
| 38 | + return fmt.Sprintf("https://security.apple.com/blog/%s", e.Slug) |
| 39 | +} |
| 40 | + |
| 41 | +func (g *AppleSecurityGenerator) RegisterItemFilter(callback ItemFilterFunc) { |
| 42 | + g.filterFunc = callback |
| 43 | +} |
| 44 | +func (g *AppleSecurityGenerator) RegisterItemModifier(callback ItemModifierFunc) { |
| 45 | + g.itemModFunc = callback |
| 46 | +} |
| 47 | + |
| 48 | +func (g *AppleSecurityGenerator) Feed() (*feeds.Feed, error) { |
| 49 | + page, err := g.getParsedPage() |
| 50 | + if err != nil { |
| 51 | + return nil, err |
| 52 | + } |
| 53 | + |
| 54 | + feed := feeds.Feed{ |
| 55 | + Title: "Apple Security Research", |
| 56 | + Link: &feeds.Link{Href: "https://security.apple.com/blog/"}, |
| 57 | + Updated: time.Now(), |
| 58 | + } |
| 59 | + |
| 60 | + for _, blog := range page.Props.PageProps.Blogs { |
| 61 | + item := &feeds.Item{ |
| 62 | + Title: blog.Title, |
| 63 | + Link: &feeds.Link{Href: blog.Link()}, |
| 64 | + Created: blog.Time(), |
| 65 | + Updated: blog.Time(), |
| 66 | + Content: blog.Description, |
| 67 | + } |
| 68 | + if g.filterFunc != nil && !g.filterFunc(item) { |
| 69 | + continue |
| 70 | + } |
| 71 | + if g.itemModFunc != nil { |
| 72 | + g.itemModFunc(item) |
| 73 | + } |
| 74 | + feed.Items = append(feed.Items, item) |
| 75 | + } |
| 76 | + return &feed, nil |
| 77 | +} |
| 78 | + |
| 79 | +func getJsonNode(nodes *html.Node) *html.Node { |
| 80 | + if nodes.Type == html.ElementNode && nodes.Data == "script" { |
| 81 | + for _, attr := range nodes.Attr { |
| 82 | + if attr.Key == "type" && attr.Val == "application/json" { |
| 83 | + return nodes |
| 84 | + } |
| 85 | + } |
| 86 | + } |
| 87 | + for c := nodes.FirstChild; c != nil; c = c.NextSibling { |
| 88 | + if data := getJsonNode(c); data != nil { |
| 89 | + return data |
| 90 | + } |
| 91 | + } |
| 92 | + return nil |
| 93 | +} |
| 94 | + |
| 95 | +func (g *AppleSecurityGenerator) getParsedPage() (*Page, error) { |
| 96 | + res, err := http.Get("https://security.apple.com/blog/") |
| 97 | + if err != nil { |
| 98 | + return nil, err |
| 99 | + } |
| 100 | + |
| 101 | + if res.StatusCode != 200 { |
| 102 | + return nil, fmt.Errorf("failed to get Apple Security blog: %d", res.StatusCode) |
| 103 | + } |
| 104 | + |
| 105 | + defer func() { _ = res.Body.Close() }() |
| 106 | + |
| 107 | + nodes, err := html.Parse(res.Body) |
| 108 | + if err != nil { |
| 109 | + return nil, err |
| 110 | + } |
| 111 | + |
| 112 | + jsonNode := getJsonNode(nodes) |
| 113 | + if jsonNode == nil { |
| 114 | + return nil, fmt.Errorf("failed to find JSON node") |
| 115 | + } |
| 116 | + |
| 117 | + var page Page |
| 118 | + if err = json.Unmarshal([]byte(jsonNode.FirstChild.Data), &page); err != nil { |
| 119 | + return nil, err |
| 120 | + } |
| 121 | + return &page, nil |
| 122 | +} |
0 commit comments