feat(ppal-aws): add Lambda handler for Discord Bot
Add Node.js 22 Lambda function for PPAL Discord Bot: - Discord Interactions API with Ed25519 signature verification - Slash commands: /ppal status/help, /miyabi issue/status, /help - DynamoDB integration for user state management - Secrets Manager integration for token retrieval - GitHub API integration for issue creation Files: - package.json: Dependencies (aws-sdk v3, discord-api-types, tweetnacl) - tsconfig.json: TypeScript strict mode, ES2022 target - Dockerfile: Multi-stage Node.js 22 Alpine build - src/index.ts: Lambda handler with signature verification - src/discord/verify.ts: Ed25519 signature validation - src/discord/commands.ts: Command handlers for /ppal, /miyabi, /help - src/services/secrets.ts: AWS Secrets Manager client - src/services/dynamodb.ts: DynamoDB DocumentClient operations Commands: - /ppal status: System status with user info - /ppal help: PPAL commands help - /miyabi issue <title>: Create GitHub issue - /miyabi status: Miyabi agent society status - /help: Full command list Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
abed5cb5cb
commit
ca0e991272
10
workers/ppal-aws/lambda/discord-handler/.dockerignore
Normal file
10
workers/ppal-aws/lambda/discord-handler/.dockerignore
Normal file
@ -0,0 +1,10 @@
|
||||
node_modules
|
||||
dist
|
||||
.git
|
||||
.gitignore
|
||||
*.md
|
||||
.env
|
||||
.DS_Store
|
||||
coverage
|
||||
.vscode
|
||||
.idea
|
||||
27
workers/ppal-aws/lambda/discord-handler/Dockerfile
Normal file
27
workers/ppal-aws/lambda/discord-handler/Dockerfile
Normal file
@ -0,0 +1,27 @@
|
||||
# Build stage
|
||||
FROM node:22-alpine AS builder
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# Production stage
|
||||
FROM node:22-alpine
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm ci --omit=dev && npm cache clean --force
|
||||
|
||||
COPY --from=builder /app/dist ./dist
|
||||
|
||||
# Set non-root user
|
||||
RUN addgroup -g 1001 -S nodejs && \
|
||||
adduser -S nodejs -u 1001
|
||||
USER nodejs
|
||||
|
||||
CMD ["node", "dist/index.js"]
|
||||
1522
workers/ppal-aws/lambda/discord-handler/package-lock.json
generated
Normal file
1522
workers/ppal-aws/lambda/discord-handler/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
workers/ppal-aws/lambda/discord-handler/package.json
Normal file
23
workers/ppal-aws/lambda/discord-handler/package.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "ppal-discord-handler",
|
||||
"version": "1.0.0",
|
||||
"description": "PPAL Discord Bot Lambda Handler",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"watch": "tsc --watch",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-dynamodb": "^3.709.0",
|
||||
"@aws-sdk/client-secrets-manager": "^3.709.0",
|
||||
"@aws-sdk/lib-dynamodb": "^3.709.0",
|
||||
"tweetnacl": "^1.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/aws-lambda": "^8.10.145",
|
||||
"@types/node": "^22.10.5",
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
291
workers/ppal-aws/lambda/discord-handler/src/discord/commands.ts
Normal file
291
workers/ppal-aws/lambda/discord-handler/src/discord/commands.ts
Normal file
@ -0,0 +1,291 @@
|
||||
import {
|
||||
InteractionType,
|
||||
InteractionResponseType,
|
||||
APIApplicationCommandInteraction,
|
||||
APIChatInputApplicationCommandInteraction,
|
||||
} from "discord-api-types/payloads/v10";
|
||||
import { getUser } from "../services/dynamodb.js";
|
||||
import { getGitHubToken } from "../services/secrets.js";
|
||||
|
||||
interface GitHubIssue {
|
||||
title: string;
|
||||
html_url: string;
|
||||
number: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* PING応答
|
||||
*/
|
||||
export function handlePing(): { type: InteractionResponseType.Pong } {
|
||||
return { type: InteractionResponseType.Pong };
|
||||
}
|
||||
|
||||
/**
|
||||
* スラッシュコマンド処理
|
||||
*/
|
||||
export async function handleApplicationCommand(
|
||||
interaction: APIApplicationCommandInteraction
|
||||
): Promise<{
|
||||
type: InteractionResponseType.ChannelMessageWithSource;
|
||||
data: { content: string; flags?: number };
|
||||
}> {
|
||||
const chatInteraction = interaction as APIChatInputApplicationCommandInteraction;
|
||||
const { name } = chatInteraction.data;
|
||||
|
||||
switch (name) {
|
||||
case "ppal":
|
||||
return await handlePpalCommand(chatInteraction);
|
||||
case "miyabi":
|
||||
return await handleMiyabiCommand(chatInteraction);
|
||||
case "help":
|
||||
return handleHelpCommand();
|
||||
default:
|
||||
return {
|
||||
type: InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: "Unknown command",
|
||||
flags: 64, // Ephemeral
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* /ppal コマンド
|
||||
*/
|
||||
async function handlePpalCommand(
|
||||
interaction: APIChatInputApplicationCommandInteraction
|
||||
): Promise<{
|
||||
type: InteractionResponseType.ChannelMessageWithSource;
|
||||
data: { content: string; flags?: number };
|
||||
}> {
|
||||
const options = interaction.data.options || [];
|
||||
const subcommand = options[0]?.name;
|
||||
|
||||
switch (subcommand) {
|
||||
case "status":
|
||||
return handlePpalStatus(interaction);
|
||||
case "help":
|
||||
return handlePpalHelp();
|
||||
default:
|
||||
return {
|
||||
type: InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: "Unknown subcommand. Use `/ppal help` for info.",
|
||||
flags: 64,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* /ppal status - システム状態確認
|
||||
*/
|
||||
async function handlePpalStatus(
|
||||
interaction: APIChatInputApplicationCommandInteraction
|
||||
): Promise<{
|
||||
type: InteractionResponseType.ChannelMessageWithSource;
|
||||
data: { content: string; flags?: number };
|
||||
}> {
|
||||
const user = await getUser(interaction.member?.user?.id || interaction.user?.id || "");
|
||||
|
||||
return {
|
||||
type: InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: `
|
||||
**PPAL System Status**
|
||||
|
||||
🟢 PPAL Discord Bot: Online
|
||||
🟢 DynamoDB: Connected
|
||||
🟢 Lambda: Active
|
||||
|
||||
${user ? `👤 User: ${user.username || interaction.member?.user?.username || "Unknown"}\n📊 State: ${user.state || "None"}` : "👤 User: Not registered"}
|
||||
|
||||
_Environment: ${process.env.ENVIRONMENT || "unknown"}_
|
||||
`.trim(),
|
||||
flags: 64, // Ephemeral
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* /ppal help - ヘルプ表示
|
||||
*/
|
||||
function handlePpalHelp(): {
|
||||
type: InteractionResponseType.ChannelMessageWithSource;
|
||||
data: { content: string; flags?: number };
|
||||
} {
|
||||
return {
|
||||
type: InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: `
|
||||
**PPAL Discord Bot Commands**
|
||||
|
||||
\`\`\`
|
||||
/ppal status - Show system status
|
||||
/ppal help - Show this help message
|
||||
|
||||
/miyabi issue <title> - Create GitHub issue
|
||||
/miyabi status - Check Miyabi system status
|
||||
/help - Show all commands
|
||||
\`\`\`
|
||||
|
||||
🔗 [Documentation](https://docs.clawd.bot)
|
||||
`.trim(),
|
||||
flags: 64,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* /miyabi コマンド
|
||||
*/
|
||||
async function handleMiyabiCommand(
|
||||
interaction: APIChatInputApplicationCommandInteraction
|
||||
): Promise<{
|
||||
type: InteractionResponseType.ChannelMessageWithSource;
|
||||
data: { content: string; flags?: number };
|
||||
}> {
|
||||
const options = interaction.data.options || [];
|
||||
const subcommand = options[0]?.name;
|
||||
|
||||
switch (subcommand) {
|
||||
case "issue":
|
||||
return await handleMiyabiIssue(interaction);
|
||||
case "status":
|
||||
return handleMiyabiStatus();
|
||||
default:
|
||||
return {
|
||||
type: InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: "Unknown subcommand. Use `/miyabi status` for info.",
|
||||
flags: 64,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* /miyabi issue <title> - GitHub Issue作成
|
||||
*/
|
||||
async function handleMiyabiIssue(
|
||||
interaction: APIChatInputApplicationCommandInteraction
|
||||
): Promise<{
|
||||
type: InteractionResponseType.ChannelMessageWithSource;
|
||||
data: { content: string; flags?: number };
|
||||
}> {
|
||||
const options = interaction.data.options || [];
|
||||
const subcommandOptions = (options[0] as { options?: Array<{ value: unknown }> })?.options || [];
|
||||
const titleOption = subcommandOptions[0];
|
||||
const title = titleOption?.value as string;
|
||||
|
||||
if (!title) {
|
||||
return {
|
||||
type: InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: "❌ Please provide a title for the issue.",
|
||||
flags: 64,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const token = await getGitHubToken();
|
||||
const response = await fetch("https://api.github.com/repos/ShunsukeHayashi/clawdbot/issues", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": `Bearer ${token}`,
|
||||
"Accept": "application/vnd.github.v3+json",
|
||||
"Content-Type": "application/json",
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title: `[PPAL Discord] ${title}`,
|
||||
body: `Created from Discord by <@${interaction.member?.user?.id || interaction.user?.id}>\n\n---\n*This issue was automatically created via PPAL Discord Bot*`,
|
||||
labels: ["ppal", "discord-bot"],
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitHub API error: ${response.status}`);
|
||||
}
|
||||
|
||||
const issue = (await response.json()) as GitHubIssue;
|
||||
|
||||
return {
|
||||
type: InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: `✅ Issue created: [${issue.title}](${issue.html_url})\n\n📝 Issue #${issue.number}`,
|
||||
flags: 64,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error creating GitHub issue:", error);
|
||||
return {
|
||||
type: InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: `❌ Failed to create issue: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
flags: 64,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* /miyabi status - Miyabi状態確認
|
||||
*/
|
||||
function handleMiyabiStatus(): {
|
||||
type: InteractionResponseType.ChannelMessageWithSource;
|
||||
data: { content: string; flags?: number };
|
||||
} {
|
||||
return {
|
||||
type: InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: `
|
||||
**Miyabi Agent Society Status**
|
||||
|
||||
🎭 しきるん (Conductor): Online
|
||||
🍁 カエデ (CodeGen): Online
|
||||
🌸 サクラ (Review): Online
|
||||
🌺 ツバキ (PR): Online
|
||||
🌼 ボタン (Deploy): Online
|
||||
🌊 ながれるん (Workflow): Online
|
||||
|
||||
📊 Active Agents: 6/6
|
||||
_Environment: ${process.env.ENVIRONMENT || "unknown"}_
|
||||
`.trim(),
|
||||
flags: 64,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* /help コマンド
|
||||
*/
|
||||
function handleHelpCommand(): {
|
||||
type: InteractionResponseType.ChannelMessageWithSource;
|
||||
data: { content: string; flags?: number };
|
||||
} {
|
||||
return {
|
||||
type: InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: `
|
||||
**PPAL Discord Bot - Command List**
|
||||
|
||||
\`\`\`
|
||||
/ppal status - Show PPAL system status
|
||||
/ppal help - Show PPAL commands
|
||||
|
||||
/miyabi issue <title> - Create GitHub issue
|
||||
/miyabi status - Show Miyabi agent status
|
||||
|
||||
/help - Show this message
|
||||
\`\`\`
|
||||
|
||||
🔗 [Full Documentation](https://docs.clawd.bot)
|
||||
💡 Need help? Join our community!
|
||||
`.trim(),
|
||||
flags: 64,
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
import { getDiscordPublicKey } from "../services/secrets.js";
|
||||
|
||||
/**
|
||||
* Discord Interactions APIの署名を検証
|
||||
*/
|
||||
export async function verifySignature(
|
||||
body: string,
|
||||
signature: string,
|
||||
timestamp: string
|
||||
): Promise<boolean> {
|
||||
const publicKey = await getDiscordPublicKey();
|
||||
const nacl = await import("tweetnacl");
|
||||
|
||||
const message = timestamp + body;
|
||||
const signatureBytes = Buffer.from(signature, "hex");
|
||||
const publicKeyBytes = Buffer.from(publicKey, "hex");
|
||||
|
||||
return nacl.sign.detached.verify(
|
||||
Buffer.from(message),
|
||||
signatureBytes,
|
||||
publicKeyBytes
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* リクエストヘッダーから署名関連の値を抽出
|
||||
*/
|
||||
export function extractSignatureHeaders(headers: {
|
||||
[key: string]: string | undefined;
|
||||
}): { signature: string; timestamp: string } | null {
|
||||
const signature = headers["x-signature-ed25519"];
|
||||
const timestamp = headers["x-signature-timestamp"];
|
||||
|
||||
if (!signature || !timestamp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { signature, timestamp };
|
||||
}
|
||||
116
workers/ppal-aws/lambda/discord-handler/src/index.ts
Normal file
116
workers/ppal-aws/lambda/discord-handler/src/index.ts
Normal file
@ -0,0 +1,116 @@
|
||||
import {
|
||||
APIInteraction,
|
||||
InteractionType,
|
||||
InteractionResponseType,
|
||||
APIApplicationCommandInteraction,
|
||||
} from "discord-api-types/payloads/v10";
|
||||
import { verifySignature, extractSignatureHeaders } from "./discord/verify.js";
|
||||
import {
|
||||
handlePing,
|
||||
handleApplicationCommand,
|
||||
} from "./discord/commands.js";
|
||||
|
||||
/**
|
||||
* Lambda ハンドラー
|
||||
*/
|
||||
export const handler = async (event: {
|
||||
headers: { [key: string]: string | undefined };
|
||||
body: string | null;
|
||||
}): Promise<{ statusCode: number; body: string; headers: Record<string, string> }> => {
|
||||
// Headers are lowercase in API Gateway
|
||||
const headers = Object.fromEntries(
|
||||
Object.entries(event.headers || {}).map(([k, v]) => [k.toLowerCase(), v])
|
||||
);
|
||||
|
||||
const signatureData = extractSignatureHeaders(headers);
|
||||
|
||||
if (!signatureData) {
|
||||
console.error("Missing signature headers");
|
||||
return errorResponse(401, "Missing signature headers");
|
||||
}
|
||||
|
||||
const { signature, timestamp } = signatureData;
|
||||
const body = event.body || "";
|
||||
|
||||
// Signature verification
|
||||
const isValid = await verifySignature(body, signature, timestamp);
|
||||
|
||||
if (!isValid) {
|
||||
console.error("Invalid signature");
|
||||
return errorResponse(401, "Invalid signature");
|
||||
}
|
||||
|
||||
// Parse interaction
|
||||
let interaction: APIInteraction;
|
||||
try {
|
||||
interaction = JSON.parse(body);
|
||||
} catch (error) {
|
||||
console.error("Failed to parse interaction body:", error);
|
||||
return errorResponse(400, "Invalid JSON");
|
||||
}
|
||||
|
||||
// Handle interaction
|
||||
try {
|
||||
let response:
|
||||
| { type: InteractionResponseType.Pong }
|
||||
| { type: InteractionResponseType.ChannelMessageWithSource; data: { content: string; flags?: number } };
|
||||
|
||||
switch (interaction.type) {
|
||||
case InteractionType.Ping:
|
||||
response = handlePing();
|
||||
break;
|
||||
|
||||
case InteractionType.ApplicationCommand:
|
||||
response = await handleApplicationCommand(
|
||||
interaction as APIApplicationCommandInteraction
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(`Unhandled interaction type: ${interaction.type}`);
|
||||
return errorResponse(400, "Unhandled interaction type");
|
||||
}
|
||||
|
||||
return jsonResponse(JSON.stringify(response));
|
||||
} catch (error) {
|
||||
console.error("Error handling interaction:", error);
|
||||
return errorResponse(500, "Internal server error");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* JSONレスポンス
|
||||
*/
|
||||
function jsonResponse(body: string): {
|
||||
statusCode: number;
|
||||
body: string;
|
||||
headers: Record<string, string>;
|
||||
} {
|
||||
return {
|
||||
statusCode: 200,
|
||||
body,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* エラーレスポンス
|
||||
*/
|
||||
function errorResponse(
|
||||
statusCode: number,
|
||||
message: string
|
||||
): {
|
||||
statusCode: number;
|
||||
body: string;
|
||||
headers: Record<string, string>;
|
||||
} {
|
||||
return {
|
||||
statusCode,
|
||||
body: JSON.stringify({ error: message }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
};
|
||||
}
|
||||
134
workers/ppal-aws/lambda/discord-handler/src/services/dynamodb.ts
Normal file
134
workers/ppal-aws/lambda/discord-handler/src/services/dynamodb.ts
Normal file
@ -0,0 +1,134 @@
|
||||
import {
|
||||
DynamoDBDocumentClient,
|
||||
GetCommand,
|
||||
PutCommand,
|
||||
UpdateCommand,
|
||||
QueryCommand,
|
||||
} from "@aws-sdk/lib-dynamodb";
|
||||
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
||||
|
||||
const client = new DynamoDBClient({});
|
||||
const docClient = DynamoDBDocumentClient.from(client);
|
||||
|
||||
const USERS_TABLE = process.env.USERS_TABLE_NAME || "";
|
||||
const CONVERSATIONS_TABLE = process.env.CONVERSATIONS_TABLE_NAME || "";
|
||||
|
||||
export interface User {
|
||||
user_id: string;
|
||||
guild_id?: string;
|
||||
username?: string;
|
||||
state?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface Conversation {
|
||||
conversation_id: string;
|
||||
user_id: string;
|
||||
guild_id?: string;
|
||||
messages: number;
|
||||
last_activity: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* ユーザーを取得
|
||||
*/
|
||||
export async function getUser(userId: string): Promise<User | null> {
|
||||
const response = await docClient.send(
|
||||
new GetCommand({
|
||||
TableName: USERS_TABLE,
|
||||
Key: { user_id: userId },
|
||||
})
|
||||
);
|
||||
|
||||
return (response.Item as User) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* ユーザーを作成または更新
|
||||
*/
|
||||
export async function putUser(user: User): Promise<void> {
|
||||
await docClient.send(
|
||||
new PutCommand({
|
||||
TableName: USERS_TABLE,
|
||||
Item: user,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* ユーザー状態を更新
|
||||
*/
|
||||
export async function updateUserState(
|
||||
userId: string,
|
||||
state: string
|
||||
): Promise<void> {
|
||||
await docClient.send(
|
||||
new UpdateCommand({
|
||||
TableName: USERS_TABLE,
|
||||
Key: { user_id: userId },
|
||||
UpdateExpression: "SET #state = :state, #updated = :updated",
|
||||
ExpressionAttributeNames: {
|
||||
"#state": "state",
|
||||
"#updated": "updated_at",
|
||||
},
|
||||
ExpressionAttributeValues: {
|
||||
":state": state,
|
||||
":updated": new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* ギルド内のユーザーを一覧取得
|
||||
*/
|
||||
export async function listUsersByGuild(guildId: string): Promise<User[]> {
|
||||
const response = await docClient.send(
|
||||
new QueryCommand({
|
||||
TableName: USERS_TABLE,
|
||||
IndexName: "GuildIndex",
|
||||
KeyConditionExpression: "guild_id = :guild_id",
|
||||
ExpressionAttributeValues: {
|
||||
":guild_id": guildId,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return (response.Items as User[]) || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 会話を作成または更新
|
||||
*/
|
||||
export async function putConversation(
|
||||
conversation: Conversation
|
||||
): Promise<void> {
|
||||
await docClient.send(
|
||||
new PutCommand({
|
||||
TableName: CONVERSATIONS_TABLE,
|
||||
Item: conversation,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* ユーザーの会話を一覧取得
|
||||
*/
|
||||
export async function listConversationsByUser(
|
||||
userId: string
|
||||
): Promise<Conversation[]> {
|
||||
const response = await docClient.send(
|
||||
new QueryCommand({
|
||||
TableName: CONVERSATIONS_TABLE,
|
||||
IndexName: "UserIndex",
|
||||
KeyConditionExpression: "user_id = :user_id",
|
||||
ExpressionAttributeValues: {
|
||||
":user_id": userId,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return (response.Items as Conversation[]) || [];
|
||||
}
|
||||
@ -0,0 +1,90 @@
|
||||
import {
|
||||
SecretsManagerClient,
|
||||
GetSecretValueCommand,
|
||||
} from "@aws-sdk/client-secrets-manager";
|
||||
|
||||
const client = new SecretsManagerClient({});
|
||||
|
||||
export interface DiscordSecrets {
|
||||
token: string;
|
||||
public_key: string;
|
||||
}
|
||||
|
||||
export interface GitHubSecret {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface OpenAISecret {
|
||||
api_key: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discord bot tokenを取得
|
||||
*/
|
||||
export async function getDiscordToken(): Promise<string> {
|
||||
const secretArn = process.env.DISCORD_BOT_TOKEN_SECRET_ARN;
|
||||
if (!secretArn) throw new Error("DISCORD_BOT_TOKEN_SECRET_ARN not set");
|
||||
|
||||
const response = await client.send(
|
||||
new GetSecretValueCommand({ SecretId: secretArn })
|
||||
);
|
||||
|
||||
const secret = JSON.parse(response.SecretString || "{}");
|
||||
return secret.token as string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discord public keyを取得
|
||||
*/
|
||||
export async function getDiscordPublicKey(): Promise<string> {
|
||||
const secretArn = process.env.DISCORD_PUBLIC_KEY_SECRET_ARN;
|
||||
if (!secretArn) throw new Error("DISCORD_PUBLIC_KEY_SECRET_ARN not set");
|
||||
|
||||
const response = await client.send(
|
||||
new GetSecretValueCommand({ SecretId: secretArn })
|
||||
);
|
||||
|
||||
const secret = JSON.parse(response.SecretString || "{}");
|
||||
return secret.public_key as string;
|
||||
}
|
||||
|
||||
/**
|
||||
* GitHub tokenを取得
|
||||
*/
|
||||
export async function getGitHubToken(): Promise<string> {
|
||||
const secretArn = process.env.GITHUB_TOKEN_SECRET_ARN;
|
||||
if (!secretArn) throw new Error("GITHUB_TOKEN_SECRET_ARN not set");
|
||||
|
||||
const response = await client.send(
|
||||
new GetSecretValueCommand({ SecretId: secretArn })
|
||||
);
|
||||
|
||||
const secret = JSON.parse(response.SecretString || "{}");
|
||||
return secret.token as string;
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenAI API keyを取得
|
||||
*/
|
||||
export async function getOpenAIApiKey(): Promise<string> {
|
||||
const secretArn = process.env.OPENAI_API_KEY_SECRET_ARN;
|
||||
if (!secretArn) throw new Error("OPENAI_API_KEY_SECRET_ARN not set");
|
||||
|
||||
const response = await client.send(
|
||||
new GetSecretValueCommand({ SecretId: secretArn })
|
||||
);
|
||||
|
||||
const secret = JSON.parse(response.SecretString || "{}");
|
||||
return secret.api_key as string;
|
||||
}
|
||||
|
||||
/**
|
||||
* すべてのDiscordシークレットを取得
|
||||
*/
|
||||
export async function getDiscordSecrets(): Promise<DiscordSecrets> {
|
||||
const [token, public_key] = await Promise.all([
|
||||
getDiscordToken(),
|
||||
getDiscordPublicKey(),
|
||||
]);
|
||||
return { token, public_key };
|
||||
}
|
||||
19
workers/ppal-aws/lambda/discord-handler/tsconfig.json
Normal file
19
workers/ppal-aws/lambda/discord-handler/tsconfig.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "node",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": false,
|
||||
"sourceMap": false
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user