feat: add support for proxies to use with IRC and Indexers (#1421)

* feat: add support for proxies

* fix(http): release handler

* fix(migrations): define proxy early

* fix(migrations): pg proxy

* fix(proxy): list update delete

* fix(proxy): remove log and imports

* feat(irc): use proxy

* feat(irc): tests

* fix(web): update imports for ProxyForms.tsx

* fix(database): migration

* feat(proxy): test

* feat(proxy): validate proxy type

* feat(proxy): validate and test

* feat(proxy): improve validate and test

* feat(proxy): fix db schema

* feat(proxy): add db tests

* feat(proxy): handle http errors

* fix(http): imports

* feat(proxy): use proxy for indexer downloads

* feat(proxy): indexerforms select proxy

* feat(proxy): handle torrent download

* feat(proxy): skip if disabled

* feat(proxy): imports

* feat(proxy): implement in Feeds

* feat(proxy): update helper text indexer proxy

* feat(proxy): add internal cache
This commit is contained in:
ze0s 2024-09-02 11:10:45 +02:00 committed by GitHub
parent 472d327308
commit bc0f4cc055
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
59 changed files with 2533 additions and 371 deletions

View file

@ -36,6 +36,8 @@ func (r *FeedRepo) FindByID(ctx context.Context, id int) (*domain.Feed, error) {
"i.identifier",
"i.identifier_external",
"i.name",
"i.use_proxy",
"i.proxy_id",
"f.name",
"f.type",
"f.enabled",
@ -66,8 +68,9 @@ func (r *FeedRepo) FindByID(ctx context.Context, id int) (*domain.Feed, error) {
var f domain.Feed
var apiKey, cookie, settings sql.NullString
var proxyID sql.NullInt64
if err := row.Scan(&f.ID, &f.Indexer.ID, &f.Indexer.Identifier, &f.Indexer.IdentifierExternal, &f.Indexer.Name, &f.Name, &f.Type, &f.Enabled, &f.URL, &f.Interval, &f.Timeout, &f.MaxAge, &apiKey, &cookie, &settings, &f.CreatedAt, &f.UpdatedAt); err != nil {
if err := row.Scan(&f.ID, &f.Indexer.ID, &f.Indexer.Identifier, &f.Indexer.IdentifierExternal, &f.Indexer.Name, &f.UseProxy, &proxyID, &f.Name, &f.Type, &f.Enabled, &f.URL, &f.Interval, &f.Timeout, &f.MaxAge, &apiKey, &cookie, &settings, &f.CreatedAt, &f.UpdatedAt); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, domain.ErrRecordNotFound
}
@ -75,6 +78,7 @@ func (r *FeedRepo) FindByID(ctx context.Context, id int) (*domain.Feed, error) {
return nil, errors.Wrap(err, "error scanning row")
}
f.ProxyID = proxyID.Int64
f.ApiKey = apiKey.String
f.Cookie = cookie.String
@ -98,6 +102,8 @@ func (r *FeedRepo) FindByIndexerIdentifier(ctx context.Context, indexer string)
"i.identifier",
"i.identifier_external",
"i.name",
"i.use_proxy",
"i.proxy_id",
"f.name",
"f.type",
"f.enabled",
@ -128,8 +134,9 @@ func (r *FeedRepo) FindByIndexerIdentifier(ctx context.Context, indexer string)
var f domain.Feed
var apiKey, cookie, settings sql.NullString
var proxyID sql.NullInt64
if err := row.Scan(&f.ID, &f.Indexer.ID, &f.Indexer.Identifier, &f.Indexer.IdentifierExternal, &f.Indexer.Name, &f.Name, &f.Type, &f.Enabled, &f.URL, &f.Interval, &f.Timeout, &f.MaxAge, &apiKey, &cookie, &settings, &f.CreatedAt, &f.UpdatedAt); err != nil {
if err := row.Scan(&f.ID, &f.Indexer.ID, &f.Indexer.Identifier, &f.Indexer.IdentifierExternal, &f.Indexer.Name, &f.UseProxy, &proxyID, &f.Name, &f.Type, &f.Enabled, &f.URL, &f.Interval, &f.Timeout, &f.MaxAge, &apiKey, &cookie, &settings, &f.CreatedAt, &f.UpdatedAt); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, domain.ErrRecordNotFound
}
@ -137,6 +144,7 @@ func (r *FeedRepo) FindByIndexerIdentifier(ctx context.Context, indexer string)
return nil, errors.Wrap(err, "error scanning row")
}
f.ProxyID = proxyID.Int64
f.ApiKey = apiKey.String
f.Cookie = cookie.String
@ -158,6 +166,8 @@ func (r *FeedRepo) Find(ctx context.Context) ([]domain.Feed, error) {
"i.identifier",
"i.identifier_external",
"i.name",
"i.use_proxy",
"i.proxy_id",
"f.name",
"f.type",
"f.enabled",
@ -196,10 +206,13 @@ func (r *FeedRepo) Find(ctx context.Context) ([]domain.Feed, error) {
var apiKey, cookie, lastRunData, settings sql.NullString
var lastRun sql.NullTime
if err := rows.Scan(&f.ID, &f.Indexer.ID, &f.Indexer.Identifier, &f.Indexer.IdentifierExternal, &f.Indexer.Name, &f.Name, &f.Type, &f.Enabled, &f.URL, &f.Interval, &f.Timeout, &f.MaxAge, &apiKey, &cookie, &lastRun, &lastRunData, &settings, &f.CreatedAt, &f.UpdatedAt); err != nil {
var proxyID sql.NullInt64
if err := rows.Scan(&f.ID, &f.Indexer.ID, &f.Indexer.Identifier, &f.Indexer.IdentifierExternal, &f.Indexer.Name, &f.UseProxy, &proxyID, &f.Name, &f.Type, &f.Enabled, &f.URL, &f.Interval, &f.Timeout, &f.MaxAge, &apiKey, &cookie, &lastRun, &lastRunData, &settings, &f.CreatedAt, &f.UpdatedAt); err != nil {
return nil, errors.Wrap(err, "error scanning row")
}
f.ProxyID = proxyID.Int64
f.LastRun = lastRun.Time
f.LastRunData = lastRunData.String
f.ApiKey = apiKey.String

View file

@ -36,8 +36,8 @@ func (r *IndexerRepo) Store(ctx context.Context, indexer domain.Indexer) (*domai
}
queryBuilder := r.db.squirrel.
Insert("indexer").Columns("enabled", "name", "identifier", "identifier_external", "implementation", "base_url", "settings").
Values(indexer.Enabled, indexer.Name, indexer.Identifier, indexer.IdentifierExternal, indexer.Implementation, indexer.BaseURL, settings).
Insert("indexer").Columns("enabled", "name", "identifier", "identifier_external", "implementation", "base_url", "use_proxy", "proxy_id", "settings").
Values(indexer.Enabled, indexer.Name, indexer.Identifier, indexer.IdentifierExternal, indexer.Implementation, indexer.BaseURL, indexer.UseProxy, toNullInt64(indexer.ProxyID), settings).
Suffix("RETURNING id").RunWith(r.db.handler)
// return values
@ -61,6 +61,8 @@ func (r *IndexerRepo) Update(ctx context.Context, indexer domain.Indexer) (*doma
Set("name", indexer.Name).
Set("identifier_external", indexer.IdentifierExternal).
Set("base_url", indexer.BaseURL).
Set("use_proxy", indexer.UseProxy).
Set("proxy_id", toNullInt64(indexer.ProxyID)).
Set("settings", settings).
Set("updated_at", time.Now().Format(time.RFC3339)).
Where(sq.Eq{"id": indexer.ID})
@ -70,16 +72,26 @@ func (r *IndexerRepo) Update(ctx context.Context, indexer domain.Indexer) (*doma
return nil, errors.Wrap(err, "error building query")
}
if _, err = r.db.handler.ExecContext(ctx, query, args...); err != nil {
result, err := r.db.handler.ExecContext(ctx, query, args...)
if err != nil {
return nil, errors.Wrap(err, "error executing query")
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return nil, errors.Wrap(err, "error rows affected")
}
if rowsAffected == 0 {
return nil, domain.ErrUpdateFailed
}
return &indexer, nil
}
func (r *IndexerRepo) List(ctx context.Context) ([]domain.Indexer, error) {
queryBuilder := r.db.squirrel.
Select("id", "enabled", "name", "identifier", "identifier_external", "implementation", "base_url", "settings").
Select("id", "enabled", "name", "identifier", "identifier_external", "implementation", "base_url", "use_proxy", "proxy_id", "settings").
From("indexer").
OrderBy("name ASC")
@ -98,27 +110,29 @@ func (r *IndexerRepo) List(ctx context.Context) ([]domain.Indexer, error) {
indexers := make([]domain.Indexer, 0)
for rows.Next() {
var f domain.Indexer
var i domain.Indexer
var identifierExternal, implementation, baseURL sql.Null[string]
var proxyID sql.Null[int64]
var settings string
var settingsMap map[string]string
if err := rows.Scan(&f.ID, &f.Enabled, &f.Name, &f.Identifier, &identifierExternal, &implementation, &baseURL, &settings); err != nil {
if err := rows.Scan(&i.ID, &i.Enabled, &i.Name, &i.Identifier, &identifierExternal, &implementation, &baseURL, &i.UseProxy, &proxyID, &settings); err != nil {
return nil, errors.Wrap(err, "error scanning row")
}
f.IdentifierExternal = identifierExternal.V
f.Implementation = implementation.V
f.BaseURL = baseURL.V
i.IdentifierExternal = identifierExternal.V
i.Implementation = implementation.V
i.BaseURL = baseURL.V
i.ProxyID = proxyID.V
if err = json.Unmarshal([]byte(settings), &settingsMap); err != nil {
return nil, errors.Wrap(err, "error unmarshal settings")
}
f.Settings = settingsMap
i.Settings = settingsMap
indexers = append(indexers, f)
indexers = append(indexers, i)
}
if err := rows.Err(); err != nil {
return nil, errors.Wrap(err, "error rows")
@ -129,7 +143,7 @@ func (r *IndexerRepo) List(ctx context.Context) ([]domain.Indexer, error) {
func (r *IndexerRepo) FindByID(ctx context.Context, id int) (*domain.Indexer, error) {
queryBuilder := r.db.squirrel.
Select("id", "enabled", "name", "identifier", "identifier_external", "implementation", "base_url", "settings").
Select("id", "enabled", "name", "identifier", "identifier_external", "implementation", "base_url", "use_proxy", "proxy_id", "settings").
From("indexer").
Where(sq.Eq{"id": id})
@ -146,8 +160,9 @@ func (r *IndexerRepo) FindByID(ctx context.Context, id int) (*domain.Indexer, er
var i domain.Indexer
var identifierExternal, implementation, baseURL, settings sql.Null[string]
var proxyID sql.Null[int64]
if err := row.Scan(&i.ID, &i.Enabled, &i.Name, &i.Identifier, &identifierExternal, &implementation, &baseURL, &settings); err != nil {
if err := row.Scan(&i.ID, &i.Enabled, &i.Name, &i.Identifier, &identifierExternal, &implementation, &baseURL, &i.UseProxy, &proxyID, &settings); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, domain.ErrRecordNotFound
}
@ -158,6 +173,7 @@ func (r *IndexerRepo) FindByID(ctx context.Context, id int) (*domain.Indexer, er
i.IdentifierExternal = identifierExternal.V
i.Implementation = implementation.V
i.BaseURL = baseURL.V
i.ProxyID = proxyID.V
var settingsMap map[string]string
if err = json.Unmarshal([]byte(settings.V), &settingsMap); err != nil {
@ -171,7 +187,7 @@ func (r *IndexerRepo) FindByID(ctx context.Context, id int) (*domain.Indexer, er
func (r *IndexerRepo) GetBy(ctx context.Context, req domain.GetIndexerRequest) (*domain.Indexer, error) {
queryBuilder := r.db.squirrel.
Select("id", "enabled", "name", "identifier", "identifier_external", "implementation", "base_url", "settings").
Select("id", "enabled", "name", "identifier", "identifier_external", "implementation", "base_url", "use_proxy", "proxy_id", "settings").
From("indexer")
if req.ID > 0 {
@ -195,8 +211,9 @@ func (r *IndexerRepo) GetBy(ctx context.Context, req domain.GetIndexerRequest) (
var i domain.Indexer
var identifierExternal, implementation, baseURL, settings sql.Null[string]
var proxyID sql.Null[int64]
if err := row.Scan(&i.ID, &i.Enabled, &i.Name, &i.Identifier, &identifierExternal, &implementation, &baseURL, &settings); err != nil {
if err := row.Scan(&i.ID, &i.Enabled, &i.Name, &i.Identifier, &identifierExternal, &implementation, &baseURL, &i.UseProxy, &proxyID, &settings); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, domain.ErrRecordNotFound
}
@ -207,6 +224,7 @@ func (r *IndexerRepo) GetBy(ctx context.Context, req domain.GetIndexerRequest) (
i.IdentifierExternal = identifierExternal.V
i.Implementation = implementation.V
i.BaseURL = baseURL.V
i.ProxyID = proxyID.V
var settingsMap map[string]string
if err = json.Unmarshal([]byte(settings.V), &settingsMap); err != nil {
@ -220,7 +238,7 @@ func (r *IndexerRepo) GetBy(ctx context.Context, req domain.GetIndexerRequest) (
func (r *IndexerRepo) FindByFilterID(ctx context.Context, id int) ([]domain.Indexer, error) {
queryBuilder := r.db.squirrel.
Select("id", "enabled", "name", "identifier", "identifier_external", "base_url", "settings").
Select("id", "enabled", "name", "identifier", "identifier_external", "base_url", "use_proxy", "proxy_id", "settings").
From("indexer").
Join("filter_indexer ON indexer.id = filter_indexer.indexer_id").
Where(sq.Eq{"filter_indexer.filter_id": id})
@ -239,13 +257,14 @@ func (r *IndexerRepo) FindByFilterID(ctx context.Context, id int) ([]domain.Inde
indexers := make([]domain.Indexer, 0)
for rows.Next() {
var f domain.Indexer
var i domain.Indexer
var settings string
var settingsMap map[string]string
var identifierExternal, baseURL sql.Null[string]
var proxyID sql.Null[int64]
if err := rows.Scan(&f.ID, &f.Enabled, &f.Name, &f.Identifier, &identifierExternal, &baseURL, &settings); err != nil {
if err := rows.Scan(&i.ID, &i.Enabled, &i.Name, &i.Identifier, &identifierExternal, &baseURL, &i.UseProxy, &proxyID, &settings); err != nil {
return nil, errors.Wrap(err, "error scanning row")
}
@ -253,11 +272,12 @@ func (r *IndexerRepo) FindByFilterID(ctx context.Context, id int) ([]domain.Inde
return nil, errors.Wrap(err, "error unmarshal settings")
}
f.IdentifierExternal = identifierExternal.V
f.BaseURL = baseURL.V
f.Settings = settingsMap
i.IdentifierExternal = identifierExternal.V
i.BaseURL = baseURL.V
i.ProxyID = proxyID.V
i.Settings = settingsMap
indexers = append(indexers, f)
indexers = append(indexers, i)
}
if err := rows.Err(); err != nil {
return nil, errors.Wrap(err, "error rows")
@ -282,13 +302,13 @@ func (r *IndexerRepo) Delete(ctx context.Context, id int) error {
return errors.Wrap(err, "error executing query")
}
rows, err := result.RowsAffected()
rowsAffected, err := result.RowsAffected()
if err != nil {
return errors.Wrap(err, "error rows affected")
}
if rows != 1 {
return errors.New("error deleting row")
if rowsAffected == 0 {
return domain.ErrRecordNotFound
}
r.log.Debug().Str("method", "delete").Msgf("successfully deleted indexer with id %v", id)
@ -297,8 +317,6 @@ func (r *IndexerRepo) Delete(ctx context.Context, id int) error {
}
func (r *IndexerRepo) ToggleEnabled(ctx context.Context, indexerID int, enabled bool) error {
var err error
queryBuilder := r.db.squirrel.
Update("indexer").
Set("enabled", enabled).
@ -310,10 +328,19 @@ func (r *IndexerRepo) ToggleEnabled(ctx context.Context, indexerID int, enabled
return errors.Wrap(err, "error building query")
}
_, err = r.db.handler.ExecContext(ctx, query, args...)
result, err := r.db.handler.ExecContext(ctx, query, args...)
if err != nil {
return errors.Wrap(err, "error executing query")
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return errors.Wrap(err, "error rows affected")
}
if rowsAffected == 0 {
return domain.ErrUpdateFailed
}
return nil
}

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) {
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", "bot_mode").
Select("id", "enabled", "name", "server", "port", "tls", "pass", "nick", "auth_mechanism", "auth_account", "auth_password", "invite_command", "bouncer_addr", "use_bouncer", "bot_mode", "use_proxy", "proxy_id").
From("irc_network").
Where(sq.Eq{"id": id})
@ -42,26 +42,27 @@ func (r *IrcRepo) GetNetworkByID(ctx context.Context, id int64) (*domain.IrcNetw
var n domain.IrcNetwork
var pass, nick, inviteCmd, bouncerAddr sql.NullString
var account, password sql.NullString
var tls sql.NullBool
var pass, nick, inviteCmd, bouncerAddr sql.Null[string]
var account, password sql.Null[string]
var tls sql.Null[bool]
var proxyId sql.Null[int64]
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, &n.BotMode); 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, &n.UseProxy, &proxyId); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, domain.ErrRecordNotFound
}
return nil, errors.Wrap(err, "error scanning row")
}
n.TLS = tls.Bool
n.Pass = pass.String
n.Nick = nick.String
n.InviteCommand = inviteCmd.String
n.Auth.Account = account.String
n.Auth.Password = password.String
n.BouncerAddr = bouncerAddr.String
n.TLS = tls.V
n.Pass = pass.V
n.Nick = nick.V
n.InviteCommand = inviteCmd.V
n.BouncerAddr = bouncerAddr.V
n.Auth.Account = account.V
n.Auth.Password = password.V
n.ProxyId = proxyId.V
return &n, nil
}
@ -111,7 +112,7 @@ func (r *IrcRepo) DeleteNetwork(ctx context.Context, id int64) error {
func (r *IrcRepo) FindActiveNetworks(ctx context.Context) ([]domain.IrcNetwork, error) {
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", "bot_mode").
Select("id", "enabled", "name", "server", "port", "tls", "pass", "nick", "auth_mechanism", "auth_account", "auth_password", "invite_command", "bouncer_addr", "use_bouncer", "bot_mode", "use_proxy", "proxy_id").
From("irc_network").
Where(sq.Eq{"enabled": true})
@ -131,22 +132,24 @@ func (r *IrcRepo) FindActiveNetworks(ctx context.Context) ([]domain.IrcNetwork,
for rows.Next() {
var net domain.IrcNetwork
var pass, nick, inviteCmd, bouncerAddr sql.NullString
var account, password sql.NullString
var tls sql.NullBool
var pass, nick, inviteCmd, bouncerAddr sql.Null[string]
var account, password sql.Null[string]
var tls sql.Null[bool]
var proxyId sql.Null[int64]
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 {
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, &net.UseProxy, &proxyId); err != nil {
return nil, errors.Wrap(err, "error scanning row")
}
net.TLS = tls.Bool
net.Pass = pass.String
net.Nick = nick.String
net.InviteCommand = inviteCmd.String
net.BouncerAddr = bouncerAddr.String
net.TLS = tls.V
net.Pass = pass.V
net.Nick = nick.V
net.InviteCommand = inviteCmd.V
net.BouncerAddr = bouncerAddr.V
net.Auth.Account = account.V
net.Auth.Password = password.V
net.Auth.Account = account.String
net.Auth.Password = password.String
net.ProxyId = proxyId.V
networks = append(networks, net)
}
@ -159,7 +162,7 @@ func (r *IrcRepo) FindActiveNetworks(ctx context.Context) ([]domain.IrcNetwork,
func (r *IrcRepo) ListNetworks(ctx context.Context) ([]domain.IrcNetwork, error) {
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", "bot_mode").
Select("id", "enabled", "name", "server", "port", "tls", "pass", "nick", "auth_mechanism", "auth_account", "auth_password", "invite_command", "bouncer_addr", "use_bouncer", "bot_mode", "use_proxy", "proxy_id").
From("irc_network").
OrderBy("name ASC")
@ -179,22 +182,24 @@ func (r *IrcRepo) ListNetworks(ctx context.Context) ([]domain.IrcNetwork, error)
for rows.Next() {
var net domain.IrcNetwork
var pass, nick, inviteCmd, bouncerAddr sql.NullString
var account, password sql.NullString
var tls sql.NullBool
var pass, nick, inviteCmd, bouncerAddr sql.Null[string]
var account, password sql.Null[string]
var tls sql.Null[bool]
var proxyId sql.Null[int64]
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 {
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, &net.UseProxy, &proxyId); err != nil {
return nil, errors.Wrap(err, "error scanning row")
}
net.TLS = tls.Bool
net.Pass = pass.String
net.Nick = nick.String
net.InviteCommand = inviteCmd.String
net.BouncerAddr = bouncerAddr.String
net.TLS = tls.V
net.Pass = pass.V
net.Nick = nick.V
net.InviteCommand = inviteCmd.V
net.BouncerAddr = bouncerAddr.V
net.Auth.Account = account.V
net.Auth.Password = password.V
net.Auth.Account = account.String
net.Auth.Password = password.String
net.ProxyId = proxyId.V
networks = append(networks, net)
}
@ -225,13 +230,13 @@ func (r *IrcRepo) ListChannels(networkID int64) ([]domain.IrcChannel, error) {
var channels []domain.IrcChannel
for rows.Next() {
var ch domain.IrcChannel
var pass sql.NullString
var pass sql.Null[string]
if err := rows.Scan(&ch.ID, &ch.Name, &ch.Enabled, &pass); err != nil {
return nil, errors.Wrap(err, "error scanning row")
}
ch.Password = pass.String
ch.Password = pass.V
channels = append(channels, ch)
}
@ -244,7 +249,7 @@ func (r *IrcRepo) ListChannels(networkID int64) ([]domain.IrcChannel, error) {
func (r *IrcRepo) CheckExistingNetwork(ctx context.Context, network *domain.IrcNetwork) (*domain.IrcNetwork, error) {
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", "bot_mode").
Select("id", "enabled", "name", "server", "port", "tls", "pass", "nick", "auth_mechanism", "auth_account", "auth_password", "invite_command", "bouncer_addr", "use_bouncer", "bot_mode", "use_proxy", "proxy_id").
From("irc_network").
Where(sq.Eq{"server": network.Server}).
Where(sq.Eq{"port": network.Port}).
@ -263,11 +268,12 @@ func (r *IrcRepo) CheckExistingNetwork(ctx context.Context, network *domain.IrcN
var net domain.IrcNetwork
var pass, nick, inviteCmd, bouncerAddr sql.NullString
var account, password sql.NullString
var tls sql.NullBool
var pass, nick, inviteCmd, bouncerAddr sql.Null[string]
var account, password sql.Null[string]
var tls sql.Null[bool]
var proxyId sql.Null[int64]
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 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, &net.UseProxy, &proxyId); err != nil {
if errors.Is(err, sql.ErrNoRows) {
// no result is not an error in our case
return nil, nil
@ -276,29 +282,20 @@ func (r *IrcRepo) CheckExistingNetwork(ctx context.Context, network *domain.IrcN
return nil, errors.Wrap(err, "error scanning row")
}
net.TLS = tls.Bool
net.Pass = pass.String
net.Nick = nick.String
net.InviteCommand = inviteCmd.String
net.BouncerAddr = bouncerAddr.String
net.Auth.Account = account.String
net.Auth.Password = password.String
net.TLS = tls.V
net.Pass = pass.V
net.Nick = nick.V
net.InviteCommand = inviteCmd.V
net.BouncerAddr = bouncerAddr.V
net.Auth.Account = account.V
net.Auth.Password = password.V
net.ProxyId = proxyId.V
return &net, nil
}
func (r *IrcRepo) StoreNetwork(ctx context.Context, network *domain.IrcNetwork) error {
netName := toNullString(network.Name)
pass := toNullString(network.Pass)
nick := toNullString(network.Nick)
inviteCmd := toNullString(network.InviteCommand)
bouncerAddr := toNullString(network.BouncerAddr)
account := toNullString(network.Auth.Account)
password := toNullString(network.Auth.Password)
var retID int64
queryBuilder := r.db.squirrel.
Insert("irc_network").
Columns(
@ -319,60 +316,49 @@ func (r *IrcRepo) StoreNetwork(ctx context.Context, network *domain.IrcNetwork)
).
Values(
network.Enabled,
netName,
toNullString(network.Name),
network.Server,
network.Port,
network.TLS,
pass,
nick,
toNullString(network.Pass),
toNullString(network.Nick),
network.Auth.Mechanism,
account,
password,
inviteCmd,
bouncerAddr,
toNullString(network.Auth.Account),
toNullString(network.Auth.Password),
toNullString(network.InviteCommand),
toNullString(network.BouncerAddr),
network.UseBouncer,
network.BotMode,
).
Suffix("RETURNING id").
RunWith(r.db.handler)
if err := queryBuilder.QueryRowContext(ctx).Scan(&retID); err != nil {
if err := queryBuilder.QueryRowContext(ctx).Scan(&network.ID); err != nil {
return errors.Wrap(err, "error executing query")
}
network.ID = retID
return nil
}
func (r *IrcRepo) UpdateNetwork(ctx context.Context, network *domain.IrcNetwork) error {
netName := toNullString(network.Name)
pass := toNullString(network.Pass)
nick := toNullString(network.Nick)
inviteCmd := toNullString(network.InviteCommand)
bouncerAddr := toNullString(network.BouncerAddr)
account := toNullString(network.Auth.Account)
password := toNullString(network.Auth.Password)
var err error
queryBuilder := r.db.squirrel.
Update("irc_network").
Set("enabled", network.Enabled).
Set("name", netName).
Set("name", toNullString(network.Name)).
Set("server", network.Server).
Set("port", network.Port).
Set("tls", network.TLS).
Set("pass", pass).
Set("nick", nick).
Set("pass", toNullString(network.Pass)).
Set("nick", toNullString(network.Nick)).
Set("auth_mechanism", network.Auth.Mechanism).
Set("auth_account", account).
Set("auth_password", password).
Set("invite_command", inviteCmd).
Set("bouncer_addr", bouncerAddr).
Set("auth_account", toNullString(network.Auth.Account)).
Set("auth_password", toNullString(network.Auth.Password)).
Set("invite_command", toNullString(network.InviteCommand)).
Set("bouncer_addr", toNullString(network.BouncerAddr)).
Set("use_bouncer", network.UseBouncer).
Set("bot_mode", network.BotMode).
Set("use_proxy", network.UseProxy).
Set("proxy_id", toNullInt64(network.ProxyId)).
Set("updated_at", time.Now().Format(time.RFC3339)).
Where(sq.Eq{"id": network.ID})
@ -414,7 +400,6 @@ func (r *IrcRepo) StoreNetworkChannels(ctx context.Context, networkID int64, cha
for _, channel := range channels {
// values
pass := toNullString(channel.Password)
channelQueryBuilder := r.db.squirrel.
Insert("irc_channel").
@ -429,21 +414,17 @@ func (r *IrcRepo) StoreNetworkChannels(ctx context.Context, networkID int64, cha
channel.Enabled,
true,
channel.Name,
pass,
toNullString(channel.Password),
networkID,
).
Suffix("RETURNING id").
RunWith(tx)
// returning
var retID int64
if err = channelQueryBuilder.QueryRowContext(ctx).Scan(&retID); err != nil {
if err = channelQueryBuilder.QueryRowContext(ctx).Scan(&channel.ID); err != nil {
return errors.Wrap(err, "error executing query storeNetworkChannels")
}
channel.ID = retID
//channelQuery, channelArgs, err := channelQueryBuilder.ToSql()
//if err != nil {
// r.log.Error().Stack().Err(err).Msg("irc.storeNetworkChannels: error building query")
@ -467,8 +448,6 @@ func (r *IrcRepo) StoreNetworkChannels(ctx context.Context, networkID int64, cha
}
func (r *IrcRepo) StoreChannel(ctx context.Context, networkID int64, channel *domain.IrcChannel) error {
pass := toNullString(channel.Password)
if channel.ID != 0 {
// update record
channelQueryBuilder := r.db.squirrel.
@ -476,7 +455,7 @@ func (r *IrcRepo) StoreChannel(ctx context.Context, networkID int64, channel *do
Set("enabled", channel.Enabled).
Set("detached", channel.Detached).
Set("name", channel.Name).
Set("password", pass).
Set("password", toNullString(channel.Password)).
Where(sq.Eq{"id": channel.ID})
query, args, err := channelQueryBuilder.ToSql()
@ -501,21 +480,17 @@ func (r *IrcRepo) StoreChannel(ctx context.Context, networkID int64, channel *do
channel.Enabled,
true,
channel.Name,
pass,
toNullString(channel.Password),
networkID,
).
Suffix("RETURNING id").
RunWith(r.db.handler)
// returning
var retID int64
if err := queryBuilder.QueryRowContext(ctx).Scan(&retID); err != nil {
if err := queryBuilder.QueryRowContext(ctx).Scan(&channel.ID); err != nil {
return errors.Wrap(err, "error executing query")
}
channel.ID = retID
//channelQuery, channelArgs, err := channelQueryBuilder.ToSql()
//if err != nil {
// r.log.Error().Stack().Err(err).Msg("irc.storeChannel: error building query")
@ -536,15 +511,13 @@ func (r *IrcRepo) StoreChannel(ctx context.Context, networkID int64, channel *do
}
func (r *IrcRepo) UpdateChannel(channel *domain.IrcChannel) error {
pass := toNullString(channel.Password)
// update record
channelQueryBuilder := r.db.squirrel.
Update("irc_channel").
Set("enabled", channel.Enabled).
Set("detached", channel.Detached).
Set("name", channel.Name).
Set("password", pass).
Set("password", toNullString(channel.Password)).
Where(sq.Eq{"id": channel.ID})
query, args, err := channelQueryBuilder.ToSql()
@ -561,7 +534,6 @@ func (r *IrcRepo) UpdateChannel(channel *domain.IrcChannel) error {
}
func (r *IrcRepo) UpdateInviteCommand(networkID int64, invite string) error {
// update record
channelQueryBuilder := r.db.squirrel.
Update("irc_network").

View file

@ -14,6 +14,20 @@ CREATE TABLE users
UNIQUE (username)
);
CREATE TABLE proxy
(
id SERIAL PRIMARY KEY,
enabled BOOLEAN,
name TEXT NOT NULL,
type TEXT NOT NULL,
addr TEXT NOT NULL,
auth_user TEXT,
auth_pass TEXT,
timeout INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE indexer
(
id SERIAL PRIMARY KEY,
@ -24,8 +38,11 @@ CREATE TABLE indexer
enabled BOOLEAN,
name TEXT NOT NULL,
settings TEXT,
use_proxy BOOLEAN DEFAULT FALSE,
proxy_id INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (proxy_id) REFERENCES proxy(id) ON DELETE SET NULL,
UNIQUE (identifier)
);
@ -51,8 +68,11 @@ CREATE TABLE irc_network
bot_mode BOOLEAN DEFAULT FALSE,
connected BOOLEAN,
connected_since TIMESTAMP,
use_proxy BOOLEAN DEFAULT FALSE,
proxy_id INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (proxy_id) REFERENCES proxy(id) ON DELETE SET NULL,
UNIQUE (server, port, nick)
);
@ -900,5 +920,39 @@ ADD COLUMN months TEXT;
ALTER TABLE filter
ADD COLUMN days TEXT;
`,
`CREATE TABLE proxy
(
id SERIAL PRIMARY KEY,
enabled BOOLEAN,
name TEXT NOT NULL,
type TEXT NOT NULL,
addr TEXT NOT NULL,
auth_user TEXT,
auth_pass TEXT,
timeout INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
ALTER TABLE indexer
ADD COLUMN proxy_id INTEGER;
ALTER TABLE indexer
ADD COLUMN use_proxy BOOLEAN DEFAULT FALSE;
ALTER TABLE indexer
ADD FOREIGN KEY (proxy_id) REFERENCES proxy
ON DELETE SET NULL;
ALTER TABLE irc_network
ADD COLUMN proxy_id INTEGER;
ALTER TABLE irc_network
ADD COLUMN use_proxy BOOLEAN DEFAULT FALSE;
ALTER TABLE irc_network
ADD FOREIGN KEY (proxy_id) REFERENCES proxy
ON DELETE SET NULL;
`,
}

265
internal/database/proxy.go Normal file
View file

@ -0,0 +1,265 @@
// Copyright (c) 2021 - 2024, Ludvig Lundgren and the autobrr contributors.
// SPDX-License-Identifier: GPL-2.0-or-later
package database
import (
"context"
"database/sql"
"time"
"github.com/autobrr/autobrr/internal/domain"
"github.com/autobrr/autobrr/internal/logger"
"github.com/autobrr/autobrr/pkg/errors"
sq "github.com/Masterminds/squirrel"
"github.com/rs/zerolog"
)
type ProxyRepo struct {
log zerolog.Logger
db *DB
}
func NewProxyRepo(log logger.Logger, db *DB) domain.ProxyRepo {
return &ProxyRepo{
log: log.With().Str("repo", "proxy").Logger(),
db: db,
}
}
func (r *ProxyRepo) Store(ctx context.Context, p *domain.Proxy) error {
queryBuilder := r.db.squirrel.
Insert("proxy").
Columns(
"enabled",
"name",
"type",
"addr",
"auth_user",
"auth_pass",
"timeout",
).
Values(
p.Enabled,
p.Name,
p.Type,
toNullString(p.Addr),
toNullString(p.User),
toNullString(p.Pass),
p.Timeout,
).
Suffix("RETURNING id").
RunWith(r.db.handler)
var retID int64
err := queryBuilder.QueryRowContext(ctx).Scan(&retID)
if err != nil {
return errors.Wrap(err, "error executing query")
}
p.ID = retID
return nil
}
func (r *ProxyRepo) Update(ctx context.Context, p *domain.Proxy) error {
queryBuilder := r.db.squirrel.
Update("proxy").
Set("enabled", p.Enabled).
Set("name", p.Name).
Set("type", p.Type).
Set("addr", p.Addr).
Set("auth_user", toNullString(p.User)).
Set("auth_pass", toNullString(p.Pass)).
Set("timeout", p.Timeout).
Set("updated_at", time.Now().Format(time.RFC3339)).
Where(sq.Eq{"id": p.ID})
query, args, err := queryBuilder.ToSql()
if err != nil {
return errors.Wrap(err, "error building query")
}
// update record
res, err := r.db.handler.ExecContext(ctx, query, args...)
if err != nil {
return errors.Wrap(err, "error executing query")
}
rowsAffected, err := res.RowsAffected()
if err != nil {
return errors.Wrap(err, "error getting affected rows")
}
if rowsAffected == 0 {
return domain.ErrUpdateFailed
}
return err
}
func (r *ProxyRepo) List(ctx context.Context) ([]domain.Proxy, error) {
queryBuilder := r.db.squirrel.
Select(
"id",
"enabled",
"name",
"type",
"addr",
"auth_user",
"auth_pass",
"timeout",
).
From("proxy").
OrderBy("name ASC")
query, args, err := queryBuilder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "error building query")
}
rows, err := r.db.handler.QueryContext(ctx, query, args...)
if err != nil {
return nil, errors.Wrap(err, "error executing query")
}
defer rows.Close()
proxies := make([]domain.Proxy, 0)
for rows.Next() {
var proxy domain.Proxy
var user, pass sql.NullString
if err := rows.Scan(&proxy.ID, &proxy.Enabled, &proxy.Name, &proxy.Type, &proxy.Addr, &user, &pass, &proxy.Timeout); err != nil {
return nil, errors.Wrap(err, "error scanning row")
}
proxy.User = user.String
proxy.Pass = pass.String
proxies = append(proxies, proxy)
}
err = rows.Err()
if err != nil {
return nil, errors.Wrap(err, "error row")
}
return proxies, nil
}
func (r *ProxyRepo) Delete(ctx context.Context, id int64) error {
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return errors.Wrap(err, "error begin transaction")
}
defer tx.Rollback()
queryBuilder := r.db.squirrel.
Delete("proxy").
Where(sq.Eq{"id": id})
query, args, err := queryBuilder.ToSql()
if err != nil {
return errors.Wrap(err, "error building query")
}
res, err := tx.ExecContext(ctx, query, args...)
if err != nil {
return errors.Wrap(err, "error executing query")
}
rowsAffected, err := res.RowsAffected()
if err != nil {
return errors.Wrap(err, "error getting affected rows")
}
if rowsAffected == 0 {
return domain.ErrDeleteFailed
}
err = tx.Commit()
if err != nil {
return errors.Wrap(err, "error commit deleting proxy")
}
return nil
}
func (r *ProxyRepo) FindByID(ctx context.Context, id int64) (*domain.Proxy, error) {
queryBuilder := r.db.squirrel.
Select(
"id",
"enabled",
"name",
"type",
"addr",
"auth_user",
"auth_pass",
"timeout",
).
From("proxy").
OrderBy("name ASC").
Where(sq.Eq{"id": id})
query, args, err := queryBuilder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "error building query")
}
row := r.db.handler.QueryRowContext(ctx, query, args...)
if err := row.Err(); err != nil {
return nil, errors.Wrap(err, "error executing query")
}
var proxy domain.Proxy
var user, pass sql.NullString
err = row.Scan(&proxy.ID, &proxy.Enabled, &proxy.Name, &proxy.Type, &proxy.Addr, &user, &pass, &proxy.Timeout)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, domain.ErrRecordNotFound
}
return nil, errors.Wrap(err, "error scanning row")
}
proxy.User = user.String
proxy.Pass = pass.String
return &proxy, nil
}
func (r *ProxyRepo) ToggleEnabled(ctx context.Context, id int64, enabled bool) error {
queryBuilder := r.db.squirrel.
Update("proxy").
Set("enabled", enabled).
Set("updated_at", time.Now().Format(time.RFC3339)).
Where(sq.Eq{"id": id})
query, args, err := queryBuilder.ToSql()
if err != nil {
return errors.Wrap(err, "error building query")
}
// update record
res, err := r.db.handler.ExecContext(ctx, query, args...)
if err != nil {
return errors.Wrap(err, "error executing query")
}
rowsAffected, err := res.RowsAffected()
if err != nil {
return errors.Wrap(err, "error getting affected rows")
}
if rowsAffected == 0 {
return domain.ErrUpdateFailed
}
return nil
}

View file

@ -0,0 +1,220 @@
// Copyright (c) 2021 - 2024, Ludvig Lundgren and the autobrr contributors.
// SPDX-License-Identifier: GPL-2.0-or-later
//go:build integration
package database
import (
"context"
"fmt"
"testing"
"time"
"github.com/autobrr/autobrr/internal/domain"
"github.com/stretchr/testify/assert"
)
func getMockProxy() *domain.Proxy {
return &domain.Proxy{
//ID: 0,
Name: "Proxy",
Enabled: true,
Type: domain.ProxyTypeSocks5,
Addr: "socks5://127.0.0.1:1080",
User: "",
Pass: "",
Timeout: 0,
}
}
func TestProxyRepo_Store(t *testing.T) {
for dbType, db := range testDBs {
log := setupLoggerForTest()
repo := NewProxyRepo(log, db)
mockData := getMockProxy()
t.Run(fmt.Sprintf("Store_Succeeds [%s]", dbType), func(t *testing.T) {
// Setup
err := repo.Store(context.Background(), mockData)
assert.NoError(t, err)
proxies, err := repo.List(context.Background())
assert.NoError(t, err)
assert.NotNil(t, proxies)
assert.Equal(t, mockData.Name, proxies[0].Name)
// Cleanup
_ = repo.Delete(context.Background(), mockData.ID)
})
t.Run(fmt.Sprintf("Store_Fails_With_Missing_or_empty_fields [%s]", dbType), func(t *testing.T) {
mockData := domain.Proxy{}
err := repo.Store(context.Background(), &mockData)
assert.Error(t, err)
proxies, err := repo.List(context.Background())
assert.NoError(t, err)
assert.Empty(t, proxies)
//assert.Nil(t, proxies)
// Cleanup
// No cleanup needed
})
t.Run(fmt.Sprintf("Store_Fails_With_Context_Timeout [%s]", dbType), func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond)
defer cancel()
err := repo.Store(ctx, mockData)
assert.Error(t, err)
})
}
}
func TestProxyRepo_Update(t *testing.T) {
for dbType, db := range testDBs {
log := setupLoggerForTest()
repo := NewProxyRepo(log, db)
mockData := getMockProxy()
t.Run(fmt.Sprintf("Update_Succeeds [%s]", dbType), func(t *testing.T) {
// Setup
err := repo.Store(context.Background(), mockData)
assert.NoError(t, err)
// Update mockData
updatedProxy := mockData
updatedProxy.Name = "Updated Proxy"
updatedProxy.Enabled = false
// Execute
err = repo.Update(context.Background(), updatedProxy)
assert.NoError(t, err)
proxies, err := repo.List(context.Background())
assert.NoError(t, err)
assert.NotNil(t, proxies)
assert.Equal(t, "Updated Proxy", proxies[0].Name)
assert.Equal(t, false, proxies[0].Enabled)
// Cleanup
_ = repo.Delete(context.Background(), proxies[0].ID)
})
t.Run(fmt.Sprintf("Update_Fails_Invalid_ID [%s]", dbType), func(t *testing.T) {
mockData.ID = -1
err := repo.Update(context.Background(), mockData)
assert.Error(t, err)
assert.ErrorIs(t, err, domain.ErrUpdateFailed)
})
}
}
func TestProxyRepo_Delete(t *testing.T) {
for dbType, db := range testDBs {
log := setupLoggerForTest()
repo := NewProxyRepo(log, db)
mockData := getMockProxy()
t.Run(fmt.Sprintf("Delete_Succeeds [%s]", dbType), func(t *testing.T) {
// Setup
err := repo.Store(context.Background(), mockData)
assert.NoError(t, err)
proxies, err := repo.List(context.Background())
assert.NoError(t, err)
assert.NotNil(t, proxies)
assert.Equal(t, mockData.Name, proxies[0].Name)
// Execute
err = repo.Delete(context.Background(), proxies[0].ID)
assert.NoError(t, err)
// Verify that the proxy is deleted and return error ErrRecordNotFound
proxy, err := repo.FindByID(context.Background(), proxies[0].ID)
assert.ErrorIs(t, err, domain.ErrRecordNotFound)
assert.Nil(t, proxy)
})
t.Run(fmt.Sprintf("Delete_Fails_No_Record [%s]", dbType), func(t *testing.T) {
err := repo.Delete(context.Background(), 9999)
assert.Error(t, err)
assert.ErrorIs(t, err, domain.ErrDeleteFailed)
})
}
}
func TestProxyRepo_ToggleEnabled(t *testing.T) {
for dbType, db := range testDBs {
log := setupLoggerForTest()
repo := NewProxyRepo(log, db)
mockData := getMockProxy()
t.Run(fmt.Sprintf("ToggleEnabled_Succeeds [%s]", dbType), func(t *testing.T) {
// Setup
err := repo.Store(context.Background(), mockData)
assert.NoError(t, err)
proxies, err := repo.List(context.Background())
assert.NoError(t, err)
assert.NotNil(t, proxies)
assert.Equal(t, true, proxies[0].Enabled)
// Execute
err = repo.ToggleEnabled(context.Background(), mockData.ID, false)
assert.NoError(t, err)
// Verify that the proxy is updated
proxy, err := repo.FindByID(context.Background(), proxies[0].ID)
assert.NoError(t, err)
assert.NotNil(t, proxy)
assert.Equal(t, false, proxy.Enabled)
// Cleanup
_ = repo.Delete(context.Background(), proxies[0].ID)
})
t.Run(fmt.Sprintf("ToggleEnabled_Fails_Invalid_ID [%s]", dbType), func(t *testing.T) {
err := repo.ToggleEnabled(context.Background(), -1, false)
assert.Error(t, err)
assert.ErrorIs(t, err, domain.ErrUpdateFailed)
})
}
}
func TestProxyRepo_FindByID(t *testing.T) {
for dbType, db := range testDBs {
log := setupLoggerForTest()
repo := NewProxyRepo(log, db)
mockData := getMockProxy()
t.Run(fmt.Sprintf("FindByID_Succeeds [%s]", dbType), func(t *testing.T) {
// Setup
err := repo.Store(context.Background(), mockData)
assert.NoError(t, err)
proxies, err := repo.List(context.Background())
assert.NoError(t, err)
assert.NotNil(t, proxies)
// Execute
proxy, err := repo.FindByID(context.Background(), proxies[0].ID)
assert.NoError(t, err)
assert.NotNil(t, proxy)
assert.Equal(t, proxies[0].ID, proxy.ID)
// Cleanup
_ = repo.Delete(context.Background(), proxies[0].ID)
})
t.Run(fmt.Sprintf("FindByID_Fails_Invalid_ID [%s]", dbType), func(t *testing.T) {
// Test using an invalid ID
proxy, err := repo.FindByID(context.Background(), -1)
assert.ErrorIs(t, err, domain.ErrRecordNotFound) // should return an error
assert.Nil(t, proxy) // should be nil
})
}
}

View file

@ -14,6 +14,20 @@ CREATE TABLE users
UNIQUE (username)
);
CREATE TABLE proxy
(
id INTEGER PRIMARY KEY,
enabled BOOLEAN,
name TEXT NOT NULL,
type TEXT NOT NULL,
addr TEXT NOT NULL,
auth_user TEXT,
auth_pass TEXT,
timeout INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE indexer
(
id INTEGER PRIMARY KEY,
@ -24,8 +38,11 @@ CREATE TABLE indexer
enabled BOOLEAN,
name TEXT NOT NULL,
settings TEXT,
use_proxy BOOLEAN DEFAULT FALSE,
proxy_id INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (proxy_id) REFERENCES proxy(id) ON DELETE SET NULL,
UNIQUE (identifier)
);
@ -51,8 +68,11 @@ CREATE TABLE irc_network
bot_mode BOOLEAN DEFAULT FALSE,
connected BOOLEAN,
connected_since TIMESTAMP,
use_proxy BOOLEAN DEFAULT FALSE,
proxy_id INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (proxy_id) REFERENCES proxy(id) ON DELETE SET NULL,
UNIQUE (server, port, nick)
);
@ -1538,5 +1558,37 @@ ADD COLUMN months TEXT;
ALTER TABLE filter
ADD COLUMN days TEXT;
`,
`CREATE TABLE proxy
(
id INTEGER PRIMARY KEY,
enabled BOOLEAN,
name TEXT NOT NULL,
type TEXT NOT NULL,
addr TEXT NOT NULL,
auth_user TEXT,
auth_pass TEXT,
timeout INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
ALTER TABLE indexer
ADD proxy_id INTEGER
CONSTRAINT indexer_proxy_id_fk
REFERENCES proxy(id)
ON DELETE SET NULL;
ALTER TABLE indexer
ADD use_proxy BOOLEAN DEFAULT FALSE;
ALTER TABLE irc_network
ADD use_proxy BOOLEAN DEFAULT FALSE;
ALTER TABLE irc_network
ADD proxy_id INTEGER
CONSTRAINT irc_network_proxy_id_fk
REFERENCES proxy(id)
ON DELETE SET NULL;
`,
}

View file

@ -16,10 +16,10 @@ func dataSourceName(configPath string, name string) string {
return name
}
func toNullString(s string) sql.NullString {
return sql.NullString{
String: s,
Valid: s != "",
func toNullString(s string) sql.Null[string] {
return sql.Null[string]{
V: s,
Valid: s != "",
}
}