Skip to content

Commit

Permalink
Merge pull request #83 from xmtp/daniel-codecs
Browse files Browse the repository at this point in the history
feat: implement codecs for attachment, reaction, and reply
  • Loading branch information
dmccartney authored Aug 23, 2023
2 parents f51dc54 + 7ac8862 commit 762b83f
Show file tree
Hide file tree
Showing 11 changed files with 402 additions and 14 deletions.
17 changes: 14 additions & 3 deletions lib/src/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import 'auth.dart';
import 'common/api.dart';
import 'common/signature.dart';
import 'contact.dart';
import 'content/attachment_codec.dart';
import 'content/codec.dart';
import 'content/codec_registry.dart';
import 'content/composite_codec.dart';
import 'content/reaction_codec.dart';
import 'content/reply_codec.dart';
import 'content/text_codec.dart';
import 'content/decoded.dart';
import 'conversation/conversation.dart';
Expand Down Expand Up @@ -117,9 +120,17 @@ class Client implements Codec<DecodedContent> {
var auth = AuthManager(address, api);
var contacts = ContactManager(api);
var codecs = CodecRegistry();
codecs.registerCodec(TextCodec());
codecs.registerCodec(CompositeCodec(codecs));
for (var codec in customCodecs) {
var commonCodecs = <Codec>[
TextCodec(),
CompositeCodec(),
ReplyCodec(),
ReactionCodec(),
AttachmentCodec(),
];
for (var codec in commonCodecs..addAll(customCodecs)) {
if (codec is NestedContentCodec) {
codec.setRegistry(codecs);
}
codecs.registerCodec(codec);
}
var v1 = ConversationManagerV1(address, api, auth, codecs, contacts);
Expand Down
47 changes: 47 additions & 0 deletions lib/src/content/attachment_codec.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import 'package:flutter/foundation.dart';
import 'package:xmtp_proto/xmtp_proto.dart' as xmtp;

import 'codec.dart';

final contentTypeAttachment = xmtp.ContentTypeId(
authorityId: "xmtp.org",
typeId: "attachment",
versionMajor: 1,
versionMinor: 0,
);

/// This is a file attached as the message content.
///
/// Note: this is limited to small files that can fit in the message payload.
/// For larger files, use [RemoteAttachment].
class Attachment {
final String filename;
final String mimeType;
final List<int> data;

Attachment(this.filename, this.mimeType, this.data);
}

/// This is a [Codec] that encodes a file attached as the message content.
class AttachmentCodec extends Codec<Attachment> {
@override
xmtp.ContentTypeId get contentType => contentTypeAttachment;

@override
Future<Attachment> decode(xmtp.EncodedContent encoded) async =>
Attachment(
encoded.parameters["filename"] ?? "",
encoded.parameters["mimeType"] ?? "",
encoded.content,
);

@override
Future<xmtp.EncodedContent> encode(Attachment decoded) async => xmtp.EncodedContent(
type: contentTypeAttachment,
parameters: {
"filename": decoded.filename,
"mimeType": decoded.mimeType,
},
content: decoded.data,
);
}
18 changes: 18 additions & 0 deletions lib/src/content/codec.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import 'package:flutter/foundation.dart';
import 'package:xmtp_proto/xmtp_proto.dart' as xmtp;

import 'decoded.dart';

/// This defines the interface for a content codec of a particular type [T].
/// It is responsible for knowing how to [encode] the content [T].
/// And it is responsible for knowing how to [decode] the [EncodedContent].
Expand All @@ -15,3 +18,18 @@ abstract class Codec<T extends Object> {
/// This is called to encode the content
Future<xmtp.EncodedContent> encode(T decoded);
}

/// This is a [Codec] that can handle nested generic content.
///
/// These codecs need the full [CodecRegistry] to decode and encode some nested
/// content as part of implementing their own [encode] and [decode].
///
/// See e.g. [CompositeCodec] and [ReplyCodec].
abstract class NestedContentCodec<T extends Object> implements Codec<T> {
@protected
late Codec<DecodedContent> registry;

void setRegistry(Codec<DecodedContent> registry_) {
registry = registry_;
}
}
15 changes: 6 additions & 9 deletions lib/src/content/composite_codec.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,13 @@ final contentTypeComposite = xmtp.ContentTypeId(

/// This is a [Codec] that can handle a composite of other content types.
///
/// It is initialized with a [CodecRegistry] that it uses to encode and decode
/// the parts of the composite.
/// It is a [NestedContentCodec] so it has a [CodecRegistry] it uses to encode
/// and decode the parts of the composite.
///
/// Both [encode] and [decode] are implemented by recursing through the
/// composite and serializing/deserializing to the [xmtp.Composite] as
/// the [content] bytes in a [xmtp.EncodedContent] of type [contentTypeComposite].
class CompositeCodec extends Codec<DecodedComposite> {
final Codec<DecodedContent> _registry;

CompositeCodec(this._registry);
class CompositeCodec extends NestedContentCodec<DecodedComposite> {

@override
xmtp.ContentTypeId get contentType => contentTypeComposite;
Expand All @@ -47,7 +44,7 @@ class CompositeCodec extends Codec<DecodedComposite> {
var results = <DecodedComposite>[];
for (var part in composite.parts) {
if (part.hasPart()) {
var decoded = await _registry.decode(part.part);
var decoded = await registry.decode(part.part);
results.add(DecodedComposite.ofContent(decoded));
} else {
var decoded = await _decode(part.composite);
Expand All @@ -65,13 +62,13 @@ class CompositeCodec extends Codec<DecodedComposite> {
/// This recursively encodes the parts of the composite.
Future<xmtp.Composite> _encode(DecodedComposite decoded) async {
if (decoded.hasContent) {
var encoded = await _registry.encode(decoded.content!);
var encoded = await registry.encode(decoded.content!);
return xmtp.Composite()..parts.add(xmtp.Composite_Part(part: encoded));
}
var result = xmtp.Composite();
for (var part in decoded.parts) {
if (part.hasContent) {
var encoded = await _registry.encode(part.content!);
var encoded = await registry.encode(part.content!);
result.parts.add(xmtp.Composite_Part(part: encoded));
} else {
result.parts.add(xmtp.Composite_Part(composite: await _encode(part)));
Expand Down
69 changes: 69 additions & 0 deletions lib/src/content/reaction_codec.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import 'dart:convert';

import 'package:xmtp_proto/xmtp_proto.dart' as xmtp;

import 'codec.dart';

final contentTypeReaction = xmtp.ContentTypeId(
authorityId: "xmtp.org",
typeId: "reaction",
versionMajor: 1,
versionMinor: 0,
);

enum ReactionAction {
added,
removed,
}

enum ReactionSchema {
unicode,
shortcode,
custom,
}

/// This is a reaction to another [reference] message.
class Reaction {
final String reference;
final ReactionAction action;
final ReactionSchema schema;
final String content;

Reaction(this.reference, this.action, this.schema, this.content);

String toJson() => json.encode({
"reference": reference,
"action": action.toString().split(".").last,
"schema": schema.toString().split(".").last,
"content": content,
});

static Reaction fromJson(String json) {
var v = jsonDecode(json);
return Reaction(
v["reference"],
ReactionAction.values
.firstWhere((e) => e.toString().split(".").last == v["action"]),
ReactionSchema.values
.firstWhere((e) => e.toString().split(".").last == v["schema"]),
v["content"],
);
}
}

/// This is a [Codec] that encodes a reaction to another message.
class ReactionCodec extends Codec<Reaction> {
@override
xmtp.ContentTypeId get contentType => contentTypeReaction;

@override
Future<Reaction> decode(xmtp.EncodedContent encoded) async =>
Reaction.fromJson(utf8.decode(encoded.content));

@override
Future<xmtp.EncodedContent> encode(Reaction decoded) async =>
xmtp.EncodedContent(
type: contentTypeReaction,
content: utf8.encode(decoded.toJson()),
);
}
50 changes: 50 additions & 0 deletions lib/src/content/reply_codec.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import 'package:xmtp_proto/xmtp_proto.dart' as xmtp;

import 'codec.dart';
import 'decoded.dart';

final contentTypeReply = xmtp.ContentTypeId(
authorityId: "xmtp.org",
typeId: "reply",
versionMajor: 1,
versionMinor: 0,
);

/// This is a reply to another [reference] message.
class Reply {
/// This is the message ID of the parent message.
/// See [DecodedMessage.id]
final String reference;
final DecodedContent content;

Reply(this.reference, this.content);
}

extension on xmtp.ContentTypeId {
String toText() => "$authorityId/$typeId:$versionMajor.$versionMinor";
}

/// This is a [Codec] that encodes a reply to another message.
class ReplyCodec extends NestedContentCodec<Reply> {

@override
xmtp.ContentTypeId get contentType => contentTypeReply;

@override
Future<Reply> decode(xmtp.EncodedContent encoded) async => Reply(
encoded.parameters["reference"] ?? "",
await registry.decode(xmtp.EncodedContent.fromBuffer(encoded.content)),
);

@override
Future<xmtp.EncodedContent> encode(Reply decoded) async =>
xmtp.EncodedContent(
type: contentTypeReply,
parameters: {
"reference": decoded.reference,
// TODO: cut when we know nothing looks here for the content type
"contentType": decoded.content.contentType.toText(),
},
content: (await registry.encode(decoded.content)).writeToBuffer(),
);
}
Loading

0 comments on commit 762b83f

Please sign in to comment.