ci: add golangci-lint

This commit is contained in:
ayn2op 2026-10-15 20:50:49 -04:00
parent 9320e20983
commit 39222f1073
17 changed files with 104 additions and 63 deletions

View file

@ -2,6 +2,17 @@ name: ci
on: [push, pull_request] on: [push, pull_request]
jobs: jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
- uses: golangci/golangci-lint-action@v9
with:
version: latest
build: build:
strategy: strategy:
fail-fast: false fail-fast: false

View file

@ -35,7 +35,7 @@ func Run() error {
if err != nil { if err != nil {
return fmt.Errorf("failed to load logger: %w", err) return fmt.Errorf("failed to load logger: %w", err)
} }
defer logFile.Close() defer logFile.Close() //nolint:errcheck
cfg, err := config.Load(*configPath) cfg, err := config.Load(*configPath)
if err != nil { if err != nil {

View file

@ -291,7 +291,7 @@ func Load(path string) (*Config, error) {
case err != nil: case err != nil:
return nil, fmt.Errorf("failed to open config file: %w", err) return nil, fmt.Errorf("failed to open config file: %w", err)
default: default:
defer file.Close() defer file.Close() //nolint:errcheck
if _, err := toml.NewDecoder(file).Decode(&cfg); err != nil { if _, err := toml.NewDecoder(file).Decode(&cfg); err != nil {
return nil, fmt.Errorf("failed to decode config: %w", err) return nil, fmt.Errorf("failed to decode config: %w", err)
} }

View file

@ -9,6 +9,7 @@ import (
"github.com/ayn2op/arikawa/v3/discord" "github.com/ayn2op/arikawa/v3/discord"
"github.com/ayn2op/tview" "github.com/ayn2op/tview"
"github.com/gdamore/tcell/v3" "github.com/gdamore/tcell/v3"
"github.com/gdamore/tcell/v3/color"
) )
var errInvalidType = errors.New("invalid type") var errInvalidType = errors.New("invalid type")
@ -89,7 +90,7 @@ func (sw *StyleWrapper) parseAttr(s string) {
} }
// An unknown attribute parses to no attribute. // An unknown attribute parses to no attribute.
attr, _ := attributes.parse(s) attr, _ := attributes.parse(s)
sw.Style = sw.Attributes(sw.GetAttributes() | attr) sw.Style = sw.Attributes(sw.GetAttributes() | attr) //nolint:staticcheck
} }
// MarshalTOML writes the style as the inline table UnmarshalTOML reads. // MarshalTOML writes the style as the inline table UnmarshalTOML reads.
@ -254,14 +255,14 @@ func defaultTheme() ThemeConfig {
dim = StyleWrapper{plain.Dim(true)} dim = StyleWrapper{plain.Dim(true)}
bold = plain.Bold(true) bold = plain.Bold(true)
italic = plain.Italic(true) italic = plain.Italic(true)
blue = plain.Foreground(tcell.ColorBlue) blue = plain.Foreground(color.Blue)
focus = StyleThemeConfig{dim, StyleWrapper{bold.Foreground(tcell.ColorGreen)}} focus = StyleThemeConfig{dim, StyleWrapper{bold.Foreground(color.Green)}}
tree = TreeThemeConfig{ tree = TreeThemeConfig{
GraphicsColor: "default", GraphicsColor: "default",
OnlineStyle: StyleWrapper{plain.Foreground(tcell.ColorGreen)}, OnlineStyle: StyleWrapper{plain.Foreground(color.Green)},
IdleStyle: StyleWrapper{plain.Foreground(tcell.ColorYellow)}, IdleStyle: StyleWrapper{plain.Foreground(color.Yellow)},
DNDStyle: StyleWrapper{plain.Foreground(tcell.ColorRed)}, DNDStyle: StyleWrapper{plain.Foreground(color.Red)},
OfflineStyle: StyleWrapper{plain.Foreground(tcell.ColorGray)}, OfflineStyle: StyleWrapper{plain.Foreground(color.Gray)},
} }
) )
return ThemeConfig{ return ThemeConfig{
@ -270,16 +271,16 @@ func defaultTheme() ThemeConfig {
Border: focus, Border: focus,
GuildsTree: GuildsTreeThemeConfig{ GuildsTree: GuildsTreeThemeConfig{
TreeThemeConfig: tree, TreeThemeConfig: tree,
SpeakingStyle: StyleWrapper{plain.Foreground(tcell.ColorGreen)}, SpeakingStyle: StyleWrapper{plain.Foreground(color.Green)},
VoiceLimitStyle: StyleWrapper{plain.Foreground(tcell.ColorDodgerBlue)}, VoiceLimitStyle: StyleWrapper{plain.Foreground(color.DodgerBlue)},
}, },
MembersTree: tree, MembersTree: tree,
ScrollBar: ScrollBarThemeConfig{TrackStyle: dim}, ScrollBar: ScrollBarThemeConfig{TrackStyle: dim},
MessagesList: MessagesListThemeConfig{ MessagesList: MessagesListThemeConfig{
MentionStyle: StyleWrapper{blue.Bold(true)}, MentionStyle: StyleWrapper{blue.Bold(true)},
EmojiStyle: StyleWrapper{plain.Foreground(tcell.ColorGreen)}, EmojiStyle: StyleWrapper{plain.Foreground(color.Green)},
URLStyle: StyleWrapper{blue}, URLStyle: StyleWrapper{blue},
AttachmentStyle: StyleWrapper{plain.Foreground(tcell.ColorYellow)}, AttachmentStyle: StyleWrapper{plain.Foreground(color.Yellow)},
ReactionStyle: dim, ReactionStyle: dim,
OwnReactionStyle: StyleWrapper{bold}, OwnReactionStyle: StyleWrapper{bold},
SelectedMessageStyle: StyleWrapper{plain.Reverse(true)}, SelectedMessageStyle: StyleWrapper{plain.Reverse(true)},

View file

@ -1,3 +1,5 @@
//go:build no_spoof_tls_fingerprint
package http package http
import ( import (

View file

@ -361,7 +361,7 @@ func (m Model) processText(channel *discord.Channel, src []byte) string {
canMention = true canMention = true
) )
ast.Walk(md.Parse(src), func(node ast.Node, enter bool) (ast.WalkStatus, error) { _ = ast.Walk(md.Parse(src), func(node ast.Node, enter bool) (ast.WalkStatus, error) {
switch node := node.(type) { switch node := node.(type) {
case *ast.CodeBlock, *ast.FencedCodeBlock: case *ast.CodeBlock, *ast.FencedCodeBlock:
canMention = !enter canMention = !enter
@ -403,7 +403,7 @@ func (m Model) expandMentions(channel *discord.Channel, src []byte) []byte {
} }
return output return output
} }
state.MemberStore.Each(channel.GuildID, func(m *discord.Member) bool { _ = state.MemberStore.Each(channel.GuildID, func(m *discord.Member) bool {
if strings.EqualFold(m.User.Username, name) { if strings.EqualFold(m.User.Username, name) {
if channelHasUser(state, channel.ID, m.User.ID) { if channelHasUser(state, channel.ID, m.User.ID) {
output = []byte(m.User.ID.Mention()) output = []byte(m.User.ID.Mention())

View file

@ -40,12 +40,12 @@ func TestModelSetChannel(t *testing.T) {
channel := discord.Channel{ID: 10, GuildID: 1} channel := discord.Channel{ID: 10, GuildID: 1}
thread := discord.Channel{ID: 11, GuildID: 1, ParentID: 10, Type: discord.GuildPublicThread} thread := discord.Channel{ID: 11, GuildID: 1, ParentID: 10, Type: discord.GuildPublicThread}
for _, err := range []error{ for _, err := range []error{
state.Cabinet.MyselfSet(discord.User{ID: 3}, false), state.MyselfSet(discord.User{ID: 3}, false),
state.Cabinet.GuildSet(&discord.Guild{ID: 1}, false), state.GuildSet(&discord.Guild{ID: 1}, false),
state.Cabinet.MemberSet(1, &discord.Member{User: discord.User{ID: 3}}, false), state.MemberSet(1, &discord.Member{User: discord.User{ID: 3}}, false),
state.Cabinet.RoleSet(1, &discord.Role{ID: 1, Permissions: discord.PermissionSendMessagesInThreads}, false), state.RoleSet(1, &discord.Role{ID: 1, Permissions: discord.PermissionSendMessagesInThreads}, false),
state.Cabinet.ChannelSet(&channel, false), state.ChannelSet(&channel, false),
state.Cabinet.ChannelSet(&thread, false), state.ChannelSet(&thread, false),
} { } {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -168,13 +168,13 @@ func TestModelSearchMember(t *testing.T) {
func TestModelTabSuggest(t *testing.T) { func TestModelTabSuggest(t *testing.T) {
c, _ := newTestModel(t) c, _ := newTestModel(t)
if err := c.state.Cabinet.MyselfSet(discord.User{ID: 1, Username: "1"}, false); err != nil { if err := c.state.MyselfSet(discord.User{ID: 1, Username: "1"}, false); err != nil {
t.Fatal(err) t.Fatal(err)
} }
channel := discord.Channel{ID: 1, Type: discord.DirectMessage} channel := discord.Channel{ID: 1, Type: discord.DirectMessage}
for id, author := range []discord.UserID{2, 3, 2, 1} { for id, author := range []discord.UserID{2, 3, 2, 1} {
message := discord.Message{ID: discord.MessageID(id + 1), ChannelID: 1, Author: discord.User{ID: author, Username: author.String()}} message := discord.Message{ID: discord.MessageID(id + 1), ChannelID: 1, Author: discord.User{ID: author, Username: author.String()}}
if err := c.state.Cabinet.MessageSet(&message, false); err != nil { if err := c.state.MessageSet(&message, false); err != nil {
t.Fatal(err) t.Fatal(err)
} }
} }

View file

@ -97,7 +97,9 @@ func closeFiles(files []sendpart.File) tview.Cmd {
return func() tview.Msg { return func() tview.Msg {
for _, file := range files { for _, file := range files {
if closer, ok := file.Reader.(io.Closer); ok { if closer, ok := file.Reader.(io.Closer); ok {
closer.Close() if err := closer.Close(); err != nil {
slog.Error("failed to close file", "err", err)
}
} }
} }
return nil return nil
@ -121,7 +123,9 @@ func (m *Model) sendTyping() tview.Cmd {
} }
channelID := selectedChannel.ID channelID := selectedChannel.ID
return func() tview.Msg { return func() tview.Msg {
m.state.Typing(channelID) if err := m.state.Typing(channelID); err != nil {
slog.Error("failed to send typing indicator", "err", err)
}
return nil return nil
} }
} }
@ -327,7 +331,11 @@ func (m Model) openEditor() tview.Cmd {
return nil return nil
} }
name := file.Name() name := file.Name()
defer os.Remove(name) defer func() {
if err := os.Remove(name); err != nil {
slog.Error("failed to remove tmp file", "err", err)
}
}()
_, _ = file.WriteString(text) _, _ = file.WriteString(text)
_ = file.Close() _ = file.Close()

View file

@ -250,7 +250,7 @@ func (m Model) createChannelNode(parent *tree.Node, channel discord.Channel) {
// showVoiceMembers lists who is in each voice channel of the guild under it. // showVoiceMembers lists who is in each voice channel of the guild under it.
func (m *Model) showVoiceMembers(guildID discord.GuildID) { func (m *Model) showVoiceMembers(guildID discord.GuildID) {
states, _ := m.state.Cabinet.VoiceStates(guildID) states, _ := m.state.VoiceStates(guildID)
slices.SortFunc(states, func(a, b discord.VoiceState) int { return cmp.Compare(a.UserID, b.UserID) }) slices.SortFunc(states, func(a, b discord.VoiceState) int { return cmp.Compare(a.UserID, b.UserID) })
channels, _ := m.state.Cabinet.Channels(guildID) channels, _ := m.state.Cabinet.Channels(guildID)
@ -340,7 +340,7 @@ func (m Model) voiceFooter() string {
name := ui.ChannelToString(*channel, m.cfg.Icons, m.state) name := ui.ChannelToString(*channel, m.cfg.Icons, m.state)
if me, _ := m.state.Cabinet.Me(); me != nil { if me, _ := m.state.Cabinet.Me(); me != nil {
if state, _ := m.state.Cabinet.VoiceState(channel.GuildID, me.ID); state != nil { if state, _ := m.state.VoiceState(channel.GuildID, me.ID); state != nil {
name += m.voiceMarkers(*state) name += m.voiceMarkers(*state)
} }
} }

View file

@ -29,16 +29,22 @@ func newTestModel(t *testing.T) Model {
func setGuildMembers(t *testing.T, m Model, items string) *discord.Channel { func setGuildMembers(t *testing.T, m Model, items string) *discord.Channel {
t.Helper() t.Helper()
channel := &discord.Channel{ID: channelID, GuildID: guildID, Type: discord.GuildText} channel := &discord.Channel{ID: channelID, GuildID: guildID, Type: discord.GuildText}
m.state.Cabinet.ChannelSet(channel, false) if err := m.state.ChannelSet(channel, false); err != nil {
m.state.Cabinet.RoleSet(guildID, &discord.Role{ID: guildID, Permissions: discord.PermissionViewChannel}, false) t.Fatal(err)
m.state.Cabinet.RoleSet(guildID, &discord.Role{ID: 3, Name: "Mods"}, false) }
if err := m.state.RoleSet(guildID, &discord.Role{ID: guildID, Permissions: discord.PermissionViewChannel}, false); err != nil {
t.Fatal(err)
}
if err := m.state.RoleSet(guildID, &discord.Role{ID: 3, Name: "Mods"}, false); err != nil {
t.Fatal(err)
}
event := &gateway.GuildMemberListUpdateEvent{ID: "everyone", GuildID: guildID} event := &gateway.GuildMemberListUpdateEvent{ID: "everyone", GuildID: guildID}
event.Ops = []gateway.GuildMemberListOp{{Op: "SYNC", Range: [2]int{0, 99}}} event.Ops = []gateway.GuildMemberListOp{{Op: "SYNC", Range: [2]int{0, 99}}}
if err := json.Unmarshal([]byte(items), &event.Ops[0].Items); err != nil { if err := json.Unmarshal([]byte(items), &event.Ops[0].Items); err != nil {
t.Fatal(err) t.Fatal(err)
} }
m.state.State.Handler.Call(event) m.state.State.Call(event)
return channel return channel
} }
@ -106,8 +112,12 @@ func TestModelRebuild(t *testing.T) {
t.Run("group dm recipients", func(t *testing.T) { t.Run("group dm recipients", func(t *testing.T) {
m := newTestModel(t) m := newTestModel(t)
m.state.Cabinet.MyselfSet(discord.User{ID: 13, Username: "me"}, false) if err := m.state.MyselfSet(discord.User{ID: 13, Username: "me"}, false); err != nil {
m.state.Cabinet.PresenceSet(discord.NullGuildID, &discord.Presence{User: discord.User{ID: 11}, Status: discord.OnlineStatus}, false) t.Fatal(err)
}
if err := m.state.PresenceSet(discord.NullGuildID, &discord.Presence{User: discord.User{ID: 11}, Status: discord.OnlineStatus}, false); err != nil {
t.Fatal(err)
}
m.channel = &discord.Channel{Type: discord.GroupDM, DMRecipients: []discord.User{ m.channel = &discord.Channel{Type: discord.GroupDM, DMRecipients: []discord.User{
{ID: 10, Username: "a"}, {ID: 10, Username: "a"},
{ID: 11, Username: "c"}, {ID: 11, Username: "c"},

View file

@ -1,6 +1,7 @@
package messageslist package messageslist
import ( import (
"errors"
"fmt" "fmt"
"io" "io"
"log/slog" "log/slog"
@ -264,7 +265,7 @@ func (m Model) buildItem(index int) tview.Widget {
item := &m.items[index] item := &m.items[index]
if item.separator { if item.separator {
date := m.items[index+1].message.Timestamp.Time().In(time.Local).Format(m.cfg.DateSeparator.Format) date := m.items[index+1].message.Timestamp.Time().In(time.Local).Format(m.cfg.DateSeparator.Format)
return dateSeparator{date: date, fill: m.cfg.DateSeparator.Character, style: m.cfg.Theme.MessagesList.MessageStyle.Style.Dim(true)} return dateSeparator{date: date, fill: m.cfg.DateSeparator.Character, style: m.cfg.Theme.MessagesList.MessageStyle.Dim(true)}
} }
message := &item.message message := &item.message
@ -1234,7 +1235,7 @@ func extractURLs(content string) []string {
// src must be the byte slice the node was parsed from (AutoLink resolves its URL against it). // src must be the byte slice the node was parsed from (AutoLink resolves its URL against it).
func urlsFromAST(node ast.Node, src []byte) []string { func urlsFromAST(node ast.Node, src []byte) []string {
var urls []string var urls []string
ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) { _ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
if entering { if entering {
switch n := n.(type) { switch n := n.(type) {
case *ast.AutoLink: case *ast.AutoLink:
@ -1297,7 +1298,7 @@ func (m Model) get(url string) (*http.Response, error) {
if err != nil || resp.StatusCode == http.StatusOK { if err != nil || resp.StatusCode == http.StatusOK {
return resp, err return resp, err
} }
resp.Body.Close() resp.Body.Close() //nolint:errcheck
var links struct { var links struct {
URLs []struct { URLs []struct {
@ -1313,7 +1314,7 @@ func (m Model) get(url string) (*http.Response, error) {
if resp, err = http.Get(links.URLs[0].Refreshed); err != nil || resp.StatusCode == http.StatusOK { if resp, err = http.Get(links.URLs[0].Refreshed); err != nil || resp.StatusCode == http.StatusOK {
return resp, err return resp, err
} }
resp.Body.Close() resp.Body.Close() //nolint:errcheck
} }
return nil, fmt.Errorf("unexpected HTTP status: %s", resp.Status) return nil, fmt.Errorf("unexpected HTTP status: %s", resp.Status)
@ -1324,14 +1325,13 @@ func (m Model) downloadAttachment(attachment discord.Attachment, destination str
if err != nil { if err != nil {
return err return err
} }
defer resp.Body.Close() defer resp.Body.Close() //nolint:errcheck
file, err := os.Create(destination) file, err := os.Create(destination)
if err != nil { if err != nil {
return err return err
} }
defer file.Close()
_, err = io.Copy(file, resp.Body) _, err = io.Copy(file, resp.Body)
return err return errors.Join(err, file.Close())
} }
// SelectLastOwn selects the last message in the selected channel that the user can edit, and returns it. // SelectLastOwn selects the last message in the selected channel that the user can edit, and returns it.

View file

@ -30,7 +30,7 @@ func testConfig(t *testing.T) *config.Config {
func newTestModel(t *testing.T, cfg *config.Config) *Model { func newTestModel(t *testing.T, cfg *config.Config) *Model {
t.Helper() t.Helper()
ml := NewModel(cfg, ningen.FromState(state.New(""))) ml := NewModel(cfg, ningen.FromState(state.New("")))
if err := ml.state.Cabinet.MyselfSet(discord.User{ID: 1}, false); err != nil { if err := ml.state.MyselfSet(discord.User{ID: 1}, false); err != nil {
t.Fatal(err) t.Fatal(err)
} }
ml.channel = &discord.Channel{ID: 1} ml.channel = &discord.Channel{ID: 1}

View file

@ -183,7 +183,7 @@ func (m Model) playVoiceMessage(attachment discord.Attachment) error {
if err != nil { if err != nil {
return err return err
} }
defer resp.Body.Close() defer resp.Body.Close() //nolint:errcheck
// It is read whole, for playing it not to wait for the network. // It is read whole, for playing it not to wait for the network.
audio, err := io.ReadAll(resp.Body) audio, err := io.ReadAll(resp.Body)
@ -305,13 +305,13 @@ func (m Model) openDownloadedAttachment(attachment discord.Attachment) tview.Cmd
if err != nil { if err != nil {
return ui.ErrorDialog("create attachment file", err) return ui.ErrorDialog("create attachment file", err)
} }
defer file.Close()
path := file.Name() path := file.Name()
if err := file.Close(); err != nil {
return ui.ErrorDialog("create attachment file", err)
}
if err := m.downloadAttachment(attachment, path); err != nil { if err := m.downloadAttachment(attachment, path); err != nil {
os.Remove(path) return ui.ErrorDialog("download attachment", errors.Join(err, os.Remove(path)))
return ui.ErrorDialog("download attachment", err)
} else if err := open.Start(path); err != nil { } else if err := open.Start(path); err != nil {
return ui.ErrorDialog("open attachment file", err) return ui.ErrorDialog("open attachment file", err)
} }

View file

@ -153,7 +153,7 @@ func (m Model) downloadPreview(proxy discord.URL, bounds image.Rectangle) (image
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer resp.Body.Close() defer resp.Body.Close() //nolint:errcheck
img, _, err := image.Decode(resp.Body) img, _, err := image.Decode(resp.Body)
return img, err return img, err
} }

View file

@ -40,7 +40,7 @@ func serveGateway(t *testing.T, acks int) http.HandlerFunc {
t.Error(err) t.Error(err)
return return
} }
defer conn.Close() defer conn.Close() //nolint:errcheck
if err := conn.WriteJSON(map[string]any{"op": "hello", "heartbeat_interval": 1}); err != nil { if err := conn.WriteJSON(map[string]any{"op": "hello", "heartbeat_interval": 1}); err != nil {
return return
@ -85,7 +85,7 @@ func TestModelGateway(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
t.Cleanup(func() { conn.Close() }) t.Cleanup(func() { conn.Close() }) //nolint:errcheck
screen, err := tcell.NewTerminfoScreenFromTty(vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24})) screen, err := tcell.NewTerminfoScreenFromTty(vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24}))
if err != nil { if err != nil {
@ -117,7 +117,7 @@ func TestCloseConn(t *testing.T) {
t.Error(err) t.Error(err)
return return
} }
defer conn.Close() defer conn.Close() //nolint:errcheck
_, _, _ = conn.ReadMessage() _, _, _ = conn.ReadMessage()
})) }))
t.Cleanup(server.Close) t.Cleanup(server.Close)

View file

@ -5,6 +5,7 @@ package voice
import ( import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"errors"
"io" "io"
"sync" "sync"
"time" "time"
@ -130,7 +131,9 @@ func (p *Player) seek(position time.Duration) error {
} }
// Nothing is left to play if position is past the end. // Nothing is left to play if position is past the end.
io.CopyN(io.Discard, decoder, (sample-first)*int64(decoder.Info().Channels)*2) if _, err := io.CopyN(io.Discard, decoder, (sample-first)*int64(decoder.Info().Channels)*2); err != nil && !errors.Is(err, io.EOF) {
return err
}
p.decoder, p.samples = decoder, sample p.decoder, p.samples = decoder, sample
return nil return nil

View file

@ -189,16 +189,18 @@ func (s *Session) setSpeaking(userID discord.UserID, speaking bool) {
} }
// setDevice starts device, which then calls data, if on, and stops it otherwise. // setDevice starts device, which then calls data, if on, and stops it otherwise.
func setDevice(device **malgo.Device, on bool, kind malgo.DeviceType, data func(output, input []byte)) (err error) { func setDevice(device **malgo.Device, on bool, kind malgo.DeviceType, data func(output, input []byte)) error {
switch { switch {
case on && *device == nil: case on && *device == nil:
var err error
*device, err = startDevice(kind, channels, data) *device, err = startDevice(kind, channels, data)
return err
case !on && *device != nil: case !on && *device != nil:
(*device).Uninit() (*device).Uninit()
*device = nil *device = nil
} }
return err return nil
} }
// setDevices captures what the user says unless mute, and plays what is said unless deafen. // setDevices captures what the user says unless mute, and plays what is said unless deafen.
@ -217,12 +219,14 @@ func (s *Session) setDevices(mute, deafen bool) error {
// left stops the devices, in no voice channel. // left stops the devices, in no voice channel.
func (s *Session) left() { func (s *Session) left() {
s.setDevices(true, true) if err := s.setDevices(true, true); err != nil {
slog.Error("failed to stop voice devices", "err", err)
}
s.setStatus(Status{}) s.setStatus(Status{})
} }
// Join joins the voice channel, muted, leaving the one that the user is in. // Join joins the voice channel, muted, leaving the one that the user is in.
func (s *Session) Join(channelID discord.ChannelID) (err error) { func (s *Session) Join(channelID discord.ChannelID) error {
if !s.mu.TryLock() { if !s.mu.TryLock() {
return nil return nil
} }
@ -230,18 +234,22 @@ func (s *Session) Join(channelID discord.ChannelID) (err error) {
s.setStatus(Status{ChannelID: channelID}) s.setStatus(Status{ChannelID: channelID})
defer func() { if err := s.join(channelID); err != nil {
// Discord may have the user in the voice channel regardless, and joining it again would then do nothing. // Discord may have the user in the voice channel regardless, and joining it again would then do nothing.
if err != nil { return errors.Join(err, s.leave())
s.leave() }
}
}()
s.setStatus(Status{ChannelID: channelID, Joined: true})
return nil
}
func (s *Session) join(channelID discord.ChannelID) error {
ctx, cancel := context.WithTimeout(context.Background(), timeout) ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel() defer cancel()
session := s.session.Load() session := s.session.Load()
if session == nil { if session == nil {
var err error
if session, err = voice.NewSession(s.state, s.state.Handler); err != nil { if session, err = voice.NewSession(s.state, s.state.Handler); err != nil {
return err return err
} }
@ -267,8 +275,6 @@ func (s *Session) Join(channelID discord.ChannelID) (err error) {
return err return err
} }
s.setStatus(Status{ChannelID: channelID, Joined: true})
return nil return nil
} }