package main
import (
"fmt"
"time"
"net/http"
"io"
"os"
"context"
"log"
"io/ioutil"
"strings"
"encoding/json"
"math/rand"
gogpt "github.com/sashabaranov/go-gpt3"
"github.com/stripe/stripe-go/v74"
"github.com/stripe/stripe-go/v74/webhook"
"github.com/stripe/stripe-go/v74/checkout/session"
"gorm.io/gorm"
"gorm.io/driver/sqlite"
)
// BoughtType values. do not reorganize these or you fuck up the database
const (
DayPass = iota
)
const (
// A-Z and 0-9, four digits means this many codes
MaxCodes = 36 * 36 * 36 * 36
)
type userCode int
type User struct {
CreatedAt time . Time
UpdatedAt time . Time
DeletedAt gorm . DeletedAt ` gorm:"index" `
Code userCode ` gorm:"primaryKey" ` // of maximum value max codes, incremented one by one. These are converted to 4 digit alphanumeric code users can remember/use
BoughtTime int64 // unix time. Used to figure out if the pass is still valid
BoughtType int // enum
IsFulfilled bool // before users are checked out they are unfulfilled
CheckoutSessionID string
}
var c * gogpt . Client
var logResponses = false
var doCors = false
var checkoutRedirectTo string
var daypassPriceId string
var webhookSecret string
var db * gorm . DB
func intPow ( n , m int ) int {
if m == 0 {
return 1
}
result := n
for i := 2 ; i <= m ; i ++ {
result *= n
}
return result
}
var numberToChar = [ ... ] rune { 'A' , 'B' , 'C' , 'D' , 'E' , 'F' , 'G' , 'H' , 'I' , 'J' , 'K' , 'L' , 'M' , 'N' , 'O' , 'P' , 'Q' , 'R' , 'S' , 'T' , 'U' , 'V' , 'W' , 'X' , 'Y' , 'Z' , '0' , '1' , '2' , '3' , '4' , '5' , '6' , '7' , '8' , '9' }
func codeToString ( code userCode ) ( string , error ) {
toReturn := ""
value := int ( code )
// converting to base 36 (A-Z + numbers) then into characters, then appending
for digit := 3 ; digit >= 0 ; digit -- {
currentPlaceValue := value / intPow ( 36 , digit )
value -= currentPlaceValue * intPow ( 36 , digit )
if currentPlaceValue >= len ( numberToChar ) { return "" , fmt . Errorf ( "Failed to generate usercode %d to string, currentPlaceValue %d and length of number to char %d" , code , currentPlaceValue , len ( numberToChar ) ) }
toReturn += string ( numberToChar [ currentPlaceValue ] )
}
return toReturn , nil ;
}
func parseUserCode ( s string ) ( userCode , error ) {
asRune := [ ] rune ( s )
if len ( asRune ) != 4 { return 0 , fmt . Errorf ( "String to deconvert is not of length 4: %s" , s ) }
var toReturn userCode = 0
for digit := 3 ; digit >= 0 ; digit -- {
curDigitNum := 0
found := false
for i , letter := range numberToChar {
if letter == asRune [ digit ] {
curDigitNum = i
found = true
}
}
if ! found { return 0 , fmt . Errorf ( "Failed to find digit's number %s" , s ) }
toReturn += userCode ( curDigitNum * intPow ( 36 , digit ) )
}
return toReturn , nil
}
func isUserOld ( user User ) bool {
return ( currentTime ( ) - user . BoughtTime ) > 24 * 60 * 60
}
func clearOld ( db * gorm . DB ) {
var users [ ] User
result := db . Find ( & users )
if result . Error != nil {
log . Fatal ( result . Error )
}
var toDelete [ ] userCode // codes
for _ , user := range users {
if user . BoughtType != 0 {
panic ( "Don't know how to handle bought type " + string ( user . BoughtType ) + " yet" )
}
if isUserOld ( user ) {
toDelete = append ( toDelete , user . Code )
}
}
for _ , del := range toDelete {
db . Delete ( & User { } , del )
}
}
func webhookResponse ( w http . ResponseWriter , req * http . Request ) {
const MaxBodyBytes = int64 ( 65536 )
req . Body = http . MaxBytesReader ( w , req . Body , MaxBodyBytes )
body , err := ioutil . ReadAll ( req . Body )
if err != nil {
log . Printf ( "Error reading request body: %v\n" , err )
w . WriteHeader ( http . StatusServiceUnavailable )
return
}
endpointSecret := webhookSecret
event , err := webhook . ConstructEvent ( body , req . Header . Get ( "Stripe-Signature" ) , endpointSecret )
if err != nil {
log . Printf ( "Error verifying webhook signature %s\n" , err )
w . WriteHeader ( http . StatusBadRequest ) // Return a 400 error on a bad signature
return
}
if event . Type == "checkout.session.completed" {
var session stripe . CheckoutSession
err := json . Unmarshal ( event . Data . Raw , & session )
if err != nil {
log . Printf ( "Error parsing webhook JSON %s" , err )
w . WriteHeader ( http . StatusBadRequest )
return
}
params := & stripe . CheckoutSessionParams { }
params . AddExpand ( "line_items" )
// Retrieve the session. If you require line items in the response, you may include them by expanding line_items.
// Fulfill the purchase...
var toFulfill User
found := false
for trial := 0 ; trial < 5 ; trial ++ {
if db . Where ( "checkout_session_id = ?" , session . ID ) . First ( & toFulfill ) . Error != nil {
log . Println ( "Failed to fulfill user with ID " + session . ID )
} else {
found = true
break
}
}
if ! found {
log . Println ( "Error Failed to find user in database to fulfill: very bad! ID: " + session . ID )
} else {
userString , err := codeToString ( toFulfill . Code )
if err != nil {
log . Printf ( "Error strange thing, saved user's code was unable to be converted to a string %s" , err )
}
log . Printf ( "Fulfilling user with code %s\n" , userString )
toFulfill . IsFulfilled = true
db . Save ( & toFulfill )
}
}
w . WriteHeader ( http . StatusOK )
}
func checkout ( w http . ResponseWriter , req * http . Request ) {
if doCors {
w . Header ( ) . Set ( "Access-Control-Allow-Origin" , "*" )
}
// generate a code
var newCode string
var newCodeUser userCode
found := false
for i := 0 ; i < 1000 ; i ++ {
codeInt := rand . Intn ( MaxCodes )
newCodeUser = userCode ( codeInt )
var tmp User
r := db . Where ( "Code = ?" , newCodeUser ) . Limit ( 1 ) . Find ( & tmp )
if r . RowsAffected == 0 {
var err error
newCode , err = codeToString ( newCodeUser )
if err != nil {
w . WriteHeader ( http . StatusInternalServerError )
log . Fatalf ( "Failed to generate code from random number: %s" , err )
return
}
found = true
break
}
}
if ! found {
w . WriteHeader ( http . StatusInternalServerError )
log . Fatal ( "Failed to find new code!!!" )
return
}
params := & stripe . CheckoutSessionParams {
LineItems : [ ] * stripe . CheckoutSessionLineItemParams {
& stripe . CheckoutSessionLineItemParams {
Price : stripe . String ( daypassPriceId ) ,
Quantity : stripe . Int64 ( 1 ) ,
} ,
} ,
Mode : stripe . String ( string ( stripe . CheckoutSessionModePayment ) ) ,
SuccessURL : stripe . String ( checkoutRedirectTo ) ,
CancelURL : stripe . String ( checkoutRedirectTo ) ,
}
s , err := session . New ( params )
if err != nil {
log . Printf ( "session.New: %v" , err )
}
log . Printf ( "Creating user with checkout session ID %s\n" , s . ID )
result := db . Create ( & User {
Code : newCodeUser ,
BoughtTime : currentTime ( ) ,
BoughtType : DayPass ,
IsFulfilled : false ,
CheckoutSessionID : s . ID ,
} )
if result . Error != nil {
w . WriteHeader ( http . StatusInternalServerError )
log . Printf ( "Failed to write to database: %s" , result . Error )
} else {
fmt . Fprintf ( w , "%s|%s" , newCode , s . URL )
}
}
func index ( w http . ResponseWriter , req * http . Request ) {
req . Body = http . MaxBytesReader ( w , req . Body , 1024 * 1024 ) // no sending huge files to crash the server
if doCors {
w . Header ( ) . Set ( "Access-Control-Allow-Origin" , "*" )
}
bodyBytes , err := io . ReadAll ( req . Body )
if err != nil {
w . WriteHeader ( http . StatusBadRequest )
log . Println ( "Bad error: " , err )
return
} else {
bodyString := string ( bodyBytes )
splitBody := strings . Split ( bodyString , "|" )
if len ( splitBody ) != 2 {
w . WriteHeader ( http . StatusBadRequest )
}
var promptString string = splitBody [ 1 ]
var userToken string = splitBody [ 0 ]
// see if need to pay
rejected := false
{
if len ( userToken ) != 4 {
log . Println ( "Rejected because not 4: `" + userToken + "`" )
rejected = true
} else {
var thisUser User
thisUserCode , err := parseUserCode ( userToken )
if err != nil {
log . Printf ( "Error: Failed to parse user token %s\n" , userToken )
rejected = true
} else {
if db . First ( & thisUser , thisUserCode ) . Error != nil {
log . Printf ( "User code %d string %s couldn't be found in the database: %s\n" , thisUserCode , userToken , db . Error )
rejected = true
} else {
if isUserOld ( thisUser ) {
log . Println ( "User code " + userToken + " is old, not valid" )
db . Delete ( & thisUser )
rejected = true
} else {
rejected = false
}
}
}
}
}
if rejected {
fmt . Fprintf ( w , "0" )
return
}
if logResponses {
log . Println ( "Println line prompt string: " , promptString )
}
ctx := context . Background ( )
req := gogpt . CompletionRequest {
Model : "curie:ft-personal-2023-03-24-03-06-24" ,
MaxTokens : 80 ,
Prompt : promptString ,
Temperature : 0.9 ,
FrequencyPenalty : 0.0 ,
PresencePenalty : 0.6 ,
TopP : 1.0 ,
Stop : [ ] string { "\"" } ,
N : 1 ,
}
resp , err := c . CreateCompletion ( ctx , req )
if err != nil {
log . Println ( "Error Failed to generate: " , err )
w . WriteHeader ( http . StatusInternalServerError )
return
}
response := resp . Choices [ 0 ] . Text
if logResponses {
log . Println ( "Println response: " , response )
}
fmt . Fprintf ( w , "1%s" , response )
}
}
func currentTime ( ) int64 {
return time . Now ( ) . Unix ( )
}
func main ( ) {
var err error
db , err = gorm . Open ( sqlite . Open ( "rpgpt.db" ) , & gorm . Config { } )
if err != nil {
log . Fatal ( err )
}
db . AutoMigrate ( & User { } )
clearOld ( db )
api_key := os . Getenv ( "OPENAI_API_KEY" )
if api_key == "" {
log . Fatal ( "Must provide openai key" )
}
checkoutRedirectTo = os . Getenv ( "REDIRECT_TO" )
if checkoutRedirectTo == "" {
log . Fatal ( "Must provide a base URL (without slash) for playgpt to redirect to" )
}
stripeKey := os . Getenv ( "STRIPE_KEY" )
if stripeKey == "" {
log . Fatal ( "Must provide stripe key" )
}
daypassPriceId = os . Getenv ( "PRICE_ID" )
if daypassPriceId == "" {
log . Fatal ( "Must provide daypass price ID" )
}
stripe . Key = stripeKey
webhookSecret = os . Getenv ( "WEBHOOK_SECRET" )
if webhookSecret == "" {
log . Fatal ( "Must provide webhook secret for receiving checkout completed events" )
}
logResponses = os . Getenv ( "LOG_RESPONSES" ) != ""
doCors = os . Getenv ( "CORS" ) != ""
c = gogpt . NewClient ( api_key )
http . HandleFunc ( "/" , index )
http . HandleFunc ( "/webhook" , webhookResponse )
http . HandleFunc ( "/checkout" , checkout )
portString := ":8090"
log . Println ( "Serving on " + portString + "..." )
http . ListenAndServe ( portString , nil )
}