/* * Copyright (C) 2022-2024 Jordan Bancino <@jordan:bancino.net> with * other valuable contributors. See CONTRIBUTORS.txt for the full list. * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation files * (the "Software"), to deal in the Software without restriction, * including without limitation the rights to use, copy, modify, merge, * publish, distribute, sublicense, and/or sell copies of the Software, * and to permit persons to whom the Software is furnished to do so, * subject to the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ #include <Cytoplasm/HttpServer.h> #include <Routes.h> #include <Schema/SyncResponse.h> #include <Schema/Filter.h> #include <Cytoplasm/HashMap.h> #include <Cytoplasm/Memory.h> #include <Cytoplasm/Json.h> #include <Cytoplasm/Util.h> #include <Cytoplasm/Str.h> #include <Filter.h> #include <State.h> #include <User.h> #include <Room.h> #include <string.h> #include <stdlib.h> static ClientEventWithoutRoomID ClientfyEventSync(HashMap *pdu) { ClientEventWithoutRoomID ret = { 0 }; char *ignored; ClientEventWithoutRoomIDFromJson(pdu, &ret, &ignored); return ret; } static StrippedStateEvent StripStateEventSync(HashMap *pdu) { StrippedStateEvent ret = { 0 }; char *ignored; StrippedStateEventFromJson(pdu, &ret, &ignored); return ret; } ROUTE_IMPL(RouteSync, path, argp) { RouteArgs *args = argp; Db *db = args->matrixArgs->db; HashMap *params = NULL; HashMap *response = NULL; SyncResponse sync = { 0 }; Filter *filterData = NULL; Array *invites; Array *joins; size_t i; User *user = NULL; char *token = NULL; char *prevBatch = NULL; char *nextBatch = NULL; char *currBatch = NULL; char *timeout = NULL; char *filter = NULL; char *err; int timeoutDuration; /* TODO: Respect `timeout', (and stop when something is * pushed, maybe by 'polling' the database? sounds like * a bad idea) */ if (HttpRequestMethodGet(args->context) != HTTP_GET) { err = "Unknown request method."; HttpResponseStatus(args->context, HTTP_BAD_REQUEST); response = MatrixErrorCreate(M_UNRECOGNIZED, err); goto finish; } response = MatrixGetAccessToken(args->context, &token); if (response) { goto finish; } user = UserAuthenticate(db, token); if (!user) { HttpResponseStatus(args->context, HTTP_UNAUTHORIZED); response = MatrixErrorCreate(M_UNKNOWN_TOKEN, NULL); goto finish; } params = HttpRequestParams(args->context); prevBatch = HashMapGet(params, "since"); timeout = HashMapGet(params, "timeout"); filter = HashMapGet(params, "filter"); timeoutDuration = timeout ? atoi(timeout) : 0; if (filter) { char *userName = UserGetName(user); if (!(filterData = FilterDecode(db, userName, filter))) { err = "Couldn't decode filter given in"; HttpResponseStatus(args->context, HTTP_BAD_REQUEST); response = MatrixErrorCreate(M_BAD_JSON, err); } } if (!prevBatch) { prevBatch = NULL; nextBatch = UserInitSyncDiff(user); UserFillSyncDiff(user, nextBatch); } else if (timeout && timeoutDuration) { char *name = StrDuplicate(UserGetName(user)); UserUnlock(user); UserAwaitNotification(name, timeoutDuration); Free(name); user = UserAuthenticate(db, token); } currBatch = prevBatch ? prevBatch : nextBatch; /* TODO: I only am manually parsing this because j2s does not support * a hashmap of unknown keys pointing to a known type. */ sync.rooms.invite = HashMapCreate(); sync.rooms.join = HashMapCreate(); /* invites */ invites = UserGetInvites(user, currBatch); for (i = 0; i < ArraySize(invites); i++) { char *roomId = ArrayGet(invites, i); InvitedRooms invited = { 0 }; HashMap *invitedObject; if (IsRoomFiltered(filterData, roomId)) { continue; } invited.invite_state.events = ArrayCreate(); invitedObject = InvitedRoomsToJson(&invited); JsonSet( sync.rooms.invite, JsonValueObject(invitedObject), 1, roomId ); InvitedRoomsFree(&invited); } UserFreeList(invites); /* Joins */ joins = UserGetJoins(user, currBatch); for (i = 0; i < ArraySize(joins); i++) { /* TODO: Rename these variables */ char *roomId = ArrayGet(joins, i); JoinedRooms joined = { 0 }; char *firstEvent = NULL; char *type, *key, *id; HashMap *joinedObj; HashMap *state; Array *el; size_t j; Room *r; if (IsRoomFiltered(filterData, roomId)) { continue; } el = UserGetEvents(user, currBatch, roomId); r = RoomLock(db, roomId); state = StateCurrent(r); joined.timeline.events = ArrayCreate(); for (j = 0; j < ArraySize(el); j++) { char *event = ArrayGet(el, j); HashMap *eventObj = RoomEventFetch(r, event); HashMap *filteredObj = FilterApply(filterData, eventObj); if (filteredObj) { ClientEventWithoutRoomID rc = ClientfyEventSync(filteredObj); ClientEventWithoutRoomID *c = Malloc(sizeof(*c)); memcpy(c, &rc, sizeof(*c)); if (!firstEvent) { firstEvent = c->event_id; } ArrayAdd(joined.timeline.events, c); } JsonFree(eventObj); JsonFree(filteredObj); } joined.timeline.prev_batch = UserNewMessageToken( user, roomId, firstEvent ); /* TODO: Don't shove the entire state. * That's a recipe for disaster, especially on large rooms. */ joined.state.events = ArrayCreate(); while (StateIterate(state, &type, &key, (void **) &id)) { HashMap *e = RoomEventFetch(r, id); StrippedStateEvent rs = StripStateEventSync(e); StrippedStateEvent *s = Malloc(sizeof(*s)); memcpy(s, &rs, sizeof(*s)); JsonFree(e); ArrayAdd(joined.state.events, s); Free(type); Free(key); } StateFree(state); RoomUnlock(r); UserFreeList(el); joinedObj = JoinedRoomsToJson(&joined); HashMapSet(sync.rooms.join, roomId, JsonValueObject(joinedObj)); JoinedRoomsFree(&joined); } UserFreeList(joins); if (prevBatch) { UserDropSync(user, prevBatch); nextBatch = UserInitSyncDiff(user); UserFillSyncDiff(user, nextBatch); } sync.next_batch = nextBatch; response = SyncResponseToJson(&sync); SyncResponseFree(&sync); finish: FilterDestroy(filterData); UserUnlock(user); (void) path; return response; }