Skip to content

Commit 901a5af

Browse files
authored
Merge pull request #103 from rmobis/rmobis/clm-integration
CLM plugin integration
2 parents 74141c2 + 7177aa9 commit 901a5af

8 files changed

Lines changed: 170 additions & 29 deletions

File tree

app_setup.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from flask import Flask
2+
import config
3+
from flask_recaptcha import ReCaptcha # type: ignore
4+
5+
app = Flask(__name__)
6+
7+
isProd = config.IS_PROD
8+
9+
# Set secret key for Flask App.
10+
app.config['SECRET_KEY'] = config.SECRET_KEY
11+
12+
if isProd:
13+
# Keys for Google reCAPTCHA.
14+
app.config['RECAPTCHA_SITE_KEY'] = config.RECAPTCHA_SITE_KEY
15+
app.config['RECAPTCHA_SECRET_KEY'] = config.RECAPTCHA_SECRET_KEY
16+
# initialize reCAPTCHA
17+
recaptcha = ReCaptcha(app)
18+
else:
19+
recaptcha = "Disabled for DEV"
20+
21+
22+
# email service account.
23+
taskapp_email = config.SECRET_KEY
24+
25+
# specifies database to use.
26+
db = config.MONGO_CLIENT["TaskAppLoginDB"]

dev_env.ps1

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
$env:FLASK_ENV='development'
2+
$env:MONGO_URI='mongodb://root:example@localhost:27017/'
3+
$env:SENDGRID_API_KEY='192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf'
4+
$env:SECRET_KEY='192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf'
5+
$env:SERVER_PORT='8080'

task_api.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import bcrypt
2+
import datetime
3+
import jwt
4+
5+
from flask import Response, request
6+
from functools import wraps
7+
from http import HTTPStatus
8+
9+
from task_database import complete_task, generate_task, get_user, manual_complete_tasks, manual_revert_tasks
10+
from app_setup import app, db
11+
from tasklists import get_task_tier, list_for_tier
12+
from user_dao import UserDatabaseObject
13+
14+
15+
def token_required_v2(f):
16+
@wraps(f)
17+
def decorated(*args, **kwargs):
18+
token = None
19+
if 'Authorization' in request.headers:
20+
auth_header = request.headers['Authorization']
21+
token = auth_header.split(" ")[1] if " " in auth_header else auth_header
22+
23+
if not token:
24+
return { 'error': 'No token found' }, HTTPStatus.UNAUTHORIZED
25+
26+
try:
27+
data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
28+
user = get_user(data['sub'])
29+
except:
30+
return { 'error': 'Token is invalid' }, HTTPStatus.UNAUTHORIZED
31+
32+
return f(user, *args, **kwargs)
33+
return decorated
34+
35+
36+
@app.route('/api/v2/login', methods=['POST'])
37+
def apiv2_login():
38+
body = request.json
39+
if not body or not body['username'] or not body['password']:
40+
return { 'error': 'Missing username and/or password' }, HTTPStatus.UNAUTHORIZED
41+
42+
users = db['users']
43+
user = users.find_one({ 'username': body['username'] }, { '_id': 0 })
44+
if not user:
45+
return { 'error': 'Invalid credentials' }, HTTPStatus.UNAUTHORIZED
46+
47+
if not bcrypt.checkpw(body['password'].encode('utf-8'), user['hashed_password']):
48+
return { 'error': 'Invalid credentials' }, HTTPStatus.UNAUTHORIZED
49+
50+
now = datetime.datetime.now(datetime.timezone.utc)
51+
token = jwt.encode({
52+
'sub': user['username'],
53+
'iat': now,
54+
'exp': now + datetime.timedelta(hours=24)
55+
}, app.config['SECRET_KEY'])
56+
57+
return { 'token': token }
58+
59+
60+
@app.route('/api/v2/task-list', methods=['GET'])
61+
def apiv2_get_task_list():
62+
tiers = request.args.getlist('tier') or ['easy', 'medium', 'hard', 'elite', 'master']
63+
64+
return { tier: list_for_tier(tier) for tier in tiers }
65+
66+
67+
@app.route('/api/v2/user/profile', methods=['GET'])
68+
@token_required_v2
69+
def apiv2_get_user_profile(user: UserDatabaseObject):
70+
return {
71+
'username': user.username,
72+
'is_official': user.is_official,
73+
'is_lms_enabled': user.lms_enabled,
74+
'active_task_id': user.current_task_id(),
75+
'completed_tasks': [
76+
*user.easy.completed_tasks,
77+
*user.medium.completed_tasks,
78+
*user.hard.completed_tasks,
79+
*user.elite.completed_tasks,
80+
*user.master.completed_tasks,
81+
]
82+
}
83+
84+
85+
@app.route('/api/v2/user/tasks/<id>', methods=['PATCH'])
86+
@token_required_v2
87+
def apiv2_update_user_task(user: UserDatabaseObject, id: str) -> None:
88+
tier = get_task_tier(id)
89+
body = request.json
90+
91+
if body['completed'] == True:
92+
if user.current_task_id() == id:
93+
complete_task(user.username)
94+
else:
95+
manual_complete_tasks(user.username, tier, id)
96+
elif body['completed'] == False:
97+
manual_revert_tasks(user.username, tier, id)
98+
99+
return Response(status=HTTPStatus.NO_CONTENT)
100+
101+
102+
@app.route('/api/v2/user/generate-task', methods=['POST'])
103+
@token_required_v2
104+
def apiv2_generate_task(user: UserDatabaseObject):
105+
if user.current_task():
106+
return { 'error': 'User already has an active task' }, HTTPStatus.BAD_REQUEST
107+
108+
generated_task = generate_task(user.username)
109+
if generated_task:
110+
return { 'task_id': generated_task.id }
111+
112+
return { 'error': 'No available tasks to generate' }, HTTPStatus.BAD_REQUEST

task_database.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@ def get_roll_candidates_for_tier(username: str, tier: str, min_candidates: int =
432432
'''
433433

434434

435-
def generate_task(username: str) -> TaskData or None: # type: ignore
435+
def generate_task(username: str) -> TaskData | None:
436436
user = get_user(username)
437437
if user.current_task() is not None:
438438
return

task_types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class TaskData:
2020
tip: str
2121
wiki_link: str
2222
image_link: str
23+
display_item_id: int
2324
tags: list["TaskTag"]
2425
verification: VerificationData
2526

taskapp.py

Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
from flask import Flask, render_template, request, redirect, flash, url_for, session, jsonify, make_response # type: ignore
2-
from flask_recaptcha import ReCaptcha # type: ignore
1+
from app_setup import app, isProd, db, taskapp_email, recaptcha
2+
from flask import render_template, request, redirect, flash, url_for, session, jsonify, make_response # type: ignore
33
import jwt
44
import datetime
55
import json
66
import bcrypt # type: ignore
7-
import config
87
from functools import wraps
98
import task_login
109
import tasklists
@@ -17,29 +16,7 @@
1716
import send_grid_email
1817
from templesync import check_logs, temple_player_data, import_logs
1918
from task_types import CollectionLogVerificationData
20-
21-
app = Flask(__name__)
22-
23-
isProd = config.IS_PROD
24-
25-
# Set secret key for Flask App.
26-
app.config['SECRET_KEY'] = config.SECRET_KEY
27-
28-
if isProd:
29-
# Keys for Google reCAPTCHA.
30-
app.config['RECAPTCHA_SITE_KEY'] = config.RECAPTCHA_SITE_KEY
31-
app.config['RECAPTCHA_SECRET_KEY'] = config.RECAPTCHA_SECRET_KEY
32-
# initialize reCAPTCHA
33-
recaptcha = ReCaptcha(app)
34-
else:
35-
recaptcha = "Disabled for DEV"
36-
37-
38-
# email service account.
39-
taskapp_email = config.SECRET_KEY
40-
41-
# specifies database to use.
42-
db = config.MONGO_CLIENT["TaskAppLoginDB"]
19+
import task_api
4320

4421

4522
'''

tasklists.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ def to_task_class(data: dict) -> TaskData:
1919
tip=data.get('tip'),
2020
wiki_link=data['wikiLink'],
2121
image_link=data['imageLink'],
22+
display_item_id=data['displayItemId'],
2223
tags=data.get('tags', []),
2324
verification=to_verification_data(data.get('verification')))
2425

@@ -27,6 +28,16 @@ def read_tasks(filename: str) -> list[TaskData]:
2728
json_list = json.load(f)
2829
return list(map(to_task_class, json_list.get('tasks')))
2930

31+
def get_task_tier(task_id: str) -> str:
32+
tiers = ['easy', 'medium', 'hard', 'elite', 'master', 'passive', 'extra', 'pets']
33+
for tier in tiers:
34+
tier_tasks = list_for_tier(tier)
35+
is_task_in_tier = next((True for task in tier_tasks if task.id == task_id), False)
36+
if (is_task_in_tier):
37+
return tier
38+
39+
return None
40+
3041

3142
easy = read_tasks('easy')
3243
medium = read_tasks('medium')

user_dao.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,15 +64,15 @@ def get_task_list(self, tier: str) -> UserTaskList:
6464
}[tier]
6565

6666

67-
def current_task_for_tier(self, tier: str) -> tuple or None: # type: ignore
67+
def current_task_for_tier(self, tier: str) -> tuple | None:
6868
user_task_list = self.get_task_list(tier)
6969
if user_task_list.current_task is None:
7070
return None
7171
task = task_info_for_id(tasklists.list_for_tier(tier), user_task_list.current_task.id)
7272
# TODO Fix this format
7373
return task.name, task.image_link, tier, task.id, task.tip, task.wiki_link, task.image_link
7474

75-
def current_task(self) -> tuple or None: # type: ignore
75+
def current_task(self) -> tuple | None:
7676
if self.easy.current_task is not None:
7777
return self.current_task_for_tier('easyTasks')
7878
elif self.medium.current_task is not None:
@@ -86,6 +86,15 @@ def current_task(self) -> tuple or None: # type: ignore
8686
else:
8787
return None
8888

89+
def current_task_id(self) -> str | None:
90+
current_task = self.current_task()
91+
92+
if not current_task:
93+
return None
94+
95+
return current_task[3]
96+
97+
8998
def get_tier_progress(self, tier: str) -> TierProgress:
9099
# remove instance of duplicate uuid in completed tasks
91100
def clean_tasklists(tier: str):

0 commit comments

Comments
 (0)