Mail support
All checks were successful
Deploy Promiscuity Auth API / deploy (push) Successful in 48s
Deploy Promiscuity Character API / deploy (push) Successful in 59s
Deploy Promiscuity Inventory API / deploy (push) Successful in 46s
Deploy Promiscuity Locations API / deploy (push) Successful in 1m0s
Deploy Promiscuity Mail API / deploy (push) Successful in 1m9s
k8s smoke test / test (push) Successful in 9s
All checks were successful
Deploy Promiscuity Auth API / deploy (push) Successful in 48s
Deploy Promiscuity Character API / deploy (push) Successful in 59s
Deploy Promiscuity Inventory API / deploy (push) Successful in 46s
Deploy Promiscuity Locations API / deploy (push) Successful in 1m0s
Deploy Promiscuity Mail API / deploy (push) Successful in 1m9s
k8s smoke test / test (push) Successful in 9s
This commit is contained in:
parent
aa99788268
commit
a8db66b93e
78
.gitea/workflows/deploy-mail.yml
Normal file
78
.gitea/workflows/deploy-mail.yml
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
name: Deploy Promiscuity Mail API
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
workflow_dispatch: {}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: self-hosted
|
||||||
|
|
||||||
|
env:
|
||||||
|
IMAGE_NAME: promiscuity-mail:latest
|
||||||
|
IMAGE_TAR: /tmp/promiscuity-mail.tar
|
||||||
|
NODES: "192.168.86.72 192.168.86.73 192.168.86.74"
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Build Docker image
|
||||||
|
run: |
|
||||||
|
cd microservices/MailApi
|
||||||
|
docker build -t "${IMAGE_NAME}" .
|
||||||
|
|
||||||
|
- name: Save Docker image to TAR
|
||||||
|
run: |
|
||||||
|
docker save "${IMAGE_NAME}" -o "${IMAGE_TAR}"
|
||||||
|
|
||||||
|
- name: Copy TAR to nodes
|
||||||
|
run: |
|
||||||
|
for node in ${NODES}; do
|
||||||
|
scp -o StrictHostKeyChecking=no "${IMAGE_TAR}" hz@"$node":/tmp/promiscuity-mail.tar
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Import image on nodes
|
||||||
|
run: |
|
||||||
|
for node in ${NODES}; do
|
||||||
|
ssh -o StrictHostKeyChecking=no hz@"$node" "sudo ctr -n k8s.io images import /tmp/promiscuity-mail.tar"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Clean TAR from nodes
|
||||||
|
run: |
|
||||||
|
for node in ${NODES}; do
|
||||||
|
ssh -o StrictHostKeyChecking=no hz@"$node" "rm -f /tmp/promiscuity-mail.tar"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Clean TAR on runner
|
||||||
|
run: |
|
||||||
|
rm -f "${IMAGE_TAR}"
|
||||||
|
|
||||||
|
- name: Write kubeconfig from secret
|
||||||
|
env:
|
||||||
|
KUBECONFIG_CONTENT: ${{ secrets.KUBECONFIG }}
|
||||||
|
run: |
|
||||||
|
mkdir -p /tmp/kube
|
||||||
|
printf '%s\n' "$KUBECONFIG_CONTENT" > /tmp/kube/config
|
||||||
|
|
||||||
|
- name: Create namespace if missing
|
||||||
|
env:
|
||||||
|
KUBECONFIG: /tmp/kube/config
|
||||||
|
run: |
|
||||||
|
kubectl create namespace promiscuity-mail --dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
|
||||||
|
- name: Apply Mail deployment & service
|
||||||
|
env:
|
||||||
|
KUBECONFIG: /tmp/kube/config
|
||||||
|
run: |
|
||||||
|
kubectl apply -f microservices/MailApi/k8s/deployment.yaml -n promiscuity-mail
|
||||||
|
kubectl apply -f microservices/MailApi/k8s/service.yaml -n promiscuity-mail
|
||||||
|
|
||||||
|
- name: Restart Mail deployment
|
||||||
|
env:
|
||||||
|
KUBECONFIG: /tmp/kube/config
|
||||||
|
run: |
|
||||||
|
kubectl rollout restart deployment/promiscuity-mail -n promiscuity-mail
|
||||||
|
kubectl rollout status deployment/promiscuity-mail -n promiscuity-mail
|
||||||
@ -3,6 +3,7 @@ extends Node3D
|
|||||||
const CHARACTER_API_URL := "https://pchar.ranaze.com/api/Characters"
|
const CHARACTER_API_URL := "https://pchar.ranaze.com/api/Characters"
|
||||||
const LOCATION_API_URL := "https://ploc.ranaze.com/api/Locations"
|
const LOCATION_API_URL := "https://ploc.ranaze.com/api/Locations"
|
||||||
const INVENTORY_API_URL := "https://pinv.ranaze.com/api/inventory"
|
const INVENTORY_API_URL := "https://pinv.ranaze.com/api/inventory"
|
||||||
|
const MAIL_API_URL := "https://pmail.ranaze.com/api/mail"
|
||||||
const START_SCREEN_SCENE := "res://scenes/UI/start_screen.tscn"
|
const START_SCREEN_SCENE := "res://scenes/UI/start_screen.tscn"
|
||||||
const SETTINGS_SCENE := "res://scenes/UI/Settings.tscn"
|
const SETTINGS_SCENE := "res://scenes/UI/Settings.tscn"
|
||||||
const CHARACTER_SLOT_COUNT := 6
|
const CHARACTER_SLOT_COUNT := 6
|
||||||
@ -34,6 +35,16 @@ const HEARTBEAT_INTERVAL := 10.0
|
|||||||
@onready var _target_slot_spin_box: SpinBox = $InventoryMenu/MarginContainer/Panel/VBoxContainer/ControlsPanel/VBoxContainer/ControlsRow/TargetSlotSpinBox
|
@onready var _target_slot_spin_box: SpinBox = $InventoryMenu/MarginContainer/Panel/VBoxContainer/ControlsPanel/VBoxContainer/ControlsRow/TargetSlotSpinBox
|
||||||
@onready var _quantity_spin_box: SpinBox = $InventoryMenu/MarginContainer/Panel/VBoxContainer/ControlsPanel/VBoxContainer/ControlsRow/QuantitySpinBox
|
@onready var _quantity_spin_box: SpinBox = $InventoryMenu/MarginContainer/Panel/VBoxContainer/ControlsPanel/VBoxContainer/ControlsRow/QuantitySpinBox
|
||||||
@onready var _inventory_status_label: Label = $InventoryMenu/MarginContainer/Panel/VBoxContainer/ControlsPanel/VBoxContainer/StatusLabel
|
@onready var _inventory_status_label: Label = $InventoryMenu/MarginContainer/Panel/VBoxContainer/ControlsPanel/VBoxContainer/StatusLabel
|
||||||
|
@onready var _mail_menu: CanvasLayer = $MailMenu
|
||||||
|
@onready var _mail_location_label: Label = $MailMenu/MarginContainer/Panel/VBoxContainer/MailboxLocationLabel
|
||||||
|
@onready var _mail_inbox_items: ItemList = $MailMenu/MarginContainer/Panel/VBoxContainer/Columns/InboxPanel/VBoxContainer/InboxItems
|
||||||
|
@onready var _mail_sent_items: ItemList = $MailMenu/MarginContainer/Panel/VBoxContainer/Columns/SentPanel/VBoxContainer/SentItems
|
||||||
|
@onready var _mail_selected_label: Label = $MailMenu/MarginContainer/Panel/VBoxContainer/DetailsComposePanel/VBoxContainer/SelectedMailLabel
|
||||||
|
@onready var _mail_selected_body: TextEdit = $MailMenu/MarginContainer/Panel/VBoxContainer/DetailsComposePanel/VBoxContainer/SelectedMailBody
|
||||||
|
@onready var _mail_recipient_line_edit: LineEdit = $MailMenu/MarginContainer/Panel/VBoxContainer/DetailsComposePanel/VBoxContainer/RecipientLineEdit
|
||||||
|
@onready var _mail_subject_line_edit: LineEdit = $MailMenu/MarginContainer/Panel/VBoxContainer/DetailsComposePanel/VBoxContainer/SubjectLineEdit
|
||||||
|
@onready var _mail_compose_body: TextEdit = $MailMenu/MarginContainer/Panel/VBoxContainer/DetailsComposePanel/VBoxContainer/ComposeBody
|
||||||
|
@onready var _mail_status_label: Label = $MailMenu/MarginContainer/Panel/VBoxContainer/DetailsComposePanel/VBoxContainer/StatusLabel
|
||||||
|
|
||||||
var _center_coord := Vector2i.ZERO
|
var _center_coord := Vector2i.ZERO
|
||||||
var _tiles_root: Node3D
|
var _tiles_root: Node3D
|
||||||
@ -61,6 +72,11 @@ var _heartbeat_in_flight := false
|
|||||||
var _visible_character_refresh_elapsed := 0.0
|
var _visible_character_refresh_elapsed := 0.0
|
||||||
var _heartbeat_elapsed := 0.0
|
var _heartbeat_elapsed := 0.0
|
||||||
var _remote_character_nodes: Dictionary = {}
|
var _remote_character_nodes: Dictionary = {}
|
||||||
|
var _mail_inbox: Array = []
|
||||||
|
var _mail_sent: Array = []
|
||||||
|
var _mail_request_in_flight := false
|
||||||
|
var _selected_inbox_mail_id := ""
|
||||||
|
var _selected_sent_mail_id := ""
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
@ -104,7 +120,7 @@ func _process(_delta: float) -> void:
|
|||||||
if _heartbeat_elapsed >= HEARTBEAT_INTERVAL:
|
if _heartbeat_elapsed >= HEARTBEAT_INTERVAL:
|
||||||
_heartbeat_elapsed = 0.0
|
_heartbeat_elapsed = 0.0
|
||||||
_send_presence_heartbeat()
|
_send_presence_heartbeat()
|
||||||
if _inventory_menu.visible:
|
if _inventory_menu.visible or _mail_menu.visible:
|
||||||
return
|
return
|
||||||
var target_world_pos := _get_stream_position()
|
var target_world_pos := _get_stream_position()
|
||||||
var target_coord := _world_to_coord(target_world_pos)
|
var target_coord := _world_to_coord(target_world_pos)
|
||||||
@ -117,13 +133,17 @@ func _process(_delta: float) -> void:
|
|||||||
|
|
||||||
func _input(event: InputEvent) -> void:
|
func _input(event: InputEvent) -> void:
|
||||||
if event.is_action_pressed("player_phone"):
|
if event.is_action_pressed("player_phone"):
|
||||||
if get_tree().paused:
|
if get_tree().paused or _mail_menu.visible:
|
||||||
return
|
return
|
||||||
_toggle_inventory_menu()
|
_toggle_inventory_menu()
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
|
|
||||||
func _unhandled_input(event: InputEvent) -> void:
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if event.is_action_pressed("ui_cancel") and _mail_menu.visible:
|
||||||
|
_close_mail_menu()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
return
|
||||||
if event.is_action_pressed("ui_cancel") and _inventory_menu.visible:
|
if event.is_action_pressed("ui_cancel") and _inventory_menu.visible:
|
||||||
_close_inventory_menu()
|
_close_inventory_menu()
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
@ -197,6 +217,8 @@ func _toggle_pause_menu() -> void:
|
|||||||
func _pause_game() -> void:
|
func _pause_game() -> void:
|
||||||
if _inventory_menu.visible:
|
if _inventory_menu.visible:
|
||||||
_close_inventory_menu()
|
_close_inventory_menu()
|
||||||
|
if _mail_menu.visible:
|
||||||
|
_close_mail_menu()
|
||||||
get_tree().paused = true
|
get_tree().paused = true
|
||||||
_pause_menu.visible = true
|
_pause_menu.visible = true
|
||||||
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
|
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
|
||||||
@ -209,6 +231,8 @@ func _resume_game() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _toggle_inventory_menu() -> void:
|
func _toggle_inventory_menu() -> void:
|
||||||
|
if _mail_menu.visible:
|
||||||
|
return
|
||||||
if _inventory_menu.visible:
|
if _inventory_menu.visible:
|
||||||
_close_inventory_menu()
|
_close_inventory_menu()
|
||||||
return
|
return
|
||||||
@ -235,6 +259,28 @@ func _close_inventory_menu() -> void:
|
|||||||
_set_player_menu_lock(false)
|
_set_player_menu_lock(false)
|
||||||
|
|
||||||
|
|
||||||
|
func _open_mail_menu() -> void:
|
||||||
|
_mail_menu.visible = true
|
||||||
|
_mail_status_label.text = "Loading mailbox..."
|
||||||
|
_selected_inbox_mail_id = ""
|
||||||
|
_selected_sent_mail_id = ""
|
||||||
|
_set_player_menu_lock(true)
|
||||||
|
_update_mail_location_label()
|
||||||
|
_refresh_mailbox()
|
||||||
|
|
||||||
|
|
||||||
|
func _close_mail_menu() -> void:
|
||||||
|
_mail_menu.visible = false
|
||||||
|
_mail_status_label.text = ""
|
||||||
|
_set_player_menu_lock(false)
|
||||||
|
|
||||||
|
|
||||||
|
func _update_mail_location_label() -> void:
|
||||||
|
var location_data := _get_location_data(_center_coord)
|
||||||
|
var location_name := String(location_data.get("name", "Mailbox")).strip_edges()
|
||||||
|
_mail_location_label.text = "%s (%d,%d)" % [location_name, _center_coord.x, _center_coord.y]
|
||||||
|
|
||||||
|
|
||||||
func _set_player_menu_lock(locked: bool) -> void:
|
func _set_player_menu_lock(locked: bool) -> void:
|
||||||
if _player == null:
|
if _player == null:
|
||||||
return
|
return
|
||||||
@ -509,6 +555,8 @@ func _create_object_material(object_key: String) -> StandardMaterial3D:
|
|||||||
material.albedo_color = Color(0.54, 0.36, 0.18, 1.0)
|
material.albedo_color = Color(0.54, 0.36, 0.18, 1.0)
|
||||||
elif object_key.contains("stone"):
|
elif object_key.contains("stone"):
|
||||||
material.albedo_color = Color(0.55, 0.57, 0.6, 1.0)
|
material.albedo_color = Color(0.55, 0.57, 0.6, 1.0)
|
||||||
|
elif object_key.contains("mailbox"):
|
||||||
|
material.albedo_color = Color(0.22, 0.44, 0.78, 1.0)
|
||||||
else:
|
else:
|
||||||
material.albedo_color = Color(0.85, 0.75, 0.3, 1.0)
|
material.albedo_color = Color(0.85, 0.75, 0.3, 1.0)
|
||||||
|
|
||||||
@ -896,6 +944,266 @@ func _fetch_character_inventory() -> Array:
|
|||||||
return _parse_floor_inventory_items(payload.get("items", []))
|
return _parse_floor_inventory_items(payload.get("items", []))
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_mailbox() -> void:
|
||||||
|
if _mail_request_in_flight or _character_id.is_empty():
|
||||||
|
return
|
||||||
|
_refresh_mailbox_async()
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_mailbox_async() -> void:
|
||||||
|
_mail_request_in_flight = true
|
||||||
|
var request := HTTPRequest.new()
|
||||||
|
add_child(request)
|
||||||
|
|
||||||
|
var headers := PackedStringArray()
|
||||||
|
if not AuthState.access_token.is_empty():
|
||||||
|
headers.append("Authorization: Bearer %s" % AuthState.access_token)
|
||||||
|
|
||||||
|
var err := request.request("%s/characters/%s" % [MAIL_API_URL, _character_id], headers, HTTPClient.METHOD_GET)
|
||||||
|
if err != OK:
|
||||||
|
request.queue_free()
|
||||||
|
_mail_status_label.text = "Failed to load mailbox."
|
||||||
|
_mail_request_in_flight = false
|
||||||
|
return
|
||||||
|
|
||||||
|
var result: Array = await request.request_completed
|
||||||
|
request.queue_free()
|
||||||
|
_mail_request_in_flight = false
|
||||||
|
|
||||||
|
var result_code: int = result[0]
|
||||||
|
var response_code: int = result[1]
|
||||||
|
var response_body: String = result[3].get_string_from_utf8()
|
||||||
|
if result_code != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300:
|
||||||
|
_mail_status_label.text = "Failed to load mailbox."
|
||||||
|
push_warning("Failed to load mailbox (%s/%s): %s" % [result_code, response_code, response_body])
|
||||||
|
return
|
||||||
|
|
||||||
|
var parsed: Variant = JSON.parse_string(response_body)
|
||||||
|
if typeof(parsed) != TYPE_DICTIONARY:
|
||||||
|
_mail_status_label.text = "Mailbox response was invalid."
|
||||||
|
return
|
||||||
|
|
||||||
|
var mailbox := parsed as Dictionary
|
||||||
|
_mail_inbox = _parse_mail_messages(mailbox.get("inbox", []))
|
||||||
|
_mail_sent = _parse_mail_messages(mailbox.get("sent", []))
|
||||||
|
_render_mailbox()
|
||||||
|
_mail_status_label.text = ""
|
||||||
|
|
||||||
|
|
||||||
|
func _render_mailbox() -> void:
|
||||||
|
var current_inbox_id := _selected_inbox_mail_id
|
||||||
|
var current_sent_id := _selected_sent_mail_id
|
||||||
|
_mail_inbox_items.clear()
|
||||||
|
for index in range(_mail_inbox.size()):
|
||||||
|
var message := _mail_inbox[index] as Dictionary
|
||||||
|
var prefix := "[NEW] " if String(message.get("readUtc", "")).is_empty() else ""
|
||||||
|
var text := "%s%s | %s" % [
|
||||||
|
prefix,
|
||||||
|
String(message.get("senderCharacterName", "")).strip_edges(),
|
||||||
|
String(message.get("subject", "")).strip_edges()
|
||||||
|
]
|
||||||
|
_mail_inbox_items.add_item(text)
|
||||||
|
_mail_inbox_items.set_item_metadata(index, message)
|
||||||
|
if String(message.get("id", "")).strip_edges() == current_inbox_id:
|
||||||
|
_mail_inbox_items.select(index)
|
||||||
|
|
||||||
|
_mail_sent_items.clear()
|
||||||
|
for index in range(_mail_sent.size()):
|
||||||
|
var message := _mail_sent[index] as Dictionary
|
||||||
|
var text := "%s | %s" % [
|
||||||
|
String(message.get("recipientCharacterName", "")).strip_edges(),
|
||||||
|
String(message.get("subject", "")).strip_edges()
|
||||||
|
]
|
||||||
|
_mail_sent_items.add_item(text)
|
||||||
|
_mail_sent_items.set_item_metadata(index, message)
|
||||||
|
if String(message.get("id", "")).strip_edges() == current_sent_id:
|
||||||
|
_mail_sent_items.select(index)
|
||||||
|
|
||||||
|
_update_mail_selection_display()
|
||||||
|
|
||||||
|
|
||||||
|
func _parse_mail_messages(value: Variant) -> Array:
|
||||||
|
var messages: Array = []
|
||||||
|
if typeof(value) != TYPE_ARRAY:
|
||||||
|
return messages
|
||||||
|
|
||||||
|
for entry in value:
|
||||||
|
if typeof(entry) != TYPE_DICTIONARY:
|
||||||
|
continue
|
||||||
|
var message := entry as Dictionary
|
||||||
|
messages.append({
|
||||||
|
"id": String(message.get("id", "")).strip_edges(),
|
||||||
|
"senderCharacterId": String(message.get("senderCharacterId", "")).strip_edges(),
|
||||||
|
"senderCharacterName": String(message.get("senderCharacterName", "")).strip_edges(),
|
||||||
|
"recipientCharacterId": String(message.get("recipientCharacterId", "")).strip_edges(),
|
||||||
|
"recipientCharacterName": String(message.get("recipientCharacterName", "")).strip_edges(),
|
||||||
|
"subject": String(message.get("subject", "")).strip_edges(),
|
||||||
|
"body": String(message.get("body", "")).strip_edges(),
|
||||||
|
"createdUtc": String(message.get("createdUtc", "")).strip_edges(),
|
||||||
|
"readUtc": String(message.get("readUtc", "")).strip_edges()
|
||||||
|
})
|
||||||
|
|
||||||
|
return messages
|
||||||
|
|
||||||
|
|
||||||
|
func _update_mail_selection_display() -> void:
|
||||||
|
var selected_message := {}
|
||||||
|
if not _selected_inbox_mail_id.is_empty():
|
||||||
|
selected_message = _find_mail_message_by_id(_mail_inbox, _selected_inbox_mail_id)
|
||||||
|
elif not _selected_sent_mail_id.is_empty():
|
||||||
|
selected_message = _find_mail_message_by_id(_mail_sent, _selected_sent_mail_id)
|
||||||
|
|
||||||
|
if selected_message.is_empty():
|
||||||
|
_mail_selected_label.text = "Selected Mail"
|
||||||
|
_mail_selected_body.text = ""
|
||||||
|
return
|
||||||
|
|
||||||
|
var subject := String(selected_message.get("subject", "")).strip_edges()
|
||||||
|
var sender_name := String(selected_message.get("senderCharacterName", "")).strip_edges()
|
||||||
|
var recipient_name := String(selected_message.get("recipientCharacterName", "")).strip_edges()
|
||||||
|
_mail_selected_label.text = "%s -> %s | %s" % [sender_name, recipient_name, subject]
|
||||||
|
_mail_selected_body.text = String(selected_message.get("body", "")).strip_edges()
|
||||||
|
|
||||||
|
|
||||||
|
func _find_mail_message_by_id(messages: Array, message_id: String) -> Dictionary:
|
||||||
|
for message_variant in messages:
|
||||||
|
if typeof(message_variant) != TYPE_DICTIONARY:
|
||||||
|
continue
|
||||||
|
var message := message_variant as Dictionary
|
||||||
|
if String(message.get("id", "")).strip_edges() == message_id:
|
||||||
|
return message
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
func _on_mail_inbox_selected(index: int) -> void:
|
||||||
|
var metadata: Variant = _mail_inbox_items.get_item_metadata(index)
|
||||||
|
if typeof(metadata) != TYPE_DICTIONARY:
|
||||||
|
return
|
||||||
|
var message := metadata as Dictionary
|
||||||
|
_selected_sent_mail_id = ""
|
||||||
|
_mail_sent_items.deselect_all()
|
||||||
|
_selected_inbox_mail_id = String(message.get("id", "")).strip_edges()
|
||||||
|
_update_mail_selection_display()
|
||||||
|
_mark_mail_read(_selected_inbox_mail_id)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_mail_sent_selected(index: int) -> void:
|
||||||
|
var metadata: Variant = _mail_sent_items.get_item_metadata(index)
|
||||||
|
if typeof(metadata) != TYPE_DICTIONARY:
|
||||||
|
return
|
||||||
|
var message := metadata as Dictionary
|
||||||
|
_selected_inbox_mail_id = ""
|
||||||
|
_mail_inbox_items.deselect_all()
|
||||||
|
_selected_sent_mail_id = String(message.get("id", "")).strip_edges()
|
||||||
|
_update_mail_selection_display()
|
||||||
|
|
||||||
|
|
||||||
|
func _mark_mail_read(message_id: String) -> void:
|
||||||
|
if message_id.is_empty():
|
||||||
|
return
|
||||||
|
_mark_mail_read_async(message_id)
|
||||||
|
|
||||||
|
|
||||||
|
func _mark_mail_read_async(message_id: String) -> void:
|
||||||
|
var request := HTTPRequest.new()
|
||||||
|
add_child(request)
|
||||||
|
|
||||||
|
var headers := PackedStringArray()
|
||||||
|
if not AuthState.access_token.is_empty():
|
||||||
|
headers.append("Authorization: Bearer %s" % AuthState.access_token)
|
||||||
|
|
||||||
|
var err := request.request("%s/characters/%s/messages/%s/read" % [MAIL_API_URL, _character_id, message_id], headers, HTTPClient.METHOD_POST)
|
||||||
|
if err != OK:
|
||||||
|
request.queue_free()
|
||||||
|
return
|
||||||
|
|
||||||
|
var result: Array = await request.request_completed
|
||||||
|
request.queue_free()
|
||||||
|
var result_code: int = result[0]
|
||||||
|
var response_code: int = result[1]
|
||||||
|
if result_code != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300:
|
||||||
|
return
|
||||||
|
|
||||||
|
for index in range(_mail_inbox.size()):
|
||||||
|
var message := _mail_inbox[index] as Dictionary
|
||||||
|
if String(message.get("id", "")).strip_edges() != message_id:
|
||||||
|
continue
|
||||||
|
message["readUtc"] = "read"
|
||||||
|
_mail_inbox[index] = message
|
||||||
|
break
|
||||||
|
_render_mailbox()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_mail_refresh_pressed() -> void:
|
||||||
|
_mail_status_label.text = "Refreshing mailbox..."
|
||||||
|
_refresh_mailbox()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_mail_send_pressed() -> void:
|
||||||
|
var recipient_name := _mail_recipient_line_edit.text.strip_edges()
|
||||||
|
var subject := _mail_subject_line_edit.text.strip_edges()
|
||||||
|
var body := _mail_compose_body.text.strip_edges()
|
||||||
|
if recipient_name.is_empty():
|
||||||
|
_mail_status_label.text = "Recipient character name is required."
|
||||||
|
return
|
||||||
|
if subject.is_empty():
|
||||||
|
_mail_status_label.text = "Subject is required."
|
||||||
|
return
|
||||||
|
if body.is_empty():
|
||||||
|
_mail_status_label.text = "Body is required."
|
||||||
|
return
|
||||||
|
_send_mail_async(recipient_name, subject, body)
|
||||||
|
|
||||||
|
|
||||||
|
func _send_mail_async(recipient_name: String, subject: String, body: String) -> void:
|
||||||
|
if _mail_request_in_flight:
|
||||||
|
return
|
||||||
|
_mail_request_in_flight = true
|
||||||
|
_mail_status_label.text = "Sending mail..."
|
||||||
|
|
||||||
|
var request := HTTPRequest.new()
|
||||||
|
add_child(request)
|
||||||
|
|
||||||
|
var headers := PackedStringArray()
|
||||||
|
if not AuthState.access_token.is_empty():
|
||||||
|
headers.append("Authorization: Bearer %s" % AuthState.access_token)
|
||||||
|
headers.append("Content-Type: application/json")
|
||||||
|
|
||||||
|
var payload := JSON.stringify({
|
||||||
|
"recipientCharacterName": recipient_name,
|
||||||
|
"subject": subject,
|
||||||
|
"body": body
|
||||||
|
})
|
||||||
|
var err := request.request("%s/characters/%s/send" % [MAIL_API_URL, _character_id], headers, HTTPClient.METHOD_POST, payload)
|
||||||
|
if err != OK:
|
||||||
|
request.queue_free()
|
||||||
|
_mail_request_in_flight = false
|
||||||
|
_mail_status_label.text = "Failed to send mail."
|
||||||
|
return
|
||||||
|
|
||||||
|
var result: Array = await request.request_completed
|
||||||
|
request.queue_free()
|
||||||
|
_mail_request_in_flight = false
|
||||||
|
|
||||||
|
var result_code: int = result[0]
|
||||||
|
var response_code: int = result[1]
|
||||||
|
var response_body: String = result[3].get_string_from_utf8()
|
||||||
|
if result_code != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300:
|
||||||
|
_mail_status_label.text = "Failed to send mail."
|
||||||
|
push_warning("Failed to send mail (%s/%s): %s" % [result_code, response_code, response_body])
|
||||||
|
return
|
||||||
|
|
||||||
|
_mail_recipient_line_edit.text = ""
|
||||||
|
_mail_subject_line_edit.text = ""
|
||||||
|
_mail_compose_body.text = ""
|
||||||
|
_mail_status_label.text = "Mail sent."
|
||||||
|
_refresh_mailbox()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_mail_close_pressed() -> void:
|
||||||
|
_close_mail_menu()
|
||||||
|
|
||||||
|
|
||||||
func _ensure_selected_location_exists(coord: Vector2i) -> void:
|
func _ensure_selected_location_exists(coord: Vector2i) -> void:
|
||||||
if _known_locations.has(coord):
|
if _known_locations.has(coord):
|
||||||
return
|
return
|
||||||
@ -1085,6 +1393,10 @@ func _interact_with_location_async(location_id: String, object_id: String) -> vo
|
|||||||
return
|
return
|
||||||
|
|
||||||
var interaction := parsed as Dictionary
|
var interaction := parsed as Dictionary
|
||||||
|
if String(interaction.get("objectType", "")).strip_edges() == "mailbox":
|
||||||
|
_open_mail_menu()
|
||||||
|
_interact_in_flight = false
|
||||||
|
return
|
||||||
_apply_interaction_result(location_id, interaction)
|
_apply_interaction_result(location_id, interaction)
|
||||||
await _refresh_location_inventory(location_id)
|
await _refresh_location_inventory(location_id)
|
||||||
_interact_in_flight = false
|
_interact_in_flight = false
|
||||||
|
|||||||
@ -234,6 +234,142 @@ text = "CLOSE"
|
|||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
text = ""
|
text = ""
|
||||||
|
|
||||||
|
[node name="MailMenu" type="CanvasLayer" parent="."]
|
||||||
|
visible = false
|
||||||
|
layer = 2
|
||||||
|
|
||||||
|
[node name="Overlay" type="ColorRect" parent="MailMenu"]
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
mouse_filter = 2
|
||||||
|
color = Color(0, 0, 0, 0.4)
|
||||||
|
|
||||||
|
[node name="MarginContainer" type="MarginContainer" parent="MailMenu"]
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
theme_override_constants/margin_left = 48
|
||||||
|
theme_override_constants/margin_top = 48
|
||||||
|
theme_override_constants/margin_right = 48
|
||||||
|
theme_override_constants/margin_bottom = 48
|
||||||
|
|
||||||
|
[node name="Panel" type="PanelContainer" parent="MailMenu/MarginContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="VBoxContainer" type="VBoxContainer" parent="MailMenu/MarginContainer/Panel"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 10
|
||||||
|
|
||||||
|
[node name="TitleLabel" type="Label" parent="MailMenu/MarginContainer/Panel/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Mailbox"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="MailboxLocationLabel" type="Label" parent="MailMenu/MarginContainer/Panel/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Mailbox"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="Columns" type="HBoxContainer" parent="MailMenu/MarginContainer/Panel/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
theme_override_constants/separation = 12
|
||||||
|
|
||||||
|
[node name="InboxPanel" type="PanelContainer" parent="MailMenu/MarginContainer/Panel/VBoxContainer/Columns"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="VBoxContainer" type="VBoxContainer" parent="MailMenu/MarginContainer/Panel/VBoxContainer/Columns/InboxPanel"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 8
|
||||||
|
|
||||||
|
[node name="InboxLabel" type="Label" parent="MailMenu/MarginContainer/Panel/VBoxContainer/Columns/InboxPanel/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Inbox"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="InboxItems" type="ItemList" parent="MailMenu/MarginContainer/Panel/VBoxContainer/Columns/InboxPanel/VBoxContainer"]
|
||||||
|
custom_minimum_size = Vector2(0, 220)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="SentPanel" type="PanelContainer" parent="MailMenu/MarginContainer/Panel/VBoxContainer/Columns"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="VBoxContainer" type="VBoxContainer" parent="MailMenu/MarginContainer/Panel/VBoxContainer/Columns/SentPanel"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 8
|
||||||
|
|
||||||
|
[node name="SentLabel" type="Label" parent="MailMenu/MarginContainer/Panel/VBoxContainer/Columns/SentPanel/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Sent"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="SentItems" type="ItemList" parent="MailMenu/MarginContainer/Panel/VBoxContainer/Columns/SentPanel/VBoxContainer"]
|
||||||
|
custom_minimum_size = Vector2(0, 220)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="DetailsComposePanel" type="PanelContainer" parent="MailMenu/MarginContainer/Panel/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="VBoxContainer" type="VBoxContainer" parent="MailMenu/MarginContainer/Panel/VBoxContainer/DetailsComposePanel"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 8
|
||||||
|
|
||||||
|
[node name="SelectedMailLabel" type="Label" parent="MailMenu/MarginContainer/Panel/VBoxContainer/DetailsComposePanel/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Selected Mail"
|
||||||
|
|
||||||
|
[node name="SelectedMailBody" type="TextEdit" parent="MailMenu/MarginContainer/Panel/VBoxContainer/DetailsComposePanel/VBoxContainer"]
|
||||||
|
custom_minimum_size = Vector2(0, 120)
|
||||||
|
layout_mode = 2
|
||||||
|
editable = false
|
||||||
|
|
||||||
|
[node name="RecipientLineEdit" type="LineEdit" parent="MailMenu/MarginContainer/Panel/VBoxContainer/DetailsComposePanel/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
placeholder_text = "Recipient character name"
|
||||||
|
|
||||||
|
[node name="SubjectLineEdit" type="LineEdit" parent="MailMenu/MarginContainer/Panel/VBoxContainer/DetailsComposePanel/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
placeholder_text = "Subject"
|
||||||
|
|
||||||
|
[node name="ComposeBody" type="TextEdit" parent="MailMenu/MarginContainer/Panel/VBoxContainer/DetailsComposePanel/VBoxContainer"]
|
||||||
|
custom_minimum_size = Vector2(0, 120)
|
||||||
|
layout_mode = 2
|
||||||
|
placeholder_text = "Write your mail here"
|
||||||
|
|
||||||
|
[node name="ActionRow" type="HBoxContainer" parent="MailMenu/MarginContainer/Panel/VBoxContainer/DetailsComposePanel/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 8
|
||||||
|
|
||||||
|
[node name="RefreshButton" type="Button" parent="MailMenu/MarginContainer/Panel/VBoxContainer/DetailsComposePanel/VBoxContainer/ActionRow"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme = ExtResource("4_button_theme")
|
||||||
|
text = "REFRESH"
|
||||||
|
|
||||||
|
[node name="SendButton" type="Button" parent="MailMenu/MarginContainer/Panel/VBoxContainer/DetailsComposePanel/VBoxContainer/ActionRow"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme = ExtResource("4_button_theme")
|
||||||
|
text = "SEND"
|
||||||
|
|
||||||
|
[node name="CloseButton" type="Button" parent="MailMenu/MarginContainer/Panel/VBoxContainer/DetailsComposePanel/VBoxContainer/ActionRow"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme = ExtResource("4_button_theme")
|
||||||
|
text = "CLOSE"
|
||||||
|
|
||||||
|
[node name="StatusLabel" type="Label" parent="MailMenu/MarginContainer/Panel/VBoxContainer/DetailsComposePanel/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = ""
|
||||||
|
|
||||||
[connection signal="pressed" from="PauseMenu/CenterContainer/Panel/VBoxContainer/ContinueButton" to="." method="_on_pause_continue_pressed"]
|
[connection signal="pressed" from="PauseMenu/CenterContainer/Panel/VBoxContainer/ContinueButton" to="." method="_on_pause_continue_pressed"]
|
||||||
[connection signal="pressed" from="PauseMenu/CenterContainer/Panel/VBoxContainer/SettingsButton" to="." method="_on_pause_settings_pressed"]
|
[connection signal="pressed" from="PauseMenu/CenterContainer/Panel/VBoxContainer/SettingsButton" to="." method="_on_pause_settings_pressed"]
|
||||||
[connection signal="pressed" from="PauseMenu/CenterContainer/Panel/VBoxContainer/MainMenuButton" to="." method="_on_pause_main_menu_pressed"]
|
[connection signal="pressed" from="PauseMenu/CenterContainer/Panel/VBoxContainer/MainMenuButton" to="." method="_on_pause_main_menu_pressed"]
|
||||||
@ -244,3 +380,8 @@ text = ""
|
|||||||
[connection signal="pressed" from="InventoryMenu/MarginContainer/Panel/VBoxContainer/ControlsPanel/VBoxContainer/ActionRow/PickupButton" to="." method="_on_inventory_pickup_pressed"]
|
[connection signal="pressed" from="InventoryMenu/MarginContainer/Panel/VBoxContainer/ControlsPanel/VBoxContainer/ActionRow/PickupButton" to="." method="_on_inventory_pickup_pressed"]
|
||||||
[connection signal="pressed" from="InventoryMenu/MarginContainer/Panel/VBoxContainer/ControlsPanel/VBoxContainer/ActionRow/RefreshButton" to="." method="_on_inventory_refresh_pressed"]
|
[connection signal="pressed" from="InventoryMenu/MarginContainer/Panel/VBoxContainer/ControlsPanel/VBoxContainer/ActionRow/RefreshButton" to="." method="_on_inventory_refresh_pressed"]
|
||||||
[connection signal="pressed" from="InventoryMenu/MarginContainer/Panel/VBoxContainer/ControlsPanel/VBoxContainer/ActionRow/CloseButton" to="." method="_on_inventory_close_pressed"]
|
[connection signal="pressed" from="InventoryMenu/MarginContainer/Panel/VBoxContainer/ControlsPanel/VBoxContainer/ActionRow/CloseButton" to="." method="_on_inventory_close_pressed"]
|
||||||
|
[connection signal="item_selected" from="MailMenu/MarginContainer/Panel/VBoxContainer/Columns/InboxPanel/VBoxContainer/InboxItems" to="." method="_on_mail_inbox_selected"]
|
||||||
|
[connection signal="item_selected" from="MailMenu/MarginContainer/Panel/VBoxContainer/Columns/SentPanel/VBoxContainer/SentItems" to="." method="_on_mail_sent_selected"]
|
||||||
|
[connection signal="pressed" from="MailMenu/MarginContainer/Panel/VBoxContainer/DetailsComposePanel/VBoxContainer/ActionRow/RefreshButton" to="." method="_on_mail_refresh_pressed"]
|
||||||
|
[connection signal="pressed" from="MailMenu/MarginContainer/Panel/VBoxContainer/DetailsComposePanel/VBoxContainer/ActionRow/SendButton" to="." method="_on_mail_send_pressed"]
|
||||||
|
[connection signal="pressed" from="MailMenu/MarginContainer/Panel/VBoxContainer/DetailsComposePanel/VBoxContainer/ActionRow/CloseButton" to="." method="_on_mail_close_pressed"]
|
||||||
|
|||||||
@ -2,6 +2,7 @@ using CharacterApi.Models;
|
|||||||
using CharacterApi.Services;
|
using CharacterApi.Services;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using MongoDB.Driver;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@ -48,7 +49,15 @@ public class CharactersController : ControllerBase
|
|||||||
CreatedUtc = DateTime.UtcNow
|
CreatedUtc = DateTime.UtcNow
|
||||||
};
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
await _characters.CreateAsync(character);
|
await _characters.CreateAsync(character);
|
||||||
|
}
|
||||||
|
catch (MongoWriteException ex) when (ex.WriteError.Category == ServerErrorCategory.DuplicateKey)
|
||||||
|
{
|
||||||
|
return Conflict("Character name is already taken");
|
||||||
|
}
|
||||||
|
|
||||||
return Ok(character);
|
return Ok(character);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,8 @@ public class CharacterStore
|
|||||||
|
|
||||||
var ownerIndex = Builders<Character>.IndexKeys.Ascending(c => c.OwnerUserId);
|
var ownerIndex = Builders<Character>.IndexKeys.Ascending(c => c.OwnerUserId);
|
||||||
_col.Indexes.CreateOne(new CreateIndexModel<Character>(ownerIndex));
|
_col.Indexes.CreateOne(new CreateIndexModel<Character>(ownerIndex));
|
||||||
|
var nameIndex = Builders<Character>.IndexKeys.Ascending(c => c.Name);
|
||||||
|
_col.Indexes.CreateOne(new CreateIndexModel<Character>(nameIndex, new CreateIndexOptions { Unique = true }));
|
||||||
var coordIndex = Builders<Character>.IndexKeys.Ascending("Coord.X").Ascending("Coord.Y");
|
var coordIndex = Builders<Character>.IndexKeys.Ascending("Coord.X").Ascending("Coord.Y");
|
||||||
_col.Indexes.CreateOne(new CreateIndexModel<Character>(coordIndex));
|
_col.Indexes.CreateOne(new CreateIndexModel<Character>(coordIndex));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -188,6 +188,23 @@ public class LocationsController : ControllerBase
|
|||||||
return BadRequest("Location object type is not supported");
|
return BadRequest("Location object type is not supported");
|
||||||
if (interact.Status == InteractStatus.ObjectConsumed)
|
if (interact.Status == InteractStatus.ObjectConsumed)
|
||||||
return Conflict("Location object is consumed");
|
return Conflict("Location object is consumed");
|
||||||
|
if (string.Equals(interact.ObjectType, "mailbox", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return Ok(new InteractLocationObjectResponse
|
||||||
|
{
|
||||||
|
LocationId = id,
|
||||||
|
CharacterId = req.CharacterId,
|
||||||
|
ObjectId = req.ObjectId,
|
||||||
|
ObjectType = interact.ObjectType,
|
||||||
|
ItemKey = string.Empty,
|
||||||
|
QuantityGranted = 0,
|
||||||
|
CharacterGrantedQuantity = 0,
|
||||||
|
FloorGrantedQuantity = 0,
|
||||||
|
RemainingQuantity = 0,
|
||||||
|
Consumed = false,
|
||||||
|
InventoryResponseJson = string.Empty
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
var inventoryBaseUrl = (_configuration["Services:InventoryApiBaseUrl"] ?? "http://localhost:5003").TrimEnd('/');
|
var inventoryBaseUrl = (_configuration["Services:InventoryApiBaseUrl"] ?? "http://localhost:5003").TrimEnd('/');
|
||||||
var token = Request.Headers.Authorization.ToString();
|
var token = Request.Headers.Authorization.ToString();
|
||||||
|
|||||||
@ -190,6 +190,18 @@ public class LocationStore
|
|||||||
return new InteractResult(InteractStatus.ObjectNotFound);
|
return new InteractResult(InteractStatus.ObjectNotFound);
|
||||||
if (!string.Equals(locationObject.ObjectId, objectId, StringComparison.Ordinal))
|
if (!string.Equals(locationObject.ObjectId, objectId, StringComparison.Ordinal))
|
||||||
return new InteractResult(InteractStatus.ObjectNotFound);
|
return new InteractResult(InteractStatus.ObjectNotFound);
|
||||||
|
if (string.Equals(locationObject.ObjectType, "mailbox", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return new InteractResult(
|
||||||
|
InteractStatus.Ok,
|
||||||
|
locationObject.ObjectId,
|
||||||
|
locationObject.ObjectType,
|
||||||
|
"",
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
CloneLocationObject(locationObject));
|
||||||
|
}
|
||||||
if (!string.Equals(locationObject.ObjectType, "gatherable", StringComparison.OrdinalIgnoreCase))
|
if (!string.Equals(locationObject.ObjectType, "gatherable", StringComparison.OrdinalIgnoreCase))
|
||||||
return new InteractResult(InteractStatus.UnsupportedObjectType);
|
return new InteractResult(InteractStatus.UnsupportedObjectType);
|
||||||
if (locationObject.State.RemainingQuantity <= 0)
|
if (locationObject.State.RemainingQuantity <= 0)
|
||||||
@ -462,7 +474,9 @@ public class LocationStore
|
|||||||
biomeKey = await DetermineBiomeKeyAsync(location.Coord.X, location.Coord.Y, biomeDefinitions);
|
biomeKey = await DetermineBiomeKeyAsync(location.Coord.X, location.Coord.Y, biomeDefinitions);
|
||||||
var elevation = hasElevation ? location.Elevation : await DetermineElevationAsync(location.Coord.X, location.Coord.Y, biomeKey);
|
var elevation = hasElevation ? location.Elevation : await DetermineElevationAsync(location.Coord.X, location.Coord.Y, biomeKey);
|
||||||
|
|
||||||
var migratedObject = TryMigrateLegacyResources(location) ?? CreateLocationObjectForBiome(biomeDefinitions, biomeKey, location.Coord.X, location.Coord.Y);
|
var migratedObject = TryMigrateLegacyResources(location);
|
||||||
|
if (migratedObject is null)
|
||||||
|
migratedObject = CreateLocationObjectForBiome(biomeDefinitions, biomeKey, location.Coord.X, location.Coord.Y);
|
||||||
var filter = Builders<Location>.Filter.And(
|
var filter = Builders<Location>.Filter.And(
|
||||||
Builders<Location>.Filter.Eq(l => l.Id, location.Id)
|
Builders<Location>.Filter.Eq(l => l.Id, location.Id)
|
||||||
);
|
);
|
||||||
|
|||||||
93
microservices/MailApi/Controllers/MailController.cs
Normal file
93
microservices/MailApi/Controllers/MailController.cs
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
using MailApi.Models;
|
||||||
|
using MailApi.Services;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace MailApi.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class MailController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly MailStore _mail;
|
||||||
|
|
||||||
|
public MailController(MailStore mail)
|
||||||
|
{
|
||||||
|
_mail = mail;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("characters/{characterId}")]
|
||||||
|
[Authorize(Roles = "USER,SUPER")]
|
||||||
|
public async Task<IActionResult> GetMailbox(string characterId)
|
||||||
|
{
|
||||||
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
if (string.IsNullOrWhiteSpace(userId))
|
||||||
|
return Unauthorized();
|
||||||
|
|
||||||
|
var access = await _mail.ResolveCharacterAsync(characterId, userId, User.IsInRole("SUPER"));
|
||||||
|
if (!access.Exists)
|
||||||
|
return NotFound();
|
||||||
|
if (!access.IsAuthorized)
|
||||||
|
return Forbid();
|
||||||
|
|
||||||
|
var inbox = await _mail.GetInboxAsync(characterId);
|
||||||
|
var sent = await _mail.GetSentAsync(characterId);
|
||||||
|
return Ok(new MailboxResponse
|
||||||
|
{
|
||||||
|
CharacterId = characterId,
|
||||||
|
Inbox = inbox.Select(MailMessageResponse.FromModel).ToList(),
|
||||||
|
Sent = sent.Select(MailMessageResponse.FromModel).ToList()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("characters/{characterId}/send")]
|
||||||
|
[Authorize(Roles = "USER,SUPER")]
|
||||||
|
public async Task<IActionResult> Send(string characterId, [FromBody] SendMailRequest req)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(req.RecipientCharacterName))
|
||||||
|
return BadRequest("recipientCharacterName required");
|
||||||
|
if (string.IsNullOrWhiteSpace(req.Subject))
|
||||||
|
return BadRequest("subject required");
|
||||||
|
if (string.IsNullOrWhiteSpace(req.Body))
|
||||||
|
return BadRequest("body required");
|
||||||
|
|
||||||
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
if (string.IsNullOrWhiteSpace(userId))
|
||||||
|
return Unauthorized();
|
||||||
|
|
||||||
|
var access = await _mail.ResolveCharacterAsync(characterId, userId, User.IsInRole("SUPER"));
|
||||||
|
if (!access.Exists)
|
||||||
|
return NotFound();
|
||||||
|
if (!access.IsAuthorized)
|
||||||
|
return Forbid();
|
||||||
|
|
||||||
|
var result = await _mail.SendAsync(characterId, req.RecipientCharacterName, req.Subject, req.Body);
|
||||||
|
return result.Status switch
|
||||||
|
{
|
||||||
|
MailStore.SendMailStatus.SenderNotFound => NotFound("Sender character not found"),
|
||||||
|
MailStore.SendMailStatus.RecipientNotFound => NotFound("Recipient character not found"),
|
||||||
|
MailStore.SendMailStatus.RecipientAmbiguous => Conflict("Recipient character name is ambiguous"),
|
||||||
|
MailStore.SendMailStatus.Invalid => BadRequest("Invalid recipient"),
|
||||||
|
_ => Ok(MailMessageResponse.FromModel(result.Message!))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("characters/{characterId}/messages/{messageId}/read")]
|
||||||
|
[Authorize(Roles = "USER,SUPER")]
|
||||||
|
public async Task<IActionResult> MarkRead(string characterId, string messageId)
|
||||||
|
{
|
||||||
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
if (string.IsNullOrWhiteSpace(userId))
|
||||||
|
return Unauthorized();
|
||||||
|
|
||||||
|
var access = await _mail.ResolveCharacterAsync(characterId, userId, User.IsInRole("SUPER"));
|
||||||
|
if (!access.Exists)
|
||||||
|
return NotFound();
|
||||||
|
if (!access.IsAuthorized)
|
||||||
|
return Forbid();
|
||||||
|
|
||||||
|
var message = await _mail.MarkReadAsync(characterId, messageId);
|
||||||
|
return message is null ? NotFound() : Ok(MailMessageResponse.FromModel(message));
|
||||||
|
}
|
||||||
|
}
|
||||||
5
microservices/MailApi/DOCUMENTS.md
Normal file
5
microservices/MailApi/DOCUMENTS.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# MailApi
|
||||||
|
|
||||||
|
- `GET /api/mail/characters/{characterId}` returns inbox and sent mail.
|
||||||
|
- `POST /api/mail/characters/{characterId}/send` sends mail to another character by exact character name.
|
||||||
|
- `POST /api/mail/characters/{characterId}/messages/{messageId}/read` marks an inbox message as read.
|
||||||
10
microservices/MailApi/Dockerfile
Normal file
10
microservices/MailApi/Dockerfile
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||||
|
WORKDIR /src
|
||||||
|
COPY . .
|
||||||
|
RUN dotnet publish -c Release -o /app/publish
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:8.0
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build /app/publish .
|
||||||
|
EXPOSE 5004
|
||||||
|
ENTRYPOINT ["dotnet", "MailApi.dll"]
|
||||||
16
microservices/MailApi/MailApi.csproj
Normal file
16
microservices/MailApi/MailApi.csproj
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.8" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.8" />
|
||||||
|
<PackageReference Include="MongoDB.Driver" Version="3.4.3" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
35
microservices/MailApi/Models/MailMessage.cs
Normal file
35
microservices/MailApi/Models/MailMessage.cs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
using MongoDB.Bson;
|
||||||
|
using MongoDB.Bson.Serialization.Attributes;
|
||||||
|
|
||||||
|
namespace MailApi.Models;
|
||||||
|
|
||||||
|
public class MailMessage
|
||||||
|
{
|
||||||
|
[BsonId]
|
||||||
|
[BsonRepresentation(BsonType.ObjectId)]
|
||||||
|
public string? Id { get; set; }
|
||||||
|
|
||||||
|
[BsonElement("senderCharacterId")]
|
||||||
|
public string SenderCharacterId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[BsonElement("senderCharacterName")]
|
||||||
|
public string SenderCharacterName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[BsonElement("recipientCharacterId")]
|
||||||
|
public string RecipientCharacterId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[BsonElement("recipientCharacterName")]
|
||||||
|
public string RecipientCharacterName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[BsonElement("subject")]
|
||||||
|
public string Subject { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[BsonElement("body")]
|
||||||
|
public string Body { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[BsonElement("createdUtc")]
|
||||||
|
public DateTime CreatedUtc { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
[BsonElement("readUtc")]
|
||||||
|
public DateTime? ReadUtc { get; set; }
|
||||||
|
}
|
||||||
35
microservices/MailApi/Models/MailMessageResponse.cs
Normal file
35
microservices/MailApi/Models/MailMessageResponse.cs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
namespace MailApi.Models;
|
||||||
|
|
||||||
|
public class MailMessageResponse
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string SenderCharacterId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string SenderCharacterName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string RecipientCharacterId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string RecipientCharacterName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Subject { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Body { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public DateTime CreatedUtc { get; set; }
|
||||||
|
|
||||||
|
public DateTime? ReadUtc { get; set; }
|
||||||
|
|
||||||
|
public static MailMessageResponse FromModel(MailMessage message) => new()
|
||||||
|
{
|
||||||
|
Id = message.Id ?? string.Empty,
|
||||||
|
SenderCharacterId = message.SenderCharacterId,
|
||||||
|
SenderCharacterName = message.SenderCharacterName,
|
||||||
|
RecipientCharacterId = message.RecipientCharacterId,
|
||||||
|
RecipientCharacterName = message.RecipientCharacterName,
|
||||||
|
Subject = message.Subject,
|
||||||
|
Body = message.Body,
|
||||||
|
CreatedUtc = message.CreatedUtc,
|
||||||
|
ReadUtc = message.ReadUtc
|
||||||
|
};
|
||||||
|
}
|
||||||
10
microservices/MailApi/Models/MailboxResponse.cs
Normal file
10
microservices/MailApi/Models/MailboxResponse.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
namespace MailApi.Models;
|
||||||
|
|
||||||
|
public class MailboxResponse
|
||||||
|
{
|
||||||
|
public string CharacterId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public List<MailMessageResponse> Inbox { get; set; } = [];
|
||||||
|
|
||||||
|
public List<MailMessageResponse> Sent { get; set; } = [];
|
||||||
|
}
|
||||||
10
microservices/MailApi/Models/SendMailRequest.cs
Normal file
10
microservices/MailApi/Models/SendMailRequest.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
namespace MailApi.Models;
|
||||||
|
|
||||||
|
public class SendMailRequest
|
||||||
|
{
|
||||||
|
public string RecipientCharacterName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Subject { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Body { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
105
microservices/MailApi/Program.cs
Normal file
105
microservices/MailApi/Program.cs
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
using MailApi.Services;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.AspNetCore.Diagnostics;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using Microsoft.OpenApi.Models;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
builder.Services.AddControllers();
|
||||||
|
builder.Services.AddSingleton<MailStore>();
|
||||||
|
|
||||||
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
|
builder.Services.AddSwaggerGen(c =>
|
||||||
|
{
|
||||||
|
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Mail API", Version = "v1" });
|
||||||
|
c.AddSecurityDefinition("bearerAuth", new OpenApiSecurityScheme
|
||||||
|
{
|
||||||
|
Type = SecuritySchemeType.Http,
|
||||||
|
Scheme = "bearer",
|
||||||
|
BearerFormat = "JWT",
|
||||||
|
Description = "Paste your access token here (no 'Bearer ' prefix needed)."
|
||||||
|
});
|
||||||
|
c.AddSecurityRequirement(new OpenApiSecurityRequirement
|
||||||
|
{
|
||||||
|
{
|
||||||
|
new OpenApiSecurityScheme
|
||||||
|
{
|
||||||
|
Reference = new OpenApiReference
|
||||||
|
{ Type = ReferenceType.SecurityScheme, Id = "bearerAuth" }
|
||||||
|
},
|
||||||
|
Array.Empty<string>()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var cfg = builder.Configuration;
|
||||||
|
var jwtKey = cfg["Jwt:Key"] ?? throw new Exception("Jwt:Key missing");
|
||||||
|
var issuer = cfg["Jwt:Issuer"] ?? "promiscuity";
|
||||||
|
var aud = cfg["Jwt:Audience"] ?? issuer;
|
||||||
|
|
||||||
|
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||||
|
.AddJwtBearer(o =>
|
||||||
|
{
|
||||||
|
o.TokenValidationParameters = new TokenValidationParameters
|
||||||
|
{
|
||||||
|
ValidateIssuer = true,
|
||||||
|
ValidIssuer = issuer,
|
||||||
|
ValidateAudience = true,
|
||||||
|
ValidAudience = aud,
|
||||||
|
ValidateIssuerSigningKey = true,
|
||||||
|
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey)),
|
||||||
|
ValidateLifetime = true,
|
||||||
|
ClockSkew = TimeSpan.FromSeconds(30)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddAuthorization();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
app.UseExceptionHandler(errorApp =>
|
||||||
|
{
|
||||||
|
errorApp.Run(async context =>
|
||||||
|
{
|
||||||
|
var feature = context.Features.Get<IExceptionHandlerFeature>();
|
||||||
|
var exception = feature?.Error;
|
||||||
|
var logger = context.RequestServices.GetRequiredService<ILoggerFactory>().CreateLogger("GlobalException");
|
||||||
|
var traceId = context.TraceIdentifier;
|
||||||
|
|
||||||
|
if (exception is not null)
|
||||||
|
{
|
||||||
|
logger.LogError(
|
||||||
|
exception,
|
||||||
|
"Unhandled exception for {Method} {Path}. TraceId={TraceId}",
|
||||||
|
context.Request.Method,
|
||||||
|
context.Request.Path,
|
||||||
|
traceId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
|
||||||
|
context.Response.ContentType = "application/problem+json";
|
||||||
|
|
||||||
|
await context.Response.WriteAsJsonAsync(new
|
||||||
|
{
|
||||||
|
type = "https://httpstatuses.com/500",
|
||||||
|
title = "Internal Server Error",
|
||||||
|
status = 500,
|
||||||
|
detail = exception?.Message ?? "An unexpected server error occurred.",
|
||||||
|
traceId
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.MapGet("/healthz", () => Results.Ok("ok"));
|
||||||
|
app.UseSwagger();
|
||||||
|
app.UseSwaggerUI(o =>
|
||||||
|
{
|
||||||
|
o.SwaggerEndpoint("/swagger/v1/swagger.json", "Mail API v1");
|
||||||
|
o.RoutePrefix = "swagger";
|
||||||
|
});
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
|
app.MapControllers();
|
||||||
|
app.Run();
|
||||||
3
microservices/MailApi/README.md
Normal file
3
microservices/MailApi/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# MailApi
|
||||||
|
|
||||||
|
Stores character-to-character mail and exposes mailbox read and send endpoints.
|
||||||
137
microservices/MailApi/Services/MailStore.cs
Normal file
137
microservices/MailApi/Services/MailStore.cs
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
using MailApi.Models;
|
||||||
|
using MongoDB.Bson;
|
||||||
|
using MongoDB.Bson.Serialization.Attributes;
|
||||||
|
using MongoDB.Driver;
|
||||||
|
|
||||||
|
namespace MailApi.Services;
|
||||||
|
|
||||||
|
public class MailStore
|
||||||
|
{
|
||||||
|
private readonly IMongoCollection<MailMessage> _messages;
|
||||||
|
private readonly IMongoCollection<CharacterDocument> _characters;
|
||||||
|
|
||||||
|
public MailStore(IConfiguration cfg)
|
||||||
|
{
|
||||||
|
var cs = cfg["MongoDB:ConnectionString"] ?? "mongodb://127.0.0.1:27017";
|
||||||
|
var dbName = cfg["MongoDB:DatabaseName"] ?? "promiscuity";
|
||||||
|
var client = new MongoClient(cs);
|
||||||
|
var db = client.GetDatabase(dbName);
|
||||||
|
_messages = db.GetCollection<MailMessage>("MailMessages");
|
||||||
|
_characters = db.GetCollection<CharacterDocument>("Characters");
|
||||||
|
|
||||||
|
_messages.Indexes.CreateOne(new CreateIndexModel<MailMessage>(
|
||||||
|
Builders<MailMessage>.IndexKeys.Ascending(m => m.RecipientCharacterId).Descending(m => m.CreatedUtc)));
|
||||||
|
_messages.Indexes.CreateOne(new CreateIndexModel<MailMessage>(
|
||||||
|
Builders<MailMessage>.IndexKeys.Ascending(m => m.SenderCharacterId).Descending(m => m.CreatedUtc)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CharacterAccessResult> ResolveCharacterAsync(string characterId, string userId, bool allowAnyOwner)
|
||||||
|
{
|
||||||
|
var character = await _characters.Find(c => c.Id == characterId).FirstOrDefaultAsync();
|
||||||
|
if (character is null)
|
||||||
|
return new CharacterAccessResult { Exists = false };
|
||||||
|
|
||||||
|
return new CharacterAccessResult
|
||||||
|
{
|
||||||
|
Exists = true,
|
||||||
|
IsAuthorized = allowAnyOwner || character.OwnerUserId == userId,
|
||||||
|
CharacterId = character.Id ?? string.Empty,
|
||||||
|
CharacterName = character.Name,
|
||||||
|
OwnerUserId = character.OwnerUserId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<MailMessage>> GetInboxAsync(string characterId) =>
|
||||||
|
await _messages.Find(m => m.RecipientCharacterId == characterId)
|
||||||
|
.SortByDescending(m => m.CreatedUtc)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
public async Task<List<MailMessage>> GetSentAsync(string characterId) =>
|
||||||
|
await _messages.Find(m => m.SenderCharacterId == characterId)
|
||||||
|
.SortByDescending(m => m.CreatedUtc)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
public async Task<SendMailResult> SendAsync(string senderCharacterId, string recipientCharacterName, string subject, string body)
|
||||||
|
{
|
||||||
|
var sender = await _characters.Find(c => c.Id == senderCharacterId).FirstOrDefaultAsync();
|
||||||
|
if (sender is null)
|
||||||
|
return new SendMailResult { Status = SendMailStatus.SenderNotFound };
|
||||||
|
|
||||||
|
var normalizedRecipientName = recipientCharacterName.Trim();
|
||||||
|
var recipients = await _characters.Find(c => c.Name == normalizedRecipientName).ToListAsync();
|
||||||
|
if (recipients.Count == 0)
|
||||||
|
return new SendMailResult { Status = SendMailStatus.RecipientNotFound };
|
||||||
|
if (recipients.Count > 1)
|
||||||
|
return new SendMailResult { Status = SendMailStatus.RecipientAmbiguous };
|
||||||
|
|
||||||
|
var recipient = recipients[0];
|
||||||
|
if (recipient.Id == sender.Id)
|
||||||
|
return new SendMailResult { Status = SendMailStatus.Invalid };
|
||||||
|
|
||||||
|
var message = new MailMessage
|
||||||
|
{
|
||||||
|
SenderCharacterId = sender.Id ?? string.Empty,
|
||||||
|
SenderCharacterName = sender.Name,
|
||||||
|
RecipientCharacterId = recipient.Id ?? string.Empty,
|
||||||
|
RecipientCharacterName = recipient.Name,
|
||||||
|
Subject = subject.Trim(),
|
||||||
|
Body = body.Trim(),
|
||||||
|
CreatedUtc = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
await _messages.InsertOneAsync(message);
|
||||||
|
return new SendMailResult { Status = SendMailStatus.Ok, Message = message };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<MailMessage?> MarkReadAsync(string characterId, string messageId)
|
||||||
|
{
|
||||||
|
var filter = Builders<MailMessage>.Filter.And(
|
||||||
|
Builders<MailMessage>.Filter.Eq(m => m.Id, messageId),
|
||||||
|
Builders<MailMessage>.Filter.Eq(m => m.RecipientCharacterId, characterId)
|
||||||
|
);
|
||||||
|
var update = Builders<MailMessage>.Update.Set(m => m.ReadUtc, DateTime.UtcNow);
|
||||||
|
var options = new FindOneAndUpdateOptions<MailMessage> { ReturnDocument = ReturnDocument.After };
|
||||||
|
return await _messages.FindOneAndUpdateAsync(filter, update, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CharacterAccessResult
|
||||||
|
{
|
||||||
|
public bool Exists { get; set; }
|
||||||
|
|
||||||
|
public bool IsAuthorized { get; set; }
|
||||||
|
|
||||||
|
public string CharacterId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string CharacterName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string OwnerUserId { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SendMailResult
|
||||||
|
{
|
||||||
|
public SendMailStatus Status { get; set; }
|
||||||
|
|
||||||
|
public MailMessage? Message { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum SendMailStatus
|
||||||
|
{
|
||||||
|
Ok,
|
||||||
|
SenderNotFound,
|
||||||
|
RecipientNotFound,
|
||||||
|
RecipientAmbiguous,
|
||||||
|
Invalid
|
||||||
|
}
|
||||||
|
|
||||||
|
[BsonIgnoreExtraElements]
|
||||||
|
private class CharacterDocument
|
||||||
|
{
|
||||||
|
[BsonId]
|
||||||
|
[BsonRepresentation(BsonType.ObjectId)]
|
||||||
|
public string? Id { get; set; }
|
||||||
|
|
||||||
|
public string OwnerUserId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
7
microservices/MailApi/appsettings.Development.json
Normal file
7
microservices/MailApi/appsettings.Development.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"Kestrel": { "Endpoints": { "Http": { "Url": "http://0.0.0.0:5004" } } },
|
||||||
|
"MongoDB": { "ConnectionString": "mongodb://127.0.0.1:27017", "DatabaseName": "promiscuity" },
|
||||||
|
"Jwt": { "Key": "SuperUltraSecureJwtKeyWithAtLeast32Chars!!", "Issuer": "promiscuity", "Audience": "promiscuity-auth-api" },
|
||||||
|
"Logging": { "LogLevel": { "Default": "Information" } },
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
||||||
7
microservices/MailApi/appsettings.json
Normal file
7
microservices/MailApi/appsettings.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"Kestrel": { "Endpoints": { "Http": { "Url": "http://0.0.0.0:5004" } } },
|
||||||
|
"MongoDB": { "ConnectionString": "mongodb://192.168.86.50:27017", "DatabaseName": "promiscuity" },
|
||||||
|
"Jwt": { "Key": "SuperUltraSecureJwtKeyWithAtLeast32Chars!!", "Issuer": "promiscuity", "Audience": "promiscuity-auth-api" },
|
||||||
|
"Logging": { "LogLevel": { "Default": "Information" } },
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
||||||
28
microservices/MailApi/k8s/deployment.yaml
Normal file
28
microservices/MailApi/k8s/deployment.yaml
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: promiscuity-mail
|
||||||
|
labels:
|
||||||
|
app: promiscuity-mail
|
||||||
|
spec:
|
||||||
|
replicas: 2
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: promiscuity-mail
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: promiscuity-mail
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: promiscuity-mail
|
||||||
|
image: promiscuity-mail:latest
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
ports:
|
||||||
|
- containerPort: 5004
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /healthz
|
||||||
|
port: 5004
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
15
microservices/MailApi/k8s/service.yaml
Normal file
15
microservices/MailApi/k8s/service.yaml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: promiscuity-mail
|
||||||
|
labels:
|
||||||
|
app: promiscuity-mail
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: promiscuity-mail
|
||||||
|
type: NodePort
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 80
|
||||||
|
targetPort: 5004
|
||||||
|
nodePort: 30084
|
||||||
@ -10,6 +10,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LocationsApi", "LocationsAp
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InventoryApi", "InventoryApi\InventoryApi.csproj", "{72AA73C5-6A42-4F79-ADCC-19F10A66B9E0}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InventoryApi", "InventoryApi\InventoryApi.csproj", "{72AA73C5-6A42-4F79-ADCC-19F10A66B9E0}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MailApi", "MailApi\MailApi.csproj", "{2A6A80E9-2D58-4E42-8B93-483F2D6E6D42}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@ -32,6 +34,10 @@ Global
|
|||||||
{72AA73C5-6A42-4F79-ADCC-19F10A66B9E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{72AA73C5-6A42-4F79-ADCC-19F10A66B9E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{72AA73C5-6A42-4F79-ADCC-19F10A66B9E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{72AA73C5-6A42-4F79-ADCC-19F10A66B9E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{72AA73C5-6A42-4F79-ADCC-19F10A66B9E0}.Release|Any CPU.Build.0 = Release|Any CPU
|
{72AA73C5-6A42-4F79-ADCC-19F10A66B9E0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{2A6A80E9-2D58-4E42-8B93-483F2D6E6D42}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{2A6A80E9-2D58-4E42-8B93-483F2D6E6D42}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{2A6A80E9-2D58-4E42-8B93-483F2D6E6D42}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{2A6A80E9-2D58-4E42-8B93-483F2D6E6D42}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user