feat(irc): add bouncer/znc support (#951)

* feat(irc): add initial bouncer support

* feat(irc): add bouncer fields to irc update form

* fix: make fields optional

* feat(db): add migrations
This commit is contained in:
ze0s 2023-06-14 21:06:28 +02:00 committed by GitHub
parent 28f0b878e1
commit 2677c16ff8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 79 additions and 29 deletions

View file

@ -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"). Select("id", "enabled", "name", "server", "port", "tls", "pass", "nick", "auth_mechanism", "auth_account", "auth_password", "invite_command", "bouncer_addr", "use_bouncer").
From("irc_network"). From("irc_network").
Where(sq.Eq{"id": id}) Where(sq.Eq{"id": id})
@ -42,12 +42,12 @@ func (r *IrcRepo) GetNetworkByID(ctx context.Context, id int64) (*domain.IrcNetw
var n domain.IrcNetwork var n domain.IrcNetwork
var pass, nick, inviteCmd sql.NullString var pass, nick, inviteCmd, bouncerAddr sql.NullString
var account, password sql.NullString var account, password sql.NullString
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); 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); err != nil {
return nil, errors.Wrap(err, "error scanning row") return nil, errors.Wrap(err, "error scanning row")
} }
@ -57,6 +57,7 @@ func (r *IrcRepo) GetNetworkByID(ctx context.Context, id int64) (*domain.IrcNetw
n.InviteCommand = inviteCmd.String n.InviteCommand = inviteCmd.String
n.Auth.Account = account.String n.Auth.Account = account.String
n.Auth.Password = password.String n.Auth.Password = password.String
n.BouncerAddr = bouncerAddr.String
return &n, nil return &n, nil
} }
@ -106,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"). Select("id", "enabled", "name", "server", "port", "tls", "pass", "nick", "auth_mechanism", "auth_account", "auth_password", "invite_command", "bouncer_addr", "use_bouncer").
From("irc_network"). From("irc_network").
Where(sq.Eq{"enabled": true}) Where(sq.Eq{"enabled": true})
@ -126,11 +127,11 @@ func (r *IrcRepo) FindActiveNetworks(ctx context.Context) ([]domain.IrcNetwork,
for rows.Next() { for rows.Next() {
var net domain.IrcNetwork var net domain.IrcNetwork
var pass, nick, inviteCmd sql.NullString var pass, nick, inviteCmd, bouncerAddr sql.NullString
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); 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); err != nil {
return nil, errors.Wrap(err, "error scanning row") return nil, errors.Wrap(err, "error scanning row")
} }
@ -138,6 +139,7 @@ func (r *IrcRepo) FindActiveNetworks(ctx context.Context) ([]domain.IrcNetwork,
net.Pass = pass.String net.Pass = pass.String
net.Nick = nick.String net.Nick = nick.String
net.InviteCommand = inviteCmd.String net.InviteCommand = inviteCmd.String
net.BouncerAddr = bouncerAddr.String
net.Auth.Account = account.String net.Auth.Account = account.String
net.Auth.Password = password.String net.Auth.Password = password.String
@ -153,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"). Select("id", "enabled", "name", "server", "port", "tls", "pass", "nick", "auth_mechanism", "auth_account", "auth_password", "invite_command", "bouncer_addr", "use_bouncer").
From("irc_network"). From("irc_network").
OrderBy("name ASC") OrderBy("name ASC")
@ -173,11 +175,11 @@ func (r *IrcRepo) ListNetworks(ctx context.Context) ([]domain.IrcNetwork, error)
for rows.Next() { for rows.Next() {
var net domain.IrcNetwork var net domain.IrcNetwork
var pass, nick, inviteCmd sql.NullString var pass, nick, inviteCmd, bouncerAddr sql.NullString
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); 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); err != nil {
return nil, errors.Wrap(err, "error scanning row") return nil, errors.Wrap(err, "error scanning row")
} }
@ -185,6 +187,7 @@ func (r *IrcRepo) ListNetworks(ctx context.Context) ([]domain.IrcNetwork, error)
net.Pass = pass.String net.Pass = pass.String
net.Nick = nick.String net.Nick = nick.String
net.InviteCommand = inviteCmd.String net.InviteCommand = inviteCmd.String
net.BouncerAddr = bouncerAddr.String
net.Auth.Account = account.String net.Auth.Account = account.String
net.Auth.Password = password.String net.Auth.Password = password.String
@ -237,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"). Select("id", "enabled", "name", "server", "port", "tls", "pass", "nick", "auth_mechanism", "auth_account", "auth_password", "invite_command", "bouncer_addr", "use_bouncer").
From("irc_network"). From("irc_network").
Where(sq.Eq{"server": network.Server}). Where(sq.Eq{"server": network.Server}).
Where(sq.Eq{"auth_account": network.Auth.Account}) Where(sq.Eq{"auth_account": network.Auth.Account})
@ -252,22 +255,24 @@ func (r *IrcRepo) CheckExistingNetwork(ctx context.Context, network *domain.IrcN
var net domain.IrcNetwork var net domain.IrcNetwork
var pass, nick, inviteCmd sql.NullString var pass, nick, inviteCmd, bouncerAddr sql.NullString
var account, password sql.NullString var account, password sql.NullString
var tls sql.NullBool var tls sql.NullBool
err = row.Scan(&net.ID, &net.Enabled, &net.Name, &net.Server, &net.Port, &tls, &pass, &nick, &net.Auth.Mechanism, &account, &password, &inviteCmd) 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 == sql.ErrNoRows { if 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
} else if err != nil { } else {
return nil, errors.Wrap(err, "error scanning row") return nil, errors.Wrap(err, "error scanning row")
}
} }
net.TLS = tls.Bool net.TLS = tls.Bool
net.Pass = pass.String net.Pass = pass.String
net.Nick = nick.String net.Nick = nick.String
net.InviteCommand = inviteCmd.String net.InviteCommand = inviteCmd.String
net.BouncerAddr = bouncerAddr.String
net.Auth.Account = account.String net.Auth.Account = account.String
net.Auth.Password = password.String net.Auth.Password = password.String
@ -279,6 +284,7 @@ func (r *IrcRepo) StoreNetwork(network *domain.IrcNetwork) error {
pass := toNullString(network.Pass) pass := toNullString(network.Pass)
nick := toNullString(network.Nick) nick := toNullString(network.Nick)
inviteCmd := toNullString(network.InviteCommand) inviteCmd := toNullString(network.InviteCommand)
bouncerAddr := toNullString(network.BouncerAddr)
account := toNullString(network.Auth.Account) account := toNullString(network.Auth.Account)
password := toNullString(network.Auth.Password) password := toNullString(network.Auth.Password)
@ -300,6 +306,8 @@ func (r *IrcRepo) StoreNetwork(network *domain.IrcNetwork) error {
"auth_account", "auth_account",
"auth_password", "auth_password",
"invite_command", "invite_command",
"bouncer_addr",
"use_bouncer",
). ).
Values( Values(
network.Enabled, network.Enabled,
@ -313,12 +321,13 @@ func (r *IrcRepo) StoreNetwork(network *domain.IrcNetwork) error {
account, account,
password, password,
inviteCmd, inviteCmd,
bouncerAddr,
network.UseBouncer,
). ).
Suffix("RETURNING id"). Suffix("RETURNING id").
RunWith(r.db.handler) RunWith(r.db.handler)
err = queryBuilder.QueryRow().Scan(&retID) if err = queryBuilder.QueryRow().Scan(&retID); err != nil {
if err != nil {
return errors.Wrap(err, "error executing query") return errors.Wrap(err, "error executing query")
} }
@ -332,6 +341,7 @@ func (r *IrcRepo) UpdateNetwork(ctx context.Context, network *domain.IrcNetwork)
pass := toNullString(network.Pass) pass := toNullString(network.Pass)
nick := toNullString(network.Nick) nick := toNullString(network.Nick)
inviteCmd := toNullString(network.InviteCommand) inviteCmd := toNullString(network.InviteCommand)
bouncerAddr := toNullString(network.BouncerAddr)
account := toNullString(network.Auth.Account) account := toNullString(network.Auth.Account)
password := toNullString(network.Auth.Password) password := toNullString(network.Auth.Password)
@ -351,6 +361,8 @@ func (r *IrcRepo) UpdateNetwork(ctx context.Context, network *domain.IrcNetwork)
Set("auth_account", account). Set("auth_account", account).
Set("auth_password", password). Set("auth_password", password).
Set("invite_command", inviteCmd). Set("invite_command", inviteCmd).
Set("bouncer_addr", bouncerAddr).
Set("use_bouncer", network.UseBouncer).
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})
@ -360,8 +372,7 @@ func (r *IrcRepo) UpdateNetwork(ctx context.Context, network *domain.IrcNetwork)
} }
// update record // update record
_, err = r.db.handler.ExecContext(ctx, query, args...) if _, err = r.db.handler.ExecContext(ctx, query, args...); err != nil {
if err != nil {
return errors.Wrap(err, "error executing query") return errors.Wrap(err, "error executing query")
} }
@ -387,8 +398,7 @@ func (r *IrcRepo) StoreNetworkChannels(ctx context.Context, networkID int64, cha
return errors.Wrap(err, "error building query") return errors.Wrap(err, "error building query")
} }
_, err = tx.ExecContext(ctx, query, args...) if _, err = tx.ExecContext(ctx, query, args...); err != nil {
if err != nil {
return errors.Wrap(err, "error executing query") return errors.Wrap(err, "error executing query")
} }
@ -418,8 +428,7 @@ func (r *IrcRepo) StoreNetworkChannels(ctx context.Context, networkID int64, cha
// returning // returning
var retID int64 var retID int64
err = channelQueryBuilder.QueryRowContext(ctx).Scan(&retID) if err = channelQueryBuilder.QueryRowContext(ctx).Scan(&retID); err != nil {
if err != nil {
return errors.Wrap(err, "error executing query storeNetworkChannels") return errors.Wrap(err, "error executing query storeNetworkChannels")
} }
@ -466,8 +475,7 @@ func (r *IrcRepo) StoreChannel(ctx context.Context, networkID int64, channel *do
return errors.Wrap(err, "error building query") return errors.Wrap(err, "error building query")
} }
_, err = r.db.handler.Exec(query, args...) if _, err = r.db.handler.ExecContext(ctx, query, args...); err != nil {
if err != nil {
return errors.Wrap(err, "error executing query") return errors.Wrap(err, "error executing query")
} }
} else { } else {
@ -493,8 +501,7 @@ func (r *IrcRepo) StoreChannel(ctx context.Context, networkID int64, channel *do
// returning // returning
var retID int64 var retID int64
err = queryBuilder.QueryRow().Scan(&retID) if err = queryBuilder.QueryRowContext(ctx).Scan(&retID); err != nil {
if err != nil {
return errors.Wrap(err, "error executing query") return errors.Wrap(err, "error executing query")
} }

View file

@ -45,6 +45,8 @@ CREATE TABLE irc_network
auth_account TEXT, auth_account TEXT,
auth_password TEXT, auth_password TEXT,
invite_command TEXT, invite_command TEXT,
use_bouncer BOOLEAN,
bouncer_addr TEXT,
connected BOOLEAN, connected BOOLEAN,
connected_since TIMESTAMP, connected_since TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
@ -699,4 +701,9 @@ ADD COLUMN topic text;`,
ALTER TABLE release_action_status ALTER TABLE release_action_status
ADD CONSTRAINT release_action_status_action_id_fk ADD CONSTRAINT release_action_status_action_id_fk
FOREIGN KEY (action_id) REFERENCES action;`, FOREIGN KEY (action_id) REFERENCES action;`,
`ALTER TABLE irc_network
ADD COLUMN use_bouncer BOOLEAN DEFAULT FALSE;
ALTER TABLE irc_network
ADD COLUMN bouncer_addr TEXT;`,
} }

View file

@ -45,6 +45,8 @@ CREATE TABLE irc_network
auth_account TEXT, auth_account TEXT,
auth_password TEXT, auth_password TEXT,
invite_command TEXT, invite_command TEXT,
use_bouncer BOOLEAN,
bouncer_addr TEXT,
connected BOOLEAN, connected BOOLEAN,
connected_since TIMESTAMP, connected_since TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
@ -1141,4 +1143,9 @@ create index release_action_status_release_id_index
create index release_action_status_status_index create index release_action_status_status_index
on release_action_status (status);`, on release_action_status (status);`,
`ALTER TABLE irc_network
ADD COLUMN use_bouncer BOOLEAN DEFAULT FALSE;
ALTER TABLE irc_network
ADD COLUMN bouncer_addr TEXT;`,
} }

View file

@ -43,6 +43,8 @@ type IrcNetwork struct {
Nick string `json:"nick"` Nick string `json:"nick"`
Auth IRCAuth `json:"auth,omitempty"` Auth IRCAuth `json:"auth,omitempty"`
InviteCommand string `json:"invite_command"` InviteCommand string `json:"invite_command"`
UseBouncer bool `json:"use_bouncer"`
BouncerAddr string `json:"bouncer_addr"`
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"`
@ -59,6 +61,8 @@ type IrcNetworkWithHealth struct {
Nick string `json:"nick"` Nick string `json:"nick"`
Auth IRCAuth `json:"auth,omitempty"` Auth IRCAuth `json:"auth,omitempty"`
InviteCommand string `json:"invite_command"` InviteCommand string `json:"invite_command"`
UseBouncer bool `json:"use_bouncer"`
BouncerAddr string `json:"bouncer_addr"`
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"`

View file

@ -156,6 +156,10 @@ func (h *Handler) Run() error {
addr := fmt.Sprintf("%v:%d", h.network.Server, h.network.Port) addr := fmt.Sprintf("%v:%d", h.network.Server, h.network.Port)
if h.network.UseBouncer && h.network.BouncerAddr != "" {
addr = h.network.BouncerAddr
}
subLogger := zstdlog.NewStdLoggerWithLevel(h.log.With().Logger(), zerolog.TraceLevel) subLogger := zstdlog.NewStdLoggerWithLevel(h.log.With().Logger(), zerolog.TraceLevel)
h.client = &ircevent.Connection{ h.client = &ircevent.Connection{

View file

@ -416,6 +416,8 @@ func (s *service) GetNetworksWithHealth(ctx context.Context) ([]domain.IrcNetwor
Nick: n.Nick, Nick: n.Nick,
Auth: n.Auth, Auth: n.Auth,
InviteCommand: n.InviteCommand, InviteCommand: n.InviteCommand,
BouncerAddr: n.BouncerAddr,
UseBouncer: n.UseBouncer,
Connected: false, Connected: false,
Channels: []domain.ChannelWithHealth{}, Channels: []domain.ChannelWithHealth{},
ConnectionErrors: []string{}, ConnectionErrors: []string{},

View file

@ -235,6 +235,8 @@ interface IrcNetworkUpdateFormValues {
nick: string; nick: string;
auth?: IrcAuth; auth?: IrcAuth;
invite_command: string; invite_command: string;
use_bouncer: boolean;
bouncer_addr: string;
channels: Array<IrcChannel>; channels: Array<IrcChannel>;
} }
@ -288,6 +290,8 @@ export function IrcNetworkUpdateForm({
pass: network.pass, pass: network.pass,
auth: network.auth, auth: network.auth,
invite_command: network.invite_command, invite_command: network.invite_command,
use_bouncer: network.use_bouncer,
bouncer_addr: network.bouncer_addr,
channels: network.channels channels: network.channels
}; };
@ -340,6 +344,15 @@ export function IrcNetworkUpdateForm({
required={true} required={true}
/> />
<SwitchGroupWide name="use_bouncer" label="Bouncer (BNC)" />
{values.use_bouncer && (
<TextFieldWide
name="bouncer_addr"
label="Bouncer address"
help="Address: Eg bouncer.server.net:6697"
/>
)}
<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>

View file

@ -14,6 +14,8 @@ interface IrcNetwork {
pass: string; pass: string;
auth: IrcAuth; // optional auth: IrcAuth; // optional
invite_command: string; invite_command: string;
use_bouncer: boolean;
bouncer_addr: string;
channels: IrcChannel[]; channels: IrcChannel[];
connected: boolean; connected: boolean;
connected_since: string; connected_since: string;
@ -29,6 +31,8 @@ interface IrcNetworkCreate {
nick: string; nick: string;
auth: IrcAuth; // optional auth: IrcAuth; // optional
invite_command: string; invite_command: string;
use_bouncer?: boolean;
bouncer_addr?: string;
channels: IrcChannel[]; channels: IrcChannel[];
connected: boolean; connected: boolean;
} }
@ -58,6 +62,8 @@ interface IrcNetworkWithHealth {
nick: string; nick: string;
auth: IrcAuth; // optional auth: IrcAuth; // optional
invite_command: string; invite_command: string;
use_bouncer: boolean;
bouncer_addr: string;
channels: IrcChannelWithHealth[]; channels: IrcChannelWithHealth[];
connected: boolean; connected: boolean;
connected_since: string; connected_since: string;