diff --git a/adaptors/tg/botsend.go b/adaptors/tg/botsend.go new file mode 100644 index 0000000..9885d8f --- /dev/null +++ b/adaptors/tg/botsend.go @@ -0,0 +1,56 @@ +package tg + +import ( + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + query "github.com/tgckpg/golifehk/query" +) + +func BotSendText(bot *tgbotapi.BotAPI, update *tgbotapi.Update, mesg *string) { + var msg tgbotapi.MessageConfig + msg = tgbotapi.NewMessage(update.Message.Chat.ID, *mesg) + msg.ParseMode = "MarkdownV2" + + msg.ReplyToMessageID = update.Message.MessageID + bot.Send(msg) +} + +func BotSend(bot *tgbotapi.BotAPI, update *tgbotapi.Update, qResult query.IQueryResult) (bool, error) { + + var msg tgbotapi.MessageConfig + mesg, err := qResult.Message() + + if err != nil { + // not sent, error + return false, err + } + + mesgType := qResult.DataType() + + switch mesgType { + case "IGNORE": + // not sent with no error tells the parent to look for other processors + return false, nil + } + + msg = tgbotapi.NewMessage(update.Message.Chat.ID, mesg) + msg.ParseMode = "MarkdownV2" + + switch mesgType { + case "PlainText": + case "Table": + + button := tgbotapi.NewInlineKeyboardButtonData( + "Show Status", // what user sees + "status_cmd", // what bot receives + ) + + msg.ReplyMarkup = tgbotapi.NewInlineKeyboardMarkup( + tgbotapi.NewInlineKeyboardRow(button, button), + ) + + } + + msg.ReplyToMessageID = update.Message.MessageID + bot.Send(msg) + return true, nil +} diff --git a/datasources/cjlookup/CChar.go b/datasources/cjlookup/CChar.go index 4898051..689e76a 100644 --- a/datasources/cjlookup/CChar.go +++ b/datasources/cjlookup/CChar.go @@ -12,7 +12,8 @@ type CChar struct { JyutPing *CJyutPing DukJam *[]*DukJam _JiDukJam *[]*CJyutPing - query.Searchable + query.Words + query.NoGeoLocation } type DukJam struct { @@ -25,7 +26,8 @@ type CJyutPing struct { SearchKey string // Searchable key TungJamZi *[]*CChar tSorted bool - query.Searchable + query.Words + query.NoGeoLocation } func (this *CJyutPing) Test(val string) bool { diff --git a/datasources/cjlookup/QueryResult.go b/datasources/cjlookup/QueryResult.go index a824bbc..16a9272 100644 --- a/datasources/cjlookup/QueryResult.go +++ b/datasources/cjlookup/QueryResult.go @@ -13,6 +13,9 @@ type QueryResult struct { Lang string Error error Query *query.QueryObject + + ResultType string + isConsumed bool } func writeCCharInfo(sb *strings.Builder, cc *CChar) { @@ -37,14 +40,20 @@ func writeCCharInfo(sb *strings.Builder, cc *CChar) { } } +func (this QueryResult) DataType() string { return this.ResultType } +func (this QueryResult) Consumed() bool { return this.isConsumed } +func (this QueryResult) GetTableData() [][]map[string]string { return nil } + func (this QueryResult) Message() (string, error) { + this.ResultType = "PlainText" + if this.Error != nil { return "", this.Error } if this.Query == nil { - panic("Query is nil") + return "", nil } sb := strings.Builder{} diff --git a/datasources/cjlookup/query.go b/datasources/cjlookup/query.go index d1aa5f0..19c039f 100644 --- a/datasources/cjlookup/query.go +++ b/datasources/cjlookup/query.go @@ -1,13 +1,16 @@ package cjlookup import ( - "fmt" "strings" "github.com/tgckpg/golifehk/query" ) -func Query(lang string, message string) query.IQueryResult { +func Query(q query.QueryMessage) query.IQueryResult { + + lang := q.Lang + message := q.Text + var qResults *query.QueryObject var err error var searchables *[]query.ISearchable @@ -17,9 +20,12 @@ func Query(lang string, message string) query.IQueryResult { // Only look up jyut ping if !strings.HasPrefix(messageU, "JP ") { - err = fmt.Errorf("Invalid query") + qr.ResultType = "IGNORE" + goto qrReturn } + qr.isConsumed = true + if err != nil { qr.Error = err goto qrReturn @@ -28,7 +34,7 @@ func Query(lang string, message string) query.IQueryResult { messageU = messageU[3:] searchables, err = getSearchables() - qResults, err = query.Parse(messageU, searchables) + qResults, err = query.MatchKeys(messageU, searchables) qr.Query = qResults diff --git a/datasources/kmb/QueryResult.go b/datasources/kmb/QueryResult.go index 3a9ee42..a1bdce0 100644 --- a/datasources/kmb/QueryResult.go +++ b/datasources/kmb/QueryResult.go @@ -1,176 +1,181 @@ package kmb import ( - "fmt" - "sort" - "strings" - "time" + "fmt" + "sort" + "strings" + "time" - query "github.com/tgckpg/golifehk/query" - utils "github.com/tgckpg/golifehk/utils" + query "github.com/tgckpg/golifehk/query" + utils "github.com/tgckpg/golifehk/utils" ) type QueryResult struct { - Schedules *map[*RouteStop] *[] *Schedule - Lang string - Error error - Query *query.QueryObject + Schedules *map[*RouteStop]*[]*Schedule + Lang string + Error error + Query *query.QueryObject + + isConsumed bool } -func writeRouteHead( sb *strings.Builder, r *RouteStop ) { - utils.WriteMDv2Text( sb, r.RouteId ) - if r.Direction == "O" { - sb.WriteString( "↑" ) - } else if r.Direction == "I" { - sb.WriteString( "↓" ) - } - if r.ServiceType != "1" { - utils.WriteMDv2Text( sb, utils.ToPower( r.ServiceType ) ) - } +func writeRouteHead(sb *strings.Builder, r *RouteStop) { + utils.WriteMDv2Text(sb, r.RouteId) + if r.Direction == "O" { + sb.WriteString("↑") + } else if r.Direction == "I" { + sb.WriteString("↓") + } + if r.ServiceType != "1" { + utils.WriteMDv2Text(sb, utils.ToPower(r.ServiceType)) + } } -func writeShortRoute( lang *string, sb *strings.Builder, r *RouteStop ) { +func writeShortRoute(lang *string, sb *strings.Builder, r *RouteStop) { - if r.PrevStop() != nil { - utils.WriteMDv2Text( sb, (*(r.PrevStop().BusStop).Name)[ *lang ] ) - sb.WriteString( " \\> " ) - } + if r.PrevStop() != nil { + utils.WriteMDv2Text(sb, (*(r.PrevStop().BusStop).Name)[*lang]) + sb.WriteString(" \\> ") + } - sb.WriteString( "*" ) - utils.WriteMDv2Text( sb, (*(r.BusStop).Name)[ *lang ] ) - sb.WriteString( "*" ) + sb.WriteString("*") + utils.WriteMDv2Text(sb, (*(r.BusStop).Name)[*lang]) + sb.WriteString("*") - if r.NextStop() != nil { - sb.WriteString( " \\> " ) - utils.WriteMDv2Text( sb, (*(r.NextStop().BusStop).Name)[ *lang ] ) - } + if r.NextStop() != nil { + sb.WriteString(" \\> ") + utils.WriteMDv2Text(sb, (*(r.NextStop().BusStop).Name)[*lang]) + } - sb.WriteString( "\n" ) + sb.WriteString("\n") } -func ( this *QueryResult ) Message() ( string, error ) { +func (this QueryResult) DataType() string { return "MarkdownV2" } +func (this QueryResult) Consumed() bool { return this.isConsumed } +func (this QueryResult) GetTableData() [][]map[string]string { return nil } - if this.Error != nil { - return "", this.Error - } +func (this *QueryResult) Message() (string, error) { - sb := strings.Builder{} + if this.Error != nil { + return "", this.Error + } - if 0 < len( *this.Query.Results ) { + sb := strings.Builder{} - // Print Stop Names, then print the list of routes - if this.Query.Key == "" { - busStops := map[string] *BusStop{} - for _, item := range *this.Query.Results { - var r *RouteStop - r = any( item ).( *RouteStop ) + if 0 < len(*this.Query.Results) { - b := r.BusStop - if b.Routes == nil { - continue - } + // Print Stop Names, then print the list of routes + if this.Query.Key == "" { + busStops := map[string]*BusStop{} + for _, item := range *this.Query.Results { + var r *RouteStop + r = any(item).(*RouteStop) - busStops[ b.BusStopId ] = b - } + b := r.BusStop + if b.Routes == nil { + continue + } - for _, b := range busStops { - utils.WriteMDv2Text( &sb, (*b.Name)[ this.Lang ] ) - sb.WriteString( "\n " ) - for _, route := range *b.Routes { - writeRouteHead( &sb, route ) - sb.WriteString( " " ) - } - sb.WriteString( "\n" ) - } + busStops[b.BusStopId] = b + } - // We got a route key - } else { + for _, b := range busStops { + utils.WriteMDv2Text(&sb, (*b.Name)[this.Lang]) + sb.WriteString("\n ") + for _, route := range *b.Routes { + writeRouteHead(&sb, route) + sb.WriteString(" ") + } + sb.WriteString("\n") + } - // We also got other search keys with 1 < Results - // Get the ETA for this stop - if 1 < len( *this.Query.SearchTerms ) { + // We got a route key + } else { - now := time.Now() - for _, item := range *this.Query.Results { - var r *RouteStop - r = any( item ).( *RouteStop ) - writeRouteHead( &sb, r ) - sb.WriteString( "\n" ) - writeShortRoute( &this.Lang, &sb, r ) + // We also got other search keys with 1 < Results + // Get the ETA for this stop + if 1 < len(*this.Query.SearchTerms) { - for _, schedule := range *(*this.Schedules)[ r ] { + now := time.Now() + for _, item := range *this.Query.Results { + var r *RouteStop + r = any(item).(*RouteStop) + writeRouteHead(&sb, r) + sb.WriteString("\n") + writeShortRoute(&this.Lang, &sb, r) - if !schedule.ETA.IsZero() { + for _, schedule := range *(*this.Schedules)[r] { - _m := schedule.ETA.Sub( now ).Minutes() + if !schedule.ETA.IsZero() { - sb.WriteString( " \\* " ) - txt := "%.0f min(s)" + _m := schedule.ETA.Sub(now).Minutes() - if this.Lang == "zh-Hant" { - txt = "%.0f 分鐘" - } + sb.WriteString(" \\* ") + txt := "%.0f min(s)" - utils.WriteMDv2Text( &sb, fmt.Sprintf( txt, _m ) ) + if this.Lang == "zh-Hant" { + txt = "%.0f 分鐘" + } - if _m < 0 { - sb.WriteString( " 走左了?" ) - } - } + utils.WriteMDv2Text(&sb, fmt.Sprintf(txt, _m)) - if schedule.Remarks_en != "" { - sb.WriteString( " \\*\\* " ) - switch this.Lang { - case "en": - utils.WriteMDv2Text( &sb, schedule.Remarks_en ) - case "zh-Hant": - utils.WriteMDv2Text( &sb, schedule.Remarks_tc ) - } - } + if _m < 0 { + sb.WriteString(" 走左了?") + } + } - sb.WriteString( "\n" ) - } + if schedule.Remarks_en != "" { + sb.WriteString(" \\*\\* ") + switch this.Lang { + case "en": + utils.WriteMDv2Text(&sb, schedule.Remarks_en) + case "zh-Hant": + utils.WriteMDv2Text(&sb, schedule.Remarks_tc) + } + } - sb.WriteString( "\n" ) - } + sb.WriteString("\n") + } - // We got only the route key, proceed to list the route stops - } else { - // Result contains all route stops, we only need the starting one - routes := [] *RouteStop{} + sb.WriteString("\n") + } - for _, item := range *this.Query.Results { - var r *RouteStop - r = any( item ).( *RouteStop ) - if r.PrevStop() == nil { - routes = append( routes, r ) - } - } + // We got only the route key, proceed to list the route stops + } else { + // Result contains all route stops, we only need the starting one + routes := []*RouteStop{} - sort.Sort( ByRoute( routes ) ) + for _, item := range *this.Query.Results { + var r *RouteStop + r = any(item).(*RouteStop) + if r.PrevStop() == nil { + routes = append(routes, r) + } + } - for _, r := range routes { - writeRouteHead( &sb, r ) - sb.WriteString( "\n" ) - for { - b := *r.BusStop - utils.WriteMDv2Text( &sb, (*b.Name)[ this.Lang ] ) - r = r.NextStop() - if r == nil { - break - } + sort.Sort(ByRoute(routes)) - sb.WriteString( " \\> " ) - } - sb.WriteString( "\n" ) - } + for _, r := range routes { + writeRouteHead(&sb, r) + sb.WriteString("\n") + for { + b := *r.BusStop + utils.WriteMDv2Text(&sb, (*b.Name)[this.Lang]) + r = r.NextStop() + if r == nil { + break + } + sb.WriteString(" \\> ") + } + sb.WriteString("\n") + } - } - } - } else { - return "", fmt.Errorf( "No Results" ) - } + } + } + } else { + return "", fmt.Errorf("No Results") + } - return sb.String(), nil + return sb.String(), nil } diff --git a/datasources/kmb/RouteStop.go b/datasources/kmb/RouteStop.go index 3a1a1f6..d9554a4 100644 --- a/datasources/kmb/RouteStop.go +++ b/datasources/kmb/RouteStop.go @@ -1,65 +1,66 @@ package kmb import ( - query "github.com/tgckpg/golifehk/query" + query "github.com/tgckpg/golifehk/query" ) type RouteStop struct { - BusStop *BusStop - RouteId string `json:"route"` - ServiceType string `json:"service_type"` - Direction string `json:"bound"` - StationSeq int `json:"seq,string"` - StationId string `json:"stop"` + BusStop *BusStop + RouteId string `json:"route"` + ServiceType string `json:"service_type"` + Direction string `json:"bound"` + StationSeq int `json:"seq,string"` + StationId string `json:"stop"` - RouteStops *map[int] *RouteStop - query.Searchable + RouteStops *map[int]*RouteStop + query.Words + query.NoGeoLocation } type RouteStops struct { - Type string `json:"type"` - Version string `json:"version"` - DateCreated string `json:"generated_timestamp"` - RouteStops [] *RouteStop `json:"data"` + Type string `json:"type"` + Version string `json:"version"` + DateCreated string `json:"generated_timestamp"` + RouteStops []*RouteStop `json:"data"` } -func ( routeStop RouteStop ) PrevStop() *RouteStop { - if v, hasKey := (*routeStop.RouteStops)[ routeStop.StationSeq - 1 ]; hasKey { - return v - } - return nil +func (routeStop RouteStop) PrevStop() *RouteStop { + if v, hasKey := (*routeStop.RouteStops)[routeStop.StationSeq-1]; hasKey { + return v + } + return nil } -func ( routeStop RouteStop ) NextStop() *RouteStop { - if v, hasKey := (*routeStop.RouteStops)[ routeStop.StationSeq + 1 ]; hasKey { - return v - } - return nil +func (routeStop RouteStop) NextStop() *RouteStop { + if v, hasKey := (*routeStop.RouteStops)[routeStop.StationSeq+1]; hasKey { + return v + } + return nil } -func ( this *RouteStop ) Reload() { +func (this *RouteStop) Reload() { - searchData := [] *string{} - busStop := *this.BusStop - searchData = append( searchData, &busStop.Name_en ) - searchData = append( searchData, &busStop.Name_tc ) + searchData := []*string{} + busStop := *this.BusStop + searchData = append(searchData, &busStop.Name_en) + searchData = append(searchData, &busStop.Name_tc) - this.Key = &this.RouteId - this.SearchData = &searchData + this.Key = &this.RouteId + this.SearchData = &searchData } -type ByRoute [] *RouteStop +type ByRoute []*RouteStop -func (a ByRoute) Len() int { return len(a) } -func (a ByRoute) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a ByRoute) Len() int { return len(a) } +func (a ByRoute) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a ByRoute) Less(i, j int) bool { - _a := *a[i] - _b := *a[j] - if _a.RouteId == _b.RouteId { - if _a.Direction == _b.Direction { - return _a.ServiceType < _b.ServiceType - } - return _a.Direction < _b.Direction - } - return _a.RouteId < _b.RouteId + _a := *a[i] + _b := *a[j] + if _a.RouteId == _b.RouteId { + if _a.Direction == _b.Direction { + return _a.ServiceType < _b.ServiceType + } + return _a.Direction < _b.Direction + } + return _a.RouteId < _b.RouteId } diff --git a/datasources/kmb/query.go b/datasources/kmb/query.go index 2ab81c0..c6b18d2 100644 --- a/datasources/kmb/query.go +++ b/datasources/kmb/query.go @@ -6,7 +6,10 @@ import ( "strings" ) -func Query(lang string, message string) query.IQueryResult { +func Query(q query.QueryMessage) query.IQueryResult { + + lang := q.Lang + message := q.Text var qo *query.QueryObject var err error @@ -18,7 +21,7 @@ func Query(lang string, message string) query.IQueryResult { goto qrReturn } - qo, err = query.Parse(strings.ToUpper(message), routeStops) + qo, err = query.MatchKeys(strings.ToUpper(message), routeStops) if err != nil { qr.Error = err goto qrReturn diff --git a/datasources/mtr/bus/BusStop.go b/datasources/mtr/bus/BusStop.go index c11fb52..d09f054 100644 --- a/datasources/mtr/bus/BusStop.go +++ b/datasources/mtr/bus/BusStop.go @@ -2,6 +2,7 @@ package bus import ( i18n "github.com/tgckpg/golifehk/i18n" + query "github.com/tgckpg/golifehk/query" ) type BusStop struct { @@ -10,8 +11,6 @@ type BusStop struct { Direction string StationSeq int StationId string - Latitude float64 - Longtitude float64 Name_zh string Name_en string @@ -22,6 +21,7 @@ type BusStop struct { AltRoutes *map[string]*BusStop i18n.Generics + query.GeoLocation } func (this *BusStop) PrevStop() *BusStop { @@ -52,6 +52,14 @@ func (this *BusStop) Reload() { this.SearchData = &searchData } +func (this BusStop) Register(registers map[string]struct{}) bool { + if _, ok := registers[this.StationId]; ok { + return false + } + registers[this.StationId] = struct{}{} + return true +} + type ByRoute []*BusStop func (a ByRoute) Len() int { return len(a) } diff --git a/datasources/mtr/bus/QueryResult.go b/datasources/mtr/bus/QueryResult.go index 9371715..0434b9d 100644 --- a/datasources/mtr/bus/QueryResult.go +++ b/datasources/mtr/bus/QueryResult.go @@ -14,7 +14,10 @@ type QueryResult struct { Schedules *map[*BusStop]*BusStopBuses Lang string Error error + Source *query.QueryMessage Query *query.QueryObject + + isConsumed bool } func writeShortRoute(lang *string, sb *strings.Builder, b *BusStop) { @@ -35,6 +38,10 @@ func writeShortRoute(lang *string, sb *strings.Builder, b *BusStop) { sb.WriteString("\n") } +func (this QueryResult) DataType() string { return "MarkdownV2" } +func (this QueryResult) Consumed() bool { return this.isConsumed } +func (this QueryResult) GetTableData() [][]map[string]string { return nil } + func (this QueryResult) Message() (string, error) { if this.Error != nil { @@ -56,12 +63,41 @@ func (this QueryResult) Message() (string, error) { } if q.Key == "" { - sort.Sort(query.ByKey(*q.Results)) - for _, entry := range *q.Results { - busStop := any(entry).(*BusStop) - utils.WriteMDv2Text(&sb, busStop.RouteId) - sb.WriteString(" ") - writeShortRoute(&this.Lang, &sb, busStop) + + loc := q.Message.Location + if loc != nil { + + // Print nearest bus stops + for _, entry := range *q.Results { + busStop := any(entry).(*BusStop) + + utils.WriteMDv2Text(&sb, fmt.Sprintf("%.2fm", busStop.Dist(loc.Lat(), loc.Lon()))) + sb.WriteString(" ") + sb.WriteString(" [") + utils.WriteMDv2Text(&sb, busStop.RouteId) + d := busStop.Direction + if d == "O" { + sb.WriteString("↑") + } else if d == "I" { + sb.WriteString("↓") + } else { + sb.WriteString("\\?") + } + utils.WriteMDv2Text(&sb, (*busStop.Name)[this.Lang]) + utils.WriteMDv2Text(&sb, busStop.RouteId) + utils.WriteMDv2Text(&sb, (*busStop.Name)[this.Lang]) + sb.WriteString(")") + sb.WriteString("\n") + } + + } else { + sort.Sort(query.ByKey(*q.Results)) + for _, entry := range *q.Results { + busStop := any(entry).(*BusStop) + utils.WriteMDv2Text(&sb, busStop.RouteId) + sb.WriteString(" ") + writeShortRoute(&this.Lang, &sb, busStop) + } } } else if 1 == len(*q.SearchTerms) { diff --git a/datasources/mtr/bus/busstops.go b/datasources/mtr/bus/busstops.go index da47e13..832e057 100644 --- a/datasources/mtr/bus/busstops.go +++ b/datasources/mtr/bus/busstops.go @@ -50,7 +50,7 @@ func readBusStopData(r io.Reader) (*map[string]*BusStop, error) { entry.Latitude = v case "STATION_LONGITUDE": v, _ := strconv.ParseFloat(value, 64) - entry.Longtitude = v + entry.Longitude = v case "STATION_NAME_CHI": entry.Name_zh = value case "STATION_NAME_ENG": diff --git a/datasources/mtr/bus/query.go b/datasources/mtr/bus/query.go index 5ce3e42..6778e6c 100644 --- a/datasources/mtr/bus/query.go +++ b/datasources/mtr/bus/query.go @@ -6,7 +6,9 @@ import ( query "github.com/tgckpg/golifehk/query" ) -func Query(lang string, message string) query.IQueryResult { +func Query(q query.QueryMessage) query.IQueryResult { + + lang := q.Lang var qBusStops *query.QueryObject var err error @@ -18,7 +20,14 @@ func Query(lang string, message string) query.IQueryResult { goto qrReturn } - qBusStops, err = query.Parse(strings.ToUpper(message), busStops) + if q.Text != "" { + qBusStops, err = query.MatchKeys(strings.ToUpper(q.Text), busStops) + } else if q.Location != nil { + qBusStops, err = query.MatchNearest(*q.Location, busStops, 100, 3) + } + + qBusStops.Message = &q + if err != nil { qr.Error = err goto qrReturn diff --git a/go.mod b/go.mod index 8406537..4c8ba31 100644 --- a/go.mod +++ b/go.mod @@ -7,3 +7,5 @@ require ( golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa golang.org/x/text v0.34.0 ) + +require github.com/tidwall/geodesic v1.52.4 // indirect diff --git a/go.sum b/go.sum index 6b356b9..49f8cdd 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= +github.com/tidwall/geodesic v1.52.4 h1:nT9cvYziVbmqFMDuvJzCJKvBJ9wFx0gRwvVrt86fpXg= +github.com/tidwall/geodesic v1.52.4/go.mod h1:SNL5vSG4X+o0ExTya69PX7/ZQ2SAvmjAxI+o5ZGJsxs= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= diff --git a/i18n/Generics.go b/i18n/Generics.go index ab9deb3..643d19b 100644 --- a/i18n/Generics.go +++ b/i18n/Generics.go @@ -1,10 +1,10 @@ package i18n import ( - query "github.com/tgckpg/golifehk/query" + query "github.com/tgckpg/golifehk/query" ) type Generics struct { - Name *map[string] string - query.Searchable + Name *map[string]string + query.Words } diff --git a/main.go b/main.go index 52f5814..2bf2ba7 100644 --- a/main.go +++ b/main.go @@ -6,6 +6,7 @@ import ( "os" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + tgadaptor "github.com/tgckpg/golifehk/adaptors/tg" cjlookup "github.com/tgckpg/golifehk/datasources/cjlookup" kmb "github.com/tgckpg/golifehk/datasources/kmb" mtrbus "github.com/tgckpg/golifehk/datasources/mtr/bus" @@ -13,15 +14,6 @@ import ( utils "github.com/tgckpg/golifehk/utils" ) -func botsend(bot *tgbotapi.BotAPI, update *tgbotapi.Update, mesg *string) { - var msg tgbotapi.MessageConfig - msg = tgbotapi.NewMessage(update.Message.Chat.ID, *mesg) - msg.ParseMode = "MarkdownV2" - - msg.ReplyToMessageID = update.Message.MessageID - bot.Send(msg) -} - func main() { bot, err := tgbotapi.NewBotAPI(os.Getenv("TELEGRAM_API_TOKEN")) if err != nil { @@ -48,12 +40,12 @@ func main() { mesg, processed := utils.SystemControl(update.Message) if processed { if mesg != "" { - botsend(bot, &update, &mesg) + tgadaptor.BotSendText(bot, &update, &mesg) } continue } - f_queries := []func(string, string) query.IQueryResult{ + f_queries := []func(query.QueryMessage) query.IQueryResult{ cjlookup.Query, mtrbus.Query, kmb.Query, @@ -61,21 +53,35 @@ func main() { var f_sent bool = false var f_err error = nil - for _, Query := range f_queries { - var err error - mesg, err = Query("zh-Hant", update.Message.Text).Message() - if err == nil { + tgMesg := update.Message + q := query.QueryMessage{Lang: "zh-Hant", Text: tgMesg.Text} + + if tgMesg.Location != nil { + q.Location = &query.GeoLocation{tgMesg.Location.Latitude, tgMesg.Location.Longitude} + } + + for _, Query := range f_queries { + + qResult := Query(q) + sent, err := tgadaptor.BotSend(bot, &update, qResult) + + if sent { f_sent = true - botsend(bot, &update, &mesg) - } else if f_err == nil { + } + + if err != nil { f_err = err } + + if qResult.Consumed() { + break + } } if !isGroup && !f_sent && f_err != nil { mesg := utils.MDv2Text(fmt.Sprintf("%s", f_err)) - botsend(bot, &update, &mesg) + tgadaptor.BotSendText(bot, &update, &mesg) } } } diff --git a/query/GeoLocation.go b/query/GeoLocation.go new file mode 100644 index 0000000..db8f654 --- /dev/null +++ b/query/GeoLocation.go @@ -0,0 +1,59 @@ +package query + +import ( + "github.com/tidwall/geodesic" + "sort" +) + +type GeoLocation struct { + Latitude float64 + Longitude float64 +} + +func (b GeoLocation) HasGeoLocation() bool { return true } + +func (b GeoLocation) Lat() float64 { return b.Latitude } +func (b GeoLocation) Lon() float64 { return b.Longitude } + +func (b GeoLocation) Dist(lat float64, lon float64) float64 { + var dist float64 + geodesic.WGS84.Inverse( + lat, lon, + b.Latitude, b.Longitude, + &dist, nil, nil, + ) + return dist +} + +type NoGeoLocation struct{} + +func (b NoGeoLocation) HasGeoLocation() bool { return false } +func (b NoGeoLocation) Lat() float64 { return 0 } +func (b NoGeoLocation) Lon() float64 { return 0 } +func (b NoGeoLocation) Dist(lat, lon float64) float64 { return 0 } +func (b NoGeoLocation) Register(map[string]struct{}) bool { + return false +} + +type GeoLocations []ISearchable + +func (m GeoLocations) SortByNearest(p GeoLocation) { + sort.Slice(m, func(i, j int) bool { + return m[i].Dist(p.Lat(), p.Lon()) < m[j].Dist(p.Lat(), p.Lon()) + }) +} + +func (b GeoLocation) Register(map[string]struct{}) bool { + return false +} + +func (m GeoLocations) Clean() *GeoLocations { + registers := map[string]struct{}{} + items := &GeoLocations{} + for _, item := range m { + if item.HasGeoLocation() && item.Register(registers) { + *items = append(*items, item) + } + } + return items +} diff --git a/query/IQueryResult.go b/query/IQueryResult.go deleted file mode 100644 index 1b30c8e..0000000 --- a/query/IQueryResult.go +++ /dev/null @@ -1,5 +0,0 @@ -package query - -type IQueryResult interface { - Message() ( string, error ) -} diff --git a/query/Searchable.go b/query/Searchable.go index 6706ba8..c72550c 100644 --- a/query/Searchable.go +++ b/query/Searchable.go @@ -1,40 +1,15 @@ package query -import ( - "strings" -) - type ISearchable interface { - Test( string ) bool - GetKey() *string + HasWords() bool + Test(string) bool + GetKey() *string + + HasGeoLocation() bool + Lat() float64 + Lon() float64 + Dist(lat float64, lon float64) float64 + + // Clean() filtering + Register(map[string]struct{}) bool } - -type Searchable struct { - Key *string - SearchData *[] *string -} - -func ( this *Searchable ) Test( val string ) bool { - - data := this.SearchData - if data == nil { - return false - } - - for _, v := range *data { - if strings.Contains( *v, val ) { - return true - } - } - return false -} - -func ( this *Searchable ) GetKey() *string { - return this.Key -} - -type ByKey [] ISearchable - -func (a ByKey) Len() int { return len(a) } -func (a ByKey) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a ByKey) Less(i, j int) bool { return *a[i].GetKey() < *a[j].GetKey() } diff --git a/query/Words.go b/query/Words.go new file mode 100644 index 0000000..bc5c12f --- /dev/null +++ b/query/Words.go @@ -0,0 +1,43 @@ +package query + +import ( + "strings" +) + +type Words struct { + Key *string + SearchData *[]*string +} + +func (this *Words) HasWords() bool { return true } + +func (this *Words) Test(val string) bool { + + data := this.SearchData + if data == nil { + return false + } + + for _, v := range *data { + if strings.Contains(*v, val) { + return true + } + } + return false +} + +func (this *Words) GetKey() *string { + return this.Key +} + +type ByKey []ISearchable + +func (a ByKey) Len() int { return len(a) } +func (a ByKey) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a ByKey) Less(i, j int) bool { return *a[i].GetKey() < *a[j].GetKey() } + +type NoWords struct{} + +func (this NoWords) HasWords() bool { return false } +func (this NoWords) Test(val string) bool { return false } +func (this NoWords) GetKey() *string { return nil } diff --git a/query/geo_test.go b/query/geo_test.go new file mode 100644 index 0000000..8c159fd --- /dev/null +++ b/query/geo_test.go @@ -0,0 +1,33 @@ +package query + +import ( + "fmt" + "testing" +) + +type City struct { + Name string + NoWords + GeoLocation +} + +func TestGeo(t *testing.T) { + + B := GeoLocations{ + City{Name: "Osaka", GeoLocation: GeoLocation{34.6937, 135.5023}}, + City{Name: "Nagoya", GeoLocation: GeoLocation{35.1815, 136.9066}}, + City{Name: "Sapporo", GeoLocation: GeoLocation{43.0618, 141.3545}}, + City{Name: "Yokohama", GeoLocation: GeoLocation{35.4437, 139.6380}}, + } + + // Reference point A (Tokyo) + A := GeoLocation{35.6895, 139.6917} + + // Sort by distance + B.SortByNearest(A) + + // Print result + for _, loc := range B { + fmt.Printf("%s: %.2f km\n", loc.(City).Name, loc.Dist(A.Lat(), A.Lon())/1000) + } +} diff --git a/query/match_keys.go b/query/match_keys.go new file mode 100644 index 0000000..7c5af38 --- /dev/null +++ b/query/match_keys.go @@ -0,0 +1,69 @@ +package query + +import ( + "fmt" + "strings" +) + +func MatchKeys(line string, entries *[]ISearchable) (*QueryObject, error) { + + var Key string = "" + + // Sanitize and assume properties for each of the keywords + terms := []*QTerm{} + for _, val := range strings.Split(line, " ") { + + if val == "" { + continue + } + + term := QTerm{ + Org: val, + Value: strings.Trim(val, " "), + } + terms = append(terms, &term) + } + +lookForKey: + for _, term := range terms { + for _, entry := range *entries { + if term.Value == *entry.GetKey() { + Key = term.Value + term.IsKey = true + break lookForKey + } + } + } + + matches := []ISearchable{} + + if Key != "" && len(terms) == 1 { + for _, entry := range *entries { + if Key == *entry.GetKey() { + matches = append(matches, entry) + } + } + return &QueryObject{Key: Key, Results: &matches, SearchTerms: &terms}, nil + } else if 0 < len(terms) { + for _, entry := range *entries { + anyFailed := false + for _, term := range terms { + if term.IsKey { + continue + } + + if !((Key == "" || Key == *entry.GetKey()) && entry.Test(term.Value)) { + anyFailed = true + break + } + } + + if !anyFailed { + matches = append(matches, entry) + } + } + return &QueryObject{Key: Key, Results: &matches, SearchTerms: &terms}, nil + } + + return nil, fmt.Errorf("Cannot parse: %s", line) +} diff --git a/query/match_locations.go b/query/match_locations.go new file mode 100644 index 0000000..7fe39f0 --- /dev/null +++ b/query/match_locations.go @@ -0,0 +1,29 @@ +package query + +import ( + "fmt" +) + +func MatchNearest(p GeoLocation, entries *[]ISearchable, dist float64, limit int) (*QueryObject, error) { + + terms := []*QTerm{ + { + Org: fmt.Sprintf("%f, %f", p.Lat(), p.Lon()), + Value: "", + }, + } + + var locs *GeoLocations + + locs = GeoLocations(*entries).Clean() + locs.SortByNearest(p) + + matches := []ISearchable{} + for i, loc := range *locs { + if i < limit { + matches = append(matches, loc) + } + } + + return &QueryObject{Key: "", Results: &matches, SearchTerms: &terms}, nil +} diff --git a/query/objects.go b/query/objects.go new file mode 100644 index 0000000..bcbe66a --- /dev/null +++ b/query/objects.go @@ -0,0 +1,27 @@ +package query + +type QTerm struct { + Org string + Value string + IsKey bool +} + +type QueryMessage struct { + Lang string + Text string + Location *GeoLocation +} + +type QueryObject struct { + Key string + Message *QueryMessage + SearchTerms *[]*QTerm + Results *[]ISearchable +} + +type IQueryResult interface { + Message() (string, error) + DataType() string + GetTableData() [][]map[string]string + Consumed() bool +} diff --git a/query/query.go b/query/query.go deleted file mode 100644 index 640744b..0000000 --- a/query/query.go +++ /dev/null @@ -1,81 +0,0 @@ -package query - -import ( - "fmt" - "strings" -) - -type QTerm struct { - Org string - Value string - IsKey bool -} - -type QueryObject struct { - Key string - SearchTerms *[] *QTerm - Results *[] ISearchable -} - -func Parse( line string, entries *[] ISearchable ) ( *QueryObject, error ) { - - var Key string = "" - - // Sanitize and assume properties for each of the keywords - terms := [] *QTerm{} - for _, val := range strings.Split( line, " " ) { - - if val == "" { - continue - } - - term := QTerm{ - Org: val, - Value: strings.Trim( val, " " ), - } - terms = append( terms, &term ) - } - - lookForKey: - for _, term := range terms { - for _, entry := range *entries { - if term.Value == *entry.GetKey() { - Key = term.Value - term.IsKey = true - break lookForKey - } - } - } - - matches := [] ISearchable{} - - if Key != "" && len( terms ) == 1 { - for _, entry := range *entries { - if Key == *entry.GetKey() { - matches = append( matches, entry ) - } - } - return &QueryObject{ Key: Key, Results: &matches, SearchTerms: &terms }, nil - } else if 0 < len( terms ) { - for _, entry := range *entries { - anyFailed := false - for _, term := range terms { - if term.IsKey { - continue - } - - if !( ( Key == "" || Key == *entry.GetKey() ) && entry.Test( term.Value ) ) { - anyFailed = true - break - } - } - - if !anyFailed { - matches = append( matches, entry ) - } - } - return &QueryObject{ Key: Key, Results: &matches, SearchTerms: &terms }, nil - } - - return nil, fmt.Errorf( "Cannot parse: %s", line ) -} diff --git a/query/query_test.go b/query/query_test.go index 56c19be..df2c853 100644 --- a/query/query_test.go +++ b/query/query_test.go @@ -5,75 +5,76 @@ import ( ) type TestItem struct { - Searchable + Words + NoGeoLocation } -func TestQuery( t *testing.T ) { - testItems := [] ISearchable{} +func TestQuery(t *testing.T) { + testItems := []ISearchable{} - s1234 := "1234" - sApple := "Apple" - s0000 := "0000" + s1234 := "1234" + sApple := "Apple" + s0000 := "0000" - sDat0 := "Dat0" - data0 := [] *string{} - data0 = append( data0, &s1234 ) - data0 = append( data0, &sApple ) - t0 := TestItem{} - t0.Key = &sDat0 - t0.SearchData = &data0 - testItems = append( testItems, &t0 ) + sDat0 := "Dat0" + data0 := []*string{} + data0 = append(data0, &s1234) + data0 = append(data0, &sApple) + t0 := TestItem{} + t0.Key = &sDat0 + t0.SearchData = &data0 + testItems = append(testItems, &t0) - sDat1 := "Dat1" - data1 := [] *string{} - data1 = append( data1, &s0000 ) - data1 = append( data1, &sApple ) - t1 := TestItem{} - t1.Key = &sDat1 - t1.SearchData = &data1 - testItems = append( testItems, &t1 ) + sDat1 := "Dat1" + data1 := []*string{} + data1 = append(data1, &s0000) + data1 = append(data1, &sApple) + t1 := TestItem{} + t1.Key = &sDat1 + t1.SearchData = &data1 + testItems = append(testItems, &t1) - sDat2 := "Dat2" - data2 := [] *string{} - data2 = append( data2, &sDat0 ) - t2 := TestItem{} - t2.Key = &sDat2 - t2.SearchData = &data2 - testItems = append( testItems, &t2 ) + sDat2 := "Dat2" + data2 := []*string{} + data2 = append(data2, &sDat0) + t2 := TestItem{} + t2.Key = &sDat2 + t2.SearchData = &data2 + testItems = append(testItems, &t2) - qo, err := Parse( "Apple", &testItems ) - if err != nil { - t.Error( err ) - } + qo, err := MatchKeys("Apple", &testItems) + if err != nil { + t.Error(err) + } - if len(*qo.Results) != 2 { - t.Error( "Expected 2 results when searching for \"Apple\"" ) - } + if len(*qo.Results) != 2 { + t.Error("Expected 2 results when searching for \"Apple\"") + } - qo, err = Parse( "0000", &testItems ) - if err != nil { - t.Error( err ) - } + qo, err = MatchKeys("0000", &testItems) + if err != nil { + t.Error(err) + } - if len(*qo.Results) != 1 { - t.Error( "Expected 1 results when searching for \"0000\"" ) - } + if len(*qo.Results) != 1 { + t.Error("Expected 1 results when searching for \"0000\"") + } - qo, err = Parse( "Dat0", &testItems ) - if err != nil { - t.Error( err ) - } + qo, err = MatchKeys("Dat0", &testItems) + if err != nil { + t.Error(err) + } - if len(*qo.Results) != 1 { - t.Error( "Expected 1 results when searching for \"Dat0\"" ) - } + if len(*qo.Results) != 1 { + t.Error("Expected 1 results when searching for \"Dat0\"") + } - qo, err = Parse( "Dat2 Dat0", &testItems ) - if err != nil { - t.Error( err ) - } + qo, err = MatchKeys("Dat2 Dat0", &testItems) + if err != nil { + t.Error(err) + } - if len(*qo.Results) != 1 { - t.Error( "Expected 1 results when searching for \"Dat2 Dat0\"" ) - } + if len(*qo.Results) != 1 { + t.Error("Expected 1 results when searching for \"Dat2 Dat0\"") + } } diff --git a/utils/system.go b/utils/system.go index a76cc75..73e4642 100644 --- a/utils/system.go +++ b/utils/system.go @@ -1,96 +1,100 @@ package utils import ( - "bytes" - "encoding/json" - "fmt" - "io" - "log" - "path/filepath" - "strings" - "time" - tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + "bytes" + "encoding/json" + "fmt" + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + "io" + "log" + "path/filepath" + "strings" + "time" ) -var JSON_SETTINGS string = filepath.Join( WORKDIR, "settings.json" ) +var JSON_SETTINGS string = filepath.Join(WORKDIR, "settings.json") -var settingsTime = time.Unix( 0, 0 ) +var settingsTime = time.Unix(0, 0) type SysSettings struct { - IgnoredChats map[int64] bool `json:"IgnoredChats"` + IgnoredChats map[int64]bool `json:"IgnoredChats"` } -var Settings = SysSettings{ IgnoredChats: map[int64] bool{} } +var Settings = SysSettings{IgnoredChats: map[int64]bool{}} func rwSettings() { - buff, err := ChangedStream( JSON_SETTINGS, writeSettings, settingsTime ) - if err != nil { - log.Panic( err ) - return - } - - conf := SysSettings{} - err = json.Unmarshal( buff.Bytes(), &conf ) - if err != nil { - log.Panic( err ) - return - } - - Settings = conf -} - -func writeSettings() ( io.Reader, error ) { - b, err := json.Marshal( Settings ) - if err != nil { - return nil, err - } - return bytes.NewBuffer( b ), nil -} - -func SystemControl( tgMesg *tgbotapi.Message ) ( string, bool ) { - - rwSettings() - - processed := false - mesg := "" - - if tgMesg.Text[0] == '/' { - processed = true - } - - chatId := tgMesg.Chat.ID - - if Settings.IgnoredChats == nil { - Settings.IgnoredChats = map[int64] bool{} - } - - if strings.Contains( tgMesg.Text, "/golifehk disable" ) { - mesg = fmt.Sprintf( "OK" ) - Settings.IgnoredChats[ chatId ] = true - processed = true - } - - if strings.Contains( tgMesg.Text, "/golifehk enable" ) { - mesg = fmt.Sprintf( "OK" ) - Settings.IgnoredChats[ chatId ] = false - processed = true - } - - if processed { - settingsTime = time.Now() - rwSettings() - } - - //// Begin processing settings - - // ignore chats if enabled - if ignore, ok := Settings.IgnoredChats[ chatId ]; ok { - processed = processed || ignore; - } else { - // default ignore - processed = true; + buff, err := ChangedStream(JSON_SETTINGS, writeSettings, settingsTime) + if err != nil { + log.Panic(err) + return } - return mesg, processed + conf := SysSettings{} + err = json.Unmarshal(buff.Bytes(), &conf) + if err != nil { + log.Panic(err) + return + } + + Settings = conf +} + +func writeSettings() (io.Reader, error) { + b, err := json.Marshal(Settings) + if err != nil { + return nil, err + } + return bytes.NewBuffer(b), nil +} + +func SystemControl(tgMesg *tgbotapi.Message) (string, bool) { + + rwSettings() + + processed := false + mesg := "" + + if tgMesg.Text == "" { + return "", false + } + + if tgMesg.Text[0] == '/' { + processed = true + } + + chatId := tgMesg.Chat.ID + + if Settings.IgnoredChats == nil { + Settings.IgnoredChats = map[int64]bool{} + } + + if strings.Contains(tgMesg.Text, "/golifehk disable") { + mesg = fmt.Sprintf("OK") + Settings.IgnoredChats[chatId] = true + processed = true + } + + if strings.Contains(tgMesg.Text, "/golifehk enable") { + mesg = fmt.Sprintf("OK") + Settings.IgnoredChats[chatId] = false + processed = true + } + + if processed { + settingsTime = time.Now() + rwSettings() + } + + //// Begin processing settings + + // ignore chats if enabled + if ignore, ok := Settings.IgnoredChats[chatId]; ok { + processed = processed || ignore + } else { + // default ignore + processed = true + } + + return mesg, processed }