Compare commits
35 Commits
log-sessio
...
lash/helpc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5081b6d4ce
|
||
| 656052dc74 | |||
|
|
6c5873da6f
|
||
| 80b96e9bf6 | |||
| b5561decd1 | |||
| f3d4f35718 | |||
| 52787bdb4d | |||
|
824d39908b
|
|||
|
a312ea5b84
|
|||
|
4836162f40
|
|||
|
|
2024cc96e2
|
||
|
|
d2d878d5d7 | ||
| c995143543 | |||
|
|
bb4037e73f
|
||
|
|
83857026d3 | ||
|
|
349051b5ef | ||
|
|
daec816a3e
|
||
|
|
ac0c43cb43
|
||
|
|
9013cc3618
|
||
|
|
056d056613
|
||
|
|
e581ec4771 | ||
|
|
e16b7445e8
|
||
|
|
1b12f0ba5f
|
||
|
|
c1e0617bb3
|
||
|
|
6723884103
|
||
|
|
b888af446d
|
||
|
|
43b2c3b78d
|
||
|
|
d67853f6d9 | ||
|
|
06230dc557
|
||
|
|
6ee2c88fe2
|
||
|
|
bb1a846cb3 | ||
|
|
967e53d83b | ||
|
|
d246cdee51
|
||
|
|
d518a76536 | ||
|
|
6f65c33be4
|
@@ -1,5 +1,6 @@
|
||||
/**
|
||||
!/cmd/africastalking
|
||||
!/cmd/ssh
|
||||
!/common
|
||||
!/config
|
||||
!/initializers
|
||||
|
||||
@@ -18,3 +18,7 @@ DB_TIMEZONE=Africa/Nairobi
|
||||
CUSTODIAL_URL_BASE=http://localhost:5003
|
||||
BEARER_TOKEN=eyJeSIsInRcCI6IkpXVCJ.yJwdWJsaWNLZXkiOiIwrrrrrr
|
||||
DATA_URL_BASE=http://localhost:5006
|
||||
|
||||
#Language
|
||||
DEFAULT_LANGUAGE=eng
|
||||
LANGUAGES=eng, swa
|
||||
|
||||
@@ -19,6 +19,7 @@ WORKDIR /build
|
||||
RUN echo "Building on $BUILDPLATFORM, building for $TARGETPLATFORM"
|
||||
RUN go mod download
|
||||
RUN go build -tags logtrace -o ussd-africastalking -ldflags="-X main.build=${BUILD} -s -w" cmd/africastalking/main.go
|
||||
RUN go build -tags logtrace -o ussd-ssh -ldflags="-X main.build=${BUILD} -s -w" cmd/ssh/main.go
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
@@ -30,6 +31,7 @@ RUN apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
WORKDIR /service
|
||||
|
||||
COPY --from=build /build/ussd-africastalking .
|
||||
COPY --from=build /build/ussd-ssh .
|
||||
COPY --from=build /build/LICENSE .
|
||||
COPY --from=build /build/README.md .
|
||||
COPY --from=build /build/services ./services
|
||||
@@ -37,5 +39,6 @@ COPY --from=build /build/.env.example .
|
||||
RUN mv .env.example .env
|
||||
|
||||
EXPOSE 7123
|
||||
EXPOSE 7122
|
||||
|
||||
CMD ["./ussd-africastalking"]
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"git.defalsify.org/vise.git/engine"
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
"git.defalsify.org/vise.git/lang"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/config"
|
||||
"git.grassecon.net/urdt/ussd/initializers"
|
||||
@@ -22,6 +23,7 @@ import (
|
||||
httpserver "git.grassecon.net/urdt/ussd/internal/http/at"
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
"git.grassecon.net/urdt/ussd/remote"
|
||||
"git.grassecon.net/urdt/ussd/internal/args"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -44,6 +46,8 @@ func main() {
|
||||
var engineDebug bool
|
||||
var host string
|
||||
var port uint
|
||||
var gettextDir string
|
||||
var langs args.LangVar
|
||||
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
|
||||
flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
|
||||
flag.StringVar(&database, "db", "gdbm", "database to be used")
|
||||
@@ -51,12 +55,21 @@ func main() {
|
||||
flag.UintVar(&size, "s", 160, "max size of output")
|
||||
flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host")
|
||||
flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port")
|
||||
flag.StringVar(&gettextDir, "gettext", "", "use gettext translations from given directory")
|
||||
flag.Var(&langs, "language", "add symbol resolution for language")
|
||||
flag.Parse()
|
||||
|
||||
logg.Infof("start command", "build", build, "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, "Database", database)
|
||||
ln, err := lang.LanguageFromCode(config.DefaultLanguage)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "default language set error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
ctx = context.WithValue(ctx, "Language", ln)
|
||||
|
||||
pfp := path.Join(scriptDir, "pp.csv")
|
||||
|
||||
cfg := engine.Config{
|
||||
|
||||
@@ -12,12 +12,14 @@ import (
|
||||
"git.defalsify.org/vise.git/engine"
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
"git.defalsify.org/vise.git/lang"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/config"
|
||||
"git.grassecon.net/urdt/ussd/initializers"
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
"git.grassecon.net/urdt/ussd/remote"
|
||||
"git.grassecon.net/urdt/ussd/internal/args"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -54,6 +56,8 @@ func main() {
|
||||
var engineDebug bool
|
||||
var host string
|
||||
var port uint
|
||||
var gettextDir string
|
||||
var langs args.LangVar
|
||||
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
|
||||
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
|
||||
flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
|
||||
@@ -62,12 +66,22 @@ func main() {
|
||||
flag.UintVar(&size, "s", 160, "max size of output")
|
||||
flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host")
|
||||
flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port")
|
||||
flag.StringVar(&gettextDir, "gettext", "", "use gettext translations from given directory")
|
||||
flag.Var(&langs, "language", "add symbol resolution for language")
|
||||
flag.Parse()
|
||||
|
||||
logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size, "sessionId", sessionId)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, "Database", database)
|
||||
|
||||
ln, err := lang.LanguageFromCode(config.DefaultLanguage)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "default language set error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
ctx = context.WithValue(ctx, "Language", ln)
|
||||
|
||||
pfp := path.Join(scriptDir, "pp.csv")
|
||||
|
||||
cfg := engine.Config{
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"git.defalsify.org/vise.git/engine"
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
"git.defalsify.org/vise.git/lang"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/config"
|
||||
"git.grassecon.net/urdt/ussd/initializers"
|
||||
@@ -21,6 +22,7 @@ import (
|
||||
httpserver "git.grassecon.net/urdt/ussd/internal/http"
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
"git.grassecon.net/urdt/ussd/remote"
|
||||
"git.grassecon.net/urdt/ussd/internal/args"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -43,6 +45,8 @@ func main() {
|
||||
var engineDebug bool
|
||||
var host string
|
||||
var port uint
|
||||
var gettextDir string
|
||||
var langs args.LangVar
|
||||
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
|
||||
flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
|
||||
flag.StringVar(&database, "db", "gdbm", "database to be used")
|
||||
@@ -50,12 +54,22 @@ func main() {
|
||||
flag.UintVar(&size, "s", 160, "max size of output")
|
||||
flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host")
|
||||
flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port")
|
||||
flag.StringVar(&gettextDir, "gettext", "", "use gettext translations from given directory")
|
||||
flag.Var(&langs, "language", "add symbol resolution for language")
|
||||
flag.Parse()
|
||||
|
||||
logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, "Database", database)
|
||||
|
||||
ln, err := lang.LanguageFromCode(config.DefaultLanguage)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "default language set error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
ctx = context.WithValue(ctx, "Language", ln)
|
||||
|
||||
pfp := path.Join(scriptDir, "pp.csv")
|
||||
|
||||
cfg := engine.Config{
|
||||
|
||||
24
cmd/main.go
24
cmd/main.go
@@ -10,10 +10,12 @@ import (
|
||||
"git.defalsify.org/vise.git/engine"
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
"git.defalsify.org/vise.git/lang"
|
||||
"git.grassecon.net/urdt/ussd/config"
|
||||
"git.grassecon.net/urdt/ussd/initializers"
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
"git.grassecon.net/urdt/ussd/internal/args"
|
||||
"git.grassecon.net/urdt/ussd/remote"
|
||||
)
|
||||
|
||||
@@ -27,6 +29,7 @@ func init() {
|
||||
initializers.LoadEnvVariables()
|
||||
}
|
||||
|
||||
// TODO: external script automatically generate language handler list from select language vise code OR consider dynamic menu generation script possibility
|
||||
func main() {
|
||||
config.LoadConfig()
|
||||
|
||||
@@ -35,18 +38,34 @@ func main() {
|
||||
var sessionId string
|
||||
var database string
|
||||
var engineDebug bool
|
||||
var gettextDir string
|
||||
var langs args.LangVar
|
||||
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
|
||||
flag.StringVar(&database, "db", "gdbm", "database to be used")
|
||||
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
|
||||
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
|
||||
flag.UintVar(&size, "s", 160, "max size of output")
|
||||
flag.StringVar(&gettextDir, "gettext", "", "use gettext translations from given directory")
|
||||
flag.Var(&langs, "language", "add symbol resolution for language")
|
||||
flag.Parse()
|
||||
|
||||
logg.Infof("start command", "dbdir", dbDir, "outputsize", size)
|
||||
|
||||
if len(langs.Langs()) == 0 {
|
||||
langs.Set(config.DefaultLanguage)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, "SessionId", sessionId)
|
||||
ctx = context.WithValue(ctx, "Database", database)
|
||||
|
||||
ln, err := lang.LanguageFromCode(config.DefaultLanguage)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "default language set error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
ctx = context.WithValue(ctx, "Language", ln)
|
||||
|
||||
pfp := path.Join(scriptDir, "pp.csv")
|
||||
|
||||
cfg := engine.Config{
|
||||
@@ -59,8 +78,11 @@ func main() {
|
||||
|
||||
resourceDir := scriptDir
|
||||
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
|
||||
if gettextDir != "" {
|
||||
menuStorageService = menuStorageService.WithGettext(gettextDir, langs.Langs())
|
||||
}
|
||||
|
||||
err := menuStorageService.EnsureDbDir()
|
||||
err = menuStorageService.EnsureDbDir()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
|
||||
34
cmd/ssh/README.md
Normal file
34
cmd/ssh/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# URDT-USSD SSH server
|
||||
|
||||
An SSH server entry point for the vise engine.
|
||||
|
||||
|
||||
## Adding public keys for access
|
||||
|
||||
Map your (client) public key to a session identifier (e.g. phone number)
|
||||
|
||||
```
|
||||
go run -v -tags logtrace ./cmd/ssh/sshkey/main.go -i <session_id> [--dbdir <dbpath>] <client_publickey_filepath>
|
||||
```
|
||||
|
||||
|
||||
## Create a private key for the server
|
||||
|
||||
```
|
||||
ssh-keygen -N "" -f <server_privatekey_filepath>
|
||||
```
|
||||
|
||||
|
||||
## Run the server
|
||||
|
||||
|
||||
```
|
||||
go run -v -tags logtrace ./cmd/ssh/main.go -h <host> -p <port> [--dbdir <dbpath>] <server_privatekey_filepath>
|
||||
```
|
||||
|
||||
|
||||
## Connect to the server
|
||||
|
||||
```
|
||||
ssh [-v] -T -p <port> -i <client_publickey_filepath> <host>
|
||||
```
|
||||
117
cmd/ssh/main.go
Normal file
117
cmd/ssh/main.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"git.defalsify.org/vise.git/db"
|
||||
"git.defalsify.org/vise.git/engine"
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/internal/ssh"
|
||||
)
|
||||
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
keyStore db.Db
|
||||
logg = logging.NewVanilla()
|
||||
scriptDir = path.Join("services", "registration")
|
||||
|
||||
build = "dev"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var dbDir string
|
||||
var resourceDir string
|
||||
var size uint
|
||||
var engineDebug bool
|
||||
var stateDebug bool
|
||||
var host string
|
||||
var port uint
|
||||
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
|
||||
flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
|
||||
flag.BoolVar(&engineDebug, "engine-debug", false, "use engine debug output")
|
||||
flag.BoolVar(&stateDebug, "state-debug", false, "use engine debug output")
|
||||
flag.UintVar(&size, "s", 160, "max size of output")
|
||||
flag.StringVar(&host, "h", "127.0.0.1", "http host")
|
||||
flag.UintVar(&port, "p", 7122, "http port")
|
||||
flag.Parse()
|
||||
|
||||
sshKeyFile := flag.Arg(0)
|
||||
_, err := os.Stat(sshKeyFile)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "cannot open ssh server private key file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
logg.WarnCtxf(ctx, "!!!!! WARNING WARNING WARNING")
|
||||
logg.WarnCtxf(ctx, "!!!!! =======================")
|
||||
logg.WarnCtxf(ctx, "!!!!! This is not a production ready server!")
|
||||
logg.WarnCtxf(ctx, "!!!!! Do not expose to internet and only use with tunnel!")
|
||||
logg.WarnCtxf(ctx, "!!!!! (See ssh -L <...>)")
|
||||
|
||||
logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size, "keyfile", sshKeyFile, "host", host, "port", port)
|
||||
|
||||
pfp := path.Join(scriptDir, "pp.csv")
|
||||
|
||||
cfg := engine.Config{
|
||||
Root: "root",
|
||||
OutputSize: uint32(size),
|
||||
FlagCount: uint32(16),
|
||||
}
|
||||
if stateDebug {
|
||||
cfg.StateDebug = true
|
||||
}
|
||||
if engineDebug {
|
||||
cfg.EngineDebug = true
|
||||
}
|
||||
|
||||
authKeyStore, err := ssh.NewSshKeyStore(ctx, dbDir)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "keystore file open error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer func() {
|
||||
logg.TraceCtxf(ctx, "shutdown auth key store reached")
|
||||
err = authKeyStore.Close()
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(ctx, "keystore close error", "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
cint := make(chan os.Signal)
|
||||
cterm := make(chan os.Signal)
|
||||
signal.Notify(cint, os.Interrupt, syscall.SIGINT)
|
||||
signal.Notify(cterm, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
runner := &ssh.SshRunner{
|
||||
Cfg: cfg,
|
||||
Debug: engineDebug,
|
||||
FlagFile: pfp,
|
||||
DbDir: dbDir,
|
||||
ResourceDir: resourceDir,
|
||||
SrvKeyFile: sshKeyFile,
|
||||
Host: host,
|
||||
Port: port,
|
||||
}
|
||||
go func() {
|
||||
select {
|
||||
case _ = <-cint:
|
||||
case _ = <-cterm:
|
||||
}
|
||||
logg.TraceCtxf(ctx, "shutdown runner reached")
|
||||
err := runner.Stop()
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(ctx, "runner stop error", "err", err)
|
||||
}
|
||||
|
||||
}()
|
||||
runner.Run(ctx, authKeyStore)
|
||||
}
|
||||
44
cmd/ssh/sshkey/main.go
Normal file
44
cmd/ssh/sshkey/main.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/internal/ssh"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var dbDir string
|
||||
var sessionId string
|
||||
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
|
||||
flag.StringVar(&sessionId, "i", "", "session id")
|
||||
flag.Parse()
|
||||
|
||||
if sessionId == "" {
|
||||
fmt.Fprintf(os.Stderr, "empty session id\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
sshKeyFile := flag.Arg(0)
|
||||
if sshKeyFile == "" {
|
||||
fmt.Fprintf(os.Stderr, "missing key file argument\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
store, err := ssh.NewSshKeyStore(ctx, dbDir)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
err = store.AddFromFile(ctx, sshKeyFile, sessionId)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/initializers"
|
||||
)
|
||||
@@ -18,6 +19,11 @@ const (
|
||||
AliasPrefix = "api/v1/alias"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultLanguage = "eng"
|
||||
languages []string
|
||||
)
|
||||
|
||||
var (
|
||||
custodialURLBase string
|
||||
dataURLBase string
|
||||
@@ -34,8 +40,28 @@ var (
|
||||
VoucherTransfersURL string
|
||||
VoucherDataURL string
|
||||
CheckAliasURL string
|
||||
DefaultLanguage string
|
||||
Languages []string
|
||||
)
|
||||
|
||||
func setLanguage() error {
|
||||
defaultLanguage = initializers.GetEnv("DEFAULT_LANGUAGE", defaultLanguage)
|
||||
languages = strings.Split(initializers.GetEnv("LANGUAGES", defaultLanguage), ",")
|
||||
haveDefaultLanguage := false
|
||||
for i, v := range(languages) {
|
||||
languages[i] = strings.ReplaceAll(v, " ", "")
|
||||
if languages[i] == defaultLanguage {
|
||||
haveDefaultLanguage = true
|
||||
}
|
||||
}
|
||||
|
||||
if !haveDefaultLanguage {
|
||||
languages = append([]string{defaultLanguage}, languages...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setBase() error {
|
||||
var err error
|
||||
|
||||
@@ -60,6 +86,10 @@ func LoadConfig() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = setLanguage()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
CreateAccountURL, _ = url.JoinPath(custodialURLBase, createAccountPath)
|
||||
TrackStatusURL, _ = url.JoinPath(custodialURLBase, trackStatusPath)
|
||||
BalanceURL, _ = url.JoinPath(custodialURLBase, balancePathPrefix)
|
||||
@@ -69,6 +99,8 @@ func LoadConfig() error {
|
||||
VoucherTransfersURL, _ = url.JoinPath(dataURLBase, voucherTransfersPathPrefix)
|
||||
VoucherDataURL, _ = url.JoinPath(dataURLBase, voucherDataPathPrefix)
|
||||
CheckAliasURL, _ = url.JoinPath(dataURLBase, AliasPrefix)
|
||||
DefaultLanguage = defaultLanguage
|
||||
Languages = languages
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
126
devtools/lang/main.go
Normal file
126
devtools/lang/main.go
Normal file
@@ -0,0 +1,126 @@
|
||||
// create language files from environment
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
"git.defalsify.org/vise.git/lang"
|
||||
"git.grassecon.net/urdt/ussd/config"
|
||||
"git.grassecon.net/urdt/ussd/initializers"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
changeHeadSrc = `LOAD reset_account_authorized 0
|
||||
LOAD reset_incorrect 0
|
||||
CATCH incorrect_pin flag_incorrect_pin 1
|
||||
CATCH pin_entry flag_account_authorized 0
|
||||
`
|
||||
|
||||
selectSrc = `LOAD set_language 6
|
||||
RELOAD set_language
|
||||
CATCH terms flag_account_created 0
|
||||
MOVE language_changed
|
||||
`
|
||||
)
|
||||
|
||||
var (
|
||||
logg = logging.NewVanilla()
|
||||
mouts string
|
||||
incmps string
|
||||
)
|
||||
|
||||
func init() {
|
||||
initializers.LoadEnvVariables()
|
||||
}
|
||||
|
||||
func toLanguageLabel(ln lang.Language) string {
|
||||
s := ln.Name
|
||||
v := strings.Split(s, " (")
|
||||
if len(v) > 1 {
|
||||
s = v[0]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func toLanguageKey(ln lang.Language) string {
|
||||
s := toLanguageLabel(ln)
|
||||
return strings.ToLower(s)
|
||||
}
|
||||
|
||||
func main() {
|
||||
var srcDir string
|
||||
|
||||
flag.StringVar(&srcDir, "o", ".", "resource dir write to")
|
||||
flag.Parse()
|
||||
|
||||
logg.Infof("start command", "dir", srcDir)
|
||||
|
||||
err := config.LoadConfig()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "config load error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
logg.Tracef("using languages", "lang", config.Languages)
|
||||
|
||||
for i, v := range(config.Languages) {
|
||||
ln, err := lang.LanguageFromCode(v)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error parsing language: %s\n", v)
|
||||
os.Exit(1)
|
||||
}
|
||||
n := i + 1
|
||||
s := toLanguageKey(ln)
|
||||
mouts += fmt.Sprintf("MOUT %s %v\n", s, n)
|
||||
v = "set_" + ln.Code
|
||||
incmps += fmt.Sprintf("INCMP %s %v\n", v, n)
|
||||
|
||||
p := path.Join(srcDir, v)
|
||||
w, err := os.OpenFile(p, os.O_WRONLY | os.O_CREATE | os.O_EXCL, 0600)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed open language set template output: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
s = toLanguageLabel(ln)
|
||||
defer w.Close()
|
||||
_, err = w.Write([]byte(s))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed write select language vis output: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
src := mouts + "HALT\n" + incmps
|
||||
src += "INCMP . *\n"
|
||||
|
||||
p := path.Join(srcDir, "select_language.vis")
|
||||
w, err := os.OpenFile(p, os.O_WRONLY | os.O_CREATE | os.O_EXCL, 0600)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed open select language vis output: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer w.Close()
|
||||
_, err = w.Write([]byte(src))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed write select language vis output: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
src = changeHeadSrc + src
|
||||
p = path.Join(srcDir, "change_language.vis")
|
||||
w, err = os.OpenFile(p, os.O_WRONLY | os.O_CREATE | os.O_EXCL, 0600)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed open select language vis output: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer w.Close()
|
||||
_, err = w.Write([]byte(src))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed write select language vis output: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,15 @@ func init() {
|
||||
}
|
||||
|
||||
|
||||
func formatItem(k []byte, v []byte) (string, error) {
|
||||
o, err := debug.FromKey(k)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
s := fmt.Sprintf("%vValue: %v\n\n", o, string(v))
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
config.LoadConfig()
|
||||
|
||||
@@ -64,12 +73,12 @@ func main() {
|
||||
if k == nil {
|
||||
break
|
||||
}
|
||||
o, err := debug.FromKey(k)
|
||||
r, err := formatItem(k, v)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
fmt.Fprintf(os.Stderr, "format db item error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("%vValue: %v\n\n", o, string(v))
|
||||
fmt.Printf(r)
|
||||
}
|
||||
|
||||
err = store.Close()
|
||||
34
internal/args/lang.go
Normal file
34
internal/args/lang.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package args
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.defalsify.org/vise.git/lang"
|
||||
)
|
||||
|
||||
type LangVar struct {
|
||||
v []lang.Language
|
||||
}
|
||||
|
||||
func(lv *LangVar) Set(s string) error {
|
||||
v, err := lang.LanguageFromCode(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lv.v = append(lv.v, v)
|
||||
return err
|
||||
}
|
||||
|
||||
func(lv *LangVar) String() string {
|
||||
var s []string
|
||||
for _, v := range(lv.v) {
|
||||
s = append(s, v.Code)
|
||||
}
|
||||
return strings.Join(s, ",")
|
||||
}
|
||||
|
||||
func(lv *LangVar) Langs() []lang.Language {
|
||||
return lv.v
|
||||
}
|
||||
|
||||
|
||||
@@ -835,7 +835,7 @@ func (h *Handlers) QuitWithHelp(ctx context.Context, sym string, input []byte) (
|
||||
l := gotext.NewLocale(translationDir, code)
|
||||
l.AddDomain("default")
|
||||
|
||||
res.Content = l.Get("For more help,please call: 0757628885")
|
||||
res.Content = l.Get("For more help, please call: 0757628885")
|
||||
res.FlagReset = append(res.FlagReset, flag_account_authorized)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
@@ -81,7 +81,8 @@ func (arp *ATRequestParser) GetInput(rq any) ([]byte, error) {
|
||||
return nil, fmt.Errorf("no input found")
|
||||
}
|
||||
|
||||
return []byte(parts[len(parts)-1]), nil
|
||||
trimmedInput := strings.TrimSpace(parts[len(parts)-1])
|
||||
return []byte(trimmedInput), nil
|
||||
}
|
||||
|
||||
func parseQueryParams(query string) map[string]string {
|
||||
|
||||
65
internal/ssh/keystore.go
Normal file
65
internal/ssh/keystore.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"git.defalsify.org/vise.git/db"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db/gdbm"
|
||||
)
|
||||
|
||||
type SshKeyStore struct {
|
||||
store db.Db
|
||||
}
|
||||
|
||||
func NewSshKeyStore(ctx context.Context, dbDir string) (*SshKeyStore, error) {
|
||||
keyStore := &SshKeyStore{}
|
||||
keyStoreFile := path.Join(dbDir, "ssh_authorized_keys.gdbm")
|
||||
keyStore.store = dbstorage.NewThreadGdbmDb()
|
||||
err := keyStore.store.Connect(ctx, keyStoreFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return keyStore, nil
|
||||
}
|
||||
|
||||
func(s *SshKeyStore) AddFromFile(ctx context.Context, fp string, sessionId string) error {
|
||||
_, err := os.Stat(fp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot open ssh server public key file: %v\n", err)
|
||||
}
|
||||
|
||||
publicBytes, err := os.ReadFile(fp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to load public key: %v", err)
|
||||
}
|
||||
pubKey, _, _, _, err := ssh.ParseAuthorizedKey(publicBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to parse public key: %v", err)
|
||||
}
|
||||
k := append([]byte{0x01}, pubKey.Marshal()...)
|
||||
s.store.SetPrefix(storage.DATATYPE_EXTEND)
|
||||
logg.Infof("Added key", "sessionId", sessionId, "public key", string(publicBytes))
|
||||
return s.store.Put(ctx, k, []byte(sessionId))
|
||||
}
|
||||
|
||||
func(s *SshKeyStore) Get(ctx context.Context, pubKey ssh.PublicKey) (string, error) {
|
||||
s.store.SetLanguage(nil)
|
||||
s.store.SetPrefix(storage.DATATYPE_EXTEND)
|
||||
k := append([]byte{0x01}, pubKey.Marshal()...)
|
||||
v, err := s.store.Get(ctx, k)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(v), nil
|
||||
}
|
||||
|
||||
func(s *SshKeyStore) Close() error {
|
||||
return s.store.Close()
|
||||
}
|
||||
287
internal/ssh/ssh.go
Normal file
287
internal/ssh/ssh.go
Normal file
@@ -0,0 +1,287 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"git.defalsify.org/vise.git/engine"
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
"git.defalsify.org/vise.git/state"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
"git.grassecon.net/urdt/ussd/remote"
|
||||
)
|
||||
|
||||
var (
|
||||
logg = logging.NewVanilla().WithDomain("ssh")
|
||||
)
|
||||
|
||||
type auther struct {
|
||||
Ctx context.Context
|
||||
keyStore *SshKeyStore
|
||||
auth map[string]string
|
||||
}
|
||||
|
||||
func NewAuther(ctx context.Context, keyStore *SshKeyStore) *auther {
|
||||
return &auther{
|
||||
Ctx: ctx,
|
||||
keyStore: keyStore,
|
||||
auth: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
func(a *auther) Check(conn ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) {
|
||||
va, err := a.keyStore.Get(a.Ctx, pubKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ka := hex.EncodeToString(conn.SessionID())
|
||||
a.auth[ka] = va
|
||||
fmt.Fprintf(os.Stderr, "connect: %s -> %s\n", ka, va)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func(a *auther) FromConn(c *ssh.ServerConn) (string, error) {
|
||||
if c == nil {
|
||||
return "", errors.New("nil server conn")
|
||||
}
|
||||
if c.Conn == nil {
|
||||
return "", errors.New("nil underlying conn")
|
||||
}
|
||||
return a.Get(c.Conn.SessionID())
|
||||
}
|
||||
|
||||
|
||||
func(a *auther) Get(k []byte) (string, error) {
|
||||
ka := hex.EncodeToString(k)
|
||||
v, ok := a.auth[ka]
|
||||
if !ok {
|
||||
return "", errors.New("not found")
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func(s *SshRunner) serve(ctx context.Context, sessionId string, ch ssh.NewChannel, en engine.Engine) error {
|
||||
if ch == nil {
|
||||
return errors.New("nil channel")
|
||||
}
|
||||
if ch.ChannelType() != "session" {
|
||||
ch.Reject(ssh.UnknownChannelType, "that is not the channel you are looking for")
|
||||
return errors.New("not a session")
|
||||
}
|
||||
channel, requests, err := ch.Accept()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer channel.Close()
|
||||
s.wg.Add(1)
|
||||
go func(reqIn <-chan *ssh.Request) {
|
||||
defer s.wg.Done()
|
||||
for req := range reqIn {
|
||||
req.Reply(req.Type == "shell", nil)
|
||||
}
|
||||
_ = requests
|
||||
}(requests)
|
||||
|
||||
cont, err := en.Exec(ctx, []byte{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("initial engine exec err: %v", err)
|
||||
}
|
||||
|
||||
var input [state.INPUT_LIMIT]byte
|
||||
for cont {
|
||||
c, err := en.Flush(ctx, channel)
|
||||
if err != nil {
|
||||
return fmt.Errorf("flush err: %v", err)
|
||||
}
|
||||
_, err = channel.Write([]byte{0x0a})
|
||||
if err != nil {
|
||||
return fmt.Errorf("newline err: %v", err)
|
||||
}
|
||||
c, err = channel.Read(input[:])
|
||||
if err != nil {
|
||||
return fmt.Errorf("read input fail: %v", err)
|
||||
}
|
||||
logg.TraceCtxf(ctx, "input read", "c", c, "input", input[:c-1])
|
||||
cont, err = en.Exec(ctx, input[:c-1])
|
||||
if err != nil {
|
||||
return fmt.Errorf("engine exec err: %v", err)
|
||||
}
|
||||
logg.TraceCtxf(ctx, "exec cont", "cont", cont, "en", en)
|
||||
_ = c
|
||||
}
|
||||
c, err := en.Flush(ctx, channel)
|
||||
if err != nil {
|
||||
return fmt.Errorf("last flush err: %v", err)
|
||||
}
|
||||
_ = c
|
||||
return nil
|
||||
}
|
||||
|
||||
type SshRunner struct {
|
||||
Ctx context.Context
|
||||
Cfg engine.Config
|
||||
FlagFile string
|
||||
DbDir string
|
||||
ResourceDir string
|
||||
Debug bool
|
||||
SrvKeyFile string
|
||||
Host string
|
||||
Port uint
|
||||
wg sync.WaitGroup
|
||||
lst net.Listener
|
||||
}
|
||||
|
||||
func(s *SshRunner) Stop() error {
|
||||
return s.lst.Close()
|
||||
}
|
||||
|
||||
func(s *SshRunner) GetEngine(sessionId string) (engine.Engine, func(), error) {
|
||||
ctx := s.Ctx
|
||||
menuStorageService := storage.NewMenuStorageService(s.DbDir, s.ResourceDir)
|
||||
|
||||
err := menuStorageService.EnsureDbDir()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
rs, err := menuStorageService.GetResource(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
pe, err := menuStorageService.GetPersister(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
userdatastore, err := menuStorageService.GetUserdataDb(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
dbResource, ok := rs.(*resource.DbResource)
|
||||
if !ok {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
lhs, err := handlers.NewLocalHandlerService(ctx, s.FlagFile, true, dbResource, s.Cfg, rs)
|
||||
lhs.SetDataStore(&userdatastore)
|
||||
lhs.SetPersister(pe)
|
||||
lhs.Cfg.SessionId = sessionId
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// TODO: clear up why pointer here and by-value other cmds
|
||||
accountService := &remote.AccountService{}
|
||||
hl, err := lhs.GetHandler(accountService)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
en := lhs.GetEngine()
|
||||
en = en.WithFirst(hl.Init)
|
||||
if s.Debug {
|
||||
en = en.WithDebug(nil)
|
||||
}
|
||||
// TODO: this is getting very hacky!
|
||||
closer := func() {
|
||||
err := menuStorageService.Close()
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(ctx, "menu storage service cleanup fail", "err", err)
|
||||
}
|
||||
}
|
||||
return en, closer, nil
|
||||
}
|
||||
|
||||
// adapted example from crypto/ssh package, NewServerConn doc
|
||||
func(s *SshRunner) Run(ctx context.Context, keyStore *SshKeyStore) {
|
||||
running := true
|
||||
|
||||
// TODO: waitgroup should probably not be global
|
||||
defer s.wg.Wait()
|
||||
|
||||
auth := NewAuther(ctx, keyStore)
|
||||
cfg := ssh.ServerConfig{
|
||||
PublicKeyCallback: auth.Check,
|
||||
}
|
||||
|
||||
privateBytes, err := os.ReadFile(s.SrvKeyFile)
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(ctx, "Failed to load private key", "err", err)
|
||||
}
|
||||
private, err := ssh.ParsePrivateKey(privateBytes)
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(ctx, "Failed to parse private key", "err", err)
|
||||
}
|
||||
srvPub := private.PublicKey()
|
||||
srvPubStr := base64.StdEncoding.EncodeToString(srvPub.Marshal())
|
||||
logg.InfoCtxf(ctx, "have server key", "type", srvPub.Type(), "public", srvPubStr)
|
||||
cfg.AddHostKey(private)
|
||||
|
||||
s.lst, err = net.Listen("tcp", fmt.Sprintf("%s:%d", s.Host, s.Port))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for running {
|
||||
conn, err := s.lst.Accept()
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(ctx, "ssh accept error", "err", err)
|
||||
running = false
|
||||
continue
|
||||
}
|
||||
|
||||
go func(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
for true {
|
||||
srvConn, nC, rC, err := ssh.NewServerConn(conn, &cfg)
|
||||
if err != nil {
|
||||
logg.InfoCtxf(ctx, "rejected client", "err", err)
|
||||
return
|
||||
}
|
||||
logg.DebugCtxf(ctx, "ssh client connected", "conn", srvConn)
|
||||
|
||||
s.wg.Add(1)
|
||||
go func() {
|
||||
ssh.DiscardRequests(rC)
|
||||
s.wg.Done()
|
||||
}()
|
||||
|
||||
sessionId, err := auth.FromConn(srvConn)
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(ctx, "Cannot find authentication")
|
||||
return
|
||||
}
|
||||
en, closer, err := s.GetEngine(sessionId)
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(ctx, "engine won't start", "err", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
err := en.Finish()
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(ctx, "engine won't stop", "err", err)
|
||||
}
|
||||
closer()
|
||||
}()
|
||||
for ch := range nC {
|
||||
err = s.serve(ctx, sessionId, ch, en)
|
||||
logg.ErrorCtxf(ctx, "ssh server finish", "err", err)
|
||||
}
|
||||
}
|
||||
}(conn)
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,10 @@ import (
|
||||
"git.defalsify.org/vise.git/persist"
|
||||
)
|
||||
|
||||
const (
|
||||
DATATYPE_EXTEND = 128
|
||||
)
|
||||
|
||||
type Storage struct {
|
||||
Persister *persist.Persister
|
||||
UserdataDb db.Db
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"git.defalsify.org/vise.git/db"
|
||||
fsdb "git.defalsify.org/vise.git/db/fs"
|
||||
"git.defalsify.org/vise.git/db/postgres"
|
||||
"git.defalsify.org/vise.git/lang"
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
"git.defalsify.org/vise.git/persist"
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
@@ -30,6 +31,7 @@ type StorageService interface {
|
||||
type MenuStorageService struct {
|
||||
dbDir string
|
||||
resourceDir string
|
||||
poResource resource.Resource
|
||||
resourceStore db.Db
|
||||
stateStore db.Db
|
||||
userDataStore db.Db
|
||||
@@ -58,6 +60,28 @@ func NewMenuStorageService(dbDir string, resourceDir string) *MenuStorageService
|
||||
}
|
||||
}
|
||||
|
||||
// WithGettext triggers use of gettext for translation of templates and menus.
|
||||
//
|
||||
// The first language in `lns` will be used as default language, to resolve node keys to
|
||||
// language strings.
|
||||
//
|
||||
// If `lns` is an empty array, gettext will not be used.
|
||||
func (ms *MenuStorageService) WithGettext(path string, lns []lang.Language) *MenuStorageService {
|
||||
if len(lns) == 0 {
|
||||
logg.Warnf("Gettext requested but no languages supplied")
|
||||
return ms
|
||||
}
|
||||
rs := resource.NewPoResource(lns[0], path)
|
||||
|
||||
for _, ln := range(lns) {
|
||||
rs = rs.WithLanguage(ln)
|
||||
}
|
||||
|
||||
ms.poResource = rs
|
||||
|
||||
return ms
|
||||
}
|
||||
|
||||
func (ms *MenuStorageService) getOrCreateDb(ctx context.Context, existingDb db.Db, fileName string) (db.Db, error) {
|
||||
database, ok := ctx.Value("Database").(string)
|
||||
if !ok {
|
||||
@@ -120,6 +144,11 @@ func (ms *MenuStorageService) GetResource(ctx context.Context) (resource.Resourc
|
||||
return nil, err
|
||||
}
|
||||
rfs := resource.NewDbResource(ms.resourceStore)
|
||||
if ms.poResource != nil {
|
||||
logg.InfoCtxf(ctx, "using poresource for menu and template")
|
||||
rfs.WithMenuGetter(ms.poResource.GetMenu)
|
||||
rfs.WithTemplateGetter(ms.poResource.GetTemplate)
|
||||
}
|
||||
return rfs, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ msgstr "Ombi lako limetumwa. %s atapokea %s %s kutoka kwa %s."
|
||||
msgid "Thank you for using Sarafu. Goodbye!"
|
||||
msgstr "Asante kwa kutumia huduma ya Sarafu. Kwaheri!"
|
||||
|
||||
msgid "For more help,please call: 0757628885"
|
||||
msgstr "Kwa usaidizi zaidi,piga: 0757628885"
|
||||
msgid "For more help, please call: 0757628885"
|
||||
msgstr "Kwa usaidizi zaidi, piga: 0757628885"
|
||||
|
||||
msgid "Balance: %s\n"
|
||||
msgstr "Salio: %s\n"
|
||||
|
||||
Reference in New Issue
Block a user