Skip to content

Commit 16c3687

Browse files
committed
many stuff i guess
1 parent 6949558 commit 16c3687

File tree

11 files changed

+560
-17
lines changed

11 files changed

+560
-17
lines changed

service/CLAUDE.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Build and Development Commands
6+
7+
- `pnpm dev` - Start service in development mode with auto-reload
8+
- `pnpm start` - Start service in production mode
9+
- `npx tsc` - Type check TypeScript files
10+
- `node --experimental-transform-types --env-file=.env src/scripts/updateConfig.ts` - Update service configuration
11+
12+
## Code Style Guidelines
13+
14+
- **TypeScript**: Use strict typing
15+
- **Imports**: Use explicit `.ts` extensions in import paths
16+
- **Formatting**: 2-space indentation, semicolons at end of statements
17+
- **Exports**: Export types and interfaces at module level
18+
- **Error Handling**: Use try/catch with consola.error for logging
19+
- **Plugin System**: Add new features by creating plugins in `/src/plugins` directory
20+
- **Platform Adapters**: Use `/src/adapters` for platform-specific code
21+
- **Media Handling**: Upload files to Azure Blob Storage via the blob service
22+
- **Modules**: Keep related functionality in dedicated modules (plugins, adapters, etc.)
23+
- **Type Definitions**: Define shared types in appropriate interface files

service/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"dependencies": {
88
"@azure/data-tables": "^13.3.0",
99
"@azure/identity": "^4.8.0",
10+
"@azure/storage-blob": "^12.17.0",
1011
"@elysiajs/node": "^1.2.6",
1112
"@line/bot-sdk": "^9.2.2",
1213
"age-encryption": "^0.2.1",

service/pnpm-lock.yaml

Lines changed: 51 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

service/src/adapters/line.ts

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { messagingApi } from '@line/bot-sdk'
2+
import consola from 'consola'
3+
import { blobService, getMimeTypeFromExtension } from '../blob.ts'
4+
import type { GenericMessage, MessageResponse } from '../brain.ts'
5+
6+
// Convert LINE messages to generic format
7+
export async function handleLINEMessage(
8+
event: any,
9+
lineConfig: { channelAccessToken: string }
10+
): Promise<GenericMessage> {
11+
if (event.message.type === 'text') {
12+
return {
13+
type: 'text',
14+
userId: event.source.userId,
15+
text: event.message.text
16+
}
17+
} else if (['image', 'audio', 'video', 'file'].includes(event.message.type)) {
18+
try {
19+
// Create blob client for LINE content
20+
const lineBlobClient = new messagingApi.MessagingApiBlobClient(lineConfig)
21+
22+
// Get content from LINE
23+
const content = await lineBlobClient.getMessageContent(event.message.id)
24+
25+
// Determine content type - LINE might not provide content type headers properly
26+
// for media messages, but we could try to infer from the file extension or message type
27+
const contentType = determineContentType(event.message)
28+
29+
// Create a unique blob name
30+
const blobName = `${event.source.userId}/${Date.now()}-${event.message.id}.${getFileExtension(event.message)}`
31+
32+
// Upload to Azure Blob Storage using our service
33+
const { blobKey, contentLength } = await blobService.uploadStream(
34+
'ephemeral',
35+
blobName,
36+
content,
37+
contentType
38+
)
39+
40+
// After successful upload, return the message with the blob key
41+
return {
42+
type: event.message.type as 'audio' | 'video' | 'file',
43+
userId: event.source.userId,
44+
blobKey,
45+
contentType,
46+
filename: event.message.fileName || `${event.message.id}.${getFileExtension(event.message)}`,
47+
size: contentLength
48+
}
49+
} catch (error) {
50+
consola.error('Error uploading media to blob storage:', error)
51+
throw new Error(`Failed to upload media: ${(error as Error).message}`)
52+
}
53+
} else {
54+
// For unsupported message types, return a text message with error
55+
throw new Error(`Unsupported message type: ${event.message.type}`)
56+
}
57+
}
58+
59+
// Helper functions for file handling
60+
function determineContentType(message: any): string {
61+
// Fallback to standard MIME types based on message type
62+
switch (message.type) {
63+
case 'image':
64+
return 'image/jpeg'
65+
case 'audio':
66+
return 'audio/mpeg'
67+
case 'video':
68+
return 'video/mp4'
69+
case 'file':
70+
// Try to determine from file extension
71+
const extension = getFileExtension(message).toLowerCase()
72+
return getMimeTypeFromExtension(extension) || 'application/octet-stream'
73+
default:
74+
return 'application/octet-stream'
75+
}
76+
}
77+
78+
function getFileExtension(message: any): string {
79+
if (message.fileName) {
80+
const parts = message.fileName.split('.')
81+
if (parts.length > 1) {
82+
return parts[parts.length - 1]
83+
}
84+
}
85+
86+
// Default extensions based on type
87+
switch (message.type) {
88+
case 'image': return 'jpg'
89+
case 'audio': return 'mp3'
90+
case 'video': return 'mp4'
91+
case 'file': return 'bin'
92+
default: return 'bin'
93+
}
94+
}
95+
96+
// Removed: getMimeTypeFromExtension is now imported from blob.ts
97+
98+
// Send response back to LINE
99+
export async function sendLINEResponse(
100+
response: MessageResponse,
101+
replyToken: string,
102+
lineConfig: { channelAccessToken: string }
103+
): Promise<void> {
104+
const client = new messagingApi.MessagingApiClient(lineConfig)
105+
if (response.type === 'text' && response.content) {
106+
await client.replyMessage({
107+
replyToken,
108+
messages: [
109+
{
110+
type: 'text',
111+
text: response.content,
112+
},
113+
],
114+
})
115+
} else if (response.type === 'media' && response.mediaUrl) {
116+
// Handle media responses if needed
117+
await client.replyMessage({
118+
replyToken,
119+
messages: [
120+
{
121+
type: 'text',
122+
text: `Media URL: ${response.mediaUrl}`,
123+
},
124+
],
125+
})
126+
} else if (response.type === 'none') {
127+
// No response needed
128+
await client.replyMessage({
129+
replyToken,
130+
messages: [
131+
{
132+
type: 'text',
133+
text: 'Message received, but no response is needed.',
134+
},
135+
],
136+
})
137+
}
138+
}

service/src/blob.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { BlobServiceClient, BlockBlobClient } from '@azure/storage-blob'
2+
import { Readable } from 'stream'
3+
import { azureStorageConnectionString } from './storage.ts'
4+
5+
/**
6+
* Azure Blob Storage service wrapper
7+
*/
8+
export class BlobService {
9+
private blobServiceClientPromise: Promise<BlobServiceClient>
10+
11+
constructor() {
12+
this.blobServiceClientPromise = this.createBlobServiceClient()
13+
}
14+
15+
private async createBlobServiceClient(): Promise<BlobServiceClient> {
16+
return BlobServiceClient.fromConnectionString(azureStorageConnectionString)
17+
}
18+
19+
/**
20+
* Uploads a stream to the specified container and creates a blob with the given name
21+
*/
22+
async uploadStream(
23+
containerName: string,
24+
blobName: string,
25+
content: Readable,
26+
contentType: string
27+
): Promise<{
28+
blobKey: string
29+
contentLength: number
30+
}> {
31+
const blobServiceClient = await this.blobServiceClientPromise
32+
const containerClient = blobServiceClient.getContainerClient(containerName)
33+
34+
// Ensure container exists
35+
await containerClient.createIfNotExists()
36+
37+
const blockBlobClient = containerClient.getBlockBlobClient(blobName)
38+
39+
// Upload using stream
40+
const uploadOptions = {
41+
blobHTTPHeaders: {
42+
blobContentType: contentType,
43+
},
44+
}
45+
46+
await blockBlobClient.uploadStream(
47+
content,
48+
undefined, // default buffer size
49+
undefined, // default max concurrency
50+
uploadOptions
51+
)
52+
53+
// Get properties for content length
54+
const properties = await blockBlobClient.getProperties()
55+
56+
return {
57+
blobKey: `${containerName}/${blobName}`,
58+
contentLength: properties.contentLength || 0,
59+
}
60+
}
61+
62+
/**
63+
* Gets a BlockBlobClient for the specified container and blob name
64+
*/
65+
async getBlobClient(
66+
containerName: string,
67+
blobName: string
68+
): Promise<BlockBlobClient> {
69+
const blobServiceClient = await this.blobServiceClientPromise
70+
const containerClient = blobServiceClient.getContainerClient(containerName)
71+
return containerClient.getBlockBlobClient(blobName)
72+
}
73+
}
74+
75+
// Export a singleton instance
76+
export const blobService = new BlobService()
77+
78+
/**
79+
* Utility functions for MIME type handling
80+
*/
81+
export function getMimeTypeFromExtension(extension: string): string | null {
82+
const mimeTypes: Record<string, string> = {
83+
jpg: 'image/jpeg',
84+
jpeg: 'image/jpeg',
85+
png: 'image/png',
86+
gif: 'image/gif',
87+
pdf: 'application/pdf',
88+
doc: 'application/msword',
89+
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
90+
xls: 'application/vnd.ms-excel',
91+
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
92+
ppt: 'application/vnd.ms-powerpoint',
93+
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
94+
mp3: 'audio/mpeg',
95+
mp4: 'video/mp4',
96+
wav: 'audio/wav',
97+
txt: 'text/plain',
98+
zip: 'application/zip',
99+
json: 'application/json',
100+
xml: 'application/xml',
101+
html: 'text/html',
102+
css: 'text/css',
103+
js: 'application/javascript',
104+
}
105+
106+
return mimeTypes[extension] || null
107+
}

0 commit comments

Comments
 (0)