at main 1636 lines 42 kB view raw
1package main 2 3import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io" 9 "net/http" 10 "os" 11 "strconv" 12 "strings" 13 "time" 14 "unicode/utf16" 15 16 "github.com/bluesky-social/indigo/api/atproto" 17 "github.com/bluesky-social/indigo/atproto/client" 18 "github.com/bluesky-social/indigo/atproto/identity" 19 "github.com/bluesky-social/indigo/atproto/syntax" 20 "github.com/bluesky-social/indigo/lex/util" 21 "github.com/charmbracelet/bubbles/list" 22 "github.com/charmbracelet/bubbles/textinput" 23 "github.com/charmbracelet/bubbles/viewport" 24 tea "github.com/charmbracelet/bubbletea" 25 "github.com/charmbracelet/lipgloss" 26 "github.com/gorilla/websocket" 27 "github.com/rachel-mp4/lrcproto/gen/go" 28 "github.com/rachel-mp4/ttyxcvr/lex" 29 "google.golang.org/protobuf/proto" 30) 31 32const White = lipgloss.Color("#ffffff") 33const Black = lipgloss.Color("#000000") 34const Olive = lipgloss.Color("#c6c013") 35const Forest = lipgloss.Color("#034732") 36const Green = lipgloss.Color("#008148") 37const Orange = lipgloss.Color("#ef8a17") 38 39var verySubduedColor = lipgloss.AdaptiveColor{Light: "#DDDADA", Dark: "#3C3C3C"} 40var subduedColor = lipgloss.AdaptiveColor{Light: "#9B9B9B", Dark: "#5C5C5C"} 41var subduedStyle = lipgloss.NewStyle().Foreground(subduedColor) 42 43var PlainStyle = lipgloss.NewStyle().Foreground(White).Background(Black) 44 45type txstate int 46 47const ( 48 Splash txstate = iota 49 Error 50 GettingChannels 51 ChannelList 52 ResolvingChannel 53 ConnectingToChannel 54 DialingChannel 55 Connected 56) 57 58type txmode int 59 60const ( 61 Normal txmode = iota 62 Insert 63) 64 65type model struct { 66 cmding bool 67 cmdout *string 68 error *error 69 prompt textinput.Model 70 clm *channellistmodel 71 cm *channelmodel 72 gsd *globalsettingsdata 73} 74 75type channellistmodel struct { 76 channels []Channel 77 list list.Model 78 gsd *globalsettingsdata 79} 80 81type channelmodel struct { 82 channel Channel 83 mode txmode 84 wsurl string 85 lrcconn *websocket.Conn 86 lexconn *websocket.Conn 87 cancel func() 88 vp viewport.Model 89 draft textinput.Model 90 msgs map[uint32]*Message 91 myid *uint32 92 render []*string 93 sentmsg *string 94 topic *string 95 signeturi *string 96 datachan chan []byte 97 gsd *globalsettingsdata 98} 99 100type globalsettingsdata struct { 101 color *uint32 102 nick *string 103 handle *string 104 xrpc *PasswordClient 105 width int 106 height int 107 state txstate 108} 109 110type Message struct { 111 nick *string 112 handle *string 113 color *uint32 114 active bool 115 text string 116 rendered *string 117} 118 119type Profile struct { 120 Type string `json:"$type"` 121 Did string `json:"did"` 122 Handle *string `json:"handle,omitempty"` 123 DisplayName *string `json:"displayName,omitempty"` 124 Status *string `json:"status,omitempty"` 125 Color *uint32 `json:"color"` 126} 127 128type Channel struct { 129 Type string `json:"$type"` 130 URI string `json:"uri"` 131 Host string `json:"host"` 132 Creator Profile `json:"creator"` 133 Title string `json:"title"` 134 Topic *string `json:"topic,omitempty"` 135 CreatedAt time.Time `json:"createdAt"` 136} 137 138type ChannelItem struct { 139 channel Channel 140} 141 142func (c ChannelItem) Title() string { 143 return c.channel.Title 144} 145 146func (c ChannelItem) Description() string { 147 if c.channel.Topic != nil { 148 return *c.channel.Topic 149 } 150 return "" 151} 152 153func (c ChannelItem) URI() string { 154 return c.channel.URI 155} 156func (c ChannelItem) Host() string { 157 return c.channel.Host 158} 159 160func (c ChannelItem) FilterValue() string { 161 return c.channel.Title 162} 163 164type ChannelItemDelegate struct{} 165 166func (d ChannelItemDelegate) Height() int { return 3 } 167func (d ChannelItemDelegate) Spacing() int { return 0 } 168func (d ChannelItemDelegate) Update(msg tea.Msg, list *list.Model) tea.Cmd { return nil } 169func (d ChannelItemDelegate) Render(w io.Writer, m list.Model, index int, item list.Item) { 170 var title string 171 var desc string 172 var uri string 173 var host string 174 var color *uint32 175 var author string 176 if i, ok := item.(ChannelItem); ok { 177 title = i.Title() 178 desc = i.Description() 179 author = fmt.Sprintf("(%s)", renderName(i.channel.Creator.DisplayName, i.channel.Creator.Handle)) 180 host = subduedStyle.Render(fmt.Sprintf("(hosted on %s)", i.Host())) 181 if desc == "" { 182 desc = subduedStyle.Render("no provided description") 183 } 184 uri = i.URI() 185 color = i.channel.Creator.Color 186 } else { 187 return 188 } 189 if index == m.Index() { 190 greenStyle := lipgloss.NewStyle().Foreground(ColorFromInt(color)) 191 title = fmt.Sprintf("│%s %s", greenStyle.Render(title), author) 192 desc = fmt.Sprintf("│%s", desc) 193 uri = fmt.Sprintf("└%s", strings.Repeat("─", m.Width()-1)) 194 } else { 195 s := lipgloss.NewStyle() 196 s = s.Foreground(subduedColor) 197 uri = s.Render(uri) 198 host = subduedStyle.Render(author) 199 } 200 fmt.Fprintf(w, "%s %s\n%s\n%s", title, host, desc, uri) 201} 202 203func initialModel() model { 204 prompt := textinput.New() 205 prompt.Prompt = ":" 206 prompt.Width = 28 //: + prompt.Width + 1 left over for blinky = initialWidth 207 nick := "wanderer" 208 color := uint32(33096) 209 gsd := globalsettingsdata{ 210 nick: &nick, 211 color: &color, 212 width: 30, 213 height: 20, 214 state: Splash, 215 } 216 return model{ 217 prompt: prompt, 218 gsd: &gsd, 219 } 220} 221func (m model) Init() tea.Cmd { 222 return nil 223} 224 225func (m model) updateSplash(msg tea.Msg) (tea.Model, tea.Cmd) { 226 switch msg := msg.(type) { 227 case tea.KeyMsg: 228 switch msg.String() { 229 case "q": 230 return m, tea.Quit 231 default: 232 m.gsd.state = GettingChannels 233 return m, GetChannels 234 } 235 } 236 return m, nil 237} 238 239func GetChannels() tea.Msg { 240 c := &http.Client{Timeout: 10 * time.Second} 241 res, err := c.Get("http://xcvr.org/xrpc/org.xcvr.feed.getChannels") 242 243 if err != nil { 244 return errMsg{err} 245 } 246 if res.StatusCode != 200 { 247 return errMsg{errors.New(fmt.Sprintf("error getting channels: %d", res.StatusCode))} 248 } 249 decoder := json.NewDecoder(res.Body) 250 var channels []Channel 251 err = decoder.Decode(&channels) 252 if err != nil { 253 return errMsg{err} 254 } 255 return channelsMsg{channels} 256} 257 258type channelsMsg struct{ channels []Channel } 259 260type errMsg struct{ err error } 261 262func login(handle string, secret string) tea.Cmd { 263 return func() tea.Msg { 264 hdl, err := syntax.ParseHandle(handle) 265 if err != nil { 266 err = errors.New("handle failed to parse: " + err.Error()) 267 return errMsg{err} 268 } 269 id, err := identity.DefaultDirectory().LookupHandle(context.Background(), hdl) 270 if err != nil { 271 err = errors.New("handle failed to loopup: " + err.Error()) 272 return errMsg{err} 273 } 274 xrpc := NewPasswordClient(id.DID.String(), id.PDSEndpoint()) 275 err = xrpc.CreateSession(context.Background(), handle, secret) 276 if err != nil { 277 return errMsg{err} 278 } 279 return loggedInMsg{xrpc} 280 } 281} 282 283type loggedInMsg struct { 284 xrpc *PasswordClient 285} 286 287func (cm *channelmodel) updateLRCIdentity() { 288 if cm != nil && cm.lrcconn != nil { 289 err := sendSet(cm.datachan, cm.gsd.nick, cm.gsd.handle, cm.gsd.color) 290 if err != nil { 291 send(errMsg{err}) 292 } 293 } 294} 295 296func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 297 switch msg := msg.(type) { 298 case tea.KeyMsg: 299 if msg.String() == "ctrl+c" { 300 return m, tea.Quit 301 } 302 if m.cmdout != nil { 303 m.cmdout = nil 304 return m, nil 305 } 306 if (m.cm != nil && m.cm.mode == Insert) || (m.clm != nil && m.clm.list.FilterState() == list.Filtering) { 307 break 308 } 309 if !m.cmding { 310 if msg.String() == ":" { 311 m.cmding = true 312 return m, m.prompt.Focus() 313 } 314 } else { 315 switch msg.String() { 316 case "esc": 317 m.cmding = false 318 m.prompt.Blur() 319 m.prompt.SetValue("") 320 return m, nil 321 case "enter": 322 m.cmding = false 323 m.prompt.Blur() 324 v := m.prompt.Value() 325 m.prompt.SetValue("") 326 return m, m.evaluateCommand(v) 327 default: 328 p, cmd := m.prompt.Update(msg) 329 m.prompt = p 330 return m, cmd 331 } 332 } 333 case errMsg: 334 m.gsd.state = Error 335 m.error = &msg.err 336 return m, nil 337 case svMsg: 338 if m.cm != nil && m.cm.myid != nil && msg.signetView.LrcId == *m.cm.myid { 339 m.cm.signeturi = &msg.signetView.URI 340 return m, nil 341 } 342 case dialMsg: 343 m.gsd.state = DialingChannel 344 return m, m.dialingChannel(msg.value) 345 346 case loginMsg: 347 if len(msg.value) == 2 { 348 return m, login(msg.value[0], msg.value[1]) 349 } 350 case loggedInMsg: 351 m.gsd.xrpc = msg.xrpc 352 return m, nil 353 354 case setMsg: 355 key, val, found := strings.Cut(msg.value, "=") 356 if !found { 357 return m, nil 358 } 359 switch key { 360 case "color", "c": 361 var b uint32 362 363 if len(val) == 7 && val[0] == '#' { 364 b64, err := strconv.ParseUint(val[1:], 16, 0) 365 if err != nil { 366 return m, nil 367 } 368 b = uint32(b64) 369 } else { 370 i, err := strconv.Atoi(val) 371 if err != nil { 372 return m, nil 373 } 374 b = uint32(i) 375 } 376 m.gsd.color = &b 377 if m.cm != nil { 378 m.cm.draft.PromptStyle = lipgloss.NewStyle().Foreground(ColorFromInt(&b)) 379 } 380 m.cm.updateLRCIdentity() 381 return m, nil 382 case "nick", "name", "n": 383 m.gsd.nick = &val 384 if m.cm != nil { 385 m.cm.draft.Prompt = renderName(m.gsd.nick, m.gsd.handle) + " " 386 m.cm.draft.Width = m.gsd.width - len(m.cm.draft.Prompt) - 1 387 } 388 m.cm.updateLRCIdentity() 389 return m, nil 390 case "handle", "h", "at", "@": 391 m.gsd.handle = &val 392 if m.cm != nil { 393 m.cm.draft.Prompt = renderName(m.gsd.nick, m.gsd.handle) + " " 394 m.cm.draft.Width = m.gsd.width - len(m.cm.draft.Prompt) - 1 395 } 396 m.cm.updateLRCIdentity() 397 return m, nil 398 } 399 400 case tea.WindowSizeMsg: 401 m.gsd.height = msg.Height 402 m.gsd.width = msg.Width 403 m.prompt.Width = msg.Width - 2 404 if m.clm != nil { 405 m.clm.list.SetSize(msg.Width, msg.Height-1) 406 } 407 if m.cm != nil { 408 m.cm.vp.Width = msg.Width 409 m.cm.vp.Height = msg.Height - 2 410 m.cm.draft.Width = m.gsd.width - len(m.cm.draft.Prompt) - 1 411 if m.cm.render != nil { 412 for _, message := range m.cm.msgs { 413 message.renderMessage(msg.Width) 414 } 415 m.cm.vp.SetContent(JoinDeref(m.cm.render, "")) 416 } 417 } 418 return m, nil 419 } 420 421 switch m.gsd.state { 422 case Splash: 423 return m.updateSplash(msg) 424 case GettingChannels: 425 return m.updateGettingChannels(msg) 426 case ChannelList: 427 clm, cmd, err := m.clm.updateChannelList(msg) 428 if err != nil { 429 m.gsd.state = Error 430 m.error = &err 431 return m, nil 432 } 433 m.clm = &clm 434 return m, cmd 435 case ResolvingChannel: 436 return m.updateResolvingChannel(msg) 437 case ConnectingToChannel: 438 return m.updateConnectingToChannel(msg) 439 case DialingChannel: 440 return m.updateDialingChannel(msg) 441 442 case Connected: 443 cm, cmd, err := m.cm.updateConnected(msg) 444 if err != nil { 445 m.gsd.state = Error 446 m.error = &err 447 return m, nil 448 } 449 m.cm = &cm 450 return m, cmd 451 } 452 453 return m, nil 454} 455 456func (cm channelmodel) updateConnected(msg tea.Msg) (channelmodel, tea.Cmd, error) { 457 switch msg := msg.(type) { 458 case lrcEvent: 459 if msg.e == nil { 460 return cm, nil, errors.New("nil lrcEvent") 461 } 462 id := msg.e.Id 463 switch msg := msg.e.Msg.(type) { 464 case *lrcpb.Event_Ping: 465 return cm, nil, nil 466 case *lrcpb.Event_Pong: 467 return cm, nil, nil 468 case *lrcpb.Event_Init: 469 err := initMessage(msg.Init, cm.msgs, &cm.render, cm.gsd.width) 470 if err != nil { 471 return cm, nil, err 472 } 473 if msg.Init.Echoed != nil && *msg.Init.Echoed { 474 cm.myid = msg.Init.Id 475 } 476 ab := cm.vp.AtBottom() 477 cm.vp.SetContent(JoinDeref(cm.render, "")) 478 if ab { 479 cm.vp.GotoBottom() 480 } 481 return cm, nil, nil 482 case *lrcpb.Event_Pub: 483 err := pubMessage(msg.Pub, cm.msgs, cm.gsd.width) 484 if err != nil { 485 return cm, nil, err 486 } 487 cm.vp.SetContent(JoinDeref(cm.render, "")) 488 return cm, nil, err 489 case *lrcpb.Event_Insert: 490 err := insertMessage(msg.Insert, cm.msgs, &cm.render, cm.gsd.width) 491 if err != nil { 492 return cm, nil, err 493 } 494 ab := cm.vp.AtBottom() 495 cm.vp.SetContent(JoinDeref(cm.render, "")) 496 if ab { 497 cm.vp.GotoBottom() 498 } 499 return cm, nil, nil 500 case *lrcpb.Event_Delete: 501 err := deleteMessage(msg.Delete, cm.msgs, &cm.render, cm.gsd.width) 502 if err != nil { 503 return cm, nil, err 504 } 505 ab := cm.vp.AtBottom() 506 cm.vp.SetContent(JoinDeref(cm.render, "")) 507 if ab { 508 cm.vp.GotoBottom() 509 } 510 return cm, nil, nil 511 case *lrcpb.Event_Mute: 512 return cm, nil, nil 513 case *lrcpb.Event_Unmute: 514 return cm, nil, nil 515 case *lrcpb.Event_Set: 516 return cm, nil, nil 517 case *lrcpb.Event_Get: 518 if msg.Get.Topic != nil { 519 cm.topic = msg.Get.Topic 520 } 521 return cm, nil, nil 522 case *lrcpb.Event_Editbatch: 523 if id == nil { 524 return cm, nil, nil 525 } 526 err := editMessage(*id, msg.Editbatch.Edits, cm.msgs, &cm.render, cm.gsd.width) 527 if err != nil { 528 return cm, nil, err 529 } 530 ab := cm.vp.AtBottom() 531 cm.vp.SetContent(JoinDeref(cm.render, "")) 532 if ab { 533 cm.vp.GotoBottom() 534 } 535 return cm, nil, nil 536 } 537 case tea.KeyMsg: 538 switch cm.mode { 539 case Normal: 540 switch msg.String() { 541 case "i", "a": 542 cm.mode = Insert 543 return cm, cm.draft.Focus(), nil 544 case "I": 545 cm.mode = Insert 546 cm.draft.CursorStart() 547 return cm, cm.draft.Focus(), nil 548 case "A": 549 cm.mode = Insert 550 cm.draft.CursorEnd() 551 return cm, cm.draft.Focus(), nil 552 } 553 case Insert: 554 switch msg.String() { 555 case "esc": 556 cm.mode = Normal 557 cm.draft.Blur() 558 return cm, nil, nil 559 case "enter": 560 if cm.sentmsg != nil { 561 if cm.gsd.xrpc != nil && cm.signeturi != nil { 562 var color64 *uint64 563 if cm.gsd.color != nil { 564 c64 := uint64(*cm.gsd.color) 565 color64 = &c64 566 } 567 lmr := lex.MessageRecord{ 568 SignetURI: *cm.signeturi, 569 Body: *cm.sentmsg, 570 Nick: cm.gsd.nick, 571 Color: color64, 572 PostedAt: syntax.DatetimeNow().String(), 573 } 574 cm.draft.SetValue("") 575 cm.sentmsg = nil 576 cm.myid = nil 577 cm.signeturi = nil 578 return cm, tea.Batch(sendPub(cm.lrcconn), createMSGCmd(cm.gsd.xrpc, &lmr)), nil 579 } 580 cm.draft.SetValue("") 581 cm.sentmsg = nil 582 return cm, sendPub(cm.lrcconn), nil 583 } 584 return cm, nil, nil 585 } 586 } 587 } 588 switch cm.mode { 589 case Normal: 590 vp, cmd := cm.vp.Update(msg) 591 cm.vp = vp 592 return cm, cmd, nil 593 case Insert: 594 draft, cmd := cm.draft.Update(msg) 595 if cm.sentmsg == nil && draft.Value() != "" { 596 nv := draft.Value() 597 cm.sentmsg = &nv 598 cm.draft = draft 599 return cm, tea.Batch(cmd, sendInsert(cm.lrcconn, nv, 0, true)), nil 600 } 601 if cm.sentmsg != nil && *cm.sentmsg != draft.Value() { 602 draftutf16 := utf16.Encode([]rune(draft.Value())) 603 sentutf16 := utf16.Encode([]rune(*cm.sentmsg)) 604 edits := Diff(sentutf16, draftutf16) 605 cm.draft = draft 606 sentmsg := draft.Value() 607 cm.sentmsg = &sentmsg 608 return cm, tea.Batch(cmd, sendEditBatch(cm.datachan, edits)), nil 609 } 610 cm.draft = draft 611 return cm, cmd, nil 612 } 613 return cm, nil, nil 614} 615 616func createMSGCmd(xrpc *PasswordClient, lmr *lex.MessageRecord) tea.Cmd { 617 return func() tea.Msg { 618 _, _, err := xrpc.CreateXCVRMessage(lmr, context.Background()) 619 if err != nil { 620 return errMsg{err} 621 } 622 return nil 623 } 624} 625 626func sendEditBatch(datachan chan []byte, edits []Edit) tea.Cmd { 627 return func() tea.Msg { 628 idx := 0 629 batch := make([]*lrcpb.Edit, 0) 630 for _, edit := range edits { 631 switch edit.EditType { 632 case EditDel: 633 idx2 := idx + len(edit.Utf16Text) 634 evt := makeDelete(uint32(idx), uint32(idx2)) 635 edit := lrcpb.Edit{Edit: &lrcpb.Edit_Delete{Delete: evt.GetDelete()}} 636 batch = append(batch, &edit) 637 case EditKeep: 638 idx = idx + len(edit.Utf16Text) 639 case EditAdd: 640 evt := makeInsert(string(utf16.Decode(edit.Utf16Text)), uint32(idx)) 641 idx = idx + len(edit.Utf16Text) 642 edit := lrcpb.Edit{Edit: &lrcpb.Edit_Insert{Insert: evt.GetInsert()}} 643 batch = append(batch, &edit) 644 } 645 } 646 evt := lrcpb.Event{Msg: &lrcpb.Event_Editbatch{Editbatch: &lrcpb.EditBatch{Edits: batch}}} 647 data, err := proto.Marshal(&evt) 648 if err != nil { 649 return errMsg{err} 650 } 651 datachan <- data 652 return nil 653 } 654} 655 656func sendPub(conn *websocket.Conn) tea.Cmd { 657 return func() tea.Msg { 658 evt := &lrcpb.Event{Msg: &lrcpb.Event_Pub{Pub: &lrcpb.Pub{}}} 659 data, err := proto.Marshal(evt) 660 if err != nil { 661 return errMsg{err} 662 } 663 err = conn.WriteMessage(websocket.BinaryMessage, data) 664 if err != nil { 665 return errMsg{err} 666 } 667 return nil 668 } 669} 670 671func makeDelete(start uint32, end uint32) *lrcpb.Event { 672 evt := &lrcpb.Event{Msg: &lrcpb.Event_Delete{Delete: &lrcpb.Delete{Utf16Start: start, Utf16End: end}}} 673 return evt 674} 675 676func makeInsert(body string, idx uint32) *lrcpb.Event { 677 evt := &lrcpb.Event{Msg: &lrcpb.Event_Insert{Insert: &lrcpb.Insert{Body: body, Utf16Index: idx}}} 678 return evt 679} 680 681func sendInsert(conn *websocket.Conn, body string, utf16idx uint32, init bool) tea.Cmd { 682 return func() tea.Msg { 683 if init { 684 evt := &lrcpb.Event{Msg: &lrcpb.Event_Init{Init: &lrcpb.Init{}}} 685 data, err := proto.Marshal(evt) 686 if err != nil { 687 return errMsg{err} 688 } 689 if conn == nil { 690 return nil 691 } 692 err = conn.WriteMessage(websocket.BinaryMessage, data) 693 if err != nil { 694 return errMsg{err} 695 } 696 } 697 evt := &lrcpb.Event{Msg: &lrcpb.Event_Insert{Insert: &lrcpb.Insert{Body: body, Utf16Index: utf16idx}}} 698 data, err := proto.Marshal(evt) 699 if err != nil { 700 return errMsg{err} 701 } 702 err = conn.WriteMessage(websocket.BinaryMessage, data) 703 if err != nil { 704 return errMsg{err} 705 } 706 return nil 707 } 708} 709 710func (m model) evaluateCommand(command string) tea.Cmd { 711 return func() tea.Msg { 712 parts := strings.Split(command, " ") 713 if parts == nil { 714 return nil 715 } 716 switch parts[0] { 717 case "q": 718 return tea.QuitMsg{} 719 case "se", "set": 720 if len(parts) != 1 { 721 return setMsg{parts[1]} 722 } 723 case "resize": 724 return tea.WindowSize() 725 case "login": 726 if len(parts) != 1 { 727 return loginMsg{parts[1:]} 728 } 729 case "dial": 730 if len(parts) != 1 { 731 return dialMsg{parts[1]} 732 } 733 } 734 return nil 735 } 736} 737 738type dialMsg struct { 739 value string 740} 741 742type loginMsg struct { 743 value []string 744} 745 746type setMsg struct { 747 value string 748} 749 750// i think that the type of renders is a bit awkward, but i want deletemessage + friends to just modify the rendered 751// messages slice in place in the event that we create a new msg. i think ideally the way to go is to make a more 752// encapsulated data structure for the map + renders which still allows edits to the messages without requiring 753// rerendering every message 754func deleteMessage(msg *lrcpb.Delete, msgmap map[uint32]*Message, renders *[]*string, width int) error { 755 if msg == nil { 756 return errors.New("no insert") 757 } 758 id := msg.Id 759 if id == nil { 760 return errors.New("no insert id") 761 } 762 m := msgmap[*id] 763 atr := false 764 if m == nil { 765 atr = true 766 renderedDefault := "" 767 m = &Message{ 768 nick: nil, 769 handle: nil, 770 color: nil, 771 active: true, 772 text: "", 773 rendered: &renderedDefault, 774 } 775 msgmap[*id] = m 776 } 777 start := msg.Utf16Start 778 end := msg.Utf16End 779 m.text = deleteBtwnUTF16Indices(m.text, start, end) 780 m.renderMessage(width) 781 if atr { 782 *renders = append(*renders, m.rendered) 783 } 784 return nil 785} 786func deleteBtwnUTF16Indices(base string, start uint32, end uint32) string { 787 if end <= start { 788 return base 789 } 790 runes := []rune(base) 791 baseUTF16Units := utf16.Encode(runes) 792 if uint32(len(baseUTF16Units)) < end { 793 end = uint32(len(baseUTF16Units)) 794 } 795 if uint32(len(baseUTF16Units)) < start { 796 return base 797 } 798 result := make([]uint16, 0, uint32(len(baseUTF16Units))+start-end) 799 result = append(result, baseUTF16Units[:start]...) 800 result = append(result, baseUTF16Units[end:]...) 801 resultRunes := utf16.Decode(result) 802 return string(resultRunes) 803} 804 805func editMessage(id uint32, edits []*lrcpb.Edit, msgmap map[uint32]*Message, renders *[]*string, width int) error { 806 for _, edit := range edits { 807 switch e := edit.Edit.(type) { 808 case *lrcpb.Edit_Insert: 809 ins := e.Insert 810 ins.Id = &id 811 err := insertMessage(ins, msgmap, renders, width) 812 if err != nil { 813 return err 814 } 815 case *lrcpb.Edit_Delete: 816 del := e.Delete 817 del.Id = &id 818 err := deleteMessage(del, msgmap, renders, width) 819 if err != nil { 820 return err 821 } 822 } 823 } 824 return nil 825} 826 827func insertMessage(msg *lrcpb.Insert, msgmap map[uint32]*Message, renders *[]*string, width int) error { 828 if msg == nil { 829 return errors.New("no insert") 830 } 831 id := msg.Id 832 if id == nil { 833 return errors.New("no insert id") 834 } 835 m := msgmap[*id] 836 atr := false 837 if m == nil { 838 atr = true 839 renderedDefault := "" 840 m = &Message{ 841 nick: nil, 842 handle: nil, 843 color: nil, 844 active: true, 845 text: "", 846 rendered: &renderedDefault, 847 } 848 msgmap[*id] = m 849 } 850 idx := msg.Utf16Index 851 body := msg.Body 852 m.text = insertAtUTF16Index(m.text, idx, body) 853 854 m.renderMessage(width) 855 if atr { 856 *renders = append(*renders, m.rendered) 857 } 858 return nil 859} 860 861func insertAtUTF16Index(base string, index uint32, insert string) string { 862 runes := []rune(base) 863 baseUTF16Units := utf16.Encode(runes) 864 if uint32(len(baseUTF16Units)) < index { 865 spacesNeeded := index - uint32(len(baseUTF16Units)) 866 padding := strings.Repeat(" ", int(spacesNeeded)) 867 base = base + padding 868 869 runes = []rune(base) 870 baseUTF16Units = utf16.Encode(runes) 871 } 872 873 insertRunes := []rune(insert) 874 insertUTF16Units := utf16.Encode(insertRunes) 875 result := make([]uint16, 0, len(baseUTF16Units)+len(insertUTF16Units)) 876 result = append(result, baseUTF16Units[:index]...) 877 result = append(result, insertUTF16Units...) 878 result = append(result, baseUTF16Units[index:]...) 879 resultRunes := utf16.Decode(result) 880 return string(resultRunes) 881} 882 883func pubMessage(msg *lrcpb.Pub, msgmap map[uint32]*Message, width int) error { 884 if msg == nil { 885 return errors.New("no pub") 886 } 887 id := msg.Id 888 if id == nil { 889 return errors.New("no pub id") 890 } 891 m := msgmap[*id] 892 if m != nil { 893 m.active = false 894 m.renderMessage(width) 895 } 896 return nil 897} 898 899func initMessage(msg *lrcpb.Init, msgmap map[uint32]*Message, renders *[]*string, width int) error { 900 if msg == nil { 901 return errors.New("beeped tf up") 902 } 903 id := msg.Id 904 if id == nil { 905 return errors.New("beeped up") 906 } 907 renderedDefault := "" 908 m := &Message{ 909 nick: msg.Nick, 910 handle: msg.ExternalID, 911 color: msg.Color, 912 active: true, 913 text: "", 914 rendered: &renderedDefault, 915 } 916 m.renderMessage(width) 917 msgmap[*id] = m 918 *renders = append(*renders, m.rendered) 919 return nil 920} 921 922func (m *Message) renderMessage(width int) { 923 if m == nil { 924 return 925 } 926 stylem := lipgloss.NewStyle().Width(width).Align(lipgloss.Left) 927 styleh := stylem.Foreground(ColorFromInt(m.color)) 928 if m.active { 929 styleh = styleh.Reverse(true) 930 stylem = styleh 931 } 932 header := styleh.Render(renderName(m.nick, m.handle)) 933 body := stylem.Render(m.text) 934 *m.rendered = fmt.Sprintf("%s\n%s\n", header, body) 935} 936 937func (m model) updateConnectingToChannel(msg tea.Msg) (tea.Model, tea.Cmd) { 938 switch msg := msg.(type) { 939 case connMsg: 940 m.gsd.state = Connected 941 cm := channelmodel{} 942 cm.wsurl = msg.wsurl 943 cm.gsd = m.gsd 944 cm.cancel = msg.cancel 945 cm.msgs = make(map[uint32]*Message) 946 vp := viewport.New(m.gsd.width, m.gsd.height-2) 947 cm.vp = vp 948 draft := textinput.New() 949 draft.Prompt = renderName(m.gsd.nick, m.gsd.handle) + " " 950 draft.PromptStyle = lipgloss.NewStyle().Foreground(ColorFromInt(m.gsd.color)) 951 draft.Placeholder = "press i to start typing" 952 draft.Width = m.gsd.width - len(draft.Prompt) - 1 953 cm.draft = draft 954 go startLRCHandlers(msg.conn, m.gsd.nick, m.gsd.handle, m.gsd.color) 955 cm.lrcconn = msg.conn 956 cm.lexconn = msg.lexconn 957 cm.datachan = make(chan []byte) 958 go listenToLexConn(msg.lexconn) 959 go LRCWriter(cm.lrcconn, cm.datachan) 960 m.cm = &cm 961 m.clm = nil 962 return m, nil 963 } 964 return m, nil 965} 966 967func (m model) updateDialingChannel(msg tea.Msg) (tea.Model, tea.Cmd) { 968 switch msg := msg.(type) { 969 case connSimpleMsg: 970 m.gsd.state = Connected 971 cm := channelmodel{} 972 cm.gsd = m.gsd 973 cm.cancel = msg.cancel 974 cm.msgs = make(map[uint32]*Message) 975 vp := viewport.New(m.gsd.width, m.gsd.height-2) 976 cm.vp = vp 977 draft := textinput.New() 978 draft.Prompt = renderName(m.gsd.nick, m.gsd.handle) + " " 979 draft.PromptStyle = lipgloss.NewStyle().Foreground(ColorFromInt(m.gsd.color)) 980 draft.Placeholder = "press i to start typing" 981 draft.Width = m.gsd.width - len(draft.Prompt) - 1 982 cm.draft = draft 983 go startLRCHandlers(msg.conn, m.gsd.nick, m.gsd.handle, m.gsd.color) 984 cm.lrcconn = msg.conn 985 cm.datachan = make(chan []byte) 986 cm.wsurl = msg.wsurl 987 m.cm = &cm 988 m.clm = nil 989 go LRCWriter(cm.lrcconn, cm.datachan) 990 } 991 return m, nil 992} 993 994func LRCWriter(conn *websocket.Conn, datachan chan []byte) { 995 for data := range datachan { 996 err := conn.WriteMessage(websocket.BinaryMessage, data) 997 if err != nil { 998 send(errMsg{err}) 999 return 1000 } 1001 } 1002} 1003 1004func renderName(nick *string, handle *string) string { 1005 var n string 1006 if nick != nil { 1007 n = *nick 1008 } 1009 var h string 1010 if handle != nil { 1011 h = fmt.Sprintf("@%s", *handle) 1012 } 1013 return fmt.Sprintf("%s%s", n, h) 1014} 1015 1016func sendSet(datachan chan []byte, nick *string, handle *string, color *uint32) error { 1017 evt := &lrcpb.Event{Msg: &lrcpb.Event_Set{Set: &lrcpb.Set{Nick: nick, ExternalID: handle, Color: color}}} 1018 data, err := proto.Marshal(evt) 1019 if err != nil { 1020 return err 1021 } 1022 datachan <- data 1023 return nil 1024 1025} 1026 1027func startLRCHandlers(conn *websocket.Conn, nick *string, handle *string, color *uint32) { 1028 if conn == nil { 1029 send(errMsg{errors.New("provided nil conn")}) 1030 return 1031 } 1032 evt := &lrcpb.Event{Msg: &lrcpb.Event_Set{Set: &lrcpb.Set{Nick: nick, ExternalID: handle, Color: color}}} 1033 data, err := proto.Marshal(evt) 1034 if err != nil { 1035 send(errMsg{errors.New("failed to marshal: " + err.Error())}) 1036 return 1037 } 1038 conn.WriteMessage(websocket.BinaryMessage, data) 1039 1040 bep := "bep" 1041 evt = &lrcpb.Event{Msg: &lrcpb.Event_Get{Get: &lrcpb.Get{Topic: &bep}}} 1042 data, err = proto.Marshal(evt) 1043 if err != nil { 1044 send(errMsg{errors.New("failed to marshal: " + err.Error())}) 1045 return 1046 } 1047 conn.WriteMessage(websocket.BinaryMessage, data) 1048 go listenToConn(conn) 1049} 1050 1051type typedJSON struct { 1052 Type string `json:"$type"` 1053} 1054 1055func listenToLexConn(conn *websocket.Conn) { 1056 for { 1057 var rawMsg json.RawMessage 1058 err := conn.ReadJSON(&rawMsg) 1059 if err != nil { 1060 send(errMsg{err}) 1061 return 1062 } 1063 var typed typedJSON 1064 err = json.Unmarshal(rawMsg, &typed) 1065 if err != nil { 1066 send(errMsg{err}) 1067 return 1068 } 1069 switch typed.Type { 1070 case "org.xcvr.lrc.defs#signetView": 1071 var sv SignetView 1072 err = json.Unmarshal(rawMsg, &sv) 1073 if err != nil { 1074 send(errMsg{err}) 1075 return 1076 } 1077 send(svMsg{&sv}) 1078 } 1079 } 1080} 1081 1082type svMsg struct { 1083 signetView *SignetView 1084} 1085 1086type SignetView struct { 1087 Type string `json:"$type,const=org.xcvr.lrc.defs#signetView"` 1088 URI string `json:"uri"` 1089 IssuerHandle string `json:"issuerHandle"` 1090 ChannelURI string `json:"channelURI"` 1091 LrcId uint32 `json:"lrcID"` 1092 AuthorHandle string `json:"authorHandle"` 1093 StartedAt time.Time `json:"startedAt"` 1094} 1095 1096func listenToConn(conn *websocket.Conn) { 1097 for { 1098 _, data, err := conn.ReadMessage() 1099 if err != nil { 1100 send(errMsg{err}) 1101 } 1102 var e lrcpb.Event 1103 err = proto.Unmarshal(data, &e) 1104 send(lrcEvent{&e}) 1105 } 1106} 1107 1108type lrcEvent struct{ e *lrcpb.Event } 1109type connlistenerexitMsg struct{} 1110type connwriterexitMsg struct{} 1111 1112func (m model) updateResolvingChannel(msg tea.Msg) (tea.Model, tea.Cmd) { 1113 switch msg := msg.(type) { 1114 case resolutionMsg: 1115 wsurl := fmt.Sprintf("%s%s", strings.TrimPrefix(msg.channelhost, "https://"), msg.resolution.URL) 1116 m.gsd.state = ConnectingToChannel 1117 ctx, cancel := context.WithCancel(context.Background()) 1118 return m, m.connectToChannel(ctx, cancel, wsurl) 1119 } 1120 return m, nil 1121} 1122 1123func (m model) dialingChannel(url string) tea.Cmd { 1124 return func() tea.Msg { 1125 dialer := websocket.DefaultDialer 1126 dialer.Subprotocols = []string{"lrc.v1"} 1127 ctx, cancel := context.WithCancel(context.Background()) 1128 conn, _, err := dialer.DialContext(ctx, fmt.Sprintf("wss://%s", url), http.Header{}) 1129 if err != nil { 1130 cancel() 1131 return errMsg{err} 1132 } 1133 return connSimpleMsg{conn, cancel, url} 1134 } 1135} 1136 1137type connSimpleMsg struct { 1138 conn *websocket.Conn 1139 cancel func() 1140 wsurl string 1141} 1142 1143func (m model) connectToChannel(ctx context.Context, cancel func(), wsurl string) tea.Cmd { 1144 return func() tea.Msg { 1145 dialer := websocket.DefaultDialer 1146 dialer.Subprotocols = []string{"lrc.v1"} 1147 conn, _, err := dialer.DialContext(ctx, fmt.Sprintf("wss://%s", wsurl), http.Header{}) 1148 if err != nil { 1149 return errMsg{err} 1150 } 1151 1152 dialer = websocket.DefaultDialer 1153 c := m.clm.curchannel() 1154 var uri string 1155 if c != nil { 1156 uri = c.URI 1157 } 1158 lexconn, _, err := dialer.DialContext(ctx, fmt.Sprintf("wss://xcvr.org/xrpc/org.xcvr.lrc.subscribeLexStream?uri=%s", uri), http.Header{}) 1159 if err != nil { 1160 return errMsg{err} 1161 } 1162 return connMsg{conn, lexconn, cancel, wsurl} 1163 } 1164} 1165 1166type connMsg struct { 1167 conn *websocket.Conn 1168 lexconn *websocket.Conn 1169 cancel func() 1170 wsurl string 1171} 1172 1173const ( 1174 bullet = "•" 1175 ellipsis = "…" 1176) 1177 1178func (m model) updateGettingChannels(msg tea.Msg) (tea.Model, tea.Cmd) { 1179 switch msg := msg.(type) { 1180 case channelsMsg: 1181 clm := channellistmodel{} 1182 items := make([]list.Item, 0, len(msg.channels)) 1183 for _, channel := range msg.channels { 1184 items = append(items, ChannelItem{channel}) 1185 } 1186 list := list.New(items, ChannelItemDelegate{}, m.gsd.width, m.gsd.height-1) 1187 list.Styles = defaultStyles() 1188 list.Title = "org.xcvr.feed.getChannels" 1189 clm.list = list 1190 m.gsd.state = ChannelList 1191 clm.gsd = m.gsd 1192 m.clm = &clm 1193 return m, nil 1194 } 1195 return m, nil 1196} 1197 1198func (clm channellistmodel) curchannel() *Channel { 1199 switch i := clm.list.SelectedItem().(type) { 1200 case ChannelItem: 1201 return &i.channel 1202 } 1203 return nil 1204} 1205 1206func (clm channellistmodel) updateChannelList(msg tea.Msg) (channellistmodel, tea.Cmd, error) { 1207 switch msg := msg.(type) { 1208 case tea.KeyMsg: 1209 switch msg.String() { 1210 case "enter": 1211 if clm.list.FilterState() == list.Filtering { 1212 break 1213 } 1214 clm.gsd.state = ResolvingChannel 1215 cc := clm.curchannel() 1216 if cc != nil { 1217 uri := cc.URI 1218 did, _ := DidFromUri(uri) 1219 rkey, err := RkeyFromUri(uri) 1220 if err != nil { 1221 return clm, nil, err 1222 } 1223 return clm, ResolveChannel(cc.Host, did, rkey), nil 1224 } else { 1225 err := errors.New("bad list type") 1226 return clm, nil, err 1227 } 1228 } 1229 } 1230 list, cmd := clm.list.Update(msg) 1231 clm.list = list 1232 return clm, cmd, nil 1233} 1234 1235func ResolveChannel(host string, did string, rkey string) tea.Cmd { 1236 return func() tea.Msg { 1237 c := &http.Client{Timeout: 10 * time.Second} 1238 var res *http.Response 1239 var err error 1240 if strings.HasPrefix(host, "did:web:") { 1241 res, err = c.Get(fmt.Sprintf("https://%s/.well-known/did.json", strings.TrimPrefix(host, "did:web:"))) 1242 } else if !strings.HasPrefix(host, "did:plc") { 1243 res, err = c.Get(fmt.Sprintf("https://plc.directory/%s", host)) 1244 } else { 1245 err = errors.New("unsupported method") 1246 } 1247 if err != nil { 1248 return errMsg{err} 1249 } 1250 dec := json.NewDecoder(res.Body) 1251 var diddoc struct { 1252 Services []struct { 1253 Id string `json:"id"` 1254 ServiceEndpoint string `json:"serviceEndpoint"` 1255 } `json:"service"` 1256 } 1257 dec.Decode(&diddoc) 1258 if diddoc.Services == nil { 1259 return errMsg{errors.New("no services")} 1260 1261 } 1262 var url string 1263 for _, s := range diddoc.Services { 1264 if s.Id != "#xcvr_ch" { 1265 continue 1266 } 1267 url = s.ServiceEndpoint 1268 } 1269 if url == "" { 1270 return errMsg{errors.New("no serviceEndpoint")} 1271 } 1272 res, err = c.Get(fmt.Sprintf("%s/xrpc/org.xcvr.actor.resolveChannel?did=%s&rkey=%s", url, did, rkey)) 1273 1274 if res.StatusCode != 200 { 1275 return errMsg{errors.New(fmt.Sprintf("error resolving channel: %d", res.StatusCode))} 1276 } 1277 decoder := json.NewDecoder(res.Body) 1278 var resolution Resolution 1279 err = decoder.Decode(&resolution) 1280 if err != nil { 1281 return errMsg{err} 1282 } 1283 return resolutionMsg{resolution, url} 1284 } 1285} 1286 1287type resolutionMsg struct { 1288 resolution Resolution 1289 channelhost string 1290} 1291 1292type Resolution struct { 1293 URL string `json:"url"` 1294 URI *string `json:"uri,omitempty"` 1295} 1296 1297func (m model) View() string { 1298 var pv string 1299 if m.cmding { 1300 pv = m.prompt.View() 1301 } 1302 switch m.gsd.state { 1303 case Splash: 1304 return m.splashView() 1305 case GettingChannels: 1306 return "loading..." 1307 case Error: 1308 if m.error != nil { 1309 return (*m.error).Error() 1310 } 1311 return "broke so bad there isn't an error" 1312 case ChannelList: 1313 return m.clm.channelListView(m.cmding, pv) 1314 case ResolvingChannel: 1315 return "resolving channel" 1316 case DialingChannel: 1317 return "dialing channel" 1318 case ConnectingToChannel: 1319 return m.connectingView() 1320 case Connected: 1321 return m.cm.connectedView(m.cmding, pv) 1322 default: 1323 return "under construction" 1324 } 1325} 1326 1327func (cm channelmodel) connectedView(cmding bool, prompt string) string { 1328 vpt := cm.vp.View() 1329 var footer string 1330 if cmding { 1331 footer = prompt 1332 } else { 1333 address := "lrc://" 1334 address = fmt.Sprintf("%s%s", address, cm.wsurl) 1335 var topic string 1336 if cm.topic != nil { 1337 topic = *cm.topic 1338 } 1339 remainingspace := cm.gsd.width - len(address) - len(topic) 1340 var footertext string 1341 if remainingspace < 1 { 1342 addressremaining := cm.gsd.width - len(address) 1343 if addressremaining < 0 { 1344 footertext = strings.Repeat(" ", cm.gsd.width) 1345 } else { 1346 footertext = fmt.Sprintf("%s%s", address, strings.Repeat(" ", cm.gsd.width-len(address))) 1347 } 1348 } else { 1349 footertext = fmt.Sprintf("%s%s%s", address, strings.Repeat(" ", remainingspace), topic) 1350 } 1351 insert := cm.mode == Insert 1352 footerstyle := lipgloss.NewStyle().Reverse(insert) 1353 footerstyle = footerstyle.Foreground(ColorFromInt(cm.gsd.color)) 1354 footer = footerstyle.Render(footertext) 1355 } 1356 draftText := cm.draft.View() 1357 return fmt.Sprintf("%s\n%s\n%s", vpt, draftText, footer) 1358} 1359 1360func (m model) connectingView() string { 1361 return "resolving channel\nconnecting to channel" 1362} 1363 1364func (clm channellistmodel) channelListView(cmding bool, prompt string) string { 1365 lv := clm.list.View() 1366 cv := "" 1367 if cmding { 1368 cv = prompt 1369 } 1370 return fmt.Sprintf("%s\n%s", lv, cv) 1371} 1372 1373func (m model) splashView() string { 1374 style := lipgloss.NewStyle().Foreground(Green) 1375 part00 := "\n ⣰⡀ ⢀⣀ ⡇ ⡇⡠ ⣰⡀ ⢀⡀ ⡀⢀ ⢀⡀ ⡀⢀ ⡇" 1376 part01 := "\n ⠘⠤ ⠣⠼ ⠣ ⠏⠢ ⠘⠤ ⠣⠜ ⣑⡺ ⠣⠜ ⠣⠼ ⠅" 1377 part02 := "\n ⣰⡀ ⡀⣀ ⢀⣀ ⣀⡀ ⢀⣀ ⢀⣀ ⢀⡀ ⠄ ⡀⢀ ⢀⡀ ⡀⣀" 1378 part03 := "\n ⠘⠤ ⠏ ⠣⠼ ⠇⠸ ⠭⠕ ⠣⠤ ⠣⠭ ⠇ ⠱⠃ ⠣⠭ ⠏" 1379 part1 := "\n\n %%%%%%% " 1380 text1 := "tty!xcvr\n" 1381 part2 := ` %%%%%%%%%% %%% % % %%%%%%%% 1382 %%%%%%%%%%%%%%%%% %%%% %%%%%%%%%%%%%%%%%% 1383 %%%%%%%%%%%%%%%%%%%%%%% %% %%%%%%%%%%%%%%%%%%%%%%%%%% 1384 %%%%%%%%%%%%%%%%%%%%% %% %%%%%%%%%%%%%%%%%%%%%%% 1385 %%%%%%%%%%%%%%%% %% %%%%%%%%%%%%%%%%%%%%% 1386 %%%%%%%%%%%%%%%%%%% %% %%%%%%%%%%%%%% 1387 %%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%% 1388 %%%%%%%%%%%% %%%%%%%%%%%%%%%%` 1389 part25 := "\n %%%%% " 1390 text2 := `made with` 1391 part3 := " %%%%%%%%%\n" 1392 text3 := " love by moth11." 1393 1394 part4 := " %\n\n" 1395 1396 text4 := ` talk to you! 1397 transceiver 1398 press a key 1399 to start! 1400 ` 1401 s := fmt.Sprintf("\n\n\n\n%s%s%s%s%s%s%s%s%s%s%s%s%s", style.Render(part00), style.Render(part01), style.Render(part02), style.Render(part03), style.Render(part1), text1, style.Render(part2), style.Render(part25), text2, style.Render(part3), text3, style.Render(part4), text4) 1402 offset := lipgloss.NewStyle().MarginLeft((m.gsd.width - 58) / 2) 1403 return offset.Render(s) 1404} 1405 1406var send func(msg tea.Msg) 1407 1408func main() { 1409 fmt.Println("if you can see me before program quits i think that you should find a better terminal,") 1410 p := tea.NewProgram(initialModel(), tea.WithAltScreen()) 1411 send = p.Send 1412 if _, err := p.Run(); err != nil { 1413 fmt.Printf("Alas, there's been an error: %v", err) 1414 os.Exit(1) 1415 } 1416 fmt.Println("kthxbai!") 1417} 1418 1419func DidFromUri(uri string) (did string, err error) { 1420 s, err := trimScheme(uri) 1421 if err != nil { 1422 return 1423 } 1424 ss, err := uriFragSplit(s) 1425 if err != nil { 1426 return 1427 } 1428 did = ss[0] 1429 return 1430} 1431 1432func trimScheme(uri string) (string, error) { 1433 s, ok := strings.CutPrefix(uri, "at://") 1434 if !ok { 1435 return "", errors.New("not a uri, missing at:// scheme") 1436 } 1437 return s, nil 1438} 1439 1440func uriFragSplit(urifrag string) ([]string, error) { 1441 ss := strings.Split(urifrag, "/") 1442 if len(ss) != 3 { 1443 return nil, errors.New("not a urifrag, incorrect number of bits") 1444 } 1445 return ss, nil 1446} 1447 1448func RkeyFromUri(uri string) (rkey string, err error) { 1449 s, err := trimScheme(uri) 1450 if err != nil { 1451 return 1452 } 1453 ss, err := uriFragSplit(s) 1454 if err != nil { 1455 return 1456 } 1457 rkey = ss[2] 1458 return 1459} 1460 1461func defaultStyles() (s list.Styles) { 1462 s.TitleBar = lipgloss.NewStyle().Padding(0, 0, 0, 0) //nolint:mnd 1463 1464 s.Title = lipgloss.NewStyle(). 1465 Foreground(subduedColor). 1466 Padding(0, 0) 1467 1468 s.Spinner = lipgloss.NewStyle(). 1469 Foreground(lipgloss.AdaptiveColor{Light: "#8E8E8E", Dark: "#747373"}) 1470 1471 s.FilterPrompt = lipgloss.NewStyle(). 1472 Foreground(lipgloss.AdaptiveColor{Light: "#04B575", Dark: "#ECFD65"}) 1473 1474 s.FilterCursor = lipgloss.NewStyle(). 1475 Foreground(lipgloss.AdaptiveColor{Light: "#EE6FF8", Dark: "#EE6FF8"}) 1476 1477 s.DefaultFilterCharacterMatch = lipgloss.NewStyle().Underline(true) 1478 1479 s.StatusBar = lipgloss.NewStyle(). 1480 Foreground(lipgloss.AdaptiveColor{Light: "#A49FA5", Dark: "#777777"}). 1481 Padding(0, 0, 0, 0) //nolint:mnd 1482 1483 s.StatusEmpty = lipgloss.NewStyle().Foreground(subduedColor) 1484 1485 s.StatusBarActiveFilter = lipgloss.NewStyle(). 1486 Foreground(lipgloss.AdaptiveColor{Light: "#1a1a1a", Dark: "#dddddd"}) 1487 1488 s.StatusBarFilterCount = lipgloss.NewStyle().Foreground(verySubduedColor) 1489 1490 s.NoItems = lipgloss.NewStyle(). 1491 Foreground(lipgloss.AdaptiveColor{Light: "#909090", Dark: "#626262"}) 1492 1493 s.ArabicPagination = lipgloss.NewStyle().Foreground(subduedColor) 1494 1495 s.PaginationStyle = lipgloss.NewStyle().PaddingLeft(2) //nolint:mnd 1496 1497 s.HelpStyle = lipgloss.NewStyle().Padding(1, 0, 0, 2) //nolint:mnd 1498 1499 s.ActivePaginationDot = lipgloss.NewStyle(). 1500 Foreground(lipgloss.AdaptiveColor{Light: "#847A85", Dark: "#979797"}). 1501 SetString(bullet) 1502 1503 s.InactivePaginationDot = lipgloss.NewStyle(). 1504 Foreground(verySubduedColor). 1505 SetString(bullet) 1506 1507 s.DividerDot = lipgloss.NewStyle(). 1508 Foreground(verySubduedColor). 1509 SetString(" " + bullet + " ") 1510 1511 return s 1512} 1513 1514const maxInt = int(^uint(0) >> 1) 1515 1516func JoinDeref(elems []*string, sep string) string { 1517 switch len(elems) { 1518 case 0: 1519 return "" 1520 case 1: 1521 return *elems[0] 1522 } 1523 1524 var n int 1525 if len(sep) > 0 { 1526 if len(sep) >= maxInt/(len(elems)-1) { 1527 panic("strings: Join output length overflow") 1528 } 1529 n += len(sep) * (len(elems) - 1) 1530 } 1531 for _, elem := range elems { 1532 if len(*elem) > maxInt-n { 1533 panic("strings: Join output length overflow") 1534 } 1535 n += len(*elem) 1536 } 1537 1538 var b strings.Builder 1539 b.Grow(n) 1540 b.WriteString(*elems[0]) 1541 for _, s := range elems[1:] { 1542 b.WriteString(sep) 1543 b.WriteString(*s) 1544 } 1545 return b.String() 1546} 1547 1548type PasswordClient struct { 1549 xrpc *client.APIClient 1550 accessjwt *string 1551 refreshjwt *string 1552 did *string 1553} 1554 1555func NewPasswordClient(did string, host string) *PasswordClient { 1556 return &PasswordClient{ 1557 xrpc: client.NewAPIClient(host), 1558 did: &did, 1559 } 1560} 1561 1562func (c *PasswordClient) CreateSession(ctx context.Context, identity string, secret string) error { 1563 input := atproto.ServerCreateSession_Input{ 1564 Identifier: identity, 1565 Password: secret, 1566 } 1567 var out atproto.ServerCreateSession_Output 1568 err := c.xrpc.LexDo(ctx, "POST", "application/json", "com.atproto.server.createSession", nil, input, &out) 1569 if err != nil { 1570 return errors.New("I couldn't create a session: " + err.Error()) 1571 } 1572 c.accessjwt = &out.AccessJwt 1573 c.refreshjwt = &out.RefreshJwt 1574 return nil 1575} 1576 1577func (c *PasswordClient) RefreshSession(ctx context.Context) error { 1578 c.xrpc.Headers.Set("Authorization", fmt.Sprintf("Bearer %s", *c.refreshjwt)) 1579 var out atproto.ServerRefreshSession_Output 1580 err := c.xrpc.LexDo(ctx, "POST", "application/json", "com.atproto.server.refreshSession", nil, nil, &out) 1581 if err != nil { 1582 return errors.New("failed to refresh session! " + err.Error()) 1583 } 1584 c.accessjwt = &out.AccessJwt 1585 c.refreshjwt = &out.RefreshJwt 1586 return nil 1587} 1588 1589func (c *PasswordClient) CreateXCVRMessage(message *lex.MessageRecord, ctx context.Context) (cid string, uri string, err error) { 1590 input := atproto.RepoCreateRecord_Input{ 1591 Collection: "org.xcvr.lrc.message", 1592 Repo: *c.did, 1593 Record: &util.LexiconTypeDecoder{Val: message}, 1594 } 1595 return c.createMyRecord(input, ctx) 1596} 1597 1598func (c *PasswordClient) createMyRecord(input atproto.RepoCreateRecord_Input, ctx context.Context) (cid string, uri string, err error) { 1599 if c.accessjwt == nil { 1600 err = errors.New("must create a session first") 1601 return 1602 } 1603 c.xrpc.Headers.Set("Authorization", fmt.Sprintf("Bearer %s", *c.accessjwt)) 1604 var out atproto.RepoCreateRecord_Output 1605 err = c.xrpc.LexDo(ctx, "POST", "application/json", "com.atproto.repo.createRecord", nil, input, &out) 1606 if err != nil { 1607 err1 := err.Error() 1608 err = c.RefreshSession(ctx) 1609 if err != nil { 1610 err = errors.New(fmt.Sprintf("failed to refresh session while creating %s! first %s then %s", input.Collection, err1, err.Error())) 1611 return 1612 } 1613 c.xrpc.Headers.Set("Authorization", fmt.Sprintf("Bearer %s", *c.accessjwt)) 1614 out = atproto.RepoCreateRecord_Output{} 1615 err = c.xrpc.LexDo(ctx, "POST", "application/json", "com.atproto.repo.createRecord", nil, input, &out) 1616 if err != nil { 1617 err = errors.New(fmt.Sprintf("not good, failed to create %s after failing then refreshing session! first %s then %s", input.Collection, err1, err.Error())) 1618 return 1619 } 1620 cid = out.Cid 1621 uri = out.Uri 1622 return 1623 } 1624 cid = out.Cid 1625 uri = out.Uri 1626 return 1627} 1628 1629func ColorFromInt(c *uint32) lipgloss.Color { 1630 if c == nil { 1631 return Green 1632 } 1633 ui := *c 1634 guess := fmt.Sprintf("#%06x", ui) 1635 return lipgloss.Color(guess[0:7]) 1636}