Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
b2b93f570a
|
|||
8247fc4524
|
|||
5dad44ad09
|
|||
f042183433
|
|||
b0be93a7a0
|
|||
1c143921e6
|
|||
68e63a9cf6
|
|||
c3162fec95
|
|||
1124aa781a
|
|||
eef0e9f2aa
|
|||
af38b06d22
|
|||
2fad6340c7
|
|||
03aa0a2282
|
|||
358c238f3d
|
|||
d65ac8ba2b
|
|||
55d02b8c65
|
|||
8f13eb2f16
|
BIN
bfcodegen/_test_example_2.tgz
Normal file
BIN
bfcodegen/_test_example_2.tgz
Normal file
Binary file not shown.
@@ -70,9 +70,9 @@ func GenerateCharsetIDSpecs(sourceDir string, destFile string) error {
|
||||
newChecksum := cryptext.BytesSha256([]byte(newChecksumStr))
|
||||
|
||||
if newChecksum != oldChecksum {
|
||||
fmt.Printf("[IDGenerate] Checksum has changed ( %s -> %s ), will generate new file\n\n", oldChecksum, newChecksum)
|
||||
fmt.Printf("[CSIDGenerate] Checksum has changed ( %s -> %s ), will generate new file\n\n", oldChecksum, newChecksum)
|
||||
} else {
|
||||
fmt.Printf("[IDGenerate] Checksum unchanged ( %s ), nothing to do\n", oldChecksum)
|
||||
fmt.Printf("[CSIDGenerate] Checksum unchanged ( %s ), nothing to do\n", oldChecksum)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@@ -12,8 +12,8 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
//go:embed _test_example.tgz
|
||||
var CSIDExampleModels []byte
|
||||
//go:embed _test_example_1.tgz
|
||||
var CSIDExampleModels1 []byte
|
||||
|
||||
func TestGenerateCSIDSpecs(t *testing.T) {
|
||||
|
||||
@@ -21,7 +21,7 @@ func TestGenerateCSIDSpecs(t *testing.T) {
|
||||
|
||||
tmpDir := filepath.Join(t.TempDir(), langext.MustHexUUID())
|
||||
|
||||
err := os.WriteFile(tmpFile, CSIDExampleModels, 0o777)
|
||||
err := os.WriteFile(tmpFile, CSIDExampleModels1, 0o777)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
t.Cleanup(func() { _ = os.Remove(tmpFile) })
|
||||
|
@@ -3,6 +3,7 @@ package bfcodegen
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go/format"
|
||||
@@ -14,6 +15,7 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/template"
|
||||
@@ -23,6 +25,8 @@ type EnumDefVal struct {
|
||||
VarName string
|
||||
Value string
|
||||
Description *string
|
||||
Data *map[string]any
|
||||
RawComment *string
|
||||
}
|
||||
|
||||
type EnumDef struct {
|
||||
@@ -37,7 +41,7 @@ var rexEnumPackage = rext.W(regexp.MustCompile(`^package\s+(?P<name>[A-Za-z0-9_]
|
||||
|
||||
var rexEnumDef = rext.W(regexp.MustCompile(`^\s*type\s+(?P<name>[A-Za-z0-9_]+)\s+(?P<type>[A-Za-z0-9_]+)\s*//\s*(@enum:type).*$`))
|
||||
|
||||
var rexEnumValueDef = rext.W(regexp.MustCompile(`^\s*(?P<name>[A-Za-z0-9_]+)\s+(?P<type>[A-Za-z0-9_]+)\s*=\s*(?P<value>("[A-Za-z0-9_:\s\-]+"|[0-9]+))\s*(//(?P<descr>.*))?.*$`))
|
||||
var rexEnumValueDef = rext.W(regexp.MustCompile(`^\s*(?P<name>[A-Za-z0-9_]+)\s+(?P<type>[A-Za-z0-9_]+)\s*=\s*(?P<value>("[A-Za-z0-9_:\s\-.]+"|[0-9]+))\s*(//(?P<comm>.*))?.*$`))
|
||||
|
||||
var rexEnumChecksumConst = rext.W(regexp.MustCompile(`const ChecksumEnumGenerator = "(?P<cs>[A-Za-z0-9_]*)"`))
|
||||
|
||||
@@ -46,11 +50,6 @@ var templateEnumGenerateText string
|
||||
|
||||
func GenerateEnumSpecs(sourceDir string, destFile string) error {
|
||||
|
||||
files, err := os.ReadDir(sourceDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
oldChecksum := "N/A"
|
||||
if _, err := os.Stat(destFile); !os.IsNotExist(err) {
|
||||
content, err := os.ReadFile(destFile)
|
||||
@@ -62,6 +61,30 @@ func GenerateEnumSpecs(sourceDir string, destFile string) error {
|
||||
}
|
||||
}
|
||||
|
||||
gocode, _, changed, err := _generateEnumSpecs(sourceDir, destFile, oldChecksum, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !changed {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = os.WriteFile(destFile, []byte(gocode), 0o755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func _generateEnumSpecs(sourceDir string, destFile string, oldChecksum string, gofmt bool) (string, string, bool, error) {
|
||||
|
||||
files, err := os.ReadDir(sourceDir)
|
||||
if err != nil {
|
||||
return "", "", false, err
|
||||
}
|
||||
|
||||
files = langext.ArrFilter(files, func(v os.DirEntry) bool { return v.Name() != path.Base(destFile) })
|
||||
files = langext.ArrFilter(files, func(v os.DirEntry) bool { return strings.HasSuffix(v.Name(), ".go") })
|
||||
files = langext.ArrFilter(files, func(v os.DirEntry) bool { return !strings.HasSuffix(v.Name(), "_gen.go") })
|
||||
@@ -71,7 +94,7 @@ func GenerateEnumSpecs(sourceDir string, destFile string) error {
|
||||
for _, f := range files {
|
||||
content, err := os.ReadFile(path.Join(sourceDir, f.Name()))
|
||||
if err != nil {
|
||||
return err
|
||||
return "", "", false, err
|
||||
}
|
||||
newChecksumStr += "\n" + f.Name() + "\t" + cryptext.BytesSha256(content)
|
||||
}
|
||||
@@ -82,7 +105,7 @@ func GenerateEnumSpecs(sourceDir string, destFile string) error {
|
||||
fmt.Printf("[EnumGenerate] Checksum has changed ( %s -> %s ), will generate new file\n\n", oldChecksum, newChecksum)
|
||||
} else {
|
||||
fmt.Printf("[EnumGenerate] Checksum unchanged ( %s ), nothing to do\n", oldChecksum)
|
||||
return nil
|
||||
return "", oldChecksum, false, nil
|
||||
}
|
||||
|
||||
allEnums := make([]EnumDef, 0)
|
||||
@@ -93,7 +116,7 @@ func GenerateEnumSpecs(sourceDir string, destFile string) error {
|
||||
fmt.Printf("========= %s =========\n\n", f.Name())
|
||||
fileEnums, pn, err := processEnumFile(sourceDir, path.Join(sourceDir, f.Name()))
|
||||
if err != nil {
|
||||
return err
|
||||
return "", "", false, err
|
||||
}
|
||||
|
||||
fmt.Printf("\n")
|
||||
@@ -106,20 +129,21 @@ func GenerateEnumSpecs(sourceDir string, destFile string) error {
|
||||
}
|
||||
|
||||
if pkgname == "" {
|
||||
return errors.New("no package name found in any file")
|
||||
return "", "", false, errors.New("no package name found in any file")
|
||||
}
|
||||
|
||||
fdata, err := format.Source([]byte(fmtEnumOutput(newChecksum, allEnums, pkgname)))
|
||||
rdata := fmtEnumOutput(newChecksum, allEnums, pkgname)
|
||||
|
||||
if !gofmt {
|
||||
return rdata, newChecksum, true, nil
|
||||
}
|
||||
|
||||
fdata, err := format.Source([]byte(rdata))
|
||||
if err != nil {
|
||||
return err
|
||||
return "", "", false, err
|
||||
}
|
||||
|
||||
err = os.WriteFile(destFile, fdata, 0o755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return string(fdata), newChecksum, true, nil
|
||||
}
|
||||
|
||||
func processEnumFile(basedir string, fn string) ([]EnumDef, string, error) {
|
||||
@@ -171,10 +195,34 @@ func processEnumFile(basedir string, fn string) ([]EnumDef, string, error) {
|
||||
|
||||
if match, ok := rexEnumValueDef.MatchFirst(line); ok {
|
||||
typename := match.GroupByName("type").Value()
|
||||
|
||||
comment := match.GroupByNameOrEmpty("comm").ValueOrNil()
|
||||
var descr *string = nil
|
||||
var data *map[string]any = nil
|
||||
if comment != nil {
|
||||
comment = langext.Ptr(strings.TrimSpace(*comment))
|
||||
if strings.HasPrefix(*comment, "{") {
|
||||
if v, ok := tryParseDataComment(*comment); ok {
|
||||
data = &v
|
||||
if anyDataDescr, ok := v["description"]; ok {
|
||||
if dataDescr, ok := anyDataDescr.(string); ok {
|
||||
descr = &dataDescr
|
||||
}
|
||||
}
|
||||
} else {
|
||||
descr = comment
|
||||
}
|
||||
} else {
|
||||
descr = comment
|
||||
}
|
||||
}
|
||||
|
||||
def := EnumDefVal{
|
||||
VarName: match.GroupByName("name").Value(),
|
||||
Value: match.GroupByName("value").Value(),
|
||||
Description: match.GroupByNameOrEmpty("descr").ValueOrNil(),
|
||||
RawComment: comment,
|
||||
Description: descr,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
found := false
|
||||
@@ -199,6 +247,41 @@ func processEnumFile(basedir string, fn string) ([]EnumDef, string, error) {
|
||||
return enums, pkgname, nil
|
||||
}
|
||||
|
||||
func tryParseDataComment(s string) (map[string]any, bool) {
|
||||
|
||||
r := make(map[string]any)
|
||||
|
||||
err := json.Unmarshal([]byte(s), &r)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
for _, v := range r {
|
||||
|
||||
rv := reflect.ValueOf(v)
|
||||
|
||||
if rv.Kind() == reflect.Ptr && rv.IsNil() {
|
||||
continue
|
||||
}
|
||||
if rv.Kind() == reflect.Bool {
|
||||
continue
|
||||
}
|
||||
if rv.Kind() == reflect.String {
|
||||
continue
|
||||
}
|
||||
if rv.Kind() == reflect.Int64 {
|
||||
continue
|
||||
}
|
||||
if rv.Kind() == reflect.Float64 {
|
||||
continue
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return r, true
|
||||
}
|
||||
|
||||
func fmtEnumOutput(cs string, enums []EnumDef, pkgname string) string {
|
||||
|
||||
templ := template.New("enum-generate")
|
||||
@@ -211,6 +294,47 @@ func fmtEnumOutput(cs string, enums []EnumDef, pkgname string) string {
|
||||
"hasDescr": func(v EnumDef) bool {
|
||||
return langext.ArrAll(v.Values, func(val EnumDefVal) bool { return val.Description != nil })
|
||||
},
|
||||
"hasData": func(v EnumDef) bool {
|
||||
return len(v.Values) > 0 && langext.ArrAll(v.Values, func(val EnumDefVal) bool { return val.Data != nil })
|
||||
},
|
||||
"gostr": func(v any) string {
|
||||
return fmt.Sprintf("%#+v", v)
|
||||
},
|
||||
"goobj": func(name string, v any) string {
|
||||
return fmt.Sprintf("%#+v", v)
|
||||
},
|
||||
"godatakey": func(v string) string {
|
||||
return strings.ToUpper(v[0:1]) + v[1:]
|
||||
},
|
||||
"godatavalue": func(v any) string {
|
||||
return fmt.Sprintf("%#+v", v)
|
||||
},
|
||||
"godatatype": func(v any) string {
|
||||
return fmt.Sprintf("%T", v)
|
||||
},
|
||||
"mapindex": func(v map[string]any, k string) any {
|
||||
return v[k]
|
||||
},
|
||||
"generalDataKeys": func(v EnumDef) map[string]string {
|
||||
r0 := make(map[string]int)
|
||||
|
||||
for _, eval := range v.Values {
|
||||
for k := range *eval.Data {
|
||||
if ctr, ok := r0[k]; ok {
|
||||
r0[k] = ctr + 1
|
||||
} else {
|
||||
r0[k] = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
r1 := langext.MapToArr(r0)
|
||||
r2 := langext.ArrFilter(r1, func(p langext.MapEntry[string, int]) bool { return p.Value == len(v.Values) })
|
||||
r3 := langext.ArrMap(r2, func(p langext.MapEntry[string, int]) string { return p.Key })
|
||||
r4 := langext.ArrToKVMap(r3, func(p string) string { return p }, func(p string) string { return fmt.Sprintf("%T", (*v.Values[0].Data)[p]) })
|
||||
|
||||
return r4
|
||||
},
|
||||
})
|
||||
|
||||
templ = template.Must(templ.Parse(templateEnumGenerateText))
|
||||
|
@@ -11,21 +11,38 @@ const ChecksumEnumGenerator = "{{.Checksum}}" // GoExtVersion: {{.GoextVersion}}
|
||||
|
||||
{{ $hasStr := ( . | hasStr ) }}
|
||||
{{ $hasDescr := ( . | hasDescr ) }}
|
||||
{{ $hasData := ( . | hasData ) }}
|
||||
|
||||
// ================================ {{.EnumTypeName}} ================================
|
||||
//
|
||||
// File: {{.FileRelative}}
|
||||
// StringEnum: {{$hasStr | boolToStr}}
|
||||
// DescrEnum: {{$hasDescr | boolToStr}}
|
||||
// DataEnum: {{$hasData | boolToStr}}
|
||||
//
|
||||
|
||||
{{ $typename := .EnumTypeName }}
|
||||
{{ $enumdef := . }}
|
||||
|
||||
var __{{.EnumTypeName}}Values = []{{.EnumTypeName}}{ {{range .Values}}
|
||||
{{.VarName}}, {{end}}
|
||||
}
|
||||
|
||||
{{if $hasDescr}}
|
||||
var __{{.EnumTypeName}}Descriptions = map[{{.EnumTypeName}}]string{ {{range .Values}}
|
||||
{{.VarName}}: "{{.Description | deref | trimSpace}}", {{end}}
|
||||
{{.VarName}}: {{.Description | deref | trimSpace | gostr}}, {{end}}
|
||||
}
|
||||
{{end}}
|
||||
|
||||
{{if $hasData}}
|
||||
type {{ .EnumTypeName }}Data struct { {{ range $datakey, $datatype := ($enumdef | generalDataKeys) }}
|
||||
{{ $datakey | godatakey }} {{ $datatype }} `json:"{{ $datakey }}"` {{ end }}
|
||||
}
|
||||
|
||||
var __{{.EnumTypeName}}Data = map[{{.EnumTypeName}}]{{.EnumTypeName}}Data{ {{range .Values}} {{ $enumvalue := . }}
|
||||
{{.VarName}}: {{ $typename }}Data{ {{ range $datakey, $datatype := $enumdef | generalDataKeys }}
|
||||
{{ $datakey | godatakey }}: {{ (mapindex $enumvalue.Data $datakey) | godatavalue }}, {{ end }}
|
||||
}, {{end}}
|
||||
}
|
||||
{{end}}
|
||||
|
||||
@@ -64,6 +81,15 @@ func (e {{.EnumTypeName}}) Description() string {
|
||||
}
|
||||
{{end}}
|
||||
|
||||
{{if $hasData}}
|
||||
func (e {{.EnumTypeName}}) Data() {{.EnumTypeName}}Data {
|
||||
if d, ok := __{{.EnumTypeName}}Data[e]; ok {
|
||||
return d
|
||||
}
|
||||
return {{.EnumTypeName}}Data{}
|
||||
}
|
||||
{{end}}
|
||||
|
||||
func (e {{.EnumTypeName}}) VarName() string {
|
||||
if d, ok := __{{.EnumTypeName}}Varnames[e]; ok {
|
||||
return d
|
||||
@@ -75,6 +101,12 @@ func (e {{.EnumTypeName}}) Meta() enums.EnumMetaValue {
|
||||
{{if $hasDescr}} return enums.EnumMetaValue{VarName: e.VarName(), Value: e, Description: langext.Ptr(e.Description())} {{else}} return enums.EnumMetaValue{VarName: e.VarName(), Value: e, Description: nil} {{end}}
|
||||
}
|
||||
|
||||
{{if $hasDescr}}
|
||||
func (e {{.EnumTypeName}}) DescriptionMeta() enums.EnumDescriptionMetaValue {
|
||||
return enums.EnumDescriptionMetaValue{VarName: e.VarName(), Value: e, Description: e.Description()}
|
||||
}
|
||||
{{end}}
|
||||
|
||||
func Parse{{.EnumTypeName}}(vv string) ({{.EnumTypeName}}, bool) {
|
||||
for _, ev := range __{{.EnumTypeName}}Values {
|
||||
if string(ev) == vv {
|
||||
@@ -94,4 +126,12 @@ func {{.EnumTypeName}}ValuesMeta() []enums.EnumMetaValue {
|
||||
}
|
||||
}
|
||||
|
||||
{{if $hasDescr}}
|
||||
func {{.EnumTypeName}}ValuesDescriptionMeta() []enums.EnumDescriptionMetaValue {
|
||||
return []enums.EnumDescriptionMetaValue{ {{range .Values}}
|
||||
{{.VarName}}.DescriptionMeta(), {{end}}
|
||||
}
|
||||
}
|
||||
{{end}}
|
||||
|
||||
{{end}}
|
@@ -12,8 +12,11 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
//go:embed _test_example.tgz
|
||||
var EnumExampleModels []byte
|
||||
//go:embed _test_example_1.tgz
|
||||
var EnumExampleModels1 []byte
|
||||
|
||||
//go:embed _test_example_2.tgz
|
||||
var EnumExampleModels2 []byte
|
||||
|
||||
func TestGenerateEnumSpecs(t *testing.T) {
|
||||
|
||||
@@ -21,7 +24,7 @@ func TestGenerateEnumSpecs(t *testing.T) {
|
||||
|
||||
tmpDir := filepath.Join(t.TempDir(), langext.MustHexUUID())
|
||||
|
||||
err := os.WriteFile(tmpFile, EnumExampleModels, 0o777)
|
||||
err := os.WriteFile(tmpFile, EnumExampleModels1, 0o777)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
t.Cleanup(func() { _ = os.Remove(tmpFile) })
|
||||
@@ -34,17 +37,53 @@ func TestGenerateEnumSpecs(t *testing.T) {
|
||||
_, err = cmdext.Runner("tar").Arg("-xvzf").Arg(tmpFile).Arg("-C").Arg(tmpDir).FailOnExitCode().FailOnTimeout().Timeout(time.Minute).Run()
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
err = GenerateEnumSpecs(tmpDir, tmpDir+"/enums_gen.go")
|
||||
s1, cs1, _, err := _generateEnumSpecs(tmpDir, "", "N/A", true)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
err = GenerateEnumSpecs(tmpDir, tmpDir+"/enums_gen.go")
|
||||
s2, cs2, _, err := _generateEnumSpecs(tmpDir, "", "N/A", true)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
tst.AssertEqual(t, cs1, cs2)
|
||||
tst.AssertEqual(t, s1, s2)
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println()
|
||||
fmt.Println()
|
||||
fmt.Println("=====================================================================================================")
|
||||
fmt.Println(string(tst.Must(os.ReadFile(tmpDir + "/enums_gen.go"))(t)))
|
||||
fmt.Println(s1)
|
||||
fmt.Println("=====================================================================================================")
|
||||
fmt.Println()
|
||||
fmt.Println()
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
func TestGenerateEnumSpecsData(t *testing.T) {
|
||||
|
||||
tmpFile := filepath.Join(t.TempDir(), langext.MustHexUUID()+".tgz")
|
||||
|
||||
tmpDir := filepath.Join(t.TempDir(), langext.MustHexUUID())
|
||||
|
||||
err := os.WriteFile(tmpFile, EnumExampleModels2, 0o777)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
t.Cleanup(func() { _ = os.Remove(tmpFile) })
|
||||
|
||||
err = os.Mkdir(tmpDir, 0o777)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
t.Cleanup(func() { _ = os.RemoveAll(tmpFile) })
|
||||
|
||||
_, err = cmdext.Runner("tar").Arg("-xvzf").Arg(tmpFile).Arg("-C").Arg(tmpDir).FailOnExitCode().FailOnTimeout().Timeout(time.Minute).Run()
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
s1, _, _, err := _generateEnumSpecs(tmpDir, "", "", true)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println()
|
||||
fmt.Println()
|
||||
fmt.Println("=====================================================================================================")
|
||||
fmt.Println(s1)
|
||||
fmt.Println("=====================================================================================================")
|
||||
fmt.Println()
|
||||
fmt.Println()
|
||||
|
@@ -12,8 +12,8 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
//go:embed _test_example.tgz
|
||||
var IDExampleModels []byte
|
||||
//go:embed _test_example_1.tgz
|
||||
var IDExampleModels1 []byte
|
||||
|
||||
func TestGenerateIDSpecs(t *testing.T) {
|
||||
|
||||
@@ -21,7 +21,7 @@ func TestGenerateIDSpecs(t *testing.T) {
|
||||
|
||||
tmpDir := filepath.Join(t.TempDir(), langext.MustHexUUID())
|
||||
|
||||
err := os.WriteFile(tmpFile, IDExampleModels, 0o777)
|
||||
err := os.WriteFile(tmpFile, IDExampleModels1, 0o777)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
t.Cleanup(func() { _ = os.Remove(tmpFile) })
|
||||
|
@@ -15,10 +15,17 @@ type StringEnum interface {
|
||||
type DescriptionEnum interface {
|
||||
Enum
|
||||
Description() string
|
||||
DescriptionMeta() EnumDescriptionMetaValue
|
||||
}
|
||||
|
||||
type EnumMetaValue struct {
|
||||
VarName string `json:"varName"`
|
||||
Value any `json:"value"`
|
||||
Value Enum `json:"value"`
|
||||
Description *string `json:"description"`
|
||||
}
|
||||
|
||||
type EnumDescriptionMetaValue struct {
|
||||
VarName string `json:"varName"`
|
||||
Value Enum `json:"value"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
@@ -59,6 +59,9 @@ var (
|
||||
TypeMarshalEntityID = NewType("MARSHAL_ENTITY_ID", langext.Ptr(400))
|
||||
TypeInvalidCSID = NewType("INVALID_CSID", langext.Ptr(400))
|
||||
|
||||
TypeGoogleStatuscode = NewType("GOOGLE_STATUSCODE", langext.Ptr(400))
|
||||
TypeGoogleResponse = NewType("GOOGLE_RESPONSE", langext.Ptr(400))
|
||||
|
||||
TypeUnauthorized = NewType("UNAUTHORIZED", langext.Ptr(401))
|
||||
TypeAuthFailed = NewType("AUTH_FAILED", langext.Ptr(401))
|
||||
|
||||
|
@@ -6,8 +6,10 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/mathext"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/rext"
|
||||
"net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -126,8 +128,8 @@ func (w *GinWrapper) DebugPrintRoutes() {
|
||||
line := [4]string{
|
||||
spec.Method,
|
||||
spec.URL,
|
||||
strings.Join(spec.Middlewares, " -> "),
|
||||
spec.Handler,
|
||||
strings.Join(langext.ArrMap(spec.Middlewares, w.cleanMiddlewareName), " -> "),
|
||||
w.cleanMiddlewareName(spec.Handler),
|
||||
}
|
||||
|
||||
lines = append(lines, line)
|
||||
@@ -138,12 +140,40 @@ func (w *GinWrapper) DebugPrintRoutes() {
|
||||
pad[3] = mathext.Max(pad[3], len(line[3]))
|
||||
}
|
||||
|
||||
fmt.Printf("Gin-Routes:\n")
|
||||
fmt.Printf("{\n")
|
||||
for _, line := range lines {
|
||||
|
||||
fmt.Printf("Gin-Route: %s %s --> %s --> %s\n",
|
||||
fmt.Printf(" %s %s --> %s --> %s\n",
|
||||
langext.StrPadRight("["+line[0]+"]", " ", pad[0]+2),
|
||||
langext.StrPadRight(line[1], " ", pad[1]),
|
||||
langext.StrPadRight(line[2], " ", pad[2]),
|
||||
langext.StrPadRight(line[3], " ", pad[3]))
|
||||
}
|
||||
fmt.Printf("}\n")
|
||||
}
|
||||
|
||||
func (w *GinWrapper) cleanMiddlewareName(fname string) string {
|
||||
|
||||
funcSuffix := rext.W(regexp.MustCompile(`\.func[0-9]+(?:\.[0-9]+)*$`))
|
||||
if match, ok := funcSuffix.MatchFirst(fname); ok {
|
||||
fname = fname[:len(fname)-match.FullMatch().Length()]
|
||||
}
|
||||
|
||||
if strings.HasSuffix(fname, ".(*GinRoutesWrapper).WithJSONFilter") {
|
||||
fname = "[JSONFilter]"
|
||||
}
|
||||
|
||||
if fname == "ginext.BodyBuffer" {
|
||||
fname = "[BodyBuffer]"
|
||||
}
|
||||
|
||||
skipPrefixes := []string{"api.(*Handler).", "api.", "ginext.", "handler.", "admin-app.", "employee-app.", "employer-app."}
|
||||
for _, pfx := range skipPrefixes {
|
||||
if strings.HasPrefix(fname, pfx) {
|
||||
fname = fname[len(pfx):]
|
||||
}
|
||||
}
|
||||
|
||||
return fname
|
||||
}
|
||||
|
@@ -3,11 +3,9 @@ package ginext
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/rext"
|
||||
"net/http"
|
||||
"path"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
@@ -196,12 +194,6 @@ func nameOfFunction(f any) string {
|
||||
fname = fname[:len(fname)-len("-fm")]
|
||||
}
|
||||
|
||||
suffix := rext.W(regexp.MustCompile(`\.func[0-9]+(?:\.[0-9]+)*$`))
|
||||
|
||||
if match, ok := suffix.MatchFirst(fname); ok {
|
||||
fname = fname[:len(fname)-match.FullMatch().Length()]
|
||||
}
|
||||
|
||||
return fname
|
||||
}
|
||||
|
||||
|
2
go.mod
2
go.mod
@@ -7,7 +7,7 @@ require (
|
||||
github.com/jmoiron/sqlx v1.3.5
|
||||
github.com/rs/xid v1.5.0
|
||||
github.com/rs/zerolog v1.31.0
|
||||
go.mongodb.org/mongo-driver v1.13.0
|
||||
go.mongodb.org/mongo-driver v1.13.1
|
||||
golang.org/x/crypto v0.16.0
|
||||
golang.org/x/sys v0.15.0
|
||||
golang.org/x/term v0.15.0
|
||||
|
2
go.sum
2
go.sum
@@ -126,6 +126,8 @@ go.mongodb.org/mongo-driver v1.12.1 h1:nLkghSU8fQNaK7oUmDhQFsnrtcoNy7Z6LVFKsEecq
|
||||
go.mongodb.org/mongo-driver v1.12.1/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ=
|
||||
go.mongodb.org/mongo-driver v1.13.0 h1:67DgFFjYOCMWdtTEmKFpV3ffWlFnh+CYZ8ZS/tXWUfY=
|
||||
go.mongodb.org/mongo-driver v1.13.0/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ=
|
||||
go.mongodb.org/mongo-driver v1.13.1 h1:YIc7HTYsKndGK4RFzJ3covLz1byri52x0IoMB0Pt/vk=
|
||||
go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y=
|
||||
golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
|
@@ -1,5 +1,5 @@
|
||||
package goext
|
||||
|
||||
const GoextVersion = "0.0.327"
|
||||
const GoextVersion = "0.0.345"
|
||||
|
||||
const GoextVersionTimestamp = "2023-12-02T13:15:19+0100"
|
||||
const GoextVersionTimestamp = "2023-12-07T19:39:31+0100"
|
||||
|
54
googleapi/README.md
Normal file
54
googleapi/README.md
Normal file
@@ -0,0 +1,54 @@
|
||||
|
||||
Google OAuth Setup (to send mails)
|
||||
==================================
|
||||
|
||||
|
||||
- Login @ https://console.cloud.google.com
|
||||
|
||||
- GMail API akivieren: https://console.cloud.google.com/apis/library/gmail.googleapis.com?
|
||||
|
||||
- Create new Project (aka 'BackendMailAPI') @ https://console.cloud.google.com/projectcreate
|
||||
User Type: Intern
|
||||
Anwendungsname: 'BackendMailAPI'
|
||||
Support-Email: ...
|
||||
Authorisierte Domains: 'heydyno.de' (or project domain)
|
||||
Kontakt-Email: ...
|
||||
|
||||
|
||||
- Unter "Anmeldedaten" neuer OAuth Client erstellen @ https://console.cloud.google.com/apis/credentials
|
||||
Anwendungstyp: Web
|
||||
Name: 'BackendMailOAuth'
|
||||
Redirect-Uri: 'http://localhost/oauth'
|
||||
Client-ID und Client-Key merken
|
||||
|
||||
- Open in Browser:
|
||||
https://accounts.google.com/o/oauth2/v2/auth?redirect_uri=http://localhost/oauth&prompt=consent&response_type=code&client_id={...}&scope=https://www.googleapis.com/auth/gmail.send&access_type=offline
|
||||
Code aus redirected URI merken
|
||||
|
||||
- Code via request einlösen (und refresh_roken merken):
|
||||
|
||||
```
|
||||
curl --request POST \
|
||||
--url https://oauth2.googleapis.com/token \
|
||||
--data code={...} \
|
||||
--data redirect_uri=http://localhost/oauth \
|
||||
--data client_id={...} \
|
||||
--data client_secret={...} \
|
||||
--data grant_type=authorization_code \
|
||||
--data scope=https://www.googleapis.com/auth/gmail.send
|
||||
```
|
||||
|
||||
- Fertig, mit `client_id`, `client_secret` und `refresh_token` kann das package benutzt werden
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
46
googleapi/attachment.go
Normal file
46
googleapi/attachment.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package googleapi
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type MailAttachment struct {
|
||||
IsInline bool
|
||||
ContentType string
|
||||
Filename string
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func (a MailAttachment) dump() []string {
|
||||
res := make([]string, 0, 4)
|
||||
|
||||
if a.ContentType != "" {
|
||||
res = append(res, "Content-Type: "+a.ContentType+"; charset=UTF-8")
|
||||
}
|
||||
|
||||
res = append(res, "Content-Transfer-Encoding: base64")
|
||||
|
||||
if a.IsInline {
|
||||
if a.Filename != "" {
|
||||
res = append(res, fmt.Sprintf("Content-Disposition: inline;filename=\"%s\"", a.Filename))
|
||||
} else {
|
||||
res = append(res, "Content-Disposition: inline")
|
||||
}
|
||||
} else {
|
||||
if a.Filename != "" {
|
||||
res = append(res, fmt.Sprintf("Content-Disposition: attachment;filename=\"%s\"", a.Filename))
|
||||
} else {
|
||||
res = append(res, "Content-Disposition: attachment")
|
||||
}
|
||||
}
|
||||
|
||||
b64 := base64.StdEncoding.EncodeToString(a.Data)
|
||||
for i := 0; i < len(b64); i += 80 {
|
||||
res = append(res, b64[i:min(i+80, len(b64))])
|
||||
}
|
||||
|
||||
res = append(res)
|
||||
|
||||
return res
|
||||
}
|
6
googleapi/body.go
Normal file
6
googleapi/body.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package googleapi
|
||||
|
||||
type MailBody struct {
|
||||
Plain string
|
||||
HTML string
|
||||
}
|
224
googleapi/mimeMessage.go
Normal file
224
googleapi/mimeMessage.go
Normal file
@@ -0,0 +1,224 @@
|
||||
package googleapi
|
||||
|
||||
import (
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
"mime"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/rfc2822
|
||||
func encodeMimeMail(from string, recipients []string, cc []string, bcc []string, subject string, body MailBody, attachments []MailAttachment) string {
|
||||
|
||||
data := make([]string, 0, 32)
|
||||
|
||||
data = append(data, "Date: "+time.Now().Format(time.RFC1123Z))
|
||||
data = append(data, "MIME-Version: 1.0")
|
||||
data = append(data, "From: "+mime.QEncoding.Encode("UTF-8", from))
|
||||
data = append(data, "To: "+strings.Join(langext.ArrMap(recipients, func(v string) string { return mime.QEncoding.Encode("UTF-8", v) }), ", "))
|
||||
if len(cc) > 0 {
|
||||
data = append(data, "To: "+strings.Join(langext.ArrMap(cc, func(v string) string { return mime.QEncoding.Encode("UTF-8", v) }), ", "))
|
||||
}
|
||||
if len(bcc) > 0 {
|
||||
data = append(data, "Bcc: "+strings.Join(langext.ArrMap(bcc, func(v string) string { return mime.QEncoding.Encode("UTF-8", v) }), ", "))
|
||||
}
|
||||
data = append(data, "Subject: "+mime.QEncoding.Encode("UTF-8", subject))
|
||||
|
||||
hasInlineAttachments := langext.ArrAny(attachments, func(v MailAttachment) bool { return v.IsInline })
|
||||
hasNormalAttachments := langext.ArrAny(attachments, func(v MailAttachment) bool { return !v.IsInline })
|
||||
hasPlain := body.Plain != ""
|
||||
hasHTML := body.HTML != ""
|
||||
|
||||
mixedBoundary := langext.MustRawHexUUID()
|
||||
relatedBoundary := langext.MustRawHexUUID()
|
||||
altBoundary := langext.MustRawHexUUID()
|
||||
|
||||
inlineAttachments := langext.ArrFilter(attachments, func(v MailAttachment) bool { return v.IsInline })
|
||||
normalAttachments := langext.ArrFilter(attachments, func(v MailAttachment) bool { return !v.IsInline })
|
||||
|
||||
if hasInlineAttachments && hasNormalAttachments {
|
||||
// "mixed+related"
|
||||
|
||||
data = append(data, "Content-Type: multipart/mixed; boundary="+mixedBoundary)
|
||||
data = append(data, "")
|
||||
data = append(data, "--"+mixedBoundary)
|
||||
|
||||
data = append(data, "Content-Type: multipart/related; boundary="+relatedBoundary)
|
||||
data = append(data, "")
|
||||
|
||||
data = append(data, dumpMailBody(body, hasInlineAttachments, hasNormalAttachments, relatedBoundary, altBoundary)...)
|
||||
data = append(data, "")
|
||||
|
||||
for i, attachment := range inlineAttachments {
|
||||
data = append(data, "--"+relatedBoundary)
|
||||
data = append(data, attachment.dump()...)
|
||||
|
||||
if i < len(inlineAttachments)-1 {
|
||||
data = append(data, "")
|
||||
}
|
||||
}
|
||||
|
||||
data = append(data, "--"+relatedBoundary+"--")
|
||||
|
||||
for i, attachment := range normalAttachments {
|
||||
data = append(data, "--"+mixedBoundary)
|
||||
data = append(data, attachment.dump()...)
|
||||
|
||||
if i < len(normalAttachments)-1 {
|
||||
data = append(data, "")
|
||||
}
|
||||
}
|
||||
|
||||
data = append(data, "--"+mixedBoundary+"--")
|
||||
|
||||
} else if hasNormalAttachments {
|
||||
// "mixed"
|
||||
|
||||
data = append(data, "Content-Type: multipart/mixed; boundary="+mixedBoundary)
|
||||
data = append(data, "")
|
||||
|
||||
data = append(data, dumpMailBody(body, hasInlineAttachments, hasNormalAttachments, mixedBoundary, altBoundary)...)
|
||||
if hasPlain && hasHTML {
|
||||
data = append(data, "")
|
||||
}
|
||||
|
||||
for i, attachment := range normalAttachments {
|
||||
data = append(data, "--"+mixedBoundary)
|
||||
data = append(data, attachment.dump()...)
|
||||
|
||||
if i < len(normalAttachments)-1 {
|
||||
data = append(data, "")
|
||||
}
|
||||
}
|
||||
|
||||
data = append(data, "--"+mixedBoundary+"--")
|
||||
|
||||
} else if hasInlineAttachments {
|
||||
// "related"
|
||||
|
||||
data = append(data, "Content-Type: multipart/related; boundary="+relatedBoundary)
|
||||
data = append(data, "")
|
||||
|
||||
data = append(data, dumpMailBody(body, hasInlineAttachments, hasNormalAttachments, relatedBoundary, altBoundary)...)
|
||||
data = append(data, "")
|
||||
|
||||
for i, attachment := range inlineAttachments {
|
||||
data = append(data, "--"+relatedBoundary)
|
||||
data = append(data, attachment.dump()...)
|
||||
|
||||
if i < len(inlineAttachments)-1 {
|
||||
data = append(data, "")
|
||||
}
|
||||
}
|
||||
|
||||
data = append(data, "--"+relatedBoundary+"--")
|
||||
|
||||
} else if hasPlain && hasHTML {
|
||||
// "alternative"
|
||||
|
||||
data = append(data, "Content-Type: multipart/alternative; boundary="+altBoundary)
|
||||
data = append(data, "")
|
||||
|
||||
data = append(data, dumpMailBody(body, hasInlineAttachments, hasNormalAttachments, altBoundary, altBoundary)...)
|
||||
data = append(data, "")
|
||||
|
||||
data = append(data, "--"+altBoundary+"--")
|
||||
|
||||
} else if hasPlain {
|
||||
// "plain"
|
||||
|
||||
data = append(data, "Content-Type: text/plain; charset=UTF-8")
|
||||
data = append(data, "Content-Transfer-Encoding: 7bit")
|
||||
data = append(data, "")
|
||||
data = append(data, body.Plain)
|
||||
|
||||
} else if hasHTML {
|
||||
// "plain"
|
||||
|
||||
data = append(data, "Content-Type: text/html; charset=UTF-8")
|
||||
data = append(data, "Content-Transfer-Encoding: 7bit")
|
||||
data = append(data, "")
|
||||
data = append(data, body.HTML)
|
||||
|
||||
} else {
|
||||
// "empty??"
|
||||
|
||||
}
|
||||
|
||||
return strings.Join(data, "\r\n")
|
||||
}
|
||||
|
||||
func dumpMailBody(body MailBody, hasInlineAttachments bool, hasNormalAttachments bool, boundary string, boundaryAlt string) []string {
|
||||
|
||||
if body.HTML != "" && body.Plain != "" && !hasInlineAttachments && hasNormalAttachments {
|
||||
data := make([]string, 0, 16)
|
||||
data = append(data, "--"+boundary)
|
||||
data = append(data, "Content-Type: multipart/alternative; boundary="+boundaryAlt)
|
||||
data = append(data, "")
|
||||
data = append(data, "--"+boundaryAlt)
|
||||
data = append(data, "Content-Type: text/plain; charset=UTF-8")
|
||||
data = append(data, "Content-Transfer-Encoding: 7bit")
|
||||
data = append(data, "")
|
||||
data = append(data, body.Plain)
|
||||
data = append(data, "")
|
||||
data = append(data, "--"+boundaryAlt)
|
||||
data = append(data, "Content-Type: text/html; charset=UTF-8")
|
||||
data = append(data, "Content-Transfer-Encoding: 7bit")
|
||||
data = append(data, "")
|
||||
data = append(data, body.HTML)
|
||||
data = append(data, "")
|
||||
data = append(data, "--"+boundaryAlt+"--")
|
||||
return data
|
||||
}
|
||||
|
||||
if body.HTML != "" && body.Plain != "" && hasInlineAttachments {
|
||||
data := make([]string, 0, 2)
|
||||
data = append(data, "--"+boundary)
|
||||
data = append(data, body.HTML)
|
||||
return data
|
||||
}
|
||||
|
||||
if body.HTML != "" && body.Plain != "" {
|
||||
data := make([]string, 0, 8)
|
||||
data = append(data, "--"+boundary)
|
||||
data = append(data, "Content-Type: text/plain; charset=UTF-8")
|
||||
data = append(data, "Content-Transfer-Encoding: 7bit")
|
||||
data = append(data, "")
|
||||
data = append(data, body.Plain)
|
||||
data = append(data, "")
|
||||
data = append(data, "--"+boundary)
|
||||
data = append(data, "Content-Type: text/html; charset=UTF-8")
|
||||
data = append(data, "Content-Transfer-Encoding: 7bit")
|
||||
data = append(data, "")
|
||||
data = append(data, body.HTML)
|
||||
return data
|
||||
}
|
||||
|
||||
if body.HTML != "" {
|
||||
data := make([]string, 0, 2)
|
||||
data = append(data, "--"+boundary)
|
||||
data = append(data, "Content-Type: text/html; charset=UTF-8")
|
||||
data = append(data, "Content-Transfer-Encoding: 7bit")
|
||||
data = append(data, "")
|
||||
data = append(data, body.HTML)
|
||||
return data
|
||||
}
|
||||
|
||||
if body.Plain != "" {
|
||||
data := make([]string, 0, 2)
|
||||
data = append(data, "--"+boundary)
|
||||
data = append(data, "Content-Type: text/plain; charset=UTF-8")
|
||||
data = append(data, "Content-Transfer-Encoding: 7bit")
|
||||
data = append(data, "")
|
||||
data = append(data, body.Plain)
|
||||
return data
|
||||
}
|
||||
|
||||
data := make([]string, 0, 16)
|
||||
data = append(data, "--"+boundary)
|
||||
data = append(data, "Content-Type: text/plain; charset=UTF-8")
|
||||
data = append(data, "Content-Transfer-Encoding: 7bit")
|
||||
data = append(data, "")
|
||||
data = append(data, "") // no content ?!?
|
||||
return data
|
||||
}
|
77
googleapi/mimeMessage_test.go
Normal file
77
googleapi/mimeMessage_test.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package googleapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/tst"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEncodeMimeMail(t *testing.T) {
|
||||
|
||||
mail := encodeMimeMail(
|
||||
"noreply@heydyno.de",
|
||||
[]string{"trash@mikescher.de"},
|
||||
nil,
|
||||
nil,
|
||||
"Hello Test Mail",
|
||||
MailBody{Plain: "Plain Text"},
|
||||
nil)
|
||||
|
||||
fmt.Printf("%s\n\n", mail)
|
||||
}
|
||||
|
||||
func TestEncodeMimeMail2(t *testing.T) {
|
||||
|
||||
mail := encodeMimeMail(
|
||||
"noreply@heydyno.de",
|
||||
[]string{"trash@mikescher.de"},
|
||||
nil,
|
||||
nil,
|
||||
"Hello Test Mail (alternative)",
|
||||
MailBody{
|
||||
Plain: "Plain Text",
|
||||
HTML: "<html><body><u>Non</u> Pl<i>ai</i>n T<b>ex</b>t</body></html>",
|
||||
},
|
||||
nil)
|
||||
|
||||
fmt.Printf("%s\n\n", mail)
|
||||
}
|
||||
|
||||
func TestEncodeMimeMail3(t *testing.T) {
|
||||
|
||||
mail := encodeMimeMail(
|
||||
"noreply@heydyno.de",
|
||||
[]string{"trash@mikescher.de"},
|
||||
nil,
|
||||
nil,
|
||||
"Hello Test Mail (alternative)",
|
||||
MailBody{
|
||||
HTML: "<html><body><u>Non</u> Pl<i>ai</i>n T<b>ex</b>t</body></html>",
|
||||
},
|
||||
[]MailAttachment{
|
||||
{Data: []byte("HelloWorld"), Filename: "test.txt", IsInline: false, ContentType: "text/plain"},
|
||||
})
|
||||
|
||||
fmt.Printf("%s\n\n", mail)
|
||||
}
|
||||
|
||||
func TestEncodeMimeMail4(t *testing.T) {
|
||||
|
||||
b := tst.Must(os.ReadFile("/home/mike/Pictures/Screenshot_20220706_190205.png"))(t)
|
||||
|
||||
mail := encodeMimeMail(
|
||||
"noreply@heydyno.de",
|
||||
[]string{"trash@mikescher.de"},
|
||||
nil,
|
||||
nil,
|
||||
"Hello Test Mail (inline)",
|
||||
MailBody{
|
||||
HTML: "<html><body><u>Non</u> Pl<i>ai</i>n T<b>ex</b>t</body></html>",
|
||||
},
|
||||
[]MailAttachment{
|
||||
{Data: b, Filename: "img.png", IsInline: true, ContentType: "image/png"},
|
||||
})
|
||||
|
||||
fmt.Printf("%s\n\n", mail)
|
||||
}
|
91
googleapi/oAuth.go
Normal file
91
googleapi/oAuth.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package googleapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/exerr"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/timeext"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type GoogleOAuth interface {
|
||||
AccessToken() (string, error)
|
||||
}
|
||||
|
||||
type oauth struct {
|
||||
clientID string
|
||||
clientSecret string
|
||||
refreshToken string
|
||||
|
||||
lock sync.RWMutex
|
||||
accessToken *string
|
||||
expiryDate *time.Time
|
||||
}
|
||||
|
||||
func NewGoogleOAuth(clientid string, clientsecret, refreshtoken string) GoogleOAuth {
|
||||
return &oauth{
|
||||
clientID: clientid,
|
||||
clientSecret: clientsecret,
|
||||
refreshToken: refreshtoken,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *oauth) AccessToken() (string, error) {
|
||||
c.lock.RLock()
|
||||
if c.accessToken != nil && c.expiryDate != nil && (*c.expiryDate).After(time.Now()) {
|
||||
c.lock.RUnlock()
|
||||
return *c.accessToken, nil // still valid
|
||||
}
|
||||
c.lock.RUnlock()
|
||||
|
||||
httpclient := http.Client{}
|
||||
|
||||
url := fmt.Sprintf("https://oauth2.googleapis.com/token?client_id=%s&client_secret=%s&grant_type=%s&refresh_token=%s",
|
||||
c.clientID,
|
||||
c.clientSecret,
|
||||
"refresh_token",
|
||||
c.refreshToken)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, url, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
reqStartTime := time.Now()
|
||||
|
||||
res, err := httpclient.Do(req)
|
||||
|
||||
type response struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
Scope string `json:"scope"`
|
||||
TokenType string `json:"token_type"`
|
||||
}
|
||||
|
||||
var r response
|
||||
|
||||
data, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(data, &r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if r.ExpiresIn == 0 || r.AccessToken == "" {
|
||||
return "", exerr.New(exerr.TypeGoogleResponse, "google oauth returned no response").Str("body", string(data)).Build()
|
||||
}
|
||||
|
||||
c.lock.Lock()
|
||||
c.expiryDate = langext.Ptr(reqStartTime.Add(timeext.FromSeconds(r.ExpiresIn - 10)))
|
||||
c.accessToken = langext.Ptr(r.AccessToken)
|
||||
c.lock.Unlock()
|
||||
|
||||
return r.AccessToken, nil
|
||||
}
|
69
googleapi/sendMail.go
Normal file
69
googleapi/sendMail.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package googleapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/exerr"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type MailRef struct {
|
||||
ID string `json:"id"`
|
||||
ThreadID string `json:"threadId"`
|
||||
LabelIDs []string `json:"labelIds"`
|
||||
}
|
||||
|
||||
func (c *client) SendMail(ctx context.Context, from string, recipients []string, cc []string, bcc []string, subject string, body MailBody, attachments []MailAttachment) (MailRef, error) {
|
||||
|
||||
mm := encodeMimeMail(from, recipients, cc, bcc, subject, body, attachments)
|
||||
|
||||
tok, err := c.oauth.AccessToken()
|
||||
if err != nil {
|
||||
return MailRef{}, exerr.Wrap(err, "").Build()
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("https://gmail.googleapis.com/gmail/v1/users/%s/messages/send?alt=json&prettyPrint=false", "me")
|
||||
|
||||
msgbody, err := json.Marshal(langext.H{"raw": base64.URLEncoding.EncodeToString([]byte(mm))})
|
||||
if err != nil {
|
||||
return MailRef{}, exerr.Wrap(err, "").Build()
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(msgbody))
|
||||
if err != nil {
|
||||
return MailRef{}, exerr.Wrap(err, "").Build()
|
||||
}
|
||||
|
||||
req.Header.Add("Authorization", "Bearer "+tok)
|
||||
req.Header.Add("X-Goog-Api-Client", "blackforestbytes-goext/"+goext.GoextVersion)
|
||||
req.Header.Add("User-Agent", "blackforestbytes-goext/"+goext.GoextVersion)
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return MailRef{}, exerr.Wrap(err, "").Build()
|
||||
}
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return MailRef{}, exerr.Wrap(err, "").Build()
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return MailRef{}, exerr.New(exerr.TypeGoogleStatuscode, "gmail returned non-200 statuscode").Int("sc", resp.StatusCode).Str("body", string(respBody)).Build()
|
||||
}
|
||||
|
||||
var respObj MailRef
|
||||
err = json.Unmarshal(respBody, &respObj)
|
||||
if err != nil {
|
||||
return MailRef{}, exerr.Wrap(err, "").Str("body", string(respBody)).Build()
|
||||
}
|
||||
|
||||
return respObj, nil
|
||||
}
|
151
googleapi/sendMail_test.go
Normal file
151
googleapi/sendMail_test.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package googleapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/exerr"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/tst"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if !exerr.Initialized() {
|
||||
exerr.Init(exerr.ErrorPackageConfigInit{ZeroLogErrTraces: langext.PFalse, ZeroLogAllTraces: langext.PFalse})
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestSendMail1(t *testing.T) {
|
||||
t.Skip()
|
||||
return
|
||||
|
||||
auth := NewGoogleOAuth(
|
||||
"554617284247-8di0j6s5dcmlk4lmk4hdf9kdn8scss54.apps.googleusercontent.com",
|
||||
"TODO",
|
||||
"TODO")
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
gclient := NewGoogleClient(auth)
|
||||
|
||||
mail, err := gclient.SendMail(
|
||||
ctx,
|
||||
"noreply@heydyno.de",
|
||||
[]string{"trash@mikescher.de"},
|
||||
nil,
|
||||
nil,
|
||||
"Hello Test Mail",
|
||||
MailBody{Plain: "Plain Text"},
|
||||
nil)
|
||||
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
fmt.Printf("mail.ID := %s\n", mail.ID)
|
||||
fmt.Printf("mail.ThreadID := %s\n", mail.ThreadID)
|
||||
fmt.Printf("mail.LabelIDs := %v\n", mail.LabelIDs)
|
||||
}
|
||||
|
||||
func TestSendMail2(t *testing.T) {
|
||||
t.Skip()
|
||||
return
|
||||
|
||||
auth := NewGoogleOAuth(
|
||||
"554617284247-8di0j6s5dcmlk4lmk4hdf9kdn8scss54.apps.googleusercontent.com",
|
||||
"TODO",
|
||||
"TODO")
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
gclient := NewGoogleClient(auth)
|
||||
|
||||
mail, err := gclient.SendMail(
|
||||
ctx,
|
||||
"noreply@heydyno.de",
|
||||
[]string{"trash@mikescher.de"},
|
||||
nil,
|
||||
nil,
|
||||
"Hello Test Mail (alternative)",
|
||||
MailBody{
|
||||
Plain: "Plain Text",
|
||||
HTML: "<html><body><u>Non</u> Pl<i>ai</i>n T<b>ex</b>t</body></html>",
|
||||
},
|
||||
nil)
|
||||
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
fmt.Printf("mail.ID := %s\n", mail.ID)
|
||||
fmt.Printf("mail.ThreadID := %s\n", mail.ThreadID)
|
||||
fmt.Printf("mail.LabelIDs := %v\n", mail.LabelIDs)
|
||||
}
|
||||
|
||||
func TestSendMail3(t *testing.T) {
|
||||
t.Skip()
|
||||
return
|
||||
|
||||
auth := NewGoogleOAuth(
|
||||
"554617284247-8di0j6s5dcmlk4lmk4hdf9kdn8scss54.apps.googleusercontent.com",
|
||||
"TODO",
|
||||
"TODO")
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
gclient := NewGoogleClient(auth)
|
||||
|
||||
mail, err := gclient.SendMail(
|
||||
ctx,
|
||||
"noreply@heydyno.de",
|
||||
[]string{"trash@mikescher.de"},
|
||||
nil,
|
||||
nil,
|
||||
"Hello Test Mail (attach)",
|
||||
MailBody{
|
||||
HTML: "<html><body><u>Non</u> Pl<i>ai</i>n T<b>ex</b>t</body></html>",
|
||||
},
|
||||
[]MailAttachment{
|
||||
{Data: []byte("HelloWorld"), Filename: "test.txt", IsInline: false, ContentType: "text/plain"},
|
||||
})
|
||||
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
fmt.Printf("mail.ID := %s\n", mail.ID)
|
||||
fmt.Printf("mail.ThreadID := %s\n", mail.ThreadID)
|
||||
fmt.Printf("mail.LabelIDs := %v\n", mail.LabelIDs)
|
||||
}
|
||||
|
||||
func TestSendMail4(t *testing.T) {
|
||||
t.Skip()
|
||||
return
|
||||
|
||||
auth := NewGoogleOAuth(
|
||||
"554617284247-8di0j6s5dcmlk4lmk4hdf9kdn8scss54.apps.googleusercontent.com",
|
||||
"TODO",
|
||||
"TODO")
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
gclient := NewGoogleClient(auth)
|
||||
|
||||
b := tst.Must(os.ReadFile("/home/mike/Pictures/Screenshot_20220706_190205.png"))(t)
|
||||
|
||||
mail, err := gclient.SendMail(
|
||||
ctx,
|
||||
"noreply@heydyno.de",
|
||||
[]string{"trash@mikescher.de"},
|
||||
nil,
|
||||
nil,
|
||||
"Hello Test Mail (inline)",
|
||||
MailBody{
|
||||
HTML: "<html><body><u>Non</u> Pl<i>ai</i>n T<b>ex</b>t</body></html>",
|
||||
},
|
||||
[]MailAttachment{
|
||||
{Data: b, Filename: "img.png", IsInline: true, ContentType: "image/png"},
|
||||
})
|
||||
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
fmt.Printf("mail.ID := %s\n", mail.ID)
|
||||
fmt.Printf("mail.ThreadID := %s\n", mail.ThreadID)
|
||||
fmt.Printf("mail.LabelIDs := %v\n", mail.LabelIDs)
|
||||
}
|
22
googleapi/service.go
Normal file
22
googleapi/service.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package googleapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type GoogleClient interface {
|
||||
SendMail(ctx context.Context, from string, recipients []string, cc []string, bcc []string, subject string, body MailBody, attachments []MailAttachment) (MailRef, error)
|
||||
}
|
||||
|
||||
type client struct {
|
||||
oauth GoogleOAuth
|
||||
http http.Client
|
||||
}
|
||||
|
||||
func NewGoogleClient(oauth GoogleOAuth) GoogleClient {
|
||||
return &client{
|
||||
oauth: oauth,
|
||||
http: http.Client{},
|
||||
}
|
||||
}
|
@@ -29,6 +29,14 @@ func ArrToMap[T comparable, V any](a []V, keyfunc func(V) T) map[T]V {
|
||||
return result
|
||||
}
|
||||
|
||||
func ArrToKVMap[T any, K comparable, V any](a []T, keyfunc func(T) K, valfunc func(T) V) map[K]V {
|
||||
result := make(map[K]V, len(a))
|
||||
for _, v := range a {
|
||||
result[keyfunc(v)] = valfunc(v)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func ArrToSet[T comparable](a []T) map[T]bool {
|
||||
result := make(map[T]bool, len(a))
|
||||
for _, v := range a {
|
||||
|
Reference in New Issue
Block a user