Skip to content

Commit

Permalink
feat: sync message from telegram to twitter
Browse files Browse the repository at this point in the history
  • Loading branch information
niracler committed Nov 26, 2023
1 parent 1892a1f commit 131bcd1
Show file tree
Hide file tree
Showing 9 changed files with 209 additions and 170 deletions.
1 change: 1 addition & 0 deletions 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 @@ -10,6 +10,7 @@
"devDependencies": {
"@cloudflare/workers-types": "^4.20230419.0",
"@types/crypto-js": "^4.2.1",
"@types/node": "^20.10.0",
"typescript": "^5.0.4",
"wrangler": "^3.0.0"
},
Expand Down
36 changes: 0 additions & 36 deletions src/demo.ts

This file was deleted.

133 changes: 2 additions & 131 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,137 +8,8 @@
* Learn more at https://developers.cloudflare.com/workers/
*/

import OAuth from 'oauth-1.0a'
import { HmacSHA1, enc } from 'crypto-js'

export interface Env {
TELEGRAM_BOT_SECRET: string
TWITTER_BEARER_TOKEN: string
TWITTER_API_KEY: string
TWITTER_API_SECRET: string
TWITTER_ACCESS_TOKEN: string
TWITTER_ACCESS_TOKEN_SECRET: string

// Example binding to KV. Learn more at https://developers.cloudflare.com/workers/runtime-apis/kv/
// MY_KV_NAMESPACE: KVNamespace
//
// Example binding to Durable Object. Learn more at https://developers.cloudflare.com/workers/runtime-apis/durable-objects/
// MY_DURABLE_OBJECT: DurableObjectNamespace
//
// Example binding to R2. Learn more at https://developers.cloudflare.com/workers/runtime-apis/r2/
// MY_BUCKET: R2Bucket
//
// Example binding to a Service. Learn more at https://developers.cloudflare.com/workers/runtime-apis/service-bindings/
// MY_SERVICE: Fetcher
//
// Example binding to a Queue. Learn more at https://developers.cloudflare.com/queues/javascript-apis/
// MY_QUEUE: Queue
}

interface TelegramChat {
id: number // Chat ID
// 添加更多的聊天相关字段
}

interface TelegramMessage {
message_id: number // Message ID
chat: TelegramChat // Chat object
text?: string // Received message text, optional
reply_to_message?: TelegramMessage // 添加这个字段来获取回复的消息
from?: {
username?: string // 发送者的用户名
}
// 添加更多的消息相关字段
}

interface TelegramUpdate {
update_id: number // Update ID from Telegram
message?: TelegramMessage // Message object, optional
// 添加更多的更新相关字段
}

async function handleTelegramUpdate(update: TelegramUpdate, env: Env) {
// 确定update是否包含消息
if (!update.message || !update.message.text) {
return
}

let replyText = ''

// 检查消息是否是同步到Twitter的命令
if (update.message.text.startsWith('/sync_twitter') && update.message.reply_to_message) {
try {
// 移除命令部分,将剩下的文本同步到Twitter
// 获取原始消息的内容
const tweetContent = `${update.message.reply_to_message.text} #sync_from_telegram`
const senderUsername = update.message.from?.username // 获取消息发送者的用户名
if (!tweetContent) {
throw new Error('No text in replied message')
}

const tweet = await postTweet(tweetContent, env)
// 构建回复的消息
replyText = `@${senderUsername} Your message has been posted to Twitter. Id: ${tweet.data.id}`
} catch (error) {
// 可以选择发送失败通知回Telegram
replyText = `Failed to post tweet ${error}`
}
} else {
// 不是同步到 Twitter 的命令,直接回复
replyText = `Echo: ${update.message.text}`
}

// 构建回复的消息
const chatId = update.message.chat.id

// 向Telegram发送回复
const response = await fetch(`https://api.telegram.org/bot${env.TELEGRAM_BOT_SECRET}/sendMessage`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
chat_id: chatId,
text: replyText,
reply_to_message_id: update.message.message_id, // 指定要回复的消息ID
}),
})

if (!response.ok) {
throw new Error(`Telegram API responded with status ${response.status}`)
}
}

async function postTweet(text: string, env: Env): Promise<any> {
const oauth = new OAuth({
consumer: { key: env.TWITTER_API_KEY, secret: env.TWITTER_API_SECRET },
signature_method: 'HMAC-SHA1',
hash_function(baseString, key) {
return HmacSHA1(baseString, key).toString(enc.Base64)
},
})

const oauthToken = {
key: env.TWITTER_ACCESS_TOKEN,
secret: env.TWITTER_ACCESS_TOKEN_SECRET,
}

const requestData = {
url: "https://api.twitter.com/2/tweets",
method: 'POST',
}

const response = await fetch(requestData.url, {
method: 'POST',
headers: {
...oauth.toHeader(oauth.authorize(requestData, oauthToken)),
'content-type': "application/json",
},
body: JSON.stringify({ text }),
})

return await response.json()
}
import { handleTelegramUpdate } from './telegram'
import { Env, TelegramUpdate } from './type'

export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
Expand Down
76 changes: 76 additions & 0 deletions src/telegram.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// telegram.js
import * as twitter from './twitter'
import { Env, TelegramUpdate } from './type'

// Process the Telegram update received
export async function handleTelegramUpdate(update: TelegramUpdate, env: Env) {
const allowedUserIds = env.ALLOW_USER_IDS
const fromUserId = update.message?.from?.id.toString() || ''
const fromUsername = update.message?.from?.username || ''

// Exit if there is no message text
if (!update.message?.text) return
let replyText = ''

// Check if the user is allowed to interact with the bot
if (!allowedUserIds.includes(fromUsername) && !allowedUserIds.includes(fromUserId)) {
return
} else if (update.message.text.startsWith('/sync_twitter')) {
replyText = await processSyncTwitterCommand(update, env)
} else {
replyText = `Echo: ${update.message.text}`
}

// Send a reply message to Telegram
await sendReplyToTelegram(update.message.chat.id, replyText, update.message.message_id, env)
}

// Process the '/sync_twitter' command
async function processSyncTwitterCommand(update: TelegramUpdate, env: Env): Promise<string> {
if (!update.message?.reply_to_message?.photo?.length) {
return 'No photo found to sync with Twitter.'
}

const bestPhoto = update.message.reply_to_message.photo[update.message.reply_to_message.photo.length - 1]
const photoUrl = await getTelegramFileUrl(bestPhoto.file_id, env.TELEGRAM_BOT_SECRET)
const mediaData = await fetch(photoUrl).then(res => res.arrayBuffer())

try {
const media = await twitter.uploadMediaToTwitter(mediaData, env)
const tweetContent = `${update.message.reply_to_message.caption} #sync_from_telegram`
const tweet = await twitter.postTweet(tweetContent, [media.media_id_string], env)

return `Your message has been posted to Twitter. Id: ${tweet.data.id}`
} catch (error) {
return `Failed to post tweet: ${error}`
}
}


// Fetch the file URL from Telegram using the file_id
async function getTelegramFileUrl(fileId: string, botSecret: string): Promise<string> {
const fileResponse = await fetch(`https://api.telegram.org/bot${botSecret}/getFile?file_id=${fileId}`)
if (!fileResponse.ok) {
throw new Error(`Telegram API getFile responded with status ${fileResponse.status}`)
}
const fileData = await fileResponse.json() as { ok: boolean, result: { file_path: string } }
const filePath = fileData.result.file_path
return `https://api.telegram.org/file/bot${botSecret}/${filePath}`
}

// Send a message back to Telegram chat
async function sendReplyToTelegram(chatId: number, text: string, messageId: number, env: Env) {
const response = await fetch(`https://api.telegram.org/bot${env.TELEGRAM_BOT_SECRET}/sendMessage`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chat_id: chatId,
text,
reply_to_message_id: messageId,
}),
})

if (!response.ok) {
throw new Error(`Telegram API sendMessage responded with status ${response.status}`)
}
}
70 changes: 70 additions & 0 deletions src/twitter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import OAuth from 'oauth-1.0a'
import { HmacSHA1, enc } from 'crypto-js'
import { Buffer } from 'node:buffer'
import { Env } from "./type"

export async function uploadMediaToTwitter(mediaData: ArrayBuffer, env: Env): Promise<any> {
const oauth = new OAuth({
consumer: { key: env.TWITTER_API_KEY, secret: env.TWITTER_API_SECRET },
signature_method: 'HMAC-SHA1',
hash_function(baseString, key) {
return HmacSHA1(baseString, key).toString(enc.Base64)
},
})

const oauthToken = {
key: env.TWITTER_ACCESS_TOKEN,
secret: env.TWITTER_ACCESS_TOKEN_SECRET,
}

const requestData = {
url: 'https://upload.twitter.com/1.1/media/upload.json?media_category=tweet_image',
method: 'POST',
data: {
media_data: Buffer.from(mediaData).toString('base64'),
},
}

// 初始化媒体上传以获取media_id
const response = await fetch(requestData.url, {
method: 'POST',
headers: {
...oauth.toHeader(oauth.authorize(requestData, oauthToken)),
'content-type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({ media_data: Buffer.from(mediaData).toString('base64') }),
})

return await response.json()
}

export async function postTweet(text: string, mediaList: string[], env: Env): Promise<any> {
const oauth = new OAuth({
consumer: { key: env.TWITTER_API_KEY, secret: env.TWITTER_API_SECRET },
signature_method: 'HMAC-SHA1',
hash_function(baseString, key) {
return HmacSHA1(baseString, key).toString(enc.Base64)
},
})

const oauthToken = {
key: env.TWITTER_ACCESS_TOKEN,
secret: env.TWITTER_ACCESS_TOKEN_SECRET,
}

const requestData = {
url: "https://api.twitter.com/2/tweets",
method: 'POST',
}

const response = await fetch(requestData.url, {
method: 'POST',
headers: {
...oauth.toHeader(oauth.authorize(requestData, oauthToken)),
'content-type': "application/json",
},
body: JSON.stringify({ text, media: { media_ids: mediaList } }),
})

return await response.json()
}
56 changes: 56 additions & 0 deletions src/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
export interface Env {
TELEGRAM_BOT_SECRET: string
TWITTER_API_KEY: string
TWITTER_API_SECRET: string
TWITTER_ACCESS_TOKEN: string
TWITTER_ACCESS_TOKEN_SECRET: string
ALLOW_USER_IDS: string[]

// Example binding to KV. Learn more at https://developers.cloudflare.com/workers/runtime-apis/kv/
// MY_KV_NAMESPACE: KVNamespace
//
// Example binding to Durable Object. Learn more at https://developers.cloudflare.com/workers/runtime-apis/durable-objects/
// MY_DURABLE_OBJECT: DurableObjectNamespace
//
// Example binding to R2. Learn more at https://developers.cloudflare.com/workers/runtime-apis/r2/
// MY_BUCKET: R2Bucket
//
// Example binding to a Service. Learn more at https://developers.cloudflare.com/workers/runtime-apis/service-bindings/
// MY_SERVICE: Fetcher
//
// Example binding to a Queue. Learn more at https://developers.cloudflare.com/queues/javascript-apis/
// MY_QUEUE: Queue
}

interface TelegramChat {
id: number // Chat ID
// 添加更多的聊天相关字段
}

interface TelegramPhoto {
file_id: string // 可用于获取文件内容
file_unique_id: string // 文件的唯一标识符
width: number // 图片宽度
height: number // 图片高度
file_size?: number // 文件大小(可选)
}

interface TelegramMessage {
message_id: number // Message ID
chat: TelegramChat // Chat object
text?: string // Received message text, optional
reply_to_message?: TelegramMessage // 添加这个字段来获取回复的消息
from: {
username: string // 发送者的用户名
id: string // 发送者的ID
},
caption?: string
photo?: TelegramPhoto[] // TelegramPhoto需要根据API定义
// 添加更多的消息相关字段
}

export interface TelegramUpdate {
update_id: number // Update ID from Telegram
message?: TelegramMessage // Message object, optional
// 添加更多的更新相关字段
}
Loading

0 comments on commit 131bcd1

Please sign in to comment.