From ed498c0aaf687a79cb141c3abbb2831e6f446131 Mon Sep 17 00:00:00 2001 From: Christoph Berg Date: Sat, 22 Nov 2025 23:42:36 +0100 Subject: [PATCH 1/2] WIP: Submit score to contestonlinescore.com This adds a new dependency on libcurl and uses it to post the current score and QSO/multiplier count to and contest online scoreboard every two minutes. Since submitting HTTP requests takes time and the current background process also handles time-critical things like sending CW, a new background thread is added that simply loops between submits the score and waiting two minutes. The score is also sent at program exit to submit the last QSOs. New logcfg parameters: ONLINESCORE, ONLINESCORE_URL, ONLINESCORE_USER, ONLINESCORE_PASS. TODO: * proper libcurl autoconf integration * the submitted multiplier information does not properly reflect the contest yet * submitted CABRILLO-OPERATORS data isn't properly displayed by https://contestonlinescore.com, need to read more specs --- .github/workflows/ci-build-ubuntu-22.yml | 2 +- .github/workflows/ci-build.yml | 2 +- configure.ac | 3 + macros/libcurl.m4 | 273 +++++++++++++++++++++++ share/logcfg.dat | 11 + src/Makefile.am | 4 +- src/background_process.c | 1 + src/main.c | 25 ++- src/onlinescore.c | 179 +++++++++++++++ src/onlinescore.h | 8 + src/parse_logcfg.c | 6 + test/data.c | 6 + tlf.1.in | 44 +++- 13 files changed, 547 insertions(+), 17 deletions(-) create mode 100644 macros/libcurl.m4 create mode 100644 src/onlinescore.c create mode 100644 src/onlinescore.h 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..422287e80 --- /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 || ! curl) + 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 From ba5e5f70dfd5bb12934d840fbbb1b3f7a3ad1c17 Mon Sep 17 00:00:00 2001 From: Christoph Berg Date: Mon, 8 Dec 2025 17:23:43 +0100 Subject: [PATCH 2/2] curl is only null if onlinescore is also null --- src/onlinescore.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/onlinescore.c b/src/onlinescore.c index 422287e80..167374c51 100644 --- a/src/onlinescore.c +++ b/src/onlinescore.c @@ -89,7 +89,7 @@ void onlinescore_init() { } void send_onlinescore() { - if (! onlinescore || ! curl) + if (! onlinescore) return; /* build the XML document */