added admin model and create_event command

This commit is contained in:
Bad_Investment
2021-05-06 17:14:29 +00:00
parent 453139cf3d
commit 939ef30d4c
11 changed files with 160 additions and 41 deletions

24
app.py
View File

@@ -22,7 +22,7 @@ API_SETTINGS = FastAPISettings.parse_obj(SETTINGS['fastapi'])
from poapbot.store import EventDataStore
from poapbot.scraper import RedditScraper
from poapbot.bot import RedditBot
from poapbot.models import metadata, database, Event, Attendee, Claim, RequestMessage, ResponseMessage
from poapbot.models import metadata, database, Event, Attendee, Admin, Claim, RequestMessage, ResponseMessage
engine = sqlalchemy.create_engine(DB_SETTINGS.url)
metadata.create_all(engine)
@@ -71,6 +71,20 @@ async def shutdown():
if db.is_connected:
await db.disconnect()
@app.post(
"/admin/create_admin",
description="Create Admin",
tags=['admin'],
response_model=Admin
)
async def create_admin(request: Request, username: str):
existing_admin = await Admin.objects.get_or_none(username__exact=username)
if existing_admin:
raise HTTPException(status_code=409, detail=f'Admin with username {username} already exists')
admin = Admin(username=username)
await admin.save()
return admin
@app.post(
"/admin/event",
description="Create Event",
@@ -81,7 +95,8 @@ async def create_event(
request: Request,
id: str,
name: str,
code: str,
code: str,
start_date: datetime,
expiry_date: datetime,
description: Optional[str] = "",
minimum_age: Optional[int] = 0,
@@ -94,8 +109,9 @@ async def create_event(
event = Event(
id=id,
name=name,
code=code,
description=description,
code=code.lower(),
description=description,
start_date=start_date,
expiry_date=expiry_date,
minimum_age=minimum_age,
minimum_karma=minimum_karma

View File

@@ -1,16 +1,19 @@
from asyncpraw import Reddit
from asyncpraw.models import Message, Redditor
from asyncpraw.models import Message, Redditor, Comment
import asyncio
from datetime import datetime
import logging
import ormar
import re
from ..models import Event, Claim, Attendee, RequestMessage, ResponseMessage
from ..models import Event, Claim, Attendee, Admin, RequestMessage, ResponseMessage
from ..store import EventDataStore
from .exceptions import ExpiredEvent, NoClaimsAvailable, InvalidCode, InsufficientAccountAge, InsufficientKarma
from .exceptions import NotStartedEvent, ExpiredEvent, NoClaimsAvailable, InvalidCode, InsufficientAccountAge, InsufficientKarma, UnauthorizedCommand
logger = logging.getLogger(__name__)
CREATE_EVENT_PATTERN = re.compile(r'create_event (?P<id>\w+) (?P<name>\w+) (?P<code>\w+) (?P<start_date>[\w:-]+) (?P<expiry_date>[\w:-]+) (?P<minimum_age>\w+) (?P<minimum_karma>\w+)')
class RedditBot:
def __init__(self, client: Reddit, store: EventDataStore):
@@ -21,6 +24,8 @@ class RedditBot:
event = await self.store.get(Event, code=code)
if not event:
raise InvalidCode
elif not event.started():
raise NotStartedEvent(event)
elif event.expired():
raise ExpiredEvent(event)
@@ -46,6 +51,66 @@ class RedditBot:
await claim.update()
return claim
async def try_claim(self, code: str, message: Message, redditor: Redditor) -> Comment:
claim = None
try:
claim = await self.reserve_claim(code, redditor)
logger.debug(f'Received valid request from {redditor.name} for event {claim.event.id}, sending link {claim.link}')
return await message.reply(f'Your claim link for {claim.event.name} is {claim.link}')
except InvalidCode:
logger.debug(f'Received request from {redditor.name} with invalid code {code}')
return await message.reply(f'Invalid event code: {code}')
except NotStartedEvent as e:
logger.debug(f'Received request from {redditor.name} for event {e.event.id}, but event has not started')
return await message.reply(f'Sorry, event {e.event.name} has not started yet')
except ExpiredEvent as e:
logger.debug(f'Received request from {redditor.name} for event {e.event.id}, but event has expired')
return await message.reply(f'Sorry, event {e.event.name} has expired')
except NoClaimsAvailable as e:
logger.debug(f'Received request from {redditor.name} for event {e.event.id}, but no more claims are available')
return await message.reply(f'Sorry, there are no more claims available for {e.event.name}')
except InsufficientAccountAge as e:
logger.debug(f'Received request from {redditor.name} for event {e.event.id}, but account is too young')
return await message.reply(f'Sorry, your account is not old enough to be eligible')
except InsufficientKarma as e:
logger.debug(f'Received request from {redditor.name} for event {e.event.id}, but not enough karma')
return await message.reply(f'Sorry, your account does not have enough karma to be eligible')
except Exception as e:
logger.error(e, exc_info=True)
return await message.reply(f'Bot encountered an unrecognized error :(')
async def is_admin(self, redditor: Redditor):
return await self.store.get(Admin, username__exact=redditor.name) is not None
async def create_event(self, message: Message, redditor: Redditor) -> Comment:
if not await self.is_admin(redditor):
logger.debug(f'Received request from {redditor.name} to create_event, but they are unauthorized')
return await message.reply('You are unauthorized to execute this command')
command_data = CREATE_EVENT_PATTERN.match(message.body)
if not command_data:
logger.debug(f'Received request from {redditor.name} to create_event, but command was malformed: {message.body}')
return await message.reply(
"""Your create_event command was malformed, must be of the format: \n\n"""
"""'create_event event_id event_name event_code start_date end_date minimum_age minimum_karma'\n\n"""
"""Date strings must be in UTC and ISO8601 formatted, eg. 2021-05-01T00:00:00"""
)
else:
command_data = command_data.groupdict()
existing_event = await self.store.get(Event, pk=command_data['id'])
if existing_event:
logger.debug(f'Received request to create event, but an event with the provided id already exists')
return await message.reply(f'Failed to create event: An event with id {command_data["id"]} already exists')
try:
event = await self.store.create(Event, **command_data)
logger.debug('Received request to create event, successful')
return await message.reply(f'Successfully created event {event.name}')
except Exception as e:
logger.error(f'Received request to create event, but it failed: {e}', exc_info=True)
return await message.reply(f'Failed to create event: {e}')
async def message_handler(self, message: Message):
redditor = message.author
if not redditor:
@@ -80,26 +145,10 @@ class RedditBot:
body=message.body
)
claim = None
try:
claim = await self.reserve_claim(code, redditor)
comment = await message.reply(f'Your claim link for {claim.event.name} is {claim.link}')
logger.debug(f'Received valid request from {redditor.name} for event {claim.event.id}, sending link {claim.link}')
except InvalidCode:
comment = await message.reply(f'Invalid event code: {code}')
logger.debug(f'Received request from {redditor.name} with invalid code {code}')
except ExpiredEvent as e:
comment = await message.reply(f'Sorry, event {e.event.name} has expired')
logger.debug(f'Received request from {redditor.name} for event {e.event.id}, but event has expired')
except NoClaimsAvailable as e:
comment = await message.reply(f'Sorry, there are no more claims available for {e.event.name}')
logger.debug(f'Received request from {redditor.name} for event {e.event.id}, but no more claims are available')
except InsufficientAccountAge as e:
comment = await message.reply(f'Sorry, your account is not old enough to be eligible')
logger.debug(f'Received request from {redditor.name} for event {e.event.id}, but account is too young')
except InsufficientKarma as e:
comment = await message.reply(f'Sorry, your account does not have enough karma to be eligible')
logger.debug(f'Received request from {redditor.name} for event {e.event.id}, but not enough karma')
if code == 'create_event':
comment = await self.create_event(message, redditor)
else:
comment = await self.try_claim(code, message, redditor)
await message.mark_read()
await self.store.create(
@@ -107,8 +156,7 @@ class RedditBot:
secondary_id=comment.id,
username=comment.author.name,
created=comment.created_utc,
body=comment.body,
claim=claim
body=comment.body
)
async def run(self):

View File

@@ -1,6 +1,11 @@
from ..models import Event
from asyncpraw.models import Redditor
class NotStartedEvent(Exception):
"""Raised when trying to claim from an event that hasn't started yet"""
def __init__(self, event: Event):
self.event = event
class ExpiredEvent(Exception):
"""Raised when trying to claim from an expired event"""
def __init__(self, event: Event):
@@ -23,4 +28,12 @@ class InsufficientKarma(Exception):
class InsufficientAccountAge(Exception):
"""Raised when requestor has insufficient account age"""
def __init__(self, event: Event):
self.event = event
self.event = event
class UnauthorizedCommand(Exception):
"""Raised when requestor has insufficient permissions"""
pass
class MalformedCommand(Exception):
"""Raised when requestor has provided a malformed command"""
pass

View File

@@ -18,5 +18,6 @@ class BaseMeta(ormar.ModelMeta):
from .event import Event
from .attendee import Attendee
from .admin import Admin
from .claim import Claim
from .message import RequestMessage, ResponseMessage

14
poapbot/models/admin.py Normal file
View File

@@ -0,0 +1,14 @@
import ormar
from datetime import datetime
from typing import List, Optional
from . import BaseMeta
class Admin(ormar.Model):
class Meta(BaseMeta):
tablename = "admins"
constraints = [ormar.UniqueColumns('username')]
id: str = ormar.Integer(primary_key=True)
username: str = ormar.String(max_length=100)

View File

@@ -1,6 +1,7 @@
import ormar
from pydantic import BaseModel
from datetime import datetime
from typing import Optional
from . import BaseMeta
@@ -11,12 +12,16 @@ class Event(ormar.Model):
id: str = ormar.String(primary_key=True, max_length=100)
name: str = ormar.String(max_length=256)
description: str = ormar.String(max_length=256)
description: Optional[str] = ormar.String(max_length=256, default="")
code: str = ormar.String(max_length=256)
start_date: datetime = ormar.DateTime()
expiry_date: datetime = ormar.DateTime()
minimum_karma: int = ormar.Integer(default=0)
minimum_age: int = ormar.Integer(default=0)
def expired(self):
return self.expiry_date < datetime.utcnow()
return self.expiry_date < datetime.utcnow()
def started(self):
return self.start_date < datetime.utcnow()

View File

@@ -26,5 +26,4 @@ class ResponseMessage(ormar.Model):
secondary_id: str = ormar.String(max_length=100)
username: str = ormar.String(max_length=100)
created: datetime = ormar.DateTime()
body: str = ormar.String(max_length=1024)
claim: Claim = ormar.ForeignKey(Claim, nullable=True, skip_reverse=True)
body: str = ormar.String(max_length=1024)

View File

@@ -0,0 +1,15 @@
import ormar
from datetime import datetime
from typing import List, Optional
from . import BaseMeta
from .event import Event
class Admin(ormar.Model):
class Meta(BaseMeta):
tablename = "admins"
constraints = [ormar.UniqueColumns('username')]
id: str = ormar.Integer(primary_key=True)
username: str = ormar.String(max_length=100)

View File

@@ -21,4 +21,5 @@ class EventDataStore:
async def create(self, cls: Model, *args, **kwargs):
obj = cls(*args, **kwargs)
await obj.save()
await obj.save()
return obj

View File

@@ -2,7 +2,7 @@ import pytest
import asyncio
from datetime import datetime
from poapbot.bot import RedditBot
from poapbot.bot.exceptions import ExpiredEvent, NoClaimsAvailable, InvalidCode, InsufficientAccountAge, InsufficientKarma
from poapbot.bot.exceptions import NotStartedEvent, ExpiredEvent, NoClaimsAvailable, InvalidCode, InsufficientAccountAge, InsufficientKarma
@pytest.fixture
def bot(store):
@@ -14,19 +14,26 @@ async def test_reserve_claim_no_event(mocker, bot, dummy_event, dummy_redditor):
with pytest.raises(InvalidCode):
await bot.reserve_claim('invalid_code', dummy_redditor)
@pytest.mark.asyncio
async def test_reserve_claim_event_not_started(mocker, bot, dummy_event, dummy_redditor):
dummy_event.start_date = datetime(2100, 1, 1)
mocker.patch.object(bot.store, 'get', return_value=dummy_event)
with pytest.raises(NotStartedEvent):
await bot.reserve_claim('test', dummy_redditor)
@pytest.mark.asyncio
async def test_reserve_claim_event_expired(mocker, bot, dummy_event, dummy_redditor):
dummy_event.expiry_date = datetime(1969, 1, 1)
mocker.patch.object(bot.store, 'get', return_value=dummy_event)
with pytest.raises(ExpiredEvent):
await bot.reserve_claim('invalid_code', dummy_redditor)
await bot.reserve_claim('test', dummy_redditor)
@pytest.mark.asyncio
async def test_reserve_claim_existing_claim(mocker, bot, dummy_event, dummy_redditor, dummy_claim_reserved):
mocker.patch.object(bot.store, 'get', return_value=dummy_event)
mocker.patch.object(bot.store, 'get_filter', return_value=dummy_claim_reserved)
claim = await bot.reserve_claim('invalid_code', dummy_redditor)
claim = await bot.reserve_claim('test', dummy_redditor)
assert claim == dummy_claim_reserved
assert claim.reserved
@@ -38,7 +45,7 @@ async def test_reserve_claim_insufficient_karma(mocker, bot, dummy_event, dummy_
mocker.patch.object(bot.store, 'get', return_value=dummy_event)
mocker.patch.object(bot.store, 'get_filter', return_value=None)
with pytest.raises(InsufficientKarma):
await bot.reserve_claim('invalid_code', dummy_redditor)
await bot.reserve_claim('test', dummy_redditor)
@pytest.mark.asyncio
async def test_reserve_claim_insufficient_age(mocker, bot, dummy_event, dummy_redditor):
@@ -47,4 +54,4 @@ async def test_reserve_claim_insufficient_age(mocker, bot, dummy_event, dummy_re
mocker.patch.object(bot.store, 'get', return_value=dummy_event)
mocker.patch.object(bot.store, 'get_filter', return_value=None)
with pytest.raises(InsufficientAccountAge):
await bot.reserve_claim('invalid_code', dummy_redditor)
await bot.reserve_claim('test', dummy_redditor)

View File

@@ -4,4 +4,4 @@ from datetime import datetime
@pytest.fixture
def dummy_event():
return Event(id='test', name='test', code='test', description="", expiry_date=datetime(2030, 1, 1))
return Event(id='test', name='test', code='test', description="", start_date=datetime(1970, 1, 1), expiry_date=datetime(2100, 1, 1))