manul sync backend works now

This commit is contained in:
Sticks 2025-05-20 13:01:02 -04:00
parent 7aa2dee280
commit 96abb94f21
9 changed files with 397 additions and 147 deletions

42
app.go
View File

@ -5,7 +5,6 @@ import (
"FanslySync/structs"
"FanslySync/utils"
"context"
"fmt"
"github.com/wailsapp/wails/v2/pkg/logger"
"github.com/wailsapp/wails/v2/pkg/runtime"
@ -16,7 +15,6 @@ type App struct {
ctx context.Context
ConfigManager handlers.ConfigManager
AppConfig *structs.Config
Logger logger.Logger
}
// NewApp creates a new App application struct
@ -40,10 +38,20 @@ func (a *App) startup(ctx context.Context, logger logger.Logger) {
}
// Create our config manager
a.ConfigManager = handlers.NewFileConfigManager(configPath, logger)
a.Logger = logger
configMgr, configMgrCreateErr := handlers.NewFileConfigManager(configPath)
logger.Info("[startup] initializing FanslySync...")
if configMgrCreateErr != nil {
// Show message box and quit
utils.ShowMessageBox(a.ctx, "FanslySync | Initialization Error", "Could not create config manager.\n\nError: "+configMgrCreateErr.Error(), utils.WithDialogType(runtime.ErrorDialog))
runtime.Quit(a.ctx)
return
}
// Set the config manager
a.ConfigManager = configMgr
logger.Info("initializing FanslySync...")
// Check our config path to see if it was set correctly. Will not contain FailedConfigPathFetch
// Do we have an old config file?
@ -56,7 +64,7 @@ func (a *App) startup(ctx context.Context, logger logger.Logger) {
}
if shouldMigrate {
logger.Info("[startup] migrating old config file...")
logger.Info("migrating old config file...")
// Migrate the old config file
err := a.ConfigManager.MigrateOldAppConfig()
if err != nil {
@ -66,7 +74,7 @@ func (a *App) startup(ctx context.Context, logger logger.Logger) {
return
} else {
// Show success message
logger.Info("[startup] old config file migrate ok")
logger.Info("old config file migrate ok")
utils.ShowMessageBox(a.ctx, "FanslySync | Notice", "We've detected an old config file (app version < 2.x and below).\n\nThe old config file has been migrated to the new format for you automatically, and the old config file has been deleted.\n\nPlease check your settings to ensure everything is correct.", utils.WithDialogType(runtime.InfoDialog))
// Now grab the new config
@ -82,7 +90,7 @@ func (a *App) startup(ctx context.Context, logger logger.Logger) {
}
} else {
// Load config as normal
logger.Info("[startup] loading config file...")
logger.Info("loading config file...")
cfg, err := a.ConfigManager.LoadConfigOrCreate()
if err != nil {
// Show the error in a message box
@ -91,26 +99,24 @@ func (a *App) startup(ctx context.Context, logger logger.Logger) {
}
// Set the config
logger.Info("[startup] config file loaded ok")
logger.Info("config file loaded ok")
a.AppConfig = cfg
}
logger.Info("FanslySync initialized successfully")
}
// Greet returns a greeting for the given name
func (a *App) Greet(token string) string {
// Create fansly API instance
fanslyAPI := handlers.NewFanslyAPIController(token, a.Logger)
fanslyAPI, createErr := handlers.NewFanslyAPIController(token)
// Get the user info
account, accountErr := fanslyAPI.GetMe()
if accountErr != nil {
return "Failed to get account info: " + accountErr.Error()
if createErr != nil {
return "Failed to create Fansly API instance: " + createErr.Error()
}
// Print the response we got
a.Logger.Info(fmt.Sprintf("[Greet] Account info: %+v", account))
// Sync
fanslyAPI.Sync(a.ctx, token, false)
// Return the greeting
return fmt.Sprintf("Hello %s! You have %d fans and %d posts likes.", account.Username, account.FollowCount, account.PostLikes)
return "Sync dispatched, check the logs for more info."
}

View File

@ -7,6 +7,7 @@ import (
"path/filepath"
"FanslySync/structs"
"FanslySync/utils"
"github.com/wailsapp/wails/v2/pkg/logger"
)
@ -43,11 +44,18 @@ type FileConfigManager struct {
log logger.Logger
}
func NewFileConfigManager(path string, log logger.Logger) ConfigManager {
func NewFileConfigManager(path string) (ConfigManager, error) {
// Create our logger
fileLogger, loggerCreateErr := utils.NewLogger("ConfigManager")
if loggerCreateErr != nil {
// Log the error and return nil
return nil, loggerCreateErr
}
return &FileConfigManager{
path: path,
log: log,
}
log: fileLogger,
}, nil
}
// GetConfigPath returns the path to the config file.
@ -65,96 +73,96 @@ func GetConfigPathForRuntime() (string, error) {
// ShouldMigrateOldAppConfig checks for an existing legacy config.json and logs the result.
func (mgr *FileConfigManager) ShouldMigrateOldAppConfig() (bool, error) {
mgr.log.Info("[ConfigManager::ShouldMigrateOldAppConfig] Checking for old config file")
mgr.log.Info("Checking for old config file")
dir, err := os.UserConfigDir()
if err != nil {
mgr.log.Error(fmt.Sprintf("[ConfigManager::ShouldMigrateOldAppConfig] Error getting user config dir: %v", err))
mgr.log.Error(fmt.Sprintf("Error getting user config dir: %v", err))
return false, err
}
oldConfigPath := filepath.Join(dir, "FanslySync", "config.json")
if _, err := os.Stat(oldConfigPath); os.IsNotExist(err) {
mgr.log.Info(fmt.Sprintf("[ConfigManager::ShouldMigrateOldAppConfig] No old config at %s", oldConfigPath))
mgr.log.Info(fmt.Sprintf("No old config at %s", oldConfigPath))
return false, nil
} else if err != nil {
mgr.log.Error(fmt.Sprintf("[ConfigManager::ShouldMigrateOldAppConfig] Error checking old config: %v", err))
mgr.log.Error(fmt.Sprintf("Error checking old config: %v", err))
return false, err
}
mgr.log.Info(fmt.Sprintf("[ConfigManager::ShouldMigrateOldAppConfig] Old config exists at %s", oldConfigPath))
mgr.log.Info(fmt.Sprintf("Old config exists at %s", oldConfigPath))
return true, nil
}
// MigrateOldAppConfig reads the legacy config.json, converts it, saves the new format,
// and removes the old file, logging each step.
func (mgr *FileConfigManager) MigrateOldAppConfig() error {
mgr.log.Info("[ConfigManager::MigrateOldAppConfig] Migrating old config file")
mgr.log.Info("Migrating old config file")
dir, err := os.UserConfigDir()
if err != nil {
mgr.log.Error(fmt.Sprintf("[ConfigManager::MigrateOldAppConfig] Error getting user config dir: %v", err))
mgr.log.Error(fmt.Sprintf("Error getting user config dir: %v", err))
return err
}
oldConfigPath := filepath.Join(dir, "FanslySync", "config.json")
if _, err := os.Stat(oldConfigPath); os.IsNotExist(err) {
mgr.log.Info("[ConfigManager::MigrateOldAppConfig] No old config to migrate")
mgr.log.Info("No old config to migrate")
return nil
} else if err != nil {
mgr.log.Error(fmt.Sprintf("[ConfigManager::MigrateOldAppConfig] Error checking old config: %v", err))
mgr.log.Error(fmt.Sprintf("Error checking old config: %v", err))
return err
}
data, err := os.ReadFile(oldConfigPath)
if err != nil {
mgr.log.Error(fmt.Sprintf("[ConfigManager::MigrateOldAppConfig] Error reading old config: %v", err))
mgr.log.Error(fmt.Sprintf("Error reading old config: %v", err))
return err
}
var oldCfg structs.OldConfig
if err := json.Unmarshal(data, &oldCfg); err != nil {
mgr.log.Error(fmt.Sprintf("[ConfigManager::MigrateOldAppConfig] Error unmarshaling old config: %v", err))
mgr.log.Error(fmt.Sprintf("Error unmarshaling old config: %v", err))
return err
}
newCfg := structs.NewConfigFromOld(&oldCfg)
if err := mgr.SaveConfig(newCfg); err != nil {
mgr.log.Error(fmt.Sprintf("[ConfigManager::MigrateOldAppConfig] Error saving new config: %v", err))
mgr.log.Error(fmt.Sprintf("Error saving new config: %v", err))
return err
}
if err := os.Remove(oldConfigPath); err != nil {
mgr.log.Error(fmt.Sprintf("[ConfigManager::MigrateOldAppConfig] Error removing old config: %v", err))
mgr.log.Error(fmt.Sprintf("Error removing old config: %v", err))
return fmt.Errorf("could not remove old config file: %w", err)
}
mgr.log.Info("[ConfigManager::MigrateOldAppConfig] Migration complete; old config removed")
mgr.log.Info("Migration complete; old config removed")
return nil
}
// GetConfig loads the config from disk if forceReload is true or no cache exists.
// It logs each step and errors encountered.
func (mgr *FileConfigManager) GetConfig(forceReload bool) (*structs.Config, error) {
mgr.log.Debug(fmt.Sprintf("[ConfigManager] GetConfig(forceReload=%v)", forceReload))
mgr.log.Debug(fmt.Sprintf("GetConfig(forceReload=%v)", forceReload))
if mgr.config != nil && !forceReload {
mgr.log.Debug("[ConfigManager::GetConfig] Returning cached config")
mgr.log.Debug("Returning cached config")
return mgr.config, nil
}
data, err := os.ReadFile(mgr.path)
if err != nil {
mgr.log.Error(fmt.Sprintf("[ConfigManager::GetConfig] Error reading config file: %v", err))
mgr.log.Error(fmt.Sprintf("Error reading config file: %v", err))
return nil, err
}
var cfg structs.Config
if err := json.Unmarshal(data, &cfg); err != nil {
mgr.log.Error(fmt.Sprintf("[ConfigManager::GetConfig] Error unmarshaling config: %v", err))
mgr.log.Error(fmt.Sprintf("Error unmarshaling config: %v", err))
return nil, err
}
mgr.config = &cfg
mgr.log.Info("[ConfigManager::GetConfig] Config loaded from disk. Cache updated.")
mgr.log.Debug(fmt.Sprintf("[ConfigManager::GetConfig] Config: %+v", cfg))
mgr.log.Info("Config loaded from disk. Cache updated.")
mgr.log.Debug(fmt.Sprintf("Config: %+v", cfg))
return mgr.config, nil
}
@ -162,44 +170,44 @@ func (mgr *FileConfigManager) GetConfig(forceReload bool) (*structs.Config, erro
func (mgr *FileConfigManager) LoadConfigOrCreate() (*structs.Config, error) {
cfg, err := mgr.GetConfig(false)
if err == nil {
mgr.log.Info("[ConfigManager::LoadConfigOrCreate] Existing config loaded")
mgr.log.Info("Existing config loaded")
return cfg, nil
}
if os.IsNotExist(err) {
mgr.log.Warning("[ConfigManager::LoadConfigOrCreate] Config missing; creating default")
mgr.log.Warning("Config missing; creating default")
defaultCfg := structs.NewConfig()
if saveErr := mgr.SaveConfig(defaultCfg); saveErr != nil {
mgr.log.Error(fmt.Sprintf("[ConfigManager::LoadConfigOrCreate] Error saving default config: %v", saveErr))
mgr.log.Error(fmt.Sprintf("Error saving default config: %v", saveErr))
return nil, saveErr
}
mgr.log.Info("[ConfigManager::LoadConfigOrCreate] Default config created and saved")
mgr.log.Info("Default config created and saved")
return defaultCfg, nil
}
mgr.log.Error(fmt.Sprintf("[ConfigManager::LoadConfigOrCreate] Error loading config: %v", err))
mgr.log.Error(fmt.Sprintf("Error loading config: %v", err))
return nil, err
}
// SaveConfig writes the config to disk, updates cache, and logs the process.
func (mgr *FileConfigManager) SaveConfig(cfg *structs.Config) error {
mgr.log.Info(fmt.Sprintf("[ConfigManager::SaveConfig] Saving config to %s", mgr.path))
mgr.log.Info(fmt.Sprintf("Saving config to %s", mgr.path))
dir := filepath.Dir(mgr.path)
if err := os.MkdirAll(dir, 0o755); err != nil {
mgr.log.Error(fmt.Sprintf("[ConfigManager::SaveConfig] Error creating config directory: %v", err))
mgr.log.Error(fmt.Sprintf("Error creating config directory: %v", err))
return fmt.Errorf("could not create config directory: %w", err)
}
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
mgr.log.Error(fmt.Sprintf("[ConfigManager::SaveConfig] Error marshaling config: %v", err))
mgr.log.Error(fmt.Sprintf("Error marshaling config: %v", err))
return fmt.Errorf("could not marshal config: %w", err)
}
if err := os.WriteFile(mgr.path, data, 0o644); err != nil {
mgr.log.Error(fmt.Sprintf("[ConfigManager::SaveConfig] Error writing config file: %v", err))
mgr.log.Error(fmt.Sprintf("Error writing config file: %v", err))
return fmt.Errorf("could not write config file: %w", err)
}
mgr.config = cfg
mgr.log.Info("[ConfigManager::SaveConfig] Config saved and cache updated")
mgr.log.Info("Config saved and cache updated")
return nil
}

View File

@ -2,13 +2,17 @@ package handlers
import (
"FanslySync/structs"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"FanslySync/utils"
"github.com/wailsapp/wails/v2/pkg/logger"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
type FanslyAPIController struct {
@ -18,16 +22,24 @@ type FanslyAPIController struct {
}
// New creates a new Fansly client with the provided token (optional).
func NewFanslyAPIController(token string, log logger.Logger) *FanslyAPIController {
func NewFanslyAPIController(token string) (*FanslyAPIController, error) {
client := &http.Client{
Timeout: 30 * time.Second,
}
apiLogger, apiLoggerCreateErr := utils.NewLogger("FanslyAPIController")
if apiLoggerCreateErr != nil {
// Log the error and return nil
fmt.Println("Failed to create logger: ", apiLoggerCreateErr)
return nil, apiLoggerCreateErr
}
return &FanslyAPIController{
client: client,
token: token,
logger: log,
}
logger: apiLogger,
}, nil
}
// GET issues a GET to /api/v1/{path}, optionally adding the Auth header,
@ -37,7 +49,7 @@ func (f *FanslyAPIController) GET(path string, needsAuth bool, out interface{})
url := "https://apiv3.fansly.com/api/v1/" + path
req, err := http.NewRequest("GET", url, nil)
if err != nil {
f.logger.Error("[FanslyAPIController] NewRequest GET " + path + ": " + err.Error())
f.logger.Error("NewRequest GET " + path + ": " + err.Error())
return err
}
@ -53,34 +65,34 @@ func (f *FanslyAPIController) GET(path string, needsAuth bool, out interface{})
// send
resp, err := f.client.Do(req)
if err != nil {
f.logger.Error("[FanslyAPIController] Do GET " + path + ": " + err.Error())
f.logger.Error("Do GET " + path + ": " + err.Error())
return err
}
defer resp.Body.Close()
// non-200
if resp.StatusCode != http.StatusOK {
f.logger.Error(fmt.Sprintf("[FanslyAPIController] GET %s failed: %s", path, resp.Status))
f.logger.Error(fmt.Sprintf("GET %s failed: %s", path, resp.Status))
// read body for logs
body, _ := io.ReadAll(resp.Body)
f.logger.Info("[FanslyAPIController] Response body: " + string(body))
f.logger.Info("Response body: " + string(body))
return fmt.Errorf("unexpected status %s", resp.Status)
}
// 200 ok, log
f.logger.Debug(fmt.Sprintf("[FanslyAPIController] GET %s succeeded: %s", path, resp.Status))
f.logger.Debug(fmt.Sprintf("GET %s succeeded: %s", path, resp.Status))
// read body and unmarshal
body, err := io.ReadAll(resp.Body)
if err != nil {
f.logger.Error("[FanslyAPIController] Request was OK, but failed to read body: " + err.Error())
f.logger.Error("Request was OK, but failed to read body: " + err.Error())
return err
}
// unmarshal into our expected response type
err = json.Unmarshal(body, &out)
if err != nil {
f.logger.Error("[FanslyAPIController] GET " + path + " failed to unmarshal response: " + err.Error())
f.logger.Error("GET " + path + " failed to unmarshal response: " + err.Error())
return err
}
@ -102,10 +114,154 @@ func (f *FanslyAPIController) GetMe() (*structs.FanslyAccount, error) {
err := f.GET("account/me", true, &response)
if err != nil {
f.logger.Error("[FanslyAPIController] GetMe failed: " + err.Error())
f.logger.Error("GetMe failed: " + err.Error())
return nil, err
}
// Return the account info
return &response.Response.Account, nil
}
func (f *FanslyAPIController) GetFollowersWithOffset(acctId string, offset int) ([]structs.FanslyFollowResponse, error) {
var response structs.FanslyBaseResponseAsArray[structs.FanslyFollowResponse]
err := f.GET(fmt.Sprintf("account/%s/followers?limit=300&offset=%d", acctId, offset), true, &response)
if err != nil {
f.logger.Error("GetFollowersWithOffset failed: " + err.Error())
return nil, err
}
// Return the followers
return response.Response, nil
}
func (f *FanslyAPIController) GetSubscribersWithOffset(offset int) (structs.FanslySubscriptionResponse, error) {
var response structs.FanslyBaseResponse[structs.FanslySubscriptionResponse]
err := f.GET(fmt.Sprintf("subscribers?status=3,4&limit=300&offset=%d", offset), true, &response)
if err != nil {
f.logger.Error("GetSubscribersWithOffset failed: " + err.Error())
return structs.FanslySubscriptionResponse{}, err
}
// Return the subscribers
return response.Response, nil
}
func (f *FanslyAPIController) Sync(ctx context.Context, token string, auto bool) {
// Start the sync worker
go syncWorker(ctx, token, auto)
}
func syncWorker(ctx context.Context, token string, auto bool) {
runtime.EventsEmit(ctx, "sync:started", nil)
progress := func(step string, curr, total int, done bool) {
p := structs.SyncProgressEvent{
Step: step,
PercentDone: utils.Percentage(curr, total),
Count: curr,
TotalCount: total,
Complete: done,
}
runtime.EventsEmit(ctx, "sync:progress", p)
}
// Create a logger dedicated to this sync run
syncLogger, err := utils.NewLogger("SyncWorker")
if err != nil {
runtime.EventsEmit(ctx, "sync:error", err.Error())
return
}
var startTime time.Time = time.Now()
syncLogger.Info(fmt.Sprintf("Starting sync at %s, auto=%t", startTime.Format(time.RFC3339), auto))
syncLogger.Info("[1/4] Fetching profile…")
// Instantiate API controller
c, err := NewFanslyAPIController(token)
if err != nil {
runtime.EventsEmit(ctx, "sync:error", err.Error())
return
}
// 1. Profile -----------------------------------------------------------
progress("Fetching profile", 0, 100, false)
acct, err := c.GetMe()
if err != nil {
runtime.EventsEmit(ctx, "sync:error", err.Error())
return
}
progress("Fetched profile", 0, 100, false)
syncLogger.Info(fmt.Sprintf("Fetched profile. %s %d followers, %d subscribers", acct.Username, acct.FollowCount, acct.SubscriberCount))
// 2. Followers ---------------------------------------------------------
syncLogger.Info("[2/4] Fetching followers…")
followers := make([]string, 0, acct.FollowCount)
offset := 0
for len(followers) < acct.FollowCount {
batch, err := c.GetFollowersWithOffset(acct.ID, offset)
if err != nil {
runtime.EventsEmit(ctx, "sync:error", err.Error())
return
}
for _, f := range batch {
followers = append(followers, f.FollowerID)
}
offset += 300
progress("Fetching followers", len(followers), acct.FollowCount, false)
syncLogger.Info(fmt.Sprintf("[followers] %d/%d (offset=%d)", len(followers), acct.FollowCount, offset))
if len(batch) == 0 || len(followers) >= acct.FollowCount {
break
}
time.Sleep(250 * time.Millisecond) // gentle throttle
}
// 3. Subscribers -------------------------------------------------------
syncLogger.Info("[3/4] Fetching subscribers…")
subs := make([]structs.Subscription, 0, acct.SubscriberCount)
offset = 0
for len(subs) < acct.SubscriberCount {
res, err := c.GetSubscribersWithOffset(offset)
if err != nil {
runtime.EventsEmit(ctx, "sync:error", err.Error())
return
}
subs = append(subs, res.Subscriptions...)
offset += 300
progress("Fetching subscribers", len(subs), acct.SubscriberCount, false)
syncLogger.Info(fmt.Sprintf("[subs] %d/%d (offset=%d)", len(subs), acct.SubscriberCount, offset))
if len(res.Subscriptions) == 0 || len(subs) >= acct.SubscriberCount {
break
}
time.Sleep(250 * time.Millisecond)
}
// 4. Upload & finish ---------------------------------------------------
syncLogger.Info("[4/4] Uploading data…")
data := structs.SyncData{
Followers: followers,
Subscriptions: subs,
}
if !auto {
payload, err := json.Marshal(data)
if err != nil {
runtime.EventsEmit(ctx, "sync:error", err.Error())
return
}
url, err := utils.UploadPaste(payload)
if err != nil {
runtime.EventsEmit(ctx, "sync:error", err.Error())
return
}
data.PasteURL = url
syncLogger.Info(fmt.Sprintf("Uploaded data to %s", url))
}
progress("Sync complete", 100, 100, true)
syncLogger.Info(fmt.Sprintf("Sync done at %s. Took %s, fetched %d followers and %d subscribers", time.Now().Format(time.RFC3339), time.Since(startTime).String(), len(followers), len(subs)))
runtime.EventsEmit(ctx, "sync:complete", data)
}

10
main.go
View File

@ -21,9 +21,11 @@ func main() {
app := NewApp()
// Create our custom file logger
fileLogger, loggerCreateErr := utils.NewRuntimeFileLogger()
if loggerCreateErr != nil {
log.Fatal(loggerCreateErr)
fileLogger, loggerCreateErr := utils.NewLogger("runtime")
startupLogger, startupLoggerCreateErr := utils.NewLogger("startup")
if loggerCreateErr != nil || startupLoggerCreateErr != nil {
log.Fatal("Failed to create one or more loggers: ", loggerCreateErr, startupLoggerCreateErr)
}
// Create application with options
@ -40,7 +42,7 @@ func main() {
LogLevelProduction: logger.INFO,
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
OnStartup: func(ctx context.Context) {
app.startup(ctx, fileLogger)
app.startup(ctx, startupLogger)
},
Bind: []interface{}{
app,

View File

@ -1,8 +1,9 @@
package structs
type SyncData struct {
Followers []FanslyFollowResponse `json:"followers"` // List of followers
Subscriptions []Subscription `json:"subscriptions"` // List of subscriptions
Followers []string `json:"followers"` // List of followers
Subscriptions []Subscription `json:"subscriptions"` // List of subscriptions
PasteURL string `json:"paste_url"` // URL of the paste
}
type Config struct {
@ -35,7 +36,7 @@ func NewConfig() *Config {
SyncInterval: 8,
LastSyncTime: "",
LastSyncData: SyncData{
Followers: []FanslyFollowResponse{},
Followers: []string{},
Subscriptions: []Subscription{},
},
}
@ -51,7 +52,7 @@ func NewConfigFromOld(oldConfig *OldConfig) *Config {
SyncInterval: oldConfig.SyncInterval,
LastSyncTime: "",
LastSyncData: SyncData{
Followers: []FanslyFollowResponse{},
Followers: []string{},
Subscriptions: []Subscription{},
},
}

View File

@ -5,14 +5,17 @@ type FanslyBaseResponse[T any] struct {
Response T `json:"response"` // The response data, type of T
}
type FanslyBaseResponseAsArray[T any] struct {
Success bool `json:"success"` // Indicates if the request was successful
Response []T `json:"response"` // The response data, type of T
}
type FanslyAccountResponse struct {
Account FanslyAccount `json:"account"`
}
type FanslyFollowResponse struct {
Followers []struct {
FollowerID string `json:"followerId"` // The ID of the follower
}
FollowerID string `json:"followerId"` // The ID of the follower
}
type FanslySubscriptionResponse struct {

View File

@ -4,7 +4,7 @@ type SyncProgressEvent struct {
// The current step of the sync process.
Step string `json:"step"`
// The current percent done of the sync process.
PercentDone int `json:"percent_done"`
PercentDone float64 `json:"percent_done"`
// The current count of the current step of the sync process.
Count int `json:"current_count"`
// The total count of the current step of the sync process.

View File

@ -4,79 +4,40 @@ import (
"fmt"
"os"
"path/filepath"
"sync"
"time"
"github.com/wailsapp/wails/v2/pkg/logger"
)
// multiLogger fans every log call out to multiple logger.Logger targets with timestamps.
type multiLogger struct {
var (
baseTargets []logger.Logger
once sync.Once
initErr error
)
// InstancedLogger prefixes each message with its instance-specific prefix and a timestamp,
// while writing to shared log outputs.
type InstancedLogger struct {
prefix string
targets []logger.Logger
}
// timestamped prefixes each message with a timestamp.
func timestamped(message string) string {
return time.Now().Format("2006-01-02 15:04:05") + " " + message
// NewLogger returns a logger instance that writes to the shared logs
//
// but prefixes every message with [prefix].
func NewLogger(prefix string) (*InstancedLogger, error) {
once.Do(func() {
baseTargets, initErr = createBaseTargets()
})
if initErr != nil {
return nil, initErr
}
return &InstancedLogger{prefix: prefix, targets: baseTargets}, nil
}
func (m *multiLogger) Print(message string) {
msg := timestamped(message)
for _, l := range m.targets {
l.Print(msg)
}
}
func (m *multiLogger) Trace(message string) {
msg := timestamped(message)
for _, l := range m.targets {
l.Trace(msg)
}
}
func (m *multiLogger) Debug(message string) {
msg := timestamped(message)
for _, l := range m.targets {
l.Debug(msg)
}
}
func (m *multiLogger) Info(message string) {
msg := timestamped(message)
for _, l := range m.targets {
l.Info(msg)
}
}
func (m *multiLogger) Warning(message string) {
msg := timestamped(message)
for _, l := range m.targets {
l.Warning(msg)
}
}
func (m *multiLogger) Error(message string) {
msg := timestamped(message)
for _, l := range m.targets {
l.Error(msg)
}
}
func (m *multiLogger) Fatal(message string) {
msg := timestamped(message)
for _, l := range m.targets {
l.Fatal(msg)
}
}
// NewRuntimeFileLogger returns a logger that writes all output both to
//
// $XDG_CONFIG_HOME/FanslySync/logs/runtime_latest.log (or OS equivalent)
//
// and to a timestamped file
//
// $XDG_CONFIG_HOME/FanslySync/logs/runtime_YYYY-MM-DD_HH-MM-SS.log (or OS equivalent)
//
// It also deletes any timestamped logs older than 14 days.
//
// The returned logger implements github.com/wailsapp/wails/v2/pkg/logger.Logger
// and will be used by Wails for all Go-side logging.
func NewRuntimeFileLogger() (logger.Logger, error) {
// Make sure the log directory exists
// We use $XDG_CONFIG_HOME/FanslySync/logs/runtime_latest.log or OS equivalent for $XDG_CONFIG_HOME
// createBaseTargets initializes the shared log file and console loggers once.
func createBaseTargets() ([]logger.Logger, error) {
cfgDir, err := os.UserConfigDir()
if err != nil {
return nil, fmt.Errorf("cannot determine user config dir: %w", err)
@ -86,8 +47,7 @@ func NewRuntimeFileLogger() (logger.Logger, error) {
return nil, fmt.Errorf("cannot create log directory: %w", err)
}
// Prune old logs
// We keep logs for 14 days, so delete any logs older than that
// Prune logs older than 14 days
cutoff := time.Now().Add(-14 * 24 * time.Hour)
entries, _ := os.ReadDir(logDir)
for _, e := range entries {
@ -103,20 +63,53 @@ func NewRuntimeFileLogger() (logger.Logger, error) {
}
}
// Remove old runtime_latest.log if it exists
runtimePath := filepath.Join(logDir, "runtime_latest.log")
if _, err := os.Stat(runtimePath); err == nil {
_ = os.Remove(runtimePath)
}
// Prepare file paths
ts := time.Now().Format("2006-01-02_15-04-05")
tsPath := filepath.Join(logDir, fmt.Sprintf("runtime_%s.log", ts))
latestPath := filepath.Join(logDir, "runtime_latest.log")
// Create loggers to attach to the multiLogger
// Create loggers
tsLogger := logger.NewFileLogger(tsPath)
latestLogger := logger.NewFileLogger(latestPath)
termLogger := logger.NewDefaultLogger()
// Spread into a multiLogger
// This will fan out all log messages to all three loggers
multi := &multiLogger{
targets: []logger.Logger{tsLogger, latestLogger, termLogger},
}
return multi, nil
return []logger.Logger{tsLogger, latestLogger, termLogger}, nil
}
// log dispatches a timestamped, prefixed message to all shared targets.
func (l *InstancedLogger) log(level, message string) {
timestamp := time.Now().Format("2006-01-02 15:04:05")
fullMsg := fmt.Sprintf("%s [%s] %s", timestamp, l.prefix, message)
for _, t := range l.targets {
switch level {
case "Print":
t.Print(fullMsg)
case "Trace":
t.Trace(fullMsg)
case "Debug":
t.Debug(fullMsg)
case "Info":
t.Info(fullMsg)
case "Warning":
t.Warning(fullMsg)
case "Error":
t.Error(fullMsg)
case "Fatal":
t.Fatal(fullMsg)
}
}
}
func (l *InstancedLogger) Print(message string) { l.log("Print", message) }
func (l *InstancedLogger) Trace(message string) { l.log("Trace", message) }
func (l *InstancedLogger) Debug(message string) { l.log("Debug", message) }
func (l *InstancedLogger) Info(message string) { l.log("Info", message) }
func (l *InstancedLogger) Warning(message string) { l.log("Warning", message) }
func (l *InstancedLogger) Error(message string) { l.log("Error", message) }
func (l *InstancedLogger) Fatal(message string) { l.log("Fatal", message) }

View File

@ -1,7 +1,15 @@
package utils
import (
"FanslySync/structs"
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
@ -54,3 +62,76 @@ func ShowMessageBox(ctx context.Context, title, message string, opts ...MessageB
// Show the message box
runtime.MessageDialog(ctx, options)
}
func UploadPaste(payload []byte) (string, error) {
// Create HTTP client
client := &http.Client{}
// Max request duration is 30s
client.Timeout = 30 * time.Second
// Create request
var payloadStruct structs.PastePayload
payloadStruct.Content = string(payload)
// Marshal the payload to JSON
payloadJSON, err := json.Marshal(payloadStruct)
if err != nil {
return "", err
}
// Create request (POST to https://paste.hep.gg/api/)
req, err := http.NewRequest("POST", "https://paste.hep.gg/api/", nil)
if err != nil {
return "", err
}
// Set headers
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "FanslySync/3.0 sticks@teamhydra.dev")
req.Header.Set("Accept", "application/json")
req.Header.Set("Accept-Encoding", "gzip")
// Set the body
req.Body = io.NopCloser(bytes.NewBuffer(payloadJSON))
// Send the request
resp, err := client.Do(req)
if err != nil {
return "", err
}
// Check for non-200 status code
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
// Read the response body into our struct
var pasteResponse structs.PastePutResponse
data, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
// Unmarshal the response into our struct
err = json.Unmarshal(data, &pasteResponse)
if err != nil {
return "", err
}
// Check for error in response
if pasteResponse.Error != "" {
return "", fmt.Errorf("error from paste server: %s", pasteResponse.Error)
}
// Return the paste ID
return fmt.Sprintf("https://paste.hep.gg/api/%s/raw", pasteResponse.Payload.Id), nil
}
// percentage calculates the percentage of curr out of total.
func Percentage(curr, total int) float64 {
if total == 0 {
return 0
}
return float64(curr) / float64(total) * 100
}