Add freemium access economy and token store

This commit is contained in:
hz
2026-07-27 22:20:21 -05:00
parent 1d5d03a4f6
commit 4bbac27b84
40 changed files with 1807 additions and 37 deletions
+33 -2
View File
@@ -19,6 +19,7 @@ signal question_rating_updated(question_id: String, question_elo: int, rating: D
signal question_skipped(question_id: String, correct_choice_id: String, question_elo: int, rating: Dictionary)
signal question_report_submitted(question_id: String, report_id: String)
signal question_report_failed(question_id: String, message: String)
signal access_required(message: String)
var questions: Array = []
var request: HTTPRequest
@@ -157,6 +158,14 @@ func _on_api_questions_loaded(result: int, response_code: int, _headers: PackedS
if response_code == 401:
AuthState.handle_unauthorized(questions_request_token)
if result != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300:
if response_code == 403:
var denied: Variant = JSON.parse_string(body.get_string_from_utf8())
if typeof(denied) == TYPE_DICTIONARY and str(denied.get("code", "")) == "qbank_limit_reached":
AccessService.apply_embedded_status(denied.get("access", {}))
var denied_message := str(denied.get("message", "Your free QBank allowance is exhausted."))
_set_load_error(denied_message, false)
access_required.emit(denied_message)
return
var message := "The connection to the question service was interrupted."
if response_code > 0:
message = "Questions API request failed with status %d." % response_code
@@ -400,6 +409,7 @@ func _enqueue_attempt(kind: String, question_id: String, choice_id: String, pres
"choice_id": choice_id,
"presented_choice_ids": presented_choice_ids.duplicate(),
"duration_seconds": maxi(0, duration_seconds),
"completion_id": _new_completion_id(question_id),
"retry_count": 0
})
_save_attempt_queue()
@@ -421,6 +431,10 @@ func _process_attempt_queue() -> void:
return
var queued: Dictionary = attempt_queue[active_attempt_index]
if str(queued.get("completion_id", "")).is_empty():
queued["completion_id"] = _new_completion_id(str(queued.get("question_id", "")))
attempt_queue[active_attempt_index] = queued
_save_attempt_queue()
attempt_request_token = AuthState.access_token
var headers := PackedStringArray([
"Content-Type: application/json",
@@ -429,13 +443,18 @@ func _process_attempt_queue() -> void:
var question_id := str(queued.get("question_id", ""))
var url := question_attempt_api_url
var body := JSON.stringify({
"completionId": str(queued.get("completion_id", "")),
"questionExternalId": question_id,
"choiceId": str(queued.get("choice_id", "")),
"presentedChoiceIds": queued.get("presented_choice_ids", []),
"durationSeconds": int(queued.get("duration_seconds", 0))
})
if str(queued.get("kind", "answer")) == "skip":
url = "%s/%s/skip" % [question_api_base_url.rstrip("/"), question_id.uri_encode()]
url = "%s/%s/skip?completionId=%s" % [
question_api_base_url.rstrip("/"),
question_id.uri_encode(),
str(queued.get("completion_id", "")).uri_encode()
]
body = ""
attempt_request_active = true
@@ -474,7 +493,14 @@ func _on_first_attempt_recorded(result: int, response_code: int, _headers: Packe
if response_code == 401:
AuthState.handle_unauthorized(attempt_request_token)
active_attempt_index = -1
elif response_code in [400, 403, 404]:
elif response_code == 403:
var denied: Variant = JSON.parse_string(body.get_string_from_utf8())
if typeof(denied) == TYPE_DICTIONARY:
AccessService.apply_embedded_status(denied.get("access", {}))
access_required.emit(str(denied.get("message", "Your free QBank allowance is exhausted.")))
_remove_active_attempt()
_process_attempt_queue()
elif response_code in [400, 404]:
push_warning("Discarding invalid question attempt with status %d." % response_code)
_remove_active_attempt()
_process_attempt_queue()
@@ -492,9 +518,14 @@ func _on_first_attempt_recorded(result: int, response_code: int, _headers: Packe
_apply_skip_response(parsed)
else:
_apply_first_attempt_response(parsed)
AccessService.apply_embedded_status(parsed.get("access", {}))
_process_attempt_queue()
func _new_completion_id(question_id: String) -> String:
return "%s-%d-%d" % [question_id.sha256_text().left(12), Time.get_ticks_usec(), randi()]
func _apply_first_attempt_response(parsed: Dictionary) -> void:
var question_id := str(parsed.get("questionExternalId", parsed.get("question_external_id", "")))