Compare commits

..
1728 Commits
Author SHA1 Message Date
Umputun 0521c7024c attempt to fix ci
another fix for ci

fix build tagged images
2022-01-12 12:16:37 -06:00
Ivan NedzveckijandUmputun c3b39b41ae telegram notifications format headers, resolve #1202 2022-01-12 11:40:00 -06:00
Paul MineevandUmputun f6772a7253 use double quotes 2021-12-24 04:17:39 -06:00
esvyridovandUmputun ab0c29cb0c Add container to a markdown table with overflow-x auto 2021-12-24 04:17:39 -06:00
esvyridovandUmputun f4ef0a7e78 Add data-testid to preloader and spinner, add test case for profile.spec 2021-12-23 17:44:45 -06:00
esvyridovandUmputun 78e2be8ea4 Show Preloader only on first request for comments in Profile sidebar 2021-12-23 17:44:45 -06:00
Pavel MineevandUmputun 6ec10c877b fix lint 2021-12-23 17:44:45 -06:00
Pavel MineevandUmputun 19121ff0b7 Review for load more comments in profile
- compose everything inside fetchComments
- put skip counter in ref and prevent unnecessary rerenders
- got rid of additional handlers for loading
- add spinner as loading indicator for loading of additional comments
2021-12-23 17:44:45 -06:00
esvyridovandUmputun 250f730d5e Remove unnecessary useMemo from Profile, add ru translation for user.load-more 2021-12-23 17:44:45 -06:00
esvyridovandUmputun 0bf5a91091 Add tests for updated getUserComments func 2021-12-23 17:44:45 -06:00
esvyridovandUmputun cd391a8b0e Add Load More button to profile sidebar 2021-12-23 17:44:45 -06:00
esvyridovandUmputun 53f02ab4cd Remove expandability of menu items from docs site 2021-12-23 12:59:16 -06:00
Dmitry VerkhoturovandUmputun 151913f371 style fixes to the documentation and CLI params description 2021-12-20 13:36:58 -06:00
Dmitry VerkhoturovandUmputun e341e25f0f improve notifications documentation (telegram, webhook) 2021-12-20 02:56:38 -06:00
Dmitry VerkhoturovandUmputun 4f672cdec5 fix formatting for telegram reply notifications 2021-12-20 00:07:15 -06:00
Dmitry VerkhoturovandUmputun 11c8bf6228 do not issue deprecation warning on notify.type by default
Default configuration for notify.type is "none",
and prior to this change it was issuing the
deprecation warning which was not an intended
behaviour.
2021-12-19 23:32:48 -06:00
Dmitry VerkhoturovandUmputun f43dfd57e0 clarify telegram channel ID param and documentation 2021-12-19 13:56:16 -06:00
Dmitry VerkhoturovandUmputun e1d629191e properly format email documentation 2021-12-19 13:54:49 -06:00
Umputun 27888eb331 switch base images to v1.8.0 2021-12-12 17:31:21 -06:00
Umputun fcb13e2016 update mod sum for examples 2021-12-07 13:12:34 -06:00
Umputun 5cb7a3f64b fix mod inconsistency 2021-12-07 13:08:23 -06:00
Umputun f621555ef4 update auth lib with /status support
potential fix for #1188 can use /auth/status
2021-12-07 13:03:12 -06:00
Bal Krishna JhaandUmputun c8361a3844 Add link of docker-compose file 2021-12-04 13:33:24 -06:00
Yuriy SynyaievandUmputun a1050472c4 feat: add cursor pointer for buttons 2021-11-29 03:00:57 -06:00
Dmitry VerkhoturovandUmputun 90e537358d update golangci-lint to 1.43.0, fix found issues 2021-11-23 15:00:40 -06:00
esvyridovandPaul Mineev 8e77acd908 Fix broken verified user icon 2021-11-22 14:12:21 -08:00
TimofeyandUmputun f03f99b5b4 update ru locale 2021-11-19 11:19:38 -06:00
adfslslddkdsandGitHub 2387518831 Add favicon to site (#1177)
* add favicon files

* do not add font description to production

* add correct image for favicon

* fix .dockerignore

by @adfslslddkds
2021-11-16 14:16:58 -06:00
witjemandUmputun 473e3328e1 updated comment for rest.SendErrorJSON 2021-11-12 03:32:14 -06:00
KsiniaandUmputun 832f4fd858 Fix typos in frontend code 2021-11-09 02:05:41 -06:00
Dmitry VerkhoturovandUmputun be3643ed10 use single loop for telegram auth and notify 2021-11-09 02:04:51 -06:00
Dmitry VerkhoturovandUmputun c751fbaf37 bump project dependencies, go-auth 2021-11-08 11:41:07 -06:00
Dmitry VerkhoturovandUmputun 90ec2ff907 move more parts of Readme to the docs site 2021-11-07 11:58:08 -06:00
Dmitry VerkhoturovandUmputun 19a4eada25 move privacy section of readme to the docs site 2021-11-07 11:53:28 -06:00
Dmitry VerkhoturovandUmputun c027dcd765 enable telegram notify trough writing bot a message
Previously it was done through writing bot first,
clicking a button, copying the token, and pasting
it into the web interface.

The new flow is way simpler: click the link
to write bot a message, then click the "Check"
button in the web UI and you got notifications
enabled.
2021-11-07 11:51:28 -06:00
Dmitry VerkhoturovandUmputun e1a2374a2d clarify telegram notification flow 2021-11-04 15:42:04 -05:00
Dmitry VerkhoturovandUmputun 6449b7d92b improve telegram notifications
These changes are designed to ease the transition into
the simplified telegram notifications verification model.
2021-11-01 14:35:37 -05:00
Dmitry VerkhoturovandUmputun c852ea4834 bump go modules 2021-10-29 11:48:46 -05:00
Umputun 8818faf189 add darwin arm64 bin release 2021-10-28 23:30:45 -05:00
Dmitry VerkhoturovandUmputun 4d03c20be6 fix non-HTTPS site link on the privacy page 2021-10-25 17:29:55 -05:00
Dmitry VerkhoturovandUmputun b3d28f97ae fix links in docs, move frontend readme to site 2021-10-25 17:21:24 -05:00
Dmitry VerkhoturovandUmputun 8fc9141f19 extract TelegramBotInfo structure 2021-10-25 16:47:15 -05:00
Dmitry VerkhoturovandUmputun ca7cbedea4 regenerate mocks and use require.NoError in place of Nil 2021-10-25 16:47:15 -05:00
Dmitry VerkhoturovandUmputun 498073c509 send telegram messages in HTML mode
That resolves problem with inability
to properly render message text in the
resulting telegram message due to markdown
escaping trickiness.
2021-10-24 14:05:06 -05:00
Dmitry VerkhoturovandUmputun f817fd38ed improve telegram messages escaping
Due to the wrong order of `html.UnescapeString`
applying, messages sometimes ended up crippled.

Due to `parse_mode=Markdown` set in Telegram
send message call, `ParseMode` in message
was ignored.
2021-10-24 14:05:06 -05:00
esvyridovandUmputun ad51e087b1 Add EOF to counter.module.css 2021-10-23 11:39:33 -05:00
esvyridovandUmputun ad5aed9787 Rename commentsCounts variable to commentsAmount, update condition for showing amount of comments 2021-10-23 11:39:33 -05:00
esvyridovandUmputun 27ee16180b Add user.my-comments and user.comments keys to it translations 2021-10-23 11:39:33 -05:00
esvyridovandUmputun f104c3822f Add tests for comments counter in profile sidebar 2021-10-23 11:39:33 -05:00
esvyridovandUmputun eb941fc095 Update profile sidebar title, add comments counter next to the title 2021-10-23 11:39:33 -05:00
Dmitry VerkhoturovandUmputun aa34dc8384 move non-technical part of the readme to site 2021-10-23 11:37:23 -05:00
Dmitry VerkhoturovandUmputun c222b8a2ba cut parameters list from readme to site 2021-10-22 01:21:33 -05:00
Dmitry VerkhoturovandUmputun 032f88c61e move technical part of readme to docs site 2021-10-21 13:06:40 -05:00
MiloandUmputun 3d06a04acd Updated documentation with new Italian translations 2021-10-19 11:50:22 -05:00
MiloandUmputun 2889cd0b40 Add italian translations 2021-10-19 11:50:22 -05:00
Umputun edb45a61fc fix example mod 2021-10-18 23:53:08 -05:00
Umputun bec8079af9 update bluemonday deps 2021-10-18 23:45:22 -05:00
Dmitry VerkhoturovandUmputun 4793a31075 move previewCommentCtrl to private REST struct 2021-10-16 11:04:03 -05:00
Dmitry VerkhoturovandUmputun d0d4f06bad replace deprecated CLI options with current ones 2021-10-16 10:58:35 -05:00
dependabot[bot]andUmputun 7b1de77dc2 Bump tmpl from 1.0.4 to 1.0.5 in /frontend
Bumps [tmpl](https://github.com/daaku/nodejs-tmpl) from 1.0.4 to 1.0.5.
- [Release notes](https://github.com/daaku/nodejs-tmpl/releases)
- [Commits](https://github.com/daaku/nodejs-tmpl/commits/v1.0.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-15 13:38:21 -05:00
UmputunandGitHub 73f7e203b6 Merge pull request #1144
patreon auth
2021-10-15 13:36:40 -05:00
romanilchyshyn df0d4d27fa patreon auth 2021-10-13 00:30:14 +03:00
Umputun 0c069c1e8f remove hard breaks from privacy policy 2021-10-10 12:49:02 -05:00
Pavel MineevandUmputun 24f989109a restore pricavy policy, add footer 2021-10-10 12:32:42 -05:00
Umputun 106fb1ff38 update security versions 2021-10-09 13:50:50 -05:00
Umputun 6d81eaefd1 revert as dup 2021-10-09 13:10:56 -05:00
Umputun 509d29dea6 add api section to docs 2021-10-09 13:05:03 -05:00
romanilchyshynandUmputun e341227cae import from commento engine 2021-10-06 13:51:31 -05:00
Dmitry VerkhoturovandUmputun 5abeab4008 stop Ticker after use to prevent the memory leak
https://github.com/golang/go/wiki/CodeReviewConcurrency#ticker-stop
2021-10-03 16:09:30 -06:00
Umputun 7f575c4dd7 add multiarch make target 2021-09-24 16:17:47 -05:00
dependabot[bot]andUmputun ab3a6f7fe6 Bump nth-check from 2.0.0 to 2.0.1 in /site
Bumps [nth-check](https://github.com/fb55/nth-check) from 2.0.0 to 2.0.1.
- [Release notes](https://github.com/fb55/nth-check/releases)
- [Commits](https://github.com/fb55/nth-check/compare/v2.0.0...v2.0.1)

---
updated-dependencies:
- dependency-name: nth-check
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-21 10:40:00 -05:00
Umputun 198759ca1b rename jobs to avoid confusions 2021-09-21 03:52:31 -05:00
Pavel MineevandUmputun 337b4d633e update site deps to the last versions 2021-09-21 03:48:54 -05:00
dependabot[bot]andUmputun 65479f8207 Bump prismjs from 1.24.0 to 1.25.0 in /site
Bumps [prismjs](https://github.com/PrismJS/prism) from 1.24.0 to 1.25.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.24.0...v1.25.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-21 02:54:19 -05:00
Umputun 8a640b98f6 fix ci formatting 2021-09-21 01:44:41 -05:00
Umputun 9fbf095225 add multi-arch docker build 2021-09-21 01:43:21 -05:00
Dmitry VerkhoturovandUmputun 3f0ede560c bump go modules in the project 2021-09-08 11:34:48 -05:00
Pavel MineevandUmputun 5c59e87f94 bump vulnerable frontend dependencies 2021-09-08 11:32:39 -05:00
Dmitry VerkhoturovandUmputun 2443b9d0a2 prevent data race within TestService_UserReplies 2021-09-04 15:22:02 -05:00
Dmitry VerkhoturovandUmputun fa6c0e0d5e don't use "t" inside assert.Eventually 2021-09-04 13:00:25 -05:00
Pavel MineevandUmputun 987a066a80 add more space between error message and retry button 2021-09-04 12:45:07 -05:00
esvyridovandUmputun c0dc64f0a0 Update user profile UI: move loading and error to center 2021-09-04 12:45:07 -05:00
esvyridovandUmputun 3d3162de41 Update UI for user comments sidebar 2021-09-04 12:45:07 -05:00
Dmitry VerkhoturovandUmputun bb3c86281c make TestService_UserReplies more robust
It flaps (see #380), and with this change, it will
have more time to get the expected output in
the flaky GitHub Actions environment.
2021-09-04 12:44:42 -05:00
Umputun 91441d1160 add controversy and imported to untrusted list 2021-09-01 14:21:52 -05:00
Umputun 72bdf1b176 fix quoting issue 2021-09-01 14:18:18 -05:00
Umputun e90dae2b94 sanitize Title on find level as well 2021-09-01 14:14:12 -05:00
Umputun a7b44eee1a sanitize PostTitle 2021-09-01 13:56:06 -05:00
Umputun 8754add874 add skip to user's comment rest request #1085 2021-08-29 12:45:35 -05:00
bakurinandUmputun be46e849a4 Webhook destination for notifications 2021-08-28 12:50:36 -05:00
Sergii GatezhandUmputun 9f6c766919 Apply feedback 2021-08-20 11:22:31 -05:00
6f3fa4084c Update frontend/app/locales/ua.json
Co-authored-by: Eugene <ievgenteslia@gmail.com>
2021-08-20 11:22:31 -05:00
de0656d6a9 Update frontend/app/locales/ua.json
Co-authored-by: Eugene <ievgenteslia@gmail.com>
2021-08-20 11:22:31 -05:00
4556133371 Update frontend/app/locales/ua.json
Co-authored-by: Eugene <ievgenteslia@gmail.com>
2021-08-20 11:22:31 -05:00
f92b49d3d0 Update frontend/app/locales/ua.json
Co-authored-by: Eugene <ievgenteslia@gmail.com>
2021-08-20 11:22:31 -05:00
Sergii GatezhandUmputun 02f671b891 Apply feedback 2021-08-20 11:22:31 -05:00
5c083e7e0a Update frontend/app/locales/ua.json
Co-authored-by: Eugene <ievgenteslia@gmail.com>
2021-08-20 11:22:31 -05:00
Sergii GatezhandUmputun 61230646b3 Update Ukrainian locale 2021-08-20 11:22:31 -05:00
Pavel MineevandUmputun 5718a699cf remove unused const, move single used var to chunk 2021-08-19 12:55:56 -05:00
Dmitry VerkhoturovandUmputun fd5df39fe2 bump backend auth module to fix telegram auth 2021-08-17 16:33:53 -05:00
RikoDEVandUmputun f0ae4b6de8 Update Polish localization 2021-08-15 14:24:42 -05:00
UmputunandGitHub de4ab97c9a Merge pull request #1101 from umputun/paskal/email_doc
clarify user and admin email instructions
2021-08-14 14:19:33 -05:00
Dmitry VerkhoturovandGitHub 58409ae05c clarify user and admin email instructions 2021-08-14 12:41:55 +02:00
Umputun 193b107590 add remote site deployment from master 2021-08-11 01:45:39 -05:00
UmputunandGitHub 059d6f0455 Merge pull request #1098 from umputun/dependabot/npm_and_yarn/frontend/url-parse-1.5.3 2021-08-10 18:53:16 -05:00
dependabot[bot]andGitHub 6f70823df8 Bump url-parse from 1.5.1 to 1.5.3 in /frontend
Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.5.1 to 1.5.3.
- [Release notes](https://github.com/unshiftio/url-parse/releases)
- [Commits](https://github.com/unshiftio/url-parse/compare/1.5.1...1.5.3)

---
updated-dependencies:
- dependency-name: url-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-10 23:52:56 +00:00
UmputunandGitHub 7945240436 Merge pull request #1097 from umputun/dependabot/npm_and_yarn/frontend/path-parse-1.0.7 2021-08-10 18:51:28 -05:00
dependabot[bot]andGitHub 29ff5aee94 Bump path-parse from 1.0.6 to 1.0.7 in /frontend
Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

---
updated-dependencies:
- dependency-name: path-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-10 23:50:46 +00:00
UmputunandGitHub f5ffb208b1 Merge pull request #1096 from umputun/site-fb-auth-manual
docs: edit facebook oauth doc
2021-08-10 02:44:26 -05:00
Pavel Mineev f2c13d3126 edit facebook oauth manual 2021-08-10 10:36:21 +03:00
Umputun 3f7fbb4b31 rename site build 2021-08-09 19:45:43 -05:00
Umputun b769fe45bd change nginx-le to reproxy 2021-08-09 19:43:13 -05:00
UmputunandGitHub ff9359aa14 Merge pull request #1086 from umputun/site-link-to-releases
set releases link on version in header
2021-08-09 16:25:03 -05:00
Pavel MineevandUmputun 184ac7efb6 fix title on icon button 2021-08-09 11:10:19 -05:00
Pavel MineevandUmputun 79386a8b14 move docs about third party soft in manual section 2021-08-09 11:09:33 -05:00
Pavel MineevandUmputun 712728f554 update license year 2021-08-09 11:08:42 -05:00
Pavel MineevandGitHub 67ab44dd82 Merge branch 'master' into site-link-to-releases 2021-08-09 10:39:28 +03:00
Pavel MineevandUmputun 5668614945 fix link to demo page in header 2021-08-09 02:04:46 -05:00
Pavel Mineev 57af9e7a66 set releases link on version in header 2021-08-09 00:49:30 +03:00
Pavel MineevandUmputun 7f0a01a6ca fix demo page locator 2021-08-08 15:50:23 -05:00
Umputun 878d5c6cf3 change CMD to RUN in the build step 2021-08-08 15:21:21 -05:00
Umputun cc686557ef typo 2021-08-08 15:16:12 -05:00
Umputun a752f14ecb no-cache for site ci 2021-08-08 15:14:47 -05:00
Umputun 7b0da93f64 add ls to ci build step 2021-08-08 15:04:32 -05:00
Umputun 8729ff81cd missing working directory 2021-08-08 14:59:46 -05:00
Umputun f7d099d8fc typo 2021-08-08 14:48:54 -05:00
Umputun 94f6ff0aef add ci build for site 2021-08-08 14:48:13 -05:00
Pavel MineevandUmputun cac1eb2601 remove unused code 2021-08-08 14:25:10 -05:00
Pavel MineevandUmputun 3216641cbc unify icons 2021-08-08 14:25:10 -05:00
Pavel MineevandUmputun 3314ed30a8 add sort-picker 2021-08-08 14:25:10 -05:00
Pavel MineevandUmputun e5e9d5a10e format 2021-08-08 14:25:10 -05:00
Pavel MineevandUmputun 76244fca88 remove unused vars 2021-08-08 14:25:10 -05:00
Pavel MineevandUmputun a733733e69 update sort-picker 2021-08-08 14:25:10 -05:00
Pavel MineevandUmputun b47174af4a add tests for sort picker and update select tests 2021-08-08 14:25:10 -05:00
Pavel MineevandUmputun 718ad691c0 add tests for select 2021-08-08 14:25:10 -05:00
Pavel MineevandUmputun 7317d46699 update locales 2021-08-08 14:25:10 -05:00
Pavel MineevandUmputun a04204b3d5 move sort picker below comment form 2021-08-08 14:25:10 -05:00
Pavel MineevandUmputun be41016266 fix: open own profile from comment 2021-08-08 14:25:10 -05:00
Pavel MineevandUmputun c6fcb06086 fix typo 2021-08-08 14:25:10 -05:00
Pavel MineevandUmputun 484e547c79 fix spinner 2021-08-08 14:25:10 -05:00
Pavel MineevandUmputun 3a20bfcbb2 profile popup 2021-08-08 14:25:10 -05:00
Umputun 628dd898b6 change site docker build to proper two-stage with repoxy to serve content 2021-08-08 13:55:10 -05:00
Dmitry VerkhoturovandUmputun 9df8de511a Run telegram auth goroutine
Fix for https://github.com/go-pkgz/auth/issues/90
2021-08-06 16:56:07 -05:00
Dmitry VerkhoturovandUmputun 7b28cb9fa9 fix typo in oauth 2021-08-05 18:14:50 -05:00
Pavel MineevandUmputun a45c1d1994 update docs nav definition 2021-08-05 12:57:43 -05:00
Pavel MineevandUmputun ad1f95dc74 format md files
- use doublequote in yaml
- right indentation in yaml
- format missformatted places
2021-08-05 12:22:49 -05:00
Pavel MineevandUmputun c5e3ea898d fix images 2021-08-05 03:23:51 -05:00
Pavel MineevandUmputun 8d33265487 remove quote style because it isn't supported 2021-08-03 03:51:22 -05:00
Pavel MineevandUmputun 36840ed240 double quotes in yml 2021-08-03 03:22:27 -05:00
Pavel MineevandUmputun b27a09fb3f Disable injection of last-comments.css in dev
...because it handled by webpack
2021-08-01 16:08:19 -05:00
Pavel MineevandUmputun 2285bae782 add eof 2021-08-01 12:26:40 -05:00
Pavel MineevandUmputun 0352d047fa use 2 space indentation for yml 2021-08-01 12:26:40 -05:00
Pavel MineevandUmputun d7936a6a29 optimize css 2021-07-29 10:35:13 -05:00
Dmitry VerkhoturovandUmputun 0efc04e5cd fix deprecated flag, improve tests 2021-07-27 11:45:42 -05:00
Umputun 4c2f2aa097 change go.mod version to 1.16, change ci to 1.16 2021-07-27 04:25:27 -05:00
Pavel MineevandUmputun 4d1d32016e fix typo 2021-07-27 04:22:40 -05:00
Pavel MineevandUmputun deb7f7c209 use spaces for indentation in markdown
- adds global editorconfig (supports in intellij by default and with plugin in vscode)
- fixes prettier config
2021-07-27 04:22:40 -05:00
Pavel MineevandUmputun 6b9770b8b6 fix placeholder 2021-07-27 04:22:40 -05:00
Pavel MineevandUmputun 07b6454b79 add docs from readme on the site 2021-07-27 04:22:40 -05:00
Umputun 5abdaaf793 clarify image proxy description 2021-07-26 21:25:22 -05:00
Umputun b66c94e002 migrate jwt lib to maintained fork and updated (the same way) go-pkgz/auth 2021-07-26 21:06:12 -05:00
Pavel MoiseenkoandUmputun 2d32765453 Update information in index.md
Synchronize the project description with the description from the repository.
2021-07-26 11:01:55 -05:00
Dmitry VerkhoturovandUmputun 2c8556ff2f consistent formatting for markdown files 2021-07-25 13:28:08 -05:00
Dmitry VerkhoturovandUmputun ea644a1a31 add telegram auth backend support 2021-07-25 13:16:50 -05:00
Pavel MoiseenkoandUmputun 50b3776d2a Improve README
* Update links
* Update table of contents
* Improve the design
* Improve formatting
* Fix abbreviations and proper names
* Fix tables
* Fix indentation in code snippets
* Fix typos
2021-07-25 13:08:34 -05:00
Dmitry VerkhoturovandUmputun f093d6a07a don't run CI pipelines on markdown file changes 2021-07-25 13:06:30 -05:00
Dmitry VerkhoturovandUmputun c15a280181 don't run CI pipelines on frontend Readme file change 2021-07-25 12:58:48 -05:00
Umputun ad54a9450b fix parent tg formatting 2021-07-04 00:30:11 -05:00
Umputun b8050f817c quote parrent comment in tg notif 2021-07-04 00:03:23 -05:00
Umputun 70245504f5 restore link to original in tg notif 2021-07-03 23:52:20 -05:00
Umputun 05a85b6bfd reformat tg message to make it readable again 2021-07-03 23:48:00 -05:00
Dmitry VerkhoturovandUmputun 83ae758573 address review commends 2021-07-03 14:57:09 -05:00
Dmitry VerkhoturovandUmputun 200733ed03 add user telegram notifications 2021-07-03 14:57:09 -05:00
dependabot[bot]andUmputun 7f081e1d2c Bump prismjs from 1.23.0 to 1.24.0 in /site
Bumps [prismjs](https://github.com/PrismJS/prism) from 1.23.0 to 1.24.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.23.0...v1.24.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-28 16:05:01 -05:00
Dmitry VerkhoturovandUmputun 936ccd825f Move existing documentation to the new site 2021-06-27 14:53:20 -05:00
Pavel MineevandUmputun 68aba33765 fix typo 2021-06-16 01:27:10 -05:00
Pavel MineevandUmputun 5aa24f95fc fix more comments button 2021-06-16 01:17:20 -05:00
Dmitry VerkhoturovandUmputun 2b15e9291f fix typos 2021-06-15 02:02:48 -05:00
Namkhai BandUmputun 98bfc7f5f5 Don't set X-XSRF-TOKEN when the user isn't logged in
An HTTP header cannot be empty, and although some webservers allow this
(nginx, Apache), others answer 400 Bad Request (lighttpd), preventing
the widget from loading.
2021-06-15 02:01:57 -05:00
Dmitry VerkhoturovandUmputun 1847184960 clarify telegram notifications code and text 2021-06-13 19:07:56 -05:00
Dmitry VerkhoturovandUmputun ef6979e7d6 improve email notifications text 2021-06-13 19:07:56 -05:00
Dmitry VerkhoturovandUmputun b96f29aa26 add Intellij http-client.env.json to gitignore 2021-06-13 19:07:56 -05:00
dfcf728e6f Site (#1049)
by @akellbl4 

* create infrastructure for site

* wip

* fix docker build and add readme

* add docker-compose as a build and a run method

* rename compose file yaml -> yml

* add `src` as volume for watching changes

* update configs

* update README

* add padding at the end of the pages

* move demo settings in config

* fetch latest release from github

* update docs navigation

- add sections
- redirect from root of the section to first doc
- nice styles for navigation
- add brand colors

* cache github data from first load

* add redirects and fix link to docs

* fix docs nav styles

* add installation page placeholder

* fix demo

* add 404

* add dark theme, add theme switcher, remove unused files

* fix dark theme on main page

* fix dark theme background

* fix node version

* change installation docs

* add note block

* minor fixes

* add code highlighting styles

* fixes

* fixes

* mobile navigation, fix code highlighting colors

* fix dev server

* fix fetching error

* fix path to edit

Co-authored-by: Pavel Mineev <pavel@mineev.me>
Co-authored-by: Dmitry Verkhoturov <paskal.07@gmail.com>
2021-06-13 18:44:49 -05:00
StanislasandUmputun 5ce89c6c29 docs(import): clarify import path for wordpress 2021-06-13 17:40:01 -05:00
Dmitry VerkhoturovandUmputun de6e541604 fix telegram token server setting 2021-06-13 15:47:52 -05:00
Dmitry VerkhoturovandUmputun fd4c6ceb18 add UserDetailTelegram support 2021-06-13 13:18:33 -05:00
Dmitry VerkhoturovandUmputun d6167980f4 remove ability to set telegram API, clarify params 2021-06-13 01:49:15 -05:00
Dmitry VerkhoturovandUmputun ea15b28bf6 clarification of notify comments and code 2021-06-12 12:59:26 -05:00
Pavel MineevandUmputun 4f0002abdb chage iframe size if any element is changed 2021-06-09 13:09:57 -05:00
dependabot[bot]andUmputun be60c7a481 Bump trim-newlines from 3.0.0 to 3.0.1 in /frontend
Bumps [trim-newlines](https://github.com/sindresorhus/trim-newlines) from 3.0.0 to 3.0.1.
- [Release notes](https://github.com/sindresorhus/trim-newlines/releases)
- [Commits](https://github.com/sindresorhus/trim-newlines/commits)

---
updated-dependencies:
- dependency-name: trim-newlines
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-09 13:03:36 -05:00
dependabot[bot]andUmputun c22d371157 Bump normalize-url from 4.5.0 to 4.5.1 in /frontend
Bumps [normalize-url](https://github.com/sindresorhus/normalize-url) from 4.5.0 to 4.5.1.
- [Release notes](https://github.com/sindresorhus/normalize-url/releases)
- [Commits](https://github.com/sindresorhus/normalize-url/commits)

---
updated-dependencies:
- dependency-name: normalize-url
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-09 11:21:29 -05:00
dependabot[bot]andUmputun 91deae576b Bump ws from 6.2.1 to 6.2.2 in /frontend
Bumps [ws](https://github.com/websockets/ws) from 6.2.1 to 6.2.2.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-04 21:10:23 -05:00
Pavel MineevandUmputun 3068dfe637 post messages from iframe
- fix user info opening
- remove iframe height checks by interval
- update height by mutation observer events
- rename events
2021-06-03 00:56:16 -05:00
Dmitry VerkhoturovandUmputun c0b392ad4c separate user and admin notifications
The current state is a mess of user and admin
notifications, which will become worse after
implementing the new user notification methods
like a telegram.

This change makes things simpler
for the remark42 users.
2021-06-03 00:30:53 -05:00
Pavel MineevandUmputun 97934e23f9 fix tests 2021-05-30 12:50:37 -05:00
Pavel MineevandUmputun 79eab8ccde fix copy message with styles 2021-05-30 12:50:37 -05:00
Dmitry VerkhoturovandUmputun c6e2c38e34 fix email templates path in tests
Before:
failed to make notify service,
failed to create email notification destination:
can't set templates:
can't read message template:
open email_reply.html.tmpl:
no such file or directory

After:
make notify, types=[email]
create notifier service, queue size=100, destinations=1
2021-05-27 12:23:12 -05:00
Vladimir RusinovandUmputun 93a75d698e Add example of Helm-less Kubernetes setup 2021-05-27 11:36:52 -05:00
dependabot[bot]andUmputun 7b580cec15 Bump dns-packet from 1.3.1 to 1.3.4 in /frontend
Bumps [dns-packet](https://github.com/mafintosh/dns-packet) from 1.3.1 to 1.3.4.
- [Release notes](https://github.com/mafintosh/dns-packet/releases)
- [Changelog](https://github.com/mafintosh/dns-packet/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mafintosh/dns-packet/compare/v1.3.1...v1.3.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-27 11:08:24 -05:00
Dmitry VerkhoturovandUmputun 1cff4eb847 clarify deprecation messages
Turned out we don't really want to break
users configurations, so deprecated values
stay with us at least before 2.0 is released.
2021-05-25 14:42:29 -05:00
Dmitry VerkhoturovandUmputun b13fa228b6 start using the provided timeout in the telegram notify 2021-05-25 13:29:19 -05:00
dependabot[bot]andUmputun 8d8c6ee93d Bump browserslist from 4.16.0 to 4.16.6 in /frontend
Bumps [browserslist](https://github.com/browserslist/browserslist) from 4.16.0 to 4.16.6.
- [Release notes](https://github.com/browserslist/browserslist/releases)
- [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/browserslist/browserslist/compare/4.16.0...4.16.6)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-24 22:45:46 -05:00
Dmitry VerkhoturovandUmputun 9fa23cc537 reset image cleanup TTL on Submit
Also:

- make commitTTL equal to EditDuration,
  so that image is committed to permanent
  storage after comment can no longer be edited
- move cleanupTTL to Cleanup function,
  as it's not used elsewhere in the code
- add variables to some tests sleeps, so that
  instead of being magic numbers they would
  rely on timers of structures they suppose
  to wait for
2021-05-24 17:33:59 -05:00
Dmitry VerkhoturovandUmputun 86b2648d66 reset image cleanup timer on comment preview 2021-05-24 17:33:59 -05:00
Dmitry VerkhoturovandUmputun 0e550e83fa add method to renew image cleanup timer 2021-05-24 17:33:59 -05:00
Dmitry VerkhoturovandUmputun 82387729a4 make docker init script chown command verbose 2021-05-24 17:16:53 -05:00
Dmitry VerkhoturovandUmputun 4f2db3cdf6 cover all auth providers with tests, clean up env 2021-05-24 17:15:58 -05:00
Dmitry VerkhoturovandUmputun 6a54ed86c7 move telegram token and timeout to a separate CLI section
This simplifies token and timeout reuse for
the notify module (used now) and for
the auth module later (not yet in the code).
SMTP credentials are already set up that way.
2021-05-24 12:15:03 -05:00
Umputun b856b6d3af add reproxy setup manual 2021-05-20 18:47:45 -05:00
Dmitry VerkhoturovandUmputun fcaf568fcc update wordpress instructions, fix #871 2021-05-20 13:30:09 -05:00
Umputun 3a8750bd58 adopt deployment destination 2021-05-20 10:45:59 -05:00
Yuriy KarpovandUmputun 6f10a5a4a3 fix mobile navigation to comment for hidden comments
issues-82
2021-05-19 03:59:25 -05:00
Dmitry VerkhoturovandUmputun 29ac37c8ea copy init script earlier than we change /srv owner
That would prevent entrypoint script
(https://github.com/umputun/baseimage/blob/master/base.alpine/files/init.sh)
failure on init.sh while running as "app" and not "root"
2021-05-18 13:56:43 -05:00
Pavel MineevandUmputun 63ae392927 fix oauth text color 2021-05-18 13:29:33 -05:00
Dmitry VerkhoturovandUmputun 4d011aa5f3 remove error return from ExtractPictures
That function returns an error in a never
expected condition, and that error would be
logged message on the caller side:
none of the callers handles it.

That change hides that error from the caller
so that function would have a signature that
better fit what it does and how it behaves.
2021-05-17 21:31:56 -05:00
Pavel MineevandUmputun 75196d185d avatar fixes after review 2021-05-17 21:29:46 -05:00
Pavel MineevandUmputun 93b29e934b renew avatar 2021-05-17 15:14:35 -05:00
Dmitry VerkhoturovandUmputun f9eb39db03 make commitTTL equal to EditDuration
So that image is committed to permanent
storage after comment can no longer be edited.

Also, move cleanupTTL to Cleanup function,
as it's not used elsewhere in the code.
2021-05-17 01:55:05 -05:00
Dmitry VerkhoturovandUmputun df547cc815 less magic consonants in tests 2021-05-17 01:55:05 -05:00
Dmitry VerkhoturovandUmputun ef1dd8162b make consistent returns in bolt_store 2021-05-17 01:55:05 -05:00
Dmitry VerkhoturovandUmputun 717c4aa638 rename variables according to golangci-lint rec. 2021-05-17 01:55:05 -05:00
cbdf5f1e47 Fix text
Co-authored-by: Dmitry Verkhoturov <paskal.07@gmail.com>
2021-05-16 16:19:44 -05:00
Pavel MineevandUmputun ba5c74a069 Add a new error message and correct another one 2021-05-16 16:19:44 -05:00
Dmitry VerkhoturovandUmputun fe716b0a71 validate image existence before post or preview 2021-05-16 13:50:09 -05:00
ElephmoonandUmputun 992b843cf5 postMessage without serialization 2021-05-16 13:41:50 -05:00
Dmitry VerkhoturovandUmputun d1ef3ff247 fix HTTP response body close in proxy/image.downloadImage 2021-05-13 18:06:20 -05:00
Dmitry VerkhoturovandUmputun 63220f330b close response body, add lint to check it 2021-05-13 17:58:59 -05:00
Umputun 57fc382956 add ability to select listening address #1000 2021-05-13 14:46:52 -05:00
Dmitry VerkhoturovandUmputun c318e2068e bump baseimage:app to be consistent with :buildgo 2021-05-13 11:22:16 -05:00
Umputun 470956bbc8 no-cache and pull to artifact build make #994 2021-05-10 19:22:56 -05:00
UmputunandGitHub 52fbac904e Admin edit (#997)
* allow admin edits without restrictions

* lint: err reassignment

* lint: suppress false positive

* add admin-edit to readme

* expose admin_edit to config controller
2021-05-10 11:36:02 -05:00
dependabot[bot]andUmputun 6d4be02dab Bump hosted-git-info from 2.8.8 to 2.8.9 in /frontend
Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.8 to 2.8.9.
- [Release notes](https://github.com/npm/hosted-git-info/releases)
- [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md)
- [Commits](https://github.com/npm/hosted-git-info/compare/v2.8.8...v2.8.9)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-10 02:59:57 -05:00
Dmitry VerkhoturovandUmputun 19ba43b843 dockerfile: remove dead code, bump buildgo 2021-05-07 23:27:58 -05:00
Umputun c055749dae revendor to auth:master to provide fix for 404 avatar 2021-05-07 20:00:24 -05:00
Dmitry VerkhoturovandUmputun 4a2ae04571 replace deprecated golangci-lint check, fix goleak reports 2021-05-07 16:09:28 -05:00
Umputun eb7b34b6b2 add related projects section 2021-05-06 19:27:55 -05:00
Umputun 5bdd1a5c72 lint: remove unneeded lambda 2021-05-06 19:14:07 -05:00
UmputunandGitHub 9838aaac70 Disqus fix (#989) 2021-05-06 18:03:15 -05:00
Pavel MineevandUmputun 048712bcee remove csso 2021-05-06 16:10:25 -05:00
Pavel MineevandUmputun cb40bae89b minify css bundle 2021-05-06 16:10:25 -05:00
Pavel MineevandUmputun d8058c6576 use named imports 2021-05-06 15:55:46 -05:00
Pavel MineevandUmputun 123cd53c09 fix linter 2021-05-06 15:39:57 -05:00
Pavel MineevandUmputun 317d932b1f latest versions 2021-05-06 15:39:57 -05:00
Pavel MineevandUmputun 58d8fe0f1c fix linter errors 2021-05-06 15:39:57 -05:00
Pavel MineevandUmputun 3c6df9e17e bump 2021-05-06 15:39:57 -05:00
Pavel MineevandUmputun b3beca1559 make classnames short 2021-05-06 15:16:39 -05:00
Pavel MineevandUmputun 1ee571a7f5 remove @types/classnames 2021-05-06 15:11:36 -05:00
Pavel MineevandUmputun 9f98b46351 replace classnames to faster and smaller package 2021-05-06 15:11:36 -05:00
Pavel MineevandUmputun 928be7d688 add type module while load modern js bundle 2021-05-06 15:08:20 -05:00
Pavel MineevandUmputun 5782495b8e add new error to locales 2021-05-06 15:08:03 -05:00
Pavel MineevandUmputun 3ad7ba2c09 make form available after failed commit sending 2021-05-06 15:08:03 -05:00
Yuriy KarpovandUmputun 4e0a4e52bd add the ability to configure the 'simple_view' parameter from the client,
the parameter is not required, but if set, it will overwrite the one that came from the backend
issues-916
2021-05-06 11:08:15 -05:00
Dmitry VerkhoturovandUmputun e841bc27fb clarify --secret usage based on #861 2021-05-05 02:39:32 -05:00
Umputun d361564815 fix user view in find for deleted comments #972 2021-05-05 01:15:03 -05:00
Umputun d3bd966df8 switch version detection to /script/version.sh in artifact release 2021-05-04 18:28:49 -05:00
Umputun 82f8f6c93e drop branch image builds 2021-05-04 18:05:11 -05:00
Pavel MineevandUmputun f4b6d73a4a use remark_config.host for oauth links 2021-05-04 03:55:52 -05:00
konstantin krivleniaandUmputun 488518cd72 update be translation 2021-05-04 03:55:29 -05:00
Umputun 692a9bdc01 rename release target 2021-05-03 23:36:03 -05:00
Pavel MineevandUmputun d1405ac479 regenerate i18n 2021-05-03 14:00:14 -05:00
Dmitry KandUmputun 01359a4fa9 Add translations: Japanese, Korean, Brazilian Portuguese
Add translations for Japanese, Korean, Brazilian Portuguese

note: This PR requires generating the code to enable them properly as described in [How to add a new translation](https://remark42.com/docs/latest/translation/#how-to-add-a-new-translation)
2021-05-03 14:00:14 -05:00
Dmitry KandUmputun 4bae705f5c Delete ko.json 2021-05-03 13:47:14 -05:00
Dmitry KandUmputun ef089df96c Delete ja.json 2021-05-03 13:47:14 -05:00
Dmitry KandUmputun b066c929fa Delete bp.json 2021-05-03 13:47:14 -05:00
Dmitry KandUmputun 4dd04d95ee Update Japanese, Brazilian Portuguese 2021-05-03 13:47:14 -05:00
Dmitry KandUmputun f4e5ee06ab Update translations
Update translations for 
- Brazilian Portuguese (`bp.json`) // Not used at the moment, would be nice to support it in [loadLocale.ts](https://github.com/umputun/remark42/blob/master/frontend/app/utils/loadLocale.ts)
- German, 
- Spanish, 
- French, 
- Japanese, 
- Korean, 
- Russian
- Turkish
- Chinese (Simplified)
2021-05-03 13:47:14 -05:00
Pavel Mineev b748771fed allow spaces in username with trim 2021-05-02 18:55:07 +03:00
Umputun a4f2542924 allow admin email login with restricted names #964 2021-04-30 12:00:24 -05:00
Pavel MineevandUmputun befb079207 fix default avatar
- fix import of default avatar
- optimaze ghost svg
- add tests
- refactor avatar-icon
2021-04-28 14:13:02 -05:00
Umputun 627ae58ecc Merge remote-tracking branch 'origin/master' 2021-04-27 00:31:18 -05:00
UmputunandGitHub d5246c9689 Deps bump (#956)
* remove redundant sprintf

* bump many deps

* missing vendors
2021-04-27 00:30:38 -05:00
Umputun abd628e1dc remove redundant sprintf 2021-04-26 23:59:39 -05:00
Umputun 52084e0510 update bluemonday and related deps 2021-04-23 03:44:17 -05:00
ronanandUmputun df4b0c80ac Support for Slack notifications
Slack notifications: remove debug log

Slack notifications: code lint

Slack notifications: code lint

Slack notifications: code lint

fix according to comments
2021-04-21 12:56:14 -05:00
Pavel MineevandUmputun c9841b81e4 move back css for last comments 2021-04-18 17:18:34 -05:00
Pavel MineevandUmputun dfed7e16ba Update README.md for v1.7 2021-04-16 17:49:49 -05:00
Pavel MineevandUmputun 6cf2849e70 rewrite tests in recommended way and add more cases coverage 2021-04-09 11:07:12 -05:00
Pavel MineevandUmputun c9ce2bbf8c use named functions instead of arrows 2021-04-09 11:07:12 -05:00
Pavel MineevandUmputun adb443571f Fix form footer 2021-04-09 11:07:12 -05:00
Pavel MineevandUmputun 8cbdf36bdd Fix error message on token tab on login 2021-04-09 11:07:12 -05:00
aepetelinandUmputun 65fca4c7e5 change parseQuery implementation to URLSearchParams. I use .forEach() because not all browsers support .entries() 2021-04-06 02:15:53 -05:00
aepetelinandUmputun 41e07c2238 fix link to doc/latest/translation.md in Frontend guide, was 404 2021-04-05 14:54:44 -05:00
aepetelinandUmputun 2f03f11c7e fix link to Frontend guide in README 2021-04-05 11:04:23 -05:00
UmputunandGitHub 7153af90d0 Create SECURITY.md 2021-04-02 20:10:59 -05:00
Umputun 961b8aacd9 update bluemonday to v1.0.5 to fix https://github.com/microcosm-cc/bluemonday/issues/111 2021-04-02 19:41:16 -05:00
johnwiiandUmputun 8ef87f400a #add auth source title international language support
with zh.json and en.json.
2021-03-30 11:13:50 -05:00
UmputunandGitHub 0b13e8a8d2 update backend deps from go-pkgz (#935) 2021-03-29 16:24:59 -05:00
Dmitry VerkhoturovandUmputun 5fabbe33d9 make flapping notify drops test less sensitive 2021-03-29 12:48:37 -05:00
William Gerald BlondelandUmputun 70833feb50 Revert default locale to en 2021-03-29 12:45:44 -05:00
William Gerald BlondelandUmputun 3a62e9e93b Add French translation (fr-FR) 2021-03-29 12:45:44 -05:00
Dmitry VerkhoturovandUmputun 8a3f434bc1 fix flapping notify drops test, clarify debug logs 2021-03-28 14:06:58 -05:00
Umputun aa14069f8b lint: fix func comment 2021-03-26 16:05:41 -05:00
Umputun ddbceb5375 post-sanitize location's url in case if stored already #927
this is done via service.alterComment the same way as we handle
leftovers from prev issues with results stored already
2021-03-26 16:00:50 -05:00
Umputun e72f594134 rollback 2021-03-26 15:45:49 -05:00
Umputun 47fee90c60 post-sanitize location's url in case if stored already #927 2021-03-26 15:37:53 -05:00
Umputun 5ed4c7100c shorter match condition 2021-03-26 15:06:39 -05:00
Umputun e762ea7b91 sanitize both locator.URL and user.Picture 2021-03-26 15:06:39 -05:00
Umputun 6f35928396 update bluemonday version 2021-03-26 15:06:39 -05:00
Umputun d624526473 adjust email test to be less go version-specific 2021-03-26 12:51:25 -05:00
William Gerald BlondelandUmputun 2238ec88f0 NPM scripts now work on Windows systems 2021-03-26 02:20:54 -05:00
Pavel MineevandUmputun e7950fcb22 Fix saving comment to LS
* save comment after image upload
* do not affect error messages by save to LS mechanism
* check if data in LS in empty
2021-03-22 03:42:03 -05:00
Michael PichtandUmputun 4254169636 corrected path in docker command to import comments from disqus
Signed-off-by: Michael Picht <mipi@fsfe.org>
2021-03-18 00:47:49 -05:00
Pavel MineevandUmputun 682758a58e resize only comments iframe, prevent to resize iframe by comments info 2021-03-14 04:16:33 -05:00
Pavel MineevandUmputun 4325a3c249 Fix dropdown opening
* change iframe height on content change
* change iframe size on dropdown open
2021-03-13 12:41:25 -06:00
Pavel MineevandUmputun 78c582493c don't add margin on user info iframe 2021-03-13 12:40:27 -06:00
Pavel MineevandUmputun ea4636afbb Fix iframe folding 2021-03-13 12:40:27 -06:00
Pavel MineevandUmputun 345ee88333 add testing-library/jest-dom, fix waitfor 2021-03-04 12:02:46 -06:00
Pavel MineevandUmputun 4bea9f1d37 new-auth: increase limit for remark.css 2021-03-04 12:02:46 -06:00
Pavel MineevandUmputun f922a51a28 new-auth: add tests
* use testing-libruaby for new tests
* tests for auth module
* add svg stub
* add global stub for `useIntl`
* add global stub for `common/settings`
2021-03-04 12:02:46 -06:00
Pavel MineevandUmputun 63218ac51c new-auth 2021-03-04 12:02:46 -06:00
UmputunandGitHub 553bcf2fd5 Merge pull request #892 from umputun/fetch-mock
Make fetch mock global
2021-02-24 12:56:55 -06:00
UmputunandGitHub 71be684739 Merge pull request #894 from umputun/bump-deps
bump deps
2021-02-24 12:56:39 -06:00
Pavel MineevandGitHub bd6ecedacb Merge branch 'master' into bump-deps 2021-02-24 18:33:00 +00:00
Pavel MineevandGitHub a2ecfa65d0 Merge branch 'master' into fetch-mock 2021-02-24 18:29:37 +00:00
Pavel MineevandUmputun ec62b1a572 refac textarea autosize 2021-02-24 12:27:32 -06:00
Pavel MineevandUmputun 23b5a0c6bb fix webpack config 2021-02-24 12:26:37 -06:00
Pavel MineevandUmputun 719b07ec6c remove unused hook 2021-02-24 12:26:13 -06:00
Pavel Mineev 0a4c8dbe45 clear localstorage-mock after each test 2021-02-24 14:56:22 +00:00
Pavel Mineev 982226e48b increase size limit after updating deps 2021-02-24 17:44:45 +03:00
Pavel Mineev 313e34a0b5 bump deps
- remove additional prettier presets because they was included inside default preset
2021-02-24 17:42:02 +03:00
Pavel MineevandUmputun 4a43092fb5 ignore autogenerated loadLocale.ts in coverage report 2021-02-22 02:13:03 -06:00
Pavel Mineev b184bb5b7b Make fetch mock global 2021-02-22 00:15:31 +03:00
Umputun d6019e55b0 update limiter deps 2021-02-21 14:04:21 -06:00
VeselovAlexandUmputun 5ec871fd37 Fix NPE when sending message to detached frame 2021-02-21 13:51:02 -06:00
Pavel MineevandUmputun 111b7a86eb change fetcher for user delete comment action 2021-02-21 13:32:57 -06:00
Aleksandr VeselovandGitHub a38fd1fdc3 Encode e-mail subject to support non-ASCII chars (#888)
* Encode e-mail subject to support non-ASCII chars

* Add test for email subject with unicode

* Fix wrong test argument

* Cleanup test function
2021-02-18 16:23:53 -06:00
Umputun 807160dcf6 prep admin to support multiple keys, per aud (siteID) 2021-02-15 13:14:23 -06:00
UmputunandGitHub a50adc823b Merge pull request #881 from Kickoman/be-translation
Add Belarusian locale
2021-02-13 13:13:42 -06:00
UmputunandGitHub c4832ff47a Merge pull request #887 from umputun/remove-content-type-on-upload
Do not send Content-Type on file upload
2021-02-13 13:12:25 -06:00
Pavel Mineev f4f4764644 Do not send Content-Type on file upload 2021-02-13 21:52:48 +03:00
Umputun 0496c6bb0e encode discuss user_id made from the name #879 2021-02-12 14:52:00 -06:00
Enes ÇakırandGitHub fb23cd6cf7 Fix empty username check for Disqus migrator (#879)
by @enescakir

* Fix user ID encoding for empty string
* Revert "Fix user ID encoding for empty string"

This reverts commit 6d901e4b11.
* Fix empty username check for Disqus migrator
* Fix linter emptyStringTest error
2021-02-12 14:47:19 -06:00
Kanstancin Novikaŭ 1f755e07a4 Belarusian locale introduced 2021-02-12 17:49:40 +03:00
Pavel MineevandUmputun 008788ad89 rollback readme till release 2021-02-10 01:59:36 -06:00
Pavel MineevandUmputun 3628467367 fix oauth providers (turn back site id) 2021-02-04 13:09:59 -06:00
Pavel MineevandUmputun 2e43f09636 Revert "send cookie with request"
This reverts commit 656ab84381.
2021-02-04 13:09:59 -06:00
Pavel MineevandUmputun f515676a72 Revert "add fetcher params to test"
This reverts commit a89a06a583.
2021-02-04 13:09:59 -06:00
Pavel MineevandUmputun a89a06a583 add fetcher params to test 2021-02-04 12:13:52 -06:00
Pavel MineevandUmputun 656ab84381 send cookie with request 2021-02-04 12:13:52 -06:00
Pavel MineevandUmputun 807249db5d remove export of fetcher creator and use predefined methods 2021-02-02 15:53:40 -06:00
Pavel MineevandUmputun 4ab2ea9246 fix default export for tests 2021-02-02 15:53:40 -06:00
Pavel MineevandUmputun 6d0b18f688 move creation of all fetchers to fetcher.ts 2021-02-02 15:53:40 -06:00
Pavel MineevandUmputun 068eaea86a fix types for fetcher methods 2021-02-02 15:53:40 -06:00
Pavel MineevandUmputun a1cf289845 Use straight declaration of available methods 2021-02-02 15:53:40 -06:00
Pavel MineevandUmputun 30394b0760 fix link to oauth window 2021-02-02 15:53:40 -06:00
Pavel MineevandUmputun 4d075f4f7a fix last comments request 2021-02-02 15:53:40 -06:00
Pavel MineevandUmputun 8572f809cc rid of legacy bundles from size-limit 2021-02-02 15:53:40 -06:00
Pavel MineevandUmputun f31146a266 add fetcher creator, create fetcher for every case, remove api.test.ts because of test become usless in terms of using URLSearchParams 2021-02-02 15:53:40 -06:00
Pavel MineevandUmputun 49b70b7b04 Add comments to fetcher 2021-02-02 15:53:40 -06:00
Pavel MineevandUmputun 8161182e7d Pass query throw args 2021-02-02 15:53:40 -06:00
Pavel MineevandUmputun 2fae0c29d2 Update fetcher and add good coverage to it 2021-02-02 15:53:40 -06:00
Pavel MineevandUmputun 77825ce947 Update frontend CI 2021-02-01 12:28:36 -06:00
Pavel MineevandUmputun e1e8da4b2a use parens for arrow funcs always 2021-01-29 03:17:10 -06:00
UmputunandGitHub be2f6d0a20 Fix admin names (#858)
* fix admin name check for anon login #605

* update readme with admin names info

* lint: list of static site params

* typo

* don't allow email users to reuse admin names

* move admin.names to restricted-names

* forgotten names member

* remove names from example admin

* remove names from prepTestStore
2021-01-20 13:12:09 -06:00
Umputun d40e354265 add more tests for voted_ip 2021-01-18 15:44:32 -06:00
Umputun 1048355b7b strip voted_ip from response 2021-01-18 15:44:32 -06:00
Umputun 5296074dae fix sanitize policy to allow loading-lazy for images #855 2021-01-18 15:36:15 -06:00
Umputun 89a4677391 enforce loading=lazy att to rendered images html 2021-01-17 16:32:26 -06:00
Umputun 849c133082 reformat long error calls to multi-line 2021-01-17 14:21:58 -06:00
Pavel MineevandUmputun ad25f83954 Add size-limit to CI 2021-01-15 19:53:51 -06:00
Umputun 6d525e4820 remove no-cache from /api/v1/avatar api 2021-01-15 16:22:45 -06:00
Umputun 12b0fbe75d remove no-cache from /avatar api 2021-01-15 16:18:10 -06:00
Umputun 0dd3cd93ee add no-cache to Cache-Control on static files 2021-01-15 14:55:32 -06:00
Pavel MineevandUmputun 758c6bb1b2 fix font in user comments 2021-01-15 12:39:20 -06:00
Umputun 477ee60068 switch to stable auth version 2021-01-15 12:38:05 -06:00
Yury AntonovandUmputun 8b53c68cc3 add missing Fragment import 2021-01-15 12:12:19 -06:00
Umputun fa22e32757 revendor with update auth #784 2021-01-15 11:37:07 -06:00
UmputunandGitHub aff79a6fa9 Same site (#850)
* switch to auth master for same-site

* add same-site policy support #784

* add parse same site param
2021-01-14 13:06:29 -06:00
Pavel MineevandUmputun 7f7973a78f Propper settings for collect coverage 2021-01-14 12:11:22 -06:00
NghiamasterandUmputun fa2433ee0d Update Vietnamese translation 2021-01-14 11:24:39 -06:00
NghiamasterandUmputun 0808599442 Update Vietnamese translation 2021-01-14 11:24:39 -06:00
NghiamasterandUmputun 3961226e3c Add Vietnamese locale 2021-01-14 11:24:39 -06:00
NghiamasterandUmputun 277ebf17f3 Add Vietnamese Language 2021-01-14 11:24:39 -06:00
Pavel MineevandUmputun 3e0f233c5a add mjs for sed 2021-01-13 23:16:29 -06:00
Pavel MineevandUmputun eb6ef87d42 set fallback for remark_config.host 2021-01-12 16:21:31 -06:00
Pavel MineevandUmputun df92500bb4 add size-limit to css 2021-01-12 13:39:26 -06:00
Pavel MineevandUmputun e429936831 replace file-loader with url-loader for inline svg to css 2021-01-12 13:39:26 -06:00
Pavel MineevandUmputun 43935bbd38 fix translation tasks for compatibility with latest version on formatjs 2021-01-12 13:39:26 -06:00
Pavel MineevandUmputun f332ab194d turn back translaton scripts 2021-01-12 13:39:26 -06:00
Pavel MineevandUmputun 42cc226b16 turn back dotenv 2021-01-12 13:39:26 -06:00
Pavel MineevandUmputun bb5907d21f Replacing REMARK_URL, fix remark_config stub, add minify html/css in templates 2021-01-12 13:39:26 -06:00
Pavel MineevandUmputun e77ffef4f4 fix comments locator in example 2021-01-12 13:39:26 -06:00
Pavel MineevandUmputun ecc480f51c rewrite script import in iframe.ejs 2021-01-12 13:39:26 -06:00
Pavel MineevandUmputun 27e0fa0701 change styles for list-comments 2021-01-12 13:39:26 -06:00
Pavel MineevandUmputun 1052898589 Change fragments to empty tag 2021-01-12 13:39:26 -06:00
Pavel MineevandUmputun b72f4d57ea Fix paths to static 2021-01-12 13:39:26 -06:00
Pavel MineevandUmputun 6474821271 Fix styles order 2021-01-12 13:39:26 -06:00
Pavel MineevandUmputun b53d4a455b Fix escaping for shortcuts in messages 2021-01-12 13:39:26 -06:00
Pavel MineevandUmputun 3bd1bb1c89 fix build 2021-01-12 13:39:26 -06:00
Pavel MineevandUmputun a144dd00e3 Remove any from codebase 2021-01-12 13:39:26 -06:00
Pavel MineevandUmputun 9518d1d83d Update copy plugin 2021-01-12 13:39:26 -06:00
Pavel MineevandUmputun 67188880a7 Changes
* turn back remark.image
* fix readme
* remove ts-ignore
2021-01-12 13:39:26 -06:00
Pavel MineevandUmputun fb60840f9f fix linter 2021-01-12 13:39:26 -06:00
Pavel MineevandUmputun 5825a55b69 Update frontend
* change root dit and change way to import modules
* update deps to latest versions
* use latest tools for building bundles
* rewrite webpack config
* use nomodule technique for loading modern bundle
* inject polyfills by babel usebuiltins
* update eslint rules
* rename all style files to CSS
* use postcss preset env for building styles
* proper typescript typing
* put all html files to templates folder
* etc
2021-01-12 13:39:26 -06:00
Janik VonrotzandUmputun 919fb7af62 Update README.md 2021-01-05 11:11:29 -06:00
Umputun ed3cd197dd bump deps 2021-01-03 03:55:00 -06:00
Umputun a0b2bbf1ed typo 2021-01-03 03:14:19 -06:00
Umputun 2a02a946e7 lint: make scopelint happy on test 2021-01-03 03:13:20 -06:00
Umputun 1e96f9a5b0 Merge remote-tracking branch 'origin/master' 2021-01-03 03:09:31 -06:00
Umputun 0cb0232fa1 escape [ ] ( ) from tg title #839 2021-01-03 03:09:21 -06:00
Umputun eb21a05b7b add optional frame-ancestors (Content-Security-Policy) for the list of hosts passed by user 2021-01-03 02:33:51 -06:00
Umputun b15f07b5f7 update auth module 2021-01-03 02:22:23 -06:00
a.kalinandUmputun 81acaef8a4 #819 Changed jwt storage to local variable 2020-12-30 13:16:23 -06:00
a.kalinandUmputun c538068d8b #819 Added jwt token to requests 2020-12-30 13:16:23 -06:00
UmputunandGitHub 9b0fee0737 Merge pull request #832 from umputun/dependabot/npm_and_yarn/frontend/ini-1.3.7
Bump ini from 1.3.5 to 1.3.7 in /frontend
2020-12-21 16:25:57 -06:00
UmputunandGitHub d27a22a6d6 Merge pull request #835 from umputun/dependabot/npm_and_yarn/frontend/node-notifier-8.0.1
Bump node-notifier from 8.0.0 to 8.0.1 in /frontend
2020-12-21 16:25:21 -06:00
dependabot[bot]andGitHub 84d06ca392 Bump node-notifier from 8.0.0 to 8.0.1 in /frontend
Bumps [node-notifier](https://github.com/mikaelbr/node-notifier) from 8.0.0 to 8.0.1.
- [Release notes](https://github.com/mikaelbr/node-notifier/releases)
- [Changelog](https://github.com/mikaelbr/node-notifier/blob/v8.0.1/CHANGELOG.md)
- [Commits](https://github.com/mikaelbr/node-notifier/compare/v8.0.0...v8.0.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-12-21 22:22:19 +00:00
Umputun 104ee308c6 make init to work in alpine, fix typo 2020-12-17 21:25:51 -06:00
dependabot[bot]andGitHub 8306b528e0 Bump ini from 1.3.5 to 1.3.7 in /frontend
Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.7.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.7)

Signed-off-by: dependabot[bot] <support@github.com>
2020-12-11 05:01:38 +00:00
UmputunandGitHub 2a9b59ebf7 Merge pull request #827 from umputun/paskal/fix_doc
readme: fix SPA link, web config syntax
2020-12-06 14:40:54 -06:00
UmputunandGitHub 097788a772 Merge pull request #828 from umputun/paskal/logs
documentation: fix email templating doc, add remap doc
2020-12-06 14:39:49 -06:00
Dmitry Verkhoturov d0d09604e1 add remap doc by @mind-overflow #810 2020-12-06 20:08:26 +01:00
Dmitry Verkhoturov 3111c0e722 fix formatting for email templating doc 2020-12-06 20:02:42 +01:00
Dmitry Verkhoturov b426055ad0 readme: fix SPA link, web config syntax 2020-12-06 18:50:53 +01:00
UmputunandGitHub 4f4c302a9d Merge pull request #826 from umputun/paskal/clean_stream
Clean unused stream code
2020-11-30 10:46:33 -06:00
Dmitry Verkhoturov 809a7c7dbd Clean unused stream code
It was supposed to solve #253 but frontend part for it in
#357 was never finished, and backend code produces false
positive test failures since day 0. The cost of just having
this code around is too high, we'll re-add it in case
frontend implementation will be finished.
2020-11-30 02:00:59 +01:00
Dmitry VerkhoturovandUmputun 6426ea154e #819 add SendJWTHeader auth option, expose it on /config 2020-11-29 18:18:22 -06:00
KsiniaandUmputun 5b4e492c1b Save logged in user email in local storage 2020-11-18 10:59:44 -06:00
UmputunandGitHub e935fedd60 Merge pull request #805 from markiewiczjulian/master
added translations to pl language
2020-11-17 11:25:40 -06:00
Julian MarkiewiczandGitHub 2df51fe111 Merge branch 'master' into master 2020-11-17 09:02:32 +01:00
sattelliteandUmputun 60f554f2fa Simplified definition of REMARK_URL to templates in webpack config 2020-11-10 11:04:07 -06:00
sattelliteandUmputun abe3b0d47a Simplified js expression in templates
REMARK_URL is required and window.location.origin is not needed.

https://github.com/umputun/remark42/pull/806#discussion_r519458452
2020-11-10 11:04:07 -06:00
sattelliteandUmputun 06c3406e30 Replace predefined site_id by SITE_ID 2020-11-10 11:04:07 -06:00
sattelliteandUmputun 0d42761a12 Replace BASE_URL constant by REMARK_URL in html 2020-11-10 11:04:07 -06:00
sattelliteandUmputun 2a48cc9682 use REMARK_URL in frontend templates 2020-11-10 11:04:07 -06:00
aa048a0db9 Apply suggestions from code review
Co-authored-by: Pavel Mineev <pavel@mineev.me>
2020-11-10 09:45:48 -06:00
KsiniaandUmputun d87135a80a Fix test for Replies link 2020-11-10 09:45:48 -06:00
KsiniaandUmputun e6aa4736ea Fix RSS links 2020-11-10 09:45:48 -06:00
Pavel MineevandUmputun f4aa7332d5 Add tests and change props/state getting to fix TS error 2020-11-09 15:43:03 -06:00
Pavel MineevandUmputun 4da83d9231 #212: Fix max length counter in Safari 2020-11-09 15:43:03 -06:00
ivanezkoandUmputun 18ba05cea0 add ukranian locale 2020-11-06 02:15:04 -06:00
ivanezkoandUmputun a8752bb41c add ukranian locale 2020-11-06 02:15:04 -06:00
ivanezkoandUmputun 3396c56f2e add ukranian locale 2020-11-06 02:15:04 -06:00
ivanezkoandUmputun f281faa0da add ukranian locale 2020-11-06 02:15:04 -06:00
IvanezkoandUmputun b88f99e099 add ukranian locale 2020-11-06 02:15:04 -06:00
Artur MullakhmetovandUmputun b04eba2d91 do not decrease comments count in case of repeated deletion 2020-11-05 11:00:30 -06:00
markiewicz.julian@gmail.com 48ed23e70d added translations to pl language 2020-10-28 08:50:51 +01:00
Vitaly ZdanevichandUmputun 55a8eb10e7 опечатка 2020-10-23 21:21:08 -05:00
Dmitry VerkhoturovandUmputun b447863542 add deduplication of emails in recursive notifications 2020-10-20 11:08:04 -05:00
Dmitry VerkhoturovandUmputun 4ba32141fa add recursive email notifications up to highest parent 2020-10-20 11:08:04 -05:00
Dmitry VerkhoturovandUmputun 1ce3cf3d1f simplify admin email notifications 2020-10-20 11:02:04 -05:00
Dmitry VerkhoturovandUmputun 91580194e9 clean up formatting and typos in documentation 2020-10-19 02:42:19 -05:00
Dmitry VerkhoturovandUmputun 6f122f93d1 extract Email.buildAndSendMessage from Email.Send 2020-10-18 17:29:44 -05:00
Dmitry VerkhoturovandUmputun ced40b525a move email notification logic into Email struct 2020-10-18 17:29:44 -05:00
Dmitry VerkhoturovandUmputun bcc2fc7a9e use correct case for GitHub mentions 2020-10-14 12:05:15 -05:00
Dmitry VerkhoturovandUmputun c9b395f60d separate notify.SubmitVerification from notify.Submit 2020-10-11 23:13:24 -05:00
Dmitry VerkhoturovandUmputun 1980f30666 rewrite notify.Service.do from range to select 2020-10-11 23:13:24 -05:00
Umputun b4f4c5e777 auto deploy docs 2020-10-11 23:06:34 -05:00
Pavel MineevandUmputun 3e37434483 Fix images paths in docs 2020-10-11 20:03:14 -05:00
Pavel MineevandUmputun 7b1ecccb4b fix files naming add files to menu 2020-10-11 17:12:14 -05:00
Pavel MineevandUmputun fc9882098d arename readme to index 2020-10-11 17:12:14 -05:00
Pavel MineevandUmputun e52499316e split docs by versions 2020-10-11 17:12:14 -05:00
Dmitry VerkhoturovandUmputun bec6bcee0d use template files instead of statik in dev Dockerfile 2020-10-07 19:32:39 -05:00
Dmitry VerkhoturovandUmputun 36dce6c2ac fix golangci-lint timeout, bump email min TLS version 2020-10-07 19:32:39 -05:00
Dmitry VerkhoturovandUmputun 593ca0387f adjust dockerfile to support go test with -race, bump images 2020-10-07 19:32:39 -05:00
konstantin krivleniaandUmputun a1fcef670e update dependencies 2020-10-04 23:46:39 -05:00
dependabot[bot]andUmputun ecb146bf20 Bump node-fetch from 2.6.0 to 2.6.1 in /frontend
Bumps [node-fetch](https://github.com/bitinn/node-fetch) from 2.6.0 to 2.6.1.
- [Release notes](https://github.com/bitinn/node-fetch/releases)
- [Changelog](https://github.com/node-fetch/node-fetch/blob/master/docs/CHANGELOG.md)
- [Commits](https://github.com/bitinn/node-fetch/compare/v2.6.0...v2.6.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-10-04 16:25:08 -05:00
Pavel PustovalovandUmputun c02590665f Setup coverage for FE 2020-09-06 14:15:56 -05:00
Pavel PustovalovandUmputun 1cb724a264 Set node version for each job 2020-09-05 12:22:02 -05:00
Pavel PustovalovandUmputun 82de2a1d8f Regenerate package-lock.json 2020-09-05 12:22:02 -05:00
Dmitry VerkhoturovandUmputun b974fd9cc4 add ability to set multiple admin emails for notifications 2020-09-02 15:09:46 -05:00
Umputun 4f135fe91b switch ci and docker build to node 12 2020-08-30 21:58:48 -05:00
Pavel PustovalovandUmputun f72603d967 make FE CI more parallel 2020-08-30 13:58:22 -05:00
Pavel PustovalovandUmputun 0ba8faefc9 change target browsers to defaults 2020-08-30 13:53:25 -05:00
Pavel PustovalovandUmputun 63b879ac45 npm dedupe 2020-08-30 13:53:25 -05:00
Pavel PustovalovandUmputun 07efd657af Update babel & jest 2020-08-30 13:53:25 -05:00
Pavel PustovalovandUmputun dfd66b95b0 Unify target browsers 2020-08-30 13:53:25 -05:00
vdimirandUmputun 295191955a Add function createAppFromCmd to server_test.go 2020-08-26 12:39:31 -05:00
vdimirandUmputun 14bcbfa901 Add function randomPath to rest_test.go 2020-08-26 12:39:31 -05:00
Pavel MineevandUmputun 5c23206d00 Hide email subscription by param from frontend 2020-08-20 18:15:50 -05:00
Pavel MineevandUmputun c9800a3799 Update vurnulable deps
* automatic update deps
* update copy-webpack-plugin to next major version
* update webpack config connected with new copy-webpack-plugin API
2020-08-20 03:58:04 -05:00
Pavel PustovalovandUmputun a59be983f2 Update lint-staged & husky 2020-08-20 02:53:33 -05:00
Pavel PustovalovandUmputun 71e0e98e05 Update prettier 2020-08-20 02:53:33 -05:00
Pavel PustovalovandUmputun 11f222c6e3 Run ui specs on CI 2020-08-20 02:52:13 -05:00
Umputun 1eedcc0eb3 don't allow anon with space prefix or suffix 2020-08-16 21:12:42 -05:00
Pacharapol WithayasakpuntandGitHub 5d6729ddd5 Fix to work without errors in Nuxt Universal (#723)
by @patarapolw 

* allow manual init and destroy for use with Nuxt

* fix onDestroy-related methods

* avoid global scope, and use function scope instead

* add createInstance function to window.REMARK42

* 1. allow DOMNode to be put in remark_config 2. check remark_config before try to attach node

* move createInstance function outside

* update embed.ts

* avoid ?.

* 📚 Docs: doc on how to make it work with SPAs

* 📚 Docs: fix spa.md to be more flexible

*  Feat: add REMARK42::ready event

* remove nuxt-specific terminologies

* tell MutationObserver to disconnect on destroy

* update docs/spa.md
2020-07-31 12:21:00 -05:00
Pacharapol WithayasakpuntandUmputun d1268bbe8c update README according to #737 2020-07-30 12:24:48 -05:00
dependabot[bot]andUmputun 424016e994 Bump elliptic from 6.5.2 to 6.5.3 in /frontend
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.2 to 6.5.3.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.2...v6.5.3)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-30 12:21:41 -05:00
Umputun 729b6d2d08 add --proxy-cors $PROXY_CORS disabling internal cors middleware #749 2020-07-27 16:58:47 -05:00
dependabot[bot]andUmputun 0f9c41cfc6 Bump lodash from 4.17.15 to 4.17.19 in /frontend
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-22 11:58:04 -05:00
0xflotusandUmputun 8b77ff953e docs: fixed small error 2020-07-22 11:57:44 -05:00
0xflotusandUmputun 69a81042db docs: enabled nginx syntax highlighting 2020-07-22 11:56:49 -05:00
vdimirandUmputun 09f06f587c Add tests for comment.Imported, refine image.SubmitAndCommit tests 2020-07-09 12:43:07 -05:00
vdimirandUmputun d7c9aecf03 Submit images from service.Create only after creation in Engine 2020-07-09 12:43:07 -05:00
vdimirandUmputun 9957ab642b Add Imported field to comment, do not wait image submission for such comments 2020-07-09 12:43:07 -05:00
vdimirandUmputun 1f9566bb47 Use comment timestamp for submitting image ttl 2020-07-09 12:43:07 -05:00
Dmitry VerkhoturovandUmputun 9510962e25 add redis PubSub web cache as an option 2020-06-25 23:14:50 -05:00
Dmitry VerkhoturovandUmputun ebf379f4d6 bump go-pkgz/lcw to v0.7.1 2020-06-25 17:28:33 -05:00
Umputun df9c05b490 add microsoft auth 2020-06-18 18:25:35 -05:00
Dmitry VerkhoturovandUmputun 42d0c67cb3 add context to server errors, properly close datastore in case of errors 2020-06-13 19:07:33 -05:00
Umputun 46f3ced83d disable some stream tests 2020-06-08 16:10:31 -05:00
Umputun 4d0f909f0e add reset of the same ip votes on -+ or +- changes and move same ip set and reject check up (above this reset) 2020-06-08 14:59:40 -05:00
dependabot[bot]andUmputun 1e60cb813a Bump websocket-extensions from 0.1.3 to 0.1.4 in /frontend
Bumps [websocket-extensions](https://github.com/faye/websocket-extensions-node) from 0.1.3 to 0.1.4.
- [Release notes](https://github.com/faye/websocket-extensions-node/releases)
- [Changelog](https://github.com/faye/websocket-extensions-node/blob/master/CHANGELOG.md)
- [Commits](https://github.com/faye/websocket-extensions-node/compare/0.1.3...0.1.4)

Signed-off-by: dependabot[bot] <support@github.com>
2020-06-07 15:46:51 -05:00
Umputun dc7bb5810f typo in from_address param 2020-06-05 03:49:49 -05:00
Dmitry Verkhoturov 80f4862c9c fix leaking goroutines and add tests for them 2020-06-01 09:58:48 +02:00
Umputun 55dd5bd25b skip LastCommentStream test 2020-05-26 15:29:56 -05:00
Dmitry VerkhoturovandUmputun 890d7154e7 bump go modules 2020-05-24 19:00:07 -05:00
Dmitry VerkhoturovandUmputun 0d67f7e53d bump golangci-lint version, fix discovered issues 2020-05-24 19:00:07 -05:00
foxbgandGitHub 13220c044c Adding BG translation (#719)
* adding BG translation part 1

* agging BG locale part 2

* adding BG translation part 1

* agging BG locale part 2

* adding BG translation part 1

* agging BG locale part 2

* adding BG translation part 1

* agging BG locale part 2

* fixing typo

* remove duplicate line
* more translation and fixed typos

@foxbg
2020-05-24 14:08:48 -05:00
UmputunandGitHub 5c36aca9ec Merge pull request #716 from Mavrin/fix_name_regexp
allow non latin letter for user name
2020-05-23 11:58:39 -05:00
Umputun 2f20b64c0f switch lcw to stable version 2020-05-22 14:18:34 -05:00
Umputun 87b44fa74c switch to tagged base images 2020-05-22 14:13:05 -05:00
konstantin krivlenia 07a9f84c49 allow non latin letter for user name 2020-05-22 00:33:54 +03:00
Umputun 80854f9d40 allow anon names to inlude non-latin. Add limit (64) to max len #715 2020-05-21 13:18:10 -05:00
konstantin krivleniaandUmputun 4682694092 update frontend dev dependencies 2020-05-19 15:11:35 -05:00
Gürkan GürandUmputun 0bb2877b07 frontend: Add Turkish language support 2020-05-17 13:34:40 -05:00
Gürkan GürandUmputun d3b58c0795 docs: Extend translation howto 2020-05-17 13:34:40 -05:00
Umputun d8ce444df0 move logging for protected routes to INFO level 2020-05-12 23:15:06 -05:00
Umputun 7edf8e0b23 pull latest docker image on dev build 2020-05-12 22:27:08 -05:00
Umputun c10354584d dbl request limit size for count query
for sites with a lot of posts on the page, the list of all urls can exceed the default 64k limit. I've see the request like this from news.radio-t.com
2020-05-12 22:26:45 -05:00
Umputun ef8407b23e typo in app image 2020-05-12 12:17:41 -05:00
Umputun 11fde80d27 replace references to legacy remark url in docs 2020-05-12 12:08:18 -05:00
Umputun 6bee151b25 switch base image 2020-05-12 12:02:13 -05:00
Umputun 0895de2f7e rename all packages from the legacy remark/ to remark42 2020-05-12 11:34:16 -05:00
Pavel MineevandUmputun b90d076fdf Move html to separete files 2020-05-10 23:51:13 -05:00
Dmitry Verkhoturov f5fcd5b254 switch from go-cache to lcw 2020-05-10 20:41:26 +02:00
Umputun a8f3e1730b update to lcw@master 2020-05-09 21:20:03 -05:00
Dmitry VerkhoturovandUmputun bbfeaeb3d0 go-consistent proposed changes 2020-05-04 02:22:16 -05:00
Serge AdamovichandUmputun 5afa74eec1 #594 Open hidden thread and jump to linked comment 2020-05-04 02:20:44 -05:00
Serge AdamovichandUmputun d111cbd3e1 #594 Scroll to parent of hidden comments 2020-05-04 02:20:44 -05:00
Dmitry VerkhoturovandUmputun e922fc31a2 enable gocritic linter and fix found issues 2020-04-26 17:05:34 -05:00
Dmitry VerkhoturovandUmputun 71dcb13a71 commit proxy images same way as normally uploaded ones 2020-04-25 12:15:26 -05:00
Dmitry VerkhoturovandUmputun 5579bd1a88 close response.Body in image proxy tests 2020-04-24 14:16:01 -05:00
Dmitry VerkhoturovandUmputun b71e0be095 move cachedImgID and sha1Str from rest.proxy to store.image 2020-04-24 14:16:01 -05:00
UmputunandGitHub 4b26b8e259 Merge pull request #699 from umputun/github_golangci_lint
change golangci-lint format to github-actions for CI
2020-04-23 18:07:03 -05:00
Dmitry Verkhoturov 1a2cceba4a change golangci-lint format to github-actions for CI 2020-04-24 01:02:03 +02:00
Umputun 42509cf0dd update linter to 1.25 2020-04-23 01:13:35 -05:00
UmputunandGitHub d3c77d0c4c Merge pull request #693 from umputun/img_commit_on_start
Image commit on start
2020-04-23 00:52:27 -05:00
Dmitry Verkhoturov 8cb274934e add DataService.ResubmitStagingImages function call on server start 2020-04-23 00:55:13 +02:00
Dmitry Verkhoturov 5ae6dcf652 introduce image.Service commitTTL and cleanupTTL properties
This allows having separate values of TTL for Commit and Cleanup
and moving them apart in time, also clarifying their connection
to EditTime which was previously outside of the package level.
2020-04-23 00:54:30 +02:00
Dmitry Verkhoturov c7f73a6c84 add image.Store.Info() function 2020-04-23 00:52:36 +02:00
UmputunandGitHub cebc40f9ce Merge pull request #697 from umputun/tests_golangci_lint
Enable golangci-lint for tests
2020-04-21 11:58:35 -05:00
Dmitry Verkhoturov 3a0529e51b enable golangci-lint for tests, fix discovered problems 2020-04-21 12:01:10 +02:00
Dmitry Verkhoturov 318feb8ed2 golangci-lint fixes for memory_store tests 2020-04-21 11:47:15 +02:00
Umputun b4a6cc5473 format comment for UserDetail 2020-04-21 02:40:24 -05:00
UmputunandGitHub cb54d0ee76 Merge pull request #691 from dellink/add-error-message-for-anonymous-in-comment-form
Add message of image upload try by anonymous user
2020-04-20 10:38:26 -05:00
Serge Adamovich 84e68b9bc8 Check messages in tests 2020-04-20 11:22:14 +03:00
Serge Adamovich 645b2390c2 Generate locales 2020-04-20 10:35:26 +03:00
Dmitry VerkhoturovandUmputun 513c8f353d sticter golangci config, fix discovered issues 2020-04-19 16:21:09 -05:00
Dmitry VerkhoturovandUmputun 2acb00d424 rename image.Store.SaveWithID to Save 2020-04-19 16:21:09 -05:00
Dmitry VerkhoturovandUmputun 36cee9cc66 simplify image.Store interface 2020-04-19 16:21:09 -05:00
Serge Adamovich 7115329060 Add tests 2020-04-19 20:41:46 +03:00
Serge Adamovich 2f966d09ca Fix work with message in onDrop method 2020-04-19 20:12:44 +03:00
Serge Adamovich 5fc67f1c12 Add message of image upload try by anonymous user 2020-04-18 17:24:54 +03:00
Pavel MineevandUmputun b5e31f3081 Remove unused sed from artifacts build 2020-04-17 13:48:54 -05:00
Dmitry VerkhoturovandUmputun 381ca75126 #614 serve proxified images disregarding if saving is enabled 2020-04-15 20:05:13 -05:00
Dmitry VerkhoturovandUmputun 4d988c427a #641 bump golangci-lint version to 1.23.8 2020-04-15 20:02:55 -05:00
Dmitry VerkhoturovandUmputun 7cebb9e4ee #641 bump CI go version to 1.14 2020-04-15 20:02:55 -05:00
Dmitry VerkhoturovandUmputun c3896934c9 don't use cache in rest tests which don't utilize it 2020-04-14 13:47:27 -05:00
UmputunandGitHub 1c28f44f08 Merge pull request #671 from akellbl4/update/dev-deps
Update dev dependencies
2020-04-14 10:15:46 -05:00
Pavel Mineev 78125ed975 revert formatjs/cli to old version 2020-04-14 11:42:31 +03:00
Pavel Mineev e5df2a0f52 up prettier 2020-04-14 11:42:31 +03:00
Pavel Mineev e552c78491 Update dev dependencies 2020-04-14 11:42:29 +03:00
Umputun 3cb07b9cd6 fix incorrect sinceTime for comments stream 2020-04-14 03:40:10 -05:00
UmputunandGitHub e94af6a059 Merge pull request #685 from umputun/fix_memory_store_tests_panic
remove t.Log from test server.Run in memory_store
2020-04-13 17:34:39 -05:00
Dmitry Verkhoturov 828669ac95 remove t.Log from test server.Run in memory_store 2020-04-14 00:20:42 +02:00
UmputunandGitHub fab32ace10 Merge pull request #679 from akellbl4/frontend-scripts
Frontend scripts and docs updates
2020-04-13 16:49:02 -05:00
Dmitry VerkhoturovandUmputun 4605d567ae make image.Close wait for all images Submit completion 2020-04-13 16:01:51 -05:00
Dmitry VerkhoturovandUmputun 7151698b18 add -race key to backend tests in CI pipeline and Dockerfile 2020-04-13 16:01:51 -05:00
Dmitry VerkhoturovandUmputun afc94adc1b remove log package usage from tests, sort imports 2020-04-13 16:01:51 -05:00
Umputun 360f47e16b go mod for memory_store example 2020-04-13 14:31:58 -05:00
Umputun ee559e7deb remake modules.txt with go 1.14 2020-04-13 14:30:48 -05:00
Pavel Mineev 5e71613d4f add instant commands 2020-04-13 22:03:37 +03:00
荒野無燈andUmputun 9459a2a2f5 notify/email.go: fixup smtp.NewClient second param 2020-04-13 14:02:00 -05:00
荒野無燈andUmputun 200f464925 smtp buildMessage(): flush quotedprintable writer buff explicitly 2020-04-13 14:01:21 -05:00
Pavel Mineev 51fc0f7a92 put all config in separate js files 2020-04-13 21:58:01 +03:00
Umputun d7442ef1a9 switch flags, revendor 2020-04-13 12:30:23 -05:00
Dmitry VerkhoturovandUmputun d6c7e151a0 bump modules 2020-04-13 12:03:26 -05:00
UmputunandGitHub b39874fe08 Merge pull request #684 from quantonganh/i444-arm-32
the correct GOARCH value is arm, not arm32
2020-04-13 11:57:56 -05:00
Quan Anh Tong 7ebea0ca9d Revert "fix artifact filename on arm32"
This reverts commit 2e47fde46e.
2020-04-13 20:20:02 +07:00
Quan Anh Tong cdda2d0319 the correct GOARCH value is arm, not arm32 2020-04-13 20:12:47 +07:00
Pavel Mineev 8d02ad432c add cross-env for npm run check 2020-04-13 16:10:35 +03:00
Pavel Mineev 7d4b6d22f0 show only warn and error messages on install without other visual noise 2020-04-13 16:08:04 +03:00
Pavel Mineev af38a64f5c Frontent infrastructure updates
* rename and combine npm scripts
* move to checkout@v2 in actions
* change dev docs
* move liststaged config to package.json because it easier to understand what husky doing
2020-04-13 14:36:19 +03:00
UmputunandGitHub a15acaf293 Merge pull request #632 from Smolevich/small-improvements
Small improvements
2020-04-13 02:38:20 -05:00
Dmitry VerkhoturovandUmputun dba77d403d bump go-pkgz modules, bbolt new url 2020-04-12 16:09:01 -05:00
Pavel MineevandUmputun 40eb39f8f0 increace deleteme size after update preact 2020-04-12 15:31:17 -05:00
Pavel MineevandUmputun c291dd491d update preact 2020-04-12 15:31:17 -05:00
Pavel MineevandUmputun 78f1f39f33 increase deleteme size 2020-04-12 14:07:16 -05:00
Pavel MineevandUmputun d5ef0db394 Update production deps 2020-04-12 14:07:16 -05:00
lalentineandUmputun 82cbb5a836 Adds background transition to .comment class 2020-04-12 13:17:58 -05:00
荒野無燈andUmputun 4b3e59f8d0 locale: zh.json: fixup typo for subscribeByEmail.submit 2020-04-12 13:16:44 -05:00
Dmitry VerkhoturovandUmputun fc63493e7e return proper content type instead of image/* 2020-04-12 03:57:08 -05:00
Umputun 7614edf167 extract wait in image close 2020-04-12 03:45:03 -05:00
Dmitry VerkhoturovandUmputun a133ab31fa add context cancellation to image.Service.Cancel() 2020-04-12 03:34:13 -05:00
Dmitry VerkhoturovandUmputun 449e375cbb fix image.Service termination in case Submit was never called 2020-04-12 03:34:13 -05:00
Dmitry VerkhoturovandUmputun 97d7467033 fix Docker capitalization in Readme, fix backend run instructions 2020-04-12 03:34:13 -05:00
Pavel MineevandUmputun 053bea18cd Cut react devtools forwarding by webpack in production build 2020-04-12 02:31:34 -05:00
lalentineandUmputun 49dd5bdf21 Resolves #559
Adds .comment_highlighting class after scrolling to comment.
2020-04-11 23:03:47 -05:00
Quan Anh TongandUmputun 2e47fde46e fix artifact filename on arm32 2020-04-11 22:51:01 -05:00
Dmitry VerkhoturovandUmputun 1e67850e87 fix docker run error without attached /srv/var 2020-04-11 20:08:35 -05:00
Dmitry VerkhoturovandUmputun f350e26740 #655 add documentation for using SendGrid and for Mailgun API 2020-04-11 20:07:41 -05:00
UmputunandGitHub c76117d5c6 Merge pull request #656 from akellbl4/docs/add-ram-requirements
Add info about RAM requirements
2020-04-10 12:10:39 -05:00
Pavel Mineev 9660e2f6c2 add info about RAM requirements 2020-04-10 15:41:12 +03:00
荒野無燈andUmputun d336ab5053 add Simplified Chinese locale 2020-04-10 02:52:43 -05:00
Smolevich 6b2bc11710 Merge branch 'master' of github.com:umputun/remark into small-improvements 2020-04-09 21:50:44 +03:00
Pavel MineevandUmputun aecad2fce9 update node deps 2020-04-09 03:35:35 -05:00
UmputunandGitHub 923f182be5 Merge pull request #646 from codiflow/master
Added new language strings
2020-04-08 11:47:24 -05:00
Pavel MineevandUmputun b53f1872bb create type check config by extending default config 2020-04-08 11:47:00 -05:00
Pavel MineevandUmputun 01befe0c29 Fix linter errors 2020-04-08 11:47:00 -05:00
Pavel MineevandUmputun ddafb40b2d Update typescript to 3.8.3 2020-04-08 11:47:00 -05:00
Pavel MineevandUmputun 2a1690afcf Encode email in subscribe request 2020-04-08 11:10:32 -05:00
codiflow 97763c681f Merge remote-tracking branch 'origin/master' 2020-04-08 14:44:43 +02:00
Christian 48aff63eca Added new language strings 2020-04-08 14:43:52 +02:00
UmputunandGitHub a4e262e9e6 Switch back to go 1.13 and lint 1.23 (#647)
* combine lint and test for examples
* combine backend test and lint
* switch linter to 1.23
* back to go 1.13
* comment -mod=mod for 1.13
2020-04-07 15:30:00 -05:00
ChristianandGitHub c7e5059341 Added new language strings 2020-04-07 11:49:48 +02:00
UmputunandGitHub 1bb8fa4b61 Merge pull request #645 from lufte/es-locale
Spanish locale
2020-04-07 03:01:36 -05:00
konstantin krivlenia 879e5c8383 fix default en message form symbol limit 2020-04-07 10:48:00 +03:00
lufte 467496f34c Add translations for "new page" and english improvements 2020-04-06 23:39:40 -03:00
lufte a0d68b12ea Spanish locale 2020-04-06 23:37:11 -03:00
Umputun 9f12cc9f5f cut-paste err 2020-04-06 18:06:33 -05:00
Umputun cadd1e8a7c enforce -mod=mod for example ci 2020-04-06 18:03:54 -05:00
Umputun 73adc23176 try to copy .golangci.yml to _example 2020-04-06 17:22:58 -05:00
Dmitry VerkhoturovandUmputun cab3b8a831 move admin email notifications call to rest/api module 2020-04-06 16:27:19 -05:00
Dmitry VerkhoturovandUmputun 04d3541de1 add admin email notifications on new comments 2020-04-06 16:27:19 -05:00
Dmitry VerkhoturovandUmputun e377bceeb4 make notify.Email support multiple messages in one Send 2020-04-06 16:27:19 -05:00
Dmitry VerkhoturovandUmputun 726a4d7fc3 add translation instructions 2020-04-06 16:18:56 -05:00
UmputunandGitHub 079721298e update lic year 2020-04-06 13:04:50 -05:00
Pavel MineevandUmputun dfeff72d3e update vulnerable packages 2020-04-06 12:07:14 -05:00
Pavel MineevandUmputun 78a5bf15b5 Fix back button in email subscription form 2020-04-06 10:14:50 -05:00
Smolevich a404b8a87a Try to delete parallel flag 2020-04-06 11:27:27 +03:00
Smolevich ca9586491b Merge branch 'master' of github.com:umputun/remark into small-improvements 2020-04-06 10:41:34 +03:00
Umputun 40db689b27 update linter to 1.24 #641 2020-04-06 01:51:23 -05:00
Dmitry VerkhoturovandUmputun 106218df19 fix memory_storage run
- add proper formatting to Readme instructions
- fix docker-compose yaml
- add Enabled flag to admin storage, as without it I saw following error on trying to post a comment:
  ```
  mem_store.r42    | 2020/04/03 15:53:52.433 [INFO]  {logger/logger.go:120 logger.(*Middleware).Handler.func1.1} POST - /cmd - 172.20.0.3 - 200 (25) - 126.8µs - {"method":"admin.enabled","params":"remark","id":29}
  remark42         | 2020/04/03 15:53:52.434 [WARN]  {rest/httperrors.go:85 rest.SendErrorJSON} can't save comment - failed to prepare comment: can't get secret for site remark: site remark disabled - 500 (0) - dev_user/dev_user - /api/v1/comment - [rest/api/rest_private.go:121 api.(*private).createCommentCtrl]
  ```
  Flag was originally added 106c018ef1
2020-04-04 14:06:39 -05:00
Dmitry VerkhoturovandUmputun 0355ba4ff7 add image storage to memory_store 2020-04-04 14:06:39 -05:00
Dmitry VerkhoturovandUmputun ce92f63215 add remote image storage support to backend 2020-04-04 14:06:39 -05:00
UmputunandDmitry Verkhoturov b6ef4f393a switch to go 1.14 #636 2020-04-04 18:38:35 +02:00
UmputunandGitHub 98b77f076c Merge pull request #628 from paskal:img_fixes
image.Service and image.Store improvements
2020-04-02 21:48:41 -05:00
Pavel MineevandUmputun 7de82e0fef Fix clear localstorage after comment sending
* add functions for saving json to localstorage and tests
* remove comment form localstorage after posting
2020-04-02 16:25:37 -05:00
Smolevich 11c7f4ff55 Remove GOPATH from last step 2020-04-02 19:54:09 +03:00
Smolevich 509b4b66bd Recover old name of step and remove GOPATH 2020-04-02 19:52:45 +03:00
Smolevich e8f1e2d6b0 Remove unused config parts 2020-04-01 17:48:10 +03:00
Smolevich be8706efcf Fix path to profile.cov 2020-04-01 17:43:49 +03:00
Smolevich 1be319b4b9 Add working directory arg 2020-04-01 17:40:43 +03:00
Smolevich 0b53c4aeaa Add go env 2020-04-01 17:25:17 +03:00
Smolevich b713d0d3f1 Add GOPATH 2020-04-01 12:53:23 +03:00
Smolevich cede669c20 Add temporary using action 2020-04-01 12:40:49 +03:00
Smolevich 197eb0c9a0 Change ci-test-backend.yml 2020-04-01 12:20:53 +03:00
Dmitry VerkhoturovandUmputun 10c1aa939f remove .golangci.yml file for golangci-lint service 2020-04-01 02:33:39 -05:00
Pavel MineevandUmputun d9f913dbb0 fix ru translate 2020-04-01 02:32:30 -05:00
Dmitry Verkhoturov 4539b8ffb4 hide image.Store from image.Service consumers 2020-03-27 21:03:49 +01:00
Dmitry Verkhoturov e1d502ecf8 add clarifying comment to image.Store interface 2020-03-27 02:24:44 +01:00
Dmitry Verkhoturov 33dde7d4dd move MaxSize, MaxHeight and MaxWidth from image Storage to Service 2020-03-27 02:10:49 +01:00
Dmitry Verkhoturov ef6634e9b3 exclude unused filename from image.Store.Save() parameters 2020-03-26 23:04:49 +01:00
Dmitry Verkhoturov d714e32668 explicitly ignore error in test fmt.Fprint 2020-03-26 23:04:31 +01:00
Dmitry Verkhoturov fa4e779fa0 bump go modules in memory_store 2020-03-26 23:02:08 +01:00
Pavel MineevandUmputun 7ce400a5e5 move getting of hidden users to utils
* get hidden user by util
* add tests for changed actions
* add tests for getting users from local storage
2020-03-25 01:52:46 -05:00
Pavel MineevandUmputun e7dfd1efbb Fix hide/unhide user and setVerifiedStatus
* add tests for user store
2020-03-25 01:52:46 -05:00
Dmitry VerkhoturovandGitHub c9a23698ba Image interface changes (#623)
@paskal 

* sort imports, add missing copyright
* regenerate engine mock
* make all image.Store interface functions public
* go mod tidy
* make image.Store.Load return []byte instead of io.ReadCloser
* separate memory_store example RPC server to multiple files by handlers groups
2020-03-22 20:43:43 -05:00
Pavel MineevandUmputun a883052e03 Fix saving of comment sorting
* Add tests
2020-03-22 16:11:01 -05:00
Yonatan RybinskiandGitHub 1ba5e7a67e Added instructions for SES email configuration (#622)
@cigan1  (Yonatan Rybinski)

* Added instructions for SES
* Fixed domain
* Changed the name to Amazon SES not AWS SES
* Removed the extra URL
2020-03-22 15:57:59 -05:00
1c5928ad0b Add russian translates and update readme (#621)
Author: Pavel Mineev @akellbl4

* add russian translates and update readme

* Fix email adress string

Co-Authored-By: Dmitry Verkhoturov <paskal.07@gmail.com>

* Fix uploading disabled message

Co-Authored-By: Dmitry Verkhoturov <paskal.07@gmail.com>

* Change phrase for empty token string

Co-Authored-By: Dmitry Verkhoturov <paskal.07@gmail.com>

* Fix grammar for invalid email string

Co-Authored-By: Dmitry Verkhoturov <paskal.07@gmail.com>

* change word for thread

Co-Authored-By: Dmitry Verkhoturov <paskal.07@gmail.com>

Co-authored-by: Dmitry Verkhoturov <paskal.07@gmail.com>
2020-03-19 11:31:43 -05:00
Nikolay KorotkiyandUmputun 79e1009b19 Updated Russian translation 2020-03-18 13:16:34 -05:00
Nikolay KorotkiyandUmputun 61b78a1beb Added Finnish translation 2020-03-18 11:15:30 -05:00
Pavel MineevandUmputun 0363855de8 add license field 2020-03-12 15:52:38 -05:00
Pavel MineevandUmputun 24f0c19378 Update deps
* update vulnerable packages
* set fresh version of node because of using Object.fromEntries
2020-03-12 15:52:38 -05:00
konstantin krivleniaandUmputun 156f8e446f check that all translation messages are commit 2020-03-12 15:45:35 -05:00
Pavel MineevandUmputun fe4b46f924 fix id and generate langs 2020-03-12 15:44:31 -05:00
Pavel MineevandUmputun 0725013481 Hide error message after login 2020-03-12 15:44:31 -05:00
Pavel MineevandUmputun 779839aeb4 Add error on image upload by anonymous user 2020-03-12 15:44:31 -05:00
Pavel MineevandUmputun c9485fab3f Use named function for AuthPanel wrapped component 2020-03-12 15:44:31 -05:00
Pavel MineevandUmputun 93fd445bd4 On-demand auth
* auth block was moved to comment form
* if comments on page read only auth form shows in old place
* comment value in form will be saved between refreshes (it is side effect form saving comment value between unauth and auth states)
2020-03-12 15:44:31 -05:00
Dmitry VerkhoturovandUmputun d85e185aa1 add caddy documentation 2020-03-09 12:59:24 -05:00
Umputun 7497561d9c set anonymose with admin names to blocked #605 2020-03-08 15:37:31 -05:00
Pavel MineevandUmputun 827bc7dc40 Fix sorting 2020-03-08 00:14:03 -06:00
Pavel MineevandGitHub 85c71716f9 Small refactoring (#609)
* Move visibility param from redux store to local state

* use func for parsing location.search

* Remove bad wrapper

* we already have wrapper with NODE_ID on page and we shouldn't another one with the same id on page
* move common part of markup to level up

* Tinny refac of setSorting

* add dummy types for pollyfils

also, a bit rewrited way to resolve imports

* Move sort flag to local store

Because we don't need to share this param between diffrent parts of interface

* Add action creators

* move action to action creators

* Remove unused functions

* Merge in conditions in one

* One way for export API methods

* add default tags

* Rewrited changing read only mode

* removed unused function

* set sort changes

* always send sort from store
* rollback if sort don't  work

* constanst

* add typing
* shorten export
* remove double define of host
2020-03-07 12:25:18 -06:00
Pavel MineevandUmputun 30f3ffe517 Reduce size of last-comments widget
* use one api method insted full api file
* remove debug for preact
* use smaller constans file
* move unique constants
2020-03-07 03:59:03 -06:00
konstantin krivleniaandUmputun ea4e95c710 fix markup for subscribe block for long description 2020-03-07 03:58:34 -06:00
ChristianandUmputun f0eeb37dc0 Fixed typo 2020-03-06 12:22:32 -06:00
ChristianandUmputun 2287e2a889 Added german translation 2020-03-06 12:22:32 -06:00
UmputunandGitHub 75663ee227 Merge pull request #599 from Mavrin/use_css_custom_properties_for_colors
Use css custom properties for colors
2020-03-06 01:21:26 -06:00
konstantin krivlenia f6e73299cd #5 fix after rebase 2020-03-05 12:35:16 +03:00
konstantin krivlenia 6aeb72c3d9 #5 implement POC for theming api 2020-03-05 12:35:15 +03:00
konstantin krivlenia be6aa8314b #5 use css custom properties instead of color constant 2020-03-05 12:34:40 +03:00
b22cd5f76a Add localization (#602)
* #10 add localization

* #10 revert locale from dev page

* #10 add more examples

* #10 localize auth panel

* #10 localize some comment message

* #10 localize some comment form message

* #10 translate vote messages

* #10 translate comment message

* #10 use messages value for translate reference

* #10 fix compile issue

* #10 use messages value for translate reference

* #10 use messages value for translate reference

* #10 use messages value for translate reference

* #10 use messages value for translate reference

* #10 use messages value for translate reference

* #10 translate comment message

* #10 translate comment form

* #10 translate comment form

* #10 translate root component

* #10 translate user info

* #10 translate settings

* #10 translate settings

* #10 translate toolbar

* #10 translate errors messages

* #10 sort dict

* #10 fix after rebase

* #10 localize anonymousLoginForm

* #10 localize emailLoginForm

* #10 update size-limit

* #10 localize subscribe by rss

* #10 localize subscribe by email

* #10 add de locale

* #10 increase limit size

* #10 auto generate loadLocale

* #10 add some documentation

* #10 fix error messages

* fix typo

* don't bundle en locale

* fix message id

* change size limits

* Update extract message pattern

Co-Authored-By: Pavel Mineev <pavel@mineev.me>

Co-authored-by: Pavel Mineev <pavel@mineev.me>
2020-03-04 14:56:00 -06:00
konstantin krivleniaandUmputun 5039e8d277 optimize last comment widget 2020-02-22 12:55:25 -06:00
konstantin krivleniaandUmputun ffb41abfd4 optimize bundle sizes 2020-02-22 00:48:22 -06:00
Konstantin KrivleniaandGitHub 390ecbe8d9 Cleanup embed.ts and add limit-size checker (#601)
* Cleanup embed.ts and add limit-size checker

* add  size limit for dist files

* check only size of files
2020-02-19 16:08:35 -06:00
Konstantin KrivleniaandGitHub 11327cee5b Introduce stylelint (#600)
* introduce stylelint config

* run stylelint check on ci
2020-02-16 11:25:59 -06:00
Umputun 1334048d89 allow /api/v1/user calls for robots 2020-02-09 14:41:38 -06:00
Pavel MineevandUmputun 75ff933add Small restyling of comment elements
* quote with left border and without background
* style for old code insetions (structure pre > code)
* unified elemets margins
* 1px separator height
2020-02-09 13:49:58 -06:00
Pavel MineevandUmputun 90bd79abbb Add style for code element
* code-highlight.scss was moved
* css vars was defined in `raw-content`
* `pre` styles was used for `code`
2020-02-09 12:18:56 -06:00
d081bf4a24 Code colors (#592)
* render code htm with classes

* code highlight styles

* revert unneded gitignore change

Co-authored-by: Umputun <umputun@gmail.com>
Co-authored-by: Dmitry Verkhoturov <paskal.07@gmail.com>
2020-02-09 03:18:42 -06:00
1e548eb85f Code colors (backend support) (#590)
* testing code highlight with bfchroma render

* add bfchroma vendoring

* render code htm with classes

* adjust bfchroma test cases inside formatter

* go mod tidy

* fix markdown code render tests

* adjust comment text sanitizer to permit code HTML classes

* go mod tidy

* add chroma classes list regexp

* unify gitignore entries style

Co-authored-by: Umputun <umputun@gmail.com>
2020-02-08 21:11:12 -06:00
UmputunandGitHub b566d7f844 Merge pull request #589 from paskal/blackfriday
Replace blackfriday.v2 with blackfriday/v2
2020-02-07 15:25:37 -06:00
Dmitry Verkhoturov 62c43cce77 replace blackfriday.v2 with blackfriday/v2 2020-02-07 22:15:05 +01:00
Pavel MineevandUmputun d22e364d1b Autosubmit token by paste and a bit more (#583)
* Use lib for typing tests instead copied types

* add jwt utils

* Add autosending

* fix

* tests

* remove atob and fix tests
2020-01-28 00:38:02 -06:00
019fa9e5ac Simpler image service (#580)
* fix potential race on close

* move validate inside

* demote commit and cleanup in image.Store to non-exposed functions.

* replace immediate image commit with delayed via Submit

* minor: remove error logging, rename tests

* minor: err wrapping, comments wording

* clarify FileSystem.Save code

* attempt to fix #584 by making submitted image commits on a half of TTL

Co-authored-by: Dmitry Verkhoturov <paskal.07@gmail.com>
2020-01-27 14:17:58 -06:00
Umputun be3e5f0246 set exclude-use-default: false for linter 2020-01-27 14:17:05 -06:00
Artur MullakhmetovandUmputun 22a88c579f Fix unstable test
Make comment consistent with code
Decrease timeout param to prevent unwanted 200 response
2020-01-26 11:35:17 -06:00
Artur MullakhmetovandUmputun dcab5a1622 switch image interpolation to CatmullRom 2020-01-26 11:35:17 -06:00
UmputunandGitHub 5ecea4f80c Update FUNDING.yml 2020-01-26 11:30:49 -06:00
Pavel MineevandUmputun 1804571d4d fix 2020-01-24 00:17:32 -06:00
Pavel MineevandUmputun 45f929868a fix name line break and add styles for blockquote 2020-01-23 21:45:32 -06:00
Dmitry VerkhoturovandUmputun f97d8232ed Add subscription link support to notification email verification template (#573)
* add subscription link support to notification email verification template

* always show token for email subscription in verification email

* hide SubscribeURL from users
2020-01-20 18:27:23 -06:00
Pavel MineevandUmputun 091ac9b4c1 Add shallow equality 2020-01-20 13:17:27 -06:00
Vyrtsev MikhailandUmputun 029f50c52c do not render dummy for comment once it gets into viewport 2020-01-19 12:51:28 -06:00
Dmitry VerkhoturovandUmputun 8c37fac06e actualise CLI options information in the readme 2020-01-19 04:10:55 -06:00
Umputun 4d2ed6bde4 typo in version info print 2020-01-18 22:42:51 -06:00
Umputun f30f9b97af missing methods comments 2020-01-18 22:39:21 -06:00
Anton PanferovandUmputun d69495a9e4 Preserving external images (issue #399) (#447)
* added passing userID to formatters

* added images preserver

* pinned version of golangci-lint

* implemented caching in proxy

* added tests for caching proxy

* removed previous implementation

* removed passing userID into formatter

This reverts commit 05014a5b24190836958377fcd7304294ed02f061.

* made linter happy

* removed storing image extension for FS store

* fixed bug with context cancelation preventing reading downloaded image

* fixed bug with trusting return value and ignorring error

* updated README

* added nolint for sha1

* removed unused resized flag

* added backward compatibility for config

* added handling deprecated cli parameter

* corrected func comment

* made linter happy

* made linter happy
2020-01-18 22:31:30 -06:00
Dmitry VerkhoturovandUmputun 0b55c4f7ad lower Cache-Control max-age from 24h to 1h to increase speed of updates delivery 2020-01-18 17:58:58 -06:00
Dmitry VerkhoturovandUmputun 540d828ea1 fix ParentCommentDate in email template, fix #561 2020-01-17 17:19:19 -06:00
Pavel MineevandUmputun a3d038f75b fix 2020-01-16 16:42:07 -06:00
Dmitry VerkhoturovandUmputun 8148650247 Update documentation for email capabilities (#555)
* replace deprecated email variables names with new ones

* clarify email authentication instructions

* add email notifications documentation

* add explanation screenshots

* fix punctuation and typos
2020-01-16 11:20:33 -06:00
Umputun b0898cd1f3 add cache control for frontend assets 2020-01-15 15:07:45 -06:00
Pavel MineevandUmputun b082c2a82f Fix
*  fix score position
* add margin before bloquote if it is placed after text
2020-01-15 02:44:05 -06:00
Pavel MineevandUmputun 4f38c3ea31 FIx height of comment info 2020-01-15 01:50:10 -06:00
Pavel MineevandUmputun 658279379a Update email templates (#550)
* Update email templates

* Fix README

* fix detail
* fix typo

* Changes connected with comment at issue

https://github.com/umputun/remark/issues/494#issuecomment-570801318

* Unify styles between templates

* remove breaking words

* use the same prefix for parent and child
2020-01-15 01:22:00 -06:00
Pavel MineevandUmputun ce01a347ca Collapse comment by thread border (#545)
* collapse comment by border

* Fix after review

* hide avatar in collapsed state
* fix lines icon
2020-01-14 17:11:30 -06:00
Dmitry VerkhoturovandUmputun d0ddd8aa80 Unify SMTP settings in separate section (#536)
* move SMTP settings to separate group

* move deprecated options in separate section in readme

* adjust variables in docker-compose

* add description to SmtpGroup

* remove SMTP option setting which is already set to same value

* remove smtp port default for consistency

* add server deprecated functions handling

* satisfy linter

* add missing bracket in description

* add test for handleDeprecatedFlags

* add HandleDeprecatedFlags function to CommonOptionsCommander

* improve HandleDeprecatedFlags behavior

* add missing result check to ServerCommand.HandleDeprecatedFlags
2020-01-14 14:49:39 -06:00
Konstantin KrivleniaandUmputun 17be003652 #378 Emoji suggestions (#546)
* #378 add emoji suggestion

* #378 use lazy load for nodeEmoji

* #378 apply remark styles only for light theme

* #378 add dark theme

* #378 fix test

* #378 use permanent class names

*  #378 flip if else for readability
2020-01-13 18:26:32 -06:00
Umputun 3c12fde0f8 fix .golangci.yml location inside docker build 2020-01-13 18:19:57 -06:00
StasandUmputun b2176a175d Use golangci lint action (#549)
* Use action insted download binary file

* Update ci

* Add config option

* Use pre-built docker image for golangci-lint-action

* Remove pre build docker action

* Update action name

* Remove env

* Update ci file

* Add working directory

* Change GITHUB_WORKSPACE

* Update ci file

* Fix ci

* Comment golangci-lint-action

* Update working-directory

* Fix path

* Update ci-build.yml

* Update path

* Revert changes

* Update .golangci.yml

* Add new path for triggers ci

* Separate steps

* Add working-directory

* Add test pwd and ls

* Fix working directory

* Update script for linting in Dockerfile

* Move .golangci.yml to dir backend

* Delete unnecessary word

* Remove multiline string

* Reformat multiline strings to single

* Update disable-all option
2020-01-13 10:56:56 -06:00
Pavel MineevandUmputun 3447afffdd fix position on "show more" button 2020-01-12 13:01:04 -06:00
Pavel MineevandUmputun 7b67b10d5d Run es-check by cli 2020-01-08 16:18:39 -06:00
Pavel MineevandUmputun 7d9e8ce076 UI for email subscription (#537)
* Add api methods for subscription

* Small changes

* little changes in remark.tsx
* disallow pass className to Button and Input

* Subscribe block

* add RSS and  Email subscription drobdowns
* add hook useTheme
* unify dropdown import/export

* Change API

* rename subscribe methods
* add unsubscribe method

* Add email subscription to settings and user

* Refactor and add new steps

* render by single component
* add final step
* add unsubscribe step it user is subscribed

* Add tests

* Update subscription logic

* test without mocking redux methods but with mocking store
* update user in store after subscribe and unsubscribe
* little changes in subscription flow

* fix drobdown size

* Fix RSS subscription link for site

* fix link
* add test for RSS subscription

* Fix showing email subscription

* it don't show to unauth users
* it don't show to anonymous users
* it tested
* isUserAnonymous is a bit rewrited
* isUserAnonymous is tested

* Make Email button visible for anon users

* React X supporst Fragments thats why .babelrc changed
* disabled button is more visible
* fix hovering on disabled buttons
* create mocks for tests
*  move email dropdown to __subscribe-by-email
2020-01-08 13:55:50 -06:00
Pavel MineevandUmputun bfa308e8da Allow voting for anonymous users 2020-01-07 23:43:49 -06:00
Pavel MineevandUmputun 5ee76febf2 Update vulnerable dependencies 2020-01-07 18:33:23 -06:00
Dmitry VerkhoturovandUmputun e4d982dbca Delete user email on /deleteme request (#540)
* delete user email on deleteme request

* add tests for user email deletion in /deleteme request
2020-01-07 14:49:24 -06:00
Dmitry VerkhoturovandUmputun eeb9c72248 check user equality for email notifications only by ID 2020-01-07 14:25:27 -06:00
Pavel MineevandUmputun a309fc8fb0 Refactor preloader
* make it simple (use only one DOM element for preloader)
* remove useless test (it tests nothing)
* unify preloader import/export
2020-01-07 11:14:59 -06:00
Pavel MineevandUmputun 78bb9dd153 Add auto-changing theme for development
Theme is changing follows to OS settings.
2020-01-07 03:57:34 -06:00
Pavel MineevandUmputun c660d2a522 Change text sign in to login 2020-01-05 18:35:22 -06:00
Pavel MineevandUmputun 07137b34e7 Run prettier on all files 2020-01-05 17:52:25 -06:00
Pavel MineevandUmputun 51d2310c1f Nice inputs and buttons (#510)
* Set default font in examle

* prettify file

* add ui elements

* Use UIButton at AuthPanel

* sort deps
* remove unsed getUserTitle method
* move UserId inside AuthPanel
* tests

* use UIButton and UInput in Anonymous login

* Use UIButton and UIInput in email login

* Replace Button to UIButton

* fix context for onTitleClick in dropdown
* rearrage class props in dropdown
* TODO: change finding over DOM to using ref

* Use UIButton in input

* rearrage deps

* Use UIButton in comment

* test

* Focus and Input buttons

* custom focus style for inputs and buttons
* the same focus style for comment input
* align buttons by top line in input

* Token dropdown & hover fix

* disable hover when button disabled
* make token dropdown markup in new style

* Fix trailing comma

* update prettier
* move prettier config to json format because it is more hendy for settings
(for example vscode can suggest rules)
* prettier fixed trailing comman in ejs

* Remove unnecessary conditions

* Files was formatted by Prettier

* fix cursor pointer on collapse button

* Move components and right naming

* ui-button -> button
* ui-input -> input
* input -> comment-form

* Change cursor behavior on disabled state

* Change button style

* font-weight: normal by default
* add small border-radius

* Change another one button to component

* Fix autofocus on username in email login form

* use preact inbuild autoFocus
* fix autofocus on back from token step
* sleep for 0s enough to wait next render before focus
* use more specific name for username input

* fix line-height at auth line on mobile

* fix className
2020-01-05 14:58:10 -06:00
Umputun aea7724769 populate version info to the local builds 2020-01-05 14:36:53 -06:00
Pavel MineevandUmputun c49b88d930 I removed build actions from test part 2020-01-05 14:17:30 -06:00
Dmitry VerkhoturovandUmputun d15176f36d Add email subscription info to User information (#528)
* replace store.Locator with SiteID where only it is used

* add EmailSubscription flag to User information

* add /user endpoint test for email subscription
2020-01-05 13:42:16 -06:00
Pavel MineevandUmputun 2efab67640 fix warnings 2020-01-05 12:59:35 -06:00
Pavel MineevandUmputun 01f19ea511 fix rtl typing in ie11/edge 2020-01-05 12:58:54 -06:00
tim.ivaikinandUmputun a4d10f54dd Support for 3x devices including 11 Pro and MacPro 2020-01-05 12:58:25 -06:00
tim.ivaikinandUmputun 0462cbcd9e Added retina display support 2020-01-05 12:58:25 -06:00
Umputun d0925ba95f ident ci lists for better readability 2020-01-04 21:09:27 -06:00
Umputun d06a27193c add codeowners file 2020-01-04 20:49:56 -06:00
Dmitry Verkhoturov c56c39c59d separate build and test CI workflow, separate frontend and backend tests 2020-01-04 20:53:39 +01:00
Dmitry VerkhoturovandUmputun b7980849b2 improve TestService_WithDrops failure output 2020-01-04 12:53:31 -06:00
Dmitry VerkhoturovandUmputun 88e46446b6 remove empty lines from dockerfile 2020-01-04 12:53:31 -06:00
Umputun 3307394932 don't run any test on rundev target, use private compose 2020-01-04 01:36:44 -06:00
Pavel MineevandUmputun c69e375e45 Add "simple view" mode (#502)
* Add "simple view" mode support

It just hide elements from view when SIMPLE_VIEW recived from server

* Fix typing and add ts check before push

* proper input styling

* Changes for frontend dev compose

* remove SIMPLE_VIEW from default settings for forntend dev
* add private compose to gitignore

* Added simpleView mode for replay and edit modes.

* `simpleView` changed to required param

* FIx border-width in reply form

* Fix border-width in editing mode
2020-01-04 01:31:06 -06:00
Dmitry VerkhoturovandUmputun 4cae7f0324 make TestServerApp_MainSignal more robust 2020-01-02 14:35:50 -06:00
Dmitry VerkhoturovandUmputun 07a14edc3b adjust name for CI build with frontend tests 2020-01-02 14:35:50 -06:00
Umputun b434f52aa1 resolve potentially concurrent writes to bolt in service tests 2020-01-02 14:02:16 -06:00
UmputunandGitHub ffc81f1f78 Merge pull request #519 from akellbl4/fix/hide-error-in-email-form
Hide error after repeated request in email login form
2020-01-02 11:54:50 -06:00
Pavel Mineev ff83b446b8 Fix error hiding after repeated request 2020-01-02 15:19:13 +03:00
UmputunandGitHub 82d0cb97d1 Merge pull request #518 from mullakhmetov/close-testserver
Shutdown test servers
2020-01-01 16:13:37 -06:00
Artur Mullakhmetov 2d942f0a2c Shutdown test servers 2020-01-02 00:29:43 +03:00
Artur MullakhmetovandUmputun 12b916b097 Make test offline friendly 2020-01-01 15:14:26 -06:00
Dmitry VerkhoturovandUmputun 6f9c87850a Adjust tests timouts to work on machine with HDD (#516)
* increase timeout for TestServerAuthHooks http client

* replace assert.Equal checks for slice length with require.Equal

* unify channel name across tests

* fix panic in Test_Main

* increase TestRest_CreateWithPictures timeout for HDD slowness

* increase TestService_VoteSameIPWithDuration timeout for HDD slowness

* increase go test timeout for HDD run

* increase TestRest_CreateWithPictures timeout for HDD slowness
2020-01-01 14:46:39 -06:00
Pavel MineevandUmputun 25782d7d3e fix ts error 2019-12-30 15:53:33 -06:00
Umputun 4352c86d1f Merge branch 'master' of github.com:umputun/remark 2019-12-30 15:31:05 -06:00
Umputun 9660f7ea7e add more private composes to ignore 2019-12-30 15:25:13 -06:00
Dmitry VerkhoturovandUmputun a1f8ad7bd8 expose email_notifications flag on /config endpoint (#514) 2019-12-30 15:05:55 -06:00
Dmitry VerkhoturovandUmputun dbd83a1f0d Fix flapping TestRest_ tests (#513)
* replace single-member wait groups with channels

* increase TestRest_LastCommentsStreamSince comment write delay

* increase timeout for TestRest_InfoStreamCancel

* increase TestServerApp timeout

* adjust waiting time in TestRest_LastCommentsStreamSince in attempt to fix false positive

* adjust comments waiting location in multiple TestRest_ tests
2019-12-30 14:31:49 -06:00
Dmitry VerkhoturovandUmputun 20f667bd1d Improve TestMigrator_* tests reliability (#512)
* simplify api tests set up

* close DataStore\Engine in all test code after usage
2019-12-30 13:41:24 -06:00
Dmitry VerkhoturovandUmputun f416c6c5eb Fixes for multiple tests (#511)
* improve TestServer* reliability

* improve TestService_UserReplies reliability

* increase timeout for Test_Main

* improve TestRest_CreateWithPictures readability and reliability

* introduce random port to REST over SSL tests

* tinker TestRest_InfoStreamSince to have more slack before failure

* finalize test errors check unification

* simplify prepServerApp in cmd package tests

* improve TestRest_InfoStreamCancel reliability
2019-12-30 12:09:05 -06:00
Dmitry VerkhoturovandUmputun 62cc504600 Improve Test_Main reliability (#509)
* adjust TestServerApp_WithSSL to use sslPort in all test checks

* make Test_Main reliable and remove 5s sleep

* make test finishing reliable using "done" channel for TestServerApp*
2019-12-30 03:44:10 -06:00
Dmitry VerkhoturovandUmputun fddb737657 Improve TestServer* tests reliability (#508)
* explicitly ignore error from test connection close

* add client with timeout to places which used default http client

* move random port creation and waiting for server in separate function for reuse

* improve TestServer tests robustness

* move all server waiting code in tests to separate functions

* change chooseRandomUnusedPort to try to listen to port before return

* fix waitForHTTPSServerStart
2019-12-29 20:46:22 -06:00
Dmitry VerkhoturovandUmputun b82be0cc2d Check if the port already in use in TestRPC_ tests server startup (#506)
* check if the port already in use in TestRPC_ tests server startup

* check for open port instead of for HTTP server listening
2019-12-29 18:24:42 -06:00
Dmitry VerkhoturovandUmputun f319cf3c05 fix "address is already in use" for TestRest_RunStaticSSLMode and TestRest_RunAutocertModeHTTPOnly (#507) 2019-12-29 17:47:20 -06:00
Dmitry VerkhoturovandUmputun 36b4f6774c make TestRPC tests reliably wait for HTTP server to start (#505)
* make TestRPC tests reliably wait for HTTP server to start

* adjust the wait period to be limited by 3 seconds
2019-12-29 16:23:47 -06:00
Dmitry VerkhoturovandUmputun 828aeda9b1 fix flapping TestFsStore_Cleanup (#504) 2019-12-29 16:02:36 -06:00
Dmitry VerkhoturovandUmputun 7bab10ab57 Replace "(Not)Nil" test checks for errors with with "(No)Error" (#503)
* replace "(Not)Nil" test checks for errors with with "(No)Error"

* unify error text checks style

* clarify error check in tests in memory_store
2019-12-29 15:42:39 -06:00
Umputun fe4e38a5b1 add make targets for backend build and test 2019-12-29 13:27:36 -06:00
Dmitry VerkhoturovandUmputun 9df2b2a9ed Move email unsubscription endpoint outside of API and make it HTML (#500)
* move email unsubscription page outside of API and make it HTML

* make separate HTML template for SendErrorHTML

* fix error template name

* add test for SendErrorHTML, introduce MustExecute function

* fix content check in test of TestSendErrorHTML

* fix logging test to be more generic and not depend on line numbers
2019-12-27 01:04:57 -06:00
Dmitry VerkhoturovandUmputun b055c61be7 Add backend support for anonymous voting (#501)
* add backend support for anonymous voting

* add test for anonymous user voting

* clarify test code
2019-12-26 17:42:03 -06:00
UmputunandGitHub 151809825a Merge pull request #499 from paskal/improve_service_tests
Add unit tests for service operations with user details
2019-12-22 16:04:57 -06:00
Dmitry Verkhoturov 997127f33e add tests for service operations with user details 2019-12-22 22:23:37 +01:00
Dmitry VerkhoturovandUmputun fde6f520c9 improve email notifications tests coverage (#498) 2019-12-21 11:31:01 -06:00
Umputun 3eacd6f201 add freebsd binary build target 2019-12-18 07:58:28 -06:00
Umputun 3b06571f62 inject version number to bin builds #456 2019-12-17 03:22:10 -06:00
Umputun 0e19d11df8 reformat mock 2019-12-17 02:54:58 -06:00
Dmitry VerkhoturovandUmputun 6a5cd79074 docker-compose format and comment fixes (#492)
* fix typos in readme

* unify format of docker-compose files, fix comments
2019-12-17 01:36:10 -06:00
Dmitry VerkhoturovandUmputun d23d119d70 Add API methods for setting and deleting email (#483)
* add API methods for setting and deleting email

* fix service.SetStringUserDetail signature to return string

* switch table test with description to t.Run()

* remove debug logging

* clarify error handling, functions names

* add email integration test

* add information about email subscription to readme

* change email API calls method from PUT to POST

* typo fix, remove unneeded capturing of range variable

* email test draft

* fix notify mock, email notification test draft

* add MockDestination to startupT return

* fix tests

* add email retrieval for notifications sending

* fix mock for notify

* rearrange mock notify declaration

* add GET /email API handler, fix typos

* revert startupT signature change

* get rid of startupTWithDest workaround

* add rest examples for rest notification

* improve email messages formatting

* fix email send repeater location

* remove unneeded context from sendMessage

* change signatures of buildMessage functions to have same field name

* add missing authenticate call on TLS connection

* add dev user auth token to email requests

* change email verification template

* email code and tests cleanup

* replace fixed spaces with normal ones

* human-readable variables names for new comment reply notification

* rename Comment to CommentText

* add html for comment email notification

* fix comment notification html style

* fix email test

* fix notify email messages rendering

* fix comments on rest examples for email

* explicitly state email notify email template fields

* clarify email API documentation

* change email test not to check quoted-printable part of message

* Fix link color, add unsubscribe link

* fix rest examples tokens

* add UnsubscribeLink support to Email

* add unsubscribe email handler

* fix new reply notification email style
2019-12-16 16:39:55 -06:00
Slava SemushinandUmputun 45fa60f5a1 README.md: fix a typo in a variable name from an example (#491)
s/TIME_ZINE/TIME_ZONE/
2019-12-15 21:28:40 -06:00
Dmitry VerkhoturovandUmputun 30c42ce36b Multiple CI and code fixes (#486)
* enable golangci-lint for momeory_store example

* add race_test option to makefile

* prune lost goroutine in TestRest_Shutdown

* run race tests without cache

* fix ci pipeline

* fix typos
2019-12-09 12:30:07 -06:00
Dmitry VerkhoturovandUmputun b40cb7866b Email sender implementation (#471)
* email sender initial implementation

* add VerificationMetadata into notify.Request

* multiple review fixes

- customizable verification notification message subject
- clarify autoFlush nature in its commentary
- defer writer.Close in Email.sendEmail instead of calling it manually,
  potentially missing the call if error happened between the creation and
  closing.

* add explanatory commentary to notify.Request structure

* fix TCP connection timout commentary typo

* improve table tests presence

* introduce parallelism to tests

* abstract smtpClientWithMaker away

* fix incorrect Email reference in Email.sendMessages

* naming fixes, remove t.Parallel() from tests

* consistent space in commentary

* rename sendEmail to smtpSend, rearrange variables definitions

* switch Email to create new connection for every Send request

* fix tests for connection-per-submit email sending

* fix tests

* simplify sender object
2019-12-02 18:59:32 -06:00
UmputunandGitHub ea8ac08c72 Merge pull request #480 from paskal/email_interface_clarification
Switch notify.Submit to receive notify.Request instead of store.Comment
2019-11-23 13:22:17 -06:00
Dmitry Verkhoturov 6c73a38c49 switch notify.Submit to receive notify.Request instead of store.Comment 2019-11-23 13:24:57 +01:00
Dmitry VerkhoturovandUmputun ddd466ec41 implement user details storage (#469)
* implement (strings) user details storage

* add rpc user details implementation

* return error from getUserDetail, rewrite tests to table tests

* make UserDetails store UserDetailEntry instead of strings

* update comment about user_details

* fix confusing return

* add user details support for memory store

* add engine.UserDetailEntry to service.UserMetaData

* add ListDetails support to memory storage

* add user details support to native migrator, ListDetails func to storage

* go mod tidy for memory storage

* increase memory storage test coverage, fix tests naming

* add ListDetails tests to memory storage

* add engine.ListDetails and  service.[Set]Metas tests

* change Fprintf to Fprint (triggered by explicitly ignoring error)

* remove Delete from engine.UserDetail, implement list via same method

* adjust service.Metas to new engine.UserDetails signature

* introduce engine.UserDetail("all") consonant

* fix Meta user detail retrieval

* extend store implementations Delete method with UserDetail deletion

* make UserDetail test answer order-independent

* fix flaky test check in TestMemData_FlagListBlocked

* delete user details alongside with comments on deleteme request

* add tests to UserDetail store.Delete implementations

* clarify engine module user details consonants names

* update comments to reflect current state of code

* check for value absence instead of it's length

* revert unneeded code change

* add extensive commentary on UserDetail return type

* remove unused check condition

* clarify UserDetail tests to be truly stateless

* add clarifying comment for pre-table test
2019-11-22 02:24:26 -06:00
Dmitry VerkhoturovandUmputun 773da16649 Improve memory store tests reliability (#479)
* remove unnecessary GIT_BRANCH export

* go mod tidy

* fix tests naming, improve coverage

* make flaky TestMemData_FlagListBlocked reliable
2019-11-17 15:47:01 -06:00
Umputun 6f969a0a76 fix _example test call 2019-11-11 18:55:23 -06:00
Umputun ae40fbeaf4 add example test 2019-11-11 18:52:13 -06:00
Umputun 21e6360ca4 update deps and vendor 2019-11-11 18:50:30 -06:00
Umputun 6abb2727c4 switch to lcw cache 2019-11-11 18:50:09 -06:00
Umputun f86f7dcc23 typo 2019-11-11 09:33:20 -06:00
Umputun b41c539d48 Merge branch 'master' of github.com:umputun/remark 2019-11-11 09:29:39 -06:00
Umputun 5d50f0d794 typo 2019-11-10 17:02:34 -06:00
UmputunandGitHub ffec9272ce Merge pull request #475 from Smolevich/fix-readme
Add href for new paragraph in README.md
2019-11-10 16:23:54 -06:00
Umputun 0672cb41d4 change From email to example.com 2019-11-10 16:17:41 -06:00
Umputun f821049d34 add docs about email 2019-11-10 13:13:43 -06:00
Umputun c5c065975a adjust tests for updated error logs 2019-11-09 13:50:14 -06:00
Umputun 731bcb9606 remove caller ip from logs on http errors 2019-11-09 13:45:36 -06:00
Umputun 0ebf5e1822 make docs to allow easier contributions #422 2019-11-09 13:33:36 -06:00
Umputun 9831f75ea9 promote rest errors to WARN level 2019-11-09 12:34:19 -06:00
Umputun 96d19afbb4 correct twitter auth description
https://github.com/umputun/remark/commit/38e9dc97e63890c3bd65970e888bf7d47c47e1ff
2019-11-09 12:14:57 -06:00
Shupilkin Stanislav Yurevich 2e473ae1b8 Add href for new paragraph in README.md 2019-11-08 18:24:34 +03:00
Umputun 38e9dc97e6 add auth twitter info 2019-11-08 01:54:13 -06:00
Umputun 18ae77a806 longer streamer timeout in cancel test 2019-11-06 17:01:43 -06:00
Umputun 9269666931 add simple-view param and env (SIMPLE_VIEW) #474 2019-11-06 13:47:45 -06:00
Dmitry VerkhoturovandUmputun 7536e265b2 Change coveralls to use build-in GITHUB_TOKEN (#465)
* change coveralls to use build-in GITHUB_TOKEN (should work in forks)

* send GITHUB_REF as GIT_BRANCH

* add debug output

* change service to github

* remove debug output

* move COVERALLS_TOKEN to environment, remove argument
2019-11-05 18:13:25 -06:00
Umputun 4c7e454d86 fix login url with wrong ? #463 2019-11-05 12:45:06 -06:00
Umputun 95fa79241b add details to aud mismatch message 2019-11-05 12:19:01 -06:00
UmputunandGitHub 9b570ae273 change to github sponsors 2019-11-04 18:59:07 -06:00
Umputun dff1e97604 remove ci debug prints 2019-11-01 03:08:13 -05:00
Umputun 524e963130 show GITHUB_EVENT_PATH content 2019-11-01 03:03:18 -05:00
Umputun 8494296b85 fix out lcov name 2019-11-01 02:31:30 -05:00
Umputun aa5ebda1eb convert 2 lcov manually 2019-11-01 02:27:32 -05:00
Umputun 9c5d1840ae use short path for coverage reports 2019-11-01 02:18:30 -05:00
Umputun 8bfe66e725 split coveralls 2019-11-01 02:13:04 -05:00
UmputunandGitHub 37b2f48144 Actions (#458)
* remove coverage report from docker, add build and test step to actions

* install deps to actions

* formatting fix

* install go 1.13

* change linter location
2019-11-01 02:01:24 -05:00
Umputun b706bbaf05 trigger rebuild 2019-10-31 23:40:42 -05:00
Umputun 93c9a2e608 remove service selection 2019-10-31 23:19:57 -05:00
Umputun a60128a957 pass GITHUB_REPOSITORY to ci 2019-10-31 22:58:46 -05:00
Umputun cd5f0b3b91 typo in passing env to ci docker build 2019-10-31 22:34:53 -05:00
Umputun 93103534bc ling: nolint for choice in flags 2019-10-31 22:20:32 -05:00
Umputun cdcf5171cf lint: new warnings from 1.21 golang-lint 2019-10-31 22:09:28 -05:00
Umputun ecb804ce41 update coveralls badge link 2019-10-31 21:56:40 -05:00
Umputun e9cf8ca69e pass more env to ci build to make coveralls happy 2019-10-31 21:43:26 -05:00
Umputun 3ad37810e6 add build compat with actions 2019-10-31 21:05:55 -05:00
Umputun c154fae28f rename actions, replace travis badge by action's 2019-10-31 20:38:13 -05:00
UmputunandGitHub 776b9117fc CI changes (#457)
* make tests more resilient

* adjust drone for v1.x and dev-box, remove travis support

* add backend build step on branch for drone

* fix from email

* change notif email

* less demanding test wait

* add settings section to drone plugins

* adjust branch build

* convert all drone ci docker confs

* rename drone targets
2019-10-31 20:20:18 -05:00
Umputun 3a7ae4f616 revert increase import expired timeout 2019-10-29 03:56:36 -05:00
Umputun 9bd4519e48 increase import expired timeout 2019-10-29 03:51:13 -05:00
Umputun 653238bc99 increase test timeouts 2019-10-29 02:43:11 -05:00
Umputun f859239bdf faster import test 2019-10-28 23:34:36 -05:00
Umputun ab0866cb77 randomize bold db for native migrator test 2019-10-28 22:19:14 -05:00
Umputun 488f9bedcf add image.bolt.file info 2019-10-28 21:40:27 -05:00
UmputunandGitHub 9be7925246 Merge pull request #454 from Reeywhaar/preact-10
update preact to preact 10
2019-10-27 19:43:36 -05:00
Vyrtsev Mikhail a2e508cc1e update deps 2019-10-28 02:46:34 +03:00
Vyrtsev Mikhail 02b2895cac update Preact to Preact X 2019-10-28 02:30:49 +03:00
Vyrtsev Mikhail 4240a4d6ee update minor deps 2019-10-27 23:22:58 +03:00
Vyrtsev Mikhail 4f3400e594 update Comment type 2019-10-27 23:22:57 +03:00
Umputun eb67ba85d1 update deps 2019-10-27 13:57:10 -05:00
Umputun 6ad2263859 Merge remote-tracking branch 'origin/master' 2019-10-27 13:32:40 -05:00
UmputunandGitHub cc5e8a6356 Merge pull request #453 from StaroKep/fix/452_dark-theme-shows-white-preview
Dark theme shows white preview #453
2019-10-24 15:56:21 -05:00
Kirill Eremin 64b2830b5b fix 2019-10-24 23:37:17 +03:00
VdimirandUmputun b7045d9771 Add bolt image storage (#449)
* Add bolt image store support

* Fix lint errors

* Use separate bolt buckets for staging and commited

* Comment bolt store public entities

* Fix spelling
2019-10-22 02:35:48 -05:00
Umputun 58f92062d9 Merge branch 'master' of github.com:umputun/remark 2019-10-19 13:05:27 -05:00
UmputunandGitHub d7faf2df0e Merge pull request #448 from Vdimir/backend-test-timeout-param
Add parameter to change backend test timeout
2019-10-19 12:42:43 -05:00
VDimir 13bdf8358a Add parameter to change backend test timeout 2019-10-19 12:48:49 +03:00
Umputun 7644259a38 update deps 2019-10-17 15:50:59 -05:00
UmputunandGitHub a310e222d9 Merge pull request #442 from umputun/twitter-auth-70
add twitter to the list of providers on frontend #70
2019-10-10 11:29:39 -05:00
Umputun d14f7e2c47 add arm target arch #444 2019-10-10 00:33:10 -05:00
UmputunandGitHub 71742f5cfa Merge pull request #443 from smaant/fix-development-docs
Fix docs
2019-10-09 16:36:14 -05:00
Anton Panferov 43ebc61eca removed mongo mentions 2019-10-09 11:26:56 -10:00
Anton Panferov 74a58562f0 corrected non docker run command 2019-10-09 11:25:50 -10:00
Umputun da63dd2c78 add twitter to the list of providers on frontend #70 2019-10-08 03:14:24 -05:00
Anton KosourovandUmputun 4dc123dbe8 Change url in comments, decorate approach (#431)
* WIP: url mapper, wrapped reader approach

* create url mapper on start

* add pattern matching in mapper

* check pattern matching in test

* change site-id from radio-t to remark42 in tests

* create new url mapper on demand, based on given func, union strict and prefix rules

* rename convert to remap

* add import with mapper test

* rename mapper func to UrlMapperMaker, create comments in test via data service

* move /import/wait to /wait ctrl

* add remap cmd

* fix url naming
2019-09-30 13:12:42 -05:00
Misha VyrtsevandUmputun 6b2addc280 Fix #430 (#440)
* add postMessage util

* use scrollTo via postMessage (fixes "scroll to parent" on Safari #430)

* use postMessage
2019-09-29 15:38:09 -05:00
Misha VyrtsevandUmputun 9fbdff106d Update deps 2019 09 (#439)
* update minor deps

* update minor deps

* update major deps

* fix eslint warnings
2019-09-29 14:09:00 -05:00
UmputunandGitHub ea2a15ea8e add patreon link 2019-09-27 12:03:16 -05:00
Umputun 1cbedb85b0 fix -time sort for user comments 2019-09-26 23:06:20 -05:00
UmputunandGitHub 8cf611690a Merge pull request #437 from bessarabov/patch-1
Added missing comma to the snippet code in README
2019-09-22 13:33:24 -05:00
Ivan BessarabovandGitHub 851c9dc3b0 Added missing comma to the snippet code in README 2019-09-22 16:19:45 +03:00
StasandUmputun 582c9d6bce Fix problem with DEBUG variable (#435)
* Fix problem with DEBUG variable

* Add condition for starting ci on pull requests

* Add default value

* Add default value in another place

* Fix error in bash statement

* Replace && on if condition
2019-09-20 15:12:50 -05:00
StasandUmputun 4f6c336f6c Use GitHub actions as ci (#434)
* Add blank.yml

* Fix ci

* Update blank.yml

* Update blank.yml

* Add debug

* Show environments variables

* Add test env for travis

* Update blank.yml

* Update formatting

* Add env variable GIT_BRANCH

* Change setting CI

* Add quotes for env variable value

* change on in ci
2019-09-20 13:51:52 -05:00
UmputunandGitHub 14f267c89e Delete FUNDING.yml 2019-09-12 17:44:12 -05:00
UmputunandGitHub 44021b4dac Update FUNDING.yml 2019-09-12 17:43:43 -05:00
UmputunandGitHub 8eb00d94ad Create FUNDING.yml 2019-09-12 17:42:41 -05:00
Umputun e018fac6c4 revendor 2019-09-05 18:56:43 -05:00
Umputun 7971ebe6f4 ignore deleted comment for activity sorting 2019-09-03 16:39:14 -05:00
Umputun cdc1ae1e85 fix dev mode test 2019-09-01 13:29:34 -05:00
Umputun 9c62453869 fix anon test failed due to email added 2019-09-01 13:24:31 -05:00
Umputun 44ff590183 reword template file description 2019-09-01 13:21:05 -05:00
Umputun 1300464d2d Merge branch 'master' of github.com:umputun/remark 2019-08-27 03:33:43 -05:00
Umputun 22e5c25668 add custom email auth template 2019-08-27 03:33:34 -05:00
Umputun 074edc25ed fix unstable auth hook test 2019-08-27 03:12:11 -05:00
Umputun 8b6452f938 missing delete event in edit request 2019-08-26 22:36:52 -05:00
Umputun 68235164ff add OnEvent event to admin on each update/create/vote/delete 2019-08-26 20:53:04 -05:00
Umputun e9414f7267 fix comment typo 2019-08-26 00:59:26 -05:00
Umputun 106c018ef1 mem store example updated with adm.Enabled support 2019-08-26 00:58:27 -05:00
UmputunandGitHub 3c321f8992 Verified site id (#425)
* WIP: start with aud verification

* adjust rest test for token's site_id remark42

* add tests for non-matching aud

* fix auth hook test with updates limiter

* check siteID with enabled call for static store

* fix site enabled check
2019-08-25 22:00:19 -05:00
Umputun ef7476d3e1 change auth lib to 0.8.2, fixed user's aud promotion 2019-08-24 14:17:35 -05:00
Umputun a6149ae064 add support for avatar store uri 2019-08-24 01:59:24 -05:00
Umputun 141c75401a switch to latest baseimage allowing custom TZ and UID 2019-08-23 17:01:09 -05:00
Umputun 7fbf2cde61 add twitter auth #70 2019-08-23 03:17:31 -05:00
Umputun c5b927904f extend User struct with site_id/aud
updated to 0.8.0 of auth pkgs supporting user-space aud
2019-08-23 03:12:08 -05:00
Umputun 8d109901b8 Merge branch 'master' of github.com:umputun/remark 2019-08-20 00:07:19 -05:00
Umputun 07b16c46dc unescape quotes from user name #415 2019-08-20 00:07:13 -05:00
UmputunandGitHub 2e90b6172b Votes ip (#416)
* change vote params to request

* limit voting for the same ip

* limit same ip vote duration

* add same ip vote check for directions

* wire RestrictVoteIP and duration

* add votes-ip and votes-ip-time description
2019-08-19 14:04:12 -05:00
Umputun 9a5684de71 rename Blocked to better named BlockedUsers 2019-08-19 01:37:56 -05:00
Vyrtsev MikhailandUmputun d6d0109e80 change default post url from window.location.href to window.location.origin + window.location.pathname 2019-08-16 16:33:04 -05:00
Ivan BessarabovandUmputun f77ad01fbf Fixing indent in README.md 2019-08-11 16:01:35 -05:00
UmputunandGitHub 3ec9d0d8b2 Merge pull request #407 from elnardu/patch-1
Small README.md fix
2019-08-05 13:58:05 -05:00
Elnard UtiushevandGitHub d9e08b65a4 Small fix
The table mentions `positive-score` twice (lines 165 and 167)
2019-08-06 00:56:26 +06:00
Umputun 1c6c9d6dcf increase timeouts in unstable tests 2019-08-05 01:28:14 -05:00
UmputunandGitHub ae1f00f3a4 Merge pull request #402 from johnpuddephatt/master
Allow ID of container node to optionally be provided in comments widget config
2019-08-04 11:35:23 -05:00
UmputunandGitHub 0d4ee15df4 Merge pull request #403 from ShoshinNikita/master
Small fix of README.md
2019-08-04 11:35:11 -05:00
UmputunandGitHub 79a1cfca91 Merge pull request #406 from Reeywhaar/master
Fixes
2019-08-04 11:23:46 -05:00
Vyrtsev Mikhail a51b00760a fix: Comment reply/edit box resets if it gets out of view 2019-08-04 18:11:32 +03:00
Vyrtsev Mikhail 437c805e96 change scss import order (after component) 2019-08-04 17:40:02 +03:00
Vyrtsev Mikhail e92cce20c0 replace map with WeakMap 2019-08-04 17:22:41 +03:00
Shoshin NikitaandGitHub c5b62fa34e Small fix of README.md
Fix description of "image.type"
2019-08-02 00:57:35 +03:00
John Puddephatt 33c235ed3b remove stray space 2019-08-01 20:39:54 +01:00
John Puddephatt 95168041b9 allow ID of container node to optionally be provided in remark_config 2019-08-01 20:08:06 +01:00
UmputunandGitHub f0750c2433 Merge pull request #401 from umputun/rpc-example
RPC example
2019-08-01 02:08:26 -05:00
Umputun 5b173a05df add dockerfile and compose 2019-08-01 01:49:58 -05:00
Umputun 796c4f45eb fix rpc deps 2019-08-01 01:49:38 -05:00
Umputun 17baa0cff6 fix admin.admins call 2019-08-01 01:49:09 -05:00
Umputun da671c005c prep compose and Dockerfile 2019-07-31 23:24:48 -05:00
Umputun 3d883ba4bb clean rpc leftovers 2019-07-31 22:49:50 -05:00
Umputun 9ab18dfe1a fix zero time check 2019-07-31 18:47:28 -05:00
Umputun 3c21cba9c1 switch core to go-pkgz/jrpc 2019-07-31 18:38:53 -05:00
Umputun e84a155ef5 switch example to go-pkgz/jrpc 2019-07-31 18:34:32 -05:00
Umputun fcf175c905 Merge branch 'master' into rpc-example 2019-07-30 00:44:14 -05:00
Christian OliffandUmputun 159f4d4faf Fix GitHub capitalization 2019-07-29 13:48:32 -05:00
Vyrtsev MikhailandUmputun b6135510ab fix lint warnings 2019-07-29 13:47:35 -05:00
Vyrtsev MikhailandUmputun 998e6a9a27 update demo page to look better on mobile 2019-07-29 13:47:35 -05:00
Vyrtsev MikhailandUmputun 1aaad59afb add intersection-observer polyfill 2019-07-29 13:47:35 -05:00
Vyrtsev MikhailandUmputun cbf6abb201 make webpack exclude work 2019-07-29 13:47:35 -05:00
Vyrtsev MikhailandUmputun 13e4f78762 add es5 compatibility check on post build 2019-07-29 13:47:35 -05:00
Vyrtsev MikhailandUmputun 0f05b5097a rename action 2019-07-29 13:47:35 -05:00
Vyrtsev MikhailandUmputun cb1b42a2e0 render Comment with InView 2019-07-29 13:47:35 -05:00
Vyrtsev MikhailandUmputun a36733a2e3 add InView component which tracks element intersection with view 2019-07-29 13:47:35 -05:00
Vyrtsev MikhailandUmputun b4f083a051 remove redundant mutation 2019-07-29 13:47:35 -05:00
Vyrtsev MikhailandUmputun 0f78bbf918 update package-lock.json after rebase 2019-07-29 13:47:35 -05:00
Vyrtsev MikhailandUmputun 49a16f9071 clear interval on component unmount 2019-07-29 13:47:35 -05:00
Vyrtsev MikhailandUmputun 8d748b6c65 reorganize comments reducers 2019-07-29 13:47:35 -05:00
Vyrtsev MikhailandUmputun 48de9e15a3 add shallow compare util 2019-07-29 13:47:35 -05:00
Vyrtsev MikhailandUmputun e0ba7564f1 add bench util 2019-07-29 13:47:35 -05:00
Vyrtsev MikhailandUmputun a9e4ce368d add cmpRef util 2019-07-29 13:47:35 -05:00
Vyrtsev MikhailandUmputun 0d6ae9edd4 add lodash 2019-07-29 13:47:35 -05:00
Vyrtsev MikhailandUmputun f34ab737d8 remove unused action 2019-07-29 13:47:35 -05:00
Vyrtsev MikhailandUmputun 734eb0ebf3 replace duplicating babel config with common excludes getter 2019-07-29 13:47:35 -05:00
Vyrtsev MikhailandUmputun 1d01a21480 replace require with import 2019-07-29 13:47:35 -05:00
Vyrtsev MikhailandUmputun 7fd3c563c7 add focus style to sort select 2019-07-26 18:02:11 -05:00
Vyrtsev MikhailandUmputun 82d1969393 remove unused test renderer 2019-07-26 18:02:11 -05:00
Vyrtsev MikhailandUmputun e1e1fdf5cf Comment test: replace inhouse test renderer with enzyme 2019-07-26 18:02:11 -05:00
Vyrtsev MikhailandUmputun a17204f85f Preloader test: replace inhouse test renderer with enzyme 2019-07-26 18:02:11 -05:00
Vyrtsev MikhailandUmputun 9da826219f AuthPanel test: replace inhouse test renderer with enzyme 2019-07-26 18:02:11 -05:00
Vyrtsev MikhailandUmputun 187674b54e remove duplicate checks in test 2019-07-26 18:02:11 -05:00
Vyrtsev MikhailandUmputun 0493ece73e fix auth panel providers sorting 2019-07-26 15:54:39 -05:00
Vyrtsev MikhailandUmputun b69cd08bf0 fix auth panel columns vertical alignment 2019-07-26 15:18:12 -05:00
Umputun f4bc6555dd Merge branch 'master' of github.com:umputun/remark 2019-07-26 12:53:18 -05:00
Umputun 2bf4ad80da add multiple auth methods to dev compose 2019-07-26 12:53:07 -05:00
Umputun 1ed0139304 fix screenshots image link 2019-07-26 11:12:20 -05:00
Umputun 86cefec13b add link to demo and screenshots to readme 2019-07-26 11:11:29 -05:00
Umputun 2d92266b60 add delete comment screenshot 2019-07-26 10:52:17 -05:00
Umputun 8b9a00ee0c add screenshots 2019-07-26 10:42:20 -05:00
Misha VyrtsevandUmputun 3b4628d0bd Auth login design (#390)
* break auth panel render into submethods

* move var definition

* break renderUnathorized into submethods

* hide login providers behind dropdown if they are exceed length of 3

* amend dropdown to behave nicely being placed in another dropdown

* add style to providers enclosed in dropdown

* add provider reducer and actions

* add provider save/restore to app flow

* place last login provider first in providers list

* infer StoreState from combineReducers return type

* move collapsed threads retoration to action

* fix: provider lost in other

* add dynamic threshold depending on window width

* fix & add tests
2019-07-25 22:54:59 -05:00
UmputunandGitHub eb3589869d Find since (#388)
* support since param in bolt find for post
* add find with since to store service
* add since param to find request, plain only
2019-07-25 13:58:22 -05:00
Misha VyrtsevandUmputun 5431f80e84 fix unblocking after page refresh stalled (#384) 2019-07-23 16:45:52 -05:00
Misha VyrtsevandUmputun 4d7699ec5e Fix blocking (#381)
* fix blocked user appears unblocked after refresh

* modify comment rendering to temporary blocking
2019-07-22 19:05:02 -05:00
Misha VyrtsevandUmputun 7103dbb589 fix sort been stalled (#379) 2019-07-22 16:06:53 -05:00
Umputun 240796b2d6 update doc with the current /config response document 2019-07-21 12:26:15 -05:00
Umputun ad3b7c695a expose emoji_enabled flag in /config response #378 2019-07-21 12:24:56 -05:00
Umputun 8759facb27 adjust alterComment test for #377 2019-07-21 11:35:45 -05:00
Umputun a60072564e temporary blocking doesn't need to delete/hide comments #377 2019-07-21 11:30:11 -05:00
Konstantin KrivleniaandUmputun 2f7c4e7e03 Add a button for image upload (#372)
* #371 add image icon for toolbar

* #371 add file upload handler

* #371 upload image from clipboard

* #371 change title and decrease size for upload button

* #371 allow upload few files

* #371 prevent paste text after file was uploaded in firefox
2019-07-16 10:27:44 -05:00
Vyrtsev MikhailandUmputun 17f61ee339 fix wrong className 2019-07-15 19:58:52 -05:00
Vyrtsev MikhailandUmputun 8cd5533aed fix lint errors 2019-07-15 17:57:43 -05:00
Vyrtsev MikhailandUmputun 9e726d5769 npm audit fix 2019-07-15 17:57:43 -05:00
Vyrtsev MikhailandUmputun 9863b2ddf0 update focus-visible 2019-07-15 17:57:43 -05:00
Vyrtsev MikhailandUmputun 461d8fbac5 update css-loader 2019-07-15 17:57:43 -05:00
Vyrtsev MikhailandUmputun d5e18d4290 update lint-staged 2019-07-15 17:57:43 -05:00
Vyrtsev MikhailandUmputun 343ea07d91 update husky 2019-07-15 17:57:43 -05:00
Vyrtsev MikhailandUmputun 387b6293ee update minor deps 2019-07-15 17:57:43 -05:00
Umputun d3a5151ab4 switch rss parent to blockquote only 2019-07-14 11:57:03 -05:00
Umputun a2aaf48c84 Merge branch 'master' of github.com:umputun/remark 2019-07-14 11:52:37 -05:00
Umputun 00cdfa2aa3 add missing help commands to readme 2019-07-14 11:52:28 -05:00
Umputun a4b24c1edb add blockquote inside summary, some rss readers seems to ignore summary 2019-07-14 00:15:26 -05:00
Umputun dfaf4d13d3 add parent summary to rss description #339 2019-07-14 00:00:02 -05:00
Umputun dda4f87db2 support optional emoji translation 2019-07-13 14:04:03 -05:00
Umputun 9585bdff34 lint: multiple warnings, mostly in tests 2019-07-13 13:40:41 -05:00
Andrey BlinovandUmputun accaae0bbd Change label in editing form from Edit to Save 2019-07-10 09:11:02 -05:00
Vyrtsev MikhailandUmputun e9e4b5476b change type of email field to email 2019-07-07 17:17:01 -05:00
Vyrtsev MikhailandUmputun 947ddd4da4 refine droppown content positioning 2019-07-07 17:17:01 -05:00
Vyrtsev MikhailandUmputun 8132819e1d add window expanding on dropdown open 2019-07-07 17:17:01 -05:00
Vyrtsev MikhailandUmputun 67cfc422b4 add onOpen/onClose props 2019-07-07 17:17:01 -05:00
Vyrtsev MikhailandUmputun cf84ad3cf5 unwrap if 2019-07-07 17:17:01 -05:00
Vyrtsev MikhailandUmputun 19efaf7e96 increase margins in email auth popup 2019-07-06 19:24:24 -05:00
Umputun 088294d36e switch to go-pkgz/auth v0.7.2 with limited verification token 2019-07-06 18:56:49 -05:00
Umputun cbbd8a7ae0 update email template 2019-07-06 18:54:28 -05:00
Umputun 820f3129fb integrate email template #366 2019-07-06 17:19:34 -05:00
Vyrtsev MikhailandUmputun fefd225b1a replace token input with textarea 2019-07-06 13:58:58 -05:00
Vyrtsev MikhailandUmputun 6fd803e891 fix undefined ref 2019-07-06 13:58:58 -05:00
Vyrtsev MikhailandUmputun 8ea5936ab0 reset email form on success 2019-07-06 13:58:58 -05:00
Vyrtsev MikhailandUmputun 29322d920a add input focus on email auth form popup 2019-07-06 13:58:58 -05:00
Vyrtsev MikhailandUmputun 18f41d21d7 add email auth ui test 2019-07-06 13:58:58 -05:00
Vyrtsev MikhailandUmputun 3d62302bf0 add enzyme test renderer package 2019-07-06 13:58:58 -05:00
Vyrtsev MikhailandUmputun 23691fb979 add jest-extended package 2019-07-06 13:58:58 -05:00
Vyrtsev MikhailandUmputun 0ddb510e0e add constants mocking 2019-07-06 13:58:58 -05:00
Vyrtsev MikhailandUmputun 3c2977f98c add redux-mock-store package 2019-07-06 13:58:58 -05:00
Vyrtsev MikhailandUmputun fedfe92781 add mockHeaders test util 2019-07-06 13:58:58 -05:00
Vyrtsev MikhailandUmputun 81bf3ef35c add fetch-mock package 2019-07-06 13:58:58 -05:00
Vyrtsev MikhailandUmputun 84a3e6db7d add aggregateTimeout to webpack watch options 2019-07-06 13:58:58 -05:00
Vyrtsev MikhailandUmputun ac60bb8750 add email auth ui 2019-07-06 13:58:58 -05:00
Vyrtsev MikhailandUmputun c3253bec85 fix: DropDown doesn't remove listeners on unmount 2019-07-06 13:58:58 -05:00
Vyrtsev MikhailandUmputun c9ced7bd58 enable email auth in frontend dev compose 2019-07-06 13:58:58 -05:00
Umputun 52ecb53d31 resort imports 2019-07-06 13:36:34 -05:00
Umputun b2de952f38 move email auth before anon 2019-07-06 13:35:35 -05:00
Umputun f4335105c2 allow gravatars 2019-07-06 12:45:33 -05:00
Umputun ed3d0a314f Merge branch 'master' into email 2019-07-06 12:43:31 -05:00
Umputun dfbeb48a45 . 2019-07-04 20:21:57 -05:00
Umputun 1094d365a9 revendor with go-pkgz/auth v0.7.1 2019-07-02 22:42:26 -05:00
Umputun 24d685c4de add email notificator with auth sender 2019-07-02 22:02:36 -05:00
Umputun af67ec0fc8 revendor bf to v 2.0.1 2019-07-01 14:04:01 -05:00
Vyrtsev MikhailandUmputun e63ccdd18d fix: controls not visible on safari because of it doesn't have hover 2019-06-30 14:19:54 -05:00
Umputun b98d7ef1ef Merge remote-tracking branch 'origin/master' 2019-06-30 12:45:20 -05:00
Umputun 8ddfa56b82 add unescape table processing after md parsing #354 2019-06-30 12:45:15 -05:00
Vyrtsev MikhailandUmputun f70fa5f72e add spellcheck attribute 2019-06-30 12:31:39 -05:00
Umputun c224676ccd enable Smartypants for md rendering 2019-06-29 21:58:50 -05:00
Umputun 49593a3327 wip: memory rpc example 2019-06-29 21:17:34 -05:00
Vyrtsev MikhailandUmputun 90103a5fe5 make levels 5+ unindented 2019-06-29 21:00:31 -05:00
Vyrtsev MikhailandUmputun 75c294a692 move thread related styles to thread component 2019-06-29 21:00:31 -05:00
Vyrtsev MikhailandUmputun 12793c1110 make level5+ lines more dimmed 2019-06-29 21:00:31 -05:00
Vyrtsev MikhailandUmputun d7ec92ee39 make thread lines dotted 2019-06-29 21:00:31 -05:00
Vyrtsev MikhailandUmputun 1ac0421bb8 move selector for dark theme according to convent 2019-06-29 21:00:31 -05:00
Konstantin LandUmputun e2237e7396 show threads 2019-06-29 19:08:52 -05:00
Umputun b95179303b enforce empty [] response on flag's list instead of null #356 2019-06-29 12:01:55 -05:00
Vyrtsev MikhailandUmputun e5cc5e677b fix: settings widget fails because it expects list but gets null 2019-06-29 11:13:05 -05:00
Umputun 60d9f207ae only permanent blocking delete all comments 2019-06-28 11:58:09 -05:00
Umputun c09eaa6e00 rename remote to rpc 2019-06-27 14:29:46 -05:00
Umputun 63a555126e add test for app with remote plugins 2019-06-25 20:06:30 -05:00
Umputun 06bac880cb lint: more test warns 2019-06-25 20:06:30 -05:00
Umputun f6e69e86bb lint: test warnings 2019-06-25 20:06:30 -05:00
Umputun 5b79f7c435 clean drone build 2019-06-25 20:06:30 -05:00
Umputun 01bdf4d839 clear mongo leftovers 2019-06-25 20:06:30 -05:00
Umputun 82ce19fd3c larger limit for remote srv throttler 2019-06-25 20:06:30 -05:00
Umputun 348bede009 fix recreation of bdb in delete 2019-06-25 20:06:30 -05:00
Umputun 48fcb0a5d9 add remote selection to store and admin 2019-06-25 20:06:30 -05:00
Umputun 126d2d93da adjust remote tests, remove legacy mongo tests 2019-06-25 20:06:30 -05:00
Umputun db6c387fe9 support remote server group handler 2019-06-25 20:06:30 -05:00
Umputun f30937c55e fix empty (no args) remote calls 2019-06-25 20:06:30 -05:00
Umputun b30492556f add remote implementation of admin store 2019-06-25 20:06:30 -05:00
Umputun 988206bed4 change remote interface to struct requests 2019-06-25 20:06:30 -05:00
Umputun bab09effa2 error responses of json api with json, time out on remote server close 2019-06-25 20:06:30 -05:00
Umputun e307722901 switch list flag to struct param 2019-06-25 20:06:30 -05:00
Umputun f2cecafb27 fix remote client tests with id 2019-06-25 20:06:30 -05:00
Umputun 9f875394e8 remote server covered with tests 2019-06-25 20:06:30 -05:00
Umputun c1d14082ca implement DeleteUser soft mode to allow mapping for blocked users #341 2019-06-25 20:06:30 -05:00
Umputun 375e4a6dec initial ver of remote server 2019-06-25 20:06:30 -05:00
Umputun d8a2bb2f7b split remote to common protocol package and engine implemetation 2019-06-25 20:06:30 -05:00
Umputun c2b56eae30 remote client adjusted to new engine flavour 2019-06-25 20:06:30 -05:00
Umputun 3aba348e87 make new engine primary, rename package 2019-06-25 20:06:30 -05:00
Umputun 92d9b7703d restore all service coverage with new engine 2019-06-25 20:06:30 -05:00
Umputun 690c0df9b8 wip: engine2 passing test with updates service 2019-06-25 20:06:30 -05:00
Umputun 8d7c486e96 eliminate write timeout 2019-06-18 18:29:04 -05:00
Umputun 7819c9ec4b add keep-alive header to streamer 2019-06-18 18:14:58 -05:00
Dmitry VerkhoturovandUmputun f1b03b4bc6 fix typos, add missing configuration options to readme 2019-06-18 13:14:45 -05:00
Umputun 896f67ecdc add info and SSE streams and response example to readme 2019-06-18 12:58:50 -05:00
Umputun 64bc99fc8d fix incorrect default since for last comments stream 2019-06-18 12:38:51 -05:00
Umputun 445aec860e add support of "since" param (unix msec) #336 #253 2019-06-18 12:14:27 -05:00
Umputun e1bec310cf convert streams to SSE #336 2019-06-18 12:01:37 -05:00
Vyrtsev MikhailandUmputun dad51f052f fix sorting change makes hidden users to appear 2019-06-16 17:35:56 -05:00
Misha VyrtsevandUmputun 556b053ae7 Fix ui errors (preview and image upload) (#347)
* fix broken preview

* fix broken image upload
2019-06-16 16:02:13 -05:00
Misha VyrtsevandUmputun a302bdbe5d Hide user comments (#334)
* add ability to hide user via localstorage

* remove closures to avoid reconcilation

* add actionBinder utilities

* add user hide/show feature

* remove unused type

* fix css for settings user id

* remove pointless confirmation for hide/show user in settings

* add redux-dev-tools support
2019-06-10 19:20:39 -05:00
Umputun 133f5fc3f2 image removal from staging increased to 5*EditTTL
this is needed in case if user worked on the comment for long time
until submitting in order to avoid false-positive cleanup from the
staging
2019-06-08 21:54:00 -05:00
UmputunandGitHub 3a266111fa Merge pull request #338 from Reeywhaar/update-deps-2019-06
Update deps 2019 06
2019-06-05 16:33:59 -05:00
Vyrtsev Mikhail 9140e87152 fix lint-staged ignoring warnings 2019-06-05 23:36:24 +03:00
Vyrtsev Mikhail c8d39a9286 update frontend deps 2019-06-05 23:28:57 +03:00
Umputun 20764b7b4e enclose streamer in rest's struct 2019-06-05 02:14:34 -05:00
Umputun a8b2dee85c update deps 2019-06-04 23:32:23 -05:00
Umputun 342b4052a9 add error checks to pub rest test 2019-06-04 23:20:40 -05:00
Umputun fe46509e8e generalize stream api, move to separate struct & file 2019-06-04 22:06:33 -05:00
Umputun e1bd0e5104 eliminate first update on info stream on cold cache 2019-06-04 21:02:28 -05:00
Umputun 0f2298d193 externalize all stream params 2019-06-03 22:48:28 -05:00
Umputun f4d346c25a limit max number of active streams 2019-06-03 19:44:39 -05:00
Umputun 458eb27d8c add stream api for /last 2019-06-03 16:44:46 -05:00
Umputun d1cf9aecb3 make stream refresh slower, every 5s 2019-06-03 13:02:12 -05:00
Umputun 994a97b13f generalize event streams 2019-06-03 12:41:04 -05:00
UmputunandGitHub ca083f451e Info stream (#336)
* add /stream/info as a cheap way to subscribe to comment updates #253

* add check for lastTS change to allow proper info streams in no-cache mode

* check write error in info stream and terminate

* flaky info stream test

* add stream info to readme

* separate timeout middleware foe each route's group

* debug info on stream close

* fix test for streams

* stream timeout on inactivity only

* throttle streams to 500

* restore common throttle
2019-06-03 11:34:00 -05:00
Umputun bccf716a8d support view=user #323 2019-05-31 16:09:40 -05:00
Umputun bddb287e68 copy licence and readme to the curr directory #335 2019-05-26 22:03:27 -05:00
Umputun 2d38018722 remove commented old rss code 2019-05-19 13:24:54 -05:00
Umputun 350cca894d move reply retrieval to service, fix incorrect RSS title for reply 2019-05-19 12:56:47 -05:00
Umputun 6de89888c3 make controllers assignment explicit 2019-05-18 12:57:49 -05:00
Umputun 0142a65441 consolidate engine interfaces 2019-05-18 01:48:11 -05:00
Umputun e8e335d104 convert rss controllers to object 2019-05-17 22:05:53 -05:00
Umputun 255b9ad9c8 move config controller to rest 2019-05-17 21:50:23 -05:00
Umputun 2b8a2a082b move routes group setup to func 2019-05-17 21:50:06 -05:00
Umputun 6e12122e44 blocking also delete comments #332 2019-05-17 13:57:52 -05:00
UmputunandGitHub 7b8bc80468 Merge pull request #331 from umputun/alter
Alter
2019-05-17 02:50:32 -05:00
Umputun 00166fe57c add test to alter comment 2019-05-17 02:40:38 -05:00
Umputun cc37fbef13 switch admin rest to interface with subset of store api 2019-05-16 23:16:08 -05:00
Umputun 622a0bdacf move all private rest controllers to private struct, restrict store 2019-05-16 23:07:26 -05:00
Umputun a80c62517f wrap public rest into struct with safe/read-only interface as store 2019-05-16 21:41:25 -05:00
Umputun b9c80e64d0 add engine mock 2019-05-16 21:06:48 -05:00
Umputun 0590246112 move comment alter functionality from rest and admin to store service 2019-05-16 20:49:07 -05:00
UmputunandGitHub 5218f3f37c Support "since" parameter in "/last" query (#330)
* strip \n and spaces from retrieved title

* add "since" support to last storage and rest #329

* add info about since to readme
2019-05-16 13:58:37 -05:00
Misha VyrtsevandUmputun 61e594786c Define remark host in runtime (#325)
* convert closest-polyfill to ts

* make remark respect host property in client setup
2019-05-13 10:51:17 -05:00
Misha VyrtsevandUmputun 53f2f82e05 Fix "delete me" feature (#326)
* wip: fix "delete me" feature

* minor fixes
2019-05-13 09:52:28 -05:00
Umputun 7d01165d1e overwrite default "token" for auth ta avoid conflict with deleteme #324 2019-05-12 21:24:59 -05:00
Umputun 3d7f08457a mod files after revendor 2019-05-12 21:23:41 -05:00
Umputun 489b8a36e3 revendor with 0.5.2 of auth, with ability to redefine token query 2019-05-12 21:23:13 -05:00
Umputun 4cf9bf2b63 add deleteme rest example 2019-05-12 13:26:47 -05:00
UmputunandGitHub 4adbca22b2 Refactor rest (#320)
* fresh vendor

* enforce vendor flag with env

* move tree to service package

* move controllers routing to rest.go, remove separate subrouters

* cleanup/formatting

* missing vendor packages

* move rss, img proxy, robots and getstarted to top-level routes

* simplify logger middleware usage

* randomize server test ports

* check for non-admin in admin tests

* lint: minor warns

* admin test to jwt admin
2019-05-07 01:54:19 -05:00
Umputun 1616f43f0c clean empty staging directory 2019-05-04 12:39:24 -05:00
Umputun cc49ee0946 fix uninitialized imageapi pattern string 2019-05-04 12:10:39 -05:00
Umputun a9a368e02d lint: typos 2019-04-28 13:13:12 -05:00
UmputunandGitHub 02b27d2c12 Merge pull request #317 from Reeywhaar/mv-web-to-frontend
Rename "web"  to "frontend"
2019-04-28 12:07:50 -05:00
Vyrtsev Mikhail 24860e124e mv web -> frontend 2019-04-28 19:27:36 +03:00
UmputunandGitHub f7d2bf976e Merge pull request #316 from Reeywhaar/fix-comment-input-top-margin
Fix comment input top margin in editing or replying mode
2019-04-21 16:03:22 -05:00
Vyrtsev Mikhail b7ece816a4 fix comment input top margin in editing or replying mode 2019-04-21 23:13:30 +03:00
UmputunandGitHub f4b0e0ca5f Merge pull request #315 from Reeywhaar/handle-error-http-code
Handle specific rest status codes in fetcher
2019-04-21 15:04:32 -05:00
Vyrtsev Mikhail a37761fe9e handle specific rest status codes in fetcher 2019-04-21 22:52:44 +03:00
Umputun 6b31741fa3 fix cache enabled for pictures 2019-04-21 01:26:51 -05:00
Umputun ace1cbc278 allow caching for /img api calls 2019-04-20 19:06:05 -05:00
UmputunandGitHub 1db544a54f Merge pull request #313 from Reeywhaar/add-ie11-support
Restore ie11 support
2019-04-20 19:02:32 -05:00
Umputun 0156c5ac97 add no-cache middleware to api calls #313 2019-04-20 18:49:42 -05:00
Vyrtsev Mikhail 38db4a5511 restore ie11 support 2019-04-21 00:47:26 +03:00
UmputunandGitHub ee21c34c12 Merge pull request #312 from Reeywhaar/fix-useless-comment-collapse
Fix useless comment collapse
2019-04-20 14:40:58 -05:00
Vyrtsev Mikhail 22006bee81 add logError: boolean param to fetcher
Case for it is getUser api method. When user is not authenticated, api returns 403 error which pops up in console and have no meaning
2019-04-20 21:24:00 +03:00
Vyrtsev Mikhail d1420286dd fix comment collapsing button works on second click on useless comment 2019-04-20 21:13:24 +03:00
Vyrtsev Mikhail 38f9fd855c fix #310: useless comment remains uncollapsed 2019-04-20 20:58:58 +03:00
Vyrtsev Mikhail 7e84b356a2 fix tests 2019-04-20 20:58:58 +03:00
Vyrtsev Mikhail 68294fd3d2 forbid dynamic type acquisition for typescript
was useful during refactoring but makes no sense now
2019-04-20 20:47:37 +03:00
UmputunandGitHub c078bc31a6 Merge pull request #308 from Reeywhaar/ui-fixes
Ui fixes
2019-04-20 12:08:12 -05:00
Vyrtsev Mikhail 0a77baff89 set eslint to fail on warnings 2019-04-20 18:54:23 +03:00
Vyrtsev Mikhail fa63b4bc70 disable eslint warnings 2019-04-20 18:54:22 +03:00
Vyrtsev Mikhail 34772fa1b6 deny anonymous image upload 2019-04-20 18:54:21 +03:00
Vyrtsev Mikhail 1dd97d42a1 hide unexpected error message from ui, still can be seen in console 2019-04-20 18:54:20 +03:00
Umputun 619e9fec3b clean image tests leftovers from tmp 2019-04-17 13:38:29 -05:00
Umputun 15a8fb946d lint: err shadowing 2019-04-17 13:36:16 -05:00
Umputun c46a21af72 simplify image Save logic by switching to loaded []byte
note: resize loaded to memory anyway
2019-04-17 13:35:41 -05:00
UmputunandGitHub 68b892f5e9 Image resize (#311)
* resize uploaded images #309

* fix temp files created in current dire after image test

* update mod

* missing image test files

* fix failed goroutine test on image-resize submit

* switch to mockery

* missing in mock change

* resize by both ResizeWidth and ResizeHeight

* add IMAGE_RESIZE params to readme

* add latest features to the description

* lint: missing file close in image test
2019-04-17 02:53:35 -05:00
UmputunandGitHub 24606e46bf Merge pull request #307 from Reeywhaar/image-upload-ui
Image upload ui
2019-04-14 22:58:39 -05:00
UmputunandGitHub 0585c325a4 Merge pull request #305 from Mavrin/master
#299 add toolbar for markdown formatting
2019-04-14 19:33:03 -05:00
konstantin krivlenia c7824e603d #299 increase height textarea 2019-04-14 11:19:03 +03:00
konstantin krivlenia 5f86519aad #299 make it working in IE11 2019-04-14 11:18:17 +03:00
konstantin krivlenia abeca3cc23 #299 add list format buttons 2019-04-14 11:18:16 +03:00
konstantin krivlenia b38bbf4f00 #299 add link format button 2019-04-14 11:18:16 +03:00
konstantin krivlenia 8fb147e8b4 #299 add code format button 2019-04-14 11:18:15 +03:00
konstantin krivlenia b13cc1cae7 #299 add quote format button 2019-04-14 11:18:14 +03:00
konstantin krivlenia 2b68c8e8d3 #299 add italic format button 2019-04-14 11:18:14 +03:00
konstantin krivlenia 4c5363835f #299 add header format button 2019-04-14 11:18:13 +03:00
konstantin krivlenia 8e19729718 #299 add bold toolbar for formatting 2019-04-14 11:18:13 +03:00
Vyrtsev Mikhail 1aea8be941 add image upload drag and drop 2019-04-14 00:06:42 +03:00
Umputun 2a53a54a78 allow image/webp
https://github.com/umputun/remark/pull/307#issuecomment-482873793
2019-04-13 14:52:21 -05:00
Umputun 6bdbcb8351 lint: missing check for header load error 2019-04-13 14:48:47 -05:00
Umputun 3debfff93d another test rejection due to image type check 2019-04-13 14:44:36 -05:00
Umputun ac261940d7 fix rest test with rejected non-pic 2019-04-13 14:39:55 -05:00
Umputun 44bd512de7 miss vendor for xid package 2019-04-13 14:28:34 -05:00
Umputun d72154b3c8 make shorted image uids, check image type and restrict to png, gif and jpeg only #285 2019-04-13 14:27:40 -05:00
Umputun e182d34c2c update readme with info about max_image_size 2019-04-12 10:20:14 -05:00
Umputun 3d61be8c44 make AuthHooks test faster 2019-04-12 10:14:17 -05:00
Umputun c52e5b3920 expose max image size via config rest #307 2019-04-12 10:13:14 -05:00
UmputunandGitHub db562ab8ab Merge pull request #306 from Reeywhaar/master
Minor fixes
2019-04-11 22:45:54 -05:00
Vyrtsev Mikhail bed8d28c5a update deps 2019-04-12 01:52:53 +03:00
Vyrtsev Mikhail 69d5a5e4a9 add admin indicator for pinned comment 2019-04-12 01:44:20 +03:00
UmputunandGitHub 25cf349dec Merge pull request #304 from Reeywhaar/fixes
Fixes
2019-04-10 17:30:23 -05:00
Vyrtsev Mikhail dae3300125 fix comment admin indicator 2019-04-11 01:17:29 +03:00
Vyrtsev Mikhail 1cd12932ca additional typecheck 2019-04-11 00:52:27 +03:00
UmputunandGitHub 1c418eed4c Merge pull request #303 from umputun/vote
Vote
2019-04-10 00:41:27 -05:00
UmputunandGitHub 67a5ed6669 Merge pull request #300 from Reeywhaar/vote
support new voting api in ui
2019-04-10 00:39:21 -05:00
Vyrtsev Mikhail 3246b66214 support new voting api in ui 2019-04-10 02:49:45 +03:00
Vyrtsev Mikhail 6f67cbc30d remove webpack log spam 2019-04-10 02:49:45 +03:00
Vyrtsev Mikhail eca230b87f fix tabs 2019-04-10 02:40:17 +03:00
Vyrtsev Mikhail bed336cfb1 update readme 2019-04-10 02:25:47 +03:00
Umputun 02be2144d5 Merge branch 'master' into vote 2019-04-09 14:21:05 -05:00
Umputun af199f46ee make main test slower for unusually slow travis storage init time(?) for master 2019-04-09 14:20:12 -05:00
Umputun 46244b855a make main test slower for unusually slow travis storage init time(?) 2019-04-09 14:17:08 -05:00
UmputunandGitHub 08bc105da9 Merge pull request #295 from Reeywhaar/feature/front-anon-login
Add frontend support for anonymous user login
2019-04-09 14:04:37 -05:00
Umputun cecf147fd0 add tests for URLKeys 2019-04-09 00:10:21 -05:00
Umputun 22d787de58 user comment cached with user_id as well 2019-04-07 18:38:06 -05:00
Umputun 078bedd8e8 separate caching key URLKeyWithUser for find only 2019-04-07 18:34:20 -05:00
Umputun dc5ceeee12 cache find with userID to prevent leaking (and incorrect) vote status 2019-04-07 18:20:05 -05:00
Umputun 128ca53725 more voting tests 2019-04-07 15:58:25 -05:00
Umputun 4845bf357a restore rest setter for vote info of the current user 2019-04-07 14:12:40 -05:00
Umputun 2e4dfe3891 move vote setter to service level 2019-04-07 14:05:19 -05:00
Umputun 446cb486a0 extend test fo votes 2019-04-07 13:47:22 -05:00
Umputun 556e330af2 merge current master 2019-04-07 13:32:12 -05:00
UmputunandGitHub 6913f3b164 Merge pull request #301 from ns-cweber/patch-1
Fix formatting in README
2019-04-07 13:13:47 -05:00
ns-cweberandGitHub db2c1c0c35 Fix formatting in README 2019-04-07 10:19:35 -05:00
Umputun 3662cd0ab5 lint: convert string to string 2019-04-05 16:49:42 -05:00
Umputun 47414a252f increase main test time to address crazy CI (travis) slowness in making buckets 2019-04-05 16:47:44 -05:00
Umputun e1d4c9ef23 add progress debug for bolt creation 2019-04-05 16:42:36 -05:00
Umputun bf17b31eaa add caller func to debug logging 2019-04-05 16:24:58 -05:00
Umputun 5d052f8eff remove pkg info for prod logging, add braces 2019-04-05 16:24:01 -05:00
Umputun 50e437c22c switch test port 2019-04-04 22:51:41 -05:00
Umputun ad3430883f make temp for integration tests 2019-04-04 13:54:23 -05:00
Umputun 0de9a7b0d6 set longer time to start integration server test 2019-04-04 09:37:28 -05:00
Umputun 70134578e1 switch to chi v4 2019-04-04 02:17:12 -05:00
Umputun 0f433722da disable picture upload for anonymous 2019-04-04 01:53:47 -05:00
Umputun 45ae2e6f13 revendor with lgr 0.6.1 2019-03-31 16:21:32 -05:00
Umputun 29dc368ea8 revendor with fresh lgr, add logger to http server 2019-03-31 13:45:44 -05:00
Umputun 56c5cd64bc lint: more test warnings 2019-03-26 00:04:59 -05:00
Umputun 6ef88bf375 lint: tests warning 2019-03-25 23:57:27 -05:00
Umputun 4f73ddc40c move error parsing for rest to separate func 2019-03-25 23:31:02 -05:00
UmputunandGitHub c21ce7d26f Merge pull request #296 from umputun/image
Image storage
2019-03-25 21:20:59 -05:00
Umputun cf09ea27eb remove mock from test coverage 2019-03-25 21:10:49 -05:00
Umputun 38da69ea49 simplify counts hashing key 2019-03-25 20:58:57 -05:00
Umputun 7a948266a3 test more image types 2019-03-25 19:54:16 -05:00
Umputun 542a1e9957 missing vendor flag in build cmd 2019-03-25 18:05:14 -05:00
Umputun 1e06c372d9 fix tests for votes default value 2019-03-25 16:28:22 -05:00
Umputun 3b5a1a62de add vote for the current user, hide list of other votes #297 2019-03-25 16:14:10 -05:00
Umputun 7f796d5ed9 add image related docs 2019-03-25 12:02:06 -05:00
Umputun 4f67afffb1 adjust partition tests 2019-03-25 02:45:59 -05:00
Umputun 2a64a71eb8 lint: missing err check in test 2019-03-25 01:15:51 -05:00
Vyrtsev Mikhail 24ea8805bb implement ui support for anonymous login https://github.com/umputun/remark/issues/279 2019-03-25 01:25:47 +03:00
Vyrtsev Mikhail 13f914f58c update npm deps 2019-03-25 01:09:49 +03:00
Umputun 65738bc158 Merge remote-tracking branch 'remotes/origin/master' into image 2019-03-24 16:39:38 -05:00
Umputun 2a03afddc6 update auth to v0.5.0 2019-03-24 16:35:23 -05:00
Umputun 02da07925c merge master 2019-03-24 16:28:12 -05:00
Umputun bba958a813 Merge remote-tracking branch 'remotes/origin/master' into image 2019-03-24 16:27:20 -05:00
Umputun b534991770 trigger drone 2019-03-24 15:31:15 -05:00
Umputun e99153ee7b trigger drone 2019-03-24 15:27:23 -05:00
Umputun 96c7e16ff7 try to trigger drone 2019-03-24 15:21:29 -05:00
Umputun 95e634108d switch drone build to 1.12 2019-03-24 15:13:40 -05:00
Misha VyrtsevandAleksei Gurianov 5809419bce Refactor frontend to typescript (#281) 2019-03-24 22:53:55 +04:00
Umputun 2c0cd1dec7 add test for submitImages 2019-03-24 03:16:37 -05:00
Umputun ded10dde6c integration test for create comment with images 2019-03-24 00:23:46 -05:00
Umputun 8aa24341c3 make image ids extraction safe for pos-edits, delays comment parsing 2019-03-23 23:30:53 -05:00
Umputun 42767b94a4 missed vendor for gomock 2019-03-23 19:15:40 -05:00
Umputun db3f22d9fd change image submit to single goroutine with active wait. flush all submitted on close 2019-03-23 19:13:58 -05:00
Umputun 58ec50e613 wire image submit to store.Service 2019-03-23 18:33:19 -05:00
Umputun c990b05c21 add image service with delayed commit. Move cleanup loop to service 2019-03-23 14:54:45 -05:00
Umputun de292a4146 add extraction of image ids 2019-03-23 03:47:07 -05:00
Umputun b49e242891 wire image staging and image cleanup 2019-03-23 03:06:24 -05:00
Umputun eb79c3d9f9 implement two-stage image commit with background cleanup 2019-03-23 02:56:58 -05:00
Umputun debd914e39 adjust image comments 2019-03-22 03:12:54 -05:00
Umputun eb3dd467ad change image location to user based, make random uuid for file name 2019-03-22 03:03:41 -05:00
Umputun bc714480d4 lint: multiple shadowed errors, missed comments for exported methods 2019-03-19 20:54:05 -05:00
Umputun 83de28c2da strip trailing / from REMARK_URL
https://github.com/umputun/remark/issues/289#issuecomment-474030504
2019-03-18 13:04:21 -05:00
Umputun 0aba6a5653 longer test time for server app 2019-03-11 14:23:12 -05:00
Umputun 3c3097cf77 add more image tests, generalize prep for those tests 2019-03-11 14:09:28 -05:00
Umputun 58eb4e8852 typos 2019-03-11 14:09:05 -05:00
Umputun 00d3930981 image hashing to sha256 2019-03-11 01:36:54 -05:00
Umputun 4def2affd8 fix robosts test with added picture 2019-03-11 01:23:13 -05:00
Umputun 3dc20bcbb5 image comments 2019-03-11 01:18:36 -05:00
Umputun 639f6c15f3 wire image save/load to rest and cmd 2019-03-11 01:16:25 -05:00
Umputun a1450cff57 add image storage 2019-03-10 19:40:14 -05:00
UmputunandGitHub 034101fb64 Merge pull request #286 from umputun/mod
switch to go mod
2019-03-10 15:42:52 -05:00
Umputun 1561dc2fda switch to go mod 2019-03-09 02:02:55 -06:00
Umputun d37ce1a912 move reports gen to test step 2019-03-08 01:31:43 -06:00
Umputun a7004f15a5 add non-anon middleware to vote post controller #279 2019-03-07 15:13:42 -06:00
Umputun 347fe1a0db randomize db name for rest test to avoid conflicts in parallel testing 2019-03-06 12:15:44 -06:00
Umputun e638fbb2c3 restore .git in build layer, needed for goverals to work right 2019-03-06 12:04:20 -06:00
Umputun 0d1ecd684c reorg ignore file 2019-03-05 22:43:28 -06:00
Umputun 7d475b185a common teardown for bolt tests 2019-03-05 22:32:57 -06:00
Umputun e3d6869cb0 common teardown for migrator tests, simplify WP import test 2019-03-05 22:32:34 -06:00
Umputun 2105b5ddfe revendor with latest lgr (logging) 2019-03-05 22:31:37 -06:00
UmputunandGitHub 084a7a9f99 Merge pull request #282 from alehano/patch-1
Update README.md
2019-02-24 03:40:38 -06:00
AlexandGitHub 279e200e3a Update README.md 2019-02-24 12:26:01 +03:00
Umputun 90a84dd886 update go-pkgz/auth to 0.4.2 with fixed direct id 2019-02-19 16:18:49 -06:00
Umputun fb6af6ca13 refactor readme about anon access 2019-02-19 14:24:30 -06:00
UmputunandGitHub 298987e662 Merge pull request #280 from umputun/anon
add anon option and direct provider #279
2019-02-19 14:10:26 -06:00
Umputun 0c950ae4f7 add anon option and direct provider #279 2019-02-19 13:59:43 -06:00
Andrei AlikovandUmputun d62825642d Add "contradiction index" metric (#278)
* show the value of `controversy` in the hover on scores
* add a new sorting mode "controversial" to "sort by" dropdown
2019-02-17 17:48:49 -06:00
Misha VyrtsevandUmputun a2b9b39640 Update dependencies (#277)
* update dependencies

* fix eslint warnings

* remove redundant sass-loader
2019-02-17 13:32:51 -06:00
Andrei AlikovandUmputun 653125d525 Hanlde limiter rejection on UI side (#275)
- Adds common function to get error message from the http response
- Adds error message for the voting displayed under the voting buttons
- Adds restoring of voting data after voting request failed

fixed re-writing error message when user tries to vote for the same comment
2019-02-12 09:35:14 -06:00
Umputun 643f255832 fix sorting after altering find results 2019-02-11 16:15:41 -06:00
Umputun 3ba38dd4c7 Revert "attempt to fix #276"
This reverts commit c98caace33.
2019-02-11 13:40:43 -06:00
Umputun c98caace33 attempt to fix #276 2019-02-11 13:11:10 -06:00
Umputun cb392c5df6 lint: missing comment for errors 2019-02-10 17:52:24 -06:00
Umputun 70ef130976 add error codes 2019-02-10 17:47:58 -06:00
Umputun 08ab725b3d lint: missing a comment for service.Find 2019-02-10 16:46:21 -06:00
Umputun 13c511b1b0 support controversy in Find for comments stored without it #274 2019-02-10 16:39:08 -06:00
Umputun ef59281112 add controversy sorting #274 2019-02-10 16:10:58 -06:00
Umputun 7128f66374 add controversy value (votes metric) #274 2019-02-10 15:53:29 -06:00
UmputunandGitHub fb0d8167eb Merge pull request #272 from AnyRoad/issue_148_strange_block_user_select_behavior
Fix block users select behavior
2019-02-06 15:02:11 -06:00
Andrei Alikov 3e72ec379a Fix block users select behavior
We need to have onBlur and onChange events listeners for the select for the "block user" as it advised by es-lint. But if we simply add onBlur event listener it will cause infinite loop of blur -> confirm pop-up -> cancel -> blur -> confirm pop-up... So we have to debounce the events to process them only one time.
2019-02-06 18:50:15 +09:00
Umputun 4917ba01ec update rest lib to fix recoverd panic 2019-02-06 00:45:47 -06:00
Umputun 58acbecf53 disable mgo debug 2019-02-02 23:46:38 -06:00
Umputun 72b60bf823 const for cache TTL 2019-02-02 23:26:03 -06:00
Umputun 38fc7e500c simplify title setter with anon func 2019-02-02 22:29:02 -06:00
Umputun 3314223120 switch native import lock to sized group 2019-02-02 22:28:16 -06:00
Umputun e6f30b0768 sync with preemptuve lock in concurrent migrator 2019-02-02 18:45:24 -06:00
Umputun 588af7fc9a switch title to lcw cache, optimize error hits 2019-02-02 18:25:57 -06:00
Umputun 0af3eda684 write imported records concurrently 2019-02-02 18:09:23 -06:00
Umputun 7ddf1fc096 add syncs lib 2019-02-02 17:51:44 -06:00
Umputun a731856bb1 fix shared title client caused races 2019-02-02 17:50:13 -06:00
Umputun 0fe3f6dd7d fix bad default for restricted words 2019-02-02 13:38:40 -06:00
Umputun 0e296bf1de less verbose MGO debug 2019-02-02 13:00:23 -06:00
Umputun 76d0cc32e2 use repeater context to avoid long calls 2019-01-30 15:59:00 -06:00
Umputun 44cb54477e reject logout on empty providers 2019-01-30 15:36:47 -06:00
UmputunandGitHub 4c2e0d0147 Merge pull request #266 from Reeywhaar/#260-ui
Support positive score only mode
2019-01-30 11:53:13 -06:00
UmputunandGitHub 5b25ea17b7 Merge pull request #271 from zonescape/comment-typo
Fix comment typo
2019-01-29 12:12:20 -06:00
zonescape de68b8d8ee fix comment typo 2019-01-29 18:37:25 +03:00
Umputun 3471c5dab8 add missing params to readme 2019-01-27 16:21:05 -06:00
UmputunandGitHub 1fb93fc823 Merge pull request #270 from nagimov/patch-1
add info about /web demo page
2019-01-27 16:15:29 -06:00
Ruslan NagimovandGitHub ef6cc71e02 add info about /web demo page 2019-01-27 14:09:37 -08:00
Umputun af6076187f set rss descriptions matching rss type 2019-01-27 15:08:39 -06:00
Umputun 93246aec0f Merge remote-tracking branch 'origin/master' 2019-01-27 14:28:35 -06:00
Umputun b106fe0fc6 increase size of last comments for reply detection to 5000 and time to 31 days #268 2019-01-27 14:28:24 -06:00
UmputunandGitHub 7712f3765f Merge pull request #269 from nagimov/patch-1
do not add .git folder to a build layer
2019-01-27 14:20:34 -06:00
Ruslan NagimovandGitHub 9870469e2e do not add .git folder to a build layer
`.git` folder isn't present in tarballs pulled from releases, e.g. `wget https://github.com/umputun/remark/archive/v.1.2.1.tar.gz`, and not used during the build anyways
2019-01-27 12:03:27 -08:00
Umputun c7468184ba fix unexpected blocking if +1 vote in positive score only mode on 0 2019-01-26 17:05:54 -06:00
Vyrtsev Mikhail 24f0c301d3 update readme 2019-01-27 01:23:04 +03:00
Vyrtsev Mikhail 7a394b8e90 support positive score only mode 2019-01-26 23:47:20 +03:00
Umputun cb5fc79dbe add site-level positve score support #260 2019-01-26 01:38:56 -06:00
Umputun 440beb3303 make sure last returns [] list #262 2019-01-25 23:20:30 -06:00
Umputun f7de26ef4b enforec empty list of comments on nil response 2019-01-25 22:49:52 -06:00
Umputun 6250f2e52e allow empty list of comments on find for fresh post without anything #262 2019-01-25 18:24:58 -06:00
Umputun 1cc09a33f6 fix lack of counter invalidation from the cache #261 2019-01-25 17:35:38 -06:00
Umputun 5a4e84550d Merge branch 'master' of github.com:umputun/remark 2019-01-25 17:10:26 -06:00
Umputun 8f0b6a6a59 add unit test for counters #261 2019-01-25 17:10:22 -06:00
Misha VyrtsevandUmputun f4a616e2de add widgets to demo page (#263)
* add widgets to demo page

* fix border color in dark mode

* demo page adjusting

* fix indentation
2019-01-25 16:43:57 -06:00
Umputun 1475060555 revendore with auth 0.4.1, fix yandex auth provider bug 2019-01-24 13:58:54 -06:00
Umputun 1761995482 skip logger pkg 2019-01-22 23:21:36 -06:00
Umputun 7aeec9535f revendor with fixed lgr pkg option 2019-01-22 18:44:59 -06:00
Umputun 32503395ea add caller pkg to log 2019-01-22 18:30:22 -06:00
Pavel AlimpievandUmputun e82a8f9824 Update README.md (#259) 2019-01-22 11:12:42 -06:00
Umputun 9375f250d8 switch restricted words logging to log 2019-01-21 17:03:22 -06:00
Umputun 84488182a2 avloid tokenization if lister failed or an empty list of restricted words #237 2019-01-21 16:52:49 -06:00
Aleksandr MelnikovandUmputun ab6c9a515c Support restricted words (#257)
* prepared interfaces and naive implementation for restricted words

* fixed whitespace tokenizer last token handling

* removed unnecessary check in whitespace tokenizer last token handling

* removed unnecessary interfaces, added matcher to service

* now using more explicit naming

* added config parameter/env variable to configure restricted words
provided simple matcher and lister implementation into service

* fixed linter warnings

* fixed linter warnings (part 2)

* added functional tests for rest api
moved check for restricted words into validation stage

* fixed missing error check in rest private test

* fixed failed test that expected validation in specific order

* fixed linter warning

* removed unnecessary 'else'

* removed redundant check from 'DataStore.EditComment'

* replaced exact matching for restricted words with wildcard matching

* fixed race condition when creating wildcardTrie
removed unnecessary wildcard masks
refactored tests in a table format

* improved documentation for restricted words param

* removed wildcardTrie caching as a premature optimization
removed unnecessary validation method ValidateCommentText
moved restricted words validation into service methods that create and update comment

* fixed error name due to linter warning
2019-01-21 16:48:35 -06:00
Umputun 2c988aa226 fix tg send test 2019-01-21 13:34:42 -06:00
Umputun 1a78c6eec6 add @ prefix for non-integer tg channelID #255 2019-01-21 13:28:03 -06:00
UmputunandGitHub dbc664e102 Merge pull request #258 from VaultVulp/feature/rework-telegram-integration
Remove prefixing of a `@` character to the channel name
2019-01-21 13:15:09 -06:00
Pavel Alimpiev a9909a91f4 Remove prefixing of a @ character to the channel name 2019-01-18 12:50:13 +03:00
Umputun 62b54eb009 resort imports 2019-01-17 18:50:02 -06:00
Umputun 7fa26343bd fix a comment 2019-01-17 15:07:47 -06:00
Umputun 97d1fa3942 add update-limit to readme options list 2019-01-17 14:57:33 -06:00
Umputun 2a3051c620 allow separate limiter for updates and posts, agressive default limit 2019-01-17 14:56:11 -06:00
Umputun 953ff03e2f missed in last commit 2019-01-17 13:18:49 -06:00
Umputun f7403f08b9 inject refresh cache to auth, revendor go-pkgz/auth to 0.4 2019-01-17 13:17:16 -06:00
UmputunandGitHub 6aae306be6 Merge pull request #250 from Reeywhaar/#248-frontend
Disable comment editing if it has reply
2019-01-17 11:27:37 -06:00
UmputunandGitHub f7851bc5f0 Merge pull request #254 from Reeywhaar/252-fix
Fix wrong label on voting buttons
2019-01-17 11:27:19 -06:00
UmputunandGitHub 225cd04980 Merge pull request #251 from Arelav/master
Prioritize disabled arrows style over general
2019-01-17 11:15:48 -06:00
Umputun 168088445f add caching for replied ids 2019-01-17 01:20:19 -06:00
Umputun ff46cbd173 require authed user to perfrom logout 2019-01-16 23:28:49 -06:00
Vyrtsev Mikhail ddd2c25498 exclude pin/unpin from last comments comment controls 2019-01-17 04:24:17 +03:00
Vyrtsev Mikhail 00bf148630 restore comments controls in last comments 2019-01-17 04:14:06 +03:00
Vyrtsev Mikhail e42110a3ba fix wrong label on voting buttons 2019-01-17 04:07:13 +03:00
Valery KharshatsandGitHub 5a034f4b52 Prioritize disabled arrows style over general
It disables hover color change for not logged in users.
2019-01-16 16:21:05 -05:00
Vyrtsev Mikhail 86fde18b7d hide edit input if original text and edited are same 2019-01-17 00:06:04 +03:00
Vyrtsev Mikhail 7ba4518587 disable comment editing if it has reply 2019-01-16 23:39:23 +03:00
Umputun 79a8bfe453 don't exclude reply to myself from HasReply detection https://github.com/umputun/remark/issues/248#issuecomment-454882841 2019-01-16 12:20:05 -06:00
Umputun dff266ec01 add detection of reply and disable edit for such comments #248 2019-01-15 17:05:30 -06:00
Umputun 367c3d0cc2 blocked users returns 401 2019-01-15 13:17:59 -06:00
Umputun 78debf05d9 auth 0.3.3 prevnets expired headers to pe passed in 2019-01-15 13:09:56 -06:00
Umputun e9585dc568 pass default logger to rest 2019-01-14 16:14:02 -06:00
Umputun 177b71655d update deps (lgr and mongo) 2019-01-14 00:06:32 -06:00
Umputun 8c8f13fd73 adopt for rest errors and revendor go-pkgz 2019-01-13 23:27:15 -06:00
Umputun 492f92e660 minor fixes for #247 2019-01-13 15:02:44 -06:00
UmputunandGitHub 9767bd42d2 Merge pull request #247 from saromanov/telegram-increase-coverage
Telegram increase coverage
2019-01-13 14:55:17 -06:00
Umputun cfe8391da5 revendor with lgr 0.2 2019-01-11 22:51:43 -06:00
Sergey c08a31809c moving back assert.equal 2019-01-12 01:22:08 +05:00
Sergey dd9b7d46ac telegram: increase coverage 2019-01-12 01:15:50 +05:00
Umputun 8b0926a338 fix #246 2019-01-11 14:06:31 -06:00
Misha VyrtsevandIgor Adamenko 57ffa5d491 Show post title in user comments sidebar (#244)
* show post title in user comments sidebar #241

* trim title with ellipsis

* show post title in user comments sidebar #241: post-review

* adjust to make user comments post title the same as last comments widget style

* remove user comments post title margin

* revert to f35ecc75b0
2019-01-09 16:20:55 +02:00
Umputun e5c8ac18de bad import 2019-01-08 12:44:07 -06:00
Umputun 835e82a441 add guid to rss with comment id 2019-01-08 12:41:10 -06:00
Umputun 3c54c159d2 Merge branch 'master' of github.com:umputun/remark 2019-01-08 11:41:26 -06:00
UmputunandGitHub 02d94ead1d Merge pull request #245 from umputun/lgr
Lgr
2019-01-08 04:01:33 -06:00
Umputun 61cf54d231 add comment id to rss 2019-01-08 03:59:15 -06:00
Umputun 781d90e98b switch to lgr 2019-01-07 22:32:50 -06:00
Umputun a6df83e960 fix wrong error wrapped in bold #243 2019-01-06 17:49:00 -06:00
Umputun f76afb9af5 increase reply rss duration to 24h 2019-01-06 17:37:58 -06:00
Umputun 6c02768f5d fix rest log prefix 2019-01-06 17:08:08 -06:00
Umputun e850dc1d3e add post title to comments api docs 2019-01-06 16:39:11 -06:00
igoradamenko 1f964c664c change colors of hovers of comment votes buttons 2019-01-07 00:38:45 +02:00
igoradamenko f2a8678715 add tabindex: 0 on buttons only if they have handlers 2019-01-07 00:38:43 +02:00
igoradamenko 88184fbc54 add link to replies rss 2019-01-07 00:38:42 +02:00
Umputun 3f6c5dbd19 Merge remote-tracking branch 'origin/master' 2019-01-06 15:31:35 -06:00
Umputun 8d978ffaf1 add post title to rss item 2019-01-06 15:31:29 -06:00
igoradamenko cb7b170b99 change color of username in last comments widget 2019-01-06 23:26:26 +02:00
Umputun 6edd24185c lint: missing comment 2019-01-06 14:57:21 -06:00
Umputun 3472475a82 add rest and batch (cleanup --title) support for title's #241 2019-01-06 14:52:43 -06:00
igoradamenko 709f75d90d show username link if there is no link on title in last comments widget 2019-01-06 21:54:28 +02:00
igoradamenko 566d5820ad fix passing title to the iframe 2019-01-06 21:22:28 +02:00
igoradamenko 0da1a8c237 fix alignment of the comment text in last comment widget 2019-01-06 21:10:54 +02:00
igoradamenko 80b7800b6a remove link from username in comment_view_preview 2019-01-06 21:01:46 +02:00
igoradamenko 52578221a9 pass page title to the iframe 2019-01-06 20:55:40 +02:00
igoradamenko d8c20da881 show comment's post title in last comments widget only if it exists 2019-01-06 11:42:01 +02:00
Umputun 4096302a06 test title not changed if presented 2019-01-05 20:58:34 -06:00
Umputun ad6491ff14 tg link with post title 2019-01-05 17:48:39 -06:00
igoradamenko 9dc83f5261 fix frontend linting errors 2019-01-05 22:24:37 +02:00
igoradamenko 87b5799a53 add title support for comments and last comments widget 2019-01-05 22:18:08 +02:00
Umputun ea2d87322c don't pull title if passed by client already 2019-01-05 13:47:11 -06:00
Umputun 0fd8657992 make title extractor optional 2019-01-05 13:22:28 -06:00
Umputun 690a0aa763 add post title to comment 2019-01-05 04:24:49 -06:00
Umputun 7b54b00c01 switched to auth 0.3 2019-01-04 18:09:18 -06:00
Umputun dcd40c42e2 vednor with auth 0.2.1 to minimize multiple token refreshes 2019-01-03 15:58:43 -06:00
Umputun 72a264b771 lint: error checks ignored in migrator test 2019-01-02 18:53:50 -06:00
UmputunandGitHub c24ff70880 Merge pull request #239 from umputun/auth-lib
Auth lib
2019-01-02 12:05:14 -06:00
Umputun a6d20b1563 lint: missing error check in tests 2019-01-02 11:50:36 -06:00
Umputun d8725caee9 chande docs to reflect auth lib usage 2019-01-02 11:19:42 -06:00
Umputun 48357609db Merge branch 'master' into auth-lib 2019-01-02 11:11:21 -06:00
Misha VyrtsevandAleksei Gurianov d48f758253 Disable voting on read-only posts (#234) 2019-01-02 19:40:58 +04:00
Umputun 17acbc6322 vednor auth v0.2.0 2019-01-02 00:16:37 -06:00
Umputun ca1943f909 revendor with auth:0.1.1 2018-12-31 17:47:25 -06:00
Umputun 189df5b0a4 revendor with latest auth lib 2018-12-31 01:05:29 -06:00
Umputun fdef63c61d make import expired test to run longer 2018-12-30 23:29:13 -06:00
Umputun 7deee9aaa1 revendor with lateas auth lib, sets logger to stdout 2018-12-30 23:06:20 -06:00
Umputun 80467c39af switch utils commands to basic auth, remove secret passing 2018-12-30 18:00:32 -06:00
Umputun 4215db8d89 change to ADMIN 2018-12-30 17:42:54 -06:00
Umputun c21f8757dd Revert "test non-0 crash"
This reverts commit a7c0041d8c.
2018-12-30 17:16:57 -06:00
Umputun a7c0041d8c test non-0 crash 2018-12-30 16:36:08 -06:00
Umputun 55cae69d49 add test triggeting auth hooks 2018-12-30 16:17:00 -06:00
Umputun 195e65ca60 adjusted tests for new auth lib 2018-12-30 13:31:03 -06:00
Umputun acf19e73cf minor formatting 2018-12-29 18:36:34 -06:00
Umputun d33997c742 pass auth by reference 2018-12-28 00:49:46 -06:00
Umputun 60700d96c7 isolate authenticator creation to a separate methos 2018-12-28 00:37:02 -06:00
Umputun 09de97ffd6 resort imports 2018-12-28 00:02:35 -06:00
Umputun 085468e788 missing remark url in auth init 2018-12-27 23:49:06 -06:00
Umputun c4b10a395a most test passes with ext auth 2018-12-27 23:11:20 -06:00
Umputun 59cdfc3d04 rest/api passes 2018-12-27 20:16:06 -06:00
Umputun b537f01b41 all api package compilable with auth lib 2018-12-27 14:59:45 -06:00
Umputun 09b44188cb longer import double test to trigger event 2018-12-24 15:40:35 -06:00
Umputun 440c7abfed switch local rest middlewares to go-pkgz/rest 2018-12-24 15:34:24 -06:00
Umputun c01bf58c0b add IsAdmin test 2018-12-24 01:08:08 -06:00
Umputun fd90623343 missing test for meta accessor with blocked users 2018-12-24 01:03:04 -06:00
UmputunandGitHub fdc419b79b Merge pull request #238 from umputun/migrator-meta
Migrator meta
2018-12-24 00:57:23 -06:00
Umputun de80a6414b Merge branch 'migrator-meta' of github.com:umputun/remark into migrator-meta 2018-12-24 00:39:34 -06:00
Umputun 2aaae35717 lint: lack to err checks in tests 2018-12-24 00:39:31 -06:00
UmputunandGitHub 2fc027299f Merge branch 'master' into migrator-meta 2018-12-24 00:35:09 -06:00
Umputun b8ed722b2e increase wrtie timeout to allow long exports 2018-12-24 00:19:12 -06:00
Umputun 7484649621 ass test for metas set 2018-12-23 03:26:19 -06:00
Umputun f3619b4fc3 adjust tests for v1 native format 2018-12-23 03:01:44 -06:00
Umputun be3d1bdf3e simplify import & export 2018-12-23 02:23:54 -06:00
Umputun 094f4c66a1 replace confusing name for native import/export provider 2018-12-21 20:01:03 -06:00
Umputun 616e4e2c1c sort metas for consistency 2018-12-21 02:21:58 -06:00
Umputun a5265bc82a lint: more shadowing 2018-12-21 00:39:26 -06:00
Umputun ca5948ab31 lint: errors not handled in export 2018-12-21 00:28:30 -06:00
Umputun a0dff0a4f0 adjust export tests 2018-12-21 00:13:13 -06:00
Umputun 110566a658 change native export to new format 2018-12-21 00:01:59 -06:00
igoradamenko d69da482a4 fix admin's username color 2018-12-21 01:26:36 +02:00
Umputun 10f21f78f0 basic test for Metas service 2018-12-20 02:49:56 -06:00
Umputun 25d3d3a5f4 add service level Metas accessor 2018-12-20 00:02:36 -06:00
Umputun f21a1a5ecf add verified func to engine to get list of verified users 2018-12-19 22:54:31 -06:00
UmputunandGitHub 2ecc08fb3e Ext cache (#236)
* remove  cache, switch to go-pkgz/rest/cache

* remove mongo cache from server test

* remove old caching code

* test tricky disqus inputs

* fix migrator test

* stacktrace dump test
2018-12-19 21:57:33 -06:00
Umputun bf716ac01f fix typo in err message 2018-12-19 13:35:10 -06:00
Umputun b6b818253f make notifier store to local interface for testability 2018-12-19 13:24:23 -06:00
Umputun e5afa2fb74 add check for manual RO https://github.com/umputun/remark/issues/230#issuecomment-448492041 2018-12-19 01:39:45 -06:00
Umputun 908eb9b631 lint: shadow err and defes on close with no err check 2018-12-18 22:24:20 -06:00
Umputun 96ddeaefc8 wrong mux gorilla deps 2018-12-18 22:15:17 -06:00
Umputun 998b390ec2 move constants up, add comments 2018-12-18 22:02:52 -06:00
Umputun 7560e98560 add cleanup tests 2018-12-18 21:57:46 -06:00
Umputun c393359511 add cleanup command with semi-automatic scoring 2018-12-18 17:40:10 -06:00
Umputun b54b23c236 replace conflicted 8080 port on SSL tests 2018-12-18 09:27:14 -06:00
Umputun 3a98d8593c fix mkdir tests #233 2018-12-18 09:09:25 -06:00
zonescapeandUmputun 20bb780deb simplify makeDirs() (#233) 2018-12-18 09:03:15 -06:00
Umputun 1ccbf3328f test for sequential dbl import 2018-12-17 03:02:15 -06:00
Umputun 6cc963a76c extract readonly check from controllers 2018-12-17 01:58:09 -06:00
Umputun f1128a74ad vote change disabled for RO posts #230 2018-12-16 23:25:50 -06:00
UmputunandGitHub 6bc0d7ee37 Import (#232)
* import with two-stages, wait api and prevnts double run for the same site #231

* lint: missing err check on tmp import file removal

* catch SIQQUIT

* add imprter comments

* add new import apis to spec

* add test for form import

* timeout for import wait api

* lint: uncecked errs
2018-12-16 23:21:01 -06:00
igoradamenko ced78f3332 remove left margin of comment__time in user comment 2018-12-16 21:32:50 +02:00
igoradamenko b7f1b2a609 set theme of comments inside user-info to light 2018-12-16 21:23:08 +02:00
igoradamenko 59ed9bb9a4 make user-info always white 2018-12-16 21:07:06 +02:00
igoradamenko de762331f0 change color of hr tag in dark theme 2018-12-16 20:53:15 +02:00
igoradamenko 99940567c7 fix score color in light & dark themes 2018-12-16 20:52:08 +02:00
Umputun 5c5abdd679 missing docs ro read-only age 2018-12-15 22:47:17 -06:00
Igor AdamenkoandUmputun 6f1b9037d5 Dark theme (#228)
* add with-theme hoc

* add dark theme styles for components

* add toggle theme button to the dev demo page

* add info about widget themes to README
2018-12-15 17:10:41 -06:00
Umputun 126edb39f0 allow short form -s (--site) for backup and restore 2018-12-14 21:26:02 -06:00
Pavel LoparevandUmputun 7b3e9bd659 Reset opacity for useless comment by mouse hover event (#227) 2018-12-08 22:48:04 -06:00
Anton KosourovandUmputun b3683c4f60 add tests for ssl mode (#226) 2018-12-08 22:46:10 -06:00
Umputun c9ce07c2e8 repeat telegram init attempt #225 2018-11-30 11:43:58 -06:00
Umputun cefb5324d5 double telegram timeout 2018-11-30 11:30:55 -06:00
Umputun 1de7ddc65c switch ui news to master 2018-11-28 14:41:25 -06:00
Umputun 5ccc998eda trigger build to test pub drone 2018-11-27 15:55:59 -06:00
Umputun 78edf6950b info about direct LE 2018-11-25 13:15:35 -06:00
Anton KosourovandUmputun 8f678ad04e Feature/add autocert support (#217)
* add le support

* fix log info

* change readme for autocert mode

* rename acme location param

* add test for http challenge

* add acme-email param

* do tls config more sane
2018-11-25 13:12:07 -06:00
igoradamenko 9e225771c7 node:10.6-alpine → node:10.11-alpine 2018-11-25 11:26:08 +02:00
igoradamenko 7b2e684316 stylish copied text 2018-11-24 23:24:33 +02:00
igoradamenko 9ea21305f5 improve copy helper for copying text styles 2018-11-24 23:23:34 +02:00
Umputun 154e23f8fb fix no-votes issue with wrong check for 0 value 2018-11-24 12:19:54 -06:00
Umputun 1432ec44d7 add unlimited votes constant to avoid magic -1 value on the caller side 2018-11-24 12:13:07 -06:00
Aleksandr MelnikovandUmputun f8517fd6a8 fixed zero vote limit not respected and treated as unlimited (#224)
* fixed problem when zero vote limit was not respected and was treated as unlimited

* fixed TestRest_Vote
2018-11-24 12:05:02 -06:00
Aleksandr MelnikovandUmputun f8e36551ea added maximum votes limit per comment (#223)
* added maximum votes limit per comment

* added 'max-votes' param description into README
2018-11-23 17:47:42 -06:00
igoradamenko 11d8338989 fix username during comment copying 2018-11-20 23:52:00 +02:00
Igor AdamenkoandGitHub 3e657709eb Merge pull request #219 from Reeywhaar/third-party-cookie-crash
third party cookies blocked fallback
2018-11-16 01:54:20 +02:00
Vyrtsev Mikhail 8e48605b75 oh no 2018-11-14 21:20:41 +03:00
Umputun 7233e95774 restore goroutine in notify non-blocking send 2018-11-14 11:08:25 -06:00
Umputun 3e49972e05 revert multiple attempt to notify, may cause dbl-posting 2018-11-14 11:05:37 -06:00
Vyrtsev Mikhail 6b11cd85d9 comments page remark href fix 2018-11-14 18:31:06 +03:00
Vyrtsev Mikhail 4bac8f4d2c comments page in style of https://remark42.com/demo/ 2018-11-14 18:28:44 +03:00
Vyrtsev Mikhail 87d011fe32 after @igoradamenko review 2018-11-14 17:56:48 +03:00
Vyrtsev Mikhail ae9332c0e5 fix unnecessary property 2018-11-14 09:52:51 +03:00
Vyrtsev Mikhail 5ac3255d17 third party cookies blocked fallback 2018-11-14 07:08:48 +03:00
igoradamenko 431d294cb6 default cursor for inactive comment control 2018-11-13 01:02:56 +02:00
igoradamenko 77bbfd4899 add copy button for admin 2018-11-13 00:56:02 +02:00
igoradamenko 344a4fa6a8 revert all temp frontend changes 2018-11-12 22:53:38 +02:00
igoradamenko aff685252f temp turn off linting & testing frontend 2018-11-12 22:41:51 +02:00
igoradamenko d5b1914b7e temp turn off linting for settings file 2018-11-12 22:33:54 +02:00
igoradamenko 586b2d3df3 first steps to removing iframe 2018-11-12 22:18:31 +02:00
igoradamenko 69167d0c95 first steps to removing iframe 2018-11-12 22:06:22 +02:00
igoradamenko 94687a9488 Revert: try to add storage access request during login, again 2018-11-12 21:28:14 +02:00
igoradamenko ad123aa3b3 add error logging for storage access 2018-11-12 20:43:31 +02:00
igoradamenko d53e1be75f open login window in new _window_ 2018-11-12 20:30:17 +02:00
igoradamenko 3f2bb253bd try to add storage access request during login, again 2018-11-12 20:30:17 +02:00
Umputun 35f8b17ac3 fix test with closed flag 2018-11-12 03:23:45 -06:00
Umputun c60c8d3f15 make norify close flag atomic 2018-11-12 02:26:52 -06:00
Umputun cbb534d2df init repeater in send loop 2018-11-12 02:10:22 -06:00
Umputun 30225e0fd0 repeater in tg send 2018-11-12 02:08:37 -06:00
igoradamenko a1a4f2b912 Revert: try to add storage access request for safari 12 during login 2018-11-11 23:10:39 +02:00
Umputun 618638b9ab missing doc for multi-post count request 2018-11-11 14:59:32 -06:00
igoradamenko 5e62155445 try to add storage access request for safari 12 during login 2018-11-11 22:42:11 +02:00
igoradamenko e09678c72f save sorting key to cookies instead of local storage 2018-11-11 15:43:57 +02:00
igoradamenko 95ea27d3ac add helper for work with cookies 2018-11-11 15:43:22 +02:00
igoradamenko 0150029bc5 fix typos in readme 2018-11-11 15:42:22 +02:00
igoradamenko 72d1aae739 add postcommit hook for fixing lint-staged 2018-11-11 15:42:03 +02:00
igoradamenko 06056a73a5 change list-staged renderer to silent 2018-11-11 15:40:38 +02:00
Umputun dec2d0062c ssl redirect test 2018-11-04 13:13:56 -06:00
Umputun a66886a731 add telegram init test with wrong port and fake token 2018-11-04 12:48:19 -06:00
Umputun 08ea544bdc missing wait for app completion in SSL server test 2018-11-04 12:35:16 -06:00
Umputun e96a782708 move SSL tests to cmd/server 2018-11-04 12:18:09 -06:00
Umputun 0f3243e19b close notifier in shutdown sequence 2018-11-04 11:51:08 -06:00
UmputunandGitHub 3747eb27fd Merge pull request #213 from akosourov/feature/add-ssl-support 2018-11-04 11:14:44 -06:00
a.kosourov 7e434ce4bc add ssl part in readme.md 2018-11-04 17:59:56 +03:00
a.kosourov 0e84c61c96 move ssl parts to ssl.go 2018-11-04 16:15:45 +03:00
Umputun 437b52ff9e default sort by activity 2018-11-01 22:58:40 -05:00
Misha VyrtsevandUmputun beab481d99 frontend: delete own comment button added (#214)
* frontend: delete own comment button added

* fix behaviour while in admin mode
2018-11-01 14:10:36 -05:00
a.kosourovandakosourov 614e87d17c fix linters warnings 2018-10-28 18:16:00 +05:00
a.kosourovandakosourov 5ebfb1518a Add https support, static mode for rest server 2018-10-28 18:16:00 +05:00
Umputun 904bc11c09 add lastCommentsScope to blocking rest 2018-10-27 13:20:50 -05:00
Umputun c589b5d65c eliminate manual json marshaling in tg 2018-10-26 13:58:31 -05:00
Umputun 16411c9375 unescape tg message 2018-10-26 12:55:30 -05:00
Umputun 5dd20062e2 adjust test to tg post 2018-10-26 12:42:46 -05:00
Umputun ae5aa5c1b7 switch tg to post 2018-10-26 12:40:18 -05:00
Umputun 07f3e07871 change arrows symb 2018-10-26 10:55:08 -05:00
Umputun aac58fac1e restore icon in tg message 2018-10-26 02:57:34 -05:00
Umputun d80b2d33cb and nop notifier 2018-10-26 00:37:58 -05:00
Umputun cc3786b9e0 change dead port 2018-10-25 23:30:30 -05:00
Umputun 43b97f8d56 typo in comment linl 2018-10-25 23:21:24 -05:00
Umputun e514af76b0 unify none keys, explicit nil for failed notifier 2018-10-25 20:41:08 -05:00
Umputun 431e8f2971 more tests for unhappy tg path 2018-10-25 20:27:35 -05:00
Umputun 267d901499 allow git at build time to fix coveralls failure 2018-10-25 20:09:48 -05:00
Umputun 928f483021 don't fail app on failed notify init 2018-10-25 13:21:07 -05:00
Umputun f08144d19e unicode arrow for tg orig link 2018-10-25 13:07:57 -05:00
Umputun 0ab71de95d unicode arrow for tg from->to 2018-10-25 12:48:23 -05:00
Umputun 7db2959eb1 mack to markdown tg formatting, html is way too limited 2018-10-25 12:38:55 -05:00
Umputun bc510f8c8e replace br to \n for telegram 2018-10-25 12:22:59 -05:00
Umputun 8f90af8343 fix br to one telegram may like more 2018-10-25 12:14:10 -05:00
Umputun b1a4d9b2da p to br in telegram formatter 2018-10-25 12:05:51 -05:00
Umputun 092161ff6f show url on err message 2018-10-25 11:52:53 -05:00
Umputun 3866d8e3b9 details to failed telegram send 2018-10-25 11:41:35 -05:00
Umputun f550438236 change telegram msg to html 2018-10-25 11:29:55 -05:00
Umputun d46ba8e913 fix table formating for md help 2018-10-25 02:20:10 -05:00
Umputun cdea2bc293 diable web page to true 2018-10-24 22:56:10 -05:00
Umputun a7b0a4bbf6 disable_web_page_preview for telegram notif 2018-10-24 22:47:00 -05:00
UmputunandGitHub 8c5531b178 Notify (#211)
* add notify service with no destinations

* change to request for destinations

* add telegram notify and wire to main

* lint: missed err checks

* fix notify factory

* externalize timeout for notify

* fix race in notifier mock

* lint: check notify send status in tests

* lint: write warn

* lint: wrong assignment

* lint: two more writes in tests

* missing notif timeout default
2018-10-24 22:28:56 -05:00
Pavel VinnykandAleksei Gurianov be41daae73 Auth handling added to deleteme.html #170 (#208) 2018-10-20 01:57:23 +04:00
Umputun 8e12b00ae8 give more time to server start 2018-10-19 16:13:37 -05:00
Umputun d2bbb5728d disable gas linter 2018-10-19 14:44:07 -05:00
Umputun e2558d84cb fix admin user in dev composes 2018-10-11 15:47:23 -05:00
Umputun ef5b18c3d1 add nop cache 2018-10-05 13:31:33 -05:00
igoradamenko a7ba0567a3 remove useless close button in user-info 2018-10-01 10:30:27 +03:00
igoradamenko 06baa07da4 fix frontend build issues 2018-09-29 23:14:20 +03:00
Jackandigoradamenko ab6bfff32f Add help page about markdown formatting #8 2018-09-29 23:04:33 +03:00
Igor AdamenkoandGitHub ca8c5d3850 Merge pull request #153 from DmitryTsepelev/user-info-redux
UserInfo uses redux store
2018-09-29 22:51:21 +03:00
Igor AdamenkoandGitHub a6e2fd30c4 Merge branch 'master' into user-info-redux 2018-09-29 22:50:33 +03:00
Umputun 72aeadcfd1 remove legacy Avatart interface from engine 2018-09-16 12:51:09 -05:00
Umputun be00380b5a lint: spell fix 2018-09-15 14:18:28 -05:00
Umputun e610657e36 better comments for avatar store 2018-09-15 12:33:04 -05:00
UmputunandGitHub c70e060d22 Feature/avatar_bolt (#205)
* add bolt implementation of avatar store

* include bolt avatar store in main's test

* change docs with new avatart params

* lint: single line declaration

* separate bucket for avatars metas

* fix comments

* test for bolt params

* closable avatar store

* fix dbl-close
2018-09-14 20:33:40 -05:00
Igor AdamenkoandGitHub d7185a861a Merge pull request #199 from Clearic/userinfo-close-by-esc
UserInfo close by Esc fix, issue #187
2018-09-14 14:26:54 +03:00
Umputun dc59fc9f7b adjust test for key store 2018-09-12 00:34:36 -05:00
Umputun a71907fd8e reduce frome AdminStore to KeyStore in Authenticator 2018-09-12 00:28:00 -05:00
Umputun bb0096dd3d add test for empty user in a token 2018-09-11 14:22:02 -05:00
UmputunandGitHub f6708c12a1 Merge pull request #202 from umputun/feature/avatar_migration
feature/avatar_migration
2018-09-11 13:15:26 -05:00
Umputun a8b92a6039 lint: detached package comment 2018-09-11 12:08:10 -05:00
Umputun 6458f622d2 refactor to combine admin and key store 2018-09-11 11:58:50 -05:00
Umputun 396b77e4ff info messages and less verbose auth with dev 2018-09-11 00:36:46 -05:00
Umputun ae8071ac00 fix web location for binary artifact builds 2018-09-10 22:18:51 -05:00
Umputun 79b1c47005 more dbg info on types 2018-09-10 22:18:23 -05:00
Umputun 7934d9af73 add cli for AvatarCmd 2018-09-10 20:48:36 -05:00
Umputun a231054424 lint: gfs ID mismatch 2018-09-10 12:41:49 -05:00
Umputun 9a6ceda003 add migrator between avatar stores 2018-09-10 12:38:00 -05:00
Umputun 5b204e509e add List to avatars and allow put without encoding 2018-09-10 12:19:52 -05:00
Umputun a856fd9c7d pass revision as common opts instead of package global 2018-09-10 10:44:22 -05:00
UmputunandGitHub 75a1edcff3 feature/adm_mongo (#201)
* add admin akd key mongo implementations

* wire admin and key stores to mongo

* lint: wrong comment on keys store

* typos and comments corrections

* fix failed test on avatar resizer

* get rid of ineffectual assignment in test

* add tests for encoding fallback and mongo's keys & admin

* empty encode test adjusted
2018-09-09 16:59:33 -05:00
Umputun 3ab05490b4 lint: warn on basic auth and non-handled err in gridfs avatar 2018-09-07 14:48:58 -05:00
Umputun 96e4116d60 fix typo in comment 2018-09-07 02:43:34 -05:00
Umputun 93347df190 Merge remote-tracking branch 'origin/master' 2018-09-06 10:35:48 -05:00
Umputun d4c36edc55 add delete flag to edit request #200 2018-09-06 10:35:31 -05:00
Alexandr 2a3b380686 UserInfo close by Esc fix 2018-09-06 09:06:42 +05:00
Umputun 061ceb4f81 lint: unused field in admin 2018-09-05 20:57:33 -05:00
Umputun add715a111 move common options to shared cmd 2018-09-05 16:42:46 -05:00
Umputun ba4cc38e40 lint: missing comment 2018-09-05 02:19:03 -05:00
Umputun 1b3904a307 populate common params to commands 2018-09-05 02:13:57 -05:00
UmputunandGitHub 84e7770359 Feature/admin-migrator (#198)
* auth with a secret as admin

* get rid of separate migrator service

* clean sanitize dbg prints

* fix dbg logging and set migrator routes
2018-09-03 23:13:29 -05:00
Umputun 3cc6762ebf add comments 2018-09-02 23:14:29 -05:00
UmputunandGitHub 0cd76dfd2f Feature/admin_store (#197)
* generalize admins and email info with store interface

* lint: missing group comment

* fix admin group with shared substruct
2018-09-02 13:00:20 -05:00
Umputun 78c27caa7d remove dbg print 2018-08-30 01:53:02 -05:00
Umputun 8b3821e797 simplify cmds 2018-08-30 00:23:36 -05:00
Umputun dfa80d46e1 missed check for server.Execute status 2018-08-26 21:57:34 -05:00
UmputunandGitHub ba6bab5b91 Feature/cmd (#195)
* support flags commands, move to cmd

* fix target name

* test for happy path importer

* add export cmd

* fix wrong import, lint warns

* increase test timeout

* add sellp to allow main test server to start

* implement all cmds

* handle backup/restore errors

* fix import status check, hide secret from logs

* backup cmd err tests

* randimize test port

* avoid dup code in Last controller

* add target to make all bin archives

* remove container in make

* add smiple scripts to simplify commands, update readme

* add docs on dockerless, enforce app user

* add restore info

* move last to lastCommentsScope const
2018-08-26 21:45:04 -05:00
Alexandr KandAleksei Gurianov 419d2ae75e Add focus-visible polyfill, issue #187 (#193)
* add focus-visible polyfill
* remove unnecessary outline: none from styles
* make buttons Pin, Block, Delete visible when focused
* fix outline of user details close button
* close user info by ESC
* user info: don't focus the close button
2018-08-23 20:46:16 +04:00
Umputun a6277d764a add CommentConverterFunc to simplify injection of custom converters 2018-08-18 23:09:17 -05:00
Umputun 031a25ad92 Merge branch 'master' of github.com:umputun/remark 2018-08-18 22:38:13 -05:00
Umputun f3880aa302 add a test for \n handling in wp, fix incorrect curl param caused removal of \n #156 2018-08-17 16:12:02 -05:00
Umputun 03eb57ef81 too many backticks by mistake 2018-08-16 18:57:41 -05:00
Umputun 8d9622cfd0 shutdown after import test 2018-08-16 18:48:34 -05:00
Umputun a05b0b81c0 add test for reported MD case https://github.com/umputun/remark/issues/156#issuecomment-413548318 2018-08-16 18:37:57 -05:00
Umputun 8503e31f64 reformat imports 2018-08-15 22:14:30 -05:00
Umputun 4e2895aa2d simplify wp converter, no need in special struct 2018-08-13 21:47:39 -05:00
Umputun 8ed75439ee fix typo in formatter name 2018-08-13 21:42:37 -05:00
Anton KosourovandUmputun d5a75cfc00 Use comment formatter for wp importer (#192)
* use comment formater in wp importer

* remove debug code

* change name to formatter
2018-08-13 21:33:00 -05:00
Umputun 6cb4881039 add info about local build procedure 2018-08-12 14:54:40 -05:00
UmputunandGitHub 30ff1dacde feature/artifact (#190)
* embed assets for web with separate dockerfile

* add artifact dockerfile

* add artifacts upload to github release

* fix statik params and add make for local builds

* fix docker image name to match compose

* fix typos
2018-08-12 14:24:46 -05:00
Umputun 92ef9882a8 simplify readme a little bit 2018-08-11 11:46:48 -05:00
Umputun c459b31aa8 fix typos in struct names 2018-08-11 11:29:31 -05:00
UmputunandGitHub 2ff955edf1 feature/comment-formater (#186)
* extract comment formatter functuionality to allow md in #156

* add abilty to pass multiple convertors to formater
2018-08-10 19:35:45 -05:00
Umputun f15af0c7dc fix table 2018-08-09 01:58:30 -05:00
Umputun e742a08a1b Merge branch 'master' of github.com:umputun/remark 2018-08-09 01:57:22 -05:00
Umputun 08e902e785 add table of contents 2018-08-09 01:57:18 -05:00
UmputunandGitHub 1b9ee3d763 feature/keystore (#185)
* add keyStore and switch all secret usage to this store

- implement static ks
- add shared secret

* lint: shadow in jwt and missing comment

* rename static key group to shared and use top-level shared secret

* move siteID extraction (from token) to internal func

* lint: merge var and assign for key test
2018-08-08 14:33:18 -05:00
Umputun b17e545773 same placeholder in compose as in widget 2018-08-06 10:52:35 -05:00
Umputun c22ed2300b clear link in md 2018-08-06 10:50:31 -05:00
Umputun af3208ac09 add info about SITE env 2018-08-06 10:49:44 -05:00
Anton KosourovandUmputun 39e426bd4b Add wordpress importer (#182)
* add wordpress, test

* add wp migrator to rest

* add readme, fix test, fix error handling
2018-08-02 22:05:53 -05:00
UmputunandGitHub e79b95de0c Feature/cors (#183)
* add cors internally #160

* revendor
2018-08-02 19:26:34 -05:00
Umputun 2a7d23c2ca clean test bolt after remark migrator tests 2018-08-01 22:19:06 -05:00
Umputun 6d82a1be93 extract common hasing with failback 2018-07-25 13:28:15 -04:00
Umputun cd7e9832d2 add avatart removal path to deleteMe test 2018-07-25 13:17:42 -04:00
Umputun 1c97e0d31f add delete avatar and call it from deleteMe controller 2018-07-25 12:48:57 -04:00
JackandAleksei Gurianov 5c9d0d6119 Fix incorrect edit comment countdown timer calculation #74 2018-07-25 17:41:13 +04:00
Umputun a817a22471 more mongo cache tests 2018-07-24 22:14:09 -04:00
Umputun 0f7819ffcf add integration test with mongo store, avatar and cache 2018-07-24 21:22:22 -04:00
UmputunandGitHub 3520de768d feature/mongo cache (#180)
* add siteID to cache Get

* indirect option setters

* add mongo cache with tests, add Key and Flusher

* lint: minor warns

* workaround for cache parallel test

* repeater in mongo cache

* missing repeater vendor

* fix nop cache

* add cache mongo benchmark

* wired mongo cache, single opts group for mongo

* disable goconst

* stop cache repeated on not found error

* use local mongo for tests in travis
2018-07-24 18:43:53 -04:00
UmputunandGitHub dbd1d4069f feature/ext-mongo (#177)
* externalize mongo wrapper

* remove mongo env from drone

* export mongo test for coverage report
2018-07-22 22:46:25 -04:00
Aleksei GurianovandUmputun fdf161850b Close DataStore on cleanup in tests (#173)
Windows fs can't remove testDb until file still used.
Therefore we need to close datastore on cleanup.
2018-07-19 09:33:40 -05:00
Umputun 1a423a464b add tests 2018-07-19 00:25:51 -05:00
Umputun b2775d92b0 chnage flatlf to panic from MustGetUserInfo 2018-07-19 00:09:32 -05:00
Umputun 7e00d40596 simplify user parsing with MustGetUserInfo 2018-07-19 00:08:13 -05:00
Umputun 0ae7f226af extend jwt claim with flags 2018-07-18 21:05:51 -05:00
Umputun 3abe4146b1 add deleteme flag to jwt token
it should prevent misuse of laked token to request user's data removal
2018-07-18 16:56:23 -05:00
Alexandr KandAleksei Gurianov c1e2616764 Input fix for Edge issue 169 (#171) 2018-07-19 00:16:37 +04:00
Aleksey Gurianov 8f3b8e11e1 Fix crossdomain clickoutside listener 2018-07-18 23:37:00 +04:00
Aleksei GurianovandUmputun 10e79cdd90 another flaky ttl block test (#172)
see #120
2018-07-18 13:27:56 -05:00
Aleksey Gurianov b27489e918 Logout user on deleteme request 2018-07-17 23:21:11 +04:00
Aleksey Gurianov 9b5c784bdc Admin page to approve deleteme request 2018-07-17 23:21:11 +04:00
Eldar AmantayandAleksei Gurianov 1d267f5db6 ISSUE-73: UI support for DeleteMe request
Closes #73

* Add the API post request deleteMe
* Add a trigger button
* Add an utils/email module with the template and window opener
* Add a dropdown component
* Add a button component
* Hide the user-id text and a logout bout in the dropdown
* Add a noop empty function to pass a default handler callback to components
2018-07-17 23:21:11 +04:00
Umputun 592def143a clarify comments 2018-07-15 12:11:48 -05:00
Umputun e278da3cd0 no need to skip in dockerfile, handled in testing.go 2018-07-14 13:45:04 -05:00
Umputun f9ba969464 another attempt to fix unexpected mongo tests 2018-07-14 13:42:00 -05:00
Umputun e2360eafe1 set mongo url to skip in order to allow PR builds 2018-07-14 12:13:24 -05:00
Umputun a22521ab92 allow mongo with URL only, update readme for different stores 2018-07-13 14:16:03 -05:00
UmputunandGitHub de013c7d5a feature/mongo (#165)
* WIP: start mongo engine

* WIP: mongo accessor and admin

* integrate mongo store to main

* disable mongo tests by default, only in CI

* connection with constructor

* add mongo buffered writer

* buffered mongo writer

* fix nil responses on an empty list from mongo

* missing mongo index for scores

* cancelable store

* add gridfs implementation of avatar store

* fix race on mongo session copy

* gridfs avatars without tmp files

* move avatar store

* minor comments and refactoring for avatar store

* merged from current master

* simplify gridfs reader

* lint: fix minor warns

* test mongo against env defined url

* pass MONGO_REMARK_TEST to docker and travis

* set dockerfile env for mongo test url

* increase connect timeout in mongo tests

* pass MONGO_REMARK_TEST to drone build

* add MONGO_REMARK_TEST to branch stage of drone

* mass mongo test url via build_args_from_env

* populate mongo IP to docker build hosts

* test env

* pass mongo ip via .mongo

* remove .mongo temp from git

* add .mongo -> env to linter step

* allow more time to autoflush writer test

* default mongo tests to "mongo" if not in env

* merge fresh master into

* add test for mongo cleanup

* msg for a failed test

* lazy fix for failed test

* add an ability to skip all mongo tests

* add backend dev instructions

* remove unused code from mongo server

* move mongo testing to connection_test

* restore testing.go

* lint: minor warns for testing code
2018-07-13 12:27:11 -05:00
Efremov AlexeyandAleksei Gurianov 4cca80e3c1 restyle close button, add close animation (#152)
closes #98
2018-07-13 11:46:22 +04:00
Jack & BurgerandAleksei Gurianov 905d41af7e Option to disable comments manually (#154) 2018-07-13 11:41:12 +04:00
Aleksei GurianovandGitHub a544aff6ac improve and document developer setup (#161)
japanese commit
fixes #158
2018-07-13 09:54:12 +04:00
Umputun 62a5d7196e add comments to dev oauth 2018-07-11 15:27:18 -05:00
Aleksey Gurianov f74f9e462c improve ux of login flow #157 2018-07-12 00:20:05 +04:00
Aleksey Gurianov 25c75011f4 tweak CI detect on frontend 2018-07-11 22:44:13 +04:00
Umputun 42b9baf0f3 respect REMARK_URL for dev callback #159 2018-07-11 13:42:00 -05:00
Umputun 547ea8375e change deletem result link to /web #73 2018-07-10 17:05:03 -05:00
Umputun b2a8434f9a add automatic github-style avatar for dev auth #106 2018-07-10 12:19:10 -05:00
DmitryTsepelev d09e45b0a8 UserInfo uses redux store 2018-07-10 17:33:51 +03:00
Aleksey Gurianov b076a06e33 regenerate package-lock 2018-07-10 14:13:53 +04:00
Dmitry TsepelevandAleksei Gurianov a02b1502a6 Basic redux integration for thread component (#141) 2018-07-10 14:05:39 +04:00
Umputun b6cda91dd9 invalidate user_id scope on vote change #151 2018-07-10 02:02:17 -05:00
Aleksei GurianovandUmputun e17ac360ff permanent duration comment fix (#150) 2018-07-09 16:33:57 -05:00
Aleksei GurianovandGitHub 2c570c2530 reveal node_modules in separate container (#123)
This allows reusing docker fs layer with node_modules
in case if there were no changes in package*.json
As a result, it decreases dependency on network
during rebuilds at least in a local environment.
2018-07-10 01:03:27 +04:00
Efremov AlexeyandAleksei Gurianov 1539c1f043 add user-info in iframe (#131)
closes #98
2018-07-09 18:32:14 +04:00
Jack & BurgerandAleksei Gurianov 4cea9d5855 Add ability to ban users for limited time and to view in the 'Show blocked' (#140)
closes #88
2018-07-09 16:58:08 +04:00
Umputun ce8c2c236f add last scope 2018-07-09 02:52:13 -05:00
Umputun b2f2156460 change rss reply scope to site 2018-07-09 02:48:35 -05:00
AleksanderandAleksei Gurianov 31cca0146f use es7/promise polyfill instead of es6 for finally support (#147)
closes #122
2018-07-09 10:57:17 +04:00
Umputun 0ee7a6940a increase last to 1000 for reply rss, add to readme 2018-07-08 23:54:00 -05:00
Umputun 0c7b6589f2 simplify erro handling for reply rss 2018-07-08 23:23:05 -05:00
Anton KosourovandUmputun 75249bd7fe RSS replies to a user #111 (#145)
* add rss feed for replies to user

* fix add consts
2018-07-08 23:12:50 -05:00
UmputunandGitHub 43092bb644 skip build steps with ARG and compose #143 (#144) 2018-07-08 18:43:58 -05:00
Mihail NovikovandAleksei Gurianov bcd28306e2 migrate from sass to PostCSS (#142)
closes #135
2018-07-09 01:17:04 +04:00
Sviataslau ShchaurouskiandAleksei Gurianov 20f08683b5 112: mobile version breaks title with author name (#126)
* 112: refactors comment component a bit - replaces few onDo/onUndo methods with their toggle equivalent

* 112: refactors comment component a bit - replaces few onDo/onUndo methods with their toggle equivalent part 2

* revert it later

* Revert "revert it later"

This reverts commit f8bee04

* Adds engine section in package.json and sourcemaps generation in webpack

* Extracts Avatar as component

* Fixes typo, linting and bug

* Removes dev changes

* Fixes linter errors

* Fixes PR comments
2018-07-08 15:42:19 +04:00
Umputun 02d298aac6 remove pullreq from drone, remove .git and default version to local 2018-07-07 14:16:52 -05:00
Umputun d58be0349e remove PULL_REQ mapping for master 2018-07-07 14:06:14 -05:00
Umputun 2afab423a7 map DRONE env 2018-07-07 14:05:43 -05:00
Umputun d8a2a42e35 try to switch to drone version on non-tagged 2018-07-07 13:58:31 -05:00
Aleksey Gurianov d1e3b14024 fix iframe container overflow
fix #80
2018-07-07 17:08:50 +04:00
DmitryTsepelevandAleksei Gurianov ea47e1d19c Fix right margin for verified user icon 2018-07-07 13:10:16 +04:00
Aleksey Gurianov dcd97a9df7 set eslint prettier rule to error level 2018-07-07 01:19:18 +04:00
PrettierandAleksey Gurianov 2df3aa01d4 apply prettier formating 2018-07-07 01:17:37 +04:00
Umputun 5d1201351a back to lightweight git-rev, exclude .git/objects 2018-07-06 15:36:21 -05:00
DmitryTsepelevandAleksei Gurianov c173647d96 Cleanup user-info, make preloader and list-comments pure functions 2018-07-06 23:23:31 +04:00
Mihail NovikovandAleksei Gurianov b08e351da0 Add a11y linter (#130) 2018-07-06 20:13:24 +04:00
Aleksey Gurianov 9949632cd7 set prettier rules to warn and fix jsx pragma 2018-07-06 19:05:35 +04:00
Efremov AlexeyandAleksei Gurianov dec8bb4fc9 Add: Issue #92 prettier + husky + lint-staged 2018-07-06 19:05:35 +04:00
Aleksey Gurianov fea398483e Revert "Add: user-info in iframe Issue #98"
This reverts commit bae2b8dcaa.
2018-07-06 03:08:53 +04:00
Mihail NovikovandAleksei Gurianov 6c41ce263c Fix comment_info block for small screens 2018-07-06 01:56:04 +04:00
Efremov AlexeyandAleksei Gurianov bae2b8dcaa Add: user-info in iframe Issue #98
Signed-off-by: Efremov Alexey <lexich121@gmail.com>
2018-07-06 01:30:37 +04:00
Umputun 841934f44b lint: unused contextkey, leftover from rewrite middleware integration 2018-07-01 11:53:43 -05:00
Umputun 70649b2712 route /index.html #103 2018-07-01 11:50:02 -05:00
Aleksei GurianovandUmputun e753346255 increase ttl block in tests to reduce flakiness (#121) 2018-07-01 10:58:34 -05:00
Aleksei GurianovandUmputun df9d92cd21 actual fix #118 (#124) 2018-07-01 10:56:12 -05:00
igoradamenko ed92a0bda5 add finally polyfill for promises 2018-06-30 23:56:52 +03:00
Umputun 6a4067d6e6 remove postFlush 2018-06-30 13:30:17 -05:00
Umputun dcb0ee60d6 simplify .sh deployment 2018-06-30 13:15:18 -05:00
Umputun e254936521 move scripts to backend 2018-06-30 13:12:04 -05:00
Umputun 1250828509 don't fail due to coveralls issue #119 2018-06-30 12:52:30 -05:00
Umputun 65bd239778 fix #118 2018-06-30 12:40:59 -05:00
Umputun 60879e8c42 rename resize limit env 2018-06-30 11:31:49 -05:00
UmputunandGitHub 43bd6c08f1 explain USER 2018-06-30 11:28:37 -05:00
UmputunandGitHub 2fea4151de update testing section with info about other users 2018-06-29 16:00:36 -05:00
igoradamenko 5baa9e83ac add part about testing frontend & backend using local server 2018-06-29 22:14:40 +03:00
Igor AdamenkoandGitHub 06544037f1 Merge pull request #105 from Guria/polyfiils
Polyfils
2018-06-29 21:56:37 +03:00
Umputun 28f3293d1e Merge branch 'master' of github.com:umputun/remark 2018-06-29 00:10:06 -05:00
Umputun 955a2cd1da add comments 2018-06-29 00:09:59 -05:00
Kirill KravinskyandUmputun 15ce0c4d6e Defer body closing in test (#115) 2018-06-29 00:08:40 -05:00
Umputun 1b1eb1a847 move nginx sample to backend 2018-06-28 21:00:42 -05:00
UmputunandGitHub 07ab1f3acd feature/subtree (#116)
* hide deleted subtree from response #114
* hide deleted sub comments with no reply #114
* lint: tree json err check
2018-06-28 19:51:47 -05:00
Umputun ca8a20ba7e lint: missing error check 2018-06-28 02:03:16 -05:00
Umputun 3cde98a480 add abilty to enter custom dev user #106 2018-06-28 01:58:53 -05:00
Umputun 822ef7df34 fix auth url spec 2018-06-27 19:32:01 -05:00
UmputunandGitHub 3e5f6ea6ff fix port num 2018-06-27 18:02:52 -05:00
igoradamenko 43099edf06 add site_id param to /auth request 2018-06-27 11:07:54 +03:00
Umputun 030f26bbef make permissions checker mandatory 2018-06-27 02:12:25 -05:00
UmputunandGitHub 7a6eca56b1 feature/jwtcleanup (#113)
* remove extra dmin check, add blocking check via jwt and auth middleware
* short jwt and refresh for expired
* lint: missing comment on UserFlager
* simplify user management in auth refresh
* allow custom max cookie age
* test blocked user
* reset cookie for blocked user
* move admin perm detection to data service
* customizable ttl with opts as a part of auth group
* add local auth provider dev
* main minimal test for dev auth mode
* add comments and update docs with current params
* add admin and auth_dev flags
* comments for dev compose
* lint: shadow err
2018-06-27 01:43:13 -05:00
UmputunandGitHub c316b27fe1 feature/block ttl (#108)
* add ttl to blocking #88

* remove unused parsing code

* add comments explaining TTL for blocking
2018-06-24 15:43:32 -05:00
Гурьянов Алексей Александрович 7088accf70 split polyfills into separate bundle 2018-06-24 16:14:46 +04:00
Гурьянов Алексей Александрович f473c2a659 fix eslint warnings 2018-06-24 16:14:07 +04:00
Igor AdamenkoandGitHub b5b7bc50af Merge pull request #104 from alexelev/refactor/APIDefinition
Refactor API definition (fixed #94)
2018-06-23 23:36:38 +03:00
Alex 76c7ea13d3 #import @method -fix 2018-06-23 23:15:56 +03:00
Alex 0b36b79d16 #remove @method -rename 2018-06-23 23:13:45 +03:00
Alex 73d875f477 #unverify @method -rename 2018-06-23 23:12:31 +03:00
Alex 72f1f774f5 #verify @method -rename 2018-06-23 23:11:34 +03:00
Alex 400905c8ff #unpin @method -rename 2018-06-23 23:09:52 +03:00
Alex 7b7934e93c #pin @method -rename 2018-06-23 23:08:41 +03:00
Alex 0171fc85b5 #edit @method -rename 2018-06-23 22:57:46 +03:00
Alex 48bcadbceb #send @method -rename 2018-06-23 22:56:54 +03:00
Alex 7e27cf5060 #vote @method -rename 2018-06-23 22:53:22 +03:00
Alex 17529d0646 #counts @method -rename 2018-06-23 22:51:14 +03:00
Alex 83d3afa291 #last @method -rename 2018-06-23 22:38:55 +03:00
Alex 37f9954f11 #find @method -rename 2018-06-23 22:35:56 +03:00
Igor AdamenkoandGitHub 7c4fbdb5a1 Merge pull request #100 from Mavrin/add_test
Setup testing environment
2018-06-23 21:11:57 +03:00
Umputun caa0e4e79a ensure dep 2018-06-23 12:33:48 -05:00
UmputunandGitHub 7aee7add08 Feature/backend (#101)
* move backend code #99

* adjust drone build for backend subdir

* move .rest to backend
2018-06-23 12:31:19 -05:00
konstantin krivlenia 66ca6173bb fix formatting 2018-06-23 20:30:51 +03:00
konstantin krivlenia 52d791a7b6 setup testing environment 2018-06-23 18:36:14 +03:00
igoradamenko 213437bf34 remove useless code 2018-06-23 13:47:31 +03:00
igoradamenko 833520c494 fix typo 2018-06-23 13:18:12 +03:00
igoradamenko 73b91ca639 remove todo.md 2018-06-23 13:12:07 +03:00
Igor AdamenkoandGitHub 328b7f76c7 Merge pull request #91 from Mavrin/master
Adds eslint check and fixes linting issues
2018-06-23 12:18:41 +03:00
Umputun ea76136b75 make admin not so red 2018-06-22 17:52:56 -05:00
konstantin krivlenia 07f82bf6a5 add eslint check 2018-06-23 01:38:03 +03:00
Umputun 422427afea don't allow blocked users to vote 2018-06-22 17:33:04 -05:00
Umputun 9965111371 add new userinfo api to docs 2018-06-22 15:55:40 -05:00
Umputun 2729fe35fb add GetUserInfo controller to auth 2018-06-22 15:53:53 -05:00
Umputun a15e4ccb62 invert error check #79 2018-06-21 21:49:11 -05:00
Umputun ff982dbd71 change short links to 48 #79 2018-06-21 21:32:49 -05:00
UmputunandGitHub c0003922af Merge pull request #89 from anatolym/add-link-shortening
add link shortening #79
2018-06-21 21:23:07 -05:00
Anatoly Milkov a7a9f9aeff add link shortening #79 2018-06-21 18:57:14 -07:00
UmputunandGitHub 7e84f4d3eb Merge pull request #87 from anatolym/add-avatar-caching
add avatar caching #85
2018-06-20 02:57:55 -05:00
Anatoly Milkov 065a0335d4 add avatar caching #85 2018-06-20 00:09:35 -07:00
Umputun 21b0d0f7fd change image in docs and examples to :latest 2018-06-19 15:32:16 -05:00
Umputun 1a817566de change env keys in tech details section 2018-06-19 15:10:39 -05:00
3271 changed files with 591149 additions and 189048 deletions
+6 -3
View File
@@ -1,11 +1,11 @@
/logs/
/target/
/var/
/web/node_modules/
/web/public/
/frontend/node_modules/
/frontend/public/
/.vscode/
/.idea/
#/.git/objects/
/bin/
# source files
docker-compose.yml
@@ -19,3 +19,6 @@ debug
debug.test
*.prof
*.test
remark42
/backend/var/
compose-private-backend.yml
-80
View File
@@ -1,80 +0,0 @@
workspace:
base: /go
path: src/github.com/umputun/remark
pipeline:
build:
image: golang:1.10-alpine
commands:
- cd app
- go build -v ./...
docker_master:
image: plugins/docker
repo: umputun/remark42
secrets: [ docker_username, docker_password ]
tags:
- ${DRONE_COMMIT_BRANCH/\//-}
when:
branch: [master, release/*]
event: push
docker_tag:
image: plugins/docker
repo: umputun/remark42
secrets: [ docker_username, docker_password ]
build_args:
- DRONE_TAG=${DRONE_TAG}
- DRONE_COMMIT=${DRONE_COMMIT}
tags:
- ${DRONE_TAG}
- latest
when:
event: tag
docker_branch:
image: plugins/docker
repo: umputun/remark42
secrets: [ docker_username, docker_password ]
tags:
- ${DRONE_COMMIT_BRANCH/\//-}
when:
branch:
exclude: [master, release/*]
event: push
docker_pullrequest:
image: docker
commands:
- docker build .
volumes:
- /var/run/docker.sock:/var/run/docker.sock
when:
event: pull_request
deploy:
image: appleboy/drone-ssh
host: remark42.com
username: umputun
port: 22
secrets: [ ssh_key ]
script:
- cd /srv/remark
- docker-compose pull
- docker-compose up -d
when:
branch: master
event: push
notify:
image: drillster/drone-email
host: smtp.mailgun.org
port: 25
username: email_username
password: email_password
from: drone@mg.umputun.com
recipients: [ umputun@gmail.com ]
secrets: [ email_username, email_password ]
when:
status: [ changed, failure ]
+11
View File
@@ -0,0 +1,11 @@
[*]
indent_style = tab
insert_final_newline = true
[*.md]
indent_style = space
trim_trailing_whitespace = false
[*.yml]
indent_size = 2
indent_style = space
+5
View File
@@ -0,0 +1,5 @@
# These owners will be the default owners for everything in the repo.
# Unless a later match takes precedence, @umputun will be requested for
# review when someone opens a pull request.
* @umputun
+4
View File
@@ -0,0 +1,4 @@
# These are supported funding model platforms
github: [umputun]
patreon: remark42
+85
View File
@@ -0,0 +1,85 @@
name: build
on:
push:
branches:
tags:
paths:
- ".github/workflows/ci-build.yml"
- "backend/**"
- "frontend/**"
- ".dockerignore"
- "docker-init.sh"
- "Dockerfile"
- "!**.md"
pull_request:
paths:
- ".github/workflows/ci-build.yml"
- "backend/**"
- "frontend/**"
- ".dockerignore"
- "docker-init.sh"
- "Dockerfile"
- "!**.md"
jobs:
build-images:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: set up QEMU
uses: docker/setup-qemu-action@v1
- name: set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- name: available platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- 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}}
run: |
ref="$(echo ${GITHUB_REF} | cut -d'/' -f3)"
echo GITHUB_REF - $ref
echo ${GITHUB_PACKAGE_TOKEN} | docker login ghcr.io -u ${USERNAME} --password-stdin
echo ${DOCKER_HUB_TOKEN} | docker login -u ${USERNAME} --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} \
--platform linux/amd64,linux/arm/v7,linux/arm64 \
-t ghcr.io/${USERNAME}/remark42:${ref} -t ${USERNAME}/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=$ref, GITHUB_SHA=${GITHUB_SHA}"
echo ${GITHUB_PACKAGE_TOKEN} | docker login ghcr.io -u ${USERNAME} --password-stdin
echo ${DOCKER_HUB_TOKEN} | docker login -u ${USERNAME} --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} \
--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 https://jess.umputun.com/update/remark42-core/${UPDATER_KEY}
@@ -0,0 +1,20 @@
name: frontend
on:
pull_request:
paths:
- ".github/workflows/ci-frontend-size-limit.yml"
- "frontend/**"
- "!**.md"
jobs:
size:
runs-on: ubuntu-latest
env:
CI_JOB_NUMBER: 1
steps:
- uses: actions/checkout@v1
- uses: andresz1/size-limit-action@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
directory: frontend
+107
View File
@@ -0,0 +1,107 @@
name: frontend
on:
push:
branches:
tags:
paths:
- ".github/workflows/ci-frontend.yml"
- "frontend/**"
- "!**.md"
pull_request:
paths:
- ".github/workflows/ci-frontend.yml"
- "frontend/**"
- "!**.md"
jobs:
check-translations:
runs-on: ubuntu-latest
strategy:
matrix:
node: [14.15]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node }}
- run: npm ci --loglevel warn
working-directory: ./frontend
- uses: actions/cache@v2
with:
path: ${{ github.workspace }}/frontend/node_modules/.cache
key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- run: npm run check:translation
working-directory: ./frontend
check-typescript:
runs-on: ubuntu-latest
strategy:
matrix:
node: [14.15]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node }}
- run: npm ci --loglevel warn
working-directory: ./frontend
- uses: actions/cache@v2
with:
path: ${{ github.workspace }}/frontend/node_modules/.cache
key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- run: npm run check:types
working-directory: ./frontend
lint:
runs-on: ubuntu-latest
strategy:
matrix:
node: [14.15]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node }}
- run: npm ci --loglevel warn
working-directory: ./frontend
- run: npx run-p lint
working-directory: ./frontend
test:
runs-on: ubuntu-latest
strategy:
matrix:
node: [14.15]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node }}
- run: npm ci --loglevel warn
working-directory: ./frontend
- run: npm run test:coverage
working-directory: ./frontend
- name: submit coverage
run: node ${{ github.workspace }}/frontend/node_modules/.bin/codecov
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+68
View File
@@ -0,0 +1,68 @@
name: site
on:
push:
branches:
tags:
paths:
- ".github/workflows/ci-site.yml"
- "site/**"
pull_request:
paths:
- ".github/workflows/ci-site.yml"
- "site/**"
jobs:
build-site:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v2
- name: set up QEMU
uses: docker/setup-qemu-action@v1
- name: set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- name: available platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- name: build and deploy master image to ghcr.io and dockerhub
if: ${{ github.ref == 'refs/heads/master' }}
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
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/${USERNAME}/remark24-site:${ref} .
- 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/${USERNAME}/remark24-site:${ref} -t ghcr.io/${USERNAME}/remark24-site:latest .
- name: remote site deployment from master
if: ${{ github.ref == 'refs/heads/master' }}
env:
UPDATER_KEY: ${{ secrets.UPDATER_KEY }}
run: curl https://jess.umputun.com/update/remark42-site/${UPDATER_KEY}
+64
View File
@@ -0,0 +1,64 @@
name: test_backend
on:
push:
branches:
tags:
paths:
- ".github/workflows/ci-test-backend.yml"
- "backend/**"
- "!backend/scripts/**"
- "!**.md"
pull_request:
paths:
- ".github/workflows/ci-test-backend.yml"
- "backend/**"
- "!backend/scripts/**"
- "!**.md"
jobs:
test-backend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: debug if needed
run: if [[ "$DEBUG" == "true" ]]; then env; fi
env:
DEBUG: ${{secrets.DEBUG}}
- name: install go
uses: actions/setup-go@v2
with:
go-version: 1.16
- name: install golangci-lint and goveralls
run: |
curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $GITHUB_WORKSPACE v1.43.0
go get -u github.com/mattn/goveralls
- name: test and lint backend
run: |
go test -race -timeout=60s -covermode=atomic -coverprofile=$GITHUB_WORKSPACE/profile.cov_tmp ./...
cat $GITHUB_WORKSPACE/profile.cov_tmp | grep -v "_mock.go" > $GITHUB_WORKSPACE/profile.cov
$GITHUB_WORKSPACE/golangci-lint --config ${GITHUB_WORKSPACE}/backend/.golangci.yml run --out-format=github-actions ./...
working-directory: backend/app
env:
GOFLAGS: "-mod=vendor"
TZ: "America/Chicago"
- name: test and lint examples
run: |
go version
$GITHUB_WORKSPACE/golangci-lint version
go test -race ./...
$GITHUB_WORKSPACE/golangci-lint --config ${GITHUB_WORKSPACE}/backend/.golangci.yml run --out-format=github-actions ./...
working-directory: backend/_example/memory_store
env:
TZ: "America/Chicago"
- name: submit coverage
run: $(go env GOPATH)/bin/goveralls -service="github" -coverprofile=$GITHUB_WORKSPACE/profile.cov
working-directory: backend
env:
COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+10 -2
View File
@@ -8,9 +8,17 @@ debug
debug.test
.vscode
.idea/
/web/node_modules/
/web/public/
*.prof
*.test
/rest-client.env.json
.DS_Store
.mongo
remark42
/bin/
/backend/var/
/backend/app/var/
compose-private-backend.yml
compose-private-frontend.yml
compose-private.yml
/backend/_example/*/vendor
http-client.env.json
-19
View File
@@ -1,19 +0,0 @@
install:
- docker --version
- docker-compose --version
script:
- docker build
--build-arg COVERALLS_TOKEN=$COVERALLS_TOKEN
--build-arg CI=$CI
--build-arg TRAVIS=$TRAVIS
--build-arg TRAVIS_BRANCH=$TRAVIS_BRANCH
--build-arg TRAVIS_COMMIT=$TRAVIS_COMMIT
--build-arg TRAVIS_JOB_ID=$TRAVIS_JOB_ID
--build-arg TRAVIS_JOB_NUMBER=$TRAVIS_JOB_NUMBER
--build-arg TRAVIS_OS_NAME=$TRAVIS_OS_NAME
--build-arg TRAVIS_PULL_REQUEST=$TRAVIS_PULL_REQUEST
--build-arg TRAVIS_PULL_REQUEST_SHA=$TRAVIS_PULL_REQUEST_SHA
--build-arg TRAVIS_REPO_SLUG=$TRAVIS_REPO_SLUG
--build-arg TRAVIS_TAG=$TRAVIS_TAG
.
+57 -52
View File
@@ -1,77 +1,82 @@
FROM umputun/baseimage:buildgo-latest as build-backend
FROM umputun/baseimage:buildgo-v1.8.0 as build-backend
ARG COVERALLS_TOKEN
ARG CI
ARG TRAVIS
ARG TRAVIS_BRANCH
ARG TRAVIS_COMMIT
ARG TRAVIS_JOB_ID
ARG TRAVIS_JOB_NUMBER
ARG TRAVIS_OS_NAME
ARG TRAVIS_PULL_REQUEST
ARG TRAVIS_PULL_REQUEST_SHA
ARG TRAVIS_REPO_SLUG
ARG TRAVIS_TAG
ARG DRONE
ARG DRONE_TAG
ARG DRONE_COMMIT
ARG DRONE_BRANCH
ARG DRONE_PULL_REQUEST
WORKDIR /go/src/github.com/umputun/remark
ARG SKIP_BACKEND_TEST
ARG BACKEND_TEST_TIMEOUT
ADD app /go/src/github.com/umputun/remark/app
ADD vendor /go/src/github.com/umputun/remark/vendor
ADD .git /go/src/github.com/umputun/remark/.git
ADD backend /build/backend
ADD .git/ /build/backend/.git/
WORKDIR /build/backend
RUN cd app && go test ./...
ENV GOFLAGS="-mod=vendor"
RUN gometalinter --disable-all --deadline=300s --vendor --enable=vet --enable=vetshadow --enable=golint \
--enable=staticcheck --enable=ineffassign --enable=goconst --enable=errcheck --enable=unconvert \
--enable=deadcode --enable=gosimple --enable=gas --exclude=test --exclude=mock --exclude=vendor ./...
RUN mkdir -p target && /script/coverage.sh
RUN if [ -z "$COVERALLS_TOKEN" ] ; then \
echo coverall not enabled ; \
else goveralls -coverprofile=.cover/cover.out -service=travis-ci -repotoken $COVERALLS_TOKEN; fi
# install gcc in order to be able to go test package with -race
RUN apk --no-cache add gcc libc-dev
# run tests
RUN \
version=$(git rev-parse --abbrev-ref HEAD)-$(git describe --abbrev=7 --always --tags)-$(date +%Y%m%d-%H:%M:%S) && \
echo "git version=$version" && \
if [ -z "$DRONE_TAG" ] ; then \
echo "runs outside of drone" ; \
else version=${DRONE_TAG}-${DRONE_COMMIT:0:7}-$(date +%Y%m%d-%H:%M:%S); fi && \
echo "final version=$version" && \
go build -o remark -ldflags "-X main.revision=${version} -s -w" ./app
cd app && \
if [ -z "$SKIP_BACKEND_TEST" ] ; then \
CGO_ENABLED=1 go test -race -p 1 -timeout="${BACKEND_TEST_TIMEOUT:-300s}" -covermode=atomic -coverprofile=/profile.cov_tmp ./... && \
cat /profile.cov_tmp | grep -v "_mock.go" > /profile.cov ; \
golangci-lint run --config ../.golangci.yml ./... ; \
else echo "skip backend tests and linter" ; fi
# if DRONE presented use DRONE_* git env to make version
RUN \
if [ -z "$DRONE" ] ; then echo "runs outside of drone" && version="$(/script/git-rev.sh)" ; \
else version=${DRONE_TAG}${DRONE_BRANCH}${DRONE_PULL_REQUEST}-${DRONE_COMMIT:0:7}-$(date +%Y%m%d-%H:%M:%S) ; fi && \
echo "version=$version" && \
go build -o remark42 -ldflags "-X main.revision=${version} -s -w" ./app
FROM node:9.4-alpine as build-frontend
FROM node:12.16-alpine as build-frontend-deps
ARG CI
ENV HUSKY_SKIP_INSTALL=true
ADD web /srv/web
RUN apk add --no-cache --update git
RUN \
cd /srv/web && \
npm i && npm run build && \
ADD frontend/package.json /srv/frontend/package.json
ADD frontend/package-lock.json /srv/frontend/package-lock.json
RUN cd /srv/frontend && CI=true npm ci --loglevel warn
FROM node:12.16-alpine as build-frontend
ARG CI
ARG SKIP_FRONTEND_TEST
ARG NODE_ENV=production
COPY --from=build-frontend-deps /srv/frontend/node_modules /srv/frontend/node_modules
ADD frontend /srv/frontend
RUN cd /srv/frontend && \
if [ -z "$SKIP_FRONTEND_TEST" ] ; then npx run-p lint test check; \
else echo "skip frontend tests and lint" ; npm run build ; fi && \
rm -rf ./node_modules
FROM umputun/baseimage:app-latest
FROM umputun/baseimage:app-v1.8.0
WORKDIR /srv
ADD scripts/import-disqus.sh /srv/import-disqus.sh
ADD scripts/restore-backup.sh /srv/restore-backup.sh
ADD scripts/migrate-data.sh /srv/migrate-data.sh
ADD scripts/create-backup.sh /srv/create-backup.sh
ADD docker-init.sh /entrypoint.sh
ADD backend/scripts/backup.sh /usr/local/bin/backup
ADD backend/scripts/restore.sh /usr/local/bin/restore
ADD backend/scripts/import.sh /usr/local/bin/import
RUN chmod +x /entrypoint.sh /usr/local/bin/backup /usr/local/bin/restore /usr/local/bin/import
ADD start.sh /srv/start.sh
RUN chmod +x /srv/start.sh /srv/import-disqus.sh /srv/restore-backup.sh /srv/migrate-data.sh /srv/create-backup.sh
COPY --from=build-backend /go/src/github.com/umputun/remark/remark /srv/
COPY --from=build-frontend /srv/web/public/ /srv/web
COPY --from=build-backend /build/backend/remark42 /srv/remark42
COPY --from=build-backend /build/backend/templates /srv
COPY --from=build-frontend /srv/frontend/public/ /srv/web
COPY docker-init.sh /srv/init.sh
RUN chown -R app:app /srv
RUN ln -s /srv/remark42 /usr/bin/remark42
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s CMD curl --fail http://localhost:8080/ping || exit 1
CMD ["/srv/start.sh"]
ENTRYPOINT ["/init.sh"]
RUN chmod +x /srv/init.sh
CMD ["/srv/remark42", "server"]
+87
View File
@@ -0,0 +1,87 @@
FROM node:12.16-alpine as build-frontend-deps
ARG CI
ARG DRONE
ARG DRONE_TAG
ARG DRONE_COMMIT
ARG DRONE_BRANCH
ENV SKIP_FRONTEND_TEST=true
RUN apk add --no-cache --update git
ADD frontend/package.json /srv/frontend/package.json
ADD frontend/package-lock.json /srv/frontend/package-lock.json
RUN cd /srv/frontend && CI=true npm ci
FROM node:12.16-alpine as build-frontend
ARG CI
ARG NODE_ENV=production
ENV SKIP_FRONTEND_TEST=true
ENV HUSKY_SKIP_INSTALL=true
COPY --from=build-frontend-deps /srv/frontend/node_modules /srv/frontend/node_modules
ADD frontend /srv/frontend
RUN cd /srv/frontend && \
npm run build && \
rm -rf ./node_modules
FROM umputun/baseimage:buildgo-latest as build-backend
ARG GITHUB_TOKEN
ENV SKIP_BACKEND_TEST=true
WORKDIR /build/backend
ADD backend /build/backend
ADD README.md /build/
ADD LICENSE /build/
ADD .git/ /build/backend/.git/
COPY --from=build-frontend /srv/frontend/public/ web
RUN \
export WEB_ROOT=/build/backend/web && \
find . -regex '.*\.\(html\|js\|mjs\)$' -print -exec sed -i "s|{% REMARK_URL %}|http://127.0.0.1:8080|g" {} \; && \
statik --src=${WEB_ROOT} --dest=/build/backend/app/rest -p api -f && \
statik --src=/build/backend/templates --dest=/build/backend/app -p templates -ns templates -f && \
ls -la /build/backend/app/templates/statik.go && \
ls -la /build/backend/app/rest/api/statik.go && \
ls -la /build/backend/web/
# if DRONE presented use DRONE_* git env to make version
RUN \
if [ -z "$DRONE" ] ; then \
echo "runs outside of drone" && version=$("/script/version.sh"); \
else version=${DRONE_TAG}${DRONE_BRANCH}${DRONE_PULL_REQUEST}-${DRONE_COMMIT:0:7}-$(date +%Y%m%d-%H:%M:%S); fi && \
echo "version=$version" && \
export GOFLAGS="-mod=vendor" && \
GOOS=linux GOARCH=amd64 go build -o remark42.linux-amd64 -ldflags "-X main.revision=${version} -s -w" ./app && \
GOOS=linux GOARCH=386 go build -o remark42.linux-386 -ldflags "-X main.revision=${version} -s -w" ./app && \
GOOS=linux GOARCH=arm go build -o remark42.linux-arm -ldflags "-X main.revision=${version} -s -w" ./app && \
GOOS=linux GOARCH=arm64 go build -o remark42.linux-arm64 -ldflags "-X main.revision=${version} -s -w" ./app && \
GOOS=windows GOARCH=amd64 go build -o remark42.windows-amd64.exe -ldflags "-X main.revision=${version} -s -w" ./app && \
GOOS=darwin GOARCH=amd64 go build -o remark42.darwin-amd64 -ldflags "-X main.revision=${version} -s -w" ./app && \
GOOS=darwin GOARCH=arm64 go build -o remark42.darwin-arm64 -ldflags "-X main.revision=${version} -s -w" ./app && \
GOOS=freebsd GOARCH=amd64 go build -o remark42.freebsd-amd64 -ldflags "-X main.revision=${version} -s -w" ./app
RUN \
if [ -z "$DRONE_TAG" ] ; then \
echo "runs outside of drone" && tag=""; \
else tag=_${DRONE_TAG}; fi && \
apk add --no-cache --update zip && \
cp ../LICENSE ./LICENSE && cp ../README.md ./README.md && \
tar cvzf remark42${tag}.linux-amd64.tar.gz remark42.linux-amd64 LICENSE README.md && \
tar cvzf remark42${tag}.linux-386.tar.gz remark42.linux-386 LICENSE README.md && \
tar cvzf remark42${tag}.linux-arm.tar.gz remark42.linux-arm LICENSE README.md && \
tar cvzf remark42${tag}.linux-arm64.tar.gz remark42.linux-arm64 LICENSE README.md && \
tar cvzf remark42${tag}.darwin-amd64.tar.gz remark42.darwin-amd64 LICENSE README.md && \
tar cvzf remark42${tag}.darwin-arm64.tar.gz remark42.darwin-arm64 LICENSE README.md && \
tar cvzf remark42${tag}.freebsd-amd64.tar.gz remark42.freebsd-amd64 LICENSE README.md && \
zip remark42${tag}.windows-amd64.zip remark42.windows-amd64.exe LICENSE README.md
FROM alpine
COPY --from=build-backend /build/backend/remark42.* /artifacts/
RUN ls -la /artifacts/*
CMD ["sleep", "100"]
Generated
-237
View File
@@ -1,237 +0,0 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
name = "cloud.google.com/go"
packages = ["compute/metadata"]
revision = "767c40d6a2e058483c25fa193e963a22da17236d"
version = "v0.18.0"
[[projects]]
name = "github.com/PuerkitoBio/goquery"
packages = ["."]
revision = "a86ea073017a6beddef78c8659e7224e8ca634b0"
version = "v1.4.0"
[[projects]]
name = "github.com/andybalholm/cascadia"
packages = ["."]
revision = "901648c87902174f774fac311d7f176f8647bdaa"
version = "v1.0.0"
[[projects]]
name = "github.com/coreos/bbolt"
packages = ["."]
revision = "583e8937c61f1af6513608ccc75c97b6abdf4ff9"
version = "v1.3.0"
[[projects]]
name = "github.com/davecgh/go-spew"
packages = ["spew"]
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
version = "v1.1.0"
[[projects]]
name = "github.com/dgrijalva/jwt-go"
packages = ["."]
revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e"
version = "v3.2.0"
[[projects]]
name = "github.com/didip/tollbooth"
packages = [
".",
"errors",
"libstring",
"limiter"
]
revision = "c95eaa3ddc98f635a91e218b48727fb2e06613ea"
version = "v4.0.0"
[[projects]]
branch = "master"
name = "github.com/didip/tollbooth_chi"
packages = ["."]
revision = "6ab5f3083f3d925e1944d58cdaebf43bbbff9238"
[[projects]]
name = "github.com/go-chi/chi"
packages = [
".",
"middleware"
]
revision = "e83ac2304db3c50cf03d96a2fcd39009d458bc35"
version = "v3.3.2"
[[projects]]
name = "github.com/go-chi/render"
packages = ["."]
revision = "9f855fadd4b8cde7773f9ef51f6b2705af239519"
version = "v1.0.0"
[[projects]]
name = "github.com/golang/protobuf"
packages = ["proto"]
revision = "925541529c1fa6821df4e44ce2723319eb2be768"
version = "v1.0.0"
[[projects]]
name = "github.com/google/uuid"
packages = ["."]
revision = "064e2069ce9c359c118179501254f67d7d37ba24"
version = "0.2"
[[projects]]
name = "github.com/gorilla/feeds"
packages = ["."]
revision = "6edcbcd2d57fd0bbd7f39947a593ed0c06648388"
version = "v1.1.0"
[[projects]]
branch = "master"
name = "github.com/hashicorp/errwrap"
packages = ["."]
revision = "7554cd9344cec97297fa6649b055a8c98c2a1e55"
[[projects]]
branch = "master"
name = "github.com/hashicorp/go-multierror"
packages = ["."]
revision = "b7773ae218740a7be65057fc60b366a49b538a44"
[[projects]]
branch = "master"
name = "github.com/hashicorp/golang-lru"
packages = [
".",
"simplelru"
]
revision = "0fb14efe8c47ae851c0034ed7a448854d3d34cf3"
[[projects]]
branch = "master"
name = "github.com/hashicorp/logutils"
packages = ["."]
revision = "0dc08b1671f34c4250ce212759ebd880f743d883"
[[projects]]
branch = "master"
name = "github.com/jessevdk/go-flags"
packages = ["."]
revision = "1c38ed7ad0cc3d9e66649ac398c30e45f395c4eb"
[[projects]]
branch = "master"
name = "github.com/microcosm-cc/bluemonday"
packages = ["."]
revision = "542fd4642604d0d0c26112396ce5b1a9d01eee0b"
[[projects]]
name = "github.com/patrickmn/go-cache"
packages = ["."]
revision = "a3647f8e31d79543b2d0f0ae2fe5c379d72cedc0"
version = "v2.1.0"
[[projects]]
name = "github.com/pkg/errors"
packages = ["."]
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
version = "v0.8.0"
[[projects]]
name = "github.com/pmezard/go-difflib"
packages = ["difflib"]
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]]
branch = "master"
name = "github.com/shurcooL/sanitized_anchor_name"
packages = ["."]
revision = "86672fcb3f950f35f2e675df2240550f2a50762f"
[[projects]]
name = "github.com/stretchr/testify"
packages = [
"assert",
"require"
]
revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71"
version = "v1.2.1"
[[projects]]
branch = "master"
name = "golang.org/x/image"
packages = [
"draw",
"math/f64"
]
revision = "af66defab954cb421ca110193eed9477c8541e2a"
[[projects]]
branch = "master"
name = "golang.org/x/net"
packages = [
"context",
"context/ctxhttp",
"html",
"html/atom"
]
revision = "f5dfe339be1d06f81b22525fe34671ee7d2c8904"
[[projects]]
branch = "master"
name = "golang.org/x/oauth2"
packages = [
".",
"facebook",
"github",
"google",
"internal",
"jws",
"jwt",
"yandex"
]
revision = "543e37812f10c46c622c9575afd7ad22f22a12ba"
[[projects]]
branch = "master"
name = "golang.org/x/sys"
packages = ["unix"]
revision = "37707fdb30a5b38865cfb95e5aab41707daec7fd"
[[projects]]
branch = "master"
name = "golang.org/x/time"
packages = ["rate"]
revision = "6dc17368e09b0e8634d71cac8168d853e869a0c7"
[[projects]]
name = "google.golang.org/appengine"
packages = [
".",
"internal",
"internal/app_identity",
"internal/base",
"internal/datastore",
"internal/log",
"internal/modules",
"internal/remote_api",
"internal/urlfetch",
"urlfetch"
]
revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a"
version = "v1.0.0"
[[projects]]
name = "gopkg.in/russross/blackfriday.v2"
packages = ["."]
revision = "cadec560ec52d93835bf2f15bd794700d3a2473b"
version = "v2.0.0"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "af8b7f1817ce6e82746722a745184bd50c341733bcc024311aa956ea32475796"
solver-name = "gps-cdcl"
solver-version = 1
-36
View File
@@ -1,36 +0,0 @@
required = ["github.com/patrickmn/go-cache"]
[[constraint]]
name = "github.com/go-chi/chi"
version = "3.0.0"
[[constraint]]
name = "github.com/go-chi/render"
version = "1.0.0"
[[constraint]]
name = "github.com/google/uuid"
version = "0.2.0"
[[constraint]]
branch = "master"
name = "github.com/hashicorp/logutils"
[[constraint]]
branch = "master"
name = "github.com/microcosm-cc/bluemonday"
[[constraint]]
name = "gopkg.in/russross/blackfriday.v2"
version = "2.0.0"
[prune]
go-tests = true
unused-packages = true
[[constraint]]
name = "github.com/patrickmn/go-cache"
version = "2.1.0"
[[constraint]]
branch = "master"
name = "github.com/jessevdk/go-flags"
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2018 Umputun
Copyright (c) 2021 Umputun
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+46
View File
@@ -0,0 +1,46 @@
OS=linux
ARCH=amd64
bin:
docker build -f Dockerfile.artifacts -t remark42.bin .
- @docker rm -f remark42.bin 2>/dev/null || exit 0
docker run -d --name=remark42.bin remark42.bin
docker cp remark42.bin:/artifacts/remark42.$(OS)-$(ARCH) remark42
docker rm -f remark42.bin
docker:
docker build -t umputun/remark42 --build-arg SKIP_FRONTEND_TEST=true --build-arg SKIP_BACKEND_TEST=true .
dockerx:
docker buildx build --build-arg SKIP_FRONTEND_TEST=true --build-arg SKIP_BACKEND_TEST=true \
--progress=plain --platform linux/amd64,linux/arm/v7,linux/arm64 \
-t ghcr.io/umputun/remark42:master -t umputun/remark42:master .
release:
docker build -f Dockerfile.artifacts --no-cache --pull -t remark42.bin .
- @docker rm -f remark42.bin 2>/dev/null || exit 0
- @mkdir -p bin
docker run -d --name=remark42.bin remark42.bin
docker cp remark42.bin:/artifacts/remark42.linux-amd64.tar.gz bin/remark42.linux-amd64.tar.gz
docker cp remark42.bin:/artifacts/remark42.linux-386.tar.gz bin/remark42.linux-386.tar.gz
docker cp remark42.bin:/artifacts/remark42.linux-arm64.tar.gz bin/remark42.linux-arm64.tar.gz
docker cp remark42.bin:/artifacts/remark42.darwin-amd64.tar.gz bin/remark42.darwin-amd64.tar.gz
docker cp remark42.bin:/artifacts/remark42.freebsd-amd64.tar.gz bin/remark42.freebsd-amd64.tar.gz
docker cp remark42.bin:/artifacts/remark42.windows-amd64.zip bin/remark42.windows-amd64.zip
docker rm -f remark42.bin
race_test:
cd backend/app && go test -race -mod=vendor -timeout=60s -count 1 ./...
backend:
docker-compose -f compose-dev-backend.yml build
frontend:
docker-compose -f compose-dev-frontend.yml build
rundev:
docker pull umputun/baseimage:buildgo-latest
SKIP_BACKEND_TEST=true SKIP_FRONTEND_TEST=true docker-compose -f compose-private.yml build
docker-compose -f compose-private.yml up
.PHONY: bin backend
+24 -462
View File
@@ -1,481 +1,43 @@
# remark42 [![Build Status](https://travis-ci.org/umputun/remark.svg?branch=master)](https://travis-ci.org/umputun/remark) [![Go Report Card](https://goreportcard.com/badge/github.com/umputun/remark)](https://goreportcard.com/report/github.com/umputun/remark) [![Coverage Status](https://coveralls.io/repos/github/umputun/remark/badge.svg?branch=master)](https://coveralls.io/github/umputun/remark?branch=master)
# Remark42 [![Build Status](https://github.com/umputun/remark42/workflows/build/badge.svg)](https://github.com/umputun/remark42/actions) [![Go Report Card](https://goreportcard.com/badge/github.com/umputun/remark42)](https://goreportcard.com/report/github.com/umputun/remark42) [![Coverage Status](https://coveralls.io/repos/github/umputun/remark42/badge.svg?branch=master)](https://coveralls.io/github/umputun/remark42?branch=master) [![codecov](https://codecov.io/gh/umputun/remark42/branch/master/graph/badge.svg)](https://app.codecov.io/gh/umputun/remark42)
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.
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, Github and Yandex
* Social login via Google, Twitter, Facebook, Microsoft, GitHub, Yandex, Patreon and Telegram
* Login via email
* Optional anonymous access
* Multi-level nested comments with both tree and plain presentations
* Import from disqus
* Markdown support
* Import from Disqus and WordPress
* Markdown support with friendly formatter toolbar
* Moderator can remove comments and block users
* Voting, pinning and verification system
* Sortable comments
* Images upload with drag-and-drop
* Extractor for recent comments, cross-post
* RSS for all comments and each post
* Export data to json with automatic backups
* Telegram, Slack, Webhook and email notifications for Admins (get notified for each new comment)
* Email and Telegram notifications for users (get notified when someone responds to your comment)
* Export data to JSON with automatic backups
* No external databases, everything embedded in a single data file
* Fully dockerized and can be deployed in a single command
* Clean, lightweight and fully customizable UI
* Self-contained executable can be deployed directly to Linux, Windows and macOS
* Clean, lightweight and customizable UI with white and dark themes
* Multi-site mode from a single instance
* Integration with automatic ssl via [nginx-le](https://github.com/umputun/nginx-le)
* Integration with automatic SSL (direct and via [nginx-le](https://github.com/nginx-le/nginx-le))
* [Privacy focused](#privacy)
## Install
[Demo site](https://remark42.com/demo/) available with all authentication methods, including email auth and anonymous access.
### Backend
<details><summary>Screenshots</summary>
* copy provided `docker-compose.yml` and customize for your needs
* prepare user id for container `` export USER=`id -u $USER` ``
* make sure you **don't keep** `DEV_PASSWD=something...` for any non-development deployments
* pull prepared images from docker hub and start - `docker-compose pull && docker-compose up -d`
* alternatively compile from sources - `docker-compose build && docker-compose up -d`
Comments example:
![](screenshots/comments.png)
#### Parameters
For admin screenshots see [Admin UI wiki](https://github.com/umputun/remark42/wiki/Admin-UI)
</details>
| Command line | Environment | Default | Description |
| ------------------ | ------------------ | --------------------- | ---------------------------------------------- |
| url | REMARK_URL | | url to remark42 server, _required_ |
| secret | SECRET | | secret key, _required_ |
| store.bolt.path | STORE_BOLT_PATH | `./var` | path to data directory |
| store.bolt.timeout | STORE_BOLT_TIMEOUT | `30s` | boltdb access timeout |
| site | SITE | `remark` | site name(s), _multi_ |
| admin | ADMIN | | admin names (list of user ids), _multi_ |
| admin-email | ADMIN_EMAIL | `admin@${REMARK_URL}` | admin email |
| backup | BACKUP_PATH | `./var/backup` | backups location |
| max-back | MAX_BACKUP_FILES | `10` | max backup files to keep |
| cache.max.items | CACHE_MAX_ITEMS | `1000` | max number of cached items, `0` - unlimited |
| cache.max.value | CACHE_MAX_VALUE | `65536` | max size of cached value, `0` - unlimited |
| cache.max.size | CACHE_MAX_SIZE | `50000000` | max size of all cached values, `0` - unlimited |
| avatar.path | AVATAR_FS_PATH | `./var/avatars` | avatars location |
| avatar.rsz-lmt | AVATAR_RSZ_LMT | 0 | max image size for resizing avatars on save |
| max-comment | MAX_COMMENT_SIZE | 2048 | comment's size limit |
| auth.google.cid | AUTH_GOOGLE_CID | | Google OAuth client ID |
| auth.google.csec | AUTH_GOOGLE_CSEC | | Google OAuth client secret |
| auth.facebook.cid | AUTH_FACEBOOK_CID | | Facebook OAuth client ID |
| auth.facebook.csec | AUTH_FACEBOOK_CSEC | | Facebook OAuth client secret |
| auth.github.cid | AUTH_GITHUB_CID | | Github OAuth client ID |
| auth.github.csec | AUTH_GITHUB_CSEC | | Github OAuth client secret |
| auth.yandex.cid | AUTH_YANDEX_CID | | Yandex OAuth client ID |
| auth.yandex.csec | AUTH_YANDEX_CSEC | | Yandex OAuth client secret |
| low-score | LOW_SCORE | `-5` | Low score threshold |
| critical-score | CRITICAL_SCORE | `-10` | Critical score threshold |
| img-proxy | IMG_PROXY | `false` | Enable http->https proxy for images |
| dbg | DEBUG | `false` | debug mode |
| dev-passwd | DEV_PASSWD | | password for `dev` user |
All remark42 documentation is available [by the link](https://remark42.com/docs/).
* command line parameters are long form `--<key>=value`, i.e. `--site=https://demo.remark42.com`
* _multi_ parameters separated by `,` in the environment or repeated with command line key, like `--site=s1 --site=s2 ...`
* _required_ parameters have to be presented in the environment or provided in command line
## Related projects
##### Required parameters
Most of the parameters have sane defaults and don't require customization. There are only a few parameters user has to define:
1. `SECRET` - secret key, can be any long and hard-to-guess string.
1. `REMARK_URL` - url pointing to your remark42 server, i.e. `https://demo.reamark42.com`
2. At least one pair of `AUTH_<PROVIDER>_CID` and `AUTH_<PROVIDER>_CSEC` defining oauth2 provider(s)
The minimal `docker-compose.yml` has to include all required parameters:
```yaml
version: '2'
services:
remark42:
image: umputun/remark42:master
restart: always
container_name: "remark42"
environment:
- REMARK_URL=https://demo.remark42.com # url pointing to your remark42 server
- SECRET=abcd-123456-xyz-$%^& # secret key
- AUTH_GITHUB_CID=12345667890 # oauth2 client ID
- AUTH_GITHUB_CSEC=abcdefg12345678 # oauth2 client secret
- USER=1001 # UID on the host machine, i.e `id -u`
volumes:
- ./var:/srv/var # persistent volume to store all remark42 data
```
#### Register oauth2 providers
Authentication handled by external providers. You should setup oauth2 for all (or some) of them to allow users to make comments. It is not mandatory to have all of them, but at least one should be correctly configured.
##### Google Auth Provider
1. Create a new project: https://console.developers.google.com/project
1. Choose the new project from the top right project dropdown (only if another project is selected)
1. In the project Dashboard center pane, choose **"API Manager"**
1. In the left Nav pane, choose **"Credentials"**
1. In the center pane, choose **"OAuth consent screen"** tab. Fill in **"Product name shown to users"** and hit save.
1. In the center pane, choose **"Credentials"** tab.
* Open the **"New credentials"** drop down
* Choose **"OAuth client ID"**
* Choose **"Web application"**
* Application name is freeform, choose something appropriate
* Authorized origins is your domain ex: `https://remark42.mysite.com`
* Authorized redirect URIs is the location of oauth2/callback constructed as domain + `/auth/google/callback`, ex: `https://remark42.mysite.com/auth/google/callback`
* Choose **"Create"**
1. Take note of the **Client ID** and **Client Secret**
_instructions for google oauth2 setup borrowed from [oauth2_proxy](https://github.com/bitly/oauth2_proxy)_
##### GitHub Auth Provider
1. Create a new **"OAuth App"**: https://github.com/settings/developers
1. Fill **"Application Name"** and **"Homepage URL"** for your site
1. Under **"Authorization callback URL"** enter the correct url constructed as domain + `/auth/github/callback`. ie `https://remark42.mysite.com/auth/github/callback`
1. Take note of the **Client ID** and **Client Secret**
##### Facebook Auth Provider
1. From https://developers.facebook.com select **"My Apps"** / **"Add a new App"**
1. Set **"Display Name"** and **"Contact email"**
1. Choose **"Facebook Login"** and then **"Web"**
1. Set "Site URL" to your domain, ex: `https://remark42.mysite.com`
1. Under **"Facebook login"** / **"Settings"** fill "Valid OAuth redirect URIs" with your callback url constructed as domain + `/auth/facebook/callback`
1. Select **"App Review"** and turn public flag on. This step may ask you to provide a link to your privacy policy.
##### Yandex Auth Provider
1. Create a new **"OAuth App"**: https://oauth.yandex.com/client/new
1. Fill **"App name"** for your site
1. Under **Platforms** select **"Web services"** and enter **"Callback URI #1"** constructed as domain + `/auth/yandex/callback`. ie `https://remark42.mysite.com/auth/yandex/callback`
1. Select **Permissions**. You need following permissions only from the **"Yandex.Passport API"** section:
* Access to user avatar
* Access to username, first name and surname, gender
1. Fill out the rest of fields if needed
1. Take note of the **ID** and **Password**
For more details refer to [Yandex OAuth](https://tech.yandex.com/oauth/doc/dg/concepts/about-docpage/) and [Yandex.Passport](https://tech.yandex.com/passport/doc/dg/index-docpage/) API documentation.
#### Initial import from Disqus
1. Disqus provides an export of all comments on your site in a g-zipped file. This is found in your Moderation panel at Disqus Admin > Setup > Export. The export will be sent into a queue and then emailed to the address associated with your account once it's ready. Direct link to export will be something like `https://<siteud>.disqus.com/admin/discussions/export/`. See [importing-exporting](https://help.disqus.com/customer/portal/articles/1104797-importing-exporting) for more details.
2. Move this file to your remark42 host within `./var` and unzip, i.e. `gunzip <disqus-export-name>.xml.gz`.
3. Run import command - `docker-compose exec remark42 /srv/import-disqus.sh <disqus-export-name>.xml <your site id>`
#### Backup and restore
##### Automatic backups
Remark42 by default makes daily backup files under `${BACKUP_PATH}` (default `./var/backup`). Backups kept up to `${MAX_BACKUP_FILES}` (default 10). Each backup file contains exported and gzipped content, i.e., all comments. At any point, the user can restore such backup and revert all comments to the desirable state. Note: restore procedure cleans the current data store and replaces all comments with comments from the backup file.
For safety and security reasons restore functionality not exposed outside of your server by default. The recommended way to restore from the backup is to use provided `scripts/restore-backup.sh`. It can run inside the container:
`docker-compose exec remark42 /srv/restore-backup.sh {backup-filename.gz} {your site id}`
##### Schema migration
One special case for backup/restore is schema migration. Some versions or remark42 may extend or change the schema
and for such upgrades migration required. Provided migration script `scripts/migrate-data.sh` makes a fresh backup and then loads it back to your remark42 instance.
`docker-compose exec remark42 /srv/migrate-data.sh {your site id}`
##### Manual backup
In addition to automatic backups user can make a backup manually. This command makes `userbackup-{site id}-{timestamp}.gz`
`docker-compose exec remark42 /srv/create-backup.sh {your site id}`
##### Backup format
Backup file is a text file with all exported comments separated by EOL. Each backup record is a valid json with all key/value
unmarshaled from `Comment` struct (see below).
#### Admin users
Admins/moderators should be defined in `docker-compose.yml` as a list of user IDs or passed in the command line.
```
environment:
- ADMIN=github_ef0f706a79cc24b17bbbb374cd234a691a034128,github_dae9983158e9e5e127ef2b87a411ef13c891e9e5
```
To get user id just login and click on your username or any other user you want to promote to admins.
It will expand login info and show full user ID.
### Frontend
Frontend part is building automatically along with backend if you use `docker-compose`.
For manual building:
* install [Node.js 8](https://nodejs.org/en/) or higher;
* run `npm install` inside `./web`;
* run `npm run build` there;
* result files will be saved in `./web/public`.
For development mode use `npm start` instead of `npm run build`.
In this case `webpack` will serve files using `webpack-dev-server` on `localhost:8080`.
URLs for development:
* `localhost:8080` — page with embedded script from `REMARK_URL` (default: `https://demo.remark42.com`);
* `localhost:8080/dev.html` — page with embedded script from local folder;
* `localhost:8080/last-comments.html` — page with embedded script for last comments;
* `localhost:8080/counter.html` — page with embedded script for counter with examples.
#### Usage
##### Comments
It's a main widget which renders list of comments.
Add this snippet to the bottom of web page:
```html
<script>
var remark_config = {
site_id: 'YOUR_SITE_ID',
url: 'PAGE_URL', // optional param; if it isn't defined window.location.href will be used
max_shown_comments: 10, // optional param; if it isn't defined default value (15) will be used
};
(function() {
var d = document, s = d.createElement('script');
s.src = '/web/embed.js'; // prepends this address with domain where remark42 is placed
(d.head || d.body).appendChild(s);
})();
</script>
```
And then add this node in the place where you want to see Remark42 widget:
```html
<div id="remark42"></div>
```
After that widget will be rendered inside this node.
##### Last comments
It's a widget which renders list of last comments from your site.
Add this snippet to the bottom of web page:
```html
<script>
var remark_config = {
site_id: 'YOUR_SITE_ID',
};
(function() {
var d = document, s = d.createElement('script');
s.src = '/web/last-comments.js'; // prepends this address with domain where remark42 is placed
(d.head || d.body).appendChild(s);
})();
</script>
```
And then add this node in the place where you want to see last comments widget:
```html
<div class="remark42__last-comments" data-max="50"></div>
```
`data-max` sets the max amount of comments (default: `15`).
##### Counter
It's a widget which renders a number of comments for the specified page.
Add this snippet to the bottom of web page:
```html
<script>
var remark_config = {
site_id: 'YOUR_SITE_ID',
};
(function() {
var d = document, s = d.createElement('script');
s.src = '/web/counter.js'; // prepends this address with domain where remark42 is placed
(d.head || d.body).appendChild(s);
})();
</script>
```
And then add a node like this in the place where you want to see a number of comments:
```html
<span class="remark42__counter" data-url="https://domain.com/path/to/article/"></span>
```
You can use as many nodes like this as you need to.
The script will found all them by the class `remark__counter`,
and it will use `data-url` attribute to define the page with comments.
Also script can uses `url` property from `remark_config` object, or `window.location.href` if nothing else is defined.
## API
### Authorization
* `GET /auth/{provider}/login?from=http://url&session=1` - perform "social" login with one of supported providers and redirect to `url`. Presence of `session` (any non-zero value) change the default cookie expiration and makes them session-only.
* `GET /auth/logout` - logout
```go
type User struct {
Name string `json:"name"`
ID string `json:"id"`
Picture string `json:"picture"`
Admin bool `json:"admin"`
Blocked bool `json:"block"`
Verified bool `json:"verified"`
}
```
_currently supported providers are `google`, `facebook`, `github` and `yandex`_
### Commenting
* `POST /api/v1/comment` - add a comment. _auth required_
```go
type Comment struct {
ID string `json:"id"` // comment ID, read only
ParentID string `json:"pid"` // parent ID
Text string `json:"text"` // comment text, after md processing
Orig string `json:"orig"` // original comment text
User User `json:"user"` // user info, read only
Locator Locator `json:"locator"` // post locator
Score int `json:"score"` // comment score, read only
Votes map[string]bool `json:"votes"` // comment votes, read only
Timestamp time.Time `json:"time"` // time stamp, read only
Pin bool `json:"pin"` // pinned status, read only
Delete bool `json:"delete"` // delete status, read only
}
type Locator struct {
SiteID string `json:"site"` // site id
URL string `json:"url"` // post url
}
```
* `POST /api/v1/preview` - preview comment in html. Body is `Comment` to render
* `GET /api/v1/find?site=site-id&url=post-url&sort=fld&format=tree|plain` - find all comments for given post
This is the primary call used by UI to show comments for given post. It can return comments in two formats - `plain` and `tree`.
In plain format result will be sorted list of `Comment`. In tree format this is going to be tree-like object with this structure:
```go
type Tree struct {
Nodes []Node `json:"comments"`
Info store.PostInfo `json:"info,omitempty"`
}
type Node struct {
Comment store.Comment `json:"comment"`
Replies []Node `json:"replies,omitempty"`
}
```
Sort can be `time`, `active` or `score`. Supported sort order with prefix -/+, i.e. `-time`. For `tree` mode sort will be applied to top-level comments only and all replies always sorted by time.
* `PUT /api/v1/comment/{id}?site=site-id&url=post-url` - edit comment, allowed once in 5min since creation
```json
Content-Type: application/json
{
"text": "edit comment blah http://radio-t.com 12345",
"summary": "fix blah"
}
```
* `GET /api/v1/last/{max}?site=site-id` - get up to `{max}` last comments
* `GET /api/v1/id/{id}?site=site-id` - get comment by `comment id`
* `GET /api/v1/comments?site=site-id&user=id&limit=N` - get comment by `user id`, returns `response` object
```go
type response struct {
Comments []store.Comment `json:"comments"`
Count int `json:"count"`
}{}
```
* `GET /api/v1/count?site=site-id&url=post-url` - get comment's count for `{url}`
* `GET /api/v1/list?site=site-id&limit=5&skip=2` - list commented posts, returns array or `PostInfo`, limit=0 will return all posts
```go
type PostInfo struct {
URL string `json:"url"`
Count int `json:"count"`
ReadOnly bool `json:"read_only,omitempty"`
FirstTS time.Time `json:"first_time,omitempty"`
LastTS time.Time `json:"last_time,omitempty"`
}
```
* `GET /api/v1/user` - get user info, _auth required_
* `PUT /api/v1/vote/{id}?site=site-id&url=post-url&vote=1` - vote for comment. `vote`=1 will increase score, -1 decrease. _auth required_
* `GET /api/v1/userdata?site=site-id` - export all user data to gz stream _auth required_
* `POST /api/v1/deleteme?site=site-id` - request deletion of user data. _auth required_
* `GET /api/v1/config?site=site-id` - returns configuration (parameters) for given site
```go
type config struct {
Version string `json:"version"`
EditDuration int `json:"edit_duration"` // seconds
Admins []string `json:"admins"`
Auth []string `json:"auth_providers"`
LowScore int `json:"low_score"`
CriticalScore int `json:"critical_score"`
}
```
* `GET /api/v1/info?site=site-idd&url=post-ur` - returns `PostInfo` for site and url
### RSS feeds
* `GET /api/v1/rss/post?site=site-id&url=post-url` - rss feed for a post
* `GET /api/v1/rss/site?site=site-id` - rss feed for given site
### Admin
* `DELETE /api/v1/admin/comment/{id}?site=site-id&url=post-url` - delete comment by `id`.
* `PUT /api/v1/admin/user/{userid}?site=site-id&block=1` - block or unblock user.
* `GET api/v1/admin/blocked&site=site-id` - list of blocked user ids.
```go
type BlockedUser struct {
ID string `json:"id"`
Name string `json:"name"`
Timestamp time.Time `json:"time"`
}
```
* `GET /api/v1/admin/export?site=side-id&mode=[stream|file]` - export all comments to json stream or gz file.
* `POST /api/v1/admin/import?site=side-id` - import comments from the backup.
* `PUT /api/v1/admin/pin/{id}?site=site-id&url=post-url&pin=1` - pin or unpin comment.
* `DELETE /api/v1/admin/user/{userid}?site=site-id&block=1` - delete all user's comments.
* `PUT /api/v1/admin/readonly?site=site-id&url=post-url&ro=1` - set read-only status
* `PUT /api/v1/admin/verify/{userid}?site=site-id&verified=1` - set verified status
* `GET /api/v1/admin/deleteme?token=token` - process deleteme user's request
_all admin calls require auth and admin privilege_
## Privacy
* Remark42 is trying to be very sensitive to any private or semi-private information.
* Authentication requesting the lowest (minimal) possible scope from providers. All extra information returned by them dropped immediately and not stored in any form.
* Generally remark42 keeps user id, username and avatar link only. None of these fields exposed directly - id and name hashed, avatar proxied.
* There is no tracking of any sort.
* Login mechanic uses JWT stored in a cookie (httpOnly, secured). The second cookie (XSRF_TOKEN) is a random id preventing Cross-Site Request Forgery
* There is no cross-site login, i.e., user's behavior can't be analyzed across independent sites running remark42.
* There are no third-party analytic services involved.
* User can request all information remark42 knows about and export to gz file.
* Supported complete cleanup of all information related to user activity on demand.
* Cookie lifespan can be restricted to session-only.
* All potentially sensitive data stored by remark42 hashed and encrypted.
## Technical details
* Data stored in [boltdb](https://github.com/coreos/bbolt) (embedded key/value database) files under `BOLTDB_PATH`
* Each site stored in a separate boltbd file.
* In order to migrate/move remark42 to another host boltbd files as well as avatars directory `AVATAR_STORE` should be transferred.
* Automatic backup process runs every 24h and exports all content in json-like format to `backup-remark-YYYYMMDD.gz`.
* Authentication implemented with [jwt](https://github.com/dgrijalva/jwt-go) stored in a cookie. It uses HttpOnly, secure cookies.
* All heavy REST calls cached internally in LRU cache limited by `MAX_CACHE_ITEMS` and `MAX_CACHE_SIZE`.
* User's activity throttled globally (up to 1000 simultaneous requests) and limited locally (per user, usually up to 10 req/sec)
* Request timeout set to 60sec
* Development mode (`--dev-password` set) allows to test remark42 without social login and with admin privileges. Adds basic-auth for username: `dev`, password: `${DEV_PASSWD}`. **should not be used in production deployment**
* User can vote for the comment multiple times but only to change his/her vote. Double-voting not allowed.
* User can edit comments in 5 mins window after creation.
* User ID hashed and prefixed by oauth provider name to avoid collisions and potential abuse.
* All avatars cached locally to prevent rate limiters from google/github/facebook/yandex.
* Docker build uses [publicly available](https://github.com/umputun/baseimage) base images.
* [A Helm chart for Remark42 on Kubernetes](https://github.com/groundhog2k/helm-charts/tree/master/charts/remark42)
* [django-remark42](https://github.com/andrewp-as-is/django-remark42.py)
+15
View File
@@ -0,0 +1,15 @@
# Security Policy
## Supported Versions
We release patches for security vulnerabilities.
| Version | Supported |
| ------- | ------------------ |
| current | :white_check_mark: |
| >=1.6.x | :white_check_mark: |
| <1.5.x | :x: |
## Reporting a Vulnerability
Please report (suspected) security vulnerabilities to umputun@gmail.com. You will receive a response from us within 48 hours. If the issue is confirmed, we will release a patch as soon as possible depending on complexity but historically within a few days.
-398
View File
@@ -1,398 +0,0 @@
package main
import (
"context"
"fmt"
"log"
"net/http"
"net/url"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/coreos/bbolt"
"github.com/hashicorp/logutils"
"github.com/jessevdk/go-flags"
"github.com/pkg/errors"
"github.com/umputun/remark/app/migrator"
"github.com/umputun/remark/app/rest/api"
"github.com/umputun/remark/app/rest/auth"
"github.com/umputun/remark/app/rest/cache"
"github.com/umputun/remark/app/rest/proxy"
"github.com/umputun/remark/app/store/engine"
"github.com/umputun/remark/app/store/service"
)
// Opts with command line flags and env
// nolint:maligned
type Opts struct {
SecretKey string `long:"secret" env:"SECRET" required:"true" description:"secret key"`
RemarkURL string `long:"url" env:"REMARK_URL" required:"true" description:"url to remark"`
Store StoreGroup `group:"store" namespace:"store" env-namespace:"STORE"`
Avatar AvatarGroup `group:"avatar" namespace:"avatar" env-namespace:"AVATAR"`
Cache CacheGroup `group:"cache" namespace:"cache" env-namespace:"CACHE"`
Sites []string `long:"site" env:"SITE" default:"remark" description:"site names" env-delim:","`
Admins []string `long:"admin" env:"ADMIN" description:"admin(s) names" env-delim:","`
AdminEmail string `long:"admin-email" env:"ADMIN_EMAIL" default:"" description:"admin email"`
DevPasswd string `long:"dev-passwd" env:"DEV_PASSWD" default:"" description:"development mode password"`
BackupLocation string `long:"backup" env:"BACKUP_PATH" default:"./var/backup" description:"backups location"`
MaxBackupFiles int `long:"max-back" env:"MAX_BACKUP_FILES" default:"10" description:"max backups to keep"`
ImageProxy bool `long:"img-proxy" env:"IMG_PROXY" description:"enable image proxy"`
MaxCommentSize int `long:"max-comment" env:"MAX_COMMENT_SIZE" default:"2048" description:"max comment size"`
LowScore int `long:"low-score" env:"LOW_SCORE" default:"-5" description:"low score threshold"`
CriticalScore int `long:"critical-score" env:"CRITICAL_SCORE" default:"-10" description:"critical score threshold"`
ReadOnlyAge int `long:"read-age" env:"READONLY_AGE" default:"0" description:"read-only age of comments"`
Port int `long:"port" env:"REMARK_PORT" default:"8080" description:"port"`
WebRoot string `long:"web-root" env:"REMARK_WEB_ROOT" default:"./web" description:"web root directory"`
Dbg bool `long:"dbg" env:"DEBUG" description:"debug mode"`
Auth struct {
Google AuthGroup `group:"google" namespace:"google" env-namespace:"GOOGLE" description:"Google OAuth"`
Github AuthGroup `group:"github" namespace:"github" env-namespace:"GITHUB" description:"Github OAuth"`
Facebook AuthGroup `group:"facebook" namespace:"facebook" env-namespace:"FACEBOOK" description:"Facebook OAuth"`
Yandex AuthGroup `group:"yandex" namespace:"yandex" env-namespace:"YANDEX" description:"Yandex OAuth"`
} `group:"auth" namespace:"auth" env-namespace:"AUTH"`
}
// AuthGroup defines options group for auth params
type AuthGroup struct {
CID string `long:"cid" env:"CID" description:"OAuth client ID"`
CSEC string `long:"csec" env:"CSEC" description:"OAuth client secret"`
}
// StoreGroup defines options group for store params
type StoreGroup struct {
Type string `long:"type" env:"TYPE" description:"type of storage" choice:"bolt" choice:"mongo" default:"bolt"`
Bolt struct {
Path string `long:"path" env:"PATH" default:"./var" description:"parent dir for bolt files"`
Timeout time.Duration `long:"timeout" env:"TIMEOUT" default:"30s" description:"bolt timeout"`
} `group:"bolt" namespace:"bolt" env-namespace:"BOLT"`
}
// AvatarGroup defines options group for avatar params
type AvatarGroup struct {
Type string `long:"type" env:"TYPE" description:"type of avatar storage" choice:"fs" choice:"mongo" default:"fs"`
FS struct {
Path string `long:"path" env:"PATH" default:"./var/avatars" description:"avatars location"`
} `group:"fs" namespace:"fs" env-namespace:"FS"`
RszLmt int `long:"rsz-lmt" env:"RSZ_LMT" default:"0" description:"max image size for resizing avatars on save"`
}
// CacheGroup defines options group for cache params
type CacheGroup struct {
Type string `long:"type" env:"TYPE" description:"type of cache" choice:"mem" choice:"redis" default:"mem"`
Max struct {
Items int `long:"items" env:"ITEMS" default:"1000" description:"max cached items"`
Value int `long:"value" env:"VALUE" default:"65536" description:"max size of cached value"`
Size int64 `long:"size" env:"SIZE" default:"50000000" description:"max size of total cache"`
} `group:"max" namespace:"max" env-namespace:"MAX"`
}
var revision = "unknown"
// Application holds all active objects
type Application struct {
Opts
restSrv *api.Rest
migratorSrv *api.Migrator
exporter migrator.Exporter
terminated chan struct{}
}
func main() {
fmt.Printf("remark %s\n", revision)
var opts Opts
p := flags.NewParser(&opts, flags.Default)
if _, e := p.ParseArgs(os.Args[1:]); e != nil {
os.Exit(1)
}
setupLog(opts.Dbg)
log.Print("[INFO] started remark")
resetEnv("SECRET", "AUTH_GOOGLE_CSEC", "AUTH_GITHUB_CSEC", "AUTH_FACEBOOK_CSEC", "AUTH_YANDEX_CSEC")
ctx, cancel := context.WithCancel(context.Background())
go func() { // catch signal and invoke graceful termination
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
<-stop
log.Print("[WARN] interrupt signal")
cancel()
}()
app, err := New(opts)
if err != nil {
log.Fatalf("[ERROR] failed to setup application, %+v", err)
}
err = app.Run(ctx)
log.Printf("[INFO] remark terminated %s", err)
}
// New prepares application and return it with all active parts
// doesn't start anything
func New(opts Opts) (*Application, error) {
if err := makeDirs(opts.BackupLocation); err != nil {
return nil, err
}
if !strings.HasPrefix(opts.RemarkURL, "http://") && !strings.HasPrefix(opts.RemarkURL, "https://") {
return nil, errors.Errorf("invalid remark42 url %s", opts.RemarkURL)
}
boltStore, err := makeDataStore(opts.Store, opts.Sites)
if err != nil {
return nil, err
}
dataService := &service.DataStore{
Interface: boltStore,
EditDuration: 5 * time.Minute,
Secret: opts.SecretKey,
MaxCommentSize: opts.MaxCommentSize,
}
loadingCache, err := cache.NewMemoryCache(cache.MaxCacheSize(opts.Cache.Max.Size), cache.MaxValSize(opts.Cache.Max.Value),
cache.MaxKeys(opts.Cache.Max.Items), cache.PostFlushFn(postFlushFn(opts.Sites, opts.Port)))
if err != nil {
return nil, err
}
jwtService := auth.NewJWT(opts.SecretKey, strings.HasPrefix(opts.RemarkURL, "https://"), 7*24*time.Hour)
avatarStore, err := makeAvatarStore(opts.Avatar)
if err != nil {
return nil, errors.Wrap(err, "failed to make avatar store")
}
avatarProxy := &proxy.Avatar{
Store: avatarStore,
RoutePath: "/api/v1/avatar",
RemarkURL: strings.TrimSuffix(opts.RemarkURL, "/"),
}
exporter := &migrator.Remark{DataStore: dataService}
migr := &api.Migrator{
Version: revision,
Cache: loadingCache,
NativeImporter: &migrator.Remark{DataStore: dataService},
DisqusImporter: &migrator.Disqus{DataStore: dataService},
NativeExported: &migrator.Remark{DataStore: dataService},
SecretKey: opts.SecretKey,
}
srv := &api.Rest{
Version: revision,
DataService: dataService,
Exporter: exporter,
WebRoot: opts.WebRoot,
RemarkURL: opts.RemarkURL,
ImageProxy: &proxy.Image{Enabled: opts.ImageProxy, RoutePath: "/api/v1/img", RemarkURL: opts.RemarkURL},
AvatarProxy: avatarProxy,
ReadOnlyAge: opts.ReadOnlyAge,
Authenticator: auth.Authenticator{
JWTService: jwtService,
Admins: opts.Admins,
AdminEmail: opts.AdminEmail,
Providers: makeAuthProviders(jwtService, avatarProxy, dataService, opts),
DevPasswd: opts.DevPasswd,
},
Cache: loadingCache,
}
// no admin email, use admin@domain
if srv.Authenticator.AdminEmail == "" {
if u, err := url.Parse(opts.RemarkURL); err == nil {
srv.Authenticator.AdminEmail = "admin@" + u.Host
}
}
srv.ScoreThresholds.Low, srv.ScoreThresholds.Critical = opts.LowScore, opts.CriticalScore
tch := make(chan struct{})
return &Application{restSrv: srv, migratorSrv: migr, exporter: exporter, Opts: opts, terminated: tch}, nil
}
// Run all application objects
func (a *Application) Run(ctx context.Context) error {
if a.DevPasswd != "" {
log.Printf("[WARN] running in dev mode")
}
go func() {
// shutdown on context cancellation
<-ctx.Done()
a.restSrv.Shutdown()
a.migratorSrv.Shutdown()
}()
a.activateBackup(ctx) // runs in goroutine for each site
go a.migratorSrv.Run(a.Port + 1)
a.restSrv.Run(a.Port)
close(a.terminated)
return nil
}
// Wait for application completion (termination)
func (a *Application) Wait() {
<-a.terminated
}
// activateBackup runs background backups for each site
func (a *Application) activateBackup(ctx context.Context) {
for _, siteID := range a.Sites {
backup := migrator.AutoBackup{
Exporter: a.exporter,
BackupLocation: a.BackupLocation,
SiteID: siteID,
KeepMax: a.MaxBackupFiles,
Duration: 24 * time.Hour,
}
go backup.Do(ctx)
}
}
// makeDataStore creates store for all sites
func makeDataStore(group StoreGroup, siteNames []string) (result engine.Interface, err error) {
switch group.Type {
case "bolt":
if err = makeDirs(group.Bolt.Path); err != nil {
return nil, err
}
sites := []engine.BoltSite{}
for _, site := range siteNames {
sites = append(sites, engine.BoltSite{SiteID: site, FileName: fmt.Sprintf("%s/%s.db", group.Bolt.Path, site)})
}
result, err = engine.NewBoltDB(bolt.Options{Timeout: group.Bolt.Timeout}, sites...)
default:
return nil, errors.Errorf("unsupported store type %s", group.Type)
}
return result, errors.Wrap(err, "can't initialize data store")
}
func makeAvatarStore(group AvatarGroup) (result proxy.AvatarStore, err error) {
switch group.Type {
case "fs":
if err = makeDirs(group.FS.Path); err != nil {
return nil, err
}
return proxy.NewFSAvatarStore(group.FS.Path, group.RszLmt), nil
}
return nil, errors.Errorf("unsupported avatart store type %s", group.Type)
}
// mkdir -p for all dirs
func makeDirs(dirs ...string) error {
// exists returns whether the given file or directory exists or not
exists := func(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return true, err
}
for _, dir := range dirs {
ex, err := exists(dir)
if err != nil {
return errors.Wrapf(err, "can't check directory status for %s", dir)
}
if !ex {
if e := os.MkdirAll(dir, 0700); e != nil {
return errors.Wrapf(err, "can't make directory %s", dir)
}
}
}
return nil
}
func makeAuthProviders(jwtService *auth.JWT, avatarProxy *proxy.Avatar, ds *service.DataStore, opts Opts) []auth.Provider {
makeParams := func(cid, secret string) auth.Params {
return auth.Params{
JwtService: jwtService,
AvatarProxy: avatarProxy,
RemarkURL: opts.RemarkURL,
Cid: cid,
Csecret: secret,
Admins: opts.Admins,
SecretKey: opts.SecretKey,
IsVerifiedFn: ds.IsVerifiedFn(),
}
}
providers := []auth.Provider{}
if opts.Auth.Google.CID != "" && opts.Auth.Google.CSEC != "" {
providers = append(providers, auth.NewGoogle(makeParams(opts.Auth.Google.CID, opts.Auth.Google.CSEC)))
}
if opts.Auth.Github.CID != "" && opts.Auth.Github.CSEC != "" {
providers = append(providers, auth.NewGithub(makeParams(opts.Auth.Github.CID, opts.Auth.Github.CSEC)))
}
if opts.Auth.Facebook.CID != "" && opts.Auth.Facebook.CSEC != "" {
providers = append(providers, auth.NewFacebook(makeParams(opts.Auth.Facebook.CID, opts.Auth.Facebook.CSEC)))
}
if opts.Auth.Yandex.CID != "" && opts.Auth.Yandex.CSEC != "" {
providers = append(providers, auth.NewYandex(makeParams(opts.Auth.Yandex.CID, opts.Auth.Yandex.CSEC)))
}
if len(providers) == 0 {
log.Printf("[WARN] no auth providers defined")
}
return providers
}
// post-flush callback invoked by cache after each flush in async way
func postFlushFn(sites []string, port int) func() {
return func() {
// list of heavy urls for pre-heating on cache change
urls := []string{
"http://localhost:%d/api/v1/list?site=%s",
"http://localhost:%d/api/v1/last/50?site=%s",
}
for _, site := range sites {
for _, u := range urls {
resp, err := http.Get(fmt.Sprintf(u, port, site))
if err != nil {
log.Printf("[WARN] failed to refresh cached list for %s, %s", site, err)
return
}
if err = resp.Body.Close(); err != nil {
log.Printf("[WARN] failed to close response body, %s", err)
}
}
}
}
}
func resetEnv(envs ...string) {
for _, env := range envs {
if err := os.Unsetenv(env); err != nil {
log.Printf("[WARN] can't unset env %s, %s", env, err)
}
}
}
func setupLog(dbg bool) {
filter := &logutils.LevelFilter{
Levels: []logutils.LogLevel{"DEBUG", "INFO", "WARN", "ERROR"},
MinLevel: logutils.LogLevel("INFO"),
Writer: os.Stdout,
}
log.SetFlags(log.Ldate | log.Ltime)
if dbg {
log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds | log.Lshortfile)
filter.MinLevel = logutils.LogLevel("DEBUG")
}
log.SetOutput(filter)
}
-135
View File
@@ -1,135 +0,0 @@
package main
import (
"context"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"strings"
"syscall"
"testing"
"time"
flags "github.com/jessevdk/go-flags"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestApplication(t *testing.T) {
app, ctx := prepApp(t, 18080, 500*time.Millisecond)
go func() { _ = app.Run(ctx) }()
time.Sleep(100 * time.Millisecond) // let server start
// send ping
resp, err := http.Get("http://localhost:18080/api/v1/ping")
require.Nil(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
body, err := ioutil.ReadAll(resp.Body)
assert.Nil(t, err)
assert.Equal(t, "pong", string(body))
// add comment
resp, err = http.Post("http://dev:password@localhost:18080/api/v1/comment", "json",
strings.NewReader(`{"text": "test 123", "locator":{"url": "https://radio-t.com/blah1", "site": "remark"}}`))
require.Nil(t, err)
assert.Equal(t, http.StatusCreated, resp.StatusCode)
body, _ = ioutil.ReadAll(resp.Body)
t.Log(string(body))
assert.Equal(t, "admin@demo.remark42.com", app.restSrv.Authenticator.AdminEmail, "default admin email")
app.Wait()
}
func TestApplicationFailed(t *testing.T) {
opts := Opts{}
p := flags.NewParser(&opts, flags.Default)
// RO bolt location
_, err := p.ParseArgs([]string{"--secret=123456", "--url=https://demo.remark42.com", "--store.bolt.path=/dev/null"})
assert.Nil(t, err)
_, err = New(opts)
assert.EqualError(t, err, "can't initialize data store: failed to make boltdb for /dev/null/remark.db: "+
"open /dev/null/remark.db: not a directory")
t.Log(err)
// RO backup location
opts = Opts{}
_, err = p.ParseArgs([]string{"--secret=123456", "--url=https://demo.remark42.com", "--store.bolt.path=/tmp",
"--backup=/dev/null/not-writable"})
assert.Nil(t, err)
_, err = New(opts)
assert.EqualError(t, err, "can't check directory status for /dev/null/not-writable: stat /dev/null/not-writable: not a directory")
t.Log(err)
// invalid url
opts = Opts{}
_, err = p.ParseArgs([]string{"--secret=123456", "--url=demo.remark42.com", "----store.bolt.path=/tmp"})
assert.Nil(t, err)
_, err = New(opts)
assert.EqualError(t, err, "invalid remark42 url demo.remark42.com")
t.Log(err)
opts = Opts{}
_, err = p.ParseArgs([]string{"--secret=123456", "--url=https://demo.remark42.com", "--store.type=mongo"})
assert.Nil(t, err)
_, err = New(opts)
assert.EqualError(t, err, "unsupported store type mongo")
t.Log(err)
}
func TestApplicationShutdown(t *testing.T) {
app, ctx := prepApp(t, 18090, 500*time.Millisecond)
st := time.Now()
err := app.Run(ctx)
assert.Nil(t, err)
assert.True(t, time.Since(st).Seconds() < 1, "should take about 500msec")
app.Wait()
}
func TestApplicationMainSignal(t *testing.T) {
os.Args = []string{"test", "--secret=123456", "--store.bolt.path=/tmp/xyz", "--backup=/tmp", "--avatar.fs.path=/tmp",
"--port=18100", "--url=https://demo.remark42.com"}
go func() {
time.Sleep(100 * time.Millisecond)
err := syscall.Kill(syscall.Getpid(), syscall.SIGTERM)
require.Nil(t, err)
}()
st := time.Now()
main()
assert.True(t, time.Since(st).Seconds() < 1, "should take about 500msec")
}
func prepApp(t *testing.T, port int, duration time.Duration) (*Application, context.Context) {
// prepare options
opts := Opts{}
p := flags.NewParser(&opts, flags.Default)
_, err := p.ParseArgs([]string{"--secret=123456", "--dev-passwd=password", "--url=https://demo.remark42.com"})
require.Nil(t, err)
opts.Avatar.FS.Path, opts.Avatar.Type, opts.BackupLocation = "/tmp", "fs", "/tmp"
opts.Store.Bolt.Path = fmt.Sprintf("/tmp/%d", port)
opts.Store.Bolt.Timeout = 10 * time.Second
opts.Auth.Github.CSEC, opts.Auth.Github.CID = "csec", "cid"
opts.Auth.Google.CSEC, opts.Auth.Google.CID = "csec", "cid"
opts.Auth.Facebook.CSEC, opts.Auth.Facebook.CID = "csec", "cid"
opts.Auth.Yandex.CSEC, opts.Auth.Yandex.CID = "csec", "cid"
opts.Port = port
os.Remove(opts.Store.Bolt.Path + "/remark.db")
// create app
app, err := New(opts)
require.Nil(t, err)
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(duration)
log.Print("[TEST] terminate app")
cancel()
}()
return app, ctx
}
-94
View File
@@ -1,94 +0,0 @@
package migrator
import (
"compress/gzip"
"context"
"fmt"
"io/ioutil"
"log"
"os"
"sort"
"strings"
"time"
"github.com/pkg/errors"
)
// AutoBackup struct handles daily backups params for siteID
type AutoBackup struct {
Exporter Exporter
BackupLocation string
SiteID string
KeepMax int
Duration time.Duration
}
// Do runs daily export to local files, keeps up to keepMax backups for given siteID
func (ab AutoBackup) Do(ctx context.Context) {
log.Printf("[INFO] activate auto-backup for %s under %s, duration %s", ab.SiteID, ab.BackupLocation, ab.Duration)
tick := time.NewTicker(ab.Duration)
log.Printf("[DEBUG] first backup for %s at %s", ab.SiteID, time.Now().Add(ab.Duration))
for {
select {
case <-tick.C:
if _, err := ab.makeBackup(); err != nil {
log.Printf("[WARN] auto-backup for %s failed, %s", ab.SiteID, err)
continue
}
ab.removeOldBackupFiles()
log.Printf("[DEBUG] next backup for %s at %s", ab.SiteID, time.Now().Add(ab.Duration))
case <-ctx.Done():
log.Printf("[WARN] terminated autobackup for %s", ab.SiteID)
return
}
}
}
func (ab AutoBackup) makeBackup() (string, error) {
log.Printf("[DEBUG] make backup for %s", ab.SiteID)
backupFile := fmt.Sprintf("%s/backup-%s-%s.gz", ab.BackupLocation, ab.SiteID, time.Now().Format("20060102"))
fh, err := os.Create(backupFile)
if err != nil {
return "", errors.Wrapf(err, "can't create backup file %s", backupFile)
}
gz := gzip.NewWriter(fh)
if _, err = ab.Exporter.Export(gz, ab.SiteID); err != nil {
return "", errors.Wrapf(err, "export failed for %s", ab.SiteID)
}
if err = gz.Close(); err != nil {
return "", errors.Wrapf(err, "can't close gz for %s", backupFile)
}
if err = fh.Close(); err != nil {
return "", errors.Wrapf(err, "can't close file handler for %s", backupFile)
}
log.Printf("[DEBUG] created backup file %s", backupFile)
return backupFile, nil
}
func (ab AutoBackup) removeOldBackupFiles() {
files, err := ioutil.ReadDir(ab.BackupLocation)
if err != nil {
log.Printf("[WARN] can't read files in backup directory %s, %s", ab.BackupLocation, err)
return
}
backFiles := []os.FileInfo{}
for _, file := range files {
if strings.HasPrefix(file.Name(), "backup-"+ab.SiteID) {
backFiles = append(backFiles, file)
}
}
sort.Slice(backFiles, func(i int, j int) bool { return backFiles[i].Name() < backFiles[j].Name() })
if len(backFiles) > ab.KeepMax {
for i := 0; i < len(backFiles)-ab.KeepMax; i++ {
fpath := ab.BackupLocation + "/" + backFiles[i].Name()
if e := os.Remove(fpath); e != nil {
log.Printf("[WARN] can't delete %s, %s", fpath, err)
continue
}
log.Printf("[DEBUG] removed %s", fpath)
}
}
}
-81
View File
@@ -1,81 +0,0 @@
package migrator
import (
"context"
"fmt"
"io"
"io/ioutil"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestBackup_RemoveOldBackupFiles(t *testing.T) {
loc := "/tmp/remark-backups.test"
defer os.RemoveAll(loc)
os.MkdirAll(loc, 0700)
for i := 1; i <= 10; i++ {
fname := fmt.Sprintf("%s/backup-site1-201712%02d.gz", loc, i)
err := ioutil.WriteFile(fname, []byte("blah"), 0600)
assert.Nil(t, err)
}
fname := fmt.Sprintf("%s/backup-site2-20171210.gz", loc)
err := ioutil.WriteFile(fname, []byte("blah"), 0600)
assert.Nil(t, err)
bk := AutoBackup{BackupLocation: loc, SiteID: "site1", KeepMax: 3}
bk.removeOldBackupFiles()
ff, err := ioutil.ReadDir(loc)
assert.Nil(t, err)
assert.Equal(t, 4, len(ff), "should keep 4 files - 3 kept for sit1, and one for site2")
assert.Equal(t, "backup-site1-20171208.gz", ff[0].Name())
assert.Equal(t, "backup-site1-20171209.gz", ff[1].Name())
assert.Equal(t, "backup-site1-20171210.gz", ff[2].Name())
assert.Equal(t, "backup-site2-20171210.gz", ff[3].Name())
}
func TestBackup_MakeBackup(t *testing.T) {
loc := "/tmp/remark-backups.test"
defer os.RemoveAll(loc)
os.MkdirAll(loc, 0700)
bk := AutoBackup{BackupLocation: loc, SiteID: "site1", KeepMax: 3, Exporter: &mockExporter{}}
fname, err := bk.makeBackup()
assert.NoError(t, err)
expFile := fmt.Sprintf("/tmp/remark-backups.test/backup-site1-%s.gz", time.Now().Format("20060102"))
assert.Equal(t, expFile, fname)
fi, err := os.Lstat(expFile)
assert.NoError(t, err)
assert.Equal(t, int64(52), fi.Size())
}
func TestBackup_Do(t *testing.T) {
loc := "/tmp/remark-backups.test"
defer os.RemoveAll(loc)
os.MkdirAll(loc, 0700)
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(time.Second)
cancel()
}()
bk := AutoBackup{BackupLocation: loc, SiteID: "site1", KeepMax: 3, Exporter: &mockExporter{}, Duration: 600 * time.Millisecond}
bk.Do(ctx)
expFile := fmt.Sprintf("/tmp/remark-backups.test/backup-site1-%s.gz", time.Now().Format("20060102"))
fi, err := os.Lstat(expFile)
assert.NoError(t, err)
assert.Equal(t, int64(52), fi.Size())
}
type mockExporter struct{}
func (mock *mockExporter) Export(w io.Writer, siteID string) (int, error) {
_, err := w.Write([]byte("some export blah blah 1234567890"))
return 1000, err
}
-165
View File
@@ -1,165 +0,0 @@
package migrator
import (
"encoding/xml"
"io"
"log"
"strings"
"time"
"github.com/pkg/errors"
"github.com/umputun/remark/app/store"
)
// Disqus implements Importer from disqus xml
type Disqus struct {
DataStore Store
}
type disqusThread struct {
UID string `xml:"id,attr"`
Forum string `xml:"forum"`
Link string `xml:"link"`
Title string `xml:"title"`
Message string `xml:"message"`
CreateAt time.Time `xml:"createdAt"`
AuthorName string `xml:"author>name"`
AuthorEmail string `xml:"author>email"`
IP string `xml:"ipAddress"`
Anonymous bool `xml:"author>isAnonymous"`
Closed bool `xml:"isClosed"`
Deleted bool `xml:"isDeleted"`
}
type disqusComment struct {
UID string `xml:"id,attr"`
ID string `xml:"id"`
Message string `xml:"message"`
CreatedAt time.Time `xml:"createdAt"`
AuthorEmail string `xml:"author>email"`
AuthorName string `xml:"author>name"`
AuthorUserName string `xml:"author>username"`
IP string `xml:"ipAddress"`
Tid uid `xml:"thread"`
Pid uid `xml:"parent"`
IsSpam bool `xml:"isSpam"`
}
type uid struct {
Val string `xml:"id,attr"`
}
// Import from disqus and save to store
func (d *Disqus) Import(r io.Reader, siteID string) (size int, err error) {
if err = d.DataStore.DeleteAll(siteID); err != nil {
return 0, err
}
commentsCh := d.convert(r, siteID)
failed, passed := 0, 0
for c := range commentsCh {
if _, err = d.DataStore.Create(c); err != nil {
failed++
continue
}
passed++
}
if failed > 0 {
return passed, errors.Errorf("failed to save %d comments", failed)
}
log.Printf("[DEBUG] imported %d comments to site %s", passed, siteID)
if failed > 0 && passed == 0 {
err = errors.New("import failed")
}
return passed, err
}
// convert disqus stream (xml) from reader and fill channel of comments.
// runs async and closes channel on completion.
func (d *Disqus) convert(r io.Reader, siteID string) (ch chan store.Comment) {
postsMap := map[string]string{} // tid:url
decoder := xml.NewDecoder(r)
commentsCh := make(chan store.Comment)
stats := struct {
inpThreads, inpComments int
commentsCount, spamComments int
failedThreads, failedPosts int
}{}
go func() {
for {
t, err := decoder.Token()
if t == nil || err != nil {
break
}
switch se := t.(type) {
case xml.StartElement:
if se.Name.Local == "thread" {
stats.inpThreads++
thread := disqusThread{}
if err := decoder.DecodeElement(&thread, &se); err != nil {
log.Printf("[WARN] can't decode disqus thread, %s", err)
stats.failedThreads++
continue
}
postsMap[thread.UID] = thread.Link
continue
}
if se.Name.Local == "post" {
stats.inpComments++
comment := disqusComment{}
if err := decoder.DecodeElement(&comment, &se); err != nil {
log.Printf("[WARN] can't decode disqus comment, %s", err)
stats.failedPosts++
continue
}
if comment.IsSpam {
stats.spamComments++
continue
}
c := store.Comment{
ID: comment.UID,
Locator: store.Locator{URL: postsMap[comment.Tid.Val], SiteID: siteID},
User: store.User{
ID: "disqus_" + store.EncodeID(comment.AuthorUserName),
Name: comment.AuthorName,
IP: comment.IP,
},
Text: d.cleanText(comment.Message),
Timestamp: comment.CreatedAt,
ParentID: comment.Pid.Val,
}
if c.User.ID == "disqus_" { // empty comment.AuthorUserName from disqus
c.User.ID = "disqus_" + c.User.Name
}
if c.ID == "" { // no comment.UID
c.ID = comment.ID
}
commentsCh <- c
stats.commentsCount++
if stats.commentsCount%1000 == 0 {
log.Printf("[DEBUG] processed %d comments", stats.commentsCount)
}
}
}
}
close(commentsCh)
log.Printf("[INFO] converted %d posts, %+v", len(postsMap), stats)
}()
return commentsCh
}
func (d *Disqus) cleanText(text string) string {
text = strings.Replace(text, "\n", "", -1)
text = strings.Replace(text, "\t", "", -1)
return text
}
-193
View File
@@ -1,193 +0,0 @@
package migrator
import (
"os"
"strings"
"testing"
"time"
"github.com/coreos/bbolt"
"github.com/stretchr/testify/require"
"github.com/umputun/remark/app/store"
"github.com/umputun/remark/app/store/engine"
"github.com/umputun/remark/app/store/service"
"github.com/stretchr/testify/assert"
)
func TestDisqus_Import(t *testing.T) {
defer os.Remove("/tmp/remark-test.db")
b, err := engine.NewBoltDB(bolt.Options{}, engine.BoltSite{FileName: "/tmp/remark-test.db", SiteID: "test"})
require.Nil(t, err, "create store")
dataStore := service.DataStore{Interface: b}
d := Disqus{DataStore: &dataStore}
size, err := d.Import(strings.NewReader(xmlTest), "test")
assert.Nil(t, err)
assert.Equal(t, 3, size)
last, err := dataStore.Last("test", 10)
assert.Nil(t, err)
assert.Equal(t, 3, len(last), "3 comments imported")
c := last[0]
assert.True(t, strings.HasPrefix(c.Text, "<p>Google App Engine"))
assert.Equal(t, "299986072", c.ID)
assert.Equal(t, "", c.ParentID)
assert.Equal(t, store.Locator{SiteID: "test", URL: "http://radio-t.umputun.com/2011/03/229_8880.html"}, c.Locator)
assert.Equal(t, "Dmitry Noname", c.User.Name)
assert.Equal(t, "disqus_8799342cdf328253e03313958ffc6a433659d7ff", c.User.ID)
assert.Equal(t, "96243f024cf6ad42b66f0c72709ae20b5d10ec14", c.User.IP)
posts, err := dataStore.List("test", 0, 0)
assert.Nil(t, err)
assert.Equal(t, 2, len(posts), "2 posts")
count, err := dataStore.Count(store.Locator{SiteID: "test", URL: "https://radio-t.com/p/2011/03/05/podcast-229/"})
assert.Nil(t, err)
assert.Equal(t, 2, count)
}
func TestDisqus_Convert(t *testing.T) {
d := Disqus{}
ch := d.convert(strings.NewReader(xmlTest), "test")
res := []store.Comment{}
for comment := range ch {
res = append(res, comment)
}
assert.Equal(t, 3, len(res), "3 comments total, 1 spam excluded")
exp0 := store.Comment{
ID: "299619020",
Locator: store.Locator{
SiteID: "test",
URL: "https://radio-t.com/p/2011/03/05/podcast-229/",
},
Text: `<p>The quick brown fox jumps over the lazy dog.</p><p><a href="https://https://radio-t.com" rel="nofollow noopener" title="radio-t">some link</a></p>`,
User: store.User{
Name: "Alexander Blah",
ID: "disqus_328c8b68974aef73785f6b38c3d3fedfdf941434",
IP: "178.178.178.178",
},
}
exp0.Timestamp, _ = time.Parse("2006-01-02T15:04:05Z", "2011-08-31T15:16:29Z")
assert.Equal(t, exp0, res[0])
}
var xmlTest = `<?xml version="1.0" encoding="utf-8"?>
<disqus xmlns="http://disqus.com" xmlns:dsq="http://disqus.com/disqus-internals" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://disqus.com/api/schemas/1.0/disqus.xsd http://disqus.com/api/schemas/1.0/disqus-internals.xsd">
<category dsq:id="707279">
<forum>radiot</forum>
<title>General</title>
<isDefault>true</isDefault>
</category>
<thread dsq:id="247918464">
<id/>
<forum>radiot</forum>
<category dsq:id="707279"/>
<link>http://radio-t.umputun.com/2011/03/229_8880.html</link>
<title>Радио-Т: Радио-Т 229</title>
<message/>
<createdAt>2011-03-07T20:46:25Z</createdAt>
<author>
<email>umputun@gmail.com</email>
<name>Umputun</name>
<isAnonymous>false</isAnonymous>
<username>umputun</username>
</author>
<ipAddress>98.212.28.115</ipAddress>
<isClosed>false</isClosed>
<isDeleted>false</isDeleted>
</thread>
<thread dsq:id="247937687">
<id>http://www.radio-t.com/p/2011/03/05/podcast-229/</id>
<forum>radiot</forum>
<category dsq:id="707279"/>
<link>https://radio-t.com/p/2011/03/05/podcast-229/</link>
<title>Радио-Т: Радио-Т 229</title>
<message/>
<createdAt>2011-03-07T21:17:17Z</createdAt>
<author>
<email>umputun@gmail.com</email>
<name>Umputun</name>
<isAnonymous>false</isAnonymous>
<username>umputun</username>
</author>
<ipAddress>80.250.214.235</ipAddress>
<isClosed>true</isClosed>
<isDeleted>false</isDeleted>
</thread>
<post dsq:id="299619020">
<id>3565798471341011339</id>
<message>
<![CDATA[<p>The quick brown fox jumps over the lazy dog.</p><p><a href="https://https://radio-t.com" rel="nofollow noopener" title="radio-t">some link</a></p>]]>
</message>
<createdAt>2011-08-31T15:16:29Z</createdAt>
<isDeleted>false</isDeleted>
<isSpam>false</isSpam>
<author>
<email/>
<name>Alexander Blah</name>
<isAnonymous>false</isAnonymous>
<username>facebook-1787732238</username>
</author>
<ipAddress>178.178.178.178</ipAddress>
<thread dsq:id="247937687"/>
</post>
<post dsq:id="299744309">
<id>3029154520436241933</id>
<message>
<![CDATA[<p>Microsoft показал проводник Windows 8 с ленточным интерфейсом.</p><p><a href="http://blogs.msdn.com/b/b8/archive/2011/08/29/improvements-in-windows-explorer.aspx" rel="nofollow noopener" title="http://blogs.msdn.com/b/b8/archive/2011/08/29/improvements-in-windows-explorer.aspx">http://blogs.msdn.com/b/b8/...</a> </p>]]>
</message>
<createdAt>2011-08-31T17:44:22Z</createdAt>
<isDeleted>false</isDeleted>
<isSpam>false</isSpam>
<author>
<email>mihail.noname@gmail.com</email>
<name>mikhail</name>
<isAnonymous>false</isAnonymous>
<username>mikhail-noname</username>
</author>
<ipAddress>195.195.195.139</ipAddress>
<thread dsq:id="247937687"/>
</post>
<post dsq:id="299986072">
<id>6580890074280459209</id>
<message>
<![CDATA[<p>Google App Engine скоро выходит из превью статуса.</p><p>Сейчас письмо пришло от гугла.</p><p>Для платных приложений использущих High Replication Datastore (HRD) будет 99.95% uptime SLA.<br>Будут Премьер аккаунты за 500 баксов/месяц с оперативной поддержкой и любым количеством приложений на аккаунте (+ плата за потребленные ресурсы).<br>В связи с переходом на новую систему оплаты, обещают снизить бесплатные квоты.<br>Всем кто включит биллинг до 31 октября, обещают 50 баксов :)</p>]]>
</message>
<createdAt>2011-08-31T22:48:43Z</createdAt>
<isDeleted>false</isDeleted>
<isSpam>false</isSpam>
<author>
<email>dmitri.noname@gmail.com</email>
<name>Dmitry Noname</name>
<isAnonymous>false</isAnonymous>
<username>google-74b9e7568ef6860e93862c5d77590123</username>
</author>
<ipAddress>89.89.89.139</ipAddress>
<thread dsq:id="247918464"/>
</post>
<post dsq:id="299986073">
<id>6580890074280459219</id>
<message>some ugly spam</message>
<createdAt>2011-09-30T22:48:43Z</createdAt>
<isDeleted>false</isDeleted>
<isSpam>true</isSpam>
<author>
<email>spam.noname@gmail.com</email>
<name>Spam Noname</name>
<isAnonymous>false</isAnonymous>
<username>google-2c5d77590123</username>
</author>
<ipAddress>189.89.89.139</ipAddress>
<thread dsq:id="247937687"/>
</post>
</disqus>
`
-67
View File
@@ -1,67 +0,0 @@
// Package migrator provides import/export functionality. It defines Importer and Exporter interfaces
// amd implements for disqus (importer only) and "native" remark (both importer and exporter).
// Also implements AutoBackup scheduler running exports as backups and saving them locally.
package migrator
import (
"io"
"log"
"os"
"github.com/pkg/errors"
"github.com/umputun/remark/app/store"
)
// Importer defines interface to convert posts from external sources
type Importer interface {
Import(r io.Reader, siteID string) (int, error)
}
// Exporter defines interface to export comments from internal store
type Exporter interface {
Export(w io.Writer, siteID string) (int, error)
}
// Store defines minimal interface needed to export and import comments
type Store interface {
Create(comment store.Comment) (commentID string, err error)
Find(locator store.Locator, sort string) ([]store.Comment, error)
List(siteID string, limit int, skip int) ([]store.PostInfo, error)
DeleteAll(siteID string) error
}
// ImportParams defines everything needed to run import
type ImportParams struct {
DataStore Store
InputFile string
Provider string
SiteID string
}
// ImportComments imports from given provider format and saves to store
func ImportComments(p ImportParams) (int, error) {
log.Printf("[INFO] import from %s (%s) to %s", p.InputFile, p.Provider, p.SiteID)
var importer Importer
switch p.Provider {
case "disqus":
importer = &Disqus{DataStore: p.DataStore}
case "native":
importer = &Remark{DataStore: p.DataStore}
default:
return 0, errors.Errorf("unsupported import provider %s", p.Provider)
}
fh, err := os.Open(p.InputFile)
if err != nil {
return 0, errors.Wrapf(err, "can't open import file %s", p.InputFile)
}
defer func() {
if err = fh.Close(); err != nil {
log.Printf("[WARN] can't close %s, %s", p.InputFile, err)
}
}()
return importer.Import(fh, p.SiteID)
}
-93
View File
@@ -1,93 +0,0 @@
package migrator
import (
"io/ioutil"
"os"
"testing"
"github.com/coreos/bbolt"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/umputun/remark/app/store/service"
"github.com/umputun/remark/app/store/engine"
)
func TestMigrator_ImportDisqus(t *testing.T) {
defer func() {
os.Remove("/tmp/remark-test.db")
os.Remove("/tmp/disqus-test.xml")
}()
err := ioutil.WriteFile("/tmp/disqus-test.xml", []byte(xmlTest), 0600)
require.Nil(t, err)
b, err := engine.NewBoltDB(bolt.Options{}, engine.BoltSite{FileName: "/tmp/remark-test.db", SiteID: "test"})
require.Nil(t, err, "create store")
dataStore := &service.DataStore{Interface: b}
size, err := ImportComments(ImportParams{
DataStore: dataStore,
InputFile: "/tmp/disqus-test.xml",
SiteID: "test",
Provider: "disqus",
})
assert.Nil(t, err)
assert.Equal(t, 3, size)
last, err := dataStore.Last("test", 10)
assert.Nil(t, err)
assert.Equal(t, 3, len(last), "3 comments imported")
}
func TestMigrator_ImportRemark(t *testing.T) {
defer func() {
os.Remove("/tmp/remark-test.db")
os.Remove("/tmp/disqus-test.r42")
}()
data := `{"id":"efbc17f177ee1a1c0ee6e1e025749966ec071adc","pid":"","text":"some text, <a href=\"http://radio-t.com\" rel=\"nofollow\">link</a>","user":{"name":"user name","id":"user1","picture":"","profile":"","admin":false},"locator":{"site":"radio-t","url":"https://radio-t.com"},"score":0,"votes":{},"time":"2017-12-20T15:18:22-06:00"}` + "\n" +
`{"id":"afbc17f177ee1a1c0ee6e1e025749966ec071adc","pid":"efbc17f177ee1a1c0ee6e1e025749966ec071adc","text":"some text2, <a href=\"http://radio-t.com\" rel=\"nofollow\">link</a>","user":{"name":"user name","id":"user1","picture":"","profile":"","admin":false},"locator":{"site":"radio-t","url":"https://radio-t.com"},"score":0,"votes":{},"time":"2017-12-20T15:18:23-06:00"}` + "\n"
err := ioutil.WriteFile("/tmp/disqus-test.r42", []byte(data), 0600)
require.Nil(t, err)
b, err := engine.NewBoltDB(bolt.Options{}, engine.BoltSite{FileName: "/tmp/remark-test.db", SiteID: "radio-t"})
require.Nil(t, err, "create store")
dataStore := &service.DataStore{Interface: b}
size, err := ImportComments(ImportParams{
DataStore: dataStore,
InputFile: "/tmp/disqus-test.r42",
SiteID: "radio-t",
Provider: "native",
})
assert.Nil(t, err)
assert.Equal(t, 2, size)
last, err := dataStore.Last("radio-t", 10)
assert.Nil(t, err)
assert.Equal(t, 2, len(last), "2 comments imported")
}
func TestMigrator_ImportFailed(t *testing.T) {
b, err := engine.NewBoltDB(bolt.Options{}, engine.BoltSite{FileName: "/tmp/remark-test.db", SiteID: "test"})
require.Nil(t, err, "create store")
dataStore := &service.DataStore{Interface: b}
_, err = ImportComments(ImportParams{
DataStore: dataStore,
InputFile: "/tmp/disqus-test.xml",
SiteID: "test",
Provider: "bad",
})
assert.EqualError(t, err, "unsupported import provider bad")
_, err = ImportComments(ImportParams{
DataStore: dataStore,
InputFile: "/tmp/disqus-test-bad.xml",
SiteID: "test",
Provider: "native",
})
assert.EqualError(t, err, "can't open import file /tmp/disqus-test-bad.xml: open /tmp/disqus-test-bad.xml: no such file or directory")
}
-95
View File
@@ -1,95 +0,0 @@
package migrator
import (
"bufio"
"bytes"
"encoding/json"
"io"
"log"
"github.com/pkg/errors"
"github.com/umputun/remark/app/store"
)
// Remark implements exporter and importer for internal store format
type Remark struct {
DataStore Store
}
// Export all comments to writer as json strings. Each comment is one string, separated by "\n"
func (r *Remark) Export(w io.Writer, siteID string) (size int, err error) {
topics, err := r.DataStore.List(siteID, 0, 0)
if err != nil {
return 0, err
}
log.Printf("[DEBUG] exporting %d topics", len(topics))
commentsCount := 0
for i := len(topics) - 1; i >= 0; i-- { // topics from List sorted in opposite direction
topic := topics[i]
comments, err := r.DataStore.Find(store.Locator{SiteID: siteID, URL: topic.URL}, "time")
if err != nil {
return commentsCount, err
}
for _, comment := range comments {
buf := &bytes.Buffer{}
enc := json.NewEncoder(buf)
enc.SetEscapeHTML(false)
if err := enc.Encode(comment); err != nil {
return commentsCount, errors.Wrapf(err, "can't marshal %v", comments)
}
data := buf.Bytes()
if _, err := w.Write(data); err != nil {
return commentsCount, errors.Wrap(err, "can't write comment data")
}
commentsCount++
}
}
log.Printf("[DEBUG] exported %d comments", commentsCount)
return commentsCount, nil
}
// Import comments from json strings produced by Remark.Export
func (r *Remark) Import(reader io.Reader, siteID string) (size int, err error) {
if err := r.DataStore.DeleteAll(siteID); err != nil {
return 0, err
}
failed := 0
total, comments := 0, 0
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
rec := scanner.Bytes()
if len(rec) < 3 {
continue
}
total++
comment := store.Comment{}
if err := json.Unmarshal(rec, &comment); err != nil {
failed++
log.Printf("[WARN] unmarshal failed for %s, %s", string(rec), err)
continue
}
if _, err := r.DataStore.Create(comment); err != nil {
failed++
log.Printf("[WARN] can't write %+v to store, %s", comment, err)
continue
}
comments++
if comments%1000 == 0 {
log.Printf("[DEBUG] imported %d comments", comments)
}
}
if scanner.Err() != nil {
return comments, errors.Wrap(scanner.Err(), "error in scan")
}
if failed > 0 {
return comments, errors.Errorf("failed to save %d comments", failed)
}
log.Printf("[INFO] imported %d comments from %d records", comments, total)
return comments, nil
}
-112
View File
@@ -1,112 +0,0 @@
package migrator
import (
"bytes"
"fmt"
"log"
"os"
"testing"
"time"
"github.com/coreos/bbolt"
"github.com/stretchr/testify/assert"
"github.com/umputun/remark/app/store"
"github.com/umputun/remark/app/store/engine"
"github.com/umputun/remark/app/store/service"
)
var testDb = "/tmp/test-remark.db"
func TestRemark_Export(t *testing.T) {
b := prep(t)
r := Remark{DataStore: b}
buf := &bytes.Buffer{}
size, err := r.Export(buf, "radio-t")
assert.Nil(t, err)
assert.Equal(t, 2, size)
c1, err := buf.ReadString('\n')
assert.Nil(t, err)
log.Print(c1)
exp := `{"id":"efbc17f177ee1a1c0ee6e1e025749966ec071adc","pid":"","text":"some text, <a href=\"http://radio-t.com\" rel=\"nofollow\">link</a>","user":{"name":"user name","id":"user1","picture":"","admin":false},"locator":{"site":"radio-t","url":"https://radio-t.com"},"score":0,"votes":{},"time":"2017-12-20T15:18:22-06:00"}` + "\n"
assert.Equal(t, exp, c1)
}
func TestRemark_Import(t *testing.T) {
r1 := `{"id":"efbc17f177ee1a1c0ee6e1e025749966ec071adc","pid":"","text":"some text, <a href=\"http://radio-t.com\" rel=\"nofollow\">link</a>","user":{"name":"user name","id":"user1","picture":"","profile":"","admin":false},"locator":{"site":"radio-t","url":"https://radio-t.com"},"score":0,"votes":{},"time":"2017-12-20T15:18:22-06:00"}` + "\n"
r2 := `{"id":"afbc17f177ee1a1c0ee6e1e025749966ec071adc","pid":"efbc17f177ee1a1c0ee6e1e025749966ec071adc","text":"some text2, <a href=\"http://radio-t.com\" rel=\"nofollow\">link</a>","user":{"name":"user name","id":"user1","picture":"","profile":"","admin":false},"locator":{"site":"radio-t","url":"https://radio-t.com"},"score":0,"votes":{},"time":"2017-12-20T15:18:23-06:00"}` + "\n"
buf := &bytes.Buffer{}
buf.WriteString(r1)
buf.WriteString(r2)
buf.WriteString("{}")
os.Remove(testDb)
b, err := engine.NewBoltDB(bolt.Options{}, engine.BoltSite{SiteID: "radio-t", FileName: testDb})
assert.Nil(t, err)
r := Remark{DataStore: &service.DataStore{Interface: b}}
size, err := r.Import(buf, "radio-t")
assert.Nil(t, err)
assert.Equal(t, 2, size)
comments, err := b.Find(store.Locator{SiteID: "radio-t", URL: "https://radio-t.com"}, "time")
assert.Nil(t, err)
assert.Equal(t, 2, len(comments))
assert.Equal(t, "efbc17f177ee1a1c0ee6e1e025749966ec071adc", comments[0].ID)
assert.Equal(t, "afbc17f177ee1a1c0ee6e1e025749966ec071adc", comments[1].ID)
assert.Equal(t, "efbc17f177ee1a1c0ee6e1e025749966ec071adc", comments[1].ParentID)
}
func TestRemark_ImportManyWithError(t *testing.T) {
goodRec := `{"id":"%d","pid":"","text":"some text, <a href=\"http://radio-t.com\" rel=\"nofollow\">link</a>","user":{"name":"user name","id":"user1","picture":"","profile":"","admin":false},"locator":{"site":"radio-t","url":"https://radio-t.com"},"score":0,"votes":{},"time":"2017-12-20T15:18:22-06:00"}` + "\n"
buf := &bytes.Buffer{}
for i := 0; i < 1200; i++ {
buf.WriteString(fmt.Sprintf(goodRec, i))
}
buf.WriteString("bad1\n")
buf.WriteString("bad2\n")
os.Remove(testDb)
b, err := engine.NewBoltDB(bolt.Options{}, engine.BoltSite{SiteID: "radio-t", FileName: testDb})
assert.Nil(t, err)
r := Remark{DataStore: &service.DataStore{Interface: b}}
n, err := r.Import(buf, "radio-t")
assert.EqualError(t, err, "failed to save 2 comments")
assert.Equal(t, 1200, n)
comments, err := b.Find(store.Locator{SiteID: "radio-t", URL: "https://radio-t.com"}, "time")
assert.Nil(t, err)
assert.Equal(t, 1200, len(comments))
}
// makes new boltdb, put two records
func prep(t *testing.T) *service.DataStore {
os.Remove(testDb)
boltStore, err := engine.NewBoltDB(bolt.Options{}, engine.BoltSite{SiteID: "radio-t", FileName: testDb})
assert.Nil(t, err)
b := &service.DataStore{Interface: boltStore}
comment := store.Comment{
ID: "efbc17f177ee1a1c0ee6e1e025749966ec071adc",
Text: `some text, <a href="http://radio-t.com">link</a>`,
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
User: store.User{ID: "user1", Name: "user name"},
}
_, err = b.Create(comment)
assert.Nil(t, err)
comment = store.Comment{
Text: "some text2", Timestamp: time.Date(2017, 12, 20, 15, 18, 23, 0, time.Local),
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
User: store.User{ID: "user1", Name: "user name"},
}
_, err = b.Create(comment)
assert.Nil(t, err)
return b
}
-245
View File
@@ -1,245 +0,0 @@
package api
import (
"compress/gzip"
"errors"
"fmt"
"io"
"log"
"net/http"
"time"
"github.com/go-chi/chi"
"github.com/go-chi/render"
"github.com/umputun/remark/app/rest/auth"
"github.com/umputun/remark/app/migrator"
"github.com/umputun/remark/app/rest"
"github.com/umputun/remark/app/rest/cache"
"github.com/umputun/remark/app/store"
"github.com/umputun/remark/app/store/service"
)
// admin provides router for all requests available for admin users only
type admin struct {
dataService *service.DataStore
exporter migrator.Exporter
cache cache.LoadingCache
authenticator auth.Authenticator
readOnlyAge int
}
func (a *admin) routes(middlewares ...func(http.Handler) http.Handler) chi.Router {
router := chi.NewRouter()
router.Use(middlewares...)
router.Delete("/comment/{id}", a.deleteCommentCtrl)
router.Put("/user/{userid}", a.setBlockCtrl)
router.Delete("/user/{userid}", a.deleteUserCtrl)
router.Get("/deleteme", a.deleteMeRequestCtrl)
router.Put("/verify/{userid}", a.setVerifyCtrl)
router.Get("/export", a.exportCtrl)
router.Put("/pin/{id}", a.setPinCtrl)
router.Get("/blocked", a.blockedUsersCtrl)
router.Put("/readonly", a.setReadOnlyCtrl)
return router
}
// DELETE /comment/{id}?site=siteID&url=post-url - removes comment
func (a *admin) deleteCommentCtrl(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")}
log.Printf("[INFO] delete comment %s", id)
err := a.dataService.Delete(locator, id, store.SoftDelete)
if err != nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't delete comment")
return
}
a.cache.Flush(locator.SiteID, locator.URL)
render.Status(r, http.StatusOK)
render.JSON(w, r, JSON{"id": id, "locator": locator})
}
// DELETE /user/{userid}?site=side-id - delete all user comments for requested userid
func (a *admin) deleteUserCtrl(w http.ResponseWriter, r *http.Request) {
userID := chi.URLParam(r, "userid")
siteID := r.URL.Query().Get("site")
log.Printf("[INFO] delete all user comments for %s, site %s", userID, siteID)
if err := a.dataService.DeleteUser(siteID, userID); err != nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't delete user")
return
}
a.cache.Flush(siteID, userID)
render.Status(r, http.StatusOK)
render.JSON(w, r, JSON{"user_id": userID, "site_id": siteID})
}
// GET /deleteme?token=jwt - delete all user comments by user's request. Gets info about deleted used from provided token
// request made GET to allow direct click from the email sent by user
func (a *admin) deleteMeRequestCtrl(w http.ResponseWriter, r *http.Request) {
token := r.URL.Query().Get("token")
claims, err := a.authenticator.JWTService.Parse(token)
if err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't process token")
return
}
log.Printf("[INFO] delete all user comments by request for %s, site %s", claims.User.ID, claims.SiteID)
if err := a.dataService.DeleteUser(claims.SiteID, claims.User.ID); err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't delete user")
return
}
a.cache.Flush(claims.SiteID, claims.User.ID)
render.Status(r, http.StatusOK)
render.JSON(w, r, JSON{"user_id": claims.User.ID, "site_id": claims.SiteID})
}
// PUT /user/{userid}?site=side-id&block=1 - block or unblock user
func (a *admin) setBlockCtrl(w http.ResponseWriter, r *http.Request) {
userID := chi.URLParam(r, "userid")
siteID := r.URL.Query().Get("site")
blockStatus := r.URL.Query().Get("block") == "1"
if err := a.dataService.SetBlock(siteID, userID, blockStatus); err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't set blocking status")
return
}
a.cache.Flush(siteID, userID)
render.JSON(w, r, JSON{"user_id": userID, "site_id": siteID, "block": blockStatus})
}
// GET /blocked?site=siteID - list blocked users
func (a *admin) blockedUsersCtrl(w http.ResponseWriter, r *http.Request) {
siteID := r.URL.Query().Get("site")
users, err := a.dataService.Blocked(siteID)
if err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get blocked users")
return
}
render.JSON(w, r, users)
}
// PUT /readonly?site=siteID&url=post-url&ro=1 - set or reset read-only status for the post
func (a *admin) setReadOnlyCtrl(w http.ResponseWriter, r *http.Request) {
locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")}
roStatus := r.URL.Query().Get("ro") == "1"
isRoByAge := func(info store.PostInfo) bool {
return a.readOnlyAge > 0 && !info.FirstTS.IsZero() &&
info.FirstTS.AddDate(0, 0, a.readOnlyAge).Before(time.Now())
}
// don't allow to reset ro for posts turned to ro by ReadOnlyAge
if !roStatus {
if info, e := a.dataService.Info(locator, a.readOnlyAge); e == nil && isRoByAge(info) {
rest.SendErrorJSON(w, r, http.StatusForbidden, errors.New("rejected"), "read-only due the age")
return
}
}
if err := a.dataService.SetReadOnly(locator, roStatus); err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't set readonly status")
return
}
a.cache.Flush(locator.SiteID)
render.JSON(w, r, JSON{"locator": locator, "read-only": roStatus})
}
// PUT /verify?site=siteID&url=post-url&ro=1 - set or reset read-only status for the post
func (a *admin) setVerifyCtrl(w http.ResponseWriter, r *http.Request) {
userID := chi.URLParam(r, "userid")
siteID := r.URL.Query().Get("site")
verifyStatus := r.URL.Query().Get("verified") == "1"
if err := a.dataService.SetVerified(siteID, userID, verifyStatus); err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't set verify status")
return
}
a.cache.Flush(siteID, userID)
render.JSON(w, r, JSON{"user": userID, "verified": verifyStatus})
}
// PUT /pin/{id}?site=siteID&url=post-url&pin=1
// mark/unmark comment as a special
func (a *admin) setPinCtrl(w http.ResponseWriter, r *http.Request) {
commentID := chi.URLParam(r, "id")
locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")}
pinStatus := r.URL.Query().Get("pin") == "1"
if err := a.dataService.SetPin(locator, commentID, pinStatus); err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't set pin status")
return
}
a.cache.Flush(locator.URL)
render.JSON(w, r, JSON{"id": commentID, "locator": locator, "pin": pinStatus})
}
// GET /export?site=site-id?mode=file|stream
// exports all comments for siteID as json stream or gz file
func (a *admin) 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" {
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)
w.WriteHeader(http.StatusOK)
gzWriter := gzip.NewWriter(w)
defer func() {
if e := gzWriter.Close(); e != nil {
log.Printf("[WARN] can't close gzip writer, %s", e)
}
}()
writer = gzWriter
}
if _, err := a.exporter.Export(writer, siteID); err != nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "export failed")
return
}
}
func (a *admin) checkBlocked(siteID string, user store.User) bool {
return a.dataService.IsBlocked(siteID, user.ID)
}
// post-processes comments, hides text of all comments for blocked users,
// resets score and votes too. Also hides sensitive info for non-admin users
func (a *admin) alterComments(comments []store.Comment, r *http.Request) (res []store.Comment) {
res = make([]store.Comment, len(comments))
user, err := rest.GetUserInfo(r)
isAdmin := err == nil && user.Admin // make separate cache key for admins
for i, c := range comments {
blocked := a.dataService.IsBlocked(c.Locator.SiteID, c.User.ID)
// process blocked users
if blocked {
if !isAdmin { // reset comment to deleted for non-admins
c.SetDeleted(store.SoftDelete)
}
c.User.Blocked = true
c.Deleted = true
}
// set verified status retroactively
if !blocked {
c.User.Verified = a.dataService.IsVerified(c.Locator.SiteID, c.User.ID)
}
// hide info from non-admins
if !isAdmin {
c.User.IP = ""
}
res[i] = c
}
return res
}
-541
View File
@@ -1,541 +0,0 @@
package api
import (
"compress/gzip"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/umputun/remark/app/rest/auth"
"github.com/umputun/remark/app/store"
)
func TestAdmin_Delete(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts)
c1 := store.Comment{Text: "test test #1", User: store.User{ID: "id", Name: "name"},
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}}
c2 := store.Comment{Text: "test test #2", User: store.User{ID: "id", Name: "name"}, ParentID: "p1",
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}}
id1 := addComment(t, c1, ts)
addComment(t, c2, ts)
client := http.Client{}
req, err := http.NewRequest(http.MethodDelete,
fmt.Sprintf("%s/api/v1/admin/comment/%s?site=radio-t&url=https://radio-t.com/blah", ts.URL, id1), nil)
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
resp, err := client.Do(req)
assert.Nil(t, err)
assert.Equal(t, 200, resp.StatusCode)
body, code := getWithAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah", ts.URL, id1))
assert.Equal(t, 200, code)
cr := store.Comment{}
err = json.Unmarshal([]byte(body), &cr)
assert.Nil(t, err)
assert.Equal(t, "", cr.Text)
assert.True(t, cr.Deleted)
}
func TestAdmin_DeleteUser(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts)
c1 := store.Comment{Text: "test test #1", Orig: "o test test #1", User: store.User{ID: "id1", Name: "name"},
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}}
c2 := store.Comment{Text: "test test #2", Orig: "o test test #2", User: store.User{ID: "id2", Name: "name"}, ParentID: "p1",
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}}
c3 := store.Comment{Text: "test test #3", Orig: "o test test #3", User: store.User{ID: "id2", Name: "name"}, ParentID: "",
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}}
// write comments directly to store to keep user id
id1, err := srv.DataService.Create(c1)
assert.NoError(t, err)
_, err = srv.DataService.Create(c2)
assert.NoError(t, err)
_, err = srv.DataService.Create(c3)
assert.NoError(t, err)
client := http.Client{}
req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("%s/api/v1/admin/user/%s?site=radio-t", ts.URL, "id2"), nil)
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
resp, err := client.Do(req)
assert.Nil(t, err)
assert.Equal(t, 200, resp.StatusCode)
// all 3 comments here, but for id2 they deleted
res, code := get(t, ts.URL+"/api/v1/find?site=radio-t&url=https://radio-t.com/blah&sort=+time")
assert.Equal(t, 200, code)
commentsWithInfo := commentsWithInfo{}
err = json.Unmarshal([]byte(res), &commentsWithInfo)
assert.Nil(t, err)
assert.Equal(t, 3, len(commentsWithInfo.Comments), "should have 3 comment")
// id1 comment untouched
assert.Equal(t, id1, commentsWithInfo.Comments[0].ID)
assert.Equal(t, "o test test #1", commentsWithInfo.Comments[0].Orig)
assert.False(t, commentsWithInfo.Comments[0].Deleted)
t.Logf("%+v", commentsWithInfo.Comments[0].User)
// id2 comments fully deleted
assert.Equal(t, "", commentsWithInfo.Comments[1].Text)
assert.Equal(t, "", commentsWithInfo.Comments[1].Orig)
assert.Equal(t, store.User{Name: "deleted", ID: "deleted", Picture: "", Admin: false, Blocked: false, IP: ""}, commentsWithInfo.Comments[1].User)
assert.True(t, commentsWithInfo.Comments[1].Deleted)
assert.Equal(t, "", commentsWithInfo.Comments[2].Text)
assert.Equal(t, "", commentsWithInfo.Comments[2].Orig)
assert.Equal(t, store.User{Name: "deleted", ID: "deleted", Picture: "", Admin: false, Blocked: false, IP: ""}, commentsWithInfo.Comments[1].User)
assert.True(t, commentsWithInfo.Comments[2].Deleted)
}
func TestAdmin_Pin(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts)
c1 := store.Comment{Text: "test test #1",
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}}
c2 := store.Comment{Text: "test test #2", ParentID: "p1",
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}}
id1 := addComment(t, c1, ts)
addComment(t, c2, ts)
pin := func(val int) int {
client := http.Client{}
req, err := http.NewRequest(http.MethodPut,
fmt.Sprintf("%s/api/v1/admin/pin/%s?site=radio-t&url=https://radio-t.com/blah&pin=%d", ts.URL, id1, val), nil)
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
resp, err := client.Do(req)
assert.Nil(t, err)
return resp.StatusCode
}
code := pin(1)
assert.Equal(t, 200, code)
body, code := get(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah", ts.URL, id1))
assert.Equal(t, 200, code)
cr := store.Comment{}
err := json.Unmarshal([]byte(body), &cr)
assert.Nil(t, err)
assert.True(t, cr.Pin)
code = pin(-1)
assert.Equal(t, 200, code)
body, code = get(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah", ts.URL, id1))
assert.Equal(t, 200, code)
cr = store.Comment{}
err = json.Unmarshal([]byte(body), &cr)
assert.Nil(t, err)
assert.False(t, cr.Pin)
}
func TestAdmin_Block(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts)
c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t",
URL: "https://radio-t.com/blah"}, User: store.User{Name: "user1 name", ID: "user1"}}
c2 := store.Comment{Text: "test test #2", ParentID: "p1", Locator: store.Locator{SiteID: "radio-t",
URL: "https://radio-t.com/blah"}, User: store.User{Name: "user2", ID: "user2"}}
_, err := srv.DataService.Create(c1)
assert.Nil(t, err)
_, err = srv.DataService.Create(c2)
assert.Nil(t, err)
block := func(val int) (code int, body []byte) {
client := http.Client{}
req, e := http.NewRequest(http.MethodPut,
fmt.Sprintf("%s/api/v1/admin/user/%s?site=radio-t&block=%d", ts.URL, "user1", val), nil)
assert.Nil(t, e)
req.SetBasicAuth("dev", "password")
resp, e := client.Do(req)
require.Nil(t, e)
body, e = ioutil.ReadAll(resp.Body)
assert.Nil(t, e)
resp.Body.Close()
return resp.StatusCode, body
}
code, body := block(1)
require.Equal(t, 200, code)
j := JSON{}
err = json.Unmarshal(body, &j)
assert.Nil(t, err)
assert.Equal(t, "user1", j["user_id"])
assert.Equal(t, true, j["block"])
assert.Equal(t, "radio-t", j["site_id"])
res, code := get(t, ts.URL+"/api/v1/find?site=radio-t&url=https://radio-t.com/blah&sort=+time")
assert.Equal(t, 200, code)
comments := commentsWithInfo{}
err = json.Unmarshal([]byte(res), &comments)
assert.Nil(t, err)
assert.Equal(t, 2, len(comments.Comments), "should have 2 comments")
assert.Equal(t, "", comments.Comments[0].Text)
assert.True(t, comments.Comments[0].Deleted)
code, body = block(-1)
require.Equal(t, 200, code)
err = json.Unmarshal(body, &j)
assert.Nil(t, err)
assert.Equal(t, false, j["block"])
}
func TestAdmin_BlockedList(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts)
client := http.Client{}
// block user1
req, err := http.NewRequest(http.MethodPut,
fmt.Sprintf("%s/api/v1/admin/user/%s?site=radio-t&block=%d", ts.URL, "user1", 1), nil)
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
_, err = client.Do(req)
require.Nil(t, err)
// block user2
req, err = http.NewRequest(http.MethodPut,
fmt.Sprintf("%s/api/v1/admin/user/%s?site=radio-t&block=%d", ts.URL, "user2", 1), nil)
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
_, err = client.Do(req)
require.Nil(t, err)
res, code := getWithAuth(t, ts.URL+"/api/v1/admin/blocked?site=radio-t")
require.Equal(t, 200, code, res)
users := []store.BlockedUser{}
err = json.Unmarshal([]byte(res), &users)
assert.Nil(t, err)
assert.Equal(t, 2, len(users), "two users blocked")
assert.Equal(t, "user1", users[0].ID)
assert.Equal(t, "user2", users[1].ID)
}
func TestAdmin_ReadOnly(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts)
c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t",
URL: "https://radio-t.com/blah"}, User: store.User{Name: "user1 name", ID: "user1"}}
c2 := store.Comment{Text: "test test #2", ParentID: "p1", Locator: store.Locator{SiteID: "radio-t",
URL: "https://radio-t.com/blah"}, User: store.User{Name: "user2", ID: "user2"}}
_, err := srv.DataService.Create(c1)
assert.Nil(t, err)
_, err = srv.DataService.Create(c2)
assert.Nil(t, err)
info, err := srv.DataService.Info(store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}, 0)
assert.Nil(t, err)
assert.False(t, info.ReadOnly)
client := http.Client{}
// set post to read-only
req, err := http.NewRequest(http.MethodPut,
fmt.Sprintf("%s/api/v1/admin/readonly?site=radio-t&url=https://radio-t.com/blah&ro=1", ts.URL), nil)
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
resp, err := client.Do(req)
require.Nil(t, err)
assert.Equal(t, 200, resp.StatusCode)
info, err = srv.DataService.Info(store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}, 0)
assert.Nil(t, err)
assert.True(t, info.ReadOnly)
// reset post's read-only
req, err = http.NewRequest(http.MethodPut,
fmt.Sprintf("%s/api/v1/admin/readonly?site=radio-t&url=https://radio-t.com/blah&ro=0", ts.URL), nil)
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
resp, err = client.Do(req)
assert.Equal(t, 200, resp.StatusCode)
require.Nil(t, err)
info, err = srv.DataService.Info(store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}, 0)
assert.Nil(t, err)
assert.False(t, info.ReadOnly)
}
func TestAdmin_ReadOnlyWithAge(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts)
c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t",
URL: "https://radio-t.com/blah"}, User: store.User{Name: "user1 name", ID: "user1"},
Timestamp: time.Date(2001, 1, 1, 1, 1, 1, 0, time.Local)}
_, err := srv.DataService.Create(c1)
assert.Nil(t, err)
info, err := srv.DataService.Info(store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}, 10)
assert.Nil(t, err)
assert.True(t, info.ReadOnly, "ro by age")
client := http.Client{}
// set post to read-only
req, err := http.NewRequest(http.MethodPut,
fmt.Sprintf("%s/api/v1/admin/readonly?site=radio-t&url=https://radio-t.com/blah&ro=1", ts.URL), nil)
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
resp, err := client.Do(req)
require.Nil(t, err)
assert.Equal(t, 200, resp.StatusCode)
info, err = srv.DataService.Info(store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}, 0)
assert.Nil(t, err)
assert.True(t, info.ReadOnly)
// reset post's read-only
req, err = http.NewRequest(http.MethodPut,
fmt.Sprintf("%s/api/v1/admin/readonly?site=radio-t&url=https://radio-t.com/blah&ro=0", ts.URL), nil)
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
resp, err = client.Do(req)
assert.Equal(t, 403, resp.StatusCode)
require.Nil(t, err)
info, err = srv.DataService.Info(store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}, 0)
assert.Nil(t, err)
assert.True(t, info.ReadOnly)
}
func TestAdmin_Verify(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts)
c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t",
URL: "https://radio-t.com/blah"}, User: store.User{Name: "user1 name", ID: "user1"}}
c2 := store.Comment{Text: "test test #2", ParentID: "p1", Locator: store.Locator{SiteID: "radio-t",
URL: "https://radio-t.com/blah"}, User: store.User{Name: "user2", ID: "user2"}}
_, err := srv.DataService.Create(c1)
assert.Nil(t, err)
_, err = srv.DataService.Create(c2)
assert.Nil(t, err)
verified := srv.DataService.IsVerified("radio-t", "user1")
assert.False(t, verified)
client := http.Client{}
req, err := http.NewRequest(http.MethodPut,
fmt.Sprintf("%s/api/v1/admin/verify/user1?site=radio-t&verified=1", ts.URL), nil)
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
_, err = client.Do(req)
require.Nil(t, err)
verified = srv.DataService.IsVerified("radio-t", "user1")
assert.True(t, verified)
res, code := get(t, ts.URL+"/api/v1/find?site=radio-t&url=https://radio-t.com/blah&sort=+time")
assert.Equal(t, 200, code)
comments := commentsWithInfo{}
err = json.Unmarshal([]byte(res), &comments)
assert.Nil(t, err)
assert.Equal(t, 2, len(comments.Comments), "should have 2 comments")
assert.Equal(t, "test test #1", comments.Comments[0].Text)
assert.True(t, comments.Comments[0].User.Verified)
req, err = http.NewRequest(http.MethodPut,
fmt.Sprintf("%s/api/v1/admin/verify/user1?site=radio-t&verified=0", ts.URL), nil)
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
_, err = client.Do(req)
require.Nil(t, err)
verified = srv.DataService.IsVerified("radio-t", "user1")
assert.False(t, verified)
res, code = get(t, ts.URL+"/api/v1/find?site=radio-t&url=https://radio-t.com/blah&sort=+time")
assert.Equal(t, 200, code)
comments = commentsWithInfo{}
err = json.Unmarshal([]byte(res), &comments)
assert.Nil(t, err)
assert.Equal(t, 2, len(comments.Comments), "should have 2 comments")
assert.Equal(t, "test test #1", comments.Comments[0].Text)
assert.False(t, comments.Comments[0].User.Verified)
}
func TestAdmin_ExportStream(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts)
c1 := store.Comment{Text: "test test #1",
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}}
c2 := store.Comment{Text: "test test #2", ParentID: "p1",
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah2"}}
addComment(t, c1, ts)
addComment(t, c2, ts)
body, code := getWithAuth(t, ts.URL+"/api/v1/admin/export?site=radio-t&mode=stream")
assert.Equal(t, 200, code)
assert.Equal(t, 2, strings.Count(body, "\n"))
assert.Equal(t, 2, strings.Count(body, "\"text\""))
t.Logf("%s", body)
}
func TestAdmin_ExportFile(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts)
c1 := store.Comment{Text: "test test #1",
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}}
c2 := store.Comment{Text: "test test #2", ParentID: "p1",
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah2"}}
addComment(t, c1, ts)
addComment(t, c2, ts)
client := &http.Client{Timeout: 5 * time.Second}
req, err := http.NewRequest("GET", ts.URL+"/api/v1/admin/export?site=radio-t&mode=file", nil)
require.Nil(t, err)
req.SetBasicAuth("dev", "password")
resp, err := client.Do(req)
require.Nil(t, err)
assert.Equal(t, 200, resp.StatusCode)
assert.Equal(t, "application/gzip", resp.Header.Get("Content-Type"))
ungzReader, err := gzip.NewReader(resp.Body)
assert.NoError(t, err)
ungzBody, err := ioutil.ReadAll(ungzReader)
assert.NoError(t, err)
assert.Equal(t, 2, strings.Count(string(ungzBody), "\n"))
assert.Equal(t, 2, strings.Count(string(ungzBody), "\"text\""))
t.Logf("%s", string(ungzBody))
}
func TestAdmin_DeleteMeRequest(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts)
c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t",
URL: "https://radio-t.com/blah"}, User: store.User{Name: "user1 name", ID: "user1"}}
c2 := store.Comment{Text: "test test #2", ParentID: "p1", Locator: store.Locator{SiteID: "radio-t",
URL: "https://radio-t.com/blah"}, User: store.User{Name: "user2", ID: "user2"}}
_, err := srv.DataService.Create(c1)
assert.Nil(t, err)
_, err = srv.DataService.Create(c2)
assert.Nil(t, err)
comments, err := srv.DataService.User("radio-t", "user1", 0, 0)
assert.Nil(t, err)
assert.Equal(t, 1, len(comments), "a comment for user1")
claims := auth.CustomClaims{
SiteID: "radio-t",
SessionOnly: true,
StandardClaims: jwt.StandardClaims{
Id: "1234567",
Issuer: "remark42",
NotBefore: time.Now().Add(-1 * time.Minute).Unix(),
ExpiresAt: time.Now().Add(30 * time.Minute).Unix(),
},
User: &store.User{
ID: "user1",
},
}
token, err := srv.Authenticator.JWTService.Token(&claims)
assert.Nil(t, err)
client := http.Client{}
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/admin/deleteme?token=%s", ts.URL, token), nil)
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
resp, err := client.Do(req)
assert.Nil(t, err)
assert.Equal(t, 200, resp.StatusCode)
_, err = srv.DataService.User("radio-t", "user1", 0, 0)
assert.EqualError(t, err, "no comments for user user1 in store")
}
func TestAdmin_DeleteMeRequestFailed(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts)
c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t",
URL: "https://radio-t.com/blah"}, User: store.User{Name: "user1 name", ID: "user1"}}
c2 := store.Comment{Text: "test test #2", ParentID: "p1", Locator: store.Locator{SiteID: "radio-t",
URL: "https://radio-t.com/blah"}, User: store.User{Name: "user2", ID: "user2"}}
_, err := srv.DataService.Create(c1)
assert.Nil(t, err)
_, err = srv.DataService.Create(c2)
assert.Nil(t, err)
// try with bad token
client := http.Client{}
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/admin/deleteme?token=%s", ts.URL, "bad token"), nil)
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
resp, err := client.Do(req)
assert.Nil(t, err)
assert.Equal(t, 400, resp.StatusCode)
// try with bad auth
claims := auth.CustomClaims{
SiteID: "radio-t",
SessionOnly: true,
StandardClaims: jwt.StandardClaims{
Id: "1234567",
Issuer: "remark42",
NotBefore: time.Now().Add(-1 * time.Minute).Unix(),
ExpiresAt: time.Now().Add(30 * time.Minute).Unix(),
},
User: &store.User{
ID: "user1",
},
}
token, err := srv.Authenticator.JWTService.Token(&claims)
assert.Nil(t, err)
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/admin/deleteme?token=%s", ts.URL, token), nil)
assert.Nil(t, err)
req.SetBasicAuth("dev", "bad-password")
resp, err = client.Do(req)
assert.Nil(t, err)
assert.Equal(t, 401, resp.StatusCode)
// try bad user
badClaims := claims
badClaims.User.ID = "no-such-id"
token, err = srv.Authenticator.JWTService.Token(&badClaims)
assert.Nil(t, err)
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/admin/deleteme?token=%s", ts.URL, token), nil)
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
resp, err = client.Do(req)
assert.Nil(t, err)
assert.Equal(t, 400, resp.StatusCode, resp.Status)
}
-169
View File
@@ -1,169 +0,0 @@
package api
import (
"bytes"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"regexp"
"runtime/debug"
"strings"
"time"
"github.com/go-chi/chi/middleware"
"github.com/umputun/remark/app/rest"
)
// JSON is a map alias, just for convenience
type JSON map[string]interface{}
// AppInfo adds custom app-info to the response header
func AppInfo(app string, version string) func(http.Handler) http.Handler {
f := func(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Org", "Umputun")
w.Header().Set("App-Name", app)
w.Header().Set("App-Version", version)
if mhost := os.Getenv("MHOST"); mhost != "" {
w.Header().Set("Host", mhost)
}
h.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
return f
}
// Ping middleware response with pong to /ping. Stops chain if ping request detected
func Ping(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" && strings.HasSuffix(strings.ToLower(r.URL.Path), "/ping") {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
if _, err := w.Write([]byte("pong")); err != nil {
log.Printf("[WARN] can't send pong, %s", err)
}
return
}
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
// Recoverer is a middleware that recovers from panics, logs the panic and returns a HTTP 500 status if possible.
func Recoverer(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rvr := recover(); rvr != nil {
log.Printf("[WARN] request panic, %v", rvr)
debug.PrintStack()
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
// LoggerFlag type
type LoggerFlag int
// logger flags enum
const (
LogAll LoggerFlag = iota
LogUser
LogBody
LogNone
)
const maxBody = 1024
var reMultWhtsp = regexp.MustCompile(`[\s\p{Zs}]{2,}`)
// Logger middleware prints http log. Customized by set of LoggerFlag
func Logger(ipFn func(ip string) string, flags ...LoggerFlag) func(http.Handler) http.Handler {
f := func(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
if inLogFlags(LogNone, flags) { // skip logging
h.ServeHTTP(w, r)
return
}
ww := middleware.NewWrapResponseWriter(w, 1)
body, user := getBodyAndUser(r, flags)
t1 := time.Now()
defer func() {
t2 := time.Now()
q := r.URL.String()
if qun, err := url.QueryUnescape(q); err == nil {
q = qun
}
remoteIP := strings.Split(r.RemoteAddr, ":")[0]
if strings.HasPrefix(r.RemoteAddr, "[") {
remoteIP = strings.Split(r.RemoteAddr, "]:")[0] + "]"
}
if ipFn != nil {
remoteIP = ipFn(remoteIP)
}
log.Printf("[INFO] REST %s - %s - %s - %d (%d) - %v %s %s",
r.Method, q, remoteIP, ww.Status(), ww.BytesWritten(), t2.Sub(t1), user, body)
}()
h.ServeHTTP(ww, r)
}
return http.HandlerFunc(fn)
}
return f
}
func getBodyAndUser(r *http.Request, flags []LoggerFlag) (body string, user string) {
ctx := r.Context()
if ctx == nil {
return "", ""
}
if inLogFlags(LogBody, flags) {
if content, err := ioutil.ReadAll(r.Body); err == nil {
body = string(content)
r.Body = ioutil.NopCloser(bytes.NewReader(content))
if len(body) > 0 {
body = strings.Replace(body, "\n", " ", -1)
body = reMultWhtsp.ReplaceAllString(body, " ")
}
if len(body) > maxBody {
body = body[:maxBody] + "..."
}
}
}
if inLogFlags(LogUser, flags) {
u, err := rest.GetUserInfo(r)
if err == nil && u.Name != "" {
user = fmt.Sprintf(" - %s %q", u.ID, u.Name)
}
}
return body, user
}
func inLogFlags(f LoggerFlag, flags []LoggerFlag) bool {
for _, flg := range flags {
if (flg == LogAll && f != LogNone) || flg == f {
return true
}
}
return false
}
-63
View File
@@ -1,63 +0,0 @@
package api
import (
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/go-chi/chi"
"github.com/stretchr/testify/assert"
"github.com/umputun/remark/app/rest"
"github.com/umputun/remark/app/store"
"github.com/stretchr/testify/require"
)
func TestMiddleware_AppInfo(t *testing.T) {
router := chi.NewRouter()
router.With(AppInfo("remark42", "12345")).Get("/blah", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
w.Write([]byte("blah blah"))
})
ts := httptest.NewServer(router)
defer ts.Close()
resp, err := http.Get(ts.URL + "/blah")
require.Nil(t, err)
assert.Equal(t, 200, resp.StatusCode)
b, err := ioutil.ReadAll(resp.Body)
assert.NoError(t, err)
assert.Equal(t, "blah blah", string(b))
assert.Equal(t, "remark42", resp.Header.Get("App-Name"))
assert.Equal(t, "12345", resp.Header.Get("App-Version"))
assert.Equal(t, "Umputun", resp.Header.Get("Org"))
}
func TestMiddleware_GetBodyAndUser(t *testing.T) {
req, err := http.NewRequest("GET", "http://example.com/request", strings.NewReader("body"))
require.Nil(t, err)
body, user := getBodyAndUser(req, []LoggerFlag{LogAll})
assert.Equal(t, "body", body)
assert.Equal(t, "", user, "no user")
req = rest.SetUserInfo(req, store.User{ID: "id1", Name: "user1"})
_, user = getBodyAndUser(req, []LoggerFlag{LogAll})
assert.Equal(t, ` - id1 "user1"`, user, "no user")
body, user = getBodyAndUser(req, nil)
assert.Equal(t, "", body)
assert.Equal(t, "", user, "no user")
body, user = getBodyAndUser(req, []LoggerFlag{LogNone})
assert.Equal(t, "", body)
assert.Equal(t, "", user, "no user")
body, user = getBodyAndUser(req, []LoggerFlag{LogUser})
assert.Equal(t, "", body)
assert.Equal(t, ` - id1 "user1"`, user, "no user")
}
-133
View File
@@ -1,133 +0,0 @@
package api
import (
"compress/gzip"
"context"
"fmt"
"log"
"net/http"
"strings"
"sync"
"time"
"github.com/didip/tollbooth"
"github.com/didip/tollbooth_chi"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"github.com/go-chi/render"
"github.com/umputun/remark/app/migrator"
"github.com/umputun/remark/app/rest"
"github.com/umputun/remark/app/rest/cache"
)
// Migrator rest runs on unexposed port and available for local requests only
type Migrator struct {
Version string
Cache cache.LoadingCache
NativeImporter migrator.Importer
DisqusImporter migrator.Importer
NativeExported migrator.Exporter
SecretKey string
httpServer *http.Server
lock sync.Mutex
}
// Run the listener and request's router, activate rest server
// this server doesn't have any authentication and SHOULDN'T BE EXPOSED in any way
func (m *Migrator) Run(port int) {
log.Printf("[INFO] activate import server on port %d", port)
router := m.routes()
m.lock.Lock()
m.httpServer = &http.Server{Addr: fmt.Sprintf("127.0.0.1:%d", port), Handler: router}
m.lock.Unlock()
err := m.httpServer.ListenAndServe()
log.Printf("[WARN] http server terminated, %s", err)
}
// Shutdown import http server
func (m *Migrator) Shutdown() {
log.Print("[WARN] shutdown import server")
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
m.lock.Lock()
if err := m.httpServer.Shutdown(ctx); err != nil {
log.Printf("[DEBUG] importer shutdown error, %s", err)
}
m.lock.Unlock()
log.Print("[DEBUG] shutdown import server completed")
}
func (m *Migrator) routes() chi.Router {
router := chi.NewRouter()
router.Use(middleware.RealIP, Recoverer)
router.Use(middleware.Throttle(1000), middleware.Timeout(15*time.Minute))
router.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(10, nil)))
router.Use(AppInfo("remark42-migrator", m.Version), Ping, Logger(nil, LogAll))
router.Post("/api/v1/admin/import", m.importCtrl)
router.Get("/api/v1/admin/export", m.exportCtrl)
return router
}
// POST /import?secret=key&site=site-id&provider=disqus|remark
// imports comments from post body.
func (m *Migrator) importCtrl(w http.ResponseWriter, r *http.Request) {
secret := r.URL.Query().Get("secret")
if strings.TrimSpace(secret) == "" || secret != m.SecretKey {
render.Status(r, http.StatusForbidden)
render.JSON(w, r, JSON{"status": "error", "details": "secret key"})
return
}
siteID := r.URL.Query().Get("site")
importer := m.NativeImporter
if r.URL.Query().Get("provider") == "disqus" {
importer = m.DisqusImporter
}
size, err := importer.Import(r.Body, siteID)
if err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "import failed")
return
}
m.Cache.Flush(siteID)
render.Status(r, http.StatusCreated)
render.JSON(w, r, JSON{"status": "ok", "size": size})
}
// GET /export?site=site-id&secret=12345
// exports all comments for siteID as gz file
func (m *Migrator) exportCtrl(w http.ResponseWriter, r *http.Request) {
secret := r.URL.Query().Get("secret")
if strings.TrimSpace(secret) == "" || secret != m.SecretKey {
render.Status(r, http.StatusForbidden)
render.JSON(w, r, JSON{"status": "error", "details": "secret key"})
return
}
siteID := r.URL.Query().Get("site")
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)
w.WriteHeader(http.StatusOK)
gzWriter := gzip.NewWriter(w)
defer func() {
if e := gzWriter.Close(); e != nil {
log.Printf("[WARN] can't close gzip writer, %s", e)
}
}()
if _, err := m.NativeExported.Export(gzWriter, siteID); err != nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "export failed")
return
}
}
-126
View File
@@ -1,126 +0,0 @@
package api
import (
"compress/gzip"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"time"
"github.com/coreos/bbolt"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/umputun/remark/app/migrator"
"github.com/umputun/remark/app/store/engine"
"github.com/umputun/remark/app/store/service"
)
func TestMigrator_Import(t *testing.T) {
srv, ts := prepImportSrv(t)
assert.NotNil(t, srv)
defer cleanupImportSrv(srv, ts)
r := strings.NewReader(`{"id":"2aa0478c-df1b-46b1-b561-03d507cf482c","pid":"","text":"<p>test test #1</p>","user":{"name":"developer one","id":"dev","picture":"/api/v1/avatar/remark.image","profile":"https://remark42.com","admin":true,"ip":"ae12fe3b5f129b5cc4cdd2b136b7b7947c4d2741"},"locator":{"site":"radio-t","url":"https://radio-t.com/blah1"},"score":0,"votes":{},"time":"2018-04-30T01:37:00.849053725-05:00"}
{"id":"83fd97fd-ff64-48d1-9fb7-ca7769c77037","pid":"p1","text":"<p>test test #2</p>","user":{"name":"developer one","id":"dev","picture":"/api/v1/avatar/remark.image","profile":"https://remark42.com","admin":true,"ip":"ae12fe3b5f129b5cc4cdd2b136b7b7947c4d2741"},"locator":{"site":"radio-t","url":"https://radio-t.com/blah2"},"score":0,"votes":{},"time":"2018-04-30T01:37:00.861387771-05:00"}`)
client := &http.Client{Timeout: 1 * time.Second}
req, err := http.NewRequest("POST", ts.URL+"/api/v1/admin/import?site=radio-t&provider=native&secret=123456", r)
assert.Nil(t, err)
resp, err := client.Do(req)
assert.Nil(t, err)
assert.Equal(t, http.StatusCreated, resp.StatusCode)
b, err := ioutil.ReadAll(resp.Body)
assert.Nil(t, err)
assert.Equal(t, `{"size":2,"status":"ok"}`+"\n", string(b))
}
func TestMigrator_ImportRejected(t *testing.T) {
srv, ts := prepImportSrv(t)
assert.NotNil(t, srv)
defer cleanupImportSrv(srv, ts)
r := strings.NewReader(`{"id":"2aa0478c-df1b-46b1-b561-03d507cf482c","pid":"","text":"<p>test test #1</p>","user":{"name":"developer one","id":"dev","picture":"/api/v1/avatar/remark.image","profile":"https://remark42.com","admin":true,"ip":"ae12fe3b5f129b5cc4cdd2b136b7b7947c4d2741"},"locator":{"site":"radio-t","url":"https://radio-t.com/blah1"},"score":0,"votes":{},"time":"2018-04-30T01:37:00.849053725-05:00"}
{"id":"83fd97fd-ff64-48d1-9fb7-ca7769c77037","pid":"p1","text":"<p>test test #2</p>","user":{"name":"developer one","id":"dev","picture":"/api/v1/avatar/remark.image","profile":"https://remark42.com","admin":true,"ip":"ae12fe3b5f129b5cc4cdd2b136b7b7947c4d2741"},"locator":{"site":"radio-t","url":"https://radio-t.com/blah2"},"score":0,"votes":{},"time":"2018-04-30T01:37:00.861387771-05:00"}`)
client := &http.Client{Timeout: 1 * time.Second}
req, err := http.NewRequest("POST", ts.URL+"/api/v1/admin/import?site=radio-t&provider=native&secret=XYZ", r)
assert.Nil(t, err)
resp, err := client.Do(req)
assert.Nil(t, err)
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
}
func TestMigrator_Export(t *testing.T) {
srv, ts := prepImportSrv(t)
assert.NotNil(t, srv)
defer cleanupImportSrv(srv, ts)
r := strings.NewReader(`{"id":"2aa0478c-df1b-46b1-b561-03d507cf482c","pid":"","text":"<p>test test #1</p>","user":{"name":"developer one","id":"dev","picture":"/api/v1/avatar/remark.image","profile":"https://remark42.com","admin":true,"ip":"ae12fe3b5f129b5cc4cdd2b136b7b7947c4d2741"},"locator":{"site":"radio-t","url":"https://radio-t.com/blah1"},"score":0,"votes":{},"time":"2018-04-30T01:37:00.849053725-05:00"}
{"id":"83fd97fd-ff64-48d1-9fb7-ca7769c77037","pid":"p1","text":"<p>test test #2</p>","user":{"name":"developer one","id":"dev","picture":"/api/v1/avatar/remark.image","profile":"https://remark42.com","admin":true,"ip":"ae12fe3b5f129b5cc4cdd2b136b7b7947c4d2741"},"locator":{"site":"radio-t","url":"https://radio-t.com/blah2"},"score":0,"votes":{},"time":"2018-04-30T01:37:00.861387771-05:00"}`)
client := &http.Client{Timeout: 1 * time.Second}
req, err := http.NewRequest("POST", ts.URL+"/api/v1/admin/import?site=radio-t&provider=native&secret=123456", r)
require.Nil(t, err)
resp, err := client.Do(req)
require.Nil(t, err)
require.Equal(t, http.StatusCreated, resp.StatusCode)
req, err = http.NewRequest("GET", ts.URL+"/api/v1/admin/export?site=radio-t&secret=123456", nil)
require.Nil(t, err)
resp, err = client.Do(req)
require.Nil(t, err)
require.Equal(t, 200, resp.StatusCode)
require.Equal(t, "application/gzip", resp.Header.Get("Content-Type"))
ungzReader, err := gzip.NewReader(resp.Body)
assert.NoError(t, err)
ungzBody, err := ioutil.ReadAll(ungzReader)
assert.NoError(t, err)
assert.Equal(t, 2, strings.Count(string(ungzBody), "\n"))
assert.Equal(t, 2, strings.Count(string(ungzBody), "\"text\""))
t.Logf("%s", string(ungzBody))
req, err = http.NewRequest("GET", ts.URL+"/api/v1/admin/export?site=radio-t&secret=bad", nil)
require.Nil(t, err)
resp, err = client.Do(req)
require.Nil(t, err)
require.Equal(t, 403, resp.StatusCode)
}
func TestMigrator_Shutdown(t *testing.T) {
srv := Migrator{}
go func() {
time.Sleep(100 * time.Millisecond)
srv.Shutdown()
}()
st := time.Now()
srv.Run(0)
assert.True(t, time.Since(st).Seconds() < 1, "should take about 100ms")
}
func prepImportSrv(t *testing.T) (svc *Migrator, ts *httptest.Server) {
b, err := engine.NewBoltDB(bolt.Options{}, engine.BoltSite{FileName: testDb, SiteID: "radio-t"})
require.Nil(t, err)
dataStore := &service.DataStore{Interface: b}
svc = &Migrator{
DisqusImporter: &migrator.Disqus{DataStore: dataStore},
NativeImporter: &migrator.Remark{DataStore: dataStore},
NativeExported: &migrator.Remark{DataStore: dataStore},
Cache: &mockCache{},
SecretKey: "123456",
}
routes := svc.routes()
ts = httptest.NewServer(routes)
return svc, ts
}
func cleanupImportSrv(srv *Migrator, ts *httptest.Server) {
ts.Close()
os.Remove(testDb)
}
-249
View File
@@ -1,249 +0,0 @@
package api
import (
"bytes"
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
"sync"
"time"
"github.com/didip/tollbooth"
"github.com/didip/tollbooth_chi"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"github.com/go-chi/render"
"github.com/pkg/errors"
"gopkg.in/russross/blackfriday.v2"
"github.com/umputun/remark/app/migrator"
"github.com/umputun/remark/app/rest"
"github.com/umputun/remark/app/rest/auth"
"github.com/umputun/remark/app/rest/cache"
"github.com/umputun/remark/app/rest/proxy"
"github.com/umputun/remark/app/store"
"github.com/umputun/remark/app/store/service"
)
// Rest is a rest access server
type Rest struct {
Version string
DataService *service.DataStore
Authenticator auth.Authenticator
Exporter migrator.Exporter
Cache cache.LoadingCache
AvatarProxy *proxy.Avatar
ImageProxy *proxy.Image
WebRoot string
RemarkURL string
ReadOnlyAge int
ScoreThresholds struct {
Low int
Critical int
}
httpServer *http.Server
lock sync.Mutex
adminService admin
}
const hardBodyLimit = 1024 * 64 // limit size of body
var mdExt = blackfriday.NoIntraEmphasis | blackfriday.Tables | blackfriday.FencedCode |
blackfriday.Strikethrough | blackfriday.SpaceHeadings | blackfriday.HardLineBreak |
blackfriday.BackslashLineBreak | blackfriday.Autolink
type commentsWithInfo struct {
Comments []store.Comment `json:"comments"`
Info store.PostInfo `json:"info,omitempty"`
}
// Run the lister and request's router, activate rest server
func (s *Rest) Run(port int) {
log.Printf("[INFO] activate rest server on port %d", port)
if len(s.Authenticator.Admins) > 0 {
log.Printf("[DEBUG] admins %+v", s.Authenticator.Admins)
}
router := s.routes()
s.lock.Lock()
s.httpServer = &http.Server{
Addr: fmt.Sprintf(":%d", port),
Handler: router,
ReadHeaderTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
IdleTimeout: 30 * time.Second,
}
s.lock.Unlock()
err := s.httpServer.ListenAndServe()
log.Printf("[WARN] http server terminated, %s", err)
}
// Shutdown rest http server
func (s *Rest) Shutdown() {
log.Print("[WARN] shutdown rest server")
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
s.lock.Lock()
if err := s.httpServer.Shutdown(ctx); err != nil {
log.Printf("[DEBUG] rest shutdown error, %s", err)
}
log.Print("[DEBUG] shutdown rest server completed")
s.lock.Unlock()
}
func (s *Rest) routes() chi.Router {
router := chi.NewRouter()
router.Use(middleware.RealIP, Recoverer)
router.Use(middleware.Throttle(1000), middleware.Timeout(60*time.Second))
router.Use(AppInfo("remark42", s.Version), Ping)
s.adminService = admin{
dataService: s.DataService,
exporter: s.Exporter,
cache: s.Cache,
authenticator: s.Authenticator,
readOnlyAge: s.ReadOnlyAge,
}
ipFn := func(ip string) string { return store.HashValue(ip, s.DataService.Secret)[:12] } // logger uses it for anonymization
// auth routes for all providers
router.Route("/auth", func(r chi.Router) {
r.Use(Logger(ipFn, LogAll), tollbooth_chi.LimitHandler(tollbooth.NewLimiter(5, nil)))
for _, provider := range s.Authenticator.Providers {
r.Mount("/"+provider.Name, provider.Routes()) // mount auth providers as /auth/{name}
}
if len(s.Authenticator.Providers) > 0 {
// shortcut, can be any of providers, all logouts do the same - removes cookie
r.Get("/logout", s.Authenticator.Providers[0].LogoutHandler)
}
})
avatarMiddlewares := []func(http.Handler) http.Handler{
Logger(ipFn, LogNone),
tollbooth_chi.LimitHandler(tollbooth.NewLimiter(100, nil)),
}
router.Mount(s.AvatarProxy.Routes(avatarMiddlewares...)) // mount avatars to /api/v1/avatar/{file.img}
// api routes
router.Route("/api/v1", func(rapi chi.Router) {
rapi.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(10, nil)))
// open routes
rapi.Group(func(ropen chi.Router) {
ropen.Use(s.Authenticator.Auth(false))
ropen.Use(Logger(ipFn, LogAll))
ropen.Get("/find", s.findCommentsCtrl)
ropen.Get("/id/{id}", s.commentByIDCtrl)
ropen.Get("/comments", s.findUserCommentsCtrl)
ropen.Get("/last/{limit}", s.lastCommentsCtrl)
ropen.Get("/count", s.countCtrl)
ropen.Post("/counts", s.countMultiCtrl)
ropen.Get("/list", s.listCtrl)
ropen.Get("/config", s.configCtrl)
ropen.Post("/preview", s.previewCommentCtrl)
ropen.Get("/info", s.infoCtrl)
ropen.Mount("/rss", s.rssRoutes())
ropen.Mount("/img", s.ImageProxy.Routes())
})
// protected routes, require auth
rapi.Group(func(rauth chi.Router) {
rauth.Use(s.Authenticator.Auth(true))
rauth.Use(Logger(ipFn, LogAll))
rauth.Post("/comment", s.createCommentCtrl)
rauth.Put("/comment/{id}", s.updateCommentCtrl)
rauth.Get("/user", s.userInfoCtrl)
rauth.Put("/vote/{id}", s.voteCtrl)
rauth.Get("/userdata", s.userAllDataCtrl)
rauth.Post("/deleteme", s.deleteMeCtrl)
// admin routes, admin users only
rauth.Mount("/admin", s.adminService.routes(s.Authenticator.AdminOnly, Logger(nil, LogAll)))
})
})
router.With(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(50, nil))).
Get("/robots.txt", func(w http.ResponseWriter, r *http.Request) {
allowed := []string{"/find", "/last", "/id", "/count", "/counts", "/list", "/config", "/img", "/avatar"}
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")
})
// file server for static content from /web
addFileServer(router, "/web", http.Dir(s.WebRoot))
return router
}
// serves static files from /web
func addFileServer(r chi.Router, path string, root http.FileSystem) {
log.Printf("[INFO] run file server for %s, path %s", root, path)
origPath := path
fs := http.StripPrefix(path, http.FileServer(root))
if path != "/" && path[len(path)-1] != '/' {
r.Get(path, http.RedirectHandler(path+"/", 301).ServeHTTP)
path += "/"
}
path += "*"
r.With(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(20, nil))).
Get(path, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// don't show dirs, just serve files
if strings.HasSuffix(r.URL.Path, "/") && len(r.URL.Path) > 1 && r.URL.Path != (origPath+"/") {
http.NotFound(w, r)
return
}
fs.ServeHTTP(w, r)
}))
}
// renderJSONWithHTML allows html tags and forces charset=utf-8
func renderJSONWithHTML(w http.ResponseWriter, r *http.Request, v interface{}) {
data, err := encodeJSONWithHTML(v)
if err != nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't render json response")
return
}
renderJSONFromBytes(w, r, data)
}
func encodeJSONWithHTML(v interface{}) ([]byte, error) {
buf := &bytes.Buffer{}
enc := json.NewEncoder(buf)
enc.SetEscapeHTML(false)
if err := enc.Encode(v); err != nil {
return nil, errors.Wrap(err, "json encoding failed")
}
return buf.Bytes(), nil
}
// renderJSONWithHTML allows html tags and forces charset=utf-8
func renderJSONFromBytes(w http.ResponseWriter, r *http.Request, data []byte) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
if status, ok := r.Context().Value(render.StatusCtxKey).(int); ok {
w.WriteHeader(status)
}
if _, err := w.Write(data); err != nil {
log.Printf("[WARN] failed to send response to %s, %s", r.RemoteAddr, err)
}
}
func filterComments(comments []store.Comment, fn func(c store.Comment) bool) (filtered []store.Comment) {
for _, c := range comments {
if fn(c) {
filtered = append(filtered, c)
}
}
return filtered
}
-262
View File
@@ -1,262 +0,0 @@
package api
import (
"compress/gzip"
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"strings"
"time"
jwt "github.com/dgrijalva/jwt-go"
"github.com/go-chi/chi"
"github.com/go-chi/render"
multierror "github.com/hashicorp/go-multierror"
blackfriday "gopkg.in/russross/blackfriday.v2"
"github.com/umputun/remark/app/rest"
"github.com/umputun/remark/app/rest/auth"
"github.com/umputun/remark/app/store"
"github.com/umputun/remark/app/store/service"
)
// POST /comment - adds comment, resets all immutable fields
func (s *Rest) createCommentCtrl(w http.ResponseWriter, r *http.Request) {
comment := store.Comment{}
if err := render.DecodeJSON(http.MaxBytesReader(w, r.Body, hardBodyLimit), &comment); err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't bind comment")
return
}
user, err := rest.GetUserInfo(r)
if err != nil { // this not suppose to happen (handled by Auth), just dbl-check
rest.SendErrorJSON(w, r, http.StatusUnauthorized, err, "can't get user info")
return
}
log.Printf("[DEBUG] create comment %+v", comment)
comment.PrepareUntrusted() // clean all fields user not supposed to set
comment.User = user
comment.User.IP = strings.Split(r.RemoteAddr, ":")[0]
comment.Orig = comment.Text // original comment text, prior to md render
if err = s.DataService.ValidateComment(&comment); err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "invalid comment")
return
}
comment.Text = string(blackfriday.Run([]byte(comment.Text), blackfriday.WithExtensions(mdExt)))
comment.Text = s.ImageProxy.Convert(comment.Text)
// check if user blocked
if s.adminService.checkBlocked(comment.Locator.SiteID, comment.User) {
rest.SendErrorJSON(w, r, http.StatusForbidden, errors.New("rejected"), "user blocked")
return
}
if s.ReadOnlyAge > 0 {
if info, e := s.DataService.Info(comment.Locator, s.ReadOnlyAge); e == nil && info.ReadOnly {
rest.SendErrorJSON(w, r, http.StatusForbidden, errors.New("rejected"), "old post, read-only")
return
}
}
id, err := s.DataService.Create(comment)
if err != nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't save comment")
return
}
// DataService modifies comment
finalComment, err := s.DataService.Get(comment.Locator, id)
if err != nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't load created comment")
return
}
s.Cache.Flush(comment.Locator.URL, "last", comment.User.ID, comment.Locator.SiteID)
render.Status(r, http.StatusCreated)
render.JSON(w, r, &finalComment)
}
// PUT /comment/{id}?site=siteID&url=post-url - update comment
func (s *Rest) updateCommentCtrl(w http.ResponseWriter, r *http.Request) {
edit := struct {
Text string
Summary string
}{}
if err := render.DecodeJSON(http.MaxBytesReader(w, r.Body, hardBodyLimit), &edit); err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't bind comment")
return
}
user, err := rest.GetUserInfo(r)
if err != nil { // this not suppose to happen (handled by Auth), just dbl-check
rest.SendErrorJSON(w, r, http.StatusUnauthorized, err, "can't get user info")
return
}
locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")}
id := chi.URLParam(r, "id")
log.Printf("[DEBUG] update comment %s", id)
var currComment store.Comment
if currComment, err = s.DataService.Get(locator, id); err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't find comment")
return
}
if currComment.User.ID != user.ID {
rest.SendErrorJSON(w, r, http.StatusForbidden, errors.New("rejected"), "can not edit comments for other users")
return
}
text := string(blackfriday.Run([]byte(edit.Text), blackfriday.WithExtensions(mdExt))) // render markdown
text = s.ImageProxy.Convert(text)
editReq := service.EditRequest{
Text: text,
Orig: edit.Text,
Summary: edit.Summary,
}
res, err := s.DataService.EditComment(locator, id, editReq)
if err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't update comment")
return
}
s.Cache.Flush(locator.URL, "last", user.ID)
render.JSON(w, r, res)
}
// GET /user?site=siteID - returns user info
func (s *Rest) userInfoCtrl(w http.ResponseWriter, r *http.Request) {
user, err := rest.GetUserInfo(r)
if err != nil {
rest.SendErrorJSON(w, r, http.StatusUnauthorized, err, "can't get user info")
return
}
if siteID := r.URL.Query().Get("site"); siteID != "" {
user.Verified = s.DataService.IsVerified(siteID, user.ID)
}
render.JSON(w, r, user)
}
// PUT /vote/{id}?site=siteID&url=post-url&vote=1 - vote for/against comment
func (s *Rest) voteCtrl(w http.ResponseWriter, r *http.Request) {
user, err := rest.GetUserInfo(r)
if err != nil {
rest.SendErrorJSON(w, r, http.StatusUnauthorized, err, "can't get user info")
return
}
locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")}
id := chi.URLParam(r, "id")
log.Printf("[DEBUG] vote for comment %s", id)
vote := r.URL.Query().Get("vote") == "1"
comment, err := s.DataService.Vote(locator, id, user.ID, vote)
if err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't vote for comment")
return
}
s.Cache.Flush(locator.URL)
render.JSON(w, r, JSON{"id": comment.ID, "score": comment.Score})
}
// GET /userdata?site=siteID - exports all data about the user as a json with user info and list of all comments
func (s *Rest) userAllDataCtrl(w http.ResponseWriter, r *http.Request) {
siteID := r.URL.Query().Get("site")
user, err := rest.GetUserInfo(r)
if err != nil {
rest.SendErrorJSON(w, r, http.StatusUnauthorized, err, "can't get user info")
return
}
userB, err := json.Marshal(&user)
if err != nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't marshal user info")
return
}
exportFile := fmt.Sprintf("%s-%s-%s.json.gz", siteID, user.ID, 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)
}
}()
write := func(val []byte) error {
_, e := gzWriter.Write(val)
return e
}
var merr error
merr = multierror.Append(merr, write([]byte(`{"info": `))) // send user prefix
merr = multierror.Append(merr, write(userB)) // send user info
merr = multierror.Append(merr, write([]byte(`, "comments":`))) // send comments prefix
// get comments in 100 in each paginated request
for i := 0; i < 100; i++ {
comments, err := s.DataService.User(siteID, user.ID, 100, i*100)
if err != nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't get user comments")
return
}
b, err := json.Marshal(comments)
if err != nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't marshal user comments")
return
}
merr = multierror.Append(merr, write(b))
if len(comments) != 100 {
break
}
}
merr = multierror.Append(merr, write([]byte(`}`)))
if merr.(*multierror.Error).ErrorOrNil() != nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError, merr, "can't write user info")
return
}
}
// POST /deleteme?site_id=site - requesting delete of all user info
// makes jwt with user info and sends it back as a part of json response
func (s *Rest) deleteMeCtrl(w http.ResponseWriter, r *http.Request) {
user, err := rest.GetUserInfo(r)
if err != nil {
rest.SendErrorJSON(w, r, http.StatusUnauthorized, err, "can't get user info")
return
}
siteID := r.URL.Query().Get("site")
claims := auth.CustomClaims{
SiteID: siteID,
StandardClaims: jwt.StandardClaims{
Issuer: "remark42",
ExpiresAt: time.Now().AddDate(0, 3, 0).Unix(),
NotBefore: time.Now().Add(-1 * time.Minute).Unix(),
},
User: &user,
}
tokenStr, err := s.Authenticator.JWTService.Token(&claims)
if err != nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't make token")
return
}
link := fmt.Sprintf("%s/api/v1/admin/deleteme?token=%s", s.RemarkURL, tokenStr)
render.JSON(w, r, JSON{"site": siteID, "user_id": user.ID, "token": tokenStr, "link": link})
}
-385
View File
@@ -1,385 +0,0 @@
package api
import (
"compress/gzip"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/umputun/remark/app/store"
)
func TestRest_Create(t *testing.T) {
srv, ts := prep(t)
require.NotNil(t, srv)
defer cleanup(ts)
resp, err := post(t, ts.URL+"/api/v1/comment",
`{"text": "test 123", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`)
assert.Nil(t, err)
require.Equal(t, http.StatusCreated, resp.StatusCode)
b, err := ioutil.ReadAll(resp.Body)
assert.Nil(t, err)
c := JSON{}
err = json.Unmarshal(b, &c)
assert.Nil(t, err)
loc := c["locator"].(map[string]interface{})
assert.Equal(t, "radio-t", loc["site"])
assert.Equal(t, "https://radio-t.com/blah1", loc["url"])
assert.True(t, len(c["id"].(string)) > 8)
}
func TestRest_CreateOldPost(t *testing.T) {
srv, ts := prep(t)
require.NotNil(t, srv)
defer cleanup(ts)
// make old, but not too old comment
old := store.Comment{Text: "test test old", ParentID: "", Timestamp: time.Now().AddDate(0, 0, -5),
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}, User: store.User{ID: "u1"}}
_, err := srv.DataService.Create(old)
assert.Nil(t, err)
comments, err := srv.DataService.Find(store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}, "time")
assert.Nil(t, err)
assert.Equal(t, 1, len(comments))
// try to add new comment to the same old post
resp, err := post(t, ts.URL+"/api/v1/comment",
`{"text": "test 123", "locator":{"site": "radio-t","url": "https://radio-t.com/blah1"}}`)
assert.Nil(t, err)
assert.Equal(t, http.StatusCreated, resp.StatusCode)
assert.Nil(t, srv.DataService.DeleteAll("radio-t"))
// make too old comment
old = store.Comment{Text: "test test old", ParentID: "", Timestamp: time.Now().AddDate(0, 0, -15),
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}, User: store.User{ID: "u1"}}
_, err = srv.DataService.Create(old)
assert.Nil(t, err)
resp, err = post(t, ts.URL+"/api/v1/comment",
`{"text": "test 123", "locator":{"site": "radio-t","url": "https://radio-t.com/blah1"}}`)
assert.Nil(t, err)
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
}
func TestRest_CreateTooBig(t *testing.T) {
srv, ts := prep(t)
require.NotNil(t, srv)
defer cleanup(ts)
longComment := fmt.Sprintf(`{"text": "%4001s", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`, "Щ")
resp, err := post(t, ts.URL+"/api/v1/comment", longComment)
assert.Nil(t, err)
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
b, err := ioutil.ReadAll(resp.Body)
assert.Nil(t, err)
c := JSON{}
err = json.Unmarshal(b, &c)
assert.Nil(t, err)
assert.Equal(t, "comment text exceeded max allowed size 4000 (4001)", c["error"])
assert.Equal(t, "invalid comment", c["details"])
veryLongComment := fmt.Sprintf(`{"text": "%70000s", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`, "Щ")
resp, err = post(t, ts.URL+"/api/v1/comment", veryLongComment)
assert.Nil(t, err)
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
b, err = ioutil.ReadAll(resp.Body)
assert.Nil(t, err)
c = JSON{}
err = json.Unmarshal(b, &c)
assert.Nil(t, err)
assert.Equal(t, "http: request body too large", c["error"])
assert.Equal(t, "can't bind comment", c["details"])
}
func TestRest_CreateRejected(t *testing.T) {
srv, ts := prep(t)
require.NotNil(t, srv)
defer cleanup(ts)
body := `{"text": "test 123", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`
// try to create without auth
resp, err := http.Post(ts.URL+"/api/v1/comment", "", strings.NewReader(body))
assert.Nil(t, err)
assert.Equal(t, 401, resp.StatusCode)
}
func TestRest_CreateAndGet(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts)
// create comment
resp, err := post(t, ts.URL+"/api/v1/comment",
`{"text": "**test** *123* http://radio-t.com", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`)
require.Nil(t, err)
require.Equal(t, http.StatusCreated, resp.StatusCode)
b, err := ioutil.ReadAll(resp.Body)
assert.Nil(t, err)
c := JSON{}
err = json.Unmarshal(b, &c)
assert.Nil(t, err)
id := c["id"].(string)
// get created comment by id
res, code := getWithAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah1", ts.URL, id))
assert.Equal(t, 200, code)
comment := store.Comment{}
err = json.Unmarshal([]byte(res), &comment)
assert.Nil(t, err)
assert.Equal(t, `<p><strong>test</strong> <em>123</em> <a href="http://radio-t.com" rel="nofollow">http://radio-t.com</a></p>`+"\n", comment.Text)
assert.Equal(t, "**test** *123* http://radio-t.com", comment.Orig)
assert.Equal(t, store.User{Name: "developer one", ID: "dev",
Picture: "/api/v1/avatar/remark.image", Admin: true, Blocked: false, IP: "dbc7c999343f003f189f70aaf52cc04443f90790"},
comment.User)
t.Logf("%+v", comment)
}
func TestRest_Update(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts)
c1 := store.Comment{Text: "test test #1", ParentID: "p1",
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}}
id := addComment(t, c1, ts)
client := http.Client{}
req, err := http.NewRequest(http.MethodPut, ts.URL+"/api/v1/comment/"+id+"?site=radio-t&url=https://radio-t.com/blah1",
strings.NewReader(`{"text":"updated text", "summary":"my edit"}`))
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
b, err := client.Do(req)
assert.Nil(t, err)
body, err := ioutil.ReadAll(b.Body)
assert.Nil(t, err)
assert.Equal(t, 200, b.StatusCode, string(body))
// comments returned by update
c2 := store.Comment{}
err = json.Unmarshal(body, &c2)
assert.Nil(t, err)
assert.Equal(t, id, c2.ID)
assert.Equal(t, "<p>updated text</p>\n", c2.Text)
assert.Equal(t, "updated text", c2.Orig)
assert.Equal(t, "my edit", c2.Edit.Summary)
assert.True(t, time.Since(c2.Edit.Timestamp) < 1*time.Second)
// read updated comment
res, code := getWithAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah1", ts.URL, id))
assert.Equal(t, 200, code)
c3 := store.Comment{}
err = json.Unmarshal([]byte(res), &c3)
assert.Nil(t, err)
assert.Equal(t, c2, c3, "same as response from update")
}
func TestRest_UpdateNotOwner(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts)
c1 := store.Comment{Text: "test test #1", ParentID: "p1",
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}, User: store.User{ID: "xyz"}}
id1, err := srv.DataService.Create(c1)
assert.Nil(t, err)
client := http.Client{}
req, err := http.NewRequest(http.MethodPut, ts.URL+"/api/v1/comment/"+id1+
"?site=radio-t&url=https://radio-t.com/blah1", strings.NewReader(`{"text":"updated text", "summary":"my edit"}`))
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
b, err := client.Do(req)
assert.Nil(t, err)
body, err := ioutil.ReadAll(b.Body)
assert.Nil(t, err)
assert.Equal(t, 403, b.StatusCode, string(body), "update from non-owner")
assert.Equal(t, `{"details":"can not edit comments for other users","error":"rejected"}`+"\n", string(body))
client = http.Client{}
req, err = http.NewRequest(http.MethodPut, ts.URL+"/api/v1/comment/"+id1+
"?site=radio-t&url=https://radio-t.com/blah1", strings.NewReader(`ERRR "text":"updated text", "summary":"my"}`))
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
b, err = client.Do(req)
assert.Nil(t, err)
assert.Equal(t, 400, b.StatusCode, string(body), "update is not json")
}
func TestRest_Vote(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts)
c1 := store.Comment{Text: "test test #1",
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}}
c2 := store.Comment{Text: "test test #2", ParentID: "p1",
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}}
id1 := addComment(t, c1, ts)
addComment(t, c2, ts)
vote := func(val int) int {
client := http.Client{}
req, err := http.NewRequest(http.MethodPut,
fmt.Sprintf("%s/api/v1/vote/%s?site=radio-t&url=https://radio-t.com/blah&vote=%d", ts.URL, id1, val), nil)
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
resp, err := client.Do(req)
assert.Nil(t, err)
return resp.StatusCode
}
assert.Equal(t, 200, vote(1), "first vote allowed")
assert.Equal(t, 400, vote(1), "second vote rejected")
body, code := get(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah", ts.URL, id1))
assert.Equal(t, 200, code)
cr := store.Comment{}
err := json.Unmarshal([]byte(body), &cr)
assert.Nil(t, err)
assert.Equal(t, 1, cr.Score)
assert.Equal(t, map[string]bool{"dev": true}, cr.Votes)
assert.Equal(t, 200, vote(-1), "opposite vote allowed")
body, code = get(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah", ts.URL, id1))
assert.Equal(t, 200, code)
cr = store.Comment{}
err = json.Unmarshal([]byte(body), &cr)
assert.Nil(t, err)
assert.Equal(t, 0, cr.Score)
assert.Equal(t, map[string]bool{}, cr.Votes)
}
func TestRest_UserAllData(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts)
// write 3 comments
user := store.User{ID: "dev", Name: "user name 1"}
c1 := store.Comment{User: user, Text: "test test #1", Locator: store.Locator{SiteID: "radio-t",
URL: "https://radio-t.com/blah1"}, Timestamp: time.Date(2018, 05, 27, 1, 14, 10, 0, time.Local)}
c2 := store.Comment{User: user, Text: "test test #2", ParentID: "p1", Locator: store.Locator{SiteID: "radio-t",
URL: "https://radio-t.com/blah1"}, Timestamp: time.Date(2018, 05, 27, 1, 14, 20, 0, time.Local)}
c3 := store.Comment{User: user, Text: "test test #3", ParentID: "p1", Locator: store.Locator{SiteID: "radio-t",
URL: "https://radio-t.com/blah1"}, Timestamp: time.Date(2018, 05, 27, 1, 14, 25, 0, time.Local)}
_, err := srv.DataService.Create(c1)
require.Nil(t, err, "%+v", err)
_, err = srv.DataService.Create(c2)
require.Nil(t, err)
_, err = srv.DataService.Create(c3)
require.Nil(t, err)
client := &http.Client{Timeout: 1 * time.Second}
req, err := http.NewRequest("GET", ts.URL+"/api/v1/userdata?site=radio-t", nil)
require.Nil(t, err)
req.SetBasicAuth("dev", "password")
resp, err := client.Do(req)
require.Nil(t, err)
require.Equal(t, 200, resp.StatusCode)
require.Equal(t, "application/gzip", resp.Header.Get("Content-Type"))
ungzReader, err := gzip.NewReader(resp.Body)
assert.NoError(t, err)
ungzBody, err := ioutil.ReadAll(ungzReader)
assert.NoError(t, err)
assert.True(t, strings.HasPrefix(string(ungzBody),
`{"info": {"name":"developer one","id":"dev","picture":"/api/v1/avatar/remark.image","admin":true}, "comments":[{`))
assert.Equal(t, 3, strings.Count(string(ungzBody), `"text":`), "3 comments inside")
t.Logf("%s", string(ungzBody))
parsed := struct {
Info store.User `json:"info"`
Comments []store.Comment `json:"comments"`
}{}
err = json.Unmarshal(ungzBody, &parsed)
assert.Nil(t, err)
assert.Equal(t, store.User{Name: "developer one", ID: "dev", Picture: "/api/v1/avatar/remark.image", Admin: true}, parsed.Info)
assert.Equal(t, 3, len(parsed.Comments))
req, err = http.NewRequest("GET", ts.URL+"/api/v1/userdata?site=radio-t", nil)
require.Nil(t, err)
resp, err = client.Do(req)
require.Nil(t, err)
require.Equal(t, 401, resp.StatusCode)
}
func TestRest_UserAllDataManyComments(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts)
user := store.User{ID: "dev", Name: "user name 1"}
c := store.Comment{User: user, Text: "test test #1", Locator: store.Locator{SiteID: "radio-t",
URL: "https://radio-t.com/blah1"}, Timestamp: time.Date(2018, 05, 27, 1, 14, 10, 0, time.Local)}
for i := 0; i < 478; i++ {
c.ID = fmt.Sprintf("id-%03d", i)
c.Timestamp = c.Timestamp.Add(time.Second)
_, err := srv.DataService.Create(c)
require.Nil(t, err)
}
client := &http.Client{Timeout: 1 * time.Second}
req, err := http.NewRequest("GET", ts.URL+"/api/v1/userdata?site=radio-t", nil)
require.Nil(t, err)
req.SetBasicAuth("dev", "password")
resp, err := client.Do(req)
require.Nil(t, err)
require.Equal(t, 200, resp.StatusCode)
require.Equal(t, "application/gzip", resp.Header.Get("Content-Type"))
ungzReader, err := gzip.NewReader(resp.Body)
assert.NoError(t, err)
ungzBody, err := ioutil.ReadAll(ungzReader)
assert.NoError(t, err)
assert.True(t, strings.HasPrefix(string(ungzBody),
`{"info": {"name":"developer one","id":"dev","picture":"/api/v1/avatar/remark.image","admin":true}, "comments":[{`))
assert.Equal(t, 478, strings.Count(string(ungzBody), `"text":`), "478 comments inside")
}
func TestRest_DeleteMe(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts)
client := http.Client{}
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/v1/deleteme?site=radio-t", ts.URL), nil)
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
resp, err := client.Do(req)
assert.Nil(t, err)
assert.Equal(t, 200, resp.StatusCode)
body, err := ioutil.ReadAll(resp.Body)
assert.Nil(t, err)
m := map[string]string{}
err = json.Unmarshal(body, &m)
assert.Nil(t, err)
assert.Equal(t, "radio-t", m["site"])
assert.Equal(t, "dev", m["user_id"])
token := m["token"]
claims, err := srv.Authenticator.JWTService.Parse(token)
assert.Nil(t, err)
assert.Equal(t, "dev", claims.User.ID)
assert.Equal(t, "https://demo.remark42.com/api/v1/admin/deleteme?token="+token, m["link"])
req, err = http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/v1/deleteme?site=radio-t", ts.URL), nil)
assert.Nil(t, err)
resp, err = client.Do(req)
assert.Nil(t, err)
assert.Equal(t, 401, resp.StatusCode)
}
-311
View File
@@ -1,311 +0,0 @@
package api
import (
"crypto/sha1"
"encoding/base64"
"log"
"net/http"
"strconv"
"strings"
"github.com/go-chi/chi"
"github.com/go-chi/render"
blackfriday "gopkg.in/russross/blackfriday.v2"
"github.com/umputun/remark/app/rest"
"github.com/umputun/remark/app/rest/cache"
"github.com/umputun/remark/app/store"
)
// GET /find?site=siteID&url=post-url&format=[tree|plain]&sort=[+/-time|+/-score]
// find comments for given post. Returns in tree or plain formats, sorted
func (s *Rest) 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")
if strings.HasPrefix(sort, " ") { // restore + replaced by " "
sort = "+" + sort[1:]
}
log.Printf("[DEBUG] get comments for %+v, sort %s, format %s", locator, sort, r.URL.Query().Get("format"))
data, err := s.Cache.Get(cache.Key(cache.URLKey(r), locator.SiteID, locator.URL), func() ([]byte, error) {
comments, e := s.DataService.Find(locator, sort)
if e != nil {
return nil, e
}
maskedComments := s.adminService.alterComments(comments, r)
var b []byte
switch r.URL.Query().Get("format") {
case "tree":
tree := rest.MakeTree(maskedComments, sort, s.ReadOnlyAge)
if s.DataService.IsReadOnly(locator) {
tree.Info.ReadOnly = true
}
b, e = encodeJSONWithHTML(tree)
default:
withInfo := commentsWithInfo{Comments: maskedComments}
if info, ee := s.DataService.Info(locator, s.ReadOnlyAge); ee == nil {
withInfo.Info = info
}
b, e = encodeJSONWithHTML(withInfo)
}
return b, e
})
if err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't find comments")
return
}
renderJSONFromBytes(w, r, data)
}
// POST /preview, body is a comment, returns rendered html
func (s *Rest) previewCommentCtrl(w http.ResponseWriter, r *http.Request) {
comment := store.Comment{}
if err := render.DecodeJSON(http.MaxBytesReader(w, r.Body, hardBodyLimit), &comment); err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't bind comment")
return
}
user, err := rest.GetUserInfo(r)
if err != nil { // this not suppose to happen (handled by Auth), just dbl-check
rest.SendErrorJSON(w, r, http.StatusUnauthorized, err, "can't get user info")
return
}
comment.User = user
comment.Orig = comment.Text
if err = s.DataService.ValidateComment(&comment); err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "invalid comment")
return
}
//comment.Text = string(blackfriday.Run([]byte(comment.Text),
// blackfriday.WithRenderer(bfchroma.NewRenderer(bfchroma.WithoutAutodetect()))))
comment.Text = string(blackfriday.Run([]byte(comment.Text), blackfriday.WithExtensions(mdExt)))
comment.Text = s.ImageProxy.Convert(comment.Text)
comment.Sanitize()
render.HTML(w, r, comment.Text)
}
// GET /info?site=siteID&url=post-url - get info about the post
func (s *Rest) infoCtrl(w http.ResponseWriter, r *http.Request) {
locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")}
data, err := s.Cache.Get(cache.Key(cache.URLKey(r), locator.SiteID, locator.URL), func() ([]byte, error) {
info, e := s.DataService.Info(locator, s.ReadOnlyAge)
if e != nil {
return nil, e
}
return encodeJSONWithHTML(info)
})
if err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get post info")
return
}
renderJSONFromBytes(w, r, data)
}
// GET /last/{limit}?site=siteID - last comments for the siteID, across all posts, sorted by time
func (s *Rest) lastCommentsCtrl(w http.ResponseWriter, r *http.Request) {
siteID := r.URL.Query().Get("site")
log.Printf("[DEBUG] get last comments for %s", siteID)
limit, err := strconv.Atoi(chi.URLParam(r, "limit"))
if err != nil {
limit = 0
}
data, err := s.Cache.Get(cache.Key(cache.URLKey(r), "last", siteID), func() ([]byte, error) {
comments, e := s.DataService.Last(siteID, limit)
if e != nil {
return nil, e
}
comments = s.adminService.alterComments(comments, r)
// filter deleted from last comments view. Blocked marked as deleted and will sneak in without
filterDeleted := []store.Comment{}
for _, c := range comments {
if c.Deleted {
continue
}
filterDeleted = append(filterDeleted, c)
}
return encodeJSONWithHTML(filterDeleted)
})
if err != nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't get last comments")
return
}
renderJSONFromBytes(w, r, data)
}
// GET /id/{id}?site=siteID&url=post-url - gets a comment by id
func (s *Rest) commentByIDCtrl(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
siteID := r.URL.Query().Get("site")
url := r.URL.Query().Get("url")
log.Printf("[DEBUG] get comments by id %s, %s %s", id, siteID, url)
comment, err := s.DataService.Get(store.Locator{SiteID: siteID, URL: url}, id)
if err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get comment by id")
return
}
comment = s.adminService.alterComments([]store.Comment{comment}, r)[0]
render.Status(r, http.StatusOK)
renderJSONWithHTML(w, r, comment)
}
// GET /comments?site=siteID&user=id - returns comments for given userID
func (s *Rest) findUserCommentsCtrl(w http.ResponseWriter, r *http.Request) {
userID := r.URL.Query().Get("user")
siteID := r.URL.Query().Get("site")
limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
if err != nil {
limit = 0
}
resp := struct {
Comments []store.Comment `json:"comments,omitempty"`
Count int `json:"count,omitempty"`
}{}
log.Printf("[DEBUG] get comments for userID %s, %s", userID, siteID)
data, err := s.Cache.Get(cache.Key(cache.URLKey(r), userID, siteID), func() ([]byte, error) {
comments, e := s.DataService.User(siteID, userID, limit, 0)
if e != nil {
return nil, e
}
comments = s.adminService.alterComments(comments, r)
comments = filterComments(comments, func(c store.Comment) bool { return !c.Deleted })
count, e := s.DataService.UserCount(siteID, userID)
if e != nil {
return nil, e
}
resp.Comments, resp.Count = comments, count
return encodeJSONWithHTML(resp)
})
if err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get comment by user id")
return
}
renderJSONFromBytes(w, r, data)
}
// GET /config?site=siteID - returns configuration
func (s *Rest) configCtrl(w http.ResponseWriter, r *http.Request) {
type config struct {
Version string `json:"version"`
EditDuration int `json:"edit_duration"`
MaxCommentSize int `json:"max_comment_size"`
Admins []string `json:"admins"`
AdminEmail string `json:"admin_email"`
Auth []string `json:"auth_providers"`
LowScore int `json:"low_score"`
CriticalScore int `json:"critical_score"`
ReadOnlyAge int `json:"readonly_age"`
}
cnf := config{
Version: s.Version,
EditDuration: int(s.DataService.EditDuration.Seconds()),
MaxCommentSize: s.DataService.MaxCommentSize,
Admins: s.Authenticator.Admins,
AdminEmail: s.Authenticator.AdminEmail,
LowScore: s.ScoreThresholds.Low,
CriticalScore: s.ScoreThresholds.Critical,
ReadOnlyAge: s.ReadOnlyAge,
}
cnf.Auth = []string{}
for _, ap := range s.Authenticator.Providers {
cnf.Auth = append(cnf.Auth, ap.Name)
}
if cnf.Admins == nil { // prevent json serialization to nil
cnf.Admins = []string{}
}
render.Status(r, http.StatusOK)
render.JSON(w, r, cnf)
}
// GET /count?site=siteID&url=post-url - get number of comments for given post
func (s *Rest) countCtrl(w http.ResponseWriter, r *http.Request) {
locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")}
count, err := s.DataService.Count(locator)
if err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get count")
return
}
render.JSON(w, r, JSON{"count": count, "locator": locator})
}
// POST /count?site=siteID - get number of comments for posts from post body
func (s *Rest) countMultiCtrl(w http.ResponseWriter, r *http.Request) {
siteID := r.URL.Query().Get("site")
posts := []string{}
if err := render.DecodeJSON(http.MaxBytesReader(w, r.Body, hardBodyLimit), &posts); err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get list of posts from request")
return
}
// key could be long for multiple posts, make it sha1
key := cache.URLKey(r) + strings.Join(posts, ",")
hasher := sha1.New()
if _, err := hasher.Write([]byte(key)); err != nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't make sha1 for list of urls")
return
}
sha := base64.URLEncoding.EncodeToString(hasher.Sum(nil))
data, err := s.Cache.Get(cache.Key(sha, siteID), func() ([]byte, error) {
counts, e := s.DataService.Counts(siteID, posts)
if e != nil {
return nil, e
}
return encodeJSONWithHTML(counts)
})
if err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get counts for "+siteID)
return
}
renderJSONFromBytes(w, r, data)
}
// GET /list?site=siteID&limit=50&skip=10 - list posts with comments
func (s *Rest) listCtrl(w http.ResponseWriter, r *http.Request) {
siteID := r.URL.Query().Get("site")
limit, skip := 0, 0
if v, err := strconv.Atoi(r.URL.Query().Get("limit")); err == nil {
limit = v
}
if v, err := strconv.Atoi(r.URL.Query().Get("skip")); err == nil {
skip = v
}
data, err := s.Cache.Get(cache.Key(cache.URLKey(r), siteID), func() ([]byte, error) {
posts, e := s.DataService.List(siteID, limit, skip)
if e != nil {
return nil, e
}
return encodeJSONWithHTML(posts)
})
if err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get list of comments for "+siteID)
return
}
renderJSONFromBytes(w, r, data)
}
-449
View File
@@ -1,449 +0,0 @@
package api
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/umputun/remark/app/rest"
"github.com/umputun/remark/app/store"
)
func TestRest_Ping(t *testing.T) {
srv, ts := prep(t)
require.NotNil(t, srv)
defer cleanup(ts)
res, code := get(t, ts.URL+"/api/v1/ping")
assert.Equal(t, "pong", res)
assert.Equal(t, 200, code)
}
func TestRest_Preview(t *testing.T) {
srv, ts := prep(t)
require.NotNil(t, srv)
defer cleanup(ts)
resp, err := post(t, ts.URL+"/api/v1/preview", `{"text": "test 123", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`)
assert.Nil(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
b, err := ioutil.ReadAll(resp.Body)
assert.Nil(t, err)
assert.Equal(t, "<p>test 123</p>\n", string(b))
}
func TestRest_PreviewWithMD(t *testing.T) {
srv, ts := prep(t)
require.NotNil(t, srv)
defer cleanup(ts)
text := `
# h1
BKT
func TestRest_Preview(t *testing.T) {
srv, ts := prep(t)
require.NotNil(t, srv)
}
BKT
`
text = strings.Replace(text, "BKT", "```", -1)
j := fmt.Sprintf(`{"text": "%s", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`, text)
j = strings.Replace(j, "\n", "\\n", -1)
t.Log(j)
resp, err := post(t, ts.URL+"/api/v1/preview", j)
assert.Nil(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
b, err := ioutil.ReadAll(resp.Body)
assert.Nil(t, err)
assert.Equal(t, "<h1>h1</h1>\n\n<pre><code>func TestRest_Preview(t *testing.T) {\nsrv, ts := prep(t)\n require.NotNil(t, srv)\n}\n</code></pre>\n", string(b))
}
func TestRest_Find(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts)
_, code := get(t, ts.URL+"/api/v1/find?site=radio-t&url=https://radio-t.com/blah1")
assert.Equal(t, 400, code, "nothing in")
c1 := store.Comment{Text: "test test #1", ParentID: "",
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}}
id1 := addComment(t, c1, ts)
c2 := store.Comment{Text: "test test #2", ParentID: id1,
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}}
id2 := addComment(t, c2, ts)
assert.NotEqual(t, id1, id2)
// get sorted by +time
res, code := get(t, ts.URL+"/api/v1/find?site=radio-t&url=https://radio-t.com/blah1&sort=+time")
assert.Equal(t, 200, code)
comments := commentsWithInfo{}
err := json.Unmarshal([]byte(res), &comments)
assert.Nil(t, err)
assert.Equal(t, 2, len(comments.Comments), "should have 2 comments")
assert.Equal(t, id1, comments.Comments[0].ID)
assert.Equal(t, id2, comments.Comments[1].ID)
assert.Equal(t, "https://radio-t.com/blah1", comments.Info.URL)
assert.Equal(t, 2, comments.Info.Count)
assert.Equal(t, false, comments.Info.ReadOnly)
assert.True(t, comments.Info.FirstTS.Before(comments.Info.LastTS))
// get sorted by -time
res, code = get(t, ts.URL+"/api/v1/find?site=radio-t&url=https://radio-t.com/blah1&sort=-time")
assert.Equal(t, 200, code)
err = json.Unmarshal([]byte(res), &comments)
assert.Nil(t, err)
assert.Equal(t, 2, len(comments.Comments), "should have 2 comments")
assert.Equal(t, id1, comments.Comments[1].ID)
assert.Equal(t, id2, comments.Comments[0].ID)
// get in tree mode
tree := rest.Tree{}
res, code = get(t, ts.URL+"/api/v1/find?site=radio-t&url=https://radio-t.com/blah1&format=tree")
assert.Equal(t, 200, code)
err = json.Unmarshal([]byte(res), &tree)
assert.Nil(t, err)
assert.Equal(t, 1, len(tree.Nodes))
assert.Equal(t, 1, len(tree.Nodes[0].Replies))
assert.Equal(t, 2, tree.Info.Count)
assert.Equal(t, "https://radio-t.com/blah1", tree.Info.URL)
assert.False(t, tree.Info.ReadOnly, "post is fresh")
}
func TestRest_FindAge(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts)
c1 := store.Comment{Text: "test test #1", ParentID: "", Timestamp: time.Now().AddDate(0, 0, -5),
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}, User: store.User{ID: "u1"}}
_, err := srv.DataService.Create(c1)
require.Nil(t, err)
c2 := store.Comment{Text: "test test #2", ParentID: "", Timestamp: time.Now().AddDate(0, 0, -15),
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah2"}, User: store.User{ID: "u1"}}
_, err = srv.DataService.Create(c2)
require.Nil(t, err)
tree := rest.Tree{}
res, code := get(t, ts.URL+"/api/v1/find?site=radio-t&url=https://radio-t.com/blah1&format=tree")
assert.Equal(t, 200, code)
err = json.Unmarshal([]byte(res), &tree)
assert.Nil(t, err)
assert.Equal(t, "https://radio-t.com/blah1", tree.Info.URL)
assert.False(t, tree.Info.ReadOnly, "post is fresh")
res, code = get(t, ts.URL+"/api/v1/find?site=radio-t&url=https://radio-t.com/blah2&format=tree")
assert.Equal(t, 200, code)
err = json.Unmarshal([]byte(res), &tree)
assert.Nil(t, err)
assert.Equal(t, "https://radio-t.com/blah2", tree.Info.URL)
assert.True(t, tree.Info.ReadOnly, "post is old")
}
func TestRest_FindReadOnly(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts)
c1 := store.Comment{Text: "test test #1", ParentID: "", Timestamp: time.Now().AddDate(0, 0, -1),
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}, User: store.User{ID: "u1"}}
_, err := srv.DataService.Create(c1)
require.Nil(t, err)
c2 := store.Comment{Text: "test test #2", ParentID: "", Timestamp: time.Now().AddDate(0, 0, -2),
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah2"}, User: store.User{ID: "u1"}}
_, err = srv.DataService.Create(c2)
require.Nil(t, err)
// set post to read-only
client := http.Client{}
req, err := http.NewRequest(http.MethodPut,
fmt.Sprintf("%s/api/v1/admin/readonly?site=radio-t&url=https://radio-t.com/blah1&ro=1", ts.URL), nil)
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
_, err = client.Do(req)
require.Nil(t, err)
tree := rest.Tree{}
res, code := get(t, ts.URL+"/api/v1/find?site=radio-t&url=https://radio-t.com/blah1&format=tree")
assert.Equal(t, 200, code)
err = json.Unmarshal([]byte(res), &tree)
require.Nil(t, err)
assert.Equal(t, "https://radio-t.com/blah1", tree.Info.URL)
assert.True(t, tree.Info.ReadOnly, "post is ro")
tree = rest.Tree{}
res, code = get(t, ts.URL+"/api/v1/find?site=radio-t&url=https://radio-t.com/blah2&format=tree")
assert.Equal(t, 200, code)
err = json.Unmarshal([]byte(res), &tree)
require.Nil(t, err)
assert.Equal(t, "https://radio-t.com/blah2", tree.Info.URL)
assert.False(t, tree.Info.ReadOnly, "post is writable")
}
func TestRest_Last(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts)
c1 := store.Comment{Text: "test test #1", ParentID: "p1",
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}}
c2 := store.Comment{Text: "test test #2", ParentID: "p1",
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah2"}}
// add 3 comments
addComment(t, c1, ts)
id1 := addComment(t, c1, ts)
id2 := addComment(t, c2, ts)
res, code := get(t, ts.URL+"/api/v1/last/2?site=radio-t")
assert.Equal(t, 200, code)
comments := []store.Comment{}
err := json.Unmarshal([]byte(res), &comments)
assert.Nil(t, err)
assert.Equal(t, 2, len(comments), "should have 2 comments")
assert.Equal(t, id1, comments[1].ID)
assert.Equal(t, id2, comments[0].ID)
res, code = get(t, ts.URL+"/api/v1/last/5?site=radio-t")
assert.Equal(t, 200, code)
err = json.Unmarshal([]byte(res), &comments)
assert.Nil(t, err)
assert.Equal(t, 3, len(comments), "should have 3 comments")
res, code = get(t, ts.URL+"/api/v1/last/X?site=radio-t")
assert.Equal(t, 200, code)
err = json.Unmarshal([]byte(res), &comments)
assert.Nil(t, err)
assert.Equal(t, 3, len(comments), "should have 3 comments")
err = srv.DataService.Delete(store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}, id1, store.SoftDelete)
assert.Nil(t, err)
res, code = get(t, ts.URL+"/api/v1/last/5?site=radio-t")
assert.Equal(t, 200, code)
err = json.Unmarshal([]byte(res), &comments)
assert.Nil(t, err)
assert.Equal(t, 2, len(comments), "should have 2 comments")
}
func TestRest_FindUserComments(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts)
c1 := store.Comment{Text: "test test #1",
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}}
c2 := store.Comment{Text: "test test #3", ParentID: "p1",
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah2"}}
// add 3 comments
addComment(t, c1, ts)
addComment(t, c2, ts)
addComment(t, c2, ts)
// add one deleted
id := addComment(t, c2, ts)
err := srv.DataService.Delete(c2.Locator, id, store.SoftDelete)
assert.NoError(t, err)
_, code := get(t, ts.URL+"/api/v1/comments?site=radio-t&user=blah")
assert.Equal(t, 400, code, "noting for user blah")
res, code := get(t, ts.URL+"/api/v1/comments?site=radio-t&user=dev")
assert.Equal(t, 200, code)
resp := struct {
Comments []store.Comment
Count int
}{}
err = json.Unmarshal([]byte(res), &resp)
assert.Nil(t, err)
assert.Equal(t, 3, len(resp.Comments), "should have 3 comments")
assert.Equal(t, 4, resp.Count, "should have 3 count")
}
func TestRest_UserInfo(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts)
body, code := getWithAuth(t, ts.URL+"/api/v1/user?site=radio-t")
assert.Equal(t, 200, code)
user := store.User{}
err := json.Unmarshal([]byte(body), &user)
assert.Nil(t, err)
assert.Equal(t, store.User{Name: "developer one", ID: "dev",
Picture: "/api/v1/avatar/remark.image", Admin: true, Blocked: false, IP: ""}, user)
}
func TestRest_Count(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts)
c1 := store.Comment{Text: "test test #1",
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}}
c2 := store.Comment{Text: "test test #2", ParentID: "p1",
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah2"}}
addComment(t, c1, ts)
addComment(t, c1, ts)
addComment(t, c1, ts)
addComment(t, c2, ts)
addComment(t, c2, ts)
body, code := get(t, ts.URL+"/api/v1/count?site=radio-t&url=https://radio-t.com/blah1")
assert.Equal(t, 200, code)
j := JSON{}
err := json.Unmarshal([]byte(body), &j)
assert.Nil(t, err)
assert.Equal(t, 3.0, j["count"])
body, code = get(t, ts.URL+"/api/v1/count?site=radio-t&url=https://radio-t.com/blah2")
assert.Equal(t, 200, code)
err = json.Unmarshal([]byte(body), &j)
assert.Nil(t, err)
assert.Equal(t, 2.0, j["count"])
}
func TestRest_Counts(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts)
c1 := store.Comment{Text: "test test #1",
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}}
c2 := store.Comment{Text: "test test #2", ParentID: "p1",
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah2"}}
addComment(t, c1, ts)
addComment(t, c1, ts)
addComment(t, c1, ts)
addComment(t, c2, ts)
addComment(t, c2, ts)
resp, err := post(t, ts.URL+"/api/v1/counts?site=radio-t", `["https://radio-t.com/blah1","https://radio-t.com/blah2"]`)
assert.Nil(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
body, err := ioutil.ReadAll(resp.Body)
assert.Nil(t, err)
j := []store.PostInfo{}
err = json.Unmarshal(body, &j)
assert.Nil(t, err)
assert.Equal(t, []store.PostInfo([]store.PostInfo{{URL: "https://radio-t.com/blah1", Count: 3},
{URL: "https://radio-t.com/blah2", Count: 2}}), j)
}
func TestRest_List(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts)
c1 := store.Comment{Text: "test test #1",
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}}
c2 := store.Comment{Text: "test test #2", ParentID: "p1",
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah2"}}
addComment(t, c1, ts)
addComment(t, c1, ts)
addComment(t, c1, ts)
addComment(t, c2, ts)
addComment(t, c2, ts)
body, code := get(t, ts.URL+"/api/v1/list?site=radio-t")
assert.Equal(t, 200, code)
pi := []store.PostInfo{}
err := json.Unmarshal([]byte(body), &pi)
assert.Nil(t, err)
assert.Equal(t, "https://radio-t.com/blah2", pi[0].URL)
assert.Equal(t, 2, pi[0].Count)
assert.Equal(t, "https://radio-t.com/blah1", pi[1].URL)
assert.Equal(t, 3, pi[1].Count)
}
func TestRest_Config(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts)
body, code := get(t, ts.URL+"/api/v1/config?site=radio-t")
assert.Equal(t, 200, code)
j := JSON{}
err := json.Unmarshal([]byte(body), &j)
assert.Nil(t, err)
assert.Equal(t, 300., j["edit_duration"])
assert.EqualValues(t, []interface{}([]interface{}{"a1", "a2"}), j["admins"])
assert.Equal(t, "admin@remark-42.com", j["admin_email"])
assert.Equal(t, 4000., j["max_comment_size"])
assert.Equal(t, -5., j["low_score"])
assert.Equal(t, -10., j["critical_score"])
assert.Equal(t, 10., j["readonly_age"])
t.Logf("%+v", j)
}
func TestRest_Info(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts)
srv.ReadOnlyAge = 10000000 // make sure we don't hit read-only
user := store.User{ID: "user1", Name: "user name 1"}
c1 := store.Comment{User: user, Text: "test test #1", Locator: store.Locator{SiteID: "radio-t",
URL: "https://radio-t.com/blah1"}, Timestamp: time.Date(2018, 05, 27, 1, 14, 10, 0, time.Local)}
c2 := store.Comment{User: user, Text: "test test #2", ParentID: "p1", Locator: store.Locator{SiteID: "radio-t",
URL: "https://radio-t.com/blah1"}, Timestamp: time.Date(2018, 05, 27, 1, 14, 20, 0, time.Local)}
c3 := store.Comment{User: user, Text: "test test #3", ParentID: "p1", Locator: store.Locator{SiteID: "radio-t",
URL: "https://radio-t.com/blah1"}, Timestamp: time.Date(2018, 05, 27, 1, 14, 25, 0, time.Local)}
_, err := srv.DataService.Create(c1)
require.Nil(t, err, "%+v", err)
_, err = srv.DataService.Create(c2)
require.Nil(t, err)
_, err = srv.DataService.Create(c3)
require.Nil(t, err)
body, code := get(t, ts.URL+"/api/v1/info?site=radio-t&url=https://radio-t.com/blah1")
assert.Equal(t, 200, code)
info := store.PostInfo{}
err = json.Unmarshal([]byte(body), &info)
assert.Nil(t, err)
exp := store.PostInfo{URL: "https://radio-t.com/blah1", Count: 3,
FirstTS: time.Date(2018, 05, 27, 1, 14, 10, 0, time.Local), LastTS: time.Date(2018, 05, 27, 1, 14, 25, 0, time.Local)}
assert.Equal(t, exp, info)
_, code = get(t, ts.URL+"/api/v1/info?site=radio-t&url=https://radio-t.com/blah-no")
assert.Equal(t, 400, code)
_, code = get(t, ts.URL+"/api/v1/info?site=radio-t-no&url=https://radio-t.com/blah-no")
assert.Equal(t, 400, code)
}
func TestRest_Robots(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts)
body, code := get(t, ts.URL+"/robots.txt")
assert.Equal(t, 200, code)
assert.Equal(t, "User-agent: *\nDisallow: /auth/\nDisallow: /api/\nAllow: /api/v1/find\n"+
"Allow: /api/v1/last\nAllow: /api/v1/id\nAllow: /api/v1/count\nAllow: /api/v1/counts\n"+
"Allow: /api/v1/list\nAllow: /api/v1/config\nAllow: /api/v1/img\nAllow: /api/v1/avatar\n", string(body))
}
-146
View File
@@ -1,146 +0,0 @@
package api
import (
"bytes"
"encoding/json"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"time"
"github.com/coreos/bbolt"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/umputun/remark/app/migrator"
"github.com/umputun/remark/app/rest/auth"
"github.com/umputun/remark/app/rest/proxy"
"github.com/umputun/remark/app/store"
"github.com/umputun/remark/app/store/engine"
"github.com/umputun/remark/app/store/service"
)
var testDb = "/tmp/test-remark.db"
var testHTML = "/tmp/test-remark.html"
func TestRest_FileServer(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts)
body, code := get(t, ts.URL+"/web/test-remark.html")
assert.Equal(t, 200, code)
assert.Equal(t, "some html", body)
}
func TestRest_Shutdown(t *testing.T) {
srv := Rest{Authenticator: auth.Authenticator{}, AvatarProxy: &proxy.Avatar{Store: proxy.NewFSAvatarStore("/tmp", 300),
RoutePath: "/api/v1/avatar"}, ImageProxy: &proxy.Image{}}
go func() {
time.Sleep(100 * time.Millisecond)
srv.Shutdown()
}()
st := time.Now()
srv.Run(0)
assert.True(t, time.Since(st).Seconds() < 1, "should take about 100ms")
}
func prep(t *testing.T) (srv *Rest, ts *httptest.Server) {
b, err := engine.NewBoltDB(bolt.Options{}, engine.BoltSite{FileName: testDb, SiteID: "radio-t"})
require.Nil(t, err)
dataStore := &service.DataStore{Interface: b, EditDuration: 5 * time.Minute, MaxCommentSize: 4000, Secret: "123456"}
srv = &Rest{
DataService: dataStore,
Authenticator: auth.Authenticator{
DevPasswd: "password",
Providers: nil,
Admins: []string{"a1", "a2"},
AdminEmail: "admin@remark-42.com",
JWTService: auth.NewJWT("12345", false, time.Minute),
},
Exporter: &migrator.Remark{DataStore: dataStore},
Cache: &mockCache{},
WebRoot: "/tmp",
RemarkURL: "https://demo.remark42.com",
AvatarProxy: &proxy.Avatar{Store: proxy.NewFSAvatarStore("/tmp", 300), RoutePath: "/api/v1/avatar"},
ImageProxy: &proxy.Image{},
ReadOnlyAge: 10,
}
srv.ScoreThresholds.Low, srv.ScoreThresholds.Critical = -5, -10
err = ioutil.WriteFile(testHTML, []byte("some html"), 0700)
assert.Nil(t, err)
ts = httptest.NewServer(srv.routes())
return srv, ts
}
func get(t *testing.T, url string) (string, int) {
r, err := http.Get(url)
require.Nil(t, err)
defer r.Body.Close()
body, err := ioutil.ReadAll(r.Body)
require.Nil(t, err)
return string(body), r.StatusCode
}
func getWithAuth(t *testing.T, url string) (string, int) {
client := &http.Client{Timeout: 5 * time.Second}
req, err := http.NewRequest("GET", url, nil)
require.Nil(t, err)
req.SetBasicAuth("dev", "password")
r, err := client.Do(req)
require.Nil(t, err)
defer r.Body.Close()
body, err := ioutil.ReadAll(r.Body)
assert.Nil(t, err)
return string(body), r.StatusCode
}
func post(t *testing.T, url string, body string) (*http.Response, error) {
client := &http.Client{Timeout: 5 * time.Second}
req, err := http.NewRequest("POST", url, strings.NewReader(body))
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
return client.Do(req)
}
func addComment(t *testing.T, c store.Comment, ts *httptest.Server) string {
b, err := json.Marshal(c)
assert.Nil(t, err, "can't marshal comment %+v", c)
client := &http.Client{Timeout: 5 * time.Second}
req, err := http.NewRequest("POST", ts.URL+"/api/v1/comment", bytes.NewBuffer(b))
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
resp, err := client.Do(req)
assert.Nil(t, err)
assert.Equal(t, http.StatusCreated, resp.StatusCode)
b, err = ioutil.ReadAll(resp.Body)
assert.Nil(t, err)
crResp := JSON{}
err = json.Unmarshal(b, &crResp)
assert.Nil(t, err)
time.Sleep(time.Nanosecond * 10)
return crResp["id"].(string)
}
func cleanup(ts *httptest.Server) {
ts.Close()
os.Remove(testDb)
os.Remove(testHTML)
}
type mockCache struct{}
func (mc *mockCache) Get(key string, fn func() ([]byte, error)) (data []byte, err error) {
return fn()
}
func (mc *mockCache) Flush(scopes ...string) {}
-129
View File
@@ -1,129 +0,0 @@
package api
import (
"fmt"
"log"
"net/http"
"time"
"github.com/go-chi/chi"
"github.com/gorilla/feeds"
"github.com/umputun/remark/app/rest"
"github.com/umputun/remark/app/rest/cache"
"github.com/umputun/remark/app/store"
)
const maxRssItems = 20
// ui uses links like <post-url>#remark42__comment-<comment-id>
const uiNav = "#remark42__comment-"
func (s *Rest) rssRoutes() chi.Router {
router := chi.NewRouter()
router.Get("/post", s.rssPostCommentsCtrl)
router.Get("/site", s.rssSiteCommentsCtrl)
return router
}
// GET /rss/post?site=siteID&url=post-url
func (s *Rest) rssPostCommentsCtrl(w http.ResponseWriter, r *http.Request) {
locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")}
log.Printf("[DEBUG] get rss for post %+v", locator)
data, err := s.Cache.Get(cache.Key(cache.URLKey(r), locator.SiteID, locator.URL), func() ([]byte, error) {
comments, e := s.DataService.Find(locator, "-time")
if e != nil {
return nil, e
}
comments = s.adminService.alterComments(comments, r)
rss, e := s.toRssFeed(locator.URL, comments)
if e != nil {
return nil, e
}
return []byte(rss), e
})
if err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't find comments")
return
}
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
w.WriteHeader(http.StatusOK)
if _, err := w.Write(data); err != nil {
log.Printf("[WARN] failed to send response to %s, %s", r.RemoteAddr, err)
}
}
// GET /rss/site?site=siteID
func (s *Rest) rssSiteCommentsCtrl(w http.ResponseWriter, r *http.Request) {
siteID := r.URL.Query().Get("site")
log.Printf("[DEBUG] get rss for site %s", siteID)
data, err := s.Cache.Get(cache.Key(cache.URLKey(r), siteID, "last"), func() ([]byte, error) {
comments, e := s.DataService.Last(siteID, maxRssItems)
if e != nil {
return nil, e
}
comments = s.adminService.alterComments(comments, r)
rss, e := s.toRssFeed(r.URL.Query().Get("site"), comments)
if e != nil {
return nil, e
}
return []byte(rss), e
})
if err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get last comments")
return
}
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
w.WriteHeader(http.StatusOK)
if _, err := w.Write(data); err != nil {
log.Printf("[WARN] failed to send response to %s, %s", r.RemoteAddr, err)
}
}
func (s *Rest) toRssFeed(url string, comments []store.Comment) (string, error) {
lastCommentTS := time.Unix(0, 0)
if len(comments) > 0 {
lastCommentTS = comments[0].Timestamp
}
feed := &feeds.Feed{
Title: "Remark42 comments",
Link: &feeds.Link{Href: url},
Description: "comment updates",
Created: lastCommentTS,
}
feed.Items = []*feeds.Item{}
for i, c := range comments {
f := feeds.Item{
Title: c.User.Name,
Link: &feeds.Link{Href: c.Locator.URL + uiNav + c.ID},
Description: c.Text,
Created: c.Timestamp,
Author: &feeds.Author{Name: c.User.Name},
}
if c.ParentID != "" {
// add indication to parent comment
parentComment, err := s.DataService.Get(c.Locator, c.ParentID)
if err == nil {
f.Title = fmt.Sprintf("%s > %s", c.User.Name, parentComment.User.Name)
} else {
log.Printf("[WARN] failed to get info about parent comment, %s", err)
}
}
feed.Items = append(feed.Items, &f)
if i > maxRssItems {
break
}
}
return feed.ToRss()
}
-181
View File
@@ -1,181 +0,0 @@
package api
import (
"fmt"
"regexp"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/umputun/remark/app/store"
)
func TestServer_RssPost(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts)
waitOnSecChange()
c1 := store.Comment{
Text: "test 123",
Locator: store.Locator{URL: "https://radio-t.com/blah1", SiteID: "radio-t"},
}
id1 := addComment(t, c1, ts)
pubDate := time.Now().Format(time.RFC1123Z)
res, code := get(t, ts.URL+"/api/v1/rss/post?site=radio-t&url=https://radio-t.com/blah1")
assert.Equal(t, 200, code)
t.Log(res)
expected := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
<title>Remark42 comments</title>
<link>https://radio-t.com/blah1</link>
<description>comment updates</description>
<pubDate>%s</pubDate>
<item>
<title>developer one</title>
<link>https://radio-t.com/blah1#remark42__comment-%s</link>
<description>&lt;p&gt;test 123&lt;/p&gt;&#xA;</description>
<author>developer one</author>
<pubDate>%s</pubDate>
</item>
</channel>
</rss>`, pubDate, id1, pubDate)
expected, res = cleanRssFormatting(expected, res)
assert.Equal(t, expected, res)
_, code = get(t, ts.URL+"/api/v1/rss/post?site=radio-t-bad&url=https://radio-t.com/blah1")
assert.Equal(t, 400, code)
}
func TestServer_RssSite(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts)
waitOnSecChange()
pubDate := time.Now().Format(time.RFC1123Z)
c1 := store.Comment{
Text: "test 123",
Locator: store.Locator{URL: "https://radio-t.com/blah10", SiteID: "radio-t"},
}
c2 := store.Comment{
Text: "xyz test",
Locator: store.Locator{URL: "https://radio-t.com/blah11", SiteID: "radio-t"},
}
id1 := addComment(t, c1, ts)
id2 := addComment(t, c2, ts)
res, code := get(t, ts.URL+"/api/v1/rss/site?site=radio-t")
assert.Equal(t, 200, code)
t.Log(res)
expected := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
<title>Remark42 comments</title>
<link>radio-t</link>
<description>comment updates</description>
<pubDate>%s</pubDate>
<item>
<title>developer one</title>
<link>https://radio-t.com/blah11#remark42__comment-%s</link>
<description>&lt;p&gt;xyz test&lt;/p&gt;&#xA;</description>
<author>developer one</author>
<pubDate>%s</pubDate>
</item>
<item>
<title>developer one</title>
<link>https://radio-t.com/blah10#remark42__comment-%s</link>
<description>&lt;p&gt;test 123&lt;/p&gt;&#xA;</description>
<author>developer one</author>
<pubDate>%s</pubDate>
</item>
</channel>
</rss>`, pubDate, id2, pubDate, id1, pubDate)
expected, res = cleanRssFormatting(expected, res)
assert.Equal(t, expected, res)
_, code = get(t, ts.URL+"/api/v1/rss/site?site=bad-radio-t")
assert.Equal(t, 400, code)
}
func TestServer_RssWithReply(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts)
waitOnSecChange()
pubDate := time.Now().Format(time.RFC1123Z)
c1 := store.Comment{
Text: "test 123",
Locator: store.Locator{URL: "https://radio-t.com/blah10", SiteID: "radio-t"},
}
c2 := store.Comment{
Text: "xyz test",
Locator: store.Locator{URL: "https://radio-t.com/blah10", SiteID: "radio-t"},
}
id1 := addComment(t, c1, ts)
c2.ParentID = id1
id2 := addComment(t, c2, ts)
res, code := get(t, ts.URL+"/api/v1/rss/post?site=radio-t&url=https://radio-t.com/blah10")
assert.Equal(t, 200, code)
t.Log(res)
expected := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
<title>Remark42 comments</title>
<link>https://radio-t.com/blah10</link>
<description>comment updates</description>
<pubDate>%s</pubDate>
<item>
<title>developer one &gt; developer one</title>
<link>https://radio-t.com/blah10#remark42__comment-%s</link>
<description>&lt;p&gt;xyz test&lt;/p&gt;&#xA;</description>
<author>developer one</author>
<pubDate>%s</pubDate>
</item>
<item>
<title>developer one</title>
<link>https://radio-t.com/blah10#remark42__comment-%s</link>
<description>&lt;p&gt;test 123&lt;/p&gt;&#xA;</description>
<author>developer one</author>
<pubDate>%s</pubDate>
</item>
</channel>
</rss>`, pubDate, id2, pubDate, id1, pubDate)
expected, res = cleanRssFormatting(expected, res)
assert.Equal(t, expected, res)
}
func waitOnSecChange() {
for {
if time.Now().Nanosecond() < 100000000 {
break
}
time.Sleep(10 * time.Nanosecond)
}
}
// clean formatting, i.e. multiple spaces, \t, \n
func cleanRssFormatting(expected, actual string) (string, string) {
reSpaces := regexp.MustCompile(`[\s\p{Zs}]{2,}`)
expected = strings.Replace(expected, "\n", " ", -1)
expected = strings.Replace(expected, "\t", " ", -1)
expected = reSpaces.ReplaceAllString(expected, " ")
actual = strings.Replace(actual, "\n", " ", -1)
actual = reSpaces.ReplaceAllString(actual, " ")
return expected, actual
}
-132
View File
@@ -1,132 +0,0 @@
// Package auth provides oauth2 support as well as related middlewares.
package auth
import (
"encoding/base64"
"log"
"net/http"
"strings"
"github.com/umputun/remark/app/rest"
"github.com/umputun/remark/app/store"
)
// Authenticator is top level auth object providing middlewares
type Authenticator struct {
JWTService *JWT
Providers []Provider
Admins []string
AdminEmail string
DevPasswd string
}
var devUser = store.User{
ID: "dev",
Name: "developer one",
Picture: "/api/v1/avatar/remark.image",
Admin: true,
}
// Auth middleware adds auth from session and populates user info
func (a *Authenticator) Auth(reqAuth bool) func(http.Handler) http.Handler {
f := func(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
if a.basicDevUser(w, r) { // fail-back to dev user if enabled
user := devUser
r = rest.SetUserInfo(r, user)
h.ServeHTTP(w, r)
return
}
claims, err := a.JWTService.Get(r)
if err != nil && reqAuth { // in full auth lack of session causes Unauthorized
log.Printf("[DEBUG] failed auth, %s", err)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
if err != nil { // in anonymous mode just pass it to the next handler
h.ServeHTTP(w, r)
return
}
if claims.User == nil && reqAuth {
log.Print("[DEBUG] failed auth, no user info presented in the claim")
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
if claims.User != nil { // if uinfo in token populate it to context
user := *claims.User
user.Admin = isAdmin(user.ID, a.Admins) // dbl-check for admin to reset admin flag even if token has it
// refresh token if it close to expiration
if _, err := a.JWTService.Refresh(w, r); err != nil {
log.Printf("[DEBUG] can't refresh jwt, %s", err)
}
r = rest.SetUserInfo(r, user)
}
h.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
return f
}
// AdminOnly allows access to admins
func (a *Authenticator) AdminOnly(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
user, err := rest.GetUserInfo(r)
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
if !user.Admin {
http.Error(w, "Access denied", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
func (a *Authenticator) basicDevUser(w http.ResponseWriter, r *http.Request) bool {
if a.DevPasswd == "" {
return false
}
s := strings.SplitN(r.Header.Get("Authorization"), " ", 2)
if len(s) != 2 {
return false
}
b, err := base64.StdEncoding.DecodeString(s[1])
if err != nil {
return false
}
pair := strings.SplitN(string(b), ":", 2)
if len(pair) != 2 {
return false
}
if pair[0] != "dev" || pair[1] != a.DevPasswd {
return false
}
return true
}
func isAdmin(userID string, admins []string) bool {
for _, admin := range admins {
if admin == userID {
return true
}
}
return false
}
-171
View File
@@ -1,171 +0,0 @@
package auth
import (
"encoding/base64"
"net/http"
"net/http/cookiejar"
"net/http/httptest"
"testing"
"time"
"github.com/go-chi/chi"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAuthJWTCookie(t *testing.T) {
a := Authenticator{DevPasswd: "123456", JWTService: NewJWT("xyz 12345", false, time.Hour)}
router := chi.NewRouter()
router.With(a.Auth(true)).Get("/auth", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(201)
})
server := httptest.NewServer(router)
defer server.Close()
expiration := int(time.Duration(365 * 24 * time.Hour).Seconds())
req, err := http.NewRequest("GET", server.URL+"/auth", nil)
require.Nil(t, err)
req.AddCookie(&http.Cookie{Name: "JWT", Value: testJwtValid, HttpOnly: true, Path: "/", MaxAge: expiration, Secure: false})
req.Header.Add("X-XSRF-TOKEN", "random id")
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Do(req)
require.NoError(t, err)
assert.Equal(t, 201, resp.StatusCode, "valid auth user")
req, err = http.NewRequest("GET", server.URL+"/auth", nil)
require.Nil(t, err)
req.AddCookie(&http.Cookie{Name: "JWT", Value: testJwtValid, HttpOnly: true, Path: "/", MaxAge: expiration, Secure: false})
req.Header.Add("X-XSRF-TOKEN", "wrong id")
resp, err = client.Do(req)
require.NoError(t, err)
assert.Equal(t, 401, resp.StatusCode, "xsrf mismatch")
req, err = http.NewRequest("GET", server.URL+"/auth", nil)
require.Nil(t, err)
req.AddCookie(&http.Cookie{Name: "JWT", Value: testJwtExpired, HttpOnly: true, Path: "/", MaxAge: expiration, Secure: false})
req.Header.Add("X-XSRF-TOKEN", "random id")
resp, err = client.Do(req)
require.NoError(t, err)
assert.Equal(t, 401, resp.StatusCode, "token expired")
}
func TestAuthJWTHeader(t *testing.T) {
a := Authenticator{DevPasswd: "123456", JWTService: NewJWT("xyz 12345", false, time.Hour)}
router := chi.NewRouter()
router.With(a.Auth(true)).Get("/auth", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(201)
})
server := httptest.NewServer(router)
defer server.Close()
jar, err := cookiejar.New(nil)
require.Nil(t, err)
client := &http.Client{Jar: jar, Timeout: 5 * time.Second}
req, err := http.NewRequest("GET", server.URL+"/auth", nil)
require.Nil(t, err)
req.Header.Add("X-JWT", testJwtValid)
resp, err := client.Do(req)
require.NoError(t, err)
assert.Equal(t, 201, resp.StatusCode, "valid auth user")
req, err = http.NewRequest("GET", server.URL+"/auth", nil)
require.Nil(t, err)
req.Header.Add("X-JWT", testJwtExpired)
resp, err = client.Do(req)
require.NoError(t, err)
assert.Equal(t, 401, resp.StatusCode, "invalid auth token")
}
func TestAuthRequired(t *testing.T) {
a := Authenticator{DevPasswd: "123456"}
router := chi.NewRouter()
router.With(a.Auth(true)).Get("/auth", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(201)
})
server := httptest.NewServer(router)
defer server.Close()
client := &http.Client{Timeout: 1 * time.Second}
req, err := http.NewRequest("GET", server.URL+"/auth", nil)
require.NoError(t, err)
req = withBasicAuth(req, "dev", "123456")
resp, err := client.Do(req)
require.NoError(t, err)
assert.Equal(t, 201, resp.StatusCode, "valid auth user")
req, err = http.NewRequest("GET", server.URL+"/auth", nil)
require.NoError(t, err)
resp, err = client.Do(req)
require.NoError(t, err)
assert.Equal(t, 401, resp.StatusCode, "no auth user")
req, err = http.NewRequest("GET", server.URL+"/auth", nil)
require.NoError(t, err)
req = withBasicAuth(req, "dev", "xyz")
resp, err = client.Do(req)
require.NoError(t, err)
assert.Equal(t, 401, resp.StatusCode, "wrong auth creds")
}
func TestAuthNotRequired(t *testing.T) {
a := Authenticator{DevPasswd: "123456"}
router := chi.NewRouter()
router.With(a.Auth(false)).Get("/auth", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(201)
})
server := httptest.NewServer(router)
defer server.Close()
client := &http.Client{Timeout: 1 * time.Second}
req, err := http.NewRequest("GET", server.URL+"/auth", nil)
require.NoError(t, err)
req = withBasicAuth(req, "dev", "123456")
resp, err := client.Do(req)
require.NoError(t, err)
assert.Equal(t, 201, resp.StatusCode, "valid auth user")
req, err = http.NewRequest("GET", server.URL+"/auth", nil)
require.NoError(t, err)
resp, err = client.Do(req)
require.NoError(t, err)
assert.Equal(t, 201, resp.StatusCode, "no auth user")
req, err = http.NewRequest("GET", server.URL+"/auth", nil)
require.NoError(t, err)
req = withBasicAuth(req, "dev", "ZZZZ123456")
resp, err = client.Do(req)
require.NoError(t, err)
assert.Equal(t, 201, resp.StatusCode, "wrong auth creds")
}
func TestAdminRequired(t *testing.T) {
a := Authenticator{DevPasswd: "123456"}
router := chi.NewRouter()
router.With(a.Auth(true), a.AdminOnly).Get("/auth", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(201)
})
server := httptest.NewServer(router)
defer server.Close()
client := &http.Client{Timeout: 1 * time.Second}
req, err := http.NewRequest("GET", server.URL+"/auth", nil)
require.NoError(t, err)
req = withBasicAuth(req, "dev", "123456")
resp, err := client.Do(req)
require.NoError(t, err)
assert.Equal(t, 201, resp.StatusCode, "valid auth user, admin")
devUser.Admin = false
req, err = http.NewRequest("GET", server.URL+"/auth", nil)
require.NoError(t, err)
req = withBasicAuth(req, "dev", "123456")
resp, err = client.Do(req)
require.NoError(t, err)
assert.Equal(t, 403, resp.StatusCode, "valid auth user, not admin")
}
func withBasicAuth(r *http.Request, username, password string) *http.Request {
auth := username + ":" + password
r.Header.Add("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth)))
return r
}
-166
View File
@@ -1,166 +0,0 @@
package auth
import (
"net/http"
"time"
jwt "github.com/dgrijalva/jwt-go"
"github.com/pkg/errors"
"github.com/umputun/remark/app/store"
)
// JWT wraps jwt operations
// supports both header and cookie jwt
type JWT struct {
secret string
secureCookies bool
exp time.Duration
}
// CustomClaims stores user info for auth and state & from from login
type CustomClaims struct {
jwt.StandardClaims
User *store.User `json:"user,omitempty"`
// state and from used for oauth handshake
State string `json:"state,omitempty"`
From string `json:"from,omitempty"`
SiteID string `json:"site_id,omitempty"`
SessionOnly bool `json:"sess_only,omitempty"`
}
const jwtCookieName = "JWT"
const jwtHeaderKey = "X-JWT"
const xsrfCookieName = "XSRF-TOKEN"
const xsrfHeaderKey = "X-XSRF-TOKEN"
// NewJWT makes JWT service
func NewJWT(secret string, secureCookies bool, exp time.Duration) *JWT {
res := JWT{
secret: secret,
secureCookies: secureCookies,
exp: exp,
}
return &res
}
// Token makes jwt with claims
func (j *JWT) Token(claims *CustomClaims) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString([]byte(j.secret))
if err != nil {
return "", errors.Wrap(err, "can't sign jwt token")
}
return tokenString, nil
}
// Parse token string and verify
func (j *JWT) Parse(tokenString string) (*CustomClaims, error) {
token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(j.secret), nil
})
if err != nil {
return nil, errors.Wrap(err, "can't parse jwt")
}
claims, ok := token.Claims.(*CustomClaims)
if !ok || !token.Valid {
return nil, errors.New("invalid jwt")
}
return claims, nil
}
// Set creates jwt cookie with xsrf cookie and put it to ResponseWriter
// accepts claims and sets expiration if none defined. permanent flag means long-living cookie, false makes it session only.
func (j *JWT) Set(w http.ResponseWriter, claims *CustomClaims, sessionOnly bool) error {
if claims.ExpiresAt == 0 {
claims.ExpiresAt = time.Now().Add(j.exp).Unix()
}
tokenString, err := j.Token(claims)
if err != nil {
return errors.Wrap(err, "failed to make jwt token")
}
cookieExpiration := 0 // session cookie
if !sessionOnly {
cookieExpiration = 365 * 24 * 3600 // 1 year
}
jwtCookie := http.Cookie{Name: jwtCookieName, Value: tokenString, HttpOnly: true, Path: "/",
MaxAge: cookieExpiration, Secure: j.secureCookies}
http.SetCookie(w, &jwtCookie)
xsrfCookie := http.Cookie{Name: xsrfCookieName, Value: claims.Id, HttpOnly: false, Path: "/",
MaxAge: cookieExpiration, Secure: j.secureCookies}
http.SetCookie(w, &xsrfCookie)
return nil
}
// Get jwt from header or cookie
// if cookie used, verify xsrf token to match
func (j *JWT) Get(r *http.Request) (*CustomClaims, error) {
fromCookie := false
tokenString := ""
// try to get from X-JWT header
if tokenHeader := r.Header.Get(jwtHeaderKey); tokenHeader != "" {
tokenString = tokenHeader
}
// try to get from JWT cookie
if tokenString == "" {
fromCookie = true
jc, err := r.Cookie(jwtCookieName)
if err != nil {
return nil, errors.Wrap(err, "jwt cookie was not presented")
}
tokenString = jc.Value
}
claims, err := j.Parse(tokenString)
if err != nil {
return nil, errors.Wrap(err, "failed to get jwt")
}
if fromCookie && claims.User != nil {
xsrf := r.Header.Get(xsrfHeaderKey)
if claims.Id != xsrf {
return nil, errors.New("xsrf mismatch")
}
}
return claims, nil
}
// Refresh gets jwt from request, checks if it will be expiring soon (1/2 of expiration) and create the new onw
func (j *JWT) Refresh(w http.ResponseWriter, r *http.Request) (*CustomClaims, error) {
claims, err := j.Get(r)
if err != nil {
return nil, err
}
untilExp := claims.ExpiresAt - time.Now().Unix()
if untilExp <= int64(j.exp.Seconds()/2) {
claims.ExpiresAt = time.Now().Add(j.exp).Unix()
e := j.Set(w, claims, claims.SessionOnly)
return claims, e
}
return claims, nil
}
// Reset token's cookies
func (j *JWT) Reset(w http.ResponseWriter) {
jwtCookie := http.Cookie{Name: jwtCookieName, Value: "", HttpOnly: false, Path: "/",
MaxAge: -1, Expires: time.Unix(0, 0), Secure: j.secureCookies}
http.SetCookie(w, &jwtCookie)
xsrfCookie := http.Cookie{Name: xsrfCookieName, Value: "", HttpOnly: false, Path: "/",
MaxAge: -1, Expires: time.Unix(0, 0), Secure: j.secureCookies}
http.SetCookie(w, &xsrfCookie)
}
-287
View File
@@ -1,287 +0,0 @@
package auth
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/umputun/remark/app/store"
)
var testJwtValid = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjI3ODkxOTE4MjIsImp0aSI6InJhbmRvbSBpZCI" +
"sImlzcyI6InJlbWFyazQyIiwibmJmIjoxNTI2ODg0MjIyLCJ1c2VyIjp7Im5hbWUiOiJuYW1lMSIsImlkIjoiaWQxIiwicGljdHVyZS" +
"I6IiIsImFkbWluIjpmYWxzZX0sInN0YXRlIjoiMTIzNDU2IiwiZnJvbSI6ImZyb20ifQ._loFgh3g45gr9TtGqvM3N584I_6EHEOJnYb6Py84stQ"
var testJwtValidSess = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjI3ODkxOTE4MjIsImp0aSI6InJhbmRvbSBpZCIsImlzcyI6In" +
"JlbWFyazQyIiwibmJmIjoxNTI2ODg0MjIyLCJ1c2VyIjp7Im5hbWUiOiJuYW1lMSIsImlkIjoiaWQxIiwicGljdHVyZSI6IiIsIm" +
"FkbWluIjpmYWxzZX0sInN0YXRlIjoiMTIzNDU2IiwiZnJvbSI6ImZyb20iLCJzZXNzX29ubHkiOnRydWV9.p6w0sM_NYaRuyhyA9jqfWlB5cx1vZPGhXGC5geSX7nA"
var testJwtExpired = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MjY4ODc4MjIsImp0aSI6InJhbmRvbSBpZCIs" +
"ImlzcyI6InJlbWFyazQyIiwibmJmIjoxNTI2ODg0MjIyLCJ1c2VyIjp7Im5hbWUiOiJuYW1lMSIsImlkIjoiaWQxIiwicGljdHVyZSI6IiI" +
"sImFkbWluIjpmYWxzZX0sInN0YXRlIjoiMTIzNDU2IiwiZnJvbSI6ImZyb20ifQ.4_dCrY9ihyfZIedz-kZwBTxmxU1a52V7IqeJrOqTzE4"
func TestJWT_Token(t *testing.T) {
j := NewJWT("xyz 12345", false, time.Hour)
claims := &CustomClaims{
State: "123456",
From: "from",
User: &store.User{
ID: "id1",
Name: "name1",
},
StandardClaims: jwt.StandardClaims{
Id: "random id",
Issuer: "remark42",
ExpiresAt: time.Date(2058, 5, 21, 1, 30, 22, 0, time.Local).Unix(),
NotBefore: time.Date(2018, 5, 21, 1, 30, 22, 0, time.Local).Unix(),
},
}
res, err := j.Token(claims)
assert.Nil(t, err)
assert.Equal(t, testJwtValid, res)
}
func TestJWT_Parse(t *testing.T) {
j := NewJWT("xyz 12345", false, time.Hour)
claims, err := j.Parse(testJwtValid)
assert.NoError(t, err)
assert.Equal(t, &store.User{Name: "name1", ID: "id1"}, claims.User)
_, err = j.Parse(testJwtExpired)
assert.NotNil(t, err, "expired token")
_, err = j.Parse("bad")
assert.NotNil(t, err, "bad token")
}
func TestJWT_Set(t *testing.T) {
j := NewJWT("xyz 12345", false, time.Hour)
claims := &CustomClaims{
State: "123456",
From: "from",
User: &store.User{
ID: "id1",
Name: "name1",
},
StandardClaims: jwt.StandardClaims{
Id: "random id",
Issuer: "remark42",
ExpiresAt: time.Date(2058, 5, 21, 1, 30, 22, 0, time.Local).Unix(),
NotBefore: time.Date(2018, 5, 21, 1, 30, 22, 0, time.Local).Unix(),
},
}
claims.SessionOnly = false
rr := httptest.NewRecorder()
err := j.Set(rr, claims, claims.SessionOnly)
assert.Nil(t, err)
cookies := rr.Result().Cookies()
t.Log(cookies)
require.Equal(t, 2, len(cookies))
assert.Equal(t, "JWT", cookies[0].Name)
assert.Equal(t, testJwtValid, cookies[0].Value)
assert.Equal(t, 31536000, cookies[0].MaxAge)
assert.Equal(t, "XSRF-TOKEN", cookies[1].Name)
assert.Equal(t, "random id", cookies[1].Value)
claims.SessionOnly = true
rr = httptest.NewRecorder()
err = j.Set(rr, claims, claims.SessionOnly)
assert.Nil(t, err)
cookies = rr.Result().Cookies()
t.Log(cookies)
require.Equal(t, 2, len(cookies))
assert.Equal(t, "JWT", cookies[0].Name)
assert.Equal(t, testJwtValidSess, cookies[0].Value)
assert.Equal(t, 0, cookies[0].MaxAge)
assert.Equal(t, "XSRF-TOKEN", cookies[1].Name)
assert.Equal(t, "random id", cookies[1].Value)
}
func TestJWT_GetFromHeader(t *testing.T) {
j := NewJWT("xyz 12345", false, time.Hour)
req := httptest.NewRequest("GET", "/", nil)
req.Header.Add(jwtHeaderKey, testJwtValid)
claims, err := j.Get(req)
assert.Nil(t, err)
assert.Equal(t, &store.User{Name: "name1", ID: "id1", Picture: "", Admin: false, Blocked: false, IP: ""}, claims.User)
assert.Equal(t, "remark42", claims.Issuer)
req = httptest.NewRequest("GET", "/", nil)
req.Header.Add(jwtHeaderKey, testJwtExpired)
_, err = j.Get(req)
assert.NotNil(t, err)
assert.True(t, strings.Contains(err.Error(), "can't parse jwt: token is expired by"), err.Error())
req = httptest.NewRequest("GET", "/", nil)
req.Header.Add(jwtHeaderKey, "bad bad token")
_, err = j.Get(req)
assert.NotNil(t, err)
assert.True(t, strings.Contains(err.Error(), "can't parse jwt: token contains an invalid number of segments"), err.Error())
}
func TestJWT_SetAndGetWithCookies(t *testing.T) {
j := NewJWT("xyz 12345", false, time.Hour)
claims := &CustomClaims{
State: "123456",
From: "from",
SessionOnly: true,
User: &store.User{
ID: "id1",
Name: "name1",
},
StandardClaims: jwt.StandardClaims{
Id: "random id",
Issuer: "remark42",
ExpiresAt: time.Date(2058, 5, 21, 1, 30, 22, 0, time.Local).Unix(),
NotBefore: time.Date(2018, 5, 21, 1, 30, 22, 0, time.Local).Unix(),
},
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/valid" {
assert.Nil(t, j.Set(w, claims, true))
w.WriteHeader(200)
}
}))
defer ts.Close()
resp, err := http.Get(ts.URL + "/valid")
require.Nil(t, err)
assert.Equal(t, 200, resp.StatusCode)
req := httptest.NewRequest("GET", "/valid", nil)
req.AddCookie(resp.Cookies()[0])
req.Header.Add(xsrfHeaderKey, "random id")
claims, err = j.Get(req)
assert.Nil(t, err)
assert.Equal(t, &store.User{Name: "name1", ID: "id1", Picture: "", Admin: false, Blocked: false, IP: ""}, claims.User)
assert.Equal(t, "remark42", claims.Issuer)
assert.Equal(t, true, claims.SessionOnly)
t.Log(resp.Cookies())
}
func TestJWT_SetAndGetWithXsrfMismatch(t *testing.T) {
j := NewJWT("xyz 12345", false, time.Hour)
claims := &CustomClaims{
State: "123456",
From: "from",
User: &store.User{
ID: "id1",
Name: "name1",
},
StandardClaims: jwt.StandardClaims{
Id: "random id",
Issuer: "remark42",
ExpiresAt: time.Date(2058, 5, 21, 1, 30, 22, 0, time.Local).Unix(),
NotBefore: time.Date(2018, 5, 21, 1, 30, 22, 0, time.Local).Unix(),
},
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/valid" {
assert.Nil(t, j.Set(w, claims, true))
w.WriteHeader(200)
}
}))
defer ts.Close()
resp, err := http.Get(ts.URL + "/valid")
require.Nil(t, err)
assert.Equal(t, 200, resp.StatusCode)
req := httptest.NewRequest("GET", "/valid", nil)
req.AddCookie(resp.Cookies()[0])
req.Header.Add(xsrfHeaderKey, "random id wrong")
claims, err = j.Get(req)
assert.EqualError(t, err, "xsrf mismatch")
}
func TestJWT_SetAndGetWithCookiesExpired(t *testing.T) {
j := NewJWT("xyz 12345", false, time.Hour)
claims := &CustomClaims{
State: "123456",
From: "from",
User: &store.User{
ID: "id1",
Name: "name1",
},
StandardClaims: jwt.StandardClaims{
Id: "random id",
Issuer: "remark42",
ExpiresAt: time.Date(2018, 5, 21, 1, 35, 22, 0, time.Local).Unix(),
NotBefore: time.Date(2018, 5, 21, 1, 30, 22, 0, time.Local).Unix(),
},
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/expired" {
assert.Nil(t, j.Set(w, claims, true))
w.WriteHeader(200)
}
}))
defer ts.Close()
resp, err := http.Get(ts.URL + "/expired")
require.Nil(t, err)
assert.Equal(t, 200, resp.StatusCode)
req := httptest.NewRequest("GET", "/expired", nil)
req.AddCookie(resp.Cookies()[0])
req.Header.Add(xsrfHeaderKey, "random id")
_, err = j.Get(req)
assert.NotNil(t, err)
assert.True(t, strings.Contains(err.Error(), "can't parse jwt: token is expired by"), err.Error())
}
func TestJWT_Refresh(t *testing.T) {
j := NewJWT("xyz 12345", false, 2*time.Second)
claims := &CustomClaims{
State: "123456",
From: "from",
User: &store.User{
ID: "id1",
Name: "name1",
},
StandardClaims: jwt.StandardClaims{
Id: "random id",
Issuer: "remark42",
},
}
// set token
rr := httptest.NewRecorder()
err := j.Set(rr, claims, true)
assert.Nil(t, err)
cookies := rr.Result().Cookies()
require.Equal(t, 2, len(cookies))
req, err := http.NewRequest("GET", "http://example.com/blah", nil)
require.Nil(t, err)
req.AddCookie(cookies[0])
req.Header.Add(xsrfHeaderKey, "random id")
claims2, err := j.Refresh(rr, req)
require.Nil(t, err)
assert.Equal(t, claims.ExpiresAt, claims2.ExpiresAt, "no refresh yet")
time.Sleep(1 * time.Second)
claims2, err = j.Refresh(rr, req)
assert.Nil(t, err)
assert.True(t, claims.ExpiresAt < claims2.ExpiresAt, "refreshed")
t.Log(claims.ExpiresAt, claims2.ExpiresAt)
}
-224
View File
@@ -1,224 +0,0 @@
package auth
import (
"context"
"crypto/rand"
"crypto/sha1"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/go-chi/chi"
"github.com/go-chi/render"
"golang.org/x/oauth2"
"github.com/umputun/remark/app/rest"
"github.com/umputun/remark/app/rest/proxy"
"github.com/umputun/remark/app/store"
)
// Provider represents oauth2 provider
type Provider struct {
Params
Name string
RedirectURL string
InfoURL string
Endpoint oauth2.Endpoint
Scopes []string
MapUser func(userData, []byte) store.User // map info from InfoURL to User
conf oauth2.Config
}
// Params to make initialized and ready to use provider
type Params struct {
RemarkURL string
AvatarProxy *proxy.Avatar
JwtService *JWT
IsVerifiedFn func(siteID string, userID string) bool
SecretKey string
Admins []string
Cid string
Csecret string
}
type userData map[string]interface{}
func (u userData) value(key string) string {
// json.Unmarshal converts json "null" value to go's "nil", in this case return empty string
if val, ok := u[key]; ok && val != nil {
return fmt.Sprintf("%v", val)
}
return ""
}
// newProvider makes auth for given provider
func initProvider(p Params, provider Provider) Provider {
log.Printf("[INFO] init auth provider %s", provider.Name)
provider.Params = p
provider.conf = oauth2.Config{
ClientID: provider.Cid,
ClientSecret: provider.Csecret,
RedirectURL: provider.RedirectURL,
Scopes: provider.Scopes,
Endpoint: provider.Endpoint,
}
log.Printf("[DEBUG] created %s auth, id=%s, redir=%s, endpoint=%s",
provider.Name, provider.Cid, provider.Endpoint, provider.RedirectURL)
return provider
}
// Routes returns auth routes for given provider
func (p Provider) Routes() chi.Router {
router := chi.NewRouter()
router.Get("/login", p.loginHandler)
router.Get("/callback", p.authHandler)
router.Get("/logout", p.LogoutHandler)
return router
}
// loginHandler - GET /login?from=redirect-back-url&site=siteID&session=1
func (p Provider) loginHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("[DEBUG] login with %s", p.Name)
// make state (random) and store in session
state := p.randToken()
claims := CustomClaims{
State: state,
From: r.URL.Query().Get("from"),
SiteID: r.URL.Query().Get("site"),
SessionOnly: r.URL.Query().Get("session") != "" && r.URL.Query().Get("session") != "0",
StandardClaims: jwt.StandardClaims{
Id: p.randToken(),
Issuer: "remark42",
ExpiresAt: time.Now().Add(30 * time.Minute).Unix(),
NotBefore: time.Now().Add(-1 * time.Minute).Unix(),
},
}
if err := p.JwtService.Set(w, &claims, false); err != nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to set jwt")
return
}
// return login url
loginURL := p.conf.AuthCodeURL(state)
log.Printf("[DEBUG] login url %s", loginURL)
http.Redirect(w, r, loginURL, http.StatusFound)
}
// authHandler fills user info and redirects to "from" url. This is callback url redirected locally by browser
// GET /callback
func (p Provider) authHandler(w http.ResponseWriter, r *http.Request) {
oauthClaims, err := p.JwtService.Get(r)
if err != nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to get jwt")
return
}
retrievedState := oauthClaims.State
if retrievedState == "" || retrievedState != r.URL.Query().Get("state") {
http.Error(w, fmt.Sprintf("unexpected state %v", retrievedState), http.StatusUnauthorized)
return
}
log.Printf("[DEBUG] auth with state %s", retrievedState)
tok, err := p.conf.Exchange(context.Background(), r.URL.Query().Get("code"))
if err != nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "exchange failed")
return
}
client := p.conf.Client(context.Background(), tok)
uinfo, err := client.Get(p.InfoURL)
if err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, fmt.Sprintf("failed to get client info via %s", p.InfoURL))
return
}
defer func() {
if e := uinfo.Body.Close(); e != nil {
log.Printf("[WARN] failed to close response body, %s", e)
}
}()
data, err := ioutil.ReadAll(uinfo.Body)
if err != nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to read user info")
return
}
jData := map[string]interface{}{}
if e := json.Unmarshal(data, &jData); e != nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to unmarshal user info")
return
}
log.Printf("[DEBUG] got raw user info %+v", jData)
u := p.MapUser(jData, data)
u = p.alterUser(u, oauthClaims)
authClaims := &CustomClaims{
User: &u,
StandardClaims: jwt.StandardClaims{
Issuer: "remark42",
Id: p.randToken(),
},
SessionOnly: oauthClaims.SessionOnly,
}
if err = p.JwtService.Set(w, authClaims, oauthClaims.SessionOnly); err != nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to save user info")
return
}
log.Printf("[DEBUG] user info %+v", u)
// redirect to back url if presented in login query params
if oauthClaims.From != "" {
http.Redirect(w, r, oauthClaims.From, http.StatusTemporaryRedirect)
return
}
render.JSON(w, r, &u)
}
// alterUser sets fields not handled by provider's MapUser, things like avatar, admin, verified
func (p Provider) alterUser(u store.User, oauthClaims *CustomClaims) store.User {
if p.AvatarProxy != nil {
if avatarURL, e := p.AvatarProxy.Put(u); e == nil {
u.Picture = avatarURL
} else {
log.Printf("[WARN] failed to proxy avatar, %s", e)
}
}
u.Admin = isAdmin(u.ID, p.Admins)
if p.IsVerifiedFn != nil {
u.Verified = p.IsVerifiedFn(oauthClaims.SiteID, u.ID)
}
return u
}
// LogoutHandler - GET /logout
func (p Provider) LogoutHandler(w http.ResponseWriter, r *http.Request) {
p.JwtService.Reset(w)
log.Printf("[DEBUG] logout")
}
func (p Provider) randToken() string {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
log.Fatalf("[ERROR] can't get randoms, %s", err)
}
s := sha1.New()
if _, err := s.Write(b); err != nil {
log.Printf("[WARN] can't write randoms, %s", err)
}
return fmt.Sprintf("%x", s.Sum(nil))
}
-218
View File
@@ -1,218 +0,0 @@
package auth
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/http/cookiejar"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
"github.com/umputun/remark/app/store"
)
func TestLogin(t *testing.T) {
ts, ots := mockProvider(t, 8981, 8982)
defer func() {
ts.Close()
ots.Close()
}()
jar, err := cookiejar.New(nil)
require.Nil(t, err)
client := &http.Client{Jar: jar, Timeout: 5 * time.Second}
// check non-admin, permanent
resp, err := client.Get("http://localhost:8981/login?site=remark")
require.Nil(t, err)
assert.Equal(t, 200, resp.StatusCode)
body, err := ioutil.ReadAll(resp.Body)
assert.Nil(t, err)
t.Logf("resp %s", string(body))
t.Logf("headers: %+v", resp.Header)
assert.Equal(t, 2, len(resp.Cookies()))
assert.Equal(t, "JWT", resp.Cookies()[0].Name)
assert.NotEqual(t, "", resp.Cookies()[0].Value, "jwt set")
assert.Equal(t, 31536000, resp.Cookies()[0].MaxAge)
assert.Equal(t, "XSRF-TOKEN", resp.Cookies()[1].Name)
assert.NotEqual(t, "", resp.Cookies()[1].Value, "xsrf cookie set")
u := store.User{}
err = json.Unmarshal(body, &u)
assert.Nil(t, err)
assert.Equal(t, store.User{Name: "blah", ID: "mock_myuser1", Picture: "http://exmple.com/pic1.png",
Admin: false, Blocked: false, IP: ""}, u)
// check admin user
resp, err = client.Get("http://localhost:8981/login?site=remark")
assert.Nil(t, err)
assert.Equal(t, 200, resp.StatusCode)
body, err = ioutil.ReadAll(resp.Body)
assert.Nil(t, err)
err = json.Unmarshal(body, &u)
assert.Nil(t, err)
assert.Equal(t, store.User{Name: "blah", ID: "mock_myuser2", Picture: "http://exmple.com/pic1.png",
Admin: true, Blocked: false, IP: "", Verified: true}, u)
}
func TestLoginSessionOnly(t *testing.T) {
ts, ots := mockProvider(t, 8981, 8982)
defer func() {
ts.Close()
ots.Close()
}()
jar, err := cookiejar.New(nil)
require.Nil(t, err)
client := &http.Client{Jar: jar, Timeout: 5 * time.Second}
// check non-admin, session
resp, err := client.Get("http://localhost:8981/login?site=remark&session=1")
require.Nil(t, err)
assert.Equal(t, 200, resp.StatusCode)
assert.Equal(t, 2, len(resp.Cookies()))
assert.Equal(t, "JWT", resp.Cookies()[0].Name)
assert.NotEqual(t, "", resp.Cookies()[0].Value, "jwt set")
assert.Equal(t, 0, resp.Cookies()[0].MaxAge)
assert.Equal(t, "XSRF-TOKEN", resp.Cookies()[1].Name)
assert.NotEqual(t, "", resp.Cookies()[1].Value, "xsrf cookie set")
req, err := http.NewRequest("GET", "http://example.com", nil)
require.Nil(t, err)
req.AddCookie(resp.Cookies()[0])
req.AddCookie(resp.Cookies()[1])
req.Header.Add("X-XSRF-TOKEN", resp.Cookies()[1].Value)
jwtService := NewJWT("12345", false, time.Hour)
res, err := jwtService.Get(req)
require.Nil(t, err)
assert.Equal(t, true, res.SessionOnly)
t.Logf("%+v", res)
}
func TestLogout(t *testing.T) {
ts, ots := mockProvider(t, 8691, 8692)
defer func() {
ts.Close()
ots.Close()
}()
jar, err := cookiejar.New(nil)
require.Nil(t, err)
client := &http.Client{Jar: jar, Timeout: 5 * time.Second}
resp, err := client.Get("http://localhost:8691/login")
require.Nil(t, err)
assert.Equal(t, 200, resp.StatusCode)
assert.Equal(t, 2, len(resp.Cookies()))
resp, err = client.Get("http://localhost:8691/logout")
require.Nil(t, err)
assert.Equal(t, 200, resp.StatusCode)
assert.Equal(t, 2, len(resp.Cookies()))
assert.Equal(t, "JWT", resp.Cookies()[0].Name, "jwt cookie cleared")
assert.Equal(t, "", resp.Cookies()[0].Value)
assert.Equal(t, "XSRF-TOKEN", resp.Cookies()[1].Name, "xsrf cookie cleared")
assert.Equal(t, "", resp.Cookies()[1].Value)
}
func TestInitProvider(t *testing.T) {
params := Params{RemarkURL: "url", SecretKey: "123456", Cid: "cid", Csecret: "csecret"}
provider := Provider{Name: "test", RedirectURL: "redir"}
res := initProvider(params, provider)
assert.Equal(t, "cid", res.conf.ClientID)
assert.Equal(t, "csecret", res.conf.ClientSecret)
assert.Equal(t, "redir", res.RedirectURL)
assert.Equal(t, "123456", res.SecretKey)
assert.Equal(t, "test", res.Name)
}
func mockProvider(t *testing.T, loginPort, authPort int) (*http.Server, *http.Server) {
provider := Provider{
Name: "mock",
Endpoint: oauth2.Endpoint{
AuthURL: fmt.Sprintf("http://localhost:%d/login/oauth/authorize", authPort),
TokenURL: fmt.Sprintf("http://localhost:%d/login/oauth/access_token", authPort),
},
RedirectURL: fmt.Sprintf("http://localhost:%d/callback", loginPort),
Scopes: []string{"user:email"},
InfoURL: fmt.Sprintf("http://localhost:%d/user", authPort),
MapUser: func(data userData, _ []byte) store.User {
userInfo := store.User{
ID: "mock_" + data.value("id"),
Name: data.value("name"),
Picture: data.value("picture"),
}
return userInfo
},
}
params := Params{RemarkURL: "url", SecretKey: "123456", Cid: "cid", Csecret: "csecret",
JwtService: NewJWT("12345", false, time.Hour), Admins: []string{"mock_myuser2"},
// AvatarProxy: &proxy.Avatar{Store: &mockAvatarStore, RoutePath: "/v1/avatar"},
IsVerifiedFn: func(siteID, userID string) bool { return userID == "mock_myuser2" }}
provider = initProvider(params, provider)
ts := &http.Server{Addr: fmt.Sprintf(":%d", loginPort), Handler: provider.Routes()}
count := 0
useIds := []string{"myuser1", "myuser2"}
oauth := &http.Server{
Addr: fmt.Sprintf(":%d", authPort),
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("[MOCK OAUTH] request %s %s %+v", r.Method, r.URL, r.Header)
switch {
case strings.HasPrefix(r.URL.Path, "/login/oauth/authorize"):
state := r.URL.Query().Get("state")
w.Header().Add("Location", fmt.Sprintf("http://localhost:%d/callback?code=g0ZGZmNjVmOWI&state=%s",
loginPort, state))
w.WriteHeader(302)
case strings.HasPrefix(r.URL.Path, "/login/oauth/access_token"):
res := `{
"access_token":"MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3",
"token_type":"bearer",
"expires_in":3600,
"refresh_token":"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk",
"scope":"create",
"state":"12345678"
}`
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
_, err := w.Write([]byte(res))
assert.NoError(t, err)
case strings.HasPrefix(r.URL.Path, "/user"):
res := fmt.Sprintf(`{
"id": "%s",
"name":"blah",
"picture":"http://exmple.com/pic1.png"
}`, useIds[count])
count++
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
_, err := w.Write([]byte(res))
assert.NoError(t, err)
default:
t.Fatalf("unexpected oauth request %s %s", r.Method, r.URL)
}
}),
}
go func() { _ = oauth.ListenAndServe() }()
go func() { _ = ts.ListenAndServe() }()
time.Sleep(time.Millisecond * 100) // let them start
return ts, oauth
}
-126
View File
@@ -1,126 +0,0 @@
package auth
import (
"encoding/json"
"fmt"
"golang.org/x/oauth2/facebook"
"golang.org/x/oauth2/github"
"golang.org/x/oauth2/google"
"golang.org/x/oauth2/yandex"
"github.com/umputun/remark/app/store"
)
// NewGoogle makes google oauth2 provider
func NewGoogle(p Params) Provider {
return initProvider(p, Provider{
Name: "google",
Endpoint: google.Endpoint,
RedirectURL: p.RemarkURL + "/auth/google/callback",
Scopes: []string{"https://www.googleapis.com/auth/userinfo.profile"},
InfoURL: "https://www.googleapis.com/oauth2/v3/userinfo",
MapUser: func(data userData, _ []byte) store.User {
userInfo := store.User{
// encode email with provider name to avoid collision if same id returned by other provider
ID: "google_" + store.EncodeID(data.value("sub")),
Name: data.value("name"),
Picture: data.value("picture"),
}
if userInfo.Name == "" {
userInfo.Name = "noname_" + userInfo.ID[8:12]
}
return userInfo
},
})
}
// NewGithub makes github oauth2 provider
func NewGithub(p Params) Provider {
return initProvider(p, Provider{
Name: "github",
Endpoint: github.Endpoint,
RedirectURL: p.RemarkURL + "/auth/github/callback",
Scopes: []string{},
InfoURL: "https://api.github.com/user",
MapUser: func(data userData, _ []byte) store.User {
userInfo := store.User{
ID: "github_" + store.EncodeID(data.value("login")),
Name: data.value("name"),
Picture: data.value("avatar_url"),
}
// github may have no user name, use login in this case
if userInfo.Name == "" {
userInfo.Name = data.value("login")
}
return userInfo
},
})
}
// NewFacebook makes facebook oauth2 provider
func NewFacebook(p Params) Provider {
// response format for fb /me call
type uinfo struct {
ID string `json:"id"`
Name string `json:"name"`
Picture struct {
Data struct {
URL string `json:"url"`
} `json:"data"`
} `json:"picture"`
}
return initProvider(p, Provider{
Name: "facebook",
Endpoint: facebook.Endpoint,
RedirectURL: p.RemarkURL + "/auth/facebook/callback",
Scopes: []string{"public_profile"},
InfoURL: "https://graph.facebook.com/me?fields=id,name,picture",
MapUser: func(data userData, bdata []byte) store.User {
userInfo := store.User{
ID: "facebook_" + store.EncodeID(data.value("id")),
Name: data.value("name"),
}
if userInfo.Name == "" {
userInfo.Name = userInfo.ID[0:16]
}
uinfoJSON := uinfo{}
if err := json.Unmarshal(bdata, &uinfoJSON); err == nil {
userInfo.Picture = uinfoJSON.Picture.Data.URL
}
return userInfo
},
})
}
// NewYandex makes yandex oauth2 provider
func NewYandex(p Params) Provider {
return initProvider(p, Provider{
Name: "yandex",
Endpoint: yandex.Endpoint,
RedirectURL: p.RemarkURL + "/auth/yandex/callback",
Scopes: []string{},
// See https://tech.yandex.com/passport/doc/dg/reference/response-docpage/
InfoURL: "https://login.yandex.ru/info?format=json",
MapUser: func(data userData, _ []byte) store.User {
userInfo := store.User{
ID: "yandex_" + store.EncodeID(data.value("id")),
Name: data.value("display_name"), // using Display Name by default
}
if userInfo.Name == "" {
userInfo.Name = data.value("real_name") // using Real Name (== full name) if Display Name is empty
}
if userInfo.Name == "" {
userInfo.Name = data.value("login") // otherwise using login
}
if data.value("default_avatar_id") != "" {
userInfo.Picture = fmt.Sprintf("https://avatars.yandex.net/get-yapic/%s/islands-200", data.value("default_avatar_id"))
}
return userInfo
},
})
}
-83
View File
@@ -1,83 +0,0 @@
package auth
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/umputun/remark/app/store"
)
func TestProviders_NewGoogle(t *testing.T) {
r := NewGoogle(Params{RemarkURL: "http://demo.remark42.com", Cid: "cid", Csecret: "cs"})
assert.Equal(t, "google", r.Name)
udata := userData{"sub": "1234567890", "name": "test user", "picture": "http://demo.remark42.com/blah.png"}
user := r.MapUser(udata, nil)
assert.Equal(t, store.User{Name: "test user", ID: "google_01b307acba4f54f55aafc33bb06bbbf6ca803e9a",
Picture: "http://demo.remark42.com/blah.png", Admin: false, Blocked: false, IP: ""}, user, "got %+v", user)
// no name in data
udata = userData{"sub": "1234567890", "picture": "http://demo.remark42.com/blah.png"}
user = r.MapUser(udata, nil)
assert.Equal(t, store.User{Name: "noname_1b30", ID: "google_01b307acba4f54f55aafc33bb06bbbf6ca803e9a",
Picture: "http://demo.remark42.com/blah.png", Admin: false, Blocked: false, IP: ""}, user, "got %+v", user)
}
func TestProviders_NewGithub(t *testing.T) {
r := NewGithub(Params{RemarkURL: "http://demo.remark42.com", Cid: "cid", Csecret: "cs"})
assert.Equal(t, "github", r.Name)
udata := userData{"login": "lll", "name": "test user", "avatar_url": "http://demo.remark42.com/blah.png"}
user := r.MapUser(udata, nil)
assert.Equal(t, store.User{Name: "test user", ID: "github_e80b2d2608711cbb3312db7c4727a46fbad9601a",
Picture: "http://demo.remark42.com/blah.png", Admin: false, Blocked: false, IP: ""}, user, "got %+v", user)
// nil name in data (json response contains `"name": null`); using login, it's always required
udata = userData{"login": "lll", "name": nil, "avatar_url": "http://demo.remark42.com/blah.png"}
user = r.MapUser(udata, nil)
assert.Equal(t, store.User{Name: "lll", ID: "github_e80b2d2608711cbb3312db7c4727a46fbad9601a",
Picture: "http://demo.remark42.com/blah.png", Admin: false, Blocked: false, IP: ""}, user, "got %+v", user)
}
func TestProviders_NewFacebook(t *testing.T) {
r := NewFacebook(Params{RemarkURL: "http://demo.remark42.com", Cid: "cid", Csecret: "cs"})
assert.Equal(t, "facebook", r.Name)
udata := userData{"id": "myid", "name": "test user"}
user := r.MapUser(udata, []byte(`{"picture": {"data": {"url": "http://demo.remark42.com/blah.png"} }}`))
assert.Equal(t, store.User{Name: "test user", ID: "facebook_6e34471f84557e1713012d64a7477c71bfdac631",
Picture: "http://demo.remark42.com/blah.png", Admin: false, Blocked: false, IP: ""}, user, "got %+v", user)
udata = userData{"id": "myid", "name": ""}
user = r.MapUser(udata, []byte(`{"picture": {"data": {"url": "http://demo.remark42.com/blah.png"} }}`))
assert.Equal(t, store.User{Name: "facebook_6e34471", ID: "facebook_6e34471f84557e1713012d64a7477c71bfdac631",
Picture: "http://demo.remark42.com/blah.png", Admin: false, Blocked: false, IP: ""}, user, "got %+v", user)
}
func TestProviders_NewYandex(t *testing.T) {
r := NewYandex(Params{RemarkURL: "http://demo.remark42.com", Cid: "cid", Csecret: "cs"})
assert.Equal(t, "yandex", r.Name)
udata := userData{"id": "1234567890", "display_name": "Vasya P", "default_avatar_id": "131652443"}
user := r.MapUser(udata, nil)
assert.Equal(t, store.User{Name: "Vasya P", ID: "yandex_01b307acba4f54f55aafc33bb06bbbf6ca803e9a",
Picture: "https://avatars.yandex.net/get-yapic/131652443/islands-200", Admin: false, Blocked: false, IP: ""}, user, "got %+v", user)
// "display_name": null, "default_avatar_id": null
udata = userData{"id": "1234567890", "login": "vasya", "display_name": nil, "real_name": "Vasya Pupkin", "default_avatar_id": nil}
user = r.MapUser(udata, nil)
assert.Equal(t, store.User{Name: "Vasya Pupkin", ID: "yandex_01b307acba4f54f55aafc33bb06bbbf6ca803e9a",
Picture: "", Admin: false, Blocked: false, IP: ""}, user, "got %+v", user)
// empty "display_name", empty "default_avatar_id", empty "real_name"
udata = userData{"id": "1234567890", "login": "vasya", "display_name": "", "real_name": "", "default_avatar_id": ""}
user = r.MapUser(udata, nil)
assert.Equal(t, store.User{Name: "vasya", ID: "yandex_01b307acba4f54f55aafc33bb06bbbf6ca803e9a",
Picture: "", Admin: false, Blocked: false, IP: ""}, user, "got %+v", user)
// "real_name": null
udata = userData{"id": "1234567890", "login": "vasya", "real_name": nil, "default_avatar_id": ""}
user = r.MapUser(udata, nil)
assert.Equal(t, store.User{Name: "vasya", ID: "yandex_01b307acba4f54f55aafc33bb06bbbf6ca803e9a",
Picture: "", Admin: false, Blocked: false, IP: ""}, user, "got %+v", user)
}
-45
View File
@@ -1,45 +0,0 @@
package cache
import (
"net/http"
"strings"
"github.com/pkg/errors"
"github.com/umputun/remark/app/rest"
)
// LoadingCache defines interface for caching
type LoadingCache interface {
Get(key string, fn func() ([]byte, error)) (data []byte, err error)
Flush(scopes ...string)
}
// Key makes full key from primary key and scopes
func Key(key string, scopes ...string) string {
return strings.Join(scopes, "$$") + "@@" + key
}
// ParseKey gets compound key created by Key func and split it to the actual key and scopes
func ParseKey(fullKey string) (key string, scopes []string, err error) {
elems := strings.Split(fullKey, "@@")
if len(elems) != 2 {
return "", nil, errors.Errorf("can't parse cache key %s", key)
}
scopes = strings.Split(elems[0], "$$")
if len(scopes) == 1 && scopes[0] == "" {
scopes = []string{}
}
key = elems[1]
return key, scopes, nil
}
// URLKey gets url from request to use it as cache key
// admins will have different keys in order to prevent leak of admin-only data to regular users
func URLKey(r *http.Request) string {
adminPrefix := "admin!!"
key := strings.TrimPrefix(r.URL.String(), adminPrefix) // prevents attach with fake url to get admin view
if user, err := rest.GetUserInfo(r); err == nil && user.Admin { // make separate cache key for admins
key = adminPrefix + key
}
return key
}
-54
View File
@@ -1,54 +0,0 @@
package cache
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/umputun/remark/app/rest"
"github.com/umputun/remark/app/store"
)
func TestCache_Keys(t *testing.T) {
tbl := []struct {
key string
scopes []string
full string
}{
{"key1", []string{"s1"}, "s1@@key1"},
{"key2", []string{"s11", "s2"}, "s11$$s2@@key2"},
{"key3", []string{}, "@@key3"},
}
for n, tt := range tbl {
full := Key(tt.key, tt.scopes...)
assert.Equal(t, tt.full, full, "making key, #%d", n)
k, s, e := ParseKey(full)
assert.Nil(t, e)
assert.Equal(t, tt.scopes, s)
assert.Equal(t, tt.key, k)
}
_, _, err := ParseKey("abc")
assert.Error(t, err)
_, _, err = ParseKey("")
assert.Error(t, err)
}
func TestCache_URLKey(t *testing.T) {
r, err := http.NewRequest("GET", "http://blah/123", nil)
assert.Nil(t, err)
key := URLKey(r)
assert.Equal(t, "http://blah/123", key)
r, err = http.NewRequest("GET", "http://blah/123?key=v&k2=v2", nil)
assert.Nil(t, err)
key = URLKey(r)
assert.Equal(t, "http://blah/123?key=v&k2=v2", key)
user := store.User{Admin: true}
r = rest.SetUserInfo(r, user)
key = URLKey(r)
assert.Equal(t, "admin!!http://blah/123?key=v&k2=v2", key)
}
-114
View File
@@ -1,114 +0,0 @@
package cache
import (
"log"
"sync/atomic"
"github.com/hashicorp/golang-lru"
"github.com/pkg/errors"
)
// memoryCache implements LoadingCache interface on top of cache.Cache (go-cache)
type memoryCache struct {
bytesCache *lru.Cache
postFlushFn func()
maxKeys int
maxValueSize int
maxCacheSize int64
currentSize int64
}
// NewMemoryCache makes memoryCache implementation
func NewMemoryCache(options ...Option) (LoadingCache, error) {
res := memoryCache{
postFlushFn: func() {},
maxKeys: 1000,
maxValueSize: 0,
}
for _, opt := range options {
if err := opt(&res); err != nil {
return nil, errors.Wrap(err, "failed to set cache option")
}
}
onEvicted := func(key interface{}, value interface{}) {
size := len(value.([]byte))
atomic.AddInt64(&res.currentSize, -1*int64(size))
}
var err error
// OnEvicted called automatically for expired and manually deleted
if res.bytesCache, err = lru.NewWithEvict(res.maxKeys, onEvicted); err != nil {
return nil, errors.Wrap(err, "failed to make cache")
}
log.Printf("[DEBUG] create lru cache, maxKeys=%d, maxValueSize=%d, maxCacheSize=%d",
res.maxKeys, res.maxValueSize, res.maxCacheSize)
return &res, nil
}
// Get is loading cache method to get value by key or load via fn if not found
func (m *memoryCache) Get(key string, fn func() ([]byte, error)) (data []byte, err error) {
if b, ok := m.bytesCache.Get(key); ok {
return b.([]byte), nil
}
if data, err = fn(); err != nil {
return data, err
}
if m.allowed(data) {
m.bytesCache.Add(key, data)
atomic.AddInt64(&m.currentSize, int64(len(data)))
if m.maxCacheSize > 0 && atomic.LoadInt64(&m.currentSize) > m.maxCacheSize {
for atomic.LoadInt64(&m.currentSize) > m.maxCacheSize {
m.bytesCache.RemoveOldest()
}
}
}
return data, nil
}
// Flush clears cache and calls postFlushFn async
func (m *memoryCache) Flush(scopes ...string) {
if len(scopes) == 0 {
m.bytesCache.Purge()
go m.postFlushFn()
return
}
// check if fullKey has matching scopes
inScope := func(fullKey string) bool {
_, keyScopes, err := ParseKey(fullKey)
if err != nil {
return false
}
for _, s := range scopes {
for _, ks := range keyScopes {
if ks == s {
return true
}
}
}
return false
}
for _, k := range m.bytesCache.Keys() {
key := k.(string)
if inScope(key) {
m.bytesCache.Remove(key) // Keys() returns copy of cache's key, safe to remove directly
}
}
if m.postFlushFn != nil {
go m.postFlushFn()
}
}
func (m *memoryCache) allowed(data []byte) bool {
if m.maxValueSize > 0 && len(data) >= m.maxValueSize {
return false
}
return true
}
-310
View File
@@ -1,310 +0,0 @@
package cache
import (
"fmt"
"math/rand"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMemoryCache_Get(t *testing.T) {
var postFnCall, coldCalls int32
lc, err := NewMemoryCache(PostFlushFn(func() { atomic.AddInt32(&postFnCall, 1) }))
require.Nil(t, err)
res, err := lc.Get("key", func() ([]byte, error) {
atomic.AddInt32(&coldCalls, 1)
return []byte("result"), nil
})
assert.Nil(t, err)
assert.Equal(t, "result", string(res))
assert.Equal(t, int32(1), atomic.LoadInt32(&coldCalls))
assert.Equal(t, int32(0), atomic.LoadInt32(&postFnCall))
res, err = lc.Get("key", func() ([]byte, error) {
atomic.AddInt32(&coldCalls, 1)
return []byte("result"), nil
})
assert.Nil(t, err)
assert.Equal(t, "result", string(res))
assert.Equal(t, int32(1), atomic.LoadInt32(&coldCalls))
assert.Equal(t, int32(0), atomic.LoadInt32(&postFnCall))
lc.Flush()
time.Sleep(100 * time.Millisecond) // let postFn to do its thing
assert.Equal(t, int32(1), atomic.LoadInt32(&postFnCall))
_, err = lc.Get("key", func() ([]byte, error) {
return nil, errors.New("err")
})
assert.NotNil(t, err)
}
func TestMemoryCache_MaxKeys(t *testing.T) {
var postFnCall, coldCalls int32
lc, err := NewMemoryCache(PostFlushFn(func() { atomic.AddInt32(&postFnCall, 1) }),
MaxKeys(5), MaxValSize(10))
require.Nil(t, err)
// put 5 keys to cache
for i := 0; i < 5; i++ {
res, e := lc.Get(fmt.Sprintf("key-%d", i), func() ([]byte, error) {
atomic.AddInt32(&coldCalls, 1)
return []byte(fmt.Sprintf("result-%d", i)), nil
})
assert.Nil(t, e)
assert.Equal(t, fmt.Sprintf("result-%d", i), string(res))
assert.Equal(t, int32(i+1), atomic.LoadInt32(&coldCalls))
assert.Equal(t, int32(0), atomic.LoadInt32(&postFnCall))
}
// check if really cached
res, err := lc.Get("key-3", func() ([]byte, error) {
return []byte("result-blah"), nil
})
assert.Nil(t, err)
assert.Equal(t, "result-3", string(res), "should be cached")
// try to cache after maxKeys reached
res, err = lc.Get("key-X", func() ([]byte, error) {
return []byte("result-X"), nil
})
assert.Nil(t, err)
assert.Equal(t, "result-X", string(res))
assert.Equal(t, 5, lc.(*memoryCache).bytesCache.Len())
// put to cache and make sure it cached
res, err = lc.Get("key-Z", func() ([]byte, error) {
return []byte("result-Z"), nil
})
assert.Nil(t, err)
assert.Equal(t, "result-Z", string(res))
res, err = lc.Get("key-Z", func() ([]byte, error) {
return []byte("result-Zzzz"), nil
})
assert.Nil(t, err)
assert.Equal(t, "result-Z", string(res), "got cached value")
assert.Equal(t, 5, lc.(*memoryCache).bytesCache.Len())
}
func TestMemoryCache_MaxValueSize(t *testing.T) {
lc, err := NewMemoryCache(MaxKeys(5), MaxValSize(10))
require.Nil(t, err)
// put good size value to cache and make sure it cached
res, err := lc.Get("key-Z", func() ([]byte, error) {
return []byte("result-Z"), nil
})
assert.Nil(t, err)
assert.Equal(t, "result-Z", string(res))
res, err = lc.Get("key-Z", func() ([]byte, error) {
return []byte("result-Zzzz"), nil
})
assert.Nil(t, err)
assert.Equal(t, "result-Z", string(res), "got cached value")
// put too big value to cache and make sure it is not cached
res, err = lc.Get("key-Big", func() ([]byte, error) {
return []byte("1234567890"), nil
})
assert.Nil(t, err)
assert.Equal(t, "1234567890", string(res))
res, err = lc.Get("key-Big", func() ([]byte, error) {
return []byte("result-big"), nil
})
assert.Nil(t, err)
assert.Equal(t, "result-big", string(res), "got not cached value")
}
func TestMemoryCache_MaxCacheSize(t *testing.T) {
lc, err := NewMemoryCache(MaxKeys(50), MaxCacheSize(20))
require.Nil(t, err)
// put good size value to cache and make sure it cached
res, err := lc.Get("key-Z", func() ([]byte, error) {
return []byte("result-Z"), nil
})
assert.Nil(t, err)
assert.Equal(t, "result-Z", string(res))
assert.Equal(t, int64(8), lc.(*memoryCache).currentSize)
_, err = lc.Get("key-Z2", func() ([]byte, error) {
return []byte("result-Z"), nil
})
assert.Nil(t, err)
assert.Equal(t, int64(16), lc.(*memoryCache).currentSize)
// this will cause removal
_, err = lc.Get("key-Z3", func() ([]byte, error) {
return []byte("result-Z"), nil
})
assert.Nil(t, err)
assert.Equal(t, int64(16), lc.(*memoryCache).currentSize)
assert.Equal(t, 2, lc.(*memoryCache).bytesCache.Len())
}
func TestMemoryCache_MaxCacheSizeParallel(t *testing.T) {
lc, err := NewMemoryCache(MaxCacheSize(123), MaxKeys(10000))
require.Nil(t, err)
wg := sync.WaitGroup{}
for i := 0; i < 1000; i++ {
wg.Add(1)
i := i
go func() {
time.Sleep(time.Duration(rand.Intn(100)) * time.Nanosecond)
defer wg.Done()
res, err := lc.Get(fmt.Sprintf("key-%d", i), func() ([]byte, error) {
return []byte(fmt.Sprintf("result-%d", i)), nil
})
require.Nil(t, err)
require.Equal(t, fmt.Sprintf("result-%d", i), string(res))
size := atomic.LoadInt64(&lc.(*memoryCache).currentSize)
require.True(t, size < 200 && size >= 0, "unexpected size=%d", size) // won't be exactly 123 due parallel
}()
}
wg.Wait()
assert.True(t, lc.(*memoryCache).currentSize < 123 && lc.(*memoryCache).currentSize >= 0)
t.Log("size=", lc.(*memoryCache).currentSize)
}
func TestMemoryCache_Parallel(t *testing.T) {
var coldCalls int32
lc, err := NewMemoryCache()
require.Nil(t, err)
res, err := lc.Get("key", func() ([]byte, error) {
return []byte("value"), nil
})
assert.Nil(t, err)
assert.Equal(t, "value", string(res))
wg := sync.WaitGroup{}
for i := 0; i < 1000; i++ {
wg.Add(1)
i := i
go func() {
defer wg.Done()
res, err := lc.Get("key", func() ([]byte, error) {
atomic.AddInt32(&coldCalls, 1)
return []byte(fmt.Sprintf("result-%d", i)), nil
})
require.Nil(t, err)
require.Equal(t, "value", string(res))
}()
}
wg.Wait()
assert.Equal(t, int32(0), atomic.LoadInt32(&coldCalls))
}
func TestMemoryCache_Scopes(t *testing.T) {
lc, err := NewMemoryCache()
require.Nil(t, err)
res, err := lc.Get(Key("key", "s1", "s2"), func() ([]byte, error) {
return []byte("value"), nil
})
assert.Nil(t, err)
assert.Equal(t, "value", string(res))
res, err = lc.Get(Key("key2", "s2"), func() ([]byte, error) {
return []byte("value2"), nil
})
assert.Nil(t, err)
assert.Equal(t, "value2", string(res))
assert.Equal(t, 2, lc.(*memoryCache).bytesCache.Len())
lc.Flush("s1")
assert.Equal(t, 1, lc.(*memoryCache).bytesCache.Len())
_, err = lc.Get(Key("key2", "s2"), func() ([]byte, error) {
assert.Fail(t, "should stay")
return nil, nil
})
assert.Nil(t, err)
res, err = lc.Get(Key("key", "s1", "s2"), func() ([]byte, error) {
return []byte("value-upd"), nil
})
assert.Nil(t, err)
assert.Equal(t, "value-upd", string(res), "was deleted, update")
}
func TestMemoryCache_Flush(t *testing.T) {
lc, err := NewMemoryCache()
require.Nil(t, err)
addToCache := func(key string, scopes ...string) {
res, err := lc.Get(key, func() ([]byte, error) {
return []byte("value" + key), nil
})
require.Nil(t, err)
require.Equal(t, "value"+key, string(res))
}
init := func() {
lc.Flush()
addToCache(Key("key1", "s1", "s2"))
addToCache(Key("key2", "s1", "s2", "s3"))
addToCache(Key("key3", "s1", "s2", "s3"))
addToCache(Key("key4", "s2", "s3"))
addToCache(Key("key5", "s2"))
addToCache(Key("key6"))
addToCache(Key("key7", "s4", "s3"))
require.Equal(t, 7, lc.(*memoryCache).bytesCache.Len(), "cache init")
}
tbl := []struct {
scopes []string
left int
msg string
}{
{[]string{}, 0, "full flush, no scopes"},
{[]string{"s0"}, 7, "flush wrong scope"},
{[]string{"s1"}, 4, "flush s1 scope"},
{[]string{"s2", "s1"}, 2, "flush s2+s1 scope"},
{[]string{"s1", "s2"}, 2, "flush s1+s2 scope"},
{[]string{"s1", "s2", "s4"}, 1, "flush s1+s2+s4 scope"},
{[]string{"s1", "s2", "s3"}, 1, "flush s1+s2+s3 scope"},
{[]string{"s1", "s2", "ss"}, 2, "flush s1+s2+wrong scope"},
}
for i, tt := range tbl {
init()
lc.Flush(tt.scopes...)
assert.Equal(t, tt.left, lc.(*memoryCache).bytesCache.Len(), "keys size, %s #%d", tt.msg, i)
}
}
func TestMemoryCache_FlushFailed(t *testing.T) {
lc, err := NewMemoryCache()
require.Nil(t, err)
val, err := lc.Get("invalid-composite", func() ([]byte, error) {
return []byte("value"), nil
})
assert.Nil(t, err)
assert.Equal(t, "value", string(val))
assert.Equal(t, 1, lc.(*memoryCache).bytesCache.Len())
lc.Flush("invalid-composite")
assert.Equal(t, 1, lc.(*memoryCache).bytesCache.Len())
}
func TestMemoryCache_BadOptions(t *testing.T) {
_, err := NewMemoryCache(MaxCacheSize(-1))
assert.EqualError(t, err, "failed to set cache option: negative size or MaxCacheSize, -1")
_, err = NewMemoryCache(MaxKeys(-1))
assert.EqualError(t, err, "failed to set cache option: negative size for MaxKeys, -1")
_, err = NewMemoryCache(MaxValSize(-1))
assert.EqualError(t, err, "failed to set cache option: negative size for MaxValSize, -1")
}
-50
View File
@@ -1,50 +0,0 @@
package cache
import "github.com/pkg/errors"
// Option func type
type Option func(lc *memoryCache) error
// MaxValSize functional option defines the largest value's size allowed to be cached
// By default it is 0, which means unlimited.
func MaxValSize(max int) Option {
return func(lc *memoryCache) error {
lc.maxValueSize = max
if max <= 0 {
return errors.Errorf("negative size for MaxValSize, %d", max)
}
return nil
}
}
// MaxKeys functional option defines how many keys to keep.
// By default it is 0, which means unlimited.
func MaxKeys(max int) Option {
return func(lc *memoryCache) error {
lc.maxKeys = max
if max <= 0 {
return errors.Errorf("negative size for MaxKeys, %d", max)
}
return nil
}
}
// MaxCacheSize functional option defines the total size of cached data.
// By default it is 0, which means unlimited.
func MaxCacheSize(max int64) Option {
return func(lc *memoryCache) error {
lc.maxCacheSize = max
if max <= 0 {
return errors.Errorf("negative size or MaxCacheSize, %d", max)
}
return nil
}
}
// PostFlushFn functional option defines how callback function called after each Flush.
func PostFlushFn(postFlushFn func()) Option {
return func(lc *memoryCache) error {
lc.postFlushFn = postFlushFn
return nil
}
}
-44
View File
@@ -1,44 +0,0 @@
package rest
import (
"fmt"
"log"
"net/http"
"net/url"
"runtime"
"strings"
"github.com/go-chi/render"
)
// SendErrorJSON makes {error: blah, details: blah} json body and responds with error code
func SendErrorJSON(w http.ResponseWriter, r *http.Request, code int, err error, details string) {
log.Printf("[DEBUG] %s", errDetailsMsg(r, code, err, details))
render.Status(r, code)
render.JSON(w, r, map[string]interface{}{"error": err.Error(), "details": details})
}
func errDetailsMsg(r *http.Request, code int, err error, details string) string {
uinfoStr := ""
if user, e := GetUserInfo(r); e == nil {
uinfoStr = user.Name + "/" + user.ID + " - "
}
q := r.URL.String()
if qun, e := url.QueryUnescape(q); e == nil {
q = qun
}
srcFileInfo := ""
if pc, file, line, ok := runtime.Caller(2); ok {
fnameElems := strings.Split(file, "/")
funcNameElems := strings.Split(runtime.FuncForPC(pc).Name(), "/")
srcFileInfo = fmt.Sprintf(" [caused by %s:%d %s]", strings.Join(fnameElems[len(fnameElems)-3:], "/"),
line, funcNameElems[len(funcNameElems)-1])
}
remoteIP := r.RemoteAddr
if pos := strings.Index(remoteIP, ":"); pos >= 0 {
remoteIP = remoteIP[:pos]
}
return fmt.Sprintf("%s - %v - %d - %s%s - %s%s", details, err, code, uinfoStr, remoteIP, q, srcFileInfo)
}
-60
View File
@@ -1,60 +0,0 @@
package rest
import (
"errors"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/umputun/remark/app/store"
)
func TestSendErrorJSON(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/error" {
t.Log("http err request", r.URL)
SendErrorJSON(w, r, 500, errors.New("error 500"), "error details 123456")
return
}
w.WriteHeader(404)
}))
defer ts.Close()
resp, err := http.Get(ts.URL + "/error")
require.Nil(t, err)
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
require.Nil(t, err)
assert.Equal(t, 500, resp.StatusCode)
assert.Equal(t, `{"details":"error details 123456","error":"error 500"}`+"\n", string(body))
}
func TestErrorDetailsMsg(t *testing.T) {
callerFn := func() {
req, err := http.NewRequest("GET", "https://example.com/test?k1=v1&k2=v2", nil)
require.Nil(t, err)
req.RemoteAddr = "1.2.3.4"
msg := errDetailsMsg(req, 500, errors.New("error 500"), "error details 123456")
assert.Equal(t, "error details 123456 - error 500 - 500 - 1.2.3.4 - https://example.com/test?k1=v1&k2=v2 [caused by app/rest/httperrors_test.go:47 rest.TestErrorDetailsMsg]", msg)
}
callerFn()
}
func TestErrorDetailsMsgWithUser(t *testing.T) {
callerFn := func() {
req, err := http.NewRequest("GET", "https://example.com/test?k1=v1&k2=v2", nil)
req.RemoteAddr = "127.0.0.1:1234"
req = SetUserInfo(req, store.User{Name: "test", ID: "id"})
require.Nil(t, err)
msg := errDetailsMsg(req, 500, errors.New("error 500"), "error details 123456")
assert.Equal(t, "error details 123456 - error 500 - 500 - test/id - 127.0.0.1 - https://example.com/test?k1=v1&k2=v2 [caused by app/rest/httperrors_test.go:59 rest.TestErrorDetailsMsgWithUser]", msg)
}
callerFn()
}
-119
View File
@@ -1,119 +0,0 @@
package proxy
import (
"io"
"log"
"net/http"
"strconv"
"strings"
"time"
"github.com/go-chi/chi"
"github.com/pkg/errors"
"github.com/umputun/remark/app/rest"
"github.com/umputun/remark/app/store"
)
// Avatar provides file-system store and http handler for avatars
// On user login auth will call Put and it will retrieve and save picture locally.
type Avatar struct {
Store AvatarStore
RoutePath string
RemarkURL string
}
const imgSfx = ".image"
// Put stores retrieved avatar to StorePath. Gets image from user info. Returns proxied url
func (p *Avatar) Put(u store.User) (avatarURL string, err error) {
// no picture for user, try default avatar
if u.Picture == "" {
return "", errors.Errorf("no picture for %s", u.ID)
}
// load avatar from remote location
client := http.Client{Timeout: 10 * time.Second}
var resp *http.Response
err = retry(5, time.Second, func() error {
var e error
resp, e = client.Get(u.Picture)
return e
})
if err != nil {
return "", errors.Wrap(err, "failed to fetch avatar from the orig")
}
defer func() {
if e := resp.Body.Close(); e != nil {
log.Printf("[WARN] can't close response body, %s", e)
}
}()
if resp.StatusCode != http.StatusOK {
return "", errors.Errorf("failed to get avatar from the orig, status %s", resp.Status)
}
avatar, err := p.Store.Put(u.ID, resp.Body)
if err != nil {
return "", err
}
log.Printf("[DEBUG] saved avatar from %s to %s, user %q", u.Picture, avatar, u.Name)
return p.RemarkURL + p.RoutePath + "/" + avatar, nil
}
// Routes returns auth routes for given provider
func (p *Avatar) Routes(middlewares ...func(http.Handler) http.Handler) (string, chi.Router) {
router := chi.NewRouter()
router.Use(middlewares...)
// GET /123456789.image
router.Get("/{avatar}", func(w http.ResponseWriter, r *http.Request) {
avatar := chi.URLParam(r, "avatar")
// enforce client-side caching
etag := `"` + avatar + `"`
w.Header().Set("Etag", etag)
w.Header().Set("Cache-Control", "max-age=2592000") // 30 days
if match := r.Header.Get("If-None-Match"); match != "" {
if strings.Contains(match, etag) {
w.WriteHeader(http.StatusNotModified)
return
}
}
avReader, size, err := p.Store.Get(avatar)
if err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't load avatar")
return
}
defer func() {
if e := avReader.Close(); e != nil {
log.Printf("[WARN] can't close avatar reader for %s, %s", avatar, e)
}
}()
w.Header().Set("Content-Type", "image/*")
w.Header().Set("Content-Length", strconv.Itoa(size))
w.WriteHeader(http.StatusOK)
if _, err = io.Copy(w, avReader); err != nil {
log.Printf("[WARN] can't send response to %s, %s", r.RemoteAddr, err)
}
})
return p.RoutePath, router
}
func retry(retries int, delay time.Duration, fn func() error) (err error) {
for i := 0; i < retries; i++ {
if err = fn(); err == nil {
return nil
}
time.Sleep(delay)
}
return errors.Wrap(err, "retry failed")
}
-145
View File
@@ -1,145 +0,0 @@
package proxy
//go:generate sh -c "mockery -inpkg -name AvatarStore -print > /tmp/mock.tmp && mv /tmp/mock.tmp avatar_store_mock.go"
import (
"bytes"
"fmt"
"hash/crc64"
"image"
"image/png"
"io"
"log"
"os"
"path"
"strings"
"sync"
// Initializing packages for supporting GIF and JPEG formats.
_ "image/gif"
_ "image/jpeg"
"github.com/pkg/errors"
"golang.org/x/image/draw"
"github.com/umputun/remark/app/store"
)
// AvatarStore defines interface to store and serve avatars
type AvatarStore interface {
Put(userID string, reader io.Reader) (avatar string, err error)
Get(avatar string) (reader io.ReadCloser, size int, err error)
}
// FSAvatarStore implements AvatarStore for local file system
type FSAvatarStore struct {
storePath string
resizeLimit int
ctcTable *crc64.Table
once sync.Once
}
// NewFSAvatarStore makes file-system avatar store
func NewFSAvatarStore(storePath string, resizeLimit int) *FSAvatarStore {
return &FSAvatarStore{storePath: storePath, resizeLimit: resizeLimit}
}
// Put avatar for userID to file and return avatar's file name (base), like 12345678.image
func (fs *FSAvatarStore) Put(userID string, reader io.Reader) (avatar string, err error) {
id := store.EncodeID(userID)
location := fs.location(id) // location adds partition to path
if _, err = os.Stat(location); os.IsNotExist(err) {
if e := os.Mkdir(location, 0700); e != nil {
return "", errors.Wrapf(e, "failed to mkdir avatar location %s", location)
}
}
avFile := path.Join(location, id+imgSfx)
fh, err := os.Create(avFile)
if err != nil {
return "", errors.Wrapf(err, "can't create file %s", avFile)
}
defer func() {
if e := fh.Close(); e != nil {
log.Printf("[WARN] can't close avatar file %s, %s", avFile, e)
}
}()
// Trying to resize avatar.
if reader = resize(reader, fs.resizeLimit); reader == nil {
return "", errors.New("avatar reader is nil")
}
if _, err = io.Copy(fh, reader); err != nil {
return "", errors.Wrapf(err, "can't save file %s", avFile)
}
return id + imgSfx, nil
}
// Get avatar reader for avatar id.image
func (fs *FSAvatarStore) Get(avatar string) (reader io.ReadCloser, size int, err error) {
location := fs.location(strings.TrimSuffix(avatar, imgSfx))
avFile := path.Join(location, avatar)
fh, err := os.Open(avFile)
if err != nil {
return nil, 0, errors.Wrapf(err, "can't load avatar %s, id")
}
if fi, e := fh.Stat(); e == nil {
size = int(fi.Size())
}
return fh, size, nil
}
// get location (directory) for user id by adding partition to final path in order to keep files
// in different subdirectories and avoid too many files in a single place.
// the end result is a full path like this - /tmp/avatars.test/92
func (fs *FSAvatarStore) location(id string) string {
fs.once.Do(func() { fs.ctcTable = crc64.MakeTable(crc64.ECMA) })
checksum64 := crc64.Checksum([]byte(id), fs.ctcTable)
partition := checksum64 % 100
return path.Join(fs.storePath, fmt.Sprintf("%02d", partition))
}
// Resizes an image of supported format (PNG, JPG, GIF) to the size of "limit" px of the biggest side
// (width or height) preserving aspect ratio.
// Returns original reader if resizing is not needed or failed.
func resize(reader io.Reader, limit int) io.Reader {
if reader == nil {
log.Print("[WARN] avatar resize(): reader is nil")
return nil
}
if limit <= 0 {
log.Print("[DEBUG] avatar resize(): limit should be greater than 0")
return reader
}
var teeBuf bytes.Buffer
tee := io.TeeReader(reader, &teeBuf)
src, _, err := image.Decode(tee)
if err != nil {
log.Printf("[WARN] avatar resize(): can't decode avatar image, %s", err)
return &teeBuf
}
bounds := src.Bounds()
w, h := bounds.Dx(), bounds.Dy()
if w <= limit && h <= limit || w <= 0 || h <= 0 {
log.Print("[DEBUG] resizing image is smaller that the limit or has 0 size")
return &teeBuf
}
newW, newH := w*limit/h, limit
if w > h {
newW, newH = limit, h*limit/w
}
m := image.NewRGBA(image.Rect(0, 0, newW, newH))
// Slower than `draw.ApproxBiLinear.Scale()` but better quality.
draw.BiLinear.Scale(m, m.Bounds(), src, src.Bounds(), draw.Src, nil)
var out bytes.Buffer
if err = png.Encode(&out, m); err != nil {
log.Printf("[WARN] avatar resize(): can't encode resized avatar to PNG, %s", err)
return &teeBuf
}
return &out
}
-135
View File
@@ -1,135 +0,0 @@
package proxy
import (
"bytes"
"image"
"io"
"io/ioutil"
"os"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAvatarStore_Put(t *testing.T) {
p := NewFSAvatarStore("/tmp/avatars.test", 300)
os.MkdirAll("/tmp/avatars.test", 0700)
defer os.RemoveAll("/tmp/avatars.test")
avatar, err := p.Put("user1", nil)
assert.Equal(t, "", avatar)
assert.EqualError(t, err, "avatar reader is nil")
avatar, err = p.Put("user1", strings.NewReader("some picture bin data"))
require.Nil(t, err)
assert.Equal(t, "b3daa77b4c04a9551b8781d03191fe098f325e67.image", avatar)
fi, err := os.Stat("/tmp/avatars.test/30/b3daa77b4c04a9551b8781d03191fe098f325e67.image")
assert.NoError(t, err)
assert.Equal(t, int64(21), fi.Size())
avatar, err = p.Put("user2", strings.NewReader("some picture bin data 123"))
require.Nil(t, err)
assert.Equal(t, "a1881c06eec96db9901c7bbfe41c42a3f08e9cb4.image", avatar)
fi, err = os.Stat("/tmp/avatars.test/84/a1881c06eec96db9901c7bbfe41c42a3f08e9cb4.image")
assert.NoError(t, err)
assert.Equal(t, int64(25), fi.Size())
// with resize
file, e := os.Open("testdata/circles.png")
require.Nil(t, e)
avatar, err = p.Put("user3", file)
require.Nil(t, err)
assert.Equal(t, "0b7f849446d3383546d15a480966084442cd2193.image", avatar)
fi, err = os.Stat("/tmp/avatars.test/60/0b7f849446d3383546d15a480966084442cd2193.image")
assert.NoError(t, err)
assert.Equal(t, int64(6986), fi.Size())
p = NewFSAvatarStore("/dev/null", 300)
_, err = p.Put("user1", strings.NewReader("some picture bin data"))
assert.EqualError(t, err, "can't create file /dev/null/30/b3daa77b4c04a9551b8781d03191fe098f325e67.image: open /dev/null/30/b3daa77b4c04a9551b8781d03191fe098f325e67.image: not a directory")
}
func TestAvatarStore_Get(t *testing.T) {
p := NewFSAvatarStore("/tmp/avatars.test", 300)
os.MkdirAll("/tmp/avatars.test/30", 0700)
defer os.RemoveAll("/tmp/avatars.test")
err := ioutil.WriteFile("/tmp/avatars.test/30/b3daa77b4c04a9551b8781d03191fe098f325e67.image", []byte("something"), 0666)
assert.Nil(t, err)
r, size, err := p.Get("b3daa77b4c04a9551b8781d03191fe098f325e67.image")
assert.Nil(t, err)
assert.Equal(t, 9, size)
data, err := ioutil.ReadAll(r)
assert.Nil(t, err)
assert.Equal(t, "something", string(data))
}
func TestAvatarStore_Location(t *testing.T) {
p := NewFSAvatarStore("/tmp/avatars.test", 300)
tbl := []struct {
id string
res string
}{
{"abc", "/tmp/avatars.test/35"},
{"xyz", "/tmp/avatars.test/69"},
{"blah blah", "/tmp/avatars.test/29"},
}
for i, tt := range tbl {
assert.Equal(t, tt.res, p.location(tt.id), "test #%d", i)
}
}
func TestAvatarStore_resize(t *testing.T) {
checkC := func(t *testing.T, r io.Reader, cExp []byte) {
content, err := ioutil.ReadAll(r)
require.NoError(t, err)
assert.Equal(t, cExp, content)
}
// Reader is nil.
resizedR := resize(nil, 100)
// assert.EqualError(t, err, "limit should be greater than 0")
assert.Nil(t, resizedR)
// Negative limit error.
resizedR = resize(strings.NewReader("some picture bin data"), -1)
require.NotNil(t, resizedR)
checkC(t, resizedR, []byte("some picture bin data"))
// Decode error.
resizedR = resize(strings.NewReader("invalid image content"), 100)
assert.NotNil(t, resizedR)
checkC(t, resizedR, []byte("invalid image content"))
cases := []struct {
file string
wr, hr int
}{
{"testdata/circles.png", 400, 300}, // full size: 800x600 px
{"testdata/circles.jpg", 300, 400}, // full size: 600x800 px
}
for _, c := range cases {
img, err := ioutil.ReadFile(c.file)
require.Nil(t, err, "can't open test file %s", c.file)
// No need for resize, avatar dimensions are smaller than resize limit.
resizedR = resize(bytes.NewReader(img), 800)
assert.NotNilf(t, resizedR, "file %s", c.file)
checkC(t, resizedR, img)
// Resizing to half of width. Check resizedR avatar format PNG.
resizedR = resize(bytes.NewReader(img), 400)
assert.NotNilf(t, resizedR, "file %s", c.file)
imgRz, format, err := image.Decode(resizedR)
assert.Nilf(t, err, "file %s", c.file)
assert.Equalf(t, "png", format, "file %s", c.file)
bounds := imgRz.Bounds()
assert.Equalf(t, c.wr, bounds.Dx(), "file %s", c.file)
assert.Equalf(t, c.hr, bounds.Dy(), "file %s", c.file)
}
}
-142
View File
@@ -1,142 +0,0 @@
package proxy
import (
"bytes"
"errors"
"fmt"
"io"
"log"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/umputun/remark/app/store"
)
func TestAvatar_Put(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/pic.png" {
w.Header().Set("Content-Type", "image/*")
fmt.Fprint(w, "some picture bin data")
return
}
http.Error(w, "not found", http.StatusNotFound)
}))
defer ts.Close()
p := Avatar{RoutePath: "/avatar", RemarkURL: "http://localhost:8080", Store: NewFSAvatarStore("/tmp/avatars.test", 300)}
os.MkdirAll("/tmp/avatars.test", 0700)
defer os.RemoveAll("/tmp/avatars.test")
u := store.User{ID: "user1", Name: "user1 name", Picture: ts.URL + "/pic.png"}
res, err := p.Put(u)
assert.NoError(t, err)
assert.Equal(t, "http://localhost:8080/avatar/b3daa77b4c04a9551b8781d03191fe098f325e67.image", res)
fi, err := os.Stat("/tmp/avatars.test/30/b3daa77b4c04a9551b8781d03191fe098f325e67.image")
assert.NoError(t, err)
assert.Equal(t, int64(21), fi.Size())
u.ID = "user2"
res, err = p.Put(u)
assert.NoError(t, err)
assert.Equal(t, "http://localhost:8080/avatar/a1881c06eec96db9901c7bbfe41c42a3f08e9cb4.image", res)
fi, err = os.Stat("/tmp/avatars.test/84/a1881c06eec96db9901c7bbfe41c42a3f08e9cb4.image")
assert.NoError(t, err)
assert.Equal(t, int64(21), fi.Size())
}
func TestAvatar_PutFailed(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Print("request: ", r.URL.Path)
w.WriteHeader(http.StatusNotFound)
}))
defer ts.Close()
p := Avatar{RoutePath: "/avatar", Store: NewFSAvatarStore("/tmp/avatars.test", 300)}
u := store.User{ID: "user1", Name: "user1 name"}
_, err := p.Put(u)
assert.EqualError(t, err, "no picture for user1")
u = store.User{ID: "user1", Name: "user1 name", Picture: "http://127.0.0.1:12345/avater/pic"}
_, err = p.Put(u)
require.Error(t, err)
assert.Contains(t, err.Error(), "connect: connection refused")
u = store.User{ID: "user1", Name: "user1 name", Picture: ts.URL + "/avatar/pic"}
_, err = p.Put(u)
require.Error(t, err)
assert.Contains(t, err.Error(), "failed to get avatar from the orig")
}
func TestAvatar_Routes(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/pic.png" {
w.Header().Set("Content-Type", "image/*")
w.Header().Set("Custom-Header", "xyz")
fmt.Fprint(w, "some picture bin data")
return
}
http.Error(w, "not found", http.StatusNotFound)
}))
defer ts.Close()
p := Avatar{RoutePath: "/avatar", Store: NewFSAvatarStore("/tmp/avatars.test", 300)}
os.MkdirAll("/tmp/avatars.test", 0700)
defer os.RemoveAll("/tmp/avatars.test")
u := store.User{ID: "user1", Name: "user1 name", Picture: ts.URL + "/pic.png"}
_, err := p.Put(u)
assert.NoError(t, err)
req, err := http.NewRequest("GET", "/b3daa77b4c04a9551b8781d03191fe098f325e67.image", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
_, routes := p.Routes()
handler := http.Handler(routes)
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
assert.Equal(t, []string{"image/*"}, rr.HeaderMap["Content-Type"])
assert.Equal(t, []string{"21"}, rr.HeaderMap["Content-Length"])
assert.Equal(t, []string(nil), rr.HeaderMap["Custom-Header"], "strip all custom headers")
assert.NotNil(t, rr.HeaderMap["Etag"])
bb := bytes.Buffer{}
sz, err := io.Copy(&bb, rr.Body)
assert.NoError(t, err)
assert.Equal(t, int64(21), sz)
assert.Equal(t, "some picture bin data", bb.String())
}
func TestAvatar_Retry(t *testing.T) {
i := 0
err := retry(5, time.Millisecond, func() error {
if i == 3 {
return nil
}
i++
return errors.New("err")
})
assert.Nil(t, err)
assert.Equal(t, 3, i)
st := time.Now()
err = retry(5, time.Millisecond, func() error {
return errors.New("err")
})
assert.NotNil(t, err)
assert.True(t, time.Since(st) >= time.Microsecond*5)
}
-126
View File
@@ -1,126 +0,0 @@
package proxy
import (
"encoding/base64"
"io"
"log"
"net/http"
"strings"
"time"
"github.com/PuerkitoBio/goquery"
"github.com/go-chi/chi"
"github.com/pkg/errors"
"github.com/umputun/remark/app/rest"
)
// Image extracts image src from comment's html and provides proxy for them
// this is needed to keep remark42 running behind of HTTPS serve all images via https
type Image struct {
RemarkURL string
RoutePath string
Enabled bool
}
// Convert all img src links without https to proxied links
func (p Image) Convert(commentHTML string) string {
if !p.Enabled || strings.HasPrefix(p.RemarkURL, "http://") {
return commentHTML
}
imgs, err := p.extract(commentHTML)
if err != nil {
return commentHTML
}
return p.replace(commentHTML, imgs)
}
// Routes returns router group to respond to proxied request
func (p Image) Routes() chi.Router {
router := chi.NewRouter()
if !p.Enabled {
return router
}
router.Get("/", func(w http.ResponseWriter, r *http.Request) {
src, err := base64.URLEncoding.DecodeString(r.URL.Query().Get("src"))
if err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't decode image url")
return
}
client := http.Client{Timeout: 30 * time.Second}
var resp *http.Response
err = retry(5, time.Second, func() error {
var e error
resp, e = client.Get(string(src))
return e
})
if err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get image "+string(src))
return
}
defer func() {
if e := resp.Body.Close(); e != nil {
log.Printf("[WARN] can't close body, %s", e)
}
}()
if resp.StatusCode != http.StatusOK {
w.WriteHeader(resp.StatusCode)
return
}
for k, v := range resp.Header {
if strings.EqualFold(k, "Content-Type") {
w.Header().Set(k, v[0])
}
if strings.EqualFold(k, "Content-Length") {
w.Header().Set(k, v[0])
}
}
// enforce client-side caching
etag := `"` + r.URL.Query().Get("src") + `"`
w.Header().Set("Etag", etag)
w.Header().Set("Cache-Control", "max-age=2592000") // 30 days
if match := r.Header.Get("If-None-Match"); match != "" {
if strings.Contains(match, etag) {
w.WriteHeader(http.StatusNotModified)
return
}
}
if _, e := io.Copy(w, resp.Body); e != nil {
log.Printf("[WARN] can't copy image stream, %s", e)
}
})
return router
}
// extract gets all non-https images and return list of src
func (p Image) extract(commentHTML string) ([]string, error) {
doc, err := goquery.NewDocumentFromReader(strings.NewReader(commentHTML))
if err != nil {
return nil, errors.Wrap(err, "can't create document")
}
result := []string{}
doc.Find("img").Each(func(i int, s *goquery.Selection) {
if im, ok := s.Attr("src"); ok {
if strings.HasPrefix(im, "http://") {
result = append(result, im)
}
}
})
return result, nil
}
// replace img links in commentHTML with route to proxy with base64 encoded original link
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)
}
return commentHTML
}
-121
View File
@@ -1,121 +0,0 @@
package proxy
import (
"encoding/base64"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPicture_Extract(t *testing.T) {
tbl := []struct {
inp string
res []string
}{
{
`<p> blah <img src="http://radio-t.com/img.png"/> test</p>`,
[]string{"http://radio-t.com/img.png"},
},
{
`<p> blah <img src="https://radio-t.com/img.png"/> test</p>`,
[]string{},
},
{
`<img src="http://radio-t.com/img2.png"/>`,
[]string{"http://radio-t.com/img2.png"},
},
{
`<img src="http://radio-t.com/img3.png"/> <div>xyz <img src="http://images.pexels.com/67636/img4.jpeg"> </div>`,
[]string{"http://radio-t.com/img3.png", "http://images.pexels.com/67636/img4.jpeg"},
},
{
`<img src="https://radio-t.com/img3.png"/> <div>xyz <img src="http://images.pexels.com/67636/img4.jpeg"> </div>`,
[]string{"http://images.pexels.com/67636/img4.jpeg"},
},
{
`abcd <b>blah</b> <h1>xxx</h1>`,
[]string{},
},
}
img := Image{Enabled: true}
for i, tt := range tbl {
res, err := img.extract(tt.inp)
assert.Nil(t, err, "err in #%d", i)
assert.Equal(t, tt.res, res, "mismatch in #%d", i)
}
}
func TestPicture_Replace(t *testing.T) {
img := Image{Enabled: true, RoutePath: "/img"}
r := img.replace(`<img src="http://radio-t.com/img3.png"/> xyz <img src="http://images.pexels.com/67636/img4.jpeg">`,
[]string{"http://radio-t.com/img3.png", "http://images.pexels.com/67636/img4.jpeg"})
assert.Equal(t, `<img src="/img?src=aHR0cDovL3JhZGlvLXQuY29tL2ltZzMucG5n"/> xyz <img src="/img?src=aHR0cDovL2ltYWdlcy5wZXhlbHMuY29tLzY3NjM2L2ltZzQuanBlZw==">`, r)
}
func TestImage_Routes(t *testing.T) {
img := Image{Enabled: true, RemarkURL: "https://demo.remark42.com", RoutePath: "/api/v1/proxy"}
router := img.Routes()
httpSrv := imgHTTPServer(t)
defer httpSrv.Close()
ts := httptest.NewServer(router)
defer ts.Close()
encodedImgURL := base64.URLEncoding.EncodeToString([]byte(httpSrv.URL + "/image/img1.png"))
resp, err := http.Get(ts.URL + "/?src=" + encodedImgURL)
require.Nil(t, err)
assert.Equal(t, 200, resp.StatusCode)
t.Logf("%+v", resp.Header)
assert.Equal(t, "123", resp.Header["Content-Length"][0])
assert.Equal(t, "image/png", resp.Header["Content-Type"][0])
encodedImgURL = base64.URLEncoding.EncodeToString([]byte(httpSrv.URL + "/image/no-such-image.png"))
resp, err = http.Get(ts.URL + "/?src=" + encodedImgURL)
require.Nil(t, err)
assert.Equal(t, 404, resp.StatusCode)
encodedImgURL = base64.URLEncoding.EncodeToString([]byte(httpSrv.URL + "bad encoding"))
resp, err = http.Get(ts.URL + "/?src=" + encodedImgURL)
require.Nil(t, err)
assert.Equal(t, 400, resp.StatusCode)
}
func TestPicture_Convert(t *testing.T) {
img := Image{Enabled: true, RoutePath: "/img"}
r := img.Convert(`<img src="http://radio-t.com/img3.png"/> xyz <img src="http://images.pexels.com/67636/img4.jpeg">`)
assert.Equal(t, `<img src="/img?src=aHR0cDovL3JhZGlvLXQuY29tL2ltZzMucG5n"/> xyz <img src="/img?src=aHR0cDovL2ltYWdlcy5wZXhlbHMuY29tLzY3NjM2L2ltZzQuanBlZw==">`, r)
r = img.Convert(`<img src="https://radio-t.com/img3.png"/> xyz <img src="http://images.pexels.com/67636/img4.jpeg">`)
assert.Equal(t, `<img src="https://radio-t.com/img3.png"/> xyz <img src="/img?src=aHR0cDovL2ltYWdlcy5wZXhlbHMuY29tLzY3NjM2L2ltZzQuanBlZw==">`, r)
img = Image{Enabled: true, RoutePath: "/img", RemarkURL: "http://example.com"}
r = img.Convert(`<img src="http://radio-t.com/img3.png"/> xyz`)
assert.Equal(t, `<img src="http://radio-t.com/img3.png"/> xyz`, r, "http:// remark url, no proxy")
img = Image{Enabled: false, RoutePath: "/img"}
r = img.Convert(`<img src="http://radio-t.com/img3.png"/> xyz`)
assert.Equal(t, `<img src="http://radio-t.com/img3.png"/> xyz`, r, "disabled, no proxy")
}
func imgHTTPServer(t *testing.T) *httptest.Server {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/image/img1.png" {
t.Log("http img request", r.URL)
w.Header().Add("Content-Length", "123")
w.Header().Add("Content-Type", "image/png")
w.Write([]byte(fmt.Sprintf("%123s", "X")))
return
}
t.Log("http img request - not found", r.URL)
w.WriteHeader(404)
}))
return ts
}
-155
View File
@@ -1,155 +0,0 @@
package rest
import (
"sort"
"strings"
"time"
"github.com/umputun/remark/app/store"
)
// Tree is formatter making tree from the list of comments
type Tree struct {
Nodes []*Node `json:"comments"`
Info store.PostInfo `json:"info,omitempty"`
}
// Node is a comment with optional replies
type Node struct {
Comment store.Comment `json:"comment"`
Replies []*Node `json:"replies,omitempty"`
tsModified time.Time
tsCreated time.Time
}
// recurData wraps all fields used in recursive processing as intermediate results
type recurData struct {
tsModified time.Time
tsCreated time.Time
visible bool
}
// MakeTree gets unsorted list of comments and produces Tree
// It will make store.PostInfo by itself and will mark Info.ReadOnly based on passed readOnlyAge
// Tree maker is local and has no access to the data store. By this reason it has to make Info and won't be able
// to handle store's read-only status. This status should be set by caller.
func MakeTree(comments []store.Comment, sortType string, readOnlyAge int) *Tree {
if len(comments) == 0 {
return &Tree{}
}
res := Tree{
Info: store.PostInfo{
URL: comments[0].Locator.URL,
FirstTS: comments[0].Timestamp,
LastTS: comments[0].Timestamp,
},
}
res.Info.Count = len(res.filter(comments, func(c store.Comment) bool { return !c.Deleted }))
topComments := res.filter(comments, func(c store.Comment) bool { return c.ParentID == "" })
res.Nodes = []*Node{}
for _, rootComment := range topComments {
node := Node{Comment: rootComment}
rd := recurData{}
commentsTree, tsModified, tsCreated := res.proc(comments, &node, &rd, rootComment.ID)
// skip deleted with no sub-comments ar all sub-comments deleted
if rootComment.Deleted && (len(commentsTree.Replies) == 0 || !rd.visible) {
continue
}
commentsTree.tsModified, commentsTree.tsCreated = tsModified, tsCreated
if commentsTree.tsCreated.Before(res.Info.FirstTS) {
res.Info.FirstTS = commentsTree.tsCreated
}
if commentsTree.tsModified.After(res.Info.LastTS) {
res.Info.LastTS = commentsTree.tsModified
}
res.Info.ReadOnly = readOnlyAge > 0 && !res.Info.FirstTS.IsZero() &&
res.Info.FirstTS.AddDate(0, 0, readOnlyAge).Before(time.Now())
res.Nodes = append(res.Nodes, commentsTree)
}
res.sortNodes(sortType)
return &res
}
// proc makes tree for one top-level comment recursively
func (t *Tree) proc(comments []store.Comment, node *Node, rd *recurData, parentID string) (*Node, time.Time, time.Time) {
if rd.tsModified.IsZero() || rd.tsCreated.IsZero() {
rd.tsModified, rd.tsCreated = node.Comment.Timestamp, node.Comment.Timestamp
}
repComments := t.filter(comments, func(comment store.Comment) bool { return comment.ParentID == parentID })
for _, rc := range repComments {
if rc.Timestamp.After(rd.tsModified) {
rd.tsModified = rc.Timestamp
}
if rc.Timestamp.Before(rd.tsCreated) {
rd.tsCreated = rc.Timestamp
}
if !rc.Deleted {
rd.visible = true
}
rnode := &Node{Comment: rc, Replies: []*Node{}}
node.Replies = append(node.Replies, rnode)
t.proc(comments, rnode, rd, rc.ID)
}
// replies always sorted by time
sort.Slice(node.Replies, func(i, j int) bool {
return node.Replies[i].Comment.Timestamp.Before(node.Replies[j].Comment.Timestamp)
})
return node, rd.tsModified, rd.tsCreated
}
// filter returns comments for parentID
func (t *Tree) filter(comments []store.Comment, fn func(comment store.Comment) bool) (f []store.Comment) {
for _, c := range comments {
if fn(c) {
f = append(f, c)
}
}
return f
}
// sort list of nodes, i.e. top-level comments
// time sort uses tsModified from latest reply
func (t *Tree) sortNodes(sortType string) {
sort.Slice(t.Nodes, func(i, j int) bool {
switch sortType {
case "+time", "-time", "time":
if strings.HasPrefix(sortType, "-") {
return t.Nodes[i].Comment.Timestamp.After(t.Nodes[j].Comment.Timestamp)
}
return t.Nodes[i].Comment.Timestamp.Before(t.Nodes[j].Comment.Timestamp)
case "+active", "-active", "active":
if strings.HasPrefix(sortType, "-") {
return t.Nodes[i].tsModified.After(t.Nodes[j].tsModified)
}
return t.Nodes[i].tsModified.Before(t.Nodes[j].tsModified)
case "+score", "-score", "score":
if strings.HasPrefix(sortType, "-") {
if t.Nodes[i].Comment.Score == t.Nodes[j].Comment.Score {
return t.Nodes[i].Comment.Timestamp.Before(t.Nodes[j].Comment.Timestamp)
}
return t.Nodes[i].Comment.Score > t.Nodes[j].Comment.Score
}
if t.Nodes[i].Comment.Score == t.Nodes[j].Comment.Score {
return t.Nodes[i].Comment.Timestamp.Before(t.Nodes[j].Comment.Timestamp)
}
return t.Nodes[i].Comment.Score < t.Nodes[j].Comment.Score
default:
return t.Nodes[i].Comment.Timestamp.Before(t.Nodes[j].Comment.Timestamp)
}
})
}
-396
View File
@@ -1,396 +0,0 @@
package rest
import (
"bytes"
"encoding/json"
"io/ioutil"
"regexp"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/umputun/remark/app/store"
)
func TestMakeTree(t *testing.T) {
loc := store.Locator{URL: "url", SiteID: "site"}
ts := func(min int, sec int) time.Time { return time.Date(2017, 12, 25, 19, min, sec, 0, time.UTC) }
// unsorted by purpose
comments := []store.Comment{
{Locator: loc, ID: "14", ParentID: "1", Timestamp: ts(46, 14)},
{Locator: loc, ID: "1", Timestamp: ts(46, 1)},
{Locator: loc, ID: "2", Timestamp: ts(47, 2)},
{Locator: loc, ID: "11", ParentID: "1", Timestamp: ts(46, 11)},
{Locator: loc, ID: "13", ParentID: "1", Timestamp: ts(46, 13)},
{Locator: loc, ID: "12", ParentID: "1", Timestamp: ts(46, 12)},
{Locator: loc, ID: "131", ParentID: "13", Timestamp: ts(46, 31)},
{Locator: loc, ID: "132", ParentID: "13", Timestamp: ts(46, 32)},
{Locator: loc, ID: "21", ParentID: "2", Timestamp: ts(47, 21)},
{Locator: loc, ID: "22", ParentID: "2", Timestamp: ts(47, 22)},
{Locator: loc, ID: "4", Timestamp: ts(47, 22)},
{Locator: loc, ID: "3", Timestamp: ts(47, 22)},
{Locator: loc, ID: "5", Deleted: true},
{Locator: loc, ID: "6", Deleted: true},
{Locator: loc, ID: "61", ParentID: "6", Deleted: true},
{Locator: loc, ID: "62", ParentID: "6", Deleted: true},
{Locator: loc, ID: "611", ParentID: "61", Deleted: true},
}
res := MakeTree(comments, "time", 0)
buf := &bytes.Buffer{}
enc := json.NewEncoder(buf)
enc.SetIndent("", " ")
err := enc.Encode(res)
assert.Nil(t, err)
expected, actual := cleanFormatting(expJSON, buf.String())
assert.Equal(t, expected, actual)
assert.Equal(t, store.PostInfo{URL: "url", Count: 12, FirstTS: ts(46, 1), LastTS: ts(47, 22)}, res.Info)
res = MakeTree([]store.Comment{}, "time", 0)
assert.Equal(t, &Tree{}, res)
res = MakeTree(comments, "time", 10)
assert.Equal(t, store.PostInfo{URL: "url", Count: 12, FirstTS: ts(46, 1), LastTS: ts(47, 22), ReadOnly: true}, res.Info)
}
func TestTreeSortNodes(t *testing.T) {
// unsorted by purpose
comments := []store.Comment{
{ID: "14", ParentID: "1", Timestamp: time.Date(2017, 12, 25, 19, 46, 14, 0, time.UTC)},
{ID: "132", ParentID: "13", Timestamp: time.Date(2017, 12, 25, 19, 46, 32, 0, time.UTC)},
{ID: "1", Timestamp: time.Date(2017, 12, 25, 19, 46, 1, 0, time.UTC), Score: 2},
{ID: "2", Timestamp: time.Date(2017, 12, 25, 19, 47, 2, 0, time.UTC), Score: 3},
{ID: "11", ParentID: "1", Timestamp: time.Date(2017, 12, 25, 19, 46, 11, 0, time.UTC)},
{ID: "13", ParentID: "1", Timestamp: time.Date(2017, 12, 25, 19, 46, 13, 0, time.UTC)},
{ID: "12", ParentID: "1", Timestamp: time.Date(2017, 12, 25, 19, 46, 14, 0, time.UTC)},
{ID: "131", ParentID: "13", Timestamp: time.Date(2017, 12, 25, 19, 50, 31, 0, time.UTC)},
{ID: "21", ParentID: "2", Timestamp: time.Date(2017, 12, 25, 19, 47, 21, 0, time.UTC)},
{ID: "22", ParentID: "2", Timestamp: time.Date(2017, 12, 25, 19, 47, 22, 0, time.UTC)},
{ID: "4", Timestamp: time.Date(2017, 12, 25, 19, 47, 22, 0, time.UTC), Score: -2},
{ID: "3", Timestamp: time.Date(2017, 12, 25, 19, 47, 22, 100, time.UTC)},
{ID: "6", Timestamp: time.Date(2017, 12, 25, 19, 47, 22, 200, time.UTC)},
{ID: "5", Deleted: true, Timestamp: time.Date(2017, 12, 25, 19, 47, 22, 150, time.UTC)},
}
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", 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", 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", 0)
assert.Equal(t, "6", res.Nodes[0].Comment.ID)
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", 0)
assert.Equal(t, "4", res.Nodes[0].Comment.ID)
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, "undefined", 0)
t.Log(res.Nodes[0].Comment.ID, res.Nodes[0].tsModified)
assert.Equal(t, "1", res.Nodes[0].Comment.ID)
}
func BenchmarkTree(b *testing.B) {
comments := []store.Comment{}
data, err := ioutil.ReadFile("testfile.json")
assert.Nil(b, err)
err = json.Unmarshal(data, &comments)
assert.Nil(b, err)
for i := 0; i < b.N; i++ {
res := MakeTree(comments, "time", 0)
assert.NotNil(b, res)
}
}
const expJSON = `{
"comments": [
{
"comment": {
"id": "1",
"pid": "",
"text": "",
"user": {
"name": "",
"id": "",
"picture": "",
"admin": false
},
"locator": {
"site": "site",
"url": "url"
},
"score": 0,
"votes": null,
"time": "2017-12-25T19:46:01Z"
},
"replies": [
{
"comment": {
"id": "11",
"pid": "1",
"text": "",
"user": {
"name": "",
"id": "",
"picture": "",
"admin": false
},
"locator": {
"site": "site",
"url": "url"
},
"score": 0,
"votes": null,
"time": "2017-12-25T19:46:11Z"
}
},
{
"comment": {
"id": "12",
"pid": "1",
"text": "",
"user": {
"name": "",
"id": "",
"picture": "",
"admin": false
},
"locator": {
"site": "site",
"url": "url"
},
"score": 0,
"votes": null,
"time": "2017-12-25T19:46:12Z"
}
},
{
"comment": {
"id": "13",
"pid": "1",
"text": "",
"user": {
"name": "",
"id": "",
"picture": "",
"admin": false
},
"locator": {
"site": "site",
"url": "url"
},
"score": 0,
"votes": null,
"time": "2017-12-25T19:46:13Z"
},
"replies": [
{
"comment": {
"id": "131",
"pid": "13",
"text": "",
"user": {
"name": "",
"id": "",
"picture": "",
"admin": false
},
"locator": {
"site": "site",
"url": "url"
},
"score": 0,
"votes": null,
"time": "2017-12-25T19:46:31Z"
}
},
{
"comment": {
"id": "132",
"pid": "13",
"text": "",
"user": {
"name": "",
"id": "",
"picture": "",
"admin": false
},
"locator": {
"site": "site",
"url": "url"
},
"score": 0,
"votes": null,
"time": "2017-12-25T19:46:32Z"
}
}
]
},
{
"comment": {
"id": "14",
"pid": "1",
"text": "",
"user": {
"name": "",
"id": "",
"picture": "",
"admin": false
},
"locator": {
"site": "site",
"url": "url"
},
"score": 0,
"votes": null,
"time": "2017-12-25T19:46:14Z"
}
}
]
},
{
"comment": {
"id": "2",
"pid": "",
"text": "",
"user": {
"name": "",
"id": "",
"picture": "",
"admin": false
},
"locator": {
"site": "site",
"url": "url"
},
"score": 0,
"votes": null,
"time": "2017-12-25T19:47:02Z"
},
"replies": [
{
"comment": {
"id": "21",
"pid": "2",
"text": "",
"user": {
"name": "",
"id": "",
"picture": "",
"admin": false
},
"locator": {
"site": "site",
"url": "url"
},
"score": 0,
"votes": null,
"time": "2017-12-25T19:47:21Z"
}
},
{
"comment": {
"id": "22",
"pid": "2",
"text": "",
"user": {
"name": "",
"id": "",
"picture": "",
"admin": false
},
"locator": {
"site": "site",
"url": "url"
},
"score": 0,
"votes": null,
"time": "2017-12-25T19:47:22Z"
}
}
]
},
{
"comment": {
"id": "4",
"pid": "",
"text": "",
"user": {
"name": "",
"id": "",
"picture": "",
"admin": false
},
"locator": {
"site": "site",
"url": "url"
},
"score": 0,
"votes": null,
"time": "2017-12-25T19:47:22Z"
}
},
{
"comment": {
"id": "3",
"pid": "",
"text": "",
"user": {
"name": "",
"id": "",
"picture": "",
"admin": false
},
"locator": {
"site": "site",
"url": "url"
},
"score": 0,
"votes": null,
"time": "2017-12-25T19:47:22Z"
}
}
],
"info": {
"url": "url",
"count": 12,
"first_time": "2017-12-25T19:46:01Z",
"last_time": "2017-12-25T19:47:22Z"
}
}
`
func cleanFormatting(expected, actual string) (string, string) {
reSpaces := regexp.MustCompile(`[\s\p{Zs}]{2,}`)
expected = strings.Replace(expected, "\n", " ", -1)
expected = strings.Replace(expected, "\t", " ", -1)
expected = reSpaces.ReplaceAllString(expected, " ")
actual = strings.Replace(actual, "\n", " ", -1)
actual = reSpaces.ReplaceAllString(actual, " ")
return expected, actual
}
-32
View File
@@ -1,32 +0,0 @@
package rest
import (
"context"
"errors"
"net/http"
"github.com/umputun/remark/app/store"
)
type contextKey string
// GetUserInfo returns user from request context
func GetUserInfo(r *http.Request) (user store.User, err error) {
ctx := r.Context()
if ctx == nil {
return store.User{}, errors.New("no info about user")
}
if u, ok := ctx.Value(contextKey("user")).(store.User); ok {
return u, nil
}
return store.User{}, errors.New("user can't be parsed")
}
// SetUserInfo sets user into request context
func SetUserInfo(r *http.Request, user store.User) *http.Request {
ctx := r.Context()
ctx = context.WithValue(ctx, contextKey("user"), user)
return r.WithContext(ctx)
}
-21
View File
@@ -1,21 +0,0 @@
package rest
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/umputun/remark/app/store"
)
func TestGetUserInfo(t *testing.T) {
r, err := http.NewRequest("GET", "http://blah.com", nil)
assert.Nil(t, err)
_, err = GetUserInfo(r)
assert.NotNil(t, err, "no user info")
r = SetUserInfo(r, store.User{Name: "test", ID: "id"})
u, err := GetUserInfo(r)
assert.Nil(t, err)
assert.Equal(t, store.User{Name: "test", ID: "id"}, u)
}
-103
View File
@@ -1,103 +0,0 @@
package store
import (
"html/template"
"regexp"
"time"
"github.com/microcosm-cc/bluemonday"
)
// Comment represents a single comment with optional reference to its parent
type Comment struct {
ID string `json:"id"`
ParentID string `json:"pid"`
Text string `json:"text"`
Orig string `json:"orig,omitempty"`
User User `json:"user"`
Locator Locator `json:"locator"`
Score int `json:"score"`
Votes map[string]bool `json:"votes"`
Timestamp time.Time `json:"time"`
Edit *Edit `json:"edit,omitempty"` // pointer to have empty default in json response
Pin bool `json:"pin,omitempty"`
Deleted bool `json:"delete,omitempty"`
}
// Locator keeps site and url of the post
type Locator struct {
SiteID string `json:"site,omitempty"`
URL string `json:"url"`
}
// Edit indication
type Edit struct {
Timestamp time.Time `json:"time"`
Summary string `json:"summary"`
}
// PostInfo holds summary for given post url
type PostInfo struct {
URL string `json:"url"`
Count int `json:"count"`
ReadOnly bool `json:"read_only,omitempty"`
FirstTS time.Time `json:"first_time,omitempty"`
LastTS time.Time `json:"last_time,omitempty"`
}
// BlockedUser holds id and ts for blocked user
type BlockedUser struct {
ID string `json:"id"`
Name string `json:"name"`
Timestamp time.Time `json:"time"`
}
// DeleteMode defines how much comment info will be erased
type DeleteMode int
// DeleteMode enum
const (
SoftDelete DeleteMode = 0
HardDelete DeleteMode = 1
)
// PrepareUntrusted pre-processes a comment received from untrusted source by clearing all
// autogen fields and reset everything users not supposed to provide
func (c *Comment) PrepareUntrusted() {
c.ID = "" // don't allow user to define ID, force auto-gen
c.Timestamp = time.Time{} // reset time, force auto-gen
c.Votes = make(map[string]bool)
c.Score = 0
c.Edit = nil
c.Pin = false
c.Deleted = false
}
// SetDeleted clears comment info, reset to deleted state. hard flag will clear all user info as well
func (c *Comment) SetDeleted(mode DeleteMode) {
c.Text = ""
c.Orig = ""
c.Score = 0
c.Votes = map[string]bool{}
c.Edit = nil
c.Deleted = true
c.Pin = false
if mode == HardDelete {
c.User.Name = "deleted"
c.User.ID = "deleted"
c.User.Picture = ""
c.User.IP = ""
}
}
// Sanitize clean dangerous html/js from the comment
func (c *Comment) Sanitize() {
p := bluemonday.UGCPolicy()
p.AllowAttrs("class").Matching(regexp.MustCompile("^language-[a-zA-Z0-9]+$")).OnElements("code")
c.Text = p.Sanitize(c.Text)
c.Orig = p.Sanitize(c.Orig)
c.User.ID = template.HTMLEscapeString(c.User.ID)
c.User.Name = template.HTMLEscapeString(c.User.Name)
c.User.Picture = p.Sanitize(c.User.Picture)
}
-112
View File
@@ -1,112 +0,0 @@
package store
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestComment_Sanitize(t *testing.T) {
tbl := []struct {
inp Comment
out Comment
}{
{inp: Comment{}, out: Comment{}},
{
inp: Comment{
Text: `blah <a href="javascript:alert('XSS1')" onmouseover="alert('XSS2')">XSS<a>` + "\n\t",
User: User{ID: `<a href="http://blah.com">username</a>`},
},
out: Comment{
Text: "blah XSS\n\t",
User: User{ID: `&lt;a href=&#34;http://blah.com&#34;&gt;username&lt;/a&gt;`},
},
},
}
for n, tt := range tbl {
tt.inp.Sanitize()
assert.Equal(t, tt.out, tt.inp, "check #%d", n)
}
}
func TestComment_PrepareUntrusted(t *testing.T) {
comment := Comment{
Text: `blah`,
User: User{ID: "username"},
ParentID: "p123",
ID: "123",
Locator: Locator{SiteID: "site", URL: "url"},
Score: 10,
Pin: true,
Deleted: true,
Timestamp: time.Date(2018, 1, 1, 9, 30, 0, 0, time.Local),
Votes: map[string]bool{"uu": true},
}
comment.PrepareUntrusted()
assert.Equal(t, "", comment.ID)
assert.Equal(t, "p123", comment.ParentID)
assert.Equal(t, "blah", comment.Text)
assert.Equal(t, 0, comment.Score)
assert.Equal(t, false, comment.Pin)
assert.Equal(t, time.Time{}, comment.Timestamp)
assert.Equal(t, false, comment.Deleted)
assert.Equal(t, make(map[string]bool), comment.Votes)
assert.Equal(t, User{ID: "username"}, comment.User)
}
func TestComment_SetDeleted(t *testing.T) {
comment := Comment{
Text: `blah`,
User: User{ID: "userid", Name: "username", IP: "123", Picture: "pic"},
ParentID: "p123",
ID: "123",
Locator: Locator{SiteID: "site", URL: "url"},
Score: 10,
Deleted: false,
Timestamp: time.Date(2018, 1, 1, 9, 30, 0, 0, time.Local),
Votes: map[string]bool{"uu": true},
Pin: true,
}
comment.SetDeleted(SoftDelete)
assert.Equal(t, "", comment.Text)
assert.Equal(t, "", comment.Orig)
assert.Equal(t, map[string]bool{}, comment.Votes)
assert.Equal(t, 0, comment.Score)
assert.True(t, comment.Deleted)
assert.Nil(t, comment.Edit)
assert.False(t, comment.Pin)
assert.Equal(t, User{Name: "username", ID: "userid", Picture: "pic", Admin: false, Blocked: false, IP: "123"}, comment.User)
}
func TestComment_SetDeletedHard(t *testing.T) {
comment := Comment{
Text: `blah`,
User: User{ID: "userid", Name: "username", IP: "123", Picture: "pic"},
ParentID: "p123",
ID: "123",
Locator: Locator{SiteID: "site", URL: "url"},
Score: 10,
Deleted: false,
Timestamp: time.Date(2018, 1, 1, 9, 30, 0, 0, time.Local),
Votes: map[string]bool{"uu": true},
Pin: true,
}
comment.SetDeleted(HardDelete)
assert.Equal(t, "", comment.Text)
assert.Equal(t, "", comment.Orig)
assert.Equal(t, map[string]bool{}, comment.Votes)
assert.Equal(t, 0, comment.Score)
assert.True(t, comment.Deleted)
assert.Nil(t, comment.Edit)
assert.False(t, comment.Pin)
assert.Equal(t, User{Name: "deleted", ID: "deleted", Picture: "", Admin: false, Blocked: false, IP: ""}, comment.User)
}
-530
View File
@@ -1,530 +0,0 @@
package engine
import (
"encoding/json"
"fmt"
"log"
"strings"
"time"
"github.com/coreos/bbolt"
"github.com/pkg/errors"
"github.com/umputun/remark/app/store"
)
// BoltDB implements store.Interface, represents multiple sites with multiplexing to different bolt dbs. Thread safe.
// there are 5 types of top-level buckets:
// - comments for post in "posts" top-level bucket. Each url (post) makes its own bucket and each k:v pair is commentID:comment
// - history of all comments. They all in a single "last" bucket (per site) and key is defined by ref struct as ts+commentID
// value is not full comment but a reference combined from post-url+commentID
// - user to comment references in "users" bucket. It used to get comments for user. Key is userID and value
// is a nested bucket named userID with kv as ts:reference
// - blocking info sits in "block" bucket. Key is userID, value - ts
// - counts per post to keep number of comments. Key is post url, value - count
// - readonly per post to keep status of manually set RO posts. Key is post url, value - ts
type BoltDB struct {
dbs map[string]*bolt.DB
}
const (
// top level buckets
postsBucketName = "posts"
lastBucketName = "last"
userBucketName = "users"
blocksBucketName = "block"
infoBucketName = "info"
readonlyBucketName = "readonly"
verifiedBucketName = "verified"
// limits
lastLimit = 1000
userLimit = 500
)
const tsNano = "2006-01-02T15:04:05.000000000Z07:00"
// BoltSite defines single site param
type BoltSite struct {
FileName string // full path to boltdb
SiteID string // ID to access given site
}
// NewBoltDB makes persistent boltdb-based store
func NewBoltDB(options bolt.Options, sites ...BoltSite) (*BoltDB, error) {
log.Printf("[INFO] bolt store for sites %+v", sites)
result := BoltDB{dbs: make(map[string]*bolt.DB)}
for _, site := range sites {
db, err := bolt.Open(site.FileName, 0600, &options) // bolt.Options{Timeout: 30 * time.Second}
if err != nil {
return nil, errors.Wrapf(err, "failed to make boltdb for %s", site.FileName)
}
// make top-level buckets
topBuckets := []string{postsBucketName, lastBucketName, userBucketName, blocksBucketName,
infoBucketName, readonlyBucketName, verifiedBucketName}
err = db.Update(func(tx *bolt.Tx) error {
for _, bktName := range topBuckets {
if _, e := tx.CreateBucketIfNotExists([]byte(bktName)); e != nil {
return errors.Wrapf(err, "failed to create top level bucket %s", bktName)
}
}
return nil
})
if err != nil {
return nil, errors.Wrap(err, "failed to create top level bucket)")
}
result.dbs[site.SiteID] = db
}
return &result, nil
}
// Create saves new comment to store. Adds to posts bucket, reference to last and user bucket and increments count bucket
func (b *BoltDB) Create(comment store.Comment) (commentID string, err error) {
bdb, err := b.db(comment.Locator.SiteID)
if err != nil {
return "", err
}
if b.IsReadOnly(comment.Locator) {
return "", errors.Errorf("post %s is read-only", comment.Locator.URL)
}
err = bdb.Update(func(tx *bolt.Tx) error {
postBkt, e := b.makePostBucket(tx, comment.Locator.URL)
if e != nil {
return e
}
// check if key already in store, reject doubles
if postBkt.Get([]byte(comment.ID)) != nil {
return errors.Errorf("key %s already in store", comment.ID)
}
// serialize comment to json []byte for bolt and save
if e = b.save(postBkt, []byte(comment.ID), comment); e != nil {
return errors.Wrapf(e, "failed to put key %s to bucket %s", comment.ID, comment.Locator.URL)
}
ref := b.makeRef(comment)
// add reference to comment to "last" bucket
lastBkt := tx.Bucket([]byte(lastBucketName))
commentTs := []byte(comment.Timestamp.Format(tsNano))
e = lastBkt.Put(commentTs, ref)
if e != nil {
return errors.Wrapf(e, "can't put reference %s to %s", ref, lastBucketName)
}
// add reference to commentID to "users" bucket
userBkt, e := b.getUserBucket(tx, comment.User.ID)
if e != nil {
return errors.Wrapf(e, "can't get bucket %s", comment.User.ID)
}
// put into individual user's bucket with ts as a key
if e = userBkt.Put(commentTs, ref); e != nil {
return errors.Wrapf(e, "failed to put user comment %s for %s", comment.ID, comment.User.ID)
}
// set info with countfor post url
if _, e = b.setInfo(tx, comment); e != nil {
return errors.Wrapf(e, "failed to set info for %s", comment.Locator)
}
return nil
})
return comment.ID, err
}
// Find returns all comments for post and sorts results
func (b *BoltDB) Find(locator store.Locator, sortFld string) (comments []store.Comment, err error) {
comments = []store.Comment{}
bdb, err := b.db(locator.SiteID)
if err != nil {
return nil, err
}
err = bdb.View(func(tx *bolt.Tx) error {
bucket, e := b.getPostBucket(tx, locator.URL)
if e != nil {
return e
}
return bucket.ForEach(func(k, v []byte) error {
comment := store.Comment{}
if e := json.Unmarshal(v, &comment); e != nil {
return errors.Wrap(e, "failed to unmarshal")
}
comments = append(comments, comment)
return nil
})
})
comments = sortComments(comments, sortFld)
return comments, err
}
// Last returns up to max last comments for given siteID
func (b *BoltDB) Last(siteID string, max int) (comments []store.Comment, err error) {
if max > lastLimit || max == 0 {
max = lastLimit
}
bdb, err := b.db(siteID)
if err != nil {
return nil, err
}
err = bdb.View(func(tx *bolt.Tx) error {
lastBkt := tx.Bucket([]byte(lastBucketName))
c := lastBkt.Cursor()
for k, v := c.Last(); k != nil; k, v = c.Prev() {
url, commentID, e := b.parseRef(v)
if e != nil {
return e
}
postBkt, e := b.getPostBucket(tx, url)
if e != nil {
return e
}
comment := store.Comment{}
if e := b.load(postBkt, []byte(commentID), &comment); e != nil {
log.Printf("[WARN] can't load comment for %s from store %s", commentID, url)
continue
}
if comment.Deleted {
continue
}
comments = append(comments, comment)
if len(comments) >= max {
break
}
}
return nil
})
return comments, err
}
// Count returns number of comments for locator
func (b *BoltDB) Count(locator store.Locator) (count int, err error) {
bdb, err := b.db(locator.SiteID)
if err != nil {
return 0, err
}
err = bdb.View(func(tx *bolt.Tx) error {
var e error
count, e = b.count(tx, locator.URL, 0)
return e
})
return count, err
}
// List returns list of all commented posts with counters
// uses count bucket to get number of comments
func (b BoltDB) List(siteID string, limit, skip int) (list []store.PostInfo, err error) {
bdb, err := b.db(siteID)
if err != nil {
return nil, err
}
err = bdb.View(func(tx *bolt.Tx) error {
postsBkt := tx.Bucket([]byte(postsBucketName))
c := postsBkt.Cursor()
n := 0
for k, _ := c.Last(); k != nil; k, _ = c.Prev() {
n++
if skip > 0 && n <= skip {
continue
}
postURL := string(k)
infoBkt := tx.Bucket([]byte(infoBucketName))
info := store.PostInfo{}
if e := b.load(infoBkt, []byte(postURL), &info); e != nil {
return errors.Wrapf(e, "can't load info for %s", postURL)
}
list = append(list, info)
if limit > 0 && len(list) >= limit {
break
}
}
return nil
})
return list, err
}
// Info returns time range and count for locator
func (b *BoltDB) Info(locator store.Locator, readOnlyAge int) (store.PostInfo, error) {
bdb, err := b.db(locator.SiteID)
if err != nil {
return store.PostInfo{}, err
}
info := store.PostInfo{}
err = bdb.View(func(tx *bolt.Tx) error {
infoBkt := tx.Bucket([]byte(infoBucketName))
if e := b.load(infoBkt, []byte(locator.URL), &info); e != nil {
return errors.Wrapf(e, "can't load info for %s", locator.URL)
}
return nil
})
// set read-only from age and manual bucket
info.ReadOnly = readOnlyAge > 0 && !info.FirstTS.IsZero() && info.FirstTS.AddDate(0, 0, readOnlyAge).Before(time.Now())
if b.IsReadOnly(locator) {
info.ReadOnly = true
}
return info, err
}
// User extracts all comments for given site and given userID
// "users" bucket has sub-bucket for each userID, and keeps it as ts:ref
func (b *BoltDB) User(siteID, userID string, limit, skip int) (comments []store.Comment, err error) {
comments = []store.Comment{}
commentRefs := []string{}
bdb, err := b.db(siteID)
if err != nil {
return nil, err
}
if limit == 0 || limit > userLimit {
limit = userLimit
}
// get list of references to comments
err = bdb.View(func(tx *bolt.Tx) error {
usersBkt := tx.Bucket([]byte(userBucketName))
userIDBkt := usersBkt.Bucket([]byte(userID))
if userIDBkt == nil {
return errors.Errorf("no comments for user %s in store", userID)
}
c := userIDBkt.Cursor()
skipComments := 0
for k, v := c.Last(); k != nil; k, v = c.Prev() {
if len(commentRefs) >= limit {
break
}
if skip > 0 && skipComments < skip {
skipComments++
continue
}
commentRefs = append(commentRefs, string(v))
}
return nil
})
if err != nil {
return comments, err
}
// retrieve comments for refs
for _, v := range commentRefs {
url, commentID, e := b.parseRef([]byte(v))
if e != nil {
return comments, errors.Wrapf(e, "can't parse reference %s", v)
}
if c, e := b.Get(store.Locator{SiteID: siteID, URL: url}, commentID); e == nil {
comments = append(comments, c)
}
}
return comments, err
}
// UserCount returns number of comments for user
func (b *BoltDB) UserCount(siteID, userID string) (int, error) {
bdb, err := b.db(siteID)
if err != nil {
return 0, err
}
count := 0
err = bdb.View(func(tx *bolt.Tx) error {
usersBkt := tx.Bucket([]byte(userBucketName))
userIDBkt := usersBkt.Bucket([]byte(userID))
if userIDBkt == nil {
return errors.Errorf("no comments for user %s in store", userID)
}
stats := userIDBkt.Stats()
count = stats.KeyN
return nil
})
return count, err
}
// Get returns comment for locator.URL and commentID string
func (b *BoltDB) Get(locator store.Locator, commentID string) (comment store.Comment, err error) {
bdb, err := b.db(locator.SiteID)
if err != nil {
return comment, err
}
err = bdb.View(func(tx *bolt.Tx) error {
bucket, e := b.getPostBucket(tx, locator.URL)
if e != nil {
return e
}
return b.load(bucket, []byte(commentID), &comment)
})
return comment, err
}
// Put updates comment for locator.URL with mutable part of comment
func (b *BoltDB) Put(locator store.Locator, comment store.Comment) error {
if curComment, err := b.Get(locator, comment.ID); err == nil {
// preserve immutable fields
comment.ParentID = curComment.ParentID
comment.Locator = curComment.Locator
comment.Timestamp = curComment.Timestamp
comment.User = curComment.User
}
bdb, err := b.db(locator.SiteID)
if err != nil {
return err
}
return bdb.Update(func(tx *bolt.Tx) error {
bucket, e := b.getPostBucket(tx, locator.URL)
if e != nil {
return e
}
return b.save(bucket, []byte(comment.ID), comment)
})
}
// getPostBucket return bucket with all comments for postURL
func (b *BoltDB) getPostBucket(tx *bolt.Tx, postURL string) (*bolt.Bucket, error) {
postsBkt := tx.Bucket([]byte(postsBucketName))
if postsBkt == nil {
return nil, errors.Errorf("no bucket %s", postsBucketName)
}
res := postsBkt.Bucket([]byte(postURL))
if res == nil {
return nil, errors.Errorf("no bucket %s in store", postURL)
}
return res, nil
}
// makePostBucket create new bucket for postURL as a key. This bucket holds all comments for the post.
func (b *BoltDB) makePostBucket(tx *bolt.Tx, postURL string) (*bolt.Bucket, error) {
postsBkt := tx.Bucket([]byte(postsBucketName))
if postsBkt == nil {
return nil, errors.Errorf("no bucket %s", postsBucketName)
}
res, err := postsBkt.CreateBucketIfNotExists([]byte(postURL))
if err != nil {
return nil, errors.Wrapf(err, "no bucket %s in store", postURL)
}
return res, nil
}
func (b *BoltDB) getUserBucket(tx *bolt.Tx, userID string) (*bolt.Bucket, error) {
usersBkt := tx.Bucket([]byte(userBucketName))
userIDBkt, e := usersBkt.CreateBucketIfNotExists([]byte(userID)) // get bucket for userID
if e != nil {
return nil, errors.Wrapf(e, "can't get bucket %s", userID)
}
return userIDBkt, nil
}
// save marshaled value to key for bucket. Should run in update tx
func (b *BoltDB) save(bkt *bolt.Bucket, key []byte, value interface{}) (err error) {
if value == nil {
return errors.Errorf("can't save nil value for %s", key)
}
jdata, jerr := json.Marshal(value)
if jerr != nil {
return errors.Wrap(jerr, "can't marshal comment")
}
if err = bkt.Put(key, jdata); err != nil {
return errors.Wrapf(err, "failed to save key %s", key)
}
return nil
}
// load and unmarshal json value by key from bucket. Should run in view tx
func (b *BoltDB) load(bkt *bolt.Bucket, key []byte, res interface{}) error {
value := bkt.Get(key)
if value == nil {
return errors.Errorf("no value for %s", key)
}
if err := json.Unmarshal(value, &res); err != nil {
return errors.Wrap(err, "failed to unmarshal")
}
return nil
}
// count adds val to counts key postURL. val can be negative to subtract. if val 0 can be used as accessor
// it uses separate counts bucket because boltdb Stat call is very slow
func (b *BoltDB) count(tx *bolt.Tx, postURL string, val int) (int, error) {
infoBkt := tx.Bucket([]byte(infoBucketName))
info := store.PostInfo{}
if err := b.load(infoBkt, []byte(postURL), &info); err != nil {
info = store.PostInfo{}
}
if val == 0 { // get current count, don't update
return info.Count, nil
}
info.Count += val
return info.Count, b.save(infoBkt, []byte(postURL), &info)
}
func (b *BoltDB) setInfo(tx *bolt.Tx, comment store.Comment) (store.PostInfo, error) {
infoBkt := tx.Bucket([]byte(infoBucketName))
info := store.PostInfo{}
if err := b.load(infoBkt, []byte(comment.Locator.URL), &info); err != nil {
info = store.PostInfo{
Count: 0,
URL: comment.Locator.URL,
FirstTS: comment.Timestamp,
LastTS: comment.Timestamp,
}
}
info.Count++
info.LastTS = comment.Timestamp
return info, b.save(infoBkt, []byte(comment.Locator.URL), &info)
}
func (b *BoltDB) db(siteID string) (*bolt.DB, error) {
if res, ok := b.dbs[siteID]; ok {
return res, nil
}
return nil, errors.Errorf("site %q not found", siteID)
}
// makeRef creates reference combining url and comment id
func (b *BoltDB) makeRef(comment store.Comment) []byte {
return []byte(fmt.Sprintf("%s!!%s", comment.Locator.URL, comment.ID))
}
// parseRef gets parts of reference
func (b *BoltDB) parseRef(val []byte) (url string, id string, err error) {
elems := strings.Split(string(val), "!!")
if len(elems) != 2 {
return "", "", errors.Errorf("invalid reference value %s", string(val))
}
return elems[0], elems[1], nil
}
-372
View File
@@ -1,372 +0,0 @@
package engine
import (
"fmt"
"os"
"testing"
"time"
"github.com/coreos/bbolt"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/umputun/remark/app/store"
)
var testDb = "test-remark.db"
func TestBoltDB_CreateAndFind(t *testing.T) {
defer os.Remove(testDb)
var b = prep(t)
res, err := b.Find(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "time")
assert.Nil(t, err)
assert.Equal(t, 2, len(res))
assert.Equal(t, `some text, <a href="http://radio-t.com">link</a>`, res[0].Text)
assert.Equal(t, "user1", res[0].User.ID)
t.Log(res[0].ID)
_, err = b.Create(store.Comment{ID: res[0].ID, Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}})
assert.NotNil(t, err)
assert.Equal(t, "key id-1 already in store", err.Error())
_, err = b.Find(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t-bad"}, "time")
assert.EqualError(t, err, `site "radio-t-bad" not found`)
}
func TestBoltDB_CreateReadOnly(t *testing.T) {
defer os.Remove(testDb)
var b = prep(t)
comment := store.Comment{
ID: "id-ro",
Text: `some text, <a href="http://radio-t.com">link</a>`,
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
Locator: store.Locator{URL: "https://radio-t.com/ro", SiteID: "radio-t"},
User: store.User{ID: "user1", Name: "user name"},
}
err := b.SetReadOnly(comment.Locator, true)
require.Nil(t, err)
_, err = b.Create(comment)
assert.NotNil(t, err)
assert.Equal(t, "post https://radio-t.com/ro is read-only", err.Error())
err = b.SetReadOnly(comment.Locator, false)
require.Nil(t, err)
_, err = b.Create(comment)
assert.Nil(t, err)
}
func TestBoltDB_Get(t *testing.T) {
defer os.Remove(testDb)
b := prep(t)
res, err := b.Find(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "time")
assert.Nil(t, err)
assert.Equal(t, 2, len(res))
comment, err := b.Get(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[1].ID)
assert.Nil(t, err)
assert.Equal(t, "some text2", comment.Text)
comment, err = b.Get(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "1234567")
assert.NotNil(t, err)
_, err = b.Get(store.Locator{URL: "https://radio-t.com", SiteID: "bad"}, res[1].ID)
assert.EqualError(t, err, `site "bad" not found`)
}
func TestBoltDB_Put(t *testing.T) {
defer os.Remove(testDb)
b := prep(t)
loc := store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}
res, err := b.Find(loc, "time")
assert.Nil(t, err)
assert.Equal(t, 2, len(res))
comment := res[0]
comment.Text = "abc 123"
comment.Score = 100
err = b.Put(loc, comment)
assert.Nil(t, err)
comment, err = b.Get(loc, res[0].ID)
assert.Nil(t, err)
assert.Equal(t, "abc 123", comment.Text)
assert.Equal(t, res[0].ID, comment.ID)
assert.Equal(t, 100, comment.Score)
err = b.Put(store.Locator{URL: "https://radio-t.com", SiteID: "bad"}, comment)
assert.EqualError(t, err, `site "bad" not found`)
err = b.Put(store.Locator{URL: "https://radio-t.com-bad", SiteID: "radio-t"}, comment)
assert.EqualError(t, err, `no bucket https://radio-t.com-bad in store`)
}
func TestBoltDB_Last(t *testing.T) {
defer os.Remove(testDb)
b := prep(t)
res, err := b.Last("radio-t", 0)
assert.Nil(t, err)
assert.Equal(t, 2, len(res))
assert.Equal(t, "some text2", res[0].Text)
res, err = b.Last("radio-t", 1)
assert.Nil(t, err)
assert.Equal(t, 1, len(res))
assert.Equal(t, "some text2", res[0].Text)
_, err = b.Last("bad", 0)
assert.EqualError(t, err, `site "bad" not found`)
}
func TestBoltDB_Count(t *testing.T) {
defer os.Remove(testDb)
b := prep(t)
c, err := b.Count(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"})
assert.Nil(t, err)
assert.Equal(t, 2, c)
c, err = b.Count(store.Locator{URL: "https://radio-t.com-xxx", SiteID: "radio-t"})
assert.Nil(t, err)
assert.Equal(t, 0, c)
_, err = b.Count(store.Locator{URL: "https://radio-t.com", SiteID: "bad"})
assert.EqualError(t, err, `site "bad" not found`)
}
func TestBoltDB_List(t *testing.T) {
defer os.Remove(testDb)
b := prep(t) // two comments for https://radio-t.com
// add one more for https://radio-t.com/2
comment := store.Comment{
ID: "12345",
Text: `some text, <a href="http://radio-t.com">link</a>`,
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"},
User: store.User{ID: "user1", Name: "user name"},
}
_, err := b.Create(comment)
assert.Nil(t, err)
ts := func(sec int) time.Time { return time.Date(2017, 12, 20, 15, 18, sec, 0, time.Local) }
res, err := b.List("radio-t", 0, 0)
assert.Nil(t, err)
assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com/2", Count: 1, FirstTS: ts(22), LastTS: ts(22)},
{URL: "https://radio-t.com", Count: 2, FirstTS: ts(22), LastTS: ts(23)}},
res)
res, err = b.List("radio-t", -1, -1)
assert.Nil(t, err)
assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com/2", Count: 1, FirstTS: ts(22), LastTS: ts(22)},
{URL: "https://radio-t.com", Count: 2, FirstTS: ts(22), LastTS: ts(23)}}, res)
res, err = b.List("radio-t", 1, 0)
assert.Nil(t, err)
assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com/2", Count: 1, FirstTS: ts(22), LastTS: ts(22)}}, res)
res, err = b.List("radio-t", 1, 1)
assert.Nil(t, err)
assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com", Count: 2, FirstTS: ts(22), LastTS: ts(23)}}, res)
_, err = b.List("bad", 1, 1)
assert.EqualError(t, err, `site "bad" not found`)
}
func TestBoltDB_Info(t *testing.T) {
defer os.Remove(testDb)
b := prep(t) // two comments for https://radio-t.com
ts := func(min int) time.Time { return time.Date(2017, 12, 20, 15, 18, min, 0, time.Local) }
// add one more for https://radio-t.com/2
comment := store.Comment{
ID: "12345",
Text: `some text, <a href="http://radio-t.com">link</a>`,
Timestamp: time.Date(2017, 12, 20, 15, 18, 24, 0, time.Local),
Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"},
User: store.User{ID: "user1", Name: "user name"},
}
_, err := b.Create(comment)
assert.Nil(t, err)
r, err := b.Info(store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"}, 0)
require.Nil(t, err)
assert.Equal(t, store.PostInfo{URL: "https://radio-t.com/2", Count: 1, FirstTS: ts(24), LastTS: ts(24)}, r)
r, err = b.Info(store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"}, 10)
require.Nil(t, err)
assert.Equal(t, store.PostInfo{URL: "https://radio-t.com/2", Count: 1, FirstTS: ts(24), LastTS: ts(24), ReadOnly: true}, r)
r, err = b.Info(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, 0)
require.Nil(t, err)
assert.Equal(t, store.PostInfo{URL: "https://radio-t.com", Count: 2, FirstTS: ts(22), LastTS: ts(23)}, r)
_, err = b.Info(store.Locator{URL: "https://radio-t.com/error", SiteID: "radio-t"}, 0)
require.NotNil(t, err)
_, err = b.Info(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t-error"}, 0)
require.NotNil(t, err)
err = b.SetReadOnly(store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"}, true)
require.Nil(t, err)
r, err = b.Info(store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"}, 0)
require.Nil(t, err)
assert.Equal(t, store.PostInfo{URL: "https://radio-t.com/2", Count: 1, FirstTS: ts(24), LastTS: ts(24), ReadOnly: true}, r)
}
func TestBoltDB_GetForUser(t *testing.T) {
defer os.Remove(testDb)
b := prep(t)
res, err := b.User("radio-t", "user1", 5, 0)
assert.Nil(t, err)
assert.Equal(t, 2, len(res))
assert.Equal(t, "some text2", res[0].Text, "sorted by -time")
res, err = b.User("radio-t", "user1", 1, 0)
assert.Nil(t, err)
assert.Equal(t, 1, len(res), "allow 1 comment")
assert.Equal(t, "some text2", res[0].Text, "sorted by -time")
res, err = b.User("radio-t", "user1", 1, 1)
assert.Nil(t, err)
assert.Equal(t, 1, len(res), "allow 1 comment")
assert.Equal(t, `some text, <a href="http://radio-t.com">link</a>`, res[0].Text, "second comment")
_, err = b.User("bad", "user1", 1, 0)
assert.EqualError(t, err, `site "bad" not found`)
_, err = b.User("radio-t", "userZ", 1, 0)
assert.EqualError(t, err, `no comments for user userZ in store`)
}
func TestBoltDB_GetForUserPagination(t *testing.T) {
os.Remove(testDb)
b, err := NewBoltDB(bolt.Options{}, BoltSite{FileName: testDb, SiteID: "radio-t"})
require.Nil(t, err)
defer os.Remove(testDb)
c := store.Comment{
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
User: store.User{ID: "user1", Name: "user name"},
}
// write 200 comments
for i := 0; i < 200; i++ {
c.ID = fmt.Sprintf("id-%d", i)
c.Text = fmt.Sprintf("text #%d", i)
c.Timestamp = time.Date(2017, 12, 20, 15, 18, i, 0, time.Local)
_, err = b.Create(c)
require.Nil(t, err)
}
// get all comments
res, err := b.User("radio-t", "user1", 0, 0)
assert.Nil(t, err)
assert.Equal(t, 200, len(res))
assert.Equal(t, "id-199", res[0].ID)
// seek 0, 5 comments
res, err = b.User("radio-t", "user1", 5, 0)
assert.Nil(t, err)
assert.Equal(t, 5, len(res))
assert.Equal(t, "id-199", res[0].ID)
assert.Equal(t, "id-195", res[4].ID)
// seek 10, 3 comments
res, err = b.User("radio-t", "user1", 3, 10)
assert.Nil(t, err)
assert.Equal(t, 3, len(res))
assert.Equal(t, "id-189", res[0].ID)
assert.Equal(t, "id-187", res[2].ID)
// seek 195, ask 10 comments
res, err = b.User("radio-t", "user1", 10, 195)
assert.Nil(t, err)
assert.Equal(t, 5, len(res))
assert.Equal(t, "id-4", res[0].ID)
assert.Equal(t, "id-0", res[4].ID)
// seek 255, ask 10 comments
res, err = b.User("radio-t", "user1", 10, 255)
assert.Nil(t, err)
assert.Equal(t, 0, len(res))
}
func TestBoltDB_GetForUserCounter(t *testing.T) {
defer os.Remove(testDb)
b := prep(t)
count, err := b.UserCount("radio-t", "user1")
assert.Nil(t, err)
assert.Equal(t, 2, count)
_, err = b.UserCount("bad", "user1")
assert.EqualError(t, err, `site "bad" not found`)
_, err = b.UserCount("radio-t", "userZ")
assert.EqualError(t, err, `no comments for user userZ in store`)
}
func TestBoltDB_Ref(t *testing.T) {
b := BoltDB{}
comment := store.Comment{
ID: "12345",
Text: `some text, <a href="http://radio-t.com">link</a>`,
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"},
User: store.User{ID: "user1", Name: "user name"},
}
res := b.makeRef(comment)
assert.Equal(t, "https://radio-t.com/2!!12345", string(res))
url, id, err := b.parseRef([]byte("https://radio-t.com/2!!12345"))
assert.Nil(t, err)
assert.Equal(t, "https://radio-t.com/2", url)
assert.Equal(t, "12345", id)
_, _, err = b.parseRef([]byte("https://radio-t.com/2"))
assert.NotNil(t, err)
}
func TestBoltDB_New(t *testing.T) {
_, err := NewBoltDB(bolt.Options{}, BoltSite{FileName: "/tmp/no-such-place/tmp.db", SiteID: "radio-t"})
assert.EqualError(t, err, "failed to make boltdb for /tmp/no-such-place/tmp.db: open /tmp/no-such-place/tmp.db: no such file or directory")
}
// makes new boltdb, put two records
func prep(t *testing.T) *BoltDB {
os.Remove(testDb)
boltStore, err := NewBoltDB(bolt.Options{}, BoltSite{FileName: testDb, SiteID: "radio-t"})
assert.Nil(t, err)
b := boltStore
comment := store.Comment{
ID: "id-1",
Text: `some text, <a href="http://radio-t.com">link</a>`,
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
User: store.User{ID: "user1", Name: "user name"},
}
_, err = b.Create(comment)
assert.Nil(t, err)
comment = store.Comment{
ID: "id-2",
Text: "some text2",
Timestamp: time.Date(2017, 12, 20, 15, 18, 23, 0, time.Local),
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
User: store.User{ID: "user1", Name: "user name"},
}
_, err = b.Create(comment)
assert.Nil(t, err)
return b
}
-305
View File
@@ -1,305 +0,0 @@
package engine
import (
"encoding/json"
"log"
"time"
"github.com/coreos/bbolt"
"github.com/pkg/errors"
"github.com/umputun/remark/app/store"
)
// Delete removes comment, by locator from the store.
// Posts collection only sets status to deleted and clear fields in order to prevent breaking trees of replies.
// From last bucket removed for real.
func (b *BoltDB) Delete(locator store.Locator, commentID string, mode store.DeleteMode) error {
bdb, err := b.db(locator.SiteID)
if err != nil {
return err
}
return bdb.Update(func(tx *bolt.Tx) error {
postBkt, e := b.getPostBucket(tx, locator.URL)
if e != nil {
return e
}
comment := store.Comment{}
if err := b.load(postBkt, []byte(commentID), &comment); err != nil {
return errors.Wrapf(err, "can't load key %s from bucket %s", commentID, locator.URL)
}
// set deleted status and clear fields
comment.SetDeleted(mode)
if err := b.save(postBkt, []byte(commentID), comment); err != nil {
return errors.Wrapf(err, "can't save deleted comment for key %s from bucket %s", commentID, locator.URL)
}
// delete from "last" bucket
lastBkt := tx.Bucket([]byte(lastBucketName))
if err := lastBkt.Delete([]byte(commentID)); err != nil {
return errors.Wrapf(err, "can't delete key %s from bucket %s", commentID, lastBucketName)
}
// decrement comments count for post url
if _, e = b.count(tx, comment.Locator.URL, -1); e != nil {
return errors.Wrapf(e, "failed to decrement count for %s", comment.Locator)
}
return nil
})
}
// DeleteAll removes all top-level buckets for given siteID
func (b *BoltDB) DeleteAll(siteID string) error {
bdb, err := b.db(siteID)
if err != nil {
return err
}
// delete all buckets except blocked users
toDelete := []string{postsBucketName, lastBucketName, userBucketName, infoBucketName}
// delete top-level buckets
err = bdb.Update(func(tx *bolt.Tx) error {
for _, bktName := range toDelete {
if e := tx.DeleteBucket([]byte(bktName)); e != nil {
return errors.Wrapf(err, "failed to delete top level bucket %s", bktName)
}
if _, e := tx.CreateBucketIfNotExists([]byte(bktName)); e != nil {
return errors.Wrapf(err, "failed to create top level bucket %s", bktName)
}
}
return nil
})
return errors.Wrapf(err, "failed to delete top level buckets fro site %s", siteID)
}
// DeleteUser removes all comments for given user. Everything will be market as deleted
// and user name and userID will be changed to "deleted". Also removes from last and from user buckets.
func (b *BoltDB) DeleteUser(siteID string, userID string) error {
bdb, err := b.db(siteID)
if err != nil {
return err
}
// get list of all comments outside of transaction loop
posts, err := b.List(siteID, 0, 0)
if err != nil {
return err
}
type commentInfo struct {
locator store.Locator
commentID string
}
// get list of commentID for all user's comment
comments := []commentInfo{}
for _, postInfo := range posts {
err = bdb.View(func(tx *bolt.Tx) error {
postsBkt := tx.Bucket([]byte(postsBucketName))
postBkt := postsBkt.Bucket([]byte(postInfo.URL))
err = postBkt.ForEach(func(postURL []byte, commentVal []byte) error {
comment := store.Comment{}
if err = json.Unmarshal(commentVal, &comment); err != nil {
return errors.Wrap(err, "failed to unmarshal")
}
if comment.User.ID == userID {
comments = append(comments, commentInfo{locator: comment.Locator, commentID: comment.ID})
}
return nil
})
return errors.Wrapf(err, "failed to collect list of comments for deletion from %s", postInfo.URL)
})
if err != nil {
return err
}
}
log.Printf("[DEBUG] comments for removal=%d", len(comments))
// delete collected comments
for _, ci := range comments {
if e := b.Delete(ci.locator, ci.commentID, store.HardDelete); e != nil {
return errors.Wrapf(err, "failed to delete comment %+v", ci)
}
}
// delete user bucket
err = bdb.Update(func(tx *bolt.Tx) error {
usersBkt := tx.Bucket([]byte(userBucketName))
if usersBkt != nil {
if e := usersBkt.DeleteBucket([]byte(userID)); e != nil {
return errors.Wrapf(err, "failed to delete user bucket for %s", userID)
}
}
return nil
})
if err != nil {
return errors.Wrap(err, "can't delete user meta")
}
if len(comments) == 0 {
return errors.Errorf("unknown user %s", userID)
}
return err
}
// SetBlock blocks/unblocks user for given site
func (b *BoltDB) SetBlock(siteID string, userID string, status bool) error {
bdb, err := b.db(siteID)
if err != nil {
return err
}
return bdb.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(blocksBucketName))
switch status {
case true:
if e := bucket.Put([]byte(userID), []byte(time.Now().Format(tsNano))); e != nil {
return errors.Wrapf(e, "failed to put %s to %s", userID, blocksBucketName)
}
case false:
if e := bucket.Delete([]byte(userID)); e != nil {
return errors.Wrapf(e, "failed to clean %s from %s", userID, blocksBucketName)
}
}
return nil
})
}
// IsBlocked checks if user blocked
func (b *BoltDB) IsBlocked(siteID string, userID string) (blocked bool) {
bdb, err := b.db(siteID)
if err != nil {
return false
}
_ = bdb.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(blocksBucketName))
blocked = bucket.Get([]byte(userID)) != nil
return nil
})
return blocked
}
// Blocked get lists of blocked users for given site
// bucket uses userID:
func (b *BoltDB) Blocked(siteID string) (users []store.BlockedUser, err error) {
users = []store.BlockedUser{}
bdb, err := b.db(siteID)
if err != nil {
return nil, err
}
err = bdb.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(blocksBucketName))
return bucket.ForEach(func(k []byte, v []byte) error {
ts, e := time.ParseInLocation(tsNano, string(v), time.Local)
if e != nil {
return errors.Wrap(e, "can't parse block ts")
}
// get user name from comment user section
userName := ""
userComments, e := b.User(siteID, string(k), 1, 0)
if e == nil && len(userComments) > 0 {
userName = userComments[0].User.Name
}
users = append(users, store.BlockedUser{ID: string(k), Name: userName, Timestamp: ts})
return nil
})
})
return users, err
}
// SetReadOnly makes post read-only or reset the ro flag
func (b *BoltDB) SetReadOnly(locator store.Locator, status bool) error {
bdb, err := b.db(locator.SiteID)
if err != nil {
return err
}
return bdb.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(readonlyBucketName))
switch status {
case true:
if e := bucket.Put([]byte(locator.URL), []byte(time.Now().Format(tsNano))); e != nil {
return errors.Wrapf(e, "failed to set ro for %s", locator.URL)
}
case false:
if e := bucket.Delete([]byte(locator.URL)); e != nil {
return errors.Wrapf(e, "failed to clean ro for %s", locator.URL)
}
}
return nil
})
}
// IsReadOnly checks if user blocked
func (b *BoltDB) IsReadOnly(locator store.Locator) (ro bool) {
bdb, err := b.db(locator.SiteID)
if err != nil {
return false
}
_ = bdb.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(readonlyBucketName))
ro = bucket.Get([]byte(locator.URL)) != nil
return nil
})
return ro
}
// SetVerified makes user verified or reset the flag
func (b *BoltDB) SetVerified(siteID string, userID string, status bool) error {
bdb, err := b.db(siteID)
if err != nil {
return err
}
return bdb.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(verifiedBucketName))
switch status {
case true:
if e := bucket.Put([]byte(userID), []byte(time.Now().Format(tsNano))); e != nil {
return errors.Wrapf(e, "failed to set verified status for %s", userID)
}
case false:
if e := bucket.Delete([]byte(userID)); e != nil {
return errors.Wrapf(e, "failed to clean verified status for %s", userID)
}
}
return nil
})
}
// IsVerified checks if user verified
func (b *BoltDB) IsVerified(siteID string, userID string) (verified bool) {
bdb, err := b.db(siteID)
if err != nil {
return false
}
_ = bdb.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(verifiedBucketName))
verified = bucket.Get([]byte(userID)) != nil
return nil
})
return verified
}
-200
View File
@@ -1,200 +0,0 @@
package engine
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/umputun/remark/app/store"
)
func TestBoltAdmin_Delete(t *testing.T) {
defer os.Remove(testDb)
b := prep(t)
loc := store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}
res, err := b.Find(loc, "time")
assert.Nil(t, err)
assert.Equal(t, 2, len(res), "initially 2 comments")
err = b.Delete(loc, res[0].ID, store.SoftDelete)
assert.Nil(t, err)
res, err = b.Find(loc, "time")
assert.Nil(t, err)
assert.Equal(t, 2, len(res))
assert.Equal(t, "", res[0].Text)
assert.True(t, res[0].Deleted, "marked deleted")
assert.Equal(t, store.User{Name: "user name", ID: "user1", Picture: "", Admin: false, Blocked: false, IP: ""}, res[0].User)
assert.Equal(t, "some text2", res[1].Text)
assert.False(t, res[1].Deleted)
comments, err := b.Last("radio-t", 10)
assert.Nil(t, err)
assert.Equal(t, 1, len(comments), "1 in last, 1 removed")
err = b.Delete(loc, "123456", store.SoftDelete)
assert.NotNil(t, err)
loc.SiteID = "bad"
err = b.Delete(loc, res[0].ID, store.SoftDelete)
assert.EqualError(t, err, `site "bad" not found`)
loc = store.Locator{URL: "https://radio-t.com/bad", SiteID: "radio-t"}
err = b.Delete(loc, res[0].ID, store.SoftDelete)
assert.EqualError(t, err, `no bucket https://radio-t.com/bad in store`)
}
func TestBoltAdmin_DeleteHard(t *testing.T) {
defer os.Remove(testDb)
b := prep(t)
loc := store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}
res, err := b.Find(loc, "time")
assert.Nil(t, err)
assert.Equal(t, 2, len(res), "initially 2 comments")
err = b.Delete(loc, res[0].ID, store.HardDelete)
assert.Nil(t, err)
res, err = b.Find(loc, "time")
assert.Nil(t, err)
assert.Equal(t, 2, len(res))
assert.Equal(t, "", res[0].Text)
assert.True(t, res[0].Deleted, "marked deleted")
assert.Equal(t, store.User{Name: "deleted", ID: "deleted", Picture: "", Admin: false, Blocked: false, IP: ""}, res[0].User)
}
func TestBoltAdmin_DeleteAll(t *testing.T) {
defer os.Remove(testDb)
b := prep(t)
loc := store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}
res, err := b.Find(loc, "time")
assert.Nil(t, err)
assert.Equal(t, 2, len(res), "initially 2 comments")
err = b.DeleteAll("radio-t")
assert.Nil(t, err)
comments, err := b.Last("radio-t", 10)
assert.Nil(t, err)
assert.Equal(t, 0, len(comments), "nothing left")
c, err := b.Count(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"})
assert.Nil(t, err)
assert.Equal(t, 0, c, "0 count")
err = b.DeleteAll("bad")
assert.EqualError(t, err, `site "bad" not found`)
}
func TestBoltAdmin_DeleteUser(t *testing.T) {
defer os.Remove(testDb)
b := prep(t)
err := b.DeleteUser("radio-t", "user1")
require.NoError(t, err)
loc := store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}
res, err := b.Find(loc, "time")
assert.Nil(t, err)
assert.Equal(t, 2, len(res), "2 comments with deleted info")
assert.Equal(t, store.User{Name: "deleted", ID: "deleted", Picture: "", Admin: false, Blocked: false, IP: ""}, res[0].User)
assert.Equal(t, store.User{Name: "deleted", ID: "deleted", Picture: "", Admin: false, Blocked: false, IP: ""}, res[1].User)
c, err := b.Count(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"})
assert.Nil(t, err)
assert.Equal(t, 0, c, "0 count")
_, err = b.User("radio-t", "user1", 5, 0)
assert.EqualError(t, err, "no comments for user user1 in store")
comments, err := b.Last("radio-t", 10)
assert.Nil(t, err)
assert.Equal(t, 0, len(comments), "nothing left")
err = b.DeleteUser("radio-t-bad", "user1")
assert.EqualError(t, err, `site "radio-t-bad" not found`)
}
func TestBoltAdmin_BlockUser(t *testing.T) {
defer os.Remove(testDb)
b := prep(t)
assert.False(t, b.IsBlocked("radio-t", "user1"), "nothing blocked")
assert.NoError(t, b.SetBlock("radio-t", "user1", true))
assert.True(t, b.IsBlocked("radio-t", "user1"), "user1 blocked")
assert.False(t, b.IsBlocked("radio-t", "user2"), "user2 still unblocked")
assert.NoError(t, b.SetBlock("radio-t", "user1", false))
assert.False(t, b.IsBlocked("radio-t", "user1"), "user1 unblocked")
assert.EqualError(t, b.SetBlock("bad", "user1", true), `site "bad" not found`)
assert.NoError(t, b.SetBlock("radio-t", "userX", false))
assert.False(t, b.IsBlocked("radio-t-bad", "user1"), "nothing blocked on wrong site")
}
func TestBoltAdmin_BlockList(t *testing.T) {
defer os.Remove(testDb)
b := prep(t)
assert.NoError(t, b.SetBlock("radio-t", "user1", true))
assert.NoError(t, b.SetBlock("radio-t", "user2", true))
assert.NoError(t, b.SetBlock("radio-t", "user3", false))
ids, err := b.Blocked("radio-t")
assert.NoError(t, err)
assert.Equal(t, 2, len(ids))
assert.Equal(t, "user1", ids[0].ID)
assert.Equal(t, "user2", ids[1].ID)
t.Logf("%+v", ids)
_, err = b.Blocked("bad")
assert.EqualError(t, err, `site "bad" not found`)
}
func TestBoltAdmin_ReadOnly(t *testing.T) {
defer os.Remove(testDb)
b := prep(t)
assert.False(t, b.IsReadOnly(store.Locator{SiteID: "radio-t", URL: "url-1"}), "nothing ro")
assert.NoError(t, b.SetReadOnly(store.Locator{SiteID: "radio-t", URL: "url-1"}, true))
assert.True(t, b.IsReadOnly(store.Locator{SiteID: "radio-t", URL: "url-1"}), "url-1 ro")
assert.False(t, b.IsReadOnly(store.Locator{SiteID: "radio-t", URL: "url-2"}), "url-2 still writable")
assert.NoError(t, b.SetReadOnly(store.Locator{SiteID: "radio-t", URL: "url-1"}, false))
assert.False(t, b.IsReadOnly(store.Locator{SiteID: "radio-t", URL: "url-1"}), "url-1 writable")
assert.EqualError(t, b.SetReadOnly(store.Locator{SiteID: "bad", URL: "url-1"}, true), `site "bad" not found`)
assert.NoError(t, b.SetReadOnly(store.Locator{SiteID: "radio-t", URL: "url-1xyz"}, false))
assert.False(t, b.IsReadOnly(store.Locator{SiteID: "radio-t-bad", URL: "url-1"}), "nothing blocked on wrong site")
}
func TestBoltAdmin_Verified(t *testing.T) {
defer os.Remove(testDb)
b := prep(t)
assert.False(t, b.IsVerified("radio-t", "u1"), "nothing verified")
assert.NoError(t, b.SetVerified("radio-t", "u1", true))
assert.True(t, b.IsVerified("radio-t", "u1"), "u1 verified")
assert.False(t, b.IsVerified("radio-t", "u2"), "u2 still not verified")
assert.NoError(t, b.SetVerified("radio-t", "u1", false))
assert.False(t, b.IsVerified("radio-t", "u1"), "u1 not verified anymore")
assert.EqualError(t, b.SetVerified("bad", "u1", true), `site "bad" not found`)
assert.NoError(t, b.SetVerified("radio-t", "u1xyz", false))
assert.False(t, b.IsVerified("radio-t-bad", "u1"), "nothing verified on wrong site")
}
-83
View File
@@ -1,83 +0,0 @@
// Package engine defines interfaces each supported storage should implement.
// Includes default implementation with boltdb
package engine
import (
"sort"
"strings"
"github.com/umputun/remark/app/store"
)
//go:generate sh -c "mockery -inpkg -name Interface -print > file.tmp && mv file.tmp engine_mock.go"
// Interface combines all store interfaces
type Interface interface {
Accessor
Admin
}
// UserRequest is the request send to get comments by user
type UserRequest struct {
SiteID string
UserID string
Limit int
Skip int
}
// Accessor defines all usual access ops avail for regular user
type Accessor interface {
Create(comment store.Comment) (commentID string, err error) // create new comment, avoid dups by id
Get(locator store.Locator, commentID string) (store.Comment, error) // get comment by id
Put(locator store.Locator, comment store.Comment) error // update comment, mutable parts only
Find(locator store.Locator, sort string) ([]store.Comment, error) // find comments for locator
Last(siteID string, limit int) ([]store.Comment, error) // last comments for given site, sorted by time
User(siteID, userID string, limit, skip int) ([]store.Comment, error) // comments by user, sorted by time
UserCount(siteID, userID string) (int, error) // comments count by user
Count(locator store.Locator) (int, error) // number of comments for the post
List(siteID string, limit int, skip int) ([]store.PostInfo, error) // list of commented posts
Info(locator store.Locator, readonlyAge int) (store.PostInfo, error) // get post info
}
// Admin defines all store ops avail for admin only
type Admin interface {
Delete(locator store.Locator, commentID string, mode store.DeleteMode) error // delete comment by id
DeleteAll(siteID string) error // delete all data from site
DeleteUser(siteID string, userID string) error // remove all comments from user
SetBlock(siteID string, userID string, status bool) error // block or unblock user
IsBlocked(siteID string, userID string) bool // check if user blocked
Blocked(siteID string) ([]store.BlockedUser, error) // get list of blocked users
SetReadOnly(locator store.Locator, status bool) error // set/reset read-only flag
IsReadOnly(locator store.Locator) bool // check if post read-only
SetVerified(siteID string, userID string, status bool) error // set/reset verified flag
IsVerified(siteID string, userID string) bool // check verified status
}
// sortComments is for engines can't sort data internally
func sortComments(comments []store.Comment, sortFld string) []store.Comment {
sort.Slice(comments, func(i, j int) bool {
switch sortFld {
case "+time", "-time", "time", "+active", "-active", "active":
if strings.HasPrefix(sortFld, "-") {
return comments[i].Timestamp.After(comments[j].Timestamp)
}
return comments[i].Timestamp.Before(comments[j].Timestamp)
case "+score", "-score", "score":
if strings.HasPrefix(sortFld, "-") {
if comments[i].Score == comments[j].Score {
return comments[i].Timestamp.Before(comments[j].Timestamp)
}
return comments[i].Score > comments[j].Score
}
if comments[i].Score == comments[j].Score {
return comments[i].Timestamp.Before(comments[j].Timestamp)
}
return comments[i].Score < comments[j].Score
default:
return comments[i].Timestamp.Before(comments[j].Timestamp)
}
})
return comments
}
-43
View File
@@ -1,43 +0,0 @@
package engine
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/umputun/remark/app/store"
)
func TestEngine_sortComments(t *testing.T) {
cc := []store.Comment{
{ID: "1", Score: 5, Timestamp: time.Date(2018, 2, 5, 10, 1, 0, 0, time.Local)},
{ID: "2", Score: 4, Timestamp: time.Date(2018, 2, 5, 10, 2, 0, 0, time.Local)},
{ID: "3", Score: 6, Timestamp: time.Date(2018, 2, 5, 10, 3, 0, 0, time.Local)},
{ID: "4", Score: 6, Timestamp: time.Date(2018, 2, 5, 10, 4, 0, 0, time.Local)},
}
sortComments(cc, "+time")
assert.Equal(t, "1", cc[0].ID)
assert.Equal(t, "2", cc[1].ID)
assert.Equal(t, "3", cc[2].ID)
assert.Equal(t, "4", cc[3].ID)
sortComments(cc, "-time")
assert.Equal(t, "4", cc[0].ID)
assert.Equal(t, "3", cc[1].ID)
assert.Equal(t, "2", cc[2].ID)
assert.Equal(t, "1", cc[3].ID)
sortComments(cc, "score")
assert.Equal(t, "2", cc[0].ID)
assert.Equal(t, "1", cc[1].ID)
assert.Equal(t, "3", cc[2].ID)
assert.Equal(t, "4", cc[3].ID)
sortComments(cc, "-score")
assert.Equal(t, "3", cc[0].ID)
assert.Equal(t, "4", cc[1].ID)
assert.Equal(t, "1", cc[2].ID)
assert.Equal(t, "2", cc[3].ID)
}
-189
View File
@@ -1,189 +0,0 @@
package service
import (
"sync"
"time"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/umputun/remark/app/store"
"github.com/umputun/remark/app/store/engine"
)
// DataStore wraps store.Interface with additional methods
type DataStore struct {
engine.Interface
EditDuration time.Duration
Secret string
MaxCommentSize int
// granular locks
scopedLocks struct {
sync.Mutex
sync.Once
locks map[string]sync.Locker
}
}
const defaultCommentMaxSize = 2000
// Create prepares comment and forward to Interface.Create
func (s *DataStore) Create(comment store.Comment) (commentID string, err error) {
// fill ID and time if empty
if comment.ID == "" {
comment.ID = uuid.New().String()
}
if comment.Timestamp.IsZero() {
comment.Timestamp = time.Now()
}
// reset votes if nothing
if comment.Votes == nil {
comment.Votes = make(map[string]bool)
}
comment.Sanitize() // clear potentially dangerous js from all parts of comment
comment.User.HashIP(s.Secret) // replace ip by hash
return s.Interface.Create(comment)
}
// SetPin pin/un-pin comment as special
func (s *DataStore) SetPin(locator store.Locator, commentID string, status bool) error {
comment, err := s.Get(locator, commentID)
if err != nil {
return err
}
comment.Pin = status
return s.Put(locator, comment)
}
// Vote for comment by id and locator
func (s *DataStore) Vote(locator store.Locator, commentID string, userID string, val bool) (comment store.Comment, err error) {
cLock := s.getsScopedLocks(locator.URL) // get lock for URL scope
cLock.Lock() // prevents race on voting
defer cLock.Unlock()
comment, err = s.Get(locator, commentID)
if err != nil {
return comment, err
}
if comment.User.ID == userID && userID != "dev" {
return comment, errors.Errorf("user %s can not vote for his own comment %s", userID, commentID)
}
if comment.Votes == nil {
comment.Votes = make(map[string]bool)
}
v, voted := comment.Votes[userID]
if voted && v == val {
return comment, errors.Errorf("user %s already voted for %s", userID, commentID)
}
// reset vote if user changed to opposite
if voted && v != val {
delete(comment.Votes, userID)
}
// add to voted map if first vote
if !voted {
comment.Votes[userID] = val
}
// update score
if val {
comment.Score++
} else {
comment.Score--
}
return comment, s.Put(locator, comment)
}
// EditRequest contains fields needed for comment update
type EditRequest struct {
Text string
Orig string
Summary string
}
// EditComment to edit text and update Edit info
func (s *DataStore) EditComment(locator store.Locator, commentID string, req EditRequest) (comment store.Comment, err error) {
comment, err = s.Get(locator, commentID)
if err != nil {
return comment, err
}
// edit allowed in editDuration window only
if s.EditDuration > 0 && time.Now().After(comment.Timestamp.Add(s.EditDuration)) {
return comment, errors.Errorf("too late to edit %s", commentID)
}
comment.Text = req.Text
comment.Orig = req.Orig
comment.Edit = &store.Edit{
Timestamp: time.Now(),
Summary: req.Summary,
}
comment.Sanitize()
err = s.Put(locator, comment)
return comment, err
}
// Counts returns postID+count list for given comments
func (s *DataStore) Counts(siteID string, postIDs []string) ([]store.PostInfo, error) {
res := []store.PostInfo{}
for _, p := range postIDs {
if c, err := s.Count(store.Locator{SiteID: siteID, URL: p}); err == nil {
res = append(res, store.PostInfo{URL: p, Count: c})
}
}
return res, nil
}
// ValidateComment checks if comment size below max and user fields set
func (s *DataStore) ValidateComment(c *store.Comment) error {
maxSize := s.MaxCommentSize
if s.MaxCommentSize <= 0 {
maxSize = defaultCommentMaxSize
}
if c.Orig == "" {
return errors.New("empty comment text")
}
if len([]rune(c.Orig)) > maxSize {
return errors.Errorf("comment text exceeded max allowed size %d (%d)", maxSize, len([]rune(c.Orig)))
}
if c.User.ID == "" || c.User.Name == "" {
return errors.Errorf("empty user info")
}
return nil
}
// IsVerifiedFn returns func to check if user verified or not
func (s *DataStore) IsVerifiedFn() func(siteID string, userID string) bool {
return func(siteID string, userID string) bool {
if siteID == "" {
return false
}
return s.IsVerified(siteID, userID)
}
}
// getsScopedLocks pull lock from the map if found or create a new one
func (s *DataStore) getsScopedLocks(id string) (lock sync.Locker) {
s.scopedLocks.Do(func() { s.scopedLocks.locks = map[string]sync.Locker{} })
s.scopedLocks.Lock()
lock, ok := s.scopedLocks.locks[id]
if !ok {
lock = &sync.Mutex{}
s.scopedLocks.locks[id] = lock
}
s.scopedLocks.Unlock()
return lock
}
-372
View File
@@ -1,372 +0,0 @@
package service
import (
"fmt"
"math/rand"
"os"
"strings"
"sync"
"testing"
"time"
"github.com/coreos/bbolt"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/umputun/remark/app/store"
"github.com/umputun/remark/app/store/engine"
)
var testDb = "/tmp/test-remark.db"
func TestService_CreateFromEmpty(t *testing.T) {
defer os.Remove(testDb)
b := DataStore{Interface: prepStoreEngine(t), Secret: "secret 123"}
comment := store.Comment{
Text: "text",
User: store.User{IP: "192.168.1.1", ID: "user", Name: "name"},
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
}
id, err := b.Create(comment)
assert.NoError(t, err)
assert.True(t, id != "", id)
res, err := b.Get(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, id)
assert.NoError(t, err)
t.Logf("%+v", res)
assert.Equal(t, "text", res.Text)
assert.True(t, time.Since(res.Timestamp).Seconds() < 1)
assert.Equal(t, "user", res.User.ID)
assert.Equal(t, "name", res.User.Name)
assert.Equal(t, "23f97cf4d5c29ef788ca2bdd1c9e75656c0e4149", res.User.IP)
assert.Equal(t, map[string]bool{}, res.Votes)
}
func TestService_CreateFromPartial(t *testing.T) {
defer os.Remove(testDb)
b := DataStore{Interface: prepStoreEngine(t), Secret: "secret 123"}
comment := store.Comment{
Text: "text",
Timestamp: time.Date(2018, 3, 25, 16, 34, 33, 0, time.UTC),
Votes: map[string]bool{"u1": true, "u2": false},
User: store.User{IP: "192.168.1.1", ID: "user", Name: "name"},
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
}
id, err := b.Create(comment)
assert.NoError(t, err)
assert.True(t, id != "", id)
res, err := b.Get(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, id)
assert.NoError(t, err)
t.Logf("%+v", res)
assert.Equal(t, "text", res.Text)
assert.Equal(t, comment.Timestamp, res.Timestamp)
assert.Equal(t, "user", res.User.ID)
assert.Equal(t, "name", res.User.Name)
assert.Equal(t, "23f97cf4d5c29ef788ca2bdd1c9e75656c0e4149", res.User.IP)
assert.Equal(t, comment.Votes, res.Votes)
}
func TestService_Vote(t *testing.T) {
defer os.Remove(testDb)
b := DataStore{Interface: prepStoreEngine(t)}
comment := store.Comment{
Text: "text",
User: store.User{IP: "192.168.1.1", ID: "user", Name: "name"},
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
}
_, err := b.Create(comment)
assert.NoError(t, err)
res, err := b.Last("radio-t", 0)
t.Logf("%+v", res[0])
assert.Nil(t, err)
assert.Equal(t, 3, len(res))
assert.Equal(t, 0, res[0].Score)
assert.Equal(t, map[string]bool{}, res[0].Votes, "no votes initially")
c, err := b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, "user1", true)
assert.Nil(t, err)
assert.Equal(t, 1, c.Score)
assert.Equal(t, map[string]bool{"user1": true}, c.Votes, "user voted +")
c, err = b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, "user", true)
assert.NotNil(t, err, "self-voting not allowed")
_, err = b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, "user1", true)
assert.NotNil(t, err, "double-voting rejected")
assert.True(t, strings.HasPrefix(err.Error(), "user user1 already voted"))
res, err = b.Last("radio-t", 0)
assert.Nil(t, err)
assert.Equal(t, 3, len(res))
assert.Equal(t, 1, res[0].Score)
_, err = b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, "user1", false)
assert.Nil(t, err, "vote reset")
res, err = b.Last("radio-t", 0)
assert.Nil(t, err)
assert.Equal(t, 3, len(res))
assert.Equal(t, 0, res[0].Score)
assert.Equal(t, map[string]bool{}, res[0].Votes, "vote reset ok")
}
func TestService_VoteAggressive(t *testing.T) {
defer os.Remove(testDb)
b := DataStore{Interface: prepStoreEngine(t)}
comment := store.Comment{
Text: "text",
User: store.User{IP: "192.168.1.1", ID: "user", Name: "name"},
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
}
_, err := b.Create(comment)
assert.NoError(t, err)
res, err := b.Last("radio-t", 0)
require.Nil(t, err)
t.Logf("%+v", res[0])
assert.Equal(t, 3, len(res))
assert.Equal(t, 0, res[0].Score)
assert.Equal(t, map[string]bool{}, res[0].Votes, "no votes initially")
// add a vote as user2
_, err = b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, "user2", true)
require.Nil(t, err)
// crazy vote +1 as user1
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, "user1", true)
}()
}
wg.Wait()
res, err = b.Last("radio-t", 0)
require.NoError(t, err)
t.Logf("%+v", res[0])
assert.Equal(t, 3, len(res))
assert.Equal(t, 2, res[0].Score, "add single +1")
assert.Equal(t, 2, len(res[0].Votes), "made a single vote")
// random +1/-1 result should be [0..2]
rand.Seed(time.Now().UnixNano())
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
val := rand.Intn(2) > 0
b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, "user1", val)
}()
}
wg.Wait()
res, err = b.Last("radio-t", 0)
require.NoError(t, err)
assert.Equal(t, 3, len(res))
t.Logf("%+v %d", res[0], res[0].Score)
assert.True(t, res[0].Score >= 0 && res[0].Score <= 2, "unexpected score %d", res[0].Score)
}
func TestService_VoteConcurrent(t *testing.T) {
defer os.Remove(testDb)
b := DataStore{Interface: prepStoreEngine(t)}
comment := store.Comment{
Text: "text",
User: store.User{IP: "192.168.1.1", ID: "user", Name: "name"},
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
}
_, err := b.Create(comment)
assert.NoError(t, err)
res, err := b.Last("radio-t", 0)
require.Nil(t, err)
// concurrent vote +1 as multiple users for the same comment
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
i := i
go func() {
defer wg.Done()
b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, fmt.Sprintf("user1-%d", i), true)
}()
}
wg.Wait()
res, err = b.Last("radio-t", 0)
require.NoError(t, err)
assert.Equal(t, 100, res[0].Score, "should have 1000 score")
assert.Equal(t, 100, len(res[0].Votes), "should have 1000 votes")
}
func TestService_Pin(t *testing.T) {
defer os.Remove(testDb)
b := DataStore{Interface: prepStoreEngine(t)}
res, err := b.Last("radio-t", 0)
t.Logf("%+v", res[0])
assert.Nil(t, err)
assert.Equal(t, 2, len(res))
assert.Equal(t, false, res[0].Pin)
err = b.SetPin(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, true)
assert.Nil(t, err)
c, err := b.Get(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID)
assert.Nil(t, err)
assert.Equal(t, true, c.Pin)
err = b.SetPin(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, false)
assert.Nil(t, err)
c, err = b.Get(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID)
assert.Nil(t, err)
assert.Equal(t, false, c.Pin)
}
func TestService_EditComment(t *testing.T) {
defer os.Remove(testDb)
b := DataStore{Interface: prepStoreEngine(t)}
res, err := b.Last("radio-t", 0)
t.Logf("%+v", res[0])
assert.Nil(t, err)
assert.Equal(t, 2, len(res))
assert.Nil(t, res[0].Edit)
comment, err := b.EditComment(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID,
EditRequest{Orig: "yyy", Text: "xxx", Summary: "my edit"})
assert.Nil(t, err)
assert.Equal(t, "my edit", comment.Edit.Summary)
assert.Equal(t, "xxx", comment.Text)
assert.Equal(t, "yyy", comment.Orig)
c, err := b.Get(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID)
assert.Nil(t, err)
assert.Equal(t, "my edit", c.Edit.Summary)
assert.Equal(t, "xxx", c.Text)
_, err = b.EditComment(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID,
EditRequest{Orig: "yyy", Text: "xxx", Summary: "my edit"})
assert.Nil(t, err, "allow second edit")
}
func TestService_EditCommentDurationFailed(t *testing.T) {
defer os.Remove(testDb)
b := DataStore{Interface: prepStoreEngine(t), EditDuration: 100 * time.Millisecond}
res, err := b.Last("radio-t", 0)
t.Logf("%+v", res[0])
assert.Nil(t, err)
assert.Equal(t, 2, len(res))
assert.Nil(t, res[0].Edit)
time.Sleep(time.Second)
_, err = b.EditComment(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID,
EditRequest{Orig: "yyy", Text: "xxx", Summary: "my edit"})
assert.NotNil(t, err)
}
func TestService_ValidateComment(t *testing.T) {
b := DataStore{MaxCommentSize: 2000}
longText := fmt.Sprintf("%4000s", "X")
tbl := []struct {
inp store.Comment
err error
}{
{inp: store.Comment{}, err: errors.New("empty comment text")},
{inp: store.Comment{Orig: "something blah", User: store.User{ID: "myid", Name: "name"}}, err: nil},
{inp: store.Comment{Orig: "something blah", User: store.User{ID: "myid"}}, err: errors.New("empty user info")},
{inp: store.Comment{Orig: longText, User: store.User{ID: "myid", Name: "name"}}, err: errors.New("comment text exceeded max allowed size 2000 (4000)")},
}
for n, tt := range tbl {
e := b.ValidateComment(&tt.inp)
if tt.err == nil {
assert.Nil(t, e, "check #%d", n)
continue
}
assert.EqualError(t, tt.err, e.Error(), "check #%d", n)
}
}
func TestService_Counts(t *testing.T) {
defer os.Remove(testDb)
b := prepStoreEngine(t) // two comments for https://radio-t.com
// add one more for https://radio-t.com/2
comment := store.Comment{
ID: "123456",
Text: `some text, <a href="http://radio-t.com">link</a>`,
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"},
User: store.User{ID: "user1", Name: "user name"},
}
_, err := b.Create(comment)
assert.Nil(t, err)
svc := DataStore{Interface: b}
res, err := svc.Counts("radio-t", []string{"https://radio-t.com/2"})
assert.Nil(t, err)
assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com/2", Count: 1}}, res)
res, err = svc.Counts("radio-t", []string{"https://radio-t.com", "https://radio-t.com/2", "blah"})
assert.Nil(t, err)
assert.Equal(t, []store.PostInfo{
{URL: "https://radio-t.com", Count: 2},
{URL: "https://radio-t.com/2", Count: 1},
{URL: "blah", Count: 0},
}, res)
}
func TestService_IsVerifiedFn(t *testing.T) {
defer os.Remove(testDb)
b := DataStore{Interface: prepStoreEngine(t)}
fn := b.IsVerifiedFn()
verified := fn("radio-t", "user1")
assert.False(t, verified)
err := b.Interface.SetVerified("radio-t", "user1", true)
assert.Nil(t, err)
verified = fn("radio-t", "user1")
assert.True(t, verified)
}
// makes new boltdb, put two records
func prepStoreEngine(t *testing.T) engine.Interface {
os.Remove(testDb)
boltStore, err := engine.NewBoltDB(bolt.Options{}, engine.BoltSite{FileName: "/tmp/test-remark.db", SiteID: "radio-t"})
assert.Nil(t, err)
b := boltStore
comment := store.Comment{
ID: "id-1",
Text: `some text, <a href="http://radio-t.com">link</a>`,
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
User: store.User{ID: "user1", Name: "user name"},
}
_, err = b.Create(comment)
assert.Nil(t, err)
comment = store.Comment{
ID: "id-2",
Text: "some text2",
Timestamp: time.Date(2017, 12, 20, 15, 18, 23, 0, time.Local),
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
User: store.User{ID: "user1", Name: "user name"},
}
_, err = b.Create(comment)
assert.Nil(t, err)
return b
}
-60
View File
@@ -1,60 +0,0 @@
package store
import (
"crypto/hmac"
"crypto/sha1"
"encoding/hex"
"fmt"
"hash/crc64"
"io"
"log"
"regexp"
)
// User holds user-related info
type User struct {
Name string `json:"name"`
ID string `json:"id"`
Picture string `json:"picture"`
IP string `json:"ip,omitempty"`
Admin bool `json:"admin"`
Blocked bool `json:"block,omitempty"`
Verified bool `json:"verified,omitempty"`
}
var reValidSha = regexp.MustCompile("^[a-fA-F0-9]{40}$")
// HashIP replace IP field with hashed hmac
func (u *User) HashIP(secret string) {
u.IP = HashValue(u.IP, secret)
}
// HashValue makes hmac with secret
func HashValue(val string, secret string) string {
if val == "" || reValidSha.MatchString(val) {
return val // already hashed or empty
}
key := []byte(secret)
h := hmac.New(sha1.New, key)
if _, err := io.WriteString(h, val); err != nil {
// fail back to crc64
log.Printf("[WARN] can't hash ip, %s", err)
return fmt.Sprintf("%x", crc64.Checksum([]byte(val), crc64.MakeTable(crc64.ECMA)))
}
return hex.EncodeToString(h.Sum(nil))
}
// EncodeID hashes id to sha1. The function intentionally left outside of User struct because in some cases
// we need hashing for parts of id, in some others hashing for non-User values.
func EncodeID(id string) string {
if reValidSha.MatchString(id) {
return id // already hashed or empty
}
h := sha1.New()
if _, err := io.WriteString(h, id); err != nil {
// fail back to crc64
log.Printf("[WARN] can't hash id %s, %s", id, err)
return fmt.Sprintf("%x", crc64.Checksum([]byte(id), crc64.MakeTable(crc64.ECMA)))
}
return hex.EncodeToString(h.Sum(nil))
}
-45
View File
@@ -1,45 +0,0 @@
package store
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestUser_EncodeID(t *testing.T) {
tbl := []struct {
id string
hash string
}{
{"myid", "6e34471f84557e1713012d64a7477c71bfdac631"},
{"", "da39a3ee5e6b4b0d3255bfef95601890afd80709"},
{"blah blah", "135a1e01bae742c4a576b20fd41a683f6483ca43"},
{"da39a3ee5e6b4b0d3255bfef95601890afd80709", "da39a3ee5e6b4b0d3255bfef95601890afd80709"},
}
for i, tt := range tbl {
assert.Equal(t, tt.hash, EncodeID(tt.id), "case #%d", i)
}
}
func TestUser_HashIP(t *testing.T) {
tbl := []struct {
ip string
hash1, hash2 string
}{
{"127.0.0.1", "ae12fe3b5f129b5cc4cdd2b136b7b7947c4d2741", "dbc7c999343f003f189f70aaf52cc04443f90790"},
{"8.8.8.8", "8cee77c27e32a2b5aec95c29888ac9946618d9a2", "70a46afce9633f010b06e129b8ad08243a1c4da9"},
{"8cee77c27e32a2b5aec95c29888ac9946618d9a2", "8cee77c27e32a2b5aec95c29888ac9946618d9a2", "8cee77c27e32a2b5aec95c29888ac9946618d9a2"},
{"", "", ""},
}
for i, tt := range tbl {
u := User{IP: tt.ip}
u.HashIP("")
assert.Equal(t, tt.hash1, u.IP, "case #%d", i)
u = User{IP: tt.ip}
u.HashIP("123456")
assert.Equal(t, tt.hash2, u.IP, "case #%d", i)
}
}
+74
View File
@@ -0,0 +1,74 @@
run:
timeout: 5m
output:
format: tab
skip-dirs:
- vendor
linters-settings:
govet:
check-shadowing: true
golint:
min-confidence: 0.1
maligned:
suggest-new: true
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
linters:
enable:
- bodyclose
- megacheck
- revive
- govet
- unconvert
- megacheck
- structcheck
- gas
- gocyclo
- dupl
- misspell
- unparam
- varcheck
- deadcode
- typecheck
- ineffassign
- varcheck
- stylecheck
- gochecknoinits
- exportloopref
- gocritic
- nakedret
- gosimple
- prealloc
fast: false
disable-all: true
issues:
exclude-rules:
- text: "at least one file in a package should have a package comment"
linters:
- stylecheck
- text: "should have a package comment, unless it's in another file for this package"
linters:
- golint
- path: _test\.go
linters:
- gosec
- dupl
exclude-use-default: false
+19
View File
@@ -0,0 +1,19 @@
FROM umputun/baseimage:buildgo-latest 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-latest
WORKDIR /srv
COPY --from=build-backend /build/bin/memory_store /srv/memory_store
RUN chown -R app:app /srv
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s CMD curl --fail http://localhost:8080/ping || exit 1
USER app
CMD ["/srv/memory_store"]
+12
View File
@@ -0,0 +1,12 @@
# sample store implementation
`memory_store` illustrates how to make a custom storage plugin for remark42.
In order to run remark42 with memory_store copy provided `compose-dev-memstore.yml` to the root directory and run:
1. `docker-compose -f compose-dev-memstore.yml build`
1. `docker-compose -f compose-dev-memstore.yml up`
As usual, demo site will run on http://127.0.0.1:8080/web/
note: in order to work with the latest (current) version of master `go.mod` uses replacement directive for the backend package. In real-life usage `replace github.com/umputun/remark42/backend => ../../` should not be used.
@@ -0,0 +1,87 @@
/*
* Copyright 2019 Umputun. All rights reserved.
* Use of this source code is governed by a MIT-style
* license that can be found in the LICENSE file.
*/
package accessor
import (
log "github.com/go-pkgz/lgr"
"github.com/pkg/errors"
"github.com/umputun/remark42/backend/app/store/admin"
)
// MemAdmin implements admin.Store with memory backend
type MemAdmin struct {
data map[string]AdminRec // admin info per site
key string
}
// AdminRec is a records per site with all admin info in
type AdminRec struct {
SiteID string
IDs []string // admin ids
Email string // admin email
Enabled bool // site enabled
CountCreated int64 // number of created posts
}
// NewMemAdminStore makes admin Store in memory
func NewMemAdminStore(key string) *MemAdmin {
log.Print("[DEBUG] make memory admin store")
return &MemAdmin{data: map[string]AdminRec{}, key: key}
}
// Key executes find by siteID and returns substructure with secret key
func (m *MemAdmin) Key(_ string) (key string, err error) {
return m.key, nil
}
// Admins executes find by siteID and returns admins ids
func (m *MemAdmin) Admins(siteID string) (ids []string, err error) {
resp, ok := m.data[siteID]
if !ok {
return nil, errors.Errorf("site %s not found", siteID)
}
log.Printf("[DEBUG] admins for %s, %+v", siteID, resp.IDs)
return resp.IDs, nil
}
// Email executes find by siteID and returns admin's email
func (m *MemAdmin) Email(siteID string) (email string, err error) {
resp, ok := m.data[siteID]
if !ok {
return "", errors.Errorf("site %s not found", siteID)
}
return resp.Email, nil
}
// Enabled return
func (m *MemAdmin) Enabled(siteID string) (ok bool, err error) {
resp, ok := m.data[siteID]
if !ok {
return false, errors.Errorf("site %s not found", siteID)
}
return resp.Enabled, nil
}
// OnEvent reacts on events from updates, created, delete and vote
func (m *MemAdmin) OnEvent(siteID string, ev admin.EventType) error {
resp, ok := m.data[siteID]
if !ok {
return errors.Errorf("site %s not found", siteID)
}
if ev == admin.EvCreate {
resp.CountCreated++ // not a good idea, just for demo
m.data[siteID] = resp
}
return nil
}
// Set admin data for siteID
func (m *MemAdmin) Set(siteID string, arec AdminRec) {
m.data[siteID] = arec
}
@@ -0,0 +1,73 @@
/*
* Copyright 2019 Umputun. All rights reserved.
* Use of this source code is governed by a MIT-style
* license that can be found in the LICENSE file.
*/
package accessor
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/umputun/remark42/backend/app/store/admin"
)
func TestMemAdmin_Get(t *testing.T) {
adm := NewMemAdminStore("secret")
var ms admin.Store = adm
adm.data = map[string]AdminRec{
"site1": {"site1", []string{"i11", "i12"}, "e1", true, 0},
}
adm.Set("site2", AdminRec{"site2", []string{"i21", "i22"}, "e2", true, 0})
adm.Set("site3", AdminRec{"site3", []string{"i21", "i22"}, "e3", false, 0})
admins, err := ms.Admins("site1")
assert.NoError(t, err)
assert.Equal(t, []string{"i11", "i12"}, admins)
email, err := ms.Email("site1")
assert.NoError(t, err)
assert.Equal(t, "e1", email)
key, err := ms.Key("any")
assert.NoError(t, err)
assert.Equal(t, "secret", key)
admins, err = ms.Admins("site2")
assert.NoError(t, err)
assert.Equal(t, []string{"i21", "i22"}, admins)
email, err = ms.Email("site2")
assert.NoError(t, err)
assert.Equal(t, "e2", email)
key, err = ms.Key("any")
assert.NoError(t, err)
assert.Equal(t, "secret", key)
admins, err = ms.Admins("no-site-in-db")
assert.EqualError(t, err, "site no-site-in-db not found")
assert.Empty(t, admins)
email, err = ms.Email("no-site-in-db")
assert.EqualError(t, err, "site no-site-in-db not found")
assert.Empty(t, email)
enabled, err := ms.Enabled("site1")
assert.NoError(t, err)
assert.True(t, enabled)
enabled, err = ms.Enabled("site3")
assert.NoError(t, err)
assert.False(t, enabled)
enabled, err = ms.Enabled("no-site-in-db")
assert.EqualError(t, err, "site no-site-in-db not found")
assert.False(t, enabled)
err = ms.OnEvent("site1", admin.EvCreate)
assert.NoError(t, err)
err = ms.OnEvent("no-site-in-db", admin.EvCreate)
assert.Error(t, err)
}
@@ -0,0 +1,571 @@
/*
* Copyright 2019 Umputun. All rights reserved.
* Use of this source code is governed by a MIT-style
* license that can be found in the LICENSE file.
*/
package accessor
import (
"log"
"sort"
"sync"
"time"
"github.com/pkg/errors"
"github.com/umputun/remark42/backend/app/store"
"github.com/umputun/remark42/backend/app/store/engine"
)
const lastLimit = 1000
// MemData implements in-memory data store
type MemData struct {
posts map[string][]store.Comment // key is siteID
metaUsers map[string]metaUser // key is userID
metaPosts map[store.Locator]metaPost // key is post's locator
mu sync.RWMutex
}
type metaPost struct {
PostURL string
SiteID string
ReadOnly bool
}
type metaUser struct {
UserID string
SiteID string
Verified bool
Blocked bool
BlockedUntil time.Time
Details engine.UserDetailEntry
}
// NewMemData makes in-memory engine.
func NewMemData() *MemData {
result := &MemData{
posts: map[string][]store.Comment{},
metaUsers: map[string]metaUser{},
metaPosts: map[store.Locator]metaPost{},
}
return result
}
// Create new comment
func (m *MemData) Create(comment store.Comment) (commentID string, err error) {
if ro, e := m.Flag(engine.FlagRequest{Flag: engine.ReadOnly, Locator: comment.Locator}); e == nil && ro {
return "", errors.Errorf("post %s is read-only", comment.Locator.URL)
}
m.mu.Lock()
defer m.mu.Unlock()
comments := m.posts[comment.Locator.SiteID]
for _, c := range comments { // don't allow duplicated IDs
if c.ID == comment.ID {
return "", errors.New("dup key")
}
}
comments = append(comments, comment)
m.posts[comment.Locator.SiteID] = comments
return comment.ID, nil
}
// Find returns all comments for post and sorts results
func (m *MemData) Find(req engine.FindRequest) (comments []store.Comment, err error) {
m.mu.RLock()
defer m.mu.RUnlock()
comments = []store.Comment{}
if req.Sort == "" {
req.Sort = "time"
}
switch {
case req.Locator.SiteID != "" && req.Locator.URL != "": // find comments for site and url
comments = m.match(m.posts[req.Locator.SiteID], func(c store.Comment) bool {
return c.Locator == req.Locator && (req.Since.IsZero() || c.Timestamp.After(req.Since))
})
case req.Locator.SiteID != "" && req.Locator.URL == "" && req.UserID == "": // find last comments for site
if req.Limit > lastLimit || req.Limit == 0 {
req.Limit = lastLimit
}
if req.Since.IsZero() {
req.Since = time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
}
comments = m.match(m.posts[req.Locator.SiteID], func(c store.Comment) bool {
return !c.Deleted && c.Timestamp.After(req.Since)
})
comments = engine.SortComments(comments, "-time")
if len(comments) > req.Limit {
comments = comments[:req.Limit]
}
return comments, nil
case req.Locator.SiteID != "" && req.UserID != "": // find comments for user
comments = m.match(m.posts[req.Locator.SiteID], func(c store.Comment) bool {
return c.User.ID == req.UserID
})
}
comments = engine.SortComments(comments, req.Sort)
if req.Skip > 0 && req.Skip > len(comments) {
return []store.Comment{}, nil
}
if req.Skip > 0 && req.Skip < len(comments) {
comments = comments[req.Skip:]
}
if req.Limit > 0 && req.Limit < len(comments) {
comments = comments[:req.Limit]
}
return comments, err
}
// Get returns comment for locator.URL and commentID string
func (m *MemData) Get(req engine.GetRequest) (comment store.Comment, err error) {
m.mu.RLock()
defer m.mu.RUnlock()
return m.get(req.Locator, req.CommentID)
}
// Update updates comment for locator.URL with mutable part of comment
func (m *MemData) Update(comment store.Comment) error {
m.mu.Lock()
defer m.mu.Unlock()
return m.updateComment(comment)
}
// Count returns number of comments for post or user
func (m *MemData) Count(req engine.FindRequest) (count int, err error) {
m.mu.RLock()
defer m.mu.RUnlock()
switch {
case req.Locator.URL != "": // comment's count for post
comments := m.match(m.posts[req.Locator.SiteID], func(c store.Comment) bool {
return c.Locator == req.Locator && !c.Deleted
})
return len(comments), nil
case req.UserID != "":
comments := m.match(m.posts[req.Locator.SiteID], func(c store.Comment) bool {
return c.User.ID == req.UserID && !c.Deleted
})
return len(comments), nil
default:
return 0, errors.Errorf("invalid count request %+v", req)
}
}
// Info get post(s) meta info
func (m *MemData) Info(req engine.InfoRequest) (res []store.PostInfo, err error) {
m.mu.RLock()
defer m.mu.RUnlock()
res = []store.PostInfo{}
if req.Locator.URL != "" { // post info
comments := m.match(m.posts[req.Locator.SiteID], func(c store.Comment) bool {
return c.Locator == req.Locator
})
if len(comments) == 0 {
return nil, errors.New("not found")
}
info := store.PostInfo{
URL: req.Locator.URL,
Count: len(comments),
ReadOnly: false,
FirstTS: comments[0].Timestamp.UTC(),
LastTS: comments[len(comments)-1].Timestamp.UTC(),
}
// set read-only from age and manual bucket
info.ReadOnly = req.ReadOnlyAge > 0 && !info.FirstTS.IsZero() &&
info.FirstTS.AddDate(0, 0, req.ReadOnlyAge).Before(time.Now())
if !info.ReadOnly {
v := m.checkFlag(engine.FlagRequest{Flag: engine.ReadOnly, Locator: req.Locator})
info.ReadOnly = v
}
return []store.PostInfo{info}, nil
}
if req.Locator.URL == "" && req.Locator.SiteID != "" { // site info (list)
if req.Limit <= 0 {
req.Limit = 1000
}
if req.Skip < 0 {
req.Skip = 0
}
infoAll := map[store.Locator]store.PostInfo{}
for _, c := range m.posts[req.Locator.SiteID] {
var info store.PostInfo
var ok bool
if info, ok = infoAll[c.Locator]; !ok {
info = store.PostInfo{URL: c.Locator.URL, FirstTS: c.Timestamp.UTC()}
}
info.Count++
info.LastTS = c.Timestamp.UTC()
infoAll[c.Locator] = info
}
for _, v := range infoAll {
res = append(res, v)
}
sort.Slice(res, func(i, j int) bool {
return res[i].URL > res[j].URL
})
if req.Skip > 0 {
if req.Skip >= len(res) {
return []store.PostInfo{}, nil
}
res = res[req.Skip:]
}
if req.Limit > 0 && req.Limit < len(res) {
res = res[:req.Limit]
}
return res, nil
}
return nil, errors.Errorf("invalid info request %+v", req)
}
// Flag sets and gets flag values
func (m *MemData) Flag(req engine.FlagRequest) (val bool, err error) {
m.mu.Lock()
defer m.mu.Unlock()
if req.Update == engine.FlagNonSet { // read flag value, no update requested
return m.checkFlag(req), nil
}
// write flag value
return m.setFlag(req)
}
// ListFlags get list of flagged keys, like blocked & verified user
// works for full locator (post flags) or with userID
func (m *MemData) ListFlags(req engine.FlagRequest) (res []interface{}, err error) {
m.mu.RLock()
defer m.mu.RUnlock()
res = []interface{}{}
switch req.Flag {
case engine.Verified:
for _, u := range m.metaUsers {
if u.SiteID == req.Locator.SiteID {
res = append(res, u.UserID)
}
}
return res, nil
case engine.Blocked:
log.Printf("%+v", m.metaUsers)
for _, u := range m.metaUsers {
if u.SiteID == req.Locator.SiteID && u.Blocked && u.BlockedUntil.After(time.Now()) {
res = append(res, store.BlockedUser{ID: u.UserID, Until: u.BlockedUntil})
}
}
return res, nil
}
return nil, errors.Errorf("flag %s not listable", req.Flag)
}
// UserDetail sets or gets single detail value, or gets all details fo§r requested site.
// UserDetail returns list even for single entry request is a compromise in order to have both single detail getting and setting
// and all site's details listing under the same function (and not to extend engine interface by two separate functions).
func (m *MemData) UserDetail(req engine.UserDetailRequest) ([]engine.UserDetailEntry, error) {
switch req.Detail {
case engine.UserEmail, engine.UserTelegram:
if req.UserID == "" {
return nil, errors.New("userid cannot be empty in request for single detail")
}
m.mu.Lock()
defer m.mu.Unlock()
if req.Update == "" { // read detail value, no update requested
return m.getUserDetail(req)
}
return m.setUserDetail(req)
case engine.AllUserDetails:
// list of all details returned in case request is a read request
// (Update is not set) and does not have UserID or Detail set
if req.Update == "" && req.UserID == "" { // read list of all details
m.mu.Lock()
defer m.mu.Unlock()
return m.listDetails(req.Locator)
}
return nil, errors.New("unsupported request with userdetail all")
default:
return nil, errors.Errorf("unsupported detail %q", req.Detail)
}
}
// Delete post(s), user, comment, user details, or everything
func (m *MemData) Delete(req engine.DeleteRequest) error {
m.mu.Lock()
defer m.mu.Unlock()
switch {
case req.UserDetail != "": // delete user detail
return m.deleteUserDetail(req.Locator, req.UserID, req.UserDetail)
case req.Locator.URL != "" && req.CommentID != "" && req.UserDetail == "": // delete comment
return m.deleteComment(req.Locator, req.CommentID, req.DeleteMode)
case req.Locator.SiteID != "" && req.UserID != "" && req.CommentID == "" && req.UserDetail == "": // delete user
comments := m.match(m.posts[req.Locator.SiteID], func(c store.Comment) bool {
return c.User.ID == req.UserID && !c.Deleted
})
for _, c := range comments {
if e := m.deleteComment(c.Locator, c.ID, req.DeleteMode); e != nil {
return e
}
}
return m.deleteUserDetail(req.Locator, req.UserID, engine.AllUserDetails)
case req.Locator.SiteID != "" && req.Locator.URL == "" && req.CommentID == "" && req.UserID == "" && req.UserDetail == "": // delete site
if _, ok := m.posts[req.Locator.SiteID]; !ok {
return errors.New("not found")
}
m.posts[req.Locator.SiteID] = []store.Comment{}
return nil
}
return errors.Errorf("invalid delete request %+v", req)
}
func (m *MemData) deleteComment(loc store.Locator, id string, mode store.DeleteMode) error {
comments := m.match(m.posts[loc.SiteID], func(c store.Comment) bool {
return c.Locator == loc && c.ID == id
})
if len(comments) == 0 {
return errors.New("not found")
}
comments[0].SetDeleted(mode)
return m.updateComment(comments[0])
}
// Close store
func (m *MemData) Close() error {
return nil
}
func (m *MemData) checkFlag(req engine.FlagRequest) (val bool) {
switch req.Flag {
case engine.Blocked:
if meta, ok := m.metaUsers[req.UserID]; ok {
if meta.SiteID != req.Locator.SiteID {
return false
}
return meta.Blocked && meta.BlockedUntil.After(time.Now())
}
case engine.Verified:
if meta, ok := m.metaUsers[req.UserID]; ok {
if meta.SiteID != req.Locator.SiteID {
return false
}
return meta.Verified
}
case engine.ReadOnly:
if meta, ok := m.metaPosts[req.Locator]; ok {
return meta.ReadOnly
}
}
return false
}
func (m *MemData) setFlag(req engine.FlagRequest) (res bool, err error) {
status := false
if req.Update == engine.FlagTrue {
status = true
}
switch req.Flag {
case engine.Blocked:
until := time.Time{}
if status {
until = time.Now().AddDate(100, 0, 0) // permanent is 100years
if req.TTL > 0 {
until = time.Now().Add(req.TTL)
}
}
meta := metaUser{
UserID: req.UserID,
SiteID: req.Locator.SiteID,
Blocked: status,
BlockedUntil: until,
}
m.metaUsers[req.UserID] = meta
case engine.Verified:
meta := metaUser{
UserID: req.UserID,
SiteID: req.Locator.SiteID,
Verified: status,
}
m.metaUsers[req.UserID] = meta
case engine.ReadOnly:
info, ok := m.metaPosts[req.Locator]
if !ok {
info.SiteID = req.Locator.SiteID
info.PostURL = req.Locator.URL
}
info.ReadOnly = status
m.metaPosts[req.Locator] = info
}
return status, errors.Wrapf(err, "failed to set flag %+v", req)
}
// getUserDetail returns UserDetailEntry with requested userDetail (omitting other details)
// as an only element of the slice.
func (m *MemData) getUserDetail(req engine.UserDetailRequest) ([]engine.UserDetailEntry, error) {
if meta, ok := m.metaUsers[req.UserID]; ok {
if meta.SiteID != req.Locator.SiteID {
return []engine.UserDetailEntry{}, nil
}
switch req.Detail {
case engine.UserEmail:
return []engine.UserDetailEntry{{UserID: req.UserID, Email: meta.Details.Email}}, nil
case engine.UserTelegram:
return []engine.UserDetailEntry{{UserID: req.UserID, Telegram: meta.Details.Telegram}}, nil
}
}
return []engine.UserDetailEntry{}, nil
}
// setUserDetail sets requested userDetail, returning complete updated UserDetailEntry as an onlyIps
// element of the slice in case of success
func (m *MemData) setUserDetail(req engine.UserDetailRequest) ([]engine.UserDetailEntry, error) {
var entry metaUser
if meta, ok := m.metaUsers[req.UserID]; ok {
if meta.SiteID != req.Locator.SiteID {
return []engine.UserDetailEntry{}, nil
}
entry = meta
}
if entry == (metaUser{}) {
entry = metaUser{
UserID: req.UserID,
SiteID: req.Locator.SiteID,
Details: engine.UserDetailEntry{UserID: req.UserID},
}
}
switch req.Detail {
case engine.UserEmail:
entry.Details.Email = req.Update
m.metaUsers[req.UserID] = entry
return []engine.UserDetailEntry{{UserID: req.UserID, Email: req.Update}}, nil
case engine.UserTelegram:
entry.Details.Telegram = req.Update
m.metaUsers[req.UserID] = entry
return []engine.UserDetailEntry{{UserID: req.UserID, Telegram: req.Update}}, nil
}
return []engine.UserDetailEntry{}, nil
}
// listDetails lists all available users details for given siteID
func (m *MemData) listDetails(loc store.Locator) ([]engine.UserDetailEntry, error) {
var res []engine.UserDetailEntry
for _, u := range m.metaUsers {
if u.SiteID == loc.SiteID {
res = append(res, u.Details)
}
}
return res, nil
}
// deleteUserDetail deletes requested UserDetail or whole UserDetailEntry,
// deletion of the absent entry doesn't produce error.
// Trying to delete user with wrong siteID doesn't to anything and doesn't produce error.
func (m *MemData) deleteUserDetail(locator store.Locator, userID string, userDetail engine.UserDetail) error {
var entry metaUser
if meta, ok := m.metaUsers[userID]; ok {
if meta.SiteID != locator.SiteID {
return nil
}
entry = meta
}
if entry == (metaUser{}) || entry.Details == (engine.UserDetailEntry{}) {
// absent entry means that we should not do anything
return nil
}
switch userDetail {
case engine.UserEmail:
entry.Details.Email = ""
case engine.UserTelegram:
entry.Details.Telegram = ""
case engine.AllUserDetails:
entry.Details = engine.UserDetailEntry{UserID: userID}
}
if entry.Details == (engine.UserDetailEntry{UserID: userID}) {
// no user details are stored, empty details entry altogether
entry.Details = engine.UserDetailEntry{}
}
m.metaUsers[userID] = entry
return nil
}
func (m *MemData) get(loc store.Locator, commentID string) (store.Comment, error) {
comments := m.match(m.posts[loc.SiteID], func(c store.Comment) bool {
return c.Locator == loc && c.ID == commentID
})
if len(comments) == 0 {
return store.Comment{}, errors.New("not found")
}
return comments[0], nil
}
func (m *MemData) updateComment(comment store.Comment) error {
comments := m.posts[comment.Locator.SiteID]
for i, c := range comments {
if c.ID != comment.ID || c.Locator != comment.Locator {
continue
}
c.Text = comment.Text
c.Orig = comment.Orig
c.Score = comment.Score
c.Votes = comment.Votes
c.Pin = comment.Pin
c.Deleted = comment.Deleted
c.User = comment.User
comments[i] = c
m.posts[comment.Locator.SiteID] = comments
return nil
}
return errors.New("not found")
}
func (m *MemData) match(comments []store.Comment, fn func(c store.Comment) bool) (res []store.Comment) {
res = []store.Comment{}
for _, c := range comments {
if fn(c) {
res = append(res, c)
}
}
return res
}
@@ -0,0 +1,819 @@
/*
* Copyright 2019 Umputun. All rights reserved.
* Use of this source code is governed by a MIT-style
* license that can be found in the LICENSE file.
*/
package accessor
import (
"fmt"
"sort"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/umputun/remark42/backend/app/store"
"github.com/umputun/remark42/backend/app/store/engine"
)
func TestMemData_CreateAndFind(t *testing.T) {
m := prepMem(t) // adds two comments
req := engine.FindRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, Sort: "time"}
res, err := m.Find(req)
assert.NoError(t, err)
require.Equal(t, 2, len(res))
assert.Equal(t, `some text, <a href="http://radio-t.com">link</a>`, res[0].Text)
assert.Equal(t, "user1", res[0].User.ID)
_, err = m.Create(store.Comment{ID: res[0].ID, Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}})
require.Error(t, err)
assert.Contains(t, err.Error(), "dup key")
id, err := m.Create(store.Comment{ID: "id-3", Locator: store.Locator{URL: "https://radio-t2.com", SiteID: "radio-t2"}})
require.NoError(t, err)
assert.Equal(t, "id-3", id)
req = engine.FindRequest{Locator: store.Locator{URL: "https://radio-t2.com", SiteID: "radio-t2"}, Sort: "time"}
res, err = m.Find(req)
assert.NoError(t, err)
require.Equal(t, 1, len(res))
}
func TestMemData_CreateFailedReadOnly(t *testing.T) {
b := prepMem(t)
comment := store.Comment{
ID: "id-ro",
Text: `some text, <a href="http://radio-t.com">link</a>`,
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
Locator: store.Locator{URL: "https://radio-t.com/ro", SiteID: "radio-t"},
User: store.User{ID: "user1", Name: "user name"},
}
flagReq := engine.FlagRequest{Locator: comment.Locator, Flag: engine.ReadOnly, Update: engine.FlagTrue}
v, err := b.Flag(flagReq)
require.NoError(t, err)
assert.Equal(t, true, v)
_, err = b.Create(comment)
assert.Error(t, err)
assert.Equal(t, "post https://radio-t.com/ro is read-only", err.Error())
flagReq = engine.FlagRequest{Locator: comment.Locator, Flag: engine.ReadOnly, Update: engine.FlagFalse}
v, err = b.Flag(flagReq)
require.NoError(t, err)
assert.Equal(t, false, v)
_, err = b.Create(comment)
assert.NoError(t, err)
}
func TestMemData_Get(t *testing.T) {
b := prepMem(t)
req := engine.FindRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, Sort: "time"}
res, err := b.Find(req)
assert.NoError(t, err)
require.Equal(t, 2, len(res), "2 records initially")
comment, err := b.Get(getReq(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[1].ID))
assert.NoError(t, err)
assert.Equal(t, "some text2", comment.Text)
_, err = b.Get(getReq(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "1234567"))
assert.EqualError(t, err, `not found`)
_, err = b.Get(getReq(store.Locator{URL: "https://radio-t.com", SiteID: "bad"}, res[1].ID))
assert.EqualError(t, err, `not found`)
}
func TestMemData_Update(t *testing.T) {
b := prepMem(t)
req := engine.FindRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, Sort: "time"}
res, err := b.Find(req)
assert.NoError(t, err)
require.Equal(t, 2, len(res), "2 records initially")
comment := res[0]
comment.Text = "abc 123"
comment.Score = 100
err = b.Update(comment)
assert.NoError(t, err)
comment, err = b.Get(getReq(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID))
assert.NoError(t, err)
assert.Equal(t, "abc 123", comment.Text)
assert.Equal(t, res[0].ID, comment.ID)
assert.Equal(t, 100, comment.Score)
comment.Locator.SiteID = "bad"
err = b.Update(comment)
assert.EqualError(t, err, `not found`)
comment.Locator.SiteID = "https://radio-t.com"
comment.Locator.URL = "https://radio-t.com-bad"
err = b.Update(comment)
assert.EqualError(t, err, `not found`)
}
func TestMemData_FindLast(t *testing.T) {
b := prepMem(t)
req := engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "-time"}
res, err := b.Find(req)
assert.NoError(t, err)
require.Equal(t, 2, len(res))
assert.Equal(t, "some text2", res[0].Text)
req.Limit = 1
res, err = b.Find(req)
assert.NoError(t, err)
require.Equal(t, 1, len(res))
assert.Equal(t, "some text2", res[0].Text)
req.Locator.SiteID = "bad"
res, err = b.Find(req)
assert.NoError(t, err)
assert.Equal(t, 0, len(res))
}
func TestMemData_FindLastSince(t *testing.T) {
b := prepMem(t)
ts := time.Date(2017, 12, 20, 15, 18, 21, 0, time.Local)
req := engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "-time", Since: ts}
res, err := b.Find(req)
assert.NoError(t, err)
require.Equal(t, 2, len(res))
assert.Equal(t, "some text2", res[0].Text)
req.Since = time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local)
res, err = b.Find(req)
assert.NoError(t, err)
require.Equal(t, 1, len(res))
assert.Equal(t, "some text2", res[0].Text)
req.Since = time.Date(2017, 12, 20, 16, 18, 22, 0, time.Local)
res, err = b.Find(req)
assert.NoError(t, err)
assert.Equal(t, 0, len(res))
}
func TestMemData_FindForUser(t *testing.T) {
b := prepMem(t)
req := engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "-time", UserID: "user1", Limit: 5}
res, err := b.Find(req)
assert.NoError(t, err)
require.Equal(t, 2, len(res))
assert.Equal(t, "some text2", res[0].Text, "sorted by -time")
req = engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "-time", UserID: "user1", Limit: 1}
res, err = b.Find(req)
assert.NoError(t, err)
require.Equal(t, 1, len(res), "allow 1 comment")
assert.Equal(t, "some text2", res[0].Text, "sorted by -time")
req = engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "-time", UserID: "user1", Limit: 1, Skip: 1}
res, err = b.Find(req)
assert.NoError(t, err)
require.Equal(t, 1, len(res), "allow 1 comment")
assert.Equal(t, `some text, <a href="http://radio-t.com">link</a>`, res[0].Text, "second comment")
req = engine.FindRequest{Locator: store.Locator{SiteID: "bad"}, Sort: "-time", UserID: "user1", Limit: 1, Skip: 1}
res, err = b.Find(req)
assert.NoError(t, err)
assert.Equal(t, 0, len(res), "no comments")
req = engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "-time", UserID: "userZ", Limit: 1, Skip: 1}
res, err = b.Find(req)
assert.NoError(t, err)
assert.Equal(t, 0, len(res), "no comments")
}
func TestMemData_FindForUserPagination(t *testing.T) {
b := NewMemData()
c := store.Comment{
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
User: store.User{ID: "user1", Name: "user name"},
}
// write 200 comments
for i := 0; i < 200; i++ {
c.ID = fmt.Sprintf("idd-%d", i)
c.Text = fmt.Sprintf("text #%d", i)
c.Timestamp = time.Date(2017, 12, 20, 15, 18, i, 0, time.Local)
_, err := b.Create(c)
require.NoError(t, err)
}
// get all comments
req := engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "-time", UserID: "user1"}
res, err := b.Find(req)
assert.NoError(t, err)
require.Equal(t, 200, len(res))
assert.Equal(t, "idd-199", res[0].ID)
// seek 0, 5 comments
req.Limit = 5
res, err = b.Find(req)
assert.NoError(t, err)
require.Equal(t, 5, len(res))
assert.Equal(t, "idd-199", res[0].ID)
assert.Equal(t, "idd-195", res[4].ID)
// seek 10, 3 comments
req.Skip, req.Limit = 10, 3
res, err = b.Find(req)
assert.NoError(t, err)
require.Equal(t, 3, len(res))
assert.Equal(t, "idd-189", res[0].ID)
assert.Equal(t, "idd-187", res[2].ID)
// seek 195, ask 10 comments
req.Skip, req.Limit = 195, 10
res, err = b.Find(req)
assert.NoError(t, err)
require.Equal(t, 5, len(res))
assert.Equal(t, "idd-4", res[0].ID)
assert.Equal(t, "idd-0", res[4].ID)
// seek 255, ask 10 comments
req.Skip, req.Limit = 255, 10
res, err = b.Find(req)
assert.NoError(t, err)
assert.Equal(t, 0, len(res))
}
func TestMemData_CountPost(t *testing.T) {
b := prepMem(t)
req := engine.FindRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}}
c, err := b.Count(req)
assert.NoError(t, err)
require.Equal(t, 2, c)
req = engine.FindRequest{Locator: store.Locator{URL: "https://radio-t.com-xxx", SiteID: "radio-t"}}
c, err = b.Count(req)
assert.NoError(t, err)
assert.Equal(t, 0, c)
req = engine.FindRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "bad"}}
c, err = b.Count(req)
assert.NoError(t, err)
assert.Equal(t, 0, c)
c, err = b.Count(engine.FindRequest{})
assert.Error(t, err)
assert.Equal(t, 0, c)
}
func TestMemData_CountUser(t *testing.T) {
b := prepMem(t)
req := engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1"}
c, err := b.Count(req)
assert.NoError(t, err)
require.Equal(t, 2, c)
req = engine.FindRequest{Locator: store.Locator{SiteID: "bad"}, UserID: "user1"}
c, err = b.Count(req)
assert.NoError(t, err)
assert.Equal(t, 0, c)
req = engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, UserID: "userZ"}
c, err = b.Count(req)
assert.NoError(t, err)
assert.Equal(t, 0, c)
}
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) }
// add one more for https://radio-t.com/2
comment := store.Comment{
ID: "12345",
Text: `some text, <a href="http://radio-t.com">link</a>`,
Timestamp: time.Date(2017, 12, 20, 15, 18, 24, 0, time.Local),
Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"},
User: store.User{ID: "user1", Name: "user name"},
}
_, err := b.Create(comment)
assert.NoError(t, err)
req := engine.InfoRequest{Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"}, ReadOnlyAge: 0}
r, err := b.Info(req)
require.NoError(t, err)
assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com/2", Count: 1, FirstTS: ts(24), LastTS: ts(24)}}, r)
req = engine.InfoRequest{Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"}, ReadOnlyAge: 10}
r, err = b.Info(req)
require.NoError(t, err)
assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com/2", Count: 1,
FirstTS: ts(24), LastTS: ts(24), ReadOnly: true}}, r)
req = engine.InfoRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, ReadOnlyAge: 0}
r, err = b.Info(req)
require.NoError(t, err)
assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com", Count: 2, FirstTS: ts(22), LastTS: ts(23)}}, r)
req = engine.InfoRequest{Locator: store.Locator{URL: "https://radio-t.com/error", SiteID: "radio-t"}, ReadOnlyAge: 0}
_, err = b.Info(req)
require.Error(t, err)
req = engine.InfoRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t-error"}, ReadOnlyAge: 0}
_, err = b.Info(req)
require.Error(t, err)
_, err = b.Info(engine.InfoRequest{})
require.Error(t, err)
fr := engine.FlagRequest{Flag: engine.ReadOnly,
Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"}, Update: engine.FlagTrue}
_, err = b.Flag(fr)
require.NoError(t, err)
req = engine.InfoRequest{Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"}, ReadOnlyAge: 0}
r, err = b.Info(req)
require.NoError(t, err)
assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com/2", Count: 1, FirstTS: ts(24), LastTS: ts(24),
ReadOnly: true}}, r)
}
func TestMemData_InfoList(t *testing.T) {
b := prepMem(t)
// add one more for https://radio-t.com/2
comment := store.Comment{
ID: "12345",
Text: `some text, <a href="http://radio-t.com">link</a>`,
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"},
User: store.User{ID: "user1", Name: "user name"},
}
_, err := b.Create(comment)
assert.NoError(t, err)
ts := func(sec int) time.Time { return time.Date(2017, 12, 20, 15, 18, sec, 0, time.Local).In(time.UTC) }
req := engine.InfoRequest{Locator: store.Locator{SiteID: "radio-t"}}
res, err := b.Info(req)
assert.NoError(t, err)
assert.EqualValues(t, []store.PostInfo{{URL: "https://radio-t.com/2", Count: 1, FirstTS: ts(22), LastTS: ts(22)},
{URL: "https://radio-t.com", Count: 2, FirstTS: ts(22), LastTS: ts(23)}}, res)
req = engine.InfoRequest{Locator: store.Locator{SiteID: "radio-t"}, Limit: -1, Skip: -1}
res, err = b.Info(req)
assert.NoError(t, err)
assert.EqualValues(t, []store.PostInfo{{URL: "https://radio-t.com/2", Count: 1, FirstTS: ts(22), LastTS: ts(22)},
{URL: "https://radio-t.com", Count: 2, FirstTS: ts(22), LastTS: ts(23)}}, res)
req = engine.InfoRequest{Locator: store.Locator{SiteID: "radio-t"}, Limit: 1}
res, err = b.Info(req)
assert.NoError(t, err)
assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com/2", Count: 1, FirstTS: ts(22), LastTS: ts(22)}}, res)
req = engine.InfoRequest{Locator: store.Locator{SiteID: "radio-t"}, Limit: 1, Skip: 1}
res, err = b.Info(req)
assert.NoError(t, err)
assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com", Count: 2, FirstTS: ts(22), LastTS: ts(23)}}, res)
req = engine.InfoRequest{Locator: store.Locator{SiteID: "bad"}, Limit: 1, Skip: 1}
res, err = b.Info(req)
assert.NoError(t, err)
assert.Equal(t, []store.PostInfo{}, res)
}
func TestMemData_FlagBlockedUser(t *testing.T) {
b := prepMem(t)
req := engine.FlagRequest{Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1"}
val, err := b.Flag(req)
assert.NoError(t, err)
assert.False(t, val, "nothing blocked yet")
req = engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1",
Update: engine.FlagTrue}
_, err = b.Flag(req)
assert.NoError(t, err)
val, err = b.Flag(engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1"})
assert.NoError(t, err)
assert.True(t, val, "user1 blocked")
req = engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1",
Update: engine.FlagTrue}
_, err = b.Flag(req)
assert.NoError(t, err)
val, err = b.Flag(engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1"})
assert.NoError(t, err)
assert.True(t, val, "user1 still blocked")
req = engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1",
Update: engine.FlagFalse}
_, err = b.Flag(req)
assert.NoError(t, err)
val, err = b.Flag(engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1"})
assert.NoError(t, err)
assert.False(t, val, "user1 unblocked")
}
func TestMemData_FlagReadOnlyPost(t *testing.T) {
b := prepMem(t)
req := engine.FlagRequest{Locator: store.Locator{SiteID: "radio-t", URL: "url-1"}, Flag: engine.ReadOnly}
val, err := b.Flag(req)
assert.NoError(t, err)
assert.False(t, val, "nothing ro")
req = engine.FlagRequest{Locator: store.Locator{SiteID: "radio-t", URL: "url-1"}, Flag: engine.ReadOnly,
Update: engine.FlagTrue}
val, err = b.Flag(req)
assert.NoError(t, err)
assert.True(t, val)
req = engine.FlagRequest{Locator: store.Locator{SiteID: "radio-t", URL: "url-1"}, Flag: engine.ReadOnly}
val, err = b.Flag(req)
assert.NoError(t, err)
assert.True(t, val, "url-1 ro")
req = engine.FlagRequest{Locator: store.Locator{SiteID: "radio-t", URL: "url-2"}, Flag: engine.ReadOnly}
val, err = b.Flag(req)
assert.NoError(t, err)
assert.False(t, val, "url-2 still writable")
req = engine.FlagRequest{Locator: store.Locator{SiteID: "radio-t", URL: "url-1"}, Flag: engine.ReadOnly,
Update: engine.FlagFalse}
_, err = b.Flag(req)
assert.NoError(t, err)
req = engine.FlagRequest{Locator: store.Locator{SiteID: "radio-t", URL: "url-1"}, Flag: engine.ReadOnly}
val, err = b.Flag(req)
assert.NoError(t, err)
assert.False(t, val, "url-1 writable")
}
func TestMemData_FlagVerified(t *testing.T) {
b := prepMem(t)
isVerified := func(site, user string) bool {
req := engine.FlagRequest{Flag: engine.Verified, Locator: store.Locator{SiteID: site}, UserID: user}
v, err := b.Flag(req)
require.NoError(t, err)
return v
}
setVerified := func(site, user string, status engine.FlagStatus) error {
req := engine.FlagRequest{Flag: engine.Verified, Locator: store.Locator{SiteID: site}, UserID: user, Update: status}
_, err := b.Flag(req)
return err
}
assert.False(t, isVerified("radio-t", "u1"), "nothing verified")
assert.NoError(t, setVerified("radio-t", "u1", engine.FlagTrue))
assert.True(t, isVerified("radio-t", "u1"), "u1 verified")
assert.False(t, isVerified("radio-t", "u2"), "u2 still not verified")
assert.NoError(t, setVerified("radio-t", "u1", engine.FlagFalse))
assert.False(t, isVerified("radio-t", "u1"), "u1 not verified anymore")
assert.NoError(t, setVerified("bad", "u1", engine.FlagTrue))
assert.NoError(t, setVerified("radio-t", "u1xyz", engine.FlagFalse))
assert.False(t, isVerified("radio-t-bad", "u1"), "nothing verified on wrong site")
assert.NoError(t, setVerified("radio-t", "u1", engine.FlagTrue))
assert.NoError(t, setVerified("radio-t", "u2", engine.FlagTrue))
assert.NoError(t, setVerified("radio-t", "u3", engine.FlagFalse))
}
func TestMemData_FlagListVerified(t *testing.T) {
b := prepMem(t)
toIDs := func(inp []interface{}) (res []string) {
res = make([]string, len(inp))
for i, v := range inp {
vv, ok := v.(string)
require.True(t, ok)
res[i] = vv
}
sort.Strings(res)
return res
}
setVerified := func(site, user string, status engine.FlagStatus) error {
req := engine.FlagRequest{Flag: engine.Verified, Locator: store.Locator{SiteID: site}, UserID: user, Update: status}
_, err := b.Flag(req)
return err
}
ids, err := b.ListFlags(engine.FlagRequest{Flag: engine.Verified, Locator: store.Locator{SiteID: "radio-t"}})
assert.NoError(t, err)
assert.Equal(t, []string{}, toIDs(ids), "verified list empty")
assert.NoError(t, setVerified("radio-t", "u1", engine.FlagTrue))
assert.NoError(t, setVerified("radio-t", "u2", engine.FlagTrue))
ids, err = b.ListFlags(engine.FlagRequest{Flag: engine.Verified, Locator: store.Locator{SiteID: "radio-t"}})
assert.NoError(t, err)
assert.EqualValues(t, []string{"u1", "u2"}, toIDs(ids), "verified 2 ids")
ids, err = b.ListFlags(engine.FlagRequest{Flag: engine.Verified, Locator: store.Locator{SiteID: "radio-t-bad"}})
assert.NoError(t, err)
assert.Equal(t, 0, len(ids))
ids, err = b.ListFlags(engine.FlagRequest{})
assert.Error(t, err)
assert.Equal(t, 0, len(ids))
}
func TestMemData_FlagListBlocked(t *testing.T) {
b := prepMem(t)
setBlocked := func(site, user string, status engine.FlagStatus, ttl time.Duration) error {
req := engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: site}, UserID: user, Update: status,
TTL: ttl}
_, err := b.Flag(req)
return err
}
toBlocked := func(inp []interface{}) (res []store.BlockedUser) {
res = make([]store.BlockedUser, len(inp))
for i, v := range inp {
vv, ok := v.(store.BlockedUser)
require.True(t, ok)
res[i] = vv
}
return res
}
assert.NoError(t, setBlocked("radio-t", "user1", engine.FlagTrue, 0))
assert.NoError(t, setBlocked("radio-t", "user2", engine.FlagTrue, 50*time.Millisecond))
assert.NoError(t, setBlocked("radio-t", "user3", engine.FlagFalse, 0))
vv, err := b.ListFlags(engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "radio-t"}})
assert.NoError(t, err)
blockedList := toBlocked(vv)
var blockedIds = make([]string, len(blockedList))
for i, x := range blockedList {
blockedIds[i] = x.ID
}
require.Equal(t, 2, len(blockedList), b.metaUsers)
assert.ElementsMatch(t, []string{"user1", "user2"}, blockedIds)
t.Logf("%+v", blockedList)
// check block expiration
time.Sleep(50 * time.Millisecond)
vv, err = b.ListFlags(engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "radio-t"}})
assert.NoError(t, err)
blockedList = toBlocked(vv)
require.Equal(t, 1, len(blockedList))
assert.Equal(t, "user1", blockedList[0].ID)
vv, err = b.ListFlags(engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "bad"}})
assert.NoError(t, err)
assert.Equal(t, 0, len(vv))
}
func TestMemData_DeleteComment(t *testing.T) {
b := prepMem(t)
reqReq := engine.FindRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, Sort: "time"}
res, err := b.Find(reqReq)
assert.NoError(t, err)
require.Equal(t, 2, len(res), "initially 2 comments")
count, err := b.Count(reqReq)
require.NoError(t, err)
require.Equal(t, 2, count, "count=2 initially")
delReq := engine.DeleteRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
CommentID: res[0].ID, DeleteMode: store.SoftDelete}
err = b.Delete(delReq)
assert.NoError(t, err)
res, err = b.Find(reqReq)
assert.NoError(t, err)
require.Equal(t, 2, len(res))
assert.Equal(t, "", res[0].Text)
assert.True(t, res[0].Deleted, "marked deleted")
assert.Equal(t, store.User{Name: "user name", ID: "user1", Picture: "", Admin: false, Blocked: false, IP: ""}, res[0].User)
assert.Equal(t, "some text2", res[1].Text)
assert.False(t, res[1].Deleted)
comments, err := b.Find(engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Limit: 10})
assert.NoError(t, err)
assert.Equal(t, 1, len(comments), "1 in last, 1 removed")
count, err = b.Count(reqReq)
require.NoError(t, err)
assert.Equal(t, 1, count)
delReq.CommentID = "123456"
err = b.Delete(delReq)
assert.Error(t, err)
delReq.Locator.SiteID = "bad"
delReq.CommentID = res[0].ID
err = b.Delete(delReq)
assert.EqualError(t, err, `not found`)
delReq.Locator = store.Locator{URL: "https://radio-t.com/bad", SiteID: "radio-t"}
err = b.Delete(delReq)
assert.EqualError(t, err, `not found`)
err = b.Delete(engine.DeleteRequest{Locator: store.Locator{SiteID: "bad"}})
assert.Error(t, err)
}
func TestMemData_Close(t *testing.T) {
b := prepMem(t)
assert.NoError(t, b.Close())
}
func TestMemData_DeleteHard(t *testing.T) {
b := prepMem(t)
reqReq := engine.FindRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, Sort: "time"}
res, err := b.Find(reqReq)
assert.NoError(t, err)
assert.Equal(t, 2, len(res), "initially 2 comments")
delReq := engine.DeleteRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
CommentID: res[0].ID, DeleteMode: store.HardDelete}
err = b.Delete(delReq)
assert.NoError(t, err)
res, err = b.Find(reqReq)
assert.NoError(t, err)
require.Equal(t, 2, len(res))
assert.Equal(t, "", res[0].Text)
assert.True(t, res[0].Deleted, "marked deleted")
assert.Equal(t, store.User{Name: "deleted", ID: "deleted", Picture: "", Admin: false, Blocked: false, IP: ""}, res[0].User)
}
func TestMemData_DeleteAll(t *testing.T) {
b := prepMem(t)
delReq := engine.DeleteRequest{Locator: store.Locator{SiteID: "radio-t"}}
err := b.Delete(delReq)
assert.NoError(t, err)
comments, err := b.Find(engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Limit: 10})
assert.NoError(t, err)
assert.Equal(t, 0, len(comments), "nothing left")
}
func TestMemData_UserDetailAll(t *testing.T) {
b := prepMem(t)
val, err := b.UserDetail(engine.UserDetailRequest{Locator: store.Locator{SiteID: "test-site"}, Detail: engine.AllUserDetails})
require.NoError(t, err)
require.Nil(t, val)
}
func TestMemData_UserDetailErrors(t *testing.T) {
b := prepMem(t)
val, err := b.UserDetail(engine.UserDetailRequest{Locator: store.Locator{SiteID: "test-site"}, Detail: engine.UserEmail, Update: "value1"})
require.EqualError(t, err, "userid cannot be empty in request for single detail")
require.Nil(t, val)
val, err = b.UserDetail(engine.UserDetailRequest{Locator: store.Locator{SiteID: "test-site"}, Detail: engine.AllUserDetails, Update: "value1"})
require.EqualError(t, err, "unsupported request with userdetail all")
require.Nil(t, val)
val, err = b.UserDetail(engine.UserDetailRequest{Locator: store.Locator{SiteID: "test-site"}, Detail: "bad"})
require.EqualError(t, err, "unsupported detail \"bad\"")
require.Nil(t, val)
}
func TestMemData_DeleteUserDetail(t *testing.T) {
var (
createEmailUser = engine.UserDetailRequest{Locator: store.Locator{SiteID: "test-site"}, UserID: "user1", Detail: engine.UserEmail, Update: "value1"}
readEmailUser = engine.UserDetailRequest{Locator: store.Locator{SiteID: "test-site"}, UserID: "user1", Detail: engine.UserEmail}
createTelegramUser = engine.UserDetailRequest{Locator: store.Locator{SiteID: "test-site"}, UserID: "user1", Detail: engine.UserTelegram, Update: "value1"}
readTelegramUser = engine.UserDetailRequest{Locator: store.Locator{SiteID: "test-site"}, UserID: "user1", Detail: engine.UserTelegram}
emailSet = []engine.UserDetailEntry{{UserID: "user1", Email: "value1"}}
emailUnset = []engine.UserDetailEntry{{UserID: "user1", Email: ""}}
telegramSet = []engine.UserDetailEntry{{UserID: "user1", Telegram: "value1"}}
telegramUnset = []engine.UserDetailEntry{{UserID: "user1", Telegram: ""}}
)
b := prepMem(t)
var testData = []struct {
delReq engine.DeleteRequest
detailReq engine.UserDetailRequest
expected []engine.UserDetailEntry
}{
{delReq: engine.DeleteRequest{Locator: store.Locator{SiteID: "test-site"}, UserID: "user1", UserDetail: engine.UserEmail},
detailReq: createEmailUser, expected: emailSet},
{delReq: engine.DeleteRequest{Locator: store.Locator{SiteID: "bad"}, UserID: "user1", UserDetail: engine.UserEmail},
detailReq: readEmailUser, expected: emailSet},
{delReq: engine.DeleteRequest{Locator: store.Locator{SiteID: "test-site"}, UserID: "user1", UserDetail: engine.UserEmail},
detailReq: readEmailUser, expected: emailUnset},
{delReq: engine.DeleteRequest{Locator: store.Locator{SiteID: "test-site"}, UserID: "user1", UserDetail: engine.UserTelegram},
detailReq: createTelegramUser, expected: telegramSet},
{delReq: engine.DeleteRequest{Locator: store.Locator{SiteID: "bad"}, UserID: "user1", UserDetail: engine.UserTelegram},
detailReq: readTelegramUser, expected: telegramSet},
{delReq: engine.DeleteRequest{Locator: store.Locator{SiteID: "test-site"}, UserID: "user1", UserDetail: engine.UserTelegram},
detailReq: readTelegramUser, expected: telegramUnset},
{delReq: engine.DeleteRequest{Locator: store.Locator{SiteID: "test-site"}, UserID: "user1", UserDetail: engine.AllUserDetails},
detailReq: createEmailUser, expected: emailSet},
{delReq: engine.DeleteRequest{Locator: store.Locator{SiteID: "test-site"}, UserID: "user1", UserDetail: engine.AllUserDetails},
detailReq: readEmailUser, expected: emailUnset},
}
for i, x := range testData {
err := b.Delete(x.delReq)
require.NoError(t, err, "delete request #%d error", i)
val, err := b.UserDetail(x.detailReq)
require.NoError(t, err, "user request #%d error", i)
require.Equal(t, x.expected, val, "user request #%d result", i)
}
}
func TestMemAdmin_DeleteUserHard(t *testing.T) {
b := prepMem(t)
err := b.Delete(engine.DeleteRequest{Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1",
DeleteMode: store.HardDelete})
require.NoError(t, err)
comments, err := b.Find(engine.FindRequest{Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com"},
Sort: "time"})
assert.NoError(t, err)
require.Equal(t, 2, len(comments), "2 comments with deleted info")
assert.Equal(t, store.User{Name: "deleted", ID: "deleted", Picture: "", Admin: false, Blocked: false, IP: ""}, comments[0].User)
assert.Equal(t, store.User{Name: "deleted", ID: "deleted", Picture: "", Admin: false, Blocked: false, IP: ""}, comments[1].User)
c, err := b.Count(engine.FindRequest{Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com"}})
assert.NoError(t, err)
assert.Equal(t, 0, c, "0 count")
_, err = b.Find(engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1", Limit: 5})
assert.NoError(t, err, "no comments for user user1 in store")
comments, err = b.Find(engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "time"})
assert.NoError(t, err)
assert.Equal(t, 0, len(comments), "nothing left")
}
func TestMemAdmin_DeleteUserSoft(t *testing.T) {
b := prepMem(t)
err := b.Delete(engine.DeleteRequest{Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1",
DeleteMode: store.SoftDelete})
require.NoError(t, err)
comments, err := b.Find(engine.FindRequest{Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com"},
Sort: "time"})
assert.NoError(t, err)
require.Equal(t, 2, len(comments), "2 comments with deleted info")
assert.Equal(t, store.User{Name: "user name", ID: "user1", Picture: "", Admin: false, Blocked: false, IP: ""}, comments[0].User)
assert.Equal(t, store.User{Name: "user name", ID: "user1", Picture: "", Admin: false, Blocked: false, IP: ""}, comments[1].User)
c, err := b.Count(engine.FindRequest{Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com"}})
assert.NoError(t, err)
assert.Equal(t, 0, c, "0 count")
comments, err = b.Find(engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1", Limit: 5})
assert.NoError(t, err, "no comments for user user1 in store")
require.Equal(t, 2, len(comments), "2 comments with deleted info")
assert.True(t, comments[0].Deleted)
assert.True(t, comments[1].Deleted)
assert.Equal(t, "", comments[0].Text)
assert.Equal(t, "", comments[1].Text)
comments, err = b.Find(engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "time"})
assert.NoError(t, err)
assert.Equal(t, 0, len(comments), "nothing left")
}
func prepMem(t *testing.T) *MemData {
m := NewMemData()
comment := store.Comment{
ID: "id-1",
Text: `some text, <a href="http://radio-t.com">link</a>`,
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
User: store.User{ID: "user1", Name: "user name"},
}
_, err := m.Create(comment)
require.NoError(t, err)
comment = store.Comment{
ID: "id-2",
Text: "some text2",
Timestamp: time.Date(2017, 12, 20, 15, 18, 23, 0, time.Local),
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
User: store.User{ID: "user1", Name: "user name"},
}
_, err = m.Create(comment)
require.NoError(t, err)
return m
}
func getReq(locator store.Locator, commentID string) engine.GetRequest {
return engine.GetRequest{
Locator: locator,
CommentID: commentID,
}
}
@@ -0,0 +1,124 @@
/*
* Copyright 2020 Umputun. All rights reserved.
* Use of this source code is governed by a MIT-style
* license that can be found in the LICENSE file.
*/
package accessor
import (
"context"
"sync"
"time"
log "github.com/go-pkgz/lgr"
"github.com/pkg/errors"
"github.com/umputun/remark42/backend/app/store/image"
)
// MemImage implements image.Store with memory backend
type MemImage struct {
imagesStaging map[string][]byte
images map[string][]byte
insertTime map[string]time.Time
mu sync.RWMutex
}
// NewMemImageStore makes admin Store in memory.
func NewMemImageStore() *MemImage {
log.Print("[DEBUG] make memory image store")
return &MemImage{
imagesStaging: map[string][]byte{},
images: map[string][]byte{},
insertTime: map[string]time.Time{},
}
}
// Save stores image with passed id to staging
func (m *MemImage) Save(id string, img []byte) error {
m.mu.Lock()
m.imagesStaging[id] = img
m.insertTime[id] = time.Now()
m.mu.Unlock()
return nil
}
// ResetCleanupTimer resets cleanup timer for the image
func (m *MemImage) ResetCleanupTimer(id string) error {
m.mu.Lock()
defer m.mu.Unlock()
if _, ok := m.insertTime[id]; ok {
m.insertTime[id] = time.Now()
return nil
}
return errors.Errorf("image %s not found", id)
}
// Load image by ID
func (m *MemImage) Load(id string) ([]byte, error) {
m.mu.RLock()
img, ok := m.images[id]
if !ok {
img, ok = m.imagesStaging[id]
}
m.mu.RUnlock()
if !ok {
return nil, errors.Errorf("image %s not found", id)
}
return img, nil
}
// Commit moves image from staging to permanent
func (m *MemImage) Commit(id string) error {
m.mu.RLock()
img, ok := m.imagesStaging[id]
m.mu.RUnlock()
if !ok {
return errors.Errorf("failed to commit %s, not found in staging", id)
}
m.mu.Lock()
m.images[id] = img
m.mu.Unlock()
return nil
}
// Cleanup runs removal loop for old images on staging
func (m *MemImage) Cleanup(_ context.Context, ttl time.Duration) error {
var idsToRemove []string
m.mu.RLock()
for id, t := range m.insertTime {
age := time.Since(t)
if age > ttl {
log.Printf("[INFO] remove staging image %s, age %v", id, age)
idsToRemove = append(idsToRemove, id)
}
}
m.mu.RUnlock()
m.mu.Lock()
for _, id := range idsToRemove {
delete(m.insertTime, id)
delete(m.imagesStaging, id)
}
m.mu.Unlock()
return nil
}
// Info returns meta information about storage
func (m *MemImage) Info() (image.StoreInfo, error) {
var ts time.Time
m.mu.RLock()
for _, t := range m.insertTime {
if ts.IsZero() || t.Before(ts) {
ts = t
}
}
m.mu.RUnlock()
return image.StoreInfo{FirstStagingImageTS: ts}, nil
}
@@ -0,0 +1,103 @@
/*
* Copyright 2020 Umputun. All rights reserved.
* Use of this source code is governed by a MIT-style
* license that can be found in the LICENSE file.
*/
package accessor
import (
"context"
"encoding/base64"
"io"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
// gopher png for test, from https://golang.org/src/image/png/example_test.go
const gopher = "iVBORw0KGgoAAAANSUhEUgAAAEsAAAA8CAAAAAALAhhPAAAFfUlEQVRYw62XeWwUVRzHf2" +
"+OPbo9d7tsWyiyaZti6eWGAhISoIGKECEKCAiJJkYTiUgTMYSIosYYBBIUIxoSPIINEBDi2VhwkQrVsj1ESgu9doHWdrul7ba" +
"73WNm3vOPtsseM9MdwvvrzTs+8/t95ze/33sI5BqiabU6m9En8oNjduLnAEDLUsQXFF8tQ5oxK3vmnNmDSMtrncks9Hhtt" +
"/qeWZapHb1ha3UqYSWVl2ZmpWgaXMXGohQAvmeop3bjTRtv6SgaK/Pb9/bFzUrYslbFAmHPp+3WhAYdr+7GN/YnpN46Opv55VDs" +
"JkoEpMrY/vO2BIYQ6LLvm0ThY3MzDzzeSJeeWNyTkgnIE5ePKsvKlcg/0T9QMzXalwXMlj54z4c0rh/mzEfr+FgWEz2w6uk" +
"8dkzFAgcARAgNp1ZYef8bH2AgvuStbc2/i6CiWGj98y2tw2l4FAXKkQBIf+exyRnteY83LfEwDQAYCoK+P6bxkZm/0966LxcAA" +
"ILHB56kgD95PPxltuYcMtFTWw/FKkY/6Opf3GGd9ZF+Qp6mzJxzuRSractOmJrH1u8XTvWFHINNkLQLMR+XHXvfPPHw967raE1xxwtA36I" +
"MRfkAAG29/7mLuQcb2WOnsJReZGfpiHsSBX81cvMKywYZHhX5hFPtOqPGWZCXnhWGAu6lX91ElKXSalcLXu3UaOXVay57ZSe5f6Gpx7J2" +
"MXAsi7EqSp09b/MirKSyJfnfEEgeDjl8FgDAfvewP03zZ+AJ0m9aFRM8eEHBDRKjfcreDXnZdQuAxXpT2NRJ7xl3UkLBhuVGU16gZiGOgZm" +
"rSbRdqkILuL/yYoSXHHkl9KXgqNu3PB8oRg0geC5vFmLjad6mUyTKLmF3OtraWDIfACyXqmephaDABawfpi6tqqBZytfQMqOz6S09iWXhkt" +
"rRaB8Xz4Yi/8gyABDm5NVe6qq/3VzPrcjELWrebVuyY2T7ar4zQyybUCtsQ5Es1FGaZVrRVQwAgHGW2ZCRZshI5bGQi7HesyE972pOSeMM0" +
"dSktlzxRdrlqb3Osa6CCS8IJoQQQgBAbTAa5l5epO34rJszibJI8rxLfGzcp1dRosutGeb2VDNgqYrwTiPNsLxXiPi3dz7LiS1WBRBDBOnqEj" +
"yy3aQb+/bLiJzz9dIkscVBBLxMfSEac7kO4Fpkngi0ruNBeSOal+u8jgOuqPz12nryMLCniEjtOOOmpt+KEIqsEdocJjYXwrh9OZqWJQyPCTo67" +
"LNS/TdxLAv6R5ZNK9npEjbYdT33gRo4o5oTqR34R+OmaSzDBWsAIPhuRcgyoteNi9gF0KzNYWVItPf2TLoXEg+7isNC7uJkgo1iQWOfRSP9NR" +
"11RtbZZ3OMG/VhL6jvx+J1m87+RCfJChAtEBQkSBX2PnSiihc/Twh3j0h7qdYQAoRVsRGmq7HU2QRbaxVGa1D6nIOqaIWRjyRZpHMQKWKpZM5fe" +
"A+lzC4ZFultV8S6T0mzQGhQohi5I8iw+CsqBSxhFMuwyLgSwbghGb0AiIKkSDmGZVmJSiKihsiyOAUs70UkywooYP0bii9GdH4sfr1UNysd3fU" +
"yLLMQN+rsmo3grHl9VNJHbbwxoa47Vw5gupIqrZcjPh9R4Nye3nRDk199V+aetmvVtDRE8/+cbgAAgMIWGb3UA0MGLE9SCbWX670TDy" +
"1y98c3D27eppUjsZ6fql3jcd5rUe7+ZIlLNQny3Rd+E5Tct3WVhTM5RBCEdiEK0b6B+/ca2gYU393nFj/n1AygRQxPIUA043M42u85+z2S" +
"nssKrPl8Mx76NL3E6eXc3be7OD+H4WHbJkKI8AU8irbITQjZ+0hQcPEgId/Fn/pl9crKH02+5o2b9T/eMx7pKoskYgAAAABJRU5ErkJggg=="
func gopherPNG() io.Reader { return base64.NewDecoder(base64.StdEncoding, strings.NewReader(gopher)) }
func TestMemImage_LoadAfterSave(t *testing.T) {
svc := NewMemImageStore()
gopher, err := io.ReadAll(gopherPNG())
assert.NoError(t, err)
img, err := svc.Load("test_id")
assert.EqualError(t, err, "image test_id not found")
assert.Empty(t, img)
id := "test_img"
err = svc.Save(id, gopher)
assert.NoError(t, err)
img, err = svc.Load(id)
assert.NoError(t, err)
assert.Equal(t, gopher, img)
svc.ResetCleanupTimer(id)
err = svc.Commit(id)
assert.NoError(t, err)
err = svc.Cleanup(context.TODO(), 0)
assert.NoError(t, err)
img, err = svc.Load(id)
assert.NoError(t, err)
assert.Equal(t, gopher, img)
}
func TestMemImage_CommitFail(t *testing.T) {
svc := NewMemImageStore()
err := svc.Commit("test_id")
assert.EqualError(t, err, "failed to commit test_id, not found in staging")
}
func TestMemImage_Cleanup(t *testing.T) {
svc := NewMemImageStore()
err := svc.Cleanup(context.TODO(), time.Minute)
assert.NoError(t, err)
}
func TestMemImage_Info(t *testing.T) {
svc := NewMemImageStore()
gopher, err := io.ReadAll(gopherPNG())
assert.NoError(t, err)
// get info on empty storage, should be zero
info, err := svc.Info()
assert.NoError(t, err)
assert.True(t, info.FirstStagingImageTS.IsZero())
// save image
err = svc.Save("test_img", gopher)
assert.NoError(t, err)
// get info after saving, should be non-zero
info, err = svc.Info()
assert.NoError(t, err)
assert.False(t, info.FirstStagingImageTS.IsZero())
}
@@ -0,0 +1,64 @@
# compose file demonstrating custom storage use. The memory_store (see backend/_example/memory_store) starts
# in a separate container and remark42 communicates to mem_store.r42 via STORE_RPC_API url
version: "2"
services:
remark42:
build:
context: ../../..
dockerfile: Dockerfile
args:
- SKIP_BACKEND_TEST=true
- SKIP_FRONTEND_TEST=true
image: umputun/remark42:dev
container_name: "remark42-dev"
hostname: "remark42-dev"
restart: always
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
ports:
- "8080:8080" # primary rest server
- "8084:8084" # local oauth2 server
environment:
- REMARK_URL=http://127.0.0.1:8080
- SECRET=123456
- BACKUP_PATH=/srv/var/backup
- DEBUG=true
- EMOJI=true
- AUTH_ANON=true
- AUTH_DEV=true
- ADMIN_TYPE=rpc
- ADMIN_RPC_API=http://mem_store.r42:8080/cmd
- STORE_TYPE=rpc
- STORE_RPC_API=http://mem_store.r42:8080/cmd
- IMAGE_TYPE=rpc
- IMAGE_RPC_API=http://mem_store.r42:8080/cmd
volumes:
- ../../../var:/srv/var
mem_store.r42:
image: umputun/mem_store.r42
build:
context: ../../..
dockerfile: backend/_example/memory_store/Dockerfile
container_name: "mem_store.r42"
hostname: "mem_store.r42"
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
environment:
- API=/cmd
- SECRET=123456
- DEBUG=true
+14
View File
@@ -0,0 +1,14 @@
module github.com/umputun/remark42/memory_store
go 1.16
require (
github.com/go-pkgz/jrpc v0.2.0
github.com/go-pkgz/lgr v0.10.4
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.7.0
github.com/umputun/go-flags v1.5.1
github.com/umputun/remark42/backend v1.8.1
)
replace github.com/umputun/remark42/backend => ../../
+786
View File
@@ -0,0 +1,786 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=
cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Depado/bfchroma v1.3.0 h1:zz14vpvySU6S0CL6yGPr1vkFevQecIt8dJdCsMS2JpM=
github.com/Depado/bfchroma v1.3.0/go.mod h1:c0bFk0tFmT+clD3TIGurjWCfD/QV8/EebfM3JGr+98M=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
github.com/alecthomas/chroma v0.7.3/go.mod h1:sko8vR34/90zvl5QdcUdvzL3J8NKjAUx9va9jPuFNoM=
github.com/alecthomas/chroma v0.9.4 h1:YL7sOAE3p8HS96T9km7RgvmsZIctqbK1qJ0b7hzed44=
github.com/alecthomas/chroma v0.9.4/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
github.com/alecthomas/kong v0.2.4/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
github.com/alecthomas/repr v0.0.0-20200325044227-4184120f674c/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/alicebob/miniredis/v2 v2.11.4/go.mod h1:VL3UDEfAH59bSa7MuHMuFToxkqyHh69s/WUbYlOAuyg=
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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.0/go.mod h1:8pFdfPkv/jr8mkChVbNVuJ0suiHe278BtWI4Tk1ujxk=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/didip/tollbooth/v6 v6.0.1/go.mod h1:j2pKs+JQ5PvU/K4jFnrnwntrmfUbYLJE5oSdxR37FD0=
github.com/didip/tollbooth/v6 v6.1.1 h1:Nt7PvWLa9Y94OrykXsFNBinVRQIu8xdy4avpl99Dc1M=
github.com/didip/tollbooth/v6 v6.1.1/go.mod h1:xjcse6CTHCLuOkzsWrEgdy9WPJFv+p/x6v+MyfP+O9s=
github.com/didip/tollbooth_chi v0.0.0-20200524181329-8b84cd7183d9/go.mod h1:YWyIfq3y4ArRfWZ9XksmuusP+7Mad+T0iFZ0kv0XG/M=
github.com/didip/tollbooth_chi v0.0.0-20200828173446-a7173453ea21 h1:x7YpwKSBIBcKe9I3aTNOqgSyJ6QKDdtOxnEkxBTsi9w=
github.com/didip/tollbooth_chi v0.0.0-20200828173446-a7173453ea21/go.mod h1:0ZVa6kSzS011nfTC1rELyxK4tjVf6vqBnOv7oY2KlsA=
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
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/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-chi/chi v4.1.1+incompatible h1:MmTgB0R8Bt/jccxp+t6S/1VGIKdJw5J74CK/c9tTfA4=
github.com/go-chi/chi v4.1.1+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-chi/chi/v5 v5.0.5/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/cors v1.2.0/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-pkgz/auth v1.18.1-0.20211207184750-78e5d290333d/go.mod h1:N1VM53c22Uw0UQSYgdTOBK1D2vtPjSc/RWGf/Fhc1y8=
github.com/go-pkgz/expirable-cache v0.0.3 h1:rTh6qNPp78z0bQE6HDhXBHUwqnV9i09Vm6dksJLXQDc=
github.com/go-pkgz/expirable-cache v0.0.3/go.mod h1:+IauqN00R2FqNRLCLA+X5YljQJrwB179PfiAoMPlTlQ=
github.com/go-pkgz/jrpc v0.2.0 h1:CLy/eZyekjraVrxZV18N2R1mYLMJ/nWrgdfyIOGPY/E=
github.com/go-pkgz/jrpc v0.2.0/go.mod h1:wd8vtQ4CgtCnuqua6x2b1SKIgv0VSOh5Dn0uUITbiUE=
github.com/go-pkgz/lcw v0.8.1/go.mod h1:Xw0/ZfApATgbjVPYRZO4XHdWyxAjErDWDWJ7TLlw1Vc=
github.com/go-pkgz/lgr v0.10.4 h1:l7qyFjqEZgwRgaQQSEp6tve4A3OU80VrfzpvtEX8ngw=
github.com/go-pkgz/lgr v0.10.4/go.mod h1:CD0s1z6EFpIUplV067gitF77tn25JItzwHNKAPqeCF0=
github.com/go-pkgz/repeater v1.1.3/go.mod h1:hVTavuO5x3Gxnu8zW7d6sQBfAneKV8X2FjU48kGfpKw=
github.com/go-pkgz/rest v1.5.0/go.mod h1:nQaM3RhSTUAmbBZWY4hfe4buyeC9VckvhoCktiQXJxI=
github.com/go-pkgz/rest v1.11.0 h1:Z//qgmM0NhBYfhXYEP/aJtDVLK5XlJGxqcb4sHFNN0E=
github.com/go-pkgz/rest v1.11.0/go.mod h1:wZ/dGipZUaF9to0vIQl7PwDHgWQDB0jsrFg1xnAKLDw=
github.com/go-pkgz/syncs v1.1.1/go.mod h1:bt9lxWRRJ9vOCMGc8Big8ttjYHLKP88ofj1y38UlaHE=
github.com/go-redis/redis/v7 v7.4.0/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg=
github.com/go-session/session v3.1.2+incompatible/go.mod h1:8B3iivBQjrz/JtC68Np2T1yBBLxTan3mn/3OM0CyRt0=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0=
github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY=
github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg=
github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs=
github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI=
github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI=
github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk=
github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28=
github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo=
github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk=
github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw=
github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360=
github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg=
github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE=
github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8=
github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc=
github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc=
github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4=
github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4=
github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ=
github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0=
github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
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.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
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.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomodule/redigo v1.7.1-0.20190322064113-39e2c31b7ca3/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/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.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
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 v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
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/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4=
github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
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/kyokomi/emoji/v2 v2.2.8/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE=
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/microcosm-cc/bluemonday v1.0.16 h1:kHmAq2t7WPWLjiGvzKa5o3HzSfahUKiOq7fAPUiMNIc=
github.com/microcosm-cc/bluemonday v1.0.16/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022/go.mod h1:x4NsS+uc7ecH/Cbm9xKQ6XzmJM57rWTkjywjfB2yQ18=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4=
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/slack-go/slack v0.9.5/go.mod h1:wWL//kk0ho+FcQXcBTmEafUI5dz4qz5f4mMk8oIkioQ=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tidwall/btree v0.0.0-20170113224114-9876f1454cf0/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8=
github.com/tidwall/buntdb v1.1.0/go.mod h1:Y39xhcDW10WlyYXeLgGftXVbjtM0QP+/kpz8xl9cbzE=
github.com/tidwall/gjson v1.3.2/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
github.com/tidwall/gjson v1.11.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb/go.mod h1:lKYYLFIr9OIgdgrtgkZ9zgRxRdvPYsExnYBsEAd8W5M=
github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
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/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/umputun/go-flags v1.5.1 h1:vRauoXV3Ultt1HrxivSxowbintgZLJE+EcBy5ta3/mY=
github.com/umputun/go-flags v1.5.1/go.mod h1:nTbvsO/hKqe7Utri/NoyN18GR3+EWf+9RrmsdwdhrEc=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w=
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
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.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
go.mongodb.org/mongo-driver v1.7.4/go.mod h1:NqaYOwnXWr5Pm7AOpO5QFxKJ503nbMse/R79oO62zWg=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ=
golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/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-20190213061140-3a22650c66bd/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-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211105192438-b53810dc28af h1:SMeNJG/vclJ5wyBBd4xupMsSJIHTd1coW9g7q6KOjmY=
golang.org/x/net v0.0.0-20211105192438-b53810dc28af/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c h1:+8miTPjMCTXwih7BQmvWwd0PjdBZq2MKp/qQaahSzEM=
golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/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.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI=
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=
google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=
google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
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.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/oauth2.v3 v3.12.0/go.mod h1:XEYgKqWX095YiPT+Aw5y3tCn+7/FMnlTFKrupgSiJ3I=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
+75
View File
@@ -0,0 +1,75 @@
/*
* Copyright 2020 Umputun. All rights reserved.
* Use of this source code is governed by a MIT-style
* license that can be found in the LICENSE file.
*/
package main
import (
"fmt"
"os"
"github.com/go-pkgz/jrpc"
log "github.com/go-pkgz/lgr"
"github.com/umputun/go-flags"
"github.com/umputun/remark42/memory_store/accessor"
"github.com/umputun/remark42/memory_store/server"
)
// opts with all cli commands and flags
var opts struct {
API string `long:"api" env:"API" default:"/" description:"api root url"`
Port int `long:"port" env:"PORT" default:"8080" description:"rpc server port"`
AuthUser string `long:"auth-user" env:"AUTH_USER" default:"" description:"rpc auth user name"`
AuthPasswd string `long:"auth-passwd" env:"AUTH_PASSWD" default:"" description:"rpc auth password"`
Secret string `long:"secret" env:"SECRET" required:"true" description:"secret key"`
Dbg bool `long:"dbg" env:"DEBUG" description:"debug mode"`
}
var revision = "unknown"
func main() {
fmt.Printf("remark42-memory module %s\n", revision)
if _, err := flags.Parse(&opts); err != nil {
os.Exit(2)
}
setupLog(opts.Dbg)
dataStore := accessor.NewMemData()
adminStore := accessor.NewMemAdminStore(opts.Secret)
imgStore := accessor.NewMemImageStore()
rpcServer := jrpc.Server{
API: opts.API,
AuthUser: opts.AuthUser,
AuthPasswd: opts.AuthPasswd,
Version: revision,
AppName: "remark42-memory",
Logger: log.Default(),
}
srv := server.NewRPC(dataStore, adminStore, imgStore, &rpcServer)
admRec := accessor.AdminRec{
SiteID: "remark",
Enabled: true,
IDs: []string{"dev_user"},
Email: "admin@example.com",
}
adminStore.Set("remark", admRec)
err := srv.Run(opts.Port)
log.Printf("[ERROR] server failed or terminated, %+v", err)
}
func setupLog(dbg bool) {
if dbg {
log.Setup(log.Debug, log.CallerFile, log.CallerFunc, log.Msec, log.LevelBraces)
return
}
log.Setup(log.Msec, log.LevelBraces)
}
@@ -0,0 +1,93 @@
/*
* Copyright 2020 Umputun. All rights reserved.
* Use of this source code is governed by a MIT-style
* license that can be found in the LICENSE file.
*/
package server
import (
"encoding/json"
"github.com/go-pkgz/jrpc"
"github.com/umputun/remark42/backend/app/store/admin"
)
// get admin key
func (s *RPC) admKeyHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
var siteID string
if err := json.Unmarshal(params, &siteID); err != nil {
return jrpc.Response{Error: err.Error()}
}
key, err := s.adm.Key(siteID)
if err != nil {
return jrpc.Response{Error: err.Error()}
}
return jrpc.EncodeResponse(id, key, err)
}
// get admins list
func (s *RPC) admAdminsHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
var siteID string
if err := json.Unmarshal(params, &siteID); err != nil {
return jrpc.Response{Error: err.Error()}
}
admins, err := s.adm.Admins(siteID)
if err != nil {
return jrpc.Response{Error: err.Error()}
}
return jrpc.EncodeResponse(id, admins, err)
}
// get admin email
func (s *RPC) admEmailHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
var siteID string
if err := json.Unmarshal(params, &siteID); err != nil {
return jrpc.Response{Error: err.Error()}
}
email, err := s.adm.Email(siteID)
if err != nil {
return jrpc.Response{Error: err.Error()}
}
return jrpc.EncodeResponse(id, email, err)
}
// return site enabled status
func (s *RPC) admEnabledHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
var siteID string
if err := json.Unmarshal(params, &siteID); err != nil {
return jrpc.Response{Error: err.Error()}
}
ok, err := s.adm.Enabled(siteID)
if err != nil {
return jrpc.Response{Error: err.Error()}
}
return jrpc.EncodeResponse(id, ok, err)
}
// onEvent returns nothing, callback to OnEvent
func (s *RPC) admEventHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
var siteID string
var ps []interface{}
if err := json.Unmarshal(params, &ps); err != nil {
return jrpc.Response{Error: err.Error()}
}
siteID, ok := ps[0].(string)
if !ok {
return jrpc.Response{Error: "wrong siteID type"}
}
evType, ok := ps[1].(float64)
if !ok {
return jrpc.Response{Error: "wrong event type"}
}
err := s.adm.OnEvent(siteID, admin.EventType(evType))
if err != nil {
return jrpc.Response{Error: err.Error()}
}
return jrpc.EncodeResponse(id, nil, err)
}
@@ -0,0 +1,89 @@
/*
* Copyright 2020 Umputun. All rights reserved.
* Use of this source code is governed by a MIT-style
* license that can be found in the LICENSE file.
*/
package server
import (
"fmt"
"net/http"
"testing"
"time"
"github.com/go-pkgz/jrpc"
"github.com/stretchr/testify/assert"
"github.com/umputun/remark42/backend/app/store/admin"
)
func TestRPC_admKeyHndl(t *testing.T) {
port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
ra := admin.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
key, err := ra.Key("any")
assert.NoError(t, err)
assert.Equal(t, "secret", key)
}
func TestRPC_admAdminsHndl(t *testing.T) {
port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
ra := admin.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
_, err := ra.Admins("bad site")
assert.EqualError(t, err, "site bad site not found")
admins, err := ra.Admins("test-site")
assert.NoError(t, err)
assert.Equal(t, []string{"id1", "id2"}, admins)
}
func TestRPC_admEmailHndl(t *testing.T) {
port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
ra := admin.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
_, err := ra.Admins("bad site")
assert.EqualError(t, err, "site bad site not found")
email, err := ra.Email("test-site")
assert.NoError(t, err)
assert.Equal(t, "admin@example.com", email)
}
func TestRPC_admEnabledHndl(t *testing.T) {
port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
ra := admin.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
_, err := ra.Enabled("bad site")
assert.EqualError(t, err, "site bad site not found")
ok, err := ra.Enabled("test-site")
assert.NoError(t, err)
assert.Equal(t, true, ok)
ok, err = ra.Enabled("test-site-disabled")
assert.NoError(t, err)
assert.Equal(t, false, ok)
}
func TestRPC_admEventHndl(t *testing.T) {
port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
ra := admin.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
err := ra.OnEvent("bad site", admin.EvCreate)
assert.EqualError(t, err, "site bad site not found")
err = ra.OnEvent("test-site", admin.EvCreate)
assert.NoError(t, err)
}
@@ -0,0 +1,125 @@
/*
* Copyright 2020 Umputun. All rights reserved.
* Use of this source code is governed by a MIT-style
* license that can be found in the LICENSE file.
*/
package server
import (
"encoding/json"
"github.com/go-pkgz/jrpc"
"github.com/umputun/remark42/backend/app/store"
"github.com/umputun/remark42/backend/app/store/engine"
)
func (s *RPC) createHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
comment := store.Comment{}
if err := json.Unmarshal(params, &comment); err != nil {
return jrpc.Response{Error: err.Error()}
}
commentID, err := s.eng.Create(comment)
return jrpc.EncodeResponse(id, commentID, err)
}
// Find comments
func (s *RPC) findHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
req := engine.FindRequest{}
if err := json.Unmarshal(params, &req); err != nil {
return jrpc.Response{Error: err.Error()}
}
resp, err := s.eng.Find(req)
return jrpc.EncodeResponse(id, resp, err)
}
// Get comment
func (s *RPC) getHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
req := engine.GetRequest{}
if err := json.Unmarshal(params, &req); err != nil {
return jrpc.Response{Error: err.Error()}
}
comment, err := s.eng.Get(req)
return jrpc.EncodeResponse(id, comment, err)
}
// Update comment
func (s *RPC) updateHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
comment := store.Comment{}
if err := json.Unmarshal(params, &comment); err != nil {
return jrpc.Response{Error: err.Error()}
}
err := s.eng.Update(comment)
return jrpc.EncodeResponse(id, nil, err)
}
// counts for site and users
func (s *RPC) countHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
req := engine.FindRequest{}
if err := json.Unmarshal(params, &req); err != nil {
return jrpc.Response{Error: err.Error()}
}
count, err := s.eng.Count(req)
return jrpc.EncodeResponse(id, count, err)
}
// info get post meta info
func (s *RPC) infoHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
req := engine.InfoRequest{}
if err := json.Unmarshal(params, &req); err != nil {
return jrpc.Response{Error: err.Error()}
}
info, err := s.eng.Info(req)
return jrpc.EncodeResponse(id, info, err)
}
// flagHndl get and sets flag value
func (s *RPC) flagHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
req := engine.FlagRequest{}
if err := json.Unmarshal(params, &req); err != nil {
return jrpc.Response{Error: err.Error()}
}
status, err := s.eng.Flag(req)
return jrpc.EncodeResponse(id, status, err)
}
// listFlagsHndl list flags for given request
func (s *RPC) listFlagsHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
req := engine.FlagRequest{}
if err := json.Unmarshal(params, &req); err != nil {
return jrpc.Response{Error: err.Error()}
}
flags, err := s.eng.ListFlags(req)
return jrpc.EncodeResponse(id, flags, err)
}
// userDetailHndl sets or gets single detail value, or gets all details for requested site.
// userDetailHndl returns list even for single entry request is a compromise in order to have both single detail getting and setting
// and all site's details listing under the same function (and not to extend engine interface by two separate functions).
func (s *RPC) userDetailHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
req := engine.UserDetailRequest{}
if err := json.Unmarshal(params, &req); err != nil {
return jrpc.Response{Error: err.Error()}
}
value, err := s.eng.UserDetail(req)
return jrpc.EncodeResponse(id, value, err)
}
// deleteHndl delete post(s), user, comment, user details, or everything
func (s *RPC) deleteHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
req := engine.DeleteRequest{}
if err := json.Unmarshal(params, &req); err != nil {
return jrpc.Response{Error: err.Error()}
}
err := s.eng.Delete(req)
return jrpc.EncodeResponse(id, nil, err)
}
// close store
func (s *RPC) closeHndl(_ uint64, _ json.RawMessage) (rr jrpc.Response) {
if err := s.eng.Close(); err != nil {
return jrpc.Response{Error: err.Error()}
}
return jrpc.Response{}
}
@@ -0,0 +1,306 @@
/*
* Copyright 2020 Umputun. All rights reserved.
* Use of this source code is governed by a MIT-style
* license that can be found in the LICENSE file.
*/
package server
import (
"fmt"
"net/http"
"testing"
"time"
"github.com/go-pkgz/jrpc"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/umputun/remark42/backend/app/store"
"github.com/umputun/remark42/backend/app/store/engine"
)
func TestRPC_createHndl(t *testing.T) {
port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
re := engine.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
id, err := re.Create(store.Comment{ID: "123456", Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
Text: "text 123", User: store.User{ID: "u1", Name: "user1"}})
assert.NoError(t, err)
assert.Equal(t, "123456", id)
}
func TestRPC_findHndl(t *testing.T) {
port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
re := engine.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
findReq := engine.FindRequest{Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"}}
comments, err := re.Find(findReq)
require.NoError(t, err)
assert.Equal(t, 0, len(comments))
c := store.Comment{ID: "123456", Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
Text: "text 123", User: store.User{ID: "u1", Name: "user1"}}
id, err := re.Create(c)
assert.NoError(t, err)
assert.Equal(t, "123456", id)
comments, err = re.Find(findReq)
require.NoError(t, err)
assert.Equal(t, 1, len(comments))
assert.Equal(t, c, comments[0])
}
func TestRPC_getHndl(t *testing.T) {
port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
re := engine.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
req := engine.GetRequest{
Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
CommentID: "123456",
}
_, err := re.Get(req)
assert.EqualError(t, err, "not found")
c := store.Comment{ID: "123456", Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
Text: "text 123", User: store.User{ID: "u1", Name: "user1"}}
_, err = re.Create(c)
assert.NoError(t, err)
comment, err := re.Get(req)
assert.NoError(t, err)
assert.Equal(t, c, comment)
}
func TestRPC_updateHndl(t *testing.T) {
port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
re := engine.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
c := store.Comment{ID: "123456", Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
Text: "text 123", User: store.User{ID: "u1", Name: "user1"}}
err := re.Update(c)
assert.EqualError(t, err, "not found")
_, err = re.Create(c)
assert.NoError(t, err)
c.Text = "updates"
err = re.Update(c)
assert.NoError(t, err)
req := engine.GetRequest{
Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
CommentID: "123456",
}
comment, err := re.Get(req)
assert.NoError(t, err)
assert.Equal(t, c, comment)
}
func TestRPC_countHndl(t *testing.T) {
port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
re := engine.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
findReq := engine.FindRequest{Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"}}
count, err := re.Count(findReq)
require.NoError(t, err)
assert.Equal(t, 0, count)
c := store.Comment{ID: "123456", Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
Text: "text 123", User: store.User{ID: "u1", Name: "user1"}}
id, err := re.Create(c)
assert.NoError(t, err)
assert.Equal(t, "123456", id)
count, err = re.Count(findReq)
require.NoError(t, err)
assert.Equal(t, 1, count)
}
func TestRPC_infoHndl(t *testing.T) {
port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
re := engine.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
c := store.Comment{ID: "123456", Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
Text: "text 123", User: store.User{ID: "u1", Name: "user1"}}
id, err := re.Create(c)
assert.NoError(t, err)
assert.Equal(t, "123456", id)
infoReq := engine.InfoRequest{Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"}}
info, err := re.Info(infoReq)
require.NoError(t, err)
assert.Equal(t, 1, len(info))
i := info[0]
assert.Equal(t, store.PostInfo{URL: "http://example.com/post1", Count: 1}, i)
}
func TestRPC_flagHndl(t *testing.T) {
port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
re := engine.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
c := store.Comment{ID: "123456", Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
Text: "text 123", User: store.User{ID: "u1", Name: "user1"}}
id, err := re.Create(c)
assert.NoError(t, err)
assert.Equal(t, "123456", id)
flagReq := engine.FlagRequest{
Flag: engine.Verified,
Locator: store.Locator{
SiteID: "test-site",
},
UserID: "u1",
}
status, err := re.Flag(flagReq)
require.NoError(t, err)
assert.Equal(t, false, status)
flagReq.Update = engine.FlagTrue
status, err = re.Flag(flagReq)
require.NoError(t, err)
assert.Equal(t, true, status)
flagReq.Update = engine.FlagNonSet
status, err = re.Flag(flagReq)
require.NoError(t, err)
assert.Equal(t, true, status)
}
func TestRPC_listFlagsHndl(t *testing.T) {
port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
re := engine.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
c := store.Comment{ID: "123456", Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
Text: "text 123", User: store.User{ID: "u1", Name: "user1"}}
id, err := re.Create(c)
assert.NoError(t, err)
assert.Equal(t, "123456", id)
flagReq := engine.FlagRequest{
Flag: engine.Verified,
UserID: "u1",
Locator: store.Locator{
SiteID: "test-site",
},
}
flags, err := re.ListFlags(flagReq)
require.NoError(t, err)
assert.Equal(t, []interface{}{}, flags)
flagReq.Update = engine.FlagTrue
status, err := re.Flag(flagReq)
require.NoError(t, err)
assert.Equal(t, true, status)
flags, err = re.ListFlags(flagReq)
require.NoError(t, err)
assert.Equal(t, []interface{}{"u1"}, flags)
}
func TestRPC_userDetailHndl(t *testing.T) {
port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
re := engine.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
// add to entries to DB before we start
result, err := re.UserDetail(engine.UserDetailRequest{Locator: store.Locator{SiteID: "test-site"}, UserID: "u1", Detail: engine.UserEmail, Update: "test@example.com"})
assert.NoError(t, err, "No error inserting entry expected")
assert.ElementsMatch(t, []engine.UserDetailEntry{{UserID: "u1", Email: "test@example.com"}}, result)
result, err = re.UserDetail(engine.UserDetailRequest{Locator: store.Locator{SiteID: "test-site"}, UserID: "u2", Detail: engine.UserEmail, Update: "other@example.com"})
assert.NoError(t, err, "No error inserting entry expected")
assert.ElementsMatch(t, []engine.UserDetailEntry{{UserID: "u2", Email: "other@example.com"}}, result)
// try to change existing entry with wrong SiteID
result, err = re.UserDetail(engine.UserDetailRequest{Locator: store.Locator{SiteID: "bad"}, UserID: "u2", Detail: engine.UserEmail, Update: "not_relevant"})
assert.NoError(t, err, "Updating existing entry with wrong SiteID doesn't produce error")
assert.ElementsMatch(t, []engine.UserDetailEntry{}, result, "Updating existing entry with wrong SiteID doesn't change anything")
// stateless tests without changing the state we set up before
var testData = []struct {
req engine.UserDetailRequest
error string
expected []engine.UserDetailEntry
}{
{req: engine.UserDetailRequest{Locator: store.Locator{SiteID: "test-site"}, UserID: "u1", Detail: engine.UserEmail},
expected: []engine.UserDetailEntry{{UserID: "u1", Email: "test@example.com"}}},
{req: engine.UserDetailRequest{Locator: store.Locator{SiteID: "bad"}, UserID: "u1", Detail: engine.UserEmail},
expected: []engine.UserDetailEntry{}},
{req: engine.UserDetailRequest{Locator: store.Locator{SiteID: "test-site"}, UserID: "u1xyz", Detail: engine.UserEmail},
expected: []engine.UserDetailEntry{}},
{req: engine.UserDetailRequest{Detail: engine.UserEmail, Update: "new_value"},
error: `userid cannot be empty in request for single detail`},
{req: engine.UserDetailRequest{Detail: engine.UserDetail("bad")},
error: `unsupported detail "bad"`},
{req: engine.UserDetailRequest{Update: "not_relevant", Detail: engine.AllUserDetails},
error: `unsupported request with userdetail all`},
{req: engine.UserDetailRequest{Locator: store.Locator{SiteID: "test-site"}, Detail: engine.AllUserDetails},
expected: []engine.UserDetailEntry{{UserID: "u1", Email: "test@example.com"}, {UserID: "u2", Email: "other@example.com"}}},
}
for i, x := range testData {
result, err := re.UserDetail(x.req)
if x.error != "" {
assert.EqualError(t, err, x.error, "Error should match expected for case %d", i)
} else {
assert.NoError(t, err, "Error is not expected expected for case %d", i)
}
assert.ElementsMatch(t, x.expected, result, "Result should match expected for case %d", i)
}
}
func TestRPC_deleteHndl(t *testing.T) {
port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
re := engine.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
req := engine.DeleteRequest{
Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
CommentID: "123456",
}
err := re.Delete(req)
assert.EqualError(t, err, "not found")
c := store.Comment{ID: "123456", Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
Text: "text 123", User: store.User{ID: "u1", Name: "user1"}}
_, err = re.Create(c)
assert.NoError(t, err)
err = re.Delete(req)
assert.NoError(t, err)
}
func TestRPC_closeHndl(t *testing.T) {
port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
re := engine.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
err := re.Close()
assert.NoError(t, err)
}
@@ -0,0 +1,71 @@
/*
* Copyright 2020 Umputun. All rights reserved.
* Use of this source code is governed by a MIT-style
* license that can be found in the LICENSE file.
*/
package server
import (
"context"
"encoding/base64"
"encoding/json"
"time"
"github.com/go-pkgz/jrpc"
)
func (s *RPC) imgSaveWithIDHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
var req [2]string
if err := json.Unmarshal(params, &req); err != nil {
return jrpc.Response{Error: err.Error()}
}
img, err := base64.StdEncoding.DecodeString(req[1])
if err != nil {
return jrpc.Response{Error: err.Error()}
}
err = s.img.Save(req[0], img)
return jrpc.EncodeResponse(id, nil, err)
}
func (s *RPC) imgResetClnTimerHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
var fileID string
if err := json.Unmarshal(params, &fileID); err != nil {
return jrpc.Response{Error: err.Error()}
}
err := s.img.ResetCleanupTimer(fileID)
return jrpc.EncodeResponse(id, nil, err)
}
func (s *RPC) imgLoadHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
var fileID string
if err := json.Unmarshal(params, &fileID); err != nil {
return jrpc.Response{Error: err.Error()}
}
value, err := s.img.Load(fileID)
return jrpc.EncodeResponse(id, value, err)
}
func (s *RPC) imgCommitHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
var fileID string
if err := json.Unmarshal(params, &fileID); err != nil {
return jrpc.Response{Error: err.Error()}
}
err := s.img.Commit(fileID)
return jrpc.EncodeResponse(id, nil, err)
}
func (s *RPC) imgCleanupHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
var ttl time.Duration
if err := json.Unmarshal(params, &ttl); err != nil {
return jrpc.Response{Error: err.Error()}
}
err := s.img.Cleanup(context.TODO(), ttl)
return jrpc.EncodeResponse(id, nil, err)
}
func (s *RPC) imgInfoHndl(id uint64, _ json.RawMessage) (rr jrpc.Response) {
info, err := s.img.Info()
return jrpc.EncodeResponse(id, info, err)
}
@@ -0,0 +1,161 @@
/*
* Copyright 2020 Umputun. All rights reserved.
* Use of this source code is governed by a MIT-style
* license that can be found in the LICENSE file.
*/
package server
import (
"context"
"encoding/base64"
"fmt"
"io"
"net/http"
"strings"
"testing"
"time"
"github.com/go-pkgz/jrpc"
"github.com/stretchr/testify/assert"
"github.com/umputun/remark42/backend/app/store/image"
)
// gopher png for test, from https://golang.org/src/image/png/example_test.go
const gopher = "iVBORw0KGgoAAAANSUhEUgAAAEsAAAA8CAAAAAALAhhPAAAFfUlEQVRYw62XeWwUVRzHf2" +
"+OPbo9d7tsWyiyaZti6eWGAhISoIGKECEKCAiJJkYTiUgTMYSIosYYBBIUIxoSPIINEBDi2VhwkQrVsj1ESgu9doHWdrul7ba" +
"73WNm3vOPtsseM9MdwvvrzTs+8/t95ze/33sI5BqiabU6m9En8oNjduLnAEDLUsQXFF8tQ5oxK3vmnNmDSMtrncks9Hhtt" +
"/qeWZapHb1ha3UqYSWVl2ZmpWgaXMXGohQAvmeop3bjTRtv6SgaK/Pb9/bFzUrYslbFAmHPp+3WhAYdr+7GN/YnpN46Opv55VDs" +
"JkoEpMrY/vO2BIYQ6LLvm0ThY3MzDzzeSJeeWNyTkgnIE5ePKsvKlcg/0T9QMzXalwXMlj54z4c0rh/mzEfr+FgWEz2w6uk" +
"8dkzFAgcARAgNp1ZYef8bH2AgvuStbc2/i6CiWGj98y2tw2l4FAXKkQBIf+exyRnteY83LfEwDQAYCoK+P6bxkZm/0966LxcAA" +
"ILHB56kgD95PPxltuYcMtFTWw/FKkY/6Opf3GGd9ZF+Qp6mzJxzuRSractOmJrH1u8XTvWFHINNkLQLMR+XHXvfPPHw967raE1xxwtA36I" +
"MRfkAAG29/7mLuQcb2WOnsJReZGfpiHsSBX81cvMKywYZHhX5hFPtOqPGWZCXnhWGAu6lX91ElKXSalcLXu3UaOXVay57ZSe5f6Gpx7J2" +
"MXAsi7EqSp09b/MirKSyJfnfEEgeDjl8FgDAfvewP03zZ+AJ0m9aFRM8eEHBDRKjfcreDXnZdQuAxXpT2NRJ7xl3UkLBhuVGU16gZiGOgZm" +
"rSbRdqkILuL/yYoSXHHkl9KXgqNu3PB8oRg0geC5vFmLjad6mUyTKLmF3OtraWDIfACyXqmephaDABawfpi6tqqBZytfQMqOz6S09iWXhkt" +
"rRaB8Xz4Yi/8gyABDm5NVe6qq/3VzPrcjELWrebVuyY2T7ar4zQyybUCtsQ5Es1FGaZVrRVQwAgHGW2ZCRZshI5bGQi7HesyE972pOSeMM0" +
"dSktlzxRdrlqb3Osa6CCS8IJoQQQgBAbTAa5l5epO34rJszibJI8rxLfGzcp1dRosutGeb2VDNgqYrwTiPNsLxXiPi3dz7LiS1WBRBDBOnqEj" +
"yy3aQb+/bLiJzz9dIkscVBBLxMfSEac7kO4Fpkngi0ruNBeSOal+u8jgOuqPz12nryMLCniEjtOOOmpt+KEIqsEdocJjYXwrh9OZqWJQyPCTo67" +
"LNS/TdxLAv6R5ZNK9npEjbYdT33gRo4o5oTqR34R+OmaSzDBWsAIPhuRcgyoteNi9gF0KzNYWVItPf2TLoXEg+7isNC7uJkgo1iQWOfRSP9NR" +
"11RtbZZ3OMG/VhL6jvx+J1m87+RCfJChAtEBQkSBX2PnSiihc/Twh3j0h7qdYQAoRVsRGmq7HU2QRbaxVGa1D6nIOqaIWRjyRZpHMQKWKpZM5fe" +
"A+lzC4ZFultV8S6T0mzQGhQohi5I8iw+CsqBSxhFMuwyLgSwbghGb0AiIKkSDmGZVmJSiKihsiyOAUs70UkywooYP0bii9GdH4sfr1UNysd3fU" +
"yLLMQN+rsmo3grHl9VNJHbbwxoa47Vw5gupIqrZcjPh9R4Nye3nRDk199V+aetmvVtDRE8/+cbgAAgMIWGb3UA0MGLE9SCbWX670TDy" +
"1y98c3D27eppUjsZ6fql3jcd5rUe7+ZIlLNQny3Rd+E5Tct3WVhTM5RBCEdiEK0b6B+/ca2gYU393nFj/n1AygRQxPIUA043M42u85+z2S" +
"nssKrPl8Mx76NL3E6eXc3be7OD+H4WHbJkKI8AU8irbITQjZ+0hQcPEgId/Fn/pl9crKH02+5o2b9T/eMx7pKoskYgAAAABJRU5ErkJggg=="
func gopherPNG() io.Reader { return base64.NewDecoder(base64.StdEncoding, strings.NewReader(gopher)) }
func gopherPNGBytes() []byte {
img, _ := io.ReadAll(gopherPNG())
return img
}
func TestRPC_imgLoadHndl(t *testing.T) {
port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
ri := image.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
// save
id := "test_img"
err := ri.Save(id, gopherPNGBytes())
assert.NoError(t, err)
// load
img, err := ri.Load(id)
assert.NoError(t, err)
assert.Equal(t, 1462, len(img))
assert.Equal(t, gopherPNGBytes(), img)
// commit
err = ri.Commit(id)
assert.NoError(t, err)
// load after commit
img, err = ri.Load(id)
assert.NoError(t, err)
assert.Equal(t, 1462, len(img))
assert.Equal(t, gopherPNGBytes(), img)
// cleanup
err = ri.Cleanup(context.TODO(), time.Second)
assert.NoError(t, err)
// load after cleanup
img, err = ri.Load(id)
assert.NoError(t, err)
assert.Equal(t, 1462, len(img))
assert.Equal(t, gopherPNGBytes(), img)
}
func TestRPC_imgCommitHndlFail(t *testing.T) {
port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
ri := image.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
err := ri.Commit("test_id")
assert.EqualError(t, err, "failed to commit test_id, not found in staging")
}
func TestRPC_imgCleanupHndl(t *testing.T) {
port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
ri := image.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
// save
id := "test_img"
err := ri.Save(id, gopherPNGBytes())
assert.NoError(t, err)
// load
img, err := ri.Load(id)
assert.NoError(t, err)
assert.Equal(t, 1462, len(img))
assert.Equal(t, gopherPNGBytes(), img)
// wait for image to expire
time.Sleep(time.Millisecond * 50)
// reset the time to cleanup
err = ri.ResetCleanupTimer(id)
assert.NoError(t, err)
// cleanup, should not affect the new image
err = ri.Cleanup(context.TODO(), time.Millisecond*45)
assert.NoError(t, err)
// load after cleanup should succeed
_, err = ri.Load(id)
assert.NoError(t, err, "image is still on staging because it's cleanup timer was reset")
// cleanup with short TTL, should remove the image from staging
err = ri.Cleanup(context.TODO(), time.Nanosecond)
assert.NoError(t, err)
// load after cleanup should fail
_, err = ri.Load(id)
assert.EqualError(t, err, "image test_img not found")
}
func TestRPC_imgInfoHndl(t *testing.T) {
port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
ri := image.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
// get info on empty storage, should be zero
info, err := ri.Info()
assert.NoError(t, err)
assert.True(t, info.FirstStagingImageTS.IsZero())
// save
err = ri.Save("test_img", gopherPNGBytes())
assert.NoError(t, err)
// get info after saving, should be non-zero
info, err = ri.Info()
assert.NoError(t, err)
assert.False(t, info.FirstStagingImageTS.IsZero())
}

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