diff --git a/.github/workflows/ci-build-ubuntu-22.yml b/.github/workflows/ci-build-ubuntu-22.yml index d54f29bc3..a18e7baf8 100644 --- a/.github/workflows/ci-build-ubuntu-22.yml +++ b/.github/workflows/ci-build-ubuntu-22.yml @@ -26,7 +26,7 @@ jobs: sudo dpkg-reconfigure man-db # sudo apt-get -qq update - sudo apt-get install -y libhamlib-dev libxmlrpc-core-c3-dev libglib2.0-dev libcmocka-dev python3-pexpect python3-dev astyle + sudo apt-get install -y libcurl4-openssl-dev libhamlib-dev libxmlrpc-core-c3-dev libglib2.0-dev libcmocka-dev python3-pexpect python3-dev astyle - name: Set up datadir run: mkdir datadir && ln -s $PWD/share datadir/tlf - name: Check source formatting diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 07991f515..0e0b3630d 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -26,7 +26,7 @@ jobs: sudo dpkg-reconfigure man-db # sudo apt-get -qq update - sudo apt-get install -y libhamlib-dev libxmlrpc-core-c3-dev libglib2.0-dev libcmocka-dev python3-pexpect python3-dev astyle + sudo apt-get install -y libcurl4-openssl-dev libhamlib-dev libxmlrpc-core-c3-dev libglib2.0-dev libcmocka-dev python3-pexpect python3-dev astyle - name: Set up datadir run: mkdir datadir && ln -s $PWD/share datadir/tlf - name: Check source formatting diff --git a/configure.ac b/configure.ac index 54c7301b1..c8182165e 100644 --- a/configure.ac +++ b/configure.ac @@ -124,6 +124,8 @@ CPPFLAGS=$tlf_saved_CPPFLAGS CFLAGS=$tlf_saved_CFLAGS +LIBCURL_CHECK_CONFIG([], [7], [wantlibcurl=true], [wantlibcurl=false]) + dnl Check if we want to use xmlrpc to read carrier from Fldigi AC_MSG_CHECKING([whether to build Fldigi XML RPC support]) AC_ARG_ENABLE([fldigi-xmlrpc], @@ -245,6 +247,7 @@ echo \ Package features: + With CURL $wantlibcurl With XML RPC $wantfldigixmlrpc With Python plugin $wantpythonplugin diff --git a/macros/libcurl.m4 b/macros/libcurl.m4 new file mode 100644 index 000000000..973493f03 --- /dev/null +++ b/macros/libcurl.m4 @@ -0,0 +1,273 @@ +#*************************************************************************** +# _ _ ____ _ +# Project ___| | | | _ \| | +# / __| | | | |_) | | +# | (__| |_| | _ <| |___ +# \___|\___/|_| \_\_____| +# +# Copyright (C) David Shaw +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at https://curl.se/docs/copyright.html. +# +# You may opt to use, copy, modify, merge, publish, distribute and/or sell +# copies of the Software, and permit persons to whom the Software is +# furnished to do so, under the terms of the COPYING file. +# +# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY +# KIND, either express or implied. +# +# SPDX-License-Identifier: curl +# +########################################################################### +# LIBCURL_CHECK_CONFIG([DEFAULT-ACTION], [MINIMUM-VERSION], +# [ACTION-IF-YES], [ACTION-IF-NO]) +# ---------------------------------------------------------- +# David Shaw May-09-2006 +# +# Checks for libcurl. DEFAULT-ACTION is the string yes or no to +# specify whether to default to --with-libcurl or --without-libcurl. +# If not supplied, DEFAULT-ACTION is yes. MINIMUM-VERSION is the +# minimum version of libcurl to accept. Pass the version as a regular +# version number like 7.10.1. If not supplied, any version is +# accepted. ACTION-IF-YES is a list of shell commands to run if +# libcurl was successfully found and passed the various tests. +# ACTION-IF-NO is a list of shell commands that are run otherwise. +# Note that using --without-libcurl does run ACTION-IF-NO. +# +# This macro #defines HAVE_LIBCURL if a working libcurl setup is +# found, and sets @LIBCURL@ and @LIBCURL_CPPFLAGS@ to the necessary +# values. Other useful defines are LIBCURL_FEATURE_xxx where xxx are +# the various features supported by libcurl, and LIBCURL_PROTOCOL_yyy +# where yyy are the various protocols supported by libcurl. Both xxx +# and yyy are capitalized. See the list of AH_TEMPLATE macros at the top +# of the macro for the complete list of possible defines. Shell +# variables $libcurl_feature_xxx and $libcurl_protocol_yyy are also +# defined to 'yes' for those features and protocols that were found. +# Note that xxx and yyy keep the same capitalization as in the +# curl-config list (e.g. it's "HTTP" and not "http"). +# +# Users may override the detected values by doing something like: +# LIBCURL="-lcurl" LIBCURL_CPPFLAGS="-I/usr/myinclude" ./configure +# +# For the sake of sanity, this macro assumes that any libcurl that is found is +# after version 7.7.2, the first version that included the curl-config script. +# Note that it is important for people packaging binary versions of libcurl to +# include this script! Without curl-config, we can only guess what protocols +# are available, or use curl_version_info to figure it out at runtime. + +AC_DEFUN([LIBCURL_CHECK_CONFIG], +[ + AH_TEMPLATE([LIBCURL_FEATURE_SSL],[Defined if libcurl supports SSL]) + AH_TEMPLATE([LIBCURL_FEATURE_KRB4],[Defined if libcurl supports KRB4]) + AH_TEMPLATE([LIBCURL_FEATURE_IPV6],[Defined if libcurl supports IPv6]) + AH_TEMPLATE([LIBCURL_FEATURE_LIBZ],[Defined if libcurl supports libz]) + AH_TEMPLATE([LIBCURL_FEATURE_ASYNCHDNS],[Defined if libcurl supports AsynchDNS]) + AH_TEMPLATE([LIBCURL_FEATURE_IDN],[Defined if libcurl supports IDN]) + AH_TEMPLATE([LIBCURL_FEATURE_SSPI],[Defined if libcurl supports SSPI]) + AH_TEMPLATE([LIBCURL_FEATURE_NTLM],[Defined if libcurl supports NTLM]) + + AH_TEMPLATE([LIBCURL_PROTOCOL_HTTP],[Defined if libcurl supports HTTP]) + AH_TEMPLATE([LIBCURL_PROTOCOL_HTTPS],[Defined if libcurl supports HTTPS]) + AH_TEMPLATE([LIBCURL_PROTOCOL_FTP],[Defined if libcurl supports FTP]) + AH_TEMPLATE([LIBCURL_PROTOCOL_FTPS],[Defined if libcurl supports FTPS]) + AH_TEMPLATE([LIBCURL_PROTOCOL_FILE],[Defined if libcurl supports FILE]) + AH_TEMPLATE([LIBCURL_PROTOCOL_TELNET],[Defined if libcurl supports TELNET]) + AH_TEMPLATE([LIBCURL_PROTOCOL_LDAP],[Defined if libcurl supports LDAP]) + AH_TEMPLATE([LIBCURL_PROTOCOL_DICT],[Defined if libcurl supports DICT]) + AH_TEMPLATE([LIBCURL_PROTOCOL_TFTP],[Defined if libcurl supports TFTP]) + AH_TEMPLATE([LIBCURL_PROTOCOL_RTSP],[Defined if libcurl supports RTSP]) + AH_TEMPLATE([LIBCURL_PROTOCOL_POP3],[Defined if libcurl supports POP3]) + AH_TEMPLATE([LIBCURL_PROTOCOL_IMAP],[Defined if libcurl supports IMAP]) + AH_TEMPLATE([LIBCURL_PROTOCOL_SMTP],[Defined if libcurl supports SMTP]) + + AC_ARG_WITH(libcurl, + AS_HELP_STRING([--with-libcurl=PREFIX],[look for the curl library in PREFIX/lib and headers in PREFIX/include]), + [_libcurl_with=$withval],[_libcurl_with=ifelse([$1],,[yes],[$1])]) + + if test "$_libcurl_with" != "no"; then + + AC_PROG_AWK + + _libcurl_version_parse="eval $AWK '{split(\$NF,A,\".\"); X=256*256*A[[1]]+256*A[[2]]+A[[3]]; print X;}'" + + _libcurl_try_link=yes + + if test -d "$_libcurl_with"; then + LIBCURL_CPPFLAGS="-I$withval/include" + _libcurl_ldflags="-L$withval/lib" + AC_PATH_PROG([_libcurl_config],[curl-config],[],["$withval/bin"]) + else + AC_PATH_PROG([_libcurl_config],[curl-config],[],[$PATH]) + fi + + if test x$_libcurl_config != "x"; then + AC_CACHE_CHECK([for the version of libcurl], + [libcurl_cv_lib_curl_version], + [libcurl_cv_lib_curl_version=`$_libcurl_config --version | $AWK '{print $[]2}'`]) + + _libcurl_version=`echo $libcurl_cv_lib_curl_version | $_libcurl_version_parse` + _libcurl_wanted=`echo ifelse([$2],,[0],[$2]) | $_libcurl_version_parse` + + if test $_libcurl_wanted -gt 0; then + AC_CACHE_CHECK([for libcurl >= version $2], + [libcurl_cv_lib_version_ok], + [ + if test $_libcurl_version -ge $_libcurl_wanted; then + libcurl_cv_lib_version_ok=yes + else + libcurl_cv_lib_version_ok=no + fi + ]) + fi + + if test $_libcurl_wanted -eq 0 || test x$libcurl_cv_lib_version_ok = xyes; then + if test x"$LIBCURL_CPPFLAGS" = "x"; then + LIBCURL_CPPFLAGS=`$_libcurl_config --cflags` + fi + if test x"$LIBCURL" = "x"; then + LIBCURL=`$_libcurl_config --libs` + + # This is so silly, but Apple actually has a bug in their + # curl-config script. Fixed in Tiger, but there are still + # lots of Panther installs around. + case "${host}" in + powerpc-apple-darwin7*) + LIBCURL=`echo $LIBCURL | sed -e 's|-arch i386||g'` + ;; + esac + fi + + # All curl-config scripts support --feature + _libcurl_features=`$_libcurl_config --feature` + + # Is it modern enough to have --protocols? (7.12.4) + if test $_libcurl_version -ge 461828; then + _libcurl_protocols=`$_libcurl_config --protocols` + fi + else + _libcurl_try_link=no + fi + + unset _libcurl_wanted + fi + + if test $_libcurl_try_link = yes; then + + # we did not find curl-config, so let's see if the user-supplied + # link line (or failing that, "-lcurl") is enough. + LIBCURL=${LIBCURL-"$_libcurl_ldflags -lcurl"} + + AC_CACHE_CHECK([whether libcurl is usable], + [libcurl_cv_lib_curl_usable], + [ + _libcurl_save_cppflags=$CPPFLAGS + CPPFLAGS="$LIBCURL_CPPFLAGS $CPPFLAGS" + _libcurl_save_libs=$LIBS + LIBS="$LIBCURL $LIBS" + + AC_LINK_IFELSE([AC_LANG_PROGRAM([[#include ]],[[ + /* Try to use a few common options to force a failure if we are + missing symbols or cannot link. */ + int x; + curl_easy_setopt(NULL,CURLOPT_URL,NULL); + x=CURL_ERROR_SIZE; + x=CURLOPT_WRITEFUNCTION; + x=CURLOPT_WRITEDATA; + x=CURLOPT_ERRORBUFFER; + x=CURLOPT_STDERR; + x=CURLOPT_VERBOSE; + if(x) {;} + ]])],libcurl_cv_lib_curl_usable=yes,libcurl_cv_lib_curl_usable=no) + + CPPFLAGS=$_libcurl_save_cppflags + LIBS=$_libcurl_save_libs + unset _libcurl_save_cppflags + unset _libcurl_save_libs + ]) + + if test $libcurl_cv_lib_curl_usable = yes; then + + # Does curl_free() exist in this version of libcurl? + # If not, fake it with free() + + _libcurl_save_cppflags=$CPPFLAGS + CPPFLAGS="$CPPFLAGS $LIBCURL_CPPFLAGS" + _libcurl_save_libs=$LIBS + LIBS="$LIBS $LIBCURL" + + AC_CHECK_DECL([curl_free],[], + [AC_DEFINE([curl_free],[free], + [Define curl_free() as free() if our version of curl lacks curl_free.])], + [[#include ]]) + + CPPFLAGS=$_libcurl_save_cppflags + LIBS=$_libcurl_save_libs + unset _libcurl_save_cppflags + unset _libcurl_save_libs + + AC_DEFINE(HAVE_LIBCURL,1, + [Define to 1 if you have a functional curl library.]) + AC_SUBST(LIBCURL_CPPFLAGS) + AC_SUBST(LIBCURL) + + for _libcurl_feature in $_libcurl_features; do + AC_DEFINE_UNQUOTED(AS_TR_CPP(libcurl_feature_$_libcurl_feature),[1]) + eval AS_TR_SH(libcurl_feature_$_libcurl_feature)=yes + done + + if test "x$_libcurl_protocols" = "x"; then + + # We do not have --protocols, so just assume that all + # protocols are available + _libcurl_protocols="HTTP FTP FILE TELNET LDAP DICT TFTP" + + if test x$libcurl_feature_SSL = xyes; then + _libcurl_protocols="$_libcurl_protocols HTTPS" + + # FTPS was not standards-compliant until version + # 7.11.0 (0x070b00 == 461568) + if test $_libcurl_version -ge 461568; then + _libcurl_protocols="$_libcurl_protocols FTPS" + fi + fi + + # RTSP, IMAP, POP3 and SMTP were added in + # 7.20.0 (0x071400 == 463872) + if test $_libcurl_version -ge 463872; then + _libcurl_protocols="$_libcurl_protocols RTSP IMAP POP3 SMTP" + fi + fi + + for _libcurl_protocol in $_libcurl_protocols; do + AC_DEFINE_UNQUOTED(AS_TR_CPP(libcurl_protocol_$_libcurl_protocol),[1]) + eval AS_TR_SH(libcurl_protocol_$_libcurl_protocol)=yes + done + else + unset LIBCURL + unset LIBCURL_CPPFLAGS + fi + fi + + unset _libcurl_try_link + unset _libcurl_version_parse + unset _libcurl_config + unset _libcurl_feature + unset _libcurl_features + unset _libcurl_protocol + unset _libcurl_protocols + unset _libcurl_version + unset _libcurl_ldflags + fi + + if test x$_libcurl_with = xno || test x$libcurl_cv_lib_curl_usable != xyes; then + # This is the IF-NO path + ifelse([$4],,:,[$4]) + else + # This is the IF-YES path + ifelse([$3],,:,[$3]) + fi + + unset _libcurl_with +]) diff --git a/share/logcfg.dat b/share/logcfg.dat index 5bf7ea213..dea1df832 100644 --- a/share/logcfg.dat +++ b/share/logcfg.dat @@ -252,6 +252,17 @@ CABRILLO-SOAPBOX(3)= - # ################################# # # +# Online Score Submission # +# # +################################# +# +ONLINESCORE +#ONLINESCORE_URL=https://contestonlinescore.com/post/ +#ONLINESCORE_USER= +#ONLINESCORE_PASS= +# +################################# +# # # CONDX (info for muf calc.) # # # ################################# diff --git a/src/Makefile.am b/src/Makefile.am index b8e76e505..b58a3abfa 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -23,6 +23,7 @@ tlf_SOURCES = \ logit.c logview.c \ main.c makelogline.c messagechange.c muf.c \ nicebox.c note.c netkeyer.c\ + onlinescore.c \ paccdx.c parse_logcfg.c plugin.c printcall.c \ qrb.c qsonr_to_str.c qtc_log.c qtcwin.c qtcutil.c readcabrillo.c \ readcalls.c readqtccalls.c readctydata.c recall_exchange.c rules.c \ @@ -38,7 +39,7 @@ tlf_SOURCES = \ tlf_LDADD = @LIBM_LIB@ @PTHREAD_LIBS@ @GLIB_LIBS@ @PANEL_LIBS@ @CURSES_LIBS@ \ @HAMLIB_LIBS@ @LIBXMLRPC_LIB@ @LIBXMLRPC_CLIENT_LIB@ \ - @LIBXMLRPC_UTIL_LIB@ @PYTHON_LIBS@ + @LIBXMLRPC_UTIL_LIB@ @PYTHON_LIBS@ -lcurl noinst_HEADERS = \ addcall.h addmult.h addpfx.h addspot.h audio.h autocq.h \ @@ -60,6 +61,7 @@ noinst_HEADERS = \ log_to_disk.h logit.h logview.h \ makelogline.h math_utils.h messagechange.h muf.h \ nicebox.h note.h netkeyer.h\ + onlinescore.h \ paccdx.h parse_logcfg.h printcall.h \ paccdx.h parse_logcfg.h plugin.h printcall.h \ qrb.h qsonr_to_str.h qtc_log.h qtcvars.h qtcwin.h qtcutil.h \ diff --git a/src/background_process.c b/src/background_process.c index 468c9e8c0..fd94e9ade 100644 --- a/src/background_process.c +++ b/src/background_process.c @@ -32,6 +32,7 @@ #include "gettxinfo.h" #include "lancode.h" #include "log_to_disk.h" +#include "onlinescore.h" #include "qsonr_to_str.h" #include "qtc_log.h" #include "qtcutil.h" diff --git a/src/main.c b/src/main.c index a53f457f0..88ee7b55f 100644 --- a/src/main.c +++ b/src/main.c @@ -49,6 +49,7 @@ #include "lancode.h" #include "logit.h" #include "netkeyer.h" +#include "onlinescore.h" #include "parse_logcfg.h" #include "plugin.h" #include "qtcvars.h" // Includes globalvars.h @@ -438,6 +439,7 @@ int bandweight_points[NBANDS] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0}; int bandweight_multis[NBANDS] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0}; pthread_t background_thread; +pthread_t onlinescore_thread; static struct termios oldt, newt; /*-------------------------parse program options---------------------------*/ @@ -1044,11 +1046,20 @@ static void mark_GPL_seen() { * logit() or background_process() */ static void tlf_cleanup() { - if (pthread_self() != background_thread) { + if (pthread_self() != background_thread + && pthread_self() != onlinescore_thread) { terminate_background_process(); pthread_join(background_thread, NULL); + + // interrupt the main onlinescore loop (also cancels any curl operation + // that might be stuck in a timeout) + pthread_cancel(onlinescore_thread); + pthread_join(onlinescore_thread, NULL); } + // send final online score before quitting + send_onlinescore(); + cleanup_telnet(); if (trxmode == CWMODE && cwkeyer == NET_KEYER) @@ -1179,6 +1190,7 @@ int main(int argc, char *argv[]) { fldigi_init(); lan_init(); keyer_init(); + onlinescore_init(); show_station_info(); @@ -1210,10 +1222,17 @@ int main(int argc, char *argv[]) { } atexit(tlf_cleanup); /* register cleanup function */ - /* Create the background thread */ + /* Create background threads */ ret = pthread_create(&background_thread, NULL, background_process, NULL); if (ret) { - perror("pthread_create: backgound_process"); + perror("pthread_create: background_process"); + endwin(); + exit(EXIT_FAILURE); + } + + ret = pthread_create(&onlinescore_thread, NULL, onlinescore_process, NULL); + if (ret) { + perror("pthread_create: onlinescore_process"); endwin(); exit(EXIT_FAILURE); } diff --git a/src/onlinescore.c b/src/onlinescore.c new file mode 100644 index 000000000..167374c51 --- /dev/null +++ b/src/onlinescore.c @@ -0,0 +1,179 @@ +#include +#include +#include + +#include +#include // get_total_score + +// configurable parameters +bool onlinescore = false; +char *onlinescore_url = NULL; +#define DEFAULT_ONLINESCORE_URL "https://contestonlinescore.com/post/" +char *onlinescore_user = NULL; +char *onlinescore_pass = NULL; + +// Real Time Contest specification for XML postings: +// The logger should send a posting every 2 minutes even if there are no changes in a +// contest log. No other rates can be accessible in logger settings. +const int onlinescore_interval = 120; + +// static data initialized by onlinescore_init +CURL *curl = NULL; +cbr_field_t *cbr_contest; +cbr_field_t *cbr_operators; +cbr_field_t *cbr_power; +cbr_field_t *cbr_assisted; +cbr_field_t *cbr_transmitter; +cbr_field_t *cbr_ops; +cbr_field_t *cbr_bands; +cbr_field_t *cbr_mode; +cbr_field_t *cbr_overlay; + +static size_t +receive_data(char *contents, size_t size, size_t nmemb, void *userp) { + // ignore data received for now + return size * nmemb; +} + +void onlinescore_init() { + if (! onlinescore) + return; + + if (! iscontest) { + onlinescore = false; + return; + } + + if (! onlinescore_url) { + onlinescore_url = strdup(DEFAULT_ONLINESCORE_URL); + } + + curl = curl_easy_init(); + if (! curl) { + // something went wrong with initializing the library, disable online score submissions + onlinescore = false; + return; + } + + curl_easy_setopt(curl, CURLOPT_URL, onlinescore_url); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, receive_data); + //curl_easy_setopt(hnd, CURLOPT_WRITEDATA, &chunk); // ignore data received for now + + /* headers */ + struct curl_slist *slist1 = NULL; + slist1 = curl_slist_append(slist1, "Content-Type: application/xml"); + slist1 = curl_slist_append(slist1, "Accept: application/json"); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, slist1); + // we'll reuse the headers for each invocation, don't free list + + if (onlinescore_user) { + curl_easy_setopt(curl, CURLOPT_HTTPAUTH, (long)CURLAUTH_BASIC); + curl_easy_setopt(curl, CURLOPT_USERNAME, onlinescore_user); + if (onlinescore_pass) { + curl_easy_setopt(curl, CURLOPT_PASSWORD, onlinescore_pass); + } else { + curl_easy_setopt(curl, CURLOPT_NETRC, (long)true); + } + } + + // cache some cabrillo field lookups + cbr_contest = find_cabrillo_field("CONTEST"); + cbr_operators = find_cabrillo_field("OPERATORS"); + cbr_power = find_cabrillo_field("CATEGORY-POWER"); + cbr_assisted = find_cabrillo_field("CATEGORY-ASSISTED"); + cbr_transmitter = find_cabrillo_field("CATEGORY-TRANSMITTER"); + cbr_ops = find_cabrillo_field("CATEGORY-OPERATOR"); + cbr_bands = find_cabrillo_field("CATEGORY-BAND"); + cbr_mode = find_cabrillo_field("CATEGORY-MODE"); + cbr_overlay = find_cabrillo_field("CATEGORY-OVERLAY"); +} + +void send_onlinescore() { + if (! onlinescore) + return; + + /* build the XML document */ + GString *data = g_string_new(NULL); + + g_string_append(data, "\n"); + g_string_append(data, "\n"); + g_string_append_printf(data, "%s\n", + cbr_contest->value ? cbr_contest->value : whichcontest); + g_string_append(data, "TLF\n"); + g_string_append(data, "" VERSION "\n"); + g_string_append_printf(data, "%s\n", my.call); + if (cbr_operators->value) g_string_append_printf(data, "%s\n", + cbr_operators->value); + // TODO: + + g_string_append(data, "value) g_string_append_printf(data, " power=\"%s\"", + cbr_power->value); + if (cbr_assisted->value) g_string_append_printf(data, " assisted=\"%s\"", + cbr_assisted->value); + if (cbr_transmitter->value) g_string_append_printf(data, " transmitter=\"%s\"", + cbr_transmitter->value); + if (cbr_ops->value) g_string_append_printf(data, " ops=\"%s\"", cbr_ops->value); + g_string_append_printf(data, " bands=\"%s\"", + cbr_bands->value ? cbr_bands->value : "ALL"); + g_string_append_printf(data, " mode=\"%s\"", + cbr_mode->value ? cbr_mode->value : "MIXED"); + g_string_append_printf(data, " overlay=\"%s\">\n", + cbr_overlay->value ? cbr_overlay->value : "N/A"); + + g_string_append(data, "\n"); + int qsos = 0; + int country_mults = 0; + int zone_mults = 0; + for (int i = 0; i < NBANDS; i++) { + GString *band_name = g_string_new(band[i]); + g_strchug(band_name->str); // remove leading whitespace + if (qsos_per_band[i] > 0) { + qsos += qsos_per_band[i]; + // TODO: include mode="" in these lines?? + g_string_append_printf(data, "%d\n", band_name->str, + qsos_per_band[i]); + } + if (countryscore[i] > 0) { + country_mults += countryscore[i]; + g_string_append_printf(data, "%d\n", + band_name->str, countryscore[i]); + } + if (zonescore[i] > 0) { + zone_mults += zonescore[i]; + g_string_append_printf(data, "%d\n", + band_name->str, zonescore[i]); + } + g_free(band_name); + } + g_string_append_printf(data, "%d\n", qsos); + g_string_append_printf(data, + "%d\n", country_mults); + g_string_append_printf(data, "%d\n", + zone_mults); + g_string_append(data, "\n"); + g_string_append_printf(data, "%d\n", get_total_score()); + + time_t now = time(NULL); + struct tm n; + gmtime_r(&now, &n); + g_string_append_printf(data, + "%04d-%02d-%02d %02d:%02d:%02d\n", + n.tm_year + 1900, n.tm_mon + 1, n.tm_mday, n.tm_hour, n.tm_min, n.tm_sec); + + g_string_append_printf(data, "\n"); + + /* send the request */ + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data->str); + curl_easy_perform(curl); + g_free(data); +} + +void *onlinescore_process(void *ptr) { + while (1) { + send_onlinescore(); + sleep(onlinescore_interval); + } + + // never returns, terminated by pthread_cancel() +} diff --git a/src/onlinescore.h b/src/onlinescore.h new file mode 100644 index 000000000..b78de189c --- /dev/null +++ b/src/onlinescore.h @@ -0,0 +1,8 @@ +extern bool onlinescore; +extern char *onlinescore_url; +extern char *onlinescore_user; +extern char *onlinescore_pass; + +void onlinescore_init(); +void send_onlinescore(); +void *onlinescore_process(void *ptr); diff --git a/src/parse_logcfg.c b/src/parse_logcfg.c index 1fbf5fed8..0f15f1d06 100644 --- a/src/parse_logcfg.c +++ b/src/parse_logcfg.c @@ -44,6 +44,7 @@ #include "ignore_unused.h" #include "lancode.h" #include "utils.h" +#include "onlinescore.h" #include "parse_logcfg.h" #include "qtcvars.h" // Includes globalvars.h #include "setcontest.h" @@ -1434,6 +1435,11 @@ static config_t logcfg_configs[] = { {"ROTPORT", CFG_STRING(rotportname)}, {"CLUSTERLOGIN", CFG_STRING_STATIC_NOCHOMP(clusterlogin, 80)}, + {"ONLINESCORE", CFG_BOOL(onlinescore)}, + {"ONLINESCORE_URL", CFG_STRING(onlinescore_url)}, + {"ONLINESCORE_USER", CFG_STRING(onlinescore_user)}, + {"ONLINESCORE_PASS", CFG_STRING(onlinescore_pass)}, + {"CALL", NEED_PARAM, cfg_call}, {"(CONTEST|RULES)", NEED_PARAM, cfg_contest}, {"TELNETPORT", NEED_PARAM, cfg_telnetport}, diff --git a/test/data.c b/test/data.c index cf37eb8c3..cb2177945 100644 --- a/test/data.c +++ b/test/data.c @@ -219,6 +219,12 @@ char qtc_phsend_message[14][80] = { "", "", "", "", "", "" }; // voice keyer file names when send QTCs bool qtcrec_record = false; + +bool onlinescore; +char *onlinescore_url; +char *onlinescore_user; +char *onlinescore_pass; + char qtcrec_record_command[2][50] = {"rec -q 8000", "-q &"}; char qtcrec_record_command_shutdown[50] = "pkill -SIGINT -n rec"; char qtc_cap_calls[40] = ""; diff --git a/tlf.1.in b/tlf.1.in index dd144d87b..cfa4aa5e5 100644 --- a/tlf.1.in +++ b/tlf.1.in @@ -35,7 +35,7 @@ . . .\" Update the date when this page is committed. -.TH TLF 1 "@PACKAGE_NAME@ @VERSION@, 2025-01-23" TLF "Ham radio" +.TH TLF 1 "@PACKAGE_NAME@ @VERSION@, 2025-12-02" TLF "Ham radio" . . .SH NAME @@ -90,7 +90,7 @@ library, and with a via telnet or packet radio. . @PACKAGE_NAME@ can project DX cluster data into the excellent Xplanet program, -written by Hari Nair. +written by Hari Nair, and post to contest online scoreboards. . .P Contest operation mimics the popular @@ -3447,25 +3447,25 @@ just by removing the prefix and using \(rq:\(lq as value separator. . .TP -.B CABRILLO\-CONTEST +.B CABRILLO\-CONTEST (*) .TQ -.B CABRILLO\-CATEGORY\-ASSISTED +.B CABRILLO\-CATEGORY\-ASSISTED (*) .TQ -.B CABRILLO\-CATEGORY\-BAND +.B CABRILLO\-CATEGORY\-BAND (*) .TQ -.B CABRILLO\-CATEGORY\-MODE +.B CABRILLO\-CATEGORY\-MODE (*) .TQ -.B CABRILLO\-CATEGORY\-OPERATOR +.B CABRILLO\-CATEGORY\-OPERATOR (*) .TQ -.B CABRILLO\-CATEGORY\-POWER +.B CABRILLO\-CATEGORY\-POWER (*) .TQ .B CABRILLO\-CATEGORY\-STATION .TQ .B CABRILLO\-CATEGORY\-TIME .TQ -.B CABRILLO\-CATEGORY\-TRANSMITTER +.B CABRILLO\-CATEGORY\-TRANSMITTER (*) .TQ -.B CABRILLO\-CATEGORY\-OVERLAY +.B CABRILLO\-CATEGORY\-OVERLAY (*) .TQ .B CABRILLO\-CERTIFICATE .TQ @@ -3499,7 +3499,7 @@ It can be disabled but not set.) .TQ .B CABRILLO\-ADDRESS\-COUNTRY .TQ -.B CABRILLO\-OPERATORS +.B CABRILLO\-OPERATORS (*) .TQ .B CABRILLO\-OFFTIME .TQ @@ -3510,6 +3510,28 @@ It can be disabled but not set.) .TQ .B CABRILLO\-SOAPBOX(3) . +. +.SS Contest Online Scoreboard Submission +. +@PACKAGE_NAME@ can post your current score to online scoreboards, also called live scoring. +QSO and multiplier counts are submitted \fBevery two minutes\fR as well as at program exit. +The category data submitted is taken from the Cabrillo configuration, specifically from the fields marked \fB(*)\fR in the \fBHeader Keywords\fR list. +. +.TP +.B ONLINESCORE +Enable online score submission. +.TQ +\fBONLINESCORE_URL\fR=\fIurl\fR +Post to this URL. +Default is \fBhttps://contestonlinescore.com/post/\fR. +.TQ +\fBONLINESCORE_USER\fR=\fIuser\fR +Optionally set a user name for the online score server. +.TQ +\fBONLINESCORE_PASS\fR=\fIpassword\fR +Optionally set a password for the online score server. +If this is unset, but \fBONLINESCORE_USER\fR is set, get the password from the \fB~/.netrc\fR file. +. .SH PYTHON PLUGIN . @PACKAGE_NAME@ uses Python plugins to customize or extend functions beyond