|
| 1 | +package handler |
| 2 | + |
| 3 | +import ( |
| 4 | + "encoding/json" |
| 5 | + "fmt" |
| 6 | + "io" |
| 7 | + "net/http" |
| 8 | + "strings" |
| 9 | +) |
| 10 | + |
| 11 | +type NewsItem struct { |
| 12 | + ID string `json:"id"` |
| 13 | + Date string `json:"date"` |
| 14 | + Headline string `json:"headline"` |
| 15 | + Body string `json:"body"` |
| 16 | + Category string `json:"category"` |
| 17 | + Products []string `json:"products"` |
| 18 | +} |
| 19 | + |
| 20 | +func fetchNews(category string) ([]NewsItem, error) { |
| 21 | + // Define the API endpoint |
| 22 | + url := "https://aws.amazon.com/api/dirs/items/search" |
| 23 | + |
| 24 | + // Define the query parameters |
| 25 | + params := map[string]string{ |
| 26 | + "item.directoryId": "whats-new-v2", |
| 27 | + "sort_by": "item.additionalFields.postDateTime", |
| 28 | + "sort_order": "desc", |
| 29 | + "size": "50", |
| 30 | + "item.locale": "en_US", |
| 31 | + } |
| 32 | + |
| 33 | + // Optional: Add category filter |
| 34 | + if category != "" { |
| 35 | + params["tags.id"] = fmt.Sprintf("whats-new-v2#marketing-marchitecture#%s", category) |
| 36 | + } |
| 37 | + |
| 38 | + // Build query string |
| 39 | + query := []string{} |
| 40 | + for key, value := range params { |
| 41 | + query = append(query, fmt.Sprintf("%s=%s", key, value)) |
| 42 | + } |
| 43 | + queryString := strings.Join(query, "&") |
| 44 | + |
| 45 | + // Define the headers |
| 46 | + headers := map[string]string{ |
| 47 | + "accept": "*/*", |
| 48 | + "accept-language": "en-US,en;q=0.9", |
| 49 | + "user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36", |
| 50 | + "x-requested-with": "XMLHttpRequest", |
| 51 | + "sec-ch-ua": `"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"`, |
| 52 | + "sec-ch-ua-mobile": "?0", |
| 53 | + "sec-ch-ua-platform": `"Linux"`, |
| 54 | + "sec-fetch-dest": "empty", |
| 55 | + "sec-fetch-mode": "cors", |
| 56 | + "sec-fetch-site": "same-origin", |
| 57 | + "priority": "u=1, i", |
| 58 | + "referer": "https://aws.amazon.com/new/?whats-new-content-all.sort-by=item.additionalFields.postDateTime&whats-new-content-all.sort-order=desc&awsf.whats-new-categories=marketing-marchitecture%23compute", |
| 59 | + } |
| 60 | + |
| 61 | + // Create HTTP request |
| 62 | + req, err := http.NewRequest("GET", fmt.Sprintf("%s?%s", url, queryString), nil) |
| 63 | + if err != nil { |
| 64 | + return nil, err |
| 65 | + } |
| 66 | + |
| 67 | + // Add headers to the request |
| 68 | + for key, value := range headers { |
| 69 | + req.Header.Add(key, value) |
| 70 | + } |
| 71 | + |
| 72 | + // Send the request |
| 73 | + client := &http.Client{} |
| 74 | + resp, err := client.Do(req) |
| 75 | + if err != nil { |
| 76 | + return nil, err |
| 77 | + } |
| 78 | + defer resp.Body.Close() |
| 79 | + // fmt.Println(resp) |
| 80 | + // Check response status |
| 81 | + if resp.StatusCode != http.StatusOK { |
| 82 | + return nil, fmt.Errorf("failed to fetch data. Status code: %d", resp.StatusCode) |
| 83 | + } |
| 84 | + |
| 85 | + // Parse response body |
| 86 | + body, err := io.ReadAll(resp.Body) |
| 87 | + if err != nil { |
| 88 | + return nil, err |
| 89 | + } |
| 90 | + // fmt.Println(body) |
| 91 | + var responseData map[string]interface{} |
| 92 | + err = json.Unmarshal(body, &responseData) |
| 93 | + if err != nil { |
| 94 | + fmt.Println("ERROR") |
| 95 | + return nil, err |
| 96 | + } |
| 97 | + // fmt.Println() |
| 98 | + // fmt.Println(responseData) |
| 99 | + // Extract and transform data |
| 100 | + items, ok := responseData["items"].([]interface{}) |
| 101 | + if !ok { |
| 102 | + return nil, fmt.Errorf("invalid response format") |
| 103 | + } |
| 104 | + |
| 105 | + var newsItems []NewsItem |
| 106 | + for _, item := range items { |
| 107 | + itemMap := item.(map[string]interface{}) |
| 108 | + header := itemMap["item"].(map[string]interface{}) |
| 109 | + tags := itemMap["tags"].([]interface{}) |
| 110 | + |
| 111 | + categoryData := "TBD" |
| 112 | + var products []string |
| 113 | + for _, tag := range tags { |
| 114 | + tagMap := tag.(map[string]interface{}) |
| 115 | + if tagMap["tagNamespaceId"] == "whats-new-v2#marketing-marchitecture" { |
| 116 | + categoryData = tagMap["name"].(string) |
| 117 | + } |
| 118 | + if tagMap["tagNamespaceId"] == "whats-new-v2#general-products" { |
| 119 | + products = append(products, tagMap["name"].(string)) |
| 120 | + } |
| 121 | + } |
| 122 | + |
| 123 | + additionalFields := header["additionalFields"].(map[string]interface{}) |
| 124 | + headline := additionalFields["headline"].(string) |
| 125 | + postBody := additionalFields["postBody"].(string) |
| 126 | + |
| 127 | + // Ignore empty rows |
| 128 | + if headline == "" { |
| 129 | + continue |
| 130 | + } |
| 131 | + |
| 132 | + newsItems = append(newsItems, NewsItem{ |
| 133 | + ID: header["name"].(string), |
| 134 | + Date: header["dateCreated"].(string), |
| 135 | + Headline: headline, |
| 136 | + Body: postBody, |
| 137 | + Category: categoryData, |
| 138 | + Products: products, |
| 139 | + }) |
| 140 | + |
| 141 | + } |
| 142 | + |
| 143 | + return newsItems, nil |
| 144 | +} |
| 145 | + |
| 146 | +func Handler(w http.ResponseWriter, r *http.Request) { |
| 147 | + w.Header().Set("Content-Type", "text/event-stream") |
| 148 | + w.Header().Set("Cache-Control", "no-cache") |
| 149 | + w.Header().Set("Connection", "keep-alive") |
| 150 | + w.Header().Set("Access-Control-Allow-Origin", "*") |
| 151 | + |
| 152 | + // Check if the query string ?json is added |
| 153 | + outputJson := false |
| 154 | + query := r.URL.Query() |
| 155 | + if _, ok := query["json"]; ok { |
| 156 | + outputJson = true |
| 157 | + } |
| 158 | + writeData := func(content []byte) { |
| 159 | + if outputJson { |
| 160 | + fmt.Fprintf(w, "%s", content) |
| 161 | + } else { |
| 162 | + fmt.Fprintf(w, "data: %s\n\n", content) |
| 163 | + } |
| 164 | + } |
| 165 | + |
| 166 | + // Allow browsers which does not support SSE to run through arg no-sse. |
| 167 | + unsupportedSSE := false |
| 168 | + |
| 169 | + // Disable manually by query string added by user |
| 170 | + if _, ok := query["no-sse"]; ok { |
| 171 | + unsupportedSSE = true |
| 172 | + } |
| 173 | + // Automatically disable on Vercel serverless as it does not support Server-Sent Events (SSE). |
| 174 | + if r.Header.Get("X-Vercel-Id") != "" { |
| 175 | + unsupportedSSE = true |
| 176 | + } |
| 177 | + flusher, ok := w.(http.Flusher) |
| 178 | + if !unsupportedSSE && !ok { |
| 179 | + http.Error(w, "SSE not supported", http.StatusInternalServerError) |
| 180 | + return |
| 181 | + } |
| 182 | + flusherFunc := func() { |
| 183 | + if !unsupportedSSE { |
| 184 | + flusher.Flush() |
| 185 | + } |
| 186 | + } |
| 187 | + |
| 188 | + // 1. Send the correct initialization message |
| 189 | + initResp := map[string]interface{}{ |
| 190 | + "jsonrpc": "2.0", |
| 191 | + "id": 1, |
| 192 | + "result": map[string]interface{}{ |
| 193 | + "capabilities": map[string]interface{}{ |
| 194 | + "completion": true, |
| 195 | + }, |
| 196 | + "tools": []map[string]interface{}{ |
| 197 | + { |
| 198 | + "name": "aws_news", |
| 199 | + "description": "Fetches latest AWS news and announcements", |
| 200 | + "parameters": map[string]interface{}{ |
| 201 | + "type": "object", |
| 202 | + "properties": map[string]interface{}{ |
| 203 | + "category": map[string]interface{}{ |
| 204 | + "type": "string", |
| 205 | + "description": "Optional category to filter news", |
| 206 | + }, |
| 207 | + }, |
| 208 | + }, |
| 209 | + }, |
| 210 | + }, |
| 211 | + }, |
| 212 | + } |
| 213 | + initJSON, _ := json.Marshal(initResp) |
| 214 | + writeData(initJSON) |
| 215 | + flusherFunc() |
| 216 | + |
| 217 | + category := "" |
| 218 | + var req struct { |
| 219 | + ID int `json:"id"` |
| 220 | + Method string `json:"method"` |
| 221 | + Params map[string]interface{} `json:"params"` |
| 222 | + } |
| 223 | + if r.Method == http.MethodPost { |
| 224 | + body, _ := io.ReadAll(r.Body) |
| 225 | + json.Unmarshal(body, &req) |
| 226 | + if c, ok := req.Params["category"].(string); ok { |
| 227 | + category = c |
| 228 | + } |
| 229 | + } else { |
| 230 | + if _, ok := query["category"]; ok { |
| 231 | + category = query["category"][0] |
| 232 | + } |
| 233 | + } |
| 234 | + |
| 235 | + news, err := fetchNews(category) |
| 236 | + if err != nil { |
| 237 | + errResp := map[string]interface{}{ |
| 238 | + "jsonrpc": "2.0", |
| 239 | + "id": req.ID, |
| 240 | + "error": map[string]interface{}{ |
| 241 | + "code": -32000, |
| 242 | + "message": fmt.Sprintf("Failed to fetch news: %v", err), |
| 243 | + }, |
| 244 | + } |
| 245 | + errJSON, _ := json.Marshal(errResp) |
| 246 | + writeData(errJSON) |
| 247 | + flusherFunc() |
| 248 | + return |
| 249 | + } |
| 250 | + for _, item := range news { |
| 251 | + newsResp := map[string]interface{}{ |
| 252 | + "jsonrpc": "2.0", |
| 253 | + "id": req.ID, |
| 254 | + "result": map[string]interface{}{ |
| 255 | + "message": map[string]interface{}{ |
| 256 | + "role": "assistant", |
| 257 | + "content": fmt.Sprintf("📢 AWS News Update (%s)\n\n**%s**\n\n%s\n\nCategory: %s\nProducts: %s", |
| 258 | + item.Date, |
| 259 | + item.Headline, |
| 260 | + item.Body, |
| 261 | + item.Category, |
| 262 | + strings.Join(item.Products, ", ")), |
| 263 | + }, |
| 264 | + }, |
| 265 | + } |
| 266 | + newsJSON, _ := json.Marshal(newsResp) |
| 267 | + writeData(newsJSON) |
| 268 | + flusherFunc() |
| 269 | + } |
| 270 | +} |
0 commit comments