You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

388 lines
9.9 KiB
Go

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)
}