Multiple changes

Started switching to generics
Added tickets
This commit is contained in:
Dusty Pianalto 2019-12-01 10:30:03 -09:00
parent 27231fda7b
commit b8f751bae9
37 changed files with 1482 additions and 144 deletions

View File

@ -1,25 +1,25 @@
version: '3' version: '3'
services: services:
base: geeksbot-base:
build: build:
context: . context: .
dockerfile: "${PWD}/services/Dockerfile-base" dockerfile: "${PWD}/services/Dockerfile-base"
image: base:latest image: geeksbot-base:latest
db: geeksbot-db:
image: postgres image: postgres
ports: ports:
- "5432:5432" - "5432:5432"
volumes: volumes:
- "${PWD}/services/postgresql/postgres.conf:/etc/postgresql/postgresql.conf" - "${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 env_file: ${PWD}/.env
redis: geeksbot-redis:
image: redis:5.0.3 image: redis:5.0.3
ports: ports:
- "6379:6379" - "6379:6379"
web: geeksbot-web:
build: build:
context: . context: .
dockerfile: "${PWD}/services/Dockerfile-web" dockerfile: "${PWD}/services/Dockerfile-web"
@ -29,9 +29,9 @@ services:
- "8000:8000" - "8000:8000"
- "443:443" - "443:443"
depends_on: depends_on:
- db - geeksbot-db
- redis - geeksbot-redis
- base - geeksbot-base
environment: environment:
- DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} - DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
- REDIS_URL=redis://${REDIS_HOST}:${REDIS_PORT}/${REDIS_DB} - REDIS_URL=redis://${REDIS_HOST}:${REDIS_PORT}/${REDIS_DB}
@ -43,12 +43,12 @@ services:
dockerfile: "${PWD}/services/Dockerfile-geeksbot" dockerfile: "${PWD}/services/Dockerfile-geeksbot"
env_file: ${PWD}/.env env_file: ${PWD}/.env
depends_on: depends_on:
- db - geeksbot-db
- redis - geeksbot-redis
- base - geeksbot-base
- web - geeksbot-web
links: links:
- web:geeksbot.app - geeksbot-web:geeksbot.app
volumes: volumes:
- ${PWD}/geeksbot:/code/geeksbot - ${PWD}/geeksbot:/code/geeksbot
- ~/.ssh/id_rsa:/root/.ssh/id_rsa - ~/.ssh/id_rsa:/root/.ssh/id_rsa
@ -56,5 +56,5 @@ services:
- ~/.ssh/known_hosts:/root/.ssh/known_hosts - ~/.ssh/known_hosts:/root/.ssh/known_hosts
volumes: volumes:
db: geeksbot-db:
external: true external: true

View File

@ -2,6 +2,7 @@
"load_list": [ "load_list": [
"admin", "admin",
"exec", "exec",
"message_events" "message_events",
"tickets"
] ]
} }

View File

@ -1 +1,218 @@
import discord 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))

View File

@ -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)

View File

@ -84,7 +84,8 @@ class Paginator:
field_name_char: str = '\uFFF6', field_name_char: str = '\uFFF6',
inline_char: str = '\uFFF5', inline_char: str = '\uFFF5',
max_line_length: int = 100, max_line_length: int = 100,
embed=False): embed=False,
header: str = ''):
_max_len = 6000 if embed else 1980 _max_len = 6000 if embed else 1980
assert 0 < max_lines <= max_chars assert 0 < max_lines <= max_chars
@ -110,6 +111,7 @@ class Paginator:
self._embed_thumbnail = None self._embed_thumbnail = None
self._embed_url = None self._embed_url = None
self._bot = bot self._bot = bot
self._header = header
def set_embed_meta(self, title: str = None, def set_embed_meta(self, title: str = None,
description: str = None, description: str = None,
@ -129,7 +131,7 @@ class Paginator:
self._embed_thumbnail = thumbnail self._embed_thumbnail = thumbnail
self._embed_url = url self._embed_url = url
def pages(self) -> typing.List[str]: def pages(self, page_headers: bool = True) -> typing.List[str]:
_pages = list() _pages = list()
_fields = list() _fields = list()
_page = '' _page = ''
@ -138,10 +140,16 @@ class Paginator:
_field_value = '' _field_value = ''
_inline = False _inline = False
def open_page(): def open_page(initial: bool = False):
nonlocal _page, _lines, _fields nonlocal _page, _lines, _fields
if not self._embed: 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 _lines = 0
else: else:
_fields = list() _fields = list()
@ -156,7 +164,7 @@ class Paginator:
_pages.append(_fields) _pages.append(_fields)
open_page() open_page()
open_page() open_page(initial=True)
if not self._embed: if not self._embed:
for part in [str(p) for p in self._parts]: for part in [str(p) for p in self._parts]:
@ -254,6 +262,9 @@ class Paginator:
# noinspection PyProtectedMember # noinspection PyProtectedMember
return self.__class__ == other.__class__ and self._parts == other._parts 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: def add_page_break(self, *, to_beginning: bool = False) -> None:
self.add(self._page_break, to_beginning=to_beginning) self.add(self._page_break, to_beginning=to_beginning)

View File

@ -1,9 +1,10 @@
from django.urls import path from django.urls import path
from .views import ChannelsAPI, ChannelDetail from .views import ChannelsAPI, ChannelDetail, AdminChannelAPI
app_name = "channels_api" app_name = "channels_api"
urlpatterns = [ urlpatterns = [
path("", view=ChannelsAPI.as_view(), name="list"), path("", view=ChannelsAPI.as_view(), name="list"),
path("<str:id>/", view=ChannelDetail.as_view(), name='detail') path("<str:id>/", view=ChannelDetail.as_view(), name='detail'),
path("<str:guild_id>/admin/", view=AdminChannelAPI.as_view(), name='admin')
] ]

View File

@ -61,9 +61,9 @@ class Channel(models.Model):
return create_success_response(channel, status.HTTP_201_CREATED, many=False) return create_success_response(channel, status.HTTP_201_CREATED, many=False)
@classmethod @classmethod
def get_channel_by_id(cls, id): def get_channel_by_id(cls, guild_id, channel_id):
try: try:
return cls.objects.get(id=id) return cls.get_guild_channels(guild_id).get(id=channel_id)
except ObjectDoesNotExist: except ObjectDoesNotExist:
return None return None
@ -71,6 +71,15 @@ class Channel(models.Model):
def get_guild_channels(cls, guild): def get_guild_channels(cls, guild):
if isinstance(guild, Guild): if isinstance(guild, Guild):
return cls.objects.filter(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): def __str__(self):
return str(id) return str(id)

View File

@ -16,25 +16,53 @@ from .utils import create_success_response
class ChannelsAPI(PaginatedAPIView): class ChannelsAPI(PaginatedAPIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
def get(self, request, format=None): def get(self, request, guild_id, format=None):
guilds = Channel.objects.all() channels = Channel.get_guild_channels(guild_id)
page = self.paginate_queryset(guilds) page = self.paginate_queryset(channels)
if page is not None: if page is not None:
return create_success_response(page, status.HTTP_200_OK, many=True) 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): def post(self, request, format=None):
data = dict(request.data) data = dict(request.data)
return Channel.add_new_channel(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): class ChannelDetail(APIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
def get(self, request, id, format=None): def get(self, request, guild_id, channel_id, format=None):
try: try:
guild = Channel.objects.get(id=id) guild = Channel.get_channel_by_id(guild_id, channel_id)
except ObjectDoesNotExist: except ObjectDoesNotExist:
return create_error_response("Channel Does not Exist", return create_error_response("Channel Does not Exist",
status=status.HTTP_404_NOT_FOUND) status=status.HTTP_404_NOT_FOUND)
@ -42,8 +70,8 @@ class ChannelDetail(APIView):
return create_success_response(guild, return create_success_response(guild,
status=status.HTTP_200_OK) status=status.HTTP_200_OK)
def put(self, request, id, format=None): def put(self, request, guild_id, channel_id, format=None):
channel = Channel.get_channel_by_id(id) channel = Channel.get_channel_by_id(guild_id, channel_id)
if channel: if channel:
data = dict(request.data) data = dict(request.data)
@ -54,14 +82,14 @@ class ChannelDetail(APIView):
return create_error_response('Channel Does Not Exist', return create_error_response('Channel Does Not Exist',
status=status.HTTP_404_NOT_FOUND) status=status.HTTP_404_NOT_FOUND)
def delete(self, request, id, format=None): def delete(self, request, guild_id, channel_id, format=None):
guild = Channel.get_guild_by_id(id) channel = Channel.get_channel_by_id(guild_id, channel_id)
if guild: if channel:
# data = dict(request.data) # data = dict(request.data)
# TODO Add a check to verify user is allowed to delete... # TODO Add a check to verify user is allowed to delete...
# Possibly in object permissions... # Possibly in object permissions...
guild.delete() channel.delete()
return create_success_response(guild, return create_success_response(guild,
status=status.HTTP_200_OK) status=status.HTTP_200_OK)
else: else:

View File

@ -24,7 +24,7 @@ DEBUG = env.bool("DJANGO_DEBUG", False)
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# though not all of them may be available with every OS. # though not all of them may be available with every OS.
# In Windows, this must be set to your system time zone. # 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 # https://docs.djangoproject.com/en/dev/ref/settings/#language-code
LANGUAGE_CODE = "en-us" LANGUAGE_CODE = "en-us"
# https://docs.djangoproject.com/en/dev/ref/settings/#site-id # https://docs.djangoproject.com/en/dev/ref/settings/#site-id

View File

@ -20,6 +20,7 @@ urlpatterns = [
path("api/guilds/", include("geeksbot_v2.guilds.api_urls", namespace="guilds_api")), 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/channels/", include("geeksbot_v2.channels.api_urls", namespace="channels_api")),
path("api/messages/", include("geeksbot_v2.dmessages.api_urls", namespace="messages_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) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
if settings.DEBUG: if settings.DEBUG:

View File

@ -2,14 +2,20 @@ from django.urls import path
from .views import MessageDetailAPI, MessagesAPI from .views import MessageDetailAPI, MessagesAPI
from .views import RequestDetailAPI, RequestsAPI 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 = [ urlpatterns = [
path("", view=MessagesAPI.as_view(), name="message_list"), path("", view=MessagesAPI.as_view(), name="message_list"),
path("<str:id>/", view=MessageDetailAPI.as_view(), name='message_detail'), path("<str:id>/", view=MessageDetailAPI.as_view(), name='message_detail'),
path("requests/", view=RequestsAPI.as_view(), name="requests_list"), path("<str:guild_id>/requests/", view=RequestsAPI.as_view(), name="requests_list"),
path("requests/<str:id>/", view=RequestDetailAPI.as_view(), name='request_detail'), path("<str:guild_id>/requests/<str:request_id>/", view=RequestDetailAPI.as_view(), name='request_detail'),
path("requests/<str:request_id>/comments/", view=CommentsAPI.as_view(), name="comments_list"), path("<str:guild_id>/requests/<str:request_id>/comments/", view=CommentsAPI.as_view(), name="comments_list"),
path("requests/<str:request_id>/comments/<str:comment_id>/", view=CommentDetailAPI.as_view(), name='comment_detail'), path("<str:guild_id>/requests/<str:request_id>/comments/count/", view=CommentsCountAPI.as_view(), name="comments_count"),
path("<str:guild_id>/requests/<str:request_id>/comments/<str:comment_id>/", view=CommentDetailAPI.as_view(), name='comment_detail'),
path("<str:guild_id>/requests/user/<str:author_id>/", view=UserRequestsAPI.as_view(), name='user_requests_list'),
path("<str:id>/wait/", view=WaitForMessageAPI.as_view(), name='wait_for_message'),
path("<str:id>/wait/<int:timeout>/", view=WaitForMessageAPI.as_view(), name='wait_for_message_timeout'),
] ]

View File

@ -57,7 +57,7 @@ class Message(models.Model):
if not isinstance(guild, Guild): if not isinstance(guild, Guild):
return create_error_response("Guild Does Not Exist", return create_error_response("Guild Does Not Exist",
status=status.HTTP_404_NOT_FOUND) 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): if not isinstance(channel, Channel):
return create_error_response("Channel Does Not Exist", return create_error_response("Channel Does Not Exist",
status=status.HTTP_404_NOT_FOUND) status=status.HTTP_404_NOT_FOUND)
@ -89,7 +89,7 @@ class Message(models.Model):
if data.get('tagged_channels'): if data.get('tagged_channels'):
tagged_channels = data.get('tagged_channels') tagged_channels = data.get('tagged_channels')
for channel_id in 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: if channel:
message.tagged_channels.add(channel) message.tagged_channels.add(channel)
@ -179,8 +179,9 @@ class AdminRequest(models.Model):
def update_request(self, data): def update_request(self, data):
completed = data.get('completed', False) completed = data.get('completed', False)
completed_by_id = data.get('completed_by') completed_by_id = data.get('completed_by')
completed_message = data.get('message') completed_message = data.get('message', '')
if not self.completed and completed: if not self.completed and completed:
self.completed = completed
self.completed_at = datetime.utcnow() self.completed_at = datetime.utcnow()
self.completed_message = completed_message self.completed_message = completed_message
user = User.get_user_by_id(completed_by_id) 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) return create_request_success_response(self, status.HTTP_202_ACCEPTED)
@classmethod @classmethod
def add_new_request(cls, data): def add_new_request(cls, guild_id, data):
guild_id = data.get('guild')
author_id = data.get('author') author_id = data.get('author')
message_id = data.get('message') message_id = data.get('message')
channel_id = data.get('channel') channel_id = data.get('channel')
@ -205,7 +205,7 @@ class AdminRequest(models.Model):
if not isinstance(guild, Guild): if not isinstance(guild, Guild):
return create_error_response('Guild Does Not Exist', return create_error_response('Guild Does Not Exist',
status=status.HTTP_404_NOT_FOUND) 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): if not isinstance(author, User):
return create_error_response('Author Does Not Exist', return create_error_response('Author Does Not Exist',
status=status.HTTP_404_NOT_FOUND) status=status.HTTP_404_NOT_FOUND)
@ -213,11 +213,13 @@ class AdminRequest(models.Model):
if not isinstance(message, Message): if not isinstance(message, Message):
return create_error_response('Message Does Not Exist', return create_error_response('Message Does Not Exist',
status=status.HTTP_404_NOT_FOUND) 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): if not isinstance(channel, Channel):
return create_error_response('Channel Does Not Exist', return create_error_response('Channel Does Not Exist',
status=status.HTTP_404_NOT_FOUND) status=status.HTTP_404_NOT_FOUND)
print('test')
request = cls( request = cls(
guild=guild, guild=guild,
author=author, author=author,
@ -233,15 +235,19 @@ class AdminRequest(models.Model):
return cls.objects.filter(guild__id=guild_id).filter(completed=False) return cls.objects.filter(guild__id=guild_id).filter(completed=False)
@classmethod @classmethod
def get_request_by_id(cls, id): def get_open_request_by_id(cls, guild_id, request_id):
try: try:
return cls.objects.get(id=id) return cls.get_open_requests_by_guild(guild_id).get(id=request_id)
except ObjectDoesNotExist: except ObjectDoesNotExist:
return None return None
def __str__(self): def __str__(self):
return f"{self.guild.id} | {self.requested_at} | By {self.author.id}" 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): class AdminComment(models.Model):
request = models.ForeignKey(AdminRequest, on_delete=models.CASCADE) 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) updated_at = models.DateTimeField(auto_now_add=True, blank=True)
@classmethod @classmethod
def add_new_comment(cls, data, request_id): def add_new_comment(cls, data, guild_id, request_id):
author_id = data.get('author') author_id = data.get('author')
content = data.get('content') content = data.get('content')
if not (request_id and author_id and content): if not (request_id and author_id and content):
return create_error_response('Request, Author, and Content are required fields', return create_error_response('Request, Author, and Content are required fields',
status=status.HTTP_400_BAD_REQUEST) 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): if not isinstance(request, AdminRequest):
return create_error_response("Admin Request Does Not Exist", return create_error_response("Admin Request Does Not Exist",
status=status.HTTP_404_NOT_FOUND) 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) return create_comment_success_response(comment, status.HTTP_201_CREATED, many=False)
@classmethod @classmethod
def get_comment_by_id(cls, id): def get_comment_by_id(cls, comment_id):
try: try:
return cls.objects.get(id=id) return cls.objects.get(id=comment_id)
except ObjectDoesNotExist: except ObjectDoesNotExist:
return None return None
@classmethod @classmethod
def get_comments_by_request(cls, request): def get_comments_by_request(cls, request):
return cls.objects.filter(request=request) return cls.objects.filter(request=request).order_by('updated_at')

View File

@ -1,3 +1,6 @@
from time import sleep
from datetime import datetime
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework import status from rest_framework import status
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
@ -59,27 +62,57 @@ class MessageDetailAPI(APIView):
status=status.HTTP_404_NOT_FOUND) 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): class RequestsAPI(PaginatedAPIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
def get(self, request, guild, format=None): def get(self, request, guild_id, format=None):
requests = AdminRequest.get_open_requests_by_guild(guild) requests = AdminRequest.get_open_requests_by_guild(guild_id)
page = self.paginate_queryset(requests) page = self.paginate_queryset(requests)
if page is not None: if page is not None:
return create_request_success_response(page, status.HTTP_200_OK, many=True) 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_request_success_response(requests, status.HTTP_200_OK, many=True)
return create_error_response("No requests found")
def post(self, request, format=None): def post(self, request, guild_id, format=None):
data = dict(request.data) 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): class RequestDetailAPI(APIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
def get(self, req, id, format=None): def get(self, req, guild_id, request_id, format=None):
req = AdminRequest.get_request_by_id(id) req = AdminRequest.get_open_request_by_id(guild_id, request_id)
if req: if req:
comments = AdminComment.get_comments_by_request(req) comments = AdminComment.get_comments_by_request(req)
if comments: if comments:
@ -92,21 +125,44 @@ class RequestDetailAPI(APIView):
return create_error_response("That Request Does Not Exist", return create_error_response("That Request Does Not Exist",
status=status.HTTP_404_NOT_FOUND) status=status.HTTP_404_NOT_FOUND)
def put(self, request, id, format=None): def put(self, request, guild_id, request_id, format=None):
req = AdminRequest.get_request_by_id(id) req = AdminRequest.get_open_request_by_id(guild_id, request_id)
if req: if req:
data = dict(request.data) data = dict(request.data)
return req.update_request(data) return req.update_request(data)
return create_error_response("That Request Does Not Exist", return create_error_response("That Request Does Not Exist",
status=status.HTTP_404_NOT_FOUND) 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): class CommentsAPI(PaginatedAPIView):
permissions_classes = [IsAuthenticated] 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) 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): class CommentDetailAPI(APIView):

View File

@ -2,11 +2,13 @@ from django.urls import path
from .views import GuildsAPI, GuildDetail from .views import GuildsAPI, GuildDetail
from .views import RolesAPI, RoleDetailAPI from .views import RolesAPI, RoleDetailAPI
from .views import AdminRolesAPI
app_name = "guilds_api" app_name = "guilds_api"
urlpatterns = [ urlpatterns = [
path("", view=GuildsAPI.as_view(), name="list"), path("", view=GuildsAPI.as_view(), name="list"),
path("<str:id>/", view=GuildDetail.as_view(), name='detail'), path("<str:id>/", view=GuildDetail.as_view(), name='detail'),
path("<str:guild_id>/roles/", view=RolesAPI.as_view(), name="list"), path("<str:guild_id>/roles/", view=RolesAPI.as_view(), name="list"),
path("<str:guild_id>/roles/admin/", view=AdminRolesAPI.as_view(), name='admin'),
path("<str:guild_id>/roles/<str:id>/", view=RoleDetailAPI.as_view(), name='detail'), path("<str:guild_id>/roles/<str:id>/", view=RoleDetailAPI.as_view(), name='detail'),
] ]

View File

@ -73,7 +73,7 @@ class Role(models.Model):
self.role_type = data.get('role_type') self.role_type = data.get('role_type')
self.save() self.save()
return create_role_success_response(self, status=status.HTTP_202_ACCEPTED, many=False) return self
@classmethod @classmethod
def add_new_role(cls, guild_id, data): 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) return create_role_success_response(role, status.HTTP_201_CREATED, many=False)
@classmethod @classmethod
def get_role_by_id(cls, id): def get_role_by_id(cls, guild_id, role_id):
try: try:
return cls.objects.get(id=id) return cls.get_guild_roles(guild_id).get(id=role_id)
except ObjectDoesNotExist: except ObjectDoesNotExist:
return None return None
@ -122,5 +122,12 @@ class Role(models.Model):
def get_guild_roles(cls, guild): def get_guild_roles(cls, guild):
return cls.objects.filter(guild__id=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): def __str__(self):
return f"{self.guild.id} | {self.id}" return f"{self.guild.id} | {self.id}"

View File

@ -87,6 +87,26 @@ class RolesAPI(PaginatedAPIView):
return Role.add_new_role(guild_id, data) 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): class RoleDetailAPI(APIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
@ -101,7 +121,7 @@ class RoleDetailAPI(APIView):
status=status.HTTP_200_OK) status=status.HTTP_200_OK)
def put(self, request, guild_id, id, format=None): 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: if role:
data = dict(request.data) data = dict(request.data)

View File

@ -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)

View File

@ -0,0 +1,10 @@
from django.urls import path
from .views import RCONServersAPI, RCONServerDetailAPI, ListPlayers
app_name = "rcon_api"
urlpatterns = [
path("<str:guild_id>/", view=RCONServersAPI.as_view(), name='guild_servers'),
path("<str:guild_id>/<str:name>/", view=RCONServerDetailAPI.as_view(), name="server_detail"),
path("<str:guild_id>/<str:name>/listplayers", view=ListPlayers.as_view(), name='listplayers'),
]

View File

@ -120,7 +120,7 @@ class RconServer(models.Model):
@classmethod @classmethod
def get_guild_servers(cls, guild_id): def get_guild_servers(cls, guild_id):
guild = Guild.get_guild_by_id(guild_id) guild = Guild.get_guild_by_id(guild_id)
if not isinstance(guild, guild): if not isinstance(guild, Guild):
return None return None
return cls.objects.filter(guild=guild) return cls.objects.filter(guild=guild)

View File

View File

@ -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

View File

@ -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'))

View File

@ -12,3 +12,8 @@ def create_success_response(rcon_data, status, many: bool = False):
return Response(RconServerSerializer(rcon_data, many=many).data, return Response(RconServerSerializer(rcon_data, many=many).data,
status=status) status=status)
def create_rcon_response(message, status):
msg_list = message.split('\n')
return Response(msg_list, status=status)

View File

@ -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. # 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)

View File

@ -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'<TicTacToe Board id="{self.id}">'
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

View File

@ -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'<TicTacToe Player name="{self.name}" id="{self.id}">'
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

View File

View File

@ -1,9 +1,11 @@
from django.urls import path 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" app_name = "users_api"
urlpatterns = [ urlpatterns = [
path("", view=UsersAPI.as_view(), name="list"), path("", view=UsersAPI.as_view(), name="list"),
path("<str:id>/", view=UserDetail.as_view(), name="detail"), path("<str:id>/", view=UserDetail.as_view(), name="detail"),
path("<str:id>/logs/", view=UserLogList.as_view(), name="log_list"),
path("<str:id>/logs/<str:log>", view=UserLogDetail.as_view(), name="log_detail"),
] ]

View File

@ -4,7 +4,7 @@ from geeksbot_v2.users.models import User
from geeksbot_v2.users.models import UserLog from geeksbot_v2.users.models import UserLog
class UserSerializer(serializers.ModelSerializer): class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta: class Meta:
model = User model = User
fields = [ fields = [
@ -21,11 +21,37 @@ class UserSerializer(serializers.ModelSerializer):
'avatar', 'avatar',
'bot', 'bot',
'banned', '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 UserLogSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = UserLog 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'
}
}

View File

@ -4,6 +4,7 @@ from django.views.generic import DetailView, RedirectView, UpdateView
from django.contrib import messages from django.contrib import messages
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework import generics
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework import status from rest_framework import status
@ -11,6 +12,10 @@ from rest_framework import status
from .models import UserLog from .models import UserLog
from geeksbot_v2.utils.api_utils import PaginatedAPIView from geeksbot_v2.utils.api_utils import PaginatedAPIView
from .models import User 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_error_response
from .utils import create_success_response from .utils import create_success_response
from .utils import create_log_success_response from .utils import create_log_success_response
@ -66,73 +71,89 @@ user_redirect_view = UserRedirectView.as_view()
# API Views # API Views
class UsersAPI(PaginatedAPIView): class UsersAPI(generics.ListCreateAPIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
serializer_class = UserSerializer
def get(self, request, guild=None, format=None): def get_queryset(self):
if guild: return User.objects.filter(guilds__id=self.request.data.get('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 get(self, request, guild=None, format=None):
# if guild:
def post(self, request, format=None): # users = User.objects.filter(guilds__id=guild)
data = dict(request.data) # else:
return User.add_new_user(data) # 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] permission_classes = [IsAuthenticated]
serializer_class = UserSerializer
lookup_field = 'id'
def get(self, request, id, format=None): def get_queryset(self):
user = User.get_user_by_id(id) return User.objects.all()
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): # def get(self, request, id, format=None):
user = User.get_user_by_id(id) # user = User.get_user_by_id(id)
if isinstance(user, User): # if not isinstance(user, User):
data = dict(request.data) # return create_error_response("User Does not Exist",
return user.update_user(data) # status=status.HTTP_404_NOT_FOUND)
else: # return create_success_response(user,
return create_error_response("User Does Not Exist", # status=status.HTTP_200_OK)
status=status.HTTP_404_NOT_FOUND) #
# 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] permission_classes = [IsAuthenticated]
serializer_class = UserLogSerializer
def get(self, request, user, action=None, format=None): def get_queryset(self):
if action: return UserLog.objects.all()
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) # def get(self, request, user, action=None, format=None):
if page is not None: # if action:
return create_log_success_response(page, status.HTTP_200_OK, many=True) # user_logs = UserLog.get_logs_by_user_action(user, action)
# else:
return create_log_success_response(user_logs, status.HTTP_200_OK, many=True) # user_logs = UserLog.get_logs_by_user(user)
#
def post(self, request, user, format=None): # page = self.paginate_queryset(user_logs)
data = dict(request.data) # if page is not None:
return UserLog.add_new_log(user, data) # 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] permission_classes = [IsAuthenticated]
serializer_class = UserLogSerializer
lookup_url_kwarg = 'log'
lookup_field = 'id'
def get(self, request, id, format=None): # def get(self, request, id, format=None):
user_log = UserLog.get_log_by_id(id) # user_log = UserLog.get_log_by_id(id)
if isinstance(user_log, UserLog): # if isinstance(user_log, UserLog):
return create_log_success_response(user_log, status.HTTP_200_OK, many=False) # return create_log_success_response(user_log, status.HTTP_200_OK, many=False)
else: # else:
return create_error_response("Log Does Not Exist", # return create_error_response("Log Does Not Exist",
status=status.HTTP_404_NOT_FOUND) # status=status.HTTP_404_NOT_FOUND)

View File

@ -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'],
}

View File

@ -1,4 +1,4 @@
discord.py discord.py>=1.2.5
aiohttp<3.6.0 aiohttp<3.6.0
aiofiles aiofiles
python-dateutil python-dateutil
@ -6,4 +6,4 @@ psutil
pytz pytz
async_timeout async_timeout
cached_property cached_property
redis-py redis

View File

@ -18,3 +18,5 @@ gevent
gunicorn==19.9.0 # https://github.com/benoitc/gunicorn gunicorn==19.9.0 # https://github.com/benoitc/gunicorn
psycopg2==2.8.3 --no-binary psycopg2 # https://github.com/psycopg/psycopg2 psycopg2==2.8.3 --no-binary psycopg2 # https://github.com/psycopg/psycopg2
Collectfast==1.0.0 # https://github.com/antonagestam/collectfast Collectfast==1.0.0 # https://github.com/antonagestam/collectfast
python-valve

View File

@ -1,4 +1,4 @@
FROM python:3.7-alpine AS base FROM python:3.7-alpine AS geeksbot-base
ENV DEBIAN_FRONTEND noninteractive ENV DEBIAN_FRONTEND noninteractive
ENV PYTHONUNBUFFERED 1 ENV PYTHONUNBUFFERED 1

View File

@ -1,4 +1,4 @@
FROM base AS geeksbot FROM geeksbot-base AS geeksbot
WORKDIR /code WORKDIR /code

View File

@ -1,4 +1,4 @@
FROM base AS web FROM geeksbot-base AS geeksbot-web
WORKDIR /code WORKDIR /code