[
  {
    "path": ".env.example",
    "content": "FATHOM_GZIP=true\nFATHOM_DEBUG=true\nFATHOM_DATABASE_DRIVER=\"sqlite3\"\nFATHOM_DATABASE_NAME=\"./fathom.db\"\nFATHOM_DATABASE_USER=\"\"\nFATHOM_DATABASE_PASSWORD=\"\"\nFATHOM_DATABASE_HOST=\"\"\nFATHOM_SECRET=\"abcdefghijklmnopqrstuvwxyz1234567890\"\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\ncustom: ['https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=LJ5WZVA9ER9GJ']\n"
  },
  {
    "path": ".github/workflows/goimports.yml",
    "content": "name: Check imports\non: [ push, pull_request ]\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/setup-go@v2\n      with:\n        go-version: '1.19'\n    - uses: actions/checkout@master\n    - name: Check imports\n      shell: bash\n      run: |\n        export PATH=$(go env GOPATH)/bin:$PATH\n        go get golang.org/x/tools/cmd/goimports\n        diff -u <(echo -n) <(goimports -d .)\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\non:\n  push:\n    tags:\n      - 'v*'\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v3\n        with:\n          fetch-depth: 0\n      - name: Set up Go\n        uses: actions/setup-go@v3\n        with:\n            go-version: '1.19'\n      - name: Run GoReleaser\n        uses: goreleaser/goreleaser-action@v3\n        with:\n          # either 'goreleaser' (default) or 'goreleaser-pro'\n          distribution: goreleaser\n          version: latest\n          args: release --rm-dist -p 1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "---\nname: Run tests\non: [ push, pull_request ]\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/setup-go@v2\n        with:\n          go-version: '1.19'\n      - uses: actions/checkout@master\n      - name: Run tests\n        run: |\n          make test\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\n.env*\n!.env.example\ncoverage.out\nbuild\ndist\n*.db\nfathom\n!cmd/fathom\n\nassets/build\nassets/dist\nbin/\n"
  },
  {
    "path": ".goreleaser.yml",
    "content": "# Documentation http://goreleaser.com\nbefore:\n  hooks:\n    - make assets/dist\n    - go install github.com/gobuffalo/packr/v2/packr2@latest\nbuilds:\n  - main: main.go\n    goos:\n      - linux\n    goarch:\n      - amd64\n      - 386\n      - arm64\n    ldflags:\n      - -extldflags \"-static\" -s -w -X \"main.version={{.Version}}\" -X \"main.commit={{.Commit}}\" -X \"main.date={{.Date}}\"\n    hooks:\n      pre: packr2\n      post: packr2 clean\nchecksum:\n  name_template: 'checksums.txt'\nsnapshot:\n  name_template: \"{{ .Tag }}-next\"\nchangelog:\n  sort: asc\n  filters:\n    exclude:\n      - '^docs:'\n      - '^test:'\nrelease:\n  draft: true\n  mode: append\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, gender identity and expression, level of experience,\neducation, socio-economic status, nationality, personal appearance, race,\nreligion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\n  advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n  address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project team at team@usefathom.com. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html\n\n[homepage]: https://www.contributor-covenant.org\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM node:alpine AS assetbuilder\nWORKDIR /app\nCOPY package*.json ./\nCOPY gulpfile.js ./\nCOPY assets/ ./assets/\nRUN npm install && NODE_ENV=production ./node_modules/gulp/bin/gulp.js\n\nFROM golang:latest AS binarybuilder\nRUN go install github.com/gobuffalo/packr/v2/packr2@latest\nWORKDIR /go/src/github.com/usefathom/fathom\nCOPY . /go/src/github.com/usefathom/fathom\nCOPY --from=assetbuilder /app/assets/build ./assets/build\nARG GOARCH=amd64\nARG GOOS=linux\nRUN make ARCH=${GOARCH} OS=${GOOS} docker\n\nFROM alpine:latest\nEXPOSE 8080\nHEALTHCHECK --retries=10 CMD [\"wget\", \"-qO-\", \"http://localhost:8080/health\"]\nRUN apk add --update --no-cache bash ca-certificates\nWORKDIR /app\nCOPY --from=binarybuilder /go/src/github.com/usefathom/fathom/fathom .\nCMD [\"./fathom\", \"server\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) Conva Ventures Inc\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "EXECUTABLE := fathom\nLDFLAGS += -extldflags \"-static\" -X \"main.version=$(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//')\" -X \"main.commit=$(shell git rev-parse HEAD)\"  -X \"main.date=$(date -u +'%Y-%m-%dT%H:%M:%SZ')\"\nMAIN_PKG := ./main.go\nPACKAGES ?= $(shell go list ./... | grep -v /vendor/)\nASSET_SOURCES ?= $(shell find assets/src/. -type f)\nGO_SOURCES ?= $(shell find . -name \"*.go\" -type f)\nGOPATH=$(shell go env GOPATH)\nARCH := amd64\nOS := linux\n\n.PHONY: all\nall: build \n\n.PHONY: build\nbuild: $(EXECUTABLE)\n\n$(EXECUTABLE): $(GO_SOURCES) assets/build\n\tgo build -o $@ $(MAIN_PKG)\n\n.PHONY: docker\ndocker: $(GO_SOURCES) $(GOPATH)/bin/packr2\n\tGOOS=$(OS) GOARCH=$(ARCH) $(GOPATH)/bin/packr2 build -v -ldflags '-w $(LDFLAGS)' -o $(EXECUTABLE) $(MAIN_PKG)\n\n$(GOPATH)/bin/packr2:\n\tGOBIN=$(GOPATH)/bin go install github.com/gobuffalo/packr/v2/packr2@latest\n\n.PHONY: npm \nnpm:\n\tif [ ! -d \"node_modules\" ]; then npm install; fi\n\nassets/build: $(ASSET_SOURCES) npm\n\t./node_modules/gulp/bin/gulp.js\t\n\nassets/dist: $(ASSET_SOURCES) npm\n\tNODE_ENV=production ./node_modules/gulp/bin/gulp.js\n\n.PHONY: clean\nclean:\n\tgo clean -i ./...\n\t$(GOPATH)/bin/packr clean\n\trm -rf $(EXECUTABLE)\n\n.PHONY: fmt\nfmt:\n\tgo fmt $(PACKAGES)\n\n.PHONY: vet\nvet:\n\tgo vet $(PACKAGES)\n\n.PHONY: errcheck\nerrcheck:\n\t@which errcheck > /dev/null; if [ $$? -ne 0 ]; then \\\n\t\tgo install github.com/kisielk/errcheck@latest; \\\n\tfi\n\terrcheck $(PACKAGES)\n\n.PHONY: lint\nlint:\n\t@which golint > /dev/null; if [ $$? -ne 0 ]; then \\\n\t\tgo install github.com/golang/lint/golint@latest; \\\n\tfi\n\tfor PKG in $(PACKAGES); do golint -set_exit_status $$PKG || exit 1; done;\n\n.PHONY: test\ntest:\n\tfor PKG in $(PACKAGES); do go test $$PKG || exit 1; done;\n\n.PHONY: referrer-spam-blacklist\nreferrer-spam-blacklist:\n\twget https://raw.githubusercontent.com/matomo-org/referrer-spam-blacklist/master/spammers.txt -O pkg/aggregator/data/blacklist.txt\n\tgo-bindata -prefix \"pkg/aggregator/data/\" -o pkg/aggregator/bindata.go -pkg aggregator pkg/aggregator/data/\n"
  },
  {
    "path": "README.md",
    "content": "Fathom Lite - simple website analytics\n==============================\n[![Go Report Card](https://goreportcard.com/badge/github.com/usefathom/fathom)](https://goreportcard.com/report/github.com/usefathom/fathom)\n[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/usefathom/fathom/master/LICENSE)\n\nFathom Lite is a previous and open-source version of [Fathom Analytics](https://usefathom.com) (a paid, hosted [Google Analytics alternative](https://usefathom.com/google-analytics-alternative)). It was the very first version of our software, and has been downloaded millions of times!\n\nWhile we are no longer adding features to this Lite version, we will be continuing to maintain it long-term and fix any bugs that come up.\n\n![Screenshot of the Fathom dashboard](https://github.com/usefathom/fathom/raw/master/assets/src/img/fathom.jpg?v=7)\n\n## Fathom Lite vs Fathom (hosted)\n\nToday’s [Fathom Analytics](https://usefathom.com) is a hosted product with [simple pricing](https://usefathom.com/pricing) based on monthly pageviews. The same core capabilities are included on every plan: for example [API access](https://usefathom.com/api), up to 50 sites, custom events and ecommerce tracking, unlimited email reports and CSV exports, and [forever data retention](https://usefathom.com/features)—with [privacy-law compliance](https://usefathom.com/compliance) and no cookie banner required for analytics.\n\nIf you’d rather not run servers or maintenance yourself, try a [30-day free trial](https://usefathom.com/ref/GITHUB) (that link applies a **$10 credit** on your first invoice). Browse [all features](https://usefathom.com/features) or the [live demo](https://app.usefathom.com/demo).\n\n![Screenshot of the Fathom Analytics Dashboard](https://usefathom.com/assets/images/fathom-screenshot.png)\n\n| Feature | Fathom Lite | Fathom (hosted) |\n|---------|-------------|-----------------|\n| Fully managed | ✗ Self-hosted; you run servers and updates | ✓ Managed for you; [pay per pageviews](https://usefathom.com/pricing) |\n| Cookie-free (no analytics banner) | ✗ Uses cookies | ✓ [Cookie-free](https://usefathom.com/features) tracking |\n| Current dashboard (real-time, live visitors, filters, details) | ✗ Older Lite UI | ✓ Full dashboard — see [features](https://usefathom.com/features) |\n| API | ✗ | ✓ [API](https://usefathom.com/api) on all plans |\n| Custom events, ecommerce, UTMs | ✗ | ✓ Events, revenue, campaigns |\n| [EU isolation](https://usefathom.com/features/eu-isolation) & [custom domains](https://usefathom.com/features/custom-domains) | ✗ | ✓ EU routing, first-party script domains |\n| [GA import](https://usefathom.com/features/ga-importer), email reports, CSV export, [shared dashboards](https://usefathom.com/features) | ✗ | ✓ Unlimited reports & exports |\n| Many sites per account | ✗ Typical single install | ✓ Up to 50 sites (more available) |\n| Email support & SLA | ✗ Community / as-is | ✓ Support on every plan |\n| Global CDN, scaling, backups | ✗ Your responsibility | ✓ Included |\n| Active feature development | ✗ Bugfixes / maintenance | ✓ Ongoing — [trial](https://usefathom.com/ref/GITHUB) · [sign up](https://app.usefathom.com/register) |\n\n\n## Installation\n\n\n### Production\n\nYou can install Fathom on your server by following [our simple instructions](docs/Installation%20instructions.md).\n\n### Development\n\nFor getting a development version of Fathom up & running, go through the following steps.\n\n1. Ensure you have [Go](https://golang.org/doc/install#install) and [NPM](https://www.npmjs.com) installed\n1. Download the code: `git clone https://github.com/usefathom/fathom.git $GOPATH/src/github.com/usefathom/fathom`\n1. Compile the project into an executable: `make build`\n1. (Optional) Set [custom configuration values](docs/Configuration.md)\n1. (Required) Register a user account: `./fathom user add --email=<email> --password=<password>`\n1. Start the webserver: `./fathom server` and then visit **http://localhost:8080** to access your analytics dashboard\n\n## Docker\n\n### Building\n\nEnsure you have Docker installed and run `docker build -t fathom .`.\nRun the container with `docker run -d -p 8080:8080 fathom`.\n\n### Running\n\nTo run [our pre-built Docker image](https://hub.docker.com/r/usefathom/fathom/), run `docker run -d -p 8080:8080 usefathom/fathom:latest`\n\n## Tracking snippet\n\nTo start tracking, create a site in your Fathom dashboard and copy the tracking snippet to the website(s) you want to track.\n\n### Content Security Policy\n\nIf you use a [Content Security Policy (CSP)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) to specify security policies for your website, Fathom requires the following CSP directives (replace `yourfathom.com` with the URL to your Fathom instance):\n\n```\nscript-src: yourfathom.com;\nimg-src: yourfathom.com;\n```\n\n## Copyright and license\n\nMIT licensed. Fathom and Fathom logo are trademarks of Fathom Analytics.\n"
  },
  {
    "path": "assets/src/404.html",
    "content": "<!DOCTYPE html>\n<html class=\"no-js\" lang=\"en\">\n<head>\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>Not found - Fathom</title> \n  <link href=\"/assets/css/styles.css\" rel=\"stylesheet\">\n  <link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/assets/img/favicon.png\">\n</head>\n<body class=\"404\">\n   <div class=\"login-page flex-rapper\">\n      <div>\n         <h1>Page not found</h1>\n         <p>Sorry, it seems that the requested page does not exist.</p>\n      </div>\n   </div>\n</body>\n</html>\n"
  },
  {
    "path": "assets/src/css/chart.css",
    "content": ".box-graph {\n\tbackground: white;\n\tmargin-left: 0 !important;\n}\n\n#chart * { \n\tbox-sizing: content-box; \n}\n\n#chart .muted { \n\tfill: #98a0a6;\n}\n\n#chart {\n\theight: 240px;\n}\n\n.bar-pageviews {\n\tfill: #88ffc6;\n}\n\n.bar-visitors {\n\tfill: #533feb;\n}\n\n.axis .domain{ \n\tstroke: none; \n}\n\n.axis line {\n\tstroke: rgba(218, 218, 218, 0.5);\n}\n\n.axis text {\n\tfont-size: 12px;\n\tfill: #98a0a6;\n}\n\n.d3-tip {\n\tfont-size: 12px;\n\tcolor: #959da5;\n\ttext-align: left;\n\tbackground: rgba(0,0,0,.8);\n\tborder-radius: 3px;\n}\n\n.tip-heading {\n\tfont-weight: 600;\n\tpadding: 10px;\n\tline-height: 1;\n}\n\n.tip-content {\n\tdisplay: flex;\n\n}\n\n.tip-content > div {\n\tpadding: 5px 10px;\n\twidth: 50%;\n\tdisplay: block;\n\tflex: 1;\n\tmin-width: 90px;\n}\n\n\n.tip-pageviews {\n\tborder-top: 3px solid rgb(136, 255, 198);\n}\n\n.tip-visitors {\n\tborder-top: 3px solid rgb(83, 63, 235);\n}\n\n.tip-number {\n\tcolor: #dfe2e5;\n\tfont-weight: 600;\n}\n"
  },
  {
    "path": "assets/src/css/fonts-overpass.css",
    "content": "@font-face {\n\tfont-family: 'overpass';\n\tsrc: url('../fonts/overpass-thin.eot'); /* IE9 Compat Modes */\n\tsrc: url('../fonts/overpass-thin.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */\n\turl('../fonts/overpass-thin.woff2') format('woff2'), /* Super Modern Browsers */\n\turl('../fonts/overpass-thin.woff') format('woff'), /* Pretty Modern Browsers */\n\turl('../fonts/overpass-thin.ttf')  format('truetype'); /* Safari, Android, iOS */\n\tfont-weight: 200;\n\tfont-style: normal;\n}\n\n@font-face {\n\tfont-family: 'overpass';\n\tsrc: url('../fonts/overpass-thin-italic.eot');\n\tsrc: url('../fonts/overpass-thin-italic.eot?#iefix') format('embedded-opentype'),\n\turl('../fonts/overpass-thin-italic.woff2') format('woff2'),\n\turl('../fonts/overpass-thin-italic.woff') format('woff'),\n\turl('../fonts/overpass-thin-italic.ttf')  format('truetype');\n\tfont-weight: 200;\n\tfont-style: italic;\n}\n\n\n@font-face {\n\tfont-family: 'overpass';\n\tsrc: url('../fonts/overpass-extralight.eot');\n\tsrc: url('../fonts/overpass-extralight.eot?#iefix') format('embedded-opentype'),\n\turl('../fonts/overpass-extralight.woff2') format('woff2'),\n\turl('../fonts/overpass-extralight.woff') format('woff'),\n\turl('../fonts/overpass-extralight.ttf')  format('truetype');\n\tfont-weight: 300;\n\tfont-style: normal;\n}\n\n@font-face {\n\tfont-family: 'overpass';\n\tsrc: url('../fonts/overpass-extralight-italic.eot');\n\tsrc: url('../fonts/overpass-extralight-italic.eot?#iefix') format('embedded-opentype'),\n\turl('../fonts/overpass-extralight-italic.woff2') format('woff2'),\n\turl('../fonts/overpass-extralight-italic.woff') format('woff'),\n\turl('../fonts/overpass-extralight-italic.ttf')  format('truetype');\n\tfont-weight: 300;\n\tfont-style: italic;\n}\n\n\n\n@font-face {\n\tfont-family: 'overpass';\n\tsrc: url('../fonts/overpass-light.eot');\n\tsrc: url('../fonts/overpass-light.eot?#iefix') format('embedded-opentype'),\n\turl('../fonts/overpass-light.woff2') format('woff2'),\n\turl('../fonts/overpass-light.woff') format('woff'),\n\turl('../fonts/overpass-light.ttf')  format('truetype');\n\tfont-weight: 400;\n\tfont-style: normal;\n}\n\n@font-face {\n\tfont-family: 'overpass';\n\tsrc: url('../fonts/overpass-light-italic.eot');\n\tsrc: url('../fonts/overpass-light-italic.eot?#iefix') format('embedded-opentype'),\n\turl('../fonts/overpass-light-italic.woff2') format('woff2'),\n\turl('../fonts/overpass-light-italic.woff') format('woff'),\n\turl('../fonts/overpass-light-italic.ttf')  format('truetype');\n\tfont-weight: 400;\n\tfont-style: italic;\n}\n\n\n\n@font-face {\n\tfont-family: 'overpass';\n\tsrc: url('../fonts/overpass-regular.eot');\n\tsrc: url('../fonts/overpass-regular.eot?#iefix') format('embedded-opentype'),\n\turl('../fonts/overpass-regular.woff2') format('woff2'),\n\turl('../fonts/overpass-regular.woff') format('woff'),\n\turl('../fonts/overpass-regular.ttf')  format('truetype');\n\tfont-weight: 500;\n\tfont-style: normal;\n}\n\n@font-face {\n\tfont-family: 'overpass';\n\tsrc: url('../fonts/overpass-italic.eot');\n\tsrc: url('../fonts/overpass-italic.eot?#iefix') format('embedded-opentype'),\n\turl('../fonts/overpass-italic.woff2') format('woff2'),\n\turl('../fonts/overpass-italic.woff') format('woff'),\n\turl('../fonts/overpass-italic.ttf')  format('truetype');\n\tfont-weight: 500;\n\tfont-style: italic;\n}\n\n\n\n\n@font-face {\n\tfont-family: 'overpass';\n\tsrc: url('../fonts/overpass-semibold.eot');\n\tsrc: url('../fonts/overpass-semibold.eot?#iefix') format('embedded-opentype'),\n\turl('../fonts/overpass-semibold.woff2') format('woff2'),\n\turl('../fonts/overpass-semibold.woff') format('woff'),\n\turl('../fonts/overpass-semibold.ttf')  format('truetype');\n\tfont-weight: 600;\n\tfont-style: normal;\n}\n\n@font-face {\n\tfont-family: 'overpass';\n\tsrc: url('../fonts/overpass-semibold-italic.eot');\n\tsrc: url('../fonts/overpass-semibold-italic.eot?#iefix') format('embedded-opentype'),\n\turl('../fonts/overpass-semibold-italic.woff2') format('woff2'),\n\turl('../fonts/overpass-semibold-italic.woff') format('woff'),\n\turl('../fonts/overpass-semibold-italic.ttf')  format('truetype');\n\tfont-weight: 600;\n\tfont-style: italic;\n}\n\n\n\n\n@font-face {\n\tfont-family: 'overpass';\n\tsrc: url('../fonts/overpass-bold.eot');\n\tsrc: url('../fonts/overpass-bold.eot?#iefix') format('embedded-opentype'),\n\turl('../fonts/overpass-bold.woff2') format('woff2'),\n\turl('../fonts/overpass-bold.woff') format('woff'),\n\turl('../fonts/overpass-bold.ttf')  format('truetype');\n\tfont-weight: 700;\n\tfont-style: normal;\n}\n\n@font-face {\n\tfont-family: 'overpass';\n\tsrc: url('../fonts/overpass-bold-italic.eot');\n\tsrc: url('../fonts/overpass-bold-italic.eot?#iefix') format('embedded-opentype'),\n\turl('../fonts/overpass-bold-italic.woff2') format('woff2'),\n\turl('../fonts/overpass-bold-italic.woff') format('woff'),\n\turl('../fonts/overpass-bold-italic.ttf')  format('truetype');\n\tfont-weight: 700;\n\tfont-style: italic;\n}\n\n\n\n@font-face {\n\tfont-family: 'overpass';\n\tsrc: url('../fonts/overpass-extrabold.eot');\n\tsrc: url('../fonts/overpass-extrabold.eot?#iefix') format('embedded-opentype'),\n\turl('../fonts/overpass-extrabold.woff2') format('woff2'),\n\turl('../fonts/overpass-extrabold.woff') format('woff'),\n\turl('../fonts/overpass-extrabold.ttf')  format('truetype');\n\tfont-weight: 800;\n\tfont-style: normal;\n}\n\n@font-face {\n\tfont-family: 'overpass';\n\tsrc: url('../fonts/overpass-extrabold-italic.eot');\n\tsrc: url('../fonts/overpass-extrabold-italic.eot?#iefix') format('embedded-opentype'),\n\turl('../fonts/overpass-extrabold-italic.woff2') format('woff2'),\n\turl('../fonts/overpass-extrabold-italic.woff') format('woff'),\n\turl('../fonts/overpass-extrabold-italic.ttf')  format('truetype');\n\tfont-weight: 800;\n\tfont-style: italic;\n}\n\n\n@font-face {\n\tfont-family: 'overpass';\n\tsrc: url('../fonts/overpass-heavy.eot');\n\tsrc: url('../fonts/overpass-heavy.eot?#iefix') format('embedded-opentype'),\n\turl('../fonts/overpass-heavy.woff2') format('woff2'),\n\turl('../fonts/overpass-heavy.woff') format('woff'),\n\turl('../fonts/overpass-heavy.ttf')  format('truetype');\n\tfont-weight: 900;\n\tfont-style: normal;\n}\n\n@font-face {\n\tfont-family: 'overpass';\n\tsrc: url('../fonts/overpass-heavy-italic.eot');\n\tsrc: url('../fonts/overpass-heavy-italic.eot?#iefix') format('embedded-opentype'),\n\turl('../fonts/overpass-heavy-italic.woff2') format('woff2'),\n\turl('../fonts/overpass-heavy-italic.woff') format('woff'),\n\turl('../fonts/overpass-heavy-italic.ttf')  format('truetype');\n\tfont-weight: 900;\n\tfont-style: italic;\n}\n"
  },
  {
    "path": "assets/src/css/pikaday.css",
    "content": "@charset \"UTF-8\";\n\n/*!\n * Pikaday\n * Copyright © 2014 David Bushell | BSD & MIT license | http://dbushell.com/\n */\n\n.pika-single {\n    z-index: 9999;\n    display: block;\n    position: relative;\n    background: #fff; \n    border-radius: 4px; \n    box-shadow: 0 2px 8px 0 rgba(70,73,77,.16);\n}\n\n.pika-single:before,\n.pika-single:after {\n    content: \" \";\n    display: table;\n}\n.pika-single:after { clear: both }\n.pika-single { *zoom: 1 }\n\n.pika-single.is-hidden {\n    display: none;\n}\n\n.pika-single.is-bound {\n    position: absolute;\n}\n\n.pika-lendar {\n    float: left;\n    width: 240px;\n    margin: 8px;\n}\n\n.pika-title {\n    position: relative;\n    text-align: center;\n}\n\n.pika-label {\n    display: inline-block;\n    *display: inline;\n    position: relative;\n    z-index: 9999;\n    overflow: hidden;\n    margin: 0;\n    padding: 5px 3px;\n    font-size: 14px;\n    line-height: 20px;\n    font-weight: 500;\n}\n.pika-title select {\n    cursor: pointer;\n    position: absolute;\n    z-index: 9998;\n    margin: 0;\n    left: 0;\n    top: 5px;\n    filter: alpha(opacity=0);\n    opacity: 0;\n}\n\n.pika-prev,\n.pika-next {\n    display: block;\n    cursor: pointer;\n    position: relative;\n    outline: none;\n    border: 0;\n    padding: 0;\n    width: 20px;\n    height: 30px;\n    text-indent: 20px;\n    white-space: nowrap;\n    overflow: hidden;\n    background-color: transparent;\n    background-position: center center;\n    background-repeat: no-repeat;\n    background-size: 75% 75%;\n    opacity: .5;\n    *position: absolute;\n    *top: 0;\n}\n\n.pika-prev:hover,\n.pika-next:hover {\n    opacity: 1;\n}\n\n.pika-prev,\n.is-rtl .pika-next {\n    float: left;\n    background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAeCAYAAAAsEj5rAAAAUklEQVR42u3VMQoAIBADQf8Pgj+OD9hG2CtONJB2ymQkKe0HbwAP0xucDiQWARITIDEBEnMgMQ8S8+AqBIl6kKgHiXqQqAeJepBo/z38J/U0uAHlaBkBl9I4GwAAAABJRU5ErkJggg==');\n    *left: 0;\n}\n\n.pika-next,\n.is-rtl .pika-prev {\n    float: right;\n    background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAeCAYAAAAsEj5rAAAAU0lEQVR42u3VOwoAMAgE0dwfAnNjU26bYkBCFGwfiL9VVWoO+BJ4Gf3gtsEKKoFBNTCoCAYVwaAiGNQGMUHMkjGbgjk2mIONuXo0nC8XnCf1JXgArVIZAQh5TKYAAAAASUVORK5CYII=');\n    *right: 0;\n}\n\n.pika-prev.is-disabled,\n.pika-next.is-disabled {\n    cursor: default;\n    opacity: .2;\n}\n\n.pika-select {\n    display: inline-block;\n    *display: inline;\n}\n\n.pika-table {\n    width: 100%;\n    border-collapse: collapse;\n    border-spacing: 0;\n    border: 0;\n}\n\n.pika-table th,\n.pika-table td {\n    width: 14.285714285714286%;\n    padding: 0;\n}\n\n.pika-table th {\n    color: #98a0a6;\n    font-size: 12px;\n    line-height: 25px;\n    font-weight: 500;\n    text-align: center;\n}\n\n.pika-button {\n    cursor: pointer;\n    display: block;\n    box-sizing: border-box;\n    -moz-box-sizing: border-box;\n    outline: none;\n    border: 0;\n    margin: 0;\n    width: 100%;\n    padding: 4px;\n    color: #666;\n    font-size: 12px;\n    line-height: 16px;\n    text-align: right;\n    background: #f5f7fa;\n}\n\n.pika-week {\n    font-size: 11px;\n    color: #98a0a6;\n}\n\n.is-today .pika-button {\n    color: #533feb;\n    font-weight: 500;\n}\n\n.is-selected .pika-button,\n.has-event .pika-button {\n    color: #fff;\n    font-weight: 500;\n    background: #46494d;\n}\n\n.has-event .pika-button {\n    background: #005da9;\n}\n\n.is-disabled .pika-button,\n.is-inrange .pika-button {\n    background: #88ffc6;\n}\n\n.is-startrange .pika-button {\n    background: #88ffc6;\n    box-shadow: none;\n}\n\n.is-endrange .pika-button {\n    color: #fff;\n    background: #33aaff;\n    box-shadow: none;\n}\n\n.is-disabled .pika-button {\n    pointer-events: none;\n    cursor: default;\n}\n\n.is-outside-current-month .pika-button {\n}\n\n.is-selection-disabled {\n    pointer-events: none;\n    cursor: default;\n}\n\n.pika-button:hover,\n.pika-row.pick-whole-week:hover .pika-button {\n    color: #46494d;\n    background: #88ffc6;\n    box-shadow: none;\n}\n\n.pika-table abbr {\n    border: none;\n    cursor: normal;\n    text-decoration: none;\n        font-size: 12px; \n}\n\n"
  },
  {
    "path": "assets/src/css/styles.css",
    "content": "/*\noverpass    200, 500, 600\npurple      #533feb\ngreen       #88ffc6\n\ndark        #46494d\nmedium      #98a0a6\nlight       #f5f7fa\n\npadding     8, 16, 20, 32, 64, 128, 256, 512, 1024\nfont size   12, 16, 64\n */\n\n::selection       { background: #a0ffd1; }\n::-moz-selection  { background: #a0ffd1; }\n* { margin: 0; padding: 0; border: none; outline: none; list-style: none; box-sizing: border-box; text-decoration: none; font-size: 100%; vertical-align: baseline; line-height: 1.2; }\n\nhtml, body, #root { height: 100%;  background: #f5f7fa; }\nbody { overflow-y: scroll;  font: 400 16px \"overpass\", sans-serif; color: #222; text-align: center; padding: 8px; }\n\n.rapper { text-align: left; margin: 0 auto; max-width: 1124px; }\n\nheader, footer { margin: 16px 0; }\nsection {}\nfooter { font-size: 12px; color: #aaa; }\n\na { transition: ease color .2s; }\n\nnav { position: relative; font-size: 12px; }\nnav a { color: #222; }\nfooter nav a { color: #aaa; padding: 4px 0; display: inline-block; }\nfooter nav a:hover { color: #222; }\n\nheader li { padding: 8px 0; }\nnav li.logo a { color: #533feb; font-size: 16px; }\nnav li.logo a:hover { color: #222; }\n\nnav li.sites { background: #fff url(\"data:image/svg+xml;charset=utf8,%3Csvg aria-hidden='true' role='img' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 512'%3E%3Cpath fill='%23533feb' d='M151.5 347.8L3.5 201c-4.7-4.7-4.7-12.3 0-17l19.8-19.8c4.7-4.7 12.3-4.7 17 0L160 282.7l119.7-118.5c4.7-4.7 12.3-4.7 17 0l19.8 19.8c4.7 4.7 4.7 12.3 0 17l-148 146.8c-4.7 4.7-12.3 4.7-17 0z'%3E%3C/path%3E%3C/svg%3E\") 97% 8px no-repeat; border-radius: 4px; padding: 8px; background-size: 10px auto; }\nnav li.sites.expanded { background: #533feb url(\"data:image/svg+xml;charset=utf8,%3Csvg aria-hidden='true' role='img' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 512'%3E%3Cpath fill='%23fff' d='M168.5 164.2l148 146.8c4.7 4.7 4.7 12.3 0 17l-19.8 19.8c-4.7 4.7-12.3 4.7-17 0L160 229.3 40.3 347.8c-4.7 4.7-12.3 4.7-17 0L3.5 328c-4.7-4.7-4.7-12.3 0-17l148-146.8c4.7-4.7 12.3-4.7 17 0z'%3E%3C/path%3E%3C/svg%3E\") 97% 8px no-repeat; background-size: 10px auto; }\nnav li ul { display: none; position: absolute; z-index: 1001; width: 100%; background: #533feb; border-radius: 4px; padding: 8px 0; margin: 0 0 0 -8px; }\nnav li.expanded ul { display: block; box-shadow: 0 2px 8px 0 rgba(34,34,34,.10); }\nnav li.sites.expanded a { color: #fff; }\nnav li ul li { padding: 0 4px; width: 100%; }\nnav li ul a { color: #ddd; display: inline-block; width: 100%; font-size: 13px; padding: 4px; }\nnav li ul a:hover { background: rgba(255,255,255,.2); border-radius: 2px; }\nnav li ul li.add-new a { color: #88ffc6 !important; }\n\nnav .settings svg { width: 16px; display: inline-block; transition: ease all .2s; }\nnav .settings svg path { fill: #533feb; }\nnav .settings svg:hover { transform: rotate(45deg); }\t\t\t\n\nnav.date-nav { margin: 32px 0 8px; }\nnav.date-nav ul { margin: 0 8px 0 0; border-radius: 4px; background: #e8ecee; padding: 8px 8px 8px 16px; display: inline-block; }\nnav.date-nav li { display: inline-block; padding: 0; }\nnav.date-nav li a,\nnav.date-nav li input { margin-right: 8px; color: #98a0a6; }\nnav.date-nav li input { background: transparent; border: 0; font-size: inherit; padding: 0; cursor: pointer; display: inline-block; width: 9.8ch; min-width: 60px; text-align: center; }\nnav.date-nav li.current a { color: #533feb; }\nnav.date-nav li a:hover { color: #2b2d2f; }\nnav.date-nav li span { margin-right: 8px; }\n\n/*\n.date-nav { margin-bottom: 12px; }\n.date-nav li a { position: relative; font-size: 12px; text-transform: uppercase; padding-right: 8px; }\n.date-nav li.custom { color: #aaa; float: right; margin: 0; }\n.date-nav li.custom input { \n\tdisplay: inline-block;\n\twidth: 75px;\n\tborder: 0;\n\tfont-size: inherit;\n\tbackground: transparent;\n\tpadding: 0;\n\tcursor: pointer; \n}\n.date-nav li a:hover { color: #aaa; }\n.date-nav li.active a { padding-right: 8px; z-index: 1; color: #222; }\n.date-nav li.active a:after { content:\"\"; background: #88ffc6; display: block; width: 100%; height: 3px; position: absolute; top: 6px; z-index: -1; margin: 0 0 0 -4px; }\n*/\n.box { background: #fff; border-radius: 4px; margin-bottom: 16px; padding: 16px; box-shadow: 0 2px 8px 0 rgba(34,34,34,.10); }\n\n.box-totals { background: #222; color: #ddd; }\n\n.totals-detail { display: grid; grid-template-columns: 1fr 1.6fr; grid-gap: 12px; }\n.total-numbers { text-align: right; }\n.current-detail div { color: #88ffc6; }\n\n.table-row { display: grid; grid-template-columns: 4fr 1fr 1fr; grid-gap: 12px; padding: 8px 0; position: relative; }\n.table-row.header { font-size: 12px; text-transform: uppercase; color: #aaa; }\n.table-row a { color: #222; }\n.table-row a:hover { color: #533feb; }\n.cell { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; text-align: right; z-index: 1; position: relative; }\n.cell.main-col { text-align: left; }\n\n.table-row:after {  content: \"\"; background: #88ffc6; position: absolute; height: 34px; top: 0; left: -16px; opacity: .2; border-right: 2px solid #45ce8c; z-index: 0; } \n.table-row.header:after { background: none; border: none; }\n\n.row.pag {\n\tmargin-top: 16px;\n\tgrid-template-columns: 1fr 1fr;\n\tgrid-gap: 4px;\n}\n.row.pag a {\n    color: #98a0a6;\n    font-size: 19px;\n}\n\n.modal-wrap { position: fixed; height: 100%; width: 100%; top: 0; left: 0; z-index: 1977; background: rgba(20,20,20,.8); display: grid; grid-template-columns: 1fr; align-items: center; }\n.modal { max-width: 480px; width: 100%; margin: 0 auto; text-align: left; background: #fff; z-index: 1978; height: auto; border-radius: 4px; box-shadow: 0 2px 8px 0 rgba(34,34,34,.10); overflow: hidden; }\n.modal p { padding: 16px; font-size: 12px; color: #aaa; }\n\nsmall { font-size: 12px; color: #aaa; }\nsmall a { color: #aaa; }\n\n.modal form { padding: 0 16px; }\nlabel { padding: 4px 0; display: block; }\nfieldset { display: block; width: 100%; margin-bottom: 32px; }\nfieldset:last-child { margin-bottom: 16px; }\ninput, textarea { background: #f5f7fa; padding: 4px; width: 100%; display: block; font: 400 16px \"overpass\", sans-serif; color: #222; border-bottom: 2px solid #e6e8eb; }\ntextarea { font-size: 12px; min-height: 120px; resize: none; overflow-y: scroll; }\nbutton { background: #88ffc6; color: #222; padding: 4px 16px; font: 400 16px \"overpass\", sans-serif; cursor: pointer; border-radius: 4px; }\ndiv.delete a { color: red; }\n\n.w100:after{width:99%}.w99:after{width:99%}.w98:after{width:98%}.w97:after{width:97%}.w96:after{width:96%}.w95:after{width:95%}.w94:after{width:94%}.w93:after{width:93%}.w92:after{width:92%}.w91:after{width:91%}.w90:after{width:90%}.w89:after{width:89%}.w88:after{width:88%}.w87:after{width:87%}.w86:after{width:86%}.w85:after{width:85%}.w84:after{width:84%}.w83:after{width:83%}.w82:after{width:82%}.w81:after{width:81%}.w80:after{width:80%}.w79:after{width:79%}.w78:after{width:78%}.w77:after{width:77%}.w76:after{width:76%}.w75:after{width:75%}.w74:after{width:74%}.w73:after{width:73%}.w72:after{width:72%}.w71:after{width:71%}.w70:after{width:70%}.w69:after{width:69%}.w68:after{width:68%}.w67:after{width:67%}.w66:after{width:66%}.w65:after{width:65%}.w64:after{width:64%}.w63:after{width:63%}.w62:after{width:62%}.w61:after{width:61%}.w60:after{width:60%}.w59:after{width:59%}.w58:after{width:58%}.w57:after{width:57%}.w56:after{width:56%}.w55:after{width:55%}.w54:after{width:54%}.w53:after{width:53%}.w52:after{width:52%}.w51:after{width:51%}.w50:after{width:50%}.w49:after{width:49%}.w48:after{width:48%}.w47:after{width:47%}.w46:after{width:46%}.w45:after{width:45%}.w44:after{width:44%}.w43:after{width:43%}.w42:after{width:42%}.w41:after{width:41%}.w40:after{width:40%}.w39:after{width:39%}.w38:after{width:38%}.w37:after{width:37%}.w36:after{width:36%}.w35:after{width:35%}.w34:after{width:34%}.w33:after{width:33%}.w32:after{width:32%}.w31:after{width:31%}.w30:after{width:30%}.w29:after{width:29%}.w28:after{width:28%}.w27:after{width:27%}.w26:after{width:26%}.w25:after{width:25%}.w24:after{width:24%}.w23:after{width:23%}.w22:after{width:22%}.w21:after{width:21%}.w20:after{width:20%}.w19:after{width:19%}.w18:after{width:18%}.w17:after{width:17%}.w16:after{width:16%}.w15:after{width:15%}.w14:after{width:14%}.w13:after{width:13%}.w12:after{width:12%}.w11:after{width:11%}.w10:after{width:10%}.w09:after{width:9%}.w08:after{width:8%}.w07:after{width:7%}.w06:after{width:6%}.w05:after{width:5%}.w04:after{width:4%}.w03:after{width:3%}.w02:after{width:2%}.w01:after{width:1%}.w00:after{width:0}\t\n\n\n\n@media ( min-width: 1220px ) {\n\tbody { padding: 0; }\n\theader, footer { margin: 32px 0; }\n\n\t.main-nav li,\n\tfooter li {\n\t\tdisplay: inline-block;\n\t\tmargin-right: 16px;\n\t}\n\n\tnav li.sites { display: inline-block; width: 204px; margin-right: 0; }\n\tnav li.sites, nav li.settings { float: right; }\n\tnav li.sites, nav li.sites.expanded { background-position: 184px 8px; }\n\n\tnav .date-nav li { margin-right: 8px; }\n\tnav li ul { width: 204px; right: 0; margin: 0;  }\n\n\t.box { margin: 0; padding: 32px 16px; }\n\t.boxes { display: grid; grid-template-columns: 276px 420px 420px; grid-gap: 4px; }\n\t.box-totals { grid-column: 1; grid-row: 1/3; }\n\t.box-graph { grid-column: 2/4; grid-row: 1;  margin: 0 0 4px 0; }\n\t.box-pages { grid-column: 2; grid-row: 2 ; }\n\t.box-referrers { grid-column: 3; grid-row: 2; }\n\n\t.half { display: grid; grid-template-columns: 1fr 1fr; grid-gap: 12px; align-items: center; }\n\t.half div { text-align: right; }\n\t.half div.submit { text-align: left; }\n\n\t.totals-detail { display: block; margin-bottom: 32px; }\n\t.total-heading { font-size: 12px; text-transform: uppercase; color: #888; line-height: .8; }\n\t.total-numbers { font-size: 68px; font-weight: 200; letter-spacing: -.06em; text-align: left; }\n\n}\n\n\n.login-page.flex-rapper { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }\n.login-rapper { text-align: left; width: 320px; }\n.login-page label { position: relative; }\n.login-page  label svg { width: 16px;  height: auto;  fill: #533feb; position: absolute; left: -28px; top: 12px; }\n.login-page  input { border-radius: 0; border: none; outline: none; background: #fff; box-sizing: border-box; width: 100%; padding: 8px; margin-bottom: 4px; display: block; }\n.login-page  input{ line-height: 1; font: 400 16px/1 'overpass', sans-serif; }\n\n.login-page  button { width: 100%; box-sizing: border-box; padding: 8px; background: #533feb; color: #fff; border: 0; font: 400 16px/1 'overpass', sans-serif; }\n\n.login-page  small { color: #98a0a6; font-size: 14px; margin: 48px auto 0 auto; text-align: center; display: block; }\n.login-page  small a { color: #98a0a6; font-size: 14px; display: inline-block; padding: 0 8px; }\n\n\n.notice { background: #ecedf4; border-radius: 4px; padding: 16px; text-align: left; color: color: #222; font-size: 14px; display: grid; grid-template-columns: 1fr min-content; margin: 32px 0; gap: 32px; align-items: center; }\n.notice a { color: #aaa; }\n.notice .notice-text a { color: #533feb; text-decoration: underline; }\n"
  },
  {
    "path": "assets/src/css/util.css",
    "content": ".small-margin {\n\tmargin-top: 20px;\n\tmargin-bottom: 20px;\n}\n\n.cf:after {\n\tcontent: \"\";\n\tdisplay: table;\n\tclear: both;\n}\n\n.ac {\n    text-transform: uppercase;\n}\n\n.sm {\n    font-size: 11px;\n    font-weight: 500;\n    color: #98a0a6;\n}\n\n@media(max-width: 600px) {\n\t.hide-on-mobile { display: none !important; }\n}\n\n.right {\n\tfloat: right;\n}\n\n.left {\n\tfloat: left;\n}\n\n.notification {\n\tposition: fixed;\n\ttop: 20px;\n\tleft: 0; right: 0;\n\ttext-align: center;\n\twidth: 100%;\n}\n\n.notification .notification-error {\n\tpadding: 4px;\n\tdisplay: inline-block;\n\tbackground-color: #f2dede;\n\tborder: 1px solid #ebccd1;\n}\n\n@keyframes fadeInUp {\n\t0% { opacity: 0; transform: translateY(20px); }\n\t100% { opacity: 1; transform: translateY(0);  }\n}\n\n@keyframes fadeInDown {\n\t0% { opacity: 0; transform: translateY(-20px); }\n\t100% { opacity: 1; transform: translateY(0);  }\n}\n\n.animated { animation-duration: .4s; animation-fill-mode: both; }\n\n.delayed_02s { animation-delay: .2s; }\n.delayed_03s { animation-delay: .3s; }\n.delayed_04s { animation-delay: .4s; }\n.delayed_05s { animation-delay: .5s; }\n.delayed_06s { animation-delay: .6s; }\n\n.fadeInUp { animation-name: fadeInUp; }\n.fadeInDown { animation-name: fadeInDown; }\n\n.loading {\n\topacity: 0.6;\n}\n"
  },
  {
    "path": "assets/src/index.html",
    "content": "<!DOCTYPE html>\n<html class=\"no-js\" lang=\"en\">\n<head>\n  <title>Fathom - simple website analytics</title>\n  <link href=\"assets/css/styles.css\" rel=\"stylesheet\">\n  <meta charset=\"utf-8\">\n  <meta name=\"referrer\" content=\"no-referrer\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"assets/img/favicon.png\">\n</head>\n<body class=\"fathom\">\n  <div id=\"root\"></div>\n  <noscript>To use Fathom, please enable JavaScript.</noscript> \n  <script>\n    document.documentElement.className = document.documentElement.className.replace('no-js', '');\n  </script>\n  <script src=\"assets/js/script.js\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "assets/src/js/components/Chart.js",
    "content": "'use strict';\n\nimport { h, Component } from 'preact';\nimport Client from '../lib/client.js';\nimport { bind } from 'decko';\nimport * as numbers from '../lib/numbers.js';\nimport * as d3 from 'd3';\nimport 'd3-transition';\nd3.tip = require('d3-tip');\n\nconst formatMonth = d3.timeFormat(\"%b\"),\n  formatMonthDay = d3.timeFormat(\"%b %e\");\n\nconst t = d3.transition().duration(600).ease(d3.easeQuadOut);\n\nfunction xTickFormat(tickStep, n) {\n  let formatters = {\n    hour: (d, i) => {\n      if(i === 0 || i === n-1) {\n        return formatMonthDay(d);\n      }\n\n      if(n <= 24 && d.getHours() === 0 || d.getHours() === 12) {\n        return d.getHours() + \":00\";\n      }     \n\n      return '';\n    },\n\n    day: (d, i, o) => {\n      if( i === 0 || i === n-1) {\n       return formatMonthDay(d);\n      }\n\n      return '';\n    },\n    month: (d, i) => {\n      if(n>28) {\n        return d.getFullYear();\n      }\n\n      return d.getMonth() === 0 ? d.getFullYear() : formatMonth(d);\n    }\n  }\n\n  return formatters[tickStep];\n}\n\n\nconst incrementers = {\n  'hour': d => d.setHours(d.getHours() + 1),\n  'day': d => d.setDate(d.getDate() + 1),\n  'month': d => d.setMonth(d.getMonth() + 1)\n}\n\nfunction incrementDate(date, incr) {\n  return incrementers[incr](date);\n}\n\nclass Chart extends Component {\n  constructor(props) {\n    super(props)\n\n    this.state = {\n      loading: false,\n      data: [],\n      chartData: [],\n      diffInDays: 1,\n    }\n  }\n\n  componentWillReceiveProps(newProps) {\n    let daysDiff = Math.round((newProps.dateRange[1]-newProps.dateRange[0])/1000/24/60/60);\n\n    this.setState({\n      diffInDays: daysDiff,\n      tickStep: newProps.tickStep,\n    })\n\n    if( newProps.siteId != this.props.siteId || newProps.dateRange[0] != this.props.dateRange[0] || newProps.dateRange[1] != this.props.dateRange[1] ) {\n      this.fetchData(newProps)\n    } else if (newProps.tickStep != this.props.tickStep) {\n      this.chartData()\n      this.redrawChart()\n    }\n  }\n\n  @bind\n  chartData() {\n    let startDate = new Date(this.props.dateRange[0]);\n    let endDate = this.props.dateRange[1];\n    let newData = [];\n\n    // if grouping by month, fix date to 1st of month\n    if(this.state.tickStep === 'month') {\n      startDate.setDate(1);\n    }\n\n    // instantiate JS Date objects\n    let data = this.state.data.map(d => {\n      d.Date = new Date(d.Date);\n      return d\n    })\n  \n    // make sure we have values for each date (so 0 value for gaps)\n    let currentDate = startDate, nextDate, tick, offset = 0;\n    while(currentDate < endDate) {\n      tick = {\n          \"Pageviews\": 0,\n          \"Visitors\": 0,\n          \"Date\": new Date(currentDate),\n      };\n\n      nextDate = new Date(currentDate)\n      nextDate = incrementDate(nextDate, this.state.tickStep)\n\n      // grab data that falls between currentDate & nextDate\n      for(let i=data.length-offset-1; i>=0; i--) {\n        // Because 9AM should be included in 9AM-10AM range, check for equality here\n        if( data[i].Date >= nextDate) {\n          break;\n        }\n\n         // increment offset so subsequent dates can skip first X items in array\n         offset += 1;\n\n        // continue to next item in array if we're still below our target date\n        if( data[i].Date < currentDate) {\n          continue;\n        }\n\n        // add to tick data\n        tick.Pageviews += data[i].Pageviews;\n        tick.Visitors += data[i].Visitors;\n      }\n\n      newData.push(tick);  \n      currentDate = nextDate;\n    }\n\n    this.setState({\n      chartData: newData,\n    })\n  }\n  \n  @bind\n  prepareChart() {\n    let padding = { top: 12, right: 12, bottom: 24, left: 40 };\n    let height = 240;\n    let width = this.base.clientWidth;\n\n    this.innerWidth = width - padding.left - padding.right;\n    this.innerHeight = height - padding.top - padding.bottom;\n\n    this.ctx =  d3.select(this.base)\n      .append('svg')\n      .attr('width', width)\n      .attr('height', height)\n      .append('g')\n      .attr('transform', 'translate(' + padding.left + ', '+padding.top+')')\n\n    this.x = d3.scaleBand().range([0, this.innerWidth]).padding(0.1)\n    this.y = d3.scaleLinear().range([this.innerHeight, 0])\n\n      // tooltip\n    this.tip = d3.tip().attr('class', 'd3-tip').html((d) => {\n      let title;\n\n      if(this.state.tickStep === 'hour') {\n        title = `${d.Date.toLocaleDateString()} ${d.Date.getHours()}:00 - ${d.Date.getHours() + 1}:00`\n      } else if(this.state.tickStep === 'day' ) {\n        title = `${d.Date.toLocaleDateString()} (${d3.timeFormat(\"%a\")(d.Date)})`\n      } else {\n        title = d3.timeFormat(\"%B %Y\")(d.Date)\n      }\n\n      return (`\n      <div class=\"tip-heading\">${title}</div>\n      <div class=\"tip-content\">\n        <div class=\"tip-pageviews\">\n          <div class=\"tip-number\">${numbers.formatPretty(d.Pageviews)}</div>\n          <div class=\"tip-metric\">Pageviews</div>\n        </div>\n        <div class=\"tip-visitors\">\n          <div class=\"tip-number\">${numbers.formatPretty(d.Visitors)}</div>\n          <div class=\"tip-metric\">Visitors</div>\n        </div>\n      </div>`\n      )});\n\n    this.ctx.call(this.tip)\n  }\n\n  @bind\n  redrawChart() {\n    let data = this.state.chartData;\n\n    if( ! this.ctx ) {\n      this.prepareChart()\n    }\n\n    let graph = this.ctx;\n    let innerWidth = this.innerWidth\n    let innerHeight = this.innerHeight\n    const max = d3.max(data, d => d.Pageviews); \n    let x = this.x.domain(data.map(d => d.Date))\n    let y = this.y.domain([0, max*1.1])\n    let yAxis = d3.axisLeft().scale(y).ticks(3).tickSize(-innerWidth).tickFormat(v => numbers.formatPretty(v))\n    let xAxis = d3.axisBottom().scale(x).tickFormat(xTickFormat(this.state.tickStep, data.length))\n\n    // only show first and last tick if we have more than 28 ticks to show\n    if(data.length > 28) {\n      let tickValues = data.map(d => d.Date).filter((d, i) => i === 0 || i === data.length-1);\n      xAxis.tickValues(tickValues)\n      xAxis.tickFormat(xTickFormat(this.state.tickStep, tickValues.length))\n    }\n\n    // empty previous graph\n    graph.selectAll('*').remove()\n\n    // add text indicating there's no data yet\n    if( max === 0 ) {\n      graph.append('text')\n        .attr('class', 'muted')\n        .attr(\"text-anchor\", \"middle\")\n        .attr('x', innerWidth / 2 - 30)\n        .attr('y', innerHeight / 2)\n        .text('Nothing here, yet.')\n    }\n\n    // add axes\n    let yTicks = graph.append(\"g\")\n      .attr(\"class\", \"y axis\")\n      .call(yAxis);\n\n    let xTicks = graph.append(\"g\")\n      .attr(\"class\", \"x axis\")\n      .attr('transform', 'translate(0,' + innerHeight + ')')\n      .call(xAxis)\n    \n    // add data for each tick that we have something to show for\n    let barWidth = x.bandwidth()\n    let ticks = graph.selectAll('.item')\n      .data(data.filter(d => d.Pageviews > 0 || d.Visitors > 0)).enter()\n      .append('g')\n      .attr('class', 'item') \n      \n    let pageviews = ticks.append('rect')\n      .attr('class', 'bar-pageviews') \n      .attr('x', d => x(d.Date))\n      .attr('width', barWidth)\n      .attr(\"y\", innerHeight)\n      .attr(\"height\", 0)\n      \n    let visitors = ticks.append('rect')\n      .attr('class', 'bar-visitors')\n      .attr('x', d => x(d.Date) )\n      .attr('width', barWidth)\n      .attr(\"y\", innerHeight)\n      .attr(\"height\", 0)\n    \n    pageviews.transition(t)\n      .attr('y', d => y(d.Pageviews))\n      .attr('height', (d) => innerHeight - y(d.Pageviews)) \n\n    visitors.transition(t)\n      .attr('height', (d) => (innerHeight - y(d.Visitors)) )\n      .attr('y', (d) => y(d.Visitors))   \n      \n    // add event listeners for tooltips\n    ticks.on('mouseover', this.tip.show).on('mouseout', this.tip.hide)   \n  }\n\n  @bind\n  fetchData(props) {\n    this.setState({ loading: true })\n\n    let before = props.dateRange[1]/1000;\n    let after = props.dateRange[0]/1000;\n\n    Client.request(`/sites/${props.siteId}/stats/site?before=${before}&after=${after}`)\n      .then(data => { \n\n        this.setState({ \n          loading: false,\n          data: data,\n        })\n\n        this.chartData()\n        this.redrawChart()\n      })\n  }\n \n  render(props, state) {\n    return (\n       <div id=\"chart\" class={state.loading ? 'loading': ''}></div>\n    )\n  }\n}\n\nexport default Chart\n"
  },
  {
    "path": "assets/src/js/components/CountWidget.js",
    "content": "'use strict';\n\nimport { h, Component } from 'preact';\nimport * as numbers from '../lib/numbers.js';\nimport { bind } from 'decko';\nimport classNames from 'classnames';\n\nconst duration = 600;\nconst easeOutQuint = function (t) { return 1+(--t)*t*t*t*t };\n\nclass CountWidget extends Component {\n  componentWillReceiveProps(newProps, newState) {\n    if(newProps.value == this.props.value) {\n      return;\n    }\n\n    this.countUp(this.props.value || 0, newProps.value);\n  }\n\n  // TODO: Move to component of its own\n  @bind\n  countUp(fromValue, toValue) {\n    const format = this.formatValue.bind(this);\n    const startValue = isFinite(fromValue) ? fromValue : 0;\n    const numberEl = this.numberEl;\n    const diff = toValue - startValue;\n    let startTime = performance.now();\n\n    const tick = function(t) {\n      let progress = Math.min(( t - startTime ) / duration, 1);\n      let newValue = startValue + (easeOutQuint(progress) * diff);\n      numberEl.textContent = format(newValue)\n\n      if(progress < 1) {\n        window.requestAnimationFrame(tick);\n      }\n    }\n\n    window.requestAnimationFrame(tick);\n  }\n\n  @bind\n  formatValue(value) {\n    let formattedValue = \"-\";\n\n    if(isFinite(value)) {\n      switch(this.props.format) {\n        case \"percentage\":\n          formattedValue = numbers.formatPercentage(value)\n        break;\n\n        default:\n        case \"number\":\n          formattedValue = numbers.formatPretty(Math.round(value))\n        break;\n\n        case \"duration\":\n          formattedValue = numbers.formatDuration(value)\n        break;\n      }\n    }\n\n    return formattedValue;\n  }\n\n  render(props, state) {\n    return (\n       <div class={classNames(\"totals-detail\", { loading: props.loading })}>\n        <div class=\"total-heading\">{props.title}</div>\n        <div class=\"total-numbers\" ref={(e) => { this.numberEl = e; }}>{this.formatValue(props.value)}</div>\n      </div>\n    )\n  }\n}\n\nexport default CountWidget\n"
  },
  {
    "path": "assets/src/js/components/DatePicker.js",
    "content": "'use strict';\n\nimport { h, Component } from 'preact';\nimport { bind } from 'decko';\nimport Pikadayer from './Pikadayer.js';\nimport classNames from 'classnames';\nimport {hashParams} from \"../lib/util\";\n\nconst padZero = (n) => n < 10 ? '0'+n : ''+n;\n\nlet now = new Date();\nwindow.setInterval(() => {\n  now = new Date();\n}, 60000 );\n\nconst availablePeriods = {\n  '1d': {\n    label: '1d',\n    start: function() {\n      return new Date(now.getFullYear(), now.getMonth(), now.getDate());\n    },\n    end: function() {\n      return this.start();\n    },\n  },\n  '1w': {\n    label: '1w',\n    start: function() {\n      return new Date(now.getFullYear(), now.getMonth(), now.getDate()-6);\n    },\n    end: function() {\n      return new Date(now.getFullYear(), now.getMonth(), now.getDate());\n    },\n },\n '4w': {\n    label: '4w',\n    start: function() {\n      return new Date(now.getFullYear(), now.getMonth(), now.getDate()-4*7+1);\n    },\n    end: function() {\n      return new Date(now.getFullYear(), now.getMonth(), now.getDate());\n    },\n },\n 'mtd': {\n    label: 'Mtd',\n    start: function() {\n      return new Date(now.getFullYear(),  now.getMonth(), 1);\n    },\n    end: function() {\n      return new Date(now.getFullYear(), now.getMonth()+1, 0);\n    },\n },\n'qtd': {\n  label: 'Qtd',\n  start: function() {\n    let qs = Math.ceil((now.getMonth()+1) / 3) * 3 - 3;\n    return new Date(now.getFullYear(), qs, 1);\n\n  },\n  end: function() {\n    let start = this.start();\n    return new Date(start.getFullYear(), start.getMonth() + 3, 0);\n  },\n },\n 'ytd': {\n  label: 'Ytd',\n  start: function() {\n    return new Date(now.getFullYear(), 0, 1);\n  },\n  end: function() {\n    return new Date(now.getFullYear()+1, 0, 0);\n  },\n },\n 'all': {\n  label: 'All',\n  start: function() {\n    return new Date(2018, 6, 1);\n  },\n  end: function() {\n    return new Date();\n  },\n }\n}\n\nclass DatePicker extends Component {\n  constructor(props) {\n    super(props)\n\n    let params = hashParams();\n\n    this.state = {\n      period: params.p || window.localStorage.getItem('period') || '1w',\n      startDate: new Date(params.s || 'now'),\n      endDate: new Date(params.e || 'now'),\n      groupBy: params.g || 'day',\n      site: params.site || 1\n    }    \n    this.state.diff = this.calculateDiff(this.state.startDate, this.state.endDate)\n\n    if(this.state.period !== 'custom') {\n      this.updateDatesFromPeriod(this.state.period, params.g)\n    } else {\n      this.props.onChange(this.state);\n    }\n  }\n\n  componentDidMount() {\n    window.addEventListener('keydown', this.handleKeyPress);\n  }\n\n  componentWillUnmount() {\n    window.removeEventListener('keydown', this.handleKeyPress)\n  }\n\n  @bind\n  updateDatesFromPeriod(period, groupBy) {\n    if(typeof(availablePeriods[period]) !== \"object\") {\n      period = \"1w\";\n    }\n    let p = availablePeriods[period];\n    this.setDateRange(p.start(), p.end(), period, groupBy);\n  }\n\n  @bind\n  setDateRange(start, end, period, groupBy) {\n    // don't update state if start > end. user may be busy picking dates.\n    if(start > end) {\n      return;\n    }\n\n    // include start & end day by forcing time\n    start.setHours(0, 0, 0);\n    end.setHours(23, 59, 59);\n\n    let diff =  this.calculateDiff(start, end)\n    if(!groupBy) {\n      groupBy = 'day';\n\n      if(diff >= 31) {\n        groupBy = 'month';\n      } else if( diff < 2) {\n        groupBy = 'hour';\n      }\n    }\n   \n   \n    this.setState({\n      period: period,\n      startDate: start,\n      endDate: end,\n      diff: diff,\n      groupBy: groupBy,\n    });\n\n    // use slight delay for updating rest of application to allow this function to be called again\n    if(!this.timeout) {\n      this.timeout = window.setTimeout(() => {\n        this.props.onChange(this.state);\n        this.updateURL()\n        this.timeout = null;\n      }, 5)\n    }\n  }\n\n  calculateDiff(start, end) {\n    return Math.round((end - start) / 1000 / 60 / 60 / 24)\n  }\n\n  updateURL() {\n    if(this.state.period !== 'custom') {\n      window.history.replaceState(this.state, null, `#!p=${this.state.period}&g=${this.state.groupBy}&site=${this.state.site}`)\n    } else {\n      window.history.replaceState(this.state, null, `#!p=custom&s=${encodeURIComponent(this.state.startDate.toISOString())}&e=${encodeURIComponent(this.state.endDate.toISOString())}&g=${this.state.groupBy}&site=${this.state.site}`)\n    }\n  }\n\n  @bind\n  setPeriod(e) {\n    e.preventDefault();\n\n    let newPeriod = e.target.getAttribute('data-value');\n    if( newPeriod === this.state.period) {\n      return;\n    }\n\n    window.localStorage.setItem('period', this.state.period)\n    this.updateDatesFromPeriod(newPeriod);\n  }\n\n  dateValue(date) {\n    return date.getFullYear() + '-' + padZero(date.getMonth() + 1) + '-' + padZero(date.getDate());\n  }\n\n  @bind\n  setStartDate(date) {\n    this.setDateRange(date, this.state.endDate, 'custom')\n  }\n\n  @bind\n  setEndDate(date) {\n    this.setDateRange(this.state.startDate, date, 'custom')\n  }\n\n  @bind\n  handleKeyPress(evt) {\n    // Don't handle input when the user is in a text field or text area.\n    let tag = evt.target.tagName;\n    if(tag === \"INPUT\" || tag === \"TEXTAREA\") {\n      return;\n    }\n\n    // TODO: Account for leap years\n    let diff = this.state.endDate - this.state.startDate + 1000;\n    let newStartDate, newEndDate;\n\n    switch(evt.which) {\n      // left-arrow\n      case 37:\n        newStartDate = new Date(+this.state.startDate - diff)\n        newEndDate = new Date(+this.state.endDate - diff)\n        this.setDateRange(newStartDate, newEndDate)\n      break;\n\n      //right-arrow\n      case 39:\n      newStartDate = new Date(+this.state.startDate + diff)\n      newEndDate = new Date(+this.state.endDate + diff)\n      this.setDateRange(newStartDate, newEndDate)\n      break;\n    }\n  }\n\n  @bind\n  setGroupBy(e) {\n    this.setState({\n      groupBy: e.target.getAttribute('data-value')\n    })\n    this.props.onChange(this.state);\n    this.updateURL()\n  }\n\n  render(props, state) {\n    const presets = Object.keys(availablePeriods).map((id) => {\n      let p = availablePeriods[id];\n      return (\n        <li class={classNames({ current: id == state.period })}>\n          <a href=\"javascript:void(0);\" data-value={id} onClick={this.setPeriod}>{p.label}</a>\n        </li>\n      );\n    });\n\n    return (\n      <nav class=\"date-nav sm ac\">\n        <ul>\n          {presets}\n        </ul>\n        <ul>\n          <li><Pikadayer value={this.dateValue(state.startDate)} onSelect={this.setStartDate} /> <span>›</span> <Pikadayer value={this.dateValue(state.endDate)} onSelect={this.setEndDate}  /></li>\n        </ul>\n        <ul>\n         {state.diff < 31 ? (<li class={classNames({ current: 'hour' === state.groupBy })}><a href=\"javascript:;\" data-value=\"hour\" onClick={this.setGroupBy}>Hourly</a></li>) : ''}\n         <li class={classNames({ current: 'day' === state.groupBy })}><a href=\"javascript:;\" data-value=\"day\" onClick={this.setGroupBy}>Daily</a></li>\n         {state.diff >= 31 ? (<li class={classNames({ current: 'month' === state.groupBy })}><a href=\"javascript:;\" data-value=\"month\" onClick={this.setGroupBy}>Monthly</a></li>) : ''}\n        </ul>\n      </nav>\n    )\n\n    /*\n    <ul>\n        <li class=\"current\"><a href=\"#\">Daily</a></li>\n        <li><a href=\"#\">Monthly</a></li>\n    </ul>\n    */\n\n  }\n}\n\nexport default DatePicker\n"
  },
  {
    "path": "assets/src/js/components/Gearwheel.js",
    "content": "'use strict';\n\nimport { h, Component } from 'preact';\n\nclass Gearwheel extends Component {\n  render(props, state) {\n    // don't show if visible prop is false\n    if(!props.visible) {\n      return '';\n    }\n\n    return (\n        <li class=\"settings\">\n            <a href=\"javascript:void(0);\" onClick={props.onClick}><svg role=\"img\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\"><path d=\"M444.788 291.1l42.616 24.599c4.867 2.809 7.126 8.618 5.459 13.985-11.07 35.642-29.97 67.842-54.689 94.586a12.016 12.016 0 0 1-14.832 2.254l-42.584-24.595a191.577 191.577 0 0 1-60.759 35.13v49.182a12.01 12.01 0 0 1-9.377 11.718c-34.956 7.85-72.499 8.256-109.219.007-5.49-1.233-9.403-6.096-9.403-11.723v-49.184a191.555 191.555 0 0 1-60.759-35.13l-42.584 24.595a12.016 12.016 0 0 1-14.832-2.254c-24.718-26.744-43.619-58.944-54.689-94.586-1.667-5.366.592-11.175 5.459-13.985L67.212 291.1a193.48 193.48 0 0 1 0-70.199l-42.616-24.599c-4.867-2.809-7.126-8.618-5.459-13.985 11.07-35.642 29.97-67.842 54.689-94.586a12.016 12.016 0 0 1 14.832-2.254l42.584 24.595a191.577 191.577 0 0 1 60.759-35.13V25.759a12.01 12.01 0 0 1 9.377-11.718c34.956-7.85 72.499-8.256 109.219-.007 5.49 1.233 9.403 6.096 9.403 11.723v49.184a191.555 191.555 0 0 1 60.759 35.13l42.584-24.595a12.016 12.016 0 0 1 14.832 2.254c24.718 26.744 43.619 58.944 54.689 94.586 1.667 5.366-.592 11.175-5.459 13.985L444.788 220.9a193.485 193.485 0 0 1 0 70.2zM336 256c0-44.112-35.888-80-80-80s-80 35.888-80 80 35.888 80 80 80 80-35.888 80-80z\"></path></svg></a>\n        </li>\n    )\n  }\n}\n\nexport default Gearwheel\n"
  },
  {
    "path": "assets/src/js/components/LoginForm.js",
    "content": "'use strict';\n\nimport { h, render, Component } from 'preact';\nimport Client from '../lib/client.js';\nimport Notification from '../components/Notification.js';\nimport { bind } from 'decko';\n\nclass LoginForm extends Component {\n\n  constructor(props) {\n    super(props)\n    this.state = {\n      email: '',\n      password: '',\n      message: ''\n    }\n  }\n\n  @bind\n  handleSubmit(e) {\n    e.preventDefault();\n    this.setState({ message: '' });\n\n    Client.request('session', {\n      method: \"POST\",\n      data: {\n        email: this.state.email,\n        password: this.state.password,\n      }\n    }).then((r) => {\n        this.props.onSuccess()\n    }).catch((e) => {\n      this.setState({\n        message: e.code === 'invalid_credentials' ? \"Invalid username or password\" : e.message,\n        password: ''\n      });\n    });\n  }\n\n  @bind\n  updatePassword(e) {\n    this.setState({ password: e.target.value });\n  }\n\n  @bind\n  updateEmail(e) {\n    this.setState({ email: e.target.value });\n  }\n\n  @bind\n  clearMessage() {\n    this.setState({ message: '' });\n  }\n\n  render(props, state) {\n    return (\n      <form method=\"POST\" onSubmit={this.handleSubmit}>\n        <div class=\"\">\n          <label><svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 496 512\"><path d=\"M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm128 421.6c-35.9 26.5-80.1 42.4-128 42.4s-92.1-15.9-128-42.4V416c0-35.3 28.7-64 64-64 11.1 0 27.5 11.4 64 11.4 36.6 0 52.8-11.4 64-11.4 35.3 0 64 28.7 64 64v13.6zm30.6-27.5c-6.8-46.4-46.3-82.1-94.6-82.1-20.5 0-30.4 11.4-64 11.4S204.6 320 184 320c-48.3 0-87.8 35.7-94.6 82.1C53.9 363.6 32 312.4 32 256c0-119.1 96.9-216 216-216s216 96.9 216 216c0 56.4-21.9 107.6-57.4 146.1zM248 120c-48.6 0-88 39.4-88 88s39.4 88 88 88 88-39.4 88-88-39.4-88-88-88zm0 144c-30.9 0-56-25.1-56-56s25.1-56 56-56 56 25.1 56 56-25.1 56-56 56z\"/></svg></label>\n          <input type=\"email\" name=\"email\" placeholder=\"Email address\" required=\"\" value={state.email} onInput={this.updateEmail}  />\n         </div>\n        \n        <div class=\"\">\n          <label><svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 448 512\"><path d=\"M224 420c-11 0-20-9-20-20v-64c0-11 9-20 20-20s20 9 20 20v64c0 11-9 20-20 20zm224-148v192c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V272c0-26.5 21.5-48 48-48h16v-64C64 71.6 136-.3 224.5 0 312.9.3 384 73.1 384 161.5V224h16c26.5 0 48 21.5 48 48zM96 224h256v-64c0-70.6-57.4-128-128-128S96 89.4 96 160v64zm320 240V272c0-8.8-7.2-16-16-16H48c-8.8 0-16 7.2-16 16v192c0 8.8 7.2 16 16 16h352c8.8 0 16-7.2 16-16z\"/></svg></label>\n          <input type=\"password\" name=\"password\" placeholder=\"**********\" required=\"\" autocomplete=\"off\" value={state.password} onInput={this.updatePassword} />\n        </div>\n         \n        <div><button type=\"submit\">Sign in</button></div>\n\n        <Notification message={state.message} kind=\"\" onDismiss={this.clearMessage} />\n      </form>\n    )\n  }\n}\n\nexport default LoginForm\n"
  },
  {
    "path": "assets/src/js/components/LogoutButton.js",
    "content": "'use strict';\n\nimport { h, Component } from 'preact';\nimport Client from '../lib/client.js';\nimport { bind } from 'decko';\n\nclass LogoutButton extends Component {\n\n  @bind\n  handleSubmit(e) {\n    e.preventDefault();\n\n    Client.request('session', {\n      method: \"DELETE\",\n    }).then((r) => { this.props.onSuccess() })\n  }\n\n  render() {\n    if(document.cookie.indexOf('auth') < 0) {\n      return ''\n    }\n\n    return (\n      <a href=\"#\" onClick={this.handleSubmit}>Sign out</a>\n    )\n  }\n}\n\nexport default LogoutButton\n"
  },
  {
    "path": "assets/src/js/components/Notification.js",
    "content": "'use strict'\n\nimport { h, Component } from 'preact';\nimport { bind } from 'decko';\n\nclass Notification extends Component {\n  constructor(props) {\n    super(props)\n\n    this.state = {\n      message: props.message,\n      kind: props.kind || 'error'\n    }\n    this.timeout = 0\n  }\n\n  componentWillReceiveProps(newProps) {\n    if(newProps.message === this.state.message) {\n      return;\n    }\n\n    this.setState({ \n      message: newProps.message, \n      kind: newProps.kind || 'error' \n    })\n    \n    window.clearTimeout(this.timeout)\n    this.timeout = window.setTimeout(this.props.onDismiss, 5000)\n  }\n\n  render(props, state) {\n    if(state.message === '') {\n      return ''\n    }\n\n    return (\n      <div class={`notification`}>\n        <div class={`notification-${state.kind}`}>\n          {state.message}\n        </div>\n      </div>\n  )}\n}\n\nexport default Notification\n"
  },
  {
    "path": "assets/src/js/components/Pikadayer.js",
    "content": "'use strict';\n\nimport Pikaday from 'pikaday';\nimport { h, Component } from 'preact';\n\nclass Pikadayer extends Component {\n  componentDidMount() {\n    this.pikaday = new Pikaday({ \n      field: this.base,\n      onSelect: this.props.onSelect,\n      position: 'bottom right',\n   })\n  }\n\n  componentWillReceiveProps(newProps) {\n    // make sure pikaday updates if we set a date using one of our presets\n    if(this.pikaday && newProps.value !== this.props.value) {\n      this.pikaday.setDate(newProps.value, true)\n    }\n  }\n\n  componentWillUnmount() {\n    this.pikaday.destroy()\n  }\n\n  render(props) {\n    return <input value={props.value} />\n  }\n}\n\nexport default Pikadayer\n"
  },
  {
    "path": "assets/src/js/components/Realtime.js",
    "content": "'use strict';\n\nimport { h, Component } from 'preact';\nimport Client from '../lib/client.js';\nimport { bind } from 'decko';\nimport * as numbers from '../lib/numbers.js';\n\nclass Realtime extends Component {\n\n  constructor(props) {\n    super(props)\n\n    this.state = {\n      count: 0\n    }\n  }\n\n  componentDidMount() {\n      this.fetchData(this.props.siteId);\n      this.interval = window.setInterval(this.handleIntervalEvent, 15000);\n  }\n\n  componentWillUnmount() {\n      window.clearInterval(this.interval);\n  }\n\n  componentWillReceiveProps(newProps, newState) {\n    if(!this.paramsChanged(this.props, newProps)) {\n      return;\n    }\n\n    this.fetchData(newProps.siteId)\n  }\n\n  paramsChanged(o, n) {\n    return o.siteId != n.siteId;\n  }\n\n  @bind\n  setDocumentTitle() {\n    // update document title\n    let visitorText = this.state.count == 1 ? 'visitor' : 'visitors';\n    document.title = ( this.state.count > 0 ? `${numbers.formatPretty(this.state.count)} current ${visitorText} — Fathom` : 'Fathom' );\n  }\n\n  @bind \n  handleIntervalEvent() {\n    this.fetchData(this.props.siteId)\n  }\n\n  @bind\n  fetchData(siteId) {\n    let url = `/sites/${siteId}/stats/site/realtime`\n    Client.request(url)\n      .then((d) => { \n        this.setState({ count: d })\n        this.setDocumentTitle();\n      })\n  }\n\n  render(props, state) {\n    let visitorText = state.count == 1 ? 'visitor' : 'visitors';\n    return (\n        <span><span class=\"count\">{numbers.formatPretty(state.count)}</span> <span>current {visitorText}</span></span>\n    )\n  }\n}\n\nexport default Realtime\n"
  },
  {
    "path": "assets/src/js/components/Sidebar.js",
    "content": "'use strict';\n\nimport { h, Component } from 'preact';\nimport Client from '../lib/client.js';\nimport { bind } from 'decko';\nimport CountWidget from './CountWidget.js';\n\n\nclass Sidebar extends Component {\n  constructor(props) {\n    super(props)\n\n    this.state = {\n      data: {},\n      loading: false,\n    }\n  }\n\n  componentWillReceiveProps(newProps, newState) {\n    if(!this.paramsChanged(this.props, newProps)) {\n      return;\n    }\n\n    this.fetchData(newProps);\n  }\n\n  paramsChanged(o, n) {\n    return o.siteId != n.siteId || o.dateRange[0] != n.dateRange[0] || o.dateRange[1] != n.dateRange[1];\n  }\n\n  @bind\n  fetchData(props) {\n    this.setState({ loading: true })\n    let before = props.dateRange[1]/1000;\n    let after = props.dateRange[0]/1000;\n\n    Client.request(`/sites/${props.siteId}/stats/site/agg?before=${before}&after=${after}`)\n      .then((data) => { \n        // request finished; check if timestamp range is still the one user wants to see\n        if(this.paramsChanged(props, this.props)) {\n          return;\n        }\n\n        // Make sure we always show at least 1 visitor when there are pageviews\n        if ( data.Visitors == 0 && data.Pageviews > 0 ) {\n          data.Visitors = 1\n        }\n\n        this.setState({ \n          loading: false,\n          data: data\n        })\n      })\n  }\n\n  render(props, state) {\n    return (\n      <div class=\"box box-totals\">\n        <CountWidget title=\"Unique visitors\" value={state.data.Visitors} loading={state.loading} />\n        <CountWidget title=\"Pageviews\" value={state.data.Pageviews} loading={state.loading} />\n        <CountWidget title=\"Avg time on site\" value={state.data.AvgDuration} format=\"duration\" loading={state.loading} />\n        <CountWidget title=\"Bounce rate\" value={state.data.BounceRate} format=\"percentage\" loading={state.loading} />\n      </div>\n    )\n  }\n}\n\nexport default Sidebar\n"
  },
  {
    "path": "assets/src/js/components/SiteSettings.js",
    "content": "'use strict';\n\nimport { h, Component } from 'preact';\nimport Client from '../lib/client.js';\nimport { bind } from 'decko';\n\nclass SiteSettings extends Component {\n    constructor(props) {\n        super(props)\n\n        this.state = {\n            copied: false,\n            updated: false,\n        }\n    }\n\n    componentDidMount() {\n        document.addEventListener('keydown', this.handleKeydownEvent);\n    }\n    componentWillUnmount() {\n        document.removeEventListener('keydown', this.handleKeydownEvent)\n    }\n\n    @bind\n    revertTemporaryState() {\n        this.setState({\n            copied: false, \n            updated: false\n        })\n    }\n\n    @bind\n    copyToClipboard(evt) {\n        this.textarea.select()\n        document.execCommand('copy')\n        this.setState({ copied: true })\n        window.setTimeout(this.revertTemporaryState, 2400)\n    }\n\n    @bind \n    deleteSite(evt) {\n        if(!confirm(\"Are you sure you want to delete this site? This action is irreversible - you will lose all the site's data.\")) {\n            return;\n        }\n\n        let site = this.props.site;\n        Client.request(`/sites/${site.id}`, {\n            method: \"DELETE\",\n          }).then((d) => {\n              this.props.onDelete(site)\n          })\n    }\n\n    @bind \n    onSubmit(evt) {\n        evt.preventDefault();\n        let site = this.props.site;\n        let url = site.unsaved ? `/sites` : `/sites/${site.id}`\n\n        Client.request(url, {\n            method: \"POST\",\n            data: {\n                name: site.name,\n            },\n          }).then((site) => {\n            this.setState({ updated: true})\n            window.setTimeout(this.revertTemporaryState, 2400)\n\n            site.unsaved = false\n            this.props.onUpdate(site)\n          })\n    }\n\n    @bind \n    handleTextareaClickEvent(evt) {\n        evt.target.select()\n    }\n\n    @bind \n    handleClickEvent(evt) {\n        // don't close if click was inside the modal\n        if ( evt.target.matches('.modal *, .modal')) {\n            return;\n        }\n\n        this.props.onClose()\n    }\n\n    @bind \n    handleKeydownEvent(evt) {\n        // close modal when pressing ESC \n        if(evt.which == 27) {\n            this.props.onClose()\n        }\n    }\n\n    @bind\n    setTextarea(el) {\n       this.textarea = el\n    }\n\n    @bind \n    updateSiteName(evt) {\n        this.props.site.name = evt.target.value;\n    }\n\n    render(props, state) {\n        return (\n        <div class=\"modal-wrap\" style={\"display: \" + ( props.visible ? '' : 'none')} onClick={this.handleClickEvent}>\n            <div class=\"modal\">\n                <p>{props.site.unsaved ? 'Add a new site to track with Fathom' : 'Update your site name or get your tracking code'}</p>\n                <form onSubmit={this.onSubmit}>\n                    <fieldset>\n                        <label for=\"site-name\">Site name</label>\n                        <input type=\"text\" name=\"site-name\" id=\"site-name\" placeholder=\"\" onChange={this.updateSiteName} value={props.site.name} />\n                    </fieldset>\n\n                    <fieldset style={props.site.unsaved ? 'display: none;' : ''}>\n                        <label>Add this code to your website    <small class=\"right\">(site ID = {props.site.trackingId})</small></label>\n                        <textarea ref={this.setTextarea} onFocus={this.handleTextareaClickEvent} readonly=\"readonly\">{`<!-- Fathom - simple website analytics - https://github.com/usefathom/fathom -->\n<script>\n(function(f, a, t, h, o, m){\n\ta[h]=a[h]||function(){\n\t\t(a[h].q=a[h].q||[]).push(arguments)\n\t};\n\to=f.createElement('script'),\n\tm=f.getElementsByTagName('script')[0];\n\to.async=1; o.src=t; o.id='fathom-script';\n\tm.parentNode.insertBefore(o,m)\n})(document, window, '//${location.host}/tracker.js', 'fathom');\nfathom('set', 'siteId', '${props.site.trackingId}');\nfathom('trackPageview');\n</script>\n<!-- / Fathom -->`}\n                    </textarea>\n                    <small><a href=\"javascript:void(0);\" onClick={this.copyToClipboard}>{state.copied ? \"Copied!\" : \"Copy code\"}</a></small>\n                </fieldset>\n\n                <fieldset>\n                    <div class=\"half\">\n                        <div class=\"submit\"><button type=\"submit\">{props.site.unsaved ? 'Create site' : 'Update site name'}</button> &nbsp; {state.updated ? 'Saved!' : ''}</div>\n                        {props.site.unsaved ? '' : (<div class=\"delete\"><a href=\"javascript:void(0);\" onClick={this.deleteSite}>Delete site</a></div>)}\n                    </div>\n                </fieldset>\n            </form>\n        </div>\n    </div>)\n    }\n}\n\nexport default SiteSettings\n"
  },
  {
    "path": "assets/src/js/components/SiteSwitcher.js",
    "content": "'use strict';\n\nimport { h, Component } from 'preact';\nimport { bind } from 'decko';\nimport { hashParams } from \"../lib/util\";\n\n\n\nfunction arrayToQueryString(array_in){\n    var out = new Array();\n\n    for(var key in array_in){\n        out.push(key + '=' + encodeURIComponent(array_in[key]));\n    }\n\n    return out.join('&');\n}\nclass SiteSwitcher extends Component {\n  constructor() {\n    super();\n    this.state = {\n      isExpanded: false\n    };\n  }\n\n  @bind \n  selectSite(evt) {\n    let itemId = evt.target.getAttribute(\"data-id\")  \n    this.props.sites.some((s) => {\n        if (s.id != itemId) {\n            return false;\n        }\n        let params = hashParams()\n        params[\"site\"] = s.id\n        window.history.replaceState(this.state, null, `#!${arrayToQueryString(params)}`)\n        this.props.onChange(s)\n        return true;\n    })   \n  }\n\n  @bind \n  addSite() {\n      this.props.onAdd({ id: 1, name: \"New site\", unsaved: true })\n  }\n\n  @bind\n  expand() {\n    this.setState({\n      isExpanded: true\n    });\n  }\n\n  @bind\n  collapse() {\n    this.setState({\n      isExpanded: false\n    });\n  }\n\n  @bind\n  toggleExpanded() {\n    this.setState({\n      isExpanded: !this.state.isExpanded\n    });\n  }\n\n  render(props, state) {\n    // show nothing if there is only 1 site and no option to add additional sites\n    if(!props.showAdd && props.sites.length == 1) {\n        return '';\n    }  \n\n    // otherwise, render list of sites + add button\n    return (\n        <li class={`sites ${state.isExpanded ? 'expanded' : ''}`} onClick={this.toggleExpanded} onMouseEnter={this.expand} onMouseLeave={this.collapse}>\n            <a href=\"javascript:void(0)\">{props.selectedSite.name}</a>\n            <ul>\n                {props.sites.map((s) => (<li class=\"site-switch\"><a href=\"javascript:void(0);\" data-id={s.id} onClick={this.selectSite}>{s.name}</a></li>)) }\n                {props.showAdd ? (<li class=\"add-new\"><a href=\"javascript:void(0);\" onClick={this.addSite}>+ Add another site</a></li>) : ''}\n            </ul>\n        </li>\n    )\n  }\n}\n\nexport default SiteSwitcher\n"
  },
  {
    "path": "assets/src/js/components/Table.js",
    "content": "'use strict';\n\nimport { h, Component } from 'preact';\nimport * as numbers from '../lib/numbers.js';\nimport Client from '../lib/client.js';\nimport { bind } from 'decko';\nimport classNames from 'classnames';\nimport { runInNewContext } from 'vm';\n\nconst dayInSeconds = 60 * 60 * 24;\n\nclass Table extends Component {\n\n  constructor(props) {\n    super(props)\n\n    this.state = {\n      records: [],\n      offset: 0,\n      limit: 15,\n      loading: true,\n      total: 0,\n    }\n  }\n\n  componentWillReceiveProps(newProps, newState) {\n    if(!this.paramsChanged(this.props, newProps)) {\n      return;\n    }\n\n    this.fetchData(newProps)\n  }\n\n  paramsChanged(o, n) {\n    return o.siteId !== n.siteId || o.dateRange[0] != n.dateRange[0] || o.dateRange[1] != n.dateRange[1];\n  }\n  \n  @bind\n  fetchData(props) {\n    this.setState({ loading: true });\n    let before = props.dateRange[1]/1000;\n    let after = props.dateRange[0]/1000;\n\n    Client.request(`/sites/${props.siteId}/stats/${props.endpoint}/agg?before=${before}&after=${after}&offset=${this.state.offset}&limit=${this.state.limit}`)\n      .then((d) => {\n         // request finished; check if timestamp range is still the one user wants to see\n        if( this.paramsChanged(props, this.props) ) {\n          return;\n        }\n\n        this.setState({\n          loading: false,\n          records: d,\n        });\n      });\n\n     // fetch totals too\n     Client.request(`/sites/${props.siteId}/stats/${props.endpoint}/agg/pageviews?before=${before}&after=${after}`)\n      .then((d) => {\n        this.setState({\n          total: d\n        });\n      });\n  }\n\n  @bind \n  paginateNext() {\n    this.setState({ offset: this.state.offset + this.state.limit })\n    this.fetchData(this.props)\n  }\n\n  @bind \n  paginatePrev() {\n    if(this.state.offset == 0) {\n      return;\n    }\n\n    this.setState({ offset: Math.max(0, this.state.offset - this.state.limit) })\n    this.fetchData(this.props)\n  }\n\n  render(props, state) {\n    const tableRows = state.records !== null && state.records.length > 0 ? state.records.map((p, i) => {\n      let href = (p.Hostname + p.Pathname) || p.URL;\n      let widthClass = \"\";\n      if(state.total > 0) {\n        widthClass = \"w\" + (\"\" + Math.min(98, Math.round(p.Pageviews / state.total * 100 * 2.5))).padStart(2, '0');\n      }\n\n      let label = p.Pathname\n      if( props.showHostname ) {\n        if( p.Group) {\n          label = p.Group\n        } else {\n          label = p.Hostname.replace('www.', '').replace('https://', '').replace('http://', '') + (p.Pathname.length > 1 ? p.Pathname : '')\n        }\n      }\n\n      return(\n      <div class={classNames(\"table-row\", widthClass)}>\n        <div class=\"cell main-col\"><a href={href}>{label}</a></div>\n        <div class=\"cell\">{numbers.formatPretty(p.Pageviews)}</div>\n        <div class=\"cell\">{numbers.formatPretty(p.Visitors)||\"-\"}</div>\n      </div>\n    )}) : <div class=\"table-row\"><div class=\"cell main-col\">Nothing here, yet.</div></div>;\n\n  // pagination row: only show when total # of results doesn't fit in one table page  \n  const pagination = tableRows.length == state.limit || state.offset >= state.limit ? (\n    <div class=\"row pag\">\n      <a href=\"javascript:void(0)\" onClick={this.paginatePrev} class=\"back\">‹</a>\n      <a href=\"javascript:void(0)\" onClick={this.paginateNext} class=\"next right\">›</a>\n    </div>) : '';\n\n    return (\n      <div class={classNames({ loading: state.loading })}>\n        <div class=\"table-row header\">\n          {props.headers.map((header, i) => {\n            return <div class={classNames(\"cell\", { \"main-col\": i === 0 })}>{header}</div>\n          })}\n        </div>\n        <div>\n          {tableRows}\n          {pagination}\n        </div>\n      </div>\n    )\n  }\n}\n\nexport default Table\n"
  },
  {
    "path": "assets/src/js/lib/client.js",
    "content": "'use strict';\n\nvar Client = {};\nClient.request = function(url, args) {\n  args = args || {};\n  args.credentials = 'same-origin'\n  args.headers = args.headers || {};\n  args.headers['Accept'] = 'application/json';\n\n  if( args.method && args.method === 'POST') {\n    args.headers['Content-Type'] = 'application/json';\n\n    if(args.data) {\n      if( typeof(args.data) !== \"string\") {\n        args.data = JSON.stringify(args.data)\n      }\n      args.body = args.data\n      delete args.data\n    }\n  }\n\n  // trim leading slash from URL\n  url = (url[0] === '/') ? url.substring(1) : url;\n\n  return window.fetch(`api/${url}`, args)\n    .then(handleRequestErrors)\n    .then(parseJSON)\n    .then(parseData)\n}\n\nfunction handleRequestErrors(r) {\n  // if response is not JSON (eg timeout), throw a generic error\n  if (! r.ok && r.headers.get(\"Content-Type\") !== \"application/json\") {\n    throw { code: \"request_error\", message: \"An error occurred\" }\n  }\n\n  return r\n}\n\nfunction parseJSON(r) {\n  return r.json()\n}\n\nfunction parseData(d) {\n\n  // if JSON response contains an Error property, use that as error code\n  // Message is generic here, so that individual components can set their own specific messages based on the error code\n  if(d.Error) {\n    throw { code: d.Error, message: \"An error occurred\" }\n  }\n\n  return d.Data\n}\n\nexport default Client\n"
  },
  {
    "path": "assets/src/js/lib/numbers.js",
    "content": "'use strict';\n\nconst M = 1000000\nconst K = 1000\nconst rx = new RegExp('\\\\.0$');\nconst commaRx = new RegExp('(\\\\d+)(\\\\d{3})');\n\nfunction formatPretty(num) {\n  let decimals = 0;\n\n  if (num >= M) {\n    num /= M\n    decimals = 3 - ((Math.round(num) + \"\").length) || 0;\n    return (num.toFixed(decimals > -1 ? decimals : 0).replace(rx, '') + 'M').replace('.00', '');\n  }\n\n  if (num >= (K * 10)) {\n    num /= K\n    decimals = 3 - ((Math.round(num) + \"\").length) || 0;\n    return num.toFixed(decimals).replace(rx, '') + 'K';\n  }\n\n  return formatWithComma(num);\n}\n\nfunction formatWithComma(nStr) {\n\tnStr += '';\n\n  if(nStr.length < 4 ) {\n    return nStr;\n  }\n\n  var\tx = nStr.split('.');\n\tvar x1 = x[0];\n\tvar x2 = x.length > 1 ? '.' + x[1] : '';\n\twhile (commaRx.test(x1)) {\n\t\tx1 = x1.replace(commaRx, '$1' + ',' + '$2');\n\t}\n\treturn x1 + x2;\n}\n\nfunction formatDuration(seconds) {\n  seconds = Math.round(seconds);\n  var date = new Date(null);\n  date.setSeconds(seconds); // specify value for SECONDS here\n  return date.toISOString().substr(14, 5);\n}\n\nfunction formatPercentage(p) {\n   return Math.round(p*100) + \"%\";\n}\n\nexport { \n  formatPretty,\n  formatWithComma, \n  formatDuration, \n  formatPercentage \n}\n"
  },
  {
    "path": "assets/src/js/lib/util.js",
    "content": "'use strict';\n\n// convert object to query string\nfunction stringifyObject(json) {\n    var keys = Object.keys(json);\n\n    return '?' +\n        keys.map(function (k) {\n            return encodeURIComponent(k) + '=' +\n                encodeURIComponent(json[k]);\n        }).join('&');\n}\n\nfunction randomString(n) {\n    var s = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\";\n    return Array(n).join().split(',').map(() => s.charAt(Math.floor(Math.random() * s.length))).join('');\n}\n\nfunction hashParams() {\n    var params = {},\n        match,\n        matches = window.location.hash.substring(2).split(\"&\");\n\n    for (var i = 0; i < matches.length; i++) {\n        match = matches[i].split('=')\n        params[match[0]] = decodeURIComponent(match[1]);\n    }\n\n    return params;\n}\n\nexport {\n    randomString,\n    stringifyObject,\n    hashParams\n}\n"
  },
  {
    "path": "assets/src/js/pages/dashboard.js",
    "content": "'use strict'\n\nimport { h, Component } from 'preact';\nimport LogoutButton from '../components/LogoutButton.js';\nimport Realtime from '../components/Realtime.js';\nimport DatePicker from '../components/DatePicker.js';\nimport Sidebar from '../components/Sidebar.js';\nimport SiteSwitcher from '../components/SiteSwitcher.js';\nimport SiteSettings from '../components/SiteSettings.js';\nimport Gearwheel from '../components/Gearwheel.js';\nimport Table from '../components/Table.js';\nimport Chart from '../components/Chart.js';\nimport { bind } from 'decko';\nimport Client from '../lib/client.js';\nimport classNames from 'classnames';\nimport {hashParams} from \"../lib/util\";\n\n\nlet defaultSite = {}\nclass Dashboard extends Component {\n  constructor(props) {\n    super(props)\n    let params = hashParams()\n    defaultSite = {\n      id: params.site || window.localStorage.getItem('site_id') || 1,\n      name: \"\",\n      unsaved: true,\n    };\n    this.state = {\n      dateRange: [],\n      groupBy: 'day',\n      isPublic: document.cookie.indexOf('auth') < 0,\n      site: defaultSite,\n      sites: [],\n      settingsOpen: false,\n      addingNewSite: false,\n    }\n  }\n\n  componentDidMount() {\n    this.fetchSites()\n  }\n\n  @bind\n  fetchSites() {\n    Client.request(`sites`)\n    .then((sites) => {\n      // open site settings when there are no sites yet\n      if(sites.length == 0) {\n        this.showSiteSettings({ id: 1, name: \"yoursite.com\", unsaved: true })\n        return;\n      }\n\n      // if there are sites, use remembered site as selected site\n      let site = sites[0];\n      let s = sites.find(s => s.id == defaultSite.id);\n      site = s ? s : site;\n\n      this.setState({\n        sites: sites,\n        site: site,\n      })\n    }).catch((e) => {\n      if(e.code === 'unauthorized') {\n        this.props.onLogout()\n      }\n    })\n  }\n\n  @bind\n  changeDateRange(s) {\n    this.setState({\n      dateRange: [ s.startDate, s.endDate ],\n      period: s.period,\n      groupBy: s.groupBy,\n    })\n  }\n\n  @bind\n  showSiteSettings(site) {\n    site = site && site.unsaved ? site : this.state.site;\n    this.setState({\n      settingsOpen: true,\n      site: site,\n      previousSite: this.state.site,\n    })\n  }\n\n  @bind\n  closeSiteSettings() {\n    this.setState({\n      settingsOpen: false,\n\n      // switch back to previous site if we were showing site settings to add a new site\n      site: this.state.site.unsaved && this.state.previousSite ? this.state.previousSite : this.state.site,\n    })\n  }\n\n  @bind\n  changeSelectedSite(site) {\n    let newState = {\n      site: site,\n    }\n\n    if(!this.state.site.unsaved) {\n      newState.previousSite = this.state.site\n    }\n\n    this.setState(newState)\n    window.localStorage.setItem('site_id', site.id)\n  }\n\n  @bind\n  updateSite(site) {\n    let updated = false;\n    let newSites = this.state.sites.map((s) => {\n      if(s.id != site.id) {\n        return s;\n      }\n\n      updated = true;\n\n      // replace site in sites array with parameter\n      return site;\n    })\n\n    if(!updated) {\n      newSites.push(site);\n    }\n\n    this.setState({sites: newSites, site: site})\n  }\n\n  @bind\n  deleteSite(site) {\n    let newSites = this.state.sites.filter((s) => (s.id != site.id))\n    let newSelectedSite = newSites.length > 0 ? newSites[0] : defaultSite;\n    this.setState({\n      sites: newSites,\n      site: newSelectedSite\n    })\n  }\n\n  render(props, state) {\n    // only show logout link if this dashboard is not public\n    let logoutMenuItem = state.isPublic ? '' : (\n      <li class=\"signout\"><span class=\"spacer\">&middot;</span><LogoutButton onSuccess={props.onLogout} /></li>\n    );\n\n    return (\n  <div class=\"app-page \">\n     <div class={`rapper animated fadeInUp delayed_02s ${state.period} ` + classNames({ ltday: state.dateRange[1] - state.dateRange[0] < 86400 })}>\n\n      <header class=\"section\">\n        <nav class=\"main-nav\">\n            <ul>\n              <li class=\"logo\"><a href=\"/\">{state.site.name || \"Fathom\"}</a></li>\n              <SiteSwitcher sites={state.sites} selectedSite={state.site} onChange={this.changeSelectedSite} onAdd={this.showSiteSettings} showAdd={!state.isPublic}/>\n              <Gearwheel onClick={this.showSiteSettings} visible={!state.isPublic} />\n              <li class=\"visitors\"><Realtime siteId={state.site.id} /></li>\n          </ul>\n        </nav>\n      </header>\n\n      <DatePicker onChange={this.changeDateRange} />\n\n      <section class=\"section\">\n        <div class=\"boxes\">\n          <Sidebar siteId={state.site.id} dateRange={state.dateRange} />\n\n          <div class=\"box box-graph\">\n            <Chart siteId={state.site.id} dateRange={state.dateRange} tickStep={state.groupBy} />\n          </div>\n          <div class=\"box box-pages\">\n            <Table endpoint=\"pages\" headers={[\"Top pages\", \"Views\", \"Uniques\"]} siteId={state.site.id} dateRange={state.dateRange} />\n          </div>\n          <div class=\"box box-referrers\">\n            <Table endpoint=\"referrers\" headers={[\"Top referrers\", \"Views\", \"Uniques\"]} siteId={state.site.id} dateRange={state.dateRange} showHostname=\"true\" />\n          </div>\n        </div>\n        <div class=\"notice\">\n          <div class=\"notice-text\"><strong>Want more features and less maintenance?</strong> Check out the current version of Fathom Analytics and <a href=\"https://usefathom.com/ref/GITHUB\">start your free trial today</a>.</div>\n        </div>\n        <footer class=\"section\">\n          <div class=\"half\">\n          <nav>\n            <ul>\n              <li><a href=\"https://usefathom.com/\">Fathom</a></li>\n              <li><a href=\"https://usefathom.com/terms/\">Terms of use</a></li>\n              <li><a href=\"https://usefathom.com/privacy/\">Privacy policy</a></li>\n              <li><a href=\"https://usefathom.com/data/\">Our data policy</a></li>\n              <li><LogoutButton onSuccess={props.onLogout} /></li>\n            </ul>\n          </nav>\n          <div class=\"hide-on-mobile\">Use <strong>the arrow keys</strong> to cycle through date ranges.</div>\n          </div>\n        </footer>\n      </section>\n    </div>\n    <SiteSettings visible={state.settingsOpen} onClose={this.closeSiteSettings} onUpdate={this.updateSite} onDelete={this.deleteSite} site={state.site} />\n  </div>\n  )}\n}\n\nexport default Dashboard\n"
  },
  {
    "path": "assets/src/js/pages/login.js",
    "content": "'use strict';\n\nimport { h, render, Component } from 'preact';\nimport LoginForm from '../components/LoginForm.js';\n\nclass Login extends Component {\n  render(props, state) {\n    return (\n      <div class=\"flex-rapper login-page animated fadeInUp delayed_02s\">\n        <div class=\"login-rapper\">\n          <LoginForm onSuccess={props.onLogin} />\n          <small><a href=\"https://usefathom.com\">Fathom Analytics</a>{/* &middot; <a href=\"#lost\">Password reset</a> */}</small>\n        </div>\n      </div>\n    )\n  }\n}\n\nexport default Login\n"
  },
  {
    "path": "assets/src/js/script.js",
    "content": "'use strict';\n\nimport { h, render, Component } from 'preact'\nimport Login from './pages/login.js'\nimport Dashboard from './pages/dashboard.js'\nimport { bind } from 'decko';\nimport Client from './lib/client.js';\n\nclass App extends Component {\n  constructor(props) {\n    super(props)\n\n    this.state = {\n      authenticated: document.cookie.indexOf('auth') > -1\n    }\n\n    this.fetchAuthStatus()\n  }\n\n  @bind\n  fetchAuthStatus() {\n    Client.request(`session`)\n      .then((d) => { \n        this.setState({ authenticated: d })\n      })\n  }\n\n  @bind\n  toggleAuth() {\n    this.setState({ \n      authenticated: !this.state.authenticated \n    })\n  }\n\n  render(props, state) {\n    // logged-in\n    if( state.authenticated ) {\n      return <Dashboard onLogout={this.toggleAuth} />\n    }\n\n    // logged-out\n    return <Login onLogin={this.toggleAuth} />\n  }\n}\n\nrender(<App />, document.getElementById('root'));\n"
  },
  {
    "path": "assets/src/js/tracker.js",
    "content": "(function() { \n  'use strict';\n\n  let queue = window.fathom.q || [];\n  let config = {\n    'siteId': '',\n    'trackerUrl': '',\n  };\n  const commands = {\n    \"set\": set,\n    \"trackPageview\": trackPageview,\n    \"setTrackerUrl\": setTrackerUrl,\n  };\n\n  function set(key, value) {\n    config[key] = value;\n  }\n\n  function setTrackerUrl(value) {\n    return set(\"trackerUrl\", value);\n  }\n\n  // convert object to query string\n  function stringifyObject(obj) {\n    var keys = Object.keys(obj);\n\n    return '?' +\n        keys.map(function(k) {\n            return encodeURIComponent(k) + '=' + encodeURIComponent(obj[k]);\n        }).join('&');\n  }\n\n  function getCookie(name) {\n    var cookies = document.cookie ? document.cookie.split('; ') : [];\n    \n    for (var i = 0; i < cookies.length; i++) {\n      var parts = cookies[i].split('=');\n      if (decodeURIComponent(parts[0]) !== name) {\n        continue;\n      }\n\n      var cookie = parts.slice(1).join('=');\n      return decodeURIComponent(cookie);\n    }\n\n    return '';\n  }\n\n  function setCookie(name, data, args) {\n    name = encodeURIComponent(name);\n    data = encodeURIComponent(String(data));\n\n    var str = name + '=' + data;\n\n    if(args.path) {\n      str += ';path=' + args.path;\n    }\n    if (args.expires) {\n      str += ';expires='+args.expires.toUTCString();\n    }\n\n    document.cookie = str + ';SameSite=None;Secure';\n  }\n\n  function newVisitorData() {\n    return {\n      isNewVisitor: true, \n      isNewSession: true,\n      pagesViewed: [],\n      previousPageviewId: '',\n      lastSeen: +new Date(),\n    }\n  }\n\n  function getData() {\n    let thirtyMinsAgo = new Date();\n    thirtyMinsAgo.setMinutes(thirtyMinsAgo.getMinutes() - 30);\n\n    let data = getCookie('_fathom');\n    if(! data) {\n      return newVisitorData();\n    }\n\n    try{\n      data = JSON.parse(data);\n    } catch(e) {\n      console.error(e);\n      return newVisitorData();\n    }\n\n    if(data.lastSeen < (+thirtyMinsAgo)) {\n      data.isNewSession = true;\n    }\n\n    return data;  \n  }\n\n  function findTrackerUrl() {\n    const el = document.getElementById('fathom-script')\n    return el ? el.src.replace('tracker.js', 'collect') : '';\n  }\n\n  function trackPageview(vars) { \n    vars = vars || {};\n\n    // Respect \"Do Not Track\" requests\n    if('doNotTrack' in navigator && navigator.doNotTrack === \"1\") {\n      return;\n    }\n\n    // ignore prerendered pages\n    if( 'visibilityState' in document && document.visibilityState === 'prerender' ) {\n      return;\n    }\n\n    // if <body> did not load yet, try again at dom ready event\n    if( document.body === null ) {\n      document.addEventListener(\"DOMContentLoaded\", () => {\n        trackPageview(vars);\n      })\n      return;\n    }\n\n    //  parse request, use canonical if there is one\n    let req = window.location;\n\n    // do not track if not served over HTTP or HTTPS (eg from local filesystem) and we're not in an Electron app\n    if(req.host === '' && navigator.userAgent.indexOf(\"Electron\") < 0) {\n      return;\n    }\n\n    // find canonical URL\n    let canonical = document.querySelector('link[rel=\"canonical\"][href]');\n    if(canonical) {\n      let a = document.createElement('a');\n      a.href = canonical.href;\n\n      // use parsed canonical as location object\n      req = a;\n    }\n    \n    let path = vars.path || ( req.pathname + req.search );\n    if(!path) {\n      path = '/';\n    }\n\n    // determine hostname\n    let hostname = vars.hostname || ( req.protocol + \"//\" + req.hostname );\n\n    // only set referrer if not internal\n    let referrer = vars.referrer || '';\n    if(document.referrer.indexOf(hostname) < 0) {\n      referrer = document.referrer;\n    }\n\n    let data = getData();\n    const d = {\n      pid: data.previousPageviewId || '',\n      p: path,\n      h: hostname,\n      r: referrer,\n      u: data.pagesViewed.indexOf(path) == -1 ? 1 : 0,\n      nv: data.isNewVisitor ? 1 : 0, \n      ns: data.isNewSession ? 1 : 0,\n      sid: config.siteId,\n    };\n\n    let url = config.trackerUrl || findTrackerUrl()\n    let img = document.createElement('img');\n    img.setAttribute('alt', '');\n    img.setAttribute('aria-hidden', 'true');\n    img.setAttribute('style', 'position:absolute');\n    img.src = url + stringifyObject(d);\n    img.addEventListener('load', function() {\n      let midnight = new Date();\n      midnight.setHours(24); midnight.setMinutes(0); midnight.setSeconds(0);\n\n      // update data in cookie\n      if( data.pagesViewed.indexOf(path) == -1 ) {\n        data.pagesViewed.push(path);\n      }\n      data.previousPageviewId = d.id;\n      data.isNewVisitor = false;\n      data.isNewSession = false;\n      data.lastSeen = +new Date();\n      setCookie('_fathom', JSON.stringify(data), { expires: midnight, path: '/' });\n\n      // remove tracking img from DOM\n      document.body.removeChild(img)\n    });\n    \n    // in case img.onload never fires, remove img after 1s & reset src attribute to cancel request\n    window.setTimeout(() => { \n      if(!img.parentNode) {\n        return;\n      }\n\n      img.src = ''; \n      document.body.removeChild(img)\n    }, 1000);\n\n    // add to DOM to fire request\n    document.body.appendChild(img);  \n  }\n\n  // override global fathom object\n  window.fathom = function() {\n    var args = [].slice.call(arguments);\n    var c = args.shift();\n    commands[c].apply(this, args);\n  };\n\n  // process existing queue\n  queue.forEach((i) => fathom.apply(this, i));\n})()\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: \"3\"\nservices:\n  fathom:\n    image: usefathom/fathom:latest\n    ports:\n      - \"8080:8080\"\n    environment:\n      - \"FATHOM_SERVER_ADDR=:8080\"\n      - \"FATHOM_GZIP=true\"\n      - \"FATHOM_DEBUG=false\"\n      - \"FATHOM_DATABASE_DRIVER=mysql\"\n      - \"FATHOM_DATABASE_NAME=fathom\"\n      - \"FATHOM_DATABASE_USER=fathom\"\n      - \"FATHOM_DATABASE_PASSWORD=password01\"\n      - \"FATHOM_DATABASE_HOST=mysql:3306\"\n      - \"FATHOM_SECRET=TWEn6GXQDx45PZfmJWvyGpXf5M8b94bszgw8JcJWEd6WxgrnUkLatS34GwjPTvZb\"\n    links:\n      - \"mysql:mysql\"\n    depends_on:\n      - mysql\n    restart: always\n  mysql:\n    image: \"mysql:5\"\n    volumes:\n      - ./mysql-data:/var/lib/mysql\n    ports:\n      - \"127.0.0.1:3306:3306\"\n    environment:\n      - \"MYSQL_ALLOW_EMPTY_PASSWORD=false\"\n      - \"MYSQL_DATABASE=fathom\"\n      - \"MYSQL_PASSWORD=password01\"\n      - \"MYSQL_ROOT_PASSWORD=password01\"\n      - \"MYSQL_USER=fathom\"\n    restart: always\n"
  },
  {
    "path": "docs/Configuration.md",
    "content": "# Configuring Fathom\n\nAll configuration in Fathom is optional. If you supply no configuration values then Fathom will default to using a SQLite database in the current working directory.\n\nIf you're already running MySQL or PostgreSQL on the server you're installing Fathom on, you'll most likely want to use one of those as your database driver.\n\nTo do so, either create a `.env` file in the working directory of your Fathom application or point Fathom to your configuration file by specifying the `--config` flag when starting Fathom.\n\n`\nfathom --config=/home/john/fathom.env server\n`\n\nThe default configuration looks like this:\n\n```\nFATHOM_GZIP=true\nFATHOM_DEBUG=true\nFATHOM_DATABASE_DRIVER=\"sqlite3\"\nFATHOM_DATABASE_NAME=\"./fathom.db\"\nFATHOM_DATABASE_USER=\"\"\nFATHOM_DATABASE_PASSWORD=\"\"\nFATHOM_DATABASE_HOST=\"\"\nFATHOM_DATABASE_SSLMODE=\"\"\nFATHOM_SECRET=\"random-secret-string\"\n```\n\n### Accepted values & defaults\n\n| Name | Default | Description\n| :---- | :---| :---\n| FATHOM_DEBUG | `false` | If `true` will write more log messages.\n| FATHOM_SERVER_ADDR | `:8080` | The server address to listen on\n| FATHOM_GZIP | `false` | if `true` will HTTP content gzipped\n| FATHOM_DATABASE_DRIVER | `sqlite3` | The database driver to use: `mysql`, `postgres` or `sqlite3`\n| FATHOM_DATABASE_NAME |  | The name of the database to connect to (or path to database file if using sqlite3)\n| FATHOM_DATABASE_USER |  | Database connection user\n| FATHOM_DATABASE_PASSWORD | | Database connection password\n| FATHOM_DATABASE_HOST |  | Database connection host\n| FATHOM_DATABASE_SSLMODE | | For a list of valid values, look [here for Postgres](https://www.postgresql.org/docs/9.1/static/libpq-ssl.html#LIBPQ-SSL-PROTECTION) and [here for MySQL](https://github.com/Go-SQL-Driver/MySQL/#tls)\n| FATHOM_DATABASE_URL | | Can be used to specify the connection string for your database, as an alternative to the previous 5 settings. \n| FATHOM_SECRET |  | Random string, used for signing session cookies\n\n### Common issues\n\n##### Fathom panics when trying to connect to Postgres: `pq: SSL is not enabled on the server`\n\nThis usually means that you're running Postgres without SSL enabled. Set the `FATHOM_DATABASE_SSLMODE` config option to remedy this.\n\n```\nFATHOM_DATABASE_SSLMODE=disable\n```\n\n##### Using `FATHOM_DATABASE_URL`\n\nWhen using `FATHOM_DATABASE_URL` to manually specify your database connection string, there are a few important things to consider.\n\n- When using MySQL, include `?parseTime=true&loc=Local` in your DSN.\n- When using SQLite, include `?_loc=auto` in your DSN.\n\nExamples of valid values:\n\n```\nFATHOM_DATABASE_DRIVER=mysql\nFATHOM_DATABASE_URL=root:@tcp/fathom1?loc=Local&parseTime=true\n```\n"
  },
  {
    "path": "docs/FAQ.md",
    "content": "# Frequently Asked Questions\n\n### How do I install Fathom on my server?\n\nHave a look at the [installation instructions](Installation%20instructions.md).\n\n---\n\n### How do I upgrade Fathom to the latest version?\n\nBy overwriting the fathom binary with the new version. Make sure to restart any running processes for the changes to take effect. More detailed instructions can be found here: [upgrading Fathom](Updating%20to%20the%20latest%20version.md).\n\n---\n\n### What databases can I use with Fathom?\n\nYou can use Fathom with either Postgres, MySQL or SQLite. \n\n\n---\n\n### How to configure Fathom?\n\nCreate a file named `.env` in the working directory of your Fathom process. You can [find a list of accepted configuration values here](Configuration.md).\n\n---\n\n### How to start tracking pageviews?\n\nAdd the tracking snippet to all pages on your site that you want to keep track of. Get your tracking snippet by clicking the gearwheel icon in your Fathom dashboard.\n\n---\n\n### What data does Fathom track?\n\nFathom tracks no personally identifiable information on your visitors. \n\nWhen Fathom tracks a pageview, your visitor is assigned a random string which is used to determine whether it's a unique pageview. If your visitor visits another page on your site, the previous pageview is processed & deleted within 1 minute. If the visitor leaves your site, the pageview is processed & deleted when the session ends (in 30 minutes).\n\nIf \"Do Not Track\" is enabled in the browser settings, Fathom respects that.\n\n---\n\n### Fathom is not tracking my pageviews\n\nIf you have the tracking snippet in place and Fathom is still not tracking you, most likely you have `navigator.doNotTrack` enabled. Fathom is respecting your browser's \"Do Not Track\" setting right now.\n"
  },
  {
    "path": "docs/Installation instructions.md",
    "content": "# Installation instructions for Fathom\n\nTo install Fathom on your server: \n\n1. [Download the latest Fathom release](https://github.com/usefathom/fathom/releases) suitable for your platform.\n2. Extract the archive to `/usr/local/bin`\n\n```sh\ntar -C /usr/local/bin -xzf fathom_$VERSION_$OS_$ARCH.tar.gz\nchmod +x /usr/local/bin/fathom\n```\n\nConfirm that Fathom is installed properly by running `fathom --version`\n\n```sh\n$ fathom --version\nFathom version 1.0.0\n```\n\n## Configuring Fathom\n\n> This step is optional. By default, Fathom will use a SQLite database file in the current working directory.\n\nTo run the Fathom web server we will need to [configure Fathom](Configuration.md) so that it can connect with your database of choice. \n\nLet's create a new directory where we can store our configuration file & SQLite database.\n\n```\nmkdir ~/my-fathom-site\ncd ~/my-fathom-site\n```\n\nThen, create a file named `.env` with the following contents.\n\n```\nFATHOM_SERVER_ADDR=9000\nFATHOM_GZIP=true\nFATHOM_DEBUG=true\nFATHOM_DATABASE_DRIVER=\"sqlite3\"\nFATHOM_DATABASE_NAME=\"fathom.db\"\nFATHOM_SECRET=\"random-secret-string\"\n```\n\nIf you now run `fathom server` then Fathom will start serving up a website on port 9000 using a SQLite database file named `fathom.db`. If that port is exposed then you should now see your Fathom instance running by browsing to `http://server-ip-address-here:9000`.\n\nCheck out the [configuration file documentation](Configuration.md) for all possible configuration values, eg if you want to use MySQL or Postgres instead.\n\n## Register your admin user\n\n> This step is required.\n\nTo register a user in the Fathom instance we just created, run the following command from the directory where your `.env` file is. \n\n```\nfathom user add --email=\"john@email.com\" --password=\"strong-password\"\n```\n\n**Note:** if you're running Fathom v1.0.1 or older, the command is `fathom register --email=\"john@email.com\" --password=\"strong-password\"`\n\n## Using NGINX with Fathom\n\nWe recommend using NGINX with Fathom, as it simplifies running multiple sites from the same server and handling SSL certificates with LetsEncrypt.\n\nCreate a new file in `/etc/nginx/sites-enabled/my-fathom-site` with the following contents. Replace `my-fathom-site.com` with the domain you would like to use for accessing your Fathom installation.\n\n```sh\nserver {\n\tserver_name my-fathom-site.com;\n\n\tlocation / {\n\t\tproxy_set_header X-Real-IP $remote_addr;\n\t\tproxy_set_header X-Forwarded-For $remote_addr;\n\t\tproxy_set_header Host $host;\n\t\tproxy_pass http://127.0.0.1:9000; \n\t}\n}\n```\n\nTest your NGINX configuration and reload NGINX.\n\n```\nnginx -t\nservice nginx reload\n```\n\nIf you now run `fathom server` again, you should be able to access your Fathom installation by browsing to `http://my-fathom-site.com`.\n\n## Automatically starting Fathom on boot\n\nTo ensure the Fathom web server keeps running whenever the system reboots, we should use a process manager. Ubuntu 16.04 and later ship with Systemd.\n\nCreate a new file called `/etc/systemd/system/my-fathom-site.service` with the following contents. Replace `$USER` with your actual username.\n\n```\n[Unit]\nDescription=Starts the fathom server\nRequires=network.target\nAfter=network.target\n\n[Service]\nType=simple\nUser=$USER\nRestart=always\nRestartSec=3\nWorkingDirectory=/home/$USER/my-fathom-site\nExecStart=/usr/local/bin/fathom server\n\n[Install]\nWantedBy=multi-user.target\n```\n\nReload the Systemd configuration & enable our service so that Fathom is automatically started whenever the system boots.\n\n```\nsystemctl daemon-reload\nsystemctl enable my-fathom-site\n```\n\nYou should now be able to manually start your Fathom web server by issuing the following command.\n\n```\nsystemctl start my-fathom-site\n```\n\n## Tracking snippet\n\nTo start tracking pageviews, copy the tracking snippet shown in your Fathom dashboard to all pages of the website you want to track.\n\n\n### SSL certificate\n\nWith [Certbot](https://certbot.eff.org/docs/) for LetsEncrypt installed, adding an SSL certificate to your Fathom installation is as easy as running the following command.\n\n```\ncertbot --nginx -d my-fathom-site.com\n```\n\n\n"
  },
  {
    "path": "docs/README.md",
    "content": "Welcome to the fathom wiki!\n\nHow to:\n\n* [Install Fathom with our One-Click DigitalOcean installer](DigitalOcean%20One-Click%20Installation%20Instructions.md)\n* [Installing and running Fathom](Installation%20instructions.md)\n* [Upgrading Fathom to the latest version](Updating%20to%20the%20latest%20version.md)\n* [Configuration](Configuration.md)\n* [Frequently asked questions](FAQ.md)\n\nMisc:\n\n* [Using Fathom with Systemd](misc/Systemd.md)\n* [Running Fathom with NGINX](misc/NGINX.md)\n* [Running Fathom on Heroku](misc/Heroku.md)\n"
  },
  {
    "path": "docs/Updating to the latest version.md",
    "content": "# Updating Fathom to the latest version\n\nTo update your existing Fathom installation to the latest version, first rename your existing Fathom installation so that we can move the new version in its place.\n\n```\nmv /usr/local/bin/fathom /usr/local/bin/fathom-old\n```\n\nThen, [download the latest release archive suitable for your system architecture from the releases page](https://github.com/usefathom/fathom/releases/latest) and place it in `/usr/local/bin`.\n\n```\ntar -C /usr/local/bin -xzf fathom_$VERSION_$OS_$ARCH.tar.gz\nchmod +x /usr/local/bin/fathom\n``` \n\nIf you now run `fathom --version`, you should see that your system is running the latest version. \n\n```\n$ fathom --version\nFathom version 1.0.0\n```\n\n\n### Restarting your Fathom web server\n\nTo start serving up the updated Fathom web application, you will have to restart the Fathom process that is running the web server.\n\nIf you've followed the [installation instructions](Installation%20instructions.md) then you are using Systemd to manage the Fathom process. Run `systemctl restart <your-fathom-service>` to restart it.\n\n```\nsystemctl restart my-fathom-site\n```\n\nAlternatively, kill all running Fathom process by issuing the following command.\n\n```\npkill fathom\n```\n"
  },
  {
    "path": "docs/misc/Heroku.md",
    "content": "# Running Fathom on Heroku\n\n### Requirements\n\n* heroku cli (logged in)\n* git\n* curl\n* wget\n* tar are required\n* ~ openssl is required to generate the secret_key, but you're free to use what you want\n\n### Create the app\n\nFirst you need to choose a unique app name, as Heroku generates a subdomain for your app.\n\n* create the app via the buildpack\n\n```bash\nheroku create UNIQUE_APP_NAME --buildpack https://github.com/ph3nx/heroku-binary-buildpack.git\n```\n\n* locally clone the newly created app\n\n```bash\nheroku git:clone -a UNIQUE_APP_NAME\ncd UNIQUE_APP_NAME\n```\n\n* create the folder that will contain fathom\n\n```bash\nmkdir -p bin\n```\n\n* download latest version of fathom for linux 64bit\n\n```bash\ncurl -s https://api.github.com/repos/usefathom/fathom/releases/latest \\\n  | grep browser_download_url \\\n  | grep linux_amd64.tar.gz \\\n  | cut -d '\"' -f 4 \\\n  | wget -qi - -O- \\\n  | tar --directory bin -xz - fathom\n```\n\n* create the Procfile for Heroku\n\n```bash\necho \"web: bin/fathom server\" > Procfile\n```\n\n* create a Postgres database (you can change the type of plan if you want - https://elements.heroku.com/addons/heroku-postgresql#pricing)\n\n```bash\nheroku addons:create heroku-postgresql:hobby-dev\n```\n\n* update the environment variables, generate a secret_key\n\nhere you can change the way you generate your secret_key.\n\n```bash\nheroku config:set PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/app/bin \\\n  FATHOM_DATABASE_DRIVER=postgres \\\n  FATHOM_DATABASE_URL=$(heroku config:get DATABASE_URL) \\\n  FATHOM_DEBUG=true \\\n  FATHOM_SECRET=$(openssl rand -base64 32) \\\n  FATHOM_GZIP=true\n```\n\n* add, commit and push all our files\n\n```bash\ngit add --all\ngit commit -m \"First Commit\"\ngit push heroku master\n```\n\n* the created app runs as a free-tier. A free-tier dyno uses the account-based pool\nof free dyno hours. If you have other free dynos running, you will need to upgrade your app to a 'hobby' one. - https://www.heroku.com/pricing\n\n```bash\nheroku dyno:resize hobby\n```\n\n* check that everything is working\n\n```bash\nheroku run fathom --version\n```\n\n* add the first user\n\n```bash\nheroku run fathom user add --email=\"test@test.com\" --password=\"test_password\"\n```\n\n* open the browser to login and add your first website\n\n```bash\nheroku open\n```\n\n* ENJOY :)\n"
  },
  {
    "path": "docs/misc/NGINX.md",
    "content": "# Using NGINX with Fathom\n\nLet's say you have the Fathom server listening on port 9000 and want to serve it on your domain, `yourfathom.com`.\n\nWe can use NGINX to redirect all traffic for a certain domain to our Fathom application by using the `proxy_pass` directive combined with the port Fathom is listening on. \n\nCreate the following file in `/etc/nginx/sites-enabled/yourfathom.com`\n\n```\nserver {\n\tserver_name yourfathom.com;\n\n\tlocation / {\n\t\tproxy_set_header X-Real-IP $remote_addr;\n\t\tproxy_set_header X-Forwarded-For $remote_addr;\n\t\tproxy_set_header Host $host;\n\t\tproxy_pass http://127.0.0.1:9000; \n\t}\n}\n```\n\nIf you wish to protect your site using a [Let's Encrypt](https://letsencrypt.org/) HTTPS certificate, you can do so using the [Certbot webroot plugin](https://certbot.eff.org/docs/using.html#webroot). \n\n```\ncertbot certonly --webroot --webroot-path /var/www/yourfathom.com -d yourfathom.com\n```\n\nYour `/etc/nginx/sites-enabled/yourfathom.com` file should be updated accordingly:\n\n```\nserver {\n\tlisten 443 ssl http2;\n\tlisten [::]:443 ssl http2;\n\n\tserver_name yourfathom.com;\n\n\tssl_certificate /path/to/your/fullchain.pem;\n\tssl_certificate_key /path/to/your/privkey.pem;\n\n\tlocation /.well-known {\n\t\talias /var/www/yourfathom.com/.well-known;\n\t}\n\n\tlocation / {\n\t\tproxy_set_header X-Real-IP $remote_addr;\n\t\tproxy_set_header X-Forwarded-For $remote_addr;\n\t\tproxy_set_header Host $host;\n\t\tproxy_pass http://127.0.0.1:9000; \n\t}\n}\n```\n\nThe `alias` directive should point to the location where your `--webroot-path` is specified when generating the certificate (with `/.well-known` appended).\n\n### Test NGINX configuration\n```\nsudo nginx -t\n```\n\n### Reload NGINX configuration\n\n```\nsudo service nginx reload\n```\n"
  },
  {
    "path": "docs/misc/Systemd.md",
    "content": "# Managing the Fathom process with Systemd\n\nTo run Fathom as a service (so it keeps on running in the background and is automatically restarted in case of a server reboot) on Ubuntu 16.04 or later, first ensure you have the `fathom` binary installed and in your `$PATH` so that the command exists.\n\nThen, create a new service config file in the `/etc/systemd/system/` directory.\n\nExample file: `/etc/systemd/system/fathom.service`\n\nThe file should have the following contents, with `$USER` substituted with your actual username.\n\n```\n[Unit]\nDescription=Starts the fathom server\nRequires=network.target\nAfter=network.target\n\n[Service]\nType=simple\nUser=$USER\nRestart=always\nRestartSec=6\nWorkingDirectory=/etc/fathom # (or where fathom should store its files)\nExecStart=fathom server\n\n[Install]\nWantedBy=multi-user.target\n```\n\nSave the file and run `sudo systemctl daemon-reload` to load the changes from disk. \n\nThen, run `sudo systemctl enable fathom` to start the service whenever the system boots.\n\n### Starting or stopping the Fathom service manually\n```\nsudo systemctl start fathom\nsudo systemctl stop fathom\n```\n\n### Using a custom configuration file\n\nIf you want to [modify the configuration values for your Fathom service](../Configuration.md), then change the line starting with `ExecStart=...` to include the path to your configuration file.\n\nFor example, if you have a configuration file `/home/john/fathom.env` then the line should look like this:\n\n```\nExecStart=fathom --config=/home/john/fathom.env server --addr=:9000\n```\n\n#### Start Fathom automatically at boot\n```\nsudo systemctl enable fathom\n```\n\n#### Stop Fathom from starting at boot\n\n```\nsudo systemctl disable fathom\n```\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/usefathom/fathom\n\ngo 1.19\n\nrequire (\n\tgithub.com/go-sql-driver/mysql v1.6.0\n\tgithub.com/gobuffalo/packr/v2 v2.8.3\n\tgithub.com/google/uuid v1.3.0\n\tgithub.com/gorilla/context v1.1.1\n\tgithub.com/gorilla/handlers v1.5.1\n\tgithub.com/gorilla/mux v1.8.0\n\tgithub.com/gorilla/sessions v1.2.1\n\tgithub.com/jmoiron/sqlx v1.3.5\n\tgithub.com/joho/godotenv v1.4.0\n\tgithub.com/kelseyhightower/envconfig v1.4.0\n\tgithub.com/lib/pq v1.10.7\n\tgithub.com/mattn/go-sqlite3 v1.14.16\n\tgithub.com/mssola/user_agent v0.5.3\n\tgithub.com/rubenv/sql-migrate v1.2.0\n\tgithub.com/sirupsen/logrus v1.9.0\n\tgithub.com/urfave/cli v1.22.10\n\tgolang.org/x/crypto v0.3.0\n)\n\nrequire (\n\tgithub.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.1 // indirect\n\tgithub.com/go-gorp/gorp/v3 v3.0.2 // indirect\n\tgithub.com/gobuffalo/logger v1.0.6 // indirect\n\tgithub.com/gobuffalo/packd v1.0.1 // indirect\n\tgithub.com/gorilla/securecookie v1.1.2-0.20180608144417-78f3d318a8bf // indirect\n\tgithub.com/karrick/godirwalk v1.16.1 // indirect\n\tgithub.com/markbates/errx v1.1.0 // indirect\n\tgithub.com/markbates/oncer v1.0.0 // indirect\n\tgithub.com/markbates/safe v1.0.1 // indirect\n\tgithub.com/russross/blackfriday/v2 v2.0.1 // indirect\n\tgithub.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect\n\tgithub.com/ziutek/mymysql v1.5.5-0.20171217234033-ff6cc86d3d93 // indirect\n\tgolang.org/x/net v0.2.0 // indirect\n\tgolang.org/x/sys v0.2.0 // indirect\n\tgolang.org/x/term v0.2.0 // indirect\n\tgolang.org/x/text v0.4.0 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=\ncloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=\ncloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=\ncloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=\ncloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=\ncloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=\ncloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=\ncloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=\ncloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=\ncloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=\ncloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=\ncloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=\ncloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=\ncloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=\ncloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=\ncloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=\ncloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=\ncloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=\ncloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=\ncloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=\ncloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=\ncloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=\ncloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=\ncloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=\ncloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=\ncloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=\ncloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=\ncloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=\ncloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=\ncloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=\ncloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=\ncloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=\ncloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=\ncloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=\ncloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=\ncloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=\ndmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=\ngithub.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=\ngithub.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=\ngithub.com/Masterminds/sprig/v3 v3.2.0/go.mod h1:tWhwTbUTndesPNeF0C900vKoq283u6zp4APT9vaF3SI=\ngithub.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=\ngithub.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=\ngithub.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=\ngithub.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=\ngithub.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=\ngithub.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=\ngithub.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=\ngithub.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=\ngithub.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=\ngithub.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=\ngithub.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=\ngithub.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-gorp/gorp/v3 v3.0.2 h1:ULqJXIekoqMx29FI5ekXXFoH1dT2Vc8UhnRzBg+Emz4=\ngithub.com/go-gorp/gorp/v3 v3.0.2/go.mod h1:BJ3q1ejpV8cVALtcXvXaXyTOlMmJhWDxTmncaR6rwBY=\ngithub.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=\ngithub.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=\ngithub.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=\ngithub.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=\ngithub.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=\ngithub.com/gobuffalo/logger v1.0.6 h1:nnZNpxYo0zx+Aj9RfMPBm+x9zAU2OayFh/xrAWi34HU=\ngithub.com/gobuffalo/logger v1.0.6/go.mod h1:J31TBEHR1QLV2683OXTAItYIg8pv2JMHnF/quuAbMjs=\ngithub.com/gobuffalo/packd v1.0.1 h1:U2wXfRr4E9DH8IdsDLlRFwTZTK7hLfq9qT/QHXGVe/0=\ngithub.com/gobuffalo/packd v1.0.1/go.mod h1:PP2POP3p3RXGz7Jh6eYEf93S7vA2za6xM7QT85L4+VY=\ngithub.com/gobuffalo/packr/v2 v2.8.3 h1:xE1yzvnO56cUC0sTpKR3DIbxZgB54AftTFMhB2XEWlY=\ngithub.com/gobuffalo/packr/v2 v2.8.3/go.mod h1:0SahksCVcx4IMnigTjiFuyldmTrdTctXsOdiU5KwbKc=\ngithub.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/godror/godror v0.24.2/go.mod h1:wZv/9vPiUib6tkoDl+AZ/QLf5YZgMravZ7jxH2eQWAE=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=\ngithub.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=\ngithub.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=\ngithub.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=\ngithub.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=\ngithub.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=\ngithub.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=\ngithub.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=\ngithub.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=\ngithub.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=\ngithub.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=\ngithub.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=\ngithub.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=\ngithub.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=\ngithub.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=\ngithub.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=\ngithub.com/gorilla/securecookie v1.1.2-0.20180608144417-78f3d318a8bf h1:tgJ+TrHb8GE+aKrvU3dWpVEByJtj9/i7QRMVtX1XMmc=\ngithub.com/gorilla/securecookie v1.1.2-0.20180608144417-78f3d318a8bf/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=\ngithub.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=\ngithub.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=\ngithub.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=\ngithub.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=\ngithub.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=\ngithub.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=\ngithub.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=\ngithub.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=\ngithub.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=\ngithub.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=\ngithub.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=\ngithub.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=\ngithub.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=\ngithub.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=\ngithub.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=\ngithub.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=\ngithub.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=\ngithub.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=\ngithub.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=\ngithub.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=\ngithub.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=\ngithub.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=\ngithub.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=\ngithub.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=\ngithub.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=\ngithub.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=\ngithub.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=\ngithub.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=\ngithub.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw=\ngithub.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk=\ngithub.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=\ngithub.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/kortschak/utter v1.0.1/go.mod h1:vSmSjbyrlKjjsL71193LmzBOKgwePk9DH6uFaWHIInc=\ngithub.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=\ngithub.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\ngithub.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=\ngithub.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\ngithub.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=\ngithub.com/markbates/errx v1.1.0 h1:QDFeR+UP95dO12JgW+tgi2UVfo0V8YBHiUIOaeBPiEI=\ngithub.com/markbates/errx v1.1.0/go.mod h1:PLa46Oex9KNbVDZhKel8v1OT7hD5JZ2eI7AHhA0wswc=\ngithub.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY=\ngithub.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI=\ngithub.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI=\ngithub.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=\ngithub.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=\ngithub.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=\ngithub.com/mattn/go-oci8 v0.1.1/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI=\ngithub.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=\ngithub.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=\ngithub.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=\ngithub.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=\ngithub.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=\ngithub.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=\ngithub.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=\ngithub.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=\ngithub.com/mitchellh/cli v1.1.4/go.mod h1:vTLESy5mRhKOs9KDp0/RATawxP1UqBmdrpVRMnpcvKQ=\ngithub.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=\ngithub.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=\ngithub.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=\ngithub.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=\ngithub.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/mssola/user_agent v0.5.3 h1:lBRPML9mdFuIZgI2cmlQ+atbpJdLdeVl2IDodjBR578=\ngithub.com/mssola/user_agent v0.5.3/go.mod h1:TTPno8LPY3wAIEKRpAtkdMT0f8SE24pLRGPahjCH4uw=\ngithub.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=\ngithub.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=\ngithub.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=\ngithub.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=\ngithub.com/poy/onpar v0.0.0-20190519213022-ee068f8ea4d1 h1:oL4IBbcqwhhNWh31bjOX8C/OCy0zs9906d/VUru+bqg=\ngithub.com/poy/onpar v0.0.0-20190519213022-ee068f8ea4d1/go.mod h1:nSbFQvMj97ZyhFRSJYtut+msi4sOY6zJDGCdSc+/rZU=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=\ngithub.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=\ngithub.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=\ngithub.com/rubenv/sql-migrate v1.2.0 h1:fOXMPLMd41sK7Tg75SXDec15k3zg5WNV6SjuDRiNfcU=\ngithub.com/rubenv/sql-migrate v1.2.0/go.mod h1:Z5uVnq7vrIrPmHbVFfR4YLHRZquxeHpckCnRq0P/K9Y=\ngithub.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=\ngithub.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=\ngithub.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=\ngithub.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=\ngithub.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=\ngithub.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=\ngithub.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=\ngithub.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=\ngithub.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=\ngithub.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=\ngithub.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=\ngithub.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=\ngithub.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=\ngithub.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=\ngithub.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=\ngithub.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=\ngithub.com/urfave/cli v1.22.10 h1:p8Fspmz3iTctJstry1PYS3HVdllxnEzTEsgIgtxTrCk=\ngithub.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=\ngithub.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=\ngithub.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=\ngithub.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=\ngithub.com/ziutek/mymysql v1.5.5-0.20171217234033-ff6cc86d3d93 h1:neEPpeeJDl7Rs0FRhSyk0SloWCRoNdwn4wB6nD4eP7Q=\ngithub.com/ziutek/mymysql v1.5.5-0.20171217234033-ff6cc86d3d93/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=\ngo.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=\ngo.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=\ngo.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=\ngo.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=\ngo.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=\ngo.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=\ngo.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=\ngo.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=\ngo.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=\ngolang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=\ngolang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=\ngolang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=\ngolang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=\ngolang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=\ngolang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=\ngolang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=\ngolang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=\ngolang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=\ngolang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=\ngolang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=\ngolang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=\ngolang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=\ngolang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=\ngolang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=\ngolang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=\ngolang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=\ngolang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM=\ngolang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=\ngolang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=\ngolang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=\ngolang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=\ngolang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=\ngolang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=\ngoogle.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=\ngoogle.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=\ngoogle.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=\ngoogle.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=\ngoogle.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=\ngoogle.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=\ngoogle.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=\ngoogle.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=\ngoogle.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=\ngoogle.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=\ngoogle.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=\ngoogle.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=\ngoogle.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=\ngoogle.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=\ngoogle.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=\ngoogle.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=\ngoogle.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=\ngoogle.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=\ngoogle.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=\ngoogle.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=\ngopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=\ngopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=\nhonnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nhonnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nrsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=\nrsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=\nrsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=\n"
  },
  {
    "path": "gulpfile.js",
    "content": "'use strict';\n\nconst browserify = require('browserify')\nconst gulp = require('gulp')\nconst source = require('vinyl-source-stream')\nconst buffer = require('vinyl-buffer')\nconst uglify = require('gulp-uglify')\nconst babel = require('gulp-babel');\nconst cachebust = require('gulp-cache-bust');\nconst concat = require('gulp-concat');\nconst gulpif = require('gulp-if')\n\nconst debug = process.env.NODE_ENV !== 'production';\n\ngulp.task('app-js', function () {\n    return browserify({\n        entries: './assets/src/js/script.js',\n        debug: debug,\n        ignoreMissing: true,\n    })\n    .transform(\"babelify\", {\n      presets: [\"@babel/preset-env\"],\n      plugins: [ \n        [\"@babel/plugin-proposal-decorators\", { \"legacy\": true }],\n        [\"@babel/plugin-transform-react-jsx\", { \"pragma\":\"h\" } ]\n      ]\n    })    \n    .bundle()  \n    .pipe(source('script.js'))\n\t.pipe(buffer())\n\t.pipe(gulpif(!debug, uglify()))\n  \t.pipe(gulp.dest(`./assets/build/js`))  \n});  \n\ngulp.task('tracker-js', function () {\n  return gulp.src('./assets/src/js/tracker.js')\n        .pipe(babel({\n            presets: [\"@babel/preset-env\"],\n        }))\n        .pipe(gulpif(!debug, uglify()))\n        .pipe(gulp.dest('./assets/build/js'));\n});\n\ngulp.task('fonts', function() {\n  return gulp.src('./assets/src/fonts/**/*')\n    .pipe(gulp.dest(`./assets/build/fonts`))\n});\n\ngulp.task('img', function() {\n  return gulp.src('./assets/src/img/**/*')\n    .pipe(gulp.dest(`./assets/build/img`))\n});\n\ngulp.task('html', function() {\n  return gulp.src('./assets/src/**/*.html')\n    .pipe(cachebust({\n      type: 'timestamp'\n    }))\n    .pipe(gulp.dest(`./assets/build/`))\n});\n\ngulp.task('css', function () {\n\treturn gulp.src('./assets/src/css/*.css')\n\t\t.pipe(concat('styles.css'))\n\t\t.pipe(gulp.dest(`./assets/build/css`))\n});\n\ngulp.task('default', gulp.series('app-js', 'tracker-js', 'css', 'html', 'img', 'fonts' ) );\n\ngulp.task('watch', gulp.series('default', function() {\n  gulp.watch(['./assets/src/js/**/*.js'], gulp.parallel('app-js', 'tracker-js') );\n  gulp.watch(['./assets/src/css/**/*.css'], gulp.parallel( 'css') );\n  gulp.watch(['./assets/src/**/*.html'], gulp.parallel( 'html') );\n  gulp.watch(['./assets/src/img/**/*'], gulp.parallel( 'img') );\n  gulp.watch(['./assets/src/fonts/**/*'], gulp.parallel( 'fonts') );\n}));\n"
  },
  {
    "path": "main.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/usefathom/fathom/pkg/cli\"\n)\n\nvar (\n\tversion = \"dev\"\n\tcommit  = \"none\"\n\tdate    = \"unknown\"\n)\n\nfunc main() {\n\terr := cli.Run(version, commit, date)\n\tif err != nil {\n\t\tfmt.Print(err)\n\t\tos.Exit(1)\n\t}\n\n\tos.Exit(0)\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/usefathom/fathom.git\"\n  },\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.0.1\",\n    \"@babel/plugin-proposal-decorators\": \"^7.0.0\",\n    \"@babel/plugin-transform-react-jsx\": \"^7.0.0\",\n    \"@babel/preset-env\": \"^7.0.0\",\n    \"babelify\": \"^10.0.0\",\n    \"browserify\": \"^16.2.0\",\n    \"gulp\": \"^4.0.0\",\n    \"gulp-babel\": \"^8.0.0\",\n    \"gulp-cache-bust\": \"^1.4.0\",\n    \"gulp-concat\": \"^2.6.1\",\n    \"gulp-if\": \"^2.0.2\",\n    \"gulp-uglify\": \"^3.0.0\",\n    \"vinyl-buffer\": \"^1.0.0\",\n    \"vinyl-source-stream\": \"^2.0.0\"\n  },\n  \"dependencies\": {\n    \"classnames\": \"^2.2.6\",\n    \"d3\": \"^5.7.0\",\n    \"d3-tip\": \"^0.9.1\",\n    \"d3-transition\": \"^1.1.3\",\n    \"decko\": \"^1.2.0\",\n    \"pikaday\": \"^1.8.0\",\n    \"preact\": \"^8.3.1\"\n  }\n}\n"
  },
  {
    "path": "pkg/aggregator/aggregator.go",
    "content": "package aggregator\n\nimport (\n\t\"errors\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/usefathom/fathom/pkg/datastore\"\n\t\"github.com/usefathom/fathom/pkg/models\"\n\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype Aggregator struct {\n\tdatabase datastore.Datastore\n}\n\ntype Report struct {\n\tProcessed int\n\tPoolEmpty bool\n\tDuration  time.Duration\n}\n\ntype results struct {\n\tSites     map[string]*models.SiteStats\n\tPages     map[string]*models.PageStats\n\tReferrers map[string]*models.ReferrerStats\n}\n\n// New returns a new aggregator instance with the database dependency injected.\nfunc New(db datastore.Datastore) *Aggregator {\n\treturn &Aggregator{\n\t\tdatabase: db,\n\t}\n}\n\n// Run processes the pageviews which are ready to be processed and adds them to daily aggregation\nfunc (agg *Aggregator) Run() Report {\n\tstartTime := time.Now()\n\n\t// Get unprocessed pageviews\n\tlimit := 10000\n\tpageviews, err := agg.database.GetProcessablePageviews(limit)\n\temptyReport := Report{\n\t\tProcessed: 0,\n\t\tPoolEmpty: true,\n\t}\n\tif err != nil && err != datastore.ErrNoResults {\n\t\tlog.Error(err)\n\t\treturn emptyReport\n\t}\n\n\t//  Do we have anything to process?\n\tn := len(pageviews)\n\tif n == 0 {\n\t\treturn emptyReport\n\t}\n\n\tresults := &results{\n\t\tSites:     map[string]*models.SiteStats{},\n\t\tPages:     map[string]*models.PageStats{},\n\t\tReferrers: map[string]*models.ReferrerStats{},\n\t}\n\n\tsites, err := agg.database.GetSites()\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn emptyReport\n\t}\n\n\t// create map of public tracking ID's => site ID\n\ttrackingIDMap := make(map[string]int64, len(sites)+1)\n\tfor _, s := range sites {\n\t\ttrackingIDMap[s.TrackingID] = s.ID\n\t}\n\n\t// if no explicit site ID was given in the tracking request, default to site with ID 1\n\ttrackingIDMap[\"\"] = 1\n\n\t// setup referrer spam blacklist\n\tblacklist, err := newBlacklist()\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn emptyReport\n\t}\n\n\t// add each pageview to the various statistics we gather\n\tfor _, p := range pageviews {\n\t\t// discard pageview if site tracking ID is unknown\n\t\tsiteID, ok := trackingIDMap[p.SiteTrackingID]\n\t\tif !ok {\n\t\t\tlog.Debugf(\"Skipping pageview because of unrecognized site tracking ID %s\", p.SiteTrackingID)\n\t\t\tcontinue\n\t\t}\n\n\t\t// start with referrer because we may want to skip this pageview altogether if it is referrer spam\n\t\tif p.Referrer != \"\" {\n\t\t\tref, err := parseReferrer(p.Referrer)\n\t\t\tif err != nil {\n\t\t\t\tlog.Debugf(\"Skipping pageview from referrer %s because of malformed referrer URL\", p.Referrer)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// ignore out pageviews from blacklisted referrers\n\t\t\t// we use Hostname() here to discard port numbers\n\t\t\tif blacklist.Has(ref.Hostname()) {\n\t\t\t\tlog.Debugf(\"Skipping pageview from referrer %s because of blacklist\", p.Referrer)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\thostname := ref.Scheme + \"://\" + ref.Host\n\t\t\treferrerStats, err := agg.getReferrerStats(results, siteID, p.Timestamp, hostname, ref.Path)\n\t\t\tif err != nil {\n\t\t\t\tlog.Error(err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treferrerStats.HandlePageview(p)\n\t\t}\n\n\t\t// get existing site stats so we can add this pageview to it\n\t\tsite, err := agg.getSiteStats(results, siteID, p.Timestamp)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\tcontinue\n\t\t}\n\t\tsite.HandlePageview(p)\n\n\t\tpageStats, err := agg.getPageStats(results, siteID, p.Timestamp, p.Hostname, p.Pathname)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\tcontinue\n\t\t}\n\t\tpageStats.HandlePageview(p)\n\t}\n\n\t// update stats\n\tfor _, site := range results.Sites {\n\t\tif err := agg.database.SaveSiteStats(site); err != nil {\n\t\t\tlog.Error(err)\n\t\t}\n\t}\n\n\tfor _, pageStats := range results.Pages {\n\t\tif err := agg.database.SavePageStats(pageStats); err != nil {\n\t\t\tlog.Error(err)\n\t\t}\n\t}\n\n\tfor _, referrerStats := range results.Referrers {\n\t\tif err := agg.database.SaveReferrerStats(referrerStats); err != nil {\n\t\t\tlog.Error(err)\n\t\t}\n\t}\n\n\t// finally, remove pageviews that we just processed\n\tif err := agg.database.DeletePageviews(pageviews); err != nil {\n\t\tlog.Error(err)\n\t}\n\n\tendTime := time.Now()\n\tdur := endTime.Sub(startTime)\n\n\treport := Report{\n\t\tProcessed: n,\n\t\tPoolEmpty: n < limit,\n\t\tDuration:  dur,\n\t}\n\tlog.Debugf(\"processed %d pageviews. took: %s, pool empty: %v\", report.Processed, report.Duration, report.PoolEmpty)\n\treturn report\n}\n\n// parseReferrer parses the referrer string & normalizes it\nfunc parseReferrer(r string) (*url.URL, error) {\n\tu, err := url.Parse(r)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// always require a hostname\n\tif u.Host == \"\" {\n\t\treturn nil, errors.New(\"malformed URL, empty host\")\n\t}\n\n\t// remove AMP & UTM vars\n\tif u.RawQuery != \"\" {\n\t\tq := u.Query()\n\t\tkeys := []string{\"amp\", \"utm_campaign\", \"utm_medium\", \"utm_source\"}\n\t\tfor _, k := range keys {\n\t\t\tq.Del(k)\n\t\t}\n\t\tu.RawQuery = q.Encode()\n\t}\n\n\t// remove amp/ suffix (but keep trailing slash)\n\tif strings.HasSuffix(u.Path, \"/amp/\") {\n\t\tu.Path = u.Path[0:(len(u.Path) - 4)]\n\t}\n\n\t// re-parse our normalized string into a new URL struct\n\treturn url.Parse(u.String())\n}\n"
  },
  {
    "path": "pkg/aggregator/aggregator_test.go",
    "content": "package aggregator\n\nimport (\n\t\"net/url\"\n\t\"testing\"\n)\n\nfunc TestParseReferrer(t *testing.T) {\n\ttestsValid := map[string]*url.URL{\n\t\t\"https://www.usefathom.com/?utm_source=github\": &url.URL{\n\t\t\tScheme: \"https\",\n\t\t\tHost:   \"www.usefathom.com\",\n\t\t\tPath:   \"/\",\n\t\t},\n\t\t\"https://www.usefathom.com/privacy/amp/?utm_source=github\": &url.URL{\n\t\t\tScheme: \"https\",\n\t\t\tHost:   \"www.usefathom.com\",\n\t\t\tPath:   \"/privacy/\",\n\t\t},\n\t}\n\ttestsErr := []string{\n\t\t\"mysite.com\",\n\t\t\"foobar\",\n\t\t\"\",\n\t}\n\n\tfor r, e := range testsValid {\n\t\tv, err := parseReferrer(r)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\n\t\tif v.Host != e.Host {\n\t\t\tt.Errorf(\"Invalid Host: expected %s, got %s\", e.Host, v.Host)\n\t\t}\n\n\t\tif v.Scheme != e.Scheme {\n\t\t\tt.Errorf(\"Invalid Scheme: expected %s, got %s\", e.Scheme, v.Scheme)\n\t\t}\n\n\t\tif v.Path != e.Path {\n\t\t\tt.Errorf(\"Invalid Path: expected %s, got %s\", e.Path, v.Path)\n\t\t}\n\n\t}\n\n\tfor _, r := range testsErr {\n\t\tv, err := parseReferrer(r)\n\t\tif err == nil {\n\t\t\tt.Errorf(\"Expected err, got %#v\", v)\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "pkg/aggregator/bindata.go",
    "content": "// Code generated by go-bindata.\n// sources:\n// pkg/aggregator/data/blacklist.txt\n// DO NOT EDIT!\n\npackage aggregator\n\nimport (\n\t\"bytes\"\n\t\"compress/gzip\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n)\n\nfunc bindataRead(data []byte, name string) ([]byte, error) {\n\tgz, err := gzip.NewReader(bytes.NewBuffer(data))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"Read %q: %v\", name, err)\n\t}\n\n\tvar buf bytes.Buffer\n\t_, err = io.Copy(&buf, gz)\n\tclErr := gz.Close()\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"Read %q: %v\", name, err)\n\t}\n\tif clErr != nil {\n\t\treturn nil, err\n\t}\n\n\treturn buf.Bytes(), nil\n}\n\ntype asset struct {\n\tbytes []byte\n\tinfo  os.FileInfo\n}\n\ntype bindataFileInfo struct {\n\tname    string\n\tsize    int64\n\tmode    os.FileMode\n\tmodTime time.Time\n}\n\nfunc (fi bindataFileInfo) Name() string {\n\treturn fi.name\n}\nfunc (fi bindataFileInfo) Size() int64 {\n\treturn fi.size\n}\nfunc (fi bindataFileInfo) Mode() os.FileMode {\n\treturn fi.mode\n}\nfunc (fi bindataFileInfo) ModTime() time.Time {\n\treturn fi.modTime\n}\nfunc (fi bindataFileInfo) IsDir() bool {\n\treturn false\n}\nfunc (fi bindataFileInfo) Sys() interface{} {\n\treturn nil\n}\n\nvar _blacklistTxt = []byte(\"\\x1f\\x8b\\x08\\x00\\x00\\x00\\x00\\x00\\x00\\xff\\x84\\x5b\\xfb\\x72\\xe3\\xae\\xaf\\xff\\x3f\\xef\\x42\\xa6\\xed\\x76\\x2f\\xdf\\xc7\\x91\\x41\\xb6\\x55\\x2e\\x62\\xb9\\x38\\x71\\x9e\\xfe\\x8c\\x00\\xa7\\xe9\\xb6\\xa7\\xbf\\x9d\\xe9\\x2c\\xba\\x18\\x63\\x01\\xe2\\x23\\x89\\x3c\\xfd\\xc0\\x33\\x85\\x99\\x4f\\x4f\\x41\\x39\\x0a\\x78\\x2e\\xdb\\xe9\\x59\\xfd\\xf7\\x5f\\x46\\x3e\\x6b\\xf6\\xa7\\x67\\x35\\x27\\x44\\x95\\x57\\x48\\xa8\\xa6\\x5a\\x0a\\x87\\xdc\\x05\\x4f\\x4f\\x86\\x9d\\x83\\x94\\xd5\\x5d\\xf7\\xe9\\x29\\x23\\x24\\xbd\\x62\\x58\\x28\\xe0\\xd0\\x7b\\xf1\\x90\\x0b\\x26\\xde\\x06\\x59\\xfb\\xfb\\x9e\\x23\\x78\\x7f\\x4e\\xf5\\xf4\\x7c\\xc1\\xa9\\xab\\x9c\\xbd\\x3b\\xbd\\xbc\\x5e\\x7f\\xab\\x8c\\x69\\xc3\\xa4\\x72\\x8d\\x91\\x53\\x39\\x67\\x2a\\x78\\x7a\\xd9\\xb9\\xa6\\xde\\xfc\\xa1\\x1c\\x96\\x82\\x49\\x19\\xf6\\x40\\x21\\x9f\\x03\\x96\\xd3\\x8f\\x0b\\xec\\x01\\xcb\\x85\\x93\\xed\\x2f\\x7e\\xa5\\x10\\xa4\\xff\\x57\\xca\\x85\\xf3\\xca\\xb1\\x73\\xef\\x6f\\xcb\\x67\\x4e\\xcb\\xe9\\xa7\\xca\\x05\\x63\\x56\\x85\\x55\\x2e\\x90\\x8a\\x9a\\x6a\\x96\\xb1\\xf7\\x3e\\x7e\\xce\\x9c\\xf0\\x2a\\xbd\\xfc\\x5a\\x39\\x46\\x0a\\x4b\\x63\\xff\\xb6\\x1c\\x85\\xf9\\xdb\\x83\\x45\\xcf\\x01\\x77\\x0e\\xcd\\x78\\x4d\\x78\\x83\\xfe\\x2a\\x98\\xb4\\xc1\\x79\\x3d\\x5f\\xf7\\x5b\\x6f\\x2f\\x47\\xd3\\x41\\x0d\\x7a\\xc5\\xd4\\xd5\\x34\\x98\\xfe\\x09\\xa0\\x21\\x25\\x64\\xe9\\x19\\x74\\x0d\\x58\\xe8\\xaa\\x12\\xce\\x98\\xd2\\xa1\\x6a\\x20\\xd0\\x3a\\x9a\\x1a\\xf2\\xd1\\x9c\\xcf\\x6e\\x3f\\x81\\xc9\\x11\\x52\\x19\\xac\\x42\\x0b\\xa6\\x73\\xb1\\x27\\x30\\x1b\\x86\\x52\\x13\\x46\\x48\\x56\\xb3\\x7c\\x24\\x69\\x18\\x5a\\x1b\\x69\\x9c\\x39\\x55\\xdf\\xe7\\x04\\xcc\\xc6\\x16\\xca\\x31\\x50\\x4c\\x6c\\xe8\\x06\\x14\\xba\\xf6\\x3c\\x73\\x32\\x30\\x39\\xbc\\xe0\\x24\\xf3\\x90\\x21\\x18\\xcf\\x13\\x39\\x84\\x18\\xf3\\xd0\\xe1\\x04\\x6d\\xfc\\x34\\xb1\\xa3\\x32\\xde\\x43\\x06\\x92\\xc7\\x09\\xdd\\xd9\\xde\\x4e\\x60\\xeb\\x1a\\xe8\\x3c\\xed\\x27\\x70\\x33\\x4c\\x5c\\xfa\\xcb\\x1c\\x4d\\x98\\x4b\\x06\\xd7\\xad\\x08\\x8e\\xf0\\xda\\x28\\xe9\\xcd\\x51\\x80\\x09\\x02\\xa1\\xb6\\x70\\x8e\\xee\\x04\\xce\\x42\\x98\\x21\\x79\\x68\\x93\\x08\\xce\\x61\\x5a\\x48\\xdb\\xf1\\x68\\xa3\\xde\\xe0\\xa0\\x6c\\xe0\\xcb\\xf8\\x40\\xe7\\x3c\\x24\\x8b\\x25\\x07\\xbc\\x18\\xd8\\x1d\\x9d\\x17\\x13\\x84\\x1d\\xf0\\x92\\xcf\\xde\\x1c\\xcd\\x97\\xd7\\x33\\x35\\xfe\\x85\\x3d\\x86\\xfb\\xc3\\x97\\x44\\xcb\\x2a\\x2b\\xb5\\x0d\\x2a\\xae\\x6d\\x04\\x6d\\xea\\x5c\\xc1\\xe4\\xe9\\x7a\\xae\\x70\\x02\\x0f\\x37\\x0e\\xb2\\x2b\\xda\\x42\\x26\\x3d\\xbe\\xc8\\x17\\x65\\xdb\\x83\\xbe\\xa0\\x53\\x5b\\x42\\x83\\xb9\\xe0\\x61\\xdd\\x00\\x4e\\x81\\x4e\\x3c\\x41\\xc9\\xe7\\x95\\xdd\\x19\\x73\\x63\\xee\\x85\\x74\\x56\\xb2\\x44\\x9a\\x99\\x02\\x44\\x50\\x14\\x42\\x6e\\x3d\\x05\\x93\\x98\\x8c\\xca\\x65\\x3f\\xcc\\x16\\xc8\\xcb\\xc0\\xb8\\xf0\\xf1\\x00\\x79\\x94\\xbf\\xae\\x5f\\x48\\x27\\xb8\\x38\\x4c\\xdd\\x6e\\xa1\\xd0\\x52\\x61\\x82\\x34\\x55\\xd3\\xe7\\x2d\\x16\\xb4\\xa0\\xda\\x97\\x35\\x3a\\x61\\x30\\x60\\x37\\x48\\x85\\x52\\x9b\\x3d\\x61\\xf0\\x26\\x13\\xd0\\xfb\\x4f\\xd6\\xd2\\xc6\\xae\\x14\\xea\\x86\\x48\\xc5\\xa0\\xe5\\x61\\xb1\\x54\\x22\\x50\\x28\\xaa\\x1b\\xbd\\x77\\x58\\x22\\xa4\\xbf\\xf5\\xa0\\x6a\\x74\\x62\\xd2\\x63\\x67\\x41\\xb6\\x6a\\xaf\\xa3\\x5d\\x1c\\x16\\xd2\\x7d\\x2c\\x6d\\xb8\\x65\\xb7\\xfd\\xbb\\x6b\\x61\\xa5\\xd9\\x47\\x87\\xd7\\xb6\\x90\\x84\\xb6\\x04\\x6a\\xae\\xce\\x99\\x44\\x1b\\xde\\xb5\\x1e\\x67\\xa1\\xf5\\x51\\x0b\\x4f\\x8e\\x17\\x21\\xda\\x64\\xd5\\xc2\\x5f\\xe9\\x08\\xaf\\x24\\x98\\x67\\xd2\\x7d\\x30\\xb5\\xf0\\x46\\x06\\x79\\x4a\\xdc\\xf6\\xde\\xd8\\x68\\x1b\\x6d\\xa0\\x1c\\x79\\xae\\xd7\\xc1\\xb0\\x37\\x48\\xb2\\xac\\xd9\\x0e\\x23\\x6c\\x85\\x29\\x14\\x4c\\xd8\\xc7\\xbe\\x15\\xde\\x76\\x5b\\x63\\xb3\\xe6\\x0d\\x52\\xd1\\xae\\x4e\\xfd\\xbd\\xb7\\xa9\\x5a\\x10\\x7f\\xdb\\xbb\\xba\\xc9\\xe7\\xd5\\xdb\\x69\\x02\\xba\\x42\\x52\\xbe\\x66\\xd2\\x90\\xd5\\x92\\xa0\\x50\\xdf\\x6f\\x13\\x38\\x30\\xb5\\x2d\\xc7\\x09\\x1c\\x15\\xae\\x89\\x65\\xc8\\x78\\x48\\x3f\\x6c\\x93\\x09\\x92\\x51\\x09\\xc1\\x89\\x54\\x3e\\x7e\\x82\\x52\\x8b\\x9a\\x6b\\xe8\\x1d\\x6c\\x90\\x08\\x16\\x32\\x67\\x83\\xa7\\x09\\x41\\xaf\\x85\\x0d\\xec\\x4d\\x86\\x26\\x31\\xfb\\xfb\\x54\\xd5\\x7c\\x9a\\x30\\xa1\\xc7\\xb0\\x43\\x5e\\x6d\\xdf\\x69\\xb2\\x7f\\x95\\x41\\x70\\x6a\\x35\\xe6\\x1c\\x13\\x77\\x8e\\x78\\xcd\\xc3\\xbc\\xaa\\x66\\x38\\x4f\\xae\\x62\\x17\\x89\\x91\\x79\\x9e\\x87\\x77\\xbb\\xb3\\x32\\xcf\\xe5\\x02\\x09\\xdb\\x02\\x7b\\xe0\\xba\\x5a\\x88\\xc3\\x5d\\xb7\\xf9\\x1e\\x2a\\x7b\\xd6\\xcc\\xe2\\xcf\\xfb\\x68\\x0f\\x69\\xeb\\x77\\x35\\x66\\xda\\x4b\\x9f\\x87\\x47\\x26\\xa2\\x79\\x67\\xde\\xdd\\xd9\\x05\\x92\\x19\\x76\\x6d\\xe7\\xcb\\x8a\\xe0\\xca\\x3a\\x21\\xd4\\x72\\xf4\\x7b\\x8b\\x49\\x5c\\x68\\x39\\x5c\\xdb\\x44\\xb3\\x4a\\xe3\\x44\\x9b\\x68\\x71\\x72\\xd6\\xcc\\x47\\x87\\x43\\xc3\\x39\\x12\\xc3\\x6b\\x07\\x39\\xf7\\xd5\\xd4\\x6c\\x4f\\x7c\\x6c\\x0b\\x7b\\x3b\\x4d\\x14\\x1d\\x04\\xd4\\x18\\x4a\\x6a\\x8b\\x77\\xa2\\x64\\x9e\\x7b\\xe3\\xf0\\xc7\\x13\\xdd\\xee\\xaf\\x72\\xa0\\xad\\x9a\\x13\\xc9\\x17\\x2f\\xd0\\xe9\\x15\\xe4\\xf4\\x2b\\xfd\\x50\\x90\\x35\\xfe\\xfc\\xf4\\xd4\\xe7\\xdd\\xf1\\xf2\\x5a\\xcf\\x85\\x63\\x6b\\xca\\x11\\x70\\x9e\\x6b\\x68\\x44\\xe1\\x02\\xae\\x4d\\xb8\\xab\\xa8\\xf2\\xdf\\x2a\\x76\\x9f\\xe8\\xd6\\xe8\\xc4\\xe2\\x9e\\xfb\\x1b\\xd9\\x15\\x70\\x96\\xfb\\x48\\x98\\x73\\xf1\\x7b\\x8c\\x7d\\x6f\\x4c\\x31\\xf1\\x73\\xef\\x3e\\x81\\xc5\\x15\\x2e\\xdd\\x11\\x4f\\xe9\\x7e\\x96\\x4c\\x09\\xc1\\xaa\\xb2\\xa2\\xd2\\x6b\\x3b\\xb2\\xbb\\xbc\\xd9\\x26\\x94\\x7d\\xbc\\x23\\x59\\x95\\x0a\\xb5\\xaf\\x4e\\x5c\\x56\\x14\\x6c\\xe1\\x9b\\x3b\\xef\\xbc\\x9a\\xc9\\xf1\\xd6\\xda\\x19\\x9d\\x6b\\x8d\\x6a\\xc8\\x85\\xe3\\x2d\\xd5\\x78\\xd9\\x5f\\x1e\\x0a\\xf5\\x45\\xd9\\x2c\\x5d\\x67\\x2c\\x5c\\x4b\\x57\\xb7\\x78\\x1f\\x53\\xb5\\xee\\x81\\x48\\x0b\\x26\\x45\\x3e\\x62\\xa2\\x31\\xb7\\x35\\x05\\x35\\x43\\x69\\x06\\xee\\xc8\\x47\\xcd\\x9c\\xd4\\x98\\xde\\xa1\\xf3\\xce\\x17\\x80\\xf2\\x8f\\x70\\x57\\x7a\\x45\\x88\\x6a\\xc0\\x83\\xfe\\x95\\x77\\x6e\\x24\\xe7\\xb2\\xe2\\x64\\x30\\xa9\\x07\\x00\\x21\\x0a\\xfd\\x50\\x6e\\x03\\xde\\x95\\x47\\x23\\x07\\x92\\xc8\\x34\\x38\\xa7\\x78\\x56\\xa6\\x1e\\x46\\xd3\\x90\\x0c\\x71\\x6e\\x40\\x69\\x7c\\xb0\\x86\\x54\\x50\\xaf\\x81\\xb4\\xf4\\xa0\\x31\\xb0\\xe5\\x7c\\x34\\x37\\x70\\xbd\\x29\\xae\\x07\\x26\\xd0\\xe7\\xc4\\x27\\xbd\\xea\\xda\\xdc\\x77\\x1f\\x58\\x49\\x35\\x17\\x34\\x6a\\x02\\x6d\\x1d\\x85\\x01\\xa8\\xf4\\x8a\\x6e\\x87\\x89\\x42\\xb6\\x67\\x43\\x18\\xa0\\xcd\\x94\\x5e\\x05\\x2e\\xa1\\x02\\x8f\\xb7\\xb1\\x39\\xf5\\x4a\\xb7\\x95\\xac\\x7a\\x69\\x72\\x6a\\x43\\xa2\\xb2\\x0b\\x40\\xa1\\xee\\x25\\x35\\x6d\\xe4\\x2e\\x90\\xca\\x8a\\x50\\xc6\\xf6\\xd7\\x0e\\x21\\x50\\x58\\x86\\xab\\xc8\\x67\\x4b\\xb8\\xb5\\x67\\x1d\\x69\\x9b\\xf9\\xd0\\x22\\x0f\\x05\\xc5\\xeb\\x8b\\xdf\\x54\\xae\\x5a\\x7e\\xeb\\x5f\\xc4\\x46\\x9c\\x00\\x0c\\xe3\\x6b\\x36\\x7b\\x9e\\xa6\\xbf\\x83\\x98\\x67\\x44\\x0f\\x79\\xa5\\xb0\\x77\\x65\\x57\\xfd\\x24\\x9f\\xdc\\xde\\xc0\\xde\\x63\\x82\\x05\\xbb\\xc8\\x47\\x15\\xd9\\x73\\xd6\\xeb\\x41\\xcb\\x2a\\xd5\\xa8\\x40\\x1c\\xb2\\xac\\x96\\x7f\\x78\\xf0\\x89\\x19\\x4c\\xc2\\xcb\\xbf\\xdc\\x09\\x12\\xd8\\x4f\\xcc\\x44\\x10\\xfe\\x65\\x1a\\xfe\\x8a\\x05\\xce\\xfc\\xcb\\x45\\x87\\xe1\\xd3\\xdb\\xe7\\x84\\x9f\\x14\\x17\\xe4\\xb4\\xe0\\xbf\\x5c\\x4a\\x1b\\x7d\\x7a\\x13\\x6d\\x9f\\x07\\xf4\\xc6\\x6b\\x68\\x3b\\xfc\\x91\\x57\\xa5\\x11\\xe0\\xce\\xaf\\x82\\xc9\\x93\\x00\\xe2\\xd2\\x0d\\x17\\x34\\x61\\x5a\\x70\\x49\\x5c\\x63\\xf3\\x44\\x9a\\x43\\x40\\x2d\\x9b\\x33\\x97\\x6a\\x88\\xc7\\xe4\\xb0\\x25\\x54\\x0e\\x2e\\x0a\\xc3\\xcc\\x49\\xcb\\xb9\\x52\\x14\\x1c\\xdf\\xf5\\xa5\\x74\\x9a\\xbe\\x93\\x6a\\xfd\\x9d\\xd4\\x98\\xef\\xa4\\x88\\xdf\\x49\\xe7\\xf9\\x3b\\xe9\\xb2\\x7c\\x27\\x5d\\xd7\\xef\\xa4\\x44\\xdf\\x49\\xdf\\xde\\xbe\\x93\\x5a\\xfb\\x9d\\xd4\\xb9\\xef\\xa4\\xde\\x7f\\x27\\x0d\\xe1\\x3b\\x29\\xf3\\x77\\xd2\\x18\\xbf\\x93\\xfe\\xfd\\xfb\\x9d\\x34\\xa5\\xef\\xa4\\x39\\x7f\\x27\\x2d\\xe5\\x3b\\x69\\xad\\xdf\\x49\\xb7\\xed\\x3b\\xe9\\xe5\\xf2\\x9d\\xf4\\x7a\\xfd\\x4e\\xba\\xef\\xdf\\x49\\x6f\\xb7\\x21\\x8d\\x7b\\x8b\\x31\\xb4\\x03\\xf2\\x79\\x6c\\x9a\\xc1\\xa3\\x90\\x0b\\x95\\x5a\\x70\\xb0\\x37\\x30\\x2b\\xe7\\x06\\xc8\\xe4\\xc4\\xd6\\xb1\\x1f\\x12\\xcd\\xa3\\xd5\\x89\\xd9\\x9e\\x73\\x8d\\x98\\x02\\x5e\\xfa\\x03\\x35\\x17\\xf6\\xb9\\xde\\x0f\\x46\\xbd\\x4f\\x98\\x94\\xe7\\x30\\x50\\x84\\x01\\x72\\x7b\\x82\\x60\\xdb\\x51\\x60\\x20\\xb1\\x81\\xee\\x75\\x8d\\xc4\\x0c\\x81\\xba\\x67\\x35\\xfd\\xd0\\xeb\\x4a\\xda\\x68\\x88\\x54\\x3a\\xa6\\x3c\\x19\\x94\\x10\\xfa\\xf9\\x87\\x6c\\x7f\\x83\\x6e\\xa6\\xa0\\xe0\\xef\\xfb\\x0b\\x8d\\x7c\\x2b\\x2c\\x58\\x47\\xaf\\x18\\x16\\x52\\x9b\\xb2\\x09\\x0d\\x09\\xce\\xe8\\x3a\\xc9\\x43\\xe1\\x0d\\x03\\x26\\x76\\xbc\\xd0\\x0e\\x43\\x39\\x6b\\x48\\xcb\\x1d\\x07\\x1f\\x30\\xb8\\x8d\\x02\\x4b\\x16\\xa3\\x5a\\x0e\\xb9\\xa4\\x6a\\x0b\\xa7\\xbd\\x0f\\x61\\x6b\\xc9\\x09\\x01\\x3a\\xa7\\xf7\\x23\\xcb\\x50\\x74\\xec\\x21\\x0b\\x72\\x6b\\x5d\\x53\\x6c\\x98\\x48\\x8c\\x64\\x04\\xc1\\x85\\x23\\x78\\x34\\x24\\x98\\x3f\\x98\\x16\\x93\\x9b\\x37\\xb4\\x57\\xe8\\x2d\\x0e\\x17\\x28\\xfd\\x7c\\x30\\xb6\\x34\\x78\\x62\\x58\\xe7\\x57\\x70\\xc3\\x10\\xac\\x33\\x24\\xbd\\x4a\\x28\\xd2\\x46\\xc8\\xba\\x1d\\xd1\\xe0\\x0e\\xb2\\xca\\xb4\\x4f\\x90\\xf1\\x03\\xa3\\x67\\x3e\\x3e\\xb2\\x04\\x4e\\x74\\xc6\\x92\\x53\\x1d\\x76\\xe7\\x37\\x4b\\x6a\\x35\\xe3\\x65\\x1e\\x28\\x48\\xb8\\xa2\\xed\\x38\\x4c\\x85\\x95\\xd7\\x40\\xbb\\x5a\\xb9\\xa0\\xeb\\xe3\\xf3\\x14\\xa0\\x60\\x4f\\x69\\x78\\x77\\x30\\x04\\x52\\x37\\x2a\\x5b\\x50\\x5b\\x46\\xdf\\x95\\x73\\x81\\x4d\\x18\\xca\\xa6\\xdd\\x8f\\x3e\\x73\\x5d\\x04\\xfe\\xf2\\xd6\\xf3\\x30\\x26\\xd5\\x38\\xec\\xb4\\x25\\x59\\x82\\x32\\x7f\\xa8\\xa6\\xba\\x23\\xe4\\x8e\\x98\\x51\\xf5\\xb3\\x55\\xe3\\x3d\\x4b\\xf4\\x91\\xf5\\xdc\\x79\\x20\\x50\\x2b\\xb1\\x57\\x12\\x68\\x6a\\x37\\xb0\\xb3\\xb0\\x09\\x82\\x6a\\x29\\x96\\x0e\\x77\\xa4\\xe7\\xe3\\xf9\\xb3\\x9e\\x4f\\xf8\\xa1\\x7f\\x99\\x41\\xe1\\xc4\\xb6\\x02\\x51\\x73\\x60\\x19\\xfb\\x09\\x0d\\xd8\\x65\\xde\\x2e\\x7f\\x9b\\x25\\xd0\\x54\\x99\\x8f\\x66\\x48\\x34\\x55\\xfa\\xbd\\x63\\x36\\x34\\xf5\\x61\\x0e\\x70\\x11\\xbc\\x84\\x7c\\xa6\\x72\\x42\\xab\\x28\\x6c\\x98\\xdb\\xd9\\x86\\x16\\x0a\\x48\\x18\\x29\\x9b\\x16\\x2d\\xab\\x45\\x62\\xfe\\x2e\\xe1\\x98\\x18\\x6d\\x51\\x36\\x75\\xba\\xf0\\x19\\xf1\\x84\\x0e\\x6d\\x49\\x64\\x79\\xa3\\xbe\\x6e\\xd0\\xb5\\x9d\\x9f\\xa3\\xab\\x39\\x0f\\x46\\x28\\x7d\\x6b\\x34\\x43\\x3a\\x4f\\x33\\xa4\\x15\\xc2\\x42\\x7d\\x68\\x6e\\xc3\\x7b\\xe8\\x86\\x1e\\x53\\xe6\\xa0\\x52\\xed\\xcf\\x26\\xd2\\x62\\x3b\\xec\\x53\\x85\\x89\\x4b\\xfb\\xee\\xac\\x39\\x15\\x51\\xca\\x82\\x32\\x9a\\x28\\x17\\x54\\x07\\xd6\\x6c\\x3d\\x15\\xa0\\x44\\x16\\x5a\\x74\\x7b\\x5e\\xd2\\x09\\x0b\\xae\\xe1\\x01\\x3f\\x63\\x55\\x5f\\x3b\\xad\\x97\\xfe\\xf5\\x35\\x71\\xcf\\xa8\\xb5\\x08\\xa7\\x8d\\xa6\\x26\\x8e\\xb0\\xf4\\x59\\x3c\\x18\\x19\\x3c\\x1b\\x74\\xb6\\x6d\\x41\\xdc\\x9a\\xa3\\x96\\x15\\x7b\\x84\\xfe\\x78\\x95\\x49\\x99\\xe9\\x3e\\xf9\\xd7\\x98\\x30\\x67\\xb5\\xed\\x1b\\xdf\\xda\\x33\\x3b\\x66\\xc5\\x41\\x70\\xb6\\x78\\xab\\xf9\\x39\\x98\\x91\\xc7\\x98\\x21\\xf0\\xc4\\xdd\\x4a\\x33\\xe4\\xa2\\x2e\\x9c\\x4c\\x7f\\xba\\x25\\xf5\\xba\\x60\\x32\\x7c\\x09\\x8e\\xc1\\x8c\\x3d\\x32\\xa3\\xa7\\x40\\xb9\\x1c\\xa9\\x80\\x99\\x0c\\xb8\\x0c\\x12\\x0c\\xb5\\x51\\xb4\\xb8\\x5c\\x56\\x41\\xa3\\x0c\\x08\\x5a\\x1e\\xdb\\x75\\x26\\x27\\x58\\x87\\x8b\\xba\\xad\\xe8\\xf0\\xd6\\xbc\\xc2\\x4c\\x01\\x82\\x26\\x70\\x2a\\x93\\xaf\\x0e\\xee\\xc1\\x6a\\x13\\x64\\xde\\xfa\\x0a\\x9e\\x49\\xc6\\xad\\x21\\x8d\\x2c\\x4d\\xd7\\xb8\\x1e\\x91\\x83\\xc2\\x94\\x38\\x0d\\xb6\\x63\\x28\\x2d\\x7c\\xfe\\x94\\x7e\\x9d\\x1d\\x5f\\x50\\xf0\\x73\\x82\\xd8\\x7c\\xdd\\x11\\x81\\x9c\\x47\\x3f\\xa7\\xb6\\xcf\\x55\\x4c\\xac\\x0b\\x0f\\x85\\x8c\\xd7\\x31\\x06\\x4e\\xe5\\xc2\\xd9\\x43\\x2a\\x1a\\xd2\\x39\\x5e\\x4e\\x2d\\xe0\\xf8\\xf5\\xdf\\x21\\xbe\\x5e\\x70\\x3a\\xcc\\x94\\xa0\\xca\\x1a\\x6d\\x7d\\x24\\x44\\x35\\x4f\\x1f\\x32\\x23\\x83\\xc7\\x6c\\xbf\\x60\\x1f\\x1f\\xf0\\x61\\xe8\\x5f\\x27\\x94\\x3b\\x9b\\x9b\\x01\\xff\\x17\\x5f\\xd6\\xdd\\x17\\xfc\\xdf\\xef\\x82\\x63\\x28\\x77\\x46\\x5b\\xde\\xaa\\x30\\xbb\\xf7\\x5e\\x0f\\x93\\xff\\x3b\\xee\\xc0\\x66\\x04\\x6b\\x42\\x5d\\x56\\x28\\x19\\x62\\x94\\xa5\\x73\\x57\\xb9\\x38\\x08\\x77\\x95\\xbc\\x06\\x20\\x97\\x8f\\x0d\\x33\\x67\\x70\\xd0\\x07\\xbf\\x80\\xc7\\x1f\\x4f\\x4f\\x62\\xbb\\x05\\x82\\x21\\x0b\\x31\\xf6\\x68\\x76\\x41\\x48\\x3a\\xc1\\x5c\\xce\\x35\\x37\\x22\\x4b\\x7c\\x94\\xd8\\xf3\\x59\\x16\\xde\\x69\\x91\\x03\\x10\\x5c\\xe4\\x14\\xda\\x1a\\x5f\\x30\\x42\\xa1\\xa2\\xe4\\x9d\\x0d\\x71\\x2f\\x98\\x3c\\x66\\x19\\xfb\\x30\\xd4\\x82\\x45\\xb5\\x58\\x89\\x6e\\x63\\xf4\\xc2\\x79\\x34\\xd3\\xe3\\x77\\xde\\x65\\x83\\xa9\\x02\\x5f\\xee\\x02\\xcf\\xa9\\x85\\x11\\x88\\x49\\x6d\\x94\\xa9\\xc8\\x8a\\xbc\\x77\\x79\\x97\\x52\\x5e\\xbf\\x10\\x67\\x64\\xb5\\xa2\\x8b\\xf7\\xde\\x5a\\x60\\xfc\\xcf\\x0c\\x1e\\xda\\x00\\x26\\x53\\xd0\\xec\\xdf\\x87\\x7c\\xe7\\xb8\\xfd\\xce\\x73\\xe0\\x27\\x4e\\xcb\\x4a\\x81\\x64\\xef\\x2f\\x58\\xe2\\xde\\x3f\\xf6\\xae\\x92\\x48\\xaf\\x7f\\xab\\x70\\xbc\\xfb\\x40\\xdf\\xbb\\x59\\xe1\\xd6\\x8f\\xc2\\x45\\x60\\xd3\\x18\\x78\\x1f\\x25\\xcd\\x65\\xa2\\xa5\\xc9\\x28\\x75\\xa3\\x4b\\xdb\\x6e\\xa8\\x45\\x47\\xda\\x0e\\xb6\\x98\\x78\\xa6\\xd2\\x29\\x9e\\x64\\xa3\\xf7\\x12\\xc6\\xc2\\x13\\x87\\x65\\x24\\x3d\\x17\\x66\\xb3\\x56\\xcf\\x69\\x84\\xec\\x42\\xc7\\xc4\\x2d\\xd3\\xdb\\x74\\x79\\x71\\xe2\\x81\\x21\\xbd\\x93\\x1e\\x12\\x1e\\xca\\x8b\\xcb\\x55\\x8f\\x90\\x7b\\xe1\\xb4\\xc0\\x6d\\xf4\\x9b\\x60\\x06\\x0f\\xbd\\x93\\x0a\\xc9\\x48\\x64\\xde\\x97\\x46\\x25\\x83\\x85\\x23\\x16\\x4c\\x79\\xaa\\xa9\\xfb\\xd3\\x55\\xd6\\x9b\\x86\\xb8\\x41\\x78\\xcf\\x8c\\xad\\x10\\xe3\\x9e\\x39\\xb4\\x4f\\x5d\\x21\\x19\\xd5\\xbe\\xd5\\xf3\\x44\\xa7\\x15\\x36\\x8c\\x35\\xe7\\x43\\xf3\\x02\\x44\\xb9\\xa6\\xb9\\x93\\xc6\\xf3\\x46\\xa8\\x41\\x02\\xe5\\xe6\\x03\\xdf\\x39\\x7d\\xac\\x2b\\x82\\x9b\\xa8\\xb9\\x1a\\x69\\x2e\\x90\\x4b\\xea\\x27\\xfe\\xca\\x1e\\x61\\x96\\x33\\xc6\\x91\\xc5\\x73\\xb1\\x8d\\xe3\\xf7\\x48\\xba\\xd4\\x34\\x18\\x61\\x99\\x21\\xbc\\xd1\\x78\\xa0\\xa1\\xda\\x0f\\x88\\x66\\xe5\\x52\\xb8\\x1e\\xed\\x9a\\xd1\\xd0\\xed\\x9d\\xf0\\xe4\\xba\\x65\\x56\\xbe\\x70\\xc4\\xa3\\x59\\x04\\xad\\xc4\\x5e\\x43\\x01\\x97\\x23\\xf8\\x33\\x8a\\xe0\\x56\\x38\\x2d\\x8a\\x63\\x9b\\xcb\\x35\\x23\\x45\\x80\\xb3\\xbd\\x9d\\xd6\\x2a\\x98\\x75\\x29\\x1c\\x22\\x8f\\x0c\\xef\\x5a\\x3d\\x04\\x6e\\xf8\\x3b\\x77\\xcc\\x27\\x26\\x5f\\x6b\\x30\\xf8\\x36\\x3e\\x6f\\x33\\x2a\\x17\\x1e\\x33\\xb8\\xee\\x14\\xd5\\x0d\\x02\\xdf\\xe0\\xec\\xf1\\x44\\x9a\\xcf\\x09\\x4f\\xb4\\x80\\x59\\xb0\\xe4\\x0b\\x27\\xd7\\x9d\\x08\\x2d\\xa9\\xaa\\xeb\\xc4\\xd7\\x66\\x4c\\x12\\xcb\\x6c\\x82\\xa3\\xbb\\xf5\\xc9\\xf1\\x86\\x9d\\x4c\\xfc\\x81\\xaa\\x9d\\xba\\xeb\\xfe\\x43\\xfa\\x0f\\x74\\x5b\\x38\\x8f\\x0c\\x19\\xfb\\x23\\xfd\\x4f\\x77\\xe2\\x2d\\xc9\\x53\\xc0\\xd0\\x3f\\x7e\\xa4\\xc3\\x66\\x72\\x6d\\x9d\\x93\\x8f\\x5c\\x30\\x14\\xea\\xf9\\x3d\\x0a\\x9a\\xca\\x2e\\xd3\\xdc\\x3b\\x6a\\x9b\\xd6\\xe2\\xde\\xbf\\x28\\xe8\\x84\\x90\\xf1\\x72\\xb9\\x1c\\x6e\\xa7\\x8f\\x26\\x88\\x8b\\x58\\x39\\x9e\\xb3\\x3c\\x32\\xa3\\x2d\\x99\\x68\\xf4\\x37\\xf3\\x03\\x50\\x3e\\x58\\xc9\\xab\\x0a\\xc7\\xc3\\x99\\xe4\\xf8\\x8e\\x62\\x94\\x50\\x30\\xcd\\x98\\x30\\xe8\\xee\\x51\\x85\\x71\\x55\\x40\\x83\\x10\\xa8\\x76\\xd4\\x18\\x29\\xdb\\x96\\xbc\\x6c\\x4d\\x2e\\x1c\\xc6\\xa2\\xa2\\x1c\\x21\\xd0\\x0e\\xaa\\x15\\xc6\\xd4\\xe4\\x20\\xe8\\x76\\x96\\x53\\x51\\x1e\\xae\\x87\\x43\\xa7\\xdb\\x9a\\x4b\\x42\\xc7\\xad\\x7e\\xf3\\xf6\\x36\\xc1\\x94\\x2d\\xb7\\x6c\\xd1\\x1b\\x4f\\x54\\xef\\x9e\\xff\\xad\\x7a\\x7b\\x20\\xc9\\xb7\\x9a\\x8b\\x25\\xe7\\x64\\x3d\\x91\\x2c\\x05\\x61\\x0c\\x0f\\x22\\x76\\xb6\\x30\\x4d\\xe0\\x60\\x55\\x09\\x8d\\x9a\\x12\\x68\\x74\\x58\\xfa\\x56\\xb2\\x30\\x35\\x33\\x4d\\x14\\x26\\x09\\xc7\\x52\\xbd\\xb3\\xb4\\xec\\xd9\\x9f\\x16\\x3e\\xf0\\xc4\\x0f\\xaa\\x56\\x4d\\xfb\\x47\\xdb\\x59\\xe5\\x71\\x81\\xb9\\x43\\xd3\\x3b\\x97\\x17\\x0a\\xca\\x97\\xfc\\xc8\\xf4\\xe4\\x3e\\x90\\xfc\\x51\\xba\\xab\\x09\\xb1\\xc1\\xc6\\x8f\\xdc\\xc8\\x7a\\x2d\\x9f\\xde\\xdb\\xb3\\x99\\x6a\\x2b\\xd3\\x23\\xb7\\x50\\xb0\\x3c\\xcf\\x1f\\x58\\x65\\x3c\\x67\\xa1\\x55\\x65\\x64\\xe5\\x58\\xf0\\x13\\x64\\x9e\\xcb\\xb0\\x85\\xa7\\xa0\\x32\\xf8\\xae\\x97\\x20\\xd6\\xdb\\x81\\xd8\\x2c\\xdc\\x0e\\x4b\\xdc\\xd2\\xb1\\x66\\x2d\\x26\\xbd\\xf6\\x00\\xc5\\xe2\\xd6\\x7a\\x95\\xe3\\xd2\\xe2\\x2e\\xa8\\x30\\x4b\\x30\\x21\\x7e\\xbf\\xc1\\xaa\\xaa\\xf5\\x51\\xea\\xfd\\x4a\\xde\\x0f\\xae\\x47\\xa5\\x15\\x92\\xe5\\x96\\x95\\xb4\\x14\\xf8\\x28\\x9d\\xb4\\xb6\\x3d\\x42\\x14\\xa1\\x5e\\x9e\\x9e\\xff\\x9c\\xb5\\x6e\\xed\\xa9\\xf6\\xf2\\x95\\xb4\\x23\\x3b\\x2c\\xfd\\x2b\\x29\\x70\\x46\\xd3\\xdb\\x81\\x16\\x0e\\x9c\\xd7\\xee\\x59\\xed\\x3f\\x19\\x48\\x3b\\x72\\x6b\\x59\\x4d\\x23\\xfc\\xb0\\xec\\xab\\xbd\\x97\\x12\\x2c\\x07\\x5b\\x93\\x00\\xcc\\xfe\\xf4\\x6d\\x85\\xcc\\x13\\xf4\\x4d\\x6a\\x13\\xe4\\x20\\x81\\xbc\\x82\\xad\\xb0\\x63\\x3f\\x41\\x32\\xad\\x8f\\x84\\x66\\x2f\\x6d\\xe6\\x46\\x3f\\x0e\\x5a\\x40\\xf8\\x4e\\x06\\xe3\\x68\\x1b\\x18\\x5b\\x28\\x9e\\x3f\\x20\\x76\\x07\\xb1\\x70\\x54\\xaf\\xca\\x1d\\xf6\\x91\\x00\\x41\\xaf\\xa8\\xad\\x2a\\x97\\x9e\\x1a\\xfa\\x94\\xb7\\xe3\\x7b\\x0e\\xed\\x53\\xd2\\xae\\x3f\\xb8\\xa6\\x91\\x81\\xfb\\x7f\\x12\\x73\\xc2\\xce\\x74\\x1d\\xed\\xab\\x49\\xe4\\xdc\\x78\\x3b\\xe2\\xc4\\x7b\\x0f\\x76\\x06\\x63\\x01\\x97\\x74\\xcb\\x8e\\x38\\x4c\\xed\\xa8\\x6b\\x53\\xe4\\xb0\\xb0\\x32\\xa0\\xd7\\xb6\\x7a\\x9c\\xf8\\x94\\x3f\\x2f\\xbd\\x19\\xc9\\xf0\\xcc\\x53\\xef\\xe1\\x3c\\xa5\\x93\\xa3\\x52\\x9c\\xa0\\xe1\\x9e\\x53\\x10\\x8b\\xcc\\x74\\x1d\\x0e\\xc4\\xd1\\x46\\xa0\\x22\\xe8\\x15\\x87\\x90\\xc2\\x92\\x98\\xbd\\x41\\xcd\\xa9\\x61\\x5f\\x32\\x08\\xf9\\x8e\\xca\\x9d\\x55\\x0b\\xe7\\x9a\\x5d\\x5d\\x5a\\x20\\xd4\\xf7\\xe2\\xc3\\xce\\x10\\x6f\\x2c\\xa7\\x7e\\x10\\x9c\\xb6\\x9e\\x5c\\x1e\\x29\\x67\\x57\\xb5\\xdd\\xa7\\xea\\xdc\\x99\\xf8\\xe4\\xaa\\x65\\x72\\x7a\\xcc\\x64\\x4b\\x65\\x6b\\xe1\\x16\\x0e\\x0f\\x41\\xaa\\xab\\xd7\\xda\\xee\\x29\\x78\\x58\\x48\\x1b\\xc2\\x72\\x5e\\xfe\\x0a\\x11\\xb0\\x90\\x7e\\xf0\\x39\\x4d\\xe5\\xf3\\x4d\\x06\\xe1\\xc5\\xc4\\x8b\\x04\\x52\\x82\\xba\\x3c\\x84\\x71\\x02\\xe8\\x43\\x21\\xd4\\x56\\xf9\\x86\\x78\\x4e\\xec\\xf0\\x5e\\xb5\\xf0\\x90\\x26\\x87\\xbd\\x2c\\xdd\\x49\\x32\\x3d\\xfe\\x94\\x85\\xd5\\xcb\\x5e\\xb2\\xa0\\x04\\xaa\\x8d\\xdb\\x18\\x88\\x76\\xf4\\x59\\x12\\xe4\\x73\\x8e\\xa0\\x51\\x88\\xb2\\xd0\\x94\\x39\\x08\\x4a\\xf6\\x70\\x55\\x10\\x63\\x82\\x51\\xfd\\xf4\\x70\\xbd\\x5e\\xc9\\x73\\x2f\\x5d\\xb7\\x0b\\x06\\x8a\\x6e\\xca\\x60\\xc2\\x0d\\xee\\xe5\\x83\\xc6\\xef\\xc5\\x62\\x5b\\xee\\x8a\\x06\\x2d\\xdf\\x43\\x6d\\x8f\\x46\\x19\\x96\\x1d\\xb7\\xf4\\x85\\x23\\x8c\\x9b\\xe1\\x24\\x00\\xe7\\x41\\x89\\x74\\xab\\x75\\x40\\xde\\xa7\\xba\\x1f\\x8a\\x79\\x78\\xbb\\x01\\xf7\\x3c\\x9a\\x7a\\x93\\x03\\x25\\xe4\\xea\\xc6\\xfb\\x16\\x88\\xec\\x28\\xab\\xff\\x7e\\x75\\xba\\x80\\x73\\xfc\\x9e\\xad\\x92\\xf3\\xef\\x9d\\x9f\\xe1\\xcd\\x50\\x07\\x68\\x9e\\x66\\x8c\\xa9\\x65\\xa4\\xda\\xd6\\xf6\\x64\\xf9\\x26\\x88\\xa6\\xbf\\x89\\x6c\\xe2\\xbe\\x37\\xee\\x63\\x14\\xd6\\x0d\\x76\\xff\\x22\\x41\\x72\\xeb\\x21\\xe0\\x02\\x7e\\xe8\\x27\\x9e\\xea\\x46\\xef\\xca\\xa9\\x70\\x6a\\x7e\\xb3\\x75\\x6e\\x7b\\x11\\x49\\x9e\\x6a\\xb7\\x3a\\xe4\\x83\\xe1\\xec\\xcd\\xc9\\xb3\\xb6\\x35\\xd6\\x7e\\x48\\x7b\\x36\\x33\\xa7\\x0b\\x0f\\x45\\x13\\x08\\xd5\\x5c\\xcb\\xc4\\x12\\xd9\\xb7\\x7e\\x98\\x02\\xdf\\x56\\x7b\\xa8\\xcb\\x62\\xbb\\xb5\\x60\\xb8\\x39\\x8e\\xae\\x12\\xb0\\xd5\\xb8\\x54\\x74\\xa0\\xc5\\xd3\\x7e\\x48\\xfb\\x34\\xf1\\xef\\xdf\\xbf\\x7f\\xf7\\xcd\\xda\\xc8\\xd2\\x6f\\xdc\\xb4\\xf6\\xed\\x76\\xeb\\xed\\x9c\\x68\\x1e\\x2d\\x01\\x74\\x01\\x53\\x35\\x9d\\xde\\x95\\x61\\x5b\\x0f\\xd3\\x08\\xa3\\x5f\\x3b\\xe9\\x2b\\xa5\\x66\\xed\\x50\\xcd\\xa0\\x5b\\xa2\\xf0\\x30\\x47\\xcd\\xa4\\x57\\x70\\x0e\\x5a\\x65\\x42\\xf4\\xf6\\xa9\\xae\\x02\\x0b\\xfd\\x3e\\x97\\x58\\xdf\\x03\\x40\\xbf\\x47\\x07\\xbb\\x80\\x9f\\x46\\x06\\x71\\x24\\xce\\xc2\\x4b\\x2b\\xd8\\x06\\x08\\xac\\xd7\\x2c\\x67\\x53\\x9b\\xd6\\x80\\x68\\x0a\\x67\\x74\\xce\\xef\\x0d\\xab\\xce\\xc7\\xe5\\x01\\x39\\x00\\x3b\\x20\\xe8\\x11\\x6d\\xc0\\x0d\\x22\\x8b\\x93\\xb6\\xfd\\xc1\\x4b\\x4e\\xdc\\x62\\xc2\\x83\\x2c\\x30\\xcf\\x60\\x72\\xbf\\xcd\\x24\\x51\\x62\\x20\\x4b\\xca\\xf7\\x75\\x16\\xe8\\x16\\x68\\x0f\\xbc\\x2d\\x9c\\xd8\\x3c\\x94\\xe8\\x02\\x6f\\x02\\xa8\\xd5\\x4a\\xaa\\x60\\x3f\\x52\\x42\\x9d\\x2a\\x39\\x83\\xad\\xf8\\xd4\\xac\\x1c\\xea\\x0c\\xbd\\x58\\xc6\\x8a\\xd5\\xf3\\xb3\\x62\\xc5\\x77\\xf2\\xd7\\x17\\x54\\xaa\\x8d\\xf8\\xf3\\x41\\xf4\\xe7\\x2e\\x9a\\x32\\xe6\\x4c\\x1c\\xe2\\x9a\\x20\\x8f\\x89\\x65\\x43\\x30\\x61\\x21\\x0b\\xeb\\xa0\\x25\\xe8\\xed\\xab\\xad\\x39\\x69\\x9e\\x25\\xa8\\x4d\\x87\\xb8\\x5d\\x65\\x78\\xb9\\xe0\\xf4\\x40\\xde\\xd3\\xa2\\x6d\\x25\\x71\\xfb\\xd6\\x30\\x62\\xda\\x01\\x3b\\x1e\\x60\\xd3\\xe0\\xac\\x34\\xca\\xd8\\x83\\xa6\\x50\\xc8\\x3f\\x3e\\xe2\\xed\\xf4\\xa0\\x5e\\xd0\\x47\\x07\\x05\\xdf\\x51\\xfd\\x47\\x40\\xd3\\xa9\\xb2\\x65\\x4c\\x84\\x59\\xb0\\x1d\\x07\\xb7\\x5f\\x58\\xa2\\x33\\x39\\x2b\\x99\\x59\\xb1\\x0b\\xcd\\xf8\\x1c\\x4b\\xe6\\x06\\xad\\x38\\x81\\xad\\xee\\x9c\\x63\\xef\\x23\\x17\\x64\\xbd\\x72\\x30\\x89\\x33\\x35\\x1f\\xcc\\x97\\xd0\\x67\\x76\\x3e\\xf1\\x0d\\xfa\\x07\\x46\\x20\\xd3\\x5f\\xf7\\x7e\\xa5\\x20\\x82\\xdb\\x28\\xdd\\xf3\\x73\\x51\\xab\\x7b\\xcd\\x34\\xd5\\x53\\xc4\\xe4\\x1f\\x26\\x3f\\x62\\x1a\\x19\\x87\\x88\\x25\\xf1\\x66\\xe1\\xb1\\xd6\\xdc\\x72\\x50\\x02\\x20\\xdb\\xee\\x6a\\x94\\xa5\\xa2\\x57\\x0c\\x06\\x33\\x2d\\x3d\\x7f\\x35\\xe2\\xb4\\x2c\\xfe\\x1f\\x06\\xc7\\xb9\\xfc\\xf2\\xba\\x0e\\xa2\\x3a\\x28\\x82\\x7a\\x6f\\xa7\\x48\\xb7\\x1b\\x7c\\xa8\\xa2\\x77\\x4e\\xd9\\x35\\x8f\\x6c\\x58\\xb4\\x8f\\xd8\\x26\\xda\\x8f\\x83\\x17\\xa4\\x62\\x5b\\x48\\x32\\x48\\xf4\\x81\\x6c\\x03\\xfc\\x91\\x4d\\x46\\x0b\\xcf\\x5d\\x40\\xd9\\xde\\xc0\\x72\\x80\\x4e\\xda\\x1a\\x61\\x77\\xb8\\x74\\x90\\x1f\\x39\\x1e\\x97\\xea\\x22\\xc7\\x7c\\x9e\\xb9\\x06\\xd3\\x7c\\x90\\xd0\\x75\\x81\\x5d\\xaf\\x34\\x3c\\x94\\xa0\\x82\\xb5\\x4e\\xa3\\xe4\\xbe\\xc0\\x3f\\x8c\\x1a\\x1a\\x78\\x3f\\x98\\xa3\\x64\\x20\\x24\\x2b\\xbd\\xb6\\x80\\xbc\\xad\\xac\\xc6\\xc1\\x76\\xe7\\xed\\x9d\\x9e\\x39\\x81\\x91\\x63\\xe0\\xfe\\xcc\\x42\\xcb\\x3b\\xb1\\x9a\\xe7\\xa7\\x3f\\x4f\\xe7\\x3e\\x1f\\x9d\\x65\\x1d\\x98\\xfe\\x05\\x29\\x70\\x18\\x81\\x56\\x23\\xa2\\xc3\\x70\\x50\\x25\\x08\\x5a\\x60\\xd3\\xa6\\x9f\\x6f\\x26\\xc1\\xe6\\xb0\\x07\\x32\\xed\\xe1\\x44\\x1a\\xc7\\x7b\\x12\\xe9\\x15\\xb3\\xa5\\x9e\\x00\\x1b\\x3c\\x01\\xc5\\x4d\\x8f\\x5b\\x0a\\x2a\\x0f\\xc2\\x00\\x7a\\xb3\\x61\\x1a\\x86\\x49\\x6c\\xaa\\xf6\\x87\\x6c\\xa3\\x05\\x0a\\xa7\\xf6\\xca\\xc4\\x73\\xdf\\xdd\\xe0\\x8e\\x6b\\x39\\x59\\x22\\xeb\\x98\\xfa\\x55\\xa7\\xa3\\xb0\\xde\\xf2\\x59\\x77\\x54\\x10\\x13\\x87\\x3e\\x93\\x89\\xb3\\xa7\\x63\\x8f\\xb6\\x2b\\x35\\x54\\x6a\\xb1\\xa4\\x7a\\x79\\x41\\xe6\\xb4\\x0f\\x21\\xc3\\xeb\\x9f\\xa6\\x53\\x83\\x5e\\xcf\\xed\\x44\\x3a\\xc5\\x9a\\xf4\\x0a\\x19\\xdb\\x22\\x0c\\x9c\\xfa\\x95\\x82\\xbf\\x15\\x1c\\x95\\xbd\\x63\\x8b\\x1b\\x8f\\xf5\\xfd\\xb7\\x52\\x51\\xd9\\x73\\x3b\\x7a\\x16\\x38\\xfd\\xbd\\x60\\x6e\\x2b\\x26\\x41\\xb0\\x1d\\x71\\x0a\\xbe\\xed\\x13\\x20\\x3c\\x0a\\x4b\\x56\\xf7\\x7b\\x77\\xad\\x0f\\x61\\x67\\x0e\\x47\\x00\\xfb\\x4e\\xca\\xf2\\x7a\\xa7\\x64\\xe3\\x27\\x88\\x64\\x9a\\x99\\x7a\\x5e\\x65\\x81\\xce\\xea\\x7b\\x57\\x3e\\x3d\\xc1\\x6d\\xe2\\x64\\x41\\x65\\x2b\\xb8\\x65\\x04\\x32\\x49\\x4f\\xcf\\x4f\\x6d\\x61\\x27\\x04\\x97\\x50\\xa0\\x43\\x76\\x34\\xce\\x89\\x84\\x26\\x50\\xc6\\xd1\\x5e\\xc4\\xd8\\xe2\\x26\\xc4\\x61\\x26\\x74\\xd8\\x88\\xf6\\xa8\\x97\\x48\\xa1\\x5f\\x90\\xe9\\x65\\x78\\x35\\x6e\\xee\\x89\\xcf\\xe9\\x4f\\x87\\xf2\\x32\\x1c\\x50\\xc2\\xe8\\x48\\x83\\xba\\x17\\xb2\\x12\\xf6\\xdb\\xbb\\x67\\x9a\\x3d\\x1f\\x0c\\xe7\\x1e\\xaf\\xd0\\x0d\\x83\\x34\\x3e\\xa6\\x96\\x20\\x1f\\x8c\\xc8\\x21\\xd3\\x26\\xae\\x33\\x77\\x38\\x91\\x70\\xc3\\x94\\x5b\\xa8\\xa0\\x4f\\x69\\x86\\xad\\xc7\\xa0\\xfd\\x02\\x65\\xc0\\xb4\\xec\\xef\\x2b\\x47\\x1c\\x19\\xd4\\x53\\x62\\x9e\\x55\\x3b\\x55\\x45\\xb1\\x9f\\x7c\\x65\\x60\\xd9\\x54\\x25\\x98\\x4d\\xa3\\x2d\\x13\\x21\\x90\\xae\\x53\\xe2\\xaa\\x7b\\xcb\\x4b\\xe8\\xd8\\x87\\x54\\x05\\x72\\x95\\xbc\\xf2\\xa5\\x8b\\x32\\x5e\\x7b\\x7e\\x23\\xd5\\x1c\\x19\\xc7\\x4b\\x7a\\x61\\x44\\xc5\\x96\\x70\\x32\\xfd\\xaa\\xef\\xc1\\x6c\\x99\\x55\\x07\\x47\\x8e\\x30\\xed\\xed\\x3e\\xdd\\x91\\x35\\xca\\x60\\x94\\x80\\x8e\\xc3\\x0b\\x67\\x30\\xbb\\xaa\\x09\\x1c\\x74\\xa9\\x2b\\x39\\xa6\\x7e\\x7b\\x2e\\x43\\x78\\xe3\\x8c\\xad\\x14\\x51\\xc7\\x4d\\xd9\\x0c\\xa1\\x40\\xc2\\x70\\x9e\\xf6\\xde\\xce\\x0b\\xcd\\x45\\xf0\\xb0\\x50\\xb8\\x06\\x3e\\xea\\x44\\x19\\x36\\x2c\\x75\\xc2\\xf7\\xfd\\xfb\\x91\\xd3\\x16\\x65\\xd6\\x10\\x32\\xcc\\xbd\\x3a\\x91\\x35\\x94\\xb3\\xac\\xbf\\x53\\xd6\\x09\\x31\\x14\\x16\\x70\\xd6\\xd7\\x52\\xd6\\x89\\x62\\xc1\\x8e\\x63\\xfa\\x7c\\xf7\\x8a\\xc3\\x03\\x03\\x83\\x4e\\x7b\\x1c\\xfa\\xa8\\x6b\\xa2\\xb2\\x2b\\xcd\\x12\\x3f\\xdd\\x8b\\x1a\\xed\\x93\\x65\\x75\\xcc\\x93\\x6a\\xf7\\x3b\\xd4\\x8a\\xe3\\xac\\xcc\\xe8\\xc1\\x95\\x87\\x66\\x07\\x92\\x9d\\x66\\xf5\\xa2\\x9e\\xee\\x6d\\x39\\x65\\x67\\x4e\\xfe\\xce\\xc8\\xde\\x0b\\xe0\\xca\\xc8\\x6d\\x0f\\x1e\\xc0\\x21\\xcb\\x51\\x89\\x82\\x42\\xaf\\x9f\\xe8\\xf6\\xc5\\xc8\\x78\\x95\\x33\\x47\\x90\\x41\\xb7\\x39\\xf2\\x1b\\x5b\\xcc\\x87\\x38\\x8e\\x5a\\x4e\\x46\\x3e\\xd6\\x72\\x8f\\xd1\\x5b\\x6f\\xd7\\x0c\\xbc\\x1f\\xed\\x1d\\x1c\\xdd\\xdb\\x05\\x31\\xdc\\xef\\xe3\\xe6\\x15\\x96\\xc2\\x3e\\x37\\xb7\\xf5\\xa1\\x82\\xd1\\xc0\\xec\\xfd\\xe6\\xe6\\xc7\\xe2\\x86\\x2c\\xba\\xc6\\x19\\x75\\x73\\xe2\\x0f\\x64\\x1b\\xd5\\x03\\x5d\\x44\\x1c\\x76\\x58\\x68\\x88\\xfa\\xc5\\x73\\x4f\\x09\\x06\\x42\\x3e\\x57\\xdb\\xd8\\xad\\x80\\x2d\\x23\\xa1\\x09\\x35\\x1f\\x38\\x31\\x93\\x6f\\xb7\\x30\\xfb\\xa1\\x9f\\x49\\x02\\x9f\\x2f\\xca\\x2d\\x99\\xc2\\x9a\\x38\\x44\\x09\\x96\\xd8\\x74\\xd5\\x82\\x4a\\xc0\\xaf\\xac\\xf8\\xe1\\x12\\x85\\xf7\\x73\\xe8\\x17\\x4c\\x14\\x6f\\x7d\\x50\\x54\\x70\\x03\\x57\\xfb\\x72\\x10\\xcf\\x97\\x2d\\x85\\xbb\\xdd\\x1c\\x18\\xcb\\x8f\\x0b\\xd6\\xa1\\xc9\\x65\\x23\\x54\\x1b\\xba\\xf1\\x59\\x6e\\x2e\\xd9\\xec\\xd3\\xd4\\x0e\\xa2\\xec\\x6c\\xbf\\x6f\\x9c\\x1d\\xfb\\xa3\\x71\\xb9\\x5f\\xa6\\x6b\\x16\\xf4\\x40\\x8e\\x7a\\xc2\\xbb\\x15\\xae\\xe2\\xca\\x01\\x0d\\x65\\xcd\\x35\\x0c\\x74\\x97\\x03\\x4c\\xb9\\xad\\x9f\\x80\\x0b\\xdf\\xc0\\x60\\xba\\xad\\x30\\xea\\xdf\\x39\\x50\\x6c\\xc6\\x6d\\xff\\x5f\\x4e\\x99\\xc1\\xca\\x5f\\x93\\xb1\\x5e\\x49\\xfd\\x30\\xa3\\xfd\\x5e\\xa8\\xe8\\xaf\\xfe\\x50\\xba\\x38\\x2e\\xea\\x7c\\x51\\xaa\\xfa\\x58\\x66\\x51\\xcf\\x8f\\x7a\\x07\\xf3\\xe5\\x2b\\xe6\\x8f\\xaf\\x98\\xaf\\x5f\\x31\\x7f\\x7e\\xc5\\xfc\\xfd\\xc8\\xbc\\x90\\x59\\xb0\\x3c\\x70\\x3e\\xac\\xc3\\xc6\\xc9\\xd8\\xaf\\x5f\\x77\\xaa\\x24\\x30\\x1d\\x7e\\x67\\x5e\\x99\\x82\\x6c\\xd6\\x36\\x45\\xed\\x06\\x3f\\x25\\x54\\x0b\\xf4\\xfb\\xe3\\x99\\x5d\\x88\\x0e\\xf2\\x78\\x38\\x1b\\x8c\\x5c\\x0c\\x4e\\x2d\\xf9\\xde\\x0d\\x50\\x37\\x0c\\x94\\xea\\xd8\\xee\\xbc\\x8d\\x9b\\x19\\xd1\\x81\\x85\\xb1\\x33\\x79\\xaf\\x37\\xd9\\xad\\x86\\xda\\xea\\x11\\x56\\x54\\x3d\\x97\\x34\\x88\\x1b\\xd8\\x1a\\x7b\\xf4\\x94\\xe3\\xa4\\xa2\\xa3\\xd2\\xf3\\x7c\\x42\\x65\\x8d\\xa1\\xd7\\x4e\\x72\\x44\\x34\\x35\\x2a\\xbf\\xf7\\x74\\x5f\\x8e\\x14\\x5e\\x9e\\x9e\\x7f\\x09\\x80\\x6e\\xb1\\xec\\x85\\x6e\\x23\\x45\\x22\\xbe\\x78\\xb3\\xf0\\xfc\\xe3\\xe9\\x81\\xda\\xe4\\x24\\x6c\\x5f\\x9a\\x68\\x0a\\xdc\\x9b\\x05\\x36\\x1a\\x20\\x48\\x34\\x0b\\x1e\\xdf\\x5e\\x28\\x81\\x7b\\x2f\\xdf\\xe7\\xc2\\xda\\x96\\x0b\\xe5\\x91\\xb3\\x6d\\xb1\\x82\\x1a\\x60\\x24\\x97\\x84\\xe0\\x55\\x31\\x87\\x2c\\xf1\\x6e\\xe1\\xf5\\x77\\x7f\\xb0\\x1a\\x0c\\xa5\\x15\\x6d\\x1a\\xdd\\xf3\\x8b\\xf7\\xfb\\x11\\xb9\\x06\\x93\\xea\\xf2\\x1e\\x7b\\xb4\\x4b\\x3c\\x84\\xc1\\xf4\\xeb\\x40\\x8d\\xf4\\xe0\\xfb\\xbd\\xbb\\x46\\x6d\\x98\\xfb\\xb5\\xd9\\xbc\\xa1\\x58\\xaa\\x6f\\x86\\x0d\\x0b\\x0b\\x68\\xe2\\xac\\xf9\\x72\\x2a\\xca\\x43\\xbb\\xab\\xd9\\xd3\\x58\\x45\\x25\\xd4\\xe7\\x5c\\x4f\\x05\\xa8\\x45\\xa5\\x8d\\x09\\xa5\\x30\\xab\\x5c\\x48\\x60\\x51\\x7e\\x67\\xad\\x7d\\x3e\\x8b\\x51\\xdd\\xc9\\xf6\\x1b\\x2f\\xfd\\x92\\xa9\\x85\\x87\\x3b\\x7f\\x05\\xcd\\x35\\xbd\\x75\\x65\\x0c\\xe5\\x31\\xed\\x52\\x30\\x3a\\x5e\\xd9\\xa8\\x25\\xe0\\xad\\xdf\\xa8\\x29\\x78\\x7d\\xbc\\x68\\x50\\x96\\xd2\\x4b\\x7a\\xed\\xf1\\x15\\xd8\\x54\\xd6\\x85\\xf5\\xa0\\x11\\x6a\\xe1\\x91\\x40\\xf7\\x4e\\x18\\xbd\\x0c\\x36\\xd6\\x5e\\x59\\xb1\\xf9\\x85\\x01\\x62\\x64\\x32\\x0b\\xdb\\x06\\x00\\x86\\x33\\x2b\\xec\\x8f\\xce\\x39\\xaa\\xa5\\x17\\x8c\\xa4\\xe9\\x5e\\x0e\\xee\\xf3\\xa7\\x9f\\x6d\\x08\\xf3\\x49\\x5d\\x46\\xfd\\xac\\x70\\x1c\\x40\\x53\\x16\\x59\\xe1\\xf8\\x70\\x9e\\x9c\\x35\\x9f\\x5a\\x7e\\x54\\x1d\\x40\\xf2\\xfe\\xe2\\x5e\\xc7\\x7a\\x79\\x55\\x4b\\xec\\x56\\x1d\\x1b\\xb7\\xfd\\x86\\x47\\xf6\\xe5\\x60\\xbc\\x34\\x86\\xcc\\xf2\\x07\\xc6\\xa3\\x46\\xbf\\x0b\\xd3\\xc6\\xd2\\x39\\x0b\\x06\\xaa\\xf9\\x51\\x67\\xe4\\x5c\\xf0\\xb1\\xa3\\x83\\x97\\x0e\\x66\\x6c\\xb3\\xee\\xa0\\xd0\\x58\\xf9\\x25\\x35\\x5f\\xde\\x62\\x8a\\x92\\xa1\\xd4\\x33\\x9a\\xda\\x09\\xad\\x2c\\x3b\\xcc\\x7d\\xce\\x6a\\xa0\\xb0\\x98\\x7e\\xd6\\x94\\x4b\\xae\\x73\\xdb\\x99\\x15\\xce\\x45\\x9f\\x2a\\xe4\\x86\\x2d\\xab\\xee\\x57\\x36\\xaa\\x81\\x9e\\x94\\x16\\xad\\xf7\\x98\\xb4\\xda\\x04\\x14\\x3a\\xf0\\xc2\\xd2\\x53\\x30\\xa7\\xea\\x54\\x4b\\x7e\\xf4\\x7d\\x5f\\x83\\xc1\\x24\\xc0\\x22\\x18\\x41\\x84\\x2d\\x32\\xed\\x6a\\x81\\xa6\\x9a\\x65\\xe5\\xd6\\x40\\x1b\\xa6\\xf7\\x0b\\x23\\x35\\x38\\xf2\\x54\\xee\\x57\\x7b\\x6a\\x88\\x09\\x0d\\xe9\\x02\\x93\\x43\\x81\\xe2\\x35\\x16\\xf2\\xa8\\x46\\x64\\x3d\\x28\\xac\\x8f\\x54\\x7d\\x94\\xf5\\x5e\\x5b\\xf3\\x88\\x14\\x1a\\xe7\\x16\\xc1\\x62\\x19\\xed\\x1a\\x16\\xea\\xb5\\xaa\\x4d\\xce\\x9b\\x0e\\x6d\\xe5\\x0b\\x36\\x70\\x64\\x6a\\x47\\xfd\\x1b\\x24\\xb2\\x7c\\x33\\x3d\\xb1\\xb3\\xa1\\xe3\\x96\\xfd\\xec\\x37\\xde\\x36\\x41\\x68\\xb1\\x08\\x12\\xdc\\x04\\x59\\xb3\\x75\\xdc\\xeb\\x0a\\x1b\\xe6\\x00\\x0d\\x0c\\xf6\\x17\\x10\\x2c\\x09\\xda\\xcf\\x23\\x9a\\xb4\\xdd\\x68\\x50\\x3d\\x4c\\xbb\\x5f\\x36\\xe9\\xdc\\x9e\\x8b\\xb8\\xd3\\x0f\\x17\\xd4\\x3f\\xfc\\xbe\\x6d\\x23\\x74\\x62\\xca\\x8d\\x6c\\xe1\\x44\\xa0\\x34\\x86\\xf1\\xf3\\xa6\\x8d\\x0d\\xb0\\xc1\\x9c\\xfb\\xd6\\xdf\\xd8\\x58\\xde\\xb0\\x39\\xd3\\xed\\xb6\\xa2\\xab\\xc6\\x76\\x03\\x6d\\xb7\\x3a\\x1d\\x09\\x9d\\xcb\\x8f\\x37\\xd8\\xa0\\x63\\xcc\\xce\\x00\\xe7\\x22\\x44\\x4c\\x06\\xf3\\x28\\xd5\\x5f\\x4c\\xbe\\xd7\\xcb\\x2e\\xd8\\x7f\\x1e\\x32\\x46\\x77\\x84\\x58\\x17\\x9c\\x94\\xc4\\x0f\\xa1\\xf6\\xb3\\xff\\xfe\\x9b\\xbd\\x0f\\x37\\x18\\x84\\x7b\\x5f\\xd5\\x32\\x6b\\xc7\\x75\\x8e\\xf7\\xf0\\x6d\\xec\\xbf\\x0f\\x02\\xd1\\x3e\\xde\\xd2\\xb8\\xed\\x18\\xe9\\xb1\\x60\\x3f\\x47\\xbe\\x10\\x60\\xfa\\x20\\xca\\x32\\x3c\\xc2\\x4b\\x3e\\x06\\xd2\\x4f\\xd2\\xb3\\x6f\\x1a\\x35\\x39\\x8e\\x78\\xd4\\x18\\x2f\\x7d\\xe8\\x0e\\x47\\xf2\\xb0\\x4d\\x8d\\xe2\\xb4\\x40\\x6e\\x7b\\xe8\\xfd\\xae\\x92\\x4e\\x78\\xe9\\x1f\\x72\\xb0\\xf4\\x71\\x06\\x5c\\x38\\x59\\xea\\x77\\x70\\xfa\\x2f\\x1c\\x69\\x6e\\xf6\\xe3\\xe4\\x8c\\x3f\\x7e\\xcc\\x72\\xa9\\xf3\\x28\\xfa\\x5c\\x2e\\x2f\\xe3\\xd7\\x2c\\x5d\\x70\\xb9\\xbc\\x47\\x4f\\xd7\\x9f\\xef\\xbf\\xc6\\xba\\x5a\\x68\\xc5\\xb4\\xd3\\x35\\xa8\\xfe\\xef\\xe7\\x0f\\x33\\x69\\x88\\x0b\\xfc\\x84\\xe2\\xa2\\x9b\\x8d\\xff\\x05\\xcb\\x33\\x4c\\xcf\\xd3\\x86\\xab\\x7b\\x9a\\x7e\\x17\\x86\\x27\\x7b\\x16\\xfd\\xf8\\x0c\\x74\\x3c\\xf8\\xcb\\x6a\\xf0\\x17\\xbc\\x68\\xf3\\xdf\\x04\\x3b\\xba\\xbf\\x9f\\x34\\x7e\\x5b\\x0d\\x70\\xd5\\xeb\\x34\\xf9\\x25\\xe8\\xf4\\x5b\\xaf\\xb7\\xfd\\xc9\\x3e\\xad\\x9f\\xfb\\xd2\\x4e\\x5b\\x03\\xfa\\xc7\\x94\\xe7\\xc5\\xcc\\xe5\\x07\\xe0\\xf4\\x16\\x7f\\x62\\xc1\\x4f\\xaa\\xbf\\xf3\\x04\\xd3\\xfa\\xa6\\x7f\\x68\\xad\\x7f\\xc2\\xb2\\x4c\\xb7\\xd9\\xcf\\xf4\\xa5\\x96\\x7f\\x86\\x55\\xbf\\x4e\\x2f\\xf0\\x77\\x9e\\x65\\xc5\\x3f\\x88\\xc2\\x4f\\x98\\xde\\x70\\x9d\\x2f\\xf4\\x67\\x7a\\xfb\\xe2\\xe1\\x29\\xe2\\x0f\\x98\\x97\\xba\\xe3\\x17\\xc2\\x95\\x5f\\x60\\xc1\\x69\\x5a\\x1d\\x6d\\xfb\\xbf\\xf2\\x3f\\x79\\x02\\x4b\\xaf\\x70\\x83\\x4b\\xfd\\x39\\x7d\\x25\\x4d\\xb4\\x4e\\xb4\\x5e\\xe3\\xf5\\xef\\x02\\xf3\\xd3\\xf2\\xdc\\x5f\\xf1\\xe7\\x09\\xcc\\x75\\xb5\\xf9\\x5d\\x71\\xc5\\x19\\x5c\\xf8\\x05\\x9a\\x66\\x03\\x1c\\x7e\\xea\\x5f\\xf3\\x2b\\x5c\\xd7\\x2f\\xba\\x74\\x8b\\x9f\\xde\\x9e\\xe1\\x79\\xb2\\x7f\\xdc\\xb9\\x73\\x9f\\x7f\\x3d\\xab\\xd7\\x0d\\xfd\\xf4\\x4b\\xbf\\xb9\\xdf\\x10\\x26\\x40\\xf8\\x01\\x73\\xa0\\xf0\\xe9\\x73\\x75\\x99\\x26\\xfd\\x66\\x7e\\x98\\x29\\xe3\\xba\\x7c\\x32\\xa5\\x2e\\xd3\\xac\\xcd\\x9b\\xfb\\x33\\x01\\xbe\\xad\\xf3\\xf4\\xcc\\x9f\\x86\\xa0\\xcb\\x44\\x4b\\xa0\\x1f\\xf0\\xf6\\xfa\\x49\\x36\\x97\\x09\\x19\\x68\\x5f\\x9e\\xc1\\x3e\\xeb\\xe9\\xb7\\xf9\\x57\\x81\\xca\\x34\\x55\\xf3\\x17\\xdf\\xa6\\x39\\x2e\\x3f\\xba\\x5b\\x1d\\x06\\x01\\x78\\xb3\\x29\\x68\\xe3\\xfe\\x9a\\xf5\\x17\\x04\\xfc\\x53\\x3e\\x3e\\x2c\\x2a\\x01\\x20\\x19\\xf1\\x36\\xab\\x46\\xff\\x0a\\xbf\\xe8\\xb1\\x03\\x03\\xcb\\xa2\\x7f\\x4e\\x66\\x75\\x33\\xf8\\x3c\\x1b\\x7a\\x7b\\x8d\\xbf\\xa7\\x4f\\x9d\\x98\\x45\\xd6\\xe9\\x2f\\xd0\\xbc\\x86\\x5f\\xe9\\xa3\\xf8\\xbf\\x27\\xd0\\x18\\xc8\\x46\\x9c\\x26\\x63\\x5e\\xe7\\x5f\\xe6\\xb3\\xfc\\x4d\\x22\\x5e\\xc0\\x1f\\xa0\\xfd\\x21\\xd4\\x73\\x93\\xea\\x67\\xd0\\xfb\\xf2\\xcf\\x1b\\xcd\\x33\\x4c\\x6f\\x4f\\x30\\xe5\\xff\\x8e\\xe3\\x60\\x70\\x69\\x66\\x7c\\x82\\xff\\x3a\\xca\\x13\\x1e\\x3e\\x8b\\x0d\\x6e\\x7a\\x0d\\x76\\x39\\xf2\\x63\\x83\\xbf\\xcc\\xaf\\xfa\\x9f\\x85\\x33\\x1c\\x65\\xbb\\xb8\\xd7\\x34\\x6f\\xbf\\x46\\xb4\\xbb\\x7f\\x3c\\xa6\\x76\\xcc\\x4a\\x73\\x8f\\xc0\\xf7\\x95\\x52\\x4d\\x4b\\x3b\\xd0\\x77\\x8b\\xfa\\xf2\\xd7\\xd1\\xb5\\xe5\\x44\\x76\\x36\\x19\\x25\\x72\\xdd\\xb9\\x46\\x4e\\xe1\\x3d\\x5b\\xfa\\x91\\x31\\xb2\\xa5\\x07\\x73\\x0c\\x53\\x0e\\x9e\\x7e\\xab\\x96\\xb2\\xe1\\x4b\\x3f\\x98\\x6e\\xb0\\x6e\\xd0\\xbc\\xd0\\x0d\\x06\\x40\\x6e\\xae\\xe8\\x06\\x9b\\x80\\xc4\\xe6\\x1b\\x6f\\xc6\\x2b\\x41\\x7d\\xfd\\x81\\x51\\x90\\x53\\x81\\x17\\xea\\x8e\\xed\\xd6\\x7e\\xff\\xa2\\x7c\\x1a\\xb7\\xc4\\xc5\\x7e\\x37\\x66\\xdf\\x2e\\xc9\\xb5\\x67\\x04\\x10\\x37\\x40\\xf1\\x7f\\x01\\x00\\x00\\xff\\xff\\xf1\\x3c\\x8a\\xce\\xe9\\x3e\\x00\\x00\")\n\nfunc blacklistTxtBytes() ([]byte, error) {\n\treturn bindataRead(\n\t\t_blacklistTxt,\n\t\t\"blacklist.txt\",\n\t)\n}\n\nfunc blacklistTxt() (*asset, error) {\n\tbytes, err := blacklistTxtBytes()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tinfo := bindataFileInfo{name: \"blacklist.txt\", size: 16105, mode: os.FileMode(420), modTime: time.Unix(1541751116, 0)}\n\ta := &asset{bytes: bytes, info: info}\n\treturn a, nil\n}\n\n// Asset loads and returns the asset for the given name.\n// It returns an error if the asset could not be found or\n// could not be loaded.\nfunc Asset(name string) ([]byte, error) {\n\tcannonicalName := strings.Replace(name, \"\\\\\", \"/\", -1)\n\tif f, ok := _bindata[cannonicalName]; ok {\n\t\ta, err := f()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"Asset %s can't read by error: %v\", name, err)\n\t\t}\n\t\treturn a.bytes, nil\n\t}\n\treturn nil, fmt.Errorf(\"Asset %s not found\", name)\n}\n\n// MustAsset is like Asset but panics when Asset would return an error.\n// It simplifies safe initialization of global variables.\nfunc MustAsset(name string) []byte {\n\ta, err := Asset(name)\n\tif err != nil {\n\t\tpanic(\"asset: Asset(\" + name + \"): \" + err.Error())\n\t}\n\n\treturn a\n}\n\n// AssetInfo loads and returns the asset info for the given name.\n// It returns an error if the asset could not be found or\n// could not be loaded.\nfunc AssetInfo(name string) (os.FileInfo, error) {\n\tcannonicalName := strings.Replace(name, \"\\\\\", \"/\", -1)\n\tif f, ok := _bindata[cannonicalName]; ok {\n\t\ta, err := f()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"AssetInfo %s can't read by error: %v\", name, err)\n\t\t}\n\t\treturn a.info, nil\n\t}\n\treturn nil, fmt.Errorf(\"AssetInfo %s not found\", name)\n}\n\n// AssetNames returns the names of the assets.\nfunc AssetNames() []string {\n\tnames := make([]string, 0, len(_bindata))\n\tfor name := range _bindata {\n\t\tnames = append(names, name)\n\t}\n\treturn names\n}\n\n// _bindata is a table, holding each asset generator, mapped to its name.\nvar _bindata = map[string]func() (*asset, error){\n\t\"blacklist.txt\": blacklistTxt,\n}\n\n// AssetDir returns the file names below a certain\n// directory embedded in the file by go-bindata.\n// For example if you run go-bindata on data/... and data contains the\n// following hierarchy:\n//     data/\n//       foo.txt\n//       img/\n//         a.png\n//         b.png\n// then AssetDir(\"data\") would return []string{\"foo.txt\", \"img\"}\n// AssetDir(\"data/img\") would return []string{\"a.png\", \"b.png\"}\n// AssetDir(\"foo.txt\") and AssetDir(\"notexist\") would return an error\n// AssetDir(\"\") will return []string{\"data\"}.\nfunc AssetDir(name string) ([]string, error) {\n\tnode := _bintree\n\tif len(name) != 0 {\n\t\tcannonicalName := strings.Replace(name, \"\\\\\", \"/\", -1)\n\t\tpathList := strings.Split(cannonicalName, \"/\")\n\t\tfor _, p := range pathList {\n\t\t\tnode = node.Children[p]\n\t\t\tif node == nil {\n\t\t\t\treturn nil, fmt.Errorf(\"Asset %s not found\", name)\n\t\t\t}\n\t\t}\n\t}\n\tif node.Func != nil {\n\t\treturn nil, fmt.Errorf(\"Asset %s not found\", name)\n\t}\n\trv := make([]string, 0, len(node.Children))\n\tfor childName := range node.Children {\n\t\trv = append(rv, childName)\n\t}\n\treturn rv, nil\n}\n\ntype bintree struct {\n\tFunc     func() (*asset, error)\n\tChildren map[string]*bintree\n}\n\nvar _bintree = &bintree{nil, map[string]*bintree{\n\t\"blacklist.txt\": &bintree{blacklistTxt, map[string]*bintree{}},\n}}\n\n// RestoreAsset restores an asset under the given directory\nfunc RestoreAsset(dir, name string) error {\n\tdata, err := Asset(name)\n\tif err != nil {\n\t\treturn err\n\t}\n\tinfo, err := AssetInfo(name)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = ioutil.WriteFile(_filePath(dir, name), data, info.Mode())\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// RestoreAssets restores an asset under the given directory recursively\nfunc RestoreAssets(dir, name string) error {\n\tchildren, err := AssetDir(name)\n\t// File\n\tif err != nil {\n\t\treturn RestoreAsset(dir, name)\n\t}\n\t// Dir\n\tfor _, child := range children {\n\t\terr = RestoreAssets(dir, filepath.Join(name, child))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc _filePath(dir, name string) string {\n\tcannonicalName := strings.Replace(name, \"\\\\\", \"/\", -1)\n\treturn filepath.Join(append([]string{dir}, strings.Split(cannonicalName, \"/\")...)...)\n}\n"
  },
  {
    "path": "pkg/aggregator/blacklist.go",
    "content": "package aggregator\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"strings\"\n)\n\ntype blacklist struct {\n\tdata []byte\n}\n\nfunc newBlacklist() (*blacklist, error) {\n\tvar err error\n\tb := &blacklist{}\n\tb.data, err = Asset(\"blacklist.txt\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn b, nil\n}\n\n// Has returns true if the given domain appears on the blacklist\n// Uses sub-string matching, so if usesfathom.com is blacklisted then this function will also return true for danny.usesfathom.com\nfunc (b *blacklist) Has(r string) bool {\n\tif r == \"\" {\n\t\treturn false\n\t}\n\n\tscanner := bufio.NewScanner(bytes.NewReader(b.data))\n\tdomain := \"\"\n\n\tfor scanner.Scan() {\n\t\tdomain = scanner.Text()\n\t\tif strings.HasSuffix(r, domain) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n"
  },
  {
    "path": "pkg/aggregator/blacklist_test.go",
    "content": "package aggregator\n\nimport (\n\t\"testing\"\n)\n\nfunc TestBlacklistHas(t *testing.T) {\n\tb, err := newBlacklist()\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\n\ttable := map[string]bool{\n\t\t\"03e.info\":      true,\n\t\t\"zvetki.ru\":     true,\n\t\t\"usefathom.com\": false,\n\t\t\"foo.03e.info\":  true, // sub-string match\n\t}\n\n\tfor r, e := range table {\n\t\tif v := b.Has(r); v != e {\n\t\t\tt.Errorf(\"Expected %v, got %v\", e, v)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/aggregator/data/blacklist.txt",
    "content": "03e.info\n0n-line.tv\n1-99seo.com\n1-free-share-buttons.com\n100dollars-seo.com\n100searchengines.com\n12masterov.com\n12u.info\n1pamm.ru\n1webmaster.ml\n24x7-server-support.site\n2your.site\n3-letter-domains.net\n3waynetworks.com\n4inn.ru\n4istoshop.com\n4webmasters.org\n5-steps-to-start-business.com\n5forex.ru\n6hopping.com\n7kop.ru\n7makemoneyonline.com\n7zap.com\nabcdefh.xyz\nabcdeg.xyz\nabclauncher.com\nacads.net\nacarreo.ru\nacunetix-referrer.com\nadanih.com\nadcash.com\nadf.ly\nadspart.com\nadtiger.tk\nadventureparkcostarica.com\nadviceforum.info\nadvokateg.xyz\naerodizain.com\naffordablewebsitesandmobileapps.com\nafora.ru\naibolita.com\naidarmebel.kz\nakuhni.by\nalfabot.xyz\nalibestsale.com\naliexsale.ru\nalinabaniecka.pl\nalkanfarma.org\nallergick.com\nallergija.com\nallknow.info\nallmarketsnewdayli.gdn\nallnews.md\nallnews24.in\nallwomen.info\nallwrighter.ru\nalpharma.net\naltermix.ua\namazon-seo-service.com\namt-k.ru\namtel-vredestein.com\nanal-acrobats.hol.es\nanalytics-ads.xyz\nanapa-inns.ru\nandroid-style.com\nanimalphotos.xyz\nanimenime.ru\nanticrawler.org\nantiguabarbuda.ru\napteka-pharm.ru\narendakvartir.kz\narendovalka.xyz\narkkivoltti.net\nartdeko.info\nartpaint-market.ru\nartparquet.ru\naruplighting.com\nask-yug.com\natleticpharm.org\natyks.ru\nauto-complex.by\nauto-kia-fulldrive.ru\nauto-seo-service.org\nautoblog.org.ua\nautoseo-service.org\nautoseo-traffic.com\nautovideobroadcast.com\naviva-limoux.com\navkzarabotok.info\navtointeres.ru\navtovykup.kz\nazartclub.org\nazbukafree.com\nazlex.uz\nbaixar-musicas-gratis.com\nbaladur.ru\nbalitouroffice.com\nbalkanfarma.org\nbard-real.com.ua\nbatut-fun.ru\nbavariagid.de\nbeachtoday.ru\nbedroomlighting.us\nberemenyashka.com\nbest-deal-hdd.pro\nbest-ping-service-usa.blue\nbest-seo-offer.com\nbest-seo-software.xyz\nbest-seo-solution.com\nbestmobilityscooterstoday.com\nbestofferhddbyt.info\nbestofferhddeed.info\nbestwebsitesawards.com\nbetterhealthbeauty.com\nbezprostatita.com\nbif-ru.info\nbiglistofwebsites.com\nbilliard-classic.com.ua\nbio-market.kz\nbiplanecentre.ru\nbird1.ru\nbiteg.xyz\nbizru.info\nblack-friday.ga\nblackhatworth.com\nblog100.org\nblog4u.top\nblogstar.fun\nblogtotal.de\nblue-square.biz\nbluerobot.info\nboltalko.xyz\nboostmyppc.com\nbpro1.top\nbrakehawk.com\nbrateg.xyz\nbreak-the-chains.com\nbrillianty.info\nbrk-rti.ru\nbrothers-smaller.ru\nbrusilov.ru\nbsell.ru\nbudilneg.xyz\nbudmavtomatika.com.ua\nbufetout.ru\nbuketeg.xyz\nbukleteg.xyz\nburger-imperia.com\nburn-fat.ga\nbuttons-for-website.com\nbuttons-for-your-website.com\nbuy-cheap-online.info\nbuy-cheap-pills-order-online.com\nbuy-forum.ru\nbuy-meds24.com\ncall-of-duty.info\ncardiosport.com.ua\ncartechnic.ru\ncenokos.ru\ncenoval.ru\ncezartabac.ro\nchcu.net\ncheap-trusted-backlinks.com\nchelyabinsk.dienai.ru\nchinese-amezon.com\nchizhik-2.ru\nci.ua\ncityadspix.com\ncivilwartheater.com\ncleaningservices.kiev.ua\nclicksor.com\nclimate.by\nclub-lukojl.ru\ncoderstate.com\ncodysbbq.com\ncoffeemashiny.ru\ncolumb.net.ua\ncommerage.ru\ncomp-pomosch.ru\ncompliance-alex.xyz\ncompliance-alexa.xyz\ncompliance-andrew.xyz\ncompliance-barak.xyz\ncompliance-brian.xyz\ncompliance-don.xyz\ncompliance-donald.xyz\ncompliance-elena.xyz\ncompliance-fred.xyz\ncompliance-george.xyz\ncompliance-irvin.xyz\ncompliance-ivan.xyz\ncompliance-john.top\ncompliance-julianna.top\ncomputer-remont.ru\nconciergegroup.org\nconnectikastudio.com\ncookie-law-enforcement-aa.xyz\ncookie-law-enforcement-bb.xyz\ncookie-law-enforcement-cc.xyz\ncookie-law-enforcement-dd.xyz\ncookie-law-enforcement-ee.xyz\ncookie-law-enforcement-ff.xyz\ncookie-law-enforcement-gg.xyz\ncookie-law-enforcement-hh.xyz\ncookie-law-enforcement-ii.xyz\ncookie-law-enforcement-jj.xyz\ncookie-law-enforcement-kk.xyz\ncookie-law-enforcement-ll.xyz\ncookie-law-enforcement-mm.xyz\ncookie-law-enforcement-nn.xyz\ncookie-law-enforcement-oo.xyz\ncookie-law-enforcement-pp.xyz\ncookie-law-enforcement-qq.xyz\ncookie-law-enforcement-rr.xyz\ncookie-law-enforcement-ss.xyz\ncookie-law-enforcement-tt.xyz\ncookie-law-enforcement-uu.xyz\ncookie-law-enforcement-vv.xyz\ncookie-law-enforcement-ww.xyz\ncookie-law-enforcement-xx.xyz\ncookie-law-enforcement-yy.xyz\ncookie-law-enforcement-zz.xyz\ncopyrightclaims.org\ncopyrightinstitute.org\ncovadhosting.biz\ncp24.com.ua\ncubook.supernew.org\ncustomsua.com.ua\ncyber-monday.ga\ndailyrank.net\ndarodar.com\ndawlenie.com\ndbutton.net\ndcdcapital.com\ndeart-13.ru\ndelfin-aqua.com.ua\ndemenageur.com\ndengi-v-kredit.in.ua\ndermatovenerologiya.com\ndescargar-musica-gratis.net\ndetskie-konstruktory.ru\ndev-seo.blog\ndienai.ru\ndiplomas-ru.com\ndipstar.org\ndistonija.com\ndividendo.ru\ndjekxa.ru\ndjonwatch.ru\ndktr.ru\ndocs4all.com\ndocsarchive.net\ndocsportal.net\ndocumentbase.net\ndocumentserver.net\ndocumentsite.net\ndogsrun.net\ndojki-hd.com\ndomain-tracker.com\ndomashniy-hotel.ru\ndominateforex.ml\ndomination.ml\ndoska-vsem.ru\ndostavka-v-krym.com\ndosugrostov.site\ndrupa.com\ndvr.biz.ua\ne-buyeasy.com\ne-commerce-seo.com\ne-commerce-seo1.com\nearn-from-articles.com\nearnian-money.info\neasycommerce.cf\necommerce-seo.org\necomp3.ru\neconom.co\nedakgfvwql.ru\nedudocs.net\neduinfosite.com\neduserver.net\negovaleo.it\nek-invest.ru\nekatalog.xyz\neko-gazon.ru\nekoproekt-kr.ru\nekto.ee\nelektrikovich.ru\nelementspluss.ru\nelentur.com.ua\nelmifarhangi.com\nelvel.com.ua\nemerson-rus.ru\neric-artem.com\nerot.co\nescort-russian.com\neste-line.com.ua\netairikavideo.gr\netehnika.com.ua\neu-cookie-law-enforcement2.xyz\neuromasterclass.ru\neuropages.com.ru\neurosamodelki.ru\nevent-tracking.com\nexdocsfiles.com\nexpress-vyvoz.ru\neyes-on-you.ga\nf1nder.org\nfanoboi.com\nfast-wordpress-start.com\nfbdownloader.com\nfeminist.org.ua\nfidalsa.de\nfilesclub.net\nfilesdatabase.net\nfilter-ot-zheleza.ru\nfinancial-simulation.com\nfinansov.info\nfindercarphotos.com\nfix-website-errors.com\nfloating-share-buttons.com\nflowertherapy.ru\nfor-your.website\nforex-procto.ru\nforsex.info\nfortwosmartcar.pw\nforum69.info\nfoxweber.com\nfrauplus.ru\nfree-fb-traffic.com\nfree-fbook-traffic.com\nfree-floating-buttons.com\nfree-share-buttons.com\nfree-social-buttons.com\nfree-social-buttons.xyz\nfree-social-buttons7.xyz\nfree-traffic.xyz\nfree-video-tool.com\nfree-website-traffic.com\nfreenode.info\nfreewhatsappload.com\nfreewlan.info\nfreshnails.com.ua\nfsalas.com\ngame300.ru\ngandikapper.ru\ngearcraft.us\ngearsadspromo.club\ngeneralporn.org\ngepatit-info.top\ngermes-trans.com\nget-clickize.info\nget-free-social-traffic.com\nget-free-traffic-now.com\nget-more-freeer-visitors.info\nget-more-freeish-visitors.info\nget-seo-help.com\nget-your-social-buttons.info\ngetaadsincome.info\ngetadsincomely.info\ngetlamborghini.ga\ngetpy-click.info\ngetrichquick.ml\ngetrichquickly.info\nghazel.ru\nghostvisitor.com\ngiftbig.ru\ngirlporn.ru\ngkvector.ru\nglavprofit.ru\nglobal-smm.ru\ngobongo.info\ngoodhumor24.com\ngoodprotein.ru\ngoogle-liar.ru\ngooglemare.com\ngooglsucks.com\ngorgaz.info\ngrafaman.ru\nguardlink.org\nguidetopetersburg.com\nhandicapvantoday.com\nhappysong.ru\nhard-porn.mobi\nhavepussy.com\nhawaiisurf.com\nhdmoviecamera.net\nhdmoviecams.com\nhealbio.ru\nhealgastro.com\nhomeafrikalike.tk\nhomemypicture.tk\nhongfanji.com\nhosting-tracker.com\nhottour.com\nhousediz.com\nhousemilan.ru\nhowopen.ru\nhowtostopreferralspam.eu\nhoztorg-opt.ru\nhseipaa.kz\nhulfingtonpost.com\nhumanorightswatch.org\nhundejo.com\nhvd-store.com\nhyip-zanoza.me\nico.re\nigadgetsworld.com\nigru-xbox.net\nilikevitaly.com\niloveitaly.ro\niloveitaly.ru\nilovevitaly.co\nilovevitaly.com\nilovevitaly.info\nilovevitaly.org\nilovevitaly.ru\nilovevitaly.xyz\niminent.com\nimperiafilm.ru\nimpotentik.com\nincitystroy.ru\nincomekey.net\nincreasewwwtraffic.info\ninet-shop.su\ninfektsii.com\ninfodocsportal.com\ninform-ua.info\ninsider.pro\ninterferencer.ru\nintex-air.ru\ninvestpamm.ru\niskalko.ru\nisotoner.com\nispaniya-costa-blanca.ru\nit-max.com.ua\nizhstrelok.ru\njjbabskoe.ru\njobius.com.ua\njumkite.com\njustkillingti.me\njustprofit.xyz\nkabbalah-red-bracelets.com\nkabinet-binbank.ru\nkabinet-card-5ka.ru\nkabinet-click-alfabank.ru\nkabinet-lk-megafon.ru\nkabinet-login-mts.ru\nkabinet-mil.ru\nkabinet-mos.ru\nkabinet-my-beeline.ru\nkabinet-my-pochtabank.ru\nkabinet-online-vtb.ru\nkabinet-tinkoff.ru\nkabinet-ttk.ru\nkakablog.net\nkambasoft.com\nkamin-sam.ru\nkarapuz.org.ua\nkazka.ru\nkazrent.com\nkerch.site\nkevblog.top\nkeywords-monitoring-success.com\nkeywords-monitoring-your-success.com\nkharkov.ua\nkino-fun.ru\nkino-key.info\nkino2018.cc\nkinobum.org\nkinopolet.net\nkinosed.net\nknigonosha.net\nkomp-pomosch.ru\nkomputers-best.ru\nkomukc.com.ua\nkonkursov.net\nkozhasobak.com\nkrasnodar-avtolombard.ru\nkredytbank.com.ua\nlaminat.com.ua\nlandliver.org\nlandoftracking.com\nlaptop-4-less.com\nlaw-check-two.xyz\nlaw-enforcement-bot-ff.xyz\nlaw-enforcement-check-three.xyz\nlaw-enforcement-ee.xyz\nlaw-six.xyz\nlaxdrills.com\nleeboyrussia.com\nlegalrc.biz\nlerporn.info\nleto-dacha.ru\nlider82.ru\nlipidofobia.com.br\nlittleberry.ru\nlivefixer.com\nlivia-pache.ru\nlivingroomdecoratingideas.website\nlk-gosuslugi.ru\nlogin-tinkoff.ru\nloveorganic.ch\nlsex.xyz\nluckybull.io\nlukoilcard.ru\nlumb.co\nluton-invest.ru\nluxup.ru\nmagicdiet.gq\nmagnetic-bracelets.ru\nmakemoneyonline.com\nmakeprogress.ga\nmanimpotence.com\nmanualterap.roleforum.ru\nmarblestyle.ru\nmaridan.com.ua\nmarketland.ml\nmasterseek.com\nmatras.space\nmattgibson.us\nmax-apprais.com\nmaxxximoda.ru\nmebel-iz-dereva.kiev.ua\nmebelcomplekt.ru\nmebeldekor.com.ua\nmed-dopomoga.com\nmed-zdorovie.com.ua\nmedicineseasybuy.com\nmeds-online24.com\nmeduza-consult.ru\nmegapolis-96.ru\nmetallo-konstruktsii.ru\nmetallosajding.ru\nmifepriston.net\nmikozstop.com\nmikrocement.com.ua\nmikrozaym2you.ru\nminegam.com\nmirobuvi.com.ua\nmirtorrent.net\nmksport.ru\nmobilemedia.md\nmockupui.com\nmodforwot.ru\nmodnie-futbolki.net\nmoinozhki.com\nmonetizationking.net\nmoney-for-placing-articles.com\nmoney7777.info\nmoneytop.ru\nmoneyzzz.ru\nmosrif.ru\nmostorgnerud.ru\nmoy-dokument.com\nmoyakuhnia.ru\nmuscle-factory.com.ua\nmusichallaudio.ru\nmybuh.kz\nmyftpupload.com\nmyplaycity.com\nnachalka21.ru\nnanochskazki.ru\nneedtosellmyhousefast.com\nnet-profits.xyz\nnevapotolok.ru\nnewsrosprom.ru\nnewstaffadsshop.club\nniki-mlt.ru\nnizniynovgorod.dienai.ru\nnovosti-hi-tech.ru\nnubuilderian.info\nnufaq.com\no-o-11-o-o.com\no-o-6-o-o.com\no-o-6-o-o.ru\no-o-8-o-o.com\no-o-8-o-o.ru\nobsessionphrases.com\nodiabetikah.com\nodsadsmobile.biz\nofermerah.com\noffice2web.com\nofficedocuments.net\nogorodnic.com\nonline-binbank.ru\nonline-hit.info\nonline-intim.com\nonline-mkb.ru\nonline-templatestore.com\nonline-vtb.ru\nonlinetvseries.me\nonlywoman.org\nooo-olni.ru\noptsol.ru\norakul.spb.ru\nosteochondrosis.ru\nownshop.cf\nozas.net\npaidonlinesites.com\npalvira.com.ua\npc-services.ru\nperm.dienai.ru\nperper.ru\npetrovka-online.com\nphoto-clip.ru\nphotokitchendesign.com\npicturesmania.com\npills24h.com\npiulatte.cz\npizza-imperia.com\npizza-tycoon.com\npk-pomosch.ru\npk-services.ru\npodarkilove.ru\npodemnik.pro\npodseka1.ru\npoiskzakona.ru\npokupaylegko.ru\npopads.net\npops.foundation\npopugaychiki.com\npornhub-forum.ga\npornhub-forum.uni.me\npornhub-ru.com\nporno-chaman.info\npornoelita.info\npornoforadult.com\npornogig.com\npornohd1080.online\npornoklad.ru\npornonik.com\npornoplen.com\nportnoff.od.ua\npozdravleniya-c.ru\npriceg.com\npricheski-video.com\nprlog.ru\nprocrafts.ru\nprodaemdveri.com\nproducm.ru\nprodvigator.ua\nprofessionalsolutions.eu\nprointer.net.ua\npromoforum.ru\npron.pro\nprosmibank.ru\nprostitutki-rostova.ru.com\npsa48.ru\npunch.media\npurchasepillsnorx.com\nqualitymarketzone.com\nquit-smoking.ga\nqwesa.ru\nrank-checker.online\nrankings-analytics.com\nranksonic.info\nranksonic.net\nranksonic.org\nrapidgator-porn.ga\nrapidsites.pro\nrazborka-skoda.org.ua\nrcb101.ru\nrealresultslist.com\nrednise.com\nregionshop.biz\nreleshop.ru\nremkompov.ru\nremont-kvartirspb.com\nrent2spb.ru\nreplica-watch.ru\nresearch.ifmo.ru\nresell-seo-services.com\nresellerclub.com\nresponsive-test.net\nreversing.cc\nrfavon.ru\nrightenergysolutions.com.au\nroof-city.ru\nrospromtest.ru\nru-lk-rt.ru\nruinfocomp.ru\nrulate.ru\nrumamba.com\nrupolitshow.ru\nrusexy.xyz\nruspoety.ru\nrussian-postindex.ru\nrussian-translator.com\nrybalka-opt.ru\nsad-torg.com.ua\nsady-urala.ru\nsaltspray.ru\nsanjosestartups.com\nsantaren.by\nsantasgift.ml\nsantehnovich.ru\nsavetubevideo.com\nsavetubevideo.info\nscansafe.net\nscat.porn\nscreentoolkit.com\nscripted.com\nsearch-error.com\nsearchencrypt.com\nsecurity-corporation.com.ua\nsell-fb-group-here.com\nsemalt.com\nsemaltmedia.com\nseo-2-0.com\nseo-platform.com\nseo-smm.kz\nseoanalyses.com\nseocheckupx.com\nseocheckupx.net\nseoexperimenty.ru\nseojokes.net\nseopub.net\nseoservices2018.com\nsexsaoy.com\nsexyali.com\nsexyteens.hol.es\nshagtomsk.ru\nshare-buttons-for-free.com\nshare-buttons.xyz\nsharebutton.io\nsharebutton.net\nsharebutton.to\nshnyagi.net\nshoppingmiracles.co.uk\nshops-ru.ru\nsibecoprom.ru\nsim-dealer.ru\nsimple-share-buttons.com\nsinhronperevod.ru\nsite-auditor.online\nsite5.com\nsiteripz.net\nsitevaluation.org\nskinali.com\nsladkoevideo.com\nsledstvie-veli.net\nslftsdybbg.ru\nslkrm.ru\nslomm.ru\nslow-website.xyz\nsmailik.org\nsmartphonediscount.info\nsnabs.kz\nsnegozaderzhatel.ru\nsnip.to\nsnip.tw\nsoaksoak.ru\nsochi-3d.ru\nsocial-button.xyz\nsocial-buttons-ii.xyz\nsocial-buttons.com\nsocial-traffic-1.xyz\nsocial-traffic-2.xyz\nsocial-traffic-3.xyz\nsocial-traffic-4.xyz\nsocial-traffic-5.xyz\nsocial-traffic-7.xyz\nsocial-widget.xyz\nsocialbuttons.xyz\nsocialseet.ru\nsocialtrade.biz\nsohoindia.net\nsolitaire-game.ru\nsolnplast.ru\nsosdepotdebilan.com\nsouvenirua.com\nsovetskie-plakaty.ru\nsoyuzexpedition.ru\nsp-laptop.ru\nsp-zakupki.ru\nspb-plitka.ru\nspb-scenar.ru\nspeedup-my.site\nspin2016.cf\nsportwizard.ru\nspravka130.ru\nspravkavspb.net\nsribno.net\nstavimdveri.ru\nsteame.ru\nstiralkovich.ru\nstocktwists.com\nstore-rx.com\nstream-tds.com\nstroyka47.ru\nstudentguide.ru\nsuccess-seo.com\nsundrugstore.com\nsuperiends.org\nsupermama.top\nsupervesti.ru\nsvetka.info\nsvetoch.moscow\nt-machinery.ru\nt-rec.su\ntaihouse.ru\ntattoo-stickers.ru\ntattooha.com\ntd-perimetr.ru\ntechnika-remont.ru\ntedxrj.com\ntentcomplekt.ru\nteplohod-gnezdo.ru\ntexnika.com.ua\ntgtclick.com\nthaoduoctoc.com\ntheautoprofit.ml\ntheguardlan.com\nthesmartsearch.net\ntokshow.online\ntomck.com\ntop-gan.ru\ntop-l2.com\ntop1-seo-service.com\ntop10-way.com\ntopquality.cf\ntopseoservices.co\ntrack-rankings.online\ntracker24-gps.ru\ntraffic-cash.xyz\ntraffic2cash.org\ntraffic2cash.xyz\ntraffic2money.com\ntrafficgenius.xyz\ntrafficmonetize.org\ntrafficmonetizer.org\ntraphouselatino.net\ntrion.od.ua\ntsatu.edu.ua\ntsc-koleso.ru\ntuningdom.ru\ntwsufa.ru\nua.tc\nuasb.ru\nucoz.ru\nudav.net\nufa.dienai.ru\nukrainian-poetry.com\nul-potolki.ru\nundergroundcityphoto.com\nunibus.su\nuniverfiles.com\nunlimitdocs.net\nunpredictable.ga\nuptime-as.net\nuptime-eu.net\nuptime-us.net\nuptime.com\nuptimechecker.com\nuzpaket.com\nuzungil.com\nvaderenergy.ru\nvalidus.pro\nvarikozdok.ru\nveloland.in.ua\nventopt.by\nveselokloun.ru\nvesnatehno.com\nviagra-soft.ru\nvideo--production.com\nvideo-woman.com\nvideos-for-your-business.com\nviel.su\nviktoria-center.ru\nvodaodessa.com\nvodkoved.ru\nvzheludke.com\nvzubkah.com\nw3javascript.com\nwallpaperdesk.info\nwdss.com.ua\nwe-ping-for-youic.info\nweb-revenue.xyz\nwebmaster-traffic.com\nwebmonetizer.net\nwebsite-analytics.online\nwebsite-analyzer.info\nwebsite-speed-check.site\nwebsite-speed-checker.site\nwebsites-reviews.com\nwebsocial.me\nweburlopener.com\nwmasterlead.com\nwoman-orgasm.ru\nwordpress-crew.net\nwordpresscore.com\nworkius.ru\nworks.if.ua\nworldmed.info\nwufak.com\nww2awards.info\nwww-lk-rt.ru\nx5market.ru\nxkaz.org\nxn-------53dbcapga5atlplfdm6ag1ab1bvehl0b7toa0k.xn--p1ai\nxn-----6kcamwewcd9bayelq.xn--p1ai\nxn-----7kcaaxchbbmgncr7chzy0k0hk.xn--p1ai\nxn-----clckdac3bsfgdft3aebjp5etek.xn--p1ai\nxn----7sbabhjc3ccc5aggbzfmfi.xn--p1ai\nxn----7sbabm1ahc4b2aqff.su\nxn----7sbabn5abjehfwi8bj.xn--p1ai\nxn----7sbbpe3afguye.xn--p1ai\nxn----7sbho2agebbhlivy.xn--p1ai\nxn----8sbaki4azawu5b.xn--p1ai\nxn----8sbarihbihxpxqgaf0g1e.xn--80adxhks\nxn----8sbhefaln6acifdaon5c6f4axh.xn--p1ai\nxn----8sblgmbj1a1bk8l.xn----161-4vemb6cjl7anbaea3afninj.xn--p1ai\nxn----ctbbcjd3dbsehgi.xn--p1ai\nxn----ctbfcdjl8baejhfb1oh.xn--p1ai\nxn----ctbigni3aj4h.xn--p1ai\nxn----ftbeoaiyg1ak1cb7d.xn--p1ai\nxn----itbbudqejbfpg3l.com\nxn--80aaajkrncdlqdh6ane8t.xn--p1ai\nxn--80aanaardaperhcem4a6i.com\nxn--80adaggc5bdhlfamsfdij4p7b.xn--p1ai\nxn--80adgcaax6acohn6r.xn--p1ai\nxn--90acenikpebbdd4f6d.xn--p1ai\nxn--90acjmaltae3acm.xn--p1acf\nxn--c1acygb.xn--p1ai\nxn--d1abj0abs9d.in.ua\nxn--d1aifoe0a9a.top\nxn--e1aaajzchnkg.ru.com\nxn--e1agf4c.xn--80adxhks\nxtrafficplus.com\nxz618.com\nyaderenergy.ru\nyes-com.com\nyhirurga.ru\nykecwqlixx.ru\nyodse.io\nyouporn-forum.ga\nyouporn-forum.uni.me\nyouporn-ru.com\nyourserverisdown.com\nzahvat.ru\nzastroyka.org\nzavod-gm.ru\nzdm-auto.com\nzdorovie-nogi.info\nzelena-mriya.com.ua\nzoominfo.com\nzvetki.ru"
  },
  {
    "path": "pkg/aggregator/store.go",
    "content": "package aggregator\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/usefathom/fathom/pkg/datastore\"\n\t\"github.com/usefathom/fathom/pkg/models\"\n)\n\nfunc (agg *Aggregator) getSiteStats(r *results, siteID int64, t time.Time) (*models.SiteStats, error) {\n\tcacheKey := fmt.Sprintf(\"%d-%s\", siteID, t.Format(\"2006-01-02T15\"))\n\tif stats, ok := r.Sites[cacheKey]; ok {\n\t\treturn stats, nil\n\n\t}\n\n\t// get from db\n\tstats, err := agg.database.GetSiteStats(siteID, t)\n\tif err != nil && err != datastore.ErrNoResults {\n\t\treturn nil, err\n\t}\n\n\tif stats == nil {\n\t\tstats = &models.SiteStats{\n\t\t\tSiteID: siteID,\n\t\t\tNew:    true,\n\t\t\tDate:   t,\n\t\t}\n\t}\n\n\tr.Sites[cacheKey] = stats\n\treturn stats, nil\n}\n\nfunc (agg *Aggregator) getPageStats(r *results, siteID int64, t time.Time, hostname string, pathname string) (*models.PageStats, error) {\n\tcacheKey := fmt.Sprintf(\"%d-%s-%s-%s\", siteID, t.Format(\"2006-01-02T15\"), hostname, pathname)\n\tif stats, ok := r.Pages[cacheKey]; ok {\n\t\treturn stats, nil\n\t}\n\n\thostnameID, err := agg.database.HostnameID(hostname)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tpathnameID, err := agg.database.PathnameID(pathname)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tstats, err := agg.database.GetPageStats(siteID, t, hostnameID, pathnameID)\n\tif err != nil && err != datastore.ErrNoResults {\n\t\treturn nil, err\n\t}\n\n\tif stats == nil {\n\t\tstats = &models.PageStats{\n\t\t\tSiteID:     siteID,\n\t\t\tNew:        true,\n\t\t\tHostnameID: hostnameID,\n\t\t\tPathnameID: pathnameID,\n\t\t\tDate:       t,\n\t\t}\n\n\t}\n\n\tr.Pages[cacheKey] = stats\n\treturn stats, nil\n}\n\nfunc (agg *Aggregator) getReferrerStats(r *results, siteID int64, t time.Time, hostname string, pathname string) (*models.ReferrerStats, error) {\n\tcacheKey := fmt.Sprintf(\"%d-%s-%s-%s\", siteID, t.Format(\"2006-01-02T15\"), hostname, pathname)\n\tif stats, ok := r.Referrers[cacheKey]; ok {\n\t\treturn stats, nil\n\t}\n\n\thostnameID, err := agg.database.HostnameID(hostname)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tpathnameID, err := agg.database.PathnameID(pathname)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// get from db\n\tstats, err := agg.database.GetReferrerStats(siteID, t, hostnameID, pathnameID)\n\tif err != nil && err != datastore.ErrNoResults {\n\t\treturn nil, err\n\t}\n\n\tif stats == nil {\n\t\tstats = &models.ReferrerStats{\n\t\t\tSiteID:     siteID,\n\t\t\tNew:        true,\n\t\t\tHostnameID: hostnameID,\n\t\t\tPathnameID: pathnameID,\n\t\t\tDate:       t,\n\t\t\tGroup:      \"\",\n\t\t}\n\n\t\tif strings.Contains(hostname, \"www.google.\") {\n\t\t\tstats.Group = \"Google\"\n\t\t} else if strings.Contains(stats.Hostname, \"www.bing.\") {\n\t\t\tstats.Group = \"Bing\"\n\t\t} else if strings.Contains(stats.Hostname, \"www.baidu.\") {\n\t\t\tstats.Group = \"Baidu\"\n\t\t} else if strings.Contains(stats.Hostname, \"www.yandex.\") {\n\t\t\tstats.Group = \"Yandex\"\n\t\t} else if strings.Contains(stats.Hostname, \"search.yahoo.\") {\n\t\t\tstats.Group = \"Yahoo!\"\n\t\t} else if strings.Contains(stats.Hostname, \"www.findx.\") {\n\t\t\tstats.Group = \"Findx\"\n\t\t}\n\t}\n\n\tr.Referrers[cacheKey] = stats\n\treturn stats, nil\n}\n"
  },
  {
    "path": "pkg/api/api.go",
    "content": "package api\n\nimport (\n\t\"github.com/gorilla/sessions\"\n\t\"github.com/usefathom/fathom/pkg/datastore\"\n)\n\ntype API struct {\n\tdatabase datastore.Datastore\n\tsessions sessions.Store\n}\n\n// New instantiates a new API object\nfunc New(db datastore.Datastore, secret string) *API {\n\treturn &API{\n\t\tdatabase: db,\n\t\tsessions: sessions.NewCookieStore([]byte(secret)),\n\t}\n}\n"
  },
  {
    "path": "pkg/api/auth.go",
    "content": "package api\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"strings\"\n\n\tgcontext \"github.com/gorilla/context\"\n\t\"github.com/usefathom/fathom/pkg/datastore\"\n)\n\ntype key int\n\nconst (\n\tuserKey key = 0\n)\n\ntype login struct {\n\tEmail    string `json:\"email\"`\n\tPassword string `json:\"password\"`\n}\n\nfunc (l *login) Sanitize() {\n\tl.Email = strings.ToLower(strings.TrimSpace(l.Email))\n}\n\n// GET /api/session\nfunc (api *API) GetSession(w http.ResponseWriter, r *http.Request) error {\n\tuserCount, err := api.database.CountUsers()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// if 0 users in database, dashboard is public\n\tif userCount == 0 {\n\t\treturn respond(w, http.StatusOK, envelope{Data: true})\n\t}\n\n\t// if existing session, assume logged-in\n\tsession, _ := api.sessions.Get(r, \"auth\")\n\tif !session.IsNew {\n\t\treturn respond(w, http.StatusOK, envelope{Data: true})\n\t}\n\n\t// otherwise: not logged-in yet\n\treturn respond(w, http.StatusOK, envelope{Data: false})\n}\n\n// URL: POST /api/session\nfunc (api *API) CreateSession(w http.ResponseWriter, r *http.Request) error {\n\t// check login creds\n\tvar l login\n\terr := json.NewDecoder(r.Body).Decode(&l)\n\tif err != nil {\n\t\treturn err\n\t}\n\tl.Sanitize()\n\n\t// find user with given email\n\tu, err := api.database.GetUserByEmail(l.Email)\n\tif err != nil && err != datastore.ErrNoResults {\n\t\treturn err\n\t}\n\n\t// compare pwd\n\tif err == datastore.ErrNoResults || u.ComparePassword(l.Password) != nil {\n\t\treturn respond(w, http.StatusUnauthorized, envelope{Error: \"invalid_credentials\"})\n\t}\n\n\t// ignore error here as we want a (new) session regardless\n\tsession, _ := api.sessions.Get(r, \"auth\")\n\tsession.Values[\"user_id\"] = u.ID\n\terr = session.Save(r, w)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn respond(w, http.StatusOK, envelope{Data: true})\n}\n\n// URL: DELETE /api/session\nfunc (api *API) DeleteSession(w http.ResponseWriter, r *http.Request) error {\n\tsession, _ := api.sessions.Get(r, \"auth\")\n\tif !session.IsNew {\n\t\tsession.Options.MaxAge = -1\n\t\terr := session.Save(r, w)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn respond(w, http.StatusOK, envelope{Data: true})\n}\n\n// Authorize is middleware that aborts the request if unauthorized\nfunc (api *API) Authorize(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t// clear context from request after it is handled\n\t\t// see http://www.gorillatoolkit.org/pkg/sessions#overview\n\t\tdefer gcontext.Clear(r)\n\n\t\t// first count users in datastore\n\t\t// if 0, assume dashboard is public\n\t\tuserCount, err := api.database.CountUsers()\n\t\tif err != nil {\n\t\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tif userCount > 0 {\n\t\t\tsession, err := api.sessions.Get(r, \"auth\")\n\t\t\t// an err is returned if cookie has been tampered with, so check that\n\t\t\tif err != nil {\n\t\t\t\trespond(w, http.StatusUnauthorized, envelope{Error: \"unauthorized\"})\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tuserID, ok := session.Values[\"user_id\"]\n\t\t\tif session.IsNew || !ok {\n\t\t\t\trespond(w, http.StatusUnauthorized, envelope{Error: \"unauthorized\"})\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// validate user ID in session\n\t\t\tif _, err := api.database.GetUser(userID.(int64)); err != nil {\n\t\t\t\trespond(w, http.StatusUnauthorized, envelope{Error: \"unauthorized\"})\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tnext.ServeHTTP(w, r)\n\t})\n}\n"
  },
  {
    "path": "pkg/api/auth_test.go",
    "content": "package api\n\nimport \"testing\"\n\nfunc TestLoginSanitize(t *testing.T) {\n\trawEmail := \"Foo@foobar.com   \"\n\tl := &login{\n\t\tEmail: rawEmail,\n\t}\n\n\tl.Sanitize()\n\tif l.Email != \"foo@foobar.com\" {\n\t\tt.Errorf(\"Expected normalized email address, got %s\", l.Email)\n\t}\n}\n"
  },
  {
    "path": "pkg/api/collect.go",
    "content": "package api\n\nimport (\n\t\"encoding/base64\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\tlog \"github.com/sirupsen/logrus\"\n\n\t\"github.com/mssola/user_agent\"\n\t\"github.com/usefathom/fathom/pkg/aggregator\"\n\t\"github.com/usefathom/fathom/pkg/datastore\"\n\t\"github.com/usefathom/fathom/pkg/models\"\n)\n\ntype Collector struct {\n\tStore     datastore.Datastore\n\tPageviews chan *models.Pageview\n\n\t// buffer vars\n\tupdates []*models.Pageview\n\tinserts []*models.Pageview\n\tsizeu   int\n\tsizei   int\n}\n\nfunc NewCollector(store datastore.Datastore) *Collector {\n\tbufferCap := 100                         // persist every 100 pageviews, see https://github.com/usefathom/fathom/issues/132\n\tbufferTimeout := 1000 * time.Millisecond // or every 1000 ms, whichever comes first\n\n\tc := &Collector{\n\t\tStore:     store,\n\t\tPageviews: make(chan *models.Pageview),\n\t\tupdates:   make([]*models.Pageview, bufferCap),\n\t\tinserts:   make([]*models.Pageview, bufferCap),\n\t\tsizeu:     0,\n\t\tsizei:     0,\n\t}\n\tgo c.aggregate()\n\tgo c.worker(bufferCap, bufferTimeout)\n\treturn c\n}\n\nfunc (c *Collector) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\tif !shouldCollect(r) {\n\t\treturn\n\t}\n\n\tq := r.URL.Query()\n\tnow := time.Now()\n\n\tpageview := &models.Pageview{\n\t\tID:             uuid.NewString()[0:30],\n\t\tSiteTrackingID: q.Get(\"sid\"),\n\t\tHostname:       parseHostname(q.Get(\"h\")),\n\t\tPathname:       parsePathname(q.Get(\"p\")),\n\t\tIsNewVisitor:   q.Get(\"nv\") == \"1\",\n\t\tIsNewSession:   q.Get(\"ns\") == \"1\",\n\t\tIsUnique:       q.Get(\"u\") == \"1\",\n\t\tReferrer:       q.Get(\"r\"),\n\t\tIsFinished:     false,\n\t\tIsBounce:       true,\n\t\tDuration:       0,\n\t\tTimestamp:      now,\n\t}\n\n\t// push pageview onto channel to be inserted (in batch) later\n\tc.Pageviews <- pageview\n\n\t// indicate that we're not tracking user data, see https://github.com/usefathom/fathom/issues/65\n\tw.Header().Set(\"Tk\", \"N\")\n\n\t// headers to prevent caching\n\tw.Header().Set(\"Content-Type\", \"image/gif\")\n\tw.Header().Set(\"Expires\", \"Mon, 01 Jan 1990 00:00:00 GMT\")\n\tw.Header().Set(\"Cache-Control\", \"no-store\")\n\tw.Header().Set(\"Pragma\", \"no-cache\")\n\n\t// response, 1x1 px transparent GIF\n\tw.WriteHeader(http.StatusOK)\n\tb, _ := base64.StdEncoding.DecodeString(\"R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7\")\n\tw.Write(b)\n\n\t// find previous pageview by same visitor\n\tpreviousPageviewID := q.Get(\"pid\")\n\tif !pageview.IsNewSession && previousPageviewID != \"\" {\n\t\tpreviousPageview, err := c.Store.GetPageview(previousPageviewID)\n\t\tif err != nil && err != datastore.ErrNoResults {\n\t\t\tlog.Errorf(\"error getting previous pageview: %s\", err)\n\t\t\treturn\n\t\t}\n\n\t\t// if we have a recent pageview that is less than 30 minutes old\n\t\tif previousPageview != nil && previousPageview.Timestamp.After(now.Add(-30*time.Minute)) {\n\t\t\tpreviousPageview.Duration = (now.Unix() - previousPageview.Timestamp.Unix())\n\t\t\tpreviousPageview.IsBounce = false\n\t\t\tpreviousPageview.IsFinished = true\n\n\t\t\t// push onto channel to be updated (in batch) later\n\t\t\tc.Pageviews <- previousPageview\n\t\t}\n\t}\n}\n\nfunc (c *Collector) aggregate() {\n\tvar report aggregator.Report\n\n\tagg := aggregator.New(c.Store)\n\ttimeout := 1 * time.Minute\n\tagg.Run()\n\n\tfor {\n\t\tselect {\n\t\tcase <-time.After(timeout):\n\t\t\t// run aggregator at least once\n\t\t\treport = agg.Run()\n\n\t\t\t// if pool is not empty yet, keep running\n\t\t\tfor !report.PoolEmpty {\n\t\t\t\treport = agg.Run()\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (c *Collector) worker(cap int, timeout time.Duration) {\n\tvar size int\n\n\tfor {\n\t\tselect {\n\t\t// persist pageviews in buffer when buffer at capacity\n\t\tcase p := <-c.Pageviews:\n\t\t\tsize = c.buffer(p)\n\t\t\tif size >= cap {\n\t\t\t\tc.persist()\n\t\t\t}\n\n\t\t// or after timeout passed\n\t\tcase <-time.After(timeout):\n\t\t\tc.persist()\n\t\t}\n\t}\n}\n\nfunc (c *Collector) buffer(p *models.Pageview) int {\n\tif !p.IsFinished {\n\t\tc.inserts[c.sizei] = p\n\t\tc.sizei++\n\t} else {\n\t\tc.updates[c.sizeu] = p\n\t\tc.sizeu++\n\t}\n\n\treturn (c.sizeu + c.sizei)\n}\n\nfunc (c *Collector) persist() {\n\tif (c.sizeu + c.sizei) == 0 {\n\t\treturn\n\t}\n\n\tlog.Debugf(\"persisting %d pageviews (%d inserts, %d updates)\", (c.sizeu + c.sizei), c.sizei, c.sizeu)\n\n\tif err := c.Store.InsertPageviews(c.inserts[0:c.sizei]); err != nil {\n\t\tlog.Errorf(\"error inserting pageviews: %s\", err)\n\t}\n\n\tif err := c.Store.UpdatePageviews(c.updates[0:c.sizeu]); err != nil {\n\t\tlog.Errorf(\"error updating pageviews: %s\", err)\n\t}\n\n\t// reset buffer\n\tc.sizei = 0\n\tc.sizeu = 0\n}\n\nfunc shouldCollect(r *http.Request) bool {\n\t// abort if DNT header is set to \"1\" (these should have been filtered client-side already)\n\tif r.Header.Get(\"DNT\") == \"1\" {\n\t\treturn false\n\t}\n\n\t// don't track prerendered pages, see https://github.com/usefathom/fathom/issues/13\n\tif r.Header.Get(\"X-Moz\") == \"prefetch\" || r.Header.Get(\"X-Purpose\") == \"preview\" {\n\t\treturn false\n\t}\n\n\t// abort if this is a bot.\n\tua := user_agent.New(r.UserAgent())\n\tif ua.Bot() {\n\t\treturn false\n\t}\n\n\t// discard if required query vars are missing\n\trequiredQueryVars := []string{\"h\", \"p\"}\n\tq := r.URL.Query()\n\tfor _, k := range requiredQueryVars {\n\t\tif q.Get(k) == \"\" {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\nfunc parsePathname(p string) string {\n\treturn \"/\" + strings.TrimLeft(strings.TrimRight(p, \"/\"), \"/\")\n}\n\nfunc parseHostname(r string) string {\n\tu, err := url.Parse(r)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\treturn u.Scheme + \"://\" + u.Host\n}\n"
  },
  {
    "path": "pkg/api/collect_test.go",
    "content": "package api\n\nimport (\n\t\"net/http\"\n\t\"testing\"\n)\n\nfunc TestShouldCollect(t *testing.T) {\n\tr, _ := http.NewRequest(\"GET\", \"/\", nil)\n\tr.Header.Add(\"User-Agent\", \"Mozilla/1.0\")\n\tr.Header.Add(\"Referer\", \"http://usefathom.com/\")\n\tif v := shouldCollect(r); v != false {\n\t\tt.Errorf(\"Expected %#v, got %#v\", true, false)\n\t}\n}\n\nfunc TestParsePathname(t *testing.T) {\n\tif v := parsePathname(\"/\"); v != \"/\" {\n\t\tt.Errorf(\"error parsing pathname. expected %#v, got %#v\", \"/\", v)\n\t}\n\n\tif v := parsePathname(\"about\"); v != \"/about\" {\n\t\tt.Errorf(\"error parsing pathname. expected %#v, got %#v\", \"/about\", v)\n\t}\n\tif v := parsePathname(\"about/\"); v != \"/about\" {\n\t\tt.Errorf(\"error parsing pathname. expected %#v, got %#v\", \"/about\", v)\n\t}\n}\n\nfunc TestParseHostname(t *testing.T) {\n\te := \"https://usefathom.com\"\n\tif v := parseHostname(\"https://usefathom.com\"); v != e {\n\t\tt.Errorf(\"error parsing hostname. expected %#v, got %#v\", e, v)\n\t}\n\n\te = \"http://usefathom.com\"\n\tif v := parseHostname(\"http://usefathom.com\"); v != e {\n\t\tt.Errorf(\"error parsing hostname. expected %#v, got %#v\", e, v)\n\t}\n}\n"
  },
  {
    "path": "pkg/api/health.go",
    "content": "package api\n\nimport \"net/http\"\n\n// GET /health\nfunc (api *API) Health(w http.ResponseWriter, _ *http.Request) error {\n\tif err := api.database.Health(); err != nil {\n\t\tw.WriteHeader(http.StatusServiceUnavailable)\n\t\treturn err\n\t}\n\n\tw.WriteHeader(http.StatusOK)\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/api/http.go",
    "content": "package api\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\n\t\"github.com/gobuffalo/packr/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// Handler is our custom HTTP handler with error returns\ntype Handler func(w http.ResponseWriter, r *http.Request) error\n\ntype envelope struct {\n\tData  interface{} `json:\",omitempty\"`\n\tError interface{} `json:\",omitempty\"`\n}\n\nfunc (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\tif err := h(w, r); err != nil {\n\t\tHandleError(w, r, err)\n\t}\n}\n\n// HandlerFunc takes a custom Handler func and converts it to http.HandlerFunc\nfunc HandlerFunc(fn Handler) http.HandlerFunc {\n\treturn http.HandlerFunc(Handler(fn).ServeHTTP)\n}\n\n// HandleError handles errors\nfunc HandleError(w http.ResponseWriter, r *http.Request, err error) {\n\tlog.WithFields(log.Fields{\n\t\t\"request\": r.Method + \" \" + r.RequestURI,\n\t\t\"error\":   err,\n\t}).Error(\"error handling request\")\n\n\tw.WriteHeader(http.StatusInternalServerError)\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tw.Write([]byte(\"false\"))\n}\n\nfunc respond(w http.ResponseWriter, statusCode int, d interface{}) error {\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tw.WriteHeader(statusCode)\n\terr := json.NewEncoder(w).Encode(d)\n\treturn err\n}\n\nfunc serveFileHandler(box *packr.Box, filename string) http.Handler {\n\treturn HandlerFunc(serveFile(box, filename))\n}\n\nfunc serveFile(box *packr.Box, filename string) Handler {\n\treturn func(w http.ResponseWriter, r *http.Request) error {\n\t\tf, err := box.Open(filename)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefer f.Close()\n\n\t\td, err := f.Stat()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// setting security and cache headers\n\t\tw.Header().Set(\"X-Content-Type-Options\", \"nosniff\")\n\t\tw.Header().Set(\"X-Xss-Protection\", \"1; mode=block\")\n\t\tw.Header().Set(\"Cache-Control\", \"max-age=432000\") // 5 days\n\n\t\thttp.ServeContent(w, r, filename, d.ModTime(), f)\n\t\treturn nil\n\t}\n}\n\nfunc NotFoundHandler(box *packr.Box) http.Handler {\n\treturn HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {\n\t\tw.WriteHeader(http.StatusNotFound)\n\t\tw.Write(box.Bytes(\"404.html\"))\n\t\treturn nil\n\t})\n}\n"
  },
  {
    "path": "pkg/api/http_test.go",
    "content": "package api\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n)\n\nfunc TestRespond(t *testing.T) {\n\tw := httptest.NewRecorder()\n\trespond(w, http.StatusOK, 15)\n\n\tif w.Code != 200 {\n\t\tt.Errorf(\"Invalid response code\")\n\t}\n\n\t// assert json header\n\tif w.Header().Get(\"Content-Type\") != \"application/json\" {\n\t\tt.Errorf(\"Invalid response header for Content-Type\")\n\t}\n\n\t// assert json response\n\tvar d int\n\terr := json.NewDecoder(w.Body).Decode(&d)\n\tif err != nil {\n\t\tt.Errorf(\"Invalid response body: %s\", err)\n\t}\n\n}\n"
  },
  {
    "path": "pkg/api/page_stats.go",
    "content": "package api\n\nimport (\n\t\"net/http\"\n)\n\n// URL: /api/sites/{id:[0-9]+}/stats/pages/agg\nfunc (api *API) GetAggregatedPageStatsHandler(w http.ResponseWriter, r *http.Request) error {\n\tparams := GetRequestParams(r)\n\tresult, err := api.database.SelectAggregatedPageStats(params.SiteID, params.StartDate, params.EndDate, params.Offset, params.Limit)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn respond(w, http.StatusOK, envelope{Data: result})\n}\n\nfunc (api *API) GetAggregatedPageStatsPageviewsHandler(w http.ResponseWriter, r *http.Request) error {\n\tparams := GetRequestParams(r)\n\tresult, err := api.database.GetAggregatedPageStatsPageviews(params.SiteID, params.StartDate, params.EndDate)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn respond(w, http.StatusOK, envelope{Data: result})\n}\n"
  },
  {
    "path": "pkg/api/params.go",
    "content": "package api\n\nimport (\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/gorilla/mux\"\n)\n\n// Params defines the commonly used API parameters\ntype Params struct {\n\tSiteID    int64\n\tOffset    int\n\tLimit     int\n\tStartDate time.Time\n\tEndDate   time.Time\n}\n\n// GetRequestParams parses the query parameters and returns commonly used API parameters, with defaults\nfunc GetRequestParams(r *http.Request) *Params {\n\tparams := &Params{\n\t\tSiteID:    0,\n\t\tLimit:     20,\n\t\tOffset:    0,\n\t\tStartDate: time.Now(),\n\t\tEndDate:   time.Now().AddDate(0, 0, -7),\n\t}\n\n\tvars := mux.Vars(r)\n\tif _, ok := vars[\"id\"]; ok {\n\t\tif siteID, err := strconv.ParseInt(vars[\"id\"], 10, 64); err == nil {\n\t\t\tparams.SiteID = siteID\n\t\t}\n\t}\n\n\tq := r.URL.Query()\n\tif q.Get(\"after\") != \"\" {\n\t\tif after, err := strconv.ParseInt(q.Get(\"after\"), 10, 64); err == nil && after > 0 {\n\t\t\tparams.StartDate = time.Unix(after, 0)\n\t\t}\n\t}\n\n\tif q.Get(\"before\") != \"\" {\n\t\tif before, err := strconv.ParseInt(q.Get(\"before\"), 10, 64); err == nil && before > 0 {\n\t\t\tparams.EndDate = time.Unix(before, 0)\n\t\t}\n\t}\n\n\tif q.Get(\"limit\") != \"\" {\n\t\tif limit, err := strconv.Atoi(q.Get(\"limit\")); err == nil && limit > 0 {\n\t\t\tparams.Limit = limit\n\t\t}\n\t}\n\n\tif q.Get(\"offset\") != \"\" {\n\t\tif offset, err := strconv.Atoi(q.Get(\"offset\")); err == nil && offset > 0 {\n\t\t\tparams.Offset = offset\n\t\t}\n\t}\n\n\treturn params\n}\n"
  },
  {
    "path": "pkg/api/params_test.go",
    "content": "package api\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestGetRequestParams(t *testing.T) {\n\tstartDate := time.Now().AddDate(0, 0, -12)\n\tendDate := time.Now().AddDate(0, 0, -5)\n\tlimit := 50\n\n\turl := fmt.Sprintf(\"/?after=%d&before=%d&limit=%d\", startDate.Unix(), endDate.Unix(), limit)\n\tr, _ := http.NewRequest(\"GET\", url, nil)\n\tparams := GetRequestParams(r)\n\n\tif params.Limit != 50 {\n\t\tt.Errorf(\"Expected %#v, got %#v\", 50, params.Limit)\n\t}\n\n\tif startDate.Unix() != params.StartDate.Unix() {\n\t\tt.Errorf(\"Expected %#v, got %#v\", startDate.Format(\"2006-01-02 15:04\"), params.StartDate.Format(\"2006-01-02 15:04\"))\n\t}\n\n\tif params.EndDate.Unix() != endDate.Unix() {\n\t\tt.Errorf(\"Expected %#v, got %#v\", endDate.Format(\"2006-01-02 15:04\"), params.EndDate.Format(\"2006-01-02 15:04\"))\n\t}\n\n}\n"
  },
  {
    "path": "pkg/api/referrer_stats.go",
    "content": "package api\n\nimport (\n\t\"net/http\"\n)\n\nfunc (api *API) GetAggregatedReferrerStatsHandler(w http.ResponseWriter, r *http.Request) error {\n\tparams := GetRequestParams(r)\n\tresult, err := api.database.SelectAggregatedReferrerStats(params.SiteID, params.StartDate, params.EndDate, params.Offset, params.Limit)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn respond(w, http.StatusOK, envelope{Data: result})\n}\n\nfunc (api *API) GetAggregatedReferrerStatsPageviewsHandler(w http.ResponseWriter, r *http.Request) error {\n\tparams := GetRequestParams(r)\n\tresult, err := api.database.GetAggregatedReferrerStatsPageviews(params.SiteID, params.StartDate, params.EndDate)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn respond(w, http.StatusOK, envelope{Data: result})\n}\n"
  },
  {
    "path": "pkg/api/routes.go",
    "content": "package api\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/gobuffalo/packr/v2\"\n\t\"github.com/gorilla/mux\"\n)\n\nfunc (api *API) Routes() *mux.Router {\n\t// register routes\n\tr := mux.NewRouter()\n\tr.Handle(\"/collect\", NewCollector(api.database)).Methods(http.MethodGet)\n\n\tr.Handle(\"/api/session\", HandlerFunc(api.GetSession)).Methods(http.MethodGet)\n\tr.Handle(\"/api/session\", HandlerFunc(api.CreateSession)).Methods(http.MethodPost)\n\tr.Handle(\"/api/session\", HandlerFunc(api.DeleteSession)).Methods(http.MethodDelete)\n\n\tr.Handle(\"/api/sites\", api.Authorize(HandlerFunc(api.GetSitesHandler))).Methods(http.MethodGet)\n\tr.Handle(\"/api/sites\", api.Authorize(HandlerFunc(api.SaveSiteHandler))).Methods(http.MethodPost)\n\tr.Handle(\"/api/sites/{id:[0-9]+}\", api.Authorize(HandlerFunc(api.SaveSiteHandler))).Methods(http.MethodPost)\n\tr.Handle(\"/api/sites/{id:[0-9]+}\", api.Authorize(HandlerFunc(api.DeleteSiteHandler))).Methods(http.MethodDelete)\n\n\tr.Handle(\"/api/sites/{id:[0-9]+}/stats/site\", api.Authorize(HandlerFunc(api.GetSiteStatsHandler))).Methods(http.MethodGet)\n\tr.Handle(\"/api/sites/{id:[0-9]+}/stats/site/agg\", api.Authorize(HandlerFunc(api.GetAggregatedSiteStatsHandler))).Methods(http.MethodGet)\n\tr.Handle(\"/api/sites/{id:[0-9]+}/stats/site/realtime\", api.Authorize(HandlerFunc(api.GetSiteStatsRealtimeHandler))).Methods(http.MethodGet)\n\n\tr.Handle(\"/api/sites/{id:[0-9]+}/stats/pages/agg\", api.Authorize(HandlerFunc(api.GetAggregatedPageStatsHandler))).Methods(http.MethodGet)\n\tr.Handle(\"/api/sites/{id:[0-9]+}/stats/pages/agg/pageviews\", api.Authorize(HandlerFunc(api.GetAggregatedPageStatsPageviewsHandler))).Methods(http.MethodGet)\n\n\tr.Handle(\"/api/sites/{id:[0-9]+}/stats/referrers/agg\", api.Authorize(HandlerFunc(api.GetAggregatedReferrerStatsHandler))).Methods(http.MethodGet)\n\tr.Handle(\"/api/sites/{id:[0-9]+}/stats/referrers/agg/pageviews\", api.Authorize(HandlerFunc(api.GetAggregatedReferrerStatsPageviewsHandler))).Methods(http.MethodGet)\n\n\tr.Handle(\"/health\", HandlerFunc(api.Health)).Methods(http.MethodGet)\n\n\t// static assets & 404 handler\n\tbox := packr.NewBox(\"./../../assets/build\")\n\tr.Path(\"/tracker.js\").Handler(serveTrackerFile(box))\n\tr.Path(\"/\").Handler(serveFileHandler(box, \"index.html\"))\n\tr.Path(\"/index.html\").Handler(serveFileHandler(box, \"index.html\"))\n\tr.PathPrefix(\"/assets\").Handler(http.StripPrefix(\"/assets\", http.FileServer(box)))\n\tr.NotFoundHandler = NotFoundHandler(box)\n\n\treturn r\n}\n\nfunc serveTrackerFile(box *packr.Box) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Set(\"Tk\", \"N\")\n\t\tnext := serveFile(box, \"js/tracker.js\")\n\t\tnext.ServeHTTP(w, r)\n\t})\n}\n"
  },
  {
    "path": "pkg/api/site_stats.go",
    "content": "package api\n\nimport (\n\t\"net/http\"\n)\n\n// URL: /api/sites/{id:[0-9]+}/stats/site/agg\nfunc (api *API) GetAggregatedSiteStatsHandler(w http.ResponseWriter, r *http.Request) error {\n\tparams := GetRequestParams(r)\n\tresult, err := api.database.GetAggregatedSiteStats(params.SiteID, params.StartDate, params.EndDate)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn respond(w, http.StatusOK, envelope{Data: result})\n}\n\n// URL: /api/sites/{id:[0-9]+}/stats/site/realtime\nfunc (api *API) GetSiteStatsRealtimeHandler(w http.ResponseWriter, r *http.Request) error {\n\tparams := GetRequestParams(r)\n\tresult, err := api.database.GetRealtimeVisitorCount(params.SiteID)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn respond(w, http.StatusOK, envelope{Data: result})\n}\n\n// URL: /api/sites/{id:[0-9]+}/stats/site\nfunc (api *API) GetSiteStatsHandler(w http.ResponseWriter, r *http.Request) error {\n\tparams := GetRequestParams(r)\n\tresult, err := api.database.SelectSiteStats(params.SiteID, params.StartDate, params.EndDate)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn respond(w, http.StatusOK, envelope{Data: result})\n}\n"
  },
  {
    "path": "pkg/api/sites.go",
    "content": "package api\n\nimport (\n\t\"encoding/json\"\n\t\"math/rand\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/gorilla/mux\"\n\t\"github.com/usefathom/fathom/pkg/models\"\n)\n\n// seed rand pkg on program init\nfunc init() {\n\trand.Seed(time.Now().UTC().UnixNano())\n}\n\n// GET /api/sites\nfunc (api *API) GetSitesHandler(w http.ResponseWriter, r *http.Request) error {\n\tresult, err := api.database.GetSites()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn respond(w, http.StatusOK, envelope{Data: result})\n}\n\n// POST /api/sites\n// POST /api/sites/{id}\nfunc (api *API) SaveSiteHandler(w http.ResponseWriter, r *http.Request) error {\n\tvar s *models.Site\n\tvars := mux.Vars(r)\n\tsid, ok := vars[\"id\"]\n\tif ok {\n\t\tid, err := strconv.ParseInt(sid, 10, 64)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\ts, err = api.database.GetSite(id)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\ts = &models.Site{\n\t\t\tTrackingID: generateTrackingID(),\n\t\t}\n\t}\n\n\terr := json.NewDecoder(r.Body).Decode(s)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err := api.database.SaveSite(s); err != nil {\n\t\treturn err\n\t}\n\n\treturn respond(w, http.StatusOK, envelope{Data: s})\n}\n\n// DELETE /api/sites/{id}\nfunc (api *API) DeleteSiteHandler(w http.ResponseWriter, r *http.Request) error {\n\tvars := mux.Vars(r)\n\tid, err := strconv.ParseInt(vars[\"id\"], 10, 64)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err := api.database.DeleteSite(&models.Site{ID: id}); err != nil {\n\t\treturn err\n\t}\n\n\treturn respond(w, http.StatusOK, envelope{Data: true})\n}\n\nfunc generateTrackingID() string {\n\treturn randomString(5)\n}\n\nfunc randomString(len int) string {\n\tbytes := make([]byte, len)\n\tfor i := 0; i < len; i++ {\n\t\tbytes[i] = byte(65 + rand.Intn(25)) //a=65 and z = 65+25\n\t}\n\n\treturn string(bytes)\n}\n"
  },
  {
    "path": "pkg/cli/cli.go",
    "content": "package cli\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/urfave/cli\"\n\t\"github.com/usefathom/fathom/pkg/config\"\n\t\"github.com/usefathom/fathom/pkg/datastore\"\n)\n\ntype App struct {\n\t*cli.App\n\tdatabase datastore.Datastore\n\tconfig   *config.Config\n}\n\n// CLI application\nvar app *App\n\n// Run parses the CLI arguments & run application command\nfunc Run(version string, commit string, buildDate string) error {\n\t// force all times in UTC, regardless of server timezone\n\ttime.Local = time.UTC\n\n\t// setup CLI app\n\tapp = &App{cli.NewApp(), nil, nil}\n\tapp.Name = \"Fathom\"\n\tapp.Usage = \"simple & transparent website analytics\"\n\tapp.Version = fmt.Sprintf(\"%v, commit %v, built at %v\", strings.TrimPrefix(version, \"v\"), commit, buildDate)\n\tapp.HelpName = \"fathom\"\n\tapp.Flags = []cli.Flag{\n\t\tcli.StringFlag{\n\t\t\tName:  \"config, c\",\n\t\t\tValue: \".env\",\n\t\t\tUsage: \"Load configuration from `FILE`\",\n\t\t},\n\t}\n\tapp.Before = before\n\tapp.After = after\n\tapp.Commands = []cli.Command{\n\t\tserverCmd,\n\t\tuserCmd,\n\t\tstatsCmd,\n\t}\n\n\tif len(os.Args) < 2 || os.Args[1] != \"--version\" {\n\t\tlog.Printf(\"%s version %s\", app.Name, app.Version)\n\t}\n\n\terr := app.Run(os.Args)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc before(c *cli.Context) error {\n\tconfigFile := c.String(\"config\")\n\tconfig.LoadEnv(configFile)\n\tapp.config = config.Parse()\n\tapp.database = datastore.New(app.config.Database)\n\treturn nil\n}\n\nfunc after(c *cli.Context) error {\n\terr := app.database.Close()\n\treturn err\n}\n"
  },
  {
    "path": "pkg/cli/server.go",
    "content": "package cli\n\nimport (\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/urfave/cli\"\n\n\t\"github.com/gorilla/handlers\"\n\t\"github.com/usefathom/fathom/pkg/api\"\n\t\"golang.org/x/crypto/acme/autocert\"\n)\n\nvar serverCmd = cli.Command{\n\tName:    \"server\",\n\tAliases: []string{\"s\"},\n\tUsage:   \"start the fathom web server\",\n\tAction:  server,\n\tFlags: []cli.Flag{\n\t\tcli.StringFlag{\n\t\t\tEnvVar: \"FATHOM_SERVER_ADDR,PORT\",\n\t\t\tName:   \"addr,port\",\n\t\t\tUsage:  \"server address\",\n\t\t\tValue:  \":8080\",\n\t\t},\n\n\t\tcli.BoolFlag{\n\t\t\tEnvVar: \"FATHOM_LETS_ENCRYPT\",\n\t\t\tName:   \"lets-encrypt\",\n\t\t},\n\n\t\tcli.BoolFlag{\n\t\t\tEnvVar: \"FATHOM_GZIP\",\n\t\t\tName:   \"gzip\",\n\t\t\tUsage:  \"enable gzip compression\",\n\t\t},\n\n\t\tcli.StringFlag{\n\t\t\tEnvVar: \"FATHOM_HOSTNAME\",\n\t\t\tName:   \"hostname\",\n\t\t\tUsage:  \"domain when using --lets-encrypt\",\n\t\t},\n\n\t\tcli.BoolFlag{\n\t\t\tEnvVar: \"FATHOM_DEBUG\",\n\t\t\tName:   \"debug, d\",\n\t\t},\n\t},\n}\n\nfunc server(c *cli.Context) error {\n\tvar h http.Handler\n\ta := api.New(app.database, app.config.Secret)\n\th = a.Routes()\n\n\t// set debug log level if --debug was passed\n\tif c.Bool(\"debug\") {\n\t\tlog.SetLevel(log.DebugLevel)\n\t} else {\n\t\tlog.SetLevel(log.WarnLevel)\n\t}\n\n\t// set gzip compression if --gzip was passed\n\tif c.Bool(\"gzip\") {\n\t\th = handlers.CompressHandler(h)\n\t}\n\n\t// if addr looks like a number, prefix with :\n\taddr := c.String(\"addr\")\n\tif _, err := strconv.Atoi(addr); err == nil {\n\t\taddr = \":\" + addr\n\t}\n\n\t// start server without letsencrypt / tls enabled\n\tif !c.Bool(\"lets-encrypt\") {\n\t\t// start listening\n\t\tserver := &http.Server{\n\t\t\tAddr:         addr,\n\t\t\tHandler:      h,\n\t\t\tReadTimeout:  10 * time.Second,\n\t\t\tWriteTimeout: 10 * time.Second,\n\t\t}\n\n\t\tlog.Infof(\"Server is now listening on %s\", server.Addr)\n\t\tlog.Fatal(server.ListenAndServe())\n\t\treturn nil\n\t}\n\n\t// start server with autocert (letsencrypt)\n\thostname := c.String(\"hostname\")\n\tlog.Infof(\"Server is now listening on %s:443\", hostname)\n\tlog.Fatal(http.Serve(autocert.NewListener(hostname), h))\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/cli/stats.go",
    "content": "package cli\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/urfave/cli\"\n)\n\nvar statsCmd = cli.Command{\n\tName:   \"stats\",\n\tUsage:  \"view stats\",\n\tAction: stats,\n\tFlags: []cli.Flag{\n\t\tcli.Int64Flag{\n\t\t\tName:  \"site-id\",\n\t\t\tUsage: \"ID of the site to retrieve stats for\",\n\t\t},\n\t\tcli.StringFlag{\n\t\t\tName:  \"start-date\",\n\t\t\tUsage: \"start date, expects a date in format 2006-01-02\",\n\t\t},\n\t\tcli.StringFlag{\n\t\t\tName:  \"end-date\",\n\t\t\tUsage: \"end date, expects a date in format 2006-01-02\",\n\t\t},\n\t\tcli.BoolFlag{\n\t\t\tName:  \"json\",\n\t\t\tUsage: \"get a json response\",\n\t\t},\n\t},\n}\n\nfunc stats(c *cli.Context) error {\n\tstart, _ := time.Parse(\"2006-01-02\", c.String(\"start-date\"))\n\tif start.IsZero() {\n\t\treturn errors.New(\"Invalid argument: supply a valid --start-date\")\n\t}\n\n\tend, _ := time.Parse(\"2006-01-02\", c.String(\"end-date\"))\n\tif end.IsZero() {\n\t\treturn errors.New(\"Invalid argument: supply a valid --end-date\")\n\t}\n\n\t// TODO: add method for getting total sum of pageviews across sites\n\tsiteID := c.Int64(\"site-id\")\n\tresult, err := app.database.GetAggregatedSiteStats(siteID, start, end)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif c.Bool(\"json\") {\n\t\treturn json.NewEncoder(os.Stdout).Encode(result)\n\t}\n\n\tfmt.Printf(\"%s - %s\\n\", start.Format(\"Jan 01, 2006\"), end.Format(\"Jan 01, 2006\"))\n\tfmt.Printf(\"===========================\\n\")\n\tfmt.Printf(\"Visitors: \\t%d\\n\", result.Visitors)\n\tfmt.Printf(\"Pageviews: \\t%d\\n\", result.Pageviews)\n\tfmt.Printf(\"Sessions: \\t%d\\n\", result.Sessions)\n\tfmt.Printf(\"Avg duration: \\t%s\\n\", result.FormattedDuration())\n\tfmt.Printf(\"Bounce rate: \\t%.0f%%\\n\", result.BounceRate*100.00)\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/cli/user.go",
    "content": "package cli\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/usefathom/fathom/pkg/models\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/urfave/cli\"\n\t\"github.com/usefathom/fathom/pkg/datastore\"\n)\n\nvar userCmd = cli.Command{\n\tName:   \"user\",\n\tUsage:  \"manage registered admin users\",\n\tAction: userAdd,\n\tSubcommands: []cli.Command{\n\t\tcli.Command{\n\t\t\tName:    \"add\",\n\t\t\tAliases: []string{\"register\"},\n\t\t\tAction:  userAdd,\n\t\t\tFlags: []cli.Flag{\n\t\t\t\tcli.StringFlag{\n\t\t\t\t\tName:  \"email, e\",\n\t\t\t\t\tUsage: \"user email\",\n\t\t\t\t},\n\t\t\t\tcli.StringFlag{\n\t\t\t\t\tName:  \"password, p\",\n\t\t\t\t\tUsage: \"user password\",\n\t\t\t\t},\n\t\t\t\tcli.BoolFlag{\n\t\t\t\t\tName:  \"skip-bcrypt\",\n\t\t\t\t\tUsage: \"store password string as-is, skipping bcrypt\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tcli.Command{\n\t\t\tName:   \"delete\",\n\t\t\tAction: userDelete,\n\t\t\tFlags: []cli.Flag{\n\t\t\t\tcli.StringFlag{\n\t\t\t\t\tName:  \"email, e\",\n\t\t\t\t\tUsage: \"user email\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n}\n\nfunc userAdd(c *cli.Context) error {\n\temail := c.String(\"email\")\n\tif email == \"\" {\n\t\treturn errors.New(\"Invalid arguments: missing email\")\n\t}\n\n\tpassword := c.String(\"password\")\n\tif password == \"\" {\n\t\treturn errors.New(\"Invalid arguments: missing password\")\n\t}\n\n\t_, err := app.database.GetUserByEmail(email)\n\tif err != nil {\n\t\tif err == datastore.ErrNoResults {\n\t\t\tuser := models.NewUser(email, password)\n\n\t\t\t// set password manually if --skip-bcrypt was given\n\t\t\t// this is used to supply an already encrypted password string\n\t\t\tif c.Bool(\"skip-bcrypt\") {\n\t\t\t\tuser.Password = password\n\t\t\t}\n\n\t\t\tif err := app.database.SaveUser(&user); err != nil {\n\t\t\t\treturn fmt.Errorf(\"Error creating user: %s\", err)\n\t\t\t}\n\n\t\t\tlog.Infof(\"Created user %s\", user.Email)\n\t\t\treturn nil\n\t\t}\n\n\t\treturn err\n\t}\n\tlog.Infof(\"A user with this email %s already exists\", email)\n\treturn nil\n\n}\n\nfunc userDelete(c *cli.Context) error {\n\temail := c.String(\"email\")\n\tif email == \"\" {\n\t\treturn errors.New(\"Invalid arguments: missing email\")\n\t}\n\n\tuser, err := app.database.GetUserByEmail(email)\n\tif err != nil {\n\t\tif err == datastore.ErrNoResults {\n\t\t\treturn fmt.Errorf(\"No user with email %s\", email)\n\t\t}\n\n\t\treturn err\n\t}\n\n\tif err := app.database.DeleteUser(user); err != nil {\n\t\treturn err\n\t}\n\n\tlog.Infof(\"Deleted user %s\", user.Email)\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/config/config.go",
    "content": "package config\n\nimport (\n\t\"math/rand\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/joho/godotenv\"\n\t\"github.com/kelseyhightower/envconfig\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/usefathom/fathom/pkg/datastore/sqlstore\"\n)\n\n// Config wraps the configuration structs for the various application parts\ntype Config struct {\n\tDatabase *sqlstore.Config\n\tSecret   string\n}\n\n// LoadEnv loads env values from the supplied file\nfunc LoadEnv(file string) {\n\tif file == \"\" {\n\t\tlog.Warn(\"Missing configuration file. Using defaults.\")\n\t\treturn\n\t}\n\n\tabsFile, _ := filepath.Abs(file)\n\t_, err := os.Stat(absFile)\n\tfileNotExists := os.IsNotExist(err)\n\n\tif fileNotExists {\n\t\tlog.Warnf(\"Error reading configuration. File `%s` does not exist.\", file)\n\t\treturn\n\t}\n\n\tlog.Printf(\"Configuration file: %s\", absFile)\n\n\t// read file into env values\n\terr = godotenv.Load(absFile)\n\tif err != nil {\n\t\tlog.Fatalf(\"Error parsing configuration file: %s\", err)\n\t}\n}\n\n// Parse environment into a Config struct\nfunc Parse() *Config {\n\tvar cfg Config\n\n\t// with config file loaded into env values, we can now parse env into our config struct\n\terr := envconfig.Process(\"Fathom\", &cfg)\n\tif err != nil {\n\t\tlog.Fatalf(\"Error parsing configuration from environment: %s\", err)\n\t}\n\n\tif cfg.Database.URL != \"\" {\n\t\tu, err := url.Parse(cfg.Database.URL)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"Error parsing DATABASE_URL from environment: %s\", err)\n\t\t}\n\t\tif u.Scheme == \"postgres\" {\n\t\t\tcfg.Database.Driver = \"postgres\"\n\t\t}\n\t}\n\n\t// alias sqlite to sqlite3\n\tif cfg.Database.Driver == \"sqlite\" {\n\t\tcfg.Database.Driver = \"sqlite3\"\n\t}\n\n\t// use absolute path to sqlite3 database\n\tif cfg.Database.Driver == \"sqlite3\" {\n\t\tcfg.Database.Name, _ = filepath.Abs(cfg.Database.Name)\n\t}\n\n\t// if secret key is empty, use a randomly generated one\n\tif cfg.Secret == \"\" {\n\t\tcfg.Secret = randomString(40)\n\t}\n\n\treturn &cfg\n}\n\nfunc randomString(len int) string {\n\tbytes := make([]byte, len)\n\tfor i := 0; i < len; i++ {\n\t\tbytes[i] = byte(65 + rand.Intn(25)) //A=65 and Z = 65+25\n\t}\n\n\treturn string(bytes)\n}\n"
  },
  {
    "path": "pkg/config/config_test.go",
    "content": "package config\n\nimport (\n\t\"io/ioutil\"\n\t\"os\"\n\t\"testing\"\n)\n\nfunc TestLoadEnv(t *testing.T) {\n\tbefore := len(os.Environ())\n\tLoadEnv(\"\")\n\tLoadEnv(\"1230\")\n\tafter := len(os.Environ())\n\n\tif before != after {\n\t\tt.Errorf(\"Expected the same number of env values\")\n\t}\n\n\tdata := []byte(\"FATHOM_DATABASE_DRIVER=\\\"sqlite3\\\"\")\n\tioutil.WriteFile(\"env_values\", data, 0644)\n\tdefer os.Remove(\"env_values\")\n\n\tLoadEnv(\"env_values\")\n\n\tgot := os.Getenv(\"FATHOM_DATABASE_DRIVER\")\n\tif got != \"sqlite3\" {\n\t\tt.Errorf(\"Expected %v, got %v\", \"sqlite3\", got)\n\t}\n}\n\nfunc TestParse(t *testing.T) {\n\t// empty config, should not fatal\n\tcfg := Parse()\n\tif cfg.Secret == \"\" {\n\t\tt.Errorf(\"expected secret, got empty string\")\n\t}\n\n\tsecret := \"my-super-secret-string\"\n\tos.Setenv(\"FATHOM_SECRET\", secret)\n\tcfg = Parse()\n\tif cfg.Secret != secret {\n\t\tt.Errorf(\"Expected %#v, got %#v\", secret, cfg.Secret)\n\t}\n\n\tos.Setenv(\"FATHOM_DATABASE_DRIVER\", \"sqlite\")\n\tcfg = Parse()\n\tif cfg.Database.Driver != \"sqlite3\" {\n\t\tt.Errorf(\"expected %#v, got %#v\", \"sqlite3\", cfg.Database.Driver)\n\t}\n}\n\nfunc TestDatabaseURL(t *testing.T) {\n\tdata := []byte(\"FATHOM_DATABASE_URL=\\\"postgres://dbuser:dbsecret@dbhost:1234/dbname\\\"\")\n\tioutil.WriteFile(\"env_values\", data, 0644)\n\tdefer os.Remove(\"env_values\")\n\n\tLoadEnv(\"env_values\")\n\tcfg := Parse()\n\tdriver := \"postgres\"\n\turl := \"postgres://dbuser:dbsecret@dbhost:1234/dbname\"\n\tif cfg.Database.Driver != driver {\n\t\tt.Errorf(\"Expected %#v, got %#v\", driver, cfg.Database.Driver)\n\t}\n\tif cfg.Database.URL != url {\n\t\tt.Errorf(\"Expected %#v, got %#v\", url, cfg.Database.URL)\n\t}\n}\n\nfunc TestRandomString(t *testing.T) {\n\tr1 := randomString(10)\n\tr2 := randomString(10)\n\n\tif r1 == r2 {\n\t\tt.Errorf(\"expected two different strings, got %#v\", r1)\n\t}\n\n\tif l := len(r1); l != 10 {\n\t\tt.Errorf(\"expected string of length %d, got string of length %d\", 10, l)\n\t}\n}\n"
  },
  {
    "path": "pkg/datastore/datastore.go",
    "content": "package datastore\n\nimport (\n\t\"time\"\n\n\t\"github.com/usefathom/fathom/pkg/datastore/sqlstore\"\n\t\"github.com/usefathom/fathom/pkg/models\"\n)\n\n// ErrNoResults is returned whenever a single-item query returns 0 results\nvar ErrNoResults = sqlstore.ErrNoResults // ???\n\n// Datastore represents a database implementations\ntype Datastore interface {\n\t// users\n\tGetUser(int64) (*models.User, error)\n\tGetUserByEmail(string) (*models.User, error)\n\tSaveUser(*models.User) error\n\tDeleteUser(*models.User) error\n\tCountUsers() (int64, error)\n\n\t// sites\n\tGetSites() ([]*models.Site, error)\n\tGetSite(id int64) (*models.Site, error)\n\tSaveSite(s *models.Site) error\n\tDeleteSite(s *models.Site) error\n\n\t// site stats\n\tGetSiteStats(int64, time.Time) (*models.SiteStats, error)\n\tGetAggregatedSiteStats(int64, time.Time, time.Time) (*models.SiteStats, error)\n\tSelectSiteStats(int64, time.Time, time.Time) ([]*models.SiteStats, error)\n\tGetRealtimeVisitorCount(int64) (int64, error)\n\tSaveSiteStats(*models.SiteStats) error\n\n\t// pageviews\n\tInsertPageviews([]*models.Pageview) error\n\tUpdatePageviews([]*models.Pageview) error\n\tGetPageview(string) (*models.Pageview, error)\n\tGetProcessablePageviews(limit int) ([]*models.Pageview, error)\n\tDeletePageviews([]*models.Pageview) error\n\n\t// page stats\n\tGetPageStats(int64, time.Time, int64, int64) (*models.PageStats, error)\n\tSavePageStats(*models.PageStats) error\n\tSelectAggregatedPageStats(int64, time.Time, time.Time, int, int) ([]*models.PageStats, error)\n\tGetAggregatedPageStatsPageviews(int64, time.Time, time.Time) (int64, error)\n\n\t// referrer stats\n\tGetReferrerStats(int64, time.Time, int64, int64) (*models.ReferrerStats, error)\n\tSaveReferrerStats(*models.ReferrerStats) error\n\tSelectAggregatedReferrerStats(int64, time.Time, time.Time, int, int) ([]*models.ReferrerStats, error)\n\tGetAggregatedReferrerStatsPageviews(int64, time.Time, time.Time) (int64, error)\n\n\t// hostnames\n\tHostnameID(name string) (int64, error)\n\tPathnameID(name string) (int64, error)\n\n\t// misc\n\tHealth() error\n\tClose() error\n}\n\n// New instantiates a new datastore from the given configuration struct\nfunc New(c *sqlstore.Config) Datastore {\n\treturn sqlstore.New(c)\n}\n"
  },
  {
    "path": "pkg/datastore/sqlstore/config.go",
    "content": "package sqlstore\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n\n\tmysql \"github.com/go-sql-driver/mysql\"\n)\n\ntype Config struct {\n\tDriver   string `default:\"sqlite3\"`\n\tURL      string `default:\"\"`\n\tHost     string `default:\"\"`\n\tUser     string `default:\"\"`\n\tPassword string `default:\"\"`\n\tName     string `default:\"fathom.db\"`\n\tSSLMode  string `default:\"\"`\n}\n\nfunc (c *Config) DSN() string {\n\tvar dsn string\n\n\t// if FATHOM_DATABASE_URL was set, use that\n\t// this relies on the user to set the appropriate parameters, eg ?parseTime=true when using MySQL\n\tif c.URL != \"\" {\n\t\treturn c.URL\n\t}\n\n\t// otherwise, generate from individual fields\n\tswitch c.Driver {\n\tcase POSTGRES:\n\t\tif c.Host != \"\" {\n\t\t\tdsn += \" host=\" + c.Host\n\t\t}\n\t\tif c.Name != \"\" {\n\t\t\tdsn += \" dbname=\" + c.Name\n\t\t}\n\t\tif c.User != \"\" {\n\t\t\tdsn += \" user=\" + c.User\n\t\t}\n\t\tif c.Password != \"\" {\n\t\t\tdsn += \" password=\" + c.Password\n\t\t}\n\t\tif c.SSLMode != \"\" {\n\t\t\tdsn += \" sslmode=\" + c.SSLMode\n\t\t}\n\n\t\tdsn = strings.TrimSpace(dsn)\n\tcase MYSQL:\n\t\tmc := mysql.NewConfig()\n\t\tmc.User = c.User\n\t\tmc.Passwd = c.Password\n\t\tmc.Addr = c.Host\n\t\tmc.Net = \"tcp\"\n\t\tmc.DBName = c.Name\n\t\tmc.Params = map[string]string{\n\t\t\t\"parseTime\": \"true\",\n\t\t}\n\t\tif c.SSLMode != \"\" {\n\t\t\tmc.Params[\"tls\"] = c.SSLMode\n\t\t}\n\t\tdsn = mc.FormatDSN()\n\tcase SQLITE:\n\t\tdsn = c.Name + \"?_busy_timeout=10000\"\n\t}\n\n\treturn dsn\n}\n\n// Dbname returns the database name, either from config values or from the connection URL\nfunc (c *Config) Dbname() string {\n\tif c.Name != \"\" {\n\t\treturn c.Name\n\t}\n\n\tre := regexp.MustCompile(`(?:dbname=|[^\\/]?\\/)(\\w+)`)\n\tm := re.FindStringSubmatch(c.URL)\n\tif len(m) > 1 {\n\t\treturn m[1]\n\t}\n\n\treturn \"\"\n}\n"
  },
  {
    "path": "pkg/datastore/sqlstore/config_test.go",
    "content": "package sqlstore\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\nfunc TestConfigDSN(t *testing.T) {\n\tc := Config{\n\t\tDriver:   \"postgres\",\n\t\tUser:     \"john\",\n\t\tPassword: \"foo\",\n\t}\n\te := fmt.Sprintf(\"user=%s password=%s\", c.User, c.Password)\n\tif v := c.DSN(); v != e {\n\t\tt.Errorf(\"Invalid DSN. Expected %s, got %s\", e, v)\n\t}\n\n\tc = Config{\n\t\tDriver:   \"postgres\",\n\t\tUser:     \"john\",\n\t\tPassword: \"foo\",\n\t\tSSLMode:  \"disable\",\n\t}\n\te = fmt.Sprintf(\"user=%s password=%s sslmode=%s\", c.User, c.Password, c.SSLMode)\n\tif v := c.DSN(); v != e {\n\t\tt.Errorf(\"Invalid DSN. Expected %s, got %s\", e, v)\n\t}\n}\n\nfunc TestConfigDbname(t *testing.T) {\n\tvar c Config\n\n\tc = Config{\n\t\tURL: \"postgres://pqgotest:password@localhost/pqgotest?sslmode=verify-full\",\n\t}\n\tif e, v := \"pqgotest\", c.Dbname(); v != e {\n\t\tt.Errorf(\"Expected %q, got %q\", e, v)\n\t}\n\n\tc = Config{\n\t\tURL: \"root@tcp(host.myhost)/mysqltest?loc=Local\",\n\t}\n\tif e, v := \"mysqltest\", c.Dbname(); v != e {\n\t\tt.Errorf(\"Expected %q, got %q\", e, v)\n\t}\n\n\tc = Config{\n\t\tURL: \"/mysqltest?loc=Local&parseTime=true\",\n\t}\n\tif e, v := \"mysqltest\", c.Dbname(); v != e {\n\t\tt.Errorf(\"Expected %q, got %q\", e, v)\n\t}\n}\n"
  },
  {
    "path": "pkg/datastore/sqlstore/hostnames.go",
    "content": "package sqlstore\n\nimport (\n\t\"database/sql\"\n)\n\nfunc (db *sqlstore) HostnameID(name string) (int64, error) {\n\tvar id int64\n\tquery := db.Rebind(\"SELECT id FROM hostnames WHERE name = ? LIMIT 1\")\n\terr := db.Get(&id, query, name)\n\n\tif err == sql.ErrNoRows {\n\t\t// Postgres does not support LastInsertID, so use a \"... RETURNING\" select query\n\t\tquery := db.Rebind(`INSERT INTO hostnames(name) VALUES(?)`)\n\t\tif db.Driver == POSTGRES {\n\t\t\terr := db.Get(&id, query+\" RETURNING id\", name)\n\t\t\treturn id, err\n\t\t}\n\n\t\t// MySQL and SQLite do support LastInsertID, so use that\n\t\tr, err := db.Exec(query, name)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\n\t\treturn r.LastInsertId()\n\t}\n\n\treturn id, err\n}\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/10_alter_stats_table_constraints.sql",
    "content": "-- +migrate Up\n\nDROP INDEX unique_daily_site_stats ON daily_site_stats; \nDROP INDEX unique_daily_page_stats ON daily_page_stats;\nDROP INDEX unique_daily_referrer_stats ON daily_referrer_stats;\n\nCREATE UNIQUE INDEX unique_daily_site_stats ON daily_site_stats(site_id, date);\nCREATE UNIQUE INDEX unique_daily_page_stats ON daily_page_stats(site_id, hostname(100), pathname(100), date);\nCREATE UNIQUE INDEX unique_daily_referrer_stats ON daily_referrer_stats(site_id, hostname(100), pathname(100), date);\n\n-- +migrate Down\n\nDROP INDEX unique_daily_site_stats ON daily_site_stats; \nDROP INDEX unique_daily_page_stats ON daily_page_stats;\nDROP INDEX unique_daily_referrer_stats ON daily_referrer_stats;\n\nCREATE UNIQUE INDEX unique_daily_site_stats ON daily_site_stats(date);\nCREATE UNIQUE INDEX unique_daily_page_stats ON daily_page_stats(hostname(100), pathname(100), date);\nCREATE UNIQUE INDEX unique_daily_referrer_stats ON daily_referrer_stats(hostname(100), pathname(100), date);"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/11_add_pageview_finished_column.sql",
    "content": "-- +migrate Up\n\nALTER TABLE pageviews ADD COLUMN is_finished TINYINT(1) NOT NULL DEFAULT 0;\n\n-- +migrate Down\n\nALTER TABLE pageviews DROP COLUMN is_finished;\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/12_create_hostnames_table.sql",
    "content": "-- +migrate Up\nCREATE TABLE hostnames(\n   id INTEGER AUTO_INCREMENT PRIMARY KEY NOT NULL,\n   name VARCHAR(255) NOT NULL\n) CHARACTER SET=utf8 ENGINE=INNODB;\n\n-- +migrate Down\nDROP TABLE IF EXISTS hostnames;\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/13_create_unique_hostname_index.sql",
    "content": "-- +migrate Up\nCREATE UNIQUE INDEX unique_hostnames_name ON hostnames(name(100));\n\n-- +migrate Down\nDROP INDEX IF EXISTS unique_hostnames_name;\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/14_create_pathnames_table.sql",
    "content": "-- +migrate Up\nCREATE TABLE pathnames(\n   id INTEGER AUTO_INCREMENT PRIMARY KEY NOT NULL,\n   name VARCHAR(255) NOT NULL\n) CHARACTER SET=utf8 ENGINE=INNODB;\n\n-- +migrate Down\nDROP TABLE IF EXISTS pathnames;\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/15_create_unique_pathname_index.sql",
    "content": "-- +migrate Up\nCREATE UNIQUE INDEX unique_pathnames_name ON pathnames(name(100));\n\n-- +migrate Down\nDROP INDEX IF EXISTS unique_pathnames_name;\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/16_fill_hostnames_table.sql",
    "content": "-- +migrate Up\nINSERT INTO hostnames(name) SELECT hostname FROM daily_page_stats UNION SELECT hostname FROM daily_referrer_stats;\n\n-- +migrate Down\n\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/17_fill_pathnames_table.sql",
    "content": "-- +migrate Up\nINSERT INTO pathnames(name) SELECT pathname FROM daily_page_stats UNION SELECT pathname FROM daily_referrer_stats;\n\n-- +migrate Down\n\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/18_alter_page_stats_table.sql",
    "content": "-- +migrate Up\nDROP TABLE IF EXISTS daily_page_stats_old;\nRENAME TABLE daily_page_stats TO daily_page_stats_old;\nCREATE TABLE daily_page_stats(\n   site_id INTEGER NOT NULL DEFAULT 1,\n   hostname_id INTEGER NOT NULL,\n   pathname_id INTEGER NOT NULL,\n   pageviews INTEGER NOT NULL,\n   visitors INTEGER NOT NULL,\n   entries INTEGER NOT NULL,\n   bounce_rate FLOAT NOT NULL,\n   known_durations INTEGER NOT NULL DEFAULT 0,\n   avg_duration FLOAT NOT NULL,\n   date DATE NOT NULL\n) CHARACTER SET=utf8;\nINSERT INTO daily_page_stats \n    SELECT site_id, h.id, p.id, pageviews, visitors, entries, bounce_rate, known_durations, avg_duration, date \n    FROM daily_page_stats_old s \n    LEFT JOIN hostnames h ON h.name = s.hostname \n    LEFT JOIN pathnames p ON p.name = s.pathname;\nDROP TABLE daily_page_stats_old;\n\n-- +migrate Down\n\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/19_alter_referrer_stats_table.sql",
    "content": "-- +migrate Up\nDROP TABLE IF EXISTS daily_referrer_stats_old;\nRENAME TABLE daily_referrer_stats TO daily_referrer_stats_old;\nCREATE TABLE daily_referrer_stats(\n    site_id INTEGER NOT NULL DEFAULT 1, \n    hostname_id INTEGER NOT NULL,\n    pathname_id INTEGER NOT NULL,\n    groupname VARCHAR(255) NULL, \n    pageviews INTEGER NOT NULL, \n    visitors INTEGER NOT NULL, \n    bounce_rate FLOAT NOT NULL, \n    known_durations INTEGER NOT NULL DEFAULT 0, \n    avg_duration FLOAT NOT NULL, \n    date DATE NOT NULL \n) CHARACTER SET=utf8;\nINSERT INTO daily_referrer_stats \n    SELECT site_id, h.id, p.id, groupname, pageviews, visitors, bounce_rate, known_durations, avg_duration, date \n    FROM daily_referrer_stats_old s \n    LEFT JOIN hostnames h ON h.name = s.hostname \n    LEFT JOIN pathnames p ON p.name = s.pathname;\nDROP TABLE daily_referrer_stats_old;\n\n-- +migrate Down\n\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/1_initial_tables.sql",
    "content": "-- +migrate Up\n\nCREATE TABLE users (\n  id INT AUTO_INCREMENT PRIMARY KEY NOT NULL,\n  email VARCHAR(100) NOT NULL,\n  password VARCHAR(255) NOT NULL\n) CHARACTER SET=utf8 ENGINE=INNODB;\n\nCREATE TABLE pageviews(\n   id INT AUTO_INCREMENT PRIMARY KEY NOT NULL,\n   hostname VARCHAR(255) NOT NULL,\n   pathname VARCHAR(255) NOT NULL,\n   session_id VARCHAR(16) NOT NULL,\n   is_new_visitor TINYINT(1) NOT NULL,\n   is_new_session TINYINT(1) NOT NULL,\n   is_unique TINYINT(1) NOT NULL,\n   is_bounce TINYINT(1) NULL,\n   referrer VARCHAR(255) NULL,\n   duration INT(4) NULL,\n   timestamp DATETIME NOT NULL\n) CHARACTER SET=utf8 ENGINE=INNODB;\n\nCREATE TABLE daily_page_stats(\n   hostname VARCHAR(255) NOT NULL,\n   pathname VARCHAR(255) NOT NULL,\n   pageviews INT NOT NULL,\n   visitors INT NOT NULL,\n   entries INT NOT NULL,\n   bounce_rate FLOAT NOT NULL,\n   avg_duration FLOAT NOT NULL,\n   date DATE NOT NULL\n) CHARACTER SET=utf8 ENGINE=INNODB;\n\nCREATE TABLE daily_site_stats(\n   pageviews INT NOT NULL,\n   visitors INT NOT NULL,\n   sessions INT NOT NULL,\n   bounce_rate FLOAT NOT NULL,\n   avg_duration FLOAT NOT NULL,\n   date DATE NOT NULL\n) CHARACTER SET=utf8 ENGINE=INNODB;\n\nCREATE TABLE daily_referrer_stats(\n   url VARCHAR(255) NOT NULL,\n   pageviews INT NOT NULL,\n   visitors INT NOT NULL,\n   bounce_rate FLOAT NOT NULL,\n   avg_duration FLOAT NOT NULL,\n   date DATE NOT NULL\n) CHARACTER SET=utf8 ENGINE=INNODB;\n\nCREATE UNIQUE INDEX unique_user_email ON users(email);\nCREATE UNIQUE INDEX unique_daily_site_stats ON daily_site_stats(date);\nCREATE UNIQUE INDEX unique_daily_page_stats ON daily_page_stats(hostname(100), pathname(100), date);\nCREATE UNIQUE INDEX unique_daily_referrer_stats ON daily_referrer_stats(url(100), date);\n\n\n-- +migrate Down\n\nDROP TABLE IF EXISTS users;\nDROP TABLE IF EXISTS pageviews;\nDROP TABLE IF EXISTS daily_page_stats;\nDROP TABLE IF EXISTS daily_site_stats;\nDROP TABLE IF EXISTS daily_referrer_stats;\n\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/20_recreate_stats_indices.sql",
    "content": "-- +migrate Up\nCREATE UNIQUE INDEX unique_daily_page_stats ON daily_page_stats(site_id, hostname_id, pathname_id, date);\nCREATE UNIQUE INDEX unique_daily_referrer_stats ON daily_referrer_stats(site_id, hostname_id, pathname_id, date);\n\n-- +migrate Down\nDROP INDEX unique_daily_page_stats ON daily_page_stats;\nDROP INDEX unique_daily_referrer_stats ON daily_referrer_stats;"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/21_alter_page_stats_table.sql",
    "content": "-- +migrate Up\nCREATE TABLE page_stats(\n   site_id INTEGER NOT NULL DEFAULT 1,\n   hostname_id INTEGER NOT NULL,\n   pathname_id INTEGER NOT NULL,\n   pageviews INTEGER NOT NULL,\n   visitors INTEGER NOT NULL,\n   entries INTEGER NOT NULL,\n   bounce_rate FLOAT NOT NULL,\n   known_durations INTEGER NOT NULL DEFAULT 0,\n   avg_duration FLOAT NOT NULL,\n   ts DATETIME NOT NULL\n) CHARACTER SET=utf8;\nINSERT INTO page_stats \n    SELECT site_id, hostname_id, pathname_id, pageviews, visitors, entries, bounce_rate, known_durations, avg_duration, CONCAT(date, ' 00:00:00')\n    FROM daily_page_stats s ;\nDROP TABLE daily_page_stats;\n\n-- +migrate Down"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/22_alter_site_stats_table.sql",
    "content": "-- +migrate Up\nCREATE TABLE site_stats(\n   site_id INTEGER NOT NULL DEFAULT 1,\n   pageviews INTEGER NOT NULL,\n   visitors INTEGER NOT NULL,\n   sessions INTEGER NOT NULL,\n   bounce_rate FLOAT NOT NULL,\n   known_durations INTEGER NOT NULL DEFAULT 0,\n   avg_duration FLOAT NOT NULL,\n   ts DATETIME NOT NULL\n) CHARACTER SET=utf8;\nINSERT INTO site_stats \n    SELECT site_id, pageviews, visitors, sessions, bounce_rate, known_durations, avg_duration,  CONCAT(date, ' 00:00:00')\n    FROM daily_site_stats s ;\nDROP TABLE daily_site_stats;\n\n-- +migrate Down"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/23_alter_referrer_stats_table.sql",
    "content": "-- +migrate Up\nCREATE TABLE referrer_stats(\n    site_id INTEGER NOT NULL DEFAULT 1, \n    hostname_id INTEGER NOT NULL,\n    pathname_id INTEGER NOT NULL,\n    groupname VARCHAR(255) NULL, \n    pageviews INTEGER NOT NULL, \n    visitors INTEGER NOT NULL, \n    bounce_rate FLOAT NOT NULL, \n    known_durations INTEGER NOT NULL DEFAULT 0, \n    avg_duration FLOAT NOT NULL, \n    ts DATETIME NOT NULL \n) CHARACTER SET=utf8 ENGINE=INNODB;\nINSERT INTO referrer_stats \n    SELECT site_id, hostname_id, pathname_id, groupname, pageviews, visitors, bounce_rate, known_durations, avg_duration, CONCAT(date, ' 00:00:00')\n    FROM daily_referrer_stats s;\nDROP TABLE daily_referrer_stats;\n\n-- +migrate Down\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/24_recreate_stat_table_indices.sql",
    "content": "-- +migrate Up\nCREATE UNIQUE INDEX unique_page_stats ON page_stats(site_id, hostname_id, pathname_id, ts);\nCREATE UNIQUE INDEX unique_referrer_stats ON referrer_stats(site_id, hostname_id, pathname_id, ts);\nCREATE UNIQUE INDEX unique_site_stats ON site_stats(site_id, ts);\n\n-- +migrate Down\nDROP INDEX unique_page_stats ON page_stats;\nDROP INDEX unique_referrer_stats ON referrer_stats;\nDROP INDEX unique_site_stats ON site_stats;\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/2_known_durations_column.sql",
    "content": "-- +migrate Up\n\nALTER TABLE daily_site_stats ADD COLUMN known_durations INTEGER NOT NULL DEFAULT 0;\nALTER TABLE daily_page_stats ADD COLUMN known_durations INTEGER NOT NULL DEFAULT 0;\nALTER TABLE daily_referrer_stats ADD COLUMN known_durations INTEGER NOT NULL DEFAULT 0;\n\n-- +migrate Down\n\nALTER TABLE daily_site_stats DROP COLUMN known_durations;\nALTER TABLE daily_page_stats DROP COLUMN known_durations;\nALTER TABLE daily_referrer_stats DROP COLUMN known_durations;\n\n\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/3_referrer_group_column.sql",
    "content": "-- +migrate Up\n\nDROP INDEX unique_daily_referrer_stats ON daily_referrer_stats;\n\nALTER TABLE daily_referrer_stats ADD COLUMN groupname VARCHAR(255);\nALTER TABLE daily_referrer_stats ADD COLUMN hostname VARCHAR(255);\nALTER TABLE daily_referrer_stats ADD COLUMN pathname VARCHAR(255);\n\n\nUPDATE daily_referrer_stats SET hostname = SUBSTRING_INDEX( url, \"/\", 3) WHERE url != \"\" AND ( hostname = \"\" OR hostname IS NULL);\nUPDATE daily_referrer_stats SET pathname = REPLACE(url, hostname, \"\") WHERE url != \"\" AND (pathname = '' OR pathname IS NULL);\n\nCREATE UNIQUE INDEX unique_daily_referrer_stats ON daily_referrer_stats(hostname(100), pathname(100), date);\nALTER TABLE daily_referrer_stats DROP COLUMN url;\n\n-- +migrate Down\n\nALTER TABLE daily_referrer_stats DROP COLUMN groupname;\nALTER TABLE daily_referrer_stats DROP COLUMN hostname;\nALTER TABLE daily_referrer_stats DROP COLUMN pathname;\n\nALTER TABLE daily_referrer_stats ADD COLUMN url VARCHAR(255) NOT NULL;\n\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/4_pageview_id_column.sql",
    "content": "-- +migrate Up\n\nALTER TABLE pageviews DROP COLUMN session_id;\nALTER TABLE pageviews DROP COLUMN id;\nALTER TABLE pageviews ADD COLUMN id VARCHAR(31) NOT NULL FIRST;\n\n-- +migrate Down\n\nALTER TABLE pageviews DROP COLUMN id;\nALTER TABLE pageviews ADD COLUMN id INT AUTO_INCREMENT PRIMARY KEY NOT NULL FIRST;\nALTER TABLE pageviews ADD COLUMN session_id VARCHAR(16) NOT NULL AFTER id;\n\n\n\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/5_create_sites_table.sql",
    "content": "-- +migrate Up\nCREATE TABLE sites (\n    id INTEGER AUTO_INCREMENT PRIMARY KEY NOT NULL,\n    tracking_id VARCHAR(8) UNIQUE,\n    name VARCHAR(100) NOT NULL\n) CHARACTER SET=utf8 ENGINE=INNODB;\n\n-- +migrate Down\nDROP TABLE IF EXISTS sites;"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/6_add_site_tracking_id_column_to_pageviews_table.sql",
    "content": "-- +migrate Up\n\nTRUNCATE pageviews;\nALTER TABLE pageviews ADD COLUMN site_tracking_id VARCHAR(8) NOT NULL;\n\n-- +migrate Down\n\nALTER TABLE pageviews DROP COLUMN site_tracking_id;\n\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/7_add_site_id_to_site_stats_table.sql",
    "content": "-- +migrate Up\n\nALTER TABLE daily_site_stats ADD COLUMN site_id INTEGER NOT NULL DEFAULT 1;\n\n-- +migrate Down\n\nALTER TABLE daily_site_stats DROP COLUMN site_id;\n\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/8_add_site_id_to_page_stats_table.sql",
    "content": "-- +migrate Up\n\nALTER TABLE daily_page_stats ADD COLUMN site_id INTEGER NOT NULL DEFAULT 1;\n\n-- +migrate Down\n\nALTER TABLE daily_page_stats DROP COLUMN site_id;\n\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/mysql/9_add_site_id_to_referrer_stats_table.sql",
    "content": "-- +migrate Up\n\nALTER TABLE daily_referrer_stats ADD COLUMN site_id INTEGER NOT NULL DEFAULT 1;\n\n-- +migrate Down\n\nALTER TABLE daily_referrer_stats DROP COLUMN site_id;\n\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/10_alter_numeric_column_precision.sql",
    "content": "-- +migrate Up\n\nALTER TABLE daily_site_stats ALTER COLUMN bounce_rate TYPE NUMERIC;\nALTER TABLE daily_page_stats ALTER COLUMN bounce_rate TYPE NUMERIC;\nALTER TABLE daily_referrer_stats ALTER COLUMN bounce_rate TYPE NUMERIC;\nALTER TABLE daily_site_stats ALTER COLUMN avg_duration TYPE NUMERIC;\nALTER TABLE daily_page_stats ALTER COLUMN avg_duration TYPE NUMERIC;\nALTER TABLE daily_referrer_stats ALTER COLUMN avg_duration TYPE NUMERIC;\n\n-- +migrate Down\n\n\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/11_alter_stats_table_constraints.sql",
    "content": "-- +migrate Up\n\nDROP INDEX IF EXISTS unique_daily_site_stats;\nDROP INDEX IF EXISTS unique_daily_page_stats;\nDROP INDEX IF EXISTS unique_daily_referrer_stats;\n\nCREATE UNIQUE INDEX unique_daily_site_stats ON daily_site_stats(site_id, date);\nCREATE UNIQUE INDEX unique_daily_page_stats ON daily_page_stats(site_id, hostname, pathname, date);\nCREATE UNIQUE INDEX unique_daily_referrer_stats ON daily_referrer_stats(site_id, hostname, pathname, date);\n\n-- +migrate Down\n\nDROP INDEX IF EXISTS unique_daily_site_stats;\nDROP INDEX IF EXISTS unique_daily_page_stats;\nDROP INDEX IF EXISTS unique_daily_referrer_stats;\n\nCREATE UNIQUE INDEX unique_daily_site_stats ON daily_site_stats(date);\nCREATE UNIQUE INDEX unique_daily_page_stats ON daily_page_stats(hostname, pathname, date);\nCREATE UNIQUE INDEX unique_daily_referrer_stats ON daily_referrer_stats(hostname, pathname, date);"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/12_add_pageview_finished_column.sql",
    "content": "-- +migrate Up\n\nALTER TABLE pageviews ADD COLUMN is_finished BOOLEAN NOT NULL DEFAULT FALSE;\n\n-- +migrate Down\n\nALTER TABLE pageviews DROP COLUMN is_finished;\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/13_create_hostnames_table.sql",
    "content": "-- +migrate Up\nCREATE TABLE hostnames(\n   id SERIAL PRIMARY KEY NOT NULL,\n   name VARCHAR(255) NOT NULL\n);\n\n-- +migrate Down\nDROP TABLE IF EXISTS hostnames;\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/14_create_unique_hostname_index.sql",
    "content": "-- +migrate Up\nCREATE UNIQUE INDEX unique_hostnames_name ON hostnames(name);\n\n-- +migrate Down\nDROP INDEX IF EXISTS unique_hostnames_name;\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/15_create_pathnames_table.sql",
    "content": "-- +migrate Up\nCREATE TABLE pathnames(\n   id SERIAL PRIMARY KEY NOT NULL,\n   name VARCHAR(255) NOT NULL\n);\n\n-- +migrate Down\nDROP TABLE IF EXISTS pathnames;\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/16_create_unique_pathname_index.sql",
    "content": "-- +migrate Up\nCREATE UNIQUE INDEX unique_pathnames_name ON pathnames(name);\n\n-- +migrate Down\nDROP INDEX IF EXISTS unique_pathnames_name;\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/17_fill_hostnames_table.sql",
    "content": "-- +migrate Up\nINSERT INTO hostnames(name) SELECT hostname FROM daily_page_stats UNION SELECT hostname FROM daily_referrer_stats;\n\n-- +migrate Down\n\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/18_fill_pathnames_table.sql",
    "content": "-- +migrate Up\nINSERT INTO pathnames(name) SELECT pathname FROM daily_page_stats UNION SELECT pathname FROM daily_referrer_stats;\n\n-- +migrate Down\n\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/19_alter_page_stats_table.sql",
    "content": "-- +migrate Up\nDROP TABLE IF EXISTS daily_page_stats_old;\nALTER TABLE daily_page_stats RENAME TO daily_page_stats_old;\nCREATE TABLE daily_page_stats(\n   site_id INTEGER NOT NULL DEFAULT 1,\n   hostname_id INTEGER NOT NULL,\n   pathname_id INTEGER NOT NULL,\n   pageviews INTEGER NOT NULL,\n   visitors INTEGER NOT NULL,\n   entries INTEGER NOT NULL,\n   bounce_rate FLOAT NOT NULL,\n   known_durations INTEGER NOT NULL DEFAULT 0,\n   avg_duration FLOAT NOT NULL,\n   date DATE NOT NULL\n);\nINSERT INTO daily_page_stats \n    SELECT site_id, h.id, p.id, pageviews, visitors, entries, bounce_rate, known_durations, avg_duration, date \n    FROM daily_page_stats_old s \n    LEFT JOIN hostnames h ON h.name = s.hostname \n    LEFT JOIN pathnames p ON p.name = s.pathname;\nDROP TABLE daily_page_stats_old;\n\n-- +migrate Down\n\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/1_initial_tables.sql",
    "content": "-- +migrate Up\n\nCREATE TABLE users(\n  id SERIAL PRIMARY KEY NOT NULL,\n  email VARCHAR(255) NOT NULL,\n  password VARCHAR(255) NOT NULL\n);\n\nCREATE TABLE pageviews(\n   id SERIAL PRIMARY KEY NOT NULL,\n   hostname VARCHAR(255) NOT NULL,\n   pathname VARCHAR(255) NOT NULL,\n   session_id VARCHAR(16) NOT NULL,\n   is_new_visitor BOOLEAN NOT NULL,\n   is_new_session BOOLEAN NOT NULL,\n   is_unique BOOLEAN NOT NULL,\n   is_bounce BOOLEAN NULL,\n   referrer VARCHAR(255) NULL,\n   duration INTEGER NULL,\n   timestamp TIMESTAMP WITH TIME ZONE NOT NULL\n);\n\nCREATE TABLE daily_page_stats(\n   hostname VARCHAR(255) NOT NULL,\n   pathname VARCHAR(255) NOT NULL,\n   pageviews INTEGER NOT NULL,\n   visitors INTEGER NOT NULL,\n   entries INTEGER NOT NULL,\n   bounce_rate NUMERIC(4) NOT NULL,\n   avg_duration NUMERIC(4) NOT NULL,\n   date DATE NOT NULL\n);\n\nCREATE TABLE daily_site_stats(\n   pageviews INTEGER NOT NULL,\n   visitors INTEGER NOT NULL,\n   sessions INTEGER NOT NULL,\n   bounce_rate NUMERIC(4) NOT NULL,\n   avg_duration NUMERIC(4) NOT NULL,\n   date DATE NOT NULL\n);\n\nCREATE TABLE daily_referrer_stats(\n   url VARCHAR(255) NOT NULL,\n   pageviews INTEGER NOT NULL,\n   visitors INTEGER NOT NULL,\n   bounce_rate NUMERIC(4) NOT NULL,\n   avg_duration NUMERIC(4) NOT NULL,\n   date DATE NOT NULL\n);\n\nCREATE UNIQUE INDEX unique_user_email ON users(email);\nCREATE UNIQUE INDEX unique_daily_site_stats ON daily_site_stats(date);\nCREATE UNIQUE INDEX unique_daily_page_stats ON daily_page_stats(hostname, pathname, date);\nCREATE UNIQUE INDEX unique_daily_referrer_stats ON daily_referrer_stats(url, date);\n\n-- +migrate Down\n\nDROP TABLE IF EXISTS users;\nDROP TABLE IF EXISTS pageviews;\nDROP TABLE IF EXISTS daily_page_stats;\nDROP TABLE IF EXISTS daily_site_stats;\nDROP TABLE IF EXISTS daily_referrer_stats;\n\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/20_alter_referrer_stats_table.sql",
    "content": "-- +migrate Up\nDROP TABLE IF EXISTS daily_referrer_stats_old;\nALTER TABLE daily_referrer_stats RENAME TO daily_referrer_stats_old;\nCREATE TABLE daily_referrer_stats(\n    site_id INTEGER NOT NULL DEFAULT 1, \n    hostname_id INTEGER NOT NULL,\n    pathname_id INTEGER NOT NULL,\n    groupname VARCHAR(255) NULL, \n    pageviews INTEGER NOT NULL, \n    visitors INTEGER NOT NULL, \n    bounce_rate FLOAT NOT NULL, \n    known_durations INTEGER NOT NULL DEFAULT 0, \n    avg_duration FLOAT NOT NULL, \n    date DATE NOT NULL \n);\nINSERT INTO daily_referrer_stats \n    SELECT site_id, h.id, p.id, groupname, pageviews, visitors, bounce_rate, known_durations, avg_duration, date \n    FROM daily_referrer_stats_old s \n    LEFT JOIN hostnames h ON h.name = s.hostname \n    LEFT JOIN pathnames p ON p.name = s.pathname;\nDROP TABLE daily_referrer_stats_old;\n\n-- +migrate Down\n\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/21_recreate_stats_indices.sql",
    "content": "-- +migrate Up\nCREATE UNIQUE INDEX unique_daily_page_stats ON daily_page_stats(site_id, hostname_id, pathname_id, date);\nCREATE UNIQUE INDEX unique_daily_referrer_stats ON daily_referrer_stats(site_id, hostname_id, pathname_id, date);\n\n-- +migrate Down\nDROP INDEX unique_daily_page_stats;\nDROP INDEX unique_daily_referrer_stats;"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/22_alter_page_stats_table.sql",
    "content": "-- +migrate Up\nCREATE TABLE page_stats(\n   site_id INTEGER NOT NULL DEFAULT 1,\n   hostname_id INTEGER NOT NULL,\n   pathname_id INTEGER NOT NULL,\n   pageviews INTEGER NOT NULL,\n   visitors INTEGER NOT NULL,\n   entries INTEGER NOT NULL,\n   bounce_rate FLOAT NOT NULL,\n   known_durations INTEGER NOT NULL DEFAULT 0,\n   avg_duration FLOAT NOT NULL,\n   ts TIMESTAMP WITHOUT TIME ZONE NOT NULL\n);\nINSERT INTO page_stats \n    SELECT site_id, hostname_id, pathname_id, pageviews, visitors, entries, bounce_rate, known_durations, avg_duration, (date || ' 00:00:00')::timestamp\n    FROM daily_page_stats s;\nDROP TABLE daily_page_stats;\n\n-- +migrate Down\n\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/23_alter_referrer_stats_table.sql",
    "content": "-- +migrate Up\nCREATE TABLE referrer_stats(\n    site_id INTEGER NOT NULL DEFAULT 1, \n    hostname_id INTEGER NOT NULL,\n    pathname_id INTEGER NOT NULL,\n    groupname VARCHAR(255) NULL, \n    pageviews INTEGER NOT NULL, \n    visitors INTEGER NOT NULL, \n    bounce_rate FLOAT NOT NULL, \n    known_durations INTEGER NOT NULL DEFAULT 0, \n    avg_duration FLOAT NOT NULL, \n    ts TIMESTAMP WITHOUT TIME ZONE NOT NULL \n);\nINSERT INTO referrer_stats \n    SELECT site_id, hostname_id, pathname_id, groupname, pageviews, visitors, bounce_rate, known_durations, avg_duration, (date || ' 00:00:00')::timestamp\n    FROM daily_referrer_stats s;\nDROP TABLE daily_referrer_stats;\n\n-- +migrate Down\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/24_alter_site_stats_table.sql",
    "content": "-- +migrate Up\nCREATE TABLE site_stats(\n   site_id INTEGER NOT NULL DEFAULT 1,\n   pageviews INTEGER NOT NULL,\n   visitors INTEGER NOT NULL,\n   sessions INTEGER NOT NULL,\n   bounce_rate FLOAT NOT NULL,\n   known_durations INTEGER NOT NULL DEFAULT 0,\n   avg_duration FLOAT NOT NULL,\n   ts TIMESTAMP WITHOUT TIME ZONE NOT NULL\n);\nINSERT INTO site_stats \n    SELECT site_id, pageviews, visitors, sessions, bounce_rate, known_durations, avg_duration, (date || ' 00:00:00')::timestamp\n    FROM daily_site_stats s;\nDROP TABLE daily_site_stats;\n\n-- +migrate Down"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/25_recreate_stat_table_indices.sql",
    "content": "-- +migrate Up\nDROP INDEX IF EXISTS unique_daily_page_stats;\nDROP INDEX IF EXISTS unique_daily_referrer_stats;\nCREATE UNIQUE INDEX unique_page_stats ON page_stats(site_id, hostname_id, pathname_id, ts);\nCREATE UNIQUE INDEX unique_referrer_stats ON referrer_stats(site_id, hostname_id, pathname_id, ts);\nCREATE UNIQUE INDEX unique_site_stats ON site_stats(site_id, ts);\n\n-- +migrate Down\nDROP INDEX IF EXISTS unique_page_stats;\nDROP INDEX IF EXISTS unique_referrer_stats;\nDROP INDEX IF EXISTS unique_site_stats;"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/26_alter_pageviews_table.sql",
    "content": "-- +migrate Up\n\nALTER TABLE pageviews ALTER COLUMN timestamp TYPE TIMESTAMP WITHOUT TIME ZONE;\n\n-- +migrate Down\n\nALTER TABLE pageviews ALTER COLUMN timestamp TYPE TIMESTAMP WITH TIME ZONE;"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/2_known_durations_column.sql",
    "content": "-- +migrate Up\n\nALTER TABLE daily_site_stats ADD COLUMN known_durations INTEGER NOT NULL DEFAULT 0;\nALTER TABLE daily_page_stats ADD COLUMN known_durations INTEGER NOT NULL DEFAULT 0;\nALTER TABLE daily_referrer_stats ADD COLUMN known_durations INTEGER NOT NULL DEFAULT 0;\n\n-- +migrate Down\n\nALTER TABLE daily_site_stats DROP COLUMN known_durations;\nALTER TABLE daily_page_stats DROP COLUMN known_durations;\nALTER TABLE daily_referrer_stats DROP COLUMN known_durations;\n\n\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/3_referrer_group_column.sql",
    "content": "-- +migrate Up\n\nALTER TABLE daily_referrer_stats ADD COLUMN groupname VARCHAR(255);\nALTER TABLE daily_referrer_stats ADD COLUMN hostname VARCHAR(255);\nALTER TABLE daily_referrer_stats ADD COLUMN pathname VARCHAR(255);\n\nUPDATE daily_referrer_stats SET hostname = CONCAT( SPLIT_PART(url, '://', 1), '://', SPLIT_PART(SPLIT_PART(url, '://', 2), '/', 1) ) WHERE url != '' AND ( hostname = '' OR hostname IS NULL);\nUPDATE daily_referrer_stats SET pathname = SPLIT_PART( url, hostname, 2 ) WHERE url != '' AND (pathname = '' OR pathname IS NULL);\n\nDROP INDEX IF EXISTS unique_daily_referrer_stats;\nALTER TABLE daily_referrer_stats DROP COLUMN url;\nCREATE UNIQUE INDEX unique_daily_referrer_stats ON daily_referrer_stats(hostname, pathname, date);\n\n-- +migrate Down\n\nALTER TABLE daily_referrer_stats DROP COLUMN groupname;\nALTER TABLE daily_referrer_stats DROP COLUMN hostname;\nALTER TABLE daily_referrer_stats DROP COLUMN pathname;\n\nALTER TABLE daily_referrer_stats ADD COLUMN url VARCHAR(255) NOT NULL;\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/4_pageview_id_column.sql",
    "content": "-- +migrate Up\n\nTRUNCATE pageviews; -- postgres will fail because of NULL values otherwise\nALTER TABLE pageviews DROP COLUMN session_id;\nALTER TABLE pageviews DROP COLUMN id;\nALTER TABLE pageviews ADD COLUMN id VARCHAR(31) NOT NULL;\n\n-- +migrate Down\n\nALTER TABLE pageviews DROP COLUMN id;\nALTER TABLE pageviews ADD COLUMN id INT AUTO_INCREMENT PRIMARY KEY NOT NULL;\nALTER TABLE pageviews ADD COLUMN session_id VARCHAR(16) NOT NULL;\n\n\n\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/5_create_sites_table.sql",
    "content": "-- +migrate Up\nCREATE TABLE sites (\n    id SERIAL PRIMARY KEY NOT NULL,\n    tracking_id VARCHAR(8) UNIQUE,\n    name VARCHAR(100) NOT NULL\n);\n\n-- +migrate Down\nDROP TABLE IF EXISTS sites;"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/6_add_site_tracking_id_column_to_pageviews_table.sql",
    "content": "-- +migrate Up\n\nTRUNCATE pageviews; -- postgres will fail because of NULL values otherwise\nALTER TABLE pageviews ADD COLUMN site_tracking_id VARCHAR(8) NOT NULL;\n\n-- +migrate Down\n\nALTER TABLE pageviews DROP COLUMN site_tracking_id;\n\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/7_add_site_id_to_site_stats_table.sql",
    "content": "-- +migrate Up\n\nALTER TABLE daily_site_stats ADD COLUMN site_id INTEGER NOT NULL DEFAULT 1;\n\n-- +migrate Down\n\nALTER TABLE daily_site_stats DROP COLUMN site_id;\n\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/8_add_site_id_to_page_stats_table.sql",
    "content": "-- +migrate Up\n\nALTER TABLE daily_page_stats ADD COLUMN site_id INTEGER NOT NULL DEFAULT 1;\n\n-- +migrate Down\n\nALTER TABLE daily_page_stats DROP COLUMN site_id;\n\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/postgres/9_add_site_id_to_referrer_stats_table.sql",
    "content": "-- +migrate Up\n\nALTER TABLE daily_referrer_stats ADD COLUMN site_id INTEGER NOT NULL DEFAULT 1;\n\n-- +migrate Down\n\nALTER TABLE daily_referrer_stats DROP COLUMN site_id;\n\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/10_alter_stats_table_constraints.sql",
    "content": "-- +migrate Up\n\nDROP INDEX IF EXISTS unique_daily_site_stats;\nDROP INDEX IF EXISTS unique_daily_page_stats;\nDROP INDEX IF EXISTS unique_daily_referrer_stats;\n\nCREATE UNIQUE INDEX unique_daily_site_stats ON daily_site_stats(site_id, date);\nCREATE UNIQUE INDEX unique_daily_page_stats ON daily_page_stats(site_id, hostname, pathname, date);\nCREATE UNIQUE INDEX unique_daily_referrer_stats ON daily_referrer_stats(site_id, hostname, pathname, date);\n\n-- +migrate Down\n\nDROP INDEX IF EXISTS unique_daily_site_stats;\nDROP INDEX IF EXISTS unique_daily_page_stats;\nDROP INDEX IF EXISTS unique_daily_referrer_stats;\n\nCREATE UNIQUE INDEX unique_daily_site_stats ON daily_site_stats(date);\nCREATE UNIQUE INDEX unique_daily_page_stats ON daily_page_stats(hostname, pathname, date);\nCREATE UNIQUE INDEX unique_daily_referrer_stats ON daily_referrer_stats(hostname, pathname, date);"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/11_add_pageview_finished_column.sql",
    "content": "-- +migrate Up\n\nDROP TABLE IF EXISTS pageviews;\nCREATE TABLE pageviews(\n   id VARCHAR(31) NOT NULL,\n   site_tracking_id VARCHAR(8) NOT NULL,\n   hostname VARCHAR(255) NOT NULL,\n   pathname VARCHAR(255) NOT NULL,\n   is_new_visitor TINYINT(1) NOT NULL,\n   is_new_session TINYINT(1) NOT NULL,\n   is_unique TINYINT(1) NOT NULL,\n   is_bounce TINYINT(1) NULL,\n   is_finished TINYINT(1) NOT NULL DEFAULT 0,\n   referrer VARCHAR(255) NULL,\n   duration INTEGER(4) NULL,\n   timestamp DATETIME NOT NULL\n);\n\n-- +migrate Down\n\nDROP TABLE IF EXISTS pageviews;\nCREATE TABLE pageviews(\n   id VARCHAR(31) NOT NULL,\n   site_tracking_id VARCHAR(8) NOT NULL,\n   hostname VARCHAR(255) NOT NULL,\n   pathname VARCHAR(255) NOT NULL,\n   is_new_visitor TINYINT(1) NOT NULL,\n   is_new_session TINYINT(1) NOT NULL,\n   is_unique TINYINT(1) NOT NULL,\n   is_bounce TINYINT(1) NULL,\n   referrer VARCHAR(255) NULL,\n   duration INTEGER(4) NULL,\n   timestamp DATETIME NOT NULL\n);"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/12_create_hostnames_table.sql",
    "content": "-- +migrate Up\nCREATE TABLE hostnames(\n   id INTEGER PRIMARY KEY,\n   name VARCHAR(255) NOT NULL\n);\n\n-- +migrate Down\nDROP TABLE IF EXISTS hostnames;\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/13_create_unique_hostname_index.sql",
    "content": "-- +migrate Up\nCREATE UNIQUE INDEX unique_hostnames_name ON hostnames(name);\n\n-- +migrate Down\nDROP INDEX IF EXISTS unique_hostnames_name;\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/14_create_pathnames_table.sql",
    "content": "-- +migrate Up\nCREATE TABLE pathnames(\n   id INTEGER PRIMARY KEY,\n   name VARCHAR(255) NOT NULL\n);\n\n-- +migrate Down\nDROP TABLE IF EXISTS pathnames;\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/15_create_unique_pathname_index.sql",
    "content": "-- +migrate Up\nCREATE UNIQUE INDEX unique_pathnames_name ON pathnames(name);\n\n-- +migrate Down\nDROP INDEX IF EXISTS unique_pathnames_name;\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/15_vacuum.sql",
    "content": "-- +migrate Up  notransaction\nVACUUM;\n\n-- +migrate Down "
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/16_fill_hostnames_table.sql",
    "content": "-- +migrate Up \nINSERT INTO hostnames(name) SELECT hostname FROM daily_page_stats UNION SELECT hostname FROM daily_referrer_stats;\n\n-- +migrate Down\n\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/17_fill_pathnames_table.sql",
    "content": "-- +migrate Up\nINSERT INTO pathnames(name) SELECT pathname FROM daily_page_stats UNION SELECT pathname FROM daily_referrer_stats;\n\n-- +migrate Down\n\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/18_alter_page_stats_table.sql",
    "content": "-- +migrate Up\nDROP TABLE IF EXISTS daily_page_stats_old;\nALTER TABLE daily_page_stats RENAME TO daily_page_stats_old;\nCREATE TABLE daily_page_stats(\n   site_id INTEGER NOT NULL DEFAULT 1,\n   hostname_id INTEGER NOT NULL,\n   pathname_id INTEGER NOT NULL,\n   pageviews INTEGER NOT NULL,\n   visitors INTEGER NOT NULL,\n   entries INTEGER NOT NULL,\n   bounce_rate FLOAT NOT NULL,\n   known_durations INTEGER NOT NULL DEFAULT 0,\n   avg_duration FLOAT NOT NULL,\n   date DATE NOT NULL\n);\nINSERT INTO daily_page_stats \n    SELECT site_id, h.id, p.id, pageviews, visitors, entries, bounce_rate, known_durations, avg_duration, date \n    FROM daily_page_stats_old s \n    LEFT JOIN hostnames h ON h.name = s.hostname \n    LEFT JOIN pathnames p ON p.name = s.pathname;\nDROP TABLE daily_page_stats_old;\n\n-- +migrate Down\n\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/19_alter_referrer_stats_table.sql",
    "content": "-- +migrate Up\nDROP TABLE IF EXISTS daily_referrer_stats_old;\nALTER TABLE daily_referrer_stats RENAME TO daily_referrer_stats_old;\nCREATE TABLE daily_referrer_stats(\n    site_id INTEGER NOT NULL DEFAULT 1, \n    hostname_id INTEGER NOT NULL,\n    pathname_id INTEGER NOT NULL,\n    groupname VARCHAR(255) NULL, \n    pageviews INTEGER NOT NULL, \n    visitors INTEGER NOT NULL, \n    bounce_rate FLOAT NOT NULL, \n    known_durations INTEGER NOT NULL DEFAULT 0, \n    avg_duration FLOAT NOT NULL, \n    date DATE NOT NULL \n);\nINSERT INTO daily_referrer_stats \n    SELECT site_id, h.id, p.id, groupname, pageviews, visitors, bounce_rate, known_durations, avg_duration, date \n    FROM daily_referrer_stats_old s \n    LEFT JOIN hostnames h ON h.name = s.hostname \n    LEFT JOIN pathnames p ON p.name = s.pathname;\nDROP TABLE daily_referrer_stats_old;\n\n-- +migrate Down\n\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/1_initial_tables.sql",
    "content": "-- +migrate Up\n\nCREATE TABLE users (\n  id INTEGER PRIMARY KEY,\n  email VARCHAR(255) NOT NULL,\n  password VARCHAR(255) NOT NULL\n);\n\nCREATE TABLE pageviews(\n   id INTEGER PRIMARY KEY,\n   hostname VARCHAR(255) NOT NULL,\n   pathname VARCHAR(255) NOT NULL,\n   session_id VARCHAR(16) NOT NULL,\n   is_new_visitor TINYINT(1) NOT NULL,\n   is_new_session TINYINT(1) NOT NULL,\n   is_unique TINYINT(1) NOT NULL,\n   is_bounce TINYINT(1) NULL,\n   referrer VARCHAR(255) NULL,\n   duration INTEGER(4) NULL,\n   timestamp DATETIME NOT NULL\n);\n\nCREATE TABLE daily_page_stats(\n   hostname VARCHAR(255) NOT NULL,\n   pathname VARCHAR(255) NOT NULL,\n   pageviews INTEGER NOT NULL,\n   visitors INTEGER NOT NULL,\n   entries INTEGER NOT NULL,\n   bounce_rate FLOAT NOT NULL,\n   avg_duration FLOAT NOT NULL,\n   date DATE NOT NULL\n);\n\nCREATE TABLE daily_site_stats(\n   pageviews INTEGER NOT NULL,\n   visitors INTEGER NOT NULL,\n   sessions INTEGER NOT NULL,\n   bounce_rate FLOAT NOT NULL,\n   avg_duration FLOAT NOT NULL,\n   date DATE NOT NULL\n);\n\nCREATE TABLE daily_referrer_stats(\n   url VARCHAR(255) NOT NULL,\n   pageviews INTEGER NOT NULL,\n   visitors INTEGER NOT NULL,\n   bounce_rate FLOAT NOT NULL,\n   avg_duration FLOAT NOT NULL,\n   date DATE NOT NULL\n);\n\nCREATE UNIQUE INDEX unique_user_email ON users(email);\nCREATE UNIQUE INDEX unique_daily_site_stats ON daily_site_stats(date);\nCREATE UNIQUE INDEX unique_daily_page_stats ON daily_page_stats(hostname, pathname, date);\nCREATE UNIQUE INDEX unique_daily_referrer_stats ON daily_referrer_stats(url, date);\n\n-- +migrate Down\n\nDROP TABLE IF EXISTS users;\nDROP TABLE IF EXISTS pageviews;\nDROP TABLE IF EXISTS daily_page_stats;\nDROP TABLE IF EXISTS daily_site_stats;\nDROP TABLE IF EXISTS daily_referrer_stats;\n\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/20_recreate_stats_indices.sql",
    "content": "-- +migrate Up\nDROP INDEX IF EXISTS unique_daily_page_stats;\nDROP INDEX IF EXISTS unique_daily_referrer_stats;\nCREATE UNIQUE INDEX unique_daily_page_stats ON daily_page_stats(site_id, hostname_id, pathname_id, date);\nCREATE UNIQUE INDEX unique_daily_referrer_stats ON daily_referrer_stats(site_id, hostname_id, pathname_id, date);\n\n-- +migrate Down\nDROP INDEX IF EXISTS unique_daily_page_stats;\nDROP INDEX IF EXISTS unique_daily_referrer_stats;"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/21_alter_page_stats_table.sql",
    "content": "-- +migrate Up\nCREATE TABLE page_stats(\n   site_id INTEGER NOT NULL DEFAULT 1,\n   hostname_id INTEGER NOT NULL,\n   pathname_id INTEGER NOT NULL,\n   pageviews INTEGER NOT NULL,\n   visitors INTEGER NOT NULL,\n   entries INTEGER NOT NULL,\n   bounce_rate FLOAT NOT NULL,\n   known_durations INTEGER NOT NULL DEFAULT 0,\n   avg_duration FLOAT NOT NULL,\n   ts DATETIME NOT NULL\n);\nINSERT INTO page_stats \n    SELECT site_id, hostname_id, pathname_id, pageviews, visitors, entries, bounce_rate, known_durations, avg_duration, date || ' 00:00:00'\n    FROM daily_page_stats s ;\nDROP TABLE daily_page_stats;\n\n-- +migrate Down"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/22_alter_site_stats_table.sql",
    "content": "-- +migrate Up\nCREATE TABLE site_stats(\n   site_id INTEGER NOT NULL DEFAULT 1,\n   pageviews INTEGER NOT NULL,\n   visitors INTEGER NOT NULL,\n   sessions INTEGER NOT NULL,\n   bounce_rate FLOAT NOT NULL,\n   known_durations INTEGER NOT NULL DEFAULT 0,\n   avg_duration FLOAT NOT NULL,\n   ts DATETIME NOT NULL\n);\nINSERT INTO site_stats \n    SELECT site_id, pageviews, visitors, sessions, bounce_rate, known_durations, avg_duration, date || ' 00:00:00'\n    FROM daily_site_stats s ;\nDROP TABLE daily_site_stats;\n\n-- +migrate Down"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/23_alter_referrer_stats_table.sql",
    "content": "-- +migrate Up\nCREATE TABLE referrer_stats(\n    site_id INTEGER NOT NULL DEFAULT 1, \n    hostname_id INTEGER NOT NULL,\n    pathname_id INTEGER NOT NULL,\n    groupname VARCHAR(255) NULL, \n    pageviews INTEGER NOT NULL, \n    visitors INTEGER NOT NULL, \n    bounce_rate FLOAT NOT NULL, \n    known_durations INTEGER NOT NULL DEFAULT 0, \n    avg_duration FLOAT NOT NULL, \n    ts DATETIME NOT NULL \n);\nINSERT INTO referrer_stats \n    SELECT site_id, hostname_id, pathname_id, groupname, pageviews, visitors, bounce_rate, known_durations, avg_duration, date || ' 00:00:00' \n    FROM daily_referrer_stats s;\nDROP TABLE daily_referrer_stats;\n\n-- +migrate Down\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/24_recreate_stat_table_indices.sql",
    "content": "-- +migrate Up\nDROP INDEX IF EXISTS unique_daily_page_stats;\nDROP INDEX IF EXISTS unique_daily_referrer_stats;\nCREATE UNIQUE INDEX unique_page_stats ON page_stats(site_id, hostname_id, pathname_id, ts);\nCREATE UNIQUE INDEX unique_referrer_stats ON referrer_stats(site_id, hostname_id, pathname_id, ts);\nCREATE UNIQUE INDEX unique_site_stats ON site_stats(site_id, ts);\n\n-- +migrate Down\nDROP INDEX IF EXISTS unique_page_stats;\nDROP INDEX IF EXISTS unique_referrer_stats;\nDROP INDEX IF EXISTS unique_site_stats;"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/25_vacuum.sql",
    "content": "-- +migrate Up  notransaction\nVACUUM;\n\n-- +migrate Down "
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/26_sites_id_autoinc.sql",
    "content": "-- +migrate Up\nDROP TABLE IF EXISTS sites_old;\nALTER TABLE sites RENAME TO sites_old;\nCREATE TABLE sites (\n    `id` INTEGER PRIMARY KEY AUTOINCREMENT,\n    `tracking_id` VARCHAR(8) UNIQUE,\n    `name` VARCHAR(100) NOT NULL\n);\nINSERT INTO sites SELECT `id`, `tracking_id`, `name` FROM sites_old;\n\n-- +migrate Down\nDROP TABLE IF EXISTS sites_old;\nALTER TABLE sites RENAME TO sites_old;\nCREATE TABLE sites (\n    `id` INTEGER PRIMARY KEY,\n    `tracking_id` VARCHAR(8) UNIQUE,\n    `name` VARCHAR(100) NOT NULL\n);\nINSERT INTO sites SELECT `id`, `tracking_id`, `name` FROM sites_old;\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/2_known_durations_column.sql",
    "content": "-- +migrate Up\n\nALTER TABLE daily_site_stats ADD COLUMN known_durations INTEGER NOT NULL DEFAULT 0;\nALTER TABLE daily_page_stats ADD COLUMN known_durations INTEGER NOT NULL DEFAULT 0;\nALTER TABLE daily_referrer_stats ADD COLUMN known_durations INTEGER NOT NULL DEFAULT 0;\n\n-- +migrate Down\n\nALTER TABLE daily_site_stats DROP COLUMN known_durations;\nALTER TABLE daily_page_stats DROP COLUMN known_durations;\nALTER TABLE daily_referrer_stats DROP COLUMN known_durations;\n\n\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/3_referrer_group_column.sql",
    "content": "-- +migrate Up\n\nALTER TABLE daily_referrer_stats ADD COLUMN groupname VARCHAR(255);\nALTER TABLE daily_referrer_stats ADD COLUMN hostname VARCHAR(255);\nALTER TABLE daily_referrer_stats ADD COLUMN pathname VARCHAR(255);\n\nUPDATE daily_referrer_stats SET hostname = SUBSTR(url, 0, (INSTR(url, '://')+3+INSTR(SUBSTR(url, INSTR(url, '://')+3), '/')-1)) WHERE url != '' AND (hostname = '' OR hostname IS NULL);\nUPDATE daily_referrer_stats SET pathname = SUBSTR(url, LENGTH(hostname)+1) WHERE url != '' AND (pathname = '' OR pathname IS NULL);\n\n-- drop `url` column... oh sqlite\nALTER TABLE daily_referrer_stats RENAME TO daily_referrer_stats_old;\nCREATE TABLE daily_referrer_stats(\n   hostname VARCHAR(255) NOT NULL,\n   pathname VARCHAR(255) NOT NULL,\n   groupname VARCHAR(255) NULL,\n   pageviews INTEGER NOT NULL,\n   visitors INTEGER NOT NULL,\n   bounce_rate FLOAT NOT NULL,\n   avg_duration FLOAT NOT NULL,\n   known_durations INTEGER NOT NULL DEFAULT 0,\n   date DATE NOT NULL\n);\nINSERT INTO daily_referrer_stats SELECT hostname, pathname, groupname, pageviews, visitors, bounce_rate, avg_duration, known_durations, date FROM daily_referrer_stats_old;\n\n-- +migrate Down\n\n-- TODO....\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/4_pageview_id_column.sql",
    "content": "-- +migrate Up\n\nDROP TABLE pageviews;\nCREATE TABLE pageviews(\n   id VARCHAR(31) NOT NULL,\n   hostname VARCHAR(255) NOT NULL,\n   pathname VARCHAR(255) NOT NULL,\n   is_new_visitor TINYINT(1) NOT NULL,\n   is_new_session TINYINT(1) NOT NULL,\n   is_unique TINYINT(1) NOT NULL,\n   is_bounce TINYINT(1) NULL,\n   referrer VARCHAR(255) NULL,\n   duration INTEGER(4) NULL,\n   timestamp DATETIME NOT NULL\n);\n\n-- +migrate Down\n\nDROP TABLE pageviews;\nCREATE TABLE pageviews(\n   id INTEGER PRIMARY KEY,\n   hostname VARCHAR(255) NOT NULL,\n   pathname VARCHAR(255) NOT NULL,\n   session_id VARCHAR(16) NOT NULL,\n   is_new_visitor TINYINT(1) NOT NULL,\n   is_new_session TINYINT(1) NOT NULL,\n   is_unique TINYINT(1) NOT NULL,\n   is_bounce TINYINT(1) NULL,\n   referrer VARCHAR(255) NULL,\n   duration INTEGER(4) NULL,\n   timestamp DATETIME NOT NULL\n);\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/5_create_sites_table.sql",
    "content": "-- +migrate Up\nCREATE TABLE sites (\n    id INTEGER PRIMARY KEY,\n    tracking_id VARCHAR(8) UNIQUE,\n    name VARCHAR(100) NOT NULL\n);\n\n-- +migrate Down\nDROP TABLE IF EXISTS sites;"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/6_add_site_tracking_id_column_to_pageviews_table.sql",
    "content": "-- +migrate Up\n\nDROP TABLE IF EXISTS pageviews;\nCREATE TABLE pageviews(\n   id VARCHAR(31) NOT NULL,\n   site_tracking_id VARCHAR(8) NOT NULL,\n   hostname VARCHAR(255) NOT NULL,\n   pathname VARCHAR(255) NOT NULL,\n   is_new_visitor TINYINT(1) NOT NULL,\n   is_new_session TINYINT(1) NOT NULL,\n   is_unique TINYINT(1) NOT NULL,\n   is_bounce TINYINT(1) NULL,\n   referrer VARCHAR(255) NULL,\n   duration INTEGER(4) NULL,\n   timestamp DATETIME NOT NULL\n);\n\n\n-- +migrate Down\n\nDROP TABLE IF EXISTS pageviews;\nCREATE TABLE pageviews(\n   id VARCHAR(31) NOT NULL,\n   hostname VARCHAR(255) NOT NULL,\n   pathname VARCHAR(255) NOT NULL,\n   is_new_visitor TINYINT(1) NOT NULL,\n   is_new_session TINYINT(1) NOT NULL,\n   is_unique TINYINT(1) NOT NULL,\n   is_bounce TINYINT(1) NULL,\n   referrer VARCHAR(255) NULL,\n   duration INTEGER(4) NULL,\n   timestamp DATETIME NOT NULL\n);\n\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/7_add_site_id_to_site_stats_table.sql",
    "content": "-- +migrate Up\n\nALTER TABLE daily_site_stats ADD COLUMN site_id INTEGER NOT NULL DEFAULT 1;\n\n-- +migrate Down\n\n\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/8_add_site_id_to_page_stats_table.sql",
    "content": "-- +migrate Up\n\nALTER TABLE daily_page_stats ADD COLUMN site_id INTEGER NOT NULL DEFAULT 1;\n\n-- +migrate Down\n\n\n"
  },
  {
    "path": "pkg/datastore/sqlstore/migrations/sqlite3/9_add_site_id_to_referrer_stats_table.sql",
    "content": "-- +migrate Up\n\nALTER TABLE daily_referrer_stats ADD COLUMN site_id INTEGER NOT NULL DEFAULT 1;\n\n-- +migrate Down\n\n\n"
  },
  {
    "path": "pkg/datastore/sqlstore/page_stats.go",
    "content": "package sqlstore\n\nimport (\n\t\"database/sql\"\n\t\"time\"\n\n\t\"github.com/usefathom/fathom/pkg/models\"\n)\n\nfunc (db *sqlstore) GetPageStats(siteID int64, date time.Time, hostnameID int64, pathnameID int64) (*models.PageStats, error) {\n\tstats := &models.PageStats{New: false}\n\tquery := db.Rebind(`SELECT * FROM page_stats WHERE site_id = ? AND hostname_id = ? AND pathname_id = ? AND ts = ? LIMIT 1`)\n\terr := db.Get(stats, query, siteID, hostnameID, pathnameID, date.Format(DATE_FORMAT))\n\tif err == sql.ErrNoRows {\n\t\treturn nil, ErrNoResults\n\t}\n\n\treturn stats, mapError(err)\n}\n\nfunc (db *sqlstore) SavePageStats(s *models.PageStats) error {\n\tif s.New {\n\t\treturn db.insertPageStats(s)\n\t}\n\n\treturn db.updatePageStats(s)\n}\n\nfunc (db *sqlstore) insertPageStats(s *models.PageStats) error {\n\tquery := db.Rebind(`INSERT INTO page_stats(pageviews, visitors, entries, bounce_rate, avg_duration, known_durations, site_id, hostname_id, pathname_id, ts) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)\n\t_, err := db.Exec(query, s.Pageviews, s.Visitors, s.Entries, s.BounceRate, s.AvgDuration, s.KnownDurations, s.SiteID, s.HostnameID, s.PathnameID, s.Date.Format(DATE_FORMAT))\n\treturn err\n}\n\nfunc (db *sqlstore) updatePageStats(s *models.PageStats) error {\n\tquery := db.Rebind(`UPDATE page_stats SET pageviews = ?, visitors = ?, entries = ?, bounce_rate = ?, avg_duration = ?, known_durations = ? WHERE site_id = ? AND hostname_id = ? AND pathname_id = ? AND ts = ?`)\n\t_, err := db.Exec(query, s.Pageviews, s.Visitors, s.Entries, s.BounceRate, s.AvgDuration, s.KnownDurations, s.SiteID, s.HostnameID, s.PathnameID, s.Date.Format(DATE_FORMAT))\n\treturn err\n}\n\nfunc (db *sqlstore) SelectAggregatedPageStats(siteID int64, startDate time.Time, endDate time.Time, offset int, limit int) ([]*models.PageStats, error) {\n\tvar result []*models.PageStats\n\tquery := db.Rebind(`SELECT \n\t\th.name AS hostname,\n\t\tp.name AS pathname,\n\t\tSUM(pageviews) AS pageviews, \n\t\tSUM(visitors) AS visitors, \n\t\tSUM(entries) AS entries, \n\t\tCOALESCE(SUM(entries*bounce_rate) / NULLIF(SUM(entries), 0), 0.00) AS bounce_rate, \n\t\tCOALESCE(SUM(pageviews*avg_duration) / SUM(pageviews), 0.00) AS avg_duration \n\t\tFROM page_stats s \n\t\t\tLEFT JOIN hostnames h ON h.id = s.hostname_id \n\t\t\tLEFT JOIN pathnames p ON p.id = s.pathname_id \n\t\tWHERE site_id = ? AND ts >= ? AND ts <= ? \n\t\tGROUP BY hostname, pathname \n\t\tORDER BY pageviews DESC LIMIT ? OFFSET ?`)\n\terr := db.Select(&result, query, siteID, startDate.Format(DATE_FORMAT), endDate.Format(DATE_FORMAT), limit, offset)\n\treturn result, err\n}\n\nfunc (db *sqlstore) GetAggregatedPageStatsPageviews(siteID int64, startDate time.Time, endDate time.Time) (int64, error) {\n\tvar result int64\n\tquery := db.Rebind(`SELECT COALESCE(SUM(pageviews), 0) FROM page_stats WHERE site_id = ? AND ts >= ? AND ts <= ?`)\n\terr := db.Get(&result, query, siteID, startDate.Format(DATE_FORMAT), endDate.Format(DATE_FORMAT))\n\treturn result, err\n}\n"
  },
  {
    "path": "pkg/datastore/sqlstore/pageviews.go",
    "content": "package sqlstore\n\nimport (\n\t\"strings\"\n\t\"time\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/usefathom/fathom/pkg/models\"\n)\n\n// GetPageview selects a single pageview by its string ID\nfunc (db *sqlstore) GetPageview(id string) (*models.Pageview, error) {\n\tresult := &models.Pageview{}\n\tquery := db.Rebind(`SELECT * FROM pageviews WHERE id = ? LIMIT 1`)\n\terr := db.Get(result, query, id)\n\n\tif err != nil {\n\t\treturn nil, mapError(err)\n\t}\n\n\treturn result, nil\n}\n\n// InsertPageviews bulks-insert multiple pageviews using a single INSERT statement\n// IMPORTANT: This does not insert the actual IsBounce, Duration and IsFinished values\nfunc (db *sqlstore) InsertPageviews(pageviews []*models.Pageview) error {\n\tn := len(pageviews)\n\tif n == 0 {\n\t\treturn nil\n\t}\n\n\t// generate placeholders string\n\tplaceholderTemplate := \"(?, ?, ?, ?, ?, ?, ?, ?, ?, TRUE, FALSE, 0),\"\n\tplaceholders := strings.Repeat(placeholderTemplate, n)\n\tplaceholders = placeholders[:len(placeholders)-1]\n\tnPlaceholders := strings.Count(placeholderTemplate, \"?\")\n\n\t// init values slice with correct length\n\tnValues := n * nPlaceholders\n\tvalues := make([]interface{}, nValues)\n\n\t// overwrite nil values in slice\n\tj := 0\n\tfor i := range pageviews {\n\n\t\t// test for columns with ignored values\n\t\tif pageviews[i].IsBounce != true || pageviews[i].Duration > 0 || pageviews[i].IsFinished != false {\n\t\t\tlog.Warnf(\"inserting pageview with invalid column values for bulk-insert\")\n\t\t}\n\n\t\tj = i * nPlaceholders\n\t\tvalues[j] = pageviews[i].ID\n\t\tvalues[j+1] = pageviews[i].SiteTrackingID\n\t\tvalues[j+2] = pageviews[i].Hostname\n\t\tvalues[j+3] = pageviews[i].Pathname\n\t\tvalues[j+4] = pageviews[i].IsNewVisitor\n\t\tvalues[j+5] = pageviews[i].IsNewSession\n\t\tvalues[j+6] = pageviews[i].IsUnique\n\t\tvalues[j+7] = pageviews[i].Referrer\n\t\tvalues[j+8] = pageviews[i].Timestamp\n\t}\n\n\t// string together query & execute with values\n\tquery := `INSERT INTO pageviews(id, site_tracking_id, hostname, pathname, is_new_visitor, is_new_session, is_unique, referrer, timestamp, is_bounce, is_finished, duration) VALUES ` + placeholders\n\tquery = db.Rebind(query)\n\t_, err := db.Exec(query, values...)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// UpdatePageviews updates multiple pageviews using a single transaction\n// IMPORTANT: this function only updates the IsFinished, IsBounce and Duration values\nfunc (db *sqlstore) UpdatePageviews(pageviews []*models.Pageview) error {\n\tif len(pageviews) == 0 {\n\t\treturn nil\n\t}\n\n\ttx, err := db.Beginx()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tquery := tx.Rebind(`UPDATE pageviews SET is_bounce = ?, duration = ?, is_finished = ? WHERE id = ?`)\n\tstmt, err := tx.Preparex(query)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor i := range pageviews {\n\t\t_, err := stmt.Exec(pageviews[i].IsBounce, pageviews[i].Duration, pageviews[i].IsFinished, pageviews[i].ID)\n\n\t\tif err != nil {\n\t\t\ttx.Rollback()\n\t\t\treturn err\n\t\t}\n\t}\n\n\terr = tx.Commit()\n\treturn err\n}\n\n// GetProcessablePageviews selects all pageviews which are \"done\" (ie not still waiting for bounce flag or duration)\nfunc (db *sqlstore) GetProcessablePageviews(limit int) ([]*models.Pageview, error) {\n\tvar results []*models.Pageview\n\tthirtyMinsAgo := time.Now().Add(-30 * time.Minute)\n\tquery := db.Rebind(`SELECT * FROM pageviews WHERE is_finished = TRUE OR timestamp < ? LIMIT ?`)\n\terr := db.Select(&results, query, thirtyMinsAgo, limit)\n\treturn results, err\n}\n\nfunc (db *sqlstore) DeletePageviews(pageviews []*models.Pageview) error {\n\tids := []string{}\n\tfor _, p := range pageviews {\n\t\tids = append(ids, \"'\"+p.ID+\"'\")\n\t}\n\tquery := db.Rebind(`DELETE FROM pageviews WHERE id IN(` + strings.Join(ids, \",\") + `)`)\n\t_, err := db.Exec(query)\n\treturn err\n}\n"
  },
  {
    "path": "pkg/datastore/sqlstore/pathnames.go",
    "content": "package sqlstore\n\nimport (\n\t\"database/sql\"\n)\n\nfunc (db *sqlstore) PathnameID(name string) (int64, error) {\n\tvar id int64\n\tquery := db.Rebind(\"SELECT id FROM pathnames WHERE name = ? LIMIT 1\")\n\terr := db.Get(&id, query, name)\n\n\tif err == sql.ErrNoRows {\n\t\t// Postgres does not support LastInsertID, so use a \"... RETURNING\" select query\n\t\tquery := db.Rebind(`INSERT INTO pathnames(name) VALUES(?)`)\n\t\tif db.Driver == POSTGRES {\n\t\t\terr := db.Get(&id, query+\" RETURNING id\", name)\n\t\t\treturn id, err\n\t\t}\n\n\t\t// MySQL and SQLite do support LastInsertID, so use that\n\t\tr, err := db.Exec(query, name)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\n\t\treturn r.LastInsertId()\n\t}\n\n\treturn id, err\n}\n"
  },
  {
    "path": "pkg/datastore/sqlstore/referrer_stats.go",
    "content": "package sqlstore\n\nimport (\n\t\"database/sql\"\n\t\"time\"\n\n\t\"github.com/usefathom/fathom/pkg/models\"\n)\n\nfunc (db *sqlstore) GetReferrerStats(siteID int64, date time.Time, hostnameID int64, pathnameID int64) (*models.ReferrerStats, error) {\n\tstats := &models.ReferrerStats{New: false}\n\tquery := db.Rebind(`SELECT * FROM referrer_stats WHERE site_id = ? AND ts = ? AND hostname_id = ? AND pathname_id = ? LIMIT 1`)\n\terr := db.Get(stats, query, siteID, date.Format(DATE_FORMAT), hostnameID, pathnameID)\n\tif err == sql.ErrNoRows {\n\t\treturn nil, ErrNoResults\n\t}\n\n\treturn stats, mapError(err)\n}\n\nfunc (db *sqlstore) SaveReferrerStats(s *models.ReferrerStats) error {\n\tif s.New {\n\t\treturn db.insertReferrerStats(s)\n\t}\n\n\treturn db.updateReferrerStats(s)\n}\n\nfunc (db *sqlstore) insertReferrerStats(s *models.ReferrerStats) error {\n\tquery := db.Rebind(`INSERT INTO referrer_stats(visitors, pageviews, bounce_rate, avg_duration, known_durations, groupname, site_id, hostname_id, pathname_id, ts) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)\n\t_, err := db.Exec(query, s.Visitors, s.Pageviews, s.BounceRate, s.AvgDuration, s.KnownDurations, s.Group, s.SiteID, s.HostnameID, s.PathnameID, s.Date.Format(DATE_FORMAT))\n\treturn err\n}\n\nfunc (db *sqlstore) updateReferrerStats(s *models.ReferrerStats) error {\n\tquery := db.Rebind(`UPDATE referrer_stats SET visitors = ?, pageviews = ?, bounce_rate = ?, avg_duration = ?, known_durations = ?, groupname = ? WHERE site_id = ? AND hostname_id = ? AND pathname_id = ? AND ts = ?`)\n\t_, err := db.Exec(query, s.Visitors, s.Pageviews, s.BounceRate, s.AvgDuration, s.KnownDurations, s.Group, s.SiteID, s.HostnameID, s.PathnameID, s.Date.Format(DATE_FORMAT))\n\treturn err\n}\n\nfunc (db *sqlstore) SelectAggregatedReferrerStats(siteID int64, startDate time.Time, endDate time.Time, offset int, limit int) ([]*models.ReferrerStats, error) {\n\tvar result []*models.ReferrerStats\n\n\tsql := `SELECT \n\t\tMIN(h.name) AS hostname,\n\t\tMIN(p.name) AS pathname,\n\t\tCOALESCE(MIN(groupname), '') AS groupname,  \n\t\tSUM(visitors) AS visitors, \n\t\tSUM(pageviews) AS pageviews, \n\t\tSUM(pageviews*bounce_rate) / SUM(pageviews) AS bounce_rate, \n\t\tSUM(pageviews*avg_duration) / SUM(pageviews) AS avg_duration \n\tFROM referrer_stats s\n\t\tLEFT JOIN hostnames h ON h.id = s.hostname_id \n\t\tLEFT JOIN pathnames p ON p.id = s.pathname_id \n\tWHERE site_id = ? AND ts >= ? AND ts <= ? `\n\n\tif db.Config.Driver == \"sqlite3\" {\n\t\tsql = sql + `GROUP BY COALESCE(NULLIF(groupname, ''), hostname_id || pathname_id ) `\n\t} else {\n\t\tsql = sql + `GROUP BY COALESCE(NULLIF(groupname, ''), CONCAT(hostname_id, pathname_id) ) `\n\t}\n\tsql = sql + ` ORDER BY pageviews DESC LIMIT ? OFFSET ?`\n\n\tquery := db.Rebind(sql)\n\n\terr := db.Select(&result, query, siteID, startDate.Format(DATE_FORMAT), endDate.Format(DATE_FORMAT), limit, offset)\n\treturn result, mapError(err)\n}\n\nfunc (db *sqlstore) GetAggregatedReferrerStatsPageviews(siteID int64, startDate time.Time, endDate time.Time) (int64, error) {\n\tvar result int64\n\tquery := db.Rebind(`SELECT COALESCE(SUM(pageviews), 0) FROM referrer_stats WHERE site_id = ? AND ts >= ? AND ts <= ?`)\n\terr := db.Get(&result, query, siteID, startDate.Format(DATE_FORMAT), endDate.Format(DATE_FORMAT))\n\treturn result, mapError(err)\n}\n"
  },
  {
    "path": "pkg/datastore/sqlstore/seed/pageviews.sql",
    "content": "INSERT INTO pageviews \n   ( session_id, pathname, is_new_visitor, is_unique, is_bounce, referrer, duration, timestamp) VALUES \n   ( LEFT(UUID(), 8), \"/\", 1, 1, 0, \"\", 15, \"2018-05-03 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/\", 1, 1, 1, \"\", 14, \"2018-05-03 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/\", 1, 1, 0, \"\", 13, \"2018-05-04 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/\", 0, 1, 0, \"\", 16, \"2018-05-04 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/\", 0, 1, 0, \"\", 16, \"2018-05-05 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/\", 0, 1, 0, \"\", 17, \"2018-05-05 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/\", 0, 1, 1, \"\", 18, \"2018-05-05 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/\", 1, 1, 0, \"\", 15, \"2018-05-05 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/\", 1, 1, 1, \"https://duckduckgo.com/\", 15, \"2018-05-05 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/\", 1, 1, 0, \"https://duckduckgo.com/\", 15, \"2018-05-05 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/\", 0, 1, 0, \"https://duckduckgo.com/\", 150, \"2018-05-05 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/\", 0, 1, 0, \"https://mozilla.org/\", 15, \"2018-05-05 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/about\", 1, 1, 0, \"\", 15, \"2018-05-05 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/about\", 1, 1, 0, \"\", 10, \"2018-05-05 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/about\", 1, 1, 0, \"\", 11, \"2018-05-05 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/contact\", 1, 1, 0, \"\", 21, \"2018-05-05 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/contact\", 1, 1, 0, \"\", 15, \"2018-05-05 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/contact\", 0, 1, 0, \"\", 15, \"2018-05-05 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/contact\", 0, 1, 1, \"\", 8, \"2018-05-05 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/contact\", 1, 1, 0, \"\", 15, \"2018-05-05 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/contact\", 1, 1, 1, \"https://wikipedia.com/\", 15, \"2018-05-05 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/pricing\", 1, 0, 0, \"\", 15, \"2018-05-05 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/pricing\", 1, 0, 1, \"\", 15, \"2018-05-05 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/pricing\", 1, 1, 0, \"\", 24, \"2018-05-05 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/pricing\", 1, 1, 1, \"\", 15, \"2018-05-05 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/pricing\", 1, 0, 0, \"\", 8, \"2018-05-05 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/pricing\", 1, 0, 1, \"\", 24, \"2018-05-05 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/pricing\", 1, 1, 0, \"\", 15, \"2018-05-05 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/pricing\", 1, 1, 1, \"\", 15, \"2018-05-05 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/pricing\", 1, 1, 0, \"\", 14, \"2018-05-05 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/pricing\", 1, 1, 0, \"\", 15, \"2018-05-05 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/pricing\", 1, 1, 0, \"\", 15, \"2018-05-05 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/\", 1, 1, 0, \"\", 24, \"2018-05-05 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/\", 1, 1, 0, \"\", 15, \"2018-05-05 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/\", 1, 1, 0, \"\", 24, \"2018-05-05 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/\", 1, 1, 1, \"\", 15, \"2018-05-05 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/\", 1, 1, 0, \"https://pjrvs.com\", 8, \"2018-05-05 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/\", 1, 1, 1, \"https://pjrvs.com\", 24, \"2018-05-05 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/\", 1, 1, 0, \"https://pjrvs.com\", 15, \"2018-05-05 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/\", 1, 0, 1, \"\", 19, \"2018-05-05 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/\", 1, 0, 0, \"\", 15, \"2018-05-05 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/\", 1, 0, 0, \"\", 19, \"2018-05-05 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/\", 1, 1, 0, \"https://dvk.co/\", 19, \"2018-05-05 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/\", 1, 1, 0, \"https://dvk.co/\", 15, \"2018-05-05 15:00:00\"),\n   ( LEFT(UUID(), 8), \"/\", 1, 1, 0, \"https://dvk.co/\", 14, \"2018-05-05 15:00:00\");\n"
  },
  {
    "path": "pkg/datastore/sqlstore/site_stats.go",
    "content": "package sqlstore\n\nimport (\n\t\"database/sql\"\n\t\"time\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/usefathom/fathom/pkg/models\"\n)\n\nfunc (db *sqlstore) GetSiteStats(siteID int64, date time.Time) (*models.SiteStats, error) {\n\tstats := &models.SiteStats{New: false}\n\tquery := db.Rebind(`SELECT * FROM site_stats WHERE site_id = ? AND ts = ? LIMIT 1`)\n\n\terr := db.Get(stats, query, siteID, date.Format(DATE_FORMAT))\n\tif err == sql.ErrNoRows {\n\t\treturn nil, ErrNoResults\n\t}\n\n\treturn stats, mapError(err)\n}\n\nfunc (db *sqlstore) SaveSiteStats(s *models.SiteStats) error {\n\tif s.New {\n\t\treturn db.insertSiteStats(s)\n\t}\n\n\treturn db.updateSiteStats(s)\n}\n\nfunc (db *sqlstore) insertSiteStats(s *models.SiteStats) error {\n\tquery := db.Rebind(`INSERT INTO site_stats(site_id, visitors, sessions, pageviews, bounce_rate, avg_duration, known_durations, ts) VALUES(?, ?, ?, ?, ?, ?, ?, ?)`)\n\t_, err := db.Exec(query, s.SiteID, s.Visitors, s.Sessions, s.Pageviews, s.BounceRate, s.AvgDuration, s.KnownDurations, s.Date.Format(DATE_FORMAT))\n\treturn err\n}\n\nfunc (db *sqlstore) updateSiteStats(s *models.SiteStats) error {\n\tquery := db.Rebind(`UPDATE site_stats SET visitors = ?, sessions = ?, pageviews = ?, bounce_rate = ?, avg_duration = ?, known_durations = ? WHERE site_id = ? AND ts = ?`)\n\t_, err := db.Exec(query, s.Visitors, s.Sessions, s.Pageviews, s.BounceRate, s.AvgDuration, s.KnownDurations, s.SiteID, s.Date.Format(DATE_FORMAT))\n\treturn err\n}\n\nfunc (db *sqlstore) SelectSiteStats(siteID int64, startDate time.Time, endDate time.Time) ([]*models.SiteStats, error) {\n\tresults := []*models.SiteStats{}\n\tquery := db.Rebind(`SELECT *\n\t\tFROM site_stats \n\t\tWHERE site_id = ? AND ts >= ? AND ts <= ? \n\t\tORDER BY ts DESC`)\n\terr := db.Select(&results, query, siteID, startDate.Format(DATE_FORMAT), endDate.Format(DATE_FORMAT))\n\treturn results, err\n}\n\nfunc (db *sqlstore) GetAggregatedSiteStats(siteID int64, startDate time.Time, endDate time.Time) (*models.SiteStats, error) {\n\tstats := &models.SiteStats{}\n\tquery := db.Rebind(`SELECT \n\t\tSUM(pageviews) AS pageviews,\n\t\tSUM(visitors) AS visitors,\n\t\tSUM(sessions) AS sessions,\n\t\tSUM(pageviews*avg_duration) / SUM(pageviews) AS avg_duration,\n\t\tCOALESCE(SUM(sessions*bounce_rate) / SUM(sessions), 0.00) AS bounce_rate\n\t FROM site_stats \n\t WHERE site_id = ? AND ts >= ? AND ts <= ? LIMIT 1`)\n\terr := db.Get(stats, query, siteID, startDate.Format(DATE_FORMAT), endDate.Format(DATE_FORMAT))\n\treturn stats, mapError(err)\n}\n\nfunc (db *sqlstore) GetRealtimeVisitorCount(siteID int64) (int64, error) {\n\tvar siteTrackingID string\n\tif err := db.Get(&siteTrackingID, db.Rebind(`SELECT tracking_id FROM sites WHERE id = ? LIMIT 1`), siteID); err != nil && err != sql.ErrNoRows {\n\t\tlog.Error(err)\n\t\treturn 0, mapError(err)\n\t}\n\n\tvar sql string\n\tvar total int64\n\n\t// for backwards compatibility with tracking snippets without an explicit site tracking ID (< 1.1.0)\n\tif siteID == 1 {\n\t\tsql = `SELECT COUNT(*) FROM pageviews p WHERE ( site_tracking_id = ? OR site_tracking_id = '' ) AND is_finished = FALSE AND timestamp > ?`\n\t} else {\n\t\tsql = `SELECT COUNT(*) FROM pageviews p WHERE site_tracking_id = ? AND is_finished = FALSE AND timestamp > ?`\n\t}\n\n\tquery := db.Rebind(sql)\n\tif err := db.Get(&total, query, siteTrackingID, time.Now().Add(-5*time.Minute)); err != nil {\n\t\treturn 0, mapError(err)\n\t}\n\n\treturn total, nil\n}\n"
  },
  {
    "path": "pkg/datastore/sqlstore/sites.go",
    "content": "package sqlstore\n\nimport (\n\t\"database/sql\"\n\n\t\"github.com/usefathom/fathom/pkg/models\"\n)\n\n// GetSites gets all sites in the database\nfunc (db *sqlstore) GetSites() ([]*models.Site, error) {\n\tresults := []*models.Site{}\n\tquery := db.Rebind(`SELECT * FROM sites`)\n\terr := db.Select(&results, query)\n\n\t// don't err on no rows\n\tif err == sql.ErrNoRows {\n\t\treturn results, nil\n\t}\n\n\treturn results, err\n}\n\nfunc (db *sqlstore) GetSite(id int64) (*models.Site, error) {\n\ts := &models.Site{}\n\tquery := db.Rebind(\"SELECT * FROM sites WHERE id = ?\")\n\terr := db.Get(s, query, id)\n\treturn s, mapError(err)\n}\n\n// SaveSite saves the website in the database (inserts or updates)\nfunc (db *sqlstore) SaveSite(s *models.Site) error {\n\tif s.ID > 0 {\n\t\treturn db.updateSite(s)\n\t}\n\n\treturn db.insertSite(s)\n}\n\n// InsertSite saves a new site in the database\nfunc (db *sqlstore) insertSite(s *models.Site) error {\n\n\t// Postgres does not support LastInsertID, so use a \"... RETURNING\" select query\n\tquery := db.Rebind(`INSERT INTO sites(tracking_id, name) VALUES(?, ?)`)\n\tif db.Driver == POSTGRES {\n\t\terr := db.Get(&s.ID, query+\" RETURNING id\", s.TrackingID, s.Name)\n\t\treturn err\n\t}\n\n\t// MySQL and SQLite do support LastInsertID, so use that\n\tr, err := db.Exec(query, s.TrackingID, s.Name)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ts.ID, err = r.LastInsertId()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// UpdateSite updates an existing site in the database\nfunc (db *sqlstore) updateSite(s *models.Site) error {\n\tquery := db.Rebind(`UPDATE sites SET name = ? WHERE id = ?`)\n\t_, err := db.Exec(query, s.Name, s.ID)\n\treturn err\n}\n\n// DeleteSite deletes the  given site in the database\nfunc (db *sqlstore) DeleteSite(s *models.Site) error {\n\tquery := db.Rebind(`DELETE FROM sites WHERE id = ?`)\n\t_, err := db.Exec(query, s.ID)\n\treturn err\n}\n"
  },
  {
    "path": "pkg/datastore/sqlstore/sqlstore.go",
    "content": "package sqlstore\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"time\"\n\n\t_ \"github.com/go-sql-driver/mysql\" // mysql driver\n\t\"github.com/gobuffalo/packr/v2\"\n\t\"github.com/jmoiron/sqlx\"\n\t_ \"github.com/lib/pq\"           // postgresql driver\n\t_ \"github.com/mattn/go-sqlite3\" //sqlite3 driver\n\tmigrate \"github.com/rubenv/sql-migrate\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nconst (\n\tMYSQL    = \"mysql\"\n\tPOSTGRES = \"postgres\"\n\tSQLITE   = \"sqlite3\"\n\n\tDATE_FORMAT = \"2006-01-02 15:00:00\"\n)\n\ntype sqlstore struct {\n\t*sqlx.DB\n\n\tDriver string\n\tConfig *Config\n}\n\n// ErrNoResults is returned when a query yielded 0 results\nvar ErrNoResults = errors.New(\"datastore: query returned 0 results\")\n\n// New creates a new database pool\nfunc New(c *Config) *sqlstore {\n\tdsn := c.DSN()\n\tdbx, err := sqlx.Connect(c.Driver, dsn)\n\tif err != nil {\n\t\tlog.Fatalf(\"Error connecting to database: %s\", err)\n\t}\n\tdb := &sqlstore{dbx, c.Driver, c}\n\n\tif c.Host == \"\" || c.Driver == SQLITE {\n\t\tlog.Printf(\"Connected to %s database: %s\", c.Driver, c.Dbname())\n\t} else {\n\t\tlog.Printf(\"Connected to %s database: %s on %s\", c.Driver, c.Dbname(), c.Host)\n\t}\n\n\t// apply database migrations (if any)\n\tdb.Migrate()\n\n\treturn db\n}\n\nfunc (db *sqlstore) Migrate() {\n\tmigrationSource := &migrate.PackrMigrationSource{\n\t\tBox: packr.NewBox(\"./migrations\"),\n\t\tDir: db.Config.Driver,\n\t}\n\tmigrate.SetTable(\"migrations\")\n\n\tmigrations, err := migrationSource.FindMigrations()\n\tif err != nil {\n\t\tlog.Errorf(\"Error loading database migrations: %s\", err)\n\t}\n\n\tif len(migrations) == 0 {\n\t\tlog.Fatalf(\"Missing database migrations\")\n\t}\n\n\tn, err := migrate.Exec(db.DB.DB, db.Config.Driver, migrationSource, migrate.Up)\n\tif err != nil {\n\t\tlog.Errorf(\"Error applying database migrations: %s\", err)\n\t}\n\n\tif n > 0 {\n\t\tlog.Infof(\"Applied %d database migrations!\", n)\n\t}\n}\n\n// Health check health of database\nfunc (db *sqlstore) Health() error {\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second*5)\n\tdefer cancel()\n\n\treturn db.PingContext(ctx)\n}\n\n// Closes the db pool\nfunc (db *sqlstore) Close() error {\n\treturn db.DB.Close()\n}\n\nfunc mapError(err error) error {\n\tif err == sql.ErrNoRows {\n\t\treturn ErrNoResults\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/datastore/sqlstore/users.go",
    "content": "package sqlstore\n\nimport (\n\t\"database/sql\"\n\n\t\"github.com/usefathom/fathom/pkg/models\"\n)\n\n// GetUser retrieves user from datastore by its ID\nfunc (db *sqlstore) GetUser(ID int64) (*models.User, error) {\n\tu := &models.User{}\n\tquery := db.Rebind(\"SELECT * FROM users WHERE id = ? LIMIT 1\")\n\terr := db.Get(u, query, ID)\n\n\tif err != nil {\n\t\tif err == sql.ErrNoRows {\n\t\t\treturn nil, ErrNoResults\n\t\t}\n\n\t\treturn nil, err\n\t}\n\n\treturn u, err\n}\n\n// GetUserByEmail retrieves user from datastore by its email\nfunc (db *sqlstore) GetUserByEmail(email string) (*models.User, error) {\n\tu := &models.User{}\n\tquery := db.Rebind(\"SELECT * FROM users WHERE email = ? LIMIT 1\")\n\terr := db.Get(u, query, email)\n\treturn u, mapError(err)\n}\n\n// SaveUser inserts the user model in the connected database\nfunc (db *sqlstore) SaveUser(u *models.User) error {\n\tif u.ID > 0 {\n\t\treturn db.updateUser(u)\n\t}\n\n\treturn db.insertUser(u)\n}\n\n// insertUser saves a new user in the database\nfunc (db *sqlstore) insertUser(u *models.User) error {\n\tvar query = db.Rebind(\"INSERT INTO users(email, password) VALUES(?, ?)\")\n\n\t// Postgres does not support LastInsertID, so use a \"... RETURNING\" select query\n\tif db.Driver == POSTGRES {\n\t\terr := db.Get(&u.ID, query+\" RETURNING id\", u.Email, u.Password)\n\t\treturn err\n\t}\n\n\t// MySQL and SQLite don't support RETURNING, but do support LastInsertId\n\tresult, err := db.Exec(query, u.Email, u.Password)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tu.ID, err = result.LastInsertId()\n\treturn err\n}\n\n// updateUser updates an existing user in the database\nfunc (db *sqlstore) updateUser(u *models.User) error {\n\tvar query = db.Rebind(\"UPDATE users SET email = ?, password = ? WHERE id = ?\")\n\t_, err := db.Exec(query, u.Email, u.Password, u.ID)\n\treturn err\n}\n\n// DeleteUser deletes the user in the datastore\nfunc (db *sqlstore) DeleteUser(u *models.User) error {\n\tquery := db.Rebind(\"DELETE FROM users WHERE id = ?\")\n\t_, err := db.Exec(query, u.ID)\n\treturn err\n}\n\n// CountUsers returns the number of users\nfunc (db *sqlstore) CountUsers() (int64, error) {\n\tvar c int64\n\tvar sql = `SELECT COUNT(*) FROM users`\n\tquery := db.Rebind(sql)\n\terr := db.Get(&c, query)\n\treturn c, err\n}\n"
  },
  {
    "path": "pkg/models/page_stats.go",
    "content": "package models\n\nimport (\n\t\"time\"\n)\n\ntype PageStats struct {\n\tNew            bool      `db:\"-\" json:\"-\"`\n\tSiteID         int64     `db:\"site_id\" json:\"-\"`\n\tHostnameID     int64     `db:\"hostname_id\" json:\"-\"`\n\tPathnameID     int64     `db:\"pathname_id\" json:\"-\"`\n\tHostname       string    `db:\"hostname\"`\n\tPathname       string    `db:\"pathname\"`\n\tPageviews      int64     `db:\"pageviews\"`\n\tVisitors       int64     `db:\"visitors\"`\n\tEntries        int64     `db:\"entries\"`\n\tBounceRate     float64   `db:\"bounce_rate\"`\n\tAvgDuration    float64   `db:\"avg_duration\"`\n\tKnownDurations int64     `db:\"known_durations\"`\n\tDate           time.Time `db:\"ts\" json:\",omitempty\"`\n}\n\nfunc (s *PageStats) HandlePageview(p *Pageview) {\n\n\ts.Pageviews += 1\n\tif p.IsUnique {\n\t\ts.Visitors += 1\n\t}\n\n\tif p.Duration > 0.00 {\n\t\ts.KnownDurations += 1\n\t\ts.AvgDuration = s.AvgDuration + ((float64(p.Duration) - s.AvgDuration) * 1 / float64(s.KnownDurations))\n\t}\n\n\tif p.IsNewSession {\n\t\ts.Entries += 1\n\n\t\tif p.IsBounce {\n\t\t\ts.BounceRate = ((float64(s.Entries-1) * s.BounceRate) + 1.00) / (float64(s.Entries))\n\t\t} else {\n\t\t\ts.BounceRate = ((float64(s.Entries-1) * s.BounceRate) + 0.00) / (float64(s.Entries))\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "pkg/models/page_stats_test.go",
    "content": "package models\n\nimport \"testing\"\n\nfunc TestPageStatsHandlePageview(t *testing.T) {\n\ts := PageStats{}\n\n\tp1 := &Pageview{\n\t\tDuration:     100,\n\t\tIsBounce:     false,\n\t\tIsUnique:     true,\n\t\tIsNewSession: true,\n\t}\n\tp2 := &Pageview{\n\t\tDuration:     60,\n\t\tIsUnique:     false,\n\t\tIsNewSession: false,\n\t\tIsBounce:     true, // should have no effect because only new sessions can bounce\n\t}\n\tp3 := &Pageview{\n\t\tIsUnique:     true,\n\t\tIsNewSession: true,\n\t\tIsBounce:     true,\n\t}\n\n\t// add first pageview & test\n\ts.HandlePageview(p1)\n\tif s.Pageviews != 1 {\n\t\tt.Errorf(\"Pageviews: expected %d, got %d\", 1, s.Pageviews)\n\t}\n\tif s.Visitors != 1 {\n\t\tt.Errorf(\"Visitors: expected %d, got %d\", 1, s.Visitors)\n\t}\n\tif s.AvgDuration != 100 {\n\t\tt.Errorf(\"AvgDuration: expected %.2f, got %.2f\", 100.00, s.AvgDuration)\n\t}\n\tif s.BounceRate != 0.00 {\n\t\tt.Errorf(\"BounceRate: expected %.2f, got %.2f\", 0.00, s.BounceRate)\n\t}\n\n\t// add second pageview\n\ts.HandlePageview(p2)\n\tif s.Pageviews != 2 {\n\t\tt.Errorf(\"Pageviews: expected %d, got %d\", 2, s.Pageviews)\n\t}\n\tif s.Visitors != 1 {\n\t\tt.Errorf(\"Visitors: expected %d, got %d\", 1, s.Visitors)\n\t}\n\tif s.AvgDuration != 80 {\n\t\tt.Errorf(\"AvgDuration: expected %.2f, got %.2f\", 80.00, s.AvgDuration)\n\t}\n\t// should still be 0.00 because p2 was not a new session\n\tif s.BounceRate != 0.00 {\n\t\tt.Errorf(\"BounceRate: expected %.2f, got %.2f\", 0.00, s.BounceRate)\n\t}\n\n\t// add third pageview\n\ts.HandlePageview(p3)\n\tif s.Visitors != 2 {\n\t\tt.Errorf(\"Visitors: expected %d, got %d\", 2, s.Visitors)\n\t}\n\n\tif s.BounceRate != 0.50 {\n\t\tt.Errorf(\"BounceRate: expected %.2f, got %.2f\", 0.50, s.BounceRate)\n\t}\n\n}\n"
  },
  {
    "path": "pkg/models/pageview.go",
    "content": "package models\n\nimport (\n\t\"time\"\n)\n\ntype Pageview struct {\n\tID             string    `db:\"id\"`\n\tSiteTrackingID string    `db:\"site_tracking_id\"`\n\tHostname       string    `db:\"hostname\"`\n\tPathname       string    `db:\"pathname\"`\n\tIsNewVisitor   bool      `db:\"is_new_visitor\"`\n\tIsNewSession   bool      `db:\"is_new_session\"`\n\tIsUnique       bool      `db:\"is_unique\"`\n\tIsBounce       bool      `db:\"is_bounce\"`\n\tIsFinished     bool      `db:\"is_finished\"`\n\tReferrer       string    `db:\"referrer\"`\n\tDuration       int64     `db:\"duration\"`\n\tTimestamp      time.Time `db:\"timestamp\"`\n}\n"
  },
  {
    "path": "pkg/models/referrer_stats.go",
    "content": "package models\n\nimport (\n\t\"time\"\n)\n\ntype ReferrerStats struct {\n\tNew            bool      `db:\"-\" json:\"-\"`\n\tSiteID         int64     `db:\"site_id\" json:\"-\"`\n\tHostnameID     int64     `db:\"hostname_id\" json:\"-\"`\n\tPathnameID     int64     `db:\"pathname_id\" json:\"-\"`\n\tHostname       string    `db:\"hostname\"`\n\tPathname       string    `db:\"pathname\"`\n\tGroup          string    `db:\"groupname\"`\n\tVisitors       int64     `db:\"visitors\"`\n\tPageviews      int64     `db:\"pageviews\"`\n\tBounceRate     float64   `db:\"bounce_rate\"`\n\tAvgDuration    float64   `db:\"avg_duration\"`\n\tKnownDurations int64     `db:\"known_durations\"`\n\tDate           time.Time `db:\"ts\" json:\",omitempty\"`\n}\n\nfunc (s *ReferrerStats) HandlePageview(p *Pageview) {\n\ts.Pageviews += 1\n\n\tif p.IsNewVisitor {\n\t\ts.Visitors += 1\n\t}\n\n\tif p.IsBounce {\n\t\ts.BounceRate = ((float64(s.Pageviews-1) * s.BounceRate) + 1.00) / (float64(s.Pageviews))\n\t} else {\n\t\ts.BounceRate = ((float64(s.Pageviews-1) * s.BounceRate) + 0.00) / (float64(s.Pageviews))\n\t}\n\n\tif p.Duration > 0.00 {\n\t\ts.KnownDurations += 1\n\t\ts.AvgDuration = s.AvgDuration + ((float64(p.Duration) - s.AvgDuration) * 1 / float64(s.KnownDurations))\n\t}\n}\n"
  },
  {
    "path": "pkg/models/referrer_stats_test.go",
    "content": "package models\n\nimport \"testing\"\n\nfunc TestReferrerStatsHandlePageview(t *testing.T) {\n\ts := ReferrerStats{}\n\tp1 := &Pageview{\n\t\tDuration:     100,\n\t\tIsBounce:     false,\n\t\tIsNewVisitor: true,\n\t}\n\tp2 := &Pageview{\n\t\tDuration:     60,\n\t\tIsNewVisitor: false,\n\t\tIsBounce:     true,\n\t}\n\tp3 := &Pageview{\n\t\tIsNewSession: true,\n\t\tIsBounce:     true,\n\t}\n\n\t// add first pageview & test\n\ts.HandlePageview(p1)\n\tif s.Pageviews != 1 {\n\t\tt.Errorf(\"Pageviews: expected %d, got %d\", 1, s.Pageviews)\n\t}\n\tif s.Visitors != 1 {\n\t\tt.Errorf(\"Visitors: expected %d, got %d\", 1, s.Visitors)\n\t}\n\tif s.AvgDuration != 100 {\n\t\tt.Errorf(\"AvgDuration: expected %.2f, got %.2f\", 100.00, s.AvgDuration)\n\t}\n\tif s.BounceRate != 0.00 {\n\t\tt.Errorf(\"BounceRate: expected %.2f, got %.2f\", 0.00, s.BounceRate)\n\t}\n\n\t// add second pageview\n\ts.HandlePageview(p2)\n\tif s.Pageviews != 2 {\n\t\tt.Errorf(\"Pageviews: expected %d, got %d\", 2, s.Pageviews)\n\t}\n\tif s.Visitors != 1 {\n\t\tt.Errorf(\"Visitors: expected %d, got %d\", 1, s.Visitors)\n\t}\n\tif s.AvgDuration != 80 {\n\t\tt.Errorf(\"AvgDuration: expected %.2f, got %.2f\", 80.00, s.AvgDuration)\n\t}\n\tif s.BounceRate != 0.50 {\n\t\tt.Errorf(\"BounceRate: expected %.2f, got %.2f\", 0.50, s.BounceRate)\n\t}\n\n\t// add third pageview\n\ts.HandlePageview(p3)\n\tif int64(100.00*s.BounceRate) != 66 {\n\t\tt.Errorf(\"BounceRate: expected %.2f, got %.2f\", 0.67, s.BounceRate)\n\t}\n\n}\n"
  },
  {
    "path": "pkg/models/site.go",
    "content": "package models\n\n// Site represents a group for tracking data\ntype Site struct {\n\tID         int64  `db:\"id\" json:\"id\"`\n\tTrackingID string `db:\"tracking_id\" json:\"trackingId\"`\n\tName       string `db:\"name\" json:\"name\"`\n}\n"
  },
  {
    "path": "pkg/models/site_stats.go",
    "content": "package models\n\nimport (\n\t\"fmt\"\n\t\"time\"\n)\n\ntype SiteStats struct {\n\tNew            bool      `db:\"-\" json:\"-\" `\n\tSiteID         int64     `db:\"site_id\" json:\"-\"`\n\tVisitors       int64     `db:\"visitors\"`\n\tPageviews      int64     `db:\"pageviews\"`\n\tSessions       int64     `db:\"sessions\"`\n\tBounceRate     float64   `db:\"bounce_rate\"`\n\tAvgDuration    float64   `db:\"avg_duration\"`\n\tKnownDurations int64     `db:\"known_durations\" json:\",omitempty\"`\n\tDate           time.Time `db:\"ts\" json:\",omitempty\"`\n}\n\nfunc (s *SiteStats) FormattedDuration() string {\n\treturn fmt.Sprintf(\"%d:%d\", int(s.AvgDuration/60.00), (int(s.AvgDuration) % 60))\n}\n\nfunc (s *SiteStats) HandlePageview(p *Pageview) {\n\ts.Pageviews += 1\n\n\tif p.Duration > 0.00 {\n\t\ts.KnownDurations += 1\n\t\ts.AvgDuration = s.AvgDuration + ((float64(p.Duration) - s.AvgDuration) * 1 / float64(s.KnownDurations))\n\t}\n\n\tif p.IsNewVisitor {\n\t\ts.Visitors += 1\n\t}\n\n\tif p.IsNewSession {\n\t\ts.Sessions += 1\n\n\t\tif p.IsBounce {\n\t\t\ts.BounceRate = ((float64(s.Sessions-1) * s.BounceRate) + 1) / (float64(s.Sessions))\n\t\t} else {\n\t\t\ts.BounceRate = ((float64(s.Sessions-1) * s.BounceRate) + 0) / (float64(s.Sessions))\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/models/site_stats_test.go",
    "content": "package models\n\nimport (\n\t\"testing\"\n)\n\nfunc TestSiteStatsFormattedDuration(t *testing.T) {\n\ts := SiteStats{\n\t\tAvgDuration: 100.00,\n\t}\n\te := \"1:40\"\n\tif v := s.FormattedDuration(); v != e {\n\t\tt.Errorf(\"FormattedDuration: expected %s, got %s\", e, v)\n\t}\n\n\ts.AvgDuration = 1040.22\n\te = \"17:20\"\n\tif v := s.FormattedDuration(); v != e {\n\t\tt.Errorf(\"FormattedDuration: expected %s, got %s\", e, v)\n\t}\n}\n\nfunc TestSiteStatsHandlePageview(t *testing.T) {\n\ts := SiteStats{}\n\tp1 := &Pageview{\n\t\tDuration:     100,\n\t\tIsBounce:     false,\n\t\tIsNewVisitor: true,\n\t\tIsNewSession: true,\n\t}\n\tp2 := &Pageview{\n\t\tDuration:     60,\n\t\tIsNewVisitor: false,\n\t\tIsNewSession: false,\n\t\tIsBounce:     true, // should have no effect because only new sessions can bounce\n\t}\n\tp3 := &Pageview{\n\t\tIsNewSession: true,\n\t\tIsBounce:     true,\n\t}\n\n\t// add first pageview & test\n\ts.HandlePageview(p1)\n\tif s.Pageviews != 1 {\n\t\tt.Errorf(\"Pageviews: expected %d, got %d\", 1, s.Pageviews)\n\t}\n\tif s.Visitors != 1 {\n\t\tt.Errorf(\"Visitors: expected %d, got %d\", 1, s.Visitors)\n\t}\n\tif s.AvgDuration != 100 {\n\t\tt.Errorf(\"AvgDuration: expected %.2f, got %.2f\", 100.00, s.AvgDuration)\n\t}\n\tif s.BounceRate != 0.00 {\n\t\tt.Errorf(\"BounceRate: expected %.2f, got %.2f\", 0.00, s.BounceRate)\n\t}\n\n\t// add second pageview\n\ts.HandlePageview(p2)\n\tif s.Pageviews != 2 {\n\t\tt.Errorf(\"Pageviews: expected %d, got %d\", 2, s.Pageviews)\n\t}\n\tif s.Visitors != 1 {\n\t\tt.Errorf(\"Visitors: expected %d, got %d\", 1, s.Visitors)\n\t}\n\tif s.AvgDuration != 80 {\n\t\tt.Errorf(\"AvgDuration: expected %.2f, got %.2f\", 80.00, s.AvgDuration)\n\t}\n\t// should still be 0.00 because p2 was not a new session\n\tif s.BounceRate != 0.00 {\n\t\tt.Errorf(\"BounceRate: expected %.2f, got %.2f\", 0.00, s.BounceRate)\n\t}\n\n\t// add third pageview\n\ts.HandlePageview(p3)\n\tif s.BounceRate != 0.50 {\n\t\tt.Errorf(\"BounceRate: expected %.2f, got %.2f\", 0.50, s.BounceRate)\n\t}\n\n}\n"
  },
  {
    "path": "pkg/models/user.go",
    "content": "package models\n\nimport (\n\t\"strings\"\n\n\t\"golang.org/x/crypto/bcrypt\"\n)\n\ntype User struct {\n\tID       int64\n\tEmail    string\n\tPassword string `json:\"-\"`\n}\n\n// NewUser creates a new User with the given email and password\nfunc NewUser(e string, pwd string) User {\n\tu := User{\n\t\tEmail: strings.ToLower(strings.TrimSpace(e)),\n\t}\n\tu.SetPassword(pwd)\n\treturn u\n}\n\n// SetPassword sets a brcrypt encrypted password from the given plaintext pwd\nfunc (u *User) SetPassword(pwd string) {\n\thash, _ := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost)\n\tu.Password = string(hash)\n}\n\n// ComparePassword returns true when the given plaintext password matches the encrypted pwd\nfunc (u *User) ComparePassword(pwd string) error {\n\treturn bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(pwd))\n}\n"
  },
  {
    "path": "pkg/models/user_test.go",
    "content": "package models\n\nimport (\n\t\"testing\"\n)\n\nfunc TestNewUser(t *testing.T) {\n\temail := \"foo@bar.com\"\n\tpwd := \"passw0rd01\"\n\tu := NewUser(email, pwd)\n\n\tif u.Email != email {\n\t\tt.Errorf(\"Email: expected %s, got %s\", email, u.Email)\n\t}\n\n\tif u.ComparePassword(pwd) != nil {\n\t\tt.Error(\"Password not set correctly\")\n\t}\n}\n\nfunc TestUserPassword(t *testing.T) {\n\tu := &User{}\n\tu.SetPassword(\"password\")\n\tif u.ComparePassword(\"password\") != nil {\n\t\tt.Errorf(\"Password should match, but does not\")\n\t}\n}\n"
  }
]