aboutsummaryrefslogtreecommitdiff
path: root/server/src/jchat_utils.erl
diff options
context:
space:
mode:
Diffstat (limited to 'server/src/jchat_utils.erl')
-rw-r--r--server/src/jchat_utils.erl163
1 files changed, 163 insertions, 0 deletions
diff --git a/server/src/jchat_utils.erl b/server/src/jchat_utils.erl
new file mode 100644
index 0000000..3f94cfb
--- /dev/null
+++ b/server/src/jchat_utils.erl
@@ -0,0 +1,163 @@
+-module(jchat_utils).
+
+-export([now_iso8601/0,
+ generate_id/0,
+ validate_id/1,
+ json_encode/1,
+ json_decode/1,
+ format_error/1,
+ validate_account_id/1,
+ extract_auth_token/1,
+ validate_method_call/1,
+ format_method_response/3,
+ format_error_response/3]).
+
+%% Generate ISO8601 timestamp
+now_iso8601() ->
+ {{Year, Month, Day}, {Hour, Minute, Second}} = calendar:universal_time(),
+ iolist_to_binary(io_lib:format("~4..0w-~2..0w-~2..0wT~2..0w:~2..0w:~2..0wZ",
+ [Year, Month, Day, Hour, Minute, Second])).
+
+%% Generate a new ID (UUID v4)
+generate_id() ->
+ <<U0:32, U1:16, _:4, U2:12, _:2, U3:62>> = crypto:strong_rand_bytes(16),
+ iolist_to_binary(io_lib:format("~8.16.0b-~4.16.0b-4~3.16.0b-~4.16.0b-~12.16.0b",
+ [U0, U1, U2, ((U3 bsr 60) band 16#03) bor 16#08, U3 band 16#0fffffffffff])).
+
+%% Validate ID format (URL-safe base64 characters)
+validate_id(Id) when is_binary(Id) ->
+ Size = byte_size(Id),
+ case Size >= 1 andalso Size =< 255 of
+ true ->
+ validate_id_chars(Id);
+ false ->
+ false
+ end;
+validate_id(_) ->
+ false.
+
+validate_id_chars(<<>>) ->
+ true;
+validate_id_chars(<<C, Rest/binary>>) ->
+ case is_valid_id_char(C) of
+ true -> validate_id_chars(Rest);
+ false -> false
+ end.
+
+is_valid_id_char(C) ->
+ (C >= $A andalso C =< $Z) orelse
+ (C >= $a andalso C =< $z) orelse
+ (C >= $0 andalso C =< $9) orelse
+ C =:= $- orelse C =:= $_.
+
+%% JSON encoding/decoding
+json_encode(Term) ->
+ jsx:encode(Term).
+
+json_decode(JSON) ->
+ try
+ {ok, jsx:decode(JSON, [return_maps])}
+ catch
+ _:_ -> {error, invalid_json}
+ end.
+
+%% Format errors for JMAP responses (RFC 8620 compliant)
+format_error(not_found) ->
+ #{type => <<"notFound">>};
+format_error(invalid_arguments) ->
+ #{type => <<"invalidArguments">>};
+format_error(account_not_found) ->
+ #{type => <<"accountNotFound">>};
+format_error(forbidden) ->
+ #{type => <<"forbidden">>};
+format_error(invalid_result_reference) ->
+ #{type => <<"invalidResultReference">>};
+format_error(anchor_not_found) ->
+ #{type => <<"anchorNotFound">>};
+format_error(unsupported_sort) ->
+ #{type => <<"unsupportedSort">>};
+format_error(unsupported_filter) ->
+ #{type => <<"unsupportedFilter">>};
+format_error(cannot_calculate_changes) ->
+ #{type => <<"cannotCalculateChanges">>};
+format_error(too_large) ->
+ #{type => <<"tooLarge">>};
+format_error(too_many_changes) ->
+ #{type => <<"tooManyChanges">>};
+format_error(rate_limited) ->
+ #{type => <<"rateLimited">>};
+format_error(request_too_large) ->
+ #{type => <<"requestTooLarge">>};
+format_error(limit_exceeded) ->
+ #{type => <<"limitExceeded">>};
+format_error(state_mismatch) ->
+ #{type => <<"stateMismatch">>};
+format_error(will_destroy) ->
+ #{type => <<"willDestroy">>};
+format_error({invalid_arguments, Description}) ->
+ #{type => <<"invalidArguments">>, description => Description};
+format_error({not_found, Description}) ->
+ #{type => <<"notFound">>, description => Description};
+format_error({forbidden, Description}) ->
+ #{type => <<"forbidden">>, description => Description};
+format_error({server_fail, Description}) ->
+ #{type => <<"serverFail">>, description => Description};
+format_error(Error) ->
+ #{type => <<"serverFail">>, description => iolist_to_binary(io_lib:format("~p", [Error]))}.
+
+%% JMAP-specific utility functions
+
+%% Validate account ID format (as per JMAP spec)
+validate_account_id(AccountId) when is_binary(AccountId) ->
+ validate_id(AccountId);
+validate_account_id(_) ->
+ false.
+
+%% Extract Bearer token from Authorization header
+extract_auth_token(undefined) ->
+ {error, no_auth_header};
+extract_auth_token(<<"Bearer ", Token/binary>>) ->
+ {ok, Token};
+extract_auth_token(_) ->
+ {error, invalid_auth_format}.
+
+%% Validate JMAP method call structure
+validate_method_call([Method, Args, CallId])
+ when is_binary(Method), is_map(Args), is_binary(CallId) ->
+ case validate_method_name(Method) of
+ true -> ok;
+ false -> {error, invalid_method_name}
+ end;
+validate_method_call(_) ->
+ {error, invalid_method_call_structure}.
+
+validate_method_name(Method) ->
+ case binary:split(Method, <<"/">>) of
+ [Type, Operation] when byte_size(Type) > 0, byte_size(Operation) > 0 ->
+ validate_method_chars(Method);
+ _ ->
+ false
+ end.
+
+validate_method_chars(<<>>) ->
+ true;
+validate_method_chars(<<C, Rest/binary>>) ->
+ case is_valid_method_char(C) of
+ true -> validate_method_chars(Rest);
+ false -> false
+ end.
+
+is_valid_method_char(C) ->
+ (C >= $A andalso C =< $Z) orelse
+ (C >= $a andalso C =< $z) orelse
+ (C >= $0 andalso C =< $9) orelse
+ C =:= $/ orelse C =:= $_.
+
+%% Format successful method response
+format_method_response(Method, Result, CallId) ->
+ [Method, Result, CallId].
+
+%% Format error method response
+format_error_response(Error, CallId, Method) ->
+ ErrorMap = format_error(Error),
+ [<<"error">>, ErrorMap#{<<"description">> => <<"Error in method: ", Method/binary>>}, CallId].