summary refs log tree commit diff
diff options
context:
space:
mode:
authorKonstantinos Sideris <sideris.konstantin@gmail.com>2017-05-02 04:22:33 +0300
committerKonstantinos Sideris <sideris.konstantin@gmail.com>2017-05-04 17:11:04 +0300
commitb064df8b45bb4b22294d1f6691638e5a98423371 (patch)
tree4d9c3c57342e408bcc67b333f0f3958f4c929843
parentmacOS fixes (#15) (diff)
downloadnheko-b064df8b45bb4b22294d1f6691638e5a98423371.tar.xz
Add event contents
-rw-r--r--CMakeLists.txt41
-rw-r--r--Makefile9
-rw-r--r--include/events/AliasesEventContent.h42
-rw-r--r--include/events/AvatarEventContent.h46
-rw-r--r--include/events/CanonicalAliasEventContent.h49
-rw-r--r--include/events/CreateEventContent.h46
-rw-r--r--include/events/HistoryVisibilityEventContent.h48
-rw-r--r--include/events/JoinRulesEventContent.h60
-rw-r--r--include/events/MemberEventContent.h77
-rw-r--r--include/events/NameEventContent.h45
-rw-r--r--include/events/PowerLevelsEventContent.h102
-rw-r--r--include/events/TopicEventContent.h45
-rw-r--r--src/events/AliasesEventContent.cc36
-rw-r--r--src/events/AvatarEventContent.cc36
-rw-r--r--src/events/CanonicalAliasEventContent.cc31
-rw-r--r--src/events/CreateEventContent.cc31
-rw-r--r--src/events/HistoryVisibilityEventContent.cc42
-rw-r--r--src/events/JoinRulesEventContent.cc42
-rw-r--r--src/events/MemberEventContent.cc55
-rw-r--r--src/events/NameEventContent.cc31
-rw-r--r--src/events/PowerLevelsEventContent.cc80
-rw-r--r--src/events/TopicEventContent.cc31
-rw-r--r--tests/events.cc499
23 files changed, 1517 insertions, 7 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 390ef837..7e2e33a8 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -2,6 +2,8 @@ cmake_minimum_required(VERSION 3.1)
 
 project(nheko CXX)
 
+option(BUILD_TESTS "Build all tests" OFF)
+
 find_package(Qt5Widgets REQUIRED)
 find_package(Qt5Network REQUIRED)
 
@@ -114,8 +116,22 @@ set(SRC_FILES
     src/ui/ThemeManager.cc
 )
 
+set(MATRIX_EVENTS
+    src/events/AliasesEventContent.cc
+    src/events/AvatarEventContent.cc
+    src/events/CanonicalAliasEventContent.cc
+    src/events/CreateEventContent.cc
+    src/events/HistoryVisibilityEventContent.cc
+    src/events/JoinRulesEventContent.cc
+    src/events/MemberEventContent.cc
+    src/events/NameEventContent.cc
+    src/events/PowerLevelsEventContent.cc
+    src/events/TopicEventContent.cc
+)
+
 include_directories(include)
 include_directories(include/ui)
+include_directories(include/events)
 
 qt5_wrap_ui (UI_HEADERS
     forms/ChatPage.ui
@@ -161,9 +177,24 @@ qt5_wrap_cpp(MOC_HEADERS
 
 qt5_add_resources(QRC resources/res.qrc)
 
-add_executable (nheko ${OS_BUNDLE} ${SRC_FILES} ${UI_HEADERS} ${MOC_HEADERS} ${QRC})
-target_link_libraries (nheko Qt5::Widgets Qt5::Network)
+add_library(matrix_events ${MATRIX_EVENTS} src/Deserializable.cc)
+target_link_libraries(matrix_events Qt5::Core)
+
+if (BUILD_TESTS)
+    enable_testing()
 
-if(WIN32)
-  target_link_libraries(nheko Qt5::WinMain)
-endif(WIN32)
+    find_package(GTest REQUIRED)
+    include_directories(${GTEST_INCLUDE_DIRS})
+
+    add_executable(events_test tests/events.cc)
+    target_link_libraries(events_test matrix_events ${GTEST_BOTH_LIBRARIES})
+
+    add_test(MatrixEvents events_test)
+else()
+    add_executable (nheko ${OS_BUNDLE} ${SRC_FILES} ${UI_HEADERS} ${MOC_HEADERS} ${QRC})
+    target_link_libraries (nheko matrix_events Qt5::Widgets Qt5::Network)
+
+    if(WIN32)
+        target_link_libraries(nheko Qt5::WinMain)
+    endif(WIN32)
+endif()
diff --git a/Makefile b/Makefile
index 8bf98a4b..477ec858 100644
--- a/Makefile
+++ b/Makefile
@@ -1,11 +1,11 @@
 SRC := $(shell find include src -type f -type f \( -iname "*.cc" -o -iname "*.h" \))
 
 debug:
-	@cmake -H. -GNinja -Bbuild -DCMAKE_BUILD_TYPE=Debug
+	@cmake -DBUILD_TESTS=OFF -H. -GNinja -Bbuild -DCMAKE_BUILD_TYPE=Debug
 	@cmake --build build
 
 release-debug:
-	@cmake -H. -GNinja -Bbuild -DCMAKE_BUILD_TYPE=RelWithDebInfo
+	@cmake -DBUILD_TESTS=OFF -H. -GNinja -Bbuild -DCMAKE_BUILD_TYPE=RelWithDebInfo
 	@cmake --build build
 
 run:
@@ -14,6 +14,11 @@ run:
 lint:
 	@clang-format -i $(SRC)
 
+test:
+	@cmake -DBUILD_TESTS=ON -H. -GNinja -Bbuild -DCMAKE_BUILD_TYPE=Release
+	@cmake --build build
+	@cd build && GTEST_COLOR=1 ctest --verbose
+
 clean:
 	rm -rf build
 
diff --git a/include/events/AliasesEventContent.h b/include/events/AliasesEventContent.h
new file mode 100644
index 00000000..03218715
--- /dev/null
+++ b/include/events/AliasesEventContent.h
@@ -0,0 +1,42 @@
+/*
+ * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef ALIASES_EVENT_CONTENT_H
+#define ALIASES_EVENT_CONTENT_H
+
+#include <QJsonValue>
+#include <QList>
+
+#include "Deserializable.h"
+
+class AliasesEventContent : public Deserializable
+{
+public:
+	void deserialize(const QJsonValue &data) override;
+
+	inline QList<QString> aliases() const;
+
+private:
+	QList<QString> aliases_;
+};
+
+inline QList<QString> AliasesEventContent::aliases() const
+{
+	return aliases_;
+}
+
+#endif  // ALIASES_EVENT_CONTENT_H
diff --git a/include/events/AvatarEventContent.h b/include/events/AvatarEventContent.h
new file mode 100644
index 00000000..c77cb6d6
--- /dev/null
+++ b/include/events/AvatarEventContent.h
@@ -0,0 +1,46 @@
+/*
+ * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef AVATAR_EVENT_CONTENT_H
+#define AVATAR_EVENT_CONTENT_H
+
+#include <QJsonValue>
+#include <QUrl>
+
+#include "Deserializable.h"
+
+/*
+ * A picture that is associated with the room.
+ */
+
+class AvatarEventContent : public Deserializable
+{
+public:
+	void deserialize(const QJsonValue &data) override;
+
+	inline QUrl url() const;
+
+private:
+	QUrl url_;
+};
+
+inline QUrl AvatarEventContent::url() const
+{
+	return url_;
+}
+
+#endif  // AVATAR_EVENT_CONTENT_H
diff --git a/include/events/CanonicalAliasEventContent.h b/include/events/CanonicalAliasEventContent.h
new file mode 100644
index 00000000..3c031b8e
--- /dev/null
+++ b/include/events/CanonicalAliasEventContent.h
@@ -0,0 +1,49 @@
+
+/*
+ * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CANONICAL_ALIAS_EVENT_CONTENT_H
+#define CANONICAL_ALIAS_EVENT_CONTENT_H
+
+#include <QJsonValue>
+
+#include "CanonicalAliasEventContent.h"
+#include "Deserializable.h"
+
+/*
+ * This event is used to inform the room about which alias should be considered
+ * the canonical one. This could be for display purposes or as suggestion to
+ * users which alias to use to advertise the room.
+ */
+
+class CanonicalAliasEventContent : public Deserializable
+{
+public:
+	void deserialize(const QJsonValue &data) override;
+
+	inline QString alias() const;
+
+private:
+	QString alias_;
+};
+
+inline QString CanonicalAliasEventContent::alias() const
+{
+	return alias_;
+}
+
+#endif  // CANONICAL_ALIAS_EVENT_CONTENT_H
diff --git a/include/events/CreateEventContent.h b/include/events/CreateEventContent.h
new file mode 100644
index 00000000..ed1a8ce7
--- /dev/null
+++ b/include/events/CreateEventContent.h
@@ -0,0 +1,46 @@
+/*
+ * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CREATE_EVENT_CONTENT_H
+#define CREATE_EVENT_CONTENT_H
+
+#include <QJsonValue>
+
+#include "Deserializable.h"
+
+/*
+ * This is the first event in a room and cannot be changed. It acts as the root of all other events.
+ */
+
+class CreateEventContent : public Deserializable
+{
+public:
+	void deserialize(const QJsonValue &data) override;
+
+	inline QString creator() const;
+
+private:
+	// The user_id of the room creator. This is set by the homeserver.
+	QString creator_;
+};
+
+inline QString CreateEventContent::creator() const
+{
+	return creator_;
+}
+
+#endif  // CREATE_EVENT_CONTENT_H
diff --git a/include/events/HistoryVisibilityEventContent.h b/include/events/HistoryVisibilityEventContent.h
new file mode 100644
index 00000000..ac7316f4
--- /dev/null
+++ b/include/events/HistoryVisibilityEventContent.h
@@ -0,0 +1,48 @@
+/*
+ * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef HISTORY_VISIBILITY_EVENT_CONTENT_H
+#define HISTORY_VISIBILITY_EVENT_CONTENT_H
+
+#include <QJsonValue>
+
+#include "Deserializable.h"
+
+enum HistoryVisibility {
+	Invited,
+	Joined,
+	Shared,
+	WorldReadable,
+};
+
+class HistoryVisibilityEventContent : public Deserializable
+{
+public:
+	inline HistoryVisibility historyVisibility() const;
+
+	void deserialize(const QJsonValue &data) override;
+
+private:
+	HistoryVisibility history_visibility_;
+};
+
+inline HistoryVisibility HistoryVisibilityEventContent::historyVisibility() const
+{
+	return history_visibility_;
+}
+
+#endif  // HISTORY_VISIBILITY_EVENT_CONTENT_H
diff --git a/include/events/JoinRulesEventContent.h b/include/events/JoinRulesEventContent.h
new file mode 100644
index 00000000..8b647fb4
--- /dev/null
+++ b/include/events/JoinRulesEventContent.h
@@ -0,0 +1,60 @@
+/*
+ * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef JOIN_RULES_EVENT_CONTENT_H
+#define JOIN_RULES_EVENT_CONTENT_H
+
+#include <QJsonValue>
+
+#include "Deserializable.h"
+
+enum JoinRule {
+	// A user who wishes to join the room must first receive
+	// an invite to the room from someone already inside of the room.
+	Invite,
+
+	// Reserved but not yet implemented by the Matrix specification.
+	Knock,
+
+	// Reserved but not yet implemented by the Matrix specification.
+	Private,
+
+	/// Anyone can join the room without any prior action.
+	Public,
+};
+
+/*
+ * Describes how users are allowed to join the room.
+ */
+
+class JoinRulesEventContent : public Deserializable
+{
+public:
+	void deserialize(const QJsonValue &data) override;
+
+	inline JoinRule joinRule() const;
+
+private:
+	JoinRule join_rule_;
+};
+
+inline JoinRule JoinRulesEventContent::joinRule() const
+{
+	return join_rule_;
+}
+
+#endif  // JOIN_RULES_EVENT_CONTENT_H
diff --git a/include/events/MemberEventContent.h b/include/events/MemberEventContent.h
new file mode 100644
index 00000000..046dda0f
--- /dev/null
+++ b/include/events/MemberEventContent.h
@@ -0,0 +1,77 @@
+/*
+ * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MEMBER_EVENT_CONTENT_H
+#define MEMBER_EVENT_CONTENT_H
+
+#include <QJsonValue>
+#include <QUrl>
+
+#include "Deserializable.h"
+
+enum Membership {
+	// The user is banned.
+	BanState,
+
+	// The user has been invited.
+	InviteState,
+
+	// The user has joined.
+	JoinState,
+
+	// The user has requested to join.
+	KnockState,
+
+	// The user has left.
+	LeaveState,
+};
+
+/*
+ * The current membership state of a user in the room.
+ */
+
+class MemberEventContent : public Deserializable
+{
+public:
+	void deserialize(const QJsonValue &data) override;
+
+	inline QUrl avatarUrl() const;
+	inline QString displayName() const;
+	inline Membership membershipState() const;
+
+private:
+	QUrl avatar_url_;
+	QString display_name_;
+	Membership membership_state_;
+};
+
+inline QUrl MemberEventContent::avatarUrl() const
+{
+	return avatar_url_;
+}
+
+inline QString MemberEventContent::displayName() const
+{
+	return display_name_;
+}
+
+inline Membership MemberEventContent::membershipState() const
+{
+	return membership_state_;
+}
+
+#endif  // MEMBER_EVENT_CONTENT_H
diff --git a/include/events/NameEventContent.h b/include/events/NameEventContent.h
new file mode 100644
index 00000000..d6f60e2d
--- /dev/null
+++ b/include/events/NameEventContent.h
@@ -0,0 +1,45 @@
+/*
+ * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef NAME_EVENT_CONTENT_H
+#define NAME_EVENT_CONTENT_H
+
+#include <QJsonValue>
+
+#include "Deserializable.h"
+
+/*
+ * A human-friendly room name designed to be displayed to the end-user.
+ */
+
+class NameEventContent : public Deserializable
+{
+public:
+	void deserialize(const QJsonValue &data) override;
+
+	inline QString name() const;
+
+private:
+	QString name_;
+};
+
+inline QString NameEventContent::name() const
+{
+	return name_;
+}
+
+#endif  // NAME_EVENT_CONTENT_H
diff --git a/include/events/PowerLevelsEventContent.h b/include/events/PowerLevelsEventContent.h
new file mode 100644
index 00000000..c2a3f1fa
--- /dev/null
+++ b/include/events/PowerLevelsEventContent.h
@@ -0,0 +1,102 @@
+/*
+ * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef POWER_LEVELS_EVENT_CONTENT_H
+#define POWER_LEVELS_EVENT_CONTENT_H
+
+#include <QJsonValue>
+#include <QMap>
+
+#include "Deserializable.h"
+
+enum PowerLevels {
+	User = 0,
+	Moderator = 50,
+	Admin = 100,
+};
+
+/*
+ * Defines the power levels (privileges) of users in the room.
+ */
+
+class PowerLevelsEventContent : public Deserializable
+{
+public:
+	void deserialize(const QJsonValue &data) override;
+
+	inline int banLevel() const;
+	inline int inviteLevel() const;
+	inline int kickLevel() const;
+	inline int redactLevel() const;
+
+	inline int eventsDefaultLevel() const;
+	inline int stateDefaultLevel() const;
+	inline int usersDefaultLevel() const;
+
+	int eventLevel(QString event_type) const;
+	int userLevel(QString user_id) const;
+
+private:
+	int ban_ = PowerLevels::Moderator;
+	int invite_ = PowerLevels::Moderator;
+	int kick_ = PowerLevels::Moderator;
+	int redact_ = PowerLevels::Moderator;
+
+	int events_default_ = PowerLevels::User;
+	int state_default_ = PowerLevels::Moderator;
+	int users_default_ = PowerLevels::User;
+
+	QMap<QString, int> events_;
+	QMap<QString, int> users_;
+};
+
+inline int PowerLevelsEventContent::banLevel() const
+{
+	return ban_;
+}
+
+inline int PowerLevelsEventContent::inviteLevel() const
+{
+	return invite_;
+}
+
+inline int PowerLevelsEventContent::kickLevel() const
+{
+	return kick_;
+}
+
+inline int PowerLevelsEventContent::redactLevel() const
+{
+	return redact_;
+}
+
+inline int PowerLevelsEventContent::eventsDefaultLevel() const
+{
+	return events_default_;
+}
+
+inline int PowerLevelsEventContent::stateDefaultLevel() const
+{
+	return state_default_;
+}
+
+inline int PowerLevelsEventContent::usersDefaultLevel() const
+{
+	return users_default_;
+}
+
+#endif  // POWER_LEVELS_EVENT_CONTENT_H
diff --git a/include/events/TopicEventContent.h b/include/events/TopicEventContent.h
new file mode 100644
index 00000000..fd12de98
--- /dev/null
+++ b/include/events/TopicEventContent.h
@@ -0,0 +1,45 @@
+/*
+ * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef TOPIC_EVENT_CONTENT_H
+#define TOPIC_EVENT_CONTENT_H
+
+#include <QJsonValue>
+
+#include "Deserializable.h"
+
+/*
+ * A topic is a short message detailing what is currently being discussed in the room.
+ */
+
+class TopicEventContent : public Deserializable
+{
+public:
+	void deserialize(const QJsonValue &data) override;
+
+	inline QString topic() const;
+
+private:
+	QString topic_;
+};
+
+inline QString TopicEventContent::topic() const
+{
+	return topic_;
+}
+
+#endif  // TOPIC_EVENT_CONTENT_H
diff --git a/src/events/AliasesEventContent.cc b/src/events/AliasesEventContent.cc
new file mode 100644
index 00000000..ccc4f2c1
--- /dev/null
+++ b/src/events/AliasesEventContent.cc
@@ -0,0 +1,36 @@
+/*
+ * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <QJsonArray>
+
+#include "AliasesEventContent.h"
+
+void AliasesEventContent::deserialize(const QJsonValue &data)
+{
+	if (!data.isObject())
+		throw DeserializationException("AliasesEventContent is not a JSON object");
+
+	auto object = data.toObject();
+
+	if (object.value("aliases") == QJsonValue::Undefined)
+		throw DeserializationException("aliases key is missing");
+
+	auto aliases = object.value("aliases").toArray();
+
+	for (const auto &alias : aliases)
+		aliases_.push_back(alias.toString());
+}
diff --git a/src/events/AvatarEventContent.cc b/src/events/AvatarEventContent.cc
new file mode 100644
index 00000000..51a60a1e
--- /dev/null
+++ b/src/events/AvatarEventContent.cc
@@ -0,0 +1,36 @@
+/*
+ * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <QDebug>
+
+#include "AvatarEventContent.h"
+
+void AvatarEventContent::deserialize(const QJsonValue &data)
+{
+	if (!data.isObject())
+		throw DeserializationException("AvatarEventContent is not a JSON object");
+
+	auto object = data.toObject();
+
+	if (object.value("url") == QJsonValue::Undefined)
+		throw DeserializationException("url key is missing");
+
+	url_ = QUrl(object.value("url").toString());
+
+	if (!url_.isValid())
+		qWarning() << "Invalid avatar url" << url_;
+}
diff --git a/src/events/CanonicalAliasEventContent.cc b/src/events/CanonicalAliasEventContent.cc
new file mode 100644
index 00000000..23a32016
--- /dev/null
+++ b/src/events/CanonicalAliasEventContent.cc
@@ -0,0 +1,31 @@
+/*
+ * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "CanonicalAliasEventContent.h"
+
+void CanonicalAliasEventContent::deserialize(const QJsonValue &data)
+{
+	if (!data.isObject())
+		throw DeserializationException("CanonicalAliasEventContent is not a JSON object");
+
+	auto object = data.toObject();
+
+	if (object.value("alias") == QJsonValue::Undefined)
+		throw DeserializationException("alias key is missing");
+
+	alias_ = object.value("alias").toString();
+}
diff --git a/src/events/CreateEventContent.cc b/src/events/CreateEventContent.cc
new file mode 100644
index 00000000..e44be13e
--- /dev/null
+++ b/src/events/CreateEventContent.cc
@@ -0,0 +1,31 @@
+/*
+ * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "CreateEventContent.h"
+
+void CreateEventContent::deserialize(const QJsonValue &data)
+{
+	if (!data.isObject())
+		throw DeserializationException("CreateEventContent is not a JSON object");
+
+	auto object = data.toObject();
+
+	if (object.value("creator") == QJsonValue::Undefined)
+		throw DeserializationException("creator key is missing");
+
+	creator_ = object.value("creator").toString();
+}
diff --git a/src/events/HistoryVisibilityEventContent.cc b/src/events/HistoryVisibilityEventContent.cc
new file mode 100644
index 00000000..b9eb7ffc
--- /dev/null
+++ b/src/events/HistoryVisibilityEventContent.cc
@@ -0,0 +1,42 @@
+/*
+ * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "HistoryVisibilityEventContent.h"
+
+void HistoryVisibilityEventContent::deserialize(const QJsonValue &data)
+{
+	if (!data.isObject())
+		throw DeserializationException("HistoryVisibilityEventContent is not a JSON object");
+
+	auto object = data.toObject();
+
+	if (object.value("history_visibility") == QJsonValue::Undefined)
+		throw DeserializationException("history_visibility key is missing");
+
+	auto value = object.value("history_visibility").toString();
+
+	if (value == "invited")
+		history_visibility_ = HistoryVisibility::Invited;
+	else if (value == "joined")
+		history_visibility_ = HistoryVisibility::Joined;
+	else if (value == "shared")
+		history_visibility_ = HistoryVisibility::Shared;
+	else if (value == "world_readable")
+		history_visibility_ = HistoryVisibility::WorldReadable;
+	else
+		throw DeserializationException(QString("Unknown history_visibility value: %1").arg(value).toUtf8().constData());
+}
diff --git a/src/events/JoinRulesEventContent.cc b/src/events/JoinRulesEventContent.cc
new file mode 100644
index 00000000..ad30a1aa
--- /dev/null
+++ b/src/events/JoinRulesEventContent.cc
@@ -0,0 +1,42 @@
+/*
+ * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "JoinRulesEventContent.h"
+
+void JoinRulesEventContent::deserialize(const QJsonValue &data)
+{
+	if (!data.isObject())
+		throw DeserializationException("JoinRulesEventContent is not a JSON object");
+
+	auto object = data.toObject();
+
+	if (object.value("join_rule") == QJsonValue::Undefined)
+		throw DeserializationException("join_rule key is missing");
+
+	auto value = object.value("join_rule").toString();
+
+	if (value == "invite")
+		join_rule_ = JoinRule::Invite;
+	else if (value == "knock")
+		join_rule_ = JoinRule::Knock;
+	else if (value == "private")
+		join_rule_ = JoinRule::Private;
+	else if (value == "public")
+		join_rule_ = JoinRule::Public;
+	else
+		throw DeserializationException(QString("Unknown join_rule value: %1").arg(value).toUtf8().constData());
+}
diff --git a/src/events/MemberEventContent.cc b/src/events/MemberEventContent.cc
new file mode 100644
index 00000000..e7814e30
--- /dev/null
+++ b/src/events/MemberEventContent.cc
@@ -0,0 +1,55 @@
+/*
+ * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <QDebug>
+
+#include "MemberEventContent.h"
+
+void MemberEventContent::deserialize(const QJsonValue &data)
+{
+	if (!data.isObject())
+		throw DeserializationException("MemberEventContent is not a JSON object");
+
+	auto object = data.toObject();
+
+	if (!object.contains("membership"))
+		throw DeserializationException("membership key is missing");
+
+	auto value = object.value("membership").toString();
+
+	if (value == "ban")
+		membership_state_ = Membership::BanState;
+	else if (value == "invite")
+		membership_state_ = Membership::InviteState;
+	else if (value == "join")
+		membership_state_ = Membership::JoinState;
+	else if (value == "knock")
+		membership_state_ = Membership::KnockState;
+	else if (value == "leave")
+		membership_state_ = Membership::LeaveState;
+	else
+		throw DeserializationException(QString("Unknown membership value: %1").arg(value).toUtf8().constData());
+
+	if (object.contains("avatar_url"))
+		avatar_url_ = QUrl(object.value("avatar_url").toString());
+
+	if (!avatar_url_.toString().isEmpty() && !avatar_url_.isValid())
+		qWarning() << "Invalid avatar url" << avatar_url_;
+
+	if (object.contains("displayname"))
+		display_name_ = object.value("displayname").toString();
+}
diff --git a/src/events/NameEventContent.cc b/src/events/NameEventContent.cc
new file mode 100644
index 00000000..09b655a7
--- /dev/null
+++ b/src/events/NameEventContent.cc
@@ -0,0 +1,31 @@
+/*
+ * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "NameEventContent.h"
+
+void NameEventContent::deserialize(const QJsonValue &data)
+{
+	if (!data.isObject())
+		throw DeserializationException("NameEventContent is not a JSON object");
+
+	auto object = data.toObject();
+
+	if (object.value("name") == QJsonValue::Undefined)
+		throw DeserializationException("name key is missing");
+
+	name_ = object.value("name").toString();
+}
diff --git a/src/events/PowerLevelsEventContent.cc b/src/events/PowerLevelsEventContent.cc
new file mode 100644
index 00000000..a6e5846a
--- /dev/null
+++ b/src/events/PowerLevelsEventContent.cc
@@ -0,0 +1,80 @@
+/*
+ * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <QJsonObject>
+
+#include "Deserializable.h"
+#include "PowerLevelsEventContent.h"
+
+void PowerLevelsEventContent::deserialize(const QJsonValue &data)
+{
+	if (!data.isObject())
+		throw DeserializationException("PowerLevelsEventContent is not a JSON object");
+
+	auto object = data.toObject();
+
+	if (object.value("ban") != QJsonValue::Undefined)
+		ban_ = object.value("ban").toInt();
+
+	if (object.value("invite") != QJsonValue::Undefined)
+		invite_ = object.value("invite").toInt();
+
+	if (object.value("kick") != QJsonValue::Undefined)
+		kick_ = object.value("kick").toInt();
+
+	if (object.value("redact") != QJsonValue::Undefined)
+		redact_ = object.value("redact").toInt();
+
+	if (object.value("events_default") != QJsonValue::Undefined)
+		events_default_ = object.value("events_default").toInt();
+
+	if (object.value("state_default") != QJsonValue::Undefined)
+		state_default_ = object.value("state_default").toInt();
+
+	if (object.value("users_default") != QJsonValue::Undefined)
+		users_default_ = object.value("users_default").toInt();
+
+	if (object.value("users").isObject()) {
+		auto users = object.value("users").toObject();
+
+		for (auto it = users.constBegin(); it != users.constEnd(); it++)
+			users_.insert(it.key(), it.value().toInt());
+	}
+
+	if (object.value("events").isObject()) {
+		auto events = object.value("events").toObject();
+
+		for (auto it = events.constBegin(); it != events.constEnd(); it++)
+			events_.insert(it.key(), it.value().toInt());
+	}
+}
+
+int PowerLevelsEventContent::eventLevel(QString event_type) const
+{
+	if (events_.contains(event_type))
+		return events_[event_type];
+
+	return events_default_;
+}
+
+int PowerLevelsEventContent::userLevel(QString userid) const
+{
+	if (users_.contains(userid))
+		return users_[userid];
+
+	return users_default_;
+}
diff --git a/src/events/TopicEventContent.cc b/src/events/TopicEventContent.cc
new file mode 100644
index 00000000..1abda151
--- /dev/null
+++ b/src/events/TopicEventContent.cc
@@ -0,0 +1,31 @@
+/*
+ * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "TopicEventContent.h"
+
+void TopicEventContent::deserialize(const QJsonValue &data)
+{
+	if (!data.isObject())
+		throw DeserializationException("TopicEventContent is not a JSON object");
+
+	auto object = data.toObject();
+
+	if (object.value("topic") == QJsonValue::Undefined)
+		throw DeserializationException("topic key is missing");
+
+	topic_ = object.value("topic").toString();
+}
diff --git a/tests/events.cc b/tests/events.cc
new file mode 100644
index 00000000..79319f38
--- /dev/null
+++ b/tests/events.cc
@@ -0,0 +1,499 @@
+#include <gtest/gtest.h>
+#include <QJsonArray>
+
+#include "AliasesEventContent.h"
+#include "AvatarEventContent.h"
+#include "CanonicalAliasEventContent.h"
+#include "CreateEventContent.h"
+#include "HistoryVisibilityEventContent.h"
+#include "JoinRulesEventContent.h"
+#include "MemberEventContent.h"
+#include "NameEventContent.h"
+#include "PowerLevelsEventContent.h"
+#include "TopicEventContent.h"
+
+TEST(AliasesEventContent, Deserialization)
+{
+	auto data = QJsonObject{
+		{"aliases", QJsonArray{"#test:matrix.org", "#test2:matrix.org"}}};
+
+	AliasesEventContent content;
+	content.deserialize(data);
+
+	EXPECT_EQ(content.aliases().size(), 2);
+}
+
+TEST(AliasesEventContent, NotAnObject)
+{
+	auto data = QJsonArray{"#test:matrix.org", "#test2:matrix.org"};
+
+	AliasesEventContent content;
+	ASSERT_THROW(content.deserialize(data), DeserializationException);
+}
+
+TEST(AliasesEventContent, MissingKey)
+{
+	auto data = QJsonObject{
+		{"key", QJsonArray{"#test:matrix.org", "#test2:matrix.org"}}};
+
+	AliasesEventContent content;
+	ASSERT_THROW(content.deserialize(data), DeserializationException);
+
+	try {
+		content.deserialize(data);
+	} catch (const DeserializationException &e) {
+		ASSERT_STREQ("aliases key is missing", e.what());
+	}
+}
+
+TEST(AvatarEventContent, Deserialization)
+{
+	auto data = QJsonObject{{"url", "https://matrix.org/avatar.png"}};
+
+	AvatarEventContent content;
+	content.deserialize(data);
+
+	EXPECT_EQ(content.url().toString(), "https://matrix.org/avatar.png");
+}
+
+TEST(AvatarEventContent, NotAnObject)
+{
+	auto data = QJsonArray{"key", "url"};
+
+	AvatarEventContent content;
+	ASSERT_THROW(content.deserialize(data), DeserializationException);
+}
+
+TEST(AvatarEventContent, MissingKey)
+{
+	auto data = QJsonObject{{"key", "https://matrix.org"}};
+
+	AvatarEventContent content;
+	ASSERT_THROW(content.deserialize(data), DeserializationException);
+
+	try {
+		content.deserialize(data);
+	} catch (const DeserializationException &e) {
+		ASSERT_STREQ("url key is missing", e.what());
+	}
+}
+
+TEST(CreateEventContent, Deserialization)
+{
+	auto data = QJsonObject{{"creator", "@alice:matrix.org"}};
+
+	CreateEventContent content;
+	content.deserialize(data);
+
+	EXPECT_EQ(content.creator(), "@alice:matrix.org");
+}
+
+TEST(CreateEventContent, NotAnObject)
+{
+	auto data = QJsonArray{"creator", "alice"};
+
+	CreateEventContent content;
+
+	ASSERT_THROW(content.deserialize(data), DeserializationException);
+}
+
+TEST(CreateEventContent, MissingKey)
+{
+	auto data = QJsonObject{{"key", "@alice:matrix.org"}};
+
+	CreateEventContent content;
+	ASSERT_THROW(content.deserialize(data), DeserializationException);
+
+	try {
+		content.deserialize(data);
+	} catch (const DeserializationException &e) {
+		ASSERT_STREQ("creator key is missing", e.what());
+	}
+}
+
+TEST(HistoryVisibilityEventContent, Deserialization)
+{
+	auto data = QJsonObject{{"history_visibility", "invited"}};
+
+	HistoryVisibilityEventContent content;
+	content.deserialize(data);
+	EXPECT_EQ(content.historyVisibility(), HistoryVisibility::Invited);
+
+	data = QJsonObject{{"history_visibility", "joined"}};
+
+	content.deserialize(data);
+	EXPECT_EQ(content.historyVisibility(), HistoryVisibility::Joined);
+
+	data = QJsonObject{{"history_visibility", "shared"}};
+
+	content.deserialize(data);
+	EXPECT_EQ(content.historyVisibility(), HistoryVisibility::Shared);
+
+	data = QJsonObject{{"history_visibility", "world_readable"}};
+
+	content.deserialize(data);
+	EXPECT_EQ(content.historyVisibility(), HistoryVisibility::WorldReadable);
+}
+
+TEST(HistoryVisibilityEventContent, NotAnObject)
+{
+	auto data = QJsonArray{"history_visibility", "alice"};
+
+	HistoryVisibilityEventContent content;
+
+	ASSERT_THROW(content.deserialize(data), DeserializationException);
+}
+
+TEST(HistoryVisibilityEventContent, InvalidHistoryVisibility)
+{
+	auto data = QJsonObject{{"history_visibility", "wrong"}};
+
+	HistoryVisibilityEventContent content;
+	ASSERT_THROW(content.deserialize(data), DeserializationException);
+
+	try {
+		content.deserialize(data);
+	} catch (const DeserializationException &e) {
+		ASSERT_STREQ("Unknown history_visibility value: wrong", e.what());
+	}
+}
+
+TEST(HistoryVisibilityEventContent, MissingKey)
+{
+	auto data = QJsonObject{{"key", "joined"}};
+
+	HistoryVisibilityEventContent content;
+	ASSERT_THROW(content.deserialize(data), DeserializationException);
+
+	try {
+		content.deserialize(data);
+	} catch (const DeserializationException &e) {
+		ASSERT_STREQ("history_visibility key is missing", e.what());
+	}
+}
+
+TEST(JoinRulesEventContent, Deserialization)
+{
+	auto data = QJsonObject{{"join_rule", "invite"}};
+
+	JoinRulesEventContent content;
+	content.deserialize(data);
+	EXPECT_EQ(content.joinRule(), JoinRule::Invite);
+
+	data = QJsonObject{{"join_rule", "knock"}};
+
+	content.deserialize(data);
+	EXPECT_EQ(content.joinRule(), JoinRule::Knock);
+
+	data = QJsonObject{{"join_rule", "private"}};
+
+	content.deserialize(data);
+	EXPECT_EQ(content.joinRule(), JoinRule::Private);
+
+	data = QJsonObject{{"join_rule", "public"}};
+
+	content.deserialize(data);
+	EXPECT_EQ(content.joinRule(), JoinRule::Public);
+}
+
+TEST(JoinRulesEventContent, NotAnObject)
+{
+	auto data = QJsonArray{"rule", "alice"};
+
+	JoinRulesEventContent content;
+
+	ASSERT_THROW(content.deserialize(data), DeserializationException);
+}
+
+TEST(JoinRulesEventContent, InvalidHistoryVisibility)
+{
+	auto data = QJsonObject{{"join_rule", "wrong"}};
+
+	JoinRulesEventContent content;
+	ASSERT_THROW(content.deserialize(data), DeserializationException);
+
+	try {
+		content.deserialize(data);
+	} catch (const DeserializationException &e) {
+		ASSERT_STREQ("Unknown join_rule value: wrong", e.what());
+	}
+}
+
+TEST(JoinRulesEventContent, MissingKey)
+{
+	auto data = QJsonObject{{"key", "invite"}};
+
+	JoinRulesEventContent content;
+	ASSERT_THROW(content.deserialize(data), DeserializationException);
+
+	try {
+		content.deserialize(data);
+	} catch (const DeserializationException &e) {
+		ASSERT_STREQ("join_rule key is missing", e.what());
+	}
+}
+
+TEST(CanonicalAliasEventContent, Deserialization)
+{
+	auto data = QJsonObject{{"alias", "Room Alias"}};
+
+	CanonicalAliasEventContent content;
+	content.deserialize(data);
+
+	EXPECT_EQ(content.alias(), "Room Alias");
+}
+
+TEST(CanonicalAliasEventContent, NotAnObject)
+{
+	auto data = QJsonArray{"alias", "Room Alias"};
+
+	CanonicalAliasEventContent content;
+
+	ASSERT_THROW(content.deserialize(data), DeserializationException);
+}
+
+TEST(CanonicalAliasEventContent, MissingKey)
+{
+	auto data = QJsonObject{{"key", "alias"}};
+
+	CanonicalAliasEventContent content;
+	ASSERT_THROW(content.deserialize(data), DeserializationException);
+
+	try {
+		content.deserialize(data);
+	} catch (const DeserializationException &e) {
+		ASSERT_STREQ("alias key is missing", e.what());
+	}
+}
+
+TEST(MemberEventContent, Deserialization)
+{
+	MemberEventContent content;
+
+	auto data = QJsonObject{{"membership", "join"}};
+
+	content.deserialize(data);
+	EXPECT_EQ(content.membershipState(), Membership::JoinState);
+
+	data = QJsonObject{{"membership", "invite"}, {"displayname", "Username"}};
+
+	content.deserialize(data);
+	EXPECT_EQ(content.membershipState(), Membership::InviteState);
+	EXPECT_EQ(content.displayName(), "Username");
+
+	data = QJsonObject{{"membership", "leave"}, {"avatar_url", "https://matrix.org"}};
+
+	content.deserialize(data);
+	EXPECT_EQ(content.membershipState(), Membership::LeaveState);
+	EXPECT_EQ(content.avatarUrl().toString(), "https://matrix.org");
+
+	data = QJsonObject{{"membership", "ban"}};
+
+	content.deserialize(data);
+	EXPECT_EQ(content.membershipState(), Membership::BanState);
+
+	data = QJsonObject{{"membership", "knock"}};
+
+	content.deserialize(data);
+	EXPECT_EQ(content.membershipState(), Membership::KnockState);
+}
+
+TEST(MemberEventContent, InvalidMembership)
+{
+	auto data = QJsonObject{{"membership", "wrong"}};
+
+	MemberEventContent content;
+	ASSERT_THROW(content.deserialize(data), DeserializationException);
+
+	try {
+		content.deserialize(data);
+	} catch (const DeserializationException &e) {
+		ASSERT_STREQ("Unknown membership value: wrong", e.what());
+	}
+}
+
+TEST(MemberEventContent, NotAnObject)
+{
+	auto data = QJsonArray{"name", "join"};
+
+	MemberEventContent content;
+
+	ASSERT_THROW(content.deserialize(data), DeserializationException);
+}
+
+TEST(MemberEventContent, MissingName)
+{
+	auto data = QJsonObject{{"key", "random"}};
+
+	MemberEventContent content;
+	ASSERT_THROW(content.deserialize(data), DeserializationException);
+
+	try {
+		content.deserialize(data);
+	} catch (const DeserializationException &e) {
+		ASSERT_STREQ("membership key is missing", e.what());
+	}
+}
+
+TEST(NameEventContent, Deserialization)
+{
+	auto data = QJsonObject{{"name", "Room Name"}};
+
+	NameEventContent content;
+	content.deserialize(data);
+
+	EXPECT_EQ(content.name(), "Room Name");
+}
+
+TEST(NameEventContent, NotAnObject)
+{
+	auto data = QJsonArray{"name", "Room Name"};
+
+	NameEventContent content;
+
+	ASSERT_THROW(content.deserialize(data), DeserializationException);
+}
+
+TEST(NameEventContent, MissingName)
+{
+	auto data = QJsonObject{{"key", "Room Name"}};
+
+	NameEventContent content;
+	ASSERT_THROW(content.deserialize(data), DeserializationException);
+
+	try {
+		content.deserialize(data);
+	} catch (const DeserializationException &e) {
+		ASSERT_STREQ("name key is missing", e.what());
+	}
+}
+
+TEST(PowerLevelsEventContent, DefaultValues)
+{
+	PowerLevelsEventContent power_levels;
+
+	EXPECT_EQ(power_levels.banLevel(), PowerLevels::Moderator);
+	EXPECT_EQ(power_levels.inviteLevel(), PowerLevels::Moderator);
+	EXPECT_EQ(power_levels.kickLevel(), PowerLevels::Moderator);
+	EXPECT_EQ(power_levels.redactLevel(), PowerLevels::Moderator);
+
+	EXPECT_EQ(power_levels.eventsDefaultLevel(), PowerLevels::User);
+	EXPECT_EQ(power_levels.usersDefaultLevel(), PowerLevels::User);
+	EXPECT_EQ(power_levels.stateDefaultLevel(), PowerLevels::Moderator);
+
+	// Default levels.
+	EXPECT_EQ(power_levels.userLevel("@joe:matrix.org"), PowerLevels::User);
+	EXPECT_EQ(power_levels.eventLevel("m.room.message"), PowerLevels::User);
+}
+
+TEST(PowerLevelsEventContent, FullDeserialization)
+{
+	auto data = QJsonObject{
+		{"ban", 1},
+		{"invite", 2},
+		{"kick", 3},
+		{"redact", 4},
+
+		{"events_default", 5},
+		{"state_default", 6},
+		{"users_default", 7},
+
+		{"events", QJsonObject{{"m.message.text", 8}, {"m.message.image", 9}}},
+		{"users", QJsonObject{{"@alice:matrix.org", 10}, {"@bob:matrix.org", 11}}},
+	};
+
+	PowerLevelsEventContent power_levels;
+	power_levels.deserialize(data);
+
+	EXPECT_EQ(power_levels.banLevel(), 1);
+	EXPECT_EQ(power_levels.inviteLevel(), 2);
+	EXPECT_EQ(power_levels.kickLevel(), 3);
+	EXPECT_EQ(power_levels.redactLevel(), 4);
+
+	EXPECT_EQ(power_levels.eventsDefaultLevel(), 5);
+	EXPECT_EQ(power_levels.stateDefaultLevel(), 6);
+	EXPECT_EQ(power_levels.usersDefaultLevel(), 7);
+
+	EXPECT_EQ(power_levels.userLevel("@alice:matrix.org"), 10);
+	EXPECT_EQ(power_levels.userLevel("@bob:matrix.org"), 11);
+	EXPECT_EQ(power_levels.userLevel("@carl:matrix.org"), 7);
+
+	EXPECT_EQ(power_levels.eventLevel("m.message.text"), 8);
+	EXPECT_EQ(power_levels.eventLevel("m.message.image"), 9);
+	EXPECT_EQ(power_levels.eventLevel("m.message.gif"), 5);
+}
+
+TEST(PowerLevelsEventContent, PartialDeserialization)
+{
+	auto data = QJsonObject{
+		{"ban", 1},
+		{"invite", 2},
+
+		{"events_default", 5},
+		{"users_default", 7},
+
+		{"users", QJsonObject{{"@alice:matrix.org", 10}, {"@bob:matrix.org", 11}}},
+	};
+
+	PowerLevelsEventContent power_levels;
+	power_levels.deserialize(data);
+
+	EXPECT_EQ(power_levels.banLevel(), 1);
+	EXPECT_EQ(power_levels.inviteLevel(), 2);
+	EXPECT_EQ(power_levels.kickLevel(), PowerLevels::Moderator);
+	EXPECT_EQ(power_levels.redactLevel(), PowerLevels::Moderator);
+
+	EXPECT_EQ(power_levels.eventsDefaultLevel(), 5);
+	EXPECT_EQ(power_levels.stateDefaultLevel(), PowerLevels::Moderator);
+	EXPECT_EQ(power_levels.usersDefaultLevel(), 7);
+
+	EXPECT_EQ(power_levels.userLevel("@alice:matrix.org"), 10);
+	EXPECT_EQ(power_levels.userLevel("@bob:matrix.org"), 11);
+	EXPECT_EQ(power_levels.userLevel("@carl:matrix.org"), 7);
+
+	EXPECT_EQ(power_levels.eventLevel("m.message.text"), 5);
+	EXPECT_EQ(power_levels.eventLevel("m.message.image"), 5);
+	EXPECT_EQ(power_levels.eventLevel("m.message.gif"), 5);
+}
+
+TEST(PowerLevelsEventContent, NotAnObject)
+{
+	auto data = QJsonArray{"test", "test2"};
+
+	PowerLevelsEventContent power_levels;
+
+	ASSERT_THROW(power_levels.deserialize(data), DeserializationException);
+}
+
+TEST(TopicEventContent, Deserialization)
+{
+	auto data = QJsonObject{{"topic", "Room Topic"}};
+
+	TopicEventContent content;
+	content.deserialize(data);
+
+	EXPECT_EQ(content.topic(), "Room Topic");
+}
+
+TEST(TopicEventContent, NotAnObject)
+{
+	auto data = QJsonArray{"topic", "Room Topic"};
+
+	TopicEventContent content;
+
+	ASSERT_THROW(content.deserialize(data), DeserializationException);
+}
+
+TEST(TopicEventContent, MissingName)
+{
+	auto data = QJsonObject{{"key", "Room Name"}};
+
+	TopicEventContent content;
+	ASSERT_THROW(content.deserialize(data), DeserializationException);
+
+	try {
+		content.deserialize(data);
+	} catch (const DeserializationException &e) {
+		ASSERT_STREQ("topic key is missing", e.what());
+	}
+}