refactor: simplify codebase
This commit is contained in:
+17
-51
@@ -6,7 +6,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -41,14 +40,12 @@ type Client struct {
|
||||
Token string
|
||||
Streamer string
|
||||
Once bool
|
||||
Debug bool
|
||||
Out io.Writer
|
||||
Events EventSink
|
||||
DialContext func(context.Context, string, string) (net.Conn, error)
|
||||
}
|
||||
|
||||
// Run connects, authenticates, joins the configured streamer channel, and stays connected.
|
||||
func (c *Client) Run(ctx context.Context) error {
|
||||
func (c *Client) Run(ctx context.Context) (err error) {
|
||||
if c.Login == "" {
|
||||
return errors.New("missing Twitch login")
|
||||
}
|
||||
@@ -58,23 +55,23 @@ func (c *Client) Run(ctx context.Context) error {
|
||||
if c.Streamer == "" {
|
||||
return errors.New("missing streamer")
|
||||
}
|
||||
defer func() {
|
||||
if ctx.Err() != nil {
|
||||
err = nil
|
||||
}
|
||||
}()
|
||||
|
||||
addr := c.Addr
|
||||
if addr == "" {
|
||||
addr = DefaultAddr
|
||||
}
|
||||
|
||||
c.status("Connecting to %s\n", addr)
|
||||
conn, err := c.dial(ctx, addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect to Twitch IRC: %w", err)
|
||||
}
|
||||
|
||||
session := &session{
|
||||
conn: conn,
|
||||
out: c.Out,
|
||||
debug: c.Debug,
|
||||
}
|
||||
session := &session{conn: conn}
|
||||
defer session.close()
|
||||
|
||||
done := make(chan struct{})
|
||||
@@ -82,24 +79,21 @@ func (c *Client) Run(ctx context.Context) error {
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
_ = session.send("QUIT :parasocial shutting down", false)
|
||||
_ = session.send("QUIT :parasocial shutting down")
|
||||
session.close()
|
||||
case <-done:
|
||||
}
|
||||
}()
|
||||
|
||||
if err := session.send("PASS oauth:"+c.Token, true); err != nil {
|
||||
if err := session.send("PASS oauth:" + c.Token); err != nil {
|
||||
return err
|
||||
}
|
||||
c.status("Sent PASS\n")
|
||||
if err := session.send("NICK "+c.Login, false); err != nil {
|
||||
if err := session.send("NICK " + c.Login); err != nil {
|
||||
return err
|
||||
}
|
||||
c.status("Sent NICK %s\n", c.Login)
|
||||
if err := session.send("JOIN #"+c.Streamer, false); err != nil {
|
||||
if err := session.send("JOIN #" + c.Streamer); err != nil {
|
||||
return err
|
||||
}
|
||||
c.status("Sent JOIN #%s\n", c.Streamer)
|
||||
|
||||
reader := bufio.NewReader(conn)
|
||||
joined := false
|
||||
@@ -116,13 +110,10 @@ func (c *Client) Run(ctx context.Context) error {
|
||||
}
|
||||
|
||||
line = strings.TrimRight(line, "\r\n")
|
||||
session.debugIn(line)
|
||||
|
||||
if payload, ok := pingPayload(line); ok {
|
||||
if err := session.send("PONG "+payload, false); err != nil {
|
||||
if err := session.send("PONG " + payload); err != nil {
|
||||
return err
|
||||
}
|
||||
c.status("Responded to PING\n")
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -133,16 +124,14 @@ func (c *Client) Run(ctx context.Context) error {
|
||||
return fmt.Errorf("IRC join denied: %s", line)
|
||||
}
|
||||
if isWelcome(line) {
|
||||
c.status("IRC authentication accepted\n")
|
||||
continue
|
||||
}
|
||||
if isJoinConfirmation(line, c.Login, c.Streamer) {
|
||||
joinedLine := fmt.Sprintf("Joined #%s as %s", c.Streamer, c.Login)
|
||||
c.status("%s\n", joinedLine)
|
||||
c.emit(StateJoined, joinedLine)
|
||||
joined = true
|
||||
if c.Once {
|
||||
if err := session.send("QUIT :parasocial --once complete", false); err != nil {
|
||||
if err := session.send("QUIT :parasocial --once complete"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -150,41 +139,25 @@ func (c *Client) Run(ctx context.Context) error {
|
||||
continue
|
||||
}
|
||||
if joined {
|
||||
c.status("%s\n", line)
|
||||
c.emit("", line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type session struct {
|
||||
conn net.Conn
|
||||
out io.Writer
|
||||
debug bool
|
||||
mu sync.Mutex
|
||||
once sync.Once
|
||||
conn net.Conn
|
||||
mu sync.Mutex
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func (s *session) send(line string, redact bool) error {
|
||||
func (s *session) send(line string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
debugLine := line
|
||||
if redact {
|
||||
debugLine = "PASS oauth:<redacted>"
|
||||
}
|
||||
if s.debug && s.out != nil {
|
||||
fmt.Fprintf(s.out, "> %s\n", debugLine)
|
||||
}
|
||||
_, err := fmt.Fprintf(s.conn, "%s\r\n", line)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *session) debugIn(line string) {
|
||||
if s.debug && s.out != nil {
|
||||
fmt.Fprintf(s.out, "< %s\n", line)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *session) close() {
|
||||
s.once.Do(func() {
|
||||
_ = s.conn.SetDeadline(time.Now())
|
||||
@@ -200,13 +173,6 @@ func (c *Client) dial(ctx context.Context, addr string) (net.Conn, error) {
|
||||
return dialer.DialContext(ctx, "tcp", addr)
|
||||
}
|
||||
|
||||
func (c *Client) status(format string, args ...any) {
|
||||
if c.Out == nil {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(c.Out, format, args...)
|
||||
}
|
||||
|
||||
func (c *Client) emit(state ConnectionState, line string) {
|
||||
if c.Events == nil {
|
||||
return
|
||||
|
||||
@@ -64,7 +64,6 @@ func TestRunOnceSendsAuthJoinAndRespondsToPing(t *testing.T) {
|
||||
Token: "token",
|
||||
Streamer: "streamer",
|
||||
Once: true,
|
||||
Out: io.Discard,
|
||||
}
|
||||
if err := client.Run(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -122,7 +121,7 @@ func TestRunEmitsJoinAndPostJoinEvents(t *testing.T) {
|
||||
for {
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
serverErr <- err
|
||||
serverErr <- nil
|
||||
return
|
||||
}
|
||||
line = strings.TrimRight(line, "\r\n")
|
||||
@@ -133,7 +132,7 @@ func TestRunEmitsJoinAndPostJoinEvents(t *testing.T) {
|
||||
_, _ = conn.Write([]byte(":viewer!viewer@viewer.tmi.twitch.tv JOIN #streamer\r\n"))
|
||||
_, _ = conn.Write([]byte(":someone!someone@someone.tmi.twitch.tv PRIVMSG #streamer :hello\r\n"))
|
||||
_, _ = conn.Write([]byte("PING :tmi.twitch.tv\r\n"))
|
||||
case "PONG :tmi.twitch.tv":
|
||||
case "QUIT :parasocial shutting down":
|
||||
serverErr <- nil
|
||||
return
|
||||
}
|
||||
@@ -151,7 +150,6 @@ func TestRunEmitsJoinAndPostJoinEvents(t *testing.T) {
|
||||
Events: func(event Event) {
|
||||
events <- event
|
||||
},
|
||||
Out: io.Discard,
|
||||
}
|
||||
|
||||
done := make(chan error, 1)
|
||||
@@ -203,6 +201,12 @@ func TestAuthFailureReturnsError(t *testing.T) {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
reader := bufio.NewReader(conn)
|
||||
for range 3 {
|
||||
if _, err := reader.ReadString('\n'); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
_, _ = conn.Write([]byte(":tmi.twitch.tv NOTICE * :Login authentication failed\r\n"))
|
||||
}()
|
||||
|
||||
@@ -211,7 +215,6 @@ func TestAuthFailureReturnsError(t *testing.T) {
|
||||
Login: "viewer",
|
||||
Token: "bad-token",
|
||||
Streamer: "streamer",
|
||||
Out: io.Discard,
|
||||
}
|
||||
err = client.Run(context.Background())
|
||||
if err == nil {
|
||||
|
||||
+3
-22
@@ -2,22 +2,13 @@ package irc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Target identifies one streamer channel that should have an active IRC connection.
|
||||
type Target struct {
|
||||
Login string
|
||||
}
|
||||
|
||||
// Manager reconciles the active IRC connections against the desired watched set.
|
||||
type Manager struct {
|
||||
Addr string
|
||||
Debug bool
|
||||
Out io.Writer
|
||||
Events EventSink
|
||||
DialContext func(context.Context, string, string) (net.Conn, error)
|
||||
RunClient func(context.Context, *Client) error
|
||||
@@ -32,7 +23,7 @@ type managedClient struct {
|
||||
}
|
||||
|
||||
// Sync applies the desired IRC target set using the provided viewer credentials.
|
||||
func (m *Manager) Sync(ctx context.Context, viewerLogin, accessToken string, targets []Target) {
|
||||
func (m *Manager) Sync(ctx context.Context, viewerLogin, accessToken string, targets []string) {
|
||||
orderedTargets, desired := normalizeTargets(targets)
|
||||
|
||||
m.mu.Lock()
|
||||
@@ -82,12 +73,9 @@ func (m *Manager) runClient(ctx context.Context, viewerLogin, accessToken, strea
|
||||
m.emit(Event{Streamer: streamer, State: StatePending})
|
||||
|
||||
_ = run(ctx, &Client{
|
||||
Addr: m.addr(),
|
||||
Login: viewerLogin,
|
||||
Token: accessToken,
|
||||
Streamer: streamer,
|
||||
Debug: m.Debug,
|
||||
Out: m.Out,
|
||||
Events: m.Events,
|
||||
DialContext: m.DialContext,
|
||||
})
|
||||
@@ -111,13 +99,6 @@ func (m *Manager) Close() {
|
||||
m.wg.Wait()
|
||||
}
|
||||
|
||||
func (m *Manager) addr() string {
|
||||
if m.Addr != "" {
|
||||
return m.Addr
|
||||
}
|
||||
return DefaultAddr
|
||||
}
|
||||
|
||||
func (m *Manager) emit(event Event) {
|
||||
if m.Events == nil {
|
||||
return
|
||||
@@ -125,11 +106,11 @@ func (m *Manager) emit(event Event) {
|
||||
m.Events(event)
|
||||
}
|
||||
|
||||
func normalizeTargets(targets []Target) ([]string, map[string]struct{}) {
|
||||
func normalizeTargets(targets []string) ([]string, map[string]struct{}) {
|
||||
ordered := make([]string, 0, 2)
|
||||
normalized := make(map[string]struct{}, len(targets))
|
||||
for _, target := range targets {
|
||||
login := strings.ToLower(strings.TrimSpace(target.Login))
|
||||
login := strings.ToLower(strings.TrimSpace(target))
|
||||
if login == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -25,11 +25,7 @@ func TestManagerSyncStartsAtMostTwoTargets(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
manager.Sync(ctx, "viewer", "token", []Target{
|
||||
{Login: "alpha"},
|
||||
{Login: "beta"},
|
||||
{Login: "gamma"},
|
||||
})
|
||||
manager.Sync(ctx, "viewer", "token", []string{"alpha", "beta", "gamma"})
|
||||
|
||||
assertStartSet(t, started, "alpha", "beta")
|
||||
assertNoStart(t, started)
|
||||
@@ -50,10 +46,10 @@ func TestManagerSyncKeepsExistingTargetsConnected(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
manager.Sync(ctx, "viewer", "token", []Target{{Login: "alpha"}})
|
||||
manager.Sync(ctx, "viewer", "token", []string{"alpha"})
|
||||
assertStartSet(t, started, "alpha")
|
||||
|
||||
manager.Sync(ctx, "viewer", "token", []Target{{Login: "alpha"}})
|
||||
manager.Sync(ctx, "viewer", "token", []string{"alpha"})
|
||||
assertNoStart(t, started)
|
||||
}
|
||||
|
||||
@@ -74,10 +70,10 @@ func TestManagerSyncCancelsRemovedTargets(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
manager.Sync(ctx, "viewer", "token", []Target{{Login: "alpha"}, {Login: "beta"}})
|
||||
manager.Sync(ctx, "viewer", "token", []string{"alpha", "beta"})
|
||||
assertStartSet(t, started, "alpha", "beta")
|
||||
|
||||
manager.Sync(ctx, "viewer", "token", []Target{{Login: "beta"}, {Login: "gamma"}})
|
||||
manager.Sync(ctx, "viewer", "token", []string{"beta", "gamma"})
|
||||
assertStartSet(t, started, "gamma")
|
||||
assertCancels(t, canceled, "alpha")
|
||||
assertNoStart(t, started)
|
||||
@@ -109,7 +105,7 @@ func TestManagerSyncRestartsTargetAfterFailure(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
manager.Sync(ctx, "viewer", "token", []Target{{Login: "alpha"}})
|
||||
manager.Sync(ctx, "viewer", "token", []string{"alpha"})
|
||||
assertStartSet(t, started, "alpha")
|
||||
|
||||
waitFor(t, func() bool {
|
||||
@@ -119,7 +115,7 @@ func TestManagerSyncRestartsTargetAfterFailure(t *testing.T) {
|
||||
return !ok
|
||||
})
|
||||
|
||||
manager.Sync(ctx, "viewer", "token", []Target{{Login: "alpha"}})
|
||||
manager.Sync(ctx, "viewer", "token", []string{"alpha"})
|
||||
assertStartSet(t, started, "alpha")
|
||||
}
|
||||
|
||||
@@ -138,7 +134,7 @@ func TestManagerEmitsPendingAndDisconnectedEvents(t *testing.T) {
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
manager.Sync(ctx, "viewer", "token", []Target{{Login: "alpha"}})
|
||||
manager.Sync(ctx, "viewer", "token", []string{"alpha"})
|
||||
cancel()
|
||||
manager.Close()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user