diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java b/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java index 9837b23..06c1584 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java @@ -18,11 +18,13 @@ package ch.dissem.bitmessage.entity; import ch.dissem.bitmessage.entity.payload.Msg; import ch.dissem.bitmessage.entity.payload.Pubkey.Feature; -import ch.dissem.bitmessage.entity.valueobject.Attachment; +import ch.dissem.bitmessage.entity.valueobject.extended.Attachment; import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding; import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import ch.dissem.bitmessage.entity.valueobject.Label; +import ch.dissem.bitmessage.entity.valueobject.extended.Message; import ch.dissem.bitmessage.exception.ApplicationException; +import ch.dissem.bitmessage.factory.ExtendedEncodingFactory; import ch.dissem.bitmessage.factory.Factory; import ch.dissem.bitmessage.utils.Decode; import ch.dissem.bitmessage.utils.Encode; @@ -333,10 +335,10 @@ public class Plaintext implements Streamable { Scanner s = new Scanner(new ByteArrayInputStream(message), "UTF-8"); String firstLine = s.nextLine(); if (encoding == EXTENDED.code) { - if (getExtendedData().getMessage() == null) { - return null; + if (Message.TYPE.equals(getExtendedData().getType())) { + return ((Message) extendedData.getContent()).getSubject(); } else { - return extendedData.getMessage().getSubject(); + return null; } } else if (encoding == SIMPLE.code) { return firstLine.substring("Subject:".length()).trim(); @@ -349,10 +351,10 @@ public class Plaintext implements Streamable { public String getText() { if (encoding == EXTENDED.code) { - if (getExtendedData().getMessage() == null) { - return null; + if (Message.TYPE.equals(getExtendedData().getType())) { + return ((Message) extendedData.getContent()).getBody(); } else { - return extendedData.getMessage().getBody(); + return null; } } else { try { @@ -370,24 +372,24 @@ public class Plaintext implements Streamable { protected ExtendedEncoding getExtendedData() { if (extendedData == null && encoding == EXTENDED.code) { // TODO: make sure errors are properly handled - extendedData = ExtendedEncoding.unzip(message); + extendedData = ExtendedEncodingFactory.getInstance().unzip(message); } return extendedData; } public List getParents() { - if (getExtendedData() == null || extendedData.getMessage() == null) { - return Collections.emptyList(); + if (Message.TYPE.equals(getExtendedData().getType())) { + return ((Message) extendedData.getContent()).getParents(); } else { - return extendedData.getMessage().getParents(); + return Collections.emptyList(); } } public List getFiles() { - if (getExtendedData() == null || extendedData.getMessage() == null) { - return Collections.emptyList(); + if (Message.TYPE.equals(getExtendedData().getType())) { + return ((Message) extendedData.getContent()).getFiles(); } else { - return extendedData.getMessage().getFiles(); + return Collections.emptyList(); } } diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/ExtendedEncoding.java b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/ExtendedEncoding.java index 699cd22..f5a15c7 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/ExtendedEncoding.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/ExtendedEncoding.java @@ -4,35 +4,38 @@ import ch.dissem.bitmessage.exception.ApplicationException; import org.msgpack.core.MessagePack; import org.msgpack.core.MessagePacker; import org.msgpack.core.MessageUnpacker; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.Serializable; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; import java.util.Objects; import java.util.zip.DeflaterOutputStream; -import java.util.zip.InflaterInputStream; /** * Extended encoding message object. */ public class ExtendedEncoding implements Serializable { private static final long serialVersionUID = 3876871488247305200L; + private static final Logger LOG = LoggerFactory.getLogger(ExtendedEncoding.class); - private Message message; + private ExtendedType content; - public ExtendedEncoding(Message message) { - this.message = message; + public ExtendedEncoding(ExtendedType content) { + this.content = content; } - private ExtendedEncoding() { + public String getType() { + if (content == null) { + return null; + } else { + return content.getType(); + } } - public Message getMessage() { - return message; + public ExtendedType getContent() { + return content; } public byte[] zip() { @@ -40,10 +43,7 @@ public class ExtendedEncoding implements Serializable { DeflaterOutputStream zipper = new DeflaterOutputStream(out)) { MessagePacker packer = MessagePack.newDefaultPacker(zipper); - // FIXME: this should work for trivial cases - if (message != null) { - message.pack(packer); - } + content.pack(packer); packer.close(); zipper.close(); return out.toByteArray(); @@ -52,152 +52,28 @@ public class ExtendedEncoding implements Serializable { } } - public static ExtendedEncoding unzip(byte[] zippedData) { - ExtendedEncoding result = new ExtendedEncoding(); - try (InflaterInputStream unzipper = new InflaterInputStream(new ByteArrayInputStream(zippedData))) { - MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(unzipper); - int mapSize = unpacker.unpackMapHeader(); - for (int i = 0; i < mapSize; i++) { - String key = unpacker.unpackString(); - switch (key) { - case "": - switch (unpacker.unpackString()) { - case "message": - result.message = new Message(); - break; - } - break; - case "subject": - result.message.subject = unpacker.unpackString(); - break; - case "body": - result.message.body = unpacker.unpackString(); - break; - default: - break; - } - } - } catch (IOException e) { - throw new ApplicationException(e); - } - return result; - } - @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ExtendedEncoding that = (ExtendedEncoding) o; - return Objects.equals(message, that.message); + return Objects.equals(content, that.content); } @Override public int hashCode() { - return Objects.hash(message); + return Objects.hash(content); } - public static class Message implements Serializable { - private static final long serialVersionUID = -2724977231484285467L; + public interface Unpacker { + String getType(); - private String subject; - private String body; - private List parents; - private List files; - - private Message() { - parents = Collections.emptyList(); - files = Collections.emptyList(); - } - - private Message(Builder builder) { - subject = builder.subject; - body = builder.body; - parents = Collections.unmodifiableList(builder.parents); - files = Collections.unmodifiableList(builder.files); - } - - public String getSubject() { - return subject; - } - - public String getBody() { - return body; - } - - public List getParents() { - return parents; - } - - public List getFiles() { - return files; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Message message = (Message) o; - return Objects.equals(subject, message.subject) && - Objects.equals(body, message.body) && - Objects.equals(parents, message.parents) && - Objects.equals(files, message.files); - } - - @Override - public int hashCode() { - return Objects.hash(subject, body, parents, files); - } - - public void pack(MessagePacker packer) throws IOException { - packer.packMapHeader(3); - packer.packString(""); - packer.packString("message"); - packer.packString("subject"); - packer.packString(subject); - packer.packString("body"); - packer.packString(body); - } - - public static class Builder { - private String subject; - private String body; - private List parents = new LinkedList<>(); - private List files = new LinkedList<>(); - - private Builder() { - } - - public Builder subject(String subject) { - this.subject = subject; - return this; - } - - public Builder body(String body) { - this.body = body; - return this; - } - - public Builder addParent(InventoryVector iv) { - parents.add(iv); - return this; - } - - public Builder addFile(Attachment file) { - files.add(file); - return this; - } - - public ExtendedEncoding build() { - return new ExtendedEncoding(new Message(this)); - } - } + T unpack(MessageUnpacker unpacker, int size); } - public static class Builder { - public Message.Builder message() { - return new Message.Builder(); - } + public interface ExtendedType extends Serializable { + String getType(); - // TODO: vote (etc.?) + void pack(MessagePacker packer) throws IOException; } } diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/Attachment.java b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Attachment.java similarity index 91% rename from core/src/main/java/ch/dissem/bitmessage/entity/valueobject/Attachment.java rename to core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Attachment.java index 8ea64de..42afc4f 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/Attachment.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Attachment.java @@ -1,4 +1,4 @@ -package ch.dissem.bitmessage.entity.valueobject; +package ch.dissem.bitmessage.entity.valueobject.extended; import java.io.Serializable; import java.util.Arrays; @@ -48,7 +48,7 @@ public class Attachment implements Serializable { return Objects.hash(name, data, type, disposition); } - private enum Disposition { + public enum Disposition { inline, attachment } @@ -83,6 +83,11 @@ public class Attachment implements Serializable { return this; } + public Builder disposition(Disposition disposition) { + this.disposition = disposition; + return this; + } + public Attachment build() { Attachment attachment = new Attachment(); attachment.type = this.type; diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Message.java b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Message.java new file mode 100644 index 0000000..33e7b81 --- /dev/null +++ b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Message.java @@ -0,0 +1,257 @@ +package ch.dissem.bitmessage.entity.valueobject.extended; + +import ch.dissem.bitmessage.entity.Plaintext; +import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding; +import ch.dissem.bitmessage.entity.valueobject.InventoryVector; +import org.msgpack.core.MessagePacker; +import org.msgpack.core.MessageUnpacker; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.net.URLConnection; +import java.nio.file.Files; +import java.util.*; + +/** + * Extended encoding type 'message'. Properties 'parents' and 'files' not yet supported by PyBitmessage, so they might not work + * properly with future PyBitmessage implementations. + */ +public class Message implements ExtendedEncoding.ExtendedType { + private static final long serialVersionUID = -2724977231484285467L; + private static final Logger LOG = LoggerFactory.getLogger(Message.class); + + public static final String TYPE = "message"; + + private String subject; + private String body; + private List parents; + private List files; + + private Message(Builder builder) { + subject = builder.subject; + body = builder.body; + parents = Collections.unmodifiableList(builder.parents); + files = Collections.unmodifiableList(builder.files); + } + + @Override + public String getType() { + return TYPE; + } + + public String getSubject() { + return subject; + } + + public String getBody() { + return body; + } + + public List getParents() { + return parents; + } + + public List getFiles() { + return files; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Message message = (Message) o; + return Objects.equals(subject, message.subject) && + Objects.equals(body, message.body) && + Objects.equals(parents, message.parents) && + Objects.equals(files, message.files); + } + + @Override + public int hashCode() { + return Objects.hash(subject, body, parents, files); + } + + public void pack(MessagePacker packer) throws IOException { + int size = 3; + if (!files.isEmpty()) { + size++; + } + if (!parents.isEmpty()) { + size++; + } + packer.packMapHeader(size); + packer.packString(""); + packer.packString("message"); + packer.packString("subject"); + packer.packString(subject); + packer.packString("body"); + packer.packString(body); + if (!files.isEmpty()) { + packer.packString("files"); + packer.packArrayHeader(files.size()); + for (Attachment file : files) { + packer.packMapHeader(4); + packer.packString("name"); + packer.packString(file.getName()); + packer.packString("data"); + packer.packBinaryHeader(file.getData().length); + packer.writePayload(file.getData()); + packer.packString("type"); + packer.packString(file.getType()); + packer.packString("disposition"); + packer.packString(file.getDisposition().name()); + } + } + if (!parents.isEmpty()) { + packer.packString("parents"); + packer.packArrayHeader(parents.size()); + for (InventoryVector parent : parents) { + packer.packBinaryHeader(parent.getHash().length); + packer.writePayload(parent.getHash()); + } + } + } + + public static class Builder { + private String subject; + private String body; + private List parents = new LinkedList<>(); + private List files = new LinkedList<>(); + + public Builder subject(String subject) { + this.subject = subject; + return this; + } + + public Builder body(String body) { + this.body = body; + return this; + } + + public Builder addParent(Plaintext parent) { + parents.add(parent.getInventoryVector()); + return this; + } + + public Builder addParent(InventoryVector iv) { + parents.add(iv); + return this; + } + + public Builder addFile(File file, Attachment.Disposition disposition) { + try { + files.add(new Attachment.Builder() + .name(file.getName()) + .disposition(disposition) + .type(URLConnection.guessContentTypeFromStream(new FileInputStream(file))) + .data(Files.readAllBytes(file.toPath())) + .build()); + } catch (IOException e) { + LOG.error(e.getMessage(), e); + } + return this; + } + + public Builder addFile(Attachment file) { + files.add(file); + return this; + } + + public ExtendedEncoding build() { + return new ExtendedEncoding(new Message(this)); + } + } + + public static class Unpacker implements ExtendedEncoding.Unpacker { + @Override + public String getType() { + return TYPE; + } + + @Override + public Message unpack(MessageUnpacker unpacker, int size) { + Message.Builder builder = new Message.Builder(); + try { + for (int i = 0; i < size; i++) { + String key = unpacker.unpackString(); + switch (key) { + case "subject": + builder.subject(unpacker.unpackString()); + break; + case "body": + builder.body(unpacker.unpackString()); + break; + case "parents": + builder.parents = unpackParents(unpacker); + break; + case "files": + builder.files = unpackFiles(unpacker); + break; + default: + LOG.error("Unexpected data with key: " + key); + break; + } + } + } catch (IOException e) { + LOG.error(e.getMessage(), e); + } + return new Message(builder); + } + + private static List unpackParents(MessageUnpacker unpacker) throws IOException { + int size = unpacker.unpackArrayHeader(); + List parents = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + int binarySize = unpacker.unpackBinaryHeader(); + parents.add(new InventoryVector(unpacker.readPayload(binarySize))); + } + return parents; + } + + private static List unpackFiles(MessageUnpacker unpacker) throws IOException { + int size = unpacker.unpackArrayHeader(); + List files = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + Attachment.Builder attachment = new Attachment.Builder(); + int mapSize = unpacker.unpackMapHeader(); + for (int j = 0; j < mapSize; j++) { + String key = unpacker.unpackString(); + switch (key) { + case "name": + attachment.name(unpacker.unpackString()); + break; + case "data": + int binarySize = unpacker.unpackBinaryHeader(); + attachment.data(unpacker.readPayload(binarySize)); + break; + case "type": + attachment.type(unpacker.unpackString()); + break; + case "disposition": + String disposition = unpacker.unpackString(); + switch (disposition) { + case "inline": + attachment.inline(); + break; + case "attachment": + attachment.attachment(); + break; + default: + LOG.debug("Unknown disposition: " + disposition); + break; + } + break; + default: + LOG.debug("Unknown file info '" + key + "' with data: " + unpacker.unpackValue()); + break; + } + } + files.add(attachment.build()); + } + return files; + } + } +} diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Vote.java b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Vote.java new file mode 100644 index 0000000..75cea67 --- /dev/null +++ b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Vote.java @@ -0,0 +1,131 @@ +package ch.dissem.bitmessage.entity.valueobject.extended; + +import ch.dissem.bitmessage.entity.Plaintext; +import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding; +import ch.dissem.bitmessage.entity.valueobject.InventoryVector; +import org.msgpack.core.MessagePacker; +import org.msgpack.core.MessageUnpacker; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.Objects; + +/** + * Extended encoding type 'vote'. Specification still outstanding, so this will need some work. + */ +public class Vote implements ExtendedEncoding.ExtendedType { + private static final long serialVersionUID = -8427038604209964837L; + private static final Logger LOG = LoggerFactory.getLogger(Vote.class); + + public static final String TYPE = "vote"; + + private InventoryVector msgId; + private String vote; + + private Vote(Builder builder) { + msgId = builder.msgId; + vote = builder.vote; + } + + @Override + public String getType() { + return TYPE; + } + + public InventoryVector getMsgId() { + return msgId; + } + + public String getVote() { + return vote; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Vote vote1 = (Vote) o; + return Objects.equals(msgId, vote1.msgId) && + Objects.equals(vote, vote1.vote); + } + + @Override + public int hashCode() { + return Objects.hash(msgId, vote); + } + + public void pack(MessagePacker packer) throws IOException { + packer.packMapHeader(3); + packer.packString(""); + packer.packString("vote"); + packer.packString("msgId"); + packer.packBinaryHeader(msgId.getHash().length); + packer.writePayload(msgId.getHash()); + packer.packString("vote"); + packer.packString(vote); + } + + public static class Builder { + private InventoryVector msgId; + private String vote; + + public ExtendedEncoding up(Plaintext message) { + msgId = message.getInventoryVector(); + vote = "1"; + return new ExtendedEncoding(new Vote(this)); + } + + public ExtendedEncoding down(Plaintext message) { + msgId = message.getInventoryVector(); + vote = "1"; + return new ExtendedEncoding(new Vote(this)); + } + + public Builder msgId(InventoryVector iv) { + this.msgId = iv; + return this; + } + + public Builder vote(String vote) { + this.vote = vote; + return this; + } + + public ExtendedEncoding build() { + return new ExtendedEncoding(new Vote(this)); + } + } + + public static class Unpacker implements ExtendedEncoding.Unpacker { + @Override + public String getType() { + return TYPE; + } + + @Override + public Vote unpack(MessageUnpacker unpacker, int size) { + Vote.Builder builder = new Vote.Builder(); + try { + for (int i = 0; i < size; i++) { + String key = unpacker.unpackString(); + switch (key) { + case "msgId": + int binarySize = unpacker.unpackBinaryHeader(); + builder.msgId(new InventoryVector(unpacker.readPayload(binarySize))); + break; + case "vote": + builder.vote(unpacker.unpackString()); + break; + default: + LOG.error("Unexpected data with key: " + key); + break; + } + } + } catch (IOException e) { + LOG.error(e.getMessage(), e); + } + return new Vote(builder); + } + } +} diff --git a/core/src/main/java/ch/dissem/bitmessage/factory/ExtendedEncodingFactory.java b/core/src/main/java/ch/dissem/bitmessage/factory/ExtendedEncodingFactory.java new file mode 100644 index 0000000..0dc419c --- /dev/null +++ b/core/src/main/java/ch/dissem/bitmessage/factory/ExtendedEncodingFactory.java @@ -0,0 +1,57 @@ +package ch.dissem.bitmessage.factory; + +import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding; +import ch.dissem.bitmessage.entity.valueobject.extended.Message; +import ch.dissem.bitmessage.entity.valueobject.extended.Vote; +import ch.dissem.bitmessage.exception.ApplicationException; +import org.msgpack.core.MessagePack; +import org.msgpack.core.MessageUnpacker; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.zip.InflaterInputStream; + +/** + * Factory that creates {@link ExtendedEncoding} objects from byte arrays. You can register your own types by adding a + * {@link ExtendedEncoding.Unpacker} using {@link #registerFactory(ExtendedEncoding.Unpacker)}. + */ +public class ExtendedEncodingFactory { + private static final Logger LOG = LoggerFactory.getLogger(ExtendedEncodingFactory.class); + private static final ExtendedEncodingFactory INSTANCE = new ExtendedEncodingFactory(); + private Map> factories = new HashMap<>(); + + private ExtendedEncodingFactory() { + registerFactory(new Message.Unpacker()); + registerFactory(new Vote.Unpacker()); + } + + public void registerFactory(ExtendedEncoding.Unpacker factory) { + factories.put(factory.getType(), factory); + } + + + public ExtendedEncoding unzip(byte[] zippedData) { + try (InflaterInputStream unzipper = new InflaterInputStream(new ByteArrayInputStream(zippedData))) { + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(unzipper); + int mapSize = unpacker.unpackMapHeader(); + String key = unpacker.unpackString(); + if (!"".equals(key)) { + LOG.error("Unexpected content: " + key); + return null; + } + String type = unpacker.unpackString(); + ExtendedEncoding.Unpacker factory = factories.get(type); + return new ExtendedEncoding(factory.unpack(unpacker, mapSize - 1)); + } catch (IOException e) { + throw new ApplicationException(e); + } + } + + public static ExtendedEncodingFactory getInstance() { + return INSTANCE; + } +} diff --git a/core/src/main/java/ch/dissem/bitmessage/utils/Bytes.java b/core/src/main/java/ch/dissem/bitmessage/utils/Bytes.java index 986c288..c0bf554 100644 --- a/core/src/main/java/ch/dissem/bitmessage/utils/Bytes.java +++ b/core/src/main/java/ch/dissem/bitmessage/utils/Bytes.java @@ -16,6 +16,11 @@ package ch.dissem.bitmessage.utils; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.Files; + /** * A helper class for working with byte arrays interpreted as unsigned big endian integers. * This is one part due to the fact that Java doesn't support unsigned numbers, and another diff --git a/core/src/test/java/ch/dissem/bitmessage/entity/ExtendedEncodingTest.java b/core/src/test/java/ch/dissem/bitmessage/entity/ExtendedEncodingTest.java index b4a1017..c417df1 100644 --- a/core/src/test/java/ch/dissem/bitmessage/entity/ExtendedEncodingTest.java +++ b/core/src/test/java/ch/dissem/bitmessage/entity/ExtendedEncodingTest.java @@ -1,39 +1,82 @@ package ch.dissem.bitmessage.entity; import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding; +import ch.dissem.bitmessage.entity.valueobject.extended.Attachment; +import ch.dissem.bitmessage.entity.valueobject.extended.Message; +import ch.dissem.bitmessage.entity.valueobject.extended.Vote; +import ch.dissem.bitmessage.factory.ExtendedEncodingFactory; import ch.dissem.bitmessage.utils.Bytes; +import ch.dissem.bitmessage.utils.TestUtils; import org.junit.Test; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; +import java.io.UnsupportedEncodingException; + +import static org.hamcrest.Matchers.*; import static org.junit.Assert.assertThat; /** * @author Christian Basler */ public class ExtendedEncodingTest { + private ExtendedEncodingFactory extendedEncodingFactory = ExtendedEncodingFactory.getInstance(); @Test public void ensureSimpleMessageIsDecoded() { - ExtendedEncoding extended = ExtendedEncoding.unzip(Bytes.fromHex("78da9d59dd8e1bb715ee359fa097c4b65b498876babb76d0760bd970368eedb68e8daed3c0700c81d25012b333c3e99063ad1c0430d0a728d002bd289077caa59fa4df39244723693705b2f0da339c730ebff3c3f343ffe33f3f94da39b5d4ff9dd97cf3e32f9d10524afe4b3a2b57762dfd4acb453bbf967355c967b2755a1a2f1f0afe791eb85f2a7c374e2ab950ce8fe5dc96b59a7b3933956a36d2e9c6a8c2bc57ded84a2e6c532a10b9d678352b342d48674a53a846e6ca2be9adfcd3d58b2f33f96a05a190748d3d64ddd87726d74e5ebedcf81504417a6eaaa563018d56f4225595cb75633c3df7d191e04c3cab9c574521c4af656d6a69c2ab2cdd927639a959b0102f372f3742ecae6ef7afdb0668c2a229eb4297baf2ac5b2689932d4576f2809f89af01d3ae61d9af57ba921bdbd2f78f1ffee999269a2837ce3766d692943113555ae7648984f16fc6b5aa9057becd8d95d038ca95579fff5902497ccdf00064ad97fac6ebcab1bcd69139eec64dfcc9aa4d5b39e90abb2e3699105f60a3f8e13cfbdd583e37f3c63abbf009cfe5279fc84bb8db14ba61476ca929221a8d58c03e399471b660fd76c5decb3ebd456c54f3fcf4ec53125fb695f11bf93837016e231fdfd40ddcdb2d91cd679a4cca569bb5a6c8b72690a5cddb4267e22945b42532f1a2d2276e653d0798fc8d6c2b7a10e22b78859e66ac0e3da5b80a14613d3ce34b96a2671b20795bd68e390aab723ce14f615408543e1bdecc4c411aade12cf9ad034222afcdfc9a500a96472b248a313882bd80954f0a73ada59d7dabe73e8b30babde23b766a6c79482ec483070fc8f7b6f109362fc5e78cf51bbe391bcbf3b1bcf7762406dfdcfce1de3737a767f83dc7efbdc10e7db4c8703a121d93101154c2128911f2b043d3a80da912bccf6b85a19c81d80f07273083c4b788d10b71eb86b3436014e67a4ac2265fa8c2e99118464823215ee344c1d92d8242156bb5a1b482e8a1e496b8e4b5deac6d038266d9d2d1c8e4954628e88633563547e273aed514d485e20413222970db9ae370a6717460e8bf221fb1f8dccee968839abc6fb1d4445ae4862b8fbc55d2c72e9e1080fc0832cea81f3ffccbed51e9e6e3877f67f299df9ab82dbc81b5a2a3a3ff6da565e09543eccd6bb38da7141642951036b65dae90d2c14319a7d4f04b3e82067b61c2dc389271f93392f3ec8510b3762127e9753812a4a541d6928daa967a78767a3abaa07a02ba8c12b31eee065b2033a3d18865654eebebe1e9284511cc30e9bc9f2c330461d82912e5b46162e0eda0a2a97cf71d393d98f7b79da1a55dc879eb3cd40a5567536b216054327b81ea575be70c1528b8997822eb011372ee53dd68e6ab907314e556f63648b437a5ceba87ceac69e5c0ce0211b5688b696e504027f23b56e6c8e44717f26c1c5ee670a9d7b472b84165d7c3d1587c2f44ae173247facdf5347d1d223c8237a459c8d960da7d994e0764427c0f9fe9072f0070b80542aa4ed2de1c2937c5c2d1dbb13c3a7e7d5c1ee7af8e9f5e1c3fbf38beca8e17472396d668df362c3dc0d2d54fc0328e4b1ece1bad8f0f018cb610a3e0ef7635b990af9a568fe52060c33b0411ec05ef762bceef0f808a1039c911bb51db731210ea85c2019ceca93512d40030cd542d15cc3b39c863bd3dc6f1f04e57d65e4ff63c37daa605ce993d5264cda2985160a2d86af34e53d620817fe4fc13296b651ab7475f221593ad2955f56a6714032921b12d28339ebc53454b85116232f1980a2be70f8aff9f776a6257421851a779edee021597b87cf042886eb67b0c9f2ccbe41d21c46c19ff3de25a49f1401b928de5642207f9602ba01708c95dd0f715c887f7cfc781d7869c3e44d64a6c8d32e81c88ec71d3d86678f455758dd358b16617f2b83992c792f104a64e0d68cf8e19121a8e77b5ab4e4279ff7c17a44240f5541b428bd12e4146593b4265b9b729a9b62a8485a46d0fcf1630f9ef60dbb17c739651a9cdeea36920ba9896f7cf0db16f0f4cfc377074a9fcae5332ee2c35490fa31ea6c936d9d3f117e251fe8e4220dfd65658b2f28d45ffff8873b52abc6e2ad4f2771cae284e8d0a1d78578353418d1533cac2f4726dea314f1d53b6c2748547d05374f16aa9eab4166aaaa37986eb428935bfb64cc725a3f206f5238e62a14ed3b108b59b7671085542596052d0719c72a92d0d5e4b6709c430e9987a64b3ac2c771ec687bdd1b6f8b877c4945c4e9c555bce48634464980d1c95046e916a74adc97e4864ba19d378e8570a63869aaf1203d19359c9208c603771f067641d182eb5ebc9e73402c2a23557f2dcc0d498027802794cf28109389cee6053ea0a5d1491f1c4a7196a94c7218139954cccadb3daa6bd455bcd8913ade55d4d0d67973492e961ea2cc672a9fd144d229e6315aa487527bbe06bb23def0f4309dc6b8b025baf92fd0acd660127036890cf1ca4d1127eafc8921d2d9efb1b8687b80ffd4401931eda2178466267b72af4a7c13d34a224dba53884d50273c7c6cd636828fb2d5ffadc2122570eb7c4a1efdbc7874eafcafb5440cb6028a98a2f2dd645ec9c29cae3a01cbb34d4e882bcbc71e36efecaad7661b026d781af356e256f17415307e6a238839e85e3411fe265020d9714d07771a3da55aa84b11a45dd7e824061fdff37e73d501a32f919861ec3b5b3c220da87749e754f52e5396731c38520c80cbc69df467768c86d20e13d03e3cc841ac41303b69ae9b9a2ab876badebdbc6513e3bb1c784cbd9d28599217f91f8b572a153e0f8a4ca5dda86ce9eaaa28eb0c765db00922f9005d62b4ca17b372cd2b5351d38a8add71dbeae2e48a73dcf575b8fc66b271e89b96a3b06032c61a0496d0c1245b2d390ac00d1e99e81669ad7e1f645ce57740af98286340c9d088d72c032252c132a2169a62321a11ad0fedce5817e8221acf58b93dfe3df1e65aa1c77f7327794c637b381ab554973ec402f9783b7e34344a35b87e1ae3c266403c68566e0cd9e482152ab25c42b1e5ebbde6b1c26e2bd8e27d6bfd4ccfd0cb56e6ba166839bcdfbf79bc1e8a7f419897e1f32213e8acc49e2dd7a33064708de1842eca9d82584413e4ee694583876f742f2343bcf6e4854837e96b2c1c1f77b72a5d028385b62bb2a1d1c04778826b4c5cc971084c44a0179d705031d468e332e43d82093bd7b8aee82a223d7377561e606c78a15a062b65690b77386b3eda4d05506b606e38ecd782aa34842a16f75f245eb5f2c3ee7b2b93d4b57ded6cf525794edbe12fa2e1184d689eecb1aebeddc16d25654bed9da6a46f791bd8b14f1e41239a27705f4e412394b514258d325e9b2b16bce9b4561e79c86d27d17df08975a55aed7d5513310521a7e0bba157c72194c99a26339cf9096e9b619c6e02db6dc05ecdb355f99d8bbd011e22ff462822bb609eaefada61b214ee73054d0252465beb7228ec22c57dc6f516e2499bba076afab62a2dce26298bbb74ff1d8a98aaef27ae6e311b57fabcc0a203952a740beefdf849351d7e9266e119a3452012d4bcd85650f57a4df02732d3ab2642ff9a80a775aeb70afb74716d38749d3dec1349a891f5ccb8b3ffee2ac1b2b5326eb5d6c73d7e0a43c917c5548ff35f1f0e1ff00b5629026")); - assertThat(extended, notNullValue()); - assertThat(extended.getMessage().getSubject(), is("Extended encoding on Windows works - but how ??")); - assertThat(extended.getMessage().getBody(), notNullValue()); - assertThat(extended.getMessage().getBody().length(), is(6233)); + ExtendedEncoding extended = extendedEncodingFactory.unzip(Bytes.fromHex("78da9d59dd8e1bb715ee359fa097c4b65b498876babb76d0760bd970368eedb68e8daed3c0700c81d25012b333c3e99063ad1c0430d0a728d002bd289077caa59fa4df39244723693705b2f0da339c730ebff3c3f343ffe33f3f94da39b5d4ff9dd97cf3e32f9d10524afe4b3a2b57762dfd4acb453bbf967355c967b2755a1a2f1f0afe791eb85f2a7c374e2ab950ce8fe5dc96b59a7b3933956a36d2e9c6a8c2bc57ded84a2e6c532a10b9d678352b342d48674a53a846e6ca2be9adfcd3d58b2f33f96a05a190748d3d64ddd87726d74e5ebedcf81504417a6eaaa563018d56f4225595cb75633c3df7d191e04c3cab9c574521c4af656d6a69c2ab2cdd927639a959b0102f372f3742ecae6ef7afdb0668c2a229eb4297baf2ac5b2689932d4576f2809f89af01d3ae61d9af57ba921bdbd2f78f1ffee999269a2837ce3766d692943113555ae7648984f16fc6b5aa9057becd8d95d038ca95579fff5902497ccdf00064ad97fac6ebcab1bcd69139eec64dfcc9aa4d5b39e90abb2e3699105f60a3f8e13cfbdd583e37f3c63abbf009cfe5279fc84bb8db14ba61476ca929221a8d58c03e399471b660fd76c5decb3ebd456c54f3fcf4ec53125fb695f11bf93837016e231fdfd40ddcdb2d91cd679a4cca569bb5a6c8b72690a5cddb4267e22945b42532f1a2d2276e653d0798fc8d6c2b7a10e22b78859e66ac0e3da5b80a14613d3ce34b96a2671b20795bd68e390aab723ce14f615408543e1bdecc4c411aade12cf9ad034222afcdfc9a500a96472b248a313882bd80954f0a73ada59d7dabe73e8b30babde23b766a6c79482ec483070fc8f7b6f109362fc5e78cf51bbe391bcbf3b1bcf7762406dfdcfce1de3737a767f83dc7efbdc10e7db4c8703a121d93101154c2128911f2b043d3a80da912bccf6b85a19c81d80f07273083c4b788d10b71eb86b3436014e67a4ac2265fa8c2e99118464823215ee344c1d92d8242156bb5a1b482e8a1e496b8e4b5deac6d038266d9d2d1c8e4954628e88633563547e273aed514d485e20413222970db9ae370a6717460e8bf221fb1f8dccee968839abc6fb1d4445ae4862b8fbc55d2c72e9e1080fc0832cea81f3ffccbed51e9e6e3877f67f299df9ab82dbc81b5a2a3a3ff6da565e09543eccd6bb38da7141642951036b65dae90d2c14319a7d4f04b3e82067b61c2dc389271f93392f3ec8510b3762127e9753812a4a541d6928daa967a78767a3abaa07a02ba8c12b31eee065b2033a3d18865654eebebe1e9284511cc30e9bc9f2c330461d82912e5b46162e0eda0a2a97cf71d393d98f7b79da1a55dc879eb3cd40a5567536b216054327b81ea575be70c1528b8997822eb011372ee53dd68e6ab907314e556f63648b437a5ceba87ceac69e5c0ce0211b5688b696e504027f23b56e6c8e44717f26c1c5ee670a9d7b472b84165d7c3d1587c2f44ae173247facdf5347d1d223c8237a459c8d960da7d994e0764427c0f9fe9072f0070b80542aa4ed2de1c2937c5c2d1dbb13c3a7e7d5c1ee7af8e9f5e1c3fbf38beca8e17472396d668df362c3dc0d2d54fc0328e4b1ece1bad8f0f018cb610a3e0ef7635b990af9a568fe52060c33b0411ec05ef762bceef0f808a1039c911bb51db731210ea85c2019ceca93512d40030cd542d15cc3b39c863bd3dc6f1f04e57d65e4ff63c37daa605ce993d5264cda2985160a2d86af34e53d620817fe4fc13296b651ab7475f221593ad2955f56a6714032921b12d28339ebc53454b85116232f1980a2be70f8aff9f776a6257421851a779edee021597b87cf042886eb67b0c9f2ccbe41d21c46c19ff3de25a49f1401b928de5642207f9602ba01708c95dd0f715c887f7cfc781d7869c3e44d64a6c8d32e81c88ec71d3d86678f455758dd358b16617f2b83992c792f104a64e0d68cf8e19121a8e77b5ab4e4279ff7c17a44240f5541b428bd12e4146593b4265b9b729a9b62a8485a46d0fcf1630f9ef60dbb17c739651a9cdeea36920ba9896f7cf0db16f0f4cfc377074a9fcae5332ee2c35490fa31ea6c936d9d3f117e251fe8e4220dfd65658b2f28d45ffff8873b52abc6e2ad4f2771cae284e8d0a1d78578353418d1533cac2f4726dea314f1d53b6c2748547d05374f16aa9eab4166aaaa37986eb428935bfb64cc725a3f206f5238e62a14ed3b108b59b7671085542596052d0719c72a92d0d5e4b6709c430e9987a64b3ac2c771ec687bdd1b6f8b877c4945c4e9c555bce48634464980d1c95046e916a74adc97e4864ba19d378e8570a63869aaf1203d19359c9208c603771f067641d182eb5ebc9e73402c2a23557f2dcc0d498027802794cf28109389cee6053ea0a5d1491f1c4a7196a94c7218139954cccadb3daa6bd455bcd8913ade55d4d0d67973492e961ea2cc672a9fd144d229e6315aa487527bbe06bb23def0f4309dc6b8b025baf92fd0acd660127036890cf1ca4d1127eafc8921d2d9efb1b8687b80ffd4401931eda2178466267b72af4a7c13d34a224dba53884d50273c7c6cd636828fb2d5ffadc2122570eb7c4a1efdbc7874eafcafb5440cb6028a98a2f2dd645ec9c29cae3a01cbb34d4e882bcbc71e36efecaad7661b026d781af356e256f17415307e6a238839e85e3411fe265020d9714d07771a3da55aa84b11a45dd7e824061fdff37e73d501a32f919861ec3b5b3c220da87749e754f52e5396731c38520c80cbc69df467768c86d20e13d03e3cc841ac41303b69ae9b9a2ab876badebdbc6513e3bb1c784cbd9d28599217f91f8b572a153e0f8a4ca5dda86ce9eaaa28eb0c765db00922f9005d62b4ca17b372cd2b5351d38a8add71dbeae2e48a73dcf575b8fc66b271e89b96a3b06032c61a0496d0c1245b2d390ac00d1e99e81669ad7e1f645ce57740af98286340c9d088d72c032252c132a2169a62321a11ad0fedce5817e8221acf58b93dfe3df1e65aa1c77f7327794c637b381ab554973ec402f9783b7e34344a35b87e1ae3c266403c68566e0cd9e482152ab25c42b1e5ebbde6b1c26e2bd8e27d6bfd4ccfd0cb56e6ba166839bcdfbf79bc1e8a7f419897e1f32213e8acc49e2dd7a33064708de1842eca9d82584413e4ee694583876f742f2343bcf6e4854837e96b2c1c1f77b72a5d028385b62bb2a1d1c04778826b4c5cc971084c44a0179d705031d468e332e43d82093bd7b8aee82a223d7377561e606c78a15a062b65690b77386b3eda4d05506b606e38ecd782aa34842a16f75f245eb5f2c3ee7b2b93d4b57ded6cf525794edbe12fa2e1184d689eecb1aebeddc16d25654bed9da6a46f791bd8b14f1e41239a27705f4e412394b514258d325e9b2b16bce9b4561e79c86d27d17df08975a55aed7d5513310521a7e0bba157c72194c99a26339cf9096e9b619c6e02db6dc05ecdb355f99d8bbd011e22ff462822bb609eaefada61b214ee73054d0252465beb7228ec22c57dc6f516e2499bba076afab62a2dce26298bbb74ff1d8a98aaef27ae6e311b57fabcc0a203952a740beefdf849351d7e9266e119a3452012d4bcd85650f57a4df02732d3ab2642ff9a80a775aeb70afb74716d38749d3dec1349a891f5ccb8b3ffee2ac1b2b5326eb5d6c73d7e0a43c917c5548ff35f1f0e1ff00b5629026")); + assertThat(extended, instanceOf(ExtendedEncoding.class)); + assertThat(extended.getContent(), instanceOf(Message.class)); + assertThat(((Message) extended.getContent()).getSubject(), is("Extended encoding on Windows works - but how ??")); + assertThat(((Message) extended.getContent()).getBody(), notNullValue()); + assertThat(((Message) extended.getContent()).getBody().length(), is(6233)); } @Test public void ensureSimpleMessageIsEncoded() { - ExtendedEncoding in = new ExtendedEncoding.Builder() - .message() + ExtendedEncoding in = new Message.Builder() .subject("Test sübject") .body("test bödy") .build(); assertThat(in.zip(), notNullValue()); - ExtendedEncoding out = ExtendedEncoding.unzip(in.zip()); + ExtendedEncoding out = extendedEncodingFactory.unzip(in.zip()); + assertThat(out, is(in)); + } + + @Test + public void ensureCompleteMessageIsEncodedAndDecoded() throws UnsupportedEncodingException { + ExtendedEncoding in = new Message.Builder() + .addParent(TestUtils.randomInventoryVector()) + .addParent(TestUtils.randomInventoryVector()) + .subject("Test sübject") + .body("test bödy") + .addFile( + new Attachment.Builder() + .name("test.txt") + .type("text/plain") + .data("test".getBytes("UTF-8")) + .attachment() + .build() + ) + .build(); + + assertThat(in.zip(), notNullValue()); + + ExtendedEncoding out = extendedEncodingFactory.unzip(in.zip()); + assertThat(out, is(in)); + } + + @Test + public void ensureVoteIsEncodedAndDecoded() { + ExtendedEncoding in = new Vote.Builder() + .msgId(TestUtils.randomInventoryVector()) + .vote("+1") + .build(); + + assertThat(in.zip(), notNullValue()); + + ExtendedEncoding out = extendedEncodingFactory.unzip(in.zip()); assertThat(out, is(in)); } } diff --git a/core/src/test/java/ch/dissem/bitmessage/entity/SerializationTest.java b/core/src/test/java/ch/dissem/bitmessage/entity/SerializationTest.java index 899c60b..aacc00f 100644 --- a/core/src/test/java/ch/dissem/bitmessage/entity/SerializationTest.java +++ b/core/src/test/java/ch/dissem/bitmessage/entity/SerializationTest.java @@ -17,9 +17,9 @@ package ch.dissem.bitmessage.entity; import ch.dissem.bitmessage.entity.payload.*; -import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding; import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import ch.dissem.bitmessage.entity.valueobject.Label; +import ch.dissem.bitmessage.entity.valueobject.extended.Message; import ch.dissem.bitmessage.factory.Factory; import ch.dissem.bitmessage.utils.TestBase; import ch.dissem.bitmessage.utils.TestUtils; @@ -31,7 +31,6 @@ import java.util.ArrayList; import java.util.Collections; import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG; -import static ch.dissem.bitmessage.utils.Singleton.cryptography; import static org.junit.Assert.*; public class SerializationTest extends TestBase { @@ -104,7 +103,7 @@ public class SerializationTest extends TestBase { Plaintext p1 = new Plaintext.Builder(MSG) .from(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8")) .to(TestUtils.loadContact()) - .message(new ExtendedEncoding.Builder().message() + .message(new Message.Builder() .subject("Subject") .body("Message") .build()) @@ -154,7 +153,7 @@ public class SerializationTest extends TestBase { public void ensureNetworkMessageIsSerializedAndDeserializedCorrectly() throws Exception { ArrayList ivs = new ArrayList<>(50000); for (int i = 0; i < 50000; i++) { - ivs.add(new InventoryVector(cryptography().randomBytes(32))); + ivs.add(TestUtils.randomInventoryVector()); } Inv inv = new Inv.Builder().inventory(ivs).build(); diff --git a/core/src/test/java/ch/dissem/bitmessage/utils/TestUtils.java b/core/src/test/java/ch/dissem/bitmessage/utils/TestUtils.java index 21f85ff..fffaba8 100644 --- a/core/src/test/java/ch/dissem/bitmessage/utils/TestUtils.java +++ b/core/src/test/java/ch/dissem/bitmessage/utils/TestUtils.java @@ -20,6 +20,7 @@ import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.ObjectMessage; import ch.dissem.bitmessage.entity.payload.Pubkey; import ch.dissem.bitmessage.entity.payload.V4Pubkey; +import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import ch.dissem.bitmessage.entity.valueobject.PrivateKey; import ch.dissem.bitmessage.exception.DecryptionFailedException; import ch.dissem.bitmessage.factory.Factory; @@ -28,6 +29,7 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.util.Random; import static org.junit.Assert.assertEquals; @@ -35,6 +37,8 @@ import static org.junit.Assert.assertEquals; * If there's ever a need for this in production code, it should be rewritten to be more efficient. */ public class TestUtils { + public static final Random RANDOM = new Random(); + public static byte[] int16(int number) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); Encode.int16(number, out); @@ -59,6 +63,12 @@ public class TestUtils { return out.toByteArray(); } + public static InventoryVector randomInventoryVector() { + byte[] bytes = new byte[32]; + RANDOM.nextBytes(bytes); + return new InventoryVector(bytes); + } + public static InputStream getResource(String resourceName) { return TestUtils.class.getClassLoader().getResourceAsStream(resourceName); } diff --git a/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcMessageRepositoryTest.java b/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcMessageRepositoryTest.java index 9d9b002..0d3feec 100644 --- a/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcMessageRepositoryTest.java +++ b/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcMessageRepositoryTest.java @@ -26,6 +26,7 @@ import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.entity.valueobject.PrivateKey; import ch.dissem.bitmessage.ports.AddressRepository; import ch.dissem.bitmessage.ports.MessageRepository; +import ch.dissem.bitmessage.utils.TestUtils; import ch.dissem.bitmessage.utils.UnixTime; import org.junit.Before; import org.junit.Test; @@ -162,7 +163,7 @@ public class JdbcMessageRepositoryTest extends TestBase { @Test public void testSave() throws Exception { Plaintext message = new Plaintext.Builder(MSG) - .IV(new InventoryVector(cryptography().randomBytes(32))) + .IV(TestUtils.randomInventoryVector()) .from(identity) .to(contactA) .message("Subject", "Message") @@ -185,7 +186,7 @@ public class JdbcMessageRepositoryTest extends TestBase { public void testUpdate() throws Exception { List messages = repo.findMessages(Plaintext.Status.DRAFT, contactA); Plaintext message = messages.get(0); - message.setInventoryVector(new InventoryVector(cryptography().randomBytes(32))); + message.setInventoryVector(TestUtils.randomInventoryVector()); repo.save(message); messages = repo.findMessages(Plaintext.Status.DRAFT, contactA); @@ -206,7 +207,7 @@ public class JdbcMessageRepositoryTest extends TestBase { @Test public void ensureUnacknowledgedMessagesAreFoundForResend() throws Exception { Plaintext message = new Plaintext.Builder(MSG) - .IV(new InventoryVector(cryptography().randomBytes(32))) + .IV(TestUtils.randomInventoryVector()) .from(identity) .to(contactA) .message("Subject", "Message") @@ -240,4 +241,4 @@ public class JdbcMessageRepositoryTest extends TestBase { .build(); repo.save(message); } -} \ No newline at end of file +}