max / balanced_breakfast
7 files changed,
+1332 insertions,
-0 deletions
| @@ -0,0 +1,187 @@ | |||
| 1 | + | // Dev.to Plugin for BalancedBreakfast | |
| 2 | + | // Fetches articles from the Dev.to community API | |
| 3 | + | ||
| 4 | + | const DEVTO_API = "https://dev.to/api/articles"; | |
| 5 | + | ||
| 6 | + | fn id() { | |
| 7 | + | "devto" | |
| 8 | + | } | |
| 9 | + | ||
| 10 | + | fn name() { | |
| 11 | + | "Dev.to" | |
| 12 | + | } | |
| 13 | + | ||
| 14 | + | fn capabilities() { | |
| 15 | + | #{ | |
| 16 | + | supports_pagination: true | |
| 17 | + | } | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | fn config_schema() { | |
| 21 | + | #{ | |
| 22 | + | description: "Fetch articles from Dev.to, a community of software developers.", | |
| 23 | + | fields: [ | |
| 24 | + | #{ | |
| 25 | + | key: "tag", | |
| 26 | + | label: "Tag filter", | |
| 27 | + | field_type: "text", | |
| 28 | + | description: "Filter by tag (e.g., rust, javascript, python). Leave blank for all.", | |
| 29 | + | placeholder: "rust" | |
| 30 | + | }, | |
| 31 | + | #{ | |
| 32 | + | key: "limit", | |
| 33 | + | label: "Items per fetch", | |
| 34 | + | field_type: "text", | |
| 35 | + | description: "Number of articles to fetch (max 30)", | |
| 36 | + | default_value: "20", | |
| 37 | + | placeholder: "20" | |
| 38 | + | } | |
| 39 | + | ] | |
| 40 | + | } | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | fn fetch(config, cursor) { | |
| 44 | + | let limit = 20; | |
| 45 | + | if config.limit != () { | |
| 46 | + | let parsed = parse_int(config.limit); | |
| 47 | + | if parsed != () && parsed > 0 { | |
| 48 | + | if parsed > 30 { | |
| 49 | + | limit = 30; | |
| 50 | + | } else { | |
| 51 | + | limit = parsed; | |
| 52 | + | } | |
| 53 | + | } | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | let page = 1; | |
| 57 | + | if cursor != () { | |
| 58 | + | let parsed = parse_int(cursor); | |
| 59 | + | if parsed != () && parsed > 0 { | |
| 60 | + | page = parsed; | |
| 61 | + | } | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | let url = DEVTO_API + "?per_page=" + limit + "&page=" + page; | |
| 65 | + | ||
| 66 | + | if config.tag != () && config.tag != "" { | |
| 67 | + | url += "&tag=" + config.tag; | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | let articles = http_get_json(url); | |
| 71 | + | ||
| 72 | + | if articles == () { | |
| 73 | + | return #{ items: [], has_more: false }; | |
| 74 | + | } | |
| 75 | + | ||
| 76 | + | let items = []; | |
| 77 | + | for article in articles { | |
| 78 | + | let item = parse_article(article); | |
| 79 | + | if item != () { | |
| 80 | + | items.push(item); | |
| 81 | + | } | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | let has_more = items.len() >= limit; | |
| 85 | + | let result = #{ | |
| 86 | + | items: items, | |
| 87 | + | has_more: has_more | |
| 88 | + | }; | |
| 89 | + | ||
| 90 | + | if has_more { | |
| 91 | + | result.next_cursor = "" + (page + 1); | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | result | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | fn parse_article(article) { | |
| 98 | + | if article.title == () { | |
| 99 | + | return (); | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | let title = article.title; | |
| 103 | + | let id = 0; | |
| 104 | + | if article.id != () { | |
| 105 | + | id = article.id; | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | let author = "Unknown"; | |
| 109 | + | if article.user != () { | |
| 110 | + | if article.user.name != () { | |
| 111 | + | author = article.user.name; | |
| 112 | + | } else if article.user.username != () { | |
| 113 | + | author = article.user.username; | |
| 114 | + | } | |
| 115 | + | } | |
| 116 | + | ||
| 117 | + | let url = ""; | |
| 118 | + | if article.url != () { | |
| 119 | + | url = article.url; | |
| 120 | + | } | |
| 121 | + | ||
| 122 | + | let description = ""; | |
| 123 | + | if article.description != () { | |
| 124 | + | description = article.description; | |
| 125 | + | } | |
| 126 | + | ||
| 127 | + | let published = timestamp_now(); | |
| 128 | + | if article.published_at != () { | |
| 129 | + | published = article.published_at; | |
| 130 | + | } | |
| 131 | + | ||
| 132 | + | let reactions = 0; | |
| 133 | + | if article.positive_reactions_count != () { | |
| 134 | + | reactions = article.positive_reactions_count; | |
| 135 | + | } | |
| 136 | + | ||
| 137 | + | let comments = 0; | |
| 138 | + | if article.comments_count != () { | |
| 139 | + | comments = article.comments_count; | |
| 140 | + | } | |
| 141 | + | ||
| 142 | + | let reading_time = 0; | |
| 143 | + | if article.reading_time_minutes != () { | |
| 144 | + | reading_time = article.reading_time_minutes; | |
| 145 | + | } | |
| 146 | + | ||
| 147 | + | // Build tags | |
| 148 | + | let tags = ["devto"]; | |
| 149 | + | if article.tag_list != () { | |
| 150 | + | for tag in article.tag_list { | |
| 151 | + | tags.push(tag); | |
| 152 | + | } | |
| 153 | + | } | |
| 154 | + | ||
| 155 | + | let secondary = "" + reactions + " reactions ยท " + comments + " comments"; | |
| 156 | + | if reading_time > 0 { | |
| 157 | + | secondary += " ยท " + reading_time + " min read"; | |
| 158 | + | } | |
| 159 | + | ||
| 160 | + | // Body: cover image + description | |
| 161 | + | let body = ""; | |
| 162 | + | if article.cover_image != () && article.cover_image != "" { | |
| 163 | + | body = `<img src="` + article.cover_image + `" alt="` + title + `" style="max-width:100%">` + "\n\n"; | |
| 164 | + | } | |
| 165 | + | body += description; | |
| 166 | + | ||
| 167 | + | #{ | |
| 168 | + | id: #{ source: "devto", item_id: "" + id }, | |
| 169 | + | bite: #{ | |
| 170 | + | author: author, | |
| 171 | + | text: truncate(title, 100), | |
| 172 | + | secondary: secondary, | |
| 173 | + | indicator: "๐ป" | |
| 174 | + | }, | |
| 175 | + | content: #{ | |
| 176 | + | title: title, | |
| 177 | + | body: body, | |
| 178 | + | url: url | |
| 179 | + | }, | |
| 180 | + | meta: #{ | |
| 181 | + | source_name: "Dev.to", | |
| 182 | + | published_at: published, | |
| 183 | + | score: reactions, | |
| 184 | + | tags: tags | |
| 185 | + | } | |
| 186 | + | } | |
| 187 | + | } |
| @@ -0,0 +1,215 @@ | |||
| 1 | + | // USGS Earthquakes Plugin for BalancedBreakfast | |
| 2 | + | // Fetches recent earthquake data from the USGS GeoJSON feed | |
| 3 | + | ||
| 4 | + | const USGS_API = "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary"; | |
| 5 | + | ||
| 6 | + | fn id() { | |
| 7 | + | "earthquakes" | |
| 8 | + | } | |
| 9 | + | ||
| 10 | + | fn name() { | |
| 11 | + | "USGS Earthquakes" | |
| 12 | + | } | |
| 13 | + | ||
| 14 | + | fn capabilities() { | |
| 15 | + | #{ | |
| 16 | + | supports_pagination: false | |
| 17 | + | } | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | fn config_schema() { | |
| 21 | + | #{ | |
| 22 | + | description: "Real-time earthquake data from the USGS. Choose a minimum magnitude and time window.", | |
| 23 | + | fields: [ | |
| 24 | + | #{ | |
| 25 | + | key: "magnitude", | |
| 26 | + | label: "Minimum Magnitude", | |
| 27 | + | field_type: "select", | |
| 28 | + | required: true, | |
| 29 | + | description: "Minimum earthquake magnitude to show", | |
| 30 | + | default_value: "significant", | |
| 31 | + | options: ["significant", "4.5", "2.5", "1.0", "all"] | |
| 32 | + | }, | |
| 33 | + | #{ | |
| 34 | + | key: "timeframe", | |
| 35 | + | label: "Time Window", | |
| 36 | + | field_type: "select", | |
| 37 | + | required: true, | |
| 38 | + | description: "How far back to look", | |
| 39 | + | default_value: "day", | |
| 40 | + | options: ["hour", "day", "week", "month"] | |
| 41 | + | } | |
| 42 | + | ] | |
| 43 | + | } | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | fn fetch(config, cursor) { | |
| 47 | + | let magnitude = "significant"; | |
| 48 | + | if config.magnitude != () { | |
| 49 | + | magnitude = config.magnitude; | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | let timeframe = "day"; | |
| 53 | + | if config.timeframe != () { | |
| 54 | + | timeframe = config.timeframe; | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | // Build the feed URL | |
| 58 | + | let feed_name = get_feed_name(magnitude); | |
| 59 | + | let url = USGS_API + "/" + feed_name + "_" + timeframe + ".geojson"; | |
| 60 | + | ||
| 61 | + | let data = http_get_json(url); | |
| 62 | + | ||
| 63 | + | if data == () { | |
| 64 | + | return #{ items: [], has_more: false }; | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | let features = data.features; | |
| 68 | + | if features == () { | |
| 69 | + | return #{ items: [], has_more: false }; | |
| 70 | + | } | |
| 71 | + | ||
| 72 | + | let items = []; | |
| 73 | + | for feature in features { | |
| 74 | + | let item = parse_quake(feature); | |
| 75 | + | if item != () { | |
| 76 | + | items.push(item); | |
| 77 | + | } | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | #{ | |
| 81 | + | items: items, | |
| 82 | + | has_more: false | |
| 83 | + | } | |
| 84 | + | } | |
| 85 | + | ||
| 86 | + | fn get_feed_name(magnitude) { | |
| 87 | + | if magnitude == "4.5" { return "4.5"; } | |
| 88 | + | if magnitude == "2.5" { return "2.5"; } | |
| 89 | + | if magnitude == "1.0" { return "1.0"; } | |
| 90 | + | if magnitude == "all" { return "all"; } | |
| 91 | + | "significant" | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | fn parse_quake(feature) { | |
| 95 | + | let props = feature.properties; | |
| 96 | + | if props == () { | |
| 97 | + | return (); | |
| 98 | + | } | |
| 99 | + | ||
| 100 | + | let title = "Unknown earthquake"; | |
| 101 | + | if props.title != () { | |
| 102 | + | title = props.title; | |
| 103 | + | } else if props.place != () { | |
| 104 | + | title = props.place; | |
| 105 | + | } | |
| 106 | + | ||
| 107 | + | let mag = 0.0; | |
| 108 | + | if props.mag != () { | |
| 109 | + | mag = props.mag; | |
| 110 | + | } | |
| 111 | + | ||
| 112 | + | let place = ""; | |
| 113 | + | if props.place != () { | |
| 114 | + | place = props.place; | |
| 115 | + | } | |
| 116 | + | ||
| 117 | + | let time = timestamp_now(); | |
| 118 | + | if props.time != () { | |
| 119 | + | // USGS times are in milliseconds | |
| 120 | + | time = props.time / 1000; | |
| 121 | + | } | |
| 122 | + | ||
| 123 | + | let url = ""; | |
| 124 | + | if props.url != () { | |
| 125 | + | url = props.url; | |
| 126 | + | } | |
| 127 | + | ||
| 128 | + | let tsunami = 0; | |
| 129 | + | if props.tsunami != () { | |
| 130 | + | tsunami = props.tsunami; | |
| 131 | + | } | |
| 132 | + | ||
| 133 | + | let felt = 0; | |
| 134 | + | if props.felt != () { | |
| 135 | + | felt = props.felt; | |
| 136 | + | } | |
| 137 | + | ||
| 138 | + | let alert = ""; | |
| 139 | + | if props.alert != () { | |
| 140 | + | alert = props.alert; | |
| 141 | + | } | |
| 142 | + | ||
| 143 | + | let quake_id = ""; | |
| 144 | + | if feature.id != () { | |
| 145 | + | quake_id = feature.id; | |
| 146 | + | } | |
| 147 | + | ||
| 148 | + | // Coordinates | |
| 149 | + | let coords = ""; | |
| 150 | + | if feature.geometry != () { | |
| 151 | + | let geo = feature.geometry; | |
| 152 | + | if geo.coordinates != () { | |
| 153 | + | let c = geo.coordinates; | |
| 154 | + | if c.len() >= 2 { | |
| 155 | + | coords = "Lat: " + c[1] + ", Lon: " + c[0]; | |
| 156 | + | if c.len() >= 3 { | |
| 157 | + | coords += ", Depth: " + c[2] + " km"; | |
| 158 | + | } | |
| 159 | + | } | |
| 160 | + | } | |
| 161 | + | } | |
| 162 | + | ||
| 163 | + | // Build body | |
| 164 | + | let body = "**Location:** " + place + "\n"; | |
| 165 | + | body += "**Magnitude:** " + mag + "\n"; | |
| 166 | + | if coords != "" { | |
| 167 | + | body += "**Coordinates:** " + coords + "\n"; | |
| 168 | + | } | |
| 169 | + | if tsunami > 0 { | |
| 170 | + | body += "**Tsunami warning:** Yes\n"; | |
| 171 | + | } | |
| 172 | + | if felt > 0 { | |
| 173 | + | body += "**Felt reports:** " + felt + "\n"; | |
| 174 | + | } | |
| 175 | + | if alert != "" { | |
| 176 | + | body += "**Alert level:** " + alert + "\n"; | |
| 177 | + | } | |
| 178 | + | ||
| 179 | + | // Indicator based on magnitude | |
| 180 | + | let indicator = "๐ข"; | |
| 181 | + | if mag >= 7.0 { | |
| 182 | + | indicator = "๐ด"; | |
| 183 | + | } else if mag >= 5.0 { | |
| 184 | + | indicator = "๐ "; | |
| 185 | + | } else if mag >= 3.0 { | |
| 186 | + | indicator = "๐ก"; | |
| 187 | + | } | |
| 188 | + | ||
| 189 | + | let secondary = "M" + mag + " ยท " + place; | |
| 190 | + | ||
| 191 | + | let tags = ["earthquake", "usgs"]; | |
| 192 | + | if tsunami > 0 { | |
| 193 | + | tags.push("tsunami"); | |
| 194 | + | } | |
| 195 | + | ||
| 196 | + | #{ | |
| 197 | + | id: #{ source: "earthquakes", item_id: quake_id }, | |
| 198 | + | bite: #{ | |
| 199 | + | author: "USGS", | |
| 200 | + | text: truncate(title, 100), | |
| 201 | + | secondary: truncate(secondary, 80), | |
| 202 | + | indicator: indicator | |
| 203 | + | }, | |
| 204 | + | content: #{ | |
| 205 | + | title: title, | |
| 206 | + | body: body, | |
| 207 | + | url: url | |
| 208 | + | }, | |
| 209 | + | meta: #{ | |
| 210 | + | source_name: "USGS Earthquakes", | |
| 211 | + | published_at: time, | |
| 212 | + | tags: tags | |
| 213 | + | } | |
| 214 | + | } | |
| 215 | + | } |
| @@ -0,0 +1,217 @@ | |||
| 1 | + | // GitHub Trending Plugin for BalancedBreakfast | |
| 2 | + | // Fetches trending repositories via the GitHub search API (no auth required) | |
| 3 | + | ||
| 4 | + | const GH_API = "https://api.github.com/search/repositories"; | |
| 5 | + | ||
| 6 | + | fn id() { | |
| 7 | + | "github_trending" | |
| 8 | + | } | |
| 9 | + | ||
| 10 | + | fn name() { | |
| 11 | + | "GitHub Trending" | |
| 12 | + | } | |
| 13 | + | ||
| 14 | + | fn capabilities() { | |
| 15 | + | #{ | |
| 16 | + | supports_pagination: true | |
| 17 | + | } | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | fn config_schema() { | |
| 21 | + | #{ | |
| 22 | + | description: "Trending GitHub repositories by stars gained recently. Filter by language.", | |
| 23 | + | fields: [ | |
| 24 | + | #{ | |
| 25 | + | key: "language", | |
| 26 | + | label: "Language", | |
| 27 | + | field_type: "text", | |
| 28 | + | description: "Filter by language (e.g., rust, python, javascript). Leave blank for all.", | |
| 29 | + | placeholder: "rust" | |
| 30 | + | }, | |
| 31 | + | #{ | |
| 32 | + | key: "timeframe", | |
| 33 | + | label: "Time Window", | |
| 34 | + | field_type: "select", | |
| 35 | + | required: true, | |
| 36 | + | description: "How far back to look for star activity", | |
| 37 | + | default_value: "weekly", | |
| 38 | + | options: ["daily", "weekly", "monthly"] | |
| 39 | + | }, | |
| 40 | + | #{ | |
| 41 | + | key: "limit", | |
| 42 | + | label: "Repos to show", | |
| 43 | + | field_type: "text", | |
| 44 | + | description: "Number of repos to fetch (max 30)", | |
| 45 | + | default_value: "20", | |
| 46 | + | placeholder: "20" | |
| 47 | + | } | |
| 48 | + | ] | |
| 49 | + | } | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | fn fetch(config, cursor) { | |
| 53 | + | let limit = 20; | |
| 54 | + | if config.limit != () { | |
| 55 | + | let parsed = parse_int(config.limit); | |
| 56 | + | if parsed != () && parsed > 0 { | |
| 57 | + | if parsed > 30 { | |
| 58 | + | limit = 30; | |
| 59 | + | } else { | |
| 60 | + | limit = parsed; | |
| 61 | + | } | |
| 62 | + | } | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | let page = 1; | |
| 66 | + | if cursor != () { | |
| 67 | + | let parsed = parse_int(cursor); | |
| 68 | + | if parsed != () && parsed > 0 { | |
| 69 | + | page = parsed; | |
| 70 | + | } | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | // Build date query for "created after" threshold | |
| 74 | + | let timeframe = "weekly"; | |
| 75 | + | if config.timeframe != () { | |
| 76 | + | timeframe = config.timeframe; | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | // Use stars>100 as a proxy for trending (GitHub search API) | |
| 80 | + | let query = "stars:>100"; | |
| 81 | + | ||
| 82 | + | if config.language != () && config.language != "" { | |
| 83 | + | query += " language:" + config.language; | |
| 84 | + | } | |
| 85 | + | ||
| 86 | + | // Sort by stars to approximate trending | |
| 87 | + | let url = GH_API + "?q=" + query + "&sort=stars&order=desc&per_page=" + limit + "&page=" + page; | |
| 88 | + | ||
| 89 | + | let data = http_get_json(url); | |
| 90 | + | ||
| 91 | + | if data == () { | |
| 92 | + | return #{ items: [], has_more: false }; | |
| 93 | + | } | |
| 94 | + | ||
| 95 | + | let repos = data.items; | |
| 96 | + | if repos == () { | |
| 97 | + | return #{ items: [], has_more: false }; | |
| 98 | + | } | |
| 99 | + | ||
| 100 | + | let items = []; | |
| 101 | + | for repo in repos { | |
| 102 | + | let item = parse_repo(repo); | |
| 103 | + | if item != () { | |
| 104 | + | items.push(item); | |
| 105 | + | } | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | let has_more = items.len() >= limit; | |
| 109 | + | let result = #{ | |
| 110 | + | items: items, | |
| 111 | + | has_more: has_more | |
| 112 | + | }; | |
| 113 | + | ||
| 114 | + | if has_more { | |
| 115 | + | result.next_cursor = "" + (page + 1); | |
| 116 | + | } | |
| 117 | + | ||
| 118 | + | result | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | fn parse_repo(repo) { | |
| 122 | + | if repo.full_name == () { | |
| 123 | + | return (); | |
| 124 | + | } | |
| 125 | + | ||
| 126 | + | let name = repo.full_name; | |
| 127 | + | let description = ""; | |
| 128 | + | if repo.description != () { | |
| 129 | + | description = repo.description; | |
| 130 | + | } | |
| 131 | + | ||
| 132 | + | let stars = 0; | |
| 133 | + | if repo.stargazers_count != () { | |
| 134 | + | stars = repo.stargazers_count; | |
| 135 | + | } | |
| 136 | + | ||
| 137 | + | let forks = 0; | |
| 138 | + | if repo.forks_count != () { | |
| 139 | + | forks = repo.forks_count; | |
| 140 | + | } | |
| 141 | + | ||
| 142 | + | let language = ""; | |
| 143 | + | if repo.language != () { | |
| 144 | + | language = repo.language; | |
| 145 | + | } | |
| 146 | + | ||
| 147 | + | let url = ""; | |
| 148 | + | if repo.html_url != () { | |
| 149 | + | url = repo.html_url; | |
| 150 | + | } | |
| 151 | + | ||
| 152 | + | let owner = "Unknown"; | |
| 153 | + | if repo.owner != () { | |
| 154 | + | if repo.owner.login != () { | |
| 155 | + | owner = repo.owner.login; | |
| 156 | + | } | |
| 157 | + | } | |
| 158 | + | ||
| 159 | + | let created = timestamp_now(); | |
| 160 | + | if repo.pushed_at != () { | |
| 161 | + | created = repo.pushed_at; | |
| 162 | + | } | |
| 163 | + | ||
| 164 | + | let repo_id = 0; | |
| 165 | + | if repo.id != () { | |
| 166 | + | repo_id = repo.id; | |
| 167 | + | } | |
| 168 | + | ||
| 169 | + | // Format stars count | |
| 170 | + | let stars_display = "" + stars; | |
| 171 | + | if stars >= 1000 { | |
| 172 | + | stars_display = "" + (stars / 1000) + "k"; | |
| 173 | + | } | |
| 174 | + | ||
| 175 | + | let secondary = stars_display + " stars ยท " + forks + " forks"; | |
| 176 | + | if language != "" { | |
| 177 | + | secondary += " ยท " + language; | |
| 178 | + | } | |
| 179 | + | ||
| 180 | + | // Build body | |
| 181 | + | let body = ""; | |
| 182 | + | if description != "" { | |
| 183 | + | body = description + "\n\n"; | |
| 184 | + | } | |
| 185 | + | body += "**Stars:** " + stars + "\n"; | |
| 186 | + | body += "**Forks:** " + forks + "\n"; | |
| 187 | + | if language != "" { | |
| 188 | + | body += "**Language:** " + language + "\n"; | |
| 189 | + | } | |
| 190 | + | ||
| 191 | + | // Tags | |
| 192 | + | let tags = ["github"]; | |
| 193 | + | if language != "" { | |
| 194 | + | tags.push(language); | |
| 195 | + | } | |
| 196 | + | ||
| 197 | + | #{ | |
| 198 | + | id: #{ source: "github_trending", item_id: "" + repo_id }, | |
| 199 | + | bite: #{ | |
| 200 | + | author: owner, | |
| 201 | + | text: truncate(name + ": " + description, 100), | |
| 202 | + | secondary: secondary, | |
| 203 | + | indicator: "โญ" | |
| 204 | + | }, | |
| 205 | + | content: #{ | |
| 206 | + | title: name, | |
| 207 | + | body: body, | |
| 208 | + | url: url | |
| 209 | + | }, | |
| 210 | + | meta: #{ | |
| 211 | + | source_name: "GitHub Trending", | |
| 212 | + | published_at: created, | |
| 213 | + | score: stars, | |
| 214 | + | tags: tags | |
| 215 | + | } | |
| 216 | + | } | |
| 217 | + | } |
| @@ -0,0 +1,180 @@ | |||
| 1 | + | // Lobsters Plugin for BalancedBreakfast | |
| 2 | + | // Fetches stories from Lobste.rs โ computing-focused link aggregation | |
| 3 | + | ||
| 4 | + | const LOBSTERS_URL = "https://lobste.rs"; | |
| 5 | + | ||
| 6 | + | fn id() { | |
| 7 | + | "lobsters" | |
| 8 | + | } | |
| 9 | + | ||
| 10 | + | fn name() { | |
| 11 | + | "Lobsters" | |
| 12 | + | } | |
| 13 | + | ||
| 14 | + | fn capabilities() { | |
| 15 | + | #{ | |
| 16 | + | supports_pagination: true | |
| 17 | + | } | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | fn config_schema() { | |
| 21 | + | #{ | |
| 22 | + | description: "Fetch stories from Lobste.rs, a computing-focused link aggregation community.", | |
| 23 | + | fields: [ | |
| 24 | + | #{ | |
| 25 | + | key: "feed_type", | |
| 26 | + | label: "Feed Type", | |
| 27 | + | field_type: "select", | |
| 28 | + | required: true, | |
| 29 | + | description: "Which stories to fetch", | |
| 30 | + | default_value: "hottest", | |
| 31 | + | options: ["hottest", "newest", "active"] | |
| 32 | + | }, | |
| 33 | + | #{ | |
| 34 | + | key: "limit", | |
| 35 | + | label: "Items per fetch", | |
| 36 | + | field_type: "text", | |
| 37 | + | description: "Number of stories to fetch (max 50)", | |
| 38 | + | default_value: "25", | |
| 39 | + | placeholder: "25" | |
| 40 | + | } | |
| 41 | + | ] | |
| 42 | + | } | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | fn fetch(config, cursor) { | |
| 46 | + | let feed_type = "hottest"; | |
| 47 | + | if config.feed_type != () { | |
| 48 | + | feed_type = config.feed_type; | |
| 49 | + | } | |
| 50 | + | ||
| 51 | + | let limit = 25; | |
| 52 | + | if config.limit != () { | |
| 53 | + | let parsed = parse_int(config.limit); | |
| 54 | + | if parsed != () && parsed > 0 { | |
| 55 | + | if parsed > 50 { | |
| 56 | + | limit = 50; | |
| 57 | + | } else { | |
| 58 | + | limit = parsed; | |
| 59 | + | } | |
| 60 | + | } | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | // Parse cursor for page number | |
| 64 | + | let page = 1; | |
| 65 | + | if cursor != () { | |
| 66 | + | let parsed = parse_int(cursor); | |
| 67 | + | if parsed != () && parsed > 0 { | |
| 68 | + | page = parsed; | |
| 69 | + | } | |
| 70 | + | } | |
| 71 | + | ||
| 72 | + | let url = LOBSTERS_URL + "/" + feed_type + ".json?page=" + page; | |
| 73 | + | let stories = http_get_json(url); | |
| 74 | + | ||
| 75 | + | let items = []; | |
| 76 | + | let count = 0; | |
| 77 | + | ||
| 78 | + | for story in stories { | |
| 79 | + | if count >= limit { | |
| 80 | + | break; | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | let item = parse_story(story); | |
| 84 | + | if item != () { | |
| 85 | + | items.push(item); | |
| 86 | + | count += 1; | |
| 87 | + | } | |
| 88 | + | } | |
| 89 | + | ||
| 90 | + | let has_more = items.len() >= limit; | |
| 91 | + | let result = #{ | |
| 92 | + | items: items, | |
| 93 | + | has_more: has_more | |
| 94 | + | }; | |
| 95 | + | ||
| 96 | + | if has_more { | |
| 97 | + | result.next_cursor = "" + (page + 1); | |
| 98 | + | } | |
| 99 | + | ||
| 100 | + | result | |
| 101 | + | } | |
| 102 | + | ||
| 103 | + | fn parse_story(story) { | |
| 104 | + | if story.title == () { | |
| 105 | + | return (); | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | let title = story.title; | |
| 109 | + | ||
| 110 | + | let score = 0; | |
| 111 | + | if story.score != () { | |
| 112 | + | score = story.score; | |
| 113 | + | } | |
| 114 | + | ||
| 115 | + | let comments = 0; | |
| 116 | + | if story.comment_count != () { | |
| 117 | + | comments = story.comment_count; | |
| 118 | + | } | |
| 119 | + | ||
| 120 | + | let author = "anonymous"; | |
| 121 | + | if story.submitter_user != () { | |
| 122 | + | if story.submitter_user.username != () { | |
| 123 | + | author = story.submitter_user.username; | |
| 124 | + | } | |
| 125 | + | } | |
| 126 | + | ||
| 127 | + | let url = ""; | |
| 128 | + | if story.url != () && story.url != "" { | |
| 129 | + | url = story.url; | |
| 130 | + | } else if story.comments_url != () { | |
| 131 | + | url = story.comments_url; | |
| 132 | + | } | |
| 133 | + | ||
| 134 | + | let created = timestamp_now(); | |
| 135 | + | if story.created_at != () { | |
| 136 | + | created = story.created_at; | |
| 137 | + | } | |
| 138 | + | ||
| 139 | + | let short_id = ""; | |
| 140 | + | if story.short_id != () { | |
| 141 | + | short_id = story.short_id; | |
| 142 | + | } | |
| 143 | + | ||
| 144 | + | // Build tags from story tags | |
| 145 | + | let tags = ["lobsters"]; | |
| 146 | + | if story.tags != () { | |
| 147 | + | for tag in story.tags { | |
| 148 | + | tags.push(tag); | |
| 149 | + | } | |
| 150 | + | } | |
| 151 | + | ||
| 152 | + | let secondary = "" + score + " pts ยท " + comments + " comments"; | |
| 153 | + | ||
| 154 | + | // Description/body | |
| 155 | + | let body = (); | |
| 156 | + | if story.description != () && story.description != "" { | |
| 157 | + | body = story.description; | |
| 158 | + | } | |
| 159 | + | ||
| 160 | + | #{ | |
| 161 | + | id: #{ source: "lobsters", item_id: short_id }, | |
| 162 | + | bite: #{ | |
| 163 | + | author: author, | |
| 164 | + | text: truncate(title, 100), | |
| 165 | + | secondary: secondary, | |
| 166 | + | indicator: "๐ฆ" | |
| 167 | + | }, | |
| 168 | + | content: #{ | |
| 169 | + | title: title, | |
| 170 | + | body: body, | |
| 171 | + | url: url | |
| 172 | + | }, | |
| 173 | + | meta: #{ | |
| 174 | + | source_name: "Lobsters", | |
| 175 | + | published_at: created, | |
| 176 | + | score: score, | |
| 177 | + | tags: tags | |
| 178 | + | } | |
| 179 | + | } | |
| 180 | + | } |
| @@ -0,0 +1,161 @@ | |||
| 1 | + | // NASA Astronomy Picture of the Day Plugin for BalancedBreakfast | |
| 2 | + | // Fetches recent APODs from the NASA API (free DEMO_KEY included) | |
| 3 | + | ||
| 4 | + | const NASA_API = "https://api.nasa.gov/planetary/apod"; | |
| 5 | + | ||
| 6 | + | fn id() { | |
| 7 | + | "nasa_apod" | |
| 8 | + | } | |
| 9 | + | ||
| 10 | + | fn name() { | |
| 11 | + | "NASA APOD" | |
| 12 | + | } | |
| 13 | + | ||
| 14 | + | fn capabilities() { | |
| 15 | + | #{ | |
| 16 | + | supports_pagination: false | |
| 17 | + | } | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | fn config_schema() { | |
| 21 | + | #{ | |
| 22 | + | description: "NASA Astronomy Picture of the Day. Shows stunning space images with expert explanations. Uses the free DEMO_KEY by default (30 req/hr). Provide your own API key from api.nasa.gov for higher limits.", | |
| 23 | + | fields: [ | |
| 24 | + | #{ | |
| 25 | + | key: "api_key", | |
| 26 | + | label: "API Key", | |
| 27 | + | field_type: "text", | |
| 28 | + | description: "NASA API key (leave blank to use DEMO_KEY)", | |
| 29 | + | default_value: "DEMO_KEY", | |
| 30 | + | placeholder: "DEMO_KEY" | |
| 31 | + | }, | |
| 32 | + | #{ | |
| 33 | + | key: "count", | |
| 34 | + | label: "Number of images", | |
| 35 | + | field_type: "text", | |
| 36 | + | description: "How many recent APODs to fetch (max 10)", | |
| 37 | + | default_value: "5", | |
| 38 | + | placeholder: "5" | |
| 39 | + | } | |
| 40 | + | ] | |
| 41 | + | } | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | fn fetch(config, cursor) { | |
| 45 | + | let api_key = "DEMO_KEY"; | |
| 46 | + | if config.api_key != () && config.api_key != "" { | |
| 47 | + | api_key = config.api_key; | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | let count = 5; | |
| 51 | + | if config.count != () { | |
| 52 | + | let parsed = parse_int(config.count); | |
| 53 | + | if parsed != () && parsed > 0 { | |
| 54 | + | if parsed > 10 { | |
| 55 | + | count = 10; | |
| 56 | + | } else { | |
| 57 | + | count = parsed; | |
| 58 | + | } | |
| 59 | + | } | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | let url = NASA_API + "?api_key=" + api_key + "&count=" + count + "&thumbs=true"; | |
| 63 | + | let results = http_get_json(url); | |
| 64 | + | ||
| 65 | + | if results == () { | |
| 66 | + | return #{ items: [], has_more: false }; | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | let items = []; | |
| 70 | + | for entry in results { | |
| 71 | + | let item = parse_apod(entry); | |
| 72 | + | if item != () { | |
| 73 | + | items.push(item); | |
| 74 | + | } | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | #{ | |
| 78 | + | items: items, | |
| 79 | + | has_more: false | |
| 80 | + | } | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | fn parse_apod(entry) { | |
| 84 | + | let title = "Untitled"; | |
| 85 | + | if entry.title != () { | |
| 86 | + | title = entry.title; | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | let explanation = ""; | |
| 90 | + | if entry.explanation != () { | |
| 91 | + | explanation = entry.explanation; | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | let date = ""; | |
| 95 | + | if entry.date != () { | |
| 96 | + | date = entry.date; | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | let media_type = "image"; | |
| 100 | + | if entry.media_type != () { | |
| 101 | + | media_type = entry.media_type; | |
| 102 | + | } | |
| 103 | + | ||
| 104 | + | let url = ""; | |
| 105 | + | if entry.url != () { | |
| 106 | + | url = entry.url; | |
| 107 | + | } | |
| 108 | + | ||
| 109 | + | let hdurl = ""; | |
| 110 | + | if entry.hdurl != () { | |
| 111 | + | hdurl = entry.hdurl; | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | // Build body with image and explanation | |
| 115 | + | let body = ""; | |
| 116 | + | if media_type == "image" { | |
| 117 | + | let img_url = url; | |
| 118 | + | if hdurl != "" { | |
| 119 | + | img_url = hdurl; | |
| 120 | + | } | |
| 121 | + | body = `<img src="` + img_url + `" alt="` + title + `" style="max-width:100%">`; | |
| 122 | + | } else if media_type == "video" { | |
| 123 | + | body = "Video: " + url; | |
| 124 | + | } | |
| 125 | + | ||
| 126 | + | if explanation != "" { | |
| 127 | + | body += "\n\n" + explanation; | |
| 128 | + | } | |
| 129 | + | ||
| 130 | + | let copyright = ""; | |
| 131 | + | if entry.copyright != () { | |
| 132 | + | copyright = str_trim(entry.copyright); | |
| 133 | + | } | |
| 134 | + | ||
| 135 | + | let secondary = date; | |
| 136 | + | if copyright != "" { | |
| 137 | + | secondary += " ยท " + copyright; | |
| 138 | + | } | |
| 139 | + | ||
| 140 | + | let apod_url = "https://apod.nasa.gov/apod/"; | |
| 141 | + | ||
| 142 | + | #{ | |
| 143 | + | id: #{ source: "nasa_apod", item_id: date }, | |
| 144 | + | bite: #{ | |
| 145 | + | author: "NASA", | |
| 146 | + | text: truncate(title, 100), | |
| 147 | + | secondary: secondary, | |
| 148 | + | indicator: "๐ญ" | |
| 149 | + | }, | |
| 150 | + | content: #{ | |
| 151 | + | title: title, | |
| 152 | + | body: body, | |
| 153 | + | url: apod_url | |
| 154 | + | }, | |
| 155 | + | meta: #{ | |
| 156 | + | source_name: "NASA APOD", | |
| 157 | + | published_at: timestamp_now(), | |
| 158 | + | tags: ["nasa", "astronomy", "space"] | |
| 159 | + | } | |
| 160 | + | } | |
| 161 | + | } |
| @@ -0,0 +1,213 @@ | |||
| 1 | + | // NWS Weather Alerts Plugin for BalancedBreakfast | |
| 2 | + | // Fetches active weather alerts from the National Weather Service API | |
| 3 | + | ||
| 4 | + | const NWS_API = "https://api.weather.gov/alerts/active"; | |
| 5 | + | ||
| 6 | + | fn id() { | |
| 7 | + | "nws_alerts" | |
| 8 | + | } | |
| 9 | + | ||
| 10 | + | fn name() { | |
| 11 | + | "NWS Weather Alerts" | |
| 12 | + | } | |
| 13 | + | ||
| 14 | + | fn capabilities() { | |
| 15 | + | #{ | |
| 16 | + | supports_pagination: false | |
| 17 | + | } | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | fn config_schema() { | |
| 21 | + | #{ | |
| 22 | + | description: "Active weather alerts from the US National Weather Service. Filter by state or view all active alerts.", | |
| 23 | + | fields: [ | |
| 24 | + | #{ | |
| 25 | + | key: "area", | |
| 26 | + | label: "State/Territory", | |
| 27 | + | field_type: "text", | |
| 28 | + | description: "Two-letter state code (e.g., CA, NY, TX). Leave blank for all US alerts.", | |
| 29 | + | placeholder: "CA" | |
| 30 | + | }, | |
| 31 | + | #{ | |
| 32 | + | key: "severity", | |
| 33 | + | label: "Minimum Severity", | |
| 34 | + | field_type: "select", | |
| 35 | + | required: true, | |
| 36 | + | description: "Minimum alert severity to show", | |
| 37 | + | default_value: "Moderate", | |
| 38 | + | options: ["Extreme", "Severe", "Moderate", "Minor"] | |
| 39 | + | } | |
| 40 | + | ] | |
| 41 | + | } | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | fn fetch(config, cursor) { | |
| 45 | + | let url = NWS_API + "?status=actual"; | |
| 46 | + | ||
| 47 | + | if config.area != () && config.area != "" { | |
| 48 | + | let area = str_trim(config.area); | |
| 49 | + | if area.len() == 2 { | |
| 50 | + | url += "&area=" + area; | |
| 51 | + | } | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | let severity = "Moderate"; | |
| 55 | + | if config.severity != () { | |
| 56 | + | severity = config.severity; | |
| 57 | + | } | |
| 58 | + | url += "&severity=" + severity; | |
| 59 | + | ||
| 60 | + | let data = http_get_json(url); | |
| 61 | + | ||
| 62 | + | if data == () { | |
| 63 | + | return #{ items: [], has_more: false }; | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | let features = data.features; | |
| 67 | + | if features == () { | |
| 68 | + | return #{ items: [], has_more: false }; | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | let items = []; | |
| 72 | + | for feature in features { | |
| 73 | + | let item = parse_alert(feature); | |
| 74 | + | if item != () { | |
| 75 | + | items.push(item); | |
| 76 | + | } | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | #{ | |
| 80 | + | items: items, | |
| 81 | + | has_more: false | |
| 82 | + | } | |
| 83 | + | } | |
| 84 | + | ||
| 85 | + | fn parse_alert(feature) { | |
| 86 | + | let props = feature.properties; | |
| 87 | + | if props == () { | |
| 88 | + | return (); | |
| 89 | + | } | |
| 90 | + | ||
| 91 | + | let headline = "Weather Alert"; | |
| 92 | + | if props.headline != () { | |
| 93 | + | headline = props.headline; | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | let event = ""; | |
| 97 | + | if props.event != () { | |
| 98 | + | event = props.event; | |
| 99 | + | } | |
| 100 | + | ||
| 101 | + | let severity = "Unknown"; | |
| 102 | + | if props.severity != () { | |
| 103 | + | severity = props.severity; | |
| 104 | + | } | |
| 105 | + | ||
| 106 | + | let urgency = ""; | |
| 107 | + | if props.urgency != () { | |
| 108 | + | urgency = props.urgency; | |
| 109 | + | } | |
| 110 | + | ||
| 111 | + | let certainty = ""; | |
| 112 | + | if props.certainty != () { | |
| 113 | + | certainty = props.certainty; | |
| 114 | + | } | |
| 115 | + | ||
| 116 | + | let description = ""; | |
| 117 | + | if props.description != () { | |
| 118 | + | description = props.description; | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | let instruction = ""; | |
| 122 | + | if props.instruction != () { | |
| 123 | + | instruction = props.instruction; | |
| 124 | + | } | |
| 125 | + | ||
| 126 | + | let area_desc = ""; | |
| 127 | + | if props.areaDesc != () { | |
| 128 | + | area_desc = props.areaDesc; | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | let sender = "NWS"; | |
| 132 | + | if props.senderName != () { | |
| 133 | + | sender = props.senderName; | |
| 134 | + | } | |
| 135 | + | ||
| 136 | + | let effective = timestamp_now(); | |
| 137 | + | if props.effective != () { | |
| 138 | + | effective = props.effective; | |
| 139 | + | } | |
| 140 | + | ||
| 141 | + | let expires = ""; | |
| 142 | + | if props.expires != () { | |
| 143 | + | expires = props.expires; | |
| 144 | + | } | |
| 145 | + | ||
| 146 | + | let alert_id = ""; | |
| 147 | + | if feature.id != () { | |
| 148 | + | alert_id = feature.id; | |
| 149 | + | } else if props.id != () { | |
| 150 | + | alert_id = props.id; | |
| 151 | + | } | |
| 152 | + | ||
| 153 | + | // Build body | |
| 154 | + | let body = ""; | |
| 155 | + | if area_desc != "" { | |
| 156 | + | body += "**Area:** " + area_desc + "\n\n"; | |
| 157 | + | } | |
| 158 | + | body += "**Severity:** " + severity; | |
| 159 | + | if urgency != "" { | |
| 160 | + | body += " ยท **Urgency:** " + urgency; | |
| 161 | + | } | |
| 162 | + | if certainty != "" { | |
| 163 | + | body += " ยท **Certainty:** " + certainty; | |
| 164 | + | } | |
| 165 | + | body += "\n\n"; | |
| 166 | + | ||
| 167 | + | if description != "" { | |
| 168 | + | body += description + "\n\n"; | |
| 169 | + | } | |
| 170 | + | if instruction != "" { | |
| 171 | + | body += "**Instructions:** " + instruction; | |
| 172 | + | } | |
| 173 | + | ||
| 174 | + | // Indicator based on severity | |
| 175 | + | let indicator = "๐ค"; | |
| 176 | + | if severity == "Extreme" { | |
| 177 | + | indicator = "๐ด"; | |
| 178 | + | } else if severity == "Severe" { | |
| 179 | + | indicator = "๐ "; | |
| 180 | + | } else if severity == "Moderate" { | |
| 181 | + | indicator = "๐ก"; | |
| 182 | + | } | |
| 183 | + | ||
| 184 | + | let secondary = severity; | |
| 185 | + | if area_desc != "" { | |
| 186 | + | secondary += " ยท " + truncate(area_desc, 60); | |
| 187 | + | } | |
| 188 | + | ||
| 189 | + | let tags = ["weather", "nws"]; | |
| 190 | + | if event != "" { | |
| 191 | + | tags.push(event); | |
| 192 | + | } | |
| 193 | + | ||
| 194 | + | #{ | |
| 195 | + | id: #{ source: "nws_alerts", item_id: alert_id }, | |
| 196 | + | bite: #{ | |
| 197 | + | author: sender, | |
| 198 | + | text: truncate(headline, 100), | |
| 199 | + | secondary: truncate(secondary, 80), | |
| 200 | + | indicator: indicator | |
| 201 | + | }, | |
| 202 | + | content: #{ | |
| 203 | + | title: headline, | |
| 204 | + | body: body, | |
| 205 | + | url: "" | |
| 206 | + | }, | |
| 207 | + | meta: #{ | |
| 208 | + | source_name: "NWS Alerts", | |
| 209 | + | published_at: effective, | |
| 210 | + | tags: tags | |
| 211 | + | } | |
| 212 | + | } | |
| 213 | + | } |
| @@ -0,0 +1,159 @@ | |||
| 1 | + | // XKCD Plugin for BalancedBreakfast | |
| 2 | + | // Fetches comics from xkcd.com via their JSON API | |
| 3 | + | ||
| 4 | + | const XKCD_URL = "https://xkcd.com"; | |
| 5 | + | ||
| 6 | + | fn id() { | |
| 7 | + | "xkcd" | |
| 8 | + | } | |
| 9 | + | ||
| 10 | + | fn name() { | |
| 11 | + | "XKCD" | |
| 12 | + | } | |
| 13 | + | ||
| 14 | + | fn capabilities() { | |
| 15 | + | #{ | |
| 16 | + | supports_pagination: true | |
| 17 | + | } | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | fn config_schema() { | |
| 21 | + | #{ | |
| 22 | + | description: "Fetch comics from XKCD. Shows the latest comics with title, alt text, and image.", | |
| 23 | + | fields: [ | |
| 24 | + | #{ | |
| 25 | + | key: "count", | |
| 26 | + | label: "Comics to fetch", | |
| 27 | + | field_type: "text", | |
| 28 | + | description: "Number of recent comics to fetch (max 20)", | |
| 29 | + | default_value: "10", | |
| 30 | + | placeholder: "10" | |
| 31 | + | } | |
| 32 | + | ] | |
| 33 | + | } | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | fn fetch(config, cursor) { | |
| 37 | + | let count = 10; | |
| 38 | + | if config.count != () { | |
| 39 | + | let parsed = parse_int(config.count); | |
| 40 | + | if parsed != () && parsed > 0 { | |
| 41 | + | if parsed > 20 { | |
| 42 | + | count = 20; | |
| 43 | + | } else { | |
| 44 | + | count = parsed; | |
| 45 | + | } | |
| 46 | + | } | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | // Get the latest comic to find the current number | |
| 50 | + | let latest = http_get_json(XKCD_URL + "/info.0.json"); | |
| 51 | + | if latest == () { | |
| 52 | + | return #{ items: [], has_more: false }; | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | let current_num = latest.num; | |
| 56 | + | ||
| 57 | + | // Parse cursor for starting comic number | |
| 58 | + | let start_num = current_num; | |
| 59 | + | if cursor != () { | |
| 60 | + | let parsed = parse_int(cursor); | |
| 61 | + | if parsed != () && parsed > 0 { | |
| 62 | + | start_num = parsed; | |
| 63 | + | } | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | let items = []; | |
| 67 | + | let num = start_num; | |
| 68 | + | let fetched = 0; | |
| 69 | + | ||
| 70 | + | while fetched < count && num > 0 { | |
| 71 | + | // Comic 404 doesn't exist (it's a joke) | |
| 72 | + | if num == 404 { | |
| 73 | + | num -= 1; | |
| 74 | + | continue; | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | let comic = (); | |
| 78 | + | if num == current_num { | |
| 79 | + | comic = latest; | |
| 80 | + | } else { | |
| 81 | + | comic = http_get_json(XKCD_URL + "/" + num + "/info.0.json"); | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | if comic != () { | |
| 85 | + | let item = parse_comic(comic); | |
| 86 | + | if item != () { | |
| 87 | + | items.push(item); | |
| 88 | + | fetched += 1; | |
| 89 | + | } | |
| 90 | + | } | |
| 91 | + | ||
| 92 | + | num -= 1; | |
| 93 | + | } | |
| 94 | + | ||
| 95 | + | let has_more = num > 0; | |
| 96 | + | let result = #{ | |
| 97 | + | items: items, | |
| 98 | + | has_more: has_more | |
| 99 | + | }; | |
| 100 | + | ||
| 101 | + | if has_more { | |
| 102 | + | result.next_cursor = "" + num; | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | result | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | fn parse_comic(comic) { | |
| 109 | + | if comic.title == () { | |
| 110 | + | return (); | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | let title = comic.title; | |
| 114 | + | let num = comic.num; | |
| 115 | + | ||
| 116 | + | let alt = ""; | |
| 117 | + | if comic.alt != () { | |
| 118 | + | alt = comic.alt; | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | let img = ""; | |
| 122 | + | if comic.img != () { | |
| 123 | + | img = comic.img; | |
| 124 | + | } | |
| 125 | + | ||
| 126 | + | let url = XKCD_URL + "/" + num + "/"; | |
| 127 | + | ||
| 128 | + | // Build a simple HTML body with the comic image and alt text | |
| 129 | + | let body = ""; | |
| 130 | + | if img != "" { | |
| 131 | + | body = `<img src="` + img + `" alt="` + title + `" style="max-width:100%">`; | |
| 132 | + | } | |
| 133 | + | if alt != "" { | |
| 134 | + | body += "\n\n_" + alt + "_"; | |
| 135 | + | } | |
| 136 | + | ||
| 137 | + | // Build date from comic fields | |
| 138 | + | let published = timestamp_now(); | |
| 139 | + | ||
| 140 | + | #{ | |
| 141 | + | id: #{ source: "xkcd", item_id: "" + num }, | |
| 142 | + | bite: #{ | |
| 143 | + | author: "xkcd", | |
| 144 | + | text: "#" + num + ": " + truncate(title, 90), | |
| 145 | + | secondary: truncate(alt, 80), | |
| 146 | + | indicator: "๐" | |
| 147 | + | }, | |
| 148 | + | content: #{ | |
| 149 | + | title: "#" + num + ": " + title, | |
| 150 | + | body: body, | |
| 151 | + | url: url | |
| 152 | + | }, | |
| 153 | + | meta: #{ | |
| 154 | + | source_name: "XKCD", | |
| 155 | + | published_at: published, | |
| 156 | + | tags: ["xkcd", "comics"] | |
| 157 | + | } | |
| 158 | + | } | |
| 159 | + | } |