Add a comment
73 |74 | 75 | 76 |
77 | 78 |
79 |
80 |
If you have a bug/issue with the commenting system, please send me an email (my email is in the "About" section).
81 | 82 |Comments are disabled for this article
91 |separator in menu */ 29 | 30 | } 31 | 32 | body { 33 | color: var(--MAIN-TEXT-color) !important; 34 | } 35 | 36 | textarea:focus, input[type="email"]:focus, input[type="number"]:focus, input[type="password"]:focus, input[type="search"]:focus, input[type="tel"]:focus, input[type="text"]:focus, input[type="url"]:focus, input[type="color"]:focus, input[type="date"]:focus, input[type="datetime"]:focus, input[type="datetime-local"]:focus, input[type="month"]:focus, input[type="time"]:focus, input[type="week"]:focus, select[multiple=multiple]:focus { 37 | border-color: none; 38 | box-shadow: none; 39 | } 40 | 41 | h2, h3, h4, h5 { 42 | color: var(--MAIN-TITLES-TEXT-color) !important; 43 | } 44 | 45 | a { 46 | color: var(--MAIN-LINK-color); 47 | } 48 | 49 | .anchor { 50 | color: var(--MAIN-ANCHOR-color); 51 | } 52 | 53 | a:hover { 54 | color: var(--MAIN-LINK-HOVER-color); 55 | } 56 | 57 | #sidebar ul li.visited > a .read-icon { 58 | color: var(--MENU-VISITED-color); 59 | } 60 | 61 | #body a.highlight:after { 62 | display: block; 63 | content: ""; 64 | height: 1px; 65 | width: 0%; 66 | -webkit-transition: width 0.5s ease; 67 | -moz-transition: width 0.5s ease; 68 | -ms-transition: width 0.5s ease; 69 | transition: width 0.5s ease; 70 | background-color: var(--MAIN-LINK-HOVER-color); 71 | } 72 | #sidebar { 73 | background-color: var(--MENU-SECTIONS-BG-color); 74 | } 75 | #sidebar #header-wrapper { 76 | background: var(--MENU-HEADER-BG-color); 77 | color: var(--MENU-SEARCH-BOX-color); 78 | border-color: var(--MENU-HEADER-BORDER-color); 79 | } 80 | #sidebar .searchbox { 81 | border-color: var(--MENU-SEARCH-BOX-color); 82 | background: var(--MENU-SEARCH-BG-color); 83 | } 84 | #sidebar ul.topics > li.parent, #sidebar ul.topics > li.active { 85 | background: var(--MENU-SECTIONS-ACTIVE-BG-color); 86 | } 87 | #sidebar .searchbox * { 88 | color: var(--MENU-SEARCH-BOX-ICONS-color); 89 | } 90 | 91 | #sidebar a { 92 | color: var(--MENU-SECTIONS-LINK-color); 93 | } 94 | 95 | #sidebar a:hover { 96 | color: var(--MENU-SECTIONS-LINK-HOVER-color); 97 | } 98 | 99 | #sidebar ul li.active > a { 100 | background: var(--MENU-SECTION-ACTIVE-CATEGORY-BG-color); 101 | color: var(--MENU-SECTION-ACTIVE-CATEGORY-color) !important; 102 | } 103 | 104 | #sidebar hr { 105 | border-color: var(--MENU-SECTION-HR-color); 106 | } 107 | 108 | #body .tags a.tag-link { 109 | background-color: var(--MENU-HEADER-BG-color); 110 | } 111 | 112 | #body .tags a.tag-link:before { 113 | border-right-color: var(--MENU-HEADER-BG-color); 114 | } 115 | 116 | #homelinks { 117 | background: var(--MENU-HEADER-BG-color); 118 | background-color: var(--MENU-HEADER-BORDER-color); 119 | border-bottom-color: var(--MENU-HEADER-BORDER-color); 120 | } 121 | 122 | #homelinks a { 123 | color: var(--MENU-HOME-LINK-color); 124 | } 125 | 126 | #homelinks a:hover { 127 | color: var(--MENU-HOME-LINK-HOVERED-color); 128 | } -------------------------------------------------------------------------------- /site/commentator/public/css/theme-red.css: -------------------------------------------------------------------------------- 1 | 2 | :root{ 3 | 4 | --MAIN-TEXT-color:#323232; /* Color of text by default */ 5 | --MAIN-TITLES-TEXT-color: #5e5e5e; /* Color of titles h2-h3-h4-h5 */ 6 | --MAIN-LINK-color:#f31c1c; /* Color of links */ 7 | --MAIN-LINK-HOVER-color:#d01616; /* Color of hovered links */ 8 | --MAIN-ANCHOR-color: #f31c1c; /* color of anchors on titles */ 9 | 10 | --MENU-HOME-LINK-color: #ccc; /* Color of the home button text */ 11 | --MENU-HOME-LINK-HOVER-color: #e6e6e6; /* Color of the hovered home button text */ 12 | 13 | --MENU-HEADER-BG-color:#dc1010; /* Background color of menu header */ 14 | --MENU-HEADER-BORDER-color:#e23131; /*Color of menu header border */ 15 | 16 | --MENU-SEARCH-BG-color:#b90000; /* Search field background color (by default borders + icons) */ 17 | --MENU-SEARCH-BOX-color: #ef2020; /* Override search field border color */ 18 | --MENU-SEARCH-BOX-ICONS-color: #fda1a1; /* Override search field icons color */ 19 | 20 | --MENU-SECTIONS-ACTIVE-BG-color:#2b2020; /* Background color of the active section and its childs */ 21 | --MENU-SECTIONS-BG-color:#312525; /* Background color of other sections */ 22 | --MENU-SECTIONS-LINK-color: #ccc; /* Color of links in menu */ 23 | --MENU-SECTIONS-LINK-HOVER-color: #e6e6e6; /* Color of links in menu, when hovered */ 24 | --MENU-SECTION-ACTIVE-CATEGORY-color: #777; /* Color of active category text */ 25 | --MENU-SECTION-ACTIVE-CATEGORY-BG-color: #fff; /* Color of background for the active category (only) */ 26 | 27 | --MENU-VISITED-color: #ff3333; /* Color of 'page visited' icons in menu */ 28 | --MENU-SECTION-HR-color: #2b2020; /* Color of
separator in menu */ 29 | 30 | } 31 | 32 | body { 33 | color: var(--MAIN-TEXT-color) !important; 34 | } 35 | 36 | textarea:focus, input[type="email"]:focus, input[type="number"]:focus, input[type="password"]:focus, input[type="search"]:focus, input[type="tel"]:focus, input[type="text"]:focus, input[type="url"]:focus, input[type="color"]:focus, input[type="date"]:focus, input[type="datetime"]:focus, input[type="datetime-local"]:focus, input[type="month"]:focus, input[type="time"]:focus, input[type="week"]:focus, select[multiple=multiple]:focus { 37 | border-color: none; 38 | box-shadow: none; 39 | } 40 | 41 | h2, h3, h4, h5 { 42 | color: var(--MAIN-TITLES-TEXT-color) !important; 43 | } 44 | 45 | a { 46 | color: var(--MAIN-LINK-color); 47 | } 48 | 49 | .anchor { 50 | color: var(--MAIN-ANCHOR-color); 51 | } 52 | 53 | a:hover { 54 | color: var(--MAIN-LINK-HOVER-color); 55 | } 56 | 57 | #sidebar ul li.visited > a .read-icon { 58 | color: var(--MENU-VISITED-color); 59 | } 60 | 61 | #body a.highlight:after { 62 | display: block; 63 | content: ""; 64 | height: 1px; 65 | width: 0%; 66 | -webkit-transition: width 0.5s ease; 67 | -moz-transition: width 0.5s ease; 68 | -ms-transition: width 0.5s ease; 69 | transition: width 0.5s ease; 70 | background-color: var(--MAIN-LINK-HOVER-color); 71 | } 72 | #sidebar { 73 | background-color: var(--MENU-SECTIONS-BG-color); 74 | } 75 | #sidebar #header-wrapper { 76 | background: var(--MENU-HEADER-BG-color); 77 | color: var(--MENU-SEARCH-BOX-color); 78 | border-color: var(--MENU-HEADER-BORDER-color); 79 | } 80 | #sidebar .searchbox { 81 | border-color: var(--MENU-SEARCH-BOX-color); 82 | background: var(--MENU-SEARCH-BG-color); 83 | } 84 | #sidebar ul.topics > li.parent, #sidebar ul.topics > li.active { 85 | background: var(--MENU-SECTIONS-ACTIVE-BG-color); 86 | } 87 | #sidebar .searchbox * { 88 | color: var(--MENU-SEARCH-BOX-ICONS-color); 89 | } 90 | 91 | #sidebar a { 92 | color: var(--MENU-SECTIONS-LINK-color); 93 | } 94 | 95 | #sidebar a:hover { 96 | color: var(--MENU-SECTIONS-LINK-HOVER-color); 97 | } 98 | 99 | #sidebar ul li.active > a { 100 | background: var(--MENU-SECTION-ACTIVE-CATEGORY-BG-color); 101 | color: var(--MENU-SECTION-ACTIVE-CATEGORY-color) !important; 102 | } 103 | 104 | #sidebar hr { 105 | border-color: var(--MENU-SECTION-HR-color); 106 | } 107 | 108 | #body .tags a.tag-link { 109 | background-color: var(--MENU-HEADER-BG-color); 110 | } 111 | 112 | #body .tags a.tag-link:before { 113 | border-right-color: var(--MENU-HEADER-BG-color); 114 | } 115 | 116 | #homelinks { 117 | background: var(--MENU-HEADER-BG-color); 118 | background-color: var(--MENU-HEADER-BORDER-color); 119 | border-bottom-color: var(--MENU-HEADER-BORDER-color); 120 | } 121 | 122 | #homelinks a { 123 | color: var(--MENU-HOME-LINK-color); 124 | } 125 | 126 | #homelinks a:hover { 127 | color: var(--MENU-HOME-LINK-HOVERED-color); 128 | } -------------------------------------------------------------------------------- /site/commentator/public/css/theme-green.css: -------------------------------------------------------------------------------- 1 | 2 | :root{ 3 | 4 | --MAIN-TEXT-color:#323232; /* Color of text by default */ 5 | --MAIN-TITLES-TEXT-color: #5e5e5e; /* Color of titles h2-h3-h4-h5 */ 6 | --MAIN-LINK-color:#599a3e; /* Color of links */ 7 | --MAIN-LINK-HOVER-color:#3f6d2c; /* Color of hovered links */ 8 | --MAIN-ANCHOR-color: #599a3e; /* color of anchors on titles */ 9 | 10 | --MENU-HOME-LINK-color: #323232; /* Color of the home button text */ 11 | --MENU-HOME-LINK-HOVER-color: #5e5e5e; /* Color of the hovered home button text */ 12 | 13 | --MENU-HEADER-BG-color:#74b559; /* Background color of menu header */ 14 | --MENU-HEADER-BORDER-color:#9cd484; /*Color of menu header border */ 15 | 16 | --MENU-SEARCH-BG-color:#599a3e; /* Search field background color (by default borders + icons) */ 17 | --MENU-SEARCH-BOX-color: #84c767; /* Override search field border color */ 18 | --MENU-SEARCH-BOX-ICONS-color: #c7f7c4; /* Override search field icons color */ 19 | 20 | --MENU-SECTIONS-ACTIVE-BG-color:#1b211c; /* Background color of the active section and its childs */ 21 | --MENU-SECTIONS-BG-color:#222723; /* Background color of other sections */ 22 | --MENU-SECTIONS-LINK-color: #ccc; /* Color of links in menu */ 23 | --MENU-SECTIONS-LINK-HOVER-color: #e6e6e6; /* Color of links in menu, when hovered */ 24 | --MENU-SECTION-ACTIVE-CATEGORY-color: #777; /* Color of active category text */ 25 | --MENU-SECTION-ACTIVE-CATEGORY-BG-color: #fff; /* Color of background for the active category (only) */ 26 | 27 | --MENU-VISITED-color: #599a3e; /* Color of 'page visited' icons in menu */ 28 | --MENU-SECTION-HR-color: #18211c; /* Color of
separator in menu */ 29 | 30 | } 31 | 32 | body { 33 | color: var(--MAIN-TEXT-color) !important; 34 | } 35 | 36 | textarea:focus, input[type="email"]:focus, input[type="number"]:focus, input[type="password"]:focus, input[type="search"]:focus, input[type="tel"]:focus, input[type="text"]:focus, input[type="url"]:focus, input[type="color"]:focus, input[type="date"]:focus, input[type="datetime"]:focus, input[type="datetime-local"]:focus, input[type="month"]:focus, input[type="time"]:focus, input[type="week"]:focus, select[multiple=multiple]:focus { 37 | border-color: none; 38 | box-shadow: none; 39 | } 40 | 41 | h2, h3, h4, h5 { 42 | color: var(--MAIN-TITLES-TEXT-color) !important; 43 | } 44 | 45 | a { 46 | color: var(--MAIN-LINK-color); 47 | } 48 | 49 | .anchor { 50 | color: var(--MAIN-ANCHOR-color); 51 | } 52 | 53 | a:hover { 54 | color: var(--MAIN-LINK-HOVER-color); 55 | } 56 | 57 | #sidebar ul li.visited > a .read-icon { 58 | color: var(--MENU-VISITED-color); 59 | } 60 | 61 | #body a.highlight:after { 62 | display: block; 63 | content: ""; 64 | height: 1px; 65 | width: 0%; 66 | -webkit-transition: width 0.5s ease; 67 | -moz-transition: width 0.5s ease; 68 | -ms-transition: width 0.5s ease; 69 | transition: width 0.5s ease; 70 | background-color: var(--MAIN-LINK-HOVER-color); 71 | } 72 | #sidebar { 73 | background-color: var(--MENU-SECTIONS-BG-color); 74 | } 75 | #sidebar #header-wrapper { 76 | background: var(--MENU-HEADER-BG-color); 77 | color: var(--MENU-SEARCH-BOX-color); 78 | border-color: var(--MENU-HEADER-BORDER-color); 79 | } 80 | #sidebar .searchbox { 81 | border-color: var(--MENU-SEARCH-BOX-color); 82 | background: var(--MENU-SEARCH-BG-color); 83 | } 84 | #sidebar ul.topics > li.parent, #sidebar ul.topics > li.active { 85 | background: var(--MENU-SECTIONS-ACTIVE-BG-color); 86 | } 87 | #sidebar .searchbox * { 88 | color: var(--MENU-SEARCH-BOX-ICONS-color); 89 | } 90 | 91 | #sidebar a { 92 | color: var(--MENU-SECTIONS-LINK-color); 93 | } 94 | 95 | #sidebar a:hover { 96 | color: var(--MENU-SECTIONS-LINK-HOVER-color); 97 | } 98 | 99 | #sidebar ul li.active > a { 100 | background: var(--MENU-SECTION-ACTIVE-CATEGORY-BG-color); 101 | color: var(--MENU-SECTION-ACTIVE-CATEGORY-color) !important; 102 | } 103 | 104 | #sidebar hr { 105 | border-color: var(--MENU-SECTION-HR-color); 106 | } 107 | 108 | #body .tags a.tag-link { 109 | background-color: var(--MENU-HEADER-BG-color); 110 | } 111 | 112 | #body .tags a.tag-link:before { 113 | border-right-color: var(--MENU-HEADER-BG-color); 114 | } 115 | 116 | #homelinks { 117 | background: var(--MENU-HEADER-BG-color); 118 | background-color: var(--MENU-HEADER-BORDER-color); 119 | border-bottom-color: var(--MENU-HEADER-BORDER-color); 120 | } 121 | 122 | #homelinks a { 123 | color: var(--MENU-HOME-LINK-color); 124 | } 125 | 126 | #homelinks a:hover { 127 | color: var(--MENU-HOME-LINK-HOVERED-color); 128 | } -------------------------------------------------------------------------------- /site/commentator/public/css/theme-commentator.css: -------------------------------------------------------------------------------- 1 | 2 | :root{ 3 | 4 | --MAIN-TEXT-color:#323232; /* Color of text by default */ 5 | --MAIN-TITLES-TEXT-color: #5e5e5e; /* Color of titles h2-h3-h4-h5 */ 6 | --MAIN-LINK-color: #0007c9; /* Color of links */ 7 | --MAIN-LINK-HOVER-color:#0007c9; /* Color of hovered links */ 8 | --MAIN-ANCHOR-color: #1C90F3; /* color of anchors on titles */ 9 | 10 | --MENU-HOME-LINK-color: #323232; /* Color of the home button text */ 11 | --MENU-HOME-LINK-HOVER-color: #5e5e5e; /* Color of the hovered home button text */ 12 | 13 | --MENU-HEADER-BG-color: #0199b1 ; /* Background color of menu header */ 14 | --MENU-HEADER-BORDER-color :#fff; /*Color of menu header border */ 15 | 16 | --MENU-SEARCH-BG-color: #e6e6e6; /* Search field background color (by default borders + icons) */ 17 | --MENU-SEARCH-BOX-color: #33a1ff; /* Override search field border color */ 18 | --MENU-SEARCH-BOX-ICONS-color: #33a1ff; /* Override search field icons color */ 19 | 20 | --MENU-SECTIONS-ACTIVE-BG-color:#20272b; /* Background color of the active section and its childs */ 21 | --MENU-SECTIONS-BG-color:#252c31; /* Background color of other sections */ 22 | --MENU-SECTIONS-LINK-color: #ccc; /* Color of links in menu */ 23 | --MENU-SECTIONS-LINK-HOVER-color: #e6e6e6; /* Color of links in menu, when hovered */ 24 | --MENU-SECTION-ACTIVE-CATEGORY-color: black; /* Color of active category text */ 25 | --MENU-SECTION-ACTIVE-CATEGORY-BG-color: #fff; /* Color of background for the active category (only) */ 26 | 27 | --MENU-VISITED-color: #33a1ff; /* Color of 'page visited' icons in menu */ 28 | --MENU-SECTION-HR-color: #20272b; /* Color of
separator in menu */ 29 | 30 | } 31 | 32 | body { 33 | color: var(--MAIN-TEXT-color) !important; 34 | } 35 | 36 | textarea:focus, input[type="email"]:focus, input[type="number"]:focus, input[type="password"]:focus, input[type="search"]:focus, input[type="tel"]:focus, input[type="text"]:focus, input[type="url"]:focus, input[type="color"]:focus, input[type="date"]:focus, input[type="datetime"]:focus, input[type="datetime-local"]:focus, input[type="month"]:focus, input[type="time"]:focus, input[type="week"]:focus, select[multiple=multiple]:focus { 37 | border-color: none; 38 | box-shadow: none; 39 | } 40 | 41 | h2, h3, h4, h5 { 42 | color: var(--MAIN-TITLES-TEXT-color) !important; 43 | } 44 | 45 | a { 46 | color: var(--MAIN-LINK-color); 47 | } 48 | 49 | .anchor { 50 | color: var(--MAIN-ANCHOR-color); 51 | } 52 | 53 | a:hover { 54 | color: var(--MAIN-LINK-HOVER-color); 55 | } 56 | 57 | #sidebar ul li.visited > a .read-icon { 58 | color: var(--MENU-VISITED-color); 59 | } 60 | 61 | #body a.highlight:after { 62 | display: block; 63 | content: ""; 64 | height: 1px; 65 | width: 0%; 66 | -webkit-transition: width 0.5s ease; 67 | -moz-transition: width 0.5s ease; 68 | -ms-transition: width 0.5s ease; 69 | transition: width 0.5s ease; 70 | background-color: var(--MAIN-LINK-HOVER-color); 71 | } 72 | #sidebar { 73 | background-color: var(--MENU-SECTIONS-BG-color); 74 | } 75 | #sidebar #header-wrapper { 76 | background: var(--MENU-HEADER-BG-color); 77 | color: var(--MENU-SEARCH-BOX-color); 78 | border-color: var(--MENU-HEADER-BORDER-color); 79 | } 80 | #sidebar .searchbox { 81 | border-color: var(--MENU-SEARCH-BOX-color); 82 | background: var(--MENU-SEARCH-BG-color); 83 | } 84 | #sidebar ul.topics > li.parent, #sidebar ul.topics > li.active { 85 | background: var(--MENU-SECTIONS-ACTIVE-BG-color); 86 | } 87 | #sidebar .searchbox * { 88 | color: var(--MENU-SEARCH-BOX-ICONS-color); 89 | } 90 | 91 | #sidebar a { 92 | color: var(--MENU-SECTIONS-LINK-color); 93 | } 94 | 95 | #sidebar a:hover { 96 | color: var(--MENU-SECTIONS-LINK-HOVER-color); 97 | } 98 | 99 | #sidebar ul li.active > a { 100 | background: var(--MENU-SECTION-ACTIVE-CATEGORY-BG-color); 101 | color: var(--MENU-SECTION-ACTIVE-CATEGORY-color) !important; 102 | } 103 | 104 | #sidebar hr { 105 | border-color: var(--MENU-SECTION-HR-color); 106 | } 107 | 108 | #body .tags a.tag-link { 109 | background-color: var(--MENU-HEADER-BG-color); 110 | } 111 | 112 | #body .tags a.tag-link:before { 113 | border-right-color: var(--MENU-HEADER-BG-color); 114 | } 115 | 116 | #homelinks { 117 | background: var(--MENU-HEADER-BG-color); 118 | background-color: var(--MENU-HEADER-BORDER-color); 119 | border-bottom-color: var(--MENU-HEADER-BORDER-color); 120 | } 121 | 122 | #homelinks a { 123 | color: var(--MENU-HOME-LINK-color); 124 | } 125 | 126 | #homelinks a:hover { 127 | color: var(--MENU-HOME-LINK-HOVERED-color); 128 | } 129 | -------------------------------------------------------------------------------- /src/commentator/api.clj: -------------------------------------------------------------------------------- 1 | (ns commentator.api 2 | (:require [clojure.spec.alpha :as s] 3 | [commentator.comment :as comment] 4 | [commentator.config :as config] 5 | [commentator.event :as event] 6 | [commentator.handler :as handler] 7 | [commentator.spec :as spec])) 8 | 9 | (s/def ::answer ::spec/non-empty-string) 10 | (s/def ::comment-id ::comment/id) 11 | (s/def ::article ::spec/non-empty-string) 12 | (s/def ::timestamp pos-int?) 13 | (s/def ::signature ::spec/non-empty-string) 14 | 15 | (s/def ::path ::spec/non-empty-string) 16 | (s/def :usage/add (s/keys :req-un [::config/website 17 | ::path])) 18 | 19 | (s/def :comment/new (s/keys :req-un [::article 20 | ::comment/author 21 | ::comment/content 22 | ::config/website 23 | ::answer 24 | ::timestamp 25 | ::signature] 26 | :opt-un [::comment/author-website])) 27 | (s/def :comment/get (s/keys :req-un [::article 28 | ::config/website 29 | ::comment-id])) 30 | (s/def :comment/for-article (s/keys :req-un [::article 31 | ::config/website])) 32 | (s/def :comment/approve (s/keys :req-un [::article ::comment-id ::config/website])) 33 | (s/def :comment/delete (s/keys :req-un [::article ::comment-id ::config/website])) 34 | (s/def :comment/delete-article (s/keys :req-un [::article ::config/website])) 35 | (s/def :comment/admin-for-article (s/keys :req-un [::article ::config/website])) 36 | (s/def :challenge/random (s/keys :req-un [::article ::config/website])) 37 | 38 | (s/def :event/delete (s/keys :req-un [::event/id ::config/website])) 39 | (s/def :usage/day ::spec/non-empty-string) 40 | (s/def :usage/month ::spec/non-empty-string) 41 | (s/def :usage/year ::spec/non-empty-string) 42 | (s/def :usage/get-for-day (s/keys :req-un [::config/website 43 | :usage/day 44 | :usage/month 45 | :usage/year])) 46 | (s/def :event/list (s/keys :req-un [::config/website])) 47 | 48 | (def router 49 | [["/api/v1/comment/:website/:article" {:post {:spec :comment/new 50 | :handler handler/new-comment} 51 | :get {:spec :comment/for-article 52 | :handler handler/comments-for-article}}] 53 | ["/api/admin/comment/:website/:article" {:get {:handler handler/admin-for-article 54 | :auth true 55 | :spec :comment/admin-for-article} 56 | :delete {:spec :comment/delete-article 57 | :auth true 58 | :handler handler/delete-article-comments}}] 59 | ["/api/admin/comment/:website/:article/:comment-id" {:get {:spec :comment/get 60 | :auth true 61 | :handler handler/get-comment} 62 | :delete {:spec :comment/delete 63 | :auth true 64 | :handler handler/delete-comment} 65 | :post {:spec :comment/approve 66 | :auth true 67 | :handler handler/approve-comment}}] 68 | ["/api/v1/challenge/:website/:article" {:get {:handler handler/random-challenge 69 | :spec :challenge/random}}] 70 | ["/api/admin/event/:website" {:get {:spec :event/list 71 | :auth true 72 | :handler handler/list-events}}] 73 | ["/api/admin/event/:website/:id" {:delete {:spec :event/delete 74 | :auth true 75 | :handler handler/delete-event}}] 76 | ["/api/v1/usage/:website" {:post {:spec :usage/add 77 | :handler handler/usage}}] 78 | ["/api/admin/usage/:website/:year/:month/:day" {:get {:spec :usage/get-for-day 79 | :auth true 80 | :handler handler/get-usage-for-day}}] 81 | ["/healthz" {:get {:handler handler/healthz}}]]) 82 | -------------------------------------------------------------------------------- /src/commentator/usage.clj: -------------------------------------------------------------------------------- 1 | (ns commentator.usage 2 | (:require [cheshire.core :as json] 3 | [com.stuartsierra.component :as component] 4 | [commentator.rate-limit :as rl] 5 | [commentator.store :as store] 6 | [corbihttp.log :as log] 7 | [exoscale.ex :as ex]) 8 | (:import java.util.concurrent.ScheduledThreadPoolExecutor 9 | java.util.concurrent.TimeUnit 10 | java.time.LocalDate 11 | java.time.format.DateTimeFormatter)) 12 | 13 | (defprotocol IWebsiteUsage 14 | (new-request [this request] "Add this request to the website usage component") 15 | (usage-for-day [this website year month day] "Get the usage per article for the given day")) 16 | 17 | (def ^DateTimeFormatter formatter (DateTimeFormatter/ofPattern "yyyy/MM/dd")) 18 | 19 | (defn now 20 | [] 21 | (.format (LocalDate/now) formatter)) 22 | 23 | (defn add-request 24 | [cache request] 25 | (let [ip (rl/source-ip request) 26 | website (get-in request [:all-params :website]) 27 | path (get-in request [:all-params :path]) 28 | day (now)] 29 | (if (get-in cache [website day path ip]) 30 | (update-in cache [website day path ip] inc) 31 | (assoc-in cache [website day path ip] 1)))) 32 | 33 | (defn resource-name 34 | [day] 35 | (str "usage/" day)) 36 | 37 | (defn sync-cache 38 | [cache s3] 39 | (try 40 | (doseq [[website website-usage] @cache] 41 | (doseq [[day content] website-usage] 42 | (log/infof {} 43 | "sync usage cache for %s - %s" 44 | website 45 | day) 46 | (store/save-resource s3 47 | website 48 | (resource-name day) 49 | (json/generate-string content)))) 50 | (catch Exception e 51 | (log/error {} e "fail to sync cache")))) 52 | 53 | (defn load-cache 54 | [cache s3 website] 55 | (log/info {:website website} "loading usage cache") 56 | (let [day (now) 57 | day-resource (resource-name day)] 58 | (when (store/exists? s3 website day-resource) 59 | (swap! cache assoc-in [website day] (-> (store/get-resource s3 website day-resource) 60 | (json/parse-string)))))) 61 | 62 | (defn purge-cache 63 | [cache] 64 | (log/info {} "purge usage cache") 65 | (try 66 | (->> (map (fn [[website website-cache]] 67 | [website (select-keys website-cache [(now)])]) 68 | cache) 69 | (into {})) 70 | (catch Exception e 71 | (log/error {} e "fail to purge cache")))) 72 | 73 | (defn get-usage 74 | [website s3 day] 75 | (let [path (resource-name day)] 76 | (if (store/exists? s3 website path) 77 | (-> (store/get-resource s3 website path) 78 | (json/parse-string false)) 79 | (ex/ex-incorrect! (format "usage for day %s not found" day) 80 | {})))) 81 | 82 | (defn compute-usage-for-day 83 | [usage] 84 | (reduce 85 | (fn [state [url accesses]] 86 | (let [unique-page (count accesses) 87 | total-page (reduce #(+ %1 (second %2)) 0 accesses)] 88 | (-> (assoc-in state [:pages url] 89 | {:unique unique-page 90 | :total total-page}) 91 | (update :unique + unique-page) 92 | (update :total + total-page)))) 93 | {:unique 0 94 | :total 0} 95 | usage)) 96 | 97 | (defrecord WebsiteUsage [websites s3 cache executor] 98 | component/Lifecycle 99 | (start [this] 100 | (let [executor (ScheduledThreadPoolExecutor. 1) 101 | c (atom {})] 102 | (doseq [website websites] 103 | (load-cache c s3 website)) 104 | (.scheduleWithFixedDelay executor 105 | ^Runnable (fn [] 106 | (sync-cache c s3)) 107 | 20 108 | 60 109 | TimeUnit/SECONDS) 110 | (.scheduleWithFixedDelay executor 111 | ^Runnable (fn [] 112 | (swap! c purge-cache)) 113 | 60 114 | 14400 115 | TimeUnit/SECONDS) 116 | (assoc this :cache c :executor executor))) 117 | 118 | (stop [this] 119 | (when executor 120 | (.shutdown executor) 121 | (.awaitTermination executor 122 | 20 123 | TimeUnit/SECONDS) 124 | ;; sync one last time 125 | (sync-cache cache s3)) 126 | (assoc this :cache nil :executor nil)) 127 | IWebsiteUsage 128 | (new-request [_ request] 129 | (swap! cache add-request request)) 130 | (usage-for-day [_ website year month day] 131 | (-> (get-usage website s3 (format "%s/%s/%s" year month day)) 132 | compute-usage-for-day))) 133 | -------------------------------------------------------------------------------- /site/commentator/content/howto/use-it/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Use it 3 | weight: 50 4 | disableToc: false 5 | --- 6 | 7 | Commentator stores the articles comments on a S3 compatible store. + 8 | Let's say we want to store comments for the blog `mcorbin.fr` for example. 9 | 10 | The first thing to do is to write the Commentator [configuration file](/howto/configuration/). 11 | 12 | As explained in the linked section of the documentation, several things are important. 13 | 14 | **Allow origin** 15 | 16 | The same Commentator instance can serve comments for several websites. You need to configure the list of websites in the `:allow-origin` section. 17 | 18 | **Rate limit** 19 | 20 | The `:rate-limit-minutes` option prevents users to create more than one comment every N minutes. 21 | 22 | The `x-forwarded-for` header is first used to get the user IP. If the header is not set it fallbacks to the request source IP. 23 | 24 | **Comment** 25 | 26 | Comments can be automatically approved by setting the `:auto-approve` value to `true`. By default, comments not approved by an administrator are not displayed. 27 | 28 | You also need to configure the `allowed-articles` key. It contains a map containing for each website the list of articles allowed to receive comments. For example: 29 | 30 | ```clojure 31 | {"mcorbin-fr" ["my-first-article" 32 | "my-second-article"]} 33 | ``` 34 | 35 | With this setup, only requests to create comments on the `/api/v1/comment/
