@@ -0,0 +1,50 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/basketikun/infinite-canvas/model"
|
||||
"github.com/basketikun/infinite-canvas/repository"
|
||||
)
|
||||
|
||||
func ListAssets(q model.Query) (model.AssetList, error) {
|
||||
items, total, err := repository.ListAssets(q)
|
||||
if err != nil {
|
||||
return model.AssetList{}, err
|
||||
}
|
||||
tags, err := repository.ListAssetTags(q)
|
||||
if err != nil {
|
||||
return model.AssetList{}, err
|
||||
}
|
||||
return model.AssetList{Items: items, Tags: tags, Total: int(total)}, nil
|
||||
}
|
||||
|
||||
func SaveAsset(item model.Asset) (model.Asset, error) {
|
||||
now := time.Now().Format(time.RFC3339)
|
||||
if item.Type == "" {
|
||||
item.Type = model.AssetTypeText
|
||||
}
|
||||
if item.ID == "" {
|
||||
item.ID = newID("asset")
|
||||
item.CreatedAt = now
|
||||
}
|
||||
item.UpdatedAt = now
|
||||
if item.CoverURL == "" {
|
||||
item.CoverURL = assetCoverURL(item)
|
||||
}
|
||||
return repository.SaveAsset(item)
|
||||
}
|
||||
|
||||
func DeleteAsset(id string) error {
|
||||
return repository.DeleteAsset(id)
|
||||
}
|
||||
|
||||
func assetCoverURL(item model.Asset) string {
|
||||
if item.CoverURL != "" {
|
||||
return item.CoverURL
|
||||
}
|
||||
if item.Type == model.AssetTypeImage {
|
||||
return item.URL
|
||||
}
|
||||
return ""
|
||||
}
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/basketikun/infinite-canvas/config"
|
||||
"github.com/basketikun/infinite-canvas/model"
|
||||
"github.com/basketikun/infinite-canvas/repository"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type TokenClaims struct {
|
||||
UserID string `json:"userId"`
|
||||
Username string `json:"username"`
|
||||
Role model.UserRole `json:"role"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func EnsureDefaultAdmin() error {
|
||||
if strings.TrimSpace(config.Cfg.AdminUsername) == "" || strings.TrimSpace(config.Cfg.AdminPassword) == "" {
|
||||
return nil
|
||||
}
|
||||
WarnDefaultSecurityConfig()
|
||||
hasAdmin, err := repository.HasAdmin()
|
||||
if err != nil || hasAdmin {
|
||||
return err
|
||||
}
|
||||
hash, err := hashPassword(config.Cfg.AdminPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = repository.SaveUser(model.User{
|
||||
ID: newID("user"),
|
||||
Username: strings.TrimSpace(config.Cfg.AdminUsername),
|
||||
Password: hash,
|
||||
Role: model.UserRoleAdmin,
|
||||
CreatedAt: now(),
|
||||
UpdatedAt: now(),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func Register(username string, password string) (model.AuthSession, error) {
|
||||
return model.AuthSession{}, errors.New("注册功能暂时关闭")
|
||||
username = strings.TrimSpace(username)
|
||||
if strings.ContainsAny(username, " \t\r\n") {
|
||||
return model.AuthSession{}, errors.New("用户名不能包含空格")
|
||||
}
|
||||
if username == "" || password == "" {
|
||||
return model.AuthSession{}, errors.New("用户名和密码不能为空")
|
||||
}
|
||||
if _, ok, err := repository.GetUserByUsername(username); err != nil || ok {
|
||||
if err != nil {
|
||||
return model.AuthSession{}, err
|
||||
}
|
||||
return model.AuthSession{}, errors.New("用户名已存在")
|
||||
}
|
||||
hash, err := hashPassword(password)
|
||||
if err != nil {
|
||||
return model.AuthSession{}, err
|
||||
}
|
||||
user, err := repository.SaveUser(model.User{
|
||||
ID: newID("user"),
|
||||
Username: username,
|
||||
Password: hash,
|
||||
Role: model.UserRoleUser,
|
||||
CreatedAt: now(),
|
||||
UpdatedAt: now(),
|
||||
})
|
||||
if err != nil {
|
||||
return model.AuthSession{}, err
|
||||
}
|
||||
return newSession(user)
|
||||
}
|
||||
|
||||
func Login(username string, password string) (model.AuthSession, error) {
|
||||
user, ok, err := repository.GetUserByUsername(strings.TrimSpace(username))
|
||||
if err != nil {
|
||||
return model.AuthSession{}, err
|
||||
}
|
||||
if !ok || bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) != nil {
|
||||
return model.AuthSession{}, errors.New("用户名或密码错误")
|
||||
}
|
||||
return newSession(user)
|
||||
}
|
||||
|
||||
func ParseToken(tokenText string) (TokenClaims, error) {
|
||||
claims := TokenClaims{}
|
||||
token, err := jwt.ParseWithClaims(tokenText, &claims, func(token *jwt.Token) (any, error) {
|
||||
return []byte(config.Cfg.JWTSecret), nil
|
||||
})
|
||||
if err != nil || !token.Valid {
|
||||
return TokenClaims{}, errors.New("登录状态无效")
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func CurrentAuthUser(tokenText string) (model.AuthUser, bool) {
|
||||
claims, err := ParseToken(tokenText)
|
||||
if err != nil {
|
||||
return model.AuthUser{}, false
|
||||
}
|
||||
user, ok, err := repository.GetUserByID(claims.UserID)
|
||||
if err != nil || !ok {
|
||||
return model.AuthUser{}, false
|
||||
}
|
||||
return model.PublicUser(user), true
|
||||
}
|
||||
|
||||
func ListUsers(q model.Query) (model.UserList, error) {
|
||||
users, total, err := repository.ListUsers(q)
|
||||
if err != nil {
|
||||
return model.UserList{}, err
|
||||
}
|
||||
for i := range users {
|
||||
users[i].Password = ""
|
||||
}
|
||||
return model.UserList{Items: users, Total: int(total)}, nil
|
||||
}
|
||||
|
||||
func SaveUser(user model.User, password string) (model.User, error) {
|
||||
user.Username = strings.TrimSpace(user.Username)
|
||||
if strings.ContainsAny(user.Username, " \t\r\n") {
|
||||
return user, errors.New("用户名不能包含空格")
|
||||
}
|
||||
if user.Username == "" {
|
||||
return user, errors.New("用户名不能为空")
|
||||
}
|
||||
if user.Role == "" || user.Role == model.UserRoleGuest {
|
||||
user.Role = model.UserRoleUser
|
||||
}
|
||||
if saved, ok, err := repository.GetUserByUsername(user.Username); err != nil {
|
||||
return user, err
|
||||
} else if ok && saved.ID != user.ID {
|
||||
return user, errors.New("用户名已存在")
|
||||
}
|
||||
if user.ID == "" {
|
||||
user.ID = newID("user")
|
||||
user.CreatedAt = now()
|
||||
} else if saved, ok, err := repository.GetUserByID(user.ID); err != nil {
|
||||
return user, err
|
||||
} else if ok {
|
||||
user.CreatedAt = saved.CreatedAt
|
||||
user.Password = saved.Password
|
||||
}
|
||||
if password != "" {
|
||||
hash, err := hashPassword(password)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
user.Password = hash
|
||||
}
|
||||
if user.Password == "" {
|
||||
return user, errors.New("密码不能为空")
|
||||
}
|
||||
user.UpdatedAt = now()
|
||||
user, err := repository.SaveUser(user)
|
||||
user.Password = ""
|
||||
return user, err
|
||||
}
|
||||
|
||||
func DeleteUser(id string) error {
|
||||
return repository.DeleteUser(id)
|
||||
}
|
||||
|
||||
func GuestUser() model.AuthUser {
|
||||
return model.AuthUser{ID: "", Username: "guest", Role: model.UserRoleGuest}
|
||||
}
|
||||
|
||||
func newSession(user model.User) (model.AuthSession, error) {
|
||||
token, err := newToken(user)
|
||||
if err != nil {
|
||||
return model.AuthSession{}, err
|
||||
}
|
||||
return model.AuthSession{Token: token, User: model.PublicUser(user)}, nil
|
||||
}
|
||||
|
||||
func newToken(user model.User) (string, error) {
|
||||
expireHours := config.Cfg.JWTExpireHours
|
||||
if expireHours <= 0 {
|
||||
expireHours = 168
|
||||
}
|
||||
claims := TokenClaims{
|
||||
UserID: user.ID,
|
||||
Username: user.Username,
|
||||
Role: user.Role,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(expireHours) * time.Hour)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
Subject: user.ID,
|
||||
},
|
||||
}
|
||||
return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(config.Cfg.JWTSecret))
|
||||
}
|
||||
|
||||
func hashPassword(password string) (string, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
return string(hash), err
|
||||
}
|
||||
|
||||
func now() string {
|
||||
return time.Now().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
func newID(prefix string) string {
|
||||
return prefix + "-" + uuid.NewString()
|
||||
}
|
||||
|
||||
func WarnDefaultSecurityConfig() {
|
||||
if config.Cfg.AdminUsername == "admin" && config.Cfg.AdminPassword == "infinite-canvas" {
|
||||
log.Println("WARNING: using default admin credentials, please set ADMIN_USERNAME and ADMIN_PASSWORD to safer values before deployment")
|
||||
}
|
||||
if config.Cfg.JWTSecret == "infinite-canvas" {
|
||||
log.Println("WARNING: using default JWT_SECRET, please set a long random value before deployment")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/basketikun/infinite-canvas/model"
|
||||
)
|
||||
|
||||
type userContextKey struct{}
|
||||
|
||||
func WithUser(ctx context.Context, user model.AuthUser) context.Context {
|
||||
return context.WithValue(ctx, userContextKey{}, user)
|
||||
}
|
||||
|
||||
func UserFromContext(ctx context.Context) (model.AuthUser, bool) {
|
||||
user, ok := ctx.Value(userContextKey{}).(model.AuthUser)
|
||||
return user, ok
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/basketikun/infinite-canvas/model"
|
||||
"github.com/basketikun/infinite-canvas/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
gptImage2RawBase = "https://raw.githubusercontent.com/EvoLinkAI/awesome-gpt-image-2-API-and-Prompts/main"
|
||||
awesomeGptImageRawBase = "https://raw.githubusercontent.com/ZeroLu/awesome-gpt-image/main"
|
||||
awesomeGpt4oImagePromptsBase = "https://raw.githubusercontent.com/ImgEdify/Awesome-GPT4o-Image-Prompts/main"
|
||||
youMindGptImage2RawBase = "https://raw.githubusercontent.com/YouMind-OpenLab/awesome-gpt-image-2/main"
|
||||
youMindNanoBananaProRawBase = "https://raw.githubusercontent.com/YouMind-OpenLab/awesome-nano-banana-pro-prompts/main"
|
||||
)
|
||||
|
||||
var gptImage2CaseFiles = []string{"README.md", "cases/ad-creative.md", "cases/character.md", "cases/comparison.md", "cases/ecommerce.md", "cases/portrait.md", "cases/poster.md", "cases/ui.md"}
|
||||
|
||||
type gptImage2Data struct {
|
||||
Records []struct {
|
||||
Title string `json:"title"`
|
||||
TweetURL string `json:"tweet_url"`
|
||||
ImageDir string `json:"image_dir"`
|
||||
Category string `json:"category"`
|
||||
AddedAt string `json:"added_at"`
|
||||
} `json:"records"`
|
||||
}
|
||||
|
||||
func SyncPromptCategory(category string) ([]model.PromptCategory, error) {
|
||||
for _, item := range repository.PromptCategories() {
|
||||
if item.Category != category {
|
||||
continue
|
||||
}
|
||||
items, err := buildPromptCategory(item.Category)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := repository.ReplacePromptCategory(item, items); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return repository.ListPromptCategories()
|
||||
}
|
||||
return nil, errors.New("未知提示词分类")
|
||||
}
|
||||
|
||||
func buildPromptCategory(category string) ([]model.Prompt, error) {
|
||||
switch category {
|
||||
case "gpt-image-2-prompts":
|
||||
return buildGptImage2Prompts()
|
||||
case "awesome-gpt-image":
|
||||
return buildAwesomeGptImagePrompts()
|
||||
case "awesome-gpt4o-image-prompts":
|
||||
return buildAwesomeGpt4oImagePrompts()
|
||||
case "youmind-gpt-image-2":
|
||||
return buildYouMindGptImage2Prompts()
|
||||
case "youmind-nano-banana-pro":
|
||||
return buildYouMindNanoBananaProPrompts()
|
||||
}
|
||||
return nil, errors.New("未知提示词分类")
|
||||
}
|
||||
|
||||
func fetchText(baseURL, file string) (string, error) {
|
||||
request, _ := http.NewRequest(http.MethodGet, baseURL+"/"+file, nil)
|
||||
client := http.Client{Timeout: 30 * time.Second}
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
||||
return "", errors.New(file + " 拉取失败")
|
||||
}
|
||||
data, err := io.ReadAll(response.Body)
|
||||
return string(data), err
|
||||
}
|
||||
|
||||
func buildGptImage2Prompts() ([]model.Prompt, error) {
|
||||
cases := map[string]string{}
|
||||
raw, err := fetchText(gptImage2RawBase, "data/ingested_tweets.json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data := gptImage2Data{}
|
||||
if err := json.Unmarshal([]byte(raw), &data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, file := range gptImage2CaseFiles {
|
||||
markdown, err := fetchText(gptImage2RawBase, file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
collectGptImage2Cases(cases, markdown)
|
||||
}
|
||||
items := []model.Prompt{}
|
||||
for _, item := range data.Records {
|
||||
prompt := cases[item.TweetURL]
|
||||
if prompt == "" {
|
||||
continue
|
||||
}
|
||||
image := gptImage2RawBase + "/" + item.ImageDir + "/output.jpg"
|
||||
items = append(items, model.Prompt{ID: "gpt-image-2-prompts-" + leftPad(len(items)+1), Title: item.Title, CoverURL: image, Prompt: prompt, Tags: tagsFromCategory(item.Category), CreatedAt: item.AddedAt, UpdatedAt: item.AddedAt, Preview: markdownPreview([]string{image})})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func collectGptImage2Cases(cases map[string]string, markdown string) {
|
||||
re := regexp.MustCompile("(?s)### Case \\d+: \\[[^\\]]+\\]\\(([^)]+)\\).*?\\*\\*Prompt:\\*\\*\\s*\\r?\\n\\s*```[\\w-]*\\r?\\n(.*?)\\r?\\n```")
|
||||
for _, match := range re.FindAllStringSubmatch(markdown, -1) {
|
||||
cases[match[1]] = strings.TrimSpace(match[2])
|
||||
}
|
||||
}
|
||||
|
||||
func buildAwesomeGptImagePrompts() ([]model.Prompt, error) {
|
||||
markdown, err := fetchText(awesomeGptImageRawBase, "README.zh-CN.md")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := []model.Prompt{}
|
||||
for _, section := range splitBeforeHeading(markdown, "## ") {
|
||||
tags := tagsFromHeading(firstMatch(section, `(?m)^##\s+(.+)$`))
|
||||
for _, block := range splitBeforeHeading(section, "### ") {
|
||||
title := strings.TrimSpace(regexp.MustCompile(`\[([^\]]+)]\([^)]+\)`).ReplaceAllString(firstMatch(block, `(?m)^###\s+(.+)$`), "$1"))
|
||||
prompt := strings.TrimSpace(firstMatch(block, "(?s)\\*\\*提示词:\\*\\*\\s*\\r?\\n\\s*```[\\w-]*\\r?\\n(.*?)\\r?\\n```"))
|
||||
if title == "" || prompt == "" {
|
||||
continue
|
||||
}
|
||||
images := extractMarkdownImages(awesomeGptImageRawBase, block)
|
||||
cover := ""
|
||||
if len(images) > 0 {
|
||||
cover = images[0]
|
||||
}
|
||||
items = append(items, model.Prompt{ID: "awesome-gpt-image-" + leftPad(len(items)+1), Title: title, CoverURL: cover, Prompt: prompt, Tags: tags, Preview: markdownPreview(images)})
|
||||
}
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func buildAwesomeGpt4oImagePrompts() ([]model.Prompt, error) {
|
||||
markdown, err := fetchText(awesomeGpt4oImagePromptsBase, "README.zh-CN.md")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := []model.Prompt{}
|
||||
for _, block := range splitBeforeHeading(markdown, "### ") {
|
||||
title := strings.TrimSpace(firstMatch(block, `(?m)^###\s+(.+)$`))
|
||||
prompt := strings.TrimSpace(firstMatch(block, "(?s)- \\*\\*提示词文本:\\*\\*\\s*`(.*?)`"))
|
||||
if title == "" || prompt == "" {
|
||||
continue
|
||||
}
|
||||
images := extractMarkdownImages(awesomeGpt4oImagePromptsBase, block)
|
||||
cover := ""
|
||||
if len(images) > 0 {
|
||||
cover = images[0]
|
||||
}
|
||||
items = append(items, model.Prompt{ID: "awesome-gpt4o-image-prompts-" + leftPad(len(items)+1), Title: title, CoverURL: cover, Prompt: prompt, Tags: []string{"gpt4o"}, Preview: markdownPreview(images)})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func buildYouMindGptImage2Prompts() ([]model.Prompt, error) {
|
||||
return buildYouMindPrompts(youMindGptImage2RawBase, "youmind-gpt-image-2", "gpt-image-2")
|
||||
}
|
||||
|
||||
func buildYouMindNanoBananaProPrompts() ([]model.Prompt, error) {
|
||||
return buildYouMindPrompts(youMindNanoBananaProRawBase, "youmind-nano-banana-pro", "nano-banana-pro")
|
||||
}
|
||||
|
||||
func buildYouMindPrompts(baseURL, idPrefix, modelTag string) ([]model.Prompt, error) {
|
||||
markdown, err := fetchText(baseURL, "README_zh.md")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := []model.Prompt{}
|
||||
for _, block := range splitBeforeHeading(markdown, "### ") {
|
||||
title := strings.TrimSpace(firstMatch(block, `(?m)^###\s+No\.\s*\d+:\s*(.+)$`))
|
||||
prompt := strings.TrimSpace(firstMatch(block, "(?s)#### .*?提示词\\s*\\r?\\n\\s*```[\\w-]*\\r?\\n(.*?)\\r?\\n```"))
|
||||
if title == "" || prompt == "" {
|
||||
continue
|
||||
}
|
||||
images := extractMarkdownImages(baseURL, block)
|
||||
cover := ""
|
||||
if len(images) > 0 {
|
||||
cover = images[0]
|
||||
}
|
||||
items = append(items, model.Prompt{ID: idPrefix + "-" + leftPad(len(items)+1), Title: title, CoverURL: cover, Prompt: prompt, Tags: youMindTags(title, modelTag), Preview: markdownPreview(images)})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func splitBeforeHeading(markdown string, prefix string) []string {
|
||||
blocks := []string{}
|
||||
lines := strings.Split(markdown, "\n")
|
||||
current := []string{}
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, prefix) && len(current) > 0 {
|
||||
blocks = append(blocks, strings.Join(current, "\n"))
|
||||
current = []string{}
|
||||
}
|
||||
current = append(current, line)
|
||||
}
|
||||
return append(blocks, strings.Join(current, "\n"))
|
||||
}
|
||||
|
||||
func firstMatch(value string, pattern string) string {
|
||||
match := regexp.MustCompile(pattern).FindStringSubmatch(value)
|
||||
if len(match) > 1 {
|
||||
return match[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func tagsFromCategory(category string) []string {
|
||||
return splitTags(regexp.MustCompile(`(?i)\s+Cases$`).ReplaceAllString(category, ""), `\s*(&|and)\s*`)
|
||||
}
|
||||
|
||||
func tagsFromHeading(heading string) []string {
|
||||
return splitTags(regexp.MustCompile(`[^\p{L}\p{N}/&、与 ]`).ReplaceAllString(heading, ""), `\s*(/|&|、|与)\s*`)
|
||||
}
|
||||
|
||||
func youMindTags(title, modelTag string) []string {
|
||||
tags := []string{modelTag}
|
||||
parts := strings.SplitN(title, " - ", 2)
|
||||
if len(parts) > 1 {
|
||||
tags = append(tags, tagsFromHeading(parts[0])...)
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
func splitTags(value string, pattern string) []string {
|
||||
tags := []string{}
|
||||
for _, tag := range regexp.MustCompile(pattern).Split(value, -1) {
|
||||
if tag = strings.ToLower(strings.TrimSpace(tag)); tag != "" {
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
func markdownPreview(images []string) string {
|
||||
lines := []string{}
|
||||
for _, image := range images {
|
||||
if image != "" {
|
||||
lines = append(lines, "")
|
||||
}
|
||||
}
|
||||
return strings.Join(lines, "\n\n")
|
||||
}
|
||||
|
||||
func extractMarkdownImages(baseURL string, block string) []string {
|
||||
seen := map[string]bool{}
|
||||
images := []string{}
|
||||
for _, pattern := range []string{`<img[^>]+src="([^"]+)"`, `!\[[^\]]*]\(([^)]+)\)`} {
|
||||
for _, match := range regexp.MustCompile(pattern).FindAllStringSubmatch(block, -1) {
|
||||
image := absoluteImage(baseURL, match[1])
|
||||
if image != "" && !seen[image] {
|
||||
seen[image] = true
|
||||
images = append(images, image)
|
||||
}
|
||||
}
|
||||
}
|
||||
return images
|
||||
}
|
||||
|
||||
func absoluteImage(baseURL, image string) string {
|
||||
if image == "" || strings.HasPrefix(image, "http://") || strings.HasPrefix(image, "https://") {
|
||||
return image
|
||||
}
|
||||
return baseURL + "/" + strings.TrimLeft(strings.TrimPrefix(image, "."), "/")
|
||||
}
|
||||
|
||||
func leftPad(value int) string {
|
||||
if value >= 1000 {
|
||||
return strconv.Itoa(value)
|
||||
}
|
||||
text := "000" + strconv.Itoa(value)
|
||||
return text[len(text)-3:]
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/basketikun/infinite-canvas/model"
|
||||
"github.com/basketikun/infinite-canvas/repository"
|
||||
)
|
||||
|
||||
func ListPrompts(q model.Query) (model.PromptList, error) {
|
||||
items, total, err := repository.ListPrompts(q)
|
||||
if err != nil {
|
||||
return model.PromptList{}, err
|
||||
}
|
||||
tags, err := repository.ListPromptTags(q)
|
||||
if err != nil {
|
||||
return model.PromptList{}, err
|
||||
}
|
||||
categories := promptCategoryCodes(ListPromptCategories())
|
||||
return model.PromptList{Items: items, Tags: tags, Categories: categories, Total: int(total)}, nil
|
||||
}
|
||||
|
||||
func ListPromptCategories() []model.PromptCategory {
|
||||
categories, _ := repository.ListPromptCategories()
|
||||
return categories
|
||||
}
|
||||
|
||||
func SavePrompt(item model.Prompt) (model.Prompt, error) {
|
||||
now := time.Now().Format(time.RFC3339)
|
||||
if item.Category == "" {
|
||||
item.Category = repository.PromptCategories()[0].Category
|
||||
}
|
||||
if item.ID == "" {
|
||||
item.ID = newID(item.Category)
|
||||
item.CreatedAt = now
|
||||
}
|
||||
item.UpdatedAt = now
|
||||
category, ok := repository.PromptCategoryByCode(item.Category)
|
||||
if !ok {
|
||||
category = repository.PromptCategories()[0]
|
||||
item.Category = category.Category
|
||||
}
|
||||
item.GithubURL = ""
|
||||
return repository.SavePrompt(item)
|
||||
}
|
||||
|
||||
func DeletePrompt(id string) error {
|
||||
return repository.DeletePrompt(id)
|
||||
}
|
||||
|
||||
func promptCategoryCodes(items []model.PromptCategory) []string {
|
||||
codes := []string{}
|
||||
for _, item := range items {
|
||||
if item.Category != "" {
|
||||
codes = append(codes, item.Category)
|
||||
}
|
||||
}
|
||||
return codes
|
||||
}
|
||||
Reference in New Issue
Block a user