Compare commits

..
129 Commits
Author SHA1 Message Date
Umputun 307e69e5c1 Bump dependencies
- chroma/v2: v2.20.0 → v2.21.1
- go-pkgz/auth/v2: v2.1.0 → v2.1.1
- go-pkgz/rest: v1.20.4 → v1.20.6
- golang.org/x/* packages to latest

Also exclude "meaningless package names" revive warning in linter config.
2025-12-24 01:48:14 -06:00
Dmitry VerkhoturovandUmputun d5b07d7670 Fix WriteHeader + RenderJSON causing wrong Content-Type header
Replace WriteHeader() + RenderJSON() pattern with EncodeJSON() which
properly sets Content-Type header before writing status code. The
previous pattern caused Content-Type to default to text/plain instead
of application/json, breaking frontend JSON parsing.

Fixes #1979
2025-12-16 13:01:19 -06:00
Dmitry VerkhoturovandGitHub 41b75eba08 Merge pull request #1977 from umputun/docker-native-arm64-runners
Improve GitHub Actions workflows security and performance
2025-12-08 14:46:13 -06:00
UmputunandGitHub 4aa8362de1 Merge pull request #1976 from umputun/ci-native-arm64-runners
Migrate Docker builds to native GitHub ARM64 runners
2025-12-07 16:01:57 -06:00
Umputun dc168f69bf Fix dual-registry Docker builds and drop armv7
- Remove armv7 platform (eliminates QEMU emulation bottleneck)
- Build separately to ghcr.io and DockerHub with distinct digests
- Create registry-specific manifests from corresponding digests
- Update expected digest count from 3 to 2 per registry
2025-12-07 15:58:35 -06:00
Dmitry Verkhoturov 2a4a1591fa Migrate Docker builds to native GitHub ARM64 runners
Replace QEMU emulation with GitHub's native ARM64 runners for faster builds:
- Use ubuntu-24.04-arm for ARM builds instead of QEMU emulation
- Split build job into matrix for parallel platform builds
- Add digest-based workflow for multi-arch manifest creation
- Keep build-test on ARM64 runner for faster PR verification
2025-12-07 21:39:59 +00:00
Dmitry VerkhoturovandUmputun bc612ddf81 Remove redundant frame ancestors log from middleware
The log message was printed on every request when allowed ancestors
were configured, creating unnecessary noise in the logs.
2025-12-04 11:15:06 -06:00
Dmitry VerkhoturovandUmputun 9132a6158b Add display name format to EMAIL_FROM examples in docs
Show users they can use "Display Name"<email@example.com> format
for AUTH_EMAIL_FROM and NOTIFY_EMAIL_FROM settings.
2025-12-04 11:14:32 -06:00
Dmitry VerkhoturovandUmputun 658bf307b1 Add mailto: to CSP frame-src directive
Allow mailto links on the deleteme page to work without being blocked
by Content Security Policy.
2025-12-04 11:13:50 -06:00
Fredrik AppelrosandUmputun c6efcc56a9 Fix deleteme feature
The last step of the data deletion feature–when the admin user visits the link sent by the user requesting to delete their data–was broken due to the deleteme.js script not being loaded.
2025-12-04 11:13:50 -06:00
dependabot[bot]andUmputun bdee00b662 Bump nanoid from 3.3.7 to 3.3.8 in /site
Bumps [nanoid](https://github.com/ai/nanoid) from 3.3.7 to 3.3.8.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.3.7...3.3.8)

---
updated-dependencies:
- dependency-name: nanoid
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-04 11:13:02 -06:00
kumudiaorongandUmputun 3013d03be2 fix: Modify the example to avoid misunderstanding 2025-12-04 11:12:32 -06:00
4a2bb5eda8 #1833 - Toolbar buttons are stuck to the main comment form (#1948)
* 1833 - Toolbar buttons are stuck to the main comment form

* Add readonly and JSDoc to CommentForm textareaId properties

Improve code quality based on review feedback: mark textareaId as
readonly since it should never change after construction, and add
JSDoc to static textareaCounter explaining its purpose.

---------

Co-authored-by: Dmitry Verkhoturov <paskal.07@gmail.com>
2025-12-04 11:12:05 -06:00
d01b738741 Implement function to prune string keeping HTML closing tags (#1870)
* Implement function to prune string keeping HTML closing tags

Fixes #1587

* change const name

remove unneeded comment

* move pruneHTML to separated file

* move const back to telegram.go

* Add unit tests for string array manipulation and HTML pruning

Introduce comprehensive test cases for stringArr methods (Push, Pop, Unshift, Shift, String) to ensure correct behavior and state management. Additionally, add tests for HTML pruning functions (pruneHTML, pruneStringToWord) to validate handling of length constraints and formatting scenarios.

* Improve behavior

* Fix pruneHTML to count visible text only, add parent text pruning

- Fix bug where HTML tags were counted toward the character limit
  instead of only visible text content
- Add pruning for parent comment text in Telegram notifications
- Simplify pruneStringToWord using strings.LastIndex
- Remove unused stringArr type and its tests
- Consolidate and simplify test cases

---------

Co-authored-by: Umputun <umputun@gmail.com>
Co-authored-by: Dmitry Verkhoturov <paskal.07@gmail.com>
2025-12-04 11:11:10 -06:00
Dmitry VerkhoturovandGitHub 564e8ff316 Update go dependencies (#1972) 2025-12-03 19:47:01 -06:00
dependabot[bot]andUmputun b451142790 Bump js-yaml from 3.14.1 to 3.14.2 in /site
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 3.14.1 to 3.14.2.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/3.14.1...3.14.2)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 3.14.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-03 18:18:10 -06:00
dependabot[bot]andUmputun 6d8a0c783b Bump the go-modules-updates group across 1 directory with 11 updates
Bumps the go-modules-updates group with 11 updates in the /backend directory:

| Package | From | To |
| --- | --- | --- |
| [github.com/PuerkitoBio/goquery](https://github.com/PuerkitoBio/goquery) | `1.10.2` | `1.11.0` |
| [github.com/alecthomas/chroma/v2](https://github.com/alecthomas/chroma) | `2.15.0` | `2.20.0` |
| [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) | `5.2.1` | `5.2.3` |
| [github.com/go-chi/cors](https://github.com/go-chi/cors) | `1.2.1` | `1.2.2` |
| [github.com/go-pkgz/jrpc](https://github.com/go-pkgz/jrpc) | `0.3.1` | `0.4.0` |
| [github.com/go-pkgz/lgr](https://github.com/go-pkgz/lgr) | `0.12.0` | `0.12.1` |
| [github.com/go-pkgz/rest](https://github.com/go-pkgz/rest) | `1.20.3` | `1.20.4` |
| [github.com/golang-jwt/jwt/v5](https://github.com/golang-jwt/jwt) | `5.2.2` | `5.3.0` |
| [github.com/stretchr/testify](https://github.com/stretchr/testify) | `1.10.0` | `1.11.1` |
| [go.etcd.io/bbolt](https://github.com/etcd-io/bbolt) | `1.4.0` | `1.4.3` |
| [golang.org/x/image](https://github.com/golang/image) | `0.26.0` | `0.33.0` |

Updates `github.com/PuerkitoBio/goquery` from 1.10.2 to 1.11.0
- [Release notes](https://github.com/PuerkitoBio/goquery/releases)
- [Commits](https://github.com/PuerkitoBio/goquery/compare/v1.10.2...v1.11.0)

Updates `github.com/alecthomas/chroma/v2` from 2.15.0 to 2.20.0
- [Release notes](https://github.com/alecthomas/chroma/releases)
- [Commits](https://github.com/alecthomas/chroma/compare/v2.15.0...v2.20.0)

Updates `github.com/go-chi/chi/v5` from 5.2.1 to 5.2.3
- [Release notes](https://github.com/go-chi/chi/releases)
- [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md)
- [Commits](https://github.com/go-chi/chi/compare/v5.2.1...v5.2.3)

Updates `github.com/go-chi/cors` from 1.2.1 to 1.2.2
- [Release notes](https://github.com/go-chi/cors/releases)
- [Commits](https://github.com/go-chi/cors/compare/v1.2.1...v1.2.2)

Updates `github.com/go-pkgz/jrpc` from 0.3.1 to 0.4.0
- [Release notes](https://github.com/go-pkgz/jrpc/releases)
- [Commits](https://github.com/go-pkgz/jrpc/compare/v0.3.1...v0.4.0)

Updates `github.com/go-pkgz/lgr` from 0.12.0 to 0.12.1
- [Release notes](https://github.com/go-pkgz/lgr/releases)
- [Commits](https://github.com/go-pkgz/lgr/compare/v0.12.0...v0.12.1)

Updates `github.com/go-pkgz/rest` from 1.20.3 to 1.20.4
- [Release notes](https://github.com/go-pkgz/rest/releases)
- [Commits](https://github.com/go-pkgz/rest/compare/v1.20.3...v1.20.4)

Updates `github.com/golang-jwt/jwt/v5` from 5.2.2 to 5.3.0
- [Release notes](https://github.com/golang-jwt/jwt/releases)
- [Commits](https://github.com/golang-jwt/jwt/compare/v5.2.2...v5.3.0)

Updates `github.com/stretchr/testify` from 1.10.0 to 1.11.1
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.10.0...v1.11.1)

Updates `go.etcd.io/bbolt` from 1.4.0 to 1.4.3
- [Release notes](https://github.com/etcd-io/bbolt/releases)
- [Commits](https://github.com/etcd-io/bbolt/compare/v1.4.0...v1.4.3)

Updates `golang.org/x/image` from 0.26.0 to 0.33.0
- [Commits](https://github.com/golang/image/compare/v0.26.0...v0.33.0)

---
updated-dependencies:
- dependency-name: github.com/PuerkitoBio/goquery
  dependency-version: 1.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules-updates
- dependency-name: github.com/alecthomas/chroma/v2
  dependency-version: 2.20.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules-updates
- dependency-name: github.com/go-chi/chi/v5
  dependency-version: 5.2.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: go-modules-updates
- dependency-name: github.com/go-chi/cors
  dependency-version: 1.2.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: go-modules-updates
- dependency-name: github.com/go-pkgz/jrpc
  dependency-version: 0.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules-updates
- dependency-name: github.com/go-pkgz/lgr
  dependency-version: 0.12.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: go-modules-updates
- dependency-name: github.com/go-pkgz/rest
  dependency-version: 1.20.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: go-modules-updates
- dependency-name: github.com/golang-jwt/jwt/v5
  dependency-version: 5.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules-updates
- dependency-name: github.com/stretchr/testify
  dependency-version: 1.11.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules-updates
- dependency-name: go.etcd.io/bbolt
  dependency-version: 1.4.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: go-modules-updates
- dependency-name: golang.org/x/image
  dependency-version: 0.33.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-03 18:17:46 -06:00
notmalicikandGitHub d925584af8 Romanian language added (#1962) 2025-12-03 12:57:07 -06:00
dependabot[bot]andUmputun 22ee7a06d5 Bump playwright from 1.25.0 to 1.55.1 in /frontend
Bumps [playwright](https://github.com/microsoft/playwright) from 1.25.0 to 1.55.1.
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.25.0...v1.55.1)

---
updated-dependencies:
- dependency-name: playwright
  dependency-version: 1.55.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-03 11:44:51 -06:00
dependabot[bot]andUmputun a311ff7b38 Bump the github-actions-updates group across 1 directory with 6 updates
Bumps the github-actions-updates group with 6 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [actions/checkout](https://github.com/actions/checkout) | `4` | `5` |
| [actions/setup-go](https://github.com/actions/setup-go) | `5` | `6` |
| [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) | `6` | `8` |
| [actions/setup-node](https://github.com/actions/setup-node) | `4` | `6` |
| [pnpm/action-setup](https://github.com/pnpm/action-setup) | `4.1.0` | `4.2.0` |
| [actions/upload-artifact](https://github.com/actions/upload-artifact) | `4` | `5` |



Updates `actions/checkout` from 4 to 5
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

Updates `actions/setup-go` from 5 to 6
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v5...v6)

Updates `golangci/golangci-lint-action` from 6 to 8
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v6...v8)

Updates `actions/setup-node` from 4 to 6
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v6)

Updates `pnpm/action-setup` from 4.1.0 to 4.2.0
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/v4.1.0...v4.2.0)

Updates `actions/upload-artifact` from 4 to 5
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions-updates
- dependency-name: actions/setup-go
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions-updates
- dependency-name: golangci/golangci-lint-action
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions-updates
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions-updates
- dependency-name: pnpm/action-setup
  dependency-version: 4.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions-updates
- dependency-name: actions/upload-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-03 11:43:50 -06:00
ClaudeandUmputun 88257931ca Update Google and X (Twitter) logos to latest brand versions
- Update Google logo to use gradient version per new brand guidelines
- Replace Twitter bird logo with X logo
- Add light/dark variants for X logo (like Apple and GitHub)
- Update oauth.consts.ts to use new X logo variants

Fixes #1957
2025-12-03 11:43:25 -06:00
dependabot[bot]andUmputun 588ec169ff Bump golang.org/x/crypto from 0.37.0 to 0.45.0 in /backend
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.37.0 to 0.45.0.
- [Commits](https://github.com/golang/crypto/compare/v0.37.0...v0.45.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.45.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-03 11:43:03 -06:00
dependabot[bot]andUmputun d9efa765d1 Bump golang.org/x/crypto in /backend/_example/memory_store
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.37.0 to 0.45.0.
- [Commits](https://github.com/golang/crypto/compare/v0.37.0...v0.45.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.45.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-03 11:42:34 -06:00
Dmitry VerkhoturovandGitHub baf0db1947 Get rid of github.com/go-chi/render use (#1919)
Replace go-chi/render with go-pkgz/rest for JSON responses and custom
helpers for HTML/plain text responses.

Key changes:
- Replace render.JSON/render.Status with rest.RenderJSON and explicit
  w.WriteHeader() calls
- Replace render.DecodeJSON with json.NewDecoder().Decode()
- Add SendErrorJSON helper that sets Content-Type header before
  WriteHeader (required since rest.RenderJSON can't set headers after
  WriteHeader is called)
- Add HTMLResponse and PlainTextResponse helpers

Fix export double-execution in migrator.go:
The original code called Export twice - once to io.Discard to check for
errors, then again to actually write. This was wasteful and had a race
condition risk. Now file mode buffers to memory first for atomic
success/failure, while stream mode writes directly with proper error
handling.
2025-12-03 11:41:29 -06:00
UmputunandGitHub 34ea4c3e83 Migrate golangci-lint to v2 and update Go version (#1965)
* migrate golangci-lint to v2 and update go version

- migrated .golangci.yml to version 2 format
- updated go.mod from 1.23.0 to 1.24
- removed deprecated run.timeout configuration

* update to go 1.25 and baseimage v1.17.0

- updated go.mod to go 1.25
- updated Dockerfile to use buildgo-v1.17.0 (go 1.25.0)
- updated Dockerfile to use app-v1.17.0

* fix flaky tests with proper synchronization

- use assert.Eventually instead of fixed sleep in TestService_Many
- wait for webhook before shutdown in TestMain_WithWebhook
- fixes race conditions exposed by Go 1.25 scheduler changes

* fix data race in MockDest.closed field

- add IsClosed() method with proper locking
- add locking to String() method
- use IsClosed() in tests instead of direct field access
- fixes race condition detected by go test -race

* update example go.mod to go 1.25

* update golangci-lint to v2.6.0 for go 1.25 support

* fix linter issue and update CLAUDE.md

- merge conditional assignment in example accessor/data.go
- add reminder in CLAUDE.md to always test and lint examples before committing

* update example Dockerfile to baseimage v1.17.0 for go 1.25
2025-11-02 14:17:39 -06:00
UmputunandGitHub 8112f445f4 Merge pull request #1947 from schnerring/add-backup-docs
docs: improve backup instructions; closes #1613
2025-10-01 02:02:46 -05:00
UmputunandGitHub bd7bbe629f Merge pull request #1953 from diosfera/master
Add Macedonian translation
2025-10-01 02:01:43 -05:00
diosfera ed8b3c314d Delete package-lock.json file 2025-09-21 20:07:12 +02:00
diosfera 51ec396691 Add Macedonian translation 2025-09-17 06:29:51 +02:00
Michael Schnerring ca8ab66812 docs: improve backup instructions; closes #1613 2025-09-04 22:20:18 +02:00
UmputunandGitHub eaa64bac45 Merge pull request #1929 from umputun/fix/auth-send-jwt-header
Fix login persistence with AUTH_SEND_JWT_HEADER enabled
2025-07-06 17:55:09 -05:00
UmputunandGitHub abdca907a7 Merge branch 'master' into fix/auth-send-jwt-header 2025-07-06 15:39:12 -05:00
UmputunandGitHub a9c8cf51a0 Merge pull request #1937 from umputun/docs/telegram-group-notifications 2025-06-16 00:43:37 -05:00
Dmitry Verkhoturov d829a57061 docs: update Telegram configuration with group notification details
Add information about sending notifications to users, groups, and channels.
Document that public group usernames can be used without @ symbol as IDs.

Addresses discussion #1756
2025-06-16 05:19:50 +01:00
UmputunandGitHub 1dffb2f16e Merge pull request #1936 from talentedunicorn/patch-1
Fixed typo
2025-06-06 12:12:56 -05:00
EzeandGitHub 9e2a1da0df Fixed typo 2025-06-07 01:11:29 +08:00
UmputunandGitHub 6fbaa806f0 Merge pull request #1934 from up9cloud/master
Fix typo (rootDissapear should be rootDisappear)
2025-06-06 10:46:29 -05:00
HsüanandGitHub 58acca4dcb Fix profile.spec.tsx typo 2025-06-04 15:18:46 -07:00
HsüanandGitHub 22d21df22b Fix profile.ts typo 2025-06-04 15:17:40 -07:00
Dmitry Verkhoturov ddb490bbc1 Fix login persistence with AUTH_SEND_JWT_HEADER enabled
With AUTH_SEND_JWT_HEADER=true, frontend now properly handles JWT authentication:
- Store JWT token in client-side cookie named 'JWT'
- Extract and store XSRF token from JWT payload
- Set Secure flag automatically when on HTTPS connection
- Update documentation to clarify this behavior

This fixes an issue where login state would be lost after page reload
when using header-based JWT authentication.
2025-04-29 23:45:55 +01:00
UmputunandGitHub 242499787e Merge pull request #1930 from umputun/paskal/update-gopkgz
Update go-pkgz, system modules
2025-04-29 02:31:12 -05:00
Dmitry Verkhoturov fd0799384f Update go-pkgz, system modules
This brings stricter check for auth provider names, slog support.
2025-04-29 08:24:53 +01:00
UmputunandGitHub 61dbf6b1f2 Merge pull request #1915 from umputun/dependabot/go_modules/backend/github.com/redis/go-redis/v9-9.7.3
Bump github.com/redis/go-redis/v9 from 9.7.0 to 9.7.3 in /backend
2025-03-24 21:51:56 -05:00
UmputunandGitHub 4a3c73db31 Merge pull request #1911 from umputun/dependabot/npm_and_yarn/site/prismjs-1.30.0
Bump prismjs from 1.29.0 to 1.30.0 in /site
2025-03-24 21:51:02 -05:00
UmputunandGitHub df510360e6 Merge pull request #1909 from umputun/dependabot/github_actions/github-actions-updates-e0e9667351
Bump pnpm/action-setup from 4.0.0 to 4.1.0 in the github-actions-updates group
2025-03-24 21:50:47 -05:00
UmputunandGitHub a3309516c3 Merge pull request #1916 from umputun/dependabot/go_modules/backend/github.com/golang-jwt/jwt/v5-5.2.2
Bump github.com/golang-jwt/jwt/v5 from 5.2.1 to 5.2.2 in /backend
2025-03-24 21:50:33 -05:00
UmputunandGitHub 5e30dc86e1 Merge pull request #1917 from umputun/paskal/golangci_lint_v2
Automatic fix of errors reported by golangci-lint v2
2025-03-24 21:50:20 -05:00
dependabot[bot]andGitHub bbcba5487e Bump github.com/redis/go-redis/v9 from 9.7.0 to 9.7.3 in /backend
Bumps [github.com/redis/go-redis/v9](https://github.com/redis/go-redis) from 9.7.0 to 9.7.3.
- [Release notes](https://github.com/redis/go-redis/releases)
- [Changelog](https://github.com/redis/go-redis/blob/master/CHANGELOG.md)
- [Commits](https://github.com/redis/go-redis/compare/v9.7.0...v9.7.3)

---
updated-dependencies:
- dependency-name: github.com/redis/go-redis/v9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-25 02:49:28 +00:00
UmputunandGitHub c32e5efdc2 Merge pull request #1918 from umputun/paskal/add_systemd
Add install instructions for setting up Remark42 as a systemd service
2025-03-24 21:48:27 -05:00
UmputunandGitHub af139a7f4c Merge pull request #1910 from umputun/dependabot/go_modules/backend/go-modules-updates-e61953c257
Bump the go-modules-updates group in /backend with 8 updates
2025-03-24 21:48:05 -05:00
dependabot[bot]andDmitry V 89221ff2bc Bump the go-modules-updates group in /backend with 8 updates
Bumps the go-modules-updates group in /backend with 8 updates:

| Package | From | To |
| --- | --- | --- |
| [github.com/PuerkitoBio/goquery](https://github.com/PuerkitoBio/goquery) | `1.10.1` | `1.10.2` |
| [github.com/alecthomas/chroma/v2](https://github.com/alecthomas/chroma) | `2.14.0` | `2.15.0` |
| [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) | `5.2.0` | `5.2.1` |
| [github.com/go-pkgz/jrpc](https://github.com/go-pkgz/jrpc) | `0.3.0` | `0.3.1` |
| [go.etcd.io/bbolt](https://github.com/etcd-io/bbolt) | `1.3.11` | `1.4.0` |
| [golang.org/x/crypto](https://github.com/golang/crypto) | `0.31.0` | `0.33.0` |
| [golang.org/x/image](https://github.com/golang/image) | `0.23.0` | `0.25.0` |
| [golang.org/x/net](https://github.com/golang/net) | `0.33.0` | `0.35.0` |

Updates `github.com/PuerkitoBio/goquery` from 1.10.1 to 1.10.2
- [Release notes](https://github.com/PuerkitoBio/goquery/releases)
- [Commits](https://github.com/PuerkitoBio/goquery/compare/v1.10.1...v1.10.2)

Updates `github.com/alecthomas/chroma/v2` from 2.14.0 to 2.15.0
- [Release notes](https://github.com/alecthomas/chroma/releases)
- [Changelog](https://github.com/alecthomas/chroma/blob/master/.goreleaser.yml)
- [Commits](https://github.com/alecthomas/chroma/compare/v2.14.0...v2.15.0)

Updates `github.com/go-chi/chi/v5` from 5.2.0 to 5.2.1
- [Release notes](https://github.com/go-chi/chi/releases)
- [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md)
- [Commits](https://github.com/go-chi/chi/compare/v5.2.0...v5.2.1)

Updates `github.com/go-pkgz/jrpc` from 0.3.0 to 0.3.1
- [Release notes](https://github.com/go-pkgz/jrpc/releases)
- [Commits](https://github.com/go-pkgz/jrpc/compare/v0.3.0...v0.3.1)

Updates `go.etcd.io/bbolt` from 1.3.11 to 1.4.0
- [Release notes](https://github.com/etcd-io/bbolt/releases)
- [Commits](https://github.com/etcd-io/bbolt/compare/v1.3.11...v1.4.0)

Updates `golang.org/x/crypto` from 0.31.0 to 0.33.0
- [Commits](https://github.com/golang/crypto/compare/v0.31.0...v0.33.0)

Updates `golang.org/x/image` from 0.23.0 to 0.25.0
- [Commits](https://github.com/golang/image/compare/v0.23.0...v0.25.0)

Updates `golang.org/x/net` from 0.33.0 to 0.35.0
- [Commits](https://github.com/golang/net/compare/v0.33.0...v0.35.0)

---
updated-dependencies:
- dependency-name: github.com/PuerkitoBio/goquery
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: go-modules-updates
- dependency-name: github.com/alecthomas/chroma/v2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules-updates
- dependency-name: github.com/go-chi/chi/v5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: go-modules-updates
- dependency-name: github.com/go-pkgz/jrpc
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: go-modules-updates
- dependency-name: go.etcd.io/bbolt
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules-updates
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules-updates
- dependency-name: golang.org/x/image
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules-updates
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-24 23:13:46 +01:00
Dmitry Verkhoturov 528242c1ca Add install instructions for setting up Remark42 as a systemd service 2025-03-24 23:09:28 +01:00
Dmitry Verkhoturov edfc5b9d76 Automatic fix of errors reported by golangci-lint v2
- Use strings.ReplaceAll
- Remove redundant internal structure names
2025-03-24 22:46:43 +01:00
dependabot[bot]andGitHub 7e944bfe0d Bump github.com/golang-jwt/jwt/v5 from 5.2.1 to 5.2.2 in /backend
Bumps [github.com/golang-jwt/jwt/v5](https://github.com/golang-jwt/jwt) from 5.2.1 to 5.2.2.
- [Release notes](https://github.com/golang-jwt/jwt/releases)
- [Changelog](https://github.com/golang-jwt/jwt/blob/main/VERSION_HISTORY.md)
- [Commits](https://github.com/golang-jwt/jwt/compare/v5.2.1...v5.2.2)

---
updated-dependencies:
- dependency-name: github.com/golang-jwt/jwt/v5
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-21 22:26:36 +00:00
UmputunandGitHub e6afc58b34 Merge pull request #1914 from umputun/paskal/add_system_requirements 2025-03-16 14:28:22 -05:00
Dmitry Verkhoturov f49b878eb4 Add system requirements section to installation guide 2025-03-16 19:26:42 +00:00
UmputunandGitHub 5fe843de71 Merge pull request #1912 from umputun/dependabot/go_modules/backend/golang.org/x/net-0.36.0
Bump golang.org/x/net from 0.33.0 to 0.36.0 in /backend
2025-03-12 20:30:34 -05:00
dependabot[bot]andDmitry Verkhoturov 6c9ade9062 Bump golang.org/x/net from 0.33.0 to 0.36.0 in /backend
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.33.0 to 0.36.0.
- [Commits](https://github.com/golang/net/compare/v0.33.0...v0.36.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-12 22:58:15 +00:00
dependabot[bot]andGitHub ed3c104ba4 Bump prismjs from 1.29.0 to 1.30.0 in /site
Bumps [prismjs](https://github.com/PrismJS/prism) from 1.29.0 to 1.30.0.
- [Release notes](https://github.com/PrismJS/prism/releases)
- [Changelog](https://github.com/PrismJS/prism/blob/master/CHANGELOG.md)
- [Commits](https://github.com/PrismJS/prism/compare/v1.29.0...v1.30.0)

---
updated-dependencies:
- dependency-name: prismjs
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-10 22:24:16 +00:00
dependabot[bot]andGitHub a90b4296c6 Bump pnpm/action-setup in the github-actions-updates group
Bumps the github-actions-updates group with 1 update: [pnpm/action-setup](https://github.com/pnpm/action-setup).


Updates `pnpm/action-setup` from 4.0.0 to 4.1.0
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/v4.0.0...v4.1.0)

---
updated-dependencies:
- dependency-name: pnpm/action-setup
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-09 18:01:26 +00:00
UmputunandGitHub 0ebe893125 Merge pull request #1908 from umputun/paskal/disable_npm_updates
Disable dependabot automatic PRs for NPM modules
2025-03-09 13:00:21 -05:00
Dmitry Verkhoturov e32ca020c0 Disable dependabot automatic PRs for NPM modules
These have to be updated manually
because there are always breaking changes.
2025-03-09 17:17:20 +01:00
UmputunandGitHub 558350c546 Merge pull request #1897 from umputun/paskal/claude
Add first version of the CLAUDE.md
2025-03-01 17:19:34 -06:00
UmputunandGitHub c50a5d6245 Merge pull request #1907 from umputun/paskal/get_rid_of_dockerhub
Migrate Docker images from Docker Hub to GitHub Container Registry
2025-03-01 17:19:01 -06:00
Dmitry Verkhoturov bb650d7526 Migrate Docker images from Docker Hub to GitHub Container Registry
This commit replaces all references to `umputun/remark42` Docker images
on Docker Hub with `ghcr.io/umputun/remark42` from the GitHub
Container Registry. It updates various Docker Compose files,
documentation, and the Makefile to use the new image location.
It also updates the kubernetes example to use the latest version.

Docker Hub is going to kill free pulls for too long by now.
2025-03-01 23:15:09 +00:00
Dmitry Verkhoturov 551212db82 Add first version of the CLAUDE.md 2025-02-25 22:05:25 +00:00
dependabot[bot]andUmputun 2e00002413 Bump the go-modules-updates group in /backend with 5 updates
Bumps the go-modules-updates group in /backend with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [github.com/PuerkitoBio/goquery](https://github.com/PuerkitoBio/goquery) | `1.10.0` | `1.10.1` |
| [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) | `5.1.0` | `5.2.0` |
| [github.com/go-pkgz/rest](https://github.com/go-pkgz/rest) | `1.19.0` | `1.20.2` |
| [golang.org/x/image](https://github.com/golang/image) | `0.22.0` | `0.23.0` |
| [golang.org/x/net](https://github.com/golang/net) | `0.31.0` | `0.33.0` |

Updates `github.com/PuerkitoBio/goquery` from 1.10.0 to 1.10.1
- [Release notes](https://github.com/PuerkitoBio/goquery/releases)
- [Commits](https://github.com/PuerkitoBio/goquery/compare/v1.10.0...v1.10.1)

Updates `github.com/go-chi/chi/v5` from 5.1.0 to 5.2.0
- [Release notes](https://github.com/go-chi/chi/releases)
- [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md)
- [Commits](https://github.com/go-chi/chi/compare/v5.1.0...v5.2.0)

Updates `github.com/go-pkgz/rest` from 1.19.0 to 1.20.2
- [Release notes](https://github.com/go-pkgz/rest/releases)
- [Commits](https://github.com/go-pkgz/rest/compare/v1.19.0...v1.20.2)

Updates `golang.org/x/image` from 0.22.0 to 0.23.0
- [Commits](https://github.com/golang/image/compare/v0.22.0...v0.23.0)

Updates `golang.org/x/net` from 0.31.0 to 0.33.0
- [Commits](https://github.com/golang/net/compare/v0.31.0...v0.33.0)

---
updated-dependencies:
- dependency-name: github.com/PuerkitoBio/goquery
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: go-modules-updates
- dependency-name: github.com/go-chi/chi/v5
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules-updates
- dependency-name: github.com/go-pkgz/rest
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules-updates
- dependency-name: golang.org/x/image
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules-updates
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-02 14:02:42 -06:00
UmputunandGitHub 81f70aa287 Merge pull request #1875 from codiflow/codiflow-patch-1 2024-12-28 20:44:53 -06:00
Christian 🦄andDmitry Verkhoturov f104e3c775 Update de.json
Updated Telegram bot description
2024-12-29 03:43:57 +01:00
UmputunandGitHub be076d03e9 Merge pull request #1878 from cubismod/webhook-params
Docs update describing available variables for webhook templating
2024-12-21 11:26:57 -06:00
Ryan e1166a48cf add a table describing additional variables available for use with webhook templating 2024-12-20 21:08:15 -06:00
Umputun 3f0789fd90 mod tidy for memory store example 2024-12-17 23:39:29 -06:00
UmputunandGitHub 518ae79556 Merge pull request #1869 from aliksend/aliksend/fix-importing-anonymous-comments-from-commento
Straightforward fix for importing anonymous comments from commento
2024-12-17 16:04:38 -06:00
UmputunandGitHub 3c55238bdd Merge pull request #1874 from umputun/dependabot/go_modules/backend/golang.org/x/crypto-0.31.0
Bump golang.org/x/crypto from 0.29.0 to 0.31.0 in /backend
2024-12-17 13:55:45 -06:00
Alik Send 556b95a655 Fix tests. Add test for anonymous comment 2024-12-16 23:16:26 -06:00
Alik Send bfef15f05f Straightforward fix for importing anonymous comments from commento
Fixes #1821
2024-12-16 23:16:26 -06:00
UmputunandGitHub c3ba55ba43 Merge pull request #1699 from umputun/paskal/comments_pagination
add pagination to GET /api/v1/find endpoint
2024-12-11 20:17:23 -06:00
dependabot[bot]andGitHub 0af8b2eab0 Bump golang.org/x/crypto from 0.29.0 to 0.31.0 in /backend
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.29.0 to 0.31.0.
- [Commits](https://github.com/golang/crypto/compare/v0.29.0...v0.31.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-11 22:23:01 +00:00
Dmitry Verkhoturov 82a0888c42 add pagination to GET /api/v1/find endpoint
`format=tree` pagination provides top-level comments with all replies
and returns the last top-level comment as `last_comment` to be used
as `offset` for the next page. If comments and replies overflow
the limit, the one stepping out of the limit will not be returned.
 If the first comment and its replies after the given offset overflow
 the limit, it will be returned with all the replies.

`format=plain` pagination works by providing all comments and returning
the last comment as `last_comment` to be used as `offset`
for the next page.
2024-12-10 14:26:55 +00:00
UmputunandGitHub d5162d3fe6 Merge pull request #1758 from umputun/paskal/jwt_v5
Update to go-pkgz/auth/v2 and golang-jwt/jwt/v5
2024-12-09 19:53:00 -06:00
Dmitry Verkhoturov e61a46efff Improve error message for checking claims.Audience 2024-12-10 00:45:04 +00:00
Dmitry Verkhoturov f473105c52 add tests for jwt5 multiple auds and improve existing tests 2024-12-09 01:54:04 +00:00
Dmitry Verkhoturov c2d386230c vendor new modules 2024-12-09 01:54:03 +00:00
Dmitry Verkhoturov e3ad01b555 migrate to go-pkgz/auth/v2 2024-12-09 01:54:03 +00:00
Dmitry Verkhoturov 8d9e55c33c update github.com/golang-jwt/jwt to v5 2024-12-09 01:54:03 +00:00
UmputunandGitHub 6402ef9cae Merge pull request #1871 from umputun/paskal/improve_tests
Improve tests
2024-12-08 19:53:15 -06:00
Dmitry Verkhoturov 4ed48dd85c Improve tests 2024-12-09 01:45:57 +00:00
UmputunandGitHub 9628312b5d Merge pull request #1865 from umputun/dependabot/go_modules/backend/go-modules-updates-81f599025a 2024-12-01 22:32:10 -06:00
UmputunandGitHub c65c2b395d Merge branch 'master' into dependabot/go_modules/backend/go-modules-updates-81f599025a 2024-12-01 22:31:58 -06:00
UmputunandGitHub 27671c50c4 Merge pull request #1866 from umputun/dependabot/npm_and_yarn/site/npm-modules-updates-for-tests-87ff55c30d 2024-12-01 22:31:40 -06:00
UmputunandGitHub 56ac1bb841 Merge branch 'master' into dependabot/npm_and_yarn/site/npm-modules-updates-for-tests-87ff55c30d 2024-12-01 22:31:28 -06:00
dependabot[bot]andDmitry Verkhoturov 0aadf0ba86 Bump the go-modules-updates group in /backend with 3 updates
Bumps the go-modules-updates group in /backend with 3 updates: [github.com/stretchr/testify](https://github.com/stretchr/testify), [golang.org/x/crypto](https://github.com/golang/crypto) and [golang.org/x/net](https://github.com/golang/net).

Updates `github.com/stretchr/testify` from 1.9.0 to 1.10.0
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.9.0...v1.10.0)

Updates `golang.org/x/crypto` from 0.27.0 to 0.29.0
- [Commits](https://github.com/golang/crypto/compare/v0.27.0...v0.29.0)

Updates `golang.org/x/net` from 0.29.0 to 0.31.0
- [Commits](https://github.com/golang/net/compare/v0.29.0...v0.31.0)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules-updates
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules-updates
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-02 04:24:48 +00:00
UmputunandGitHub 34c667b83a Merge pull request #1862 from umputun/paskal/improve_server_test 2024-12-01 22:24:11 -06:00
dependabot[bot]andDmitry Verkhoturov f2e5758b6c Bump the npm-modules-updates-for-tests group in /site with 2 updates
Bumps the npm-modules-updates-for-tests group in /site with 2 updates: [prettier](https://github.com/prettier/prettier) and [tailwindcss](https://github.com/tailwindlabs/tailwindcss).

Updates `prettier` from 3.3.3 to 3.4.1
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.3.3...3.4.1)

Updates `tailwindcss` from 3.4.14 to 3.4.15
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/v3.4.15/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/compare/v3.4.14...v3.4.15)

---
updated-dependencies:
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-modules-updates-for-tests
- dependency-name: tailwindcss
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-modules-updates-for-tests
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-02 04:03:21 +00:00
Dmitry Verkhoturov cd7f616596 Fix server test configuration
Previously, tmp boltdb was always created in directory 8080.
2024-12-02 03:36:32 +00:00
UmputunandGitHub a0c412ee1e Merge pull request #1861 from umputun/paskal/example_dependabot 2024-12-01 21:33:33 -06:00
Dmitry VerkhoturovandGitHub 15f5b7dde5 Stop updating examples via dependabot
Examples should be updated alongside the backend directory and it makes no sense to have separate update PRs for it.
2024-12-02 03:04:46 +00:00
UmputunandGitHub a561588117 Merge pull request #1859 from umputun/dependabot/github_actions/github-actions-updates-d109cd9e8b
Bump codecov/codecov-action from 4 to 5 in the github-actions-updates group
2024-12-01 21:01:52 -06:00
dependabot[bot]andGitHub ac36dccd19 Bump codecov/codecov-action in the github-actions-updates group
Bumps the github-actions-updates group with 1 update: [codecov/codecov-action](https://github.com/codecov/codecov-action).


Updates `codecov/codecov-action` from 4 to 5
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-01 11:33:06 +00:00
UmputunandGitHub afdac27651 Merge pull request #1853 from umputun/paskal/docker-compose
Update docker-compose.yml with example for local reverse proxy
2024-11-19 18:59:10 -06:00
Dmitry VerkhoturovandGitHub 995c4963fb Update docker-compose.yml with example for local reverse proxy
In discussion #1852 I realised that we don't have example with reverse proxy running outside of docker, this commit fixes that.
2024-11-20 00:57:46 +00:00
UmputunandGitHub 16ff2690f0 Merge pull request #1824 from mgkbadola/discord-oauth
feat: Discord OAuth support
2024-11-18 14:57:31 -06:00
Mrigank BadolaandGitHub d0dc1131ea Merge branch 'master' into discord-oauth 2024-11-12 21:24:24 +05:30
Dmitry VerkhoturovandUmputun d5e3602e54 Fix fetch module import so that latest release version would be shown on website 2024-11-10 18:30:48 -06:00
Dmitry VerkhoturovandUmputun a4772bd9de Do not fail if frontend pre-commit hook fails 2024-11-10 18:12:43 -06:00
Dmitry VerkhoturovandUmputun d23b7003c6 Downgrade eleventy to fix the website 2024-11-10 18:05:51 -06:00
Dmitry VerkhoturovandUmputun 3646c4a871 Drop linux/arm/v7 docker image from site as it doesn't build well 2024-11-10 17:25:16 -06:00
Dmitry VerkhoturovandUmputun 29fc63f116 Clarify steps names for site deployment 2024-11-10 17:25:16 -06:00
Dmitry VerkhoturovandUmputun 8c59bab921 Add nvmrc to /site, update yarn.lock 2024-11-10 17:25:16 -06:00
Dmitry VerkhoturovandUmputun c42511d5a1 Fix cache-dependency-path for actions/setup-go caching to work, fix CI files path 2024-11-10 17:25:16 -06:00
Dmitry VerkhoturovandUmputun 9b5f6ee2c5 Remove deprecated set-output function usage in GitHub actions
The `set-output` command is deprecated and will be disabled soon. Please upgrade to using Environment Files. For more information see: https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/
2024-11-10 17:25:16 -06:00
dependabot[bot]andUmputun b5579152cb Bump the npm-modules-updates-for-tests group across 1 directory with 12 updates
Bumps the npm-modules-updates-for-tests group with 12 updates in the /frontend/packages/api directory:

| Package | From | To |
| --- | --- | --- |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `18.19.64` | `18.19.64` |
| [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `5.62.0` | `8.13.0` |
| [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `5.62.0` | `8.13.0` |
| [@vitest/coverage-c8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-c8) | `0.22.1` | `0.33.0` |
| [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) | `8.10.0` | `9.1.0` |
| [eslint-plugin-prettier](https://github.com/prettier/eslint-plugin-prettier) | `4.2.1` | `5.2.1` |
| [jsdom](https://github.com/jsdom/jsdom) | `20.0.3` | `25.0.1` |
| [msw](https://github.com/mswjs/msw) | `0.44.2` | `1.3.5` |
| [prettier](https://github.com/prettier/prettier) | `2.8.8` | `3.3.3` |
| [typescript](https://github.com/microsoft/TypeScript) | `4.7.4` | `4.9.5` |
| [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `0.22.1` | `2.1.4` |

Updates `@types/node` from 18.19.64 to 22.9.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@typescript-eslint/eslint-plugin` from 5.62.0 to 8.13.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.13.0/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 5.62.0 to 8.13.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.13.0/packages/parser)

Updates `@vitest/coverage-c8` from 0.22.1 to 0.33.0
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v0.33.0/packages/coverage-c8)

Updates `eslint-config-prettier` from 8.10.0 to 9.1.0
- [Changelog](https://github.com/prettier/eslint-config-prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-config-prettier/compare/v8.10.0...v9.1.0)

Updates `eslint-plugin-prettier` from 4.2.1 to 5.2.1
- [Release notes](https://github.com/prettier/eslint-plugin-prettier/releases)
- [Changelog](https://github.com/prettier/eslint-plugin-prettier/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-plugin-prettier/compare/v4.2.1...v5.2.1)

Updates `jsdom` from 20.0.3 to 25.0.1
- [Release notes](https://github.com/jsdom/jsdom/releases)
- [Changelog](https://github.com/jsdom/jsdom/blob/main/Changelog.md)
- [Commits](https://github.com/jsdom/jsdom/compare/20.0.3...25.0.1)

Updates `msw` from 0.44.2 to 2.6.3
- [Release notes](https://github.com/mswjs/msw/releases)
- [Changelog](https://github.com/mswjs/msw/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mswjs/msw/compare/v0.44.2...v2.6.3)

Updates `prettier` from 2.8.8 to 3.3.3
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/2.8.8...3.3.3)

Updates `typescript` from 4.9.5 to 5.6.3
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v4.9.5...v5.6.3)

Updates `vitest` from 0.22.1 to 2.1.4
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v2.1.4/packages/vitest)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-modules-updates-for-tests
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-modules-updates-for-tests
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-modules-updates-for-tests
- dependency-name: "@vitest/coverage-c8"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-modules-updates-for-tests
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-modules-updates-for-tests
- dependency-name: eslint-config-prettier
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-modules-updates-for-tests
- dependency-name: eslint-plugin-prettier
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-modules-updates-for-tests
- dependency-name: jsdom
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-modules-updates-for-tests
- dependency-name: msw
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-modules-updates-for-tests
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-modules-updates-for-tests
- dependency-name: typescript
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-modules-updates-for-tests
- dependency-name: vitest
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-modules-updates-for-tests
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-10 14:00:52 -06:00
dependabot[bot]andUmputun 3c78a54be6 Bump the npm-modules-updates-for-tests group
Bumps the npm-modules-updates-for-tests group in /frontend/e2e with 3 updates: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node), [nanoid](https://github.com/ai/nanoid) and [typescript](https://github.com/microsoft/TypeScript).

Updates `@types/node` from 18.19.64 to 22.9.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `nanoid` from 4.0.2 to 5.0.8
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/4.0.2...5.0.8)

Updates `typescript` from 4.9.5 to 5.6.3
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v4.9.5...v5.6.3)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-modules-updates-for-tests
- dependency-name: nanoid
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-modules-updates-for-tests
- dependency-name: typescript
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-modules-updates-for-tests
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-10 13:55:30 -06:00
Dmitry VerkhoturovandUmputun 3001b37812 Update pnpm from v7 to v8
That suppose to help with Dependabot security updates not updating lockfile.
2024-11-10 13:31:03 -06:00
Dmitry VerkhoturovandUmputun 63048cc798 Specify package manager version in frontend project configuration
This is necessary for Dependabot updates to work properly.
2024-11-10 12:42:07 -06:00
Mrigank Badola 1a27913404 chore: update go-pkgz/auth package 2024-11-10 12:40:07 +05:30
Mrigank Badola 4a39ceee8d sync: Merge branch 'master' of https://github.com/mgkbadola/remark42 into discord-oauth 2024-11-10 10:43:10 +05:30
dependabot[bot]andUmputun 1e659917d2 Bump the npm-modules-updates-for-tests group in /site with 7 updates
Bumps the npm-modules-updates-for-tests group in /site with 7 updates:

| Package | From | To |
| --- | --- | --- |
| [@11ty/eleventy](https://github.com/11ty/eleventy) | `2.0.1` | `3.0.0` |
| [@tailwindcss/typography](https://github.com/tailwindlabs/tailwindcss-typography) | `0.5.10` | `0.5.15` |
| [date-fns](https://github.com/date-fns/date-fns) | `3.3.1` | `4.1.0` |
| [markdown-it](https://github.com/markdown-it/markdown-it) | `14.0.0` | `14.1.0` |
| [markdown-it-anchor](https://github.com/valeriangalliat/markdown-it-anchor) | `8.6.7` | `9.2.0` |
| [prettier](https://github.com/prettier/prettier) | `2.8.8` | `3.3.3` |
| [tailwindcss](https://github.com/tailwindlabs/tailwindcss) | `3.4.1` | `3.4.14` |


Updates `@11ty/eleventy` from 2.0.1 to 3.0.0
- [Release notes](https://github.com/11ty/eleventy/releases)
- [Changelog](https://github.com/11ty/eleventy/blob/main/docs/release-instructions.md)
- [Commits](https://github.com/11ty/eleventy/compare/v2.0.1...v3.0.0)

Updates `@tailwindcss/typography` from 0.5.10 to 0.5.15
- [Release notes](https://github.com/tailwindlabs/tailwindcss-typography/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss-typography/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss-typography/compare/v0.5.10...v0.5.15)

Updates `date-fns` from 3.3.1 to 4.1.0
- [Release notes](https://github.com/date-fns/date-fns/releases)
- [Changelog](https://github.com/date-fns/date-fns/blob/main/CHANGELOG.md)
- [Commits](https://github.com/date-fns/date-fns/compare/v3.3.1...v4.1.0)

Updates `markdown-it` from 14.0.0 to 14.1.0
- [Changelog](https://github.com/markdown-it/markdown-it/blob/master/CHANGELOG.md)
- [Commits](https://github.com/markdown-it/markdown-it/compare/14.0.0...14.1.0)

Updates `markdown-it-anchor` from 8.6.7 to 9.2.0
- [Release notes](https://github.com/valeriangalliat/markdown-it-anchor/releases)
- [Changelog](https://github.com/valeriangalliat/markdown-it-anchor/blob/master/CHANGELOG.md)
- [Commits](https://github.com/valeriangalliat/markdown-it-anchor/compare/v8.6.7...v9.2.0)

Updates `prettier` from 2.8.8 to 3.3.3
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/2.8.8...3.3.3)

Updates `tailwindcss` from 3.4.1 to 3.4.14
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/v3.4.14/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/compare/v3.4.1...v3.4.14)

---
updated-dependencies:
- dependency-name: "@11ty/eleventy"
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-modules-updates-for-tests
- dependency-name: "@tailwindcss/typography"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-modules-updates-for-tests
- dependency-name: date-fns
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-modules-updates-for-tests
- dependency-name: markdown-it
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-modules-updates-for-tests
- dependency-name: markdown-it-anchor
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-modules-updates-for-tests
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-modules-updates-for-tests
- dependency-name: tailwindcss
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-modules-updates-for-tests
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-08 17:54:33 -06:00
daniyalandUmputun 5c586e12bd feat: add "fa" locale 2024-11-08 16:38:09 -06:00
Dmitry VerkhoturovandUmputun 73ca4a1b6d Capitalize "AS" keyword in Dockerfile FROM instructions 2024-11-08 16:27:10 -06:00
Dmitry VerkhoturovandUmputun 7604548035 Update go to 1.23, golangci-lint to 1.61, baseimage to 1.14 2024-11-08 16:19:58 -06:00
dependabot[bot]andUmputun 7529aa7e17 Bump the go-modules-updates group in /backend with 9 updates
Bumps the go-modules-updates group in /backend with 9 updates:

| Package | From | To |
| --- | --- | --- |
| [github.com/PuerkitoBio/goquery](https://github.com/PuerkitoBio/goquery) | `1.9.2` | `1.10.0` |
| [github.com/alecthomas/chroma/v2](https://github.com/alecthomas/chroma) | `2.13.0` | `2.14.0` |
| [github.com/go-pkgz/notify](https://github.com/go-pkgz/notify) | `1.1.1` | `1.2.0` |
| [github.com/go-pkgz/repeater](https://github.com/go-pkgz/repeater) | `1.1.3` | `1.2.0` |
| [github.com/rs/xid](https://github.com/rs/xid) | `1.5.0` | `1.6.0` |
| [go.etcd.io/bbolt](https://github.com/etcd-io/bbolt) | `1.3.10` | `1.3.11` |
| [golang.org/x/crypto](https://github.com/golang/crypto) | `0.25.0` | `0.27.0` |
| [golang.org/x/image](https://github.com/golang/image) | `0.18.0` | `0.22.0` |
| [golang.org/x/net](https://github.com/golang/net) | `0.27.0` | `0.29.0` |

Updates `github.com/PuerkitoBio/goquery` from 1.9.2 to 1.10.0
- [Release notes](https://github.com/PuerkitoBio/goquery/releases)
- [Commits](https://github.com/PuerkitoBio/goquery/compare/v1.9.2...v1.10.0)

Updates `github.com/alecthomas/chroma/v2` from 2.13.0 to 2.14.0
- [Release notes](https://github.com/alecthomas/chroma/releases)
- [Changelog](https://github.com/alecthomas/chroma/blob/master/.goreleaser.yml)
- [Commits](https://github.com/alecthomas/chroma/compare/v2.13.0...v2.14.0)

Updates `github.com/go-pkgz/notify` from 1.1.1 to 1.2.0
- [Release notes](https://github.com/go-pkgz/notify/releases)
- [Commits](https://github.com/go-pkgz/notify/compare/v1.1.1...v1.2.0)

Updates `github.com/go-pkgz/repeater` from 1.1.3 to 1.2.0
- [Release notes](https://github.com/go-pkgz/repeater/releases)
- [Commits](https://github.com/go-pkgz/repeater/compare/v1.1.3...v1.2.0)

Updates `github.com/rs/xid` from 1.5.0 to 1.6.0
- [Release notes](https://github.com/rs/xid/releases)
- [Commits](https://github.com/rs/xid/compare/v1.5.0...v1.6.0)

Updates `go.etcd.io/bbolt` from 1.3.10 to 1.3.11
- [Release notes](https://github.com/etcd-io/bbolt/releases)
- [Commits](https://github.com/etcd-io/bbolt/compare/v1.3.10...v1.3.11)

Updates `golang.org/x/crypto` from 0.25.0 to 0.27.0
- [Commits](https://github.com/golang/crypto/compare/v0.25.0...v0.27.0)

Updates `golang.org/x/image` from 0.18.0 to 0.22.0
- [Commits](https://github.com/golang/image/compare/v0.18.0...v0.22.0)

Updates `golang.org/x/net` from 0.27.0 to 0.29.0
- [Commits](https://github.com/golang/net/compare/v0.27.0...v0.29.0)

---
updated-dependencies:
- dependency-name: github.com/PuerkitoBio/goquery
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules-updates
- dependency-name: github.com/alecthomas/chroma/v2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules-updates
- dependency-name: github.com/go-pkgz/notify
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules-updates
- dependency-name: github.com/go-pkgz/repeater
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules-updates
- dependency-name: github.com/rs/xid
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules-updates
- dependency-name: go.etcd.io/bbolt
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: go-modules-updates
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules-updates
- dependency-name: golang.org/x/image
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules-updates
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-08 16:19:58 -06:00
Dmitry VerkhoturovandUmputun c1a447b873 unpin bugfix js version, use codecov github action 2024-11-08 15:56:59 -06:00
dependabot[bot]andUmputun 3c110eb0ec Bump the github-actions-updates group with 5 updates
Bumps the github-actions-updates group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) | `3` | `6` |
| [pnpm/action-setup](https://github.com/pnpm/action-setup) | `2.4.0` | `4.0.0` |
| [actions/cache](https://github.com/actions/cache) | `3` | `4` |
| [andresz1/size-limit-action](https://github.com/andresz1/size-limit-action) | `7313b26c76b3666c1dc41e2ca05370e201a9b7de` | `94bc357df29c36c8f8d50ea497c3e225c3c95d1d` |
| [actions/upload-artifact](https://github.com/actions/upload-artifact) | `2` | `4` |


Updates `golangci/golangci-lint-action` from 3 to 6
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v3...v6)

Updates `pnpm/action-setup` from 2.4.0 to 4.0.0
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/v2.4.0...v4.0.0)

Updates `actions/cache` from 3 to 4
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3...v4)

Updates `andresz1/size-limit-action` from 7313b26c76b3666c1dc41e2ca05370e201a9b7de to 94bc357df29c36c8f8d50ea497c3e225c3c95d1d
- [Release notes](https://github.com/andresz1/size-limit-action/releases)
- [Commits](https://github.com/andresz1/size-limit-action/compare/7313b26c76b3666c1dc41e2ca05370e201a9b7de...94bc357df29c36c8f8d50ea497c3e225c3c95d1d)

Updates `actions/upload-artifact` from 2 to 4
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v2...v4)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions-updates
- dependency-name: pnpm/action-setup
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions-updates
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions-updates
- dependency-name: andresz1/size-limit-action
  dependency-type: direct:production
  dependency-group: github-actions-updates
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-08 15:56:59 -06:00
Dmitry VerkhoturovandUmputun d7494d5392 add dependabot updates for Go, TypeScript, GitHub Actions 2024-11-07 19:13:55 -06:00
Mrigank Badola cff261c15b chore: add discord in server test file 2024-10-20 11:47:47 +05:30
Mrigank Badola 18ea40582a sync: Merge remote-tracking branch 'origin' into discord-oauth 2024-10-20 11:46:33 +05:30
Mrigank Badola e5ae07b1c2 chore: add cid and csec in readmes and backend docker compose yml 2024-10-09 09:45:10 +05:30
Mrigank Badola 4fbb3b59be chore: add changes in accordance to paskal/discord_poc 2024-10-09 08:57:47 +05:30
1040 changed files with 67687 additions and 17904 deletions
+72
View File
@@ -0,0 +1,72 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
groups:
"GitHub Actions updates":
patterns:
- "*"
- package-ecosystem: "gomod"
directory: "/backend"
schedule:
interval: "monthly"
groups:
"Go modules updates":
dependency-type: "production"
- package-ecosystem: "npm"
directory: "/frontend"
open-pull-requests-limit: 0
schedule:
interval: "monthly"
groups:
"NPM modules updates":
dependency-type: "production"
"NPM modules updates for tests":
dependency-type: "development"
- package-ecosystem: "npm"
directory: "/frontend/packages/api"
open-pull-requests-limit: 0
schedule:
interval: "monthly"
groups:
"NPM modules updates":
dependency-type: "production"
"NPM modules updates for tests":
dependency-type: "development"
- package-ecosystem: "npm"
directory: "/frontend/e2e"
open-pull-requests-limit: 0
schedule:
interval: "monthly"
groups:
"NPM modules updates":
dependency-type: "production"
"NPM modules updates for tests":
dependency-type: "development"
- package-ecosystem: "npm"
directory: "/frontend/apps/remark42"
open-pull-requests-limit: 0
schedule:
interval: "monthly"
groups:
"NPM modules updates":
dependency-type: "production"
"NPM modules updates for tests":
dependency-type: "development"
- package-ecosystem: "npm"
directory: "/site"
open-pull-requests-limit: 0
schedule:
interval: "monthly"
groups:
"NPM modules updates":
dependency-type: "production"
"NPM modules updates for tests":
dependency-type: "development"
+15 -9
View File
@@ -5,24 +5,29 @@ on:
branches:
tags:
paths:
- ".github/workflows/ci-test-backend.yml"
- ".github/workflows/ci-backend.yml"
- "backend/**"
- "!backend/scripts/**"
- "!**.md"
pull_request:
types: [opened, reopened]
paths:
- ".github/workflows/ci-test-backend.yml"
- ".github/workflows/ci-backend.yml"
- "backend/**"
- "!backend/scripts/**"
- "!**.md"
jobs:
test:
name: Test & Coverage
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: debug if needed
run: if [[ "$DEBUG" == "true" ]]; then env; fi
@@ -30,9 +35,10 @@ jobs:
DEBUG: ${{secrets.DEBUG}}
- name: install go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: "1.21"
go-version: "1.25"
cache-dependency-path: backend
- name: test and build backend
run: |
@@ -52,15 +58,15 @@ jobs:
TZ: "America/Chicago"
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
uses: golangci/golangci-lint-action@v9
with:
version: v1.58
version: "v2.6.0"
working-directory: backend/app
- name: golangci-lint on example directory
uses: golangci/golangci-lint-action@v3
uses: golangci/golangci-lint-action@v9
with:
version: v1.58
version: "v2.6.0"
args: --config ../../.golangci.yml
working-directory: backend/_example/memory_store
+34 -69
View File
@@ -1,18 +1,6 @@
name: build
on:
push:
branches:
tags:
paths:
- ".github/workflows/ci-build.yml"
- "backend/**"
- "frontend/apps/**"
- ".dockerignore"
- "docker-init.sh"
- "Dockerfile"
- "!**.md"
- "!frontend/packages/**"
pull_request:
paths:
- ".github/workflows/ci-build.yml"
@@ -23,79 +11,56 @@ on:
- "Dockerfile"
- "!**.md"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build-images:
name: Build Docker images
name: Validate Docker build
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- name: set up QEMU
uses: docker/setup-qemu-action@v3
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
- name: available platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- name: expose GitHub Actions cache
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: build docker image without pushing (only outside master)
if: ${{ github.ref != 'refs/heads/master' }}
- name: free disk space
run: |
docker buildx build \
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
sudo rm -rf /usr/local/share/boost
docker system prune -af
- name: build docker image without pushing
run: |
docker buildx build --load \
--cache-from type=local,src=/tmp/.buildx-cache \
--cache-to type=local,dest=/tmp/.buildx-cache-new,mode=max \
--build-arg SKIP_BACKEND_TEST=true --build-arg SKIP_FRONTEND_TEST=true \
--platform linux/amd64 .
- name: build example docker image without pushing (only outside master)
if: ${{ github.ref != 'refs/heads/master' }}
- name: build example docker image without pushing
run: |
docker buildx build \
docker buildx build --load \
--cache-from type=local,src=/tmp/.buildx-cache \
--build-arg SKIP_BACKEND_TEST=true --build-arg SKIP_FRONTEND_TEST=true \
--platform linux/amd64 -f backend/_example/memory_store/Dockerfile .
- name: build and deploy master image to ghcr.io and dockerhub
if: ${{ github.ref == 'refs/heads/master' }}
env:
GITHUB_PACKAGE_TOKEN: ${{ secrets.PKG_TOKEN }}
DOCKER_HUB_TOKEN: ${{ secrets.DOCKER_HUB_TOKEN }}
USERNAME: ${{ github.actor }}
GITHUB_SHA: ${{ github.sha}}
GITHUB_REF: ${{ github.ref}}
- name: rotate cache
run: |
ref="$(echo ${GITHUB_REF} | cut -d'/' -f3)"
echo "GITHUB_REF=${GITHUB_REF}, GITHUB_SHA=${GITHUB_SHA}, GIT_BRANCH=${ref}"
echo ${GITHUB_PACKAGE_TOKEN} | docker login ghcr.io -u ${USERNAME} --password-stdin
echo ${DOCKER_HUB_TOKEN} | docker login -u umputun --password-stdin
docker buildx build --push \
--build-arg SKIP_BACKEND_TEST=true --build-arg SKIP_FRONTEND_TEST=true --build-arg CI=github \
--build-arg GITHUB_SHA=${GITHUB_SHA} --build-arg GIT_BRANCH=${ref} --build-arg GITHUB_REF=${GITHUB_REF} \
--platform linux/amd64,linux/arm/v7,linux/arm64 \
-t ghcr.io/umputun/remark42:${ref} -t umputun/remark42:${ref} .
- name: deploy tagged (latest) to ghcr.io and dockerhub
if: ${{ startsWith(github.ref, 'refs/tags/') }}
env:
GITHUB_PACKAGE_TOKEN: ${{ secrets.PKG_TOKEN }}
DOCKER_HUB_TOKEN: ${{ secrets.DOCKER_HUB_TOKEN }}
USERNAME: ${{ github.actor }}
GITHUB_SHA: ${{ github.sha}}
GITHUB_REF: ${{ github.ref}}
run: |
ref="$(echo ${GITHUB_REF} | cut -d'/' -f3)"
echo "GITHUB_REF=${GITHUB_REF}, GITHUB_SHA=${GITHUB_SHA}, GIT_BRANCH=${ref}"
echo ${GITHUB_PACKAGE_TOKEN} | docker login ghcr.io -u ${USERNAME} --password-stdin
echo ${DOCKER_HUB_TOKEN} | docker login -u umputun --password-stdin
docker buildx build --push \
--build-arg SKIP_BACKEND_TEST=true --build-arg SKIP_FRONTEND_TEST=true --build-arg CI=github \
--build-arg GITHUB_SHA=${GITHUB_SHA} --build-arg GIT_BRANCH=${ref} --build-arg GITHUB_REF=${GITHUB_REF} \
--platform linux/amd64,linux/arm/v7,linux/arm64 \
-t ghcr.io/umputun/remark42:${ref} -t ghcr.io/umputun/remark42:latest \
-t umputun/remark42:${ref} -t umputun/remark42:latest .
- name: remote deployment to remark42.com from master
if: ${{ github.ref == 'refs/heads/master' }}
env:
UPDATER_KEY: ${{ secrets.UPDATER_KEY }}
run: curl -s https://jess.umputun.com/update/remark42-core/${UPDATER_KEY}
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache || true
+42 -31
View File
@@ -18,35 +18,38 @@ jobs:
type-check:
name: Type check
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
matrix:
node: [16.20.2]
node: [ 16 ]
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Install node
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
- name: Install pnpm
uses: pnpm/action-setup@v2.4.0
uses: pnpm/action-setup@v4.2.0
id: pnpm-install
with:
version: 7
version: 8
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
run: echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
path: ${{ env.pnpm_cache_dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
@@ -62,35 +65,38 @@ jobs:
lint:
name: Lint
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
matrix:
node: [16.20.2]
node: [ 16 ]
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Install node
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
- name: Install pnpm
uses: pnpm/action-setup@v2.4.0
uses: pnpm/action-setup@v4.2.0
id: pnpm-install
with:
version: 7
version: 8
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
run: echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
path: ${{ env.pnpm_cache_dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
@@ -106,35 +112,38 @@ jobs:
test:
name: Tests & Coverage
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
matrix:
node: [16.20.2]
node: [ 16 ]
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Install node
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
- name: Install pnpm
uses: pnpm/action-setup@v2.4.0
uses: pnpm/action-setup@v4.2.0
id: pnpm-install
with:
version: 7
version: 8
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
run: echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
path: ${{ env.pnpm_cache_dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
@@ -147,7 +156,9 @@ jobs:
run: pnpm coverage:api
working-directory: ./frontend
- name: Submit coverage
run: ${{ github.workspace }}/frontend/apps/remark42/node_modules/.bin/codecov
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
working-directory: ./frontend
codecov_yml_path: ./frontend/apps/remark42/codecov.yml
+63 -44
View File
@@ -18,35 +18,38 @@ jobs:
translations-check:
name: Translations check
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
matrix:
node: [16.20.2]
node: [16]
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Install node
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
- name: Install pnpm
uses: pnpm/action-setup@v2.4.0
uses: pnpm/action-setup@v4.2.0
id: pnpm-install
with:
version: 7
version: 8
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
run: echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
path: ${{ env.pnpm_cache_dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
@@ -62,35 +65,38 @@ jobs:
type-check:
name: Type check
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
matrix:
node: [16.20.2]
node: [16]
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Install node
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
- name: Install pnpm
uses: pnpm/action-setup@v2.4.0
uses: pnpm/action-setup@v4.2.0
id: pnpm-install
with:
version: 7
version: 8
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
run: echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
path: ${{ env.pnpm_cache_dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
@@ -106,35 +112,38 @@ jobs:
lint:
name: Eslint & Stylelint
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
matrix:
node: [16.20.2]
node: [16]
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Install node
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
- name: Install pnpm
uses: pnpm/action-setup@v2.4.0
uses: pnpm/action-setup@v4.2.0
id: pnpm-install
with:
version: 7
version: 8
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
run: echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
path: ${{ env.pnpm_cache_dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
@@ -151,21 +160,26 @@ jobs:
name: Size limit
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
permissions:
contents: read
pull-requests: write
env:
CI_JOB_NUMBER: 1
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Install pnpm
uses: pnpm/action-setup@v2.4.0
uses: pnpm/action-setup@v4.2.0
id: pnpm-install
with:
version: 7
version: 8
run_install: false
- name: Check bundle size
uses: andresz1/size-limit-action@7313b26c76b3666c1dc41e2ca05370e201a9b7de
uses: andresz1/size-limit-action@94bc357df29c36c8f8d50ea497c3e225c3c95d1d
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
directory: ./frontend/apps/remark42
@@ -174,35 +188,38 @@ jobs:
test:
name: Tests & Coverage
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
matrix:
node: [16.20.2]
node: [16]
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Install node
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
- name: Install pnpm
uses: pnpm/action-setup@v2.4.0
uses: pnpm/action-setup@v4.2.0
id: pnpm-install
with:
version: 7
version: 8
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
run: echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
path: ${{ env.pnpm_cache_dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
@@ -215,7 +232,9 @@ jobs:
run: pnpm coverage
working-directory: ./frontend/apps/remark42
- name: Submit coverage
run: ${{ github.workspace }}/frontend/apps/remark42/node_modules/.bin/codecov
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
working-directory: ./frontend/apps/remark42
codecov_yml_path: ./frontend/apps/remark42/codecov.yml
+110 -37
View File
@@ -13,57 +13,130 @@ on:
- ".github/workflows/ci-site.yml"
- "site/**"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
name: Build
runs-on: ubuntu-latest
name: Build site image (${{ matrix.platform }})
if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/')
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
artifact: linux-amd64
- platform: linux/arm64
runner: ubuntu-24.04-arm
artifact: linux-arm64
steps:
- name: checkout
uses: actions/checkout@v4
- name: set up QEMU
uses: docker/setup-qemu-action@v3
uses: actions/checkout@v6
with:
persist-credentials: false
- name: set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
- name: available platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- name: login to ghcr.io
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.PKG_TOKEN }}
- name: build and deploy master image to ghcr.io and dockerhub
if: ${{ github.ref == 'refs/heads/master' }}
- name: build and push by digest
id: build
uses: docker/build-push-action@v6
with:
context: ./site
platforms: ${{ matrix.platform }}
cache-from: type=gha,scope=site-${{ matrix.platform }}
cache-to: type=gha,scope=site-${{ matrix.platform }},mode=max
outputs: type=image,name=ghcr.io/umputun/remark42-site,push-by-digest=true,name-canonical=true,push=true
- name: export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: upload digest
uses: actions/upload-artifact@v5
with:
name: site-digests-${{ matrix.artifact }}
path: /tmp/digests/*
retention-days: 1
merge:
name: Create site multi-arch manifest
runs-on: ubuntu-latest
needs: build
permissions:
contents: read
packages: write
steps:
- name: download digests
uses: actions/download-artifact@v6
with:
path: /tmp/digests
pattern: site-digests-*
merge-multiple: true
- name: verify all digests present
run: |
expected=2
actual=$(find /tmp/digests -maxdepth 1 -type f | wc -l)
if [ "$actual" -ne "$expected" ]; then
echo "Expected $expected digests, found $actual"
ls -la /tmp/digests
exit 1
fi
echo "All $expected digests present"
- name: set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: login to ghcr.io
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.PKG_TOKEN }}
- name: create manifest and push
working-directory: /tmp/digests
env:
GITHUB_PACKAGE_TOKEN: ${{ secrets.PKG_TOKEN }}
USERNAME: ${{ github.actor }}
GITHUB_SHA: ${{ github.sha}}
GITHUB_REF: ${{ github.ref}}
working-directory: ./site
GITHUB_REF: ${{ github.ref }}
run: |
ref="$(echo ${GITHUB_REF} | cut -d'/' -f3)"
echo GITHUB_REF - $ref
echo ${GITHUB_PACKAGE_TOKEN} | docker login ghcr.io -u ${USERNAME} --password-stdin
docker buildx build --push --no-cache --platform linux/amd64,linux/arm/v7,linux/arm64 \
-t ghcr.io/umputun/remark42-site:${ref} .
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
docker buildx imagetools create \
-t ghcr.io/umputun/remark42-site:${ref} \
-t ghcr.io/umputun/remark42-site:latest \
$(printf 'ghcr.io/umputun/remark42-site@sha256:%s ' *)
else
docker buildx imagetools create \
-t ghcr.io/umputun/remark42-site:${ref} \
$(printf 'ghcr.io/umputun/remark42-site@sha256:%s ' *)
fi
- name: deploy tagged (latest) to ghcr.io and dockerhub
if: ${{ startsWith(github.ref, 'refs/tags/') }}
env:
GITHUB_PACKAGE_TOKEN: ${{ secrets.PKG_TOKEN }}
USERNAME: ${{ github.actor }}
GITHUB_SHA: ${{ github.sha}}
GITHUB_REF: ${{ github.ref}}
working-directory: ./site
run: |
ref="$(echo ${GITHUB_REF} | cut -d'/' -f3)"
echo "GITHUB_REF=$ref, GITHUB_SHA=${GITHUB_SHA}"
echo ${GITHUB_PACKAGE_TOKEN} | docker login ghcr.io -u ${USERNAME} --password-stdin
docker buildx build --push --no-cache --platform linux/amd64,linux/arm/v7,linux/arm64 \
-t ghcr.io/umputun/remark42-site:${ref} -t ghcr.io/umputun/remark42-site:latest .
deploy:
name: Deploy site
runs-on: ubuntu-latest
needs: merge
if: github.ref == 'refs/heads/master'
- name: remote site deployment from master
if: ${{ github.ref == 'refs/heads/master' }}
steps:
- name: trigger deployment
env:
UPDATER_KEY: ${{ secrets.UPDATER_KEY }}
run: curl https://jess.umputun.com/update/remark42-site/${UPDATER_KEY}
run: curl -sf https://jess.umputun.com/update/remark42-site/${UPDATER_KEY}
+214
View File
@@ -0,0 +1,214 @@
name: docker
on:
workflow_run:
workflows: [backend]
types: [completed]
concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: true
jobs:
build:
name: Build Docker image (${{ matrix.platform }})
if: >-
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event != 'pull_request' &&
(github.event.workflow_run.head_branch == 'master' ||
startsWith(github.event.workflow_run.head_branch, 'v'))
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
artifact: linux-amd64
- platform: linux/arm64
runner: ubuntu-24.04-arm
artifact: linux-arm64
runs-on: ${{ matrix.runner }}
steps:
- name: checkout
uses: actions/checkout@v6
with:
ref: ${{ github.event.workflow_run.head_sha }}
persist-credentials: false
- name: set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: login to ghcr.io
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.PKG_TOKEN }}
- name: login to DockerHub
uses: docker/login-action@v3
with:
username: umputun
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: free disk space
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
sudo rm -rf /usr/local/share/boost
docker system prune -af
- name: build and push to ghcr.io by digest
id: build-ghcr
uses: docker/build-push-action@v6
with:
context: .
platforms: ${{ matrix.platform }}
cache-from: type=gha,scope=${{ matrix.platform }}
cache-to: type=gha,scope=${{ matrix.platform }},mode=max
build-args: |
SKIP_BACKEND_TEST=true
SKIP_FRONTEND_TEST=true
CI=github
GITHUB_SHA=${{ github.event.workflow_run.head_sha }}
GIT_BRANCH=${{ github.event.workflow_run.head_branch }}
GITHUB_REF=refs/heads/${{ github.event.workflow_run.head_branch }}
outputs: type=image,name=ghcr.io/umputun/remark42,push-by-digest=true,name-canonical=true,push=true
- name: build and push to DockerHub by digest
id: build-dockerhub
uses: docker/build-push-action@v6
with:
context: .
platforms: ${{ matrix.platform }}
cache-from: type=gha,scope=${{ matrix.platform }}
build-args: |
SKIP_BACKEND_TEST=true
SKIP_FRONTEND_TEST=true
CI=github
GITHUB_SHA=${{ github.event.workflow_run.head_sha }}
GIT_BRANCH=${{ github.event.workflow_run.head_branch }}
GITHUB_REF=refs/heads/${{ github.event.workflow_run.head_branch }}
outputs: type=image,name=umputun/remark42,push-by-digest=true,name-canonical=true,push=true
- name: export digests
run: |
mkdir -p /tmp/digests/ghcr /tmp/digests/dockerhub
digest_ghcr="${{ steps.build-ghcr.outputs.digest }}"
digest_dockerhub="${{ steps.build-dockerhub.outputs.digest }}"
touch "/tmp/digests/ghcr/${digest_ghcr#sha256:}"
touch "/tmp/digests/dockerhub/${digest_dockerhub#sha256:}"
- name: upload ghcr digest
uses: actions/upload-artifact@v5
with:
name: digests-ghcr-${{ matrix.artifact }}
path: /tmp/digests/ghcr/*
retention-days: 1
- name: upload dockerhub digest
uses: actions/upload-artifact@v5
with:
name: digests-dockerhub-${{ matrix.artifact }}
path: /tmp/digests/dockerhub/*
retention-days: 1
merge:
name: Create multi-arch manifest
runs-on: ubuntu-latest
needs: build
permissions:
contents: read
packages: write
steps:
- name: download ghcr digests
uses: actions/download-artifact@v6
with:
path: /tmp/digests/ghcr
pattern: digests-ghcr-*
merge-multiple: true
- name: download dockerhub digests
uses: actions/download-artifact@v6
with:
path: /tmp/digests/dockerhub
pattern: digests-dockerhub-*
merge-multiple: true
- name: verify all digests present
run: |
expected=2
for registry in ghcr dockerhub; do
actual=$(find /tmp/digests/$registry -maxdepth 1 -type f | wc -l)
if [ "$actual" -ne "$expected" ]; then
echo "Expected $expected digests for $registry, found $actual"
ls -la /tmp/digests/$registry
exit 1
fi
done
echo "All digests present for both registries"
- name: set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: login to ghcr.io
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.PKG_TOKEN }}
- name: login to DockerHub
uses: docker/login-action@v3
with:
username: umputun
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: create ghcr.io manifest and push
working-directory: /tmp/digests/ghcr
env:
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
run: |
if [[ "$HEAD_BRANCH" == v* ]]; then
docker buildx imagetools create \
-t ghcr.io/umputun/remark42:${HEAD_BRANCH} \
-t ghcr.io/umputun/remark42:latest \
$(printf 'ghcr.io/umputun/remark42@sha256:%s ' *)
else
docker buildx imagetools create \
-t ghcr.io/umputun/remark42:${HEAD_BRANCH} \
$(printf 'ghcr.io/umputun/remark42@sha256:%s ' *)
fi
- name: create DockerHub manifest and push
working-directory: /tmp/digests/dockerhub
env:
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
run: |
if [[ "$HEAD_BRANCH" == v* ]]; then
docker buildx imagetools create \
-t umputun/remark42:${HEAD_BRANCH} \
-t umputun/remark42:latest \
$(printf 'umputun/remark42@sha256:%s ' *)
else
docker buildx imagetools create \
-t umputun/remark42:${HEAD_BRANCH} \
$(printf 'umputun/remark42@sha256:%s ' *)
fi
deploy:
name: Deploy to remark42.com
runs-on: ubuntu-latest
needs: merge
if: github.event.workflow_run.head_branch == 'master'
steps:
- name: trigger deployment
env:
UPDATER_KEY: ${{ secrets.UPDATER_KEY }}
run: curl -sf https://jess.umputun.com/update/remark42-core/${UPDATER_KEY}
+8 -2
View File
@@ -4,6 +4,7 @@ on:
push:
branches: [master]
paths:
- ".github/workflows/e2e-tests.yml"
- "frontend/apps/remark42/**"
- "frontend/e2e/**"
- "frontend/Dockerfile.e2e"
@@ -11,6 +12,7 @@ on:
pull_request:
branches: [master]
paths:
- ".github/workflows/e2e-tests.yml"
- "frontend/apps/remark42/**"
- "frontend/e2e/**"
- "frontend/Dockerfile.e2e"
@@ -20,15 +22,19 @@ jobs:
name: Tests
timeout-minutes: 60
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Build & run containers
id: tests
run: COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker compose -f compose-e2e-test.yml up --build --quiet-pull --exit-code-from tests
- uses: actions/upload-artifact@v2
- uses: actions/upload-artifact@v5
if: always()
with:
name: playwright-report
+36
View File
@@ -0,0 +1,36 @@
# Remark42 Development Guidelines
## Build/Test/Lint Commands
- **Backend**:
- Run server: `make rundev`
- Build: `make backend`
- Race test: `make race_test`
- **Backend Testing**:
- Run all tests: `cd backend/app && go test -timeout=60s -count 1 ./...`
- Run single test: `cd backend/app && go test -run TestName ./path/to/package`
- **IMPORTANT**: Run example tests: `cd backend/_example/memory_store && go test -race ./... && go build -race ./...`
- **Frontend**:
- Development: `cd frontend && pnpm dev:app`
- Tests: `cd frontend && pnpm test`
- **Lint**:
- Backend: `cd backend && golangci-lint run`
- **IMPORTANT**: Example lint: `cd backend/_example/memory_store && golangci-lint run --config ../../.golangci.yml`
- Frontend: `cd frontend && pnpm lint`
- **Before committing**: Always run tests and linter on both main backend AND examples
## Code Style
- **Backend**: Formatting with golangci-lint, strict error handling
- **Frontend**: TypeScript with ESLint, Stylelint and Prettier
- **Imports**: Group stdlib, external packages, then internal packages
- **CSS**: CSS Modules for new components (`component.module.css`)
## Key Backend Packages
- **Web/API**: `github.com/go-chi/chi/v5`, `github.com/go-pkgz/rest`
- **Auth**: `github.com/go-pkgz/auth/v2`
- **Logging**: `github.com/go-pkgz/lgr`
- **Testing**: `github.com/stretchr/testify`
- **Notifications**: `github.com/go-pkgz/notify`
## Repository Structure
- Backend: Go server using BoltDB for storage
- Frontend: Preact/Redux-based UI with iframe embedding
+3 -3
View File
@@ -11,7 +11,7 @@ COPY ./frontend/apps/remark42/package.json /srv/frontend/apps/remark42/
RUN \
if [[ -z "$SKIP_FRONTEND_BUILD" || -z "$SKIP_FRONTEND_TEST" ]]; then \
apk add --no-cache --update git && \
npm i -g pnpm@7; \
npm i -g pnpm@8; \
fi
RUN --mount=type=cache,id=pnpm,target=/root/.pnpm-store/v3 \
@@ -45,7 +45,7 @@ RUN \
echo 'Skip frontend build'; \
fi
FROM umputun/baseimage:buildgo-v1.13.0 as build-backend
FROM umputun/baseimage:buildgo-v1.17.0 AS build-backend
ARG CI
ARG GITHUB_REF
@@ -81,7 +81,7 @@ RUN \
echo "version=$version" && \
go build -o remark42 -ldflags "-X main.revision=${version} -s -w" ./app
FROM umputun/baseimage:app-v1.13.0
FROM umputun/baseimage:app-v1.17.0
ARG GITHUB_SHA
+3 -3
View File
@@ -1,4 +1,4 @@
FROM node:16.20.2-alpine AS frontend-deps
FROM node:16-alpine AS frontend-deps
ENV CI=true
@@ -7,7 +7,7 @@ WORKDIR /srv/frontend
COPY ./frontend/package.json ./frontend/pnpm-lock.yaml ./frontend/pnpm-workspace.yaml /srv/frontend/
COPY ./frontend/apps/remark42/package.json /srv/frontend/apps/remark42/package.json
RUN apk add --no-cache --update git && npm i -g pnpm@7
RUN apk add --no-cache --update git && npm i -g pnpm@8
RUN --mount=type=cache,id=pnpm,target=/root/.pnpm-store/v3 pnpm i
FROM frontend-deps AS build-frontend
@@ -19,7 +19,7 @@ WORKDIR /srv/frontend/apps/remark42/
COPY ./frontend/apps/remark42/ /srv/frontend/apps/remark42/
RUN pnpm build
FROM umputun/baseimage:buildgo-v1.13.0 as build-backend
FROM umputun/baseimage:buildgo-v1.14.0 AS build-backend
ARG GITHUB_TOKEN
ARG GITHUB_REF
+1 -1
View File
@@ -11,7 +11,7 @@ bin:
docker rm -f remark42.bin
docker:
DOCKER_BUILDKIT=1 docker build -t umputun/remark42 --build-arg GITHUB_REF=$(GITHUB_REF) --build-arg GITHUB_SHA=$(GITHUB_SHA) \
DOCKER_BUILDKIT=1 docker build -t umputun/remark42 -t ghcr.io/umputun/remark42 --build-arg GITHUB_REF=$(GITHUB_REF) --build-arg GITHUB_SHA=$(GITHUB_SHA) \
--build-arg CI=true --build-arg SKIP_FRONTEND_TEST=true --build-arg SKIP_BACKEND_TEST=true .
dockerx:
+1 -1
View File
@@ -2,7 +2,7 @@
Remark42 is a self-hosted, lightweight and simple (yet functional) comment engine, which doesn't spy on users. It can be embedded into blogs, articles, or any other place where readers add comments.
* Social login via Google, Facebook, Microsoft, GitHub, Apple, Yandex, Patreon and Telegram
* Social login via Google, Facebook, Microsoft, GitHub, Apple, Yandex, Patreon, Discord and Telegram
* Login via email
* Optional anonymous access
* Multi-level nested comments with both tree and plain presentations
+51 -47
View File
@@ -1,38 +1,14 @@
run:
timeout: 5m
linters-settings:
govet:
enable:
- shadow
goconst:
min-len: 2
min-occurrences: 2
misspell:
locale: US
lll:
line-length: 140
gocritic:
enabled-tags:
- performance
- style
- experimental
disabled-checks:
- wrapperFunc
# TODO: feel free to remove these excludes and fix the code
- hugeParam
- rangeValCopy
version: "2"
linters:
default: none
enable:
- bodyclose
- copyloopvar
- dupl
- exportloopref
- gochecknoinits
- gocritic
- gocyclo
- gosec
- gosimple
- govet
- ineffassign
- misspell
@@ -40,26 +16,54 @@ linters:
- prealloc
- revive
- staticcheck
- stylecheck
- typecheck
- unconvert
- unparam
- unused
fast: false
disable-all: true
issues:
exclude-dirs:
- vendor
exclude-rules:
- text: "at least one file in a package should have a package comment"
linters:
- stylecheck
- text: "package-comments: should have a package comment"
linters:
- revive
- path: _test\.go
linters:
- gosec
- dupl
exclude-use-default: false
settings:
goconst:
min-len: 2
min-occurrences: 2
gocritic:
disabled-checks:
- wrapperFunc
- hugeParam
- rangeValCopy
enabled-tags:
- performance
- style
- experimental
govet:
enable:
- shadow
lll:
line-length: 140
misspell:
locale: US
exclusions:
generated: lax
rules:
- linters:
- staticcheck
text: at least one file in a package should have a package comment
- linters:
- revive
text: 'package-comments: should have a package comment'
- linters:
- revive
text: 'var-naming: avoid meaningless package names'
- linters:
- dupl
- gosec
path: _test\.go
paths:
- vendor
- third_party$
- builtin$
- examples$
formatters:
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
+2 -2
View File
@@ -1,11 +1,11 @@
FROM umputun/baseimage:buildgo-v1.13.0 as build-backend
FROM umputun/baseimage:buildgo-v1.17.0 AS build-backend
ADD backend /build/backend
WORKDIR /build/backend/_example/memory_store
RUN go build -o /build/bin/memory_store -ldflags "-X main.revision=0.0.0 -s -w"
FROM umputun/baseimage:app-v1.13.0
FROM umputun/baseimage:app-v1.17.0
ARG GITHUB_SHA
@@ -391,10 +391,7 @@ func (m *MemData) checkFlag(req engine.FlagRequest) (val bool) {
func (m *MemData) setFlag(req engine.FlagRequest) (res bool, err error) {
status := false
if req.Update == engine.FlagTrue {
status = true
}
status := req.Update == engine.FlagTrue
switch req.Flag {
@@ -286,7 +286,7 @@ func TestMemData_CountUser(t *testing.T) {
func TestMemData_InfoPost(t *testing.T) {
b := prepMem(t)
ts := func(min int) time.Time { return time.Date(2017, 12, 20, 15, 18, min, 0, time.Local).In(time.UTC) }
ts := func(minute int) time.Time { return time.Date(2017, 12, 20, 15, 18, minute, 0, time.Local).In(time.UTC) }
// add one more for https://radio-t.com/2
comment := store.Comment{
@@ -11,7 +11,7 @@ services:
args:
- SKIP_BACKEND_TEST=true
- SKIP_FRONTEND_TEST=true
image: umputun/remark42:dev
image: ghcr.io/umputun/remark42:dev
container_name: "remark42-dev"
hostname: "remark42-dev"
restart: always
+18 -21
View File
@@ -1,42 +1,39 @@
module github.com/umputun/remark42/memory_store
go 1.21
go 1.25
require (
github.com/go-pkgz/jrpc v0.3.0
github.com/go-pkgz/lgr v0.11.1
github.com/go-pkgz/jrpc v0.4.0
github.com/go-pkgz/lgr v0.12.1
github.com/jessevdk/go-flags v1.6.1
github.com/stretchr/testify v1.9.0
github.com/stretchr/testify v1.11.1
github.com/umputun/remark42/backend v1.1000.0
)
require (
github.com/Depado/bfchroma/v2 v2.0.0 // indirect
github.com/PuerkitoBio/goquery v1.9.2 // indirect
github.com/ajg/form v1.5.1 // indirect
github.com/alecthomas/chroma/v2 v2.13.0 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/PuerkitoBio/goquery v1.11.0 // indirect
github.com/alecthomas/chroma/v2 v2.21.1 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/didip/tollbooth/v7 v7.0.2 // indirect
github.com/didip/tollbooth_chi v0.0.0-20220719025231-d662a7f6928f // indirect
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/go-chi/chi/v5 v5.1.0 // indirect
github.com/go-chi/render v1.0.3 // indirect
github.com/go-pkgz/expirable-cache/v3 v3.0.0 // indirect
github.com/go-pkgz/rest v1.19.0 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/go-pkgz/rest v1.20.6 // indirect
github.com/go-pkgz/routegroup v1.6.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rs/xid v1.5.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
go.etcd.io/bbolt v1.3.10 // indirect
golang.org/x/image v0.18.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/sys v0.22.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
go.etcd.io/bbolt v1.4.3 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/image v0.34.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sys v0.39.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+78 -69
View File
@@ -1,45 +1,31 @@
github.com/Depado/bfchroma/v2 v2.0.0 h1:IRpN9BPkNwEpR6w1ectIcNWOuhDSLx+8f1pn83fzxx8=
github.com/Depado/bfchroma/v2 v2.0.0/go.mod h1:wFwW/Pw8Tnd0irzgO9Zxtxgzp3aPS8qBWlyadxujxmw=
github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE=
github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU=
github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.13.0 h1:VP72+99Fb2zEcYM0MeaWJmV+xQvz5v5cxRHd+ooU1lI=
github.com/alecthomas/chroma/v2 v2.13.0/go.mod h1:BUGjjsD+ndS6eX37YgTchSEG+Jg9Jv1GiZs9sqPqztk=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.21.1 h1:FaSDrp6N+3pphkNKU6HPCiYLgm8dbe5UXIXcoBhZSWA=
github.com/alecthomas/chroma/v2 v2.21.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/didip/tollbooth/v7 v7.0.0/go.mod h1:VZhDSGl5bDSPj4wPsih3PFa4Uh9Ghv8hgacaTm5PRT4=
github.com/didip/tollbooth/v7 v7.0.2 h1:WYEfusYI6g64cN0qbZgekDrYfuYBZjUZd5+RlWi69p4=
github.com/didip/tollbooth/v7 v7.0.2/go.mod h1:RtRYfEmFGX70+ike5kSndSvLtQ3+F2EAmTI4Un/VXNc=
github.com/didip/tollbooth_chi v0.0.0-20220719025231-d662a7f6928f h1:jtKwihcLmUC9BAhoJ9adCUqdSSZcOdH2KL7mPTUm2aw=
github.com/didip/tollbooth_chi v0.0.0-20220719025231-d662a7f6928f/go.mod h1:q9C80dnsuVRP2dAskjnXRNWdUJqtGgwG9wNrzt0019s=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-pkgz/expirable-cache v0.1.0/go.mod h1:GTrEl0X+q0mPNqN6dtcQXksACnzCBQ5k/k1SwXJsZKs=
github.com/go-pkgz/expirable-cache/v3 v3.0.0 h1:u3/gcu3sabLYiTCevoRKv+WzjIn5oo7P8XtiXBeRDLw=
github.com/go-pkgz/expirable-cache/v3 v3.0.0/go.mod h1:2OQiDyEGQalYecLWmXprm3maPXeVb5/6/X7yRPYTzec=
github.com/go-pkgz/jrpc v0.3.0 h1:Fls38KqPsHzvp0FWfivr6cGnncC+iFBodHBqvUPY+0U=
github.com/go-pkgz/jrpc v0.3.0/go.mod h1:MFtKs75JESiSqVicsQkgN2iDFFuCd3gVT1/vKiwRi00=
github.com/go-pkgz/lgr v0.11.1 h1:hXFhZcznehI6imLhEa379oMOKFz7TQUmisAqb3oLOSM=
github.com/go-pkgz/lgr v0.11.1/go.mod h1:tgDF4RXQnBfIgJqjgkv0yOeTQ3F1yewWIZkpUhHnAkU=
github.com/go-pkgz/rest v1.15.6/go.mod h1:KUWAqbDteYGS/CiXftomQsKjtEOifXsJ36Ka0skYbmk=
github.com/go-pkgz/rest v1.19.0 h1:FNMi5QX5dDIkuC+/e0r+CWsTuOTwUiWMRSA16Ou+9+A=
github.com/go-pkgz/rest v1.19.0/go.mod h1:Po+W6zQzpMPP6XDGLdAN2aW7UKk1IyrLSb48Lp1N3oQ=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/go-pkgz/jrpc v0.4.0 h1:oD7xiGrzDkndkuCjeHGugQXxbggLSV7O1QmHhoc5pYY=
github.com/go-pkgz/jrpc v0.4.0/go.mod h1:JFoY3bRjRyx4M3CbEVDFQStMB1m2gmQ7OjqFK7q3kOo=
github.com/go-pkgz/lgr v0.12.1 h1:8GVfG2rSARq3Eaj5PP158rtBR2LHVGkwioIkQBGbvKg=
github.com/go-pkgz/lgr v0.12.1/go.mod h1:A4AxjOthFVFK6jRnVYMeusno5SeDAxcLVHd0kI/lN/Y=
github.com/go-pkgz/rest v1.20.6 h1:O/IhQ3I2cS4bJYvL1TLcy63w2OcXZTTBG3R+wTIqPS4=
github.com/go-pkgz/rest v1.20.6/go.mod h1:NY+MX1is2kJckJt+nHDNovS/5j9jmF4yQuSno4qg7XU=
github.com/go-pkgz/routegroup v1.6.0 h1:44XHZgF6JIIldRlv+zjg6SygULASmjifnfIQjwCT0e4=
github.com/go-pkgz/routegroup v1.6.0/go.mod h1:Pmu04fhgWhRtBMIJ8HXppnnzOPjnL/IEPBIdO2zmeqg=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -47,81 +33,104 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4=
github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0=
go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8=
golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+1 -1
View File
@@ -7,7 +7,7 @@ import (
log "github.com/go-pkgz/lgr"
bolt "go.etcd.io/bbolt"
"github.com/go-pkgz/auth/avatar"
"github.com/go-pkgz/auth/v2/avatar"
)
// AvatarCommand set of flags and command for avatar migration
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"os"
"testing"
"github.com/go-pkgz/auth/avatar"
"github.com/go-pkgz/auth/v2/avatar"
"github.com/jessevdk/go-flags"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+1 -1
View File
@@ -77,7 +77,7 @@ func (cc *CleanupCommand) procSpam(comments []store.Comment) int {
log.Printf("[WARN] can't remove comment, %v", err)
}
}
comment.Text = strings.Replace(comment.Text, "\n", " ", -1)
comment.Text = strings.ReplaceAll(comment.Text, "\n", " ")
log.Printf("[SPAM] %+v [%.0f%%]", comment, score)
}
}
-1
View File
@@ -46,7 +46,6 @@ func TestCleanup_IsSpam(t *testing.T) {
}
for n, tt := range tbl {
tt := tt
checkName := fmt.Sprintf("check-%d-%s", n, tt.name)
t.Run(checkName, func(t *testing.T) {
c := store.Comment{ID: checkName, Text: tt.text, Score: tt.score}
+30 -18
View File
@@ -19,15 +19,15 @@ import (
"github.com/go-pkgz/lcw/v2/eventbus"
log "github.com/go-pkgz/lgr"
ntf "github.com/go-pkgz/notify"
"github.com/golang-jwt/jwt"
"github.com/golang-jwt/jwt/v5"
"github.com/kyokomi/emoji/v2"
bolt "go.etcd.io/bbolt"
"github.com/go-pkgz/auth"
"github.com/go-pkgz/auth/avatar"
"github.com/go-pkgz/auth/provider"
"github.com/go-pkgz/auth/provider/sender"
"github.com/go-pkgz/auth/token"
"github.com/go-pkgz/auth/v2"
"github.com/go-pkgz/auth/v2/avatar"
"github.com/go-pkgz/auth/v2/provider"
"github.com/go-pkgz/auth/v2/provider/sender"
"github.com/go-pkgz/auth/v2/token"
cache "github.com/go-pkgz/lcw/v2"
"github.com/umputun/remark42/backend/app/migrator"
@@ -96,7 +96,7 @@ type ServerCommand struct {
Cookie time.Duration `long:"cookie" env:"COOKIE" default:"200h" description:"auth cookie TTL"`
} `group:"ttl" namespace:"ttl" env-namespace:"TTL"`
SendJWTHeader bool `long:"send-jwt-header" env:"SEND_JWT_HEADER" description:"send JWT as a header instead of cookie"`
SendJWTHeader bool `long:"send-jwt-header" env:"SEND_JWT_HEADER" description:"send JWT as a header instead of server-set cookie; with this enabled, frontend stores the JWT in a client-side cookie (note: increases vulnerability to XSS attacks)"`
SameSite string `long:"same-site" env:"SAME_SITE" description:"set same site policy for cookies" choice:"default" choice:"none" choice:"lax" choice:"strict" default:"default"` // nolint
Apple AppleGroup `group:"apple" namespace:"apple" env-namespace:"APPLE" description:"Apple OAuth"`
@@ -107,6 +107,7 @@ type ServerCommand struct {
Yandex AuthGroup `group:"yandex" namespace:"yandex" env-namespace:"YANDEX" description:"Yandex OAuth"`
Twitter AuthGroup `group:"twitter" namespace:"twitter" env-namespace:"TWITTER" description:"[deprecated, doesn't work] Twitter OAuth"`
Patreon AuthGroup `group:"patreon" namespace:"patreon" env-namespace:"PATREON" description:"Patreon OAuth"`
Discord AuthGroup `group:"discord" namespace:"discord" env-namespace:"DISCORD" description:"Discord OAuth"`
Telegram bool `long:"telegram" env:"TELEGRAM" description:"Enable Telegram auth (using token from telegram.token)"`
Dev bool `long:"dev" env:"DEV" description:"enable dev (local) oauth2"`
Anonymous bool `long:"anon" env:"ANON" description:"enable anonymous login"`
@@ -321,6 +322,7 @@ func (s *ServerCommand) Execute(_ []string) error {
"AUTH_TWITTER_CSEC",
"AUTH_YANDEX_CSEC",
"AUTH_PATREON_CSEC",
"AUTH_DISCORD_CSEC",
"TELEGRAM_TOKEN",
"SMTP_PASSWORD",
"ADMIN_PASSWD",
@@ -952,6 +954,10 @@ func (s *ServerCommand) addAuthProviders(authenticator *auth.Service) error {
authenticator.AddProvider("patreon", s.Auth.Patreon.CID, s.Auth.Patreon.CSEC)
providersCount++
}
if s.Auth.Discord.CID != "" && s.Auth.Discord.CSEC != "" {
authenticator.AddProvider("discord", s.Auth.Discord.CID, s.Auth.Discord.CSEC)
providersCount++
}
if s.Auth.Dev {
log.Print("[INFO] dev access enabled")
@@ -1103,10 +1109,10 @@ func (s *ServerCommand) makeNotifyDestinations(authenticator *auth.Service) ([]n
TokenGenFn: func(userID, email, site string) (string, error) {
claims := token.Claims{
Handshake: &token.Handshake{ID: userID + "::" + email},
StandardClaims: jwt.StandardClaims{
Audience: site,
ExpiresAt: time.Now().Add(100 * 365 * 24 * time.Hour).Unix(),
NotBefore: time.Now().Add(-1 * time.Minute).Unix(),
RegisteredClaims: jwt.RegisteredClaims{
Audience: jwt.ClaimStrings{site},
ExpiresAt: jwt.NewNumericDate(time.Now().Add(100 * 365 * 24 * time.Hour)),
NotBefore: jwt.NewNumericDate(time.Now().Add(-1 * time.Minute)),
Issuer: "remark42",
},
}
@@ -1209,10 +1215,16 @@ func (s *ServerCommand) getAuthenticator(ds *service.DataStore, avas avatar.Stor
if c.User == nil {
return c
}
c.User.SetAdmin(ds.IsAdmin(c.Audience, c.User.ID))
c.User.SetBoolAttr("blocked", ds.IsBlocked(c.Audience, c.User.ID))
// Audience is a slice but we set it to a single element, and situation when there is no audience or there are more than one is unexpected
if len(c.Audience) != 1 {
return c
}
audience := c.Audience[0]
c.User.SetAdmin(ds.IsAdmin(audience, c.User.ID))
c.User.SetBoolAttr("blocked", ds.IsBlocked(audience, c.User.ID))
var err error
c.User.Email, err = ds.GetUserEmail(c.Audience, c.User.ID)
c.User.Email, err = ds.GetUserEmail(audience, c.User.ID)
if err != nil {
log.Printf("[WARN] can't read email for %s, %v", c.User.ID, err)
}
@@ -1344,11 +1356,11 @@ func newAuthRefreshCache() *authRefreshCache {
}
// Get implements cache getter with key converted to string
func (c *authRefreshCache) Get(key interface{}) (interface{}, bool) {
return c.LoadingCache.Peek(key.(string))
func (c *authRefreshCache) Get(key string) (token.Claims, bool) {
return c.Peek(key)
}
// Set implements cache setter with key converted to string
func (c *authRefreshCache) Set(key, value interface{}) {
_, _ = c.LoadingCache.Get(key.(string), func() (token.Claims, error) { return value.(token.Claims), nil })
func (c *authRefreshCache) Set(key string, value token.Claims) {
_, _ = c.LoadingCache.Get(key, func() (token.Claims, error) { return value, nil })
}
+53 -14
View File
@@ -15,8 +15,8 @@ import (
"testing"
"time"
"github.com/go-pkgz/auth/token"
"github.com/golang-jwt/jwt"
"github.com/go-pkgz/auth/v2/token"
"github.com/golang-jwt/jwt/v5"
"github.com/jessevdk/go-flags"
"go.uber.org/goleak"
@@ -79,7 +79,7 @@ func TestServerApp_DevMode(t *testing.T) {
waitForHTTPServerStart(port)
providers := app.restSrv.Authenticator.Providers()
require.Equal(t, 10+1, len(providers), "extra auth provider")
require.Equal(t, 11+1, len(providers), "extra auth provider")
assert.Equal(t, "dev", providers[len(providers)-2].Name(), "dev auth provider")
// send ping
resp, err := http.Get(fmt.Sprintf("http://localhost:%d/api/v1/ping", port))
@@ -107,7 +107,7 @@ func TestServerApp_AnonMode(t *testing.T) {
waitForHTTPServerStart(port)
providers := app.restSrv.Authenticator.Providers()
require.Equal(t, 10+1, len(providers), "extra auth provider for anon")
require.Equal(t, 11+1, len(providers), "extra auth provider for anon")
assert.Equal(t, "anonymous", providers[len(providers)-1].Name(), "anon auth provider")
client := http.Client{Timeout: 10 * time.Second}
@@ -609,11 +609,11 @@ func TestServerAuthHooks(t *testing.T) {
tkService.TokenDuration = time.Second
claims := token.Claims{
StandardClaims: jwt.StandardClaims{
Audience: "remark",
RegisteredClaims: jwt.RegisteredClaims{
Audience: jwt.ClaimStrings{"remark"},
Issuer: "remark",
ExpiresAt: time.Now().Add(time.Second).Unix(),
NotBefore: time.Now().Add(-1 * time.Minute).Unix(),
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Second)),
NotBefore: jwt.NewNumericDate(time.Now().Add(-1 * time.Minute)),
},
User: &token.User{
ID: "github_dev",
@@ -637,10 +637,10 @@ func TestServerAuthHooks(t *testing.T) {
require.NoError(t, resp.Body.Close())
assert.Equal(t, http.StatusCreated, resp.StatusCode, "non-blocked user able to post")
// add comment with no-aud claim
claimsNoAud := claims
claimsNoAud.Audience = ""
tkNoAud, err := tkService.Token(claimsNoAud)
// try to add comment with no-aud claim
badClaimsNoAud := claims
badClaimsNoAud.Audience = jwt.ClaimStrings{""}
tkNoAud, err := tkService.Token(badClaimsNoAud)
require.NoError(t, err)
t.Logf("no-aud claims: %s", tkNoAud)
req, err = http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/api/v1/comment", port),
@@ -655,6 +655,43 @@ func TestServerAuthHooks(t *testing.T) {
require.NoError(t, resp.Body.Close())
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode, "user without aud claim rejected, \n"+tkNoAud+"\n"+string(body))
// try to add comment with multiple auds
badClaimsMultipleAud := claims
badClaimsMultipleAud.Audience = jwt.ClaimStrings{"remark", "second_aud"}
tkMultipleAuds, err := tkService.Token(badClaimsMultipleAud)
require.NoError(t, err)
t.Logf("multiple aud claims: %s", tkMultipleAuds)
req, err = http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/api/v1/comment", port),
strings.NewReader(`{"text": "test 123", "locator":{"url": "https://radio-t.com/p/2018/12/29/podcast-631/",
"site": "remark"}}`))
require.NoError(t, err)
req.Header.Set("X-JWT", tkMultipleAuds)
resp, err = client.Do(req)
require.NoError(t, err)
body, err = io.ReadAll(resp.Body)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode, "user with multiple auds claim rejected, \n"+tkMultipleAuds+"\n"+string(body))
// try to add comment without user set
badClaimsNoUser := claims
badClaimsNoUser.Audience = jwt.ClaimStrings{"remark"}
badClaimsNoUser.User = nil
tkNoUser, err := tkService.Token(badClaimsNoUser)
require.NoError(t, err)
t.Logf("no user claims: %s", tkNoUser)
req, err = http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/api/v1/comment", port),
strings.NewReader(`{"text": "test 123", "locator":{"url": "https://radio-t.com/p/2018/12/29/podcast-631/",
"site": "remark"}}`))
require.NoError(t, err)
req.Header.Set("X-JWT", tkNoUser)
resp, err = client.Do(req)
require.NoError(t, err)
body, err = io.ReadAll(resp.Body)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode, "user without user information rejected, \n"+tkNoUser+"\n"+string(body))
// block user github_dev as admin
req, err = http.NewRequest(http.MethodPut,
fmt.Sprintf("http://localhost:%d/api/v1/admin/user/github_dev?site=remark&block=1&ttl=10d", port), http.NoBody)
@@ -701,7 +738,6 @@ func TestServerCommand_parseSameSite(t *testing.T) {
cmd := ServerCommand{}
for i, tt := range tbl {
tt := tt
t.Run(strconv.Itoa(i), func(t *testing.T) {
assert.Equal(t, tt.res, cmd.parseSameSite(tt.inp))
})
@@ -797,7 +833,6 @@ func prepServerApp(t *testing.T, fn func(o ServerCommand) ServerCommand) (*serve
_, err := p.ParseArgs([]string{"--admin-passwd=password", "--site=remark"})
require.NoError(t, err)
cmd.Avatar.FS.Path, cmd.Avatar.Type, cmd.BackupLocation, cmd.Image.FS.Path = "/tmp/remark42_test", "fs", "/tmp/remark42_test", "/tmp/remark42_test"
cmd.Store.Bolt.Path = fmt.Sprintf("/tmp/%d", cmd.Port)
cmd.Store.Bolt.Timeout = 10 * time.Second
cmd.Auth.Apple.CID, cmd.Auth.Apple.KID, cmd.Auth.Apple.TID = "cid", "kid", "tid"
cmd.Auth.Apple.PrivateKeyFilePath = "testdata/apple.p8"
@@ -808,6 +843,7 @@ func prepServerApp(t *testing.T, fn func(o ServerCommand) ServerCommand) (*serve
cmd.Auth.Microsoft.CSEC, cmd.Auth.Microsoft.CID = "csec", "cid"
cmd.Auth.Twitter.CSEC, cmd.Auth.Twitter.CID = "csec", "cid"
cmd.Auth.Patreon.CSEC, cmd.Auth.Patreon.CID = "csec", "cid"
cmd.Auth.Discord.CSEC, cmd.Auth.Discord.CID = "csec", "cid"
cmd.Auth.Telegram = true
cmd.Telegram.Token = "token"
cmd.Auth.Email.Enable = true
@@ -828,7 +864,10 @@ func prepServerApp(t *testing.T, fn func(o ServerCommand) ServerCommand) (*serve
cmd.RestrictedNames = []string{"umputun", "bobuk"}
cmd.emailMsgTemplatePath = "../../templates/email_reply.html.tmpl"
cmd.emailVerificationTemplatePath = "../../templates/email_confirmation_subscription.html.tmpl"
cmd = fn(cmd)
// as is uses port, call it after fn which could set it
cmd.Store.Bolt.Path = fmt.Sprintf("/tmp/%d", cmd.Port)
app, ctx, cancel := createAppFromCmd(t, cmd)
+8 -6
View File
@@ -98,9 +98,6 @@ func TestMain_WithWebhook(t *testing.T) {
finished := make(chan struct{})
go func() {
main()
assert.Eventually(t, func() bool {
return atomic.LoadInt32(&webhookSent) == int32(1)
}, time.Second, 100*time.Millisecond, "webhook was not sent")
close(finished)
}()
@@ -117,13 +114,18 @@ func TestMain_WithWebhook(t *testing.T) {
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusCreated, resp.StatusCode)
// wait for webhook to be sent before shutting down
assert.Eventually(t, func() bool {
return atomic.LoadInt32(&webhookSent) == int32(1)
}, time.Second, 100*time.Millisecond, "webhook was not sent")
}
func TestGetDump(t *testing.T) {
dump := getDump()
assert.True(t, strings.Contains(dump, "goroutine"))
assert.True(t, strings.Contains(dump, "[running]"))
assert.True(t, strings.Contains(dump, "backend/app/main.go"))
assert.Contains(t, dump, "goroutine")
assert.Contains(t, dump, "[running]")
assert.Contains(t, dump, "backend/app/main.go")
t.Logf("\n dump: %s", dump)
}
+5
View File
@@ -101,6 +101,11 @@ func (d *Commento) convert(r io.Reader, siteID string) (ch chan store.Comment) {
}
}
usersMap["anonymous"] = store.User{
Name: "Anonymous",
ID: "commento_" + store.EncodeID("anonymous"),
}
for _, comment := range exportedData.Comments {
u, ok := usersMap[comment.CommenterHex]
if !ok {
+16 -3
View File
@@ -27,11 +27,11 @@ func TestCommento_Import(t *testing.T) {
require.NoError(t, err)
size, err := d.Import(fh, "test")
assert.NoError(t, err)
assert.Equal(t, 2, size)
assert.Equal(t, 3, size)
last, err := dataStore.Last("test", 10, time.Time{}, adminUser)
assert.NoError(t, err)
require.Equal(t, 2, len(last), "2 comments imported")
require.Equal(t, 3, len(last), "3 comments imported")
t.Log(last[0])
@@ -44,11 +44,24 @@ func TestCommento_Import(t *testing.T) {
assert.Equal(t, "commento_35369aeb6ac5255de30410a0f86dc71eb9c6d0ca", c.User.ID)
assert.True(t, c.Imported)
c = last[2] // anonymous comment
assert.Equal(t, "Example comment created by user.", c.Text)
assert.Equal(t, "e7069a7dfcfaed43caf62300a9b0edb1c124ad79d0f5887c93649c15d7f69945", c.ID)
assert.Equal(t, "", c.ParentID)
assert.Equal(t, store.Locator{SiteID: "test", URL: "https://example.com/blog/post/2"}, c.Locator)
assert.Equal(t, "Anonymous", c.User.Name)
assert.Equal(t, "commento_0a92fab3230134cca6eadd9898325b9b2ae67998", c.User.ID)
assert.True(t, c.Imported)
posts, err := dataStore.List("test", 0, 0)
assert.NoError(t, err)
assert.Equal(t, 1, len(posts), "1 post")
assert.Equal(t, 2, len(posts), "2 posts")
count, err := dataStore.Count(store.Locator{SiteID: "test", URL: "https://example.com/blog/post/1"})
assert.NoError(t, err)
assert.Equal(t, 2, count)
count, err = dataStore.Count(store.Locator{SiteID: "test", URL: "https://example.com/blog/post/2"})
assert.NoError(t, err)
assert.Equal(t, 1, count)
}
+2 -2
View File
@@ -175,7 +175,7 @@ func (d *Disqus) convert(r io.Reader, siteID string) (ch chan store.Comment) {
func (*Disqus) cleanText(text string) string {
text = strings.TrimSpace(text)
text = strings.Replace(text, "\n", "", -1)
text = strings.Replace(text, "\t", "", -1)
text = strings.ReplaceAll(text, "\n", "")
text = strings.ReplaceAll(text, "\t", "")
return text
}
+2 -2
View File
@@ -77,11 +77,11 @@ func TestMigrator_ImportCommento(t *testing.T) {
Provider: "commento",
})
assert.NoError(t, err)
assert.Equal(t, 2, size)
assert.Equal(t, 3, size)
last, err := dataStore.Last("test", 10, time.Time{}, store.User{})
assert.NoError(t, err)
assert.Equal(t, 2, len(last), "2 comments imported")
assert.Equal(t, 3, len(last), "3 comments imported")
}
func TestMigrator_ImportNative(t *testing.T) {
+1 -1
View File
@@ -4,7 +4,7 @@
{
"commentHex": "e7069a7dfcfaed43caf62300a9b0edb1c124ad79d0f5887c93649c15d7f69945",
"domain": "example.com",
"url": "/blog/post/1",
"url": "/blog/post/2",
"commenterHex": "anonymous",
"markdown": "Example comment created by user.",
"html": "",
+3 -3
View File
@@ -10,7 +10,7 @@ import (
log "github.com/go-pkgz/lgr"
ntf "github.com/go-pkgz/notify"
"github.com/go-pkgz/repeater"
"github.com/go-pkgz/repeater/v2"
"github.com/hashicorp/go-multierror"
"github.com/umputun/remark42/backend/app/templates"
@@ -161,7 +161,7 @@ func (e *Email) buildAndSendMessage(ctx context.Context, req Request, email stri
return err
}
return repeater.NewDefault(5, time.Millisecond*250).Do(
return repeater.NewFixed(5, time.Millisecond*250).Do(
ctx,
func() error {
return e.Email.Send(
@@ -196,7 +196,7 @@ func (e *Email) SendVerification(ctx context.Context, req VerificationRequest) e
return err
}
return repeater.NewDefault(5, time.Millisecond*250).Do(
return repeater.NewFixed(5, time.Millisecond*250).Do(
ctx,
func() error {
return e.Email.Send(
+1 -2
View File
@@ -34,7 +34,7 @@ func TestEmailNew(t *testing.T) {
assert.NotNil(t, email, "email returned")
assert.NotNil(t, email.msgTmpl, "e.template is set")
assert.Equal(t, emailParams.From, email.EmailParams.From, "emailParams.From unchanged after creation")
assert.Equal(t, emailParams.From, email.From, "emailParams.From unchanged after creation")
if smtpParams.TimeOut == 0 {
assert.Equal(t, defaultEmailTimeout, email.TimeOut, "empty emailParams.TimeOut changed to default")
} else {
@@ -88,7 +88,6 @@ func Test_initTemplatesErr(t *testing.T) {
}
for _, d := range testSet {
d := d
t.Run(d.name, func(t *testing.T) {
e, err := NewEmail(d.emailParams, ntf.SMTPParams{})
require.Error(t, err)
+12 -1
View File
@@ -66,4 +66,15 @@ func (m *MockDest) GetVerify() []VerificationRequest {
return res
}
func (m *MockDest) String() string { return fmt.Sprintf("mock id=%d, closed=%v", m.id, m.closed) }
// IsClosed returns closed status safely
func (m *MockDest) IsClosed() bool {
m.lock.Lock()
defer m.lock.Unlock()
return m.closed
}
func (m *MockDest) String() string {
m.lock.Lock()
defer m.lock.Unlock()
return fmt.Sprintf("mock id=%d, closed=%v", m.id, m.closed)
}
+5 -3
View File
@@ -102,15 +102,17 @@ func TestService_Many(t *testing.T) {
time.Sleep(time.Millisecond * time.Duration(rand.Int31n(20)))
}
s.Close()
time.Sleep(time.Millisecond * 10)
// wait for destinations to close
assert.Eventually(t, func() bool { return d1.IsClosed() && d2.IsClosed() }, 100*time.Millisecond, 10*time.Millisecond)
assert.NotEqual(t, 10, len(d1.Get()), "some comments dropped from d1")
assert.NotEqual(t, 10, len(d1.GetVerify()), "some verifications dropped from d1")
assert.NotEqual(t, 10, len(d2.Get()), "some comments dropped from d2")
assert.NotEqual(t, 10, len(d2.GetVerify()), "some verifications dropped from d2")
assert.True(t, d1.closed)
assert.True(t, d2.closed)
assert.True(t, d1.IsClosed())
assert.True(t, d2.IsClosed())
assert.Equal(t, "mock id=1, closed=true", d1.String())
}
+77
View File
@@ -0,0 +1,77 @@
package notify
import (
"fmt"
"strings"
"golang.org/x/net/html"
)
// pruneHTML prunes string keeping HTML closing tags.
// maxLength applies to visible text only, not HTML tags.
func pruneHTML(htmlText string, maxLength int) string {
var result strings.Builder
var endTokens []string
visibleLen := 0
suffix := "..."
suffixLen := len(suffix)
tokenizer := html.NewTokenizer(strings.NewReader(htmlText))
for {
if tokenizer.Next() == html.ErrorToken {
return result.String()
}
token := tokenizer.Token()
switch token.Type {
case html.CommentToken, html.DoctypeToken:
continue
case html.StartTagToken:
endTokens = append([]string{fmt.Sprintf("</%s>", token.Data)}, endTokens...)
result.WriteString(token.String())
case html.EndTagToken:
if len(endTokens) > 0 {
endTokens = endTokens[1:]
}
result.WriteString(token.String())
case html.SelfClosingTagToken:
result.WriteString(token.String())
case html.TextToken:
text := token.String()
if visibleLen+len(text)+suffixLen > maxLength {
remaining := maxLength - visibleLen - suffixLen
text = pruneStringToWord(text, remaining)
result.WriteString(text)
result.WriteString(suffix)
for _, endTag := range endTokens {
result.WriteString(endTag)
}
return result.String()
}
visibleLen += len(text)
result.WriteString(text)
}
}
}
// pruneStringToWord prunes string to specified length respecting word boundaries
func pruneStringToWord(text string, maxLength int) string {
if maxLength <= 0 {
return ""
}
if len(text) <= maxLength {
return text
}
// find last space at or before maxLength to cut at word boundary
lastSpace := strings.LastIndex(text[:maxLength+1], " ")
if lastSpace <= 0 {
return ""
}
return text[:lastSpace]
}
+47
View File
@@ -0,0 +1,47 @@
package notify
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestPruneHTML(t *testing.T) {
tests := []struct {
name string
html string
maxLength int
expected string
}{
{"within limit", "<p>Hello</p>", 20, "<p>Hello</p>"},
{"exceeds limit", "<p>Hello world, this is a long text</p>", 15, "<p>Hello world,...</p>"},
{"nested tags", "<div><p>Hello world</p><p>More text</p></div>", 20, "<div><p>Hello world</p><p>More...</p></div>"},
{"html comment stripped", "<!-- comment --><p>Hello</p>", 20, "<p>Hello</p>"},
{"self-closing tag", "<p>Hello<br/>World</p>", 8, "<p>Hello<br/>...</p>"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expected, pruneHTML(tt.html, tt.maxLength))
})
}
}
func TestPruneStringToWord(t *testing.T) {
tests := []struct {
name string
text string
maxLength int
expected string
}{
{"within limit", "hello world", 15, "hello world"},
{"cut at word boundary", "hello world and more", 11, "hello world"},
{"zero length", "hello", 0, ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expected, pruneStringToWord(tt.text, tt.maxLength))
})
}
}
+4 -2
View File
@@ -10,6 +10,8 @@ import (
"github.com/hashicorp/go-multierror"
)
const commentTextLengthLimit = 100
// TelegramParams contain settings for telegram notifications
type TelegramParams struct {
AdminChannelID string // unique identifier for the target chat or username of the target channel (in the format @channelusername)
@@ -85,10 +87,10 @@ func (t *Telegram) buildMessage(req Request) string {
msg += fmt.Sprintf(" -> <a href=%q>%s</a>", commentURLPrefix+req.parent.ID, ntf.EscapeTelegramText(req.parent.User.Name))
}
msg += fmt.Sprintf("\n\n%s", ntf.TelegramSupportedHTML(req.Comment.Text))
msg += fmt.Sprintf("\n\n%s", pruneHTML(ntf.TelegramSupportedHTML(req.Comment.Text), commentTextLengthLimit))
if req.Comment.ParentID != "" {
msg += fmt.Sprintf("\n\n\"<i>%s</i>\"", ntf.TelegramSupportedHTML(req.parent.Text))
msg += fmt.Sprintf("\n\n\"<i>%s</i>\"", pruneHTML(ntf.TelegramSupportedHTML(req.parent.Text), commentTextLengthLimit))
}
if req.Comment.PostTitle != "" {
+9
View File
@@ -53,6 +53,15 @@ some text
<b>Hello</b><i><b>World</b></i>`,
res)
// prune string keeping HTML closing tags
c = store.Comment{
Text: "<b>Lorem ipsum <i>dolor sit amet</i>, consectetur adipiscing <code>elit, sed do eiusmod tempor incididunt</code> ut labore et dolore magna aliqua.</b>",
}
res = tb.buildMessage(Request{Comment: c})
assert.Equal(t, `<a href="#remark42__comment-"></a>
<b>Lorem ipsum <i>dolor sit amet</i>, consectetur adipiscing <code>elit, sed do eiusmod tempor incididunt</code> ut...</b>`, res)
}
func TestTelegram_SendVerification(t *testing.T) {
+22 -20
View File
@@ -7,8 +7,7 @@ import (
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
"github.com/go-pkgz/auth"
"github.com/go-pkgz/auth/v2"
cache "github.com/go-pkgz/lcw/v2"
log "github.com/go-pkgz/lgr"
R "github.com/go-pkgz/rest"
@@ -54,8 +53,7 @@ func (a *admin) deleteCommentCtrl(w http.ResponseWriter, r *http.Request) {
return
}
a.cache.Flush(cache.Flusher(locator.SiteID).Scopes(locator.SiteID, locator.URL, lastCommentsScope))
render.Status(r, http.StatusOK)
render.JSON(w, r, R.JSON{"id": id, "locator": locator})
R.RenderJSON(w, R.JSON{"id": id, "locator": locator})
}
// DELETE /user/{userid}?site=side-id - delete all user comments for requested userid
@@ -69,8 +67,7 @@ func (a *admin) deleteUserCtrl(w http.ResponseWriter, r *http.Request) {
return
}
a.cache.Flush(cache.Flusher(siteID).Scopes(userID, siteID, lastCommentsScope))
render.Status(r, http.StatusOK)
render.JSON(w, r, R.JSON{"user_id": userID, "site_id": siteID})
R.RenderJSON(w, R.JSON{"user_id": userID, "site_id": siteID})
}
// GET /user/{userid}?site=side-id - get user info for requested userid
@@ -84,8 +81,7 @@ func (a *admin) getUserInfoCtrl(w http.ResponseWriter, r *http.Request) {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get user info", rest.ErrInternal)
return
}
render.Status(r, http.StatusOK)
render.JSON(w, r, ucomments[0].User)
R.RenderJSON(w, ucomments[0].User)
}
// GET /deleteme?token=jwt - delete all user comments and details by user's request. Gets info about deleted used from provided token
@@ -107,13 +103,21 @@ func (a *admin) deleteMeRequestCtrl(w http.ResponseWriter, r *http.Request) {
return
}
if err = a.dataService.DeleteUserDetail(claims.Audience, claims.User.ID, engine.AllUserDetails); err != nil {
// Audience is a slice but we set it to a single element, and situation when there is no audience or there are more than one is unexpected
if len(claims.Audience) != 1 {
rest.SendErrorJSON(w, r, http.StatusBadRequest, fmt.Errorf("bad request"), "can't process token, claims.Audience expected to be a single element but it's not", rest.ErrActionRejected)
return
}
audience := claims.Audience[0]
if err = a.dataService.DeleteUserDetail(audience, claims.User.ID, engine.AllUserDetails); err != nil {
code := parseError(err, rest.ErrInternal)
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't delete user details for user", code)
return
}
if err = a.dataService.DeleteUser(claims.Audience, claims.User.ID, store.HardDelete); err != nil {
if err = a.dataService.DeleteUser(audience, claims.User.ID, store.HardDelete); err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't delete user", rest.ErrNoAccess)
return
}
@@ -126,9 +130,8 @@ func (a *admin) deleteMeRequestCtrl(w http.ResponseWriter, r *http.Request) {
}
}
a.cache.Flush(cache.Flusher(claims.Audience).Scopes(claims.Audience, claims.User.ID, lastCommentsScope))
render.Status(r, http.StatusOK)
render.JSON(w, r, R.JSON{"user_id": claims.User.ID, "site_id": claims.Audience})
a.cache.Flush(cache.Flusher(audience).Scopes(audience, claims.User.ID, lastCommentsScope))
R.RenderJSON(w, R.JSON{"user_id": claims.User.ID, "site_id": claims.Audience})
}
// PUT /user/{userid}?site=side-id&block=1&ttl=7d - block or unblock user
@@ -156,7 +159,7 @@ func (a *admin) setBlockCtrl(w http.ResponseWriter, r *http.Request) {
}
}
a.cache.Flush(cache.Flusher(siteID).Scopes(userID, siteID, lastCommentsScope))
render.JSON(w, r, R.JSON{"user_id": userID, "site_id": siteID, "block": blockStatus})
R.RenderJSON(w, R.JSON{"user_id": userID, "site_id": siteID, "block": blockStatus})
}
// GET /blocked?site=siteID - list blocked users
@@ -167,7 +170,7 @@ func (a *admin) blockedUsersCtrl(w http.ResponseWriter, r *http.Request) {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get blocked users", rest.ErrSiteNotFound)
return
}
render.JSON(w, r, users)
R.RenderJSON(w, users)
}
// PUT /readonly?site=siteID&url=post-url&ro=1 - set or reset read-only status for the post
@@ -194,7 +197,7 @@ func (a *admin) setReadOnlyCtrl(w http.ResponseWriter, r *http.Request) {
return
}
a.cache.Flush(cache.Flusher(locator.SiteID).Scopes(locator.URL, locator.SiteID))
render.JSON(w, r, R.JSON{"locator": locator, "read-only": roStatus})
R.RenderJSON(w, R.JSON{"locator": locator, "read-only": roStatus})
}
// PUT /title/{id}?site=siteID&url=post-url - set comment PostTitle to page's title
@@ -210,8 +213,7 @@ func (a *admin) setTitleCtrl(w http.ResponseWriter, r *http.Request) {
log.Printf("[INFO] set comment's title %s to %q", id, c.PostTitle)
a.cache.Flush(cache.Flusher(locator.SiteID).Scopes(locator.URL, lastCommentsScope))
render.Status(r, http.StatusOK)
render.JSON(w, r, R.JSON{"id": id, "locator": locator})
R.RenderJSON(w, R.JSON{"id": id, "locator": locator})
}
// PUT /verify?site=siteID&url=post-url&ro=1 - set or reset read-only status for the post
@@ -225,7 +227,7 @@ func (a *admin) setVerifyCtrl(w http.ResponseWriter, r *http.Request) {
return
}
a.cache.Flush(cache.Flusher(siteID).Scopes(siteID, userID))
render.JSON(w, r, R.JSON{"user": userID, "verified": verifyStatus})
R.RenderJSON(w, R.JSON{"user": userID, "verified": verifyStatus})
}
// PUT /pin/{id}?site=siteID&url=post-url&pin=1
@@ -240,5 +242,5 @@ func (a *admin) setPinCtrl(w http.ResponseWriter, r *http.Request) {
return
}
a.cache.Flush(cache.Flusher(locator.SiteID).Scopes(locator.URL))
render.JSON(w, r, R.JSON{"id": commentID, "locator": locator, "pin": pinStatus})
R.RenderJSON(w, R.JSON{"id": commentID, "locator": locator, "pin": pinStatus})
}
+38 -19
View File
@@ -13,10 +13,10 @@ import (
"testing"
"time"
"github.com/go-pkgz/auth/token"
"github.com/go-pkgz/auth/v2/token"
cache "github.com/go-pkgz/lcw/v2"
R "github.com/go-pkgz/rest"
"github.com/golang-jwt/jwt"
"github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -708,12 +708,12 @@ func TestAdmin_DeleteMeRequest(t *testing.T) {
claims := token.Claims{
SessionOnly: true,
StandardClaims: jwt.StandardClaims{
Audience: "remark42",
Id: "1234567",
RegisteredClaims: jwt.RegisteredClaims{
Audience: jwt.ClaimStrings{"remark42"},
ID: "1234567",
Issuer: "remark42",
NotBefore: time.Now().Add(-1 * time.Minute).Unix(),
ExpiresAt: time.Now().Add(30 * time.Minute).Unix(),
NotBefore: jwt.NewNumericDate(time.Now().Add(-1 * time.Minute)),
ExpiresAt: jwt.NewNumericDate(time.Now().Add(30 * time.Minute)),
},
User: &token.User{
ID: "user1",
@@ -777,12 +777,12 @@ func TestAdmin_DeleteMeRequestFailed(t *testing.T) {
// try with bad auth
claims := token.Claims{
SessionOnly: true,
StandardClaims: jwt.StandardClaims{
Audience: "remark42",
Id: "provider1_1234567",
RegisteredClaims: jwt.RegisteredClaims{
Audience: jwt.ClaimStrings{"remark42"},
ID: "provider1_1234567",
Issuer: "remark42",
NotBefore: time.Now().Add(-1 * time.Minute).Unix(),
ExpiresAt: time.Now().Add(30 * time.Minute).Unix(),
NotBefore: jwt.NewNumericDate(time.Now().Add(-1 * time.Minute)),
ExpiresAt: jwt.NewNumericDate(time.Now().Add(30 * time.Minute)),
},
User: &token.User{
ID: "provider1_user1",
@@ -803,9 +803,9 @@ func TestAdmin_DeleteMeRequestFailed(t *testing.T) {
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
// try bad user
badClaims := claims
badClaims.User.ID = "no-such-id"
tkn, err = srv.Authenticator.TokenService().Token(badClaims)
badClaimsUser := claims
badClaimsUser.User.ID = "no-such-id"
tkn, err = srv.Authenticator.TokenService().Token(badClaimsUser)
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/admin/deleteme?token=%s", ts.URL, tkn), http.NoBody)
assert.NoError(t, err)
@@ -814,11 +814,12 @@ func TestAdmin_DeleteMeRequestFailed(t *testing.T) {
assert.NoError(t, err)
assert.NoError(t, resp.Body.Close())
assert.Equal(t, http.StatusBadRequest, resp.StatusCode, resp.Status)
badClaimsUser.User.ID = "provider1_user1"
// try without deleteme flag
badClaims2 := claims
badClaims2.User.SetBoolAttr("delete_me", false)
tkn, err = srv.Authenticator.TokenService().Token(badClaims2)
badClaimsWithoutDeleteMe := claims
badClaimsWithoutDeleteMe.User.SetBoolAttr("delete_me", false)
tkn, err = srv.Authenticator.TokenService().Token(badClaimsWithoutDeleteMe)
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/admin/deleteme?token=%s", ts.URL, tkn), http.NoBody)
assert.NoError(t, err)
@@ -829,7 +830,25 @@ func TestAdmin_DeleteMeRequestFailed(t *testing.T) {
b, err := io.ReadAll(resp.Body)
assert.NoError(t, err)
assert.NoError(t, resp.Body.Close())
assert.True(t, strings.Contains(string(b), "can't use provided token"))
assert.Contains(t, string(b), "can't use provided token")
badClaimsWithoutDeleteMe.User.SetBoolAttr("delete_me", true)
// try with wrong audience
badClaimsMultipleAudience := claims
badClaimsMultipleAudience.Audience = jwt.ClaimStrings{"remark42", "something else"}
tkn, err = srv.Authenticator.TokenService().Token(badClaimsMultipleAudience)
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/admin/deleteme?token=%s", ts.URL, tkn), http.NoBody)
assert.NoError(t, err)
req.SetBasicAuth("admin", "password")
resp, err = client.Do(req)
assert.NoError(t, err)
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
b, err = io.ReadAll(resp.Body)
assert.NoError(t, err)
assert.NoError(t, resp.Body.Close())
assert.Contains(t, string(b), "can't process token, claims.Audience expected to be a single element but it's not")
badClaimsMultipleAudience.Audience = jwt.ClaimStrings{"remark42"}
}
func TestAdmin_GetUserInfo(t *testing.T) {
+28 -25
View File
@@ -1,16 +1,17 @@
package api
import (
"bytes"
"compress/gzip"
"context"
"fmt"
"io"
"net/http"
"os"
"strconv"
"sync"
"time"
"github.com/go-chi/render"
cache "github.com/go-pkgz/lcw/v2"
log "github.com/go-pkgz/lgr"
R "github.com/go-pkgz/rest"
@@ -58,8 +59,7 @@ func (m *Migrator) importCtrl(w http.ResponseWriter, r *http.Request) {
go m.runImport(siteID, r.URL.Query().Get("provider"), tmpfile) // import runs in background and sets busy flag for site
render.Status(r, http.StatusAccepted)
render.JSON(w, r, R.JSON{"status": "import request accepted"})
_ = R.EncodeJSON(w, http.StatusAccepted, R.JSON{"status": "import request accepted"})
}
// POST /import/form?secret=key&site=site-id&provider=disqus|remark|wordpress
@@ -93,8 +93,7 @@ func (m *Migrator) importFormCtrl(w http.ResponseWriter, r *http.Request) {
go m.runImport(siteID, r.URL.Query().Get("provider"), tmpfile) // import runs in background and sets busy flag for site
render.Status(r, http.StatusAccepted)
render.JSON(w, r, R.JSON{"status": "import request accepted"})
_ = R.EncodeJSON(w, http.StatusAccepted, R.JSON{"status": "import request accepted"})
}
// GET /wait?site=site-id
@@ -110,20 +109,16 @@ func (m *Migrator) waitCtrl(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(context.Background(), timeOut)
defer cancel()
for {
if !m.isBusy(siteID) {
break
}
for m.isBusy(siteID) {
select {
case <-ctx.Done():
render.Status(r, http.StatusGatewayTimeout)
render.JSON(w, r, R.JSON{"status": "timeout expired", "site_id": siteID})
_ = R.EncodeJSON(w, http.StatusGatewayTimeout, R.JSON{"status": "timeout expired", "site_id": siteID})
return
case <-time.After(100 * time.Millisecond):
}
}
render.Status(r, http.StatusOK)
render.JSON(w, r, R.JSON{"status": "completed", "site_id": siteID})
R.RenderJSON(w, R.JSON{"status": "completed", "site_id": siteID})
}
// GET /export?site=site-id&secret=12345&?mode=file|stream
@@ -131,23 +126,32 @@ func (m *Migrator) waitCtrl(w http.ResponseWriter, r *http.Request) {
func (m *Migrator) exportCtrl(w http.ResponseWriter, r *http.Request) {
siteID := r.URL.Query().Get("site")
var writer io.Writer = w
if r.URL.Query().Get("mode") == "file" {
// buffer to memory to handle errors before committing to response
var buf bytes.Buffer
gzWriter := gzip.NewWriter(&buf)
if _, err := m.NativeExporter.Export(gzWriter, siteID); err != nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "export failed", rest.ErrInternal)
return
}
if err := gzWriter.Close(); err != nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "export failed", rest.ErrInternal)
return
}
exportFile := fmt.Sprintf("%s-%s.json.gz", siteID, time.Now().Format("20060102"))
w.Header().Set("Content-Type", "application/gzip")
w.Header().Set("Content-Disposition", "attachment;filename="+exportFile)
gzWriter := gzip.NewWriter(w)
defer func() {
if e := gzWriter.Close(); e != nil {
log.Printf("[WARN] can't close gzip writer, %s", e)
}
}()
writer = gzWriter
w.Header().Set("Content-Length", strconv.Itoa(buf.Len()))
if _, err := io.Copy(w, &buf); err != nil {
log.Printf("[WARN] failed to write export response: %v", err)
}
return
}
if _, err := m.NativeExporter.Export(writer, siteID); err != nil {
// stream mode - write directly to response
if _, err := m.NativeExporter.Export(w, siteID); err != nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "export failed", rest.ErrInternal)
return
}
}
@@ -203,8 +207,7 @@ func (m *Migrator) remapCtrl(w http.ResponseWriter, r *http.Request) {
log.Printf("[DEBUG] convert request completed. site=%s, comments=%d", siteID, size)
}()
render.Status(r, http.StatusAccepted)
render.JSON(w, r, R.JSON{"status": "convert request accepted"})
_ = R.EncodeJSON(w, http.StatusAccepted, R.JSON{"status": "convert request accepted"})
}
// runImport reads from tmpfile and import for given siteID and provider
+1 -1
View File
@@ -123,7 +123,7 @@ func TestMigrator_ImportFromWP(t *testing.T) {
ts, _, teardown := startupT(t)
defer teardown()
r := strings.NewReader(strings.Replace(xmlTestWP, "'", "`", -1))
r := strings.NewReader(strings.ReplaceAll(xmlTestWP, "'", "`"))
client := &http.Client{Timeout: 1 * time.Second}
defer client.CloseIdleConnections()
+29 -19
View File
@@ -15,13 +15,12 @@ import (
"sync"
"time"
"github.com/didip/tollbooth/v7"
"github.com/didip/tollbooth_chi"
"github.com/didip/tollbooth/v8"
"github.com/didip/tollbooth/v8/limiter"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
"github.com/go-chi/render"
"github.com/go-pkgz/auth"
"github.com/go-pkgz/auth/v2"
"github.com/go-pkgz/lcw/v2"
log "github.com/go-pkgz/lgr"
R "github.com/go-pkgz/rest"
@@ -235,14 +234,14 @@ func (s *Rest) routes() chi.Router {
router.Group(func(r chi.Router) {
r.Use(middleware.Timeout(5 * time.Second))
r.Use(logInfoWithBody, tollbooth_chi.LimitHandler(tollbooth.NewLimiter(2, nil)), middleware.NoCache)
r.Use(logInfoWithBody, rateLimiter(2), middleware.NoCache)
r.Use(validEmailAuth()) // reject suspicious email logins
r.Mount("/auth", authHandler)
})
router.Group(func(r chi.Router) {
r.Use(middleware.Timeout(5 * time.Second))
r.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(100, nil)))
r.Use(rateLimiter(100))
r.Mount("/avatar", avatarHandler)
})
@@ -252,14 +251,14 @@ func (s *Rest) routes() chi.Router {
router.Route("/api/v1", func(rapi chi.Router) {
rapi.Group(func(rava chi.Router) {
rava.Use(middleware.Timeout(5 * time.Second))
rava.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(100, nil)))
rava.Use(rateLimiter(100))
rava.Mount("/avatar", avatarHandler)
})
// open routes
rapi.Group(func(ropen chi.Router) {
ropen.Use(middleware.Timeout(30 * time.Second))
ropen.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(s.openRouteLimiter, nil)))
ropen.Use(rateLimiter(s.openRouteLimiter))
ropen.Use(authMiddleware.Trace, middleware.NoCache, logInfoWithBody)
ropen.Get("/config", s.configCtrl)
ropen.Get("/find", s.pubRest.findCommentsCtrl)
@@ -282,7 +281,7 @@ func (s *Rest) routes() chi.Router {
// open routes, cached
rapi.Group(func(ropen chi.Router) {
ropen.Use(middleware.Timeout(30 * time.Second))
ropen.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(10, nil)))
ropen.Use(rateLimiter(10))
ropen.Use(authMiddleware.Trace, logInfoWithBody)
ropen.Get("/picture/{user}/{id}", s.pubRest.loadPictureCtrl)
ropen.Get("/qr/telegram", s.pubRest.telegramQrCtrl)
@@ -291,7 +290,7 @@ func (s *Rest) routes() chi.Router {
// protected routes, require auth
rapi.Group(func(rauth chi.Router) {
rauth.Use(middleware.Timeout(30 * time.Second))
rauth.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(10, nil)))
rauth.Use(rateLimiter(10))
rauth.Use(authMiddleware.Auth, matchSiteID, middleware.NoCache, logInfoWithBody)
rauth.Get("/user", s.privRest.userInfoCtrl)
rauth.Get("/userdata", s.privRest.userAllDataCtrl)
@@ -300,7 +299,7 @@ func (s *Rest) routes() chi.Router {
// admin routes, require auth and admin users only
rapi.Route("/admin", func(radmin chi.Router) {
radmin.Use(middleware.Timeout(30 * time.Second))
radmin.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(10, nil)))
radmin.Use(rateLimiter(10))
radmin.Use(authMiddleware.Auth, authMiddleware.AdminOnly, matchSiteID)
radmin.Use(middleware.NoCache, logInfoWithBody)
@@ -326,7 +325,7 @@ func (s *Rest) routes() chi.Router {
// protected routes, throttled to 10/s by default, controlled by external UpdateLimiter param
rapi.Group(func(rauth chi.Router) {
rauth.Use(middleware.Timeout(10 * time.Second))
rauth.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(s.updateLimiter(), nil)))
rauth.Use(rateLimiter(s.updateLimiter()))
rauth.Use(authMiddleware.Auth, matchSiteID, subscribersOnly(s.SubscribersOnly))
rauth.Use(middleware.NoCache, logInfoWithBody)
@@ -346,7 +345,7 @@ func (s *Rest) routes() chi.Router {
// protected routes, anonymous rejected
rapi.Group(func(rauth chi.Router) {
rauth.Use(middleware.Timeout(10 * time.Second))
rauth.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(s.updateLimiter(), nil)))
rauth.Use(rateLimiter(s.updateLimiter()))
rauth.Use(authMiddleware.Auth, rejectAnonUser, matchSiteID)
rauth.Use(logger.New(logger.Log(log.Default()), logger.Prefix("[DEBUG]"), logger.IPfn(ipFn)).Handler)
rauth.Post("/picture", s.privRest.savePictureCtrl)
@@ -356,7 +355,7 @@ func (s *Rest) routes() chi.Router {
// open routes on root level
router.Group(func(rroot chi.Router) {
rroot.Use(middleware.Timeout(10 * time.Second))
rroot.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(50, nil)))
rroot.Use(rateLimiter(50))
rroot.Get("/robots.txt", s.pubRest.robotsCtrl)
rroot.Get("/email/unsubscribe.html", s.privRest.emailUnsubscribeCtrl)
rroot.Post("/email/unsubscribe.html", s.privRest.emailUnsubscribeCtrl)
@@ -473,8 +472,7 @@ func (s *Rest) configCtrl(w http.ResponseWriter, r *http.Request) {
if cnf.Admins == nil { // prevent json serialization to nil
cnf.Admins = []string{}
}
render.Status(r, http.StatusOK)
render.JSON(w, r, cnf)
R.RenderJSON(w, cnf)
}
// serves static files from the webRoot directory or files embedded into the compiled binary if that directory is absent
@@ -493,7 +491,7 @@ func addFileServer(r chi.Router, embedFS embed.FS, webRoot, version string) {
webFS = http.StripPrefix("/web", webFS)
r.Get("/web", http.RedirectHandler("/web/", http.StatusMovedPermanently).ServeHTTP)
r.With(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(20, nil)),
r.With(rateLimiter(20),
middleware.Timeout(10*time.Second),
cacheControl(time.Hour, version),
).Get("/web/*", func(w http.ResponseWriter, r *http.Request) {
@@ -630,10 +628,9 @@ func securityHeadersMiddleware(imageProxyEnabled bool, allowedAncestors []string
}
frameAncestors := "*"
if len(allowedAncestors) > 0 {
log.Printf("[INFO] frame embedding allowed from %+v only", allowedAncestors)
frameAncestors = strings.Join(allowedAncestors, " ")
}
w.Header().Set("Content-Security-Policy", fmt.Sprintf("default-src 'none'; base-uri 'none'; form-action 'none'; connect-src 'self'; frame-src 'self'; img-src %s; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; font-src data:; object-src 'none'; frame-ancestors %s;", imgSrc, frameAncestors))
w.Header().Set("Content-Security-Policy", fmt.Sprintf("default-src 'none'; base-uri 'none'; form-action 'none'; connect-src 'self'; frame-src 'self' mailto:; img-src %s; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; font-src data:; object-src 'none'; frame-ancestors %s;", imgSrc, frameAncestors))
w.Header().Set("Permissions-Policy", "accelerometer=(), autoplay=(), camera=(), cross-origin-isolated=(), display-capture=(), encrypted-media=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), xr-spatial-tracking=(), clipboard-read=(), clipboard-write=(), gamepad=(), hid=(), idle-detection=(), interest-cohort=(), serial=(), unload=(), window-management=()")
next.ServeHTTP(w, r)
})
@@ -730,3 +727,16 @@ func parseError(err error, defaultCode int) (code int) {
return code
}
// rateLimiter creates a rate limiting middleware with proper IP lookup configuration.
// tollbooth v8 requires explicit IP lookup method to be set.
// uses RemoteAddr which is set by chi's middleware.RealIP to the real client IP
// from X-Forwarded-For, X-Real-IP, or True-Client-IP headers.
func rateLimiter(maxReq float64) func(http.Handler) http.Handler {
lmt := tollbooth.NewLimiter(maxReq, nil)
lmt.SetIPLookup(limiter.IPLookup{
Name: "RemoteAddr",
IndexFromRight: 0,
})
return tollbooth.HTTPMiddleware(lmt)
}
+31 -34
View File
@@ -15,13 +15,12 @@ import (
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
"github.com/go-pkgz/auth"
"github.com/go-pkgz/auth/token"
"github.com/go-pkgz/auth/v2"
"github.com/go-pkgz/auth/v2/token"
cache "github.com/go-pkgz/lcw/v2"
log "github.com/go-pkgz/lgr"
R "github.com/go-pkgz/rest"
"github.com/golang-jwt/jwt"
"github.com/golang-jwt/jwt/v5"
"github.com/hashicorp/go-multierror"
"github.com/umputun/remark42/backend/app/notify"
@@ -77,7 +76,7 @@ func (s *private) previewCommentCtrl(w http.ResponseWriter, r *http.Request) {
user := rest.MustGetUserInfo(r)
comment := store.Comment{}
if err := render.DecodeJSON(http.MaxBytesReader(w, r.Body, hardBodyLimit), &comment); err != nil {
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, hardBodyLimit)).Decode(&comment); err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't bind comment", rest.ErrDecode)
return
}
@@ -100,14 +99,13 @@ func (s *private) previewCommentCtrl(w http.ResponseWriter, r *http.Request) {
return
}
}
render.HTML(w, r, comment.Text)
rest.HTMLResponse(w, http.StatusOK, comment.Text)
}
// POST /comment - adds comment, resets all immutable fields
func (s *private) createCommentCtrl(w http.ResponseWriter, r *http.Request) {
comment := store.Comment{}
if err := render.DecodeJSON(http.MaxBytesReader(w, r.Body, hardBodyLimit), &comment); err != nil {
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, hardBodyLimit)).Decode(&comment); err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't bind comment", rest.ErrDecode)
return
}
@@ -176,8 +174,7 @@ func (s *private) createCommentCtrl(w http.ResponseWriter, r *http.Request) {
log.Printf("[DEBUG] created comment %+v", finalComment)
render.Status(r, http.StatusCreated)
render.JSON(w, r, &finalComment)
_ = R.EncodeJSON(w, http.StatusCreated, &finalComment)
}
// PUT /comment/{id}?site=siteID&url=post-url - update comment
@@ -188,7 +185,7 @@ func (s *private) updateCommentCtrl(w http.ResponseWriter, r *http.Request) {
Delete bool
}{}
if err := render.DecodeJSON(http.MaxBytesReader(w, r.Body, hardBodyLimit), &edit); err != nil {
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, hardBodyLimit)).Decode(&edit); err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't read comment details from body", rest.ErrDecode)
return
}
@@ -233,7 +230,7 @@ func (s *private) updateCommentCtrl(w http.ResponseWriter, r *http.Request) {
}
s.cache.Flush(cache.Flusher(locator.SiteID).Scopes(locator.SiteID, locator.URL, lastCommentsScope, user.ID))
render.JSON(w, r, res)
R.RenderJSON(w, res)
}
// GET /user?site=siteID - returns user info
@@ -251,7 +248,7 @@ func (s *private) userInfoCtrl(w http.ResponseWriter, r *http.Request) {
}
}
render.JSON(w, r, user)
R.RenderJSON(w, user)
}
// PUT /vote/{id}?site=siteID&url=post-url&vote=1 - vote for/against comment
@@ -292,7 +289,7 @@ func (s *private) voteCtrl(w http.ResponseWriter, r *http.Request) {
return
}
s.cache.Flush(cache.Flusher(locator.SiteID).Scopes(locator.URL, comment.User.ID))
render.JSON(w, r, R.JSON{"id": comment.ID, "score": comment.Score})
R.RenderJSON(w, R.JSON{"id": comment.ID, "score": comment.Score})
}
// getEmailCtrl gets email address for authenticated user.
@@ -305,7 +302,7 @@ func (s *private) getEmailCtrl(w http.ResponseWriter, r *http.Request) {
log.Printf("[WARN] can't read email for %s, %v", user.ID, err)
}
render.JSON(w, r, R.JSON{"user": user, "address": address})
R.RenderJSON(w, R.JSON{"user": user, "address": address})
}
// sendEmailConfirmationCtrl gets address and siteID from query, makes confirmation token and sends it to user.
@@ -322,7 +319,7 @@ func (s *private) sendEmailConfirmationCtrl(w http.ResponseWriter, r *http.Reque
Address string
autoConfirm bool
}{autoConfirm: true}
if err := render.DecodeJSON(http.MaxBytesReader(w, r.Body, hardBodyLimit), &subscribe); err != nil {
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, hardBodyLimit)).Decode(&subscribe); err != nil {
if err != io.EOF {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't parse request body", rest.ErrDecode)
return
@@ -362,10 +359,10 @@ func (s *private) sendEmailConfirmationCtrl(w http.ResponseWriter, r *http.Reque
claims := token.Claims{
Handshake: &token.Handshake{ID: user.ID + "::" + subscribe.Address},
StandardClaims: jwt.StandardClaims{
Audience: r.URL.Query().Get("site"),
ExpiresAt: time.Now().Add(30 * time.Minute).Unix(),
NotBefore: time.Now().Add(-1 * time.Minute).Unix(),
RegisteredClaims: jwt.RegisteredClaims{
Audience: jwt.ClaimStrings{r.URL.Query().Get("site")},
ExpiresAt: jwt.NewNumericDate(time.Now().Add(30 * time.Minute)),
NotBefore: jwt.NewNumericDate(time.Now().Add(-1 * time.Minute)),
Issuer: "remark42",
},
}
@@ -385,7 +382,7 @@ func (s *private) sendEmailConfirmationCtrl(w http.ResponseWriter, r *http.Reque
},
)
render.JSON(w, r, R.JSON{"user": user, "address": subscribe.Address, "updated": false})
R.RenderJSON(w, R.JSON{"user": user, "address": subscribe.Address, "updated": false})
}
// telegramSubscribeCtrl generates and verifies telegram notification request
@@ -423,7 +420,7 @@ func (s *private) telegramSubscribeCtrl(w http.ResponseWriter, r *http.Request)
s.telegramService.AddToken(tkn, user.ID, siteID, expires)
render.JSON(w, r, R.JSON{"token": tkn, "bot": s.telegramService.GetBotUsername()})
R.RenderJSON(w, R.JSON{"token": tkn, "bot": s.telegramService.GetBotUsername()})
return
}
@@ -445,7 +442,7 @@ func (s *private) telegramSubscribeCtrl(w http.ResponseWriter, r *http.Request)
return
}
render.JSON(w, r, R.JSON{"updated": true, "address": val})
R.RenderJSON(w, R.JSON{"updated": true, "address": val})
}
// setConfirmedEmailCtrl uses provided token parameter (generated by sendEmailConfirmationCtrl) to set email and add it to user token
@@ -457,7 +454,7 @@ func (s *private) setConfirmedEmailCtrl(w http.ResponseWriter, r *http.Request)
Site string
Token string
}{}
if err := render.DecodeJSON(http.MaxBytesReader(w, r.Body, hardBodyLimit), &confirm); err != nil {
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, hardBodyLimit)).Decode(&confirm); err != nil {
if err != io.EOF {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't parse request body", rest.ErrDecode)
return
@@ -513,7 +510,7 @@ func (s *private) setEmail(w http.ResponseWriter, r *http.Request, userID, siteI
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to set token", rest.ErrInternal)
return
}
render.JSON(w, r, R.JSON{"updated": true, "address": val})
R.RenderJSON(w, R.JSON{"updated": true, "address": val})
}
// POST/GET /email/unsubscribe.html?site=siteID&tkn=jwt - unsubscribe the user in token from email notifications
@@ -597,7 +594,7 @@ func (s *private) emailUnsubscribeCtrl(w http.ResponseWriter, r *http.Request) {
tmpl := template.Must(template.New("unsubscribe").Parse(tmplstr))
msg := bytes.Buffer{}
MustExecute(tmpl, &msg, nil)
render.HTML(w, r, msg.String())
rest.HTMLResponse(w, http.StatusOK, msg.String())
}
// DELETE /email?site=siteID - removes user's email
@@ -624,7 +621,7 @@ func (s *private) deleteEmailCtrl(w http.ResponseWriter, r *http.Request) {
return
}
}
render.JSON(w, r, R.JSON{"deleted": true})
R.RenderJSON(w, R.JSON{"deleted": true})
}
// DELETE /telegram?site=siteID - removes user's telegram
@@ -638,7 +635,7 @@ func (s *private) deleteTelegramCtrl(w http.ResponseWriter, r *http.Request) {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't delete telegram for user", code)
return
}
render.JSON(w, r, R.JSON{"deleted": true})
R.RenderJSON(w, R.JSON{"deleted": true})
}
// GET /userdata?site=siteID - exports all data about the user as a json with user info and list of all comments
@@ -704,11 +701,11 @@ func (s *private) deleteMeCtrl(w http.ResponseWriter, r *http.Request) {
siteID := r.URL.Query().Get("site")
claims := token.Claims{
StandardClaims: jwt.StandardClaims{
Audience: siteID,
RegisteredClaims: jwt.RegisteredClaims{
Audience: jwt.ClaimStrings{siteID},
Issuer: "remark42",
ExpiresAt: time.Now().AddDate(0, 3, 0).Unix(),
NotBefore: time.Now().Add(-1 * time.Minute).Unix(),
ExpiresAt: jwt.NewNumericDate(time.Now().AddDate(0, 3, 0)),
NotBefore: jwt.NewNumericDate(time.Now().Add(-1 * time.Minute)),
},
User: &token.User{
ID: user.ID,
@@ -726,7 +723,7 @@ func (s *private) deleteMeCtrl(w http.ResponseWriter, r *http.Request) {
}
link := fmt.Sprintf("%s/web/deleteme.html?token=%s", s.remarkURL, tokenStr)
render.JSON(w, r, R.JSON{"site": siteID, "user_id": user.ID, "token": tokenStr, "link": link})
R.RenderJSON(w, R.JSON{"site": siteID, "user_id": user.ID, "token": tokenStr, "link": link})
}
// POST /image - save image with form request
@@ -751,7 +748,7 @@ func (s *private) savePictureCtrl(w http.ResponseWriter, r *http.Request) {
return
}
render.JSON(w, r, R.JSON{"id": id})
R.RenderJSON(w, R.JSON{"id": id})
}
func (s *private) isReadOnly(locator store.Locator) bool {
+8 -10
View File
@@ -16,11 +16,10 @@ import (
"testing"
"time"
"github.com/go-chi/render"
"github.com/go-pkgz/auth/token"
"github.com/go-pkgz/auth/v2/token"
"github.com/go-pkgz/lgr"
R "github.com/go-pkgz/rest"
"github.com/golang-jwt/jwt"
"github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -861,10 +860,10 @@ func TestRest_EmailAndTelegram(t *testing.T) {
// issue good token
claims := token.Claims{
Handshake: &token.Handshake{ID: "provider1_dev::good@example.com"},
StandardClaims: jwt.StandardClaims{
Audience: "remark42",
ExpiresAt: time.Now().Add(10 * time.Minute).Unix(),
NotBefore: time.Now().Add(-1 * time.Minute).Unix(),
RegisteredClaims: jwt.RegisteredClaims{
Audience: jwt.ClaimStrings{"remark42"},
ExpiresAt: jwt.NewNumericDate(time.Now().Add(10 * time.Minute)),
NotBefore: jwt.NewNumericDate(time.Now().Add(-1 * time.Minute)),
Issuer: "remark42",
},
}
@@ -916,7 +915,6 @@ func TestRest_EmailAndTelegram(t *testing.T) {
client := http.Client{}
defer client.CloseIdleConnections()
for _, x := range testData {
x := x
t.Run(x.description, func(t *testing.T) {
reqBody := io.NopCloser(strings.NewReader(x.body))
if x.body == "" {
@@ -971,7 +969,7 @@ func TestRest_EmailNotification(t *testing.T) {
require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusCreated, resp.StatusCode, string(body))
parentComment := store.Comment{}
require.NoError(t, render.DecodeJSON(strings.NewReader(string(body)), &parentComment))
require.NoError(t, json.Unmarshal(body, &parentComment))
// wait for mock notification Submit to kick off
time.Sleep(time.Millisecond * 30)
require.Equal(t, 1, len(mockDestination.Get()))
@@ -1221,7 +1219,7 @@ func TestRest_TelegramNotification(t *testing.T) {
require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusCreated, resp.StatusCode, string(body))
parentComment := store.Comment{}
require.NoError(t, render.DecodeJSON(strings.NewReader(string(body)), &parentComment))
require.NoError(t, json.Unmarshal(body, &parentComment))
// wait for mock notification Submit to kick off
time.Sleep(time.Millisecond * 30)
require.Equal(t, 1, len(mockDestination.Get()))
+66 -10
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"crypto/sha1" // nolint
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
@@ -12,10 +13,10 @@ import (
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
cache "github.com/go-pkgz/lcw/v2"
log "github.com/go-pkgz/lgr"
R "github.com/go-pkgz/rest"
"github.com/google/uuid"
"github.com/skip2/go-qrcode"
"github.com/umputun/remark42/backend/app/rest"
@@ -48,10 +49,18 @@ type pubStore interface {
Counts(siteID string, postIDs []string) ([]store.PostInfo, error)
}
// GET /find?site=siteID&url=post-url&format=[tree|plain]&sort=[+/-time|+/-score|+/-controversy]&view=[user|all]&since=unix_ts_msec
// find comments for given post. Returns in tree or plain formats, sorted
// GET /find?site=siteID&url=post-url&format=[tree|plain]&sort=[+/-time|+/-score|+/-controversy]&view=[user|all]&since=unix_ts_msec&limit=100&offset_id={id}
// find comments for given post. Returns in tree or plain formats, sorted.
//
// When `url` parameter is not set (e.g. request is for site-wide comments), does not return deleted comments.
//
// When `limit` is set, first {limit} comments are returned. When `offset_id` is set, comments are returned starting
// after the comment with the given id.
// format="tree" limits comments by top-level comments and all their replies,
// and never returns parent comment with only part of replies.
//
// `count` in the response refers to total number of non-deleted comments,
// `count_left` to amount of comments left to be returned _including deleted_.
func (s *public) findCommentsCtrl(w http.ResponseWriter, r *http.Request) {
locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")}
sort := r.URL.Query().Get("sort")
@@ -70,7 +79,24 @@ func (s *public) findCommentsCtrl(w http.ResponseWriter, r *http.Request) {
since = time.Time{} // since doesn't make sense for tree
}
log.Printf("[DEBUG] get comments for %+v, sort %s, format %s, since %v", locator, sort, format, since)
limitParam := r.URL.Query().Get("limit")
var limit int
if limitParam != "" {
if limit, err = strconv.Atoi(limitParam); err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "bad limit value", rest.ErrCommentNotFound)
return
}
}
offsetID := r.URL.Query().Get("offset_id")
if offsetID != "" {
if _, err = uuid.Parse(offsetID); err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "bad offset_id value", rest.ErrCommentNotFound)
return
}
}
log.Printf("[DEBUG] get comments for %+v, sort %s, format %s, since %v, limit %d, offset %s", locator, sort, format, since, limit, offsetID)
key := cache.NewKey(locator.SiteID).ID(URLKeyWithUser(r)).Scopes(locator.SiteID, locator.URL)
data, err := s.cache.Get(key, func() ([]byte, error) {
@@ -102,12 +128,20 @@ func (s *public) findCommentsCtrl(w http.ResponseWriter, r *http.Request) {
var b []byte
switch format {
case "tree":
withInfo := treeWithInfo{Tree: service.MakeTree(comments, sort), Info: commentsInfo}
withInfo := treeWithInfo{Tree: service.MakeTree(comments, sort, limit, offsetID), Info: commentsInfo}
withInfo.Info.CountLeft = withInfo.CountLeft()
withInfo.Info.LastComment = withInfo.LastComment()
if withInfo.Nodes == nil { // eliminate json nil serialization
withInfo.Nodes = []*service.Node{}
}
b, e = encodeJSONWithHTML(withInfo)
default:
if limit > 0 || offsetID != "" {
comments, commentsInfo.CountLeft = limitComments(comments, limit, offsetID)
}
if limit > 0 && len(comments) > 0 {
commentsInfo.LastComment = comments[len(comments)-1].ID
}
withInfo := commentsWithInfo{Comments: comments, Info: commentsInfo}
b, e = encodeJSONWithHTML(withInfo)
}
@@ -198,7 +232,6 @@ func (s *public) commentByIDCtrl(w http.ResponseWriter, r *http.Request) {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get comment by id", rest.ErrCommentNotFound)
return
}
render.Status(r, http.StatusOK)
if err = R.RenderJSONWithHTML(w, r, comment); err != nil {
log.Printf("[WARN] can't render last comments for url=%s, id=%s", url, id)
@@ -263,7 +296,7 @@ func (s *public) countCtrl(w http.ResponseWriter, r *http.Request) {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get count", rest.ErrPostNotFound)
return
}
render.JSON(w, r, R.JSON{"count": count, "locator": locator})
R.RenderJSON(w, R.JSON{"count": count, "locator": locator})
}
// POST /counts?site=siteID - get number of comments for posts from post body
@@ -271,7 +304,7 @@ func (s *public) countMultiCtrl(w http.ResponseWriter, r *http.Request) {
const countBodyLimit int64 = 1024 * 128 // count request can be big for some site because it lists all urls
siteID := r.URL.Query().Get("site")
posts := []string{}
if err := render.DecodeJSON(http.MaxBytesReader(w, r.Body, countBodyLimit), &posts); err != nil {
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, countBodyLimit)).Decode(&posts); err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get list of posts from request", rest.ErrSiteNotFound)
return
}
@@ -359,13 +392,14 @@ func (s *public) loadPictureCtrl(w http.ResponseWriter, r *http.Request) {
}
// GET /robots.txt
func (s *public) robotsCtrl(w http.ResponseWriter, r *http.Request) {
func (s *public) robotsCtrl(w http.ResponseWriter, _ *http.Request) {
allowed := []string{"/find", "/last", "/id", "/count", "/counts", "/list", "/config", "/user",
"/img", "/avatar", "/picture"}
for i := range allowed {
allowed[i] = "Allow: /api/v1" + allowed[i]
}
render.PlainText(w, r, "User-agent: *\nDisallow: /auth/\nDisallow: /api/\n"+strings.Join(allowed, "\n")+"\n")
responseText := fmt.Sprintf("User-agent: *\nDisallow: /auth/\nDisallow: /api/\n%s\n", strings.Join(allowed, "\n"))
rest.PlainTextResponse(w, http.StatusOK, responseText)
}
// GET /qr/telegram - generates QR for provided URL, used for Telegram auth and notifications subscription. The first
@@ -432,3 +466,25 @@ func (s *public) parseSince(r *http.Request) (time.Time, error) {
}
return sinceTS, nil
}
// limitComments returns limited list of comments and count of comments left after limit.
// If offsetID is provided, the list will be sliced starting from the comment with this ID.
// If offsetID is not found, the full list will be returned.
// It's used for only "
func limitComments(c []store.Comment, limit int, offsetID string) (comments []store.Comment, countLeft int) {
if offsetID != "" {
for i, comment := range c {
if comment.ID == offsetID {
c = c[i+1:]
break
}
}
}
if limit > 0 && len(c) > limit {
countLeft = len(c) - limit
c = c[:limit]
}
return c, countLeft
}
+110 -24
View File
@@ -14,6 +14,7 @@ import (
cache "github.com/go-pkgz/lcw/v2"
R "github.com/go-pkgz/rest"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -140,9 +141,9 @@ srv, ts := prep(t)
}
BKT
`
text = strings.Replace(text, "BKT", "```", -1)
text = strings.ReplaceAll(text, "BKT", "```")
j := fmt.Sprintf(`{"text": %q, "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`, text)
j = strings.Replace(j, "\n", "\\n", -1)
j = strings.ReplaceAll(j, "\n", "\\n")
resp, err := post(t, ts.URL+"/api/v1/preview", j)
assert.NoError(t, err)
@@ -168,17 +169,17 @@ func TestRest_PreviewCode(t *testing.T) {
func main(aa string) int {return 0}
BKT
`
text = strings.Replace(text, "BKT", "```", -1)
text = strings.ReplaceAll(text, "BKT", "```")
j := fmt.Sprintf(`{"text": %q, "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`, text)
j = strings.Replace(j, "\n", "\\n", -1)
j = strings.ReplaceAll(j, "\n", "\\n")
resp, err := post(t, ts.URL+"/api/v1/preview", j)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
b, err := io.ReadAll(resp.Body)
assert.NoError(t, err)
assert.Equal(t, `<pre class="chroma"><code><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">main</span><span class="p">(</span><span class="nx">aa</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">int</span> <span class="p">{</span><span class="k">return</span> <span class="mi">0</span><span class="p">}</span>
</span></span></code></pre>`, string(b))
assert.Equal(t, `<pre class="chroma"><code><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">main</span><span class="p">(</span><span class="nx">aa</span><span class="w"> </span><span class="kt">string</span><span class="p">)</span><span class="w"> </span><span class="kt">int</span><span class="w"> </span><span class="p">{</span><span class="k">return</span><span class="w"> </span><span class="mi">0</span><span class="p">}</span><span class="w">
</span></span></span></code></pre>`, string(b))
assert.NoError(t, resp.Body.Close())
}
@@ -598,7 +599,7 @@ func TestPublic_FindCommentsCtrl_ConsistentCount(t *testing.T) {
setScore(commentLocator, ids[4], -3)
time.Sleep(time.Millisecond * 5)
c6 := store.Comment{Text: "third-level comment 2", ParentID: ids[4], Locator: commentLocator}
c6 := store.Comment{Text: "deleted third-level comment 2", ParentID: ids[4], Locator: commentLocator}
ids[5], timestamps[5] = addCommentGetCreatedTime(t, c6, ts)
// deleted later so not visible in site-wide requests
setScore(commentLocator, ids[5], 10)
@@ -612,7 +613,7 @@ func TestPublic_FindCommentsCtrl_ConsistentCount(t *testing.T) {
setScore(commentLocator, ids[6], 1)
time.Sleep(time.Millisecond * 5)
c8 := store.Comment{Text: "second-level comment 3", ParentID: ids[6], Locator: commentLocator}
c8 := store.Comment{Text: "deleted second-level comment 3", ParentID: ids[6], Locator: commentLocator}
ids[7], timestamps[7] = addCommentGetCreatedTime(t, c8, ts)
// deleted later so not visible in site-wide requests
setScore(commentLocator, ids[7], -20)
@@ -646,18 +647,19 @@ func TestPublic_FindCommentsCtrl_ConsistentCount(t *testing.T) {
params string
expectedBody string
}{
{"", fmt.Sprintf(`"info":{"count":7,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[8])},
{"url=test-url", fmt.Sprintf(`"info":{"url":"test-url","count":6,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[7])},
{"format=plain", fmt.Sprintf(`"info":{"count":7,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[8])},
{"format=plain&url=test-url", fmt.Sprintf(`"info":{"url":"test-url","count":6,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[7])},
{"since=" + sinceTenSecondsAgo, fmt.Sprintf(`"info":{"count":7,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[8])},
{"url=test-url&since=" + sinceTenSecondsAgo, fmt.Sprintf(`"info":{"url":"test-url","count":6,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[7])},
{"since=" + sinceTS[0], fmt.Sprintf(`"info":{"count":7,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[8])},
{"url=test-url&since=" + sinceTS[0], fmt.Sprintf(`"info":{"url":"test-url","count":6,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[7])},
{"since=" + sinceTS[1], fmt.Sprintf(`"info":{"count":6,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[8])},
{"url=test-url&since=" + sinceTS[1], fmt.Sprintf(`"info":{"url":"test-url","count":5,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[7])},
{"since=" + sinceTS[4], fmt.Sprintf(`"info":{"count":3,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[8])},
{"url=test-url&since=" + sinceTS[4], fmt.Sprintf(`"info":{"url":"test-url","count":2,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[7])},
// test parameters url, format, since, sort
{"", fmt.Sprintf(`"info":{"count":7,"count_left":0,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[8])},
{"url=test-url", fmt.Sprintf(`"info":{"url":"test-url","count":6,"count_left":0,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[7])},
{"format=plain", fmt.Sprintf(`"info":{"count":7,"count_left":0,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[8])},
{"format=plain&url=test-url", fmt.Sprintf(`"info":{"url":"test-url","count":6,"count_left":0,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[7])},
{"since=" + sinceTenSecondsAgo, fmt.Sprintf(`"info":{"count":7,"count_left":0,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[8])},
{"url=test-url&since=" + sinceTenSecondsAgo, fmt.Sprintf(`"info":{"url":"test-url","count":6,"count_left":0,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[7])},
{"since=" + sinceTS[0], fmt.Sprintf(`"info":{"count":7,"count_left":0,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[8])},
{"url=test-url&since=" + sinceTS[0], fmt.Sprintf(`"info":{"url":"test-url","count":6,"count_left":0,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[7])},
{"since=" + sinceTS[1], fmt.Sprintf(`"info":{"count":6,"count_left":0,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[8])},
{"url=test-url&since=" + sinceTS[1], fmt.Sprintf(`"info":{"url":"test-url","count":5,"count_left":0,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[7])},
{"since=" + sinceTS[4], fmt.Sprintf(`"info":{"count":3,"count_left":0,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[8])},
{"url=test-url&since=" + sinceTS[4], fmt.Sprintf(`"info":{"url":"test-url","count":2,"count_left":0,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[7])},
{"format=tree", `"info":{"count":7`},
{"format=tree&url=test-url", `"info":{"url":"test-url","count":6`},
{"format=tree&sort=+time", `"info":{"count":7`},
@@ -677,19 +679,103 @@ func TestPublic_FindCommentsCtrl_ConsistentCount(t *testing.T) {
// three comments of which last one deleted and doesn't have controversy so returned last
{"sort=-controversy&url=test-url&since=" + sinceTS[5], fmt.Sprintf(`"score":0,"vote":0,"time":%q,"delete":true}],"info":{"url":"test-url","count":1`, formattedTS[7])},
// test readonly status for the post without comments
{"url=readonly-test", `"info":{"count":0,"read_only":true`},
{"format=tree&url=readonly-test", `"info":{"count":0,"read_only":true`},
{"url=readonly-test", `"info":{"count":0,"count_left":0,"read_only":true`},
{"format=tree&url=readonly-test", `"info":{"count":0,"count_left":0,"read_only":true`},
// test parameters limit, offset_id for format=plain
{"limit=bad", `{"code":1,"details":"bad limit value","error":"strconv.Atoi: parsing \"bad\": invalid syntax"}`},
{"offset_id=bad", `{"code":1,"details":"bad offset_id value","error":"invalid UUID length: 3"}`},
{"limit=2", `"info":{"count":7,"count_left":5,"last_comment":"` + ids[1]},
{"limit=6", `"info":{"count":7,"count_left":1,"last_comment":"` + ids[6]},
{"limit=7", `"info":{"count":7,"count_left":0,"last_comment":"` + ids[8]},
{"limit=2&url=test-url", `"info":{"url":"test-url","count":6,"count_left":6,"last_comment":"` + ids[1]},
{"limit=6&url=test-url", `"info":{"url":"test-url","count":6,"count_left":2,"last_comment":"` + ids[5]},
{"limit=7&url=test-url", `"info":{"url":"test-url","count":6,"count_left":1,"last_comment":"` + ids[6]},
{fmt.Sprintf("limit=2&offset_id=%s", ids[2]), `"info":{"count":7,"count_left":2,"last_comment":"` + ids[4]},
{fmt.Sprintf("limit=2&offset_id=%s", ids[3]), `"info":{"count":7,"count_left":1,"last_comment":"` + ids[6]},
{fmt.Sprintf("limit=2&offset_id=%s", ids[4]), `"info":{"count":7,"count_left":0`},
{fmt.Sprintf("limit=1&offset_id=%s", ids[6]), `"info":{"count":7,"count_left":0`},
{fmt.Sprintf("limit=2&offset_id=%s", ids[8]), `"info":{"count":7,"count_left":0`},
{fmt.Sprintf("limit=2&url=test-url&offset_id=%s", ids[2]), `"info":{"url":"test-url","count":6,"count_left":3,"last_comment":"` + ids[4]},
{fmt.Sprintf("limit=2&url=test-url&offset_id=%s", ids[3]), `"info":{"url":"test-url","count":6,"count_left":2,"last_comment":"` + ids[5]},
{fmt.Sprintf("limit=2&url=test-url&offset_id=%s", ids[4]), `"info":{"url":"test-url","count":6,"count_left":1,"last_comment":"` + ids[6]},
{fmt.Sprintf("limit=1&url=test-url&offset_id=%s", ids[6]), `"info":{"url":"test-url","count":6,"count_left":0,"last_comment":"` + ids[7]},
{fmt.Sprintf("limit=2&url=test-url&offset_id=%s", ids[8]), `"info":{"url":"test-url","count":6,"count_left":6,`},
// deleted comment, offset is ignored in site-wide request but not for particular URL
{fmt.Sprintf("limit=2&offset_id=%s", ids[5]), `"info":{"count":7,"count_left":5,"last_comment":"` + ids[1]},
{fmt.Sprintf("limit=2&url=test-url&offset_id=%s", ids[5]), `"info":{"url":"test-url","count":6,"count_left":0,"last_comment":"` + ids[7]},
// non-existing comment, offset is ignored, deleted comments included into request with "url"
{fmt.Sprintf("limit=1&offset_id=%s", uuid.New().String()), `"info":{"count":7,"count_left":6,"last_comment":"` + ids[0]},
{fmt.Sprintf("limit=1&url=test-url&offset_id=%s", uuid.New().String()), `"info":{"url":"test-url","count":6,"count_left":7,"last_comment":"` + ids[0]},
// since is ignored for tree format, so we test it only for plain
{"limit=6&since=" + sinceTenSecondsAgo, `"info":{"count":7,"count_left":1,"last_comment":"` + ids[6]},
{"limit=1&since=" + sinceTS[4], `"info":{"count":3,"count_left":2,"last_comment":"` + ids[4]},
{"limit=6&url=test-url&since=" + sinceTenSecondsAgo, `"info":{"url":"test-url","count":6,"count_left":2,"last_comment":"` + ids[5]},
{"limit=1&url=test-url&since=" + sinceTS[4], `"info":{"url":"test-url","count":2,"count_left":3,"last_comment":"` + ids[4]},
// start with deleted comment timestamp
{"limit=1&since=" + sinceTS[5], `"info":{"count":2,"count_left":1,"last_comment":"` + ids[6]},
{"limit=1&since=" + sinceTS[6], `"info":{"count":2,"count_left":1,"last_comment":"` + ids[6]},
{"limit=1&url=test-url&since=" + sinceTS[5], `"info":{"url":"test-url","count":1,"count_left":2,"last_comment":"` + ids[5]},
{"limit=1&url=test-url&since=" + sinceTS[6], `"info":{"url":"test-url","count":1,"count_left":1,"last_comment":"` + ids[6]},
// test sort
{"limit=1&sort=+time&url=test-url", `"info":{"url":"test-url","count":6,"count_left":7,"last_comment":"` + ids[0]},
{"limit=1&sort=-time&url=test-url", `"info":{"url":"test-url","count":6,"count_left":7,"last_comment":"` + ids[7]},
{"limit=1&sort=+score&url=test-url", `"info":{"url":"test-url","count":6,"count_left":7,"last_comment":"` + ids[6]},
{"limit=1&sort=-score&url=test-url", `"info":{"url":"test-url","count":6,"count_left":7,"last_comment":"` + ids[2]},
{"limit=1&sort=+controversy&url=test-url", `"info":{"url":"test-url","count":6,"count_left":7,"last_comment":"` + ids[0]},
{"limit=1&sort=-controversy&url=test-url", `"info":{"url":"test-url","count":6,"count_left":7,"last_comment":"` + ids[3]},
// test parameters limit, offset_id for format=tree
{"format=tree&limit=bad", `{"code":1,"details":"bad limit value","error":"strconv.Atoi: parsing \"bad\": invalid syntax"}`},
{"format=tree&offset_id=bad", `{"code":1,"details":"bad offset_id value","error":"invalid UUID length: 3"}`},
{"format=tree&limit=2", `"info":{"count":7,"count_left":4,"last_comment":"` + ids[0]},
{"format=tree&limit=6", `"info":{"count":7,"count_left":2,"last_comment":"` + ids[1]},
{"format=tree&limit=7", `"info":{"count":7,"count_left":1,"last_comment":"` + ids[6]},
{"format=tree&url=test-url&limit=2", `"info":{"url":"test-url","count":6,"count_left":3,"last_comment":"` + ids[0]},
{"format=tree&url=test-url&limit=6", `"info":{"url":"test-url","count":6,"count_left":1,"last_comment":"` + ids[1]},
{"format=tree&url=test-url&limit=7", `"info":{"url":"test-url","count":6,"count_left":0,"last_comment":"` + ids[6]},
// start after first top-level comment
{fmt.Sprintf("format=tree&limit=2&offset_id=%s", ids[0]), `"info":{"count":7,"count_left":2,"last_comment":"` + ids[1]},
{fmt.Sprintf("format=tree&url=test-url&limit=2&offset_id=%s", ids[0]), `"info":{"url":"test-url","count":6,"count_left":1,"last_comment":"` + ids[1]},
// start after second top-level comment
{fmt.Sprintf("format=tree&limit=2&offset_id=%s", ids[1]), `"info":{"count":7,"count_left":1,"last_comment":"` + ids[6]},
{fmt.Sprintf("format=tree&url=test-url&limit=2&offset_id=%s", ids[1]), `"info":{"url":"test-url","count":6,"count_left":0,"last_comment":"` + ids[6]},
// start after third top-level comment, so expect comment to post 2, or no comments on post 1 if "url" is set
{fmt.Sprintf("format=tree&limit=1&offset_id=%s", ids[6]), `"info":{"count":7,"count_left":0,"last_comment":"` + ids[8]},
{fmt.Sprintf("format=tree&url=test-url&limit=1&offset_id=%s", ids[6]), `"info":{"url":"test-url","count":6,"count_left":0`},
// non-root comment IDs or non-existing IDs are ignored
{fmt.Sprintf("format=tree&limit=2&offset_id=%s", ids[2]), `"info":{"count":7,"count_left":4,"last_comment":"` + ids[0]},
{fmt.Sprintf("format=tree&limit=2&offset_id=%s", ids[3]), `"info":{"count":7,"count_left":4,"last_comment":"` + ids[0]},
{fmt.Sprintf("format=tree&limit=2&offset_id=%s", ids[4]), `"info":{"count":7,"count_left":4,"last_comment":"` + ids[0]},
{fmt.Sprintf("format=tree&limit=2&offset_id=%s", ids[7]), `"info":{"count":7,"count_left":4,"last_comment":"` + ids[0]},
{fmt.Sprintf("format=tree&limit=1&offset_id=%s", uuid.New().String()), `"info":{"count":7,"count_left":4,"last_comment":"` + ids[0]},
{fmt.Sprintf("format=tree&url=test-url&limit=2&offset_id=%s", ids[2]), `"info":{"url":"test-url","count":6,"count_left":3,"last_comment":"` + ids[0]},
{fmt.Sprintf("format=tree&url=test-url&limit=2&offset_id=%s", ids[3]), `"info":{"url":"test-url","count":6,"count_left":3,"last_comment":"` + ids[0]},
{fmt.Sprintf("format=tree&url=test-url&limit=2&offset_id=%s", ids[4]), `"info":{"url":"test-url","count":6,"count_left":3,"last_comment":"` + ids[0]},
{fmt.Sprintf("format=tree&url=test-url&limit=2&offset_id=%s", ids[7]), `"info":{"url":"test-url","count":6,"count_left":3,"last_comment":"` + ids[0]},
{fmt.Sprintf("format=tree&url=test-url&limit=1&offset_id=%s", uuid.New().String()), `"info":{"url":"test-url","count":6,"count_left":3,"last_comment":"` + ids[0]},
// test sort
{"format=tree&limit=1&sort=+time&url=test-url", `"info":{"url":"test-url","count":6,"count_left":3,"last_comment":"` + ids[0]},
{"format=tree&limit=1&sort=-time&url=test-url", `"info":{"url":"test-url","count":6,"count_left":5,"last_comment":"` + ids[6]},
{"format=tree&limit=1&sort=+score&url=test-url", `"info":{"url":"test-url","count":6,"count_left":5,"last_comment":"` + ids[6]},
{"format=tree&limit=1&sort=-score&url=test-url", `"info":{"url":"test-url","count":6,"count_left":4,"last_comment":"` + ids[1]},
{"format=tree&limit=1&sort=+controversy&url=test-url", `"info":{"url":"test-url","count":6,"count_left":3,"last_comment":"` + ids[0]},
{"format=tree&limit=1&sort=-controversy&url=test-url", `"info":{"url":"test-url","count":6,"count_left":5,"last_comment":"` + ids[6]},
}
for _, tc := range testCases {
t.Run(tc.params, func(t *testing.T) {
url := fmt.Sprintf(ts.URL+"/api/v1/find?site=remark42&%s", tc.params)
body, code := get(t, url)
assert.Equal(t, http.StatusOK, code)
expectedStatus := http.StatusOK
if strings.Contains(tc.params, "=bad") {
expectedStatus = http.StatusBadRequest
}
assert.Equal(t, expectedStatus, code)
assert.Contains(t, body, tc.expectedBody)
t.Log(body)
// prevent hit limiter from engaging
time.Sleep(50 * time.Millisecond)
time.Sleep(80 * time.Millisecond)
})
}
}
+4 -9
View File
@@ -16,10 +16,10 @@ import (
"testing"
"time"
"github.com/go-pkgz/auth"
"github.com/go-pkgz/auth/avatar"
"github.com/go-pkgz/auth/provider"
"github.com/go-pkgz/auth/token"
"github.com/go-pkgz/auth/v2"
"github.com/go-pkgz/auth/v2/avatar"
"github.com/go-pkgz/auth/v2/provider"
"github.com/go-pkgz/auth/v2/token"
cache "github.com/go-pkgz/lcw/v2"
R "github.com/go-pkgz/rest"
"github.com/stretchr/testify/assert"
@@ -227,7 +227,6 @@ func Test_URLKey(t *testing.T) {
}
for i, tt := range tbl {
tt := tt
t.Run(strconv.Itoa(i), func(t *testing.T) {
r, err := http.NewRequest("GET", tt.url, http.NoBody)
require.NoError(t, err)
@@ -252,7 +251,6 @@ func Test_URLKeyWithUser(t *testing.T) {
}
for i, tt := range tbl {
tt := tt
t.Run(strconv.Itoa(i), func(t *testing.T) {
r, err := http.NewRequest("GET", tt.url, http.NoBody)
require.NoError(t, err)
@@ -279,7 +277,6 @@ func TestRest_parseError(t *testing.T) {
}
for n, tt := range tbl {
tt := tt
t.Run(strconv.Itoa(n), func(t *testing.T) {
res := parseError(tt.err, rest.ErrInternal)
assert.Equal(t, tt.res, res)
@@ -302,7 +299,6 @@ func TestRest_cacheControl(t *testing.T) {
}
for i, tt := range tbl {
tt := tt
t.Run(strconv.Itoa(i), func(t *testing.T) {
req := httptest.NewRequest("GET", tt.url, http.NoBody)
w := httptest.NewRecorder()
@@ -388,7 +384,6 @@ func TestRest_subscribersOnly(t *testing.T) {
}
for i, tt := range tbl {
tt := tt
t.Run(strconv.Itoa(i), func(t *testing.T) {
req := httptest.NewRequest("GET", "http://example.com", http.NoBody)
if tt.setUser {
+4 -7
View File
@@ -271,10 +271,7 @@ func TestServer_RssReplies(t *testing.T) {
}
func waitOnSecChange() {
for {
if time.Now().Nanosecond() < 100000000 {
break
}
for time.Now().Nanosecond() >= 100000000 {
time.Sleep(10 * time.Nanosecond)
}
}
@@ -283,11 +280,11 @@ func waitOnSecChange() {
func cleanRssFormatting(expected, actual string) (cleanExp, cleanAct string) {
reSpaces := regexp.MustCompile(`[\s\p{Zs}]{2,}`)
expected = strings.Replace(expected, "\n", " ", -1)
expected = strings.Replace(expected, "\t", " ", -1)
expected = strings.ReplaceAll(expected, "\n", " ")
expected = strings.ReplaceAll(expected, "\t", " ")
expected = reSpaces.ReplaceAllString(expected, " ")
actual = strings.Replace(actual, "\n", " ", -1)
actual = strings.ReplaceAll(actual, "\n", " ")
actual = reSpaces.ReplaceAllString(actual, " ")
return expected, actual
}
+20 -5
View File
@@ -10,7 +10,6 @@ import (
"runtime"
"strings"
"github.com/go-chi/render"
log "github.com/go-pkgz/lgr"
"github.com/go-pkgz/rest"
@@ -67,20 +66,36 @@ func SendErrorHTML(w http.ResponseWriter, r *http.Request, httpStatusCode int, e
tmplstr := MustRead("error_response.html.tmpl")
tmpl := template.Must(template.New("error").Parse(tmplstr))
log.Printf("[WARN] %s", errDetailsMsg(r, httpStatusCode, err, details, errCode))
render.Status(r, httpStatusCode)
msg := bytes.Buffer{}
MustExecute(tmpl, &msg, errTmplData{
Error: err.Error(),
Details: details,
})
render.HTML(w, r, msg.String())
HTMLResponse(w, httpStatusCode, msg.String())
}
// SendErrorJSON makes {error: blah, details: blah, code: 42} json body and responds with error code
func SendErrorJSON(w http.ResponseWriter, r *http.Request, httpStatusCode int, err error, details string, errCode int) {
log.Printf("[WARN] %s", errDetailsMsg(r, httpStatusCode, err, details, errCode))
render.Status(r, httpStatusCode)
render.JSON(w, r, rest.JSON{"error": err.Error(), "details": details, "code": errCode})
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(httpStatusCode)
rest.RenderJSON(w, rest.JSON{"error": err.Error(), "details": details, "code": errCode})
}
// HTMLResponse writes HTML content with the given status code
func HTMLResponse(w http.ResponseWriter, status int, html string) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(status)
_, _ = w.Write([]byte(html))
}
// PlainTextResponse writes plain text content with the given status code
func PlainTextResponse(w http.ResponseWriter, status int, text string) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(status)
_, _ = w.Write([]byte(text))
}
func errDetailsMsg(r *http.Request, httpStatusCode int, err error, details string, errCode int) string {
+3 -3
View File
@@ -12,7 +12,7 @@ import (
"github.com/PuerkitoBio/goquery"
log "github.com/go-pkgz/lgr"
"github.com/go-pkgz/repeater"
"github.com/go-pkgz/repeater/v2"
"github.com/umputun/remark42/backend/app/rest"
"github.com/umputun/remark42/backend/app/store/image"
@@ -72,7 +72,7 @@ func (p Image) replace(commentHTML string, imgs []string) string {
for _, img := range imgs {
encodedImgURL := base64.URLEncoding.EncodeToString([]byte(img))
resImgURL := p.RemarkURL + p.RoutePath + "?src=" + encodedImgURL
commentHTML = strings.Replace(commentHTML, img, resImgURL, -1)
commentHTML = strings.ReplaceAll(commentHTML, img, resImgURL)
}
return commentHTML
@@ -151,7 +151,7 @@ func (p Image) downloadImage(ctx context.Context, imgURL string) ([]byte, error)
client := http.Client{Timeout: 30 * time.Second}
defer client.CloseIdleConnections()
var resp *http.Response
err := repeater.NewDefault(5, time.Second).Do(ctx, func() error {
err := repeater.NewFixed(5, time.Second).Do(ctx, func() error {
var e error
req, e := http.NewRequest("GET", imgURL, http.NoBody)
if e != nil {
+1 -2
View File
@@ -77,7 +77,6 @@ func TestImage_Extract(t *testing.T) {
img := Image{HTTP2HTTPS: true}
for i, tt := range tbl {
tt := tt
t.Run(strconv.Itoa(i), func(t *testing.T) {
res, err := img.extract(tt.inp, func(src string) bool { return strings.HasPrefix(src, "http://") })
assert.NoError(t, err)
@@ -263,7 +262,7 @@ func TestImage_RoutesTimedOut(t *testing.T) {
assert.NoError(t, resp.Body.Close())
require.NoError(t, err)
t.Log(string(b))
assert.True(t, strings.Contains(string(b), "deadline exceeded"))
assert.Contains(t, string(b), "deadline exceeded")
assert.Equal(t, 1, len(imageStore.LoadCalls()))
}
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"fmt"
"net/http"
"github.com/go-pkgz/auth/token"
"github.com/go-pkgz/auth/v2/token"
"github.com/umputun/remark42/backend/app/store"
)
+11 -9
View File
@@ -45,11 +45,13 @@ type Edit struct {
// PostInfo holds summary for given post url
type PostInfo struct {
URL string `json:"url,omitempty"` // can be attached to site-wide comments but won't be set then
Count int `json:"count"`
ReadOnly bool `json:"read_only,omitempty" bson:"read_only,omitempty"` // can be attached to site-wide comments but won't be set then
FirstTS time.Time `json:"first_time,omitempty" bson:"first_time,omitempty"`
LastTS time.Time `json:"last_time,omitempty" bson:"last_time,omitempty"`
URL string `json:"url,omitempty"` // can be attached to site-wide comments but won't be set then
Count int `json:"count"`
CountLeft int `json:"count_left"` // used only with returning search results limited by number, otherwise zero
LastComment string `json:"last_comment,omitempty"` // used only with returning search results limited by number
ReadOnly bool `json:"read_only,omitempty" bson:"read_only,omitempty"` // can be attached to site-wide comments but won't be set then
FirstTS time.Time `json:"first_time,omitempty" bson:"first_time,omitempty"`
LastTS time.Time `json:"last_time,omitempty" bson:"last_time,omitempty"`
}
// BlockedUser holds id and ts for blocked user
@@ -142,7 +144,7 @@ func (c *Comment) Snippet(limit int) string {
if limit <= 0 {
limit = snippetLen
}
cleanText := strings.Replace(c.Text, "\n", " ", -1)
cleanText := strings.ReplaceAll(c.Text, "\n", " ")
size := len([]rune(cleanText))
if size < limit {
return cleanText
@@ -177,9 +179,9 @@ func (c *Comment) SanitizeAsURL(inp string) string {
func (c *Comment) escapeHTMLWithSome(inp string) string {
res := template.HTMLEscapeString(inp)
res = strings.Replace(res, "&amp;", "&", -1)
res = strings.Replace(res, "&#34;", "\"", -1)
res = strings.Replace(res, "&#39;", "'", -1)
res = strings.ReplaceAll(res, "&amp;", "&")
res = strings.ReplaceAll(res, "&#34;", "\"")
res = strings.ReplaceAll(res, "&#39;", "'")
return res
}
-3
View File
@@ -209,7 +209,6 @@ func TestComment_Snippet(t *testing.T) {
}
for i, tt := range tbl {
tt := tt
t.Run(strconv.Itoa(i), func(t *testing.T) {
c := Comment{Text: tt.inp}
out := c.Snippet(tt.limit)
@@ -245,7 +244,6 @@ func TestComment_sanitizeAsURL(t *testing.T) {
}
for i, tt := range tbl {
tt := tt
c := Comment{}
t.Run(strconv.Itoa(i), func(t *testing.T) {
assert.Equal(t, tt.out, c.SanitizeAsURL(tt.inp))
@@ -276,7 +274,6 @@ func TestComment_sanitizeText(t *testing.T) {
}
for i, tt := range tbl {
tt := tt
c := Comment{}
t.Run(strconv.Itoa(i), func(t *testing.T) {
assert.Equal(t, tt.out, c.SanitizeText(tt.inp))
-1
View File
@@ -865,7 +865,6 @@ func (b *BoltDB) deleteUser(bdb *bolt.DB, siteID, userID string, mode store.Dele
// get list of commentID for all user's comment
comments := []commentInfo{}
for _, postInfo := range posts {
postInfo := postInfo
err = bdb.View(func(tx *bolt.Tx) error {
postsBkt := tx.Bucket([]byte(postsBucketName))
postBkt := postsBkt.Bucket([]byte(postInfo.URL))
+1 -1
View File
@@ -324,7 +324,7 @@ func TestBoltDB_InfoPost(t *testing.T) {
b, teardown := prep(t) // two comments for https://radio-t.com
defer teardown()
ts := func(min int) time.Time { return time.Date(2017, 12, 20, 15, 18, min, 0, time.Local) }
ts := func(minute int) time.Time { return time.Date(2017, 12, 20, 15, 18, minute, 0, time.Local) }
// add one more for https://radio-t.com/2
comment := store.Comment{
+1 -1
View File
@@ -99,7 +99,7 @@ func (f *CommentFormatter) unEscape(txt string) (res string) {
}
res = txt
for _, e := range elems {
res = strings.Replace(res, e.from, e.to, -1)
res = strings.ReplaceAll(res, e.from, e.to)
}
return res
}
+2 -4
View File
@@ -43,14 +43,13 @@ func TestFormatter_FormatText(t *testing.T) {
{`no_smartpants «quoted» text`, "<p>no_smartpants «quoted» text</p>\n!converted", "French guillemets without smartpants"},
{`«quoted» text`, "<p>«quoted» text</p>\n!converted", "French guillemets with smartpants"},
{"smth\n```go\nfunc main(aa string) int {return 0}\n```", `<p>smth</p>
<pre class="chroma"><code><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">main</span><span class="p">(</span><span class="nx">aa</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">int</span> <span class="p">{</span><span class="k">return</span> <span class="mi">0</span><span class="p">}</span>
</span></span></code></pre>!converted`, "code with language"},
<pre class="chroma"><code><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">main</span><span class="p">(</span><span class="nx">aa</span><span class="w"> </span><span class="kt">string</span><span class="p">)</span><span class="w"> </span><span class="kt">int</span><span class="w"> </span><span class="p">{</span><span class="k">return</span><span class="w"> </span><span class="mi">0</span><span class="p">}</span><span class="w">
</span></span></span></code></pre>!converted`, "code with language"},
{"```\ntest_code\n```", `<pre class="chroma"><code><span class="line"><span class="cl">test_code
</span></span></code></pre>!converted`, "code without language"},
}
f := NewCommentFormatter(mockConverter{})
for _, tt := range tbl {
tt := tt
t.Run(tt.name, func(t *testing.T) {
raw := strings.HasPrefix(tt.in, `no_smartpants`)
t.Logf("raw: %v", raw)
@@ -146,7 +145,6 @@ func TestCommentFormatter_lazyImage(t *testing.T) {
f := NewCommentFormatter(nil)
for i, tt := range tbl {
tt := tt
t.Run(strconv.Itoa(i), func(t *testing.T) {
assert.Equal(t, tt.out, f.lazyImage(tt.inp))
})
-1
View File
@@ -173,7 +173,6 @@ func TestFsStore_location(t *testing.T) {
{0, "user/12345", "/tmp/user/12345"},
}
for n, tt := range tbl {
tt := tt
t.Run(strconv.Itoa(n), func(t *testing.T) {
svc := FileSystem{Location: "/tmp", Partitions: tt.partitions}
assert.Equal(t, tt.res, svc.location("/tmp", tt.id))
-1
View File
@@ -259,7 +259,6 @@ func TestGetProportionalSizes(t *testing.T) {
}
for i, tt := range tbl {
tt := tt
t.Run(strconv.Itoa(i), func(t *testing.T) {
resW, resH := getProportionalSizes(tt.inpW, tt.inpH, tt.limitW, tt.limitH)
assert.Equal(t, tt.resW, resW, "width")
+3 -5
View File
@@ -659,10 +659,8 @@ func (s *DataStore) ValidateComment(c *store.Comment) error {
parser := bf.New(bf.WithRenderer(rend), bf.WithExtensions(bf.CommonExtensions), bf.WithExtensions(mdExt))
var wrongLinkError error
parser.Parse([]byte(c.Orig)).Walk(func(node *bf.Node, _ bool) bf.WalkStatus {
if len(node.LinkData.Destination) != 0 &&
!(strings.HasPrefix(string(node.LinkData.Destination), "http://") ||
strings.HasPrefix(string(node.LinkData.Destination), "https://") ||
strings.HasPrefix(string(node.LinkData.Destination), "mailto:")) {
if len(node.Destination) != 0 &&
(!strings.HasPrefix(string(node.Destination), "http://") && !strings.HasPrefix(string(node.Destination), "https://") && !strings.HasPrefix(string(node.Destination), "mailto:")) {
wrongLinkError = fmt.Errorf("links should start with mailto:, http:// or https://")
return bf.Terminate
}
@@ -981,7 +979,7 @@ func (s *DataStore) Last(siteID string, limit int, since time.Time, user store.U
func (s *DataStore) Close() error {
errs := new(multierror.Error)
if s.repliesCache.LoadingCache != nil {
errs = multierror.Append(errs, s.repliesCache.LoadingCache.Close())
errs = multierror.Append(errs, s.repliesCache.Close())
}
if s.TitleExtractor != nil {
errs = multierror.Append(errs, s.TitleExtractor.Close())
@@ -771,7 +771,6 @@ func TestService_Controversy(t *testing.T) {
b := DataStore{}
for i, tt := range tbl {
tt := tt
t.Run(fmt.Sprintf("check-%d-%d:%d", i, tt.ups, tt.downs), func(t *testing.T) {
assert.InDelta(t, tt.res, b.controversy(tt.ups, tt.downs), 0.01)
})
+1 -1
View File
@@ -116,7 +116,7 @@ func (t *TitleExtractor) isTitleElement(n *html.Node) bool {
func (t *TitleExtractor) traverse(n *html.Node) (string, bool) {
if t.isTitleElement(n) {
title := n.FirstChild.Data
title = strings.Replace(title, "\n", "", -1)
title = strings.ReplaceAll(title, "\n", "")
title = strings.TrimSpace(title)
return title, true
}
-1
View File
@@ -31,7 +31,6 @@ func TestTitle_GetTitle(t *testing.T) {
ex := NewTitleExtractor(http.Client{Timeout: 5 * time.Second}, []string{})
defer ex.Close()
for i, tt := range tbl {
tt := tt
t.Run(fmt.Sprintf("check-%d", i), func(t *testing.T) {
title, ok := ex.getTitle(strings.NewReader(tt.page))
assert.Equal(t, tt.ok, ok)
+93 -1
View File
@@ -11,6 +11,9 @@ import (
// Tree is formatter making tree from the list of comments
type Tree struct {
Nodes []*Node `json:"comments"`
countLeft int
lastLimitedComment string
}
// Node is a comment with optional replies
@@ -29,7 +32,7 @@ type recurData struct {
}
// MakeTree gets unsorted list of comments and produces Tree
func MakeTree(comments []store.Comment, sortType string) *Tree {
func MakeTree(comments []store.Comment, sortType string, limit int, offsetID string) *Tree {
if len(comments) == 0 {
return &Tree{}
}
@@ -53,9 +56,20 @@ func MakeTree(comments []store.Comment, sortType string) *Tree {
}
res.sortNodes(sortType)
res.limit(limit, offsetID)
return &res
}
// CountLeft returns number of comments left after limit, 0 if no limit was set
func (t *Tree) CountLeft() int {
return t.countLeft
}
// LastComment returns ID of the last comment in the tree after limit, empty string if no limit was set
func (t *Tree) LastComment() string {
return t.lastLimitedComment
}
// proc makes tree for one top-level comment recursively
func (t *Tree) proc(comments []store.Comment, node *Node, rd *recurData, parentID string) (result *Node) {
if rd.tsModified.IsZero() || rd.tsCreated.IsZero() {
@@ -145,3 +159,81 @@ func (t *Tree) sortNodes(sortType string) {
}
})
}
// limit limits number of comments in tree and sets countLeft and lastLimitedComment,
// starting with comment next after offsetID.
//
// If offsetID is empty or invalid, it starts from the beginning. If limit is 0, it doesn't limit anything.
//
// Limit is applied to top-level comments only, so top-level comments only returned with all replies,
// and lastLimitedComment is set to the last top-level comment and not last reply in it.
//
// In case limit is less than the number of replies to first comment after given offset, that first comment is
// returned completely with all replies.
func (t *Tree) limit(limit int, offsetID string) {
if offsetID == "" && limit <= 0 {
return
}
start := 0
if offsetID != "" {
for i, n := range t.Nodes {
if n.Comment.ID == offsetID {
start = i + 1
break
}
}
}
if start == len(t.Nodes) { // If the start index is beyond the available nodes, clear the nodes
t.Nodes = []*Node{}
return
}
t.Nodes = t.Nodes[start:]
// if there is only offset and no limit, there are no comments left and no point in returning
// the last comment ID as there are no comments beyond it.
if limit <= 0 {
return
}
// Traverse and limit the number of top-level nodes, including their replies
limitedNodes := []*Node{}
commentsCount := 0
for _, node := range t.Nodes {
repliesCount := countReplies(node) + 1 // Count this node and its replies
// If the limit is already reached or exceeded, calculate countLeft and move to the next node
if commentsCount >= limit {
t.countLeft += repliesCount
continue
}
// Check if we just exceeded the limit and there are already some nodes in the list,
// as otherwise we would have to return the first node with all its replies even if it exceeds the limit.
if commentsCount+repliesCount >= limit && len(limitedNodes) > 0 {
t.countLeft += repliesCount
commentsCount = limit // Adjust commentsCount to stop checking limit for the next nodes
continue
}
// Add the node and its replies to the list
limitedNodes = append(limitedNodes, node)
commentsCount += repliesCount
}
t.lastLimitedComment = limitedNodes[len(limitedNodes)-1].Comment.ID
t.Nodes = limitedNodes
}
// countReplies counts the total number of replies recursively for a given node.
func countReplies(node *Node) int {
count := 0
for _, reply := range node.Replies {
count++ // Count the reply itself
count += countReplies(reply) // Recursively count its replies
}
return count
}
+16 -16
View File
@@ -14,7 +14,7 @@ import (
func TestMakeTree(t *testing.T) {
loc := store.Locator{URL: "url", SiteID: "site"}
ts := func(min, sec int) time.Time { return time.Date(2017, 12, 25, 19, min, sec, 0, time.UTC) }
ts := func(minute, second int) time.Time { return time.Date(2017, 12, 25, 19, minute, second, 0, time.UTC) }
// unsorted by purpose
comments := []store.Comment{
@@ -37,20 +37,20 @@ func TestMakeTree(t *testing.T) {
{Locator: loc, ID: "611", ParentID: "61", Deleted: true},
}
res := MakeTree(comments, "time")
res := MakeTree(comments, "time", 0, "")
resJSON, err := json.Marshal(&res)
require.NoError(t, err)
expJSON := mustLoadJSONFile(t, "testdata/tree.json")
assert.Equal(t, expJSON, resJSON)
res = MakeTree([]store.Comment{}, "time")
res = MakeTree([]store.Comment{}, "time", 0, "")
assert.Equal(t, &Tree{}, res)
}
func TestMakeEmptySubtree(t *testing.T) {
loc := store.Locator{URL: "url", SiteID: "site"}
ts := func(min, sec int) time.Time { return time.Date(2017, 12, 25, 19, min, sec, 0, time.UTC) }
ts := func(minute, second int) time.Time { return time.Date(2017, 12, 25, 19, minute, second, 0, time.UTC) }
// unsorted by purpose
comments := []store.Comment{
@@ -75,7 +75,7 @@ func TestMakeEmptySubtree(t *testing.T) {
{Locator: loc, ID: "3", Timestamp: ts(48, 1), Deleted: true}, // deleted top level
}
res := MakeTree(comments, "time")
res := MakeTree(comments, "time", 0, "")
resJSON, err := json.Marshal(&res)
require.NoError(t, err)
t.Log(string(resJSON))
@@ -104,50 +104,50 @@ func TestTreeSortNodes(t *testing.T) {
{ID: "5", Deleted: true, Timestamp: time.Date(2017, 12, 25, 19, 47, 22, 150, time.UTC)},
}
res := MakeTree(comments, "+active")
res := MakeTree(comments, "+active", 0, "")
assert.Equal(t, "2", res.Nodes[0].Comment.ID)
t.Log(res.Nodes[0].Comment.ID, res.Nodes[0].tsModified)
res = MakeTree(comments, "-active")
res = MakeTree(comments, "-active", 0, "")
t.Log(res.Nodes[0].Comment.ID, res.Nodes[0].tsModified)
assert.Equal(t, "1", res.Nodes[0].Comment.ID)
res = MakeTree(comments, "+time")
res = MakeTree(comments, "+time", 0, "")
t.Log(res.Nodes[0].Comment.ID, res.Nodes[0].tsModified)
assert.Equal(t, "1", res.Nodes[0].Comment.ID)
res = MakeTree(comments, "-time")
res = MakeTree(comments, "-time", 0, "")
assert.Equal(t, "6", res.Nodes[0].Comment.ID)
res = MakeTree(comments, "score")
res = MakeTree(comments, "score", 0, "")
assert.Equal(t, "4", res.Nodes[0].Comment.ID)
assert.Equal(t, "3", res.Nodes[1].Comment.ID)
assert.Equal(t, "6", res.Nodes[2].Comment.ID)
assert.Equal(t, "1", res.Nodes[3].Comment.ID)
res = MakeTree(comments, "+score")
res = MakeTree(comments, "+score", 0, "")
assert.Equal(t, "4", res.Nodes[0].Comment.ID)
res = MakeTree(comments, "-score")
res = MakeTree(comments, "-score", 0, "")
assert.Equal(t, "2", res.Nodes[0].Comment.ID)
assert.Equal(t, "1", res.Nodes[1].Comment.ID)
assert.Equal(t, "3", res.Nodes[2].Comment.ID)
assert.Equal(t, "6", res.Nodes[3].Comment.ID)
res = MakeTree(comments, "+controversy")
res = MakeTree(comments, "+controversy", 0, "")
assert.Equal(t, "3", res.Nodes[0].Comment.ID)
assert.Equal(t, "6", res.Nodes[1].Comment.ID)
assert.Equal(t, "2", res.Nodes[2].Comment.ID)
assert.Equal(t, "4", res.Nodes[3].Comment.ID)
assert.Equal(t, "1", res.Nodes[4].Comment.ID)
res = MakeTree(comments, "-controversy")
res = MakeTree(comments, "-controversy", 0, "")
assert.Equal(t, "1", res.Nodes[0].Comment.ID)
assert.Equal(t, "4", res.Nodes[1].Comment.ID)
assert.Equal(t, "2", res.Nodes[2].Comment.ID)
assert.Equal(t, "3", res.Nodes[3].Comment.ID)
res = MakeTree(comments, "undefined")
res = MakeTree(comments, "undefined", 0, "")
t.Log(res.Nodes[0].Comment.ID, res.Nodes[0].tsModified)
assert.Equal(t, "1", res.Nodes[0].Comment.ID)
}
@@ -160,7 +160,7 @@ func BenchmarkTree(b *testing.B) {
assert.NoError(b, err)
for i := 0; i < b.N; i++ {
res := MakeTree(comments, "time")
res := MakeTree(comments, "time", 0, "")
assert.NotNil(b, res)
}
}
+38 -39
View File
@@ -1,74 +1,73 @@
module github.com/umputun/remark42/backend
go 1.21
go 1.25
require (
github.com/Depado/bfchroma/v2 v2.0.0
github.com/PuerkitoBio/goquery v1.9.2
github.com/alecthomas/chroma/v2 v2.13.0
github.com/didip/tollbooth/v7 v7.0.2
github.com/didip/tollbooth_chi v0.0.0-20220719025231-d662a7f6928f
github.com/go-chi/chi/v5 v5.1.0
github.com/go-chi/cors v1.2.1
github.com/go-chi/render v1.0.3
github.com/go-pkgz/auth v1.24.2
github.com/go-pkgz/jrpc v0.3.0
github.com/PuerkitoBio/goquery v1.11.0
github.com/alecthomas/chroma/v2 v2.21.1
github.com/didip/tollbooth/v8 v8.0.1
github.com/go-chi/chi/v5 v5.2.3
github.com/go-chi/cors v1.2.2
github.com/go-pkgz/auth/v2 v2.1.1
github.com/go-pkgz/jrpc v0.4.0
github.com/go-pkgz/lcw/v2 v2.0.0
github.com/go-pkgz/lgr v0.11.1
github.com/go-pkgz/notify v1.1.1
github.com/go-pkgz/repeater v1.1.3
github.com/go-pkgz/rest v1.19.0
github.com/go-pkgz/lgr v0.12.1
github.com/go-pkgz/notify v1.3.0
github.com/go-pkgz/repeater/v2 v2.2.0
github.com/go-pkgz/rest v1.20.6
github.com/go-pkgz/syncs v1.3.2
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/google/uuid v1.6.0
github.com/gorilla/feeds v1.2.0
github.com/hashicorp/go-multierror v1.1.1
github.com/jessevdk/go-flags v1.6.1
github.com/kyokomi/emoji/v2 v2.2.13
github.com/microcosm-cc/bluemonday v1.0.27
github.com/rs/xid v1.5.0
github.com/rs/xid v1.6.0
github.com/russross/blackfriday/v2 v2.1.0
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/stretchr/testify v1.9.0
go.etcd.io/bbolt v1.3.10
github.com/stretchr/testify v1.11.1
go.etcd.io/bbolt v1.4.3
go.uber.org/goleak v1.3.0
golang.org/x/crypto v0.25.0
golang.org/x/image v0.18.0
golang.org/x/net v0.27.0
golang.org/x/crypto v0.46.0
golang.org/x/image v0.34.0
golang.org/x/net v0.48.0
)
require (
cloud.google.com/go/compute/metadata v0.5.0 // indirect
github.com/ajg/form v1.5.1 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dghubble/oauth1 v0.7.3 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/go-oauth2/oauth2/v4 v4.5.2 // indirect
github.com/go-pkgz/email v0.5.0 // indirect
github.com/go-pkgz/expirable-cache/v3 v3.0.0 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/go-oauth2/oauth2/v4 v4.5.4 // indirect
github.com/go-pkgz/email v0.6.0 // indirect
github.com/go-pkgz/expirable-cache/v3 v3.1.0 // indirect
github.com/go-pkgz/repeater v1.2.0 // indirect
github.com/go-pkgz/routegroup v1.6.0 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/redis/go-redis/v9 v9.4.0 // indirect
github.com/redis/go-redis/v9 v9.17.2 // indirect
github.com/rrivera/identicon v0.0.0-20240116195454-d5ba35832c0d // indirect
github.com/slack-go/slack v0.12.5 // indirect
github.com/slack-go/slack v0.17.3 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/scram v1.2.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.mongodb.org/mongo-driver v1.16.0 // indirect
golang.org/x/oauth2 v0.21.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
go.mongodb.org/mongo-driver v1.17.6 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+130 -209
View File
@@ -1,128 +1,96 @@
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
github.com/Depado/bfchroma/v2 v2.0.0 h1:IRpN9BPkNwEpR6w1ectIcNWOuhDSLx+8f1pn83fzxx8=
github.com/Depado/bfchroma/v2 v2.0.0/go.mod h1:wFwW/Pw8Tnd0irzgO9Zxtxgzp3aPS8qBWlyadxujxmw=
github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE=
github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU=
github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.13.0 h1:VP72+99Fb2zEcYM0MeaWJmV+xQvz5v5cxRHd+ooU1lI=
github.com/alecthomas/chroma/v2 v2.13.0/go.mod h1:BUGjjsD+ndS6eX37YgTchSEG+Jg9Jv1GiZs9sqPqztk=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.21.1 h1:FaSDrp6N+3pphkNKU6HPCiYLgm8dbe5UXIXcoBhZSWA=
github.com/alecthomas/chroma/v2 v2.21.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE=
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/alicebob/miniredis/v2 v2.31.1 h1:7XAt0uUg3DtwEKW5ZAGa+K7FZV2DdKQo5K/6TTnfX8Y=
github.com/alicebob/miniredis/v2 v2.31.1/go.mod h1:UB/T2Uztp7MlFSDakaX1sTXUv5CASoprx0wulRT6HBg=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dghubble/oauth1 v0.7.3 h1:EkEM/zMDMp3zOsX2DC/ZQ2vnEX3ELK0/l9kb+vs4ptE=
github.com/dghubble/oauth1 v0.7.3/go.mod h1:oxTe+az9NSMIucDPDCCtzJGsPhciJV33xocHfcR2sVY=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/didip/tollbooth/v7 v7.0.0/go.mod h1:VZhDSGl5bDSPj4wPsih3PFa4Uh9Ghv8hgacaTm5PRT4=
github.com/didip/tollbooth/v7 v7.0.2 h1:WYEfusYI6g64cN0qbZgekDrYfuYBZjUZd5+RlWi69p4=
github.com/didip/tollbooth/v7 v7.0.2/go.mod h1:RtRYfEmFGX70+ike5kSndSvLtQ3+F2EAmTI4Un/VXNc=
github.com/didip/tollbooth_chi v0.0.0-20220719025231-d662a7f6928f h1:jtKwihcLmUC9BAhoJ9adCUqdSSZcOdH2KL7mPTUm2aw=
github.com/didip/tollbooth_chi v0.0.0-20220719025231-d662a7f6928f/go.mod h1:q9C80dnsuVRP2dAskjnXRNWdUJqtGgwG9wNrzt0019s=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
github.com/didip/tollbooth/v8 v8.0.1 h1:VAAapTo1t4Bn6bbpcHjuovwoa9u3JH++wgjbpWv+rB8=
github.com/didip/tollbooth/v8 v8.0.1/go.mod h1:oEd9l+ep373d7DmvKLc0a5gasPOev2mTewi6KPQBGJ4=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/gavv/httpexpect v2.0.0+incompatible h1:1X9kcRshkSKEjNJJxX9Y9mQ5BRfbxU5kORdjhlA1yX8=
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-oauth2/oauth2/v4 v4.5.2 h1:CuZhD3lhGuI6aNLyUbRHXsgG2RwGRBOuCBfd4WQKqBQ=
github.com/go-oauth2/oauth2/v4 v4.5.2/go.mod h1:wk/2uLImWIa9VVQDgxz99H2GDbhmfi/9/Xr+GvkSUSQ=
github.com/go-pkgz/auth v1.24.0 h1:jo7Ke18nFGTWHdzmMKeF8zqXIC383Ptu/OXCkcQOZmI=
github.com/go-pkgz/auth v1.24.0/go.mod h1:xmnzq6g8mhemW1nHnkuByXkBXsHrNf9/qkiVwJugWIs=
github.com/go-pkgz/auth v1.24.1-0.20240919232608-9e446b888187 h1:1oHLySWdk0HfDIFzXFNqA6dEK7sTgZ/7s+l/YxYql7Q=
github.com/go-pkgz/auth v1.24.1-0.20240919232608-9e446b888187/go.mod h1:xmnzq6g8mhemW1nHnkuByXkBXsHrNf9/qkiVwJugWIs=
github.com/go-pkgz/auth v1.24.1 h1:izSFGxwNEZ2MujKJWXddKc+lUW+kVP02JBXouQIW8b4=
github.com/go-pkgz/auth v1.24.1/go.mod h1:xmnzq6g8mhemW1nHnkuByXkBXsHrNf9/qkiVwJugWIs=
github.com/go-pkgz/auth v1.24.2-0.20240921022538-30916c085e04 h1:DZzcdFb/EzXjQex8lDq/NqubVqwCXeyGxU5swYEVAYk=
github.com/go-pkgz/auth v1.24.2-0.20240921022538-30916c085e04/go.mod h1:xmnzq6g8mhemW1nHnkuByXkBXsHrNf9/qkiVwJugWIs=
github.com/go-pkgz/auth v1.24.2 h1:imMjUvTM0c8iOvP/GNGcuNcB/7gF3jFTF9dIPzlAOqI=
github.com/go-pkgz/auth v1.24.2/go.mod h1:xmnzq6g8mhemW1nHnkuByXkBXsHrNf9/qkiVwJugWIs=
github.com/go-pkgz/email v0.5.0 h1:fdtMDGJ8NwyBACLR0LYHaCIK/OeUwZHMhH7Q0+oty9U=
github.com/go-pkgz/email v0.5.0/go.mod h1:BdxglsQnymzhfdbnncEE72a6DrucZHy6I+42LK2jLEc=
github.com/go-pkgz/expirable-cache v0.1.0/go.mod h1:GTrEl0X+q0mPNqN6dtcQXksACnzCBQ5k/k1SwXJsZKs=
github.com/go-pkgz/expirable-cache/v3 v3.0.0 h1:u3/gcu3sabLYiTCevoRKv+WzjIn5oo7P8XtiXBeRDLw=
github.com/go-pkgz/expirable-cache/v3 v3.0.0/go.mod h1:2OQiDyEGQalYecLWmXprm3maPXeVb5/6/X7yRPYTzec=
github.com/go-pkgz/jrpc v0.3.0 h1:Fls38KqPsHzvp0FWfivr6cGnncC+iFBodHBqvUPY+0U=
github.com/go-pkgz/jrpc v0.3.0/go.mod h1:MFtKs75JESiSqVicsQkgN2iDFFuCd3gVT1/vKiwRi00=
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-oauth2/oauth2/v4 v4.5.4 h1:YjI0tmGW8oxVhn9QSBIxlr641QugWrJY5UWa6XmLcW0=
github.com/go-oauth2/oauth2/v4 v4.5.4/go.mod h1:BXiOY+QZtZy2ewbsGk2B5P8TWmtz/Rf7ES5ZttQFxfQ=
github.com/go-pkgz/auth/v2 v2.1.1 h1:CBH3Z6ovLT51Nx9TkBcu2L8Dd/xwL6CgJcgqUnC2isQ=
github.com/go-pkgz/auth/v2 v2.1.1/go.mod h1:9LwzESczjMavmXNZo1XhYpfYdKWtoCbXt/ZIi0GTvF0=
github.com/go-pkgz/email v0.6.0 h1:snZnXldjeF4PgKSjnx9Fa25mtOgFpAOEeWvnQvrxjLE=
github.com/go-pkgz/email v0.6.0/go.mod h1:+wgi4x7S33IuCzfcCM5euN0GwQG6XvO/PBLxrNffYLI=
github.com/go-pkgz/expirable-cache/v3 v3.1.0 h1:s05P851/O6QJ6Mc+7o2bh9aGtD3romB1SxDTXifdoqc=
github.com/go-pkgz/expirable-cache/v3 v3.1.0/go.mod h1:6pVgNleydKPj0J2/mzrI02/RDo4ivKx5v2XlNmIjhjo=
github.com/go-pkgz/jrpc v0.4.0 h1:oD7xiGrzDkndkuCjeHGugQXxbggLSV7O1QmHhoc5pYY=
github.com/go-pkgz/jrpc v0.4.0/go.mod h1:JFoY3bRjRyx4M3CbEVDFQStMB1m2gmQ7OjqFK7q3kOo=
github.com/go-pkgz/lcw/v2 v2.0.0 h1:gTwXpiJBhQeA1rXuqkRuLcV79uATFna8CckH8ZBBrH0=
github.com/go-pkgz/lcw/v2 v2.0.0/go.mod h1:yxJHOn+IbQBQHxUqkCtMrbGjIfdYcsBAZcVCBaL1Va8=
github.com/go-pkgz/lgr v0.11.1 h1:hXFhZcznehI6imLhEa379oMOKFz7TQUmisAqb3oLOSM=
github.com/go-pkgz/lgr v0.11.1/go.mod h1:tgDF4RXQnBfIgJqjgkv0yOeTQ3F1yewWIZkpUhHnAkU=
github.com/go-pkgz/notify v1.1.1 h1:xjd5r0a9WUIKutrHkHfuNWdoXZsm84WNAoG35MD8QZA=
github.com/go-pkgz/notify v1.1.1/go.mod h1:8ri5Zx+IAjc87HB9kF9+KnVTm0yO5agsphvPbcRo54U=
github.com/go-pkgz/repeater v1.1.3 h1:q6+JQF14ESSy28Dd7F+wRelY4F+41HJ0LEy/szNnMiE=
github.com/go-pkgz/repeater v1.1.3/go.mod h1:hVTavuO5x3Gxnu8zW7d6sQBfAneKV8X2FjU48kGfpKw=
github.com/go-pkgz/rest v1.15.6/go.mod h1:KUWAqbDteYGS/CiXftomQsKjtEOifXsJ36Ka0skYbmk=
github.com/go-pkgz/rest v1.19.0 h1:FNMi5QX5dDIkuC+/e0r+CWsTuOTwUiWMRSA16Ou+9+A=
github.com/go-pkgz/rest v1.19.0/go.mod h1:Po+W6zQzpMPP6XDGLdAN2aW7UKk1IyrLSb48Lp1N3oQ=
github.com/go-pkgz/lgr v0.12.1 h1:8GVfG2rSARq3Eaj5PP158rtBR2LHVGkwioIkQBGbvKg=
github.com/go-pkgz/lgr v0.12.1/go.mod h1:A4AxjOthFVFK6jRnVYMeusno5SeDAxcLVHd0kI/lN/Y=
github.com/go-pkgz/notify v1.3.0 h1:YxF/ThEoCetdcoghWdyeqaBpCkZ8mvyve7HXbCAOzYU=
github.com/go-pkgz/notify v1.3.0/go.mod h1:qdfi5OsViKlIFPryIOaINHTOtS9GFhOYXPqJmAMlaGU=
github.com/go-pkgz/repeater v1.2.0 h1:oJFvjyKdTDd5RCzpzxlzYIZFFj6Zfl17rE1aUfu6UjQ=
github.com/go-pkgz/repeater v1.2.0/go.mod h1:vypP6xamA53MFmafnGUucqOmALKk36xgKu2hSG73LHM=
github.com/go-pkgz/repeater/v2 v2.2.0 h1:8nZR/NaknmLfx2YMHbr78u9OL4Xj+8+romm9dz4FpMg=
github.com/go-pkgz/repeater/v2 v2.2.0/go.mod h1:RgX5vUbLKq7PV82QUDP5pFbQS1os4Z+U9XzKymK23A8=
github.com/go-pkgz/rest v1.20.6 h1:O/IhQ3I2cS4bJYvL1TLcy63w2OcXZTTBG3R+wTIqPS4=
github.com/go-pkgz/rest v1.20.6/go.mod h1:NY+MX1is2kJckJt+nHDNovS/5j9jmF4yQuSno4qg7XU=
github.com/go-pkgz/routegroup v1.6.0 h1:44XHZgF6JIIldRlv+zjg6SygULASmjifnfIQjwCT0e4=
github.com/go-pkgz/routegroup v1.6.0/go.mod h1:Pmu04fhgWhRtBMIJ8HXppnnzOPjnL/IEPBIdO2zmeqg=
github.com/go-pkgz/syncs v1.3.2 h1:gmioASlJNy3gNosPlgvWOM2QP0Hdjzn2u+/sUShgd8E=
github.com/go-pkgz/syncs v1.3.2/go.mod h1:qjgzpp7OpuhDf7BWsW/FHCu9DLjE32NPy6/vXAXT/Cw=
github.com/go-session/session v3.1.2+incompatible/go.mod h1:8B3iivBQjrz/JtC68Np2T1yBBLxTan3mn/3OM0CyRt0=
github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
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/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/gorilla/feeds v1.2.0 h1:O6pBiXJ5JHhPvqy53NsjKOThq+dNFm8+DFrxBEdzSCc=
github.com/gorilla/feeds v1.2.0/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -134,107 +102,74 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk=
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4=
github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
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/kyokomi/emoji/v2 v2.2.13 h1:GhTfQa67venUUvmleTNFnb+bi7S3aocF7ZCXU9fSO7U=
github.com/kyokomi/emoji/v2 v2.2.13/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE=
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs=
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.4.0 h1:Yzoz33UZw9I/mFhx4MNrB6Fk+XHO1VukNcCa1+lwyKk=
github.com/redis/go-redis/v9 v9.4.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rrivera/identicon v0.0.0-20240116195454-d5ba35832c0d h1:l3+2LWCbVxn5itfvXAfH9n4YL9jh8l1g5zcncbIc1cs=
github.com/rrivera/identicon v0.0.0-20240116195454-d5ba35832c0d/go.mod h1:TbpErkob6SY7cyozRVSGoB3OlO2qOAgVN8O3KAJ4fMI=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
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/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/slack-go/slack v0.12.5 h1:ddZ6uz6XVaB+3MTDhoW04gG+Vc/M/X1ctC+wssy2cqs=
github.com/slack-go/slack v0.12.5/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/slack-go/slack v0.17.3 h1:zV5qO3Q+WJAQ/XwbGfNFrRMaJ5T/naqaonyPV/1TP4g=
github.com/slack-go/slack v0.17.3/go.mod h1:X+UqOufi3LYQHDnMG1vxf0J8asC6+WllXrVrhl8/Prk=
github.com/smartystreets/assertions v1.1.0 h1:MkTeG1DMwsrdH7QtLXy5W+fUxWq+vmb6cLmyJ7aRtF0=
github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8=
github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI=
github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY=
github.com/tidwall/buntdb v1.1.2/go.mod h1:xAzi36Hir4FarpSHyfuZ6JzPJdjRZ8QlLZSntE2mqlI=
github.com/tidwall/buntdb v1.3.1 h1:HKoDF01/aBhl9RjYtbaLnvX9/OuenwvQiC3OP1CcL4o=
github.com/tidwall/buntdb v1.3.1/go.mod h1:lZZrZUWzlyDJKlLQ6DKAy53LnG7m5kHyrEHvvcDmBpU=
github.com/tidwall/gjson v1.3.4/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U=
github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb/go.mod h1:lKYYLFIr9OIgdgrtgkZ9zgRxRdvPYsExnYBsEAd8W5M=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tidwall/btree v1.8.1 h1:27ehoXvm5AG/g+1VxLS1SD3vRhp/H7LuEfwNvddEdmA=
github.com/tidwall/btree v1.8.1/go.mod h1:jBbTdUWhSZClZWoDg54VnvV7/54modSOzDN7VXftj1A=
github.com/tidwall/buntdb v1.3.2 h1:qd+IpdEGs0pZci37G4jF51+fSKlkuUTMXuHhXL1AkKg=
github.com/tidwall/buntdb v1.3.2/go.mod h1:lZZrZUWzlyDJKlLQ6DKAy53LnG7m5kHyrEHvvcDmBpU=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/grect v0.1.4 h1:dA3oIgNgWdSspFzn1kS4S/RDpZFLrIxAZOdJKjYapOg=
github.com/tidwall/grect v0.1.4/go.mod h1:9FBsaYRaR0Tcy4UwefBX/UDcDcDy9V5jUcxHzv2jd5Q=
github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/rtred v0.1.2 h1:exmoQtOLvDoO8ud++6LwVsAMTu0KPzLTUrMln8u1yu8=
github.com/tidwall/rtred v0.1.2/go.mod h1:hd69WNXQ5RP9vHd7dqekAz+RIdtfBogmglkZSRxCHFQ=
github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e/go.mod h1:/h+UnNGt0IhNNJLkGikcdcJqm66zGD/uJGMRxK/9+Ao=
github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563/go.mod h1:mLqSmt7Dv/CNneF2wfcChfN1rvapyQr01LGKnKex0DQ=
github.com/tidwall/tinyqueue v0.1.1 h1:SpNEvEggbpyN5DIReaJ2/1ndroY8iyEGxPYxoSaymYE=
github.com/tidwall/tinyqueue v0.1.1/go.mod h1:O/QNHwrnjqr6IHItYrzoHAKYhBkLI67Q096fQP5zMYw=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.34.0 h1:d3AAQJ2DRcxJYHm7OXNXtXt2as1vMDfxeIcFvhmGGm4=
github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=
github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
@@ -251,109 +186,95 @@ github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCO
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0=
go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ=
go.mongodb.org/mongo-driver v1.16.0 h1:tpRsfBJMROVHKpdGyc1BBEzzjDUWjItxbVSZ8Ls4BQ4=
go.mongodb.org/mongo-driver v1.16.0/go.mod h1:oB6AhJQvFQL4LEHyXi6aJzQJtBiTQHiAd83l0GdFaiw=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8=
golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/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-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
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-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+6
View File
@@ -5,6 +5,12 @@ GET {{host}}/api/v1/find?site={{site}}&sort=-time&format=tree&url={{url}}
### find request with plain
GET {{host}}/api/v1/find?site={{site}}&sort=-controversy&format=plain&url={{url}}
### find 10 first comments for given URL
GET {{host}}/api/v1/find?site={{site}}&sort=-controversy&format=plain&url={{url}}&limit=10
### find 10 comments after given comment ID (3665976683 in this example) for given URL
GET {{host}}/api/v1/find?site={{site}}&sort=-controversy&format=plain&url={{url}}&limit=10&after=3665976683
### find request with plain. dev token for secret=12345, not admin
GET {{host}}/api/v1/find?site={{site}}&sort=-controversy&format=plain&url={{url}}
X-JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcmsiLCJleHAiOjE5NzYwNTY3NTYsImp0aSI6IjJlOGJmMTE5OTI0MjQxMDRjYjFhZGRlODllMWYwNGFiMTg4YWZjMzQiLCJpYXQiOjE1NzYwNTY0NTYsImlzcyI6InJlbWFyazQyIiwidXNlciI6eyJuYW1lIjoiZGV2X3VzZXIiLCJpZCI6ImRldl91c2VyIiwicGljdHVyZSI6Imh0dHA6Ly8xMjcuMC4wLjE6ODA4MC9hcGkvdjEvYXZhdGFyL2NjZmEyYWJkMDE2Njc2MDViNGUxZmM0ZmNiOTFiMWUxYWYzMjMyNDAuaW1hZ2UiLCJhdHRycyI6eyJhZG1pbiI6dHJ1ZSwiYmxvY2tlZCI6ZmFsc2V9fX0.6Qt5s2enBMRC-Jmsua01yViVYI95Dx6BPBMaNjj36d4
+70
View File
@@ -1,5 +1,75 @@
# Changes
## [0.9.0](https://github.com/googleapis/google-cloud-go/compare/compute/metadata/v0.8.4...compute/metadata/v0.9.0) (2025-09-24)
### Features
* **compute/metadata:** Retry on HTTP 429 ([#12932](https://github.com/googleapis/google-cloud-go/issues/12932)) ([1e91f5c](https://github.com/googleapis/google-cloud-go/commit/1e91f5c07acacd38ecdd4ff3e83e092b745e0bc2))
## [0.8.4](https://github.com/googleapis/google-cloud-go/compare/compute/metadata/v0.8.3...compute/metadata/v0.8.4) (2025-09-18)
### Bug Fixes
* **compute/metadata:** Set subClient for UseDefaultClient case ([#12911](https://github.com/googleapis/google-cloud-go/issues/12911)) ([9e2646b](https://github.com/googleapis/google-cloud-go/commit/9e2646b1821231183fd775bb107c062865eeaccd))
## [0.8.3](https://github.com/googleapis/google-cloud-go/compare/compute/metadata/v0.8.2...compute/metadata/v0.8.3) (2025-09-17)
### Bug Fixes
* **compute/metadata:** Disable Client timeouts for subscription client ([#12910](https://github.com/googleapis/google-cloud-go/issues/12910)) ([187a58a](https://github.com/googleapis/google-cloud-go/commit/187a58a540494e1e8562b046325b8cad8cf7af4a))
## [0.8.2](https://github.com/googleapis/google-cloud-go/compare/compute/metadata/v0.8.1...compute/metadata/v0.8.2) (2025-09-17)
### Bug Fixes
* **compute/metadata:** Racy test and uninitialized subClient ([#12892](https://github.com/googleapis/google-cloud-go/issues/12892)) ([4943ca2](https://github.com/googleapis/google-cloud-go/commit/4943ca2bf83908a23806247bc4252dfb440d09cc)), refs [#12888](https://github.com/googleapis/google-cloud-go/issues/12888)
## [0.8.1](https://github.com/googleapis/google-cloud-go/compare/compute/metadata/v0.8.0...compute/metadata/v0.8.1) (2025-09-16)
### Bug Fixes
* **compute/metadata:** Use separate client for subscribe methods ([#12885](https://github.com/googleapis/google-cloud-go/issues/12885)) ([76b80f8](https://github.com/googleapis/google-cloud-go/commit/76b80f8df9bf9339d175407e8c15936fe1ac1c9c))
## [0.8.0](https://github.com/googleapis/google-cloud-go/compare/compute/metadata/v0.7.0...compute/metadata/v0.8.0) (2025-08-06)
### Features
* **compute/metadata:** Add Options.UseDefaultClient ([#12657](https://github.com/googleapis/google-cloud-go/issues/12657)) ([1a88209](https://github.com/googleapis/google-cloud-go/commit/1a8820900f20e038291c4bb2c5284a449196e81f)), refs [#11078](https://github.com/googleapis/google-cloud-go/issues/11078)
## [0.7.0](https://github.com/googleapis/google-cloud-go/compare/compute/metadata/v0.6.0...compute/metadata/v0.7.0) (2025-05-13)
### Features
* **compute/metadata:** Allow canceling GCE detection ([#11786](https://github.com/googleapis/google-cloud-go/issues/11786)) ([78100fe](https://github.com/googleapis/google-cloud-go/commit/78100fe7e28cd30f1e10b47191ac3c9839663b64))
## [0.6.0](https://github.com/googleapis/google-cloud-go/compare/compute/metadata/v0.5.2...compute/metadata/v0.6.0) (2024-12-13)
### Features
* **compute/metadata:** Add debug logging ([#11078](https://github.com/googleapis/google-cloud-go/issues/11078)) ([a816814](https://github.com/googleapis/google-cloud-go/commit/a81681463906e4473570a2f426eb0dc2de64e53f))
## [0.5.2](https://github.com/googleapis/google-cloud-go/compare/compute/metadata/v0.5.1...compute/metadata/v0.5.2) (2024-09-20)
### Bug Fixes
* **compute/metadata:** Close Response Body for failed request ([#10891](https://github.com/googleapis/google-cloud-go/issues/10891)) ([e91d45e](https://github.com/googleapis/google-cloud-go/commit/e91d45e4757a9e354114509ba9800085d9e0ff1f))
## [0.5.1](https://github.com/googleapis/google-cloud-go/compare/compute/metadata/v0.5.0...compute/metadata/v0.5.1) (2024-09-12)
### Bug Fixes
* **compute/metadata:** Check error chain for retryable error ([#10840](https://github.com/googleapis/google-cloud-go/issues/10840)) ([2bdedef](https://github.com/googleapis/google-cloud-go/commit/2bdedeff621b223d63cebc4355fcf83bc68412cd))
## [0.5.0](https://github.com/googleapis/google-cloud-go/compare/compute/metadata/v0.4.0...compute/metadata/v0.5.0) (2024-07-10)
+149
View File
@@ -0,0 +1,149 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package metadata
import (
"bytes"
"context"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"strings"
)
// Code below this point is copied from github.com/googleapis/gax-go/v2/internallog
// to avoid the dependency. The compute/metadata module is used by too many
// non-client library modules that can't justify the dependency.
// The handler returned if logging is not enabled.
type noOpHandler struct{}
func (h noOpHandler) Enabled(_ context.Context, _ slog.Level) bool {
return false
}
func (h noOpHandler) Handle(_ context.Context, _ slog.Record) error {
return nil
}
func (h noOpHandler) WithAttrs(_ []slog.Attr) slog.Handler {
return h
}
func (h noOpHandler) WithGroup(_ string) slog.Handler {
return h
}
// httpRequest returns a lazily evaluated [slog.LogValuer] for a
// [http.Request] and the associated body.
func httpRequest(req *http.Request, body []byte) slog.LogValuer {
return &request{
req: req,
payload: body,
}
}
type request struct {
req *http.Request
payload []byte
}
func (r *request) LogValue() slog.Value {
if r == nil || r.req == nil {
return slog.Value{}
}
var groupValueAttrs []slog.Attr
groupValueAttrs = append(groupValueAttrs, slog.String("method", r.req.Method))
groupValueAttrs = append(groupValueAttrs, slog.String("url", r.req.URL.String()))
var headerAttr []slog.Attr
for k, val := range r.req.Header {
headerAttr = append(headerAttr, slog.String(k, strings.Join(val, ",")))
}
if len(headerAttr) > 0 {
groupValueAttrs = append(groupValueAttrs, slog.Any("headers", headerAttr))
}
if len(r.payload) > 0 {
if attr, ok := processPayload(r.payload); ok {
groupValueAttrs = append(groupValueAttrs, attr)
}
}
return slog.GroupValue(groupValueAttrs...)
}
// httpResponse returns a lazily evaluated [slog.LogValuer] for a
// [http.Response] and the associated body.
func httpResponse(resp *http.Response, body []byte) slog.LogValuer {
return &response{
resp: resp,
payload: body,
}
}
type response struct {
resp *http.Response
payload []byte
}
func (r *response) LogValue() slog.Value {
if r == nil {
return slog.Value{}
}
var groupValueAttrs []slog.Attr
groupValueAttrs = append(groupValueAttrs, slog.String("status", fmt.Sprint(r.resp.StatusCode)))
var headerAttr []slog.Attr
for k, val := range r.resp.Header {
headerAttr = append(headerAttr, slog.String(k, strings.Join(val, ",")))
}
if len(headerAttr) > 0 {
groupValueAttrs = append(groupValueAttrs, slog.Any("headers", headerAttr))
}
if len(r.payload) > 0 {
if attr, ok := processPayload(r.payload); ok {
groupValueAttrs = append(groupValueAttrs, attr)
}
}
return slog.GroupValue(groupValueAttrs...)
}
func processPayload(payload []byte) (slog.Attr, bool) {
peekChar := payload[0]
if peekChar == '{' {
// JSON object
var m map[string]any
if err := json.Unmarshal(payload, &m); err == nil {
return slog.Any("payload", m), true
}
} else if peekChar == '[' {
// JSON array
var m []any
if err := json.Unmarshal(payload, &m); err == nil {
return slog.Any("payload", m), true
}
} else {
// Everything else
buf := &bytes.Buffer{}
if err := json.Compact(buf, payload); err != nil {
// Write raw payload incase of error
buf.Write(payload)
}
return slog.String("payload", buf.String()), true
}
return slog.Attr{}, false
}
+197 -96
View File
@@ -22,8 +22,10 @@ package metadata // import "cloud.google.com/go/compute/metadata"
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log/slog"
"net"
"net/http"
"net/url"
@@ -60,19 +62,27 @@ var (
instID = &cachedValue{k: "instance/id", trim: true}
)
var defaultClient = &Client{hc: newDefaultHTTPClient()}
var defaultClient = &Client{
hc: newDefaultHTTPClient(true),
subClient: newDefaultHTTPClient(false),
logger: slog.New(noOpHandler{}),
}
func newDefaultHTTPClient() *http.Client {
return &http.Client{
Transport: &http.Transport{
Dial: (&net.Dialer{
Timeout: 2 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
IdleConnTimeout: 60 * time.Second,
},
Timeout: 5 * time.Second,
func newDefaultHTTPClient(enableTimeouts bool) *http.Client {
transport := &http.Transport{
Dial: (&net.Dialer{
Timeout: 2 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
}
c := &http.Client{
Transport: transport,
}
if enableTimeouts {
transport.IdleConnTimeout = 60 * time.Second
c.Timeout = 5 * time.Second
}
return c
}
// NotDefinedError is returned when requested metadata is not defined.
@@ -113,82 +123,20 @@ var (
// NOTE: True returned from `OnGCE` does not guarantee that the metadata server
// is accessible from this process and have all the metadata defined.
func OnGCE() bool {
onGCEOnce.Do(initOnGCE)
return OnGCEWithContext(context.Background())
}
// OnGCEWithContext reports whether this process is running on Google Compute Platforms.
// This function's return value is memoized for better performance.
// NOTE: True returned from `OnGCEWithContext` does not guarantee that the metadata server
// is accessible from this process and have all the metadata defined.
func OnGCEWithContext(ctx context.Context) bool {
onGCEOnce.Do(func() {
onGCE = defaultClient.OnGCEWithContext(ctx)
})
return onGCE
}
func initOnGCE() {
onGCE = testOnGCE()
}
func testOnGCE() bool {
// The user explicitly said they're on GCE, so trust them.
if os.Getenv(metadataHostEnv) != "" {
return true
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
resc := make(chan bool, 2)
// Try two strategies in parallel.
// See https://github.com/googleapis/google-cloud-go/issues/194
go func() {
req, _ := http.NewRequest("GET", "http://"+metadataIP, nil)
req.Header.Set("User-Agent", userAgent)
res, err := newDefaultHTTPClient().Do(req.WithContext(ctx))
if err != nil {
resc <- false
return
}
defer res.Body.Close()
resc <- res.Header.Get("Metadata-Flavor") == "Google"
}()
go func() {
resolver := &net.Resolver{}
addrs, err := resolver.LookupHost(ctx, "metadata.google.internal.")
if err != nil || len(addrs) == 0 {
resc <- false
return
}
resc <- strsContains(addrs, metadataIP)
}()
tryHarder := systemInfoSuggestsGCE()
if tryHarder {
res := <-resc
if res {
// The first strategy succeeded, so let's use it.
return true
}
// Wait for either the DNS or metadata server probe to
// contradict the other one and say we are running on
// GCE. Give it a lot of time to do so, since the system
// info already suggests we're running on a GCE BIOS.
timer := time.NewTimer(5 * time.Second)
defer timer.Stop()
select {
case res = <-resc:
return res
case <-timer.C:
// Too slow. Who knows what this system is.
return false
}
}
// There's no hint from the system info that we're running on
// GCE, so use the first probe's result as truth, whether it's
// true or false. The goal here is to optimize for speed for
// users who are NOT running on GCE. We can't assume that
// either a DNS lookup or an HTTP request to a blackholed IP
// address is fast. Worst case this should return when the
// metaClient's Transport.ResponseHeaderTimeout or
// Transport.Dial.Timeout fires (in two seconds).
return <-resc
}
// Subscribe calls Client.SubscribeWithContext on the default client.
//
// Deprecated: Please use the context aware variant [SubscribeWithContext].
@@ -409,21 +357,160 @@ func strsContains(ss []string, s string) bool {
// A Client provides metadata.
type Client struct {
hc *http.Client
// subClient by default is a HTTP Client that is only used for subscribe
// methods that should not specify a timeout. If the user specifies a client
// this with be the same as 'hc'.
subClient *http.Client
logger *slog.Logger
}
// Options for configuring a [Client].
type Options struct {
// Client is the HTTP client used to make requests. Optional.
// If UseDefaultClient is true, this field is ignored.
// If this field is nil, a new default http.Client will be created.
Client *http.Client
// Logger is used to log information about HTTP request and responses.
// If not provided, nothing will be logged. Optional.
Logger *slog.Logger
// UseDefaultClient specifies that the client should use the same default
// internal http.Client that is used in functions such as GetWithContext.
// This is useful for sharing a single TCP connection pool across requests.
// The difference vs GetWithContext is the ability to use this struct
// to provide a custom logger. If this field is true, the Client
// field is ignored.
UseDefaultClient bool
}
// NewClient returns a Client that can be used to fetch metadata.
// Returns the client that uses the specified http.Client for HTTP requests.
// If nil is specified, returns the default client.
// If nil is specified, returns the default internal Client that is
// also used in functions such as GetWithContext. This is useful for sharing
// a single TCP connection pool across requests.
func NewClient(c *http.Client) *Client {
if c == nil {
// Preserve original behavior for nil argument.
return defaultClient
}
return &Client{hc: c}
// Return a new client with a no-op logger for backward compatibility.
return &Client{hc: c, subClient: c, logger: slog.New(noOpHandler{})}
}
// NewWithOptions returns a Client that is configured with the provided Options.
func NewWithOptions(opts *Options) *Client {
// Preserve original behavior for nil opts.
if opts == nil {
return defaultClient
}
// Handle explicit request for the internal default http.Client.
if opts.UseDefaultClient {
logger := opts.Logger
if logger == nil {
logger = slog.New(noOpHandler{})
}
return &Client{hc: defaultClient.hc, subClient: defaultClient.subClient, logger: logger}
}
// Handle isolated client creation.
client := opts.Client
subClient := opts.Client
if client == nil {
client = newDefaultHTTPClient(true)
subClient = newDefaultHTTPClient(false)
}
logger := opts.Logger
if logger == nil {
logger = slog.New(noOpHandler{})
}
return &Client{hc: client, subClient: subClient, logger: logger}
}
// NOTE: metadataRequestStrategy is assigned to a variable for test stubbing purposes.
var metadataRequestStrategy = func(ctx context.Context, httpClient *http.Client, resc chan bool) {
req, _ := http.NewRequest("GET", "http://"+metadataIP, nil)
req.Header.Set("User-Agent", userAgent)
res, err := httpClient.Do(req.WithContext(ctx))
if err != nil {
resc <- false
return
}
defer res.Body.Close()
resc <- res.Header.Get("Metadata-Flavor") == "Google"
}
// NOTE: dnsRequestStrategy is assigned to a variable for test stubbing purposes.
var dnsRequestStrategy = func(ctx context.Context, resc chan bool) {
resolver := &net.Resolver{}
addrs, err := resolver.LookupHost(ctx, "metadata.google.internal.")
if err != nil || len(addrs) == 0 {
resc <- false
return
}
resc <- strsContains(addrs, metadataIP)
}
// OnGCEWithContext reports whether this process is running on Google Compute Platforms.
// NOTE: True returned from `OnGCEWithContext` does not guarantee that the metadata server
// is accessible from this process and have all the metadata defined.
func (c *Client) OnGCEWithContext(ctx context.Context) bool {
// The user explicitly said they're on GCE, so trust them.
if os.Getenv(metadataHostEnv) != "" {
return true
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
resc := make(chan bool, 2)
// Try two strategies in parallel.
// See https://github.com/googleapis/google-cloud-go/issues/194
go metadataRequestStrategy(ctx, c.hc, resc)
go dnsRequestStrategy(ctx, resc)
tryHarder := systemInfoSuggestsGCE()
if tryHarder {
res := <-resc
if res {
// The first strategy succeeded, so let's use it.
return true
}
// Wait for either the DNS or metadata server probe to
// contradict the other one and say we are running on
// GCE. Give it a lot of time to do so, since the system
// info already suggests we're running on a GCE BIOS.
// Ensure cancellations from the calling context are respected.
waitContext, cancelWait := context.WithTimeout(ctx, 5*time.Second)
defer cancelWait()
select {
case res = <-resc:
return res
case <-waitContext.Done():
// Too slow. Who knows what this system is.
return false
}
}
// There's no hint from the system info that we're running on
// GCE, so use the first probe's result as truth, whether it's
// true or false. The goal here is to optimize for speed for
// users who are NOT running on GCE. We can't assume that
// either a DNS lookup or an HTTP request to a blackholed IP
// address is fast. Worst case this should return when the
// metaClient's Transport.ResponseHeaderTimeout or
// Transport.Dial.Timeout fires (in two seconds).
return <-resc
}
// getETag returns a value from the metadata service as well as the associated ETag.
// This func is otherwise equivalent to Get.
func (c *Client) getETag(ctx context.Context, suffix string) (value, etag string, err error) {
return c.getETagWithSubClient(ctx, suffix, false)
}
func (c *Client) getETagWithSubClient(ctx context.Context, suffix string, enableSubClient bool) (value, etag string, err error) {
// Using a fixed IP makes it very difficult to spoof the metadata service in
// a container, which is an important use-case for local testing of cloud
// deployments. To enable spoofing of the metadata service, the environment
@@ -448,14 +535,30 @@ func (c *Client) getETag(ctx context.Context, suffix string) (value, etag string
req.Header.Set("User-Agent", userAgent)
var res *http.Response
var reqErr error
var body []byte
retryer := newRetryer()
hc := c.hc
if enableSubClient {
hc = c.subClient
}
for {
res, reqErr = c.hc.Do(req)
c.logger.DebugContext(ctx, "metadata request", "request", httpRequest(req, nil))
res, reqErr = hc.Do(req)
var code int
if res != nil {
code = res.StatusCode
body, err = io.ReadAll(res.Body)
if err != nil {
res.Body.Close()
return "", "", err
}
c.logger.DebugContext(ctx, "metadata response", "response", httpResponse(res, body))
res.Body.Close()
}
if delay, shouldRetry := retryer.Retry(code, reqErr); shouldRetry {
if res != nil && res.Body != nil {
res.Body.Close()
}
if err := sleep(ctx, delay); err != nil {
return "", "", err
}
@@ -466,18 +569,13 @@ func (c *Client) getETag(ctx context.Context, suffix string) (value, etag string
if reqErr != nil {
return "", "", reqErr
}
defer res.Body.Close()
if res.StatusCode == http.StatusNotFound {
return "", "", NotDefinedError(suffix)
}
all, err := io.ReadAll(res.Body)
if err != nil {
return "", "", err
}
if res.StatusCode != 200 {
return "", "", &Error{Code: res.StatusCode, Message: string(all)}
return "", "", &Error{Code: res.StatusCode, Message: string(body)}
}
return string(all), res.Header.Get("Etag"), nil
return string(body), res.Header.Get("Etag"), nil
}
// Get returns a value from the metadata service.
@@ -791,7 +889,7 @@ func (c *Client) SubscribeWithContext(ctx context.Context, suffix string, fn fun
const failedSubscribeSleep = time.Second * 5
// First check to see if the metadata value exists at all.
val, lastETag, err := c.getETag(ctx, suffix)
val, lastETag, err := c.getETagWithSubClient(ctx, suffix, true)
if err != nil {
return err
}
@@ -807,8 +905,11 @@ func (c *Client) SubscribeWithContext(ctx context.Context, suffix string, fn fun
suffix += "?wait_for_change=true&last_etag="
}
for {
val, etag, err := c.getETag(ctx, suffix+url.QueryEscape(lastETag))
val, etag, err := c.getETagWithSubClient(ctx, suffix+url.QueryEscape(lastETag), true)
if err != nil {
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return err
}
if _, deleted := err.(NotDefinedError); !deleted {
time.Sleep(failedSubscribeSleep)
continue // Retry on other errors.
+3
View File
@@ -95,6 +95,9 @@ func shouldRetry(status int, err error) bool {
if 500 <= status && status <= 599 {
return true
}
if status == http.StatusTooManyRequests {
return true
}
if err == io.ErrUnexpectedEOF {
return true
}
+7 -2
View File
@@ -17,10 +17,15 @@
package metadata
import "syscall"
import (
"errors"
"syscall"
)
func init() {
// Initialize syscallRetryable to return true on transient socket-level
// errors. These errors are specific to Linux.
syscallRetryable = func(err error) bool { return err == syscall.ECONNRESET || err == syscall.ECONNREFUSED }
syscallRetryable = func(err error) bool {
return errors.Is(err, syscall.ECONNRESET) || errors.Is(err, syscall.ECONNREFUSED)
}
}
+3 -1
View File
@@ -20,7 +20,9 @@ package metadata
// doing network requests) suggests that we're running on GCE. If this
// returns true, testOnGCE tries a bit harder to reach its metadata
// server.
func systemInfoSuggestsGCE() bool {
//
// NOTE: systemInfoSuggestsGCE is assigned to a varible for test stubbing purposes.
var systemInfoSuggestsGCE = func() bool {
// We don't currently have checks for other GOOS
return false
}
+3 -1
View File
@@ -21,8 +21,10 @@ import (
"strings"
)
func systemInfoSuggestsGCE() bool {
// NOTE: systemInfoSuggestsGCE is assigned to a varible for test stubbing purposes.
var systemInfoSuggestsGCE = func() bool {
b, _ := os.ReadFile("/sys/class/dmi/id/product_name")
name := strings.TrimSpace(string(b))
return name == "Google" || name == "Google Compute Engine"
}
+2 -1
View File
@@ -22,7 +22,8 @@ import (
"golang.org/x/sys/windows/registry"
)
func systemInfoSuggestsGCE() bool {
// NOTE: systemInfoSuggestsGCE is assigned to a varible for test stubbing purposes.
var systemInfoSuggestsGCE = func() bool {
k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SYSTEM\HardwareConfig\Current`, registry.QUERY_VALUE)
if err != nil {
return false
+20 -6
View File
@@ -22,7 +22,14 @@ Syntax-wise, it is as close as possible to jQuery, with the same function names
## Installation
Please note that starting with version `v1.9.0` of goquery, Go 1.18+ is required due to the use of generics. For previous goquery versions, a Go version of 1.1+ was required because of the `net/html` dependency. Ongoing goquery development is tested on the latest 2 versions of Go.
Required Go version:
* Starting with version `v1.11.0` of goquery, Go 1.24+ is required due to its dependencies.
* Starting with version `v1.10.0` of goquery, Go 1.23+ is required due to the use of function-based iterators.
* For `v1.9.0` of goquery, Go 1.18+ is required due to the use of generics.
* For previous goquery versions, a Go version of 1.1+ was required because of the `net/html` dependency.
Ongoing goquery development is tested on the latest 2 versions of Go.
$ go get github.com/PuerkitoBio/goquery
@@ -40,6 +47,12 @@ Please note that starting with version `v1.9.0` of goquery, Go 1.18+ is required
**Note that goquery's API is now stable, and will not break.**
* **2025-11-16 (v1.11.0)** : Update `go.mod` dependencies, add go1.25 to the test matrix, **goquery now requires Go version 1.24+**.
* **2025-04-11 (v1.10.3)** : Update `go.mod` dependencies, small optimization (thanks [@myxzlpltk](https://github.com/myxzlpltk)).
* **2025-02-13 (v1.10.2)** : Update `go.mod` dependencies, add go1.24 to the test matrix.
* **2024-12-26 (v1.10.1)** : Update `go.mod` dependencies.
* **2024-09-06 (v1.10.0)** : Add `EachIter` which provides an iterator that can be used in `for..range` loops on the `*Selection` object. **goquery now requires Go version 1.23+** (thanks [@amikai](https://github.com/amikai)).
* **2024-09-06 (v1.9.3)** : Update `go.mod` dependencies.
* **2024-04-29 (v1.9.2)** : Update `go.mod` dependencies.
* **2024-02-29 (v1.9.1)** : Improve allocation and performance of the `Map` function and `Selection.Map` method, better document the cascadia differences (thanks [@jwilsson](https://github.com/jwilsson)).
* **2024-02-22 (v1.9.0)** : Add a generic `Map` function, **goquery now requires Go version 1.18+** (thanks [@Fesaa](https://github.com/Fesaa)).
@@ -161,6 +174,7 @@ func main() {
- [goskyr](https://github.com/jakopako/goskyr), an easily configurable command-line scraper written in Go.
- [goGetJS](https://github.com/davemolk/goGetJS), a tool for extracting, searching, and saving JavaScript files (with optional headless browser).
- [fitter](https://github.com/PxyUp/fitter), a tool for selecting values from JSON, XML, HTML and XPath formatted pages.
- [seltabl](github.com/conneroisu/seltabl), an orm-like package and supporting language server for extracting values from HTML
## Support
@@ -183,15 +197,15 @@ There are a number of ways you can support the project:
The [BSD 3-Clause license][bsd], the same as the [Go language][golic]. Cascadia's license is [here][caslic].
[jquery]: http://jquery.com/
[go]: http://golang.org/
[jquery]: https://jquery.com/
[go]: https://go.dev/
[cascadia]: https://github.com/andybalholm/cascadia
[cascadiacli]: https://github.com/suntong/cascadia
[bsd]: http://opensource.org/licenses/BSD-3-Clause
[golic]: http://golang.org/LICENSE
[bsd]: https://opensource.org/licenses/BSD-3-Clause
[golic]: https://go.dev/LICENSE
[caslic]: https://github.com/andybalholm/cascadia/blob/master/LICENSE
[doc]: https://pkg.go.dev/github.com/PuerkitoBio/goquery
[index]: http://api.jquery.com/index/
[index]: https://api.jquery.com/index/
[gonet]: https://github.com/golang/net/
[html]: https://pkg.go.dev/golang.org/x/net/html
[wiki]: https://github.com/PuerkitoBio/goquery/wiki/Tips-and-tricks
+1 -1
View File
@@ -62,7 +62,7 @@ func (s *Selection) AddBackFiltered(selector string) *Selection {
}
// AddBackMatcher reduces the previous set of elements on the stack to those that match
// the mateher, and adds them to the curernt set.
// the matcher, and adds them to the current set.
// It returns a new Selection object containing the current Selection combined
// with the filtered previous one
func (s *Selection) AddBackMatcher(m Matcher) *Selection {
+14
View File
@@ -1,5 +1,7 @@
package goquery
import "iter"
// Each iterates over a Selection object, executing a function for each
// matched element. It returns the current Selection object. The function
// f is called for each element in the selection with the index of the
@@ -12,6 +14,18 @@ func (s *Selection) Each(f func(int, *Selection)) *Selection {
return s
}
// EachIter returns an iterator that yields the Selection object in order.
// The implementation is similar to Each, but it returns an iterator instead.
func (s *Selection) EachIter() iter.Seq2[int, *Selection] {
return func(yield func(int, *Selection) bool) {
for i, n := range s.Nodes {
if !yield(i, newSingleSelection(n, s.document)) {
return
}
}
}
}
// EachWithBreak iterates over a Selection object, executing a function for each
// matched element. It is identical to Each except that it is possible to break
// out of the loop by returning false in the callback function. It returns the
+10 -10
View File
@@ -1,7 +1,6 @@
package goquery
import (
"bytes"
"regexp"
"strings"
@@ -60,14 +59,14 @@ func (s *Selection) SetAttr(attrName, val string) *Selection {
// Text gets the combined text contents of each element in the set of matched
// elements, including their descendants.
func (s *Selection) Text() string {
var buf bytes.Buffer
var builder strings.Builder
// Slightly optimized vs calling Each: no single selection object created
var f func(*html.Node)
f = func(n *html.Node) {
if n.Type == html.TextNode {
// Keep newlines and spaces, like jQuery
buf.WriteString(n.Data)
builder.WriteString(n.Data)
}
if n.FirstChild != nil {
for c := n.FirstChild; c != nil; c = c.NextSibling {
@@ -79,7 +78,7 @@ func (s *Selection) Text() string {
f(n)
}
return buf.String()
return builder.String()
}
// Size is an alias for Length.
@@ -97,16 +96,16 @@ func (s *Selection) Length() int {
func (s *Selection) Html() (ret string, e error) {
// Since there is no .innerHtml, the HTML content must be re-created from
// the nodes using html.Render.
var buf bytes.Buffer
var builder strings.Builder
if len(s.Nodes) > 0 {
for c := s.Nodes[0].FirstChild; c != nil; c = c.NextSibling {
e = html.Render(&buf, c)
e = html.Render(&builder, c)
if e != nil {
return
}
}
ret = buf.String()
ret = builder.String()
}
return
@@ -168,7 +167,7 @@ func (s *Selection) RemoveClass(class ...string) *Selection {
} else {
classes, attr := getClassesAndAttr(n, true)
for _, rcl := range rclasses {
classes = strings.Replace(classes, " "+rcl+" ", " ", -1)
classes = strings.ReplaceAll(classes, " "+rcl+" ", " ")
}
setClasses(n, attr, classes)
@@ -192,8 +191,9 @@ func (s *Selection) ToggleClass(class ...string) *Selection {
for _, n := range s.Nodes {
classes, attr := getClassesAndAttr(n, true)
for _, tcl := range tcls {
if strings.Contains(classes, " "+tcl+" ") {
classes = strings.Replace(classes, " "+tcl+" ", " ", -1)
spaceAroundTcl := " " + tcl + " "
if strings.Contains(classes, spaceAroundTcl) {
classes = strings.ReplaceAll(classes, spaceAroundTcl, " ")
} else {
classes += tcl + " "
}
+10 -11
View File
@@ -1,8 +1,8 @@
package goquery
import (
"bytes"
"io"
"strings"
"golang.org/x/net/html"
)
@@ -26,13 +26,12 @@ var nodeNames = []string{
// Go's net/html package defines the following node types, listed with
// the corresponding returned value from this function:
//
// ErrorNode : #error
// TextNode : #text
// DocumentNode : #document
// ElementNode : the element's tag name
// CommentNode : #comment
// DoctypeNode : the name of the document type
//
// ErrorNode : #error
// TextNode : #text
// DocumentNode : #document
// ElementNode : the element's tag name
// CommentNode : #comment
// DoctypeNode : the name of the document type
func NodeName(s *Selection) string {
if s.Length() == 0 {
return ""
@@ -77,11 +76,11 @@ func Render(w io.Writer, s *Selection) error {
// because this is not a jQuery method (in javascript-land, this is
// a property provided by the DOM).
func OuterHtml(s *Selection) (string, error) {
var buf bytes.Buffer
if err := Render(&buf, s); err != nil {
var builder strings.Builder
if err := Render(&builder, s); err != nil {
return "", err
}
return buf.String(), nil
return builder.String(), nil
}
// Loop through all container nodes to search for the target node.
-25
View File
@@ -1,25 +0,0 @@
## Copyright 2014 Alvaro J. Genial. All rights reserved.
## Use of this source code is governed by a BSD-style
## license that can be found in the LICENSE file.
language: go
go:
- tip
- 1.6
- 1.5
- 1.4
- 1.3
# 1.2
before_install:
# - go get -v golang.org/x/tools/cmd/cover
# - go get -v golang.org/x/tools/cmd/vet
# - go get -v golang.org/x/lint/golint
- export PATH=$PATH:/home/travis/gopath/bin
script:
- go build -v ./...
- go test -v -cover ./...
- go vet ./...
# - golint .
-27
View File
@@ -1,27 +0,0 @@
Copyright (c) 2014 Alvaro J. Genial. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-247
View File
@@ -1,247 +0,0 @@
form
====
A Form Encoding & Decoding Package for Go, written by [Alvaro J. Genial](http://alva.ro).
[![Build Status](https://travis-ci.org/ajg/form.png?branch=master)](https://travis-ci.org/ajg/form)
[![GoDoc](https://godoc.org/github.com/ajg/form?status.png)](https://godoc.org/github.com/ajg/form)
Synopsis
--------
This library is designed to allow seamless, high-fidelity encoding and decoding of arbitrary data in `application/x-www-form-urlencoded` format and as [`url.Values`](http://golang.org/pkg/net/url/#Values). It is intended to be useful primarily in dealing with web forms and URI query strings, both of which natively employ said format.
Unsurprisingly, `form` is modeled after other Go [`encoding`](http://golang.org/pkg/encoding/) packages, in particular [`encoding/json`](http://golang.org/pkg/encoding/json/), and follows the same conventions (see below for more.) It aims to automatically handle any kind of concrete Go [data value](#values) (i.e., not functions, channels, etc.) while providing mechanisms for custom behavior.
Status
------
The implementation is in usable shape and is fairly well tested with its accompanying test suite. The API is unlikely to change much, but still may. Lastly, the code has not yet undergone a security review to ensure it is free of vulnerabilities. Please file an issue or send a pull request for fixes & improvements.
Dependencies
------------
The only requirement is [Go 1.2](http://golang.org/doc/go1.2) or later.
Usage
-----
```go
import "github.com/ajg/form"
// or: "gopkg.in/ajg/form.v1"
```
Given a type like the following...
```go
type User struct {
Name string `form:"name"`
Email string `form:"email"`
Joined time.Time `form:"joined,omitempty"`
Posts []int `form:"posts"`
Preferences map[string]string `form:"prefs"`
Avatar []byte `form:"avatar"`
PasswordHash int64 `form:"-"`
}
```
...it is easy to encode data of that type...
```go
func PostUser(url string, u User) error {
var c http.Client
_, err := c.PostForm(url, form.EncodeToValues(u))
return err
}
```
...as well as decode it...
```go
func Handler(w http.ResponseWriter, r *http.Request) {
var u User
d := form.NewDecoder(r.Body)
if err := d.Decode(&u); err != nil {
http.Error(w, "Form could not be decoded", http.StatusBadRequest)
return
}
fmt.Fprintf(w, "Decoded: %#v", u)
}
```
...without having to do any grunt work.
Field Tags
----------
Like other encoding packages, `form` supports the following options for fields:
- `` `form:"-"` ``: Causes the field to be ignored during encoding and decoding.
- `` `form:"<name>"` ``: Overrides the field's name; useful especially when dealing with external identifiers in camelCase, as are commonly found on the web.
- `` `form:",omitempty"` ``: Elides the field during encoding if it is empty (typically meaning equal to the type's zero value.)
- `` `form:"<name>,omitempty"` ``: The way to combine the two options above.
Values
------
### Simple Values
Values of the following types are all considered simple:
- `bool`
- `int`, `int8`, `int16`, `int32`, `int64`, `rune`
- `uint`, `uint8`, `uint16`, `uint32`, `uint64`, `byte`
- `float32`, `float64`
- `complex64`, `complex128`
- `string`
- `[]byte` (see note)
- [`time.Time`](http://golang.org/pkg/time/#Time)
- [`url.URL`](http://golang.org/pkg/net/url/#URL)
- An alias of any of the above
- A pointer to any of the above
### Composite Values
A composite value is one that can contain other values. Values of the following kinds...
- Maps
- Slices; except `[]byte` (see note)
- Structs; except [`time.Time`](http://golang.org/pkg/time/#Time) and [`url.URL`](http://golang.org/pkg/net/url/#URL)
- Arrays
- An alias of any of the above
- A pointer to any of the above
...are considered composites in general, unless they implement custom marshaling/unmarshaling. Composite values are encoded as a flat mapping of paths to values, where the paths are constructed by joining the parent and child paths with a period (`.`).
(Note: a byte slice is treated as a `string` by default because it's more efficient, but can also be decoded as a slice—i.e., with indexes.)
### Untyped Values
While encouraged, it is not necessary to define a type (e.g. a `struct`) in order to use `form`, since it is able to encode and decode untyped data generically using the following rules:
- Simple values will be treated as a `string`.
- Composite values will be treated as a `map[string]interface{}`, itself able to contain nested values (both scalar and compound) ad infinitum.
- However, if there is a value (of any supported type) already present in a map for a given key, then it will be used when possible, rather than being replaced with a generic value as specified above; this makes it possible to handle partially typed, dynamic or schema-less values.
### Zero Values
By default, and without custom marshaling, zero values (also known as empty/default values) are encoded as the empty string. To disable this behavior, meaning to keep zero values in their literal form (e.g. `0` for integral types), `Encoder` offers a `KeepZeros` setter method, which will do just that when set to `true`.
### Unsupported Values
Values of the following kinds aren't supported and, if present, must be ignored.
- Channel
- Function
- Unsafe pointer
- An alias of any of the above
- A pointer to any of the above
Custom Marshaling
-----------------
There is a default (generally lossless) marshaling & unmarshaling scheme for any concrete data value in Go, which is good enough in most cases. However, it is possible to override it and use a custom scheme. For instance, a "binary" field could be marshaled more efficiently using [base64](http://golang.org/pkg/encoding/base64/) to prevent it from being percent-escaped during serialization to `application/x-www-form-urlencoded` format.
Because `form` provides support for [`encoding.TextMarshaler`](http://golang.org/pkg/encoding/#TextMarshaler) and [`encoding.TextUnmarshaler`](http://golang.org/pkg/encoding/#TextUnmarshaler) it is easy to do that; for instance, like this:
```go
import "encoding"
type Binary []byte
var (
_ encoding.TextMarshaler = &Binary{}
_ encoding.TextUnmarshaler = &Binary{}
)
func (b Binary) MarshalText() ([]byte, error) {
return []byte(base64.URLEncoding.EncodeToString([]byte(b))), nil
}
func (b *Binary) UnmarshalText(text []byte) error {
bs, err := base64.URLEncoding.DecodeString(string(text))
if err == nil {
*b = Binary(bs)
}
return err
}
```
Now any value with type `Binary` will automatically be encoded using the [URL](http://golang.org/pkg/encoding/base64/#URLEncoding) variant of base64. It is left as an exercise to the reader to improve upon this scheme by eliminating the need for padding (which, besides being superfluous, uses `=`, a character that will end up percent-escaped.)
Keys
----
In theory any value can be a key as long as it has a string representation. However, by default, periods have special meaning to `form`, and thus, under the hood (i.e. in encoded form) they are transparently escaped using a preceding backslash (`\`). Backslashes within keys, themselves, are also escaped in this manner (e.g. as `\\`) in order to permit representing `\.` itself (as `\\\.`).
(Note: it is normally unnecessary to deal with this issue unless keys are being constructed manually—e.g. literally embedded in HTML or in a URI.)
The default delimiter and escape characters used for encoding and decoding composite keys can be changed using the `DelimitWith` and `EscapeWith` setter methods of `Encoder` and `Decoder`, respectively. For example...
```go
package main
import (
"os"
"github.com/ajg/form"
)
func main() {
type B struct {
Qux string `form:"qux"`
}
type A struct {
FooBar B `form:"foo.bar"`
}
a := A{FooBar: B{"XYZ"}}
os.Stdout.WriteString("Default: ")
form.NewEncoder(os.Stdout).Encode(a)
os.Stdout.WriteString("\nCustom: ")
form.NewEncoder(os.Stdout).DelimitWith('/').Encode(a)
os.Stdout.WriteString("\n")
}
```
...will produce...
```
Default: foo%5C.bar.qux=XYZ
Custom: foo.bar%2Fqux=XYZ
```
(`%5C` and `%2F` represent `\` and `/`, respectively.)
Limitations
-----------
- Circular (self-referential) values are untested.
Future Work
-----------
The following items would be nice to have in the future—though they are not being worked on yet:
- An option to treat all values as if they had been tagged with `omitempty`.
- An option to automatically treat all field names in `camelCase` or `underscore_case`.
- Built-in support for the types in [`math/big`](http://golang.org/pkg/math/big/).
- Built-in support for the types in [`image/color`](http://golang.org/pkg/image/color/).
- Improve encoding/decoding by reading/writing directly from/to the `io.Reader`/`io.Writer` when possible, rather than going through an intermediate representation (i.e. `node`) which requires more memory.
(Feel free to implement any of these and then send a pull request.)
Related Work
------------
- Package [gorilla/schema](https://github.com/gorilla/schema), which only implements decoding.
- Package [google/go-querystring](https://github.com/google/go-querystring), which only implements encoding.
License
-------
This library is distributed under a BSD-style [LICENSE](./LICENSE).
-4
View File
@@ -1,4 +0,0 @@
TODO
====
- Document IgnoreCase and IgnoreUnknownKeys in README.
-370
View File
@@ -1,370 +0,0 @@
// Copyright 2014 Alvaro J. Genial. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package form
import (
"fmt"
"io"
"io/ioutil"
"net/url"
"reflect"
"strconv"
"time"
)
// NewDecoder returns a new form Decoder.
func NewDecoder(r io.Reader) *Decoder {
return &Decoder{r, defaultDelimiter, defaultEscape, false, false}
}
// Decoder decodes data from a form (application/x-www-form-urlencoded).
type Decoder struct {
r io.Reader
d rune
e rune
ignoreUnknown bool
ignoreCase bool
}
// DelimitWith sets r as the delimiter used for composite keys by Decoder d and returns the latter; it is '.' by default.
func (d *Decoder) DelimitWith(r rune) *Decoder {
d.d = r
return d
}
// EscapeWith sets r as the escape used for delimiters (and to escape itself) by Decoder d and returns the latter; it is '\\' by default.
func (d *Decoder) EscapeWith(r rune) *Decoder {
d.e = r
return d
}
// Decode reads in and decodes form-encoded data into dst.
func (d Decoder) Decode(dst interface{}) error {
bs, err := ioutil.ReadAll(d.r)
if err != nil {
return err
}
vs, err := url.ParseQuery(string(bs))
if err != nil {
return err
}
v := reflect.ValueOf(dst)
return d.decodeNode(v, parseValues(d.d, d.e, vs, canIndexOrdinally(v)))
}
// IgnoreUnknownKeys if set to true it will make the Decoder ignore values
// that are not found in the destination object instead of returning an error.
func (d *Decoder) IgnoreUnknownKeys(ignoreUnknown bool) {
d.ignoreUnknown = ignoreUnknown
}
// IgnoreCase if set to true it will make the Decoder try to set values in the
// destination object even if the case does not match.
func (d *Decoder) IgnoreCase(ignoreCase bool) {
d.ignoreCase = ignoreCase
}
// DecodeString decodes src into dst.
func (d Decoder) DecodeString(dst interface{}, src string) error {
vs, err := url.ParseQuery(src)
if err != nil {
return err
}
v := reflect.ValueOf(dst)
return d.decodeNode(v, parseValues(d.d, d.e, vs, canIndexOrdinally(v)))
}
// DecodeValues decodes vs into dst.
func (d Decoder) DecodeValues(dst interface{}, vs url.Values) error {
v := reflect.ValueOf(dst)
return d.decodeNode(v, parseValues(d.d, d.e, vs, canIndexOrdinally(v)))
}
// DecodeString decodes src into dst.
func DecodeString(dst interface{}, src string) error {
return NewDecoder(nil).DecodeString(dst, src)
}
// DecodeValues decodes vs into dst.
func DecodeValues(dst interface{}, vs url.Values) error {
return NewDecoder(nil).DecodeValues(dst, vs)
}
func (d Decoder) decodeNode(v reflect.Value, n node) (err error) {
defer func() {
if e := recover(); e != nil {
err = fmt.Errorf("%v", e)
}
}()
if v.Kind() == reflect.Slice {
return fmt.Errorf("could not decode directly into slice; use pointer to slice")
}
d.decodeValue(v, n)
return nil
}
func (d Decoder) decodeValue(v reflect.Value, x interface{}) {
t := v.Type()
k := v.Kind()
if k == reflect.Ptr && v.IsNil() {
v.Set(reflect.New(t.Elem()))
}
if unmarshalValue(v, x) {
return
}
empty := isEmpty(x)
switch k {
case reflect.Ptr:
d.decodeValue(v.Elem(), x)
return
case reflect.Interface:
if !v.IsNil() {
d.decodeValue(v.Elem(), x)
return
} else if empty {
return // Allow nil interfaces only if empty.
} else {
panic("form: cannot decode non-empty value into into nil interface")
}
}
if empty {
v.Set(reflect.Zero(t)) // Treat the empty string as the zero value.
return
}
switch k {
case reflect.Struct:
if t.ConvertibleTo(timeType) {
d.decodeTime(v, x)
} else if t.ConvertibleTo(urlType) {
d.decodeURL(v, x)
} else {
d.decodeStruct(v, x)
}
case reflect.Slice:
d.decodeSlice(v, x)
case reflect.Array:
d.decodeArray(v, x)
case reflect.Map:
d.decodeMap(v, x)
case reflect.Invalid, reflect.Uintptr, reflect.UnsafePointer, reflect.Chan, reflect.Func:
panic(t.String() + " has unsupported kind " + k.String())
default:
d.decodeBasic(v, x)
}
}
func (d Decoder) decodeStruct(v reflect.Value, x interface{}) {
t := v.Type()
for k, c := range getNode(x) {
if f, ok := findField(v, k, d.ignoreCase); !ok && k == "" {
panic(getString(x) + " cannot be decoded as " + t.String())
} else if !ok {
if !d.ignoreUnknown {
panic(k + " doesn't exist in " + t.String())
}
} else if !f.CanSet() {
panic(k + " cannot be set in " + t.String())
} else {
d.decodeValue(f, c)
}
}
}
func (d Decoder) decodeMap(v reflect.Value, x interface{}) {
t := v.Type()
if v.IsNil() {
v.Set(reflect.MakeMap(t))
}
for k, c := range getNode(x) {
i := reflect.New(t.Key()).Elem()
d.decodeValue(i, k)
w := v.MapIndex(i)
if w.IsValid() { // We have an actual element value to decode into.
if w.Kind() == reflect.Interface {
w = w.Elem()
}
w = reflect.New(w.Type()).Elem()
} else if t.Elem().Kind() != reflect.Interface { // The map's element type is concrete.
w = reflect.New(t.Elem()).Elem()
} else {
// The best we can do here is to decode as either a string (for scalars) or a map[string]interface {} (for the rest).
// We could try to guess the type based on the string (e.g. true/false => bool) but that'll get ugly fast,
// especially if we have to guess the kind (slice vs. array vs. map) and index type (e.g. string, int, etc.)
switch c.(type) {
case node:
w = reflect.MakeMap(stringMapType)
case string:
w = reflect.New(stringType).Elem()
default:
panic("value is neither node nor string")
}
}
d.decodeValue(w, c)
v.SetMapIndex(i, w)
}
}
func (d Decoder) decodeArray(v reflect.Value, x interface{}) {
t := v.Type()
for k, c := range getNode(x) {
i, err := strconv.Atoi(k)
if err != nil {
panic(k + " is not a valid index for type " + t.String())
}
if l := v.Len(); i >= l {
panic("index is above array size")
}
d.decodeValue(v.Index(i), c)
}
}
func (d Decoder) decodeSlice(v reflect.Value, x interface{}) {
t := v.Type()
if t.Elem().Kind() == reflect.Uint8 {
// Allow, but don't require, byte slices to be encoded as a single string.
if s, ok := x.(string); ok {
v.SetBytes([]byte(s))
return
}
}
// NOTE: Implicit indexing is currently done at the parseValues level,
// so if if an implicitKey reaches here it will always replace the last.
implicit := 0
for k, c := range getNode(x) {
var i int
if k == implicitKey {
i = implicit
implicit++
} else {
explicit, err := strconv.Atoi(k)
if err != nil {
panic(k + " is not a valid index for type " + t.String())
}
i = explicit
implicit = explicit + 1
}
// "Extend" the slice if it's too short.
if l := v.Len(); i >= l {
delta := i - l + 1
v.Set(reflect.AppendSlice(v, reflect.MakeSlice(t, delta, delta)))
}
d.decodeValue(v.Index(i), c)
}
}
func (d Decoder) decodeBasic(v reflect.Value, x interface{}) {
t := v.Type()
switch k, s := t.Kind(), getString(x); k {
case reflect.Bool:
if b, e := strconv.ParseBool(s); e == nil {
v.SetBool(b)
} else {
panic("could not parse bool from " + strconv.Quote(s))
}
case reflect.Int,
reflect.Int8,
reflect.Int16,
reflect.Int32,
reflect.Int64:
if i, e := strconv.ParseInt(s, 10, 64); e == nil {
v.SetInt(i)
} else {
panic("could not parse int from " + strconv.Quote(s))
}
case reflect.Uint,
reflect.Uint8,
reflect.Uint16,
reflect.Uint32,
reflect.Uint64:
if u, e := strconv.ParseUint(s, 10, 64); e == nil {
v.SetUint(u)
} else {
panic("could not parse uint from " + strconv.Quote(s))
}
case reflect.Float32,
reflect.Float64:
if f, e := strconv.ParseFloat(s, 64); e == nil {
v.SetFloat(f)
} else {
panic("could not parse float from " + strconv.Quote(s))
}
case reflect.Complex64,
reflect.Complex128:
var c complex128
if n, err := fmt.Sscanf(s, "%g", &c); n == 1 && err == nil {
v.SetComplex(c)
} else {
panic("could not parse complex from " + strconv.Quote(s))
}
case reflect.String:
v.SetString(s)
default:
panic(t.String() + " has unsupported kind " + k.String())
}
}
func (d Decoder) decodeTime(v reflect.Value, x interface{}) {
t := v.Type()
s := getString(x)
// TODO: Find a more efficient way to do this.
for _, f := range allowedTimeFormats {
if p, err := time.Parse(f, s); err == nil {
v.Set(reflect.ValueOf(p).Convert(v.Type()))
return
}
}
panic("cannot decode string `" + s + "` as " + t.String())
}
func (d Decoder) decodeURL(v reflect.Value, x interface{}) {
t := v.Type()
s := getString(x)
if u, err := url.Parse(s); err == nil {
v.Set(reflect.ValueOf(*u).Convert(v.Type()))
return
}
panic("cannot decode string `" + s + "` as " + t.String())
}
var allowedTimeFormats = []string{
"2006-01-02T15:04:05.999999999Z07:00",
"2006-01-02T15:04:05.999999999Z07",
"2006-01-02T15:04:05.999999999Z",
"2006-01-02T15:04:05.999999999",
"2006-01-02T15:04:05Z07:00",
"2006-01-02T15:04:05Z07",
"2006-01-02T15:04:05Z",
"2006-01-02T15:04:05",
"2006-01-02T15:04Z",
"2006-01-02T15:04",
"2006-01-02T15Z",
"2006-01-02T15",
"2006-01-02",
"2006-01",
"2006",
"15:04:05.999999999Z07:00",
"15:04:05.999999999Z07",
"15:04:05.999999999Z",
"15:04:05.999999999",
"15:04:05Z07:00",
"15:04:05Z07",
"15:04:05Z",
"15:04:05",
"15:04Z",
"15:04",
"15Z",
"15",
}
-388
View File
@@ -1,388 +0,0 @@
// Copyright 2014 Alvaro J. Genial. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package form
import (
"encoding"
"errors"
"fmt"
"io"
"net/url"
"reflect"
"strconv"
"strings"
"time"
)
// NewEncoder returns a new form Encoder.
func NewEncoder(w io.Writer) *Encoder {
return &Encoder{w, defaultDelimiter, defaultEscape, false}
}
// Encoder provides a way to encode to a Writer.
type Encoder struct {
w io.Writer
d rune
e rune
z bool
}
// DelimitWith sets r as the delimiter used for composite keys by Encoder e and returns the latter; it is '.' by default.
func (e *Encoder) DelimitWith(r rune) *Encoder {
e.d = r
return e
}
// EscapeWith sets r as the escape used for delimiters (and to escape itself) by Encoder e and returns the latter; it is '\\' by default.
func (e *Encoder) EscapeWith(r rune) *Encoder {
e.e = r
return e
}
// KeepZeros sets whether Encoder e should keep zero (default) values in their literal form when encoding, and returns the former; by default zero values are not kept, but are rather encoded as the empty string.
func (e *Encoder) KeepZeros(z bool) *Encoder {
e.z = z
return e
}
// Encode encodes dst as form and writes it out using the Encoder's Writer.
func (e Encoder) Encode(dst interface{}) error {
v := reflect.ValueOf(dst)
n, err := encodeToNode(v, e.z)
if err != nil {
return err
}
s := n.values(e.d, e.e).Encode()
l, err := io.WriteString(e.w, s)
switch {
case err != nil:
return err
case l != len(s):
return errors.New("could not write data completely")
}
return nil
}
// EncodeToString encodes dst as a form and returns it as a string.
func EncodeToString(dst interface{}) (string, error) {
v := reflect.ValueOf(dst)
n, err := encodeToNode(v, false)
if err != nil {
return "", err
}
vs := n.values(defaultDelimiter, defaultEscape)
return vs.Encode(), nil
}
// EncodeToValues encodes dst as a form and returns it as Values.
func EncodeToValues(dst interface{}) (url.Values, error) {
v := reflect.ValueOf(dst)
n, err := encodeToNode(v, false)
if err != nil {
return nil, err
}
vs := n.values(defaultDelimiter, defaultEscape)
return vs, nil
}
func encodeToNode(v reflect.Value, z bool) (n node, err error) {
defer func() {
if e := recover(); e != nil {
err = fmt.Errorf("%v", e)
}
}()
return getNode(encodeValue(v, z)), nil
}
func encodeValue(v reflect.Value, z bool) interface{} {
t := v.Type()
k := v.Kind()
if s, ok := marshalValue(v); ok {
return s
} else if !z && isEmptyValue(v) {
return "" // Treat the zero value as the empty string.
}
switch k {
case reflect.Ptr, reflect.Interface:
return encodeValue(v.Elem(), z)
case reflect.Struct:
if t.ConvertibleTo(timeType) {
return encodeTime(v)
} else if t.ConvertibleTo(urlType) {
return encodeURL(v)
}
return encodeStruct(v, z)
case reflect.Slice:
return encodeSlice(v, z)
case reflect.Array:
return encodeArray(v, z)
case reflect.Map:
return encodeMap(v, z)
case reflect.Invalid, reflect.Uintptr, reflect.UnsafePointer, reflect.Chan, reflect.Func:
panic(t.String() + " has unsupported kind " + t.Kind().String())
default:
return encodeBasic(v)
}
}
func encodeStruct(v reflect.Value, z bool) interface{} {
t := v.Type()
n := node{}
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
k, oe := fieldInfo(f)
if k == "-" {
continue
} else if fv := v.Field(i); oe && isEmptyValue(fv) {
delete(n, k)
} else {
n[k] = encodeValue(fv, z)
}
}
return n
}
func encodeMap(v reflect.Value, z bool) interface{} {
n := node{}
for _, i := range v.MapKeys() {
k := getString(encodeValue(i, z))
n[k] = encodeValue(v.MapIndex(i), z)
}
return n
}
func encodeArray(v reflect.Value, z bool) interface{} {
n := node{}
for i := 0; i < v.Len(); i++ {
n[strconv.Itoa(i)] = encodeValue(v.Index(i), z)
}
return n
}
func encodeSlice(v reflect.Value, z bool) interface{} {
t := v.Type()
if t.Elem().Kind() == reflect.Uint8 {
return string(v.Bytes()) // Encode byte slices as a single string by default.
}
n := node{}
for i := 0; i < v.Len(); i++ {
n[strconv.Itoa(i)] = encodeValue(v.Index(i), z)
}
return n
}
func encodeTime(v reflect.Value) string {
t := v.Convert(timeType).Interface().(time.Time)
if t.Year() == 0 && (t.Month() == 0 || t.Month() == 1) && (t.Day() == 0 || t.Day() == 1) {
return t.Format("15:04:05.999999999Z07:00")
} else if t.Hour() == 0 && t.Minute() == 0 && t.Second() == 0 && t.Nanosecond() == 0 {
return t.Format("2006-01-02")
}
return t.Format("2006-01-02T15:04:05.999999999Z07:00")
}
func encodeURL(v reflect.Value) string {
u := v.Convert(urlType).Interface().(url.URL)
return u.String()
}
func encodeBasic(v reflect.Value) string {
t := v.Type()
switch k := t.Kind(); k {
case reflect.Bool:
return strconv.FormatBool(v.Bool())
case reflect.Int,
reflect.Int8,
reflect.Int16,
reflect.Int32,
reflect.Int64:
return strconv.FormatInt(v.Int(), 10)
case reflect.Uint,
reflect.Uint8,
reflect.Uint16,
reflect.Uint32,
reflect.Uint64:
return strconv.FormatUint(v.Uint(), 10)
case reflect.Float32:
return strconv.FormatFloat(v.Float(), 'g', -1, 32)
case reflect.Float64:
return strconv.FormatFloat(v.Float(), 'g', -1, 64)
case reflect.Complex64, reflect.Complex128:
s := fmt.Sprintf("%g", v.Complex())
return strings.TrimSuffix(strings.TrimPrefix(s, "("), ")")
case reflect.String:
return v.String()
}
panic(t.String() + " has unsupported kind " + t.Kind().String())
}
func isEmptyValue(v reflect.Value) bool {
switch t := v.Type(); v.Kind() {
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
return v.Len() == 0
case reflect.Bool:
return !v.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return v.Uint() == 0
case reflect.Float32, reflect.Float64:
return v.Float() == 0
case reflect.Complex64, reflect.Complex128:
return v.Complex() == 0
case reflect.Interface, reflect.Ptr:
return v.IsNil()
case reflect.Struct:
if t.ConvertibleTo(timeType) {
return v.Convert(timeType).Interface().(time.Time).IsZero()
}
return reflect.DeepEqual(v, reflect.Zero(t))
}
return false
}
// canIndexOrdinally returns whether a value contains an ordered sequence of elements.
func canIndexOrdinally(v reflect.Value) bool {
if !v.IsValid() {
return false
}
switch t := v.Type(); t.Kind() {
case reflect.Ptr, reflect.Interface:
return canIndexOrdinally(v.Elem())
case reflect.Slice, reflect.Array:
return true
}
return false
}
func fieldInfo(f reflect.StructField) (k string, oe bool) {
if f.PkgPath != "" { // Skip private fields.
return omittedKey, oe
}
k = f.Name
tag := f.Tag.Get("form")
if tag == "" {
return k, oe
}
ps := strings.SplitN(tag, ",", 2)
if ps[0] != "" {
k = ps[0]
}
if len(ps) == 2 {
oe = ps[1] == "omitempty"
}
return k, oe
}
func findField(v reflect.Value, n string, ignoreCase bool) (reflect.Value, bool) {
t := v.Type()
l := v.NumField()
var lowerN string
caseInsensitiveMatch := -1
if ignoreCase {
lowerN = strings.ToLower(n)
}
// First try named fields.
for i := 0; i < l; i++ {
f := t.Field(i)
k, _ := fieldInfo(f)
if k == omittedKey {
continue
} else if n == k {
return v.Field(i), true
} else if ignoreCase && lowerN == strings.ToLower(k) {
caseInsensitiveMatch = i
}
}
// If no exact match was found try case insensitive match.
if caseInsensitiveMatch != -1 {
return v.Field(caseInsensitiveMatch), true
}
// Then try anonymous (embedded) fields.
for i := 0; i < l; i++ {
f := t.Field(i)
k, _ := fieldInfo(f)
if k == omittedKey || !f.Anonymous { // || k != "" ?
continue
}
fv := v.Field(i)
fk := fv.Kind()
for fk == reflect.Ptr || fk == reflect.Interface {
fv = fv.Elem()
fk = fv.Kind()
}
if fk != reflect.Struct {
continue
}
if ev, ok := findField(fv, n, ignoreCase); ok {
return ev, true
}
}
return reflect.Value{}, false
}
var (
stringType = reflect.TypeOf(string(""))
stringMapType = reflect.TypeOf(map[string]interface{}{})
timeType = reflect.TypeOf(time.Time{})
timePtrType = reflect.TypeOf(&time.Time{})
urlType = reflect.TypeOf(url.URL{})
)
func skipTextMarshalling(t reflect.Type) bool {
/*// Skip time.Time because its text unmarshaling is overly rigid:
return t == timeType || t == timePtrType*/
// Skip time.Time & convertibles because its text unmarshaling is overly rigid:
return t.ConvertibleTo(timeType) || t.ConvertibleTo(timePtrType)
}
func unmarshalValue(v reflect.Value, x interface{}) bool {
if skipTextMarshalling(v.Type()) {
return false
}
tu, ok := v.Interface().(encoding.TextUnmarshaler)
if !ok && !v.CanAddr() {
return false
} else if !ok {
return unmarshalValue(v.Addr(), x)
}
s := getString(x)
if err := tu.UnmarshalText([]byte(s)); err != nil {
panic(err)
}
return true
}
func marshalValue(v reflect.Value) (string, bool) {
if skipTextMarshalling(v.Type()) {
return "", false
}
tm, ok := v.Interface().(encoding.TextMarshaler)
if !ok && !v.CanAddr() {
return "", false
} else if !ok {
return marshalValue(v.Addr())
}
bs, err := tm.MarshalText()
if err != nil {
panic(err)
}
return string(bs), true
}
-14
View File
@@ -1,14 +0,0 @@
// Copyright 2014 Alvaro J. Genial. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package form implements encoding and decoding of application/x-www-form-urlencoded data.
package form
const (
implicitKey = "_"
omittedKey = "-"
defaultDelimiter = '.'
defaultEscape = '\\'
)
-152
View File
@@ -1,152 +0,0 @@
// Copyright 2014 Alvaro J. Genial. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package form
import (
"net/url"
"strconv"
"strings"
)
type node map[string]interface{}
func (n node) values(d, e rune) url.Values {
vs := url.Values{}
n.merge(d, e, "", &vs)
return vs
}
func (n node) merge(d, e rune, p string, vs *url.Values) {
for k, x := range n {
switch y := x.(type) {
case string:
vs.Add(p+escape(d, e, k), y)
case node:
y.merge(d, e, p+escape(d, e, k)+string(d), vs)
default:
panic("value is neither string nor node")
}
}
}
// TODO: Add tests for implicit indexing.
func parseValues(d, e rune, vs url.Values, canIndexFirstLevelOrdinally bool) node {
// NOTE: Because of the flattening of potentially multiple strings to one key, implicit indexing works:
// i. At the first level; e.g. Foo.Bar=A&Foo.Bar=B becomes 0.Foo.Bar=A&1.Foo.Bar=B
// ii. At the last level; e.g. Foo.Bar._=A&Foo.Bar._=B becomes Foo.Bar.0=A&Foo.Bar.1=B
// TODO: At in-between levels; e.g. Foo._.Bar=A&Foo._.Bar=B becomes Foo.0.Bar=A&Foo.1.Bar=B
// (This last one requires that there only be one placeholder in order for it to be unambiguous.)
m := map[string]string{}
for k, ss := range vs {
indexLastLevelOrdinally := strings.HasSuffix(k, string(d)+implicitKey)
for i, s := range ss {
if canIndexFirstLevelOrdinally {
k = strconv.Itoa(i) + string(d) + k
} else if indexLastLevelOrdinally {
k = strings.TrimSuffix(k, implicitKey) + strconv.Itoa(i)
}
m[k] = s
}
}
n := node{}
for k, s := range m {
n = n.split(d, e, k, s)
}
return n
}
func splitPath(d, e rune, path string) (k, rest string) {
esc := false
for i, r := range path {
switch {
case !esc && r == e:
esc = true
case !esc && r == d:
return unescape(d, e, path[:i]), path[i+1:]
default:
esc = false
}
}
return unescape(d, e, path), ""
}
func (n node) split(d, e rune, path, s string) node {
k, rest := splitPath(d, e, path)
if rest == "" {
return add(n, k, s)
}
if _, ok := n[k]; !ok {
n[k] = node{}
}
c := getNode(n[k])
n[k] = c.split(d, e, rest, s)
return n
}
func add(n node, k, s string) node {
if n == nil {
return node{k: s}
}
if _, ok := n[k]; ok {
panic("key " + k + " already set")
}
n[k] = s
return n
}
func isEmpty(x interface{}) bool {
switch y := x.(type) {
case string:
return y == ""
case node:
if s, ok := y[""].(string); ok {
return s == ""
}
return false
}
panic("value is neither string nor node")
}
func getNode(x interface{}) node {
switch y := x.(type) {
case string:
return node{"": y}
case node:
return y
}
panic("value is neither string nor node")
}
func getString(x interface{}) string {
switch y := x.(type) {
case string:
return y
case node:
if s, ok := y[""].(string); ok {
return s
}
return ""
}
panic("value is neither string nor node")
}
func escape(d, e rune, s string) string {
s = strings.Replace(s, string(e), string(e)+string(e), -1) // Escape the escape (\ => \\)
s = strings.Replace(s, string(d), string(e)+string(d), -1) // Escape the delimiter (. => \.)
return s
}
func unescape(d, e rune, s string) string {
s = strings.Replace(s, string(e)+string(d), string(d), -1) // Unescape the delimiter (\. => .)
s = strings.Replace(s, string(e)+string(e), string(e), -1) // Unescape the escape (\\ => \)
return s
}
-18
View File
@@ -1,18 +0,0 @@
#!/bin/bash -eu
# TODO: Only colorize messages given a suitable terminal.
# FIXME: Handle case in which no stash entry is created due to no changes.
printf "\e[30m=== PRE-COMMIT STARTING ===\e[m\n"
git stash save --quiet --keep-index --include-untracked
if go build -v ./... && go test -v -cover ./... && go vet ./... && golint . && travis-lint; then
result=$?
printf "\e[32m=== PRE-COMMIT SUCCEEDED ===\e[m\n"
else
result=$?
printf "\e[31m=== PRE-COMMIT FAILED ===\e[m\n"
fi
git stash pop --quiet
exit $result
@@ -23,3 +23,6 @@ _models/
_examples/
*.min.*
build/
cmd/chromad/static/chroma.wasm
cmd/chromad/static/wasm_exec.js
+8 -11
View File
@@ -1,7 +1,5 @@
run:
tests: true
skip-dirs:
- _examples
output:
print-issued-lines: false
@@ -9,8 +7,6 @@ output:
linters:
enable-all: true
disable:
- maligned
- megacheck
- lll
- gocyclo
- dupl
@@ -18,9 +14,7 @@ linters:
- funlen
- godox
- wsl
- gomnd
- gocognit
- goerr113
- nolintlint
- testpackage
- godot
@@ -28,12 +22,10 @@ linters:
- paralleltest
- nlreturn
- cyclop
- exhaustivestruct
- gci
- gofumpt
- errorlint
- exhaustive
- ifshort
- wrapcheck
- stylecheck
- thelper
@@ -46,15 +38,18 @@ linters:
- ireturn
- maintidx
- govet
- nosnakecase
- testableexamples
- musttag
- depguard
- goconst
- perfsprint
- mnd
- predeclared
- recvcheck
- tenv
- err113
linters-settings:
govet:
check-shadowing: true
gocyclo:
min-complexity: 10
dupl:
@@ -69,6 +64,8 @@ linters-settings:
issues:
exclude-dirs:
- _examples
max-per-linter: 0
max-same: 0
exclude-use-default: false
+18 -21
View File
@@ -4,34 +4,31 @@ release:
owner: alecthomas
name: chroma
brews:
-
install: bin.install "chroma"
- install: bin.install "chroma"
env:
- CGO_ENABLED=0
builds:
- goos:
- linux
- darwin
- windows
goarch:
- arm64
- amd64
- "386"
goarm:
- "6"
dir: ./cmd/chroma
main: .
ldflags: -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}
binary: chroma
- goos:
- linux
- darwin
- windows
goarch:
- arm64
- amd64
- "386"
goarm:
- "6"
dir: ./cmd/chroma
main: .
ldflags: -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}
binary: chroma
archives:
-
format: tar.gz
name_template: '{{ .Binary }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{
.Arm }}{{ end }}'
- format: tar.gz
name_template: "{{ .Binary }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
files:
- COPYING
- README*
snapshot:
name_template: SNAPSHOT-{{ .Commit }}
checksum:
name_template: '{{ .ProjectName }}-{{ .Version }}-checksums.txt'
name_template: "{{ .ProjectName }}-{{ .Version }}-checksums.txt"
+65
View File
@@ -0,0 +1,65 @@
# Multi-stage Dockerfile for chromad Go application using Hermit-managed tools
# Build stage
FROM ubuntu:24.04 AS builder
# Install system dependencies
RUN apt-get update && apt-get install -y \
curl \
git \
make \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Set working directory
WORKDIR /app
# Copy the entire project (including bin directory with Hermit tools)
COPY . .
# Make Hermit tools executable and add to PATH
ENV PATH="/app/bin:${PATH}"
# Set Go environment variables for static compilation
ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GOARCH=amd64
# Build the application using make
RUN make build/chromad
# Runtime stage
FROM alpine:3.23 AS runtime
# Install ca-certificates for HTTPS requests
RUN apk --no-cache add ca-certificates curl
# Create a non-root user
RUN addgroup -g 1001 chromad && \
adduser -D -s /bin/sh -u 1001 -G chromad chromad
# Set working directory
WORKDIR /app
# Copy the binary from build stage
COPY --from=builder /app/build/chromad /app/chromad
# Change ownership to non-root user
RUN chown chromad:chromad /app/chromad
# Switch to non-root user
USER chromad
# Expose port (default is 8080, but can be overridden via PORT env var)
EXPOSE 8080
# Set default environment variables
ENV PORT=8080
ENV CHROMA_CSRF_KEY="testtest"
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -fsSL http://127.0.0.1:8080/ > /dev/null
# Run the application
CMD ["sh", "-c", "./chromad --csrf-key=$CHROMA_CSRF_KEY --bind=0.0.0.0:$PORT"]
+24 -5
View File
@@ -6,17 +6,36 @@ export GOARCH ?= amd64
all: README.md tokentype_string.go
README.md: lexers/*/*.go
./table.py
README.md: lexers/*.go lexers/embedded/*.xml
GOOS= GOARCH= ./table.py
tokentype_string.go: types.go
go generate
chromad:
.PHONY: format-js
format-js:
biome format --write cmd/chromad/static/{index.js,chroma.js}
.PHONY: chromad
chromad: build/chromad
build/chromad: $(shell find cmd/chromad -name '*.go' -o -name '*.html' -o -name '*.css' -o -name '*.js') \
cmd/chromad/static/wasm_exec.js \
cmd/chromad/static/chroma.wasm
rm -rf build
esbuild --bundle cmd/chromad/static/index.js --minify --outfile=cmd/chromad/static/index.min.js
esbuild --platform=node --bundle cmd/chromad/static/index.js --minify --outfile=cmd/chromad/static/index.min.js
esbuild --bundle cmd/chromad/static/index.css --minify --outfile=cmd/chromad/static/index.min.css
(export CGOENABLED=0 ; cd ./cmd/chromad && go build -ldflags="-X 'main.version=$(VERSION)'" -o ../../build/chromad .)
(export CGOENABLED=0 ; go build -C cmd/chromad -ldflags="-X 'main.version=$(VERSION)'" -o ../../build/chromad .)
cmd/chromad/static/wasm_exec.js: $(shell tinygo env TINYGOROOT)/targets/wasm_exec.js
install -m644 $< $@
cmd/chromad/static/chroma.wasm: $(shell git ls-files | grep '\.go|\.xml')
if type tinygo > /dev/null; then \
tinygo build -no-debug -target wasm -o $@ cmd/libchromawasm/main.go; \
else \
GOOS=js GOARCH=wasm go build -o $@ cmd/libchromawasm/main.go; \
fi
upload: build/chromad
scp build/chromad root@swapoff.org: && \
+43 -33
View File
@@ -1,6 +1,9 @@
# Chroma — A general purpose syntax highlighter in pure Go
![Chroma](chroma.jpg)
# A general purpose syntax highlighter in pure Go
[![Go Reference](https://pkg.go.dev/badge/github.com/alecthomas/chroma/v2.svg)](https://pkg.go.dev/github.com/alecthomas/chroma/v2) [![CI](https://github.com/alecthomas/chroma/actions/workflows/ci.yml/badge.svg)](https://github.com/alecthomas/chroma/actions/workflows/ci.yml) [![Slack chat](https://img.shields.io/static/v1?logo=slack&style=flat&label=slack&color=green&message=gophers)](https://invite.slack.golangbridge.org/)
[![Golang Documentation](https://godoc.org/github.com/alecthomas/chroma?status.svg)](https://godoc.org/github.com/alecthomas/chroma) [![CI](https://github.com/alecthomas/chroma/actions/workflows/ci.yml/badge.svg)](https://github.com/alecthomas/chroma/actions/workflows/ci.yml) [![Slack chat](https://img.shields.io/static/v1?logo=slack&style=flat&label=slack&color=green&message=gophers)](https://invite.slack.golangbridge.org/)
Chroma takes source code and other structured text and converts it into syntax
highlighted HTML, ANSI-coloured text, etc.
@@ -31,33 +34,34 @@ translators for Pygments lexers and styles.
## Supported languages
| Prefix | Language |
| :----: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| A | ABAP, ABNF, ActionScript, ActionScript 3, Ada, Agda, AL, Alloy, Angular2, ANTLR, ApacheConf, APL, AppleScript, ArangoDB AQL, Arduino, ArmAsm, AutoHotkey, AutoIt, Awk |
| B | Ballerina, Bash, Bash Session, Batchfile, BibTeX, Bicep, BlitzBasic, BNF, BQN, Brainfuck |
| C | C, C#, C++, Caddyfile, Caddyfile Directives, Cap'n Proto, Cassandra CQL, Ceylon, CFEngine3, cfstatement, ChaiScript, Chapel, Cheetah, Clojure, CMake, COBOL, CoffeeScript, Common Lisp, Coq, Crystal, CSS, Cython |
| D | D, Dart, Dax, Desktop Entry, Diff, Django/Jinja, dns, Docker, DTD, Dylan |
| E | EBNF, Elixir, Elm, EmacsLisp, Erlang |
| F | Factor, Fennel, Fish, Forth, Fortran, FortranFixed, FSharp |
| G | GAS, GDScript, Genshi, Genshi HTML, Genshi Text, Gherkin, GLSL, Gnuplot, Go, Go HTML Template, Go Text Template, GraphQL, Groff, Groovy |
| H | Handlebars, Hare, Haskell, Haxe, HCL, Hexdump, HLB, HLSL, HolyC, HTML, HTTP, Hy |
| I | Idris, Igor, INI, Io, ISCdhcpd |
| J | J, Java, JavaScript, JSON, Julia, Jungle |
| K | Kotlin |
| L | Lighttpd configuration file, LLVM, Lua |
| M | Makefile, Mako, markdown, Mason, Materialize SQL dialect, Mathematica, Matlab, mcfunction, Meson, Metal, MiniZinc, MLIR, Modula-2, MonkeyC, MorrowindScript, Myghty, MySQL |
| N | NASM, Natural, Newspeak, Nginx configuration file, Nim, Nix |
| O | Objective-C, OCaml, Octave, Odin, OnesEnterprise, OpenEdge ABL, OpenSCAD, Org Mode |
| P | PacmanConf, Perl, PHP, PHTML, Pig, PkgConfig, PL/pgSQL, plaintext, Plutus Core, Pony, PostgreSQL SQL dialect, PostScript, POVRay, PowerQuery, PowerShell, Prolog, PromQL, Promela, properties, Protocol Buffer, PRQL, PSL, Puppet, Python, Python 2 |
| Q | QBasic, QML |
| R | R, Racket, Ragel, Raku, react, ReasonML, reg, Rego, reStructuredText, Rexx, RPMSpec, Ruby, Rust |
| S | SAS, Sass, Scala, Scheme, Scilab, SCSS, Sed, Sieve, Smali, Smalltalk, Smarty, Snobol, Solidity, SourcePawn, SPARQL, SQL, SquidConf, Standard ML, stas, Stylus, Svelte, Swift, SYSTEMD, systemverilog |
| T | TableGen, Tal, TASM, Tcl, Tcsh, Termcap, Terminfo, Terraform, TeX, Thrift, TOML, TradingView, Transact-SQL, Turing, Turtle, Twig, TypeScript, TypoScript, TypoScriptCssData, TypoScriptHtmlData |
| V | V, V shell, Vala, VB.net, verilog, VHDL, VHS, VimL, vue |
| W | WDTE, WebGPU Shading Language, Whiley |
| X | XML, Xorg |
| Y | YAML, YANG |
| Z | Z80 Assembly, Zed, Zig |
| Prefix | Language
| :----: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| A | ABAP, ABNF, ActionScript, ActionScript 3, Ada, Agda, AL, Alloy, Angular2, ANTLR, ApacheConf, APL, AppleScript, ArangoDB AQL, Arduino, ArmAsm, ATL, AutoHotkey, AutoIt, Awk
| B | Ballerina, Bash, Bash Session, Batchfile, Beef, BibTeX, Bicep, BlitzBasic, BNF, BQN, Brainfuck
| C | C, C#, C++, C3, Caddyfile, Caddyfile Directives, Cap'n Proto, Cassandra CQL, Ceylon, CFEngine3, cfstatement, ChaiScript, Chapel, Cheetah, Clojure, CMake, COBOL, CoffeeScript, Common Lisp, Coq, Core, Crystal, CSS, CSV, CUE, Cython
| D | D, Dart, Dax, Desktop file, Diff, Django/Jinja, dns, Docker, DTD, Dylan
| E | EBNF, Elixir, Elm, EmacsLisp, Erlang
| F | Factor, Fennel, Fish, Forth, Fortran, FortranFixed, FSharp
| G | GAS, GDScript, GDScript3, Gemtext, Genshi, Genshi HTML, Genshi Text, Gherkin, Gleam, GLSL, Gnuplot, Go, Go HTML Template, Go Template, Go Text Template, GraphQL, Groff, Groovy
| H | Handlebars, Hare, Haskell, Haxe, HCL, Hexdump, HLB, HLSL, HolyC, HTML, HTTP, Hy
| I | Idris, Igor, INI, Io, ISCdhcpd
| J | J, Janet, Java, JavaScript, JSON, JSONata, Jsonnet, Julia, Jungle
| K | Kakoune, Kotlin
| L | Lean4, Lighttpd configuration file, LLVM, lox, Lua
| M | Makefile, Mako, markdown, Mason, Materialize SQL dialect, Mathematica, Matlab, MCFunction, Meson, Metal, MiniZinc, MLIR, Modelica, Modula-2, Mojo, MonkeyC, MoonScript, MorrowindScript, Myghty, MySQL
| N | NASM, Natural, NDISASM, Newspeak, Nginx configuration file, Nim, Nix, NSIS, Nu
| O | Objective-C, ObjectPascal, OCaml, Octave, Odin, OnesEnterprise, OpenEdge ABL, OpenSCAD, Org Mode
| P | PacmanConf, Perl, PHP, PHTML, Pig, PkgConfig, PL/pgSQL, plaintext, Plutus Core, Pony, PostgreSQL SQL dialect, PostScript, POVRay, PowerQuery, PowerShell, Prolog, Promela, PromQL, properties, Protocol Buffer, Protocol Buffer Text Format, PRQL, PSL, Puppet, Python, Python 2
| Q | QBasic, QML
| R | R, Racket, Ragel, Raku, react, ReasonML, reg, Rego, reStructuredText, Rexx, RGBDS Assembly, Ring, RPGLE, RPMSpec, Ruby, Rust
| S | SAS, Sass, Scala, Scheme, Scilab, SCSS, Sed, Sieve, Smali, Smalltalk, Smarty, SNBT, Snobol, Solidity, SourcePawn, SPARQL, SQL, SquidConf, Standard ML, stas, Stylus, Svelte, Swift, SYSTEMD, systemverilog
| T | TableGen, Tal, TASM, Tcl, Tcsh, Termcap, Terminfo, Terraform, TeX, Thrift, TOML, TradingView, Transact-SQL, Turing, Turtle, Twig, TypeScript, TypoScript, TypoScriptCssData, TypoScriptHtmlData, Typst
| U | ucode
| V | V, V shell, Vala, VB.net, verilog, VHDL, VHS, VimL, vue
| W | WDTE, WebAssembly Text Format, WebGPU Shading Language, WebVTT, Whiley
| X | XML, Xorg
| Y | YAML, YANG
| Z | Z80 Assembly, Zed, Zig
_I will attempt to keep this section up to date, but an authoritative list can be
displayed with `chroma --list`._
@@ -205,13 +209,12 @@ using the included Python 3 script `pygments2chroma_xml.py`. I use something lik
the following:
```sh
python3 _tools/pygments2chroma_xml.py \
uv run --script _tools/pygments2chroma_xml.py \
pygments.lexers.jvm.KotlinLexer \
> lexers/embedded/kotlin.xml
```
See notes in [pygments-lexers.txt](https://github.com/alecthomas/chroma/blob/master/pygments-lexers.txt)
for a list of lexers, and notes on some of the issues importing them.
A list of all lexers available in Pygments can be found in [pygments-lexers.txt](https://github.com/alecthomas/chroma/blob/master/pygments-lexers.txt).
### Formatters
@@ -267,12 +270,19 @@ on under the hood for easy integration with [lesspipe shipping with
Debian and derivatives](https://manpages.debian.org/lesspipe#USER_DEFINED_FILTERS);
for that setup the `chroma` executable can be just symlinked to `~/.lessfilter`.
## Projects using Chroma
* [`moor`](https://github.com/walles/moor) is a full-blown pager that colorizes
its input using Chroma
* [Hugo](https://gohugo.io/) is a static site generator that [uses Chroma for syntax
highlighting code examples](https://gohugo.io/content-management/syntax-highlighting/)
## Testing lexers
If you edit some lexers and want to try it, open a shell in `cmd/chromad` and run:
```shell
go run .
go run . --csrf-key=securekey
```
A Link will be printed. Open it in your Browser. Now you can test on the Playground with your local changes.

Some files were not shown because too many files have changed in this diff Show More