Skip to content

Commit

Permalink
Print notice if password recovery email is not set
Browse files Browse the repository at this point in the history
This was requested by the Winlink Development Team.

Resolves la5nta#442
  • Loading branch information
martinhpedersen committed Mar 29, 2024
1 parent 7e1488d commit 5b56bc2
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 93 deletions.
8 changes: 4 additions & 4 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -377,10 +377,10 @@ func main() {
if config.VersionReportingDisabled {
return
}

for { // Check every 6 hours, but it won't post more frequent than 24h.
postVersionUpdate() // Ignore errors
time.Sleep(6 * time.Hour)
for {
postVersionUpdate() // 24 hour hold on success
checkPasswordRecoveryEmailIsSet(ctx) // 14 day hold on success
time.Sleep(6 * time.Hour) // Retry every 6 hours
}
}()
}
Expand Down
89 changes: 0 additions & 89 deletions version_report.go

This file was deleted.

137 changes: 137 additions & 0 deletions winlink_api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package main

import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"runtime"
"time"

"github.com/la5nta/pat/internal/buildinfo"
"github.com/la5nta/pat/internal/cmsapi"
"github.com/la5nta/pat/internal/debug"
"github.com/la5nta/pat/internal/directories"
)

// doIfElapsed implements a per-callsign rate limited function.
func doIfElapsed(name string, t time.Duration, fn func() error) error {
filePath := filepath.Join(directories.StateDir(), "."+name+"_"+fOptions.MyCall+".json")
file, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE, 0o600)
if err != nil {
return err
}
defer file.Close()

var lastUpdated time.Time
json.NewDecoder(file).Decode(&lastUpdated)
if since := time.Since(lastUpdated); since < t {
debug.Printf("Skipping %q (last run: %s ago)", name, since.Truncate(time.Minute))
return nil
}

if err := fn(); err != nil {
return err
}

file.Truncate(0)
file.Seek(0, 0)
return json.NewEncoder(file).Encode(time.Now())
}

func postVersionUpdate() {
const interval = 24 * time.Hour
err := doIfElapsed("version_report", interval, func() error {
debug.Printf("Posting version update...")
// WDT do not want us to post version reports for callsigns without a registered account
if exists, err := accountExists(fOptions.MyCall); err != nil {
return err
} else if !exists {
return fmt.Errorf("account does not exist")
}
return cmsapi.VersionAdd{
Callsign: fOptions.MyCall,
Program: buildinfo.AppName,
Version: buildinfo.Version,
Comments: fmt.Sprintf("%s - %s/%s", buildinfo.GitRev, runtime.GOOS, runtime.GOARCH),
}.Post()
})
if err != nil {
debug.Printf("Failed to post version update: %v", err)
}
}

func checkPasswordRecoveryEmailIsSet(ctx context.Context) {
const interval = 14 * 24 * time.Hour
err := doIfElapsed("pw_recovery_email_check", interval, func() error {
debug.Printf("Checking if winlink.org password recovery email is set...")
set, err := passwordRecoveryEmailSet(ctx)
if err != nil {
return err
}
debug.Printf("Password recovery email set: %t", set)
if set {
return nil
}
fmt.Println("")
fmt.Println("WINLINK NOTICE: Password recovery email is not set for your Winlink account. It is highly recommended to do so.")
fmt.Println("Run `" + os.Args[0] + " account --help` for help setting your recovery address. You can also manage your account settings at https://winlink.org/.")
fmt.Println("")
return nil
})
if err != nil {
debug.Printf("Failed to check if password recovery email is set: %v", err)
}
}

func passwordRecoveryEmailSet(ctx context.Context) (bool, error) {
if config.SecureLoginPassword == "" {
return false, fmt.Errorf("missing password")
}
switch exists, err := accountExists(fOptions.MyCall); {
case err != nil:
return false, fmt.Errorf("error checking if account exist: %w", err)
case !exists:
return false, fmt.Errorf("account does not exist")
}
email, err := cmsapi.PasswordRecoveryEmailGet(ctx, fOptions.MyCall, config.SecureLoginPassword)
return email != "", err
}

func accountExists(callsign string) (bool, error) {
var cache struct {
Expires time.Time
AccountExists bool
}

fileName := fmt.Sprintf(".cached_account_check_%s.json", callsign)
filePath := filepath.Join(directories.StateDir(), fileName)
f, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE, 0o600)
if err != nil {
return false, err
}
json.NewDecoder(f).Decode(&cache)
if time.Since(cache.Expires) < 0 {
return cache.AccountExists, nil
}
defer func() {
f.Truncate(0)
f.Seek(0, 0)
json.NewEncoder(f).Encode(cache)
}()

debug.Printf("Checking if account exists...")
exists, err := cmsapi.AccountExists(callsign)
debug.Printf("Account exists: %t (%v)", exists, err)
if !exists || err != nil {
// Let's try again in 48 hours
cache.Expires = time.Now().Add(48 * time.Hour)
return false, err
}

// Keep this response for a month. It will probably not change.
cache.Expires = time.Now().Add(30 * 24 * time.Hour)
cache.AccountExists = exists
return exists, err
}

0 comments on commit 5b56bc2

Please sign in to comment.