Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 97 additions & 6 deletions backend/radiance.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"go.opentelemetry.io/otel/trace"

C "github.com/getlantern/common"
wire "github.com/getlantern/common/usermessage"
"github.com/getlantern/publicip"

"github.com/getlantern/radiance/account"
Expand All @@ -38,6 +39,7 @@ import (
"github.com/getlantern/radiance/telemetry"
"github.com/getlantern/radiance/traces"
"github.com/getlantern/radiance/unbounded"
clientmessage "github.com/getlantern/radiance/usermessage"
"github.com/getlantern/radiance/vpn"

lbA "github.com/getlantern/lantern-box/adapter"
Expand All @@ -57,6 +59,7 @@ type LocalBackend struct {
confHandler *config.ConfigHandler
issueReporter *issue.IssueReporter
accountClient *account.Client
userMessages *clientmessage.Service

srvManager *servers.Manager
vpnClient *vpn.VPNClient
Expand Down Expand Up @@ -227,13 +230,40 @@ func NewLocalBackend(ctx context.Context, opts Options) (*LocalBackend, error) {
}
r.sessionHistory = vpn.NewSessionHistory(slog.Default().With("service", "session_history"), r.sessionInfo())
r.shutdownFuncs = append(r.shutdownFuncs, func() error { r.sessionHistory.Close(); return nil })
userMessages, err := clientmessage.New(clientmessage.Options{
DataDir: dataDir,
Fetcher: clientmessage.NewHTTPFetcher(
kindling.HTTPClient(),
clientmessage.Endpoint(common.GetBaseURL()),
),
ContextProvider: func() clientmessage.ClientContext {
return clientmessage.ClientContext{
UserID: settings.GetString(settings.UserIDKey),
ProToken: settings.GetString(settings.TokenKey),
Locale: clientmessage.NormalizeLocale(settings.GetString(settings.LocaleKey)),
Platform: clientmessage.NormalizePlatform(common.Platform),
AppVersion: common.GetVersion(),
}
},
})
if err != nil {
slog.Error("Loading user-message state", "error", err)
} else {
r.userMessages = userMessages
}
r.clearSelectedIfMissing()
return r, nil
}

func (r *LocalBackend) Start() {
// eagerly start kindling so it's ready by the time we need to make network requests
kindling.Init()
if r.userMessages != nil {
events.SubscribeContext(r.ctx, func(account.UserChangeEvent) {
r.userMessages.Refresh()
})
r.userMessages.Start(r.ctx)
}
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
result, err := publicip.Detect(ctx, &publicip.Config{
Expand Down Expand Up @@ -601,6 +631,9 @@ func (r *LocalBackend) PatchSettings(updates settings.Settings) error {
if err := settings.Patch(diff); err != nil {
return fmt.Errorf("failed to update settings: %w", err)
}
if _, ok := diff[settings.LocaleKey]; ok {
r.refreshUserMessages()
}
// telemetry settings
if _, ok := diff[settings.TelemetryKey]; ok {
if settings.GetBool(settings.TelemetryKey) {
Expand Down Expand Up @@ -1548,19 +1581,35 @@ func (r *LocalBackend) RemoveSplitTunnelItems(items vpn.SplitTunnelFilter) error
/////////////

func (r *LocalBackend) NewUser(ctx context.Context) (*account.UserData, error) {
return r.accountClient.NewUser(ctx)
userData, err := r.accountClient.NewUser(ctx)
if err == nil {
r.refreshUserMessages()
}
return userData, err
}

func (r *LocalBackend) Login(ctx context.Context, email, password string) (*account.UserData, error) {
return r.accountClient.Login(ctx, email, password)
userData, err := r.accountClient.Login(ctx, email, password)
if err == nil {
r.refreshUserMessages()
}
return userData, err
}

func (r *LocalBackend) Logout(ctx context.Context, email string) (*account.UserData, error) {
return r.accountClient.Logout(ctx, email)
userData, err := r.accountClient.Logout(ctx, email)
if err == nil {
r.refreshUserMessages()
}
return userData, err
}

func (r *LocalBackend) FetchUserData(ctx context.Context) (*account.UserData, error) {
return r.accountClient.FetchUserData(ctx)
userData, err := r.accountClient.FetchUserData(ctx)
if err == nil {
r.refreshUserMessages()
}
return userData, err
}

func (r *LocalBackend) VerifyPassword(ctx context.Context, email, password string) error {
Expand All @@ -1585,7 +1634,11 @@ func (r *LocalBackend) CompleteRecoveryByEmail(ctx context.Context, email, newPa
}

func (r *LocalBackend) DeleteAccount(ctx context.Context, email, password string) (*account.UserData, error) {
return r.accountClient.DeleteAccount(ctx, email, password)
userData, err := r.accountClient.DeleteAccount(ctx, email, password)
if err == nil {
r.refreshUserMessages()
}
return userData, err
}

func (r *LocalBackend) SignUp(ctx context.Context, email, password string) ([]byte, *account.SignupResponse, error) {
Expand Down Expand Up @@ -1652,7 +1705,11 @@ func (r *LocalBackend) RemoveDevice(ctx context.Context, deviceID string) (*acco
}

func (r *LocalBackend) OAuthLoginCallback(ctx context.Context, oAuthToken string) (*account.UserData, error) {
return r.accountClient.OAuthLoginCallback(ctx, oAuthToken)
userData, err := r.accountClient.OAuthLoginCallback(ctx, oAuthToken)
if err == nil {
r.refreshUserMessages()
}
return userData, err
}

func (r *LocalBackend) OAuthLoginURL(ctx context.Context, provider string) (string, error) {
Expand All @@ -1671,6 +1728,40 @@ func (r *LocalBackend) UserData() (*account.UserData, error) {
return &userData, nil
}

// CurrentUserMessage returns the current account's pending message.
func (r *LocalBackend) CurrentUserMessage() (*wire.ResolvedUserMessage, error) {
if r.userMessages == nil {
return nil, nil
}
return r.userMessages.Current()
}

// RefreshUserMessages schedules an immediate eligibility refresh.
func (r *LocalBackend) RefreshUserMessages() {
r.refreshUserMessages()
}

func (r *LocalBackend) refreshUserMessages() {
if r.userMessages != nil {
r.userMessages.Refresh()
}
}

// AcknowledgeUserMessage records that the UI displayed displayID.
func (r *LocalBackend) AcknowledgeUserMessage(displayID string) error {
if r.userMessages == nil {
return clientmessage.ErrMessageNotPending
}
return r.userMessages.Acknowledge(displayID)
}

// SetUserMessageActivity adjusts polling for app and network lifecycle changes.
func (r *LocalBackend) SetUserMessageActivity(active, online bool) {
if r.userMessages != nil {
r.userMessages.SetActivity(active, online)
}
}

///////////////////
// Subscriptions //
///////////////////
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ require (
github.com/alitto/pond v1.9.2
github.com/getlantern/amp v0.0.0-20260606002220-a8629924577c
github.com/getlantern/broflake v0.0.0-20260810172605-bef5e5234952
github.com/getlantern/common v1.2.1-0.20260708083946-cc657b08792c
github.com/getlantern/common v1.2.1-0.20260818065623-10c2257aa54f
github.com/getlantern/dnstt v0.0.0-20260603191204-3b860502c0ac
github.com/getlantern/domainfront v0.0.0-20260722204513-8c1f8acfa715
github.com/getlantern/keepcurrent v0.0.0-20260616120552-f204338b01a3
Expand All @@ -67,6 +67,7 @@ require (
go.opentelemetry.io/otel/sdk v1.43.0
go.opentelemetry.io/otel/sdk/metric v1.43.0
golang.org/x/term v0.41.0
golang.org/x/text v0.35.0
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
google.golang.org/protobuf v1.36.11
gopkg.in/natefinch/lumberjack.v2 v2.2.1
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -238,8 +238,8 @@ github.com/getlantern/amp v0.0.0-20260606002220-a8629924577c h1:ZzxuhIWO295y4eE6
github.com/getlantern/amp v0.0.0-20260606002220-a8629924577c/go.mod h1:b5teAOFT+vpBqc2CHoz74QTEM15iOv1PLdQogwXcfrM=
github.com/getlantern/broflake v0.0.0-20260810172605-bef5e5234952 h1:nD8iJ4IpaTq/09PhoOzmaGTwatOxsR41neSmbCY6RS8=
github.com/getlantern/broflake v0.0.0-20260810172605-bef5e5234952/go.mod h1:1+1kCIg9Zj+2CgN+vl868AlAZVUItuN4wLhFur5QakA=
github.com/getlantern/common v1.2.1-0.20260708083946-cc657b08792c h1:Hpxu12ORnAcyYuIqV2yAqrmDAnTaY1Cd3yL7TvFB6ME=
github.com/getlantern/common v1.2.1-0.20260708083946-cc657b08792c/go.mod h1:eSSuV4bMPgQJnczBw+KWWqWNo1itzmVxC++qUBPRTt0=
github.com/getlantern/common v1.2.1-0.20260818065623-10c2257aa54f h1:8iZqf4mUkFXKmUnDj2Iej0WySAzLuQmHOX736b4pCYU=
github.com/getlantern/common v1.2.1-0.20260818065623-10c2257aa54f/go.mod h1:eSSuV4bMPgQJnczBw+KWWqWNo1itzmVxC++qUBPRTt0=
github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201 h1:oEZYEpZo28Wdx+5FZo4aU7JFXu0WG/4wJWese5reQSA=
github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201/go.mod h1:Y9WZUHEb+mpra02CbQ/QczLUe6f0Dezxaw5DCJlJQGo=
github.com/getlantern/dnstt v0.0.0-20260603191204-3b860502c0ac h1:TMvkNgLVyIYAfu1dYrOuRPYMVY+cHEo1C0CYWhtSw2A=
Expand Down
30 changes: 30 additions & 0 deletions ipc/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"syscall"
"time"

wire "github.com/getlantern/common/usermessage"
box "github.com/getlantern/lantern-box"

"github.com/getlantern/radiance/account"
Expand Down Expand Up @@ -205,6 +206,35 @@ func (c *Client) UpdateConfig(ctx context.Context) error {
return err
}

// CurrentUserMessage returns the pending message for the current account.
func (c *Client) CurrentUserMessage(ctx context.Context) (*wire.ResolvedUserMessage, error) {
var response CurrentUserMessageResponse
if err := c.doJSON(ctx, http.MethodGet, userMessageEndpoint, nil, &response); err != nil {
return nil, err
}
return response.Message, nil
}

// RefreshUserMessages schedules an immediate server eligibility refresh.
func (c *Client) RefreshUserMessages(ctx context.Context) error {
_, err := c.do(ctx, http.MethodPost, userMessageRefreshEndpoint, nil)
return err
}

// AcknowledgeUserMessage records that the UI displayed displayID.
func (c *Client) AcknowledgeUserMessage(ctx context.Context, displayID string) error {
_, err := c.do(ctx, http.MethodPost, userMessageAcknowledgeEndpoint,
UserMessageAcknowledgeRequest{DisplayID: displayID})
return err
}

// SetUserMessageActivity updates the app and connectivity lifecycle used by polling.
func (c *Client) SetUserMessageActivity(ctx context.Context, active, online bool) error {
_, err := c.do(ctx, http.MethodPatch, userMessageActivityEndpoint,
UserMessageActivityRequest{Active: active, Online: online})
return err
}

///////////////////////
// Server management //
///////////////////////
Expand Down
52 changes: 52 additions & 0 deletions ipc/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
rlog "github.com/getlantern/radiance/log"
"github.com/getlantern/radiance/peer"
"github.com/getlantern/radiance/unbounded"
clientmessage "github.com/getlantern/radiance/usermessage"
"github.com/getlantern/radiance/vpn"

sjson "github.com/sagernet/sing/common/json"
Expand Down Expand Up @@ -54,6 +55,11 @@ const (
configEventsEndpoint = "/config/events"
configUpdateEndpoint = "/config/update"

userMessageEndpoint = "/user-messages"
userMessageRefreshEndpoint = "/user-messages/refresh"
userMessageAcknowledgeEndpoint = "/user-messages/acknowledge"
userMessageActivityEndpoint = "/user-messages/activity"

// Server management endpoints
serversEndpoint = "/servers"
serversAddEndpoint = "/servers/add"
Expand Down Expand Up @@ -229,6 +235,11 @@ func newLocalAPI(b *backend.LocalBackend, withAuth bool) *localapi {
mux.HandleFunc("GET "+configEventsEndpoint, s.configEventsHandler)
mux.HandleFunc("POST "+configUpdateEndpoint, traced(s.configUpdateHandler))

mux.HandleFunc("GET "+userMessageEndpoint, traced(s.userMessageHandler))
mux.HandleFunc("POST "+userMessageRefreshEndpoint, traced(s.userMessageRefreshHandler))
mux.HandleFunc("POST "+userMessageAcknowledgeEndpoint, traced(s.userMessageAcknowledgeHandler))
mux.HandleFunc("PATCH "+userMessageActivityEndpoint, traced(s.userMessageActivityHandler))

// Server management
mux.HandleFunc("GET "+serversEndpoint, traced(s.serversHandler))
mux.HandleFunc("POST "+serversAddEndpoint, traced(s.serversAddHandler))
Expand Down Expand Up @@ -896,6 +907,47 @@ func (s *localapi) settingsHandler(w http.ResponseWriter, r *http.Request) {
}
}

func (s *localapi) userMessageHandler(w http.ResponseWriter, r *http.Request) {
message, err := s.backend(r.Context()).CurrentUserMessage()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, http.StatusOK, CurrentUserMessageResponse{Message: message})
}

func (s *localapi) userMessageRefreshHandler(w http.ResponseWriter, r *http.Request) {
s.backend(r.Context()).RefreshUserMessages()
w.WriteHeader(http.StatusNoContent)
}

func (s *localapi) userMessageAcknowledgeHandler(w http.ResponseWriter, r *http.Request) {
var request UserMessageAcknowledgeRequest
if err := decodeJSON(r, &request); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := s.backend(r.Context()).AcknowledgeUserMessage(request.DisplayID); err != nil {
status := http.StatusInternalServerError
if errors.Is(err, clientmessage.ErrMessageNotPending) {
status = http.StatusConflict
}
http.Error(w, err.Error(), status)
return
}
w.WriteHeader(http.StatusNoContent)
}

func (s *localapi) userMessageActivityHandler(w http.ResponseWriter, r *http.Request) {
var request UserMessageActivityRequest
if err := decodeJSON(r, &request); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
s.backend(r.Context()).SetUserMessageActivity(request.Active, request.Online)
w.WriteHeader(http.StatusNoContent)
}

func (s *localapi) envHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPatch:
Expand Down
17 changes: 17 additions & 0 deletions ipc/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package ipc

import (
"github.com/getlantern/common"
wire "github.com/getlantern/common/usermessage"

"github.com/getlantern/radiance/account"
"github.com/getlantern/radiance/issue"
Expand Down Expand Up @@ -116,6 +117,17 @@ type IssueReportRequest struct {
Attachments []*issue.Attachment `json:"attachments"`
}

// UserMessageAcknowledgeRequest identifies the pending message displayed by the UI.
type UserMessageAcknowledgeRequest struct {
DisplayID string `json:"displayID"`
}

// UserMessageActivityRequest reports whether foreground polling should run.
type UserMessageActivityRequest struct {
Active bool `json:"active"`
Online bool `json:"online"`
}

// Shared response types used by both client and server.

type SelectedServerResponse struct {
Expand Down Expand Up @@ -148,6 +160,11 @@ type PlansResponse struct {
Plans string `json:"plans"`
}

// CurrentUserMessageResponse contains the pending message, if one exists.
type CurrentUserMessageResponse struct {
Message *wire.ResolvedUserMessage `json:"message,omitempty"`
}

type ResultResponse struct {
Result string `json:"result"`
}
Loading
Loading