ci: add golangci-lint
This commit is contained in:
parent
9320e20983
commit
39222f1073
17 changed files with 104 additions and 63 deletions
11
.github/workflows/ci.yml
vendored
11
.github/workflows/ci.yml
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)},
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
//go:build no_spoof_tls_fingerprint
|
||||||
|
|
||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"},
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue