diff --git a/go.mod b/go.mod index f54f2731..5f2d895d 100644 --- a/go.mod +++ b/go.mod @@ -47,6 +47,7 @@ require ( github.com/containerd/console v1.0.4 // indirect github.com/deckarep/golang-set/v2 v2.6.0 // indirect github.com/denisbrodbeck/machineid v1.0.1 // indirect + github.com/enescakir/emoji v1.0.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/go-jose/go-jose/v3 v3.0.1 // indirect github.com/go-stack/stack v1.8.1 // indirect diff --git a/go.sum b/go.sum index acca2b39..706bf1d5 100644 --- a/go.sum +++ b/go.sum @@ -24,6 +24,8 @@ github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80N github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ= github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI= +github.com/enescakir/emoji v1.0.0 h1:W+HsNql8swfCQFtioDGDHCHri8nudlK1n5p2rHCJoog= +github.com/enescakir/emoji v1.0.0/go.mod h1:Bt1EKuLnKDTYpLALApstIkAjdDrS/8IAgTkKp+WKFD0= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= diff --git a/internal/viewer/renderer/debug.go b/internal/viewer/renderer/debug.go index e40ff783..fbcb26f1 100644 --- a/internal/viewer/renderer/debug.go +++ b/internal/viewer/renderer/debug.go @@ -1,6 +1,7 @@ package renderer import ( + "context" "encoding/json" "html" "html/template" @@ -10,11 +11,11 @@ import ( type Debug struct{} -func (d *Debug) RenderText(s string) (v template.HTML) { +func (d *Debug) RenderText(ctx context.Context, s string) (v template.HTML) { return template.HTML("
" + html.EscapeString(s) + "
") } -func (d *Debug) Render(m *slack.Message) (v template.HTML) { +func (d *Debug) Render(ctx context.Context, m *slack.Message) (v template.HTML) { b, err := json.MarshalIndent(m, "", " ") if err != nil { panic(err) diff --git a/internal/viewer/renderer/renderer.go b/internal/viewer/renderer/renderer.go index 569d9c69..1e73bffe 100644 --- a/internal/viewer/renderer/renderer.go +++ b/internal/viewer/renderer/renderer.go @@ -1,12 +1,13 @@ package renderer import ( + "context" "html/template" "github.com/rusq/slack" ) type Renderer interface { - RenderText(s string) (v template.HTML) - Render(m *slack.Message) (v template.HTML) + RenderText(ctx context.Context, s string) (v template.HTML) + Render(ctx context.Context, m *slack.Message) (v template.HTML) } diff --git a/internal/viewer/renderer/slack.go b/internal/viewer/renderer/slack.go index fd04868f..005c7a53 100644 --- a/internal/viewer/renderer/slack.go +++ b/internal/viewer/renderer/slack.go @@ -1,6 +1,7 @@ package renderer import ( + "context" "encoding/json" "fmt" "html" @@ -15,16 +16,41 @@ import ( const debug = true -type Slack struct{} +type Slack struct { + uu map[string]slack.User // map of user id to user + cc map[string]slack.Channel // map of channel id to channel +} + +type SlackOption func(*Slack) + +func WithUsers(uu map[string]slack.User) SlackOption { + return func(sm *Slack) { + sm.uu = uu + } +} + +func WithChannels(cc map[string]slack.Channel) SlackOption { + return func(sm *Slack) { + sm.cc = cc + } +} + +func NewSlack(opts ...SlackOption) *Slack { + s := &Slack{} + for _, opt := range opts { + opt(s) + } + return s +} -func (*Slack) RenderText(s string) (v template.HTML) { +func (*Slack) RenderText(ctx context.Context, s string) (v template.HTML) { // TODO parse legacy markdown return template.HTML("
" + html.EscapeString(s) + "
") } -func (sm *Slack) Render(m *slack.Message) (v template.HTML) { +func (s *Slack) Render(ctx context.Context, m *slack.Message) (v template.HTML) { if len(m.Blocks.BlockSet) == 0 { - return sm.RenderText(m.Text) + return s.RenderText(ctx, m.Text) } attrMsgID := slog.String("message_ts", m.Timestamp) @@ -33,17 +59,17 @@ func (sm *Slack) Render(m *slack.Message) (v template.HTML) { for _, b := range m.Blocks.BlockSet { fn, ok := blockAction[b.BlockType()] if !ok { - slog.Warn("unhandled block type", "block_type", b.BlockType(), attrMsgID) + slog.WarnContext(ctx, "unhandled block type", "block_type", b.BlockType(), attrMsgID) maybeprint(b) continue } - s, err := fn(b) + html, err := fn(s, b) if err != nil { - slog.Error("error rendering block", "error", err, "block_type", b.BlockType(), attrMsgID) + slog.ErrorContext(ctx, "error rendering block", "error", err, "block_type", b.BlockType(), attrMsgID) maybeprint(b) continue } - buf.WriteString(string(s)) + buf.WriteString(html) } return template.HTML(buf.String()) } @@ -57,10 +83,10 @@ func maybeprint(b slack.Block) { } } -var blockAction = map[slack.MessageBlockType]func(slack.Block) (string, error){ - slack.MBTRichText: mbtRichText, - slack.MBTImage: mbtImage, - slack.MBTContext: mbtContext, +var blockAction = map[slack.MessageBlockType]func(*Slack, slack.Block) (string, error){ + slack.MBTRichText: (*Slack).mbtRichText, + slack.MBTImage: (*Slack).mbtImage, + slack.MBTContext: (*Slack).mbtContext, } const stackframe = 1 diff --git a/internal/viewer/renderer/slack_context.go b/internal/viewer/renderer/slack_context.go index 197eace3..87c9f798 100644 --- a/internal/viewer/renderer/slack_context.go +++ b/internal/viewer/renderer/slack_context.go @@ -7,7 +7,7 @@ import ( "github.com/rusq/slack" ) -func mbtContext(ib slack.Block) (string, error) { +func (s *Slack) mbtContext(ib slack.Block) (string, error) { b, ok := ib.(*slack.ContextBlock) if !ok { return "", NewErrIncorrectType(&slack.ContextBlock{}, ib) @@ -18,7 +18,7 @@ func mbtContext(ib slack.Block) (string, error) { if !ok { return "", NewErrMissingHandler(el.MixedElementType()) } - s, err := fn(el) + s, err := fn(s, el) if err != nil { return "", err } @@ -28,12 +28,12 @@ func mbtContext(ib slack.Block) (string, error) { return buf.String(), nil } -var contextElementHandlers = map[slack.MixedElementType]func(slack.MixedElement) (string, error){ - slack.MixedElementImage: metImage, - slack.MixedElementText: metText, +var contextElementHandlers = map[slack.MixedElementType]func(*Slack, slack.MixedElement) (string, error){ + slack.MixedElementImage: (*Slack).metImage, + slack.MixedElementText: (*Slack).metText, } -func metImage(ie slack.MixedElement) (string, error) { +func (*Slack) metImage(ie slack.MixedElement) (string, error) { e, ok := ie.(*slack.ImageBlockElement) if !ok { return "", NewErrIncorrectType(&slack.ImageBlockElement{}, ie) @@ -41,7 +41,7 @@ func metImage(ie slack.MixedElement) (string, error) { return fmt.Sprintf(`%s`, e.ImageURL, e.AltText), nil } -func metText(ie slack.MixedElement) (string, error) { +func (*Slack) metText(ie slack.MixedElement) (string, error) { e, ok := ie.(*slack.TextBlockObject) if !ok { return "", NewErrIncorrectType(&slack.TextBlockObject{}, ie) diff --git a/internal/viewer/renderer/slack_image.go b/internal/viewer/renderer/slack_image.go index 71b59f1f..73157f5d 100644 --- a/internal/viewer/renderer/slack_image.go +++ b/internal/viewer/renderer/slack_image.go @@ -6,10 +6,10 @@ import ( "github.com/rusq/slack" ) -func mbtImage(ib slack.Block) (string, error) { +func (*Slack) mbtImage(ib slack.Block) (string, error) { b, ok := ib.(*slack.ImageBlock) if !ok { return "", NewErrIncorrectType(&slack.ImageBlock{}, ib) } - return fmt.Sprintf(`%s`, b.ImageURL, b.AltText), nil + return fmt.Sprintf(`
%[2]s
%[2]s
`, b.ImageURL, b.AltText), nil } diff --git a/internal/viewer/renderer/slack_rich_text.go b/internal/viewer/renderer/slack_rich_text.go index c051a9a9..375c7cfa 100644 --- a/internal/viewer/renderer/slack_rich_text.go +++ b/internal/viewer/renderer/slack_rich_text.go @@ -2,21 +2,23 @@ package renderer import ( "fmt" + "log/slog" "strings" + emj "github.com/enescakir/emoji" "github.com/rusq/slack" ) -var rteTypeHandlers = map[slack.RichTextElementType]func(slack.RichTextElement) (string, error){} +var rteTypeHandlers = map[slack.RichTextElementType]func(*Slack, slack.RichTextElement) (string, error){} func init() { - rteTypeHandlers[slack.RTESection] = rteSection - rteTypeHandlers[slack.RTEList] = rteList - rteTypeHandlers[slack.RTEQuote] = rteQuote - rteTypeHandlers[slack.RTEPreformatted] = rtePreformatted + rteTypeHandlers[slack.RTESection] = (*Slack).rteSection + rteTypeHandlers[slack.RTEList] = (*Slack).rteList + rteTypeHandlers[slack.RTEQuote] = (*Slack).rteQuote + rteTypeHandlers[slack.RTEPreformatted] = (*Slack).rtePreformatted } -func mbtRichText(ib slack.Block) (string, error) { +func (s *Slack) mbtRichText(ib slack.Block) (string, error) { b, ok := ib.(*slack.RichTextBlock) if !ok { return "", NewErrIncorrectType(&slack.RichTextBlock{}, ib) @@ -27,7 +29,7 @@ func mbtRichText(ib slack.Block) (string, error) { if !ok { return "", NewErrMissingHandler(el.RichTextElementType()) } - s, err := fn(el) + s, err := fn(s, el) if err != nil { return "", err } @@ -37,7 +39,7 @@ func mbtRichText(ib slack.Block) (string, error) { return buf.String(), nil } -func rteSection(ie slack.RichTextElement) (string, error) { +func (s *Slack) rteSection(ie slack.RichTextElement) (string, error) { e, ok := ie.(*slack.RichTextSection) if !ok { return "", NewErrIncorrectType(&slack.RichTextSection{}, ie) @@ -48,7 +50,7 @@ func rteSection(ie slack.RichTextElement) (string, error) { if !ok { return "", NewErrMissingHandler(el.RichTextSectionElementType()) } - s, err := fn(el) + s, err := fn(s, el) if err != nil { return "", err } @@ -58,15 +60,15 @@ func rteSection(ie slack.RichTextElement) (string, error) { return buf.String(), nil } -var rtseHandlers = map[slack.RichTextSectionElementType]func(slack.RichTextSectionElement) (string, error){ - slack.RTSEText: rtseText, - slack.RTSELink: rtseLink, - slack.RTSEUser: rtseUser, - slack.RTSEEmoji: rtseEmoji, - slack.RTSEChannel: rtseChannel, +var rtseHandlers = map[slack.RichTextSectionElementType]func(*Slack, slack.RichTextSectionElement) (string, error){ + slack.RTSEText: (*Slack).rtseText, + slack.RTSELink: (*Slack).rtseLink, + slack.RTSEUser: (*Slack).rtseUser, + slack.RTSEEmoji: (*Slack).rtseEmoji, + slack.RTSEChannel: (*Slack).rtseChannel, } -func rtseText(ie slack.RichTextSectionElement) (string, error) { +func (s *Slack) rtseText(ie slack.RichTextSectionElement) (string, error) { e, ok := ie.(*slack.RichTextSectionTextElement) if !ok { return "", NewErrIncorrectType(&slack.RichTextSectionTextElement{}, ie) @@ -95,7 +97,7 @@ func applyStyle(s string, style *slack.RichTextSectionTextStyle) string { return s } -func rtseLink(ie slack.RichTextSectionElement) (string, error) { +func (s *Slack) rtseLink(ie slack.RichTextSectionElement) (string, error) { e, ok := ie.(*slack.RichTextSectionLinkElement) if !ok { return "", NewErrIncorrectType(&slack.RichTextSectionLinkElement{}, ie) @@ -106,7 +108,7 @@ func rtseLink(ie slack.RichTextSectionElement) (string, error) { return fmt.Sprintf("%s", e.URL, e.Text), nil } -func rteList(ie slack.RichTextElement) (string, error) { +func (s *Slack) rteList(ie slack.RichTextElement) (string, error) { e, ok := ie.(*slack.RichTextList) if !ok { return "", NewErrIncorrectType(&slack.RichTextList{}, ie) @@ -128,7 +130,7 @@ func rteList(ie slack.RichTextElement) (string, error) { if !ok { return "", NewErrMissingHandler(el.RichTextElementType()) } - s, err := fn(el) + s, err := fn(s, el) if err != nil { return "", err } @@ -138,7 +140,7 @@ func rteList(ie slack.RichTextElement) (string, error) { return buf.String(), nil } -func rteQuote(ie slack.RichTextElement) (string, error) { +func (s *Slack) rteQuote(ie slack.RichTextElement) (string, error) { e, ok := ie.(*slack.RichTextQuote) if !ok { return "", NewErrIncorrectType(&slack.RichTextQuote{}, ie) @@ -150,7 +152,7 @@ func rteQuote(ie slack.RichTextElement) (string, error) { if !ok { return "", NewErrMissingHandler(el.RichTextSectionElementType()) } - s, err := fn(el) + s, err := fn(s, el) if err != nil { return "", err } @@ -160,7 +162,7 @@ func rteQuote(ie slack.RichTextElement) (string, error) { return buf.String(), nil } -func rtePreformatted(ie slack.RichTextElement) (string, error) { +func (s *Slack) rtePreformatted(ie slack.RichTextElement) (string, error) { e, ok := ie.(*slack.RichTextPreformatted) if !ok { return "", NewErrIncorrectType(&slack.RichTextPreformatted{}, ie) @@ -172,7 +174,7 @@ func rtePreformatted(ie slack.RichTextElement) (string, error) { if !ok { return "", NewErrMissingHandler(el.RichTextSectionElementType()) } - s, err := fn(el) + s, err := fn(s, el) if err != nil { return "", err } @@ -182,28 +184,47 @@ func rtePreformatted(ie slack.RichTextElement) (string, error) { return buf.String(), nil } -func rtseUser(ie slack.RichTextSectionElement) (string, error) { +func (s *Slack) rtseUser(ie slack.RichTextSectionElement) (string, error) { e, ok := ie.(*slack.RichTextSectionUserElement) if !ok { return "", NewErrIncorrectType(&slack.RichTextSectionUserElement{}, ie) } + var name string + u, ok := s.uu[e.UserID] + if ok { + name = u.Name + } else { + slog.Warn("user not found", "user_id", e.UserID, "user", u) + name = e.UserID + } + // TODO: link user. - return applyStyle(fmt.Sprintf("<@%s>", e.UserID), e.Style), nil + return applyStyle(fmt.Sprintf("<@%s>", name), e.Style), nil } -func rtseEmoji(ie slack.RichTextSectionElement) (string, error) { +func (s *Slack) rtseEmoji(ie slack.RichTextSectionElement) (string, error) { e, ok := ie.(*slack.RichTextSectionEmojiElement) if !ok { return "", NewErrIncorrectType(&slack.RichTextSectionEmojiElement{}, ie) } // TODO: resolve and render emoji. - return applyStyle(fmt.Sprintf(":%s:", e.Name), e.Style), nil + em := emj.Parse(fmt.Sprintf(":%s:", e.Name)) + return applyStyle(em, e.Style), nil } -func rtseChannel(ie slack.RichTextSectionElement) (string, error) { +func (s *Slack) rtseChannel(ie slack.RichTextSectionElement) (string, error) { e, ok := ie.(*slack.RichTextSectionChannelElement) if !ok { return "", NewErrIncorrectType(&slack.RichTextSectionChannelElement{}, ie) } - return applyStyle(fmt.Sprintf("<#%s>", e.ChannelID), e.Style), nil + var name string + c, ok := s.uu[e.ChannelID] + if ok { + name = c.Name + } else { + slog.Warn("channel not found", "channel_id", e.ChannelID) + name = e.ChannelID + } + + return applyStyle(fmt.Sprintf("<#%s>", name), e.Style), nil } diff --git a/internal/viewer/renderer/slack_rich_text_test.go b/internal/viewer/renderer/slack_rich_text_test.go index d76ec567..a1820b51 100644 --- a/internal/viewer/renderer/slack_rich_text_test.go +++ b/internal/viewer/renderer/slack_rich_text_test.go @@ -12,12 +12,14 @@ func Test_rtseText(t *testing.T) { } tests := []struct { name string + s *Slack args args want string wantErr bool }{ { "valid text section", + &Slack{}, args{ ie: slack.RichTextSectionElement(slack.NewRichTextSectionTextElement("New Message", nil)), }, @@ -26,6 +28,7 @@ func Test_rtseText(t *testing.T) { }, { "multiline", + &Slack{}, args{ ie: slack.RichTextSectionElement(slack.NewRichTextSectionTextElement("New\nMessage", nil)), }, @@ -34,6 +37,7 @@ func Test_rtseText(t *testing.T) { }, { "bold", + &Slack{}, args{ ie: slack.RichTextSectionElement(slack.NewRichTextSectionTextElement("New Message", &slack.RichTextSectionTextStyle{Bold: true})), }, @@ -42,6 +46,7 @@ func Test_rtseText(t *testing.T) { }, { "italic", + &Slack{}, args{ ie: slack.RichTextSectionElement(slack.NewRichTextSectionTextElement("New Message", &slack.RichTextSectionTextStyle{Italic: true})), }, @@ -50,6 +55,7 @@ func Test_rtseText(t *testing.T) { }, { "strike", + &Slack{}, args{ ie: slack.RichTextSectionElement(slack.NewRichTextSectionTextElement("New Message", &slack.RichTextSectionTextStyle{Strike: true})), }, @@ -58,6 +64,7 @@ func Test_rtseText(t *testing.T) { }, { "code", + &Slack{}, args{ ie: slack.RichTextSectionElement(slack.NewRichTextSectionTextElement("Code message", &slack.RichTextSectionTextStyle{Code: true})), }, @@ -66,6 +73,7 @@ func Test_rtseText(t *testing.T) { }, { "bold italic", + &Slack{}, args{ ie: slack.RichTextSectionElement(slack.NewRichTextSectionTextElement("New Message", &slack.RichTextSectionTextStyle{Bold: true, Italic: true})), }, @@ -75,7 +83,7 @@ func Test_rtseText(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := rtseText(tt.args.ie) + got, err := tt.s.rtseText(tt.args.ie) if (err != nil) != tt.wantErr { t.Errorf("rtseText() error = %v, wantErr %v", err, tt.wantErr) return @@ -93,12 +101,14 @@ func Test_rtseLink(t *testing.T) { } tests := []struct { name string + s *Slack args args want string wantErr bool }{ { "valid link", + &Slack{}, args{ ie: slack.RichTextSectionElement(slack.NewRichTextSectionLinkElement("https://example.com", "example.com", nil)), }, @@ -107,6 +117,7 @@ func Test_rtseLink(t *testing.T) { }, { "empty text", + &Slack{}, args{ ie: slack.RichTextSectionElement(slack.NewRichTextSectionLinkElement("https://example.com", "", nil)), }, @@ -116,7 +127,7 @@ func Test_rtseLink(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := rtseLink(tt.args.ie) + got, err := tt.s.rtseLink(tt.args.ie) if (err != nil) != tt.wantErr { t.Errorf("rtseLink() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/internal/viewer/renderer/slack_test.go b/internal/viewer/renderer/slack_test.go index 0a998265..944d4941 100644 --- a/internal/viewer/renderer/slack_test.go +++ b/internal/viewer/renderer/slack_test.go @@ -1,6 +1,7 @@ package renderer import ( + "context" "html/template" "reflect" "testing" @@ -31,7 +32,7 @@ func TestSlack_Render(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { sm := &Slack{} - if gotV := sm.Render(tt.args.m); !reflect.DeepEqual(gotV, tt.wantV) { + if gotV := sm.Render(context.Background(), tt.args.m); !reflect.DeepEqual(gotV, tt.wantV) { t.Errorf("Slack.Render() = %v, want %v", gotV, tt.wantV) } }) diff --git a/internal/viewer/templates/index.html b/internal/viewer/templates/index.html index 0b638cf9..e22ca962 100644 --- a/internal/viewer/templates/index.html +++ b/internal/viewer/templates/index.html @@ -119,11 +119,11 @@

Thread: {{ .ThreadID }}

{{end}} {{ define "render_message" }} -
+
{{ displayname .User }} - {{ time $.Timestamp }} + {{ time .Timestamp }}

{{/* .Text | rendertext */}}{{ render . }}

diff --git a/internal/viewer/viewer.go b/internal/viewer/viewer.go index c1004f4e..d3967d6c 100644 --- a/internal/viewer/viewer.go +++ b/internal/viewer/viewer.go @@ -66,13 +66,14 @@ func New(ctx context.Context, dir *chunk.Directory, addr string) (*Viewer, error if err != nil { return nil, err } - + sr := renderer.NewSlack(renderer.WithUsers(indexusers(uu)), renderer.WithChannels(indexchannels(all))) + // sr := &renderer.Debug{} v := &Viewer{ d: dir, ch: cc, um: st.NewUserIndex(uu), lg: logger.FromContext(ctx), - r: &renderer.Slack{}, + r: sr, } // postinit { @@ -81,8 +82,8 @@ func New(ctx context.Context, dir *chunk.Directory, addr string) (*Viewer, error "rendername": v.name, "displayname": v.um.DisplayName, "time": localtime, - "rendertext": v.r.RenderText, // render message text - "render": v.r.Render, // render message + "rendertext": func(s string) template.HTML { return v.r.RenderText(context.Background(), s) }, // render message text + "render": func(m *slack.Message) template.HTML { return v.r.Render(context.Background(), m) }, // render message "is_thread_start": st.IsThreadStart, }, ).ParseFS(fsys, "templates/*.html")) @@ -151,3 +152,19 @@ func (v *Viewer) name(ch slack.Channel) (who string) { } return who } + +func indexusers(uu []slack.User) (m map[string]slack.User) { + m = make(map[string]slack.User, len(uu)) + for i := range uu { + m[uu[i].ID] = uu[i] + } + return m +} + +func indexchannels(cc []slack.Channel) (m map[string]slack.Channel) { + m = make(map[string]slack.Channel, len(cc)) + for i := range cc { + m[cc[i].ID] = cc[i] + } + return m +}