Can now accept location

This commit is contained in:
2026-03-08 15:38:28 +08:00
parent 912f9fd0ad
commit 33a7c04e09
12 changed files with 286 additions and 148 deletions

View File

@@ -1,6 +1,7 @@
package tg package tg
import ( import (
"fmt"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
query "github.com/tgckpg/golifehk/query" query "github.com/tgckpg/golifehk/query"
) )
@@ -32,25 +33,37 @@ func BotSend(bot *tgbotapi.BotAPI, update *tgbotapi.Update, qResult query.IQuery
return false, nil 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" msg.ParseMode = "MarkdownV2"
switch mesgType { switch mesgType {
case "PlainText": case "PlainText":
case "Table": case "Table":
button := tgbotapi.NewInlineKeyboardButtonData( buttonRows := [][]tgbotapi.InlineKeyboardButton{}
"Show Status", // what user sees for _, row := range qResult.GetTableData() {
"status_cmd", // what bot receives buttons := []tgbotapi.InlineKeyboardButton{}
) for _, cell := range row {
button := tgbotapi.NewInlineKeyboardButtonData(cell.Name, cell.Value)
msg.ReplyMarkup = tgbotapi.NewInlineKeyboardMarkup( buttons = append(buttons, button)
tgbotapi.NewInlineKeyboardRow(button, button), fmt.Println(cell)
) }
buttonRows = append(buttonRows, buttons)
}
msg.ReplyMarkup = tgbotapi.NewInlineKeyboardMarkup(buttonRows...)
} }
msg.ReplyToMessageID = update.Message.MessageID
bot.Send(msg) bot.Send(msg)
return true, nil return true, nil
} }

View File

@@ -40,9 +40,9 @@ func writeCCharInfo(sb *strings.Builder, cc *CChar) {
} }
} }
func (this QueryResult) DataType() string { return this.ResultType } func (this QueryResult) DataType() string { return this.ResultType }
func (this QueryResult) Consumed() bool { return this.isConsumed } func (this QueryResult) Consumed() bool { return this.isConsumed }
func (this QueryResult) GetTableData() [][]map[string]string { return nil } func (this QueryResult) GetTableData() [][]query.TableCell { return nil }
func (this QueryResult) Message() (string, error) { func (this QueryResult) Message() (string, error) {

View File

@@ -1,40 +1,52 @@
package kmb package kmb
import ( 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 { type BusStop struct {
BusStopId string `json:"stop"` BusStopId string
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"`
// Routes[ Route ][ Direction ] // Routes[ Route ][ Direction ]
Routes *[] *RouteStop Routes *[]*RouteStop
i18n.Generics i18n.Generics
query.GeoLocation
} }
type BusStops struct { type BusStopsJson struct {
Type string `json:"type"` Type string `json:"type"`
Version string `json:"version"` Version string `json:"version"`
DateCreated string `json:"generated_timestamp"` DateCreated string `json:"generated_timestamp"`
BusStops [] *BusStop `json:"data"` BusStops []*BusStopJson `json:"data"`
} }
func ( this *BusStop ) Reload() { func (b BusStop) Register(registers map[string]struct{}) bool {
i18n_Name := map[string] string{} if _, ok := registers[b.BusStopId]; ok {
i18n_Name["en"] = this.Name_en return false
i18n_Name["zh-Hant"] = this.Name_tc }
registers[b.BusStopId] = struct{}{}
searchData := [] *string{} return true
searchData = append( searchData, &this.Name_en ) }
searchData = append( searchData, &this.Name_tc )
func (this *BusStop) Reload() {
this.Name = &i18n_Name searchData := []*string{}
this.Key = &this.BusStopId searchData = append(searchData, &this.BusStopId)
this.SearchData = &searchData
for _, v := range *this.Name {
searchData = append(searchData, &v)
}
this.Key = &this.BusStopId
this.SearchData = &searchData
} }

View File

@@ -17,6 +17,8 @@ type QueryResult struct {
Query *query.QueryObject Query *query.QueryObject
isConsumed bool isConsumed bool
dataType string
tableData [][]query.TableCell
} }
func writeRouteHead(sb *strings.Builder, r *RouteStop) { func writeRouteHead(sb *strings.Builder, r *RouteStop) {
@@ -50,12 +52,14 @@ func writeShortRoute(lang *string, sb *strings.Builder, r *RouteStop) {
sb.WriteString("\n") sb.WriteString("\n")
} }
func (this QueryResult) DataType() string { return "MarkdownV2" } func (this QueryResult) DataType() string { return this.dataType }
func (this QueryResult) Consumed() bool { return this.isConsumed } func (this QueryResult) Consumed() bool { return this.isConsumed }
func (this QueryResult) GetTableData() [][]map[string]string { return nil } 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 { if this.Error != nil {
return "", this.Error return "", this.Error
} }
@@ -66,27 +70,61 @@ func (this *QueryResult) Message() (string, error) {
// Print Stop Names, then print the list of routes // Print Stop Names, then print the list of routes
if this.Query.Key == "" { if this.Query.Key == "" {
busStops := map[string]*BusStop{} loc := this.Query.Message.Location
for _, item := range *this.Query.Results { if loc != nil {
var r *RouteStop sb.WriteString("九巴 100m")
r = any(item).(*RouteStop) this.dataType = "Table"
b := r.BusStop table := [][]query.TableCell{}
if b.Routes == nil {
continue 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 _, b := range busStops { for _, route := range *b.Routes {
utils.WriteMDv2Text(&sb, (*b.Name)[this.Lang]) writeRouteHead(&sb, route)
sb.WriteString("\n ") sb.WriteString(" ")
for _, route := range *b.Routes { }
writeRouteHead(&sb, route) sb.WriteString("\n")
sb.WriteString(" ")
} }
sb.WriteString("\n")
} }
// We got a route key // We got a route key

View File

@@ -40,13 +40,8 @@ func (routeStop RouteStop) NextStop() *RouteStop {
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)
this.Key = &this.RouteId this.Key = &this.RouteId
this.SearchData = &searchData this.SearchData = this.BusStop.SearchData
} }
type ByRoute []*RouteStop type ByRoute []*RouteStop

View File

@@ -9,19 +9,39 @@ import (
func Query(q query.QueryMessage) query.IQueryResult { func Query(q query.QueryMessage) query.IQueryResult {
lang := q.Lang lang := q.Lang
message := q.Text
var qo *query.QueryObject var qo *query.QueryObject
var err error var err error
var routeStops *[]query.ISearchable
qr := QueryResult{Lang: lang} qr := QueryResult{Lang: lang}
routeStops, err := getRouteStops()
busStops, err := readBusStopsData()
if err != nil { if err != nil {
qr.Error = err qr.Error = err
goto qrReturn 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 { if err != nil {
qr.Error = err qr.Error = err
goto qrReturn goto qrReturn

View File

@@ -32,7 +32,6 @@ func readRouteStopsData(busStops *map[string]*BusStop, buff *bytes.Buffer) (*[]*
if busStop == nil { if busStop == nil {
busStop = &BusStop{ busStop = &BusStop{
BusStopId: entry.StationId, BusStopId: entry.StationId,
Name_en: "???", Name_tc: "???", Name_sc: "???",
} }
busStop.Reload() busStop.Reload()
@@ -76,37 +75,15 @@ func readRouteStopsData(busStops *map[string]*BusStop, buff *bytes.Buffer) (*[]*
return &allRouteStops, nil return &allRouteStops, nil
} }
func readBusStopsData(buff *bytes.Buffer) (*map[string]*BusStop, error) { func readBusStopsData() (*map[string]*BusStop, error) {
busStopsData := BusStops{} busStopsData := BusStopsJson{}
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
QUERY_FUNC := func() (io.ReadCloser, error) { QUERY_FUNC := func() (io.ReadCloser, error) {
return utils.HttpGet("https://data.etabus.gov.hk/v1/transport/kmb/stop") return utils.HttpGet("https://data.etabus.gov.hk/v1/transport/kmb/stop")
} }
PARSE_FUNC := func(buff *bytes.Buffer) error { PARSE_FUNC := func(buff *bytes.Buffer) error {
var err error err := json.Unmarshal(buff.Bytes(), &busStopsData)
busStopMap, err = readBusStopsData(buff)
return err return err
} }
@@ -120,18 +97,49 @@ func getRouteStops() (*[]query.ISearchable, error) {
return nil, err 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 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") 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 var err error
routeStops, err = readRouteStopsData(busStopMap, buff) routeStops, err = readRouteStopsData(busStopMap, buff)
return err return err
} }
cs, err = utils.CacheStreamEx(JSON_ROUTESTOPS, QUERY_FUNC) cs, err := utils.CacheStreamEx(JSON_ROUTESTOPS, QUERY_FUNC)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -18,6 +18,8 @@ type QueryResult struct {
Query *query.QueryObject Query *query.QueryObject
isConsumed bool isConsumed bool
dataType string
tableData [][]query.TableCell
} }
func writeShortRoute(lang *string, sb *strings.Builder, b *BusStop) { 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") sb.WriteString("\n")
} }
func (this QueryResult) DataType() string { return "MarkdownV2" } func (this QueryResult) DataType() string { return this.dataType }
func (this QueryResult) Consumed() bool { return this.isConsumed } func (this QueryResult) Consumed() bool { return this.isConsumed }
func (this QueryResult) GetTableData() [][]map[string]string { return nil } 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 { if this.Error != nil {
return "", this.Error return "", this.Error
@@ -67,29 +71,40 @@ func (this QueryResult) Message() (string, error) {
loc := q.Message.Location loc := q.Message.Location
if loc != nil { if loc != nil {
// Print nearest bus stops this.dataType = "Table"
sb.WriteString("K巴 100m")
table := [][]query.TableCell{}
for _, entry := range *q.Results { for _, entry := range *q.Results {
busStop := any(entry).(*BusStop) busStop := any(entry).(*BusStop)
utils.WriteMDv2Text(&sb, fmt.Sprintf("%.2fm", busStop.Dist(loc.Lat(), loc.Lon()))) sb_i := strings.Builder{}
sb.WriteString(" ") sb_i.WriteString(fmt.Sprintf("%.2fm", busStop.Dist(loc.Lat(), loc.Lon())))
sb.WriteString(" [") sb_i.WriteString(" ")
utils.WriteMDv2Text(&sb, busStop.RouteId) utils.WriteMDv2Text(&sb_i, busStop.RouteId)
d := busStop.Direction d := busStop.Direction
if d == "O" { if d == "O" {
sb.WriteString("↑") sb_i.WriteString("↑")
} else if d == "I" { } else if d == "I" {
sb.WriteString("↓") sb_i.WriteString("↓")
} else { } else {
sb.WriteString("\\?") sb_i.WriteString("\\?")
} }
utils.WriteMDv2Text(&sb, (*busStop.Name)[this.Lang]) sb_i.WriteString(" ")
utils.WriteMDv2Text(&sb, busStop.RouteId) utils.WriteMDv2Text(&sb_i, (*busStop.Name)[this.Lang])
utils.WriteMDv2Text(&sb, (*busStop.Name)[this.Lang])
sb.WriteString(")") row := []query.TableCell{
sb.WriteString("\n") 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 { } else {
sort.Sort(query.ByKey(*q.Results)) sort.Sort(query.ByKey(*q.Results))
for _, entry := range *q.Results { for _, entry := range *q.Results {

74
main.go
View File

@@ -30,6 +30,22 @@ func main() {
updates := bot.GetUpdatesChan(u) updates := bot.GetUpdatesChan(u)
for update := range updates { 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 { if update.Message == nil {
continue continue
} }
@@ -45,15 +61,6 @@ func main() {
continue 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 tgMesg := update.Message
q := query.QueryMessage{Lang: "zh-Hant", Text: tgMesg.Text} 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} q.Location = &query.GeoLocation{tgMesg.Location.Latitude, tgMesg.Location.Longitude}
} }
for _, Query := range f_queries { f_sent, f_err := processQuery(bot, &update, q)
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
}
}
if !isGroup && !f_sent && f_err != nil { if !isGroup && !f_sent && f_err != nil {
mesg := utils.MDv2Text(fmt.Sprintf("%s", f_err)) 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
}

View File

@@ -5,6 +5,14 @@ import (
"sort" "sort"
) )
type IGeoLocation interface {
HasGeoLocation() bool
Lat() float64
Lon() float64
Dist(lat, lon float64) float64
Register(map[string]struct{}) bool
}
type GeoLocation struct { type GeoLocation struct {
Latitude float64 Latitude float64
Longitude float64 Longitude float64
@@ -19,7 +27,7 @@ func (b GeoLocation) Dist(lat float64, lon float64) float64 {
var dist float64 var dist float64
geodesic.WGS84.Inverse( geodesic.WGS84.Inverse(
lat, lon, lat, lon,
b.Latitude, b.Longitude, b.Lat(), b.Lon(),
&dist, nil, nil, &dist, nil, nil,
) )
return dist return dist
@@ -37,13 +45,14 @@ func (b NoGeoLocation) Register(map[string]struct{}) bool {
type GeoLocations []ISearchable type GeoLocations []ISearchable
func (m GeoLocations) SortByNearest(p GeoLocation) { func (m GeoLocations) SortByNearest(p IGeoLocation) {
sort.Slice(m, func(i, j int) bool { sort.Slice(m, func(i, j int) bool {
return m[i].Dist(p.Lat(), p.Lon()) < m[j].Dist(p.Lat(), p.Lon()) return m[i].Dist(p.Lat(), p.Lon()) < m[j].Dist(p.Lat(), p.Lon())
}) })
} }
func (b GeoLocation) Register(map[string]struct{}) bool { func (b GeoLocation) Register(map[string]struct{}) bool {
panic("GeoLocation: Default is called")
return false return false
} }

View File

@@ -4,7 +4,7 @@ import (
"fmt" "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{ terms := []*QTerm{
{ {
@@ -19,9 +19,10 @@ func MatchNearest(p GeoLocation, entries *[]ISearchable, dist float64, limit int
locs.SortByNearest(p) locs.SortByNearest(p)
matches := []ISearchable{} matches := []ISearchable{}
for i, loc := range *locs { for i, item := range *locs {
if i < limit { loc := item.(IGeoLocation)
matches = append(matches, loc) if i < limit && loc.Dist(p.Lat(), p.Lon()) <= dist {
matches = append(matches, item)
} }
} }

View File

@@ -19,9 +19,14 @@ type QueryObject struct {
Results *[]ISearchable Results *[]ISearchable
} }
type TableCell struct {
Name string
Value string
}
type IQueryResult interface { type IQueryResult interface {
Message() (string, error) Message() (string, error)
DataType() string DataType() string
GetTableData() [][]map[string]string GetTableData() [][]TableCell
Consumed() bool Consumed() bool
} }