Skip to content

Commit 1991d53

Browse files
authored
feat: hub bot app (#254)
* feat: hub bot app * chore: update * chore: update
1 parent a56e52b commit 1991d53

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+1659
-26
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
name: Deploy @roll-stack/hub-telegram
2+
3+
on:
4+
workflow_run:
5+
workflows: ["Docker Nightly", "Docker Release"]
6+
types:
7+
- completed
8+
workflow_dispatch:
9+
10+
jobs:
11+
deploy:
12+
if: ${{ github.event.workflow_run.conclusion == 'success' }}
13+
strategy:
14+
fail-fast: false
15+
matrix:
16+
include:
17+
- url: "https://sushi-love.ru"
18+
environment: "Production Hub Telegram"
19+
deployment: "hub-telegram"
20+
namespace: "sushi"
21+
runs-on: ubuntu-latest
22+
permissions:
23+
deployments: write
24+
contents: read
25+
26+
steps:
27+
- name: Checkout
28+
uses: actions/checkout@v5
29+
30+
- uses: chrnorm/deployment-action@v2
31+
name: Create GitHub deployment
32+
id: deployment
33+
with:
34+
token: '${{ github.token }}'
35+
environment-url: ${{ matrix.url }}
36+
environment: ${{ matrix.environment }}
37+
38+
- name: Connect to k8s cluster and restart deployment
39+
uses: actions-hub/kubectl@master
40+
env:
41+
KUBE_CONFIG: ${{ secrets.KUBE_CONFIG }}
42+
with:
43+
args: rollout restart deployment/${{ matrix.deployment }} -n ${{ matrix.namespace }}
44+
45+
- name: Update deployment status (success)
46+
if: success()
47+
uses: chrnorm/deployment-status@v2
48+
with:
49+
token: '${{ github.token }}'
50+
environment-url: ${{ steps.deployment.outputs.environment_url }}
51+
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
52+
state: 'success'
53+
54+
- name: Update deployment status (failure)
55+
if: failure()
56+
uses: chrnorm/deployment-status@v2
57+
with:
58+
token: '${{ github.token }}'
59+
environment-url: ${{ steps.deployment.outputs.environment_url }}
60+
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
61+
state: 'failure'

.github/workflows/docker-nightly.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
- name: Set matrix
2727
id: set-matrix
2828
run: |
29-
APPS=("web-app" "web-storefront" "web-parser" "atrium-telegram" "storefront-telegram" "core-telegram" "webinar")
29+
APPS=("web-app" "web-storefront" "web-parser" "atrium-telegram" "hub-telegram" "storefront-telegram" "core-telegram" "webinar")
3030
CHANGED=()
3131
3232
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then

.github/workflows/docker-release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ jobs:
3333
- name: Set package in env
3434
id: set-package
3535
run: |
36-
APPS=("web-app" "web-storefront" "web-parser" "atrium-telegram" "storefront-telegram" "core-telegram" "webinar")
36+
APPS=("web-app" "web-storefront" "web-parser" "atrium-telegram" "hub-telegram" "storefront-telegram" "core-telegram" "webinar")
3737
MATCH="${{ steps.regex-match.outputs.match }}"
3838
3939
if [ -z "$MATCH" ]; then

apps/atrium-telegram/app/app.config.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,33 @@ export default defineAppConfig({
33
badge: {
44
variants: {
55
color: {
6-
secondary: '!text-white disabled:!bg-inverted/25',
6+
secondary: 'text-white! disabled:bg-inverted/25!',
77
},
88
},
99
},
1010
input: {
1111
slots: {
12-
base: '!py-2.5 !rounded-lg !text-lg/5 !font-bold !ring-default !placeholder:text-muted/50',
12+
base: 'py-2.5! rounded-lg! text-lg/5! font-bold! ring-default! placeholder:text-muted/50!',
1313
},
1414
},
1515
inputMenu: {
1616
slots: {
17-
base: '!ring-default placeholder:text-muted/50',
17+
base: 'ring-default! placeholder:text-muted/50',
1818
},
1919
},
2020
select: {
2121
slots: {
22-
base: '!rounded-lg !text-lg/5 !font-bold !ring-default !placeholder:text-muted/50',
22+
base: 'rounded-lg! text-lg/5! font-bold! ring-default! placeholder:text-muted/50!',
2323
},
2424
},
2525
selectMenu: {
2626
slots: {
27-
base: '!rounded-lg !text-lg/5 !font-bold !ring-default !placeholder:text-muted/50',
27+
base: 'rounded-lg! text-lg/5! font-bold! ring-default! placeholder:text-muted/50!',
2828
},
2929
},
3030
textarea: {
3131
slots: {
32-
base: '!py-2.5 !rounded-lg !text-lg/5 !font-bold !ring-default !placeholder:text-muted/50',
32+
base: 'py-2.5! rounded-lg! text-lg/5! font-bold! ring-default! placeholder:text-muted/50!',
3333
},
3434
},
3535
button: {
@@ -43,22 +43,22 @@ export default defineAppConfig({
4343
},
4444
},
4545
color: {
46-
secondary: '!text-white disabled:!bg-inverted/25',
46+
secondary: 'text-white! disabled:bg-inverted/25!',
4747
},
4848
},
4949
},
5050
tabs: {
5151
variants: {
5252
variant: {
5353
pill: {
54-
trigger: 'data-[state=active]:!text-white',
54+
trigger: 'data-[state=active]:text-white!',
5555
},
5656
},
5757
},
5858
},
5959
modal: {
6060
slots: {
61-
content: 'divide-y-0 !ring-default',
61+
content: 'divide-y-0 ring-default!',
6262
header: 'pb-0 min-h-12',
6363
title: 'font-semibold',
6464
},
@@ -67,7 +67,7 @@ export default defineAppConfig({
6767
slots: {
6868
header: 'text-xl/6 font-bold font-headers',
6969
body: 'mb-12 hide-scroll',
70-
content: '!max-h-10/12 ring-default/50 hide-scroll',
70+
content: 'max-h-10/12! ring-default/50 hide-scroll',
7171
},
7272
},
7373
navigationMenu: {

apps/core-telegram/server/plugins/04.telegram.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import process from 'node:process'
22
import { useCreateAtriumBot } from '../services/telegram/atrium-bot'
3+
import { useCreateHubBot } from '../services/telegram/hub-bot'
34
import { useCreateOrderBot } from '../services/telegram/order-bot'
4-
import { useCreateWasabiBot } from '../services/telegram/wasabi-bot'
55

66
export default defineNitroPlugin(async () => {
77
const logger = useLogger('plugin:start-telegram')
@@ -20,7 +20,7 @@ export default defineNitroPlugin(async () => {
2020

2121
// Start the bots (using long polling)
2222
await Promise.all([
23-
useCreateWasabiBot(),
23+
useCreateHubBot(),
2424
useCreateAtriumBot(),
2525
useCreateOrderBot(),
2626
])

apps/core-telegram/server/services/queue/index.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,17 +66,15 @@ async function handleFlowItemCreated(data: FlowItemCreated['data']): Promise<boo
6666
const separator = 'zzzzz'
6767
const startAppData = `flow${separator}${data.itemId}`
6868

69-
// Get first words
70-
const messageIntro = data.description.split(' ').slice(0, 100).join(' ')
71-
const preparedMessage = `${messageIntro}...\n\nОстальное в Атриуме 🙃`
69+
const preparedMessage = data.description
7270

7371
await useAtriumBot().api.sendMessage(telegram.teamGroupId, preparedMessage, {
7472
link_preview_options: {
7573
is_disabled: true,
7674
},
7775
reply_markup: {
7876
inline_keyboard: [[{
79-
text: '👉 Открыть Атриум',
77+
text: '👉 Открыть в Атриуме',
8078
url: `https://t.me/sushi_atrium_bot/app?startapp=${startAppData}`,
8179
}]],
8280
},
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import type { User } from '@roll-stack/database'
2+
import type { Context } from 'grammy'
3+
import { createId } from '@paralleldrive/cuid2'
4+
import { db } from '@roll-stack/database'
5+
import { Bot } from 'grammy'
6+
import { generateAccessCode, getBotToken, requestContactPhone } from './common'
7+
8+
const logger = useLogger('telegram:hub-bot')
9+
const { telegram } = useRuntimeConfig()
10+
11+
let bot: Bot | null = null
12+
13+
export async function useCreateHubBot() {
14+
const token = await getBotToken(telegram.wasabiBotId)
15+
if (!token) {
16+
throw new Error('Hub bot is not configured')
17+
}
18+
19+
bot = new Bot(token, {
20+
client: { apiRoot: telegram.localBotApiServerUrl },
21+
})
22+
23+
bot.on('message:text', async (ctx) => {
24+
if (ctx.hasCommand('start')) {
25+
return handleStart(ctx)
26+
}
27+
28+
return handleMessage(ctx)
29+
})
30+
31+
// User shared contact
32+
bot.on('message:contact', async (ctx) => {
33+
return handleContact(ctx)
34+
})
35+
36+
// Somebody invited bot to a group
37+
bot.on('my_chat_member', async (ctx) => {
38+
logger.log('my_chat_member', ctx.update)
39+
})
40+
41+
try {
42+
await bot.start()
43+
logger.info('Hub bot started successfully')
44+
} catch (error) {
45+
logger.error('Failed to start Hub bot:', error)
46+
throw error
47+
}
48+
}
49+
50+
async function handleStart(ctx: Context) {
51+
if (!ctx.message) {
52+
return
53+
}
54+
55+
// Not private chat?
56+
if (ctx.message.chat.type !== 'private') {
57+
await ctx.reply('Я пока не умею отвечать на групповые сообщения.')
58+
return
59+
}
60+
61+
// Find user
62+
const telegramUser = await db.telegram.findUserByTelegramIdAndBotId(ctx.message.from.id.toString(), telegram.wasabiBotId)
63+
if (!telegramUser) {
64+
// Request phone number from user
65+
await requestContactPhone(ctx)
66+
return
67+
}
68+
69+
if (!telegramUser.user || !telegramUser.user.isActive) {
70+
await ctx.reply('Нет доступа. Используйте ранее полученный Ключ доступа.')
71+
return
72+
}
73+
74+
await ctx.reply('Вы уже авторизованы.')
75+
}
76+
77+
async function handleMessage(ctx: Context) {
78+
if (!ctx.message || !ctx.message.text) {
79+
return
80+
}
81+
82+
const telegramUser = await db.telegram.findUserByTelegramIdAndBotId(ctx.message.from.id.toString(), telegram.wasabiBotId)
83+
if (!telegramUser?.user) {
84+
return
85+
}
86+
87+
logger.log('message', telegramUser.user.id, ctx.message.from.id, ctx.message.text)
88+
}
89+
90+
async function handleContact(ctx: Context) {
91+
if (!ctx.message?.contact) {
92+
return
93+
}
94+
95+
// Not private chat?
96+
if (ctx.message.chat.type !== 'private') {
97+
await ctx.reply('Я пока не умею отвечать на групповые сообщения.')
98+
return
99+
}
100+
101+
const botToken = await getBotToken(telegram.wasabiBotId)
102+
if (!botToken) {
103+
return null
104+
}
105+
106+
const phone = ctx.message.contact.phone_number.replace(/\D/g, '')
107+
const user = await findOrCreateHubUser({
108+
phone,
109+
user: {
110+
name: ctx.message.from.first_name,
111+
surname: ctx.message.from.last_name,
112+
},
113+
ctx,
114+
botToken,
115+
})
116+
117+
const telegramUser = await db.telegram.findUserByTelegramIdAndBotId(ctx.message.from.id.toString(), telegram.wasabiBotId)
118+
if (!telegramUser?.id) {
119+
const accessKey = await generateAccessCode()
120+
121+
const createdTelegramUser = await db.telegram.createUser({
122+
telegramUserType: ctx.message.chat.type,
123+
telegramId: ctx.message.from.id.toString(),
124+
firstName: ctx.message.from.first_name,
125+
lastName: ctx.message.from.last_name,
126+
username: ctx.message.from.username,
127+
botId: telegram.wasabiBotId,
128+
accessKey,
129+
userId: user.id,
130+
})
131+
132+
logger.log('New Telegram user', createdTelegramUser)
133+
134+
await ctx.setChatMenuButton({
135+
chat_id: ctx.message.chat.id,
136+
menu_button: {
137+
type: 'web_app',
138+
text: 'Хаб',
139+
web_app: {
140+
url: 'https://t.me/wasabi_hub_bot/app',
141+
},
142+
},
143+
})
144+
145+
await ctx.reply(`🎉 Успех! Через этого бота вы можете открыть Хаб. Это место, в котором мы проводим события и делимся полезной информацией.`, {
146+
reply_markup: {
147+
remove_keyboard: true,
148+
},
149+
})
150+
return
151+
}
152+
153+
await ctx.reply('Номер уже подтвержден.')
154+
}
155+
156+
async function findOrCreateHubUser(data: { phone: string, user: { name: string, surname: string | undefined }, ctx: Context, botToken: string }): Promise<User> {
157+
const userInDB = await db.user.findByPhone(data.phone)
158+
if (!userInDB) {
159+
const id = createId()
160+
const avatarUrl = `https://avatar.nextorders.ru/${id}?emotion=7&gender=female`
161+
162+
const createdUser = await db.user.create({
163+
id,
164+
phone: data.phone,
165+
type: 'prospective_partner',
166+
name: data.user.name,
167+
surname: data.user.surname,
168+
avatarUrl,
169+
gender: 'female',
170+
caption: 'Новый пользователь',
171+
})
172+
logger.log('New prospective partner', createdUser)
173+
174+
return createdUser
175+
}
176+
177+
return userInDB
178+
}
179+
180+
export function useHubBot(): Bot {
181+
if (!bot) {
182+
throw new Error('Hub bot is not initialized. Call useCreateHubBot() first.')
183+
}
184+
185+
return bot
186+
}

apps/hub-telegram/.env.example

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Main database
2+
DATABASE_URL=
3+
4+
# Queue
5+
QUEUE_URL=
6+
7+
# Telegram
8+
NUXT_TELEGRAM_ADMIN_ID=
9+
NUXT_TELEGRAM_HUB_BOT_ID=
10+
NUXT_TELEGRAM_HUB_BOT_TOKEN=
11+
NUXT_TELEGRAM_DEV_BOT_TOKEN=
12+
NUXT_TELEGRAM_TEAM_GROUP_ID=
13+
14+
# App version
15+
VERSION=

0 commit comments

Comments
 (0)