2024-11-24 21:36:03 -06:00
package main
import (
2024-11-24 22:05:22 -06:00
"database/sql"
2024-12-03 13:54:46 -08:00
"encoding/json"
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
2024-12-03 15:01:42 -08:00
help * template . Template
2024-11-26 15:04:05 -06:00
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-12-02 21:25:40 -08:00
domainAliases map [ string ] string
writableDomains map [ string ] bool
2024-11-26 15:04:05 -06:00
}
2024-11-27 22:29:03 -06:00
type setResponse struct {
2024-12-03 13:54:46 -08:00
Short string ` json:"short" `
Domain string ` json:"domain" `
2024-12-03 20:33:57 -08:00
URL string ` json:"url" `
2024-11-26 21:38:49 -06:00
}
2024-11-27 22:29:03 -06:00
type suggestResponse struct {
Shorts [ ] string ` json:"shorts" `
2024-12-03 13:54:46 -08:00
Domain string ` json:"domain" `
2024-11-27 22:29:03 -06:00
}
2024-12-02 21:25:40 -08:00
func NewShortLinks ( db * sql . DB , domainAliases map [ string ] string , writableDomains map [ string ] bool ) ( * 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-12-03 15:01:42 -08:00
help , err := template . New ( "help.html" ) . ParseFiles ( "static/help.html" )
if err != nil {
return nil , fmt . Errorf ( "static/help.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 ,
2024-12-03 15:01:42 -08:00
help : help ,
2024-11-26 15:04:05 -06:00
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-12-02 21:25:40 -08:00
domainAliases : domainAliases ,
writableDomains : writableDomains ,
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 ) {
2024-12-03 13:54:46 -08:00
err := sl . parseForm ( r )
if err != nil {
sendError ( w , http . StatusBadRequest , "parse form: %s" , err )
return
}
2024-12-02 21:25:40 -08:00
2024-12-03 13:15:36 -08:00
if sl . isWritable ( r . Host ) {
sl . serveRootWithPath ( w , r , "" )
return
}
2024-12-02 22:54:22 -08:00
parts := strings . SplitN ( r . Host , "." , 2 )
if len ( parts ) != 2 {
sl . serveRootWithPath ( w , r , "" )
return
}
long , err := sl . getLong ( parts [ 0 ] , sl . getDomain ( parts [ 1 ] ) )
if err != nil {
sl . serveRootWithPath ( w , r , "" )
return
}
http . Redirect ( w , r , long , http . StatusTemporaryRedirect )
2024-11-26 22:07:05 -06:00
}
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 ) {
2024-12-03 13:54:46 -08:00
err := sl . parseForm ( r )
2024-12-02 22:29:01 -08:00
if err != nil {
2024-12-03 13:54:46 -08:00
sendError ( w , http . StatusBadRequest , "parse form: %s" , err )
2024-12-02 22:29:01 -08:00
return
}
2024-12-02 22:54:22 -08:00
2024-12-02 21:25:40 -08:00
if ! sl . isWritable ( r . Host ) {
2024-12-02 23:03:48 -08:00
sendError ( w , http . StatusNotFound , "not found" )
2024-12-02 21:25:40 -08:00
return
}
2024-12-02 22:29:01 -08:00
err = sl . tmpl . Execute ( w , map [ string ] any {
2024-11-26 22:07:05 -06:00
"path" : path ,
2024-12-02 21:25:40 -08:00
"host" : sl . getDomain ( r . Host ) ,
2024-12-02 22:29:01 -08:00
"long" : r . Form . Get ( "long" ) ,
2024-11-26 22:07:05 -06:00
} )
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-12-03 13:54:46 -08:00
log . Printf ( "%s %s %s %s %s %s" , r . RemoteAddr , r . Method , r . Host , sl . getDomain ( r . Host ) , r . URL , r . Form )
2024-11-26 22:07:05 -06:00
short := r . PathValue ( "short" )
2024-12-03 15:01:42 -08:00
if sl . isWritable ( r . Host ) && short == "_help" {
sl . serveHelp ( w , r )
return
}
2024-12-02 22:54:22 -08:00
long , err := sl . getLong ( short , sl . getDomain ( r . Host ) )
2024-11-26 22:07:05 -06:00
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 ) {
2024-12-03 13:54:46 -08:00
err := sl . parseForm ( r )
2024-11-26 21:38:49 -06:00
if err != nil {
2024-12-03 13:54:46 -08:00
sendError ( w , http . StatusBadRequest , "parse form: %s" , err )
2024-11-26 21:38:49 -06:00
return
}
2024-12-02 21:25:40 -08:00
if ! sl . isWritable ( r . Host ) {
2024-12-02 23:03:48 -08:00
sendError ( w , http . StatusNotFound , "not found" )
2024-12-02 21:25:40 -08:00
return
}
2024-11-26 21:38:49 -06:00
short := r . Form . Get ( "short" )
2024-12-02 21:13:34 -08:00
generated := false
2024-11-26 21:38:49 -06:00
2024-11-26 22:20:10 -06:00
if short == "" {
2024-12-02 21:25:40 -08:00
short , err = sl . genShort ( sl . getDomain ( r . Host ) )
2024-11-26 22:20:10 -06:00
if err != nil {
sendError ( w , http . StatusInternalServerError , "genShort: %s" , err )
return
}
2024-12-02 21:13:34 -08:00
generated = true
2024-11-26 22:20:10 -06:00
}
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-02 21:25:40 -08:00
_ , err = sl . db . Exec ( ` SELECT update_link($1, $2, $3, $4); ` , short , long , sl . getDomain ( r . Host ) , generated )
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-12-03 13:54:46 -08:00
Short : short ,
Domain : sl . getDomain ( r . Host ) ,
2024-12-03 20:33:57 -08:00
URL : fmt . Sprintf ( "https://%s/%s" , sl . getDomain ( r . Host ) , short ) ,
2024-11-26 21:38:49 -06:00
} )
}
2024-11-27 22:29:03 -06:00
func ( sl * ShortLinks ) serveSuggest ( w http . ResponseWriter , r * http . Request ) {
2024-12-03 13:54:46 -08:00
err := sl . parseForm ( r )
2024-11-27 22:29:03 -06:00
if err != nil {
2024-12-03 13:54:46 -08:00
sendError ( w , http . StatusBadRequest , "parse form: %s" , err )
2024-11-27 22:29:03 -06:00
return
}
2024-12-02 21:25:40 -08:00
if ! sl . isWritable ( r . Host ) {
2024-12-02 23:03:48 -08:00
sendError ( w , http . StatusNotFound , "not found" )
2024-12-02 21:25:40 -08:00
return
}
2024-11-27 22:29:03 -06:00
2024-12-03 13:54:46 -08:00
if ! r . Form . Has ( "shorts" ) {
sendError ( w , http . StatusBadRequest , "shorts= param required" )
2024-11-27 22:29:03 -06:00
return
}
2024-12-03 13:54:46 -08:00
user := strings . Join ( r . Form [ "shorts" ] , "\n" )
2024-11-27 22:29:03 -06:00
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-12-03 13:54:46 -08:00
Domain : sl . getDomain ( r . Host ) ,
2024-11-27 22:29:03 -06:00
} )
}
2024-12-03 15:01:42 -08:00
func ( sl * ShortLinks ) serveHelp ( w http . ResponseWriter , r * http . Request ) {
if ! sl . isWritable ( r . Host ) {
sendError ( w , http . StatusNotFound , "not found" )
return
}
err := sl . help . Execute ( w , map [ string ] any {
"host" : r . Host ,
} )
if err != nil {
sendError ( w , http . StatusInternalServerError , "error executing template: %s" , err )
return
}
}
2024-12-02 21:13:34 -08:00
func ( sl * ShortLinks ) genShort ( domain string ) ( string , error ) {
2024-11-26 22:20:10 -06:00
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
2024-12-02 21:13:34 -08:00
err := sl . db . QueryRow ( "SELECT EXISTS(SELECT 1 FROM links WHERE short = $1 AND domain = $2)" , short , domain ) . Scan ( & exists )
2024-11-26 22:20:10 -06:00
if err != nil {
return "" , fmt . Errorf ( "check exists: %w" , err )
}
if ! exists {
return short , nil
}
}
return "" , fmt . Errorf ( "no available short link found" )
}
2024-12-02 21:25:40 -08:00
func ( sl * ShortLinks ) getDomain ( host string ) string {
if alias , ok := sl . domainAliases [ host ] ; ok {
return alias
}
return host
}
func ( sl * ShortLinks ) isWritable ( host string ) bool {
return sl . writableDomains [ host ]
}
2024-12-02 22:54:22 -08:00
func ( sl * ShortLinks ) getLong ( short , domain string ) ( string , error ) {
var long string
err := sl . db . QueryRow ( "SELECT long FROM links WHERE short = $1 AND domain = $2" , short , domain ) . Scan ( & long )
if err != nil {
return "" , err
}
return long , nil
}
2024-12-03 13:54:46 -08:00
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 )
}
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 (
2024-12-02 21:13:34 -08:00
short VARCHAR ( 100 ) NOT NULL ,
long TEXT NOT NULL ,
domain VARCHAR ( 255 ) NOT NULL ,
generated BOOLEAN NOT NULL ,
PRIMARY KEY ( short , domain )
2024-12-01 13:05:16 -08:00
) ;
` ,
`
CREATE TABLE IF NOT EXISTS links_history (
short VARCHAR ( 100 ) ,
long TEXT NOT NULL ,
2024-12-02 21:13:34 -08:00
domain VARCHAR ( 255 ) NOT NULL ,
generated BOOLEAN NOT NULL ,
2024-12-01 13:05:16 -08:00
until TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
) ;
` ,
`
CREATE OR REPLACE FUNCTION update_link (
_short VARCHAR ( 100 ) ,
2024-12-02 21:13:34 -08:00
_long TEXT ,
_domain VARCHAR ( 255 ) ,
_generated BOOLEAN
2024-12-01 13:05:16 -08:00
) RETURNS void AS $ $
DECLARE
old RECORD ;
BEGIN
2024-12-02 21:13:34 -08:00
SELECT * INTO old FROM links WHERE short = _short AND domain = _domain ;
2024-12-01 13:05:16 -08:00
IF old IS NOT NULL THEN
2024-12-02 21:13:34 -08:00
INSERT INTO links_history ( short , long , domain , generated )
VALUES ( old . short , old . long , old . domain , old . generated ) ;
2024-12-01 13:05:16 -08:00
UPDATE links
2024-12-02 21:13:34 -08:00
SET long = _long , generated = _generated
WHERE short = _short AND domain = _domain ;
2024-12-01 13:05:16 -08:00
ELSE
2024-12-02 21:13:34 -08:00
INSERT INTO links ( short , long , domain , generated )
VALUES ( _short , _long , _domain , _generated ) ;
2024-12-01 13:05:16 -08:00
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-12-02 21:25:40 -08:00
domainAliases , err := loadDomainAliases ( )
if err != nil {
log . Fatalf ( "Failed to load domain aliases: %v" , err )
}
writableDomains , err := loadWritableDomains ( )
if err != nil {
log . Fatalf ( "Failed to load writable domains: %v" , err )
}
sl , err := NewShortLinks ( db , domainAliases , writableDomains )
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 )
}
}
2024-12-02 21:25:40 -08:00
func loadDomainAliases ( ) ( map [ string ] string , error ) {
ret := map [ string ] string { }
s := os . Getenv ( "DOMAIN_ALIASES" )
if s == "" {
return ret , nil
}
for _ , pair := range strings . Split ( s , "," ) {
parts := strings . SplitN ( pair , "=" , 2 )
if len ( parts ) != 2 {
return nil , fmt . Errorf ( "invalid domain alias: %s" , pair )
}
ret [ parts [ 0 ] ] = parts [ 1 ]
}
return ret , nil
}
func loadWritableDomains ( ) ( map [ string ] bool , error ) {
ret := map [ string ] bool { }
s := os . Getenv ( "WRITABLE_DOMAINS" )
if s == "" {
return ret , nil
}
for _ , domain := range strings . Split ( s , "," ) {
ret [ domain ] = true
}
return ret , nil
}