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