mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 08:49:13 +00:00
feat(irc): support optional bot mode (#1246)
* feat(irc): set bot mode when the server supports it See https://ircv3.net/specs/extensions/bot-mode. * feat(irc): add a config option per network for bot mode
This commit is contained in:
parent
c6c74c7f3b
commit
f89a25d645
8 changed files with 67 additions and 10 deletions
|
@ -30,7 +30,7 @@ func NewIrcRepo(log logger.Logger, db *DB) domain.IrcRepo {
|
||||||
|
|
||||||
func (r *IrcRepo) GetNetworkByID(ctx context.Context, id int64) (*domain.IrcNetwork, error) {
|
func (r *IrcRepo) GetNetworkByID(ctx context.Context, id int64) (*domain.IrcNetwork, error) {
|
||||||
queryBuilder := r.db.squirrel.
|
queryBuilder := r.db.squirrel.
|
||||||
Select("id", "enabled", "name", "server", "port", "tls", "pass", "nick", "auth_mechanism", "auth_account", "auth_password", "invite_command", "bouncer_addr", "use_bouncer").
|
Select("id", "enabled", "name", "server", "port", "tls", "pass", "nick", "auth_mechanism", "auth_account", "auth_password", "invite_command", "bouncer_addr", "use_bouncer", "bot_mode").
|
||||||
From("irc_network").
|
From("irc_network").
|
||||||
Where(sq.Eq{"id": id})
|
Where(sq.Eq{"id": id})
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ func (r *IrcRepo) GetNetworkByID(ctx context.Context, id int64) (*domain.IrcNetw
|
||||||
var tls sql.NullBool
|
var tls sql.NullBool
|
||||||
|
|
||||||
row := r.db.handler.QueryRowContext(ctx, query, args...)
|
row := r.db.handler.QueryRowContext(ctx, query, args...)
|
||||||
if err := row.Scan(&n.ID, &n.Enabled, &n.Name, &n.Server, &n.Port, &tls, &pass, &nick, &n.Auth.Mechanism, &account, &password, &inviteCmd, &bouncerAddr, &n.UseBouncer); err != nil {
|
if err := row.Scan(&n.ID, &n.Enabled, &n.Name, &n.Server, &n.Port, &tls, &pass, &nick, &n.Auth.Mechanism, &account, &password, &inviteCmd, &bouncerAddr, &n.UseBouncer, &n.BotMode); err != nil {
|
||||||
return nil, errors.Wrap(err, "error scanning row")
|
return nil, errors.Wrap(err, "error scanning row")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,7 +107,7 @@ func (r *IrcRepo) DeleteNetwork(ctx context.Context, id int64) error {
|
||||||
|
|
||||||
func (r *IrcRepo) FindActiveNetworks(ctx context.Context) ([]domain.IrcNetwork, error) {
|
func (r *IrcRepo) FindActiveNetworks(ctx context.Context) ([]domain.IrcNetwork, error) {
|
||||||
queryBuilder := r.db.squirrel.
|
queryBuilder := r.db.squirrel.
|
||||||
Select("id", "enabled", "name", "server", "port", "tls", "pass", "nick", "auth_mechanism", "auth_account", "auth_password", "invite_command", "bouncer_addr", "use_bouncer").
|
Select("id", "enabled", "name", "server", "port", "tls", "pass", "nick", "auth_mechanism", "auth_account", "auth_password", "invite_command", "bouncer_addr", "use_bouncer", "bot_mode").
|
||||||
From("irc_network").
|
From("irc_network").
|
||||||
Where(sq.Eq{"enabled": true})
|
Where(sq.Eq{"enabled": true})
|
||||||
|
|
||||||
|
@ -131,7 +131,7 @@ func (r *IrcRepo) FindActiveNetworks(ctx context.Context) ([]domain.IrcNetwork,
|
||||||
var account, password sql.NullString
|
var account, password sql.NullString
|
||||||
var tls sql.NullBool
|
var tls sql.NullBool
|
||||||
|
|
||||||
if err := rows.Scan(&net.ID, &net.Enabled, &net.Name, &net.Server, &net.Port, &tls, &pass, &nick, &net.Auth.Mechanism, &account, &password, &inviteCmd, &bouncerAddr, &net.UseBouncer); err != nil {
|
if err := rows.Scan(&net.ID, &net.Enabled, &net.Name, &net.Server, &net.Port, &tls, &pass, &nick, &net.Auth.Mechanism, &account, &password, &inviteCmd, &bouncerAddr, &net.UseBouncer, &net.BotMode); err != nil {
|
||||||
return nil, errors.Wrap(err, "error scanning row")
|
return nil, errors.Wrap(err, "error scanning row")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,7 +155,7 @@ func (r *IrcRepo) FindActiveNetworks(ctx context.Context) ([]domain.IrcNetwork,
|
||||||
|
|
||||||
func (r *IrcRepo) ListNetworks(ctx context.Context) ([]domain.IrcNetwork, error) {
|
func (r *IrcRepo) ListNetworks(ctx context.Context) ([]domain.IrcNetwork, error) {
|
||||||
queryBuilder := r.db.squirrel.
|
queryBuilder := r.db.squirrel.
|
||||||
Select("id", "enabled", "name", "server", "port", "tls", "pass", "nick", "auth_mechanism", "auth_account", "auth_password", "invite_command", "bouncer_addr", "use_bouncer").
|
Select("id", "enabled", "name", "server", "port", "tls", "pass", "nick", "auth_mechanism", "auth_account", "auth_password", "invite_command", "bouncer_addr", "use_bouncer", "bot_mode").
|
||||||
From("irc_network").
|
From("irc_network").
|
||||||
OrderBy("name ASC")
|
OrderBy("name ASC")
|
||||||
|
|
||||||
|
@ -179,7 +179,7 @@ func (r *IrcRepo) ListNetworks(ctx context.Context) ([]domain.IrcNetwork, error)
|
||||||
var account, password sql.NullString
|
var account, password sql.NullString
|
||||||
var tls sql.NullBool
|
var tls sql.NullBool
|
||||||
|
|
||||||
if err := rows.Scan(&net.ID, &net.Enabled, &net.Name, &net.Server, &net.Port, &tls, &pass, &nick, &net.Auth.Mechanism, &account, &password, &inviteCmd, &bouncerAddr, &net.UseBouncer); err != nil {
|
if err := rows.Scan(&net.ID, &net.Enabled, &net.Name, &net.Server, &net.Port, &tls, &pass, &nick, &net.Auth.Mechanism, &account, &password, &inviteCmd, &bouncerAddr, &net.UseBouncer, &net.BotMode); err != nil {
|
||||||
return nil, errors.Wrap(err, "error scanning row")
|
return nil, errors.Wrap(err, "error scanning row")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -240,7 +240,7 @@ func (r *IrcRepo) ListChannels(networkID int64) ([]domain.IrcChannel, error) {
|
||||||
|
|
||||||
func (r *IrcRepo) CheckExistingNetwork(ctx context.Context, network *domain.IrcNetwork) (*domain.IrcNetwork, error) {
|
func (r *IrcRepo) CheckExistingNetwork(ctx context.Context, network *domain.IrcNetwork) (*domain.IrcNetwork, error) {
|
||||||
queryBuilder := r.db.squirrel.
|
queryBuilder := r.db.squirrel.
|
||||||
Select("id", "enabled", "name", "server", "port", "tls", "pass", "nick", "auth_mechanism", "auth_account", "auth_password", "invite_command", "bouncer_addr", "use_bouncer").
|
Select("id", "enabled", "name", "server", "port", "tls", "pass", "nick", "auth_mechanism", "auth_account", "auth_password", "invite_command", "bouncer_addr", "use_bouncer", "bot_mode").
|
||||||
From("irc_network").
|
From("irc_network").
|
||||||
Where(sq.Eq{"server": network.Server}).
|
Where(sq.Eq{"server": network.Server}).
|
||||||
Where(sq.Eq{"port": network.Port}).
|
Where(sq.Eq{"port": network.Port}).
|
||||||
|
@ -260,7 +260,7 @@ func (r *IrcRepo) CheckExistingNetwork(ctx context.Context, network *domain.IrcN
|
||||||
var account, password sql.NullString
|
var account, password sql.NullString
|
||||||
var tls sql.NullBool
|
var tls sql.NullBool
|
||||||
|
|
||||||
if err = row.Scan(&net.ID, &net.Enabled, &net.Name, &net.Server, &net.Port, &tls, &pass, &nick, &net.Auth.Mechanism, &account, &password, &inviteCmd, &bouncerAddr, &net.UseBouncer); err != nil {
|
if err = row.Scan(&net.ID, &net.Enabled, &net.Name, &net.Server, &net.Port, &tls, &pass, &nick, &net.Auth.Mechanism, &account, &password, &inviteCmd, &bouncerAddr, &net.UseBouncer, &net.BotMode); err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
// no result is not an error in our case
|
// no result is not an error in our case
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
@ -308,6 +308,7 @@ func (r *IrcRepo) StoreNetwork(ctx context.Context, network *domain.IrcNetwork)
|
||||||
"invite_command",
|
"invite_command",
|
||||||
"bouncer_addr",
|
"bouncer_addr",
|
||||||
"use_bouncer",
|
"use_bouncer",
|
||||||
|
"bot_mode",
|
||||||
).
|
).
|
||||||
Values(
|
Values(
|
||||||
network.Enabled,
|
network.Enabled,
|
||||||
|
@ -323,6 +324,7 @@ func (r *IrcRepo) StoreNetwork(ctx context.Context, network *domain.IrcNetwork)
|
||||||
inviteCmd,
|
inviteCmd,
|
||||||
bouncerAddr,
|
bouncerAddr,
|
||||||
network.UseBouncer,
|
network.UseBouncer,
|
||||||
|
network.BotMode,
|
||||||
).
|
).
|
||||||
Suffix("RETURNING id").
|
Suffix("RETURNING id").
|
||||||
RunWith(r.db.handler)
|
RunWith(r.db.handler)
|
||||||
|
@ -363,6 +365,7 @@ func (r *IrcRepo) UpdateNetwork(ctx context.Context, network *domain.IrcNetwork)
|
||||||
Set("invite_command", inviteCmd).
|
Set("invite_command", inviteCmd).
|
||||||
Set("bouncer_addr", bouncerAddr).
|
Set("bouncer_addr", bouncerAddr).
|
||||||
Set("use_bouncer", network.UseBouncer).
|
Set("use_bouncer", network.UseBouncer).
|
||||||
|
Set("bot_mode", network.BotMode).
|
||||||
Set("updated_at", time.Now().Format(time.RFC3339)).
|
Set("updated_at", time.Now().Format(time.RFC3339)).
|
||||||
Where(sq.Eq{"id": network.ID})
|
Where(sq.Eq{"id": network.ID})
|
||||||
|
|
||||||
|
|
|
@ -47,6 +47,7 @@ CREATE TABLE irc_network
|
||||||
invite_command TEXT,
|
invite_command TEXT,
|
||||||
use_bouncer BOOLEAN,
|
use_bouncer BOOLEAN,
|
||||||
bouncer_addr TEXT,
|
bouncer_addr TEXT,
|
||||||
|
bot_mode BOOLEAN DEFAULT FALSE,
|
||||||
connected BOOLEAN,
|
connected BOOLEAN,
|
||||||
connected_since TIMESTAMP,
|
connected_since TIMESTAMP,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
@ -827,5 +828,8 @@ ALTER TABLE filter_external
|
||||||
`,
|
`,
|
||||||
`ALTER TABLE filter_external
|
`ALTER TABLE filter_external
|
||||||
DROP COLUMN IF EXISTS webhook_retry_max_jitter_seconds;
|
DROP COLUMN IF EXISTS webhook_retry_max_jitter_seconds;
|
||||||
|
`,
|
||||||
|
`ALTER TABLE irc_network
|
||||||
|
ADD COLUMN bot_mode BOOLEAN DEFAULT FALSE;
|
||||||
`,
|
`,
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,6 +47,7 @@ CREATE TABLE irc_network
|
||||||
invite_command TEXT,
|
invite_command TEXT,
|
||||||
use_bouncer BOOLEAN,
|
use_bouncer BOOLEAN,
|
||||||
bouncer_addr TEXT,
|
bouncer_addr TEXT,
|
||||||
|
bot_mode BOOLEAN DEFAULT FALSE,
|
||||||
connected BOOLEAN,
|
connected BOOLEAN,
|
||||||
connected_since TIMESTAMP,
|
connected_since TIMESTAMP,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
@ -1421,4 +1422,7 @@ ALTER TABLE filter_external_dg_tmp
|
||||||
`ALTER TABLE filter_external
|
`ALTER TABLE filter_external
|
||||||
DROP COLUMN webhook_retry_max_jitter_seconds;
|
DROP COLUMN webhook_retry_max_jitter_seconds;
|
||||||
`,
|
`,
|
||||||
|
`ALTER TABLE irc_network
|
||||||
|
ADD COLUMN bot_mode BOOLEAN DEFAULT FALSE;
|
||||||
|
`,
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,7 @@ type IrcNetwork struct {
|
||||||
InviteCommand string `json:"invite_command"`
|
InviteCommand string `json:"invite_command"`
|
||||||
UseBouncer bool `json:"use_bouncer"`
|
UseBouncer bool `json:"use_bouncer"`
|
||||||
BouncerAddr string `json:"bouncer_addr"`
|
BouncerAddr string `json:"bouncer_addr"`
|
||||||
|
BotMode bool `json:"bot_mode"`
|
||||||
Channels []IrcChannel `json:"channels"`
|
Channels []IrcChannel `json:"channels"`
|
||||||
Connected bool `json:"connected"`
|
Connected bool `json:"connected"`
|
||||||
ConnectedSince *time.Time `json:"connected_since"`
|
ConnectedSince *time.Time `json:"connected_since"`
|
||||||
|
@ -63,6 +64,7 @@ type IrcNetworkWithHealth struct {
|
||||||
InviteCommand string `json:"invite_command"`
|
InviteCommand string `json:"invite_command"`
|
||||||
UseBouncer bool `json:"use_bouncer"`
|
UseBouncer bool `json:"use_bouncer"`
|
||||||
BouncerAddr string `json:"bouncer_addr"`
|
BouncerAddr string `json:"bouncer_addr"`
|
||||||
|
BotMode bool `json:"bot_mode"`
|
||||||
CurrentNick string `json:"current_nick"`
|
CurrentNick string `json:"current_nick"`
|
||||||
PreferredNick string `json:"preferred_nick"`
|
PreferredNick string `json:"preferred_nick"`
|
||||||
Channels []ChannelWithHealth `json:"channels"`
|
Channels []ChannelWithHealth `json:"channels"`
|
||||||
|
|
|
@ -85,6 +85,8 @@ type Handler struct {
|
||||||
connectionErrors []string
|
connectionErrors []string
|
||||||
failedNickServAttempts int
|
failedNickServAttempts int
|
||||||
|
|
||||||
|
botModeChar string
|
||||||
|
|
||||||
authenticated bool
|
authenticated bool
|
||||||
saslauthed bool
|
saslauthed bool
|
||||||
}
|
}
|
||||||
|
@ -201,6 +203,9 @@ func (h *Handler) Run() error {
|
||||||
h.client.AddDisconnectCallback(h.onDisconnect)
|
h.client.AddDisconnectCallback(h.onDisconnect)
|
||||||
|
|
||||||
h.client.AddCallback("MODE", h.handleMode)
|
h.client.AddCallback("MODE", h.handleMode)
|
||||||
|
if h.network.BotMode {
|
||||||
|
h.client.AddCallback("501", h.handleModeUnknownFlag)
|
||||||
|
}
|
||||||
h.client.AddCallback("INVITE", h.handleInvite)
|
h.client.AddCallback("INVITE", h.handleInvite)
|
||||||
h.client.AddCallback("366", h.handleJoined)
|
h.client.AddCallback("366", h.handleJoined)
|
||||||
h.client.AddCallback("PART", h.handlePart)
|
h.client.AddCallback("PART", h.handlePart)
|
||||||
|
@ -383,8 +388,13 @@ func (h *Handler) onConnect(m ircmsg.Message) {
|
||||||
|
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
h.authenticate()
|
if h.network.BotMode && h.botModeSupported() {
|
||||||
|
// if we set Bot Mode, we'll try to authenticate after the MODE response
|
||||||
|
h.setBotMode()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.authenticate()
|
||||||
}
|
}
|
||||||
|
|
||||||
// onDisconnect is the disconnect callback
|
// onDisconnect is the disconnect callback
|
||||||
|
@ -492,6 +502,20 @@ func (h *Handler) handleNickServ(msg ircmsg.Message) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// botModeSupported checks if IRCv3 Bot Mode is supported by the server
|
||||||
|
// See https://ircv3.net/specs/extensions/bot-mode
|
||||||
|
func (h *Handler) botModeSupported() bool {
|
||||||
|
h.botModeChar = h.client.ISupport()["BOT"]
|
||||||
|
|
||||||
|
return h.botModeChar != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// setBotMode attempts to set Bot Mode on ourself
|
||||||
|
// See https://ircv3.net/specs/extensions/bot-mode
|
||||||
|
func (h *Handler) setBotMode() {
|
||||||
|
h.client.Send("MODE", h.CurrentNick(), "+"+h.botModeChar)
|
||||||
|
}
|
||||||
|
|
||||||
// authenticate sends NickServIdentify if not authenticated
|
// authenticate sends NickServIdentify if not authenticated
|
||||||
func (h *Handler) authenticate() bool {
|
func (h *Handler) authenticate() bool {
|
||||||
h.m.RLock()
|
h.m.RLock()
|
||||||
|
@ -869,7 +893,15 @@ func (h *Handler) handleMode(msg ircmsg.Message) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
if h.network.BotMode && h.botModeChar != "" && h.isOurCurrentNick(msg.Params[0]) && strings.Contains(msg.Params[1], "+"+h.botModeChar) {
|
||||||
|
h.authenticate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// listens for ERR_UMODEUNKNOWNFLAG events
|
||||||
|
func (h *Handler) handleModeUnknownFlag(msg ircmsg.Message) {
|
||||||
|
// if Bot Mode setting failed, still try to authenticate
|
||||||
|
h.authenticate()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) SendMsg(channel, msg string) error {
|
func (h *Handler) SendMsg(channel, msg string) error {
|
||||||
|
|
|
@ -211,6 +211,10 @@ func (s *service) checkIfNetworkRestartNeeded(network *domain.IrcNetwork) error
|
||||||
restartNeeded = true
|
restartNeeded = true
|
||||||
fieldsChanged = append(fieldsChanged, "bouncer addr")
|
fieldsChanged = append(fieldsChanged, "bouncer addr")
|
||||||
}
|
}
|
||||||
|
if handler.BotMode != network.BotMode {
|
||||||
|
restartNeeded = true
|
||||||
|
fieldsChanged = append(fieldsChanged, "bot mode")
|
||||||
|
}
|
||||||
if handler.Auth.Mechanism != network.Auth.Mechanism {
|
if handler.Auth.Mechanism != network.Auth.Mechanism {
|
||||||
restartNeeded = true
|
restartNeeded = true
|
||||||
fieldsChanged = append(fieldsChanged, "auth mechanism")
|
fieldsChanged = append(fieldsChanged, "auth mechanism")
|
||||||
|
@ -447,6 +451,7 @@ func (s *service) GetNetworksWithHealth(ctx context.Context) ([]domain.IrcNetwor
|
||||||
InviteCommand: n.InviteCommand,
|
InviteCommand: n.InviteCommand,
|
||||||
BouncerAddr: n.BouncerAddr,
|
BouncerAddr: n.BouncerAddr,
|
||||||
UseBouncer: n.UseBouncer,
|
UseBouncer: n.UseBouncer,
|
||||||
|
BotMode: n.BotMode,
|
||||||
Connected: false,
|
Connected: false,
|
||||||
Channels: []domain.ChannelWithHealth{},
|
Channels: []domain.ChannelWithHealth{},
|
||||||
ConnectionErrors: []string{},
|
ConnectionErrors: []string{},
|
||||||
|
|
|
@ -268,6 +268,7 @@ interface IrcNetworkUpdateFormValues {
|
||||||
invite_command: string;
|
invite_command: string;
|
||||||
use_bouncer: boolean;
|
use_bouncer: boolean;
|
||||||
bouncer_addr: string;
|
bouncer_addr: string;
|
||||||
|
bot_mode: boolean;
|
||||||
channels: Array<IrcChannel>;
|
channels: Array<IrcChannel>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -323,6 +324,7 @@ export function IrcNetworkUpdateForm({
|
||||||
invite_command: network.invite_command,
|
invite_command: network.invite_command,
|
||||||
use_bouncer: network.use_bouncer,
|
use_bouncer: network.use_bouncer,
|
||||||
bouncer_addr: network.bouncer_addr,
|
bouncer_addr: network.bouncer_addr,
|
||||||
|
bot_mode: network.bot_mode,
|
||||||
channels: network.channels
|
channels: network.channels
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -384,6 +386,8 @@ export function IrcNetworkUpdateForm({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<SwitchGroupWide name="bot_mode" label="IRCv3 Bot Mode" />
|
||||||
|
|
||||||
<div className="border-t border-gray-200 dark:border-gray-700 py-5">
|
<div className="border-t border-gray-200 dark:border-gray-700 py-5">
|
||||||
<div className="px-4 space-y-1 mb-8">
|
<div className="px-4 space-y-1 mb-8">
|
||||||
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Identification</Dialog.Title>
|
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Identification</Dialog.Title>
|
||||||
|
|
3
web/src/types/Irc.d.ts
vendored
3
web/src/types/Irc.d.ts
vendored
|
@ -16,6 +16,7 @@ interface IrcNetwork {
|
||||||
invite_command: string;
|
invite_command: string;
|
||||||
use_bouncer: boolean;
|
use_bouncer: boolean;
|
||||||
bouncer_addr: string;
|
bouncer_addr: string;
|
||||||
|
bot_mode: boolean;
|
||||||
channels: IrcChannel[];
|
channels: IrcChannel[];
|
||||||
connected: boolean;
|
connected: boolean;
|
||||||
connected_since: string;
|
connected_since: string;
|
||||||
|
@ -33,6 +34,7 @@ interface IrcNetworkCreate {
|
||||||
invite_command: string;
|
invite_command: string;
|
||||||
use_bouncer?: boolean;
|
use_bouncer?: boolean;
|
||||||
bouncer_addr?: string;
|
bouncer_addr?: string;
|
||||||
|
bot_mode?: boolean;
|
||||||
channels: IrcChannel[];
|
channels: IrcChannel[];
|
||||||
connected: boolean;
|
connected: boolean;
|
||||||
}
|
}
|
||||||
|
@ -64,6 +66,7 @@ interface IrcNetworkWithHealth {
|
||||||
invite_command: string;
|
invite_command: string;
|
||||||
use_bouncer: boolean;
|
use_bouncer: boolean;
|
||||||
bouncer_addr: string;
|
bouncer_addr: string;
|
||||||
|
bot_mode: boolean;
|
||||||
channels: IrcChannelWithHealth[];
|
channels: IrcChannelWithHealth[];
|
||||||
connected: boolean;
|
connected: boolean;
|
||||||
connected_since: string;
|
connected_since: string;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue