From f103dff221e179e1ff38394a78957b7762c8d7fc Mon Sep 17 00:00:00 2001 From: Ludvig Lundgren Date: Sat, 8 Jan 2022 21:18:38 +0100 Subject: [PATCH 1/3] feat: show irc network status in settings list --- internal/domain/indexer.go | 18 +++++++ internal/domain/irc.go | 38 ++++++++++++++ internal/http/irc.go | 3 +- internal/irc/handler.go | 85 ++++++++++++++++++++++++++++---- internal/irc/service.go | 76 ++++++++++++++++++++++++++++ web/src/screens/settings/Irc.tsx | 12 ++++- 6 files changed, 221 insertions(+), 11 deletions(-) diff --git a/internal/domain/indexer.go b/internal/domain/indexer.go index 702eea6..bd7ce9a 100644 --- a/internal/domain/indexer.go +++ b/internal/domain/indexer.go @@ -72,6 +72,24 @@ type IndexerIRC struct { Settings []IndexerSetting `json:"settings"` } +func (i IndexerIRC) ValidAnnouncer(announcer string) bool { + for _, a := range i.Announcers { + if a == announcer { + return true + } + } + return false +} + +func (i IndexerIRC) ValidChannel(channel string) bool { + for _, a := range i.Channels { + if a == channel { + return true + } + } + return false +} + type IndexerParse struct { Type string `json:"type"` Lines []IndexerParseExtract `json:"lines"` diff --git a/internal/domain/irc.go b/internal/domain/irc.go index f04b540..06bbf09 100644 --- a/internal/domain/irc.go +++ b/internal/domain/irc.go @@ -34,6 +34,44 @@ type IrcNetwork struct { ConnectedSince *time.Time `json:"connected_since"` } +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 []IrcChannel `json:"channels"` + Channels []ChannelWithHealth `json:"channels"` + //Channels []struct { + // IrcChannel + // ChannelHealth + //} `json:"channels"` + Connected bool `json:"connected"` + ConnectedSince time.Time `json:"connected_since"` +} + +type ChannelWithHealth struct { + ID int64 `json:"id"` + Enabled bool `json:"enabled"` + Name string `json:"name"` + Password string `json:"password"` + Detached bool `json:"detached"` + Monitoring bool `json:"monitoring"` + MonitoringSince time.Time `json:"monitoring_since"` + LastAnnounce time.Time `json:"last_announce"` +} + +type ChannelHealth struct { + Name string `json:"name"` + Monitoring bool `json:"monitoring"` + MonitoringSince time.Time `json:"monitoring_since"` + LastAnnounce time.Time `json:"last_announce"` +} + type IrcRepo interface { StoreNetwork(network *IrcNetwork) error UpdateNetwork(ctx context.Context, network *IrcNetwork) error diff --git a/internal/http/irc.go b/internal/http/irc.go index 4c2dc5a..8591eb5 100644 --- a/internal/http/irc.go +++ b/internal/http/irc.go @@ -13,6 +13,7 @@ import ( type ircService interface { ListNetworks(ctx context.Context) ([]domain.IrcNetwork, error) + GetNetworksWithHealth(ctx context.Context) ([]domain.IrcNetworkWithHealth, error) DeleteNetwork(ctx context.Context, id int64) error GetNetworkByID(id int64) (*domain.IrcNetwork, error) StoreNetwork(ctx context.Context, network *domain.IrcNetwork) error @@ -46,7 +47,7 @@ func (h ircHandler) Routes(r chi.Router) { func (h ircHandler) listNetworks(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - networks, err := h.service.ListNetworks(ctx) + networks, err := h.service.GetNetworksWithHealth(ctx) if err != nil { // } diff --git a/internal/irc/handler.go b/internal/irc/handler.go index 95e5df5..d4fef7a 100644 --- a/internal/irc/handler.go +++ b/internal/irc/handler.go @@ -27,10 +27,13 @@ type channelHealth struct { name string monitoring bool monitoringSince time.Time - lastPing time.Time lastAnnounce time.Time } +func (h *channelHealth) SetLastAnnounce() { + h.lastAnnounce = time.Now() +} + type Handler struct { network *domain.IrcNetwork filterService filter.Service @@ -44,11 +47,15 @@ type Handler struct { stopped chan struct{} cancel context.CancelFunc - lastPing time.Time - lastAnnounce time.Time + lastPing time.Time + lastAnnounce time.Time + connected bool + connectedSince time.Time + // tODO disconnectedTime validAnnouncers map[string]struct{} - channels map[string]channelHealth + validChannels map[string]struct{} + channelHealth map[string]*channelHealth } func NewHandler(network domain.IrcNetwork, filterService filter.Service, releaseService release.Service, definitions []domain.IndexerDefinition) *Handler { @@ -63,6 +70,8 @@ func NewHandler(network domain.IrcNetwork, filterService filter.Service, release definitions: definitions, announceProcessors: map[string]announce.Processor{}, validAnnouncers: map[string]struct{}{}, + validChannels: map[string]struct{}{}, + channelHealth: map[string]*channelHealth{}, } // Networks can be shared by multiple indexers but channels are unique @@ -73,12 +82,22 @@ func NewHandler(network domain.IrcNetwork, filterService filter.Service, release channel = strings.ToLower(channel) h.announceProcessors[channel] = announce.NewAnnounceProcessor(definition, filterService, releaseService) + + h.channelHealth[channel] = &channelHealth{ + name: channel, + monitoring: false, + } } // create map of valid announcers for _, announcer := range definition.IRC.Announcers { h.validAnnouncers[announcer] = struct{}{} } + + // create map of valid channels + for _, ch := range definition.IRC.Channels { + h.validChannels[ch] = struct{}{} + } } return h @@ -216,10 +235,19 @@ func (s *Handler) Run() error { s.client = client + // set connected since now + s.connectedSince = time.Now() + s.connected = true + // Connect err = client.RunContext(ctx) if err != nil { log.Error().Err(err).Msgf("could not connect to %v", addr) + + // set connected false if we loose connection or stop + s.connectedSince = time.Time{} + s.connected = false + return err } @@ -279,7 +307,7 @@ func (s *Handler) onConnect(channels []domain.IrcChannel) error { if s.network.NickServ.Password != "" { err := s.handleNickServPRIVMSG(s.network.NickServ.Account, s.network.NickServ.Password) if err != nil { - log.Error().Err(err).Msgf("error nickserv: %v", s.network.Name) + log.Error().Stack().Err(err).Msgf("error nickserv: %v", s.network.Name) return err } identified = true @@ -291,7 +319,7 @@ func (s *Handler) onConnect(channels []domain.IrcChannel) error { err := s.handleInvitePRIVMSG(s.network.InviteCommand) if err != nil { - log.Error().Err(err).Msgf("error sending connect command %v to network: %v", s.network.InviteCommand, s.network.Name) + log.Error().Stack().Err(err).Msgf("error sending connect command %v to network: %v", s.network.InviteCommand, s.network.Name) return err } @@ -302,7 +330,7 @@ func (s *Handler) onConnect(channels []domain.IrcChannel) error { for _, channel := range channels { err := s.HandleJoinChannel(channel.Name, channel.Password) if err != nil { - log.Error().Err(err) + log.Error().Stack().Err(err) return err } } @@ -322,6 +350,12 @@ func (s *Handler) onMessage(msg *irc.Message) error { message := msg.Trailing() // TODO add network + // check if message is from a valid channel, if not return + validChannel := s.isValidChannel(*channel) + if !validChannel { + return nil + } + // check if message is from announce bot, if not return validAnnouncer := s.isValidAnnouncer(*announcer) if !validAnnouncer { @@ -358,6 +392,13 @@ func (s *Handler) sendToAnnounceProcessor(channel string, msg string) error { return err } + v, ok := s.channelHealth[channel] + if !ok { + return nil + } + + v.SetLastAnnounce() + return nil } @@ -392,11 +433,16 @@ func (s *Handler) HandleJoinChannel(channel string, password string) error { err := s.client.Write(m.String()) if err != nil { - log.Error().Err(err).Msgf("error handling join: %v", m.String()) + log.Error().Stack().Err(err).Msgf("error handling join: %v", m.String()) return err } - //log.Info().Msgf("Monitoring channel %v %s", s.network.Name, channel) + // only set values if channel is found in map + v, ok := s.channelHealth[channel] + if ok { + v.monitoring = true + v.monitoringSince = time.Now() + } return nil } @@ -570,6 +616,15 @@ func (s *Handler) isValidAnnouncer(nick string) bool { return true } +func (s *Handler) isValidChannel(channel string) bool { + _, ok := s.validChannels[channel] + if !ok { + return false + } + + return true +} + func (s *Handler) setLastAnnounce() { s.lastAnnounce = time.Now() } @@ -578,6 +633,14 @@ func (s *Handler) GetLastAnnounce() time.Time { return s.lastAnnounce } +//func (s *Handler) setConnectedSince() { +// s.network.ConnectedSince = time.Now() +//} +// +//func (s *Handler) GetConnectedSince() time.Time { +// return s.lastAnnounce +//} + func (s *Handler) setLastPing() { s.lastPing = time.Now() } @@ -586,6 +649,10 @@ func (s *Handler) GetLastPing() time.Time { return s.lastPing } +func (s *Handler) GetChannelHealth() map[string]*channelHealth { + return s.channelHealth +} + // irc line can contain lots of extra stuff like color so lets clean that func cleanMessage(message string) string { var regexMessageClean = `\x0f|\x1f|\x02|\x03(?:[\d]{1,2}(?:,[\d]{1,2})?)?` diff --git a/internal/irc/service.go b/internal/irc/service.go index 57188c0..a37d372 100644 --- a/internal/irc/service.go +++ b/internal/irc/service.go @@ -3,6 +3,7 @@ package irc import ( "context" "fmt" + "strings" "sync" "github.com/autobrr/autobrr/internal/domain" @@ -18,6 +19,7 @@ type Service interface { StopHandlers() StopNetwork(name string) error ListNetworks(ctx context.Context) ([]domain.IrcNetwork, error) + GetNetworksWithHealth(ctx context.Context) ([]domain.IrcNetworkWithHealth, error) GetNetworkByID(id int64) (*domain.IrcNetwork, error) DeleteNetwork(ctx context.Context, id int64) error StoreNetwork(ctx context.Context, network *domain.IrcNetwork) error @@ -349,12 +351,86 @@ func (s *service) ListNetworks(ctx context.Context) ([]domain.IrcNetwork, error) } n.Channels = append(n.Channels, channels...) + // TODO get handler channelHealth + ret = append(ret, n) } return ret, nil } +func (s *service) GetNetworksWithHealth(ctx context.Context) ([]domain.IrcNetworkWithHealth, error) { + networks, err := s.repo.ListNetworks(ctx) + if err != nil { + log.Error().Err(err).Msgf("failed to list networks: %v", err) + return nil, err + } + + var ret []domain.IrcNetworkWithHealth + + 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, + } + + handler, ok := s.handlers[n.Server] + if ok { + // only set connected and connected since if we have an active handler and connection + if handler.conn != nil { + netw.Connected = handler.connected + netw.ConnectedSince = handler.connectedSince + } + } + + channels, err := s.repo.ListChannels(n.ID) + if err != nil { + log.Error().Msgf("failed to list channels for network %q: %v", n.Server, err) + return nil, err + } + + // combine from repo and handler + for _, channel := range channels { + ch := domain.ChannelWithHealth{ + ID: channel.ID, + Enabled: channel.Enabled, + Name: channel.Name, + Password: channel.Password, + Detached: channel.Detached, + //Monitoring: false, + //MonitoringSince: time.Time{}, + //LastAnnounce: time.Time{}, + } + + // only check if we have a handler + if handler != nil { + name := strings.ToLower(channel.Name) + + chan1, ok := handler.channelHealth[name] + if ok { + ch.Monitoring = chan1.monitoring + ch.MonitoringSince = chan1.monitoringSince + ch.LastAnnounce = chan1.lastAnnounce + } + } + + netw.Channels = append(netw.Channels, ch) + } + + ret = append(ret, netw) + } + + return ret, nil +} + func (s *service) DeleteNetwork(ctx context.Context, id int64) error { network, err := s.GetNetworkByID(id) if err != nil { diff --git a/web/src/screens/settings/Irc.tsx b/web/src/screens/settings/Irc.tsx index d29da13..61cf4bc 100644 --- a/web/src/screens/settings/Irc.tsx +++ b/web/src/screens/settings/Irc.tsx @@ -134,7 +134,17 @@ const ListItem = ({ idx, network }: any) => { /> - {network.name} + + + {network.connected ? ( + + + + + ) : } + {network.name} + + {network.server}:{network.port} {network.tls && TLS} {network.nickserv?.account} From 140fc9739839c0dd0c55a001845c091de56ca00a Mon Sep 17 00:00:00 2001 From: Ludvig Lundgren Date: Sun, 9 Jan 2022 02:41:19 +0100 Subject: [PATCH 2/3] feat: show irc channel status --- internal/irc/handler.go | 35 ++--- web/src/screens/Releases.tsx | 2 +- web/src/screens/settings/Irc.tsx | 237 +++++++++++++++++++------------ 3 files changed, 155 insertions(+), 119 deletions(-) diff --git a/internal/irc/handler.go b/internal/irc/handler.go index d4fef7a..140b0bb 100644 --- a/internal/irc/handler.go +++ b/internal/irc/handler.go @@ -30,10 +30,17 @@ type channelHealth struct { lastAnnounce time.Time } +// SetLastAnnounce set last announce to now func (h *channelHealth) SetLastAnnounce() { h.lastAnnounce = time.Now() } +// SetMonitoring set monitoring and time +func (h *channelHealth) SetMonitoring() { + h.monitoring = true + h.monitoringSince = time.Now() +} + type Handler struct { network *domain.IrcNetwork filterService filter.Service @@ -48,7 +55,6 @@ type Handler struct { cancel context.CancelFunc lastPing time.Time - lastAnnounce time.Time connected bool connectedSince time.Time // tODO disconnectedTime @@ -366,8 +372,6 @@ func (s *Handler) onMessage(msg *irc.Message) error { cleanedMsg := cleanMessage(message) log.Debug().Msgf("%v: %v %v: %v", s.network.Server, *channel, *announcer, cleanedMsg) - s.setLastAnnounce() - if err := s.sendToAnnounceProcessor(*channel, cleanedMsg); err != nil { log.Error().Stack().Err(err).Msgf("could not queue line: %v", cleanedMsg) return err @@ -440,8 +444,7 @@ func (s *Handler) HandleJoinChannel(channel string, password string) error { // only set values if channel is found in map v, ok := s.channelHealth[channel] if ok { - v.monitoring = true - v.monitoringSince = time.Now() + v.SetMonitoring() } return nil @@ -607,6 +610,7 @@ func (s *Handler) handlePing(msg *irc.Message) error { return nil } +// check if announcer is one from the list in the definition func (s *Handler) isValidAnnouncer(nick string) bool { _, ok := s.validAnnouncers[nick] if !ok { @@ -616,6 +620,7 @@ func (s *Handler) isValidAnnouncer(nick string) bool { return true } +// check if channel is one from the list in the definition func (s *Handler) isValidChannel(channel string) bool { _, ok := s.validChannels[channel] if !ok { @@ -625,22 +630,6 @@ func (s *Handler) isValidChannel(channel string) bool { return true } -func (s *Handler) setLastAnnounce() { - s.lastAnnounce = time.Now() -} - -func (s *Handler) GetLastAnnounce() time.Time { - return s.lastAnnounce -} - -//func (s *Handler) setConnectedSince() { -// s.network.ConnectedSince = time.Now() -//} -// -//func (s *Handler) GetConnectedSince() time.Time { -// return s.lastAnnounce -//} - func (s *Handler) setLastPing() { s.lastPing = time.Now() } @@ -649,10 +638,6 @@ func (s *Handler) GetLastPing() time.Time { return s.lastPing } -func (s *Handler) GetChannelHealth() map[string]*channelHealth { - return s.channelHealth -} - // irc line can contain lots of extra stuff like color so lets clean that func cleanMessage(message string) string { var regexMessageClean = `\x0f|\x1f|\x02|\x03(?:[\d]{1,2}(?:,[\d]{1,2})?)?` diff --git a/web/src/screens/Releases.tsx b/web/src/screens/Releases.tsx index a46de84..d49b2e2 100644 --- a/web/src/screens/Releases.tsx +++ b/web/src/screens/Releases.tsx @@ -140,7 +140,7 @@ export function ReleaseStatusCell({ value, column, row }: ReleaseStatusCellProps } return (
- {value.map(v =>
{statusMap[v.status]}
)} + {value.map((v, idx) =>
{statusMap[v.status]}
)}
) } diff --git a/web/src/screens/settings/Irc.tsx b/web/src/screens/settings/Irc.tsx index 61cf4bc..0101818 100644 --- a/web/src/screens/settings/Irc.tsx +++ b/web/src/screens/settings/Irc.tsx @@ -2,21 +2,56 @@ import { useEffect } from "react"; import { IrcNetworkAddForm, IrcNetworkUpdateForm } from "../../forms"; import { useToggle } from "../../hooks/hooks"; import { useQuery } from "react-query"; -import { Switch } from "@headlessui/react"; -import { classNames } from "../../utils"; import { EmptySimple } from "../../components/emptystates"; import APIClient from "../../api/APIClient"; +import { formatDistanceToNowStrict, formatISO9075 } from "date-fns"; interface IrcNetwork { id: number; name: string; enabled: boolean; addr: string; + server: string; + port: string; nick: string; username: string; realname: string; pass: string; - // connect_commands: string; + connected: boolean; + connected_since: string; + tls: boolean; + nickserv: { + account: string; + } + channels: Channel[] +} + +interface Channel { + id: number; + enabled: boolean; + name: string; + password: string; + detached: boolean; + monitoring: boolean; + monitoring_since: string; + last_announce: string; +} + +function IsEmptyDate(date: string) { + if (date !== "0001-01-01T00:00:00Z") { + return formatDistanceToNowStrict( + new Date(date), + { addSuffix: true } + ) + } + return "n/a" +} + +function simplifyDate(date: string) { + if (date !== "0001-01-01T00:00:00Z") { + return formatISO9075(new Date(date)) + } + return "n/a" } function IrcSettings() { @@ -25,7 +60,7 @@ function IrcSettings() { useEffect(() => { }, []); - const { data } = useQuery('networks', APIClient.irc.getNetworks, + const { data } = useQuery('networks', APIClient.irc.getNetworks, { refetchOnWindowFocus: false } @@ -40,7 +75,7 @@ function IrcSettings() {

IRC

- IRC networks and channels. + IRC networks and channels. Click on a network to view channel status.

@@ -54,106 +89,122 @@ function IrcSettings() {
-
- {data && data.length > 0 ? -
-
-
- - - - - - - - - - - - {data && data.map((network: IrcNetwork, idx) => ( - - ))} - -
- Enabled - - Network - - Server - - Nick - - Edit -
-
-
-
- : - } -
+ {data && data.length > 0 ? +
+
    +
  1. + {/*
    Enabled
    */} +
    Network
    +
    Server
    +
    Nick
    +
  2. + + {data && data.map((network: IrcNetwork, idx) => ( + + ))} +
+
+ : } ) } -const ListItem = ({ idx, network }: any) => { +interface LiItemProps { + idx: number; + network: IrcNetwork; +} + +const LiItem = ({ idx, network }: LiItemProps) => { const [updateIsOpen, toggleUpdate] = useToggle(false) + const [edit, toggleEdit] = useToggle(false); return ( - - - - - Enable -