Skip to content

Commit

Permalink
feat: Simplifying the search feature (#58)
Browse files Browse the repository at this point in the history
* feat: auto delete command & reply the tweet url

* docs: add comment about reply

* feat: update to use gpt-4o for allow user (#57)

* docs: TODO

* help

* refactor: stash

* typo: twitter -> xlog

* update comment

* go to sleep

* feat: handle sender_chat

* do anything

* refactor: remove inline bot

* feat: update to use gpt-4o for allow user

* refactor: use grammy instead myself type

* fix: LLM permission

* feat: add comment to github issue
  • Loading branch information
niracler authored Aug 2, 2024
1 parent 9a7b7c2 commit 81e964c
Show file tree
Hide file tree
Showing 18 changed files with 516 additions and 251 deletions.
1 change: 1 addition & 0 deletions README.cn.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ Nyaruko 需要以下环境变量的支持来发挥其作用:
|`TWITTER_API_SECRET`||`secret`|您的 Twitter API 密钥密文|
|`TWITTER_ACCESS_TOKEN`||`secret`|Twitter 的访问令牌|
|`TWITTER_ACCESS_TOKEN_SECRET`||`secret`|Twitter 的访问令牌密文|
|`TWITTER_USER_ID`||`wrangler.yml`|您的 Twitter 用户 ID|
|`OPENAI_API_KEY`||`secret`|OpenAI 的 API 密钥。用于开启 ai 聊天功能|

### 关于设置环境变量
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ Nyaruko requires the following environment variables to function:
|`TWITTER_API_SECRET`|No|`secret`|Your Twitter API secret key|
|`TWITTER_ACCESS_TOKEN`|No|`secret`|Twitter access token|
|`TWITTER_ACCESS_TOKEN_SECRET`|No|`secret`|Twitter access token secret|
|`TWITTER_USER_ID`|No|`wrangler.yml`|Twitter user id|
|`OPENAI_API_KEY`|No|`secret`|OpenAI API key. Used to enable AI chat|

### About Setting Environment Variables
Expand Down Expand Up @@ -142,8 +143,9 @@ Now, Nyaruko is ready to use! 🎉
Find botfather, then enter `/setcommands`, then select your bot, and then enter the following content:
```bash
search - Search for messages in the chat.
sync_twitter - Sync msg to Twitter.
sync_xlog - Sync msg to Twitter.
sync_xlog - Sync msg to XLog.
ping - Test if the bot is online.
getchatid - Get the ID of the current chat.
getuserid - Get the ID of the current user.
Expand Down
22 changes: 21 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"dependencies": {
"crossbell": "^1.12.0",
"crypto-js": "^4.2.0",
"grammy": "^1.23.0",
"oauth-1.0a": "^2.2.6",
"openai": "^4.33.0",
"telegramify-markdown": "git+ssh://[email protected]/niracler/telegramify-markdown.git#master"
Expand Down
27 changes: 27 additions & 0 deletions src/channel/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Env as CoreEnv } from "@/core/type"
import { Update } from "grammy/types"

export type Env = {} & CoreEnv

/**
* Processes the syncXLog command by syncing a message with XLog.
*
* @param update - The Telegram update object.
* @param env - The environment object.
* @returns A promise that resolves to a string indicating the result of the sync operation.
*/
export async function processChannel(update: Update, env: Env): Promise<string> {

const message = update.message?.text || update.message?.caption || ''
const command = message.split(' ')[0]
const content = message.slice(command.length + 1)

const res = await env.DB.prepare('SELECT * FROM telegram_messages WHERE text LIKE ?').bind(`%${content}%`).all()
// console.log('channel', command, content)

// const aa = "niracler_channel"

// const res = await env.DB.prepare('SELECT * FROM telegram_messages WHERE sender_chat_username LIKE ?').bind(`%${aa}%`).all()

return `channel ${command} ${content} ${JSON.stringify(res)}`
}
12 changes: 6 additions & 6 deletions src/core/db.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Env, TelegramUpdate } from "./type"
import { Env } from "./type"
import { Update } from 'grammy/types'

/**
* Inserts the Telegram update data into the database.
Expand All @@ -7,7 +8,9 @@ import { Env, TelegramUpdate } from "./type"
* @param env - The environment object.
* @returns A promise that resolves when the data is successfully inserted into the database.
*/
export async function syncToDatabase(update: TelegramUpdate, env: Env) {
export async function syncToDatabase(update: Update, env: Env) {
console.log('syncToDatabase', JSON.stringify(update, null, 2))
if (!update.message) return

await env.DB.prepare(`
INSERT INTO telegram_messages (
Expand Down Expand Up @@ -67,10 +70,7 @@ export async function syncToDatabase(update: TelegramUpdate, env: Env) {
update.message.reply_to_message?.sender_chat?.title || null,
update.message.reply_to_message?.sender_chat?.type || null,
update.message.reply_to_message?.date || null,
update.message.forward_from_chat?.id || null,
update.message.forward_from_chat?.title || null,
update.message.forward_from_chat?.type || null,
update.message.forward_from_message_id || null,
null, null, null, null,
update.message.media_group_id || null,
update.message.photo?.length ? update.message.photo[update.message.photo.length - 1].file_id : null,
update.message.caption || null,
Expand Down
74 changes: 64 additions & 10 deletions src/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,101 @@

import telegramifyMarkdown from "telegramify-markdown"
import { Env, TelegramUpdate } from "./type"
import { Env } from "./type"
import { Update } from 'grammy/types'
import { syncToDatabase } from "./db"

// Process the Telegram update received
export async function handleTelegramUpdate(update: TelegramUpdate, env: Env, handler: () => Promise<string | undefined>) {
export async function handleTelegramUpdate(update: Update, env: Env, handler: () => Promise<string | undefined>) {
if (!update.message) return
let replyText: string | undefined

try {
await syncToDatabase(update, env)

if (!update.inline_query) {
await syncToDatabase(update, env)
}
replyText = await handler()
if (!replyText) return // No reply text, do nothing
} catch (error) {
replyText = `Failed to insert message to database: ${error}`
}

await sendReplyToTelegram(update.message.chat.id, replyText, update.message.message_id, env)

try {
const content = update.message?.text || ''
if (content.startsWith('/')) {
await deleteCommand(update.message.chat.id, update.message.message_id, env)
}
} catch (error) {
await sendReplyToTelegram(update.message.chat.id, `Failed to delete command: ${error}`, update.message.message_id, env)
}
}

// Process the '/getgroupid' command
export async function processGetGroupIdCommand(update: TelegramUpdate, env: Env): Promise<string> {
return `Your chat ID is ${update.message?.chat.id}`
export async function processGetGroupIdCommand(update: Update, env: Env): Promise<string> {
const fromUsername = update.message?.from?.username || ''
const formFirstName = update.message?.from?.first_name || ''
let replyName = fromUsername ? `@${fromUsername}` : formFirstName

if (replyName === "@GroupAnonymousBot") {
const username = update.message?.sender_chat?.username || ''
const title = update.message?.sender_chat?.title || ''
replyName = username ? `@${username}` : title
}

return `哟呼~记下来啦!${replyName} 的聊天 ID 是 \`${update.message?.chat.id}\` 呢~ (。•̀ᴗ-)✧ `
}

// Process the '/getuserid' command
export async function processGetUserIdCommand(update: TelegramUpdate, env: Env): Promise<string> {
return `Your user ID is ${update.message?.from?.id}`
export async function processGetUserIdCommand(update: Update, env: Env): Promise<string> {
const fromUsername = update.message?.from?.username || ''
const formFirstName = update.message?.from?.first_name || ''
let replyName = fromUsername ? `@${fromUsername}` : formFirstName
let id = update.message?.from?.id

console.log(`hi ${replyName}`)

if (replyName === "@GroupAnonymousBot") {
const username = update.message?.sender_chat?.username || ''
const title = update.message?.sender_chat?.title || ''
replyName = username ? `@${username}` : title
id = update.message?.sender_chat?.id || 0
}

return `呀~ ${replyName} ,您的 ID 是 \`${id}\` 哦!ヽ(^Д^)ノ`
}

// Process the '/ping' command
export async function processPingCommand(update: TelegramUpdate, env: Env): Promise<string> {
export async function processPingCommand(update: Update, env: Env): Promise<string> {
// Show more information about this chat
return JSON.stringify(update.message?.chat, null, 2)
}

// Process the '/debug' command
async function processDebugCommand(update: TelegramUpdate, env: Env): Promise<string> {
async function processDebugCommand(update: Update, env: Env): Promise<string> {
// Show more information about this update
return JSON.stringify(update, null, 2)
}

// Delete the message that triggered the command
async function deleteCommand(chatId: number, messageId: number, env: Env): Promise<void> {
const response = await fetch(`https://api.telegram.org/bot${env.TELEGRAM_BOT_SECRET}/deleteMessage`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chat_id: chatId,
message_id: messageId,
}),
})

if (!response.ok) {
throw new Error(`Telegram API deleteMessage responded with status ${response.status} and body ${await response.text()}`)
}
}

// Send a message back to Telegram chat
async function sendReplyToTelegram(chatId: number, text: string, messageId: number, env: Env) {
if (!text) return
const response = await fetch(`https://api.telegram.org/bot${env.TELEGRAM_BOT_SECRET}/sendMessage`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
Expand All @@ -58,3 +111,4 @@ async function sendReplyToTelegram(chatId: number, text: string, messageId: numb
throw new Error(`Telegram API sendMessage responded with status ${response.status}`)
}
}

52 changes: 4 additions & 48 deletions src/core/type.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,6 @@
export type Env = {
TELEGRAM_BOT_SECRET: string
DB: D1Database
}

export interface TelegramUpdate {
update_id: number // Update ID from Telegram
message: TelegramMessage // Message object, optional
// 添加更多的更新相关字段| Add more update related fields
}

interface TelegramChat {
id: number // Chat ID
title?: string // Chat title, optional
username?: string // Username, optional
type: string // Type of chat, one of "private", "group", "supergroup" or "channel"
// 添加更多的聊天相关字段 | Add more chat related fields
}

interface TelegramPhoto {
file_id: string // 可用于获取文件内容 | Can be used to get file content
file_unique_id: string // 文件的唯一标识符 | Unique identifier for this file
width: number // 图片宽度 | Image width
height: number // 图片高度 | Image height
file_size?: number // 文件大小(可选) | File size (optional)
}

export interface TelegramMessage {
message_id: number // Message ID
chat: TelegramChat // Chat object
text?: string // Received message text, optional
reply_to_message?: TelegramMessage // 添加这个字段来获取回复的消息 | Add this field to get the replied message
from: {
username: string // 发送者的用户名 | Sender's username
id: string // 发送者的ID | Sender's ID
first_name: string // 发送者的名字 | Sender's first name
},
sender_chat?: TelegramChat // Sender's chat object, optional
caption?: string
photo?: TelegramPhoto[] // TelegramPhoto需要根据API定义 | TelegramPhoto needs to be defined according to the API
forward_from_chat?: TelegramChat // Forwarded from chat object, optional
forward_from_message_id?: number // Forwarded from message ID, optional
media_group_id?: string // Media group ID
quote?: {
text: string
is_manual?: boolean
}
date: number // Unix timestamp
// 添加更多的消息相关字段 | Add more message related fields
TELEGRAM_BOT_USERNAME: string
TELEGRAM_BOT_SECRET: string
ALLOW_USER_IDS: string[]
DB: D1Database
}
6 changes: 4 additions & 2 deletions src/core/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Env, TelegramMessage } from "./type"
import { Env } from "./type"
import { Message } from "grammy/types"


/**
* Retrieves the URL list of Telegram photos from a given message.
Expand All @@ -8,7 +10,7 @@ import { Env, TelegramMessage } from "./type"
* @param env - The environment object containing necessary configurations.
* @returns A promise that resolves to an array of photo URLs.
*/
export async function getTelegramPhotoUrlList(message: TelegramMessage, env: Env): Promise<string[]> {
export async function getTelegramPhotoUrlList(message: Message, env: Env): Promise<string[]> {
let photoIdList = []
if (!message.media_group_id) {
if (message.photo?.length) {
Expand Down
28 changes: 16 additions & 12 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { handleTelegramUpdate, processGetGroupIdCommand, processGetUserIdCommand, processPingCommand } from './core'
import { processLLM } from './llm'
import { processSyncXLogCommand } from './xlog'
import { processSyncTwitterCommand } from './twitter'
import twitter from './twitter'
import { processChannel } from './channel'

import { TelegramUpdate } from './core/type'
import { Env as LLMEnv } from './llm'
import { Env as XLogEnv } from './xlog'
import { Env as TwitterEnv } from './twitter'
import { Env as RandomEnv } from './random'
import { Update } from 'grammy/types'
import { processRandom } from './random'

export type Env = {
TELEGRAM_BOT_USERNAME: string
} & LLMEnv & XLogEnv & TwitterEnv
export type Env = LLMEnv & XLogEnv & TwitterEnv & RandomEnv

async function handler(update: TelegramUpdate, env: Env): Promise<string | undefined> {
async function handler(update: Update, env: Env): Promise<string | undefined> {
const content = update.message?.text || update.message?.caption || ''

if (content.startsWith('/getchatid')) {
Expand All @@ -24,25 +25,28 @@ async function handler(update: TelegramUpdate, env: Env): Promise<string | undef
} else if (content.startsWith('/ping')) {
return await processPingCommand(update, env)

} else if (env.TELEGRAM_BOT_USERNAME && content.includes(`@${env.TELEGRAM_BOT_USERNAME}`) && !content.startsWith('/')) {
return await processLLM(update, env)

} else if (content.startsWith('/sync_twitter')) {
return await processSyncTwitterCommand(update, env)
return await twitter.processSyncTwitterCommand(update, env)

} else if (content.startsWith('/search')) {
return await processChannel(update, env)

} else if (content.startsWith('/sync_xlog')) {
return await processSyncXLogCommand(update, env)

} else if (update.message?.reply_to_message?.text?.includes('#random_todolist')) {
return await processRandom(update, env)

} else {
return undefined
return await processLLM(update, env)
}
}

export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
if (request.method === 'POST') {
try {
const update = await request.json() as TelegramUpdate
const update = await request.json() as Update
await handleTelegramUpdate(update, env, async () => {
return await handler(update, env)
})
Expand Down
Loading

0 comments on commit 81e964c

Please sign in to comment.