2024-11-24 21:36:03 -06:00
package main
import (
2024-11-24 22:05:22 -06:00
"database/sql"
2024-11-24 21:36:03 -06:00
"fmt"
2024-11-26 15:04:05 -06:00
"html/template"
2024-11-24 21:36:03 -06:00
"log"
"net/http"
"os"
2024-11-27 22:29:03 -06:00
"strings"
2024-11-26 22:20:10 -06:00
"time"
2024-11-24 22:05:22 -06:00
_ "github.com/lib/pq"
2024-11-26 22:20:10 -06:00
"golang.org/x/exp/rand"
2024-11-24 21:36:03 -06:00
)
2024-11-26 15:04:05 -06:00
type ShortLinks struct {
tmpl * template . Template
mux * http . ServeMux
2024-11-26 21:46:19 -06:00
db * sql . DB
2024-11-26 22:20:10 -06:00
r * rand . Rand
2024-11-27 22:29:03 -06:00
oai * oaiClient
2024-11-26 15:04:05 -06:00
}
2024-11-27 22:29:03 -06:00
type setResponse struct {
2024-11-26 21:38:49 -06:00
Short string ` json:"short" `
}
2024-11-27 22:29:03 -06:00
type suggestResponse struct {
Shorts [ ] string ` json:"shorts" `
}
2024-11-26 21:46:19 -06:00
func NewShortLinks ( db * sql . DB ) ( * ShortLinks , error ) {
2024-11-26 15:04:05 -06:00
tmpl := template . New ( "index.html" )
tmpl , err := tmpl . ParseFiles ( "static/index.html" )
if err != nil {
return nil , fmt . Errorf ( "static/index.html: %w" , err )
}
2024-11-27 22:29:03 -06:00
oai , err := newOAIClientFromEnv ( )
if err != nil {
return nil , fmt . Errorf ( "newOAIClientFromEnv: %w" , err )
}
2024-11-26 15:04:05 -06:00
sl := & ShortLinks {
tmpl : tmpl ,
mux : http . NewServeMux ( ) ,
2024-11-26 21:46:19 -06:00
db : db ,
2024-11-26 22:20:10 -06:00
r : rand . New ( rand . NewSource ( uint64 ( time . Now ( ) . UnixNano ( ) ) ) ) ,
2024-11-27 22:29:03 -06:00
oai : oai ,
2024-11-26 15:04:05 -06:00
}
2024-11-26 21:38:49 -06:00
sl . mux . HandleFunc ( "GET /{$}" , sl . serveRoot )
sl . mux . HandleFunc ( "GET /{short}" , sl . serveShort )
sl . mux . HandleFunc ( "POST /{$}" , sl . serveSet )
2024-11-27 22:29:03 -06:00
sl . mux . HandleFunc ( "QUERY /{$}" , sl . serveSuggest )
2024-11-26 15:04:05 -06:00
return sl , nil
}
func ( sl * ShortLinks ) ServeHTTP ( w http . ResponseWriter , r * http . Request ) {
sl . mux . ServeHTTP ( w , r )
}
func ( sl * ShortLinks ) serveRoot ( w http . ResponseWriter , r * http . Request ) {
log . Printf ( "%s %s" , r . RemoteAddr , r . URL . Path )
2024-11-26 22:07:05 -06:00
sl . serveRootWithPath ( w , r , "" )
}
2024-11-26 15:04:05 -06:00
2024-11-26 22:07:05 -06:00
func ( sl * ShortLinks ) serveRootWithPath ( w http . ResponseWriter , r * http . Request , path string ) {
err := sl . tmpl . Execute ( w , map [ string ] any {
"path" : path ,
} )
2024-11-26 15:04:05 -06:00
if err != nil {
sendError ( w , http . StatusInternalServerError , "error executing template: %s" , err )
return
}
}
2024-11-26 21:38:49 -06:00
func ( sl * ShortLinks ) serveShort ( w http . ResponseWriter , r * http . Request ) {
2024-11-26 22:07:05 -06:00
log . Printf ( "%s %s" , r . RemoteAddr , r . URL . Path )
short := r . PathValue ( "short" )
row := sl . db . QueryRow ( ` SELECT long FROM links WHERE short = $1 ` , short )
var long string
err := row . Scan ( & long )
if err != nil {
sl . serveRootWithPath ( w , r , short )
return
}
http . Redirect ( w , r , long , http . StatusTemporaryRedirect )
2024-11-26 21:38:49 -06:00
}
func ( sl * ShortLinks ) serveSet ( w http . ResponseWriter , r * http . Request ) {
err := r . ParseForm ( )
if err != nil {
sendError ( w , http . StatusBadRequest , "Parse form: %s" , err )
return
}
log . Printf ( "%s %s %s" , r . RemoteAddr , r . URL . Path , r . Form . Encode ( ) )
short := r . Form . Get ( "short" )
2024-11-26 22:20:10 -06:00
if short == "" {
short , err = sl . genShort ( )
if err != nil {
sendError ( w , http . StatusInternalServerError , "genShort: %s" , err )
return
}
}
2024-11-26 21:38:49 -06:00
long := r . Form . Get ( "long" )
if long == "" {
sendError ( w , http . StatusBadRequest , "long= param required" )
return
}
2024-12-01 13:05:16 -08:00
_ , err = sl . db . Exec ( ` SELECT update_link($1, $2); ` , short , long )
2024-11-26 21:46:19 -06:00
if err != nil {
2024-12-01 13:05:16 -08:00
sendError ( w , http . StatusInternalServerError , "update_link: %s" , err )
2024-11-26 21:46:19 -06:00
return
}
2024-11-26 21:38:49 -06:00
2024-11-27 22:29:03 -06:00
sendJSON ( w , setResponse {
2024-11-26 21:38:49 -06:00
Short : short ,
} )
}
2024-11-27 22:29:03 -06:00
func ( sl * ShortLinks ) serveSuggest ( w http . ResponseWriter , r * http . Request ) {
err := r . ParseForm ( )
if err != nil {
sendError ( w , http . StatusBadRequest , "Parse form: %s" , err )
return
}
log . Printf ( "%s %s %s" , r . RemoteAddr , r . URL . Path , r . Form . Encode ( ) )
if ! r . Form . Has ( "short" ) {
sendError ( w , http . StatusBadRequest , "short= param required" )
return
}
user := strings . Join ( r . Form [ "short" ] , "\n" )
comp , err := sl . oai . completeChat (
2024-11-28 07:57:10 -06:00
"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." ,
2024-11-27 22:29:03 -06:00
user ,
)
if err != nil {
sendError ( w , http . StatusInternalServerError , "oai.completeChat: %s" , err )
return
}
shorts := [ ] string { }
for _ , short := range strings . Split ( comp , "\n" ) {
if short != "" {
shorts = append ( shorts , strings . TrimSpace ( short ) )
}
}
sendJSON ( w , suggestResponse {
Shorts : shorts ,
} )
}
2024-11-26 22:20:10 -06:00
func ( sl * ShortLinks ) genShort ( ) ( string , error ) {
for chars := 3 ; chars <= 10 ; chars ++ {
b := make ( [ ] byte , chars )
for i := range b {
b [ i ] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" [ sl . r . Intn ( 62 ) ]
}
short := string ( b )
exists := false
err := sl . db . QueryRow ( "SELECT EXISTS(SELECT 1 FROM links WHERE short = $1)" , short ) . Scan ( & exists )
if err != nil {
return "" , fmt . Errorf ( "check exists: %w" , err )
}
if ! exists {
return short , nil
}
}
return "" , fmt . Errorf ( "no available short link found" )
}
2024-11-24 21:36:03 -06:00
func main ( ) {
port := os . Getenv ( "PORT" )
if port == "" {
2024-11-24 22:05:22 -06:00
log . Fatalf ( "please set PORT" )
}
pgConn := os . Getenv ( "PGCONN" )
if pgConn == "" {
log . Fatalf ( "please set PGCONN" )
}
2024-11-25 10:21:25 -06:00
db , err := sql . Open ( "postgres" , pgConn )
2024-11-24 22:05:22 -06:00
if err != nil {
log . Fatal ( err )
2024-11-24 21:36:03 -06:00
}
2024-12-01 13:05:16 -08:00
stmts := [ ] string {
`
CREATE TABLE IF NOT EXISTS links (
short VARCHAR ( 100 ) PRIMARY KEY ,
long TEXT NOT NULL
) ;
` ,
`
CREATE TABLE IF NOT EXISTS links_history (
short VARCHAR ( 100 ) ,
long TEXT NOT NULL ,
until TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
) ;
` ,
`
CREATE OR REPLACE FUNCTION update_link (
_short VARCHAR ( 100 ) ,
_long TEXT
) RETURNS void AS $ $
DECLARE
old RECORD ;
BEGIN
SELECT * INTO old FROM links WHERE short = _short ;
IF old IS NOT NULL THEN
INSERT INTO links_history ( short , long )
VALUES ( old . short , old . long ) ;
UPDATE links
SET long = _long
WHERE short = _short ;
ELSE
INSERT INTO links ( short , long )
VALUES ( _short , _long ) ;
END IF ;
END ;
$ $ LANGUAGE plpgsql ;
` ,
}
for _ , stmt := range stmts {
_ , err := db . Exec ( stmt )
if err != nil {
log . Fatalf ( "Failed to create tables & functions: %v" , err )
}
2024-11-25 10:21:25 -06:00
}
2024-11-26 21:46:19 -06:00
sl , err := NewShortLinks ( db )
2024-11-26 15:04:05 -06:00
if err != nil {
log . Fatalf ( "Failed to create shortlinks: %v" , err )
}
http . Handle ( "/" , sl )
2024-11-24 21:36:03 -06:00
bind := fmt . Sprintf ( ":%s" , port )
log . Printf ( "listening on %s" , bind )
if err := http . ListenAndServe ( bind , nil ) ; err != nil {
log . Fatalf ( "listen: %s" , err )
}
}