diff --git a/.dependabot/config.yml b/.dependabot/config.yml index 1df9896f..4cad1bc6 100644 --- a/.dependabot/config.yml +++ b/.dependabot/config.yml @@ -5,4 +5,4 @@ update_configs: update_schedule: "live" allowed_updates: - match: - update_type: "security" \ No newline at end of file + update_type: "security" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index eccfe75f..7b16aa38 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -2,12 +2,12 @@ name: "CodeQL" on: push: - branches: [ main ] + branches: [main] pull_request: # The branches below must be a subset of the branches above - branches: [ main ] + branches: [main] schedule: - - cron: '28 7 * * 0' + - cron: "28 7 * * 0" permissions: contents: read @@ -19,13 +19,13 @@ jobs: runs-on: ubuntu-latest steps: - - name: Checkout repository - uses: actions/checkout@v2 + - name: Checkout repository + uses: actions/checkout@v2 - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - queries: security-and-quality + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + queries: security-and-quality - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/InclusionBot.md b/InclusionBot.md index 8bc65088..0b6097da 100644 --- a/InclusionBot.md +++ b/InclusionBot.md @@ -49,32 +49,32 @@ approves your changes, they will be merged and become active in the bot. # Racist -| Instead of | Consider | Why | -| ------------------------------------------------ | --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| ghetto | haphazard
distressed
derelict
slapdash | This term is used to describe neighborhoods that the speaker considers undesirable. This term has a deeply racist history against Jewish and Black communities.

See [Responding to Inequalities: Common words or phrases with racist history](https://www.tmj4.com/news/local-news/responding-to-inequalities-common-words-or-phrases-with-racist-history) for more. | -| uppity | outspoken
ambitious
highfalutin
vocal
assertive | This term is often used to silence Black people and imply that they do not have a right to be assertive.

See [Responding to Inequalities: Common words or phrases with racist history](https://www.tmj4.com/news/local-news/responding-to-inequalities-common-words-or-phrases-with-racist-history) for more. | -| peanut gallery | teeming masses
onlookers | This term refers to people who are seen as rowdy or disruptive, indicating that their viewpoints should be ignored. This term arose from theater performance seating, and has racist and classist origins.

See [Responding to Inequalities: Common words or phrases with racist history](https://www.tmj4.com/news/local-news/responding-to-inequalities-common-words-or-phrases-with-racist-history) for more. | -| gypsy, gypsies | a member of the Romani people
a nomad | The term "gypsy" has historically been used as a racial slur against the Romani people.

See [Why Being 'Gypped' Hurts the Roma](https://www.npr.org/sections/codeswitch/2013/12/30/242429836/why-being-gypped-hurts-the-roma-more-than-it-hurts-you) for more. | -| gyp, gypped | swindled
cheated | This term is an abbreviated form of “gypsy” (a term that has historically been used as a racist slur for Romani people), and is based on a racist stereotype that Romani people are dishonest or thieves.

See [Why Being 'Gypped' Hurts the Roma](https://www.npr.org/sections/codeswitch/2013/12/30/242429836/why-being-gypped-hurts-the-roma-more-than-it-hurts-you) for more. | -| no can do | sorry, I can't | This term phrase originated as mocking Chinese immigrants’ English.

See [The Problematic Origins of Common Business Jargon](https://www.fastcompany.com/90239290/six-business-phrases-that-have-racist-origins) for more. | -| long time no see | it's been a while | This term phrase originated as mocking Chinese immigrants’ English.

See [The Problematic Origins of Common Business Jargon](https://www.fastcompany.com/90239290/six-business-phrases-that-have-racist-origins) for more. | -| no problemo | no hay problema
no problem | "No problemo" is mock Spanish and arose as a way of devaluing Spanish in favor of an "English-only" United States. | -| sherpa | guide
shepherd
coach | The Sherpa are a Tibetan ethnic group, but the word is often used to refer to mountain guides. Sherpa are a people, not a job.

See [It's time to rethink how we use the word "Sherpa"](https://thiswonderfulword.com/2014/05/01/sherpa/) for more. | -| pow-wow, powwow, pow wow | meeting
working session
huddle
jam session | Powwows are a sacred component of some Indigneous spiritual traditions. We want to show respect towards all religious traditions.

See [Use these culturally offensive phrases and questions at your own risk](https://www.ictinc.ca/blog/culturally-offensive-phrases-you-should-use-at) for more. | -| circle the wagons | hunker down | This term came to be associated with "savage" Native Americans due to Hollywood westerns of the 1950s and 1960s. We want to avoid reinforcing harmful stereotypes. | -| spirit animal | role model
kindred spirit
inspiration
secret twin
when I grow up, I want to be... | Spirit animals are a sacred component of some Indigneous spiritual traditions. We want to show respect towards all religious traditions.

See [Use these culturally offensive phrases and questions at your own risk](https://www.ictinc.ca/blog/culturally-offensive-phrases-you-should-use-at) for more. | -| totem pole | level of seniority | Totem poles are a sacred component of some Indigneous spiritual traditions. We want to show respect towards all religious traditions.

See [Use these culturally offensive phrases and questions at your own risk](https://www.ictinc.ca/blog/culturally-offensive-phrases-you-should-use-at) for more. | -| off the reservation | off-topic, off-script, breaking the rules | This expression refers to laws that restricted the free movement of Indigenous people. A person who was “off the reservation” was behaving “lawlessly.” We want to avoid upholding colonizing language and ideas. | -| guru | authority
maestro
virtuoso | A guru is a sacred role in some Indian spiritual traditions and is not merely an expert or guide. We want to show respect towards all religious traditions.

See [The Power of Words](https://www.klipfolio.com/blog/power-of-words) for more. | -| ninja | expert
whiz | A ninja was a covert mercenary in feudal Japan. We want to show respect towards cultural traditions.

See [The Power of Words](https://www.klipfolio.com/blog/power-of-words) for more. | -| blacklist, black list, blacklisted, black listed | blocked list
deny list
exclude list
reject list | This term implies an association between being Black and being bad. We want to avoid this association.

See [Terminology: It's not black and white](https://www.ncsc.gov.uk/blog-post/terminology-its-not-black-and-white) for more. | -| whitelist, white list, whitelisted, white listed | unblocked list
allow list
permit list | This term implies that being white is the only way of being good. We want to avoid this association.

See [Terminology: It's not black and white](https://www.ncsc.gov.uk/blog-post/terminology-its-not-black-and-white) for more. | -| dark pattern | antipattern
anti-UX pattern
deceptive design pattern | This term implies an association between darkness and Blackness and being bad. We want to avoid this association.

See [Terminology: It's not black and white](https://www.ncsc.gov.uk/blog-post/terminology-its-not-black-and-white) for more. | -| master/slave, master / slave | primary/secondary
primary/replica
leader/worker
main/replica | This term is rooted in the enslavement of human beings. We want to avoid slavery references.

See [GitHub to replace "master" with alternative term to avoid slavery references](https://www.zdnet.com/article/github-to-replace-master-with-alternative-term-to-avoid-slavery-references/) for more. | -| open the kimono, open kimono | share information | This term arises from the sexualization of Asian women. We want to avoid sexist, racist, and sexual references.

See [The Problematic Origins of Common Business Jargon](https://www.fastcompany.com/90239290/six-business-phrases-that-have-racist-origins) for more. | -| grandfathered | legacy
included retroactively | This term arose from voter restrictions that prevented Black people from voting. We want to avoid slavery and racist oppression references.

See [The Problematic Origins of Common Business Jargon](https://www.fastcompany.com/90239290/six-business-phrases-that-have-racist-origins) for more. | -| mumbo jumbo | jargon
confusing language | The term ":TERM:" was used to stereotype African languages and cultures that didn't make sense to European colonizers.

See [some other commonly used terms that have racist origins](https://abcnews.go.com/Politics/commonly-terms-racist-origins/story?id=71840410). | -| kowtow, kotow, kow tow, kow-tow, cowtow, cow tow, cow-tow | submit
grovel | The modern usage of this word refers to groveling or submission, which is a disconnect from its real meaning as an act of deep respect. Modern usage of this term misses cultural nuance and perpetuates cultural misunderstanding.

See [the Slack thread](https://gsa-tts.slack.com/archives/C10EGE62W/p1727210606818389) where teammates discuss the term for more information.| +| Instead of | Consider | Why | +| --------------------------------------------------------- | --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| ghetto | haphazard
distressed
derelict
slapdash | This term is used to describe neighborhoods that the speaker considers undesirable. This term has a deeply racist history against Jewish and Black communities.

See [Responding to Inequalities: Common words or phrases with racist history](https://www.tmj4.com/news/local-news/responding-to-inequalities-common-words-or-phrases-with-racist-history) for more. | +| uppity | outspoken
ambitious
highfalutin
vocal
assertive | This term is often used to silence Black people and imply that they do not have a right to be assertive.

See [Responding to Inequalities: Common words or phrases with racist history](https://www.tmj4.com/news/local-news/responding-to-inequalities-common-words-or-phrases-with-racist-history) for more. | +| peanut gallery | teeming masses
onlookers | This term refers to people who are seen as rowdy or disruptive, indicating that their viewpoints should be ignored. This term arose from theater performance seating, and has racist and classist origins.

See [Responding to Inequalities: Common words or phrases with racist history](https://www.tmj4.com/news/local-news/responding-to-inequalities-common-words-or-phrases-with-racist-history) for more. | +| gypsy, gypsies | a member of the Romani people
a nomad | The term "gypsy" has historically been used as a racial slur against the Romani people.

See [Why Being 'Gypped' Hurts the Roma](https://www.npr.org/sections/codeswitch/2013/12/30/242429836/why-being-gypped-hurts-the-roma-more-than-it-hurts-you) for more. | +| gyp, gypped | swindled
cheated | This term is an abbreviated form of “gypsy” (a term that has historically been used as a racist slur for Romani people), and is based on a racist stereotype that Romani people are dishonest or thieves.

See [Why Being 'Gypped' Hurts the Roma](https://www.npr.org/sections/codeswitch/2013/12/30/242429836/why-being-gypped-hurts-the-roma-more-than-it-hurts-you) for more. | +| no can do | sorry, I can't | This term phrase originated as mocking Chinese immigrants’ English.

See [The Problematic Origins of Common Business Jargon](https://www.fastcompany.com/90239290/six-business-phrases-that-have-racist-origins) for more. | +| long time no see | it's been a while | This term phrase originated as mocking Chinese immigrants’ English.

See [The Problematic Origins of Common Business Jargon](https://www.fastcompany.com/90239290/six-business-phrases-that-have-racist-origins) for more. | +| no problemo | no hay problema
no problem | "No problemo" is mock Spanish and arose as a way of devaluing Spanish in favor of an "English-only" United States. | +| sherpa | guide
shepherd
coach | The Sherpa are a Tibetan ethnic group, but the word is often used to refer to mountain guides. Sherpa are a people, not a job.

See [It's time to rethink how we use the word "Sherpa"](https://thiswonderfulword.com/2014/05/01/sherpa/) for more. | +| pow-wow, powwow, pow wow | meeting
working session
huddle
jam session | Powwows are a sacred component of some Indigneous spiritual traditions. We want to show respect towards all religious traditions.

See [Use these culturally offensive phrases and questions at your own risk](https://www.ictinc.ca/blog/culturally-offensive-phrases-you-should-use-at) for more. | +| circle the wagons | hunker down | This term came to be associated with "savage" Native Americans due to Hollywood westerns of the 1950s and 1960s. We want to avoid reinforcing harmful stereotypes. | +| spirit animal | role model
kindred spirit
inspiration
secret twin
when I grow up, I want to be... | Spirit animals are a sacred component of some Indigneous spiritual traditions. We want to show respect towards all religious traditions.

See [Use these culturally offensive phrases and questions at your own risk](https://www.ictinc.ca/blog/culturally-offensive-phrases-you-should-use-at) for more. | +| totem pole | level of seniority | Totem poles are a sacred component of some Indigneous spiritual traditions. We want to show respect towards all religious traditions.

See [Use these culturally offensive phrases and questions at your own risk](https://www.ictinc.ca/blog/culturally-offensive-phrases-you-should-use-at) for more. | +| off the reservation | off-topic, off-script, breaking the rules | This expression refers to laws that restricted the free movement of Indigenous people. A person who was “off the reservation” was behaving “lawlessly.” We want to avoid upholding colonizing language and ideas. | +| guru | authority
maestro
virtuoso | A guru is a sacred role in some Indian spiritual traditions and is not merely an expert or guide. We want to show respect towards all religious traditions.

See [The Power of Words](https://www.klipfolio.com/blog/power-of-words) for more. | +| ninja | expert
whiz | A ninja was a covert mercenary in feudal Japan. We want to show respect towards cultural traditions.

See [The Power of Words](https://www.klipfolio.com/blog/power-of-words) for more. | +| blacklist, black list, blacklisted, black listed | blocked list
deny list
exclude list
reject list | This term implies an association between being Black and being bad. We want to avoid this association.

See [Terminology: It's not black and white](https://www.ncsc.gov.uk/blog-post/terminology-its-not-black-and-white) for more. | +| whitelist, white list, whitelisted, white listed | unblocked list
allow list
permit list | This term implies that being white is the only way of being good. We want to avoid this association.

See [Terminology: It's not black and white](https://www.ncsc.gov.uk/blog-post/terminology-its-not-black-and-white) for more. | +| dark pattern | antipattern
anti-UX pattern
deceptive design pattern | This term implies an association between darkness and Blackness and being bad. We want to avoid this association.

See [Terminology: It's not black and white](https://www.ncsc.gov.uk/blog-post/terminology-its-not-black-and-white) for more. | +| master/slave, master / slave | primary/secondary
primary/replica
leader/worker
main/replica | This term is rooted in the enslavement of human beings. We want to avoid slavery references.

See [GitHub to replace "master" with alternative term to avoid slavery references](https://www.zdnet.com/article/github-to-replace-master-with-alternative-term-to-avoid-slavery-references/) for more. | +| open the kimono, open kimono | share information | This term arises from the sexualization of Asian women. We want to avoid sexist, racist, and sexual references.

See [The Problematic Origins of Common Business Jargon](https://www.fastcompany.com/90239290/six-business-phrases-that-have-racist-origins) for more. | +| grandfathered | legacy
included retroactively | This term arose from voter restrictions that prevented Black people from voting. We want to avoid slavery and racist oppression references.

See [The Problematic Origins of Common Business Jargon](https://www.fastcompany.com/90239290/six-business-phrases-that-have-racist-origins) for more. | +| mumbo jumbo | jargon
confusing language | The term ":TERM:" was used to stereotype African languages and cultures that didn't make sense to European colonizers.

See [some other commonly used terms that have racist origins](https://abcnews.go.com/Politics/commonly-terms-racist-origins/story?id=71840410). | +| kowtow, kotow, kow tow, kow-tow, cowtow, cow tow, cow-tow | submit
grovel | The modern usage of this word refers to groveling or submission, which is a disconnect from its real meaning as an act of deep respect. Modern usage of this term misses cultural nuance and perpetuates cultural misunderstanding.

See [the Slack thread](https://gsa-tts.slack.com/archives/C10EGE62W/p1727210606818389) where teammates discuss the term for more information. | # Sexist @@ -99,4 +99,4 @@ approves your changes, they will be merged and become active in the bot. | Instead of | Consider | Why | | ------------------------------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Goddamn, goddam, goddamnit, goddammit | awful
terrible
wretched | This term can be offensive or triggering to some religious traditions because it implies a deity cursing or reproaching people. We want to show respect towards all religious traditions. | -| jesus fucking, fucking christ | Wow
oh no
oh my | We want to show respect towards all religious traditions. | +| jesus fucking, fucking christ | Wow
oh no
oh my | We want to show respect towards all religious traditions. | \ No newline at end of file diff --git a/manifest.yml b/manifest.yml index 702cb01d..c0ba4737 100644 --- a/manifest.yml +++ b/manifest.yml @@ -1,12 +1,12 @@ --- applications: -- name: charlie - buildpack: nodejs_buildpack - memory: 192MB - instances: 1 - command: ((command)) - routes: - - route: charlie-((environment)).app.cloud.gov - services: - - charlie-brain - - charlie-config \ No newline at end of file + - name: charlie + buildpack: nodejs_buildpack + memory: 192MB + instances: 1 + command: ((command)) + routes: + - route: charlie-((environment)).app.cloud.gov + services: + - charlie-brain + - charlie-config diff --git a/package-lock.json b/package-lock.json index 23eff00c..d73cbf1f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "js-yaml": "^4.2.0", "moment": "^2.29.2", "moment-timezone": "^0.5.35", + "node-cron": "^4.5.0", "node-schedule": "^2.0.0", "pg": "^8.7.1", "plural": "^1.1.0" @@ -86,6 +87,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.0", @@ -1462,6 +1464,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1910,6 +1913,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001669", "electron-to-chromium": "^1.5.41", @@ -2838,6 +2842,7 @@ "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -2970,6 +2975,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", "dev": true, + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.8", @@ -5278,6 +5284,15 @@ "node": ">= 0.6" } }, + "node_modules/node-cron": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.5.0.tgz", + "integrity": "sha512-4Trh+kjvbXokyJkwQumvD5YAgeJfgHLR/sKyu71uSmxfCR5QMO1hldpvmFZOICN5pLgNY+J5Y8+ar3XKo5/4tQ==", + "license": "ISC", + "engines": { + "node": ">=20" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -5698,6 +5713,7 @@ "version": "8.13.1", "resolved": "https://registry.npmjs.org/pg/-/pg-8.13.1.tgz", "integrity": "sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==", + "peer": true, "dependencies": { "pg-connection-string": "^2.7.0", "pg-pool": "^3.7.0", diff --git a/package.json b/package.json index 8ad2ec3d..6c4a80cb 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "js-yaml": "^4.2.0", "moment": "^2.29.2", "moment-timezone": "^0.5.35", + "node-cron": "^4.5.0", "node-schedule": "^2.0.0", "pg": "^8.7.1", "plural": "^1.1.0" diff --git a/src/scripts/erg-inviter.yaml b/src/scripts/erg-inviter.yaml index 8f811bc4..045feffb 100644 --- a/src/scripts/erg-inviter.yaml +++ b/src/scripts/erg-inviter.yaml @@ -40,10 +40,10 @@ ergs: career development, address the challenges we encounter in the workplace, and help one another in and around TTS. channel: "#womxn" - + Not Your Dude: description: > Private channel for cis women, trans women, trans men, non-binary people, and those who are otherwise marginalized (i.e., non-hegemonic masculine folks). - channel: "#womxn-private" \ No newline at end of file + channel: "#womxn-private" diff --git a/src/scripts/inclusion-bot.yaml b/src/scripts/inclusion-bot.yaml index 8065a465..353e15ff 100644 --- a/src/scripts/inclusion-bot.yaml +++ b/src/scripts/inclusion-bot.yaml @@ -34,7 +34,6 @@ message: > are some alternatives that might work better: triggers: - - matches: - blind to - blinded to @@ -170,7 +169,7 @@ triggers: - matches: - fuck alternatives: - - '' + - "" why: >- ":TERM:" can trigger emotions related to abuse, trauma, and sexual assault that employees may not want to process at work. @@ -416,7 +415,7 @@ triggers: - expert - whiz ignore: - - ':ninja:' + - ":ninja:" - ninja turtle - ninja turtles - ninja-turtle @@ -557,7 +556,7 @@ triggers: - suicidal - kill yourself alternatives: - - '' + - "" why: >- ":TERM:" can trigger emotions related to death that employees may not want to process at work. @@ -565,7 +564,7 @@ triggers: - matches: - shit alternatives: - - '' + - "" why: >- ":TERM:" can trigger emotions related to vulgarity that employees may not want to process at work. diff --git a/src/track-meet.js b/src/track-meet.js new file mode 100644 index 00000000..0fff96f3 --- /dev/null +++ b/src/track-meet.js @@ -0,0 +1,174 @@ +/** + * Track Meet Facilitator Bot + * Posts a message to the shared channel every 2nd/4th Friday at 11am PT + * listing facilitators for the upcoming Wednesday track meets. + * Reads facilitator names from each track's Slack Canvas table. + */ + +const cron = require("node-cron"); + +/** + * SHARED_CHANNEL_ID is the #usdc-all-fellows channel where the bot posts the message. + */ +const SHARED_CHANNEL_ID = "C05A6Q2PK6G"; + +/** + * TRACK_CANVASES is an object mapping track names to their respective Slack Canvas IDs. + */ +const TRACK_CANVASES = { + Cyber: "F091D6QD9RN", + Data: "F091D7L8M5E", + Design: "F090GG1L478", + Product: "F090QH1MQMQ", + Software: "F0911VCAJTB", +}; + +// ── DATE LOGIC ──────────────────────────────────────────────────────────────── + +function getWednesdayNumber(d) { + return Math.floor((d.getDate() - 1) / 7) + 1; +} + +const NEW_SCHEDULE_ANCHOR = new Date("2026-07-15"); // First track meet under new every-other-Wednesday rule + +function isTrackMeetWeek(d) { + if (d < NEW_SCHEDULE_ANCHOR) { + // Old rule: 2nd and 4th Wednesdays only + const n = getWednesdayNumber(d); + return n === 2 || n === 4; + } + // New rule: every other Wednesday anchored to 7/15/26 + const msPerWeek = 7 * 24 * 60 * 60 * 1000; + const weeksDiff = Math.round((d - NEW_SCHEDULE_ANCHOR) / msPerWeek); + return weeksDiff % 2 === 0; +} + +function getNextNextWednesday() { + // From Friday, skip the immediate Wednesday (5 days away) + // and get the one after that (12 days away) + const d = new Date(); + const daysAhead = (3 - d.getDay() + 7) % 7 || 7; // days until next Wednesday + d.setDate(d.getDate() + daysAhead + 7); // add another 7 to get the one after + return d; +} + +function getFridayBefore(wed) { + const d = new Date(wed); + d.setDate(d.getDate() - 5); + return d.toLocaleDateString("en-US", { month: "long", day: "numeric" }); +} + +function formatDate(d) { + return d.toLocaleDateString("en-US", { + month: "long", + day: "numeric", + year: "numeric", + }); +} + +// ── CANVAS READING ──────────────────────────────────────────────────────────── + +async function getFacilitatorForDate(client, canvasId, targetDate) { + const targetStrs = [ + targetDate.toLocaleDateString("en-US", { month: "numeric", day: "numeric", year: "2-digit" }), + targetDate.toLocaleDateString("en-US", { month: "numeric", day: "numeric", year: "numeric" }), + targetDate.toLocaleDateString("en-US", { month: "long", day: "numeric" }), + ]; + + try { + const res = await client.canvases.sections.lookup({ + canvas_id: canvasId, + criteria: { section_types: ["any_ordered_list", "table"] }, + }); + + for (const section of res.sections || []) { + const content = section.content || ""; + for (const fmt of targetStrs) { + if (content.includes(fmt)) { + for (const line of content.split("\n")) { + if (line.includes(fmt)) { + const parts = line.split("|").map((p) => p.trim()).filter(Boolean); + if (parts.length >= 2) return parts[1]; + } + } + } + } + } + return "TBD"; + } catch (err) { + console.error(`Error reading canvas ${canvasId}:`, err.message); + return "TBD"; + } +} + +// ── MESSAGE BUILDER ─────────────────────────────────────────────────────────── + +function buildMessage(facilitators, facilitationDate) { + const dateStr = formatDate(facilitationDate); + const fridayStr = getFridayBefore(facilitationDate); + const bullets = Object.entries(facilitators) + .map(([track, name]) => `• ${track}: ${name}`) + .join("\n"); + + return `Track Meet facilitators on deck for ${dateStr}. Facilitators, please coordinate directly with any presenters and let the track channel know if there is anything they will need to prepare. Also, please share the agenda in your track channel (and in this thread, noting if the meet is open to other tracks!) by Noon ET on ${fridayStr}. The facilitators for next week's track meets are below: + +${bullets} + +To organize a cross-track meet, reach out to the facilitator for that track. Track facilitation schedules are also pinned in the respective track Slack channels. As a reminder, the track facilitation schedules can be found in the "Meet Facilitation Schedules" folder in this channel's tabs, above. + +If you have a conflict (planned leave, agency conflict, etc) with your date to facilitate your track meet, it is your responsibility to coordinate a backup to cover facilitation.`; +} + +// ── MAIN JOB ────────────────────────────────────────────────────────────────── + +async function runBot(client) { + const targetWed = getNextNextWednesday(); + console.log(`Track meet bot running. Target Wednesday: ${formatDate(targetWed)}`); + + if (!isTrackMeetWeek(targetWed)) { + console.log( + `Skipping — ${getWednesdayNumber(targetWed)} Wednesday of the month (all-hands or wild card).` + ); + return; + } + + const trackEntries = Object.entries(TRACK_CANVASES); + const facilitatorList = await Promise.all( + trackEntries.map(([, canvasId]) => getFacilitatorForDate(client, canvasId, targetWed)) + ); + + const facilitators = {}; + trackEntries.forEach(([track], i) => { + facilitators[track] = facilitatorList[i]; + console.log(` ${track}: ${facilitators[track]}`); + }); + + // Only skip if ALL tracks are TBD — a single missing facilitator still posts + const allTBD = Object.values(facilitators).every((name) => name === "TBD"); + if (allTBD) { + console.log("No facilitators found in any canvas — skipping post."); + return; + } + + const message = buildMessage(facilitators, targetWed); + + try { + await client.chat.postMessage({ channel: SHARED_CHANNEL_ID, text: message }); + console.log("Track meet message posted successfully."); + } catch (err) { + console.error("Error posting message:", err.message); + } +} + +// ── CHARLIE ENTRY POINT ─────────────────────────────────────────────────────── + +module.exports = (app) => { + // Runs every Friday at 11:00 AM PT (19:00 UTC during daylight saving time) + cron.schedule( + "0 19 * * 5", + () => { runBot(app.client); }, + { timezone: "America/Los_Angeles" } + ); + + console.log("Track meet bot loaded. Scheduled for Fridays at 11:00 AM PT."); +}; \ No newline at end of file