Skip to content

Commit

Permalink
emojis repository
Browse files Browse the repository at this point in the history
  • Loading branch information
kheina committed Aug 7, 2024
1 parent 704ef77 commit 7ed6f66
Show file tree
Hide file tree
Showing 17 changed files with 541 additions and 38 deletions.
2 changes: 1 addition & 1 deletion configs/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ async def v1Funding() -> FundingResponse :

@app.patch('/user', status_code=204)
async def v1UpdateUserConfig(req: Request, body: UserConfigRequest) -> None :
await req.user.authenticated(Scope.user)
await req.user.verify_scope(Scope.user)
await configs.setUserConfig(
req.user,
body,
Expand Down
21 changes: 21 additions & 0 deletions db/3/00-emojis.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
drop table if exists public.emojis;

create table public.emojis (
emoji text not null primary key,
alias text null
references public.emojis (emoji)
on update cascade
on delete cascade,
owner bigint null
references public.users (user_id)
on update cascade
on delete set null,
post_id bigint null
references public.posts (post_id)
on update cascade
on delete cascade,
alt text null,
filename text not null,
unique (alias),
unique (post_id)
);
5 changes: 5 additions & 0 deletions db/3/01-report-replies.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
alter table public.reports
add column parent bigint null
references public.reports (report_id)
on update cascade
on delete cascade;
44 changes: 44 additions & 0 deletions emojis/emoji.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from typing import Self

from shared.auth import KhUser
from shared.exceptions.http_error import Conflict, HttpErrorHandler, NotFound

from .models import AliasRequest, CreateRequest, Emoji, InternalEmoji
from .repository import EmojiRepository, users
from psycopg2.errors import UniqueViolation


class Emojis(EmojiRepository) :

@HttpErrorHandler('creating emoji',
handlers={
UniqueViolation: (Conflict, 'emoji or alias already exists'),
},
)
async def create(self: Self, user: KhUser, req: CreateRequest) -> Emoji :
iemoji = InternalEmoji(
emoji = req.emoji,
owner = await users._handle_to_user_id(req.owner) if req.owner else None,
post_id = req.post_id.int() if req.post_id else None,
filename = req.filename,
)
await super().create(iemoji)
return await self.emoji(user, iemoji)


async def read(self: Self, user: KhUser, emoji: str) -> Emoji :
iemoji = await self._read(emoji)

if not iemoji :
raise NotFound('emoji does not exist')

return await self.emoji(user, iemoji)


@HttpErrorHandler('creating emoji alias',
handlers={
UniqueViolation: (Conflict, 'emoji or alias already exists'),
},
)
async def alias(self: Self, user: KhUser, req: AliasRequest) -> Emoji :
return await self.emoji(user, await super().alias(req.alias_of, req.emoji))
50 changes: 50 additions & 0 deletions emojis/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from typing import Optional

from pydantic import BaseModel, Field, validator

from shared.models._shared import PostId, UserPortable, _post_id_converter
from shared.sql.query import Table


class InternalEmoji(BaseModel) :
_post_id_converter = validator('post_id', pre=True, always=True, allow_reuse=True)(_post_id_converter)
__table_name__: Table = Table('kheina.public.emojis')

emoji: str = Field(description='orm:"pk"')
alt: Optional[str] = None
alias: Optional[str] = None
owner: Optional[int] = None
post_id: Optional[int] = None
filename: str


class Emoji(BaseModel) :
_post_id_converter = validator('post_id', pre=True, always=True, allow_reuse=True)(_post_id_converter)

class Config:
validate_assignment = True

emoji: str
alt: Optional[str] = None
owner: Optional[UserPortable] = None
post_id: Optional[PostId] = None
filename: str
url: str = ''

@validator('url', pre=True, always=True)
def validate_url(cls, v, values) :
return 'emoji/' + values['filename']


class CreateRequest(BaseModel) :
_post_id_converter = validator('post_id', pre=True, always=True, allow_reuse=True)(_post_id_converter)

emoji: str
owner: Optional[str] = None
post_id: Optional[PostId] = None
filename: str


class AliasRequest(BaseModel) :
emoji: str
alias_of: str
160 changes: 160 additions & 0 deletions emojis/repository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
from typing import Optional, Self

from shared.auth import KhUser
from shared.caching import AerospikeCache
from shared.caching.key_value_store import KeyValueStore
from shared.exceptions.http_error import BadRequest, NotFound
from shared.models import PostId
from shared.sql import SqlInterface
from users.repository import Users

from .models import Emoji, InternalEmoji


kvs: KeyValueStore = KeyValueStore('kheina', 'emojis', local_TTL=60)
aliaskvs: KeyValueStore = KeyValueStore('kheina', 'emoji_alias', local_TTL=3600)
users = Users()


class EmojiRepository(SqlInterface) :

async def create(self: Self, emoji: InternalEmoji) -> None :
if emoji.alias :
raise BadRequest('cannot create an emoji with an alias')

await self.insert(emoji)


@AerospikeCache('kheina', 'emojis', '{emoji}')
async def _read(self: Self, emoji: str) -> Optional[InternalEmoji] :
data: Optional[tuple[str, Optional[str], Optional[str], Optional[int], Optional[int], str]] = await self.query_async("""
select
emojis.emoji,
emojis.alt,
emojis.alias,
emojis.owner,
emojis.post_id,
emojis.filename
from kheina.public.emojis
where emojis.emoji = %s
limit 1;
""", (
emoji,
),
fetch_one=True,
)

if not data :
return None

return InternalEmoji(
emoji = data[0],
alt = data[1],
alias = data[2],
owner = data[3],
post_id = data[4],
filename = data[5],
)


async def emoji(self: Self, user: KhUser, iemoji: InternalEmoji) -> Emoji :
return Emoji(
emoji = iemoji.alias or iemoji.emoji,
alt = iemoji.alt,
owner = await users.portable(user, await users._get_user(iemoji.owner)) if iemoji.owner else None,
post_id = PostId(iemoji.post_id) if iemoji.post_id else None,
filename = iemoji.filename,
)


@AerospikeCache('kheina', 'emoji_alias', '{emoji}', _kvs=aliaskvs)
async def aliases(self: Self, emoji: str) -> list[str] :
data: list[tuple[str]] = await self.query_async("""
select emoji
from kheina.public.emojis
where alias = %s;
""", (
emoji,
),
fetch_all = True,
)

return list(map(lambda x : x[0], data))


async def alias(self: Self, emoji: str, alias: str) -> InternalEmoji :
"""
creates a new emoji alias from the given emoji. alias will be a clone of emoji
an alias cannot be created of another alias. Use the original emoji instead.
"""

if not emoji :
raise BadRequest('empty emoji given')

if not alias :
raise BadRequest('empty alias given')

iemoji = await self._read(emoji)

if not iemoji :
raise BadRequest('emoji not found')

if iemoji.alias :
raise BadRequest('cannot create an alias of another alias')

iemoji.alias = iemoji.emoji
iemoji.emoji = alias
aliases = await self.aliases(iemoji.alias)
aliases.append(iemoji.emoji)
await kvs.put_async(iemoji.emoji, iemoji)
await aliaskvs.put_async(iemoji.alias, aliases)
return await self.insert(iemoji)


async def update(self: Self, emoji: InternalEmoji) -> None :
"""
updates an emoji and all of its aliases
aliases cannot be updated
"""
raise NotImplementedError("doesn't exist yet")
iemoji = await self._read(emoji.emoji)

if not iemoji :
raise NotFound('emoji does not exist')

if iemoji.alias :
raise BadRequest('cannot edit an alias')

aliases = await self.aliases(iemoji.emoji)

await self.query_async("""
update kheina.public.emojis
set
where emoji = any(%s);
""", (
aliases + [iemoji.emoji],
),
commit = True,
)

# update all caches
# await


async def delete(self: Self, emoji: str) -> None :
raise NotImplementedError("doesn't exist yet")


@AerospikeCache('kheina', 'emoji_search', '{emoji_substring}', TTL_hours=1)
async def list(self: Self, emoji_substring: str) -> list[str] :
data: list[tuple[str]] = await self.query_async("""
select emojis.emoji
from kheina.public.emojis
where emojis.emoji like '%%' || %s || '%%';
""", (
emoji_substring,
),
fetch_all = True,
)

return list(map(lambda x : x[0], data))
53 changes: 53 additions & 0 deletions emojis/router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from fastapi import APIRouter

from shared.models.auth import Scope
from shared.server import Request
from shared.timing import timed

from .emoji import Emojis
from .models import AliasRequest, CreateRequest, Emoji


emojiRouter = APIRouter(
prefix='/emoji',
)
emojisRouter = APIRouter(
prefix='/emojis',
)

emojis = Emojis()


@emojiRouter.put('')
@timed.root
async def v1CreateEmoji(req: Request, body: CreateRequest) -> Emoji :
await req.user.verify_scope(Scope.admin)
return await emojis.create(req.user, body)


@emojiRouter.put('/alias')
@timed.root
async def v1CreateAlias(req: Request, body: AliasRequest) -> Emoji :
await req.user.verify_scope(Scope.admin)
return await emojis.alias(req.user, body)


@emojiRouter.get('/{emoji}')
@timed.root
async def v1GetEmoji(req: Request, emoji: str) -> Emoji :
return await emojis.read(req.user, emoji)


@emojisRouter.get('/{emoji}')
@timed.root
async def v1ListEmojis(emoji: str) -> list[str] :
return await emojis.list(emoji)


app = APIRouter(
prefix='/v1',
tags=['emoji'],
)

app.include_router(emojiRouter)
app.include_router(emojisRouter)
1 change: 1 addition & 0 deletions images/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.
Loading

0 comments on commit 7ed6f66

Please sign in to comment.