diff --git a/README.md b/README.md index 29cf6a04..689c7481 100644 --- a/README.md +++ b/README.md @@ -31,24 +31,26 @@ - ๐Ÿ“ฆ Cache system 8. ๐ŸŽˆ ๅฏน่ฏ่ฎฐๅฟ†ๅŠŸ่ƒฝ - ๐ŸŽˆ Conversation memorization -9. ๐ŸŽ ๅ›พ็‰‡็”ŸๆˆๅŠŸ่ƒฝ - - ๐ŸŽ Image generation -10. ๐Ÿ”” PWA ๅบ”็”จ +9. ๐Ÿ‘‹ ๅฏน่ฏๅˆ†ไบซ + - ๐Ÿ‘‹ Conversation sharing +10. ๐ŸŽ ๅ›พ็‰‡็”ŸๆˆๅŠŸ่ƒฝ + - ๐ŸŽ Image generation +11. ๐Ÿ”” PWA ๅบ”็”จ - ๐Ÿ”” PWA application -11. โšก Token ่ฎก่ดน็ณป็ปŸ +12. โšก Token ่ฎก่ดน็ณป็ปŸ - โšก Token billing system -12. ๐Ÿ“š ้€†ๅ‘ๅทฅ็จ‹ๆจกๅž‹ๆ”ฏๆŒ +13. ๐Ÿ“š ้€†ๅ‘ๅทฅ็จ‹ๆจกๅž‹ๆ”ฏๆŒ - ๐Ÿ“š Reverse engineering model support -13. ๐ŸŒ ๅ›ฝ้™…ๅŒ–ๆ”ฏๆŒ +14. ๐ŸŒ ๅ›ฝ้™…ๅŒ–ๆ”ฏๆŒ - ๐ŸŒ Internationalization support - ๐Ÿ‡จ๐Ÿ‡ณ ็ฎ€ไฝ“ไธญๆ–‡ - ๐Ÿ‡บ๐Ÿ‡ธ English - ๐Ÿ‡ท๐Ÿ‡บ ะ ัƒััะบะธะน -14. ๐ŸŽ ไธป้ข˜ๅˆ‡ๆข +15. ๐ŸŽ ไธป้ข˜ๅˆ‡ๆข - ๐ŸŽ Theme switching -15. ๐Ÿฅช Key ไธญ่ฝฌๆœๅŠก +16. ๐Ÿฅช Key ไธญ่ฝฌๆœๅŠก - ๐Ÿฅช Key relay service -16. ๐Ÿ”จ ๅคšๆจกๅž‹ๆ”ฏๆŒ +17. ๐Ÿ”จ ๅคšๆจกๅž‹ๆ”ฏๆŒ - ๐Ÿ”จ Multi-model support diff --git a/app/src/conf.ts b/app/src/conf.ts index d65e2ec8..c9f8461a 100644 --- a/app/src/conf.ts +++ b/app/src/conf.ts @@ -1,7 +1,7 @@ import axios from "axios"; import { Model } from "./conversation/types.ts"; -export const version = "3.5.6"; +export const version = "3.5.7"; export const dev: boolean = window.location.hostname === "localhost"; export const deploy: boolean = true; export let rest_api: string = "http://localhost:8094"; diff --git a/app/src/conversation/history.ts b/app/src/conversation/history.ts index 31be451a..5969e785 100644 --- a/app/src/conversation/history.ts +++ b/app/src/conversation/history.ts @@ -23,11 +23,7 @@ export async function loadConversation( ): Promise { const resp = await axios.get(`/conversation/load?id=${id}`); if (resp.data.status) return resp.data.data as ConversationInstance; - return { - id, - name: "", - message: [{ role: "assistant", content: "load conversation failed" }], - }; + return { id, name: "", message: [] }; } export async function deleteConversation( diff --git a/auth/controller.go b/auth/controller.go index 8c118cdd..bb179008 100644 --- a/auth/controller.go +++ b/auth/controller.go @@ -3,6 +3,7 @@ package auth import ( "chat/utils" "github.com/gin-gonic/gin" + "strings" ) type BuyForm struct { @@ -141,3 +142,36 @@ func BuyAPI(c *gin.Context) { }) } } + +func InviteAPI(c *gin.Context) { + user := GetUserByCtx(c) + if user == nil { + return + } + + db := utils.GetDBFromContext(c) + code := strings.TrimSpace(c.Query("code")) + if len(code) == 0 { + c.JSON(200, gin.H{ + "status": false, + "error": "invalid code", + "quota": 0., + }) + return + } + + if quota, err := user.UseInvitation(db, code); err != nil { + c.JSON(200, gin.H{ + "status": false, + "error": err.Error(), + "quota": 0., + }) + return + } else { + c.JSON(200, gin.H{ + "status": true, + "error": "success", + "quota": quota, + }) + } +} diff --git a/auth/invitation.go b/auth/invitation.go new file mode 100644 index 00000000..4c277c8a --- /dev/null +++ b/auth/invitation.go @@ -0,0 +1,107 @@ +package auth + +import ( + "chat/utils" + "database/sql" + "errors" + "fmt" +) + +type Invitation struct { + Id int64 `json:"id"` + Code string `json:"code"` + Quota float32 `json:"quota"` + Type string `json:"type"` + Used bool `json:"used"` + UsedId int64 `json:"used_id"` +} + +func GenerateCodes(db *sql.DB, num int, quota float32, t string) ([]string, error) { + arr := make([]string, 0) + idx := 0 + for idx < num { + code := fmt.Sprintf("%s-%s", t, utils.GenerateChar(24)) + if err := GenerateCode(db, code, quota, t); err != nil { + // unique constraint + if errors.Is(err, sql.ErrNoRows) { + continue + } + return nil, fmt.Errorf("failed to generate code: %w", err) + } + arr = append(arr, code) + idx++ + } + + return arr, nil +} + +func GenerateCode(db *sql.DB, code string, quota float32, t string) error { + _, err := db.Exec(` + INSERT INTO invitation (code, quota, type) + VALUES (?, ?, ?) + `, code, quota, t) + return err +} + +func GetInvitation(db *sql.DB, code string) (*Invitation, error) { + row := db.QueryRow(` + SELECT id, code, quota, type, used, used_id + FROM invitation + WHERE code = ? + `, code) + var invitation Invitation + err := row.Scan(&invitation.Id, &invitation.Code, &invitation.Quota, &invitation.Type, &invitation.Used, &invitation.UsedId) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, fmt.Errorf("invitation code not found") + } + return nil, fmt.Errorf("failed to get invitation: %w", err) + } + return &invitation, nil +} + +func (i *Invitation) IsUsed() bool { + return i.Used +} + +func (i *Invitation) Use(db *sql.DB, userId int64) error { + _, err := db.Exec(` + UPDATE invitation SET used = TRUE, used_id = ? WHERE id = ? + `, userId, i.Id) + return err +} + +func (i *Invitation) GetQuota() float32 { + return i.Quota +} + +func (i *Invitation) UseInvitation(db *sql.DB, user User) error { + if i.IsUsed() { + return fmt.Errorf("this invitation has been used") + } + + if err := i.Use(db, user.GetID(db)); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return fmt.Errorf("invitation code not found") + } + return fmt.Errorf("failed to use invitation: %w", err) + } + + if !user.IncreaseQuota(db, i.GetQuota()) { + return fmt.Errorf("failed to increase quota for user") + } + + return nil +} + +func (u *User) UseInvitation(db *sql.DB, code string) (float32, error) { + if invitation, err := GetInvitation(db, code); err != nil { + return 0, err + } else { + if err := invitation.UseInvitation(db, *u); err != nil { + return 0, fmt.Errorf("failed to use invitation: %w", err) + } + + return invitation.GetQuota(), nil + } +} diff --git a/auth/router.go b/auth/router.go index 389f34ef..b73277b4 100644 --- a/auth/router.go +++ b/auth/router.go @@ -11,4 +11,5 @@ func Register(app *gin.Engine) { app.POST("/buy", BuyAPI) app.GET("/subscription", SubscriptionAPI) app.POST("/subscribe", SubscribeAPI) + app.GET("/invite", InviteAPI) } diff --git a/connection/database.go b/connection/database.go index 816725a9..4a4826c9 100644 --- a/connection/database.go +++ b/connection/database.go @@ -33,6 +33,7 @@ func ConnectMySQL() *sql.DB { CreateQuotaTable(db) CreateSubscriptionTable(db) CreateApiKeyTable(db) + CreateInvitationTable(db) return db } @@ -151,3 +152,23 @@ func CreateApiKeyTable(db *sql.DB) { log.Fatal(err) } } + +func CreateInvitationTable(db *sql.DB) { + _, err := db.Exec(` + CREATE TABLE IF NOT EXISTS invitation ( + id INT PRIMARY KEY AUTO_INCREMENT, + code VARCHAR(255) UNIQUE, + quota DECIMAL(6, 4), + type VARCHAR(255), + used BOOLEAN DEFAULT FALSE, + used_id INT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY (used_id, type), + FOREIGN KEY (used_id) REFERENCES auth(id) + ); + `) + if err != nil { + log.Fatal(err) + } +} diff --git a/middleware/throttle.go b/middleware/throttle.go index 8d00b36a..d452d2cb 100644 --- a/middleware/throttle.go +++ b/middleware/throttle.go @@ -38,6 +38,7 @@ var limits = map[string]Limiter{ "/subscription": {Duration: 1, Count: 2}, "/chat": {Duration: 1, Count: 5}, "/conversation": {Duration: 1, Count: 5}, + "/invite": {Duration: 7200, Count: 20}, } func GetPrefixMap[T comparable](s string, p map[string]T) *T {