NNID Archiver Service (source code)
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys


def main():
    """Run administrative tasks."""
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'savemii.settings')
    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError(
            "Couldn't import Django. Are you sure it's installed and "
            "available on your PYTHONPATH environment variable? Did you "
            "forget to activate a virtual environment?"
        ) from exc
    execute_from_command_line(sys.argv)


if __name__ == '__main__':
    main()

manage.py
LetsShop3DS/SaveMii
from django.contrib import admin
from miiapp.models import *

# Register your models here.
admin.site.register(NintendoNetworkID)
admin.site.register(Favorite)
admin.site.register(Complaint)
admin.site.register(BlockedNNID)
admin.site.register(Ban)
miiapp/admin.py
LetsShop3DS/SaveMii
from django.apps import AppConfig


class MiiappConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'miiapp'

miiapp/apps.py
LetsShop3DS/SaveMii
"""
General purpose middleware.
"""
from savemii import settings
from miiapp.models import Ban
from django.http import HttpResponse, JsonResponse
from django.core.exceptions import ObjectDoesNotExist


class MiiAppMiddleware(object):
    def __init__(self, get_response):
        self.get_response = get_response
    def __call__(self, request):
        if settings.MAINTENANCE:
            if request.path.startswith("/api/") or request.path.startswith("/mii/"):
                return JsonResponse({"error": True, "message": "Service in maintenance."})
            else:
                return HttpResponse("Service in maintenance.")
        if request.user.is_authenticated:
            try:
                ban = Ban.objects.get(target=request.user)
                return HttpResponse("You've been banned by "+ban.source.username+" for the following reason: "+ban.reason)
            except ObjectDoesNotExist:
                pass
        if request.user_agent.is_mobile or request.user_agent.is_tablet:
            request.is_mobile = True
        else:
            request.is_mobile = False
        request.version = settings.VERSION
        response = self.get_response(request)
        return response
miiapp/middleware.py
LetsShop3DS/SaveMii
# Generated by Django 4.1.7 on 2023-10-04 14:22

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    initial = True

    dependencies = [
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
    ]

    operations = [
        migrations.CreateModel(
            name='NintendoNetworkID',
            fields=[
                ('id', models.IntegerField(blank=True, primary_key=True, serialize=False)),
                ('nnid', models.CharField(max_length=16, verbose_name='Nintendo Network ID')),
                ('mii_hash', models.CharField(max_length=13)),
                ('mii_data', models.TextField()),
                ('nickname', models.CharField(max_length=11)),
                ('pid', models.IntegerField()),
                ('rank', models.IntegerField()),
                ('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
            ],
        ),
        migrations.CreateModel(
            name='Favorite',
            fields=[
                ('id', models.IntegerField(blank=True, primary_key=True, serialize=False)),
                ('source', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
                ('target', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='miiapp.nintendonetworkid')),
            ],
        ),
        migrations.CreateModel(
            name='Complaint',
            fields=[
                ('id', models.IntegerField(blank=True, primary_key=True, serialize=False)),
                ('content', models.TextField()),
                ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
                ('target', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='miiapp.nintendonetworkid')),
            ],
        ),
    ]

miiapp/migrations/0001_initial.py
LetsShop3DS/SaveMii
# Generated by Django 4.1.7 on 2023-10-04 17:55

from django.db import migrations


class Migration(migrations.Migration):

    dependencies = [
        ('miiapp', '0001_initial'),
    ]

    operations = [
        migrations.AlterModelOptions(
            name='nintendonetworkid',
            options={'verbose_name': 'Nintendo Network ID', 'verbose_name_plural': 'Nintendo Network ID'},
        ),
    ]

miiapp/migrations/0002_alter_nintendonetworkid_options.py
LetsShop3DS/SaveMii
# Generated by Django 4.1.7 on 2023-10-07 10:58

import datetime
from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('miiapp', '0002_alter_nintendonetworkid_options'),
    ]

    operations = [
        migrations.AddField(
            model_name='nintendonetworkid',
            name='archived_on',
            field=models.DateTimeField(blank=True, default=datetime.datetime(2023, 10, 7, 10, 58, 38, 261385, tzinfo=datetime.timezone.utc)),
        ),
        migrations.AddField(
            model_name='nintendonetworkid',
            name='refreshed_on',
            field=models.DateTimeField(blank=True, default=datetime.datetime(2023, 10, 7, 10, 58, 38, 261490, tzinfo=datetime.timezone.utc)),
        ),
        migrations.AlterField(
            model_name='nintendonetworkid',
            name='mii_hash',
            field=models.CharField(max_length=13, unique=True),
        ),
        migrations.AlterField(
            model_name='nintendonetworkid',
            name='nnid',
            field=models.CharField(max_length=16, unique=True, verbose_name='Nintendo Network ID'),
        ),
        migrations.AlterField(
            model_name='nintendonetworkid',
            name='pid',
            field=models.IntegerField(unique=True),
        ),
    ]

miiapp/migrations/0003_nintendonetworkid_archived_on_and_more.py
LetsShop3DS/SaveMii
# Generated by Django 4.1.7 on 2023-10-07 14:06

import datetime
from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('miiapp', '0003_nintendonetworkid_archived_on_and_more'),
    ]

    operations = [
        migrations.CreateModel(
            name='BlockList',
            fields=[
                ('id', models.IntegerField(blank=True, primary_key=True, serialize=False)),
                ('nnid', models.CharField(max_length=16, unique=True, verbose_name='Nintendo Network ID')),
            ],
        ),
        migrations.AlterField(
            model_name='nintendonetworkid',
            name='archived_on',
            field=models.DateTimeField(blank=True, default=datetime.datetime(2023, 10, 7, 14, 6, 57, 953359, tzinfo=datetime.timezone.utc)),
        ),
        migrations.AlterField(
            model_name='nintendonetworkid',
            name='refreshed_on',
            field=models.DateTimeField(blank=True, default=datetime.datetime(2023, 10, 7, 14, 6, 57, 953391, tzinfo=datetime.timezone.utc)),
        ),
    ]

miiapp/migrations/0004_blocklist_alter_nintendonetworkid_archived_on_and_more.py
LetsShop3DS/SaveMii
# Generated by Django 4.1.7 on 2023-10-07 14:07

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('miiapp', '0004_blocklist_alter_nintendonetworkid_archived_on_and_more'),
    ]

    operations = [
        migrations.AlterField(
            model_name='nintendonetworkid',
            name='archived_on',
            field=models.DateTimeField(blank=True),
        ),
        migrations.AlterField(
            model_name='nintendonetworkid',
            name='refreshed_on',
            field=models.DateTimeField(blank=True),
        ),
    ]

miiapp/migrations/0005_alter_nintendonetworkid_archived_on_and_more.py
LetsShop3DS/SaveMii
# Generated by Django 4.1.7 on 2023-10-07 14:41

from django.db import migrations


class Migration(migrations.Migration):

    dependencies = [
        ('miiapp', '0005_alter_nintendonetworkid_archived_on_and_more'),
    ]

    operations = [
        migrations.RenameModel(
            old_name='BlockList',
            new_name='BlockedNNID',
        ),
        migrations.AlterModelOptions(
            name='blockednnid',
            options={'verbose_name': 'Blocked NNID', 'verbose_name_plural': 'Blocked NNID'},
        ),
    ]

miiapp/migrations/0006_rename_blocklist_blockednnid_and_more.py
LetsShop3DS/SaveMii
# Generated by Django 4.1.7 on 2023-10-07 15:49

import datetime
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    dependencies = [
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
        ('miiapp', '0006_rename_blocklist_blockednnid_and_more'),
    ]

    operations = [
        migrations.AddField(
            model_name='nintendonetworkid',
            name='refresher',
            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='refresher', to=settings.AUTH_USER_MODEL),
        ),
        migrations.AlterField(
            model_name='nintendonetworkid',
            name='archived_on',
            field=models.DateTimeField(blank=True, default=datetime.datetime(2023, 10, 7, 15, 49, 8, 471103, tzinfo=datetime.timezone.utc)),
        ),
        migrations.AlterField(
            model_name='nintendonetworkid',
            name='owner',
            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='owner', to=settings.AUTH_USER_MODEL),
        ),
        migrations.AlterField(
            model_name='nintendonetworkid',
            name='refreshed_on',
            field=models.DateTimeField(blank=True, default=datetime.datetime(2023, 10, 7, 15, 49, 8, 471135, tzinfo=datetime.timezone.utc)),
        ),
    ]

miiapp/migrations/0007_nintendonetworkid_refresher_and_more.py
LetsShop3DS/SaveMii
# Generated by Django 4.1.7 on 2023-10-07 16:30

import datetime
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    dependencies = [
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
        ('miiapp', '0007_nintendonetworkid_refresher_and_more'),
    ]

    operations = [
        migrations.AlterField(
            model_name='nintendonetworkid',
            name='archived_on',
            field=models.DateTimeField(blank=True, default=datetime.datetime(2023, 10, 7, 16, 30, 26, 811474, tzinfo=datetime.timezone.utc)),
        ),
        migrations.AlterField(
            model_name='nintendonetworkid',
            name='refreshed_on',
            field=models.DateTimeField(blank=True, default=datetime.datetime(2023, 10, 7, 16, 30, 26, 811502, tzinfo=datetime.timezone.utc)),
        ),
        migrations.CreateModel(
            name='Ban',
            fields=[
                ('id', models.IntegerField(blank=True, primary_key=True, serialize=False)),
                ('reason', models.TextField()),
                ('source', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='banner', to=settings.AUTH_USER_MODEL)),
                ('target', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='banned', to=settings.AUTH_USER_MODEL)),
            ],
        ),
    ]

miiapp/migrations/0008_alter_nintendonetworkid_archived_on_and_more.py
LetsShop3DS/SaveMii
"""
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣧⠀⠀⠀⠀⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣧⠀⠀⠀⢰⡿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⡟⡆⠀⠀⣿⡇⢻⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⠀⣿⠀⢰⣿⡇⢸⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⡄⢸⠀⢸⣿⡇⢸⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⣿⡇⢸⡄⠸⣿⡇⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢿⣿⢸⡅⠀⣿⢠⡏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⣿⣿⣥⣾⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⡿⡿⣿⣿⡿⡅⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠉⠀⠉⡙⢔⠛⣟⢋⠦⢵⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⣄⠀⠀⠁⣿⣯⡥⠃⠀⢳⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⡇⠀⠀⠀⠐⠠⠊⢀⠀⢸⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⣿⡿⠀⠀⠀⠀⠀⠈⠁⠀⠀⠘⣿⣄⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⣠⣿⣿⣿⣿⣿⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⣿⣷⡀⠀⠀⠀
⠀⠀⠀⠀⣾⣿⣿⣿⣿⣿⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⣿⣿⣧⠀⠀
⠀⠀⠀⡜⣭⠤⢍⣿⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⢛⢭⣗⠀
⠀⠀⠀⠁⠈⠀⠀⣀⠝⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠄⠠⠀⠀⠰⡅
⠀⠀⠀⢀⠀⠀⡀⠡⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠁⠔⠠⡕⠀
⠀⠀⠀⠀⣿⣷⣶⠒⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⠀⠀⠀⠀
⠀⠀⠀⠀⠘⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠰⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠈⢿⣿⣦⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠊⠉⢆⠀⠀⠀⠀
⠀⢀⠤⠀⠀⢤⣤⣽⣿⣿⣦⣀⢀⡠⢤⡤⠄⠀⠒⠀⠁⠀⠀⠀⢘⠔⠀⠀⠀⠀
⠀⠀⠀⡐⠈⠁⠈⠛⣛⠿⠟⠑⠈⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀you found the easter egg in savemii code
"""
from django.db import models
from django.contrib.auth.models import User
from django.utils.timezone import now

# nnid table
class NintendoNetworkID(models.Model):
    id = models.IntegerField(primary_key=True, blank=True)
    nnid = models.CharField(verbose_name="Nintendo Network ID", max_length=16, null=False, unique=True)
    mii_hash = models.CharField(max_length=13, null=False, unique=True)
    mii_data = models.TextField(null=False)
    nickname = models.CharField(max_length=11, null=False)
    pid = models.IntegerField(null=False, unique=True)
    rank = models.IntegerField(null=False, unique=True)
    owner = models.ForeignKey(User, on_delete=models.DO_NOTHING, null=True, blank=True, related_name="owner")
    refresher = models.ForeignKey(User, on_delete=models.DO_NOTHING, null=True, blank=True, related_name="refresher")
    archived_on = models.DateTimeField(null=False, blank=True, default=now())
    refreshed_on = models.DateTimeField(null=False, blank=True, default=now())
    class Meta:
        verbose_name = "Nintendo Network ID"
        verbose_name_plural = "Nintendo Network ID"
    def __str__(self):
        return self.nnid
    
# block list
class BlockedNNID(models.Model):
    id = models.IntegerField(primary_key=True, blank=True)
    nnid = models.CharField(verbose_name="Nintendo Network ID", max_length=16, null=False, unique=True)
    class Meta:
        verbose_name = "Blocked NNID"
        verbose_name_plural = "Blocked NNID"
    def __str__(self):
        return self.nnid

# favorite table, when the user favorites a nnid
class Favorite(models.Model):
    id = models.IntegerField(primary_key=True, blank=True)
    target = models.ForeignKey(NintendoNetworkID, on_delete=models.CASCADE, null=False)
    source = models.ForeignKey(User, on_delete=models.CASCADE, null=False)
    def __str__(self):
        return self.target.nnid+"favorited by "+self.source.username 

# complaints table, in case someone wants a nnid removed from the db
class Complaint(models.Model):
    id = models.IntegerField(primary_key=True, blank=True)
    target = models.ForeignKey(NintendoNetworkID, on_delete=models.CASCADE, null=False)
    content = models.TextField(null=False)
    author = models.ForeignKey(User, on_delete=models.CASCADE, null=False)
    def __str__(self):
        return "Complaint by "+self.author.username+" for "+self.target.nnid
    
# ban table
class Ban(models.Model):
    id = models.IntegerField(primary_key=True, blank=True)
    source = models.ForeignKey(User, on_delete=models.CASCADE, null=False, related_name="banner")
    target = models.ForeignKey(User, on_delete=models.CASCADE, null=False, related_name="banned")
    reason = models.TextField(null=False)
    def __str__(self):
        return self.target

miiapp/models.py
LetsShop3DS/SaveMii
from django.urls import path

from miiapp import views

urlpatterns = [
    path("", views.index, name="index"),
    path("signin", views.signin, name="signin"),
    path("signup", views.signup, name="signup"),
    path("privacy", views.privacy, name="privacy"),
    path("archive", views.archive, name="archive"),
    path("logout", views.disconnect, name="disconnect"),
    path("nnid/<str:nnid>", views.viewNNID, name="viewnnid"),
    path("random", views.randomNNID, name="randomnnid"),
    path("search", views.search, name="search"),
    path("owned", views.owned, name="owned"),
    path("api", views.api, name="api"),
    path("api/nnidinfo", views.getNNIDInfo, name="nnidinfo"),
    path("api/gethash", views.getHash, name="gethash"),
    path("mii/<str:hash>/<str:type>", views.miiImage, name="miiimage"),
    path("complaint/<str:nnid>", views.complaint, name="complaint"),
    path("miniadmin", views.miniadmin, name="miniadmin"),
    path("complaints", views.complaints, name="complaints"),
    path("delete", views.delete, name="delete"),
    path("refresh/<str:nnid>", views.refresh, name="refresh"),
    path("favorite/<str:nnid>", views.favorite, name="favorite"),
    path("favorites", views.favorites, name="favorites"),
    path("qna", views.qna, name="qna")
]
miiapp/urls.py
LetsShop3DS/SaveMii
from django.shortcuts import render
from miiapp.models import *
from django.core.exceptions import ObjectDoesNotExist
from django.contrib.auth import authenticate, login, logout
from django.http import JsonResponse, Http404, HttpResponseRedirect, HttpResponse
from requests import get
from json import loads
import os
import shutil
from savemii.settings import BASE_DIR
from django.utils.timezone import now
import xmltodict

print("Hi there! Checking if /archives/ exists....")
if os.path.isdir(str(BASE_DIR)+"/archives/"):
    print("It does!")
else:
    print("It doesn't. Creating directory.")
    os.mkdir(str(BASE_DIR)+"/archives/")
# This is the actual NNID archiver code. The website is a wrapper for that only function.
def nnidArchiver(nnid: str, user, refresh):
    if not refresh:
        print("Archiving "+nnid+"!")
    else:
        print("Refreshing "+nnid+"!")
    # had the nnid been archived already?
    try:
        conflict = NintendoNetworkID.objects.get(nnid=nnid)
        if not refresh:
            return "This NNID has already been archived. If the NNID has been changed since its archival, please use the 'Refresh NNID' button."
        else:
            if (now()-conflict.refreshed_on).days < 1:
                return "This NNID has been refreshed today. Please try again tomorrow."
    except ObjectDoesNotExist:
        pass
    try:
        is_blocked = BlockedNNID.objects.get(nnid=nnid)
        return "This NNID cannot be archived because it has been blocked."
    except ObjectDoesNotExist:
        pass

    # use the nintendo network api
        
    #default headers, according to https://github.com/kinnay/NintendoClients/wiki/Account-Server
    headers = {
        "X-Nintendo-Client-ID": "a2efa818a34fa16b8afbc8a74eba3eda",
        "X-Nintendo-Client-Secret": "c91cdb5658bd4954ade78533a339cf9a"
    }

    #get principal id (pid)
    url = "https://accountws.nintendo.net/v1/api/admin/mapped_ids"
    payload = {'input_type': 'user_id', 'output_type': 'pid', 'input': nnid}
    response = get(url, params=payload, headers=headers)
    pid = xmltodict.parse(response.text)['mapped_ids']['mapped_id']['out_id']

    #get mii data with pid
    url = "https://accountws.nintendo.net/v1/api/miis"
    payload = {'pids': pid}
    response = get(url, params=payload, headers=headers)
    api = xmltodict.parse(response.text)['miis']['mii']

    api['images']['hash'] = api["images"]["image"][0]["url"].split("/")[-1].split("_")[0]

    # if message (error) key exists, return it
    if "message" in api:
        return api['message']
    #else, continue with the archiving process
    #WARNING: POORLY WRITTEN CODE AHEAD!!
    # save mii images
    miis = api['images']['image']
    if not os.path.isdir(str(BASE_DIR)+"/archives/"+nnid):
        os.mkdir(str(BASE_DIR)+"/archives/"+nnid)
    for url in miis:
        #loop through every Mii URL in the json, then save
        image = get(url["url"], verify=False)
        #dirty fix for checking for the TGA format mii image
        if url['type'] == "standard":
            extension = ".tga"
        else:
            extension = ".png"
        file = open(str(BASE_DIR)+"/archives/"+nnid+"/"+url['type']+extension, "wb")
        file.write(image.content)
        file.close()
    if not user.is_authenticated:
        user = None
    if not refresh:
        NintendoNetworkID.objects.create(nnid=nnid, mii_hash=api['images']['hash'], mii_data=api['data'], nickname=api['name'], pid=api['pid'], rank=(1800000000 - int(api['pid'])), owner=user)
    else:
        conflict.mii_data = api['data']
        conflict.nickname = api['name']
        conflict.refreshed_on = now()
        conflict.refresher = user
        conflict.save()
    print("Archived/Refreshed "+nnid+"!")   


def index(request):
    count_of_nnid = NintendoNetworkID.objects.all().count()
    return render(request, 'index.html', {"title": "Home", "count": count_of_nnid})

def archive(request):
    if request.method == 'POST':
        if not request.POST.get('nnid'):
            return render(request, "archive.html", {"title": "Archive an NNID", "message": "You didn't put an NNID."})
        else:
            nnid = nnidArchiver(request.POST.get('nnid'), request.user, False)
            if nnid != None:
                return render(request, "archive.html", {"title": "Archive an NNID", "message": nnid})
            else:
                return render(request, "archive.html", {"title": "Archive an NNID", "success": True, "nnid": request.POST.get('nnid')})
    return render(request, 'archive.html', {"title": "Archive an NNID"})

def privacy(request):
    return render(request, 'privacy.html', {"title": "Privacy Policy"})

def qna(request):
    return render(request, 'qna.html', {"title": "QnA"})

def disconnect(request):
    if request.user.is_authenticated:
        logout(request)
        return HttpResponseRedirect("/")
    else:
        return HttpResponseRedirect("/")

def signin(request):
    if request.user.is_authenticated:
        return HttpResponseRedirect("/")
    if request.method == 'POST':
        if not request.POST.get('username') or not request.POST.get('password'): 
            return render(request, "signin.html", {"title": "Sign In", "message": "All fields weren't filled."})
        else:
            user = authenticate(username=request.POST.get("username"), password=request.POST.get("password"))
            if user == None:
                return render(request, "signin.html", {"title": "Sign In", "message": "The username/password is incorrect."})
            else:
                login(request, user)
                return HttpResponseRedirect("/")
    return render(request, 'signin.html', {"title": "Sign In"})

def signup(request):
    if request.user.is_authenticated:
        return HttpResponseRedirect("/")
    if request.method == 'POST':
        if not request.POST.get('username') or not request.POST.get('password') or not request.POST.get('cpassword'): 
            return render(request, "signup.html", {"title": "Sign Up", "message": "All fields weren't filled."})
        else:
            if request.POST.get("password") != request.POST.get("cpassword"):
                return render(request, "signup.html", {"title": "Sign Up", "message": "Passwords don't match."})
            if request.POST.get('username') == "Anonymous":
                return render(request, "signup.html", {"title": "Sign Up", "message": "You cannot use that name."})
            try:
                conflict = User.objects.get(username=request.POST.get("username"))
                return render(request, "signup.html", {"title": "Sign Up", "message": "A user with the same username already exists."})
            except ObjectDoesNotExist:
                pass
            user = User.objects.create_user(request.POST.get("username"), None, request.POST.get("password"))
            user.save()
            return HttpResponseRedirect("/signin")
    return render(request, 'signup.html', {"title": "Sign Up"})

def randomNNID(request):
    nnid = NintendoNetworkID.objects.order_by('?')[0]
    return HttpResponseRedirect("/nnid/"+nnid.nnid)

def favorite(request, nnid):
    if not request.user.is_authenticated:
        return HttpResponseRedirect("/")
    try:
        nnid_db = NintendoNetworkID.objects.get(nnid=nnid)
    except ObjectDoesNotExist:
        raise Http404()
    try:
        favorite = Favorite.objects.get(target=nnid_db, source=request.user)
        favorite.delete()
        return HttpResponseRedirect("/favorites")
    except ObjectDoesNotExist:
        Favorite.objects.create(target=nnid_db, source=request.user)
        return HttpResponseRedirect("/nnid/"+nnid_db.nnid)

def viewNNID(request, nnid):
    try:
        nnid_db = NintendoNetworkID.objects.get(nnid=nnid)
    except ObjectDoesNotExist:
        raise Http404()
    if request.user.is_authenticated:
        try:
            favorite = Favorite.objects.get(target=nnid_db, source=request.user)
            is_fav = True
        except ObjectDoesNotExist:
            is_fav = False
    else:
        is_fav = False
    if nnid_db.owner == None:
        owner = "Anonymous"
    else:
        owner = nnid_db.owner
    return render(request, "viewnnid.html", {"title": nnid, "nnid": nnid_db, "owner": owner, "is_fav": is_fav})

def search(request):
    if request.method == 'POST':
        if not request.POST.get('search'):
            return HttpResponseRedirect("/")
        nnids = NintendoNetworkID.objects.filter(nnid__icontains=request.POST.get('search')).order_by("?")
        return render(request, "search.html", {"title": "Search", "results": nnids, "query": request.POST.get('search')})
    else:
        return HttpResponseRedirect("/")
    
def complaint(request, nnid):
    if not request.user.is_authenticated:
        return HttpResponseRedirect("/")
    try:
        nnid = NintendoNetworkID.objects.get(nnid=nnid)
    except ObjectDoesNotExist:
        raise Http404()
    if request.method == 'POST':
        if not request.POST.get('complaint'):
            return render(request, "complaint.html", {"title": "Send a complaint", "message": "All fields weren't filled.", "nnid": nnid.nnid})
        Complaint.objects.create(target=nnid, content=request.POST.get('complaint'), author=request.user)
        return render(request, "complaint.html", {"title": "Send a complaint", "success": True, "nnid": nnid.nnid})
    return render(request, "complaint.html", {"title": "Send a complaint", "nnid": nnid.nnid})

def refresh(request, nnid):
    if not request.user.is_authenticated:
        return HttpResponseRedirect("/")
    result = nnidArchiver(nnid, request.user, True)
    if result != None:
        return render(request, "refresh.html", {"title": "Refresh "+nnid, "message": result, "nnid":nnid})
    else:
        return render(request, "refresh.html", {"title": "Refresh "+nnid, "success": True, "nnid":nnid})
    

def owned(request):
    if not request.user.is_authenticated:
        return HttpResponseRedirect("/")
    nnids = NintendoNetworkID.objects.filter(owner=request.user).order_by("?")
    return render(request, "owned.html", {"title": "NNIDs you've archived", "results": nnids})

def favorites(request):
    if not request.user.is_authenticated:
        return HttpResponseRedirect("/")
    nnids = Favorite.objects.filter(source=request.user).order_by("?")
    return render(request, "favorites.html", {"title": "NNIDs you've put as favorite", "results": nnids})

def miniadmin(request):
    if not request.user.is_authenticated:
        return HttpResponseRedirect("/")
    if not request.user.is_staff:
        return HttpResponseRedirect("/")
    return render(request, "admin/miniadmin.html", {"title": "Mini admin panel"})

def complaints(request):
    if not request.user.is_authenticated:
        return HttpResponseRedirect("/")
    if not request.user.is_staff:
        return HttpResponseRedirect("/")
    complaint = Complaint.objects.all()
    if request.method == 'POST':
        if not request.POST.get('complaint'):
            return HttpResponse("missing complaint id")
        try:
            selected = Complaint.objects.get(id=request.POST.get('complaint'))
        except ObjectDoesNotExist:
            return HttpResponse("complaint not found sorry")
        selected.delete()
    return render(request, "admin/complaints.html", {"title": "Complaints list", "complaints": complaint})

def delete(request):
    if not request.user.is_authenticated:
        return HttpResponseRedirect("/")
    if not request.user.is_staff:
        return HttpResponseRedirect("/")
    if request.method == 'POST':
        if not request.POST.get('nnid'):
            return HttpResponse("missing nnid")
        try:
            nnid = NintendoNetworkID.objects.get(nnid=request.POST.get('nnid'))
        except ObjectDoesNotExist:
            return HttpResponse("nnid not found sorry")
        if not os.path.isdir(str(BASE_DIR)+"/archives/"+nnid.nnid):
            pass
        else:
            shutil.rmtree(str(BASE_DIR)+"/archives/"+nnid.nnid)
        nnid.delete()
        if request.POST.get('blocklist'):
            BlockedNNID.objects.create(nnid=request.POST.get('nnid'))
        return HttpResponse("deleted")
    return render(request, "admin/delete.html", {"title": "Delete an NNID"})

def api(request):
    if not request.user.is_authenticated:
        return HttpResponseRedirect("/")
    return render(request, "api.html", {"title": "API"})

def miiImage(request, hash, type):
    try:
        nnid = NintendoNetworkID.objects.get(mii_hash=hash)
    except ObjectDoesNotExist:
        return JsonResponse({"error": True, "message": "NNID corresponding to Hash not found"}, status=404)
    if type=="standard":
        extension = ".tga"
        content_type = "image/x-tga"
    else:
        extension = ".png"
        content_type = "image/png"
    path = str(BASE_DIR)+"/archives/"+nnid.nnid+"/"+type+extension
    if not os.path.isfile(path):
        return JsonResponse({"error": True, "message": "Image not found"}, status=400)
    image = open(path, "rb")
    return HttpResponse(image.read(), content_type=content_type)

def getNNIDInfo(request):
    if not request.GET.get("nnid"):
        return JsonResponse({"error": False, "message": "Missing parameters"})
    try:
        nnid = NintendoNetworkID.objects.get(nnid=request.GET.get("nnid"))
    except ObjectDoesNotExist:
        return JsonResponse({"error": True, "message": "NNID not found/not archived"}, status=404)
    if nnid.owner == None:
        owner = None
    else:
        owner = nnid.owner.username
    if nnid.refresher == None:
        refresher = None
    else:
        refresher = nnid.refresher.username
    return JsonResponse({"error": False, "nnid": {"nnid": nnid.nnid, "mii": {"nickname": nnid.nickname, "hash": nnid.mii_hash, "data": nnid.mii_data}, "pid": nnid.pid, "rank": nnid.rank, "savemii": {"owner": owner, "refresher": refresher, "archived_on": nnid.archived_on, "refreshed_on": nnid.refreshed_on}}})

def getHash(request):
    if not request.GET.get("nnid"):
        return JsonResponse({"error": False, "message": "Missing parameters"})
    try:
        nnid = NintendoNetworkID.objects.get(nnid=request.GET.get("nnid"))
    except ObjectDoesNotExist:
        return JsonResponse({"error": True, "message": "NNID not found/not archived"}, status=404)
    return HttpResponse(nnid.mii_hash)

def err404(request, exception):
    return render(request, "404.html", {"title": "404"})

miiapp/views.py
LetsShop3DS/SaveMii
"""
ASGI config for miiarchiver project.

It exposes the ASGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/
"""

import os

from django.core.asgi import get_asgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'miiarchiver.settings')

application = get_asgi_application()

savemii/asgi.py
LetsShop3DS/SaveMii
"""
Django settings for miiarchiver project.

Generated by 'django-admin startproject' using Django 4.1.7.

For more information on this file, see
https://docs.djangoproject.com/en/4.1/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.1/ref/settings/
"""

from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret! (CHANGE THIS)
SECRET_KEY = 'django-insecure-i#a7fd8@glnoum2y7*q7fe46j#&zw)pan1sy$v2h#6tz(%up%t'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

MAINTENANCE = False

ALLOWED_HOSTS = ["insert allowed domain here"]

VERSION = "1.0.0b"

CSRF_TRUSTED_ORIGINS = ["insert url here"]


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'miiapp.apps.MiiappConfig',
    'django_user_agents'
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'django_user_agents.middleware.UserAgentMiddleware',
    'miiapp.middleware.MiiAppMiddleware',
]

ROOT_URLCONF = 'savemii.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'savemii.wsgi.application'


# Database
# https://docs.djangoproject.com/en/4.1/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
    }
}

# Name of cache backend to cache user agents. If it not specified default
# cache alias will be used. Set to `None` to disable caching.
USER_AGENTS_CACHE = 'default'


# Password validation
# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/4.1/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.1/howto/static-files/

STATIC_URL = 'static/'

# Default primary key field type
# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

savemii/settings.py
LetsShop3DS/SaveMii
"""miiarchiver URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/4.1/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include

handler404 = 'miiapp.views.err404'

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include("miiapp.urls"))
]

savemii/urls.py
LetsShop3DS/SaveMii
"""
WSGI config for miiarchiver project.

It exposes the WSGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/
"""

import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'miiarchiver.settings')

application = get_wsgi_application()

savemii/wsgi.py
LetsShop3DS/SaveMii