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" webview "github.com/webview/webview_go" "encoding/json" "path/filepath" "errors" "fmt" "log" "os" "context" ) var App fyne.App var MainWindow fyne.Window 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 Profiles []*FederaleProfile // Profiles loaded from FS go here. var ProfileSetupDone bool = false var ProfileSetupProfile *FederaleProfile var ProfileSelectionDone bool = false var ProfileSelectionProfile *FederaleProfile // This function asks for the profile to launch Federale with. func ProfileLaunch() (error, *FederaleProfile) { 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() { 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() 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 = "boob" NewProfile.InternalName = "boob123456" // TODO: Use UUID 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 = App.NewWindow("Please pick a profile") MainWindow.SetContent(RootBox) MainWindow.SetFixedSize(true) MainWindow.ShowAndRun() if ProfileSetupDone { // A new profile was created. Return this new profile. return nil, ProfileSetupProfile } else { // A profile was either picked from the list OR no profile was picked by the user. if ProfileSelectionDone { return nil, ProfileSelectionProfile } else { return errors.New("no profile specified"), new(FederaleProfile) } } } func main() { err, profile := ProfileLaunch() if err != nil { panic(err) } LoadedProfile = profile //////////////////////////////////////////////////// App = app.New() MainWindow = App.NewWindow("Federalé") MainWindow.SetContent(widget.NewLabel("Hello World!")) MainWindow.ShowAndRun() }