@cuylabs/channel-slack 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -136
- package/dist/app-home.d.ts +23 -0
- package/dist/app-home.js +40 -0
- package/dist/artifacts/index.d.ts +135 -0
- package/dist/artifacts/index.js +299 -0
- package/dist/{assistant.d.ts → assistant/index.d.ts} +1 -1
- package/dist/{assistant.js → assistant/index.js} +2 -2
- package/dist/auth/index.d.ts +56 -0
- package/dist/auth/index.js +168 -0
- package/dist/{chunk-IDVDMJ5U.js → chunk-6JSGIVQH.js} +110 -3
- package/dist/chunk-6WHFQUYQ.js +54 -0
- package/dist/{bolt.js → chunk-73QXT7MA.js} +25 -320
- package/dist/{chunk-CMR6B76C.js → chunk-DNVSH7H5.js} +407 -1
- package/dist/chunk-IRFKUPJN.js +235 -0
- package/dist/chunk-QJYCHWN6.js +76 -0
- package/dist/chunk-S3SWPYXJ.js +81 -0
- package/dist/{chunk-JZG4IETE.js → chunk-X4WBBBYM.js} +0 -52
- package/dist/core.js +5 -3
- package/dist/diagnostics/index.d.ts +71 -0
- package/dist/{diagnostics.js → diagnostics/index.js} +5 -1
- package/dist/entrypoints/index.d.ts +120 -0
- package/dist/entrypoints/index.js +132 -0
- package/dist/{feedback.js → feedback/index.js} +5 -7
- package/dist/{history.d.ts → history/index.d.ts} +2 -2
- package/dist/{history.js → history/index.js} +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +28 -15
- package/dist/{policy.d.ts → policy/index.d.ts} +103 -2
- package/dist/{policy.js → policy/index.js} +13 -1
- package/dist/runtime-BNBHOZSQ.d.ts +53 -0
- package/dist/{setup.d.ts → setup/index.d.ts} +30 -3
- package/dist/{setup.js → setup/index.js} +137 -3
- package/dist/transports/http/index.d.ts +68 -0
- package/dist/transports/http/index.js +8 -0
- package/dist/transports/index.d.ts +8 -0
- package/dist/transports/index.js +24 -0
- package/dist/transports/socket/index.d.ts +94 -0
- package/dist/transports/socket/index.js +19 -0
- package/dist/types-B9NfCVrk.d.ts +141 -0
- package/dist/views/index.d.ts +98 -0
- package/dist/views/index.js +22 -0
- package/docs/README.md +32 -0
- package/docs/concepts/activity.md +3 -3
- package/docs/concepts/artifacts.md +56 -0
- package/docs/concepts/entrypoints.md +73 -0
- package/docs/concepts/setup-requirements.md +23 -0
- package/docs/concepts/{bolt-runtime.md → transport-runtime.md} +9 -4
- package/docs/concepts/views.md +46 -0
- package/docs/recipes/generate-slack-manifest.md +16 -0
- package/docs/recipes/publish-artifact.md +45 -0
- package/docs/recipes/slash-command-and-shortcut.md +51 -0
- package/docs/recipes/socket-mode-app.md +1 -1
- package/docs/reference/channel-slack-boundary.md +10 -6
- package/docs/reference/exports.md +18 -12
- package/docs/reference/source-layout.md +36 -0
- package/package.json +68 -39
- package/dist/bolt.d.ts +0 -364
- package/dist/chunk-NE57BLLU.js +0 -0
- package/dist/diagnostics.d.ts +0 -22
- package/dist/shared.d.ts +0 -2
- package/dist/shared.js +0 -43
- /package/dist/{feedback.d.ts → feedback/index.d.ts} +0 -0
- /package/dist/{targets.d.ts → targets/index.d.ts} +0 -0
- /package/dist/{targets.js → targets/index.js} +0 -0
- /package/dist/{users.d.ts → users/index.d.ts} +0 -0
- /package/dist/{users.js → users/index.js} +0 -0
|
@@ -651,6 +651,406 @@ function createSlackMessagePolicyResolver(config = {}) {
|
|
|
651
651
|
};
|
|
652
652
|
}
|
|
653
653
|
|
|
654
|
+
// src/policy/thread-participation.ts
|
|
655
|
+
var DEFAULT_MAX_THREADS = 1e4;
|
|
656
|
+
var DEFAULT_POSTGRES_TABLE = "channel_slack_thread_participation";
|
|
657
|
+
var DEFAULT_POSTGRES_RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
658
|
+
var DEFAULT_POSTGRES_PRUNE_BATCH_SIZE = 1e3;
|
|
659
|
+
var DEFAULT_POSTGRES_PRUNE_INTERVAL_MS = 6 * 60 * 60 * 1e3;
|
|
660
|
+
function resolveSlackThreadParticipationEligibility({
|
|
661
|
+
activity,
|
|
662
|
+
policy,
|
|
663
|
+
stateStore
|
|
664
|
+
}) {
|
|
665
|
+
if (!stateStore) {
|
|
666
|
+
return { available: false, reason: "state-store-unavailable" };
|
|
667
|
+
}
|
|
668
|
+
if (activity.channelType === "dm") {
|
|
669
|
+
return { available: false, reason: "direct-message" };
|
|
670
|
+
}
|
|
671
|
+
if (activity.channelType !== "channel" && activity.channelType !== "thread") {
|
|
672
|
+
return { available: false, reason: "unsupported-channel-type" };
|
|
673
|
+
}
|
|
674
|
+
if (policy.messagePolicy !== "mentioned-threads") {
|
|
675
|
+
return {
|
|
676
|
+
available: false,
|
|
677
|
+
reason: "message-policy-not-mentioned-threads"
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
if (policy.threadReplyPolicy === "mention-required") {
|
|
681
|
+
return {
|
|
682
|
+
available: false,
|
|
683
|
+
reason: "thread-replies-require-mention"
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
const threadTs = activity.threadTs ?? activity.messageTs;
|
|
687
|
+
if (!threadTs) {
|
|
688
|
+
return { available: false, reason: "missing-thread-timestamp" };
|
|
689
|
+
}
|
|
690
|
+
return {
|
|
691
|
+
available: true,
|
|
692
|
+
threadKey: createSlackMessagePolicyThreadKey(activity),
|
|
693
|
+
threadTs
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
function createInMemorySlackThreadParticipationStateStore({
|
|
697
|
+
maxThreads = DEFAULT_MAX_THREADS,
|
|
698
|
+
now = () => /* @__PURE__ */ new Date()
|
|
699
|
+
} = {}) {
|
|
700
|
+
const states = /* @__PURE__ */ new Map();
|
|
701
|
+
const order = [];
|
|
702
|
+
const resolvedMaxThreads = Math.max(1, maxThreads);
|
|
703
|
+
function rememberKey(key) {
|
|
704
|
+
if (!states.has(key)) {
|
|
705
|
+
order.push(key);
|
|
706
|
+
}
|
|
707
|
+
while (order.length > resolvedMaxThreads) {
|
|
708
|
+
const oldest = order.shift();
|
|
709
|
+
if (oldest !== void 0) {
|
|
710
|
+
states.delete(oldest);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return {
|
|
715
|
+
async get(key) {
|
|
716
|
+
return states.get(key);
|
|
717
|
+
},
|
|
718
|
+
async setQuiet(key, state) {
|
|
719
|
+
rememberKey(key);
|
|
720
|
+
states.set(key, {
|
|
721
|
+
...state,
|
|
722
|
+
mode: "quiet",
|
|
723
|
+
updatedAt: now().toISOString()
|
|
724
|
+
});
|
|
725
|
+
},
|
|
726
|
+
async clear(key) {
|
|
727
|
+
states.delete(key);
|
|
728
|
+
const index = order.indexOf(key);
|
|
729
|
+
if (index >= 0) {
|
|
730
|
+
order.splice(index, 1);
|
|
731
|
+
}
|
|
732
|
+
},
|
|
733
|
+
async prune() {
|
|
734
|
+
return { quietThreadsDeleted: 0 };
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
function createPostgresSlackThreadParticipationStateStore({
|
|
739
|
+
client,
|
|
740
|
+
connectionString,
|
|
741
|
+
ensureSchema = true,
|
|
742
|
+
onPruneError,
|
|
743
|
+
pruneBatchSize = DEFAULT_POSTGRES_PRUNE_BATCH_SIZE,
|
|
744
|
+
pruneIntervalMs = DEFAULT_POSTGRES_PRUNE_INTERVAL_MS,
|
|
745
|
+
retentionMs = DEFAULT_POSTGRES_RETENTION_MS,
|
|
746
|
+
schema,
|
|
747
|
+
tableName = DEFAULT_POSTGRES_TABLE
|
|
748
|
+
}) {
|
|
749
|
+
let activeClient = client;
|
|
750
|
+
let ownsClient = false;
|
|
751
|
+
let initialized;
|
|
752
|
+
let pruneTimer;
|
|
753
|
+
async function getClient() {
|
|
754
|
+
if (activeClient) {
|
|
755
|
+
return activeClient;
|
|
756
|
+
}
|
|
757
|
+
if (!connectionString) {
|
|
758
|
+
throw new Error(
|
|
759
|
+
"connectionString is required when a Postgres Slack thread participation client is not provided"
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
const Pool = await importPostgresPoolConstructor2();
|
|
763
|
+
activeClient = new Pool({ connectionString });
|
|
764
|
+
ownsClient = true;
|
|
765
|
+
return activeClient;
|
|
766
|
+
}
|
|
767
|
+
async function ensureInitialized() {
|
|
768
|
+
const currentClient = await getClient();
|
|
769
|
+
initialized ??= initializePostgresSlackThreadParticipationState({
|
|
770
|
+
client: currentClient,
|
|
771
|
+
ensureSchema,
|
|
772
|
+
schema,
|
|
773
|
+
tableName
|
|
774
|
+
});
|
|
775
|
+
await initialized;
|
|
776
|
+
startPruneTimer();
|
|
777
|
+
return currentClient;
|
|
778
|
+
}
|
|
779
|
+
function startPruneTimer() {
|
|
780
|
+
if (pruneTimer || pruneIntervalMs <= 0) {
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
pruneTimer = setInterval(() => {
|
|
784
|
+
void prune().catch((error) => {
|
|
785
|
+
onPruneError?.(error);
|
|
786
|
+
});
|
|
787
|
+
}, pruneIntervalMs);
|
|
788
|
+
pruneTimer.unref?.();
|
|
789
|
+
}
|
|
790
|
+
async function prune() {
|
|
791
|
+
const currentClient = await ensureInitialized();
|
|
792
|
+
return prunePostgresSlackThreadParticipationState({
|
|
793
|
+
client: currentClient,
|
|
794
|
+
pruneBatchSize,
|
|
795
|
+
retentionMs,
|
|
796
|
+
schema,
|
|
797
|
+
tableName
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
return {
|
|
801
|
+
async get(key) {
|
|
802
|
+
const currentClient = await ensureInitialized();
|
|
803
|
+
const result = await currentClient.query(
|
|
804
|
+
`SELECT
|
|
805
|
+
mode,
|
|
806
|
+
reason,
|
|
807
|
+
source_message_ts,
|
|
808
|
+
updated_at,
|
|
809
|
+
updated_by_user_id
|
|
810
|
+
FROM ${qualifiedTableName2({ schema, tableName })}
|
|
811
|
+
WHERE key = $1::text`,
|
|
812
|
+
[key]
|
|
813
|
+
);
|
|
814
|
+
const row = result.rows[0];
|
|
815
|
+
if (!row || row.mode !== "quiet") {
|
|
816
|
+
return void 0;
|
|
817
|
+
}
|
|
818
|
+
return {
|
|
819
|
+
mode: "quiet",
|
|
820
|
+
...row.reason ? { reason: row.reason } : {},
|
|
821
|
+
...row.source_message_ts ? { sourceMessageTs: row.source_message_ts } : {},
|
|
822
|
+
updatedAt: row.updated_at instanceof Date ? row.updated_at.toISOString() : String(row.updated_at),
|
|
823
|
+
...row.updated_by_user_id ? { updatedByUserId: row.updated_by_user_id } : {}
|
|
824
|
+
};
|
|
825
|
+
},
|
|
826
|
+
async setQuiet(key, state, context) {
|
|
827
|
+
const currentClient = await ensureInitialized();
|
|
828
|
+
await currentClient.query(
|
|
829
|
+
`INSERT INTO ${qualifiedTableName2({ schema, tableName })} (
|
|
830
|
+
key,
|
|
831
|
+
team_id,
|
|
832
|
+
channel_id,
|
|
833
|
+
thread_ts,
|
|
834
|
+
mode,
|
|
835
|
+
reason,
|
|
836
|
+
updated_by_user_id,
|
|
837
|
+
source_message_ts,
|
|
838
|
+
updated_at
|
|
839
|
+
)
|
|
840
|
+
VALUES ($1::text, $2::text, $3::text, $4::text, 'quiet', $5::text, $6::text, $7::text, now())
|
|
841
|
+
ON CONFLICT (key) DO UPDATE SET
|
|
842
|
+
team_id = EXCLUDED.team_id,
|
|
843
|
+
channel_id = EXCLUDED.channel_id,
|
|
844
|
+
thread_ts = EXCLUDED.thread_ts,
|
|
845
|
+
mode = 'quiet',
|
|
846
|
+
reason = EXCLUDED.reason,
|
|
847
|
+
updated_by_user_id = EXCLUDED.updated_by_user_id,
|
|
848
|
+
source_message_ts = EXCLUDED.source_message_ts,
|
|
849
|
+
updated_at = now()`,
|
|
850
|
+
[
|
|
851
|
+
key,
|
|
852
|
+
context.activity.teamId ?? null,
|
|
853
|
+
context.activity.channelId,
|
|
854
|
+
context.activity.threadTs ?? context.activity.messageTs ?? null,
|
|
855
|
+
state.reason ?? null,
|
|
856
|
+
state.updatedByUserId ?? null,
|
|
857
|
+
state.sourceMessageTs ?? context.activity.messageTs ?? null
|
|
858
|
+
]
|
|
859
|
+
);
|
|
860
|
+
},
|
|
861
|
+
async clear(key) {
|
|
862
|
+
const currentClient = await ensureInitialized();
|
|
863
|
+
await currentClient.query(
|
|
864
|
+
`DELETE FROM ${qualifiedTableName2({ schema, tableName })}
|
|
865
|
+
WHERE key = $1::text`,
|
|
866
|
+
[key]
|
|
867
|
+
);
|
|
868
|
+
},
|
|
869
|
+
prune,
|
|
870
|
+
async close() {
|
|
871
|
+
if (pruneTimer) {
|
|
872
|
+
clearInterval(pruneTimer);
|
|
873
|
+
pruneTimer = void 0;
|
|
874
|
+
}
|
|
875
|
+
if (ownsClient) {
|
|
876
|
+
await activeClient?.end?.();
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
async function initializePostgresSlackThreadParticipationState({
|
|
882
|
+
client,
|
|
883
|
+
ensureSchema = true,
|
|
884
|
+
schema,
|
|
885
|
+
tableName = DEFAULT_POSTGRES_TABLE
|
|
886
|
+
}) {
|
|
887
|
+
if (schema && ensureSchema) {
|
|
888
|
+
await client.query(
|
|
889
|
+
`CREATE SCHEMA IF NOT EXISTS ${quoteIdentifier2(schema)}`
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
const table = qualifiedTableName2({ schema, tableName });
|
|
893
|
+
await client.query(`
|
|
894
|
+
CREATE TABLE IF NOT EXISTS ${table} (
|
|
895
|
+
key text PRIMARY KEY,
|
|
896
|
+
team_id text,
|
|
897
|
+
channel_id text NOT NULL,
|
|
898
|
+
thread_ts text,
|
|
899
|
+
mode text NOT NULL CHECK (mode IN ('quiet')),
|
|
900
|
+
reason text,
|
|
901
|
+
updated_by_user_id text,
|
|
902
|
+
source_message_ts text,
|
|
903
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
904
|
+
)
|
|
905
|
+
`);
|
|
906
|
+
const indexPrefix = slackThreadParticipationIndexPrefix({
|
|
907
|
+
schema,
|
|
908
|
+
tableName
|
|
909
|
+
});
|
|
910
|
+
await client.query(
|
|
911
|
+
`CREATE INDEX IF NOT EXISTS ${quoteIdentifier2(
|
|
912
|
+
`${indexPrefix}_updated_idx`
|
|
913
|
+
)} ON ${table} (updated_at DESC)`
|
|
914
|
+
);
|
|
915
|
+
await client.query(
|
|
916
|
+
`CREATE INDEX IF NOT EXISTS ${quoteIdentifier2(
|
|
917
|
+
`${indexPrefix}_channel_thread_idx`
|
|
918
|
+
)} ON ${table} (channel_id, thread_ts)`
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
async function prunePostgresSlackThreadParticipationState({
|
|
922
|
+
client,
|
|
923
|
+
pruneBatchSize = DEFAULT_POSTGRES_PRUNE_BATCH_SIZE,
|
|
924
|
+
retentionMs = DEFAULT_POSTGRES_RETENTION_MS,
|
|
925
|
+
schema,
|
|
926
|
+
tableName = DEFAULT_POSTGRES_TABLE
|
|
927
|
+
}) {
|
|
928
|
+
if (retentionMs <= 0) {
|
|
929
|
+
return { quietThreadsDeleted: 0 };
|
|
930
|
+
}
|
|
931
|
+
const batchSize = Math.max(1, Math.floor(pruneBatchSize));
|
|
932
|
+
const table = qualifiedTableName2({ schema, tableName });
|
|
933
|
+
const result = await client.query(
|
|
934
|
+
`WITH expired AS (
|
|
935
|
+
SELECT key
|
|
936
|
+
FROM ${table}
|
|
937
|
+
WHERE updated_at < now() - ($1::bigint * interval '1 millisecond')
|
|
938
|
+
ORDER BY updated_at ASC
|
|
939
|
+
LIMIT $2::integer
|
|
940
|
+
)
|
|
941
|
+
DELETE FROM ${table} target
|
|
942
|
+
USING expired
|
|
943
|
+
WHERE target.key = expired.key`,
|
|
944
|
+
[Math.max(1, Math.floor(retentionMs)), batchSize]
|
|
945
|
+
);
|
|
946
|
+
return { quietThreadsDeleted: result.rowCount ?? 0 };
|
|
947
|
+
}
|
|
948
|
+
function qualifiedTableName2({
|
|
949
|
+
schema,
|
|
950
|
+
tableName
|
|
951
|
+
}) {
|
|
952
|
+
return schema ? `${quoteIdentifier2(schema)}.${quoteIdentifier2(tableName)}` : quoteIdentifier2(tableName);
|
|
953
|
+
}
|
|
954
|
+
function quoteIdentifier2(value) {
|
|
955
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
956
|
+
}
|
|
957
|
+
function slackThreadParticipationIndexPrefix({
|
|
958
|
+
schema,
|
|
959
|
+
tableName
|
|
960
|
+
}) {
|
|
961
|
+
const raw = [schema, tableName].filter(Boolean).join("_");
|
|
962
|
+
return raw.replace(/[^A-Za-z0-9_]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 40) || "channel_slack_thread_participation";
|
|
963
|
+
}
|
|
964
|
+
async function importPostgresPoolConstructor2() {
|
|
965
|
+
const dynamicImport = new Function(
|
|
966
|
+
"specifier",
|
|
967
|
+
"return import(specifier)"
|
|
968
|
+
);
|
|
969
|
+
try {
|
|
970
|
+
const pg = await dynamicImport("pg");
|
|
971
|
+
return pg.Pool;
|
|
972
|
+
} catch (error) {
|
|
973
|
+
throw new Error(
|
|
974
|
+
`The "pg" package is required when using connectionString with createPostgresSlackThreadParticipationStateStore. Install pg or pass a client. ${formatImportError2(
|
|
975
|
+
error
|
|
976
|
+
)}`
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
function formatImportError2(error) {
|
|
981
|
+
return error instanceof Error ? error.message : String(error);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// src/policy/thread-message-policy.ts
|
|
985
|
+
function createAsyncSlackThreadAwareMessagePolicyResolver(config = {}) {
|
|
986
|
+
const { threadParticipationStore, ...messagePolicyConfig } = config;
|
|
987
|
+
const resolver = createAsyncSlackMessagePolicyResolver(messagePolicyConfig);
|
|
988
|
+
async function resolve(activity) {
|
|
989
|
+
const threadParticipation = await resolveThreadParticipationState({
|
|
990
|
+
activity,
|
|
991
|
+
config,
|
|
992
|
+
stateStore: threadParticipationStore
|
|
993
|
+
});
|
|
994
|
+
if (threadParticipation?.quiet) {
|
|
995
|
+
if (!activity.isMention) {
|
|
996
|
+
return {
|
|
997
|
+
accepted: false,
|
|
998
|
+
activity,
|
|
999
|
+
reason: "quiet-thread",
|
|
1000
|
+
threadKey: threadParticipation.key,
|
|
1001
|
+
...threadParticipation.reason ? { threadParticipationReason: threadParticipation.reason } : {},
|
|
1002
|
+
...threadParticipation.threadTs ? { threadTs: threadParticipation.threadTs } : {}
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
await threadParticipationStore?.clear(threadParticipation.key, {
|
|
1006
|
+
activity,
|
|
1007
|
+
key: threadParticipation.key
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
const decision = await resolver.resolve(activity);
|
|
1011
|
+
if (!threadParticipation?.quiet || !activity.isMention) {
|
|
1012
|
+
return decision;
|
|
1013
|
+
}
|
|
1014
|
+
return {
|
|
1015
|
+
...decision,
|
|
1016
|
+
reactivatedThreadParticipation: {
|
|
1017
|
+
threadKey: threadParticipation.key,
|
|
1018
|
+
...threadParticipation.reason ? { reason: threadParticipation.reason } : {},
|
|
1019
|
+
...threadParticipation.threadTs ? { threadTs: threadParticipation.threadTs } : {}
|
|
1020
|
+
}
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
return {
|
|
1024
|
+
resolve,
|
|
1025
|
+
async resolveMessage(activity) {
|
|
1026
|
+
const decision = await resolve(activity);
|
|
1027
|
+
return decision.accepted ? decision.text : void 0;
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
async function resolveThreadParticipationState({
|
|
1032
|
+
activity,
|
|
1033
|
+
config,
|
|
1034
|
+
stateStore
|
|
1035
|
+
}) {
|
|
1036
|
+
const eligibility = resolveSlackThreadParticipationEligibility({
|
|
1037
|
+
activity,
|
|
1038
|
+
policy: config,
|
|
1039
|
+
stateStore
|
|
1040
|
+
});
|
|
1041
|
+
if (!eligibility.available || !eligibility.threadKey || !stateStore) {
|
|
1042
|
+
return void 0;
|
|
1043
|
+
}
|
|
1044
|
+
const key = eligibility.threadKey;
|
|
1045
|
+
const state = await stateStore.get(key, { activity, key });
|
|
1046
|
+
return state?.mode === "quiet" ? {
|
|
1047
|
+
key,
|
|
1048
|
+
quiet: true,
|
|
1049
|
+
...state.reason ? { reason: state.reason } : {},
|
|
1050
|
+
...eligibility.threadTs ? { threadTs: eligibility.threadTs } : {}
|
|
1051
|
+
} : void 0;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
654
1054
|
export {
|
|
655
1055
|
createSlackMessagePolicyMessageKey,
|
|
656
1056
|
createSlackMessagePolicyThreadKey,
|
|
@@ -660,5 +1060,11 @@ export {
|
|
|
660
1060
|
initializePostgresSlackMessagePolicyState,
|
|
661
1061
|
prunePostgresSlackMessagePolicyState,
|
|
662
1062
|
shouldRegisterSlackPassiveChannelMessages,
|
|
663
|
-
createSlackMessagePolicyResolver
|
|
1063
|
+
createSlackMessagePolicyResolver,
|
|
1064
|
+
resolveSlackThreadParticipationEligibility,
|
|
1065
|
+
createInMemorySlackThreadParticipationStateStore,
|
|
1066
|
+
createPostgresSlackThreadParticipationStateStore,
|
|
1067
|
+
initializePostgresSlackThreadParticipationState,
|
|
1068
|
+
prunePostgresSlackThreadParticipationState,
|
|
1069
|
+
createAsyncSlackThreadAwareMessagePolicyResolver
|
|
664
1070
|
};
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// src/views/client.ts
|
|
2
|
+
async function openSlackModal({
|
|
3
|
+
client,
|
|
4
|
+
triggerId,
|
|
5
|
+
view,
|
|
6
|
+
token
|
|
7
|
+
}) {
|
|
8
|
+
return await requireViewsMethod(
|
|
9
|
+
client,
|
|
10
|
+
"open"
|
|
11
|
+
)({
|
|
12
|
+
...tokenArgs(token),
|
|
13
|
+
trigger_id: requireTrimmed(triggerId, "triggerId"),
|
|
14
|
+
view
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
async function pushSlackModal({
|
|
18
|
+
client,
|
|
19
|
+
triggerId,
|
|
20
|
+
view,
|
|
21
|
+
token
|
|
22
|
+
}) {
|
|
23
|
+
return await requireViewsMethod(
|
|
24
|
+
client,
|
|
25
|
+
"push"
|
|
26
|
+
)({
|
|
27
|
+
...tokenArgs(token),
|
|
28
|
+
trigger_id: requireTrimmed(triggerId, "triggerId"),
|
|
29
|
+
view
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
async function updateSlackView({
|
|
33
|
+
client,
|
|
34
|
+
view,
|
|
35
|
+
viewId,
|
|
36
|
+
externalId,
|
|
37
|
+
hash,
|
|
38
|
+
token
|
|
39
|
+
}) {
|
|
40
|
+
const normalizedViewId = trimOptional(viewId);
|
|
41
|
+
const normalizedExternalId = trimOptional(externalId);
|
|
42
|
+
if (!normalizedViewId && !normalizedExternalId) {
|
|
43
|
+
throw new Error("updateSlackView requires viewId or externalId.");
|
|
44
|
+
}
|
|
45
|
+
return await requireViewsMethod(
|
|
46
|
+
client,
|
|
47
|
+
"update"
|
|
48
|
+
)({
|
|
49
|
+
...tokenArgs(token),
|
|
50
|
+
...normalizedViewId ? { view_id: normalizedViewId } : {},
|
|
51
|
+
...normalizedExternalId ? { external_id: normalizedExternalId } : {},
|
|
52
|
+
...trimOptional(hash) ? { hash: hash?.trim() } : {},
|
|
53
|
+
view
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
async function publishSlackHomeView({
|
|
57
|
+
client,
|
|
58
|
+
userId,
|
|
59
|
+
view,
|
|
60
|
+
hash,
|
|
61
|
+
token
|
|
62
|
+
}) {
|
|
63
|
+
return await requireViewsMethod(
|
|
64
|
+
client,
|
|
65
|
+
"publish"
|
|
66
|
+
)({
|
|
67
|
+
...tokenArgs(token),
|
|
68
|
+
user_id: requireTrimmed(userId, "userId"),
|
|
69
|
+
...trimOptional(hash) ? { hash: hash?.trim() } : {},
|
|
70
|
+
view
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function requireViewsMethod(client, methodName) {
|
|
74
|
+
const method = client.views?.[methodName];
|
|
75
|
+
if (typeof method !== "function") {
|
|
76
|
+
throw new Error(`Slack client does not expose views.${methodName}.`);
|
|
77
|
+
}
|
|
78
|
+
return method.bind(client.views);
|
|
79
|
+
}
|
|
80
|
+
function requireTrimmed(value, label) {
|
|
81
|
+
const trimmed = value.trim();
|
|
82
|
+
if (!trimmed) {
|
|
83
|
+
throw new Error(`${label} is required.`);
|
|
84
|
+
}
|
|
85
|
+
return trimmed;
|
|
86
|
+
}
|
|
87
|
+
function trimOptional(value) {
|
|
88
|
+
const trimmed = value?.trim();
|
|
89
|
+
return trimmed ? trimmed : void 0;
|
|
90
|
+
}
|
|
91
|
+
function tokenArgs(token) {
|
|
92
|
+
const trimmed = token?.trim();
|
|
93
|
+
return trimmed ? { token: trimmed } : {};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/views/metadata.ts
|
|
97
|
+
var SLACK_VIEW_PRIVATE_METADATA_MAX_CHARS = 3e3;
|
|
98
|
+
function encodeSlackViewPrivateMetadata(value) {
|
|
99
|
+
if (value === void 0 || value === null) {
|
|
100
|
+
return "";
|
|
101
|
+
}
|
|
102
|
+
const encoded = typeof value === "string" ? value : JSON.stringify(value);
|
|
103
|
+
if (encoded.length > SLACK_VIEW_PRIVATE_METADATA_MAX_CHARS) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`Slack view private_metadata exceeds ${SLACK_VIEW_PRIVATE_METADATA_MAX_CHARS} characters.`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
return encoded;
|
|
109
|
+
}
|
|
110
|
+
function decodeSlackViewPrivateMetadata(privateMetadata) {
|
|
111
|
+
const normalized = privateMetadata?.trim();
|
|
112
|
+
if (!normalized) {
|
|
113
|
+
return void 0;
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
return JSON.parse(normalized);
|
|
117
|
+
} catch {
|
|
118
|
+
return normalized;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function readSlackViewPrivateMetadata(view) {
|
|
122
|
+
return decodeSlackViewPrivateMetadata(view.private_metadata);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/views/workflow.ts
|
|
126
|
+
function registerSlackViewWorkflow({
|
|
127
|
+
boltApp,
|
|
128
|
+
callbackId,
|
|
129
|
+
decodePrivateMetadata,
|
|
130
|
+
onSubmission,
|
|
131
|
+
onClose,
|
|
132
|
+
onError
|
|
133
|
+
}) {
|
|
134
|
+
if (onSubmission) {
|
|
135
|
+
boltApp.view(
|
|
136
|
+
{ callback_id: callbackId, type: "view_submission" },
|
|
137
|
+
// Bolt's listener args are structurally stable, but the exact generic
|
|
138
|
+
// narrows differ between view_submission and view_closed.
|
|
139
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
140
|
+
async ({ ack, body, client, view }) => {
|
|
141
|
+
const baseContext = createWorkflowContext({
|
|
142
|
+
body,
|
|
143
|
+
client,
|
|
144
|
+
view
|
|
145
|
+
});
|
|
146
|
+
try {
|
|
147
|
+
const metadata = resolveMetadata(
|
|
148
|
+
decodePrivateMetadata,
|
|
149
|
+
view,
|
|
150
|
+
baseContext
|
|
151
|
+
);
|
|
152
|
+
const response = await onSubmission({
|
|
153
|
+
...baseContext,
|
|
154
|
+
metadata
|
|
155
|
+
});
|
|
156
|
+
await ack(response);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
try {
|
|
159
|
+
await ack();
|
|
160
|
+
} catch {
|
|
161
|
+
}
|
|
162
|
+
await onError?.(error, baseContext);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
if (onClose) {
|
|
168
|
+
boltApp.view(
|
|
169
|
+
{ callback_id: callbackId, type: "view_closed" },
|
|
170
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
171
|
+
async ({ ack, body, client, view }) => {
|
|
172
|
+
const baseContext = createWorkflowContext({
|
|
173
|
+
body,
|
|
174
|
+
client,
|
|
175
|
+
view
|
|
176
|
+
});
|
|
177
|
+
await ack();
|
|
178
|
+
try {
|
|
179
|
+
const metadata = resolveMetadata(
|
|
180
|
+
decodePrivateMetadata,
|
|
181
|
+
view,
|
|
182
|
+
baseContext
|
|
183
|
+
);
|
|
184
|
+
await onClose({
|
|
185
|
+
...baseContext,
|
|
186
|
+
metadata
|
|
187
|
+
});
|
|
188
|
+
} catch (error) {
|
|
189
|
+
await onError?.(error, baseContext);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
return { callbackId };
|
|
195
|
+
}
|
|
196
|
+
function createWorkflowContext({
|
|
197
|
+
body,
|
|
198
|
+
client,
|
|
199
|
+
view
|
|
200
|
+
}) {
|
|
201
|
+
const bodyRecord = isRecord(body) ? body : {};
|
|
202
|
+
const user = isRecord(bodyRecord.user) ? bodyRecord.user : {};
|
|
203
|
+
const team = isRecord(bodyRecord.team) ? bodyRecord.team : {};
|
|
204
|
+
const enterprise = isRecord(bodyRecord.enterprise) ? bodyRecord.enterprise : {};
|
|
205
|
+
return {
|
|
206
|
+
body,
|
|
207
|
+
client,
|
|
208
|
+
view,
|
|
209
|
+
...typeof view.callback_id === "string" ? { callbackId: view.callback_id } : {},
|
|
210
|
+
...typeof bodyRecord.trigger_id === "string" ? { triggerId: bodyRecord.trigger_id } : {},
|
|
211
|
+
actor: {
|
|
212
|
+
...typeof user.id === "string" ? { userId: user.id } : {},
|
|
213
|
+
...typeof team.id === "string" ? { teamId: team.id } : {},
|
|
214
|
+
...typeof enterprise.id === "string" ? { enterpriseId: enterprise.id } : {}
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function resolveMetadata(decodePrivateMetadata, view, context) {
|
|
219
|
+
return decodePrivateMetadata ? decodePrivateMetadata(view.private_metadata, context) : decodeSlackViewPrivateMetadata(view.private_metadata);
|
|
220
|
+
}
|
|
221
|
+
function isRecord(value) {
|
|
222
|
+
return typeof value === "object" && value !== null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export {
|
|
226
|
+
openSlackModal,
|
|
227
|
+
pushSlackModal,
|
|
228
|
+
updateSlackView,
|
|
229
|
+
publishSlackHomeView,
|
|
230
|
+
SLACK_VIEW_PRIVATE_METADATA_MAX_CHARS,
|
|
231
|
+
encodeSlackViewPrivateMetadata,
|
|
232
|
+
decodeSlackViewPrivateMetadata,
|
|
233
|
+
readSlackViewPrivateMetadata,
|
|
234
|
+
registerSlackViewWorkflow
|
|
235
|
+
};
|