Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Jason Carpenter 2020-04-24 17:14:46 -04:00
commit 1a9d57ed0c
11 changed files with 73 additions and 54 deletions

View File

@ -1,6 +1,15 @@
<img src="images/logo.png" height="50" alt="Shynet logo">
Modern, privacy-friendly, and cookie-free web analytics. <p align="center">
<img align="center" src="images/logo.png" height="50" alt="Shynet logo">
<br>
<p align="center">
Modern, privacy-friendly, and cookie-free web analytics.
<br>
<strong><a href="#installation">Getting started »</a></strong>
</p>
<p align="center"><a href="#screenshots">Screenshots</a> &bull; <a href="#features">Features</a> &bull; <a href="#roadmap">Roadmap</a></p>
</p>
<br> <br>
@ -31,10 +40,10 @@ Not shown: service view, management view, session view, full service view. (You'
#### Architecture #### Architecture
* **Runs on a single machine** &mdash; Because it's so small, Shynet can easily run as a single docker container on a single small VPS. * **Runs on a single machine** &mdash; Because it's so small, Shynet can easily run as a single docker container on a single small VPS
* **...or across a giant Kubernetes cluster** &mdash; For higher traffic installations, Shynet can be deployed with as many parallelized ingress nodes as needed, with Redis caching and separate backend workers for database IO. * **...or across a giant Kubernetes cluster** &mdash; For higher traffic installations, Shynet can be deployed with as many parallelized ingress nodes as needed, with Redis caching and separate backend workers for database IO
* **Built using Django** &mdash; Shynet is built using Django, so deploying, updating, and migrating can be done without headaches. * **Built using Django** &mdash; Shynet is built using Django, so deploying, updating, and migrating can be done without headaches
* **Multiple users and sites** &mdash; A single Shynet instance can support multiple users, each tracking multiple different sites. * **Multiple users and sites** &mdash; A single Shynet instance can support multiple users, each tracking multiple different sites
#### Tracking #### Tracking
@ -61,7 +70,7 @@ Here's the information Shynet can give you about your visitors:
#### Workflow #### Workflow
* **Collaboration built-in** &mdash; Administrators can easily share services with other users, as well * **Collaboration built-in** &mdash; Administrators can easily share services with other users, as well
* **Accounts (or not)** &mdash; Shynet has a fully featured account management workflow (powered by [Django Allauth](https://github.com/pennersr/django-allauth/)). * **Accounts (or not)** &mdash; Shynet has a fully featured account management workflow (powered by [Django Allauth](https://github.com/pennersr/django-allauth/))
## Recommendations ## Recommendations
@ -79,13 +88,13 @@ Shynet is pretty simple, but there are a few key terms you need to know in order
## Installation ## Installation
You can find installation instructions in our [Getting Started Guide](GUIDE.md#installation). You can find installation instructions in the [Getting Started Guide](GUIDE.md#installation).
## FAQ ## FAQ
**Does Shynet respond to Do Not Track (DNT) signals?** Yes. While there isn't any standardized way to handle DNT requests, Shynet allows you to specify whether you want to collect any data from users with DNT enabled on a per-service basis. (By default, Shynet will _not_ collect any data from users who specify DNT.) **Does Shynet respond to Do Not Track (DNT) signals?** Yes. While there isn't any standardized way to handle DNT requests, Shynet allows you to specify whether you want to collect any data from users with DNT enabled on a per-service basis. (By default, Shynet will _not_ collect any data from users who specify DNT.)
**Is this GDPR compliant?** It also depends on how you use it. If you're worried about GDPR, you should talk to a lawyer about your particular data collection practices. I'm not a lawyer. (And this isn't legal advice.) **Is this GDPR compliant?** It depends on how you use it. If you're worried about GDPR, you should talk to a lawyer about your particular data collection practices. I'm not a lawyer. (And this isn't legal advice.)
## Roadmap ## Roadmap

View File

@ -42,7 +42,15 @@ def _geoip2_lookup(ip):
@shared_task @shared_task
def ingress_request( def ingress_request(
service_uuid, tracker, time, payload, ip, location, user_agent, dnt=False, identifier="" service_uuid,
tracker,
time,
payload,
ip,
location,
user_agent,
dnt=False,
identifier="",
): ):
try: try:
service = Service.objects.get(pk=service_uuid, status=Service.ACTIVE) service = Service.objects.get(pk=service_uuid, status=Service.ACTIVE)
@ -78,7 +86,8 @@ def ingress_request(
if ( if (
ua.is_bot ua.is_bot
or (ua.browser.family or "").strip().lower() == "googlebot" or (ua.browser.family or "").strip().lower() == "googlebot"
or (ua.device.family or ua.device.model or "").strip().lower() == "spider" or (ua.device.family or ua.device.model or "").strip().lower()
== "spider"
): ):
device_type = "ROBOT" device_type = "ROBOT"
elif ua.is_mobile: elif ua.is_mobile:

View File

@ -2,14 +2,15 @@ import base64
import json import json
from django.conf import settings from django.conf import settings
from django.core.cache import cache
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import render, reverse from django.shortcuts import render, reverse
from django.utils import timezone from django.utils import timezone
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.views.generic import TemplateView, View from django.views.generic import TemplateView, View
from django.core.cache import cache
from ipware import get_client_ip from ipware import get_client_ip
from core.models import Service from core.models import Service
from ..tasks import ingress_request from ..tasks import ingress_request

View File

@ -1,10 +1,12 @@
from django.core.management.base import BaseCommand, CommandError import traceback
import uuid
from django.conf import settings from django.conf import settings
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from core.models import User from django.core.management.base import BaseCommand, CommandError
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
import uuid
import traceback from core.models import User
class Command(BaseCommand): class Command(BaseCommand):
@ -12,8 +14,7 @@ class Command(BaseCommand):
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument(
"hostname", "hostname", type=str,
type=str,
) )
def handle(self, *args, **options): def handle(self, *args, **options):

View File

@ -1,10 +1,12 @@
from django.core.management.base import BaseCommand, CommandError import traceback
import uuid
from django.conf import settings from django.conf import settings
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from core.models import User from django.core.management.base import BaseCommand, CommandError
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
import uuid
import traceback from core.models import User
class Command(BaseCommand): class Command(BaseCommand):
@ -12,18 +14,13 @@ class Command(BaseCommand):
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument(
"email", "email", type=str,
type=str,
) )
def handle(self, *args, **options): def handle(self, *args, **options):
email = options.get("email") email = options.get("email")
password = get_random_string() password = get_random_string()
User.objects.create_superuser( User.objects.create_superuser(str(uuid.uuid4()), email=email, password=password)
str(uuid.uuid4()), email=email, password=password self.stdout.write(self.style.SUCCESS("Successfully created a Shynet superuser"))
)
self.stdout.write(
self.style.SUCCESS("Successfully created a Shynet superuser")
)
self.stdout.write(f"Email address: {email}") self.stdout.write(f"Email address: {email}")
self.stdout.write(f"Password: {password}") self.stdout.write(f"Password: {password}")

View File

@ -1,10 +1,12 @@
from django.core.management.base import BaseCommand, CommandError import traceback
import uuid
from django.conf import settings from django.conf import settings
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from core.models import User from django.core.management.base import BaseCommand, CommandError
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
import uuid
import traceback from core.models import User
class Command(BaseCommand): class Command(BaseCommand):
@ -12,8 +14,7 @@ class Command(BaseCommand):
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument(
"name", "name", type=str,
type=str,
) )
def handle(self, *args, **options): def handle(self, *args, **options):

View File

@ -6,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('core', '0002_auto_20200415_1742'), ("core", "0002_auto_20200415_1742"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='service', model_name="service",
name='respect_dnt', name="respect_dnt",
field=models.BooleanField(default=True), field=models.BooleanField(default=True),
), ),
] ]

View File

@ -1,8 +1,8 @@
from allauth.account.admin import EmailAddress
from django import forms from django import forms
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from core.models import Service, User from core.models import Service, User
from allauth.account.admin import EmailAddress
class ServiceForm(forms.ModelForm): class ServiceForm(forms.ModelForm):
@ -27,7 +27,10 @@ class ServiceForm(forms.ModelForm):
"respect_dnt": "Should visitors who have enabled <a href='https://en.wikipedia.org/wiki/Do_Not_Track'>Do Not Track</a> be excluded from all data?", "respect_dnt": "Should visitors who have enabled <a href='https://en.wikipedia.org/wiki/Do_Not_Track'>Do Not Track</a> be excluded from all data?",
} }
collaborators = forms.CharField(help_text="Which users should have read-only access to this service? (Comma separated list of emails.)", required=False) collaborators = forms.CharField(
help_text="Which users should have read-only access to this service? (Comma separated list of emails.)",
required=False,
)
def clean_collaborators(self): def clean_collaborators(self):
collaborators = [] collaborators = []
@ -35,7 +38,9 @@ class ServiceForm(forms.ModelForm):
email = collaborator_email.strip() email = collaborator_email.strip()
if email == "": if email == "":
continue continue
collaborator_email_linked = EmailAddress.objects.filter(email__iexact=email).first() collaborator_email_linked = EmailAddress.objects.filter(
email__iexact=email
).first()
if collaborator_email_linked is None: if collaborator_email_linked is None:
raise forms.ValidationError(f"Email '{email}' is not registered") raise forms.ValidationError(f"Email '{email}' is not registered")
collaborators.append(collaborator_email_linked.user) collaborators.append(collaborator_email_linked.user)

View File

@ -1,6 +1,6 @@
from datetime import datetime, time
from urllib.parse import urlparse from urllib.parse import urlparse
from datetime import time, datetime
from django.utils import timezone from django.utils import timezone

View File

@ -1,6 +1,7 @@
from urllib.parse import urlparse from urllib.parse import urlparse
import pycountry
import flag import flag
import pycountry
from django import template from django import template
from django.utils import timezone from django.utils import timezone
from django.utils.html import escape from django.utils.html import escape
@ -41,12 +42,7 @@ def country_name(isocode):
@register.simple_tag @register.simple_tag
def relative_stat_tone( def relative_stat_tone(
start, start, end, good="UP", good_classes=None, bad_classes=None, neutral_classes=None,
end,
good="UP",
good_classes=None,
bad_classes=None,
neutral_classes=None,
): ):
good_classes = good_classes or "~positive" good_classes = good_classes or "~positive"
bad_classes = bad_classes or "~critical" bad_classes = bad_classes or "~critical"

View File

@ -1,5 +1,6 @@
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.core.cache import cache
from django.db.models import Q from django.db.models import Q
from django.shortcuts import get_object_or_404, reverse from django.shortcuts import get_object_or_404, reverse
from django.utils import timezone from django.utils import timezone
@ -12,7 +13,6 @@ from django.views.generic import (
UpdateView, UpdateView,
) )
from rules.contrib.views import PermissionRequiredMixin from rules.contrib.views import PermissionRequiredMixin
from django.core.cache import cache
from analytics.models import Session from analytics.models import Session
from core.models import Service from core.models import Service