this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: remove deprecated API endpoints, add /api/v1/preview

Remove all legacy/deprecated routes: /irclink/, *.cgi aliases,
/v0/* routes, /buttons/, /api/caching/invalidate, and
/api/kitten/fetch. Move /ogpreview to /api/v1/preview and
update the frontend JS accordingly.

Remove dead code: ButtonHandler, irclinkURL template funcs,
buttons assets and template. Update nav to remove buttons
links. Migrate test scripts to use v1 API endpoints. Update
OpenAPI spec and README.

+190 -758
+1 -1
README.md
··· 173 173 - **IRC Source** (`source=irc`): Returns ID with duplicate marker if applicable 174 174 - **HTML**: Shows duplicate notification with original poster and timestamp 175 175 176 - The legacy `/irclink/` endpoint is still supported but `/link/` is preferred. 176 + Use `POST /api/v1/links` for programmatic link submission. 177 177 178 178 ### Link and Quote Deletion 179 179
+2 -24
cmd/tumble/main.go
··· 295 295 296 296 // Main Routes 297 297 mux.HandleFunc("/", h.Index) 298 - mux.HandleFunc("/index.cgi", h.Index) 299 298 mux.HandleFunc("/stats", h.Stats) 300 299 mux.HandleFunc("/stats.json", h.StatsJSON) 301 300 mux.HandleFunc("/search", h.Search) 302 - mux.HandleFunc("/search.cgi", h.Search) 303 - mux.HandleFunc("/link/", h.IRCLinkHandler) // Primary endpoint for links 304 - mux.HandleFunc("/irclink/", h.IRCLinkHandler) // Legacy endpoint (backwards compatibility) 305 - 306 - mux.HandleFunc("/ogpreview", h.OGPreviewHandler) 307 - mux.HandleFunc("/ogpreview.cgi", h.OGPreviewHandler) 308 - mux.HandleFunc("/api/caching/invalidate", h.InvalidateCacheHandler) 309 - mux.HandleFunc("/api/kitten/fetch", h.FetchKittenHandler) 310 - mux.HandleFunc("/buttons/", h.ButtonHandler) // Handle /buttons/ with ButtonHandler (landing + result) 311 - mux.HandleFunc("/buttons/button.cgi", h.ButtonHandler) // Legacy explicit path 312 - 313 - // v0 Routes (Aliased) 314 - mux.HandleFunc("/v0/", h.Index) 315 - mux.HandleFunc("/v0/index.cgi", h.Index) 316 - mux.HandleFunc("/v0/search.cgi", h.Search) 317 - mux.HandleFunc("/v0/link/", h.IRCLinkHandler) // Primary v0 endpoint 318 - mux.HandleFunc("/v0/irclink/", h.IRCLinkHandler) // Legacy v0 endpoint 319 - mux.HandleFunc("/v0/ogpreview.cgi", h.OGPreviewHandler) 320 - mux.HandleFunc("/v0/quote/", h.QuoteHandler) 321 - 322 - // Quote Handler (Legacy) 301 + mux.HandleFunc("/link/", h.IRCLinkHandler) 323 302 mux.HandleFunc("/quote/", h.QuoteHandler) 324 - mux.HandleFunc("/quote/index.cgi", h.QuoteHandler) 325 303 326 304 // SEO Routes 327 305 mux.HandleFunc("/sitemap.xml", h.SitemapHandler) ··· 333 311 fileServer := http.FileServer(http.FS(assets.StaticFS)) 334 312 mux.Handle("/css/", fileServer) 335 313 mux.Handle("/img/", fileServer) 336 - // mux.Handle("/buttons/", fileServer) // Removed in favor of ButtonHandler check 337 314 mux.Handle("/favicon.ico", fileServer) 338 315 // Legacy static files 339 316 mux.Handle("/apple-touch-icon.png", fileServer) ··· 355 332 mux.HandleFunc("/api/v1/search", h.APIv1SearchHandler) 356 333 mux.HandleFunc("/api/v1/cache", h.APIv1CacheHandler) 357 334 mux.HandleFunc("/api/v1/kittens/", h.APIv1KittensDailyHandler) 335 + mux.HandleFunc("/api/v1/preview", h.OGPreviewHandler) 358 336 359 337 // Public redirect shortlink 360 338 mux.HandleFunc("/go/", h.APIv1RedirectHandler)
+115
docs/plans/2026-02-12-remove-deprecated-endpoints-design.md
··· 1 + # Remove Deprecated API Endpoints 2 + 3 + ## Goal 4 + 5 + Remove all legacy/deprecated API endpoints and consolidate on the v1 6 + API. Move `/ogpreview` under the v1 namespace as `/api/v1/preview`. 7 + 8 + ## Scope 9 + 10 + ### Routes to Remove (`cmd/tumble/main.go`) 11 + 12 + | Route | Handler | Reason | 13 + |-------|---------|--------| 14 + | `/irclink/` | `IRCLinkHandler` | Replaced by `/api/v1/links` | 15 + | `/index.cgi` | `Index` | CGI alias for `/` | 16 + | `/search.cgi` | `Search` | CGI alias for `/search` | 17 + | `/ogpreview` | `OGPreviewHandler` | Moving to `/api/v1/preview` | 18 + | `/ogpreview.cgi` | `OGPreviewHandler` | CGI alias | 19 + | `/buttons/` | `ButtonHandler` | Feature removed | 20 + | `/buttons/button.cgi` | `ButtonHandler` | Feature removed | 21 + | `/v0/` | `Index` | v0 alias | 22 + | `/v0/index.cgi` | `Index` | v0 alias | 23 + | `/v0/search.cgi` | `Search` | v0 alias | 24 + | `/v0/link/` | `IRCLinkHandler` | v0 alias | 25 + | `/v0/irclink/` | `IRCLinkHandler` | v0 alias | 26 + | `/v0/ogpreview.cgi` | `OGPreviewHandler` | v0 alias | 27 + | `/v0/quote/` | `QuoteHandler` | v0 alias | 28 + | `/api/caching/invalidate` | `InvalidateCacheHandler` | Replaced by `DELETE /api/v1/cache` | 29 + | `/api/kitten/fetch` | `FetchKittenHandler` | Replaced by `PUT /api/v1/kittens/daily` | 30 + 31 + ### Route to Add 32 + 33 + | Route | Handler | Notes | 34 + |-------|---------|-------| 35 + | `/api/v1/preview` | `OGPreviewHandler` | Replaces `/ogpreview` | 36 + 37 + ### Routes Kept (not deprecated) 38 + 39 + `/`, `/link/`, `/quote/`, `/search`, `/stats`, `/stats.json`, 40 + `/index.xml`, `/go/`, `/api/v1/*`, `/api/docs` 41 + 42 + ## Dead Code Removal 43 + 44 + ### Handler code 45 + 46 + - `ButtonHandler()` in `internal/handler/handlers.go` 47 + 48 + ### Template functions 49 + 50 + - `irclinkURL` in `internal/templates/renderer.go` (both HTML and 51 + XML variants) 52 + 53 + ### Templates 54 + 55 + - `internal/templates/views/tumble_buttons.html` 56 + 57 + ### Static assets 58 + 59 + - `internal/assets/buttons/` directory (`button.cgi`, `index.html`) 60 + 61 + ### Template updates 62 + 63 + - `internal/templates/views/header.html` -- remove `/buttons/` from 64 + desktop nav and mobile drawer nav 65 + - `internal/templates/views/index.html` -- change `/ogpreview` JS 66 + call to `/api/v1/preview`, remove `/buttons/` links 67 + 68 + ## Test Updates 69 + 70 + ### `tests/add_link.sh` 71 + 72 + Change from: 73 + ``` 74 + curl -s "$BASE_URL/irclink/?user=$USER&url=$URL&source=irc" 75 + ``` 76 + To POST to `/api/v1/links` with JSON body. 77 + 78 + ### `tests/delete_link.sh` 79 + 80 + Change from: 81 + ``` 82 + curl -v -X DELETE "$BASE_URL/irclink/?id=$ID" 83 + ``` 84 + To `DELETE /api/v1/links/$ID` with `X-API-Key` header. 85 + 86 + ### `tests/api_test.sh` 87 + 88 + - Remove tests for `/irclink/`, `/search.cgi`, `/v0/*`, 89 + `/ogpreview.cgi` 90 + - Add test for `/api/v1/preview` 91 + - Keep tests for routes that remain (`/`, `/search`, etc.) 92 + 93 + ## Documentation Updates 94 + 95 + ### `internal/assets/openapi.json` 96 + 97 + - Add `/api/v1/preview` endpoint with query parameter `url` and 98 + JSON response schema 99 + - Remove any references to deprecated endpoints 100 + 101 + ### `README.md` 102 + 103 + - Remove `/irclink/` documentation 104 + - Update endpoint references to v1 API 105 + 106 + ## Implementation Order 107 + 108 + 1. Add `/api/v1/preview` route 109 + 2. Update frontend JS to use `/api/v1/preview` 110 + 3. Remove deprecated routes from `main.go` 111 + 4. Remove dead handler code, templates, and assets 112 + 5. Update nav templates 113 + 6. Update test scripts 114 + 7. Update OpenAPI spec and README 115 + 8. Run `make test` and `make test-api` to verify
-82
internal/assets/buttons/button.cgi
··· 1 - #!/usr/bin/perl -w 2 - 3 - use CGI; 4 - 5 - use YAML qw( LoadFile ); 6 - 7 - use strict; 8 - 9 - my $cgi = new CGI; 10 - 11 - my $user = $cgi->param( 'user' ); 12 - 13 - my $config = LoadFile( '../config.yaml' ); 14 - my $url = $config->{'baseurl'}; 15 - 16 - if ( $user ) { 17 - print "Content-type: text/html\n\n"; 18 - 19 - print qq(<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> 20 - <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> 21 - 22 - <head> 23 - <title>tumblefish buttons</title> 24 - <script type="text/javascript"> 25 - var _gaq = _gaq || []; 26 - _gaq.push(['_setAccount', 'UA-24593498-1']); 27 - _gaq.push(['_trackPageview']); 28 - (function() { 29 - var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; 30 - ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; 31 - var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); 32 - })(); 33 - </script> 34 - <link rel="stylesheet" href="$url/css/screen.css" type="text/css" media="screen" /> 35 - </head> 36 - 37 - <body> 38 - <div id="page"> 39 - <div id="masthead"> 40 - tumblefish. 41 - </div> 42 - 43 - <div id="content"> 44 - <div class="tumble_date"> 45 - <div class="tumble_date_date">!!</div> 46 - <div class="tumble_date_mon">buttons</div> 47 - <div class="tumble_date_day">yay!</div> 48 - </div> 49 - <div class="tumble_item_quote"> 50 - <div class="tumble_item_top"></div> 51 - <span class="tumble_item_quote_quote">So how do I install this crap??</span> 52 - <div class="tumble_item_bottom"></div> 53 - </div> 54 - <div class="tumble_item_ircLink"> 55 - <div class="tumble_item_top"></div> 56 - <span class="tumble_item_ircLink_title">); 57 - print qq(Drag this link: <a href="javascript:location.href='$url/irclink/?user=$user&source=web&url='+encodeURIComponent(location.href)" onclick="window.alert('No clicky! Drag this link to your Bookmarks toolbar or menu, or right-click it and choose Bookmark This Link...');return false;">post to tumblefish!</a> up to your Bookmarks toolbar or menu.); 58 - print qq(</span> 59 - <div class="tumble_item_bottom"></div> 60 - <div> 61 - <div class="tumble_item_ircLink"> 62 - <div class="tumble_item_top"></div> 63 - <span class="tumble_item_ircLink_title">PS - Unfortunately, tumblebuttons don't work with Microsoft Internet Explorer. MSIE sucks. Stop using it.</span> 64 - <div class="tumble_item_bottom"></div> 65 - </div> 66 - <div> 67 - <div class="tumble_item_ircLink"> 68 - <div class="tumble_item_top"></div> 69 - <span class="tumble_item_ircLink_title">PPS - If your name is Greg Buchanan and you just read the above postscript, you can suck my ass.</span> 70 - <div class="tumble_item_bottom"></div> 71 - 72 - </div> 73 - </div> 74 - </body> 75 - 76 - </html> 77 - ); 78 - } 79 - else { 80 - print "Content-type: text/plain\n\n"; 81 - print "Oh no! You didn't enter your name!"; 82 - }
-56
internal/assets/buttons/index.html
··· 1 - <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> 2 - <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> 3 - 4 - <head> 5 - <title>tumblefish buttons</title> 6 - <script type="text/javascript"> 7 - var _gaq = _gaq || []; 8 - _gaq.push(['_setAccount', 'UA-24593498-1']); 9 - _gaq.push(['_trackPageview']); 10 - (function() { 11 - var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; 12 - ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; 13 - var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); 14 - })(); 15 - </script> 16 - <link rel="stylesheet" href="/css/screen.css" type="text/css" media="screen" /> 17 - </head> 18 - 19 - <body> 20 - <div id="page"> 21 - <div id="masthead"> 22 - tumblefish. 23 - </div> 24 - 25 - <div id="content"> 26 - <div class="tumble_date"> 27 - <div class="tumble_date_date">!!</div> 28 - <div class="tumble_date_mon">buttons</div> 29 - <div class="tumble_date_day">yay!</div> 30 - </div> 31 - <div class="tumble_item_quote"> 32 - <div class="tumble_item_top"></div> 33 - <span class="tumble_item_quote_quote">WTF is a tumblebutton?!?</span> 34 - <div class="tumble_item_bottom"></div> 35 - </div> 36 - <div class="tumble_item_ircLink"> 37 - <div class="tumble_item_top"></div> 38 - <span class="tumble_item_ircLink_title">Buttons (bookmarklets) are links you add to your browser's Bookmarks toolbar or menu. They are an easy way to post your awesome links to tumblefish.</span> 39 - <div class="tumble_item_bottom"></div> 40 - </div> 41 - <div class="tumble_item_ircLink"> 42 - <div class="tumble_item_top"></div> 43 - <span class="tumble_item_ircLink_title">To get started, type your name in the box below.</span> 44 - <div class="tumble_item_bottom"></div> 45 - </div> 46 - <div class="tumble_item_ircLink"> 47 - <div class="tumble_item_top"></div> 48 - <span class="tumble_item_ircLink_title"><form method="post" action="button.cgi" name=""><input name="user" /><input type="submit" name="" value=" GO!! " /></form></span> 49 - <div class="tumble_item_bottom"></div> 50 - </div> 51 - 52 - </div> 53 - </div> 54 - </body> 55 - 56 - </html>
+1 -1
internal/assets/fs.go
··· 2 2 3 3 import "embed" 4 4 5 - //go:embed css img buttons favicon.ico openapi.json robots.txt apple-touch-icon.png 2202 roast 5 + //go:embed css img favicon.ico openapi.json robots.txt apple-touch-icon.png 2202 roast 6 6 var StaticFS embed.FS
+45 -412
internal/assets/openapi.json
··· 2 2 "openapi": "3.0.0", 3 3 "info": { 4 4 "title": "Tumble API", 5 - "description": "API for the Tumble content application. The v1 API provides a RESTful, versioned interface with consistent request/response formats. Legacy endpoints are deprecated and will be removed in a future release.", 5 + "description": "API for the Tumble content application. The v1 API provides a RESTful, versioned interface with consistent request/response formats.", 6 6 "version": "2.0.0" 7 7 }, 8 8 "servers": [ ··· 2024 2024 } 2025 2025 } 2026 2026 }, 2027 - "/api/caching/invalidate": { 2027 + "/api/v1/preview": { 2028 2028 "get": { 2029 - "summary": "Invalidate Link Preview Cache (Deprecated)", 2030 - "description": "DEPRECATED: Use DELETE /api/v1/cache instead. Removes a URL from the server-side link preview cache.", 2031 - "deprecated": true, 2032 - "tags": ["Deprecated"], 2029 + "summary": "Get OpenGraph Preview", 2030 + "description": "Fetches OpenGraph metadata for a URL. Returns cached data if available, otherwise fetches fresh metadata. Supports special handling for YouTube, Reddit, Twitter, Flickr, and OEmbed providers.", 2031 + "tags": ["Preview"], 2033 2032 "parameters": [ 2034 2033 { 2035 2034 "name": "url", 2036 2035 "in": "query", 2037 - "description": "The URL to invalidate", 2036 + "description": "The URL to fetch OpenGraph metadata for", 2038 2037 "required": true, 2039 2038 "schema": { 2040 2039 "type": "string" ··· 2043 2042 ], 2044 2043 "responses": { 2045 2044 "200": { 2046 - "description": "Cache Invalidated", 2045 + "description": "OpenGraph metadata for the URL", 2047 2046 "content": { 2048 2047 "application/json": { 2049 2048 "schema": { 2050 2049 "type": "object", 2051 2050 "properties": { 2052 - "status": { "type": "string" }, 2053 - "message": { "type": "string" } 2051 + "title": { 2052 + "type": "string", 2053 + "description": "Page title" 2054 + }, 2055 + "description": { 2056 + "type": "string", 2057 + "description": "Page description" 2058 + }, 2059 + "image": { 2060 + "type": "string", 2061 + "description": "Preview image URL" 2062 + }, 2063 + "icon": { 2064 + "type": "string", 2065 + "description": "Site favicon URL" 2066 + }, 2067 + "type": { 2068 + "type": "string", 2069 + "description": "Content type (e.g. video, rich)" 2070 + }, 2071 + "html": { 2072 + "type": "string", 2073 + "description": "Embed HTML for video/rich content" 2074 + }, 2075 + "status": { 2076 + "type": "integer", 2077 + "description": "HTTP status code from fetching the URL" 2078 + } 2054 2079 } 2080 + }, 2081 + "example": { 2082 + "title": "Example Article", 2083 + "description": "An interesting article about something", 2084 + "image": "https://example.com/image.jpg", 2085 + "icon": "https://example.com/favicon.ico" 2055 2086 } 2056 2087 } 2057 2088 } 2058 2089 }, 2059 2090 "400": { 2060 - "description": "Missing URL" 2061 - } 2062 - } 2063 - } 2064 - }, 2065 - "/api/kitten/fetch": { 2066 - "post": { 2067 - "summary": "Force Fetch Daily Kitten (Deprecated)", 2068 - "description": "DEPRECATED: Use PUT /api/v1/kittens/daily instead. Forces a new daily kitten to be fetched from cataas.com.", 2069 - "deprecated": true, 2070 - "tags": ["Deprecated"], 2071 - "responses": { 2072 - "200": { 2073 - "description": "Kitten fetched successfully", 2074 - "content": { 2075 - "application/json": { 2076 - "schema": { 2077 - "type": "object", 2078 - "properties": { 2079 - "status": { "type": "string", "example": "ok" }, 2080 - "url": { "type": "string", "example": "https://cataas.com/cat/abc123xyz" } 2081 - } 2082 - } 2083 - } 2084 - } 2085 - }, 2086 - "405": { 2087 - "description": "Method not allowed (use POST)" 2088 - }, 2089 - "500": { 2090 - "description": "Failed to fetch kitten", 2091 - "content": { 2092 - "application/json": { 2093 - "schema": { 2094 - "type": "object", 2095 - "properties": { 2096 - "status": { "type": "string", "example": "error" }, 2097 - "error": { "type": "string" } 2098 - } 2099 - } 2100 - } 2101 - } 2102 - } 2103 - } 2104 - } 2105 - }, 2106 - "/link/": { 2107 - "post": { 2108 - "summary": "Submit a new Link (Deprecated)", 2109 - "description": "DEPRECATED: Use POST /api/v1/links instead. Submits a new link using query parameters.", 2110 - "deprecated": true, 2111 - "tags": ["Deprecated"], 2112 - "parameters": [ 2113 - { 2114 - "name": "user", 2115 - "in": "query", 2116 - "description": "The username of the submitter", 2117 - "required": true, 2118 - "schema": { 2119 - "type": "string" 2120 - } 2121 - }, 2122 - { 2123 - "name": "url", 2124 - "in": "query", 2125 - "description": "The URL to submit", 2126 - "required": true, 2127 - "schema": { 2128 - "type": "string" 2129 - } 2130 - }, 2131 - { 2132 - "name": "source", 2133 - "in": "query", 2134 - "description": "Source of submission", 2135 - "schema": { 2136 - "type": "string", 2137 - "enum": ["irc", "api", "web"] 2138 - } 2139 - } 2140 - ], 2141 - "responses": { 2142 - "201": { 2143 - "description": "Link created" 2144 - }, 2145 - "208": { 2146 - "description": "Duplicate link created" 2147 - }, 2148 - "500": { 2149 - "description": "Server Error" 2150 - } 2151 - } 2152 - } 2153 - }, 2154 - "/link/{id}": { 2155 - "get": { 2156 - "summary": "Redirect to a Link (Deprecated)", 2157 - "description": "DEPRECATED: Use GET /go/{id} instead. Redirects to the URL associated with the given ID.", 2158 - "deprecated": true, 2159 - "tags": ["Deprecated"], 2160 - "parameters": [ 2161 - { 2162 - "name": "id", 2163 - "in": "path", 2164 - "description": "The ID of the link", 2165 - "required": true, 2166 - "schema": { 2167 - "type": "integer" 2168 - } 2169 - } 2170 - ], 2171 - "responses": { 2172 - "302": { 2173 - "description": "Redirects to the target URL" 2174 - }, 2175 - "404": { 2176 - "description": "Link not found" 2177 - } 2178 - } 2179 - }, 2180 - "delete": { 2181 - "summary": "Delete a link (Deprecated)", 2182 - "description": "DEPRECATED: Use DELETE /api/v1/links/{id} instead.", 2183 - "deprecated": true, 2184 - "tags": ["Deprecated"], 2185 - "parameters": [ 2186 - { 2187 - "name": "id", 2188 - "in": "path", 2189 - "description": "The ID of the link", 2190 - "required": true, 2191 - "schema": { 2192 - "type": "integer" 2193 - } 2194 - } 2195 - ], 2196 - "responses": { 2197 - "200": { 2198 - "description": "Link deleted" 2199 - }, 2200 - "403": { 2201 - "description": "Forbidden" 2091 + "description": "Missing url parameter" 2202 2092 }, 2203 2093 "404": { 2204 - "description": "Not found" 2205 - } 2206 - } 2207 - } 2208 - }, 2209 - "/link/{id}.json": { 2210 - "get": { 2211 - "summary": "Get Link Metadata (Deprecated)", 2212 - "description": "DEPRECATED: Use GET /api/v1/links/{id} instead. Returns link metadata as JSON.", 2213 - "deprecated": true, 2214 - "tags": ["Deprecated"], 2215 - "parameters": [ 2216 - { 2217 - "name": "id", 2218 - "in": "path", 2219 - "description": "The ID of the link", 2220 - "required": true, 2221 - "schema": { 2222 - "type": "integer" 2223 - } 2224 - } 2225 - ], 2226 - "responses": { 2227 - "200": { 2228 - "description": "Link metadata", 2229 - "content": { 2230 - "application/json": { 2231 - "schema": { 2232 - "type": "object", 2233 - "properties": { 2234 - "ircLinkID": { "type": "integer" }, 2235 - "timestamp": { "type": "string", "format": "date-time" }, 2236 - "user": { "type": "string" }, 2237 - "title": { "type": "string" }, 2238 - "url": { "type": "string" }, 2239 - "clicks": { "type": "integer" } 2240 - } 2241 - } 2242 - } 2243 - } 2244 - }, 2245 - "404": { 2246 - "description": "Link not found" 2247 - } 2248 - } 2249 - } 2250 - }, 2251 - "/quote/": { 2252 - "get": { 2253 - "summary": "Get a Random Quote (Deprecated)", 2254 - "description": "DEPRECATED: Use GET /api/v1/quotes instead.", 2255 - "deprecated": true, 2256 - "tags": ["Deprecated"], 2257 - "responses": { 2258 - "200": { 2259 - "description": "Random Quote" 2260 - } 2261 - } 2262 - }, 2263 - "post": { 2264 - "summary": "Submit a Quote (Deprecated)", 2265 - "description": "DEPRECATED: Use POST /api/v1/quotes instead.", 2266 - "deprecated": true, 2267 - "tags": ["Deprecated"], 2268 - "requestBody": { 2269 - "content": { 2270 - "application/x-www-form-urlencoded": { 2271 - "schema": { 2272 - "type": "object", 2273 - "properties": { 2274 - "quote": { "type": "string" }, 2275 - "author": { "type": "string" }, 2276 - "poster": { "type": "string" } 2277 - }, 2278 - "required": ["quote"] 2279 - } 2280 - } 2281 - } 2282 - }, 2283 - "responses": { 2284 - "200": { 2285 - "description": "Quote created" 2286 - }, 2287 - "400": { 2288 - "description": "Missing inputs" 2289 - } 2290 - } 2291 - } 2292 - }, 2293 - "/quote/{id}": { 2294 - "get": { 2295 - "summary": "Get Quote by ID (Deprecated)", 2296 - "description": "DEPRECATED: Use GET /api/v1/quotes/{id} instead.", 2297 - "deprecated": true, 2298 - "tags": ["Deprecated"], 2299 - "parameters": [ 2300 - { 2301 - "name": "id", 2302 - "in": "path", 2303 - "description": "The ID of the quote", 2304 - "required": true, 2305 - "schema": { 2306 - "type": "integer" 2307 - } 2308 - } 2309 - ], 2310 - "responses": { 2311 - "200": { 2312 - "description": "Quote found" 2313 - }, 2314 - "404": { 2315 - "description": "Quote not found" 2316 - } 2317 - } 2318 - }, 2319 - "delete": { 2320 - "summary": "Delete a quote (Deprecated)", 2321 - "description": "DEPRECATED: Use DELETE /api/v1/quotes/{id} instead.", 2322 - "deprecated": true, 2323 - "tags": ["Deprecated"], 2324 - "parameters": [ 2325 - { 2326 - "name": "id", 2327 - "in": "path", 2328 - "description": "The ID of the quote", 2329 - "required": true, 2330 - "schema": { 2331 - "type": "integer" 2332 - } 2333 - } 2334 - ], 2335 - "responses": { 2336 - "200": { 2337 - "description": "Quote deleted" 2338 - }, 2339 - "403": { 2340 - "description": "Forbidden" 2341 - }, 2342 - "404": { 2343 - "description": "Not found" 2344 - } 2345 - } 2346 - } 2347 - }, 2348 - "/quote/{id}.json": { 2349 - "get": { 2350 - "summary": "Get Quote Metadata as JSON (Deprecated)", 2351 - "description": "DEPRECATED: Use GET /api/v1/quotes/{id} instead.", 2352 - "deprecated": true, 2353 - "tags": ["Deprecated"], 2354 - "parameters": [ 2355 - { 2356 - "name": "id", 2357 - "in": "path", 2358 - "description": "The ID of the quote", 2359 - "required": true, 2360 - "schema": { 2361 - "type": "integer" 2362 - } 2363 - } 2364 - ], 2365 - "responses": { 2366 - "200": { 2367 - "description": "Quote metadata" 2368 - }, 2369 - "404": { 2370 - "description": "Quote not found" 2371 - } 2372 - } 2373 - } 2374 - }, 2375 - "/search": { 2376 - "get": { 2377 - "summary": "Search Content (Deprecated)", 2378 - "description": "DEPRECATED: Use GET /api/v1/search instead.", 2379 - "deprecated": true, 2380 - "tags": ["Deprecated"], 2381 - "parameters": [ 2382 - { 2383 - "name": "search", 2384 - "in": "query", 2385 - "required": true, 2386 - "schema": { 2387 - "type": "string", 2388 - "minLength": 4 2389 - } 2390 - } 2391 - ], 2392 - "responses": { 2393 - "200": { 2394 - "description": "Search Results HTML" 2395 - } 2396 - } 2397 - } 2398 - }, 2399 - "/stats": { 2400 - "get": { 2401 - "summary": "Get User Statistics (Deprecated)", 2402 - "description": "DEPRECATED: Use GET /api/v1/stats instead.", 2403 - "deprecated": true, 2404 - "tags": ["Deprecated"], 2405 - "responses": { 2406 - "200": { 2407 - "description": "Statistics HTML Page" 2408 - } 2409 - } 2410 - } 2411 - }, 2412 - "/stats.json": { 2413 - "get": { 2414 - "summary": "Get User Statistics as JSON (Deprecated)", 2415 - "description": "DEPRECATED: Use GET /api/v1/stats instead.", 2416 - "deprecated": true, 2417 - "tags": ["Deprecated"], 2418 - "parameters": [ 2419 - { 2420 - "name": "limit", 2421 - "in": "query", 2422 - "schema": { 2423 - "type": "integer", 2424 - "default": 50 2425 - } 2426 - }, 2427 - { 2428 - "name": "offset", 2429 - "in": "query", 2430 - "schema": { 2431 - "type": "integer", 2432 - "default": 0 2433 - } 2434 - } 2435 - ], 2436 - "responses": { 2437 - "200": { 2438 - "description": "Array of user statistics" 2439 - } 2440 - } 2441 - } 2442 - }, 2443 - "/ogpreview": { 2444 - "get": { 2445 - "summary": "Get OpenGraph Preview (Deprecated)", 2446 - "description": "DEPRECATED: This endpoint may be removed in a future release.", 2447 - "deprecated": true, 2448 - "tags": ["Deprecated"], 2449 - "parameters": [ 2450 - { 2451 - "name": "url", 2452 - "in": "query", 2453 - "required": true, 2454 - "schema": { 2455 - "type": "string" 2456 - } 2457 - } 2458 - ], 2459 - "responses": { 2460 - "200": { 2461 - "description": "OpenGraph Metadata" 2094 + "description": "Could not fetch preview for the URL" 2462 2095 } 2463 2096 } 2464 2097 } ··· 2498 2131 "description": "Daily kitten management" 2499 2132 }, 2500 2133 { 2501 - "name": "Deprecated", 2502 - "description": "Legacy endpoints - will be removed in a future release" 2134 + "name": "Preview", 2135 + "description": "OpenGraph link preview fetching" 2503 2136 } 2504 2137 ] 2505 2138 }
+1 -1
internal/data/gorm_store.go
··· 522 522 } 523 523 err := s.db.WithContext(ctx).Raw(` 524 524 SELECT DISTINCT lp.url FROM link_previews lp 525 - WHERE CAST(lp.data AS `+castType+`) LIKE '%"error":%' 525 + WHERE CAST(lp.data AS ` + castType + `) LIKE '%"error":%' 526 526 AND lp.url NOT IN (SELECT al.url FROM archive_lookups al) 527 527 `).Scan(&urls).Error 528 528 return urls, err
-15
internal/handler/handlers.go
··· 530 530 return base 531 531 } 532 532 533 - func (h *Handler) ButtonHandler(w http.ResponseWriter, r *http.Request) { 534 - user := r.FormValue("user") 535 - data := map[string]interface{}{ 536 - "User": user, 537 - "BaseURL": h.Config.BaseURL, 538 - "Hot": h.getHotHTML(r.Context()), 539 - "GitCommit": version.CommitHash, 540 - "GitCommitURL": fmt.Sprintf("https://github.com/websages/tumble/commit/%s", version.CommitHash), 541 - } 542 - 543 - if err := h.Renderer.Render(w, "tumble_buttons.html", data); err != nil { 544 - slog.Error("Error rendering buttons", "error", err) 545 - } 546 - } 547 - 548 533 func (h *Handler) Search(w http.ResponseWriter, r *http.Request) { 549 534 ctx := r.Context() 550 535 query := sanitizeSearch(r.URL.Query().Get("search"))
-17
internal/templates/renderer.go
··· 32 32 } 33 33 return fmt.Sprintf("%s/link/%d", baseURL, id) 34 34 }, 35 - // irclinkURL builds a click-tracking URL for IRC links (legacy, use linkURL instead) 36 - // If a signature is provided, it's appended for verified click tracking 37 - "irclinkURL": func(baseURL string, id int, sig string) string { 38 - if sig != "" { 39 - return fmt.Sprintf("%s/irclink/?%d&sig=%s", baseURL, id, sig) 40 - } 41 - return fmt.Sprintf("%s/irclink/?%d", baseURL, id) 42 - }, 43 35 // truncate shortens a string to max characters with ellipsis 44 36 "truncate": func(s string, max int) string { 45 37 if len(s) > max { ··· 67 59 return fmt.Sprintf("%s/link/%d?sig=%s", baseURL, id, sig) 68 60 } 69 61 return fmt.Sprintf("%s/link/%d", baseURL, id) 70 - }, 71 - // irclinkURL builds a click-tracking URL for IRC links (legacy, use linkURL instead) 72 - // If a signature is provided, it's appended for verified click tracking 73 - // Note: Uses &amp; for XML-safe output since text/template doesn't auto-escape 74 - "irclinkURL": func(baseURL string, id int, sig string) string { 75 - if sig != "" { 76 - return fmt.Sprintf("%s/irclink/?%d&amp;sig=%s", baseURL, id, sig) 77 - } 78 - return fmt.Sprintf("%s/irclink/?%d", baseURL, id) 79 62 }, 80 63 "truncate": func(s string, max int) string { 81 64 if len(s) > max {
-2
internal/templates/views/header.html
··· 222 222 <div class="nav-header"> 223 223 <div class="nav-left"> 224 224 <a href="/" onclick="event.preventDefault(); window.location = window.location.origin + '/';">home</a> 225 - <a href="/buttons/">buttons</a> 226 225 <a href="/index.xml">feed</a> 227 226 <a href="/stats">stats</a> 228 227 <a href="/api/docs">api docs</a> ··· 278 277 </form> 279 278 <nav class="mobile-drawer-nav"> 280 279 <a href="/" onclick="event.preventDefault(); window.location = window.location.origin + '/';">home</a> 281 - <a href="/buttons/">buttons</a> 282 280 <a href="/index.xml">feed</a> 283 281 <a href="/stats">stats</a> 284 282 <a href="/api/docs">api docs</a>
+2 -3
internal/templates/views/index.html
··· 168 168 } 169 169 170 170 // Create preview endpoint URL 171 - var previewUrl = "/ogpreview?url=" + encodeURIComponent(url); 171 + var previewUrl = "/api/v1/preview?url=" + encodeURIComponent(url); 172 172 173 173 // Load preview asynchronously 174 174 var xhr = new XMLHttpRequest(); ··· 557 557 </div> 558 558 <div class="item"> 559 559 <div class="header">also</div> 560 - <div class="sm"><div class="link"><a href="/buttons/">buttons</a></div></div> 561 560 <div class="sm"><div class="link"><a href="/index.xml">feed</a></div></div> 562 561 </div> 563 562 </div> ··· 574 573 {{if .IsFallbackContent}} 575 574 <div class="item" style="border: 1px solid var(--og-border); padding: 15px; margin-bottom: 20px; background-color: var(--footer-bg); border-radius: 4px; text-align: center;"> 576 575 <div style="font-weight: bold; margin-bottom: 5px; font-size: 18px;">It's been a bit quiet lately...</div> 577 - <div style="font-size: 14px;">Here is some content from the archives. Why not <a href="/buttons/" style="font-weight: bold; color: var(--link-color);">share something new</a>?</div> 576 + <div style="font-size: 14px;">Here is some content from the archives.</div> 578 577 </div> 579 578 {{end}} 580 579 {{if or .NavP .NavN}}
-106
internal/templates/views/tumble_buttons.html
··· 1 - <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> 2 - <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> 3 - 4 - <head> 5 - <title>tumblefish buttons</title> 6 - <script type="text/javascript"> 7 - var _gaq = _gaq || []; 8 - _gaq.push(['_setAccount', 'UA-24593498-1']); 9 - _gaq.push(['_trackPageview']); 10 - (function() { 11 - var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; 12 - ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; 13 - var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); 14 - })(); 15 - </script> 16 - <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" /> 17 - <link rel="stylesheet" href="{{.BaseURL}}/css/screen.css" type="text/css" media="screen" /> 18 - <!-- Theme Init --> 19 - <script> 20 - (function () { 21 - var savedTheme = localStorage.getItem("theme"); 22 - var prefersDark = window.matchMedia( 23 - "(prefers-color-scheme: dark)" 24 - ).matches; 25 - if (savedTheme === "dark" || (!savedTheme && prefersDark)) { 26 - document.documentElement.setAttribute("data-theme", "dark"); 27 - } 28 - })(); 29 - </script> 30 - </head> 31 - 32 - <style> 33 - /* Prevent grid rows from stretching to fill the page height */ 34 - #page { align-content: start; } 35 - </style> 36 - 37 - <body> 38 - {{template "header" .}} 39 - <div id="page"> 40 - <div id="masthead"> 41 - <a href="/" onclick="event.preventDefault(); window.location = window.location.origin + '/';">tumblefish.</a> 42 - </div> 43 - {{if .User}} 44 - <div id="content"> 45 - <div class="tumble_date"> 46 - <div class="tumble_date_date">!!</div> 47 - <div class="tumble_date_mon">buttons</div> 48 - <div class="tumble_date_day">yay!</div> 49 - </div> 50 - <div class="tumble_item_quote"> 51 - <div class="tumble_item_top"></div> 52 - <span class="tumble_item_quote_quote">So how do I install this crap??</span> 53 - <div class="tumble_item_bottom"></div> 54 - </div> 55 - <div class="tumble_item_ircLink"> 56 - <div class="tumble_item_top"></div> 57 - <span class="tumble_item_ircLink_title"> 58 - Drag this link: <a href="javascript:location.href='{{.BaseURL}}/link/?user={{.User}}&source=web&url='+encodeURIComponent(location.href)" onclick="window.alert('No clicky! Drag this link to your Bookmarks toolbar or menu, or right-click it and choose Bookmark This Link...');return false;">post to tumblefish!</a> up to your Bookmarks toolbar or menu. 59 - </span> 60 - <div class="tumble_item_bottom"></div> 61 - </div> 62 - <div class="tumble_item_ircLink"> 63 - <div class="tumble_item_top"></div> 64 - <span class="tumble_item_ircLink_title">PS - Unfortunately, tumblebuttons don't work with Microsoft Internet Explorer. MSIE sucks. Stop using it.</span> 65 - <div class="tumble_item_bottom"></div> 66 - </div> 67 - <div class="tumble_item_ircLink"> 68 - <div class="tumble_item_top"></div> 69 - <span class="tumble_item_ircLink_title">PPS - If your name is Greg Buchanan and you just read the above postscript, you can suck my ass.</span> 70 - <div class="tumble_item_bottom"></div> 71 - </div> 72 - </div> 73 - {{else}} 74 - <div id="content"> 75 - <div class="tumble_date"> 76 - <div class="tumble_date_date">!!</div> 77 - <div class="tumble_date_mon">buttons</div> 78 - <div class="tumble_date_day">yay!</div> 79 - </div> 80 - <div class="tumble_item_quote"> 81 - <div class="tumble_item_top"></div> 82 - <span class="tumble_item_quote_quote">WTF is a tumblebutton?!?</span> 83 - <div class="tumble_item_bottom"></div> 84 - </div> 85 - <div class="tumble_item_ircLink"> 86 - <div class="tumble_item_top"></div> 87 - <span class="tumble_item_ircLink_title">Buttons (bookmarklets) are links you add to your browser's Bookmarks toolbar or menu. They are an easy way to post your awesome links to tumblefish.</span> 88 - <div class="tumble_item_bottom"></div> 89 - </div> 90 - <div class="tumble_item_ircLink"> 91 - <div class="tumble_item_top"></div> 92 - <span class="tumble_item_ircLink_title">To get started, type your name in the box below.</span> 93 - <div class="tumble_item_bottom"></div> 94 - </div> 95 - <div class="tumble_item_ircLink"> 96 - <div class="tumble_item_top"></div> 97 - <span class="tumble_item_ircLink_title"><form method="post" action="button.cgi" name=""><input name="user" /><input type="submit" name="" value=" GO!! " /></form></span> 98 - <div class="tumble_item_bottom"></div> 99 - </div> 100 - </div> 101 - {{end}} 102 - </div> 103 - {{template "footer" .}} 104 - </body> 105 - 106 - </html>
+4 -4
tests/add_link.sh
··· 1 1 #!/bin/bash 2 - # Script to add an IRC Link 2 + # Script to add a Link via v1 API 3 3 # Usage: ./add_link.sh <user> <url> 4 4 5 5 USER=$1 ··· 11 11 exit 1 12 12 fi 13 13 14 - 15 - curl -s "$BASE_URL/irclink/?user=$USER&url=$URL&source=irc" >/dev/null 16 - 14 + curl -s -X POST "$BASE_URL/api/v1/links" \ 15 + -H "Content-Type: application/json" \ 16 + -d "{\"user\":\"$USER\",\"url\":\"$URL\"}"
+16 -31
tests/api_test.sh
··· 1 1 #!/bin/bash 2 - # API Integration Tests for Tumble Go rewrite 2 + # API Integration Tests for Tumble 3 3 4 4 BASE_URL="http://localhost:8080" 5 5 FAIL=0 ··· 59 59 fi 60 60 61 61 # 3. Search (HTML) 62 - check_200 "/search?search=test" # New 63 - check_200 "/search.cgi?search=test" # Legacy 62 + check_200 "/search?search=test" 64 63 65 - # 4. IRCLink Redirect (Setup needed for real test, checking 404/400 for bad ID) 66 - echo -n "Checking /irclink/?id=999999 (Expect 404/Redirect)... " 67 - status=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/irclink/?id=999999") 68 - if [ "$status" == "404" ] || [ "$status" == "302" ]; then 69 - echo "OK (Status: $status)" 70 - else 71 - echo "FAIL (Status: $status)" 72 - FAIL=1 73 - fi 74 - 75 - # 5. v0 Endpoints 76 - check_200 "/v0/" 77 - check_200 "/v0/search.cgi?search=test" 78 - 79 - # 6. OGPreview Routes 80 - check_200 "/ogpreview.cgi?url=https://example.com" # Legacy 81 - # YouTube 404 Check (New Route) 82 - echo -n "Checking YouTube value for missing video (Expect 200 OK + status: 404) on /ogpreview... " 83 - resp=$(curl -s "$BASE_URL/ogpreview?url=https://www.youtube.com/watch?v=video_gone") 84 - # Check if response contains '"status": 404' (or 'status":404' depending on spacing) 64 + # 4. API v1 Preview 65 + echo -n "Checking YouTube value for missing video (Expect 200 OK + status: 404) on /api/v1/preview... " 66 + resp=$(curl -s "$BASE_URL/api/v1/preview?url=https://www.youtube.com/watch?v=video_gone") 85 67 if [[ "$resp" == *'"status":404'* ]] || [[ "$resp" == *'"status": 404'* ]]; then 86 68 echo "OK" 87 69 else ··· 89 71 FAIL=1 90 72 fi 91 73 92 - # 7. Delete Link Test (Create -> Delete -> Verify) 93 - echo -n "Testing DELETE /irclink/ flow... " 94 - # Create a link first 95 - CREATE_OUT=$(curl -s "$BASE_URL/irclink/?user=testdel&url=http://delete-test.com&source=irc") 74 + # 5. API v1 Link Create -> Delete -> Verify 75 + echo -n "Testing API v1 Link Create/Delete flow... " 76 + # Create a link 77 + CREATE_OUT=$(curl -s -X POST "$BASE_URL/api/v1/links" \ 78 + -H "Content-Type: application/json" \ 79 + -H "Accept: text/plain" \ 80 + -d '{"user":"testdel","url":"http://delete-test.com"}') 96 81 # Check if we got an ID (numeric) 97 82 if [[ "$CREATE_OUT" =~ ^[0-9]+$ ]]; then 98 83 DEL_ID=$CREATE_OUT 99 - # Delete it (using admin secret from test config) 100 - DEL_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE -H "X-Admin-Secret: test-admin-secret" "$BASE_URL/irclink/?id=$DEL_ID") 101 - if [ "$DEL_STATUS" == "200" ]; then 84 + # Delete it 85 + DEL_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE -H "X-API-Key: test-admin-secret" "$BASE_URL/api/v1/links/$DEL_ID") 86 + if [ "$DEL_STATUS" == "204" ]; then 102 87 # Verify it's gone 103 - GONE_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/irclink/?id=$DEL_ID") 88 + GONE_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/api/v1/links/$DEL_ID") 104 89 if [ "$GONE_STATUS" == "404" ]; then 105 90 echo "OK" 106 91 else
+3 -3
tests/delete_link.sh
··· 1 1 #!/bin/bash 2 - # Script to delete an IRC Link 2 + # Script to delete a Link via v1 API 3 3 # Usage: ./delete_link.sh <id> [admin_secret] 4 4 # If admin_secret is not provided, uses TUMBLE_ADMIN_SECRET env var 5 5 ··· 15 15 16 16 if [ -z "$SECRET" ]; then 17 17 echo "Warning: No admin secret provided. Request may fail." 18 - curl -v -X DELETE "$BASE_URL/irclink/?id=$ID" 18 + curl -v -X DELETE "$BASE_URL/api/v1/links/$ID" 19 19 else 20 - curl -v -X DELETE -H "X-Admin-Secret: $SECRET" "$BASE_URL/irclink/?id=$ID" 20 + curl -v -X DELETE -H "X-API-Key: $SECRET" "$BASE_URL/api/v1/links/$ID" 21 21 fi 22 22 echo ""