diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..3b45765 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +/internal/odoo/* linguist-generated=true diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..3625be2 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @arnested diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..6243c15 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,28 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: + +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..6d1cb10 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,6 @@ +--- +blank_issues_enabled: true +contact_links: + - name: Ask a question or get support + url: https://github.com/spejder/aarsstjerner/discussions + about: Please use the discussions forum diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..6440a09 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always +frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..f53db48 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,11 @@ +# Security Policy + +## Supported Versions + +Latest version. + +## Reporting a Vulnerability + +Security issues can be reported to [Arne Jørgensen](https://github.com/arnested) +either by [mail](mailto:arne@arnested.dk) or any other channel you prefer and +trust (see my [Keybase profile](https://keybase.io/arnested)). diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..1aff100 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,24 @@ +--- +version: 2 +updates: + - package-ecosystem: gomod + directory: / + schedule: + interval: daily + timezone: Europe/Copenhagen + reviewers: + - arnested + - package-ecosystem: docker + directory: / + schedule: + interval: daily + timezone: Europe/Copenhagen + reviewers: + - arnested + - package-ecosystem: github-actions + directory: / + schedule: + interval: daily + timezone: Europe/Copenhagen + reviewers: + - arnested diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..4b67631 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,5 @@ + diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..3349940 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,50 @@ +--- +name: Lint +on: pull_request + +jobs: + dockerfile: + name: dockerfile + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - name: Run hadolint + uses: hadolint/hadolint-action@v3.1.0 + with: + trusted-registries: docker.io + + yamllint: + name: Yamllint + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - name: Run Yamllint + uses: frenck/action-yamllint@v1.4.2 + with: + strict: true + + markdownlint: + name: markdown + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - name: Run markdownlint + uses: DavidAnson/markdownlint-cli2-action@v13 + + golangci-lint: + name: go + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - uses: arnested/go-version-action@v1 + id: go-version + - name: Setup Go ${{ steps.go-version.outputs.minimal }} + uses: WillAbides/setup-go-faster@v1.12.0 + with: + go-version: ${{ steps.go-version.outputs.minimal }} + - run: go version + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + version: latest + only-new-issues: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..78280c4 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,43 @@ +--- +name: Release + +on: + workflow_run: + workflows: ["Build and test"] + branches: [main] + types: + - completed + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: '0' + - name: Bump version and push tag + uses: anothrNick/github-tag-action@1.67.0 + id: version + env: + GITHUB_TOKEN: ${{ github.token }} + WITH_V: true + DEFAULT_BUMP: patch + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + - uses: arnested/go-version-action@v1 + id: go-version + - name: Set up Go ${{ steps.go-version.outputs.latest }}.x + uses: WillAbides/setup-go-faster@v1.12.0 + with: + go-version: ${{ steps.go-version.outputs.latest }}.x + ignore-local: true + - run: go version + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v5 + with: + version: latest + args: release + env: + GITHUB_TOKEN: ${{ github.token }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..8e67d05 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,44 @@ +--- +name: Build and test +on: + - push + +permissions: + contents: read + +jobs: + go-version: + name: Lookup go versions + runs-on: ubuntu-latest + outputs: + minimal: ${{ steps.go-version.outputs.minimal }} + matrix: ${{ steps.go-version.outputs.matrix }} + steps: + - uses: actions/checkout@v4 + - uses: arnested/go-version-action@v1 + id: go-version + build_and_test: + name: Build and test + needs: go-version + runs-on: ubuntu-latest + strategy: + matrix: + go-version: ${{ fromJSON(needs.go-version.outputs.matrix) }} + steps: + - uses: actions/checkout@v4 + - name: Setup Go ${{ matrix.go-version }}.x + uses: WillAbides/setup-go-faster@v1.12.0 + with: + go-version: ${{ matrix.go-version }}.x + ignore-local: true + - run: go version + - name: go test + env: + # We enable cgo to be able to test with `-race`. + CGO_ENABLED: 1 + run: >- + go test -v -race -cover -covermode=atomic -coverprofile=coverage.txt ./... + - name: Upload coverage report to Codecov + uses: codecov/codecov-action@v3 + with: + flags: go${{ matrix.go-version }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..73154e0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/.envrc +/aarsstjerner +/dist diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..4db2ff4 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,27 @@ +--- +linters: + enable-all: true + disable: + - depguard + - exhaustruct + - exhaustivestruct + +linters-settings: + exhaustive: + # indicates that switch statements are to be considered exhaustive if a + # 'default' case is present, even if all enum members aren't listed in the + # switch + default-signifies-exhaustive: true + errcheck: + check-blank: true + check-type-assertions: true + +run: + skip-dirs: + - internal/ms + +issues: + exclude-rules: + - path: _test\.go + linters: + - ifshort diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..b1e4fe2 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,25 @@ +--- +builds: + - env: + - CGO_ENABLED=0 +archives: + - format: binary + name_template: >- + {{ .ProjectName }}_ + {{- if eq .Os "linux" }}Linux + {{- else }}{{ .Os }}{{ end }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} +checksum: + name_template: 'checksums.txt' +snapshot: + name_template: "{{ .Tag }}-next" +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' +release: + prerelease: auto diff --git a/.markdownlint.yml b/.markdownlint.yml new file mode 100644 index 0000000..8589cec --- /dev/null +++ b/.markdownlint.yml @@ -0,0 +1,3 @@ +--- +MD013: + code_blocks: false diff --git a/.yamllint b/.yamllint new file mode 100644 index 0000000..764b57d --- /dev/null +++ b/.yamllint @@ -0,0 +1,17 @@ +--- +extends: default + +ignore-from-file: + - .gitignore + +rules: + indentation: + spaces: 2 + line-length: disable + truthy: + check-keys: false + braces: + min-spaces-inside: 1 + max-spaces-inside: 1 + min-spaces-inside-empty: 0 + max-spaces-inside-empty: 0 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..f1987c7 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,77 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to make participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies within all project spaces, and it also applies when +an individual is representing the project or its community in public spaces. +Examples of representing a project or community include using an official +project e-mail address, posting via an official social media account, or acting +as an appointed representative at an online or offline event. Representation of +a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at . All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 1.4, available at + + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..1fb2a90 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,40 @@ +# Developer Certificate of Origin + +```text +Version 1.1 + +Copyright (C) 2004, 2006 The Linux Foundation and its contributors. +1 Letterman Drive +Suite D4700 +San Francisco, CA, 94129 + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + + +Developer's Certificate of Origin 1.1 + +By making a contribution to this project, I certify that: + +(a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + +(b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am + permitted to submit under a different license), as indicated + in the file; or + +(c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + +(d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with +   this project or the open source license(s) involved. +``` diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a8d1d8b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,5 @@ +FROM golang:1.21.5 + +# The purpose of this Dockerfile is just to "trick" Dependabot into +# creating a pull request when a new version of Go is released. This +# way we can create a release and build it with the new Go version. diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..80fa359 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,22 @@ +MIT License +=========== + +Copyright (c) 2023 Arne Jørgensen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6a6c320 --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +# Lav liste over hvem der skal have nye årsstjerner + +> [!CAUTION] +> Work in progress. + +Et CLI-tool der kan trække en liste over hvem der skal have nye +årsstjerner. + +For at komme på listen skal man: + +- Være aktivt medlem +- Ikke være leder +- Have været medlem i mindst 1 år og højest 10 år (svarende til de + årstejner der kan uddeles) + +Længden af medlemsskab beregnes ud fra de medlemsskaber (aktive og +tidligere) der er registreret i Medlemsservice. + +Som udgangspunkt er det tilstrækkeligt hvis man "optjener" en +årsstjerne indenfor de kommende 90 dage. Man kan selv antallet af dage +med `--slack` tilvalget. + +Der er [mange måder at tælle årsstjerner +på](https://www.facebook.com/groups/6581285931/posts/10160729337995932/). Udgangspunktet +har været hvad der fungerer for mig. + +![Listen som den åbnes i browseren](docs/aarsstjerner.png) + +## Hjælpeteksten + +```console +$ aarsstjerner help +NAME: + aarsstjerner - Lav liste over årsstjerner + +USAGE: + aarsstjerner [global options] command [command options] [arguments...] + +VERSION: + v0.0.1 + +AUTHOR: + Arne Jørgensen + +COMMANDS: + browser Open årsstjerner in browser (default) + markdown List årsstjerner + license View the license + edit-config Open config file in editor + help, h Shows a list of commands or help for one command + +GLOBAL OPTIONS: + --username value The Medlemsservice username [$MS_USERNAME] + --1pass value A 1Password secret reference for the Medlemsservice password [$MS_1PASS] + --slack value Days of slack in the calculation (default: 90) [$AARSSTJERNER_SLACK] + --ms-url value The Medlemsservice URL (default: "https://medlem.dds.dk") [$MS_URL] + --ms-database value The Medlemsservice database name (default: "dds") [$MS_DATABASE] + --config FILE Read config from FILE (default: "/home/arne/.config/aarsstjerner.yaml") [$AARSSTJERNER_CONFIG_FILE] + --all Include all not just those who needs new (default: false) [$AARSSTJERNER_ALL] + --help, -h show help + --version, -v print the version + +COPYRIGHT: + MIT License, run `aarsstjerner license` to view +``` + +> [!NOTE] +> Er kun testet med [DDS's Medlemsservice](https://medlem.dds.dk). diff --git a/aarsstjerner.go b/aarsstjerner.go new file mode 100644 index 0000000..8801679 --- /dev/null +++ b/aarsstjerner.go @@ -0,0 +1,164 @@ +package main + +import ( + "fmt" + "os" + "slices" + "sort" + "time" + + "bitbucket.org/long174/go-odoo" + "github.com/gomarkdown/markdown" + "github.com/gomarkdown/markdown/html" + "github.com/gomarkdown/markdown/parser" + "github.com/pkg/browser" + "github.com/spejder/aarsstjerner/internal/ms" + "github.com/urfave/cli/v2" +) + +type otherInfo struct { + Aarstjerne int `yaml:"årsstjerne"` +} + +func getHTML(ctx *cli.Context) (string, error) { + listInMarkdown, err := getMarkdown(ctx) + if err != nil { + return "", err + } + + // create markdown parser with extensions + p := parser.New() + doc := p.Parse([]byte(listInMarkdown)) + + // create HTML renderer with extensions + htmlFlags := html.CompletePage | html.Smartypants + opts := html.RendererOptions{Flags: htmlFlags} + renderer := html.NewRenderer(opts) + + output := markdown.Render(doc, renderer) + + return string(output), nil +} + +func runBrowser(ctx *cli.Context) error { + output, err := getHTML(ctx) + if err != nil { + return fmt.Errorf("building html: %w", err) + } + + tmpFile, err := os.CreateTemp("", "aarsstjerner.*.html") + if err != nil { + return fmt.Errorf("could not create temporary file: %w", err) + } + + defer tmpFile.Close() + defer os.Remove(tmpFile.Name()) + + _, err = tmpFile.WriteString(output) + if err != nil { + return fmt.Errorf("writing temporary file: %w", err) + } + + err = tmpFile.Close() + if err != nil { + return fmt.Errorf("closing temporary file: %w", err) + } + + err = browser.OpenFile(tmpFile.Name()) + if err != nil { + return fmt.Errorf("opening browser: %w", err) + } + + time.Sleep(1 * time.Second) + + return nil +} + +func list(ctx *cli.Context) error { + md, err := getMarkdown(ctx) + if err != nil { + return err + } + + fmt.Fprintln(os.Stdout, md) + + return nil +} + +//nolint:funlen,cyclop +func getMarkdown(ctx *cli.Context) (string, error) { + username, password, err := credentials(ctx) + if err != nil { + return "", fmt.Errorf("getting credentials: %w", err) + } + + config := &odoo.ClientConfig{ + Admin: username, + Password: password, + Database: ctx.String("ms-database"), + URL: ctx.String("ms-url"), + } + + oc, err := odoo.NewClient(config) + if err != nil { + return "", fmt.Errorf("creating Odoo client: %w", err) + } + + client := &ms.Client{Client: *oc} + + fmt.Fprintln(os.Stderr, "Henter medlemmer...") + + profiles, err := profiles(client) + if err != nil { + return "", err + } + + fmt.Fprintln(os.Stderr, "Henter medlemskaber...") + + res := make(map[int][]string) + + ids := make([]int64, 0, len(*profiles)) + for _, profile := range *profiles { + ids = append(ids, profile.MembershipIds.Get()...) + } + + memberships, err := memberships(client, ids) + if err != nil { + return "", err + } + + membershipsMap := make(map[int64]ms.MemberMembership, len(*memberships)) + for _, membership := range *memberships { + membershipsMap[membership.Id.Get()] = membership + } + + for _, profile := range *profiles { + years, name := calculate(ctx, profile, membershipsMap) + if years == 0 { + continue + } + + res[years] = append(res[years], name) + } + + keys := make([]int, 0, len(res)) + for k := range res { + keys = append(keys, k) + } + + sort.Ints(keys) + slices.Reverse(keys) + + output := "" + for _, years := range keys { + output += fmt.Sprintf("\n# %d-årsstjerner (%d stk)\n", years, len(res[years])) + + for _, name := range res[years] { + output += fmt.Sprintf("- %s\n", name) + } + } + + output += fmt.Sprintf("\n---\n_%s_\n", time.Now().Format("2006-01-02 15:04:05")) + + return output, nil +} diff --git a/calculate.go b/calculate.go new file mode 100644 index 0000000..acde914 --- /dev/null +++ b/calculate.go @@ -0,0 +1,70 @@ +package main + +import ( + "fmt" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/spejder/aarsstjerner/internal/ms" + "github.com/urfave/cli/v2" + "gopkg.in/yaml.v3" +) + +//nolint:cyclop +func calculate(ctx *cli.Context, profile ms.MemberProfile, membershipsMap map[int64]ms.MemberMembership) (int, string) { + if len(profile.MembershipIds.Get()) == 0 { + return 0, "" + } + + active := false + seconds := 0 + + for _, msid := range profile.MembershipIds.Get() { + memberships := membershipsMap[msid] + + active = active || memberships.ActiveFlag.Get() + start := memberships.StartDate.Get().Unix() + end := memberships.EndDate.Get().Unix() + + if end < 0 { + end = time.Now().Unix() + } + + seconds += int(end - start) + } + + //nolint:gomnd + years := ((seconds / 86400) + ctx.Int("slack")) / 365 + + if !active { + return 0, "" + } + + if years < 1 || years > 10 { + return 0, "" + } + + aarstjerne := "" + info := otherInfo{} + + err := yaml.Unmarshal([]byte(profile.OtherInfo.Get()), &info) + if err == nil && !ctx.Bool("all") && info.Aarstjerne == years { + return 0, "" + } + + if err == nil && info.Aarstjerne > 0 { + aarstjerne = fmt.Sprintf(" (har %d-årsstjerne)", info.Aarstjerne) + } + + name := profile.DisplayName.Get() + if scoutName := profile.ScoutName.Get(); scoutName != "" { + name += fmt.Sprintf(", \"%s\"", scoutName) + } + + if ctx.Bool("fake-names") { + faker := gofakeit.New(profile.Id.Get()) + name = fmt.Sprintf("%s, \"%s\"", faker.Name(), faker.PetName()) + } + + return years, fmt.Sprintf("%s%s", name, aarstjerne) +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..39fd6e7 --- /dev/null +++ b/config.go @@ -0,0 +1,44 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + + "github.com/urfave/cli/v2" + "tailscale.com/atomicfile" +) + +func editConfig(ctx *cli.Context) error { + configFile := ctx.String("config") + + if _, err := os.Stat(configFile); os.IsNotExist(err) { + err := createConfig(configFile) + if err != nil { + return err + } + } + + editCmd := ctx.String("editor") + " '" + configFile + "'" + cmd := exec.Command("sh", "-c", editCmd) + + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to run editor command: %w", err) + } + + return nil +} + +func createConfig(configFile string) error { + //nolint:gomnd + err := atomicfile.WriteFile(configFile, []byte{}, 0o600) + if err != nil { + return fmt.Errorf("failed to write config file: %w", err) + } + + return nil +} diff --git a/credentials.go b/credentials.go new file mode 100644 index 0000000..24d3fcb --- /dev/null +++ b/credentials.go @@ -0,0 +1,51 @@ +package main + +import ( + "bufio" + "fmt" + "log" + "os" + "os/exec" + "strings" + + "github.com/howeyc/gopass" + "github.com/urfave/cli/v2" +) + +func credentials(ctx *cli.Context) (string, string, error) { + reader := bufio.NewReader(os.Stdin) + + username := ctx.String("username") + if username == "" { + fmt.Fprint(os.Stderr, "Username: ") + + usernameInput, err := reader.ReadString('\n') + if err != nil { + return "", "", fmt.Errorf("reading username: %w", err) + } + + username = strings.TrimSpace(usernameInput) + } + + var password string + + if opPath, err := exec.LookPath("op"); err == nil && ctx.String("1pass") != "" { + fmt.Fprintf(os.Stderr, "Henter Medlemsservice-adgangskode for %s fra 1Password...\n", username) + + bytePassword, err := exec.Command(opPath, "read", ctx.String("1pass"), "--no-newline").Output() + if err != nil { + log.Fatal(err) + } + + password = string(bytePassword) + } else { + bytePassword, err := gopass.GetPasswdPrompt("Password: ", true, os.Stdin, os.Stderr) + if err != nil { + log.Fatal(err) + } + + password = string(bytePassword) + } + + return username, password, nil +} diff --git a/docs/aarsstjerner.png b/docs/aarsstjerner.png new file mode 100644 index 0000000..d714d70 Binary files /dev/null and b/docs/aarsstjerner.png differ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..add22c8 --- /dev/null +++ b/go.mod @@ -0,0 +1,28 @@ +module github.com/spejder/aarsstjerner + +go 1.21 + +require ( + bitbucket.org/long174/go-odoo v1.12.1 + github.com/brianvoe/gofakeit/v6 v6.26.3 + github.com/carlmjohnson/versioninfo v0.22.5 + github.com/gomarkdown/markdown v0.0.0-20231115200524-a660076da3fd + github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef + github.com/mattn/go-isatty v0.0.20 + github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 + github.com/urfave/cli/v2 v2.26.0 + gopkg.in/yaml.v3 v3.0.1 + tailscale.com v1.56.1 +) + +require ( + github.com/BurntSushi/toml v1.3.2 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect + github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect + golang.org/x/crypto v0.16.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/term v0.15.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5caa8f4 --- /dev/null +++ b/go.sum @@ -0,0 +1,177 @@ +bitbucket.org/long174/go-odoo v1.12.1 h1:8RY5GYjrSGSdyJXhG7ngp2JpuO53hQx+W2X0dQsKfLI= +bitbucket.org/long174/go-odoo v1.12.1/go.mod h1:KglL89dM63Hb/sdlhFNUc5bvvOcp6MvEd1ydw8icXmQ= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/brianvoe/gofakeit/v6 v6.26.3 h1:3ljYrjPwsUNAUFdUIr2jVg5EhKdcke/ZLop7uVg1Er8= +github.com/brianvoe/gofakeit/v6 v6.26.3/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= +github.com/carlmjohnson/versioninfo v0.22.5 h1:O00sjOLUAFxYQjlN/bzYTuZiS0y6fWDQjMRvwtKgwwc= +github.com/carlmjohnson/versioninfo v0.22.5/go.mod h1:QT9mph3wcVfISUKd0i9sZfVrPviHuSF+cUtLjm2WSf8= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/gomarkdown/markdown v0.0.0-20231115200524-a660076da3fd h1:PppHBegd3uPZ3Y/Iax/2mlCFJm1w4Qf/zP1MdW4ju2o= +github.com/gomarkdown/markdown v0.0.0-20231115200524-a660076da3fd/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef h1:A9HsByNhogrvm9cWb28sjiS3i7tcKCkflWFEkHfuAgM= +github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs= +github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kolo/xmlrpc v0.0.0-20200310150728-e0350524596b/go.mod h1:o03bZfuBwAXHetKXuInt4S7omeXUu62/A845kiycsSQ= +github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00= +github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/urfave/cli/v2 v2.26.0 h1:3f3AMg3HpThFNT4I++TKOejZO8yU55t3JnnSr4S4QEI= +github.com/urfave/cli/v2 v2.26.0/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e h1:+SOyEddqYF09QP7vr7CgJ1eti3pY9Fn3LHO1M1r/0sI= +github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +tailscale.com v1.56.1 h1:V3HBDJai3u7xo22Xlv7ioqKNZQdxOJebLYCNqCXVwZg= +tailscale.com v1.56.1/go.mod h1:XQk6fCN8oMJ+qbCmW+2WS/VM3jTA9nIHT6O19t0hZeQ= diff --git a/internal/ms/client.go b/internal/ms/client.go new file mode 100644 index 0000000..8335110 --- /dev/null +++ b/internal/ms/client.go @@ -0,0 +1,17 @@ +package ms + +import ( + "reflect" + + "bitbucket.org/long174/go-odoo" +) + +// Client provides high and low level functions to interact with Medlemsservice. +type Client struct { + odoo.Client +} + +// UID of the authenticated client. +func (c *Client) UID() int64 { + return reflect.ValueOf(c.Client).FieldByName("uid").Int() +} diff --git a/internal/ms/member_membership.go b/internal/ms/member_membership.go new file mode 100644 index 0000000..fd83ce6 --- /dev/null +++ b/internal/ms/member_membership.go @@ -0,0 +1,117 @@ +package ms + +import ( + "fmt" + + "bitbucket.org/long174/go-odoo" +) + +// MemberMembership represents member.membership model. +type MemberMembership struct { + ActiveFlag *odoo.Bool `xmlrpc:"active_flag,omptempty"` + EndDate *odoo.Time `xmlrpc:"end_date,omptempty"` + Id *odoo.Int `xmlrpc:"id,omptempty"` + StartDate *odoo.Time `xmlrpc:"start_date,omptempty"` +} + +// MemberMemberships represents array of member.membership model. +type MemberMemberships []MemberMembership + +// MemberMembershipModel is the odoo model name. +const MemberMembershipModel = "member.membership" + +// Many2One convert MemberMembership to *Many2One. +func (mm *MemberMembership) Many2One() *odoo.Many2One { + return odoo.NewMany2One(mm.Id.Get(), "") +} + +// CreateMemberMembership creates a new member.membership model and returns its id. +func (c *Client) CreateMemberMembership(mm *MemberMembership) (int64, error) { + return c.Create(MemberMembershipModel, mm) +} + +// UpdateMemberMembership updates an existing member.membership record. +func (c *Client) UpdateMemberMembership(mm *MemberMembership) error { + return c.UpdateMemberMemberships([]int64{mm.Id.Get()}, mm) +} + +// UpdateMemberMemberships updates existing member.membership records. +// All records (represented by ids) will be updated by mp values. +func (c *Client) UpdateMemberMemberships(ids []int64, mm *MemberMembership) error { + return c.Update(MemberMembershipModel, ids, mm) +} + +// DeleteMemberMembership deletes an existing member.membership record. +func (c *Client) DeleteMemberMembership(id int64) error { + return c.DeleteMemberMemberships([]int64{id}) +} + +// DeleteMemberMemberships deletes existing member.membership records. +func (c *Client) DeleteMemberMemberships(ids []int64) error { + return c.Delete(MemberMembershipModel, ids) +} + +// GetMemberMembership gets member.membership existing record. +func (c *Client) GetMemberMembership(id int64) (*MemberMembership, error) { + mms, err := c.GetMemberMemberships([]int64{id}) + if err != nil { + return nil, err + } + if mms != nil && len(*mms) > 0 { + return &((*mms)[0]), nil + } + return nil, fmt.Errorf("id %v of member.membership not found", id) +} + +// GetMemberMemberships gets member.membership existing records. +func (c *Client) GetMemberMemberships(ids []int64) (*MemberMemberships, error) { + mms := &MemberMemberships{} + if err := c.Read(MemberMembershipModel, ids, nil, mms); err != nil { + return nil, err + } + return mms, nil +} + +// FindMemberMembership finds member.membership record by querying it with criteria. +func (c *Client) FindMemberMembership(criteria *odoo.Criteria) (*MemberMembership, error) { + mms := &MemberMemberships{} + if err := c.SearchRead(MemberMembershipModel, criteria, odoo.NewOptions().Limit(1), mms); err != nil { + return nil, err + } + if mms != nil && len(*mms) > 0 { + return &((*mms)[0]), nil + } + return nil, fmt.Errorf("member.membership was not found") +} + +// FindMemberMemberships finds member.membership records by querying it +// and filtering it with criteria and options. +func (c *Client) FindMemberMemberships(criteria *odoo.Criteria, options *odoo.Options) (*MemberMemberships, error) { + mms := &MemberMemberships{} + if err := c.SearchRead(MemberMembershipModel, criteria, options, mms); err != nil { + return nil, err + } + return mms, nil +} + +// FindMemberMembershipIds finds records ids by querying it +// and filtering it with criteria and options. +func (c *Client) FindMemberMembershipIds(criteria *odoo.Criteria, options *odoo.Options) ([]int64, error) { + ids, err := c.Search(MemberMembershipModel, criteria, options) + if err != nil { + return []int64{}, err + } + return ids, nil +} + +// FindMemberMembershipId finds record id by querying it with criteria. +func (c *Client) FindMemberMembershipId(criteria *odoo.Criteria, options *odoo.Options) (int64, error) { + ids, err := c.Search(MemberMembershipModel, criteria, options) + if err != nil { + return -1, err + } + if len(ids) > 0 { + return ids[0], nil + } + return -1, fmt.Errorf("member.membership was not found") +} diff --git a/internal/ms/member_profile.go b/internal/ms/member_profile.go new file mode 100644 index 0000000..82c078d --- /dev/null +++ b/internal/ms/member_profile.go @@ -0,0 +1,363 @@ +package ms + +import ( + "fmt" + + "bitbucket.org/long174/go-odoo" +) + +// MemberProfile represents member.profile model. +type MemberProfile struct { + AcceptSpecials *odoo.Selection `xmlrpc:"accept_specials,omptempty"` + AccNumber *odoo.String `xmlrpc:"acc_number,omptempty"` + AccountInvoiceIds *odoo.Relation `xmlrpc:"account_invoice_ids,omptempty"` + AccountInvoiceLineIds *odoo.Relation `xmlrpc:"account_invoice_line_ids,omptempty"` + AccountInvoiceNotDraftIds *odoo.Relation `xmlrpc:"account_invoice_not_draft_ids,omptempty"` + AccountInvoiceOpenIds *odoo.Relation `xmlrpc:"account_invoice_open_ids,omptempty"` + AccountTotalResiduals *odoo.Float `xmlrpc:"account_total_residuals,omptempty"` + Active *odoo.Bool `xmlrpc:"active,omptempty"` + ActiveFunctionIds *odoo.Relation `xmlrpc:"active_function_ids,omptempty"` + ActiveFunctionsInCurrentOrganization *odoo.Relation `xmlrpc:"active_functions_in_current_organization,omptempty"` + ActiveFunctionsInProfile *odoo.Relation `xmlrpc:"active_functions_in_profile,omptempty"` + ActiveMembershipIds *odoo.Relation `xmlrpc:"active_membership_ids,omptempty"` + ActiveMembershipsInCurrentOrganization *odoo.Relation `xmlrpc:"active_memberships_in_current_organization,omptempty"` + ActiveMembershipsInProfile *odoo.Relation `xmlrpc:"active_memberships_in_profile,omptempty"` + ActiveProfileForUserIds *odoo.Relation `xmlrpc:"active_profile_for_user_ids,omptempty"` + ActiveProfileIds *odoo.Relation `xmlrpc:"active_profile_ids,omptempty"` + AddressCo *odoo.String `xmlrpc:"address_co,omptempty"` + Age *odoo.Int `xmlrpc:"age,omptempty"` + AllFunctionsInCurrentOrganization *odoo.Relation `xmlrpc:"all_functions_in_current_organization,omptempty"` + AllFunctionsInProfile *odoo.Relation `xmlrpc:"all_functions_in_profile,omptempty"` + AllMembershipsInCurrentOrganization *odoo.Relation `xmlrpc:"all_memberships_in_current_organization,omptempty"` + AllMembershipsInProfile *odoo.Relation `xmlrpc:"all_memberships_in_profile,omptempty"` + Anonymized *odoo.Bool `xmlrpc:"anonymized,omptempty"` + AnonymizeWarning *odoo.Bool `xmlrpc:"anonymize_warning,omptempty"` + BankIds *odoo.Relation `xmlrpc:"bank_ids,omptempty"` + BelongsToCompany *odoo.Bool `xmlrpc:"belongs_to_company,omptempty"` + Birthdate *odoo.Time `xmlrpc:"birthdate,omptempty"` + BirthdateShort *odoo.String `xmlrpc:"birthdate_short,omptempty"` + Bmhash *odoo.String `xmlrpc:"bmhash,omptempty"` + BmId *odoo.String `xmlrpc:"bm_id,omptempty"` + BmPin *odoo.String `xmlrpc:"bm_pin,omptempty"` + BmPwdhash *odoo.String `xmlrpc:"bm_pwdhash,omptempty"` + Bmref *odoo.String `xmlrpc:"bmref,omptempty"` + BmSsoId *odoo.Int `xmlrpc:"bm_sso_id,omptempty"` + CalendarLastNotifAck *odoo.Time `xmlrpc:"calendar_last_notif_ack,omptempty"` + CanAccessContactInfo *odoo.Bool `xmlrpc:"can_access_contact_info,omptempty"` + CanApprove *odoo.Bool `xmlrpc:"can_approve,omptempty"` + CanDelete *odoo.Bool `xmlrpc:"can_delete,omptempty"` + CanEdit *odoo.Bool `xmlrpc:"can_edit,omptempty"` + CanExpense *odoo.Bool `xmlrpc:"can_expense,omptempty"` + CanFullAll *odoo.Bool `xmlrpc:"can_full_all,omptempty"` + CanRead *odoo.Bool `xmlrpc:"can_read,omptempty"` + CategoryId *odoo.Relation `xmlrpc:"category_id,omptempty"` + ChildIds *odoo.Relation `xmlrpc:"child_ids,omptempty"` + City *odoo.String `xmlrpc:"city,omptempty"` + CkrCheckIds *odoo.Relation `xmlrpc:"ckr_check_ids,omptempty"` + CkrPane *odoo.Bool `xmlrpc:"ckr_pane,omptempty"` + Color *odoo.Int `xmlrpc:"color,omptempty"` + Comment *odoo.String `xmlrpc:"comment,omptempty"` + CommercialPartnerId *odoo.Many2One `xmlrpc:"commercial_partner_id,omptempty"` + CompanyId *odoo.Many2One `xmlrpc:"company_id,omptempty"` + CompleteAddress *odoo.String `xmlrpc:"complete_address,omptempty"` + ContactAddress *odoo.String `xmlrpc:"contact_address,omptempty"` + ContextAge *odoo.Int `xmlrpc:"context_age,omptempty"` + ContractIds *odoo.Relation `xmlrpc:"contract_ids,omptempty"` + ContractsCount *odoo.Int `xmlrpc:"contracts_count,omptempty"` + CountryId *odoo.Many2One `xmlrpc:"country_id,omptempty"` + CreateDate *odoo.Time `xmlrpc:"create_date,omptempty"` + CreateUid *odoo.Many2One `xmlrpc:"create_uid,omptempty"` + Credit *odoo.Float `xmlrpc:"credit,omptempty"` + CreditLimit *odoo.Float `xmlrpc:"credit_limit,omptempty"` + CurEventId *odoo.Many2One `xmlrpc:"cur_event_id,omptempty"` + Customer *odoo.Bool `xmlrpc:"customer,omptempty"` + Date *odoo.Time `xmlrpc:"date,omptempty"` + DateLocalization *odoo.Time `xmlrpc:"date_localization,omptempty"` + Debit *odoo.Float `xmlrpc:"debit,omptempty"` + DebitLimit *odoo.Float `xmlrpc:"debit_limit,omptempty"` + Diseases *odoo.String `xmlrpc:"diseases,omptempty"` + DisplayName *odoo.String `xmlrpc:"display_name,omptempty"` + Ean13 *odoo.String `xmlrpc:"ean13,omptempty"` + EditBirthdate *odoo.Bool `xmlrpc:"edit_birthdate,omptempty"` + EditMemberNumber *odoo.Bool `xmlrpc:"edit_member_number,omptempty"` + EditName *odoo.Bool `xmlrpc:"edit_name,omptempty"` + EditOrganizationId *odoo.Bool `xmlrpc:"edit_organization_id,omptempty"` + EditRestrictedFields *odoo.Bool `xmlrpc:"edit_restricted_fields,omptempty"` + Email *odoo.String `xmlrpc:"email,omptempty"` + Employee *odoo.Bool `xmlrpc:"employee,omptempty"` + EventRegistrationIds *odoo.Relation `xmlrpc:"event_registration_ids,omptempty"` + Externalid *odoo.String `xmlrpc:"externalid,omptempty"` + Fax *odoo.String `xmlrpc:"fax,omptempty"` + Firstname *odoo.String `xmlrpc:"firstname,omptempty"` + Function *odoo.String `xmlrpc:"function,omptempty"` + FunctionIds *odoo.Relation `xmlrpc:"function_ids,omptempty"` + FunctionsText *odoo.String `xmlrpc:"functions_text,omptempty"` + Gender *odoo.Selection `xmlrpc:"gender,omptempty"` + Handicap *odoo.String `xmlrpc:"handicap,omptempty"` + HasImage *odoo.Bool `xmlrpc:"has_image,omptempty"` + HelpInfo *odoo.String `xmlrpc:"help_info,omptempty"` + Id *odoo.Int `xmlrpc:"id,omptempty"` + Image *odoo.String `xmlrpc:"image,omptempty"` + ImageMedium *odoo.String `xmlrpc:"image_medium,omptempty"` + ImageSmall *odoo.String `xmlrpc:"image_small,omptempty"` + ImportStatus *odoo.String `xmlrpc:"import_status,omptempty"` + InvoiceIds *odoo.Relation `xmlrpc:"invoice_ids,omptempty"` + IsActiveLeader *odoo.Bool `xmlrpc:"is_active_leader,omptempty"` + IsCompany *odoo.Bool `xmlrpc:"is_company,omptempty"` + JournalItemCount *odoo.Int `xmlrpc:"journal_item_count,omptempty"` + Lang *odoo.Selection `xmlrpc:"lang,omptempty"` + LastActiveDate *odoo.Time `xmlrpc:"last_active_date,omptempty"` + LastContactConfirm *odoo.Time `xmlrpc:"last_contact_confirm,omptempty"` + LastImport *odoo.Time `xmlrpc:"last_import,omptempty"` + LastKnownCompleteAddress *odoo.String `xmlrpc:"last_known_complete_address,omptempty"` + LastKnownContactInfo *odoo.String `xmlrpc:"last_known_contact_info,omptempty"` + LastKnownEmail *odoo.String `xmlrpc:"last_known_email,omptempty"` + LastKnownPhoneCombo *odoo.String `xmlrpc:"last_known_phone_combo,omptempty"` + Lastname *odoo.String `xmlrpc:"lastname,omptempty"` + LastReconciliationDate *odoo.Time `xmlrpc:"last_reconciliation_date,omptempty"` + LastUpdate *odoo.Time `xmlrpc:"__last_update,omptempty"` + LeaderFunctionIds *odoo.Relation `xmlrpc:"leader_function_ids,omptempty"` + LocalOrgApproverIds *odoo.Relation `xmlrpc:"local_org_approver_ids,omptempty"` + LockingCkrCheckIds *odoo.Relation `xmlrpc:"locking_ckr_check_ids,omptempty"` + LocksNameIds *odoo.Relation `xmlrpc:"locks_name_ids,omptempty"` + MeetingCount *odoo.Int `xmlrpc:"meeting_count,omptempty"` + MeetingIds *odoo.Relation `xmlrpc:"meeting_ids,omptempty"` + MemberAnonymized *odoo.Bool `xmlrpc:"member_anonymized,omptempty"` + MemberId *odoo.Many2One `xmlrpc:"member_id,omptempty"` + MemberMagazineOptionId *odoo.Many2One `xmlrpc:"member_magazine_option_id,omptempty"` + MemberNumber *odoo.String `xmlrpc:"member_number,omptempty"` + MembershipIds *odoo.Relation `xmlrpc:"membership_ids,omptempty"` + MessageFollowerIds *odoo.Relation `xmlrpc:"message_follower_ids,omptempty"` + MessageIds *odoo.Relation `xmlrpc:"message_ids,omptempty"` + MessageIsFollower *odoo.Bool `xmlrpc:"message_is_follower,omptempty"` + MessageLastPost *odoo.Time `xmlrpc:"message_last_post,omptempty"` + MessageSummary *odoo.String `xmlrpc:"message_summary,omptempty"` + MessageUnread *odoo.Bool `xmlrpc:"message_unread,omptempty"` + Mobile *odoo.String `xmlrpc:"mobile,omptempty"` + MobileClean *odoo.String `xmlrpc:"mobile_clean,omptempty"` + MunicipalityId *odoo.Many2One `xmlrpc:"municipality_id,omptempty"` + Name *odoo.String `xmlrpc:"name,omptempty"` + NotifyEmail *odoo.Selection `xmlrpc:"notify_email,omptempty"` + OpportunityCount *odoo.Int `xmlrpc:"opportunity_count,omptempty"` + OpportunityIds *odoo.Relation `xmlrpc:"opportunity_ids,omptempty"` + OptOut *odoo.Bool `xmlrpc:"opt_out,omptempty"` + OrganizationId *odoo.Many2One `xmlrpc:"organization_id,omptempty"` + OrganizationStructureParentId *odoo.Many2One `xmlrpc:"organization_structure_parent_id,omptempty"` + OrganizationTypeId *odoo.Many2One `xmlrpc:"organization_type_id,omptempty"` + OtherInfo *odoo.String `xmlrpc:"other_info,omptempty"` + OwnEventRegistrationIds *odoo.Relation `xmlrpc:"own_event_registration_ids,omptempty"` + ParentId *odoo.Many2One `xmlrpc:"parent_id,omptempty"` + ParentName *odoo.String `xmlrpc:"parent_name,omptempty"` + ParishId *odoo.Many2One `xmlrpc:"parish_id,omptempty"` + PartnerId *odoo.Many2One `xmlrpc:"partner_id,omptempty"` + PartnerLatitude *odoo.Float `xmlrpc:"partner_latitude,omptempty"` + PartnerLongitude *odoo.Float `xmlrpc:"partner_longitude,omptempty"` + PartnerPayerId *odoo.Many2One `xmlrpc:"partner_payer_id,omptempty"` + PayerForProfileIds *odoo.Relation `xmlrpc:"payer_for_profile_ids,omptempty"` + PayerForProfileThisOrganizationIds *odoo.Relation `xmlrpc:"payer_for_profile_this_organization_ids,omptempty"` + Pbmhash *odoo.String `xmlrpc:"pbmhash,omptempty"` + Pbmref *odoo.String `xmlrpc:"pbmref,omptempty"` + PermissionPhoto *odoo.Selection `xmlrpc:"permission_photo,omptempty"` + Phone *odoo.String `xmlrpc:"phone,omptempty"` + PhonecallCount *odoo.Int `xmlrpc:"phonecall_count,omptempty"` + PhonecallIds *odoo.Relation `xmlrpc:"phonecall_ids,omptempty"` + PhoneCombo *odoo.String `xmlrpc:"phone_combo,omptempty"` + PlastImport *odoo.Time `xmlrpc:"plast_import,omptempty"` + PreliminaryOrganizationId *odoo.Many2One `xmlrpc:"preliminary_organization_id,omptempty"` + PrimaryMembershipOrganizationId *odoo.Many2One `xmlrpc:"primary_membership_organization_id,omptempty"` + ProfileAnonymized *odoo.Bool `xmlrpc:"profile_anonymized,omptempty"` + ProfileIds *odoo.Relation `xmlrpc:"profile_ids,omptempty"` + PromotionIds *odoo.Relation `xmlrpc:"promotion_ids,omptempty"` + PropertyAccountPayable *odoo.Many2One `xmlrpc:"property_account_payable,omptempty"` + PropertyAccountPosition *odoo.Many2One `xmlrpc:"property_account_position,omptempty"` + PropertyAccountReceivable *odoo.Many2One `xmlrpc:"property_account_receivable,omptempty"` + PropertyPaymentTerm *odoo.Many2One `xmlrpc:"property_payment_term,omptempty"` + PropertyProductPricelist *odoo.Many2One `xmlrpc:"property_product_pricelist,omptempty"` + PropertyStockCustomer *odoo.Many2One `xmlrpc:"property_stock_customer,omptempty"` + PropertyStockSupplier *odoo.Many2One `xmlrpc:"property_stock_supplier,omptempty"` + PropertySupplierPaymentTerm *odoo.Many2One `xmlrpc:"property_supplier_payment_term,omptempty"` + Ref *odoo.String `xmlrpc:"ref,omptempty"` + RefCompanies *odoo.Relation `xmlrpc:"ref_companies,omptempty"` + Registered *odoo.Bool `xmlrpc:"registered,omptempty"` + RegNumber *odoo.String `xmlrpc:"reg_number,omptempty"` + RelationAllIds *odoo.Relation `xmlrpc:"relation_all_ids,omptempty"` + RelationAllMemberIds *odoo.Relation `xmlrpc:"relation_all_member_ids,omptempty"` + RelationCount *odoo.Int `xmlrpc:"relation_count,omptempty"` + RelationIds *odoo.Relation `xmlrpc:"relation_ids,omptempty"` + RelationPartnerList *odoo.String `xmlrpc:"relation_partner_list,omptempty"` + RelationPrimaryMemberIds *odoo.Relation `xmlrpc:"relation_primary_member_ids,omptempty"` + RelativeForProfileId *odoo.Many2One `xmlrpc:"relative_for_profile_id,omptempty"` + RelativeMemberId *odoo.Many2One `xmlrpc:"relative_member_id,omptempty"` + RelativePartnerId *odoo.Many2One `xmlrpc:"relative_partner_id,omptempty"` + RelativeTypeId *odoo.Many2One `xmlrpc:"relative_type_id,omptempty"` + SaleOrderCount *odoo.Int `xmlrpc:"sale_order_count,omptempty"` + SaleOrderIds *odoo.Relation `xmlrpc:"sale_order_ids,omptempty"` + School *odoo.String `xmlrpc:"school,omptempty"` + SchoolClassLetter *odoo.String `xmlrpc:"school_class_letter,omptempty"` + SchoolClassNumber *odoo.String `xmlrpc:"school_class_number,omptempty"` + SchoolStartYear *odoo.Int `xmlrpc:"school_start_year,omptempty"` + ScoutName *odoo.String `xmlrpc:"scout_name,omptempty"` + SearchRelationDate *odoo.Time `xmlrpc:"search_relation_date,omptempty"` + SearchRelationId *odoo.Many2One `xmlrpc:"search_relation_id,omptempty"` + SearchRelationPartnerCategoryId *odoo.Many2One `xmlrpc:"search_relation_partner_category_id,omptempty"` + SearchRelationPartnerId *odoo.Many2One `xmlrpc:"search_relation_partner_id,omptempty"` + SectionId *odoo.Many2One `xmlrpc:"section_id,omptempty"` + Self *odoo.Many2One `xmlrpc:"self,omptempty"` + SelfRelationPartnerList *odoo.String `xmlrpc:"self_relation_partner_list,omptempty"` + SignupExpiration *odoo.Time `xmlrpc:"signup_expiration,omptempty"` + SignupToken *odoo.String `xmlrpc:"signup_token,omptempty"` + SignupType *odoo.String `xmlrpc:"signup_type,omptempty"` + SignupUrl *odoo.String `xmlrpc:"signup_url,omptempty"` + SignupValid *odoo.Bool `xmlrpc:"signup_valid,omptempty"` + Speaker *odoo.Bool `xmlrpc:"speaker,omptempty"` + SpecialConsiderations *odoo.String `xmlrpc:"special_considerations,omptempty"` + State *odoo.Selection `xmlrpc:"state,omptempty"` + StateId *odoo.Many2One `xmlrpc:"state_id,omptempty"` + StoredPartnerId *odoo.Many2One `xmlrpc:"stored_partner_id,omptempty"` + Street *odoo.String `xmlrpc:"street,omptempty"` + Street2 *odoo.String `xmlrpc:"street2,omptempty"` + StreetFloor *odoo.String `xmlrpc:"street_floor,omptempty"` + StreetLetter *odoo.String `xmlrpc:"street_letter,omptempty"` + StreetName *odoo.String `xmlrpc:"street_name,omptempty"` + StreetNumber *odoo.String `xmlrpc:"street_number,omptempty"` + StreetPlacement *odoo.String `xmlrpc:"street_placement,omptempty"` + SubscriptionCard *odoo.String `xmlrpc:"subscription_card,omptempty"` + SubscriptionFeeDateEnd *odoo.Time `xmlrpc:"subscription_fee_date_end,omptempty"` + SubscriptionFeeDateStart *odoo.Time `xmlrpc:"subscription_fee_date_start,omptempty"` + SubscriptionFeeHasDraftLines *odoo.Bool `xmlrpc:"subscription_fee_has_draft_lines,omptempty"` + SubscriptionFeeReceivable *odoo.Float `xmlrpc:"subscription_fee_receivable,omptempty"` + SubscriptionLastChargedEndDate *odoo.Time `xmlrpc:"subscription_last_charged_end_date,omptempty"` + SubscriptionLastWarningDate *odoo.Time `xmlrpc:"subscription_last_warning_date,omptempty"` + SubscriptionProductId *odoo.Many2One `xmlrpc:"subscription_product_id,omptempty"` + SubscriptionTransaction *odoo.Many2One `xmlrpc:"subscription_transaction,omptempty"` + Supplier *odoo.Bool `xmlrpc:"supplier,omptempty"` + TaskCount *odoo.Int `xmlrpc:"task_count,omptempty"` + TaskIds *odoo.Relation `xmlrpc:"task_ids,omptempty"` + Title *odoo.Many2One `xmlrpc:"title,omptempty"` + TotalInvoiced *odoo.Float `xmlrpc:"total_invoiced,omptempty"` + Type *odoo.Selection `xmlrpc:"type,omptempty"` + Tz *odoo.Selection `xmlrpc:"tz,omptempty"` + TzOffset *odoo.String `xmlrpc:"tz_offset,omptempty"` + UseParentAddress *odoo.Bool `xmlrpc:"use_parent_address,omptempty"` + UserFullIds *odoo.Relation `xmlrpc:"user_full_ids,omptempty"` + UserId *odoo.Many2One `xmlrpc:"user_id,omptempty"` + UserIds *odoo.Relation `xmlrpc:"user_ids,omptempty"` + UserReadIds *odoo.Relation `xmlrpc:"user_read_ids,omptempty"` + UserReadLimitedIds *odoo.Relation `xmlrpc:"user_read_limited_ids,omptempty"` + Vat *odoo.String `xmlrpc:"vat,omptempty"` + VatSubjected *odoo.Bool `xmlrpc:"vat_subjected,omptempty"` + WaitinglistDate *odoo.Time `xmlrpc:"waitinglist_date,omptempty"` + Website *odoo.String `xmlrpc:"website,omptempty"` + WebsiteDescription *odoo.String `xmlrpc:"website_description,omptempty"` + WebsiteMessageIds *odoo.Relation `xmlrpc:"website_message_ids,omptempty"` + WebsiteMetaDescription *odoo.String `xmlrpc:"website_meta_description,omptempty"` + WebsiteMetaKeywords *odoo.String `xmlrpc:"website_meta_keywords,omptempty"` + WebsiteMetaTitle *odoo.String `xmlrpc:"website_meta_title,omptempty"` + WebsitePublished *odoo.Bool `xmlrpc:"website_published,omptempty"` + WebsiteShortDescription *odoo.String `xmlrpc:"website_short_description,omptempty"` + WriteDate *odoo.Time `xmlrpc:"write_date,omptempty"` + WriteUid *odoo.Many2One `xmlrpc:"write_uid,omptempty"` + Zip *odoo.String `xmlrpc:"zip,omptempty"` +} + +// MemberProfiles represents array of member.profile model. +type MemberProfiles []MemberProfile + +// MemberProfileModel is the odoo model name. +const MemberProfileModel = "member.profile" + +// Many2One convert MemberProfile to *Many2One. +func (mp *MemberProfile) Many2One() *odoo.Many2One { + return odoo.NewMany2One(mp.Id.Get(), "") +} + +// CreateMemberProfile creates a new member.profile model and returns its id. +func (c *Client) CreateMemberProfile(mp *MemberProfile) (int64, error) { + return c.Create(MemberProfileModel, mp) +} + +// UpdateMemberProfile updates an existing member.profile record. +func (c *Client) UpdateMemberProfile(mp *MemberProfile) error { + return c.UpdateMemberProfiles([]int64{mp.Id.Get()}, mp) +} + +// UpdateMemberProfiles updates existing member.profile records. +// All records (represented by ids) will be updated by mp values. +func (c *Client) UpdateMemberProfiles(ids []int64, mp *MemberProfile) error { + return c.Update(MemberProfileModel, ids, mp) +} + +// DeleteMemberProfile deletes an existing member.profile record. +func (c *Client) DeleteMemberProfile(id int64) error { + return c.DeleteMemberProfiles([]int64{id}) +} + +// DeleteMemberProfiles deletes existing member.profile records. +func (c *Client) DeleteMemberProfiles(ids []int64) error { + return c.Delete(MemberProfileModel, ids) +} + +// GetMemberProfile gets member.profile existing record. +func (c *Client) GetMemberProfile(id int64) (*MemberProfile, error) { + mps, err := c.GetMemberProfiles([]int64{id}) + if err != nil { + return nil, err + } + if mps != nil && len(*mps) > 0 { + return &((*mps)[0]), nil + } + return nil, fmt.Errorf("id %v of member.profile not found", id) +} + +// GetMemberProfiles gets member.profile existing records. +func (c *Client) GetMemberProfiles(ids []int64) (*MemberProfiles, error) { + mps := &MemberProfiles{} + if err := c.Read(MemberProfileModel, ids, nil, mps); err != nil { + return nil, err + } + return mps, nil +} + +// FindMemberProfile finds member.profile record by querying it with criteria. +func (c *Client) FindMemberProfile(criteria *odoo.Criteria) (*MemberProfile, error) { + mps := &MemberProfiles{} + if err := c.SearchRead(MemberProfileModel, criteria, odoo.NewOptions().Limit(1), mps); err != nil { + return nil, err + } + if mps != nil && len(*mps) > 0 { + return &((*mps)[0]), nil + } + return nil, fmt.Errorf("member.profile was not found") +} + +// FindMemberProfiles finds member.profile records by querying it +// and filtering it with criteria and options. +func (c *Client) FindMemberProfiles(criteria *odoo.Criteria, options *odoo.Options) (*MemberProfiles, error) { + mps := &MemberProfiles{} + if err := c.SearchRead(MemberProfileModel, criteria, options, mps); err != nil { + return nil, err + } + return mps, nil +} + +// FindMemberProfileIds finds records ids by querying it +// and filtering it with criteria and options. +func (c *Client) FindMemberProfileIds(criteria *odoo.Criteria, options *odoo.Options) ([]int64, error) { + ids, err := c.Search(MemberProfileModel, criteria, options) + if err != nil { + return []int64{}, err + } + return ids, nil +} + +// FindMemberProfileId finds record id by querying it with criteria. +func (c *Client) FindMemberProfileId(criteria *odoo.Criteria, options *odoo.Options) (int64, error) { + ids, err := c.Search(MemberProfileModel, criteria, options) + if err != nil { + return -1, err + } + if len(ids) > 0 { + return ids[0], nil + } + return -1, fmt.Errorf("member.profile was not found") +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..4f80a3b --- /dev/null +++ b/main.go @@ -0,0 +1,154 @@ +package main + +import ( + _ "embed" + "fmt" + "log" + "os" + + "github.com/mattn/go-isatty" + "github.com/urfave/cli/v2" + "github.com/urfave/cli/v2/altsrc" +) + +var ( + //go:embed LICENSE.md + license string + // Version is the version string to be set at compile time via command line. + version string +) + +//nolint:funlen +func main() { + if isatty.IsTerminal(os.Stdout.Fd()) { + log.SetFlags(0) + } + + app := cli.NewApp() + app.Name = "aarsstjerner" + app.Usage = "Lav liste over årsstjerner" + app.EnableBashCompletion = true + app.Authors = []*cli.Author{ + { + Name: "Arne Jørgensen", + Email: "arne@arnested.dk", + }, + } + app.Version = getVersion() + app.Copyright = fmt.Sprintf("MIT License, run `%s license` to view", app.Name) + + configPath := "" + userConfigDir, err := os.UserConfigDir() + + if err == nil { + configPath = userConfigDir + "/aarsstjerner.yaml" + } + + app.Flags = []cli.Flag{ + altsrc.NewStringFlag(&cli.StringFlag{ + Name: "username", + Value: "", + Usage: "The Medlemsservice username", + EnvVars: []string{"MS_USERNAME"}, + }), + altsrc.NewStringFlag(&cli.StringFlag{ + Name: "1pass", + Value: "", + Usage: "A 1Password secret reference for the Medlemsservice password", + EnvVars: []string{"MS_1PASS"}, + }), + altsrc.NewIntFlag(&cli.IntFlag{ + Name: "slack", + Value: 90, //nolint:gomnd + Usage: "Days of slack in the calculation", + EnvVars: []string{"AARSSTJERNER_SLACK"}, + }), + altsrc.NewStringFlag(&cli.StringFlag{ + Name: "ms-url", + Value: "https://medlem.dds.dk", + Usage: "The Medlemsservice URL", + EnvVars: []string{"MS_URL"}, + }), + altsrc.NewStringFlag(&cli.StringFlag{ + Name: "ms-database", + Value: "dds", + Usage: "The Medlemsservice database name", + EnvVars: []string{"MS_DATABASE"}, + }), + &cli.StringFlag{ + Name: "config", + Value: configPath, + Usage: "Read config from `FILE`", + EnvVars: []string{"AARSSTJERNER_CONFIG_FILE"}, + TakesFile: true, + }, + &cli.BoolFlag{ + Name: "all", + Value: false, + Usage: "Include all not just those who needs new", + EnvVars: []string{"AARSSTJERNER_ALL"}, + }, + &cli.BoolFlag{ + Name: "fake-names", + Value: false, + Usage: "Display fake names (for demo purposes)", + EnvVars: []string{"AARSSTJERNER_FAKE_NAMES"}, + Hidden: true, + }, + } + + app.Commands = []*cli.Command{ + { + Name: "browser", + Usage: "Open årsstjerner in browser (default)", + Action: runBrowser, + }, + { + Name: "markdown", + Usage: "List årsstjerner", + Action: list, + }, + { + Name: "license", + Usage: "View the license", + Action: func(c *cli.Context) error { + fmt.Fprintln(os.Stdout, license) + + return nil + }, + }, + { + Name: "edit-config", + Usage: "Open config file in editor", + Action: editConfig, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "editor", + Value: "vi", + Usage: "Use `EDITOR` to edit config file (create it of it doesn't exist)", + EnvVars: []string{"EDITOR"}, + }, + }, + }, + } + + app.DefaultCommand = "browser" + + app.Before = func(ctx *cli.Context) error { + if _, err := os.Stat(configPath); !os.IsNotExist(err) { + initConfig := altsrc.InitInputSourceWithContext(app.Flags, altsrc.NewYamlSourceFromFlagFunc("config")) + + err = initConfig(ctx) + if err != nil { + return fmt.Errorf("reading config file: %w", err) + } + } + + return nil + } + + err = app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} diff --git a/profiles.go b/profiles.go new file mode 100644 index 0000000..2d14276 --- /dev/null +++ b/profiles.go @@ -0,0 +1,49 @@ +package main + +import ( + "fmt" + + "bitbucket.org/long174/go-odoo" + "github.com/spejder/aarsstjerner/internal/ms" +) + +func profiles(client *ms.Client) (*ms.MemberProfiles, error) { + criteria := odoo.NewCriteria().Add("can_access_contact_info", "=", true) + + criteria. + Add("state", "=", "active"). + Add("is_active_leader", "=", false) + + options := odoo.NewOptions().FetchFields( + "id", + "display_name", + "scout_name", + "membership_ids", + "other_info", + ) + + profiles, err := client.FindMemberProfiles(criteria, options) + if err != nil { + return &ms.MemberProfiles{}, fmt.Errorf("finding member profiles: %w", err) + } + + return profiles, nil +} + +func memberships(client *ms.Client, ids []int64) (*ms.MemberMemberships, error) { + criteria := odoo.NewCriteria().Add("id", "=", ids) + + options := odoo.NewOptions().FetchFields( + "id", + "active_flag", + "start_date", + "end_date", + ) + + memberships, err := client.FindMemberMemberships(criteria, options) + if err != nil { + return &ms.MemberMemberships{}, fmt.Errorf("finding member profiles: %w", err) + } + + return memberships, nil +} diff --git a/version.go b/version.go new file mode 100644 index 0000000..d4170aa --- /dev/null +++ b/version.go @@ -0,0 +1,25 @@ +package main + +import ( + "runtime/debug" + + "github.com/carlmjohnson/versioninfo" +) + +func getVersion() string { + if version == "" { + version = versioninfo.Revision + + if versioninfo.DirtyBuild { + version += "-dirty" + } + } + + buildinfo, ok := debug.ReadBuildInfo() + + if ok && (buildinfo != nil) && (buildinfo.Main.Version != "(devel)") { + version = buildinfo.Main.Version + } + + return version +}