mirror of
				https://github.com/dancojocaru2000/CfrTrainInfoTelegramBot.git
				synced 2025-11-04 00:46:37 +02:00 
			
		
		
		
	Initial commit
This commit is contained in:
		
						commit
						b44f1c92d2
					
				
					 16 changed files with 1023 additions and 0 deletions
				
			
		
							
								
								
									
										26
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,26 @@
 | 
				
			||||||
 | 
					debug
 | 
				
			||||||
 | 
					bot_db.sqlite
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Go Template from https://github.com/github/gitignore/blob/main/Go.gitignore
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# If you prefer the allow list template instead of the deny list, see community template:
 | 
				
			||||||
 | 
					# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Binaries for programs and plugins
 | 
				
			||||||
 | 
					*.exe
 | 
				
			||||||
 | 
					*.exe~
 | 
				
			||||||
 | 
					*.dll
 | 
				
			||||||
 | 
					*.so
 | 
				
			||||||
 | 
					*.dylib
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Test binary, built with `go test -c`
 | 
				
			||||||
 | 
					*.test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Output of the go coverage tool, specifically when used with LiteIDE
 | 
				
			||||||
 | 
					*.out
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Dependency directories (remove the comment below to include it)
 | 
				
			||||||
 | 
					# vendor/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Go workspace file
 | 
				
			||||||
 | 
					go.work
 | 
				
			||||||
							
								
								
									
										8
									
								
								.idea/.gitignore
									
										
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.idea/.gitignore
									
										
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,8 @@
 | 
				
			||||||
 | 
					# Default ignored files
 | 
				
			||||||
 | 
					/shelf/
 | 
				
			||||||
 | 
					/workspace.xml
 | 
				
			||||||
 | 
					# Editor-based HTTP Client requests
 | 
				
			||||||
 | 
					/httpRequests/
 | 
				
			||||||
 | 
					# Datasource local storage ignored files
 | 
				
			||||||
 | 
					/dataSources/
 | 
				
			||||||
 | 
					/dataSources.local.xml
 | 
				
			||||||
							
								
								
									
										9
									
								
								.idea/CfrTrainInfoTelegramBot.iml
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								.idea/CfrTrainInfoTelegramBot.iml
									
										
									
										generated
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,9 @@
 | 
				
			||||||
 | 
					<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
 | 
					<module type="WEB_MODULE" version="4">
 | 
				
			||||||
 | 
					  <component name="Go" enabled="true" />
 | 
				
			||||||
 | 
					  <component name="NewModuleRootManager">
 | 
				
			||||||
 | 
					    <content url="file://$MODULE_DIR$" />
 | 
				
			||||||
 | 
					    <orderEntry type="inheritedJdk" />
 | 
				
			||||||
 | 
					    <orderEntry type="sourceFolder" forTests="false" />
 | 
				
			||||||
 | 
					  </component>
 | 
				
			||||||
 | 
					</module>
 | 
				
			||||||
							
								
								
									
										8
									
								
								.idea/modules.xml
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.idea/modules.xml
									
										
									
										generated
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,8 @@
 | 
				
			||||||
 | 
					<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
 | 
					<project version="4">
 | 
				
			||||||
 | 
					  <component name="ProjectModuleManager">
 | 
				
			||||||
 | 
					    <modules>
 | 
				
			||||||
 | 
					      <module fileurl="file://$PROJECT_DIR$/.idea/CfrTrainInfoTelegramBot.iml" filepath="$PROJECT_DIR$/.idea/CfrTrainInfoTelegramBot.iml" />
 | 
				
			||||||
 | 
					    </modules>
 | 
				
			||||||
 | 
					  </component>
 | 
				
			||||||
 | 
					</project>
 | 
				
			||||||
							
								
								
									
										15
									
								
								.idea/vcs.xml
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								.idea/vcs.xml
									
										
									
										generated
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,15 @@
 | 
				
			||||||
 | 
					<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
 | 
					<project version="4">
 | 
				
			||||||
 | 
					  <component name="CommitMessageInspectionProfile">
 | 
				
			||||||
 | 
					    <profile version="1.0">
 | 
				
			||||||
 | 
					      <inspection_tool class="BodyLimit" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
 | 
				
			||||||
 | 
					      <inspection_tool class="SubjectBodySeparation" enabled="true" level="WARNING" enabled_by_default="true" />
 | 
				
			||||||
 | 
					      <inspection_tool class="SubjectLimit" enabled="true" level="WARNING" enabled_by_default="true">
 | 
				
			||||||
 | 
					        <option name="RIGHT_MARGIN" value="50" />
 | 
				
			||||||
 | 
					      </inspection_tool>
 | 
				
			||||||
 | 
					    </profile>
 | 
				
			||||||
 | 
					  </component>
 | 
				
			||||||
 | 
					  <component name="VcsDirectoryMappings">
 | 
				
			||||||
 | 
					    <mapping directory="$PROJECT_DIR$" vcs="Git" />
 | 
				
			||||||
 | 
					  </component>
 | 
				
			||||||
 | 
					</project>
 | 
				
			||||||
							
								
								
									
										15
									
								
								go.mod
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								go.mod
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,15 @@
 | 
				
			||||||
 | 
					module dcdev.ro/CfrTrainInfoTelegramBot
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					go 1.20
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					require (
 | 
				
			||||||
 | 
						github.com/go-telegram/bot v0.7.15
 | 
				
			||||||
 | 
						gorm.io/driver/sqlite v1.5.3
 | 
				
			||||||
 | 
						gorm.io/gorm v1.25.4
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					require (
 | 
				
			||||||
 | 
						github.com/jinzhu/inflection v1.0.0 // indirect
 | 
				
			||||||
 | 
						github.com/jinzhu/now v1.1.5 // indirect
 | 
				
			||||||
 | 
						github.com/mattn/go-sqlite3 v1.14.17 // indirect
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
							
								
								
									
										12
									
								
								go.sum
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								go.sum
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,12 @@
 | 
				
			||||||
 | 
					github.com/go-telegram/bot v0.7.15 h1:Xi1PGEUjcJvZ4qG0EssFPUkcxlDbEIx1VWStMeG6GvE=
 | 
				
			||||||
 | 
					github.com/go-telegram/bot v0.7.15/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM=
 | 
				
			||||||
 | 
					github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
 | 
				
			||||||
 | 
					github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
 | 
				
			||||||
 | 
					github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
 | 
				
			||||||
 | 
					github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
 | 
				
			||||||
 | 
					github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
 | 
				
			||||||
 | 
					github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
 | 
				
			||||||
 | 
					gorm.io/driver/sqlite v1.5.3 h1:7/0dUgX28KAcopdfbRWWl68Rflh6osa4rDh+m51KL2g=
 | 
				
			||||||
 | 
					gorm.io/driver/sqlite v1.5.3/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
 | 
				
			||||||
 | 
					gorm.io/gorm v1.25.4 h1:iyNd8fNAe8W9dvtlgeRI5zSVZPsq3OpcTu37cYcpCmw=
 | 
				
			||||||
 | 
					gorm.io/gorm v1.25.4/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
 | 
				
			||||||
							
								
								
									
										349
									
								
								main.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										349
									
								
								main.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,349 @@
 | 
				
			||||||
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"os/signal"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"syscall"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"dcdev.ro/CfrTrainInfoTelegramBot/pkg/database"
 | 
				
			||||||
 | 
						"dcdev.ro/CfrTrainInfoTelegramBot/pkg/handlers"
 | 
				
			||||||
 | 
						"dcdev.ro/CfrTrainInfoTelegramBot/pkg/subscriptions"
 | 
				
			||||||
 | 
						"dcdev.ro/CfrTrainInfoTelegramBot/pkg/utils"
 | 
				
			||||||
 | 
						tgBot "github.com/go-telegram/bot"
 | 
				
			||||||
 | 
						"github.com/go-telegram/bot/models"
 | 
				
			||||||
 | 
						"gorm.io/driver/sqlite"
 | 
				
			||||||
 | 
						"gorm.io/gorm"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						trainInfoCommand   = "/train_info"
 | 
				
			||||||
 | 
						stationInfoCommand = "/station_info"
 | 
				
			||||||
 | 
						routeCommand       = "/route"
 | 
				
			||||||
 | 
						cancelCommand      = "/cancel"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						initialMessage = `Hello. 😄
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					You can send the following commands:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					` + trainInfoCommand + ` - Find information about a certain train.
 | 
				
			||||||
 | 
					` + stationInfoCommand + ` - Find departures or arrivals at a certain station.
 | 
				
			||||||
 | 
					` + routeCommand + ` - Find trains for a certain route.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					You may use ` + cancelCommand + ` to cancel any ongoing command.`
 | 
				
			||||||
 | 
						waitingForTrainNumberMessage = "Please send the number of the train you want information for."
 | 
				
			||||||
 | 
						pleaseWaitMessage            = "Please wait..."
 | 
				
			||||||
 | 
						cancelResponseMessage        = "Command cancelled."
 | 
				
			||||||
 | 
						chooseDateMessage            = `Please choose the date of departure from the first station for this train.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					You may also send the date as a message in the following formats: dd.mm.yyyy, m/d/yyyy, yyyy-mm-dd, UNIX timestamp.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Keep in mind that, for night trains, this date might be yesterday.`
 | 
				
			||||||
 | 
						invalidDateMessage = "Invalid date. Please try again or us " + cancelCommand + " to cancel."
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func main() {
 | 
				
			||||||
 | 
						ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
 | 
				
			||||||
 | 
						defer cancel()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						log.SetOutput(os.Stderr)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						botToken := os.Getenv("CFR_BOT.TOKEN")
 | 
				
			||||||
 | 
						if len(botToken) == 0 {
 | 
				
			||||||
 | 
							log.Fatal("ERROR: No bot token supplied; supply with CFR_BOT.TOKEN")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						db, err := gorm.Open(sqlite.Open("bot_db.sqlite"), &gorm.Config{})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							panic(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := db.AutoMigrate(&handlers.ChatFlow{}); err != nil {
 | 
				
			||||||
 | 
							panic(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := db.AutoMigrate(&subscriptions.SubData{}); err != nil {
 | 
				
			||||||
 | 
							panic(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						database.SetDatabase(db)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						subs, err := subscriptions.LoadSubscriptions()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							subs = nil
 | 
				
			||||||
 | 
							fmt.Printf("WARN : Could not load subscriptions: %s\n", err.Error())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						go subs.CheckSubscriptions(ctx)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						bot, err := tgBot.New(botToken, tgBot.WithDefaultHandler(handlerBuilder(subs)))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							panic(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						log.Print("INFO : Starting...")
 | 
				
			||||||
 | 
						bot.Start(ctx)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func handlerBuilder(subs *subscriptions.Subscriptions) func(context.Context, *tgBot.Bot, *models.Update) {
 | 
				
			||||||
 | 
						return func(ctx context.Context, b *tgBot.Bot, update *models.Update) {
 | 
				
			||||||
 | 
							handler(ctx, b, update, subs)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func handler(ctx context.Context, b *tgBot.Bot, update *models.Update, subs *subscriptions.Subscriptions) {
 | 
				
			||||||
 | 
						var response *handlers.HandlerResponse
 | 
				
			||||||
 | 
						var toEditId int
 | 
				
			||||||
 | 
						defer func() {
 | 
				
			||||||
 | 
							if response == nil {
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if response.ProgressMessageToEditId != 0 {
 | 
				
			||||||
 | 
								toEditId = response.ProgressMessageToEditId
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if response.Message != nil {
 | 
				
			||||||
 | 
								response.Message.ChatID = response.Injected.ChatId
 | 
				
			||||||
 | 
								if toEditId != 0 {
 | 
				
			||||||
 | 
									b.EditMessageText(ctx, &tgBot.EditMessageTextParams{
 | 
				
			||||||
 | 
										ChatID:                response.Message.ChatID,
 | 
				
			||||||
 | 
										MessageID:             toEditId,
 | 
				
			||||||
 | 
										Text:                  response.Message.Text,
 | 
				
			||||||
 | 
										ParseMode:             response.Message.ParseMode,
 | 
				
			||||||
 | 
										Entities:              response.Message.Entities,
 | 
				
			||||||
 | 
										DisableWebPagePreview: response.Message.DisableWebPagePreview,
 | 
				
			||||||
 | 
										ReplyMarkup:           response.Message.ReplyMarkup,
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									b.SendMessage(ctx, response.Message)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if response.CallbackAnswer != nil {
 | 
				
			||||||
 | 
								b.AnswerCallbackQuery(ctx, response.CallbackAnswer)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							for _, edit := range response.MessageEdits {
 | 
				
			||||||
 | 
								if (edit.ChatID == nil || edit.MessageID == 0) && edit.InlineMessageID == "" {
 | 
				
			||||||
 | 
									edit.ChatID = response.Injected.ChatId
 | 
				
			||||||
 | 
									edit.MessageID = response.Injected.MessageId
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								b.EditMessageText(ctx, edit)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							for _, edit := range response.MessageMarkupEdits {
 | 
				
			||||||
 | 
								if (edit.ChatID == nil || edit.MessageID == 0) && edit.InlineMessageID == "" {
 | 
				
			||||||
 | 
									edit.ChatID = response.Injected.ChatId
 | 
				
			||||||
 | 
									edit.MessageID = response.Injected.MessageId
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								b.EditMessageReplyMarkup(ctx, edit)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if update.Message != nil {
 | 
				
			||||||
 | 
							defer func() {
 | 
				
			||||||
 | 
								if response == nil {
 | 
				
			||||||
 | 
									response = &handlers.HandlerResponse{}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								response.Injected.ChatId = update.Message.Chat.ID
 | 
				
			||||||
 | 
								response.Injected.MessageId = update.Message.ID
 | 
				
			||||||
 | 
							}()
 | 
				
			||||||
 | 
							log.Printf("DEBUG: Got message: %s\n", update.Message.Text)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							chatFlow := handlers.GetChatFlow(update.Message.Chat.ID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							switch {
 | 
				
			||||||
 | 
							case strings.HasPrefix(update.Message.Text, trainInfoCommand):
 | 
				
			||||||
 | 
								response = handleFindTrainStages(ctx, b, update, subs)
 | 
				
			||||||
 | 
							case strings.HasPrefix(update.Message.Text, cancelCommand):
 | 
				
			||||||
 | 
								handlers.SetChatFlow(chatFlow, handlers.InitialFlowType, handlers.InitialFlowType, "")
 | 
				
			||||||
 | 
								response = &handlers.HandlerResponse{
 | 
				
			||||||
 | 
									Message: &tgBot.SendMessageParams{
 | 
				
			||||||
 | 
										Text: cancelResponseMessage,
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							default:
 | 
				
			||||||
 | 
								switch chatFlow.Type {
 | 
				
			||||||
 | 
								case handlers.InitialFlowType:
 | 
				
			||||||
 | 
									b.SendMessage(ctx, &tgBot.SendMessageParams{
 | 
				
			||||||
 | 
										ChatID: update.Message.Chat.ID,
 | 
				
			||||||
 | 
										Text:   initialMessage,
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
								case handlers.TrainInfoFlowType:
 | 
				
			||||||
 | 
									log.Printf("DEBUG: trainInfoFlowType with stage %s\n", chatFlow.Stage)
 | 
				
			||||||
 | 
									response = handleFindTrainStages(ctx, b, update, subs)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if update.CallbackQuery != nil {
 | 
				
			||||||
 | 
							defer func() {
 | 
				
			||||||
 | 
								if response == nil {
 | 
				
			||||||
 | 
									response = &handlers.HandlerResponse{
 | 
				
			||||||
 | 
										CallbackAnswer: &tgBot.AnswerCallbackQueryParams{
 | 
				
			||||||
 | 
											CallbackQueryID: update.CallbackQuery.ID,
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								response.Injected.ChatId = update.CallbackQuery.Message.Chat.ID
 | 
				
			||||||
 | 
								response.Injected.MessageId = update.CallbackQuery.Message.ID
 | 
				
			||||||
 | 
								if response.CallbackAnswer == nil {
 | 
				
			||||||
 | 
									response.CallbackAnswer = &tgBot.AnswerCallbackQueryParams{
 | 
				
			||||||
 | 
										CallbackQueryID: update.CallbackQuery.ID,
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if response.CallbackAnswer.CallbackQueryID == "" {
 | 
				
			||||||
 | 
									response.CallbackAnswer.CallbackQueryID = update.CallbackQuery.ID
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							chatFlow := handlers.GetChatFlow(update.CallbackQuery.Message.Chat.ID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if len(update.CallbackQuery.Data) != 0 {
 | 
				
			||||||
 | 
								splitted := strings.Split(update.CallbackQuery.Data, "\x1b")
 | 
				
			||||||
 | 
								switch splitted[0] {
 | 
				
			||||||
 | 
								case handlers.TrainInfoChooseDateCallbackQuery:
 | 
				
			||||||
 | 
									trainNumber := splitted[1]
 | 
				
			||||||
 | 
									dateInt, _ := strconv.ParseInt(splitted[2], 10, 64)
 | 
				
			||||||
 | 
									date := time.Unix(dateInt, 0)
 | 
				
			||||||
 | 
									message, err := b.SendMessage(ctx, &tgBot.SendMessageParams{
 | 
				
			||||||
 | 
										ChatID: update.CallbackQuery.Message.Chat.ID,
 | 
				
			||||||
 | 
										Text:   pleaseWaitMessage,
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
									response = handlers.HandleTrainNumberCommand(ctx, trainNumber, date, -1)
 | 
				
			||||||
 | 
									if err == nil {
 | 
				
			||||||
 | 
										response.ProgressMessageToEditId = message.ID
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									handlers.SetChatFlow(chatFlow, handlers.InitialFlowType, handlers.InitialFlowType, "")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								case handlers.TrainInfoChooseGroupCallbackQuery:
 | 
				
			||||||
 | 
									dateInt, _ := strconv.ParseInt(splitted[2], 10, 64)
 | 
				
			||||||
 | 
									date := time.Unix(dateInt, 0)
 | 
				
			||||||
 | 
									groupIndex, _ := strconv.ParseInt(splitted[3], 10, 31)
 | 
				
			||||||
 | 
									log.Printf("%s, %v, %d", update.CallbackQuery.Data, splitted, groupIndex)
 | 
				
			||||||
 | 
									originalResponse := handlers.HandleTrainNumberCommand(ctx, splitted[1], date, int(groupIndex))
 | 
				
			||||||
 | 
									response = &handlers.HandlerResponse{
 | 
				
			||||||
 | 
										MessageEdits: []*tgBot.EditMessageTextParams{
 | 
				
			||||||
 | 
											{
 | 
				
			||||||
 | 
												Text:                  originalResponse.Message.Text,
 | 
				
			||||||
 | 
												ParseMode:             originalResponse.Message.ParseMode,
 | 
				
			||||||
 | 
												Entities:              originalResponse.Message.Entities,
 | 
				
			||||||
 | 
												DisableWebPagePreview: originalResponse.Message.DisableWebPagePreview,
 | 
				
			||||||
 | 
												ReplyMarkup:           originalResponse.Message.ReplyMarkup,
 | 
				
			||||||
 | 
											},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func handleFindTrainStages(ctx context.Context, b *tgBot.Bot, update *models.Update, subs *subscriptions.Subscriptions) *handlers.HandlerResponse {
 | 
				
			||||||
 | 
						log.Println("DEBUG: handleFindTrainStages")
 | 
				
			||||||
 | 
						var response *handlers.HandlerResponse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var chatId int64
 | 
				
			||||||
 | 
						if update.Message != nil {
 | 
				
			||||||
 | 
							chatId = update.Message.Chat.ID
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if update.CallbackQuery != nil {
 | 
				
			||||||
 | 
							chatId = update.CallbackQuery.Message.Chat.ID
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						chatFlow := handlers.GetChatFlow(chatId)
 | 
				
			||||||
 | 
						switch chatFlow.Type {
 | 
				
			||||||
 | 
						case handlers.InitialFlowType:
 | 
				
			||||||
 | 
							// Only command is possible here
 | 
				
			||||||
 | 
							commandParamsString := strings.TrimPrefix(update.Message.Text, trainInfoCommand)
 | 
				
			||||||
 | 
							commandParamsString = strings.TrimSpace(commandParamsString)
 | 
				
			||||||
 | 
							commandParams := strings.Split(commandParamsString, " ")
 | 
				
			||||||
 | 
							if len(commandParams) > 1 {
 | 
				
			||||||
 | 
								message, err := b.SendMessage(ctx, &tgBot.SendMessageParams{
 | 
				
			||||||
 | 
									ChatID: update.Message.Chat.ID,
 | 
				
			||||||
 | 
									Text:   pleaseWaitMessage,
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								trainNumber := commandParams[0]
 | 
				
			||||||
 | 
								date := time.Now()
 | 
				
			||||||
 | 
								groupIndex := -1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if len(commandParams) > 1 {
 | 
				
			||||||
 | 
									date, _ = time.Parse(time.RFC3339, commandParams[1])
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if len(commandParams) > 2 {
 | 
				
			||||||
 | 
									groupIndex, _ = strconv.Atoi(commandParams[2])
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								response = handlers.HandleTrainNumberCommand(ctx, trainNumber, date, groupIndex)
 | 
				
			||||||
 | 
								if err == nil {
 | 
				
			||||||
 | 
									response.ProgressMessageToEditId = message.ID
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else if len(commandParams) > 0 && len(commandParams[0]) != 0 {
 | 
				
			||||||
 | 
								// Got only train number
 | 
				
			||||||
 | 
								trainNumber := commandParams[0]
 | 
				
			||||||
 | 
								response = getTrainInfoChooseDateResponse(trainNumber)
 | 
				
			||||||
 | 
								handlers.SetChatFlow(chatFlow, handlers.TrainInfoFlowType, handlers.WaitingForDateStage, trainNumber)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								response = &handlers.HandlerResponse{
 | 
				
			||||||
 | 
									Message: &tgBot.SendMessageParams{
 | 
				
			||||||
 | 
										Text: waitingForTrainNumberMessage,
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								handlers.SetChatFlow(chatFlow, handlers.TrainInfoFlowType, handlers.WaitingForTrainNumberStage, "")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						case handlers.TrainInfoFlowType:
 | 
				
			||||||
 | 
							switch chatFlow.Stage {
 | 
				
			||||||
 | 
							case handlers.WaitingForTrainNumberStage:
 | 
				
			||||||
 | 
								trainNumber := update.Message.Text
 | 
				
			||||||
 | 
								response = getTrainInfoChooseDateResponse(trainNumber)
 | 
				
			||||||
 | 
								handlers.SetChatFlow(chatFlow, handlers.TrainInfoFlowType, handlers.WaitingForDateStage, trainNumber)
 | 
				
			||||||
 | 
							case handlers.WaitingForDateStage:
 | 
				
			||||||
 | 
								date, err := utils.ParseDate(update.Message.Text)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									response = &handlers.HandlerResponse{
 | 
				
			||||||
 | 
										Message: &tgBot.SendMessageParams{
 | 
				
			||||||
 | 
											Text: invalidDateMessage,
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									message, err := b.SendMessage(ctx, &tgBot.SendMessageParams{
 | 
				
			||||||
 | 
										ChatID: update.Message.Chat.ID,
 | 
				
			||||||
 | 
										Text:   pleaseWaitMessage,
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
									response = handlers.HandleTrainNumberCommand(ctx, chatFlow.Extra, date, -1)
 | 
				
			||||||
 | 
									if err == nil {
 | 
				
			||||||
 | 
										response.ProgressMessageToEditId = message.ID
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									handlers.SetChatFlow(chatFlow, handlers.InitialFlowType, handlers.InitialFlowType, "")
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return response
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func getTrainInfoChooseDateResponse(trainNumber string) *handlers.HandlerResponse {
 | 
				
			||||||
 | 
						replyButtons := make([][]models.InlineKeyboardButton, 0, 4)
 | 
				
			||||||
 | 
						replyButtons = append(replyButtons, []models.InlineKeyboardButton{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								Text:         fmt.Sprintf("Yesterday (%s)", time.Now().Add(time.Hour*-24).In(utils.Location).Format("02.01.2006")),
 | 
				
			||||||
 | 
								CallbackData: fmt.Sprintf(handlers.TrainInfoChooseDateCallbackQuery+"\x1b%s\x1b%d", trainNumber, time.Now().Add(time.Hour*-24).Unix()),
 | 
				
			||||||
 | 
							}, {
 | 
				
			||||||
 | 
								Text:         fmt.Sprintf("Today (%s)", time.Now().In(utils.Location).Format("02.01.2006")),
 | 
				
			||||||
 | 
								CallbackData: fmt.Sprintf(handlers.TrainInfoChooseDateCallbackQuery+"\x1b%s\x1b%d", trainNumber, time.Now().Unix()),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						for i := 1; i < 4; i++ {
 | 
				
			||||||
 | 
							arr := make([]models.InlineKeyboardButton, 0, 7)
 | 
				
			||||||
 | 
							for j := 0; j < 7; j++ {
 | 
				
			||||||
 | 
								ts := time.Now().Add(time.Hour * time.Duration(24*(j+(i-1)*7+1))).In(utils.Location)
 | 
				
			||||||
 | 
								arr = append(arr, models.InlineKeyboardButton{
 | 
				
			||||||
 | 
									Text:         ts.Format("02.01"),
 | 
				
			||||||
 | 
									CallbackData: fmt.Sprintf(handlers.TrainInfoChooseDateCallbackQuery+"\x1b%s\x1b%d", trainNumber, ts.Unix()),
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							replyButtons = append(replyButtons, arr)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return &handlers.HandlerResponse{
 | 
				
			||||||
 | 
							Message: &tgBot.SendMessageParams{
 | 
				
			||||||
 | 
								Text: chooseDateMessage,
 | 
				
			||||||
 | 
								ReplyMarkup: models.InlineKeyboardMarkup{
 | 
				
			||||||
 | 
									InlineKeyboard: replyButtons,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										111
									
								
								pkg/api/trains.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								pkg/api/trains.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,111 @@
 | 
				
			||||||
 | 
					package api
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"net/url"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type TrainResponse struct {
 | 
				
			||||||
 | 
						Rank     string       `json:"rank"`
 | 
				
			||||||
 | 
						Number   string       `json:"number"`
 | 
				
			||||||
 | 
						Date     string       `json:"date"`
 | 
				
			||||||
 | 
						Operator string       `json:"operator"`
 | 
				
			||||||
 | 
						Groups   []TrainGroup `json:"groups"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type TrainGroup struct {
 | 
				
			||||||
 | 
						Route struct {
 | 
				
			||||||
 | 
							From string `json:"from"`
 | 
				
			||||||
 | 
							To   string `json:"to"`
 | 
				
			||||||
 | 
						} `json:"route"`
 | 
				
			||||||
 | 
						Status *struct {
 | 
				
			||||||
 | 
							Delay   int    `json:"delay"`
 | 
				
			||||||
 | 
							Station string `json:"station"`
 | 
				
			||||||
 | 
							State   string `json:"state"`
 | 
				
			||||||
 | 
						} `json:"status"`
 | 
				
			||||||
 | 
						Stations []TrainStation `json:"stations"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type TrainStation struct {
 | 
				
			||||||
 | 
						Name         string       `json:"name"`
 | 
				
			||||||
 | 
						LinkName     string       `json:"linkName"`
 | 
				
			||||||
 | 
						Km           int          `json:"km"`
 | 
				
			||||||
 | 
						StoppingTime *int         `json:"stoppingTime"`
 | 
				
			||||||
 | 
						Platform     *string      `json:"platform"`
 | 
				
			||||||
 | 
						Arrival      *TrainArrDep `json:"arrival"`
 | 
				
			||||||
 | 
						Departure    *TrainArrDep `json:"departure"`
 | 
				
			||||||
 | 
						Notes        []any        `json:"notes"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type TrainArrDep struct {
 | 
				
			||||||
 | 
						ScheduleTime time.Time `json:"scheduleTime"`
 | 
				
			||||||
 | 
						Status       *struct {
 | 
				
			||||||
 | 
							Delay     int  `json:"delay"`
 | 
				
			||||||
 | 
							Real      bool `json:"real"`
 | 
				
			||||||
 | 
							Cancelled bool `json:"cancelled"`
 | 
				
			||||||
 | 
						} `json:"status"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						trainApiEndpoint = "https://scraper.infotren.dcdev.ro/v3"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						TrainNotFound = fmt.Errorf("train not found")
 | 
				
			||||||
 | 
						ServerError   = fmt.Errorf("server error")
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func GetTrain(ctx context.Context, trainNumber string, date time.Time) (*TrainResponse, error) {
 | 
				
			||||||
 | 
						u, _ := url.Parse(trainApiEndpoint)
 | 
				
			||||||
 | 
						u.Path, _ = url.JoinPath(u.Path, "trains", trainNumber)
 | 
				
			||||||
 | 
						query := u.Query()
 | 
				
			||||||
 | 
						query.Add("date", date.Format(time.RFC3339))
 | 
				
			||||||
 | 
						u.RawQuery = query.Encode()
 | 
				
			||||||
 | 
						req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("error getting train %s: %w", trainNumber, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						res, err := http.DefaultClient.Do(req)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("error getting train %s: %w", trainNumber, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer func() {
 | 
				
			||||||
 | 
							_ = res.Body.Close()
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						switch {
 | 
				
			||||||
 | 
						case res.StatusCode == http.StatusNotFound:
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("error getting train %s: %w", trainNumber, TrainNotFound)
 | 
				
			||||||
 | 
						case res.StatusCode/100 != 2:
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("error getting train %s: status code %d: %w", trainNumber, res.StatusCode, ServerError)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var body []byte
 | 
				
			||||||
 | 
						if res.ContentLength > 0 {
 | 
				
			||||||
 | 
							body = make([]byte, res.ContentLength)
 | 
				
			||||||
 | 
							n, err := io.ReadFull(res.Body, body)
 | 
				
			||||||
 | 
							if err != nil && err != io.EOF {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("error getting train %s: %w", trainNumber, err)
 | 
				
			||||||
 | 
							} else if n != int(res.ContentLength) {
 | 
				
			||||||
 | 
								body = body[0:n]
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							body, err = io.ReadAll(res.Body)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("error getting train %s: %w", trainNumber, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var trainData TrainResponse
 | 
				
			||||||
 | 
						if err := json.Unmarshal(body, &trainData); err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("error getting train %s: %w", trainNumber, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return &trainData, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										28
									
								
								pkg/database/database.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								pkg/database/database.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,28 @@
 | 
				
			||||||
 | 
					package database
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"sync"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"gorm.io/gorm"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						db    *gorm.DB
 | 
				
			||||||
 | 
						mutex sync.RWMutex
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func SetDatabase(d *gorm.DB) {
 | 
				
			||||||
 | 
						db = d
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func ReadDB[T any](callback func(*gorm.DB) (T, error)) (T, error) {
 | 
				
			||||||
 | 
						mutex.RLock()
 | 
				
			||||||
 | 
						defer mutex.RUnlock()
 | 
				
			||||||
 | 
						return callback(db)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func WriteDB[T any](callback func(*gorm.DB) (T, error)) (T, error) {
 | 
				
			||||||
 | 
						mutex.Lock()
 | 
				
			||||||
 | 
						defer mutex.Unlock()
 | 
				
			||||||
 | 
						return callback(db)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										57
									
								
								pkg/handlers/chatFlow.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								pkg/handlers/chatFlow.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,57 @@
 | 
				
			||||||
 | 
					package handlers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"dcdev.ro/CfrTrainInfoTelegramBot/pkg/database"
 | 
				
			||||||
 | 
						"gorm.io/gorm"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						InitialFlowType     = "initial"
 | 
				
			||||||
 | 
						TrainInfoFlowType   = "trainInfo"
 | 
				
			||||||
 | 
						StationInfoFlowType = "stationInfo"
 | 
				
			||||||
 | 
						RouteFlowType       = "route"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						WaitingForTrainNumberStage = "waitingForTrainNumber"
 | 
				
			||||||
 | 
						WaitingForDateStage        = "waitingForDate"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ChatFlow struct {
 | 
				
			||||||
 | 
						gorm.Model
 | 
				
			||||||
 | 
						ChatId int64
 | 
				
			||||||
 | 
						Type   string
 | 
				
			||||||
 | 
						Stage  string
 | 
				
			||||||
 | 
						Extra  string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func GetChatFlow(chatId int64) *ChatFlow {
 | 
				
			||||||
 | 
						chatFlow := &ChatFlow{}
 | 
				
			||||||
 | 
						result, _ := database.ReadDB(func(db *gorm.DB) (*gorm.DB, error) {
 | 
				
			||||||
 | 
							return db.First(chatFlow, "chat_id = ?", chatId), nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						if result.RowsAffected == 0 {
 | 
				
			||||||
 | 
							log.Printf("DEBUG: Chat not found in DB: %d\n", chatId)
 | 
				
			||||||
 | 
							chatFlow = &ChatFlow{
 | 
				
			||||||
 | 
								ChatId: chatId,
 | 
				
			||||||
 | 
								Type:   InitialFlowType,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							_, _ = database.WriteDB(func(db *gorm.DB) (*gorm.DB, error) {
 | 
				
			||||||
 | 
								return db.Create(chatFlow), nil
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							log.Printf("DEBUG: Chat found in DB: %d, type %s, stage %s\n", chatId, chatFlow.Type, chatFlow.Stage)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return chatFlow
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func SetChatFlow(chatFlow *ChatFlow, flowType string, stage string, extra string) {
 | 
				
			||||||
 | 
						_, _ = database.WriteDB(func(db *gorm.DB) (*gorm.DB, error) {
 | 
				
			||||||
 | 
							return db.Model(chatFlow).Updates(ChatFlow{
 | 
				
			||||||
 | 
								Type:  flowType,
 | 
				
			||||||
 | 
								Stage: stage,
 | 
				
			||||||
 | 
								Extra: extra,
 | 
				
			||||||
 | 
							}), nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						log.Printf("DEBUG: setChatFlow type %s, stage %s", flowType, stage)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										164
									
								
								pkg/handlers/findTrain.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								pkg/handlers/findTrain.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,164 @@
 | 
				
			||||||
 | 
					package handlers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"net/url"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"dcdev.ro/CfrTrainInfoTelegramBot/pkg/api"
 | 
				
			||||||
 | 
						"github.com/go-telegram/bot"
 | 
				
			||||||
 | 
						"github.com/go-telegram/bot/models"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						TrainInfoChooseDateCallbackQuery  = "TI_CHOOSE_DATE"
 | 
				
			||||||
 | 
						TrainInfoChooseGroupCallbackQuery = "TI_CHOOSE_GROUP"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						viewInKaiBaseUrl = "https://kai.infotren.dcdev.ro/view-train.html"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func HandleTrainNumberCommand(ctx context.Context, trainNumber string, date time.Time, groupIndex int) *HandlerResponse {
 | 
				
			||||||
 | 
						trainData, err := api.GetTrain(ctx, trainNumber, date)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						switch {
 | 
				
			||||||
 | 
						case err == nil:
 | 
				
			||||||
 | 
							break
 | 
				
			||||||
 | 
						case errors.Is(err, api.TrainNotFound):
 | 
				
			||||||
 | 
							log.Printf("ERROR: In handle train number: %s", err.Error())
 | 
				
			||||||
 | 
							return &HandlerResponse{
 | 
				
			||||||
 | 
								Message: &bot.SendMessageParams{
 | 
				
			||||||
 | 
									Text: fmt.Sprintf("The train %s was not found.", trainNumber),
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						case errors.Is(err, api.ServerError):
 | 
				
			||||||
 | 
							log.Printf("ERROR: In handle train number: %s", err.Error())
 | 
				
			||||||
 | 
							return &HandlerResponse{
 | 
				
			||||||
 | 
								Message: &bot.SendMessageParams{
 | 
				
			||||||
 | 
									Text: fmt.Sprintf("Unknown server error when searching for train %s.", trainNumber),
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							log.Printf("ERROR: In handle train number: %s", err.Error())
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(trainData.Groups) == 1 {
 | 
				
			||||||
 | 
							groupIndex = 0
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						kaiUrl, _ := url.Parse(viewInKaiBaseUrl)
 | 
				
			||||||
 | 
						kaiUrlQuery := kaiUrl.Query()
 | 
				
			||||||
 | 
						kaiUrlQuery.Add("train", trainData.Number)
 | 
				
			||||||
 | 
						kaiUrlQuery.Add("date", trainData.Groups[0].Stations[0].Departure.ScheduleTime.Format(time.RFC3339))
 | 
				
			||||||
 | 
						if groupIndex != -1 {
 | 
				
			||||||
 | 
							kaiUrlQuery.Add("groupIndex", strconv.Itoa(groupIndex))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						kaiUrl.RawQuery = kaiUrlQuery.Encode()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						message := bot.SendMessageParams{}
 | 
				
			||||||
 | 
						if groupIndex == -1 {
 | 
				
			||||||
 | 
							message.Text = fmt.Sprintf("Train %s %s contains multiple groups. Please choose one.", trainData.Rank, trainData.Number)
 | 
				
			||||||
 | 
							replyButtons := make([][]models.InlineKeyboardButton, len(trainData.Groups)+1)
 | 
				
			||||||
 | 
							for i := range replyButtons {
 | 
				
			||||||
 | 
								if i == len(trainData.Groups) {
 | 
				
			||||||
 | 
									replyButtons[i] = append(replyButtons[i], models.InlineKeyboardButton{
 | 
				
			||||||
 | 
										Text: "Open in WebApp",
 | 
				
			||||||
 | 
										URL:  kaiUrl.String(),
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									group := &trainData.Groups[i]
 | 
				
			||||||
 | 
									replyButtons[i] = append(replyButtons[i], models.InlineKeyboardButton{
 | 
				
			||||||
 | 
										Text:         fmt.Sprintf("%s ➔ %s", group.Route.From, group.Route.To),
 | 
				
			||||||
 | 
										CallbackData: fmt.Sprintf(TrainInfoChooseGroupCallbackQuery+"\x1b%s\x1b%d\x1b%d", trainNumber, date.Unix(), i),
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							message.ReplyMarkup = models.InlineKeyboardMarkup{
 | 
				
			||||||
 | 
								InlineKeyboard: replyButtons,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else if len(trainData.Groups) > groupIndex {
 | 
				
			||||||
 | 
							group := &trainData.Groups[groupIndex]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							messageText := strings.Builder{}
 | 
				
			||||||
 | 
							messageText.WriteString(fmt.Sprintf("Train %s %s\n%s ➔ %s\n\n", trainData.Rank, trainData.Number, group.Route.From, group.Route.To))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							messageText.WriteString(fmt.Sprintf("Date: %s\n", trainData.Date))
 | 
				
			||||||
 | 
							messageText.WriteString(fmt.Sprintf("Operator: %s\n", trainData.Operator))
 | 
				
			||||||
 | 
							if group.Status != nil {
 | 
				
			||||||
 | 
								messageText.WriteString("Status: ")
 | 
				
			||||||
 | 
								if group.Status.Delay == 0 {
 | 
				
			||||||
 | 
									messageText.WriteString("on time when ")
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									messageText.WriteString(fmt.Sprintf("%d min ", func(x int) int {
 | 
				
			||||||
 | 
										if x < 0 {
 | 
				
			||||||
 | 
											return -x
 | 
				
			||||||
 | 
										} else {
 | 
				
			||||||
 | 
											return x
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}(group.Status.Delay)))
 | 
				
			||||||
 | 
									if group.Status.Delay < 0 {
 | 
				
			||||||
 | 
										messageText.WriteString("early when ")
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										messageText.WriteString("late when ")
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								switch group.Status.State {
 | 
				
			||||||
 | 
								case "arrival":
 | 
				
			||||||
 | 
									messageText.WriteString("arriving at ")
 | 
				
			||||||
 | 
								case "departure":
 | 
				
			||||||
 | 
									messageText.WriteString("departing from ")
 | 
				
			||||||
 | 
								case "passing":
 | 
				
			||||||
 | 
									messageText.WriteString("passing through ")
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								messageText.WriteString(group.Status.Station)
 | 
				
			||||||
 | 
								messageText.WriteString("\n")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							message.Text = messageText.String()
 | 
				
			||||||
 | 
							message.Entities = []models.MessageEntity{
 | 
				
			||||||
 | 
								{
 | 
				
			||||||
 | 
									Type:   models.MessageEntityTypeBold,
 | 
				
			||||||
 | 
									Offset: 6,
 | 
				
			||||||
 | 
									Length: len(fmt.Sprintf("%s %s", trainData.Rank, trainData.Number)),
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							message.ReplyMarkup = models.InlineKeyboardMarkup{
 | 
				
			||||||
 | 
								InlineKeyboard: [][]models.InlineKeyboardButton{
 | 
				
			||||||
 | 
									{
 | 
				
			||||||
 | 
										models.InlineKeyboardButton{
 | 
				
			||||||
 | 
											Text: "Open in WebApp",
 | 
				
			||||||
 | 
											URL:  kaiUrl.String(),
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							message.Text = fmt.Sprintf("The status of the train %s %s is unknown.", trainData.Rank, trainData.Number)
 | 
				
			||||||
 | 
							message.Entities = []models.MessageEntity{
 | 
				
			||||||
 | 
								{
 | 
				
			||||||
 | 
									Type:   models.MessageEntityTypeBold,
 | 
				
			||||||
 | 
									Offset: 24,
 | 
				
			||||||
 | 
									Length: len(fmt.Sprintf("%s %s", trainData.Rank, trainData.Number)),
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							message.ReplyMarkup = models.InlineKeyboardMarkup{
 | 
				
			||||||
 | 
								InlineKeyboard: [][]models.InlineKeyboardButton{
 | 
				
			||||||
 | 
									{
 | 
				
			||||||
 | 
										models.InlineKeyboardButton{
 | 
				
			||||||
 | 
											Text: "Open in WebApp",
 | 
				
			||||||
 | 
											URL:  kaiUrl.String(),
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return &HandlerResponse{
 | 
				
			||||||
 | 
							Message: &message,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										15
									
								
								pkg/handlers/response.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								pkg/handlers/response.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,15 @@
 | 
				
			||||||
 | 
					package handlers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import "github.com/go-telegram/bot"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type HandlerResponse struct {
 | 
				
			||||||
 | 
						Message                 *bot.SendMessageParams
 | 
				
			||||||
 | 
						ProgressMessageToEditId int
 | 
				
			||||||
 | 
						CallbackAnswer          *bot.AnswerCallbackQueryParams
 | 
				
			||||||
 | 
						MessageEdits            []*bot.EditMessageTextParams
 | 
				
			||||||
 | 
						MessageMarkupEdits      []*bot.EditMessageReplyMarkupParams
 | 
				
			||||||
 | 
						Injected                struct {
 | 
				
			||||||
 | 
							ChatId    int64
 | 
				
			||||||
 | 
							MessageId int
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										143
									
								
								pkg/subscriptions/subscriptions.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								pkg/subscriptions/subscriptions.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,143 @@
 | 
				
			||||||
 | 
					package subscriptions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"sync"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"dcdev.ro/CfrTrainInfoTelegramBot/pkg/database"
 | 
				
			||||||
 | 
						"gorm.io/gorm"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type SubData struct {
 | 
				
			||||||
 | 
						gorm.Model
 | 
				
			||||||
 | 
						ChatId      int64
 | 
				
			||||||
 | 
						MessageId   int
 | 
				
			||||||
 | 
						TrainNumber string
 | 
				
			||||||
 | 
						Date        time.Time
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Subscriptions struct {
 | 
				
			||||||
 | 
						mutex sync.RWMutex
 | 
				
			||||||
 | 
						data  map[int64][]SubData
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func LoadSubscriptions() (*Subscriptions, error) {
 | 
				
			||||||
 | 
						subs := make([]SubData, 0)
 | 
				
			||||||
 | 
						_, err := database.ReadDB(func(db *gorm.DB) (*gorm.DB, error) {
 | 
				
			||||||
 | 
							result := db.Find(&subs)
 | 
				
			||||||
 | 
							return result, result.Error
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						result := map[int64][]SubData{}
 | 
				
			||||||
 | 
						for _, sub := range subs {
 | 
				
			||||||
 | 
							result[sub.ChatId] = append(result[sub.ChatId], sub)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return &Subscriptions{
 | 
				
			||||||
 | 
							mutex: sync.RWMutex{},
 | 
				
			||||||
 | 
							data:  result,
 | 
				
			||||||
 | 
						}, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (sub *Subscriptions) Replace(chatId int64, data []SubData) error {
 | 
				
			||||||
 | 
						// Only allow replacing if all records use same chatId
 | 
				
			||||||
 | 
						for _, d := range data {
 | 
				
			||||||
 | 
							if d.ChatId != chatId {
 | 
				
			||||||
 | 
								return fmt.Errorf("data contains item whose ChatId (%d) doesn't match chatId (%d)", d.ChatId, chatId)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						sub.mutex.Lock()
 | 
				
			||||||
 | 
						defer sub.mutex.Unlock()
 | 
				
			||||||
 | 
						sub.data[chatId] = data
 | 
				
			||||||
 | 
						_, err := database.WriteDB(func(db *gorm.DB) (*gorm.DB, error) {
 | 
				
			||||||
 | 
							db.Delete(&SubData{}, "chat_id = ?", chatId)
 | 
				
			||||||
 | 
							db.Create(&data)
 | 
				
			||||||
 | 
							return db, db.Error
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						return err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (sub *Subscriptions) InsertSubscription(chatId int64, data SubData) error {
 | 
				
			||||||
 | 
						sub.mutex.Lock()
 | 
				
			||||||
 | 
						defer sub.mutex.Unlock()
 | 
				
			||||||
 | 
						datas := sub.data[chatId]
 | 
				
			||||||
 | 
						datas = append(datas, data)
 | 
				
			||||||
 | 
						sub.data[chatId] = datas
 | 
				
			||||||
 | 
						_, err := database.WriteDB(func(db *gorm.DB) (*gorm.DB, error) {
 | 
				
			||||||
 | 
							db.Create(&data)
 | 
				
			||||||
 | 
							return db, db.Error
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						return err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (sub *Subscriptions) DeleteChat(chatId int64) error {
 | 
				
			||||||
 | 
						sub.mutex.Lock()
 | 
				
			||||||
 | 
						defer sub.mutex.Unlock()
 | 
				
			||||||
 | 
						delete(sub.data, chatId)
 | 
				
			||||||
 | 
						_, err := database.WriteDB(func(db *gorm.DB) (*gorm.DB, error) {
 | 
				
			||||||
 | 
							db.Delete(&SubData{}, "chat_id = ?", chatId)
 | 
				
			||||||
 | 
							return db, db.Error
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						return err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (sub *Subscriptions) DeleteSubscription(chatId int64, messageId int) (*SubData, error) {
 | 
				
			||||||
 | 
						sub.mutex.Lock()
 | 
				
			||||||
 | 
						defer sub.mutex.Unlock()
 | 
				
			||||||
 | 
						datas := sub.data[chatId]
 | 
				
			||||||
 | 
						deleteIndex := -1
 | 
				
			||||||
 | 
						for i := range datas {
 | 
				
			||||||
 | 
							if datas[i].MessageId == messageId {
 | 
				
			||||||
 | 
								deleteIndex = i
 | 
				
			||||||
 | 
								break
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						var result *SubData
 | 
				
			||||||
 | 
						if deleteIndex != -1 {
 | 
				
			||||||
 | 
							result = &SubData{}
 | 
				
			||||||
 | 
							*result = datas[deleteIndex]
 | 
				
			||||||
 | 
							datas[deleteIndex] = datas[len(datas)-1]
 | 
				
			||||||
 | 
							datas = datas[:len(datas)-1]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							_, err := database.WriteDB(func(db *gorm.DB) (*gorm.DB, error) {
 | 
				
			||||||
 | 
								db.Delete(result)
 | 
				
			||||||
 | 
								return db, db.Error
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("subscription chatId %d messageId %d not found", chatId, messageId)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if len(datas) == 0 {
 | 
				
			||||||
 | 
							delete(sub.data, chatId)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							sub.data[chatId] = datas
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return result, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (sub *Subscriptions) CheckSubscriptions(ctx context.Context) {
 | 
				
			||||||
 | 
						ticker := time.NewTicker(time.Second * 90)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							select {
 | 
				
			||||||
 | 
							case <-ticker.C:
 | 
				
			||||||
 | 
								func() {
 | 
				
			||||||
 | 
									sub.mutex.RLock()
 | 
				
			||||||
 | 
									defer sub.mutex.RUnlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									for chatId, datas := range sub.data {
 | 
				
			||||||
 | 
										// TODO: Check for updates
 | 
				
			||||||
 | 
										for i := range datas {
 | 
				
			||||||
 | 
											data := &datas[i]
 | 
				
			||||||
 | 
											log.Printf("DEBUG: Timer tick, update for chat %d, train %s", chatId, data.TrainNumber)
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}()
 | 
				
			||||||
 | 
							case <-ctx.Done():
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										7
									
								
								pkg/utils/location.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								pkg/utils/location.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,7 @@
 | 
				
			||||||
 | 
					package utils
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import "time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						Location, _ = time.LoadLocation("Europe/Bucharest")
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
							
								
								
									
										56
									
								
								pkg/utils/parseDate.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								pkg/utils/parseDate.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,56 @@
 | 
				
			||||||
 | 
					package utils
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						InvalidDateFormat = fmt.Errorf("invalid date format")
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func ParseDate(input string) (time.Time, error) {
 | 
				
			||||||
 | 
						if strings.Contains(input, "-") {
 | 
				
			||||||
 | 
							return parse3Part(input, "-", 0, 1, 2)
 | 
				
			||||||
 | 
						} else if strings.Contains(input, "/") {
 | 
				
			||||||
 | 
							return parse3Part(input, "/", 2, 0, 1)
 | 
				
			||||||
 | 
						} else if strings.Contains(input, ".") {
 | 
				
			||||||
 | 
							return parse3Part(input, ".", 2, 1, 0)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							parsed, err := strconv.ParseInt(input, 10, 63)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return time.Time{}, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return time.Unix(parsed, 0), nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func parse3Part(input string, sep string, yearIndex int, monthIndex int, dayIndex int) (time.Time, error) {
 | 
				
			||||||
 | 
						splitted := strings.Split(input, sep)
 | 
				
			||||||
 | 
						if len(splitted) == 2 && yearIndex == 2 {
 | 
				
			||||||
 | 
							// If the year is the last part of the format, allow omitting it
 | 
				
			||||||
 | 
							splitted = append(splitted, fmt.Sprintf("%d", time.Now().Year()))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if len(splitted) != 3 {
 | 
				
			||||||
 | 
							return time.Time{}, InvalidDateFormat
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						year, err := strconv.Atoi(splitted[yearIndex])
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return time.Time{}, InvalidDateFormat
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if year < 100 {
 | 
				
			||||||
 | 
							// Assume xx.xx.23 or x/x/23 => 2023
 | 
				
			||||||
 | 
							year = (time.Now().Year() / 100 * 100) + year
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						month, err := strconv.Atoi(splitted[monthIndex])
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return time.Time{}, InvalidDateFormat
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						day, err := strconv.Atoi(splitted[dayIndex])
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return time.Time{}, InvalidDateFormat
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return time.Date(year, time.Month(month), day, 12, 0, 0, 0, Location), nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
	Add table
		
		Reference in a new issue