diff --git a/main.go b/main.go index 4e4e320..f918c5b 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "database/sql" + "encoding/json" "fmt" "html/template" "log" @@ -26,11 +27,13 @@ type ShortLinks struct { } type setResponse struct { - Short string `json:"short"` + Short string `json:"short"` + Domain string `json:"domain"` } type suggestResponse struct { Shorts []string `json:"shorts"` + Domain string `json:"domain"` } func NewShortLinks(db *sql.DB, domainAliases map[string]string, writableDomains map[string]bool) (*ShortLinks, error) { @@ -70,7 +73,11 @@ func (sl *ShortLinks) ServeHTTP(w http.ResponseWriter, r *http.Request) { } func (sl *ShortLinks) serveRoot(w http.ResponseWriter, r *http.Request) { - log.Printf("%s %s %s %s %s", r.RemoteAddr, r.Method, r.Host, sl.getDomain(r.Host), r.URL) + err := sl.parseForm(r) + if err != nil { + sendError(w, http.StatusBadRequest, "parse form: %s", err) + return + } if sl.isWritable(r.Host) { sl.serveRootWithPath(w, r, "") @@ -93,14 +100,12 @@ func (sl *ShortLinks) serveRoot(w http.ResponseWriter, r *http.Request) { } func (sl *ShortLinks) serveRootWithPath(w http.ResponseWriter, r *http.Request, path string) { - err := r.ParseForm() + err := sl.parseForm(r) if err != nil { - sendError(w, http.StatusBadRequest, "Parse form: %s", err) + sendError(w, http.StatusBadRequest, "parse form: %s", err) return } - log.Printf("%s %s %s %s %s", r.RemoteAddr, r.Method, r.Host, sl.getDomain(r.Host), r.URL) - if !sl.isWritable(r.Host) { sendError(w, http.StatusNotFound, "not found") return @@ -118,7 +123,7 @@ func (sl *ShortLinks) serveRootWithPath(w http.ResponseWriter, r *http.Request, } func (sl *ShortLinks) serveShort(w http.ResponseWriter, r *http.Request) { - log.Printf("%s %s %s %s %s", r.RemoteAddr, r.Method, r.Host, sl.getDomain(r.Host), r.URL) + log.Printf("%s %s %s %s %s %s", r.RemoteAddr, r.Method, r.Host, sl.getDomain(r.Host), r.URL, r.Form) short := r.PathValue("short") @@ -132,14 +137,12 @@ func (sl *ShortLinks) serveShort(w http.ResponseWriter, r *http.Request) { } func (sl *ShortLinks) serveSet(w http.ResponseWriter, r *http.Request) { - err := r.ParseForm() + err := sl.parseForm(r) if err != nil { - sendError(w, http.StatusBadRequest, "Parse form: %s", err) + sendError(w, http.StatusBadRequest, "parse form: %s", err) return } - log.Printf("%s %s %s %s %s", r.RemoteAddr, r.Method, r.Host, sl.getDomain(r.Host), r.URL) - if !sl.isWritable(r.Host) { sendError(w, http.StatusNotFound, "not found") return @@ -171,30 +174,29 @@ func (sl *ShortLinks) serveSet(w http.ResponseWriter, r *http.Request) { } sendJSON(w, setResponse{ - Short: short, + Short: short, + Domain: sl.getDomain(r.Host), }) } func (sl *ShortLinks) serveSuggest(w http.ResponseWriter, r *http.Request) { - err := r.ParseForm() + err := sl.parseForm(r) if err != nil { - sendError(w, http.StatusBadRequest, "Parse form: %s", err) + sendError(w, http.StatusBadRequest, "parse form: %s", err) return } - log.Printf("%s %s %s %s %s", r.RemoteAddr, r.Method, r.Host, sl.getDomain(r.Host), r.URL) - if !sl.isWritable(r.Host) { sendError(w, http.StatusNotFound, "not found") return } - if !r.Form.Has("short") { - sendError(w, http.StatusBadRequest, "short= param required") + if !r.Form.Has("shorts") { + sendError(w, http.StatusBadRequest, "shorts= param required") return } - user := strings.Join(r.Form["short"], "\n") + user := strings.Join(r.Form["shorts"], "\n") comp, err := sl.oai.completeChat( "You are an assistant helping a user choose useful short names for a URL shortener. The request contains a list recents names chosen by the user, separated by newlines, with the most recent names first. Respond with only a list of possible suggestions for additional short names, separated by newlines. In descending order of preference, suggestions should include: plural/singular variations, 2 and 3 letter abbreivations, conceptual variations, other variations that are likely to be useful. Your bar for suggestions should be relatively high; responding with a shorter list of high quality suggestions is preferred.", @@ -214,6 +216,7 @@ func (sl *ShortLinks) serveSuggest(w http.ResponseWriter, r *http.Request) { sendJSON(w, suggestResponse{ Shorts: shorts, + Domain: sl.getDomain(r.Host), }) } @@ -263,6 +266,45 @@ func (sl *ShortLinks) getLong(short, domain string) (string, error) { return long, nil } +func (sl *ShortLinks) parseForm(r *http.Request) error { + defer r.Body.Close() + + err := r.ParseForm() + if err != nil { + return err + } + + if r.Header.Get("Content-Type") == "application/json" { + dec := json.NewDecoder(r.Body) + js := map[string]any{} + err := dec.Decode(&js) + if err != nil { + return err + } + + for k, v := range js { + switch v := v.(type) { + case []any: + for _, s := range v { + r.Form.Add(k, fmt.Sprintf("%v", s)) + } + + default: + log.Printf("unknown type: %T", v) + r.Form.Set(k, fmt.Sprintf("%v", v)) + } + } + } + + sl.logRequest(r) + + return nil +} + +func (sl *ShortLinks) logRequest(r *http.Request) { + log.Printf("%s %s %s %s %s %#v", r.RemoteAddr, r.Method, r.Host, sl.getDomain(r.Host), r.URL, r.Form) +} + func main() { port := os.Getenv("PORT") if port == "" { diff --git a/static/index.html b/static/index.html index e528b9d..9e2723e 100644 --- a/static/index.html +++ b/static/index.html @@ -110,21 +110,24 @@ async function setFromInputs() { async function set(short, long) { if (short != '') { - setShortItem(short, 'check-square-fill'); + setShortItem(short, null, 'check-square-fill'); } - const params = new URLSearchParams(); - params.set('short', short); - params.set('long', long); - const oldShort = document.getElementById('short').value; const oldLong = document.getElementById('long').value; let resp; try { - resp = await fetch(`./?${params.toString()}`, { + resp = await fetch('./', { method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + short: short, + long: long, + }), }); } catch (err) { console.log(err); @@ -139,8 +142,9 @@ async function set(short, long) { const data = await resp.json(); const newShort = data.short; + const newDomain = data.domain; - setShortItem(newShort, 'check-square'); + setShortItem(newShort, newDomain, 'check-square'); // Only set the icons if we were actually setting from these inputs if (document.getElementById('short').value == short && document.getElementById('long').value == long) { @@ -151,31 +155,42 @@ async function set(short, long) { // Only set the clipboard if the user didn't change the inputs if (document.getElementById('short').value == oldShort && document.getElementById('long').value == oldLong) { try { - await navigator.clipboard.writeText(`${window.location.protocol}//{{ .host }}/${newShort}`); + await navigator.clipboard.writeText(`${window.location.protocol}//${newDomain}/${newShort}`); } catch (err) { console.log(err); } } - const suggestParams = new URLSearchParams(); + const shorts = []; for (const elem of document.getElementById('tree').children) { const icon = elem.getElementsByTagName('sl-icon')[0]; if (icon.getAttribute('name') == 'check-square-fill' || icon.getAttribute('name') == 'check-square') { - suggestParams.append('short', elem.textContent); + shorts.push(elem.textContent); } } - fetch(`./?${suggestParams.toString()}`, { - method: 'QUERY', - }).then(async (resp) => { - for (const short of (await resp.json()).shorts) { - appendShortItem(short, long); - } - }).catch((err) => {}); + try { + resp = await fetch('./', { + method: 'QUERY', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + shorts: shorts, + }), + }); + } catch (err) { + console.log(err); + return; + } + + for (const short of (await resp.json()).shorts) { + appendShortItem(short, long); + } } -function setShortItem(short, icon) { +function setShortItem(short, domain, icon) { const tree = document.getElementById('tree'); for (const item of tree.children) { @@ -187,9 +202,12 @@ function setShortItem(short, icon) { const item = document.createElement('sl-tree-item'); item.appendChild(document.createElement('sl-icon')).setAttribute('name', icon); item.appendChild(document.createTextNode(short)); - item.addEventListener('click', () => { - navigator.clipboard.writeText(`${window.location.protocol}//{{ .host }}/${short}`); - }); + + if (domain != null) { + item.addEventListener('click', () => { + navigator.clipboard.writeText(`${window.location.protocol}//${domain}/${short}`); + }); + } tree.insertBefore(item, tree.firstChild); }