From 33a7c04e0980d21dd27610f97e372503388b8c43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=9F=E9=85=8C=20=E9=B5=AC=E5=85=84?= Date: Sun, 8 Mar 2026 15:38:28 +0800 Subject: [PATCH] Can now accept location --- adaptors/tg/botsend.go | 33 ++++++++---- datasources/cjlookup/QueryResult.go | 6 +-- datasources/kmb/BusStop.go | 66 ++++++++++++++---------- datasources/kmb/QueryResult.go | 78 +++++++++++++++++++++-------- datasources/kmb/RouteStop.go | 7 +-- datasources/kmb/query.go | 26 ++++++++-- datasources/kmb/routestops.go | 66 +++++++++++++----------- datasources/mtr/bus/QueryResult.go | 49 +++++++++++------- main.go | 74 +++++++++++++++++---------- query/GeoLocation.go | 13 ++++- query/match_locations.go | 9 ++-- query/objects.go | 7 ++- 12 files changed, 286 insertions(+), 148 deletions(-) diff --git a/adaptors/tg/botsend.go b/adaptors/tg/botsend.go index 9885d8f..8229da2 100644 --- a/adaptors/tg/botsend.go +++ b/adaptors/tg/botsend.go @@ -1,6 +1,7 @@ package tg import ( + "fmt" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" query "github.com/tgckpg/golifehk/query" ) @@ -32,25 +33,37 @@ func BotSend(bot *tgbotapi.BotAPI, update *tgbotapi.Update, qResult query.IQuery return false, nil } - msg = tgbotapi.NewMessage(update.Message.Chat.ID, mesg) + var chatId int64 + if update.Message != nil { + chatId = update.Message.Chat.ID + msg.ReplyToMessageID = update.Message.MessageID + } + + if update.CallbackQuery != nil { + chatId = update.CallbackQuery.Message.Chat.ID + } + + msg = tgbotapi.NewMessage(chatId, 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), - ) + buttonRows := [][]tgbotapi.InlineKeyboardButton{} + for _, row := range qResult.GetTableData() { + buttons := []tgbotapi.InlineKeyboardButton{} + for _, cell := range row { + button := tgbotapi.NewInlineKeyboardButtonData(cell.Name, cell.Value) + buttons = append(buttons, button) + fmt.Println(cell) + } + buttonRows = append(buttonRows, buttons) + } + msg.ReplyMarkup = tgbotapi.NewInlineKeyboardMarkup(buttonRows...) } - msg.ReplyToMessageID = update.Message.MessageID bot.Send(msg) return true, nil } diff --git a/datasources/cjlookup/QueryResult.go b/datasources/cjlookup/QueryResult.go index 16a9272..976583c 100644 --- a/datasources/cjlookup/QueryResult.go +++ b/datasources/cjlookup/QueryResult.go @@ -40,9 +40,9 @@ 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) DataType() string { return this.ResultType } +func (this QueryResult) Consumed() bool { return this.isConsumed } +func (this QueryResult) GetTableData() [][]query.TableCell { return nil } func (this QueryResult) Message() (string, error) { diff --git a/datasources/kmb/BusStop.go b/datasources/kmb/BusStop.go index 0019705..79b7ffe 100644 --- a/datasources/kmb/BusStop.go +++ b/datasources/kmb/BusStop.go @@ -1,40 +1,52 @@ package kmb import ( - i18n "github.com/tgckpg/golifehk/i18n" + i18n "github.com/tgckpg/golifehk/i18n" + query "github.com/tgckpg/golifehk/query" ) +type BusStopJson struct { + BusStopId string `json:"stop"` + Latitude float64 `json:"lat,string"` + Longitude float64 `json:"long,string"` + Name_en string `json:"name_en"` + Name_tc string `json:"name_tc"` + Name_sc string `json:"name_sc"` +} + type BusStop struct { - BusStopId string `json:"stop"` - Latitude float64 `json:"lat,string"` - Longtitude float64 `json:"long,string"` - Name_en string `json:"name_en"` - Name_tc string `json:"name_tc"` - Name_sc string `json:"name_sc"` + BusStopId string - // Routes[ Route ][ Direction ] - Routes *[] *RouteStop + // Routes[ Route ][ Direction ] + Routes *[]*RouteStop - i18n.Generics + i18n.Generics + query.GeoLocation } -type BusStops struct { - Type string `json:"type"` - Version string `json:"version"` - DateCreated string `json:"generated_timestamp"` - BusStops [] *BusStop `json:"data"` +type BusStopsJson struct { + Type string `json:"type"` + Version string `json:"version"` + DateCreated string `json:"generated_timestamp"` + BusStops []*BusStopJson `json:"data"` } -func ( this *BusStop ) Reload() { - i18n_Name := map[string] string{} - i18n_Name["en"] = this.Name_en - i18n_Name["zh-Hant"] = this.Name_tc - - searchData := [] *string{} - searchData = append( searchData, &this.Name_en ) - searchData = append( searchData, &this.Name_tc ) - - this.Name = &i18n_Name - this.Key = &this.BusStopId - this.SearchData = &searchData +func (b BusStop) Register(registers map[string]struct{}) bool { + if _, ok := registers[b.BusStopId]; ok { + return false + } + registers[b.BusStopId] = struct{}{} + return true +} + +func (this *BusStop) Reload() { + searchData := []*string{} + searchData = append(searchData, &this.BusStopId) + + for _, v := range *this.Name { + searchData = append(searchData, &v) + } + + this.Key = &this.BusStopId + this.SearchData = &searchData } diff --git a/datasources/kmb/QueryResult.go b/datasources/kmb/QueryResult.go index a1bdce0..730c411 100644 --- a/datasources/kmb/QueryResult.go +++ b/datasources/kmb/QueryResult.go @@ -17,6 +17,8 @@ type QueryResult struct { Query *query.QueryObject isConsumed bool + dataType string + tableData [][]query.TableCell } func writeRouteHead(sb *strings.Builder, r *RouteStop) { @@ -50,12 +52,14 @@ func writeShortRoute(lang *string, sb *strings.Builder, r *RouteStop) { 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) DataType() string { return this.dataType } +func (this QueryResult) Consumed() bool { return this.isConsumed } +func (this QueryResult) GetTableData() [][]query.TableCell { return this.tableData } func (this *QueryResult) Message() (string, error) { + this.dataType = "PlainText" + if this.Error != nil { return "", this.Error } @@ -66,27 +70,61 @@ func (this *QueryResult) Message() (string, error) { // 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) + loc := this.Query.Message.Location + if loc != nil { + sb.WriteString("九巴 100m") + this.dataType = "Table" - b := r.BusStop - if b.Routes == nil { - continue + table := [][]query.TableCell{} + + for _, item := range *this.Query.Results { + b := any(item).(*BusStop) + + row := []query.TableCell{} + + cell := query.TableCell{ + Name: fmt.Sprintf("%.2fm %s", b.Dist(loc.Lat(), loc.Lon()), (*b.Name)[this.Lang]), + Value: fmt.Sprintf("%s", b.BusStopId), + } + row = append(row, cell) + + for _, r := range *b.Routes { + sb_i := strings.Builder{} + writeRouteHead(&sb_i, r) + cell := query.TableCell{ + Name: sb_i.String(), + Value: fmt.Sprintf("%s %s", r.RouteId, (*b.Name)[this.Lang]), + } + row = append(row, cell) + } + + table = append(table, row) + } + this.tableData = table + + } else { + busStops := map[string]*BusStop{} + for _, item := range *this.Query.Results { + var r *RouteStop + r = any(item).(*RouteStop) + + b := r.BusStop + if b.Routes == nil { + continue + } + + busStops[b.BusStopId] = b } - busStops[b.BusStopId] = b - } - - for _, b := range busStops { - utils.WriteMDv2Text(&sb, (*b.Name)[this.Lang]) - sb.WriteString("\n ") - for _, route := range *b.Routes { - writeRouteHead(&sb, route) - sb.WriteString(" ") + 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") } - sb.WriteString("\n") } // We got a route key diff --git a/datasources/kmb/RouteStop.go b/datasources/kmb/RouteStop.go index d9554a4..6e2ec8c 100644 --- a/datasources/kmb/RouteStop.go +++ b/datasources/kmb/RouteStop.go @@ -40,13 +40,8 @@ func (routeStop RouteStop) NextStop() *RouteStop { func (this *RouteStop) Reload() { - 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.SearchData = this.BusStop.SearchData } type ByRoute []*RouteStop diff --git a/datasources/kmb/query.go b/datasources/kmb/query.go index c6b18d2..ba04adc 100644 --- a/datasources/kmb/query.go +++ b/datasources/kmb/query.go @@ -9,19 +9,39 @@ import ( func Query(q query.QueryMessage) query.IQueryResult { lang := q.Lang - message := q.Text var qo *query.QueryObject var err error + var routeStops *[]query.ISearchable qr := QueryResult{Lang: lang} - routeStops, err := getRouteStops() + + busStops, err := readBusStopsData() if err != nil { qr.Error = err goto qrReturn } - qo, err = query.MatchKeys(strings.ToUpper(message), routeStops) + routeStops, err = getRouteStops(busStops) + if err != nil { + qr.Error = err + goto qrReturn + } + + if q.Text != "" { + qo, err = query.MatchKeys(strings.ToUpper(q.Text), routeStops) + } else if q.Location != nil { + bList := []query.ISearchable{} + + for _, b := range *busStops { + bList = append(bList, b) + } + + qo, err = query.MatchNearest(*q.Location, &bList, 100, 3) + } + + qo.Message = &q + if err != nil { qr.Error = err goto qrReturn diff --git a/datasources/kmb/routestops.go b/datasources/kmb/routestops.go index 2bfb63a..1e0dcc0 100644 --- a/datasources/kmb/routestops.go +++ b/datasources/kmb/routestops.go @@ -32,7 +32,6 @@ func readRouteStopsData(busStops *map[string]*BusStop, buff *bytes.Buffer) (*[]* if busStop == nil { busStop = &BusStop{ BusStopId: entry.StationId, - Name_en: "???", Name_tc: "???", Name_sc: "???", } busStop.Reload() @@ -76,37 +75,15 @@ func readRouteStopsData(busStops *map[string]*BusStop, buff *bytes.Buffer) (*[]* return &allRouteStops, nil } -func readBusStopsData(buff *bytes.Buffer) (*map[string]*BusStop, error) { - busStopsData := BusStops{} - err := json.Unmarshal(buff.Bytes(), &busStopsData) - if err != nil { - return nil, err - } - - busStopMap := map[string]*BusStop{} - for _, entry := range busStopsData.BusStops { - - entry.Reload() - - if _, ok := busStopMap[entry.BusStopId]; ok { - return nil, fmt.Errorf("Duplicated BusStop: %s", entry.BusStopId) - } - - busStopMap[entry.BusStopId] = entry - } - return &busStopMap, nil -} - -func getRouteStops() (*[]query.ISearchable, error) { - var busStopMap *map[string]*BusStop +func readBusStopsData() (*map[string]*BusStop, error) { + busStopsData := BusStopsJson{} QUERY_FUNC := func() (io.ReadCloser, error) { return utils.HttpGet("https://data.etabus.gov.hk/v1/transport/kmb/stop") } PARSE_FUNC := func(buff *bytes.Buffer) error { - var err error - busStopMap, err = readBusStopsData(buff) + err := json.Unmarshal(buff.Bytes(), &busStopsData) return err } @@ -120,18 +97,49 @@ func getRouteStops() (*[]query.ISearchable, error) { return nil, err } + busStopMap := map[string]*BusStop{} + for _, entry := range busStopsData.BusStops { + + b := BusStop{ + BusStopId: entry.BusStopId, + } + + b.Latitude = entry.Latitude + b.Longitude = entry.Longitude + + n := map[string]string{ + "en": entry.Name_en, + "zh-Hant": entry.Name_tc, + "zh-Hans": entry.Name_sc, + } + + b.Name = &n + + b.Reload() + + if _, ok := busStopMap[b.BusStopId]; ok { + return nil, fmt.Errorf("Duplicated BusStop: %s", b.BusStopId) + } + + busStopMap[b.BusStopId] = &b + } + return &busStopMap, nil +} + +func getRouteStops(busStopMap *map[string]*BusStop) (*[]query.ISearchable, error) { var routeStops *[]*RouteStop - QUERY_FUNC = func() (io.ReadCloser, error) { + + QUERY_FUNC := func() (io.ReadCloser, error) { return utils.HttpGet("https://data.etabus.gov.hk/v1/transport/kmb/route-stop") } - PARSE_FUNC = func(buff *bytes.Buffer) error { + PARSE_FUNC := func(buff *bytes.Buffer) error { var err error routeStops, err = readRouteStopsData(busStopMap, buff) return err } - cs, err = utils.CacheStreamEx(JSON_ROUTESTOPS, QUERY_FUNC) + cs, err := utils.CacheStreamEx(JSON_ROUTESTOPS, QUERY_FUNC) if err != nil { return nil, err } diff --git a/datasources/mtr/bus/QueryResult.go b/datasources/mtr/bus/QueryResult.go index 0434b9d..87556aa 100644 --- a/datasources/mtr/bus/QueryResult.go +++ b/datasources/mtr/bus/QueryResult.go @@ -18,6 +18,8 @@ type QueryResult struct { Query *query.QueryObject isConsumed bool + dataType string + tableData [][]query.TableCell } func writeShortRoute(lang *string, sb *strings.Builder, b *BusStop) { @@ -38,11 +40,13 @@ 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) DataType() string { return this.dataType } +func (this QueryResult) Consumed() bool { return this.isConsumed } +func (this QueryResult) GetTableData() [][]query.TableCell { return this.tableData } -func (this QueryResult) Message() (string, error) { +func (this *QueryResult) Message() (string, error) { + + this.dataType = "PlainText" if this.Error != nil { return "", this.Error @@ -67,29 +71,40 @@ func (this QueryResult) Message() (string, error) { loc := q.Message.Location if loc != nil { - // Print nearest bus stops + this.dataType = "Table" + sb.WriteString("K巴 100m") + + table := [][]query.TableCell{} + 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) + sb_i := strings.Builder{} + sb_i.WriteString(fmt.Sprintf("%.2fm", busStop.Dist(loc.Lat(), loc.Lon()))) + sb_i.WriteString(" ") + utils.WriteMDv2Text(&sb_i, busStop.RouteId) d := busStop.Direction if d == "O" { - sb.WriteString("↑") + sb_i.WriteString("↑") } else if d == "I" { - sb.WriteString("↓") + sb_i.WriteString("↓") } else { - sb.WriteString("\\?") + sb_i.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") + sb_i.WriteString(" ") + utils.WriteMDv2Text(&sb_i, (*busStop.Name)[this.Lang]) + + row := []query.TableCell{ + query.TableCell{ + Name: sb_i.String(), + Value: fmt.Sprintf("%s %s", busStop.RouteId, (*busStop.Name)[this.Lang]), + }, + } + + table = append(table, row) } + this.tableData = table } else { sort.Sort(query.ByKey(*q.Results)) for _, entry := range *q.Results { diff --git a/main.go b/main.go index 2bf2ba7..f32b85f 100644 --- a/main.go +++ b/main.go @@ -30,6 +30,22 @@ func main() { updates := bot.GetUpdatesChan(u) for update := range updates { + + if update.CallbackQuery != nil { + callback := tgbotapi.NewCallback(update.CallbackQuery.ID, "") + bot.Request(callback) + + q := query.QueryMessage{Lang: "zh-Hant", Text: update.CallbackQuery.Data} + f_sent, f_err := processQuery(bot, &update, q) + + if !f_sent && f_err != nil { + mesg := utils.MDv2Text(fmt.Sprintf("%s", f_err)) + tgadaptor.BotSendText(bot, &update, &mesg) + } + + continue + } + if update.Message == nil { continue } @@ -45,15 +61,6 @@ func main() { continue } - f_queries := []func(query.QueryMessage) query.IQueryResult{ - cjlookup.Query, - mtrbus.Query, - kmb.Query, - } - - var f_sent bool = false - var f_err error = nil - tgMesg := update.Message q := query.QueryMessage{Lang: "zh-Hant", Text: tgMesg.Text} @@ -61,23 +68,7 @@ func main() { 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 - } - - if err != nil { - f_err = err - } - - if qResult.Consumed() { - break - } - } + f_sent, f_err := processQuery(bot, &update, q) if !isGroup && !f_sent && f_err != nil { mesg := utils.MDv2Text(fmt.Sprintf("%s", f_err)) @@ -85,3 +76,34 @@ func main() { } } } + +func processQuery(bot *tgbotapi.BotAPI, update *tgbotapi.Update, q query.QueryMessage) (bool, error) { + var f_sent bool = false + var f_err error = nil + + f_queries := []func(query.QueryMessage) query.IQueryResult{ + cjlookup.Query, + mtrbus.Query, + kmb.Query, + } + + for _, Query := range f_queries { + + qResult := Query(q) + sent, err := tgadaptor.BotSend(bot, update, qResult) + + if sent { + f_sent = true + } + + if err != nil { + f_err = err + } + + if qResult.Consumed() { + break + } + } + + return f_sent, f_err +} diff --git a/query/GeoLocation.go b/query/GeoLocation.go index db8f654..30c5800 100644 --- a/query/GeoLocation.go +++ b/query/GeoLocation.go @@ -5,6 +5,14 @@ import ( "sort" ) +type IGeoLocation interface { + HasGeoLocation() bool + Lat() float64 + Lon() float64 + Dist(lat, lon float64) float64 + Register(map[string]struct{}) bool +} + type GeoLocation struct { Latitude float64 Longitude float64 @@ -19,7 +27,7 @@ func (b GeoLocation) Dist(lat float64, lon float64) float64 { var dist float64 geodesic.WGS84.Inverse( lat, lon, - b.Latitude, b.Longitude, + b.Lat(), b.Lon(), &dist, nil, nil, ) return dist @@ -37,13 +45,14 @@ func (b NoGeoLocation) Register(map[string]struct{}) bool { type GeoLocations []ISearchable -func (m GeoLocations) SortByNearest(p GeoLocation) { +func (m GeoLocations) SortByNearest(p IGeoLocation) { 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 { + panic("GeoLocation: Default is called") return false } diff --git a/query/match_locations.go b/query/match_locations.go index 7fe39f0..96f07ad 100644 --- a/query/match_locations.go +++ b/query/match_locations.go @@ -4,7 +4,7 @@ import ( "fmt" ) -func MatchNearest(p GeoLocation, entries *[]ISearchable, dist float64, limit int) (*QueryObject, error) { +func MatchNearest(p IGeoLocation, entries *[]ISearchable, dist float64, limit int) (*QueryObject, error) { terms := []*QTerm{ { @@ -19,9 +19,10 @@ func MatchNearest(p GeoLocation, entries *[]ISearchable, dist float64, limit int locs.SortByNearest(p) matches := []ISearchable{} - for i, loc := range *locs { - if i < limit { - matches = append(matches, loc) + for i, item := range *locs { + loc := item.(IGeoLocation) + if i < limit && loc.Dist(p.Lat(), p.Lon()) <= dist { + matches = append(matches, item) } } diff --git a/query/objects.go b/query/objects.go index bcbe66a..50120e0 100644 --- a/query/objects.go +++ b/query/objects.go @@ -19,9 +19,14 @@ type QueryObject struct { Results *[]ISearchable } +type TableCell struct { + Name string + Value string +} + type IQueryResult interface { Message() (string, error) DataType() string - GetTableData() [][]map[string]string + GetTableData() [][]TableCell Consumed() bool }