diff --git a/.gitignore b/.gitignore index 4a5ebb2..f9259fb 100644 --- a/.gitignore +++ b/.gitignore @@ -82,3 +82,4 @@ Makefile.in /src/matrix-event-call-hangup.c /src/matrix-event-call-base.c /src/matrix-glib-0.0.pc +/src/matrix-message-base.c diff --git a/src/Makefile.am b/src/Makefile.am index 8426a0c..1508c15 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -50,6 +50,7 @@ libmatrix_glib_0_0_la_VALA_SOURCES = \ matrix-event-call-candidates.vala \ matrix-event-call-answer.vala \ matrix-event-call-hangup.vala \ + matrix-message-base.vala \ $(NULL) AM_CPPFLAGS += \ diff --git a/src/matrix-event-room-message.vala b/src/matrix-event-room-message.vala index b9055b8..abbd8e9 100644 --- a/src/matrix-event-room-message.vala +++ b/src/matrix-event-room-message.vala @@ -16,38 +16,75 @@ * . */ +/** + * Class to hold an m.room.message event + * + * This event is used when sending messages in a room. Messages are + * not limited to be text. The `msgtype` key outlines the type of + * message, e.g. text, audio, image, video, etc. The `body` key is + * text and MUST be used with every kind of `msgtype` as a fallback + * mechanism for when a client cannot render a message. This allows + * clients to display *something* even if it is just plain text. + */ public class Matrix.Event.RoomMessage : Matrix.Event.Room { - public string msg_type { get; set; } - public string body { get; set; } + /** + * The message as a Matrix.Message object. + */ + public Matrix.Message.Base? message { get; set; default = null; } + + /** + * The message as a JSON object. This gets set by + * Matrix.Message.Base.new_from_json if no handler is installed + * for the given message type. + */ + public Json.Node? fallback_content { + get { + return _fallback_content; + } + } + + private Json.Node? _fallback_content; protected override void from_json(Json.Node json_data) throws Matrix.Error { - var root = json_data.get_object(); + var content_node = json_data.get_object().get_member("content"); - if (root.get_member("content") == null) { - throw new Matrix.Error.INCOMPLETE( - "Message event without a content!"); + try { + _message = Matrix.Message.Base.new_from_json(content_node); + + // We don't want to fail on unknown message types, so + // let's save the JSON content instead. Silent + // (ie. without exception) null is only returned if there + // is no handler class installed + if (_message == null) { + _fallback_content = content_node; + } else { + _fallback_content = null; + } + } catch (GLib.Error e) { + throw new Matrix.Error.INVALID_TYPE( + "Error during message initialization:%s", + e.message); } - var content_root = root.get_member("content").get_object(); - Json.Node? node; - - if ((node = content_root.get_member("msgtype")) == null) { - throw new Matrix.Error.INCOMPLETE( - "Message event without a message type!"); - } - - _msg_type = node.get_string(); - - if ((node = content_root.get_member("body")) == null) { - throw new Matrix.Error.INCOMPLETE( - "Message event without a body!"); - } - - _body = node.get_string(); - base.from_json(json_data); } + + protected override void + to_json(Json.Node json_data) + throws Matrix.Error + { + if (_message == null) { + throw new Matrix.Error.INCOMPLETE( + "Won't generate a m.room.message event without content"); + } + + var root = json_data.get_object(); + + root.set_member("content", _message.json); + + base.to_json(json_data); + } } diff --git a/src/matrix-message-base.vala b/src/matrix-message-base.vala new file mode 100644 index 0000000..7e58d72 --- /dev/null +++ b/src/matrix-message-base.vala @@ -0,0 +1,220 @@ +/* + * This file is part of matrix-glib-sdk + * + * matrix-glib-sdk is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * matrix-glib-sdk 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with matrix-glib-sdk. If not, see + * . + */ + +/** + * Base class for message handlers. + * + * Message handler base class. + */ +public abstract class Matrix.Message.Base : Object, Initable { + /** + * The message type. + */ + public string? message_type { get; set; default = null; } + + /** + * The body of the message. + */ + public string? body { get; set; default = null; } + + public Json.Node? json { + get { + _json = new Json.Node(Json.NodeType.OBJECT); + _json.set_object(new Json.Object()); + + try { + to_json(_json); + } catch (Matrix.Error e) { + return null; + } + + return _json; + } + + construct { + if (value != null) { + try { + initialize_from_json(value); + } catch (Matrix.Error e) {} + } + } + } + + private bool _inited = false; + private Error? _construct_error = null; + private Json.Node? _json = null; + + public bool + init(Cancellable? cancellable = null) + throws GLib.Error + { + if (cancellable != null) { + throw new Matrix.Error.UNSUPPORTED( + "Cancellable initialization not supported"); + } + + if (_construct_error != null) { + throw _construct_error; + } + + _inited = true; + + return true; + } + + private void + initialize_from_json(Json.Node json_data) + throws Matrix.Error + { + if (json_data.get_node_type() != Json.NodeType.OBJECT) { + throw new Matrix.Error.INVALID_FORMAT( + "The message is not valid"); + } + + from_json(json_data); + } + + public static Matrix.Message.Base? + new_from_json(Json.Node json_data) + throws Matrix.Error, GLib.Error + { + var root = json_data.get_object(); + Json.Node? node; + + if ((node = root.get_member("msgtype")) == null) { + throw new Matrix.Error.INCOMPLETE( + "Can not process a message without msgtype"); + } + + Type? msg_gtype; + + if ((msg_gtype = get_handler(node.get_string())) == null) { + return null; + } + + var ret = (Base)Object.new(msg_gtype, + json : json_data); + + ret.init(); + + return ret; + } + + /** + * Subclasses should override this method to initialize themselves + * from JSON data while chaining up to allow parent classes to do + * the same. + */ + public virtual void + from_json(Json.Node json_data) + throws Matrix.Error + { + var root = json_data.get_object(); + Json.Node? node; + + if ((node = root.get_member("msgtype")) != null) { + _message_type = node.get_string(); + } else if (Config.DEBUG) { + warning("msgtype is not present in a message"); + } + } + + /** + * Subclasses should override this method to export their data to + * JSON, while chaining up to allow parent classes to do the same. + */ + public virtual void + to_json(Json.Node json_data) + throws Matrix.Error + { + var root = json_data.get_object(); + + if (_message_type == null) { + throw new Matrix.Error.INCOMPLETE( + "Won't generate a message with an empty msgtype"); + } + + root.set_string_member("msgtype", _message_type); + } +} + +namespace Matrix.Message { + +private HashTable? type_handlers = null; + +/** + * Get the {@link GLib.Type} of the class that is registered to handle + * messages with type @param message_type. + * + * @param message_type the message type to look up + * @return a {@link GLib.Type} or `null` if no handler is registered + */ +public static GLib.Type? +get_handler(string message_type) +{ + unowned GLib.TypeClass? klass = null; + + if ((type_handlers != null) + && ((klass = type_handlers.get(message_type)) != null)) { + return klass.get_type(); + } + + if (Config.DEBUG) { + warning("No registered type for message %s", message_type); + } + + return null; +} + +/** + * Register @param message_type to be handled by the + * type @param message_gtype. + * + * @param message_type the type of the message + * @param message_gtype the {@link GLib.Type} of the event's handler + */ +public static void +register_type(string message_type, GLib.Type message_gtype) + throws Matrix.Error +{ + if (!message_gtype.is_a(typeof(Matrix.Message.Base))) { + throw new Matrix.Error.INVALID_TYPE( + "Invalid message handler type"); + } + + if (type_handlers == null) { + type_handlers = new HashTable( + str_hash, str_equal); + } + + type_handlers.replace(message_type, message_gtype.class_ref()); +} + +/** + * Unregister @param message_type. + * + * @param message_type the message type to remove + */ +public void +unregister_type(string message_type) +{ + if (type_handlers != null) { + type_handlers.remove(message_type); + } +} +} diff --git a/src/namespace-info.vala.in b/src/namespace-info.vala.in index a1eacfa..f90d8b5 100644 --- a/src/namespace-info.vala.in +++ b/src/namespace-info.vala.in @@ -20,4 +20,7 @@ namespace Matrix { [CCode (gir_namespace = "Event", gir_version = "@MATRIX_GLIB_API_VERSION@")] namespace Event {} + + [CCode (gir_namespace = "Message", gir_version = "@MATRIX_GLIB_API_VERSION@")] + namespace Message {} } diff --git a/src/test-client.c b/src/test-client.c index a8e92c8..1c92bfe 100644 --- a/src/test-client.c +++ b/src/test-client.c @@ -75,10 +75,19 @@ cb_room_message_event(MatrixClient *client, MatrixEventBase *event, gpointer user_data) { - g_printf("Message from %s: %s\n", - matrix_event_room_get_sender(MATRIX_EVENT_ROOM(event)), - matrix_event_room_message_get_body( - MATRIX_EVENT_ROOM_MESSAGE(event))); + MatrixMessageBase *message; + + if ((message = matrix_event_room_message_get_message( + MATRIX_EVENT_ROOM_MESSAGE(event))) != NULL) { + g_printf("Message from %s in %s: %s\n", + matrix_event_room_get_sender(MATRIX_EVENT_ROOM(event)), + matrix_event_room_get_room_id(MATRIX_EVENT_ROOM(event)), + matrix_message_base_get_body(message)); + } else { + g_printf("Unknown message received from %s in %s\n", + matrix_event_room_get_sender(MATRIX_EVENT_ROOM(event)), + matrix_event_room_get_room_id(MATRIX_EVENT_ROOM(event))); + } } int