admin: Global settings can be specified on the command-line. Removed some previous, less thorough ways of configuring things. #102
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
This commit is contained in:
parent
6247529799
commit
c794c1b885
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "🎛",
|
"emoji": "🎛",
|
||||||
"previous": "&R49FywYF8CXPhoSEydLbSCgvCddeyTiBwGuDU/gqY+M=.sha256"
|
"previous": "&kmKNyb/uaXNb24gCinJtfS8iWx4cLUWdtl0y2DwEUas=.sha256"
|
||||||
}
|
}
|
||||||
|
@ -72,7 +72,7 @@ ${description.value}</textarea
|
|||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else if (description.type != 'hidden') {
|
||||||
return html`
|
return html`
|
||||||
<li class="w3-row">
|
<li class="w3-row">
|
||||||
<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold">${title_case(key)}</label>
|
<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold">${title_case(key)}</label>
|
||||||
|
@ -470,7 +470,6 @@ async function getProcessBlob(blobId, key, options) {
|
|||||||
imports.ssb = Object.fromEntries(
|
imports.ssb = Object.fromEntries(
|
||||||
Object.keys(ssb).map((key) => [key, ssb[key].bind(ssb)])
|
Object.keys(ssb).map((key) => [key, ssb[key].bind(ssb)])
|
||||||
);
|
);
|
||||||
imports.ssb.port = tildefriends.ssb_port;
|
|
||||||
imports.ssb.createIdentity = () => process.createIdentity();
|
imports.ssb.createIdentity = () => process.createIdentity();
|
||||||
imports.ssb.addIdentity = function (id) {
|
imports.ssb.addIdentity = function (id) {
|
||||||
if (
|
if (
|
||||||
|
@ -2319,35 +2319,6 @@ static void _httpd_endpoint_app_socket(tf_http_request_t* request)
|
|||||||
JS_FreeValue(context, global);
|
JS_FreeValue(context, global);
|
||||||
}
|
}
|
||||||
|
|
||||||
static int _tf_httpd_get_tildefriends_int(JSContext* context, const char* arg)
|
|
||||||
{
|
|
||||||
JSValue global = JS_GetGlobalObject(context);
|
|
||||||
JSValue tildefriends = JS_GetPropertyStr(context, global, "tildefriends");
|
|
||||||
JSValue arg_value = JS_GetPropertyStr(context, tildefriends, arg);
|
|
||||||
int value = 0;
|
|
||||||
JS_ToInt32(context, &value, arg_value);
|
|
||||||
JS_FreeValue(context, arg_value);
|
|
||||||
JS_FreeValue(context, tildefriends);
|
|
||||||
JS_FreeValue(context, global);
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
static const char* _tf_httpd_get_tildefriends_arg_string(JSContext* context, const char* arg)
|
|
||||||
{
|
|
||||||
JSValue global = JS_GetGlobalObject(context);
|
|
||||||
JSValue tildefriends = JS_GetPropertyStr(context, global, "tildefriends");
|
|
||||||
JSValue args = JS_GetPropertyStr(context, tildefriends, "args");
|
|
||||||
JSValue arg_value = JS_GetPropertyStr(context, args, arg);
|
|
||||||
const char* value = !JS_IsUndefined(arg_value) ? JS_ToCString(context, arg_value) : NULL;
|
|
||||||
const char* result = value ? tf_strdup(value) : NULL;
|
|
||||||
JS_FreeCString(context, value);
|
|
||||||
JS_FreeValue(context, arg_value);
|
|
||||||
JS_FreeValue(context, args);
|
|
||||||
JS_FreeValue(context, tildefriends);
|
|
||||||
JS_FreeValue(context, global);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void _httpd_free_user_data(void* user_data)
|
static void _httpd_free_user_data(void* user_data)
|
||||||
{
|
{
|
||||||
tf_free(user_data);
|
tf_free(user_data);
|
||||||
@ -2394,10 +2365,6 @@ void tf_httpd_register(JSContext* context)
|
|||||||
fprintf(stderr, "Failed to register Request.\n");
|
fprintf(stderr, "Failed to register Request.\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
int http_port = _tf_httpd_get_tildefriends_int(context, "http_port");
|
|
||||||
int https_port = _tf_httpd_get_tildefriends_int(context, "https_port");
|
|
||||||
const char* out_http_port_file = _tf_httpd_get_tildefriends_arg_string(context, "out_http_port_file");
|
|
||||||
|
|
||||||
JSValue global = JS_GetGlobalObject(context);
|
JSValue global = JS_GetGlobalObject(context);
|
||||||
JSValue httpd = JS_NewObjectClass(context, _httpd_class_id);
|
JSValue httpd = JS_NewObjectClass(context, _httpd_class_id);
|
||||||
|
|
||||||
@ -2408,6 +2375,15 @@ void tf_httpd_register(JSContext* context)
|
|||||||
tf_http_set_trace(http, tf_task_get_trace(task));
|
tf_http_set_trace(http, tf_task_get_trace(task));
|
||||||
JS_SetOpaque(httpd, http);
|
JS_SetOpaque(httpd, http);
|
||||||
|
|
||||||
|
int64_t http_port = 0;
|
||||||
|
int64_t https_port = 0;
|
||||||
|
char out_http_port_file[512] = "";
|
||||||
|
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
|
||||||
|
tf_ssb_db_get_global_setting_int64(db, "http_port", &http_port);
|
||||||
|
tf_ssb_db_get_global_setting_int64(db, "https_port", &https_port);
|
||||||
|
tf_ssb_db_get_global_setting_string(db, "out_http_port_file", out_http_port_file, sizeof(out_http_port_file));
|
||||||
|
tf_ssb_release_db_reader(ssb, db);
|
||||||
|
|
||||||
if (https_port)
|
if (https_port)
|
||||||
{
|
{
|
||||||
http_user_data_t* user_data = tf_http_get_user_data(http);
|
http_user_data_t* user_data = tf_http_get_user_data(http);
|
||||||
@ -2463,14 +2439,14 @@ void tf_httpd_register(JSContext* context)
|
|||||||
JS_SetPropertyStr(context, global, "httpd", httpd);
|
JS_SetPropertyStr(context, global, "httpd", httpd);
|
||||||
JS_FreeValue(context, global);
|
JS_FreeValue(context, global);
|
||||||
|
|
||||||
if (http_port > 0 || out_http_port_file)
|
if (http_port > 0 || *out_http_port_file)
|
||||||
{
|
{
|
||||||
httpd_listener_t* listener = tf_malloc(sizeof(httpd_listener_t));
|
httpd_listener_t* listener = tf_malloc(sizeof(httpd_listener_t));
|
||||||
*listener = (httpd_listener_t) { 0 };
|
*listener = (httpd_listener_t) { 0 };
|
||||||
int assigned_port = tf_http_listen(http, http_port, NULL, _httpd_listener_cleanup, listener);
|
int assigned_port = tf_http_listen(http, http_port, NULL, _httpd_listener_cleanup, listener);
|
||||||
tf_printf(CYAN "~😎 Tilde Friends" RESET " " YELLOW VERSION_NUMBER RESET " is now up at " MAGENTA "http://127.0.0.1:%d/" RESET ".\n", assigned_port);
|
tf_printf(CYAN "~😎 Tilde Friends" RESET " " YELLOW VERSION_NUMBER RESET " is now up at " MAGENTA "http://127.0.0.1:%d/" RESET ".\n", assigned_port);
|
||||||
|
|
||||||
if (out_http_port_file)
|
if (*out_http_port_file)
|
||||||
{
|
{
|
||||||
const char* actual_http_port_file = tf_task_get_path_with_root(task, out_http_port_file);
|
const char* actual_http_port_file = tf_task_get_path_with_root(task, out_http_port_file);
|
||||||
FILE* file = fopen(actual_http_port_file, "wb");
|
FILE* file = fopen(actual_http_port_file, "wb");
|
||||||
@ -2507,6 +2483,4 @@ void tf_httpd_register(JSContext* context)
|
|||||||
tf_free((char*)private_key);
|
tf_free((char*)private_key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tf_free((void*)out_http_port_file);
|
|
||||||
}
|
}
|
||||||
|
41
src/main.c
41
src/main.c
@ -1217,9 +1217,6 @@ typedef struct tf_run_args_t
|
|||||||
{
|
{
|
||||||
const char* script;
|
const char* script;
|
||||||
const char* network_key;
|
const char* network_key;
|
||||||
int ssb_port;
|
|
||||||
int http_port;
|
|
||||||
int https_port;
|
|
||||||
const char* db_path;
|
const char* db_path;
|
||||||
int count;
|
int count;
|
||||||
const char* args;
|
const char* args;
|
||||||
@ -1244,9 +1241,6 @@ static int _tf_run_task(const tf_run_args_t* args, int index)
|
|||||||
tf_printf("setting zip path to %s\n", args->zip);
|
tf_printf("setting zip path to %s\n", args->zip);
|
||||||
tf_task_set_zip_path(task, args->zip);
|
tf_task_set_zip_path(task, args->zip);
|
||||||
tf_task_set_ssb_network_key(task, args->network_key);
|
tf_task_set_ssb_network_key(task, args->network_key);
|
||||||
tf_task_set_ssb_port(task, args->ssb_port ? args->ssb_port + index : 0);
|
|
||||||
tf_task_set_http_port(task, args->http_port ? args->http_port + index : 0);
|
|
||||||
tf_task_set_https_port(task, args->https_port ? args->https_port + index : 0);
|
|
||||||
tf_task_set_args(task, args->args);
|
tf_task_set_args(task, args->args);
|
||||||
tf_task_set_one_proc(task, args->one_proc);
|
tf_task_set_one_proc(task, args->one_proc);
|
||||||
const char* db_path = args->db_path;
|
const char* db_path = args->db_path;
|
||||||
@ -1301,7 +1295,14 @@ static int _tf_run_task(const tf_run_args_t* args, int index)
|
|||||||
tf_task_activate(task);
|
tf_task_activate(task);
|
||||||
tf_ssb_set_verbose(tf_task_get_ssb(task), args->verbose);
|
tf_ssb_set_verbose(tf_task_get_ssb(task), args->verbose);
|
||||||
tf_ssb_start_periodic(tf_task_get_ssb(task));
|
tf_ssb_start_periodic(tf_task_get_ssb(task));
|
||||||
if (args->http_port || args->https_port)
|
tf_ssb_t* ssb = tf_task_get_ssb(task);
|
||||||
|
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
|
||||||
|
int64_t http_port = 0;
|
||||||
|
int64_t https_port = 0;
|
||||||
|
tf_ssb_db_get_global_setting_int64(db, "http_port", &http_port);
|
||||||
|
tf_ssb_db_get_global_setting_int64(db, "https_port", &https_port);
|
||||||
|
tf_ssb_release_db_reader(ssb, db);
|
||||||
|
if (http_port || https_port)
|
||||||
{
|
{
|
||||||
if (args->zip)
|
if (args->zip)
|
||||||
{
|
{
|
||||||
@ -1417,9 +1418,6 @@ static int _tf_command_run(const char* file, int argc, char* argv[])
|
|||||||
const char* default_db_path = _get_db_path();
|
const char* default_db_path = _get_db_path();
|
||||||
tf_run_args_t args = {
|
tf_run_args_t args = {
|
||||||
.count = 1,
|
.count = 1,
|
||||||
.http_port = 12345,
|
|
||||||
.https_port = 12346,
|
|
||||||
.ssb_port = 8008,
|
|
||||||
.db_path = default_db_path,
|
.db_path = default_db_path,
|
||||||
};
|
};
|
||||||
bool show_usage = false;
|
bool show_usage = false;
|
||||||
@ -1436,10 +1434,7 @@ static int _tf_command_run(const char* file, int argc, char* argv[])
|
|||||||
{
|
{
|
||||||
static const struct option k_options[] = {
|
static const struct option k_options[] = {
|
||||||
{ "script", required_argument, NULL, 's' },
|
{ "script", required_argument, NULL, 's' },
|
||||||
{ "ssb-port", required_argument, NULL, 'b' },
|
|
||||||
{ "ssb-network-key", required_argument, NULL, 'k' },
|
{ "ssb-network-key", required_argument, NULL, 'k' },
|
||||||
{ "http-port", required_argument, NULL, 'p' },
|
|
||||||
{ "https-port", required_argument, NULL, 'q' },
|
|
||||||
{ "db-path", required_argument, NULL, 'd' },
|
{ "db-path", required_argument, NULL, 'd' },
|
||||||
{ "count", required_argument, NULL, 'n' },
|
{ "count", required_argument, NULL, 'n' },
|
||||||
{ "args", required_argument, NULL, 'a' },
|
{ "args", required_argument, NULL, 'a' },
|
||||||
@ -1448,7 +1443,7 @@ static int _tf_command_run(const char* file, int argc, char* argv[])
|
|||||||
{ "verbose", no_argument, NULL, 'v' },
|
{ "verbose", no_argument, NULL, 'v' },
|
||||||
{ "help", no_argument, NULL, 'h' },
|
{ "help", no_argument, NULL, 'h' },
|
||||||
};
|
};
|
||||||
int c = getopt_long(argc, argv, "s:b:k:p:q:d:n:a:oz:vh", k_options, NULL);
|
int c = getopt_long(argc, argv, "s:k:d:n:a:oz:vh", k_options, NULL);
|
||||||
if (c == -1)
|
if (c == -1)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
@ -1467,15 +1462,6 @@ static int _tf_command_run(const char* file, int argc, char* argv[])
|
|||||||
case 'k':
|
case 'k':
|
||||||
args.network_key = optarg;
|
args.network_key = optarg;
|
||||||
break;
|
break;
|
||||||
case 'b':
|
|
||||||
args.ssb_port = atoi(optarg);
|
|
||||||
break;
|
|
||||||
case 'p':
|
|
||||||
args.http_port = atoi(optarg);
|
|
||||||
break;
|
|
||||||
case 'q':
|
|
||||||
args.https_port = atoi(optarg);
|
|
||||||
break;
|
|
||||||
case 'd':
|
case 'd':
|
||||||
args.db_path = optarg;
|
args.db_path = optarg;
|
||||||
break;
|
break;
|
||||||
@ -1502,13 +1488,11 @@ static int _tf_command_run(const char* file, int argc, char* argv[])
|
|||||||
tf_printf("\n%s run [options]\n\n", file);
|
tf_printf("\n%s run [options]\n\n", file);
|
||||||
tf_printf("options\n");
|
tf_printf("options\n");
|
||||||
tf_printf(" -s, --script script Script to run (default: core/core.js).\n");
|
tf_printf(" -s, --script script Script to run (default: core/core.js).\n");
|
||||||
tf_printf(" -b, --ssb-port port Port on which to run SSB (default: 8008, 0 disables).\n");
|
|
||||||
tf_printf(" -p, --http-port port Port on which to run Tilde Friends web server (default: 12345).\n");
|
|
||||||
tf_printf(" -q, --https-port port Port on which to run secure Tilde Friends web server (default: 12346).\n");
|
|
||||||
tf_printf(" -d, --db-path path SQLite database path (default: %s).\n", default_db_path);
|
tf_printf(" -d, --db-path path SQLite database path (default: %s).\n", default_db_path);
|
||||||
tf_printf(" -k, --ssb-network-key key SSB network key to use.\n");
|
tf_printf(" -k, --ssb-network-key key SSB network key to use.\n");
|
||||||
tf_printf(" -n, --count count Number of instances to run.\n");
|
tf_printf(" -n, --count count Number of instances to run.\n");
|
||||||
tf_printf(" -a, --args args Arguments of the format key=value,foo=bar,verbose=true.\n");
|
tf_printf(" -a, --args args Arguments of the format key=value,foo=bar,verbose=true (note: these are persisted to the database).\n");
|
||||||
|
tf_util_document_settings(" ");
|
||||||
tf_printf(" -o, --one-proc Run everything in one process (unsafely!).\n");
|
tf_printf(" -o, --one-proc Run everything in one process (unsafely!).\n");
|
||||||
tf_printf(" -z, --zip path Zip archive from which to load files.\n");
|
tf_printf(" -z, --zip path Zip archive from which to load files.\n");
|
||||||
tf_printf(" -v, --verbose Log raw messages.\n");
|
tf_printf(" -v, --verbose Log raw messages.\n");
|
||||||
@ -1847,9 +1831,6 @@ void tf_run_thread_start(const char* zip_path)
|
|||||||
tf_run_thread_data_t* data = tf_malloc(sizeof(tf_run_thread_data_t));
|
tf_run_thread_data_t* data = tf_malloc(sizeof(tf_run_thread_data_t));
|
||||||
tf_run_args_t args = {
|
tf_run_args_t args = {
|
||||||
.count = 1,
|
.count = 1,
|
||||||
.http_port = 12345,
|
|
||||||
.https_port = 12346,
|
|
||||||
.ssb_port = 8008,
|
|
||||||
.db_path = "db.sqlite",
|
.db_path = "db.sqlite",
|
||||||
.one_proc = true,
|
.one_proc = true,
|
||||||
.zip = zip_path,
|
.zip = zip_path,
|
||||||
|
45
src/ssb.db.c
45
src/ssb.db.c
@ -2126,6 +2126,51 @@ bool tf_ssb_db_get_global_setting_string(sqlite3* db, const char* name, char* ou
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool tf_ssb_db_set_global_setting_from_string(sqlite3* db, const char* name, char* value)
|
||||||
|
{
|
||||||
|
tf_setting_kind_t kind = tf_util_get_global_setting_kind(name);
|
||||||
|
if (kind == k_kind_unknown)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool result = false;
|
||||||
|
sqlite3_stmt* statement;
|
||||||
|
if (sqlite3_prepare(db,
|
||||||
|
"INSERT INTO properties (id, key, value) VALUES ('core', 'settings', json_object(?1, ?2)) ON CONFLICT DO UPDATE SET value = json_set(value, '$.' || ?1, ?2)", -1,
|
||||||
|
&statement, NULL) == SQLITE_OK)
|
||||||
|
{
|
||||||
|
if (sqlite3_bind_text(statement, 1, name, -1, NULL) == SQLITE_OK)
|
||||||
|
{
|
||||||
|
bool bound = false;
|
||||||
|
switch (kind)
|
||||||
|
{
|
||||||
|
case k_kind_bool:
|
||||||
|
bound = sqlite3_bind_int(statement, 2, value && (strcmp(value, "true") == 0 || atoi(value))) == SQLITE_OK;
|
||||||
|
break;
|
||||||
|
case k_kind_int:
|
||||||
|
bound = sqlite3_bind_int(statement, 2, atoi(value)) == SQLITE_OK;
|
||||||
|
break;
|
||||||
|
case k_kind_string:
|
||||||
|
bound = sqlite3_bind_text(statement, 2, value, -1, NULL) == SQLITE_OK;
|
||||||
|
break;
|
||||||
|
case k_kind_unknown:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (bound && sqlite3_step(statement) == SQLITE_DONE)
|
||||||
|
{
|
||||||
|
result = sqlite3_changes(db) != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sqlite3_finalize(statement);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
tf_printf("prepare failed: %s\n", sqlite3_errmsg(db));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
const char* tf_ssb_db_get_profile(sqlite3* db, const char* id)
|
const char* tf_ssb_db_get_profile(sqlite3* db, const char* id)
|
||||||
{
|
{
|
||||||
const char* result = NULL;
|
const char* result = NULL;
|
||||||
|
@ -486,6 +486,15 @@ bool tf_ssb_db_get_global_setting_int64(sqlite3* db, const char* name, int64_t*
|
|||||||
*/
|
*/
|
||||||
bool tf_ssb_db_get_global_setting_string(sqlite3* db, const char* name, char* out_value, size_t size);
|
bool tf_ssb_db_get_global_setting_string(sqlite3* db, const char* name, char* out_value, size_t size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
** Set a global setting from a string representation of its value.
|
||||||
|
** @param db The database.
|
||||||
|
** @param name The setting name.
|
||||||
|
** @param value The settinv value.
|
||||||
|
** @return true if the setting was set.
|
||||||
|
*/
|
||||||
|
bool tf_ssb_db_set_global_setting_from_string(sqlite3* db, const char* name, char* value);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
** Get the latest profile information for the given identity.
|
** Get the latest profile information for the given identity.
|
||||||
** @param db The database.
|
** @param db The database.
|
||||||
|
@ -2357,6 +2357,12 @@ static JSValue _tf_ssb_set_user_permission(JSContext* context, JSValueConst this
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static JSValue _tf_ssb_port(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
|
||||||
|
{
|
||||||
|
tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId);
|
||||||
|
return JS_NewInt32(context, tf_ssb_server_get_port(ssb));
|
||||||
|
}
|
||||||
|
|
||||||
void tf_ssb_register(JSContext* context, tf_ssb_t* ssb)
|
void tf_ssb_register(JSContext* context, tf_ssb_t* ssb)
|
||||||
{
|
{
|
||||||
JS_NewClassID(&_tf_ssb_classId);
|
JS_NewClassID(&_tf_ssb_classId);
|
||||||
@ -2403,6 +2409,7 @@ void tf_ssb_register(JSContext* context, tf_ssb_t* ssb)
|
|||||||
JS_SetPropertyStr(context, object, "createTunnel", JS_NewCFunction(context, _tf_ssb_createTunnel, "createTunnel", 3));
|
JS_SetPropertyStr(context, object, "createTunnel", JS_NewCFunction(context, _tf_ssb_createTunnel, "createTunnel", 3));
|
||||||
JS_SetPropertyStr(context, object, "following", JS_NewCFunction(context, _tf_ssb_following, "following", 2));
|
JS_SetPropertyStr(context, object, "following", JS_NewCFunction(context, _tf_ssb_following, "following", 2));
|
||||||
JS_SetPropertyStr(context, object, "sync", JS_NewCFunction(context, _tf_ssb_sync, "sync", 0));
|
JS_SetPropertyStr(context, object, "sync", JS_NewCFunction(context, _tf_ssb_sync, "sync", 0));
|
||||||
|
JS_SetPropertyStr(context, object, "port", JS_NewCFunction(context, _tf_ssb_port, "port", 0));
|
||||||
/* Write. */
|
/* Write. */
|
||||||
JS_SetPropertyStr(context, object, "storeMessage", JS_NewCFunction(context, _tf_ssb_storeMessage, "storeMessage", 1));
|
JS_SetPropertyStr(context, object, "storeMessage", JS_NewCFunction(context, _tf_ssb_storeMessage, "storeMessage", 1));
|
||||||
JS_SetPropertyStr(context, object, "blobStore", JS_NewCFunction(context, _tf_ssb_blobStore, "blobStore", 1));
|
JS_SetPropertyStr(context, object, "blobStore", JS_NewCFunction(context, _tf_ssb_blobStore, "blobStore", 1));
|
||||||
|
@ -913,7 +913,7 @@ static void _write_file(const char* path, const char* contents)
|
|||||||
fclose(file);
|
fclose(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
#define TEST_ARGS " --ssb-port=0 --http-port=0 --https-port=0"
|
#define TEST_ARGS " --args=ssb_port=0,http_port=0,https_port=0"
|
||||||
|
|
||||||
void tf_ssb_test_encrypt(const tf_test_options_t* options)
|
void tf_ssb_test_encrypt(const tf_test_options_t* options)
|
||||||
{
|
{
|
||||||
|
100
src/task.c
100
src/task.c
@ -9,6 +9,7 @@
|
|||||||
#include "packetstream.h"
|
#include "packetstream.h"
|
||||||
#include "serialize.h"
|
#include "serialize.h"
|
||||||
#include "socket.js.h"
|
#include "socket.js.h"
|
||||||
|
#include "ssb.db.h"
|
||||||
#include "ssb.h"
|
#include "ssb.h"
|
||||||
#include "ssb.js.h"
|
#include "ssb.js.h"
|
||||||
#include "taskstub.js.h"
|
#include "taskstub.js.h"
|
||||||
@ -154,9 +155,6 @@ typedef struct _tf_task_t
|
|||||||
JSValue _loadedFiles;
|
JSValue _loadedFiles;
|
||||||
|
|
||||||
const char* _network_key;
|
const char* _network_key;
|
||||||
int _ssb_port;
|
|
||||||
int _http_port;
|
|
||||||
int _https_port;
|
|
||||||
char _db_path[256];
|
char _db_path[256];
|
||||||
char _zip_path[256];
|
char _zip_path[256];
|
||||||
char _root_path[256];
|
char _root_path[256];
|
||||||
@ -398,6 +396,7 @@ static JSValue _tf_task_exit(JSContext* context, JSValueConst this_val, int argc
|
|||||||
int exitCode = 0;
|
int exitCode = 0;
|
||||||
JS_ToInt32(task->_context, &exitCode, argv[0]);
|
JS_ToInt32(task->_context, &exitCode, argv[0]);
|
||||||
tf_trace_end(task->_trace);
|
tf_trace_end(task->_trace);
|
||||||
|
tf_printf("EXIT %d\n", exitCode);
|
||||||
exit(exitCode);
|
exit(exitCode);
|
||||||
return JS_UNDEFINED;
|
return JS_UNDEFINED;
|
||||||
}
|
}
|
||||||
@ -1680,38 +1679,6 @@ void tf_task_activate(tf_task_t* task)
|
|||||||
JS_DefinePropertyGetSet(context, global, atom, JS_NewCFunction(context, _tf_task_get_parent, "parent", 0), JS_UNDEFINED, 0);
|
JS_DefinePropertyGetSet(context, global, atom, JS_NewCFunction(context, _tf_task_get_parent, "parent", 0), JS_UNDEFINED, 0);
|
||||||
JS_FreeAtom(context, atom);
|
JS_FreeAtom(context, atom);
|
||||||
|
|
||||||
JSValue tildefriends = JS_NewObject(context);
|
|
||||||
JSValue args = JS_NewObject(context);
|
|
||||||
JS_SetPropertyStr(context, tildefriends, "args", args);
|
|
||||||
if (task->_args)
|
|
||||||
{
|
|
||||||
char* saveptr = NULL;
|
|
||||||
char* copy = tf_strdup(task->_args);
|
|
||||||
char* start = copy;
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
char* assignment = strtok_r(start, ",", &saveptr);
|
|
||||||
start = NULL;
|
|
||||||
if (!assignment)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
char* equals = strchr(assignment, '=');
|
|
||||||
if (equals)
|
|
||||||
{
|
|
||||||
*equals = '\0';
|
|
||||||
JS_SetPropertyStr(context, args, assignment, JS_NewString(context, equals + 1));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
tf_printf("Assignment missing '=': %s.\n", assignment);
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tf_free(copy);
|
|
||||||
}
|
|
||||||
JS_SetPropertyStr(context, global, "tildefriends", tildefriends);
|
|
||||||
|
|
||||||
task->_trace = tf_trace_create();
|
task->_trace = tf_trace_create();
|
||||||
if (task->_trusted)
|
if (task->_trusted)
|
||||||
{
|
{
|
||||||
@ -1732,23 +1699,45 @@ void tf_task_activate(tf_task_t* task)
|
|||||||
tf_ssb_register(context, task->_ssb);
|
tf_ssb_register(context, task->_ssb);
|
||||||
tf_ssb_set_hitch_callback(task->_ssb, _tf_task_record_hitch, task);
|
tf_ssb_set_hitch_callback(task->_ssb, _tf_task_record_hitch, task);
|
||||||
|
|
||||||
int actual_ssb_port = task->_ssb_port;
|
if (task->_args)
|
||||||
|
{
|
||||||
|
sqlite3* db = tf_ssb_acquire_db_writer(task->_ssb);
|
||||||
|
char* saveptr = NULL;
|
||||||
|
char* copy = tf_strdup(task->_args);
|
||||||
|
char* start = copy;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
char* assignment = strtok_r(start, ",", &saveptr);
|
||||||
|
start = NULL;
|
||||||
|
if (!assignment)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
char* equals = strchr(assignment, '=');
|
||||||
|
if (equals)
|
||||||
|
{
|
||||||
|
*equals = '\0';
|
||||||
|
tf_ssb_db_set_global_setting_from_string(db, assignment, equals + 1);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
tf_printf("Assignment missing '=': %s.\n", assignment);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tf_free(copy);
|
||||||
|
tf_ssb_release_db_writer(task->_ssb, db);
|
||||||
|
}
|
||||||
|
|
||||||
if (task->_ssb_port)
|
int64_t ssb_port = 0;
|
||||||
|
sqlite3* db = tf_ssb_acquire_db_reader(task->_ssb);
|
||||||
|
tf_ssb_db_get_global_setting_int64(db, "ssb_port", &ssb_port);
|
||||||
|
tf_ssb_release_db_reader(task->_ssb, db);
|
||||||
|
if (ssb_port)
|
||||||
{
|
{
|
||||||
tf_ssb_broadcast_listener_start(task->_ssb, false);
|
tf_ssb_broadcast_listener_start(task->_ssb, false);
|
||||||
tf_ssb_broadcast_sender_start(task->_ssb);
|
tf_ssb_broadcast_sender_start(task->_ssb);
|
||||||
actual_ssb_port = tf_ssb_server_open(task->_ssb, task->_ssb_port);
|
tf_ssb_server_open(task->_ssb, ssb_port);
|
||||||
}
|
|
||||||
|
|
||||||
JS_SetPropertyStr(context, tildefriends, "ssb_port", JS_NewInt32(context, actual_ssb_port));
|
|
||||||
if (task->_http_port)
|
|
||||||
{
|
|
||||||
JS_SetPropertyStr(context, tildefriends, "http_port", JS_NewInt32(context, task->_http_port));
|
|
||||||
}
|
|
||||||
if (task->_https_port)
|
|
||||||
{
|
|
||||||
JS_SetPropertyStr(context, tildefriends, "https_port", JS_NewInt32(context, task->_https_port));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
JS_SetPropertyStr(context, global, "getStats", JS_NewCFunction(context, _tf_task_getStats, "getStats", 0));
|
JS_SetPropertyStr(context, global, "getStats", JS_NewCFunction(context, _tf_task_getStats, "getStats", 0));
|
||||||
@ -1999,21 +1988,6 @@ void tf_task_set_ssb_network_key(tf_task_t* task, const char* network_key)
|
|||||||
task->_network_key = network_key;
|
task->_network_key = network_key;
|
||||||
}
|
}
|
||||||
|
|
||||||
void tf_task_set_ssb_port(tf_task_t* task, int port)
|
|
||||||
{
|
|
||||||
task->_ssb_port = port;
|
|
||||||
}
|
|
||||||
|
|
||||||
void tf_task_set_http_port(tf_task_t* task, int port)
|
|
||||||
{
|
|
||||||
task->_http_port = port;
|
|
||||||
}
|
|
||||||
|
|
||||||
void tf_task_set_https_port(tf_task_t* task, int port)
|
|
||||||
{
|
|
||||||
task->_https_port = port;
|
|
||||||
}
|
|
||||||
|
|
||||||
void tf_task_set_db_path(tf_task_t* task, const char* db_path)
|
void tf_task_set_db_path(tf_task_t* task, const char* db_path)
|
||||||
{
|
{
|
||||||
snprintf(task->_db_path, sizeof(task->_db_path), "%s", db_path);
|
snprintf(task->_db_path, sizeof(task->_db_path), "%s", db_path);
|
||||||
|
21
src/task.h
21
src/task.h
@ -76,27 +76,6 @@ void tf_task_configure_from_fd(tf_task_t* task, int fd);
|
|||||||
*/
|
*/
|
||||||
void tf_task_set_ssb_network_key(tf_task_t* task, const char* network_key);
|
void tf_task_set_ssb_network_key(tf_task_t* task, const char* network_key);
|
||||||
|
|
||||||
/**
|
|
||||||
** Set the port number on which to run an SSB secure handshake server.
|
|
||||||
** @param task The task.
|
|
||||||
** @param port The port number or 0 to disable.
|
|
||||||
*/
|
|
||||||
void tf_task_set_ssb_port(tf_task_t* task, int port);
|
|
||||||
|
|
||||||
/**
|
|
||||||
** Set the port number on which to run an HTTP server.
|
|
||||||
** @param task The task.
|
|
||||||
** @param port The port number of 0 to disable.
|
|
||||||
*/
|
|
||||||
void tf_task_set_http_port(tf_task_t* task, int port);
|
|
||||||
|
|
||||||
/**
|
|
||||||
** Set the port number on which to run an HTTPS server.
|
|
||||||
** @param task The task.
|
|
||||||
** @param port The port number of 0 to disable.
|
|
||||||
*/
|
|
||||||
void tf_task_set_https_port(tf_task_t* task, int port);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
** Set the path to the SQLite database.
|
** Set the path to the SQLite database.
|
||||||
** @param task The task.
|
** @param task The task.
|
||||||
|
11
src/tests.c
11
src/tests.c
@ -32,7 +32,7 @@
|
|||||||
#include <TargetConditionals.h>
|
#include <TargetConditionals.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#define TEST_ARGS " --ssb-port=0 --http-port=0 --https-port=0"
|
#define TEST_ARGS " --args=ssb_port=0,http_port=0,https_port=0"
|
||||||
|
|
||||||
#if !TARGET_OS_IPHONE
|
#if !TARGET_OS_IPHONE
|
||||||
static void _write_file(const char* path, const char* contents)
|
static void _write_file(const char* path, const char* contents)
|
||||||
@ -311,7 +311,7 @@ static void _test_database(const tf_test_options_t* options)
|
|||||||
" print('Expected but did not find: ' + JSON.stringify(expected));\n"
|
" print('Expected but did not find: ' + JSON.stringify(expected));\n"
|
||||||
" exit(4);\n"
|
" exit(4);\n"
|
||||||
" }\n"
|
" }\n"
|
||||||
" if (JSON.stringify(await databases.list('%')) != '[\"testdb\"]') {\n"
|
" if (JSON.stringify(await databases.list('%')) != '[\"core\",\"testdb\"]') {\n"
|
||||||
" exit(7);\n"
|
" exit(7);\n"
|
||||||
" }\n"
|
" }\n"
|
||||||
"}\n"
|
"}\n"
|
||||||
@ -864,9 +864,6 @@ static void _test_httpd(const tf_test_options_t* options)
|
|||||||
uv_loop_init(&loop);
|
uv_loop_init(&loop);
|
||||||
|
|
||||||
unlink("out/test_db0.sqlite");
|
unlink("out/test_db0.sqlite");
|
||||||
char command[256];
|
|
||||||
snprintf(command, sizeof(command), "%s run -b 0 --db-path=out/test_db0.sqlite" TEST_ARGS, options->exe_path);
|
|
||||||
|
|
||||||
uv_stdio_container_t stdio[] = {
|
uv_stdio_container_t stdio[] = {
|
||||||
[STDIN_FILENO] = { .flags = UV_IGNORE },
|
[STDIN_FILENO] = { .flags = UV_IGNORE },
|
||||||
[STDOUT_FILENO] = { .flags = UV_INHERIT_FD },
|
[STDOUT_FILENO] = { .flags = UV_INHERIT_FD },
|
||||||
@ -876,7 +873,7 @@ static void _test_httpd(const tf_test_options_t* options)
|
|||||||
uv_spawn(&loop, &process,
|
uv_spawn(&loop, &process,
|
||||||
&(uv_process_options_t) {
|
&(uv_process_options_t) {
|
||||||
.file = options->exe_path,
|
.file = options->exe_path,
|
||||||
.args = (char*[]) { (char*)options->exe_path, "run", "-b0", "--db-path=out/test_db0.sqlite", "--http-port=8080", "--https-port=0", NULL },
|
.args = (char*[]) { (char*)options->exe_path, "run", "--db-path=out/test_db0.sqlite", "--args=ssb_port=0,http_port=8080,https_port=0", NULL },
|
||||||
.stdio_count = sizeof(stdio) / sizeof(*stdio),
|
.stdio_count = sizeof(stdio) / sizeof(*stdio),
|
||||||
.stdio = stdio,
|
.stdio = stdio,
|
||||||
});
|
});
|
||||||
@ -960,7 +957,7 @@ static void _test_auto(const tf_test_options_t* options)
|
|||||||
unlink("out/selenium.sqlite-shm");
|
unlink("out/selenium.sqlite-shm");
|
||||||
unlink("out/selenium.sqlite-wal");
|
unlink("out/selenium.sqlite-wal");
|
||||||
|
|
||||||
char* args[] = { executable, "run", "-d", "out/selenium.sqlite", "-b", "0", "-p", "8888", NULL };
|
char* args[] = { executable, "run", "-d", "out/selenium.sqlite", "-a", "ssb_port=0,http_port=8888", NULL };
|
||||||
|
|
||||||
uv_stdio_container_t io[3] = {
|
uv_stdio_container_t io[3] = {
|
||||||
{ .flags = UV_INHERIT_FD },
|
{ .flags = UV_INHERIT_FD },
|
||||||
|
@ -314,13 +314,6 @@ static JSValue _util_parseHttpResponse(JSContext* context, JSValueConst this_val
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef enum _value_kind_t
|
|
||||||
{
|
|
||||||
k_kind_bool,
|
|
||||||
k_kind_int,
|
|
||||||
k_kind_string,
|
|
||||||
} value_kind_t;
|
|
||||||
|
|
||||||
static const char* k_kind_name[] = {
|
static const char* k_kind_name[] = {
|
||||||
[k_kind_bool] = "bool",
|
[k_kind_bool] = "bool",
|
||||||
[k_kind_int] = "int",
|
[k_kind_int] = "int",
|
||||||
@ -329,7 +322,7 @@ static const char* k_kind_name[] = {
|
|||||||
|
|
||||||
typedef struct _setting_value_t
|
typedef struct _setting_value_t
|
||||||
{
|
{
|
||||||
value_kind_t kind;
|
tf_setting_kind_t kind;
|
||||||
union
|
union
|
||||||
{
|
{
|
||||||
bool bool_value;
|
bool bool_value;
|
||||||
@ -348,6 +341,13 @@ typedef struct _setting_t
|
|||||||
|
|
||||||
static const setting_t k_settings[] = {
|
static const setting_t k_settings[] = {
|
||||||
{ .name = "code_of_conduct", .type = "textarea", .description = "Code of conduct presented at sign-in.", .default_value = { .kind = k_kind_string, .string_value = NULL } },
|
{ .name = "code_of_conduct", .type = "textarea", .description = "Code of conduct presented at sign-in.", .default_value = { .kind = k_kind_string, .string_value = NULL } },
|
||||||
|
{ .name = "ssb_port",
|
||||||
|
.type = "integer",
|
||||||
|
.description = "Port on which to listen for SSB secure handshake connections.",
|
||||||
|
.default_value = { .kind = k_kind_int, .int_value = 8008 } },
|
||||||
|
{ .name = "http_port", .type = "integer", .description = "Port on which to listen for HTTP connections.", .default_value = { .kind = k_kind_int, .int_value = 12345 } },
|
||||||
|
{ .name = "https_port", .type = "integer", .description = "Port on which to listen for secure HTTP connections.", .default_value = { .kind = k_kind_int, .int_value = 0 } },
|
||||||
|
{ .name = "out_http_port_file", .type = "hidden", .description = "File to which to write bound HTTP port.", .default_value = { .kind = k_kind_string, .string_value = NULL } },
|
||||||
{ .name = "blob_fetch_age_seconds",
|
{ .name = "blob_fetch_age_seconds",
|
||||||
.type = "integer",
|
.type = "integer",
|
||||||
.description = "Only blobs mentioned more recently than this age will be automatically fetched.",
|
.description = "Only blobs mentioned more recently than this age will be automatically fetched.",
|
||||||
@ -393,21 +393,31 @@ static const setting_t k_settings[] = {
|
|||||||
.type = "boolean",
|
.type = "boolean",
|
||||||
.description = "Whether connections are accepted from accounts that aren't in the replication range or otherwise already known.",
|
.description = "Whether connections are accepted from accounts that aren't in the replication range or otherwise already known.",
|
||||||
.default_value = { .kind = k_kind_bool, .bool_value = true } },
|
.default_value = { .kind = k_kind_bool, .bool_value = true } },
|
||||||
|
{ .name = "autologin", .type = "boolean", .description = "Whether mobile autologin is supported.", .default_value = { .kind = k_kind_bool, .bool_value = TF_IS_MOBILE != 0 } },
|
||||||
};
|
};
|
||||||
|
|
||||||
static const setting_t* _util_get_setting(const char* name, value_kind_t kind)
|
static const setting_t* _util_get_setting(const char* name, tf_setting_kind_t kind)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < tf_countof(k_settings); i++)
|
for (int i = 0; i < tf_countof(k_settings); i++)
|
||||||
{
|
{
|
||||||
if (strcmp(k_settings[i].name, name) == 0 && k_settings[i].default_value.kind == kind)
|
if (strcmp(k_settings[i].name, name) == 0 && (kind == k_kind_unknown || k_settings[i].default_value.kind == kind))
|
||||||
{
|
{
|
||||||
return &k_settings[i];
|
return &k_settings[i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tf_printf("Did not find global setting of type %s: %s.\n", k_kind_name[kind], name);
|
if (kind != k_kind_unknown)
|
||||||
|
{
|
||||||
|
tf_printf("Did not find global setting of type %s: %s.\n", k_kind_name[kind], name);
|
||||||
|
}
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tf_setting_kind_t tf_util_get_global_setting_kind(const char* name)
|
||||||
|
{
|
||||||
|
const setting_t* setting = _util_get_setting(name, k_kind_unknown);
|
||||||
|
return setting ? setting->default_value.kind : k_kind_unknown;
|
||||||
|
}
|
||||||
|
|
||||||
bool tf_util_get_default_global_setting_bool(const char* name)
|
bool tf_util_get_default_global_setting_bool(const char* name)
|
||||||
{
|
{
|
||||||
const setting_t* setting = _util_get_setting(name, k_kind_bool);
|
const setting_t* setting = _util_get_setting(name, k_kind_bool);
|
||||||
@ -446,12 +456,41 @@ static JSValue _util_defaultGlobalSettings(JSContext* context, JSValueConst this
|
|||||||
JS_SetPropertyStr(
|
JS_SetPropertyStr(
|
||||||
context, entry, "default_value", k_settings[i].default_value.string_value ? JS_NewString(context, k_settings[i].default_value.string_value) : JS_UNDEFINED);
|
context, entry, "default_value", k_settings[i].default_value.string_value ? JS_NewString(context, k_settings[i].default_value.string_value) : JS_UNDEFINED);
|
||||||
break;
|
break;
|
||||||
|
case k_kind_unknown:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
JS_SetPropertyStr(context, settings, k_settings[i].name, entry);
|
JS_SetPropertyStr(context, settings, k_settings[i].name, entry);
|
||||||
}
|
}
|
||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void tf_util_document_settings(const char* line_prefix)
|
||||||
|
{
|
||||||
|
char buffer[32];
|
||||||
|
for (int i = 0; i < tf_countof(k_settings); i++)
|
||||||
|
{
|
||||||
|
const char* default_value = NULL;
|
||||||
|
const char* quote = "";
|
||||||
|
switch (k_settings[i].default_value.kind)
|
||||||
|
{
|
||||||
|
case k_kind_bool:
|
||||||
|
default_value = k_settings[i].default_value.bool_value ? "true" : "false";
|
||||||
|
break;
|
||||||
|
case k_kind_string:
|
||||||
|
quote = "\"";
|
||||||
|
default_value = k_settings[i].default_value.string_value ? k_settings[i].default_value.string_value : "";
|
||||||
|
break;
|
||||||
|
case k_kind_int:
|
||||||
|
snprintf(buffer, sizeof(buffer), "%d", k_settings[i].default_value.int_value);
|
||||||
|
default_value = buffer;
|
||||||
|
break;
|
||||||
|
case k_kind_unknown:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
tf_printf("%s%s (default: %s%s%s): %s\n", line_prefix, k_settings[i].name, quote, default_value, quote, k_settings[i].description);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
JSValue tf_util_new_uint8_array(JSContext* context, const uint8_t* data, size_t size)
|
JSValue tf_util_new_uint8_array(JSContext* context, const uint8_t* data, size_t size)
|
||||||
{
|
{
|
||||||
JSValue array_buffer = JS_NewArrayBufferCopy(context, data, size);
|
JSValue array_buffer = JS_NewArrayBufferCopy(context, data, size);
|
||||||
|
@ -10,6 +10,17 @@
|
|||||||
|
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
** Type of a setting.
|
||||||
|
*/
|
||||||
|
typedef enum _tf_setting_kind_t
|
||||||
|
{
|
||||||
|
k_kind_unknown,
|
||||||
|
k_kind_bool,
|
||||||
|
k_kind_int,
|
||||||
|
k_kind_string,
|
||||||
|
} tf_setting_kind_t;
|
||||||
|
|
||||||
/** An event loop. */
|
/** An event loop. */
|
||||||
typedef struct uv_loop_s uv_loop_t;
|
typedef struct uv_loop_s uv_loop_t;
|
||||||
|
|
||||||
@ -194,6 +205,19 @@ int tf_util_get_default_global_setting_int(const char* name);
|
|||||||
*/
|
*/
|
||||||
const char* tf_util_get_default_global_setting_string(const char* name);
|
const char* tf_util_get_default_global_setting_string(const char* name);
|
||||||
|
|
||||||
|
/**
|
||||||
|
** Get the expected kind of a global setting.
|
||||||
|
** @param name The setting name.
|
||||||
|
** @return The setting kind or unknown if nonexistent.
|
||||||
|
*/
|
||||||
|
tf_setting_kind_t tf_util_get_global_setting_kind(const char* name);
|
||||||
|
|
||||||
|
/**
|
||||||
|
** Log documentation for the available settings.
|
||||||
|
** @param line_prefix Text to prefix each line with."
|
||||||
|
*/
|
||||||
|
void tf_util_document_settings(const char* line_prefix);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
** Check if the app is running on a mobile device.
|
** Check if the app is running on a mobile device.
|
||||||
** @return true for iPhone/Android, false otherwise.
|
** @return true for iPhone/Android, false otherwise.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user