manul sync backend works now
This commit is contained in:
parent
7aa2dee280
commit
96abb94f21
42
app.go
42
app.go
@ -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."
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
10
main.go
10
main.go
@ -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,
|
||||
|
@ -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{},
|
||||
},
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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.
|
||||
|
139
utils/logger.go
139
utils/logger.go
@ -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) }
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user