Skip to content

Commit

Permalink
feat(bot): add initial migration, schemas and business logic
Browse files Browse the repository at this point in the history
  • Loading branch information
evermake committed Mar 10, 2024
1 parent 2af9dab commit 96cfb8e
Show file tree
Hide file tree
Showing 14 changed files with 167 additions and 2 deletions.
2 changes: 2 additions & 0 deletions backend/src/bot/context.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type { Context } from 'grammy'
import type { LoggingFlavor } from './plugins/logging'
import type { TranslationsFlavor } from './plugins/translations'
import type { DomainFlavor } from './plugins/domain'

export type Ctx =
& Context
& LoggingFlavor
& DomainFlavor
& TranslationsFlavor
8 changes: 8 additions & 0 deletions backend/src/bot/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,27 @@ import { Bot } from 'grammy'
import plugins from './plugins'
import type { Ctx } from './context'
import type { Logger } from '~/lib/logging'
import type { Domain } from '~/domain'

export function createBot({
logger,
token,
domain,
}: {
logger: Logger
token: string
domain: Domain
}): Bot<Ctx> {
const bot = new Bot<Ctx>(token)

plugins.logging.install(bot, { logger })
plugins.floodControl.install(bot)
plugins.domain.install(bot, { domain })
plugins.translations.install(bot)

bot.command('start', async (ctx) => {
ctx.reply(`${ctx.t.Welcome}\n\n${JSON.stringify(ctx.user)}`)
})

return bot
}
28 changes: 28 additions & 0 deletions backend/src/bot/plugins/domain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { InstallFn } from '.'
import type { User } from '~/domain/schemas/user'
import type { Domain } from '~/domain'

export type DomainOptions = {
domain: Domain
}

export type DomainFlavor = {
domain: Domain
user?: User
}

export const install: InstallFn<DomainFlavor, DomainOptions> = (bot, { domain }) => {
bot.use(async (ctx, next) => {
ctx.domain = domain
if (ctx.from) {
ctx.user = await domain.upsertUser({
telegramId: ctx.from.id,
username: ctx.from.username,
firstName: ctx.from.first_name,
lastName: ctx.from.last_name,
notificationPreferences: { classes: [] },
})
}
return next()
})
}
6 changes: 4 additions & 2 deletions backend/src/bot/plugins/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { Bot, Context } from 'grammy'
import * as logging from './logging'
import * as translations from './translations'
import * as floodControl from './flood-control'
import * as domain from './domain'
import * as translations from './translations'

// eslint-disable-next-line ts/ban-types
export type InstallFn<F = {}, O = undefined> =
Expand All @@ -11,6 +12,7 @@ export type InstallFn<F = {}, O = undefined> =

export default {
logging,
translations,
floodControl,
domain,
translations,
}
43 changes: 43 additions & 0 deletions backend/src/domain/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { User } from './schemas/user'
import type { Logger } from '~/lib/logging'
import { type Database, Prisma } from '~/services/database'
import type { Sport } from '~/services/sport'

export class Domain {
private logger: Logger
private db: Database
private sport: Sport

constructor(options: {
logger: Logger
db: Database
sport: Sport
}) {
this.logger = options.logger
this.db = options.db
this.sport = options.sport
}

public async upsertUser({
telegramId,
...data
}: Omit<Partial<User> & Pick<User, 'telegramId' | 'firstName' | 'notificationPreferences'>, 'createdAt'>): Promise<User> {
const user = await this.db.user.upsert({
where: { telegramId },
create: { telegramId, ...data },
update: { ...data },
})
return User.parse(user)
}

public async updateUser({
telegramId,
...patch
}: Partial<User> & Pick<User, 'telegramId'>): Promise<User> {
const user = await this.db.user.update({
where: { telegramId },
data: patch,
})
return User.parse(user)
}
}
4 changes: 4 additions & 0 deletions backend/src/domain/schemas/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { z } from 'zod'

export const Language = z.string().regex(/^[a-z]{2}$/)
export type Language = z.infer<typeof Language>
1 change: 1 addition & 0 deletions backend/src/domain/schemas/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './notifications'
28 changes: 28 additions & 0 deletions backend/src/domain/schemas/notifications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { z } from 'zod'

export const ClassesNotificationPreferenceSpecificTime = z.object({
type: z.literal('specific-time'),
daysBefore: z.number().int(),
time: z.object({
hours: z.number().int(),
minutes: z.number().int(),
}),
})
export type ClassesNotificationPreferenceSpecificTime = z.infer<typeof ClassesNotificationPreferenceSpecificTime>

export const ClassesNotificationPreferenceRelative = z.object({
type: z.literal('relative'),
minutesBefore: z.number().int(),
})
export type ClassesNotificationPreferenceRelative = z.infer<typeof ClassesNotificationPreferenceRelative>

export const ClassesNotificationPreference = z.union([
ClassesNotificationPreferenceSpecificTime,
ClassesNotificationPreferenceRelative,
])
export type ClassesNotificationPreference = z.infer<typeof ClassesNotificationPreference>

export const NotificationPreferences = z.object({
classes: z.array(ClassesNotificationPreference),
})
export type NotificationPreferences = z.infer<typeof NotificationPreferences>
14 changes: 14 additions & 0 deletions backend/src/domain/schemas/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { z } from 'zod'
import { Language } from './common'
import { NotificationPreferences } from './notifications'

export const User = z.object({
telegramId: z.coerce.number().int(),
createdAt: z.date(),
username: z.string().nullable(),
firstName: z.string(),
lastName: z.string().nullable(),
language: Language.nullable(),
notificationPreferences: NotificationPreferences,
})
export type User = z.infer<typeof User>
11 changes: 11 additions & 0 deletions backend/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { loadConfigFromEnv } from './config'
import { createLogger } from './lib/logging'
import { createBot } from './bot'
import { Domain } from './domain'
import { createDatabase } from './services/database'
import { Sport } from './services/sport'

async function main() {
const config = loadConfigFromEnv()
Expand All @@ -13,6 +16,14 @@ async function main() {
const bot = createBot({
logger: logger.child({ tag: 'bot' }),
token: config.bot.token,
domain: new Domain({
logger: logger.child({ tag: 'domain' }),
db: await createDatabase({
logger: logger.child({ tag: 'database' }),
connectionUrl: config.postgresUrl,
}),
sport: new Sport(),
}),
})

await bot.start()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-- CreateTable
CREATE TABLE "User" (
"telegramId" BIGINT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"username" TEXT,
"firstName" TEXT NOT NULL,
"lastName" TEXT,
"language" TEXT,
"notificationPreferences" JSONB NOT NULL,

CONSTRAINT "User_pkey" PRIMARY KEY ("telegramId")
);
3 changes: 3 additions & 0 deletions backend/src/services/database/migrations/migration_lock.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"
6 changes: 6 additions & 0 deletions backend/src/services/database/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,10 @@ model User {
username String?
firstName String
lastName String?
// Interface language user prefers as ISO 639-1 language code
// or null if user did not specify it.
language String?
notificationPreferences Json
}
3 changes: 3 additions & 0 deletions backend/src/services/sport/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export class Sport {

}

0 comments on commit 96cfb8e

Please sign in to comment.