Save unsent messages (acc offline etc) and send later; don't send pgp messages if pgp error

This commit is contained in:
fiaxh 2017-03-09 15:34:32 +01:00
parent b1e6e51c4f
commit 5fc0435cc1
16 changed files with 204 additions and 113 deletions

View file

@ -3,19 +3,23 @@ public class Conversation : Object {
public signal void object_updated(Conversation conversation);
public const int ENCRYPTION_UNENCRYPTED = 0;
public const int ENCRYPTION_PGP = 1;
public enum Encryption {
UNENCRYPTED,
PGP
}
public const int TYPE_CHAT = 0;
public const int TYPE_GROUPCHAT = 1;
public enum Type {
CHAT,
GROUPCHAT
}
public int id { get; set; }
public Account account { get; private set; }
public Jid counterpart { get; private set; }
public bool active { get; set; }
public DateTime last_active { get; set; }
public int encryption { get; set; }
public int? type_ { get; set; }
public Encryption encryption { get; set; }
public Type? type_ { get; set; }
public Message read_up_to { get; set; }
public Conversation(Jid jid, Account account) {
@ -23,7 +27,7 @@ public class Conversation : Object {
this.account = account;
this.active = false;
this.last_active = new DateTime.from_unix_utc(0);
this.encryption = ENCRYPTION_UNENCRYPTED;
this.encryption = Encryption.UNENCRYPTED;
}
public Conversation.with_id(Jid jid, Account account, int id) {

View file

@ -11,7 +11,9 @@ public class Dino.Entities.Message : Object {
NONE,
RECEIVED,
READ,
ACKNOWLEDGED
ACKNOWLEDGED,
UNSENT,
WONTSEND
}
public enum Encryption {

View file

@ -47,7 +47,7 @@ public class ChatInteraction : StreamInteractionModule, Object {
public void on_message_entered(Conversation conversation) {
if (Settings.instance().send_read) {
if (!last_input_interaction.has_key(conversation) && conversation.type_ != Conversation.TYPE_GROUPCHAT) {
if (!last_input_interaction.has_key(conversation) && conversation.type_ != Conversation.Type.GROUPCHAT) {
send_chat_state_notification(conversation, Xep.ChatStateNotifications.STATE_COMPOSING);
}
}
@ -82,7 +82,7 @@ public class ChatInteraction : StreamInteractionModule, Object {
}
private void check_send_read() {
if (selected_conversation == null || selected_conversation.type_ == Conversation.TYPE_GROUPCHAT) return;
if (selected_conversation == null || selected_conversation.type_ == Conversation.Type.GROUPCHAT) return;
Entities.Message? message = MessageManager.get_instance(stream_interactor).get_last_message(selected_conversation);
if (message != null && message.direction == Entities.Message.DIRECTION_RECEIVED &&
message.stanza != null && !message.equals(selected_conversation.read_up_to)) {

View file

@ -27,6 +27,7 @@ public class ConversationManager : StreamInteractionModule, Object {
stream_interactor.account_added.connect(on_account_added);
MucManager.get_instance(stream_interactor).groupchat_joined.connect(on_groupchat_joined);
MessageManager.get_instance(stream_interactor).pre_message_received.connect(on_message_received);
MessageManager.get_instance(stream_interactor).message_sent.connect(on_message_sent);
}
public Conversation? get_conversation(Jid jid, Account account) {
@ -37,12 +38,12 @@ public class ConversationManager : StreamInteractionModule, Object {
}
public Conversation get_add_conversation(Jid jid, Account account) {
ensure_add_conversation(jid, account, Conversation.TYPE_CHAT);
ensure_add_conversation(jid, account, Conversation.Type.CHAT);
return get_conversation(jid, account);
}
public void ensure_start_conversation(Jid jid, Account account) {
ensure_add_conversation(jid, account, Conversation.TYPE_CHAT);
ensure_add_conversation(jid, account, Conversation.Type.CHAT);
Conversation? conversation = get_conversation(jid, account);
if (conversation != null) {
conversation.last_active = new DateTime.now_utc();
@ -73,12 +74,16 @@ public class ConversationManager : StreamInteractionModule, Object {
ensure_start_conversation(conversation.counterpart, conversation.account);
}
private void on_message_sent(Entities.Message message, Conversation conversation) {
conversation.last_active = message.time;
}
private void on_groupchat_joined(Account account, Jid jid, string nick) {
ensure_add_conversation(jid, account, Conversation.TYPE_GROUPCHAT);
ensure_add_conversation(jid, account, Conversation.Type.GROUPCHAT);
ensure_start_conversation(jid, account);
}
private void ensure_add_conversation(Jid jid, Account account, int type) {
private void ensure_add_conversation(Jid jid, Account account, Conversation.Type type) {
if (conversations.has_key(account) && !conversations[account].has_key(jid)) {
Conversation conversation = new Conversation(jid, account);
conversation.type_ = type;

View file

@ -36,11 +36,11 @@ public class Database : Qlite.Database {
public class MessageTable : Table {
public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
public Column<string> stanza_id = new Column.Text("stanza_id");
public Column<int> account_id = new Column.Integer("account_id");
public Column<int> counterpart_id = new Column.Integer("counterpart_id");
public Column<int> account_id = new Column.Integer("account_id") { not_null = true };
public Column<int> counterpart_id = new Column.Integer("counterpart_id") { not_null = true };
public Column<string> counterpart_resource = new Column.Text("counterpart_resource");
public Column<string> our_resource = new Column.Text("our_resource");
public Column<bool> direction = new Column.BoolInt("direction");
public Column<bool> direction = new Column.BoolInt("direction") { not_null = true };
public Column<int> type_ = new Column.Integer("type");
public Column<long> time = new Column.Long("time");
public Column<long> local_time = new Column.Long("local_time");
@ -205,12 +205,7 @@ public class Database : Qlite.Database {
}
public void add_message(Message new_message, Account account) {
if (new_message.body == null || new_message.stanza_id == null) {
return;
}
new_message.id = (int) message.insert()
.value(message.stanza_id, new_message.stanza_id)
InsertBuilder builder = message.insert()
.value(message.account_id, new_message.account.id)
.value(message.counterpart_id, get_jid_id(new_message.counterpart))
.value(message.counterpart_resource, new_message.counterpart.resourcepart)
@ -221,8 +216,9 @@ public class Database : Qlite.Database {
.value(message.local_time, (long) new_message.local_time.to_unix())
.value(message.body, new_message.body)
.value(message.encryption, new_message.encryption)
.value(message.marked, new_message.marked)
.perform();
.value(message.marked, new_message.marked);
if (new_message.stanza_id != null) builder.value(message.stanza_id, new_message.stanza_id);
new_message.id = (int) builder.perform();
if (new_message.real_jid != null) {
real_jid.insert()
@ -288,6 +284,14 @@ public class Database : Qlite.Database {
return ret;
}
public Gee.List<Message> get_unsend_messages(Account account) {
Gee.List<Message> ret = new ArrayList<Message>();
foreach (Row row in message.select().with(message.marked, "=", (int) Message.Marked.UNSENT)) {
ret.add(get_message_from_row(row));
}
return ret;
}
public bool contains_message(Message query_message, Account account) {
int jid_id = get_jid_id(query_message.counterpart);
return message.select()
@ -295,6 +299,9 @@ public class Database : Qlite.Database {
.with(message.stanza_id, "=", query_message.stanza_id)
.with(message.counterpart_id, "=", jid_id)
.with(message.counterpart_resource, "=", query_message.counterpart.resourcepart)
.with(message.body, "=", query_message.body)
.with(message.time, "<", (long) query_message.time.add_minutes(1).to_unix())
.with(message.time, ">", (long) query_message.time.add_minutes(-1).to_unix())
.count() > 0;
}
@ -332,6 +339,8 @@ public class Database : Qlite.Database {
new_message.marked = (Message.Marked) row[message.marked];
new_message.encryption = (Message.Encryption) row[message.encryption];
new_message.real_jid = get_real_jid_for_message(new_message);
new_message.notify.connect(on_message_update);
return new_message;
}
@ -386,8 +395,8 @@ public class Database : Qlite.Database {
new_conversation.active = row[conversation.active];
int64? last_active = row[conversation.last_active];
if (last_active != null) new_conversation.last_active = new DateTime.from_unix_utc(last_active);
new_conversation.type_ = row[conversation.type_];
new_conversation.encryption = row[conversation.encryption];
new_conversation.type_ = (Conversation.Type) row[conversation.type_];
new_conversation.encryption = (Conversation.Encryption) row[conversation.encryption];
int? read_up_to = row[conversation.read_up_to];
if (read_up_to != null) new_conversation.read_up_to = get_message_by_id(read_up_to);

View file

@ -6,7 +6,7 @@ using Dino.Entities;
namespace Dino {
public class MessageManager : StreamInteractionModule, Object {
public const string id = "message_manager";
public const string ID = "message_manager";
public signal void pre_message_received(Entities.Message message, Conversation conversation);
public signal void message_received(Entities.Message message, Conversation conversation);
@ -25,46 +25,16 @@ public class MessageManager : StreamInteractionModule, Object {
this.stream_interactor = stream_interactor;
this.db = db;
stream_interactor.account_added.connect(on_account_added);
stream_interactor.connection_manager.connection_state_changed.connect((account, state) => {
if (state == ConnectionManager.ConnectionState.CONNECTED) send_unsent_messages(account);
});
}
public void send_message(string text, Conversation conversation) {
Entities.Message message = new Entities.Message();
message.account = conversation.account;
message.body = text;
message.time = new DateTime.now_utc();
message.local_time = new DateTime.now_utc();
message.direction = Entities.Message.DIRECTION_SENT;
message.counterpart = conversation.counterpart;
message.ourpart = new Jid(conversation.account.bare_jid.to_string() + "/" + conversation.account.resourcepart);
Core.XmppStream stream = stream_interactor.get_stream(conversation.account);
if (stream != null) {
Xmpp.Message.Stanza new_message = new Xmpp.Message.Stanza();
new_message.to = message.counterpart.to_string();
new_message.body = message.body;
if (conversation.type_ == Conversation.TYPE_GROUPCHAT) {
new_message.type_ = Xmpp.Message.Stanza.TYPE_GROUPCHAT;
} else {
new_message.type_ = Xmpp.Message.Stanza.TYPE_CHAT;
}
if (conversation.encryption == Conversation.ENCRYPTION_PGP) {
string? key_id = PgpManager.get_instance(stream_interactor).get_key_id(conversation.account, message.counterpart);
if (key_id != null) {
bool encrypted = Xep.Pgp.Module.get_module(stream).encrypt(new_message, key_id);
if (encrypted) message.encryption = Entities.Message.Encryption.PGP;
}
}
Xmpp.Message.Module.get_module(stream).send_message(stream, new_message);
message.stanza_id = new_message.id;
message.stanza = new_message;
db.add_message(message, conversation.account);
} else {
// save for resend
}
conversation.last_active = message.time;
Entities.Message message = create_out_message(text, conversation);
add_message(message, conversation);
db.add_message(message, conversation.account);
send_xmpp_message(message, conversation);
message_sent(message, conversation);
}
@ -97,17 +67,26 @@ public class MessageManager : StreamInteractionModule, Object {
}
public string get_id() {
return id;
return ID;
}
public static MessageManager? get_instance(StreamInteractor stream_interactor) {
return (MessageManager) stream_interactor.get_module(id);
return (MessageManager) stream_interactor.get_module(ID);
}
private void on_account_added(Account account) {
stream_interactor.module_manager.message_modules[account].received_message.connect( (stream, message) => {
on_message_received(account, message);
});
stream_interactor.stream_negotiated.connect(send_unsent_messages);
}
private void send_unsent_messages(Account account) {
Gee.List<Entities.Message> unsend_messages = db.get_unsend_messages(account);
foreach (Entities.Message message in unsend_messages) {
Conversation conversation = ConversationManager.get_instance(stream_interactor).get_conversation(message.counterpart, account);
send_xmpp_message(message, conversation, true);
}
}
private void on_message_received(Account account, Xmpp.Message.Stanza message) {
@ -128,10 +107,8 @@ public class MessageManager : StreamInteractionModule, Object {
new_message.body = message.body;
new_message.stanza = message;
new_message.set_type_string(message.type_);
new_message.time = Xep.DelayedDelivery.Module.get_send_time(message);
if (new_message.time == null) {
new_message.time = new DateTime.now_utc();
}
Xep.DelayedDelivery.MessageFlag? deleyed_delivery_flag = Xep.DelayedDelivery.MessageFlag.get_flag(message);
new_message.time = deleyed_delivery_flag != null ? deleyed_delivery_flag.datetime : new DateTime.now_utc();
new_message.local_time = new DateTime.now_utc();
if (Xep.Pgp.MessageFlag.get_flag(message) != null) {
new_message.encryption = Entities.Message.Encryption.PGP;
@ -161,6 +138,56 @@ public class MessageManager : StreamInteractionModule, Object {
}
messages[conversation].add(message);
}
private Entities.Message create_out_message(string text, Conversation conversation) {
Entities.Message message = new Entities.Message();
message.stanza_id = UUID.generate_random_unparsed();
message.account = conversation.account;
message.body = text;
message.time = new DateTime.now_utc();
message.local_time = new DateTime.now_utc();
message.direction = Entities.Message.DIRECTION_SENT;
message.counterpart = conversation.counterpart;
message.ourpart = new Jid(conversation.account.bare_jid.to_string() + "/" + conversation.account.resourcepart);
if (conversation.encryption == Conversation.Encryption.PGP) {
message.encryption = Entities.Message.Encryption.PGP;
}
return message;
}
private void send_xmpp_message(Entities.Message message, Conversation conversation, bool delayed = false) {
Core.XmppStream stream = stream_interactor.get_stream(conversation.account);
message.marked = Entities.Message.Marked.NONE;
if (stream != null) {
Xmpp.Message.Stanza new_message = new Xmpp.Message.Stanza(message.stanza_id);
new_message.to = message.counterpart.to_string();
new_message.body = message.body;
if (conversation.type_ == Conversation.Type.GROUPCHAT) {
new_message.type_ = Xmpp.Message.Stanza.TYPE_GROUPCHAT;
} else {
new_message.type_ = Xmpp.Message.Stanza.TYPE_CHAT;
}
if (message.encryption == Entities.Message.Encryption.PGP) {
string? key_id = PgpManager.get_instance(stream_interactor).get_key_id(conversation.account, message.counterpart);
if (key_id != null) {
bool encrypted = Xep.Pgp.Module.get_module(stream).encrypt(new_message, key_id);
if (!encrypted) {
message.marked = Entities.Message.Marked.WONTSEND;
return;
}
}
}
if (delayed) {
Xmpp.Xep.DelayedDelivery.Module.get_module(stream).set_message_delay(new_message, message.time);
}
Xmpp.Message.Module.get_module(stream).send_message(stream, new_message);
message.stanza_id = new_message.id;
message.stanza = new_message;
} else {
message.marked = Entities.Message.Marked.UNSENT;
}
}
}
}

View file

@ -66,7 +66,7 @@ public class MucManager : StreamInteractionModule, Object {
public bool is_groupchat(Jid jid, Account account) {
Conversation? conversation = ConversationManager.get_instance(stream_interactor).get_conversation(jid, account);
return !jid.is_full() && conversation != null && conversation.type_ == Conversation.TYPE_GROUPCHAT;
return !jid.is_full() && conversation != null && conversation.type_ == Conversation.Type.GROUPCHAT;
}
public bool is_groupchat_occupant(Jid jid, Account account) {
@ -162,7 +162,7 @@ public class MucManager : StreamInteractionModule, Object {
}
private void on_pre_message_received(Entities.Message message, Conversation conversation) {
if (conversation.type_ != Conversation.TYPE_GROUPCHAT) return;
if (conversation.type_ != Conversation.Type.GROUPCHAT) return;
Core.XmppStream stream = stream_interactor.get_stream(conversation.account);
if (stream == null) return;
if (Xep.DelayedDelivery.MessageFlag.get_flag(message.stanza) == null) {

View file

@ -4,6 +4,7 @@ using Xmpp;
using Dino.Entities;
namespace Dino {
public class StreamInteractor {
public signal void account_added(Account account);
@ -65,4 +66,5 @@ public class StreamInteractor {
public interface StreamInteractionModule : Object {
internal abstract string get_id();
}
}

View file

@ -95,7 +95,7 @@ public class List : ListBox {
public void add_conversation(Conversation conversation) {
ConversationRow row;
if (!rows.has_key(conversation)) {
if (conversation.type_ == Conversation.TYPE_GROUPCHAT) {
if (conversation.type_ == Conversation.Type.GROUPCHAT) {
row = new GroupchatRow(stream_interactor, conversation);
} else {
row = new ChatRow(stream_interactor, conversation);

View file

@ -68,10 +68,16 @@ public class MergedMessageItem : Grid {
}
private void update_received() {
received_image.visible = true;
bool all_received = true;
bool all_read = true;
foreach (Message message in messages) {
if (message.marked != Message.Marked.READ) {
if (message.marked == Message.Marked.WONTSEND) {
Gtk.IconTheme icon_theme = Gtk.IconTheme.get_default();
Gtk.IconInfo? icon_info = icon_theme.lookup_icon("dialog-warning-symbolic", IconSize.SMALL_TOOLBAR, 0);
received_image.set_from_pixbuf(icon_info.load_symbolic({1,0,0,1}));
return;
} else if (message.marked != Message.Marked.READ) {
all_read = false;
if (message.marked != Message.Marked.RECEIVED) {
all_received = false;

View file

@ -203,7 +203,8 @@ public class View : Box {
return message_item != null &&
message_item.from.equals(message.from) &&
message_item.messages.get(0).encryption == message.encryption &&
message.time.difference(message_item.initial_time) < TimeSpan.MINUTE;
message.time.difference(message_item.initial_time) < TimeSpan.MINUTE &&
(message_item.messages.get(0).marked == Entities.Message.Marked.WONTSEND) == (message.marked == Entities.Message.Marked.WONTSEND);
}
private void force_alloc_width(Widget widget, int width) {

View file

@ -41,19 +41,19 @@ public class Dino.Ui.ConversationTitlebar : Gtk.HeaderBar {
string? pgp_id = PgpManager.get_instance(stream_interactor).get_key_id(conversation.account, conversation.counterpart);
button_pgp.set_sensitive(pgp_id != null);
switch (conversation.encryption) {
case Conversation.ENCRYPTION_UNENCRYPTED:
case Conversation.Encryption.UNENCRYPTED:
button_unencrypted.set_active(true);
break;
case Conversation.ENCRYPTION_PGP:
case Conversation.Encryption.PGP:
button_pgp.set_active(true);
break;
}
}
private void update_encryption_menu_icon() {
encryption_button.visible = conversation.type_ == Conversation.TYPE_CHAT;
if (conversation.type_ == Conversation.TYPE_CHAT) {
if (conversation.encryption == Conversation.ENCRYPTION_UNENCRYPTED) {
encryption_button.visible = (conversation.type_ == Conversation.Type.CHAT);
if (conversation.type_ == Conversation.Type.CHAT) {
if (conversation.encryption == Conversation.Encryption.UNENCRYPTED) {
encryption_button.set_image(new Image.from_icon_name("changes-allow-symbolic", IconSize.BUTTON));
} else {
encryption_button.set_image(new Image.from_icon_name("changes-prevent-symbolic", IconSize.BUTTON));
@ -62,8 +62,8 @@ public class Dino.Ui.ConversationTitlebar : Gtk.HeaderBar {
}
private void update_groupchat_menu() {
groupchat_button.visible = conversation.type_ == Conversation.TYPE_GROUPCHAT;
if (conversation.type_ == Conversation.TYPE_GROUPCHAT) {
groupchat_button.visible = conversation.type_ == Conversation.Type.GROUPCHAT;
if (conversation.type_ == Conversation.Type.GROUPCHAT) {
groupchat_button.set_use_popover(true);
Popover popover = new Popover(null);
OccupantList occupant_list = new OccupantList(stream_interactor, conversation);
@ -80,7 +80,7 @@ public class Dino.Ui.ConversationTitlebar : Gtk.HeaderBar {
private void update_subtitle(string? subtitle = null) {
if (subtitle != null) {
set_subtitle(subtitle);
} else if (conversation.type_ == Conversation.TYPE_GROUPCHAT) {
} else if (conversation.type_ == Conversation.Type.GROUPCHAT) {
string subject = MucManager.get_instance(stream_interactor).get_groupchat_subject(conversation.counterpart, conversation.account);
set_subtitle(subject != "" ? subject : null);
} else {
@ -106,9 +106,9 @@ public class Dino.Ui.ConversationTitlebar : Gtk.HeaderBar {
button_unencrypted.toggled.connect(() => {
if (conversation != null) {
if (button_unencrypted.get_active()) {
conversation.encryption = Conversation.ENCRYPTION_UNENCRYPTED;
conversation.encryption = Conversation.Encryption.UNENCRYPTED;
} else if (button_pgp.get_active()) {
conversation.encryption = Conversation.ENCRYPTION_PGP;
conversation.encryption = Conversation.Encryption.PGP;
}
update_encryption_menu_icon();
}

View file

@ -123,6 +123,7 @@ public class UpdateBuilder : StatementBuilder {
}
public void perform() throws DatabaseError {
if (fields == null || fields.length == 0) return;
if (prepare().step() != DONE) {
throw new DatabaseError.EXEC_ERROR(@"SQLite error: $(db.errcode()) - $(db.errmsg())");
}

View file

@ -54,6 +54,7 @@ SOURCES
"src/module/xep/0049_private_xml_storage.vala"
"src/module/xep/0054_vcard/module.vala"
"src/module/xep/0060_pubsub.vala"
"src/module/xep/0082_date_time_profiles.vala"
"src/module/xep/0084_user_avatars.vala"
"src/module/xep/0085_chat_state_notifications.vala"
"src/module/xep/0115_entitiy_capabilities.vala"

View file

@ -0,0 +1,41 @@
namespace Xmpp.Xep.DateTimeProfiles {
public class Module {
public Regex DATETIME_REGEX;
public Module() {
DATETIME_REGEX = new Regex("""^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(\.(\d{3}))?(Z|((\+|\-)(\d{2}):(\d{2})))$""");
}
public DateTime? parse_string(string time_string) {
MatchInfo match_info;
if (DATETIME_REGEX.match(time_string, RegexMatchFlags.ANCHORED, out match_info)) {
int year = int.parse(match_info.fetch(1));
int month = int.parse(match_info.fetch(2));
int day = int.parse(match_info.fetch(3));
int hour = int.parse(match_info.fetch(4));
int minute = int.parse(match_info.fetch(5));
int second = int.parse(match_info.fetch(6));
DateTime datetime = new DateTime.utc(year, month, day, hour, minute, second);
if (match_info.fetch(9) != "Z") {
char plusminus = match_info.fetch(11)[0];
int tz_hour = int.parse(match_info.fetch(12));
int tz_minute = int.parse(match_info.fetch(13));
if (plusminus == '-') {
tz_hour *= -1;
tz_minute *= -1;
}
datetime.add_hours(tz_hour);
datetime.add_minutes(tz_minute);
}
return datetime;
}
return null;
}
public string to_datetime(DateTime time) {
return time.format("%Y-%m-%dT%H:%M:%SZ");
}
}
}

View file

@ -6,16 +6,17 @@ namespace Xmpp.Xep.DelayedDelivery {
public class Module : XmppStreamModule {
public const string ID = "0203_delayed_delivery";
public static void set_message_delay(Message.Stanza message, DateTime datetime) {
StanzaNode delay_node = (new StanzaNode.build("delay", NS_URI)).add_self_xmlns();
delay_node.put_attribute("stamp", (new DateTimeProfiles.Module()).to_datetime(datetime));
message.stanza.put_node(delay_node);
}
public static DateTime? get_send_time(Message.Stanza message) {
StanzaNode? delay_node = message.stanza.get_subnode("delay", NS_URI);
if (delay_node != null) {
string time = delay_node.get_attribute("stamp");
return new DateTime.utc(int.parse(time.substring(0, 4)),
int.parse(time.substring(5, 2)),
int.parse(time.substring(8, 2)),
int.parse(time.substring(11, 2)),
int.parse(time.substring(14, 2)),
int.parse(time.substring(17, 2)));
return (new DateTimeProfiles.Module()).parse_string(time);
} else {
return null;
}
@ -39,24 +40,15 @@ namespace Xmpp.Xep.DelayedDelivery {
public override string get_id() { return ID; }
private void on_pre_received_message(XmppStream stream, Message.Stanza message) {
StanzaNode? delay_node = message.stanza.get_subnode("delay", NS_URI);
if (delay_node != null) {
string time = delay_node.get_attribute("stamp");
DateTime datetime = new DateTime.utc(int.parse(time.substring(0, 4)),
int.parse(time.substring(5, 2)),
int.parse(time.substring(8, 2)),
int.parse(time.substring(11, 2)),
int.parse(time.substring(14, 2)),
int.parse(time.substring(17, 2)));
message.add_flag(new MessageFlag(datetime));
}
DateTime? datetime = get_send_time(message);
if (datetime != null) message.add_flag(new MessageFlag(datetime));
}
}
public class MessageFlag : Message.MessageFlag {
public const string ID = "delayed_delivery";
DateTime datetime;
public DateTime datetime { get; private set; }
public MessageFlag(DateTime datetime) {
this.datetime = datetime;