Compare commits
10 commits
66ec5d6c3d
...
252bc8140b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
252bc8140b | ||
|
|
067b62c23a | ||
|
|
096535292d | ||
|
|
a0265cc1ea | ||
|
|
91d4b99aee | ||
|
|
9474e6f31a | ||
|
|
1b79fe8079 | ||
|
|
ab51774554 | ||
|
|
a4ec585aed | ||
|
|
ab733cb573 |
9 changed files with 1761 additions and 612 deletions
|
|
@ -220,9 +220,14 @@ add_library(naut_example MODULE plugins/example/example.c)
|
|||
target_include_directories(naut_example PRIVATE ${CMAKE_SOURCE_DIR}/include)
|
||||
set_target_properties(naut_example PROPERTIES PREFIX "")
|
||||
|
||||
add_library(naut_webui MODULE plugins/webui/webui.c)
|
||||
# SQLite backs the webui account store.
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(SQLITE3 REQUIRED IMPORTED_TARGET sqlite3)
|
||||
|
||||
add_library(naut_webui MODULE plugins/webui/webui.c plugins/webui/webui_store.c)
|
||||
target_include_directories(naut_webui PRIVATE ${CMAKE_SOURCE_DIR}/include)
|
||||
target_link_libraries(naut_webui PRIVATE ${NAUT_JANSSON_TARGET} naut_net pthread)
|
||||
target_link_libraries(naut_webui PRIVATE ${NAUT_JANSSON_TARGET} naut_net
|
||||
PkgConfig::SQLITE3 OpenSSL::Crypto pthread)
|
||||
set_target_properties(naut_webui PROPERTIES PREFIX "")
|
||||
|
||||
# --- swarm: multi-peer download driver over the torrent-peer engine ---------
|
||||
|
|
|
|||
|
|
@ -15,3 +15,4 @@
|
|||
- ✅ I should be able to force re-run a rule for cases where it was modified.
|
||||
- ✅ RSS manual download button doesn't work — there's no + next to articles (feeds whose items only carry a <link>/Atom href had no source).
|
||||
- ✅ Rules should show their current matches.
|
||||
- ✅ We need a real login system backed by a database.
|
||||
|
|
@ -114,15 +114,6 @@ struct daemon_state {
|
|||
json_t *script_settings_schema; /* array of {key,label,type,default} */
|
||||
json_t *script_settings; /* object: key -> value string (user-set)*/
|
||||
char settings_file[PATH_MAX]; /* <state_dir>/script_settings.json */
|
||||
/* Label taxonomy: the web UI's full category + tag lists (including ones
|
||||
* created but not yet assigned). Web-layer schema; the daemon just persists
|
||||
* it so they survive restarts. */
|
||||
pthread_mutex_t taxonomy_lock;
|
||||
json_t *label_categories; /* array of {name, savePath} */
|
||||
json_t *label_tags; /* array of tag name strings */
|
||||
char taxonomy_file[PATH_MAX]; /* <state_dir>/labels.json */
|
||||
char data_dir[PATH_MAX]; /* the resolved state dir (for blobs) */
|
||||
pthread_mutex_t blob_lock; /* guards webui_<key>.json blob files */
|
||||
pthread_mutex_t torrent_lock;
|
||||
torrent_task *torrents[MAX_TORRENTS];
|
||||
size_t torrent_count;
|
||||
|
|
@ -1320,153 +1311,6 @@ static json_t *rpc_set_script_settings(void *opaque, const json_t *params,
|
|||
return script_status_json(state);
|
||||
}
|
||||
|
||||
/* --- label taxonomy (web-layer category + tag lists, persisted here) ------- */
|
||||
|
||||
static void persist_taxonomy(daemon_state *state) {
|
||||
if (!state->persist_enabled) return;
|
||||
pthread_mutex_lock(&state->taxonomy_lock);
|
||||
json_t *cats = state->label_categories
|
||||
? json_deep_copy(state->label_categories) : json_array();
|
||||
json_t *tags = state->label_tags
|
||||
? json_deep_copy(state->label_tags) : json_array();
|
||||
pthread_mutex_unlock(&state->taxonomy_lock);
|
||||
json_t *doc = json_object();
|
||||
if (!doc) { json_decref(cats); json_decref(tags); return; }
|
||||
json_object_set_new(doc, "categories", cats ? cats : json_array());
|
||||
json_object_set_new(doc, "tags", tags ? tags : json_array());
|
||||
char tmp[PATH_MAX + 8];
|
||||
snprintf(tmp, sizeof tmp, "%s.tmp", state->taxonomy_file);
|
||||
if (json_dump_file(doc, tmp, JSON_INDENT(2)) != 0 ||
|
||||
rename(tmp, state->taxonomy_file) != 0) {
|
||||
NAUT_WARN("persist: write %s failed", state->taxonomy_file);
|
||||
unlink(tmp);
|
||||
}
|
||||
json_decref(doc);
|
||||
}
|
||||
|
||||
static void load_taxonomy(daemon_state *state) {
|
||||
if (!state->persist_enabled) return;
|
||||
json_error_t jerr;
|
||||
json_t *doc = json_load_file(state->taxonomy_file, 0, &jerr);
|
||||
if (!doc) return;
|
||||
json_t *cats = json_object_get(doc, "categories");
|
||||
json_t *tags = json_object_get(doc, "tags");
|
||||
pthread_mutex_lock(&state->taxonomy_lock);
|
||||
if (json_is_array(cats)) {
|
||||
json_decref(state->label_categories);
|
||||
state->label_categories = json_deep_copy(cats);
|
||||
}
|
||||
if (json_is_array(tags)) {
|
||||
json_decref(state->label_tags);
|
||||
state->label_tags = json_deep_copy(tags);
|
||||
}
|
||||
pthread_mutex_unlock(&state->taxonomy_lock);
|
||||
json_decref(doc);
|
||||
}
|
||||
|
||||
static json_t *rpc_get_label_taxonomy(void *opaque, const json_t *params,
|
||||
naut_err *error) {
|
||||
(void)params;
|
||||
daemon_state *state = opaque;
|
||||
pthread_mutex_lock(&state->taxonomy_lock);
|
||||
json_t *cats = state->label_categories
|
||||
? json_deep_copy(state->label_categories) : json_array();
|
||||
json_t *tags = state->label_tags
|
||||
? json_deep_copy(state->label_tags) : json_array();
|
||||
pthread_mutex_unlock(&state->taxonomy_lock);
|
||||
json_t *out = json_object();
|
||||
if (!out) { json_decref(cats); json_decref(tags); *error = NAUT_ERR_NOMEM; return NULL; }
|
||||
json_object_set_new(out, "categories", cats ? cats : json_array());
|
||||
json_object_set_new(out, "tags", tags ? tags : json_array());
|
||||
*error = NAUT_OK;
|
||||
return out;
|
||||
}
|
||||
|
||||
static json_t *rpc_set_label_taxonomy(void *opaque, const json_t *params,
|
||||
naut_err *error) {
|
||||
daemon_state *state = opaque;
|
||||
if (!json_is_object(params)) { *error = NAUT_ERR_INVAL; return NULL; }
|
||||
json_t *cats = json_object_get(params, "categories");
|
||||
json_t *tags = json_object_get(params, "tags");
|
||||
pthread_mutex_lock(&state->taxonomy_lock);
|
||||
if (json_is_array(cats)) {
|
||||
json_decref(state->label_categories);
|
||||
state->label_categories = json_deep_copy(cats);
|
||||
}
|
||||
if (json_is_array(tags)) {
|
||||
json_decref(state->label_tags);
|
||||
state->label_tags = json_deep_copy(tags);
|
||||
}
|
||||
pthread_mutex_unlock(&state->taxonomy_lock);
|
||||
persist_taxonomy(state);
|
||||
*error = NAUT_OK;
|
||||
return json_object();
|
||||
}
|
||||
|
||||
/* --- generic web-UI blob store (RSS feeds, indexer config, ...) ------------ *
|
||||
* The web layer owns these schemas; the daemon only persists them, one JSON
|
||||
* document per key, under <state_dir>/webui_<key>.json. Keys are sanitized to a
|
||||
* safe filename charset so a key can never escape the state directory. */
|
||||
|
||||
static bool blob_path(daemon_state *state, const char *key, char *out, size_t n) {
|
||||
if (!key || !*key || !state->data_dir[0]) return false;
|
||||
char safe[64];
|
||||
size_t j = 0;
|
||||
for (size_t i = 0; key[i] && j + 1 < sizeof safe; i++) {
|
||||
char c = key[i];
|
||||
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
||||
(c >= '0' && c <= '9') || c == '_' || c == '-')
|
||||
safe[j++] = c;
|
||||
}
|
||||
safe[j] = 0;
|
||||
if (j == 0) return false;
|
||||
return (size_t)snprintf(out, n, "%s/webui_%s.json", state->data_dir, safe) < n;
|
||||
}
|
||||
|
||||
static json_t *rpc_get_webui_blob(void *opaque, const json_t *params,
|
||||
naut_err *error) {
|
||||
daemon_state *state = opaque;
|
||||
const char *key = json_string_value(json_object_get(params, "key"));
|
||||
char path[PATH_MAX];
|
||||
if (!blob_path(state, key, path, sizeof path)) {
|
||||
*error = NAUT_ERR_INVAL;
|
||||
return NULL;
|
||||
}
|
||||
pthread_mutex_lock(&state->blob_lock);
|
||||
json_error_t jerr;
|
||||
json_t *value = json_load_file(path, 0, &jerr);
|
||||
pthread_mutex_unlock(&state->blob_lock);
|
||||
json_t *out = json_object();
|
||||
if (!out) { json_decref(value); *error = NAUT_ERR_NOMEM; return NULL; }
|
||||
json_object_set_new(out, "value", value ? value : json_null());
|
||||
*error = NAUT_OK;
|
||||
return out;
|
||||
}
|
||||
|
||||
static json_t *rpc_set_webui_blob(void *opaque, const json_t *params,
|
||||
naut_err *error) {
|
||||
daemon_state *state = opaque;
|
||||
const char *key = json_string_value(json_object_get(params, "key"));
|
||||
json_t *value = json_object_get(params, "value");
|
||||
char path[PATH_MAX];
|
||||
if (!value || !blob_path(state, key, path, sizeof path)) {
|
||||
*error = NAUT_ERR_INVAL;
|
||||
return NULL;
|
||||
}
|
||||
*error = NAUT_OK;
|
||||
if (!state->persist_enabled) return json_object();
|
||||
pthread_mutex_lock(&state->blob_lock);
|
||||
char tmp[PATH_MAX + 8];
|
||||
snprintf(tmp, sizeof tmp, "%s.tmp", path);
|
||||
if (json_dump_file(value, tmp, JSON_INDENT(2)) != 0 ||
|
||||
rename(tmp, path) != 0) {
|
||||
NAUT_WARN("persist: write %s failed", path);
|
||||
unlink(tmp);
|
||||
}
|
||||
pthread_mutex_unlock(&state->blob_lock);
|
||||
return json_object();
|
||||
}
|
||||
|
||||
/* --- data-overlap guard (block torrents that would write the same files) --- */
|
||||
|
||||
static uint8_t *slurp_file(const char *path, size_t *len) {
|
||||
|
|
@ -2393,10 +2237,6 @@ static bool register_commands(daemon_state *state) {
|
|||
naut_rpc_register(state->rpc, "script_status", rpc_script_status, state) == NAUT_OK &&
|
||||
naut_rpc_register(state->rpc, "update_script", rpc_update_script, state) == NAUT_OK &&
|
||||
naut_rpc_register(state->rpc, "set_script_settings", rpc_set_script_settings, state) == NAUT_OK &&
|
||||
naut_rpc_register(state->rpc, "get_label_taxonomy", rpc_get_label_taxonomy, state) == NAUT_OK &&
|
||||
naut_rpc_register(state->rpc, "set_label_taxonomy", rpc_set_label_taxonomy, state) == NAUT_OK &&
|
||||
naut_rpc_register(state->rpc, "get_webui_blob", rpc_get_webui_blob, state) == NAUT_OK &&
|
||||
naut_rpc_register(state->rpc, "set_webui_blob", rpc_set_webui_blob, state) == NAUT_OK &&
|
||||
naut_rpc_register(state->rpc, "unload_script", rpc_unload_script, state) == NAUT_OK &&
|
||||
naut_rpc_register(state->rpc, "shutdown", rpc_shutdown, state) == NAUT_OK;
|
||||
}
|
||||
|
|
@ -2685,12 +2525,6 @@ static bool resolve_state_dir(daemon_state *state, const char *override) {
|
|||
"%s/script_settings.json", dir) >=
|
||||
sizeof state->settings_file)
|
||||
return false;
|
||||
if ((size_t)snprintf(state->taxonomy_file, sizeof state->taxonomy_file,
|
||||
"%s/labels.json", dir) >= sizeof state->taxonomy_file)
|
||||
return false;
|
||||
if ((size_t)snprintf(state->data_dir, sizeof state->data_dir, "%s", dir) >=
|
||||
sizeof state->data_dir)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -2796,9 +2630,6 @@ int main(int argc, char **argv) {
|
|||
pthread_mutex_init(&state.script_lock, NULL);
|
||||
pthread_mutex_init(&state.settings_lock, NULL);
|
||||
load_script_settings(&state); /* user-set values; schema comes from the script */
|
||||
pthread_mutex_init(&state.taxonomy_lock, NULL);
|
||||
load_taxonomy(&state); /* persisted category + tag lists for the web UI */
|
||||
pthread_mutex_init(&state.blob_lock, NULL);
|
||||
state.events = naut_event_bus_create();
|
||||
state.rpc = naut_rpc_registry_create();
|
||||
state.plugins = naut_plugin_manager_create(state.rpc, state.events);
|
||||
|
|
@ -2881,9 +2712,5 @@ int main(int argc, char **argv) {
|
|||
json_decref(state.script_settings);
|
||||
json_decref(state.script_settings_schema);
|
||||
pthread_mutex_destroy(&state.settings_lock);
|
||||
json_decref(state.label_categories);
|
||||
json_decref(state.label_tags);
|
||||
pthread_mutex_destroy(&state.taxonomy_lock);
|
||||
pthread_mutex_destroy(&state.blob_lock);
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,10 @@ static uint32_t target_peer_count(void) {
|
|||
return (uint32_t)NAUT_MIN(value, MAX_TARGET_PEERS);
|
||||
}
|
||||
|
||||
/* Below this many connected peers the swarm is "starved": re-announce as often
|
||||
* as the tracker's min interval allows instead of waiting the full interval. */
|
||||
#define LOW_PEER_THRESHOLD 10
|
||||
|
||||
static double tracker_delay_seconds(int32_t interval) {
|
||||
if (interval <= 0) return TRACKER_DEFAULT_INTERVAL;
|
||||
if (interval < (int32_t)TRACKER_MIN_INTERVAL)
|
||||
|
|
@ -65,6 +69,20 @@ static double tracker_delay_seconds(int32_t interval) {
|
|||
return (double)interval;
|
||||
}
|
||||
|
||||
/* Seconds to wait before the next tracker announce. Normally the tracker's full
|
||||
* advertised interval, but when the swarm is starved (< LOW_PEER_THRESHOLD
|
||||
* connected peers) we re-announce sooner — down to the tracker's min_interval,
|
||||
* never below our 60s floor — so a thin swarm can actually recover. */
|
||||
static double next_announce_delay(int32_t interval, int32_t min_interval,
|
||||
uint32_t peers_connected) {
|
||||
double full = interval > 0 ? tracker_delay_seconds(interval)
|
||||
: TRACKER_FAILURE_RETRY_INTERVAL;
|
||||
if (peers_connected >= LOW_PEER_THRESHOLD) return full;
|
||||
double floor_s = min_interval > 0 ? (double)min_interval : TRACKER_MIN_INTERVAL;
|
||||
if (floor_s < TRACKER_MIN_INTERVAL) floor_s = TRACKER_MIN_INTERVAL;
|
||||
return floor_s < full ? floor_s : full;
|
||||
}
|
||||
|
||||
static void random_bytes(uint8_t *output, size_t length) {
|
||||
int fd = open("/dev/urandom", O_RDONLY);
|
||||
size_t offset = 0;
|
||||
|
|
@ -385,6 +403,7 @@ static bool discover_trackers(const uint8_t info_hash[20], uint64_t total_length
|
|||
naut_tracker_event event,
|
||||
endpoint_t **eps, size_t *neps, size_t *cap,
|
||||
int32_t *announce_interval,
|
||||
int32_t *announce_min_interval,
|
||||
naut_swarm_tracker_stats *tracker_stats,
|
||||
uint32_t tracker_count) {
|
||||
naut_announce_req req;
|
||||
|
|
@ -399,6 +418,7 @@ static bool discover_trackers(const uint8_t info_hash[20], uint64_t total_length
|
|||
req.numwant = 100;
|
||||
memcpy(&req.key, peerid + 8, sizeof req.key);
|
||||
if (announce_interval) *announce_interval = 0;
|
||||
if (announce_min_interval) *announce_min_interval = 0;
|
||||
|
||||
size_t tier_start = 0;
|
||||
while (tier_start < num_trackers) {
|
||||
|
|
@ -451,6 +471,8 @@ static bool discover_trackers(const uint8_t info_hash[20], uint64_t total_length
|
|||
tier_succeeded = true;
|
||||
if (announce_interval && response.interval > 0)
|
||||
*announce_interval = response.interval;
|
||||
if (announce_min_interval && response.min_interval > 0)
|
||||
*announce_min_interval = response.min_interval;
|
||||
if (tracker_stat) {
|
||||
tracker_set_status(tracker_stat, "working", "");
|
||||
tracker_stat->seeds = response.seeders;
|
||||
|
|
@ -574,7 +596,7 @@ naut_err naut_swarm_run(const naut_swarm_config *config) {
|
|||
endpoint_t *endpoints = NULL;
|
||||
size_t neps = 0, epcap = 0;
|
||||
uint32_t target_peers = target_peer_count();
|
||||
int32_t tracker_interval = 0;
|
||||
int32_t tracker_interval = 0, tracker_min_interval = 0;
|
||||
naut_swarm_tracker_stats tracker_stats[NAUT_SWARM_MAX_TRACKER_STATS];
|
||||
uint32_t tracker_count = 0;
|
||||
if (config->num_peers > 0) {
|
||||
|
|
@ -608,8 +630,8 @@ naut_err naut_swarm_run(const naut_swarm_config *config) {
|
|||
if (!discover_trackers(hash, 0, trackers, num_trackers,
|
||||
tracker_tiers, peerid, 0, 0,
|
||||
NAUT_TEV_STARTED, &endpoints, &neps, &epcap,
|
||||
&tracker_interval, tracker_stats,
|
||||
tracker_count) ||
|
||||
&tracker_interval, &tracker_min_interval,
|
||||
tracker_stats, tracker_count) ||
|
||||
(neps < target_peers &&
|
||||
!discover_dht(hash, &endpoints, &neps, &epcap))) {
|
||||
NAUT_ERROR("out of memory collecting discovered peers");
|
||||
|
|
@ -731,8 +753,8 @@ naut_err naut_swarm_run(const naut_swarm_config *config) {
|
|||
mi.tracker_tiers, peerid,
|
||||
resumed_bytes, resumed_left,
|
||||
NAUT_TEV_STARTED, &endpoints, &neps, &epcap,
|
||||
&tracker_interval, tracker_stats,
|
||||
tracker_count) ||
|
||||
&tracker_interval, &tracker_min_interval,
|
||||
tracker_stats, tracker_count) ||
|
||||
(neps < target_peers &&
|
||||
!discover_dht(mi.infohash_v1, &endpoints, &neps, &epcap))) {
|
||||
NAUT_ERROR("out of memory collecting discovered peers");
|
||||
|
|
@ -787,10 +809,11 @@ naut_err naut_swarm_run(const naut_swarm_config *config) {
|
|||
feed_engine(eng, torrent_id, endpoints, neps, &fed);
|
||||
|
||||
double t0 = now();
|
||||
/* Use the discovered endpoint count as the initial peer proxy: if we start
|
||||
* thin, schedule a quick re-announce instead of waiting the full interval. */
|
||||
double next_tracker_announce =
|
||||
t0 + (tracker_interval > 0
|
||||
? tracker_delay_seconds(tracker_interval)
|
||||
: TRACKER_FAILURE_RETRY_INTERVAL);
|
||||
t0 + next_announce_delay(tracker_interval, tracker_min_interval,
|
||||
(uint32_t)neps);
|
||||
double next_dht_lookup = t0 + DHT_REFRESH_INTERVAL;
|
||||
naut_err run_error = NAUT_OK;
|
||||
bool cancelled = false;
|
||||
|
|
@ -827,21 +850,20 @@ naut_err naut_swarm_run(const naut_swarm_config *config) {
|
|||
uint64_t downloaded = naut_download_bytes_done(d);
|
||||
uint64_t left = (uint64_t)mi.total_length > downloaded
|
||||
? (uint64_t)mi.total_length - downloaded : 0;
|
||||
int32_t interval = 0;
|
||||
int32_t interval = 0, min_interval = 0;
|
||||
if (!discover_trackers(mi.infohash_v1,
|
||||
(uint64_t)mi.total_length,
|
||||
mi.trackers, mi.num_trackers,
|
||||
mi.tracker_tiers, peerid,
|
||||
downloaded, left, NAUT_TEV_NONE,
|
||||
&endpoints, &neps, &epcap,
|
||||
&interval, tracker_stats,
|
||||
tracker_count)) {
|
||||
&interval, &min_interval,
|
||||
tracker_stats, tracker_count)) {
|
||||
run_error = NAUT_ERR_NOMEM;
|
||||
break;
|
||||
}
|
||||
next_tracker_announce = t + (interval > 0
|
||||
? tracker_delay_seconds(interval)
|
||||
: TRACKER_FAILURE_RETRY_INTERVAL);
|
||||
next_tracker_announce = t + next_announce_delay(
|
||||
interval, min_interval, ts.peers_connected);
|
||||
feed_engine(eng, torrent_id, endpoints, neps, &fed);
|
||||
}
|
||||
if (t >= next_dht_lookup) {
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ typedef struct {
|
|||
|
||||
typedef struct {
|
||||
int32_t interval;
|
||||
int32_t min_interval; /* tracker's floor, 0 if not advertised */
|
||||
int32_t seeders, leechers; /* -1 if absent */
|
||||
naut_peer_addr *peers;
|
||||
size_t num_peers;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
1135
plugins/webui/webui_store.c
Normal file
1135
plugins/webui/webui_store.c
Normal file
File diff suppressed because it is too large
Load diff
107
plugins/webui/webui_store.h
Normal file
107
plugins/webui/webui_store.h
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
/* webui_store.h — SQLite-backed persistence for all web-UI-owned state:
|
||||
* accounts, the category/tag taxonomy, and RSS feeds/rules/indexers.
|
||||
*
|
||||
* Owned entirely by the webui plugin (the daemon persists none of this).
|
||||
* Account passwords are PBKDF2-HMAC-SHA256 with a per-user random salt. All
|
||||
* calls are thread-safe (the store serializes access to its SQLite handle). */
|
||||
#ifndef NAUT_WEBUI_STORE_H
|
||||
#define NAUT_WEBUI_STORE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <jansson.h>
|
||||
|
||||
typedef struct webui_store webui_store;
|
||||
|
||||
/* Open (creating if needed) the account database at `path`. Returns NULL on
|
||||
* failure. The schema is created/migrated on open. */
|
||||
webui_store *webui_store_open(const char *path);
|
||||
void webui_store_close(webui_store *s);
|
||||
|
||||
/* Number of accounts, or -1 on error. */
|
||||
int webui_store_user_count(webui_store *s);
|
||||
/* Number of admin accounts, or -1 on error. */
|
||||
int webui_store_admin_count(webui_store *s);
|
||||
bool webui_store_user_exists(webui_store *s, const char *username);
|
||||
|
||||
/* Verify a username/password pair (constant-time). On success, copies the
|
||||
* account's role ("admin"/"user") into role_out. */
|
||||
bool webui_store_verify(webui_store *s, const char *username,
|
||||
const char *password, char *role_out, size_t role_sz);
|
||||
|
||||
/* Create an account. `role` must be "admin" or "user" (defaults to "user" if
|
||||
* NULL/invalid). Returns false if the username already exists or on error. */
|
||||
bool webui_store_create_user(webui_store *s, const char *username,
|
||||
const char *password, const char *role);
|
||||
|
||||
bool webui_store_set_password(webui_store *s, const char *username,
|
||||
const char *password);
|
||||
/* Change an account's role ("admin"/"user"). */
|
||||
bool webui_store_set_role(webui_store *s, const char *username, const char *role);
|
||||
bool webui_store_delete_user(webui_store *s, const char *username);
|
||||
|
||||
/* Append {username, role, createdAt} objects (sorted by username) to the
|
||||
* json array `out`. Returns false on error. */
|
||||
bool webui_store_list_users(webui_store *s, json_t *out);
|
||||
|
||||
/* --- sessions (persisted so logins survive daemon restarts) --------------- *
|
||||
* Only a SHA-256 of the bearer token is stored, so a DB read can't be replayed
|
||||
* as a live cookie. `expires` is an absolute unix time. */
|
||||
bool webui_store_session_create(webui_store *s, const char *token,
|
||||
const char *user, const char *role, long expires);
|
||||
/* On a live (unexpired) session, copies username/role and the stored expiry. */
|
||||
bool webui_store_session_lookup(webui_store *s, const char *token,
|
||||
char *user, size_t user_sz,
|
||||
char *role, size_t role_sz, long *expires_out);
|
||||
bool webui_store_session_touch(webui_store *s, const char *token, long expires);
|
||||
bool webui_store_session_delete(webui_store *s, const char *token);
|
||||
bool webui_store_sessions_delete_user(webui_store *s, const char *user);
|
||||
void webui_store_sessions_prune(webui_store *s, long now);
|
||||
|
||||
/* --- category / tag taxonomy (web-UI organization, owned here) ------------- *
|
||||
* The save_* calls replace the whole list atomically; the load_* calls append
|
||||
* to the (array) `out`. Categories are {name, savePath}; tags are strings. */
|
||||
bool webui_store_save_categories(webui_store *s, json_t *cats);
|
||||
bool webui_store_load_categories(webui_store *s, json_t *out);
|
||||
bool webui_store_save_tags(webui_store *s, json_t *tags);
|
||||
bool webui_store_load_tags(webui_store *s, json_t *out);
|
||||
|
||||
/* --- RSS: feeds, articles, auto-download rules, Torznab indexers ----------- *
|
||||
* Fully relational: articles live in their own table (deduped by feed+key,
|
||||
* indexed), and a rule's feed scope lives in a rule_feeds join table. The web
|
||||
* layer operates on rows, not whole-list blobs. */
|
||||
|
||||
/* Feeds. upsert preserves an existing feed's lastUpdate (only the url changes);
|
||||
* remove also drops the feed's articles. feed_list appends
|
||||
* {name,url,lastUpdate,articles:[...]} (newest article first). feed_targets
|
||||
* appends lightweight {name,url} objects for the poller. */
|
||||
bool webui_store_feed_upsert(webui_store *s, const char *name, const char *url);
|
||||
bool webui_store_feed_remove(webui_store *s, const char *name);
|
||||
bool webui_store_feed_set_updated(webui_store *s, const char *name, long ts);
|
||||
bool webui_store_feed_list(webui_store *s, json_t *out);
|
||||
bool webui_store_feed_targets(webui_store *s, json_t *out);
|
||||
bool webui_store_feed_exists(webui_store *s, const char *name);
|
||||
|
||||
/* Articles. add inserts unless (feed,key) already exists: returns 1 if newly
|
||||
* inserted, 0 if a duplicate, -1 on error. trim keeps the newest `keep` for a
|
||||
* feed. mark_grabbed flags every article with this key. ungrabbed appends
|
||||
* {feed,key,title,magnet,torrentUrl} for not-yet-grabbed articles. */
|
||||
int webui_store_article_add(webui_store *s, const char *feed, json_t *article);
|
||||
bool webui_store_article_trim(webui_store *s, const char *feed, int keep);
|
||||
bool webui_store_article_mark_grabbed(webui_store *s, const char *key);
|
||||
bool webui_store_articles_ungrabbed(webui_store *s, json_t *out);
|
||||
|
||||
/* Rules. upsert replaces the rule row and its feed scope; list/get assemble the
|
||||
* rule with its affectedFeeds array. */
|
||||
bool webui_store_rule_upsert(webui_store *s, json_t *rule);
|
||||
bool webui_store_rule_remove(webui_store *s, const char *name);
|
||||
bool webui_store_rule_list(webui_store *s, json_t *out);
|
||||
json_t *webui_store_rule_get(webui_store *s, const char *name);
|
||||
bool webui_store_rule_set_match(webui_store *s, const char *name, long ts);
|
||||
|
||||
/* Torznab indexers. */
|
||||
bool webui_store_indexer_upsert(webui_store *s, json_t *indexer);
|
||||
bool webui_store_indexer_remove(webui_store *s, const char *name);
|
||||
bool webui_store_indexer_list(webui_store *s, json_t *out);
|
||||
|
||||
#endif /* NAUT_WEBUI_STORE_H */
|
||||
|
|
@ -51,6 +51,7 @@ static naut_err collect_peers(const tracker_peer *peers, size_t count,
|
|||
const tracker_announce_response *resp,
|
||||
naut_tracker_response *out) {
|
||||
out->interval = (int32_t)resp->interval;
|
||||
out->min_interval = (int32_t)resp->min_interval;
|
||||
out->seeders = (int32_t)resp->complete;
|
||||
out->leechers = (int32_t)resp->incomplete;
|
||||
out->peers = NULL;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue