Overview
The TextUtils module provides formatting functions for creating human-readable text representations of proposals, vote tallies, and other bot messages. It integrates with the DateTime library for timestamp formatting.
Formats a single proposal into a human-readable text representation.
public func formatProposal(proposal : ProposalAPI) : Text
The proposal object to format
Formatted proposal text including ID, title, topic, creation date, and proposer
Example
import TextUtils "./backend/TextUtils";
let formattedProposal = TextUtils.formatProposal(proposal);
// Output:
// Proposal 12345
// Title: Upgrade NNS Governance Canister
// Topic: System Canister Management
// Date Created: 2026-03-06 14:30
// Proposer: 123456789
Formats multiple proposals into a combined text representation.
public func formatProposals(proposals : [Proposal]) : Text
Array of proposals to format (includes proposalData, messageIndex, and attempts)
Formatted text of all proposals separated by double newlines
Example
import TextUtils "./backend/TextUtils";
let formattedList = TextUtils.formatProposals(proposalsArray);
// Output:
// Proposal 12345
// Title: Upgrade NNS Governance Canister
// ...
//
// Proposal 12346
// Title: Update Subnet Configuration
// ...
Formats a proposal message for an OpenChat thread with clickable links.
public func formatProposalThreadMsg(
ocGroupId : Text,
proposalId : Nat64,
ocGroupMessageId : ?Nat32
) : Text
The OpenChat group identifier
Optional OpenChat message ID for creating a direct link
Formatted message with links to the ICP dashboard and OpenChat
Example
import TextUtils "./backend/TextUtils";
let threadMsg = TextUtils.formatProposalThreadMsg(
"abc123-group",
12345,
?987654
);
// Output:
// Proposal 12345:
// [Dashboard Link](https://dashboard.internetcomputer.org/proposal/12345)
// [OpenChat Link to vote](https://oc.app/group/abc123-group/987654)
Formats multiple proposals for an OpenChat thread.
public func formatBatchProposalThreadMsg(
ocGroupId : Text,
proposals : [Proposal]
) : Text
The OpenChat group identifier
Array of proposals to format
Combined formatted messages for all proposals
Example
import TextUtils "./backend/TextUtils";
let batchMsg = TextUtils.formatBatchProposalThreadMsg("abc123-group", proposalsArray);
Proposal Analysis
isSeparateBuildProcess
Determines if a proposal requires a separate build process by checking for specific canister IDs.
public func isSeparateBuildProcess(title : Text) : Bool
The proposal title to analyze
True if the title contains specific NNS canister IDs (qoctq-giaaa-aaaaa-aaaea-cai or rdmx6-jaaaa-aaaaa-aaadq-cai)
Example
import TextUtils "./backend/TextUtils";
let needsSeparateBuild = TextUtils.isSeparateBuildProcess(
"Upgrade qoctq-giaaa-aaaaa-aaaea-cai to version 2.0"
);
// Returns: true
Extracts a Git hash from the proposal description.
public func extractGitHash(title : Text, description : ?Text) : ?Text
The proposal title (not currently used in extraction)
The proposal description containing the Git hash
The extracted Git hash, or null if not found
Supported Patterns
Git hash: <hash>
### Git Hash: <hash>
Example
import TextUtils "./backend/TextUtils";
let description = "Upgrade proposal\n\nGit hash: abc123def456\n\nDetails...";
let hash = TextUtils.extractGitHash("", ?description);
// Returns: ?"abc123def456"
let description2 = "Upgrade\n### Git Hash: `xyz789`\nMore info...";
let hash2 = TextUtils.extractGitHash("", ?description2);
// Returns: ?"xyz789"
voteToText
Converts a vote value to human-readable text.
public func voteToText(vote : Vote) : Text
Vote enum (#Abstained, #Approved, #Rejected, or #Pending)
Human-readable vote status
Example
import TextUtils "./backend/TextUtils";
let voteText = TextUtils.voteToText(#Approved);
// Returns: "Accepted"
let voteText2 = TextUtils.voteToText(#Pending);
// Returns: "Pending"
proposalStatusToText
Converts a proposal status to human-readable text.
public func proposalStatusToText(status : ProposalStatus) : Text
Proposal status enum (#Pending or #Executed with verdict)
Example
import TextUtils "./backend/TextUtils";
let statusText = TextUtils.proposalStatusToText(#Executed(#Approved));
// Returns: "Approved"
let statusText2 = TextUtils.proposalStatusToText(#Pending);
// Returns: "Pending"
Formats vote tally data into a detailed message showing individual votes.
public func formatMessage(tally : TallyData) : Text
Tally data containing votes and overall status
Formatted message with tally status, counts, and individual neuron votes
Example
import TextUtils "./backend/TextUtils";
let tallyMsg = TextUtils.formatMessage(tallyData);
// Output:
// Tally Status: Accepted
// Approves: 5 Rejects: 1 Total: 6
// Neuron ID: 123456789 | Alias: My Neuron | Vote: Accepted
// Neuron ID: 987654321 | Alias: () | Vote: Rejected
// ...
Complete Example
import TextUtils "./backend/TextUtils";
import Types "./backend/Types";
import TrackerTypes "./backend/TrackerTypes";
actor ProposalFormatter {
// Format a proposal for display
public func displayProposal(proposal : TrackerTypes.ProposalAPI) : async Text {
let formatted = TextUtils.formatProposal(proposal);
formatted
};
// Create an OpenChat message with links
public func createProposalMessage(
groupId : Text,
proposalId : Nat64,
messageId : ?Nat32
) : async Text {
TextUtils.formatProposalThreadMsg(groupId, proposalId, messageId)
};
// Analyze proposal for build requirements
public func analyzeProposal(
title : Text,
description : ?Text
) : async (Bool, ?Text) {
let needsSeparateBuild = TextUtils.isSeparateBuildProcess(title);
let gitHash = TextUtils.extractGitHash(title, description);
(needsSeparateBuild, gitHash)
};
// Format tally results
public func displayTally(tally : Types.TallyData) : async Text {
TextUtils.formatMessage(tally)
};
// Convert vote to readable format
public func getVoteStatus(vote : Types.Vote) : async Text {
TextUtils.voteToText(vote)
};
}
Best Practices
Always provide the OpenChat group ID and message ID for creating functional links: // Good - includes message ID for direct link
formatProposalThreadMsg(groupId, proposalId, ?messageId)
// Acceptable - no message ID, only dashboard link shown
formatProposalThreadMsg(groupId, proposalId, null)
The formatMessage function provides detailed vote breakdown. For neuron aliases, it shows () when no display name is available: let msg = TextUtils.formatMessage(tallyData);
// Includes approve/reject counts and individual neuron votes