diff --git a/.gitignore b/.gitignore index 606851e..8ae43ee 100644 --- a/.gitignore +++ b/.gitignore @@ -3,13 +3,14 @@ *.beam *.o *.plt +*.lock # Dipendenze scaricate /deps/ # File di test e log .eunit/ -logs/ +log/ erl_crash.dump # File generati da strumenti diff --git a/priv/note.json b/priv/note.json new file mode 100644 index 0000000..b70b30c --- /dev/null +++ b/priv/note.json @@ -0,0 +1,18 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "ActivityPub Note", + "type": "object", + "required": ["type", "id", "content"], + "properties": { + "@context": { "type": "string" }, + "type": { "type": "string", "enum": ["Note"] }, + "id": { "type": "string" }, + "attributedTo": { "type": "string" }, + "content": { "type": "string" }, + "to": { + "type": "array", + "items": { "type": "string" } + } + }, + "additionalProperties": true +} diff --git a/rebar.config b/rebar.config index d6185ee..43136a7 100644 --- a/rebar.config +++ b/rebar.config @@ -1,4 +1,6 @@ {deps, [ + jsonlog, + jesse, {cowboy, "2.10.0"}, {jsx, "3.1.0"} ]}. diff --git a/rebar.lock b/rebar.lock deleted file mode 100644 index b15d609..0000000 --- a/rebar.lock +++ /dev/null @@ -1,17 +0,0 @@ -{"1.2.0", -[{<<"cowboy">>,{pkg,<<"cowboy">>,<<"2.10.0">>},0}, - {<<"cowlib">>,{pkg,<<"cowlib">>,<<"2.12.1">>},1}, - {<<"jsx">>,{pkg,<<"jsx">>,<<"3.1.0">>},0}, - {<<"ranch">>,{pkg,<<"ranch">>,<<"1.8.0">>},1}]}. -[ -{pkg_hash,[ - {<<"cowboy">>, <<"FF9FFEFF91DAE4AE270DD975642997AFE2A1179D94B1887863E43F681A203E26">>}, - {<<"cowlib">>, <<"A9FA9A625F1D2025FE6B462CB865881329B5CAFF8F1854D1CBC9F9533F00E1E1">>}, - {<<"jsx">>, <<"D12516BAA0BB23A59BB35DCCAF02A1BD08243FCBB9EFE24F2D9D056CCFF71268">>}, - {<<"ranch">>, <<"8C7A100A139FD57F17327B6413E4167AC559FBC04CA7448E9BE9057311597A1D">>}]}, -{pkg_hash_ext,[ - {<<"cowboy">>, <<"3AFDCCB7183CC6F143CB14D3CF51FA00E53DB9EC80CDCD525482F5E99BC41D6B">>}, - {<<"cowlib">>, <<"163B73F6367A7341B33C794C4E88E7DBFE6498AC42DCD69EF44C5BC5507C8DB0">>}, - {<<"jsx">>, <<"0C5CC8FDC11B53CC25CF65AC6705AD39E54ECC56D1C22E4ADB8F5A53FB9427F3">>}, - {<<"ranch">>, <<"49FBCFD3682FAB1F5D109351B61257676DA1A2FDBE295904176D5E521A2DDFE5">>}]} -]. diff --git a/src/activitypub_server_app.erl b/src/activitypub_server_app.erl index baaa627..e8607f0 100644 --- a/src/activitypub_server_app.erl +++ b/src/activitypub_server_app.erl @@ -10,6 +10,7 @@ start(_Type, _Args) -> %% creiamo un utente di test, solo per vedere se mnesia va + log_handler:init(), %% va per primo, se no, non logga l'avvio db_organization:setup(), db_organization:init(), users_local_check:check_ENV(), %% controlla le variabili d'ambiente diff --git a/src/db_safe_insert.erl b/src/db_safe_insert.erl index a06b6e2..163b9e3 100644 --- a/src/db_safe_insert.erl +++ b/src/db_safe_insert.erl @@ -26,7 +26,7 @@ remove_oldest(Tab, Fraction) -> lists:foreach(fun(Rec) -> mnesia:dirty_delete_object(Rec) end, ToDelete). make_pattern(global_message) -> #global_message{id='_', activity='_', timestamp='_'}; -make_pattern(ap_users) -> #ap_users{id='_', name='_', email='_', created_at='_'}. %% <-- PUNTO QUI DIOCANE! +make_pattern(ap_users) -> #ap_users{id='_', username='_', email='_', created_at='_'}. %% <-- PUNTO QUI DIOCANE! get_timestamp(#global_message{timestamp=T}) -> T; get_timestamp(#ap_users{created_at=T}) -> T. %% <-- PUNTO SOLO ALLA FINE, DIOCANE! diff --git a/src/index_handler.erl b/src/index_handler.erl index b9c9c61..c31d76a 100644 --- a/src/index_handler.erl +++ b/src/index_handler.erl @@ -1,40 +1,66 @@ %% index_handler.erl -module(index_handler). -behaviour(cowboy_handler). +-include("db_safe_insert.hrl"). -export([init/2]). init(Req, State) -> - {ok, Body, Req2} = cowboy_req:read_body(Req), - Activity = jsx:decode(Body, [return_maps]), - - %% Validazione dei campi obbligatori - case validate_activity(Activity) of - false -> - {ok, Resp} = cowboy_req:reply(400, #{}, <<"Invalid ActivityPub message">>, Req2), - {ok, Resp, State}; - true -> + {Body, Req2} = read_full_body(Req), + case json_validate:validate_activity(Body) of + ok -> + %% Decodifica il JSON già validato + Activity = jsx:decode(Body, [return_maps]), To = maps:get(<<"to">>, Activity, []), - %% Qui uso la funzione del nuovo modulo! case users_local_check:has_local_recipient(To) of true -> - {ok, Resp} = cowboy_req:reply(200, #{}, <<"Delivered to local user">>, Req2), - {ok, Resp, State}; + cowboy_req:reply(200, #{}, <<"Delivered to local user">>, Req2), + {ok, Req2, State}; false -> - %% Salva nell'inbox globale - timeline_db:add_message(Activity), - {ok, Resp} = cowboy_req:reply(202, #{}, <<"Saved to global inbox">>, Req2), - {ok, Resp, State} - end + %% SAFE INSERT NELLA GLOBAL INBOX + InsertResult = db_safe_insert:safe_insert(global_message, #global_message{ + id = make_ref(), + activity = Activity, + timestamp = erlang:system_time(second) + }), + case InsertResult of + ok -> + cowboy_req:reply(202, #{}, <<"Saved to global inbox">>, Req2), + {ok, Req2, State}; + {aborted, Reason} -> + logger:error("DB aborted: ~p", [Reason]), + cowboy_req:reply(500, #{}, <<"Database error">>, Req2), + {ok, Req2, State}; + Other -> + logger:error("DB unknown error: ~p", [Other]), + cowboy_req:reply(500, #{}, <<"Unknown DB error">>, Req2), + {ok, Req2, State} + end + end; + {error, malformed_json} -> + cowboy_req:reply(400, #{}, <<"Malformed JSON">>, Req2), + {ok, Req2, State}; + {error, missing_type} -> + cowboy_req:reply(400, #{}, <<"Missing 'type' field">>, Req2), + {ok, Req2, State}; + {error, {unsupported_type, TypeBin}} -> + Msg = <<"Unsupported type: ", TypeBin/binary>>, + cowboy_req:reply(400, #{}, Msg, Req2), + {ok, Req2, State}; + {error, invalid_type_field} -> + cowboy_req:reply(400, #{}, <<"Invalid 'type' field">>, Req2), + {ok, Req2, State}; + {error, Errors} -> + ErrorMsg = io_lib:format("Invalid ActivityPub message: ~p", [Errors]), + cowboy_req:reply(400, #{}, list_to_binary(ErrorMsg), Req2), + {ok, Req2, State} end. - -%% vediamo di controllare se ci sono i campi obbligatori minimi di ActivityPub: - -validate_activity(Activity) when is_map(Activity) -> - maps:is_key(<<"type">>, Activity) andalso - maps:is_key(<<"to">>, Activity) andalso - maps:is_key(<<"actor">>, Activity) andalso - maps:is_key(<<"object">>, Activity) andalso - maps:is_key(<<"content">>, Activity). - - +%% Funzione di utilità per leggere tutto il body +read_full_body(Req) -> + case cowboy_req:read_body(Req) of + {ok, Body, Req2} -> + {Body, Req2}; + {more, Data, Req2} -> + {Rest, FinalReq} = read_full_body(Req2), + {<>, FinalReq} + end. diff --git a/src/json_validate.erl b/src/json_validate.erl new file mode 100644 index 0000000..d271888 --- /dev/null +++ b/src/json_validate.erl @@ -0,0 +1,32 @@ +-module(json_validate). +-export([validate_activity/1, load_schema/1]). + +load_schema(SchemaName) -> + Filename = "priv/" ++ SchemaName ++ ".json", + {ok, Bin} = file:read_file(Filename), + jsx:decode(Bin, [return_maps]). + +validate_activity(JsonBin) -> + case catch jsx:decode(JsonBin, [return_maps]) of + {'EXIT', _} -> + {error, malformed_json}; + JsonMap when is_map(JsonMap) -> + case maps:get(<<"type">>, JsonMap, undefined) of + undefined -> + {error, missing_type}; + TypeBin when is_binary(TypeBin) -> + TypeLower = string:lowercase(binary_to_list(TypeBin)), + case TypeLower of + "note" -> + Schema = load_schema("note"), + case jesse:validate_with_schema(Schema, JsonMap) of + {ok, _} -> ok; + {error, Errors} -> {error, Errors} + end; + OtherType -> + {error, {unsupported_type, TypeBin}} + end; + _ -> + {error, invalid_type_field} + end + end. diff --git a/src/log_handler.erl b/src/log_handler.erl new file mode 100644 index 0000000..3add5dc --- /dev/null +++ b/src/log_handler.erl @@ -0,0 +1,36 @@ +-module(log_handler). +-export([init/0, info/2, error/2, warning/2, debug/2, log_map/2]). + +init() -> + ok = ensure_log_dir(), + lists:foreach(fun(H) -> logger:remove_handler(H) end, logger:get_handler_ids()), + logger:add_handler(file, logger_std_h, #{ + level => debug, + config => #{file => "log/erlang.json", sync_mode_qlen => 0}, + formatter => {logger_formatter, #{}} + }), + ok. + + +info(Format, Args) -> + logger:info(Format, Args). + +error(Format, Args) -> + logger:error(Format, Args). + +warning(Format, Args) -> + logger:warning(Format, Args). + +debug(Format, Args) -> + logger:debug(Format, Args). + +%% Logga direttamente una mappa come evento JSON +log_map(Level, Map) when is_map(Map) -> + logger:log(Level, Map). + +%% Utility per assicurarsi che la directory log/ esista +ensure_log_dir() -> + case file:read_file_info("log") of + {ok, _} -> ok; + _ -> file:make_dir("log") + end. diff --git a/src/user_db.erl b/src/user_db.erl index 8e46f28..46ce933 100644 --- a/src/user_db.erl +++ b/src/user_db.erl @@ -5,7 +5,7 @@ add_user(Name, Email) -> Id = erlang:unique_integer([monotonic, positive]), Timestamp = erlang:system_time(microsecond), - Record = #ap_users{id=Id, name=Name, email=Email, created_at=Timestamp}, + Record = #ap_users{id=Id, username=Name, email=Email, created_at=Timestamp}, db_safe_insert:safe_insert(ap_users, Record). get_user(Id) -> @@ -20,7 +20,7 @@ get_user(Id) -> all_users() -> F = fun() -> - mnesia:match_object(#ap_users{id='_', name='_', email='_', created_at='_'}) + mnesia:match_object(#ap_users{id='_', username='_', email='_', created_at='_'}) end, {atomic, Users} = mnesia:transaction(F), Users. diff --git a/src/users_local_check.erl b/src/users_local_check.erl index 8706a42..bb59594 100644 --- a/src/users_local_check.erl +++ b/src/users_local_check.erl @@ -12,9 +12,10 @@ check_ENV() -> case os:getenv("AP_FQDN") of false -> - io:format("Errore: la variabile di ambiente AP_FQDN non è settata!~n"), + log_handler:error("Errore: la variabile di ambiente AP_FQDN non è settata!~n"), erlang:halt(1); _ -> + log_handler:debug("La variabile AP_FQDN esiste, bene", []), ok end. @@ -24,10 +25,10 @@ get_fqdn() -> logger:notice("Chiamata get_fqdn()~n"), case os:getenv("AP_FQDN") of false -> - logger:notice("Errore: la variabile di ambiente AP_FQDN deve essere settata!~n"), + log_handler:error("Errore: la variabile di ambiente AP_FQDN deve essere settata!~n"), exit({error, ap_fqdn_not_set}); FQDN -> - logger:notice("FQDN settata correttamente: ~p", [FQDN]), + log_handler:debug("FQDN settata correttamente: ~p", [FQDN]), list_to_binary(FQDN) end. diff --git a/start.sh b/start.sh index 09e6177..4f3c61d 100755 --- a/start.sh +++ b/start.sh @@ -3,6 +3,8 @@ export AP_FQDN="localhost" export AP_DB_DIR="./mnesia" +rm -rf mnesia +rebar3 get-deps rebar3 compile rebar3 shell --apps activitypub_server