Mercurial
view connectors/service.c @ 281:c57149ad216e default tip
Copilot-Session: f68442b1-fa8f-46a0-9689-81710613bbd4
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Tue, 18 Aug 2026 22:18:15 -0700 |
| parents | b3b547563ec7 |
| children |
line wrap: on
line source
#include "connectors/connector.h" #include "connectors/auth_test_page.h" #include <ctype.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <strings.h> #include <time.h> static Connector_Store *g_store; static const Connector_Google_Config *g_google; static Connector_Auth_Adapter g_auth; static const char *map_value(Seobeo_Request_Entry *map, const char *key) { if (!map || !key) return NULL; for (size_t i = 0; i < Dowa_Array_Length(map); ++i) { if (map[i].key && !strcasecmp(map[i].key, key)) return map[i].value; } return NULL; } static Seobeo_Request_Entry *json_response( Dowa_Arena *arena, const char *status, const char *body) { Seobeo_Request_Entry *response = NULL; Dowa_HashMap_Push_Arena(response, "status", (char *)status, arena); Dowa_HashMap_Push_Arena( response, "content-type", "application/json", arena); Dowa_HashMap_Push_Arena(response, "body", (char *)body, arena); return response; } static Seobeo_Request_Entry *Auth_Test_Page( Seobeo_Request_Entry *request, Dowa_Arena *arena) { (void)request; Seobeo_Request_Entry *response = NULL; Dowa_HashMap_Push_Arena(response, "status", "200", arena); Dowa_HashMap_Push_Arena( response, "content-type", "text/html; charset=utf-8", arena); Dowa_HashMap_Push_Arena( response, "body", (char *)Connector_Auth_Test_Page(), arena); return response; } static const char *current_user( Seobeo_Request_Entry *request, boolean require_csrf, Dowa_Arena *arena) { return g_auth.resolve_user ? g_auth.resolve_user( request, require_csrf, arena, g_auth.context) : NULL; } static boolean safe_component(const char *value) { if (!value || !value[0] || strlen(value) >= CONNECTOR_ID_MAX) return FALSE; for (size_t i = 0; value[i]; ++i) if (!(isalnum((uint8)value[i]) || value[i] == '-' || value[i] == '_' || value[i] == ':' || value[i] == '.')) return FALSE; return TRUE; } static Seobeo_Request_Entry *Health( Seobeo_Request_Entry *request, Dowa_Arena *arena) { (void)request; return json_response(arena, "200", "{\"ok\":true}"); } static Seobeo_Request_Entry *Auth_Session( Seobeo_Request_Entry *request, Dowa_Arena *arena) { Connector_Auth_Session session; if (!g_auth.resolve_session || !g_auth.resolve_session(request, &session, g_auth.context)) return json_response( arena, "401", "{\"authenticated\":false,\"error\":\"unauthorized\"}"); char *user_id = Dowa_JSON_Escape_String(session.user_id, 0, arena); char *username = Dowa_JSON_Escape_String(session.username, 0, arena); char *role = Dowa_JSON_Escape_String(session.role, 0, arena); char *csrf = Dowa_JSON_Escape_String(session.csrf_token, 0, arena); if (!user_id || !username || !role || !csrf) return json_response(arena, "500", "{\"error\":\"session_failed\"}"); size_t length = strlen(user_id) + strlen(username) + strlen(role) + strlen(csrf) + 128; char *body = Dowa_Arena_Allocate(arena, length); snprintf( body, length, "{\"authenticated\":true,\"userId\":\"%s\",\"username\":\"%s\"," "\"role\":\"%s\",\"csrfToken\":\"%s\"}", user_id, username, role, csrf); return json_response(arena, "200", body); } static Seobeo_Request_Entry *List_Accounts( Seobeo_Request_Entry *request, Dowa_Arena *arena) { const char *user = current_user(request, FALSE, arena); if (!user) return json_response(arena, "401", "{\"error\":\"unauthorized\"}"); Connector_Account_Summary *accounts = NULL; if (!Connector_Store_List_Accounts(g_store, user, &accounts, arena)) return json_response(arena, "500", "{\"error\":\"account_list_failed\"}"); size_t capacity = 64 + Dowa_Array_Length(accounts) * 1600; char *body = Dowa_Arena_Allocate(arena, capacity); size_t offset = (size_t)snprintf(body, capacity, "{\"accounts\":["); for (size_t i = 0; i < Dowa_Array_Length(accounts); i++) { char *account_id = Dowa_JSON_Escape_String(accounts[i].account_id, 0, arena); char *provider = Dowa_JSON_Escape_String(accounts[i].provider, 0, arena); char *email = Dowa_JSON_Escape_String(accounts[i].email, 0, arena); char *scopes = Dowa_JSON_Escape_String(accounts[i].scopes, 0, arena); if (!account_id || !provider || !email || !scopes) return json_response(arena, "500", "{\"error\":\"account_list_failed\"}"); offset += (size_t)snprintf( body + offset, capacity - offset, "%s{\"accountId\":\"%s\",\"provider\":\"%s\",\"email\":\"%s\"," "\"expiresAt\":%lld,\"scopes\":\"%s\"}", i ? "," : "", account_id, provider, email, (long long)accounts[i].expires_at, scopes); } snprintf(body + offset, capacity - offset, "]}"); return json_response(arena, "200", body); } static Seobeo_Request_Entry *AI_Tools( Seobeo_Request_Entry *request, Dowa_Arena *arena) { (void)request; return json_response( arena, "200", "{\"version\":1," "\"documentation\":\"connectors/wiki/README.md\"," "\"contextStrategy\":[" "\"discover a connection with connector.accounts.list\"," "\"search or list lightweight references\"," "\"hydrate only the most relevant IDs with get operations\"," "\"normalize bounded text with source IDs before model inference\"]," "\"tools\":[" "{\"name\":\"connector.accounts.list\",\"method\":\"GET\"," "\"path\":\"/v1/accounts\"}," "{\"name\":\"connector.gmail.search\",\"method\":\"GET\"," "\"path\":\"/v1/accounts/{account_id}/gmail/messages\"," "\"query\":[\"q\",\"maxResults\",\"pageToken\"]}," "{\"name\":\"connector.gmail.get\",\"method\":\"GET\"," "\"path\":\"/v1/accounts/{account_id}/gmail/messages/{message_id}\"," "\"query\":[\"format\",\"metadataHeaders\"]}," "{\"name\":\"connector.drive.search\",\"method\":\"GET\"," "\"path\":\"/v1/accounts/{account_id}/drive/files\"," "\"query\":[\"q\",\"pageSize\",\"pageToken\",\"fields\"]}," "{\"name\":\"connector.drive.get\",\"method\":\"GET\"," "\"path\":\"/v1/accounts/{account_id}/drive/files/{file_id}\"}," "{\"name\":\"connector.gmail.draft\",\"method\":\"POST\"," "\"path\":\"/v1/accounts/{account_id}/gmail/drafts\"}," "{\"name\":\"connector.gmail.send\",\"method\":\"POST\"," "\"path\":\"/v1/accounts/{account_id}/gmail/send\"," "\"confirmation\":\"policy-controlled\"}]}"); } static Seobeo_Request_Entry *OAuth_Start( Seobeo_Request_Entry *request, Dowa_Arena *arena) { const char *user = current_user(request, TRUE, arena); if (!user) return json_response(arena, "401", "{\"error\":\"unauthorized\"}"); Connector_OAuth_Start start; if (!Connector_OAuth_PKCE_Start(&start) || !Connector_Store_Create_State( g_store, user, &start, (int64)time(NULL) + 600)) return json_response(arena, "500", "{\"error\":\"oauth_start_failed\"}"); char *url = Connector_Google_Authorization_URL(g_google, &start, arena); if (!url) return json_response(arena, "500", "{\"error\":\"oauth_start_failed\"}"); size_t length = strlen(url) + 32; char *body = Dowa_Arena_Allocate(arena, length); snprintf(body, length, "{\"authorization_url\":\"%s\"}", url); return json_response(arena, "200", body); } static Seobeo_Request_Entry *OAuth_Callback( Seobeo_Request_Entry *request, Dowa_Arena *arena) { const char *user = current_user(request, FALSE, arena); const char *state = map_value(request, "query_state"); const char *code = map_value(request, "query_code"); if (!user) return json_response(arena, "401", "{\"error\":\"unauthorized\"}"); if (!state || !code) return json_response(arena, "400", "{\"error\":\"missing_oauth_fields\"}"); char verifier[128]; if (!Connector_Store_Consume_State( g_store, user, state, (int64)time(NULL), verifier, sizeof(verifier))) return json_response(arena, "400", "{\"error\":\"invalid_oauth_state\"}"); Connector_Account account; Connector_Provider_Error provider_error; Connector_Status status = Connector_Google_Exchange_Code( g_google, code, verifier, &account, &provider_error, arena); if (status != CONNECTOR_OK) { char *code_safe = Dowa_JSON_Escape_String( provider_error.code[0] ? provider_error.code : "unknown", 0, arena); char *description_safe = Dowa_JSON_Escape_String( provider_error.description, 0, arena); char *body = Dowa_Arena_Allocate(arena, 512); if (!code_safe || !description_safe || !body) return json_response( arena, "502", "{\"error\":\"token_exchange_failed\"}"); snprintf( body, 512, "{\"error\":\"token_exchange_failed\",\"provider_error\":\"%s\"," "\"provider_description\":\"%s\",\"provider_status\":%d}", code_safe, description_safe, provider_error.http_status); return json_response(arena, "502", body); } if (strlen(user) >= sizeof(account.user_id)) return json_response(arena, "400", "{\"error\":\"invalid_user\"}"); strcpy(account.user_id, user); if (!Connector_Store_Save_Account(g_store, &account)) return json_response(arena, "500", "{\"error\":\"account_save_failed\"}"); char encoded_account[CONNECTOR_ID_MAX * 3]; char encoded_email[sizeof(account.email) * 3]; if (!Connector_Form_Encode( account.account_id, encoded_account, sizeof(encoded_account)) || !Connector_Form_Encode( account.email, encoded_email, sizeof(encoded_email))) return json_response(arena, "500", "{\"error\":\"redirect_failed\"}"); char *location = Dowa_Arena_Allocate(arena, 1024); snprintf( location, 1024, "/auth-test.html?account_id=%s&email=%s", encoded_account, encoded_email); Seobeo_Request_Entry *response = NULL; Dowa_HashMap_Push_Arena(response, "status", "303", arena); Dowa_HashMap_Push_Arena(response, "Location", location, arena); Dowa_HashMap_Push_Arena(response, "content-type", "text/plain", arena); Dowa_HashMap_Push_Arena(response, "body", "Google account connected", arena); return response; } static Seobeo_Request_Entry *Disconnect( Seobeo_Request_Entry *request, Dowa_Arena *arena) { const char *user = current_user(request, TRUE, arena); const char *account_id = map_value(request, ":account_id"); if (!user) return json_response(arena, "401", "{\"error\":\"unauthorized\"}"); Connector_Account account; if (!safe_component(account_id) || !Connector_Store_Get_Account(g_store, user, account_id, &account)) return json_response(arena, "404", "{\"error\":\"account_not_found\"}"); const char *token = account.refresh_token[0] ? account.refresh_token : account.access_token; if (Connector_Google_Revoke(g_google, token, arena) != CONNECTOR_OK) return json_response(arena, "502", "{\"error\":\"revoke_failed\"}"); if (!Connector_Store_Delete_Account(g_store, user, account_id)) return json_response(arena, "500", "{\"error\":\"disconnect_failed\"}"); return json_response(arena, "200", "{\"disconnected\":true}"); } static const char *confirmation_action( Connector_Operation operation, boolean overwrite) { if (operation == CONNECTOR_OP_GMAIL_SEND) return "gmail.send"; if (operation == CONNECTOR_OP_DRIVE_UPDATE && overwrite) return "drive.overwrite"; return Connector_Operation_Name(operation); } static Seobeo_Request_Entry *execute_operation( Seobeo_Request_Entry *request, Dowa_Arena *arena, Connector_Operation operation, const char *method, const char *path_prefix, boolean append_resource, boolean overwrite) { const char *user = current_user( request, Connector_Operation_Is_Mutation(operation), arena); const char *account_id = map_value(request, ":account_id"); if (!user) return json_response(arena, "401", "{\"error\":\"unauthorized\"}"); if (!safe_component(account_id)) return json_response(arena, "400", "{\"error\":\"invalid_account\"}"); Connector_Account account; if (!Connector_Store_Get_Account(g_store, user, account_id, &account)) return json_response(arena, "404", "{\"error\":\"account_not_found\"}"); if (account.expires_at <= (int64)time(NULL) + 60) { if (Connector_Google_Refresh(g_google, &account, arena) != CONNECTOR_OK || !Connector_Store_Save_Account(g_store, &account)) return json_response(arena, "502", "{\"error\":\"token_refresh_failed\"}"); } const char *resource = map_value(request, ":resource_id"); char path[1024]; if (append_resource) { if (!safe_component(resource)) return json_response(arena, "400", "{\"error\":\"invalid_resource\"}"); snprintf(path, sizeof(path), "%s/%s", path_prefix, resource); } else snprintf(path, sizeof(path), "%s", path_prefix); const char *body = map_value(request, "Body"); const char *content_length = map_value(request, "Content-Length"); size_t body_length = body ? strlen(body) : 0; if (content_length) { char *end = NULL; unsigned long parsed = strtoul(content_length, &end, 10); if (!end || *end || parsed > CONNECTOR_MAX_JSON_BYTES || (parsed && !body)) return json_response(arena, "400", "{\"error\":\"invalid_body\"}"); body_length = parsed; } Connector_Provider_Request provider_request = { .method = method, .path = path, .query = map_value(request, "QueryString"), .content_type = map_value(request, "Content-Type"), .body = body, .body_length = body_length, .download_path = NULL, .overwrite = overwrite }; char digest[65]; if (!Connector_Request_Digest( user, account_id, operation, &provider_request, digest)) return json_response(arena, "400", "{\"error\":\"invalid_request\"}"); const char *idempotency = map_value(request, "Idempotency-Key"); if (Connector_Operation_Is_Mutation(operation)) { if (!idempotency || !safe_component(idempotency)) return json_response( arena, "400", "{\"error\":\"idempotency_key_required\"}"); int32 cached_status = 0; char cached_body[8192]; if (Connector_Store_Get_Idempotent( g_store, user, idempotency, digest, &cached_status, cached_body, sizeof(cached_body))) { char status_text[16]; snprintf(status_text, sizeof(status_text), "%d", cached_status); char *copy = Dowa_String_Copy_Arena(cached_body, arena); return json_response(arena, status_text, copy); } if (Connector_Store_Idempotency_Conflict( g_store, user, idempotency, digest)) return json_response( arena, "409", "{\"error\":\"idempotency_key_conflict\"}"); } if (operation == CONNECTOR_OP_DRIVE_UPDATE && body && (strstr(body, "\"trashed\"") || strstr(body, "\"permissions\""))) return json_response( arena, "403", "{\"error\":\"drive_mutation_not_allowed\"}"); const char *action = confirmation_action(operation, overwrite); if (Connector_Store_Get_Policy(g_store, user, action) == CONNECTOR_CONFIRM_ALWAYS) { const char *token = map_value(request, "X-Connector-Confirmation"); if (!token || !Connector_Store_Consume_Confirmation( g_store, user, digest, token, (int64)time(NULL))) { char *response = Dowa_Arena_Allocate(arena, 160); snprintf( response, 160, "{\"error\":\"confirmation_required\",\"request_digest\":\"%s\"}", digest); return json_response(arena, "409", response); } } Connector_Provider_Response provider_response = {0}; Connector_Status status = Connector_Google_Execute( g_google, operation, &provider_request, account.access_token, &provider_response, arena); int32 http_status = status == CONNECTOR_OK ? provider_response.provider_status : 502; const char *response_body = provider_response.body ? provider_response.body : "{\"error\":\"provider_error\"}"; if (Connector_Operation_Is_Mutation(operation)) { Connector_Store_Put_Idempotent( g_store, user, idempotency, digest, http_status, response_body); Connector_Store_Audit( g_store, user, account_id, Connector_Operation_Name(operation), digest, http_status); } char status_text[16]; snprintf(status_text, sizeof(status_text), "%d", http_status); if (operation == CONNECTOR_OP_DRIVE_DOWNLOAD && provider_response.body && status == CONNECTOR_OK) { Seobeo_Request_Entry *download = NULL; char *content_length = Dowa_Arena_Allocate(arena, 32); snprintf( content_length, 32, "%zu", provider_response.body_length); Dowa_HashMap_Push_Arena(download, "status", status_text, arena); Dowa_HashMap_Push_Arena( download, "content-type", "application/octet-stream", arena); Dowa_HashMap_Push_Arena(download, "body", provider_response.body, arena); Dowa_HashMap_Push_Arena( download, "content-length", content_length, arena); return download; } return json_response(arena, status_text, response_body); } #define OP_HANDLER(name, operation, method, path, resource, overwrite) \ static Seobeo_Request_Entry *name( \ Seobeo_Request_Entry *request, Dowa_Arena *arena) { \ return execute_operation( \ request, arena, operation, method, path, resource, overwrite); \ } OP_HANDLER(Drive_List, CONNECTOR_OP_DRIVE_LIST, "GET", "/drive/v3/files", FALSE, FALSE) OP_HANDLER(Drive_Get, CONNECTOR_OP_DRIVE_GET, "GET", "/drive/v3/files", TRUE, FALSE) OP_HANDLER(Drive_Changes, CONNECTOR_OP_DRIVE_CHANGES, "GET", "/drive/v3/changes", FALSE, FALSE) OP_HANDLER(Drive_Create, CONNECTOR_OP_DRIVE_CREATE, "POST", "/drive/v3/files", FALSE, FALSE) OP_HANDLER(Drive_Upload, CONNECTOR_OP_DRIVE_UPLOAD, "POST", "/upload/drive/v3/files", FALSE, FALSE) OP_HANDLER(Drive_Update, CONNECTOR_OP_DRIVE_UPDATE, "PATCH", "/upload/drive/v3/files", TRUE, TRUE) OP_HANDLER(Gmail_List, CONNECTOR_OP_GMAIL_LIST, "GET", "/gmail/v1/users/me/messages", FALSE, FALSE) OP_HANDLER(Gmail_Get, CONNECTOR_OP_GMAIL_GET, "GET", "/gmail/v1/users/me/messages", TRUE, FALSE) OP_HANDLER(Gmail_History, CONNECTOR_OP_GMAIL_HISTORY, "GET", "/gmail/v1/users/me/history", FALSE, FALSE) OP_HANDLER(Gmail_Draft, CONNECTOR_OP_GMAIL_DRAFT_CREATE, "POST", "/gmail/v1/users/me/drafts", FALSE, FALSE) OP_HANDLER(Gmail_Send, CONNECTOR_OP_GMAIL_SEND, "POST", "/gmail/v1/users/me/messages/send", FALSE, FALSE) static Seobeo_Request_Entry *Drive_Download( Seobeo_Request_Entry *request, Dowa_Arena *arena) { Seobeo_Request_Entry *query = Dowa_HashMap_Get_Ptr(request, "QueryString"); if (query && query->value && query->value[0]) { size_t length = strlen(query->value) + 11; char *with_media = Dowa_Arena_Allocate(arena, length); snprintf(with_media, length, "%s&alt=media", query->value); query->value = with_media; } else Dowa_HashMap_Push_Arena(request, "QueryString", "alt=media", arena); return execute_operation( request, arena, CONNECTOR_OP_DRIVE_DOWNLOAD, "GET", "/drive/v3/files", TRUE, FALSE); } static Seobeo_Request_Entry *Gmail_Attachment( Seobeo_Request_Entry *request, Dowa_Arena *arena) { const char *message = map_value(request, ":message_id"); const char *attachment = map_value(request, ":resource_id"); if (!safe_component(message) || !safe_component(attachment)) return json_response(arena, "400", "{\"error\":\"invalid_resource\"}"); char path[512]; snprintf( path, sizeof(path), "/gmail/v1/users/me/messages/%s/attachments", message); return execute_operation( request, arena, CONNECTOR_OP_GMAIL_ATTACHMENT, "GET", path, TRUE, FALSE); } static Seobeo_Request_Entry *Create_Confirmation( Seobeo_Request_Entry *request, Dowa_Arena *arena) { const char *user = current_user(request, TRUE, arena); const char *body = map_value(request, "Body"); if (!user) return json_response(arena, "401", "{\"error\":\"unauthorized\"}"); if (!body || strlen(body) > 4096) return json_response(arena, "400", "{\"error\":\"invalid_body\"}"); Dowa_JSON_Value parsed = Dowa_JSON_Parse(body, (int32)strlen(body), arena); if (parsed.type != DOWA_JSON_OBJECT) return json_response(arena, "400", "{\"error\":\"invalid_json\"}"); char *digest = Dowa_JSON_Get_String(parsed.object_val, "request_digest"); if (!digest || strlen(digest) != 64) return json_response(arena, "400", "{\"error\":\"invalid_digest\"}"); Connector_OAuth_Start random; if (!Connector_OAuth_PKCE_Start(&random) || !Connector_Store_Create_Confirmation( g_store, user, digest, random.state, (int64)time(NULL) + 300)) return json_response(arena, "500", "{\"error\":\"confirmation_failed\"}"); char *response = Dowa_Arena_Allocate(arena, 180); snprintf( response, 180, "{\"confirmation_token\":\"%s\",\"expires_in\":300}", random.state); return json_response(arena, "201", response); } static boolean configurable_confirmation_action(const char *action) { return action && (!strcmp(action, "gmail.send") || !strcmp(action, "drive.overwrite")); } static Seobeo_Request_Entry *Get_Confirmation_Policy( Seobeo_Request_Entry *request, Dowa_Arena *arena) { const char *user = current_user(request, FALSE, arena); const char *action = map_value(request, ":action"); if (!user) return json_response(arena, "401", "{\"error\":\"unauthorized\"}"); if (!configurable_confirmation_action(action)) return json_response(arena, "400", "{\"error\":\"invalid_action\"}"); Connector_Confirmation_Policy policy = Connector_Store_Get_Policy(g_store, user, action); char *response = Dowa_Arena_Allocate(arena, 96); snprintf( response, 96, "{\"action\":\"%s\",\"policy\":\"%s\"}", action, policy == CONNECTOR_CONFIRM_ALWAYS ? "always" : "never"); return json_response(arena, "200", response); } static Seobeo_Request_Entry *Set_Confirmation_Policy( Seobeo_Request_Entry *request, Dowa_Arena *arena) { const char *user = current_user(request, TRUE, arena); const char *action = map_value(request, ":action"); const char *body = map_value(request, "Body"); if (!user) return json_response(arena, "401", "{\"error\":\"unauthorized\"}"); if (!configurable_confirmation_action(action)) return json_response(arena, "400", "{\"error\":\"invalid_action\"}"); if (!body || strlen(body) > 256) return json_response(arena, "400", "{\"error\":\"invalid_body\"}"); Dowa_JSON_Value parsed = Dowa_JSON_Parse(body, (int32)strlen(body), arena); if (parsed.type != DOWA_JSON_OBJECT) return json_response(arena, "400", "{\"error\":\"invalid_json\"}"); const char *policy_text = Dowa_JSON_Get_String(parsed.object_val, "policy"); Connector_Confirmation_Policy policy; if (policy_text && !strcmp(policy_text, "always")) policy = CONNECTOR_CONFIRM_ALWAYS; else if (policy_text && !strcmp(policy_text, "never")) policy = CONNECTOR_CONFIRM_NEVER; else return json_response(arena, "400", "{\"error\":\"invalid_policy\"}"); if (!Connector_Store_Set_Policy(g_store, user, action, policy)) return json_response(arena, "500", "{\"error\":\"policy_save_failed\"}"); return json_response(arena, "200", policy == CONNECTOR_CONFIRM_ALWAYS ? "{\"policy\":\"always\"}" : "{\"policy\":\"never\"}"); } static Seobeo_Request_Entry *Rejected_Mutation( Seobeo_Request_Entry *request, Dowa_Arena *arena) { (void)request; return json_response( arena, "403", "{\"error\":\"operation_not_allowed\",\"allowed_mutations\":[" "\"drive.create\",\"drive.upload\",\"drive.update\"," "\"gmail.draft.create\",\"gmail.send\"]}"); } void Connector_Service_Configure( Connector_Store *store, const Connector_Google_Config *google, Connector_Auth_Adapter auth) { g_store = store; g_google = google; g_auth = auth; } void Connector_Service_Register_Routes(void) { Seobeo_Router_Register("GET", "/health", Health); Seobeo_Router_Register("GET", "/auth-test.html", Auth_Test_Page); Seobeo_Router_Register("GET", "/v1/ai/tools", AI_Tools); Seobeo_Router_Register("GET", "/v1/auth/session", Auth_Session); Seobeo_Router_Register("GET", "/v1/accounts", List_Accounts); Seobeo_Router_Register("POST", "/v1/oauth/google/start", OAuth_Start); Seobeo_Router_Register("GET", "/v1/oauth/google/callback", OAuth_Callback); Seobeo_Router_Register("DELETE", "/v1/accounts/:account_id", Disconnect); Seobeo_Router_Register("POST", "/v1/confirmations", Create_Confirmation); Seobeo_Router_Register( "GET", "/v1/settings/confirmations/:action", Get_Confirmation_Policy); Seobeo_Router_Register( "PUT", "/v1/settings/confirmations/:action", Set_Confirmation_Policy); Seobeo_Router_Register("GET", "/v1/accounts/:account_id/drive/files", Drive_List); Seobeo_Router_Register("GET", "/v1/accounts/:account_id/drive/files/:resource_id", Drive_Get); Seobeo_Router_Register("GET", "/v1/accounts/:account_id/drive/files/:resource_id/download", Drive_Download); Seobeo_Router_Register("GET", "/v1/accounts/:account_id/drive/changes", Drive_Changes); Seobeo_Router_Register("POST", "/v1/accounts/:account_id/drive/files", Drive_Create); Seobeo_Router_Register("POST", "/v1/accounts/:account_id/drive/uploads", Drive_Upload); Seobeo_Router_Register("PATCH", "/v1/accounts/:account_id/drive/files/:resource_id", Drive_Update); Seobeo_Router_Register("DELETE", "/v1/accounts/:account_id/drive/files/:resource_id", Rejected_Mutation); Seobeo_Router_Register("POST", "/v1/accounts/:account_id/drive/files/:resource_id/permissions", Rejected_Mutation); Seobeo_Router_Register("GET", "/v1/accounts/:account_id/gmail/messages", Gmail_List); Seobeo_Router_Register("GET", "/v1/accounts/:account_id/gmail/messages/:resource_id", Gmail_Get); Seobeo_Router_Register("GET", "/v1/accounts/:account_id/gmail/messages/:message_id/attachments/:resource_id", Gmail_Attachment); Seobeo_Router_Register("GET", "/v1/accounts/:account_id/gmail/history", Gmail_History); Seobeo_Router_Register("POST", "/v1/accounts/:account_id/gmail/drafts", Gmail_Draft); Seobeo_Router_Register("POST", "/v1/accounts/:account_id/gmail/send", Gmail_Send); Seobeo_Router_Register("DELETE", "/v1/accounts/:account_id/gmail/messages/:resource_id", Rejected_Mutation); Seobeo_Router_Register("PATCH", "/v1/accounts/:account_id/gmail/messages/:resource_id/labels", Rejected_Mutation); Seobeo_Router_Register("PATCH", "/v1/accounts/:account_id/gmail/messages/:resource_id/read-state", Rejected_Mutation); }