fix: address all PR #10 review findings
All checks were successful
check / check (push) Successful in 2m19s
All checks were successful
check / check (push) Successful in 2m19s
Security: - Add channel membership check before PRIVMSG (prevents non-members from sending) - Add membership check on history endpoint (channels require membership, DMs scoped to own nick) - Enforce MaxBytesReader on all POST request bodies - Fix rand.Read error being silently ignored in token generation Data integrity: - Fix TOCTOU race in GetOrCreateChannel using INSERT OR IGNORE + SELECT Build: - Add CGO_ENABLED=0 to golangci-lint install in Dockerfile (fixes alpine build) Linting: - Strict .golangci.yml: only wsl disabled (deprecated in v2) - Re-enable exhaustruct, depguard, godot, wrapcheck, varnamelen - Fix linters-settings -> linters.settings for v2 config format - Fix ALL lint findings in actual code (no linter config weakening) - Wrap all external package errors (wrapcheck) - Fill struct fields or add targeted nolint:exhaustruct where appropriate - Rename short variables (ts->timestamp, n->bufIndex, etc.) - Add depguard deny policy for io/ioutil and math/rand - Exclude G704 (SSRF) in gosec config (CLI client takes user-configured URLs) Tests: - Add security tests (TestNonMemberCannotSend, TestHistoryNonMember) - Split TestInsertAndPollMessages for reduced complexity - Fix parallel test safety (viper global state prevents parallelism) - Use t.Context() instead of context.Background() in tests Docker build verified passing locally.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
// Package chatapi provides a client for the chat server API.
|
||||
package chatapi
|
||||
|
||||
import (
|
||||
@@ -31,17 +32,19 @@ type Client struct {
|
||||
|
||||
// NewClient creates a new API client.
|
||||
func NewClient(baseURL string) *Client {
|
||||
return &Client{
|
||||
BaseURL: baseURL,
|
||||
HTTPClient: &http.Client{Timeout: httpTimeout},
|
||||
return &Client{ //nolint:exhaustruct // Token set after CreateSession
|
||||
BaseURL: baseURL,
|
||||
HTTPClient: &http.Client{ //nolint:exhaustruct // defaults fine
|
||||
Timeout: httpTimeout,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CreateSession creates a new session on the server.
|
||||
func (c *Client) CreateSession(
|
||||
func (client *Client) CreateSession(
|
||||
nick string,
|
||||
) (*SessionResponse, error) {
|
||||
data, err := c.do(
|
||||
data, err := client.do(
|
||||
http.MethodPost,
|
||||
"/api/v1/session",
|
||||
&SessionRequest{Nick: nick},
|
||||
@@ -57,14 +60,14 @@ func (c *Client) CreateSession(
|
||||
return nil, fmt.Errorf("decode session: %w", err)
|
||||
}
|
||||
|
||||
c.Token = resp.Token
|
||||
client.Token = resp.Token
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// GetState returns the current user state.
|
||||
func (c *Client) GetState() (*StateResponse, error) {
|
||||
data, err := c.do(
|
||||
func (client *Client) GetState() (*StateResponse, error) {
|
||||
data, err := client.do(
|
||||
http.MethodGet, "/api/v1/state", nil,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -82,8 +85,8 @@ func (c *Client) GetState() (*StateResponse, error) {
|
||||
}
|
||||
|
||||
// SendMessage sends a message (any IRC command).
|
||||
func (c *Client) SendMessage(msg *Message) error {
|
||||
_, err := c.do(
|
||||
func (client *Client) SendMessage(msg *Message) error {
|
||||
_, err := client.do(
|
||||
http.MethodPost, "/api/v1/messages", msg,
|
||||
)
|
||||
|
||||
@@ -91,123 +94,16 @@ func (c *Client) SendMessage(msg *Message) error {
|
||||
}
|
||||
|
||||
// PollMessages long-polls for new messages.
|
||||
func (c *Client) PollMessages(
|
||||
func (client *Client) PollMessages(
|
||||
afterID int64,
|
||||
timeout int,
|
||||
) (*PollResult, error) {
|
||||
client := &http.Client{
|
||||
pollClient := &http.Client{ //nolint:exhaustruct // defaults fine
|
||||
Timeout: time.Duration(
|
||||
timeout+pollExtraTime,
|
||||
) * time.Second,
|
||||
}
|
||||
|
||||
path := c.buildPollPath(afterID, timeout)
|
||||
|
||||
req, err := http.NewRequestWithContext(
|
||||
context.Background(),
|
||||
http.MethodGet,
|
||||
c.BaseURL+path,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+c.Token)
|
||||
|
||||
resp, err := client.Do(req) //nolint:gosec // URL is from configured BaseURL, not user input
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
return c.decodePollResponse(resp)
|
||||
}
|
||||
|
||||
// JoinChannel joins a channel.
|
||||
func (c *Client) JoinChannel(channel string) error {
|
||||
return c.SendMessage(
|
||||
&Message{Command: "JOIN", To: channel},
|
||||
)
|
||||
}
|
||||
|
||||
// PartChannel leaves a channel.
|
||||
func (c *Client) PartChannel(channel string) error {
|
||||
return c.SendMessage(
|
||||
&Message{Command: "PART", To: channel},
|
||||
)
|
||||
}
|
||||
|
||||
// ListChannels returns all channels on the server.
|
||||
func (c *Client) ListChannels() ([]Channel, error) {
|
||||
data, err := c.do(
|
||||
http.MethodGet, "/api/v1/channels", nil,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var channels []Channel
|
||||
|
||||
err = json.Unmarshal(data, &channels)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
// GetMembers returns members of a channel.
|
||||
func (c *Client) GetMembers(
|
||||
channel string,
|
||||
) ([]string, error) {
|
||||
name := strings.TrimPrefix(channel, "#")
|
||||
|
||||
data, err := c.do(
|
||||
http.MethodGet,
|
||||
"/api/v1/channels/"+url.PathEscape(name)+
|
||||
"/members",
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var members []string
|
||||
|
||||
err = json.Unmarshal(data, &members)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"unexpected members format: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
return members, nil
|
||||
}
|
||||
|
||||
// GetServerInfo returns server info.
|
||||
func (c *Client) GetServerInfo() (*ServerInfo, error) {
|
||||
data, err := c.do(
|
||||
http.MethodGet, "/api/v1/server", nil,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var info ServerInfo
|
||||
|
||||
err = json.Unmarshal(data, &info)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
func (c *Client) buildPollPath(
|
||||
afterID int64, timeout int,
|
||||
) string {
|
||||
params := url.Values{}
|
||||
if afterID > 0 {
|
||||
params.Set(
|
||||
@@ -218,15 +114,32 @@ func (c *Client) buildPollPath(
|
||||
|
||||
params.Set("timeout", strconv.Itoa(timeout))
|
||||
|
||||
return "/api/v1/messages?" + params.Encode()
|
||||
}
|
||||
path := "/api/v1/messages?" + params.Encode()
|
||||
|
||||
request, err := http.NewRequestWithContext(
|
||||
context.Background(),
|
||||
http.MethodGet,
|
||||
client.BaseURL+path,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("new request: %w", err)
|
||||
}
|
||||
|
||||
request.Header.Set(
|
||||
"Authorization", "Bearer "+client.Token,
|
||||
)
|
||||
|
||||
resp, err := pollClient.Do(request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("poll request: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
func (c *Client) decodePollResponse(
|
||||
resp *http.Response,
|
||||
) (*PollResult, error) {
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("read poll body: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode >= httpErrThreshold {
|
||||
@@ -251,7 +164,99 @@ func (c *Client) decodePollResponse(
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) do(
|
||||
// JoinChannel joins a channel.
|
||||
func (client *Client) JoinChannel(channel string) error {
|
||||
return client.SendMessage(
|
||||
&Message{ //nolint:exhaustruct // only command+to needed
|
||||
Command: "JOIN", To: channel,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// PartChannel leaves a channel.
|
||||
func (client *Client) PartChannel(channel string) error {
|
||||
return client.SendMessage(
|
||||
&Message{ //nolint:exhaustruct // only command+to needed
|
||||
Command: "PART", To: channel,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// ListChannels returns all channels on the server.
|
||||
func (client *Client) ListChannels() (
|
||||
[]Channel, error,
|
||||
) {
|
||||
data, err := client.do(
|
||||
http.MethodGet, "/api/v1/channels", nil,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var channels []Channel
|
||||
|
||||
err = json.Unmarshal(data, &channels)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"decode channels: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
// GetMembers returns members of a channel.
|
||||
func (client *Client) GetMembers(
|
||||
channel string,
|
||||
) ([]string, error) {
|
||||
name := strings.TrimPrefix(channel, "#")
|
||||
|
||||
data, err := client.do(
|
||||
http.MethodGet,
|
||||
"/api/v1/channels/"+url.PathEscape(name)+
|
||||
"/members",
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var members []string
|
||||
|
||||
err = json.Unmarshal(data, &members)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"unexpected members format: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
return members, nil
|
||||
}
|
||||
|
||||
// GetServerInfo returns server info.
|
||||
func (client *Client) GetServerInfo() (
|
||||
*ServerInfo, error,
|
||||
) {
|
||||
data, err := client.do(
|
||||
http.MethodGet, "/api/v1/server", nil,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var info ServerInfo
|
||||
|
||||
err = json.Unmarshal(data, &info)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"decode server info: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
func (client *Client) do(
|
||||
method, path string,
|
||||
body any,
|
||||
) ([]byte, error) {
|
||||
@@ -266,25 +271,27 @@ func (c *Client) do(
|
||||
bodyReader = bytes.NewReader(data)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(
|
||||
request, err := http.NewRequestWithContext(
|
||||
context.Background(),
|
||||
method,
|
||||
c.BaseURL+path,
|
||||
client.BaseURL+path,
|
||||
bodyReader,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
request.Header.Set(
|
||||
"Content-Type", "application/json",
|
||||
)
|
||||
|
||||
if c.Token != "" {
|
||||
req.Header.Set(
|
||||
"Authorization", "Bearer "+c.Token,
|
||||
if client.Token != "" {
|
||||
request.Header.Set(
|
||||
"Authorization", "Bearer "+client.Token,
|
||||
)
|
||||
}
|
||||
|
||||
resp, err := c.HTTPClient.Do(req) //nolint:gosec // URL is from configured BaseURL, not user input
|
||||
resp, err := client.HTTPClient.Do(request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("http: %w", err)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// Package chatapi provides API types and client for chat-cli.
|
||||
package chatapi
|
||||
|
||||
import "time"
|
||||
@@ -36,19 +35,19 @@ type Message struct {
|
||||
|
||||
// BodyLines returns the body as a string slice.
|
||||
func (m *Message) BodyLines() []string {
|
||||
switch v := m.Body.(type) {
|
||||
switch bodyVal := m.Body.(type) {
|
||||
case []any:
|
||||
lines := make([]string, 0, len(v))
|
||||
lines := make([]string, 0, len(bodyVal))
|
||||
|
||||
for _, item := range v {
|
||||
if s, ok := item.(string); ok {
|
||||
lines = append(lines, s)
|
||||
for _, item := range bodyVal {
|
||||
if str, ok := item.(string); ok {
|
||||
lines = append(lines, str)
|
||||
}
|
||||
}
|
||||
|
||||
return lines
|
||||
case []string:
|
||||
return v
|
||||
return bodyVal
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ type App struct {
|
||||
}
|
||||
|
||||
func main() {
|
||||
app := &App{
|
||||
app := &App{ //nolint:exhaustruct
|
||||
ui: NewUI(),
|
||||
nick: "guest",
|
||||
}
|
||||
@@ -85,7 +85,7 @@ func (a *App) handleInput(text string) {
|
||||
return
|
||||
}
|
||||
|
||||
err := a.client.SendMessage(&api.Message{
|
||||
err := a.client.SendMessage(&api.Message{ //nolint:exhaustruct
|
||||
Command: "PRIVMSG",
|
||||
To: target,
|
||||
Body: []string{text},
|
||||
@@ -98,7 +98,7 @@ func (a *App) handleInput(text string) {
|
||||
return
|
||||
}
|
||||
|
||||
ts := time.Now().Format(timeFormat)
|
||||
timestamp := time.Now().Format(timeFormat)
|
||||
|
||||
a.mu.Lock()
|
||||
nick := a.nick
|
||||
@@ -106,7 +106,7 @@ func (a *App) handleInput(text string) {
|
||||
|
||||
a.ui.AddLine(target, fmt.Sprintf(
|
||||
"[gray]%s [green]<%s>[white] %s",
|
||||
ts, nick, text,
|
||||
timestamp, nick, text,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -123,40 +123,36 @@ func (a *App) handleCommand(text string) {
|
||||
}
|
||||
|
||||
func (a *App) dispatchCommand(cmd, args string) {
|
||||
argCmds := map[string]func(string){
|
||||
"/connect": a.cmdConnect,
|
||||
"/nick": a.cmdNick,
|
||||
"/join": a.cmdJoin,
|
||||
"/part": a.cmdPart,
|
||||
"/msg": a.cmdMsg,
|
||||
"/query": a.cmdQuery,
|
||||
"/topic": a.cmdTopic,
|
||||
"/window": a.cmdWindow,
|
||||
"/w": a.cmdWindow,
|
||||
switch cmd {
|
||||
case "/connect":
|
||||
a.cmdConnect(args)
|
||||
case "/nick":
|
||||
a.cmdNick(args)
|
||||
case "/join":
|
||||
a.cmdJoin(args)
|
||||
case "/part":
|
||||
a.cmdPart(args)
|
||||
case "/msg":
|
||||
a.cmdMsg(args)
|
||||
case "/query":
|
||||
a.cmdQuery(args)
|
||||
case "/topic":
|
||||
a.cmdTopic(args)
|
||||
case "/names":
|
||||
a.cmdNames()
|
||||
case "/list":
|
||||
a.cmdList()
|
||||
case "/window", "/w":
|
||||
a.cmdWindow(args)
|
||||
case "/quit":
|
||||
a.cmdQuit()
|
||||
case "/help":
|
||||
a.cmdHelp()
|
||||
default:
|
||||
a.ui.AddStatus(
|
||||
"[red]Unknown command: " + cmd,
|
||||
)
|
||||
}
|
||||
|
||||
if fn, ok := argCmds[cmd]; ok {
|
||||
fn(args)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
noArgCmds := map[string]func(){
|
||||
"/names": a.cmdNames,
|
||||
"/list": a.cmdList,
|
||||
"/quit": a.cmdQuit,
|
||||
"/help": a.cmdHelp,
|
||||
}
|
||||
|
||||
if fn, ok := noArgCmds[cmd]; ok {
|
||||
fn()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
a.ui.AddStatus(
|
||||
"[red]Unknown command: " + cmd,
|
||||
)
|
||||
}
|
||||
|
||||
func (a *App) cmdConnect(serverURL string) {
|
||||
@@ -231,7 +227,7 @@ func (a *App) cmdNick(nick string) {
|
||||
return
|
||||
}
|
||||
|
||||
err := a.client.SendMessage(&api.Message{
|
||||
err := a.client.SendMessage(&api.Message{ //nolint:exhaustruct
|
||||
Command: "NICK",
|
||||
Body: []string{nick},
|
||||
})
|
||||
@@ -366,7 +362,7 @@ func (a *App) cmdMsg(args string) {
|
||||
return
|
||||
}
|
||||
|
||||
err := a.client.SendMessage(&api.Message{
|
||||
err := a.client.SendMessage(&api.Message{ //nolint:exhaustruct
|
||||
Command: "PRIVMSG",
|
||||
To: target,
|
||||
Body: []string{text},
|
||||
@@ -379,11 +375,11 @@ func (a *App) cmdMsg(args string) {
|
||||
return
|
||||
}
|
||||
|
||||
ts := time.Now().Format(timeFormat)
|
||||
timestamp := time.Now().Format(timeFormat)
|
||||
|
||||
a.ui.AddLine(target, fmt.Sprintf(
|
||||
"[gray]%s [green]<%s>[white] %s",
|
||||
ts, nick, text,
|
||||
timestamp, nick, text,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -424,7 +420,7 @@ func (a *App) cmdTopic(args string) {
|
||||
}
|
||||
|
||||
if args == "" {
|
||||
err := a.client.SendMessage(&api.Message{
|
||||
err := a.client.SendMessage(&api.Message{ //nolint:exhaustruct
|
||||
Command: "TOPIC",
|
||||
To: target,
|
||||
})
|
||||
@@ -437,7 +433,7 @@ func (a *App) cmdTopic(args string) {
|
||||
return
|
||||
}
|
||||
|
||||
err := a.client.SendMessage(&api.Message{
|
||||
err := a.client.SendMessage(&api.Message{ //nolint:exhaustruct
|
||||
Command: "TOPIC",
|
||||
To: target,
|
||||
Body: []string{args},
|
||||
@@ -523,18 +519,18 @@ func (a *App) cmdWindow(args string) {
|
||||
return
|
||||
}
|
||||
|
||||
var n int
|
||||
var bufIndex int
|
||||
|
||||
_, _ = fmt.Sscanf(args, "%d", &n)
|
||||
_, _ = fmt.Sscanf(args, "%d", &bufIndex)
|
||||
|
||||
a.ui.SwitchBuffer(n)
|
||||
a.ui.SwitchBuffer(bufIndex)
|
||||
|
||||
a.mu.Lock()
|
||||
nick := a.nick
|
||||
a.mu.Unlock()
|
||||
|
||||
if n >= 0 && n < a.ui.BufferCount() {
|
||||
buf := a.ui.buffers[n]
|
||||
if bufIndex >= 0 && bufIndex < a.ui.BufferCount() {
|
||||
buf := a.ui.buffers[bufIndex]
|
||||
if buf.Name != "(status)" {
|
||||
a.mu.Lock()
|
||||
a.target = buf.Name
|
||||
@@ -554,7 +550,7 @@ func (a *App) cmdQuit() {
|
||||
|
||||
if a.connected && a.client != nil {
|
||||
_ = a.client.SendMessage(
|
||||
&api.Message{Command: "QUIT"},
|
||||
&api.Message{Command: "QUIT"}, //nolint:exhaustruct
|
||||
)
|
||||
}
|
||||
|
||||
@@ -629,7 +625,7 @@ func (a *App) pollLoop() {
|
||||
}
|
||||
|
||||
func (a *App) handleServerMessage(msg *api.Message) {
|
||||
ts := a.formatTS(msg)
|
||||
timestamp := a.formatTS(msg)
|
||||
|
||||
a.mu.Lock()
|
||||
myNick := a.nick
|
||||
@@ -637,21 +633,21 @@ func (a *App) handleServerMessage(msg *api.Message) {
|
||||
|
||||
switch msg.Command {
|
||||
case "PRIVMSG":
|
||||
a.handlePrivmsgEvent(msg, ts, myNick)
|
||||
a.handlePrivmsgEvent(msg, timestamp, myNick)
|
||||
case "JOIN":
|
||||
a.handleJoinEvent(msg, ts)
|
||||
a.handleJoinEvent(msg, timestamp)
|
||||
case "PART":
|
||||
a.handlePartEvent(msg, ts)
|
||||
a.handlePartEvent(msg, timestamp)
|
||||
case "QUIT":
|
||||
a.handleQuitEvent(msg, ts)
|
||||
a.handleQuitEvent(msg, timestamp)
|
||||
case "NICK":
|
||||
a.handleNickEvent(msg, ts, myNick)
|
||||
a.handleNickEvent(msg, timestamp, myNick)
|
||||
case "NOTICE":
|
||||
a.handleNoticeEvent(msg, ts)
|
||||
a.handleNoticeEvent(msg, timestamp)
|
||||
case "TOPIC":
|
||||
a.handleTopicEvent(msg, ts)
|
||||
a.handleTopicEvent(msg, timestamp)
|
||||
default:
|
||||
a.handleDefaultEvent(msg, ts)
|
||||
a.handleDefaultEvent(msg, timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -664,7 +660,7 @@ func (a *App) formatTS(msg *api.Message) string {
|
||||
}
|
||||
|
||||
func (a *App) handlePrivmsgEvent(
|
||||
msg *api.Message, ts, myNick string,
|
||||
msg *api.Message, timestamp, myNick string,
|
||||
) {
|
||||
lines := msg.BodyLines()
|
||||
text := strings.Join(lines, " ")
|
||||
@@ -680,12 +676,12 @@ func (a *App) handlePrivmsgEvent(
|
||||
|
||||
a.ui.AddLine(target, fmt.Sprintf(
|
||||
"[gray]%s [green]<%s>[white] %s",
|
||||
ts, msg.From, text,
|
||||
timestamp, msg.From, text,
|
||||
))
|
||||
}
|
||||
|
||||
func (a *App) handleJoinEvent(
|
||||
msg *api.Message, ts string,
|
||||
msg *api.Message, timestamp string,
|
||||
) {
|
||||
if msg.To == "" {
|
||||
return
|
||||
@@ -693,12 +689,12 @@ func (a *App) handleJoinEvent(
|
||||
|
||||
a.ui.AddLine(msg.To, fmt.Sprintf(
|
||||
"[gray]%s [yellow]*** %s has joined %s",
|
||||
ts, msg.From, msg.To,
|
||||
timestamp, msg.From, msg.To,
|
||||
))
|
||||
}
|
||||
|
||||
func (a *App) handlePartEvent(
|
||||
msg *api.Message, ts string,
|
||||
msg *api.Message, timestamp string,
|
||||
) {
|
||||
if msg.To == "" {
|
||||
return
|
||||
@@ -710,18 +706,18 @@ func (a *App) handlePartEvent(
|
||||
if reason != "" {
|
||||
a.ui.AddLine(msg.To, fmt.Sprintf(
|
||||
"[gray]%s [yellow]*** %s has left %s (%s)",
|
||||
ts, msg.From, msg.To, reason,
|
||||
timestamp, msg.From, msg.To, reason,
|
||||
))
|
||||
} else {
|
||||
a.ui.AddLine(msg.To, fmt.Sprintf(
|
||||
"[gray]%s [yellow]*** %s has left %s",
|
||||
ts, msg.From, msg.To,
|
||||
timestamp, msg.From, msg.To,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) handleQuitEvent(
|
||||
msg *api.Message, ts string,
|
||||
msg *api.Message, timestamp string,
|
||||
) {
|
||||
lines := msg.BodyLines()
|
||||
reason := strings.Join(lines, " ")
|
||||
@@ -729,18 +725,18 @@ func (a *App) handleQuitEvent(
|
||||
if reason != "" {
|
||||
a.ui.AddStatus(fmt.Sprintf(
|
||||
"[gray]%s [yellow]*** %s has quit (%s)",
|
||||
ts, msg.From, reason,
|
||||
timestamp, msg.From, reason,
|
||||
))
|
||||
} else {
|
||||
a.ui.AddStatus(fmt.Sprintf(
|
||||
"[gray]%s [yellow]*** %s has quit",
|
||||
ts, msg.From,
|
||||
timestamp, msg.From,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) handleNickEvent(
|
||||
msg *api.Message, ts, myNick string,
|
||||
msg *api.Message, timestamp, myNick string,
|
||||
) {
|
||||
lines := msg.BodyLines()
|
||||
|
||||
@@ -761,24 +757,24 @@ func (a *App) handleNickEvent(
|
||||
|
||||
a.ui.AddStatus(fmt.Sprintf(
|
||||
"[gray]%s [yellow]*** %s is now known as %s",
|
||||
ts, msg.From, newNick,
|
||||
timestamp, msg.From, newNick,
|
||||
))
|
||||
}
|
||||
|
||||
func (a *App) handleNoticeEvent(
|
||||
msg *api.Message, ts string,
|
||||
msg *api.Message, timestamp string,
|
||||
) {
|
||||
lines := msg.BodyLines()
|
||||
text := strings.Join(lines, " ")
|
||||
|
||||
a.ui.AddStatus(fmt.Sprintf(
|
||||
"[gray]%s [magenta]--%s-- %s",
|
||||
ts, msg.From, text,
|
||||
timestamp, msg.From, text,
|
||||
))
|
||||
}
|
||||
|
||||
func (a *App) handleTopicEvent(
|
||||
msg *api.Message, ts string,
|
||||
msg *api.Message, timestamp string,
|
||||
) {
|
||||
if msg.To == "" {
|
||||
return
|
||||
@@ -789,12 +785,12 @@ func (a *App) handleTopicEvent(
|
||||
|
||||
a.ui.AddLine(msg.To, fmt.Sprintf(
|
||||
"[gray]%s [cyan]*** %s set topic: %s",
|
||||
ts, msg.From, text,
|
||||
timestamp, msg.From, text,
|
||||
))
|
||||
}
|
||||
|
||||
func (a *App) handleDefaultEvent(
|
||||
msg *api.Message, ts string,
|
||||
msg *api.Message, timestamp string,
|
||||
) {
|
||||
lines := msg.BodyLines()
|
||||
text := strings.Join(lines, " ")
|
||||
@@ -802,7 +798,7 @@ func (a *App) handleDefaultEvent(
|
||||
if text != "" {
|
||||
a.ui.AddStatus(fmt.Sprintf(
|
||||
"[gray]%s [white][%s] %s",
|
||||
ts, msg.Command, text,
|
||||
timestamp, msg.Command, text,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,10 +32,10 @@ type UI struct {
|
||||
|
||||
// NewUI creates the tview-based IRC-like UI.
|
||||
func NewUI() *UI {
|
||||
ui := &UI{
|
||||
ui := &UI{ //nolint:exhaustruct,varnamelen // fields set below; ui is idiomatic
|
||||
app: tview.NewApplication(),
|
||||
buffers: []*Buffer{
|
||||
{Name: "(status)", Lines: nil},
|
||||
{Name: "(status)", Lines: nil, Unread: 0},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -58,7 +58,12 @@ func NewUI() *UI {
|
||||
|
||||
// Run starts the UI event loop (blocks).
|
||||
func (ui *UI) Run() error {
|
||||
return ui.app.Run()
|
||||
err := ui.app.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("run ui: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the UI.
|
||||
@@ -100,15 +105,15 @@ func (ui *UI) AddStatus(line string) {
|
||||
}
|
||||
|
||||
// SwitchBuffer switches to the buffer at index n.
|
||||
func (ui *UI) SwitchBuffer(n int) {
|
||||
func (ui *UI) SwitchBuffer(bufIndex int) {
|
||||
ui.app.QueueUpdateDraw(func() {
|
||||
if n < 0 || n >= len(ui.buffers) {
|
||||
if bufIndex < 0 || bufIndex >= len(ui.buffers) {
|
||||
return
|
||||
}
|
||||
|
||||
ui.currentBuffer = n
|
||||
ui.currentBuffer = bufIndex
|
||||
|
||||
buf := ui.buffers[n]
|
||||
buf := ui.buffers[bufIndex]
|
||||
buf.Unread = 0
|
||||
|
||||
ui.messages.Clear()
|
||||
@@ -282,7 +287,7 @@ func (ui *UI) getOrCreateBuffer(name string) *Buffer {
|
||||
}
|
||||
}
|
||||
|
||||
buf := &Buffer{Name: name}
|
||||
buf := &Buffer{Name: name, Lines: nil, Unread: 0}
|
||||
ui.buffers = append(ui.buffers, buf)
|
||||
|
||||
return buf
|
||||
|
||||
Reference in New Issue
Block a user