package main import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/container" "github.com/kirsle/configdir" "github.com/mattn/go-mastodon" "github.com/google/uuid" webview "github.com/webview/webview_go" "encoding/json" "path/filepath" "errors" "fmt" "log" "os" "context" ) var App fyne.App var MainWindow fyne.Window // Federale config settings apply to all profiles. // The config stores the name of the profile to launch, // as well as if the profile selection screen should // show when the program is next launched. type FederaleConfig struct { ProfileName string DoNotDropToProfileSelection bool } type FederaleProfile struct { // Blueprint for a Federale profile Name string // Name displayed to user InternalName string // Filename Server string // Homeserver to connect to Username string // Username of user (user@domain) ClientID string // ID of the client ClientSecret string // Secret of the client UserAuthorizationCode string // Authorization code of the user Running bool // Whether the profile is currently running } var LoadedProfile *FederaleProfile // Profile currently loaded into memory for this Federale instance var LoadedConfig *FederaleConfig // Config currently loaded into memory var Profiles []*FederaleProfile // Profiles loaded from FS go here. var ProfileSetupDone bool = false var ProfileSetupProfile *FederaleProfile var ProfileSelectionDone bool = false var ProfileSelectionProfile *FederaleProfile // This function saves the config in memory to disk. func SaveConfigToDisk() error { ConfigPath := configdir.LocalConfig("federale") // Federale foler in the user's config directory err := configdir.MakePath(ConfigPath) // Ensure it exists. if err != nil { return err } ConfigFilePath := filepath.Join(ConfigPath, "federale.json") b, err := json.MarshalIndent(LoadedConfig, "", "\t") if err != nil { return err } log.Println("Saving configuration to disk") err = os.WriteFile(filepath.Join(ConfigFilePath), b, 0644) if err != nil { return err } return nil } // This function asks for the profile to launch Federale with. func ProfileLaunch() { ConfigPath := configdir.LocalConfig("federale") // Federale foler in the user's config directory err := configdir.MakePath(ConfigPath) // Ensure it exists. if err != nil { panic(err) } Files, err := os.ReadDir(ConfigPath) if err != nil { panic(err) } for _, v := range Files { if !v.IsDir() { if v.Name() == "federale.json" { continue } Profile := new(FederaleProfile) dat, err := os.ReadFile(filepath.Join(ConfigPath, v.Name())) if err != nil { log.Println("ERR - " + err.Error()) continue } err = json.Unmarshal(dat, Profile) if err != nil { log.Println("ERR - " + err.Error()) continue } Profiles = append(Profiles, Profile) } } App = app.New() MainWindow = App.NewWindow("Select a profile") Box := container.NewVBox() Box.Add(widget.NewRichTextFromMarkdown("# Please pick a profile")) Box.Add(widget.NewLabel(fmt.Sprintf("There are %d profile(s) available.", len(Profiles)))) ProfileSelection := container.NewVBox( container.NewHBox( widget.NewButton("+ Add profile", func() { AddProfileBox := container.NewVBox() InstanceEntry := widget.NewEntry() InstanceEntry.SetPlaceHolder("https://example.com") AddProfileWindow := App.NewWindow("Add a profile") AddProfileWindow.SetFixedSize(true) GoButton := widget.NewButton("Go", func() { Domain := InstanceEntry.Text // Step one: register the application AppConfig := &mastodon.AppConfig{ Server: Domain, ClientName: "Federalé", Scopes: "read write push", Website: "https://forge.sunglocto.net", RedirectURIs: "urn:ietf:wg:oauth:2.0:oob", } app, err := mastodon.RegisterApp(context.Background(), AppConfig) if err != nil { dialog.ShowError(err, AddProfileWindow) } fmt.Println("App successfully created:\n", app) // Step two: the user now needs to log in Webview := webview.New(false) defer Webview.Destroy() Webview.SetTitle("Authenticate - copy the authorisation code when logged in.") Webview.SetSize(480, 320, webview.HintNone) Webview.Navigate(app.AuthURI) Webview.Run() AddProfileWindow.Resize(fyne.NewSize(500, 500)) // Step three: get the authorization code from the user dialog.ShowConfirm("Confirm", "Do you have an authorisation code?", func (b bool) { if b { NewProfile := new(FederaleProfile) NewProfile.Name = fmt.Sprintf("Profile %d", len(Profiles) + 1) NewProfile.InternalName = uuid.New().String() NewProfile.Server = Domain NewProfile.ClientID = app.ClientID NewProfile.ClientSecret = app.ClientSecret AuthPasswordWidget := widget.NewPasswordEntry() AuthCodeInput := new(widget.FormItem) AuthCodeInput.Text = "Authorization code" AuthCodeInput.Widget = AuthPasswordWidget var FormItems []*widget.FormItem FormItems = append(FormItems, AuthCodeInput) dialog.ShowForm("Enter authorization code", "Continue", "Exit", FormItems, func(b bool) { if b { NewProfile.UserAuthorizationCode = AuthPasswordWidget.Text fmt.Println("Successfully created user profile:\n", NewProfile) // Save profile to disk b, err := json.MarshalIndent(NewProfile, "", "\t") if err != nil { panic(err) } err = os.WriteFile(filepath.Join(ConfigPath, NewProfile.InternalName + ".json"), b, 0644) if err != nil { panic(err) } ProfileSetupDone = true ProfileSetupProfile = NewProfile AddProfileWindow.Close() MainWindow.Close() } }, AddProfileWindow) } }, AddProfileWindow) }) GoButton.Importance = widget.HighImportance AddProfileBox.Add(widget.NewLabel("Federalé will log in via OAuth2\nInstance:")) AddProfileBox.Add(InstanceEntry) AddProfileBox.Add(GoButton) AddProfileWindow.SetContent(AddProfileBox) AddProfileWindow.Show() }), ), ) for _, v := range Profiles { ProfileSelection.Add( container.NewHBox( widget.NewLabel(v.Name), widget.NewButton("Log in", func() { ProfileSelectionDone = true ProfileSelectionProfile = v MainWindow.Close() }), ), ) } Box.Add(ProfileSelection) RootBox := container.New(layout.NewCenterLayout(), Box) MainWindow.SetContent(RootBox) MainWindow.SetFixedSize(true) MainWindow.ShowAndRun() if ProfileSetupDone { // A new profile was created. Return this new profile. LoadedConfig.ProfileName = ProfileSetupProfile.InternalName LoadedConfig.DoNotDropToProfileSelection = true err = SaveConfigToDisk() if err != nil { panic(err) } App.SendNotification(fyne.NewNotification("Done", "Relaunch the application")) } else { // A profile was either picked from the list OR no profile was picked by the user. if ProfileSelectionDone { LoadedConfig.ProfileName = ProfileSelectionProfile.InternalName LoadedConfig.DoNotDropToProfileSelection = true err = SaveConfigToDisk() if err != nil { panic(err) } App.SendNotification(fyne.NewNotification("Done", "Relaunch the application")) } else { panic(errors.New("no profile specified")) } } } func main() { log.Println("Checking for federale config") ConfigPath := configdir.LocalConfig("federale") // Federale foler in the user's config directory err := configdir.MakePath(ConfigPath) // Ensure it exists. log.Println("Creating federale folder if it does not exist") if err != nil { panic(err) } log.Println("Checking if configuration file exists") // Check if the Federale configuration file exists _, err = os.Stat(filepath.Join(ConfigPath, "federale.json")) if errors.Is(err, os.ErrNotExist) { log.Println("Creating new configuration") // Create a new configuration EmptyConfig := new(FederaleConfig) EmptyConfig.DoNotDropToProfileSelection = false log.Println("Converting configuration to JSON") b, err := json.MarshalIndent(EmptyConfig, "", "\t") if err != nil { panic(err) } log.Println("Saving configuration to disk") err = os.WriteFile(filepath.Join(ConfigPath, "federale.json"), b, 0644) if err != nil { panic(err) } } else if err != nil { panic(err) } // Read the config from disk log.Println("Grabbing config from disk") b, err := os.ReadFile(filepath.Join(ConfigPath, "federale.json")) // You get the gist by now if err != nil { panic(err) } log.Println("Unmarshalling JSON") tempconf := new(FederaleConfig) err = json.Unmarshal(b, tempconf) fmt.Println(tempconf) if err != nil { panic(err) } LoadedConfig = tempconf if !LoadedConfig.DoNotDropToProfileSelection { log.Println("Launching profile selection") ProfileLaunch() return } ProfilePath := filepath.Join(ConfigPath, LoadedConfig.ProfileName + ".json") log.Println("Reading profile from disk") b, err = os.ReadFile(ProfilePath) if err != nil { panic(err) } log.Println("Unmarshalling config to RAM") err = json.Unmarshal(b, LoadedConfig) if err != nil { return } //////////////////////////////////////////////////// App = app.New() MainWindow = App.NewWindow("Federalé") MainWindow.SetContent(widget.NewLabel("Hello World!")) MainWindow.ShowAndRun() }