diff --git a/Schema/PduV1.json b/Schema/PduV1.json index 6e24bda..d63f2df 100644 --- a/Schema/PduV1.json +++ b/Schema/PduV1.json @@ -34,6 +34,10 @@ "pdu_status": { "type": "PduV1Status", "required": false + }, + "redacted_because": { + "type": "object", + "required": false } } }, diff --git a/src/Room.c b/src/Room.c index 952729b..9b436d9 100644 --- a/src/Room.c +++ b/src/Room.c @@ -617,6 +617,64 @@ RoomLeave(Room *room, User *user, char **errp) } return ret; } +char * +RoomRedact(Room *room, User *user, char *eventID, char **errp) +{ + CommonID *userId = NULL; + char *userString = NULL; + char *server = NULL; + HashMap *event = NULL; + HashMap *pdu = NULL; + char *ret = NULL; + + if (!room || !user || !eventID) + { + return NULL; + } + + server = ConfigGetServerName(room->db); + if (!server) + { + if (errp) + { + *errp = "Could not retrieve servername."; + } + return NULL; + } + userId = UserIdParse(UserGetName(user), server); + userId->sigil = '@'; + userString = ParserRecomposeCommonID(*userId); + Free(server); + server = NULL; + + if (!RoomContainsUser(room, userString)) + { + ret = NULL; + if (errp) + { + *errp = "User is not already in-room."; + } + goto end; + } + + event = RoomEventCreate(userString, + "m.room.redaction", NULL, + HashMapCreate() + ); + HashMapSet(event, "redacts", JsonValueString(eventID)); + pdu = RoomEventSend(room, event, errp); + + ret = StrDuplicate(JsonValueAsString(HashMapGet(pdu, "event_id"))); +end: + UserIdFree(userId); + JsonFree(event); + JsonFree(pdu); + if (userString) + { + Free(userString); + } + return ret; +} bool RoomJoin(Room *room, User *user, char **errp) { diff --git a/src/Room/V1/Populate.c b/src/Room/V1/Populate.c index 5285975..54ea282 100644 --- a/src/Room/V1/Populate.c +++ b/src/Room/V1/Populate.c @@ -27,7 +27,8 @@ PopulateEventV1(Room * room, HashMap * event, PduV1 * pdu, ServerPart serv) StrDuplicate(JsonValueAsString(JsonGet(event, 1, "sender"))); pdu->type = StrDuplicate(JsonValueAsString(JsonGet(event, 1, "type"))); - pdu->redacts = NULL; + pdu->redacts = + StrDuplicate(JsonValueAsString(JsonGet(event, 1, "redacts"))); if (JsonGet(event, 1, "state_key")) { pdu->state_key = diff --git a/src/Room/V1/Send.c b/src/Room/V1/Send.c index 7727875..42f7396 100644 --- a/src/Room/V1/Send.c +++ b/src/Room/V1/Send.c @@ -99,6 +99,113 @@ RoomGetEventStatusV1(Room *room, PduV1 *pdu, State *prev, bool client, char **er return PDUV1_STATUS_ACCEPTED; } + +static void +RedactPDU1(HashMap *obj, HashMap *redactor) +{ + Array *keys; + HashMap *content, *_unsigned; + char *type = JsonValueAsString(HashMapGet(obj, "type")); + size_t i; + if (!obj) + { + return; + } + + keys = HashMapKeys(obj); + for (i = 0; i < ArraySize(keys); i++) + { + char *key = ArrayGet(keys, i); + if (!StrEquals(key, "event_id") && !StrEquals(key, "type") && + !StrEquals(key, "room_id") && !StrEquals(key, "sender") && + !StrEquals(key, "state_key")&& !StrEquals(key, "content") && + !StrEquals(key, "hashes") && !StrEquals(key, "signature") && + !StrEquals(key, "depth") && !StrEquals(key, "prev_events") && + !StrEquals(key, "auth_events")&& !StrEquals(key, "origin") && + !StrEquals(key, "unsigned") && + !StrEquals(key, "origin_server_ts")&&!StrEquals(key, "membership")) + { + JsonValueFree(HashMapDelete(obj, key)); + continue; + } + } + ArrayFree(keys); + + _unsigned = JsonValueAsObject(HashMapGet(obj, "unsigned")); + keys = HashMapKeys(_unsigned); + for (i = 0; i < ArraySize(keys); i++) + { + char *key = ArrayGet(keys, i); + if (!StrEquals(key, "next_events") && !StrEquals(key, "pdu_status")) + { + JsonValueFree(HashMapDelete(_unsigned, key)); + continue; + } + } + ArrayFree(keys); + JsonValueFree(HashMapSet(_unsigned, + "redacted_because", JsonValueObject(JsonDuplicate(redactor)) + )); + + content = JsonValueAsObject(HashMapGet(obj, "content")); + keys = HashMapKeys(content); + for (i = 0; i < ArraySize(keys); i++) + { + char *key = ArrayGet(keys, i); + if (StrEquals(type, "m.room.member")) + { + if (StrEquals(key, "membership")) + { + continue; + } + } + if (StrEquals(type, "m.room.create")) + { + if (StrEquals(key, "creator")) + { + continue; + } + } + if (StrEquals(type, "m.room.join_rules")) + { + if (StrEquals(key, "join_rule")) + { + continue; + } + } + if (StrEquals(type, "m.room.aliases")) + { + if (StrEquals(key, "aliases")) + { + continue; + } + } + if (StrEquals(type, "m.room.history_visibility")) + { + if (StrEquals(key, "history_visibility")) + { + continue; + } + } + if (StrEquals(type, "m.room.power_levels")) + { + if (StrEquals(key, "ban") || + StrEquals(key, "events") || + StrEquals(key, "events_default") || + StrEquals(key, "kick") || + StrEquals(key, "redact") || + StrEquals(key, "state_default") || + StrEquals(key, "users") || + StrEquals(key, "users_default")) + { + continue; + } + } + + JsonValueFree(HashMapDelete(content, key)); + } + ArrayFree(keys); +} bool RoomAddEventV1(Room *room, PduV1 pdu) { @@ -215,7 +322,7 @@ RoomAddEventV1(Room *room, PduV1 pdu) if (StrEquals(pdu.type, "m.room.member")) { CommonID *id = UserIdParse(pdu.state_key, NULL); - User *user = UserLock(room->db, id->local); + User *user = UserLockID(room->db, id); char *membership = JsonValueAsString( HashMapGet(pdu.content, "membership") ); @@ -240,15 +347,25 @@ RoomAddEventV1(Room *room, PduV1 pdu) UserIdFree(id); UserUnlock(user); } + else if (StrEquals(pdu.type, "m.room.redaction") && pdu.redacts) + { + char *redacted = pdu.redacts; + DbRef *eventRef = DbLock(room->db, + 4, "rooms", room->id, + "events", redacted + ); + + RedactPDU1(DbJson(eventRef), pdu_json); + DbUnlock(room->db, eventRef); + } - /* Notify the user by pushing out the user */ state = StateCurrent(room); while (StateIterate(state, &type, &state_key, (void **) &event_id)) { if (StrEquals(type, "m.room.member")) { CommonID *id = UserIdParse(state_key, NULL); - User *user = UserLock(room->db, id->local); + User *user = UserLockID(room->db, id); UserPushEvent(user, pdu_json); diff --git a/src/Routes.c b/src/Routes.c index 32f8f68..01d893a 100644 --- a/src/Routes.c +++ b/src/Routes.c @@ -78,6 +78,7 @@ RouterBuild(void) R("/_matrix/client/v3/user/(.*)/filter/(.*)", RouteFilter); R("/_matrix/client/v3/rooms/(.*)/send/(.*)/(.*)", RouteSendEvent); + R("/_matrix/client/v3/rooms/(.*)/redact/(.*)/(.*)", RouteRedact); R("/_matrix/client/v3/rooms/(.*)/state/(.*)/(.*)", RouteSendState); R("/_matrix/client/v3/rooms/(.*)/state/(.*)", RouteSendState); R("/_matrix/client/v3/rooms/(.*)/event/(.*)", RouteFetchEvent); diff --git a/src/Routes/RouteRedact.c b/src/Routes/RouteRedact.c new file mode 100644 index 0000000..c83b5e3 --- /dev/null +++ b/src/Routes/RouteRedact.c @@ -0,0 +1,148 @@ +/* + * 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 + + +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include + +ROUTE_IMPL(RouteRedact, path, argp) +{ + RouteArgs *args = argp; + Db *db = args->matrixArgs->db; + + HashMap *request = NULL; + HashMap *response = NULL; + + User *user = NULL; + CommonID *id = NULL; + char *token = NULL; + + char *serverName = NULL; + + char *roomId = ArrayGet(path, 0); + char *eventId = ArrayGet(path, 1); + char *transId = ArrayGet(path, 2); + char *redactId = NULL; + char *sender = NULL; + + Room *room = NULL; + + char *err = NULL; + + if (!roomId || !eventId || !transId) + { + /* Should be impossible */ + HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR); + return MatrixErrorCreate(M_UNKNOWN, NULL); + } + if (HttpRequestMethodGet(args->context) != HTTP_PUT) + { + err = "Unknown request method."; + HttpResponseStatus(args->context, HTTP_BAD_REQUEST); + response = MatrixErrorCreate(M_UNRECOGNIZED, err); + goto finish; + } + + serverName = ConfigGetServerName(db); + if (!serverName) + { + HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR); + response = MatrixErrorCreate(M_UNKNOWN, NULL); + goto finish; + } + + response = MatrixGetAccessToken(args->context, &token); + if (response) + { + goto finish; + } + + request = JsonDecode(HttpServerStream(args->context)); + if (!request) + { + HttpResponseStatus(args->context, HTTP_BAD_REQUEST); + response = MatrixErrorCreate(M_NOT_JSON, NULL); + goto finish; + } + + user = UserAuthenticate(db, token); + if (!user) + { + HttpResponseStatus(args->context, HTTP_UNAUTHORIZED); + response = MatrixErrorCreate(M_UNKNOWN_TOKEN, NULL); + goto finish; + } + id = UserIdParse(UserGetName(user), serverName); + id->sigil = '@'; + sender = ParserRecomposeCommonID(*id); + + if ((response = UserGetTransaction(user, transId, "redact"))) + { + goto finish; + } + + room = RoomLock(db, roomId); + if (!RoomContainsUser(room, sender)) + { + err = "User is not in the room."; + HttpResponseStatus(args->context, HTTP_UNAUTHORIZED); + response = MatrixErrorCreate(M_FORBIDDEN, err); + goto finish; + } + + if (!(redactId = RoomRedact(room, user, eventId, &err))) + { + HttpResponseStatus(args->context, HTTP_UNAUTHORIZED); + response = MatrixErrorCreate(M_FORBIDDEN, err); + goto finish; + } + + response = HashMapCreate(); + HashMapSet(response, "event_id", JsonValueString(redactId)); + UserSetTransaction(user, transId, "redact", response); + Free(redactId); +finish: + RoomUnlock(room); + Free(serverName); + if (sender) + { + Free(sender); + } + UserIdFree(id); + UserUnlock(user); + JsonFree(request); + return response; +} diff --git a/src/User.c b/src/User.c index efcfbb5..df012a6 100644 --- a/src/User.c +++ b/src/User.c @@ -23,7 +23,6 @@ * SOFTWARE. */ #include -#include #include #include #include @@ -33,6 +32,8 @@ #include #include +#include +#include #include #include @@ -113,6 +114,25 @@ UserExists(Db * db, char *name) return DbExists(db, 2, "users", name); } +User * +UserLockID(Db *db, CommonID *id) +{ + char *server; + if (!db || !id || id->sigil != '@') + { + return NULL; + } + + server = ConfigGetServerName(db); + if (!ParserServerNameEquals(id->server, server)) + { + Free(server); + return NULL; + } + Free(server); + + return UserLock(db, id->local); +} User * UserLock(Db * db, char *name) { diff --git a/src/include/Room.h b/src/include/Room.h index 30fe483..ac4ccb5 100644 --- a/src/include/Room.h +++ b/src/include/Room.h @@ -231,6 +231,11 @@ extern bool RoomContainsUser(Room *, char *); */ extern bool RoomCanJoin(Room *, char *); +/** + * Makes a local user redact an event(from it's ID). + */ +extern char * RoomRedact(Room *, User *, char *, char **); + /** * Makes a local user join a room, and returns true if * the room was joined. diff --git a/src/include/Routes.h b/src/include/Routes.h index 4bf0bca..0300266 100644 --- a/src/include/Routes.h +++ b/src/include/Routes.h @@ -102,6 +102,7 @@ ROUTE(RoutePrivileges); ROUTE(RouteCreateRoom); ROUTE(RouteSendEvent); +ROUTE(RouteRedact); ROUTE(RouteSendState); ROUTE(RouteJoinRoom); ROUTE(RouteKickRoom); diff --git a/src/include/User.h b/src/include/User.h index 06eb326..b07edb6 100644 --- a/src/include/User.h +++ b/src/include/User.h @@ -135,6 +135,13 @@ extern User * UserCreate(Db *, char *, char *); */ extern User * UserLock(Db *, char *); +/** + * Behaves like + * .Fn UserLock , + * but tries to check from a CommonID, and verifies + * for the serverpart. */ +extern User * UserLockID(Db *, CommonID *); + /** * Take an access token, figure out what user it belongs to, and then * returns a reference to that user. This function should be used by