Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions contrib/poll_notify.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env bash

# (C) 2025 The Gridcoin Developers
# Distributed under the MIT/X11 software license, see the accompanying
# file COPYING or https://opensource.org/licenses/mit-license.php.


# A basic script for poll notification with Gridcoin, normally to
# be used with the -pollnotify=<cmd> Gridcoin startup or config file
# argument.
#
# If using this script with -pollnotify, the command string should
# appear like the following:
#
# -pollnotify="/path/to/script/poll_notify.sh <gridcoin daemon executable> <gridcoin data directory> <network> <sender email address> <recipient email address> %s1 %s2"
#
# The script requires the command-line version of gridcoin, gridcoinresearchd,
# jq (the JSON parser), and properly configured mailx.

export LC_ALL=C

# Check if the correct number of arguments is provided
if [ "$#" -ne 7 ]; then
echo "Usage: $0 <gridcoin daemon executable> <gridcoin data directory> <network> <sender email address> <recipient email_address> <poll_txid> <notification_type>"
echo "The gridcoin executable should include the path. Specify the data directory for the node. For network, specify mainnet or testnet."
exit 1
fi

# Assign input arguments to variables
GRIDCOIN_EXECUTABLE="$1"
GRIDCOIN_DATA_DIRECTORY="$2"

GRIDCOIN_NETWORK=""
if [[ "$3" == "testnet" ]]; then
GRIDCOIN_NETWORK="-testnet"
fi

SENDER_EMAIL_ADDRESS="$4"
RECIPIENT_EMAIL_ADDRESS="$5"
POLL_TXID="$6"
NOTIFICATION_TYPE="$7"

# Fetch all poll details using gridcoinresearchd
ALL_POLLS=$("$GRIDCOIN_EXECUTABLE" -datadir="$GRIDCOIN_DATA_DIRECTORY" "$GRIDCOIN_NETWORK" listpolls true 2>/dev/null)

# Check if the poll list was retrieved successfully
if [ -z "$ALL_POLLS" ]; then
echo "Failed to retrieve poll list."
exit 2
fi

# Extract poll details for the specific TXID using jq
POLL_DETAILS=$(echo "$ALL_POLLS" | jq --arg txid "$POLL_TXID" '.[] | select(.id == $txid)')

# Check if the poll details for the given TXID were found
if [ -z "$POLL_DETAILS" ]; then
echo "Poll with TXID $POLL_TXID not found."
exit 3
fi

# Extract specific fields from the poll details
POLL_TITLE=$(echo "$POLL_DETAILS" | jq -r '.title')
POLL_QUESTION=$(echo "$POLL_DETAILS" | jq -r '.question')
POLL_URL=$(echo "$POLL_DETAILS" | jq -r '.url')
POLL_TYPE=$(echo "$POLL_DETAILS" | jq -r '.poll_type')
POLL_EXPIRATION=$(echo "$POLL_DETAILS" | jq -r '.expiration')
POLL_CHOICES=$(echo "$POLL_DETAILS" | jq -r '.choices | map(.label) | join(", ")')
POLL_ADDITIONAL_FIELDS=$(echo "$POLL_DETAILS" | jq -r '.additional_fields | map("\(.name): \(.value)") | join("\n")')

# Subject and body for the email
EMAIL_SUBJECT="Poll \"$POLL_TITLE\" $NOTIFICATION_TYPE"
EMAIL_BODY="Poll Notification:\\n
Title: $POLL_TITLE\n
Type: $POLL_TYPE\n
Question: $POLL_QUESTION\n
Expiration: $POLL_EXPIRATION\n
Choices: $POLL_CHOICES\n
Additional Details:\n$POLL_ADDITIONAL_FIELDS\n
Poll URL: $POLL_URL\n
Notification Type: $NOTIFICATION_TYPE\n
Poll TXID: $POLL_TXID\n
This is an automated notification sent by Gridcoin."

# Send the email using mailx and check if the email was sent successfully
if echo -e "$EMAIL_BODY" | mailx -r "$SENDER_EMAIL_ADDRESS" -s "$EMAIL_SUBJECT" "$RECIPIENT_EMAIL_ADDRESS"; then
echo "Notification email sent to $RECIPIENT_EMAIL_ADDRESS successfully."
else
echo "Failed to send email to $RECIPIENT_EMAIL_ADDRESS."
exit 4
fi
35 changes: 35 additions & 0 deletions src/gridcoin/gridcoin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,15 @@ void ScheduleRegistriesPassivation(CScheduler& scheduler)

scheduler.scheduleEvery(RunDBPassivation, std::chrono::minutes{5});
}

void SchedulePollNotifications(CScheduler& scheduler)
{
// Run poll notifications every 5 minutes. This is a very thin call most of the time.
// Please see the NotifyPoll function and notify_poll.

scheduler.scheduleEvery(NotifyPoll, std::chrono::minutes{5});
}

} // Anonymous namespace

// -----------------------------------------------------------------------------
Expand Down Expand Up @@ -552,6 +561,10 @@ void GRC::ScheduleBackgroundJobs(CScheduler& scheduler)
ScheduleBackups(scheduler);
ScheduleUpdateChecks(scheduler);
ScheduleRegistriesPassivation(scheduler);

#if HAVE_SYSTEM
SchedulePollNotifications(scheduler);
#endif
}

bool GRC::CleanConfig() {
Expand Down Expand Up @@ -615,3 +628,25 @@ void GRC::RunDBPassivation()
registry.PassivateDB();
}
}

void GRC::NotifyPoll()
{
PollRegistry& poll_registry = GetPollRegistry();

LOCK2(cs_main, poll_registry.cs_poll_registry);

// Get the expiration warning time from the configuration. Default is 7 days in hours.
int64_t expiration_warning_time = gArgs.GetArg("-pollexpirewarningtime", 7 * 24);

for (const auto& poll : poll_registry.Polls()) {
if (!poll->Ref().Expired(GetAdjustedTime())
&& !poll->Ref().IsExpiringWarningNotified()
&& poll->Ref().Expiration() - GetAdjustedTime() < expiration_warning_time * 3600) {

// Note this will set m_is_expiring_warning_notified to true as well as execute the poll notification
// free thread to run the provided command. So this is effectively a "single-shot" expiration
// warning notification for each expiring poll.
poll->Ref().Notify(PollReference::PollNotificationType::POLL_EXPIRE_WARNING);
}
}
}
5 changes: 5 additions & 0 deletions src/gridcoin/gridcoin.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ bool CleanConfig();
//! with registry db.
//!
void RunDBPassivation();

//!
//! \brief Function to provide for poll expiration/warning notification.
//!
void NotifyPoll();
} // namespace GRC

#endif // GRIDCOIN_GRIDCOIN_H
57 changes: 56 additions & 1 deletion src/gridcoin/voting/registry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,7 @@ PollReference::PollReference()
, m_duration_days(0)
, m_votes({})
, m_magnitude_weight_factor(Fraction())
, m_expiration_warn_notified(false)
{
}

Expand Down Expand Up @@ -764,6 +765,53 @@ std::optional<CAmount> PollReference::GetActiveVoteWeight(const PollResultOption
}
}

std::string PollReference::NotifyTypeToString(const PollNotificationType& notify_type) const
{
switch (notify_type) {
case PollNotificationType::POLL_ADD:
return "added";
case PollNotificationType::POLL_DELETE:
return "deleted";
case PollNotificationType::POLL_EXPIRE_WARNING:
return "expiration_warning";
case PollNotificationType::POLL_NOTIFY_TEST:
return "test";
default:
return "unknown";
}
}

std::string PollReference::Notify(const PollNotificationType& notify_type) const
{
std::string strCmd = gArgs.GetArg("-pollnotify", std::string {});

#if HAVE_SYSTEM
// Support running an external command on poll creation. Do not notify for already expired polls. This is especially
// important during a sync to avoid spamming poll notifications.
if (!Expired(GetAdjustedTime())) {
if (!strCmd.empty()) {
// The placeholders %s1 and %s2 are replaced with the txid and the notification type.
boost::replace_all(strCmd, "%s1", m_txid.ToString());
boost::replace_all(strCmd, "%s2", NotifyTypeToString(notify_type));
boost::thread t(runCommand, strCmd); // thread runs free

LogPrint(BCLog::LogFlags::MISC, "INFO: %s: poll notify command: %s", __func__, strCmd);

if (notify_type == POLL_EXPIRE_WARNING) {
m_expiration_warn_notified = true;
}
}
}
#endif

return strCmd;
}

bool PollReference::IsExpiringWarningNotified() const
{
return m_expiration_warn_notified;
}

void PollReference::LinkVote(const uint256 txid)
{
m_votes.emplace_back(txid);
Expand Down Expand Up @@ -1064,6 +1112,8 @@ void PollRegistry::AddPoll(const ContractContext& ctx) EXCLUSIVE_LOCKS_REQUIRED(

poll_ref.m_magnitude_weight_factor = payload->m_poll.ResolveMagnitudeWeightFactor(poll_ref.GetStartingBlockIndexPtr());

poll_ref.Notify(PollReference::PollNotificationType::POLL_ADD);

if (fQtActive && !poll_ref.Expired(GetAdjustedTime())) {
uiInterface.NewPollReceived(poll_ref.Time());
}
Expand Down Expand Up @@ -1142,6 +1192,12 @@ void PollRegistry::DeletePoll(const ContractContext& ctx) EXCLUSIVE_LOCKS_REQUIR

int64_t poll_time = payload->m_poll.m_timestamp;

PollReference* poll_ref = TryBy(payload->m_poll.m_title);

// Note this reference will effectively disappear once this function exits, but this is ok, because there will
// be no need to further reference it after this point for purposes of notification.
poll_ref->Notify(PollReference::PollNotificationType::POLL_DELETE);

m_polls.erase(ToLower(payload->m_poll.m_title));

m_polls_by_txid.erase(ctx.m_tx.GetHash());
Expand All @@ -1155,7 +1211,6 @@ void PollRegistry::DeletePoll(const ContractContext& ctx) EXCLUSIVE_LOCKS_REQUIR
if (fQtActive) {
uiInterface.NewPollReceived(poll_time);;
}

}

void PollRegistry::DeleteVote(const ContractContext& ctx) EXCLUSIVE_LOCKS_REQUIRED(cs_main, PollRegistry::cs_poll_registry)
Expand Down
31 changes: 31 additions & 0 deletions src/gridcoin/voting/registry.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@ class PollReference
friend class PollRegistry;

public:
enum PollNotificationType
{
UNKNOWN,
POLL_ADD,
POLL_DELETE,
POLL_EXPIRE_WARNING,
POLL_NOTIFY_TEST
};

//!
//! \brief Initialize an empty, invalid poll reference object.
//!
Expand Down Expand Up @@ -167,6 +176,27 @@ class PollReference
//!
std::optional<CAmount> GetActiveVoteWeight(const PollResultOption &result) const;

//!
//! \brief Provides string equivalent of PollNotificationType
//! \param notify_type
//! \return string equivalent of PollNotificationType
//!
std::string NotifyTypeToString(const PollNotificationType& notify_type) const;

//!
//! \brief Runs a free thread executing provided poll notification command. Also sets m_expiration_warn_notified
//! to true if the notification type is EXPIRE_WARNING.
//! \param notify_type
//! \return std::string of command run
//!
std::string Notify(const PollNotificationType& notify_type) const;

//!
//! \brief Returns whether the expiration warning was sent.
//! \return boolean indicating whether the expiration warning was sent.
//!
bool IsExpiringWarningNotified() const;

//!
//! \brief Record a transaction that contains a response to the poll.
//!
Expand All @@ -193,6 +223,7 @@ class PollReference
uint32_t m_duration_days; //!< Number of days the poll remains active.
std::vector<uint256> m_votes; //!< Hashes of the linked vote transactions.
mutable Fraction m_magnitude_weight_factor; //!< Magnitude weight factor for the poll (defined at poll start).
mutable bool m_expiration_warn_notified; //!< Flag to indicate whether the expiration warning was sent.
}; // PollReference

//!
Expand Down
6 changes: 6 additions & 0 deletions src/init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,12 @@ void SetupServerArgs()
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-walletnotify=<cmd>", "Execute command when a wallet transaction changes (%s in cmd is replaced by TxID)",
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-pollnotify=<cmd>", "Execute command when a poll is added/deleted/expiring (%s1 in cmd is replaced by poll id,"
"%s2 is replaced by status: added, deleted, expiration warning)",
ArgsManager::ALLOW_ANY | ArgsManager::IMMEDIATE_EFFECT, OptionsCategory::OPTIONS);
argsman.AddArg("-pollexpirewarningtime=<hours>", "Hours before poll expiration to execute notification command. Default is "
"7 days (168 hours)",
ArgsManager::ALLOW_ANY | ArgsManager::IMMEDIATE_EFFECT, OptionsCategory::OPTIONS);
#endif
argsman.AddArg("-confchange", "Require confirmations for change (default: 0)",
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
Expand Down
1 change: 1 addition & 0 deletions src/rpc/server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,7 @@ static const CRPCCommand vRPCCommands[] =
{ "getpollresults", &getpollresults, cat_voting },
{ "getvotingclaim", &getvotingclaim, cat_voting },
{ "listpolls", &listpolls, cat_voting },
{ "testpollnotification", &testpollnotification, cat_voting },
{ "vote", &vote, cat_voting },
{ "votebyid", &votebyid, cat_voting },
{ "votedetails", &votedetails, cat_voting },
Expand Down
1 change: 1 addition & 0 deletions src/rpc/server.h
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ extern UniValue addpoll(const UniValue& params, bool fHelp);
extern UniValue getpollresults(const UniValue& params, bool fHelp);
extern UniValue getvotingclaim(const UniValue& params, bool fHelp);
extern UniValue listpolls(const UniValue& params, bool fHelp);
extern UniValue testpollnotification(const UniValue& params, bool fHelp);
extern UniValue vote(const UniValue& params, bool fHelp);
extern UniValue votebyid(const UniValue& params, bool fHelp);
extern UniValue votedetails(const UniValue& params, bool fHelp);
Expand Down
24 changes: 24 additions & 0 deletions src/rpc/voting.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -743,3 +743,27 @@ UniValue votedetails(const UniValue& params, bool fHelp)

throw JSONRPCError(RPC_MISC_ERROR, "No matching poll found");
}

UniValue testpollnotification(const UniValue& params, bool fHelp)
{
if (fHelp || params.size() != 1) {
throw std::runtime_error(
"testpollnotification <poll txid>\n"
"\n"
"<poll txid> --> Transaction id of the poll to test notification.\n"
"\n"
"Test the poll notification system.\n");
}

const uint256 txid = uint256S(params[0].get_str());

const PollReference* ref = GetPollRegistry().TryByTxid(txid);

if (!ref) {
throw JSONRPCError(RPC_MISC_ERROR, "No poll exists for that ID");
}

std::string cmd = ref->Notify(PollReference::PollNotificationType::POLL_NOTIFY_TEST);

return strprintf("Notification command: %s", cmd);
}
Loading