diff --git a/geeksbot/__main__.py b/geeksbot/__main__.py index 07ce153..0f1c31d 100644 --- a/geeksbot/__main__.py +++ b/geeksbot/__main__.py @@ -78,6 +78,7 @@ class Geeksbot(commands.Bot): self.config_dir = 'geeksbot/config/' self.config_file = 'bot_config.json' self.extension_dir = 'exts' + self.api_token = os.environ['API_TOKEN'] self.aio_session = aiohttp.ClientSession(loop=self.loop) with open(f'{self.config_dir}{self.config_file}') as f: self.bot_config = json.load(f) @@ -90,6 +91,16 @@ class Geeksbot(commands.Bot): self.git_url = 'https://github.com/dustinpianalto/geeksbot_v2' self.load_default_extensions() + self.book_emojis = { + 'unlock': '🔓', + 'start': '⏮', + 'back': '◀', + 'hash': '#\N{COMBINING ENCLOSING KEYCAP}', + 'forward': '▶', + 'end': '⏭', + 'close': '🇽', + } + async def load_ext(self, mod): self.load_extension(f'geeksbot.{self.extension_dir}.{mod}') logger.info(f'Extension Loaded: {mod}') diff --git a/geeksbot/exts/exec.py b/geeksbot/exts/exec.py index 9ba442a..228062a 100644 --- a/geeksbot/exts/exec.py +++ b/geeksbot/exts/exec.py @@ -64,9 +64,9 @@ class Exec(commands.Cog): ret = await func() except Exception: pag.add(stdout.getvalue()) - pag.add(traceback.format_exc()) - for page in pag.pages(): - await ctx.send(page) + pag.add(f'\n\uFFF8{traceback.format_exc()}') + book = Book(pag, (None, ctx.channel, ctx.bot, ctx.message)) + await book.create_book() else: value = stdout.getvalue() # noinspection PyBroadException @@ -76,10 +76,10 @@ class Exec(commands.Cog): pass value = format_output(value) pag.add(value) - pag.add(f'\nReturned: {ret}') + pag.add(f'\n\uFFF8Returned: {ret}') self._last_result = ret - for page in pag.pages(): - await ctx.send(page) + book = Book(pag, (None, ctx.channel, ctx.bot, ctx.message)) + await book.create_book() @commands.command(hidden=True) async def repl(self, ctx): @@ -161,8 +161,8 @@ class Exec(commands.Cog): body = self.cleanup_code(body) pag = Paginator(self.bot) pag.add(await asyncio.wait_for(self.bot.loop.create_task(run_command(body)), 120)) - for page in pag.pages(): - await ctx.send(page) + book = Book(pag, (None, ctx.channel, ctx.bot, ctx.message)) + await book.create_book() await ctx.message.add_reaction('✅') except asyncio.TimeoutError: await ctx.send(f"Command did not complete in the time allowed.") diff --git a/geeksbot/imports/utils.py b/geeksbot/imports/utils.py index 7926b94..531451d 100644 --- a/geeksbot/imports/utils.py +++ b/geeksbot/imports/utils.py @@ -156,7 +156,7 @@ class Paginator: if not self._embed: for part in [str(p) for p in self._parts]: - if part == self._page_break: + if part.startswith(self._page_break): close_page() new_chars = len(_page) + len(part) @@ -185,9 +185,8 @@ class Paginator: open_field('\uFFF0') for part in [str(p) for p in self._parts]: - if part.strip() == self._page_break: + if part.strip().startswith(self._page_break): close_page() - continue elif part == self._field_break: if len(_fields) + 1 < 25: close_field(next_name='\uFFF0') diff --git a/geeksbot_v2/channels/__init__.py b/geeksbot_v2/channels/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/geeksbot_v2/channels/admin.py b/geeksbot_v2/channels/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/geeksbot_v2/channels/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/geeksbot_v2/channels/apps.py b/geeksbot_v2/channels/apps.py new file mode 100644 index 0000000..b1b71ee --- /dev/null +++ b/geeksbot_v2/channels/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ChannelsConfig(AppConfig): + name = 'channels' diff --git a/geeksbot_v2/channels/migrations/__init__.py b/geeksbot_v2/channels/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/geeksbot_v2/channels/models.py b/geeksbot_v2/channels/models.py new file mode 100644 index 0000000..225b703 --- /dev/null +++ b/geeksbot_v2/channels/models.py @@ -0,0 +1,46 @@ +from django.db import models +from django.core.exceptions import ObjectDoesNotExist +from rest_framework import status + +from geeksbot_v2.guilds.models import Guild +from .utils import create_error_response +from .utils import create_success_response + +# Create your models here. + + +class Channel(models.Model): + id = models.CharField(max_length=30, primary_key=True) + guild = models.ForeignKey(Guild, on_delete=models.CASCADE) + + @classmethod + def add_new_channel(cls, data): + id = data.get('id') + if id and cls.get_channel_by_id(id): + return create_error_response('Channel Already Exists', + status=status.HTTP_409_CONFLICT) + guild_id = data.get('guild') + if not (id and guild_id): + return create_error_response('Id and Guild are required', + status=status.HTTP_400_BAD_REQUEST) + guild = Guild.get_guild_by_id(guild_id) + if not isinstance(guild, Guild): + return create_error_response('Guild Does Not Exist', + status=status.HTTP_404_NOT_FOUND) + + channel = cls( + id=id, + guild=guild + ) + channel.save() + return create_success_response(channel, status.HTTP_201_CREATED, many=False) + + @classmethod + def get_channel_by_id(cls, id): + try: + return cls.objects.get(id=id) + except ObjectDoesNotExist: + return None + + def __str__(self): + return str(id) diff --git a/geeksbot_v2/channels/serializers.py b/geeksbot_v2/channels/serializers.py new file mode 100644 index 0000000..81af17b --- /dev/null +++ b/geeksbot_v2/channels/serializers.py @@ -0,0 +1,9 @@ +from rest_framework import serializers + +from .models import Channel + + +class ChannelSerializer(serializers.ModelSerializer): + class Meta: + model = Channel + fields = "__all__" diff --git a/geeksbot_v2/channels/tests.py b/geeksbot_v2/channels/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/geeksbot_v2/channels/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/geeksbot_v2/channels/utils.py b/geeksbot_v2/channels/utils.py new file mode 100644 index 0000000..11fe383 --- /dev/null +++ b/geeksbot_v2/channels/utils.py @@ -0,0 +1,14 @@ +from rest_framework.response import Response +from rest_framework import status + + +def create_error_response(msg, status=status.HTTP_404_NOT_FOUND): + return Response({'details': msg}, + status=status) + + +def create_success_response(channel_data, status, many: bool = False): + from .serializers import ChannelSerializer + + return Response(ChannelSerializer(channel_data, many=many).data, + status=status) diff --git a/geeksbot_v2/channels/views.py b/geeksbot_v2/channels/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/geeksbot_v2/channels/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/geeksbot_v2/config/settings/base.py b/geeksbot_v2/config/settings/base.py index d7c1cba..9e41752 100644 --- a/geeksbot_v2/config/settings/base.py +++ b/geeksbot_v2/config/settings/base.py @@ -43,6 +43,7 @@ LOCALE_PATHS = [ROOT_DIR.path("locale")] # https://docs.djangoproject.com/en/dev/ref/settings/#databases DATABASES = {"default": env.db("DATABASE_URL")} DATABASES["default"]["ATOMIC_REQUESTS"] = True +DATABASES['default']['CONN_MAX_AGE'] = 0 # URLS # ------------------------------------------------------------------------------ @@ -263,6 +264,9 @@ ACCOUNT_EMAIL_VERIFICATION = "optional" ACCOUNT_ADAPTER = "geeksbot_v2.users.adapters.AccountAdapter" # https://django-allauth.readthedocs.io/en/latest/configuration.html SOCIALACCOUNT_ADAPTER = "geeksbot_v2.users.adapters.SocialAccountAdapter" +ACCOUNT_FORMS = { + 'signup': 'geeksbot_v2.users.forms.UserCreateForm', +} # Your stuff... @@ -275,6 +279,9 @@ REST_FRAMEWORK = { "DEFAULT_RENDERER_CLASSES": [ "rest_framework.renderers.JSONRenderer", ], + "DEFAULT_PARSER_CLASSES": [ + 'rest_framework.parsers.JSONParser', + ], "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination", "PAGE_SIZE": 100, } diff --git a/geeksbot_v2/contrib/sites/migrations/0001_initial.py b/geeksbot_v2/contrib/sites/migrations/0001_initial.py index 304cd6d..c0c8906 100644 --- a/geeksbot_v2/contrib/sites/migrations/0001_initial.py +++ b/geeksbot_v2/contrib/sites/migrations/0001_initial.py @@ -39,4 +39,4 @@ class Migration(migrations.Migration): bases=(models.Model,), managers=[("objects", django.contrib.sites.models.SiteManager())], ) - ] + ] \ No newline at end of file diff --git a/geeksbot_v2/contrib/sites/migrations/0002_alter_domain_unique.py b/geeksbot_v2/contrib/sites/migrations/0002_alter_domain_unique.py index 2c8d6da..2fcbc4e 100644 --- a/geeksbot_v2/contrib/sites/migrations/0002_alter_domain_unique.py +++ b/geeksbot_v2/contrib/sites/migrations/0002_alter_domain_unique.py @@ -17,4 +17,4 @@ class Migration(migrations.Migration): verbose_name="domain name", ), ) - ] + ] \ No newline at end of file diff --git a/geeksbot_v2/contrib/sites/migrations/0003_set_site_domain_and_name.py b/geeksbot_v2/contrib/sites/migrations/0003_set_site_domain_and_name.py index d77c8d0..93aab61 100644 --- a/geeksbot_v2/contrib/sites/migrations/0003_set_site_domain_and_name.py +++ b/geeksbot_v2/contrib/sites/migrations/0003_set_site_domain_and_name.py @@ -1,6 +1,5 @@ """ To understand why this file is here, please read: - http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django """ from django.conf import settings @@ -31,4 +30,4 @@ class Migration(migrations.Migration): dependencies = [("sites", "0002_alter_domain_unique")] - operations = [migrations.RunPython(update_site_forward, update_site_backward)] + operations = [migrations.RunPython(update_site_forward, update_site_backward)] \ No newline at end of file diff --git a/geeksbot_v2/dmessages/migrations/0001_initial.py b/geeksbot_v2/dmessages/migrations/0001_initial.py index a0b9602..1927f12 100644 --- a/geeksbot_v2/dmessages/migrations/0001_initial.py +++ b/geeksbot_v2/dmessages/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2.4 on 2019-09-16 05:23 +# Generated by Django 2.2.4 on 2019-09-17 19:31 from django.conf import settings import django.contrib.postgres.fields @@ -11,8 +11,8 @@ class Migration(migrations.Migration): initial = True dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('guilds', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ diff --git a/geeksbot_v2/dmessages/models.py b/geeksbot_v2/dmessages/models.py index 6b23008..8d49192 100644 --- a/geeksbot_v2/dmessages/models.py +++ b/geeksbot_v2/dmessages/models.py @@ -1,8 +1,18 @@ +from datetime import datetime + from django.db import models +from django.core.exceptions import ObjectDoesNotExist from django.contrib.postgres.fields import ArrayField +from rest_framework import status from geeksbot_v2.guilds.models import Guild +from geeksbot_v2.guilds.models import Role from geeksbot_v2.users.models import User +from geeksbot_v2.channels.models import Channel +from .utils import create_error_response +from .utils import create_success_response +from .utils import create_request_success_response +from .utils import create_comment_success_response # Create your models here. @@ -11,18 +21,124 @@ class Message(models.Model): id = models.CharField(max_length=30, primary_key=True) author = models.ForeignKey(User, on_delete=models.CASCADE) guild = models.ForeignKey(Guild, on_delete=models.CASCADE) - channel = models.CharField(max_length=30) + channel = models.ForeignKey(Channel, on_delete=models.CASCADE) created_at = models.DateTimeField() - modified_at = models.DateTimeField(null=True) - deleted_at = models.DateTimeField(null=True) + modified_at = models.DateTimeField(null=True, blank=True) + deleted_at = models.DateTimeField(null=True, blank=True) content = models.CharField(max_length=2000) - previous_content = ArrayField(models.CharField(max_length=2000)) - tagged_users = ArrayField(models.CharField(max_length=30)) - tagged_channels = ArrayField(models.CharField(max_length=30)) - tagged_roles = ArrayField(models.CharField(max_length=30)) + previous_content = ArrayField(models.CharField(max_length=2000), default=[]) + tagged_users = models.ManyToManyField(User) + tagged_channels = models.ManyToManyField(Channel) + tagged_roles = models.ManyToManyField(Role) tagged_everyone = models.BooleanField() - embeds = ArrayField(models.TextField()) - previous_embeds = ArrayField(ArrayField(models.TextField())) + embeds = ArrayField(models.TextField(), default=[]) + previous_embeds = ArrayField(ArrayField(models.TextField()), default=[]) + + @classmethod + def add_new_message(cls, data): + id = data.get('id') + if id and cls.get_message_by_id(id): + return create_error_response("Message Already Exists", + status=status.HTTP_409_CONFLICT) + author_id = data.get('author') + guild_id = data.get('guild') + channel_id = data.get('channel') + created_at = data.get('created_at') + content = data.get('content') + tagged_everyone = data.get('tagged_everyone') + if not (id and author_id and guild_id and channel_id and created_at and content and tagged_everyone): + return create_error_response("One or more required fields are missing.", + status=status.HTTP_400_BAD_REQUEST) + author = User.get_user_by_id(author_id) + if not isinstance(author, User): + return create_error_response("Author Does Not Exist", + status=status.HTTP_404_NOT_FOUND) + guild = Guild.get_guild_by_id(guild_id) + if not isinstance(guild, Guild): + return create_error_response("Guild Does Not Exist", + status=status.HTTP_404_NOT_FOUND) + channel = Channel.get_channel_by_id(channel_id) + if not isinstance(channel, Channel): + return create_error_response("Channel Does Not Exist", + status=status.HTTP_404_NOT_FOUND) + created_at = datetime.fromtimestamp(created_at) + + message = cls( + id=id, + author=author, + guild=guild, + channel=channel, + created_at=created_at, + tagged_everyone=tagged_everyone or False, + content=content or '', + embeds=data.get('embeds') or [] + ) + message.save() + if data.get('tagged_users'): + tagged_users = data.get('tagged_users') + for user_id in tagged_users: + user = User.get_user_by_id(user_id) + if user: + message.tagged_users.add(user) + if data.get('tagged_roles'): + tagged_roles = data.get('tagged_roles') + for role_id in tagged_roles: + role = Role.get_role_by_id(role_id) + if role: + message.tagged_roles.add(role) + if data.get('tagged_channels'): + tagged_channels = data.get('tagged_channels') + for channel_id in tagged_channels: + channel = Channel.get_channel_by_id(channel_id) + if channel: + message.tagged_channels.add(channel) + + return create_success_response(message, status.HTTP_201_CREATED, many=False) + + def update_message(self, data): + if data.get('modified_at'): + self.modified_at = data.get('modified_at') + if data.get('deleted_at'): + self.modified_at = data.get('deleted_at') + if data.get('content'): + content = data.get('content') + if content != self.content: + self.previous_content.append(self.content) + self.content = content + if data.get('embeds'): + embeds = data.get('embeds') + if embeds != self.embeds: + self.previous_embeds.append(self.embeds) + self.embeds = embeds + if data.get('tagged_everyone'): + tagged_everyone = data.get('tagged_everyone') + if self.tagged_everyone or tagged_everyone: + self.tagged_everyone = True + if data.get('tagged_users'): + tagged_users = data.get('tagged_users') + for user in tagged_users: + if user not in self.tagged_users: + self.tagged_users.append(user) + if data.get('tagged_roles'): + tagged_roles = data.get('tagged_roles') + for role in tagged_roles: + if role not in self.tagged_roles: + self.tagged_roles.append(role) + if data.get('tagged_channels'): + tagged_channels = data.get('tagged_channels') + for channel in tagged_channels: + if channel not in self.tagged_channels: + self.tagged_channels.append(channel) + + self.save() + return create_success_response(self, status.HTTP_202_ACCEPTED, many=False) + + @classmethod + def get_message_by_id(cls, id): + try: + return cls.objects.get(id=id) + except ObjectDoesNotExist: + return None def __str__(self): return (f'{self.created_at} | ' @@ -48,11 +164,113 @@ class GuildInfo(models.Model): class AdminRequest(models.Model): guild = models.ForeignKey(Guild, on_delete=models.CASCADE) - author = models.ForeignKey(User, on_delete=models.CASCADE) - message = models.ForeignKey(Message, on_delete=models.CASCADE) - completed = models.BooleanField() - requested_at = models.DateTimeField() - completed_at = models.DateTimeField() + author = models.ForeignKey(User, on_delete=models.DO_NOTHING) + message = models.ForeignKey(Message, on_delete=models.DO_NOTHING) + channel = models.ForeignKey(Channel, on_delete=models.DO_NOTHING) + completed = models.BooleanField(default=False) + requested_at = models.DateTimeField(auto_now_add=True, blank=True) + completed_at = models.DateTimeField(null=True, blank=True, default=None) + completed_by = models.ForeignKey(User, on_delete=models.DO_NOTHING, null=True, blank=True, default=None) + completed_message = models.CharField(max_length=1000, null=True, blank=True, default=None) + content = models.CharField(max_length=2000) + + def update_request(self, data): + completed = data.get('completed', False) + completed_by_id = data.get('completed_by') + completed_message = data.get('message') + if not self.completed and completed: + self.completed_at = datetime.now() + self.completed_message = completed_message + user = User.get_user_by_id(completed_by_id) + if not isinstance(user, User): + return create_error_response('User Does Not Exist', + status=status.HTTP_404_NOT_FOUND) + self.completed_by = user + self.save() + return create_request_success_response(self, status.HTTP_202_ACCEPTED) + + @classmethod + def add_new_request(cls, data): + guild_id = data.get('guild') + author_id = data.get('author') + message_id = data.get('message') + channel_id = data.get('channel') + content = data.get('content') + if not (guild_id and author_id and message_id and channel_id and content): + return create_error_response("One or more of the required fields are missing.", + status=status.HTTP_400_BAD_REQUEST) + guild = Guild.get_guild_by_id(guild_id) + if not isinstance(guild, Guild): + return create_error_response('Guild Does Not Exist', + status=status.HTTP_404_NOT_FOUND) + author = User.get_author_by_id(author_id) + if not isinstance(author, User): + return create_error_response('Author Does Not Exist', + status=status.HTTP_404_NOT_FOUND) + message = Message.get_message_by_id(message_id) + if not isinstance(message, Message): + return create_error_response('Message Does Not Exist', + status=status.HTTP_404_NOT_FOUND) + channel = Channel.get_channel_by_id(channel_id) + if not isinstance(channel, Channel): + return create_error_response('Channel Does Not Exist', + status=status.HTTP_404_NOT_FOUND) + + request = cls( + guild=guild, + author=author, + message=message, + channel=channel, + content=content + ) + request.save() + return create_request_success_response(request, status.HTTP_201_CREATED, many=False) + + @classmethod + def get_request_by_id(cls, id): + try: + return cls.objects.get(id=id) + except ObjectDoesNotExist: + return None def __str__(self): return f"{self.guild.id} | {self.requested_at} | By {self.author.id}" + + +class AdminComment(models.Model): + request = models.ForeignKey(AdminRequest, on_delete=models.CASCADE) + author = models.ForeignKey(User, on_delete=models.DO_NOTHING) + content = models.CharField(max_length=1000) + updated_at = models.DateTimeField(auto_now_add=True, blank=True) + + @classmethod + def add_new_comment(cls, data): + request_id = data.get('request') + author_id = data.get('author') + content = data.get('content') + if not (request_id and author_id and content): + return create_error_response('Request, Author, and Content are required fields', + status=status.HTTP_400_BAD_REQUEST) + request = AdminRequest.get_request_by_id(request_id) + if not isinstance(request, AdminRequest): + return create_error_response("Admin Request Does Not Exist", + status=status.HTTP_404_NOT_FOUND) + author = User.get_user_by_id(author_id) + if not isinstance(author, User): + return create_error_response("Author Does Not Exist", + status=status.HTTP_404_NOT_FOUND) + + comment = cls( + request=request, + author=author, + content=content + ) + comment.save() + return create_comment_success_response(comment, status.HTTP_201_CREATED, many=False) + + @classmethod + def get_comment_by_id(cls, id): + try: + return cls.objects.get(id=id) + except ObjectDoesNotExist: + return None diff --git a/geeksbot_v2/dmessages/serializers.py b/geeksbot_v2/dmessages/serializers.py index 52bd27b..082cb25 100644 --- a/geeksbot_v2/dmessages/serializers.py +++ b/geeksbot_v2/dmessages/serializers.py @@ -3,6 +3,7 @@ from rest_framework import serializers from .models import Message from .models import GuildInfo from .models import AdminRequest +from .models import AdminComment class MessageSerializer(serializers.ModelSerializer): @@ -10,6 +11,7 @@ class MessageSerializer(serializers.ModelSerializer): model = Message fields = "__all__" + class GuildInfoSerializer(serializers.ModelSerializer): class Meta: model = GuildInfo @@ -20,3 +22,9 @@ class AdminRequestSerializer(serializers.ModelSerializer): class Meta: model = AdminRequest fields = "__all__" + + +class AdminCommentSerializer(serializers.ModelSerializer): + class Meta: + model = AdminComment + fields = "__all__" diff --git a/geeksbot_v2/dmessages/utils.py b/geeksbot_v2/dmessages/utils.py new file mode 100644 index 0000000..ab5c646 --- /dev/null +++ b/geeksbot_v2/dmessages/utils.py @@ -0,0 +1,28 @@ +from rest_framework.response import Response +from rest_framework import status + + +def create_error_response(msg, status=status.HTTP_404_NOT_FOUND): + return Response({'details': msg}, + status=status) + + +def create_success_response(message_data, status, many: bool = False): + from .serializers import MessageSerializer + + return Response(MessageSerializer(message_data, many=many).data, + status=status) + + +def create_request_success_response(request_data, status, many: bool = False): + from .serializers import AdminRequestSerializer + + return Response(AdminRequestSerializer(request_data, many=many).data, + status=status) + + +def create_comment_success_response(comment_data, status, many: bool = False): + from .serializers import AdminCommentSerializer + + return Response(AdminCommentSerializer(comment_data, many=many).data, + status=status) diff --git a/geeksbot_v2/guilds/api_urls.py b/geeksbot_v2/guilds/api_urls.py index dc65d71..ee138fa 100644 --- a/geeksbot_v2/guilds/api_urls.py +++ b/geeksbot_v2/guilds/api_urls.py @@ -1,8 +1,9 @@ from django.urls import path -from .views import GuildsAPI +from .views import GuildsAPI, GuildDetail app_name = "users_api" urlpatterns = [ - path("/", view=GuildsAPI.as_view(), name="list") + path("/", view=GuildsAPI.as_view(), name="list"), + path("//", view=GuildDetail.as_view(), name='detail') ] diff --git a/geeksbot_v2/guilds/migrations/0001_initial.py b/geeksbot_v2/guilds/migrations/0001_initial.py index c66c8a4..103c8fd 100644 --- a/geeksbot_v2/guilds/migrations/0001_initial.py +++ b/geeksbot_v2/guilds/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2.4 on 2019-09-16 05:23 +# Generated by Django 2.2.4 on 2019-09-17 19:31 import django.contrib.postgres.fields from django.db import migrations, models @@ -17,10 +17,10 @@ class Migration(migrations.Migration): name='Guild', fields=[ ('id', models.CharField(max_length=30, primary_key=True, serialize=False)), - ('admin_chat', models.CharField(max_length=30)), - ('new_patron_message', models.TextField(blank=True, max_length=1000)), + ('admin_chat', models.CharField(blank=True, max_length=30, null=True)), + ('new_patron_message', models.TextField(blank=True, max_length=1000, null=True)), ('default_channel', models.CharField(max_length=30)), - ('new_patron_channel', models.CharField(max_length=30)), + ('new_patron_channel', models.CharField(blank=True, max_length=30, null=True)), ('prefixes', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=10), size=None)), ], ), diff --git a/geeksbot_v2/guilds/migrations/0002_auto_20190917_0508.py b/geeksbot_v2/guilds/migrations/0002_auto_20190917_0508.py deleted file mode 100644 index 60272ae..0000000 --- a/geeksbot_v2/guilds/migrations/0002_auto_20190917_0508.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 2.2.4 on 2019-09-17 05:08 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('guilds', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='guild', - name='admin_chat', - field=models.CharField(blank=True, max_length=30, null=True), - ), - migrations.AlterField( - model_name='guild', - name='new_patron_channel', - field=models.CharField(blank=True, max_length=30, null=True), - ), - migrations.AlterField( - model_name='guild', - name='new_patron_message', - field=models.TextField(blank=True, max_length=1000, null=True), - ), - ] diff --git a/geeksbot_v2/guilds/models.py b/geeksbot_v2/guilds/models.py index b954330..849c003 100644 --- a/geeksbot_v2/guilds/models.py +++ b/geeksbot_v2/guilds/models.py @@ -1,5 +1,14 @@ +import os + from django.db import models from django.contrib.postgres.fields import ArrayField +from django.core.exceptions import ObjectDoesNotExist +from rest_framework import status + +from .utils import create_error_response +from .utils import create_success_response +from .utils import create_role_success_response + # Create your models here. @@ -15,11 +24,113 @@ class Guild(models.Model): def __str__(self): return self.id + def update_guild(self, data): + if data.get('admin_chat'): + self.admin_chat = data.get('admin_chat') + if data.get('new_patron_message'): + self.new_patron_message = data.get('new_patron_message') + if data.get('default_channel'): + self.default_channel = data.get('default_channel') + if data.get('new_patron_channel'): + self.new_patron_channel = data.get('new_patron_channel') + if data.get('add_prefix'): + if data.get('add_prefix') not in self.prefixes: + self.prefixes.append(data.get('add_prefix')) + if data.get('remove_prefix'): + if data.get('remove_prefix') in self.prefixes: + self.prefixes.remove(data.get('remove_prefix')) + if len(self.prefixes) <= 0: + self.prefixes = [os.environ['DISCORD_DEFAULT_PREFIX'], ] + + self.save() + return self + + @classmethod + def get_guild_by_id(cls, id): + try: + return cls.objects.get(id=id) + except ObjectDoesNotExist: + return None + + @classmethod + def create_guild(cls, data): + id = data.get('id') + default_channel = data.get('default_channel') + if not (id and default_channel): + return create_error_response('id and default_channel are required', + status=status.HTTP_400_BAD_REQUEST) + + if cls.get_guild_by_id(id): + return create_error_response('That Guild already exists', + status.HTTP_409_CONFLICT) + + guild = cls( + id=id, + default_channel=default_channel, + prefixes=data.get('prefixes'), + admin_chat=data.get('admin_chat'), + new_patron_message=data.get('new_patron_message'), + new_patron_channel=data.get('new_patron_channel') + ) + guild.save() + return create_success_response(guild, status.HTTP_201_CREATED, many=False) + class Role(models.Model): id = models.CharField(max_length=30, primary_key=True) guild = models.ForeignKey(Guild, on_delete=models.CASCADE, null=False) - type = models.PositiveSmallIntegerField() + role_type = models.PositiveSmallIntegerField() + + def update_role(self, data): + if data.get('role_type'): + self.role_type = data.get('role_type') + + self.save() + return create_role_success_response(self, status=status.HTTP_202_ACCEPTED, many=False) + + @classmethod + def add_new_role(cls, data): + id = data.get('id') + guild_id = data.get('guild') + role_type = data.get('role_type') + if not (id and guild_id and role_type): + return create_error_response("The Role ID, Guild, and Role Type are required", + status=status.HTTP_400_BAD_REQUEST) + + if cls.get_role_by_id(id): + return create_error_response("That Role Already Exists", + status=status.HTTP_409_CONFLICT) + guild = Guild.get_guild_by_id(guild_id) + if not isinstance(guild, Guild): + return create_error_response("Guild Does Not Exist", + status=status.HTTP_404_NOT_FOUND) + + try: + role_type = int(role_type) + except ValueError: + return create_error_response("Role Type must be a positive number", + status=status.HTTP_400_BAD_REQUEST) + if role_type < 0: + return create_error_response("Role Type must be a positive number", + status=status.HTTP_400_BAD_REQUEST) + elif 1000 < role_type: + return create_error_response("Role Type must be less than 1000", + status=status.HTTP_400_BAD_REQUEST) + + role = cls( + id=id, + guild=guild, + role_type=role_type + ) + role.save() + return create_role_success_response(role, status.HTTP_201_CREATED, many=False) + + @classmethod + def get_role_by_id(cls, id): + try: + return cls.objects.get(id=id) + except ObjectDoesNotExist: + return None def __str__(self): return f"{self.guild.id} | {self.id}" diff --git a/geeksbot_v2/guilds/permissions.py b/geeksbot_v2/guilds/permissions.py new file mode 100644 index 0000000..50d64b0 --- /dev/null +++ b/geeksbot_v2/guilds/permissions.py @@ -0,0 +1,8 @@ +from rest_framework.permissions import BasePermission + +class GuildPermissions(BasePermission): + def has_permission(self, request, view): + return super().has_permission(request, view) + + def has_object_permission(self, request, view, obj): + return super().has_object_permission(request, view, obj) diff --git a/geeksbot_v2/guilds/utils.py b/geeksbot_v2/guilds/utils.py new file mode 100644 index 0000000..78314d8 --- /dev/null +++ b/geeksbot_v2/guilds/utils.py @@ -0,0 +1,21 @@ +from rest_framework.response import Response +from rest_framework import status + + +def create_error_response(msg, status=status.HTTP_404_NOT_FOUND): + return Response({'details': msg}, + status=status) + + +def create_success_response(guild_data, status, many: bool = False): + from .serializers import GuildSerializer + + return Response(GuildSerializer(guild_data, many=many).data, + status=status) + + +def create_role_success_response(role_data, status, many: bool = False): + from .serializers import RoleSerializer + + return Response(RoleSerializer(role_data, many=many).data, + status=status) diff --git a/geeksbot_v2/guilds/views.py b/geeksbot_v2/guilds/views.py index fc08748..532896c 100644 --- a/geeksbot_v2/guilds/views.py +++ b/geeksbot_v2/guilds/views.py @@ -1,13 +1,12 @@ -import os - -from django.shortcuts import render -from rest_framework.response import Response from rest_framework.views import APIView from rest_framework import status +from rest_framework.permissions import IsAuthenticated +from django.core.exceptions import ObjectDoesNotExist from geeksbot_v2.utils.api_utils import PaginatedAPIView from .models import Guild -from .serializers import GuildSerializer +from .utils import create_error_response +from .utils import create_success_response # Create your views here. @@ -15,37 +14,56 @@ from .serializers import GuildSerializer class GuildsAPI(PaginatedAPIView): - def get(self, request, format=None): - users = Guild.objects.all() - page = self.paginate_queryset(users) - if page is not None: - serialized_users = GuildSerializer(users, many=True) - return self.get_paginated_response(serialized_users.data) + permission_classes = [IsAuthenticated] - serialized_users = GuildSerializer(users, many=True) - return Response(serialized_users.data) + def get(self, request, format=None): + guilds = Guild.objects.all() + page = self.paginate_queryset(guilds) + if page is not None: + return create_success_response(page, status.HTTP_200_OK, many=True) + + return create_success_response(guilds, status.HTTP_200_OK, many=True) def post(self, request, format=None): data = dict(request.data) - print(data) - id = data.get('id') - default_channel = data.get('default_channel') - if not (id and default_channel): - return Response({'msg': 'id and default_channel are required'}, status=status.HTTP_400_BAD_REQUEST) + return Guild.create_guild(data) - admin_chat = data.get('admin_chat') - new_patron_message = data.get('new_patron_message') - default_prefix = os.environ['DISCORD_DEFAULT_PREFIX'] - prefixes = data.get('prefixes', [default_prefix, ]) - print(prefixes) - guild = Guild( - id=id[0] if isinstance(id, list) else id, - default_channel=default_channel[0] if isinstance(default_channel, list) else default_channel, - prefixes=prefixes, - admin_chat=admin_chat[0] if isinstance(admin_chat, list) else admin_chat, - new_patron_message=new_patron_message[0] if isinstance(new_patron_message, list) else new_patron_message - ) - guild.save() +class GuildDetail(APIView): + permission_classes = [IsAuthenticated] - return Response(GuildSerializer(guild).data, status=status.HTTP_201_CREATED) + def get(self, request, id, format=None): + try: + guild = Guild.objects.get(id=id) + except ObjectDoesNotExist: + return create_error_response("Guild Does not Exist", + status=status.HTTP_404_NOT_FOUND) + else: + return create_success_response(guild, + status=status.HTTP_200_OK) + + def put(self, request, id, format=None): + guild = Guild.get_guild_by_id(id) + + if guild: + data = dict(request.data) + guild = guild.update_guild(data) + return create_success_response(guild, + status=status.HTTP_202_ACCEPTED) + else: + return create_error_response('Guild Does Not Exist', + status=status.HTTP_404_NOT_FOUND) + + def delete(self, request, id, format=None): + guild = Guild.get_guild_by_id(id) + + if guild: + # data = dict(request.data) + # TODO Add a check to verify user is allowed to delete... + # Possibly in object permissions... + guild.delete() + return create_success_response(guild, + status=status.HTTP_200_OK) + else: + return create_error_response('Guild Does Not Exist', + status=status.HTTP_404_NOT_FOUND) diff --git a/geeksbot_v2/patreon/migrations/0001_initial.py b/geeksbot_v2/patreon/migrations/0001_initial.py index b74b38f..1c9db3e 100644 --- a/geeksbot_v2/patreon/migrations/0001_initial.py +++ b/geeksbot_v2/patreon/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2.4 on 2019-09-16 05:23 +# Generated by Django 2.2.4 on 2019-09-17 19:31 from django.db import migrations, models import django.db.models.deletion diff --git a/geeksbot_v2/patreon/models.py b/geeksbot_v2/patreon/models.py index 1035fa8..fa200fd 100644 --- a/geeksbot_v2/patreon/models.py +++ b/geeksbot_v2/patreon/models.py @@ -1,15 +1,63 @@ from django.db import models +from django.core.exceptions import ObjectDoesNotExist +from rest_framework import status from geeksbot_v2.guilds.models import Guild from geeksbot_v2.guilds.models import Role +from .utils import create_error_response +from .utils import create_success_creator_response +from .utils import create_success_tier_response # Create your models here. class PatreonCreator(models.Model): - guild = models.ForeignKey(Guild, on_delete=models.CASCADE, null=False) - creator = models.CharField(max_length=50, null=False) - link = models.CharField(max_length=100, null=False) + guilds = models.ManyToManyField(Guild) + creator = models.CharField(max_length=50, null=False, primary_key=True) + link = models.CharField(max_length=100, null=False, unique=True) + + def update_creator(self, data): + if data.get('guild'): + guild = Guild.get_guild_by_id(data.get('guild')) + if not isinstance(guild, Guild): + return create_error_response('Guild Does Not Exist', + status=status.HTTP_404_NOT_FOUND) + self.guilds.add(guild) + if data.get('link'): + self.link = data.get('link') + + self.save() + return create_success_creator_response(self, status.HTTP_202_ACCEPTED, many=False) + + @classmethod + def add_new_creator(cls, data): + creator = data.get('creator') + if PatreonCreator.get_creator_by_name(creator): + return create_error_response('That Creator already exists', + status=status.HTTP_409_CONFLICT) + link = data.get('link') + if not (creator and link): + return create_error_response('Creator and Link are both required fields', + status=status.HTTP_400_BAD_REQUEST) + guild = Guild.get_guild_by_id(data.get('guild')) + if not guild: + return create_error_response('A Valid Guild is required', + status=status.HTTP_400_BAD_REQUEST) + + new_creator = cls( + creator=creator, + link=link + ) + new_creator.save() + new_creator.guilds.add(guild) + return create_success_creator_response(new_creator, status.HTTP_201_CREATED, many=False) + + @classmethod + def get_creator_by_name(cls, name): + try: + return cls.objects.get(creator=name) + except ObjectDoesNotExist: + return None def __str__(self): return f"{self.guild.id} | {self.creator}" @@ -17,11 +65,100 @@ class PatreonCreator(models.Model): class PatreonTier(models.Model): creator = models.ForeignKey(PatreonCreator, on_delete=models.CASCADE) - guild = models.ForeignKey(Guild, on_delete=models.CASCADE) + guild = models.ManyToManyField(Guild) name = models.CharField(max_length=50) description = models.TextField() role = models.ForeignKey(Role, on_delete=models.CASCADE) amount = models.IntegerField(null=True) + next_lower_tier = models.ForeignKey('self', null=True, blank=True) + + def update_tier(self, data): + if data.get('guild'): + guild = Guild.get_guild_by_id(data.get('guild')) + if not isinstance(guild, Guild): + return create_error_response('Guild Does Not Exist', + status=status.HTTP_404_NOT_FOUND) + self.guilds.add(guild) + if data.get('name'): + self.name = data.get('name') + if data.get('description'): + self.description = data.get('description') + if data.get('role'): + role = Role.get_role_by_id(data.get('role')) + if not isinstance(role, Role): + return create_error_response('Role Does Not Exist', + status=status.HTTP_404_NOT_FOUND) + self.role = role + if data.get('amount'): + self.amount = data.get('amount') + if data.get('next_lower_tier'): + tier = self.get_tier_by_id(data.get('next_lower_tier')) + if not isinstance(tier, self.__class__): + return create_error_response('Next Lower Tier Does Not Exist', + status=status.HTTP_404_NOT_FOUND) + self.next_lower_tier = tier + + self.save() + return create_success_tier_response(tier, status.HTTP_202_ACCEPTED, many=False) + + @classmethod + def get_tier_by_id(cls, id): + try: + return cls.objects.get(id=id) + except ObjectDoesNotExist: + return None + + @classmethod + def add_new_tier(cls, data): + creator_str = data.get('creator') + guild_id = data.get('guild') + name = data.get('name') + description = data.get('description') + role_id = data.get('role') + next_lower_tier_id = data.get('next_lower_tier') + if not (creator_str and guild_id and name and description and role_id): + return create_error_response("The Creator's name, Guild, Tier name, Description, " + "and Discord Role are all required.", + status=status.HTTP_400_BAD_REQUEST) + creator = PatreonCreator.get_creator_by_name(creator_str) + if not isinstance(creator, PatreonCreator): + return create_error_response("Creator Does Not Exist", + status=status.HTTP_404_NOT_FOUND) + guild = Guild.get_guild_by_id(guild_id) + if not isinstance(guild, Guild): + return create_error_response("Guild Does Not Exist", + status=status.HTTP_404_NOT_FOUND) + role = Role.get_role_by_id(role_id) + if not isinstance(role, Role): + return create_error_response("Role Does Not Exist", + status=status.HTTP_404_NOT_FOUND) + if next_lower_tier_id: + next_lower_tier = cls.get_tier_by_id(next_lower_tier_id) + if not isinstance(next_lower_tier, cls): + return create_error_response("Next Lower Tier Does Not Exist", + status=status.HTTP_404_NOT_FOUND) + if next_lower_tier.guild != guild: + return create_error_response("The given next lower tier is not for the same guild.", + status=status.HTTP_400_BAD_REQUEST) + if next_lower_tier.creator != creator: + return create_error_response("The given next lower tier is not for the same creator.", + status=status.HTTP_400_BAD_REQUEST) + try: + PatreonTier.objects.filter(creator=creator, guilds__id=guild.id).get(name=name) + except ObjectDoesNotExist: + tier = cls( + creator=creator, + name=name, + description=description, + role=role, + amount=data.get('amount'), + next_lower_tier=next_lower_tier if next_lower_tier_id else None + ) + tier.save() + return create_success_tier_response(tier, status.HTTP_201_CREATED, many=False) + else: + return create_error_response("A Tier with that name already exists for that creator in this guild.", + status=status.HTTP_409_CONFLICT) def __str__(self): return f"{self.guild.id} | {self.creator.creator} | {self.name}" diff --git a/geeksbot_v2/patreon/utils.py b/geeksbot_v2/patreon/utils.py new file mode 100644 index 0000000..c738729 --- /dev/null +++ b/geeksbot_v2/patreon/utils.py @@ -0,0 +1,21 @@ +from rest_framework.response import Response +from rest_framework import status + + +def create_error_response(msg, status=status.HTTP_404_NOT_FOUND): + return Response({'details': msg}, + status=status) + + +def create_success_creator_response(creator_data, status, many: bool = False): + from .serializers import PatreonCreatorSerializer + + return Response(PatreonCreatorSerializer(creator_data, many=many).data, + status=status) + + +def create_success_tier_response(tier_data, status, many: bool = False): + from .serializers import PatreonTierSerializer + + return Response(PatreonTierSerializer(tier_data, many=many).data, + status=status) diff --git a/geeksbot_v2/rcon/migrations/0001_initial.py b/geeksbot_v2/rcon/migrations/0001_initial.py index 56e2bd6..e58313a 100644 --- a/geeksbot_v2/rcon/migrations/0001_initial.py +++ b/geeksbot_v2/rcon/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2.4 on 2019-09-16 05:23 +# Generated by Django 2.2.4 on 2019-09-17 19:31 from django.conf import settings from django.db import migrations, models @@ -10,9 +10,9 @@ class Migration(migrations.Migration): initial = True dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('guilds', '0001_initial'), ('dmessages', '0001_initial'), + ('guilds', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ diff --git a/geeksbot_v2/rcon/models.py b/geeksbot_v2/rcon/models.py index b341b7b..42c9bca 100644 --- a/geeksbot_v2/rcon/models.py +++ b/geeksbot_v2/rcon/models.py @@ -1,8 +1,13 @@ from django.db import models +from django.core.exceptions import ObjectDoesNotExist +from rest_framework import status from geeksbot_v2.guilds.models import Guild from geeksbot_v2.dmessages.models import Message from geeksbot_v2.users.models import User +from geeksbot_v2.channels.models import Channel +from .utils import create_error_response +from .utils import create_success_response # Create your models here. @@ -14,16 +19,110 @@ class RconServer(models.Model): port = models.PositiveIntegerField() password = models.CharField(max_length=50) monitor_chat = models.BooleanField() - monitor_chat_channel = models.CharField(max_length=30, blank=True) - alerts_channel = models.CharField(max_length=30, blank=True) - info_channel = models.CharField(max_length=30, blank=True) + monitor_chat_channel = models.ForeignKey( + Channel, on_delete=models.DO_NOTHING, related_name="+", null=True, blank=True, default=None + ) + alerts_channel = models.ForeignKey( + Channel, on_delete=models.DO_NOTHING, related_name="+", null=True, blank=True, default=None + ) + info_channel = models.ForeignKey( + Channel, on_delete=models.DO_NOTHING, related_name="+", null=True, blank=True, default=None + ) info_message = models.ForeignKey( - Message, on_delete=models.CASCADE, related_name="+", blank=True + Message, on_delete=models.DO_NOTHING, related_name="+", null=True, blank=True, default=None ) settings_message = models.ForeignKey( - Message, on_delete=models.CASCADE, related_name="+", blank=True + Message, on_delete=models.DO_NOTHING, related_name="+", null=True, blank=True, default=None ) whitelist = models.ManyToManyField(User, blank=True) + def update_server(self, data): + if data.get('name'): + self.name = data.get('name') + if data.get('ip'): + self.ip = data.get('ip') + if data.get('port'): + self.port = data.get('port') + if data.get('password'): + self.password = data.get('password') + if data.get('monitor_chat'): + self.monitor_chat = data.get('monitor_chat') + if 'monitor_chat_channel' in data.keys(): + self.monitor_chat_channel = Channel.get_channel_by_id(data.get('monitor_chat_channel')) + if 'alerts_channel' in data.keys(): + self.alerts_channel = Channel.get_channel_by_id(data.get('alerts_channel')) + if 'info_channel' in data.keys(): + self.alerts_channel = Channel.get_channel_by_id(data.get('info_channel')) + if 'info_message' in data.keys(): + self.info_message = Message.get_message_by_id(data.get('info_message')) + if 'settings_message' in data.keys(): + self.settings_message = Message.get_message_by_id(data.get('settings_message')) + + self.save() + return create_success_response(self, status.HTTP_202_ACCEPTED, many=False) + + def add_whitelist(self, user_id): + user = User.get_user_by_id(user_id) + if not isinstance(user, User): + return create_error_response("User Does Not Exist", + status=status.HTTP_404_NOT_FOUND) + if not user.steam_id: + return create_error_response("User does not have a Steam 64ID attached to their account", + status=status.HTTP_406_NOT_ACCEPTABLE) + self.whitelist.add(user) + return create_error_response("User has been added to the whitelist", + status=status.HTTP_200_OK) + + def remove_from_whitelist(self, user_id): + user = User.get_user_by_id(user_id) + if not isinstance(user, User): + return create_error_response("User Does Not Exist", + status=status.HTTP_404_NOT_FOUND) + self.whitelist.remove(user) + return create_error_response("User has been removed from the whitelist", + status=status.HTTP_200_OK) + + @classmethod + def add_new_server(cls, data): + guild_id = data.get('guild') + name = data.get('name') + ip = data.get('ip') + port = data.get('port') + password = data.get('password') + if not (guild_id and name and ip and port and password): + return create_error_response("One or more of the required fields are missing", + status=status.HTTP_400_BAD_REQUEST) + guild = Guild.get_guild_by_id(guild_id) + if not isinstance(guild, Guild): + return create_error_response("Guild Does Not Exist", + status=status.HTTP_404_NOT_FOUND) + server = cls( + guild=guild, + name=name, + ip=ip, + port=port, + password=password, + monitor_chat=data.get('monitor_chat', False) + ) + server.save() + return create_success_response(server, status.HTTP_201_CREATED, many=False) + + @classmethod + def get_server(cls, guild_id, name): + guild_servers = cls.get_guild_servers(guild_id) + if guild_servers: + try: + return guild_servers.get(name=name) + except ObjectDoesNotExist: + return None + return None + + @classmethod + def get_guild_servers(cls, guild_id): + guild = Guild.get_guild_by_id(guild_id) + if not isinstance(guild, guild): + return None + return cls.objects.filter(guild=guild) + def __str__(self): return f"{self.guild.id} | {self.name}" diff --git a/geeksbot_v2/rcon/utils.py b/geeksbot_v2/rcon/utils.py new file mode 100644 index 0000000..9092407 --- /dev/null +++ b/geeksbot_v2/rcon/utils.py @@ -0,0 +1,14 @@ +from rest_framework.response import Response +from rest_framework import status + + +def create_error_response(msg, status=status.HTTP_404_NOT_FOUND): + return Response({'details': msg}, + status=status) + + +def create_success_response(rcon_data, status, many: bool = False): + from .serializers import RconServerSerializer + + return Response(RconServerSerializer(rcon_data, many=many).data, + status=status) \ No newline at end of file diff --git a/geeksbot_v2/templates/users/user_detail.html b/geeksbot_v2/templates/users/user_detail.html index 9190a5f..c8c5d13 100644 --- a/geeksbot_v2/templates/users/user_detail.html +++ b/geeksbot_v2/templates/users/user_detail.html @@ -13,7 +13,7 @@ {% if object.name %}

{{ object.name }}

{% endif %} - {{ user.id }} + {{ user.auth_token }} diff --git a/geeksbot_v2/users/admin.py b/geeksbot_v2/users/admin.py index 60a20f6..c3af8e4 100644 --- a/geeksbot_v2/users/admin.py +++ b/geeksbot_v2/users/admin.py @@ -1,13 +1,17 @@ from django.contrib import admin from django.contrib.auth import admin as auth_admin -from .forms import UserChangeForm +from .forms import UserChangeForm, UserCreateForm from .models import User class UserAdmin(auth_admin.UserAdmin): model = User form = UserChangeForm + add_form = UserCreateForm + add_fieldsets = auth_admin.UserAdmin.add_fieldsets + ( + (None, {'fields': ('id')}), + ) admin.site.register(User, UserAdmin) diff --git a/geeksbot_v2/users/api_urls.py b/geeksbot_v2/users/api_urls.py index 6d500cb..7664afe 100644 --- a/geeksbot_v2/users/api_urls.py +++ b/geeksbot_v2/users/api_urls.py @@ -1,8 +1,9 @@ from django.urls import path -from geeksbot_v2.users.views import UsersAPI +from geeksbot_v2.users.views import UsersAPI, UserDetail app_name = "users_api" urlpatterns = [ - path("users/", view=UsersAPI.as_view(), name="list") + path("/", view=UsersAPI.as_view(), name="list"), + path("//", view=UserDetail.as_view(), name="detail"), ] diff --git a/geeksbot_v2/users/forms.py b/geeksbot_v2/users/forms.py index 8be2bc6..74227a8 100644 --- a/geeksbot_v2/users/forms.py +++ b/geeksbot_v2/users/forms.py @@ -1,8 +1,20 @@ from django.contrib.auth import forms +from django.forms import CharField +from allauth.account.forms import SignupForm from .models import User +class UserCreateForm(SignupForm): + id = CharField(max_length=30, label='Discord ID') + + def save(self, request): + user = super(UserCreateForm, self).save(request) + user.id = self.cleaned_data['id'] + user.save() + return user + + class UserChangeForm(forms.UserChangeForm): class Meta(forms.UserChangeForm.Meta): model = User diff --git a/geeksbot_v2/users/migrations/0001_initial.py b/geeksbot_v2/users/migrations/0001_initial.py new file mode 100644 index 0000000..b2530ae --- /dev/null +++ b/geeksbot_v2/users/migrations/0001_initial.py @@ -0,0 +1,70 @@ +# Generated by Django 2.2.4 on 2019-09-17 19:38 + +from django.conf import settings +import django.contrib.auth.models +import django.contrib.auth.validators +import django.contrib.postgres.fields +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0011_update_proxy_permissions'), + ('guilds', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('name', models.CharField(blank=True, max_length=255, verbose_name='Name of User')), + ('id', models.CharField(max_length=30, primary_key=True, serialize=False)), + ('discord_username', models.CharField(max_length=100, null=True)), + ('previous_discord_usernames', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), blank=True, null=True, size=None)), + ('discriminator', models.IntegerField(null=True)), + ('previous_discriminators', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), blank=True, null=True, size=None)), + ('steam_id', models.CharField(blank=True, max_length=30, null=True)), + ('animated', models.BooleanField(blank=True, null=True)), + ('avatar', models.CharField(blank=True, max_length=100, null=True)), + ('bot', models.BooleanField(blank=True, null=True)), + ('banned', models.BooleanField(default=False)), + ('logging_enabled', models.BooleanField(default=True)), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), + ('guilds', models.ManyToManyField(blank=True, null=True, to='guilds.Guild')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), + ], + options={ + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + 'abstract': False, + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + migrations.CreateModel( + name='UserLog', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('time', models.DateTimeField()), + ('action', models.IntegerField()), + ('description', models.CharField(max_length=100)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/geeksbot_v2/users/migrations/0002_auto_20190917_2109.py b/geeksbot_v2/users/migrations/0002_auto_20190917_2109.py new file mode 100644 index 0000000..f7b4f6a --- /dev/null +++ b/geeksbot_v2/users/migrations/0002_auto_20190917_2109.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.4 on 2019-09-17 21:09 + +import django.contrib.auth.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='username', + field=models.CharField(help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username'), + ), + ] diff --git a/geeksbot_v2/users/migrations/0003_auto_20190918_0554.py b/geeksbot_v2/users/migrations/0003_auto_20190918_0554.py new file mode 100644 index 0000000..9a07dad --- /dev/null +++ b/geeksbot_v2/users/migrations/0003_auto_20190918_0554.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.4 on 2019-09-18 05:54 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0002_auto_20190917_2109'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='discriminator', + field=models.CharField(max_length=4, null=True), + ), + migrations.AlterField( + model_name='user', + name='previous_discriminators', + field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=4), blank=True, null=True, size=None), + ), + ] diff --git a/geeksbot_v2/users/migrations/__init__.py b/geeksbot_v2/users/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/geeksbot_v2/users/models.py b/geeksbot_v2/users/models.py index 3b893b2..2eb7e62 100644 --- a/geeksbot_v2/users/models.py +++ b/geeksbot_v2/users/models.py @@ -1,4 +1,5 @@ from django.contrib.auth.models import AbstractUser +from django.contrib.auth.validators import UnicodeUsernameValidator from django.db.models import CharField from django.urls import reverse from django.utils.translation import ugettext_lazy as _ @@ -8,8 +9,14 @@ from django.conf import settings from django.db.models.signals import post_save from django.dispatch import receiver from rest_framework.authtoken.models import Token +from django.core.exceptions import ObjectDoesNotExist +from rest_framework import status from geeksbot_v2.guilds.models import Guild +from .utils import verify_user_data +from .utils import create_error_response +from .utils import create_log_success_response +from .utils import create_success_response @receiver(post_save, sender=settings.AUTH_USER_MODEL) @@ -23,11 +30,18 @@ class User(AbstractUser): # First Name and Last Name do not cover name patterns # around the globe. name = CharField(_("Name of User"), blank=True, max_length=255) + username = models.CharField( + _('username'), + max_length=150, + unique=False, + help_text=_('Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'), + validators=[UnicodeUsernameValidator()], + ) id = models.CharField(max_length=30, primary_key=True) discord_username = models.CharField(max_length=100, null=True) previous_discord_usernames = ArrayField(models.CharField(max_length=100), blank=True, null=True) - discriminator = models.IntegerField(null=True) - previous_discriminators = ArrayField(models.IntegerField(), blank=True, null=True) + discriminator = models.CharField(max_length=4, null=True) + previous_discriminators = ArrayField(models.CharField(max_length=4), blank=True, null=True) guilds = models.ManyToManyField(Guild, blank=True, null=True) steam_id = models.CharField(max_length=30, blank=True, null=True) animated = models.BooleanField(blank=True, null=True) @@ -36,15 +50,158 @@ class User(AbstractUser): banned = models.BooleanField(default=False) logging_enabled = models.BooleanField(default=True) + @classmethod + def add_new_user(cls, data): + if not verify_user_data(data): + return create_error_response("Not all required fields are present.", + status=status.HTTP_400_BAD_REQUEST) + id = data.get('id') + if id: + if User.objects.filter(id=id).exists(): + return create_error_response("User Exists please update instead of create", + status=status.HTTP_409_CONFLICT) + discord_username = data.get('username') + discriminator = data.get('discriminator') + guild_id = data.get('guild') + try: + guild = Guild.objects.get(id=str(guild_id)) + except ObjectDoesNotExist: + return create_error_response("That is not a valid Guild", + status=status.HTTP_400_BAD_REQUEST) + animated = data.get('animated') + avatar = data.get('avatar') + bot = data.get('bot') + banned = data.get('banned') + logging = data.get('logging') + if not (avatar and (animated is not None) and (bot is not None)): + return create_error_response("All required fields must contain a value", + status.HTTP_400_BAD_REQUEST) + + user = User( + id=id, + discord_username=discord_username, + discriminator=discriminator, + animated=animated, + avatar=avatar, + bot=bot, + banned=banned or False, + logging_enabled=logging or True + ) + user.save() + user.guilds.add(guild) + return create_success_response(user, status.HTTP_201_CREATED, many=False) + + def update_user(self, data): + if data.get('username') and data.get('username') != self.discord_username: + if isinstance(self.previous_discord_usernames, list): + self.previous_discord_usernames.append(self.discord_username) + else: + self.previous_discord_usernames = [self.discord_username, ] + self.discord_username = data.get('username') + if data.get('discriminator') and data.get('discriminator') != self.discriminator: + if isinstance(self.previous_discriminators, list): + self.previous_discriminators.append(self.discriminator) + else: + self.previous_discriminators = [self.discriminator, ] + self.discriminator = data.get('discriminator') + if data.get('guild'): + guild = Guild.get_guild_by_id(data.get('guild')) + if not isinstance(guild, Guild): + return create_error_response("That is not a valid Guild", + status=status.HTTP_400_BAD_REQUEST) + self.guilds.add(guild) + if data.get('steam_id'): + self.steam_id = data.get('steam_id') + if data.get('animated'): + self.animated = data.get('animated') + if data.get('avatar'): + self.avatar = data.get('avatar') + if data.get('bot'): + self.bot = data.get('bot') + if data.get('banned'): + self.banned = data.get('banned') + if data.get('logging'): + self.logging_enabled = data.get('logging') + + self.save() + return create_success_response(self, status.HTTP_202_ACCEPTED, many=False) + + @classmethod + def get_user_by_id(cls, id): + try: + return cls.objects.get(id=id) + except ObjectDoesNotExist: + return None + def get_absolute_url(self): return reverse("users:detail", kwargs={"username": self.username}) class UserLog(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) - time = models.DateTimeField() + time = models.DateTimeField(auto_now_add=True, blank=True) action = models.IntegerField() - description = models.CharField(max_length=100) + description = models.CharField(max_length=100, null=True, blank=True) + + @classmethod + def add_new_log(cls, user, data): + user_id = data.get('user') + action = data.get('action') + description = data.get('description') + if not (user_id and action): + return create_error_response("User and Action are required.", + status=status.HTTP_400_BAD_REQUEST) + user = User.get_user_by_id(user_id) + if not isinstance(user, User): + return create_error_response("User Does Not Exist", + status=status.HTTP_404_NOT_FOUND) + try: + action = int(action) + except ValueError: + return create_error_response("The Action must be a number", + status=status.HTTP_400_BAD_REQUEST) + log = cls( + user=user, + action=action, + description=description + ) + log.save() + return create_log_success_response(log, status.HTTP_201_CREATED, many=False) + + @classmethod + def get_log_by_id(cls, id): + try: + return cls.objects.get(id=id) + except ObjectDoesNotExist: + return None + + @classmethod + def get_logs_by_user(cls, user_id, count: int = None): + user = User.get_user_by_id(user_id) + if isinstance(user, User): + user_logs = cls.objects.filter(user=user).order_by('-time') + if count: + user_logs = user_logs[:count] + if len(user_logs) > 0: + return user_logs + else: + return [] + else: + return [] + + @classmethod + def get_logs_by_user_action(cls, user_id, action, count: int = None): + user = User.get_user_by_id(user_id) + if isinstance(user, User): + user_logs = cls.objects.filter(user=user, action=action).order_by('-time') + if count: + user_logs = user_logs[:count] + if len(user_logs) > 0: + return user_logs + else: + return [] + else: + return [] def __str__(self): return f"{self.time} | {self.user.id} | {self.action}" diff --git a/geeksbot_v2/users/serializers.py b/geeksbot_v2/users/serializers.py index 340aef1..d8d11aa 100644 --- a/geeksbot_v2/users/serializers.py +++ b/geeksbot_v2/users/serializers.py @@ -7,7 +7,22 @@ from geeksbot_v2.users.models import UserLog class UserSerializer(serializers.ModelSerializer): class Meta: model = User - fields = "__all__" + fields = [ + 'id', + 'username', + 'name', + 'discord_username', + 'previous_discord_usernames', + 'discriminator', + 'previous_discriminators', + 'guilds', + 'steam_id', + 'animated', + 'avatar', + 'bot', + 'banned', + 'logging_enabled' + ] class UserLogSerializer(serializers.ModelSerializer): diff --git a/geeksbot_v2/users/utils.py b/geeksbot_v2/users/utils.py new file mode 100644 index 0000000..393d9bd --- /dev/null +++ b/geeksbot_v2/users/utils.py @@ -0,0 +1,36 @@ +from rest_framework.response import Response +from rest_framework import status + + +def create_error_response(msg, status=status.HTTP_404_NOT_FOUND): + return Response({'details': msg}, + status=status) + + +def create_success_response(user_data, status, many: bool = False): + from .serializers import UserSerializer + + return Response(UserSerializer(user_data, many=many).data, + status=status) + + +def create_log_success_response(log_data, status, many: bool = False): + from .serializers import UserLogSerializer + + return Response(UserLogSerializer(log_data, many=many).data, + status=status) + + +required_fields = [ + 'id', + 'username', + 'discriminator', + 'guild', + 'animated', + 'avatar', + 'bot', +] + + +def verify_user_data(data): + return all([field in data.keys() for field in required_fields]) diff --git a/geeksbot_v2/users/views.py b/geeksbot_v2/users/views.py index 3758c2e..67b81d3 100644 --- a/geeksbot_v2/users/views.py +++ b/geeksbot_v2/users/views.py @@ -1,19 +1,19 @@ -from django.contrib.auth import get_user_model from django.contrib.auth.mixins import LoginRequiredMixin from django.urls import reverse from django.views.generic import DetailView, RedirectView, UpdateView from django.contrib import messages from django.utils.translation import ugettext_lazy as _ -from rest_framework.response import Response from rest_framework.views import APIView +from rest_framework.permissions import IsAuthenticated +from rest_framework import status -from geeksbot_v2.users.serializers import UserSerializer -from geeksbot_v2.users.serializers import UserLogSerializer -from geeksbot_v2.users.models import UserLog +from .models import UserLog from geeksbot_v2.utils.api_utils import PaginatedAPIView - -User = get_user_model() +from .models import User +from .utils import create_error_response +from .utils import create_success_response +from .utils import create_log_success_response class UserDetailView(LoginRequiredMixin, DetailView): @@ -24,6 +24,7 @@ class UserDetailView(LoginRequiredMixin, DetailView): def get(self, request, *args, **kwargs): self.object = self.get_object() + context = self.get_context_data(object=self.object, user=request.user) return self.render_to_response(context) @@ -66,44 +67,72 @@ user_redirect_view = UserRedirectView.as_view() class UsersAPI(PaginatedAPIView): - def get(self, request, guild, format=None): - users = User.objects.filter(guilds__id=guild) + permission_classes = [IsAuthenticated] + + def get(self, request, guild=None, format=None): + if guild: + users = User.objects.filter(guilds__id=guild) + else: + users = User.objects.all() page = self.paginate_queryset(users) if page is not None: - serialized_users = UserSerializer(users, many=True) - return self.get_paginated_response(serialized_users.data) + return create_success_response(page, status.HTTP_200_OK, many=True) - serialized_users = UserSerializer(users, many=True) - return Response(serialized_users.data) + return create_success_response(users, status.HTTP_200_OK, many=True) + + def post(self, request, format=None): + data = dict(request.data) + return User.add_new_user(data) class UserDetail(APIView): - def get(self, request, guild, id, format=None): - user = User.objects.filter(guilds__id=guild).get(id=id) - return Response(UserSerializer(user).data) + permission_classes = [IsAuthenticated] + + def get(self, request, id, format=None): + user = User.get_user_by_id(id) + if not isinstance(user, User): + return create_error_response("User Does not Exist", + status=status.HTTP_404_NOT_FOUND) + return create_success_response(user, + status=status.HTTP_200_OK) + + def put(self, request, id, format=None): + user = User.get_user_by_id(id) + if isinstance(user, User): + data = dict(request.data) + return user.update_user(data) + else: + return create_error_response("User Does Not Exist", + status=status.HTTP_404_NOT_FOUND) class UserLogList(PaginatedAPIView): + permission_classes = [IsAuthenticated] + def get(self, request, user, action=None, format=None): if action: - user_logs = ( - UserLog.objects.filter(user=user) - .filter(action=action) - .order_by("-time") - ) + user_logs = UserLog.get_logs_by_user_action(user, action) else: - user_logs = UserLog.objects.filter(user=user).order_by("-time") + user_logs = UserLog.get_logs_by_user(user) page = self.paginate_queryset(user_logs) if page is not None: - serialized_logs = UserLogSerializer(page, many=True) - return self.get_paginated_response(serialized_logs.data) + return create_log_success_response(page, status.HTTP_200_OK, many=True) - serialized_logs = UserLogSerializer(user_logs, many=True) - return Response(serialized_logs.data) + return create_log_success_response(user_logs, status.HTTP_200_OK, many=True) + + def post(self, request, user, format=None): + data = dict(request.data) + return UserLog.add_new_log(user, data) class UserLogDetail(APIView): + permission_classes = [IsAuthenticated] + def get(self, request, id, format=None): - user_log = UserLog.objects.get(id=id) - return Response(UserLogSerializer(user_log).data) + user_log = UserLog.get_log_by_id(id) + if isinstance(user_log, UserLog): + return create_log_success_response(user_log, status.HTTP_200_OK, many=False) + else: + return create_error_response("Log Does Not Exist", + status=status.HTTP_404_NOT_FOUND) diff --git a/services/postgresql/postgres.conf b/services/postgresql/postgres.conf index 0415c2c..44c7a46 100644 --- a/services/postgresql/postgres.conf +++ b/services/postgresql/postgres.conf @@ -61,7 +61,7 @@ listen_addresses = '*' # defaults to 'localhost'; use '*' for all # (change requires restart) #port = 5432 # (change requires restart) -#max_connections = 100 # (change requires restart) +max_connections = 1000 # (change requires restart) #superuser_reserved_connections = 3 # (change requires restart) #unix_socket_directories = '/tmp' # comma-separated list of directories # (change requires restart)