Files
Hashbang/main.go

405 lines
11 KiB
Go
Raw Normal View History

2025-11-20 12:46:41 +00:00
package main
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
2025-11-22 09:15:52 +00:00
"fyne.io/fyne/v2/container"
2025-11-20 12:46:41 +00:00
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/layout"
2025-11-22 09:15:52 +00:00
"fyne.io/fyne/v2/widget"
2025-11-20 12:46:41 +00:00
2025-11-22 09:15:52 +00:00
"github.com/google/uuid"
2025-11-20 12:46:41 +00:00
"github.com/kirsle/configdir"
"github.com/mattn/go-mastodon"
webview "github.com/webview/webview_go"
"encoding/json"
"path/filepath"
2025-11-22 09:15:52 +00:00
"context"
2025-11-20 12:46:41 +00:00
"errors"
"fmt"
"log"
"os"
2025-11-22 09:15:52 +00:00
"strings"
2025-11-20 12:46:41 +00:00
)
var App fyne.App
var MainWindow fyne.Window
2025-11-23 07:33:01 +00:00
var Version string = "25w47a"
2025-11-22 09:15:52 +00:00
// Client used for posting, getting posts, etc.
var Client *mastodon.Client
2025-11-22 08:10:40 +00:00
// 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 {
2025-11-22 09:15:52 +00:00
ProfileName string
2025-11-22 08:10:40 +00:00
DoNotDropToProfileSelection bool
}
2025-11-20 12:46:41 +00:00
type FederaleProfile struct { // Blueprint for a Federale profile
2025-11-22 09:15:52 +00:00
Name string // Name displayed to user
2025-11-20 12:46:41 +00:00
InternalName string // Filename
2025-11-22 09:15:52 +00:00
Server string // Homeserver to connect to
2025-11-20 12:46:41 +00:00
Username string // Username of user (user@domain)
2025-11-22 09:15:52 +00:00
ClientID string // ID of the client
2025-11-20 12:46:41 +00:00
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
2025-11-22 09:15:52 +00:00
var LoadedConfig *FederaleConfig // Config currently loaded into memory
var Profiles []*FederaleProfile // Profiles loaded from FS go here.
2025-11-20 12:46:41 +00:00
var ProfileSetupDone bool = false
var ProfileSetupProfile *FederaleProfile
var ProfileSelectionDone bool = false
var ProfileSelectionProfile *FederaleProfile
2025-11-22 08:10:40 +00:00
// 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 {
2025-11-22 09:15:52 +00:00
return err
2025-11-22 08:10:40 +00:00
}
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
}
2025-11-20 12:46:41 +00:00
// This function asks for the profile to launch Federale with.
2025-11-22 08:10:40 +00:00
func ProfileLaunch() {
2025-11-20 12:46:41 +00:00
ConfigPath := configdir.LocalConfig("federale") // Federale foler in the user's config directory
err := configdir.MakePath(ConfigPath) // Ensure it exists.
if err != nil {
2025-11-22 09:15:52 +00:00
panic(err)
2025-11-20 12:46:41 +00:00
}
Files, err := os.ReadDir(ConfigPath)
if err != nil {
panic(err)
}
for _, v := range Files {
if !v.IsDir() {
2025-11-22 08:10:40 +00:00
if v.Name() == "federale.json" {
continue
}
2025-11-20 12:46:41 +00:00
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)
}
}
2025-11-22 08:10:40 +00:00
2025-11-20 12:46:41 +00:00
App = app.New()
2025-11-22 08:10:40 +00:00
MainWindow = App.NewWindow("Select a profile")
2025-11-20 12:46:41 +00:00
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() {
2025-11-22 09:15:52 +00:00
if !strings.HasPrefix(InstanceEntry.Text, "https://") {
InstanceEntry.SetText("https://" + InstanceEntry.Text) // FIXME: This may not work with darknet instances?
}
2025-11-20 12:46:41 +00:00
Domain := InstanceEntry.Text
// Step one: register the application
AppConfig := &mastodon.AppConfig{
2025-11-22 09:15:52 +00:00
Server: Domain,
ClientName: "Federalé",
Scopes: "read write push",
Website: "https://forge.sunglocto.net",
2025-11-20 12:46:41 +00:00
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
2025-11-22 09:15:52 +00:00
dialog.ShowConfirm("Confirm", "Do you have an authorisation code?", func(b bool) {
2025-11-20 12:46:41 +00:00
if b {
NewProfile := new(FederaleProfile)
2025-11-22 09:15:52 +00:00
NewProfile.Name = fmt.Sprintf("Profile %d", len(Profiles)+1)
2025-11-22 08:10:40 +00:00
NewProfile.InternalName = uuid.New().String()
2025-11-20 12:46:41 +00:00
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 {
2025-11-22 09:15:52 +00:00
config := &mastodon.Config{
Server: NewProfile.Server,
ClientID: NewProfile.ClientID,
ClientSecret: NewProfile.ClientSecret,
}
// Create the client
c := mastodon.NewClient(config)
err = c.GetUserAccessToken(context.Background(), AuthPasswordWidget.Text, app.RedirectURI)
if err != nil {
panic(err)
}
NewProfile.UserAuthorizationCode = c.Config.AccessToken
2025-11-20 12:46:41 +00:00
fmt.Println("Successfully created user profile:\n", NewProfile)
// Save profile to disk
b, err := json.MarshalIndent(NewProfile, "", "\t")
if err != nil {
panic(err)
}
2025-11-22 09:15:52 +00:00
err = os.WriteFile(filepath.Join(ConfigPath, NewProfile.InternalName+".json"), b, 0644)
2025-11-20 12:46:41 +00:00
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 {
2025-11-22 08:10:40 +00:00
2025-11-20 12:46:41 +00:00
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.
2025-11-22 08:10:40 +00:00
LoadedConfig.ProfileName = ProfileSetupProfile.InternalName
LoadedConfig.DoNotDropToProfileSelection = true
err = SaveConfigToDisk()
if err != nil {
panic(err)
}
App.SendNotification(fyne.NewNotification("Done", "Relaunch the application"))
2025-11-20 12:46:41 +00:00
} else { // A profile was either picked from the list OR no profile was picked by the user.
if ProfileSelectionDone {
2025-11-22 08:10:40 +00:00
LoadedConfig.ProfileName = ProfileSelectionProfile.InternalName
LoadedConfig.DoNotDropToProfileSelection = true
err = SaveConfigToDisk()
if err != nil {
panic(err)
}
App.SendNotification(fyne.NewNotification("Done", "Relaunch the application"))
2025-11-20 12:46:41 +00:00
} else {
2025-11-22 08:10:40 +00:00
panic(errors.New("no profile specified"))
2025-11-20 12:46:41 +00:00
}
}
}
func main() {
2025-11-22 08:10:40 +00:00
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 {
2025-11-22 09:15:52 +00:00
panic(err)
2025-11-22 08:10:40 +00:00
}
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
2025-11-20 12:46:41 +00:00
if err != nil {
panic(err)
}
2025-11-22 08:10:40 +00:00
log.Println("Unmarshalling JSON")
tempconf := new(FederaleConfig)
err = json.Unmarshal(b, tempconf)
fmt.Println(tempconf)
if err != nil {
panic(err)
}
LoadedConfig = tempconf
2025-11-23 07:33:01 +00:00
if !LoadedConfig.DoNotDropToProfileSelection || (len(os.Args) > 1 && os.Args[1] == "new") {
2025-11-22 08:10:40 +00:00
log.Println("Launching profile selection")
ProfileLaunch()
return
}
2025-11-22 09:15:52 +00:00
ProfilePath := filepath.Join(ConfigPath, LoadedConfig.ProfileName+".json")
2025-11-22 08:10:40 +00:00
b, err = os.ReadFile(ProfilePath)
if err != nil {
panic(err)
}
2025-11-22 09:15:52 +00:00
tempprof := new(FederaleProfile)
2025-11-22 08:10:40 +00:00
log.Println("Unmarshalling config to RAM")
2025-11-22 09:15:52 +00:00
err = json.Unmarshal(b, tempprof)
2025-11-22 08:10:40 +00:00
if err != nil {
2025-11-22 09:15:52 +00:00
panic(err)
2025-11-22 08:10:40 +00:00
}
2025-11-22 09:15:52 +00:00
LoadedProfile = tempprof
2025-11-20 12:46:41 +00:00
////////////////////////////////////////////////////
2025-11-22 09:15:52 +00:00
config := &mastodon.Config{
Server: LoadedProfile.Server,
ClientID: LoadedProfile.ClientID,
ClientSecret: LoadedProfile.ClientSecret,
AccessToken: LoadedProfile.UserAuthorizationCode,
}
Client = mastodon.NewClient(config)
log.Println(Client)
2025-11-20 12:46:41 +00:00
App = app.New()
MainWindow = App.NewWindow("Federalé")
2025-11-23 07:33:01 +00:00
TootEntry := widget.NewMultiLineEntry()
ReplyIDEntry := widget.NewEntry()
ReplyIDLabel := widget.NewLabel("In reply to")
ReplyBox := container.NewHBox(ReplyIDLabel, ReplyIDEntry)
Timeline := container.NewVBox(widget.NewLabel("Please Wait..."))
go func() {
NewTimeline := container.NewVBox(widget.NewLabel("Posts"))
//pg := new(mastodon.Pagination)
///
Timeline = NewTimeline
}()
MainWindow.SetContent(container.NewHBox(container.NewVBox(TootEntry, ReplyBox, widget.NewButton("Post", func() {
2025-11-22 09:15:52 +00:00
toot := mastodon.Toot{
Status: TootEntry.Text,
Visibility: "public",
2025-11-23 07:33:01 +00:00
InReplyToID: mastodon.ID(ReplyIDEntry.Text),
2025-11-22 09:15:52 +00:00
}
_, err := Client.PostStatus(context.Background(), &toot)
if err != nil {
dialog.ShowError(err, MainWindow)
}
2025-11-23 07:33:01 +00:00
})), Timeline))
2025-11-20 12:46:41 +00:00
MainWindow.ShowAndRun()
}