mirror of https://github.com/rocboss/paopao-ce
Introduce a registry-backed KV settings system with encrypted secret storage, add an admin UI for grouped system configuration, and shrink bootstrap YAML to startup-critical settings with docs updated for the new workflow.pull/717/head
parent
46bcd8b518
commit
f2df8af8ef
@ -0,0 +1,98 @@
|
||||
// Copyright 2026 ROC. All rights reserved.
|
||||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package sitesetting
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/rocboss/paopao-ce/internal/conf"
|
||||
"github.com/rocboss/paopao-ce/pkg/xerror"
|
||||
)
|
||||
|
||||
const encryptedValuePrefix = "enc:v1:"
|
||||
|
||||
type secretCodec struct {
|
||||
key []byte
|
||||
}
|
||||
|
||||
func newSecretCodec() *secretCodec {
|
||||
if conf.AdminSettingsSetting == nil {
|
||||
return &secretCodec{}
|
||||
}
|
||||
seed := strings.TrimSpace(conf.AdminSettingsSetting.EncryptionKey)
|
||||
if seed == "" {
|
||||
return &secretCodec{}
|
||||
}
|
||||
sum := sha256.Sum256([]byte(seed))
|
||||
return &secretCodec{key: sum[:]}
|
||||
}
|
||||
|
||||
func (c *secretCodec) enabled() bool {
|
||||
return len(c.key) == 32
|
||||
}
|
||||
|
||||
func (c *secretCodec) Encrypt(value string) (string, error) {
|
||||
if value == "" {
|
||||
return "", nil
|
||||
}
|
||||
if !c.enabled() {
|
||||
return "", xerror.ServerError.WithDetails("admin settings encryption key is not configured")
|
||||
}
|
||||
block, err := aes.NewCipher(c.key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return "", err
|
||||
}
|
||||
ciphertext := gcm.Seal(nil, nonce, []byte(value), nil)
|
||||
payload := append(nonce, ciphertext...)
|
||||
return encryptedValuePrefix + base64.StdEncoding.EncodeToString(payload), nil
|
||||
}
|
||||
|
||||
func (c *secretCodec) Decrypt(value string) (string, error) {
|
||||
if value == "" {
|
||||
return "", nil
|
||||
}
|
||||
if !strings.HasPrefix(value, encryptedValuePrefix) {
|
||||
return value, nil
|
||||
}
|
||||
if !c.enabled() {
|
||||
return "", xerror.ServerError.WithDetails("admin settings encryption key is not configured")
|
||||
}
|
||||
raw, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(value, encryptedValuePrefix))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("decode encrypted setting: %w", err)
|
||||
}
|
||||
block, err := aes.NewCipher(c.key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(raw) < gcm.NonceSize() {
|
||||
return "", xerror.ServerError.WithDetails("invalid encrypted setting payload")
|
||||
}
|
||||
nonce, ciphertext := raw[:gcm.NonceSize()], raw[gcm.NonceSize():]
|
||||
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(plaintext), nil
|
||||
}
|
||||
@ -0,0 +1,688 @@
|
||||
// Copyright 2026 ROC. All rights reserved.
|
||||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package sitesetting
|
||||
|
||||
import (
|
||||
stdjson "encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/alimy/tryst/cfg"
|
||||
"github.com/rocboss/paopao-ce/internal/conf"
|
||||
"github.com/rocboss/paopao-ce/pkg/xerror"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type ValueType string
|
||||
type ApplyMode string
|
||||
|
||||
const (
|
||||
TypeBool ValueType = "bool"
|
||||
TypeInt ValueType = "int"
|
||||
TypeFloat ValueType = "float"
|
||||
TypeString ValueType = "string"
|
||||
|
||||
ApplyModeLive ApplyMode = "live"
|
||||
ApplyModeRestartRequired ApplyMode = "restart_required"
|
||||
ApplyModeBootstrapOnly ApplyMode = "bootstrap_only"
|
||||
)
|
||||
|
||||
type bootstrapSnapshot struct {
|
||||
App confAppSnapshot
|
||||
TweetSearch confTweetSearchSnapshot
|
||||
Zinc confZincSnapshot
|
||||
Meili confMeiliSnapshot
|
||||
ObjectStorage confObjectStorageSnapshot
|
||||
AliOSS confAliOSSSnapshot
|
||||
COS confCOSSnapshot
|
||||
HuaweiOBS confHuaweiOBSSnapshot
|
||||
MinIO confMinIOSnapshot
|
||||
S3 confS3Snapshot
|
||||
LocalOSS confLocalOSSSnapshot
|
||||
SmsJuhe confSmsJuheSnapshot
|
||||
Alipay confAlipaySnapshot
|
||||
WebProfile confWebProfileSnapshot
|
||||
}
|
||||
|
||||
type confAppSnapshot struct {
|
||||
AttachmentIncomeRate float64
|
||||
MaxCommentCount int64
|
||||
MaxWhisperDaily int64
|
||||
MaxCaptchaTimes int
|
||||
DefaultPageSize int
|
||||
MaxPageSize int
|
||||
}
|
||||
|
||||
type confTweetSearchSnapshot struct {
|
||||
MaxUpdateQPS int
|
||||
MinWorker int
|
||||
}
|
||||
|
||||
type confZincSnapshot struct {
|
||||
Host string
|
||||
Index string
|
||||
User string
|
||||
Password string
|
||||
Secure bool
|
||||
}
|
||||
|
||||
type confMeiliSnapshot struct {
|
||||
Host string
|
||||
Index string
|
||||
ApiKey string
|
||||
Secure bool
|
||||
}
|
||||
|
||||
type confObjectStorageSnapshot struct {
|
||||
RetainInDays int
|
||||
TempDir string
|
||||
}
|
||||
|
||||
type confAliOSSSnapshot struct {
|
||||
Endpoint string
|
||||
AccessKeyID string
|
||||
AccessKeySecret string
|
||||
Bucket string
|
||||
Domain string
|
||||
}
|
||||
|
||||
type confCOSSnapshot struct {
|
||||
SecretID string
|
||||
SecretKey string
|
||||
Region string
|
||||
Bucket string
|
||||
Domain string
|
||||
}
|
||||
|
||||
type confHuaweiOBSSnapshot struct {
|
||||
AccessKey string
|
||||
SecretKey string
|
||||
Endpoint string
|
||||
Bucket string
|
||||
Domain string
|
||||
}
|
||||
|
||||
type confMinIOSnapshot struct {
|
||||
AccessKey string
|
||||
SecretKey string
|
||||
Secure bool
|
||||
Endpoint string
|
||||
Bucket string
|
||||
Domain string
|
||||
}
|
||||
|
||||
type confS3Snapshot struct {
|
||||
AccessKey string
|
||||
SecretKey string
|
||||
Secure bool
|
||||
Endpoint string
|
||||
Bucket string
|
||||
Domain string
|
||||
}
|
||||
|
||||
type confLocalOSSSnapshot struct {
|
||||
SavePath string
|
||||
Secure bool
|
||||
Bucket string
|
||||
Domain string
|
||||
}
|
||||
|
||||
type confSmsJuheSnapshot struct {
|
||||
Gateway string
|
||||
Key string
|
||||
TplID string
|
||||
TplVal string
|
||||
}
|
||||
|
||||
type confAlipaySnapshot struct {
|
||||
AppID string
|
||||
PrivateKey string
|
||||
RootCertFile string
|
||||
PublicCertFile string
|
||||
AppPublicCertFile string
|
||||
InProduction bool
|
||||
}
|
||||
|
||||
type confWebProfileSnapshot struct {
|
||||
UseFriendship bool
|
||||
EnableTrendsBar bool
|
||||
EnableWallet bool
|
||||
AllowTweetAttachment bool
|
||||
AllowTweetAttachmentPrice bool
|
||||
AllowTweetVideo bool
|
||||
AllowUserRegister bool
|
||||
AllowPhoneBind bool
|
||||
DefaultTweetMaxLength int
|
||||
TweetWebEllipsisSize int
|
||||
TweetMobileEllipsisSize int
|
||||
DefaultTweetVisibility string
|
||||
DefaultMsgLoopInterval int
|
||||
CopyrightTop string
|
||||
CopyrightLeft string
|
||||
CopyrightLeftLink string
|
||||
CopyrightRight string
|
||||
CopyrightRightLink string
|
||||
}
|
||||
|
||||
type Option struct {
|
||||
Label string `json:"label"`
|
||||
Value any `json:"value"`
|
||||
}
|
||||
|
||||
type Definition struct {
|
||||
Key string
|
||||
Group string
|
||||
Section string
|
||||
Type ValueType
|
||||
Label string
|
||||
Description string
|
||||
ApplyMode ApplyMode
|
||||
Secret bool
|
||||
Readonly bool
|
||||
Options []Option
|
||||
Active func() bool
|
||||
CurrentValue func() any
|
||||
BootstrapDefault func() any
|
||||
ValidateValue func(any) (any, error)
|
||||
BootstrapOverride func(any)
|
||||
}
|
||||
|
||||
var (
|
||||
ReadonlyFields = []string{"allow_user_register", "allow_phone_bind"}
|
||||
bootstrapConfig *bootstrapSnapshot
|
||||
visibilityOption = []Option{{Label: "Public", Value: "public"}, {Label: "Following", Value: "following"}, {Label: "Friend", Value: "friend"}, {Label: "Private", Value: "private"}}
|
||||
)
|
||||
|
||||
func ensureBootstrapSnapshot() {
|
||||
if bootstrapConfig != nil {
|
||||
return
|
||||
}
|
||||
bootstrapConfig = &bootstrapSnapshot{}
|
||||
if conf.AppSetting != nil {
|
||||
bootstrapConfig.App = confAppSnapshot{
|
||||
AttachmentIncomeRate: conf.AppSetting.AttachmentIncomeRate,
|
||||
MaxCommentCount: conf.AppSetting.MaxCommentCount,
|
||||
MaxWhisperDaily: conf.AppSetting.MaxWhisperDaily,
|
||||
MaxCaptchaTimes: conf.AppSetting.MaxCaptchaTimes,
|
||||
DefaultPageSize: conf.AppSetting.DefaultPageSize,
|
||||
MaxPageSize: conf.AppSetting.MaxPageSize,
|
||||
}
|
||||
}
|
||||
if conf.TweetSearchSetting != nil {
|
||||
bootstrapConfig.TweetSearch = confTweetSearchSnapshot{MaxUpdateQPS: conf.TweetSearchSetting.MaxUpdateQPS, MinWorker: conf.TweetSearchSetting.MinWorker}
|
||||
}
|
||||
if conf.ZincSetting != nil {
|
||||
bootstrapConfig.Zinc = confZincSnapshot{Host: conf.ZincSetting.Host, Index: conf.ZincSetting.Index, User: conf.ZincSetting.User, Password: conf.ZincSetting.Password, Secure: conf.ZincSetting.Secure}
|
||||
}
|
||||
if conf.MeiliSetting != nil {
|
||||
bootstrapConfig.Meili = confMeiliSnapshot{Host: conf.MeiliSetting.Host, Index: conf.MeiliSetting.Index, ApiKey: conf.MeiliSetting.ApiKey, Secure: conf.MeiliSetting.Secure}
|
||||
}
|
||||
if conf.ObjectStorage != nil {
|
||||
bootstrapConfig.ObjectStorage = confObjectStorageSnapshot{RetainInDays: conf.ObjectStorage.RetainInDays, TempDir: conf.ObjectStorage.TempDir}
|
||||
}
|
||||
if conf.AliOSSSetting != nil {
|
||||
bootstrapConfig.AliOSS = confAliOSSSnapshot{Endpoint: conf.AliOSSSetting.Endpoint, AccessKeyID: conf.AliOSSSetting.AccessKeyID, AccessKeySecret: conf.AliOSSSetting.AccessKeySecret, Bucket: conf.AliOSSSetting.Bucket, Domain: conf.AliOSSSetting.Domain}
|
||||
}
|
||||
if conf.COSSetting != nil {
|
||||
bootstrapConfig.COS = confCOSSnapshot{SecretID: conf.COSSetting.SecretID, SecretKey: conf.COSSetting.SecretKey, Region: conf.COSSetting.Region, Bucket: conf.COSSetting.Bucket, Domain: conf.COSSetting.Domain}
|
||||
}
|
||||
if conf.HuaweiOBSSetting != nil {
|
||||
bootstrapConfig.HuaweiOBS = confHuaweiOBSSnapshot{AccessKey: conf.HuaweiOBSSetting.AccessKey, SecretKey: conf.HuaweiOBSSetting.SecretKey, Endpoint: conf.HuaweiOBSSetting.Endpoint, Bucket: conf.HuaweiOBSSetting.Bucket, Domain: conf.HuaweiOBSSetting.Domain}
|
||||
}
|
||||
if conf.MinIOSetting != nil {
|
||||
bootstrapConfig.MinIO = confMinIOSnapshot{AccessKey: conf.MinIOSetting.AccessKey, SecretKey: conf.MinIOSetting.SecretKey, Secure: conf.MinIOSetting.Secure, Endpoint: conf.MinIOSetting.Endpoint, Bucket: conf.MinIOSetting.Bucket, Domain: conf.MinIOSetting.Domain}
|
||||
}
|
||||
if conf.S3Setting != nil {
|
||||
bootstrapConfig.S3 = confS3Snapshot{AccessKey: conf.S3Setting.AccessKey, SecretKey: conf.S3Setting.SecretKey, Secure: conf.S3Setting.Secure, Endpoint: conf.S3Setting.Endpoint, Bucket: conf.S3Setting.Bucket, Domain: conf.S3Setting.Domain}
|
||||
}
|
||||
if conf.LocalOSSSetting != nil {
|
||||
bootstrapConfig.LocalOSS = confLocalOSSSnapshot{SavePath: conf.LocalOSSSetting.SavePath, Secure: conf.LocalOSSSetting.Secure, Bucket: conf.LocalOSSSetting.Bucket, Domain: conf.LocalOSSSetting.Domain}
|
||||
}
|
||||
if conf.SmsJuheSetting != nil {
|
||||
bootstrapConfig.SmsJuhe = confSmsJuheSnapshot{Gateway: conf.SmsJuheSetting.Gateway, Key: conf.SmsJuheSetting.Key, TplID: conf.SmsJuheSetting.TplID, TplVal: conf.SmsJuheSetting.TplVal}
|
||||
}
|
||||
if conf.AlipaySetting != nil {
|
||||
bootstrapConfig.Alipay = confAlipaySnapshot{AppID: conf.AlipaySetting.AppID, PrivateKey: conf.AlipaySetting.PrivateKey, RootCertFile: conf.AlipaySetting.RootCertFile, PublicCertFile: conf.AlipaySetting.PublicCertFile, AppPublicCertFile: conf.AlipaySetting.AppPublicCertFile, InProduction: conf.AlipaySetting.InProduction}
|
||||
}
|
||||
if conf.WebProfileSetting != nil {
|
||||
bootstrapConfig.WebProfile = confWebProfileSnapshot{
|
||||
UseFriendship: conf.WebProfileSetting.UseFriendship,
|
||||
EnableTrendsBar: conf.WebProfileSetting.EnableTrendsBar,
|
||||
EnableWallet: conf.WebProfileSetting.EnableWallet,
|
||||
AllowTweetAttachment: conf.WebProfileSetting.AllowTweetAttachment,
|
||||
AllowTweetAttachmentPrice: conf.WebProfileSetting.AllowTweetAttachmentPrice,
|
||||
AllowTweetVideo: conf.WebProfileSetting.AllowTweetVideo,
|
||||
AllowUserRegister: conf.WebProfileSetting.AllowUserRegister,
|
||||
AllowPhoneBind: conf.WebProfileSetting.AllowPhoneBind,
|
||||
DefaultTweetMaxLength: conf.WebProfileSetting.DefaultTweetMaxLength,
|
||||
TweetWebEllipsisSize: conf.WebProfileSetting.TweetWebEllipsisSize,
|
||||
TweetMobileEllipsisSize: conf.WebProfileSetting.TweetMobileEllipsisSize,
|
||||
DefaultTweetVisibility: conf.WebProfileSetting.DefaultTweetVisibility,
|
||||
DefaultMsgLoopInterval: conf.WebProfileSetting.DefaultMsgLoopInterval,
|
||||
CopyrightTop: conf.WebProfileSetting.CopyrightTop,
|
||||
CopyrightLeft: conf.WebProfileSetting.CopyrightLeft,
|
||||
CopyrightLeftLink: conf.WebProfileSetting.CopyrightLeftLink,
|
||||
CopyrightRight: conf.WebProfileSetting.CopyrightRight,
|
||||
CopyrightRightLink: conf.WebProfileSetting.CopyrightRightLink,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func CloneReadonlyFields() []string {
|
||||
res := make([]string, len(ReadonlyFields))
|
||||
copy(res, ReadonlyFields)
|
||||
return res
|
||||
}
|
||||
|
||||
func Registry() []Definition {
|
||||
ensureBootstrapSnapshot()
|
||||
return []Definition{
|
||||
boolDef("web_profile.use_friendship", "web", "profile", "Use friendship", "Switch the frontend friendship model.", ApplyModeLive, false, true, func() any { return conf.WebProfileSetting.UseFriendship }, func() any { return bootstrapConfig.WebProfile.UseFriendship }, func(v any) { conf.WebProfileSetting.UseFriendship = v.(bool) }),
|
||||
boolDef("web_profile.enable_trends_bar", "web", "profile", "Enable trends bar", "Show the trends sidebar in the web UI.", ApplyModeLive, false, true, func() any { return conf.WebProfileSetting.EnableTrendsBar }, func() any { return bootstrapConfig.WebProfile.EnableTrendsBar }, func(v any) { conf.WebProfileSetting.EnableTrendsBar = v.(bool) }),
|
||||
boolDef("web_profile.enable_wallet", "web", "profile", "Enable wallet", "Enable wallet-related frontend features.", ApplyModeLive, false, true, func() any { return conf.WebProfileSetting.EnableWallet }, func() any { return bootstrapConfig.WebProfile.EnableWallet }, func(v any) { conf.WebProfileSetting.EnableWallet = v.(bool) }),
|
||||
boolDef("web_profile.allow_tweet_attachment", "web", "profile", "Allow attachments", "Allow file attachments on posts.", ApplyModeLive, false, true, func() any { return conf.WebProfileSetting.AllowTweetAttachment }, func() any { return bootstrapConfig.WebProfile.AllowTweetAttachment }, func(v any) { conf.WebProfileSetting.AllowTweetAttachment = v.(bool) }),
|
||||
boolDef("web_profile.allow_tweet_attachment_price", "web", "profile", "Allow paid attachments", "Allow post attachments to have a price.", ApplyModeLive, false, true, func() any { return conf.WebProfileSetting.AllowTweetAttachmentPrice }, func() any { return bootstrapConfig.WebProfile.AllowTweetAttachmentPrice }, func(v any) { conf.WebProfileSetting.AllowTweetAttachmentPrice = v.(bool) }),
|
||||
boolDef("web_profile.allow_tweet_video", "web", "profile", "Allow video posts", "Allow video uploads on posts.", ApplyModeLive, false, true, func() any { return conf.WebProfileSetting.AllowTweetVideo }, func() any { return bootstrapConfig.WebProfile.AllowTweetVideo }, func(v any) { conf.WebProfileSetting.AllowTweetVideo = v.(bool) }),
|
||||
boolDef("web_profile.allow_user_register", "web", "profile", "Allow user registration", "Bootstrap-only registration gate from YAML/features.", ApplyModeBootstrapOnly, false, false, func() any { return conf.WebProfileSetting.AllowUserRegister }, func() any { return bootstrapConfig.WebProfile.AllowUserRegister }, nil),
|
||||
boolDef("web_profile.allow_phone_bind", "web", "profile", "Allow phone binding", "Bootstrap-only phone binding gate from YAML/features.", ApplyModeBootstrapOnly, false, false, func() any { return conf.WebProfileSetting.AllowPhoneBind }, func() any { return bootstrapConfig.WebProfile.AllowPhoneBind }, nil),
|
||||
intDef("web_profile.default_tweet_max_length", "web", "profile", "Default tweet max length", "Maximum allowed tweet length.", ApplyModeLive, false, true, func() any { return conf.WebProfileSetting.DefaultTweetMaxLength }, func() any { return bootstrapConfig.WebProfile.DefaultTweetMaxLength }, func(v int) error { return between(v, 1, 2000, "default_tweet_max_length") }, func(v any) { conf.WebProfileSetting.DefaultTweetMaxLength = v.(int) }),
|
||||
intDef("web_profile.tweet_web_ellipsis_size", "web", "profile", "Web ellipsis size", "Truncated feed length on web.", ApplyModeLive, false, true, func() any { return conf.WebProfileSetting.TweetWebEllipsisSize }, func() any { return bootstrapConfig.WebProfile.TweetWebEllipsisSize }, func(v int) error {
|
||||
return between(v, 1, conf.WebProfileSetting.DefaultTweetMaxLength, "tweet_web_ellipsis_size")
|
||||
}, func(v any) { conf.WebProfileSetting.TweetWebEllipsisSize = v.(int) }),
|
||||
intDef("web_profile.tweet_mobile_ellipsis_size", "web", "profile", "Mobile ellipsis size", "Truncated feed length on mobile.", ApplyModeLive, false, true, func() any { return conf.WebProfileSetting.TweetMobileEllipsisSize }, func() any { return bootstrapConfig.WebProfile.TweetMobileEllipsisSize }, func(v int) error {
|
||||
return between(v, 1, conf.WebProfileSetting.DefaultTweetMaxLength, "tweet_mobile_ellipsis_size")
|
||||
}, func(v any) { conf.WebProfileSetting.TweetMobileEllipsisSize = v.(int) }),
|
||||
stringDef("web_profile.default_tweet_visibility", "web", "profile", "Default tweet visibility", "Default visibility for new posts.", ApplyModeLive, false, true, visibilityOption, func() any { return conf.WebProfileSetting.DefaultTweetVisibility }, func() any { return bootstrapConfig.WebProfile.DefaultTweetVisibility }, validateVisibility, func(v any) { conf.WebProfileSetting.DefaultTweetVisibility = v.(string) }),
|
||||
intDef("web_profile.default_msg_loop_interval", "web", "profile", "Message polling interval", "Polling interval for unread messages in milliseconds.", ApplyModeLive, false, true, func() any { return conf.WebProfileSetting.DefaultMsgLoopInterval }, func() any { return bootstrapConfig.WebProfile.DefaultMsgLoopInterval }, func(v int) error { return between(v, 1000, 60000, "default_msg_loop_interval") }, func(v any) { conf.WebProfileSetting.DefaultMsgLoopInterval = v.(int) }),
|
||||
stringDef("web_profile.copyright_top", "web", "profile", "Copyright top", "Top copyright line displayed in the UI.", ApplyModeLive, false, true, nil, func() any { return conf.WebProfileSetting.CopyrightTop }, func() any { return bootstrapConfig.WebProfile.CopyrightTop }, validateRequiredTrimmed("copyright_top", 255), func(v any) { conf.WebProfileSetting.CopyrightTop = v.(string) }),
|
||||
stringDef("web_profile.copyright_left", "web", "profile", "Copyright left", "Left footer label.", ApplyModeLive, false, true, nil, func() any { return conf.WebProfileSetting.CopyrightLeft }, func() any { return bootstrapConfig.WebProfile.CopyrightLeft }, validateRequiredTrimmed("copyright_left", 255), func(v any) { conf.WebProfileSetting.CopyrightLeft = v.(string) }),
|
||||
stringDef("web_profile.copyright_left_link", "web", "profile", "Copyright left link", "Optional left footer link.", ApplyModeLive, false, true, nil, func() any { return conf.WebProfileSetting.CopyrightLeftLink }, func() any { return bootstrapConfig.WebProfile.CopyrightLeftLink }, validateOptionalURL("copyright_left_link", 255), func(v any) { conf.WebProfileSetting.CopyrightLeftLink = v.(string) }),
|
||||
stringDef("web_profile.copyright_right", "web", "profile", "Copyright right", "Right footer label.", ApplyModeLive, false, true, nil, func() any { return conf.WebProfileSetting.CopyrightRight }, func() any { return bootstrapConfig.WebProfile.CopyrightRight }, validateRequiredTrimmed("copyright_right", 255), func(v any) { conf.WebProfileSetting.CopyrightRight = v.(string) }),
|
||||
stringDef("web_profile.copyright_right_link", "web", "profile", "Copyright right link", "Optional right footer link.", ApplyModeLive, false, true, nil, func() any { return conf.WebProfileSetting.CopyrightRightLink }, func() any { return bootstrapConfig.WebProfile.CopyrightRightLink }, validateOptionalURL("copyright_right_link", 255), func(v any) { conf.WebProfileSetting.CopyrightRightLink = v.(string) }),
|
||||
|
||||
int64Def("app.max_comment_count", "app", "general", "Max comment count", "Maximum comments allowed on a post.", ApplyModeLive, func() any { return conf.AppSetting.MaxCommentCount }, func() any { return bootstrapConfig.App.MaxCommentCount }, func(v int64) error { return betweenInt64(v, 1, 100000, "max_comment_count") }, func(v any) { conf.AppSetting.MaxCommentCount = v.(int64) }),
|
||||
floatDef("app.attachment_income_rate", "app", "general", "Attachment income rate", "Revenue share for paid attachments.", ApplyModeLive, func() any { return conf.AppSetting.AttachmentIncomeRate }, func() any { return bootstrapConfig.App.AttachmentIncomeRate }, func(v float64) error {
|
||||
if v < 0 || v > 1 {
|
||||
return xerror.InvalidParams.WithDetails("attachment_income_rate must be between 0 and 1")
|
||||
}
|
||||
return nil
|
||||
}, func(v any) { conf.AppSetting.AttachmentIncomeRate = v.(float64) }),
|
||||
intDef("app.default_page_size", "app", "general", "Default page size", "Default pagination size.", ApplyModeLive, false, true, func() any { return conf.AppSetting.DefaultPageSize }, func() any { return bootstrapConfig.App.DefaultPageSize }, func(v int) error { return between(v, 1, conf.AppSetting.MaxPageSize, "default_page_size") }, func(v any) { conf.AppSetting.DefaultPageSize = v.(int) }),
|
||||
intDef("app.max_page_size", "app", "general", "Max page size", "Maximum pagination size.", ApplyModeLive, false, true, func() any { return conf.AppSetting.MaxPageSize }, func() any { return bootstrapConfig.App.MaxPageSize }, func(v int) error {
|
||||
return between(v, maxInt(conf.AppSetting.DefaultPageSize, 1), 1000, "max_page_size")
|
||||
}, func(v any) { conf.AppSetting.MaxPageSize = v.(int) }),
|
||||
int64Def("app.max_whisper_daily", "app", "limits", "Max daily whispers", "Daily whisper send limit. Restart required because servants cache it at startup.", ApplyModeRestartRequired, func() any { return conf.AppSetting.MaxWhisperDaily }, func() any { return bootstrapConfig.App.MaxWhisperDaily }, func(v int64) error { return betweenInt64(v, 1, 1000000, "max_whisper_daily") }, func(v any) { conf.AppSetting.MaxWhisperDaily = v.(int64) }),
|
||||
intDef("app.max_captcha_times", "app", "limits", "Max captcha times", "Max captcha request count cached at startup by web servants.", ApplyModeRestartRequired, false, true, func() any { return conf.AppSetting.MaxCaptchaTimes }, func() any { return bootstrapConfig.App.MaxCaptchaTimes }, func(v int) error { return between(v, 1, 1000, "max_captcha_times") }, func(v any) { conf.AppSetting.MaxCaptchaTimes = v.(int) }),
|
||||
|
||||
intDef("tweet_search.max_update_qps", "search", "bridge", "Search update QPS", "Buffered update throughput for search indexing.", ApplyModeRestartRequired, false, true, func() any { return conf.TweetSearchSetting.MaxUpdateQPS }, func() any { return bootstrapConfig.TweetSearch.MaxUpdateQPS }, func(v int) error { return between(v, 10, 10000, "max_update_qps") }, func(v any) { conf.TweetSearchSetting.MaxUpdateQPS = v.(int) }),
|
||||
intDef("tweet_search.min_worker", "search", "bridge", "Search worker count", "Background worker count for search indexing.", ApplyModeRestartRequired, false, true, func() any { return conf.TweetSearchSetting.MinWorker }, func() any { return bootstrapConfig.TweetSearch.MinWorker }, func(v int) error { return between(v, 5, 1000, "min_worker") }, func(v any) { conf.TweetSearchSetting.MinWorker = v.(int) }),
|
||||
|
||||
stringDefWithActive("meili.host", "search", "meili", "Meili host", "Meilisearch host.", ApplyModeRestartRequired, false, true, nil, func() bool { return cfg.If("Meili") }, func() any { return conf.MeiliSetting.Host }, func() any { return bootstrapConfig.Meili.Host }, validateTrimmedMax("meili.host", 255), func(v any) { conf.MeiliSetting.Host = v.(string) }),
|
||||
stringDefWithActive("meili.index", "search", "meili", "Meili index", "Meilisearch index name.", ApplyModeRestartRequired, false, true, nil, func() bool { return cfg.If("Meili") }, func() any { return conf.MeiliSetting.Index }, func() any { return bootstrapConfig.Meili.Index }, validateTrimmedMax("meili.index", 255), func(v any) { conf.MeiliSetting.Index = v.(string) }),
|
||||
stringDefWithActive("meili.api_key", "search", "meili", "Meili API key", "Meilisearch API key.", ApplyModeRestartRequired, true, true, nil, func() bool { return cfg.If("Meili") }, func() any { return conf.MeiliSetting.ApiKey }, func() any { return bootstrapConfig.Meili.ApiKey }, validateTrimmedMax("meili.api_key", 512), func(v any) { conf.MeiliSetting.ApiKey = v.(string) }),
|
||||
boolDefWithActive("meili.secure", "search", "meili", "Meili secure", "Use HTTPS for Meilisearch.", ApplyModeRestartRequired, false, true, func() bool { return cfg.If("Meili") }, func() any { return conf.MeiliSetting.Secure }, func() any { return bootstrapConfig.Meili.Secure }, func(v any) { conf.MeiliSetting.Secure = v.(bool) }),
|
||||
|
||||
stringDefWithActive("zinc.host", "search", "zinc", "Zinc host", "Zinc host.", ApplyModeRestartRequired, false, true, nil, func() bool { return cfg.If("Zinc") }, func() any { return conf.ZincSetting.Host }, func() any { return bootstrapConfig.Zinc.Host }, validateTrimmedMax("zinc.host", 255), func(v any) { conf.ZincSetting.Host = v.(string) }),
|
||||
stringDefWithActive("zinc.index", "search", "zinc", "Zinc index", "Zinc index name.", ApplyModeRestartRequired, false, true, nil, func() bool { return cfg.If("Zinc") }, func() any { return conf.ZincSetting.Index }, func() any { return bootstrapConfig.Zinc.Index }, validateTrimmedMax("zinc.index", 255), func(v any) { conf.ZincSetting.Index = v.(string) }),
|
||||
stringDefWithActive("zinc.user", "search", "zinc", "Zinc user", "Zinc username.", ApplyModeRestartRequired, false, true, nil, func() bool { return cfg.If("Zinc") }, func() any { return conf.ZincSetting.User }, func() any { return bootstrapConfig.Zinc.User }, validateTrimmedMax("zinc.user", 255), func(v any) { conf.ZincSetting.User = v.(string) }),
|
||||
stringDefWithActive("zinc.password", "search", "zinc", "Zinc password", "Zinc password.", ApplyModeRestartRequired, true, true, nil, func() bool { return cfg.If("Zinc") }, func() any { return conf.ZincSetting.Password }, func() any { return bootstrapConfig.Zinc.Password }, validateTrimmedMax("zinc.password", 512), func(v any) { conf.ZincSetting.Password = v.(string) }),
|
||||
boolDefWithActive("zinc.secure", "search", "zinc", "Zinc secure", "Use HTTPS for Zinc.", ApplyModeRestartRequired, false, true, func() bool { return cfg.If("Zinc") }, func() any { return conf.ZincSetting.Secure }, func() any { return bootstrapConfig.Zinc.Secure }, func(v any) { conf.ZincSetting.Secure = v.(bool) }),
|
||||
|
||||
intDef("object_storage.retain_in_days", "storage", "common", "Object retention days", "Retention window for temporary objects.", ApplyModeRestartRequired, false, true, func() any { return conf.ObjectStorage.RetainInDays }, func() any { return bootstrapConfig.ObjectStorage.RetainInDays }, func(v int) error { return between(v, 0, 3650, "retain_in_days") }, func(v any) { conf.ObjectStorage.RetainInDays = v.(int) }),
|
||||
stringDef("object_storage.temp_dir", "storage", "common", "Object temp dir", "Temporary directory/prefix for objects.", ApplyModeRestartRequired, false, true, nil, func() any { return conf.ObjectStorage.TempDir }, func() any { return bootstrapConfig.ObjectStorage.TempDir }, validateTrimmedMax("temp_dir", 255), func(v any) { conf.ObjectStorage.TempDir = v.(string) }),
|
||||
|
||||
stringDefWithActive("local_oss.save_path", "storage", "local_oss", "Local OSS save path", "Filesystem path for local object storage.", ApplyModeRestartRequired, false, true, nil, func() bool { return cfg.If("LocalOSS") }, func() any { return conf.LocalOSSSetting.SavePath }, func() any { return bootstrapConfig.LocalOSS.SavePath }, validateTrimmedMax("local_oss.save_path", 1024), func(v any) { conf.LocalOSSSetting.SavePath = v.(string) }),
|
||||
boolDefWithActive("local_oss.secure", "storage", "local_oss", "Local OSS secure", "Generate HTTPS URLs for local object storage.", ApplyModeRestartRequired, false, true, func() bool { return cfg.If("LocalOSS") }, func() any { return conf.LocalOSSSetting.Secure }, func() any { return bootstrapConfig.LocalOSS.Secure }, func(v any) { conf.LocalOSSSetting.Secure = v.(bool) }),
|
||||
stringDefWithActive("local_oss.bucket", "storage", "local_oss", "Local OSS bucket", "Bucket folder name for local storage.", ApplyModeRestartRequired, false, true, nil, func() bool { return cfg.If("LocalOSS") }, func() any { return conf.LocalOSSSetting.Bucket }, func() any { return bootstrapConfig.LocalOSS.Bucket }, validateTrimmedMax("local_oss.bucket", 255), func(v any) { conf.LocalOSSSetting.Bucket = v.(string) }),
|
||||
stringDefWithActive("local_oss.domain", "storage", "local_oss", "Local OSS domain", "Public domain for local object storage.", ApplyModeRestartRequired, false, true, nil, func() bool { return cfg.If("LocalOSS") }, func() any { return conf.LocalOSSSetting.Domain }, func() any { return bootstrapConfig.LocalOSS.Domain }, validateTrimmedMax("local_oss.domain", 255), func(v any) { conf.LocalOSSSetting.Domain = v.(string) }),
|
||||
|
||||
stringDefWithActive("minio.access_key", "storage", "minio", "MinIO access key", "MinIO access key.", ApplyModeRestartRequired, false, true, nil, func() bool { return cfg.If("MinIO") }, func() any { return conf.MinIOSetting.AccessKey }, func() any { return bootstrapConfig.MinIO.AccessKey }, validateTrimmedMax("minio.access_key", 255), func(v any) { conf.MinIOSetting.AccessKey = v.(string) }),
|
||||
stringDefWithActive("minio.secret_key", "storage", "minio", "MinIO secret key", "MinIO secret key.", ApplyModeRestartRequired, true, true, nil, func() bool { return cfg.If("MinIO") }, func() any { return conf.MinIOSetting.SecretKey }, func() any { return bootstrapConfig.MinIO.SecretKey }, validateTrimmedMax("minio.secret_key", 512), func(v any) { conf.MinIOSetting.SecretKey = v.(string) }),
|
||||
boolDefWithActive("minio.secure", "storage", "minio", "MinIO secure", "Use HTTPS for MinIO.", ApplyModeRestartRequired, false, true, func() bool { return cfg.If("MinIO") }, func() any { return conf.MinIOSetting.Secure }, func() any { return bootstrapConfig.MinIO.Secure }, func(v any) { conf.MinIOSetting.Secure = v.(bool) }),
|
||||
stringDefWithActive("minio.endpoint", "storage", "minio", "MinIO endpoint", "MinIO endpoint.", ApplyModeRestartRequired, false, true, nil, func() bool { return cfg.If("MinIO") }, func() any { return conf.MinIOSetting.Endpoint }, func() any { return bootstrapConfig.MinIO.Endpoint }, validateTrimmedMax("minio.endpoint", 255), func(v any) { conf.MinIOSetting.Endpoint = v.(string) }),
|
||||
stringDefWithActive("minio.bucket", "storage", "minio", "MinIO bucket", "MinIO bucket name.", ApplyModeRestartRequired, false, true, nil, func() bool { return cfg.If("MinIO") }, func() any { return conf.MinIOSetting.Bucket }, func() any { return bootstrapConfig.MinIO.Bucket }, validateTrimmedMax("minio.bucket", 255), func(v any) { conf.MinIOSetting.Bucket = v.(string) }),
|
||||
stringDefWithActive("minio.domain", "storage", "minio", "MinIO domain", "MinIO public domain.", ApplyModeRestartRequired, false, true, nil, func() bool { return cfg.If("MinIO") }, func() any { return conf.MinIOSetting.Domain }, func() any { return bootstrapConfig.MinIO.Domain }, validateTrimmedMax("minio.domain", 255), func(v any) { conf.MinIOSetting.Domain = v.(string) }),
|
||||
|
||||
stringDefWithActive("s3.access_key", "storage", "s3", "S3 access key", "Amazon S3 access key.", ApplyModeRestartRequired, false, true, nil, func() bool { return cfg.If("S3") }, func() any { return conf.S3Setting.AccessKey }, func() any { return bootstrapConfig.S3.AccessKey }, validateTrimmedMax("s3.access_key", 255), func(v any) { conf.S3Setting.AccessKey = v.(string) }),
|
||||
stringDefWithActive("s3.secret_key", "storage", "s3", "S3 secret key", "Amazon S3 secret key.", ApplyModeRestartRequired, true, true, nil, func() bool { return cfg.If("S3") }, func() any { return conf.S3Setting.SecretKey }, func() any { return bootstrapConfig.S3.SecretKey }, validateTrimmedMax("s3.secret_key", 512), func(v any) { conf.S3Setting.SecretKey = v.(string) }),
|
||||
boolDefWithActive("s3.secure", "storage", "s3", "S3 secure", "Use HTTPS for S3.", ApplyModeRestartRequired, false, true, func() bool { return cfg.If("S3") }, func() any { return conf.S3Setting.Secure }, func() any { return bootstrapConfig.S3.Secure }, func(v any) { conf.S3Setting.Secure = v.(bool) }),
|
||||
stringDefWithActive("s3.endpoint", "storage", "s3", "S3 endpoint", "Amazon S3 endpoint.", ApplyModeRestartRequired, false, true, nil, func() bool { return cfg.If("S3") }, func() any { return conf.S3Setting.Endpoint }, func() any { return bootstrapConfig.S3.Endpoint }, validateTrimmedMax("s3.endpoint", 255), func(v any) { conf.S3Setting.Endpoint = v.(string) }),
|
||||
stringDefWithActive("s3.bucket", "storage", "s3", "S3 bucket", "Amazon S3 bucket name.", ApplyModeRestartRequired, false, true, nil, func() bool { return cfg.If("S3") }, func() any { return conf.S3Setting.Bucket }, func() any { return bootstrapConfig.S3.Bucket }, validateTrimmedMax("s3.bucket", 255), func(v any) { conf.S3Setting.Bucket = v.(string) }),
|
||||
stringDefWithActive("s3.domain", "storage", "s3", "S3 domain", "Amazon S3 public domain.", ApplyModeRestartRequired, false, true, nil, func() bool { return cfg.If("S3") }, func() any { return conf.S3Setting.Domain }, func() any { return bootstrapConfig.S3.Domain }, validateTrimmedMax("s3.domain", 255), func(v any) { conf.S3Setting.Domain = v.(string) }),
|
||||
|
||||
stringDefWithActive("alioss.endpoint", "storage", "alioss", "AliOSS endpoint", "AliOSS endpoint.", ApplyModeRestartRequired, false, true, nil, func() bool { return cfg.If("AliOSS") }, func() any { return conf.AliOSSSetting.Endpoint }, func() any { return bootstrapConfig.AliOSS.Endpoint }, validateTrimmedMax("alioss.endpoint", 255), func(v any) { conf.AliOSSSetting.Endpoint = v.(string) }),
|
||||
stringDefWithActive("alioss.access_key_id", "storage", "alioss", "AliOSS access key ID", "AliOSS access key ID.", ApplyModeRestartRequired, false, true, nil, func() bool { return cfg.If("AliOSS") }, func() any { return conf.AliOSSSetting.AccessKeyID }, func() any { return bootstrapConfig.AliOSS.AccessKeyID }, validateTrimmedMax("alioss.access_key_id", 255), func(v any) { conf.AliOSSSetting.AccessKeyID = v.(string) }),
|
||||
stringDefWithActive("alioss.access_key_secret", "storage", "alioss", "AliOSS access key secret", "AliOSS secret key.", ApplyModeRestartRequired, true, true, nil, func() bool { return cfg.If("AliOSS") }, func() any { return conf.AliOSSSetting.AccessKeySecret }, func() any { return bootstrapConfig.AliOSS.AccessKeySecret }, validateTrimmedMax("alioss.access_key_secret", 512), func(v any) { conf.AliOSSSetting.AccessKeySecret = v.(string) }),
|
||||
stringDefWithActive("alioss.bucket", "storage", "alioss", "AliOSS bucket", "AliOSS bucket name.", ApplyModeRestartRequired, false, true, nil, func() bool { return cfg.If("AliOSS") }, func() any { return conf.AliOSSSetting.Bucket }, func() any { return bootstrapConfig.AliOSS.Bucket }, validateTrimmedMax("alioss.bucket", 255), func(v any) { conf.AliOSSSetting.Bucket = v.(string) }),
|
||||
stringDefWithActive("alioss.domain", "storage", "alioss", "AliOSS domain", "AliOSS public domain.", ApplyModeRestartRequired, false, true, nil, func() bool { return cfg.If("AliOSS") }, func() any { return conf.AliOSSSetting.Domain }, func() any { return bootstrapConfig.AliOSS.Domain }, validateTrimmedMax("alioss.domain", 255), func(v any) { conf.AliOSSSetting.Domain = v.(string) }),
|
||||
|
||||
stringDefWithActive("cos.secret_id", "storage", "cos", "COS secret ID", "Tencent COS secret ID.", ApplyModeRestartRequired, false, true, nil, func() bool { return cfg.If("COS") }, func() any { return conf.COSSetting.SecretID }, func() any { return bootstrapConfig.COS.SecretID }, validateTrimmedMax("cos.secret_id", 255), func(v any) { conf.COSSetting.SecretID = v.(string) }),
|
||||
stringDefWithActive("cos.secret_key", "storage", "cos", "COS secret key", "Tencent COS secret key.", ApplyModeRestartRequired, true, true, nil, func() bool { return cfg.If("COS") }, func() any { return conf.COSSetting.SecretKey }, func() any { return bootstrapConfig.COS.SecretKey }, validateTrimmedMax("cos.secret_key", 512), func(v any) { conf.COSSetting.SecretKey = v.(string) }),
|
||||
stringDefWithActive("cos.region", "storage", "cos", "COS region", "Tencent COS region.", ApplyModeRestartRequired, false, true, nil, func() bool { return cfg.If("COS") }, func() any { return conf.COSSetting.Region }, func() any { return bootstrapConfig.COS.Region }, validateTrimmedMax("cos.region", 255), func(v any) { conf.COSSetting.Region = v.(string) }),
|
||||
stringDefWithActive("cos.bucket", "storage", "cos", "COS bucket", "Tencent COS bucket.", ApplyModeRestartRequired, false, true, nil, func() bool { return cfg.If("COS") }, func() any { return conf.COSSetting.Bucket }, func() any { return bootstrapConfig.COS.Bucket }, validateTrimmedMax("cos.bucket", 255), func(v any) { conf.COSSetting.Bucket = v.(string) }),
|
||||
stringDefWithActive("cos.domain", "storage", "cos", "COS domain", "Tencent COS public domain.", ApplyModeRestartRequired, false, true, nil, func() bool { return cfg.If("COS") }, func() any { return conf.COSSetting.Domain }, func() any { return bootstrapConfig.COS.Domain }, validateTrimmedMax("cos.domain", 255), func(v any) { conf.COSSetting.Domain = v.(string) }),
|
||||
|
||||
stringDefWithActive("huawei_obs.access_key", "storage", "huawei_obs", "Huawei OBS access key", "Huawei OBS access key.", ApplyModeRestartRequired, false, true, nil, func() bool { return cfg.If("HuaweiOBS") }, func() any { return conf.HuaweiOBSSetting.AccessKey }, func() any { return bootstrapConfig.HuaweiOBS.AccessKey }, validateTrimmedMax("huawei_obs.access_key", 255), func(v any) { conf.HuaweiOBSSetting.AccessKey = v.(string) }),
|
||||
stringDefWithActive("huawei_obs.secret_key", "storage", "huawei_obs", "Huawei OBS secret key", "Huawei OBS secret key.", ApplyModeRestartRequired, true, true, nil, func() bool { return cfg.If("HuaweiOBS") }, func() any { return conf.HuaweiOBSSetting.SecretKey }, func() any { return bootstrapConfig.HuaweiOBS.SecretKey }, validateTrimmedMax("huawei_obs.secret_key", 512), func(v any) { conf.HuaweiOBSSetting.SecretKey = v.(string) }),
|
||||
stringDefWithActive("huawei_obs.endpoint", "storage", "huawei_obs", "Huawei OBS endpoint", "Huawei OBS endpoint.", ApplyModeRestartRequired, false, true, nil, func() bool { return cfg.If("HuaweiOBS") }, func() any { return conf.HuaweiOBSSetting.Endpoint }, func() any { return bootstrapConfig.HuaweiOBS.Endpoint }, validateTrimmedMax("huawei_obs.endpoint", 255), func(v any) { conf.HuaweiOBSSetting.Endpoint = v.(string) }),
|
||||
stringDefWithActive("huawei_obs.bucket", "storage", "huawei_obs", "Huawei OBS bucket", "Huawei OBS bucket.", ApplyModeRestartRequired, false, true, nil, func() bool { return cfg.If("HuaweiOBS") }, func() any { return conf.HuaweiOBSSetting.Bucket }, func() any { return bootstrapConfig.HuaweiOBS.Bucket }, validateTrimmedMax("huawei_obs.bucket", 255), func(v any) { conf.HuaweiOBSSetting.Bucket = v.(string) }),
|
||||
stringDefWithActive("huawei_obs.domain", "storage", "huawei_obs", "Huawei OBS domain", "Huawei OBS public domain.", ApplyModeRestartRequired, false, true, nil, func() bool { return cfg.If("HuaweiOBS") }, func() any { return conf.HuaweiOBSSetting.Domain }, func() any { return bootstrapConfig.HuaweiOBS.Domain }, validateTrimmedMax("huawei_obs.domain", 255), func(v any) { conf.HuaweiOBSSetting.Domain = v.(string) }),
|
||||
|
||||
stringDefWithActive("sms_juhe.gateway", "notifications", "sms_juhe", "SMS gateway", "Juhe SMS gateway URL.", ApplyModeRestartRequired, false, true, nil, func() bool { return cfg.If("Sms") }, func() any { return conf.SmsJuheSetting.Gateway }, func() any { return bootstrapConfig.SmsJuhe.Gateway }, validateTrimmedMax("sms_juhe.gateway", 255), func(v any) { conf.SmsJuheSetting.Gateway = v.(string) }),
|
||||
stringDefWithActive("sms_juhe.key", "notifications", "sms_juhe", "SMS key", "Juhe SMS key.", ApplyModeRestartRequired, true, true, nil, func() bool { return cfg.If("Sms") }, func() any { return conf.SmsJuheSetting.Key }, func() any { return bootstrapConfig.SmsJuhe.Key }, validateTrimmedMax("sms_juhe.key", 255), func(v any) { conf.SmsJuheSetting.Key = v.(string) }),
|
||||
stringDefWithActive("sms_juhe.tpl_id", "notifications", "sms_juhe", "SMS template ID", "Juhe SMS template ID.", ApplyModeRestartRequired, false, true, nil, func() bool { return cfg.If("Sms") }, func() any { return conf.SmsJuheSetting.TplID }, func() any { return bootstrapConfig.SmsJuhe.TplID }, validateTrimmedMax("sms_juhe.tpl_id", 255), func(v any) { conf.SmsJuheSetting.TplID = v.(string) }),
|
||||
stringDefWithActive("sms_juhe.tpl_val", "notifications", "sms_juhe", "SMS template value", "Juhe SMS template value format.", ApplyModeRestartRequired, false, true, nil, func() bool { return cfg.If("Sms") }, func() any { return conf.SmsJuheSetting.TplVal }, func() any { return bootstrapConfig.SmsJuhe.TplVal }, validateTrimmedMax("sms_juhe.tpl_val", 255), func(v any) { conf.SmsJuheSetting.TplVal = v.(string) }),
|
||||
|
||||
stringDefWithActive("alipay.app_id", "payments", "alipay", "Alipay app ID", "Alipay application ID.", ApplyModeRestartRequired, false, true, nil, func() bool { return cfg.If("Alipay") }, func() any { return conf.AlipaySetting.AppID }, func() any { return bootstrapConfig.Alipay.AppID }, validateTrimmedMax("alipay.app_id", 255), func(v any) { conf.AlipaySetting.AppID = v.(string) }),
|
||||
stringDefWithActive("alipay.private_key", "payments", "alipay", "Alipay private key", "Alipay private key PEM content.", ApplyModeRestartRequired, true, true, nil, func() bool { return cfg.If("Alipay") }, func() any { return conf.AlipaySetting.PrivateKey }, func() any { return bootstrapConfig.Alipay.PrivateKey }, validateTrimmedMax("alipay.private_key", 8192), func(v any) { conf.AlipaySetting.PrivateKey = v.(string) }),
|
||||
stringDefWithActive("alipay.root_cert_file", "payments", "alipay", "Alipay root cert file", "Path to the Alipay root certificate file.", ApplyModeRestartRequired, false, true, nil, func() bool { return cfg.If("Alipay") }, func() any { return conf.AlipaySetting.RootCertFile }, func() any { return bootstrapConfig.Alipay.RootCertFile }, validateTrimmedMax("alipay.root_cert_file", 1024), func(v any) { conf.AlipaySetting.RootCertFile = v.(string) }),
|
||||
stringDefWithActive("alipay.public_cert_file", "payments", "alipay", "Alipay public cert file", "Path to the Alipay public certificate file.", ApplyModeRestartRequired, false, true, nil, func() bool { return cfg.If("Alipay") }, func() any { return conf.AlipaySetting.PublicCertFile }, func() any { return bootstrapConfig.Alipay.PublicCertFile }, validateTrimmedMax("alipay.public_cert_file", 1024), func(v any) { conf.AlipaySetting.PublicCertFile = v.(string) }),
|
||||
stringDefWithActive("alipay.app_public_cert_file", "payments", "alipay", "Alipay app public cert file", "Path to the app public certificate file.", ApplyModeRestartRequired, false, true, nil, func() bool { return cfg.If("Alipay") }, func() any { return conf.AlipaySetting.AppPublicCertFile }, func() any { return bootstrapConfig.Alipay.AppPublicCertFile }, validateTrimmedMax("alipay.app_public_cert_file", 1024), func(v any) { conf.AlipaySetting.AppPublicCertFile = v.(string) }),
|
||||
boolDefWithActive("alipay.in_production", "payments", "alipay", "Alipay production mode", "Use Alipay production environment.", ApplyModeRestartRequired, false, true, func() bool { return cfg.If("Alipay") }, func() any { return conf.AlipaySetting.InProduction }, func() any { return bootstrapConfig.Alipay.InProduction }, func(v any) { conf.AlipaySetting.InProduction = v.(bool) }),
|
||||
}
|
||||
}
|
||||
|
||||
func registryMap() map[string]Definition {
|
||||
defs := Registry()
|
||||
res := make(map[string]Definition, len(defs))
|
||||
for _, def := range defs {
|
||||
res[def.Key] = def
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func boolDef(key, group, section, label, description string, applyMode ApplyMode, secret, editable bool, current func() any, bootstrap func() any, apply func(any)) Definition {
|
||||
return boolDefWithActive(key, group, section, label, description, applyMode, secret, editable, func() bool { return true }, current, bootstrap, apply)
|
||||
}
|
||||
|
||||
func boolDefWithActive(key, group, section, label, description string, applyMode ApplyMode, secret, editable bool, active func() bool, current func() any, bootstrap func() any, apply func(any)) Definition {
|
||||
return Definition{Key: key, Group: group, Section: section, Type: TypeBool, Label: label, Description: description, ApplyMode: applyMode, Secret: secret, Readonly: !editable, Active: active, CurrentValue: current, BootstrapDefault: bootstrap, ValidateValue: func(v any) (any, error) {
|
||||
b, ok := v.(bool)
|
||||
if !ok {
|
||||
return nil, invalidType(key, "bool")
|
||||
}
|
||||
return b, nil
|
||||
}, BootstrapOverride: apply}
|
||||
}
|
||||
|
||||
func intDef(key, group, section, label, description string, applyMode ApplyMode, secret, editable bool, current func() any, bootstrap func() any, validator func(int) error, apply func(any)) Definition {
|
||||
return Definition{Key: key, Group: group, Section: section, Type: TypeInt, Label: label, Description: description, ApplyMode: applyMode, Secret: secret, Readonly: !editable, Active: func() bool { return true }, CurrentValue: current, BootstrapDefault: bootstrap, ValidateValue: func(v any) (any, error) {
|
||||
i, ok := asInt(v)
|
||||
if !ok {
|
||||
return nil, invalidType(key, "int")
|
||||
}
|
||||
if validator != nil {
|
||||
if err := validator(i); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return i, nil
|
||||
}, BootstrapOverride: apply}
|
||||
}
|
||||
|
||||
func int64Def(key, group, section, label, description string, applyMode ApplyMode, current func() any, bootstrap func() any, validator func(int64) error, apply func(any)) Definition {
|
||||
return Definition{Key: key, Group: group, Section: section, Type: TypeInt, Label: label, Description: description, ApplyMode: applyMode, Active: func() bool { return true }, CurrentValue: current, BootstrapDefault: bootstrap, ValidateValue: func(v any) (any, error) {
|
||||
i, ok := asInt64(v)
|
||||
if !ok {
|
||||
return nil, invalidType(key, "int")
|
||||
}
|
||||
if validator != nil {
|
||||
if err := validator(i); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return i, nil
|
||||
}, BootstrapOverride: apply}
|
||||
}
|
||||
|
||||
func floatDef(key, group, section, label, description string, applyMode ApplyMode, current func() any, bootstrap func() any, validator func(float64) error, apply func(any)) Definition {
|
||||
return Definition{Key: key, Group: group, Section: section, Type: TypeFloat, Label: label, Description: description, ApplyMode: applyMode, Active: func() bool { return true }, CurrentValue: current, BootstrapDefault: bootstrap, ValidateValue: func(v any) (any, error) {
|
||||
f, ok := asFloat64(v)
|
||||
if !ok {
|
||||
return nil, invalidType(key, "float")
|
||||
}
|
||||
if validator != nil {
|
||||
if err := validator(f); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return f, nil
|
||||
}, BootstrapOverride: apply}
|
||||
}
|
||||
|
||||
func stringDef(key, group, section, label, description string, applyMode ApplyMode, secret, editable bool, options []Option, current func() any, bootstrap func() any, validator func(string) error, apply func(any)) Definition {
|
||||
return stringDefWithActive(key, group, section, label, description, applyMode, secret, editable, options, func() bool { return true }, current, bootstrap, validator, apply)
|
||||
}
|
||||
|
||||
func stringDefWithActive(key, group, section, label, description string, applyMode ApplyMode, secret, editable bool, options []Option, active func() bool, current func() any, bootstrap func() any, validator func(string) error, apply func(any)) Definition {
|
||||
return Definition{Key: key, Group: group, Section: section, Type: TypeString, Label: label, Description: description, ApplyMode: applyMode, Secret: secret, Readonly: !editable, Options: options, Active: active, CurrentValue: current, BootstrapDefault: bootstrap, ValidateValue: func(v any) (any, error) {
|
||||
s, ok := v.(string)
|
||||
if !ok {
|
||||
return nil, invalidType(key, "string")
|
||||
}
|
||||
s = strings.TrimSpace(s)
|
||||
if validator != nil {
|
||||
if err := validator(s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return s, nil
|
||||
}, BootstrapOverride: apply}
|
||||
}
|
||||
|
||||
func validateVisibility(v string) error {
|
||||
for _, option := range visibilityOption {
|
||||
if option.Value == v {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return xerror.InvalidParams.WithDetails("default_tweet_visibility must be one of public/following/friend/private")
|
||||
}
|
||||
|
||||
func validateRequiredTrimmed(name string, max int) func(string) error {
|
||||
return func(v string) error {
|
||||
if v == "" {
|
||||
return xerror.InvalidParams.WithDetails(name + " must not be empty")
|
||||
}
|
||||
if len(v) > max {
|
||||
return xerror.InvalidParams.WithDetails(fmt.Sprintf("%s length must be <= %d", name, max))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func validateTrimmedMax(name string, max int) func(string) error {
|
||||
return func(v string) error {
|
||||
if len(v) > max {
|
||||
return xerror.InvalidParams.WithDetails(fmt.Sprintf("%s length must be <= %d", name, max))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func validateOptionalURL(name string, max int) func(string) error {
|
||||
return func(v string) error {
|
||||
if len(v) > max {
|
||||
return xerror.InvalidParams.WithDetails(fmt.Sprintf("%s length must be <= %d", name, max))
|
||||
}
|
||||
if v == "" {
|
||||
return nil
|
||||
}
|
||||
parsed, err := url.ParseRequestURI(v)
|
||||
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
||||
return xerror.InvalidParams.WithDetails(name + " must be a valid URL")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func between(v, min, max int, name string) error {
|
||||
if v < min || v > max {
|
||||
return xerror.InvalidParams.WithDetails(fmt.Sprintf("%s must be between %d and %d", name, min, max))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func betweenInt64(v, min, max int64, name string) error {
|
||||
if v < min || v > max {
|
||||
return xerror.InvalidParams.WithDetails(fmt.Sprintf("%s must be between %d and %d", name, min, max))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func invalidType(key, want string) error {
|
||||
return xerror.InvalidParams.WithDetails(fmt.Sprintf("%s must be %s", key, want))
|
||||
}
|
||||
|
||||
func asInt(v any) (int, bool) {
|
||||
switch n := v.(type) {
|
||||
case int:
|
||||
return n, true
|
||||
case int32:
|
||||
return int(n), true
|
||||
case int64:
|
||||
return int(n), true
|
||||
case float64:
|
||||
if n != float64(int(n)) {
|
||||
return 0, false
|
||||
}
|
||||
return int(n), true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func asInt64(v any) (int64, bool) {
|
||||
switch n := v.(type) {
|
||||
case int:
|
||||
return int64(n), true
|
||||
case int32:
|
||||
return int64(n), true
|
||||
case int64:
|
||||
return n, true
|
||||
case float64:
|
||||
if n != float64(int64(n)) {
|
||||
return 0, false
|
||||
}
|
||||
return int64(n), true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func asFloat64(v any) (float64, bool) {
|
||||
switch n := v.(type) {
|
||||
case float64:
|
||||
return n, true
|
||||
case float32:
|
||||
return float64(n), true
|
||||
case int:
|
||||
return float64(n), true
|
||||
case int64:
|
||||
return float64(n), true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func parseRequestValue(def Definition, raw stdjson.RawMessage) (any, error) {
|
||||
var target any
|
||||
switch def.Type {
|
||||
case TypeBool:
|
||||
var v bool
|
||||
if err := stdjson.Unmarshal(raw, &v); err != nil {
|
||||
return nil, invalidType(def.Key, "bool")
|
||||
}
|
||||
target = v
|
||||
case TypeInt:
|
||||
var v float64
|
||||
if err := stdjson.Unmarshal(raw, &v); err != nil {
|
||||
return nil, invalidType(def.Key, "int")
|
||||
}
|
||||
target = v
|
||||
case TypeFloat:
|
||||
var v float64
|
||||
if err := stdjson.Unmarshal(raw, &v); err != nil {
|
||||
return nil, invalidType(def.Key, "float")
|
||||
}
|
||||
target = v
|
||||
case TypeString:
|
||||
var v string
|
||||
if err := stdjson.Unmarshal(raw, &v); err != nil {
|
||||
return nil, invalidType(def.Key, "string")
|
||||
}
|
||||
target = v
|
||||
default:
|
||||
return nil, xerror.ServerError.WithDetails("unsupported settings type")
|
||||
}
|
||||
return def.ValidateValue(target)
|
||||
}
|
||||
|
||||
func parseStoredValue(def Definition, raw string) (any, error) {
|
||||
switch def.Type {
|
||||
case TypeBool:
|
||||
return strconv.ParseBool(raw)
|
||||
case TypeInt:
|
||||
if i, err := strconv.ParseInt(raw, 10, 64); err == nil {
|
||||
return def.ValidateValue(i)
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
case TypeFloat:
|
||||
if f, err := strconv.ParseFloat(raw, 64); err == nil {
|
||||
return def.ValidateValue(f)
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
case TypeString:
|
||||
return def.ValidateValue(raw)
|
||||
default:
|
||||
return nil, xerror.ServerError.WithDetails("unsupported settings type")
|
||||
}
|
||||
}
|
||||
|
||||
func serializeValue(def Definition, value any) string {
|
||||
switch def.Type {
|
||||
case TypeBool:
|
||||
return strconv.FormatBool(value.(bool))
|
||||
case TypeInt:
|
||||
switch v := value.(type) {
|
||||
case int:
|
||||
return strconv.Itoa(v)
|
||||
case int64:
|
||||
return strconv.FormatInt(v, 10)
|
||||
default:
|
||||
return fmt.Sprintf("%v", value)
|
||||
}
|
||||
case TypeFloat:
|
||||
return strconv.FormatFloat(value.(float64), 'f', -1, 64)
|
||||
default:
|
||||
return value.(string)
|
||||
}
|
||||
}
|
||||
|
||||
func valuesEqual(left, right any) bool {
|
||||
return fmt.Sprintf("%v", left) == fmt.Sprintf("%v", right)
|
||||
}
|
||||
|
||||
func activeState(def Definition) bool {
|
||||
if def.Active == nil {
|
||||
return true
|
||||
}
|
||||
return def.Active()
|
||||
}
|
||||
|
||||
func maxInt(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func logBootstrapApplyError(def Definition, err error) {
|
||||
logrus.WithError(err).WithField("key", def.Key).Warn("sitesetting: skip invalid persisted override")
|
||||
}
|
||||
@ -0,0 +1,486 @@
|
||||
// Copyright 2026 ROC. All rights reserved.
|
||||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package sitesetting
|
||||
|
||||
import (
|
||||
"context"
|
||||
stdjson "encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rocboss/paopao-ce/internal/conf"
|
||||
"github.com/rocboss/paopao-ce/internal/model/web"
|
||||
"github.com/rocboss/paopao-ce/pkg/xerror"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type EditableProfile struct {
|
||||
UseFriendship bool
|
||||
EnableTrendsBar bool
|
||||
EnableWallet bool
|
||||
AllowTweetAttachment bool
|
||||
AllowTweetAttachmentPrice bool
|
||||
AllowTweetVideo bool
|
||||
DefaultTweetMaxLength int
|
||||
TweetWebEllipsisSize int
|
||||
TweetMobileEllipsisSize int
|
||||
DefaultTweetVisibility string
|
||||
DefaultMsgLoopInterval int
|
||||
CopyrightTop string
|
||||
CopyrightLeft string
|
||||
CopyrightLeftLink string
|
||||
CopyrightRight string
|
||||
CopyrightRightLink string
|
||||
}
|
||||
|
||||
type settingRecord struct {
|
||||
Key string `gorm:"column:key;type:varchar(191);primaryKey"`
|
||||
Value string `gorm:"column:value;type:text;not null"`
|
||||
IsEncrypted bool `gorm:"column:is_encrypted;not null"`
|
||||
CreatedOn int64 `gorm:"column:created_on;not null"`
|
||||
ModifiedOn int64 `gorm:"column:modified_on;not null"`
|
||||
DeletedOn int64 `gorm:"column:deleted_on;not null"`
|
||||
IsDel int8 `gorm:"column:is_del;not null"`
|
||||
}
|
||||
|
||||
func (settingRecord) TableName() string {
|
||||
if conf.DatabaseSetting == nil {
|
||||
return "p_" + conf.TableSiteSettings
|
||||
}
|
||||
return conf.DatabaseSetting.TablePrefix + conf.TableSiteSettings
|
||||
}
|
||||
|
||||
func (r *settingRecord) BeforeCreate(_ *gorm.DB) error {
|
||||
now := time.Now().Unix()
|
||||
r.CreatedOn = now
|
||||
r.ModifiedOn = now
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *settingRecord) BeforeUpdate(tx *gorm.DB) error {
|
||||
if !tx.Statement.Changed("modified_on") {
|
||||
r.ModifiedOn = time.Now().Unix()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
db *gorm.DB
|
||||
registry map[string]Definition
|
||||
codec *secretCodec
|
||||
}
|
||||
|
||||
func NewService(db *gorm.DB) *Service {
|
||||
ensureBootstrapSnapshot()
|
||||
return &Service{db: db, registry: registryMap(), codec: newSecretCodec()}
|
||||
}
|
||||
|
||||
func Bootstrap(ctx context.Context, db *gorm.DB) {
|
||||
ensureBootstrapSnapshot()
|
||||
if db == nil {
|
||||
return
|
||||
}
|
||||
if err := NewService(db).ApplyPersistedOverrides(ctx); err != nil {
|
||||
logrus.WithError(err).Warn("sitesetting: bootstrap override load failed; using bootstrap config only")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) ApplyPersistedOverrides(ctx context.Context) error {
|
||||
records, err := s.loadOverridesLenient(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for key, record := range records {
|
||||
def, ok := s.registry[key]
|
||||
if !ok || def.BootstrapOverride == nil || def.ApplyMode == ApplyModeBootstrapOnly {
|
||||
continue
|
||||
}
|
||||
value, err := s.parseRecord(def, record)
|
||||
if err != nil {
|
||||
logBootstrapApplyError(def, err)
|
||||
continue
|
||||
}
|
||||
def.BootstrapOverride(value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) GetProfile(ctx context.Context) (*web.SiteProfileResp, error) {
|
||||
_ = ctx
|
||||
return &web.SiteProfileResp{
|
||||
UseFriendship: conf.WebProfileSetting.UseFriendship,
|
||||
EnableTrendsBar: conf.WebProfileSetting.EnableTrendsBar,
|
||||
EnableWallet: conf.WebProfileSetting.EnableWallet,
|
||||
AllowTweetAttachment: conf.WebProfileSetting.AllowTweetAttachment,
|
||||
AllowTweetAttachmentPrice: conf.WebProfileSetting.AllowTweetAttachmentPrice,
|
||||
AllowTweetVideo: conf.WebProfileSetting.AllowTweetVideo,
|
||||
AllowUserRegister: conf.WebProfileSetting.AllowUserRegister,
|
||||
AllowPhoneBind: conf.WebProfileSetting.AllowPhoneBind,
|
||||
DefaultTweetMaxLength: conf.WebProfileSetting.DefaultTweetMaxLength,
|
||||
TweetWebEllipsisSize: conf.WebProfileSetting.TweetWebEllipsisSize,
|
||||
TweetMobileEllipsisSize: conf.WebProfileSetting.TweetMobileEllipsisSize,
|
||||
DefaultTweetVisibility: conf.WebProfileSetting.DefaultTweetVisibility,
|
||||
DefaultMsgLoopInterval: conf.WebProfileSetting.DefaultMsgLoopInterval,
|
||||
CopyrightTop: conf.WebProfileSetting.CopyrightTop,
|
||||
CopyrightLeft: conf.WebProfileSetting.CopyrightLeft,
|
||||
CopyrightLeftLink: conf.WebProfileSetting.CopyrightLeftLink,
|
||||
CopyrightRight: conf.WebProfileSetting.CopyrightRight,
|
||||
CopyrightRightLink: conf.WebProfileSetting.CopyrightRightLink,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) UpdateEditableProfile(ctx context.Context, input EditableProfile) (*web.SiteProfileResp, error) {
|
||||
if err := ValidateEditableProfile(input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := []web.AdminSettingValueInput{
|
||||
{Key: "web_profile.use_friendship", Value: boolRaw(input.UseFriendship)},
|
||||
{Key: "web_profile.enable_trends_bar", Value: boolRaw(input.EnableTrendsBar)},
|
||||
{Key: "web_profile.enable_wallet", Value: boolRaw(input.EnableWallet)},
|
||||
{Key: "web_profile.allow_tweet_attachment", Value: boolRaw(input.AllowTweetAttachment)},
|
||||
{Key: "web_profile.allow_tweet_attachment_price", Value: boolRaw(input.AllowTweetAttachmentPrice)},
|
||||
{Key: "web_profile.allow_tweet_video", Value: boolRaw(input.AllowTweetVideo)},
|
||||
{Key: "web_profile.default_tweet_max_length", Value: intRaw(input.DefaultTweetMaxLength)},
|
||||
{Key: "web_profile.tweet_web_ellipsis_size", Value: intRaw(input.TweetWebEllipsisSize)},
|
||||
{Key: "web_profile.tweet_mobile_ellipsis_size", Value: intRaw(input.TweetMobileEllipsisSize)},
|
||||
{Key: "web_profile.default_tweet_visibility", Value: stringRaw(input.DefaultTweetVisibility)},
|
||||
{Key: "web_profile.default_msg_loop_interval", Value: intRaw(input.DefaultMsgLoopInterval)},
|
||||
{Key: "web_profile.copyright_top", Value: stringRaw(input.CopyrightTop)},
|
||||
{Key: "web_profile.copyright_left", Value: stringRaw(input.CopyrightLeft)},
|
||||
{Key: "web_profile.copyright_left_link", Value: stringRaw(input.CopyrightLeftLink)},
|
||||
{Key: "web_profile.copyright_right", Value: stringRaw(input.CopyrightRight)},
|
||||
{Key: "web_profile.copyright_right_link", Value: stringRaw(input.CopyrightRightLink)},
|
||||
}
|
||||
if _, err := s.SaveValues(ctx, items); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.GetProfile(ctx)
|
||||
}
|
||||
|
||||
func (s *Service) GetSchema() (*web.AdminSettingsSchemaResp, error) {
|
||||
items := make([]web.AdminSettingSchemaItem, 0, len(s.registry))
|
||||
for _, def := range Registry() {
|
||||
item := web.AdminSettingSchemaItem{
|
||||
Key: def.Key,
|
||||
Group: def.Group,
|
||||
Section: def.Section,
|
||||
Type: string(def.Type),
|
||||
Label: def.Label,
|
||||
Description: def.Description,
|
||||
ApplyMode: string(def.ApplyMode),
|
||||
Secret: def.Secret,
|
||||
Readonly: def.Readonly,
|
||||
Active: activeState(def),
|
||||
Options: def.Options,
|
||||
}
|
||||
bootstrap := def.BootstrapDefault()
|
||||
if def.Secret {
|
||||
item.BootstrapConfigured = configuredValue(bootstrap)
|
||||
} else {
|
||||
item.BootstrapValue = bootstrap
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return &web.AdminSettingsSchemaResp{Items: items}, nil
|
||||
}
|
||||
|
||||
func (s *Service) GetValues(ctx context.Context) (*web.AdminSettingsValuesResp, error) {
|
||||
records, err := s.loadOverridesLenient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items, pending := s.buildValueItems(records)
|
||||
return &web.AdminSettingsValuesResp{Items: items, HasPendingRestart: pending}, nil
|
||||
}
|
||||
|
||||
func (s *Service) SaveValues(ctx context.Context, inputs []web.AdminSettingValueInput) (*web.AdminSettingsSaveResp, error) {
|
||||
if len(inputs) == 0 {
|
||||
return nil, xerror.InvalidParams.WithDetails("items must not be empty")
|
||||
}
|
||||
prepared := make([]preparedValue, 0, len(inputs))
|
||||
seen := make(map[string]struct{}, len(inputs))
|
||||
for _, input := range inputs {
|
||||
if len(input.Value) == 0 {
|
||||
return nil, xerror.InvalidParams.WithDetails("missing setting value: " + input.Key)
|
||||
}
|
||||
def, ok := s.registry[input.Key]
|
||||
if !ok {
|
||||
return nil, xerror.InvalidParams.WithDetails("unknown setting key: " + input.Key)
|
||||
}
|
||||
if def.Readonly || def.ApplyMode == ApplyModeBootstrapOnly || def.BootstrapOverride == nil {
|
||||
return nil, xerror.InvalidParams.WithDetails("setting is not editable: " + input.Key)
|
||||
}
|
||||
if _, ok := seen[input.Key]; ok {
|
||||
return nil, xerror.InvalidParams.WithDetails("duplicate setting key: " + input.Key)
|
||||
}
|
||||
seen[input.Key] = struct{}{}
|
||||
value, err := parseRequestValue(def, input.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
prepared = append(prepared, preparedValue{Definition: def, Value: value})
|
||||
}
|
||||
if err := validatePreparedBatch(prepared); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.persistPrepared(ctx, prepared); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range prepared {
|
||||
if item.ApplyMode == ApplyModeLive {
|
||||
item.BootstrapOverride(item.Value)
|
||||
}
|
||||
}
|
||||
values, err := s.GetValues(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
updatedKeys := make([]string, 0, len(prepared))
|
||||
for _, item := range prepared {
|
||||
updatedKeys = append(updatedKeys, item.Key)
|
||||
}
|
||||
return &web.AdminSettingsSaveResp{Items: values.Items, UpdatedKeys: updatedKeys, HasPendingRestart: values.HasPendingRestart}, nil
|
||||
}
|
||||
|
||||
type preparedValue struct {
|
||||
Definition
|
||||
Value any
|
||||
}
|
||||
|
||||
func validatePreparedBatch(prepared []preparedValue) error {
|
||||
defaultTweetMaxLength := preparedInt(prepared, "web_profile.default_tweet_max_length", conf.WebProfileSetting.DefaultTweetMaxLength)
|
||||
tweetWebEllipsisSize := preparedInt(prepared, "web_profile.tweet_web_ellipsis_size", conf.WebProfileSetting.TweetWebEllipsisSize)
|
||||
tweetMobileEllipsisSize := preparedInt(prepared, "web_profile.tweet_mobile_ellipsis_size", conf.WebProfileSetting.TweetMobileEllipsisSize)
|
||||
defaultPageSize := preparedInt(prepared, "app.default_page_size", conf.AppSetting.DefaultPageSize)
|
||||
maxPageSize := preparedInt(prepared, "app.max_page_size", conf.AppSetting.MaxPageSize)
|
||||
|
||||
if tweetWebEllipsisSize < 1 || tweetWebEllipsisSize > defaultTweetMaxLength {
|
||||
return xerror.InvalidParams.WithDetails("tweet_web_ellipsis_size must be between 1 and default_tweet_max_length")
|
||||
}
|
||||
if tweetMobileEllipsisSize < 1 || tweetMobileEllipsisSize > defaultTweetMaxLength {
|
||||
return xerror.InvalidParams.WithDetails("tweet_mobile_ellipsis_size must be between 1 and default_tweet_max_length")
|
||||
}
|
||||
if defaultPageSize < 1 || defaultPageSize > maxPageSize {
|
||||
return xerror.InvalidParams.WithDetails("default_page_size must be between 1 and max_page_size")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func preparedInt(prepared []preparedValue, key string, fallback int) int {
|
||||
for _, item := range prepared {
|
||||
if item.Key == key {
|
||||
if value, ok := item.Value.(int); ok {
|
||||
return value
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func (s *Service) persistPrepared(ctx context.Context, prepared []preparedValue) error {
|
||||
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
for _, item := range prepared {
|
||||
serialized := serializeValue(item.Definition, item.Value)
|
||||
if valuesEqual(item.BootstrapDefault(), item.Value) {
|
||||
if err := tx.Where("key = ?", item.Key).Delete(&settingRecord{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
storedValue := serialized
|
||||
isEncrypted := false
|
||||
if item.Secret {
|
||||
encrypted, err := s.codec.Encrypt(serialized)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
storedValue = encrypted
|
||||
isEncrypted = true
|
||||
}
|
||||
record := settingRecord{Key: item.Key, Value: storedValue, IsEncrypted: isEncrypted}
|
||||
if err := tx.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "key"}}, DoUpdates: clause.AssignmentColumns([]string{"value", "is_encrypted", "modified_on", "deleted_on", "is_del"})}).Create(&record).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) buildValueItems(records map[string]settingRecord) ([]web.AdminSettingValue, bool) {
|
||||
items := make([]web.AdminSettingValue, 0, len(s.registry))
|
||||
hasPendingRestart := false
|
||||
for _, def := range Registry() {
|
||||
current := def.CurrentValue()
|
||||
item := web.AdminSettingValue{Key: def.Key, Source: "bootstrap", Active: activeState(def)}
|
||||
if def.Secret {
|
||||
item.Configured = configuredValue(current)
|
||||
} else {
|
||||
item.Value = current
|
||||
item.EffectiveValue = current
|
||||
}
|
||||
if record, ok := records[def.Key]; ok {
|
||||
storedValue, err := s.parseRecord(def, record)
|
||||
if err == nil {
|
||||
item.Source = "override"
|
||||
if def.Secret {
|
||||
item.Configured = configuredValue(storedValue)
|
||||
} else {
|
||||
item.Value = storedValue
|
||||
item.EffectiveValue = current
|
||||
}
|
||||
if def.ApplyMode == ApplyModeRestartRequired && !valuesEqual(storedValue, current) {
|
||||
item.PendingRestart = true
|
||||
hasPendingRestart = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if def.Secret && item.Source == "bootstrap" {
|
||||
item.Configured = configuredValue(def.BootstrapDefault())
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return items, hasPendingRestart
|
||||
}
|
||||
|
||||
func (s *Service) parseRecord(def Definition, record settingRecord) (any, error) {
|
||||
raw := record.Value
|
||||
if record.IsEncrypted {
|
||||
decrypted, err := s.codec.Decrypt(record.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
raw = decrypted
|
||||
}
|
||||
return parseStoredValue(def, raw)
|
||||
}
|
||||
|
||||
func (s *Service) loadOverrides(ctx context.Context) (map[string]settingRecord, error) {
|
||||
var records []settingRecord
|
||||
err := s.db.WithContext(ctx).Where("is_del = ?", 0).Find(&records).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := make(map[string]settingRecord, len(records))
|
||||
for _, record := range records {
|
||||
res[record.Key] = record
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *Service) loadOverridesLenient(ctx context.Context) (map[string]settingRecord, error) {
|
||||
records, err := s.loadOverrides(ctx)
|
||||
if err != nil {
|
||||
if isMissingTableError(err) {
|
||||
logrus.WithError(err).Warn("sitesetting: overrides table unavailable; falling back to bootstrap values")
|
||||
return map[string]settingRecord{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func isMissingTableError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
text := strings.ToLower(err.Error())
|
||||
return strings.Contains(text, "no such table") || strings.Contains(text, "doesn't exist") || strings.Contains(text, "does not exist") || errors.Is(err, gorm.ErrRecordNotFound)
|
||||
}
|
||||
|
||||
func configuredValue(v any) bool {
|
||||
switch value := v.(type) {
|
||||
case nil:
|
||||
return false
|
||||
case string:
|
||||
return strings.TrimSpace(value) != ""
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func boolRaw(v bool) []byte {
|
||||
b, _ := stdjson.Marshal(v)
|
||||
return b
|
||||
}
|
||||
|
||||
func intRaw(v int) []byte {
|
||||
b, _ := stdjson.Marshal(v)
|
||||
return b
|
||||
}
|
||||
|
||||
func stringRaw(v string) []byte {
|
||||
b, _ := stdjson.Marshal(strings.TrimSpace(v))
|
||||
return b
|
||||
}
|
||||
|
||||
func ValidateEditableProfile(input EditableProfile) error {
|
||||
return validateProfileInput(input)
|
||||
}
|
||||
|
||||
func validateProfileInput(input EditableProfile) error {
|
||||
if err := between(input.DefaultTweetMaxLength, 1, 2000, "default_tweet_max_length"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := between(input.TweetWebEllipsisSize, 1, input.DefaultTweetMaxLength, "tweet_web_ellipsis_size"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := between(input.TweetMobileEllipsisSize, 1, input.DefaultTweetMaxLength, "tweet_mobile_ellipsis_size"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := between(input.DefaultMsgLoopInterval, 1000, 60000, "default_msg_loop_interval"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateVisibility(strings.TrimSpace(input.DefaultTweetVisibility)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateRequiredTrimmed("copyright_top", 255)(input.CopyrightTop); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateRequiredTrimmed("copyright_left", 255)(input.CopyrightLeft); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateRequiredTrimmed("copyright_right", 255)(input.CopyrightRight); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateOptionalURL("copyright_left_link", 255)(input.CopyrightLeftLink); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateOptionalURL("copyright_right_link", 255)(input.CopyrightRightLink); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func EditableFromRequest(req *web.SiteSettingsReq) EditableProfile {
|
||||
return EditableProfile{
|
||||
UseFriendship: *req.UseFriendship,
|
||||
EnableTrendsBar: *req.EnableTrendsBar,
|
||||
EnableWallet: *req.EnableWallet,
|
||||
AllowTweetAttachment: *req.AllowTweetAttachment,
|
||||
AllowTweetAttachmentPrice: *req.AllowTweetAttachmentPrice,
|
||||
AllowTweetVideo: *req.AllowTweetVideo,
|
||||
DefaultTweetMaxLength: *req.DefaultTweetMaxLength,
|
||||
TweetWebEllipsisSize: *req.TweetWebEllipsisSize,
|
||||
TweetMobileEllipsisSize: *req.TweetMobileEllipsisSize,
|
||||
DefaultTweetVisibility: *req.DefaultTweetVisibility,
|
||||
DefaultMsgLoopInterval: *req.DefaultMsgLoopInterval,
|
||||
CopyrightTop: *req.CopyrightTop,
|
||||
CopyrightLeft: *req.CopyrightLeft,
|
||||
CopyrightLeftLink: stringValue(req.CopyrightLeftLink),
|
||||
CopyrightRight: *req.CopyrightRight,
|
||||
CopyrightRightLink: stringValue(req.CopyrightRightLink),
|
||||
}
|
||||
}
|
||||
|
||||
func stringValue(v *string) string {
|
||||
if v == nil {
|
||||
return ""
|
||||
}
|
||||
return *v
|
||||
}
|
||||
@ -0,0 +1,222 @@
|
||||
package sitesetting
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/rocboss/paopao-ce/internal/conf"
|
||||
"github.com/rocboss/paopao-ce/internal/model/web"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/schema"
|
||||
)
|
||||
|
||||
func TestGetProfileUsesBootstrapDefaultsWhenNoOverride(t *testing.T) {
|
||||
svc := newTestService(t)
|
||||
|
||||
profile, err := svc.GetProfile(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("GetProfile() error = %v", err)
|
||||
}
|
||||
if !profile.AllowUserRegister {
|
||||
t.Fatalf("AllowUserRegister = false, want bootstrap true")
|
||||
}
|
||||
if profile.DefaultTweetVisibility != "friend" {
|
||||
t.Fatalf("DefaultTweetVisibility = %q, want friend", profile.DefaultTweetVisibility)
|
||||
}
|
||||
if profile.CopyrightRight != "fallback-right" {
|
||||
t.Fatalf("CopyrightRight = %q, want fallback-right", profile.CopyrightRight)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateEditableProfilePersistsOnlyEditableKeys(t *testing.T) {
|
||||
svc := newTestService(t)
|
||||
|
||||
profile, err := svc.UpdateEditableProfile(context.Background(), EditableProfile{
|
||||
UseFriendship: false,
|
||||
EnableTrendsBar: true,
|
||||
EnableWallet: true,
|
||||
AllowTweetAttachment: false,
|
||||
AllowTweetAttachmentPrice: false,
|
||||
AllowTweetVideo: false,
|
||||
DefaultTweetMaxLength: 1200,
|
||||
TweetWebEllipsisSize: 300,
|
||||
TweetMobileEllipsisSize: 200,
|
||||
DefaultTweetVisibility: "public",
|
||||
DefaultMsgLoopInterval: 3000,
|
||||
CopyrightTop: "top",
|
||||
CopyrightLeft: "left",
|
||||
CopyrightLeftLink: "https://left.example.com",
|
||||
CopyrightRight: "right",
|
||||
CopyrightRightLink: "https://right.example.com",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateEditableProfile() error = %v", err)
|
||||
}
|
||||
if !profile.AllowUserRegister {
|
||||
t.Fatalf("AllowUserRegister = false, want bootstrap true")
|
||||
}
|
||||
if !profile.AllowPhoneBind {
|
||||
t.Fatalf("AllowPhoneBind = false, want bootstrap true")
|
||||
}
|
||||
if profile.DefaultTweetVisibility != "public" {
|
||||
t.Fatalf("DefaultTweetVisibility = %q, want public", profile.DefaultTweetVisibility)
|
||||
}
|
||||
values, err := svc.GetValues(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("GetValues() error = %v", err)
|
||||
}
|
||||
if !hasValue(values.Items, "web_profile.enable_wallet", true, false) {
|
||||
t.Fatalf("web_profile.enable_wallet override not found")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveValuesEncryptsSecretsAtRest(t *testing.T) {
|
||||
svc := newTestService(t)
|
||||
conf.AdminSettingsSetting.EncryptionKey = "bootstrap-test-encryption-key"
|
||||
svc.codec = newSecretCodec()
|
||||
|
||||
_, err := svc.SaveValues(context.Background(), []web.AdminSettingValueInput{{
|
||||
Key: "meili.api_key",
|
||||
Value: []byte(`"top-secret-key"`),
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatalf("SaveValues() error = %v", err)
|
||||
}
|
||||
var record settingRecord
|
||||
if err := svc.db.WithContext(context.Background()).First(&record, "key = ?", "meili.api_key").Error; err != nil {
|
||||
t.Fatalf("load record error = %v", err)
|
||||
}
|
||||
if !record.IsEncrypted {
|
||||
t.Fatalf("IsEncrypted = false, want true")
|
||||
}
|
||||
if strings.Contains(record.Value, "top-secret-key") {
|
||||
t.Fatalf("record.Value stored plaintext = %q", record.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestartRequiredValuesReportPendingRestart(t *testing.T) {
|
||||
svc := newTestService(t)
|
||||
|
||||
resp, err := svc.SaveValues(context.Background(), []web.AdminSettingValueInput{{
|
||||
Key: "meili.host",
|
||||
Value: []byte(`"pending-restart:7700"`),
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatalf("SaveValues() error = %v", err)
|
||||
}
|
||||
if !resp.HasPendingRestart {
|
||||
t.Fatal("HasPendingRestart = false, want true")
|
||||
}
|
||||
if !hasValue(resp.Items, "meili.host", "pending-restart:7700", true) {
|
||||
t.Fatalf("meili.host pending_restart not reported")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveValuesRejectsInvalidSingleIntUpdate(t *testing.T) {
|
||||
svc := newTestService(t)
|
||||
|
||||
_, err := svc.SaveValues(context.Background(), []web.AdminSettingValueInput{{
|
||||
Key: "web_profile.default_tweet_max_length",
|
||||
Value: []byte(`120`),
|
||||
}})
|
||||
if err == nil {
|
||||
t.Fatal("SaveValues() error = nil, want invalid params error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveValuesAcceptsCoupledProfileUpdate(t *testing.T) {
|
||||
svc := newTestService(t)
|
||||
|
||||
resp, err := svc.SaveValues(context.Background(), []web.AdminSettingValueInput{
|
||||
{Key: "web_profile.default_tweet_max_length", Value: []byte(`120`)},
|
||||
{Key: "web_profile.tweet_web_ellipsis_size", Value: []byte(`120`)},
|
||||
{Key: "web_profile.tweet_mobile_ellipsis_size", Value: []byte(`120`)},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SaveValues() error = %v", err)
|
||||
}
|
||||
if !hasValue(resp.Items, "web_profile.default_tweet_max_length", 120, false) {
|
||||
t.Fatalf("web_profile.default_tweet_max_length override not found")
|
||||
}
|
||||
if conf.WebProfileSetting.DefaultTweetMaxLength != 120 {
|
||||
t.Fatalf("DefaultTweetMaxLength = %d, want 120", conf.WebProfileSetting.DefaultTweetMaxLength)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBootstrapAppliesPersistedOverrides(t *testing.T) {
|
||||
svc := newTestService(t)
|
||||
conf.AdminSettingsSetting.EncryptionKey = "bootstrap-test-encryption-key"
|
||||
svc.codec = newSecretCodec()
|
||||
|
||||
_, err := svc.SaveValues(context.Background(), []web.AdminSettingValueInput{{
|
||||
Key: "web_profile.enable_wallet",
|
||||
Value: []byte(`true`),
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatalf("SaveValues() error = %v", err)
|
||||
}
|
||||
conf.WebProfileSetting.EnableWallet = false
|
||||
Bootstrap(context.Background(), svc.db)
|
||||
if !conf.WebProfileSetting.EnableWallet {
|
||||
t.Fatal("Bootstrap() did not apply persisted web_profile.enable_wallet override")
|
||||
}
|
||||
}
|
||||
|
||||
func newTestService(t *testing.T) *Service {
|
||||
t.Helper()
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("os.Getwd() error = %v", err)
|
||||
}
|
||||
root := filepath.Clean(filepath.Join(wd, "..", ".."))
|
||||
if err := os.Chdir(root); err != nil {
|
||||
t.Fatalf("os.Chdir(%q) error = %v", root, err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = os.Chdir(wd)
|
||||
})
|
||||
conf.Initial(nil, false)
|
||||
conf.WebProfileSetting = &conf.WebProfileConf{
|
||||
UseFriendship: true,
|
||||
EnableTrendsBar: false,
|
||||
EnableWallet: false,
|
||||
AllowTweetAttachment: true,
|
||||
AllowTweetAttachmentPrice: true,
|
||||
AllowTweetVideo: true,
|
||||
AllowUserRegister: true,
|
||||
AllowPhoneBind: true,
|
||||
DefaultTweetMaxLength: 2000,
|
||||
TweetWebEllipsisSize: 400,
|
||||
TweetMobileEllipsisSize: 300,
|
||||
DefaultTweetVisibility: "friend",
|
||||
DefaultMsgLoopInterval: 5000,
|
||||
CopyrightTop: "fallback-top",
|
||||
CopyrightLeft: "fallback-left",
|
||||
CopyrightLeftLink: "",
|
||||
CopyrightRight: "fallback-right",
|
||||
CopyrightRightLink: "https://fallback.example.com",
|
||||
}
|
||||
bootstrapConfig = nil
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{NamingStrategy: schema.NamingStrategy{TablePrefix: "p_", SingularTable: true}})
|
||||
if err != nil {
|
||||
t.Fatalf("gorm.Open() error = %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(&settingRecord{}); err != nil {
|
||||
t.Fatalf("AutoMigrate() error = %v", err)
|
||||
}
|
||||
return NewService(db)
|
||||
}
|
||||
|
||||
func hasValue(items []web.AdminSettingValue, key string, expected any, pending bool) bool {
|
||||
for _, item := range items {
|
||||
if item.Key != key {
|
||||
continue
|
||||
}
|
||||
return item.Value == expected && item.PendingRestart == pending
|
||||
}
|
||||
return false
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS `p_site_settings`;
|
||||
@ -0,0 +1,24 @@
|
||||
CREATE TABLE `p_site_settings` (
|
||||
`id` BIGINT NOT NULL DEFAULT 1,
|
||||
`use_friendship` TINYINT NOT NULL DEFAULT 0,
|
||||
`enable_trends_bar` TINYINT NOT NULL DEFAULT 0,
|
||||
`enable_wallet` TINYINT NOT NULL DEFAULT 0,
|
||||
`allow_tweet_attachment` TINYINT NOT NULL DEFAULT 0,
|
||||
`allow_tweet_attachment_price` TINYINT NOT NULL DEFAULT 0,
|
||||
`allow_tweet_video` TINYINT NOT NULL DEFAULT 0,
|
||||
`default_tweet_max_length` INT NOT NULL DEFAULT 2000,
|
||||
`tweet_web_ellipsis_size` INT NOT NULL DEFAULT 400,
|
||||
`tweet_mobile_ellipsis_size` INT NOT NULL DEFAULT 300,
|
||||
`default_tweet_visibility` VARCHAR(32) NOT NULL DEFAULT 'friend',
|
||||
`default_msg_loop_interval` INT NOT NULL DEFAULT 5000,
|
||||
`copyright_top` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`copyright_left` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`copyright_left_link` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`copyright_right` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`copyright_right_link` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`created_on` BIGINT NOT NULL DEFAULT 0,
|
||||
`modified_on` BIGINT NOT NULL DEFAULT 0,
|
||||
`deleted_on` BIGINT NOT NULL DEFAULT 0,
|
||||
`is_del` TINYINT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='站点配置';
|
||||
@ -0,0 +1,26 @@
|
||||
DROP TABLE IF EXISTS `p_site_settings`;
|
||||
|
||||
CREATE TABLE `p_site_settings` (
|
||||
`id` BIGINT NOT NULL DEFAULT 1,
|
||||
`use_friendship` TINYINT NOT NULL DEFAULT 0,
|
||||
`enable_trends_bar` TINYINT NOT NULL DEFAULT 0,
|
||||
`enable_wallet` TINYINT NOT NULL DEFAULT 0,
|
||||
`allow_tweet_attachment` TINYINT NOT NULL DEFAULT 0,
|
||||
`allow_tweet_attachment_price` TINYINT NOT NULL DEFAULT 0,
|
||||
`allow_tweet_video` TINYINT NOT NULL DEFAULT 0,
|
||||
`default_tweet_max_length` INT NOT NULL DEFAULT 2000,
|
||||
`tweet_web_ellipsis_size` INT NOT NULL DEFAULT 400,
|
||||
`tweet_mobile_ellipsis_size` INT NOT NULL DEFAULT 300,
|
||||
`default_tweet_visibility` VARCHAR(32) NOT NULL DEFAULT 'friend',
|
||||
`default_msg_loop_interval` INT NOT NULL DEFAULT 5000,
|
||||
`copyright_top` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`copyright_left` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`copyright_left_link` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`copyright_right` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`copyright_right_link` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`created_on` BIGINT NOT NULL DEFAULT 0,
|
||||
`modified_on` BIGINT NOT NULL DEFAULT 0,
|
||||
`deleted_on` BIGINT NOT NULL DEFAULT 0,
|
||||
`is_del` TINYINT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='站点配置';
|
||||
@ -0,0 +1,49 @@
|
||||
RENAME TABLE `p_site_settings` TO `p_site_settings_legacy`;
|
||||
|
||||
CREATE TABLE `p_site_settings` (
|
||||
`key` VARCHAR(191) NOT NULL,
|
||||
`value` TEXT NOT NULL,
|
||||
`is_encrypted` TINYINT NOT NULL DEFAULT 0,
|
||||
`created_on` BIGINT NOT NULL DEFAULT 0,
|
||||
`modified_on` BIGINT NOT NULL DEFAULT 0,
|
||||
`deleted_on` BIGINT NOT NULL DEFAULT 0,
|
||||
`is_del` TINYINT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`key`) USING BTREE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='Admin settings overrides';
|
||||
|
||||
INSERT INTO `p_site_settings` (`key`, `value`, `is_encrypted`, `created_on`, `modified_on`, `deleted_on`, `is_del`)
|
||||
SELECT `key`, `value`, 0, created_on, modified_on, 0, 0 FROM (
|
||||
SELECT 'web_profile.use_friendship' AS `key`, IF(use_friendship <> 0, 'true', 'false') AS `value`, created_on, modified_on FROM `p_site_settings_legacy` WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.enable_trends_bar', IF(enable_trends_bar <> 0, 'true', 'false'), created_on, modified_on FROM `p_site_settings_legacy` WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.enable_wallet', IF(enable_wallet <> 0, 'true', 'false'), created_on, modified_on FROM `p_site_settings_legacy` WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.allow_tweet_attachment', IF(allow_tweet_attachment <> 0, 'true', 'false'), created_on, modified_on FROM `p_site_settings_legacy` WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.allow_tweet_attachment_price', IF(allow_tweet_attachment_price <> 0, 'true', 'false'), created_on, modified_on FROM `p_site_settings_legacy` WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.allow_tweet_video', IF(allow_tweet_video <> 0, 'true', 'false'), created_on, modified_on FROM `p_site_settings_legacy` WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.default_tweet_max_length', CAST(default_tweet_max_length AS CHAR), created_on, modified_on FROM `p_site_settings_legacy` WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.tweet_web_ellipsis_size', CAST(tweet_web_ellipsis_size AS CHAR), created_on, modified_on FROM `p_site_settings_legacy` WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.tweet_mobile_ellipsis_size', CAST(tweet_mobile_ellipsis_size AS CHAR), created_on, modified_on FROM `p_site_settings_legacy` WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.default_tweet_visibility', default_tweet_visibility, created_on, modified_on FROM `p_site_settings_legacy` WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.default_msg_loop_interval', CAST(default_msg_loop_interval AS CHAR), created_on, modified_on FROM `p_site_settings_legacy` WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.copyright_top', copyright_top, created_on, modified_on FROM `p_site_settings_legacy` WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.copyright_left', copyright_left, created_on, modified_on FROM `p_site_settings_legacy` WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.copyright_left_link', copyright_left_link, created_on, modified_on FROM `p_site_settings_legacy` WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.copyright_right', copyright_right, created_on, modified_on FROM `p_site_settings_legacy` WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.copyright_right_link', copyright_right_link, created_on, modified_on FROM `p_site_settings_legacy` WHERE id = 1
|
||||
) AS legacy_rows;
|
||||
|
||||
DROP TABLE `p_site_settings_legacy`;
|
||||
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS p_site_settings;
|
||||
@ -0,0 +1,23 @@
|
||||
CREATE TABLE p_site_settings (
|
||||
id BIGINT PRIMARY KEY DEFAULT 1,
|
||||
use_friendship BOOLEAN NOT NULL DEFAULT false,
|
||||
enable_trends_bar BOOLEAN NOT NULL DEFAULT false,
|
||||
enable_wallet BOOLEAN NOT NULL DEFAULT false,
|
||||
allow_tweet_attachment BOOLEAN NOT NULL DEFAULT false,
|
||||
allow_tweet_attachment_price BOOLEAN NOT NULL DEFAULT false,
|
||||
allow_tweet_video BOOLEAN NOT NULL DEFAULT false,
|
||||
default_tweet_max_length INTEGER NOT NULL DEFAULT 2000,
|
||||
tweet_web_ellipsis_size INTEGER NOT NULL DEFAULT 400,
|
||||
tweet_mobile_ellipsis_size INTEGER NOT NULL DEFAULT 300,
|
||||
default_tweet_visibility VARCHAR(32) NOT NULL DEFAULT 'friend',
|
||||
default_msg_loop_interval INTEGER NOT NULL DEFAULT 5000,
|
||||
copyright_top VARCHAR(255) NOT NULL DEFAULT '',
|
||||
copyright_left VARCHAR(255) NOT NULL DEFAULT '',
|
||||
copyright_left_link VARCHAR(255) NOT NULL DEFAULT '',
|
||||
copyright_right VARCHAR(255) NOT NULL DEFAULT '',
|
||||
copyright_right_link VARCHAR(255) NOT NULL DEFAULT '',
|
||||
created_on BIGINT NOT NULL DEFAULT 0,
|
||||
modified_on BIGINT NOT NULL DEFAULT 0,
|
||||
deleted_on BIGINT NOT NULL DEFAULT 0,
|
||||
is_del SMALLINT NOT NULL DEFAULT 0
|
||||
);
|
||||
@ -0,0 +1,25 @@
|
||||
DROP TABLE IF EXISTS p_site_settings;
|
||||
|
||||
CREATE TABLE p_site_settings (
|
||||
id BIGINT PRIMARY KEY DEFAULT 1,
|
||||
use_friendship BOOLEAN NOT NULL DEFAULT false,
|
||||
enable_trends_bar BOOLEAN NOT NULL DEFAULT false,
|
||||
enable_wallet BOOLEAN NOT NULL DEFAULT false,
|
||||
allow_tweet_attachment BOOLEAN NOT NULL DEFAULT false,
|
||||
allow_tweet_attachment_price BOOLEAN NOT NULL DEFAULT false,
|
||||
allow_tweet_video BOOLEAN NOT NULL DEFAULT false,
|
||||
default_tweet_max_length INTEGER NOT NULL DEFAULT 2000,
|
||||
tweet_web_ellipsis_size INTEGER NOT NULL DEFAULT 400,
|
||||
tweet_mobile_ellipsis_size INTEGER NOT NULL DEFAULT 300,
|
||||
default_tweet_visibility VARCHAR(32) NOT NULL DEFAULT 'friend',
|
||||
default_msg_loop_interval INTEGER NOT NULL DEFAULT 5000,
|
||||
copyright_top VARCHAR(255) NOT NULL DEFAULT '',
|
||||
copyright_left VARCHAR(255) NOT NULL DEFAULT '',
|
||||
copyright_left_link VARCHAR(255) NOT NULL DEFAULT '',
|
||||
copyright_right VARCHAR(255) NOT NULL DEFAULT '',
|
||||
copyright_right_link VARCHAR(255) NOT NULL DEFAULT '',
|
||||
created_on BIGINT NOT NULL DEFAULT 0,
|
||||
modified_on BIGINT NOT NULL DEFAULT 0,
|
||||
deleted_on BIGINT NOT NULL DEFAULT 0,
|
||||
is_del SMALLINT NOT NULL DEFAULT 0
|
||||
);
|
||||
@ -0,0 +1,48 @@
|
||||
ALTER TABLE p_site_settings RENAME TO p_site_settings_legacy;
|
||||
|
||||
CREATE TABLE p_site_settings (
|
||||
key VARCHAR(191) PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
is_encrypted BOOLEAN NOT NULL DEFAULT false,
|
||||
created_on BIGINT NOT NULL DEFAULT 0,
|
||||
modified_on BIGINT NOT NULL DEFAULT 0,
|
||||
deleted_on BIGINT NOT NULL DEFAULT 0,
|
||||
is_del SMALLINT NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
INSERT INTO p_site_settings (key, value, is_encrypted, created_on, modified_on, deleted_on, is_del)
|
||||
SELECT key, value, false, created_on, modified_on, 0, 0 FROM (
|
||||
SELECT 'web_profile.use_friendship' AS key, CASE WHEN use_friendship THEN 'true' ELSE 'false' END AS value, created_on, modified_on FROM p_site_settings_legacy WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.enable_trends_bar', CASE WHEN enable_trends_bar THEN 'true' ELSE 'false' END, created_on, modified_on FROM p_site_settings_legacy WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.enable_wallet', CASE WHEN enable_wallet THEN 'true' ELSE 'false' END, created_on, modified_on FROM p_site_settings_legacy WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.allow_tweet_attachment', CASE WHEN allow_tweet_attachment THEN 'true' ELSE 'false' END, created_on, modified_on FROM p_site_settings_legacy WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.allow_tweet_attachment_price', CASE WHEN allow_tweet_attachment_price THEN 'true' ELSE 'false' END, created_on, modified_on FROM p_site_settings_legacy WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.allow_tweet_video', CASE WHEN allow_tweet_video THEN 'true' ELSE 'false' END, created_on, modified_on FROM p_site_settings_legacy WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.default_tweet_max_length', default_tweet_max_length::TEXT, created_on, modified_on FROM p_site_settings_legacy WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.tweet_web_ellipsis_size', tweet_web_ellipsis_size::TEXT, created_on, modified_on FROM p_site_settings_legacy WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.tweet_mobile_ellipsis_size', tweet_mobile_ellipsis_size::TEXT, created_on, modified_on FROM p_site_settings_legacy WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.default_tweet_visibility', default_tweet_visibility, created_on, modified_on FROM p_site_settings_legacy WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.default_msg_loop_interval', default_msg_loop_interval::TEXT, created_on, modified_on FROM p_site_settings_legacy WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.copyright_top', copyright_top, created_on, modified_on FROM p_site_settings_legacy WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.copyright_left', copyright_left, created_on, modified_on FROM p_site_settings_legacy WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.copyright_left_link', copyright_left_link, created_on, modified_on FROM p_site_settings_legacy WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.copyright_right', copyright_right, created_on, modified_on FROM p_site_settings_legacy WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.copyright_right_link', copyright_right_link, created_on, modified_on FROM p_site_settings_legacy WHERE id = 1
|
||||
) legacy_rows;
|
||||
|
||||
DROP TABLE p_site_settings_legacy;
|
||||
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS "p_site_settings";
|
||||
@ -0,0 +1,24 @@
|
||||
CREATE TABLE "p_site_settings" (
|
||||
"id" integer NOT NULL DEFAULT 1,
|
||||
"use_friendship" integer NOT NULL DEFAULT 0,
|
||||
"enable_trends_bar" integer NOT NULL DEFAULT 0,
|
||||
"enable_wallet" integer NOT NULL DEFAULT 0,
|
||||
"allow_tweet_attachment" integer NOT NULL DEFAULT 0,
|
||||
"allow_tweet_attachment_price" integer NOT NULL DEFAULT 0,
|
||||
"allow_tweet_video" integer NOT NULL DEFAULT 0,
|
||||
"default_tweet_max_length" integer NOT NULL DEFAULT 2000,
|
||||
"tweet_web_ellipsis_size" integer NOT NULL DEFAULT 400,
|
||||
"tweet_mobile_ellipsis_size" integer NOT NULL DEFAULT 300,
|
||||
"default_tweet_visibility" text(32) NOT NULL DEFAULT 'friend',
|
||||
"default_msg_loop_interval" integer NOT NULL DEFAULT 5000,
|
||||
"copyright_top" text(255) NOT NULL DEFAULT '',
|
||||
"copyright_left" text(255) NOT NULL DEFAULT '',
|
||||
"copyright_left_link" text(255) NOT NULL DEFAULT '',
|
||||
"copyright_right" text(255) NOT NULL DEFAULT '',
|
||||
"copyright_right_link" text(255) NOT NULL DEFAULT '',
|
||||
"created_on" integer NOT NULL DEFAULT 0,
|
||||
"modified_on" integer NOT NULL DEFAULT 0,
|
||||
"deleted_on" integer NOT NULL DEFAULT 0,
|
||||
"is_del" integer NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
@ -0,0 +1,26 @@
|
||||
DROP TABLE IF EXISTS "p_site_settings";
|
||||
|
||||
CREATE TABLE "p_site_settings" (
|
||||
"id" integer NOT NULL DEFAULT 1,
|
||||
"use_friendship" integer NOT NULL DEFAULT 0,
|
||||
"enable_trends_bar" integer NOT NULL DEFAULT 0,
|
||||
"enable_wallet" integer NOT NULL DEFAULT 0,
|
||||
"allow_tweet_attachment" integer NOT NULL DEFAULT 0,
|
||||
"allow_tweet_attachment_price" integer NOT NULL DEFAULT 0,
|
||||
"allow_tweet_video" integer NOT NULL DEFAULT 0,
|
||||
"default_tweet_max_length" integer NOT NULL DEFAULT 2000,
|
||||
"tweet_web_ellipsis_size" integer NOT NULL DEFAULT 400,
|
||||
"tweet_mobile_ellipsis_size" integer NOT NULL DEFAULT 300,
|
||||
"default_tweet_visibility" text(32) NOT NULL DEFAULT 'friend',
|
||||
"default_msg_loop_interval" integer NOT NULL DEFAULT 5000,
|
||||
"copyright_top" text(255) NOT NULL DEFAULT '',
|
||||
"copyright_left" text(255) NOT NULL DEFAULT '',
|
||||
"copyright_left_link" text(255) NOT NULL DEFAULT '',
|
||||
"copyright_right" text(255) NOT NULL DEFAULT '',
|
||||
"copyright_right_link" text(255) NOT NULL DEFAULT '',
|
||||
"created_on" integer NOT NULL DEFAULT 0,
|
||||
"modified_on" integer NOT NULL DEFAULT 0,
|
||||
"deleted_on" integer NOT NULL DEFAULT 0,
|
||||
"is_del" integer NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
@ -0,0 +1,49 @@
|
||||
ALTER TABLE "p_site_settings" RENAME TO "p_site_settings_legacy";
|
||||
|
||||
CREATE TABLE "p_site_settings" (
|
||||
"key" text NOT NULL,
|
||||
"value" text NOT NULL DEFAULT '',
|
||||
"is_encrypted" integer NOT NULL DEFAULT 0,
|
||||
"created_on" integer NOT NULL DEFAULT 0,
|
||||
"modified_on" integer NOT NULL DEFAULT 0,
|
||||
"deleted_on" integer NOT NULL DEFAULT 0,
|
||||
"is_del" integer NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY ("key")
|
||||
);
|
||||
|
||||
INSERT INTO "p_site_settings" ("key", "value", "is_encrypted", "created_on", "modified_on", "deleted_on", "is_del")
|
||||
SELECT "key", "value", 0, created_on, modified_on, 0, 0 FROM (
|
||||
SELECT 'web_profile.use_friendship' AS "key", CASE WHEN use_friendship <> 0 THEN 'true' ELSE 'false' END AS "value", created_on, modified_on FROM "p_site_settings_legacy" WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.enable_trends_bar', CASE WHEN enable_trends_bar <> 0 THEN 'true' ELSE 'false' END, created_on, modified_on FROM "p_site_settings_legacy" WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.enable_wallet', CASE WHEN enable_wallet <> 0 THEN 'true' ELSE 'false' END, created_on, modified_on FROM "p_site_settings_legacy" WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.allow_tweet_attachment', CASE WHEN allow_tweet_attachment <> 0 THEN 'true' ELSE 'false' END, created_on, modified_on FROM "p_site_settings_legacy" WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.allow_tweet_attachment_price', CASE WHEN allow_tweet_attachment_price <> 0 THEN 'true' ELSE 'false' END, created_on, modified_on FROM "p_site_settings_legacy" WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.allow_tweet_video', CASE WHEN allow_tweet_video <> 0 THEN 'true' ELSE 'false' END, created_on, modified_on FROM "p_site_settings_legacy" WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.default_tweet_max_length', CAST(default_tweet_max_length AS TEXT), created_on, modified_on FROM "p_site_settings_legacy" WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.tweet_web_ellipsis_size', CAST(tweet_web_ellipsis_size AS TEXT), created_on, modified_on FROM "p_site_settings_legacy" WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.tweet_mobile_ellipsis_size', CAST(tweet_mobile_ellipsis_size AS TEXT), created_on, modified_on FROM "p_site_settings_legacy" WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.default_tweet_visibility', default_tweet_visibility, created_on, modified_on FROM "p_site_settings_legacy" WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.default_msg_loop_interval', CAST(default_msg_loop_interval AS TEXT), created_on, modified_on FROM "p_site_settings_legacy" WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.copyright_top', copyright_top, created_on, modified_on FROM "p_site_settings_legacy" WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.copyright_left', copyright_left, created_on, modified_on FROM "p_site_settings_legacy" WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.copyright_left_link', copyright_left_link, created_on, modified_on FROM "p_site_settings_legacy" WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.copyright_right', copyright_right, created_on, modified_on FROM "p_site_settings_legacy" WHERE id = 1
|
||||
UNION ALL
|
||||
SELECT 'web_profile.copyright_right_link', copyright_right_link, created_on, modified_on FROM "p_site_settings_legacy" WHERE id = 1
|
||||
);
|
||||
|
||||
DROP TABLE "p_site_settings_legacy";
|
||||
@ -1 +0,0 @@
|
||||
import{A as e,D as t,O as n,Q as r,W as i,w as a}from"./@css-render-YsNuQTaE.js";import{i as o}from"./vue-router-Dw9lfhBK.js";import{$ as s,m as c,x as l}from"./naive-ui-Dv03sO69.js";import"./@juggle-BrKhuOcS.js";import{r as u}from"./sidebar-B2rODElh.js";import{t as d}from"./main-nav-BW6UKmWQ.js";var f=u(e({__name:`404`,setup(e){let u=o(),f=()=>{u.push({path:`/`})};return(e,o)=>{let u=d,p=s,m=c,h=l;return i(),a(`div`,null,[n(u,{title:`404`}),n(h,{class:`main-content-wrap wrap404`,bordered:``},{default:r(()=>[n(m,{status:`404`,title:`404 资源不存在`,description:`再看看其他的吧`},{footer:r(()=>[n(p,{onClick:f},{default:r(()=>[...o[0]||=[t(`回主页`,-1)]]),_:1})]),_:1})]),_:1})])}}}),[[`__scopeId`,`data-v-21b99e62`]]);export{f as default};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1 +0,0 @@
|
||||
import{A as e,Bt as t,C as n,H as r,It as i,K as a,O as o,Q as s,S as c,W as l,h as u,ht as d,lt as f,w as p,x as m}from"./@css-render-YsNuQTaE.js";import{r as h}from"./pinia-TbaIYBWH.js";import{r as g}from"./vue-router-Dw9lfhBK.js";import{X as _,b as v,ot as y,x as b}from"./naive-ui-Dv03sO69.js";import"./@juggle-BrKhuOcS.js";import{l as x,r as S}from"./sidebar-B2rODElh.js";import"./moment-D_9q9veS.js";import{i as C}from"./index-BzZcw6rK.js";import{t as w}from"./main-nav-BW6UKmWQ.js";import{t as T}from"./post-skeleton-CJ8iRGBT.js";var E={key:0,class:`pagination-wrap`},D={key:0,class:`skeleton-wrap`},O={key:1},k={key:0,class:`empty-wrap`},A={class:`bill-line`},j=S(e({__name:`Anouncement`,setup(e){let{collapsedRight:S}=h(x()),j=g(),M=f(!1),N=f([]),P=f(+j.query.p||1),F=f(20),I=f(0),L=e=>{P.value=e};return r(()=>{}),(e,r)=>{let f=w,h=_,g=T,x=y,j=v,R=b;return l(),p(`div`,null,[o(f,{title:`公告`}),o(R,{class:`main-content-wrap`,bordered:``},{footer:s(()=>[I.value>1?(l(),p(`div`,E,[o(h,{page:P.value,"onUpdate:page":L,"page-slot":d(S)?5:8,"page-count":I.value},null,8,[`page`,`page-slot`,`page-count`])])):n(``,!0)]),default:s(()=>[M.value?(l(),p(`div`,D,[o(g,{num:F.value},null,8,[`num`])])):(l(),p(`div`,O,[N.value.length===0?(l(),p(`div`,k,[o(x,{size:`large`,description:`暂无数据`})])):n(``,!0),(l(!0),p(u,null,a(N.value,e=>(l(),c(j,{key:e.id},{default:s(()=>[m(`div`,A,[m(`div`,null,`NO.`+t(e.id),1),m(`div`,null,t(e.reason),1),m(`div`,{class:i({income:e.change_amount>=0,out:e.change_amount<0})},t((e.change_amount>0?`+`:``)+(e.change_amount/100).toFixed(2)),3),m(`div`,null,t(d(C)(e.created_on)),1)])]),_:2},1024))),128))]))]),_:1})])}}}),[[`__scopeId`,`data-v-2279d607`]]);export{j as default};
|
||||
@ -1 +0,0 @@
|
||||
import{o as e}from"./rolldown-runtime-Bhmf7a9N.js";import{A as t,Bt as n,C as r,H as i,K as a,O as o,Q as s,S as c,W as l,h as u,ht as d,lt as f,w as p,x as m}from"./@css-render-YsNuQTaE.js";import{r as h}from"./pinia-TbaIYBWH.js";import{r as g}from"./vue-router-Dw9lfhBK.js";import{P as _,U as v,b as y,f as b,ot as x,x as S}from"./naive-ui-Dv03sO69.js";import"./@juggle-BrKhuOcS.js";import{a as C,c as w,l as T,r as E}from"./sidebar-B2rODElh.js";import"./moment-D_9q9veS.js";import{t as D}from"./main-nav-BW6UKmWQ.js";import{t as O}from"./post-skeleton-CJ8iRGBT.js";import{n as k}from"./useUserAction-DVqObL5J.js";import"./usePostContent-DV6QgefX.js";import"./copy-to-clipboard-qdVIK2zo.js";import{t as A}from"./post-item-iSi0SGnI.js";import"./@vue-cLJXtEPP.js";import{t as j}from"./v3-infinite-loading-Dj1QGmV8.js";var M=e(j()),N={key:0,class:`skeleton-wrap`},P={key:1},F={key:0,class:`empty-wrap`},I={class:`load-more-wrap`},L={class:`load-more-spinner`},R=E(t({__name:`Collection`,setup(e){let t=T(),E=w(),{collapsedRight:j,desktopModelShow:R}=h(t),{userInfo:z}=h(E),B=g();v();let V=f(!1),H=f(!1),U=f([]),W=f(+B.query.p||1),G=f(20),K=f(0),q=f(!1),J=f({id:0,avatar:``,username:``,nickname:``,is_admin:!1,is_friend:!0,is_following:!1,created_on:0,follows:0,followings:0,status:1}),Y=e=>{J.value=e,q.value=!0},X=()=>{q.value=!1};function Z(e,t){for(let n in U.value)U.value[n].user_id==e&&(U.value[n].user.is_following=t)}let Q=()=>{V.value=!0,C.v1.user.get.collections({page:W.value,page_size:G.value}).then(e=>{V.value=!1,e.list.length===0&&(H.value=!0),W.value>1?U.value=U.value.concat(e.list):(U.value=e.list,window.scrollTo(0,0)),K.value=Math.ceil(e.pager.total_rows/G.value)}).catch(e=>{V.value=!1,W.value>1&&W.value--})},$=()=>{W.value<K.value||K.value==0?(H.value=!1,W.value++,Q()):H.value=!0};return i(()=>{Q()}),(e,t)=>{let i=D,f=O,h=x,g=A,v=y,C=k,w=S,T=b,E=_;return l(),p(`div`,null,[o(i,{title:`收藏`}),o(w,{class:`main-content-wrap`,bordered:``},{default:s(()=>[V.value&&U.value.length===0?(l(),p(`div`,N,[o(f,{num:G.value},null,8,[`num`])])):(l(),p(`div`,P,[U.value.length===0?(l(),p(`div`,F,[o(h,{size:`large`,description:`暂无数据`})])):r(``,!0),(l(!0),p(u,null,a(U.value,e=>(l(),c(v,{key:e.id},{default:s(()=>[o(g,{post:e,isOwner:d(z).id==e.user_id,isMobile:!d(R),addFollowAction:``,onSendWhisper:Y,onPostFollowAction:Z},null,8,[`post`,`isOwner`,`isMobile`])]),_:2},1024))),128))])),o(C,{show:q.value,user:J.value,onSuccess:X},null,8,[`show`,`user`])]),_:1}),K.value>0?(l(),c(E,{key:0,justify:`center`},{default:s(()=>[o(d(M.default),{class:`load-more`,slots:{complete:`没有更多收藏了`,error:`加载出错`},onInfinite:$},{spinner:s(()=>[m(`div`,I,[H.value?r(``,!0):(l(),c(T,{key:0,size:14})),m(`span`,L,n(H.value?`没有更多收藏了`:`加载更多`),1)])]),_:1})]),_:1})):r(``,!0)])}}}),[[`__scopeId`,`data-v-c376cb92`]]);export{R as default};
|
||||
@ -1 +0,0 @@
|
||||
import{A as e,C as t,H as n,K as r,O as i,Q as a,S as o,W as s,h as c,ht as l,lt as u,w as d}from"./@css-render-YsNuQTaE.js";import{r as f}from"./vue-router-Dw9lfhBK.js";import{b as p,ot as m,x as h}from"./naive-ui-Dv03sO69.js";import"./@juggle-BrKhuOcS.js";import{a as g,r as _}from"./sidebar-B2rODElh.js";import"./moment-D_9q9veS.js";import{t as v}from"./main-nav-BW6UKmWQ.js";import{t as y}from"./post-skeleton-CJ8iRGBT.js";import{n as b}from"./useUserAction-DVqObL5J.js";import"./@vue-cLJXtEPP.js";import"./v3-infinite-loading-Dj1QGmV8.js";import{t as x}from"./usePagination-B2kaN_Rm.js";import{t as S}from"./infinite-load-more-CuisQVv6.js";import{t as C}from"./user-card-CSodY5hi.js";var w={key:0,class:`skeleton-wrap`},T={key:1},E={key:0,class:`empty-wrap`},D=_(e({__name:`Contacts`,setup(e){let _=f(),{loading:D,noMore:O,page:k,pageSize:A,totalPage:j}=x(20),M=u([]),N=u(!1),P=u({id:0,avatar:``,username:``,nickname:``,is_admin:!1,is_friend:!0,is_following:!1,created_on:0,follows:0,followings:0,status:1});k.value=+_.query.p||1;let F=e=>{P.value=e,N.value=!0},I=()=>{N.value=!1},L=()=>{k.value<j.value||j.value==0?(O.value=!1,k.value++,R()):O.value=!0};n(()=>{R()});let R=(e=!1)=>{M.value.length===0&&(D.value=!0),g.v1.user.get.contacts({page:k.value,page_size:A.value}).then(t=>{D.value=!1,t.list.length===0&&(O.value=!0),k.value>1?M.value=M.value.concat(t.list):(M.value=t.list,e&&setTimeout(()=>{window.scrollTo(0,99999)},50)),j.value=Math.ceil(t.pager.total_rows/A.value)}).catch(e=>{D.value=!1,k.value>1&&k.value--})};return(e,n)=>{let u=v,f=y,g=m,_=p,x=b,k=h;return s(),d(`div`,null,[i(u,{title:`好友`}),i(k,{class:`main-content-wrap`,bordered:``},{default:a(()=>[l(D)&&M.value.length===0?(s(),d(`div`,w,[i(f,{num:l(A)},null,8,[`num`])])):(s(),d(`div`,T,[M.value.length===0?(s(),d(`div`,E,[i(g,{size:`large`,description:`暂无数据`})])):t(``,!0),(s(!0),d(c,null,r(M.value,e=>(s(),o(_,{class:`list-item`,key:e.user_id},{default:a(()=>[i(C,{type:`contact`,contact:e,onSendWhisper:F},null,8,[`contact`])]),_:2},1024))),128))])),i(x,{show:N.value,user:P.value,onSuccess:I},null,8,[`show`,`user`])]),_:1}),i(S,{"total-page":l(j),"no-more":l(O),"complete-text":`没有更多好友了`,onLoadMore:L},null,8,[`total-page`,`no-more`])])}}}),[[`__scopeId`,`data-v-98a9eb10`]]);export{D as default};
|
||||
@ -1 +0,0 @@
|
||||
import{o as e}from"./rolldown-runtime-Bhmf7a9N.js";import{A as t,Bt as n,C as r,H as i,K as a,O as o,Q as s,S as c,W as l,b as u,h as d,ht as f,lt as p,w as m,x as h}from"./@css-render-YsNuQTaE.js";import{r as g}from"./vue-router-Dw9lfhBK.js";import{P as _,b as v,c as ee,f as y,l as b,ot as x,x as S}from"./naive-ui-Dv03sO69.js";import"./@juggle-BrKhuOcS.js";import{a as C,r as w}from"./sidebar-B2rODElh.js";import"./moment-D_9q9veS.js";import{t as T}from"./main-nav-BW6UKmWQ.js";import{t as E}from"./post-skeleton-CJ8iRGBT.js";import{n as D,t as O}from"./useUserAction-DVqObL5J.js";import"./@vue-cLJXtEPP.js";import{t as k}from"./v3-infinite-loading-Dj1QGmV8.js";import{t as A}from"./usePagination-B2kaN_Rm.js";import{t as j}from"./user-card-CSodY5hi.js";var M=e(k()),N={key:0,class:`skeleton-wrap`},P={key:1},F={key:0,class:`empty-wrap`},I={class:`load-more-wrap`},L={class:`load-more-spinner`},R=w(t({__name:`Following`,setup(e){let t=g(),w=p([]),k=t.query.n||`粉丝详情`,R=t.query.s||``,z=p(t.query.t||`follows`);p(!1);let{loading:B,noMore:V,page:H,pageSize:U,totalPage:W,reset:G,nextPage:K}=A(20),{showWhisper:q,whisperReceiver:J,onSendWhisper:Y,whisperSuccess:X}=O.useWhisper();function Z(e){w.value=[],G(),z.value=e}let Q=u(()=>z.value==`follows`?`没有更多关注了`:`没有更多粉丝了`),te=()=>{K($)},ne=e=>{Z(e),$()},$=()=>{z.value===`follows`?ie(R):z.value===`followings`&&ae(R)},re=()=>{z.value===`follows`&&(Z(`follows`),$())},ie=(e,t=!1)=>{w.value.length===0&&(B.value=!0),C.v1.user.get.follows({username:e,page:H.value,page_size:U.value}).then(e=>{B.value=!1,e.list.length===0&&(V.value=!0),H.value>1?w.value=w.value.concat(e.list):(w.value=e.list,t&&setTimeout(()=>{window.scrollTo(0,99999)},50)),W.value=Math.ceil(e.pager.total_rows/U.value)}).catch(e=>{B.value=!1,H.value>1&&H.value--})},ae=(e,t=!1)=>{w.value.length===0&&(B.value=!0),C.v1.user.get.followings({username:e,page:H.value,page_size:U.value}).then(e=>{B.value=!1,e.list.length===0&&(V.value=!0),H.value>1?w.value=w.value.concat(e.list):(w.value=e.list,t&&setTimeout(()=>{window.scrollTo(0,99999)},50)),W.value=Math.ceil(e.pager.total_rows/U.value)}).catch(e=>{B.value=!1,H.value>1&&H.value--})};return i(()=>{$()}),(e,t)=>{let i=T,u=b,p=ee,g=E,C=x,O=v,A=D,R=S,H=y,G=_;return l(),m(d,null,[h(`div`,null,[o(i,{title:f(k),back:!0},null,8,[`title`]),o(R,{class:`main-content-wrap`,bordered:``},{default:s(()=>[o(p,{type:`line`,animated:``,"default-value":z.value,"onUpdate:value":ne},{default:s(()=>[o(u,{name:`follows`,tab:`正在关注`}),o(u,{name:`followings`,tab:`我的粉丝`})]),_:1},8,[`default-value`]),f(B)&&w.value.length===0?(l(),m(`div`,N,[o(g,{num:f(U)},null,8,[`num`])])):(l(),m(`div`,P,[w.value.length===0?(l(),m(`div`,F,[o(C,{size:`large`,description:`暂无数据`})])):r(``,!0),(l(!0),m(d,null,a(w.value,e=>(l(),c(O,{key:e.user_id},{default:s(()=>[o(j,{type:`follow`,contact:e,onSendWhisper:f(Y),onUnfollowSuccess:re},null,8,[`contact`,`onSendWhisper`])]),_:2},1024))),128))])),o(A,{show:f(q),user:f(J),onSuccess:f(X)},null,8,[`show`,`user`,`onSuccess`])]),_:1})]),f(W)>0?(l(),c(G,{key:0,justify:`center`},{default:s(()=>[o(f(M.default),{class:`load-more`,slots:{complete:Q.value,error:`加载出错`},onInfinite:te},{spinner:s(()=>[h(`div`,I,[f(V)?r(``,!0):(l(),c(H,{key:0,size:14})),h(`span`,L,n(f(V)?Q.value:`加载更多`),1)])]),_:1},8,[`slots`])]),_:1})):r(``,!0)],64)}}}),[[`__scopeId`,`data-v-ce50f282`]]);export{R as default};
|
||||
@ -1 +0,0 @@
|
||||
.compose-wrap{box-sizing:border-box;width:100%;padding:16px}.compose-wrap .compose-line{flex-direction:row;display:flex}.compose-wrap .compose-line .compose-user{align-items:center;width:42px;height:42px;display:flex}.compose-wrap .compose-line.compose-options{justify-content:space-between;margin-top:6px;padding-left:42px;display:flex}.compose-wrap .compose-line.compose-options .submit-wrap{align-items:center;display:flex}.compose-wrap .compose-line.compose-options .submit-wrap .text-statistic{width:20px;height:20px;margin-right:8px;transform:rotate(180deg)}.compose-wrap .link-wrap{margin-left:42px;margin-right:42px}.compose-wrap .eye-wrap{margin-left:64px}.compose-wrap .login-only-wrap{justify-content:center;width:100%;display:flex}.compose-wrap .login-only-wrap button{width:50%;margin:0 4px}.compose-wrap .login-wrap{justify-content:center;width:100%;display:flex}.compose-wrap .login-wrap .login-banner{opacity:.8;margin-bottom:12px}.compose-wrap .login-wrap button{margin:0 4px}.attachment-list-wrap{margin-top:12px;margin-left:42px}.attachment-list-wrap .n-upload-file-info__thumbnail{overflow:hidden}.dark .compose-wrap{background-color:#101014bf}.tiny-slide-bar .tiny-slide-bar__list>div.tiny-slide-bar__select .slide-bar-item .slide-bar-item-title[data-v-bb68ff14]{color:#18a058;opacity:.8}.tiny-slide-bar .tiny-slide-bar__list>div:hover .slide-bar-item[data-v-bb68ff14]{cursor:pointer}.tiny-slide-bar .tiny-slide-bar__list>div:hover .slide-bar-item .slide-bar-item-avatar[data-v-bb68ff14],.tiny-slide-bar .tiny-slide-bar__list>div:hover .slide-bar-item .slide-bar-item-title[data-v-bb68ff14]{color:#18a058;opacity:.8}.style-wrap[data-v-bb68ff14]{opacity:.8;margin-top:10px;margin-bottom:4px;margin-left:16px}.style-wrap .style-item.hover[data-v-bb68ff14]{cursor:pointer}.tiny-slide-bar[data-v-bb68ff14]{margin-top:-30px;margin-bottom:-30px}.tiny-slide-bar .slide-bar-item[data-v-bb68ff14]{flex-direction:column;justify-content:center;align-items:center;width:64px;min-height:170px;margin-top:8px;display:flex}.tiny-slide-bar .slide-bar-item .slide-bar-item-title[data-v-bb68ff14]{justify-content:center;height:40px;margin-top:4px;font-size:12px}.load-more[data-v-bb68ff14]{margin:20px}.load-more .load-more-wrap[data-v-bb68ff14]{flex-direction:row;justify-content:center;align-items:center;gap:14px;display:flex}.load-more .load-more-wrap .load-more-spinner[data-v-bb68ff14]{opacity:.65;font-size:14px}.dark .main-content-wrap[data-v-bb68ff14],.dark .pagination-wrap[data-v-bb68ff14],.dark .empty-wrap[data-v-bb68ff14],.dark .skeleton-wrap[data-v-bb68ff14]{background-color:#101014bf}.dark .tiny-slide-bar .tiny-slide-bar__list>div.tiny-slide-bar__select .slide-bar-item .slide-bar-item-title[data-v-bb68ff14],.dark .tiny-slide-bar .tiny-slide-bar__list>div:hover .slide-bar-item .slide-bar-item-title[data-v-bb68ff14]{color:#63e2b7;opacity:.8}.dark .tiny-slide-bar[data-v-bb68ff14]{--ti-slider-progress-box-arrow-hover-text-color:#f2f2f2;--ti-slider-progress-box-arrow-normal-text-color:gray}
|
||||
File diff suppressed because one or more lines are too long
@ -1 +0,0 @@
|
||||
var e=function(e){return e[e.TITLE=1]=`TITLE`,e[e.TEXT=2]=`TEXT`,e[e.IMAGEURL=3]=`IMAGEURL`,e[e.VIDEOURL=4]=`VIDEOURL`,e[e.AUDIOURL=5]=`AUDIOURL`,e[e.LINKURL=6]=`LINKURL`,e[e.ATTACHMENT=7]=`ATTACHMENT`,e[e.CHARGEATTACHMENT=8]=`CHARGEATTACHMENT`,e}({}),t=function(e){return e[e.PUBLIC=0]=`PUBLIC`,e[e.PRIVATE=1]=`PRIVATE`,e[e.FRIEND=2]=`FRIEND`,e[e.Following=3]=`Following`,e}({}),n=function(e){return e[e.NO=0]=`NO`,e[e.YES=1]=`YES`,e}({});export{t as n,n as r,e as t};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1 +0,0 @@
|
||||
import{A as e,Bt as t,C as n,D as r,H as i,J as a,K as o,O as s,Q as c,S as l,T as u,W as d,X as f,b as p,h as m,ht as h,lt as g,w as _}from"./@css-render-YsNuQTaE.js";import{r as v}from"./pinia-TbaIYBWH.js";import{$ as y,G as b,P as x,W as S,at as C,c as w,f as T,l as E,ot as D,s as O,tt as k,x as A}from"./naive-ui-Dv03sO69.js";import"./@juggle-BrKhuOcS.js";import{c as j,l as M,n as N,r as P}from"./sidebar-B2rODElh.js";import{a as F}from"./@vicons-CZeOmejd.js";import{E as I,S as L,d as R,j as z,v as B}from"./index-BzZcw6rK.js";import{t as V}from"./main-nav-BW6UKmWQ.js";var H={key:0,class:`tag-item`},U={key:0,class:`tag-quote`},W={key:1,class:`tag-quote tag-follow`},G={key:0,class:`options`},K=e({__name:`tag-item`,props:{tag:{},showAction:{type:Boolean},checkFollowing:{type:Boolean},checkPin:{type:Boolean}},setup(e){let o=g(!1),u=e,f=p(()=>u.tag.user?u.tag.user.avatar:N),m=p(()=>{let e=[];return u.tag.is_following===0?e.push({label:`关注`,key:`follow`}):(u.tag.is_pin===0?e.push({label:`钉住`,key:`pin`}):e.push({label:`取消钉住`,key:`unpin`}),u.tag.is_top===0?e.push({label:`置顶`,key:`stick`}):e.push({label:`取消置顶`,key:`unstick`}),e.push({label:`取消关注`,key:`unfollow`})),e}),v=e=>{switch(e){case`follow`:R({topic_id:u.tag.id}).then(e=>{u.tag.is_following=1,window.$message.success(`关注成功`)}).catch(e=>{console.log(e)});break;case`unfollow`:z({topic_id:u.tag.id}).then(e=>{u.tag.is_following=0,window.$message.success(`取消关注`)}).catch(e=>{console.log(e)});break;case`pin`:L({topic_id:u.tag.id}).then(e=>{u.tag.is_pin=1,window.$message.success(`钉住成功`)}).catch(e=>{console.log(e)});break;case`unpin`:L({topic_id:u.tag.id}).then(e=>{u.tag.is_pin=0,window.$message.success(`取消钉住`)}).catch(e=>{console.log(e)});break;case`stick`:I({topic_id:u.tag.id}).then(e=>{u.tag.is_top=e.top_status,window.$message.success(`置顶成功`)}).catch(e=>{console.log(e)});break;case`unstick`:I({topic_id:u.tag.id}).then(e=>{u.tag.is_top=e.top_status,window.$message.success(`取消置顶`)}).catch(e=>{console.log(e)});break;default:break}};return i(()=>{o.value=!1}),(i,o)=>{let u=a(`router-link`),p=k,g=C,x=b,w=y,T=S,E=O;return!e.checkFollowing&&!e.checkPin||e.checkFollowing&&e.tag.is_following===1||e.checkPin&&e.tag.is_following===1&&e.tag.is_pin===1?(d(),_(`div`,H,[s(E,null,{header:c(()=>[(d(),l(g,{type:`success`,size:`large`,round:``,key:e.tag.id},{avatar:c(()=>[s(p,{src:f.value},null,8,[`src`])]),default:c(()=>[s(u,{class:`hash-link`,to:{name:`home`,query:{q:e.tag.tag,t:`tag`}}},{default:c(()=>[r(` #`+t(e.tag.tag),1)]),_:1},8,[`to`]),e.showAction?n(``,!0):(d(),_(`span`,U,`(`+t(e.tag.quote_num)+`)`,1)),e.showAction?(d(),_(`span`,W,`(`+t(e.tag.quote_num)+`)`,1)):n(``,!0)]),_:1}))]),"header-extra":c(()=>[e.showAction?(d(),_(`div`,G,[s(T,{placement:`bottom-end`,trigger:`click`,size:`small`,options:m.value,onSelect:v},{default:c(()=>[s(w,{type:`success`,quaternary:``,circle:``,block:``},{icon:c(()=>[s(x,null,{default:c(()=>[s(h(F))]),_:1})]),_:1})]),_:1},8,[`options`])])):n(``,!0)]),_:1})])):n(``,!0)}}}),q={key:0,class:`empty-wrap`},J=P(e({__name:`Topic`,setup(e){let a=M(),{userLogined:y}=v(j()),b=g([]),S=g(`hot`),O=g(!1),k=g(!1),N=g(!1),P=g(!1);f(k,()=>{k.value||(window.$message.success(`保存成功`),a.doRefreshTopicFollow())});let F=p({get:()=>{let e=`编辑`;return k.value&&(e=`保存`),e},set:e=>{}}),I=()=>{O.value=!0,B({type:S.value,num:50}).then(e=>{b.value=e.topics,O.value=!1}).catch(e=>{b.value=[],console.log(e),O.value=!1})},L=e=>{S.value=e,N.value=e===`follow`,P.value=e===`pin`,I()};return i(()=>{I()}),(e,i)=>{let a=V,f=E,p=C,g=w,v=K,S=x,j=D,M=T,I=A;return d(),_(`div`,null,[s(a,{title:`话题`}),s(I,{class:`main-content-wrap tags-wrap`,bordered:``},{default:c(()=>[s(g,{type:`line`,animated:``,"onUpdate:value":L},u({default:c(()=>[s(f,{name:`hot`,tab:`热门`}),s(f,{name:`new`,tab:`最新`}),h(y)?(d(),l(f,{key:0,name:`follow`,tab:`关注`})):n(``,!0),h(y)?(d(),l(f,{key:1,name:`pin`,tab:`钉住`})):n(``,!0)]),_:2},[h(y)?{name:`suffix`,fn:c(()=>[s(p,{checked:k.value,"onUpdate:checked":i[0]||=e=>k.value=e,checkable:``},{default:c(()=>[r(t(F.value),1)]),_:1},8,[`checked`])]),key:`0`}:void 0]),1024),s(M,{show:O.value},{default:c(()=>[s(S,null,{default:c(()=>[(d(!0),_(m,null,o(b.value,e=>(d(),l(v,{tag:e,showAction:h(y)&&k.value,checkFollowing:N.value,checkPin:P.value},null,8,[`tag`,`showAction`,`checkFollowing`,`checkPin`]))),256))]),_:1}),b.value.length===0?(d(),_(`div`,q,[s(j,{size:`large`,description:`暂无数据`})])):n(``,!0)]),_:1},8,[`show`])]),_:1})])}}}),[[`__scopeId`,`data-v-5410fcc5`]]);export{J as default};
|
||||
File diff suppressed because one or more lines are too long
@ -1 +0,0 @@
|
||||
import{o as e}from"./rolldown-runtime-Bhmf7a9N.js";import{$ as t,A as n,Bt as r,C as i,D as a,H as o,It as ee,K as s,O as c,Q as l,S as u,W as d,d as f,h as p,ht as m,lt as h,p as te,w as g,x as _}from"./@css-render-YsNuQTaE.js";import{r as v}from"./pinia-TbaIYBWH.js";import{r as y}from"./vue-router-Dw9lfhBK.js";import{$ as ne,G as re,H as ie,P as ae,Q as oe,X as se,_ as b,b as x,d as ce,et as le,ot as ue,x as de}from"./naive-ui-Dv03sO69.js";import"./@juggle-BrKhuOcS.js";import{a as S,c as C,l as w,r as T}from"./sidebar-B2rODElh.js";import{M as fe}from"./@vicons-CZeOmejd.js";import"./moment-D_9q9veS.js";import{N as E,i as pe}from"./index-BzZcw6rK.js";import{t as D}from"./main-nav-BW6UKmWQ.js";import{t as O}from"./post-skeleton-CJ8iRGBT.js";import{t as k}from"./qrcode-DSuf5wHF.js";import"./dijkstrajs-B3GuVsqA.js";var A=e(k()),j={class:`balance-wrap`},M={class:`balance-line`},N={class:`balance-opts`},P={key:0,class:`pagination-wrap`},F={key:0,class:`skeleton-wrap`},I={key:1},L={key:0,class:`empty-wrap`},R={class:`bill-line`},z={key:0,class:`amount-options`},B={key:1,style:{"margin-top":`10px`}},me={class:`qrcode-wrap`},he={class:`pay-tips`},ge={class:`pay-sub-tips`},V=T(n({__name:`Wallet`,setup(e){let n=w(),T=C(),{collapsedRight:k}=v(n),{userInfo:V}=v(T),H=y(),U=h(!1),W=h(100),G=h(!1),K=h(``),q=h(!1),J=h([]),Y=h(+H.query.p||1),X=h(20),Z=h(0),_e=h([100,200,300,500,1e3,3e3,5e3,1e4,5e4]),Q=()=>{q.value=!0,S.v1.user.get.wallet.bills({page:Y.value,page_size:X.value}).then(e=>{q.value=!1,J.value=e.list,Z.value=Math.ceil(e.pager.total_rows/X.value),window.scrollTo(0,0)}).catch(e=>{q.value=!1})},ve=e=>{Y.value=e,Q()},$=()=>{let e=localStorage.getItem(`PAOPAO_TOKEN`)||``;e?E(e).then(e=>{T.updateUserinfo(e),n.triggerAuth(!1),Q()}).catch(e=>{n.triggerAuth(!0),T.userLogout()}):(n.triggerAuth(!0),T.userLogout())},ye=()=>{U.value=!0},be=e=>{G.value=!0,S.v1.user.post.recharge({amount:W.value}).then(e=>{G.value=!1,K.value=e.pay,A.toCanvas(document.querySelector(`#qrcode-container`),e.pay,{width:150,margin:2});let t=setInterval(()=>{S.v1.user.get.recharge({id:e.id}).then(e=>{e.status===`TRADE_SUCCESS`&&(clearInterval(t),window.$message.success(`充值成功`),U.value=!1,K.value=``,$())}).catch(e=>{console.log(e)})},2e3)}).catch(e=>{G.value=!1})},xe=()=>{V.value.balance==0?window.$message.warning(`您暂无可提现资金`):window.$message.warning(`该功能即将开放`)};return o(()=>{$()}),(e,n)=>{let o=D,h=b,v=ce,y=ne,S=ae,C=se,w=O,T=ue,E=x,A=de,H=re,Q=le,$=oe,Se=ie;return d(),g(`div`,null,[c(o,{title:`钱包`}),c(A,{class:`main-content-wrap`,bordered:``},{footer:l(()=>[Z.value>1?(d(),g(`div`,P,[c(C,{page:Y.value,"onUpdate:page":ve,"page-slot":m(k)?5:8,"page-count":Z.value},null,8,[`page`,`page-slot`,`page-count`])])):i(``,!0)]),default:l(()=>[_(`div`,j,[_(`div`,M,[c(v,{label:`账户余额 (元)`},{default:l(()=>[c(h,{from:0,to:(m(V).balance||0)/100,duration:500,precision:2},null,8,[`to`])]),_:1}),_(`div`,N,[c(S,{vertical:``},{default:l(()=>[c(y,{size:`small`,secondary:``,type:`primary`,onClick:ye},{default:l(()=>[...n[1]||=[a(` 充值 `,-1)]]),_:1}),c(y,{size:`small`,secondary:``,type:`tertiary`,onClick:xe},{default:l(()=>[...n[2]||=[a(` 提现 `,-1)]]),_:1})]),_:1})])])]),q.value?(d(),g(`div`,F,[c(w,{num:X.value},null,8,[`num`])])):(d(),g(`div`,I,[J.value.length===0?(d(),g(`div`,L,[c(T,{size:`large`,description:`暂无数据`})])):i(``,!0),(d(!0),g(p,null,s(J.value,e=>(d(),u(E,{key:e.id},{default:l(()=>[_(`div`,R,[_(`div`,null,`NO.`+r(e.id),1),_(`div`,null,r(e.reason),1),_(`div`,{class:ee({income:e.change_amount>=0,out:e.change_amount<0})},r((e.change_amount>0?`+`:``)+(e.change_amount/100).toFixed(2)),3),_(`div`,null,r(m(pe)(e.created_on)),1)])]),_:2},1024))),128))]))]),_:1}),c(Se,{show:U.value,"onUpdate:show":n[0]||=e=>U.value=e},{default:l(()=>[c($,{bordered:!1,title:`请选择充值金额`,role:`dialog`,"aria-modal":`true`,style:{width:`100%`,"max-width":`330px`}},{default:l(()=>[K.value.length===0?(d(),g(`div`,z,[c(S,{align:`baseline`},{default:l(()=>[(d(!0),g(p,null,s(_e.value,e=>(d(),u(y,{key:e,size:`small`,secondary:``,type:W.value===e?`info`:`default`,onClick:te(t=>W.value=e,[`stop`])},{default:l(()=>[a(r(e/100)+`元 `,1)]),_:2},1032,[`type`,`onClick`]))),128))]),_:1})])):i(``,!0),W.value>0&&K.value.length===0?(d(),g(`div`,B,[c(y,{loading:G.value,strong:``,secondary:``,type:`info`,style:{width:`100%`},onClick:be},{icon:l(()=>[c(H,null,{default:l(()=>[c(m(fe))]),_:1})]),default:l(()=>[n[3]||=a(` 前往支付 `,-1)]),_:1},8,[`loading`])])):i(``,!0),t(_(`div`,me,[n[5]||=_(`canvas`,{id:`qrcode-container`},null,-1),_(`div`,he,` 请使用支付宝扫码支付`+r((W.value/100).toFixed(2))+`元 `,1),_(`div`,ge,[c(Q,{value:100,type:`info`,dot:``,processing:``}),n[4]||=_(`span`,{style:{"margin-left":`6px`}},` 支付结果实时同步中... `,-1)])],512),[[f,K.value.length>0]])]),_:1})]),_:1},8,[`show`])])}}}),[[`__scopeId`,`data-v-f2cb897a`]]);export{V as default};
|
||||
File diff suppressed because one or more lines are too long
@ -1 +0,0 @@
|
||||
import{t as e}from"./rolldown-runtime-Bhmf7a9N.js";var t=e(((e,t)=>{t.exports=function(){var e=document.getSelection();if(!e.rangeCount)return function(){};for(var t=document.activeElement,n=[],r=0;r<e.rangeCount;r++)n.push(e.getRangeAt(r));switch(t.tagName.toUpperCase()){case`INPUT`:case`TEXTAREA`:t.blur();break;default:t=null;break}return e.removeAllRanges(),function(){e.type===`Caret`&&e.removeAllRanges(),e.rangeCount||n.forEach(function(t){e.addRange(t)}),t&&t.focus()}}})),n=e(((e,n)=>{var r=t(),i={"text/plain":`Text`,"text/html":`Url`,default:`Text`},a=`Copy to clipboard: #{key}, Enter`;function o(e){var t=(/mac os x/i.test(navigator.userAgent)?`⌘`:`Ctrl`)+`+C`;return e.replace(/#{\s*key\s*}/g,t)}function s(e,t){var n,s,c,l,u,d,f=!1;t||={},n=t.debug||!1;try{if(c=r(),l=document.createRange(),u=document.getSelection(),d=document.createElement(`span`),d.textContent=e,d.ariaHidden=`true`,d.style.all=`unset`,d.style.position=`fixed`,d.style.top=0,d.style.clip=`rect(0, 0, 0, 0)`,d.style.whiteSpace=`pre`,d.style.webkitUserSelect=`text`,d.style.MozUserSelect=`text`,d.style.msUserSelect=`text`,d.style.userSelect=`text`,d.addEventListener(`copy`,function(r){if(r.stopPropagation(),t.format)if(r.preventDefault(),r.clipboardData===void 0){n&&console.warn(`unable to use e.clipboardData`),n&&console.warn(`trying IE specific stuff`),window.clipboardData.clearData();var a=i[t.format]||i.default;window.clipboardData.setData(a,e)}else r.clipboardData.clearData(),r.clipboardData.setData(t.format,e);t.onCopy&&(r.preventDefault(),t.onCopy(r.clipboardData))}),document.body.appendChild(d),l.selectNodeContents(d),u.addRange(l),!document.execCommand(`copy`))throw Error(`copy command was unsuccessful`);f=!0}catch(r){n&&console.error(`unable to copy using execCommand: `,r),n&&console.warn(`trying IE specific stuff`);try{window.clipboardData.setData(t.format||`text`,e),t.onCopy&&t.onCopy(window.clipboardData),f=!0}catch(r){n&&console.error(`unable to copy using clipboardData: `,r),n&&console.error(`falling back to prompt`),s=o(`message`in t?t.message:a),window.prompt(s,e)}}finally{u&&(typeof u.removeRange==`function`?u.removeRange(l):u.removeAllRanges()),d&&document.body.removeChild(d),c()}return f}n.exports=s}));export{n as t};
|
||||
@ -1 +0,0 @@
|
||||
var e=e=>e>=1e3?(e/1e3).toFixed(1)+`千`:e>=1e4?(e/1e4).toFixed(1)+`万`:e;export{e as t};
|
||||
@ -1 +0,0 @@
|
||||
import{t as e}from"./rolldown-runtime-Bhmf7a9N.js";var t=e(((e,t)=>{var n={single_source_shortest_paths:function(e,t,r){var i={},a={};a[t]=0;var o=n.PriorityQueue.make();o.push(t,0);for(var s,c,l,u,d,f,p,m,h;!o.empty();)for(l in s=o.pop(),c=s.value,u=s.cost,d=e[c]||{},d)d.hasOwnProperty(l)&&(f=d[l],p=u+f,m=a[l],h=a[l]===void 0,(h||m>p)&&(a[l]=p,o.push(l,p),i[l]=c));if(r!==void 0&&a[r]===void 0){var g=[`Could not find a path from `,t,` to `,r,`.`].join(``);throw Error(g)}return i},extract_shortest_path_from_predecessor_list:function(e,t){for(var n=[],r=t;r;)n.push(r),e[r],r=e[r];return n.reverse(),n},find_path:function(e,t,r){var i=n.single_source_shortest_paths(e,t,r);return n.extract_shortest_path_from_predecessor_list(i,r)},PriorityQueue:{make:function(e){var t=n.PriorityQueue,r={},i;for(i in e||={},t)t.hasOwnProperty(i)&&(r[i]=t[i]);return r.queue=[],r.sorter=e.sorter||t.default_sorter,r},default_sorter:function(e,t){return e.cost-t.cost},push:function(e,t){var n={value:e,cost:t};this.queue.push(n),this.queue.sort(this.sorter)},pop:function(){return this.queue.shift()},empty:function(){return this.queue.length===0}}};t!==void 0&&(t.exports=n)}));export{t};
|
||||
File diff suppressed because one or more lines are too long
@ -1 +0,0 @@
|
||||
import{o as e}from"./rolldown-runtime-Bhmf7a9N.js";import{A as t,Bt as n,C as r,O as i,Q as a,S as o,W as s,ht as c,x as l}from"./@css-render-YsNuQTaE.js";import{P as u,f as d}from"./naive-ui-Dv03sO69.js";import{r as f}from"./sidebar-B2rODElh.js";import{t as p}from"./v3-infinite-loading-Dj1QGmV8.js";var m=e(p()),h={class:`load-more-wrap`},g={class:`load-more-spinner`},_=f(t({__name:`infinite-load-more`,props:{totalPage:{},noMore:{type:Boolean},completeText:{default:`没有更多了`}},emits:[`load-more`],setup(e,{emit:t}){let f=t,p=()=>{f(`load-more`)};return(t,f)=>{let _=d,v=u;return e.totalPage>0?(s(),o(v,{key:0,justify:`center`},{default:a(()=>[i(c(m.default),{class:`load-more`,slots:{complete:e.completeText,error:`加载出错`},onInfinite:p},{spinner:a(()=>[l(`div`,h,[e.noMore?r(``,!0):(s(),o(_,{key:0,size:14})),l(`span`,g,n(e.noMore?e.completeText:`加载更多`),1)])]),_:1},8,[`slots`])]),_:1})):r(``,!0)}}}),[[`__scopeId`,`data-v-fadcc2b3`]]);export{_ as t};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1 +0,0 @@
|
||||
import{A as e,Bt as t,C as n,D as r,H as i,O as a,Q as o,S as s,W as c,h as l,ht as u,lt as d,w as f,x as p}from"./@css-render-YsNuQTaE.js";import{r as m}from"./pinia-TbaIYBWH.js";import{i as h}from"./vue-router-Dw9lfhBK.js";import{$ as g,G as _,I as v,L as y,Q as b,u as x,z as S}from"./naive-ui-Dv03sO69.js";import{t as C}from"./vooks-C54Sdpwj.js";import{l as w,t as T}from"./sidebar-B2rODElh.js";import{c as E,l as D,s as O,u as k}from"./@vicons-CZeOmejd.js";var A={key:0},j={class:`navbar`},M=e({__name:`main-nav`,props:{title:{default:``},back:{type:Boolean,default:!1},theme:{type:Boolean,default:!0}},setup(e){let M=w(),{desktopModelShow:N,drawerModelShow:P,theme:F}=m(M),I=h(),L=d(!1),R=d(`left`),z=e,B=e=>{e?(localStorage.setItem(`PAOPAO_THEME`,`dark`),M.triggerTheme(`dark`)):(localStorage.setItem(`PAOPAO_THEME`,`light`),M.triggerTheme(`light`))},V=()=>{window.history.length<=1?I.push({path:`/`}):I.go(-1)},H=()=>{L.value=!0};return i(()=>{localStorage.getItem(`PAOPAO_THEME`)||B(C()===`dark`),N.value||(window.$message=S())}),(i,d)=>{let m=T,h=v,S=y,C=_,w=g,M=x,N=b;return c(),f(l,null,[u(P)?(c(),f(`div`,A,[a(S,{show:L.value,"onUpdate:show":d[0]||=e=>L.value=e,width:212,placement:R.value,resizable:``},{default:o(()=>[a(h,null,{default:o(()=>[a(m)]),_:1})]),_:1},8,[`show`,`placement`])])):n(``,!0),a(N,{size:`small`,bordered:!0,class:`nav-title-card`},{header:o(()=>[p(`div`,j,[u(P)&&!e.back?(c(),s(w,{key:0,class:`drawer-btn`,onClick:H,quaternary:``,circle:``,size:`medium`},{icon:o(()=>[a(C,null,{default:o(()=>[a(u(E))]),_:1})]),_:1})):n(``,!0),e.back?(c(),s(w,{key:1,class:`back-btn`,onClick:V,quaternary:``,circle:``,size:`small`},{icon:o(()=>[a(C,null,{default:o(()=>[a(u(k))]),_:1})]),_:1})):n(``,!0),r(` `+t(z.title)+` `,1),z.theme?(c(),s(M,{key:2,value:u(F)===`dark`,"onUpdate:value":B,size:`small`,class:`theme-switch-wrap`},{"checked-icon":o(()=>[a(C,{component:u(D)},null,8,[`component`])]),"unchecked-icon":o(()=>[a(C,{component:u(O)},null,8,[`component`])]),_:1},8,[`value`])):n(``,!0)])]),_:1})],64)}}});export{M as t};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1 +0,0 @@
|
||||
import{A as e,K as t,O as n,W as r,h as i,w as a,x as o}from"./@css-render-YsNuQTaE.js";import{p as s}from"./naive-ui-Dv03sO69.js";import{r as c}from"./sidebar-B2rODElh.js";var l={class:`user`},u={class:`content`},d=c(e({__name:`post-skeleton`,props:{num:{default:1}},setup(e){return(c,d)=>{let f=s;return r(!0),a(i,null,t(Array(e.num),e=>(r(),a(`div`,{class:`skeleton-item`,key:e},[o(`div`,l,[n(f,{circle:``,size:`small`})]),o(`div`,u,[n(f,{text:``,repeat:3}),n(f,{text:``,style:{width:`60%`}})])]))),128)}}}),[[`__scopeId`,`data-v-c4e511bd`]]);export{d as t};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1 +0,0 @@
|
||||
import{lt as e}from"./@css-render-YsNuQTaE.js";function t(t=20){let n=e(!1),r=e(!1),i=e(1),a=e(t),o=e(0);return{loading:n,noMore:r,page:i,pageSize:a,totalPage:o,reset:()=>{n.value=!1,r.value=!1,i.value=1,o.value=0},nextPage:e=>{i.value<o.value||o.value==0?(r.value=!1,i.value++,e()):r.value=!0}}}export{t};
|
||||
File diff suppressed because one or more lines are too long
@ -1 +0,0 @@
|
||||
import{A as e,Bt as t,D as n,O as r,Q as i,S as a,W as o,lt as s,x as c}from"./@css-render-YsNuQTaE.js";import{$ as l,E as u,H as d,K as f,it as p,rt as m}from"./naive-ui-Dv03sO69.js";import{a as h,r as g}from"./sidebar-B2rODElh.js";var _={class:`whisper-wrap`},v={class:`whisper-line`},y={class:`whisper-line send-wrap`},b=g(e({__name:`whisper`,props:{show:{type:Boolean,default:!1},user:{}},emits:[`success`],setup(e,{emit:g}){let b=e,x=s(``),S=s(!1),C=g,w=()=>{C(`success`)},T=()=>{S.value=!0,h.v1.user.post.whisper({user_id:b.user.id,content:x.value}).then(e=>{window.$message.success(`发送成功`),S.value=!1,x.value=``,w()}).catch(e=>{S.value=!1})};return(s,h)=>{let g=u,b=f,C=p,E=m,D=l,O=d;return o(),a(O,{show:e.show,"onUpdate:show":w,class:`whisper-card`,preset:`card`,size:`small`,title:`私信`,"mask-closable":!1,bordered:!1,style:{width:`360px`}},{default:i(()=>[c(`div`,_,[r(C,{"show-icon":!1},{default:i(()=>[h[1]||=n(` 即将发送私信给: `,-1),r(b,{style:{"max-width":`100%`}},{default:i(()=>[r(g,{type:`success`},{default:i(()=>[n(t(e.user.nickname)+`@`+t(e.user.username),1)]),_:1})]),_:1})]),_:1}),c(`div`,v,[r(E,{type:`textarea`,placeholder:`请输入私信内容(请勿发送不和谐内容,否则将会被封号)`,autosize:{minRows:5,maxRows:10},value:x.value,"onUpdate:value":h[0]||=e=>x.value=e,maxlength:`200`,"show-count":``},null,8,[`value`])]),c(`div`,y,[r(D,{strong:``,secondary:``,type:`primary`,loading:S.value,onClick:T},{default:i(()=>[...h[2]||=[n(` 发送 `,-1)]]),_:1},8,[`loading`])])])]),_:1},8,[`show`])}}}),[[`__scopeId`,`data-v-2a2273dc`]]),x=class{static followAction(e,t,n,r){return new Promise((i,a)=>{e.success({title:`提示`,content:`确定`+(r?`取消关注 @`:`关注 @`)+n+` 吗?`,positiveText:`确定`,negativeText:`取消`,onPositiveClick:()=>{r?h.v1.user.post.unfollow({user_id:t}).then(e=>{window.$message.success(`操作成功`),i(!1)}).catch(e=>{a(e)}):h.v1.user.post.follow({user_id:t}).then(e=>{window.$message.success(`关注成功`),i(!0)}).catch(e=>{a(e)})}})})}static useWhisper(){let e=s(!1),t=s({id:0,avatar:``,username:``,nickname:``,is_admin:!1,is_friend:!0,is_following:!1,created_on:0,follows:0,followings:0,status:1});return{showWhisper:e,whisperReceiver:t,onSendWhisper:n=>{t.value=n,e.value=!0},whisperSuccess:()=>{e.value=!1}}}};export{b as n,x as t};
|
||||
@ -1 +0,0 @@
|
||||
import{A as e,Bt as t,C as n,D as r,J as i,M as a,O as o,Q as s,S as c,W as l,b as u,ht as d,p as f,w as p,x as m}from"./@css-render-YsNuQTaE.js";import{$ as h,G as g,U as _,W as v,at as y,s as b,tt as x}from"./naive-ui-Dv03sO69.js";import{r as S}from"./sidebar-B2rODElh.js";import{_ as C,at as w,k as T,o as E}from"./@vicons-CZeOmejd.js";import{t as D}from"./index-BzZcw6rK.js";import{t as O}from"./useUserAction-DVqObL5J.js";var k={class:`user-card`},A={class:`nickname-wrap`},j={class:`username-wrap`},M={class:`user-info`},N={class:`info-item`},P={class:`info-item`},F={class:`item-header-extra`},I=S(e({__name:`user-card`,props:{contact:{},type:{default:`contact`}},emits:[`send-whisper`,`unfollow-success`],setup(e,{emit:S}){let I=_(),L=e,R=u(()=>L.type===`follow`),z=u(()=>L.type===`follow`),B=S,V=e=>()=>a(g,null,{default:()=>a(e)}),H=()=>{let e=L.contact.is_following;O.followAction(I,L.contact.user_id,L.contact.username,L.contact.is_following).then(t=>{L.contact.is_following=t,e&&!t&&B(`unfollow-success`)}).catch(e=>{console.log(e)})},U=u(()=>{let e=[{label:`私信 @`+L.contact.username,key:`whisper`,icon:V(T)}];return z.value&&(L.contact.is_following?e.push({label:`取消关注 @`+L.contact.username,key:`unfollow`,icon:V(C)}):e.push({label:`关注 @`+L.contact.username,key:`follow`,icon:V(w)})),e}),W=e=>{switch(e){case`follow`:case`unfollow`:H();break;case`whisper`:B(`send-whisper`,{id:L.contact.user_id,avatar:L.contact.avatar,username:L.contact.username,nickname:L.contact.nickname,is_admin:!1,is_friend:!0,is_following:!1,created_on:0,follows:0,followings:0,status:1});break;default:break}};return(a,u)=>{let _=x,S=i(`router-link`),C=y,w=h,T=v,O=b;return l(),p(`div`,k,[o(O,{"content-indented":``},{avatar:s(()=>[o(_,{size:54,src:e.contact.avatar},null,8,[`src`])]),header:s(()=>[m(`span`,A,[o(S,{onClick:u[0]||=f(()=>{},[`stop`]),class:`username-link`,to:{name:`user`,query:{s:e.contact.username}}},{default:s(()=>[r(t(e.contact.nickname),1)]),_:1},8,[`to`])]),m(`span`,j,` @`+t(e.contact.username),1),R.value&&e.contact.is_following?(l(),c(C,{key:0,class:`top-tag`,type:`success`,size:`small`,round:``},{default:s(()=>[...u[1]||=[r(` 已关注 `,-1)]]),_:1})):n(``,!0),m(`div`,M,[m(`span`,N,` UID. `+t(e.contact.user_id),1),m(`span`,P,t(d(D)(e.contact.created_on))+`\xA0加入 `,1)])]),"header-extra":s(()=>[m(`div`,F,[o(T,{placement:`bottom-end`,trigger:`click`,size:`small`,options:U.value,onSelect:W},{default:s(()=>[o(w,{quaternary:``,circle:``},{icon:s(()=>[o(d(g),null,{default:s(()=>[o(d(E))]),_:1})]),_:1})]),_:1},8,[`options`])])]),_:1})])}}}),[[`__scopeId`,`data-v-f1712b5f`]]);export{I as t};
|
||||
@ -1 +0,0 @@
|
||||
import{a as e,t}from"./rolldown-runtime-Bhmf7a9N.js";import{Ct as n,l as r,s as i,zt as a}from"./@css-render-YsNuQTaE.js";import{n as o,t as s}from"./@vue-cLJXtEPP.js";var c=t((t=>{Object.defineProperty(t,`__esModule`,{value:!0});var c=(o(),e(s)),l=(i(),e(r)),u=(n(),e(a));function d(e){var t=Object.create(null);if(e)for(var n in e)t[n]=e[n];return t.default=e,Object.freeze(t)}var f=d(l),p=Object.create(null);function m(e,t){if(!u.isString(e))if(e.nodeType)e=e.innerHTML;else return u.NOOP;let n=u.genCacheKey(e,t),r=p[n];if(r)return r;if(e[0]===`#`){let t=document.querySelector(e);e=t?t.innerHTML:``}let i=u.extend({hoistStatic:!0,onError:void 0,onWarn:u.NOOP},t);!i.isCustomElement&&typeof customElements<`u`&&(i.isCustomElement=e=>!!customElements.get(e));let{code:a}=c.compile(e,i),o=Function(`Vue`,a)(f);return o._rc=!0,p[n]=o}l.registerRuntimeCompiler(m),t.compile=m,Object.keys(l).forEach(function(e){e!==`default`&&!Object.prototype.hasOwnProperty.call(t,e)&&(t[e]=l[e])})})),l=t(((e,t)=>{t.exports=c()})),u=t(((e,t)=>{(function(n,r){typeof e==`object`&&typeof t<`u`?r(e,l()):typeof define==`function`&&define.amd?define([`exports`,`vue`],r):(n=typeof globalThis<`u`?globalThis:n||self,r(n.V3InfiniteLoading={},n.Vue))})(e,function(e,t){function n(e,t=null){if(!e)return!1;let n=e.getBoundingClientRect(),r=t?t.getBoundingClientRect():{top:0,left:0,bottom:window.innerHeight,right:window.innerWidth};return n.bottom>=r.top&&n.top<=r.bottom&&n.right>=r.left&&n.left<=r.right}async function r(e){return e?(await t.nextTick(),e.value instanceof HTMLElement?e.value:e.value?document.querySelector(e.value):null):null}function i(e){let t=`0px 0px ${e.distance}px 0px`;e.top&&(t=`${e.distance}px 0px 0px 0px`);let n=new IntersectionObserver(t=>{t[0].isIntersecting&&(e.firstload&&e.emit(),e.firstload=!0)},{root:e.parentEl,rootMargin:t});return e.infiniteLoading.value&&n.observe(e.infiniteLoading.value),n}async function a(e,n){if(await t.nextTick(),!e.top)return;let r=e.parentEl||document.documentElement;r.scrollTop=r.scrollHeight-n}let o=(e,t)=>{let n=e.__vccOpts||e;for(let[e,r]of t)n[e]=r;return n},s={},c=e=>(t.pushScopeId(`data-v-d3e37633`),e=e(),t.popScopeId(),e),l={class:`container`},u=[c(()=>t.createElementVNode(`div`,{class:`spinner`},null,-1))];function d(e,n){return t.openBlock(),t.createElementBlock(`div`,l,u)}let f=o(s,[[`render`,d],[`__scopeId`,`data-v-d3e37633`]]),p={class:`state-error`};e.default=o(t.defineComponent({__name:`InfiniteLoading`,props:{top:{type:Boolean,default:!1},target:{},distance:{default:0},identifier:{},firstload:{type:Boolean,default:!0},slots:{}},emits:[`infinite`],setup(e,{emit:o}){let s=e,c=null,l=0,u=t.ref(null),d=t.ref(``),{top:m,firstload:h,distance:g}=s,{identifier:_,target:v}=t.toRefs(s),y={infiniteLoading:u,top:m,firstload:h,distance:g,parentEl:null,emit(){l=(y.parentEl||document.documentElement).scrollHeight,b.loading(),o(`infinite`,b)}},b={loading(){d.value=`loading`},async loaded(){d.value=`loaded`,await a(y,l),n(u.value,y.parentEl)&&y.emit()},async complete(){d.value=`complete`,await a(y,l),c?.disconnect()},error(){d.value=`error`}};function x(){c?.disconnect(),c=i(y)}return t.watch(_,x),t.onMounted(async()=>{y.parentEl=await r(v),x()}),t.onUnmounted(()=>c?.disconnect()),(e,n)=>(t.openBlock(),t.createElementBlock(`div`,{ref_key:`infiniteLoading`,ref:u,class:`v3-infinite-loading`},[t.withDirectives(t.createElementVNode(`div`,null,[t.renderSlot(e.$slots,`spinner`,{},()=>[t.createVNode(f)],!0)],512),[[t.vShow,d.value==`loading`]]),d.value==`complete`?t.renderSlot(e.$slots,`complete`,{key:0},()=>[t.createElementVNode(`span`,null,t.toDisplayString(e.slots?.complete||`No more results!`),1)],!0):t.createCommentVNode(``,!0),d.value==`error`?t.renderSlot(e.$slots,`error`,{key:1,retry:y.emit},()=>[t.createElementVNode(`span`,p,[t.createElementVNode(`span`,null,t.toDisplayString(e.slots?.error||`Oops something went wrong!`),1),t.createElementVNode(`button`,{class:`retry`,onClick:n[0]||=(...e)=>y.emit&&y.emit(...e)},`retry`)])],!0):t.createCommentVNode(``,!0)],512))}}),[[`__scopeId`,`data-v-4bdee133`]]),Object.defineProperties(e,{__esModule:{value:!0},[Symbol.toStringTag]:{value:`Module`}})})}));export{u as t};
|
||||
@ -1 +0,0 @@
|
||||
import{B as e,ct as t,lt as n,z as r}from"./@css-render-YsNuQTaE.js";import{st as i}from"./naive-ui-Dv03sO69.js";var a=0,o=typeof window<`u`&&window.matchMedia!==void 0,s=n(null),c,l;function u(e){e.matches&&(s.value=`dark`)}function d(e){e.matches&&(s.value=`light`)}function f(){c=window.matchMedia(`(prefers-color-scheme: dark)`),l=window.matchMedia(`(prefers-color-scheme: light)`),c.matches?s.value=`dark`:l.matches?s.value=`light`:s.value=null,c.addEventListener?(c.addEventListener(`change`,u),l.addEventListener(`change`,d)):c.addListener&&(c.addListener(u),l.addListener(d))}function p(){`removeEventListener`in c?(c.removeEventListener(`change`,u),l.removeEventListener(`change`,d)):`removeListener`in c&&(c.removeListener(u),l.removeListener(d)),c=void 0,l=void 0}var m=!0;function h(){return o?(a===0&&f(),(m&&=i())&&(r(()=>{a+=1}),e(()=>{--a,a===0&&p()})),t(s)):t(s)}export{h as t};
|
||||
File diff suppressed because one or more lines are too long
@ -1 +0,0 @@
|
||||
import{A as e,Bt as t,D as n,O as r,Q as i,S as a,W as o,lt as s,x as c}from"./@css-render-YsNuQTaE.js";import{$ as l,E as u,H as d,K as f,it as p,rt as m}from"./naive-ui-Dv03sO69.js";import{a as h,r as g}from"./sidebar-B2rODElh.js";var _={class:`whisper-wrap`},v={class:`whisper-line`},y={class:`whisper-line send-wrap`},b=g(e({__name:`whisper-add-friend`,props:{show:{type:Boolean,default:!1},user:{}},emits:[`success`],setup(e,{emit:g}){let b=e,x=s(``),S=s(!1),C=g,w=()=>{C(`success`)},T=()=>{S.value=!0,h.v1.friend.post.requesting({user_id:b.user.id,greetings:x.value}).then(e=>{window.$message.success(`发送成功`),S.value=!1,x.value=``,w()}).catch(e=>{S.value=!1})};return(s,h)=>{let g=u,b=f,C=p,E=m,D=l,O=d;return o(),a(O,{show:e.show,"onUpdate:show":w,class:`whisper-card`,preset:`card`,size:`small`,title:`申请添加朋友`,"mask-closable":!1,bordered:!1,style:{width:`360px`}},{default:i(()=>[c(`div`,_,[r(C,{"show-icon":!1},{default:i(()=>[h[1]||=n(` 发送添加朋友申请给: `,-1),r(b,{style:{"max-width":`100%`}},{default:i(()=>[r(g,{type:`success`},{default:i(()=>[n(t(e.user.nickname)+`@`+t(e.user.username),1)]),_:1})]),_:1})]),_:1}),c(`div`,v,[r(E,{type:`textarea`,placeholder:`请输入真挚的问候语`,autosize:{minRows:5,maxRows:10},value:x.value,"onUpdate:value":h[0]||=e=>x.value=e,maxlength:`120`,"show-count":``},null,8,[`value`])]),c(`div`,y,[r(D,{strong:``,secondary:``,type:`primary`,loading:S.value,onClick:T},{default:i(()=>[...h[2]||=[n(` 发送 `,-1)]]),_:1},8,[`loading`])])])]),_:1},8,[`show`])}}}),[[`__scopeId`,`data-v-9dc7c1f1`]]);export{b as t};
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue