Refactor irc client (#19)

* refactor: update http handlers

* feat: add trace log level

* refactir: irc handler

* refactor(definitions): add irc settings and invite cmd:

* feat: add dft values to inputs

* refactor: indexer irc forms

* refactor(definitions): fix nickserv.password var:

* feat: pre fill indexer name field

* refactor: handle stopping and updates
This commit is contained in:
Ludvig Lundgren 2021-08-29 23:23:02 +02:00 committed by GitHub
parent 5f69ae9380
commit 4d40d41628
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 1380 additions and 943 deletions

View file

@ -94,22 +94,25 @@ func main() {
srv.Port = cfg.Port srv.Port = cfg.Port
sigCh := make(chan os.Signal, 1) sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGKILL, syscall.SIGTERM)
if err := srv.Start(); err != nil { if err := srv.Start(); err != nil {
log.Fatal().Err(err).Msg("could not start server") log.Fatal().Stack().Err(err).Msg("could not start server")
return
} }
for sig := range sigCh { for sig := range sigCh {
switch sig { switch sig {
case syscall.SIGHUP: case syscall.SIGHUP:
log.Print("shutting down server") log.Print("shutting down server sighup")
srv.Shutdown()
os.Exit(1) os.Exit(1)
case syscall.SIGINT, syscall.SIGTERM: case syscall.SIGINT, syscall.SIGQUIT:
log.Print("shutting down server") srv.Shutdown()
//srv.Shutdown() os.Exit(1)
case syscall.SIGKILL, syscall.SIGTERM:
srv.Shutdown()
os.Exit(1) os.Exit(1)
return
} }
} }
} }

View file

@ -33,7 +33,7 @@ port = 8989
# #
# Options: "ERROR", "DEBUG", "INFO", "WARN" # Options: "ERROR", "DEBUG", "INFO", "WARN"
# #
logLevel = "DEBUG" logLevel = "TRACE"
# Session secret # Session secret
# #

View file

@ -26,12 +26,15 @@ func (r *IndexerRepo) Store(indexer domain.Indexer) (*domain.Indexer, error) {
return nil, err return nil, err
} }
_, err = r.db.Exec(`INSERT INTO indexer (enabled, name, identifier, settings) VALUES (?, ?, ?, ?)`, indexer.Enabled, indexer.Name, indexer.Identifier, settings) res, err := r.db.Exec(`INSERT INTO indexer (enabled, name, identifier, settings) VALUES (?, ?, ?, ?)`, indexer.Enabled, indexer.Name, indexer.Identifier, settings)
if err != nil { if err != nil {
log.Error().Stack().Err(err).Msg("error executing query") log.Error().Stack().Err(err).Msg("error executing query")
return nil, err return nil, err
} }
id, _ := res.LastInsertId()
indexer.ID = id
return &indexer, nil return &indexer, nil
} }

View file

@ -3,7 +3,6 @@ package database
import ( import (
"context" "context"
"database/sql" "database/sql"
"strings"
"github.com/autobrr/autobrr/internal/domain" "github.com/autobrr/autobrr/internal/domain"
@ -24,7 +23,7 @@ func (ir *IrcRepo) Store(announce domain.Announce) error {
func (ir *IrcRepo) GetNetworkByID(id int64) (*domain.IrcNetwork, error) { func (ir *IrcRepo) GetNetworkByID(id int64) (*domain.IrcNetwork, error) {
row := ir.db.QueryRow("SELECT id, enabled, name, addr, tls, nick, pass, connect_commands, sasl_mechanism, sasl_plain_username, sasl_plain_password FROM irc_network WHERE id = ?", id) row := ir.db.QueryRow("SELECT id, enabled, name, server, port, tls, pass, invite_command, nickserv_account, nickserv_password FROM irc_network WHERE id = ?", id)
if err := row.Err(); err != nil { if err := row.Err(); err != nil {
log.Fatal().Err(err) log.Fatal().Err(err)
return nil, err return nil, err
@ -32,22 +31,19 @@ func (ir *IrcRepo) GetNetworkByID(id int64) (*domain.IrcNetwork, error) {
var n domain.IrcNetwork var n domain.IrcNetwork
var pass, connectCommands sql.NullString var pass, inviteCmd sql.NullString
var saslMechanism, saslPlainUsername, saslPlainPassword sql.NullString var nsAccount, nsPassword sql.NullString
var tls sql.NullBool var tls sql.NullBool
if err := row.Scan(&n.ID, &n.Enabled, &n.Name, &n.Addr, &tls, &n.Nick, &pass, &connectCommands, &saslMechanism, &saslPlainUsername, &saslPlainPassword); err != nil { if err := row.Scan(&n.ID, &n.Enabled, &n.Name, &n.Server, &n.Port, &tls, &pass, &inviteCmd, &nsAccount, &nsPassword); err != nil {
log.Fatal().Err(err) log.Fatal().Err(err)
} }
n.TLS = tls.Bool n.TLS = tls.Bool
n.Pass = pass.String n.Pass = pass.String
if connectCommands.Valid { n.InviteCommand = inviteCmd.String
n.ConnectCommands = strings.Split(connectCommands.String, "\r\n") n.NickServ.Account = nsAccount.String
} n.NickServ.Password = nsPassword.String
n.SASL.Mechanism = saslMechanism.String
n.SASL.Plain.Username = saslPlainUsername.String
n.SASL.Plain.Password = saslPlainPassword.String
return &n, nil return &n, nil
} }
@ -84,7 +80,7 @@ func (ir *IrcRepo) DeleteNetwork(ctx context.Context, id int64) error {
func (ir *IrcRepo) ListNetworks(ctx context.Context) ([]domain.IrcNetwork, error) { func (ir *IrcRepo) ListNetworks(ctx context.Context) ([]domain.IrcNetwork, error) {
rows, err := ir.db.QueryContext(ctx, "SELECT id, enabled, name, addr, tls, nick, pass, connect_commands FROM irc_network") rows, err := ir.db.QueryContext(ctx, "SELECT id, enabled, name, server, port, tls, pass, invite_command, nickserv_account, nickserv_password FROM irc_network")
if err != nil { if err != nil {
log.Fatal().Err(err) log.Fatal().Err(err)
} }
@ -95,20 +91,16 @@ func (ir *IrcRepo) ListNetworks(ctx context.Context) ([]domain.IrcNetwork, error
for rows.Next() { for rows.Next() {
var net domain.IrcNetwork var net domain.IrcNetwork
//var username, realname, pass, connectCommands sql.NullString var pass, inviteCmd sql.NullString
var pass, connectCommands sql.NullString
var tls sql.NullBool var tls sql.NullBool
if err := rows.Scan(&net.ID, &net.Enabled, &net.Name, &net.Addr, &tls, &net.Nick, &pass, &connectCommands); err != nil { if err := rows.Scan(&net.ID, &net.Enabled, &net.Name, &net.Server, &net.Port, &tls, &pass, &inviteCmd, &net.NickServ.Account, &net.NickServ.Password); err != nil {
log.Fatal().Err(err) log.Fatal().Err(err)
} }
net.TLS = tls.Bool net.TLS = tls.Bool
net.Pass = pass.String net.Pass = pass.String
net.InviteCommand = inviteCmd.String
if connectCommands.Valid {
net.ConnectCommands = strings.Split(connectCommands.String, "\r\n")
}
networks = append(networks, net) networks = append(networks, net)
} }
@ -131,7 +123,6 @@ func (ir *IrcRepo) ListChannels(networkID int64) ([]domain.IrcChannel, error) {
for rows.Next() { for rows.Next() {
var ch domain.IrcChannel var ch domain.IrcChannel
//if err := rows.Scan(&ch.ID, &ch.Name, &ch.Enabled, &ch.Pass, &ch.InviteCommand, &ch.InviteHTTPURL, &ch.InviteHTTPHeader, &ch.InviteHTTPData); err != nil {
if err := rows.Scan(&ch.ID, &ch.Name, &ch.Enabled); err != nil { if err := rows.Scan(&ch.ID, &ch.Name, &ch.Enabled); err != nil {
log.Fatal().Err(err) log.Fatal().Err(err)
} }
@ -149,20 +140,10 @@ func (ir *IrcRepo) StoreNetwork(network *domain.IrcNetwork) error {
netName := toNullString(network.Name) netName := toNullString(network.Name)
pass := toNullString(network.Pass) pass := toNullString(network.Pass)
connectCommands := toNullString(strings.Join(network.ConnectCommands, "\r\n")) inviteCmd := toNullString(network.InviteCommand)
var saslMechanism, saslPlainUsername, saslPlainPassword sql.NullString nsAccount := toNullString(network.NickServ.Account)
if network.SASL.Mechanism != "" { nsPassword := toNullString(network.NickServ.Password)
saslMechanism = toNullString(network.SASL.Mechanism)
switch network.SASL.Mechanism {
case "PLAIN":
saslPlainUsername = toNullString(network.SASL.Plain.Username)
saslPlainPassword = toNullString(network.SASL.Plain.Password)
default:
log.Warn().Msgf("unsupported SASL mechanism: %q", network.SASL.Mechanism)
//return fmt.Errorf("cannot store network: unsupported SASL mechanism %q", network.SASL.Mechanism)
}
}
var err error var err error
if network.ID != 0 { if network.ID != 0 {
@ -170,26 +151,24 @@ func (ir *IrcRepo) StoreNetwork(network *domain.IrcNetwork) error {
_, err = ir.db.Exec(`UPDATE irc_network _, err = ir.db.Exec(`UPDATE irc_network
SET enabled = ?, SET enabled = ?,
name = ?, name = ?,
addr = ?, server = ?,
port = ?,
tls = ?, tls = ?,
nick = ?,
pass = ?, pass = ?,
connect_commands = ?, invite_command = ?,
sasl_mechanism = ?, nickserv_account = ?,
sasl_plain_username = ?, nickserv_password = ?,
sasl_plain_password = ?,
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE id = ?`, WHERE id = ?`,
network.Enabled, network.Enabled,
netName, netName,
network.Addr, network.Server,
network.Port,
network.TLS, network.TLS,
network.Nick,
pass, pass,
connectCommands, inviteCmd,
saslMechanism, nsAccount,
saslPlainUsername, nsPassword,
saslPlainPassword,
network.ID, network.ID,
) )
} else { } else {
@ -198,25 +177,23 @@ func (ir *IrcRepo) StoreNetwork(network *domain.IrcNetwork) error {
res, err = ir.db.Exec(`INSERT INTO irc_network ( res, err = ir.db.Exec(`INSERT INTO irc_network (
enabled, enabled,
name, name,
addr, server,
port,
tls, tls,
nick,
pass, pass,
connect_commands, invite_command,
sasl_mechanism, nickserv_account,
sasl_plain_username, nickserv_password
sasl_plain_password ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
network.Enabled, network.Enabled,
netName, netName,
network.Addr, network.Server,
network.Port,
network.TLS, network.TLS,
network.Nick,
pass, pass,
connectCommands, inviteCmd,
saslMechanism, nsAccount,
saslPlainUsername, nsPassword,
saslPlainPassword,
) )
if err != nil { if err != nil {
log.Error().Stack().Err(err).Msg("error executing query") log.Error().Stack().Err(err).Msg("error executing query")

View file

@ -24,7 +24,8 @@ CREATE TABLE indexer
name TEXT NOT NULL, name TEXT NOT NULL,
settings TEXT, settings TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE (identifier)
); );
CREATE TABLE irc_network CREATE TABLE irc_network
@ -32,29 +33,30 @@ CREATE TABLE irc_network
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
enabled BOOLEAN, enabled BOOLEAN,
name TEXT NOT NULL, name TEXT NOT NULL,
addr TEXT NOT NULL, server TEXT NOT NULL,
nick TEXT NOT NULL, port INTEGER NOT NULL,
tls BOOLEAN, tls BOOLEAN,
pass TEXT, pass TEXT,
connect_commands TEXT, invite_command TEXT,
sasl_mechanism TEXT, nickserv_account TEXT,
sasl_plain_username TEXT, nickserv_password TEXT,
sasl_plain_password TEXT, connected BOOLEAN,
connected_since TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
unique (addr, nick) UNIQUE (server, port, nickserv_account)
); );
CREATE TABLE irc_channel CREATE TABLE irc_channel
( (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
enabled BOOLEAN, enabled BOOLEAN,
name TEXT NOT NULL, name TEXT NOT NULL,
password TEXT, password TEXT,
detached BOOLEAN, detached BOOLEAN,
network_id INTEGER NOT NULL, network_id INTEGER NOT NULL,
FOREIGN KEY (network_id) REFERENCES irc_network(id), FOREIGN KEY (network_id) REFERENCES irc_network(id),
unique (network_id, name) UNIQUE (network_id, name)
); );
CREATE TABLE filter CREATE TABLE filter

View file

@ -9,7 +9,7 @@ type IndexerRepo interface {
} }
type Indexer struct { type Indexer struct {
ID int `json:"id"` ID int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Identifier string `json:"identifier"` Identifier string `json:"identifier"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
@ -39,15 +39,21 @@ type IndexerSetting struct {
Type string `json:"type"` Type string `json:"type"`
Value string `json:"value,omitempty"` Value string `json:"value,omitempty"`
Label string `json:"label"` Label string `json:"label"`
Description string `json:"description"` Default string `json:"default,omitempty"`
Description string `json:"description,omitempty"`
Help string `json:"help,omitempty"`
Regex string `json:"regex,omitempty"` Regex string `json:"regex,omitempty"`
} }
type IndexerIRC struct { type IndexerIRC struct {
Network string Network string `json:"network"`
Server string Server string `json:"server"`
Channels []string Port int `json:"port"`
Announcers []string TLS bool `json:"tls"`
Channels []string `json:"channels"`
Announcers []string `json:"announcers"`
SettingsMap map[string]string `json:"-"`
Settings []IndexerSetting `json:"settings"`
} }
type IndexerParse struct { type IndexerParse struct {

View file

@ -1,35 +1,37 @@
package domain package domain
import "context" import (
"context"
"time"
)
type IrcChannel struct { type IrcChannel struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
Detached bool `json:"detached"` Name string `json:"name"`
Name string `json:"name"` Password string `json:"password"`
Password string `json:"password"` Detached bool `json:"detached"`
Monitoring bool `json:"monitoring"`
} }
type SASL struct { type NickServ struct {
Mechanism string `json:"mechanism,omitempty"` Account string `json:"account,omitempty"`
Password string `json:"password,omitempty"`
Plain struct {
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
} `json:"plain,omitempty"`
} }
type IrcNetwork struct { type IrcNetwork struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
Addr string `json:"addr"` Server string `json:"server"`
TLS bool `json:"tls"` Port int `json:"port"`
Nick string `json:"nick"` TLS bool `json:"tls"`
Pass string `json:"pass"` Pass string `json:"pass"`
ConnectCommands []string `json:"connect_commands"` InviteCommand string `json:"invite_command"`
SASL SASL `json:"sasl,omitempty"` NickServ NickServ `json:"nickserv,omitempty"`
Channels []IrcChannel `json:"channels"` Channels []IrcChannel `json:"channels"`
Connected bool `json:"connected"`
ConnectedSince *time.Time `json:"connected_since"`
} }
type IrcRepo interface { type IrcRepo interface {

View file

@ -183,7 +183,7 @@ func (s *service) Update(filter domain.Filter) (*domain.Filter, error) {
} }
for _, i := range filter.Indexers { for _, i := range filter.Indexers {
if err = s.repo.StoreIndexerConnection(f.ID, i.ID); err != nil { if err = s.repo.StoreIndexerConnection(f.ID, int(i.ID)); err != nil {
log.Error().Err(err).Msgf("could not store filter indexer connections: %v", filter.Name) log.Error().Err(err).Msgf("could not store filter indexer connections: %v", filter.Name)
return nil, err return nil, err
} }

View file

@ -2,6 +2,7 @@ package http
import ( import (
"encoding/json" "encoding/json"
"errors"
"net/http" "net/http"
"strconv" "strconv"
@ -17,97 +18,96 @@ type actionService interface {
} }
type actionHandler struct { type actionHandler struct {
encoder encoder encoder encoder
actionService actionService service actionService
}
func newActionHandler(encoder encoder, service actionService) *actionHandler {
return &actionHandler{
encoder: encoder,
service: service,
}
} }
func (h actionHandler) Routes(r chi.Router) { func (h actionHandler) Routes(r chi.Router) {
r.Get("/", h.getActions) r.Get("/", h.getActions)
r.Post("/", h.storeAction) r.Post("/", h.storeAction)
r.Delete("/{actionID}", h.deleteAction) r.Delete("/{id}", h.deleteAction)
r.Put("/{actionID}", h.updateAction) r.Put("/{id}", h.updateAction)
r.Patch("/{actionID}/toggleEnabled", h.toggleActionEnabled) r.Patch("/{id}/toggleEnabled", h.toggleActionEnabled)
} }
func (h actionHandler) getActions(w http.ResponseWriter, r *http.Request) { func (h actionHandler) getActions(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() actions, err := h.service.Fetch()
actions, err := h.actionService.Fetch()
if err != nil { if err != nil {
// encode error // encode error
} }
h.encoder.StatusResponse(ctx, w, actions, http.StatusOK) h.encoder.StatusResponse(r.Context(), w, actions, http.StatusOK)
} }
func (h actionHandler) storeAction(w http.ResponseWriter, r *http.Request) { func (h actionHandler) storeAction(w http.ResponseWriter, r *http.Request) {
var ( var data domain.Action
ctx = r.Context()
data domain.Action
)
if err := json.NewDecoder(r.Body).Decode(&data); err != nil { if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
// encode error // encode error
return return
} }
action, err := h.actionService.Store(data) action, err := h.service.Store(data)
if err != nil { if err != nil {
// encode error // encode error
} }
h.encoder.StatusResponse(ctx, w, action, http.StatusCreated) h.encoder.StatusResponse(r.Context(), w, action, http.StatusCreated)
} }
func (h actionHandler) updateAction(w http.ResponseWriter, r *http.Request) { func (h actionHandler) updateAction(w http.ResponseWriter, r *http.Request) {
var ( var data domain.Action
ctx = r.Context()
data domain.Action
)
if err := json.NewDecoder(r.Body).Decode(&data); err != nil { if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
// encode error // encode error
return return
} }
action, err := h.actionService.Store(data) action, err := h.service.Store(data)
if err != nil { if err != nil {
// encode error // encode error
} }
h.encoder.StatusResponse(ctx, w, action, http.StatusCreated) h.encoder.StatusResponse(r.Context(), w, action, http.StatusCreated)
} }
func (h actionHandler) deleteAction(w http.ResponseWriter, r *http.Request) { func (h actionHandler) deleteAction(w http.ResponseWriter, r *http.Request) {
var ( actionID, err := parseInt(chi.URLParam(r, "id"))
ctx = r.Context() if err != nil {
actionID = chi.URLParam(r, "actionID") h.encoder.StatusResponse(r.Context(), w, errors.New("bad param id"), http.StatusBadRequest)
) }
// if !actionID return error if err := h.service.Delete(actionID); err != nil {
id, _ := strconv.Atoi(actionID)
if err := h.actionService.Delete(id); err != nil {
// encode error // encode error
} }
h.encoder.StatusResponse(ctx, w, nil, http.StatusNoContent) h.encoder.StatusResponse(r.Context(), w, nil, http.StatusNoContent)
} }
func (h actionHandler) toggleActionEnabled(w http.ResponseWriter, r *http.Request) { func (h actionHandler) toggleActionEnabled(w http.ResponseWriter, r *http.Request) {
var ( actionID, err := parseInt(chi.URLParam(r, "id"))
ctx = r.Context() if err != nil {
actionID = chi.URLParam(r, "actionID") h.encoder.StatusResponse(r.Context(), w, errors.New("bad param id"), http.StatusBadRequest)
) }
// if !actionID return error if err := h.service.ToggleEnabled(actionID); err != nil {
id, _ := strconv.Atoi(actionID)
if err := h.actionService.ToggleEnabled(id); err != nil {
// encode error // encode error
} }
h.encoder.StatusResponse(ctx, w, nil, http.StatusCreated) h.encoder.StatusResponse(r.Context(), w, nil, http.StatusCreated)
}
func parseInt(s string) (int, error) {
u, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return 0, err
}
return int(u), nil
} }

View file

@ -16,8 +16,15 @@ type authService interface {
} }
type authHandler struct { type authHandler struct {
encoder encoder encoder encoder
authService authService service authService
}
func newAuthHandler(encoder encoder, service authService) *authHandler {
return &authHandler{
encoder: encoder,
service: service,
}
} }
var ( var (
@ -49,7 +56,7 @@ func (h authHandler) login(w http.ResponseWriter, r *http.Request) {
store.Options.SameSite = http.SameSiteStrictMode store.Options.SameSite = http.SameSiteStrictMode
session, _ := store.Get(r, "user_session") session, _ := store.Get(r, "user_session")
_, err := h.authService.Login(data.Username, data.Password) _, err := h.service.Login(data.Username, data.Password)
if err != nil { if err != nil {
h.encoder.StatusResponse(ctx, w, nil, http.StatusUnauthorized) h.encoder.StatusResponse(ctx, w, nil, http.StatusUnauthorized)
return return

View file

@ -20,6 +20,10 @@ type configHandler struct {
encoder encoder encoder encoder
} }
func newConfigHandler(encoder encoder) *configHandler {
return &configHandler{encoder: encoder}
}
func (h configHandler) Routes(r chi.Router) { func (h configHandler) Routes(r chi.Router) {
r.Get("/", h.getConfig) r.Get("/", h.getConfig)
} }

View file

@ -18,8 +18,15 @@ type downloadClientService interface {
} }
type downloadClientHandler struct { type downloadClientHandler struct {
encoder encoder encoder encoder
downloadClientService downloadClientService service downloadClientService
}
func newDownloadClientHandler(encoder encoder, service downloadClientService) *downloadClientHandler {
return &downloadClientHandler{
encoder: encoder,
service: service,
}
} }
func (h downloadClientHandler) Routes(r chi.Router) { func (h downloadClientHandler) Routes(r chi.Router) {
@ -33,7 +40,7 @@ func (h downloadClientHandler) Routes(r chi.Router) {
func (h downloadClientHandler) listDownloadClients(w http.ResponseWriter, r *http.Request) { func (h downloadClientHandler) listDownloadClients(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
clients, err := h.downloadClientService.List() clients, err := h.service.List()
if err != nil { if err != nil {
// //
} }
@ -52,7 +59,7 @@ func (h downloadClientHandler) store(w http.ResponseWriter, r *http.Request) {
return return
} }
client, err := h.downloadClientService.Store(data) client, err := h.service.Store(data)
if err != nil { if err != nil {
// encode error // encode error
} }
@ -72,7 +79,7 @@ func (h downloadClientHandler) test(w http.ResponseWriter, r *http.Request) {
return return
} }
err := h.downloadClientService.Test(data) err := h.service.Test(data)
if err != nil { if err != nil {
// encode error // encode error
h.encoder.StatusResponse(ctx, w, nil, http.StatusBadRequest) h.encoder.StatusResponse(ctx, w, nil, http.StatusBadRequest)
@ -93,7 +100,7 @@ func (h downloadClientHandler) update(w http.ResponseWriter, r *http.Request) {
return return
} }
client, err := h.downloadClientService.Store(data) client, err := h.service.Store(data)
if err != nil { if err != nil {
// encode error // encode error
} }
@ -111,7 +118,7 @@ func (h downloadClientHandler) delete(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.Atoi(clientID) id, _ := strconv.Atoi(clientID)
if err := h.downloadClientService.Delete(id); err != nil { if err := h.service.Delete(id); err != nil {
// encode error // encode error
} }

View file

@ -20,8 +20,15 @@ type filterService interface {
} }
type filterHandler struct { type filterHandler struct {
encoder encoder encoder encoder
filterService filterService service filterService
}
func newFilterHandler(encoder encoder, service filterService) *filterHandler {
return &filterHandler{
encoder: encoder,
service: service,
}
} }
func (h filterHandler) Routes(r chi.Router) { func (h filterHandler) Routes(r chi.Router) {
@ -35,7 +42,7 @@ func (h filterHandler) Routes(r chi.Router) {
func (h filterHandler) getFilters(w http.ResponseWriter, r *http.Request) { func (h filterHandler) getFilters(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
trackers, err := h.filterService.ListFilters() trackers, err := h.service.ListFilters()
if err != nil { if err != nil {
// //
} }
@ -51,7 +58,7 @@ func (h filterHandler) getByID(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.Atoi(filterID) id, _ := strconv.Atoi(filterID)
filter, err := h.filterService.FindByID(id) filter, err := h.service.FindByID(id)
if err != nil { if err != nil {
h.encoder.StatusNotFound(ctx, w) h.encoder.StatusNotFound(ctx, w)
return return
@ -68,7 +75,7 @@ func (h filterHandler) storeFilterAction(w http.ResponseWriter, r *http.Request)
id, _ := strconv.Atoi(filterID) id, _ := strconv.Atoi(filterID)
filter, err := h.filterService.FindByID(id) filter, err := h.service.FindByID(id)
if err != nil { if err != nil {
// //
} }
@ -87,7 +94,7 @@ func (h filterHandler) store(w http.ResponseWriter, r *http.Request) {
return return
} }
filter, err := h.filterService.Store(data) filter, err := h.service.Store(data)
if err != nil { if err != nil {
// encode error // encode error
return return
@ -107,7 +114,7 @@ func (h filterHandler) update(w http.ResponseWriter, r *http.Request) {
return return
} }
filter, err := h.filterService.Update(data) filter, err := h.service.Update(data)
if err != nil { if err != nil {
// encode error // encode error
return return
@ -124,7 +131,7 @@ func (h filterHandler) delete(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.Atoi(filterID) id, _ := strconv.Atoi(filterID)
if err := h.filterService.Delete(id); err != nil { if err := h.service.Delete(id); err != nil {
// return err // return err
} }

View file

@ -20,8 +20,17 @@ type indexerService interface {
} }
type indexerHandler struct { type indexerHandler struct {
encoder encoder encoder encoder
indexerService indexerService service indexerService
ircSvc ircService
}
func newIndexerHandler(encoder encoder, service indexerService, ircSvc ircService) *indexerHandler {
return &indexerHandler{
encoder: encoder,
service: service,
ircSvc: ircSvc,
}
} }
func (h indexerHandler) Routes(r chi.Router) { func (h indexerHandler) Routes(r chi.Router) {
@ -36,7 +45,7 @@ func (h indexerHandler) Routes(r chi.Router) {
func (h indexerHandler) getSchema(w http.ResponseWriter, r *http.Request) { func (h indexerHandler) getSchema(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
indexers, err := h.indexerService.GetTemplates() indexers, err := h.service.GetTemplates()
if err != nil { if err != nil {
// //
} }
@ -45,21 +54,20 @@ func (h indexerHandler) getSchema(w http.ResponseWriter, r *http.Request) {
} }
func (h indexerHandler) store(w http.ResponseWriter, r *http.Request) { func (h indexerHandler) store(w http.ResponseWriter, r *http.Request) {
var ( var data domain.Indexer
ctx = r.Context()
data domain.Indexer
)
if err := json.NewDecoder(r.Body).Decode(&data); err != nil { if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
return return
} }
indexer, err := h.indexerService.Store(data) indexer, err := h.service.Store(data)
if err != nil { if err != nil {
// //
h.encoder.StatusResponse(r.Context(), w, nil, http.StatusBadRequest)
return
} }
h.encoder.StatusResponse(ctx, w, indexer, http.StatusCreated) h.encoder.StatusResponse(r.Context(), w, indexer, http.StatusCreated)
} }
func (h indexerHandler) update(w http.ResponseWriter, r *http.Request) { func (h indexerHandler) update(w http.ResponseWriter, r *http.Request) {
@ -72,7 +80,7 @@ func (h indexerHandler) update(w http.ResponseWriter, r *http.Request) {
return return
} }
indexer, err := h.indexerService.Update(data) indexer, err := h.service.Update(data)
if err != nil { if err != nil {
// //
} }
@ -88,7 +96,7 @@ func (h indexerHandler) delete(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.Atoi(idParam) id, _ := strconv.Atoi(idParam)
if err := h.indexerService.Delete(id); err != nil { if err := h.service.Delete(id); err != nil {
// return err // return err
} }
@ -98,7 +106,7 @@ func (h indexerHandler) delete(w http.ResponseWriter, r *http.Request) {
func (h indexerHandler) getAll(w http.ResponseWriter, r *http.Request) { func (h indexerHandler) getAll(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
indexers, err := h.indexerService.GetAll() indexers, err := h.service.GetAll()
if err != nil { if err != nil {
// //
} }
@ -109,7 +117,7 @@ func (h indexerHandler) getAll(w http.ResponseWriter, r *http.Request) {
func (h indexerHandler) list(w http.ResponseWriter, r *http.Request) { func (h indexerHandler) list(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
indexers, err := h.indexerService.List() indexers, err := h.service.List()
if err != nil { if err != nil {
// //
} }

View file

@ -21,8 +21,15 @@ type ircService interface {
} }
type ircHandler struct { type ircHandler struct {
encoder encoder encoder encoder
ircService ircService service ircService
}
func newIrcHandler(encoder encoder, service ircService) *ircHandler {
return &ircHandler{
encoder: encoder,
service: service,
}
} }
func (h ircHandler) Routes(r chi.Router) { func (h ircHandler) Routes(r chi.Router) {
@ -38,7 +45,7 @@ func (h ircHandler) Routes(r chi.Router) {
func (h ircHandler) listNetworks(w http.ResponseWriter, r *http.Request) { func (h ircHandler) listNetworks(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
networks, err := h.ircService.ListNetworks(ctx) networks, err := h.service.ListNetworks(ctx)
if err != nil { if err != nil {
// //
} }
@ -54,7 +61,7 @@ func (h ircHandler) getNetworkByID(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.Atoi(networkID) id, _ := strconv.Atoi(networkID)
network, err := h.ircService.GetNetworkByID(int64(id)) network, err := h.service.GetNetworkByID(int64(id))
if err != nil { if err != nil {
// //
} }
@ -72,9 +79,11 @@ func (h ircHandler) storeNetwork(w http.ResponseWriter, r *http.Request) {
return return
} }
err := h.ircService.StoreNetwork(&data) err := h.service.StoreNetwork(&data)
if err != nil { if err != nil {
// //
h.encoder.StatusResponse(ctx, w, nil, http.StatusBadRequest)
return
} }
h.encoder.StatusResponse(ctx, w, nil, http.StatusCreated) h.encoder.StatusResponse(ctx, w, nil, http.StatusCreated)
@ -93,7 +102,7 @@ func (h ircHandler) storeChannel(w http.ResponseWriter, r *http.Request) {
return return
} }
err := h.ircService.StoreChannel(int64(id), &data) err := h.service.StoreChannel(int64(id), &data)
if err != nil { if err != nil {
// //
} }
@ -107,7 +116,7 @@ func (h ircHandler) stopNetwork(w http.ResponseWriter, r *http.Request) {
networkID = chi.URLParam(r, "networkID") networkID = chi.URLParam(r, "networkID")
) )
err := h.ircService.StopNetwork(networkID) err := h.service.StopNetwork(networkID)
if err != nil { if err != nil {
// //
} }
@ -123,7 +132,7 @@ func (h ircHandler) deleteNetwork(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.Atoi(networkID) id, _ := strconv.Atoi(networkID)
err := h.ircService.DeleteNetwork(ctx, int64(id)) err := h.service.DeleteNetwork(ctx, int64(id))
if err != nil { if err != nil {
// //
} }

View file

@ -64,56 +64,19 @@ func (s Server) Handler() http.Handler {
fileSystem.ServeHTTP(w, r) fileSystem.ServeHTTP(w, r)
}) })
authHandler := authHandler{ r.Route("/api/auth", newAuthHandler(encoder, s.authService).Routes)
encoder: encoder,
authService: s.authService,
}
r.Route("/api/auth", authHandler.Routes)
r.Group(func(r chi.Router) { r.Group(func(r chi.Router) {
r.Use(IsAuthenticated) r.Use(IsAuthenticated)
actionHandler := actionHandler{ r.Route("/api", func(r chi.Router) {
encoder: encoder, r.Route("/actions", newActionHandler(encoder, s.actionService).Routes)
actionService: s.actionService, r.Route("/config", newConfigHandler(encoder).Routes)
} r.Route("/download_clients", newDownloadClientHandler(encoder, s.downloadClientService).Routes)
r.Route("/filters", newFilterHandler(encoder, s.filterService).Routes)
r.Route("/api/actions", actionHandler.Routes) r.Route("/irc", newIrcHandler(encoder, s.ircService).Routes)
r.Route("/indexer", newIndexerHandler(encoder, s.indexerService, s.ircService).Routes)
downloadClientHandler := downloadClientHandler{ })
encoder: encoder,
downloadClientService: s.downloadClientService,
}
r.Route("/api/download_clients", downloadClientHandler.Routes)
filterHandler := filterHandler{
encoder: encoder,
filterService: s.filterService,
}
r.Route("/api/filters", filterHandler.Routes)
ircHandler := ircHandler{
encoder: encoder,
ircService: s.ircService,
}
r.Route("/api/irc", ircHandler.Routes)
indexerHandler := indexerHandler{
encoder: encoder,
indexerService: s.indexerService,
}
r.Route("/api/indexer", indexerHandler.Routes)
configHandler := configHandler{
encoder: encoder,
}
r.Route("/api/config", configHandler.Routes)
}) })
//r.HandleFunc("/*", handler.ServeHTTP) //r.HandleFunc("/*", handler.ServeHTTP)

View file

@ -14,31 +14,46 @@ supports:
source: gazelle source: gazelle
settings: settings:
- name: authkey - name: authkey
type: text type: secret
label: Auth key label: Auth key
tooltip: Right click DL on a torrent and get the authkey. help: Right click DL on a torrent and get the authkey.
description: Right click DL on a torrent and get the authkey.
- name: torrent_pass - name: torrent_pass
type: text type: secret
label: Torrent pass label: Torrent pass
tooltip: Right click DL on a torrent and get the torrent_pass. help: Right click DL on a torrent and get the torrent_pass.
description: Right click DL on a torrent and get the torrent_pass.
irc: irc:
network: AlphaRatio network: AlphaRatio
server: irc.alpharatio.cc:6697 server: irc.alpharatio.cc
port: 6697 port: 6697
tls: true
channels: channels:
- "#Announce" - "#Announce"
announcers: announcers:
- Voyager - Voyager
settings:
- name: nickserv.account
type: text
required: true
label: NickServ Account
help: NickServ account. Make sure to group your user and bot. Eg. user-bot
- name: nickserv.password
type: secret
required: true
label: NickServ Password
help: NickServ password
- name: invite_command
type: secret
default: "Voyager autobot USERNAME IRCKey"
required: false
label: Invite command
help: Invite auth with Voyager.
parse: parse:
type: multi type: multi
lines: lines:
- - test:
test: - "[New Release]-[MovieHD]-[That.Movie.2017.INTERNAL.1080p.BluRay.CRF.x264-GROUP]-[URL]-[ https://alpharatio.cc/torrents.php?id=000000 ]-[ 000000 ]-[ Uploaded 2 Mins, 59 Secs after pre. ]"
- "[New Release]-[MovieHD]-[War.For.The.Planet.Of.The.Apes.2017.INTERNAL.1080p.BluRay.CRF.x264-SAPHiRE]-[URL]-[ https://alpharatio.cc/torrents.php?id=699463 ]-[ 699434 ]-[ Uploaded 2 Mins, 59 Secs after pre. ]"
pattern: \[New Release\]-\[(.*)\]-\[(.*)\]-\[URL\]-\[ (https?://.*)id=\d+ \]-\[ (\d+) \](?:-\[ Uploaded (.*) after pre. ])? pattern: \[New Release\]-\[(.*)\]-\[(.*)\]-\[URL\]-\[ (https?://.*)id=\d+ \]-\[ (\d+) \](?:-\[ Uploaded (.*) after pre. ])?
vars: vars:
- category - category
@ -46,9 +61,8 @@ parse:
- baseUrl - baseUrl
- torrentId - torrentId
- preTime - preTime
- - test:
test: - "[AutoDL]-[MovieHD]-[000000]-[ 1 | 10659 | 1 | 1 ]-[That.Movie.2017.INTERNAL.1080p.BluRay.CRF.x264-GROUP]"
- "[AutoDL]-[MovieHD]-[699434]-[ 1 | 10659 | 1 | 1 ]-[War.For.The.Planet.Of.The.Apes.2017.INTERNAL.1080p.BluRay.CRF.x264-SAPHiRE]"
pattern: \[AutoDL\]-\[.*\]-\[.*\]-\[ ([01]) \| (\d+) \| ([01]) \| ([01]) \]-\[.+\] pattern: \[AutoDL\]-\[.*\]-\[.*\]-\[ ([01]) \| (\d+) \| ([01]) \| ([01]) \]-\[.+\]
vars: vars:
- scene - scene

View file

@ -13,28 +13,44 @@ supports:
- rss - rss
source: UNIT3D (F3NIX) source: UNIT3D (F3NIX)
settings: settings:
- name: passkey - name: rsskey
type: text type: secret
label: Passkey label: RSS key
tooltip: The passkey in your BeyondHD RSS feed. help: "Go to your profile, My Security, RSS Key and copy RSS key."
description: "Go to your profile and copy and paste your RSS link to extract the rsskey."
irc: irc:
network: BeyondHD-IRC network: BeyondHD-IRC
server: irc.beyond-hd.me:6697 server: irc.beyond-hd.me
port: 6697 port: 6697
tls: true
channels: channels:
- "#bhd_announce" - "#bhd_announce"
announcers: announcers:
- Willie - Willie
- Millie - Millie
settings:
- name: nickserv.account
type: text
required: true
label: NickServ Account
help: NickServ account. Make sure to group your user and bot. Eg. user|autodl
- name: nickserv.password
type: secret
required: true
label: NickServ Password
help: NickServ password
- name: invite_command
type: secret
default: "Millie announce ircKey"
required: true
label: Invite command
help: Invite auth with Millie.
parse: parse:
type: single type: single
lines: lines:
- - test:
test: - "New Torrent: That.Show.S01.1080p.Blu-ray.AVC.DTS-HD.MA.5.1-Test Category: TV By: Uploader Size: 137.73 GB Link: https://beyond-hd.me/details.php?id=00000"
- "New Torrent: Orange.Is.the.New.Black.S01.1080p.Blu-ray.AVC.DTS-HD.MA.5.1-Test Category: TV By: Uploader Size: 137.73 GB Link: https://beyond-hd.me/details.php?id=25918"
pattern: 'New Torrent:(.*)Category:(.*)By:(.*)Size:(.*)Link: https?\:\/\/([^\/]+\/).*[&\?]id=(\d+)' pattern: 'New Torrent:(.*)Category:(.*)By:(.*)Size:(.*)Link: https?\:\/\/([^\/]+\/).*[&\?]id=(\d+)'
vars: vars:
- torrentName - torrentName

View file

@ -13,42 +13,57 @@ supports:
- rss - rss
source: gazelle source: gazelle
settings: settings:
- name: authkey - name: authkey
type: text type: secret
label: Auth key label: Auth key
description: Right click DL on a torrent and get the authkey. help: Right click DL on a torrent and get the authkey.
- name: torrent_pass - name: torrent_pass
type: text type: secret
label: Torrent pass label: Torrent pass
description: Right click DL on a torrent and get the torrent_pass. help: Right click DL on a torrent and get the torrent_pass.
irc: irc:
network: BroadcasTheNet network: BroadcasTheNet
server: irc.broadcasthenet.net:6697 server: irc.broadcasthenet.net
port: 6697 port: 6697
tls: true
channels: channels:
- "#BTN-Announce" - "#BTN-Announce"
announcers: announcers:
- Barney - Barney
settings:
- name: nickserv.account
type: text
required: true
label: NickServ Account
help: NickServ account. Make sure to group your user and bot. Eg. user|autodl
- name: nickserv.password
type: secret
required: true
label: NickServ Password
help: NickServ password
- name: invite_command
type: secret
default: "CableGuy IDENTIFY USERNAME IRCKey"
required: true
label: Invite command
help: Invite auth with CableGuy.
parse: parse:
type: multi type: multi
lines: lines:
- - test:
test: - "NOW BROADCASTING! [ The Show S06E07 720p WEB-DL DD 5.1 H.264 - LP ]"
- "NOW BROADCASTING! [ Lost S06E07 720p WEB-DL DD 5.1 H.264 - LP ]"
pattern: ^NOW BROADCASTING! \[(.*)\] pattern: ^NOW BROADCASTING! \[(.*)\]
vars: vars:
- torrentName - torrentName
- - test:
test: - "[ Title: S06E07 ] [ Series: The Show ]"
- "[ Title: S06E07 ] [ Series: Lost ]"
pattern: '^\[ Title: (.*) \] \[ Series: (.*) \]' pattern: '^\[ Title: (.*) \] \[ Series: (.*) \]'
vars: vars:
- title - title
- name1 - name1
- - test:
test:
- "[ 2010 ] [ Episode ] [ MKV | x264 | WEB ] [ Uploader: Uploader1 ]" - "[ 2010 ] [ Episode ] [ MKV | x264 | WEB ] [ Uploader: Uploader1 ]"
pattern: '^(?:\[ (\d+) \] )?\[ (.*) \] \[ (.*) \] \[ Uploader: (.*?) \](?: \[ Pretime: (.*) \])?' pattern: '^(?:\[ (\d+) \] )?\[ (.*) \] \[ (.*) \] \[ Uploader: (.*?) \](?: \[ Pretime: (.*) \])?'
vars: vars:
@ -57,10 +72,9 @@ parse:
- tags - tags
- uploader - uploader
- preTime - preTime
- - test:
test:
- "[ https://XXXXXXXXX/torrents.php?id=7338 / https://XXXXXXXXX/torrents.php?action=download&id=9116 ]" - "[ https://XXXXXXXXX/torrents.php?id=7338 / https://XXXXXXXXX/torrents.php?action=download&id=9116 ]"
pattern: ^\[ .* / (https?://.*id=\d+) \] pattern: ^\[ .* \/ (https?:\/\/.*id=\d+) \]
vars: vars:
- baseUrl - baseUrl

View file

@ -14,28 +14,39 @@ supports:
source: gazelle source: gazelle
settings: settings:
- name: authkey - name: authkey
type: text type: secret
label: Auth key label: Auth key
description: Right click DL on a torrent and get the authkey. help: Right click DL on a torrent and get the authkey.
- name: torrent_pass - name: torrent_pass
type: text type: secret
label: Torrent pass label: Torrent pass
description: Right click DL on a torrent and get the torrent_pass. help: Right click DL on a torrent and get the torrent_pass.
irc: irc:
network: DigitalIRC network: DigitalIRC
server: irc.empornium.is:6697 server: irc.empornium.is
port: 6697 port: 6697
tls: true
channels: channels:
- "#empornium-announce" - "#empornium-announce"
announcers: announcers:
- "^Wizard^" - "^Wizard^"
settings:
- name: nickserv.account
type: text
required: true
label: NickServ Account
help: NickServ account. Make sure to group your user and bot. Eg. user_bot. Must have staff permission first.
- name: nickserv.password
type: secret
required: true
label: NickServ Password
help: NickServ password
parse: parse:
type: single type: single
lines: lines:
- - pattern: '^(.*?) - Size: ([0-9]+?.*?) - Uploader: (.*?) - Tags: (.*?) - (https://.*torrents.php\?)id=(.*)$'
pattern: '^(.*?) - Size: ([0-9]+?.*?) - Uploader: (.*?) - Tags: (.*?) - (https://.*torrents.php\?)id=(.*)$'
vars: vars:
- torrentName - torrentName
- torrentSize - torrentSize

View file

@ -14,28 +14,38 @@ supports:
source: custom source: custom
settings: settings:
- name: passkey - name: passkey
type: text type: secret
label: Passkey label: Passkey
tooltip: The passkey in your profile. help: "The passkey in your profile."
description: "The passkey in your profile."
irc: irc:
network: FileList network: FileList
server: irc.filelist.io:6697 server: irc.filelist.io
port: 6697 port: 6697
tls: true
channels: channels:
- "#announce" - "#announce"
announcers: announcers:
- Announce - Announce
settings:
- name: nickserv.account
type: text
required: true
label: NickServ Account
help: NickServ account. Make sure to group your user and bot. Eg. user_dl
- name: nickserv.password
type: secret
required: false
label: NickServ Password
help: NickServ password
parse: parse:
type: single type: single
lines: lines:
- - test:
test: - 'New Torrent: This.Really.Old.Movie.1965.DVDRip.DD1.0.x264 -- [Filme SD] [1.91 GB] -- https://filelist.io/details.php?id=000000 -- by uploader1'
- 'New Torrent: This.Really.Old.Movie.1965.DVDRip.DD1.0.x264 -- [Filme SD] [1.91 GB] -- https://filelist.io/details.php?id=746781 -- by uploader1' - 'New Torrent: This.New.Movie.2021.1080p.Blu-ray.AVC.DTS-HD.MA.5.1-BEATRIX -- [FreeLeech!] -- [Filme Blu-Ray] [26.78 GB] -- https://filelist.io/details.php?id=000000 -- by uploader1'
- 'New Torrent: This.New.Movie.2021.1080p.Blu-ray.AVC.DTS-HD.MA.5.1-BEATRIX -- [FreeLeech!] -- [Filme Blu-Ray] [26.78 GB] -- https://filelist.io/details.php?id=746782 -- by uploader1' - 'New Torrent: This.New.Movie.2021.1080p.Remux.AVC.DTS-HD.MA.5.1-playBD -- [FreeLeech!] -- [Internal!] -- [Filme Blu-Ray] [17.69 GB] -- https://filelist.io/details.php?id=000000 -- by uploader1'
- 'New Torrent: This.New.Movie.2021.1080p.Remux.AVC.DTS-HD.MA.5.1-playBD -- [FreeLeech!] -- [Internal!] -- [Filme Blu-Ray] [17.69 GB] -- https://filelist.io/details.php?id=746789 -- by uploader1'
pattern: 'New Torrent: (.*?) (?:-- \[(FreeLeech!)] )?(?:-- \[(Internal!)] )?-- \[(.*)] \[(.*)] -- (https?:\/\/filelist.io\/).*id=(.*) -- by (.*)' pattern: 'New Torrent: (.*?) (?:-- \[(FreeLeech!)] )?(?:-- \[(Internal!)] )?-- \[(.*)] \[(.*)] -- (https?:\/\/filelist.io\/).*id=(.*) -- by (.*)'
vars: vars:
- torrentName - torrentName

View file

@ -14,32 +14,47 @@ supports:
source: gazelle source: gazelle
settings: settings:
- name: authkey - name: authkey
type: text type: secret
label: Auth key label: Auth key
tooltip: Right click DL on a torrent and get the authkey. help: Right click DL on a torrent and get the authkey.
description: Right click DL on a torrent and get the authkey.
- name: torrent_pass - name: torrent_pass
type: text type: secret
label: Torrent pass label: Torrent pass
tooltip: Right click DL on a torrent and get the torrent_pass. help: Right click DL on a torrent and get the torrent_pass.
description: Right click DL on a torrent and get the torrent_pass.
irc: irc:
network: GGn network: GGn
server: irc.gazellegames.net:7000 server: irc.gazellegames.net
port: 7000 port: 7000
tls: true
channels: channels:
- "#GGn-Announce" - "#GGn-Announce"
announcers: announcers:
- Vertigo - Vertigo
settings:
- name: nickserv.account
type: text
required: true
label: NickServ Account
help: NickServ account. Make sure to group your user and bot. Eg. user|bot
- name: nickserv.password
type: secret
required: true
label: NickServ Password
help: NickServ password
- name: invite_command
type: secret
default: "Vertigo ENTER #GGn-Announce USERNAME IRCKey"
required: true
label: Invite command
help: Invite auth with Vertigo.
parse: parse:
type: single type: single
lines: lines:
- - test:
test: - "Uploader :-: Nintendo 3DS :-: Cool.Game.KOR.3DS-BigBlueBox in Cool Game [2013] ::Korean, Multi-Region, Scene:: https://gazellegames.net/torrents.php?torrentid=00000 - adventure, role_playing_game, nintendo;"
- "Uploader :-: Nintendo 3DS :-: Yo-Kai.Watch.KOR.3DS-BigBlueBox in Yo-kai Watch [2013] ::Korean, Multi-Region, Scene:: https://gazellegames.net/torrents.php?torrentid=78851 - adventure, role_playing_game, nintendo;" - "Uploader :-: Windows :-: Other.Game-HI2U in Other Game [2016] ::English, Scene:: FREELEECH! :: https://gazellegames.net/torrents.php?torrentid=00000 - action, adventure, casual, indie, role.playing.game;"
- "Uploader :-: Windows :-: Warriors.Wrath.Evil.Challenge-HI2U in Warriors' Wrath [2016] ::English, Scene:: FREELEECH! :: https://gazellegames.net/torrents.php?torrentid=78902 - action, adventure, casual, indie, role.playing.game;"
pattern: '^(.+) :-: (.+) :-: (.+) \[(\d+)\] ::(.+?):: ?(.+? ::)? https?:\/\/([^\/]+\/)torrents.php\?torrentid=(\d+) ?-? ?(.*?)?;?$' pattern: '^(.+) :-: (.+) :-: (.+) \[(\d+)\] ::(.+?):: ?(.+? ::)? https?:\/\/([^\/]+\/)torrents.php\?torrentid=(\d+) ?-? ?(.*?)?;?$'
vars: vars:
- uploader - uploader

View file

@ -14,26 +14,37 @@ supports:
- rss - rss
source: xbtit source: xbtit
settings: settings:
- name: cookie - name: cookie
type: text type: text
label: Cookie label: Cookie
description: "FireFox -> Preferences -> Privacy -> Show Cookies and find the uid and pass cookies. Example: uid=1234; pass=asdf12347asdf13" help: "Check how to get cookies in your browser and find the uid and pass cookies. Example: uid=1234; pass=asdf12347asdf13"
irc: irc:
network: P2P-NET network: P2P-NET
server: irc.p2p-network.net:6697 server: irc.p2p-network.net
port: 6697 port: 6697
tls: true
channels: channels:
- "#HD-Torrents.Announce" - "#HD-Torrents.Announce"
announcers: announcers:
- HoboLarry - HoboLarry
settings:
- name: nickserv.account
type: text
required: true
label: NickServ Account
help: NickServ account. Make sure to group your user and bot. Eg. user|bot
- name: nickserv.password
type: secret
required: true
label: NickServ Password
help: NickServ password
parse: parse:
type: single type: single
lines: lines:
- - test:
test: - "New Torrent in category [Movies/Remux] That Movie (2008) Blu-ray 1080p REMUX AVC DTS-HD MA 7 1 (14.60 GB) uploaded! Download: https://hd-torrents.org/download.php?id=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
- "New Torrent in category [XXX/Blu-ray] Erotische Fantasien 3D (2008) Blu-ray 1080p AVC DTS-HD MA 7 1 (14.60 GB) uploaded! Download: https://hd-torrents.org/download.php?id=806bc36530d146969d300c5352483a5e6e0639e9"
pattern: 'New Torrent in category \[([^\]]*)\] (.*) \(([^\)]*)\) uploaded! Download\: https?\:\/\/([^\/]+\/).*[&\?]id=([a-f0-9]+)' pattern: 'New Torrent in category \[([^\]]*)\] (.*) \(([^\)]*)\) uploaded! Download\: https?\:\/\/([^\/]+\/).*[&\?]id=([a-f0-9]+)'
vars: vars:
- category - category

View file

@ -15,29 +15,38 @@ supports:
source: unknown source: unknown
settings: settings:
- name: passkey - name: passkey
type: text type: secret
label: Passkey label: Passkey
tooltip: Copy the passkey from your details page help: "Copy the passkey from your details page."
description: "Copy the passkey from your details page."
irc: irc:
network: IPTorrents network: IPTorrents
server: irc.iptorrents.com:6697 server: irc.iptorrents.com
port: 6697 port: 6697
tls: true
channels: channels:
- "#ipt.announce" - "#ipt.announce"
- "#ipt.announce2"
announcers: announcers:
- IPT - IPT
- FunTimes - FunTimes
settings:
- name: nickserv.account
type: text
required: true
label: NickServ Account
help: NickServ account. Make sure to group your user and bot. Eg. user_autodl
- name: nickserv.password
type: secret
required: false
label: NickServ Password
help: NickServ password
parse: parse:
type: single type: single
lines: lines:
- - test:
test: - "[Movies/XviD] The Movie 2010 DVDRip XviD-GROUP FREELEECH - http://www.iptorrents.com/details.php?id=000000 - 716.219 MB"
- "[Movie/XXX] Audrey Bitoni HD Pack FREELEECH - http://www.iptorrents.com/details.php?id=789421 - 14.112 GB" - "[Movies/XviD] The Movie 2010 DVDRip XviD-GROUP - http://www.iptorrents.com/details.php?id=000000 - 716.219 MB"
- "[Movies/XviD] The First Men In The Moon 2010 DVDRip XviD-VoMiT - http://www.iptorrents.com/details.php?id=396589 - 716.219 MB"
pattern: '^\[([^\]]*)](.*?)\s*(FREELEECH)*\s*-\s+https?\:\/\/([^\/]+).*[&\?]id=(\d+)\s*-(.*)' pattern: '^\[([^\]]*)](.*?)\s*(FREELEECH)*\s*-\s+https?\:\/\/([^\/]+).*[&\?]id=(\d+)\s*-(.*)'
vars: vars:
- category - category

View file

@ -13,33 +13,41 @@ supports:
- rss - rss
source: gazelle source: gazelle
settings: settings:
- name: authkey - name: authkey
type: text type: secret
label: Auth key label: Auth key
tooltip: Right click DL on a torrent and get the authkey. help: Right click DL on a torrent and get the authkey.
description: Right click DL on a torrent and get the authkey. - name: torrent_pass
- name: torrent_pass type: secret
type: text label: Torrent pass
label: Torrent pass help: Right click DL on a torrent and get the torrent_pass.
tooltip: Right click DL on a torrent and get the torrent_pass.
description: Right click DL on a torrent and get the torrent_pass.
irc: irc:
network: Nebulance network: Nebulance
server: irc.nebulance.cc:6697 server: irc.nebulance.cc
port: 6697 port: 6697
channels: channels:
- "#nbl-announce" - "#nbl-announce"
announcers: announcers:
- DRADIS - DRADIS
settings:
- name: nickserv.account
type: text
required: true
label: NickServ Account
help: NickServ account. Make sure to group your user and bot. Eg. user|bot
- name: nickserv.password
type: secret
required: true
label: NickServ Password
help: NickServ password
parse: parse:
type: single type: single
lines: lines:
- - test:
test: - "[Episodes] The Show - S02E08 [WebRip / x264 / MKV / 720p / HD / VLAD / The.Show.S02E08.Episode.Name.720p.ANPL.WEBRip.AAC2.0.x264-GROUP.mkv] [702.00 MB - Uploader: UPLOADER] - http://nebulance.io/torrents.php?id=000 [Tags: comedy,subtitles,cbs]"
- "[Episodes] The Vet Life - S02E08 [WebRip / x264 / MKV / 720p / HD / VLAD / The.Vet.Life.S02E08.Tuskegee.Reunion.720p.ANPL.WEBRip.AAC2.0.x264-VLAD.mkv] [702.00 MB - Uploader: UPLOADER] - http://nebulance.io/torrents.php?id=147 [Tags: comedy,subtitles,cbs]" - "[Seasons] Other Show - S10 [HDTV / x264 / MKV / MP4 / 480p / SD / BTN / Other.Show.S10.HDTV.x264-GROUP] [5.27 GB - Uploader: UPLOADER] - http://nebulance.io/torrents.php?id=0000 [Tags: comedy,subtitles,cbs]"
- "[Seasons] Police Interceptors - S10 [HDTV / x264 / MKV / MP4 / 480p / SD / BTN / Police.Interceptors.S10.HDTV.x264-BTN] [5.27 GB - Uploader: UPLOADER] - http://nebulance.io/torrents.php?id=1472 [Tags: comedy,subtitles,cbs]"
pattern: '\[(.*?)\] (.*?) \[(.*?)\] \[(.*?) - Uploader: (.*?)\] - (https?://.*)id=(\d+) \[Tags: (.*)\]' pattern: '\[(.*?)\] (.*?) \[(.*?)\] \[(.*?) - Uploader: (.*?)\] - (https?://.*)id=(\d+) \[Tags: (.*)\]'
vars: vars:
- category - category

View file

@ -13,33 +13,48 @@ supports:
- rss - rss
source: gazelle source: gazelle
settings: settings:
- name: authkey - name: authkey
type: text type: text
label: Auth key label: Auth key
tooltip: Right click DL on a torrent and get the authkey. help: Right click DL on a torrent and get the authkey.
description: Right click DL on a torrent and get the authkey. - name: torrent_pass
- name: torrent_pass type: text
type: text label: Torrent pass
label: Torrent pass help: Right click DL on a torrent and get the torrent_pass.
tooltip: Right click DL on a torrent and get the torrent_pass.
description: Right click DL on a torrent and get the torrent_pass.
irc: irc:
network: Orpheus network: Orpheus
server: irc.orpheus.network:7000 server: irc.orpheus.network
port: 7000 port: 7000
tls: true
channels: channels:
- "#announce" - "#announce"
announcers: announcers:
- hermes - hermes
settings:
- name: nickserv.account
type: text
required: true
label: NickServ Account
help: NickServ account. Make sure to group your user and bot. Eg. user|bot
- name: nickserv.password
type: secret
required: true
label: NickServ Password
help: NickServ password
- name: invite_command
type: secret
default: "hermes enter #announce USERNAME IRCKey"
required: true
label: Invite command
help: Invite auth with Hermes.
parse: parse:
type: single type: single
lines: lines:
- - test:
test: - "TORRENT: That Artist - Albuum [2002] [Single] - FLAC / Lossless / WEB - 2000s,house,uk.garage,garage.house - https://orpheus.network/torrents.php?id=000000 / https://orpheus.network/torrents.php?action=download&id=0000000"
- "TORRENT: Todd Edwards - You Came To Me [2002] [Single] - FLAC / Lossless / WEB - 2000s,house,uk.garage,garage.house - https://orpheus.network/torrents.php?id=756102 / https://orpheus.network/torrents.php?action=download&id=1647868" - "TORRENT: Something [2021] [Album] - FLAC / Lossless / CD - - https://orpheus.network/torrents.php?id=000000 / https://orpheus.network/torrents.php?action=download&id=0000000"
- "TORRENT: THE BOOK [2021] [Album] - FLAC / Lossless / CD - - https://orpheus.network/torrents.php?id=693523 / https://orpheus.network/torrents.php?action=download&id=1647867"
pattern: 'TORRENT: (.*) - (.*) - https?://.* / (https?://.*id=\d+)' pattern: 'TORRENT: (.*) - (.*) - https?://.* / (https?://.*id=\d+)'
vars: vars:
- torrentName - torrentName

View file

@ -13,33 +13,48 @@ supports:
- rss - rss
source: gazelle source: gazelle
settings: settings:
- name: authkey - name: authkey
type: text type: secret
label: Auth key label: Auth key
tooltip: Right click DL on a torrent and get the authkey. help: Right click DL on a torrent and get the authkey.
description: Right click DL on a torrent and get the authkey. - name: torrent_pass
- name: torrent_pass type: secret
type: text label: Torrent pass
label: Torrent pass help: Right click DL on a torrent and get the torrent_pass.
tooltip: Right click DL on a torrent and get the torrent_pass.
description: Right click DL on a torrent and get the torrent_pass.
irc: irc:
network: PassThePopcorn network: PassThePopcorn
server: irc.passthepopcorn.me:7000 server: irc.passthepopcorn.me
port: 7000 port: 7000
tls: true
channels: channels:
- "#ptp-announce" - "#ptp-announce"
announcers: announcers:
- Hummingbird - Hummingbird
settings:
- name: nickserv.account
type: text
required: true
label: NickServ Account
help: NickServ account. Make sure to group your user and bot. Eg. user|autodl
- name: nickserv.password
type: secret
required: true
label: NickServ Password
help: NickServ password
- name: invite_command
type: secret
default: "Hummingbird ENTER USERNAME IRCKey #ptp-announce"
required: true
label: Invite command
help: Invite auth with Hummingbird.
parse: parse:
type: single type: single
lines: lines:
- - test:
test: - "That Movie [2008] by Director - XviD / DVD / AVI / 640x352 - http://passthepopcorn.me/torrents.php?id=00000 / http://passthepopcorn.me/torrents.php?action=download&id=00000 - crime, drama, mystery"
- "Irene Huss - Nattrond AKA The Night Round [2008] by Anders Engström - XviD / DVD / AVI / 640x352 - http://passthepopcorn.me/torrents.php?id=51627 / http://passthepopcorn.me/torrents.php?action=download&id=97333 - crime, drama, mystery" - "Some Old Movie [1988] by Director - x264 / Blu-ray / MKV / 720p - http://passthepopcorn.me/torrents.php?id=00000 / http://passthepopcorn.me/torrents.php?action=download&id=00000 - comedy, crime"
- "Dirty Rotten Scoundrels [1988] by Frank Oz - x264 / Blu-ray / MKV / 720p - http://passthepopcorn.me/torrents.php?id=10735 / http://passthepopcorn.me/torrents.php?action=download&id=97367 - comedy, crime"
pattern: '^(.*)-\s*https?:.*[&\?]id=.*https?\:\/\/([^\/]+\/).*[&\?]id=(\d+)\s*-\s*(.*)' pattern: '^(.*)-\s*https?:.*[&\?]id=.*https?\:\/\/([^\/]+\/).*[&\?]id=(\d+)\s*-\s*(.*)'
vars: vars:
- torrentName - torrentName

View file

@ -13,33 +13,48 @@ supports:
- rss - rss
source: gazelle source: gazelle
settings: settings:
- name: authkey - name: authkey
type: text type: secret
label: Auth key label: Auth key
tooltip: Right click DL on a torrent and get the authkey. help: Right click DL on a torrent and get the authkey.
description: Right click DL on a torrent and get the authkey. - name: torrent_pass
- name: torrent_pass type: secret
type: text label: Torrent pass
label: Torrent pass help: Right click DL on a torrent and get the torrent_pass.
tooltip: Right click DL on a torrent and get the torrent_pass.
description: Right click DL on a torrent and get the torrent_pass.
irc: irc:
network: Scratch-Network network: Scratch-Network
server: irc.scratch-network.net:6697 server: irc.scratch-network.net
port: 6697 port: 6697
tls: true
channels: channels:
- "#red-announce" - "#red-announce"
announcers: announcers:
- Drone - Drone
settings:
- name: nickserv.account
type: text
required: true
label: NickServ Account
help: NickServ account. Make sure to group your user and bot. Eg. user-autodl
- name: nickserv.password
type: secret
required: true
label: NickServ Password
help: NickServ password
- name: invite_command
type: secret
default: "Drone enter #red-announce USERNAME IRCKey"
required: true
label: Invite command
help: Invite auth with Drone.
parse: parse:
type: single type: single
lines: lines:
- - test:
test: - "Artist - Albumname [2008] [Single] - FLAC / Lossless / Log / 100% / Cue / CD - https://redacted.ch/torrents.php?id=0000000 / https://redacted.ch/torrents.php?action=download&id=0000000 - hip.hop,rhythm.and.blues,2000s"
- "JR Get Money - Nobody But You [2008] [Single] - FLAC / Lossless / Log / 100% / Cue / CD - https://redacted.ch/torrents.php?id=1592366 / https://redacted.ch/torrents.php?action=download&id=3372962 - hip.hop,rhythm.and.blues,2000s" - "A really long name here - Concertos 5 and 6, Suite No 2 [1991] [Album] - FLAC / Lossless / Log / 100% / Cue / CD - https://redacted.ch/torrents.php?id=0000000 / https://redacted.ch/torrents.php?action=download&id=0000000 - classical"
- "Johann Sebastian Bach performed by Festival Strings Lucerne under Rudolf Baumgartner - Brandenburg Concertos 5 and 6, Suite No 2 [1991] [Album] - FLAC / Lossless / Log / 100% / Cue / CD - https://redacted.ch/torrents.php?id=1592367 / https://redacted.ch/torrents.php?action=download&id=3372963 - classical"
pattern: '^(.*)\s+-\s+https?:.*[&\?]id=.*https?\:\/\/([^\/]+\/).*[&\?]id=(\d+)\s*-\s*(.*)' pattern: '^(.*)\s+-\s+https?:.*[&\?]id=.*https?\:\/\/([^\/]+\/).*[&\?]id=(\d+)\s*-\s*(.*)'
vars: vars:
- torrentName - torrentName

View file

@ -14,27 +14,37 @@ supports:
source: rartracker source: rartracker
settings: settings:
- name: passkey - name: passkey
type: text type: secret
label: Passkey label: Passkey
tooltip: Copy the passkey from the /rss page help: "Copy the passkey from the /rss page."
description: "Copy the passkey from the /rss page."
irc: irc:
network: SuperBits network: SuperBits
server: irc.superbits.org:6697 server: irc.superbits.org
port: 6697 port: 6697
tls: true
channels: channels:
- "#autodl" - "#autodl"
announcers: announcers:
- SuperBits - SuperBits
settings:
- name: nickserv.account
type: text
required: true
label: NickServ Account
help: NickServ account. Make sure to group your user and bot. Eg. user-bot
- name: nickserv.password
type: secret
required: false
label: NickServ Password
help: NickServ password
parse: parse:
type: single type: single
lines: lines:
- - test:
test: - "-[archive Film 1080]2[A.Movie.1985.FRENCH.1080p.BluRay.x264-GROUP]3[000000]4[Size: 4.41 GB]5[FL: no]6[Scene: yes]"
- "-[archive Film 1080]2[Asterix.Et.La.Surprise.De.Cesar.1985.FRENCH.1080p.BluRay.x264-TSuNaMi]3[844551]4[Size: 4.41 GB]5[FL: no]6[Scene: yes]" - "-[new TV]2[Some.Show.S05E05.720p.WEB.h264-GROUP]3[000000]4[Size: 964.04 MB]5[FL: no]6[Scene: yes]7[Pred 1m 30s ago]"
- "-[new TV]2[Party.Down.South.S05E05.720p.WEB.h264-DiRT]3[844557]4[Size: 964.04 MB]5[FL: no]6[Scene: yes]7[Pred 1m 30s ago]"
pattern: '\-\[(.*)\]2\[(.*)\]3\[(\d+)\]4\[Size\:\s(.*)\]5\[FL\:\s(no|yes)\]6\[Scene\:\s(no|yes)\](?:7\[Pred\s(.*)\sago\])?' pattern: '\-\[(.*)\]2\[(.*)\]3\[(\d+)\]4\[Size\:\s(.*)\]5\[FL\:\s(no|yes)\]6\[Scene\:\s(no|yes)\](?:7\[Pred\s(.*)\sago\])?'
vars: vars:
- category - category

View file

@ -14,28 +14,38 @@ supports:
source: custom source: custom
settings: settings:
- name: rsskey - name: rsskey
type: text type: secret
label: RSS key label: RSS key
tooltip: The rsskey in your TorrentLeech RSS feed link. help: "Go to your profile and copy your RSS key"
description: "Go to your profile and copy and paste your RSS link to extract the rsskey."
regex: /([\da-fA-F]{20}) regex: /([\da-fA-F]{20})
irc: irc:
network: TorrentLeech.org network: TorrentLeech.org
server: irc.torrentleech.org:7021 server: irc.torrentleech.org
port: 7021 port: 7021
tls: true
channels: channels:
- "#tlannounces" - "#tlannounces"
announcers: announcers:
- _AnnounceBot_ - _AnnounceBot_
settings:
- name: nickserv.account
type: text
required: false
label: NickServ Account
help: NickServ account. Make sure to group your user and bot. Eg. user_bot
- name: nickserv.password
type: secret
required: false
label: NickServ Password
help: NickServ password
parse: parse:
type: single type: single
lines: lines:
- - test:
test: - "New Torrent Announcement: <PC :: Iso> Name:'debian live 10 6 0 amd64 standard iso' uploaded by 'Anonymous' - http://www.tracker01.test/torrent/000000"
- "New Torrent Announcement: <PC :: Iso> Name:'debian live 10 6 0 amd64 standard iso' uploaded by 'Anonymous' - http://www.tracker01.test/torrent/263302" - "New Torrent Announcement: <PC :: Iso> Name:'debian live 10 6 0 amd64 standard iso' uploaded by 'Anonymous' freeleech - http://www.tracker01.test/torrent/000000"
- "New Torrent Announcement: <PC :: Iso> Name:'debian live 10 6 0 amd64 standard iso' uploaded by 'Anonymous' freeleech - http://www.tracker01.test/torrent/263302"
pattern: New Torrent Announcement:\s*<([^>]*)>\s*Name:'(.*)' uploaded by '([^']*)'\s*(freeleech)*\s*-\s*https?\:\/\/([^\/]+\/)torrent\/(\d+) pattern: New Torrent Announcement:\s*<([^>]*)>\s*Name:'(.*)' uploaded by '([^']*)'\s*(freeleech)*\s*-\s*https?\:\/\/([^\/]+\/)torrent\/(\d+)
vars: vars:
- category - category

View file

@ -13,33 +13,48 @@ supports:
- rss - rss
source: gazelle source: gazelle
settings: settings:
- name: authkey - name: authkey
type: text type: secret
label: Auth key label: Auth key
tooltip: Right click DL on a torrent and get the authkey. help: Right click DL on a torrent and get the authkey.
description: Right click DL on a torrent and get the authkey. - name: torrent_pass
- name: torrent_pass type: secret
type: text label: Torrent pass
label: Torrent pass help: Right click DL on a torrent and get the torrent_pass.
tooltip: Right click DL on a torrent and get the torrent_pass.
description: Right click DL on a torrent and get the torrent_pass.
irc: irc:
network: P2P-Network network: P2P-Network
server: irc.p2p-network.net:6697 server: irc.p2p-network.net
port: 6697 port: 6697
tls: true
channels: channels:
- "#UHD.Announce" - "#UHD.Announce"
announcers: announcers:
- UHDBot - UHDBot
- cr0nusbot - cr0nusbot
settings:
- name: nickserv.account
type: text
required: true
label: NickServ Account
help: NickServ account. Make sure to group your user and bot. Eg. user|autodl
- name: nickserv.password
type: secret
required: true
label: NickServ Password
help: NickServ password
- name: invite_command
type: secret
default: "UHDBot invite IRCKey"
required: true
label: Invite command
help: Invite auth with UHDBot.
parse: parse:
type: single type: single
lines: lines:
- - test:
test: - "New Torrent: A Movie [2015] - GROUP Type: Movie / 1080p / Encode / Freeleech: 100 Size: 7.00GB - https://uhdbits.org/torrents.php?id=00000 / https://uhdbits.org/torrents.php?action=download&id=00000"
- "New Torrent: D'Ardennen [2015] - TayTO Type: Movie / 1080p / Encode / Freeleech: 100 Size: 7.00GB - https://uhdbits.org/torrents.php?id=13882 / https://uhdbits.org/torrents.php?action=download&id=20488"
pattern: 'New Torrent: (.*) Type: (.*?) Freeleech: (.*) Size: (.*) - https?:\/\/.* \/ (https?:\/\/.*id=\d+)' pattern: 'New Torrent: (.*) Type: (.*?) Freeleech: (.*) Size: (.*) - https?:\/\/.* \/ (https?:\/\/.*id=\d+)'
vars: vars:
- torrentName - torrentName

View file

@ -5,9 +5,8 @@ import (
"io/fs" "io/fs"
"strings" "strings"
"gopkg.in/yaml.v2"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"gopkg.in/yaml.v2"
"github.com/autobrr/autobrr/internal/domain" "github.com/autobrr/autobrr/internal/domain"
) )
@ -26,9 +25,15 @@ type Service interface {
} }
type service struct { type service struct {
repo domain.IndexerRepo repo domain.IndexerRepo
indexerDefinitions map[string]domain.IndexerDefinition
indexerInstances map[string]domain.IndexerDefinition // contains all raw indexer definitions
indexerDefinitions map[string]domain.IndexerDefinition
// contains indexers with data set
indexerInstances map[string]domain.IndexerDefinition
// map server:channel:announce to indexer.Identifier
mapIndexerIRCToName map[string]string mapIndexerIRCToName map[string]string
} }
@ -44,6 +49,14 @@ func NewService(repo domain.IndexerRepo) Service {
func (s *service) Store(indexer domain.Indexer) (*domain.Indexer, error) { func (s *service) Store(indexer domain.Indexer) (*domain.Indexer, error) {
i, err := s.repo.Store(indexer) i, err := s.repo.Store(indexer)
if err != nil { if err != nil {
log.Error().Stack().Err(err).Msgf("failed to store indexer: %v", indexer.Name)
return nil, err
}
// add to indexerInstances
err = s.addIndexer(*i)
if err != nil {
log.Error().Stack().Err(err).Msgf("failed to add indexer: %v", indexer.Name)
return nil, err return nil, err
} }
@ -56,6 +69,13 @@ func (s *service) Update(indexer domain.Indexer) (*domain.Indexer, error) {
return nil, err return nil, err
} }
// add to indexerInstances
err = s.addIndexer(*i)
if err != nil {
log.Error().Stack().Err(err).Msgf("failed to add indexer: %v", indexer.Name)
return nil, err
}
return i, nil return i, nil
} }
@ -94,46 +114,60 @@ func (s *service) GetAll() ([]*domain.IndexerDefinition, error) {
var res = make([]*domain.IndexerDefinition, 0) var res = make([]*domain.IndexerDefinition, 0)
for _, indexer := range indexers { for _, indexer := range indexers {
in := s.getDefinitionByName(indexer.Identifier) indexerDefinition, err := s.mapIndexer(indexer)
if in == nil { if err != nil {
// if no indexerDefinition found, continue
continue continue
} }
temp := domain.IndexerDefinition{ if indexerDefinition == nil {
ID: indexer.ID, continue
Name: in.Name,
Identifier: in.Identifier,
Enabled: indexer.Enabled,
Description: in.Description,
Language: in.Language,
Privacy: in.Privacy,
Protocol: in.Protocol,
URLS: in.URLS,
Settings: nil,
SettingsMap: make(map[string]string),
IRC: in.IRC,
Parse: in.Parse,
} }
// map settings res = append(res, indexerDefinition)
// add value to settings objects
for _, setting := range in.Settings {
if v, ok := indexer.Settings[setting.Name]; ok {
setting.Value = v
temp.SettingsMap[setting.Name] = v
}
temp.Settings = append(temp.Settings, setting)
}
res = append(res, &temp)
} }
return res, nil return res, nil
} }
func (s *service) mapIndexer(indexer domain.Indexer) (*domain.IndexerDefinition, error) {
in := s.getDefinitionByName(indexer.Identifier)
if in == nil {
// if no indexerDefinition found, continue
return nil, nil
}
indexerDefinition := domain.IndexerDefinition{
ID: int(indexer.ID),
Name: in.Name,
Identifier: in.Identifier,
Enabled: indexer.Enabled,
Description: in.Description,
Language: in.Language,
Privacy: in.Privacy,
Protocol: in.Protocol,
URLS: in.URLS,
Settings: nil,
SettingsMap: make(map[string]string),
IRC: in.IRC,
Parse: in.Parse,
}
// map settings
// add value to settings objects
for _, setting := range in.Settings {
if v, ok := indexer.Settings[setting.Name]; ok {
setting.Value = v
indexerDefinition.SettingsMap[setting.Name] = v
}
indexerDefinition.Settings = append(indexerDefinition.Settings, setting)
}
return &indexerDefinition, nil
}
func (s *service) GetTemplates() ([]domain.IndexerDefinition, error) { func (s *service) GetTemplates() ([]domain.IndexerDefinition, error) {
definitions := s.indexerDefinitions definitions := s.indexerDefinitions
@ -152,44 +186,81 @@ func (s *service) Start() error {
return err return err
} }
indexers, err := s.GetAll() indexerDefinitions, err := s.GetAll()
if err != nil { if err != nil {
return err return err
} }
for _, indexer := range indexers { for _, indexerDefinition := range indexerDefinitions {
if !indexer.Enabled { s.indexerInstances[indexerDefinition.Identifier] = *indexerDefinition
continue
}
s.indexerInstances[indexer.Identifier] = *indexer s.mapIRCIndexerLookup(indexerDefinition.Identifier, *indexerDefinition)
// map irc stuff to indexer.name
if indexer.IRC != nil {
server := indexer.IRC.Server
for _, channel := range indexer.IRC.Channels {
for _, announcer := range indexer.IRC.Announcers {
val := fmt.Sprintf("%v:%v:%v", server, channel, announcer)
s.mapIndexerIRCToName[val] = indexer.Identifier
}
}
}
} }
return nil return nil
} }
func (s *service) removeIndexer(indexer domain.Indexer) error {
delete(s.indexerDefinitions, indexer.Identifier)
// TODO delete from mapIndexerIRCToName
return nil
}
func (s *service) addIndexer(indexer domain.Indexer) error {
// TODO only add if not already there?? Overwrite?
indexerDefinition, err := s.mapIndexer(indexer)
if err != nil {
return err
}
// TODO only add enabled?
//if !indexer.Enabled {
// continue
//}
s.indexerInstances[indexerDefinition.Identifier] = *indexerDefinition
s.mapIRCIndexerLookup(indexer.Identifier, *indexerDefinition)
return nil
}
func (s *service) mapIRCIndexerLookup(indexerIdentifier string, indexerDefinition domain.IndexerDefinition) {
// map irc stuff to indexer.name
// map[irc.network.test:channel:announcer1] = indexer1
// map[irc.network.test:channel:announcer2] = indexer2
if indexerDefinition.IRC != nil {
server := indexerDefinition.IRC.Server
channels := indexerDefinition.IRC.Channels
announcers := indexerDefinition.IRC.Announcers
for _, channel := range channels {
for _, announcer := range announcers {
// format to server:channel:announcer
val := fmt.Sprintf("%v:%v:%v", server, channel, announcer)
val = strings.ToLower(val)
s.mapIndexerIRCToName[val] = indexerIdentifier
}
}
}
}
// LoadIndexerDefinitions load definitions from golang embed fs // LoadIndexerDefinitions load definitions from golang embed fs
func (s *service) LoadIndexerDefinitions() error { func (s *service) LoadIndexerDefinitions() error {
entries, err := fs.ReadDir(Definitions, "definitions") entries, err := fs.ReadDir(Definitions, "definitions")
if err != nil { if err != nil {
log.Fatal().Msgf("failed reading directory: %s", err) log.Fatal().Stack().Msgf("failed reading directory: %s", err)
} }
if len(entries) == 0 { if len(entries) == 0 {
log.Fatal().Msgf("failed reading directory: %s", err) log.Fatal().Stack().Msgf("failed reading directory: %s", err)
return err return err
} }
@ -197,19 +268,19 @@ func (s *service) LoadIndexerDefinitions() error {
filePath := "definitions/" + f.Name() filePath := "definitions/" + f.Name()
if strings.Contains(f.Name(), ".yaml") { if strings.Contains(f.Name(), ".yaml") {
log.Debug().Msgf("parsing: %v", filePath) log.Trace().Msgf("parsing: %v", filePath)
var d domain.IndexerDefinition var d domain.IndexerDefinition
data, err := fs.ReadFile(Definitions, filePath) data, err := fs.ReadFile(Definitions, filePath)
if err != nil { if err != nil {
log.Debug().Err(err).Msgf("failed reading file: %v", filePath) log.Error().Stack().Err(err).Msgf("failed reading file: %v", filePath)
return err return err
} }
err = yaml.Unmarshal(data, &d) err = yaml.Unmarshal(data, &d)
if err != nil { if err != nil {
log.Error().Err(err).Msgf("failed unmarshal file: %v", filePath) log.Error().Stack().Err(err).Msgf("failed unmarshal file: %v", filePath)
return err return err
} }
@ -217,10 +288,13 @@ func (s *service) LoadIndexerDefinitions() error {
} }
} }
log.Info().Msgf("Loaded %d indexer definitions", len(s.indexerDefinitions))
return nil return nil
} }
func (s *service) GetIndexerByAnnounce(name string) *domain.IndexerDefinition { func (s *service) GetIndexerByAnnounce(name string) *domain.IndexerDefinition {
name = strings.ToLower(name)
if identifier, idOk := s.mapIndexerIRCToName[name]; idOk { if identifier, idOk := s.mapIndexerIRCToName[name]; idOk {
if indexer, ok := s.indexerInstances[identifier]; ok { if indexer, ok := s.indexerInstances[identifier]; ok {

View file

@ -25,14 +25,19 @@ type Handler struct {
network *domain.IrcNetwork network *domain.IrcNetwork
announceService announce.Service announceService announce.Service
client *irc.Client
conn net.Conn conn net.Conn
ctx context.Context ctx context.Context
stopped chan struct{} stopped chan struct{}
cancel context.CancelFunc cancel context.CancelFunc
lastPing time.Time
lastAnnounce time.Time
} }
func NewHandler(network domain.IrcNetwork, announceService announce.Service) *Handler { func NewHandler(network domain.IrcNetwork, announceService announce.Service) *Handler {
return &Handler{ return &Handler{
client: nil,
conn: nil, conn: nil,
ctx: nil, ctx: nil,
stopped: make(chan struct{}), stopped: make(chan struct{}),
@ -44,7 +49,7 @@ func NewHandler(network domain.IrcNetwork, announceService announce.Service) *Ha
func (s *Handler) Run() error { func (s *Handler) Run() error {
//log.Debug().Msgf("server %+v", s.network) //log.Debug().Msgf("server %+v", s.network)
if s.network.Addr == "" { if s.network.Server == "" {
return errors.New("addr not set") return errors.New("addr not set")
} }
@ -59,7 +64,7 @@ func (s *Handler) Run() error {
var netConn net.Conn var netConn net.Conn
var err error var err error
addr := s.network.Addr addr := fmt.Sprintf("%v:%v", s.network.Server, s.network.Port)
// decide to use SSL or not // decide to use SSL or not
if s.network.TLS { if s.network.TLS {
@ -88,9 +93,9 @@ func (s *Handler) Run() error {
log.Info().Msgf("Connected to: %v", addr) log.Info().Msgf("Connected to: %v", addr)
config := irc.ClientConfig{ config := irc.ClientConfig{
Nick: s.network.Nick, Nick: s.network.NickServ.Account,
User: s.network.Nick, User: s.network.NickServ.Account,
Name: s.network.Nick, Name: s.network.NickServ.Account,
Pass: s.network.Pass, Pass: s.network.Pass,
Handler: irc.HandlerFunc(func(c *irc.Client, m *irc.Message) { Handler: irc.HandlerFunc(func(c *irc.Client, m *irc.Message) {
switch m.Command { switch m.Command {
@ -101,38 +106,66 @@ func (s *Handler) Run() error {
log.Error().Msgf("error joining channels %v", err) log.Error().Msgf("error joining channels %v", err)
} }
case "366": // 322 TOPIC
// TODO: handle joined // 333 UP
log.Debug().Msgf("JOINED: %v", m) // 353 @
// 396 Displayed host
case "366": // JOINED
s.handleJoined(m)
case "JOIN":
log.Debug().Msgf("%v: JOIN %v", s.network.Server, m.Trailing())
case "433": case "433":
// TODO: handle nick in use // TODO: handle nick in use
log.Debug().Msgf("NICK IN USE: %v", m) log.Debug().Msgf("%v: NICK IN USE: %v", s.network.Server, m)
case "448", "475", "477": case "448", "473", "475", "477":
// TODO: handle join failed // TODO: handle join failed
log.Debug().Msgf("JOIN FAILED: %v", m) log.Debug().Msgf("%v: JOIN FAILED %v: %v", s.network.Server, m.Params[1], m.Trailing())
case "900": // Invite bot logged in
log.Debug().Msgf("%v: %v", s.network.Server, m.Trailing())
case "KICK": case "KICK":
log.Debug().Msgf("KICK: %v", m) log.Debug().Msgf("%v: KICK: %v", s.network.Server, m)
case "MODE": case "MODE":
// TODO: handle mode change err := s.handleMode(m)
log.Debug().Msgf("MODE CHANGE: %v", m) if err != nil {
log.Error().Err(err).Msgf("error MODE change: %v", m)
}
case "INVITE": case "INVITE":
// TODO: handle invite // TODO: handle invite
log.Debug().Msgf("INVITE: %v", m) log.Debug().Msgf("%v: INVITE: %v", s.network.Server, m)
case "PART": case "PART":
// TODO: handle parted // TODO: handle parted
log.Debug().Msgf("PART: %v", m) log.Debug().Msgf("%v: PART: %v", s.network.Server, m)
case "PRIVMSG": case "PRIVMSG":
err := s.onMessage(m) err := s.onMessage(m)
if err != nil { if err != nil {
log.Error().Msgf("error on message %v", err) log.Error().Msgf("error on message %v", err)
} }
case "CAP":
log.Debug().Msgf("%v: CAP: %v", s.network.Server, m)
case "NOTICE":
log.Trace().Msgf("%v: %v", s.network.Server, m.Trailing())
case "PING":
err := s.handlePing(m)
if err != nil {
log.Error().Stack().Err(err)
}
//case "372":
// log.Debug().Msgf("372: %v", m)
default:
log.Trace().Msgf("%v: %v", s.network.Server, m.Trailing())
} }
}), }),
} }
@ -140,6 +173,8 @@ func (s *Handler) Run() error {
// Create the client // Create the client
client := irc.NewClient(s.conn, config) client := irc.NewClient(s.conn, config)
s.client = client
// Connect // Connect
err = client.RunContext(ctx) err = client.RunContext(ctx)
if err != nil { if err != nil {
@ -157,9 +192,9 @@ func (s *Handler) GetNetwork() *domain.IrcNetwork {
func (s *Handler) Stop() { func (s *Handler) Stop() {
s.cancel() s.cancel()
//if !s.isStopped() { if !s.isStopped() {
// close(s.stopped) close(s.stopped)
//} }
if s.conn != nil { if s.conn != nil {
s.conn.Close() s.conn.Close()
@ -176,41 +211,40 @@ func (s *Handler) isStopped() bool {
} }
func (s *Handler) onConnect(client *irc.Client, channels []domain.IrcChannel) error { func (s *Handler) onConnect(client *irc.Client, channels []domain.IrcChannel) error {
// TODO check commands like nickserv before joining identified := false
for _, command := range s.network.ConnectCommands { time.Sleep(2 * time.Second)
cmd := strings.TrimLeft(command, "/")
log.Info().Msgf("send connect command: %v to network: %s", cmd, s.network.Name) if s.network.NickServ.Password != "" {
err := s.handleNickServPRIVMSG(s.network.NickServ.Account, s.network.NickServ.Password)
err := client.Write(cmd)
if err != nil { if err != nil {
log.Error().Err(err).Msgf("error sending connect command %v to network: %v", command, s.network.Name) log.Error().Err(err).Msgf("error nickserv: %v", s.network.Name)
continue return err
//return err
} }
identified = true
time.Sleep(1 * time.Second)
} }
for _, ch := range channels { time.Sleep(3 * time.Second)
myChan := fmt.Sprintf("JOIN %s", ch.Name)
// handle channel password if s.network.InviteCommand != "" {
if ch.Password != "" {
myChan = fmt.Sprintf("JOIN %s %s", ch.Name, ch.Password)
}
err := client.Write(myChan) err := s.handleInvitePRIVMSG(s.network.InviteCommand)
if err != nil { if err != nil {
log.Error().Err(err).Msgf("error joining channel: %v", ch.Name) log.Error().Err(err).Msgf("error sending connect command %v to network: %v", s.network.InviteCommand, s.network.Name)
continue return err
//return err
} }
log.Info().Msgf("Monitoring channel %s", ch.Name) time.Sleep(2 * time.Second)
}
time.Sleep(1 * time.Second) if !identified {
for _, channel := range channels {
err := s.handleJoinChannel(channel.Name)
if err != nil {
log.Error().Err(err)
return err
}
}
} }
return nil return nil
@ -221,7 +255,12 @@ func (s *Handler) OnJoin(msg string) (interface{}, error) {
} }
func (s *Handler) onMessage(msg *irc.Message) error { func (s *Handler) onMessage(msg *irc.Message) error {
log.Debug().Msgf("msg: %v", msg) //log.Debug().Msgf("raw msg: %v", msg)
// check if message is from announce bot and correct channel, if not return
//if msg.Name != s.network. {
//
//}
// parse announce // parse announce
channel := &msg.Params[0] channel := &msg.Params[0]
@ -231,10 +270,14 @@ func (s *Handler) onMessage(msg *irc.Message) error {
// add correlationID and tracing // add correlationID and tracing
announceID := fmt.Sprintf("%v:%v:%v", s.network.Addr, *channel, *announcer) announceID := fmt.Sprintf("%v:%v:%v", s.network.Server, *channel, *announcer)
announceID = strings.ToLower(announceID)
// clean message // clean message
cleanedMsg := cleanMessage(message) cleanedMsg := cleanMessage(message)
log.Debug().Msgf("%v: %v %v: %v", s.network.Server, *channel, *announcer, cleanedMsg)
s.lastAnnounce = time.Now()
go func() { go func() {
err := s.announceService.Parse(announceID, cleanedMsg) err := s.announceService.Parse(announceID, cleanedMsg)
@ -246,6 +289,145 @@ func (s *Handler) onMessage(msg *irc.Message) error {
return nil return nil
} }
func (s *Handler) sendPrivMessage(msg string) error {
msg = strings.TrimLeft(msg, "/")
privMsg := fmt.Sprintf("PRIVMSG %s", msg)
err := s.client.Write(privMsg)
if err != nil {
log.Error().Err(err).Msgf("could not send priv msg: %v", msg)
return err
}
return nil
}
func (s *Handler) handleJoinChannel(channel string) error {
m := irc.Message{
Command: "JOIN",
Params: []string{channel},
}
log.Debug().Msgf("%v: %v", s.network.Server, m.String())
time.Sleep(1 * time.Second)
err := s.client.Write(m.String())
if err != nil {
log.Error().Err(err).Msgf("error handling join: %v", m.String())
return err
}
//log.Info().Msgf("Monitoring channel %v %s", s.network.Name, channel)
return nil
}
func (s *Handler) handleJoined(msg *irc.Message) {
log.Debug().Msgf("%v: JOINED: %v", s.network.Server, msg.Trailing())
log.Info().Msgf("%v: Monitoring channel %s", s.network.Server, msg.Params[1])
}
func (s *Handler) handleInvitePRIVMSG(msg string) error {
msg = strings.TrimPrefix(msg, "/msg")
split := strings.Split(msg, " ")
m := irc.Message{
Command: "PRIVMSG",
Params: split,
}
log.Info().Msgf("%v: Invite command: %v", s.network.Server, m.String())
err := s.client.Write(m.String())
if err != nil {
log.Error().Err(err).Msgf("error handling invite: %v", m.String())
return err
}
return nil
}
func (s *Handler) handlePRIVMSG(msg string) error {
msg = strings.TrimLeft(msg, "/")
m := irc.Message{
Command: "PRIVMSG",
Params: []string{msg},
}
log.Debug().Msgf("%v: Handle privmsg: %v", s.network.Server, m.String())
err := s.client.Write(m.String())
if err != nil {
log.Error().Err(err).Msgf("error handling PRIVMSG: %v", m.String())
return err
}
return nil
}
func (s *Handler) handleNickServPRIVMSG(nick, password string) error {
m := irc.Message{
Command: "PRIVMSG",
Params: []string{"NickServ", "IDENTIFY", nick, password},
}
log.Debug().Msgf("%v: NickServ: %v", s.network.Server, m.String())
err := s.client.Write(m.String())
if err != nil {
log.Error().Err(err).Msgf("error identifying with nickserv: %v", m.String())
return err
}
return nil
}
func (s *Handler) handleMode(msg *irc.Message) error {
log.Debug().Msgf("%v: MODE: %v %v", s.network.Server, msg.User, msg.Trailing())
time.Sleep(2 * time.Second)
if s.network.NickServ.Password != "" && !strings.Contains(msg.String(), s.client.CurrentNick()) || !strings.Contains(msg.String(), "+r") {
log.Trace().Msgf("%v: MODE: Not correct permission yet: %v", s.network.Server, msg.String())
return nil
}
for _, ch := range s.network.Channels {
err := s.handleJoinChannel(ch.Name)
if err != nil {
log.Error().Err(err).Msgf("error joining channel: %v", ch.Name)
continue
}
time.Sleep(1 * time.Second)
}
return nil
}
func (s *Handler) handlePing(msg *irc.Message) error {
//log.Trace().Msgf("%v: %v", s.network.Server, msg)
pong := irc.Message{
Command: "PONG",
Params: msg.Params,
}
log.Trace().Msgf("%v: %v", s.network.Server, pong.String())
err := s.client.Write(pong.String())
if err != nil {
log.Error().Err(err).Msgf("error PING PONG response: %v", pong.String())
return err
}
s.lastPing = time.Now()
return nil
}
// irc line can contain lots of extra stuff like color so lets clean that // irc line can contain lots of extra stuff like color so lets clean that
func cleanMessage(message string) string { func cleanMessage(message string) string {
var regexMessageClean = `\x0f|\x1f|\x02|\x03(?:[\d]{1,2}(?:,[\d]{1,2})?)?` var regexMessageClean = `\x0f|\x1f|\x02|\x03(?:[\d]{1,2}(?:,[\d]{1,2})?)?`

View file

@ -13,6 +13,7 @@ import (
type Service interface { type Service interface {
StartHandlers() StartHandlers()
StopHandlers()
StopNetwork(name string) error StopNetwork(name string) error
ListNetworks(ctx context.Context) ([]domain.IrcNetwork, error) ListNetworks(ctx context.Context) ([]domain.IrcNetwork, error)
GetNetworkByID(id int64) (*domain.IrcNetwork, error) GetNetworkByID(id int64) (*domain.IrcNetwork, error)
@ -56,7 +57,7 @@ func (s *service) StartHandlers() {
s.lock.Lock() s.lock.Lock()
channels, err := s.repo.ListChannels(network.ID) channels, err := s.repo.ListChannels(network.ID)
if err != nil { if err != nil {
log.Error().Err(err).Msgf("failed to list channels for network %q", network.Addr) log.Error().Err(err).Msgf("failed to list channels for network %q", network.Server)
} }
network.Channels = channels network.Channels = channels
@ -79,6 +80,15 @@ func (s *service) StartHandlers() {
} }
} }
func (s *service) StopHandlers() {
for _, handler := range s.handlers {
log.Info().Msgf("stopping network: %+v", handler.network.Name)
handler.Stop()
}
log.Info().Msg("stopped all irc handlers")
}
func (s *service) startNetwork(network domain.IrcNetwork) error { func (s *service) startNetwork(network domain.IrcNetwork) error {
// look if we have the network in handlers already, if so start it // look if we have the network in handlers already, if so start it
if handler, found := s.handlers[network.Name]; found { if handler, found := s.handlers[network.Name]; found {
@ -134,7 +144,7 @@ func (s *service) GetNetworkByID(id int64) (*domain.IrcNetwork, error) {
channels, err := s.repo.ListChannels(network.ID) channels, err := s.repo.ListChannels(network.ID)
if err != nil { if err != nil {
log.Error().Err(err).Msgf("failed to list channels for network %q", network.Addr) log.Error().Err(err).Msgf("failed to list channels for network %q", network.Server)
return nil, err return nil, err
} }
network.Channels = append(network.Channels, channels...) network.Channels = append(network.Channels, channels...)
@ -154,7 +164,7 @@ func (s *service) ListNetworks(ctx context.Context) ([]domain.IrcNetwork, error)
for _, n := range networks { for _, n := range networks {
channels, err := s.repo.ListChannels(n.ID) channels, err := s.repo.ListChannels(n.ID)
if err != nil { if err != nil {
log.Error().Msgf("failed to list channels for network %q: %v", n.Addr, err) log.Error().Msgf("failed to list channels for network %q: %v", n.Server, err)
return nil, err return nil, err
} }
n.Channels = append(n.Channels, channels...) n.Channels = append(n.Channels, channels...)
@ -166,11 +176,21 @@ func (s *service) ListNetworks(ctx context.Context) ([]domain.IrcNetwork, error)
} }
func (s *service) DeleteNetwork(ctx context.Context, id int64) error { func (s *service) DeleteNetwork(ctx context.Context, id int64) error {
if err := s.repo.DeleteNetwork(ctx, id); err != nil { network, err := s.GetNetworkByID(id)
if err != nil {
return err return err
} }
log.Debug().Msgf("delete network: %+v", id) log.Debug().Msgf("delete network: %v", id)
// Remove network and handler
if err = s.StopNetwork(network.Name); err != nil {
return err
}
if err = s.repo.DeleteNetwork(ctx, id); err != nil {
return err
}
return nil return nil
} }
@ -191,24 +211,40 @@ func (s *service) StoreNetwork(network *domain.IrcNetwork) error {
} }
// stop or start network // stop or start network
if !network.Enabled { if network.Enabled {
log.Debug().Msgf("stopping network: %+v", network.Name)
err := s.StopNetwork(network.Name)
if err != nil {
log.Error().Err(err).Msgf("could not stop network: %+v", network.Name)
return fmt.Errorf("could not stop network: %v", network.Name)
}
} else {
log.Debug().Msgf("starting network: %+v", network.Name)
err := s.startNetwork(*network) err := s.startNetwork(*network)
if err != nil { if err != nil {
log.Error().Err(err).Msgf("could not start network: %+v", network.Name) log.Error().Err(err).Msgf("could not start network: %+v", network.Name)
return fmt.Errorf("could not start network: %v", network.Name) return fmt.Errorf("could not start network: %v", network.Name)
} }
} else {
err := s.StopNetwork(network.Name)
if err != nil {
log.Error().Err(err).Msgf("could not stop network: %+v", network.Name)
return fmt.Errorf("could not stop network: %v", network.Name)
}
} }
// stop or start network
//if !network.Enabled {
// log.Debug().Msgf("stopping network: %+v", network.Name)
//
// err := s.StopNetwork(network.Name)
// if err != nil {
// log.Error().Err(err).Msgf("could not stop network: %+v", network.Name)
// return fmt.Errorf("could not stop network: %v", network.Name)
// }
//} else {
// log.Debug().Msgf("starting network: %+v", network.Name)
//
// err := s.startNetwork(*network)
// if err != nil {
// log.Error().Err(err).Msgf("could not start network: %+v", network.Name)
// return fmt.Errorf("could not start network: %v", network.Name)
// }
//}
return nil return nil
} }

View file

@ -24,6 +24,8 @@ func Setup(cfg domain.Config) {
zerolog.SetGlobalLevel(zerolog.ErrorLevel) zerolog.SetGlobalLevel(zerolog.ErrorLevel)
case "WARN": case "WARN":
zerolog.SetGlobalLevel(zerolog.WarnLevel) zerolog.SetGlobalLevel(zerolog.WarnLevel)
case "TRACE":
zerolog.SetGlobalLevel(zerolog.TraceLevel)
default: default:
zerolog.SetGlobalLevel(zerolog.ErrorLevel) zerolog.SetGlobalLevel(zerolog.ErrorLevel)
} }

View file

@ -24,7 +24,7 @@ func NewService(actionService action.Service) Service {
} }
func (s *service) Process(announce domain.Announce) error { func (s *service) Process(announce domain.Announce) error {
log.Debug().Msgf("start to process release: %+v", announce) log.Trace().Msgf("start to process release: %+v", announce)
if announce.Filter.Actions == nil { if announce.Filter.Actions == nil {
return fmt.Errorf("no actions for filter: %v", announce.Filter.Name) return fmt.Errorf("no actions for filter: %v", announce.Filter.Name)

View file

@ -41,3 +41,10 @@ func (s *Server) Start() error {
return nil return nil
} }
func (s *Server) Shutdown() {
log.Info().Msg("Shutting down server")
// stop all irc handlers
s.ircService.StopHandlers()
}

View file

@ -7,10 +7,11 @@ interface Props {
name: string; name: string;
label: string; label: string;
description?: string; description?: string;
defaultValue?: boolean;
className?: string; className?: string;
} }
const SwitchGroup: React.FC<Props> = ({name, label, description}) => ( const SwitchGroup: React.FC<Props> = ({name, label, description, defaultValue}) => (
<ul className="mt-2 divide-y divide-gray-200"> <ul className="mt-2 divide-y divide-gray-200">
<Switch.Group as="li" className="py-4 flex items-center justify-between"> <Switch.Group as="li" className="py-4 flex items-center justify-between">
<div className="flex flex-col"> <div className="flex flex-col">
@ -27,6 +28,7 @@ const SwitchGroup: React.FC<Props> = ({name, label, description}) => (
<Field <Field
name={name} name={name}
defaultValue={defaultValue as any}
render={({input: {onChange, checked, value}}) => ( render={({input: {onChange, checked, value}}) => (
<Switch <Switch
value={value} value={value}

View file

@ -1,18 +1,21 @@
import {Field} from "react-final-form"; import { Field } from "react-final-form";
import React from "react"; import React from "react";
import Error from "./Error"; import Error from "./Error";
import {classNames} from "../../styles/utils"; import { classNames } from "../../styles/utils";
interface Props { interface Props {
name: string; name: string;
label?: string; label?: string;
help?: string;
placeholder?: string; placeholder?: string;
defaultValue?: string;
className?: string; className?: string;
required?: boolean; required?: boolean;
hidden?: boolean;
} }
const TextFieldWide: React.FC<Props> = ({name, label, placeholder, required, className}) => ( const TextFieldWide: React.FC<Props> = ({ name, label, help, placeholder, defaultValue, required, hidden, className}) => (
<div <div hidden={hidden}
className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5"> className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
<div> <div>
@ -23,17 +26,22 @@ const TextFieldWide: React.FC<Props> = ({name, label, placeholder, required, cla
<div className="sm:col-span-2"> <div className="sm:col-span-2">
<Field <Field
name={name} name={name}
render={({input, meta}) => ( defaultValue={defaultValue}
render={({ input, meta }) => (
<input <input
{...input} {...input}
id={name} id={name}
type="text" type="text"
className={classNames(meta.touched && meta.error ? "focus:ring-red-500 focus:border-red-500 border-red-500" : "focus:ring-indigo-500 focus:border-indigo-500 border-gray-300", "block w-full shadow-sm sm:text-sm rounded-md")} className={classNames(meta.touched && meta.error ? "focus:ring-red-500 focus:border-red-500 border-red-500" : "focus:ring-indigo-500 focus:border-indigo-500 border-gray-300", "block w-full shadow-sm sm:text-sm rounded-md")}
placeholder={placeholder} placeholder={placeholder}
hidden={hidden}
/> />
)} )}
/> />
<Error name={name} classNames="block text-red-500 mt-2"/> {help && (
<p className="mt-2 text-sm text-gray-500" id="email-description">{help}</p>
)}
<Error name={name} classNames="block text-red-500 mt-2" />
</div> </div>
</div> </div>
) )

View file

@ -7,15 +7,19 @@ interface Props {
name: string; name: string;
label?: string; label?: string;
placeholder?: string; placeholder?: string;
defaultValue?: number;
className?: string; className?: string;
required?: boolean; required?: boolean;
hidden?: boolean;
} }
const NumberFieldWide: React.FC<Props> = ({ const NumberFieldWide: React.FC<Props> = ({
name, name,
label, label,
placeholder, placeholder,
defaultValue,
required, required,
hidden,
className, className,
}) => ( }) => (
<div className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5"> <div className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
@ -30,6 +34,7 @@ const NumberFieldWide: React.FC<Props> = ({
<div className="sm:col-span-2"> <div className="sm:col-span-2">
<Field <Field
name={name} name={name}
defaultValue={defaultValue}
parse={(v) => v & parseInt(v, 10)} parse={(v) => v & parseInt(v, 10)}
render={({ input, meta }) => ( render={({ input, meta }) => (
<input <input

View file

@ -0,0 +1,56 @@
import { Field } from "react-final-form";
import Error from "../Error";
import { classNames } from "../../../styles/utils";
import { useToggle } from "../../../hooks/hooks";
import { EyeIcon, EyeOffIcon } from "@heroicons/react/solid";
interface Props {
name: string;
label?: string;
placeholder?: string;
defaultValue?: string;
help?: string;
required?: boolean;
}
function PasswordField({ name, label, placeholder, defaultValue, help, required }: Props) {
const [isVisible, toggleVisibility] = useToggle(false)
return (
<div
className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
<div>
<label htmlFor={name} className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2">
{label} {required && <span className="text-gray-500">*</span>}
</label>
</div>
<div className="sm:col-span-2">
<Field
name={name}
defaultValue={defaultValue}
render={({ input, meta }) => (
<div className="relative">
<input
{...input}
id={name}
type={isVisible ? "text" : "password"}
className={classNames(meta.touched && meta.error ? "focus:ring-red-500 focus:border-red-500 border-red-500" : "focus:ring-indigo-500 focus:border-indigo-500 border-gray-300", "block w-full shadow-sm sm:text-sm rounded-md")}
placeholder={placeholder}
/>
<div className="absolute inset-y-0 right-0 px-3 flex items-center" onClick={toggleVisibility}>
{!isVisible ? <EyeIcon className="h-5 w-5 text-gray-400 hover:text-gray-500" aria-hidden="true" /> : <EyeOffIcon className="h-5 w-5 text-gray-400 hover:text-gray-500" aria-hidden="true" />}
</div>
</div>
)}
/>
{help && (
<p className="mt-2 text-sm text-gray-500" id="email-description">{help}</p>
)}
<Error name={name} classNames="block text-red-500 mt-2" />
</div>
</div>
)
}
export default PasswordField;

View file

@ -1,3 +1,4 @@
export { default as NumberFieldWide } from "./NumberField"; export { default as NumberFieldWide } from "./NumberField";
export { default as PasswordFieldWide } from "./PasswordField";
export { default as RadioFieldsetWide } from "./RadioFieldsetWide"; export { default as RadioFieldsetWide } from "./RadioFieldsetWide";
export { default as SelectFieldWide } from "./SelectField"; export { default as SelectFieldWide } from "./SelectField";

View file

@ -31,6 +31,43 @@ export interface Indexer {
settings: object | any; settings: object | any;
} }
export interface IndexerSchema {
// id: number;
name: string;
identifier: string;
description: string;
language: string;
privacy: string;
protocol: string;
urls: string[];
settings: IndexerSchemaSettings[];
irc: IndexerSchemaIRC;
}
export interface IndexerSchemaSettings {
name: string;
type: string;
required: boolean;
label: string;
help: string;
description: string;
default: string;
}
export interface IndexerSchemaIRC {
network: string;
server: string;
port: number;
tls: boolean;
nickserv: boolean;
announcers: string[];
channels: string[];
invite: string[];
invite_command: string;
settings: IndexerSchemaSettings[];
}
export interface Filter { export interface Filter {
id: number; id: number;
name: string; name: string;
@ -87,16 +124,29 @@ export interface DownloadClient {
settings: object; settings: object;
} }
export interface NickServ {
account: string;
password: string;
}
export interface Network { export interface Network {
id: number; id?: number;
name: string; name: string;
enabled: boolean; enabled: boolean;
addr: string; server: string;
nick: string; port: number;
username: string; tls: boolean;
realname: string; invite_command: string;
pass: string; nickserv: {
sasl: SASL; account: string;
password: string;
}
channels: Channel[];
settings: object;
}
export interface Channel {
name: string;
} }
export interface SASL { export interface SASL {

View file

@ -1,23 +1,24 @@
import React, {Fragment} from "react"; import React, { Fragment } from "react";
import {useMutation, useQuery} from "react-query"; import { useMutation, useQuery } from "react-query";
import {Indexer} from "../../domain/interfaces"; import { Channel, Indexer, IndexerSchema, IndexerSchemaSettings, Network } from "../../domain/interfaces";
import {sleep} from "../../utils/utils"; import { sleep } from "../../utils/utils";
import {XIcon} from "@heroicons/react/solid"; import { XIcon } from "@heroicons/react/solid";
import {Dialog, Transition} from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import {Field, Form} from "react-final-form"; import { Field, Form } from "react-final-form";
import DEBUG from "../../components/debug"; import DEBUG from "../../components/debug";
import Select from "react-select"; import Select from "react-select";
import {queryClient} from "../../App"; import { queryClient } from "../../App";
import { SwitchGroup } from "../../components/inputs"; import { SwitchGroup, TextFieldWide } from "../../components/inputs";
import APIClient from "../../api/APIClient"; import APIClient from "../../api/APIClient";
import { NumberFieldWide, PasswordFieldWide } from "../../components/inputs/wide";
interface props { interface props {
isOpen: boolean; isOpen: boolean;
toggle: any; toggle: any;
} }
function IndexerAddForm({isOpen, toggle}: props) { function IndexerAddForm({ isOpen, toggle }: props) {
const {data} = useQuery<any[], Error>('indexerSchema', APIClient.indexers.getSchema, const { data } = useQuery<IndexerSchema[], Error>('indexerSchema', APIClient.indexers.getSchema,
{ {
enabled: isOpen, enabled: isOpen,
refetchOnWindowFocus: false refetchOnWindowFocus: false
@ -33,59 +34,122 @@ function IndexerAddForm({isOpen, toggle}: props) {
} }
}) })
const onSubmit = (data: any) => { const ircMutation = useMutation((network: Network) => APIClient.irc.createNetwork(network), {
mutation.mutate(data) onSuccess: (data) => {
console.log("irc mutation: ", data);
// queryClient.invalidateQueries(['indexer']);
// sleep(1500)
// toggle()
}
})
const onSubmit = (formData: any) => {
let ind = data && data.find(i => i.identifier === formData.identifier)
if (!ind) {
return
}
let channels: Channel[] = []
if (ind.irc.channels.length) {
ind.irc.channels.forEach(element => {
channels.push({ name: element })
});
}
const network: Network = {
name: ind.name,
enabled: false,
server: formData.irc.server,
port: formData.irc.port,
tls: formData.irc.tls,
nickserv: formData.irc.nickserv,
invite_command: formData.irc.invite_command,
settings: formData.irc.settings,
channels: channels,
}
console.log("network: ", network);
mutation.mutate(formData, {
onSuccess: (data) => {
// create irc
ircMutation.mutate(network)
}
})
}; };
const renderSettingFields = (indexer: string) => { const renderSettingFields = (indexer: string) => {
if (indexer !== "") { if (indexer !== "") {
// let ind = data.find(i => i.implementation_name === indexer)
let ind = data && data.find(i => i.identifier === indexer) let ind = data && data.find(i => i.identifier === indexer)
return ( return (
<div key="opt"> <div key="opt">
{ind && ind.settings && ind.settings.map((f: any, idx: number) => { {ind && ind.settings && ind.settings.map((f: any, idx: number) => {
switch (f.type) { switch (f.type) {
case "text": case "text":
return ( return (
<div className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5" key={idx}> <TextFieldWide name={`settings.${f.name}`} label={f.label} key={idx} help={f.help} defaultValue=""/>
<div> )
<label case "secret":
htmlFor={f.name} return (
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2" <PasswordFieldWide name={`settings.${f.name}`} label={f.label} key={idx} help={f.help} defaultValue="" />
> )
{f.label} }
</label> })}
</div> <div hidden={true}>
<div className="sm:col-span-2"> <TextFieldWide name={`name`} label="Name" defaultValue={ind?.name} />
<Field name={"settings."+f.name}> </div>
{({input, meta}) => (
<div className="sm:col-span-2">
<input
type="text"
{...input}
className="block w-full shadow-sm sm:text-sm focus:ring-indigo-500 focus:border-indigo-500 border-gray-300 rounded-md"
/>
{meta.touched && meta.error &&
<span>{meta.error}</span>}
</div>
)}
</Field>
</div>
</div>
)
}
})}
</div> </div>
) )
} }
} }
const renderIrcSettingFields = (indexer: string) => {
if (indexer !== "") {
let ind = data && data.find(i => i.identifier === indexer)
return (
<Fragment>
{ind && ind.irc && ind.irc.settings && (
<div className="border-t border-gray-200 py-5">
<div className="px-6 space-y-1">
<Dialog.Title className="text-lg font-medium text-gray-900">IRC</Dialog.Title>
<p className="text-sm text-gray-500">
Networks, channels and invite commands are configured automatically.
</p>
</div>
{ind.irc.settings.map((f: IndexerSchemaSettings, idx: number) => {
switch (f.type) {
case "text":
return <TextFieldWide name={`irc.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} />
case "secret":
return <PasswordFieldWide name={`irc.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} defaultValue={f.default} />
}
})}
<div hidden={true}>
<TextFieldWide name={`irc.server`} label="Server" defaultValue={ind.irc.server} />
<NumberFieldWide name={`irc.port`} label="Port" defaultValue={ind.irc.port} />
<SwitchGroup name="irc.tls" label="TLS" defaultValue={ind.irc.tls} />
</div>
</div>
)}
</Fragment>
)
}
}
return ( return (
<Transition.Root show={isOpen} as={Fragment}> <Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" static className="fixed inset-0 overflow-hidden" open={isOpen} onClose={toggle}> <Dialog as="div" static className="fixed inset-0 overflow-hidden" open={isOpen} onClose={toggle}>
<div className="absolute inset-0 overflow-hidden"> <div className="absolute inset-0 overflow-hidden">
<Dialog.Overlay className="absolute inset-0"/> <Dialog.Overlay className="absolute inset-0" />
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16"> <div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
<Transition.Child <Transition.Child
@ -100,18 +164,17 @@ function IndexerAddForm({isOpen, toggle}: props) {
<div className="w-screen max-w-2xl"> <div className="w-screen max-w-2xl">
<Form <Form
initialValues={{ initialValues={{
name: "",
enabled: true, enabled: true,
identifier: "", identifier: "",
irc: {}
}} }}
onSubmit={onSubmit} onSubmit={onSubmit}
> >
{({handleSubmit, values}) => { {({ handleSubmit, values }) => {
return ( return (
<form className="h-full flex flex-col bg-white shadow-xl overflow-y-scroll" <form className="h-full flex flex-col bg-white shadow-xl overflow-y-scroll"
onSubmit={handleSubmit}> onSubmit={handleSubmit}>
<div className="flex-1"> <div className="flex-1">
{/* Header */}
<div className="px-4 py-6 bg-gray-50 sm:px-6"> <div className="px-4 py-6 bg-gray-50 sm:px-6">
<div className="flex items-start justify-between space-x-3"> <div className="flex items-start justify-between space-x-3">
<div className="space-y-1"> <div className="space-y-1">
@ -129,43 +192,14 @@ function IndexerAddForm({isOpen, toggle}: props) {
onClick={toggle} onClick={toggle}
> >
<span className="sr-only">Close panel</span> <span className="sr-only">Close panel</span>
<XIcon className="h-6 w-6" aria-hidden="true"/> <XIcon className="h-6 w-6" aria-hidden="true" />
</button> </button>
</div> </div>
</div> </div>
</div> </div>
{/* Divider container */}
<div <div
className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200"> className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<div
className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
<div>
<label
htmlFor="name"
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
>
Name
</label>
</div>
<Field name="name">
{({input, meta}) => (
<div className="sm:col-span-2">
<input
type="text"
{...input}
className="block w-full shadow-sm sm:text-sm focus:ring-indigo-500 focus:border-indigo-500 border-gray-300 rounded-md"
/>
{meta.touched && meta.error &&
<span>{meta.error}</span>}
</div>
)}
</Field>
</div>
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="enabled" label="Enabled" />
</div>
<div <div
className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5"> className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
@ -182,26 +216,32 @@ function IndexerAddForm({isOpen, toggle}: props) {
name="identifier" name="identifier"
parse={val => val && val.value} parse={val => val && val.value}
format={val => data && data.find((o: any) => o.value === val)} format={val => data && data.find((o: any) => o.value === val)}
render={({input, meta}) => ( render={({ input, meta }) => (
<React.Fragment> <React.Fragment>
<Select {...input} <Select {...input}
isClearable={true} isClearable={true}
placeholder="Choose an indexer" placeholder="Choose an indexer"
options={data && data.sort((a,b): any => a.name.localeCompare(b.name)).map(v => ({
label: v.name, options={data && data.sort((a, b): any => a.name.localeCompare(b.name)).map(v => ({
value: v.identifier label: v.name,
// value: v.implementation_name value: v.identifier
}))}/> }))} />
{/*<Error name={input.name} classNames="text-red mt-2 block" />*/}
</React.Fragment> </React.Fragment>
)} )}
/> />
</div> </div>
</div> </div>
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="enabled" label="Enabled" />
</div>
{renderSettingFields(values.identifier)} {renderSettingFields(values.identifier)}
</div> </div>
{renderIrcSettingFields(values.identifier)}
</div> </div>
<div <div
@ -223,7 +263,7 @@ function IndexerAddForm({isOpen, toggle}: props) {
</div> </div>
</div> </div>
<DEBUG values={values}/> <DEBUG values={values} />
</form> </form>
) )
}} }}

View file

@ -1,15 +1,16 @@
import {Fragment, useRef} from "react"; import { Fragment, useRef } from "react";
import {useMutation } from "react-query"; import { useMutation } from "react-query";
import {Indexer} from "../../domain/interfaces"; import { Indexer } from "../../domain/interfaces";
import {sleep} from "../../utils/utils"; import { sleep } from "../../utils/utils";
import {ExclamationIcon, XIcon} from "@heroicons/react/solid"; import { ExclamationIcon, XIcon } from "@heroicons/react/solid";
import {Dialog, Transition} from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import {Field, Form} from "react-final-form"; import { Field, Form } from "react-final-form";
import DEBUG from "../../components/debug"; import DEBUG from "../../components/debug";
import { SwitchGroup } from "../../components/inputs"; import { SwitchGroup, TextFieldWide } from "../../components/inputs";
import {useToggle} from "../../hooks/hooks"; import { useToggle } from "../../hooks/hooks";
import APIClient from "../../api/APIClient"; import APIClient from "../../api/APIClient";
import {queryClient} from "../../App"; import { queryClient } from "../../App";
import { PasswordFieldWide } from "../../components/inputs/wide";
interface props { interface props {
isOpen: boolean; isOpen: boolean;
@ -17,7 +18,7 @@ interface props {
indexer: Indexer; indexer: Indexer;
} }
function IndexerUpdateForm({isOpen, toggle, indexer}: props) { function IndexerUpdateForm({ isOpen, toggle, indexer }: props) {
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false) const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false)
const mutation = useMutation((indexer: Indexer) => APIClient.indexers.update(indexer), { const mutation = useMutation((indexer: Indexer) => APIClient.indexers.update(indexer), {
@ -55,31 +56,11 @@ function IndexerUpdateForm({isOpen, toggle, indexer}: props) {
switch (f.type) { switch (f.type) {
case "text": case "text":
return ( return (
<div className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5" key={idx}> <TextFieldWide name={`settings.${f.name}`} label={f.label} key={idx} help={f.help} />
<div> )
<label case "secret":
htmlFor={f.name} return (
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2" <PasswordFieldWide name={`settings.${f.name}`} label={f.label} key={idx} help={f.help} />
>
{f.label}
</label>
</div>
<div className="sm:col-span-2">
<Field name={"settings."+f.name}>
{({input, meta}) => (
<div className="sm:col-span-2">
<input
type="text"
{...input}
className="block w-full shadow-sm sm:text-sm focus:ring-indigo-500 focus:border-indigo-500 border-gray-300 rounded-md"
/>
{meta.touched && meta.error &&
<span>{meta.error}</span>}
</div>
)}
</Field>
</div>
</div>
) )
} }
})} })}
@ -88,9 +69,6 @@ function IndexerUpdateForm({isOpen, toggle, indexer}: props) {
} }
} }
// const setss = indexer.settings.reduce((o: any, obj: any) => ({ ...o, [obj.name]: obj.value }), {})
// console.log("setts", setss)
return ( return (
<Transition.Root show={isOpen} as={Fragment}> <Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" static className="fixed inset-0 overflow-hidden" open={isOpen} onClose={toggle}> <Dialog as="div" static className="fixed inset-0 overflow-hidden" open={isOpen} onClose={toggle}>
@ -119,8 +97,8 @@ function IndexerUpdateForm({isOpen, toggle, indexer}: props) {
{/* This element is to trick the browser into centering the modal contents. */} {/* This element is to trick the browser into centering the modal contents. */}
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true"> <span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
&#8203; &#8203;
</span> </span>
<Transition.Child <Transition.Child
as={Fragment} as={Fragment}
enter="ease-out duration-300" enter="ease-out duration-300"
@ -172,7 +150,7 @@ function IndexerUpdateForm({isOpen, toggle, indexer}: props) {
</Dialog> </Dialog>
</Transition.Root> </Transition.Root>
<div className="absolute inset-0 overflow-hidden"> <div className="absolute inset-0 overflow-hidden">
<Dialog.Overlay className="absolute inset-0"/> <Dialog.Overlay className="absolute inset-0" />
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16"> <div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
<Transition.Child <Transition.Child
@ -195,12 +173,11 @@ function IndexerUpdateForm({isOpen, toggle, indexer}: props) {
}} }}
onSubmit={onSubmit} onSubmit={onSubmit}
> >
{({handleSubmit, values}) => { {({ handleSubmit, values }) => {
return ( return (
<form className="h-full flex flex-col bg-white shadow-xl overflow-y-scroll" <form className="h-full flex flex-col bg-white shadow-xl overflow-y-scroll"
onSubmit={handleSubmit}> onSubmit={handleSubmit}>
<div className="flex-1"> <div className="flex-1">
{/* Header */}
<div className="px-4 py-6 bg-gray-50 sm:px-6"> <div className="px-4 py-6 bg-gray-50 sm:px-6">
<div className="flex items-start justify-between space-x-3"> <div className="flex items-start justify-between space-x-3">
<div className="space-y-1"> <div className="space-y-1">
@ -218,13 +195,12 @@ function IndexerUpdateForm({isOpen, toggle, indexer}: props) {
onClick={toggle} onClick={toggle}
> >
<span className="sr-only">Close panel</span> <span className="sr-only">Close panel</span>
<XIcon className="h-6 w-6" aria-hidden="true"/> <XIcon className="h-6 w-6" aria-hidden="true" />
</button> </button>
</div> </div>
</div> </div>
</div> </div>
{/* Divider container */}
<div <div
className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200"> className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<div <div
@ -238,7 +214,7 @@ function IndexerUpdateForm({isOpen, toggle, indexer}: props) {
</label> </label>
</div> </div>
<Field name="name"> <Field name="name">
{({input, meta}) => ( {({ input, meta }) => (
<div className="sm:col-span-2"> <div className="sm:col-span-2">
<input <input
type="text" type="text"
@ -246,7 +222,7 @@ function IndexerUpdateForm({isOpen, toggle, indexer}: props) {
className="block w-full shadow-sm sm:text-sm focus:ring-indigo-500 focus:border-indigo-500 border-gray-300 rounded-md" className="block w-full shadow-sm sm:text-sm focus:ring-indigo-500 focus:border-indigo-500 border-gray-300 rounded-md"
/> />
{meta.touched && meta.error && {meta.touched && meta.error &&
<span>{meta.error}</span>} <span>{meta.error}</span>}
</div> </div>
)} )}
</Field> </Field>
@ -289,7 +265,7 @@ function IndexerUpdateForm({isOpen, toggle, indexer}: props) {
</div> </div>
</div> </div>
<DEBUG values={values}/> <DEBUG values={values} />
</form> </form>
) )
}} }}

View file

@ -5,26 +5,14 @@ import {Dialog, Transition} from "@headlessui/react";
import {XIcon} from "@heroicons/react/solid"; import {XIcon} from "@heroicons/react/solid";
import {Field, Form} from "react-final-form"; import {Field, Form} from "react-final-form";
import DEBUG from "../../components/debug"; import DEBUG from "../../components/debug";
import {SwitchGroup, TextAreaWide, TextFieldWide} from "../../components/inputs"; import {SwitchGroup, TextFieldWide} from "../../components/inputs";
import {queryClient} from "../../App"; import {queryClient} from "../../App";
import arrayMutators from "final-form-arrays"; import arrayMutators from "final-form-arrays";
import { FieldArray } from "react-final-form-arrays"; import { FieldArray } from "react-final-form-arrays";
import {classNames} from "../../styles/utils"; import {classNames} from "../../styles/utils";
import APIClient from "../../api/APIClient"; import APIClient from "../../api/APIClient";
import { NumberFieldWide, PasswordFieldWide } from "../../components/inputs/wide";
// interface radioFieldsetOption {
// label: string;
// description: string;
// value: string;
// }
// const saslTypeOptions: radioFieldsetOption[] = [
// {label: "None", description: "None", value: ""},
// {label: "Plain", description: "SASL plain", value: "PLAIN"},
// {label: "NickServ", description: "/NS identify", value: "NICKSERV"},
// ];
function IrcNetworkAddForm({isOpen, toggle}: any) { function IrcNetworkAddForm({isOpen, toggle}: any) {
const mutation = useMutation((network: Network) => APIClient.irc.createNetwork(network), { const mutation = useMutation((network: Network) => APIClient.irc.createNetwork(network), {
@ -53,12 +41,8 @@ function IrcNetworkAddForm({isOpen, toggle}: any) {
errors.name = "Required"; errors.name = "Required";
} }
if (!values.addr) { if (!values.server) {
errors.addr = "Required"; errors.server = "Required";
}
if (!values.nick) {
errors.nick = "Required";
} }
return errors; return errors;
@ -86,18 +70,9 @@ function IrcNetworkAddForm({isOpen, toggle}: any) {
initialValues={{ initialValues={{
name: "", name: "",
enabled: true, enabled: true,
addr: "", server: "",
tls: false, tls: false,
nick: "",
pass: "", pass: "",
// connect_commands: "",
// sasl: {
// mechanism: "",
// plain: {
// username: "",
// password: "",
// }
// },
}} }}
mutators={{ mutators={{
...arrayMutators ...arrayMutators
@ -110,7 +85,6 @@ function IrcNetworkAddForm({isOpen, toggle}: any) {
<form className="h-full flex flex-col bg-white shadow-xl overflow-y-scroll" <form className="h-full flex flex-col bg-white shadow-xl overflow-y-scroll"
onSubmit={handleSubmit}> onSubmit={handleSubmit}>
<div className="flex-1"> <div className="flex-1">
{/* Header */}
<div className="px-4 py-6 bg-gray-50 sm:px-6"> <div className="px-4 py-6 bg-gray-50 sm:px-6">
<div className="flex items-start justify-between space-x-3"> <div className="flex items-start justify-between space-x-3">
<div className="space-y-1"> <div className="space-y-1">
@ -144,88 +118,19 @@ function IrcNetworkAddForm({isOpen, toggle}: any) {
</div> </div>
<div> <div>
<TextFieldWide name="server" label="Server" placeholder="Address: Eg irc.server.net" required={true} />
<TextFieldWide name="addr" label="Address" placeholder="Address:port eg irc.server.net:6697" required={true} /> <NumberFieldWide name="port" label="Port" required={true} />
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200"> <div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="tls" label="TLS"/> <SwitchGroup name="tls" label="TLS"/>
</div> </div>
<TextFieldWide name="nick" label="Nick" placeholder="Nick" required={true} /> <PasswordFieldWide name="pass" label="Password" help="Network password" />
<TextFieldWide name="password" label="Password" placeholder="Network password" /> <TextFieldWide name="nickserv.account" label="NickServ Account" required={true} />
<PasswordFieldWide name="nickserv.password" label="NickServ Password" />
<TextAreaWide name="connect_commands" label="Connect commands" placeholder="/msg test this" /> <PasswordFieldWide name="invite_command" label="Invite command" />
{/* <Field*/}
{/* name="sasl.mechanism"*/}
{/* type="select"*/}
{/* render={({input}) => (*/}
{/* <Listbox value={input.value} onChange={input.onChange}>*/}
{/* {({open}) => (*/}
{/* <div className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">*/}
{/* <div>*/}
{/* <Listbox.Label className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2">SASL / auth</Listbox.Label>*/}
{/* </div>*/}
{/* <div className="sm:col-span-2 relative">*/}
{/* <Listbox.Button*/}
{/* className="bg-white relative w-full border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">*/}
{/* <span className="block truncate">{input.value ? saslTypeOptions.find(c => c.value === input.value)!.label : "Choose auth method"}</span>*/}
{/* <span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">*/}
{/* <SelectorIcon className="h-5 w-5 text-gray-400" aria-hidden="true"/>*/}
{/*</span>*/}
{/* </Listbox.Button>*/}
{/* <Transition*/}
{/* show={open}*/}
{/* as={Fragment}*/}
{/* leave="transition ease-in duration-100"*/}
{/* leaveFrom="opacity-100"*/}
{/* leaveTo="opacity-0"*/}
{/* >*/}
{/* <Listbox.Options*/}
{/* static*/}
{/* className="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"*/}
{/* >*/}
{/* {saslTypeOptions.map((opt: any) => (*/}
{/* <Listbox.Option*/}
{/* key={opt.value}*/}
{/* className={({active}) =>*/}
{/* classNames(*/}
{/* active ? 'text-white bg-indigo-600' : 'text-gray-900',*/}
{/* 'cursor-default select-none relative py-2 pl-3 pr-9'*/}
{/* )*/}
{/* }*/}
{/* value={opt.value}*/}
{/* >*/}
{/* {({selected, active}) => (*/}
{/* <>*/}
{/* <span className={classNames(selected ? 'font-semibold' : 'font-normal', 'block truncate')}>*/}
{/* {opt.label}*/}
{/* </span>*/}
{/* {selected ? (*/}
{/* <span*/}
{/* className={classNames(*/}
{/* active ? 'text-white' : 'text-indigo-600',*/}
{/* 'absolute inset-y-0 right-0 flex items-center pr-4'*/}
{/* )}*/}
{/* >*/}
{/* <CheckIcon className="h-5 w-5" aria-hidden="true"/>*/}
{/* </span>*/}
{/* ) : null}*/}
{/* </>*/}
{/* )}*/}
{/* </Listbox.Option>*/}
{/* ))}*/}
{/* </Listbox.Options>*/}
{/* </Transition>*/}
{/* </div>*/}
{/* </div>*/}
{/* )}*/}
{/* </Listbox>*/}
{/* )} />*/}
</div> </div>
</div> </div>
@ -295,7 +200,6 @@ function IrcNetworkAddForm({isOpen, toggle}: any) {
<button <button
type="submit" type="submit"
disabled={pristine || invalid} disabled={pristine || invalid}
// className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
className={classNames(pristine || invalid ? "bg-indigo-300" : "bg-indigo-600 hover:bg-indigo-700","inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500")} className={classNames(pristine || invalid ? "bg-indigo-300" : "bg-indigo-600 hover:bg-indigo-700","inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500")}
> >
Create Create

View file

@ -1,34 +1,22 @@
import {Fragment, useEffect, useRef} from "react"; import { Fragment, useEffect, useRef } from "react";
import {useMutation} from "react-query"; import { useMutation } from "react-query";
import {Network} from "../../domain/interfaces"; import { Network } from "../../domain/interfaces";
import {Dialog, Transition} from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import {XIcon} from "@heroicons/react/solid"; import { XIcon } from "@heroicons/react/solid";
import {Field, Form} from "react-final-form"; import { Field, Form } from "react-final-form";
import DEBUG from "../../components/debug"; import DEBUG from "../../components/debug";
import {SwitchGroup, TextAreaWide, TextFieldWide} from "../../components/inputs"; import { SwitchGroup, TextFieldWide } from "../../components/inputs";
import {queryClient} from "../../App"; import { queryClient } from "../../App";
import arrayMutators from "final-form-arrays"; import arrayMutators from "final-form-arrays";
import { FieldArray } from "react-final-form-arrays"; import { FieldArray } from "react-final-form-arrays";
import {classNames} from "../../styles/utils"; import { classNames } from "../../styles/utils";
import {useToggle} from "../../hooks/hooks"; import { useToggle } from "../../hooks/hooks";
import {DeleteModal} from "../../components/modals"; import { DeleteModal } from "../../components/modals";
import APIClient from "../../api/APIClient"; import APIClient from "../../api/APIClient";
import { NumberFieldWide, PasswordFieldWide } from "../../components/inputs/wide";
function IrcNetworkUpdateForm({ isOpen, toggle, network }: any) {
// interface radioFieldsetOption {
// label: string;
// description: string;
// value: string;
// }
//
// const saslTypeOptions: radioFieldsetOption[] = [
// {label: "None", description: "None", value: ""},
// {label: "Plain", description: "SASL plain", value: "PLAIN"},
// {label: "NickServ", description: "/NS identify", value: "NICKSERV"},
// ];
function IrcNetworkUpdateForm({isOpen, toggle, network}: any) {
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false) const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false)
const mutation = useMutation((network: Network) => APIClient.irc.updateNetwork(network), { const mutation = useMutation((network: Network) => APIClient.irc.updateNetwork(network), {
@ -59,7 +47,7 @@ function IrcNetworkUpdateForm({isOpen, toggle, network}: any) {
// let cmds = data.connect_commands && data.connect_commands.length > 0 ? data.connect_commands.replace(/\r\n/g,"\n").split("\n") : []; // let cmds = data.connect_commands && data.connect_commands.length > 0 ? data.connect_commands.replace(/\r\n/g,"\n").split("\n") : [];
// data.connect_commands = cmds // data.connect_commands = cmds
// console.log("formatted", data) // console.log("formatted", data)
mutation.mutate(data) mutation.mutate(data)
}; };
@ -70,12 +58,16 @@ function IrcNetworkUpdateForm({isOpen, toggle, network}: any) {
errors.name = "Required"; errors.name = "Required";
} }
if (!values.addr) { if (!values.server) {
errors.addr = "Required"; errors.server = "Required";
} }
if (!values.nick) { if (!values.port) {
errors.nick = "Required"; errors.port = "Required";
}
if (!values.nickserv.account) {
errors.nickserv.account = "Required";
} }
return errors; return errors;
@ -98,7 +90,7 @@ function IrcNetworkUpdateForm({isOpen, toggle, network}: any) {
text="Are you sure you want to remove this network and channels? This action cannot be undone." text="Are you sure you want to remove this network and channels? This action cannot be undone."
/> />
<div className="absolute inset-0 overflow-hidden"> <div className="absolute inset-0 overflow-hidden">
<Dialog.Overlay className="absolute inset-0"/> <Dialog.Overlay className="absolute inset-0" />
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16"> <div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
<Transition.Child <Transition.Child
@ -117,19 +109,13 @@ function IrcNetworkUpdateForm({isOpen, toggle, network}: any) {
id: network.id, id: network.id,
name: network.name, name: network.name,
enabled: network.enabled, enabled: network.enabled,
addr: network.addr, server: network.server,
port: network.port,
tls: network.tls, tls: network.tls,
nick: network.nick, nickserv: network.nickserv,
pass: network.pass, pass: network.pass,
invite_command: network.invite_command,
connect_commands: network.connect_commands, connect_commands: network.connect_commands,
sasl: network.sasl,
// sasl: {
// mechanism: network.sasl.mechanism,
// plain: {
// username: network.sasl.plain.username,
// password: network.sasl.plain.password,
// }
// },
channels: network.channels channels: network.channels
}} }}
mutators={{ mutators={{
@ -138,10 +124,10 @@ function IrcNetworkUpdateForm({isOpen, toggle, network}: any) {
validate={validate} validate={validate}
onSubmit={onSubmit} onSubmit={onSubmit}
> >
{({handleSubmit, values, pristine, invalid}) => { {({ handleSubmit, values, pristine, invalid }) => {
return ( return (
<form className="h-full flex flex-col bg-white shadow-xl overflow-y-scroll" <form className="h-full flex flex-col bg-white shadow-xl overflow-y-scroll"
onSubmit={handleSubmit}> onSubmit={handleSubmit}>
<div className="flex-1"> <div className="flex-1">
{/* Header */} {/* Header */}
<div className="px-4 py-6 bg-gray-50 sm:px-6"> <div className="px-4 py-6 bg-gray-50 sm:px-6">
@ -160,106 +146,48 @@ function IrcNetworkUpdateForm({isOpen, toggle, network}: any) {
onClick={toggle} onClick={toggle}
> >
<span className="sr-only">Close panel</span> <span className="sr-only">Close panel</span>
<XIcon className="h-6 w-6" aria-hidden="true"/> <XIcon className="h-6 w-6" aria-hidden="true" />
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<TextFieldWide name="name" label="Name" placeholder="Name" required={true} /> <TextFieldWide name="name" label="Name" placeholder="Name" required={true} />
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200"> <div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<div <div
className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200"> className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="enabled" label="Enabled"/> <SwitchGroup name="enabled" label="Enabled" />
</div> </div>
<div> <div>
<div className="px-6 space-y-1 mt-6">
<TextFieldWide name="addr" label="Address" placeholder="Address:port eg irc.server.net:6697" required={true} /> <Dialog.Title className="text-lg font-medium text-gray-900">Connection</Dialog.Title>
{/* <p className="text-sm text-gray-500">
Networks, channels and invite commands are configured automatically.
</p> */}
</div>
<TextFieldWide name="server" label="Server" placeholder="Address: Eg irc.server.net" required={true} />
<NumberFieldWide name="port" label="Port" />
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200"> <div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="tls" label="TLS"/> <SwitchGroup name="tls" label="TLS" />
</div> </div>
<TextFieldWide name="nick" label="Nick" placeholder="Nick" required={true} /> <PasswordFieldWide name="pass" label="Password" help="Network password" />
<TextFieldWide name="password" label="Password" placeholder="Network password" /> <div className="px-6 space-y-1 border-t pt-6">
<Dialog.Title className="text-lg font-medium text-gray-900">Account</Dialog.Title>
{/* <p className="text-sm text-gray-500">
Networks, channels and invite commands are configured automatically.
</p> */}
</div>
<TextAreaWide name="connect_commands" label="Connect commands" placeholder="/msg test this" /> <TextFieldWide name="nickserv.account" label="NickServ Account" required={true} />
<PasswordFieldWide name="nickserv.password" label="NickServ Password" />
<PasswordFieldWide name="invite_command" label="Invite command" />
{/* <Field*/}
{/* name="sasl.mechanism"*/}
{/* type="select"*/}
{/* render={({input}) => (*/}
{/* <Listbox value={input.value} onChange={input.onChange}>*/}
{/* {({open}) => (*/}
{/* <div className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">*/}
{/* <div>*/}
{/* <Listbox.Label className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2">SASL / auth</Listbox.Label>*/}
{/* </div>*/}
{/* <div className="sm:col-span-2 relative">*/}
{/* <Listbox.Button*/}
{/* className="bg-white relative w-full border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">*/}
{/* <span className="block truncate">{input.value ? saslTypeOptions.find(c => c.value === input.value)!.label : "Choose a auth type"}</span>*/}
{/* /!*<span className="block truncate">Choose a auth type</span>*!/*/}
{/* <span*/}
{/* className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">*/}
{/* <SelectorIcon className="h-5 w-5 text-gray-400" aria-hidden="true"/>*/}
{/*</span>*/}
{/* </Listbox.Button>*/}
{/* <Transition*/}
{/* show={open}*/}
{/* as={Fragment}*/}
{/* leave="transition ease-in duration-100"*/}
{/* leaveFrom="opacity-100"*/}
{/* leaveTo="opacity-0"*/}
{/* >*/}
{/* <Listbox.Options*/}
{/* static*/}
{/* className="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"*/}
{/* >*/}
{/* {saslTypeOptions.map((opt: any) => (*/}
{/* <Listbox.Option*/}
{/* key={opt.value}*/}
{/* className={({active}) =>*/}
{/* classNames(*/}
{/* active ? 'text-white bg-indigo-600' : 'text-gray-900',*/}
{/* 'cursor-default select-none relative py-2 pl-3 pr-9'*/}
{/* )*/}
{/* }*/}
{/* value={opt.value}*/}
{/* >*/}
{/* {({selected, active}) => (*/}
{/* <>*/}
{/* <span className={classNames(selected ? 'font-semibold' : 'font-normal', 'block truncate')}>*/}
{/* {opt.label}*/}
{/* </span>*/}
{/* {selected ? (*/}
{/* <span*/}
{/* className={classNames(*/}
{/* active ? 'text-white' : 'text-indigo-600',*/}
{/* 'absolute inset-y-0 right-0 flex items-center pr-4'*/}
{/* )}*/}
{/* >*/}
{/* <CheckIcon className="h-5 w-5" aria-hidden="true"/>*/}
{/* </span>*/}
{/* ) : null}*/}
{/* </>*/}
{/* )}*/}
{/* </Listbox.Option>*/}
{/* ))}*/}
{/* </Listbox.Options>*/}
{/* </Transition>*/}
{/* </div>*/}
{/* </div>*/}
{/* )}*/}
{/* </Listbox>*/}
{/* )} />*/}
</div> </div>
</div> </div>
@ -294,7 +222,7 @@ function IrcNetworkUpdateForm({isOpen, toggle, network}: any) {
onClick={() => fields.remove(index)} onClick={() => fields.remove(index)}
> >
<span className="sr-only">Remove</span> <span className="sr-only">Remove</span>
<XIcon className="h-6 w-6" aria-hidden="true"/> <XIcon className="h-6 w-6" aria-hidden="true" />
</button> </button>
</div> </div>
)) ))
@ -337,7 +265,7 @@ function IrcNetworkUpdateForm({isOpen, toggle, network}: any) {
<button <button
type="submit" type="submit"
disabled={pristine || invalid} disabled={pristine || invalid}
className={classNames(pristine || invalid ? "bg-indigo-300" : "bg-indigo-600 hover:bg-indigo-700","inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500")} className={classNames(pristine || invalid ? "bg-indigo-300" : "bg-indigo-600 hover:bg-indigo-700", "inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500")}
> >
Save Save
</button> </button>
@ -345,27 +273,7 @@ function IrcNetworkUpdateForm({isOpen, toggle, network}: any) {
</div> </div>
</div> </div>
{/*<div*/} <DEBUG values={values} />
{/* className="flex-shrink-0 px-4 border-t border-gray-200 py-5 sm:px-6">*/}
{/* <div className="space-x-3 flex justify-end">*/}
{/* <button*/}
{/* type="button"*/}
{/* className="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"*/}
{/* onClick={toggle}*/}
{/* >*/}
{/* Cancel*/}
{/* </button>*/}
{/* <button*/}
{/* type="submit"*/}
{/* disabled={pristine || invalid}*/}
{/* className={classNames(pristine || invalid ? "bg-indigo-300" : "bg-indigo-600 hover:bg-indigo-700","inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500")}*/}
{/* >*/}
{/* Save*/}
{/* </button>*/}
{/* </div>*/}
{/*</div>*/}
<DEBUG values={values}/>
</form> </form>
) )
}} }}

View file

@ -82,7 +82,7 @@ function IrcSettings() {
scope="col" scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
> >
Addr Server
</th> </th>
<th <th
scope="col" scope="col"
@ -138,8 +138,8 @@ const ListItem = ({ idx, network }: any) => {
</Switch> </Switch>
</td> </td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{network.name}</td> <td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{network.name}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{network.addr} {network.tls && <span className="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">TLS</span>}</td> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{network.server}:{network.port} {network.tls && <span className="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">TLS</span>}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{network.nick}</td> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{network.nickserv?.account}</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<span className="text-indigo-600 hover:text-indigo-900 cursor-pointer" onClick={toggleUpdate}> <span className="text-indigo-600 hover:text-indigo-900 cursor-pointer" onClick={toggleUpdate}>
Edit Edit