-
-
+
+

+
+ {resultText}
+
+
+
+
- )
+ );
}
-export default App
+export default App;
diff --git a/handlers/fansly.go b/handlers/fansly.go
new file mode 100644
index 0000000..89d1998
--- /dev/null
+++ b/handlers/fansly.go
@@ -0,0 +1,111 @@
+package handlers
+
+import (
+ "FanslySync/structs"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "time"
+
+ "github.com/wailsapp/wails/v2/pkg/logger"
+)
+
+type FanslyAPIController struct {
+ client *http.Client
+ token string
+ logger logger.Logger
+}
+
+// New creates a new Fansly client with the provided token (optional).
+func NewFanslyAPIController(token string, log logger.Logger) *FanslyAPIController {
+ client := &http.Client{
+ Timeout: 30 * time.Second,
+ }
+
+ return &FanslyAPIController{
+ client: client,
+ token: token,
+ logger: log,
+ }
+}
+
+// GET issues a GET to /api/v1/{path}, optionally adding the Auth header,
+// and unmarshals the JSON response into the result parameter.
+func (f *FanslyAPIController) GET(path string, needsAuth bool, out interface{}) error {
+ // build request
+ 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())
+ return err
+ }
+
+ // Set headers
+ req.Header.Set("User-Agent", "FanslySync/3.0 sticks@teamhydra.dev")
+ req.Header.Set("Accept", "application/json")
+
+ // set auth
+ if needsAuth && f.token != "" {
+ req.Header.Set("Authorization", f.token)
+ }
+
+ // send
+ resp, err := f.client.Do(req)
+ if err != nil {
+ f.logger.Error("[FanslyAPIController] 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))
+ // read body for logs
+ body, _ := io.ReadAll(resp.Body)
+ f.logger.Info("[FanslyAPIController] 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))
+
+ // 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())
+ 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())
+ return err
+ }
+
+ return nil
+}
+
+// SetToken updates the authorization token for subsequent requests.
+func (f *FanslyAPIController) SetToken(token string) {
+ f.token = token
+}
+
+// Returns the current user's account information from the Fansly API.
+//
+// Will error if the token is not set or the request fails.
+//
+// Returns a FanslyAccount struct containing the account information.
+func (f *FanslyAPIController) GetMe() (*structs.FanslyAccount, error) {
+ var response structs.FanslyBaseResponse[structs.FanslyAccountResponse]
+
+ err := f.GET("account/me", true, &response)
+ if err != nil {
+ f.logger.Error("[FanslyAPIController] GetMe failed: " + err.Error())
+ return nil, err
+ }
+
+ // Return the account info
+ return &response.Response.Account, nil
+}
diff --git a/structs/fansly.go b/structs/fansly.go
index ca36a79..e72a78f 100644
--- a/structs/fansly.go
+++ b/structs/fansly.go
@@ -5,6 +5,10 @@ type FanslyBaseResponse[T any] struct {
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
@@ -50,4 +54,23 @@ type Subscription struct {
PromoStatus any `json:"promoStatus"`
PromoStartsAt any `json:"promoStartsAt"`
PromoEndsAt any `json:"promoEndsAt"`
-}
\ No newline at end of file
+}
+
+type FanslyAccount struct {
+ ID string `json:"id"`
+ Email string `json:"email"`
+ Username string `json:"username"`
+ DisplayName string `json:"displayName,omitempty"`
+ Flags int `json:"flags"`
+ Version int `json:"version"`
+ CreatedAt int64 `json:"createdAt"`
+ FollowCount int `json:"followCount"`
+ SubscriberCount int `json:"subscriberCount"`
+ AccountMediaLikes int `json:"accountMediaLikes"`
+ StatusID int `json:"statusId"`
+ LastSeenAt int `json:"lastSeenAt"`
+ About string `json:"about,omitempty"`
+ Location string `json:"location,omitempty"`
+ PostLikes int `json:"postLikes"`
+ ProfileAccess bool `json:"profileAccess"`
+}
diff --git a/structs/sync.go b/structs/sync.go
new file mode 100644
index 0000000..9b7f6ae
--- /dev/null
+++ b/structs/sync.go
@@ -0,0 +1,35 @@
+package structs
+
+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"`
+ // 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.
+ TotalCount int `json:"total_count"`
+ // Are we complete?
+ Complete bool `json:"complete"`
+}
+
+type PasteDataReply struct {
+ // The ID of the paste.
+ Id string `json:"id"`
+
+ // The content of the paste.
+ Content string `json:"content"`
+}
+
+type PastePutResponse struct {
+ // If Error is not empty, the request failed.
+ Error string `json:"error"`
+
+ // If the request was successful, this will of type PasteDataReply.
+ // If the request was not successful, this will be empty.
+ Payload PasteDataReply `json:"payload"`
+}
+
+type PastePayload struct {
+ Content string `json:"content"`
+}
diff --git a/utils/logger.go b/utils/logger.go
index eeee497..0499995 100644
--- a/utils/logger.go
+++ b/utils/logger.go
@@ -64,18 +64,19 @@ func (m *multiLogger) Fatal(message string) {
// NewRuntimeFileLogger returns a logger that writes all output both to
//
-// $XDG_CONFIG_HOME/FanslySync/logs/runtime_latest.log
+// $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
+// $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) {
- // 1) Ensure log directory exists
+ // Make sure the log directory exists
+ // We use $XDG_CONFIG_HOME/FanslySync/logs/runtime_latest.log or OS equivalent for $XDG_CONFIG_HOME
cfgDir, err := os.UserConfigDir()
if err != nil {
return nil, fmt.Errorf("cannot determine user config dir: %w", err)
@@ -85,7 +86,8 @@ func NewRuntimeFileLogger() (logger.Logger, error) {
return nil, fmt.Errorf("cannot create log directory: %w", err)
}
- // 2) Prune old timestamped logs (>14 days)
+ // Prune old logs
+ // We keep logs for 14 days, so delete any logs older than that
cutoff := time.Now().Add(-14 * 24 * time.Hour)
entries, _ := os.ReadDir(logDir)
for _, e := range entries {
@@ -101,17 +103,17 @@ func NewRuntimeFileLogger() (logger.Logger, error) {
}
}
- // 3) Build paths for timestamped + latest
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")
- // 4) Create both loggers
+ // Create loggers to attach to the multiLogger
tsLogger := logger.NewFileLogger(tsPath)
latestLogger := logger.NewFileLogger(latestPath)
termLogger := logger.NewDefaultLogger()
- // 5) Fan-out into a multiLogger
+ // Spread into a multiLogger
+ // This will fan out all log messages to all three loggers
multi := &multiLogger{
targets: []logger.Logger{tsLogger, latestLogger, termLogger},
}
diff --git a/utils/utils.go b/utils/utils.go
index 52f5d49..01e7c87 100644
--- a/utils/utils.go
+++ b/utils/utils.go
@@ -2,7 +2,6 @@ package utils
import (
"context"
-
"github.com/wailsapp/wails/v2/pkg/runtime"
)