manul sync backend works now
This commit is contained in:
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
Reference in New Issue
Block a user