Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add links in event description #2

Merged
merged 16 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,4 @@ data_*.txt

test/
docs/
database.db
16 changes: 0 additions & 16 deletions .github/workflows/build_image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,6 @@ jobs:
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Install requirements
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt

- name: Run Black check
run: black --check .

- name: Run Pytest
run: pytest

- name: Login to Docker Hub
uses: docker/login-action@v3
Expand Down
36 changes: 36 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: test

on:
push:
branches:
- "main"
paths-ignore:
- 'docs'
- '**.md'
pull_request:
branches:
- "main"

jobs:
test_and_lint:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install requirements
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt

- name: Run Black check
run: black --check .

- name: Run Mypy
run: mypy .

- name: Run Pytest
run: pytest
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,5 @@ cython_debug/
*.ics

data_*.txt
database.db

11 changes: 8 additions & 3 deletions Readme.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[![Github Container Pulls](https://img.shields.io/badge/Github%20Container%20Pulls-960-blue
[![Github Container Pulls](https://img.shields.io/badge/Github%20Container%20Pulls-1K-blue
)](https://github.com/tbmc/sporteasy-calendar-connector/pkgs/container/sporteasy-calendar-connector)
[![Docker Pulls](https://img.shields.io/docker/pulls/tbmc/sporteasy-calendar-connector)](https://hub.docker.com/r/tbmc/sporteasy-calendar-connector)

Expand Down Expand Up @@ -71,10 +71,15 @@ You can use mine, but at your own risk.
:warning: Data in base64 are not ciphered.

``
https://sporteasy-calendar-connector.tbmc.ovh?data={base64Data}
https://sporteasy-calendar-connector.tbmc.ovh/api?data={base64Data}
``

You can add a parameter `disable_save_login` to disable saving of logins and password, but it deactivates links in event description to set present or absent. Without saving logins, it can not connect to SportEasy servers.

``
https://sporteasy-calendar-connector.tbmc.ovh/api?data={base64Data}&disable_save_login=True
``

## Info

SportEasy block IPs from server providers, so you should have a domestic IP.

32 changes: 31 additions & 1 deletion app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,15 @@

import flask
from flask import send_from_directory
from werkzeug.exceptions import BadRequestKeyError

from calendar_connector.calendar_converter import CalendarConverter
from calendar_connector.consts import route_change_presence, PRESENCE
from calendar_connector.data_decoder import decode_data
from calendar_connector.sporteasy_connector import SporteasyConnector
from calendar_connector.database.user import generate_links_data
from calendar_connector.custom_exceptions import BadTokenException
from calendar_connector.presence_updater import set_presence_to_event

logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
Expand All @@ -33,8 +39,12 @@ def request_handler() -> flask.Response:
ip = flask.request.remote_addr
logging.info(f"New incoming request from {ip=} and {username=}")

url_root = flask.request.url_root
disable_save_login = flask.request.args.get("disable_save_login") is not None
calendar_converter = CalendarConverter()
calendar_text = calendar_converter.get_calendar_text(username, password, team_id)
calendar_text = calendar_converter.get_calendar_text(
username, password, not disable_save_login, url_root, team_id
)

return flask.Response(
calendar_text,
Expand All @@ -43,6 +53,26 @@ def request_handler() -> flask.Response:
)


@app.route(route_change_presence)
def change_my_presence() -> flask.Response:
try:
team_id = flask.request.args["team_id"]
event_id = flask.request.args["event_id"]
user_id = flask.request.args["user_id"]
token = flask.request.args["token"]
presence = flask.request.args["presence"].lower() == PRESENCE.present
except BadRequestKeyError as e:
return flask.Response("Parameter missing", status=500)

hash_token = generate_links_data(team_id, event_id, user_id, presence)
if token != hash_token:
raise BadTokenException()

set_presence_to_event(int(team_id), int(event_id), int(user_id), presence)

return flask.send_file("calendar_connector/html/auto_close.html")


@app.route("/api/list-teams")
def list_teams() -> flask.Response:
if flask.request.method == "OPTIONS":
Expand Down
67 changes: 60 additions & 7 deletions calendar_connector/calendar_converter.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
from typing import Optional, cast

from icalendar import Calendar, vText

from calendar_connector.database.user import save_user
from calendar_connector.datetime_utils import (
get_current_datetime,
get_formated_current_time,
)
from calendar_connector.env import load_env_data
from calendar_connector.event_convertor import event_to_calendar_event
from calendar_connector.event_utils.description import GenerateLinksData
from calendar_connector.sporteasy_connector import SporteasyConnector


Expand All @@ -13,9 +17,50 @@ def __init__(self) -> None:
self.connector = SporteasyConnector()

def get_calendar_text(
self, username: str, password: str, team_id: str | None = None
self,
username: str,
password: str,
save_login: bool,
url_root: str,
team_id: Optional[str] = None,
) -> str:
"""
Retrieves the calendar text from SportEasy for the specified team.

Args:
username (str): The username for the SportEasy account.
password (str): The password for the SportEasy account.
save_login (bool): If True, save the login credentials.
url_root (str): The base URL for the SportEasy API.
team_id (Optional[str], optional): The ID of the team to retrieve events for. Defaults to None.

Returns:
str: The calendar text in iCalendar format.

Raises:
Exception: If an error occurs while retrieving the calendar text.

Example:
```python
calendar_converter = CalendarConverter()
calendar_content = calendar_converter.get_calendar_text(
"example_username", "example_password", False, "http://localhost:5000/", "12345"
)
```
"""
self.connector.login(username, password)

links_data: Optional[GenerateLinksData] = None
if save_login:
user = save_user(username, password)
links_data = GenerateLinksData(
cast(int, user.id),
cast(str, username),
cast(str, password),
cast(str, user.salt),
url_root,
)

teams = self.connector.list_teams()

cal = Calendar()
Expand All @@ -28,12 +73,14 @@ def get_calendar_text(
cal.add("x-wr-calname", "SportEasy Calendar")
cal.add("x-wr-timezone", "Europe/Paris")

formatted_time = get_current_datetime().strftime("%Y-%m-%d %H:%M:%S")
cal.add("x-wr-caldesc", f"SportEasy Calendar | Last sync: {formatted_time}")
cal.add(
"x-wr-caldesc",
f"SportEasy Calendar | Last sync: {get_formated_current_time()}",
)
cal.add("REFRESH-INTERVAL;VALUE=DURATION", "PT8H")
cal.add("X-PUBLISHED-TTL", "PT8H")

for current_team_id, team_name in teams:
for current_team_id, team_name, team_url in teams:
# Ignore other teams
if (
team_id is not None
Expand All @@ -43,7 +90,11 @@ def get_calendar_text(
continue
events = self.connector.list_events(current_team_id)
for event in events:
cal.add_component(event_to_calendar_event(team_name, event))
cal.add_component(
event_to_calendar_event(
current_team_id, team_name, event, links_data, team_url
)
)

text_calendar: str = cal.to_ical().decode("utf-8").strip()

Expand All @@ -54,7 +105,9 @@ def main() -> None:
username, password, team_id = load_env_data()

calendar_converter = CalendarConverter()
calendar_content = calendar_converter.get_calendar_text(username, password, team_id)
calendar_content = calendar_converter.get_calendar_text(
username, password, False, "http://localhost:5000/", team_id
)
with open("./test.ics", "w", encoding="utf-8") as f:
f.write(calendar_content)

Expand Down
22 changes: 18 additions & 4 deletions calendar_connector/consts.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
from typing import Any
from collections import namedtuple
import pytz

url_authenticate = "https://api.sporteasy.net/v2.1/account/authenticate/"
url_list_teams = "https://api.sporteasy.net/v2.1/me/teams/"
url_list_seasons = "https://api.sporteasy.net/v2.1/teams/{team_id}/seasons/"
url_list_events = "https://api.sporteasy.net/v2.1/teams/{team_id}/events/"
url_base = "https://api.sporteasy.net/v2.1"
url_authenticate = f"{url_base}/account/authenticate/"
url_csrf = f"{url_base}/account/csrf/"
url_me = f"{url_base}/me/"
url_list_teams = f"{url_base}/me/teams/"

url_team_id_base = url_base + "/teams/{team_id}"
url_list_seasons = f"{url_team_id_base}/seasons/"
url_list_events = f"{url_team_id_base}/events/"

url_event_base = url_team_id_base + "/events/{event_id}"
url_put_event_presence = url_event_base + "/profiles/{profile_id}/"

route_change_presence = "/api/change_my_presence"

PLAYED_WORDS = "played", "present", "available"
EVENT_TYPE = dict[str, int | str | Any | list[Any] | dict[str, Any]]
Expand All @@ -29,3 +40,6 @@
"unavailable": "CANCELLED",
"not_played": "CANCELLED",
}

_presence_type = namedtuple("_presence_type", ["present", "absent"])
PRESENCE = _presence_type("yes", "no")
31 changes: 31 additions & 0 deletions calendar_connector/cryptography.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import string
import random
import hashlib

from calendar_connector.consts import PRESENCE

_alphabet = string.printable


def generate_salt() -> str:
chars: list[str] = []
for i in range(random.randint(30, 50)):
chars.append(random.choice(_alphabet))
return "".join(chars)


def generate_hash(
team_id: int | str,
event_id: int | str,
user_id: int | str,
username: str,
password: str,
salt: str,
presence: bool,
) -> str:
presence_str = PRESENCE.present if presence else PRESENCE.absent
to_hash = (
f"{team_id}:{event_id}:{user_id}:{username}:{password}:{salt}:{presence_str}"
)
m = hashlib.sha3_256(to_hash.encode("utf-8"))
return m.hexdigest()
19 changes: 19 additions & 0 deletions calendar_connector/custom_exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from typing import Optional


class AttributeNotFoundException(Exception):
def __init__(self, name: str) -> None:
super().__init__(f"{name} is not found")


class BadTokenException(Exception):
def __init__(self) -> None:
super().__init__(f"Your token is not valid")


class TooManyUsersException(Exception):
def __init__(self, mail: str, n: Optional[int] = None) -> None:
super().__init__(
f'There are too many users with the same mail "{mail}", this should not happen.'
+ ("" if n is None else f"Number of users {n}.")
)
Empty file.
3 changes: 3 additions & 0 deletions calendar_connector/database/all_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from calendar_connector.database.user import User

ALL_MODELS = [User]
10 changes: 10 additions & 0 deletions calendar_connector/database/base_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from peewee import Model

from calendar_connector.database.db_connector import get_db

db = get_db()


class BaseModel(Model):
class Meta:
database = db
12 changes: 12 additions & 0 deletions calendar_connector/database/create_tables.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from peewee import SqliteDatabase

from calendar_connector.database.db_connector import get_db
from calendar_connector.database.all_models import ALL_MODELS


def create_db(db: SqliteDatabase) -> None:
db.create_tables(ALL_MODELS)


if __name__ == "__main__":
create_db(get_db())
Loading