Provide a more flexible way to label messages.
I'm not quite sure about chans yet
This commit is contained in:
parent
4f7f80c12a
commit
e8ddf90363
@ -6,7 +6,7 @@ subprojects {
|
|||||||
|
|
||||||
sourceCompatibility = 1.7
|
sourceCompatibility = 1.7
|
||||||
group = 'ch.dissem.jabit'
|
group = 'ch.dissem.jabit'
|
||||||
version = '1.0.2-SNAPSHOT'
|
version = '1.1.0-SNAPSHOT'
|
||||||
|
|
||||||
ext.isReleaseVersion = !version.endsWith("SNAPSHOT")
|
ext.isReleaseVersion = !version.endsWith("SNAPSHOT")
|
||||||
|
|
||||||
|
@ -17,7 +17,10 @@
|
|||||||
package ch.dissem.bitmessage;
|
package ch.dissem.bitmessage;
|
||||||
|
|
||||||
import ch.dissem.bitmessage.entity.*;
|
import ch.dissem.bitmessage.entity.*;
|
||||||
import ch.dissem.bitmessage.entity.payload.*;
|
import ch.dissem.bitmessage.entity.payload.Broadcast;
|
||||||
|
import ch.dissem.bitmessage.entity.payload.Msg;
|
||||||
|
import ch.dissem.bitmessage.entity.payload.ObjectPayload;
|
||||||
|
import ch.dissem.bitmessage.entity.payload.ObjectType;
|
||||||
import ch.dissem.bitmessage.entity.payload.Pubkey.Feature;
|
import ch.dissem.bitmessage.entity.payload.Pubkey.Feature;
|
||||||
import ch.dissem.bitmessage.entity.valueobject.Label;
|
import ch.dissem.bitmessage.entity.valueobject.Label;
|
||||||
import ch.dissem.bitmessage.entity.valueobject.PrivateKey;
|
import ch.dissem.bitmessage.entity.valueobject.PrivateKey;
|
||||||
@ -65,6 +68,7 @@ public class BitmessageContext {
|
|||||||
|
|
||||||
private final InternalContext ctx;
|
private final InternalContext ctx;
|
||||||
|
|
||||||
|
private final Labeler labeler;
|
||||||
private final Listener listener;
|
private final Listener listener;
|
||||||
private final NetworkHandler.MessageListener networkListener;
|
private final NetworkHandler.MessageListener networkListener;
|
||||||
|
|
||||||
@ -72,8 +76,9 @@ public class BitmessageContext {
|
|||||||
|
|
||||||
private BitmessageContext(Builder builder) {
|
private BitmessageContext(Builder builder) {
|
||||||
ctx = new InternalContext(builder);
|
ctx = new InternalContext(builder);
|
||||||
|
labeler = builder.labeler;
|
||||||
listener = builder.listener;
|
listener = builder.listener;
|
||||||
networkListener = new DefaultMessageListener(ctx, listener);
|
networkListener = new DefaultMessageListener(ctx, labeler, listener);
|
||||||
|
|
||||||
// As this thread is used for parts that do POW, which itself uses parallel threads, only
|
// As this thread is used for parts that do POW, which itself uses parallel threads, only
|
||||||
// one should be executed at any time.
|
// one should be executed at any time.
|
||||||
@ -97,6 +102,10 @@ public class BitmessageContext {
|
|||||||
return ctx.getMessageRepository();
|
return ctx.getMessageRepository();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Labeler labeler() {
|
||||||
|
return labeler;
|
||||||
|
}
|
||||||
|
|
||||||
public BitmessageAddress createIdentity(boolean shorter, Feature... features) {
|
public BitmessageAddress createIdentity(boolean shorter, Feature... features) {
|
||||||
final BitmessageAddress identity = new BitmessageAddress(new PrivateKey(
|
final BitmessageAddress identity = new BitmessageAddress(new PrivateKey(
|
||||||
shorter,
|
shorter,
|
||||||
@ -280,7 +289,9 @@ public class BitmessageContext {
|
|||||||
try {
|
try {
|
||||||
Broadcast broadcast = (Broadcast) object.getPayload();
|
Broadcast broadcast = (Broadcast) object.getPayload();
|
||||||
broadcast.decrypt(address);
|
broadcast.decrypt(address);
|
||||||
listener.receive(broadcast.getPlaintext());
|
// This decrypts it twice, but on the other hand it doesn't try to decrypt the objects with
|
||||||
|
// other subscriptions and the interface stays as simple as possible.
|
||||||
|
networkListener.receive(object);
|
||||||
} catch (DecryptionFailedException ignore) {
|
} catch (DecryptionFailedException ignore) {
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOG.debug(e.getMessage(), e);
|
LOG.debug(e.getMessage(), e);
|
||||||
@ -318,6 +329,7 @@ public class BitmessageContext {
|
|||||||
Cryptography cryptography;
|
Cryptography cryptography;
|
||||||
MessageCallback messageCallback;
|
MessageCallback messageCallback;
|
||||||
CustomCommandHandler customCommandHandler;
|
CustomCommandHandler customCommandHandler;
|
||||||
|
Labeler labeler;
|
||||||
Listener listener;
|
Listener listener;
|
||||||
int connectionLimit = 150;
|
int connectionLimit = 150;
|
||||||
long connectionTTL = 30 * MINUTE;
|
long connectionTTL = 30 * MINUTE;
|
||||||
@ -378,6 +390,11 @@ public class BitmessageContext {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Builder labeler(Labeler labeler) {
|
||||||
|
this.labeler = labeler;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public Builder listener(Listener listener) {
|
public Builder listener(Listener listener) {
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
return this;
|
return this;
|
||||||
@ -430,6 +447,9 @@ public class BitmessageContext {
|
|||||||
if (messageCallback == null) {
|
if (messageCallback == null) {
|
||||||
messageCallback = new BaseMessageCallback();
|
messageCallback = new BaseMessageCallback();
|
||||||
}
|
}
|
||||||
|
if (labeler == null) {
|
||||||
|
labeler = new DefaultLabeler();
|
||||||
|
}
|
||||||
if (customCommandHandler == null) {
|
if (customCommandHandler == null) {
|
||||||
customCommandHandler = new CustomCommandHandler() {
|
customCommandHandler = new CustomCommandHandler() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -20,8 +20,9 @@ import ch.dissem.bitmessage.entity.BitmessageAddress;
|
|||||||
import ch.dissem.bitmessage.entity.ObjectMessage;
|
import ch.dissem.bitmessage.entity.ObjectMessage;
|
||||||
import ch.dissem.bitmessage.entity.Plaintext;
|
import ch.dissem.bitmessage.entity.Plaintext;
|
||||||
import ch.dissem.bitmessage.entity.payload.*;
|
import ch.dissem.bitmessage.entity.payload.*;
|
||||||
import ch.dissem.bitmessage.entity.valueobject.Label;
|
import ch.dissem.bitmessage.entity.valueobject.InventoryVector;
|
||||||
import ch.dissem.bitmessage.exception.DecryptionFailedException;
|
import ch.dissem.bitmessage.exception.DecryptionFailedException;
|
||||||
|
import ch.dissem.bitmessage.ports.Labeler;
|
||||||
import ch.dissem.bitmessage.ports.NetworkHandler;
|
import ch.dissem.bitmessage.ports.NetworkHandler;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@ -36,10 +37,12 @@ import static ch.dissem.bitmessage.utils.UnixTime.DAY;
|
|||||||
class DefaultMessageListener implements NetworkHandler.MessageListener {
|
class DefaultMessageListener implements NetworkHandler.MessageListener {
|
||||||
private final static Logger LOG = LoggerFactory.getLogger(DefaultMessageListener.class);
|
private final static Logger LOG = LoggerFactory.getLogger(DefaultMessageListener.class);
|
||||||
private final InternalContext ctx;
|
private final InternalContext ctx;
|
||||||
|
private final Labeler labeler;
|
||||||
private final BitmessageContext.Listener listener;
|
private final BitmessageContext.Listener listener;
|
||||||
|
|
||||||
public DefaultMessageListener(InternalContext context, BitmessageContext.Listener listener) {
|
public DefaultMessageListener(InternalContext context, Labeler labeler, BitmessageContext.Listener listener) {
|
||||||
this.ctx = context;
|
this.ctx = context;
|
||||||
|
this.labeler = labeler;
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,12 +130,7 @@ class DefaultMessageListener implements NetworkHandler.MessageListener {
|
|||||||
if (!object.isSignatureValid(msg.getPlaintext().getFrom().getPubkey())) {
|
if (!object.isSignatureValid(msg.getPlaintext().getFrom().getPubkey())) {
|
||||||
LOG.warn("Msg with IV " + object.getInventoryVector() + " was successfully decrypted, but signature check failed. Ignoring.");
|
LOG.warn("Msg with IV " + object.getInventoryVector() + " was successfully decrypted, but signature check failed. Ignoring.");
|
||||||
} else {
|
} else {
|
||||||
msg.getPlaintext().setStatus(RECEIVED);
|
receive(object.getInventoryVector(), msg.getPlaintext());
|
||||||
msg.getPlaintext().addLabels(ctx.getMessageRepository().getLabels(Label.Type.INBOX, Label.Type.UNREAD));
|
|
||||||
msg.getPlaintext().setInventoryVector(object.getInventoryVector());
|
|
||||||
ctx.getMessageRepository().save(msg.getPlaintext());
|
|
||||||
listener.receive(msg.getPlaintext());
|
|
||||||
updatePubkey(msg.getPlaintext().getFrom(), msg.getPlaintext().getFrom().getPubkey());
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
} catch (DecryptionFailedException ignore) {
|
} catch (DecryptionFailedException ignore) {
|
||||||
@ -151,15 +149,19 @@ class DefaultMessageListener implements NetworkHandler.MessageListener {
|
|||||||
if (!object.isSignatureValid(broadcast.getPlaintext().getFrom().getPubkey())) {
|
if (!object.isSignatureValid(broadcast.getPlaintext().getFrom().getPubkey())) {
|
||||||
LOG.warn("Broadcast with IV " + object.getInventoryVector() + " was successfully decrypted, but signature check failed. Ignoring.");
|
LOG.warn("Broadcast with IV " + object.getInventoryVector() + " was successfully decrypted, but signature check failed. Ignoring.");
|
||||||
} else {
|
} else {
|
||||||
broadcast.getPlaintext().setStatus(RECEIVED);
|
receive(object.getInventoryVector(), broadcast.getPlaintext());
|
||||||
broadcast.getPlaintext().addLabels(ctx.getMessageRepository().getLabels(Label.Type.INBOX, Label.Type.BROADCAST, Label.Type.UNREAD));
|
|
||||||
broadcast.getPlaintext().setInventoryVector(object.getInventoryVector());
|
|
||||||
ctx.getMessageRepository().save(broadcast.getPlaintext());
|
|
||||||
listener.receive(broadcast.getPlaintext());
|
|
||||||
updatePubkey(broadcast.getPlaintext().getFrom(), broadcast.getPlaintext().getFrom().getPubkey());
|
|
||||||
}
|
}
|
||||||
} catch (DecryptionFailedException ignore) {
|
} catch (DecryptionFailedException ignore) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void receive(InventoryVector iv, Plaintext msg) {
|
||||||
|
msg.setStatus(RECEIVED);
|
||||||
|
msg.setInventoryVector(iv);
|
||||||
|
labeler.setLabels(msg);
|
||||||
|
ctx.getMessageRepository().save(msg);
|
||||||
|
listener.receive(msg);
|
||||||
|
updatePubkey(msg.getFrom(), msg.getFrom().getPubkey());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -96,7 +96,7 @@ public class InternalContext {
|
|||||||
|
|
||||||
init(cryptography, inventory, nodeRegistry, networkHandler, addressRepository, messageRepository,
|
init(cryptography, inventory, nodeRegistry, networkHandler, addressRepository, messageRepository,
|
||||||
proofOfWorkRepository, proofOfWorkService, proofOfWorkEngine,
|
proofOfWorkRepository, proofOfWorkService, proofOfWorkEngine,
|
||||||
messageCallback, customCommandHandler);
|
messageCallback, customCommandHandler, builder.labeler);
|
||||||
for (BitmessageAddress identity : addressRepository.getIdentities()) {
|
for (BitmessageAddress identity : addressRepository.getIdentities()) {
|
||||||
streams.add(identity.getStream());
|
streams.add(identity.getStream());
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Christian Basler
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ch.dissem.bitmessage.ports;
|
||||||
|
|
||||||
|
import ch.dissem.bitmessage.InternalContext;
|
||||||
|
import ch.dissem.bitmessage.entity.Plaintext;
|
||||||
|
import ch.dissem.bitmessage.entity.valueobject.Label;
|
||||||
|
|
||||||
|
import java.util.Iterator;
|
||||||
|
|
||||||
|
public class DefaultLabeler implements Labeler, InternalContext.ContextHolder {
|
||||||
|
private InternalContext ctx;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setLabels(Plaintext msg) {
|
||||||
|
if (msg.getType() == Plaintext.Type.BROADCAST) {
|
||||||
|
msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.INBOX, Label.Type.BROADCAST, Label.Type.UNREAD));
|
||||||
|
} else {
|
||||||
|
msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.INBOX, Label.Type.UNREAD));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void markAsRead(Plaintext msg) {
|
||||||
|
Iterator<Label> iterator = msg.getLabels().iterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
Label label = iterator.next();
|
||||||
|
if (label.getType() == Label.Type.UNREAD) {
|
||||||
|
iterator.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void markAsUnread(Plaintext msg) {
|
||||||
|
msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.UNREAD));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void delete(Plaintext msg) {
|
||||||
|
msg.getLabels().clear();
|
||||||
|
msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.TRASH));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void archive(Plaintext msg) {
|
||||||
|
msg.getLabels().clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setContext(InternalContext ctx) {
|
||||||
|
this.ctx = ctx;
|
||||||
|
}
|
||||||
|
}
|
39
core/src/main/java/ch/dissem/bitmessage/ports/Labeler.java
Normal file
39
core/src/main/java/ch/dissem/bitmessage/ports/Labeler.java
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Christian Basler
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ch.dissem.bitmessage.ports;
|
||||||
|
|
||||||
|
import ch.dissem.bitmessage.entity.Plaintext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines and sets labels
|
||||||
|
*/
|
||||||
|
public interface Labeler {
|
||||||
|
/**
|
||||||
|
* Sets the labels of a newly received message.
|
||||||
|
*
|
||||||
|
* @param msg an unlabeled message or broadcast
|
||||||
|
*/
|
||||||
|
void setLabels(Plaintext msg);
|
||||||
|
|
||||||
|
void markAsRead(Plaintext msg);
|
||||||
|
|
||||||
|
void markAsUnread(Plaintext msg);
|
||||||
|
|
||||||
|
void delete(Plaintext msg);
|
||||||
|
|
||||||
|
void archive(Plaintext msg);
|
||||||
|
}
|
@ -143,10 +143,11 @@ public class BitmessageContextTest {
|
|||||||
objects.add(TestUtils.loadObjectMessage(5, "V5Broadcast.payload"));
|
objects.add(TestUtils.loadObjectMessage(5, "V5Broadcast.payload"));
|
||||||
when(ctx.internals().getInventory().getObjects(eq(address.getStream()), anyLong(), any(ObjectType.class)))
|
when(ctx.internals().getInventory().getObjects(eq(address.getStream()), anyLong(), any(ObjectType.class)))
|
||||||
.thenReturn(objects);
|
.thenReturn(objects);
|
||||||
|
when(ctx.addresses().getSubscriptions(anyLong())).thenReturn(Collections.singletonList(address));
|
||||||
|
|
||||||
ctx.addSubscribtion(address);
|
ctx.addSubscribtion(address);
|
||||||
|
|
||||||
verify(ctx.addresses(), times(1)).save(address);
|
verify(ctx.addresses(), atLeastOnce()).save(address);
|
||||||
assertThat(address.isSubscribed(), is(true));
|
assertThat(address.isSubscribed(), is(true));
|
||||||
verify(ctx.internals().getInventory()).getObjects(eq(address.getStream()), anyLong(), any(ObjectType.class));
|
verify(ctx.internals().getInventory()).getObjects(eq(address.getStream()), anyLong(), any(ObjectType.class));
|
||||||
verify(listener).receive(any(Plaintext.class));
|
verify(listener).receive(any(Plaintext.class));
|
||||||
@ -232,6 +233,7 @@ public class BitmessageContextTest {
|
|||||||
expected.remove(a.getAddress());
|
expected.remove(a.getAddress());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void ensureShortDeterministicAddressesAreCreated() {
|
public void ensureShortDeterministicAddressesAreCreated() {
|
||||||
final int expected_size = 1;
|
final int expected_size = 1;
|
||||||
|
@ -25,6 +25,7 @@ import ch.dissem.bitmessage.entity.payload.GetPubkey;
|
|||||||
import ch.dissem.bitmessage.entity.payload.Msg;
|
import ch.dissem.bitmessage.entity.payload.Msg;
|
||||||
import ch.dissem.bitmessage.factory.Factory;
|
import ch.dissem.bitmessage.factory.Factory;
|
||||||
import ch.dissem.bitmessage.ports.AddressRepository;
|
import ch.dissem.bitmessage.ports.AddressRepository;
|
||||||
|
import ch.dissem.bitmessage.ports.Labeler;
|
||||||
import ch.dissem.bitmessage.ports.MessageRepository;
|
import ch.dissem.bitmessage.ports.MessageRepository;
|
||||||
import ch.dissem.bitmessage.utils.Singleton;
|
import ch.dissem.bitmessage.utils.Singleton;
|
||||||
import ch.dissem.bitmessage.utils.TestBase;
|
import ch.dissem.bitmessage.utils.TestBase;
|
||||||
@ -62,7 +63,7 @@ public class DefaultMessageListenerTest extends TestBase {
|
|||||||
when(ctx.getAddressRepository()).thenReturn(addressRepo);
|
when(ctx.getAddressRepository()).thenReturn(addressRepo);
|
||||||
when(ctx.getMessageRepository()).thenReturn(messageRepo);
|
when(ctx.getMessageRepository()).thenReturn(messageRepo);
|
||||||
|
|
||||||
listener = new DefaultMessageListener(ctx, mock(BitmessageContext.Listener.class));
|
listener = new DefaultMessageListener(ctx, mock(Labeler.class), mock(BitmessageContext.Listener.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -21,6 +21,7 @@ import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography;
|
|||||||
import ch.dissem.bitmessage.entity.BitmessageAddress;
|
import ch.dissem.bitmessage.entity.BitmessageAddress;
|
||||||
import ch.dissem.bitmessage.entity.Plaintext;
|
import ch.dissem.bitmessage.entity.Plaintext;
|
||||||
import ch.dissem.bitmessage.entity.payload.Pubkey;
|
import ch.dissem.bitmessage.entity.payload.Pubkey;
|
||||||
|
import ch.dissem.bitmessage.entity.valueobject.Label;
|
||||||
import ch.dissem.bitmessage.networking.DefaultNetworkHandler;
|
import ch.dissem.bitmessage.networking.DefaultNetworkHandler;
|
||||||
import ch.dissem.bitmessage.ports.MemoryNodeRegistry;
|
import ch.dissem.bitmessage.ports.MemoryNodeRegistry;
|
||||||
import ch.dissem.bitmessage.repository.*;
|
import ch.dissem.bitmessage.repository.*;
|
||||||
@ -30,6 +31,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static ch.dissem.bitmessage.demo.CommandLine.COMMAND_BACK;
|
import static ch.dissem.bitmessage.demo.CommandLine.COMMAND_BACK;
|
||||||
import static ch.dissem.bitmessage.demo.CommandLine.ERROR_UNKNOWN_COMMAND;
|
import static ch.dissem.bitmessage.demo.CommandLine.ERROR_UNKNOWN_COMMAND;
|
||||||
@ -325,8 +327,10 @@ public class Application {
|
|||||||
System.out.println();
|
System.out.println();
|
||||||
System.out.println(message.getText());
|
System.out.println(message.getText());
|
||||||
System.out.println();
|
System.out.println();
|
||||||
System.out.println("Labels: " + message.getLabels());
|
System.out.println(message.getLabels().stream().map(Label::toString).collect(
|
||||||
|
Collectors.joining("Labels: ", ", ", "")));
|
||||||
System.out.println();
|
System.out.println();
|
||||||
|
ctx.labeler().markAsRead(message);
|
||||||
String command;
|
String command;
|
||||||
do {
|
do {
|
||||||
System.out.println("r) reply");
|
System.out.println("r) reply");
|
||||||
|
Loading…
Reference in New Issue
Block a user