diff --git a/internal/db/queries.go b/internal/db/queries.go index 9029337..d954571 100644 --- a/internal/db/queries.go +++ b/internal/db/queries.go @@ -2165,6 +2165,52 @@ func (database *Database) SetChannelSecret( return nil } +// --- No External Messages (+n) --- + +// IsChannelNoExternal checks if a channel has +n mode. +func (database *Database) IsChannelNoExternal( + ctx context.Context, + channelID int64, +) (bool, error) { + var isNoExternal int + + err := database.conn.QueryRowContext(ctx, + `SELECT is_no_external FROM channels + WHERE id = ?`, + channelID, + ).Scan(&isNoExternal) + if err != nil { + return false, fmt.Errorf( + "check no external: %w", err, + ) + } + + return isNoExternal != 0, nil +} + +// SetChannelNoExternal sets or unsets +n mode. +func (database *Database) SetChannelNoExternal( + ctx context.Context, + channelID int64, + noExternal bool, +) error { + val := 0 + if noExternal { + val = 1 + } + + _, err := database.conn.ExecContext(ctx, + `UPDATE channels + SET is_no_external = ?, updated_at = ? + WHERE id = ?`, + val, time.Now(), channelID) + if err != nil { + return fmt.Errorf("set no external: %w", err) + } + + return nil +} + // ListAllChannelsWithCountsFiltered returns all channels // with member counts, excluding secret channels that // the given session is not a member of. diff --git a/internal/db/schema/001_initial.sql b/internal/db/schema/001_initial.sql index a29bdaa..e53c48b 100644 --- a/internal/db/schema/001_initial.sql +++ b/internal/db/schema/001_initial.sql @@ -44,6 +44,7 @@ CREATE TABLE IF NOT EXISTS channels ( is_topic_locked INTEGER NOT NULL DEFAULT 1, is_invite_only INTEGER NOT NULL DEFAULT 0, is_secret INTEGER NOT NULL DEFAULT 0, + is_no_external INTEGER NOT NULL DEFAULT 1, channel_key TEXT NOT NULL DEFAULT '', user_limit INTEGER NOT NULL DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, diff --git a/internal/handlers/api.go b/internal/handlers/api.go index 22825af..3989583 100644 --- a/internal/handlers/api.go +++ b/internal/handlers/api.go @@ -2016,62 +2016,14 @@ func (hdlr *Handlers) handleChannelMode( } // buildChannelModeString constructs the current mode -// string for a channel, including +n (always on), +t, +m, -// +i, +s, +k, +l, and +H with their parameters. +// string for a channel by delegating to the service +// layer's QueryChannelMode, which returns the complete +// mode string including all flags and parameters. func (hdlr *Handlers) buildChannelModeString( ctx context.Context, chID int64, ) string { - modes := "+n" - - isInviteOnly, ioErr := hdlr.params.Database. - IsChannelInviteOnly(ctx, chID) - if ioErr == nil && isInviteOnly { - modes += "i" - } - - isModerated, modErr := hdlr.params.Database. - IsChannelModerated(ctx, chID) - if modErr == nil && isModerated { - modes += "m" - } - - isSecret, secErr := hdlr.params.Database. - IsChannelSecret(ctx, chID) - if secErr == nil && isSecret { - modes += "s" - } - - isTopicLocked, tlErr := hdlr.params.Database. - IsChannelTopicLocked(ctx, chID) - if tlErr == nil && isTopicLocked { - modes += "t" - } - - var modeParams string - - key, keyErr := hdlr.params.Database. - GetChannelKey(ctx, chID) - if keyErr == nil && key != "" { - modes += "k" - modeParams += " " + key - } - - limit, limErr := hdlr.params.Database. - GetChannelUserLimit(ctx, chID) - if limErr == nil && limit > 0 { - modes += "l" - modeParams += " " + strconv.Itoa(limit) - } - - bits, bitsErr := hdlr.params.Database. - GetChannelHashcashBits(ctx, chID) - if bitsErr == nil && bits > 0 { - modes += "H" - modeParams += " " + strconv.Itoa(bits) - } - - return modes + modeParams + return hdlr.svc.QueryChannelMode(ctx, chID) } // queryChannelMode sends RPL_CHANNELMODEIS and diff --git a/internal/ircserver/commands.go b/internal/ircserver/commands.go index 297ea4f..03deaa6 100644 --- a/internal/ircserver/commands.go +++ b/internal/ircserver/commands.go @@ -490,6 +490,124 @@ func (c *Conn) handleChannelMode( ) } +// modeResult holds the delta strings produced by a +// single mode-char application. +type modeResult struct { + applied string + appliedArgs string + consumed int + skip bool +} + +// applyHashcashMode handles +H/-H (hashcash difficulty). +func (c *Conn) applyHashcashMode( + ctx context.Context, + chID int64, + adding bool, + args []string, + argIdx int, +) modeResult { + if !adding { + _ = c.database.SetChannelHashcashBits( + ctx, chID, 0, + ) + + return modeResult{ + applied: "-H", + appliedArgs: "", + consumed: 0, + skip: false, + } + } + + if argIdx >= len(args) { + return modeResult{ + applied: "", + appliedArgs: "", + consumed: 0, + skip: true, + } + } + + bitsStr := args[argIdx] + + bits, parseErr := strconv.Atoi(bitsStr) + if parseErr != nil || + bits < 1 || bits > maxHashcashBits { + c.sendNumeric( + irc.ErrUnknownMode, "H", + "is unknown mode char to me", + ) + + return modeResult{ + applied: "", + appliedArgs: "", + consumed: 1, + skip: true, + } + } + + _ = c.database.SetChannelHashcashBits( + ctx, chID, bits, + ) + + return modeResult{ + applied: "+H", + appliedArgs: " " + bitsStr, + consumed: 1, + skip: false, + } +} + +// applyMemberMode handles +o/-o and +v/-v. +func (c *Conn) applyMemberMode( + ctx context.Context, + chID int64, + channel string, + modeChar rune, + adding bool, + args []string, + argIdx int, +) modeResult { + if argIdx >= len(args) { + return modeResult{ + applied: "", + appliedArgs: "", + consumed: 0, + skip: true, + } + } + + targetNick := args[argIdx] + + err := c.svc.ApplyMemberMode( + ctx, chID, channel, + targetNick, modeChar, adding, + ) + if err != nil { + c.sendIRCError(err) + + return modeResult{ + applied: "", + appliedArgs: "", + consumed: 1, + skip: true, + } + } + + prefix := "+" + if !adding { + prefix = "-" + } + + return modeResult{ + applied: prefix + string(modeChar), + appliedArgs: " " + targetNick, + consumed: 1, + skip: false, + } +} + // applyChannelModes applies mode changes using the // service for individual mode operations. func (c *Conn) applyChannelModes( @@ -505,52 +623,57 @@ func (c *Conn) applyChannelModes( appliedArgs := "" for _, modeChar := range modeStr { + var res modeResult + switch modeChar { case '+': adding = true + + continue case '-': adding = false - case 'm', 't': + + continue + case 'i', 'm', 'n', 's', 't': _ = c.svc.SetChannelFlag( ctx, chID, modeChar, adding, ) - if adding { - applied += "+" + string(modeChar) - } else { - applied += "-" + string(modeChar) - } - case 'o', 'v': - if argIdx >= len(args) { - break + prefix := "+" + if !adding { + prefix = "-" } - targetNick := args[argIdx] - argIdx++ - - err := c.svc.ApplyMemberMode( - ctx, chID, channel, - targetNick, modeChar, adding, + res = modeResult{ + applied: prefix + string(modeChar), + appliedArgs: "", + consumed: 0, + skip: false, + } + case 'H': + res = c.applyHashcashMode( + ctx, chID, adding, args, argIdx, + ) + case 'o', 'v': + res = c.applyMemberMode( + ctx, chID, channel, + modeChar, adding, args, argIdx, ) - if err != nil { - c.sendIRCError(err) - - continue - } - - if adding { - applied += "+" + string(modeChar) - } else { - applied += "-" + string(modeChar) - } - - appliedArgs += " " + targetNick default: c.sendNumeric( irc.ErrUnknownMode, string(modeChar), "is unknown mode char to me", ) + + continue + } + + argIdx += res.consumed + + if !res.skip { + applied += res.applied + appliedArgs += res.appliedArgs } } diff --git a/internal/ircserver/conn.go b/internal/ircserver/conn.go index 173ac19..835f452 100644 --- a/internal/ircserver/conn.go +++ b/internal/ircserver/conn.go @@ -19,15 +19,16 @@ import ( ) const ( - maxLineLen = 512 - readTimeout = 5 * time.Minute - writeTimeout = 30 * time.Second - dnsTimeout = 3 * time.Second - pollInterval = 100 * time.Millisecond - pingInterval = 90 * time.Second - pongDeadline = 30 * time.Second - maxNickLen = 32 - minPasswordLen = 8 + maxLineLen = 512 + readTimeout = 5 * time.Minute + writeTimeout = 30 * time.Second + dnsTimeout = 3 * time.Second + pollInterval = 100 * time.Millisecond + pingInterval = 90 * time.Second + pongDeadline = 30 * time.Second + maxNickLen = 32 + minPasswordLen = 8 + maxHashcashBits = 40 ) // cmdHandler is the signature for registered IRC command @@ -434,7 +435,7 @@ func (c *Conn) deliverWelcome() { "CHANTYPES=#", "NICKLEN=32", "PREFIX=(ov)@+", - "CHANMODES=,,H,mnst", + "CHANMODES=,,H,imnst", "NETWORK="+c.serverSfx, "are supported by this server", ) diff --git a/internal/ircserver/export_test.go b/internal/ircserver/export_test.go index dc75ee7..e1583c9 100644 --- a/internal/ircserver/export_test.go +++ b/internal/ircserver/export_test.go @@ -19,12 +19,9 @@ func NewTestServer( database *db.Database, brk *broker.Broker, ) *Server { - svc := &service.Service{ - DB: database, - Broker: brk, - Config: cfg, - Log: log, - } + svc := service.NewTestService( + database, brk, cfg, log, + ) return &Server{ //nolint:exhaustruct log: log, diff --git a/internal/service/service.go b/internal/service/service.go index a8304fa..6a8e298 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -8,6 +8,7 @@ import ( "encoding/json" "fmt" "log/slog" + "strconv" "strings" "git.eeqj.de/sneak/neoirc/internal/broker" @@ -30,19 +31,35 @@ type Params struct { // Service provides shared business logic for IRC commands. type Service struct { - DB *db.Database - Broker *broker.Broker - Config *config.Config - Log *slog.Logger + db *db.Database + broker *broker.Broker + config *config.Config + log *slog.Logger } // New creates a new Service. func New(params Params) *Service { return &Service{ - DB: params.Database, - Broker: params.Broker, - Config: params.Config, - Log: params.Logger.Get(), + db: params.Database, + broker: params.Broker, + config: params.Config, + log: params.Logger.Get(), + } +} + +// NewTestService creates a Service for use in tests +// outside the service package. +func NewTestService( + database *db.Database, + brk *broker.Broker, + cfg *config.Config, + log *slog.Logger, +) *Service { + return &Service{ + db: database, + broker: brk, + config: cfg, + log: log, } } @@ -76,7 +93,7 @@ func (s *Service) FanOut( params, body, meta json.RawMessage, sessionIDs []int64, ) (int64, string, error) { - dbID, msgUUID, err := s.DB.InsertMessage( + dbID, msgUUID, err := s.db.InsertMessage( ctx, command, from, to, params, body, meta, ) if err != nil { @@ -84,8 +101,8 @@ func (s *Service) FanOut( } for _, sid := range sessionIDs { - _ = s.DB.EnqueueToSession(ctx, sid, dbID) - s.Broker.Notify(sid) + _ = s.db.EnqueueToSession(ctx, sid, dbID) + s.broker.Notify(sid) } return dbID, msgUUID, nil @@ -120,7 +137,7 @@ func (s *Service) SendChannelMessage( nick, command, channel string, body, meta json.RawMessage, ) (int64, string, error) { - chID, err := s.DB.GetChannelByName(ctx, channel) + chID, err := s.db.GetChannelByName(ctx, channel) if err != nil { return 0, "", &IRCError{ irc.ErrNoSuchChannel, @@ -129,7 +146,7 @@ func (s *Service) SendChannelMessage( } } - isMember, _ := s.DB.IsChannelMember( + isMember, _ := s.db.IsChannelMember( ctx, chID, sessionID, ) if !isMember { @@ -141,7 +158,7 @@ func (s *Service) SendChannelMessage( } // Ban check — banned users cannot send messages. - isBanned, banErr := s.DB.IsSessionBanned( + isBanned, banErr := s.db.IsSessionBanned( ctx, chID, sessionID, ) if banErr == nil && isBanned { @@ -152,12 +169,12 @@ func (s *Service) SendChannelMessage( } } - moderated, _ := s.DB.IsChannelModerated(ctx, chID) + moderated, _ := s.db.IsChannelModerated(ctx, chID) if moderated { - isOp, _ := s.DB.IsChannelOperator( + isOp, _ := s.db.IsChannelOperator( ctx, chID, sessionID, ) - isVoiced, _ := s.DB.IsChannelVoiced( + isVoiced, _ := s.db.IsChannelVoiced( ctx, chID, sessionID, ) @@ -170,7 +187,7 @@ func (s *Service) SendChannelMessage( } } - memberIDs, _ := s.DB.GetChannelMemberIDs(ctx, chID) + memberIDs, _ := s.db.GetChannelMemberIDs(ctx, chID) recipients := excludeSession(memberIDs, sessionID) dbID, uuid, fanErr := s.FanOut( @@ -193,7 +210,7 @@ func (s *Service) SendDirectMessage( nick, command, target string, body, meta json.RawMessage, ) (*DirectMsgResult, error) { - targetSID, err := s.DB.GetSessionByNick(ctx, target) + targetSID, err := s.db.GetSessionByNick(ctx, target) if err != nil { return nil, &IRCError{ irc.ErrNoSuchNick, @@ -202,7 +219,7 @@ func (s *Service) SendDirectMessage( } } - away, _ := s.DB.GetAway(ctx, targetSID) + away, _ := s.db.GetAway(ctx, targetSID) recipients := []int64{targetSID} if targetSID != sessionID { @@ -228,19 +245,19 @@ func (s *Service) JoinChannel( sessionID int64, nick, channel, suppliedKey string, ) (*JoinResult, error) { - chID, err := s.DB.GetOrCreateChannel(ctx, channel) + chID, err := s.db.GetOrCreateChannel(ctx, channel) if err != nil { return nil, fmt.Errorf("get/create channel: %w", err) } - memberCount, countErr := s.DB.CountChannelMembers( + memberCount, countErr := s.db.CountChannelMembers( ctx, chID, ) isCreator := countErr == nil && memberCount == 0 if !isCreator { if joinErr := checkJoinRestrictions( - ctx, s.DB, chID, sessionID, + ctx, s.db, chID, sessionID, channel, suppliedKey, memberCount, ); joinErr != nil { return nil, joinErr @@ -248,11 +265,11 @@ func (s *Service) JoinChannel( } if isCreator { - err = s.DB.JoinChannelAsOperator( + err = s.db.JoinChannelAsOperator( ctx, chID, sessionID, ) } else { - err = s.DB.JoinChannel(ctx, chID, sessionID) + err = s.db.JoinChannel(ctx, chID, sessionID) } if err != nil { @@ -260,9 +277,9 @@ func (s *Service) JoinChannel( } // Clear invite after successful join. - _ = s.DB.ClearChannelInvite(ctx, chID, sessionID) + _ = s.db.ClearChannelInvite(ctx, chID, sessionID) - memberIDs, _ := s.DB.GetChannelMemberIDs(ctx, chID) + memberIDs, _ := s.db.GetChannelMemberIDs(ctx, chID) body, _ := json.Marshal([]string{channel}) //nolint:errchkjson _, _, _ = s.FanOut( //nolint:dogsled // fire-and-forget broadcast @@ -284,7 +301,7 @@ func (s *Service) PartChannel( sessionID int64, nick, channel, reason string, ) error { - chID, err := s.DB.GetChannelByName(ctx, channel) + chID, err := s.db.GetChannelByName(ctx, channel) if err != nil { return &IRCError{ irc.ErrNoSuchChannel, @@ -293,7 +310,7 @@ func (s *Service) PartChannel( } } - isMember, _ := s.DB.IsChannelMember( + isMember, _ := s.db.IsChannelMember( ctx, chID, sessionID, ) if !isMember { @@ -304,7 +321,7 @@ func (s *Service) PartChannel( } } - memberIDs, _ := s.DB.GetChannelMemberIDs(ctx, chID) + memberIDs, _ := s.db.GetChannelMemberIDs(ctx, chID) recipients := excludeSession(memberIDs, sessionID) body, _ := json.Marshal([]string{reason}) //nolint:errchkjson @@ -313,8 +330,8 @@ func (s *Service) PartChannel( nil, body, nil, recipients, ) - s.DB.PartChannel(ctx, chID, sessionID) //nolint:errcheck,gosec - s.DB.DeleteChannelIfEmpty(ctx, chID) //nolint:errcheck,gosec + s.db.PartChannel(ctx, chID, sessionID) //nolint:errcheck,gosec + s.db.DeleteChannelIfEmpty(ctx, chID) //nolint:errcheck,gosec return nil } @@ -326,7 +343,7 @@ func (s *Service) SetTopic( sessionID int64, nick, channel, topic string, ) error { - chID, err := s.DB.GetChannelByName(ctx, channel) + chID, err := s.db.GetChannelByName(ctx, channel) if err != nil { return &IRCError{ irc.ErrNoSuchChannel, @@ -335,7 +352,7 @@ func (s *Service) SetTopic( } } - isMember, _ := s.DB.IsChannelMember( + isMember, _ := s.db.IsChannelMember( ctx, chID, sessionID, ) if !isMember { @@ -346,9 +363,9 @@ func (s *Service) SetTopic( } } - topicLocked, _ := s.DB.IsChannelTopicLocked(ctx, chID) + topicLocked, _ := s.db.IsChannelTopicLocked(ctx, chID) if topicLocked { - isOp, _ := s.DB.IsChannelOperator( + isOp, _ := s.db.IsChannelOperator( ctx, chID, sessionID, ) if !isOp { @@ -360,15 +377,15 @@ func (s *Service) SetTopic( } } - if setErr := s.DB.SetTopic( + if setErr := s.db.SetTopic( ctx, channel, topic, ); setErr != nil { return fmt.Errorf("set topic: %w", setErr) } - _ = s.DB.SetTopicMeta(ctx, channel, topic, nick) + _ = s.db.SetTopicMeta(ctx, channel, topic, nick) - memberIDs, _ := s.DB.GetChannelMemberIDs(ctx, chID) + memberIDs, _ := s.db.GetChannelMemberIDs(ctx, chID) body, _ := json.Marshal([]string{topic}) //nolint:errchkjson _, _, _ = s.FanOut( //nolint:dogsled // fire-and-forget broadcast @@ -387,7 +404,7 @@ func (s *Service) KickUser( sessionID int64, nick, channel, targetNick, reason string, ) error { - chID, err := s.DB.GetChannelByName(ctx, channel) + chID, err := s.db.GetChannelByName(ctx, channel) if err != nil { return &IRCError{ irc.ErrNoSuchChannel, @@ -396,7 +413,7 @@ func (s *Service) KickUser( } } - isOp, _ := s.DB.IsChannelOperator( + isOp, _ := s.db.IsChannelOperator( ctx, chID, sessionID, ) if !isOp { @@ -407,7 +424,7 @@ func (s *Service) KickUser( } } - targetSID, err := s.DB.GetSessionByNick( + targetSID, err := s.db.GetSessionByNick( ctx, targetNick, ) if err != nil { @@ -418,7 +435,7 @@ func (s *Service) KickUser( } } - isMember, _ := s.DB.IsChannelMember( + isMember, _ := s.db.IsChannelMember( ctx, chID, targetSID, ) if !isMember { @@ -429,7 +446,7 @@ func (s *Service) KickUser( } } - memberIDs, _ := s.DB.GetChannelMemberIDs(ctx, chID) + memberIDs, _ := s.db.GetChannelMemberIDs(ctx, chID) body, _ := json.Marshal([]string{reason}) //nolint:errchkjson params, _ := json.Marshal( //nolint:errchkjson []string{targetNick}, @@ -440,8 +457,8 @@ func (s *Service) KickUser( params, body, nil, memberIDs, ) - s.DB.PartChannel(ctx, chID, targetSID) //nolint:errcheck,gosec - s.DB.DeleteChannelIfEmpty(ctx, chID) //nolint:errcheck,gosec + s.db.PartChannel(ctx, chID, targetSID) //nolint:errcheck,gosec + s.db.DeleteChannelIfEmpty(ctx, chID) //nolint:errcheck,gosec return nil } @@ -453,7 +470,7 @@ func (s *Service) ChangeNick( sessionID int64, oldNick, newNick string, ) error { - err := s.DB.ChangeNick(ctx, sessionID, newNick) + err := s.db.ChangeNick(ctx, sessionID, newNick) if err != nil { if strings.Contains(err.Error(), "UNIQUE") || db.IsUniqueConstraintError(err) { @@ -485,7 +502,7 @@ func (s *Service) BroadcastQuit( sessionID int64, nick, reason string, ) { - channels, err := s.DB.GetSessionChannels( + channels, err := s.db.GetSessionChannels( ctx, sessionID, ) if err != nil { @@ -495,7 +512,7 @@ func (s *Service) BroadcastQuit( notified := make(map[int64]bool) for _, ch := range channels { - memberIDs, memErr := s.DB.GetChannelMemberIDs( + memberIDs, memErr := s.db.GetChannelMemberIDs( ctx, ch.ID, ) if memErr != nil { @@ -526,11 +543,11 @@ func (s *Service) BroadcastQuit( } for _, ch := range channels { - s.DB.PartChannel(ctx, ch.ID, sessionID) //nolint:errcheck,gosec - s.DB.DeleteChannelIfEmpty(ctx, ch.ID) //nolint:errcheck,gosec + s.db.PartChannel(ctx, ch.ID, sessionID) //nolint:errcheck,gosec + s.db.DeleteChannelIfEmpty(ctx, ch.ID) //nolint:errcheck,gosec } - s.DB.DeleteSession(ctx, sessionID) //nolint:errcheck,gosec + s.db.DeleteSession(ctx, sessionID) //nolint:errcheck,gosec } // SetAway sets or clears the away message. Returns true @@ -540,7 +557,7 @@ func (s *Service) SetAway( sessionID int64, message string, ) (bool, error) { - err := s.DB.SetAway(ctx, sessionID, message) + err := s.db.SetAway(ctx, sessionID, message) if err != nil { return false, fmt.Errorf("set away: %w", err) } @@ -555,8 +572,8 @@ func (s *Service) Oper( sessionID int64, name, password string, ) error { - cfgName := s.Config.OperName - cfgPassword := s.Config.OperPassword + cfgName := s.config.OperName + cfgPassword := s.config.OperPassword // Use constant-time comparison and return the same // error for all failures to prevent information @@ -575,7 +592,7 @@ func (s *Service) Oper( } } - _ = s.DB.SetSessionOper(ctx, sessionID, true) + _ = s.db.SetSessionOper(ctx, sessionID, true) return nil } @@ -587,7 +604,7 @@ func (s *Service) ValidateChannelOp( sessionID int64, channel string, ) (int64, error) { - chID, err := s.DB.GetChannelByName(ctx, channel) + chID, err := s.db.GetChannelByName(ctx, channel) if err != nil { return 0, &IRCError{ irc.ErrNoSuchChannel, @@ -596,7 +613,7 @@ func (s *Service) ValidateChannelOp( } } - isOp, _ := s.DB.IsChannelOperator( + isOp, _ := s.db.IsChannelOperator( ctx, chID, sessionID, ) if !isOp { @@ -619,7 +636,7 @@ func (s *Service) ApplyMemberMode( mode rune, adding bool, ) error { - targetSID, err := s.DB.GetSessionByNick( + targetSID, err := s.db.GetSessionByNick( ctx, targetNick, ) if err != nil { @@ -630,7 +647,7 @@ func (s *Service) ApplyMemberMode( } } - isMember, _ := s.DB.IsChannelMember( + isMember, _ := s.db.IsChannelMember( ctx, chID, targetSID, ) if !isMember { @@ -643,11 +660,11 @@ func (s *Service) ApplyMemberMode( switch mode { case 'o': - _ = s.DB.SetChannelMemberOperator( + _ = s.db.SetChannelMemberOperator( ctx, chID, targetSID, adding, ) case 'v': - _ = s.DB.SetChannelMemberVoiced( + _ = s.db.SetChannelMemberVoiced( ctx, chID, targetSID, adding, ) } @@ -655,7 +672,8 @@ func (s *Service) ApplyMemberMode( return nil } -// SetChannelFlag applies +m/-m or +t/-t on a channel. +// SetChannelFlag applies a simple boolean channel mode +// (+m/-m, +t/-t, +i/-i, +s/-s, +n/-n). func (s *Service) SetChannelFlag( ctx context.Context, chID int64, @@ -664,29 +682,37 @@ func (s *Service) SetChannelFlag( ) error { switch flag { case 'm': - if err := s.DB.SetChannelModerated( + if err := s.db.SetChannelModerated( ctx, chID, setting, ); err != nil { return fmt.Errorf("set moderated: %w", err) } case 't': - if err := s.DB.SetChannelTopicLocked( + if err := s.db.SetChannelTopicLocked( ctx, chID, setting, ); err != nil { return fmt.Errorf("set topic locked: %w", err) } case 'i': - if err := s.DB.SetChannelInviteOnly( + if err := s.db.SetChannelInviteOnly( ctx, chID, setting, ); err != nil { return fmt.Errorf("set invite only: %w", err) } case 's': - if err := s.DB.SetChannelSecret( + if err := s.db.SetChannelSecret( ctx, chID, setting, ); err != nil { return fmt.Errorf("set secret: %w", err) } + case 'n': + if err := s.db.SetChannelNoExternal( + ctx, chID, setting, + ); err != nil { + return fmt.Errorf( + "set no external: %w", err, + ) + } } return nil @@ -700,7 +726,7 @@ func (s *Service) BroadcastMode( chID int64, modeText string, ) { - memberIDs, _ := s.DB.GetChannelMemberIDs(ctx, chID) + memberIDs, _ := s.db.GetChannelMemberIDs(ctx, chID) body, _ := json.Marshal([]string{modeText}) //nolint:errchkjson _, _, _ = s.FanOut( //nolint:dogsled // fire-and-forget broadcast @@ -709,24 +735,60 @@ func (s *Service) BroadcastMode( ) } -// QueryChannelMode returns the channel mode string. +// QueryChannelMode returns the complete channel mode +// string including all flags and parameterized modes. func (s *Service) QueryChannelMode( ctx context.Context, chID int64, ) string { modes := "+" - moderated, _ := s.DB.IsChannelModerated(ctx, chID) + noExternal, _ := s.db.IsChannelNoExternal(ctx, chID) + if noExternal { + modes += "n" + } + + inviteOnly, _ := s.db.IsChannelInviteOnly(ctx, chID) + if inviteOnly { + modes += "i" + } + + moderated, _ := s.db.IsChannelModerated(ctx, chID) if moderated { modes += "m" } - topicLocked, _ := s.DB.IsChannelTopicLocked(ctx, chID) + secret, _ := s.db.IsChannelSecret(ctx, chID) + if secret { + modes += "s" + } + + topicLocked, _ := s.db.IsChannelTopicLocked(ctx, chID) if topicLocked { modes += "t" } - return modes + var modeParams string + + key, _ := s.db.GetChannelKey(ctx, chID) + if key != "" { + modes += "k" + modeParams += " " + key + } + + limit, _ := s.db.GetChannelUserLimit(ctx, chID) + if limit > 0 { + modes += "l" + modeParams += " " + strconv.Itoa(limit) + } + + bits, _ := s.db.GetChannelHashcashBits(ctx, chID) + if bits > 0 { + modes += "H" + modeParams += " " + strconv.Itoa(bits) + } + + return modes + modeParams } // broadcastNickChange notifies channel peers of a nick @@ -736,7 +798,7 @@ func (s *Service) broadcastNickChange( sessionID int64, oldNick, newNick string, ) { - channels, err := s.DB.GetSessionChannels( + channels, err := s.db.GetSessionChannels( ctx, sessionID, ) if err != nil { @@ -746,7 +808,7 @@ func (s *Service) broadcastNickChange( body, _ := json.Marshal([]string{newNick}) //nolint:errchkjson notified := make(map[int64]bool) - dbID, _, insErr := s.DB.InsertMessage( + dbID, _, insErr := s.db.InsertMessage( ctx, irc.CmdNick, oldNick, "", nil, body, nil, ) @@ -755,12 +817,12 @@ func (s *Service) broadcastNickChange( } // Notify the user themselves (for multi-client sync). - _ = s.DB.EnqueueToSession(ctx, sessionID, dbID) - s.Broker.Notify(sessionID) + _ = s.db.EnqueueToSession(ctx, sessionID, dbID) + s.broker.Notify(sessionID) notified[sessionID] = true for _, ch := range channels { - memberIDs, memErr := s.DB.GetChannelMemberIDs( + memberIDs, memErr := s.db.GetChannelMemberIDs( ctx, ch.ID, ) if memErr != nil { @@ -774,8 +836,8 @@ func (s *Service) broadcastNickChange( notified[mid] = true - _ = s.DB.EnqueueToSession(ctx, mid, dbID) - s.Broker.Notify(mid) + _ = s.db.EnqueueToSession(ctx, mid, dbID) + s.broker.Notify(mid) } } }