114 Commits

Author SHA1 Message Date
Adhityaa Chandrasekar
e0f188909f release: v1.6.2 2019-02-19 00:12:28 -05:00
Adhityaa Chandrasekar
0b78e9e70c owner_reset_password.go: use ownerHex in SELECT 2019-02-19 00:11:26 -05:00
Adhityaa Chandrasekar
ca797cd165 release: v1.6.1 2019-02-18 18:36:17 -05:00
Adhityaa Chandrasekar
15d729c6ac docker-compose.yml: remove db ports exposure 2019-02-18 18:35:28 -05:00
Adhityaa Chandrasekar
af1d1dcd0c Dockerfile: copy fonts file 2019-02-18 18:28:06 -05:00
Adhityaa Chandrasekar
b21c630208 release: v1.6.0 2019-02-18 17:40:22 -05:00
Adhityaa Chandrasekar
ef68dadcd7 email_moderate.go: include email in error message 2019-02-18 17:40:22 -05:00
Adhityaa Chandrasekar
8a7348ed6a email.go: run go fmt 2019-02-18 17:38:58 -05:00
Adhityaa Chandrasekar
5df5b5f112 fonts: add source sans fonts 2019-02-18 17:34:43 -05:00
Adhityaa Chandrasekar
c9677385f8 commenter_self.go: include email details 2019-02-18 17:34:43 -05:00
Adhityaa Chandrasekar
220109a157 commento.js: close login box when logged in 2019-02-18 17:34:43 -05:00
Adhityaa Chandrasekar
2e2d022c9b commento.js: add login button 2019-02-18 17:34:43 -05:00
Adhityaa Chandrasekar
63c4da0b8d api: add email moderation 2019-02-18 17:34:43 -05:00
Adhityaa Chandrasekar
e1c94ecf15 api,frontend: add unsubscribe 2019-02-18 17:34:43 -05:00
Adhityaa Chandrasekar
60a9f2cc15 button.scss: move .button to separate file 2019-02-18 17:34:43 -05:00
Adhityaa Chandrasekar
06f0f6f014 everywhere: add email notifications 2019-02-18 17:34:43 -05:00
Adhityaa Chandrasekar
69aba94590 commento.scss: move highlighted to commento-card.scss 2019-02-18 17:34:43 -05:00
Adhityaa Chandrasekar
52ce1e2660 comment_new.go: enforce RequireIdentification when anonymous
Yikes, I can't believe I forgot about this.
2019-02-18 17:34:43 -05:00
Adhityaa Chandrasekar
7fc3910009 comment_new.go: use RequireModeration in anonymous clause 2019-02-18 17:34:43 -05:00
Adhityaa Chandrasekar
619231e32f commento.js: add scroll into view based on hash 2019-02-18 17:34:43 -05:00
Adhityaa Chandrasekar
a22b49a112 commenter_get.go: add TODO comment 2019-02-18 17:34:43 -05:00
Adhityaa Chandrasekar
b77089388f commento.js: show oauth above email login 2019-02-18 17:34:43 -05:00
Adhityaa Chandrasekar
b35155b9e5 commento.js: default to login instead of signup 2019-02-18 17:34:43 -05:00
Adhityaa Chandrasekar
5bb51bb131 commento-mod-tools.scss: use 12px font 2019-02-18 17:34:43 -05:00
Adhityaa Chandrasekar
8a8e0b53fc commento.scss: move avatar and dark-card to commento-card.scss 2019-02-18 17:34:43 -05:00
Adhityaa Chandrasekar
2b00384219 commento.scss: move mod-tools to separate file 2019-02-18 17:34:43 -05:00
Adhityaa Chandrasekar
bd695c53fd commento-input.scss: darken textarea placeholder 2019-02-18 17:34:43 -05:00
Adhityaa Chandrasekar
24ddf0657b commento-input.scss: remove unused approve and delete classes 2019-02-18 17:34:43 -05:00
Adhityaa Chandrasekar
e70546fb56 commento-oauth.scss: move oauth button styling to file 2019-02-18 17:34:43 -05:00
Adhityaa Chandrasekar
4ceb85ae51 commento.js, commento-input.scss: make textarea cleaner 2019-02-18 17:34:43 -05:00
Adhityaa Chandrasekar
41b0c8e5ca checkbox.scss: add commento- prefix 2019-02-18 17:33:44 -05:00
Adhityaa Chandrasekar
0acdd67e39 commenter_login.go: include commenter struct in response 2019-02-18 17:33:44 -05:00
Adhityaa Chandrasekar
caca7b8c41 commento-input.scss: use Source Sans Pro 2019-02-18 17:33:44 -05:00
Adhityaa Chandrasekar
24de2dbcb3 checkbox.scss: move checkbox element to separate file 2019-02-18 17:33:44 -05:00
Adhityaa Chandrasekar
9a14801990 commento.scss: rename buttons, tags, card file 2019-02-18 17:33:44 -05:00
Adhityaa Chandrasekar
de98ed81cd commento-logo.scss: rename to commento-footer.scss 2019-02-18 17:33:44 -05:00
Adhityaa Chandrasekar
3f7b65dee9 commento.js: fix anonymous selfHex logic in commentNew 2019-02-18 17:33:44 -05:00
Adhityaa Chandrasekar
8c09aa0ff6 commento.scss: remove unnecessary common-main include 2019-02-18 17:33:44 -05:00
dasZGFz
ce47f80e8e commento.js: Add iframe support 2019-02-12 23:35:33 -08:00
Adhityaa Chandrasekar
e434f59f9a release: v1.5.0 2019-02-04 18:11:03 -05:00
Adhityaa Chandrasekar
a4fbf67d73 api: run go fmt 2019-02-04 18:11:03 -05:00
Adhityaa Chandrasekar
1aea90cb07 dashboard.html: hide email settings for now 2019-02-04 18:08:17 -05:00
Adhityaa Chandrasekar
20b6660fa9 domain_import_disqus.go: update disqus export spec 2019-02-04 18:05:51 -05:00
Adhityaa Chandrasekar
815628c5ee dashboard-statistics.js: namespace numberify with global 2019-01-31 02:25:05 -05:00
Adhityaa Chandrasekar
6caa3e312c cron_domain_export_cleanup.go: change cron period 2019-01-31 02:19:47 -05:00
Adhityaa Chandrasekar
94829d9b83 api: add cron job to clean up views table 2019-01-31 02:19:12 -05:00
Adhityaa Chandrasekar
7be22b091f domain_export.go: raise error if SMTP is not configured 2019-01-31 02:08:13 -05:00
Adhityaa Chandrasekar
fff5e5c0e1 everywhere: add option to export data 2019-01-31 02:06:11 -05:00
Adhityaa Chandrasekar
f1ece27c99 dashboard: overhaul 2019-01-30 23:49:16 -05:00
Adhityaa Chandrasekar
5e48da6940 dashboard: improve moderator list explanation text 2019-01-30 22:49:25 -05:00
Adhityaa Chandrasekar
28fe1aaa89 dashboard.html: improve readability in panes 2019-01-30 22:44:36 -05:00
Adhityaa Chandrasekar
f846935a2a comment_new.go, commento.js: don't refresh when creating comments 2019-01-30 22:19:16 -05:00
Adhityaa Chandrasekar
42b452b9f8 commento-input.scss: biggen oauth button text size 2019-01-30 21:26:14 -05:00
Adhityaa Chandrasekar
514535a607 oauth_google_callback.go: fix potential nil panic 2019-01-30 21:25:09 -05:00
Adhityaa Chandrasekar
55f24b2de2 api: add github oauth
Closes https://gitlab.com/commento/commento/issues/20
2019-01-30 21:22:46 -05:00
Adhityaa Chandrasekar
24d76c2fb6 frontend: add favicon to all html files 2019-01-30 20:13:08 -05:00
Max
f2ff2b4940 Update docker.md with correct postgresql directory for persitence 2019-01-27 16:16:24 +00:00
Adhityaa Chandrasekar
6d1563e22a email-main.scss: unset width 2019-01-24 07:01:16 -05:00
Adhityaa Chandrasekar
9a3c181442 commento-input.scss: use pixels instead of rem 2019-01-24 06:56:30 -05:00
Adhityaa Chandrasekar
010b7336cd signup.js: fix document.location typo
I have no idea what that was.
2019-01-24 06:12:44 -05:00
Adhityaa Chandrasekar
00c197e2ee owner_new.go: perform email check before processing 2019-01-24 06:11:37 -05:00
Adhityaa Chandrasekar
c6a98d93e4 dashboard.html: update allow-anonymous subtext 2019-01-23 18:39:12 -05:00
Adhityaa Chandrasekar
edd8aae7a7 auth-common.js, reset.js: use global namespace for paramGet 2019-01-23 17:48:39 -05:00
Adhityaa Chandrasekar
3677d43aab templates: use plaintext instead of fancy HTML 2019-01-23 02:52:13 -05:00
Adhityaa Chandrasekar
0cdba65e48 release: v1.4.2 2019-01-23 00:27:55 -05:00
Adhityaa Chandrasekar
022fc06257 everywhere: improve anonymous comments logic 2019-01-23 00:26:25 -05:00
Adhityaa Chandrasekar
61bc73e705 dashboard.html: rephrase requireIdentification text on-screen 2019-01-22 23:55:18 -05:00
Adhityaa Chandrasekar
e0cf9a89f9 release: v1.4.1 2019-01-16 23:03:32 -05:00
Adhityaa Chandrasekar
bc92df8083 commento.js: use single arg in onclick
Fixes https://gitlab.com/commento/commento/issues/96
2019-01-16 23:03:32 -05:00
Adhityaa Chandrasekar
71947bbe2c README.md: use h3 for sections 2019-01-14 11:44:50 -05:00
Adhityaa Chandrasekar
5a029e2786 README.md: update with FAQ 2019-01-14 11:26:08 -05:00
Adhityaa Chandrasekar
7f9a39c330 .gitlab-ci.yml: specify AWS variables in job 2018-12-29 01:59:54 -05:00
Adhityaa Chandrasekar
633ccf427c .gitlab-ci.yml: environment: aws-upload-tags 2018-12-29 01:37:27 -05:00
Adhityaa Chandrasekar
51e4608c19 .gitlab-ci.yml: use environment for aws-upload-tags 2018-12-29 01:14:21 -05:00
Adhityaa Chandrasekar
612e620ffc .gitlab-ci.yml: remove ce suffix from CI scripts 2018-12-29 00:57:25 -05:00
Adhityaa Chandrasekar
642076a231 .gitlab-ci.yml: add automated aws push on release 2018-12-29 00:39:25 -05:00
Adhityaa Chandrasekar
f63639782c check-dco: add execute permission bit 2018-12-29 00:14:55 -05:00
Adhityaa Chandrasekar
02615088ff everywhere: remove -ce suffix 2018-12-28 12:58:01 -05:00
Adhityaa Chandrasekar
5aa3bc86eb release: v1.4.0 2018-12-28 12:18:54 -05:00
Adhityaa Chandrasekar
d5769d56c1 version.go: remove unused import 2018-12-28 12:18:54 -05:00
Adhityaa Chandrasekar
8eb0bc147c router_static.go: go fmt 2018-12-28 12:14:15 -05:00
Adhityaa Chandrasekar
96589a2658 commento.js: warn user if no div with id=commento
Closes https://gitlab.com/commento/commento/issues/42
2018-12-28 11:59:02 -05:00
Adhityaa Chandrasekar
34e39edcda version.go: remove ORIGIN and reduce frequency of logs 2018-12-28 11:47:54 -05:00
Adhityaa Chandrasekar
6d00a8e3aa frontend,api: refactor static router 2018-12-24 21:49:53 -05:00
Adhityaa Chandrasekar
c29b3a7a25 auth-common.js: add semicolon after use strict 2018-12-24 21:49:31 -05:00
Adhityaa Chandrasekar
a99bf15332 dashboard-installation.js: remove unnecessary commento namespace 2018-12-24 21:47:23 -05:00
Adhityaa Chandrasekar
7074800ecc gulpfile.js: remove redundant 'js' subpath 2018-12-24 21:40:28 -05:00
Adhityaa Chandrasekar
80fb09d941 dashboard-installation.js: use namespaced cdn variable 2018-12-24 21:40:28 -05:00
Adhityaa Chandrasekar
afabc25037 frontend: do not uglify devel JS files 2018-12-20 05:26:03 -05:00
Anton Linevych
c9e7a3f40a dashboard-setting.js: simplified if condition 2018-12-20 04:20:34 -05:00
Anton Linevych
4d82106aff gitlab-ci.yml: Install yarn for new F/E building system 2018-12-20 04:19:36 -05:00
Adhityaa Chandrasekar
4c0e261a8e autoserve: make it easy to switch between devel and prod 2018-12-20 04:18:43 -05:00
Anton Linevych
9e3935b3b2 frontend: use gulp, eslint, code refactor
Apologies in advance for the insanely huge commit. This commit is
primarily based on Anton Linevych's amazing work found here [1]. While
he had gone through the pains of making small, atomic changes to each
file, I had put off reviewing the PR for a long time. By the time I
finally got around to it, the project had changed so much that it didn't
make sense to keep the commits the same way. So I've cherry-picked most
of his commits, with some changes here and there, and I've squashed them
into one commit.

[1] https://gitlab.com/linevych/commento-ce/tree/feature/frontend_building_improvements
2018-12-20 04:14:48 -05:00
Adhityaa Chandrasekar
e4f71fe402 frontend: display options at the bottom on mobile 2018-12-20 01:27:55 -05:00
Adhityaa Chandrasekar
06c71f4e65 frontend: use commento namespace, event handlers, UI 2018-12-20 00:50:00 -05:00
Adhityaa Chandrasekar
9fcf67d667 api,frontend: add Akismet spam flagging integration 2018-12-19 22:57:02 -05:00
Adhityaa Chandrasekar
d1318daaca commento.scss: use yellow instead of blue for moderation 2018-12-19 22:14:36 -05:00
Adhityaa Chandrasekar
87a0c577bb frontend: display anonymous button in textarea 2018-12-18 19:46:43 -05:00
Adhityaa Chandrasekar
bcc81e1ad8 frontend: redesign score and timeago subtitle 2018-12-18 19:10:12 -05:00
Adhityaa Chandrasekar
1f8f3b3a36 commento.js: update avatar colors to be more distinct 2018-12-18 19:02:01 -05:00
Adhityaa Chandrasekar
610b61831d frontend,api: open source comment sticky 2018-12-18 18:57:32 -05:00
Adhityaa Chandrasekar
cf0b394b05 release: v1.3.1 2018-10-18 02:17:52 -04:00
Adhityaa Chandrasekar
a36b11f07d api: use dep instead of go get 2018-10-18 02:17:51 -04:00
Adhityaa Chandrasekar
93c9ce0cad release: v1.3.0 2018-10-18 01:05:01 -04:00
Adhityaa Chandrasekar
af88db42b2 owner_get.go: fix incorrent owner session selection field 2018-10-18 01:03:12 -04:00
Adhityaa Chandrasekar
0c6ccdc0a1 domain_delete.go: delete entries from pages on domain delete 2018-10-08 02:34:06 -04:00
Adhityaa Chandrasekar
6d3f8171e5 release: v1.2.0 2018-10-07 23:14:55 -04:00
Adhityaa Chandrasekar
3f1c570e84 db: store version in config table 2018-10-07 23:13:26 -04:00
Adhityaa Chandrasekar
cd88ae264e commento-input.scss: make create account button width 150px 2018-10-07 01:33:39 -04:00
Adhityaa Chandrasekar
b2abcae319 README.md: add Mozilla and DO 2018-10-05 22:48:52 -04:00
Adhityaa Chandrasekar
41a5c675bf release: v1.1.3 2018-09-28 09:39:38 -04:00
Adhityaa Chandrasekar
ac9f896a22 commento.js: reverse sorting algorithm 2018-09-28 09:39:07 -04:00
Adhityaa Chandrasekar
36d57914b2 release: v1.1.2 2018-09-27 17:13:05 -04:00
Adhityaa Chandrasekar
800ba5dd0d .gitlab-ci.yml: install git before usage 2018-09-27 17:11:57 -04:00
161 changed files with 8421 additions and 1612 deletions

View File

@@ -3,6 +3,7 @@ stages:
- go-fmt - go-fmt
- go-test - go-test
- build-src - build-src
- aws-upload-tags
- build-docker - build-docker
- docker-registry-master - docker-registry-master
- docker-registry-tags - docker-registry-tags
@@ -21,22 +22,37 @@ check-dco:
build-src: build-src:
stage: build-src stage: build-src
image: debian:buster image: debian:buster
variables:
GOPATH: $CI_PROJECT_DIR
except: except:
- master - master
- tags - tags
before_script:
- bash $CI_PROJECT_DIR/scripts/gitlab-ci-build-prescript
script: script:
- apt update - export GOPATH=/go
- apt install -y curl gnupg git make golang - export PATH=$PATH:/go/bin
- curl -sL https://deb.nodesource.com/setup_10.x | bash - - cd /go/src/$CI_PROJECT_NAME
- apt update
- apt install -y nodejs
- npm install -g html-minifier@3.5.7 uglify-js@3.4.1 sass@1.5.1
- mkdir -p src/gitlab.com/commento && cd src/gitlab.com/commento && ln -s $CI_PROJECT_DIR && cd $CI_PROJECT_NAME
- make devel - make devel
- make prod - make prod
aws-upload-tags:
stage: aws-upload-tags
image: debian:buster
environment: aws-upload-tags
variables:
AWS_ACCESS_KEY_ID: $AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY: $AWS_SECRET_ACCESS_KEY
only:
- tags
before_script:
- bash $CI_PROJECT_DIR/scripts/gitlab-ci-build-prescript
script:
- export GOPATH=/go
- export PATH=$PATH:/go/bin
- cd /go/src/$CI_PROJECT_NAME
- make prod
- cd build/prod && tar -zcvf /commento-linux-amd64-$(git describe --tags).tgz .
- aws s3 cp /commento-linux-amd64-$(git describe --tags).tgz s3://commento-release/
build-docker: build-docker:
stage: build-docker stage: build-docker
image: docker:stable image: docker:stable
@@ -46,7 +62,7 @@ build-docker:
- master - master
- tags - tags
script: script:
- docker build -t commento-ce . - docker build -t commento .
go-test: go-test:
stage: go-test stage: go-test
@@ -58,12 +74,17 @@ go-test:
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres
POSTGRES_DB: commento_test POSTGRES_DB: commento_test
COMMENTO_POSTGRES: postgres://postgres:postgres@postgres/commento_test?sslmode=disable COMMENTO_POSTGRES: postgres://postgres:postgres@postgres/commento_test?sslmode=disable
GOPATH: $CI_PROJECT_DIR
except: except:
- master - master
- tags - tags
before_script:
- mkdir -p /go/src /go/bin /go/pkg
- export GOPATH=/go
- export PATH=$PATH:/go/bin
- curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
- ln -s $CI_PROJECT_DIR /go/src/$CI_PROJECT_NAME
script: script:
- mkdir -p src/gitlab.com/commento && cd src/gitlab.com/commento && ln -s $CI_PROJECT_DIR && cd $CI_PROJECT_NAME - cd /go/src/$CI_PROJECT_NAME
- make test - make test
go-fmt: go-fmt:
@@ -82,13 +103,13 @@ docker-registry-master:
services: services:
- docker:dind - docker:dind
only: only:
- master@commento/commento-ce - master@commento/commento
before_script: before_script:
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.gitlab.com - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.gitlab.com
script: script:
- docker pull registry.gitlab.com/commento/commento-ce:latest || true - docker pull registry.gitlab.com/commento/commento:latest || true
- docker build --cache-from registry.gitlab.com/commento/commento-ce:latest --tag registry.gitlab.com/commento/commento-ce:latest . - docker build --cache-from registry.gitlab.com/commento/commento:latest --tag registry.gitlab.com/commento/commento:latest .
- docker push registry.gitlab.com/commento/commento-ce:latest - docker push registry.gitlab.com/commento/commento:latest
docker-registry-tags: docker-registry-tags:
stage: docker-registry-tags stage: docker-registry-tags
@@ -100,5 +121,6 @@ docker-registry-tags:
before_script: before_script:
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.gitlab.com - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.gitlab.com
script: script:
- docker build --tag registry.gitlab.com/commento/commento-ce:$(git describe --tags) . - apk add git
- docker push registry.gitlab.com/commento/commento-ce:$(git describe --tags) - docker build --tag registry.gitlab.com/commento/commento:$(git describe --tags) .
- docker push registry.gitlab.com/commento/commento:$(git describe --tags)

View File

@@ -1,10 +1,11 @@
# backend build (api server) # backend build (api server)
FROM golang:1.10.2-alpine AS api-build FROM golang:1.10.2-alpine AS api-build
COPY ./api /go/src/commento-ce/api COPY ./api /go/src/commento/api
WORKDIR /go/src/commento-ce/api WORKDIR /go/src/commento/api
RUN apk update && apk add bash make git RUN apk update && apk add bash make git curl
RUN curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
RUN make prod -j$(($(nproc) + 1)) RUN make prod -j$(($(nproc) + 1))
@@ -12,8 +13,8 @@ RUN make prod -j$(($(nproc) + 1))
# frontend build (html, js, css, images) # frontend build (html, js, css, images)
FROM node:10.3.0-alpine AS frontend-build FROM node:10.3.0-alpine AS frontend-build
COPY ./frontend /commento-ce/frontend/ COPY ./frontend /commento/frontend/
WORKDIR /commento-ce/frontend/ WORKDIR /commento/frontend/
RUN apk update && apk add bash make RUN apk update && apk add bash make
RUN npm install -g html-minifier@3.5.7 uglify-js@3.4.1 sass@1.5.1 RUN npm install -g html-minifier@3.5.7 uglify-js@3.4.1 sass@1.5.1
@@ -24,8 +25,8 @@ RUN make prod -j$(($(nproc) + 1))
# templates build # templates build
FROM alpine:3.7 AS templates-build FROM alpine:3.7 AS templates-build
COPY ./templates /commento-ce/templates COPY ./templates /commento/templates
WORKDIR /commento-ce/templates WORKDIR /commento/templates
RUN apk update && apk add bash make RUN apk update && apk add bash make
@@ -35,8 +36,8 @@ RUN make prod -j$(($(nproc) + 1))
# db build # db build
FROM alpine:3.7 AS db-build FROM alpine:3.7 AS db-build
COPY ./db /commento-ce/db COPY ./db /commento/db
WORKDIR /commento-ce/db WORKDIR /commento/db
RUN apk update && apk add bash make RUN apk update && apk add bash make
@@ -46,19 +47,20 @@ RUN make prod -j$(($(nproc) + 1))
# final image # final image
FROM alpine:3.7 FROM alpine:3.7
COPY --from=api-build /go/src/commento-ce/api/build/prod/commento-ce /commento-ce/commento-ce COPY --from=api-build /go/src/commento/api/build/prod/commento /commento/commento
COPY --from=frontend-build /commento-ce/frontend/build/prod/*.html /commento-ce/ COPY --from=frontend-build /commento/frontend/build/prod/*.html /commento/
COPY --from=frontend-build /commento-ce/frontend/build/prod/css/*.css /commento-ce/css/ COPY --from=frontend-build /commento/frontend/build/prod/css/*.css /commento/css/
COPY --from=frontend-build /commento-ce/frontend/build/prod/js/*.js /commento-ce/js/ COPY --from=frontend-build /commento/frontend/build/prod/js/*.js /commento/js/
COPY --from=frontend-build /commento-ce/frontend/build/prod/images/* /commento-ce/images/ COPY --from=frontend-build /commento/frontend/build/prod/images/* /commento/images/
COPY --from=templates-build /commento-ce/templates/build/prod/templates/ /commento-ce/templates/ COPY --from=frontend-build /commento/frontend/build/prod/fonts/* /commento/fonts/
COPY --from=db-build /commento-ce/db/build/prod/db/ /commento-ce/db/ COPY --from=templates-build /commento/templates/build/prod/templates/ /commento/templates/
COPY --from=db-build /commento/db/build/prod/db/ /commento/db/
RUN apk update && apk add ca-certificates --no-cache RUN apk update && apk add ca-certificates --no-cache
EXPOSE 8080 EXPOSE 8080
WORKDIR /commento-ce/ WORKDIR /commento/
ENV COMMENTO_BIND_ADDRESS="0.0.0.0" ENV COMMENTO_BIND_ADDRESS="0.0.0.0"
ENTRYPOINT ["/commento-ce/commento-ce"] ENTRYPOINT ["/commento/commento"]

View File

@@ -4,41 +4,69 @@
<p align="center"><b>A bloat-free and privacy-focused discussion platform.</b></p> <p align="center"><b>A bloat-free and privacy-focused discussion platform.</b></p>
Commento is a discussion platform that you can embed on your blog, news articles, and any place where you want your readers to add comments. Commento is fast, lightweight, and privacy-focused; we'll never sell your data, show ads, embed third-party tracking scripts, or inject affiliate links. ### What is Commento?
#### Features Commento allows you to foster discussion on your website &ndash; if you have a blog, you can embed Commento if you want your readers to add comments. It's fast and bloat-free, has a modern interface, and is reasonably secure. Unlike most alternatives, Commento is lightweight and privacy-focused; I'll never sell your data, show ads, embed third-party tracking scripts, or inject affiliate links.
- Privacy-focused ### Frequently Asked Questions
- Super lightweight, allowing for fast pageloads
- Automatic spam filtering
- Review and approve or delete comments through the moderation interface
- Modern interface with a clean design
- OAuth support (Google login, for example)
- Custom CSS theming
- Import from existing services (like Disqus)
- Completely free and open source (MIT Expat license)
#### Editions **I don't want to install and manage Commento on a server.**
You can use [Commento.io](https://commento.io), the cloud version of Commento, where I do the server hosting, updates, and security and performance tuning for you. To make the hosted service self-sustainable, it is not free. You may choose the plan that best matches your financial situation and needs &ndash; all plans have all features.
There are three editions of Commento. **What features does Commento have?**
Commento comes with a lot of useful features out-of-the-box: rich text support, upvotes and downvotes, automatic spam detection, moderation tools, sticky comments, thread locking, OAuth login, email notifications, and more!
- **Commento Community Edition (CE)** is open source software that's freely available under the MIT license. **What does Commento look like? Do you have a demo?**
- [**Commento Enterprise Edition (EE)**](https://commento.io/pricing#self-hosted) includes extra features geared towards organizations that want to self-host. Check out [demo.commento.io](https://demo.commento.io) to play around with a live demo of Commento.
- [**Commento Hosted**](https://commento.io) is a hosted version of Commento for those who don't want to host and manage servers. This is currently in private beta and you can [add yourself to the waiting list here](https://commento.io).
#### Installation and Configuration **How is Commento different from Disqus, Facebook Comments, and the rest?**
Most other products in this space do not respect your privacy; showing adverts is their primary business model and that nearly always comes at the users' cost. There is no free lunch. Commento is also orders of magnitude lighter than alternatives &ndash; while Disqus and Facebook take megabytes of download to load, Commento is just 11 kB.
See our [documentation on how to install Commento](http://docs.commento.io/installation.html) to get started. We offer several ways to install the software, including a Docker image. **Is Commento free software?**
Yes. Commento is made [freely available](https://gitlab.com/commento/commento) under the [MIT license](https://gitlab.com/commento/commento/blob/master/LICENSE). And it will always stay that way.
Once you've installed the software, you need to configure it with various environment variables before starting the service. To learn more about this, refer to our documentation on [configuring Commento](https://docs.commento.io/configuration.html). **Disqus has a free plan. Why is the [cloud version](https://commento.io) not free of cost?**
When I say Commento is free, I mean [free as in freedom](https://www.gnu.org/philosophy/free-sw.en.html). The cloud version is not offered free of cost because servers cost money and offering the service for free would not be sustainable. Unlike most alternatives, Commento does not operate on adverts and shady tactics; you're the customer, not the product.
#### Contributing **I have nothing to hide. Why should I care about my privacy?**
The thing about privacy is that once you give up control over your information, you can't get it back. You may be fine with having your personal information sold to unknown third-parties today, but when your insurance company uses this information against you tomorrow, you'll regret it. And you'll have no recourse to correct this. Read [this Wikipedia article](https://en.wikipedia.org/wiki/Nothing_to_hide_argument) for more information.
Commento is possible only because of its community. If this is your first contribution to Commento, please go through the [development documentation](https://docs.commento.io/contributing.html) before you begin. <div><p style="margin: 0px 0px"><b>As a blog owner, why should I worry about my readers' privacy?</b><br>
Good question. For starters, your readers value their privacy. Not caring about them is disrespectful and you will end up alienating your audience; they won't come back. But even if you ignore this, you have bigger questions to answer:</p>
<ul>
<li><b>Legality</b>: Did you know that Disqus still isn't GDPR-compliant (according to their <a href="https://help.disqus.com/terms-and-policies/privacy-faq" title="At the time of writing (28 December 2018)" rel="nofollow">privacy policy</a>)?</li>
<li><b>Security</b>: What happens when a random third-party script is injected into your website?</li>
<li><b>Performance</b>: Did you know that half a second increase in page load time results in a 20% decrease in engagement and site traffic?</li>
<li><b>Ownership</b>: Who owns the content when your readers create comments?</li>
</ul></div>
**Who's behind this? Are you an evil corporation?**
My name is <a href="https://adtac.in">Adhityaa</a>, and I created the project. As someone who's still a student, I promise you I'm neither evil nor a corporation. But I'm not the only one &ndash; dozens of people have contributed to the project and Commento would not exist without these wonderful people.
**Okay, how do I get started?**
Glad you asked! You have two options &ndash; self-hosting Commento on your own server or using the [cloud version](https://commento.io). Start [from here](https://docs.commento.io/getting-started/) to decide which option is right for you and proceed from there.
### Installation
See our [documentation on how to install Commento](https://docs.commento.io/installation/) to get started.
### Contributing
Commento is possible only because of its community. If this is your first contribution to Commento, please go through the [documentation](https://docs.commento.io/contributing/) before you begin.
Help will always be given to those who ask for it. We use IRC for chat to collaborate with other developers. You're invited to [hang out with us](https://irc.commento.io) in the `#commento-dev` channel on freenode if you want to contribute to Commento! Help will always be given to those who ask for it. We use IRC for chat to collaborate with other developers. You're invited to [hang out with us](https://irc.commento.io) in the `#commento-dev` channel on freenode if you want to contribute to Commento!
#### License ### Sponsors
Commento development is partially sponsored by [Mozilla](https://mozilla.org) and [DigitalOcean](https://www.digitalocean.com/) independently.
<p align="center">
<a href="https://www.mozilla.org/en-US/"><img src="https://user-images.githubusercontent.com/7521600/32265838-d05b2d08-bf0a-11e7-92e1-2cb183eae616.png" title="Mozilla" height="40"></a>
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
<a href="https://www.digitalocean.com"><img src="https://user-images.githubusercontent.com/7521600/32265839-d093c7da-bf0a-11e7-8d99-96a940041d06.png" title="DigitalOcean" height="40"></a>
</p>
### License
``` ```
Copyright 2018 Commento, Inc. Copyright 2018 Commento, Inc.

View File

@@ -9,6 +9,14 @@
revision = "64a2037ec6be8a4b0c1d1f706ed35b428b989239" revision = "64a2037ec6be8a4b0c1d1f706ed35b428b989239"
version = "v0.26.0" version = "v0.26.0"
[[projects]]
branch = "master"
digest = "1:9769b231d8f5ff406a012aa7f293e45ed69d11617832a1c3c7b8c6ce1558a2a1"
name = "github.com/adtac/go-akismet"
packages = ["akismet"]
pruneopts = "UT"
revision = "0ca9e1023047c869ecd4bd3c20780511597a4a77"
[[projects]] [[projects]]
digest = "1:15042ad3498153684d09f393bbaec6b216c8eec6d61f63dff711de7d64ed8861" digest = "1:15042ad3498153684d09f393bbaec6b216c8eec6d61f63dff711de7d64ed8861"
name = "github.com/golang/protobuf" name = "github.com/golang/protobuf"
@@ -108,10 +116,11 @@
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:bea0314c10bd362ab623af4880d853b5bad3b63d0ab9945c47e461b8d04203ed" digest = "1:82e6e4dc5ab71680d89684e4649be630fdeeaf81feb8e88e4a56273a0cd4d966"
name = "golang.org/x/oauth2" name = "golang.org/x/oauth2"
packages = [ packages = [
".", ".",
"github",
"google", "google",
"internal", "internal",
"jws", "jws",
@@ -143,6 +152,7 @@
analyzer-name = "dep" analyzer-name = "dep"
analyzer-version = 1 analyzer-version = 1
input-imports = [ input-imports = [
"github.com/adtac/go-akismet/akismet",
"github.com/gorilla/handlers", "github.com/gorilla/handlers",
"github.com/gorilla/mux", "github.com/gorilla/mux",
"github.com/lib/pq", "github.com/lib/pq",
@@ -151,7 +161,9 @@
"github.com/op/go-logging", "github.com/op/go-logging",
"github.com/russross/blackfriday", "github.com/russross/blackfriday",
"golang.org/x/crypto/bcrypt", "golang.org/x/crypto/bcrypt",
"golang.org/x/net/html",
"golang.org/x/oauth2", "golang.org/x/oauth2",
"golang.org/x/oauth2/github",
"golang.org/x/oauth2/google", "golang.org/x/oauth2/google",
] ]
solver-name = "gps-cdcl" solver-name = "gps-cdcl"

View File

@@ -7,9 +7,9 @@ PROD_BUILD_DIR = $(BUILD_DIR)/prod
GO_SRC_DIR = . GO_SRC_DIR = .
GO_SRC_FILES = $(wildcard $(GO_SRC_DIR)/*.go) GO_SRC_FILES = $(wildcard $(GO_SRC_DIR)/*.go)
GO_DEVEL_BUILD_DIR = $(DEVEL_BUILD_DIR) GO_DEVEL_BUILD_DIR = $(DEVEL_BUILD_DIR)
GO_DEVEL_BUILD_BINARY = $(GO_DEVEL_BUILD_DIR)/commento-ce GO_DEVEL_BUILD_BINARY = $(GO_DEVEL_BUILD_DIR)/commento
GO_PROD_BUILD_DIR = $(PROD_BUILD_DIR) GO_PROD_BUILD_DIR = $(PROD_BUILD_DIR)
GO_PROD_BUILD_BINARY = $(GO_PROD_BUILD_DIR)/commento-ce GO_PROD_BUILD_BINARY = $(GO_PROD_BUILD_DIR)/commento
devel: devel-go devel: devel-go
@@ -25,15 +25,15 @@ clean:
# later down the line). # later down the line).
devel-go: devel-go:
go get -v . dep ensure
go build -i -v -o $(GO_DEVEL_BUILD_BINARY) go build -i -v -o $(GO_DEVEL_BUILD_BINARY)
prod-go: prod-go:
go get -v . dep ensure
go build -i -v -o $(GO_PROD_BUILD_BINARY) go build -i -v -o $(GO_PROD_BUILD_BINARY)
test-go: test-go:
go get -v . dep ensure
go test -v . go test -v .
$(shell mkdir -p $(GO_DEVEL_BUILD_DIR) $(GO_PROD_BUILD_DIR)) $(shell mkdir -p $(GO_DEVEL_BUILD_DIR) $(GO_PROD_BUILD_DIR))

31
api/akismet.go Normal file
View File

@@ -0,0 +1,31 @@
package main
import (
"github.com/adtac/go-akismet/akismet"
"os"
)
func isSpam(domain string, userIp string, userAgent string, name string, email string, url string, markdown string) bool {
akismetKey := os.Getenv("AKISMET_KEY")
if akismetKey == "" {
return false
}
res, err := akismet.Check(&akismet.Comment{
Blog: domain,
UserIP: userIp,
UserAgent: userAgent,
CommentType: "comment",
CommentAuthor: name,
CommentAuthorEmail: email,
CommentAuthorURL: url,
CommentContent: markdown,
}, akismetKey)
if err != nil {
logger.Errorf("error: cannot validate commenet using Akismet: %v", err)
return true
}
return res
}

View File

@@ -77,20 +77,34 @@ func commentNewHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
if d.RequireIdentification && *x.CommenterToken == "anonymous" {
bodyMarshal(w, response{"success": false, "message": errorNotAuthorised.Error()})
return
}
// logic: (empty column indicates the value doesn't matter) // logic: (empty column indicates the value doesn't matter)
// | anonymous | moderator | requireIdentification | requireModeration | approved? | // | anonymous | moderator | requireIdentification | requireModeration | moderateAllAnonymous | approved? |
// |-----------+-----------+-----------------------+-------------------+-----------| // |-----------+-----------+-----------------------+-------------------+----------------------+-----------|
// | yes | | | | no | // | yes | | | | no | yes |
// | no | yes | | | yes | // | yes | | | | yes | no |
// | no | no | | yes | yes | // | no | yes | | | | yes |
// | no | no | | no | no | // | no | no | | yes | | yes |
// | no | no | | no | | no |
var commenterHex string var commenterHex string
var state string var state string
if *x.CommenterToken == "anonymous" { if *x.CommenterToken == "anonymous" {
state = "unapproved"
commenterHex = "anonymous" commenterHex = "anonymous"
if isSpam(*x.Domain, getIp(r), getUserAgent(r), "Anonymous", "", "", *x.Markdown) {
state = "flagged"
} else {
if d.ModerateAllAnonymous || d.RequireModeration {
state = "unapproved"
} else {
state = "approved"
}
}
} else { } else {
c, err := commenterGetByCommenterToken(*x.CommenterToken) c, err := commenterGetByCommenterToken(*x.CommenterToken)
if err != nil { if err != nil {
@@ -111,6 +125,9 @@ func commentNewHandler(w http.ResponseWriter, r *http.Request) {
if isModerator { if isModerator {
state = "approved" state = "approved"
} else {
if isSpam(*x.Domain, getIp(r), getUserAgent(r), c.Name, c.Email, c.Link, *x.Markdown) {
state = "flagged"
} else { } else {
if d.RequireModeration { if d.RequireModeration {
state = "unapproved" state = "unapproved"
@@ -119,6 +136,7 @@ func commentNewHandler(w http.ResponseWriter, r *http.Request) {
} }
} }
} }
}
commentHex, err := commentNew(commenterHex, domain, path, *x.ParentHex, *x.Markdown, state, time.Now().UTC()) commentHex, err := commentNew(commenterHex, domain, path, *x.ParentHex, *x.Markdown, state, time.Now().UTC())
if err != nil { if err != nil {
@@ -126,5 +144,11 @@ func commentNewHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
bodyMarshal(w, response{"success": true, "commentHex": commentHex, "approved": state == "approved"}) // TODO: reuse html in commentNew and do only one markdown to HTML conversion?
html := markdownToHtml(*x.Markdown)
bodyMarshal(w, response{"success": true, "commentHex": commentHex, "state": state, "html": html})
if smtpConfigured {
go emailNotificationNew(d, path, commenterHex, commentHex, *x.ParentHex, state)
}
} }

View File

@@ -66,5 +66,6 @@ func commenterGetByCommenterToken(commenterToken string) (commenter, error) {
return commenter{}, errorNoSuchToken return commenter{}, errorNoSuchToken
} }
// TODO: use a join instead of two queries?
return commenterGetByHex(commenterHex) return commenterGetByHex(commenterHex)
} }

View File

@@ -67,5 +67,12 @@ func commenterLoginHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
bodyMarshal(w, response{"success": true, "commenterToken": commenterToken}) // TODO: modify commenterLogin to directly return c?
c, err := commenterGetByCommenterToken(commenterToken)
if err != nil {
bodyMarshal(w, response{"success": false, "message": err.Error()})
return
}
bodyMarshal(w, response{"success": true, "commenterToken": commenterToken, "commenter": c})
} }

View File

@@ -27,6 +27,10 @@ func commenterNew(email string, name string, link string, photo string, provider
return "", errorEmailAlreadyExists return "", errorEmailAlreadyExists
} }
if err := emailNew(email); err != nil {
return "", errorInternal
}
commenterHex, err := randomHex(32) commenterHex, err := randomHex(32)
if err != nil { if err != nil {
return "", errorInternal return "", errorInternal

View File

@@ -21,5 +21,11 @@ func commenterSelfHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
bodyMarshal(w, response{"success": true, "commenter": c}) e, err := emailGet(c.Email)
if err != nil {
bodyMarshal(w, response{"success": false, "message": err.Error()})
return
}
bodyMarshal(w, response{"success": true, "commenter": c, "email": e})
} }

View File

@@ -43,8 +43,13 @@ func configParse() error {
"SMTP_PORT": "", "SMTP_PORT": "",
"SMTP_FROM_ADDRESS": "", "SMTP_FROM_ADDRESS": "",
"AKISMET_KEY": "",
"GOOGLE_KEY": "", "GOOGLE_KEY": "",
"GOOGLE_SECRET": "", "GOOGLE_SECRET": "",
"GITHUB_KEY": "",
"GITHUB_SECRET": "",
} }
for key, value := range defaults { for key, value := range defaults {

View File

@@ -1,4 +1,3 @@
package main package main
var edition = "ce" var version = "v1.6.2"
var version = "v1.1.1"

View File

@@ -0,0 +1,25 @@
package main
import (
"time"
)
func domainExportCleanupBegin() error {
go func() {
for {
statement := `
DELETE FROM exports
WHERE creationDate < $1;
`
_, err := db.Exec(statement, time.Now().UTC().AddDate(0, 0, -7))
if err != nil {
logger.Errorf("error cleaning up export rows: %v", err)
return
}
time.Sleep(2 * time.Hour)
}
}()
return nil
}

View File

@@ -0,0 +1,66 @@
package main
import (
"time"
)
func emailNotificationBegin() error {
go func() {
for {
statement := `
SELECT email, sendModeratorNotifications, sendReplyNotifications
FROM emails
WHERE pendingEmails > 0 AND lastEmailNotificationDate < $1;
`
rows, err := db.Query(statement, time.Now().UTC().Add(time.Duration(-10)*time.Minute))
if err != nil {
logger.Errorf("cannot query domains: %v", err)
return
}
defer rows.Close()
for rows.Next() {
var email string
var sendModeratorNotifications bool
var sendReplyNotifications bool
if err = rows.Scan(&email, &sendModeratorNotifications, &sendReplyNotifications); err != nil {
logger.Errorf("cannot scan email in cron job to send notifications: %v", err)
continue
}
if _, ok := emailQueue[email]; !ok {
if err = emailNotificationPendingReset(email); err != nil {
logger.Errorf("error resetting pendingEmails: %v", err)
continue
}
}
cont := true
kindListMap := map[string][]emailNotification{}
for cont {
select {
case e := <-emailQueue[email]:
if _, ok := kindListMap[e.Kind]; !ok {
kindListMap[e.Kind] = []emailNotification{}
}
if (e.Kind == "reply" && sendReplyNotifications) || sendModeratorNotifications {
kindListMap[e.Kind] = append(kindListMap[e.Kind], e)
}
default:
cont = false
break
}
}
for kind, list := range kindListMap {
go emailNotificationSend(email, kind, list)
}
}
time.Sleep(10 * time.Minute)
}
}()
return nil
}

25
api/cron_views_cleanup.go Normal file
View File

@@ -0,0 +1,25 @@
package main
import (
"time"
)
func viewsCleanupBegin() error {
go func() {
for {
statement := `
DELETE FROM views
WHERE viewDate < $1;
`
_, err := db.Exec(statement, time.Now().UTC().AddDate(0, 0, -45))
if err != nil {
logger.Errorf("error cleaning up views: %v", err)
return
}
time.Sleep(24 * time.Hour)
}
}()
return nil
}

View File

@@ -6,6 +6,10 @@ import (
"strings" "strings"
) )
var goMigrations = map[string](func() error){
"20190213033530-email-notifications.sql": migrateEmails,
}
func migrate() error { func migrate() error {
return migrateFromDir(os.Getenv("STATIC") + "/db") return migrateFromDir(os.Getenv("STATIC") + "/db")
} }
@@ -69,6 +73,13 @@ func migrateFromDir(dir string) error {
return err return err
} }
if fn, ok := goMigrations[file.Name()]; ok {
if err = fn(); err != nil {
logger.Errorf("cannot execute Go migration associated with SQL %s: %v", f, err)
return err
}
}
completed++ completed++
} }
} }

View File

@@ -0,0 +1,37 @@
package main
import ()
func migrateEmails() error {
statement := `
SELECT commenters.email
FROM commenters
UNION
SELECT owners.email
FROM owners
UNION
SELECT moderators.email
FROM moderators;
`
rows, err := db.Query(statement)
if err != nil {
logger.Errorf("cannot get comments: %v", err)
return errorDatabaseMigration
}
defer rows.Close()
for rows.Next() {
var email string
if err = rows.Scan(&email); err != nil {
logger.Errorf("cannot get email from tables during migration: %v", err)
return errorDatabaseMigration
}
if err = emailNew(email); err != nil {
logger.Errorf("cannot insert email during migration: %v", err)
return errorDatabaseMigration
}
}
return nil
}

View File

@@ -14,5 +14,7 @@ type domain struct {
AutoSpamFilter bool `json:"autoSpamFilter"` AutoSpamFilter bool `json:"autoSpamFilter"`
RequireModeration bool `json:"requireModeration"` RequireModeration bool `json:"requireModeration"`
RequireIdentification bool `json:"requireIdentification"` RequireIdentification bool `json:"requireIdentification"`
ModerateAllAnonymous bool `json:"moderateAllAnonymous"`
Moderators []moderator `json:"moderators"` Moderators []moderator `json:"moderators"`
EmailNotificationPolicy string `json:"emailNotificationPolicy"`
} }

View File

@@ -60,6 +60,16 @@ func domainDelete(domain string) error {
return errorInternal return errorInternal
} }
statement = `
DELETE FROM pages
WHERE pages.domain = $1;
`
_, err = db.Exec(statement, domain)
if err != nil {
logger.Errorf(statement, domain)
return errorInternal
}
return nil return nil
} }

151
api/domain_export.go Normal file
View File

@@ -0,0 +1,151 @@
package main
import (
"encoding/json"
"net/http"
"time"
)
func domainExportBeginError(email string, toName string, domain string, err error) {
// we're not using err at the moment because it's all errorInternal
if err2 := smtpDomainExportError(email, toName, domain); err2 != nil {
logger.Errorf("cannot send domain export error email for %s: %v", domain, err2)
return
}
}
func domainExportBegin(email string, toName string, domain string) {
type dataExport struct {
Version int `json:"version"`
Comments []comment `json:"comments"`
Commenters []commenter `json:"commenters"`
}
e := dataExport{Version: 1, Comments: []comment{}, Commenters: []commenter{}}
statement := `
SELECT commentHex, domain, path, commenterHex, markdown, parentHex, score, state, creationDate
FROM comments
WHERE domain = $1;
`
rows1, err := db.Query(statement, domain)
if err != nil {
logger.Errorf("cannot select comments while exporting %s: %v", domain, err)
domainExportBeginError(email, toName, domain, errorInternal)
return
}
defer rows1.Close()
for rows1.Next() {
c := comment{}
if err = rows1.Scan(&c.CommentHex, &c.Domain, &c.Path, &c.CommenterHex, &c.Markdown, &c.ParentHex, &c.Score, &c.State, &c.CreationDate); err != nil {
logger.Errorf("cannot scan comment while exporting %s: %v", domain, err)
domainExportBeginError(email, toName, domain, errorInternal)
return
}
e.Comments = append(e.Comments, c)
}
statement = `
SELECT commenters.commenterHex, commenters.email, commenters.name, commenters.link, commenters.photo, commenters.provider, commenters.joinDate
FROM commenters, comments
WHERE comments.domain = $1 AND commenters.commenterHex = comments.commenterHex;
`
rows2, err := db.Query(statement, domain)
if err != nil {
logger.Errorf("cannot select commenters while exporting %s: %v", domain, err)
domainExportBeginError(email, toName, domain, errorInternal)
return
}
defer rows2.Close()
for rows2.Next() {
c := commenter{}
if err := rows2.Scan(&c.CommenterHex, &c.Email, &c.Name, &c.Link, &c.Photo, &c.Provider, &c.JoinDate); err != nil {
logger.Errorf("cannot scan commenter while exporting %s: %v", domain, err)
domainExportBeginError(email, toName, domain, errorInternal)
return
}
e.Commenters = append(e.Commenters, c)
}
je, err := json.Marshal(e)
if err != nil {
logger.Errorf("cannot marshall JSON while exporting %s: %v", domain, err)
domainExportBeginError(email, toName, domain, errorInternal)
return
}
gje, err := gzipStatic(je)
if err != nil {
logger.Errorf("cannot gzip JSON while exporting %s: %v", domain, err)
domainExportBeginError(email, toName, domain, errorInternal)
return
}
exportHex, err := randomHex(32)
if err != nil {
logger.Errorf("cannot generate exportHex while exporting %s: %v", domain, err)
domainExportBeginError(email, toName, domain, errorInternal)
return
}
statement = `
INSERT INTO
exports (exportHex, binData, domain, creationDate)
VALUES ($1, $2, $3 , $4 );
`
_, err = db.Exec(statement, exportHex, gje, domain, time.Now().UTC())
if err != nil {
logger.Errorf("error inserting expiry binary data while exporting %s: %v", domain, err)
domainExportBeginError(email, toName, domain, errorInternal)
return
}
err = smtpDomainExport(email, toName, domain, exportHex)
if err != nil {
logger.Errorf("error sending data export email for %s: %v", domain, err)
return
}
}
func domainExportBeginHandler(w http.ResponseWriter, r *http.Request) {
type request struct {
OwnerToken *string `json:"ownerToken"`
Domain *string `json:"domain"`
}
var x request
if err := bodyUnmarshal(r, &x); err != nil {
bodyMarshal(w, response{"success": false, "message": err.Error()})
return
}
if !smtpConfigured {
bodyMarshal(w, response{"success": false, "message": errorSmtpNotConfigured.Error()})
return
}
o, err := ownerGetByOwnerToken(*x.OwnerToken)
if err != nil {
bodyMarshal(w, response{"success": false, "message": err.Error()})
return
}
isOwner, err := domainOwnershipVerify(o.OwnerHex, *x.Domain)
if err != nil {
bodyMarshal(w, response{"success": false, "message": err.Error()})
return
}
if !isOwner {
bodyMarshal(w, response{"success": false, "message": errorNotAuthorised.Error()})
return
}
go domainExportBegin(o.Email, o.Name, *x.Domain)
bodyMarshal(w, response{"success": true})
}

View File

@@ -0,0 +1,33 @@
package main
import (
"fmt"
"net/http"
"time"
)
func domainExportDownloadHandler(w http.ResponseWriter, r *http.Request) {
exportHex := r.FormValue("exportHex")
if exportHex == "" {
fmt.Fprintf(w, "Error: empty exportHex\n")
return
}
statement := `
SELECT domain, binData, creationDate
FROM exports
WHERE exportHex = $1;
`
row := db.QueryRow(statement, exportHex)
var domain string
var binData []byte
var creationDate time.Time
if err := row.Scan(&domain, &binData, &creationDate); err != nil {
fmt.Fprintf(w, "Error: that exportHex does not exist\n")
}
w.Header().Set("Content-Disposition", fmt.Sprintf(`inline; filename="%s-%v.gz"`, domain, creationDate.Unix()))
w.Header().Set("Content-Encoding", "gzip")
w.Write(binData)
}

View File

@@ -8,7 +8,7 @@ func domainGet(dmn string) (domain, error) {
} }
statement := ` statement := `
SELECT domain, ownerHex, name, creationDate, state, importedComments, autoSpamFilter, requireModeration, requireIdentification SELECT domain, ownerHex, name, creationDate, state, importedComments, autoSpamFilter, requireModeration, requireIdentification, moderateAllAnonymous, emailNotificationPolicy
FROM domains FROM domains
WHERE domain = $1; WHERE domain = $1;
` `
@@ -16,7 +16,7 @@ func domainGet(dmn string) (domain, error) {
var err error var err error
d := domain{} d := domain{}
if err = row.Scan(&d.Domain, &d.OwnerHex, &d.Name, &d.CreationDate, &d.State, &d.ImportedComments, &d.AutoSpamFilter, &d.RequireModeration, &d.RequireIdentification); err != nil { if err = row.Scan(&d.Domain, &d.OwnerHex, &d.Name, &d.CreationDate, &d.State, &d.ImportedComments, &d.AutoSpamFilter, &d.RequireModeration, &d.RequireIdentification, &d.ModerateAllAnonymous, &d.EmailNotificationPolicy); err != nil {
return d, errorNoSuchDomain return d, errorNoSuchDomain
} }

View File

@@ -18,9 +18,9 @@ type disqusThread struct {
type disqusAuthor struct { type disqusAuthor struct {
XMLName xml.Name `xml:"author"` XMLName xml.Name `xml:"author"`
IsAnonymous bool `xml:"isAnonymous"`
Name string `xml:"name"` Name string `xml:"name"`
Email string `xml:"email"` IsAnonymous bool `xml:"isAnonymous"`
Username string `xml:"username"`
} }
type disqusThreadId struct { type disqusThreadId struct {
@@ -43,7 +43,6 @@ type disqusPost struct {
Id string `xml:"http://disqus.com/disqus-internals id,attr"` Id string `xml:"http://disqus.com/disqus-internals id,attr"`
ThreadId disqusThreadId `xml:"thread"` ThreadId disqusThreadId `xml:"thread"`
ParentId disqusParentId `xml:"parent"` ParentId disqusParentId `xml:"parent"`
PostId disqusPostId `xml:"post"`
Message string `xml:"message"` Message string `xml:"message"`
CreationDate time.Time `xml:"createdAt"` CreationDate time.Time `xml:"createdAt"`
IsDeleted bool `xml:"isDeleted"` IsDeleted bool `xml:"isDeleted"`
@@ -98,24 +97,26 @@ func domainImportDisqus(domain string, url string) (int, error) {
// Map Disqus emails to commenterHex (if not available, create a new one // Map Disqus emails to commenterHex (if not available, create a new one
// with a random password that can be reset later). // with a random password that can be reset later).
commenterHex := make(map[string]string) commenterHex := map[string]string{}
for _, post := range x.Posts { for _, post := range x.Posts {
if post.IsDeleted || post.IsSpam { if post.IsDeleted || post.IsSpam {
continue continue
} }
if _, ok := commenterHex[post.Author.Email]; ok { email := post.Author.Username + "@disqus.com"
if _, ok := commenterHex[email]; ok {
continue continue
} }
c, err := commenterGetByEmail("commento", post.Author.Email) c, err := commenterGetByEmail("commento", email)
if err != nil && err != errorNoSuchCommenter { if err != nil && err != errorNoSuchCommenter {
logger.Errorf("cannot get commenter by email: %v", err) logger.Errorf("cannot get commenter by email: %v", err)
return 0, errorInternal return 0, errorInternal
} }
if err == nil { if err == nil {
commenterHex[post.Author.Email] = c.CommenterHex commenterHex[email] = c.CommenterHex
continue continue
} }
@@ -125,7 +126,7 @@ func domainImportDisqus(domain string, url string) (int, error) {
return 0, errorInternal return 0, errorInternal
} }
commenterHex[post.Author.Email], err = commenterNew(post.Author.Email, post.Author.Name, "undefined", "undefined", "commento", randomPassword) commenterHex[email], err = commenterNew(email, post.Author.Name, "undefined", "undefined", "commento", randomPassword)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@@ -134,12 +135,17 @@ func domainImportDisqus(domain string, url string) (int, error) {
// For each Disqus post, create a Commento comment. Attempt to convert the // For each Disqus post, create a Commento comment. Attempt to convert the
// HTML to markdown. // HTML to markdown.
numImported := 0 numImported := 0
disqusIdMap := make(map[string]string) disqusIdMap := map[string]string{}
for _, post := range x.Posts { for _, post := range x.Posts {
if post.IsDeleted || post.IsSpam { if post.IsDeleted || post.IsSpam {
continue continue
} }
cHex := "anonymous"
if !post.Author.IsAnonymous {
cHex = commenterHex[post.Author.Username+"@disqus.com"]
}
parentHex := "root" parentHex := "root"
if val, ok := disqusIdMap[post.ParentId.Id]; ok { if val, ok := disqusIdMap[post.ParentId.Id]; ok {
parentHex = val parentHex = val
@@ -148,7 +154,7 @@ func domainImportDisqus(domain string, url string) (int, error) {
// TODO: restrict the list of tags to just the basics: <a>, <b>, <i>, <code> // TODO: restrict the list of tags to just the basics: <a>, <b>, <i>, <code>
// Especially remove <img> (convert it to <a>). // Especially remove <img> (convert it to <a>).
commentHex, err := commentNew( commentHex, err := commentNew(
commenterHex[post.Author.Email], cHex,
domain, domain,
pathStrip(threads[post.ThreadId.Id].URL), pathStrip(threads[post.ThreadId.Id].URL),
parentHex, parentHex,
@@ -159,7 +165,7 @@ func domainImportDisqus(domain string, url string) (int, error) {
return numImported, err return numImported, err
} }
disqusIdMap[post.PostId.Id] = commentHex disqusIdMap[post.Id] = commentHex
numImported += 1 numImported += 1
} }

View File

@@ -10,7 +10,7 @@ func domainList(ownerHex string) ([]domain, error) {
} }
statement := ` statement := `
SELECT domain, ownerHex, name, creationDate, state, importedComments, autoSpamFilter, requireModeration, requireIdentification SELECT domain, ownerHex, name, creationDate, state, importedComments, autoSpamFilter, requireModeration, requireIdentification, moderateAllAnonymous, emailNotificationPolicy
FROM domains FROM domains
WHERE ownerHex=$1; WHERE ownerHex=$1;
` `
@@ -24,7 +24,7 @@ func domainList(ownerHex string) ([]domain, error) {
domains := []domain{} domains := []domain{}
for rows.Next() { for rows.Next() {
d := domain{} d := domain{}
if err = rows.Scan(&d.Domain, &d.OwnerHex, &d.Name, &d.CreationDate, &d.State, &d.ImportedComments, &d.AutoSpamFilter, &d.RequireModeration, &d.RequireIdentification); err != nil { if err = rows.Scan(&d.Domain, &d.OwnerHex, &d.Name, &d.CreationDate, &d.State, &d.ImportedComments, &d.AutoSpamFilter, &d.RequireModeration, &d.RequireIdentification, &d.ModerateAllAnonymous, &d.EmailNotificationPolicy); err != nil {
logger.Errorf("cannot Scan domain: %v", err) logger.Errorf("cannot Scan domain: %v", err)
return nil, errorInternal return nil, errorInternal
} }

View File

@@ -10,6 +10,11 @@ func domainModeratorNew(domain string, email string) error {
return errorMissingField return errorMissingField
} }
if err := emailNew(email); err != nil {
logger.Errorf("cannot create email when creating moderator: %v", err)
return errorInternal
}
statement := ` statement := `
INSERT INTO INSERT INTO
moderators (domain, email, addDate) moderators (domain, email, addDate)

View File

@@ -7,11 +7,11 @@ import (
func domainUpdate(d domain) error { func domainUpdate(d domain) error {
statement := ` statement := `
UPDATE domains UPDATE domains
SET name=$2, state=$3, autoSpamFilter=$4, requireModeration=$5, requireIdentification=$6 SET name=$2, state=$3, autoSpamFilter=$4, requireModeration=$5, requireIdentification=$6, moderateAllAnonymous=$7, emailNotificationPolicy=$8
WHERE domain=$1; WHERE domain=$1;
` `
_, err := db.Exec(statement, d.Domain, d.Name, d.State, d.AutoSpamFilter, d.RequireModeration, d.RequireIdentification) _, err := db.Exec(statement, d.Domain, d.Name, d.State, d.AutoSpamFilter, d.RequireModeration, d.RequireIdentification, d.ModerateAllAnonymous, d.EmailNotificationPolicy)
if err != nil { if err != nil {
logger.Errorf("cannot update non-moderators: %v", err) logger.Errorf("cannot update non-moderators: %v", err)
return errorInternal return errorInternal

14
api/email.go Normal file
View File

@@ -0,0 +1,14 @@
package main
import (
"time"
)
type email struct {
Email string `json:"email"`
UnsubscribeSecretHex string `json:"unsubscribeSecretHex"`
LastEmailNotificationDate time.Time `json:"lastEmailNotificationDate"`
PendingEmails int `json:"-"`
SendReplyNotifications bool `json:"sendReplyNotifications"`
SendModeratorNotifications bool `json:"sendModeratorNotifications"`
}

59
api/email_get.go Normal file
View File

@@ -0,0 +1,59 @@
package main
import (
"net/http"
)
func emailGet(em string) (email, error) {
statement := `
SELECT email, unsubscribeSecretHex, lastEmailNotificationDate, pendingEmails, sendReplyNotifications, sendModeratorNotifications
FROM emails
WHERE email = $1;
`
row := db.QueryRow(statement, em)
e := email{}
if err := row.Scan(&e.Email, &e.UnsubscribeSecretHex, &e.LastEmailNotificationDate, &e.PendingEmails, &e.SendReplyNotifications, &e.SendModeratorNotifications); err != nil {
// TODO: is this the only error?
return e, errorNoSuchEmail
}
return e, nil
}
func emailGetByUnsubscribeSecretHex(unsubscribeSecretHex string) (email, error) {
statement := `
SELECT email, unsubscribeSecretHex, lastEmailNotificationDate, pendingEmails, sendReplyNotifications, sendModeratorNotifications
FROM emails
WHERE unsubscribeSecretHex = $1;
`
row := db.QueryRow(statement, unsubscribeSecretHex)
e := email{}
if err := row.Scan(&e.Email, &e.UnsubscribeSecretHex, &e.LastEmailNotificationDate, &e.PendingEmails, &e.SendReplyNotifications, &e.SendModeratorNotifications); err != nil {
// TODO: is this the only error?
return e, errorNoSuchUnsubscribeSecretHex
}
return e, nil
}
func emailGetHandler(w http.ResponseWriter, r *http.Request) {
type request struct {
UnsubscribeSecretHex *string `json:"unsubscribeSecretHex"`
}
var x request
if err := bodyUnmarshal(r, &x); err != nil {
bodyMarshal(w, response{"success": false, "message": err.Error()})
return
}
e, err := emailGetByUnsubscribeSecretHex(*x.UnsubscribeSecretHex)
if err != nil {
bodyMarshal(w, response{"success": false, "message": err.Error()})
return
}
bodyMarshal(w, response{"success": true, "email": e})
}

66
api/email_moderate.go Normal file
View File

@@ -0,0 +1,66 @@
package main
import (
"fmt"
"net/http"
)
func emailModerateHandler(w http.ResponseWriter, r *http.Request) {
unsubscribeSecretHex := r.FormValue("unsubscribeSecretHex")
e, err := emailGetByUnsubscribeSecretHex(unsubscribeSecretHex)
if err != nil {
fmt.Fprintf(w, "error: %v", err.Error())
return
}
action := r.FormValue("action")
if action != "delete" && action != "approve" {
fmt.Fprintf(w, "error: invalid action")
return
}
commentHex := r.FormValue("commentHex")
if commentHex == "" {
fmt.Fprintf(w, "error: invalid commentHex")
return
}
statement := `
SELECT domain
FROM comments
WHERE commentHex = $1;
`
row := db.QueryRow(statement, commentHex)
var domain string
if err = row.Scan(&domain); err != nil {
// TODO: is this the only error?
fmt.Fprintf(w, "error: no such comment found (perhaps it has been deleted?)")
return
}
isModerator, err := isDomainModerator(domain, e.Email)
if err != nil {
logger.Errorf("error checking if %s is a moderator: %v", e.Email, err)
fmt.Fprintf(w, "error checking if %s is a moderator: %v", e.Email, err)
return
}
if !isModerator {
fmt.Fprintf(w, "error: you're not a moderator for that domain")
return
}
if action == "approve" {
err = commentApprove(commentHex)
} else {
err = commentDelete(commentHex)
}
if err != nil {
fmt.Fprintf(w, "error: %v", err)
return
}
fmt.Fprintf(w, "comment successfully %sd", action)
}

26
api/email_new.go Normal file
View File

@@ -0,0 +1,26 @@
package main
import (
"time"
)
func emailNew(email string) error {
unsubscribeSecretHex, err := randomHex(32)
if err != nil {
return errorInternal
}
statement := `
INSERT INTO
emails (email, unsubscribeSecretHex, lastEmailNotificationDate)
VALUES ($1, $2, $3 )
ON CONFLICT DO NOTHING;
`
_, err = db.Exec(statement, email, unsubscribeSecretHex, time.Now().UTC())
if err != nil {
logger.Errorf("cannot insert email into emails: %v", err)
return errorInternal
}
return nil
}

81
api/email_notification.go Normal file
View File

@@ -0,0 +1,81 @@
package main
import (
"time"
)
type emailNotification struct {
Email string
CommenterName string
Domain string
Path string
Title string
CommentHex string
Kind string
}
var emailQueue map[string](chan emailNotification) = map[string](chan emailNotification){}
func emailNotificationPendingResetAll() error {
statement := `
UPDATE emails
SET pendingEmails = 0;
`
_, err := db.Exec(statement)
if err != nil {
logger.Errorf("cannot reset pendingEmails: %v", err)
return err
}
return nil
}
func emailNotificationPendingIncrement(email string) error {
statement := `
UPDATE emails
SET pendingEmails = pendingEmails + 1
WHERE email = $1;
`
_, err := db.Exec(statement, email)
if err != nil {
logger.Errorf("cannot increment pendingEmails: %v", err)
return err
}
return nil
}
func emailNotificationPendingReset(email string) error {
statement := `
UPDATE emails
SET pendingEmails = 0, lastEmailNotificationDate = $2
WHERE email = $1;
`
_, err := db.Exec(statement, email, time.Now().UTC())
if err != nil {
logger.Errorf("cannot decrement pendingEmails: %v", err)
return err
}
return nil
}
func emailNotificationEnqueue(e emailNotification) error {
if err := emailNotificationPendingIncrement(e.Email); err != nil {
logger.Errorf("cannot increment pendingEmails when enqueueing: %v", err)
return err
}
if _, ok := emailQueue[e.Email]; !ok {
// don't enqueue more than 10 emails as we won't send more than 10 comments
// in one email anyway
emailQueue[e.Email] = make(chan emailNotification, 10)
}
select {
case emailQueue[e.Email] <- e:
default:
}
return nil
}

View File

@@ -0,0 +1,138 @@
package main
import ()
func emailNotificationModerator(d domain, path string, title string, commenterHex string, commentHex string, state string) {
if d.EmailNotificationPolicy == "none" {
return
}
// We'll need to check again when we're sending in case the comment was
// approved midway anyway.
if d.EmailNotificationPolicy == "pending-moderation" && state == "approved" {
return
}
var commenterName string
var commenterEmail string
if commenterHex == "anonymous" {
commenterName = "Anonymous"
} else {
c, err := commenterGetByHex(commenterHex)
if err != nil {
logger.Errorf("cannot get commenter to send email notification: %v", err)
return
}
commenterName = c.Name
commenterEmail = c.Email
}
kind := d.EmailNotificationPolicy
if state != "approved" {
kind = "pending-moderation"
}
for _, m := range d.Moderators {
// Do not email the commenting moderator their own comment.
if commenterHex != "anonymous" && m.Email == commenterEmail {
continue
}
emailNotificationPendingIncrement(m.Email)
emailNotificationEnqueue(emailNotification{
Email: m.Email,
CommenterName: commenterName,
Domain: d.Domain,
Path: path,
Title: title,
CommentHex: commentHex,
Kind: kind,
})
}
}
func emailNotificationReply(d domain, path string, title string, commenterHex string, commentHex string, parentHex string, state string) {
// No reply notifications for root comments.
if parentHex == "root" {
return
}
// No reply notification emails for unapproved comments.
if state != "approved" {
return
}
statement := `
SELECT commenterHex
FROM comments
WHERE commentHex = $1;
`
row := db.QueryRow(statement, parentHex)
var parentCommenterHex string
err := row.Scan(&parentCommenterHex)
if err != nil {
logger.Errorf("cannot scan commenterHex and parentCommenterHex: %v", err)
return
}
// No reply notification emails for anonymous users.
if parentCommenterHex == "anonymous" {
return
}
// No reply notification email for self replies.
if parentCommenterHex == commenterHex {
return
}
pc, err := commenterGetByHex(parentCommenterHex)
if err != nil {
logger.Errorf("cannot get commenter to send email notification: %v", err)
return
}
var commenterName string
if commenterHex == "anonymous" {
commenterName = "Anonymous"
} else {
c, err := commenterGetByHex(commenterHex)
if err != nil {
logger.Errorf("cannot get commenter to send email notification: %v", err)
return
}
commenterName = c.Name
}
// We'll check if they want to receive reply notifications later at the time
// of sending.
emailNotificationEnqueue(emailNotification{
Email: pc.Email,
CommenterName: commenterName,
Domain: d.Domain,
Path: path,
Title: title,
CommentHex: commentHex,
Kind: "reply",
})
}
func emailNotificationNew(d domain, path string, commenterHex string, commentHex string, parentHex string, state string) {
p, err := pageGet(d.Domain, path)
if err != nil {
logger.Errorf("cannot get page to send email notification: %v", err)
return
}
if p.Title == "" {
p.Title, err = pageTitleUpdate(d.Domain, path)
if err != nil {
logger.Errorf("cannot update/get page title to send email notification: %v", err)
return
}
}
emailNotificationModerator(d, path, p.Title, commenterHex, commentHex, state)
emailNotificationReply(d, path, p.Title, commenterHex, commentHex, parentHex, state)
}

View File

@@ -0,0 +1,63 @@
package main
import (
"html/template"
)
func emailNotificationSend(em string, kind string, notifications []emailNotification) {
if len(notifications) == 0 {
return
}
e, err := emailGet(em)
if err != nil {
logger.Errorf("cannot get email: %v", err)
return
}
messages := []emailNotificationText{}
for _, notification := range notifications {
statement := `
SELECT html
FROM comments
WHERE commentHex = $1;
`
row := db.QueryRow(statement, notification.CommentHex)
var html string
if err = row.Scan(&html); err != nil {
// the comment was deleted?
// TODO: is this the only error?
return
}
messages = append(messages, emailNotificationText{
emailNotification: notification,
Html: template.HTML(html),
})
}
statement := `
SELECT name
FROM commenters
WHERE email = $1;
`
row := db.QueryRow(statement, em)
var name string
if err := row.Scan(&name); err != nil {
// The moderator has probably not created a commenter account. Let's just
// use their email as name.
name = nameFromEmail(em)
}
if err := emailNotificationPendingReset(em); err != nil {
logger.Errorf("cannot reset after email notification: %v", err)
return
}
if err := smtpEmailNotification(em, name, e.UnsubscribeSecretHex, messages, kind); err != nil {
logger.Errorf("cannot send email notification: %v", err)
return
}
}

39
api/email_update.go Normal file
View File

@@ -0,0 +1,39 @@
package main
import (
"net/http"
)
func emailUpdate(e email) error {
statement := `
UPDATE emails
SET sendReplyNotifications = $3, sendModeratorNotifications = $4
WHERE email = $1 AND unsubscribeSecretHex = $2;
`
_, err := db.Exec(statement, e.Email, e.UnsubscribeSecretHex, e.SendReplyNotifications, e.SendModeratorNotifications)
if err != nil {
logger.Errorf("error updating email: %v", err)
return errorInternal
}
return nil
}
func emailUpdateHandler(w http.ResponseWriter, r *http.Request) {
type request struct {
Email *email `json:"email"`
}
var x request
if err := bodyUnmarshal(r, &x); err != nil {
bodyMarshal(w, response{"success": false, "message": err.Error()})
return
}
if err := emailUpdate(*x.Email); err != nil {
bodyMarshal(w, response{"success": true, "message": err.Error()})
return
}
bodyMarshal(w, response{"success": true})
}

View File

@@ -42,3 +42,5 @@ var errorInvalidConfigFile = errors.New("Invalid config file.")
var errorInvalidConfigValue = errors.New("Invalid config value.") var errorInvalidConfigValue = errors.New("Invalid config value.")
var errorNewOwnerForbidden = errors.New("New user registrations are forbidden and closed.") var errorNewOwnerForbidden = errors.New("New user registrations are forbidden and closed.")
var errorThreadLocked = errors.New("This thread is locked. You cannot add new comments.") var errorThreadLocked = errors.New("This thread is locked. You cannot add new comments.")
var errorDatabaseMigration = errors.New("Encountered error applying database migration.")
var errorNoSuchUnsubscribeSecretHex = errors.New("Invalid unsubscribe link.")

View File

@@ -9,8 +9,12 @@ func main() {
exitIfError(smtpTemplatesLoad()) exitIfError(smtpTemplatesLoad())
exitIfError(oauthConfigure()) exitIfError(oauthConfigure())
exitIfError(markdownRendererCreate()) exitIfError(markdownRendererCreate())
exitIfError(emailNotificationPendingResetAll())
exitIfError(emailNotificationBegin())
exitIfError(sigintCleanupSetup()) exitIfError(sigintCleanupSetup())
exitIfError(versionCheckStart()) exitIfError(versionCheckStart())
exitIfError(domainExportCleanupBegin())
exitIfError(viewsCleanupBegin())
exitIfError(routesServe()) exitIfError(routesServe())
} }

View File

@@ -11,5 +11,9 @@ func oauthConfigure() error {
return err return err
} }
if err := githubOauthConfigure(); err != nil {
return err
}
return nil return nil
} }

43
api/oauth_github.go Normal file
View File

@@ -0,0 +1,43 @@
package main
import (
"golang.org/x/oauth2"
"golang.org/x/oauth2/github"
"os"
)
var githubConfig *oauth2.Config
func githubOauthConfigure() error {
githubConfig = nil
if os.Getenv("GITHUB_KEY") == "" && os.Getenv("GITHUB_SECRET") == "" {
return nil
}
if os.Getenv("GITHUB_KEY") == "" {
logger.Errorf("COMMENTO_GITHUB_KEY not configured, but COMMENTO_GITHUB_SECRET is set")
return errorOauthMisconfigured
}
if os.Getenv("GITHUB_SECRET") == "" {
logger.Errorf("COMMENTO_GITHUB_SECRET not configured, but COMMENTO_GITHUB_KEY is set")
return errorOauthMisconfigured
}
logger.Infof("loading github OAuth config")
githubConfig = &oauth2.Config{
RedirectURL: os.Getenv("ORIGIN") + "/api/oauth/github/callback",
ClientID: os.Getenv("GITHUB_KEY"),
ClientSecret: os.Getenv("GITHUB_SECRET"),
Scopes: []string{
"read:user",
"user:email",
},
Endpoint: github.Endpoint,
}
configuredOauths = append(configuredOauths, "github")
return nil
}

View File

@@ -0,0 +1,115 @@
package main
import (
"encoding/json"
"fmt"
"golang.org/x/oauth2"
"io/ioutil"
"net/http"
)
func githubGetPrimaryEmail(accessToken string) (string, error) {
resp, err := http.Get("https://api.github.com/user/emails?access_token=" + accessToken)
defer resp.Body.Close()
contents, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", errorCannotReadResponse
}
user := []map[string]interface{}{}
if err := json.Unmarshal(contents, &user); err != nil {
logger.Errorf("error unmarshaling github user: %v", err)
return "", errorInternal
}
nonPrimaryEmail := ""
for _, email := range user {
nonPrimaryEmail = email["email"].(string)
if email["primary"].(bool) {
return email["email"].(string), nil
}
}
return nonPrimaryEmail, nil
}
func githubCallbackHandler(w http.ResponseWriter, r *http.Request) {
commenterToken := r.FormValue("state")
code := r.FormValue("code")
_, err := commenterGetByCommenterToken(commenterToken)
if err != nil && err != errorNoSuchToken {
fmt.Fprintf(w, "Error: %s\n", err.Error())
return
}
token, err := githubConfig.Exchange(oauth2.NoContext, code)
if err != nil {
fmt.Fprintf(w, "Error: %s", err.Error())
return
}
email, err := githubGetPrimaryEmail(token.AccessToken)
if err != nil {
fmt.Fprintf(w, "Error: %s", err.Error())
return
}
resp, err := http.Get("https://api.github.com/user?access_token=" + token.AccessToken)
defer resp.Body.Close()
contents, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Fprintf(w, "Error: %s", errorCannotReadResponse.Error())
return
}
user := make(map[string]interface{})
if err := json.Unmarshal(contents, &user); err != nil {
fmt.Fprintf(w, "Error: %s", errorInternal.Error())
return
}
if email == "" {
if user["email"] == nil {
fmt.Fprintf(w, "Error: no email address returned by Github")
return
}
email = user["email"].(string)
}
c, err := commenterGetByEmail("github", email)
if err != nil && err != errorNoSuchCommenter {
fmt.Fprintf(w, "Error: %s", err.Error())
return
}
var commenterHex string
// TODO: in case of returning users, update the information we have on record?
if err == errorNoSuchCommenter {
var link string
if val, ok := user["html_url"]; ok {
link = val.(string)
} else {
link = "undefined"
}
commenterHex, err = commenterNew(email, user["name"].(string), link, user["avatar_url"].(string), "github", "")
if err != nil {
fmt.Fprintf(w, "Error: %s", err.Error())
return
}
} else {
commenterHex = c.CommenterHex
}
if err := commenterSessionUpdate(commenterToken, commenterHex); err != nil {
fmt.Fprintf(w, "Error: %s", err.Error())
return
}
fmt.Fprintf(w, "<html><script>window.parent.close()</script></html>")
}

View File

@@ -0,0 +1,25 @@
package main
import (
"fmt"
"net/http"
)
func githubRedirectHandler(w http.ResponseWriter, r *http.Request) {
if githubConfig == nil {
logger.Errorf("github oauth access attempt without configuration")
fmt.Fprintf(w, "error: this website has not configured github OAuth")
return
}
commenterToken := r.FormValue("commenterToken")
_, err := commenterGetByCommenterToken(commenterToken)
if err != nil && err != errorNoSuchToken {
fmt.Fprintf(w, "error: %s\n", err.Error())
return
}
url := githubConfig.AuthCodeURL(commenterToken)
http.Redirect(w, r, url, http.StatusFound)
}

View File

@@ -39,7 +39,14 @@ func googleCallbackHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
c, err := commenterGetByEmail("google", user["email"].(string)) if user["email"] == nil {
fmt.Fprintf(w, "Error: no email address returned by Github")
return
}
email := user["email"].(string)
c, err := commenterGetByEmail("google", email)
if err != nil && err != errorNoSuchCommenter { if err != nil && err != errorNoSuchCommenter {
fmt.Fprintf(w, "Error: %s", err.Error()) fmt.Fprintf(w, "Error: %s", err.Error())
return return
@@ -49,14 +56,6 @@ func googleCallbackHandler(w http.ResponseWriter, r *http.Request) {
// TODO: in case of returning users, update the information we have on record? // TODO: in case of returning users, update the information we have on record?
if err == errorNoSuchCommenter { if err == errorNoSuchCommenter {
var email string
if _, ok := user["email"]; ok {
email = user["email"].(string)
} else {
fmt.Fprintf(w, "Error: %s", errorInvalidEmail.Error())
return
}
var link string var link string
if val, ok := user["link"]; ok { if val, ok := user["link"]; ok {
link = val.(string) link = val.(string)

View File

@@ -31,8 +31,8 @@ func ownerGetByOwnerToken(ownerToken string) (owner, error) {
statement := ` statement := `
SELECT ownerHex, email, name, confirmedEmail, joinDate SELECT ownerHex, email, name, confirmedEmail, joinDate
FROM owners FROM owners
WHERE email IN ( WHERE ownerHex IN (
SELECT email FROM ownerSessions SELECT ownerHex FROM ownerSessions
WHERE ownerToken = $1 WHERE ownerToken = $1
); );
` `
@@ -46,3 +46,24 @@ func ownerGetByOwnerToken(ownerToken string) (owner, error) {
return o, nil return o, nil
} }
func ownerGetByOwnerHex(ownerHex string) (owner, error) {
if ownerHex == "" {
return owner{}, errorMissingField
}
statement := `
SELECT ownerHex, email, name, confirmedEmail, joinDate
FROM owners
WHERE ownerHex = $1;
`
row := db.QueryRow(statement, ownerHex)
var o owner
if err := row.Scan(&o.OwnerHex, &o.Email, &o.Name, &o.ConfirmedEmail, &o.JoinDate); err != nil {
logger.Errorf("cannot scan owner: %v\n", err)
return owner{}, errorInternal
}
return o, nil
}

View File

@@ -16,6 +16,14 @@ func ownerNew(email string, name string, password string) (string, error) {
return "", errorNewOwnerForbidden return "", errorNewOwnerForbidden
} }
if _, err := ownerGetByEmail(email); err == nil {
return "", errorEmailAlreadyExists
}
if err := emailNew(email); err != nil {
return "", errorInternal
}
ownerHex, err := randomHex(32) ownerHex, err := randomHex(32)
if err != nil { if err != nil {
logger.Errorf("cannot generate ownerHex: %v", err) logger.Errorf("cannot generate ownerHex: %v", err)

View File

@@ -18,8 +18,9 @@ func ownerResetPassword(resetHex string, password string) error {
statement := ` statement := `
UPDATE owners SET passwordHash=$1 UPDATE owners SET passwordHash=$1
WHERE email IN ( WHERE ownerHex = (
SELECT email FROM ownerResetHexes SELECT ownerHex
FROM ownerResetHexes
WHERE resetHex=$2 WHERE resetHex=$2
); );
` `

View File

@@ -7,4 +7,6 @@ type page struct {
Path string `json:"path"` Path string `json:"path"`
IsLocked bool `json:"isLocked"` IsLocked bool `json:"isLocked"`
CommentCount int `json:"commentCount"` CommentCount int `json:"commentCount"`
StickyCommentHex string `json:"stickyCommentHex"`
Title string `json:"title"`
} }

View File

@@ -11,20 +11,22 @@ func pageGet(domain string, path string) (page, error) {
} }
statement := ` statement := `
SELECT isLocked, commentCount SELECT isLocked, commentCount, stickyCommentHex, title
FROM pages FROM pages
WHERE domain=$1 AND path=$2; WHERE domain=$1 AND path=$2;
` `
row := db.QueryRow(statement, domain, path) row := db.QueryRow(statement, domain, path)
p := page{Domain: domain, Path: path} p := page{Domain: domain, Path: path}
if err := row.Scan(&p.IsLocked, &p.CommentCount); err != nil { if err := row.Scan(&p.IsLocked, &p.CommentCount, &p.StickyCommentHex, &p.Title); err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
// If there haven't been any comments, there won't be a record for this // If there haven't been any comments, there won't be a record for this
// page. The sane thing to do is return defaults. // page. The sane thing to do is return defaults.
// TODO: the defaults are hard-coded in two places: here and the schema // TODO: the defaults are hard-coded in two places: here and the schema
p.IsLocked = false p.IsLocked = false
p.CommentCount = 0 p.CommentCount = 0
p.StickyCommentHex = "none"
p.Title = ""
} else { } else {
logger.Errorf("error scanning page: %v", err) logger.Errorf("error scanning page: %v", err)
return page{}, errorInternal return page{}, errorInternal

28
api/page_title.go Normal file
View File

@@ -0,0 +1,28 @@
package main
import ()
func pageTitleUpdate(domain string, path string) (string, error) {
title, err := htmlTitleGet("http://" + domain + path)
if err != nil {
// This could fail due to a variety of reasons that we can't control such
// as the user's URL 404 or something, so let's not pollute the error log
// with messages. Just use a sane title. Maybe we'll have the ability to
// retry later.
logger.Errorf("%v", err)
title = domain
}
statement := `
UPDATE pages
SET title = $3
WHERE domain = $1 AND path = $2;
`
_, err = db.Exec(statement, domain, path, title)
if err != nil {
logger.Errorf("cannot update pages table with title: %v", err)
return "", err
}
return title, nil
}

View File

@@ -13,12 +13,12 @@ func pageUpdate(p page) error {
// commentCount // commentCount
statement := ` statement := `
INSERT INTO INSERT INTO
pages (domain, path, isLocked) pages (domain, path, isLocked, stickyCommentHex)
VALUES ($1, $2, $3 ) VALUES ($1, $2, $3, $4 )
ON CONFLICT (domain, path) DO ON CONFLICT (domain, path) DO
UPDATE SET isLocked = $3; UPDATE SET isLocked = $3, stickyCommentHex = $4;
` `
_, err := db.Exec(statement, p.Domain, p.Path, p.IsLocked) _, err := db.Exec(statement, p.Domain, p.Path, p.IsLocked, p.StickyCommentHex)
if err != nil { if err != nil {
logger.Errorf("error setting page attributes: %v", err) logger.Errorf("error setting page attributes: %v", err)
return errorInternal return errorInternal

View File

@@ -20,15 +20,24 @@ func apiRouterInit(router *mux.Router) error {
router.HandleFunc("/api/domain/moderator/delete", domainModeratorDeleteHandler).Methods("POST") router.HandleFunc("/api/domain/moderator/delete", domainModeratorDeleteHandler).Methods("POST")
router.HandleFunc("/api/domain/statistics", domainStatisticsHandler).Methods("POST") router.HandleFunc("/api/domain/statistics", domainStatisticsHandler).Methods("POST")
router.HandleFunc("/api/domain/import/disqus", domainImportDisqusHandler).Methods("POST") router.HandleFunc("/api/domain/import/disqus", domainImportDisqusHandler).Methods("POST")
router.HandleFunc("/api/domain/export/begin", domainExportBeginHandler).Methods("POST")
router.HandleFunc("/api/domain/export/download", domainExportDownloadHandler).Methods("GET")
router.HandleFunc("/api/commenter/token/new", commenterTokenNewHandler).Methods("GET") router.HandleFunc("/api/commenter/token/new", commenterTokenNewHandler).Methods("GET")
router.HandleFunc("/api/commenter/new", commenterNewHandler).Methods("POST") router.HandleFunc("/api/commenter/new", commenterNewHandler).Methods("POST")
router.HandleFunc("/api/commenter/login", commenterLoginHandler).Methods("POST") router.HandleFunc("/api/commenter/login", commenterLoginHandler).Methods("POST")
router.HandleFunc("/api/commenter/self", commenterSelfHandler).Methods("POST") router.HandleFunc("/api/commenter/self", commenterSelfHandler).Methods("POST")
router.HandleFunc("/api/email/get", emailGetHandler).Methods("POST")
router.HandleFunc("/api/email/update", emailUpdateHandler).Methods("POST")
router.HandleFunc("/api/email/moderate", emailModerateHandler).Methods("GET")
router.HandleFunc("/api/oauth/google/redirect", googleRedirectHandler).Methods("GET") router.HandleFunc("/api/oauth/google/redirect", googleRedirectHandler).Methods("GET")
router.HandleFunc("/api/oauth/google/callback", googleCallbackHandler).Methods("GET") router.HandleFunc("/api/oauth/google/callback", googleCallbackHandler).Methods("GET")
router.HandleFunc("/api/oauth/github/redirect", githubRedirectHandler).Methods("GET")
router.HandleFunc("/api/oauth/github/callback", githubCallbackHandler).Methods("GET")
router.HandleFunc("/api/comment/new", commentNewHandler).Methods("POST") router.HandleFunc("/api/comment/new", commentNewHandler).Methods("POST")
router.HandleFunc("/api/comment/list", commentListHandler).Methods("POST") router.HandleFunc("/api/comment/list", commentListHandler).Methods("POST")
router.HandleFunc("/api/comment/count", commentCountHandler).Methods("POST") router.HandleFunc("/api/comment/count", commentCountHandler).Methods("POST")

View File

@@ -1,41 +1,81 @@
package main package main
import ( import (
"bytes"
"fmt"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"html/template"
"io/ioutil" "io/ioutil"
"mime" "mime"
"net/http" "net/http"
"os" "os"
"path" "path"
"strings"
) )
func redirectLogin(w http.ResponseWriter, r *http.Request) { func redirectLogin(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, os.Getenv("ORIGIN")+"/login", 301) http.Redirect(w, r, os.Getenv("ORIGIN")+"/login", 301)
} }
type staticAssetPlugs struct { type staticPlugs struct {
Origin string
}
type staticHtmlPlugs struct {
Origin string Origin string
CdnPrefix string CdnPrefix string
Footer template.HTML Footer string
}
var asset map[string][]byte = make(map[string][]byte)
var contentType map[string]string = make(map[string]string)
var footer string
var compress bool
func fileDetemplate(f string) ([]byte, error) {
contents, err := ioutil.ReadFile(f)
if err != nil {
logger.Errorf("cannot read file %s: %v", f, err)
return []byte{}, err
}
x := string(contents)
x = strings.Replace(x, "[[[.Origin]]]", os.Getenv("ORIGIN"), -1)
x = strings.Replace(x, "[[[.CdnPrefix]]]", os.Getenv("CDN_PREFIX"), -1)
x = strings.Replace(x, "[[[.Footer]]]", footer, -1)
return []byte(x), nil
}
func footerInit() error {
contents, err := fileDetemplate(os.Getenv("STATIC") + "/footer.html")
if err != nil {
logger.Errorf("cannot init footer: %v", err)
return err
}
footer = string(contents)
return nil
}
func fileLoad(f string) ([]byte, error) {
b, err := fileDetemplate(f)
if err != nil {
logger.Errorf("cannot load file %s: %v", f, err)
return []byte{}, err
}
if !compress {
return b, nil
}
return gzipStatic(b)
} }
func staticRouterInit(router *mux.Router) error { func staticRouterInit(router *mux.Router) error {
var err error
subdir := pathStrip(os.Getenv("ORIGIN")) subdir := pathStrip(os.Getenv("ORIGIN"))
asset := make(map[string][]byte) if err = footerInit(); err != nil {
gzippedAsset := make(map[string][]byte) logger.Errorf("error initialising static router: %v", err)
return err
for _, dir := range []string{"js", "css", "images"} { }
sl := string(os.PathSeparator)
dir = sl + dir
for _, dir := range []string{"/js", "/css", "/images", "/fonts"} {
files, err := ioutil.ReadDir(os.Getenv("STATIC") + dir) files, err := ioutil.ReadDir(os.Getenv("STATIC") + dir)
if err != nil { if err != nil {
logger.Errorf("cannot read directory %s%s: %v", os.Getenv("STATIC"), dir, err) logger.Errorf("cannot read directory %s%s: %v", os.Getenv("STATIC"), dir, err)
@@ -43,108 +83,48 @@ func staticRouterInit(router *mux.Router) error {
} }
for _, file := range files { for _, file := range files {
p := dir + sl + file.Name() f := dir + "/" + file.Name()
asset[subdir+f], err = fileLoad(os.Getenv("STATIC") + f)
contents, err := ioutil.ReadFile(os.Getenv("STATIC") + p)
if err != nil { if err != nil {
logger.Errorf("cannot read file %s%s: %v", os.Getenv("STATIC"), p, err) logger.Errorf("cannot detemplate %s%s: %v", os.Getenv("STATIC"), f, err)
return err
}
prefix := ""
if dir == "/js" {
prefix = "window.commentoOrigin='" + os.Getenv("ORIGIN") + "';\n"
prefix += "window.commentoCdn='" + os.Getenv("CDN_PREFIX") + "';\n"
}
gzip := (os.Getenv("GZIP_STATIC") == "true")
asset[subdir+p] = []byte(prefix + string(contents))
if gzip {
gzippedAsset[subdir+p], err = gzipStatic(asset[subdir+p])
if err != nil {
logger.Errorf("error gzipping %s: %v", p, err)
return err return err
} }
} }
// faster than checking inside the handler
if !gzip {
router.HandleFunc(p, func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", mime.TypeByExtension(path.Ext(r.URL.Path)))
w.Write(asset[r.URL.Path])
})
} else {
router.HandleFunc(p, func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", mime.TypeByExtension(path.Ext(r.URL.Path)))
w.Header().Set("Content-Encoding", "gzip")
w.Write(gzippedAsset[r.URL.Path])
})
}
}
}
footer, err := ioutil.ReadFile(os.Getenv("STATIC") + string(os.PathSeparator) + "footer.html")
if err != nil {
logger.Errorf("cannot read file footer.html: %v", err)
return err
} }
pages := []string{ pages := []string{
"login", "/login",
"forgot", "/forgot",
"reset-password", "/reset-password",
"signup", "/signup",
"confirm-email", "/confirm-email",
"dashboard", "/unsubscribe",
"logout", "/dashboard",
} "/logout",
html := make(map[string]string)
for _, page := range pages {
html[subdir+page] = ""
} }
for _, page := range pages { for _, page := range pages {
sl := string(os.PathSeparator) f := page + ".html"
page = sl + page asset[subdir+page], err = fileLoad(os.Getenv("STATIC") + f)
file := page + ".html"
contents, err := ioutil.ReadFile(os.Getenv("STATIC") + file)
if err != nil { if err != nil {
logger.Errorf("cannot read file %s%s: %v", os.Getenv("STATIC"), file, err) logger.Errorf("cannot detemplate %s%s: %v", os.Getenv("STATIC"), f, err)
return err return err
} }
result := string(contents)
for {
t, err := template.New(page).Delims("[[[", "]]]").Parse(result)
if err != nil {
logger.Errorf("cannot parse %s%s template: %v", os.Getenv("STATIC"), file, err)
return err
} }
var buf bytes.Buffer for p, _ := range asset {
t.Execute(&buf, &staticHtmlPlugs{ if path.Ext(p) != "" {
Origin: os.Getenv("ORIGIN"), contentType[p] = mime.TypeByExtension(path.Ext(p))
CdnPrefix: os.Getenv("CDN_PREFIX"),
Footer: template.HTML(string(footer)),
})
result = buf.String()
if result == html[subdir+page] {
break
} else { } else {
html[subdir+page] = result contentType[p] = mime.TypeByExtension("html")
continue
}
}
} }
for _, page := range pages { router.HandleFunc(p, func(w http.ResponseWriter, r *http.Request) {
router.HandleFunc("/"+page, func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", contentType[r.URL.Path])
fmt.Fprint(w, html[r.URL.Path]) if compress {
w.Header().Set("Content-Encoding", "gzip")
}
w.Write(asset[r.URL.Path])
}) })
} }

29
api/smtp_domain_export.go Normal file
View File

@@ -0,0 +1,29 @@
package main
import (
"bytes"
"net/smtp"
"os"
)
type domainExportPlugs struct {
Origin string
Domain string
ExportHex string
}
func smtpDomainExport(to string, toName string, domain string, exportHex string) error {
var header bytes.Buffer
headerTemplate.Execute(&header, &headerPlugs{FromAddress: os.Getenv("SMTP_FROM_ADDRESS"), ToAddress: to, ToName: toName, Subject: "Commento Data Export"})
var body bytes.Buffer
templates["domain-export"].Execute(&body, &domainExportPlugs{Origin: os.Getenv("ORIGIN"), ExportHex: exportHex})
err := smtp.SendMail(os.Getenv("SMTP_HOST")+":"+os.Getenv("SMTP_PORT"), smtpAuth, os.Getenv("SMTP_FROM_ADDRESS"), []string{to}, concat(header, body))
if err != nil {
logger.Errorf("cannot send data export email: %v", err)
return errorCannotSendEmail
}
return nil
}

View File

@@ -0,0 +1,28 @@
package main
import (
"bytes"
"net/smtp"
"os"
)
type domainExportErrorPlugs struct {
Origin string
Domain string
}
func smtpDomainExportError(to string, toName string, domain string) error {
var header bytes.Buffer
headerTemplate.Execute(&header, &headerPlugs{FromAddress: os.Getenv("SMTP_FROM_ADDRESS"), ToAddress: to, ToName: toName, Subject: "Commento Data Export"})
var body bytes.Buffer
templates["data-export-error"].Execute(&body, &domainExportPlugs{Origin: os.Getenv("ORIGIN")})
err := smtp.SendMail(os.Getenv("SMTP_HOST")+":"+os.Getenv("SMTP_PORT"), smtpAuth, os.Getenv("SMTP_FROM_ADDRESS"), []string{to}, concat(header, body))
if err != nil {
logger.Errorf("cannot send data export error email: %v", err)
return errorCannotSendEmail
}
return nil
}

View File

@@ -0,0 +1,86 @@
package main
import (
"bytes"
"fmt"
ht "html/template"
"net/smtp"
"os"
tt "text/template"
)
type emailNotificationText struct {
emailNotification
Html ht.HTML
}
type emailNotificationPlugs struct {
Origin string
Kind string
Subject string
UnsubscribeSecretHex string
Notifications []emailNotificationText
}
func smtpEmailNotification(to string, toName string, unsubscribeSecretHex string, notifications []emailNotificationText, kind string) error {
var subject string
if kind == "reply" {
var verb string
if len(notifications) > 1 {
verb = "replies"
} else {
verb = "reply"
}
subject = fmt.Sprintf("%d new comment %s", len(notifications), verb)
} else {
var verb string
if len(notifications) > 1 {
verb = "comments"
} else {
verb = "comment"
}
if kind == "pending-moderation" {
subject = fmt.Sprintf("%d new %s pending moderation", len(notifications), verb)
} else {
subject = fmt.Sprintf("%d new %s on your website", len(notifications), verb)
}
}
h, err := tt.New("header").Parse(`MIME-Version: 1.0
From: Commento <{{.FromAddress}}>
To: {{.ToName}} <{{.ToAddress}}>
Content-Type: text/html; charset=UTF-8
Subject: {{.Subject}}
`)
var header bytes.Buffer
h.Execute(&header, &headerPlugs{FromAddress: os.Getenv("SMTP_FROM_ADDRESS"), ToAddress: to, ToName: toName, Subject: "[Commento] " + subject})
t, err := ht.ParseFiles(fmt.Sprintf("%s/templates/email-notification.txt", os.Getenv("STATIC")))
if err != nil {
logger.Errorf("cannot parse %s/templates/email-notification.txt: %v", os.Getenv("STATIC"), err)
return errorMalformedTemplate
}
var body bytes.Buffer
err = t.Execute(&body, &emailNotificationPlugs{
Origin: os.Getenv("ORIGIN"),
Kind: kind,
Subject: subject,
UnsubscribeSecretHex: unsubscribeSecretHex,
Notifications: notifications,
})
if err != nil {
logger.Errorf("error generating templated HTML for email notification: %v", err)
return err
}
err = smtp.SendMail(os.Getenv("SMTP_HOST")+":"+os.Getenv("SMTP_PORT"), smtpAuth, os.Getenv("SMTP_FROM_ADDRESS"), []string{to}, concat(header, body))
if err != nil {
logger.Errorf("cannot send email notification: %v", err)
return errorCannotSendEmail
}
return nil
}

View File

@@ -2,8 +2,8 @@ package main
import ( import (
"fmt" "fmt"
"html/template"
"os" "os"
"text/template"
) )
var headerTemplate *template.Template var headerTemplate *template.Template
@@ -20,9 +20,9 @@ var templates map[string]*template.Template
func smtpTemplatesLoad() error { func smtpTemplatesLoad() error {
var err error var err error
headerTemplate, err = template.New("header").Parse(`MIME-Version: 1.0 headerTemplate, err = template.New("header").Parse(`MIME-Version: 1.0
Content-Type: text/html; charset=UTF-8 From: Commento <{{.FromAddress}}>
From: {{.FromAddress}}
To: {{.ToName}} <{{.ToAddress}}> To: {{.ToName}} <{{.ToAddress}}>
Content-Type: text/plain; charset=UTF-8
Subject: {{.Subject}} Subject: {{.Subject}}
`) `)
@@ -31,7 +31,12 @@ Subject: {{.Subject}}
return errorMalformedTemplate return errorMalformedTemplate
} }
names := []string{"confirm-hex", "reset-hex"} names := []string{
"confirm-hex",
"reset-hex",
"domain-export",
"domain-export-error",
}
templates = make(map[string]*template.Template) templates = make(map[string]*template.Template)
@@ -39,9 +44,9 @@ Subject: {{.Subject}}
for _, name := range names { for _, name := range names {
var err error var err error
templates[name] = template.New(name) templates[name] = template.New(name)
templates[name], err = template.ParseFiles(fmt.Sprintf("%s/templates/%s.html", os.Getenv("STATIC"), name)) templates[name], err = template.ParseFiles(fmt.Sprintf("%s/templates/%s.txt", os.Getenv("STATIC"), name))
if err != nil { if err != nil {
logger.Errorf("cannot parse %s/templates/%s.html: %v", os.Getenv("STATIC"), name, err) logger.Errorf("cannot parse %s/templates/%s.txt: %v", os.Getenv("STATIC"), name, err)
return errorMalformedTemplate return errorMalformedTemplate
} }
} }

36
api/utils_html.go Normal file
View File

@@ -0,0 +1,36 @@
package main
import (
"golang.org/x/net/html"
"net/http"
)
func htmlTitleRecurse(h *html.Node) string {
if h.Type == html.ElementNode && h.Data == "title" {
return h.FirstChild.Data
}
for c := h.FirstChild; c != nil; c = c.NextSibling {
res := htmlTitleRecurse(c)
if res != "" {
return res
}
}
return ""
}
func htmlTitleGet(url string) (string, error) {
resp, err := http.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
h, err := html.Parse(resp.Body)
if err != nil {
return "", err
}
return htmlTitleRecurse(h), nil
}

View File

@@ -43,3 +43,16 @@ func bodyMarshal(w http.ResponseWriter, x map[string]interface{}) error {
w.Write(resp) w.Write(resp)
return nil return nil
} }
func getIp(r *http.Request) string {
ip := r.RemoteAddr
if r.Header.Get("X-Forwarded-For") != "" {
ip = r.Header.Get("X-Forwarded-For")
}
return ip
}
func getUserAgent(r *http.Request) string {
return r.Header.Get("User-Agent")
}

View File

@@ -10,6 +10,16 @@ func concat(a bytes.Buffer, b bytes.Buffer) []byte {
return append(a.Bytes(), b.Bytes()...) return append(a.Bytes(), b.Bytes()...)
} }
func nameFromEmail(email string) string {
for i, c := range email {
if c == '@' {
return email[:i]
}
}
return email
}
func exitIfError(err error) { func exitIfError(err error) {
if err != nil { if err != nil {
fmt.Printf("fatal error: %v\n", err) fmt.Printf("fatal error: %v\n", err)

View File

@@ -6,7 +6,6 @@ import (
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"os"
"time" "time"
) )
@@ -14,13 +13,12 @@ func versionCheckStart() error {
go func() { go func() {
printedError := false printedError := false
errorCount := 0 errorCount := 0
latestSeen := ""
for { for {
time.Sleep(5 * time.Minute) time.Sleep(5 * time.Minute)
data := url.Values{ data := url.Values{
"origin": {os.Getenv("ORIGIN")},
"edition": {edition},
"version": {version}, "version": {version},
} }
@@ -65,8 +63,9 @@ func versionCheckStart() error {
continue continue
} }
if r.NewUpdate { if r.NewUpdate && r.Latest != latestSeen {
logger.Infof("New update available! Latest version: %s", r.Latest) logger.Infof("New update available! Latest version: %s", r.Latest)
latestSeen = r.Latest
} }
errorCount = 0 errorCount = 0

View File

@@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS config (
version TEXT NOT NULL
);
INSERT INTO
config (version)
VALUES ('v1.1.3');

View File

@@ -0,0 +1,2 @@
UPDATE config
SET version = 'v1.2.0';

View File

@@ -0,0 +1,2 @@
ALTER TABLE pages
ADD stickyCommentHex TEXT NOT NULL DEFAULT 'none';

View File

@@ -0,0 +1,2 @@
UPDATE config
SET version = 'v1.4.0';

View File

@@ -0,0 +1,2 @@
UPDATE config
SET version = 'v1.4.1';

View File

@@ -0,0 +1,4 @@
-- Allow the owner to change whether anonymous comments are put into moderation by default.
ALTER TABLE domains
ADD COLUMN moderateAllAnonymous BOOLEAN DEFAULT true;

View File

@@ -0,0 +1,2 @@
UPDATE config
SET version = 'v1.4.2';

View File

@@ -0,0 +1,8 @@
-- add export feature
CREATE TABLE IF NOT EXISTS exports (
exportHex TEXT NOT NULL UNIQUE PRIMARY KEY,
binData BYTEA NOT NULL,
domain TEXT NOT NULL,
creationDate TIMESTAMP NOT NULL
);

View File

@@ -0,0 +1,2 @@
UPDATE config
SET version = 'v1.5.0';

View File

@@ -0,0 +1,38 @@
-- Email notifications
-- There are two kinds of email notifications: those sent to domain moderators
-- and those sent to commenters. Domain owners can choose to subscribe their
-- moderators to all comments, those pending moderation, or no emails. Each
-- moderator can independently opt out of these emails, of course. Commenters,
-- on the other, can choose to opt into reply notifications by email.
-- TODO: daily and weekly digests instead of just batched real-time emails?
-- TODO: more granular options to unsubscribe from emails for particular
-- domains can be provided - add unsubscribedReplyDomains []TEXT and
-- unsubscribedModeratorDomains []TEXT to emails table?
-- Each address has a cooldown period so that emails aren't sent within 10
-- minutes of each other. Why is this a separate table instead of another
-- column on commenters/owners? Because there may be some mods that haven't
-- logged in to create a row in the commenter table.
CREATE TABLE IF NOT EXISTS emails (
email TEXT NOT NULL UNIQUE PRIMARY KEY,
unsubscribeSecretHex TEXT NOT NULL UNIQUE,
lastEmailNotificationDate TIMESTAMP NOT NULL,
pendingEmails INTEGER NOT NULL DEFAULT 0,
sendReplyNotifications BOOLEAN NOT NULL DEFAULT false,
sendModeratorNotifications BOOLEAN NOT NULL DEFAULT true
);
CREATE INDEX IF NOT EXISTS unsubscribeSecretHexIndex ON emails(unsubscribeSecretHex);
-- Which comments should be sent?
-- Possible values: all, pending-moderation, none
-- Default to pending-moderation because this is critical. If the user forgets
-- to moderate, some comments will never see the light of day.
ALTER TABLE domains
ADD COLUMN emailNotificationPolicy TEXT DEFAULT 'pending-moderation';
-- Each page now needs to store the title of the page.
ALTER TABLE pages
ADD COLUMN title TEXT DEFAULT '';

View File

@@ -0,0 +1,2 @@
UPDATE config
SET version = 'v1.6.0';

View File

View File

@@ -0,0 +1,2 @@
UPDATE config
SET version = 'v1.6.0';

View File

@@ -2,7 +2,7 @@ version: '3'
services: services:
server: server:
image: registry.gitlab.com/commento/commento-ce image: registry.gitlab.com/commento/commento
ports: ports:
- 8080:8080 - 8080:8080
environment: environment:
@@ -19,12 +19,10 @@ services:
POSTGRES_DB: commento POSTGRES_DB: commento
POSTGRES_USER: postgres POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
networks: networks:
- db_network - db_network
volumes: volumes:
- postgres_data_volume:/var/lib/postgres - postgres_data_volume:/var/lib/postgresql/data
networks: networks:
db_network: db_network:

28
etc/bsd-rc/commento Executable file
View File

@@ -0,0 +1,28 @@
#!/bin/sh
# PROVIDE: commento
# REQUIRE: LOGIN postgresql
# KEYWORD: shutdown
PATH="/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin"
. /etc/rc.subr
desc="Commento daemon"
name=commento
rcvar=commento_enable
load_rc_config $name
: ${commento_enable:=NO}
commento_env="COMMENTO_ORIGIN=https://commento.example.com \
COMMENTO_PORT=8080 \
COMMENTO_POSTGRES=postgres://commento:commento@db:5432/commento?sslmode=disable \
COMMENTO_STATIC=/usr/local/share/commento"
commento_user=www
command="/usr/local/bin/commento"
command_args=" >> /var/log/commento/${name}.log 2>&1 &"
run_rc_command "$1"

View File

@@ -1,28 +0,0 @@
#!/bin/sh
# PROVIDE: commento_ce
# REQUIRE: LOGIN postgresql
# KEYWORD: shutdown
PATH="/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin"
. /etc/rc.subr
desc="Commento CE daemon"
name=commento_ce
rcvar=commento_ce_enable
load_rc_config $name
: ${commento_ce_enable:=NO}
commento_ce_env="COMMENTO_ORIGIN=https://commento.example.com \
COMMENTO_PORT=8080 \
COMMENTO_POSTGRES=postgres://commento:commento@db:5432/commento?sslmode=disable \
COMMENTO_STATIC=/usr/local/share/commento-ce"
commento_ce_user=www
command="/usr/local/bin/commento-ce"
command_args=" >> /var/log/commento_ce/${name}.log 2>&1 &"
run_rc_command "$1"

View File

@@ -1,14 +1,14 @@
[Unit] [Unit]
Description=Commento-CE daemon service Description=Commento daemon service
After=network.target postgresql.service After=network.target postgresql.service
[Service] [Service]
Type=simple Type=simple
ExecStart=/usr/bin/commento-ce ExecStart=/usr/bin/commento
Environment=COMMENTO_ORIGIN=https://commento.example.com Environment=COMMENTO_ORIGIN=https://commento.example.com
Environment=COMMENTO_PORT=8080 Environment=COMMENTO_PORT=8080
Environment=COMMENTO_POSTGRES=postgres://commento:commento@db:5432/commento?sslmode=disable Environment=COMMENTO_POSTGRES=postgres://commento:commento@db:5432/commento?sslmode=disable
Environment=COMMENTO_STATIC=/usr/share/commento-ce Environment=COMMENTO_STATIC=/usr/share/commento
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

33
frontend/.eslintrc Normal file
View File

@@ -0,0 +1,33 @@
{
"env": {
"browser": true
},
"globals": {
"$": true
},
"rules": {
"no-bitwise": 2,
"camelcase": 2,
"brace-style": ["error", "1tbs"],
"curly": ["error", "all"],
"eqeqeq": ["error", "smart"],
"indent": ["error", 2],
"no-use-before-define": [
2,
{
"functions": false
}
],
"new-cap": 2,
"no-caller": 2,
"quotes": [
2,
"double"
],
"no-unused-vars": 2,
"strict": [
2,
"function"
]
}
}

1
frontend/.gitignore vendored
View File

@@ -1 +1,2 @@
.sass-cache .sass-cache
node_modules/

View File

@@ -1,117 +1,13 @@
SHELL = bash
# list of JS files to be built
JS_BUILD = jquery.js vue.js highlight.js chartist.js login.js forgot.js reset.js signup.js dashboard.js logout.js commento.js
jquery.js = jquery.js
vue.js = vue.js
highlight.js = highlight.js
chartist.js = chartist.js
login.js = utils.js http.js auth-common.js login.js
forgot.js = utils.js http.js forgot.js
reset.js = utils.js http.js reset.js
signup.js = utils.js http.js auth-common.js signup.js
dashboard.js = utils.js http.js errors.js self.js dashboard.js dashboard-setting.js dashboard-domain.js dashboard-installation.js dashboard-general.js dashboard-moderation.js dashboard-statistics.js dashboard-import.js dashboard-danger.js
logout.js = utils.js logout.js
commento.js = commento.js
# for each file in $(JS_BUILD), list its composition
BUILD_DIR = build BUILD_DIR = build
DEVEL_BUILD_DIR = $(BUILD_DIR)/devel GULP = node_modules/.bin/gulp
PROD_BUILD_DIR = $(BUILD_DIR)/prod
HTML_SRC_DIR = . devel:
HTML_SRC_FILES = $(wildcard $(HTML_SRC_DIR)/*.html) yarn install
HTML_DEVEL_BUILD_DIR = $(DEVEL_BUILD_DIR) $(GULP) devel
HTML_DEVEL_BUILD_FILES = $(patsubst $(HTML_SRC_DIR)/%, $(HTML_DEVEL_BUILD_DIR)/%, $(HTML_SRC_FILES))
HTML_PROD_BUILD_DIR = $(PROD_BUILD_DIR)
HTML_PROD_BUILD_FILES = $(patsubst $(HTML_SRC_DIR)/%, $(HTML_PROD_BUILD_DIR)/%, $(HTML_SRC_FILES))
HTML_MINIFIER = html-minifier prod:
HTML_MINIFIER_FLAGS = --collapse-whitespace --remove-comments yarn install
$(GULP) prod
JS_SRC_DIR = js
JS_SRC_FILES = $(wildcard $(JS_SRC_DIR)/*.js)
JS_DEVEL_BUILD_DIR = $(DEVEL_BUILD_DIR)/js
JS_DEVEL_BUILD_FILES = $(addprefix $(JS_DEVEL_BUILD_DIR)/, $(JS_BUILD))
JS_PROD_BUILD_DIR = $(PROD_BUILD_DIR)/js
JS_PROD_BUILD_FILES = $(addprefix $(JS_PROD_BUILD_DIR)/, $(JS_BUILD))
JS_MINIFIER = uglifyjs
JS_MINIFIER_FLAGS = --compress --mangle
SASS_SRC_DIR = sass
SASS_SRC_FILES = $(wildcard $(SASS_SRC_DIR)/*.scss)
CSS_DEVEL_BUILD_DIR = $(DEVEL_BUILD_DIR)/css
CSS_DEVEL_BUILD_FILES = $(patsubst $(SASS_SRC_DIR)/%.scss, $(CSS_DEVEL_BUILD_DIR)/%.css, $(SASS_SRC_FILES))
CSS_PROD_BUILD_DIR = $(PROD_BUILD_DIR)/css
CSS_PROD_BUILD_FILES = $(patsubst $(SASS_SRC_DIR)/%.scss, $(CSS_PROD_BUILD_DIR)/%.css, $(SASS_SRC_FILES))
CSS = sass
CSS_DEVEL_FLAGS =
CSS_PROD_FLAGS = $(CSS_DEVEL_FLAGS) --style compressed
IMGS_SRC_DIR = images
IMGS_SRC_FILES = $(wildcard $(IMGS_SRC_DIR)/*)
IMGS_DEVEL_BUILD_DIR = $(DEVEL_BUILD_DIR)/images
IMGS_DEVEL_BUILD_FILES = $(patsubst $(IMGS_SRC_DIR)/%, $(IMGS_DEVEL_BUILD_DIR)/%, $(IMGS_SRC_FILES))
IMGS_PROD_BUILD_DIR = $(PROD_BUILD_DIR)/images
IMGS_PROD_BUILD_FILES = $(patsubst $(IMGS_SRC_DIR)/%, $(IMGS_PROD_BUILD_DIR)/%, $(IMGS_SRC_FILES))
devel: devel-html devel-js devel-css devel-imgs
prod: devel prod-html prod-js prod-css prod-imgs
clean: clean:
-rm -rf $(BUILD_DIR); -rm -rf $(BUILD_DIR);
devel-html: $(HTML_DEVEL_BUILD_FILES)
$(HTML_DEVEL_BUILD_FILES): $(HTML_DEVEL_BUILD_DIR)/%.html: $(HTML_SRC_DIR)/%.html
cp $^ $@;
prod-html: $(HTML_PROD_BUILD_FILES)
$(HTML_PROD_BUILD_FILES): $(HTML_PROD_BUILD_DIR)/%.html: $(HTML_DEVEL_BUILD_DIR)/%.html
$(HTML_MINIFIER) $(HTML_MINIFIER_FLAGS) -o $@ $^;
devel-js: $(JS_DEVEL_BUILD_FILES)
.SECONDEXPANSION:
$(JS_DEVEL_BUILD_FILES): $(JS_DEVEL_BUILD_DIR)/%.js: $$(addprefix $$(JS_SRC_DIR)/, $$(%.js))
>$@; \
for f in $^; do \
printf "// %s\n" "$$f" >>$@; \
cat $$f >>$@; \
printf "\n" >>$@; \
done;
prod-js: $(JS_PROD_BUILD_FILES)
$(JS_PROD_BUILD_FILES): $(JS_PROD_BUILD_DIR)/%.js: $(JS_DEVEL_BUILD_DIR)/%.js
$(JS_MINIFIER) $(JS_MINIFIER_FLAGS) -o $@ $^;
devel-css: $(CSS_DEVEL_BUILD_FILES)
$(CSS_DEVEL_BUILD_FILES): $(CSS_DEVEL_BUILD_DIR)/%.css: $(SASS_SRC_DIR)/%.scss $(SASS_SRC_FILES)
$(CSS) $(CSS_DEVEL_FLAGS) $< $@;
prod-css: $(CSS_PROD_BUILD_FILES)
$(CSS_PROD_BUILD_FILES): $(CSS_PROD_BUILD_DIR)/%.css: $(SASS_SRC_DIR)/%.scss
$(CSS) $(CSS_PROD_FLAGS) $^ $@;
$(shell mkdir -p $(HTML_DEVEL_BUILD_DIR) $(JS_DEVEL_BUILD_DIR) $(CSS_DEVEL_BUILD_DIR) $(HTML_PROD_BUILD_DIR) $(JS_PROD_BUILD_DIR) $(CSS_PROD_BUILD_DIR))
devel-imgs: $(IMGS_DEVEL_BUILD_FILES)
$(IMGS_DEVEL_BUILD_FILES): $(IMGS_DEVEL_BUILD_DIR)/%: $(IMGS_SRC_DIR)/%
cp $^ $@;
prod-imgs: $(IMGS_PROD_BUILD_FILES)
$(IMGS_PROD_BUILD_FILES): $(IMGS_PROD_BUILD_DIR)/%: $(IMGS_SRC_DIR)/%
cp $^ $@
$(shell mkdir -p $(HTML_DEVEL_BUILD_DIR) $(JS_DEVEL_BUILD_DIR) $(CSS_DEVEL_BUILD_DIR) $(IMGS_DEVEL_BUILD_DIR) $(HTML_PROD_BUILD_DIR) $(JS_PROD_BUILD_DIR) $(CSS_PROD_BUILD_DIR) $(IMGS_PROD_BUILD_DIR))

View File

@@ -2,6 +2,7 @@
<head> <head>
<meta name="viewport" content="user-scalable=no, initial-scale=1.0"> <meta name="viewport" content="user-scalable=no, initial-scale=1.0">
<script src="[[[.CdnPrefix]]]/js/jquery.js"></script> <script src="[[[.CdnPrefix]]]/js/jquery.js"></script>
<link rel="icon" href="[[[.CdnPrefix]]]/images/120x120.png">
<link rel="stylesheet" href="[[[.CdnPrefix]]]/css/auth.css"> <link rel="stylesheet" href="[[[.CdnPrefix]]]/css/auth.css">
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:400,700|Source+Sans+Pro:200,300,400,700" rel="stylesheet"> <link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:400,700|Source+Sans+Pro:200,300,400,700" rel="stylesheet">
<title>Commento: Email Confirmation</title> <title>Commento: Email Confirmation</title>

View File

@@ -5,6 +5,7 @@
<script src="[[[.CdnPrefix]]]/js/highlight.js"></script> <script src="[[[.CdnPrefix]]]/js/highlight.js"></script>
<script src="[[[.CdnPrefix]]]/js/chartist.js"></script> <script src="[[[.CdnPrefix]]]/js/chartist.js"></script>
<script src="[[[.CdnPrefix]]]/js/dashboard.js"></script> <script src="[[[.CdnPrefix]]]/js/dashboard.js"></script>
<link rel="icon" href="[[[.CdnPrefix]]]/images/120x120.png">
<link rel="stylesheet" href="[[[.CdnPrefix]]]/css/chartist.css"> <link rel="stylesheet" href="[[[.CdnPrefix]]]/css/chartist.css">
<link rel="stylesheet" href="[[[.CdnPrefix]]]/css/dashboard.css"> <link rel="stylesheet" href="[[[.CdnPrefix]]]/css/dashboard.css">
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:400,700|Source+Sans+Pro:200,300,400,700" rel="stylesheet"> <link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:400,700|Source+Sans+Pro:200,300,400,700" rel="stylesheet">
@@ -21,10 +22,10 @@
<script> <script>
window.onload = function() { window.onload = function() {
window.selfGet(function() { window.commento.selfGet(function() {
window.vueConstruct(function() { window.commento.vueConstruct(function() {
window.navbarFill(); window.commento.navbarFill();
window.domainRefresh(); window.commento.domainRefresh();
$(document).ready(function(){ $(document).ready(function(){
$("ul.tabs li").click(function(){ $("ul.tabs li").click(function(){
var tab_id = $(this).attr("data-tab"); var tab_id = $(this).attr("data-tab");
@@ -51,13 +52,13 @@
It's so quiet in here. It's so quiet in here.
</div> </div>
<div class="pane-setting" v-for="domain in domains" v-on:click="window.domainSelect(domain.domain)" id="{{domain.hex}}" v-bind:class="{selected: domain.selected}" v-if="domain.show"> <div class="pane-setting" v-for="domain in domains" v-on:click="window.commento.domainSelect(domain.domain)" id="{{domain.hex}}" v-bind:class="{selected: domain.selected}" v-if="domain.show">
<div class="pane-setting-inside"> <div class="pane-setting-inside">
<div class="setting-title">{{domain.name}}</div> <div class="setting-title">{{domain.name}}</div>
<div class="setting-subtitle">{{domain.domain}}</div> <div class="setting-subtitle">{{domain.domain}}</div>
</div> </div>
</div> </div>
<div class="pane-setting" id="domain-add" onclick="window.location.hash='#new-domain-modal'"> <div class="pane-setting" id="domain-add" onclick="document.location.hash='#new-domain-modal'">
<div class="pane-setting-inside super-setting"> <div class="pane-setting-inside super-setting">
<div class="super-setting-title">+</div> <div class="super-setting-title">+</div>
<div class="super-setting-text">New Domain</div> <div class="super-setting-text">New Domain</div>
@@ -66,7 +67,7 @@
</div> </div>
<div class="pane-middle"> <div class="pane-middle">
<div v-if="showSettings" class="pane-setting" v-for="setting in settings" v-on:click="window.settingSelect(setting.id)" id="{{setting.id}}" v-bind:class="{selected: setting.selected}"> <div v-if="showSettings" class="pane-setting" v-for="setting in settings" v-on:click="window.commento.settingSelect(setting.id)" id="{{setting.id}}" v-bind:class="{selected: setting.selected}">
<div class="pane-setting-inside"> <div class="pane-setting-inside">
<div class="setting-title">{{setting.text}}</div> <div class="setting-title">{{setting.text}}</div>
<div class="setting-subtitle">{{setting.meaning}}</div> <div class="setting-subtitle">{{setting.meaning}}</div>
@@ -81,23 +82,29 @@
<!-- Installation --> <!-- Installation -->
<div id="installation-view" class="view hidden"> <div id="installation-view" class="view hidden">
<div class="view-inside"> <div class="view-inside">
<div class="large-view"> <div class="mid-view">
<div class="tabs-container"> <div class="tabs-container">
<div class="tab"> <div class="tab">
<ul class="tabs"> <ul class="tabs">
<li class="tab-link original current" data-tab="install-tab-1">Universal Snippet</li> <li class="tab-link original current" data-tab="installation-tab-1">Universal Snippet</li>
</ul> </ul>
<div id="install-tab-1" class="content original current"> <div id="installation-tab-1" class="content original current">
<div class="import-text"> <div class="normal-text">
Copy the following piece of HTML code and paste it where you'd like Commento to load. Copy the following piece of HTML code and paste it where you'd like Commento to load.
</div> </div>
<pre><code id="code-div" class="html"></code></pre> <pre><code id="code-div" class="html"></code></pre>
<div class="text"> <div class="normal-text">
And that's it. All your settings, themes, and comments would be automagically loaded. Commento is mobile-responsive too, as it simply fills the container it is put in. And that's it. All your settings, themes, and comments would be automagically loaded. Commento is mobile-responsive too, as it simply fills the container it is put in.
</div> </div>
<br>
<div class="normal-text">
Read the Commento documentation <a href="https://docs.commento.io/configuration/">on configuration</a>.
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -112,6 +119,11 @@
<div class="center center-title"> <div class="center center-title">
Analytics Analytics
</div> </div>
<div class="normal-text">
Anonymous statistics such as monthly pageviews and number of comments
</div>
<div class="stat"> <div class="stat">
<div class="number"> <div class="number">
<div class="digits gray-digits">{{domains[cd].viewsLast30Days.zeros}}</div> <div class="digits gray-digits">{{domains[cd].viewsLast30Days.zeros}}</div>
@@ -142,33 +154,100 @@
<!-- moderation --> <!-- moderation -->
<div id="moderation-view" class="view hidden"> <div id="moderation-view" class="view hidden">
<div class="view-inside"> <div class="view-inside">
<div class="small-view mid-view"> <div class="mid-view">
<div class="tabs-container"> <div class="tabs-container">
<div class="tab"> <div class="tab">
<ul class="tabs"> <ul class="tabs">
<li class="tab-link original current" data-tab="mod-tab-1">Moderator List</li> <li class="tab-link original current" data-tab="mod-tab-1">General</li>
<li class="tab-link" data-tab="mod-tab-2">Add/Remove Moderators</li>
<li class="tab-link" data-tab="mod-tab-3">Email Settings</li>
</ul> </ul>
<div id="mod-tab-1" class="content original current"> <div id="mod-tab-1" class="content original current">
<div class="row no-border commento-round-check">
<input type="checkbox" v-model="domains[cd].autoSpamFilter" id="spam-filtering">
<label for="spam-filtering">Automatic spam filtering</label>
<div class="pitch"> <div class="pitch">
Moderators have the power to approve and delete comments. To make someone a moderator, add their email address down below. Once added, shiny new moderation buttons will appear on each comment for that person on each page on this domain. Commento uses Akismet's advanced spam detection to automatically identify and remove spam comments. This is strongly recommended. Requires backend configuration.
</div>
</div>
<div class="row no-border commento-round-check">
<input type="checkbox" v-model="domains[cd].requireModeration" id="require-moderation">
<label for="require-moderation">Require all comments to be approved manually</label>
<div class="pitch">
Enabling this would require a moderator to approve all comments. This is generally recommended if your site doesn't receive too much traffic.
</div>
</div>
<div class="row no-border commento-round-check">
<input type="checkbox" v-model="domains[cd].allowAnonymous" id="allow-anonymous">
<label for="allow-anonymous">Allow anonymous comments</label>
<div class="pitch">
Enabling this would allow your readers to comment anonymously. Disabling would require the to authenticate themselves (using their Google account, for example). Recommended.
</div>
</div>
<div class="row no-border commento-round-check indent" v-if="domains[cd].allowAnonymous">
<input type="checkbox" v-model="domains[cd].moderateAllAnonymous" id="moderate-all-anonymous">
<label for="moderate-all-anonymous">Require anonymous comments to be approved manually</label>
<div class="pitch">
Enabling this would require a moderator to approve all anonymous comments. This is recommended if most of your spam comments are from anonymous users.
</div>
</div>
<div class="center">
<button id="save-general-button" onclick="window.commento.generalSaveHandler()" class="button">Save Changes</button>
</div>
</div>
<div id="mod-tab-2" class="content">
<div class="normal-text">
Moderators have the power to approve/delete comments and lock threads. Once you add an user as a moderator, shiny new buttons will appear on each comment on each page when they log in.<br><br>
You're still the only administrator and the only person who can add and remove moderators. Moderators do not have access to this dashboard. Their access is restricted to pages on your website.
</div> </div>
<div class="commento-email-container"> <div class="commento-email-container">
<div class="commento-email"> <div class="commento-email">
<input class="commento-input" type="text" id="new-mod" placeholder="Email"> <input class="commento-input" type="text" id="new-mod" placeholder="Email">
<button id="new-mod-button" class="commento-email-button" onclick="window.moderatorNewHandler()">Add moderator</button> <button id="new-mod-button" class="commento-email-button" onclick="window.commento.moderatorNewHandler()">Add moderator</button>
</div> </div>
</div> </div>
<div class="mod-emails-container"> <div class="mod-emails-container">
<div class="content"> <div class="content">
<div class="mod-email" v-for="email in domains[cd].moderators" v-if="domains[cd].moderators.length > 0"> <div class="mod-email" v-for="email in domains[cd].moderators" v-if="domains[cd].moderators.length > 0">
<div class="email">{{email.email}}</div> <div class="email">{{email.email}}</div>
<div class="delete" v-on:click="window.moderatorDeleteHandler(email.email)">Delete</div> <div class="delete" v-on:click="window.commento.moderatorDeleteHandler(email.email)">Delete</div>
<div class="date">added {{email.timeAgo}}</div> <div class="date">added {{email.timeAgo}}</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div id="mod-tab-3" class="content">
<div class="normal-text">
You can enable email notifications to notify your moderators when a new comment is posted or when a comment is pending moderation. Commento tries to be smart about how often an email is sent. Emails will be delayed and batched until you go 10 minutes without one. This requires valid SMTP settings in order to send emails.<br><br>
</div>
<div class="question">
When do you want emails sent to moderators?
</div>
<div class="row no-border commento-round-check indent">
<input type="radio" id="email-all" value="all" v-model="domains[cd].emailNotificationPolicy">
<label for="email-all">Whenever a new comment is created</label>
</div>
<div class="row no-border commento-round-check indent">
<input type="radio" id="email-pending-moderation" value="pending-moderation" v-model="domains[cd].emailNotificationPolicy">
<label for="email-pending-moderation">Only for comments pending moderation</label>
</div>
<div class="row no-border commento-round-check indent">
<input type="radio" id="email-none" value="none" v-model="domains[cd].emailNotificationPolicy">
<label for="email-none">Do not email moderators</label>
</div>
<br>
<div class="center">
<button id="save-general-button" onclick="window.commento.generalSaveHandler()" class="button">Save Changes</button>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -178,44 +257,38 @@
<!-- Configure Domain --> <!-- Configure Domain -->
<div id="general-view" class="view hidden"> <div id="general-view" class="view hidden">
<div class="view-inside"> <div class="view-inside">
<div class="small-mid-view"> <div class="mid-view">
<div class="center center-title"> <div class="tabs-container">
Configure Domain <div class="tab">
</div> <ul class="tabs">
<li class="tab-link original current" data-tab="configure-tab-1">General</li>
<li class="tab-link" data-tab="configure-tab-2">Export Data</li>
</ul>
<div id="configure-tab-1" class="content original current">
<div class="box"> <div class="box">
<div class="row"> <div class="row">
<div class="label">Website Name</div> <div class="label">Website Name</div>
<input class="input gray-input" id="cur-domain-name" type="text" :placeholder="domains[cd].origName" v-model="domains[cd].name"> <input class="input gray-input" id="cur-domain-name" type="text" :placeholder="domains[cd].origName" v-model="domains[cd].name">
</div> </div>
<div class="row no-border round-check">
<input type="checkbox" class="switch" v-model="domains[cd].autoSpamFilter" id="spam-filtering">
<label for="spam-filtering">Automatic spam filtering</label>
<div class="pitch">
Commento uses Akismet's advanced spam detection to automatically identify and remove spam comments. We strongly recommended you have this enabled.
</div>
</div>
<div class="row no-border round-check">
<input type="checkbox" class="switch" v-model="domains[cd].requireModeration" id="require-moderation">
<label for="require-moderation">Require all comments to be approved manually</label>
<div class="pitch">
Enabling this would require a moderator to approve every comment. Moderators can manually delete comments even if this is not enabled.
</div>
</div>
<div class="row no-border round-check">
<input type="checkbox" class="switch" v-model="domains[cd].requireIdentification" id="require-identification">
<label for="require-identification">Require identification</label>
<div class="pitch">
Enabling this would require all commenters to authenticate themselves (using their Google account, for example). Disabling would allow anonymous comments.
</div>
</div>
<div id="new-domain-error" class="modal-error-box"></div>
</div> </div>
<div class="center"> <div class="center">
<button id="save-general-button" onclick="window.generalSaveHandler()" class="button">Save Changes</button> <button id="save-general-button" onclick="window.commento.generalSaveHandler()" class="button">Save Changes</button>
</div>
</div>
<div id="configure-tab-2" class="content">
<div class="normal-text">
You can export an archive of this domain's data (which includes all comments and commenters) in the JSON format. To initiate and queue an archive request, click the button below. You will receive an email containing the archive once it's ready.<br><br>
Please note that this requires valid SMTP settings in order to send emails.<br><br>
<div class="center">
<button id="domain-export-button" onclick="window.commento.domainExportBegin()" class="button">Initiate Data Export</button>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -224,7 +297,7 @@
<!-- Import Comments --> <!-- Import Comments -->
<div id="import-view" class="view hidden"> <div id="import-view" class="view hidden">
<div class="view-inside"> <div class="view-inside">
<div class="large-view"> <div class="mid-view">
<div class="tabs-container"> <div class="tabs-container">
<div class="tab"> <div class="tab">
<ul class="tabs"> <ul class="tabs">
@@ -232,32 +305,41 @@
</ul> </ul>
<div id="install-tab-1" class="content original current"> <div id="install-tab-1" class="content original current">
<div class="import-text"> <div class="normal-text">
If you're currently using Disqus and want to import all your comments into Commento, you can do so: If you're currently using Disqus, you can import all comments into Commento:
<ul> <ul>
<li> <li>
Go to <a href="http://disqus.com/admin/discussions/export/">the admin export section</a> in Disqus and click on <b>Export Comments</b>. This should start the process of exporting your comments. Go to <a href="http://disqus.com/admin/discussions/export/">the admin export section</a> in Disqus and click on <b>Export Comments</b>. This should start the process of exporting your comments in the background.
</li> </li>
<li> <li>
After a while, you'll receive an email from Disqus with a link to a compressed archive of all comments and associated data. Copy and paste that link here and start the import process: You'll receive an email from Disqus with a link to a compressed archive of all comments and associated data. Copy and paste that link here to start the import process:
<br><br> <br><br>
<div class="commento-email-container"> <div class="commento-email-container">
<div class="commento-email"> <div class="commento-email">
<input class="commento-input" type="text" id="disqus-url" placeholder="https://media.disqus.com/uploads/..."> <input class="commento-input" type="text" id="disqus-url" placeholder="https://media.disqus.com/uploads/...">
<button id="disqus-import-button" class="commento-email-button" onclick="importDisqus()">Import</button> <button id="disqus-import-button" class="commento-email-button" onclick="window.commento.importDisqus()">Import</button>
</div> </div>
</div> </div>
<!--
<div class="subtext-container"> <div class="subtext-container">
<div class="subtext"> <div class="subtext">
<div>Note: it is strongly recommended you do this only once. Multiple imports for the same domain may have unintended effects.</div> <div>By using this service, you grant Commento the permission to download and process your Disqus information.</div>
</div> </div>
</div> </div>
-->
<br>
</li> </li>
<li> <li>
We'll automatically download this file, extract it, parse it and import comments into Commento. The URL information will be preserved. By using this service, you grant Commento the permission to download and process your Disqus information. Commento will automatically download this file, extract it, parse it and import comments into Commento. URL information, comment authors, text formatting, and nested replies will be preserved.
</li>
<li>
It is strongly recommended you do this only once. Importing multiple times may have unintended effects.
</li> </li>
</ul> </ul>
</div> </div>
@@ -286,7 +368,7 @@
If you desire to re-allow comments again on your website, you can do so. You can, of course, freeze the site again in the future. If you desire to re-allow comments again on your website, you can do so. You can, of course, freeze the site again in the future.
</div> </div>
<button onclick="window.location.hash='#unfreeze-domain-modal'" class="button green-button">Unfreeze Domain</button> <button onclick="document.location.hash='#unfreeze-domain-modal'" class="button green-button">Unfreeze Domain</button>
</div> </div>
<div class="box" v-if="domains[cd].state != 'frozen'"> <div class="box" v-if="domains[cd].state != 'frozen'">
@@ -294,7 +376,7 @@
If you desire to temporarily freeze new comments (domain-wide), thereby making it read-only, you can do so. You can choose to unfreeze later; this is temporary. If you desire to temporarily freeze new comments (domain-wide), thereby making it read-only, you can do so. You can choose to unfreeze later; this is temporary.
</div> </div>
<button id="orange-button" onclick="window.location.hash='#freeze-domain-modal'" class="button orange-button">Freeze Domain</button> <button id="orange-button" onclick="document.location.hash='#freeze-domain-modal'" class="button orange-button">Freeze Domain</button>
</div> </div>
</div> </div>
</div> </div>
@@ -305,7 +387,7 @@
Want to completely remove Commento from your website? This will permanently delete all comments and there is literally no way to retrieve your data once you do this. Want to completely remove Commento from your website? This will permanently delete all comments and there is literally no way to retrieve your data once you do this.
</div> </div>
<button id="big-red-button" class="button big-red-button" onclick="window.location.hash='#delete-domain-modal'">Delete Domain</button> <button id="big-red-button" class="button big-red-button" onclick="document.location.hash='#delete-domain-modal'">Delete Domain</button>
</div> </div>
</div> </div>
</div> </div>
@@ -323,7 +405,7 @@
Are you absolutely sure you want to freeze your domain, thereby making it read-only? You can choose to unfreeze later; this is temporary. Are you absolutely sure you want to freeze your domain, thereby making it read-only? You can choose to unfreeze later; this is temporary.
</div> </div>
<div class="modal-contents"> <div class="modal-contents">
<button id="orange-button" class="button orange-button" onclick="window.domainFreezeHandler()">Freeze Domain</button> <button id="orange-button" class="button orange-button" onclick="window.commento.domainFreezeHandler()">Freeze Domain</button>
</div> </div>
</div> </div>
</div> </div>
@@ -336,7 +418,7 @@
Are you absolutely sure you want to unfreeze your domain? This will re-allow new comments. You can choose to freeze again in the future. Are you absolutely sure you want to unfreeze your domain? This will re-allow new comments. You can choose to freeze again in the future.
</div> </div>
<div class="modal-contents"> <div class="modal-contents">
<button id="blue-button" class="button green-button" onclick="window.domainUnfreezeHandler()">Unfreeze Domain</button> <button id="blue-button" class="button green-button" onclick="window.commento.domainUnfreezeHandler()">Unfreeze Domain</button>
</div> </div>
</div> </div>
</div> </div>
@@ -349,7 +431,7 @@
Are you absolutely sure? This will permanently delete all comments and there is literally no way to retrieve your data once you do this. Are you absolutely sure? This will permanently delete all comments and there is literally no way to retrieve your data once you do this.
</div> </div>
<div class="modal-contents"> <div class="modal-contents">
<button id="big-red-button" class="button big-red-button" onclick="window.domainDeleteHandler()">Delete Domain</button> <button id="big-red-button" class="button big-red-button" onclick="window.commento.domainDeleteHandler()">Delete Domain</button>
</div> </div>
</div> </div>
</div> </div>
@@ -374,7 +456,7 @@
</div> </div>
<div id="new-domain-error" class="modal-error-box"></div> <div id="new-domain-error" class="modal-error-box"></div>
<div class="center"> <div class="center">
<button id="add-site-button" onclick="window.domainNewHandler()" class="button">Add Domain</button> <button id="add-site-button" onclick="window.commento.domainNewHandler()" class="button">Add Domain</button>
</div> </div>
</div> </div>
</div> </div>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -3,6 +3,7 @@
<meta name="viewport" content="user-scalable=no, initial-scale=1.0"> <meta name="viewport" content="user-scalable=no, initial-scale=1.0">
<script src="[[[.CdnPrefix]]]/js/jquery.js"></script> <script src="[[[.CdnPrefix]]]/js/jquery.js"></script>
<script src="[[[.CdnPrefix]]]/js/forgot.js"></script> <script src="[[[.CdnPrefix]]]/js/forgot.js"></script>
<link rel="icon" href="[[[.CdnPrefix]]]/images/120x120.png">
<link rel="stylesheet" href="[[[.CdnPrefix]]]/css/auth.css"> <link rel="stylesheet" href="[[[.CdnPrefix]]]/css/auth.css">
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:400,700|Source+Sans+Pro:200,300,400,700" rel="stylesheet"> <link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:400,700|Source+Sans+Pro:200,300,400,700" rel="stylesheet">
<title>Commento: Reset your Password</title> <title>Commento: Reset your Password</title>
@@ -25,7 +26,7 @@
</div> </div>
<div class="err" id="err"></div> <div class="err" id="err"></div>
<div class="msg" id="msg"></div> <div class="msg" id="msg"></div>
<button id="reset-button" class="button" onclick="sendResetHex()">Send Reset Password Link</button> <button id="reset-button" class="button" onclick="window.commento.sendResetHex()">Send Reset Password Link</button>
<a class="link" href="[[[.Origin]]]/login">Suddenly remembered your password? Login.</a> <a class="link" href="[[[.Origin]]]/login">Suddenly remembered your password? Login.</a>
</div> </div>
</div> </div>

156
frontend/gulpfile.js Normal file
View File

@@ -0,0 +1,156 @@
"use strict";
const gulp = require("gulp");
const sass = require("gulp-sass");
const sourcemaps = require("gulp-sourcemaps");
const cleanCss = require("gulp-clean-css");
const htmlMinifier = require("gulp-html-minifier");
const uglify = require("gulp-uglify");
const concat = require("gulp-concat");
const rename = require("gulp-rename");
const eslint = require("gulp-eslint");
const develPath = "build/devel/";
const prodPath = "build/prod/";
const scssSrc = "./sass/*.scss";
const cssDir = "css/";
const fontsDir = "fonts/";
const fontsGlob = fontsDir + "**/*";
const imagesDir = "images/";
const imagesGlob = imagesDir + "**/*";
const jsDir = "js/";
const jsGlob = jsDir + "*.js";
const htmlGlob = "./*.html";
const jsCompileMap = {
"js/jquery.js": ["node_modules/jquery/dist/jquery.min.js"],
"js/vue.js": ["node_modules/vue/dist/vue.min.js"],
"js/highlight.js": ["node_modules/highlightjs/highlight.pack.min.js"],
"js/chartist.js": ["node_modules/chartist/dist/chartist.min.js"],
"js/login.js": [
"js/constants.js",
"js/utils.js",
"js/http.js",
"js/auth-common.js",
"js/login.js"
],
"js/forgot.js": [
"js/constants.js",
"js/utils.js",
"js/http.js",
"js/forgot.js"
],
"js/reset.js": [
"js/constants.js",
"js/utils.js",
"js/http.js",
"js/reset.js"
],
"js/signup.js": [
"js/constants.js",
"js/utils.js",
"js/http.js",
"js/auth-common.js",
"js/signup.js"
],
"js/dashboard.js": [
"js/constants.js",
"js/utils.js",
"js/http.js",
"js/errors.js",
"js/self.js",
"js/dashboard.js",
"js/dashboard-setting.js",
"js/dashboard-domain.js",
"js/dashboard-installation.js",
"js/dashboard-general.js",
"js/dashboard-moderation.js",
"js/dashboard-statistics.js",
"js/dashboard-import.js",
"js/dashboard-danger.js",
"js/dashboard-export.js",
],
"js/logout.js": [
"js/constants.js",
"js/utils.js",
"js/logout.js"
],
"js/commento.js": ["js/commento.js"],
"js/unsubscribe.js": [
"js/constants.js",
"js/utils.js",
"js/http.js",
"js/unsubscribe.js",
],
};
gulp.task("scss-devel", function () {
return gulp.src(scssSrc)
.pipe(sourcemaps.init())
.pipe(sass({outputStyle: "expanded"}).on("error", sass.logError))
.pipe(sourcemaps.write())
.pipe(gulp.dest(develPath + cssDir));
});
gulp.task("scss-prod", function () {
return gulp.src(scssSrc)
.pipe(sass({outputStyle: "compressed"}).on("error", sass.logError))
.pipe(cleanCss({compatibility: "ie8", level: 2}))
.pipe(gulp.dest(prodPath + cssDir));
});
gulp.task("html-devel", function () {
gulp.src([htmlGlob]).pipe(gulp.dest(develPath));
});
gulp.task("html-prod", function () {
gulp.src(htmlGlob)
.pipe(htmlMinifier({collapseWhitespace: true, removeComments: true}))
.pipe(gulp.dest(prodPath))
});
gulp.task("fonts-devel", function () {
gulp.src([fontsGlob]).pipe(gulp.dest(develPath + fontsDir));
});
gulp.task("fonts-prod", function () {
gulp.src([fontsGlob]).pipe(gulp.dest(prodPath + fontsDir));
});
gulp.task("images-devel", function () {
gulp.src([imagesGlob]).pipe(gulp.dest(develPath + imagesDir));
});
gulp.task("images-prod", function () {
gulp.src([imagesGlob]).pipe(gulp.dest(prodPath + imagesDir));
});
gulp.task("js-devel", function () {
for (let outputFile in jsCompileMap) {
gulp.src(jsCompileMap[outputFile])
.pipe(sourcemaps.init())
.pipe(concat(outputFile))
.pipe(rename(outputFile))
.pipe(sourcemaps.write())
.pipe(gulp.dest(develPath))
}
});
gulp.task("js-prod", function () {
for (let outputFile in jsCompileMap) {
gulp.src(jsCompileMap[outputFile])
.pipe(concat(outputFile))
.pipe(rename(outputFile))
.pipe(uglify())
.pipe(gulp.dest(prodPath))
}
});
gulp.task("lint", function () {
return gulp.src(jsGlob)
.pipe(eslint())
.pipe(eslint.failAfterError())
});
gulp.task("devel", ["scss-devel", "html-devel", "fonts-devel", "images-devel", "lint", "js-devel"]);
gulp.task("prod", ["scss-prod", "html-prod", "fonts-prod", "images-prod", "lint", "js-prod"]);

View File

@@ -1,20 +1,22 @@
(function (global, document) { (function (global, document) {
"use strict";
// Redirect the user to the dashboard if there's a cookie. If the cookie is // Redirect the user to the dashboard if there's a cookie. If the cookie is
// invalid, they would be redirected back to the login page *after* the // invalid, they would be redirected back to the login page *after* the
// cookie is deleted. // cookie is deleted.
global.loggedInRedirect = function() { global.loggedInRedirect = function() {
if (global.cookieGet("commentoOwnerToken") !== undefined) if (global.cookieGet("commentoOwnerToken") !== undefined) {
document.location = global.commentoOrigin + "/dashboard"; document.location = global.origin + "/dashboard";
}
} }
// Prefills the email field from the URL parameter. // Prefills the email field from the URL parameter.
global.prefillEmail = function() { global.prefillEmail = function() {
if (paramGet("email") != undefined) { if (global.paramGet("email") !== undefined) {
$("#email").val(paramGet("email")); $("#email").val(global.paramGet("email"));
$("#password").click(); $("#password").click();
} }
}; };
} (window, document)); } (window.commento, document));

File diff suppressed because one or more lines are too long

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