diff --git a/app.go b/app.go index a4698d5..97918cc 100644 --- a/app.go +++ b/app.go @@ -98,6 +98,19 @@ func (a *App) startup(ctx context.Context, logger logger.Logger) { } // Greet returns a greeting for the given name -func (a *App) Greet(name string) string { - return fmt.Sprintf("Hello %s, It's show time!", name) +func (a *App) Greet(token string) string { + // Create fansly API instance + fanslyAPI := handlers.NewFanslyAPIController(token, a.Logger) + + // Get the user info + account, accountErr := fanslyAPI.GetMe() + if accountErr != nil { + return "Failed to get account info: " + accountErr.Error() + } + + // Print the response we got + a.Logger.Info(fmt.Sprintf("[Greet] Account info: %+v", account)) + + // Return the greeting + return fmt.Sprintf("Hello %s! You have %d fans and %d posts likes.", account.Username, account.FollowCount, account.PostLikes) } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a6e56f9..3f0de86 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,10 +1,12 @@ -import {useState} from 'react'; +import { useState } from 'react'; import logo from './assets/images/logo-universal.png'; import './App.css'; -import {Greet} from "../wailsjs/go/main/App"; +import { Greet } from '../wailsjs/go/main/App'; function App() { - const [resultText, setResultText] = useState("Please enter your name below 👇"); + const [resultText, setResultText] = useState( + 'Enter your fansly API token, then press Go!', + ); const [name, setName] = useState(''); const updateName = (e: any) => setName(e.target.value); const updateResultText = (result: string) => setResultText(result); @@ -14,15 +16,26 @@ function App() { } return ( -
- -
{resultText}
-
- - +
+ +
+ {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" )