Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User engagement #365

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions package-lock.json

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

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,11 @@
"litllm": "^3.0.0",
"lokijs": "^1.5.12",
"lru-cache": "^10.0.0",
"marked": "^14.1.2",
"moment": "^2.29.4",
"moment-timezone": "^0.5.43",
"node-cron": "^3.0.2",
"node-html-parser": "^6.1.13",
"openai": "^4.0.1",
"otpauth": "^9.1.3",
"pg": "^8.11.2",
Expand Down
41 changes: 31 additions & 10 deletions src/bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
type OnMessageContext, type PayableBot, type PayableBotConfig,
RequestState, type UtilityBot
} from './modules/types'
import { mainMenu } from './pages'
import { groupsMainMenu, mainMenu, privateChatMainMenu } from './pages'
import { TranslateBot } from './modules/translate/TranslateBot'
import { VoiceMemo } from './modules/voice-memo'
// import { QRCodeBot } from './modules/qrcode/QRCodeBot'
Expand Down Expand Up @@ -55,9 +55,10 @@ import { VoiceToTextBot } from './modules/voice-to-text'
import { now } from './utils/perf'
import { VoiceToVoiceGPTBot } from './modules/voice-to-voice-gpt'
// import { VoiceCommand } from './modules/voice-command'
import { createInitialSessionData } from './helpers'
import { createInitialSessionData, addQuotePrefix, markdownToTelegramHtml } from './helpers'
import { LlamaAgent } from './modules/subagents'
import { llmModelManager } from './modules/llms/utils/llmModelsManager'
import { HmnyBot } from './modules/hmny'

Events.EventEmitter.defaultMaxListeners = 30

Expand Down Expand Up @@ -184,7 +185,10 @@ bot.use(async (ctx: BotContext, next: NextFunction): Promise<void> => {

bot.use(
session({
initial: createInitialSessionData,
initial: () => {
logger.info('Creating new session')
return createInitialSessionData()
},
storage: enhanceStorage<BotSessionData>({
storage: new MemorySessionStorage<Enhance<BotSessionData>>(),
millisecondsToLive: config.sessionTimeout * 60 * 60 * 1000 // 48 hours
Expand All @@ -193,6 +197,8 @@ bot.use(
)
bot.use(autoChatAction())
bot.use(mainMenu)
bot.use(privateChatMainMenu)
bot.use(groupsMainMenu)

const voiceMemo = new VoiceMemo()
// const qrCodeBot = new QRCodeBot()
Expand All @@ -212,7 +218,7 @@ const voiceTranslateBot = new VoiceTranslateBot(payments)
const textToSpeechBot = new TextToSpeechBot(payments)
const voiceToTextBot = new VoiceToTextBot(payments)
const voiceToVoiceGPTBot = new VoiceToVoiceGPTBot(payments)

const hmnyBot = new HmnyBot()
// const voiceCommand = new VoiceCommand(openAiBot)

bot.on('message:new_chat_members:me', async (ctx) => {
Expand Down Expand Up @@ -324,6 +330,7 @@ const PayableBots: Record<string, PayableBotConfig> = {
// voiceCommand: { bot: voiceCommand },
// qrCodeBot: { bot: qrCodeBot },
// sdImagesBot: { bot: sdImagesBot },
hmny: { bot: hmnyBot },
voiceTranslate: { bot: voiceTranslateBot },
voiceMemo: { bot: voiceMemo },
translateBot: { bot: translateBot },
Expand Down Expand Up @@ -356,8 +363,14 @@ const executeOrRefund = async (ctx: OnMessageContext, price: number, bot: Payabl

const onMessage = async (ctx: OnMessageContext): Promise<void> => {
try {
// bot doesn't handle forwarded messages
if (!ctx.message.forward_origin) {
const { voice, audio } = ctx.update.message
const isVoiceForwardingEnabled = ctx.session.voiceMemo.isVoiceForwardingEnabled ||
ctx.session.voiceMemo.isOneTimeForwardingVoiceEnabled
// bot doesn't handle forwarded messages unless is audio/voice message and is isVoiceForwardingEnabled is true
if (!ctx.message.forward_origin ||
(isVoiceForwardingEnabled &&
ctx.message.forward_origin &&
(!!voice || !!audio))) {
await assignFreeCredits(ctx)

if (llamaAgent.isSupportedEvent(ctx)) {
Expand Down Expand Up @@ -460,16 +473,24 @@ bot.command(['start', 'help', 'menu'], async (ctx) => {
const { totalCreditsAmount } = await chatService.getUserCredits(accountId)
const balance = addressBalance.plus(totalCreditsAmount)
const balanceOne = payments.toONE(balance, false).toFixed(2)

const broadcastMessage = ctx.session.lastBroadcast
? `\n<b></b>\n<b>Latest from the team</b>\n${await addQuotePrefix(ctx.session.lastBroadcast)}`
: ''

const startText = commandsHelpText.start
.replaceAll('$BROADCAST', broadcastMessage)
.replaceAll('$CREDITS', balanceOne + '')
.replaceAll('$WALLET_ADDRESS', account.address)

await ctx.reply(startText, {
parse_mode: 'Markdown',
reply_markup: mainMenu,
const htmlStartText = await markdownToTelegramHtml(startText)

await ctx.reply(htmlStartText, {
parse_mode: 'HTML',
reply_markup: ctx.chat.type === 'private' ? privateChatMainMenu : groupsMainMenu,
link_preview_options: { is_disabled: true },
message_thread_id: ctx.message?.message_thread_id
})
}).catch(e => { console.log(e) })
})

const logErrorHandler = (ex: any): void => {
Expand Down
14 changes: 8 additions & 6 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,15 @@ export default {
}
},
chatGpt: {
chatCompletionContext:
'You are an AI Bot powered by Harmony. Your strengths are ai api aggregation for chat, image, and voice interactions. Leveraging a suite of sophisticated subagents, you have the capability to perform tasks such as internet browsing and accessing various services. Your responses should be adaptable to the conversation while maintaining brevity, ideally not exceeding 100 words.',
chatCompletionContext: 'Reply ideally not exceeding 100 words',
// 'You are an AI Bot powered by Harmony. Your strengths are ai api aggregation for chat, image, and voice interactions. Leveraging a suite of sophisticated subagents, you have the capability to perform tasks such as internet browsing and accessing various services. Your responses should be adaptable to the conversation while maintaining brevity, ideally not exceeding 100 words.',
// 'You are an AI Bot powered dby Harmony. Your strengths are ai api aggregation for chat, image, and voice interactions, and more. You have subagents that helps you with task like browsing the internet, and other services. Respond flexibly, but try to stay within 100 words in all of your responses.',
webCrawlerContext: 'You will receive a web crawling text. Please get keys concepts, but try to stay within 4000 words in your response.',
visionCompletionContext: `You are a concise AI Bot powered by Harmony, capable of providing complete responses within a 100-word limit.
For each additional image, extend your response by 30 words. Your responses should be informative and comprehensive,
wrapping up all details without leaving them hanging. Use your flexibility to adapt to any topic, and deliver engaging and fulfilling
conversations in a succinct manner.`,
visionCompletionContext: 'Response within a 100-word limit',
// `You are a concise AI Bot powered by Harmony, capable of providing complete responses within a 100-word limit.
// For each additional image, extend your response by 30 words. Your responses should be informative and comprehensive,
// wrapping up all details without leaving them hanging. Use your flexibility to adapt to any topic, and deliver engaging and fulfilling
// conversations in a succinct manner.`,
maxTokens: parseInt(process.env.OPENAI_MAX_TOKENS ?? '800'), // telegram messages has a char limit
wordLimit: 30,
wordCountBetween: 10,
Expand Down Expand Up @@ -105,6 +106,7 @@ export default {
},
voiceMemo: {
isEnabled: Boolean(parseInt(process.env.VOICE_MEMO_ENABLED ?? '1')),
isVoiceForwardingEnabled: Boolean(parseInt(process.env.VOICE_MEMO_FORWARDING_ENABLED ?? '0')),
telegramApiId: parseInt(process.env.TELEGRAM_API_ID ?? ''),
telegramApiHash: process.env.TELEGRAM_API_HASH ?? '',
speechmaticsApiKey: process.env.SPEECHMATICS_API_KEY ?? '',
Expand Down
43 changes: 28 additions & 15 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export enum MenuIds {
MAIN_MENU = 'main-menu',
PRIVATE_MAIN_MENU = 'private-main-menu',
IMAGE_MENU = 'image-menu-main',
QR_BOT_MAIN = 'qrbot-menu-main',
QR_BOT_CHANGE_OPTIONS = 'qrbot-menu-change-options',
Expand All @@ -16,30 +17,37 @@ export enum MenuIds {
CHAT_GPT_MODEL = 'chat-gpt-model',
}

export const MENU_URL_BUTTONS = [
{
text: '🛠 Build on Harmony',
url: 'https://docs.harmony.one/home'
},
{
text: '🏠 Harmony',
url: 'https://harmony.one'
}
]

export const docsMenuLabel = 'A fast and open platform for decentralized applications of AI ∩ Crypto. To scale trust, create a radically fair economy, and push humanity into becoming deus.'

const DOUBLE_NEW_LINE = '\n<b></b>\n'
// const balance = await payments.getAddressBalance(userWalletAddress);
// const balanceOne = payments.toONE(balance, false).toFixed(2);
// const startText = commandsHelpText.start
// .replace("$CREDITS", balanceOne + "")
// .replace("$WALLET_ADDRESS", userWalletAddress);

// Your credits: $CREDITS ONE tokens. Send to $WALLET_ADDRESS for recharge.
let startText = "Hello, I'm ONE Bot on Telegram from Harmony – for ALL your AI wishes 🧚‍♀️.$BROADCAST"
startText += `${DOUBLE_NEW_LINE}/ask how to add harmony to metamask`
startText += `${DOUBLE_NEW_LINE}/image glimpses of a herd of wild elephants crossing a savanna`
startText += `${DOUBLE_NEW_LINE}/more Summarize voice messages, artistic QR code, ChatGPT 32K, DALL-E, Wallet Connect, send tokens, sign transactions...`
startText += `${DOUBLE_NEW_LINE}/help Show this message. Join user group @onebotlove or read docs at harmony.one/bot.`
startText += `${DOUBLE_NEW_LINE}Your credits in 1Bot Credits: $CREDITS`
startText += `${DOUBLE_NEW_LINE}Send ONE to: \`$WALLET_ADDRESS\``

export const commandsHelpText = {
start: `Hello, I'm ONE Bot on Telegram from Harmony – for ALL your AI wishes 🧚‍♀️.

/ask act like elon musk, expand our [q4 roadmap](https://xn--qv9h.s.country/p/generating-roadmap-as-ceo-vs-cto) "telegram ai bot"
/ask act like mark zuckerberg instead

/image glimpses of a herd of wild elephants crossing a savanna

/more Summarize voice messages, artistic QR code, ChatGPT 32K, DALL-E, Wallet Connect, send tokens, sign transactions...

/help Show this message. Join user group @onebotlove or read docs at harmony.one/bot.

Your credits in 1Bot Credits: $CREDITS

Send ONE to: \`$WALLET_ADDRESS\`
`,
start: startText,
// more: `/ explain like i am 5, what is a superconductor?
// . explain like i have a phd, what is category theory?

Expand Down Expand Up @@ -278,6 +286,11 @@ export const PROMPTS = {
'(KHFB, AuroraNegative),(Worst Quality, Low Quality:1.4), ugly, tiling, poorly drawn hands, poorly drawn feet, poorly drawn face, out of frame, extra limbs, disfigured, deformed, body out of frame, bad anatomy, watermark, signature, cut off, low contrast, underexposed, overexposed, bad art, beginner, amateur, distorted face, blurry, draft, grainy'
}

export const VOICE_MEMO_FORWARDING = {
enabled: 'Voice note forwarding is now active. The next voice note you send will be forwarded automatically. This setting will deactivate after forwarding one voice note.',
restricted: 'Sorry, voice note forwarding can only be enabled by admin users. If you need this feature, please contact an admin for assistance.'
}

export const ALIAS = {
text: `
Shortcut Commands ⏳
Expand Down
7 changes: 7 additions & 0 deletions src/database/stats.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,4 +239,11 @@ export class StatsService {

return await queryBuilder.execute()
}

public async getAllChatId (): Promise<number[]> {
const queryBuilder = logRepository.createQueryBuilder('logs')
.select('distinct("groupId")')

return await queryBuilder.execute()
}
}
81 changes: 79 additions & 2 deletions src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import config from './config'
import { LlmModelsEnum } from './modules/llms/utils/llmModelsManager'
import { type DalleImageSize } from './modules/llms/utils/types'
import { type BotSessionData } from './modules/types'

import { marked } from 'marked'
import { parse as parseHtml, HTMLElement } from 'node-html-parser'
export function createInitialSessionData (): BotSessionData {
return {
oneCountry: { lastDomain: '' },
Expand Down Expand Up @@ -47,6 +48,82 @@ export function createInitialSessionData (): BotSessionData {
isInscriptionLotteryEnabled: config.openAi.dalle.isInscriptionLotteryEnabled,
imgInquiried: []
},
currentModel: LlmModelsEnum.GPT_4O
voiceMemo: {
isOneTimeForwardingVoiceEnabled: false,
isVoiceForwardingEnabled: config.voiceMemo.isVoiceForwardingEnabled
},
currentModel: LlmModelsEnum.GPT_4O,
lastBroadcast: ''
}
}

type AllowedAttributesType = Record<string, string[]>

function sanitizeHtml (html: string): string {
const allowedTags = [
'b', 'strong', 'i', 'em', 'u', 'ins', 's', 'strike', 'del',
'span', 'tg-spoiler', 'a', 'code', 'pre', 'tg-emoji', 'blockquote'
]
const allowedAttributes: AllowedAttributesType = {
a: ['href'],
span: ['class'],
'tg-emoji': ['emoji-id'],
pre: ['class'],
code: ['class'],
blockquote: ['expandable']
}
const root = parseHtml(html)

function walk (node: HTMLElement): void {
if (node.nodeType === 1 && node.tagName) { // ELEMENT_NODE with a tagName
const tagName = node.tagName.toLowerCase()
if (!allowedTags.includes(tagName)) {
const children = node.childNodes
node.replaceWith(...children)
children.forEach(child => {
if (child instanceof HTMLElement) {
walk(child)
}
})
return
} else {
// Remove disallowed attributes
const allowedAttrs = allowedAttributes[tagName] || []
const attributes = node.attributes
Object.keys(attributes).forEach(attrName => {
if (!allowedAttrs.includes(attrName)) {
node.removeAttribute(attrName)
}
})
// Special case for span with tg-spoiler class
if (tagName === 'span' && node.getAttribute('class') !== 'tg-spoiler') {
node.removeAttribute('class')
}
}
}
node.childNodes.forEach(child => {
if (child instanceof HTMLElement) {
walk(child)
}
})
}

walk(root)
return root.toString()
}

export async function markdownToTelegramHtml (text: string): Promise<string> {
try {
const html = await marked(text)
return sanitizeHtml(html)
} catch (error) {
console.error('Error parsing markdown:', error)
return text // Return original text if parsing fails
}
}

export async function addQuotePrefix (text: string): Promise<string> {
if (!text) return ''
const htmlText = await markdownToTelegramHtml(text)
return `<blockquote>${htmlText}</blockquote>`
}
Loading
Loading