mirror of
https://github.com/cupcakearmy/hagen-control-station.git
synced 2025-09-09 00:20:39 +00:00
initial
This commit is contained in:
21
src/config.ts
Normal file
21
src/config.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import dotenv from 'dotenv'
|
||||
import { Logger } from './logger.js'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
const token = process.env['BOT_TOKEN']
|
||||
if (!token) {
|
||||
Logger.error('No token found')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const password = process.env['HCS_PASSWORD']
|
||||
if (!password) {
|
||||
Logger.error('No password found')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
export const Config = {
|
||||
token,
|
||||
password,
|
||||
}
|
24
src/cron.ts
Normal file
24
src/cron.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import dayjs from 'dayjs'
|
||||
import cron from 'node-cron'
|
||||
import { DB } from './db.js'
|
||||
import { bot } from './index.js'
|
||||
import { getLatests } from './routes/status.js'
|
||||
|
||||
async function fn() {
|
||||
const now = dayjs()
|
||||
for (const user of DB.data?.users ?? []) {
|
||||
const { fed, clean } = getLatests()
|
||||
if (!fed || dayjs(fed.timestamp).isBefore(now.subtract(12, 'hours'))) {
|
||||
await bot.telegram.sendMessage(user.id, '🥜 You have not fed me in the last 12 hours!')
|
||||
}
|
||||
|
||||
if (!clean || dayjs(clean.timestamp).isBefore(now.subtract(7, 'days'))) {
|
||||
await bot.telegram.sendMessage(user.id, '🛁 You have not cleaned me in the last 7 days!')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function init() {
|
||||
cron.schedule('0 22 * * *', fn)
|
||||
// cron.schedule('*/10 * * * * *', fn)
|
||||
}
|
38
src/db.ts
Normal file
38
src/db.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { mkdir } from 'fs/promises'
|
||||
import { Low, JSONFile } from 'lowdb'
|
||||
import { join, resolve } from 'path'
|
||||
|
||||
const dataDir = resolve('./data')
|
||||
|
||||
export type Feeding = {
|
||||
timestamp: number
|
||||
by: string
|
||||
}
|
||||
|
||||
export type Cleaning = {
|
||||
timestamp: number
|
||||
by: string
|
||||
}
|
||||
|
||||
export type User = {
|
||||
username: string
|
||||
id: number
|
||||
}
|
||||
|
||||
export type Data = {
|
||||
feeding: Feeding[]
|
||||
cleaning: Cleaning[]
|
||||
users: User[]
|
||||
}
|
||||
|
||||
export const DB = new Low<Data>(new JSONFile(join(dataDir, 'db.json')))
|
||||
|
||||
export async function init() {
|
||||
await mkdir(dataDir, { recursive: true })
|
||||
await DB.read()
|
||||
DB.data ||= {
|
||||
cleaning: [],
|
||||
feeding: [],
|
||||
users: [],
|
||||
}
|
||||
}
|
1
src/feeding.ts
Normal file
1
src/feeding.ts
Normal file
@@ -0,0 +1 @@
|
||||
import { Telegraf, Markup } from 'telegraf'
|
32
src/index.ts
Normal file
32
src/index.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Context, Telegraf } from 'telegraf'
|
||||
import { Config } from './config.js'
|
||||
import { init as initCron } from './cron.js'
|
||||
import { init as initDB, User } from './db.js'
|
||||
import { Logger } from './logger.js'
|
||||
import { init as initMiddleware } from './middleware.js'
|
||||
import { init as initRoutes } from './routes/index.js'
|
||||
import { Version } from './routes/version.js'
|
||||
|
||||
export interface HCSContext extends Context {
|
||||
user: User
|
||||
authenticated: boolean
|
||||
}
|
||||
|
||||
export const bot = new Telegraf<HCSContext>(Config.token)
|
||||
export type Bot = typeof bot
|
||||
|
||||
// Enable graceful stop
|
||||
process.once('SIGINT', () => bot.stop('SIGINT'))
|
||||
process.once('SIGTERM', () => bot.stop('SIGTERM'))
|
||||
|
||||
async function init() {
|
||||
await initDB()
|
||||
initMiddleware(bot)
|
||||
initRoutes(bot)
|
||||
initCron()
|
||||
|
||||
await bot.launch()
|
||||
Logger.info('Bot started')
|
||||
Logger.info(`Version: ${Version}`)
|
||||
}
|
||||
init()
|
4
src/logger.ts
Normal file
4
src/logger.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import pino from 'pino'
|
||||
import pp from 'pino-pretty'
|
||||
|
||||
export const Logger = pino(pp({ ignore: 'pid,hostname' }))
|
6
src/messages.ts
Normal file
6
src/messages.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const messages = {
|
||||
welcome: 'Welcome to the HCS\nThe Hagen Control Station!',
|
||||
requestAccess: '🗝 Insert the password to access the HCS',
|
||||
invalidPassword: '🤔 Invalid password',
|
||||
gainedAccess: '🥳 You gained access to the HCS',
|
||||
}
|
56
src/middleware.ts
Normal file
56
src/middleware.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import ms from 'ms'
|
||||
import { Bot } from '.'
|
||||
import { Config } from './config.js'
|
||||
import { DB } from './db.js'
|
||||
import { Logger } from './logger.js'
|
||||
import { messages } from './messages.js'
|
||||
|
||||
export function init(bot: Bot) {
|
||||
// Logger
|
||||
bot.use(async (ctx, next) => {
|
||||
const now = Date.now()
|
||||
await next()
|
||||
const elapsed = Date.now() - now
|
||||
Logger.info(`Processed: ${ctx.updateType} (${ms(elapsed)})`)
|
||||
})
|
||||
|
||||
// Auth
|
||||
bot.use(async (ctx, next) => {
|
||||
ctx.authenticated = false
|
||||
const username = ctx.chat && ctx.chat.type === 'private' && ctx.chat.username
|
||||
if (!username) {
|
||||
ctx.reply('No username')
|
||||
return
|
||||
}
|
||||
|
||||
const user = DB.data?.users.find((u) => u.username === username)
|
||||
if (user) {
|
||||
ctx.authenticated = true
|
||||
ctx.user = user
|
||||
} else {
|
||||
const message = (ctx.updateType === 'message' && 'text' in ctx.message! && ctx.message.text) || ''
|
||||
switch (message.toLowerCase().trim()) {
|
||||
case '/start':
|
||||
case '/help':
|
||||
break
|
||||
case Config.password:
|
||||
const user = {
|
||||
username,
|
||||
id: ctx.chat.id,
|
||||
}
|
||||
DB.data?.users.push(user)
|
||||
DB.write()
|
||||
ctx.authenticated = true
|
||||
ctx.user = user
|
||||
ctx.deleteMessage()
|
||||
ctx.reply(messages.gainedAccess)
|
||||
break
|
||||
default:
|
||||
if (ctx.updateType === 'message') ctx.reply(messages.invalidPassword)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
await next()
|
||||
})
|
||||
}
|
22
src/routes/auth.ts
Normal file
22
src/routes/auth.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { Bot } from '..'
|
||||
import { DB } from '../db.js'
|
||||
import { Logger } from '../logger.js'
|
||||
import { messages } from '../messages.js'
|
||||
|
||||
export function init(bot: Bot) {
|
||||
bot.on('my_chat_member', (ctx) => {
|
||||
if (ctx.myChatMember.new_chat_member.status === 'kicked' || ctx.myChatMember.new_chat_member.status === 'left') {
|
||||
Logger.info(`User ${ctx.user.username} left chat`)
|
||||
if (DB.data?.users) {
|
||||
DB.data.users = DB.data.users.filter((u) => u.username !== ctx.user.username)
|
||||
DB.write()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
bot.start((ctx) => {
|
||||
if (!ctx.from.username) throw new Error('No username')
|
||||
ctx.reply(messages.welcome)
|
||||
ctx.reply(messages.requestAccess)
|
||||
})
|
||||
}
|
12
src/routes/index.ts
Normal file
12
src/routes/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Bot } from '..'
|
||||
import { init as initAuth } from './auth.js'
|
||||
import { init as initLog } from './log.js'
|
||||
import { init as initStatus } from './status.js'
|
||||
import { init as initVersion } from './version.js'
|
||||
|
||||
export function init(bot: Bot) {
|
||||
initAuth(bot)
|
||||
initLog(bot)
|
||||
initStatus(bot)
|
||||
initVersion(bot)
|
||||
}
|
47
src/routes/log.ts
Normal file
47
src/routes/log.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Markup } from 'telegraf'
|
||||
import type { Bot } from '..'
|
||||
import { DB } from '../db.js'
|
||||
import { disappearingMessage } from '../utils.js'
|
||||
|
||||
enum LogCommands {
|
||||
Fed = 'log:fed',
|
||||
Clean = 'log:clean',
|
||||
Cancel = 'log:cancel',
|
||||
}
|
||||
|
||||
export async function init(bot: Bot) {
|
||||
bot.command('log', (ctx) => {
|
||||
ctx.deleteMessage()
|
||||
const buttons = Markup.inlineKeyboard([
|
||||
[Markup.button.callback('🥜 Fed', LogCommands.Fed)],
|
||||
[Markup.button.callback('🛁 Cleaned', LogCommands.Clean)],
|
||||
[Markup.button.callback('❌ Cancel', LogCommands.Cancel)],
|
||||
])
|
||||
ctx.replyWithMarkdownV2('What do you want to log?', { reply_markup: buttons.reply_markup })
|
||||
})
|
||||
|
||||
bot.action(LogCommands.Clean, (ctx) => {
|
||||
ctx.deleteMessage()
|
||||
DB.data?.cleaning.push({
|
||||
timestamp: Date.now(),
|
||||
by: ctx.user.username,
|
||||
})
|
||||
DB.write()
|
||||
disappearingMessage(ctx, 'Saved')
|
||||
})
|
||||
|
||||
bot.action(LogCommands.Fed, (ctx) => {
|
||||
ctx.deleteMessage()
|
||||
DB.data?.feeding.push({
|
||||
timestamp: Date.now(),
|
||||
by: ctx.user.username,
|
||||
})
|
||||
DB.write()
|
||||
disappearingMessage(ctx, 'Saved')
|
||||
})
|
||||
|
||||
bot.action(LogCommands.Cancel, (ctx) => {
|
||||
ctx.deleteMessage()
|
||||
disappearingMessage(ctx, 'Cancelled')
|
||||
})
|
||||
}
|
22
src/routes/status.ts
Normal file
22
src/routes/status.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { DB } from '../db.js'
|
||||
import { Bot } from '../index.js'
|
||||
import { format, getLatest } from '../utils.js'
|
||||
|
||||
export function getLatests() {
|
||||
const fed = getLatest(DB.data?.feeding || [])
|
||||
const clean = getLatest(DB.data?.cleaning || [])
|
||||
return { fed, clean }
|
||||
}
|
||||
|
||||
export function init(bot: Bot) {
|
||||
bot.command('status', (ctx) => {
|
||||
ctx.deleteMessage()
|
||||
const { fed, clean } = getLatests()
|
||||
let msg = ''
|
||||
msg += '**🥜 Fed**\n'
|
||||
msg += fed ? `${format(fed.timestamp)} by ${fed.by}` : '🥲 Never'
|
||||
msg += '\n\n**🛁 Cleaned**\n'
|
||||
msg += clean ? `${format(clean.timestamp)} by ${clean.by}` : '🥲 Never'
|
||||
ctx.replyWithMarkdownV2(msg)
|
||||
})
|
||||
}
|
10
src/routes/version.ts
Normal file
10
src/routes/version.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Bot } from '..'
|
||||
|
||||
export const Version = process.env['npm_package_version']
|
||||
|
||||
export function init(bot: Bot) {
|
||||
bot.command('/version', async (ctx) => {
|
||||
// ctx.reply('Version: ' + version)
|
||||
ctx.reply(`Version: ${Version}`)
|
||||
})
|
||||
}
|
19
src/utils.ts
Normal file
19
src/utils.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import dayjs from 'dayjs'
|
||||
import type { Cleaning, Feeding } from './db.js'
|
||||
import { HCSContext } from './index.js'
|
||||
|
||||
export function format(timestamp: number): string {
|
||||
const d = dayjs(timestamp)
|
||||
return d.format('DD MMM') + ' at ' + d.format('HH:mm')
|
||||
}
|
||||
|
||||
export function getLatest<T extends Cleaning | Feeding>(data: T[]): T | null {
|
||||
return data.sort((a, b) => b.timestamp - a.timestamp)[0] || null
|
||||
}
|
||||
|
||||
export async function disappearingMessage(ctx: HCSContext, msg: string, timeout = 2000) {
|
||||
const message = await ctx.reply(msg)
|
||||
setTimeout(() => {
|
||||
ctx.deleteMessage(message.message_id)
|
||||
}, timeout)
|
||||
}
|
Reference in New Issue
Block a user