Skip to content

Commit

Permalink
multi user access, deeplinks
Browse files Browse the repository at this point in the history
  • Loading branch information
sliterok committed Jul 1, 2024
1 parent 2fa4175 commit 1847395
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 52 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
node_modules
dist
.env
vscode-profile-*
vscode-profile-*
deeplinkUsers.json
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"@hattip/response": "0.0.45",
"cron": "^3.1.7",
"cross-env": "^7.0.3",
"cryptr": "^6.3.0",
"d3-color": "^3.1.0",
"d3-interpolate": "^3.0.1",
"dotenv": "^16.4.5",
Expand Down
7 changes: 7 additions & 0 deletions pnpm-lock.yaml

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

134 changes: 83 additions & 51 deletions src/backend/telegram/bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,21 @@ import { settings } from '../../settings'
import { MenuTemplate, MenuMiddleware } from 'grammy-inline-menu'
import { config } from '../config'
import { IMode, ISettings } from 'src/typings'
import { allowedTelegramUsers } from '.'
import { InlineKeyboardButton, Message } from 'grammy/types'
import { TextBody } from 'grammy-inline-menu/dist/source/body'
import { dynamic } from '../shared'
import { getIsWeekend } from '../night/static'
import Cryptr from 'cryptr'
import { addDeeplinkUser, deeplinkUsers } from './deeplink'

const allowedTelegramUsers = new Set([...config.tgAllowedUsers.split(',').map(el => parseInt(el)), deeplinkUsers])
const cryptr = new Cryptr(config.tgAllowedUsers, { encoding: 'base64', saltLength: 1, pbkdf2Iterations: 10 })

const bot = new Bot(config.tgApiKey)

const formatBool = (val: boolean, text: string) => val && text

const menuTemplate = new MenuTemplate<Context>(ctx =>
const menuTemplate = new MenuTemplate<Context>(() =>
[
new Date().toLocaleString('en', { weekday: 'long', hour: 'numeric', minute: '2-digit', hour12: false }),
formatBool(dynamic.isAway, 'away'),
Expand All @@ -33,6 +37,7 @@ const toggleTemplate = (title: string, key: IBooleanSettingsKeys) =>
set: async (ctx, val) => {
settings[key] = val
await ctx.answerCallbackQuery(`${title} ${val ? 'on' : 'off'}`)
await updateKeyboard(ctx.chat!.id)

return true
},
Expand Down Expand Up @@ -60,7 +65,8 @@ menuTemplate.select(
const selectedMode = IMode[settings.mode]
// eslint-disable-next-line no-console
console.log('selected mode:', selectedMode)
await ctx.answerCallbackQuery(`Selected mode: ${selectedMode}`)
await ctx.answerCallbackQuery(settings.mode ? `${selectedMode} mode` : 'off')
await updateKeyboard(ctx.chat!.id)

return true
},
Expand All @@ -73,70 +79,96 @@ menuTemplate.manual({
},
text: 'select color',
})

const menuMiddleware = new MenuMiddleware('/', menuTemplate)
let lastContext: CommandContext<Context> | undefined
let lastMenu: Message.TextMessage | undefined
let lastReact = 0

interface IUserData {
ctx: CommandContext<Context>
menu: Message.TextMessage
}

const userData: Map<number, IUserData> = new Map()

bot.command('start', async ctx => {
if (allowedTelegramUsers.has(ctx.chat.id)) {
await ctx.deleteMessages([ctx.message?.message_id, lastMenu?.message_id].filter(el => el) as number[])
lastContext = ctx
lastMenu = (await menuMiddleware.replyToContext(lastContext!)) as Message.TextMessage
} else {
const now = Date.now()
const diff = now - lastReact
if (diff > 500) {
lastReact = now
await ctx.react(diff > 1000 ? '👎' : '🤬')
let deeplinked = false
if (ctx.match) {
try {
const username = cryptr.decrypt(ctx.match)
if (ctx.from?.username?.slice(0, 15) === username) {
allowedTelegramUsers.add(ctx.from.id)
await addDeeplinkUser(ctx.from.id)
deeplinked = true
}
} catch (error) {
return console.error(error)
}
}

if (deeplinked || allowedTelegramUsers.has(ctx.chat.id)) {
const user = userData.get(ctx.chat.id)
await ctx.deleteMessages([ctx.message?.message_id, user?.menu?.message_id].filter(el => el) as number[])
userData.set(ctx.chat.id, { ctx, menu: (await menuMiddleware.replyToContext(ctx!)) as Message.TextMessage })
} else {
await ctx.react('👎')
}
})

bot.use(async (ctx: Context, next: NextFunction) => {
const authorized = allowedTelegramUsers.has(ctx.chat!.id)
if (!authorized) {
await ctx.answerCallbackQuery('Unauthorized')
if (!ctx.chat || !allowedTelegramUsers.has(ctx.chat.id)) {
if (ctx.callbackQuery) await ctx.answerCallbackQuery('Unauthorized')
} else {
lastContext = ctx as CommandContext<Context>
lastMenu = ctx.callbackQuery?.message as Message.TextMessage
userData.set(ctx.chat.id, {
ctx: ctx as CommandContext<Context>,
menu: ctx.callbackQuery?.message as Message.TextMessage,
})
await next()
}
}).use(menuMiddleware)
})

export async function updateKeyboard() {
if (!lastContext || !lastMenu) return
bot.use(menuMiddleware)

const keyboard = await menuTemplate.renderKeyboard(lastContext, '/')
try {
await bot.api.editMessageReplyMarkup(lastMenu.chat.id, lastMenu.message_id, {
reply_markup: { inline_keyboard: keyboard as InlineKeyboardButton[][] },
})
} catch (error) {
if (
'description' in (error as any) &&
!(error as GrammyError).description.endsWith('are exactly the same as a current content and reply markup of the message')
)
throw error
bot.hears(/https:\/\/t.me\/(.+)/, async ctx => {
const [, username] = ctx.match
await ctx.reply(`[control lights](https://t.me/${ctx.me.username}?start=${cryptr.encrypt(username.slice(0, 15))})`, { parse_mode: 'MarkdownV2' })
})

export async function updateKeyboard(except?: number) {
for (const [userId, { ctx: lastContext, menu: lastMenu }] of userData.entries()) {
if (userId === except) continue

const keyboard = await menuTemplate.renderKeyboard(lastContext, '/')
try {
await bot.api.editMessageReplyMarkup(lastMenu.chat.id, lastMenu.message_id, {
reply_markup: { inline_keyboard: keyboard as InlineKeyboardButton[][] },
})
} catch (error) {
if (
'description' in (error as any) &&
!(error as GrammyError).description.endsWith('are exactly the same as a current content and reply markup of the message')
)
console.error(error)
}
}
}

export async function updateMessage() {
if (!lastContext || !lastMenu) return

const body = await menuTemplate.renderBody(lastContext, '/')
const text = typeof body === 'string' ? body : (body as TextBody).text
const keyboard = await menuTemplate.renderKeyboard(lastContext, '/')
try {
await bot.api.editMessageText(lastMenu.chat.id, lastMenu.message_id, text, {
reply_markup: { inline_keyboard: keyboard as InlineKeyboardButton[][] },
})
} catch (error) {
if (
'description' in (error as any) &&
!(error as GrammyError).description.endsWith('are exactly the same as a current content and reply markup of the message')
)
throw error
export async function updateMessage(except?: number) {
for (const [userId, { ctx: lastContext, menu: lastMenu }] of userData.entries()) {
if (userId === except) continue

const body = await menuTemplate.renderBody(lastContext, '/')
const text = typeof body === 'string' ? body : (body as TextBody).text
const keyboard = await menuTemplate.renderKeyboard(lastContext, '/')
try {
await bot.api.editMessageText(lastMenu.chat.id, lastMenu.message_id, text, {
reply_markup: { inline_keyboard: keyboard as InlineKeyboardButton[][] },
})
} catch (error) {
if (
'description' in (error as any) &&
!(error as GrammyError).description.endsWith('are exactly the same as a current content and reply markup of the message')
)
console.error(error)
}
}
}

Expand Down
16 changes: 16 additions & 0 deletions src/backend/telegram/deeplink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import fs from 'fs'
import fsp from 'fs/promises'

export const deeplinkUsers = getDeeplinkUsers()

function getDeeplinkUsers(): number[] {
try {
return JSON.parse(fs.readFileSync('./deeplinkUsers.json', 'utf8'))
} catch (error) {
return []
}
}

export function addDeeplinkUser(user: number) {
if (!deeplinkUsers.includes(user)) return fsp.writeFile('./deeplinkUsers.json', JSON.stringify([...deeplinkUsers, user]))
}

0 comments on commit 1847395

Please sign in to comment.