Skip to content

Commit

Permalink
Merge pull request #35 from milselarch/feat/user_deletion
Browse files Browse the repository at this point in the history
Feat/user deletion
  • Loading branch information
milselarch authored Oct 13, 2024
2 parents ccca59a + 78d9020 commit 587eb0e
Show file tree
Hide file tree
Showing 13 changed files with 851 additions and 220 deletions.
38 changes: 38 additions & 0 deletions .github/workflows/algo_tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Core Algorithm Tests

on: [pull_request]

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.12'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
- name: Install Rust
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source $HOME/.cargo/env
- name: Build Maturin crate
run: |
source venv/bin/activate
maturin develop --bindings pyo3 --release
- name: Run tests
run: |
source venv/bin/activate
python -m pytest tests/enum_test.py tests/test_ranked_vote.py
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ celerybeat.pid
# config files
*.yml
!config.example.yml
!.github/**/*.yml

# Environments
.env
Expand Down
106 changes: 75 additions & 31 deletions BaseAPI.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,25 @@
import json
import secrets
import string

import telegram
import time
import hashlib
import textwrap
import dataclasses

from telegram.ext import ApplicationBuilder

import database
import aioredlock
import ranked_choice_vote
import redis

from enum import IntEnum
from typing_extensions import Any
from collections import defaultdict
from ranked_choice_vote import ranked_choice_vote
from strenum import StrEnum
from load_config import TELEGRAM_BOT_TOKEN

from typing import List, Dict, Optional, Tuple
from result import Ok, Err, Result
Expand All @@ -27,7 +33,7 @@
Polls, PollVoters, UsernameWhitelist, PollOptions, VoteRankings,
db, Users
)
from database.database import PollWinners, BaseModel, UserID
from database.database import PollWinners, BaseModel, UserID, PollMetadata
from aioredlock import Aioredlock, LockError


Expand Down Expand Up @@ -89,17 +95,6 @@ class PollMessage(object):
reply_markup: Optional[InlineKeyboardMarkup]


@dataclasses.dataclass
class PollMetadata(object):
id: int
question: str
num_voters: int
num_votes: int

open_registration: bool
closed: bool


class GetPollWinnerStatus(IntEnum):
CACHED = 0
NEWLY_COMPUTED = 1
Expand All @@ -121,12 +116,56 @@ class BaseAPI(object):
POLL_WINNER_LOCK_KEY = "POLL_WINNER_LOCK"
# CACHE_LOCK_NAME = "REDIS_CACHE_LOCK"
POLL_CACHE_EXPIRY = 60
DELETION_TOKEN_EXPIRY = 60 * 5
SHORT_HASH_LENGTH = 6

def __init__(self):
database.initialize_db()
self.redis_cache = redis.Redis()
self.redis_lock_manager = Aioredlock()

@staticmethod
def __get_telegram_token():
# TODO: move methods using tele token to a separate class
return TELEGRAM_BOT_TOKEN

def generate_delete_token(self, user: Users):
stamp = int(time.time())
hex_stamp = hex(stamp)[2:].upper()
user_id = user.get_user_id()
hash_input = f'{user_id}:{stamp}'

signed_message = self.sign_message(hash_input).upper()
short_signed_message = signed_message[:self.SHORT_HASH_LENGTH]
return f'{hex_stamp}:{short_signed_message}'

def validate_delete_token(
self, user: Users, stamp: int, short_hash: str
) -> Result[bool, str]:
current_stamp = int(time.time())
if abs(current_stamp - stamp) > self.DELETION_TOKEN_EXPIRY:
return Err('Token expired')

user_id = user.get_user_id()
hash_input = f'{user_id}:{stamp}'
signed_message = self.sign_message(hash_input).upper()
short_signed_message = signed_message[:self.SHORT_HASH_LENGTH]

if short_signed_message != short_hash:
return Err('Invalid token')

return Ok(True)

@classmethod
def create_tele_bot(cls):
return telegram.Bot(token=cls.__get_telegram_token())

@classmethod
def create_application_builder(cls):
builder = ApplicationBuilder()
builder.token(cls.__get_telegram_token())
return builder

@staticmethod
def _build_cache_key(header: str, key: str):
return f"{header}:{key}"
Expand Down Expand Up @@ -166,13 +205,15 @@ def fetch_poll(poll_id: int) -> Result[Polls, MessageBuilder]:
return Ok(poll)

@classmethod
def get_num_poll_voters(cls, poll_id: int) -> Result[int, MessageBuilder]:
def get_num_active_poll_voters(
cls, poll_id: int
) -> Result[int, MessageBuilder]:
result = cls.fetch_poll(poll_id)
if result.is_err():
return result

poll = result.unwrap()
return Ok(poll.num_voters)
return Ok(poll.num_active_voters)

@staticmethod
async def refresh_lock(lock: aioredlock.Lock, interval: float):
Expand Down Expand Up @@ -261,7 +302,7 @@ def _determine_poll_winner(cls, poll_id: int) -> Optional[int]:
:return:
ID of winning option, or None if there's no winner
"""
num_poll_voters_result = cls.get_num_poll_voters(poll_id)
num_poll_voters_result = cls.get_num_active_poll_voters(poll_id)
if num_poll_voters_result.is_err():
return None

Expand Down Expand Up @@ -483,7 +524,7 @@ def _register_user_id(
return Err(UserRegistrationStatus.POLL_NOT_FOUND)

# print('NUM_VOTES', poll.num_voters, poll.max_voters)
voter_limit_reached = (poll.num_voters >= poll.max_voters)
voter_limit_reached = (poll.num_active_voters >= poll.max_voters)
if ignore_voter_limit:
voter_limit_reached = False

Expand Down Expand Up @@ -669,7 +710,7 @@ def _generate_poll_message(
poll_metadata.id, poll_metadata.question,
poll_info.poll_options, closed=poll_metadata.closed,
bot_username=bot_username,
num_voters=poll_metadata.num_voters,
num_voters=poll_metadata.num_active_voters,
num_votes=poll_metadata.num_votes
)

Expand Down Expand Up @@ -714,19 +755,9 @@ def read_poll_info(

return Ok(cls._read_poll_info(poll_id=poll_id))

@classmethod
def _read_poll_metadata(cls, poll_id: int) -> PollMetadata:
poll = Polls.select().where(Polls.id == poll_id).get()
return PollMetadata(
id=poll.id, question=poll.desc,
num_voters=poll.num_voters, num_votes=poll.num_votes,
open_registration=poll.open_registration,
closed=poll.closed
)

@classmethod
def _read_poll_info(cls, poll_id: int) -> PollInfo:
poll_metadata = cls._read_poll_metadata(poll_id)
poll_metadata = Polls.read_poll_metadata(poll_id)
poll_option_rows = PollOptions.select().where(
PollOptions.poll == poll_id
).order_by(PollOptions.option_number)
Expand Down Expand Up @@ -865,10 +896,11 @@ def make_data_check_string(

return data_check_string

@staticmethod
@classmethod
def sign_data_check_string(
data_check_string: str, bot_token: str
cls, data_check_string: str
) -> str:
bot_token = cls.__get_telegram_token()
secret_key = hmac.new(
key=b"WebAppData", msg=bot_token.encode(),
digestmod=hashlib.sha256
Expand All @@ -877,7 +909,19 @@ def sign_data_check_string(
validation_hash = hmac.new(
secret_key, data_check_string.encode(), hashlib.sha256
).hexdigest()
return validation_hash

@classmethod
def sign_message(cls, message: str) -> str:
bot_token = cls.__get_telegram_token()
secret_key = hmac.new(
key=b"SIGN_MESSAGE", msg=bot_token.encode(),
digestmod=hashlib.sha256
).digest()

validation_hash = hmac.new(
secret_key, message.encode(), hashlib.sha256
).hexdigest()
return validation_hash

@staticmethod
Expand Down
22 changes: 22 additions & 0 deletions ModifiedTeleUpdate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from database import Users
from telegram import Update as BaseTeleUpdate


class ModifiedTeleUpdate(object):
def __init__(
self, update: BaseTeleUpdate, user: Users
):
self.update: BaseTeleUpdate = update
self.user: Users = user

@property
def callback_query(self):
return self.update.callback_query

@property
def message(self):
return self.update.message

@property
def effective_message(self):
return self.update.effective_message
Loading

0 comments on commit 587eb0e

Please sign in to comment.