mirror of
https://github.com/stake-house/poap-reddit-bot.git
synced 2026-01-09 14:07:58 -05:00
added admin model and create_event command
This commit is contained in:
24
app.py
24
app.py
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
@@ -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
14
poapbot/models/admin.py
Normal 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)
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
15
poapbot/models/settings/admin.py
Normal file
15
poapbot/models/settings/admin.py
Normal 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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
2
tests/fixtures/event.py
vendored
2
tests/fixtures/event.py
vendored
@@ -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))
|
||||
Reference in New Issue
Block a user