Skip to content

Commit

Permalink
Merge pull request #3 from bit0r1n/classrooms-search
Browse files Browse the repository at this point in the history
Classrooms: free classrooms search
  • Loading branch information
bit0r1n authored Feb 6, 2025
2 parents cdc8182 + 7faae49 commit 79b22cb
Show file tree
Hide file tree
Showing 23 changed files with 569 additions and 60 deletions.
274 changes: 274 additions & 0 deletions bot/src/commands/actions/schedule/classrooms/classroomSchedule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
import { Composer, Markup } from 'telegraf'
import {
callbackIdBuild,
ClassroomScheduleType,
getDayBounds,
IDayBounds,
inlineKeyboards,
SuperDuperUpgradedContext
} from '../../../../utils'
import { Classroom, ClassroomLocation, classroomLocationToHuman, Keeper, Lesson, LessonTime, lessonTimeToHuman } from '../../../../keeper'

export const classroomScheduleHandler = new Composer<SuperDuperUpgradedContext>()

const keeper = new Keeper(process.env.KEEPER_URL!)

export enum ClassroomLocationSearch {
InBuilding,
OldBuilding,
NewBuilding,
Dormitory,
Everywhere
}

const classroomLocationCarousel = [
ClassroomLocationSearch.InBuilding,
ClassroomLocationSearch.OldBuilding,
ClassroomLocationSearch.NewBuilding,
ClassroomLocationSearch.Dormitory,
ClassroomLocationSearch.Everywhere
]

const lessonTimeCarousel = [
LessonTime.First,
LessonTime.Second,
LessonTime.Third,
LessonTime.Fourth,
LessonTime.Fifth,
LessonTime.Sixth,
LessonTime.Seventh,
LessonTime.Eighth
]

const searchLocationToKeeper = (location: ClassroomLocationSearch): ClassroomLocation[] => {
switch (location) {
case ClassroomLocationSearch.InBuilding:
return [ ClassroomLocation.NewBuilding, ClassroomLocation.OldBuilding ]
case ClassroomLocationSearch.OldBuilding:
return [ ClassroomLocation.OldBuilding ]
case ClassroomLocationSearch.NewBuilding:
return [ ClassroomLocation.OldBuilding ]
case ClassroomLocationSearch.Dormitory:
return [ ClassroomLocation.Dormitory ]
case ClassroomLocationSearch.Everywhere:
return [ ClassroomLocation.NewBuilding, ClassroomLocation.OldBuilding, ClassroomLocation.NewBuilding ]
}
}

const classroomLocationToString = (location: ClassroomLocationSearch) => {
switch (location) {
case ClassroomLocationSearch.InBuilding:
return 'В университете'
case ClassroomLocationSearch.OldBuilding:
return 'Старый корпус'
case ClassroomLocationSearch.NewBuilding:
return 'Новый корпус'
case ClassroomLocationSearch.Dormitory:
return 'Общежитие'
case ClassroomLocationSearch.Everywhere:
return 'Везде'
}
}

function filterUnusedClassrooms(classrooms: Classroom[], lessons: Lesson[]): Classroom[] {
const usedClassrooms = new Set(lessons.flatMap(lesson => lesson.classrooms))
return classrooms.filter(classroom => !usedClassrooms.has(classroom.name))
}

function setRange(bounds: IDayBounds, range: string): IDayBounds {
const match = range.match(/^(\d{2}):(\d{2}) - (\d{2}):(\d{2})$/)
if (!match) {
throw new Error('Invalid range')
}

const [ , startHour, startMinute, endHour, endMinute ] = match.map(Number)

bounds.start.setHours(startHour - 3, startMinute, 0, 0)
bounds.end.setHours(endHour - 3, endMinute, 0, 0)

return bounds
}

function groupClassrooms(classrooms: Classroom[]): Record<ClassroomLocation, Classroom[]> {
return classrooms.reduce((acc, classroom) => {
if (!acc[classroom.location]) {
acc[classroom.location] = []
}

acc[classroom.location].push(classroom)

acc[classroom.location].sort((a, b) => a.floor - b.floor)

return acc
}, {} as Record<ClassroomLocation, Classroom[]>)
}

async function updateFreeSearchMessage(ctx: SuperDuperUpgradedContext) {
if (!ctx.session?.classroomSearch) return

const computerEmoji = ctx.session.classroomSearch.onlyComputer ? '✅' : '❌'

await ctx.editMessageText('🥂 Выбери где и к скольки ты хочешь увидеть свободные на сегодня аудитории', {
reply_markup: Markup.inlineKeyboard([
[
Markup.button.callback('<',
callbackIdBuild('classroom_schedule', [
ClassroomScheduleType.Free,
'type',
'prev'
])
),
Markup.button.callback(classroomLocationToString(classroomLocationCarousel[ctx.session?.classroomSearch.locationIndex]),
callbackIdBuild('classroom_schedule', [
ClassroomScheduleType.Free,
'type',
'current'
])
),
Markup.button.callback('>',
callbackIdBuild('classroom_schedule', [
ClassroomScheduleType.Free,
'type',
'next'
])
)
],
[
Markup.button.callback('<',
callbackIdBuild('classroom_schedule', [
ClassroomScheduleType.Free,
'time',
'prev'
])
),
Markup.button.callback(lessonTimeToHuman(lessonTimeCarousel[ctx.session.classroomSearch.timeIndex]),
callbackIdBuild('classroom_schedule', [
ClassroomScheduleType.Free,
'time',
'current'
])
),
Markup.button.callback('>',
callbackIdBuild('classroom_schedule', [
ClassroomScheduleType.Free,
'time',
'next'
])
)
],
[
Markup.button.callback(computerEmoji + ' Компьютерная аудитория',
callbackIdBuild('classroom_schedule', [
ClassroomScheduleType.Free,
'only_computer'
])
)
],
[
Markup.button.callback('Искать',
callbackIdBuild('classroom_schedule', [
ClassroomScheduleType.Free,
'confirm'
])
)
]
]).reply_markup
})
}

classroomScheduleHandler.action('classroom_schedule', async (ctx) => {
ctx.session = { classroomSearch: { locationIndex: 0, timeIndex: 0, onlyComputer: false } }
await ctx.editMessageText('🥏 Выбери что тебе нужно найти', {
reply_markup: inlineKeyboards.classroomScheduleType.reply_markup
})
})

classroomScheduleHandler.action(callbackIdBuild('classroom_schedule', [ ClassroomScheduleType.Free ]), async (ctx) => {
await updateFreeSearchMessage(ctx)
})

classroomScheduleHandler.action(
[
callbackIdBuild('classroom_schedule', [ ClassroomScheduleType.Free, 'type', 'next' ]),
callbackIdBuild('classroom_schedule', [ ClassroomScheduleType.Free, 'type', 'prev' ]),
callbackIdBuild('classroom_schedule', [ ClassroomScheduleType.Free, 'type', 'current' ])
], async (ctx) => {
if (ctx.match.input.includes('current')) return
if (!ctx.session?.classroomSearch) {
return await ctx.editMessageText('🤥')
}

ctx.session.classroomSearch.locationIndex =
(ctx.match.input.includes('next')
? (ctx.session.classroomSearch.locationIndex + 1)
: (ctx.session.classroomSearch.locationIndex - 1 + classroomLocationCarousel.length))
% classroomLocationCarousel.length

await updateFreeSearchMessage(ctx)
})

classroomScheduleHandler.action(
[
callbackIdBuild('classroom_schedule', [ ClassroomScheduleType.Free, 'time', 'next' ]),
callbackIdBuild('classroom_schedule', [ ClassroomScheduleType.Free, 'time', 'prev' ]),
callbackIdBuild('classroom_schedule', [ ClassroomScheduleType.Free, 'time', 'current' ])
], async (ctx) => {
if (ctx.match.input.includes('current')) return
if (!ctx.session?.classroomSearch) {
return await ctx.editMessageText('🤥')
}

ctx.session.classroomSearch.timeIndex =
(ctx.match.input.includes('next')
? (ctx.session.classroomSearch.timeIndex + 1)
: (ctx.session.classroomSearch.timeIndex - 1 + lessonTimeCarousel.length))
% lessonTimeCarousel.length

await updateFreeSearchMessage(ctx)
})

classroomScheduleHandler.action(callbackIdBuild('classroom_schedule', [ ClassroomScheduleType.Free, 'only_computer' ]), async (ctx) => {
if (!ctx.session?.classroomSearch) {
return await ctx.editMessageText('🤥')
}

ctx.session.classroomSearch.onlyComputer = !ctx.session.classroomSearch.onlyComputer

await updateFreeSearchMessage(ctx)
})

classroomScheduleHandler.action(callbackIdBuild('classroom_schedule', [ ClassroomScheduleType.Free, 'confirm' ]), async (ctx) => {
if (!ctx.session?.classroomSearch) {
return await ctx.editMessageText('🤥')
}

const { locationIndex, timeIndex, onlyComputer } = ctx.session.classroomSearch

const locationSearch = searchLocationToKeeper(classroomLocationCarousel[locationIndex])
const timeSearch = lessonTimeCarousel[timeIndex]
const todayBounds = getDayBounds()
setRange(todayBounds, lessonTimeToHuman(timeSearch))

const classrooms = await keeper.getClassrooms({ location: locationSearch, is_computer: onlyComputer })
const lessons = await keeper.getLessons({ from: todayBounds.start, before: todayBounds.end })

const unused = filterUnusedClassrooms(classrooms, lessons)

if (!unused.length) {
return await ctx.editMessageText('🚗 Где все')
}

const groupedUnused = groupClassrooms(unused)
let unusedInfo = ''

for (const [ location, classrooms ] of Object.entries(groupedUnused)) {
unusedInfo
+= '\t⛳ ' + classroomLocationToHuman(parseInt(location))
+ '\n'
+ '\t\t' + classrooms.map(c => c.name).join(', ')
+ '\n\n'
}

await ctx.editMessageText(`🪙 Свободные на сегодня (${lessonTimeToHuman(lessonTimeCarousel[timeIndex])}) аудитории:\n\n`
+ unusedInfo)
})
8 changes: 8 additions & 0 deletions bot/src/commands/actions/schedule/classrooms/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Composer } from 'telegraf'
import { SuperDuperUpgradedContext } from '../../../../utils'
import { classroomScheduleHandler } from './classroomSchedule'

export const classroomScheduleMasterHandler = new Composer<SuperDuperUpgradedContext>()

classroomScheduleMasterHandler
.use(classroomScheduleHandler)
2 changes: 2 additions & 0 deletions bot/src/commands/actions/schedule/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './classrooms'
export * from './weeks'
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import {
replyKeyboards,
SuperDuperUpgradedContext,
WeeksArchiveType
} from '../../../utils'
import { Group, Parser } from '../../../parser'
import { getWeekStart, Keeper, lessonsToMessage, weekToHuman } from '../../../keeper'
import { UserState } from '../../../schemas/User'
} from '../../../../utils'
import { Group, Parser } from '../../../../parser'
import { getWeekStart, Keeper, lessonsToMessage, weekToHuman } from '../../../../keeper'
import { UserState } from '../../../../schemas/User'

export const groupWeekHandler = new Composer<SuperDuperUpgradedContext>()

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Composer } from 'telegraf'
import { SuperDuperUpgradedContext } from '../../../utils'
import { SuperDuperUpgradedContext } from '../../../../utils'
import { groupWeekHandler } from './groupWeek'
import { selfWeekHandler } from './selfWeek'
import { teacherWeekHandler } from './teacherWeek'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Composer } from 'telegraf'
import { callbackIdParse, CallbackIdSplitter, SuperDuperUpgradedContext, WeeksArchiveType } from '../../../utils'
import { Parser } from '../../../parser'
import { Keeper, lessonsToMessage } from '../../../keeper'
import { UserRole } from '../../../schemas/User'
import { callbackIdParse, CallbackIdSplitter, SuperDuperUpgradedContext, WeeksArchiveType } from '../../../../utils'
import { Parser } from '../../../../parser'
import { Keeper, lessonsToMessage } from '../../../../keeper'
import { UserRole } from '../../../../schemas/User'

export const selfWeekHandler = new Composer<SuperDuperUpgradedContext>()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import {
replyKeyboards,
SuperDuperUpgradedContext,
WeeksArchiveType
} from '../../../utils'
import { Parser } from '../../../parser'
import { getWeekStart, Keeper, lessonsToMessage, weekToHuman } from '../../../keeper'
import { UserState } from '../../../schemas/User'
} from '../../../../utils'
import { Parser } from '../../../../parser'
import { getWeekStart, Keeper, lessonsToMessage, weekToHuman } from '../../../../keeper'
import { UserState } from '../../../../schemas/User'

export const teacherWeekHandler = new Composer<SuperDuperUpgradedContext>()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import {
SuperDuperUpgradedContext,
WeeksArchiveAction,
WeeksArchiveType
} from '../../../utils'
} from '../../../../utils'
import { Composer, Markup } from 'telegraf'
import { getWeekStart, Keeper, lessonsToMessage, weekToHuman } from '../../../keeper'
import { getWeekStart, Keeper, lessonsToMessage, weekToHuman } from '../../../../keeper'
import { InlineKeyboardButton } from 'telegraf/types'
import { Parser } from '../../../parser'
import { Parser } from '../../../../parser'

export const weeksArchiveHandler = new Composer<SuperDuperUpgradedContext>()

Expand Down
15 changes: 13 additions & 2 deletions bot/src/commands/chatHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,14 @@ chatHandler.on(message('text'), async (ctx) => {
}

if (ctx.user.state === UserState.AskingWeekGroup) {
if (ctx.message.text.length < 2) {
return await ctx.reply('😨 Давай конкретнее, слишком маленький запрос')
}

const groups = await parser.getGroups({ display: ctx.message.text })

if (!groups.length) {
await ctx.reply('🥺 Такой группы не нашлось. Попробуй другой номер группы')
return
return await ctx.reply('🥺 Такой группы не нашлось. Попробуй другой номер группы')
}

ctx.user.state = UserState.MainMenu
Expand All @@ -42,6 +45,10 @@ chatHandler.on(message('text'), async (ctx) => {
}

if (ctx.user.state === UserState.AskingWeekTeacher) {
if (ctx.message.text.length < 3) {
return await ctx.reply('😨 Давай конкретнее, слишком маленький запрос')
}

const teachers: string[] = await keeper.getTeachers({ name: ctx.message.text })

if (!teachers.length) {
Expand All @@ -65,6 +72,10 @@ chatHandler.on(message('text'), async (ctx) => {
}

if (ctx.user.state === UserState.AskingFollowingEntity) {
if (ctx.message.text.length < 2) {
return await ctx.reply('😨 Давай конкретнее, слишком маленький запрос')
}

const isStudent = ctx.user.role !== UserRole.Teacher
if (isStudent) {
const groups = await parser.getGroups({ display: ctx.message.text })
Expand Down
Loading

0 comments on commit 79b22cb

Please sign in to comment.