From b8f751bae95bc2b21f25148b4df03ae003eb56c5 Mon Sep 17 00:00:00 2001 From: Dusty Pianalto Date: Sun, 1 Dec 2019 10:30:03 -0900 Subject: [PATCH] Multiple changes Started switching to generics Added tickets --- docker-compose.yml | 32 +-- geeksbot/config/bot_config.json | 5 +- geeksbot/exts/tickets.py | 217 ++++++++++++++++++ geeksbot/imports/message_logging.py | 158 +++++++++++++ geeksbot/imports/utils.py | 21 +- geeksbot_v2/channels/api_urls.py | 5 +- geeksbot_v2/channels/models.py | 17 +- geeksbot_v2/channels/views.py | 52 ++++- geeksbot_v2/config/settings/base.py | 2 +- geeksbot_v2/config/urls.py | 1 + geeksbot_v2/dmessages/api_urls.py | 18 +- geeksbot_v2/dmessages/models.py | 36 +-- geeksbot_v2/dmessages/views.py | 80 ++++++- geeksbot_v2/guilds/api_urls.py | 2 + geeksbot_v2/guilds/models.py | 13 +- geeksbot_v2/guilds/views.py | 22 +- geeksbot_v2/patreon/patron.py | 77 +++++++ geeksbot_v2/rcon/api_urls.py | 10 + geeksbot_v2/rcon/models.py | 2 +- geeksbot_v2/rcon/rcon_lib/__init__.py | 0 geeksbot_v2/rcon/rcon_lib/arcon.py | 118 ++++++++++ geeksbot_v2/rcon/rcon_lib/rcon.py | 183 +++++++++++++++ geeksbot_v2/rcon/utils.py | 7 +- geeksbot_v2/rcon/views.py | 77 ++++++- geeksbot_v2/shared_libs/TicTacToe/__init__.py | 0 geeksbot_v2/shared_libs/TicTacToe/board.py | 74 ++++++ geeksbot_v2/shared_libs/TicTacToe/player.py | 195 ++++++++++++++++ geeksbot_v2/shared_libs/__init__.py | 0 geeksbot_v2/users/api_urls.py | 4 +- geeksbot_v2/users/serializers.py | 32 ++- geeksbot_v2/users/views.py | 127 +++++----- geeksbot_v2/utils/permissions.py | 27 +++ requirements/geeksbot.txt | 4 +- requirements/web.txt | 2 + services/Dockerfile-base | 2 +- services/Dockerfile-geeksbot | 2 +- services/Dockerfile-web | 2 +- 37 files changed, 1482 insertions(+), 144 deletions(-) create mode 100644 geeksbot/imports/message_logging.py create mode 100644 geeksbot_v2/patreon/patron.py create mode 100644 geeksbot_v2/rcon/api_urls.py create mode 100644 geeksbot_v2/rcon/rcon_lib/__init__.py create mode 100644 geeksbot_v2/rcon/rcon_lib/arcon.py create mode 100644 geeksbot_v2/rcon/rcon_lib/rcon.py create mode 100644 geeksbot_v2/shared_libs/TicTacToe/__init__.py create mode 100644 geeksbot_v2/shared_libs/TicTacToe/board.py create mode 100644 geeksbot_v2/shared_libs/TicTacToe/player.py create mode 100644 geeksbot_v2/shared_libs/__init__.py create mode 100644 geeksbot_v2/utils/permissions.py diff --git a/docker-compose.yml b/docker-compose.yml index ab4cee1..83652f6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,25 +1,25 @@ version: '3' services: - base: + geeksbot-base: build: context: . dockerfile: "${PWD}/services/Dockerfile-base" - image: base:latest - - db: + image: geeksbot-base:latest + + geeksbot-db: image: postgres ports: - "5432:5432" volumes: - "${PWD}/services/postgresql/postgres.conf:/etc/postgresql/postgresql.conf" - - "db:/var/lib/postgresql/data:rw" + - "geeksbot-db:/var/lib/postgresql/data:rw" env_file: ${PWD}/.env - redis: + geeksbot-redis: image: redis:5.0.3 ports: - "6379:6379" - web: + geeksbot-web: build: context: . dockerfile: "${PWD}/services/Dockerfile-web" @@ -29,9 +29,9 @@ services: - "8000:8000" - "443:443" depends_on: - - db - - redis - - base + - geeksbot-db + - geeksbot-redis + - geeksbot-base environment: - DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} - REDIS_URL=redis://${REDIS_HOST}:${REDIS_PORT}/${REDIS_DB} @@ -43,12 +43,12 @@ services: dockerfile: "${PWD}/services/Dockerfile-geeksbot" env_file: ${PWD}/.env depends_on: - - db - - redis - - base - - web + - geeksbot-db + - geeksbot-redis + - geeksbot-base + - geeksbot-web links: - - web:geeksbot.app + - geeksbot-web:geeksbot.app volumes: - ${PWD}/geeksbot:/code/geeksbot - ~/.ssh/id_rsa:/root/.ssh/id_rsa @@ -56,5 +56,5 @@ services: - ~/.ssh/known_hosts:/root/.ssh/known_hosts volumes: - db: + geeksbot-db: external: true diff --git a/geeksbot/config/bot_config.json b/geeksbot/config/bot_config.json index fe58878..2bccce0 100644 --- a/geeksbot/config/bot_config.json +++ b/geeksbot/config/bot_config.json @@ -2,6 +2,7 @@ "load_list": [ "admin", "exec", - "message_events" + "message_events", + "tickets" ] -} \ No newline at end of file +} diff --git a/geeksbot/exts/tickets.py b/geeksbot/exts/tickets.py index e3ae20d..5d18926 100644 --- a/geeksbot/exts/tickets.py +++ b/geeksbot/exts/tickets.py @@ -1 +1,218 @@ import discord +from discord.ext import commands + +from geeksbot.imports.utils import Paginator, Book + + +class Tickets(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @commands.command() + async def request(self, ctx, *, message=None): + if not ctx.guild: + await ctx.send('This command must be run from inside a guild.') + return + + if not message: + await ctx.send('Please include a message containing your request') + return + + if len(message) > 1000: + await ctx.send('Request is too long, please keep your request to less than 1000 characters.') + return + + data = { + 'author': ctx.author.id, + 'message': ctx.message.id, + 'channel': ctx.channel.id, + 'content': message + } + msg_resp = await self.bot.aio_session.get(f'{self.bot.api_base}/messages/{ctx.message.id}/wait/', headers=self.bot.auth_header) + if msg_resp.status == 404: + error = await msg_resp.json() + await ctx.send(error['details']) + return + + resp = await self.bot.aio_session.post(f'{self.bot.api_base}/messages/{ctx.guild.id}/requests/', headers=self.bot.auth_header, json=data) + + if resp.status == 201: + admin_channel_resp = await self.bot.aio_session.get(f'{self.bot.api_base}/channels/{ctx.guild.id}/admin/', headers=self.bot.auth_header) + request = await resp.json() + + if admin_channel_resp.status == 200: + admin_chan_data = await admin_channel_resp.json() + msg = f'' + admin_roles_resp = await self.bot.aio_session.get(f'{self.bot.api_base}/guilds/{ctx.guild.id}/roles/admin/', headers=self.bot.auth_header) + if admin_roles_resp.status == 200: + admin_roles_data = await admin_roles_resp.json() + for role in admin_roles_data: + msg += f'{ctx.guild.get_role(int(role["id"])).mention} ' + msg += f"New Request ID: {request['id']} " \ + f"{ctx.author.mention} has requested assistance: \n" \ + f"```{request['content']}``` \n" \ + f"Requested at: {request['requested_at'].split('.')[0].replace('T', ' ')} GMT\n" \ + f"In {ctx.guild.get_channel(int(request['channel'])).name}" + admin_chan = ctx.guild.get_channel(int(admin_chan_data['id'])) + await admin_chan.send(msg) + await ctx.send(f'{ctx.author.mention} The admin have received your request.\n' + f'If you would like to update or close your request please reference Request ID `{request["id"]}`') + + @commands.command(aliases=['comment']) + async def update(self, ctx, request_id=None, *, comment: str = None): + try: + request_id = int(request_id) + except ValueError: + await ctx.send("Please include the ID of the request you would like to update as the first thing after the command.") + return + + if not comment: + await ctx.send("There is nothing to update since you didn't include a message.") + return + + data = { + 'author': ctx.author.id, + 'content': comment + } + + comment_resp = await self.bot.aio_session.post(f'{self.bot.api_base}/messages/{ctx.guild.id}/requests/{request_id}/comments/', headers=self.bot.auth_header, json=data) + + if comment_resp.status == 201: + comment = await comment_resp.json() + admin_channel_resp = await self.bot.aio_session.get(f'{self.bot.api_base}/channels/{ctx.guild.id}/admin/', + headers=self.bot.auth_header) + + if admin_channel_resp.status == 200: + admin_channel_data = await admin_channel_resp.json() + admin_channel = ctx.guild.get_channel(int(admin_channel_data['id'])) + if admin_channel: + request_resp = await self.bot.aio_session.get(f'{self.bot.api_base}/messages/{ctx.guild.id}/requests/{request_id}/', headers=self.bot.auth_header) + pag = Paginator(self.bot, prefix='```md', suffix='```') + header = f'{ctx.author.mention} has commented on request {request_id}\n' + if request_resp.status == 200: + request = await request_resp.json() + requestor = ctx.guild.get_member(int(request["author"])) + header += (f'Original Request by {requestor.mention if requestor else "`User cannot be found`"}:\n' + f'```{request["content"]}```') + pag.set_header(header) + + if request.get('comments'): + comments = request['comments'] + for comment in comments: + author = ctx.guild.get_member(int(comment['author'])) + pag.add(f'{author.display_name}: {comment["content"]}', keep_intact=True) + if ctx.author != requestor and requestor: + for page in pag.pages(page_headers=False): + await requestor.send(page) + book = Book(pag, (None, admin_channel, self.bot, ctx.message)) + await book.create_book() + await ctx.send(f'{ctx.author.mention} Your comment has been added to the request.') + + @commands.command(name='requests_list', aliases=['rl']) + async def _requests_list(self, ctx, closed: str = ''): + pag = Paginator(self.bot) + admin_roles_resp = await self.bot.aio_session.get(f'{self.bot.api_base}/guilds/{ctx.guild.id}/roles/admin/', headers=self.bot.auth_header) + if admin_roles_resp.status == 200: + admin_roles_data = await admin_roles_resp.json() + admin_roles = [ctx.guild.get_role(int(role['id'])) for role in admin_roles_data] + if any([role in ctx.author.roles for role in admin_roles]): + requests_resp = await self.bot.aio_session.get(f'{self.bot.api_base}/messages/{ctx.guild.id}/requests/', headers=self.bot.auth_header) + if requests_resp.status == 200: + requests_data = await requests_resp.json() + requests_list = requests_data['requests'] if isinstance(requests_data, dict) else requests_data + while isinstance(requests_data, dict) and requests_data.get('next'): + requests_resp = await self.bot.aio_session.get( + requests_data['next'], headers=self.bot.auth_header) + if requests_resp.status == 200: + requests_data = await requests_resp.json() + requests_list.extend(requests_data['requests'] if isinstance(requests_data, dict) else requests_data) + for request in requests_list: + member = discord.utils.get(ctx.guild.members, id=int(request['author'])) + title = (f"<{'Request ID':^20} {'Requested By':^20}>\n" + f"<{request['id']:^20} {member.display_name if member else 'None':^20}>") + orig_channel = ctx.guild.get_channel(int(request.get('channel'))) + comments_count_resp = await self.bot.aio_session.get(f'{self.bot.api_base}/messages/{ctx.guild.id}/requests/{request["id"]}/comments/count/', headers=self.bot.auth_header) + pag.add(f"\n\n{title}\n" + f"{request['content']}\n\n" + f"Comments: {await comments_count_resp.text() if comments_count_resp.status == 200 else 0}\n" + f"Requested at: " + f"{request['requested_at'].split('.')[0].replace('T', ' ')} GMT\n" + f"In {orig_channel.name if orig_channel else 'N/A'}", keep_intact=True) + pag.add(f'\n\uFFF8\nThere are currently {len(requests_list)} requests open.') + else: + pag.add('There are no open requests for this guild.', keep_intact=True) + else: + requests_resp = await self.bot.aio_session.get(f'{self.bot.api_base}/messages/{ctx.guild.id}/requests/user/{ctx.author.id}/', + headers=self.bot.auth_header) + if requests_resp.status == 200: + requests_data = await requests_resp.json() + requests_list = requests_data['requests'] if isinstance(requests_data, dict) else requests_data + while isinstance(requests_data, dict) and requests_data.get('next'): + requests_resp = await self.bot.aio_session.get( + requests_data['next'], headers=self.bot.auth_header) + if requests_resp.status == 200: + requests_data = await requests_resp.json() + requests_list.extend( + requests_data['requests'] if isinstance(requests_data, dict) else requests_data) + for request in requests_list: + title = (f"<{'Request ID':^20}>\n" + f"<{request['id']:^20}>") + orig_channel = ctx.guild.get_channel(int(request.get('channel'))) + comments_count_resp = await self.bot.aio_session.get( + f'{self.bot.api_base}/messages/{ctx.guild.id}/requests/{request["id"]}/comments/count/', + headers=self.bot.auth_header) + pag.add(f"\n\n{title}\n" + f"{request['content']}\n\n" + f"Comments: {await comments_count_resp.text() if comments_count_resp.status == 200 else 0}\n" + f"Requested at: " + f"{request['requested_at'].split('.')[0].replace('T', ' ')} GMT\n" + f"In {orig_channel.name if orig_channel else 'N/A'}", keep_intact=True) + pag.add(f'\n\uFFF8\nYou currently have {len(requests_list)} requests open.') + else: + pag.add('You have no open requests for this guild.', keep_intact=True) + for page in pag.pages(): + await ctx.send(page) + + @commands.command() + async def close(self, ctx, *, ids=None): + if not ids: + await ctx.send('Please include at least one Request ID to close.') + return + + admin = False + admin_roles_resp = await self.bot.aio_session.get(f'{self.bot.api_base}/guilds/{ctx.guild.id}/roles/admin/', + headers=self.bot.auth_header) + if admin_roles_resp.status == 200: + admin_roles_data = await admin_roles_resp.json() + admin_roles = [ctx.guild.get_role(int(role['id'])) for role in admin_roles_data] + if any([role in ctx.author.roles for role in admin_roles]): + admin = True + + ids = [id.strip() for id in ids.replace(' ', '').split(',')] + + for id in ids: + request_resp = await self.bot.aio_session.get(f'{self.bot.api_base}/messages/{ctx.guild.id}/requests/{id}/', headers=self.bot.auth_header) + if request_resp.status == 200: + request = await request_resp.json() + requestor = ctx.guild.get_member(int(request['author'])) + if requestor == ctx.author or admin: + data = { + 'completed_by': ctx.author.id + } + delete_resp = await self.bot.aio_session.delete(f'{self.bot.api_base}/messages/{ctx.guild.id}/requests/{id}/', headers=self.bot.auth_header, json=data) + if delete_resp.status == 202: + delete_data = await delete_resp.json() + if delete_data['completed']: + await ctx.send(f'Request {id} closed.') + await requestor.send(f'{ctx.author.display_name} has closed request {id} which was ' + f'opened by you in the ' + f'{ctx.guild.get_channel(int(request["channel"])).name} ' + f'channel.' + f'```{request["content"]}```' + f'If there are any issues please open a new request.') + else: + await ctx.send('That is not your request to close.') + + +def setup(bot): + bot.add_cog(Tickets(bot)) diff --git a/geeksbot/imports/message_logging.py b/geeksbot/imports/message_logging.py new file mode 100644 index 0000000..375ef99 --- /dev/null +++ b/geeksbot/imports/message_logging.py @@ -0,0 +1,158 @@ +# noinspection PyPackageRequirements +import discord + + +async def on_message(bot, message, user_info): + if not user_info.get('disable_logging'): + if message.guild: + msg_data = { + 'author': str(message.author.id), + 'channel': str(message.channel.id), + 'mention_everyone': message.mention_everyone, + 'created_at': message.created_at + } + if message.mentions: + msg_data['mentions'] = [str(user.id) for user in message.mentions] + if message.channel_mentions: + msg_data['channel_mentions'] = [str(channel.id) for channel in message.channel_mentions] + if message.role_mentions: + msg_data['role_mentions'] = [str(role.id) for role in message.role_mentions] + if message.embeds: + msg_data['embeds'] = [e.to_dict() for e in message.embeds] + if message.content: + msg_data['content'] = message.content + if message.webhook_id: + msg_data['webhook_id'] = message.webhook_id + if message.tts: + msg_data['tts'] = message.tts + if message.attachments: + msg_data['attachments'] = [{ + 'id': str(a.id), + 'size': a.size, + 'filename': a.filename, + 'url': a.url + } for a in message.attachments] + + bot.fs_db.document(f'guilds/{message.guild.id}/messages/{message.id}').set(msg_data) + else: + msg_data = { + 'author': str(message.author.id), + 'created_at': message.created_at + } + if message.mentions: + msg_data['mentions'] = [str(user.id) for user in message.mentions] + if message.embeds: + msg_data['embeds'] = [e.to_dict() for e in message.embeds] + if message.content: + msg_data['content'] = message.content + if message.webhook_id: + msg_data['webhook_id'] = message.webhook_id + if message.tts: + msg_data['tts'] = message.tts + if message.attachments: + msg_data['attachments'] = [{ + 'id': str(a.id), + 'size': a.size, + 'filename': a.filename, + 'url': a.url + } for a in message.attachments] + + bot.fs_db.document(f'dm_channels/{message.channel.id}/messages/{message.id}').set(msg_data) + + +async def on_message_edit(bot, before: discord.Message, after: discord.Message, user_config): + if not user_config.get('disable_logging'): + if after.guild: + msg_ref = bot.fs_db.document(f'guilds/{after.guild.id}/messages/{after.id}') + msg_data = (await bot.loop.run_in_executor(bot.tpe, msg_ref.get)).to_dict() + if before.content != after.content: + if before.content: + if msg_data.get('previous_content') and isinstance(msg_data['previous_content'], list): + msg_data['previous_content'].append(before.content) + else: + msg_data['previous_content'] = [before.content, ] + msg_data['content'] = after.content + if before.embeds != after.embeds: + if before.embeds: + if msg_data.get('previous_embeds') and isinstance(msg_data['previous_embeds'], list): + msg_data['previous_embeds'].append(before.embeds[0].to_dict()) + else: + msg_data['previous_embeds'] = [before.embeds[0].to_dict(), ] + msg_data['embeds'] = [e.to_dict() for e in after.embeds] + if before.pinned != after.pinned: + msg_data['pinned'] = after.pinned + if before.mentions != after.mentions: + msg_data['mentions'] = [str(user.id) for user in after.mentions] + if before.channel_mentions != after.channel_mentions: + msg_data['channel_mentions'] = [str(user.id) for user in after.channel_mentions] + if before.role_mentions != after.role_mentions: + msg_data['role_mentions'] = [str(user.id) for user in after.role_mentions] + if before.attachments != after.attachments: + if before.attachments: + if msg_data.get('previous_attachments') and isinstance(msg_data['previous_attachments'], list): + msg_data['previous_attachments'].append([{ + 'id': str(a.id), + 'size': a.size, + 'filename': a.filename, + 'url': a.url + } for a in before.attachments]) + else: + msg_data['previous_attachments'] = [[{ + 'id': a.id, + 'size': a.size, + 'filename': a.filename, + 'url': a.url + } for a in before.attachments], ] + msg_data['attachments'] = [{ + 'id': a.id, + 'size': a.size, + 'filename': a.filename, + 'url': a.url + } for a in after.attachments] + + bot.fs_db.document(f'guilds/{after.guild.id}/messages/{after.id}').set(msg_data) + else: + msg_ref = bot.fs_db.document(f'dm_channels/{after.channel.id}/messages/{after.id}') + msg_data = (await bot.loop.run_in_executor(bot.tpe, msg_ref.get)).to_dict() + if before.content != after.content: + if before.content: + if msg_data.get('previous_content') and isinstance(msg_data['previous_content'], list): + msg_data['previous_content'].append(before.content) + else: + msg_data['previous_content'] = [before.content, ] + msg_data['content'] = after.content + if before.embeds != after.embeds: + if before.embeds: + if msg_data.get('previous_embeds') and isinstance(msg_data['previous_embeds'], list): + msg_data['previous_embeds'].append(before.embeds[0].to_dict()) + else: + msg_data['previous_embeds'] = [before.embeds[0].to_dict(), ] + msg_data['embeds'] = [e.to_dict() for e in after.embeds] + if before.pinned != after.pinned: + msg_data['pinned'] = after.pinned + if before.mentions != after.mentions: + msg_data['mentions'] = [str(user.id) for user in after.mentions] + if before.attachments != after.attachments: + if before.attachments: + if msg_data.get('previous_attachments') and isinstance(msg_data['previous_attachments'], list): + msg_data['previous_attachments'].append([{ + 'id': str(a.id), + 'size': a.size, + 'filename': a.filename, + 'url': a.url + } for a in before.attachments]) + else: + msg_data['previous_attachments'] = [[{ + 'id': a.id, + 'size': a.size, + 'filename': a.filename, + 'url': a.url + } for a in before.attachments], ] + msg_data['attachments'] = [{ + 'id': a.id, + 'size': a.size, + 'filename': a.filename, + 'url': a.url + } for a in after.attachments] + + bot.fs_db.document(f'dm_channels/{after.channel.id}/messages/{after.id}').set(msg_data) diff --git a/geeksbot/imports/utils.py b/geeksbot/imports/utils.py index cd204b1..08c6e75 100644 --- a/geeksbot/imports/utils.py +++ b/geeksbot/imports/utils.py @@ -84,7 +84,8 @@ class Paginator: field_name_char: str = '\uFFF6', inline_char: str = '\uFFF5', max_line_length: int = 100, - embed=False): + embed=False, + header: str = ''): _max_len = 6000 if embed else 1980 assert 0 < max_lines <= max_chars @@ -110,6 +111,7 @@ class Paginator: self._embed_thumbnail = None self._embed_url = None self._bot = bot + self._header = header def set_embed_meta(self, title: str = None, description: str = None, @@ -129,7 +131,7 @@ class Paginator: self._embed_thumbnail = thumbnail self._embed_url = url - def pages(self) -> typing.List[str]: + def pages(self, page_headers: bool = True) -> typing.List[str]: _pages = list() _fields = list() _page = '' @@ -138,10 +140,16 @@ class Paginator: _field_value = '' _inline = False - def open_page(): + def open_page(initial: bool = False): nonlocal _page, _lines, _fields if not self._embed: - _page = self._prefix + if initial and not page_headers: + _page = self._header + elif page_headers: + _page = self._header + else: + _page = '' + _page += self._prefix _lines = 0 else: _fields = list() @@ -156,7 +164,7 @@ class Paginator: _pages.append(_fields) open_page() - open_page() + open_page(initial=True) if not self._embed: for part in [str(p) for p in self._parts]: @@ -254,6 +262,9 @@ class Paginator: # noinspection PyProtectedMember return self.__class__ == other.__class__ and self._parts == other._parts + def set_header(self, header: str = ''): + self._header = header + def add_page_break(self, *, to_beginning: bool = False) -> None: self.add(self._page_break, to_beginning=to_beginning) diff --git a/geeksbot_v2/channels/api_urls.py b/geeksbot_v2/channels/api_urls.py index 0b54f91..63a1fdf 100644 --- a/geeksbot_v2/channels/api_urls.py +++ b/geeksbot_v2/channels/api_urls.py @@ -1,9 +1,10 @@ from django.urls import path -from .views import ChannelsAPI, ChannelDetail +from .views import ChannelsAPI, ChannelDetail, AdminChannelAPI app_name = "channels_api" urlpatterns = [ path("", view=ChannelsAPI.as_view(), name="list"), - path("/", view=ChannelDetail.as_view(), name='detail') + path("/", view=ChannelDetail.as_view(), name='detail'), + path("/admin/", view=AdminChannelAPI.as_view(), name='admin') ] diff --git a/geeksbot_v2/channels/models.py b/geeksbot_v2/channels/models.py index 6c216fc..244c603 100644 --- a/geeksbot_v2/channels/models.py +++ b/geeksbot_v2/channels/models.py @@ -31,7 +31,7 @@ class Channel(models.Model): self.new_patron = data.get('new_patron') if data.get('admin'): self.admin = data.get('admin') - + self.save() return self @@ -61,16 +61,25 @@ class Channel(models.Model): return create_success_response(channel, status.HTTP_201_CREATED, many=False) @classmethod - def get_channel_by_id(cls, id): + def get_channel_by_id(cls, guild_id, channel_id): try: - return cls.objects.get(id=id) + return cls.get_guild_channels(guild_id).get(id=channel_id) except ObjectDoesNotExist: return None - + @classmethod def get_guild_channels(cls, guild): if isinstance(guild, Guild): return cls.objects.filter(guild=guild) + elif isinstance(guild, (str, int)): + return cls.objects.filter(guild__id=guild) + + @classmethod + def get_admin_channel(cls, guild_id): + try: + return cls.get_guild_channels(guild_id).get(admin=True) + except ObjectDoesNotExist: + return None def __str__(self): return str(id) diff --git a/geeksbot_v2/channels/views.py b/geeksbot_v2/channels/views.py index 2edf1f5..ded5d42 100644 --- a/geeksbot_v2/channels/views.py +++ b/geeksbot_v2/channels/views.py @@ -16,25 +16,53 @@ from .utils import create_success_response class ChannelsAPI(PaginatedAPIView): permission_classes = [IsAuthenticated] - def get(self, request, format=None): - guilds = Channel.objects.all() - page = self.paginate_queryset(guilds) + def get(self, request, guild_id, format=None): + channels = Channel.get_guild_channels(guild_id) + page = self.paginate_queryset(channels) 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) + return create_success_response(channels, status.HTTP_200_OK, many=True) def post(self, request, format=None): data = dict(request.data) return Channel.add_new_channel(data) +class AdminChannelAPI(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request, guild_id, format=None): + channel = Channel.get_admin_channel(guild_id) + if channel: + return create_success_response(channel, status=status.HTTP_200_OK) + return create_error_response('There is no admin channel configured for that guild', + status=status.HTTP_404_NOT_FOUND) + + def put(self, request, guild_id, format=None): + data = dict(request.data) + channel = Channel.get_channel_by_id(guild_id, data['channel']) + if channel: + channel = channel.update_channel({'admin': True}) + return create_success_response(channel, status=status.HTTP_202_ACCEPTED) + return create_error_response("That channel does not exist", + status=status.HTTP_404_NOT_FOUND) + + def delete(self, request, guild_id, format=None): + channel = Channel.get_admin_channel(guild_id) + if channel: + channel = channel.update_channel({'admin': False}) + return create_success_response(channel, status=status.HTTP_202_ACCEPTED) + return create_error_response("There is no admin channel configured", + status=status.HTTP_404_NOT_FOUND) + + class ChannelDetail(APIView): permission_classes = [IsAuthenticated] - def get(self, request, id, format=None): + def get(self, request, guild_id, channel_id, format=None): try: - guild = Channel.objects.get(id=id) + guild = Channel.get_channel_by_id(guild_id, channel_id) except ObjectDoesNotExist: return create_error_response("Channel Does not Exist", status=status.HTTP_404_NOT_FOUND) @@ -42,8 +70,8 @@ class ChannelDetail(APIView): return create_success_response(guild, status=status.HTTP_200_OK) - def put(self, request, id, format=None): - channel = Channel.get_channel_by_id(id) + def put(self, request, guild_id, channel_id, format=None): + channel = Channel.get_channel_by_id(guild_id, channel_id) if channel: data = dict(request.data) @@ -54,14 +82,14 @@ class ChannelDetail(APIView): return create_error_response('Channel Does Not Exist', status=status.HTTP_404_NOT_FOUND) - def delete(self, request, id, format=None): - guild = Channel.get_guild_by_id(id) + def delete(self, request, guild_id, channel_id, format=None): + channel = Channel.get_channel_by_id(guild_id, channel_id) - if guild: + if channel: # data = dict(request.data) # TODO Add a check to verify user is allowed to delete... # Possibly in object permissions... - guild.delete() + channel.delete() return create_success_response(guild, status=status.HTTP_200_OK) else: diff --git a/geeksbot_v2/config/settings/base.py b/geeksbot_v2/config/settings/base.py index 460b496..c03fc31 100644 --- a/geeksbot_v2/config/settings/base.py +++ b/geeksbot_v2/config/settings/base.py @@ -24,7 +24,7 @@ DEBUG = env.bool("DJANGO_DEBUG", False) # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name # though not all of them may be available with every OS. # In Windows, this must be set to your system time zone. -TIME_ZONE = "America/Anchorage" +TIME_ZONE = "UTC" # https://docs.djangoproject.com/en/dev/ref/settings/#language-code LANGUAGE_CODE = "en-us" # https://docs.djangoproject.com/en/dev/ref/settings/#site-id diff --git a/geeksbot_v2/config/urls.py b/geeksbot_v2/config/urls.py index 1eb6ea8..80663f0 100644 --- a/geeksbot_v2/config/urls.py +++ b/geeksbot_v2/config/urls.py @@ -20,6 +20,7 @@ urlpatterns = [ path("api/guilds/", include("geeksbot_v2.guilds.api_urls", namespace="guilds_api")), path("api/channels/", include("geeksbot_v2.channels.api_urls", namespace="channels_api")), path("api/messages/", include("geeksbot_v2.dmessages.api_urls", namespace="messages_api")), + path("api/rcon/", include("geeksbot_v2.rcon.api_urls", namespace="rcon_api")), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) if settings.DEBUG: diff --git a/geeksbot_v2/dmessages/api_urls.py b/geeksbot_v2/dmessages/api_urls.py index db96aef..5891225 100644 --- a/geeksbot_v2/dmessages/api_urls.py +++ b/geeksbot_v2/dmessages/api_urls.py @@ -2,14 +2,20 @@ from django.urls import path from .views import MessageDetailAPI, MessagesAPI from .views import RequestDetailAPI, RequestsAPI -from .views import CommentDetailAPI, CommentsAPI +from .views import CommentDetailAPI, CommentsAPI, CommentsCountAPI +from .views import WaitForMessageAPI +from .views import UserRequestsAPI -app_name = "channels_api" +app_name = "messages_api" urlpatterns = [ path("", view=MessagesAPI.as_view(), name="message_list"), path("/", view=MessageDetailAPI.as_view(), name='message_detail'), - path("requests/", view=RequestsAPI.as_view(), name="requests_list"), - path("requests//", view=RequestDetailAPI.as_view(), name='request_detail'), - path("requests//comments/", view=CommentsAPI.as_view(), name="comments_list"), - path("requests//comments//", view=CommentDetailAPI.as_view(), name='comment_detail'), + path("/requests/", view=RequestsAPI.as_view(), name="requests_list"), + path("/requests//", view=RequestDetailAPI.as_view(), name='request_detail'), + path("/requests//comments/", view=CommentsAPI.as_view(), name="comments_list"), + path("/requests//comments/count/", view=CommentsCountAPI.as_view(), name="comments_count"), + path("/requests//comments//", view=CommentDetailAPI.as_view(), name='comment_detail'), + path("/requests/user//", view=UserRequestsAPI.as_view(), name='user_requests_list'), + path("/wait/", view=WaitForMessageAPI.as_view(), name='wait_for_message'), + path("/wait//", view=WaitForMessageAPI.as_view(), name='wait_for_message_timeout'), ] diff --git a/geeksbot_v2/dmessages/models.py b/geeksbot_v2/dmessages/models.py index 4f9d863..e2ed99c 100644 --- a/geeksbot_v2/dmessages/models.py +++ b/geeksbot_v2/dmessages/models.py @@ -57,7 +57,7 @@ class Message(models.Model): 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) + channel = Channel.get_channel_by_id(guild_id, channel_id) if not isinstance(channel, Channel): return create_error_response("Channel Does Not Exist", status=status.HTTP_404_NOT_FOUND) @@ -89,7 +89,7 @@ class Message(models.Model): 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) + channel = Channel.get_channel_by_id(guild_id, channel_id) if channel: message.tagged_channels.add(channel) @@ -179,8 +179,9 @@ class AdminRequest(models.Model): def update_request(self, data): completed = data.get('completed', False) completed_by_id = data.get('completed_by') - completed_message = data.get('message') + completed_message = data.get('message', '') if not self.completed and completed: + self.completed = completed self.completed_at = datetime.utcnow() self.completed_message = completed_message user = User.get_user_by_id(completed_by_id) @@ -192,8 +193,7 @@ class AdminRequest(models.Model): return create_request_success_response(self, status.HTTP_202_ACCEPTED) @classmethod - def add_new_request(cls, data): - guild_id = data.get('guild') + def add_new_request(cls, guild_id, data): author_id = data.get('author') message_id = data.get('message') channel_id = data.get('channel') @@ -205,7 +205,7 @@ class AdminRequest(models.Model): 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) + 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) @@ -213,11 +213,13 @@ class AdminRequest(models.Model): 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) + channel = Channel.get_channel_by_id(guild_id, channel_id) if not isinstance(channel, Channel): return create_error_response('Channel Does Not Exist', status=status.HTTP_404_NOT_FOUND) + print('test') + request = cls( guild=guild, author=author, @@ -233,15 +235,19 @@ class AdminRequest(models.Model): return cls.objects.filter(guild__id=guild_id).filter(completed=False) @classmethod - def get_request_by_id(cls, id): + def get_open_request_by_id(cls, guild_id, request_id): try: - return cls.objects.get(id=id) + return cls.get_open_requests_by_guild(guild_id).get(id=request_id) except ObjectDoesNotExist: return None def __str__(self): return f"{self.guild.id} | {self.requested_at} | By {self.author.id}" + @classmethod + def get_open_requests_by_guild_author(cls, guild_id, author_id): + return cls.get_open_requests_by_guild(guild_id).filter(author__id=author_id) + class AdminComment(models.Model): request = models.ForeignKey(AdminRequest, on_delete=models.CASCADE) @@ -250,13 +256,13 @@ class AdminComment(models.Model): updated_at = models.DateTimeField(auto_now_add=True, blank=True) @classmethod - def add_new_comment(cls, data, request_id): + def add_new_comment(cls, data, guild_id, request_id): 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) + request = AdminRequest.get_open_request_by_id(guild_id, request_id) if not isinstance(request, AdminRequest): return create_error_response("Admin Request Does Not Exist", status=status.HTTP_404_NOT_FOUND) @@ -274,12 +280,12 @@ class AdminComment(models.Model): return create_comment_success_response(comment, status.HTTP_201_CREATED, many=False) @classmethod - def get_comment_by_id(cls, id): + def get_comment_by_id(cls, comment_id): try: - return cls.objects.get(id=id) + return cls.objects.get(id=comment_id) except ObjectDoesNotExist: return None - + @classmethod def get_comments_by_request(cls, request): - return cls.objects.filter(request=request) + return cls.objects.filter(request=request).order_by('updated_at') diff --git a/geeksbot_v2/dmessages/views.py b/geeksbot_v2/dmessages/views.py index f3c80ba..c18433a 100644 --- a/geeksbot_v2/dmessages/views.py +++ b/geeksbot_v2/dmessages/views.py @@ -1,3 +1,6 @@ +from time import sleep +from datetime import datetime + from rest_framework.views import APIView from rest_framework import status from rest_framework.permissions import IsAuthenticated @@ -59,27 +62,57 @@ class MessageDetailAPI(APIView): status=status.HTTP_404_NOT_FOUND) +class WaitForMessageAPI(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request, id, timeout: int = 3, format=None): + message = Message.get_message_by_id(id) + try_count = 0 + while not message: + sleep(0.1) + try_count += 1 + if try_count > timeout * 10: + return create_error_response("Timeout reached before message is available.", + statu=status.HTTP_404_NOT_FOUND) + message = Message.get_message_by_id(id) + return create_success_response(message, status=status.HTTP_200_OK) + + class RequestsAPI(PaginatedAPIView): permission_classes = [IsAuthenticated] - def get(self, request, guild, format=None): - requests = AdminRequest.get_open_requests_by_guild(guild) + def get(self, request, guild_id, format=None): + requests = AdminRequest.get_open_requests_by_guild(guild_id) page = self.paginate_queryset(requests) if page is not None: return create_request_success_response(page, status.HTTP_200_OK, many=True) + if requests: + return create_request_success_response(requests, status.HTTP_200_OK, many=True) + return create_error_response("No requests found") - return create_request_success_response(requests, status.HTTP_200_OK, many=True) - - def post(self, request, format=None): + def post(self, request, guild_id, format=None): data = dict(request.data) - return AdminRequest.add_new_request(data) + return AdminRequest.add_new_request(guild_id, data) + + +class UserRequestsAPI(PaginatedAPIView): + permission_classes = [IsAuthenticated] + + def get(self, request, guild_id, author_id, format=None): + requests = AdminRequest.get_open_requests_by_guild_author(guild_id, author_id) + page = self.paginate_queryset(requests) + if page is not None: + return create_request_success_response(page, status.HTTP_200_OK, many=True) + if requests: + return create_request_success_response(requests, status.HTTP_200_OK, many=True) + return create_error_response("No requests found") class RequestDetailAPI(APIView): permission_classes = [IsAuthenticated] - def get(self, req, id, format=None): - req = AdminRequest.get_request_by_id(id) + def get(self, req, guild_id, request_id, format=None): + req = AdminRequest.get_open_request_by_id(guild_id, request_id) if req: comments = AdminComment.get_comments_by_request(req) if comments: @@ -92,21 +125,44 @@ class RequestDetailAPI(APIView): return create_error_response("That Request Does Not Exist", status=status.HTTP_404_NOT_FOUND) - def put(self, request, id, format=None): - req = AdminRequest.get_request_by_id(id) + def put(self, request, guild_id, request_id, format=None): + req = AdminRequest.get_open_request_by_id(guild_id, request_id) if req: data = dict(request.data) return req.update_request(data) return create_error_response("That Request Does Not Exist", status=status.HTTP_404_NOT_FOUND) + def delete(self, request, guild_id, request_id, format=None): + data = dict(request.data) + request = AdminRequest.get_open_request_by_id(guild_id, request_id) + data['completed'] = True + data['completed_at'] = datetime.utcnow() + return request.update_request(data) + class CommentsAPI(PaginatedAPIView): permissions_classes = [IsAuthenticated] - def post(self, request, request_id, format=None): + def get(self, request, guild_id, request_id, format=None): + comments = AdminComment.get_comments_by_request(request_id) + if comments: + return create_comment_success_response(comments, status=status.HTTP_200_OK, many=True) + return create_error_response("No Comments found") + + def post(self, request, guild_id, request_id, format=None): data = dict(request.data) - return AdminComment.add_new_comment(data, request_id) + return AdminComment.add_new_comment(data, guild_id, request_id) + + +class CommentsCountAPI(PaginatedAPIView): + permissions_classes = [IsAuthenticated] + + def get(self, request, guild_id, request_id, format=None): + comments = AdminComment.get_comments_by_request(request_id) + if comments: + return Response(len(comments), status=status.HTTP_200_OK) + return Response(0, status.HTTP_200_OK) class CommentDetailAPI(APIView): diff --git a/geeksbot_v2/guilds/api_urls.py b/geeksbot_v2/guilds/api_urls.py index a086d0d..6b2f3d7 100644 --- a/geeksbot_v2/guilds/api_urls.py +++ b/geeksbot_v2/guilds/api_urls.py @@ -2,11 +2,13 @@ from django.urls import path from .views import GuildsAPI, GuildDetail from .views import RolesAPI, RoleDetailAPI +from .views import AdminRolesAPI app_name = "guilds_api" urlpatterns = [ path("", view=GuildsAPI.as_view(), name="list"), path("/", view=GuildDetail.as_view(), name='detail'), path("/roles/", view=RolesAPI.as_view(), name="list"), + path("/roles/admin/", view=AdminRolesAPI.as_view(), name='admin'), path("/roles//", view=RoleDetailAPI.as_view(), name='detail'), ] diff --git a/geeksbot_v2/guilds/models.py b/geeksbot_v2/guilds/models.py index 07ed01d..56afbac 100644 --- a/geeksbot_v2/guilds/models.py +++ b/geeksbot_v2/guilds/models.py @@ -73,7 +73,7 @@ class Role(models.Model): self.role_type = data.get('role_type') self.save() - return create_role_success_response(self, status=status.HTTP_202_ACCEPTED, many=False) + return self @classmethod def add_new_role(cls, guild_id, data): @@ -112,9 +112,9 @@ class Role(models.Model): return create_role_success_response(role, status.HTTP_201_CREATED, many=False) @classmethod - def get_role_by_id(cls, id): + def get_role_by_id(cls, guild_id, role_id): try: - return cls.objects.get(id=id) + return cls.get_guild_roles(guild_id).get(id=role_id) except ObjectDoesNotExist: return None @@ -122,5 +122,12 @@ class Role(models.Model): def get_guild_roles(cls, guild): return cls.objects.filter(guild__id=guild) + @classmethod + def get_admin_roles(cls, guild_id): + try: + return cls.get_guild_roles(guild_id).filter(role_type__gte=90) + except ObjectDoesNotExist: + return None + def __str__(self): return f"{self.guild.id} | {self.id}" diff --git a/geeksbot_v2/guilds/views.py b/geeksbot_v2/guilds/views.py index ca64476..e461771 100644 --- a/geeksbot_v2/guilds/views.py +++ b/geeksbot_v2/guilds/views.py @@ -87,6 +87,26 @@ class RolesAPI(PaginatedAPIView): return Role.add_new_role(guild_id, data) +class AdminRolesAPI(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request, guild_id, format=None): + roles = Role.get_admin_roles(guild_id) + if roles: + return create_role_success_response(roles, status=status.HTTP_200_OK, many=True) + return create_error_response('There are no admin roles configured', + status=status.HTTP_404_NOT_FOUND) + + def put(self, request, guild_id, format=None): + data = dict(request.data) + role = Role.get_role_by_id(guild_id, data['role']) + if role: + role = role.update_role({'role_type': 100}) + return create_role_success_response(role, status=status.HTTP_202_ACCEPTED) + return create_error_response("That role does not exist", + status=status.HTTP_404_NOT_FOUND) + + class RoleDetailAPI(APIView): permission_classes = [IsAuthenticated] @@ -101,7 +121,7 @@ class RoleDetailAPI(APIView): status=status.HTTP_200_OK) def put(self, request, guild_id, id, format=None): - role = Role.get_role_by_id(id) + role = Role.get_role_by_id(guild_id, id) if role: data = dict(request.data) diff --git a/geeksbot_v2/patreon/patron.py b/geeksbot_v2/patreon/patron.py new file mode 100644 index 0000000..6a4e430 --- /dev/null +++ b/geeksbot_v2/patreon/patron.py @@ -0,0 +1,77 @@ +import discord +import gspread +from oauth2client.service_account import ServiceAccountCredentials + + +class Patron: + def __init__(self, *, discord_name: str=None, steam_id: int=None, patreon_tier: str=None, patron_of: str=None, + discord_discrim: int=None, discord_id: int=None, patreon_name: str=None, steam_name: str=None): + self.discord_name = discord_name + self.discord_discrim = discord_discrim + self.steam_id = steam_id + self.discord_id = discord_id + self.patreon_tier = patreon_tier + self.patron_of = patron_of + self.patreon_name = patreon_name + self.steam_name = steam_name + + @classmethod + async def from_id(cls, bot, steam_id: int, *, discord_id: int=None): + scope = ['https://spreadsheets.google.com/feeds', + 'https://www.googleapis.com/auth/drive'] + credentials = ServiceAccountCredentials.from_json_keyfile_dict(bot.google_secret, scope) + + gc = gspread.authorize(credentials) + sh = gc.open_by_key(bot.bot_secrets['sheet']) + ws = sh.worksheet('Current Whitelist') + try: + cell = ws.find(f'{steam_id}') + except gspread.CellNotFound: + return -1 + else: + steam_name = None + if discord_id: + user_ref = bot.fs_db.document(f'users/{discord_id}') + user_info = (await bot.loop.run_in_executor(bot.tpe, user_ref.get)).to_dict() + if user_info: + steam_name = user_info.get('steam_name') + row = ws.row_values(cell.row) + return cls(patreon_name=row[1], + discord_name=row[2], + steam_id=row[5], + patreon_tier=row[4].split(' (')[1].strip(')') if len(row[4].split(' (')) > 1 else row[4], + patron_of=row[3].split(' (')[0], + discord_id=discord_id, + steam_name=steam_name) + + @classmethod + async def from_name(cls, bot, discord_name: discord.Member, *, discord_id: int=None): + scope = ['https://spreadsheets.google.com/feeds', + 'https://www.googleapis.com/auth/drive'] + credentials = ServiceAccountCredentials.from_json_keyfile_dict(bot.google_secret, scope) + + gc = gspread.authorize(credentials) + sh = gc.open_by_key(bot.bot_secrets['sheet']) + ws = sh.worksheet('Current Whitelist') + try: + cell = ws.find(f'{discord_name.name if isinstance(discord_name, discord.Member) else discord_name}') + except gspread.CellNotFound: + try: + cell = ws.find(f'{discord_name.nick if isinstance(discord_name, discord.Member) else discord_name}') + except gspread.CellNotFound: + return -1 + steam_name = None + discord_id = discord_name.id if isinstance(discord_name, discord.Member) else discord_id + if discord_id: + user_ref = bot.fs_db.document(f'users/{discord_id}') + user_info = (await bot.loop.run_in_executor(bot.tpe, user_ref.get)).to_dict() + if user_info: + steam_name = user_info.get('steam_name') + row = ws.row_values(cell.row) + return cls(patreon_name=row[1], + discord_name=row[2], + discord_id=discord_id, + steam_id=row[5], + patreon_tier=row[4].split(' (')[1].strip(')') if len(row[4].split(' (')) > 1 else row[4], + patron_of=row[3].split(' (')[0], + steam_name=steam_name) diff --git a/geeksbot_v2/rcon/api_urls.py b/geeksbot_v2/rcon/api_urls.py new file mode 100644 index 0000000..6496d3c --- /dev/null +++ b/geeksbot_v2/rcon/api_urls.py @@ -0,0 +1,10 @@ +from django.urls import path + +from .views import RCONServersAPI, RCONServerDetailAPI, ListPlayers + +app_name = "rcon_api" +urlpatterns = [ + path("/", view=RCONServersAPI.as_view(), name='guild_servers'), + path("//", view=RCONServerDetailAPI.as_view(), name="server_detail"), + path("//listplayers", view=ListPlayers.as_view(), name='listplayers'), +] diff --git a/geeksbot_v2/rcon/models.py b/geeksbot_v2/rcon/models.py index 42c9bca..fc0ed3b 100644 --- a/geeksbot_v2/rcon/models.py +++ b/geeksbot_v2/rcon/models.py @@ -120,7 +120,7 @@ class RconServer(models.Model): @classmethod def get_guild_servers(cls, guild_id): guild = Guild.get_guild_by_id(guild_id) - if not isinstance(guild, guild): + if not isinstance(guild, Guild): return None return cls.objects.filter(guild=guild) diff --git a/geeksbot_v2/rcon/rcon_lib/__init__.py b/geeksbot_v2/rcon/rcon_lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/geeksbot_v2/rcon/rcon_lib/arcon.py b/geeksbot_v2/rcon/rcon_lib/arcon.py new file mode 100644 index 0000000..71d776a --- /dev/null +++ b/geeksbot_v2/rcon/rcon_lib/arcon.py @@ -0,0 +1,118 @@ +from . import rcon +import asyncio +from typing import Union +import logging + +arcon_log = logging.getLogger('arcon_lib') + + +class ARKServer(rcon.RCONConnection): + def __init__(self, *args, monitor_chat: bool=False, server_chat_channel: int=None, + server_messages_channel: int=None, **kwargs): + self.monitor_chat = monitor_chat + self.server_chat_channel = server_chat_channel + self.server_messages_channel = server_messages_channel + super().__init__(*args, **kwargs) + + async def run_command(self, command: str, multi_packet: bool=False, reconnect_counter: int=0) \ + -> Union[rcon.RCONPacket, str]: + arcon_log.debug(f'Command requested: {command}') + if self.authenticated: + packet = rcon.RCONPacket(next(self.packet_id), rcon.SERVERDATA_EXECCOMMAND, command) + with await self.lock: + try: + arcon_log.debug(f'Sending packet {packet.packet_id}') + await self.send_packet(packet) + arcon_log.debug(f'Packet Sent.') + except ConnectionResetError: + arcon_log.info(f'Connection to {self.host}:{self.port} lost, Reconnecting...') + self.lock.release() + await self._reconnect_and_resend(packet) + await self.lock.acquire() + finally: + arcon_log.debug(f'Waiting for response to packet {packet.packet_id}') + try: + response = await self.read(packet, multi_packet=multi_packet) + except asyncio.TimeoutError as e: + if reconnect_counter > 5: + return 'Reached max reconnects. Closing connection.' + arcon_log.warning(f'No response received: {e}\nAttempting to reconnect #{reconnect_counter}') + self.lock.release() + await self._reconnect() + await self.lock.acquire() + response = await self.run_command(command=command, multi_packet=multi_packet, + reconnect_counter=reconnect_counter + 1) + arcon_log.debug(f'Response Received:\n{response.packet_type}:{response.packet_id}:{response.body}') + response.body = response.body.strip('\x00\x00').strip() + return response + else: + return 'Server is not Authenticated. Please let the Admin know of this issue.' + + async def getchat(self) -> str: + response = await self.run_command(command='getchat', multi_packet=True) + return response.body if isinstance(response, rcon.RCONPacket) else response + + async def saveworld(self) -> str: + response = await self.run_command(command='saveworld') + return response.body if isinstance(response, rcon.RCONPacket) else response + + async def serverchat(self, message: str) -> str: + response = await self.run_command(command=f'serverchat {message}') + return response.body if isinstance(response, rcon.RCONPacket) else response + + async def broadcast(self, message: str) -> str: + response = await self.run_command(command=f'broadcast {message}') + return response.body if isinstance(response, rcon.RCONPacket) else response + + async def listplayers(self) -> str: + response = await self.run_command(command=f'listplayers') + return response.body if isinstance(response, rcon.RCONPacket) else response + + async def whitelist(self, steam_id: str) -> str: + response = await self.run_command(command=f'AllowPlayerToJoinNoCheck {steam_id}') + return response.body if isinstance(response, rcon.RCONPacket) else response + + async def ban_player(self, steam_id: int) -> str: + response = await self.run_command(command=f'BanPlayer {steam_id}') + return response.body if isinstance(response, rcon.RCONPacket) else response + + async def unban_player(self, steam_id: int) -> str: + response = await self.run_command(command=f'UnbanPlayer {steam_id}') + return response.body if isinstance(response, rcon.RCONPacket) else response + + async def kick_player(self, steam_id: int) -> str: + response = await self.run_command(command=f'KickPlayer {steam_id}') + return response.body if isinstance(response, rcon.RCONPacket) else response + + async def stop_server(self) -> int: + saved = await self.saveworld() + if saved == 'World Saved': + await self.serverchat(saved) + await asyncio.sleep(10) + response = await self.run_command(command='DoExit') + if response.body == 'Exiting...': + return 0 + else: + return 2 + else: + return 1 + + async def get_logs(self): + response = await self.run_command(command=f'GetGameLog', multi_packet=True) + return response.body if isinstance(response, rcon.RCONPacket) else response + + async def server_chat_to_steam_id(self, steam_id: int, message: str) -> str: + response = await self.run_command(command=f'ServerChatTo {steam_id} {message}') + return response.body if isinstance(response, rcon.RCONPacket) else response + + async def server_chat_to_player_name(self, player_name: str, message: str) -> str: + response = await self.run_command(command=f'ServerChatToPlayer "{player_name}" {message}') + return response.body if isinstance(response, rcon.RCONPacket) else response + + async def set_time_of_day(self, hour: int, minute: int=00, seconds: int=00) -> str: + response = await self.run_command(command=f'SetTimeOfDay {hour}:{minute}:{seconds}') + return response.body if isinstance(response, rcon.RCONPacket) else response + + async def destroy_wild_dinos(self): + response = await self.run_command(command='DestroyWildDinos') + return response.body if isinstance(response, rcon.RCONPacket) else response diff --git a/geeksbot_v2/rcon/rcon_lib/rcon.py b/geeksbot_v2/rcon/rcon_lib/rcon.py new file mode 100644 index 0000000..deeafee --- /dev/null +++ b/geeksbot_v2/rcon/rcon_lib/rcon.py @@ -0,0 +1,183 @@ +import asyncio +import logging +import itertools +import struct + +# Packet types +SERVERDATA_AUTH = 3 +SERVERDATA_AUTH_RESPONSE = 2 +SERVERDATA_EXECCOMMAND = 2 +SERVERDATA_RESPONSE_VALUE = 0 + +__all__ = ['RCONPacket', 'RCONConnection'] + +rcon_log = logging.getLogger('rcon_lib') + + +class RCONPacket: + def __init__(self, packet_id: int=0, packet_type: int=-1, body: str=''): + self.packet_id = packet_id + self.packet_type = packet_type + self.body = body + + def __str__(self): + """Return the body of the packet""" + return self.body + + def size(self): + """Return the size of the packet""" + return len(self.body) + 10 + + def pack(self): + """Return the packed packet""" + return struct.pack(f'<3i{len(self.body) + 2}s', + self.size(), + self.packet_id, + self.packet_type, + bytearray(self.body, 'utf-8')) + + +class RCONConnection: + """Connection to an RCON server""" + + def __init__(self, host: str, port: int, password: str='', single_packet: bool=False, loop=None): + """Create a New RCON Connection + + Parameters: + host (str): The hostname or IP address of the server to connect to + port (int): The port to connect to on the server + password (str): The password to authenticate with the server + single_packet (bool): True for servers who don't give 0 length SERVERDATA_RESPONSE_VALUE requests + """ + + self.host = host + self.port = port + self.password = password + self.single_packet = single_packet + self.packet_id = itertools.count(1) + self.loop = loop or asyncio.get_event_loop() + self.reader = None + self.writer = None + self.lock = asyncio.Lock() + self.authenticated = False + + async def connect(self): + """Returns -1 if connection times out + Returns 1 if connection and auth are successful + Returns 0 if auth fails""" + try: + rcon_log.debug(f'Connecting to {self.host}:{self.port}...') + self.reader, self.writer = await asyncio.open_connection(self.host, self.port, loop=self.loop) + except TimeoutError as e: + rcon_log.error(f'Timeout error: {e}') + return -1 + else: + rcon_log.debug('Connected. Attempting to Authenticate...') + auth_packet = RCONPacket(next(self.packet_id), SERVERDATA_AUTH, self.password) + with await self.lock: + await self.send_packet(auth_packet) + response = await self.read() + if response.packet_type == SERVERDATA_AUTH_RESPONSE and response.packet_id != -1: + rcon_log.debug(f'Authorized {response.packet_type}:{response.packet_id}:{response.body}') + self.authenticated = True + return 1 + else: + rcon_log.debug(f'Not Authorized {response.packet_type}:{response.packet_id}:{response.body}') + self.authenticated = False + return 0 + + async def _reconnect(self): + self.writer = None + self.reader = None + connected = await self.connect() + rcon_log.info(f'Connection completed with a return of {connected}') + if connected != -1: + rcon_log.info('Connected') + else: + rcon_log.warning('Connection Failed') + return connected + + async def _reconnect_and_resend(self, packet): + connected = await self._reconnect() + if connected != -1: + await asyncio.sleep(0.1) + rcon_log.info(f'Re-sending packet {packet.packet_id}') + await self.send_packet(packet) + rcon_log.info(f'Packet Sent.') + return connected + else: + return connected + + async def keep_alive(self): + while True: + await asyncio.sleep(60) + ka_packet = RCONPacket(next(self.packet_id), SERVERDATA_EXECCOMMAND, '') + try: + with await self.lock: + await asyncio.wait_for(self.send_packet(ka_packet), 10, loop=self.loop) + await asyncio.wait_for(self.read(ka_packet), 10, loop=self.loop) + except asyncio.TimeoutError: + self.reader = None + self.writer = None + await self.connect() + + async def send_packet(self, packet): + if packet.size() > 4096: + rcon_log.error('Packet Size is larger than 4096 bytes. Cannot send packet.') + raise RuntimeWarning('Packet Size is larger than 4096 bytes. Cannot send packet.') + if self.writer is None: + await self.connect() + rcon_log.debug(f'Sending Packet {packet.packet_id}: {packet.pack() if packet.packet_type is not SERVERDATA_AUTH else "Censored for Password Security."}') + self.writer.write(packet.pack()) + await self.writer.drain() + rcon_log.debug(f'Packet {packet.packet_id} Sent.') + + async def read(self, request: RCONPacket=None, multi_packet=False) -> RCONPacket: + rcon_log.debug(f'Waiting to receive response to packet {request.packet_id if request else None}') + response = RCONPacket() + try: + if request: + while response.packet_id != request.packet_id and response.packet_id < request.packet_id: + if multi_packet: + if request is None: + rcon_log.warning('A request packet is required to receive a multi packet response') + raise ValueError('A request packet is required to receive a multi packet response') + await asyncio.sleep(.01) + response = await self._receive_multi_packet() + rcon_log.debug(f'Received Multi-Packet response to packet {request.packet_id}:\n' + f'{response.packet_type}:{response.packet_id}:{response.body}') + else: + response = await self.receive_packet() + rcon_log.debug(f'Received Single-Packet response to packet {request.packet_id}:\n' + f'{response.packet_type}:{response.packet_id}:{response.body}') + else: + response = await self.receive_packet() + rcon_log.debug(f'Received Single-Packet response:\n' + f'{response.packet_type}:{response.packet_id}:{response.body}') + except struct.error as e: + rcon_log.error(f'Struct Error: {e}') + response = RCONPacket(body='Error receiving data from the server. Attempting to reconnect. ' + 'Please try again in a little bit.') + self.lock.release() + await self._reconnect() + await self.lock.acquire() + except AttributeError as e: + rcon_log.error(f'Attribute Error: {e}') + response = RCONPacket(body='Error receiving data from the server. Attempting to reconnect. ' + 'Please try again in a little bit.') + self.lock.release() + await self._reconnect() + await self.lock.acquire() + return response + + async def receive_packet(self): + header = await self.reader.read(struct.calcsize('<3i')) + (packet_size, packet_id, packet_type) = struct.unpack('<3i', header) + body = await self.reader.read(packet_size - 8) + return RCONPacket(packet_id, packet_type, body.decode('ascii')) + + async def _receive_multi_packet(self): + header = await self.reader.read(struct.calcsize('<3i')) + (packet_size, packet_id, packet_type) = struct.unpack('<3i', header) + body = await self.reader.readuntil(separator=b'\x00\x00') + return RCONPacket(packet_id, packet_type, body.decode('ascii')) diff --git a/geeksbot_v2/rcon/utils.py b/geeksbot_v2/rcon/utils.py index 9092407..1d78983 100644 --- a/geeksbot_v2/rcon/utils.py +++ b/geeksbot_v2/rcon/utils.py @@ -11,4 +11,9 @@ 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 + status=status) + + +def create_rcon_response(message, status): + msg_list = message.split('\n') + return Response(msg_list, status=status) diff --git a/geeksbot_v2/rcon/views.py b/geeksbot_v2/rcon/views.py index 91ea44a..1d99e76 100644 --- a/geeksbot_v2/rcon/views.py +++ b/geeksbot_v2/rcon/views.py @@ -1,3 +1,78 @@ -from django.shortcuts import render +import asyncio + +from rest_framework.views import APIView +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework import status +from .rcon_lib import arcon + +from .models import RconServer +from .utils import create_error_response, create_success_response, create_rcon_response +from geeksbot_v2.utils.api_utils import PaginatedAPIView +from .serializers import RconServerSerializer # Create your views here. + +# API Views + + +class RCONServersAPI(PaginatedAPIView): + permission_classes = [IsAuthenticated] + + def get(self, request, guild_id, format=None): + servers = RconServer.get_guild_servers(guild_id) + page = self.paginate_queryset(servers) + if page: + return create_success_response(page, status.HTTP_200_OK, many=True) + return create_success_response(servers, status.HTTP_200_OK, many=True) + + def post(self, request, guild_id, format=None): + data = dict(request.data) + data['guild'] = guild_id + return RconServer.add_new_server(data) + + +class RCONServerDetailAPI(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request, guild_id, name, format=None): + server = RconServer.get_server(guild_id, name) + if server: + return create_success_response(server, status.HTTP_200_OK, many=False) + else: + return create_error_response("RCON Server Does Not Exist", + status=status.HTTP_404_NOT_FOUND) + + def put(self, request, guild_id, name, format=None): + data = dict(request.data) + server = RconServer.get_server(guild_id, name) + if server: + return server.update_server(data) + else: + return create_error_response('RCON Server Does Not Exist', + status=status.HTTP_404_NOT_FOUND) + + +class ListPlayers(PaginatedAPIView): + permission_classes = [IsAuthenticated] + + def get(self, request, guild_id, name, format=None): + server: RconServer = RconServer.get_server(guild_id, name) + if server: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop = asyncio.get_event_loop() + ark = arcon.ARKServer(host=server.ip, port=server.port, password=server.password, loop=loop) + connected = loop.run_until_complete(ark.connect()) + if connected == 1: + resp = loop.run_until_complete(ark.listplayers()) + if resp == 'No Players Connected': + return create_rcon_response(resp, status=status.HTTP_204_NO_CONTENT) + else: + return create_rcon_response(resp, status=status.HTTP_200_OK) + else: + return create_error_response('Connection failure', + status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + return create_error_response('RCON Server Does Not Exist', + status=status.HTTP_404_NOT_FOUND) diff --git a/geeksbot_v2/shared_libs/TicTacToe/__init__.py b/geeksbot_v2/shared_libs/TicTacToe/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/geeksbot_v2/shared_libs/TicTacToe/board.py b/geeksbot_v2/shared_libs/TicTacToe/board.py new file mode 100644 index 0000000..35365c6 --- /dev/null +++ b/geeksbot_v2/shared_libs/TicTacToe/board.py @@ -0,0 +1,74 @@ +from src.shared_libs.guid import Guid +from src.shared_libs.TicTacToe.player import Player + + +class Board: + def __init__(self): + self.id = Guid() + self.board = [[' ', ' ', ' '], + [' ', ' ', ' '], + [' ', ' ', ' ']] + self.history = [] + self.winner = False + self.draw = False + self.play_count = 0 + self.remaining_moves = [1, 2, 3, 4, 5, 6, 7, 8, 9] + self.winning_states = [[(0, 0), (0, 1), (0, 2)], + [(0, 0), (1, 1), (2, 2)], + [(1, 0), (1, 1), (1, 2)], + [(2, 0), (2, 1), (2, 2)], + [(0, 0), (1, 0), (2, 0)], + [(0, 1), (1, 1), (2, 1)], + [(0, 2), (1, 2), (2, 2)], + [(2, 0), (1, 1), (0, 2)]] + + def __repr__(self): + return f'' + + def __str__(self): + return '┌───┬───┬───┐\n' \ + '│ {0[0][0]} │ {0[0][1]} │ {0[0][2]} │\n' \ + '├───┼───┼───┤\n' \ + '│ {0[1][0]} │ {0[1][1]} │ {0[1][2]} │\n' \ + '├───┼───┼───┤\n' \ + '│ {0[2][0]} │ {0[2][1]} │ {0[2][2]} │\n' \ + '└───┴───┴───┘\n'.format(self.board) + + def make_play(self, player: Player, position: int): + assert 1 <= position <= 9 + assert isinstance(player, Player) + move = ((position - 1) // 3, (position - 1) % 3) + if not self.board[move[0]][move[1]] == ' ': + raise Warning("That cell is already taken. Please try again.") + self.history.append(self.board) + self.board[move[0]][move[1]] = player + self.play_count += 1 + self.winner = self.check_winner() + self.draw = self.check_draw() + self.remaining_moves.remove(position) + + def check_winner(self): + for state in self.winning_states: + if (self.board[state[0][0]][state[0][1]] == + self.board[state[1][0]][state[1][1]] == + self.board[state[2][0]][state[2][1]]) and \ + self.board[state[0][0]][state[0][1]] != ' ': + return self.board[state[0][0]][state[0][1]] + return False + + def check_draw(self): + for row in self.board: + for cell in row: + if cell == ' ': + return False + else: + return True + + def clear(self): + self.board = [[' ', ' ', ' '], + [' ', ' ', ' '], + [' ', ' ', ' ']] + self.history = [] + self.winner = False + self.play_count = 0 + diff --git a/geeksbot_v2/shared_libs/TicTacToe/player.py b/geeksbot_v2/shared_libs/TicTacToe/player.py new file mode 100644 index 0000000..e90278b --- /dev/null +++ b/geeksbot_v2/shared_libs/TicTacToe/player.py @@ -0,0 +1,195 @@ +from src.shared_libs.guid import Guid +import random +from copy import deepcopy + +__all__ = ['Player', 'AIPlayer'] + + +class Player: + def __init__(self, token: str, *, name: str=None, id: str=None, discord_id: int=None): + if len(token) != 1: + raise Warning('Token must be exactly one character long.') + self.token = token + self.name = name or f'Player {self.token}' + self.id = id or Guid() + self.starting_player = False + self.discord_id = discord_id + + def __repr__(self): + return f'' + + def __str__(self): + return self.token + + def __eq__(self, other): + if isinstance(other, Player) and other.id == self.id: + return True + elif isinstance(other, str): + return self.token == other + + +class AIPlayer(Player): + def __init__(self, token: str=None, name: str=None, human: Player=None, *, id: str=None): + token = token or '🇽' + if human: + if human.token == token and human.token != '🇴': + token = '🇴' + elif human.token == '🇴': + token = '🇽' + super().__init__(token, name=name or f'Robot {token}', id=id) + self._corner_moves = [1, 3, 7, 9] + self._side_moves = [2, 4, 6, 8] + self._center_move = 5 + self.remaining_corners = deepcopy(self._corner_moves) + self.remaining_sides = deepcopy(self._side_moves) + + def make_selection(self, board, last_play: int=None) -> int: + if last_play in self.remaining_corners: + self.remaining_corners.remove(last_play) + elif last_play in self.remaining_sides: + self.remaining_sides.remove(last_play) + + winning_move = self.check_winning_move(board) + if winning_move: + move = winning_move + else: + blocking_move = self.check_blocking_move(board) + if blocking_move: + move = blocking_move + else: + trap_move = self.attempt_trap(board) + if trap_move: + move = trap_move + else: + starting_move = self.starting_strategy(board) + if self.starting_player and starting_move: + move = starting_move + else: + if board.board[1][1] == ' ': + move = 5 + else: + if self.check_corner_trap(board): + move = random.choice(self.remaining_sides) + else: + if self.remaining_corners: + move = random.choice(self.remaining_corners) + else: + move = random.choice(self.remaining_sides) + if move in self.remaining_corners: + self.remaining_corners.remove(move) + elif move in self.remaining_sides: + self.remaining_sides.remove(move) + print(move) + return move + + def starting_strategy(self, board): + move = False + if board.play_count == 0: + move = random.choice(self.remaining_corners) + self.remaining_corners.remove(move) + elif board.play_count == 2: + if (board.board[0][0] == self and ' ' != board.board[2][2] != self) \ + or (board.board[2][2] == self and ' ' != board.board[0][0] != self) \ + or (board.board[2][0] == self and ' ' != board.board[0][2] != self) \ + or (board.board[0][2] == self and ' ' != board.board[2][0] != self): + move = random.choice(self.remaining_corners) + else: + if board.board[0][0] == self: + move = 9 + elif board.board[2][2] == self: + move = 1 + elif board.board[0][2] == self: + move = 7 + elif board.board[2][0] == self: + move = 3 + self.remaining_corners.remove(move) + elif board.play_count == 4 and self.remaining_corners: + move = random.choice(self.remaining_corners) + self.remaining_corners.remove(move) + return move + + + def check_corner_trap(self, board): + if ' ' != board.board[0][0] == board.board[2][2] != self: + return True + elif ' ' != board.board[0][2] == board.board[2][0] != self: + return True + return False + + def check_blocking_move(self, board): + for position in board.winning_states: + if ' ' != board.board[position[0][0]][position[0][1]] == \ + board.board[position[1][0]][position[1][1]] != self \ + and board.board[position[2][0]][position[2][1]] == ' ': + return ((position[2][0] * 3) + position[2][1]) + 1 + elif ' ' != board.board[position[0][0]][position[0][1]] == \ + board.board[position[2][0]][position[2][1]] != self \ + and board.board[position[1][0]][position[1][1]] == ' ': + return ((position[1][0] * 3) + position[1][1]) + 1 + elif ' ' != board.board[position[2][0]][position[2][1]] == \ + board.board[position[1][0]][position[1][1]] != self \ + and board.board[position[0][0]][position[0][1]] == ' ': + return ((position[0][0] * 3) + position[0][1]) + 1 + return False + + def check_winning_move(self, board): + for position in board.winning_states: + if board.board[position[0][0]][position[0][1]] == board.board[position[1][0]][position[1][1]] == self \ + and board.board[position[2][0]][position[2][1]] == ' ': + return ((position[2][0] * 3) + position[2][1]) + 1 + elif board.board[position[0][0]][position[0][1]] == board.board[position[2][0]][position[2][1]] == self \ + and board.board[position[1][0]][position[1][1]] == ' ': + return ((position[1][0] * 3) + position[1][1]) + 1 + elif board.board[position[2][0]][position[2][1]] == board.board[position[1][0]][position[1][1]] == self \ + and board.board[position[0][0]][position[0][1]] == ' ': + return ((position[0][0] * 3) + position[0][1]) + 1 + return False + + def attempt_trap(self, board): + if board.board[1][1] == self: + if board.board[0][0] == self and \ + board.board[0][1] == ' ' and \ + board.board[0][2] == ' ' and \ + board.board[2][0] == ' ': + return 3 + elif board.board[0][0] == self and \ + board.board[1][0] == ' ' and \ + board.board[0][2] == ' ' and \ + board.board[2][0] == ' ': + return 7 + elif board.board[0][2] == self and \ + board.board[0][1] == ' ' and \ + board.board[0][0] == ' ' and \ + board.board[2][2] == ' ': + return 1 + elif board.board[0][2] == self and \ + board.board[1][2] == ' ' and \ + board.board[0][0] == ' ' and \ + board.board[2][2] == ' ': + return 9 + elif board.board[2][0] == self and \ + board.board[0][0] == ' ' and \ + board.board[0][1] == ' ' and \ + board.board[2][2] == ' ': + return 1 + elif board.board[2][0] == self and \ + board.board[2][1] == ' ' and \ + board.board[2][2] == ' ' and \ + board.board[0][0] == ' ': + return 9 + elif board.board[2][2] == self and \ + board.board[2][1] == ' ' and \ + board.board[2][0] == ' ' and \ + board.board[0][2] == ' ': + return 7 + elif board.board[2][2] == self and \ + board.board[1][2] == ' ' and \ + board.board[0][2] == ' ' and \ + board.board[2][0] == ' ': + return 3 + return False + + def reset_game(self): + self.remaining_sides = deepcopy(self._side_moves) + self.remaining_corners = deepcopy(self._corner_moves) + self.starting_player = False diff --git a/geeksbot_v2/shared_libs/__init__.py b/geeksbot_v2/shared_libs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/geeksbot_v2/users/api_urls.py b/geeksbot_v2/users/api_urls.py index 5007257..7047da4 100644 --- a/geeksbot_v2/users/api_urls.py +++ b/geeksbot_v2/users/api_urls.py @@ -1,9 +1,11 @@ from django.urls import path -from geeksbot_v2.users.views import UsersAPI, UserDetail +from geeksbot_v2.users.views import UsersAPI, UserDetail, UserLogList, UserLogDetail app_name = "users_api" urlpatterns = [ path("", view=UsersAPI.as_view(), name="list"), path("/", view=UserDetail.as_view(), name="detail"), + path("/logs/", view=UserLogList.as_view(), name="log_list"), + path("/logs/", view=UserLogDetail.as_view(), name="log_detail"), ] diff --git a/geeksbot_v2/users/serializers.py b/geeksbot_v2/users/serializers.py index d8d11aa..a989d4a 100644 --- a/geeksbot_v2/users/serializers.py +++ b/geeksbot_v2/users/serializers.py @@ -4,7 +4,7 @@ from geeksbot_v2.users.models import User from geeksbot_v2.users.models import UserLog -class UserSerializer(serializers.ModelSerializer): +class UserSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = User fields = [ @@ -21,11 +21,37 @@ class UserSerializer(serializers.ModelSerializer): 'avatar', 'bot', 'banned', - 'logging_enabled' + 'logging_enabled', + 'is_staff', + 'is_superuser', + 'url' ] + extra_kwargs = { + 'url': { + 'view_name': 'users_api:detail', + 'lookup_field': 'id' + }, + 'guilds': { + 'view_name': 'guilds_api:detail', + 'lookup_field': 'id' + } + } class UserLogSerializer(serializers.ModelSerializer): class Meta: model = UserLog - fields = "__all__" + fields = [ + 'user', + 'time', + 'action', + 'description', + 'url' + ] + extra_fields = { + 'url': { + 'view_name': 'users_api:log_detail', + 'lookup_field': 'id', + 'lookup_url_kwarg': 'log' + } + } diff --git a/geeksbot_v2/users/views.py b/geeksbot_v2/users/views.py index 67b81d3..e1ac3ca 100644 --- a/geeksbot_v2/users/views.py +++ b/geeksbot_v2/users/views.py @@ -4,6 +4,7 @@ from django.views.generic import DetailView, RedirectView, UpdateView from django.contrib import messages from django.utils.translation import ugettext_lazy as _ from rest_framework.views import APIView +from rest_framework import generics from rest_framework.permissions import IsAuthenticated from rest_framework import status @@ -11,6 +12,10 @@ from rest_framework import status from .models import UserLog from geeksbot_v2.utils.api_utils import PaginatedAPIView from .models import User +from .serializers import UserSerializer +from .serializers import UserLogSerializer +from geeksbot_v2.utils.permissions import CustomDjangoModelPermissions +from geeksbot_v2.utils.permissions import CustomDjangoObjectPermissions from .utils import create_error_response from .utils import create_success_response from .utils import create_log_success_response @@ -66,73 +71,89 @@ user_redirect_view = UserRedirectView.as_view() # API Views -class UsersAPI(PaginatedAPIView): +class UsersAPI(generics.ListCreateAPIView): permission_classes = [IsAuthenticated] + serializer_class = UserSerializer - 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: - return create_success_response(page, status.HTTP_200_OK, many=True) + def get_queryset(self): + return User.objects.filter(guilds__id=self.request.data.get('guild')) - 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) + # 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: + # return create_success_response(page, status.HTTP_200_OK, many=True) + # + # 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): +class UserDetail(generics.RetrieveUpdateDestroyAPIView): permission_classes = [IsAuthenticated] + serializer_class = UserSerializer + lookup_field = 'id' - 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 get_queryset(self): + return User.objects.all() - 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) + # 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): +class UserLogList(generics.ListCreateAPIView): permission_classes = [IsAuthenticated] + serializer_class = UserLogSerializer - def get(self, request, user, action=None, format=None): - if action: - user_logs = UserLog.get_logs_by_user_action(user, action) - else: - user_logs = UserLog.get_logs_by_user(user) + def get_queryset(self): + return UserLog.objects.all() - page = self.paginate_queryset(user_logs) - if page is not None: - return create_log_success_response(page, status.HTTP_200_OK, many=True) - - 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) + # def get(self, request, user, action=None, format=None): + # if action: + # user_logs = UserLog.get_logs_by_user_action(user, action) + # else: + # user_logs = UserLog.get_logs_by_user(user) + # + # page = self.paginate_queryset(user_logs) + # if page is not None: + # return create_log_success_response(page, status.HTTP_200_OK, many=True) + # + # 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): +class UserLogDetail(generics.RetrieveUpdateAPIView): permission_classes = [IsAuthenticated] + serializer_class = UserLogSerializer + lookup_url_kwarg = 'log' + lookup_field = 'id' - def get(self, request, id, format=None): - 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) + # def get(self, request, id, format=None): + # 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/geeksbot_v2/utils/permissions.py b/geeksbot_v2/utils/permissions.py new file mode 100644 index 0000000..fb55609 --- /dev/null +++ b/geeksbot_v2/utils/permissions.py @@ -0,0 +1,27 @@ +from rest_framework.permissions import DjangoModelPermissions, DjangoObjectPermissions + + +class CustomDjangoModelPermissions(DjangoModelPermissions): + # Overriding to require view permissions + perms_map = { + 'GET': ['%(app_label)s.view_%(model_name)s'], + 'OPTIONS': ['%(app_label)s.view_%(model_name)s'], + 'HEAD': ['%(app_label)s.view_%(model_name)s'], + 'POST': ['%(app_label)s.add_%(model_name)s'], + 'PUT': ['%(app_label)s.change_%(model_name)s'], + 'PATCH': ['%(app_label)s.change_%(model_name)s'], + 'DELETE': ['%(app_label)s.delete_%(model_name)s'], + } + + +class CustomDjangoObjectPermissions(DjangoObjectPermissions): + # Overriding to require view permissions + perms_map = { + 'GET': ['%(app_label)s.view_%(model_name)s'], + 'OPTIONS': ['%(app_label)s.view_%(model_name)s'], + 'HEAD': ['%(app_label)s.view_%(model_name)s'], + 'POST': ['%(app_label)s.add_%(model_name)s'], + 'PUT': ['%(app_label)s.change_%(model_name)s'], + 'PATCH': ['%(app_label)s.change_%(model_name)s'], + 'DELETE': ['%(app_label)s.delete_%(model_name)s'], + } diff --git a/requirements/geeksbot.txt b/requirements/geeksbot.txt index 455219f..30e37ac 100644 --- a/requirements/geeksbot.txt +++ b/requirements/geeksbot.txt @@ -1,4 +1,4 @@ -discord.py +discord.py>=1.2.5 aiohttp<3.6.0 aiofiles python-dateutil @@ -6,4 +6,4 @@ psutil pytz async_timeout cached_property -redis-py \ No newline at end of file +redis diff --git a/requirements/web.txt b/requirements/web.txt index 1d63278..8298e9d 100644 --- a/requirements/web.txt +++ b/requirements/web.txt @@ -18,3 +18,5 @@ gevent gunicorn==19.9.0 # https://github.com/benoitc/gunicorn psycopg2==2.8.3 --no-binary psycopg2 # https://github.com/psycopg/psycopg2 Collectfast==1.0.0 # https://github.com/antonagestam/collectfast + +python-valve diff --git a/services/Dockerfile-base b/services/Dockerfile-base index 220ee85..f2b0e0a 100644 --- a/services/Dockerfile-base +++ b/services/Dockerfile-base @@ -1,4 +1,4 @@ -FROM python:3.7-alpine AS base +FROM python:3.7-alpine AS geeksbot-base ENV DEBIAN_FRONTEND noninteractive ENV PYTHONUNBUFFERED 1 diff --git a/services/Dockerfile-geeksbot b/services/Dockerfile-geeksbot index 27d2248..9592c30 100644 --- a/services/Dockerfile-geeksbot +++ b/services/Dockerfile-geeksbot @@ -1,4 +1,4 @@ -FROM base AS geeksbot +FROM geeksbot-base AS geeksbot WORKDIR /code diff --git a/services/Dockerfile-web b/services/Dockerfile-web index ecf1d56..45fc2e1 100644 --- a/services/Dockerfile-web +++ b/services/Dockerfile-web @@ -1,4 +1,4 @@ -FROM base AS web +FROM geeksbot-base AS geeksbot-web WORKDIR /code