Compare commits
26 Commits
afb73b70ff
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e94adbed4 | |||
| 0ed4c618dc | |||
| c22cdac1cb | |||
| 4efd346ce1 | |||
| e4275f60b7 | |||
| 726916ed45 | |||
| 91cc543fe4 | |||
| 446847a7a8 | |||
| 55d4ac4adc | |||
| a062e37a4c | |||
| 35b303f796 | |||
| 41be1db381 | |||
| b251e35be4 | |||
| 7fd8d4fb6a | |||
| 3376d9eb96 | |||
| 292665c49b | |||
| 25ccd64a88 | |||
| 67e3ec1500 | |||
| 89770c0864 | |||
| 9724abe2e2 | |||
| 79a873becd | |||
| f022b237ef | |||
| 877ef47dc9 | |||
| 619a99a6dd | |||
| 9b48f2ee50 | |||
| 534bc70f0f |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
|||||||
|
.DS_Store
|
||||||
|
./golifehk
|
||||||
*.csv
|
*.csv
|
||||||
*.json
|
*.json
|
||||||
*.swp
|
*.swp
|
||||||
|
|||||||
23
Dockerfile
Normal file
23
Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
FROM golang:1.19-alpine AS build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY [ "go.mod", "go.sum", "./" ]
|
||||||
|
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY "datasources" "./datasources/"
|
||||||
|
COPY "utils" "./utils/"
|
||||||
|
COPY "query" "./query/"
|
||||||
|
COPY "i18n" "./i18n/"
|
||||||
|
COPY *.go ./
|
||||||
|
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /golifehkbot
|
||||||
|
|
||||||
|
FROM scratch
|
||||||
|
|
||||||
|
COPY --from=alpine:latest /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||||
|
COPY --from=build /golifehkbot /
|
||||||
|
|
||||||
|
WORKDIR /
|
||||||
|
CMD [ "/golifehkbot" ]
|
||||||
24
concourse/build.yaml
Normal file
24
concourse/build.yaml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
platform: linux
|
||||||
|
|
||||||
|
image_resource:
|
||||||
|
type: registry-image
|
||||||
|
source:
|
||||||
|
repository: concourse/oci-build-task
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
- name: project-src
|
||||||
|
path: .
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
- name: image
|
||||||
|
|
||||||
|
caches:
|
||||||
|
- path: cache
|
||||||
|
|
||||||
|
params:
|
||||||
|
CONTEXT: .
|
||||||
|
# Reference: https://concourse-ci.org/building-an-image-and-using-it-in-a-task.html#build-the-image
|
||||||
|
UNPACK_ROOTFS: true
|
||||||
|
|
||||||
|
run:
|
||||||
|
path: build
|
||||||
21
concourse/prepare-deployment-resources.yaml
Normal file
21
concourse/prepare-deployment-resources.yaml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
platform: linux
|
||||||
|
|
||||||
|
image_resource:
|
||||||
|
type: registry-image
|
||||||
|
source:
|
||||||
|
repository: alpine
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
- name: project-src
|
||||||
|
path: .
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
- name: deploy-confs
|
||||||
|
|
||||||
|
run:
|
||||||
|
path: sh
|
||||||
|
args:
|
||||||
|
- -exc
|
||||||
|
- |
|
||||||
|
VERSION=$( cat commit.sha )
|
||||||
|
sed "s/IMAGE_TAG/$VERSION/g" k8s/deployments.yaml >> deploy-confs/prod.yaml
|
||||||
40
datasources/kmb/BusStop.go
Normal file
40
datasources/kmb/BusStop.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package kmb
|
||||||
|
|
||||||
|
import (
|
||||||
|
i18n "github.com/tgckpg/golifehk/i18n"
|
||||||
|
)
|
||||||
|
|
||||||
|
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"`
|
||||||
|
|
||||||
|
// Routes[ Route ][ Direction ]
|
||||||
|
Routes *[] *RouteStop
|
||||||
|
|
||||||
|
i18n.Generics
|
||||||
|
}
|
||||||
|
|
||||||
|
type BusStops struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
DateCreated string `json:"generated_timestamp"`
|
||||||
|
BusStops [] *BusStop `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
|
||||||
|
}
|
||||||
176
datasources/kmb/QueryResult.go
Normal file
176
datasources/kmb/QueryResult.go
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
package kmb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ) {
|
||||||
|
|
||||||
|
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( "*" )
|
||||||
|
|
||||||
|
if r.NextStop() != nil {
|
||||||
|
sb.WriteString( " \\> " )
|
||||||
|
utils.WriteMDv2Text( sb, (*(r.NextStop().BusStop).Name)[ *lang ] )
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.WriteString( "\n" )
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
65
datasources/kmb/RouteStop.go
Normal file
65
datasources/kmb/RouteStop.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package kmb
|
||||||
|
|
||||||
|
import (
|
||||||
|
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"`
|
||||||
|
|
||||||
|
RouteStops *map[int] *RouteStop
|
||||||
|
query.Searchable
|
||||||
|
}
|
||||||
|
|
||||||
|
type RouteStops struct {
|
||||||
|
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 ) NextStop() *RouteStop {
|
||||||
|
if v, hasKey := (*routeStop.RouteStops)[ routeStop.StationSeq + 1 ]; hasKey {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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) 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
|
||||||
|
}
|
||||||
49
datasources/kmb/query.go
Normal file
49
datasources/kmb/query.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package kmb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
query "github.com/tgckpg/golifehk/query"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Query( lang string, message string ) query.IQueryResult {
|
||||||
|
|
||||||
|
var qo *query.QueryObject
|
||||||
|
var err error
|
||||||
|
|
||||||
|
qr := QueryResult{ Lang: lang }
|
||||||
|
routeStops, err := getRouteStops()
|
||||||
|
if err != nil {
|
||||||
|
qr.Error = err
|
||||||
|
goto qrReturn
|
||||||
|
}
|
||||||
|
|
||||||
|
qo, err = query.Parse( strings.ToUpper( message ), routeStops )
|
||||||
|
if err != nil {
|
||||||
|
qr.Error = err
|
||||||
|
goto qrReturn
|
||||||
|
}
|
||||||
|
|
||||||
|
qr.Query = qo
|
||||||
|
|
||||||
|
if 0 < len( *qo.Results ) && 1 < len( *qo.SearchTerms ) {
|
||||||
|
|
||||||
|
rSchedules := map[*RouteStop] *[] *Schedule{}
|
||||||
|
for _, item := range *qo.Results {
|
||||||
|
var r *RouteStop
|
||||||
|
r = any( item ).( *RouteStop )
|
||||||
|
schedules, err := getSchedule( r )
|
||||||
|
if err != nil {
|
||||||
|
qr.Error = err
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
rSchedules[r] = schedules
|
||||||
|
}
|
||||||
|
qr.Schedules = &rSchedules
|
||||||
|
}
|
||||||
|
|
||||||
|
qrReturn:
|
||||||
|
var iqr query.IQueryResult
|
||||||
|
iqr = &qr
|
||||||
|
return iqr
|
||||||
|
}
|
||||||
36
datasources/kmb/query_test.go
Normal file
36
datasources/kmb/query_test.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package kmb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestQuerySchedule( t *testing.T ) {
|
||||||
|
qo := Query( "zh-Hant", "68X" )
|
||||||
|
mesg, err := qo.Message()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf( "Unexpected Error: %s", err )
|
||||||
|
}
|
||||||
|
fmt.Println( mesg )
|
||||||
|
|
||||||
|
qo = Query( "zh-Hant", "K66 朗屏" )
|
||||||
|
mesg, err = qo.Message()
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf( "Expected Error: %s, got \"\" instead", mesg )
|
||||||
|
}
|
||||||
|
|
||||||
|
qo = Query( "zh-Hant", "大欖" )
|
||||||
|
mesg, err = qo.Message()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf( "Unexpected Error: %s", err )
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println( mesg )
|
||||||
|
|
||||||
|
qo = Query( "zh-Hant", "261B 大欖" )
|
||||||
|
mesg, err = qo.Message()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf( "Unexpected Error: %s", err )
|
||||||
|
}
|
||||||
|
fmt.Println( mesg )
|
||||||
|
}
|
||||||
147
datasources/kmb/routestops.go
Normal file
147
datasources/kmb/routestops.go
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
package kmb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
// "strings"
|
||||||
|
|
||||||
|
query "github.com/tgckpg/golifehk/query"
|
||||||
|
"github.com/tgckpg/golifehk/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
var JSON_ROUTESTOPS string = filepath.Join( utils.WORKDIR, "kmb-routestops.json" )
|
||||||
|
var JSON_BUSSTOPS string = filepath.Join( utils.WORKDIR, "kmb-busstops.json" )
|
||||||
|
|
||||||
|
func readRouteStopsData( busStops *map[string] *BusStop, buff *bytes.Buffer ) ( *[] *RouteStop, error ) {
|
||||||
|
|
||||||
|
routeStopsData := RouteStops{}
|
||||||
|
err := json.Unmarshal( buff.Bytes(), &routeStopsData )
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// routeStops[ Route ][ ServiceType ][ Direction ][ Seq ] = RouteStop
|
||||||
|
routeStops := map[string] *map[string] *map[ string ] *map[ int ] *RouteStop{}
|
||||||
|
allRouteStops := [] *RouteStop{}
|
||||||
|
|
||||||
|
for _, entry := range routeStopsData.RouteStops {
|
||||||
|
|
||||||
|
busStop := (*busStops)[ entry.StationId ]
|
||||||
|
if busStop == nil {
|
||||||
|
busStop = &BusStop {
|
||||||
|
BusStopId: entry.StationId,
|
||||||
|
Name_en: "???", Name_tc: "???", Name_sc: "???",
|
||||||
|
}
|
||||||
|
busStop.Reload()
|
||||||
|
|
||||||
|
(*busStops)[ entry.StationId ] = busStop
|
||||||
|
}
|
||||||
|
|
||||||
|
if busStop.Routes == nil {
|
||||||
|
busStopRoutes := [] *RouteStop{}
|
||||||
|
busStop.Routes = &busStopRoutes
|
||||||
|
}
|
||||||
|
|
||||||
|
(*busStop.Routes) = append( (*busStop.Routes), entry )
|
||||||
|
entry.BusStop = busStop
|
||||||
|
|
||||||
|
route := routeStops[ entry.RouteId ]
|
||||||
|
if route == nil {
|
||||||
|
route = &map[string] *map[ string ] *map[ int ] *RouteStop{}
|
||||||
|
routeStops[ entry.RouteId ] = route
|
||||||
|
}
|
||||||
|
|
||||||
|
service := (*route)[ entry.ServiceType ]
|
||||||
|
if service == nil {
|
||||||
|
service = &map[ string ] *map[ int ] *RouteStop{}
|
||||||
|
(*route)[ entry.ServiceType ] = service
|
||||||
|
}
|
||||||
|
|
||||||
|
direction := (*service)[ entry.Direction ]
|
||||||
|
if direction == nil {
|
||||||
|
direction = &map[ int ] *RouteStop{}
|
||||||
|
(*service)[ entry.Direction ] = direction
|
||||||
|
}
|
||||||
|
entry.RouteStops = direction
|
||||||
|
|
||||||
|
seq := (*direction)[ entry.StationSeq ]
|
||||||
|
if seq == nil {
|
||||||
|
(*direction)[ entry.StationSeq ] = entry
|
||||||
|
}
|
||||||
|
allRouteStops = append( allRouteStops, entry )
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
|
||||||
|
QUERY_FUNC := func() ( io.ReadCloser, error ) {
|
||||||
|
resp, err := http.Get( "https://data.etabus.gov.hk/v1/transport/kmb/stop" )
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.Body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
buff, err := utils.CacheStream( JSON_BUSSTOPS, QUERY_FUNC, 7 * 24 * 3600 )
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
busStopMap, err := readBusStopsData( buff )
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
QUERY_FUNC = func() ( io.ReadCloser, error ) {
|
||||||
|
resp, err := http.Get( "https://data.etabus.gov.hk/v1/transport/kmb/route-stop" )
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.Body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
buff, err = utils.CacheStream( JSON_ROUTESTOPS, QUERY_FUNC, 7 * 24 * 3600 )
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
routeStops, err := readRouteStopsData( busStopMap, buff )
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
searchables := [] query.ISearchable{}
|
||||||
|
for _, routeStop := range *routeStops {
|
||||||
|
searchables = append( searchables, routeStop )
|
||||||
|
routeStop.Reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
return &searchables, nil
|
||||||
|
}
|
||||||
12
datasources/kmb/routestops_test.go
Normal file
12
datasources/kmb/routestops_test.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package kmb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRouteStops(t *testing.T) {
|
||||||
|
_, err := getRouteStops()
|
||||||
|
if err != nil {
|
||||||
|
t.Error( err )
|
||||||
|
}
|
||||||
|
}
|
||||||
62
datasources/kmb/schedules.go
Normal file
62
datasources/kmb/schedules.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package kmb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tgckpg/golifehk/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Schedule struct {
|
||||||
|
ETA time.Time `json:"eta,string"`
|
||||||
|
Index int `json:"eta_seq"`
|
||||||
|
DateCreated time.Time `json:"data_timestamp,string"`
|
||||||
|
Remarks_en string `json:"rmk_en"`
|
||||||
|
Remarks_sc string `json:"rmk_sc"`
|
||||||
|
Remarks_tc string `json:"rmk_tc"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Schedules struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
DateCreated time.Time `json:"generated_timestamp,string"`
|
||||||
|
Schedules [] *Schedule `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSchedule( r *RouteStop ) ( *[] *Schedule, error ) {
|
||||||
|
|
||||||
|
CACHE_PATH := filepath.Join(
|
||||||
|
utils.WORKDIR,
|
||||||
|
"kmb-" + r.BusStop.BusStopId + "-" + r.RouteId + "-" + r.ServiceType + ".json",
|
||||||
|
)
|
||||||
|
|
||||||
|
QUERY_FUNC := func() ( io.ReadCloser, error ) {
|
||||||
|
resp, err := http.Get( fmt.Sprintf(
|
||||||
|
"https://data.etabus.gov.hk/v1/transport/kmb/eta/%s/%s/%s",
|
||||||
|
r.BusStop.BusStopId,
|
||||||
|
r.RouteId,
|
||||||
|
r.ServiceType,
|
||||||
|
) )
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.Body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
buff, err := utils.CacheStream( CACHE_PATH, QUERY_FUNC, 60 )
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
schedules := Schedules{}
|
||||||
|
err = json.Unmarshal( buff.Bytes(), &schedules )
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &schedules.Schedules, nil
|
||||||
|
}
|
||||||
63
datasources/mtr/bus/BusStop.go
Normal file
63
datasources/mtr/bus/BusStop.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package bus
|
||||||
|
|
||||||
|
import (
|
||||||
|
i18n "github.com/tgckpg/golifehk/i18n"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BusStop struct {
|
||||||
|
RouteId string
|
||||||
|
ReferenceId string
|
||||||
|
Direction string
|
||||||
|
StationSeq int
|
||||||
|
StationId string
|
||||||
|
Latitude float64
|
||||||
|
Longtitude float64
|
||||||
|
Name_zh string
|
||||||
|
Name_en string
|
||||||
|
|
||||||
|
// RouteStops[ StationSeq ] = BusStop
|
||||||
|
RouteStops *map[int]*BusStop
|
||||||
|
|
||||||
|
i18n.Generics
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *BusStop) PrevStop() *BusStop {
|
||||||
|
if v, hasKey := (*this.RouteStops)[this.StationSeq-1]; hasKey {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *BusStop) NextStop() *BusStop {
|
||||||
|
if v, hasKey := (*this.RouteStops)[this.StationSeq+1]; hasKey {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *BusStop) Reload() {
|
||||||
|
i18n_Name := map[string]string{}
|
||||||
|
i18n_Name["en"] = this.Name_en
|
||||||
|
i18n_Name["zh-Hant"] = this.Name_zh
|
||||||
|
|
||||||
|
searchData := []*string{}
|
||||||
|
searchData = append(searchData, &this.Name_en)
|
||||||
|
searchData = append(searchData, &this.Name_zh)
|
||||||
|
|
||||||
|
this.Name = &i18n_Name
|
||||||
|
this.Key = &this.RouteId
|
||||||
|
this.SearchData = &searchData
|
||||||
|
}
|
||||||
|
|
||||||
|
type ByRoute []*BusStop
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return _a.Direction < _b.Direction
|
||||||
|
}
|
||||||
|
return _a.RouteId < _b.RouteId
|
||||||
|
}
|
||||||
148
datasources/mtr/bus/QueryResult.go
Normal file
148
datasources/mtr/bus/QueryResult.go
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
package bus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
query "github.com/tgckpg/golifehk/query"
|
||||||
|
utils "github.com/tgckpg/golifehk/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type QueryResult struct {
|
||||||
|
Schedules *map[*BusStop] *BusStopBuses
|
||||||
|
Lang string
|
||||||
|
Error error
|
||||||
|
Query *query.QueryObject
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeShortRoute( lang *string, sb *strings.Builder, b *BusStop ) {
|
||||||
|
if b.PrevStop() != nil {
|
||||||
|
utils.WriteMDv2Text( sb, (*b.PrevStop().Name)[ *lang ] )
|
||||||
|
sb.WriteString( " \\> " )
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.WriteString( "*" )
|
||||||
|
utils.WriteMDv2Text( sb, (*b.Name)[ *lang ] )
|
||||||
|
sb.WriteString( "*" )
|
||||||
|
|
||||||
|
if b.NextStop() != nil {
|
||||||
|
sb.WriteString( " \\> " )
|
||||||
|
utils.WriteMDv2Text( sb, (*b.NextStop().Name)[ *lang ] )
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.WriteString( "\n" )
|
||||||
|
}
|
||||||
|
|
||||||
|
func ( this QueryResult ) Message() ( string, error ) {
|
||||||
|
|
||||||
|
if this.Error != nil {
|
||||||
|
return "", this.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
sb := strings.Builder{}
|
||||||
|
|
||||||
|
if this.Schedules == nil {
|
||||||
|
|
||||||
|
q := *this.Query
|
||||||
|
|
||||||
|
if len( *q.Results ) == 0 {
|
||||||
|
terms := make( []string, len(*q.SearchTerms), len(*q.SearchTerms) )
|
||||||
|
for i, term := range *q.SearchTerms {
|
||||||
|
terms[i] = term.Org
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf( "Not Found: \"%s\"", strings.Join( terms, "\", \"" ) )
|
||||||
|
}
|
||||||
|
|
||||||
|
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 )
|
||||||
|
}
|
||||||
|
} else if 1 == len( *q.SearchTerms ) {
|
||||||
|
|
||||||
|
// Route listing
|
||||||
|
st := map[string] *BusStop{}
|
||||||
|
keys := [] string{}
|
||||||
|
|
||||||
|
for _, entry := range *q.Results {
|
||||||
|
|
||||||
|
busStop := any( entry ).( *BusStop )
|
||||||
|
if _, ok := st[ busStop.Direction ]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
st[ busStop.Direction ] = busStop
|
||||||
|
keys = append( keys, busStop.Direction )
|
||||||
|
|
||||||
|
for st[ busStop.Direction ].PrevStop() != nil {
|
||||||
|
st[ busStop.Direction ] = st[ busStop.Direction ].PrevStop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings( keys )
|
||||||
|
|
||||||
|
for _, d := range keys {
|
||||||
|
b := st[ d ]
|
||||||
|
utils.WriteMDv2Text( &sb, q.Key )
|
||||||
|
|
||||||
|
if d == "O" {
|
||||||
|
sb.WriteString( "↑" )
|
||||||
|
} else if d == "I" {
|
||||||
|
sb.WriteString( "↓" )
|
||||||
|
} else {
|
||||||
|
sb.WriteString( "\\?" )
|
||||||
|
}
|
||||||
|
sb.WriteString( "\n " )
|
||||||
|
|
||||||
|
for {
|
||||||
|
utils.WriteMDv2Text( &sb, (*b.Name)[ this.Lang ] )
|
||||||
|
b = b.NextStop()
|
||||||
|
if b == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.WriteString( " \\> " )
|
||||||
|
}
|
||||||
|
sb.WriteString( "\n" )
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return "", fmt.Errorf( "%s", "Unreachable condition occured!?" )
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if 0 < len( *this.Schedules ) {
|
||||||
|
|
||||||
|
busStops := [] *BusStop{}
|
||||||
|
|
||||||
|
for b, _ := range *this.Schedules {
|
||||||
|
busStops = append( busStops, b )
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Sort( ByRoute( busStops ) )
|
||||||
|
|
||||||
|
for _, busStop := range busStops {
|
||||||
|
buses := (*this.Schedules)[ busStop ]
|
||||||
|
|
||||||
|
writeShortRoute( &this.Lang, &sb, busStop )
|
||||||
|
for _, bus := range buses.Buses {
|
||||||
|
sb.WriteString( " \\* " )
|
||||||
|
if bus.ETAText == "" {
|
||||||
|
utils.WriteMDv2Text( &sb, bus.ETDText )
|
||||||
|
} else {
|
||||||
|
utils.WriteMDv2Text( &sb, bus.ETAText )
|
||||||
|
}
|
||||||
|
sb.WriteString( "\n" )
|
||||||
|
}
|
||||||
|
sb.WriteString( "\n" )
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
utils.WriteMDv2Text( &sb, "Schedules are empty...perhaps Out of Service Time?" )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String(), nil
|
||||||
|
}
|
||||||
@@ -3,9 +3,12 @@ package bus
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/tgckpg/golifehk/utils"
|
"github.com/tgckpg/golifehk/utils"
|
||||||
)
|
)
|
||||||
@@ -27,14 +30,33 @@ type Bus struct {
|
|||||||
LineRef string `json:"lineRef"`
|
LineRef string `json:"lineRef"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type BusStopSchedules struct {
|
type BusStopBuses struct {
|
||||||
Buses []Bus `json:"bus"`
|
Buses []Bus `json:"bus"`
|
||||||
|
BusStopId string `json:"busStopId"`
|
||||||
Suspended string `json:"isSuspended"`
|
Suspended string `json:"isSuspended"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ScheduleStatusTime struct {
|
||||||
|
time.Time
|
||||||
|
}
|
||||||
|
|
||||||
type BusSchedule struct {
|
type BusSchedule struct {
|
||||||
RefreshTime int `json:"appRefreshTimeInSecond,string"`
|
RefreshTime int `json:"appRefreshTimeInSecond,string"`
|
||||||
BusStops [] BusStopSchedules `json:"busStop"`
|
BusStops []BusStopBuses `json:"busStop"`
|
||||||
|
StatusTime ScheduleStatusTime `json:"routeStatusTime"`
|
||||||
|
RemarksTitle string `json:"routeStatusRemarkTitle"`
|
||||||
|
// 0 = OK
|
||||||
|
// 100 = Rate limit ( Unconfirmed. Not in spec )
|
||||||
|
Status int `json:"status,string"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ScheduleStatusTime) UnmarshalJSON(b []byte) (err error) {
|
||||||
|
date, err := time.Parse(`"2006\/01\/02 15:04"`, string(b))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.Time = date
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSchedule(lang string, routeName string) (*BusSchedule, error) {
|
func getSchedule(lang string, routeName string) (*BusSchedule, error) {
|
||||||
@@ -44,9 +66,14 @@ func getSchedule( lang string, routeName string ) ( *BusSchedule, error ) {
|
|||||||
"mtr_bsch"+"-"+lang+"-"+routeName+".json",
|
"mtr_bsch"+"-"+lang+"-"+routeName+".json",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
postLang := "en"
|
||||||
|
if lang == "zh-Hant" {
|
||||||
|
postLang = "zh"
|
||||||
|
}
|
||||||
|
|
||||||
QUERY_FUNC := func() (io.ReadCloser, error) {
|
QUERY_FUNC := func() (io.ReadCloser, error) {
|
||||||
// Query Remote
|
// Query Remote
|
||||||
values := map[string]string { "language": lang , "routeName": routeName }
|
values := map[string]string{"language": postLang, "routeName": routeName}
|
||||||
jsonValue, _ := json.Marshal(values)
|
jsonValue, _ := json.Marshal(values)
|
||||||
resp, err := http.Post(
|
resp, err := http.Post(
|
||||||
"https://rt.data.gov.hk/v1/transport/mtr/bus/getSchedule",
|
"https://rt.data.gov.hk/v1/transport/mtr/bus/getSchedule",
|
||||||
@@ -61,16 +88,59 @@ func getSchedule( lang string, routeName string ) ( *BusSchedule, error ) {
|
|||||||
return resp.Body, nil
|
return resp.Body, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
buff, err := utils.CacheStream( CACHE_PATH, QUERY_FUNC, 60 )
|
cs, err := utils.CacheStreamEx(CACHE_PATH, QUERY_FUNC)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
schedules := BusSchedule{}
|
oldSch := BusSchedule{
|
||||||
err = json.Unmarshal( buff.Bytes(), &schedules )
|
Status: -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
if cs.Local != nil {
|
||||||
|
err = json.Unmarshal(cs.Local.Bytes(), &oldSch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return &schedules, nil
|
|
||||||
|
newSch := BusSchedule{
|
||||||
|
Status: -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
|
||||||
|
if cs.Remote != nil {
|
||||||
|
err = json.Unmarshal(cs.Remote.Bytes(), &newSch)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if newSch.Status == 0 {
|
||||||
|
cs.Commit()
|
||||||
|
return &newSch, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if oldSch.Status == 0 {
|
||||||
|
if cs.NotExpired(60) || oldSch.StatusTime.Time == newSch.StatusTime.Time {
|
||||||
|
log.Printf("Using cache: %s", CACHE_PATH)
|
||||||
|
return &oldSch, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// First time + try again i times
|
||||||
|
err = cs.Reload()
|
||||||
|
log.Printf("Reloading (%d): %s", i, CACHE_PATH)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Error retrieving data: %s", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if newSch.Status != 0 {
|
||||||
|
err = fmt.Errorf("%s (%d)", newSch.RemarksTitle, newSch.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &newSch, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,30 +4,18 @@ import (
|
|||||||
"encoding/csv"
|
"encoding/csv"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
|
|
||||||
|
query "github.com/tgckpg/golifehk/query"
|
||||||
"github.com/tgckpg/golifehk/utils"
|
"github.com/tgckpg/golifehk/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type BusStop struct {
|
|
||||||
RouteId string
|
|
||||||
Direction string
|
|
||||||
StationSeq string
|
|
||||||
StationId string
|
|
||||||
Latitude float64
|
|
||||||
Longtitude float64
|
|
||||||
Name_zhant string
|
|
||||||
Name_en string
|
|
||||||
}
|
|
||||||
|
|
||||||
var mBusStops *map[string]BusStop
|
|
||||||
|
|
||||||
var CSV_BUSSTOPS string = filepath.Join(utils.WORKDIR, "mtr_bus_stops.csv")
|
var CSV_BUSSTOPS string = filepath.Join(utils.WORKDIR, "mtr_bus_stops.csv")
|
||||||
|
|
||||||
func readBusStopData( r io.Reader ) ( *map[string]BusStop, error ) {
|
func readBusStopData(r io.Reader) (*map[string]*BusStop, error) {
|
||||||
|
|
||||||
reader := csv.NewReader(r)
|
reader := csv.NewReader(r)
|
||||||
entries, err := reader.ReadAll()
|
entries, err := reader.ReadAll()
|
||||||
@@ -35,16 +23,18 @@ func readBusStopData( r io.Reader ) ( *map[string]BusStop, error ) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
busStops := map[string]BusStop{}
|
busStops := map[string]*BusStop{}
|
||||||
|
routeStops := map[string]map[string]map[int]*BusStop{}
|
||||||
|
|
||||||
var headers []string
|
var headers []string
|
||||||
for i, line := range entries {
|
for i, line := range entries {
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
headers = line
|
headers = line
|
||||||
line[0] = strings.TrimLeft( line[0], utils.BOM )
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var entry BusStop
|
var entry BusStop
|
||||||
|
|
||||||
for j, value := range line {
|
for j, value := range line {
|
||||||
switch headers[j] {
|
switch headers[j] {
|
||||||
case "ROUTE_ID":
|
case "ROUTE_ID":
|
||||||
@@ -52,7 +42,8 @@ func readBusStopData( r io.Reader ) ( *map[string]BusStop, error ) {
|
|||||||
case "DIRECTION":
|
case "DIRECTION":
|
||||||
entry.Direction = value
|
entry.Direction = value
|
||||||
case "STATION_SEQNO":
|
case "STATION_SEQNO":
|
||||||
entry.StationSeq = value
|
v, _ := strconv.ParseFloat(value, 64)
|
||||||
|
entry.StationSeq = int(v)
|
||||||
case "STATION_ID":
|
case "STATION_ID":
|
||||||
entry.StationId = value
|
entry.StationId = value
|
||||||
case "STATION_LATITUDE":
|
case "STATION_LATITUDE":
|
||||||
@@ -62,24 +53,53 @@ func readBusStopData( r io.Reader ) ( *map[string]BusStop, error ) {
|
|||||||
v, _ := strconv.ParseFloat(value, 64)
|
v, _ := strconv.ParseFloat(value, 64)
|
||||||
entry.Longtitude = v
|
entry.Longtitude = v
|
||||||
case "STATION_NAME_CHI":
|
case "STATION_NAME_CHI":
|
||||||
entry.Name_zhant = value
|
entry.Name_zh = value
|
||||||
case "STATION_NAME_ENG":
|
case "STATION_NAME_ENG":
|
||||||
entry.Name_en = value
|
entry.Name_en = value
|
||||||
|
case "REFERENCE_ID":
|
||||||
|
entry.ReferenceId = value
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("Unknown header \"%s\"", headers[j])
|
return nil, fmt.Errorf("Unknown header \"%s\"", headers[j])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, t := busStops[ entry.StationId ]; t {
|
// Ignoring special route for now as getSchedule does not reflect
|
||||||
|
// which bus is a special route
|
||||||
|
if entry.ReferenceId != entry.RouteId {
|
||||||
|
log.Printf("Ignoring special Route: %s", entry.ReferenceId)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if busStops[entry.StationId] != nil {
|
||||||
return nil, fmt.Errorf("Duplicated entry %+v", entry)
|
return nil, fmt.Errorf("Duplicated entry %+v", entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
busStops[ entry.StationId ] = entry
|
routeDir, hasKey := routeStops[entry.RouteId]
|
||||||
|
if !hasKey {
|
||||||
|
routeStops[entry.RouteId] = map[string]map[int]*BusStop{}
|
||||||
|
routeDir = routeStops[entry.RouteId]
|
||||||
|
}
|
||||||
|
|
||||||
|
route, hasKey := routeDir[entry.Direction]
|
||||||
|
if !hasKey {
|
||||||
|
routeDir[entry.Direction] = map[int]*BusStop{}
|
||||||
|
route = routeDir[entry.Direction]
|
||||||
|
}
|
||||||
|
|
||||||
|
_, hasKey = route[entry.StationSeq]
|
||||||
|
if !hasKey {
|
||||||
|
route[entry.StationSeq] = &entry
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.RouteStops = &route
|
||||||
|
entry.Reload()
|
||||||
|
|
||||||
|
busStops[entry.StationId] = &entry
|
||||||
}
|
}
|
||||||
return &busStops, nil
|
return &busStops, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getBusStops() (*map[string]BusStop, error) {
|
func getBusStops() (*[]query.ISearchable, error) {
|
||||||
|
|
||||||
QUERY_FUNC := func() (io.ReadCloser, error) {
|
QUERY_FUNC := func() (io.ReadCloser, error) {
|
||||||
resp, err := http.Get("https://opendata.mtr.com.hk/data/mtr_bus_stops.csv")
|
resp, err := http.Get("https://opendata.mtr.com.hk/data/mtr_bus_stops.csv")
|
||||||
@@ -94,5 +114,17 @@ func getBusStops() (*map[string]BusStop, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return readBusStopData( buff )
|
utils.ReadBOM(buff)
|
||||||
|
busStopMap, err := readBusStopData(buff)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
searchables := []query.ISearchable{}
|
||||||
|
for _, busStop := range *busStopMap {
|
||||||
|
searchables = append(searchables, busStop)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &searchables, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,103 +1,59 @@
|
|||||||
package bus
|
package bus
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
"github.com/tgckpg/golifehk/utils"
|
query "github.com/tgckpg/golifehk/query"
|
||||||
)
|
)
|
||||||
|
|
||||||
type QueryObject struct {
|
func Query( lang string, message string ) query.IQueryResult {
|
||||||
Route string
|
|
||||||
BusStops *[]BusStop
|
|
||||||
}
|
|
||||||
|
|
||||||
type QueryResult struct {
|
var qBusStops *query.QueryObject
|
||||||
BusStops []BusStop
|
var err error
|
||||||
err string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Term struct {
|
qr := QueryResult{ Lang: lang }
|
||||||
Value string
|
|
||||||
ProblyRoute bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func Query( message string ) *QueryResult {
|
|
||||||
return &QueryResult{ err: "No Result" }
|
|
||||||
}
|
|
||||||
|
|
||||||
func test( entry BusStop, val string ) bool {
|
|
||||||
switch true {
|
|
||||||
case strings.Contains( entry.Name_zhant, val ):
|
|
||||||
fallthrough
|
|
||||||
case strings.Contains( entry.Name_en, val ):
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func parse( line string ) ( *QueryObject, error ) {
|
|
||||||
busStops, err := getBusStops()
|
busStops, err := getBusStops()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
qr.Error = err
|
||||||
|
goto qrReturn
|
||||||
}
|
}
|
||||||
|
|
||||||
var route string = ""
|
qBusStops, err = query.Parse( strings.ToUpper( message ), busStops )
|
||||||
var searches = []string{}
|
if err != nil {
|
||||||
matches := []BusStop{}
|
qr.Error = err
|
||||||
|
goto qrReturn
|
||||||
// Sanitize and assume properties for each of the keywords
|
|
||||||
terms := []Term{}
|
|
||||||
for _, val := range strings.Split( line, " " ) {
|
|
||||||
val = strings.ToUpper( strings.Trim( val, " " ) )
|
|
||||||
term := Term{
|
|
||||||
Value: val,
|
|
||||||
ProblyRoute: strings.ContainsAny( val, utils.ROUTE_CHARS ),
|
|
||||||
}
|
|
||||||
terms = append( terms, term )
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search for route name first, otherwise search in other props
|
qr.Query = qBusStops
|
||||||
for _, entry := range *busStops {
|
if 0 < len( *qBusStops.Results ) && 1 < len( *qBusStops.SearchTerms ) {
|
||||||
|
schedules, err := getSchedule( lang, qBusStops.Key )
|
||||||
// Search for RouteId
|
if err != nil {
|
||||||
for _, term := range terms {
|
qr.Error = err
|
||||||
|
goto qrReturn
|
||||||
if term.ProblyRoute && term.Value == entry.RouteId {
|
|
||||||
if route != "" && route != term.Value {
|
|
||||||
return nil, fmt.Errorf( "Cannot %s & %s", route, term.Value )
|
|
||||||
}
|
|
||||||
matches = append( matches, entry )
|
|
||||||
route = entry.RouteId
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
searches = append( searches, term.Value )
|
if len( schedules.BusStops ) == 0 {
|
||||||
if test( entry, term.Value ) {
|
qr.Schedules = &map[*BusStop] *BusStopBuses{}
|
||||||
matches = append( matches, entry )
|
goto qrReturn
|
||||||
|
}
|
||||||
|
|
||||||
|
matches := map[*BusStop] *BusStopBuses{}
|
||||||
|
for _, entry := range *qBusStops.Results {
|
||||||
|
busStop := any( entry ).( *BusStop )
|
||||||
|
|
||||||
|
for _, busStopSch := range schedules.BusStops {
|
||||||
|
if busStopSch.BusStopId == busStop.StationId {
|
||||||
|
matches[busStop] = &busStopSch
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If route found, filter out all other route
|
qr.Schedules = &matches
|
||||||
// then search within that route
|
|
||||||
if route != "" && 0 < len( searches ) {
|
|
||||||
matches_in := []BusStop{}
|
|
||||||
for _, entry := range matches {
|
|
||||||
if entry.RouteId != route {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, val := range searches {
|
qrReturn:
|
||||||
if test( entry, val ) {
|
var iqr query.IQueryResult
|
||||||
matches_in = append( matches_in, entry )
|
iqr = &qr
|
||||||
break
|
return iqr
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
matches = matches_in
|
|
||||||
}
|
|
||||||
|
|
||||||
return &QueryObject{ Route: route, BusStops: &matches }, err
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,32 @@ package bus
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestQuerySchedule(t *testing.T) {
|
func TestQuery( t *testing.T ) {
|
||||||
qo, err := parse( "K74 天瑞" )
|
qo := Query( "zh-Hant", "K73" )
|
||||||
|
mesg, err := qo.Message()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error( err )
|
t.Errorf( "Unexpected Error: %s", err )
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf( "%+v", qo )
|
if !strings.Contains( mesg, "K73\\-O" ) {
|
||||||
|
t.Errorf( "Expected Route Listing, got \"%s\" instead", mesg )
|
||||||
}
|
}
|
||||||
|
|
||||||
|
qo = Query( "zh-Hant", "K76 池" )
|
||||||
|
mesg, err = qo.Message()
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf( "Expecting error, got \"%s\" instead", mesg )
|
||||||
|
}
|
||||||
|
|
||||||
|
qo = Query( "zh-Hant", "K73 池" )
|
||||||
|
mesg, err = qo.Message()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf( "Unexpected Error: %s", err )
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println( mesg )
|
||||||
|
}
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -1,3 +1,5 @@
|
|||||||
module github.com/tgckpg/golifehk
|
module github.com/tgckpg/golifehk
|
||||||
|
|
||||||
go 1.19
|
go 1.19
|
||||||
|
|
||||||
|
require github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 // indirect
|
||||||
|
|||||||
2
go.sum
Normal file
2
go.sum
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
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=
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"html"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Index(w http.ResponseWriter, r *http.Request) {
|
|
||||||
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
|
|
||||||
}
|
|
||||||
10
i18n/Generics.go
Normal file
10
i18n/Generics.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package i18n
|
||||||
|
|
||||||
|
import (
|
||||||
|
query "github.com/tgckpg/golifehk/query"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Generics struct {
|
||||||
|
Name *map[string] string
|
||||||
|
query.Searchable
|
||||||
|
}
|
||||||
35
k8s/deployments.yaml
Normal file
35
k8s/deployments.yaml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: golifehk
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: golifehk
|
||||||
|
replicas: 1
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: golifehk
|
||||||
|
srv: go
|
||||||
|
spec:
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: registry-auth
|
||||||
|
containers:
|
||||||
|
- name: app
|
||||||
|
image: registry.k8s.astropenguin.net/golifehk:IMAGE_TAG
|
||||||
|
env:
|
||||||
|
- name: GOLIFEHK_WORKDIR
|
||||||
|
value: "/workdir"
|
||||||
|
- name: TELEGRAM_API_TOKEN
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: golifehk-conf
|
||||||
|
key: TELEGRAM_API_TOKEN
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /workdir
|
||||||
|
name: workdir
|
||||||
|
volumes:
|
||||||
|
- name: workdir
|
||||||
|
emptyDir:
|
||||||
|
sizeLimit: 10Mi
|
||||||
76
main.go
76
main.go
@@ -1,12 +1,78 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"os"
|
||||||
"github.com/tgckpg/golifehk/handlers"
|
utils "github.com/tgckpg/golifehk/utils"
|
||||||
|
mtrbus "github.com/tgckpg/golifehk/datasources/mtr/bus"
|
||||||
|
kmb "github.com/tgckpg/golifehk/datasources/kmb"
|
||||||
|
query "github.com/tgckpg/golifehk/query"
|
||||||
|
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func botsend( bot *tgbotapi.BotAPI, update *tgbotapi.Update, mesg *string ) {
|
||||||
http.HandleFunc( "/", handlers.Index )
|
var msg tgbotapi.MessageConfig
|
||||||
log.Fatal(http.ListenAndServe(":8000", nil))
|
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 {
|
||||||
|
log.Panic( err )
|
||||||
|
}
|
||||||
|
|
||||||
|
bot.Debug = true
|
||||||
|
|
||||||
|
log.Printf("Authorized on account %s", bot.Self.UserName)
|
||||||
|
|
||||||
|
u := tgbotapi.NewUpdate(0)
|
||||||
|
u.Timeout = 60
|
||||||
|
|
||||||
|
updates := bot.GetUpdatesChan(u)
|
||||||
|
|
||||||
|
for update := range updates {
|
||||||
|
if update.Message == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf( "[%s] %s", update.Message.From.UserName, update.Message.Text )
|
||||||
|
isGroup := ( update.Message.Chat.ID < 0 )
|
||||||
|
|
||||||
|
mesg, processed := utils.SystemControl( update.Message )
|
||||||
|
if processed {
|
||||||
|
if mesg != "" {
|
||||||
|
botsend( bot, &update, &mesg )
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
f_queries := []func( string, string ) query.IQueryResult{
|
||||||
|
mtrbus.Query,
|
||||||
|
kmb.Query,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
f_sent = true
|
||||||
|
botsend( bot, &update, &mesg )
|
||||||
|
} else if f_err == nil {
|
||||||
|
f_err = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isGroup && !f_sent && f_err != nil {
|
||||||
|
mesg := utils.MDv2Text( fmt.Sprintf( "%s", f_err ) )
|
||||||
|
botsend( bot, &update, &mesg )
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
5
query/IQueryResult.go
Normal file
5
query/IQueryResult.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package query
|
||||||
|
|
||||||
|
type IQueryResult interface {
|
||||||
|
Message() ( string, error )
|
||||||
|
}
|
||||||
40
query/Searchable.go
Normal file
40
query/Searchable.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
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 {
|
||||||
|
|
||||||
|
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() }
|
||||||
81
query/query.go
Normal file
81
query/query.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
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 )
|
||||||
|
}
|
||||||
79
query/query_test.go
Normal file
79
query/query_test.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
package query
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TestItem struct {
|
||||||
|
Searchable
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQuery( t *testing.T ) {
|
||||||
|
testItems := [] ISearchable{}
|
||||||
|
|
||||||
|
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 )
|
||||||
|
|
||||||
|
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 )
|
||||||
|
|
||||||
|
qo, err := Parse( "Apple", &testItems )
|
||||||
|
if err != nil {
|
||||||
|
t.Error( err )
|
||||||
|
}
|
||||||
|
|
||||||
|
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 )
|
||||||
|
}
|
||||||
|
|
||||||
|
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 )
|
||||||
|
}
|
||||||
|
|
||||||
|
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 )
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(*qo.Results) != 1 {
|
||||||
|
t.Error( "Expected 1 results when searching for \"Dat2 Dat0\"" )
|
||||||
|
}
|
||||||
|
}
|
||||||
130
utils/cache.go
130
utils/cache.go
@@ -3,23 +3,92 @@ package utils
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type CacheStreams struct {
|
||||||
|
Local *bytes.Buffer
|
||||||
|
Remote *bytes.Buffer
|
||||||
|
Path string
|
||||||
|
Query func() ( io.ReadCloser, error )
|
||||||
|
}
|
||||||
|
|
||||||
func CacheStream( path string, readStream func() ( io.ReadCloser, error ), expires int ) ( *bytes.Buffer, error ) {
|
func ( cs *CacheStreams ) Commit() error {
|
||||||
|
|
||||||
|
f, err := os.OpenFile( cs.Path, os.O_CREATE | os.O_WRONLY | os.O_TRUNC, 0644 )
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy( f, bytes.NewReader( cs.Remote.Bytes() ) )
|
||||||
|
log.Printf( "Commit: %s", cs.Path )
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func ( cs *CacheStreams ) Reload() error {
|
||||||
|
|
||||||
|
s, err := cs.Query()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
cs.Remote = bytes.NewBuffer( []byte{} )
|
||||||
|
_, err = io.Copy( cs.Remote, s )
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func ( cs *CacheStreams ) NotExpired( expires time.Duration ) bool {
|
||||||
|
|
||||||
|
cache, err := os.Stat( cs.Path )
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
expired := cache.ModTime().Add( expires * 1e9 )
|
||||||
|
return time.Now().Before( expired )
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func CacheStreamEx( path string, readStream func() ( io.ReadCloser, error ) ) ( *CacheStreams, error ) {
|
||||||
|
|
||||||
|
cs := CacheStreams{}
|
||||||
|
cs.Path = path
|
||||||
|
cs.Query = readStream
|
||||||
|
|
||||||
|
f, err := os.Open( path )
|
||||||
|
if err == nil {
|
||||||
|
defer f.Close()
|
||||||
|
cs.Local = bytes.NewBuffer( []byte{} )
|
||||||
|
_, err = io.Copy( cs.Local, f )
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &cs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func CacheStream( path string, readStream func() ( io.ReadCloser, error ), expires time.Duration ) ( *bytes.Buffer, error ) {
|
||||||
|
|
||||||
cache, err := os.Stat( path )
|
cache, err := os.Stat( path )
|
||||||
|
|
||||||
// Check if cache exists and not expired
|
// Check if cache exists and not expired
|
||||||
if err == nil {
|
if err == nil {
|
||||||
expired := cache.ModTime().Add( 60 * 1e9 )
|
expired := cache.ModTime().Add( expires * 1e9 )
|
||||||
if time.Now().Before( expired ) {
|
if time.Now().Before( expired ) {
|
||||||
f, err := os.Open( path )
|
f, err := os.Open( path )
|
||||||
if err == nil {
|
if err == nil {
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
log.Printf( "Using cache: %s", path )
|
||||||
writeBuff := bytes.NewBuffer( []byte{} )
|
writeBuff := bytes.NewBuffer( []byte{} )
|
||||||
_, err = io.Copy( writeBuff, f )
|
_, err = io.Copy( writeBuff, f )
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -31,8 +100,7 @@ func CacheStream( path string, readStream func() ( io.ReadCloser, error ), expir
|
|||||||
|
|
||||||
err = os.MkdirAll( filepath.Dir( path ), 0750 )
|
err = os.MkdirAll( filepath.Dir( path ), 0750 )
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err }
|
||||||
}
|
|
||||||
|
|
||||||
writeBuff := bytes.NewBuffer( []byte{} )
|
writeBuff := bytes.NewBuffer( []byte{} )
|
||||||
|
|
||||||
@@ -64,3 +132,57 @@ func CacheStream( path string, readStream func() ( io.ReadCloser, error ), expir
|
|||||||
|
|
||||||
return writeBuff, nil
|
return writeBuff, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ChangedStream( path string, readStream func() ( io.Reader, error ), dataModTime time.Time ) ( *bytes.Buffer, error ) {
|
||||||
|
|
||||||
|
cache, err := os.Stat( path )
|
||||||
|
|
||||||
|
// Check if cache exists and not expired
|
||||||
|
if err == nil {
|
||||||
|
if dataModTime.Before( cache.ModTime() ) {
|
||||||
|
f, err := os.Open( path )
|
||||||
|
if err == nil {
|
||||||
|
defer f.Close()
|
||||||
|
log.Printf( "Reading from file: %s", path )
|
||||||
|
writeBuff := bytes.NewBuffer( []byte{} )
|
||||||
|
_, err = io.Copy( writeBuff, f )
|
||||||
|
if err == nil {
|
||||||
|
return writeBuff, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.MkdirAll( filepath.Dir( path ), 0750 )
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
writeBuff := bytes.NewBuffer( []byte{} )
|
||||||
|
|
||||||
|
// Get the reader that return new data
|
||||||
|
s, err := readStream()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy( writeBuff, s )
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.OpenFile( path, os.O_CREATE | os.O_WRONLY | os.O_TRUNC, 0644 )
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
data := writeBuff.Bytes()
|
||||||
|
_, err = io.Copy( f, bytes.NewReader( data ) )
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return writeBuff, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,4 +9,8 @@ var WORKDIR string = TryGetEnv( "GOLIFEHK_WORKDIR", filepath.Join( os.TempDir(),
|
|||||||
|
|
||||||
var BOM string = bytes.NewBuffer([]byte{ 0xEF, 0xBB, 0xBF }).String()
|
var BOM string = bytes.NewBuffer([]byte{ 0xEF, 0xBB, 0xBF }).String()
|
||||||
|
|
||||||
const ROUTE_CHARS string = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-"
|
var POWER_NUMBERS map[string] string = map[string] string {
|
||||||
|
"0": "⁰", "1": "¹", "2": "²", "3": "³",
|
||||||
|
"4": "⁴", "5": "⁵", "6": "⁶", "7": "⁷",
|
||||||
|
"8": "⁸", "9": "⁹",
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,60 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var MARKDOWN_ESC []string = []string{"_", "*", "[", "]", "(", ")", "~", "`", ">", "#", "+", "-", "=", "|", "{", "}", ".", "!"}
|
||||||
|
|
||||||
|
func WriteMDv2Text( sb *strings.Builder, t string ) {
|
||||||
|
for _, c := range MARKDOWN_ESC {
|
||||||
|
t = strings.Replace( t, c, "\\" + c, -1 )
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.WriteString( t )
|
||||||
|
}
|
||||||
|
|
||||||
|
func MDv2Text( t string ) string {
|
||||||
|
sb := strings.Builder{}
|
||||||
|
|
||||||
|
for _, c := range MARKDOWN_ESC {
|
||||||
|
t = strings.Replace( t, c, "\\" + c, -1 )
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.WriteString( t )
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToPower( t string ) string {
|
||||||
|
for s, r := range POWER_NUMBERS {
|
||||||
|
t = strings.ReplaceAll( t, s, r )
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadBOM( buff *bytes.Buffer ) {
|
||||||
|
b, _ := buff.ReadByte()
|
||||||
|
if b != 0xef {
|
||||||
|
buff.UnreadByte()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b, _ = buff.ReadByte()
|
||||||
|
if b != 0xbb {
|
||||||
|
buff.UnreadByte()
|
||||||
|
buff.UnreadByte()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b, _ = buff.ReadByte()
|
||||||
|
if b != 0xbf {
|
||||||
|
buff.UnreadByte()
|
||||||
|
buff.UnreadByte()
|
||||||
|
buff.UnreadByte()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TryGetEnv[T any]( name string, fallback T ) T {
|
func TryGetEnv[T any]( name string, fallback T ) T {
|
||||||
v := os.Getenv( name )
|
v := os.Getenv( name )
|
||||||
|
|
||||||
|
|||||||
96
utils/system.go
Normal file
96
utils/system.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
var JSON_SETTINGS string = filepath.Join( WORKDIR, "settings.json" )
|
||||||
|
|
||||||
|
var settingsTime = time.Unix( 0, 0 )
|
||||||
|
|
||||||
|
type SysSettings struct {
|
||||||
|
IgnoredChats map[int64] bool `json:"IgnoredChats"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mesg, processed
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user