feat(irc): improve reconnect and add notifications (#315)

* refactor(irc): nickserv and methods

* feat(notifications): add new events and refactor send

* feat(irc): handle disconnect reconnect and connect

* feat(irc): update config for ergo local irc server

* feat(irc): retry initial connect

* feat(irc): show nickserv errors
This commit is contained in:
Ludvig Lundgren 2022-06-29 16:50:38 +02:00 committed by GitHub
parent 41eef4e8be
commit f63ace662a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 1343 additions and 133 deletions

View file

@ -97,8 +97,8 @@ func main() {
indexerService = indexer.NewService(log, cfg.Config, indexerRepo, apiService, schedulingService)
filterService = filter.NewService(log, filterRepo, actionRepo, apiService, indexerService)
releaseService = release.NewService(log, releaseRepo, actionService, filterService)
ircService = irc.NewService(log, ircRepo, releaseService, indexerService)
notificationService = notification.NewService(log, notificationRepo)
ircService = irc.NewService(log, ircRepo, releaseService, indexerService, notificationService)
feedService = feed.NewService(log, feedRepo, feedCacheRepo, releaseService, schedulingService)
)

1
go.mod
View file

@ -6,6 +6,7 @@ require (
github.com/Masterminds/squirrel v1.5.1
github.com/anacrolix/torrent v1.11.0
github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef
github.com/avast/retry-go v3.0.0+incompatible
github.com/dcarbone/zadapters/zstdlog v0.3.1
github.com/dustin/go-humanize v1.0.0
github.com/ergochat/irc-go v0.1.1-0.20220619021632-5b9a0365c50a

2
go.sum
View file

@ -118,6 +118,8 @@ github.com/anacrolix/utp v0.0.0-20180219060659-9e0e1d1d0572/go.mod h1:MDwc+vsGEq
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef h1:2JGTg6JapxP9/R33ZaagQtAM4EkkSYnIAlOG5EI8gkM=
github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef/go.mod h1:JS7hed4L1fj0hXcyEejnW57/7LCetXggd+vwrRnYeII=
github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0=
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
github.com/benbjohnson/immutable v0.2.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=

View file

@ -35,18 +35,21 @@ type IrcNetwork struct {
}
type IrcNetworkWithHealth struct {
ID int64 `json:"id"`
Name string `json:"name"`
Enabled bool `json:"enabled"`
Server string `json:"server"`
Port int `json:"port"`
TLS bool `json:"tls"`
Pass string `json:"pass"`
InviteCommand string `json:"invite_command"`
NickServ NickServ `json:"nickserv,omitempty"`
Channels []ChannelWithHealth `json:"channels"`
Connected bool `json:"connected"`
ConnectedSince time.Time `json:"connected_since"`
ID int64 `json:"id"`
Name string `json:"name"`
Enabled bool `json:"enabled"`
Server string `json:"server"`
Port int `json:"port"`
TLS bool `json:"tls"`
Pass string `json:"pass"`
InviteCommand string `json:"invite_command"`
NickServ NickServ `json:"nickserv,omitempty"`
CurrentNick string `json:"current_nick"`
PreferredNick string `json:"preferred_nick"`
Channels []ChannelWithHealth `json:"channels"`
Connected bool `json:"connected"`
ConnectedSince time.Time `json:"connected_since"`
ConnectionErrors []string `json:"connection_errors"`
}
type ChannelWithHealth struct {

View file

@ -78,11 +78,13 @@ const (
type NotificationEvent string
const (
//NotificationEventAppUpdateAvailable NotificationEvent = "APP_UPDATE_AVAILABLE"
NotificationEventPushApproved NotificationEvent = "PUSH_APPROVED"
NotificationEventPushRejected NotificationEvent = "PUSH_REJECTED"
NotificationEventPushError NotificationEvent = "PUSH_ERROR"
NotificationEventUpdateAvailable NotificationEvent = "UPDATE_AVAILABLE"
NotificationEventIRCHealth NotificationEvent = "IRC_HEALTH"
NotificationEventIRCDisconnected NotificationEvent = "IRC_DISCONNECTED"
NotificationEventIRCReconnected NotificationEvent = "IRC_RECONNECTED"
NotificationEventTest NotificationEvent = "TEST"
)

View file

@ -57,7 +57,5 @@ func (s Subscriber) releasePushStatus(actionStatus *domain.ReleaseActionStatus)
func (s Subscriber) sendNotification(event *domain.NotificationEvent, payload *domain.NotificationPayload) {
s.log.Trace().Msgf("events: '%v' '%+v'", event, payload)
if err := s.notificationSvc.Send(*event, *payload); err != nil {
s.log.Error().Err(err).Msgf("events: '%v' error sending notification", event)
}
s.notificationSvc.Send(*event, *payload)
}

View file

@ -11,18 +11,16 @@ import (
"github.com/autobrr/autobrr/internal/announce"
"github.com/autobrr/autobrr/internal/domain"
"github.com/autobrr/autobrr/internal/logger"
"github.com/autobrr/autobrr/internal/notification"
"github.com/autobrr/autobrr/internal/release"
"github.com/avast/retry-go"
"github.com/dcarbone/zadapters/zstdlog"
"github.com/ergochat/irc-go/ircevent"
"github.com/ergochat/irc-go/ircmsg"
"github.com/rs/zerolog"
)
var (
connectTimeout = 15 * time.Second
)
type channelHealth struct {
m sync.RWMutex
@ -52,40 +50,46 @@ func (h *channelHealth) resetMonitoring() {
h.m.Lock()
h.monitoring = false
h.monitoringSince = time.Time{}
h.lastAnnounce = time.Time{}
h.m.Unlock()
}
type Handler struct {
log zerolog.Logger
network *domain.IrcNetwork
releaseSvc release.Service
announceProcessors map[string]announce.Processor
definitions map[string]*domain.IndexerDefinition
log zerolog.Logger
network *domain.IrcNetwork
releaseSvc release.Service
notificationService notification.Service
announceProcessors map[string]announce.Processor
definitions map[string]*domain.IndexerDefinition
client *ircevent.Connection
m sync.RWMutex
lastPing time.Time
connected bool
connectedSince time.Time
// tODO disconnectedTime
connected bool
connectedSince time.Time
haveDisconnected bool
manuallyDisconnected bool
validAnnouncers map[string]struct{}
validChannels map[string]struct{}
channelHealth map[string]*channelHealth
connectionErrors []string
failedNickServAttempts int
}
func NewHandler(log logger.Logger, network domain.IrcNetwork, definitions []*domain.IndexerDefinition, releaseSvc release.Service) *Handler {
func NewHandler(log logger.Logger, network domain.IrcNetwork, definitions []*domain.IndexerDefinition, releaseSvc release.Service, notificationSvc notification.Service) *Handler {
h := &Handler{
log: log.With().Str("network", network.Server).Logger(),
client: nil,
network: &network,
releaseSvc: releaseSvc,
definitions: map[string]*domain.IndexerDefinition{},
announceProcessors: map[string]announce.Processor{},
validAnnouncers: map[string]struct{}{},
validChannels: map[string]struct{}{},
channelHealth: map[string]*channelHealth{},
log: log.With().Str("network", network.Server).Logger(),
client: nil,
network: &network,
releaseSvc: releaseSvc,
notificationService: notificationSvc,
definitions: map[string]*domain.IndexerDefinition{},
announceProcessors: map[string]announce.Processor{},
validAnnouncers: map[string]struct{}{},
validChannels: map[string]struct{}{},
channelHealth: map[string]*channelHealth{},
}
// init indexer, announceProcessor
@ -135,6 +139,10 @@ func (h *Handler) removeIndexer() {
}
func (h *Handler) Run() error {
// TODO validate
// check if network requires nickserv
// chech if network or channels requires invite command
addr := fmt.Sprintf("%v:%d", h.network.Server, h.network.Port)
subLogger := zstdlog.NewStdLoggerWithLevel(h.log.With().Logger(), zerolog.TraceLevel)
@ -160,11 +168,14 @@ func (h *Handler) Run() error {
}
h.client.AddConnectCallback(h.onConnect)
h.client.AddDisconnectCallback(h.onDisconnect)
h.client.AddCallback("MODE", h.handleMode)
h.client.AddCallback("INVITE", h.handleInvite)
h.client.AddCallback("366", h.handleJoined)
h.client.AddCallback("PART", h.handlePart)
h.client.AddCallback("PRIVMSG", h.onMessage)
h.client.AddCallback("NOTICE", h.onNotice)
h.client.AddCallback("NICK", h.onNick)
if err := h.client.Connect(); err != nil {
h.log.Error().Stack().Err(err).Msg("connect error")
@ -172,11 +183,32 @@ func (h *Handler) Run() error {
// reset connection status on handler and channels
h.resetConnectionStatus()
//return err
}
// count connect attempts
connectAttempts := 1
// set connected since now
h.setConnectionStatus()
// retry initial connect if network is down
// using exponential backoff of 15 seconds
err := retry.Do(
func() error {
h.log.Debug().Msgf("connect attempt %d", connectAttempts)
err := h.client.Connect()
if err != nil {
connectAttempts++
return err
}
h.log.Debug().Msgf("connected at attempt %d", connectAttempts)
return nil
},
retry.Delay(time.Second*15),
retry.Attempts(25),
retry.DelayType(func(n uint, err error, config *retry.Config) time.Duration {
return retry.BackOffDelay(n, err, config)
}),
)
h.log.Error().Stack().Err(err).Msgf("connect error: attempt %d", connectAttempts)
}
h.client.Loop()
@ -239,12 +271,20 @@ func (h *Handler) AddChannelHealth(channel string) {
func (h *Handler) Stop() {
h.log.Debug().Msg("Disconnecting...")
h.m.Lock()
h.manuallyDisconnected = true
h.m.Unlock()
h.client.Quit()
}
func (h *Handler) Restart() error {
h.log.Debug().Msg("Restarting network...")
h.m.Lock()
h.manuallyDisconnected = true
h.m.Unlock()
h.client.Quit()
time.Sleep(4 * time.Second)
@ -253,41 +293,182 @@ func (h *Handler) Restart() error {
}
func (h *Handler) onConnect(m ircmsg.Message) {
identified := false
// 1. No nickserv, no invite command - join
// 2. Nickserv - join after auth
// 3. nickserv and invite command - join after nickserv
// 4. invite command - join
time.Sleep(4 * time.Second)
h.resetConnectErrors()
h.setConnectionStatus()
if h.haveDisconnected {
h.notificationService.Send(domain.NotificationEventIRCReconnected, domain.NotificationPayload{
Subject: "IRC Reconnected",
Message: fmt.Sprintf("Network: %v", h.network.Name),
})
// reset haveDisconnected
h.haveDisconnected = false
}
h.log.Debug().Msgf("connected to: %v", h.network.Name)
time.Sleep(2 * time.Second)
if h.network.NickServ.Password != "" {
err := h.HandleNickServIdentify(h.network.NickServ.Password)
if err != nil {
if err := h.NickServIdentify(h.network.NickServ.Password); err != nil {
h.log.Error().Stack().Err(err).Msg("error nickserv")
return
}
identified = true
// return and wait for NOTICE of nickserv auth
return
}
time.Sleep(4 * time.Second)
if h.network.InviteCommand != "" {
err := h.handleConnectCommands(h.network.InviteCommand)
if err != nil {
if h.network.InviteCommand != "" && h.network.NickServ.Password == "" {
if err := h.sendConnectCommands(h.network.InviteCommand); err != nil {
h.log.Error().Stack().Err(err).Msgf("error sending connect command %v", h.network.InviteCommand)
return
}
time.Sleep(1 * time.Second)
return
}
if !identified {
for _, channel := range h.network.Channels {
err := h.HandleJoinChannel(channel.Name, channel.Password)
if err != nil {
h.log.Error().Stack().Err(err).Msgf("error joining channels %v", err)
// join channels if no password or no invite command
h.JoinChannels()
}
func (h *Handler) onDisconnect(m ircmsg.Message) {
h.log.Debug().Msgf("DISCONNECT")
h.haveDisconnected = true
h.resetConnectionStatus()
// check if we are responsible for disconnect
if !h.manuallyDisconnected {
// only send notification if we did not initiate disconnect/restart/stop
h.notificationService.Send(domain.NotificationEventIRCDisconnected, domain.NotificationPayload{
Subject: "IRC Disconnected unexpectedly",
Message: fmt.Sprintf("Network: %v", h.network.Name),
})
// reset
h.manuallyDisconnected = false
}
}
func (h *Handler) onNotice(msg ircmsg.Message) {
if msg.Nick() == "NickServ" {
h.log.Debug().Msgf("NOTICE from nickserv: %v", msg.Params)
if contains(msg.Params[1],
"Invalid account credentials",
"Authentication failed: Invalid account credentials",
"password incorrect",
) {
h.addConnectError("authentication failed: Bad account credentials")
h.log.Warn().Msg("NickServ: authentication failed - bad account credentials")
if h.failedNickServAttempts >= 1 {
h.log.Warn().Msgf("NickServ %d failed login attempts", h.failedNickServAttempts)
// stop network and notify user
h.Stop()
}
h.failedNickServAttempts++
}
if contains(msg.Params[1],
"Account does not exist",
"Authentication failed: Account does not exist", // Nick ANICK isn't registered
) {
h.addConnectError("authentication failed: account does not exist")
if h.failedNickServAttempts >= 2 {
h.log.Warn().Msgf("NickServ %d failed login attempts", h.failedNickServAttempts)
// stop network and notify user
h.Stop()
}
h.failedNickServAttempts++
}
if contains(msg.Params[1],
"This nickname is registered and protected",
"please choose a different nick",
"choose a different nick",
) {
if h.failedNickServAttempts >= 3 {
h.log.Warn().Msgf("NickServ %d failed login attempts", h.failedNickServAttempts)
h.addConnectError("authentication failed: nick in use and not authenticated")
// stop network and notify user
h.Stop()
}
h.failedNickServAttempts++
}
// params: [test-bot You're now logged in as test-bot]
// Password accepted - you are now recognized.
if contains(msg.Params[1], "you're now logged in as", "password accepted", "you are now recognized") {
h.log.Debug().Msgf("NOTICE nickserv logged in: %v", msg.Params)
h.resetConnectErrors()
h.failedNickServAttempts = 0
// if no invite command, join
if h.network.InviteCommand == "" {
h.JoinChannels()
return
}
// else send connect commands
if err := h.sendConnectCommands(h.network.InviteCommand); err != nil {
h.log.Error().Stack().Err(err).Msgf("error sending connect command %v", h.network.InviteCommand)
return
}
}
}
//[test-bot Invalid parameters. For usage, do /msg NickServ HELP IDENTIFY]
// fallback for networks that require both password and nick to NickServ IDENTIFY
if contains(msg.Params[1], "invalid parameters", "help identify") {
h.log.Debug().Msgf("NOTICE nickserv invalid: %v", msg.Params)
if err := h.client.Send("PRIVMSG", "NickServ", fmt.Sprintf("IDENTIFY %v %v", h.network.NickServ.Account, h.network.NickServ.Password)); err != nil {
return
}
}
// Your nickname is not registered
}
}
func contains(s string, substr ...string) bool {
s = strings.ToLower(s)
for _, c := range substr {
c = strings.ToLower(c)
if strings.Contains(s, c) {
return true
} else if c == s {
return true
}
}
return false
}
func (h *Handler) onNick(msg ircmsg.Message) {
h.log.Debug().Msgf("NICK event: %v params: %v", msg.Nick(), msg.Params)
if h.client.CurrentNick() != h.client.PreferredNick() {
h.log.Debug().Msgf("nick miss-match: got %v want %v", h.client.CurrentNick(), h.client.PreferredNick())
}
}
func (h *Handler) onMessage(msg ircmsg.Message) {
@ -349,7 +530,18 @@ func (h *Handler) sendToAnnounceProcessor(channel string, msg string) error {
return nil
}
func (h *Handler) HandleJoinChannel(channel string, password string) error {
func (h *Handler) JoinChannels() {
for _, channel := range h.network.Channels {
if err := h.JoinChannel(channel.Name, channel.Password); err != nil {
h.log.Error().Stack().Err(err).Msgf("error joining channel %v", channel.Name)
continue
}
time.Sleep(1 * time.Second)
}
}
func (h *Handler) JoinChannel(channel string, password string) error {
m := ircmsg.Message{
Command: "JOIN",
Params: []string{channel},
@ -397,12 +589,12 @@ func (h *Handler) handlePart(msg ircmsg.Message) {
// TODO remove announceProcessor
h.log.Info().Msgf("Left channel '%v'", channel)
h.log.Debug().Msgf("Left channel '%v'", channel)
return
}
func (h *Handler) HandlePartChannel(channel string) error {
func (h *Handler) PartChannel(channel string) error {
h.log.Debug().Msgf("PART channel %v", channel)
err := h.client.Part(channel)
@ -452,7 +644,7 @@ func (h *Handler) handleJoined(msg ircmsg.Message) {
}
}
func (h *Handler) handleConnectCommands(msg string) error {
func (h *Handler) sendConnectCommands(msg string) error {
connectCommand := strings.ReplaceAll(msg, "/msg", "")
connectCommands := strings.Split(connectCommand, ",")
@ -495,7 +687,7 @@ func (h *Handler) handleInvite(msg ircmsg.Message) {
return
}
func (h *Handler) HandleNickServIdentify(password string) error {
func (h *Handler) NickServIdentify(password string) error {
m := ircmsg.Message{
Command: "PRIVMSG",
Params: []string{"NickServ", "IDENTIFY", password},
@ -512,7 +704,7 @@ func (h *Handler) HandleNickServIdentify(password string) error {
return nil
}
func (h *Handler) HandleNickChange(nick string) error {
func (h *Handler) NickChange(nick string) error {
h.log.Debug().Msgf("Nick change: %v", nick)
h.client.SetNick(nick)
@ -520,6 +712,14 @@ func (h *Handler) HandleNickChange(nick string) error {
return nil
}
func (h *Handler) CurrentNick() string {
return h.client.CurrentNick()
}
func (h *Handler) PreferredNick() string {
return h.client.PreferredNick()
}
func (h *Handler) handleMode(msg ircmsg.Message) {
h.log.Debug().Msgf("MODE: %+v", msg)
@ -528,22 +728,15 @@ func (h *Handler) handleMode(msg ircmsg.Message) {
return
}
time.Sleep(5 * time.Second)
if h.network.NickServ.Password != "" && !strings.Contains(msg.Params[0], h.client.Nick) || !strings.Contains(msg.Params[1], "+r") {
h.log.Trace().Msgf("MODE: Not correct permission yet: %v", msg.Params)
return
}
for _, ch := range h.network.Channels {
err := h.HandleJoinChannel(ch.Name, ch.Password)
if err != nil {
h.log.Error().Err(err).Msgf("error joining channel: %v", ch.Name)
continue
}
time.Sleep(2 * time.Second)
time.Sleep(1 * time.Second)
}
// join channels
h.JoinChannels()
return
}
@ -568,14 +761,6 @@ func (h *Handler) isValidChannel(channel string) bool {
return true
}
func (h *Handler) setLastPing() {
h.lastPing = time.Now()
}
func (h *Handler) GetLastPing() time.Time {
return h.lastPing
}
// irc line can contain lots of extra stuff like color so lets clean that
func (h *Handler) cleanMessage(message string) string {
var regexMessageClean = `\x0f|\x1f|\x02|\x03(?:[\d]{1,2}(?:,[\d]{1,2})?)?`
@ -588,3 +773,17 @@ func (h *Handler) cleanMessage(message string) string {
return rxp.ReplaceAllString(message, "")
}
func (h *Handler) addConnectError(message string) {
h.m.Lock()
defer h.m.Unlock()
h.connectionErrors = append(h.connectionErrors, message)
}
func (h *Handler) resetConnectErrors() {
h.m.Lock()
defer h.m.Unlock()
h.connectionErrors = []string{}
}

View file

@ -9,6 +9,7 @@ import (
"github.com/autobrr/autobrr/internal/domain"
"github.com/autobrr/autobrr/internal/indexer"
"github.com/autobrr/autobrr/internal/logger"
"github.com/autobrr/autobrr/internal/notification"
"github.com/autobrr/autobrr/internal/release"
"github.com/pkg/errors"
@ -28,24 +29,26 @@ type Service interface {
}
type service struct {
log logger.Logger
repo domain.IrcRepo
releaseService release.Service
indexerService indexer.Service
indexerMap map[string]string
handlers map[handlerKey]*Handler
log logger.Logger
repo domain.IrcRepo
releaseService release.Service
indexerService indexer.Service
notificationService notification.Service
indexerMap map[string]string
handlers map[handlerKey]*Handler
stopWG sync.WaitGroup
lock sync.Mutex
}
func NewService(log logger.Logger, repo domain.IrcRepo, releaseSvc release.Service, indexerSvc indexer.Service) Service {
func NewService(log logger.Logger, repo domain.IrcRepo, releaseSvc release.Service, indexerSvc indexer.Service, notificationSvc notification.Service) Service {
return &service{
log: log,
repo: repo,
releaseService: releaseSvc,
indexerService: indexerSvc,
handlers: make(map[handlerKey]*Handler),
log: log,
repo: repo,
releaseService: releaseSvc,
indexerService: indexerSvc,
notificationService: notificationSvc,
handlers: make(map[handlerKey]*Handler),
}
}
@ -79,7 +82,7 @@ func (s *service) StartHandlers() {
definitions := s.indexerService.GetIndexersByIRCNetwork(network.Server)
// init new irc handler
handler := NewHandler(s.log, network, definitions, s.releaseService)
handler := NewHandler(s.log, network, definitions, s.releaseService, s.notificationService)
// use network.Server + nick to use multiple indexers with different nick per network
// this allows for multiple handlers to one network
@ -135,7 +138,7 @@ func (s *service) startNetwork(network domain.IrcNetwork) error {
definitions := s.indexerService.GetIndexersByIRCNetwork(network.Server)
// init new irc handler
handler := NewHandler(s.log, network, definitions, s.releaseService)
handler := NewHandler(s.log, network, definitions, s.releaseService, s.notificationService)
s.handlers[handlerKey{network.Server, network.NickServ.Account}] = handler
s.lock.Unlock()
@ -198,15 +201,13 @@ func (s *service) checkIfNetworkRestartNeeded(network *domain.IrcNetwork) error
if handler.NickServ.Account != network.NickServ.Account {
s.log.Debug().Msg("changing nick")
err := existingHandler.HandleNickChange(network.NickServ.Account)
if err != nil {
if err := existingHandler.NickChange(network.NickServ.Account); err != nil {
s.log.Error().Stack().Err(err).Msgf("failed to change nick %q", network.NickServ.Account)
}
} else if handler.NickServ.Password != network.NickServ.Password {
s.log.Debug().Msg("nickserv: changing password")
err := existingHandler.HandleNickServIdentify(network.NickServ.Password)
if err != nil {
if err := existingHandler.NickServIdentify(network.NickServ.Password); err != nil {
s.log.Error().Stack().Err(err).Msgf("failed to identify with nickserv %q", network.NickServ.Account)
}
}
@ -252,8 +253,8 @@ func (s *service) checkIfNetworkRestartNeeded(network *domain.IrcNetwork) error
// leave channels
for _, leaveChannel := range channelsToLeave {
s.log.Debug().Msgf("%v: part channel %v", network.Server, leaveChannel)
err := existingHandler.HandlePartChannel(leaveChannel)
if err != nil {
if err := existingHandler.PartChannel(leaveChannel); err != nil {
s.log.Error().Stack().Err(err).Msgf("failed to leave channel: %q", leaveChannel)
}
}
@ -261,8 +262,8 @@ func (s *service) checkIfNetworkRestartNeeded(network *domain.IrcNetwork) error
// join channels
for _, joinChannel := range channelsToJoin {
s.log.Debug().Msgf("%v: join new channel %v", network.Server, joinChannel)
err := existingHandler.HandleJoinChannel(joinChannel.Name, joinChannel.Password)
if err != nil {
if err := existingHandler.JoinChannel(joinChannel.Name, joinChannel.Password); err != nil {
s.log.Error().Stack().Err(err).Msgf("failed to join channel: %q", joinChannel.Name)
}
}
@ -386,17 +387,18 @@ func (s *service) GetNetworksWithHealth(ctx context.Context) ([]domain.IrcNetwor
for _, n := range networks {
netw := domain.IrcNetworkWithHealth{
ID: n.ID,
Name: n.Name,
Enabled: n.Enabled,
Server: n.Server,
Port: n.Port,
TLS: n.TLS,
Pass: n.Pass,
InviteCommand: n.InviteCommand,
NickServ: n.NickServ,
Connected: false,
Channels: []domain.ChannelWithHealth{},
ID: n.ID,
Name: n.Name,
Enabled: n.Enabled,
Server: n.Server,
Port: n.Port,
TLS: n.TLS,
Pass: n.Pass,
InviteCommand: n.InviteCommand,
NickServ: n.NickServ,
Connected: false,
Channels: []domain.ChannelWithHealth{},
ConnectionErrors: []string{},
}
handler, ok := s.handlers[handlerKey{n.Server, n.NickServ.Account}]
@ -404,10 +406,21 @@ func (s *service) GetNetworksWithHealth(ctx context.Context) ([]domain.IrcNetwor
// only set connected and connected since if we have an active handler and connection
if handler.client.Connected() {
handler.m.RLock()
netw.Connected = handler.connected
netw.ConnectedSince = handler.connectedSince
// current and preferred nick is only available if the network is connected
netw.CurrentNick = handler.CurrentNick()
netw.PreferredNick = handler.PreferredNick()
handler.m.RUnlock()
}
// if we have any connection errors like bad nickserv auth add them here
if len(handler.connectionErrors) > 0 {
netw.ConnectionErrors = handler.connectionErrors
}
}
channels, err := s.repo.ListChannels(n.ID)

View file

@ -140,6 +140,10 @@ func (a *discordSender) buildEmbed(event domain.NotificationEvent, payload domai
color = GRAY
case domain.NotificationEventPushError:
color = RED
case domain.NotificationEventIRCDisconnected:
color = RED
case domain.NotificationEventIRCReconnected:
color = GREEN
case domain.NotificationEventTest:
color = LIGHT_BLUE
}

View file

@ -13,7 +13,7 @@ type Service interface {
Store(ctx context.Context, n domain.Notification) (*domain.Notification, error)
Update(ctx context.Context, n domain.Notification) (*domain.Notification, error)
Delete(ctx context.Context, id int) error
Send(event domain.NotificationEvent, payload domain.NotificationPayload) error
Send(event domain.NotificationEvent, payload domain.NotificationPayload)
Test(ctx context.Context, notification domain.Notification) error
}
@ -109,17 +109,19 @@ func (s *service) registerSenders() {
}
// Send notifications
func (s *service) Send(event domain.NotificationEvent, payload domain.NotificationPayload) error {
func (s *service) Send(event domain.NotificationEvent, payload domain.NotificationPayload) {
s.log.Debug().Msgf("sending notification for %v", string(event))
for _, sender := range s.senders {
// check if sender is active and have notification types
if sender.CanSend(event) {
sender.Send(event, payload)
go func() {
for _, sender := range s.senders {
// check if sender is active and have notification types
if sender.CanSend(event) {
sender.Send(event, payload)
}
}
}
}()
return nil
return
}
func (s *service) Test(ctx context.Context, notification domain.Notification) error {

View file

@ -2,12 +2,13 @@ version: "3.8"
services:
ergo:
image: ghcr.io/ergochat/ergo:stable
image: ghcr.io/ergochat/ergo:latest
ports:
- "6667:6667/tcp"
- "6697:6697/tcp"
volumes:
- data:/ircd
- ergo-data:/ircd
- ./ircd.yaml:/ircd/ircd.yaml
volumes:
data:
ergo-data:

964
test/integration/ircd.yaml Normal file
View file

@ -0,0 +1,964 @@
# This is the "traditional" or "mainstream" config file for Ergo.
# It tries to replicate the behavior of other ircds, at the cost of not
# taking full advantage of Ergo's features. This config is suitable for use
# in IRCv3 conformance testing.
# network configuration
network:
# name of the network
name: ErgoTest
# server configuration
server:
# server name
name: ergo.test
# addresses to listen on
listeners:
# This version of the config provides a public plaintext listener on
# port 6667 for testing and compatibility with legacy applications.
# We recommend disabling this listener in a production setting
# and replacing it with loopback-only listeners (see default.yaml):
":6667":
# The standard SSL/TLS port for IRC is 6697. This will listen on all interfaces:
":6697":
# this is a standard TLS configuration with a single certificate;
# see the manual for instructions on how to configure SNI
tls:
cert: fullchain.pem
key: privkey.pem
# 'proxy' should typically be false. It's for cloud load balancers that
# always send a PROXY protocol header ahead of the connection. See the
# manual ("Reverse proxies") for more details.
proxy: false
# optionally set the minimum TLS version (defaults to 1.0):
# min-tls-version: 1.2
# Example of a Unix domain socket for proxying:
# "/tmp/ergo_sock":
# Example of a Tor listener: any connection that comes in on this listener will
# be considered a Tor connection. It is strongly recommended that this listener
# *not* be on a public interface --- it should be on 127.0.0.0/8 or unix domain:
# "/hidden_service_sockets/ergo_tor_sock":
# tor: true
# Example of a WebSocket listener:
# ":8097":
# websocket: true
# tls:
# cert: fullchain.pem
# key: privkey.pem
# sets the permissions for Unix listen sockets. on a typical Linux system,
# the default is 0775 or 0755, which prevents other users/groups from connecting
# to the socket. With 0777, it behaves like a normal TCP socket
# where anyone can connect.
unix-bind-mode: 0777
# configure the behavior of Tor listeners (ignored if you didn't enable any):
tor-listeners:
# if this is true, connections from Tor must authenticate with SASL
require-sasl: false
# what hostname should be displayed for Tor connections?
vhost: "tor-network.onion"
# allow at most this many connections at once (0 for no limit):
max-connections: 64
# connection throttling (limit how many connection attempts are allowed at once):
throttle-duration: 10m
# set to 0 to disable throttling:
max-connections-per-duration: 64
# strict transport security, to get clients to automagically use TLS
sts:
# whether to advertise STS
#
# to stop advertising STS, leave this enabled and set 'duration' below to "0". this will
# advertise to connecting users that the STS policy they have saved is no longer valid
enabled: false
# how long clients should be forced to use TLS for.
# setting this to a too-long time will mean bad things if you later remove your TLS.
# the default duration below is 1 month, 2 days and 5 minutes.
duration: 1mo2d5m
# tls port - you should be listening on this port above
port: 6697
# should clients include this STS policy when they ship their inbuilt preload lists?
preload: false
websockets:
# Restrict the origin of WebSocket connections by matching the "Origin" HTTP
# header. This setting causes ergo to reject websocket connections unless
# they originate from a page on one of the whitelisted websites in this list.
# This prevents malicious websites from making their visitors connect to your
# ergo instance without their knowledge. An empty list means there are no
# restrictions.
allowed-origins:
# - "https://ergo.chat"
# - "https://*.ergo.chat"
# casemapping controls what kinds of strings are permitted as identifiers (nicknames,
# channel names, account names, etc.), and how they are normalized for case.
# with the recommended default of 'precis', UTF8 identifiers that are "sane"
# (according to RFC 8265) are allowed, and the server additionally tries to protect
# against confusable characters ("homoglyph attacks").
# the other options are 'ascii' (traditional ASCII-only identifiers), and 'permissive',
# which allows identifiers to contain unusual characters like emoji, but makes users
# vulnerable to homoglyph attacks. unless you're really confident in your decision,
# we recommend leaving this value at its default (changing it once the network is
# already up and running is problematic).
casemapping: "precis"
# enforce-utf8 controls whether the server will preemptively discard non-UTF8
# messages (since they cannot be relayed to websocket clients), or will allow
# them and relay them to non-websocket clients (as in traditional IRC).
enforce-utf8: true
# whether to look up user hostnames with reverse DNS. there are 3 possibilities:
# 1. [enabled here] lookup-hostnames enabled, IP cloaking disabled; users will see each other's hostnames
# 2. lookup-hostnames disabled, IP cloaking disabled; users will see each other's numeric IPs
# 3. IP cloaking enabled; users will see cloaked hostnames
lookup-hostnames: true
# whether to confirm hostname lookups using "forward-confirmed reverse DNS", i.e., for
# any hostname returned from reverse DNS, resolve it back to an IP address and reject it
# unless it matches the connecting IP
forward-confirm-hostnames: true
# use ident protocol to get usernames
check-ident: true
# ignore the supplied user/ident string from the USER command, always setting user/ident
# to the following literal value; this can potentially reduce confusion and simplify bans.
# the value must begin with a '~' character. comment out / omit to disable:
#coerce-ident: '~u'
# password to login to the server, generated using `ergo genpasswd`:
#password: "$2a$04$0123456789abcdef0123456789abcdef0123456789abcdef01234"
# motd filename
# if you change the motd, you should move it to ircd.motd
motd: ergo.motd
# motd formatting codes
# if this is true, the motd is escaped using formatting codes like $c, $b, and $i
motd-formatting: true
# relaying using the RELAYMSG command
relaymsg:
# is relaymsg enabled at all?
enabled: true
# which character(s) are reserved for relayed nicks?
separators: "/"
# can channel operators use RELAYMSG in their channels?
# our implementation of RELAYMSG makes it safe for chanops to use without the
# possibility of real users being silently spoofed
available-to-chanops: true
# IPs/CIDRs the PROXY command can be used from
# This should be restricted to localhost (127.0.0.1/8, ::1/128, and unix sockets).
# Unless you have a good reason. you should also add these addresses to the
# connection limits and throttling exemption lists.
proxy-allowed-from:
- localhost
# - "192.168.1.1"
# - "192.168.10.1/24"
# controls the use of the WEBIRC command (by IRC<->web interfaces, bouncers and similar)
webirc:
# one webirc block -- should correspond to one set of gateways
-
# SHA-256 fingerprint of the TLS certificate the gateway must use to connect
# (comment this out to use passwords only)
certfp: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"
# password the gateway uses to connect, made with `ergo genpasswd`
password: "$2a$04$abcdef0123456789abcdef0123456789abcdef0123456789abcde" # test
# IPs/CIDRs that can use this webirc command
# you should also add these addresses to the connection limits and throttling exemption lists
hosts:
- localhost
# - "192.168.1.1"
# - "192.168.10.1/24"
# maximum length of clients' sendQ in bytes
# this should be big enough to hold bursts of channel/direct messages
max-sendq: 96k
# compatibility with legacy clients
compatibility:
# many clients require that the final parameter of certain messages be an
# RFC1459 trailing parameter, i.e., prefixed with :, whether or not this is
# actually required. this forces Ergo to send those parameters
# as trailings. this is recommended unless you're testing clients for conformance;
# defaults to true when unset for that reason.
force-trailing: true
# some clients (ZNC 1.6.x and lower, Pidgin 2.12 and lower) do not
# respond correctly to SASL messages with the server name as a prefix:
# https://github.com/znc/znc/issues/1212
# this works around that bug, allowing them to use SASL.
send-unprefixed-sasl: true
# traditionally, IRC servers will truncate and send messages that are
# too long to be relayed intact. this behavior can be disabled by setting
# allow-truncation to false, in which case Ergo will reject the message
# and return an error to the client. (note that this option defaults to true
# when unset.)
allow-truncation: true
# IP-based DoS protection
ip-limits:
# whether to limit the total number of concurrent connections per IP/CIDR
count: true
# maximum concurrent connections per IP/CIDR
max-concurrent-connections: 16
# whether to restrict the rate of new connections per IP/CIDR
throttle: true
# how long to keep track of connections for
window: 10m
# maximum number of new connections per IP/CIDR within the given duration
max-connections-per-window: 32
# how wide the CIDR should be for IPv4 (a /32 is a fully specified IPv4 address)
cidr-len-ipv4: 32
# how wide the CIDR should be for IPv6 (a /64 is the typical prefix assigned
# by an ISP to an individual customer for their LAN)
cidr-len-ipv6: 64
# IPs/networks which are exempted from connection limits
exempted:
- "localhost"
# - "192.168.1.1"
# - "2001:0db8::/32"
# custom connection limits for certain IPs/networks.
custom-limits:
#"irccloud":
# nets:
# - "192.184.9.108" # highgate.irccloud.com
# - "192.184.9.110" # ealing.irccloud.com
# - "192.184.9.112" # charlton.irccloud.com
# - "192.184.10.118" # brockwell.irccloud.com
# - "192.184.10.9" # tooting.irccloud.com
# - "192.184.8.73" # hathersage.irccloud.com
# - "192.184.8.103" # stonehaven.irccloud.com
# - "5.254.36.57" # tinside.irccloud.com
# - "5.254.36.56/29" # additional ipv4 net
# - "2001:67c:2f08::/48"
# - "2a03:5180:f::/64"
# max-concurrent-connections: 2048
# max-connections-per-window: 2048
# pluggable IP ban mechanism, via subprocess invocation
# this can be used to check new connections against a DNSBL, for example
# see the manual for details on how to write an IP ban checking script
ip-check-script:
enabled: false
command: "/usr/local/bin/check-ip-ban"
# constant list of args to pass to the command; the actual query
# and result are transmitted over stdin/stdout:
args: []
# timeout for process execution, after which we send a SIGTERM:
timeout: 9s
# how long after the SIGTERM before we follow up with a SIGKILL:
kill-timeout: 1s
# how many scripts are allowed to run at once? 0 for no limit:
max-concurrency: 64
# if true, only check anonymous connections (not logged into an account)
# at the very end of the handshake:
exempt-sasl: false
# IP cloaking hides users' IP addresses from other users and from channel admins
# (but not from server admins), while still allowing channel admins to ban
# offending IP addresses or networks. In place of hostnames derived from reverse
# DNS, users see fake domain names like pwbs2ui4377257x8.irc. These names are
# generated deterministically from the underlying IP address, but if the underlying
# IP is not already known, it is infeasible to recover it from the cloaked name.
ip-cloaking:
# whether to enable IP cloaking
enabled: false
# whether to use these cloak settings (specifically, `netname` and `num-bits`)
# to produce unique hostnames for always-on clients. you can enable this even if
# you disabled IP cloaking for normal clients above. if this is disabled,
# always-on clients will all have an identical hostname (the server name).
enabled-for-always-on: true
# fake TLD at the end of the hostname, e.g., pwbs2ui4377257x8.irc
# you may want to use your network name here
netname: "irc"
# the cloaked hostname is derived only from the CIDR (most significant bits
# of the IP address), up to a configurable number of bits. this is the
# granularity at which bans will take effect for IPv4. Note that changing
# this value will invalidate any stored bans.
cidr-len-ipv4: 32
# analogous granularity for IPv6
cidr-len-ipv6: 64
# number of bits of hash output to include in the cloaked hostname.
# more bits means less likelihood of distinct IPs colliding,
# at the cost of a longer cloaked hostname. if this value is set to 0,
# all users will receive simply `netname` as their cloaked hostname.
num-bits: 64
# secure-nets identifies IPs and CIDRs which are secure at layer 3,
# for example, because they are on a trusted internal LAN or a VPN.
# plaintext connections from these IPs and CIDRs will be considered
# secure (clients will receive the +Z mode and be allowed to resume
# or reattach to secure connections). note that loopback IPs are always
# considered secure:
secure-nets:
# - "10.0.0.0/8"
# Ergo will write files to disk under certain circumstances, e.g.,
# CPU profiling or data export. by default, these files will be written
# to the working directory. set this to customize:
#output-path: "/home/ergo/out"
# the hostname used by "services", e.g., NickServ, defaults to "localhost",
# e.g., `NickServ!NickServ@localhost`. uncomment this to override:
#override-services-hostname: "example.network"
# in a "closed-loop" system where you control the server and all the clients,
# you may want to increase the maximum (non-tag) length of an IRC line from
# the default value of 512. DO NOT change this on a public server:
# max-line-len: 512
# send all 0's as the LUSERS (user counts) output to non-operators; potentially useful
# if you don't want to publicize how popular the server is
suppress-lusers: false
# account options
accounts:
# is account authentication enabled, i.e., can users log into existing accounts?
authentication-enabled: true
# account registration
registration:
# can users register new accounts for themselves? if this is false, operators with
# the `accreg` capability can still create accounts with `/NICKSERV SAREGISTER`
enabled: true
# can users use the REGISTER command to register before fully connecting?
allow-before-connect: true
# global throttle on new account creation
throttling:
enabled: true
# window
duration: 10m
# number of attempts allowed within the window
max-attempts: 30
# this is the bcrypt cost we'll use for account passwords
# (note that 4 is the lowest value allowed by the bcrypt library)
bcrypt-cost: 4
# length of time a user has to verify their account before it can be re-registered
verify-timeout: "32h"
# options for email verification of account registrations
email-verification:
enabled: false
sender: "admin@my.network"
require-tls: true
helo-domain: "my.network" # defaults to server name if unset
# options to enable DKIM signing of outgoing emails (recommended, but
# requires creating a DNS entry for the public key):
# dkim:
# domain: "my.network"
# selector: "20200229"
# key-file: "dkim.pem"
# to use an MTA/smarthost instead of sending email directly:
# mta:
# server: localhost
# port: 25
# username: "admin"
# password: "hunter2"
blacklist-regexes:
# - ".*@mailinator.com"
timeout: 60s
# email-based password reset:
password-reset:
enabled: false
# time before we allow resending the email
cooldown: 1h
# time for which a password reset code is valid
timeout: 1d
# throttle account login attempts (to prevent either password guessing, or DoS
# attacks on the server aimed at forcing repeated expensive bcrypt computations)
login-throttling:
enabled: true
# window
duration: 1m
# number of attempts allowed within the window
max-attempts: 3
# some clients (notably Pidgin and Hexchat) offer only a single password field,
# which makes it impossible to specify a separate server password (for the PASS
# command) and SASL password. if this option is set to true, a client that
# successfully authenticates with SASL will not be required to send
# PASS as well, so it can be configured to authenticate with SASL only.
skip-server-password: false
# enable login to accounts via the PASS command, e.g., PASS account:password
# this is useful for compatibility with old clients that don't support SASL
login-via-pass-command: false
# require-sasl controls whether clients are required to have accounts
# (and sign into them using SASL) to connect to the server
require-sasl:
# if this is enabled, all clients must authenticate with SASL while connecting.
# WARNING: for a private server, you MUST set accounts.registration.enabled
# to false as well, in order to prevent non-administrators from registering
# accounts.
enabled: false
# IPs/CIDRs which are exempted from the account requirement
exempted:
- "localhost"
# - '10.10.0.0/16'
# nick-reservation controls how, and whether, nicknames are linked to accounts
nick-reservation:
# is there any enforcement of reserved nicknames?
enabled: true
# how many nicknames, in addition to the account name, can be reserved?
# (note that additional nicks are unusable under force-nick-equals-account
# or if the client is always-on)
additional-nick-limit: 2
# method describes how nickname reservation is handled
# strict: users must already be logged in to their account (via
# SASL, PASS account:password, or /NickServ IDENTIFY)
# in order to use their reserved nickname(s)
# optional: no enforcement by default, but allow users to opt in to
# the enforcement level of their choice
method: optional
# allow users to set their own nickname enforcement status, e.g.,
# to opt out of strict enforcement
allow-custom-enforcement: true
# format for guest nicknames:
# 1. these nicknames cannot be registered or reserved
# 2. if a client is automatically renamed by the server,
# this is the template that will be used (e.g., Guest-nccj6rgmt97cg)
# 3. if enforce-guest-format (see below) is enabled, clients without
# a registered account will have this template applied to their
# nicknames (e.g., 'katie' will become 'Guest-katie')
guest-nickname-format: "Guest-*"
# when enabled, forces users not logged into an account to use
# a nickname matching the guest template. a caveat: this may prevent
# users from choosing nicknames in scripts different from the guest
# nickname format.
force-guest-format: false
# when enabled, forces users logged into an account to use the
# account name as their nickname. when combined with strict nickname
# enforcement, this lets users treat nicknames and account names
# as equivalent for the purpose of ban/invite/exception lists.
force-nick-equals-account: false
# parallel setting to force-nick-equals-account: if true, this forbids
# anonymous users (i.e., users not logged into an account) to change their
# nickname after the initial connection is complete
forbid-anonymous-nick-changes: false
# multiclient controls whether Ergo allows multiple connections to
# attach to the same client/nickname identity; this is part of the
# functionality traditionally provided by a bouncer like ZNC
multiclient:
# when disabled, each connection must use a separate nickname (as is the
# typical behavior of IRC servers). when enabled, a new connection that
# has authenticated with SASL can associate itself with an existing
# client
enabled: true
# if this is disabled, clients have to opt in to bouncer functionality
# using nickserv or the cap system. if it's enabled, they can opt out
# via nickserv
allowed-by-default: false
# whether to allow clients that remain on the server even
# when they have no active connections. The possible values are:
# "disabled", "opt-in", "opt-out", or "mandatory".
always-on: "disabled"
# whether to mark always-on clients away when they have no active connections:
auto-away: "opt-in"
# QUIT always-on clients from the server if they go this long without connecting
# (use 0 or omit for no expiration):
#always-on-expiration: 90d
# vhosts controls the assignment of vhosts (strings displayed in place of the user's
# hostname/IP) by the HostServ service
vhosts:
# are vhosts enabled at all?
enabled: true
# maximum length of a vhost
max-length: 64
# regexp for testing the validity of a vhost
# (make sure any changes you make here are RFC-compliant)
valid-regexp: '^[0-9A-Za-z.\-_/]+$'
# modes that are set by default when a user connects
# if unset, no user modes will be set by default
# +i is invisible (a user's channels are hidden from whois replies)
# see /QUOTE HELP umodes for more user modes
# default-user-modes: +i
# pluggable authentication mechanism, via subprocess invocation
# see the manual for details on how to write an authentication plugin script
auth-script:
enabled: false
command: "/usr/local/bin/authenticate-irc-user"
# constant list of args to pass to the command; the actual authentication
# data is transmitted over stdin/stdout:
args: []
# should we automatically create users if the plugin returns success?
autocreate: true
# timeout for process execution, after which we send a SIGTERM:
timeout: 9s
# how long after the SIGTERM before we follow up with a SIGKILL:
kill-timeout: 1s
# how many scripts are allowed to run at once? 0 for no limit:
max-concurrency: 64
# channel options
channels:
# modes that are set when new channels are created
# +n is no-external-messages and +t is op-only-topic
# see /QUOTE HELP cmodes for more channel modes
default-modes: +nt
# how many channels can a client be in at once?
max-channels-per-client: 100
# if this is true, new channels can only be created by operators with the
# `chanreg` operator capability
operator-only-creation: false
# channel registration - requires an account
registration:
# can users register new channels?
enabled: true
# restrict new channel registrations to operators only?
# (operators can then transfer channels to regular users using /CS TRANSFER)
operator-only: false
# how many channels can each account register?
max-channels-per-account: 15
# as a crude countermeasure against spambots, anonymous connections younger
# than this value will get an empty response to /LIST (a time period of 0 disables)
list-delay: 0s
# INVITE to an invite-only channel expires after this amount of time
# (0 or omit for no expiration):
invite-expiration: 24h
# operator classes:
# an operator has a single "class" (defining a privilege level), which can include
# multiple "capabilities" (defining privileged actions they can take). all
# currently available operator capabilities are associated with either the
# 'chat-moderator' class (less privileged) or the 'server-admin' class (full
# privileges) below: you can mix and match to create new classes.
oper-classes:
# chat moderator: can ban/unban users from the server, join channels,
# fix mode issues and sort out vhosts.
"chat-moderator":
# title shown in WHOIS
title: Chat Moderator
# capability names
capabilities:
- "kill" # disconnect user sessions
- "ban" # ban IPs, CIDRs, NUH masks, and suspend accounts (UBAN / DLINE / KLINE)
- "nofakelag" # exempted from "fakelag" restrictions on rate of message sending
- "relaymsg" # use RELAYMSG in any channel (see the `relaymsg` config block)
- "vhosts" # add and remove vhosts from users
- "sajoin" # join arbitrary channels, including private channels
- "samode" # modify arbitrary channel and user modes
- "snomasks" # subscribe to arbitrary server notice masks
- "roleplay" # use the (deprecated) roleplay commands in any channel
# server admin: has full control of the ircd, including nickname and
# channel registrations
"server-admin":
# title shown in WHOIS
title: Server Admin
# oper class this extends from
extends: "chat-moderator"
# capability names
capabilities:
- "rehash" # rehash the server, i.e. reload the config at runtime
- "accreg" # modify arbitrary account registrations
- "chanreg" # modify arbitrary channel registrations
- "history" # modify or delete history messages
- "defcon" # use the DEFCON command (restrict server capabilities)
- "massmessage" # message all users on the server
# ircd operators
opers:
# default operator named 'admin'; log in with /OPER admin <password>
admin:
# which capabilities this oper has access to
class: "server-admin"
# traditionally, operator status is visible to unprivileged users in
# WHO and WHOIS responses. this can be disabled with 'hidden'.
hidden: false
# custom whois line (if `hidden` is enabled, visible only to other operators)
whois-line: is the server administrator
# custom hostname (ignored if `hidden` is enabled)
vhost: "staff"
# modes are modes to auto-set upon opering-up. uncomment this to automatically
# enable snomasks ("server notification masks" that alert you to server events;
# see `/quote help snomasks` while opered-up for more information):
#modes: +is acdjknoqtuxv
# operators can be authenticated either by password (with the /OPER command),
# or by certificate fingerprint, or both. if a password hash is set, then a
# password is required to oper up (e.g., /OPER dan mypassword). to generate
# the hash, use `ergo genpasswd`.
password: "$2a$04$am7Whn1NKJfKHnBXefLlpu0kiX7a.smAyO5Irai/Pf4NcO0ddbUfa"
# if a SHA-256 certificate fingerprint is configured here, then it will be
# required to /OPER. if you comment out the password hash above, then you can
# /OPER without a password.
#certfp: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"
# if 'auto' is set (and no password hash is set), operator permissions will be
# granted automatically as soon as you connect with the right fingerprint.
#auto: true
# example of a moderator named 'alice'
# (log in with /OPER alice <password>):
#alice:
# class: "chat-moderator"
# whois-line: "can help with moderation issues!"
# password: "$2a$04$0123456789abcdef0123456789abcdef0123456789abcdef01234"
# logging, takes inspiration from Insp
logging:
-
# how to log these messages
#
# file log to a file
# stdout log to stdout
# stderr log to stderr
# (you can specify multiple methods, e.g., to log to both stderr and a file)
method: stderr
# filename to log to, if file method is selected
# filename: ircd.log
# type(s) of logs to keep here. you can use - to exclude those types
#
# exclusions take precedent over inclusions, so if you exclude a type it will NEVER
# be logged, even if you explicitly include it
#
# useful types include:
# * everything (usually used with exclusing some types below)
# server server startup, rehash, and shutdown events
# accounts account registration and authentication
# channels channel creation and operations
# opers oper actions, authentication, etc
# services actions related to NickServ, ChanServ, etc.
# internal unexpected runtime behavior, including potential bugs
# userinput raw lines sent by users
# useroutput raw lines sent to users
type: "* -userinput -useroutput"
# one of: debug info warn error
level: info
#-
# # example of a file log that avoids logging IP addresses
# method: file
# filename: ircd.log
# type: "* -userinput -useroutput -connect-ip"
# level: debug
# debug options
debug:
# when enabled, Ergo will attempt to recover from certain kinds of
# client-triggered runtime errors that would normally crash the server.
# this makes the server more resilient to DoS, but could result in incorrect
# behavior. deployments that would prefer to "start from scratch", e.g., by
# letting the process crash and auto-restarting it with systemd, can set
# this to false.
recover-from-errors: true
# optionally expose a pprof http endpoint: https://golang.org/pkg/net/http/pprof/
# it is strongly recommended that you don't expose this on a public interface;
# if you need to access it remotely, you can use an SSH tunnel.
# set to `null`, "", leave blank, or omit to disable
# pprof-listener: "localhost:6060"
# lock file preventing multiple instances of Ergo from accidentally being
# started at once. comment out or set to the empty string ("") to disable.
# this path is relative to the working directory; if your datastore.path
# is absolute, you should use an absolute path here as well.
lock-file: "ircd.lock"
# datastore configuration
datastore:
# path to the datastore
path: ircd.db
# if the database schema requires an upgrade, `autoupgrade` will attempt to
# perform it automatically on startup. the database will be backed
# up, and if the upgrade fails, the original database will be restored.
autoupgrade: true
# connection information for MySQL (currently only used for persistent history):
mysql:
enabled: false
host: "localhost"
port: 3306
# if socket-path is set, it will be used instead of host:port
#socket-path: "/var/run/mysqld/mysqld.sock"
user: "ergo"
password: "hunter2"
history-database: "ergo_history"
timeout: 3s
max-conns: 4
# this may be necessary to prevent middleware from closing your connections:
#conn-max-lifetime: 180s
# languages config
languages:
# whether to load languages
enabled: false
# default language to use for new clients
# 'en' is the default English language in the code
default: en
# which directory contains our language files
path: languages
# limits - these need to be the same across the network
limits:
# nicklen is the max nick length allowed
nicklen: 32
# identlen is the max ident length allowed
identlen: 20
# channellen is the max channel length allowed
channellen: 64
# awaylen is the maximum length of an away message
awaylen: 390
# kicklen is the maximum length of a kick message
kicklen: 390
# topiclen is the maximum length of a channel topic
topiclen: 390
# maximum number of monitor entries a client can have
monitor-entries: 100
# whowas entries to store
whowas-entries: 100
# maximum length of channel lists (beI modes)
chan-list-modes: 60
# maximum number of messages to accept during registration (prevents
# DoS / resource exhaustion attacks):
registration-messages: 1024
# message length limits for the new multiline cap
multiline:
max-bytes: 4096 # 0 means disabled
max-lines: 100 # 0 means no limit
# fakelag: prevents clients from spamming commands too rapidly
fakelag:
# whether to enforce fakelag
enabled: true
# time unit for counting command rates
window: 1s
# clients can send this many commands without fakelag being imposed
burst-limit: 5
# once clients have exceeded their burst allowance, they can send only
# this many commands per `window`:
messages-per-window: 2
# client status resets to the default state if they go this long without
# sending any commands:
cooldown: 2s
# the roleplay commands are semi-standardized extensions to IRC that allow
# sending and receiving messages from pseudo-nicknames. this can be used either
# for actual roleplaying, or for bridging IRC with other protocols.
roleplay:
# are roleplay commands enabled at all? (channels and clients still have to
# opt in individually with the +E mode)
enabled: false
# require the "roleplay" oper capability to send roleplay messages?
require-oper: false
# require channel operator permissions to send roleplay messages?
require-chanops: false
# add the real nickname, in parentheses, to the end of every roleplay message?
add-suffix: true
# external services can integrate with the ircd using JSON Web Tokens (https://jwt.io).
# in effect, the server can sign a token attesting that the client is present on
# the server, is a member of a particular channel, etc.
extjwt:
# # default service config (for `EXTJWT #channel`).
# # expiration time for the token:
# expiration: 45s
# # you can configure tokens to be signed either with HMAC and a symmetric secret:
# secret: "65PHvk0K1_sM-raTsCEhatVkER_QD8a0zVV8gG2EWcI"
# # or with an RSA private key:
# #rsa-private-key-file: "extjwt.pem"
# # named services (for `EXTJWT #channel service_name`):
# services:
# "jitsi":
# expiration: 30s
# secret: "qmamLKDuOzIzlO8XqsGGewei_At11lewh6jtKfSTbkg"
# history message storage: this is used by CHATHISTORY, HISTORY, znc.in/playback,
# various autoreplay features, and the resume extension
history:
# should we store messages for later playback?
# by default, messages are stored in RAM only; they do not persist
# across server restarts. however, you may want to understand how message
# history interacts with the GDPR and/or any data privacy laws that apply
# in your country and the countries of your users.
enabled: true
# how many channel-specific events (messages, joins, parts) should be tracked per channel?
channel-length: 2048
# how many direct messages and notices should be tracked per user?
client-length: 256
# how long should we try to preserve messages?
# if `autoresize-window` is 0, the in-memory message buffers are preallocated to
# their maximum length. if it is nonzero, the buffers are initially small and
# are dynamically expanded up to the maximum length. if the buffer is full
# and the oldest message is older than `autoresize-window`, then it will overwrite
# the oldest message rather than resize; otherwise, it will expand if possible.
autoresize-window: 3d
# number of messages to automatically play back on channel join (0 to disable):
autoreplay-on-join: 0
# maximum number of CHATHISTORY messages that can be
# requested at once (0 disables support for CHATHISTORY)
chathistory-maxmessages: 1000
# maximum number of messages that can be replayed at once during znc emulation
# (znc.in/playback, or automatic replay on initial reattach to a persistent client):
znc-maxmessages: 2048
# options to delete old messages, or prevent them from being retrieved
restrictions:
# if this is set, messages older than this cannot be retrieved by anyone
# (and will eventually be deleted from persistent storage, if that's enabled)
expire-time: 1w
# this restricts access to channel history (it can be overridden by channel
# owners). options are: 'none' (no restrictions), 'registration-time'
# (logged-in users cannot retrieve messages older than their account
# registration date, and anonymous users cannot retrieve messages older than
# their sign-on time, modulo the grace-period described below), and
# 'join-time' (users cannot retrieve messages older than the time they
# joined the channel, so only always-on clients can view history).
query-cutoff: 'none'
# if query-cutoff is set to 'registration-time', this allows retrieval
# of messages that are up to 'grace-period' older than the above cutoff.
# if you use 'registration-time', this is recommended to allow logged-out
# users to query history after disconnections.
grace-period: 1h
# options to store history messages in a persistent database (currently only MySQL).
# in order to enable any of this functionality, you must configure a MySQL server
# in the `datastore.mysql` section.
persistent:
enabled: false
# store unregistered channel messages in the persistent database?
unregistered-channels: false
# for a registered channel, the channel owner can potentially customize
# the history storage setting. as the server operator, your options are
# 'disabled' (no persistent storage, regardless of per-channel setting),
# 'opt-in', 'opt-out', and 'mandatory' (force persistent storage, ignoring
# per-channel setting):
registered-channels: "opt-out"
# direct messages are only stored in the database for logged-in clients;
# you can control how they are stored here (same options as above).
# if you enable this, strict nickname reservation is strongly recommended
# as well.
direct-messages: "opt-out"
# options to control how messages are stored and deleted:
retention:
# allow users to delete their own messages from history?
allow-individual-delete: false
# if persistent history is enabled, create additional index tables,
# allowing deletion of JSON export of an account's messages. this
# may be needed for compliance with data privacy regulations.
enable-account-indexing: false
# options to control storage of TAGMSG
tagmsg-storage:
# by default, should TAGMSG be stored?
default: false
# if `default` is false, store TAGMSG containing any of these tags:
whitelist:
- "+draft/react"
- "+react"
# if `default` is true, don't store TAGMSG containing any of these tags:
#blacklist:
# - "+draft/typing"
# - "typing"
# whether to allow customization of the config at runtime using environment variables,
# e.g., ERGO__SERVER__MAX_SENDQ=128k. see the manual for more details.
allow-environment-overrides: true

View file

@ -315,5 +315,15 @@ export const EventOptions: SelectOption[] = [
label: "Push Error",
value: "PUSH_ERROR",
description: "On push error for the arrs or download client"
},
{
label: "IRC Disconnected",
value: "IRC_DISCONNECTED",
description: "Unexpectedly disconnected from irc network"
},
{
label: "IRC Reconnected",
value: "IRC_RECONNECTED",
description: "Reconnected to irc network after error"
}
];

View file

@ -11,6 +11,7 @@ import {
import { useToggle } from "../../hooks/hooks";
import { APIClient } from "../../api/APIClient";
import { EmptySimple } from "../../components/emptystates";
import {ExclamationCircleIcon} from "@heroicons/react/outline";
export const IrcSettings = () => {
const [addNetworkIsOpen, toggleAddNetwork] = useToggle(false);
@ -88,12 +89,12 @@ const ListItem = ({ idx, network }: ListItemProps) => {
<span className="relative inline-flex items-center">
{
network.enabled ? (
network.connected ? (
networkHealthy(network) ? (
<span className="mr-3 flex h-3 w-3 relative" title={`Connected since: ${simplifyDate(network.connected_since)}`}>
<span className="animate-ping inline-flex h-full w-full rounded-full bg-green-400 opacity-75"/>
<span className="inline-flex absolute rounded-full h-3 w-3 bg-green-500"/>
</span>
) : <span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-red-400" />
) : <span className="mr-3 flex items-center" title={network.connection_errors.toString()}><ExclamationCircleIcon className="h-4 w-4 text-red-400 hover:text-red-600" /></span>
) : <span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-gray-500" />
}
{network.name}
@ -155,3 +156,11 @@ const ListItem = ({ idx, network }: ListItemProps) => {
</li>
);
};
function networkHealthy(network: IrcNetworkWithHealth): boolean {
if (network.connection_errors.length > 0) {
return false
}
return true
}

View file

@ -53,6 +53,7 @@ interface IrcNetworkWithHealth {
channels: IrcChannelWithHealth[];
connected: boolean;
connected_since: string;
connection_errors: string[];
}
interface NickServ {

View file

@ -1,11 +1,12 @@
type NotificationType = "DISCORD" | "TELEGRAM";
type NotificationEvent = "PUSH_APPROVED" | "PUSH_REJECTED" | "PUSH_ERROR" | "IRC_DISCONNECTED" | "IRC_RECONNECTED";
interface Notification {
id: number;
name: string;
enabled: boolean;
type: NotificationType;
events: string[];
events: NotificationEvent[];
webhook?: string;
token?: string;
channel?: string;