penguin/golifehk

@golifehk bot in Telegram

commit 912f9fd0ad210e5fd22cd3cb640cf109e49b86dd

author斟酌 鵬兄 <tgckpg@gmail.com>
date2026-03-07T14:16:14Z
subjectRefactoring codes for more tg message types
commit 912f9fd0ad210e5fd22cd3cb640cf109e49b86dd
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date:   2026-03-07T14:16:14Z

    Refactoring codes for more tg message types
---
 adaptors/tg/botsend.go              |  56 +++++++
 datasources/cjlookup/CChar.go       |   6 +-
 datasources/cjlookup/QueryResult.go |  11 +-
 datasources/cjlookup/query.go       |  14 +-
 datasources/kmb/QueryResult.go      | 319 ++++++++++++++++++------------------
 datasources/kmb/RouteStop.go        |  85 +++++-----
 datasources/kmb/query.go            |   7 +-
 datasources/mtr/bus/BusStop.go      |  12 +-
 datasources/mtr/bus/QueryResult.go  |  48 +++++-
 datasources/mtr/bus/busstops.go     |   2 +-
 datasources/mtr/bus/query.go        |  13 +-
 go.mod                              |   2 +
 go.sum                              |   2 +
 i18n/Generics.go                    |   6 +-
 main.go                             |  40 +++--
 query/GeoLocation.go                |  59 +++++++
 query/IQueryResult.go               |   5 -
 query/Searchable.go                 |  43 +----
 query/Words.go                      |  43 +++++
 query/geo_test.go                   |  33 ++++
 query/match_keys.go                 |  69 ++++++++
 query/match_locations.go            |  29 ++++
 query/objects.go                    |  27 +++
 query/query.go                      |  81 ---------
 query/query_test.go                 | 115 ++++++-------
 utils/system.go                     | 128 ++++++++-------
 26 files changed, 777 insertions(+), 478 deletions(-)

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 }
+
+func (this *QueryResult) Message() (string, error) {
 
-    if this.Error != nil {
-        return "", this.Error
-    }
-
-    sb := strings.Builder{}
-
-    if 0 < len( *this.Query.Results ) {
-
-        // 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 )
-
-                b := r.BusStop
-                if b.Routes == nil {
-                    continue
-                }
-
-                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( " " )
-                }
-                sb.WriteString( "\n" )
-            }
-
-        // We got a route key
-        } else {
-
-            // We also got other search keys with 1 < Results
-            // Get the ETA for this stop
-            if 1 < len( *this.Query.SearchTerms )  {
-
-                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 )
-
-                    for _, schedule := range *(*this.Schedules)[ r ] {
-
-                        if !schedule.ETA.IsZero() {
-
-                            _m := schedule.ETA.Sub( now ).Minutes()
-
-                            sb.WriteString( "  \\* " )
-                            txt := "%.0f min(s)"
-
-                            if this.Lang == "zh-Hant" {
-                                txt = "%.0f 分鐘"
-                            }
-
-                            utils.WriteMDv2Text( &sb, fmt.Sprintf( txt, _m ) )
-
-                            if _m < 0 {
-                                sb.WriteString( " 走左了?" )
-                            }
-                        }
-
-                        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{}
-
-                for _, item := range *this.Query.Results {
-                    var r *RouteStop
-                    r = any( item ).( *RouteStop )
-                    if r.PrevStop() == nil {
-                        routes = append( routes, r )
-                    }
-                }
-
-                sort.Sort( ByRoute( routes ) )
-
-                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" )
-    }
-
-    return sb.String(), nil
+	if this.Error != nil {
+		return "", this.Error
+	}
+
+	sb := strings.Builder{}
+
+	if 0 < len(*this.Query.Results) {
+
+		// 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)
+
+				b := r.BusStop
+				if b.Routes == nil {
+					continue
+				}
+
+				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(" ")
+				}
+				sb.WriteString("\n")
+			}
+
+			// We got a route key
+		} else {
+
+			// We also got other search keys with 1 < Results
+			// Get the ETA for this stop
+			if 1 < len(*this.Query.SearchTerms) {
+
+				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)
+
+					for _, schedule := range *(*this.Schedules)[r] {
+
+						if !schedule.ETA.IsZero() {
+
+							_m := schedule.ETA.Sub(now).Minutes()
+
+							sb.WriteString("  \\* ")
+							txt := "%.0f min(s)"
+
+							if this.Lang == "zh-Hant" {
+								txt = "%.0f 分鐘"
+							}
+
+							utils.WriteMDv2Text(&sb, fmt.Sprintf(txt, _m))
+
+							if _m < 0 {
+								sb.WriteString(" 走左了?")
+							}
+						}
+
+						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{}
+
+				for _, item := range *this.Query.Results {
+					var r *RouteStop
+					r = any(item).(*RouteStop)
+					if r.PrevStop() == nil {
+						routes = append(routes, r)
+					}
+				}
+
+				sort.Sort(ByRoute(routes))
+
+				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")
+	}
+
+	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
+
+		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 {
-			var err error
-			mesg, err = Query("zh-Hant", update.Message.Text).Message()
 
-			if err == nil {
+			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
-}
-
-type Searchable struct {
-    Key *string
-    SearchData *[] *string
-}
-
-func ( this *Searchable ) Test( val string ) bool {
+	HasWords() bool
+	Test(string) bool
+	GetKey() *string
 
-    data := this.SearchData
-    if data == nil {
-        return false
-    }
+	HasGeoLocation() bool
+	Lat() float64
+	Lon() float64
+	Dist(lat float64, lon float64) float64
 
-    for _, v := range *data {
-        if strings.Contains( *v, val ) {
-            return true
-        }
-    }
-    return false
+	// Clean() filtering
+	Register(map[string]struct{}) bool
 }
-
-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
-    }
+	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
-    }
+	conf := SysSettings{}
+	err = json.Unmarshal(buff.Bytes(), &conf)
+	if err != nil {
+		log.Panic(err)
+		return
+	}
 
-    Settings = conf
+	Settings = conf
 }
 
-func writeSettings() ( io.Reader, error ) {
-    b, err := json.Marshal( Settings )
-    if err != nil {
-        return nil, err
-    }
-    return bytes.NewBuffer( b ), nil
+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 ) {
+func SystemControl(tgMesg *tgbotapi.Message) (string, bool) {
 
-    rwSettings()
+	rwSettings()
 
-    processed := false
-    mesg := ""
+	processed := false
+	mesg := ""
+
+	if tgMesg.Text == "" {
+		return "", false
+	}
 
-    if tgMesg.Text[0] == '/' {
-        processed = true
-    }
+	if tgMesg.Text[0] == '/' {
+		processed = true
+	}
 
-    chatId := tgMesg.Chat.ID
+	chatId := tgMesg.Chat.ID
 
-    if Settings.IgnoredChats == nil {
-        Settings.IgnoredChats = map[int64] bool{}
-    }
+	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 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 strings.Contains(tgMesg.Text, "/golifehk enable") {
+		mesg = fmt.Sprintf("OK")
+		Settings.IgnoredChats[chatId] = false
+		processed = true
+	}
 
-    if processed {
-        settingsTime = time.Now()
-        rwSettings()
-    }
+	if processed {
+		settingsTime = time.Now()
+		rwSettings()
+	}
 
-    //// Begin processing settings
+	//// Begin processing settings
 
-    // ignore chats if enabled
-	if ignore, ok := Settings.IgnoredChats[ chatId ]; ok {
-        processed = processed || ignore;
+	// ignore chats if enabled
+	if ignore, ok := Settings.IgnoredChats[chatId]; ok {
+		processed = processed || ignore
 	} else {
 		// default ignore
-		processed = true;
+		processed = true
 	}
 
-    return mesg, processed
+	return mesg, processed
 }