feat: add traditional IRC wire protocol listener (closes #89) #94
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ const (
|
||||
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",
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user