| Title: | RLadies+ GitHub Organisation Management |
|---|---|
| Description: | GitHub App for managing the RLadies+ organisation. Handles onboarding and offboarding of global team members, chapter onboarding, PR review automation, org-wide reporting, Airtable sync, website automation, chapter monitoring, and interactive issue commands. |
| Authors: | RLadies+ [aut, cre, cph] (ROR: <https://ror.org/05wpb1k41>) |
| Maintainer: | RLadies+ <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.2.0.9006 |
| Built: | 2026-10-04 13:26:26 UTC |
| Source: | https://github.com/rladies/jinx |
Caches the allowed-bases list in KV (same AIRTABLE_BASES namespace and
1h TTL the Worker used) rather than calling Airtable's Meta API on every
webhook. R port of airtable_base_is_allowed()/
airtable_allowed_bases_get() from the deleted
worker/src/airtable-meta.js.
airtable_base_allowed( base_id, namespace_id = airtable_bases_namespace_id(), account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), api_token = Sys.getenv("CLOUDFLARE_API_TOKEN"), api_key = Sys.getenv("AIRTABLE_API_KEY") )airtable_base_allowed( base_id, namespace_id = airtable_bases_namespace_id(), account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), api_token = Sys.getenv("CLOUDFLARE_API_TOKEN"), api_key = Sys.getenv("AIRTABLE_API_KEY") )
base_id |
Airtable base ID to check. |
namespace_id |
KV namespace ID for |
account_id |
Cloudflare account ID. Defaults to env
|
api_token |
Cloudflare API token. Defaults to env
|
api_key |
Airtable API key. Defaults to env |
TRUE if the base is within the token's scope.
R's first Airtable write primitive - everything in R/airtable-sync.R
is read-only. R port of airtable_record_update() from the deleted
worker/src/airtable-invite.js.
airtable_record_update( base_id, table_id, record_id, fields, api_key = Sys.getenv("AIRTABLE_API_KEY") )airtable_record_update( base_id, table_id, record_id, fields, api_key = Sys.getenv("AIRTABLE_API_KEY") )
base_id |
Airtable base ID. |
table_id |
Airtable table ID. |
record_id |
Airtable record ID. |
fields |
Named list of fields to update. |
api_key |
Airtable API key. Defaults to env |
Invisibly, TRUE.
The airtable_webhook event handler registered in jinx_events().
Checks the base is within the configured Airtable token's scope, builds
the approval-request card, and posts it to the community-invite
channel. R port of the remainder of airtable_webhook_handle() (after
shared-secret verification, which stays in the Worker) from the
deleted worker/src/airtable-invite.js.
airtable_webhook_process( email, name = NULL, chapter = NULL, record_id, base_id, table_id, channel = Sys.getenv("SLACK_COMMUNITY_INVITE_CHANNEL") )airtable_webhook_process( email, name = NULL, chapter = NULL, record_id, base_id, table_id, channel = Sys.getenv("SLACK_COMMUNITY_INVITE_CHANNEL") )
email |
Applicant email. |
name |
Applicant name, or |
chapter |
Chapter name, or |
record_id |
Airtable record ID. |
base_id |
Airtable base ID. |
table_id |
Airtable table ID. |
channel |
Slack channel to post the request to. Defaults to env
|
Invisibly, TRUE if posted, FALSE if the base was rejected.
Queries the GitHub API for repository activity metrics across all non-meetup repos in the organization.
analytics_collect_chapter_activity( org = "rladies", months = 12, exclude_pattern = "^meetup-" )analytics_collect_chapter_activity( org = "rladies", months = 12, exclude_pattern = "^meetup-" )
org |
GitHub organization. |
months |
Number of months of history. |
exclude_pattern |
Regex pattern to exclude repos. |
Data frame with columns: chapter, month, commits, prs, issues.
Collect contributor growth data
analytics_collect_contributor_growth(org = "rladies", months = 12)analytics_collect_contributor_growth(org = "rladies", months = 12)
org |
GitHub organization. |
months |
Number of months of history. |
Data frame with columns: month, new_contributors, total_contributors, active_repos.
Calculates month-over-month changes and rolling averages from chapter activity data.
analytics_compute_trends(activity_data)analytics_compute_trends(activity_data)
activity_data |
Data frame from |
Data frame with trend columns added.
Format analytics as markdown
analytics_format_markdown(trends, contributor_growth)analytics_format_markdown(trends, contributor_growth)
trends |
Data frame from |
contributor_growth |
Data frame from
|
Formatted markdown string.
Orchestrates data collection, trend computation, and formatting.
analytics_generate_dashboard(org = "rladies", months = 12, output_path = NULL)analytics_generate_dashboard(org = "rladies", months = 12, output_path = NULL)
org |
GitHub organization. |
months |
Number of months of history. |
output_path |
Optional path to write JSON data. |
Named list with trends, growth, and markdown (invisibly).
Create a formatted announcement message
announce_create_message(frontmatter, uri)announce_create_message(frontmatter, uri)
frontmatter |
Named list from YAML front matter (needs |
uri |
URL of the post. |
Formatted message string.
Reads YAML front matter from a blog post, creates an announcement message, and posts to selected platforms.
announce_post( post, platforms = c("bluesky", "linkedin", "mastodon"), newsletter = TRUE )announce_post( post, platforms = c("bluesky", "linkedin", "mastodon"), newsletter = TRUE )
post |
File path to the blog post (markdown with YAML front matter). |
platforms |
Character vector of platforms to post to.
Defaults to all: |
newsletter |
Logical, whether to also send a newsletter. Defaults
to |
Invisibly returns a named list with the posting result for each platform.
Post an announcement to Bluesky
announce_post_bluesky(text, image = NULL, image_alt = NULL)announce_post_bluesky(text, image = NULL, image_alt = NULL)
text |
Post text. |
image |
Optional file path to an image. |
image_alt |
Alt text for the image. |
Post response (invisibly).
Post an announcement to Mastodon
announce_post_mastodon(text, image = NULL, image_alt = NULL)announce_post_mastodon(text, image = NULL, image_alt = NULL)
text |
Post text. |
image |
Optional file path to an image. |
image_alt |
Alt text for the image. |
Post response (invisibly).
Posts as the authenticated user (jinx[bot] when using a GitHub App token).
announce_post_reply(owner, repo, issue_number, body)announce_post_reply(owner, repo, issue_number, body)
owner |
Repository owner. |
repo |
Repository name. |
issue_number |
Issue or PR number. |
body |
Comment body (markdown). |
The API response (invisibly).
Send a newsletter via ConvertKit
announce_send_newsletter(frontmatter, url)announce_send_newsletter(frontmatter, url)
frontmatter |
Named list with |
url |
URL of the post. |
API response (invisibly).
Builds an entry from the URL's metadata and opens a PR adding it to the awesome-rladies-creations content directory. If an entry for the URL's domain already exists, no PR is opened.
blog_add_pr( url, org = "rladies", repo = "awesome-rladies-creations", content_path = "data/content", language = "en", author_name = NULL, base = "main" )blog_add_pr( url, org = "rladies", repo = "awesome-rladies-creations", content_path = "data/content", language = "en", author_name = NULL, base = "main" )
url |
Blog URL. |
org, repo, content_path
|
Location of the blog entry JSON files. |
language |
Language code. Defaults to |
author_name |
Author name. When |
base |
Base branch. Defaults to |
A list with status ("created" or "exists"), filename,
and url (the PR URL when created).
Check blog URLs and RSS feeds for broken links
blog_check_links(blogs_path)blog_check_links(blogs_path)
blogs_path |
Path to directory containing blog JSON files. |
A data frame with columns file, url, rss_feed,
url_status, rss_status.
Lists the blog entry JSON files in the content directory via the GitHub
API and checks each entry's url and rss_feed for broken links.
blog_check_links_repo( org = "rladies", repo = "awesome-rladies-creations", content_path = "data/content" )blog_check_links_repo( org = "rladies", repo = "awesome-rladies-creations", content_path = "data/content" )
org, repo, content_path
|
Location of the blog entry JSON files. |
A data frame with columns file, url, rss_feed,
url_status, rss_status.
Fetches OpenGraph metadata from the URL and writes a JSON entry compatible with awesome-rladies-creations format.
blog_create_entry(url, language = "en", author_name, output_dir = ".")blog_create_entry(url, language = "en", author_name, output_dir = ".")
url |
Blog URL. |
language |
Language code. Defaults to |
author_name |
Author name. Required. |
output_dir |
Directory to write the JSON file. Defaults to |
File path of the created JSON (invisibly).
The heading, a linked bold title and the post's own description go in one section; the preview image follows as its own block so Slack renders it full width rather than as a thumbnail; the byline sits in a context block, which Slack renders small and grey so it reads as attribution rather than as part of the post.
blog_feed_blocks(item, source, og = list())blog_feed_blocks(item, source, og = list())
item |
A one-row data frame from |
source |
A one-row data frame from |
og |
Preview data from |
Every piece of text here comes from a feed or a page that contributors control, so all of it is escaped - the link markup around the title is the only markup the message is allowed to introduce.
A list of Slack Block Kit blocks.
Polls each source in turn, keeping going when an individual feed is down so one dead blog can't silence the rest, then returns the items sorted oldest first so a channel reads in publication order.
blog_feed_collect( entries = NULL, max_age_days = 14, now = as.numeric(Sys.time()) )blog_feed_collect( entries = NULL, max_age_days = 14, now = as.numeric(Sys.time()) )
entries |
Curated content entries. Fetched when |
max_age_days |
Only announce items published within this window. |
now |
Current time, as a unix timestamp. |
Deliberately knows nothing about what any workspace has already
announced: both blog channels want the same posts, so a run polls the
feeds once and each workspace filters the result against its own
seen-set with blog_feed_pending(). Polling per workspace would
double the requests every contributor's blog receives.
A list with posts (a list of item/source pairs, oldest
first), feedless (titles with no rss_feed), and sources (how
many feeds were polled).
Titles and author names come from feeds and curated JSON that
contributors control, so both are escaped before they go into a
broadcast message - that neutralises injected links and
<!channel>/<!everyone> mass-pings while leaving the message's own
link markup intact.
blog_feed_format(item, source)blog_feed_format(item, source)
item |
A one-row data frame from |
source |
A one-row data frame from |
Character scalar Slack mrkdwn message.
Matched on local-name() rather than a namespace prefix: the feeds in
the curated list are a mix of RSS 2.0, Atom, and RSS 1.0/RDF written by
half a dozen site generators, and only the element names are reliably
shared between them.
blog_feed_items(xml, base = NULL, max_items = 25L)blog_feed_items(xml, base = NULL, max_items = 25L)
xml |
Feed document as text. |
base |
Feed URL, used to resolve relative item links. |
max_items |
Keep at most this many items from the top of the feed. |
A data frame with columns id, title, link, and date (a
unix timestamp, 0 when the feed gave no usable date).
Drops anything already announced and anything older than max_age_days
a blog joining the curated list arrives with its whole back catalogue in the feed, and without the age gate its first run would announce years of posts at once. Items with no usable date are treated as too old for the same reason.
blog_feed_new_items( items, seen = character(), max_age_days = 14, now = as.numeric(Sys.time()) )blog_feed_new_items( items, seen = character(), max_age_days = 14, now = as.numeric(Sys.time()) )
items |
Data frame from |
seen |
Character vector of item ids already announced. |
max_age_days |
Only announce items published within this window. |
now |
Current time, as a unix timestamp. |
A subset of items, oldest first.
One extra request per new post - not per feed, and not per item - so at a post a week this is nothing next to polling the feeds themselves. Every failure returns empty: a post with no preview is still worth announcing, so nothing here is allowed to abort the run.
blog_feed_og(url)blog_feed_og(url)
url |
The post's URL. |
A list with description, image, and image_alt, each a
character scalar that may be "".
Keep the posts one workspace still owes its channel
blog_feed_pending(posts, seen = character(), limit = 20L)blog_feed_pending(posts, seen = character(), limit = 20L)
posts |
List of |
seen |
Character vector of item ids already announced there. |
limit |
Most posts to announce in one run. |
The subset still to announce, oldest first.
Replaces the Slack Feed app's per-blog subscriptions with one run
driven by awesome-rladies-creations: a blog added to the curated list
is announced without anyone touching Slack.
blog_feed_post( workspace = c("community", "organiser"), channel = env_default("SLACK_BLOG_CHANNEL", "blogs-by-rladies"), dry_run = FALSE, posts = NULL, entries = NULL, max_age_days = 14, limit = 20L, slack_token = NULL, og_fetch = blog_feed_og_memo(), namespace_id = slack_tokens_namespace_id(), account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), api_token = Sys.getenv("CLOUDFLARE_API_TOKEN") )blog_feed_post( workspace = c("community", "organiser"), channel = env_default("SLACK_BLOG_CHANNEL", "blogs-by-rladies"), dry_run = FALSE, posts = NULL, entries = NULL, max_age_days = 14, limit = 20L, slack_token = NULL, og_fetch = blog_feed_og_memo(), namespace_id = slack_tokens_namespace_id(), account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), api_token = Sys.getenv("CLOUDFLARE_API_TOKEN") )
workspace |
Which Slack workspace to post in. |
channel |
Channel to post to. Defaults to env
|
dry_run |
When |
posts |
Collected posts from |
entries |
Curated content entries. Fetched when |
max_age_days |
Only announce items published within this window. |
limit |
Most posts to announce in one run. |
slack_token |
Bot token for |
og_fetch |
Reader for a post's preview data. Defaults to a
per-call cache; |
namespace_id |
KV namespace ID for |
account_id |
Cloudflare account ID. Defaults to env
|
api_token |
Cloudflare API token. Defaults to env
|
Announced ids are recorded in KV per workspace, and only after a successful post - a failed run re-announces nothing and loses nothing.
Use blog_feed_run() to serve both workspaces, which polls the feeds
once for the pair rather than once each.
Invisibly, the number of posts announced.
The scheduled entry point. Both channels want the same posts, so the feeds are polled once and each workspace is then filtered against its own seen-set - at a ten-minute cadence, polling per workspace would double the requests every contributor's blog receives for no gain.
blog_feed_run( workspaces = c("community", "organiser"), channel = env_default("SLACK_BLOG_CHANNEL", "blogs-by-rladies"), dry_run = FALSE, seed = FALSE, entries = NULL, max_age_days = 14, limit = 20L, namespace_id = slack_tokens_namespace_id(), account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), api_token = Sys.getenv("CLOUDFLARE_API_TOKEN") )blog_feed_run( workspaces = c("community", "organiser"), channel = env_default("SLACK_BLOG_CHANNEL", "blogs-by-rladies"), dry_run = FALSE, seed = FALSE, entries = NULL, max_age_days = 14, limit = 20L, namespace_id = slack_tokens_namespace_id(), account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), api_token = Sys.getenv("CLOUDFLARE_API_TOKEN") )
workspaces |
Workspaces to announce in. |
channel |
Channel to post to. Defaults to env
|
dry_run |
When |
seed |
When |
entries |
Curated content entries. Fetched when |
max_age_days |
Only announce items published within this window. |
limit |
Most posts to announce in one run. |
namespace_id |
KV namespace ID for |
account_id |
Cloudflare account ID. Defaults to env
|
api_token |
Cloudflare API token. Defaults to env
|
One workspace failing does not stop the others: each is attempted, and the error is re-raised at the end so the run still goes red.
Invisibly, a named integer of posts announced per workspace.
An entry is pollable when it carries an rss_feed, whatever its type
YouTube channels and plain websites serve the same Atom/RSS XML as blogs do. Entries without a feed can't be polled at all, so they are returned separately for the run to report rather than dropped silently.
blog_feed_sources(entries)blog_feed_sources(entries)
entries |
List of entry records from the curated content list. |
A list with sources (a data frame with columns title,
site, feed, author, and type) and feedless (a character
vector of titles).
Format Workers invocation metrics as a markdown table
cf_ops_format_workers_report(df)cf_ops_format_workers_report(df)
df |
Data frame from |
Character string with markdown-formatted report.
Read-only, for interactive/console use during an incident — see
cf_ops_list_kv_keys() for why this isn't wired into a /jinx command.
cf_ops_get_kv_value( account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), namespace_id, key_name, token = cf_ops_token() )cf_ops_get_kv_value( account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), namespace_id, key_name, token = cf_ops_token() )
account_id |
Cloudflare account ID. Defaults to env
|
namespace_id |
KV namespace ID. |
key_name |
Key to read. |
token |
Cloudflare API token. Defaults to env
|
Character string with the stored value.
Wraps cloudflarer::cf_delete_kv_value(). See cf_ops_kv_put() for
why this is narrowly scoped rather than a general-purpose tool.
cf_ops_kv_delete( account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), namespace_id, key_name, token = cf_ops_token() )cf_ops_kv_delete( account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), namespace_id, key_name, token = cf_ops_token() )
account_id |
Cloudflare account ID. Defaults to env
|
namespace_id |
KV namespace ID. |
key_name |
Key to delete. |
token |
Cloudflare API token. Defaults to env
|
The API response (invisibly).
Wraps cloudflarer::cf_put_kv_value(). Unlike cf_ops_list_kv_keys()/
cf_ops_get_kv_value(), this is not a general-purpose "read/write any
key" tool exposed to a /jinx command — it's called internally by
event/command handlers against a small set of hardcoded key patterns
(e.g. pending_link:{email}, channel_index:{team_id}), the same
scoping the exfiltration concern in those two functions' docs warns
about.
cf_ops_kv_put( account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), namespace_id, key_name, value, ttl_seconds = NULL, token = cf_ops_token() )cf_ops_kv_put( account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), namespace_id, key_name, value, ttl_seconds = NULL, token = cf_ops_token() )
account_id |
Cloudflare account ID. Defaults to env
|
namespace_id |
KV namespace ID. |
key_name |
Key to write. |
value |
Character value to store. |
ttl_seconds |
Optional expiration TTL in seconds. |
token |
Cloudflare API token. Defaults to env
|
The API response (invisibly).
Read-only, for interactive/console use during an incident — not wired
into a /jinx command, since KV namespaces on this account
(SLACK_TOKENS, AIRTABLE_BASES) hold operational secrets, and a
chat-triggered arbitrary-namespace read is a real exfiltration path.
cf_ops_list_kv_keys( account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), namespace_id, prefix = NULL, token = cf_ops_token() )cf_ops_list_kv_keys( account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), namespace_id, prefix = NULL, token = cf_ops_token() )
account_id |
Cloudflare account ID. Defaults to env
|
namespace_id |
KV namespace ID. |
prefix |
Optional key-name prefix filter. |
token |
Cloudflare API token. Defaults to env
|
Data frame of key records.
Thin wrapper over cloudflarer::cf_list_workers_scripts().
cf_ops_list_workers_scripts( account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), token = cf_ops_token() )cf_ops_list_workers_scripts( account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), token = cf_ops_token() )
account_id |
Cloudflare account ID. Defaults to env
|
token |
Cloudflare API token. Defaults to env
|
Data frame of Workers script records.
Thin wrapper over cloudflarer::cf_purge_cache() that deliberately
does not expose purge_everything — a maintainer who genuinely needs
a full-zone wipe calls cloudflarer::cf_purge_cache(purge_everything = TRUE) directly. That extra step is intentional friction on the most
dangerous call this integration can make.
cf_ops_purge_cache( zone_id = Sys.getenv("CLOUDFLARE_ZONE_ID"), files = NULL, prefixes = NULL, token = cf_ops_token() )cf_ops_purge_cache( zone_id = Sys.getenv("CLOUDFLARE_ZONE_ID"), files = NULL, prefixes = NULL, token = cf_ops_token() )
zone_id |
Cloudflare zone ID. Defaults to env |
files |
Optional character vector of URLs to purge. |
prefixes |
Optional character vector of URL prefixes to purge
(without scheme), e.g. |
token |
Cloudflare API token. Defaults to env
|
A named list with the purge job id.
Thin wrapper over cloudflarer::cf_workers_invocations(), defaulting
script_name to "jinx" (matches wrangler.jsonc's worker name)
rather than requiring the caller to know it.
cf_ops_workers_invocations( account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), since = Sys.Date() - 1, until = Sys.Date(), script_name = "jinx", token = cf_ops_token() )cf_ops_workers_invocations( account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), since = Sys.Date() - 1, until = Sys.Date(), script_name = "jinx", token = cf_ops_token() )
account_id |
Cloudflare account ID. Defaults to env
|
since, until
|
Date range. Defaults to the last day. |
script_name |
Worker script name. Defaults to |
token |
Cloudflare API token. Defaults to env
|
Data frame with columns date, script, requests, errors,
subrequests, cpu_p50_us, cpu_p99_us.
Check CFP deadlines and post reminders
cfp_check_deadlines(org = "rladies", repo = "global-team", warn_days = 7)cfp_check_deadlines(org = "rladies", repo = "global-team", warn_days = 7)
org |
GitHub organization. |
repo |
Repository where CFP issues are tracked. |
warn_days |
Number of days before deadline to warn. |
Data frame of approaching CFPs (invisibly).
Create a CFP tracking issue
cfp_create_issue( conference, deadline, url, topics = character(0), org = "rladies", repo = "global-team" )cfp_create_issue( conference, deadline, url, topics = character(0), org = "rladies", repo = "global-team" )
conference |
Conference name. |
deadline |
Submission deadline (YYYY-MM-DD). |
url |
CFP URL. |
topics |
Character vector of topic tags. |
org |
GitHub organization. |
repo |
Repository to create the issue in. |
Issue URL (invisibly).
List open CFPs tracked as GitHub issues
cfp_list_open(org = "rladies", repo = "global-team")cfp_list_open(org = "rladies", repo = "global-team")
org |
GitHub organization. |
repo |
Repository where CFP issues are tracked. |
Data frame with columns: conference, deadline, url, number, status.
Adds a speaker recommendation as a comment on the CFP tracking issue.
cfp_recommend_speaker( conference, speaker_name, speaker_github = NULL, expertise = character(0), org = "rladies", repo = "global-team" )cfp_recommend_speaker( conference, speaker_name, speaker_github = NULL, expertise = character(0), org = "rladies", repo = "global-team" )
conference |
Conference name (matched against open CFP issues). |
speaker_name |
Speaker name or GitHub username. |
speaker_github |
Optional GitHub username. |
expertise |
Character vector of expertise areas. |
org |
GitHub organization. |
repo |
Repository where CFP issues are tracked. |
Comment URL (invisibly).
Only rows with status "apply" are sent to Slack. Everything else is
returned untouched so the caller keeps a full record of what was
skipped and why.
channel_copy_apply( plan, token, dry_run = TRUE, skip = character(), join = TRUE, include_drift = FALSE, user_token = NULL )channel_copy_apply( plan, token, dry_run = TRUE, skip = character(), join = TRUE, include_drift = FALSE, user_token = NULL )
plan |
A plan from |
token |
Slack bot token. |
dry_run |
When |
skip |
Channel names to leave alone. |
join |
Join channels the bot is not a member of first. Slack
refuses |
include_drift |
Also apply rows whose live value has changed since the review. Off by default: drift means someone edited the channel after the copy was written, so applying overwrites the newer text with older reviewed text. |
user_token |
Optional Slack user token used for |
plan with applied and error columns added.
Compares the reviewed proposals against the workspace's live state and
classifies every intended change. A row is "unchanged" when Slack
already holds the proposed value, "drift" when the live value no
longer matches what the review recorded (someone edited it since, so
the proposal may be stale), "missing" when the channel is not in the
workspace, and "apply" otherwise.
channel_copy_plan( workspace = c("organiser", "community"), index = NULL, proposals = NULL, renames = NULL )channel_copy_plan( workspace = c("organiser", "community"), index = NULL, proposals = NULL, renames = NULL )
workspace |
Either |
index |
Optional pre-fetched channel index, for testing. |
proposals, renames
|
Optional proposal data frames, for testing. |
Renames are planned last so a rename cannot hide the channel from the copy rows in the same pass.
A data frame of planned changes.
Read the reviewed channel copy proposals
channel_copy_proposals(workspace = c("organiser", "community"), path = NULL)channel_copy_proposals(workspace = c("organiser", "community"), path = NULL)
workspace |
Either |
path |
Optional path to the proposals CSV, for testing. |
A data frame of proposals for that workspace, excluding channels that do not exist yet.
Callers resolving several channel names at once (e.g.
welcome_message_render()'s starter-channel list) should call this once
and look names up in the result, rather than calling
slack_channel_id_lookup() per name - each call independently checks the
KV cache, so looking up N names one-by-one means N redundant KV reads
(and N cache-miss races) for data that doesn't change between them.
channel_index_load( team_id, workspace, namespace_id = slack_tokens_namespace_id(), account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), api_token = Sys.getenv("CLOUDFLARE_API_TOKEN") )channel_index_load( team_id, workspace, namespace_id = slack_tokens_namespace_id(), account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), api_token = Sys.getenv("CLOUDFLARE_API_TOKEN") )
team_id |
Slack team id. |
workspace |
|
namespace_id |
KV namespace ID for |
account_id |
Cloudflare account ID. Defaults to env
|
api_token |
Cloudflare API token. Defaults to env
|
A named list mapping channel name to channel id, or NULL if
the channel list couldn't be fetched.
Lists the community workspace's public channels, keeps those with a description, picks the next one round-robin (state stored in KV), and drafts a Jinx-voiced invitation for it - falling back to the plain description if the model call fails.
channel_promo_build( team_id = Sys.getenv("SLACK_COMMUNITY_TEAM_ID"), target_channel = env_default("SLACK_PROMO_CHANNEL", "general"), skip = promo_skip_channels(), namespace_id = slack_tokens_namespace_id(), account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), api_token = Sys.getenv("CLOUDFLARE_API_TOKEN"), model = workers_ai_chat_model() )channel_promo_build( team_id = Sys.getenv("SLACK_COMMUNITY_TEAM_ID"), target_channel = env_default("SLACK_PROMO_CHANNEL", "general"), skip = promo_skip_channels(), namespace_id = slack_tokens_namespace_id(), account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), api_token = Sys.getenv("CLOUDFLARE_API_TOKEN"), model = workers_ai_chat_model() )
team_id |
Community Slack team id. Defaults to env
|
target_channel |
Channel the spotlight is posted to (and never
promotes). Defaults to env |
skip |
Additional channel names to exclude. Defaults to the
comma-separated env |
namespace_id |
KV namespace ID for |
account_id |
Cloudflare account ID. Defaults to env
|
api_token |
Cloudflare API token. Defaults to env
|
model |
Workers AI chat model used for drafting. |
A list with text (the Slack message), channel_id,
channel_name, and recent (the updated round-robin state to persist),
or NULL when no channel is eligible.
Links the featured channel in the header and appends the invitation. The
blurb is escaped with escape_markdown() before it goes out: a channel's
description is user-controlled and flows through the model into a broadcast
message, so this neutralises injected links and <!channel>/<!everyone>
mass-pings while leaving *bold*/_italic_ intact.
channel_promo_format(id, name, blurb)channel_promo_format(id, name, blurb)
id |
Featured channel id. |
name |
Featured channel name (without |
blurb |
Invitation text from |
Character scalar Slack mrkdwn message.
Posts one channel's spotlight to the target channel, then records the pick in KV so next week's run features a different channel. The round-robin state is only advanced after a successful post, so a failed run doesn't burn a channel's turn.
channel_promo_post( team_id = Sys.getenv("SLACK_COMMUNITY_TEAM_ID"), target_channel = env_default("SLACK_PROMO_CHANNEL", "general"), slack_token = slack_bot_token("community"), skip = promo_skip_channels(), namespace_id = slack_tokens_namespace_id(), account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), api_token = Sys.getenv("CLOUDFLARE_API_TOKEN"), model = workers_ai_chat_model() )channel_promo_post( team_id = Sys.getenv("SLACK_COMMUNITY_TEAM_ID"), target_channel = env_default("SLACK_PROMO_CHANNEL", "general"), slack_token = slack_bot_token("community"), skip = promo_skip_channels(), namespace_id = slack_tokens_namespace_id(), account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), api_token = Sys.getenv("CLOUDFLARE_API_TOKEN"), model = workers_ai_chat_model() )
team_id |
Community Slack team id. Defaults to env
|
target_channel |
Channel the spotlight is posted to (and never
promotes). Defaults to env |
slack_token |
Community bot token. Defaults to
|
skip |
Additional channel names to exclude. Defaults to the
comma-separated env |
namespace_id |
KV namespace ID for |
account_id |
Cloudflare account ID. Defaults to env
|
api_token |
Cloudflare API token. Defaults to env
|
model |
Workers AI chat model used for drafting. |
Invisibly, TRUE if a spotlight was posted, FALSE if there was
nothing eligible to promote.
Read the reviewed channel rename proposals
channel_rename_proposals(workspace = c("organiser", "community"), path = NULL)channel_rename_proposals(workspace = c("organiser", "community"), path = NULL)
workspace |
Either |
path |
Optional path to the proposals CSV, for testing. |
A data frame of old/new channel names for that workspace.
Identifies chapters that have been inactive (no events) for a specified number of months.
chapter_check_health( months = 12, org = "rladies", data_repo = "meetup_archive" )chapter_check_health( months = 12, org = "rladies", data_repo = "meetup_archive" )
months |
Number of months without activity to consider inactive. Defaults to 12. |
org |
GitHub organization. Defaults to |
data_repo |
Repository containing chapter/event data.
Defaults to |
A data frame with columns chapter, last_event, status,
and months_inactive.
Tick a checklist item on an onboarding issue
chapter_checklist_tick( issue_number, pattern, org = "rladies", onboarding_repo = "new-chapters-onboarding" )chapter_checklist_tick( issue_number, pattern, org = "rladies", onboarding_repo = "new-chapters-onboarding" )
issue_number |
Onboarding issue number. |
pattern |
Text identifying the item to tick. |
org |
GitHub organization. Defaults to |
onboarding_repo |
Repository holding onboarding issues. |
TRUE when an item was ticked, FALSE otherwise.
Sends the confirmation email from [email protected], cc'ing
[email protected] so the onboarding inbox keeps the thread, and
notes on the issue that it went out.
chapter_confirm_send( issue_number, to, city = NULL, email = NULL, meetup_url = NULL, force = FALSE, org = "rladies", onboarding_repo = "new-chapters-onboarding" )chapter_confirm_send( issue_number, to, city = NULL, email = NULL, meetup_url = NULL, force = FALSE, org = "rladies", onboarding_repo = "new-chapters-onboarding" )
issue_number |
Onboarding issue number. |
to |
Organiser email addresses. |
city |
Chapter city. Read from the issue when |
email |
Chapter email address to quote in the message. |
meetup_url |
Meetup URL to quote in the message. |
force |
Send even with checklist steps outstanding. |
org |
GitHub organization. Defaults to |
onboarding_repo |
Repository holding onboarding issues. |
Refuses to send while checklist steps are outstanding, since the email
tells the organisers their chapter is set up. Pass force = TRUE to
override, which is occasionally right - a step may be recorded
elsewhere - but it should be a decision rather than an accident.
Recipient addresses are passed in rather than read from the issue: organiser emails are deliberately not stored in the issue metadata, and this function does not put them back there either. The issue comment records how many organisers were written to, not who.
The sent message id (invisibly).
Generates a chapter JSON entry for the website.
chapter_create( city, country, organizers, region = NULL, social_media = list(), status = "prospective", output_dir = "." )chapter_create( city, country, organizers, region = NULL, social_media = list(), status = "prospective", output_dir = "." )
city |
City name. |
country |
Country name. |
organizers |
Character vector of organizer names. |
region |
State, region or province, or |
social_media |
Named list of social media links (e.g.
|
status |
Chapter status. Defaults to |
output_dir |
Directory to write the JSON file. |
File path of the created JSON (invisibly).
Generates the chapter JSON entry, validates it against the bundled schema, and opens a PR adding it to the website. A prospective chapter has no Meetup group or chapter email yet, so both are optional and are simply left out of the entry.
chapter_create_pr( city, country, region = NULL, meetup_urlname = NULL, email = NULL, organizers = character(0), status = "prospective", social_media = list(), org = "rladies", website_repo = "rladies.github.io", team_reviewers = "leadership" )chapter_create_pr( city, country, region = NULL, meetup_urlname = NULL, email = NULL, organizers = character(0), status = "prospective", social_media = list(), org = "rladies", website_repo = "rladies.github.io", team_reviewers = "leadership" )
city |
Chapter city. |
country |
Chapter country. |
region |
State/region/province (optional). |
meetup_urlname |
Meetup group URL name, or |
email |
Chapter email address, or |
organizers |
Character vector of organizer names. |
status |
Chapter status. Defaults to |
social_media |
Named list of social media handles (optional). |
org |
GitHub organization. Defaults to |
website_repo |
Website repository name. |
team_reviewers |
Teams to request review from. Defaults to
|
PR URL (invisibly).
Opens a tracking issue in the new-chapters-onboarding repo with the full checklist for setting up a new RLadies+ chapter.
chapter_create_setup( city, country, organizers, region = NULL, org = "rladies", onboarding_repo = "new-chapters-onboarding" )chapter_create_setup( city, country, organizers, region = NULL, org = "rladies", onboarding_repo = "new-chapters-onboarding" )
city |
Chapter city name. |
country |
Chapter country. |
organizers |
Character vector of organizer names. |
region |
State/region/province, or |
org |
GitHub organization. Defaults to |
onboarding_repo |
Repository for chapter onboarding issues. |
Issue URL (invisibly).
Opens a tracking issue for updating an existing chapter's infrastructure.
chapter_create_update( city, country, org = "rladies", onboarding_repo = "new-chapters-onboarding" )chapter_create_update( city, country, org = "rladies", onboarding_repo = "new-chapters-onboarding" )
city |
Chapter city name. |
country |
Chapter country. |
org |
GitHub organization. Defaults to |
onboarding_repo |
Repository for chapter onboarding issues. |
Issue URL (invisibly).
Performs the first step of the chapter onboarding checklist: searches
the website's data/chapters entries for a chapter in the same city,
for chapters with a near-identical city name, and for chapters in the
same country within radius_km of the requested city.
chapter_duplicate_check( city, country, region = NULL, radius_km = 100, org = "rladies", website_repo = "rladies.github.io" )chapter_duplicate_check( city, country, region = NULL, radius_km = 100, org = "rladies", website_repo = "rladies.github.io" )
city |
Requested chapter city. |
country |
Requested chapter country. |
region |
State/region/province, or |
radius_km |
Radius in kilometres for the proximity search. Set to
|
org |
GitHub organization. Defaults to |
website_repo |
Repository holding |
Only chapters in the requested country, or whose filename contains the requested city slug, are fetched in full, so the check costs one directory listing plus a handful of file reads.
A data frame with one row per match and columns city,
country, status, email, meetup, match, and distance_km.
match is one of "exact", "similar", or "nearby".
Runs chapter_duplicate_check() and comments the rendered report on
the issue. Failures are reported but never abort onboarding: the
check is an aid to the team, not a gate.
chapter_duplicate_comment( issue_number, city, country, region = NULL, org = "rladies", onboarding_repo = "new-chapters-onboarding" )chapter_duplicate_comment( issue_number, city, country, region = NULL, org = "rladies", onboarding_repo = "new-chapters-onboarding" )
issue_number |
Onboarding issue number. |
city |
Requested chapter city. |
country |
Requested chapter country. |
region |
State/region/province, or |
org |
GitHub organization. Defaults to |
onboarding_repo |
Repository holding onboarding issues. |
The rendered report (invisibly), or NULL on failure.
Turns the output of chapter_duplicate_check() into the markdown the
onboarding team reads first: whether the city already has a chapter,
which nearby chapters should be cc'd, and which names are close enough
to be worth a second look.
chapter_duplicate_report(matches, city, country, radius_km = 100)chapter_duplicate_report(matches, city, country, radius_km = 100)
matches |
Data frame from |
city |
Requested chapter city. |
country |
Requested chapter country. |
radius_km |
Radius used for the proximity search. |
Markdown body as a single string.
An approval is a comment containing the approval marker, written by a confirmed member of an approving team. Bot comments never count, so jinx cannot approve its own work.
chapter_email_approvals( issue_number, org = "rladies", onboarding_repo = "new-chapters-onboarding" )chapter_email_approvals( issue_number, org = "rladies", onboarding_repo = "new-chapters-onboarding" )
issue_number |
Onboarding issue number. |
org |
GitHub organization. Defaults to |
onboarding_repo |
Repository holding onboarding issues. |
Character vector of approver logins, possibly empty.
Reads the chapter from the onboarding issue's own machine-readable block, checks that a member of an approving team has left an approval comment, and only then asks the worker to create the mailbox. The address is posted back on the issue and the email step of the checklist is ticked.
chapter_email_provision( issue_number, org = "rladies", onboarding_repo = "new-chapters-onboarding" )chapter_email_provision( issue_number, org = "rladies", onboarding_repo = "new-chapters-onboarding" )
issue_number |
Onboarding issue number. |
org |
GitHub organization. Defaults to |
onboarding_repo |
Repository holding onboarding issues. |
The approval is a separate, deliberate act on the specific issue: being allowed to run the command is not on its own enough to create an account, so a stray dispatch or a mistaken command cannot provision anything.
The created address (invisibly).
Matches on the issue title, which onboarding issues open as
"
chapter_find_issue( city, org = "rladies", onboarding_repo = "new-chapters-onboarding" )chapter_find_issue( city, org = "rladies", onboarding_repo = "new-chapters-onboarding" )
city |
Chapter city. |
org |
GitHub organization. Defaults to |
onboarding_repo |
Repository holding onboarding issues. |
The issue number, or NULL when no open issue matches.
Used as the start of the six-month window in which a chapter with no events counts as unbegun rather than inactive.
chapter_initiated_date(files, repo = ".", bulk_import = as.Date("2023-01-04"))chapter_initiated_date(files, repo = ".", bulk_import = as.Date("2023-01-04"))
files |
Chapter file paths, relative to |
repo |
Path to the website repository. |
bulk_import |
Date of the bulk import to treat as unknown, or
|
Two things make this fragile, and both are handled here. Renaming a
chapter file would otherwise reset its date to the rename, so the
lookup follows renames. A shallow clone has no history to read, so
the caller's checkout needs fetch-depth: 0; without it every
chapter looks as though it appeared today.
Dates before bulk_import are not meaningful: 118 chapters were
added to the website in one import on 2023-01-04, so that date
records the import rather than when any of those chapters started.
Those come back as NA.
A Date vector, NA where the date is unknown or untrusted.
Asks the jinx worker to provision <city>@rladies.org in the
/Chapters organisational unit. The Workspace service account key
lives in the worker rather than in CI, so this call carries only the
worker API key: the most a stolen CI token can do is ask for a
mailbox whose name passes the worker's validation.
chapter_mailbox_create( city, given_name = NULL, family_name = NULL, base_url = "https://jinx.rladies.org", api_key = worker_api_key() )chapter_mailbox_create( city, given_name = NULL, family_name = NULL, base_url = "https://jinx.rladies.org", api_key = worker_api_key() )
city |
Chapter city. The worker transliterates and validates it, and refuses reserved addresses. |
given_name |
Given name on the account. Defaults to |
family_name |
Family name on the account. Defaults to the city. |
base_url |
Worker base URL. |
api_key |
Worker API key. Defaults to the |
The signing and Directory API calls could be done here in R - openssl
signs RS256 perfectly well - but that would require the service account
key on the Actions runner that runs this function, which is the thing
the current arrangement avoids. worker/src/workspace.js carries the
full reasoning; it is a deliberate exception to moving worker logic
into this package.
The generated password is never returned. The account is created with change-password-at-next-login set, and the onboarding team issues the handover from the Admin console.
The created address (invisibly).
Everything the Meetup Pro team needs in one comment: the exact group name and URL, the settings the organisational guidelines call for, and the standard description ready to paste.
chapter_meetup_brief(city, country, status = NULL)chapter_meetup_brief(city, country, status = NULL)
city |
Chapter city. |
country |
Chapter country. |
status |
Result of |
Markdown body as a single string.
The website's data/chapters entries are the source of truth for
which chapters exist and which Meetup group each uses, so the sync
derives its list from there rather than from a hand-maintained one
that would drift the day it was written.
chapter_meetup_groups( org = "rladies", website_repo = "rladies.github.io", statuses = c("active", "prospective") )chapter_meetup_groups( org = "rladies", website_repo = "rladies.github.io", statuses = c("active", "prospective") )
org |
GitHub organization. Defaults to |
website_repo |
Repository holding |
statuses |
Chapter statuses to include. |
Retired chapters are excluded by default: they have no upcoming events and querying them only spends requests.
A data frame with columns file, city, country,
status, and urlname.
Meetup uploads in two steps: a mutation registers the photo and
returns a signed uploadUrl, then the bytes are sent to that URL. The
mutation is named for event photos, but photoType carries a
GROUP_PHOTO value and setAsMain makes it the group's image.
chapter_meetup_logo_upload(urlname, image = chapter_meetup_logo_path())chapter_meetup_logo_upload(urlname, image = chapter_meetup_logo_path())
urlname |
Meetup group urlname. |
image |
Path to the image. Defaults to the bundled RLadies+ profile logo. |
The uploaded image path reported by Meetup (invisibly).
The Meetup group name for a chapter
chapter_meetup_name(city)chapter_meetup_name(city)
city |
Chapter city, used as written apart from trimming. |
The group name.
Reads the chapter from the issue's own machine-readable block, so in the usual case only the issue number is needed.
chapter_meetup_request( issue_number, city = NULL, country = NULL, org = "rladies", onboarding_repo = "new-chapters-onboarding" )chapter_meetup_request( issue_number, city = NULL, country = NULL, org = "rladies", onboarding_repo = "new-chapters-onboarding" )
issue_number |
Onboarding issue number. |
city |
Chapter city. Read from the issue when |
country |
Chapter country. Read from the issue when |
org |
GitHub organization. Defaults to |
onboarding_repo |
Repository holding onboarding issues. |
The rendered brief (invisibly).
meetup.com/rladies-<city>, hyphenated for multi-word cities and
transliterated the way the website filenames already are. This value
and the group name cannot be changed after the group is created, so
getting them right first time is the whole point of rendering them
here rather than having someone type them.
chapter_meetup_urlname(city)chapter_meetup_urlname(city)
city |
Chapter city. |
The urlname, without the meetup.com prefix.
Read the machine-readable block off an onboarding issue
chapter_meta_fetch( issue_number, org = "rladies", onboarding_repo = "new-chapters-onboarding" )chapter_meta_fetch( issue_number, org = "rladies", onboarding_repo = "new-chapters-onboarding" )
issue_number |
Onboarding issue number. |
org |
GitHub organization. Defaults to |
onboarding_repo |
Repository holding onboarding issues. |
A named list of chapter details, or NULL when the issue
carries no block.
Determines chapter status based on meetup event data. Chapters are classified as active, inactive, or unbegun based on their last event and founding date.
chapter_monitor_status(data_path = NULL, inactive_months = 6, org = "rladies")chapter_monitor_status(data_path = NULL, inactive_months = 6, org = "rladies")
data_path |
Path to a CSV export from Meetup Pro Dashboard,
or |
inactive_months |
Months without events to consider inactive. Defaults to 6. |
org |
GitHub organization. |
Data frame with chapter status information.
The owner of the first unchecked item, or NA when the checklist is
complete or carries no owner.
chapter_onboard_blocker(state)chapter_onboard_blocker(state)
state |
Data frame from |
A team slug such as "rladies/email", or NA_character_.
Parses the task list in an onboarding issue body into one row per checklist item, carrying the section it sits under and the team that section says is responsible for it. Nested items inherit their parent's owner.
chapter_onboard_state( issue_number, org = "rladies", onboarding_repo = "new-chapters-onboarding" )chapter_onboard_state( issue_number, org = "rladies", onboarding_repo = "new-chapters-onboarding" )
issue_number |
Onboarding issue number. |
org |
GitHub organization. Defaults to |
onboarding_repo |
Repository holding onboarding issues. |
A data frame with columns section, owner, item, and
done.
Reports progress per section, names the next unchecked item and who owns it, and says plainly when everything is done.
chapter_onboard_summary(state, title = "Chapter onboarding")chapter_onboard_summary(state, title = "Chapter onboarding")
state |
Data frame from |
title |
Chapter title to put in the heading. |
Markdown body as a single string.
Opens the website PR with the chapter's prospective entry, requests review from the leadership team, links the PR on the onboarding issue, and ticks the website step of the checklist.
chapter_onboard_website( issue_number, city = NULL, country = NULL, region = NULL, organizers = NULL, org = "rladies", onboarding_repo = "new-chapters-onboarding", website_repo = "rladies.github.io" )chapter_onboard_website( issue_number, city = NULL, country = NULL, region = NULL, organizers = NULL, org = "rladies", onboarding_repo = "new-chapters-onboarding", website_repo = "rladies.github.io" )
issue_number |
Onboarding issue number. |
city |
Chapter city. Read from the issue when |
country |
Chapter country. Read from the issue when |
region |
State/region/province. Read from the issue when |
organizers |
Character vector of organizer names. Read from the
issue when |
org |
GitHub organization. Defaults to |
onboarding_repo |
Repository holding onboarding issues. |
website_repo |
Website repository name. |
A prospective entry deliberately carries no chapter email and no Meetup group: neither exists at this point in onboarding, and the entry is filled in as those steps complete.
Anything left NULL is read from the issue's own machine-readable
block, so in the usual case only the issue number is needed.
The website PR URL (invisibly).
The form's chapter field is free text and inconsistently written -
"Cologne, Germany" beside "Wake Forest, North Carolina EE.UU" - so
matching is by city slug appearing anywhere in it, and the caller is
expected to show a human what was found rather than act on it.
chapter_organiser_submissions(city, api_key = Sys.getenv("AIRTABLE_API_KEY"))chapter_organiser_submissions(city, api_key = Sys.getenv("AIRTABLE_API_KEY"))
city |
Chapter city. |
api_key |
Airtable API key. |
A data frame with record_id, name, chapter, status,
and type; empty when nothing matches.
Finds open onboarding issues with no activity for days days and
comments on each, naming the team that owns the first unchecked step
rather than pinging everyone. Onboardings stall on the email and
Meetup Pro teams more than anywhere else, so the ping is targeted.
chapter_remind_stale( org = "rladies", onboarding_repo = "new-chapters-onboarding", days = 14, labels = c("new chapter", "chapter update") )chapter_remind_stale( org = "rladies", onboarding_repo = "new-chapters-onboarding", days = 14, labels = c("new chapter", "chapter update") )
org |
GitHub organization. Defaults to |
onboarding_repo |
Repository holding onboarding issues. |
days |
Days without activity before nudging. Defaults to 14. |
labels |
Issue labels to consider. |
Invisibly, a list of the issues nudged.
Each chapter may carry a social_media$github value. Two shapes are
legitimate: owner/repo for a chapter that keeps its files in a
repository, and a bare organisation name for a chapter that runs its
own GitHub organisation. Anything that resolves to neither is a dead
link on the chapter's page.
chapter_repo_audit(chapters_dir, org = "rladies")chapter_repo_audit(chapters_dir, org = "rladies")
chapters_dir |
Directory of chapter JSON files. |
org |
Organisation the conventional repository would live in. |
Where a reference is dead and the conventional
<org>/meetup-presentations_<city> repository exists, that is
reported as the likely correction.
A data frame with columns file, github, state and
suggestion. state is "repo", "org", "url", "prefixed"
or "missing". A "url" row resolves but is written as a full
URL; a "prefixed" row is an organisation the chapter owns,
written as though it were a repository under org. Both carry the
same reference in the expected shape as their suggestion. Only
rows needing attention are returned.
Render a chapter GitHub audit as markdown
chapter_repo_audit_report(audit)chapter_repo_audit_report(audit)
audit |
Data frame from |
A markdown string.
Opt-in: not every chapter wants one, and only 111 of the 122 existing teams have a repository, so this is never called as part of onboarding without someone asking for it.
chapter_repo_create(team_slug, city, country, region = NULL, org = "rladies")chapter_repo_create(team_slug, city, country, region = NULL, org = "rladies")
team_slug |
Team slug, from |
city |
Chapter city. |
country |
Chapter country. |
region |
State/region/province, or |
org |
GitHub organization. Defaults to |
The repository is public and the chapter's team is given admin,
matching every existing chapter repository - organisers administer
their own.
The repository name (invisibly).
meetup-presentations_<team slug>, matching the 127 repositories the
org already has. Hyphens are kept: san-diego, sao-paulo and
kansas-city all have them, and sanfrancisco is the outlier rather
than the rule.
chapter_repo_name(team_slug)chapter_repo_name(team_slug)
team_slug |
Team slug, from |
The repository name.
Analyzes chapter activity data and returns a formatted markdown summary.
chapter_report_health(months = 6, org = "rladies")chapter_report_health(months = 6, org = "rladies")
months |
Inactivity threshold in months. Defaults to 6. |
org |
GitHub organization. Defaults to |
Markdown report body (invisibly), or NULL if no data.
Prompt the onboarding issue for the organiser Slack invite
chapter_slack_prompt( issue_number, city = NULL, org = "rladies", onboarding_repo = "new-chapters-onboarding" )chapter_slack_prompt( issue_number, city = NULL, org = "rladies", onboarding_repo = "new-chapters-onboarding" )
issue_number |
Onboarding issue number. |
city |
Chapter city. Read from the issue when |
org |
GitHub organization. Defaults to |
onboarding_repo |
Repository holding onboarding issues. |
The posted body (invisibly).
Record that the organisers Slack invite was sent
chapter_slack_sent( issue_number, org = "rladies", onboarding_repo = "new-chapters-onboarding" )chapter_slack_sent( issue_number, org = "rladies", onboarding_repo = "new-chapters-onboarding" )
issue_number |
Onboarding issue number. |
org |
GitHub organization. Defaults to |
onboarding_repo |
Repository holding onboarding issues. |
TRUE when the checklist step was ticked.
The website's status field is set by hand, so it drifts from what
the chapters are actually doing. This compares it against the event
history in meetup_archive and reports the two mismatches worth
acting on: a chapter running events that is not marked active, and a
chapter marked active that has gone quiet.
chapter_status_reconcile( chapters_dir, health = NULL, months = 6, retire_months = 12, initiated = NULL )chapter_status_reconcile( chapters_dir, health = NULL, months = 6, retire_months = 12, initiated = NULL )
chapters_dir |
Directory of chapter JSON files, typically
|
health |
Output of |
months |
Months without an event before a chapter is flagged. Defaults to 6, matching the guide. |
retire_months |
Months without an event before a chapter should simply be marked inactive. Defaults to 12. |
initiated |
Optional |
The categories follow the guide: a chapter is active when it has run
an event in the last months, and inactive when it has not.
Chapters create their events on Meetup even when they promote them elsewhere, precisely so activity can be tracked, so an absence of Meetup events is a real signal rather than an artefact of where a chapter advertises. That holds for every chapter, so one with no Meetup group at all is included here rather than skipped: having no group means it never began, or stopped before it started.
A data frame with one row per chapter that has a urlname,
A data frame with one row per chapter, with columns file,
urlname, stored, last_event, months_inactive and action.
action is "promote to active", "flag quiet",
"mark inactive", or NA when nothing needs doing.
Render a status reconciliation as markdown
chapter_status_reconcile_report(reconciled, months = 6)chapter_status_reconcile_report(reconciled, months = 6)
reconciled |
Data frame from |
months |
Months used for the inactivity window, for the wording. |
A markdown string.
Accepts either an issue number or a city name, and renders the
checklist summary that /jinx chapter-status replies with.
chapter_status_report( ref, org = "rladies", onboarding_repo = "new-chapters-onboarding" )chapter_status_report( ref, org = "rladies", onboarding_repo = "new-chapters-onboarding" )
ref |
Issue number, or a city name to look one up by. |
org |
GitHub organization. Defaults to |
onboarding_repo |
Repository holding onboarding issues. |
Markdown body as a single string.
Reports rather than repairs. Renaming a team changes its URL and where its notifications go, so which of these to act on is a human decision.
chapter_team_audit( org = "rladies", website_repo = "rladies.github.io", parent_team = "chapters", statuses = "active" )chapter_team_audit( org = "rladies", website_repo = "rladies.github.io", parent_team = "chapters", statuses = "active" )
org |
GitHub organization. Defaults to |
website_repo |
Repository holding |
parent_team |
Slug of the parent team. |
statuses |
Chapter statuses expected to have a team. |
Each row is one of:
ok - the team matches a chapter's urlname
slug_mismatch - a chapter matches by city, but its urlname would
give a different slug
no_chapter - no chapter in the website data explains this team
missing_team - an active chapter with a Meetup group and no team
A data frame with finding, team, display, chapter, and
expected_slug.
Render a team audit as markdown
chapter_team_audit_report(audit)chapter_team_audit_report(audit)
audit |
Data frame from |
Markdown body as a single string.
Creates the team nested under the chapters parent, named so that its
slug matches the chapter's Meetup urlname, with the chapter spelled
out in the description.
chapter_team_create( urlname, city, country, region = NULL, org = "rladies", parent_team = "chapters", overrides = character(0) )chapter_team_create( urlname, city, country, region = NULL, org = "rladies", parent_team = "chapters", overrides = character(0) )
urlname |
Meetup urlname for the chapter. |
city |
Chapter city. |
country |
Chapter country. |
region |
State/region/province, or |
org |
GitHub organization. Defaults to |
parent_team |
Slug of the parent team. |
overrides |
Passed to |
The created team's slug (invisibly).
Free text, so unlike the name it can spell the chapter out in full without changing the slug.
chapter_team_description(city, country, region = NULL)chapter_team_description(city, country, region = NULL)
city |
Chapter city. |
country |
Chapter country. |
region |
State/region/province, or |
The description.
GitHub derives a team's slug from its name and gives no way to set the
slug directly, so the name has to be chosen such that it slugifies
back to the chapter's urlname. That rules out "Portland, Oregon, USA"
as a name - it would produce portland-oregon-usa rather than pdx.
chapter_team_name(slug, acronyms = chapter_team_acronyms())chapter_team_name(slug, acronyms = chapter_team_acronyms())
slug |
Team slug, from |
acronyms |
Slugs to render upper case rather than title case. |
The readable description of the chapter goes in the team's description instead, which is free text and does not affect the slug.
The display name.
The Meetup urlname, minus the rladies- prefix. This is the
convention 108 of the 122 existing chapter teams already follow, and
it is the only chapter field that is unique: seven city names are
shared by more than one chapter, and it is the urlname that separates
them - london from ldnont, pdx from portland-maine.
chapter_team_slug(urlname, overrides = character(0))chapter_team_slug(urlname, overrides = character(0))
urlname |
Meetup urlname, with or without the |
overrides |
Named character vector mapping a urlname to a slug,
for chapters whose urlname makes a poor team name. Warsaw's is
|
The team slug, or NA_character_ when there is no urlname.
Runs the checks that keep data/chapters/*.json on the website
consistent with the conventions jinx relies on elsewhere: the schema,
the filename derivation, and the agreement between urlname and
social_media$meetup.
chapter_validate_files(paths, added = character())chapter_validate_files(paths, added = character())
paths |
Character vector of paths to chapter JSON files. |
added |
Character vector of paths (a subset of |
Designed to run over the files changed in a pull request, so that pre-existing drift in untouched files does not block unrelated work.
A data frame with columns file, level ("error" or
"warning"), check, and message. Zero rows when everything
passes.
Render chapter validation issues as markdown
chapter_validate_report(issues, n_files = NA_integer_)chapter_validate_report(issues, n_files = NA_integer_)
issues |
Data frame returned by |
n_files |
Number of files that were checked. |
A markdown string suitable for a pull request comment.
Splits a markdown document by H1/H2 sections, then packs paragraphs
into chunks of roughly target_chars characters. Returns a list of
chunk records with heading, title, and lastmod metadata.
chunk_markdown(markdown, meta, target_chars = 1800, min_chars = 200)chunk_markdown(markdown, meta, target_chars = 1800, min_chars = 200)
markdown |
Markdown text (may include YAML frontmatter). |
meta |
Named list with |
target_chars |
Approximate target chunk size. |
min_chars |
Minimum chunk size; smaller chunks are dropped. |
List of chunk records (one list per chunk).
Fails if the token has access to zero or more than one account.
cloudflare_account_id(api_token)cloudflare_account_id(api_token)
api_token |
Cloudflare API token. |
Account ID string.
Calls the Cloudflare REST endpoint
accounts/{id}/ai/run/{model} and returns the embedding vectors.
Custom because cloudflarer
does not wrap Workers AI inference; built on its cloudflarer::cf_request()
and cloudflarer::cf_resp() for consistent auth and error handling.
cloudflare_embed( texts, account_id, api_token, model = "@cf/baai/bge-base-en-v1.5" )cloudflare_embed( texts, account_id, api_token, model = "@cf/baai/bge-base-en-v1.5" )
texts |
Character vector of texts. |
account_id |
Cloudflare account ID. |
api_token |
Cloudflare API token. |
model |
Workers AI embedding model. |
List of numeric vectors, one per input text.
Calls the Cloudflare REST endpoint accounts/{id}/ai/run/{model} with a
chat-style messages array and returns the generated text. Sibling of
cloudflare_embed() - same auth/retry/error-handling idiom, built on
cloudflarer::cf_request() and cloudflarer::cf_resp() because
cloudflarer does not wrap Workers AI inference.
cloudflare_generate( messages, account_id, api_token, model = workers_ai_chat_model(), max_tokens = 256 )cloudflare_generate( messages, account_id, api_token, model = workers_ai_chat_model(), max_tokens = 256 )
messages |
List of chat messages, each |
account_id |
Cloudflare account ID. |
api_token |
Cloudflare API token. |
model |
Workers AI chat/instruct model. |
max_tokens |
Maximum tokens to generate. |
Character scalar with the model's response text.
Custom because cloudflarer
does not wrap Vectorize v2; built on its cloudflarer::cf_request() and
cloudflarer::cf_resp() for consistent auth and error handling.
cloudflare_vectorize_upsert(vectors, account_id, api_token, index_name)cloudflare_vectorize_upsert(vectors, account_id, api_token, index_name)
vectors |
List of vector records, each with |
account_id |
Cloudflare account ID. |
api_token |
Cloudflare API token. |
index_name |
Vectorize index name. |
The unwrapped result payload from the Cloudflare response.
A handful of dispatched commands ("feedback", "setup-channel") act on
the Slack workspace/channel the command was run from, rather than only on
fields parsed from the command text itself. Owning that merge here -
mirroring how event_parse() owns shaping the slack-event payload -
keeps the calling workflow YAML a thin, untested plumbing layer instead
of the place command semantics live.
cmd_attach_slack_context(command, team_id, channel_id, channel_name)cmd_attach_slack_context(command, team_id, channel_id, channel_name)
command |
Parsed command list from |
team_id |
Slack team id the command was run from. |
channel_id |
Slack channel id the command was run from. |
channel_name |
Slack channel display name. |
command with team_id/channel_id/channel_name added, or
NULL unchanged if command was NULL.
Privileged commands (those with a jinx_gated keyword in the command
registry, i.e. anything not labelled jinx_safe) may only be run by
members of the global team, identified via the Airtable member
directory. Read-only commands are always allowed. The check fails
closed: an unknown actor is denied, and a directory lookup error is
also denied (with a distinct "try again" message).
cmd_authorize( command, actor = NULL, source = c("github", "slack"), workspace = NULL, api_key = Sys.getenv("AIRTABLE_API_KEY") )cmd_authorize( command, actor = NULL, source = c("github", "slack"), workspace = NULL, api_key = Sys.getenv("AIRTABLE_API_KEY") )
command |
Parsed command list from |
actor |
The requesting actor: a GitHub login when |
source |
Origin of the command, |
workspace |
Originating Slack workspace for |
api_key |
Airtable API key used for the directory lookup. |
Slack identity is only trusted in the organisers workspace. The
community workspace is openly joinable and its user ids come from a
different Slack team, so an id from there must not authorize privileged
actions; privileged Slack commands are only honored when workspace is
"organiser".
A list with ok (logical) and message (a refusal string
when ok is FALSE, otherwise NULL).
Returns the response message as a character string. The caller is responsible for routing the message to the right destination (GitHub issue comment, Slack, R console, etc.).
cmd_execute(command)cmd_execute(command)
command |
Parsed command list from |
Character string with the response message.
Parses commands like:
/jinx invite @username to website
/jinx offboard @username from blog
/jinx report weekly
/jinx help
cmd_parse(body)cmd_parse(body)
body |
Character string, the comment body. |
A named list with action and action-specific fields, or NULL
if the comment is not a jinx command.
Summarizes active CFPs, speaker recommendations, and upcoming deadlines.
conference_generate_report(org = "rladies", repo = "global-team")conference_generate_report(org = "rladies", repo = "global-team")
org |
GitHub organization. |
repo |
Repository where CFP issues are tracked. |
Formatted markdown string.
List speaker recommendations for a conference
conference_list_speakers(conference, org = "rladies", repo = "global-team")conference_list_speakers(conference, org = "rladies", repo = "global-team")
conference |
Conference name. |
org |
GitHub organization. |
repo |
Repository where CFP issues are tracked. |
Data frame with columns: speaker, expertise, recommended_by.
Creates a markdown table or image grid of contributors for inclusion in README or other documents.
contributor_format(contributors, format = c("table", "grid"), cols = 7)contributor_format(contributors, format = c("table", "grid"), cols = 7)
contributors |
Data frame from |
format |
Output format: |
cols |
Number of columns for grid format. Defaults to 7. |
Character string with markdown content.
Collects all contributors from merged PRs and commits, returning a deduplicated list with contribution counts.
contributor_list(owner, repo, include_bots = FALSE)contributor_list(owner, repo, include_bots = FALSE)
owner |
Repository owner. |
repo |
Repository name. |
include_bots |
Whether to include bot accounts. Defaults to |
Data frame with login, contributions, avatar_url, and
profile_url columns, sorted by contribution count.
Aggregates contributor data across all non-meetup repos in the org.
contributor_list_org(org = "rladies", exclude_pattern = "^meetup-")contributor_list_org(org = "rladies", exclude_pattern = "^meetup-")
org |
GitHub organization. |
exclude_pattern |
Regex to exclude repos. |
Data frame with login, repos, contributions,
avatar_url, and profile_url. contributions is the sum across
all repos; repos is the count of distinct repos contributed to.
Fetches contributors and commits the rendered list directly to the default branch. No PR is opened.
contributor_update( owner, repo, file_path = ".github/contributors.md", format = "grid", branch = "main" )contributor_update( owner, repo, file_path = ".github/contributors.md", format = "grid", branch = "main" )
owner |
Repository owner. |
repo |
Repository name. |
file_path |
Path in the repo to update with the contributor list.
Defaults to |
format |
Format for the contributor list. |
branch |
Branch to commit to. Defaults to |
Commit URL if changes were made, NULL otherwise (invisibly).
Request GitHub Copilot as a reviewer on a pull request
copilot_request_review( owner, repo, pr_number, reviewer = copilot_reviewer_login() )copilot_request_review( owner, repo, pr_number, reviewer = copilot_reviewer_login() )
owner |
Repository owner. |
repo |
Repository name. |
pr_number |
Pull request number. |
reviewer |
Copilot reviewer login. |
TRUE if the request was accepted, FALSE otherwise.
Requests Copilot as a reviewer and posts a short scoping comment naming
the grimoire gate. Copilot then applies the instruction files synced by
copilot_sync_repo(). Returns a status message suitable for replying to
the requester.
copilot_review_pr( owner, repo, pr_number, gate = NULL, comment = TRUE, config = load_copilot_review_config() )copilot_review_pr( owner, repo, pr_number, gate = NULL, comment = TRUE, config = load_copilot_review_config() )
owner |
Repository owner. |
repo |
Repository name. |
pr_number |
Pull request number. |
gate |
Gate key ( |
comment |
Whether to post the scoping comment. Defaults to |
config |
Config from |
A status string.
Fetches the four grimoire review-gate skills (plus the foundation brand
ruleset) and lands them in owner/repo as
.github/copilot-instructions.md and path-scoped
.github/instructions/*.instructions.md files, via a pull request.
This is what makes GitHub Copilot's code review grimoire-aware. Re-run
whenever grimoire changes to refresh the instructions.
copilot_sync_repo( owner = "rladies", repo, base = "main", branch = "jinx/copilot-review-instructions", config = load_copilot_review_config() )copilot_sync_repo( owner = "rladies", repo, base = "main", branch = "jinx/copilot-review-instructions", config = load_copilot_review_config() )
owner |
Target repository owner. Defaults to |
repo |
Target repository name. |
base |
Base branch. Defaults to |
branch |
Working branch. Defaults to
|
config |
Config from |
A list with status ("created" or "unchanged") and url
(the PR URL when created).
Uses magick to crop to a square and resize for web display.
directory_crop_image( path, width = 400, height = 400, gravity = "Center", output = path )directory_crop_image( path, width = 400, height = 400, gravity = "Center", output = path )
path |
Path to the image file. |
width |
Target width in pixels. Defaults to 400. |
height |
Target height in pixels. Defaults to 400. |
gravity |
Crop gravity. Defaults to |
output |
Output path. Defaults to overwriting the input. |
Output path (invisibly).
Reconciles Airtable against the directory and flips the
directory_synced_field() checkbox on every submission whose work is done,
so future syncs skip it:
directory_mark_synced( base_id = directory_base_id(), api_key = Sys.getenv("AIRTABLE_API_KEY"), org = "rladies", directory_repo = "directory" )directory_mark_synced( base_id = directory_base_id(), api_key = Sys.getenv("AIRTABLE_API_KEY"), org = "rladies", directory_repo = "directory" )
base_id |
Airtable base ID. Defaults to the directory base. |
api_key |
Airtable API key. Defaults to |
org |
GitHub organization. Defaults to |
directory_repo |
Directory repository name. |
an update is done once applying it to main would change nothing —
its entry is already fully incorporated (the same predicate the sync uses
to decide whether to commit);
a delete request is done once its entry file is absent from main —
the reviewed purge workflow has removed it.
Stateless and idempotent — safe to run on every sync-PR merge and after a purge completes.
Character vector of marked record ids (invisibly).
Resizes to max width and compresses.
directory_optimize_image(path, max_width = 800, quality = 85, output = path)directory_optimize_image(path, max_width = 800, quality = 85, output = path)
path |
Path to the image file. |
max_width |
Maximum width in pixels. Defaults to 800. |
quality |
JPEG quality (1-100). Defaults to 85. |
output |
Output path. Defaults to overwriting the input. |
Output path (invisibly).
GDPR right-to-erasure: after the purge workflow removes a member's entry from
the directory, every submissions row that resolves to one of slugs (by
directory_id, falling back to identifier) is deleted from Airtable so
no submitted PII remains — and so a later sync cannot re-create the entry
from a leftover row. Unlike directory_mark_synced(), which only flags the
synced checkbox, this destroys the rows.
directory_purge_submissions( slugs, base_id = directory_base_id(), api_key = Sys.getenv("AIRTABLE_API_KEY") )directory_purge_submissions( slugs, base_id = directory_base_id(), api_key = Sys.getenv("AIRTABLE_API_KEY") )
slugs |
Character vector of directory slugs to erase. |
base_id |
Airtable base ID. Defaults to the directory base. |
api_key |
Airtable API key. Defaults to the |
Character vector of deleted record ids (invisibly).
Fetches directory submissions from Airtable, transforms each eligible record into the directory entry schema, and opens (or updates) a PR on the directory repo containing only the entries whose content changed.
directory_sync_airtable( base_id = directory_base_id(), api_key = Sys.getenv("AIRTABLE_API_KEY"), org = "rladies", directory_repo = "directory" )directory_sync_airtable( base_id = directory_base_id(), api_key = Sys.getenv("AIRTABLE_API_KEY"), org = "rladies", directory_repo = "directory" )
base_id |
Airtable base ID. Defaults to the directory base. |
api_key |
Airtable API key. Defaults to |
org |
GitHub organization. Defaults to |
directory_repo |
Directory repository name. |
Only submissions marked minority_gender == "yes" and not flagged as a
DEFAULT ROW are processed. Returning submitters are matched to their
existing entry by slug (directory_id, falling back to identifier), and
their submission is merged onto the existing file: fields listed in
clear_fields are wiped first, then submitted fields overlay the rest, so
a partial update never drops data the submitter left blank.
Delete requests are collected and reported in the PR body but are not executed here; destructive removal stays with the reviewed purge workflow.
PR URL if changes found, NULL otherwise (invisibly).
Checks that filenames follow conventions: lowercase, no hashes,
ASCII-only, .json extension.
directory_validate_filename(filename)directory_validate_filename(filename)
filename |
Filename (not full path) to validate. |
A named list with valid (logical) and issues (character
vector of problems found).
Performs HTTP HEAD requests to check if profiles resolve.
directory_verify_handles(entry)directory_verify_handles(entry)
entry |
Named list representing a directory entry, with a
|
A data frame with columns platform, handle, status,
and valid.
Events are gated on the originating team being an allowed workspace,
not on actor privilege (there is no "actor" for a passive event like a
reaction or a team join - anyone triggers these). airtable_webhook
events have no team_id at all (Airtable webhooks aren't Slack-team
scoped) and are trusted by construction: the Worker already verified
the webhook's shared secret before dispatching. The other three kinds
are checked against the organiser/community team allowlist, mirroring
the Worker's own slack_team_is_allowed().
event_authorize( event, organiser_id = Sys.getenv("SLACK_ORGANIZER_TEAM_ID"), community_id = Sys.getenv("SLACK_COMMUNITY_TEAM_ID") )event_authorize( event, organiser_id = Sys.getenv("SLACK_ORGANIZER_TEAM_ID"), community_id = Sys.getenv("SLACK_COMMUNITY_TEAM_ID") )
event |
Parsed event list from |
organiser_id |
Organiser workspace team id. Defaults to env
|
community_id |
Community workspace team id. Defaults to env
|
A list with ok (logical) and message (a refusal string
when ok is FALSE, otherwise NULL).
Create a formatted event summary
event_create_summary(events, period = c("weekly", "monthly"))event_create_summary(events, period = c("weekly", "monthly"))
events |
Data frame from |
period |
Summary period label. |
Formatted markdown string.
Unlike cmd_execute(), handlers perform their own Slack/Airtable API
calls directly rather than returning a reply string for a workflow
step to relay - there is no single "response destination" for a
passive event the way there is for a command. The return value is a
short status string for the GitHub Actions run log only.
event_execute(event)event_execute(event)
event |
Parsed event list from |
Character string describing the outcome, for logging.
Queries Meetup Pro for recent events.
event_list_chapter(chapter, months = 3)event_list_chapter(chapter, months = 3)
chapter |
Chapter identifier (e.g. "rladies-berlin"). |
months |
Number of months of history. |
Data frame with columns: title, date, url, rsvp_count, source, chapter.
slack-event dispatch payloadThe Worker's repository_dispatch payload is already structured JSON
(unlike slash commands, which arrive as free text for cmd_parse() to
split), so this is a thin validator rather than a parser: it confirms
kind is one of the registered jinx_events() entries and normalizes
the shape callers can rely on.
event_parse(payload)event_parse(payload)
payload |
A list with |
A list with kind, team_id, response_url, event; kind
is "unknown" when the payload's kind isn't registered.
Fetches events from all configured sources and writes a summary.
event_sync_chapters( org = "rladies", target_repo = "event-archive", months = 3, dry_run = TRUE, chapters = NULL )event_sync_chapters( org = "rladies", target_repo = "event-archive", months = 3, dry_run = TRUE, chapters = NULL )
org |
GitHub organization. |
target_repo |
Repository for the event archive. |
months |
Number of months of history. |
dry_run |
If |
chapters |
Meetup urlnames to sync. Read from the website data
when |
The chapter list comes from the website's data/chapters entries via
chapter_meetup_groups(), not from configuration: that data is
already the source of truth for which chapters exist and which Meetup
group each uses, and a hand-maintained list would drift immediately.
Data frame of all events (invisibly).
Looks up gather_<type>() where <type> has dashes replaced by
underscores. Sources extend the indexer by defining a new
gather_<type>(src) function.
gather_rag_source(src)gather_rag_source(src)
src |
Source spec list (must have |
List of chunk records (with chunk_idx set per document).
Reads the base branch tip and either creates branch there or
force-updates it. Used by helpers that need to (re-)open a clean
working branch before pushing files.
gh_branch_upsert(org, repo, branch, base = "main", force = TRUE)gh_branch_upsert(org, repo, branch, base = "main", force = TRUE)
org |
Repository owner. |
repo |
Repository name. |
branch |
Branch to create or update. |
base |
Base branch whose tip is used as the new SHA.
Defaults to |
force |
Whether to force-update if the branch already exists.
Defaults to |
The SHA the branch now points to (invisibly).
Thin wrapper around gh_welcome_contributor() that fixes is_pr = TRUE for use in PR-only workflows. Reusable across any repo.
gh_greet_contributor( owner, repo, number, author, org = "rladies", extra_message = NULL )gh_greet_contributor( owner, repo, number, author, org = "rladies", extra_message = NULL )
owner |
Repository owner. |
repo |
Repository name. |
number |
PR number. |
author |
GitHub login of the author. |
org |
Organization name. |
extra_message |
Optional extra markdown to append after the standard welcome message. |
Comment URL or NULL if author is a team member (invisibly).
If a PR is already open from branch into base, returns its URL;
otherwise opens a new PR with the given title and body.
gh_open_or_update_pr( org, repo, branch, base = "main", title, body, team_reviewers = NULL )gh_open_or_update_pr( org, repo, branch, base = "main", title, body, team_reviewers = NULL )
org |
Repository owner. |
repo |
Repository name. |
branch |
Head branch. |
base |
Base branch. Defaults to |
title |
PR title. |
body |
PR body. |
team_reviewers |
Character vector of org team slugs to request
review from, or |
PR URL.
Posts a review checklist comment tailored for content PRs (blog posts, news posts, or any markdown-driven content). Generic across content kinds — the only difference between blog and news today is the path filter the caller workflow applies.
gh_post_checklist(owner, repo, pr_number)gh_post_checklist(owner, repo, pr_number)
owner |
Repository owner. |
repo |
Repository name. |
pr_number |
PR number. |
Comment URL (invisibly).
Posts a thank-you message tailored to whether this is the author's
first merged PR, and credits everyone else who helped: co-authors
(commit authors and Co-authored-by: trailers), reviewers, and
people who commented on the PR.
gh_thank_contributor(owner, repo, pr_number, author, org = "rladies")gh_thank_contributor(owner, repo, pr_number, author, org = "rladies")
owner |
Repository owner. |
repo |
Repository name. |
pr_number |
PR number. |
author |
GitHub login of the PR author. |
org |
Organization name. |
Comment URL (invisibly).
Posts a thank-you when an issue is closed, crediting the person who raised it (with a warmer message if it was their first here) plus everyone who was assigned to it or commented on it.
gh_thank_issue_contributors(owner, repo, number, author, completed = TRUE)gh_thank_issue_contributors(owner, repo, number, author, completed = TRUE)
owner |
Repository owner. |
repo |
Repository name. |
number |
Issue number. |
author |
GitHub login of the person who opened the issue. |
completed |
Whether the issue was closed as completed. An issue
closed as |
Comment URL (invisibly), or NULL for bot authors.
Checks if the PR/issue author has previous contributions to the repo. If this is their first, posts a warm welcome message. If not, posts a shorter thank-you. No-op for bots and global team members.
gh_welcome_contributor( owner, repo, number, author, is_pr = TRUE, org = "rladies", extra_message = NULL )gh_welcome_contributor( owner, repo, number, author, is_pr = TRUE, org = "rladies", extra_message = NULL )
owner |
Repository owner. |
repo |
Repository name. |
number |
Issue or PR number. |
author |
GitHub login of the author. |
is_pr |
Whether this is a PR (TRUE) or issue (FALSE). |
org |
Organization name. |
extra_message |
Optional extra markdown to append after the
standard welcome message (e.g. a project-specific reminder).
Ignored when blank or |
Comment URL or NULL if author is a team member (invisibly).
Scans all non-meetup repositories in the organization, collects workflow information, and generates a JSON report.
gha_generate_dashboard( org = "rladies", exclude_pattern = "^meetup-", output_path = NULL )gha_generate_dashboard( org = "rladies", exclude_pattern = "^meetup-", output_path = NULL )
org |
GitHub organization. Defaults to |
exclude_pattern |
Regex pattern to exclude repos.
Defaults to |
output_path |
Path to write the JSON report.
If |
List of workflow data (invisibly).
Lists pending invitations and triggers finalization for accepted ones.
gt_check_invitations(org = "rladies")gt_check_invitations(org = "rladies")
org |
GitHub organization. Defaults to |
Character vector of usernames that have accepted their invitation (invisibly).
Create a global team offboarding issue
gt_create_offboarding(username, team, name = username, org = "rladies")gt_create_offboarding(username, team, name = username, org = "rladies")
username |
GitHub username. |
team |
Team slug. |
name |
Full name. Defaults to the username. |
org |
GitHub organization. Defaults to |
The created issue URL (invisibly).
Finalize global team offboarding by removing user from teams
gt_finalize_offboarding(username, team, org = "rladies")gt_finalize_offboarding(username, team, org = "rladies")
username |
GitHub username. |
team |
Team slug. |
org |
GitHub organization. Defaults to |
Invisibly returns NULL. Called for its side effect of removing
the user from the specified teams.
Adds the user to their specific team and creates a tracking issue with the onboarding checklist.
gt_finalize_onboarding(username, team, name = username, org = "rladies")gt_finalize_onboarding(username, team, name = username, org = "rladies")
username |
GitHub username. |
team |
Team slug (e.g. "website"). |
name |
Full name. |
org |
GitHub organization. Defaults to |
The created issue URL (invisibly).
Sends an organization membership invitation and adds the user to the global team.
gt_invite(username, team, name = username, org = "rladies")gt_invite(username, team, name = username, org = "rladies")
username |
GitHub username (without |
team |
Team slug (e.g. "website"). |
name |
Full name (used in issue templates). |
org |
GitHub organization. Defaults to |
Invisibly returns NULL. Called for its side effect of sending
the invitation and adding the user to the specified team.
Finds open issues with onboarding or offboarding labels that haven't
been updated in days days, and posts a reminder comment.
gt_remind_stale(org = "rladies", days = 30, repo = "global-team")gt_remind_stale(org = "rladies", days = 30, repo = "global-team")
org |
GitHub organization. Defaults to |
days |
Number of days without activity before reminding. Defaults to 30. |
repo |
Repository to check. Defaults to |
Invisibly returns NULL. Called for its side effect of posting
reminder comments on stale issues.
Part of global team onboarding: opens a "find a time" poll on samkoma so the new member and their team can settle on an onboarding meeting slot. The poll offers a two-week window of candidate dates starting one week after it is created, and the poll link is posted as a comment on the onboarding issue. The poll is created (and the comment posted) under Jinx's identity.
gt_schedule_onboarding_meeting( issue_number, name, team_name, org = "rladies", repo = "global-team", start = Sys.Date() + 7, from = "08:00", to = "20:00", slot = 30, tz = "UTC" )gt_schedule_onboarding_meeting( issue_number, name, team_name, org = "rladies", repo = "global-team", start = Sys.Date() + 7, from = "08:00", to = "20:00", slot = 30, tz = "UTC" )
issue_number |
Onboarding issue number the poll link is posted to. |
name |
Full name of the new member (used in the poll title). |
team_name |
Human-readable team name (used in the poll title). |
org |
GitHub organization. Defaults to |
repo |
Repository holding the onboarding issue. Defaults to
|
start |
First candidate date. Defaults to one week from today. |
from, to
|
Daily availability window in 24h |
slot |
Slot length in minutes. Defaults to 30. |
tz |
IANA timezone the poll window is painted in. Defaults to
|
The poll URL (invisibly).
The guide writes the templates that are sent as email with their
subject on a leading SUBJECT: line, followed by
BODY OF THE MESSAGE:. This splits the two apart so that callers send
the guide's subject rather than inventing one, and errors if the
template carries no subject at all.
guide_email_template(name, variables = list(), base_url = guide_base_url())guide_email_template(name, variables = list(), base_url = guide_base_url())
name |
Template name without extension, as it is filed in the
guide (for example |
variables |
Named list of |
base_url |
Guide base URL, for testing against a local build. |
A list with name, subject and body.
guide_template() for a template with no subject line.
## Not run: notice <- guide_email_template("chapter-inactive-first-notice") notice$subject ## End(Not run)## Not run: notice <- guide_email_template("chapter-inactive-first-notice") notice$subject ## End(Not run)
Organisational communications - what RLadies+ says to organisers and
chapters - are written and edited in the guide, under
static/templates/, and served as markdown from
https://guide.rladies.org/templates/<name>.md. jinx fetches them
instead of shipping copies, so a volunteer can fix the wording
without waiting for a package release, and the text an organiser
reads in the guide is the text jinx sends.
guide_template(name, variables = list(), base_url = guide_base_url())guide_template(name, variables = list(), base_url = guide_base_url())
name |
Template name without extension, as it is filed in the
guide (for example |
variables |
Named list of |
base_url |
Guide base URL, for testing against a local build. |
A failed fetch is an error. There is deliberately no vendored fallback, because a second copy is the drift this replaces.
Use guide_email_template() for a template that is sent as email and
so carries its own subject line.
The template text, as a single string.
## Not run: guide_template("meetup-group-description") guide_template( "chapter-onboarding-welcome", list(FIRST_NAME = "Ada", CITY = "Oslo") ) ## End(Not run)## Not run: guide_template("meetup-group-description") guide_template( "chapter-onboarding-welcome", list(FIRST_NAME = "Ada", CITY = "Oslo") ) ## End(Not run)
Check translation coverage across languages
i18n_check_coverage()i18n_check_coverage()
Data frame with columns: language, total_templates, translated, coverage_pct.
Looks up the chapter's language preference from its metadata. Falls back to English if not set.
i18n_get_chapter_language(chapter, org = "rladies")i18n_get_chapter_language(chapter, org = "rladies")
chapter |
Chapter slug (e.g. "rladies-berlin"). |
org |
GitHub organization. |
Language code string.
List supported languages
i18n_list_languages()i18n_list_languages()
Data frame with columns: code, name, native_name, direction.
Looks up inst/translations/{language}/{template_name}.md first,
falls back to inst/templates/{template_name}.md if the translation
is missing.
i18n_translate_template(template_name, language = "en", variables = list())i18n_translate_template(template_name, language = "en", variables = list())
template_name |
Template filename without path (e.g. "global-team-onboarding.md"). |
language |
Language code (e.g. "es", "pt", "fr"). Defaults to "en". |
variables |
Named list of placeholder values for
|
Rendered template as a single character string.
Checks that all translated templates have the same <KEY> placeholders
as the English baseline.
i18n_validate_translations(language = NULL)i18n_validate_translations(language = NULL)
language |
Language code to validate. If |
Data frame with columns: template, language, status, missing_keys, extra_keys.
Get LinkedIn API version string
li_get_version()li_get_version()
Version string in YYYYMM format.
Upload media to LinkedIn
li_media_upload(author, media)li_media_upload(author, media)
author |
LinkedIn URN of the owner. |
media |
File path to the image. |
LinkedIn image ID string.
Interactive OAuth2 flow for obtaining a LinkedIn token.
li_oauth()li_oauth()
OAuth token object.
Post to LinkedIn
li_post_write(author, text, image = NULL, image_alt = "")li_post_write(author, text, image = NULL, image_alt = "")
author |
LinkedIn URN of the author. |
text |
Post text. |
image |
Optional file path to an image. |
image_alt |
Alt text for the image. |
LinkedIn post ID (invisibly).
Create a base LinkedIn API request
li_req(endpoint_version = "rest", ...)li_req(endpoint_version = "rest", ...)
endpoint_version |
API version path. Defaults to |
... |
Passed to |
Configured httr2 request.
Get the LinkedIn URN for the authenticated user
li_urn_me()li_urn_me()
URN string like "urn:li:person:abc123".
Locates the grimoire source skills and the per-repository file globs
each review gate watches. See inst/config/copilot-review.yml.
load_copilot_review_config()load_copilot_review_config()
Named list with grimoire and repos entries.
Load file-path to label mappings
load_labels_config()load_labels_config()
Named list with label mappings.
Load the configured RAG source list
load_rag_sources()load_rag_sources()
List of source specs.
Load PR review rules
load_review_rules()load_review_rules()
Named list with defaults, rules, and naming conventions.
Load teams configuration
load_teams_config()load_teams_config()
Named list with organization, global_team_id, teams, and default_assignees.
Sends plain text mail from [email protected], cc'ing
[email protected] by default so the onboarding inbox keeps the
thread. jinx never sends as chapters@, which is why this needs
only a single account's gmail.send token rather than domain-wide
delegation.
mail_send( to, subject, body, cc = "[email protected]", from = "[email protected]" )mail_send( to, subject, body, cc = "[email protected]", from = "[email protected]" )
to |
Recipient address, or a vector of them. |
subject |
Message subject. |
body |
Plain text body. |
cc |
Cc address. Defaults to |
from |
Sender address. Defaults to |
The sent message id (invisibly).
Get the ranked best slots for a poll
meeting_poll_best(id, edit_token = NULL, base_url = samkoma_base_url())meeting_poll_best(id, edit_token = NULL, base_url = samkoma_base_url())
id |
Poll id. |
edit_token |
Optional host edit token, required to read a hidden poll's responses. |
base_url |
API base URL. |
A data frame with columns slot, count, and names
(comma-separated), ordered best-first. The total number of
respondents is attached as the total attribute.
Creates a "find a time" poll where participants paint their
availability over a set of days and time slots. Polls are public by
default so results can be read back without a stored secret; the
returned edit_token is the host secret needed to lock the final
slot or read a hidden poll.
meeting_poll_create( title, days, from, to, slot, tz = "UTC", kind = c("dates", "weekdays"), public = TRUE, results_hidden = FALSE, deadline = NULL, base_url = samkoma_base_url() )meeting_poll_create( title, days, from, to, slot, tz = "UTC", kind = c("dates", "weekdays"), public = TRUE, results_hidden = FALSE, deadline = NULL, base_url = samkoma_base_url() )
title |
Poll title (1-200 characters). |
days |
Character vector of day identifiers: ISO dates
( |
from, to
|
Start and end of the daily window, |
slot |
Slot length in minutes. |
tz |
IANA timezone name (e.g. |
kind |
Either |
public |
Whether the poll is publicly readable. Defaults to |
|
Whether responses are hidden from voters. |
|
deadline |
Optional ISO 8601 datetime after which voting closes. |
base_url |
API base URL. |
A list with id, url, and edit_token.
Format ranked best slots as markdown
meeting_poll_format_best(best, title = NULL, top = 3)meeting_poll_format_best(best, title = NULL, top = 3)
best |
A data frame from |
title |
Optional poll title for the heading. |
top |
Maximum number of slots to show. |
Character string of markdown.
Deliberately omits the edit token, which is a host secret.
meeting_poll_format_created(created, title)meeting_poll_format_created(created, title)
created |
A list returned by |
title |
The poll title. |
Character string of markdown.
Fetch a poll and its aggregated responses
meeting_poll_get(id, edit_token = NULL, base_url = samkoma_base_url())meeting_poll_get(id, edit_token = NULL, base_url = samkoma_base_url())
id |
Poll id. |
edit_token |
Optional host edit token, required to read a hidden poll's responses. |
base_url |
API base URL. |
Parsed poll object as a list.
Export the locked slot of a poll as an iCalendar (.ics) string
meeting_poll_ics(id, base_url = samkoma_base_url())meeting_poll_ics(id, base_url = samkoma_base_url())
id |
Poll id. |
base_url |
API base URL. |
The .ics file contents as a character string.
Host-only: requires the poll's edit_token.
meeting_poll_lock(id, slot, edit_token, base_url = samkoma_base_url())meeting_poll_lock(id, slot, edit_token, base_url = samkoma_base_url())
id |
Poll id. |
slot |
The chosen slot identifier ( |
edit_token |
Optional host edit token, required to read a hidden poll's responses. |
base_url |
API base URL. |
Invisibly, the parsed response.
Advisory only. Meetup serves its single-page app with HTTP 200 for
groups that do not exist, so the status code says nothing; the check
looks for the "Group not found" copy in the body instead. That copy
can change, so this reports "unknown" rather than guessing when it
cannot tell, and a human should confirm before creating the group.
meetup_urlname_status(urlname)meetup_urlname_status(urlname)
urlname |
Meetup urlname to check. |
One of "available", "taken", or "unknown".
Reads and deletes the KV pending_link:{email} record (same
SLACK_TOKENS namespace and key format the Airtable invite flow
writes to when marking an invite sent). R port of
slack_pending_link_consume() from worker/src/slack-events.js.
pending_link_consume( email, namespace_id = slack_tokens_namespace_id(), account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), api_token = Sys.getenv("CLOUDFLARE_API_TOKEN") )pending_link_consume( email, namespace_id = slack_tokens_namespace_id(), account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), api_token = Sys.getenv("CLOUDFLARE_API_TOKEN") )
email |
Email address to look up. |
namespace_id |
KV namespace ID for |
account_id |
Cloudflare account ID. Defaults to env
|
api_token |
Cloudflare API token. Defaults to env
|
The parsed link record, or NULL if none was pending.
Identifies inactive chapters and prepares the notice the guide documents for them. The wording and the subject line come from the guide, fetched at call time, so the message organisers receive is the one a volunteer can edit there.
prepare_inactivity_emails( chapters, template = "chapter-inactive-first-notice", dry_run = TRUE )prepare_inactivity_emails( chapters, template = "chapter-inactive-first-notice", dry_run = TRUE )
chapters |
Chapter status data from |
template |
Name of the guide template to send. Defaults to the
first inactivity notice; the guide also documents a retirement
notice ( |
dry_run |
Controls the wording of the summary only. Either way
the prepared emails are returned rather than sent: delivery goes
through |
Data frame of prepared emails, one row per inactive chapter,
with chapter, email, subject and body (invisibly).
Calls Workers AI to write a short, warm invitation from the channel's name
and description. Failures are swallowed and return NULL so the caller can
fall back to a plain-description blurb rather than skip the promotion.
promo_blurb( name, description, account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), api_token = Sys.getenv("CLOUDFLARE_API_TOKEN"), model = workers_ai_chat_model() )promo_blurb( name, description, account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), api_token = Sys.getenv("CLOUDFLARE_API_TOKEN"), model = workers_ai_chat_model() )
name |
Channel name (without |
description |
Cleaned channel description. |
account_id |
Cloudflare account ID. Defaults to env
|
api_token |
Cloudflare API token. Defaults to env
|
model |
Workers AI chat model. |
Character scalar invitation, or NULL if the model call failed or
returned nothing usable.
Keeps non-archived public channels that carry a description (Slack
"purpose"), dropping the channel we post the spotlight to and any names in
skip. The description is what the weekly spotlight is built from, so a
channel with an empty purpose has nothing to promote and is left out.
promo_eligible_channels(channels, target_channel = NULL, skip = character())promo_eligible_channels(channels, target_channel = NULL, skip = character())
channels |
List of Slack channel objects, as returned by
|
target_channel |
Name of the channel the spotlight is posted to, excluded so it never promotes itself. |
skip |
Character vector of additional channel names to exclude. |
A data frame with one row per eligible channel and columns id,
name, and description (cleaned of link markup and URLs).
Chooses the alphabetically-first eligible channel that has not been
featured since the cycle began, so every channel gets a turn before any
repeats. recent is pruned to currently-eligible ids first (channels that
were archived or lost their description drop out), and the cycle resets
once every eligible channel has been seen.
promo_pick_channel(eligible, recent = character())promo_pick_channel(eligible, recent = character())
eligible |
Data frame from |
recent |
Character vector of channel ids featured earlier this cycle. |
NULL when nothing is eligible, otherwise a list with channel
(the chosen single-row data frame) and recent (the updated vector to
persist).
Count coding questions Jinx declined
question_coding_declined_count(rows)question_coding_declined_count(rows)
rows |
Data frame from |
Integer count of outcome == "coding_declined" rows.
Narrower sibling of question_gaps_rank(): excludes "coding_declined"
rows, since declining a coding question is working as designed, not a
corpus gap. R port of content_gaps() from the deleted
worker/src/question-digest.js.
question_content_gaps(rows, min_count = 1, limit = 10)question_content_gaps(rows, min_count = 1, limit = 10)
rows |
Data frame from |
min_count |
Minimum occurrence count to keep a gap. |
limit |
Maximum number of gaps to return. |
Data frame with columns question, outcome, count.
Queries the anonymous question log, ranks content gaps and downvoted
answers, drafts a proposed Guide answer for the top gaps, and formats
the lot as Slack mrkdwn. R port of question_digest_build() from the
deleted worker/src/question-digest.js.
question_digest_build( days = 7, account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), database_id = "4500d886-2593-44f9-9a01-d38cfa26e8dc", api_token = Sys.getenv("CLOUDFLARE_API_TOKEN"), model = workers_ai_chat_model() )question_digest_build( days = 7, account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), database_id = "4500d886-2593-44f9-9a01-d38cfa26e8dc", api_token = Sys.getenv("CLOUDFLARE_API_TOKEN"), model = workers_ai_chat_model() )
days |
Number of days to cover. Defaults to 7. |
account_id |
Cloudflare account ID. Defaults to env
|
database_id |
D1 database ID. Defaults to the provisioned
|
api_token |
Cloudflare API token. Defaults to env
|
model |
Workers AI chat model used for drafting. |
Character scalar digest text, or NULL when nothing was logged.
Distinct from question_log_format(), which renders a GitHub-comment
markdown report for /jinx questions - this builds the Slack message
text posted by question_digest_post(). R port of format_digest()
from the deleted worker/src/question-digest.js.
question_digest_format(days, total, gaps, drafts, downvoted, coding_count)question_digest_format(days, total, gaps, drafts, downvoted, coding_count)
days |
Number of days the report covers. |
total |
Total questions logged in the period. |
gaps |
Data frame from |
drafts |
Data frame: the top-drafted subset of |
downvoted |
Data frame from |
coding_count |
Integer from |
Character scalar Slack mrkdwn message.
Build and post the weekly question-gap digest to Slack
question_digest_post( days = 7, channel = env_default("SLACK_DIGEST_CHANNEL", "team-jinx"), slack_token = Sys.getenv("SLACK_TOKEN"), account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), database_id = "4500d886-2593-44f9-9a01-d38cfa26e8dc", api_token = Sys.getenv("CLOUDFLARE_API_TOKEN"), model = workers_ai_chat_model() )question_digest_post( days = 7, channel = env_default("SLACK_DIGEST_CHANNEL", "team-jinx"), slack_token = Sys.getenv("SLACK_TOKEN"), account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), database_id = "4500d886-2593-44f9-9a01-d38cfa26e8dc", api_token = Sys.getenv("CLOUDFLARE_API_TOKEN"), model = workers_ai_chat_model() )
days |
Number of days to cover. Defaults to 7. |
channel |
Slack channel to post to. Defaults to env
|
slack_token |
Slack bot token. Defaults to env |
account_id |
Cloudflare account ID. Defaults to env
|
database_id |
D1 database ID. Defaults to the provisioned
|
api_token |
Cloudflare API token. Defaults to env
|
model |
Workers AI chat model used for drafting. |
Invisibly, TRUE if a digest was posted, FALSE if there was
nothing to report.
R port of question_downvoted_rank() in worker/src/question-log.js.
question_downvoted_rank(rows, limit = 10)question_downvoted_rank(rows, limit = 10)
rows |
Data frame from |
limit |
Maximum number of rows to return. |
Subset of rows where down > up, ordered by down - up
descending.
Calls Workers AI to draft a short, human-reviewable answer for a question
Jinx could not answer well. Failures are swallowed - a digest with an
undrafted gap is better than a failed scheduled run. R port of
draft_guide_snippet() from the deleted worker/src/question-digest.js.
question_draft_guide_snippet( question, account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), api_token = Sys.getenv("CLOUDFLARE_API_TOKEN"), model = workers_ai_chat_model() )question_draft_guide_snippet( question, account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), api_token = Sys.getenv("CLOUDFLARE_API_TOKEN"), model = workers_ai_chat_model() )
question |
The question Jinx could not answer well. |
account_id |
Cloudflare account ID. Defaults to env
|
api_token |
Cloudflare API token. Defaults to env
|
model |
Workers AI chat model. |
Character scalar draft, or NULL if the model call failed or
returned nothing usable.
Format a reaction-feedback summary as a reply
question_feedback_format(summary)question_feedback_format(summary)
summary |
A list from |
Character scalar reply text.
Aggregates the reaction_log:{team_id}:{day}:{reaction} KV counters
reaction_log_increment() writes, across the last days days.
Backs the "feedback" command. R port of slash_feedback() from
the deleted worker/src/slash-local.js.
question_feedback_summary( team_id, days = 7, namespace_id = slack_tokens_namespace_id(), account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), api_token = Sys.getenv("CLOUDFLARE_API_TOKEN") )question_feedback_summary( team_id, days = 7, namespace_id = slack_tokens_namespace_id(), account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), api_token = Sys.getenv("CLOUDFLARE_API_TOKEN") )
team_id |
Slack team id. Required (aborts if missing) - a blank prefix would otherwise scan every key in the namespace across both workspaces instead of just this one's. |
days |
Number of days to look back. |
namespace_id |
KV namespace ID for |
account_id |
Cloudflare account ID. Defaults to env
|
api_token |
Cloudflare API token. Defaults to env
|
A list with days, entries (integer count of KV entries
scanned), and totals (a named integer vector, reaction name to
summed count, sorted descending).
R port of question_gaps_rank() in worker/src/question-log.js.
Near-identical questions (case/whitespace-insensitive) are folded
together so a popular unanswered question rises to the top.
question_gaps_rank(rows, limit = 10)question_gaps_rank(rows, limit = 10)
rows |
Data frame from |
limit |
Maximum number of gaps to return. |
Data frame with columns question, outcome, count,
ordered by count descending.
Format a question-log report as markdown
question_log_format(rows, gaps, downvoted, days)question_log_format(rows, gaps, downvoted, days)
rows |
Data frame from |
gaps |
Data frame from |
downvoted |
Data frame from |
days |
Number of days the report covers, for the header. |
Character string with markdown-formatted report.
Deletes rows from the Cloudflare D1 jinx-question-log database older
than retention_days, honouring the 180-day retention promise in
PRIVACY.md. Formerly question_log_purge() in
worker/src/question-log.js, ported here so the retention guarantee
has a single implementation.
question_log_purge( retention_days = 180, account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), database_id = "4500d886-2593-44f9-9a01-d38cfa26e8dc", api_token = Sys.getenv("CLOUDFLARE_API_TOKEN") )question_log_purge( retention_days = 180, account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), database_id = "4500d886-2593-44f9-9a01-d38cfa26e8dc", api_token = Sys.getenv("CLOUDFLARE_API_TOKEN") )
retention_days |
Number of days to retain rows for. Defaults to 180. |
account_id |
Cloudflare account ID. Defaults to env
|
database_id |
D1 database ID. Defaults to the provisioned
|
api_token |
Cloudflare API token. Defaults to env
|
Integer number of rows deleted.
Reads rows from the Cloudflare D1 jinx-question-log database via
cloudflarer::cf_d1_query() - the same table
worker/src/question-log.js writes to from inside the Cloudflare
Worker when @Jinx answers a question. No Slack user id, channel,
or thread timestamp is stored, so a row cannot be traced to who asked.
question_log_query( since_day = as.character(Sys.Date() - 7), account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), database_id = "4500d886-2593-44f9-9a01-d38cfa26e8dc", api_token = Sys.getenv("CLOUDFLARE_API_TOKEN") )question_log_query( since_day = as.character(Sys.Date() - 7), account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), database_id = "4500d886-2593-44f9-9a01-d38cfa26e8dc", api_token = Sys.getenv("CLOUDFLARE_API_TOKEN") )
since_day |
Character |
account_id |
Cloudflare account ID. Defaults to env
|
database_id |
D1 database ID. Defaults to the provisioned
|
api_token |
Cloudflare API token. Defaults to env
|
Data frame with columns id, day, question, outcome,
top_score, sources, up, down.
R port of question_vote_apply() from worker/src/slack-events.js
(deleted as part of the reaction-handling migration). Looks up the D1
row an answer message links to via the answer_link:{team_id}: {channel}:{ts} KV key, and increments its up/down count.
question_vote_apply( team_id, item, reaction, namespace_id = slack_tokens_namespace_id(), account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), database_id = "4500d886-2593-44f9-9a01-d38cfa26e8dc", api_token = Sys.getenv("CLOUDFLARE_API_TOKEN") )question_vote_apply( team_id, item, reaction, namespace_id = slack_tokens_namespace_id(), account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), database_id = "4500d886-2593-44f9-9a01-d38cfa26e8dc", api_token = Sys.getenv("CLOUDFLARE_API_TOKEN") )
team_id |
Slack team id. |
item |
Reaction event's |
reaction |
Raw reaction name. |
namespace_id |
KV namespace ID for |
account_id |
Cloudflare account ID. Defaults to env
|
database_id |
D1 database ID. Defaults to the provisioned
|
api_token |
Cloudflare API token. Defaults to env
|
TRUE if a vote was applied, FALSE otherwise.
Second, independent Workers AI pass that is prompted to reject the challenge unless it is confident it is correct, unambiguous, and fairly rated. Fails closed: any error or unparseable verdict counts as rejection.
r_challenge_adversarial_check( challenge, reprex_output = character(), account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), api_token = Sys.getenv("CLOUDFLARE_API_TOKEN"), model = workers_ai_chat_model() )r_challenge_adversarial_check( challenge, reprex_output = character(), account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), api_token = Sys.getenv("CLOUDFLARE_API_TOKEN"), model = workers_ai_chat_model() )
challenge |
Parsed challenge from |
reprex_output |
Captured verification output from
|
account_id |
Cloudflare account ID. Defaults to env
|
api_token |
Cloudflare API token. Defaults to env
|
model |
Workers AI chat model. |
List with ok (logical), verdict, and issues (character
vector).
Constrains what the generator may use and what the container running the reprex verification must provide: base R plus a small tidyverse core.
r_challenge_allowed_packages()r_challenge_allowed_packages()
Character vector of package names.
Difficulty levels for the weekly R challenge
r_challenge_difficulties()r_challenge_difficulties()
Data frame with columns level, label, and badge (a Slack
emoji), one row per difficulty.
Slack emoji badge for a difficulty level
r_challenge_difficulty_badge(difficulty)r_challenge_difficulty_badge(difficulty)
difficulty |
One of the levels in |
Character scalar emoji.
Human-readable label for a difficulty level
r_challenge_difficulty_label(difficulty)r_challenge_difficulty_label(difficulty)
difficulty |
One of the levels in |
Character scalar label.
Orchestrates the full generator: draft with Workers AI, parse, verify the
reference solution with reprex, render the statement as a reprex, then
adversarially review. Regenerates on any failed gate, up to max_tries, and
returns NULL if none passes, so a scheduled run never emits unverified
output.
r_challenge_draft_build( difficulty = NULL, max_tries = 3L, account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), api_token = Sys.getenv("CLOUDFLARE_API_TOKEN"), model = workers_ai_chat_model() )r_challenge_draft_build( difficulty = NULL, max_tries = 3L, account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), api_token = Sys.getenv("CLOUDFLARE_API_TOKEN"), model = workers_ai_chat_model() )
difficulty |
Optional difficulty to request; |
max_tries |
Maximum generate-and-verify attempts before giving up. |
account_id |
Cloudflare account ID. Defaults to env
|
api_token |
Cloudflare API token. Defaults to env
|
model |
Workers AI chat model. |
A list with challenge, statement (rendered statement reprex),
reprex (solution verification output), critique, and attempts, or
NULL if no verified challenge was produced.
Top-level entry for the draft cron: builds a fully verified challenge with
r_challenge_draft_build() and, if one passes every gate, opens it as a
status: proposed issue via r_challenge_open_issue() for an organiser to
review. Does nothing (and does not error) when no challenge survives
verification, so a scheduled run never proposes unverified output.
r_challenge_draft_post( difficulty = NULL, repo = "rladies/cauldron", max_tries = 3L, account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), api_token = Sys.getenv("CLOUDFLARE_API_TOKEN"), model = workers_ai_chat_model() )r_challenge_draft_post( difficulty = NULL, repo = "rladies/cauldron", max_tries = 3L, account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), api_token = Sys.getenv("CLOUDFLARE_API_TOKEN"), model = workers_ai_chat_model() )
difficulty |
Optional difficulty to request; |
repo |
Target review repo as |
max_tries |
Maximum generate-and-verify attempts before giving up. |
account_id |
Cloudflare account ID. Defaults to env
|
api_token |
Cloudflare API token. Defaults to env
|
model |
Workers AI chat model. |
Invisibly, TRUE if a challenge was proposed, else FALSE.
Builds the Markdown body an organiser reviews before approving: the rendered statement in the open, with the reference solution, reprex verification output, and adversarial notes in collapsed sections.
r_challenge_format_issue(build, source = "ai")r_challenge_format_issue(build, source = "ai")
build |
Result of |
source |
Where the challenge came from, e.g. |
Character scalar Markdown issue body.
Renders the difficulty badge, title, and the challenge's reprex statement
for posting to the community channel. The title flows through
escape_markdown() and the statement sits in a fenced code block (its
backticks stripped), so model- or user-generated text cannot inject links or
<!channel> mass-pings. The reference solution is never shown.
r_challenge_format_slack(title, difficulty, statement)r_challenge_format_slack(title, difficulty, statement)
title |
Challenge title. |
difficulty |
Difficulty level (see |
statement |
Rendered reprex statement (character vector or scalar). |
Character scalar Slack mrkdwn message.
Low-level generator: returns the model's raw completion, which
r_challenge_parse() turns into a structured challenge. Most callers want
r_challenge_draft_build(), which also verifies the result.
r_challenge_generate( difficulty = NULL, account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), api_token = Sys.getenv("CLOUDFLARE_API_TOKEN"), model = workers_ai_chat_model() )r_challenge_generate( difficulty = NULL, account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), api_token = Sys.getenv("CLOUDFLARE_API_TOKEN"), model = workers_ai_chat_model() )
difficulty |
Optional difficulty to request; |
account_id |
Cloudflare account ID. Defaults to env
|
api_token |
Cloudflare API token. Defaults to env
|
model |
Workers AI chat model. |
Character scalar with the model's raw response.
GitHub issue title for a drafted challenge
r_challenge_issue_title(challenge)r_challenge_issue_title(challenge)
challenge |
Parsed challenge from |
Character scalar issue title.
Creates a status: proposed issue in the challenge repo (default
rladies/cauldron) from a verified draft, labelled with its source and its
themed difficulty, ready for an organiser to review. Requires a
GITHUB_PAT/GITHUB_TOKEN with issues: write on the target repo.
r_challenge_open_issue(build, repo = "rladies/cauldron", source = "ai")r_challenge_open_issue(build, repo = "rladies/cauldron", source = "ai")
build |
Result of |
repo |
Target review repo as |
source |
Where the challenge came from, e.g. |
The created issue's URL, invisibly.
Tolerates prose or code fences around the JSON. Returns NULL for any
output missing a required field, carrying an unknown difficulty, or without
at least one machine-checkable test, so callers can regenerate.
r_challenge_parse(text)r_challenge_parse(text)
text |
Raw model completion from |
A list with elements title, difficulty, statement, solution,
and tests (character vector), or NULL if invalid.
Runs the statement (task comment + setup + expected output as literals) through the reprex runner, producing the rendered block shown to solvers. Because it runs the statement on its own, a statement that leaks the answer by calling the not-yet-written function errors out and is rejected.
r_challenge_render_statement(statement, runner = r_challenge_reprex_runner)r_challenge_render_statement(statement, runner = r_challenge_reprex_runner)
statement |
Character scalar of R source stating the challenge. |
runner |
Function taking code lines and returning rendered reprex output. Defaults to the isolated reprex runner. |
List with ok (logical) and rendered (character vector).
Assembles the solution plus a base::stopifnot() for each test, runs it
through runner, and treats an #> Error line (a failed test or a broken
solution) as verification failure.
r_challenge_reprex_check(solution, tests, runner = r_challenge_reprex_runner)r_challenge_reprex_check(solution, tests, runner = r_challenge_reprex_runner)
solution |
Character scalar reference solution. |
tests |
Character vector of R expressions expected to be |
runner |
Function taking a character vector of code lines and returning the rendered output. Defaults to the isolated reprex runner. |
List with ok (logical) and output (character vector).
Matches the JS indexer scheme: first 32 hex chars of
sha256("{repo}|{path}|{chunk_idx}").
rag_chunk_id(repo, path, chunk_idx)rag_chunk_id(repo, path, chunk_idx)
repo |
Source repo (e.g. |
path |
Path within the source (e.g. |
chunk_idx |
Zero-based chunk index within the document. |
32-character hex string.
Gathers chunks from every configured source, embeds them with
Cloudflare Workers AI, and upserts the vectors into a Vectorize
index. The source list is loaded from inst/config/rag-sources.yml
unless sources is supplied directly.
rag_index_build( sources = NULL, account_id = NULL, api_token = Sys.getenv("CLOUDFLARE_API_TOKEN"), index_name = NULL, batch_size = 50L, model = "@cf/baai/bge-base-en-v1.5" )rag_index_build( sources = NULL, account_id = NULL, api_token = Sys.getenv("CLOUDFLARE_API_TOKEN"), index_name = NULL, batch_size = 50L, model = "@cf/baai/bge-base-en-v1.5" )
sources |
Optional list of source specs (see |
account_id |
Cloudflare account ID. Falls back to env
|
api_token |
Cloudflare API token. Defaults to env
|
index_name |
Vectorize index. Defaults to env |
batch_size |
Number of chunks per embed/upsert call. |
model |
Workers AI embedding model. |
Invisibly: list with chunks (total) and upsert (API response).
Select a random emoji
random_emoji()random_emoji()
Single emoji character.
R port of reaction_direction() from worker/src/question-log.js.
Strips a skin-tone modifier suffix (e.g. "thumbsup::skin-tone-3")
before matching.
reaction_direction(reaction)reaction_direction(reaction)
reaction |
Raw reaction name. |
"up", "down", or NULL for a neutral reaction.
The reaction_added event handler registered in jinx_events():
increments the reaction-feedback tally and applies a vote to the
question the reacted-to message answered, if any.
reaction_event_apply(team_id, event)reaction_event_apply(team_id, event)
team_id |
Slack team id. |
event |
The event's |
Invisibly, NULL.
R port of the KV counter logic in slack_event_handle_reaction()
(worker/src/slack-events.js, deleted as part of the reaction-handling
migration). Backs /jinx feedback.
reaction_log_increment( team_id, reaction, namespace_id = slack_tokens_namespace_id(), account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), api_token = Sys.getenv("CLOUDFLARE_API_TOKEN") )reaction_log_increment( team_id, reaction, namespace_id = slack_tokens_namespace_id(), account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), api_token = Sys.getenv("CLOUDFLARE_API_TOKEN") )
team_id |
Slack team id. |
reaction |
Raw reaction name. |
namespace_id |
KV namespace ID for |
account_id |
Cloudflare account ID. Defaults to env
|
api_token |
Cloudflare API token. Defaults to env
|
Invisibly, the new count for today.
Format a report as markdown
report_format_markdown(report)report_format_markdown(report)
report |
Report data from |
Character string with markdown-formatted report.
Collects stats across org repos: commits, PRs, issues.
report_generate( type = c("weekly", "monthly"), org = "rladies", exclude_pattern = "^meetup-" )report_generate( type = c("weekly", "monthly"), org = "rladies", exclude_pattern = "^meetup-" )
type |
Report type: |
org |
GitHub organization. Defaults to |
exclude_pattern |
Regex to exclude repos. Defaults to |
A named list with report data (invisibly).
Validates that changed files follow repository naming conventions.
review_check_pr_naming(owner, repo, pr_number)review_check_pr_naming(owner, repo, pr_number)
owner |
Repository owner. |
repo |
Repository name. |
pr_number |
PR number. |
Data frame with filename and valid columns.
Assigns reviewers, labels the PR, and posts a checklist comment.
review_run(owner, repo, pr_number)review_run(owner, repo, pr_number)
owner |
Repository owner. |
repo |
Repository name. |
pr_number |
Pull request number. |
Invisibly returns NULL. Called for its side effects of
assigning reviewers, labelling the PR, and posting a checklist comment.
Queries the Cloudflare Web Analytics beacon data for a date range,
via cloudflarer::cf_rum_page_views() and cloudflarer::cf_rum_top().
A sibling to website_collect_analytics() (which queries Plausible) —
Cloudflare RUM is a separate, beacon-based data source.
rum_collect_analytics( account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), site_tag = Sys.getenv("CLOUDFLARE_RUM_SITE_TAG"), token = Sys.getenv("CLOUDFLARE_API_TOKEN"), since = Sys.Date() - 30, until = Sys.Date() )rum_collect_analytics( account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), site_tag = Sys.getenv("CLOUDFLARE_RUM_SITE_TAG"), token = Sys.getenv("CLOUDFLARE_API_TOKEN"), since = Sys.Date() - 30, until = Sys.Date() )
account_id |
Cloudflare account ID. Defaults to env
|
site_tag |
RUM site tag, as returned by
|
token |
Cloudflare API token. Defaults to env
|
since, until
|
Date range, half-open |
Named list with site_tag, since, until, pageviews,
top_pages, top_sources, and top_countries.
Format Cloudflare RUM analytics as markdown
rum_format_analytics(analytics)rum_format_analytics(analytics)
analytics |
Data from |
Character string with markdown-formatted report.
Collects Cloudflare Web Analytics data and formats it as markdown. A
sibling to website_generate_report() (Plausible-backed).
rum_generate_report( account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), site_tag = Sys.getenv("CLOUDFLARE_RUM_SITE_TAG"), token = Sys.getenv("CLOUDFLARE_API_TOKEN"), since = Sys.Date() - 30, until = Sys.Date(), output_path = NULL )rum_generate_report( account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), site_tag = Sys.getenv("CLOUDFLARE_RUM_SITE_TAG"), token = Sys.getenv("CLOUDFLARE_API_TOKEN"), since = Sys.Date() - 30, until = Sys.Date(), output_path = NULL )
account_id |
Cloudflare account ID. Defaults to env
|
site_tag |
RUM site tag, as returned by
|
token |
Cloudflare API token. Defaults to env
|
since, until
|
Date range, half-open |
output_path |
Optional path to write JSON data. |
Named list with analytics and markdown (invisibly).
The "setup-channel" command handler: joins the channel if needed
(public channels only - a private channel requires a human invite
first), then adds any bookmarks from inst/config/bookmarks.json
the channel doesn't already have. R port of slash_setup_channel()
from the deleted worker/src/slash-local.js; reads the bookmarks
config directly from this package's inst/ instead of the Worker's
redundant GitHub-raw fetch of the same file.
setup_channel_process(team_id, channel_id, channel_name)setup_channel_process(team_id, channel_id, channel_name)
team_id |
Slack team id. |
channel_id |
Channel to set up. |
channel_name |
Channel display name, for the reply text. |
Character scalar reply text.
Shorten a URL using the Short.io API
short_url(uri)short_url(uri)
uri |
URL to shorten. |
Shortened URL string.
slack_post_message()'s single
chat.postMessage call - DM opening, channel lookup, bookmarks, and
so on. Retries on a 429 or 5xx response, honouring the Retry-After
header up to 60s. Slack's Retry-After for a Tier 2 method is
routinely 30-60s, so a shorter cap guarantees failure under sustained
rate limiting - which a bulk pass over a workspace's channels hits
readily.Generic low-level Slack API caller shared by the event/command
handlers that need more than slack_post_message()'s single
chat.postMessage call - DM opening, channel lookup, bookmarks, and
so on. Retries on a 429 or 5xx response, honouring the Retry-After
header up to 60s. Slack's Retry-After for a Tier 2 method is
routinely 30-60s, so a shorter cap guarantees failure under sustained
rate limiting - which a bulk pass over a workspace's channels hits
readily.
slack_api_call(token, method, body = list(), encode = c("json", "form"))slack_api_call(token, method, body = list(), encode = c("json", "form"))
token |
Slack bot token. |
method |
Slack Web API method name, e.g. |
body |
Named list of request parameters. |
encode |
How to send |
The parsed JSON response (a list).
R/GitHub Actions only ever operates in two known workspaces (organiser, community) via two static secrets, unlike the Worker's per-team OAuth-token KV store which supports arbitrary installs - jinx isn't installed anywhere else, so the simpler static lookup is enough.
slack_bot_token(workspace = c("organiser", "community"))slack_bot_token(workspace = c("organiser", "community"))
workspace |
Either |
The bot token string.
Caches the workspace's full channel list in KV (channel_index:{team_id},
same SLACK_TOKENS namespace and 1h TTL the Worker used) to avoid a
paginated conversations.list call on every lookup. R port of
slack_channel_id_lookup() from worker/src/slack-api.js.
slack_channel_id_lookup( team_id, name, workspace, namespace_id = slack_tokens_namespace_id(), account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), api_token = Sys.getenv("CLOUDFLARE_API_TOKEN") )slack_channel_id_lookup( team_id, name, workspace, namespace_id = slack_tokens_namespace_id(), account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), api_token = Sys.getenv("CLOUDFLARE_API_TOKEN") )
team_id |
Slack team id. |
name |
Channel name (without |
workspace |
|
namespace_id |
KV namespace ID for |
account_id |
Cloudflare account ID. Defaults to env
|
api_token |
Cloudflare API token. Defaults to env
|
The channel ID, or NULL if not found.
Shapes the paginated output of the internal slack_conversations_list()
helper into the frame the copy plan compares against.
slack_channel_index(workspace = c("organiser", "community"), channels = NULL)slack_channel_index(workspace = c("organiser", "community"), channels = NULL)
workspace |
Either |
channels |
Optional pre-fetched channel list, for testing. |
A data frame with id, name, topic, purpose and
is_member.
<#id|name> mentionFalls back to plain #name text if the lookup fails - matches the
Worker's channel_mention() behaviour.
slack_channel_mention(team_id, name, workspace, channel_index = NULL)slack_channel_mention(team_id, name, workspace, channel_index = NULL)
team_id |
Slack team id. |
name |
Channel name (without |
workspace |
|
channel_index |
Optional pre-loaded index from
|
Character string mention.
Open a Slack direct message channel
slack_conversations_open(team_id, user_id, workspace)slack_conversations_open(team_id, user_id, workspace)
team_id |
Slack team id. |
user_id |
Slack user id to DM. |
workspace |
|
The DM channel ID, or NULL on failure.
The slack_interaction event handler registered in jinx_events().
Branches on action_id exactly like the three JS functions it replaces
(slack_invite_process_approval/_sent/_denial, deleted from
worker/src/airtable-invite.js), replying via response_url for each.
slack_interaction_process(action_id, action_data, admin_user, response_url)slack_interaction_process(action_id, action_data, admin_user, response_url)
action_id |
One of |
action_data |
Parsed button |
admin_user |
Slack username who clicked the button. |
response_url |
The interaction's |
Invisibly, NULL.
R port of slack_invite_approval_checklist_blocks() from the deleted
worker/src/airtable-invite.js.
slack_invite_approval_checklist_blocks( email, approver, record_id, base_id, table_id )slack_invite_approval_checklist_blocks( email, approver, record_id, base_id, table_id )
email |
Applicant email. |
approver |
Slack username who approved the request. |
record_id |
Airtable record ID. |
base_id |
Airtable base ID. |
table_id |
Airtable table ID. |
A list of Block Kit blocks.
Fetches pending invitees from Airtable and prepares invite emails with a Slack invitation link.
slack_invite_batch( invite_link, base_id = slack_invitees_base_id(), api_key = Sys.getenv("AIRTABLE_API_KEY"), sender_name = "RLadies+ Global", sender_email = "[email protected]", dry_run = TRUE )slack_invite_batch( invite_link, base_id = slack_invitees_base_id(), api_key = Sys.getenv("AIRTABLE_API_KEY"), sender_name = "RLadies+ Global", sender_email = "[email protected]", dry_run = TRUE )
invite_link |
Active Slack invite URL (valid for 30 days). |
base_id |
Airtable base ID for Slack invitees. |
api_key |
Airtable API key. |
sender_name |
Name of the sender. |
sender_email |
Sender email address. |
dry_run |
If |
Data frame of prepared emails (invisibly).
The bot cannot invite users directly (Slack admin scopes are not
available to community apps). Instead, this function posts a
message to an organisers' Slack channel with the email address and
instructions for a workspace admin to action the invite manually,
then flips the matching Airtable record's invited flag for audit.
slack_invite_request( email, channel = Sys.getenv("SLACK_INVITE_REQUEST_CHANNEL"), base_id = slack_invitees_base_id(), api_key = Sys.getenv("AIRTABLE_API_KEY"), token = Sys.getenv("SLACK_TOKEN") )slack_invite_request( email, channel = Sys.getenv("SLACK_INVITE_REQUEST_CHANNEL"), base_id = slack_invitees_base_id(), api_key = Sys.getenv("AIRTABLE_API_KEY"), token = Sys.getenv("SLACK_TOKEN") )
email |
Email address to request an invite for. |
channel |
Slack channel where the request is posted. Defaults
to |
base_id |
Airtable base ID for Slack invitees. |
api_key |
Airtable API key. If unset, the Airtable audit step is skipped. |
token |
Slack bot token ( |
A single status string suitable for posting back as a PR comment.
R port of slack_invite_request_blocks() from the deleted
worker/src/airtable-invite.js. chapter is shown but, matching the
JS original, deliberately not carried in the button value payload.
slack_invite_request_blocks(email, name, chapter, record_id, base_id, table_id)slack_invite_request_blocks(email, name, chapter, record_id, base_id, table_id)
email |
Applicant email. |
name |
Applicant name, or |
chapter |
Chapter name, or |
record_id |
Airtable record ID. |
base_id |
Airtable base ID. |
table_id |
Airtable table ID. |
A list of Block Kit blocks.
Sends a markdown-formatted message to the specified Slack channel using the Slack Web API.
slack_post_message( text, channel, token = Sys.getenv("SLACK_TOKEN"), unfurl = FALSE, blocks = NULL )slack_post_message( text, channel, token = Sys.getenv("SLACK_TOKEN"), unfurl = FALSE, blocks = NULL )
text |
Message text (supports Slack mrkdwn formatting). |
channel |
Slack channel name (without #). |
token |
Slack API token. Defaults to |
unfurl |
Whether Slack may expand links into previews. Off by default: most of what Jinx posts links to an issue or a chapter page, where a preview card adds noise. Messages whose whole point is the linked page (community blog posts) turn it on. |
blocks |
Optional Slack Block Kit blocks. When given, |
API response (invisibly).
response_url
Posts directly from R rather than relying on a GitHub Actions workflow
step to curl the reply - keeps the reply logic testable and in one
language. Slack's response_url accepts a message replacement for up
to 30 minutes after the original interaction.
slack_response_url_post( response_url, text = NULL, blocks = NULL, replace_original = FALSE )slack_response_url_post( response_url, text = NULL, blocks = NULL, replace_original = FALSE )
response_url |
The |
text |
Fallback plain text (shown if |
blocks |
Block Kit blocks list, or |
replace_original |
Whether to replace the original message. |
The raw response body (invisibly).
Slack only runs slash commands typed by a real user, so a bot cannot
subscribe a feed directly. This posts an actionable request asking a
human in the channel to run /feed subscribe <url>.
slack_subscribe_rss( rss_url, channel = "rladiesblogs", token = Sys.getenv("SLACK_TOKEN") )slack_subscribe_rss( rss_url, channel = "rladiesblogs", token = Sys.getenv("SLACK_TOKEN") )
rss_url |
RSS feed URL to subscribe. |
channel |
Slack channel name (without #). Defaults to "rladiesblogs". |
token |
Slack API token. |
API response (invisibly).
Map a Slack team id to its workspace name
slack_workspace_for_team( team_id, organiser_id = Sys.getenv("SLACK_ORGANIZER_TEAM_ID"), community_id = Sys.getenv("SLACK_COMMUNITY_TEAM_ID") )slack_workspace_for_team( team_id, organiser_id = Sys.getenv("SLACK_ORGANIZER_TEAM_ID"), community_id = Sys.getenv("SLACK_COMMUNITY_TEAM_ID") )
team_id |
Slack team id from an event/command payload. |
organiser_id |
Organiser workspace team id. Defaults to env
|
community_id |
Community workspace team id. Defaults to env
|
"organiser" or "community".
Convert tags to hashtag string
tags2hash(tags)tags2hash(tags)
tags |
Character vector of tags. |
Single string with hashtags.
Validate blog entry JSON files against schema
validate_blog_entry( path, schema = system.file("schemas", "blog-entry.json", package = "jinx") )validate_blog_entry( path, schema = system.file("schemas", "blog-entry.json", package = "jinx") )
path |
Path to a directory containing blog JSON files, or a single JSON file path. |
schema |
Path to JSON schema file. Uses the bundled schema by default. |
A data frame with columns file, valid, and errors.
Validate directory entry JSON files against schema
validate_directory_entries( path, schema = system.file("schemas", "directory-entry.json", package = "jinx") )validate_directory_entries( path, schema = system.file("schemas", "directory-entry.json", package = "jinx") )
path |
Path to a directory containing JSON entry files, or a single JSON file path. |
schema |
Path to JSON schema file. Uses the bundled schema by default. |
A data frame with columns file, valid, and errors.
Runs the checks that used to be manual review-checklist items on the entry files changed in a PR (filenames, likely-duplicate slugs, contact method vs. social entry, stray contact info in free text) and posts a single consolidated checklist. Each entry lists clickable profile links so reviewers can confirm the social accounts resolve, rather than an unreliable automated check (the platforms block bot requests). Schema validity is covered separately by the directory repo's JSON validation.
validate_directory_pr(owner, repo, pr_number)validate_directory_pr(owner, repo, pr_number)
owner |
Repository owner. |
repo |
Repository name. |
pr_number |
PR number. |
Invisibly NULL.
Queries the Cloudflare GraphQL Analytics API for visitor and pageview
metrics over a date window. The website moved to Cloudflare, so Plausible has
been removed; the Cloudflare collector is pending a token and site tag and is
not yet implemented. The intended return shape matches what
website_format_analytics() consumes: aggregate, timeseries,
top_pages, and top_sources.
website_collect_analytics( from = NULL, to = NULL, site_tag = Sys.getenv("CLOUDFLARE_SITE_TAG"), account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), api_token = Sys.getenv("CLOUDFLARE_API_TOKEN") )website_collect_analytics( from = NULL, to = NULL, site_tag = Sys.getenv("CLOUDFLARE_SITE_TAG"), account_id = Sys.getenv("CLOUDFLARE_ACCOUNT_ID"), api_token = Sys.getenv("CLOUDFLARE_API_TOKEN") )
from, to
|
Date window (a |
site_tag |
Cloudflare Web Analytics site tag. Defaults to
|
account_id |
Cloudflare account tag. Defaults to
|
api_token |
Cloudflare API token with Account Analytics Read. Defaults
to |
Named list with aggregate, timeseries, top_pages, and
top_sources.
Format website analytics as markdown
website_format_analytics(analytics)website_format_analytics(analytics)
analytics |
Data from |
Formatted markdown string.
Collects website analytics and formats as markdown.
website_generate_report( from = NULL, to = NULL, period = NULL, output_path = NULL )website_generate_report( from = NULL, to = NULL, period = NULL, output_path = NULL )
from, to
|
Date window (a |
period |
Deprecated Plausible period string, accepted for backward compatibility with the command registry and ignored. |
output_path |
Optional path to write JSON data. |
Named list with analytics and markdown (invisibly).
Checks open PRs with "pending" label in the website repo. If a blog post's date matches today, merges the PR.
website_merge_pending(org = "rladies", repo = "rladies.github.io")website_merge_pending(org = "rladies", repo = "rladies.github.io")
org |
GitHub organization. |
repo |
Repository name. |
Character vector of merged PR URLs (invisibly).
Publish website analytics as a GitHub issue
website_publish_report( report_data, org = "rladies", target_repo = "global-team", slack_channel = NULL )website_publish_report( report_data, org = "rladies", target_repo = "global-team", slack_channel = NULL )
report_data |
Data from |
org |
GitHub organization. |
target_repo |
Repository to publish to. |
slack_channel |
Optional Slack channel to post a summary to. |
Issue URL (invisibly).
Reads the welcome config and template directly from this package's
inst/ (no GitHub-raw fetch needed - the content already ships here).
Falls back to a hardcoded plain-text greeting if either read fails.
R port of welcome_message_render() from worker/src/slack-events.js.
welcome_message_render(team_id, user_id, workspace, link = NULL)welcome_message_render(team_id, user_id, workspace, link = NULL)
team_id |
Slack team id. |
user_id |
Slack user id of the new member. |
workspace |
|
link |
Optional pending chapter sign-up link from
|
Character string with the rendered welcome message.
The team_join event handler registered in jinx_events(): consumes
any pending chapter sign-up link, opens a DM, renders the welcome
message, and posts it. R port of slack_event_handle_team_join() from
worker/src/slack-events.js.
welcome_send(team_id, user)welcome_send(team_id, user)
team_id |
Slack team id. |
user |
The event's Slack user object list: |
Invisibly, NULL.