From 638354fd44e8848f0772f54d7ae73cd4b23c67cb Mon Sep 17 00:00:00 2001 From: dotcomaki Date: Wed, 20 May 2026 06:27:36 +0530 Subject: [PATCH 1/7] Add createStandup activity task Creates a per-project Slack standup via the Standup & Prosper API for all students in each project that has a Slack channel configured on the event. --- src/activities/tasks/createStandup.ts | 97 +++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 src/activities/tasks/createStandup.ts diff --git a/src/activities/tasks/createStandup.ts b/src/activities/tasks/createStandup.ts new file mode 100644 index 0000000..d68d824 --- /dev/null +++ b/src/activities/tasks/createStandup.ts @@ -0,0 +1,97 @@ +import { PrismaClient } from "@prisma/client"; +import Container from "typedi"; +import { Context } from '../../context'; +import { + SlackEventWithProjects, + SlackMentorInfo, + SlackStudentInfo, + slackEventInfoSelect, +} from "../../slack"; +import { makeDebug } from "../../utils"; + +const DEBUG = makeDebug('activities:tasks:createStandup'); + +const STANDUP_API_BASE = 'https://api.standup-and-prosper.com/v1'; +const EXCLUDED_SLACK_IDS = new Set(['U07ACCWHDSA']); + +const DEFAULT_STANDUP = { + type: 'SLACK', + days: ['Monday', 'Wednesday', 'Friday'], + time: '10:00:00', + reportTime: '12:00:00', + timezone: 'America/Los_Angeles', + schedule: { type: 'WEEKLY' }, + questions: [ + { text: 'What did you do since last standup?' }, + { text: 'What will you do until next standup?' }, + { text: 'Is anything blocking you?' }, + ], + groupBy: 'USER_SINGLE_MESSAGE', + reportSortOrder: 'DISPLAY_NAME', + allowEditsAfterCompletion: true, + asThread: false, + syncWithChannel: false, + hideAnnouncements: false, +}; + +export default async function createStandup({ auth }: Context): Promise { + const apiKey = process.env.STANDUP_API_KEY; + if (!apiKey) throw new Error('STANDUP_API_KEY environment variable is not set'); + + const prisma = Container.get(PrismaClient); + const events = await prisma.event.findMany({ + where: { + id: auth.eventId, + slackWorkspaceAccessToken: { not: null }, + slackWorkspaceId: { not: null }, + }, + select: slackEventInfoSelect, + }) as SlackEventWithProjects[]; + + for (const event of events) { + const teamId = event.slackWorkspaceId; + + for (const project of event.projects) { + if (!project.slackChannelId) { + DEBUG(`Skipping project ${project.id} — no Slack channel`); + continue; + } + + const users = project.students + .filter((s) => s.slackId && !EXCLUDED_SLACK_IDS.has(s.slackId)) + .map((s) => ({ userId: s.slackId! })); + + if (users.length === 0) { + DEBUG(`Skipping project ${project.id} — no eligible student Slack IDs`); + continue; + } + + const body = { + ...DEFAULT_STANDUP, + channel: project.slackChannelId, + channelId: project.slackChannelId, + users, + }; + + try { + const res = await fetch(`${STANDUP_API_BASE}/teams/${teamId}/standups`, { + method: 'POST', + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + + if (!res.ok) { + const text = await res.text(); + DEBUG(`Failed for project ${project.id}: ${res.status} ${text}`); + } else { + DEBUG(`Created standup for project ${project.id} in channel ${project.slackChannelId}`); + } + } catch (ex) { + DEBUG(ex); + } + } + } +} From 2ef88b6bcd632e7196190baf1cdfe9cb4a406d3d Mon Sep 17 00:00:00 2001 From: dotcomaki Date: Wed, 20 May 2026 06:43:30 +0530 Subject: [PATCH 2/7] Fix createStandup to skip projects with existing standups Uses slackEventInfoSelect to match the established pattern, and adds a pre-query to skip projects that already have a standupId set. Saves the returned standup ID back to the project after creation. --- src/activities/tasks/createStandup.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/activities/tasks/createStandup.ts b/src/activities/tasks/createStandup.ts index d68d824..0311a21 100644 --- a/src/activities/tasks/createStandup.ts +++ b/src/activities/tasks/createStandup.ts @@ -39,6 +39,13 @@ export default async function createStandup({ auth }: Context): Promise { if (!apiKey) throw new Error('STANDUP_API_KEY environment variable is not set'); const prisma = Container.get(PrismaClient); + + const existing = await prisma.project.findMany({ + where: { event: { id: auth.eventId }, standupId: { not: null } }, + select: { id: true }, + }); + const existingStandupIds = new Set(existing.map((p: { id: string }) => p.id)); + const events = await prisma.event.findMany({ where: { id: auth.eventId, @@ -52,6 +59,11 @@ export default async function createStandup({ auth }: Context): Promise { const teamId = event.slackWorkspaceId; for (const project of event.projects) { + if (existingStandupIds.has(project.id)) { + DEBUG(`Skipping project ${project.id} — standup already exists`); + continue; + } + if (!project.slackChannelId) { DEBUG(`Skipping project ${project.id} — no Slack channel`); continue; @@ -87,6 +99,13 @@ export default async function createStandup({ auth }: Context): Promise { const text = await res.text(); DEBUG(`Failed for project ${project.id}: ${res.status} ${text}`); } else { + const data = await res.json() as { id?: string }; + if (data.id) { + await prisma.project.update({ + where: { id: project.id }, + data: { standupId: data.id }, + }); + } DEBUG(`Created standup for project ${project.id} in channel ${project.slackChannelId}`); } } catch (ex) { From 9987159404aa3633254c3be5d36c673217358b66 Mon Sep 17 00:00:00 2001 From: dotcomaki Date: Wed, 20 May 2026 22:27:34 +0530 Subject: [PATCH 3/7] =?UTF-8?q?Remove=20unnecessary=20EXCLUDED=5FSLACK=5FI?= =?UTF-8?q?DS=20=E2=80=94=20students-only=20filter=20is=20sufficient?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/activities/tasks/createStandup.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/activities/tasks/createStandup.ts b/src/activities/tasks/createStandup.ts index 0311a21..41296ce 100644 --- a/src/activities/tasks/createStandup.ts +++ b/src/activities/tasks/createStandup.ts @@ -12,7 +12,6 @@ import { makeDebug } from "../../utils"; const DEBUG = makeDebug('activities:tasks:createStandup'); const STANDUP_API_BASE = 'https://api.standup-and-prosper.com/v1'; -const EXCLUDED_SLACK_IDS = new Set(['U07ACCWHDSA']); const DEFAULT_STANDUP = { type: 'SLACK', @@ -70,7 +69,7 @@ export default async function createStandup({ auth }: Context): Promise { } const users = project.students - .filter((s) => s.slackId && !EXCLUDED_SLACK_IDS.has(s.slackId)) + .filter((s) => s.slackId) .map((s) => ({ userId: s.slackId! })); if (users.length === 0) { From b066efd51c6fc8ea3a8bf1df12753a8ea557088e Mon Sep 17 00:00:00 2001 From: dotcomaki Date: Wed, 20 May 2026 22:32:18 +0530 Subject: [PATCH 4/7] Use per-event standupAndProsperToken via StandupAndProsper client --- src/activities/tasks/createStandup.ts | 41 +++++++--------------- src/standupAndProsper/StandupAndProsper.ts | 20 +++++++++++ 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/src/activities/tasks/createStandup.ts b/src/activities/tasks/createStandup.ts index 41296ce..493da02 100644 --- a/src/activities/tasks/createStandup.ts +++ b/src/activities/tasks/createStandup.ts @@ -7,12 +7,11 @@ import { SlackStudentInfo, slackEventInfoSelect, } from "../../slack"; +import { EventWithStandupAndProsper, getClientForEvent } from "../../standupAndProsper/StandupAndProsper"; import { makeDebug } from "../../utils"; const DEBUG = makeDebug('activities:tasks:createStandup'); -const STANDUP_API_BASE = 'https://api.standup-and-prosper.com/v1'; - const DEFAULT_STANDUP = { type: 'SLACK', days: ['Monday', 'Wednesday', 'Friday'], @@ -34,9 +33,6 @@ const DEFAULT_STANDUP = { }; export default async function createStandup({ auth }: Context): Promise { - const apiKey = process.env.STANDUP_API_KEY; - if (!apiKey) throw new Error('STANDUP_API_KEY environment variable is not set'); - const prisma = Container.get(PrismaClient); const existing = await prisma.project.findMany({ @@ -50,12 +46,13 @@ export default async function createStandup({ auth }: Context): Promise { id: auth.eventId, slackWorkspaceAccessToken: { not: null }, slackWorkspaceId: { not: null }, + standupAndProsperToken: { not: null }, }, - select: slackEventInfoSelect, - }) as SlackEventWithProjects[]; + select: { ...slackEventInfoSelect, standupAndProsperToken: true }, + }) as (SlackEventWithProjects & EventWithStandupAndProsper)[]; for (const event of events) { - const teamId = event.slackWorkspaceId; + const client = getClientForEvent(event); for (const project of event.projects) { if (existingStandupIds.has(project.id)) { @@ -85,28 +82,14 @@ export default async function createStandup({ auth }: Context): Promise { }; try { - const res = await fetch(`${STANDUP_API_BASE}/teams/${teamId}/standups`, { - method: 'POST', - headers: { - Authorization: `Bearer ${apiKey}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(body), - }); - - if (!res.ok) { - const text = await res.text(); - DEBUG(`Failed for project ${project.id}: ${res.status} ${text}`); - } else { - const data = await res.json() as { id?: string }; - if (data.id) { - await prisma.project.update({ - where: { id: project.id }, - data: { standupId: data.id }, - }); - } - DEBUG(`Created standup for project ${project.id} in channel ${project.slackChannelId}`); + const result = await client.createStandup(body); + if (result.standupId) { + await prisma.project.update({ + where: { id: project.id }, + data: { standupId: result.standupId }, + }); } + DEBUG(`Created standup for project ${project.id} in channel ${project.slackChannelId}`); } catch (ex) { DEBUG(ex); } diff --git a/src/standupAndProsper/StandupAndProsper.ts b/src/standupAndProsper/StandupAndProsper.ts index 4418402..88d05c1 100644 --- a/src/standupAndProsper/StandupAndProsper.ts +++ b/src/standupAndProsper/StandupAndProsper.ts @@ -69,6 +69,26 @@ export class StandupAndPropser { if (!Array.isArray(result.threads)) throw new Error(`Expected array, got ${JSON.stringify(result)}`); return result.threads; } + + async post(path: string, body: object): Promise { + const result = await fetch( + `https://api.standup-and-prosper.com/v1/teams/${this.teamId}${path}`, + { + method: 'POST', + headers: { + Authorization: `Bearer ${this.token}`, + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify(body), + }, + ); + return result.json(); + } + + async createStandup(options: object): Promise { + return this.post('/standups', options); + } } export function getClientForEvent( From 70dd8ca7b1fa8f1df3cfb9847a2d114501197c18 Mon Sep 17 00:00:00 2001 From: Tyler Menezes Date: Thu, 21 May 2026 10:42:29 -0700 Subject: [PATCH 5/7] Refactoring to one event; add bot automatically --- src/activities/tasks/createStandup.ts | 127 ++++++++++++++------------ 1 file changed, 70 insertions(+), 57 deletions(-) diff --git a/src/activities/tasks/createStandup.ts b/src/activities/tasks/createStandup.ts index 493da02..d569c9d 100644 --- a/src/activities/tasks/createStandup.ts +++ b/src/activities/tasks/createStandup.ts @@ -1,31 +1,35 @@ import { PrismaClient } from "@prisma/client"; import Container from "typedi"; -import { Context } from '../../context'; +import { Context } from "../../context"; import { SlackEventWithProjects, SlackMentorInfo, SlackStudentInfo, + getSlackClientForEvent, slackEventInfoSelect, } from "../../slack"; -import { EventWithStandupAndProsper, getClientForEvent } from "../../standupAndProsper/StandupAndProsper"; +import { + EventWithStandupAndProsper, + getClientForEvent, +} from "../../standupAndProsper/StandupAndProsper"; import { makeDebug } from "../../utils"; -const DEBUG = makeDebug('activities:tasks:createStandup'); - +const DEBUG = makeDebug("activities:tasks:createStandup"); +const STANDUP_USER = "U026ZCTG0CB"; const DEFAULT_STANDUP = { - type: 'SLACK', - days: ['Monday', 'Wednesday', 'Friday'], - time: '10:00:00', - reportTime: '12:00:00', - timezone: 'America/Los_Angeles', - schedule: { type: 'WEEKLY' }, + type: "SLACK", + days: ["Monday", "Wednesday", "Friday"], + time: "10:00:00", + reportTime: "12:00:00", + timezone: "America/Los_Angeles", + schedule: { type: "WEEKLY" }, questions: [ - { text: 'What did you do since last standup?' }, - { text: 'What will you do until next standup?' }, - { text: 'Is anything blocking you?' }, + { text: "What did you do since last standup?" }, + { text: "What will you do until next standup?" }, + { text: "Is anything blocking you?" }, ], - groupBy: 'USER_SINGLE_MESSAGE', - reportSortOrder: 'DISPLAY_NAME', + groupBy: "USER_SINGLE_MESSAGE", + reportSortOrder: "DISPLAY_NAME", allowEditsAfterCompletion: true, asThread: false, syncWithChannel: false, @@ -35,64 +39,73 @@ const DEFAULT_STANDUP = { export default async function createStandup({ auth }: Context): Promise { const prisma = Container.get(PrismaClient); - const existing = await prisma.project.findMany({ - where: { event: { id: auth.eventId }, standupId: { not: null } }, - select: { id: true }, - }); - const existingStandupIds = new Set(existing.map((p: { id: string }) => p.id)); - - const events = await prisma.event.findMany({ + const event = (await prisma.event.findFirst({ + rejectOnNotFound: true, where: { id: auth.eventId, slackWorkspaceAccessToken: { not: null }, slackWorkspaceId: { not: null }, standupAndProsperToken: { not: null }, }, - select: { ...slackEventInfoSelect, standupAndProsperToken: true }, - }) as (SlackEventWithProjects & EventWithStandupAndProsper)[]; - - for (const event of events) { - const client = getClientForEvent(event); + select: { + ...slackEventInfoSelect, + projects: { + ...slackEventInfoSelect.projects, + where: { + ...slackEventInfoSelect.projects.where, + standupId: null, + slackChannelId: { not: null }, + students: { + some: { + slackId: { not: null }, + }, + }, + }, + }, + standupAndProsperToken: true, + }, + })) as SlackEventWithProjects & + EventWithStandupAndProsper; - for (const project of event.projects) { - if (existingStandupIds.has(project.id)) { - DEBUG(`Skipping project ${project.id} — standup already exists`); - continue; - } + const client = getClientForEvent(event); + const slack = getSlackClientForEvent(event); - if (!project.slackChannelId) { - DEBUG(`Skipping project ${project.id} — no Slack channel`); - continue; - } + for (const project of event.projects) { + try { + await slack.conversations.invite({ + channel: project.slackChannelId!, + users: STANDUP_USER, + }); + } catch (ex) {} - const users = project.students - .filter((s) => s.slackId) - .map((s) => ({ userId: s.slackId! })); + DEBUG(project.students); + const users = project.students + .filter((s) => s.slackId) + .map((s) => ({ userId: s.slackId! })); - if (users.length === 0) { - DEBUG(`Skipping project ${project.id} — no eligible student Slack IDs`); - continue; - } + if (users.length === 0) { + DEBUG(`Skipping project ${project.id} — no eligible student Slack IDs`); + continue; + } - const body = { + try { + const result = await client.createStandup({ ...DEFAULT_STANDUP, channel: project.slackChannelId, channelId: project.slackChannelId, users, - }; - - try { - const result = await client.createStandup(body); - if (result.standupId) { - await prisma.project.update({ - where: { id: project.id }, - data: { standupId: result.standupId }, - }); - } - DEBUG(`Created standup for project ${project.id} in channel ${project.slackChannelId}`); - } catch (ex) { - DEBUG(ex); + }); + if (result.standupId) { + await prisma.project.update({ + where: { id: project.id }, + data: { standupId: result.standupId }, + }); } + DEBUG( + `Created standup for project ${project.id} in channel ${project.slackChannelId}`, + ); + } catch (ex) { + DEBUG(ex); } } } From 4763f3573db306e72af649f6fcc8de469bbb0b45 Mon Sep 17 00:00:00 2001 From: Tyler Menezes Date: Thu, 21 May 2026 11:16:00 -0700 Subject: [PATCH 6/7] Update standup configuration --- src/activities/tasks/createStandup.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/activities/tasks/createStandup.ts b/src/activities/tasks/createStandup.ts index d569c9d..695b0ba 100644 --- a/src/activities/tasks/createStandup.ts +++ b/src/activities/tasks/createStandup.ts @@ -16,11 +16,13 @@ import { makeDebug } from "../../utils"; const DEBUG = makeDebug("activities:tasks:createStandup"); const STANDUP_USER = "U026ZCTG0CB"; +const ADMIN_USER = "U024H3101"; + const DEFAULT_STANDUP = { - type: "SLACK", + admins: [ADMIN_USER], days: ["Monday", "Wednesday", "Friday"], - time: "10:00:00", - reportTime: "12:00:00", + time: "20:00:00", + reminders: [{ time: "10:00:00" }, { time: "18:00:00" }], timezone: "America/Los_Angeles", schedule: { type: "WEEKLY" }, questions: [ @@ -30,10 +32,10 @@ const DEFAULT_STANDUP = { ], groupBy: "USER_SINGLE_MESSAGE", reportSortOrder: "DISPLAY_NAME", - allowEditsAfterCompletion: true, + allowEditsAfterCompletion: "EXTENDED", asThread: false, syncWithChannel: false, - hideAnnouncements: false, + hideAnnouncements: true, }; export default async function createStandup({ auth }: Context): Promise { From 258a9c2aa4673b8a3dd02792a4e98c594c7eb872 Mon Sep 17 00:00:00 2001 From: dotcomaki Date: Fri, 22 May 2026 00:32:00 +0530 Subject: [PATCH 7/7] Add self as standup admin alongside Tyler --- src/activities/tasks/createStandup.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/activities/tasks/createStandup.ts b/src/activities/tasks/createStandup.ts index 695b0ba..838fdb2 100644 --- a/src/activities/tasks/createStandup.ts +++ b/src/activities/tasks/createStandup.ts @@ -16,10 +16,9 @@ import { makeDebug } from "../../utils"; const DEBUG = makeDebug("activities:tasks:createStandup"); const STANDUP_USER = "U026ZCTG0CB"; -const ADMIN_USER = "U024H3101"; const DEFAULT_STANDUP = { - admins: [ADMIN_USER], + admins: ["U024H3101", "U07ACCWHDSA"], days: ["Monday", "Wednesday", "Friday"], time: "20:00:00", reminders: [{ time: "10:00:00" }, { time: "18:00:00" }],