Compare commits

...

25 Commits

Author SHA1 Message Date
0ed4c618dc default mode to disable 2024-11-01 03:15:14 +08:00
c22cdac1cb Fixed file not found error 2023-12-13 06:25:49 +08:00
4efd346ce1 Use better caching mechanism for mtr schedules 2023-12-13 06:12:13 +08:00
e4275f60b7 Fixed mtr new csv format 2023-06-29 18:45:46 +08:00
726916ed45 Typo 2023-01-31 04:47:02 +08:00
91cc543fe4 Added emptyDir for caching settings 2023-01-31 04:36:58 +08:00
446847a7a8 Added option to disable chat 2023-01-06 02:22:12 +08:00
55d4ac4adc MTR: Sort schedules by route 2022-11-15 00:22:39 +08:00
a062e37a4c Simplify query calls 2022-10-18 16:20:58 +08:00
35b303f796 Use negative chat id to detect group 2022-09-25 19:09:03 +08:00
41be1db381 Added kmb schedules 2022-09-25 18:48:22 +08:00
b251e35be4 Added direction notes 2022-09-17 06:42:49 +08:00
7fd8d4fb6a Fixed docker build 2022-09-17 05:58:52 +08:00
3376d9eb96 Added kmb and refactored query.Parse 2022-09-17 04:33:47 +08:00
292665c49b Using markdown v2 2022-09-15 21:38:04 +08:00
25ccd64a88 ParseMode = Markdown 2022-09-15 20:32:06 +08:00
67e3ec1500 Only reply to mentions 2022-09-15 20:25:29 +08:00
89770c0864 Support route listing & search 2022-09-15 20:00:49 +08:00
9724abe2e2 Fixed cert issue 2022-09-15 00:05:24 +08:00
79a873becd Fixed incorrect build settings 2022-09-15 00:00:45 +08:00
f022b237ef no chown 2022-09-14 23:48:00 +08:00
877ef47dc9 Smaller image 2022-09-14 23:40:51 +08:00
619a99a6dd Added CI resources 2022-09-14 23:21:27 +08:00
9b48f2ee50 Early telegram integrations 2022-09-14 22:54:51 +08:00
534bc70f0f Partially working draft 2022-09-14 21:21:39 +08:00
32 changed files with 1649 additions and 136 deletions

2
.gitignore vendored
View File

@@ -1,3 +1,5 @@
.DS_Store
./golifehk
*.csv
*.json
*.swp

23
Dockerfile Normal file
View 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
View 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

View 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

View 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
}

View 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
}

View 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
View 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
}

View 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 )
}

View 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
}

View File

@@ -0,0 +1,12 @@
package kmb
import (
"testing"
)
func TestRouteStops(t *testing.T) {
_, err := getRouteStops()
if err != nil {
t.Error( err )
}
}

View 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
}

View File

@@ -0,0 +1,62 @@
package bus
import (
i18n "github.com/tgckpg/golifehk/i18n"
)
type BusStop struct {
RouteId 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
}

View 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
}

View File

@@ -1,11 +1,14 @@
package bus
import (
"fmt"
"bytes"
"encoding/json"
"io"
"log"
"net/http"
"path/filepath"
"time"
"github.com/tgckpg/golifehk/utils"
)
@@ -27,14 +30,33 @@ type Bus struct {
LineRef string `json:"lineRef"`
}
type BusStopSchedules struct {
type BusStopBuses struct {
Buses [] Bus `json:"bus"`
BusStopId string `json:"busStopId"`
Suspended string `json:"isSuspended"`
}
type ScheduleStatusTime struct {
time.Time
}
type BusSchedule struct {
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 ) {
@@ -44,9 +66,14 @@ func getSchedule( lang string, routeName string ) ( *BusSchedule, error ) {
"mtr_bsch" + "-" + lang + "-" + routeName + ".json",
)
postLang := "en"
if lang == "zh-Hant" {
postLang = "zh"
}
QUERY_FUNC := func() ( io.ReadCloser, error ) {
// Query Remote
values := map[string]string { "language": lang , "routeName": routeName }
values := map[string]string { "language": postLang, "routeName": routeName }
jsonValue, _ := json.Marshal(values)
resp, err := http.Post(
"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
}
buff, err := utils.CacheStream( CACHE_PATH, QUERY_FUNC, 60 )
cs, err := utils.CacheStreamEx( CACHE_PATH, QUERY_FUNC )
if err != nil {
return nil, err
}
schedules := BusSchedule{}
err = json.Unmarshal( buff.Bytes(), &schedules )
if err != nil {
return nil, err
oldSch := BusSchedule{
Status: -1,
}
return &schedules, nil
if cs.Local != nil {
err = json.Unmarshal( cs.Local.Bytes(), &oldSch )
if err != nil {
return nil, err
}
}
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
}

View File

@@ -7,27 +7,14 @@ import (
"net/http"
"path/filepath"
"strconv"
"strings"
query "github.com/tgckpg/golifehk/query"
"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" )
func readBusStopData( r io.Reader ) ( *map[string]BusStop, error ) {
func readBusStopData( r io.Reader ) ( *map[string] *BusStop, error ) {
reader := csv.NewReader( r )
entries, err := reader.ReadAll()
@@ -35,16 +22,18 @@ func readBusStopData( r io.Reader ) ( *map[string]BusStop, error ) {
return nil, err
}
busStops := map[string]BusStop{}
busStops := map[string] *BusStop{}
routeStops := map[string] map[string] map[int] *BusStop{}
var headers []string
for i, line := range entries {
if i == 0 {
headers = line
line[0] = strings.TrimLeft( line[0], utils.BOM )
continue
}
var entry BusStop
for j, value := range line {
switch headers[j] {
case "ROUTE_ID":
@@ -52,7 +41,8 @@ func readBusStopData( r io.Reader ) ( *map[string]BusStop, error ) {
case "DIRECTION":
entry.Direction = value
case "STATION_SEQNO":
entry.StationSeq = value
v, _ := strconv.ParseFloat( value, 64 )
entry.StationSeq = int( v )
case "STATION_ID":
entry.StationId = value
case "STATION_LATITUDE":
@@ -62,7 +52,7 @@ func readBusStopData( r io.Reader ) ( *map[string]BusStop, error ) {
v, _ := strconv.ParseFloat( value, 64 )
entry.Longtitude = v
case "STATION_NAME_CHI":
entry.Name_zhant = value
entry.Name_zh = value
case "STATION_NAME_ENG":
entry.Name_en = value
default:
@@ -70,16 +60,36 @@ func readBusStopData( r io.Reader ) ( *map[string]BusStop, error ) {
}
}
if _, t := busStops[ entry.StationId ]; t {
if busStops[ entry.StationId ] != nil {
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
}
func getBusStops() (*map[string]BusStop, error) {
func getBusStops() (*[] query.ISearchable, error) {
QUERY_FUNC := func() ( io.ReadCloser, error ) {
resp, err := http.Get( "https://opendata.mtr.com.hk/data/mtr_bus_stops.csv" )
@@ -94,5 +104,17 @@ func getBusStops() (*map[string]BusStop, error) {
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
}

View File

@@ -1,103 +1,59 @@
package bus
import (
"fmt"
"strings"
"github.com/tgckpg/golifehk/utils"
query "github.com/tgckpg/golifehk/query"
)
type QueryObject struct {
Route string
BusStops *[]BusStop
}
func Query( lang string, message string ) query.IQueryResult {
type QueryResult struct {
BusStops []BusStop
err string
}
var qBusStops *query.QueryObject
var err error
type Term struct {
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 ) {
qr := QueryResult{ Lang: lang }
busStops, err := getBusStops()
if err != nil {
return nil, err
qr.Error = err
goto qrReturn
}
var route string = ""
var searches = []string{}
matches := []BusStop{}
qBusStops, err = query.Parse( strings.ToUpper( message ), busStops )
if err != nil {
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 ),
qr.Query = qBusStops
if 0 < len( *qBusStops.Results ) && 1 < len( *qBusStops.SearchTerms ) {
schedules, err := getSchedule( lang, qBusStops.Key )
if err != nil {
qr.Error = err
goto qrReturn
}
terms = append( terms, term )
}
// Search for route name first, otherwise search in other props
for _, entry := range *busStops {
// Search for RouteId
for _, term := range terms {
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 test( entry, term.Value ) {
matches = append( matches, entry )
break
}
if len( schedules.BusStops ) == 0 {
qr.Schedules = &map[*BusStop] *BusStopBuses{}
goto qrReturn
}
}
// If route found, filter out all other route
// then search within that route
if route != "" && 0 < len( searches ) {
matches_in := []BusStop{}
for _, entry := range matches {
if entry.RouteId != route {
continue
}
matches := map[*BusStop] *BusStopBuses{}
for _, entry := range *qBusStops.Results {
busStop := any( entry ).( *BusStop )
for _, val := range searches {
if test( entry, val ) {
matches_in = append( matches_in, entry )
for _, busStopSch := range schedules.BusStops {
if busStopSch.BusStopId == busStop.StationId {
matches[busStop] = &busStopSch
break
}
}
}
matches = matches_in
qr.Schedules = &matches
}
return &QueryObject{ Route: route, BusStops: &matches }, err
qrReturn:
var iqr query.IQueryResult
iqr = &qr
return iqr
}

View File

@@ -2,15 +2,32 @@ package bus
import (
"fmt"
"strings"
"testing"
)
func TestQuerySchedule(t *testing.T) {
qo, err := parse( "K74 天瑞" )
func TestQuery( t *testing.T ) {
qo := Query( "zh-Hant", "K73" )
mesg, err := qo.Message()
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
View File

@@ -1,3 +1,5 @@
module github.com/tgckpg/golifehk
go 1.19
require github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 // indirect

2
go.sum Normal file
View 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=

View File

@@ -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
View 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
View 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
View File

@@ -1,12 +1,78 @@
package main
import (
"fmt"
"log"
"net/http"
"github.com/tgckpg/golifehk/handlers"
"os"
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() {
http.HandleFunc( "/", handlers.Index )
log.Fatal(http.ListenAndServe(":8000", nil))
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 {
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
View File

@@ -0,0 +1,5 @@
package query
type IQueryResult interface {
Message() ( string, error )
}

40
query/Searchable.go Normal file
View 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
View 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
View 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\"" )
}
}

View File

@@ -3,23 +3,92 @@ package utils
import (
"bytes"
"io"
"log"
"os"
"path/filepath"
"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 )
// Check if cache exists and not expired
if err == nil {
expired := cache.ModTime().Add( 60 * 1e9 )
expired := cache.ModTime().Add( expires * 1e9 )
if time.Now().Before( expired ) {
f, err := os.Open( path )
if err == nil {
defer f.Close()
log.Printf( "Using cache: %s", path )
writeBuff := bytes.NewBuffer( []byte{} )
_, err = io.Copy( writeBuff, f )
if err == nil {
@@ -31,8 +100,7 @@ func CacheStream( path string, readStream func() ( io.ReadCloser, error ), expir
err = os.MkdirAll( filepath.Dir( path ), 0750 )
if err != nil {
return nil, err
}
return nil, err }
writeBuff := bytes.NewBuffer( []byte{} )
@@ -64,3 +132,57 @@ func CacheStream( path string, readStream func() ( io.ReadCloser, error ), expir
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
}

View File

@@ -9,4 +9,8 @@ var WORKDIR string = TryGetEnv( "GOLIFEHK_WORKDIR", filepath.Join( os.TempDir(),
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": "⁹",
}

View File

@@ -1,10 +1,60 @@
package utils
import (
"bytes"
"os"
"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 {
v := os.Getenv( name )

96
utils/system.go Normal file
View 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
}