Policy Examples¶
This is a library of example policies to get started.
Vote on renaming Slack channels¶
When a channel is renamed in Slack, revert the change and perform a vote. If the vote passes, execute the rename.
Policy Kind: Platform
Action Types: slackrenameconversation
Filter:
return True
Initialize:
pass
Check:
yes_votes = proposal.get_yes_votes().count()
no_votes = proposal.get_no_votes().count()
logger.debug(f"{yes_votes} for, {no_votes} against")
if yes_votes >= 1:
return PASSED
elif no_votes >= 1:
return FAILED
logger.debug("No votes yet....")
return PROPOSED
Notify:
message = f"Should this channel be renamed to #{action.name}? Vote with :thumbsup: or :thumbsdown: on this post."
slack.initiate_vote(text=message)
Pass:
text = f"Proposal to rename this channel to #{action.name} passed."
slack.post_message(text=text, channel=action.channel, thread_ts=proposal.vote_post_id)
Fail:
text = f"Proposal to rename this channel to #{action.name} failed."
slack.post_message(text=text, channel=action.channel, thread_ts=proposal.vote_post_id)
Vote on OpenCollective expenses in Slack with restricted voting¶
Posts OpenCollective expenses to Slack channel to be voted on. If expense has three or more yes votes and one or less no votes after three hours, approves, otherwise waits until three days has passed and approves if there are any yes votes and no no votes, otherwise rejects. Once the vote is resolved, posts to both Slack thread and the OpenCollective expense thread with vote results. Restricts voting on OpenCollective expenses to eligible voters.
Policy Kind: Trigger
Action Types: expensecreated
Filter:
return True
Initialize:
pass
Check:
if not proposal.vote_post_id:
return None #the vote hasn't started yet
# Check the status of the vote and evaluate a closing condition.
yes_votes = proposal.get_yes_votes().count()
no_votes = proposal.get_no_votes().count()
#logger.debug(f"{yes_votes} for, {no_votes} against")
time_elapsed = proposal.get_time_elapsed()
# we never close a vote before three hours
if time_elapsed < datetime.timedelta(hours=3):
return None
# if there are three or more votes for, and one or less against, it passes
if yes_votes >= 2 and no_votes <= 1:
return PASSED
# if there's more than one vote against, and less than three yes votes, it fails
if no_votes > 0:
return FAILED
# if it's been three days, and there are any yeses and no nos, pass, otherwise fail
if time_elapsed > datetime.timedelta(days=3):
logger.info("Ending based on time")
reached_threshold = yes_votes > 0 and no_votes == 0
return PASSED if reached_threshold else FAILED
return PROPOSED # still pending
Notify:
# Start a vote on Slack. Only 2 users are eligible to vote.
discussion_channel = "C0177HZTXXX"
eligible_voters = ["UABC123", "UABC999"] # Slack member IDs for Alice and Bob
message = f"Vote on whether to approve <{action.url}|this request> for funds: {action.description}"
slack.initiate_vote(text=message, channel=discussion_channel, users=eligible_voters)
# Start a discussion thread on the voting message
slack.post_message(text="Discuss here! :meow-wave:", channel=discussion_channel, thread_ts=proposal.vote_post_id)
# Add a comment to the expense on Open Collective with a link to the Slack vote
link = f"<a href='{proposal.vote_url}'>on Slack</a>"
text = f"Thank you for submitting a request! A vote has been started {link}."
opencollective.post_message(text=text, expense_id=action.expense_id)
Pass:
# approve the expense
opencollective.process_expense(action="APPROVE", expense_id=action.expense_id)
yes_votes = proposal.get_yes_votes().count()
no_votes = proposal.get_no_votes().count()
message = f"Expense approved. The vote passed with {yes_votes} for and {no_votes} against."
# comment on the expense
opencollective.post_message(text=message, expense_id=action.expense_id)
# update the Slack thread
discussion_channel = "C0177HZTXXX"
slack.post_message(text=message, channel=discussion_channel, thread_ts=proposal.vote_post_id)
Fail:
# reject the expense
opencollective.process_expense(action="REJECT", expense_id=action.expense_id)
yes_votes = proposal.get_yes_votes().count()
no_votes = proposal.get_no_votes().count()
message = f"Expense rejected. The vote failed with {yes_votes} for and {no_votes} against."
# comment on the expense
opencollective.post_message(text=message, expense_id=action.expense_id)
# update the Slack thread
discussion_channel = "C0177HZTXXX"
slack.post_message(text=message, channel=discussion_channel, thread_ts=proposal.vote_post_id)
Vote on OpenCollective expenses in Loomio¶
When an expense is submitted in Open Collective, start a proposal on Loomio. The vote closes when 3 days have passed. Approve or reject the OC expense based on the vote results, and post a comment on the OC expense with the vote count.
Policy Kind: Trigger
Action Types: expensecreated
Filter:
return True
Initialize:
pass
Check:
if not proposal.is_vote_closed:
# Proposal is still pending in Looomio
return PROPOSED
consent_count = proposal.get_choice_votes(value="consent").count()
objection_count = proposal.get_choice_votes(value="objection").count()
oc_action = None
oc_message = None
if objection_count > 0:
# If 1 or more people objected, reject the expense.
oc_action = "REJECT"
oc_message = f"Expense rejected. Loomio proposal had {objection_count} objection{'s' if objection_count > 1 else ''}."
elif consent_count > 2:
# If enough people consented, approve the expense.
oc_action = "APPROVE"
oc_message = f"Expense approved. {consent_count} members consented on Loomio."
else:
# If nobody objected, but not enough people consented, leave the expense open.
oc_message = f"Consensus was not reached on Loomio. {consent_count} members consented and {objection_count} members objected."
# Process the expense (if applicable)
if oc_action:
opencollective.process_expense(action=oc_action, expense_id=action.expense_id)
# Comment on the expense
opencollective.post_message(text=oc_message, expense_id=action.expense_id)
return PASSED
Notify:
closing_at = proposal.proposal_time + datetime.timedelta(days=3)
# Start vote on Loomio
loomio.initiate_vote(
proposal,
title=f"Expense '{action.description}'",
poll_type="proposal",
details=f"Submitted on Open Collective: {action.url}",
options=["consent", "objection"],
closing_at=closing_at,
)
loomio_poll_url = proposal.vote_url
logger.debug(f"Vote started at {loomio_poll_url}")
# Record start of vote on Open Collective
link = f"<a href='{loomio_poll_url}'>on Loomio</a>"
text = f"Thank you for submitting a request! A vote has been started {link}."
opencollective.post_message(text=text, expense_id=action.expense_id)
Pass:
pass
Fail:
pass
Trigger a vote in Discord¶
This policy performs a 5-minute vote. It is triggered using a Slash command in a Discord channel. For example: ‘/policykit vote should we cancel todays meeting?’
Policy Kind: Trigger
Action Types: discordslashcommand
Filter:
VOTE_TRIGGER_PREFIX = "vote"
return action.value.startswith(VOTE_TRIGGER_PREFIX)
Initialize:
pass
Check:
VOTE_DURATION_MINUTES = 5
if proposal.get_time_elapsed() < datetime.timedelta(minutes=VOTE_DURATION_MINUTES):
logger.debug("waiting for 5 minutes to elapse...")
return PROPOSED
msg = None
if proposal.get_yes_votes().count() >= 1:
msg = "vote passed!"
elif proposal.get_no_votes().count() >= 1:
msg = "vote failed!"
else:
msg = "not enough votes to make a decision"
discord.post_message(text=msg, channel=action.channel, message_id=proposal.vote_post_id)
return PASSED
Notify:
VOTE_TRIGGER_PREFIX = "vote"
VOTE_DURATION_MINUTES = 5
vote_text = action.value[len(VOTE_TRIGGER_PREFIX):].strip()
vote_text += f" Vote closes in {VOTE_DURATION_MINUTES} minutes."
discord.initiate_vote(text=vote_text)
Pass:
pass
Fail:
pass
Minimum SourceCred value required for creating Discourse Topic¶
Only users with at least 300 cred can create new topics in a certain Discourse category. If a user does not pass the threshold, the topic gets deleted (aka the ‘governed action’ is reverted).
Policy Kind: Platform
Action Types: discoursecreatetopic
Filter:
return action.category == 8
Initialize:
pass
Check:
value = sourcecred.get_cred(username=action.initiator.username)
return PASSED if value > 300 else FAILED
Notify:
pass
Pass:
pass
Fail:
pass
Don’t allow posts in Slack channel¶
This could be extended to add any logic to determine who can post in a given channel. Posts in the channel are deleted, and the user is notified about why it happened.
Policy Kind: Platform
Action Types: slackpostmessage
Filter:
return action.channel == "C025LDZ76R3"
Initialize:
pass
Check:
return FAILED
Notify:
pass
Pass:
pass
Fail:
# create an ephemeral post that is only visible to the poster
message = f"Post was deleted because of policy '{policy.name}'"
slack.post_message(
channel=action.channel,
users=[action.initiator],
post_type="ephemeral",
text=message
)
Discord ping¶
Testing policy that responds to a slash command invocation in Discord.
Policy Kind: Trigger
Action Types: discordslashcommand
Filter:
return action.value == "ping"
Initialize:
discord.post_message("pong", action.channel)
Check:
return PASSED
Notify:
pass
Pass:
pass
Fail:
pass
Consensus vote in Slack with restricted voting + reminder¶
Allows users to trigger a vote in the #core-group slack channel. Only core group members are able to cast votes. Following community policy, all votes must pass via consensus. One day before a policy is about to be resolved by default, reminds core-group members that the vote is still open.
Policy Kind: Trigger
Action Types: slackpostmessage
Filter:
return action.text.startswith("core-group-vote")
Initialize:
pass
Check:
# CONSTANTS
CHANNEL = "C0ABC123"
LENGTH_OF_VOTE_IN_DAYS = 5
MINIMUM_YES_VOTES = 2
MAXIMUM_NO_VOTES = 0
ELIGIBLE_VOTERS = ["U01", "U02", "U03", "U04", "U05", "U06"]
# save for later
proposal.data.set("eligible_voters", ELIGIBLE_VOTERS)
proposal.data.set("discussion_channel", CHANNEL)
if not proposal.vote_post_id:
return PROPOSED #the vote hasn't started yet
# vote info
consent_votes = proposal.get_choice_votes(value="consent")
object_votes = proposal.get_choice_votes(value="object")
abstain_votes = proposal.get_choice_votes(value="abstain")
logger.debug(f"{consent_votes} for, {object_votes} against, {abstain_votes} abstain")
# if everyone has consented or abstained, the vote passes
if len(consent_votes) + len(abstain_votes) == len(ELIGIBLE_VOTERS):
return PASSED
# if voting is almost over, remind people who haven't voted
reminder_sent = proposal.data.get('reminder_sent')
if not reminder_sent and proposal.get_time_elapsed() > datetime.timedelta(days=LENGTH_OF_VOTE_IN_DAYS-1):
already_voted = proposal.get_choice_votes().values_list("user__username")
already_voted_list = [voter[0] for voter in already_voted]
nonvoter_list = [f"<@{uid}>" for uid in ELIGIBLE_VOTERS if uid not in already_voted_list]
nonvoter_string = ", ".join(nonvoter_list)
logger.debug(f"Eligible voters: {ELIGIBLE_VOTERS}; Already voted: {already_voted}; Nonvoter list: {nonvoter_list}")
reminder_msg = f"There is one day left to vote on this proposal. {nonvoter_string} have not voted yet."
slack.post_message(text=reminder_msg, channel=action.channel, thread_ts=proposal.vote_post_id)
proposal.data.set("reminder_sent", True)
# if time's up, resolve
if proposal.get_time_elapsed() > datetime.timedelta(days=LENGTH_OF_VOTE_IN_DAYS):
if object_votes.count() > MAXIMUM_NO_VOTES:
return FAILED
if consent_votes.count() < MINIMUM_YES_VOTES:
return FAILED
return PASSED
return PROPOSED # still pending
Notify:
# Start a vote on Slack
discussion_channel = proposal.data.get('discussion_channel')
eligible_voters = proposal.data.get('eligible_voters')
proposal_text = action.text.replace("core-group-vote ", "").strip()
message = f"<@channel>, please respond to this proposal: {proposal_text} If all core group members respond by consenting or abstaining, or if after five days there are no objections and at least two consents, the proposal passes."
slack.initiate_vote(text=message, channel=discussion_channel,
users=eligible_voters, options=["consent", "object", "abstain"])
slack.post_message(text="Discuss here! :meow-wave:", channel=discussion_channel, thread_ts=proposal.vote_post_id)
Pass:
# Announce result on thread
text = f"<@{action.initiator.username}>'s proposal passed!"
slack.post_message(text=text, channel=action.channel, thread_ts=proposal.vote_post_id)
Fail:
# Announce result on thread
text = f"<@{action.initiator.username}>'s proposal failed."
slack.post_message(text=text, channel=action.channel, thread_ts=proposal.vote_post_id)
All Constitution Actions need 1 approval in Slack¶
All constitution actions trigger a Slack vote. In order to pass, the proposal must get at least 1 approval within 3 hours. If the proposal gets any downvotes, it fails.
Policy Kind: Constitution
Filter:
return True
Initialize:
pass
Check:
if proposal.get_yes_votes().count() > 0:
return PASSED
if proposal.get_no_votes().count() > 0:
return FAILED
if proposal.get_time_elapsed() > datetime.timedelta(hours=3):
logger.debug(f"Failing proposed constitution change, no approvals in 3 hours")
return FAILED
return PROPOSED
Notify:
message = f"'{action}' proposed by {action.initiator.readable_name}"
governance_channel = "C0001"
slack.initiate_vote(text=message, channel=governance_channel)
Pass:
message = f"Constitutional change accepted!"
governance_channel = "C0001"
slack.post_message(text=message, channel=governance_channel, thread_ts=proposal.vote_post_id)
Fail:
message = f"Constitutional change rejected."
governance_channel = "C0001"
slack.post_message(text=message, channel=governance_channel, thread_ts=proposal.vote_post_id)
All Constitution Actions Pass and get posted in Slack¶
All constitution actions pass automatically and get posted to a governance channel in Slack. Constitution actions are changes to policies, roles, and documents.
Policy Kind: Constitution
Filter:
return True
Initialize:
pass
Check:
return PASSED
Notify:
pass
Pass:
message = f"Constitution change '{action}' proposed by {action.initiator.readable_name} passed."
governance_channel = "C000111"
slack.post_message(text=message, channel=governance_channel)
Fail:
pass