package main import ( "citrons.xyz/talk/client/client" "citrons.xyz/talk/client/object" "citrons.xyz/talk/client/window" "citrons.xyz/talk/tui" "citrons.xyz/talk/proto" ) type application struct { client.Client quit bool connected bool reconnecting bool authenticated bool uid string cache object.ObjCache windowCache window.WindowCache currentWindow window.Location windowHist []window.Location channelList channelList channelScroll tui.ScrollState redraw bool cmdWindow cmdWindow prompts []window.Prompt activePaste <-chan string } func newApplication(serverAddress string) *application { var app application app.Client = client.New(serverAddress) app.cache = object.NewCache(&app) app.windowCache = window.NewCache() app.goTo(app.cmdWindow.Location()) app.cmdWindow.info("connecting to %s", app.Client.Address) app.redraw = true return &app } func (a *application) OnConnect() { u := a.cache.Get(a.uid) var username string if u != nil { username = u.Fields[""] } a.pushPrompt(newLoginPrompt(username)) a.connected = true a.reconnecting = false a.cache = object.NewCache(a) a.cmdWindow.info("connected to %s", a.Client.Address) } func (a *application) OnDisconnect(err error) { a.connected = false a.authenticated = false a.prompts = nil a.windowCache = window.NewCache() if !a.reconnecting { a.cmdWindow.err( "disconnected from %s: %s\nreconnecting...", a.Client.Address, err, ) a.reconnecting = true } } func (a *application) OnEvent(cmd proto.Command) { switch cmd.Kind { case "p": if len(cmd.Args) > 0 { a.onPut(cmd.Target, cmd.Args[0]) } case "update": if len(cmd.Args) > 0 { a.onUpdate(cmd.Target, cmd.Args[0]) } case "delete": a.cache.Gone(cmd.Target) fallthrough case "leave": cl := channelLocation {id: cmd.Target} w := a.windowCache.Get(cl) if w != nil { w.(*channelWindow).onLeaveChannel() } a.channelList.remove(cl) case "join": a.cache.Fetch(cmd.Target, func(ch *proto.Object) { if ch != nil { a.newChannel(*ch) } }) } } func (a *application) onPut(target string, msg proto.Object) { a.windowCache.ForAll(func(win window.Window) { switch win.(type) { case *channelWindow: if win.(*channelWindow).location.id == target { win.(*channelWindow).put(msg) } } }) } func (a *application) OnResponse(requestId string, cmd proto.Command) { switch cmd.Kind { case "you-are": if len(cmd.Args) > 0 { a.cmdWindow.info("your name is: %s", cmd.Args[0].Fields[""]) } } a.OnEvent(cmd) } func (a *application) onUpdate(target string, update proto.Object) { if target == a.uid { a.logUserUpdate(target, update) } a.cache.Update(target, update) } func (a *application) Sub(id string) { a.Request(proto.NewCmd("s", id), func(cmd proto.Command) { if cmd.Kind == "i" && len(cmd.Args) > 0 { a.cache.Update(id, cmd.Args[0]) } }) } func (a *application) Unsub(id string) { a.Request(proto.NewCmd("u", id), nil) } func (a *application) GetInfo(id string, callback func(*proto.Object)) { a.Request(proto.NewCmd("i", id), func(cmd proto.Command) { if cmd.Kind == "i" && len(cmd.Args) > 0 { callback(&cmd.Args[0]) } else { callback(nil) } }) } func (a *application) authAnon(as string, callback func(ok bool, uid string)) { cb := func(response proto.Command) { switch response.Kind { case "you-are": if len(response.Args) == 0 { callback(false, "") break } me := response.Args[0] a.onAuth(me.Id) if callback != nil { callback(true, me.Id) } case "fail": var uid string if len(response.Args) != 0 { if response.Args[0].Kind == "name-taken" && response.Args[0].Fields["anonymous"] == "no" { uid = response.Args[0].Fields["id"] } else { a.cmdWindow.err(proto.Strfail(response.Args[0])) } } if callback != nil { callback(false, uid) } } } a.Request(proto.NewCmd("auth", "", proto.Object { "anonymous", "", map[string]string {"": as}, }), cb) } func (a *application) authPassword( uid string, password string, callback func(ok bool)) { cb := func(response proto.Command) { switch response.Kind { case "you-are": if len(response.Args) == 0 { callback(false) break } me := response.Args[0] a.onAuth(me.Id) if callback != nil { callback(true) } case "fail": if len(response.Args) != 0 { a.cmdWindow.err(proto.Strfail(response.Args[0])) } if callback != nil { callback(false) } } } a.Request(proto.NewCmd("auth", "", proto.Object { "password", "", map[string]string {"": password, "id": uid}, }), cb) } func (a *application) setPassword(password string, callback func(ok bool)) { cb := func(response proto.Command) { switch response.Kind { case "ok": if callback != nil { callback(true) } case "fail": if len(response.Args) != 0 && response.Args[0].Kind == "password-required" { lp := newLoginPrompt("") lp.uid = a.uid lp.customPrompt = "current password" lp.callback = func(ok bool) { if ok { a.setPassword(password, callback) } else { if callback != nil { callback(false) } } } a.pushPrompt(lp) } else { if len(response.Args) != 0 { a.cmdWindow.err(proto.Strfail(response.Args[0])) } if callback != nil { callback(false) } } } } a.Request(proto.NewCmd("auth-update", a.uid, proto.Object { "password", "", map[string]string {"": password}, }), cb) } func (a *application) onAuth(uid string) { a.authenticated = true a.uid = uid a.cache.Watch(uid) a.Request(proto.NewCmd("channels", ""), func(response proto.Command) { if response.Kind == "channels" { previousChannels := make(channelList, len(a.channelList)) copy(previousChannels, a.channelList) a.channelList.setChannels(response.Args) u := a.cache.Get(a.uid) if u.Fields["anonymous"] == "yes" { for _, c := range previousChannels { cb := func(response proto.Command) { if response.Kind == "ok" { a.channelList = append(a.channelList, c) } w := a.windowCache.Get(c.location) switch w.(type) { case *channelWindow: w.(*channelWindow).endOfHistory = false } } a.Request(proto.NewCmd("join", c.location.id), cb) } } } }) a.cmdWindow.info("welcome! type /help for help. try: /join talk") } func (a *application) sendUpdate(o proto.Object, cb func(proto.Command)) { a.Request(proto.NewCmd("update", o.Id, o), cb) } func (a *application) lookup( name string, kind string, callback func(*proto.Object, *proto.Fail)) { a.lookupAll([]string {name}, kind, func(os []proto.Object, f *proto.Fail) { if f == nil { callback(&os[0], f) } else { callback(nil, f) } }) } func (a *application) lookupAll(names []string, kind string, callback func([]proto.Object, *proto.Fail)) { cb := func(response proto.Command) { switch response.Kind { case "i": callback(response.Args, nil) case "fail": if len(response.Args) > 0 { f := proto.Fail(response.Args[0]) callback(nil, &f) } } } var os []proto.Object for _, name := range names { os = append(os, proto.Object {kind, "", map[string]string {"": name}}) } a.Request(proto.NewCmd("lookup", "", os...), cb) } func (a *application) join(channelName string) { a.lookup(channelName, "channel", func(ch *proto.Object, f *proto.Fail) { if f != nil { a.cmdWindow.err(f.Error()) return } a.Request(proto.NewCmd("join", ch.Id), func(response proto.Command) { switch response.Kind { case "ok": a.newChannel(*ch) w := a.windowCache.Get(channelLocation {id: ch.Id}) switch w.(type) { case *channelWindow: w.(*channelWindow).endOfHistory = false } a.goTo(channelLocation {id: ch.Id}) case "fail": if len(response.Args) > 0 { f := proto.Fail(response.Args[0]) a.cmdWindow.err(f.Error()) } } }) }) } func (a *application) joinDirect(among []proto.Object) { cb := func(response proto.Command) { switch response.Kind { case "direct": if len(response.Args) > 0 { ch := response.Args[0] a.goTo(channelLocation {id: ch.Id}) } case "fail": if len(response.Args) > 0 { f := proto.Fail(response.Args[0]) a.cmdWindow.err(f.Error()) } } } a.Request(proto.Command{"direct", "", among}, cb) } func (a *application) createChannel(name string) { ch := proto.Object {"channel", "", map[string]string {"": name}} a.Request(proto.NewCmd("create", "", ch), func(response proto.Command) { switch response.Kind { case "create": if len(response.Args) > 0 { a.newChannel(response.Args[0]) a.goTo(channelLocation {id: response.Args[0].Id}) } case "fail": if len(response.Args) > 0 { f := proto.Fail(response.Args[0]) a.cmdWindow.err(f.Error()) } } }) } func (a *application) newChannel(ch proto.Object) { a.channelList.add(ch.Fields[""], channelLocation {id: ch.Id}) a.channelScroll.Set(0) } func (a *application) getNick() string { if !a.authenticated { return "" } u := a.cache.Get(a.uid) if u != nil { return u.Fields[""] } return "" } func (a *application) setNick(newName string) { if !a.authenticated { return } callback := func(response proto.Command) { switch response.Kind { case "fail": if len(response.Args) != 0 { a.cmdWindow.err(proto.Strfail(response.Args[0])) } } } a.sendUpdate( proto.Object {"u", a.uid, map[string]string {"": newName}}, callback, ) }