Public: anyone can read this; the crew can change it.
The prompts settings page and its forms leave settings.ts for src/web/pages/prompts.ts #6319
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/6308-the-prompts-settings-page-and-its-forms → main 3 commits fork
3 files
changed +265
−242
src/http/server.ts +6 −4
| @@ -376,10 +376,6 @@ import { | ||
| 376 | 376 | crewRoleUpdate, |
| 377 | 377 | crewSettingsPage, |
| 378 | 378 | dangerPage, |
| 379 | promptSourceAdd, | |
| 380 | promptSourceDelete, | |
| 381 | promptSourceRun, | |
| 382 | promptsSettingsPage, | |
| 383 | 379 | repoDelete, |
| 384 | 380 | settingsPage, |
| 385 | 381 | settingsUpdate, |
| @@ -389,6 +385,12 @@ import { | ||
| 389 | 385 | mirrorStop, |
| 390 | 386 | } from "../web/pages/settings.ts"; |
| 391 | 387 | import { hooksPage, webhookCreate, webhookDelete } from "../web/pages/hooks.ts"; |
| 388 | import { | |
| 389 | promptSourceAdd, | |
| 390 | promptSourceDelete, | |
| 391 | promptSourceRun, | |
| 392 | promptsSettingsPage, | |
| 393 | } from "../web/pages/prompts.ts"; | |
| 392 | 394 | import { |
| 393 | 395 | labelCreate, |
| 394 | 396 | labelDelete, |
src/web/pages/prompts.ts added +259
| @@ -0,0 +1,259 @@ | ||
| 1 | /** | |
| 2 | * Settings › Prompts, admin-only: the Instagram account whose top comment on | |
| 3 | * the latest Reel becomes the crew's next tell, and the three forms that | |
| 4 | * connect it, fetch it now and remove it. The frame, the tab bar and the | |
| 5 | * admin gate are the settings page's, imported from it. | |
| 6 | */ | |
| 7 | ||
| 8 | import { | |
| 9 | addPromptSource, | |
| 10 | listPromptSources, | |
| 11 | removePromptSource, | |
| 12 | runPromptSource, | |
| 13 | type PromptSourceView, | |
| 14 | } from "../../actions/prompts.ts"; | |
| 15 | import { REFRESH_AFTER_DAYS } from "../../crew/prompts.ts"; | |
| 16 | import { timePhase } from "../../core/route_timing.ts"; | |
| 17 | import type { Ctx } from "../../http/context.ts"; | |
| 18 | import { repoBase, repoNotFound } from "../chrome.ts"; | |
| 19 | import { field } from "../form.ts"; | |
| 20 | import { html, type Html } from "../html.ts"; | |
| 21 | import { icon } from "../icons.ts"; | |
| 22 | import { flashTo } from "../layout.ts"; | |
| 23 | import { | |
| 24 | badge, | |
| 25 | btn, | |
| 26 | card, | |
| 27 | confirmAction, | |
| 28 | field as formField, | |
| 29 | time, | |
| 30 | timeAgo, | |
| 31 | } from "../ui.ts"; | |
| 32 | import { adminForm, settingsPageResponse, timedAdminRepo } from "./settings.ts"; | |
| 33 | ||
| 34 | const PROMPT_ERRORS: Record<string, string> = { | |
| 35 | invalid_at: "The time is hours and minutes in UTC, like 15:00.", | |
| 36 | invalid_token: | |
| 37 | "Paste the long-lived token as Instagram gave it: one line, no spaces.", | |
| 38 | invalid_kind: "Only Instagram is known here.", | |
| 39 | already_connected: | |
| 40 | "An Instagram account is already connected here. Remove it first.", | |
| 41 | no_such_source: "No account by that id.", | |
| 42 | not_running: "The prompt routine is not running on this forge.", | |
| 43 | }; | |
| 44 | ||
| 45 | /** The day the forge will refresh a token pasted or refreshed at `iso`. */ | |
| 46 | function refreshDay(iso: string): string { | |
| 47 | return new Date(Date.parse(iso) + REFRESH_AFTER_DAYS * 24 * 3_600_000) | |
| 48 | .toISOString() | |
| 49 | .slice(0, 10); | |
| 50 | } | |
| 51 | ||
| 52 | /** | |
| 53 | * The Instagram card: the account and what the last run said when one is | |
| 54 | * connected, the form when none is. Beneath it, the one-time steps at Meta, | |
| 55 | * because the forge cannot take them for anyone. The token is a password | |
| 56 | * field on the way in and nowhere on the way out. | |
| 57 | */ | |
| 58 | function promptsView(base: string, sources: PromptSourceView[]): Html { | |
| 59 | const source = sources.find((s) => s.kind === "instagram"); | |
| 60 | const at = (id: string) => | |
| 61 | `${base}/settings/prompts/${encodeURIComponent(id)}`; | |
| 62 | const body = | |
| 63 | source === undefined | |
| 64 | ? html`<div class="card-body"> | |
| 65 | <p class="muted small"> | |
| 66 | Once a day, the top comment on your latest Reel becomes the crew's | |
| 67 | next tell. | |
| 68 | </p> | |
| 69 | </div> | |
| 70 | <form | |
| 71 | class="card-body" | |
| 72 | method="post" | |
| 73 | action="${base}/settings/prompts" | |
| 74 | > | |
| 75 | ${formField({ | |
| 76 | label: "Token", | |
| 77 | name: "token", | |
| 78 | type: "password", | |
| 79 | hint: "A long-lived Instagram token. The forge seals it, refreshes it before it expires, and never shows it again.", | |
| 80 | required: true, | |
| 81 | autocomplete: "off", | |
| 82 | mono: true, | |
| 83 | })} | |
| 84 | ${formField({ | |
| 85 | label: "Every day at … UTC", | |
| 86 | name: "at", | |
| 87 | type: "time", | |
| 88 | value: "15:00", | |
| 89 | required: true, | |
| 90 | })} | |
| 91 | <div class="form-actions"> | |
| 92 | ${btn("Add", { variant: "primary", icon: "plus" })} | |
| 93 | </div> | |
| 94 | </form>` | |
| 95 | : html`<ul class="list"> | |
| 96 | <li> | |
| 97 | <div class="row"> | |
| 98 | <span class="row-lead">${icon("comment")}</span> | |
| 99 | <span class="row-main"> | |
| 100 | <span class="row-title" | |
| 101 | >${source.account === "" | |
| 102 | ? html`<span class="muted">not read yet</span>` | |
| 103 | : `@${source.account}`} | |
| 104 | ${badge(`every day at ${source.atUtc} UTC`)}</span | |
| 105 | > | |
| 106 | <span class="row-meta" | |
| 107 | >${source.lastRanAt === null | |
| 108 | ? "Has not run yet." | |
| 109 | : html`Last ran ${time(source.lastRanAt)}. | |
| 110 | ${source.lastSaid}`}</span | |
| 111 | > | |
| 112 | <span class="row-meta" | |
| 113 | >Token refreshed ${timeAgo(source.tokenRefreshedAt)}; the | |
| 114 | forge refreshes it again around | |
| 115 | ${refreshDay(source.tokenRefreshedAt)}.</span | |
| 116 | > | |
| 117 | </span> | |
| 118 | <span class="row-trail"> | |
| 119 | <form method="post" action="${at(source.id)}/run"> | |
| 120 | ${btn("Fetch now", { size: "sm", icon: "sync" })} | |
| 121 | </form> | |
| 122 | ${confirmAction({ | |
| 123 | action: `${at(source.id)}/delete`, | |
| 124 | verb: "Remove", | |
| 125 | size: "sm", | |
| 126 | })} | |
| 127 | </span> | |
| 128 | </div> | |
| 129 | </li> | |
| 130 | </ul>`; | |
| 131 | return html`${card(body, { | |
| 132 | head: html`${icon("comment")} | |
| 133 | <h2>Instagram</h2>`, | |
| 134 | pad: false, | |
| 135 | })} | |
| 136 | ${card( | |
| 137 | html`<p class="muted small"> | |
| 138 | The one-time steps at Meta, for a professional account of your own. None | |
| 139 | of them needs App Review. | |
| 140 | </p> | |
| 141 | <ol class="muted"> | |
| 142 | <li> | |
| 143 | Make the Instagram account a professional one — Business or Creator — | |
| 144 | in the Instagram app's settings. | |
| 145 | </li> | |
| 146 | <li> | |
| 147 | At developers.facebook.com, make a Meta app and add the Instagram | |
| 148 | product with <b>Business Login for Instagram</b>. No Facebook Page is | |
| 149 | needed. | |
| 150 | </li> | |
| 151 | <li> | |
| 152 | Get a token for the account with the scopes | |
| 153 | <span class="mono">instagram_business_basic</span> and | |
| 154 | <span class="mono">instagram_business_manage_comments</span>. | |
| 155 | </li> | |
| 156 | <li> | |
| 157 | Exchange it for a long-lived token, the one that lives sixty days; the | |
| 158 | product's docs show the one call. | |
| 159 | </li> | |
| 160 | <li> | |
| 161 | Paste the long-lived token above. The forge refreshes it before it | |
| 162 | expires, and the paste is never shown again. | |
| 163 | </li> | |
| 164 | </ol>`, | |
| 165 | { head: html`<h2>Getting a token</h2>` }, | |
| 166 | )}`; | |
| 167 | } | |
| 168 | ||
| 169 | // GET /:owner/:name/settings/prompts | |
| 170 | export function promptsSettingsPage(ctx: Ctx): Response { | |
| 171 | const repo = timedAdminRepo(ctx); | |
| 172 | if (!repo) return repoNotFound(ctx); | |
| 173 | const listed = timePhase(ctx.request, "sources", () => listPromptSources(ctx.app, ctx.auth.user, repo)); | |
| 174 | return timePhase(ctx.request, "render", () => | |
| 175 | settingsPageResponse( | |
| 176 | ctx, | |
| 177 | repo, | |
| 178 | "prompts", | |
| 179 | "Prompts", | |
| 180 | promptsView(repoBase(repo), listed.ok ? listed.value : []), | |
| 181 | ), | |
| 182 | ); | |
| 183 | } | |
| 184 | ||
| 185 | // POST /:owner/:name/settings/prompts — connect an account. | |
| 186 | export async function promptSourceAdd(ctx: Ctx): Promise<Response> { | |
| 187 | const gate = await adminForm(ctx); | |
| 188 | if (gate instanceof Response) return gate; | |
| 189 | const { repo, form } = gate; | |
| 190 | const added = addPromptSource(ctx.app, ctx.auth.user, repo, { | |
| 191 | kind: "instagram", | |
| 192 | token: field(form, "token"), | |
| 193 | at: field(form, "at"), | |
| 194 | }); | |
| 195 | if (!added.ok && added.error === "not_found") return repoNotFound(ctx); | |
| 196 | return flashTo( | |
| 197 | ctx, | |
| 198 | `${repoBase(repo)}/settings/prompts`, | |
| 199 | added.ok | |
| 200 | ? { | |
| 201 | message: `Instagram is connected. Every day at ${added.value.atUtc} UTC the top comment on the latest Reel is told to the crew; press Fetch now to try it.`, | |
| 202 | } | |
| 203 | : { | |
| 204 | tone: "danger", | |
| 205 | message: | |
| 206 | PROMPT_ERRORS[added.error] ?? "The account could not be connected.", | |
| 207 | }, | |
| 208 | ); | |
| 209 | } | |
| 210 | ||
| 211 | // POST /:owner/:name/settings/prompts/:id/run — look at Instagram now. | |
| 212 | export async function promptSourceRun(ctx: Ctx): Promise<Response> { | |
| 213 | const gate = await adminForm(ctx); | |
| 214 | if (gate instanceof Response) return gate; | |
| 215 | const { repo } = gate; | |
| 216 | const ran = await runPromptSource( | |
| 217 | ctx.app, | |
| 218 | ctx.auth.user, | |
| 219 | repo, | |
| 220 | ctx.params.id ?? "", | |
| 221 | ); | |
| 222 | if (!ran.ok && ran.error === "not_found") return repoNotFound(ctx); | |
| 223 | return flashTo( | |
| 224 | ctx, | |
| 225 | `${repoBase(repo)}/settings/prompts`, | |
| 226 | ran.ok | |
| 227 | ? { | |
| 228 | ...(ran.value.lastSaid.startsWith("Told the crew") | |
| 229 | ? {} | |
| 230 | : { tone: "attn" as const }), | |
| 231 | message: ran.value.lastSaid, | |
| 232 | } | |
| 233 | : { | |
| 234 | tone: "danger", | |
| 235 | message: PROMPT_ERRORS[ran.error] ?? "No account by that id.", | |
| 236 | }, | |
| 237 | ); | |
| 238 | } | |
| 239 | ||
| 240 | // POST /:owner/:name/settings/prompts/:id/delete | |
| 241 | export async function promptSourceDelete(ctx: Ctx): Promise<Response> { | |
| 242 | const gate = await adminForm(ctx); | |
| 243 | if (gate instanceof Response) return gate; | |
| 244 | const { repo } = gate; | |
| 245 | const removed = removePromptSource( | |
| 246 | ctx.app, | |
| 247 | ctx.auth.user, | |
| 248 | repo, | |
| 249 | ctx.params.id ?? "", | |
| 250 | ); | |
| 251 | if (!removed.ok && removed.error === "not_found") return repoNotFound(ctx); | |
| 252 | return flashTo( | |
| 253 | ctx, | |
| 254 | `${repoBase(repo)}/settings/prompts`, | |
| 255 | removed.ok | |
| 256 | ? { message: "Instagram is disconnected. The token is forgotten." } | |
| 257 | : { tone: "danger", message: "No account by that id." }, | |
| 258 | ); | |
| 259 | } |
src/web/pages/settings.ts −238
| @@ -31,15 +31,7 @@ import { | ||
| 31 | 31 | revokeAppEgress, |
| 32 | 32 | type EgressView, |
| 33 | 33 | } from "../../actions/egress_grants.ts"; |
| 34 | import { | |
| 35 | addPromptSource, | |
| 36 | listPromptSources, | |
| 37 | removePromptSource, | |
| 38 | runPromptSource, | |
| 39 | type PromptSourceView, | |
| 40 | } from "../../actions/prompts.ts"; | |
| 41 | 34 | import type { DnsRecord } from "../../apps/domains.ts"; |
| 42 | import { REFRESH_AFTER_DAYS } from "../../crew/prompts.ts"; | |
| 43 | 35 | import { |
| 44 | 36 | listAppEnvNames, |
| 45 | 37 | NAME_MAX, |
| @@ -103,7 +95,6 @@ import { | ||
| 103 | 95 | row, |
| 104 | 96 | selectField, |
| 105 | 97 | time, |
| 106 | timeAgo, | |
| 107 | 98 | tlEvent, |
| 108 | 99 | } from "../ui.ts"; |
| 109 | 100 | import { cutWaveFor } from "../../actions/cut_wave.ts"; |
| @@ -1438,235 +1429,6 @@ function redeploying(ctx: Ctx, how: string): string { | ||
| 1438 | 1429 | : ` Production and every preview are starting again ${how}.`; |
| 1439 | 1430 | } |
| 1440 | 1431 | |
| 1441 | // ---- prompts ---- | |
| 1442 | ||
| 1443 | const PROMPT_ERRORS: Record<string, string> = { | |
| 1444 | invalid_at: "The time is hours and minutes in UTC, like 15:00.", | |
| 1445 | invalid_token: | |
| 1446 | "Paste the long-lived token as Instagram gave it: one line, no spaces.", | |
| 1447 | invalid_kind: "Only Instagram is known here.", | |
| 1448 | already_connected: | |
| 1449 | "An Instagram account is already connected here. Remove it first.", | |
| 1450 | no_such_source: "No account by that id.", | |
| 1451 | not_running: "The prompt routine is not running on this forge.", | |
| 1452 | }; | |
| 1453 | ||
| 1454 | /** The day the forge will refresh a token pasted or refreshed at `iso`. */ | |
| 1455 | function refreshDay(iso: string): string { | |
| 1456 | return new Date(Date.parse(iso) + REFRESH_AFTER_DAYS * 24 * 3_600_000) | |
| 1457 | .toISOString() | |
| 1458 | .slice(0, 10); | |
| 1459 | } | |
| 1460 | ||
| 1461 | /** | |
| 1462 | * The Instagram card: the account and what the last run said when one is | |
| 1463 | * connected, the form when none is. Beneath it, the one-time steps at Meta, | |
| 1464 | * because the forge cannot take them for anyone. The token is a password | |
| 1465 | * field on the way in and nowhere on the way out. | |
| 1466 | */ | |
| 1467 | function promptsView(base: string, sources: PromptSourceView[]): Html { | |
| 1468 | const source = sources.find((s) => s.kind === "instagram"); | |
| 1469 | const at = (id: string) => | |
| 1470 | `${base}/settings/prompts/${encodeURIComponent(id)}`; | |
| 1471 | const body = | |
| 1472 | source === undefined | |
| 1473 | ? html`<div class="card-body"> | |
| 1474 | <p class="muted small"> | |
| 1475 | Once a day, the top comment on your latest Reel becomes the crew's | |
| 1476 | next tell. | |
| 1477 | </p> | |
| 1478 | </div> | |
| 1479 | <form | |
| 1480 | class="card-body" | |
| 1481 | method="post" | |
| 1482 | action="${base}/settings/prompts" | |
| 1483 | > | |
| 1484 | ${formField({ | |
| 1485 | label: "Token", | |
| 1486 | name: "token", | |
| 1487 | type: "password", | |
| 1488 | hint: "A long-lived Instagram token. The forge seals it, refreshes it before it expires, and never shows it again.", | |
| 1489 | required: true, | |
| 1490 | autocomplete: "off", | |
| 1491 | mono: true, | |
| 1492 | })} | |
| 1493 | ${formField({ | |
| 1494 | label: "Every day at … UTC", | |
| 1495 | name: "at", | |
| 1496 | type: "time", | |
| 1497 | value: "15:00", | |
| 1498 | required: true, | |
| 1499 | })} | |
| 1500 | <div class="form-actions"> | |
| 1501 | ${btn("Add", { variant: "primary", icon: "plus" })} | |
| 1502 | </div> | |
| 1503 | </form>` | |
| 1504 | : html`<ul class="list"> | |
| 1505 | <li> | |
| 1506 | <div class="row"> | |
| 1507 | <span class="row-lead">${icon("comment")}</span> | |
| 1508 | <span class="row-main"> | |
| 1509 | <span class="row-title" | |
| 1510 | >${source.account === "" | |
| 1511 | ? html`<span class="muted">not read yet</span>` | |
| 1512 | : `@${source.account}`} | |
| 1513 | ${badge(`every day at ${source.atUtc} UTC`)}</span | |
| 1514 | > | |
| 1515 | <span class="row-meta" | |
| 1516 | >${source.lastRanAt === null | |
| 1517 | ? "Has not run yet." | |
| 1518 | : html`Last ran ${time(source.lastRanAt)}. | |
| 1519 | ${source.lastSaid}`}</span | |
| 1520 | > | |
| 1521 | <span class="row-meta" | |
| 1522 | >Token refreshed ${timeAgo(source.tokenRefreshedAt)}; the | |
| 1523 | forge refreshes it again around | |
| 1524 | ${refreshDay(source.tokenRefreshedAt)}.</span | |
| 1525 | > | |
| 1526 | </span> | |
| 1527 | <span class="row-trail"> | |
| 1528 | <form method="post" action="${at(source.id)}/run"> | |
| 1529 | ${btn("Fetch now", { size: "sm", icon: "sync" })} | |
| 1530 | </form> | |
| 1531 | ${confirmAction({ | |
| 1532 | action: `${at(source.id)}/delete`, | |
| 1533 | verb: "Remove", | |
| 1534 | size: "sm", | |
| 1535 | })} | |
| 1536 | </span> | |
| 1537 | </div> | |
| 1538 | </li> | |
| 1539 | </ul>`; | |
| 1540 | return html`${card(body, { | |
| 1541 | head: html`${icon("comment")} | |
| 1542 | <h2>Instagram</h2>`, | |
| 1543 | pad: false, | |
| 1544 | })} | |
| 1545 | ${card( | |
| 1546 | html`<p class="muted small"> | |
| 1547 | The one-time steps at Meta, for a professional account of your own. None | |
| 1548 | of them needs App Review. | |
| 1549 | </p> | |
| 1550 | <ol class="muted"> | |
| 1551 | <li> | |
| 1552 | Make the Instagram account a professional one — Business or Creator — | |
| 1553 | in the Instagram app's settings. | |
| 1554 | </li> | |
| 1555 | <li> | |
| 1556 | At developers.facebook.com, make a Meta app and add the Instagram | |
| 1557 | product with <b>Business Login for Instagram</b>. No Facebook Page is | |
| 1558 | needed. | |
| 1559 | </li> | |
| 1560 | <li> | |
| 1561 | Get a token for the account with the scopes | |
| 1562 | <span class="mono">instagram_business_basic</span> and | |
| 1563 | <span class="mono">instagram_business_manage_comments</span>. | |
| 1564 | </li> | |
| 1565 | <li> | |
| 1566 | Exchange it for a long-lived token, the one that lives sixty days; the | |
| 1567 | product's docs show the one call. | |
| 1568 | </li> | |
| 1569 | <li> | |
| 1570 | Paste the long-lived token above. The forge refreshes it before it | |
| 1571 | expires, and the paste is never shown again. | |
| 1572 | </li> | |
| 1573 | </ol>`, | |
| 1574 | { head: html`<h2>Getting a token</h2>` }, | |
| 1575 | )}`; | |
| 1576 | } | |
| 1577 | ||
| 1578 | // GET /:owner/:name/settings/prompts | |
| 1579 | export function promptsSettingsPage(ctx: Ctx): Response { | |
| 1580 | const repo = timedAdminRepo(ctx); | |
| 1581 | if (!repo) return repoNotFound(ctx); | |
| 1582 | const listed = timePhase(ctx.request, "sources", () => listPromptSources(ctx.app, ctx.auth.user, repo)); | |
| 1583 | return timePhase(ctx.request, "render", () => | |
| 1584 | settingsPageResponse( | |
| 1585 | ctx, | |
| 1586 | repo, | |
| 1587 | "prompts", | |
| 1588 | "Prompts", | |
| 1589 | promptsView(repoBase(repo), listed.ok ? listed.value : []), | |
| 1590 | ), | |
| 1591 | ); | |
| 1592 | } | |
| 1593 | ||
| 1594 | // POST /:owner/:name/settings/prompts — connect an account. | |
| 1595 | export async function promptSourceAdd(ctx: Ctx): Promise<Response> { | |
| 1596 | const gate = await adminForm(ctx); | |
| 1597 | if (gate instanceof Response) return gate; | |
| 1598 | const { repo, form } = gate; | |
| 1599 | const added = addPromptSource(ctx.app, ctx.auth.user, repo, { | |
| 1600 | kind: "instagram", | |
| 1601 | token: field(form, "token"), | |
| 1602 | at: field(form, "at"), | |
| 1603 | }); | |
| 1604 | if (!added.ok && added.error === "not_found") return repoNotFound(ctx); | |
| 1605 | return flashTo( | |
| 1606 | ctx, | |
| 1607 | `${repoBase(repo)}/settings/prompts`, | |
| 1608 | added.ok | |
| 1609 | ? { | |
| 1610 | message: `Instagram is connected. Every day at ${added.value.atUtc} UTC the top comment on the latest Reel is told to the crew; press Fetch now to try it.`, | |
| 1611 | } | |
| 1612 | : { | |
| 1613 | tone: "danger", | |
| 1614 | message: | |
| 1615 | PROMPT_ERRORS[added.error] ?? "The account could not be connected.", | |
| 1616 | }, | |
| 1617 | ); | |
| 1618 | } | |
| 1619 | ||
| 1620 | // POST /:owner/:name/settings/prompts/:id/run — look at Instagram now. | |
| 1621 | export async function promptSourceRun(ctx: Ctx): Promise<Response> { | |
| 1622 | const gate = await adminForm(ctx); | |
| 1623 | if (gate instanceof Response) return gate; | |
| 1624 | const { repo } = gate; | |
| 1625 | const ran = await runPromptSource( | |
| 1626 | ctx.app, | |
| 1627 | ctx.auth.user, | |
| 1628 | repo, | |
| 1629 | ctx.params.id ?? "", | |
| 1630 | ); | |
| 1631 | if (!ran.ok && ran.error === "not_found") return repoNotFound(ctx); | |
| 1632 | return flashTo( | |
| 1633 | ctx, | |
| 1634 | `${repoBase(repo)}/settings/prompts`, | |
| 1635 | ran.ok | |
| 1636 | ? { | |
| 1637 | ...(ran.value.lastSaid.startsWith("Told the crew") | |
| 1638 | ? {} | |
| 1639 | : { tone: "attn" as const }), | |
| 1640 | message: ran.value.lastSaid, | |
| 1641 | } | |
| 1642 | : { | |
| 1643 | tone: "danger", | |
| 1644 | message: PROMPT_ERRORS[ran.error] ?? "No account by that id.", | |
| 1645 | }, | |
| 1646 | ); | |
| 1647 | } | |
| 1648 | ||
| 1649 | // POST /:owner/:name/settings/prompts/:id/delete | |
| 1650 | export async function promptSourceDelete(ctx: Ctx): Promise<Response> { | |
| 1651 | const gate = await adminForm(ctx); | |
| 1652 | if (gate instanceof Response) return gate; | |
| 1653 | const { repo } = gate; | |
| 1654 | const removed = removePromptSource( | |
| 1655 | ctx.app, | |
| 1656 | ctx.auth.user, | |
| 1657 | repo, | |
| 1658 | ctx.params.id ?? "", | |
| 1659 | ); | |
| 1660 | if (!removed.ok && removed.error === "not_found") return repoNotFound(ctx); | |
| 1661 | return flashTo( | |
| 1662 | ctx, | |
| 1663 | `${repoBase(repo)}/settings/prompts`, | |
| 1664 | removed.ok | |
| 1665 | ? { message: "Instagram is disconnected. The token is forgotten." } | |
| 1666 | : { tone: "danger", message: "No account by that id." }, | |
| 1667 | ); | |
| 1668 | } | |
| 1669 | ||
| 1670 | 1432 | // ---- crew ---- |
| 1671 | 1433 | |
| 1672 | 1434 | // GET /:owner/:name/settings/crew |
new above
offline