Compare commits

...

23 commits
master ... v0.4

Author SHA1 Message Date
Marvin W df0013e3f4
Fix potential crash in video calls 2023-07-09 18:24:34 +02:00
Marvin W 0521f50146
Fix certificate start time
I doubt anyone ever looked at it, but it shouldn't be 1 day in the future ;)
2023-07-09 18:24:34 +02:00
Marvin W 92b7d97339
Do not send DTLS datagrams to RTP even after handshake
Also post debug message in case we drop datagrams
2023-07-09 18:24:34 +02:00
Stephen Paul Weber 7c048ecaef
Ignore non-DTLS data before handshake is complete
https://datatracker.ietf.org/doc/html/rfc9147#name-demul
https://datatracker.ietf.org/doc/html/rfc5764#section-5.1.2

If data is received before handshake is complete, discard it rather than
forwarding it blindly to GnuTLS which can get confused.
2023-07-09 18:24:34 +02:00
Robert Mader da383a2fe9
data: Set X-Purism-FormFactor in .desktop file
So the app is detected as mobile-friendly on Phosh.
2023-07-09 18:24:15 +02:00
Marvin W 3a925c4c00
Fix reactions being made to the wrong message
fixes #1426
2023-07-09 18:23:43 +02:00
fiaxh 0ed0495e0a Fix chat input for IME
fixes #1419

Co-authored-by: Marvin W <git@larma.de>
2023-05-14 14:39:20 +02:00
fiaxh 569d4141cf Fix chat input status having a fixed width requirement
fixes #1439
2023-05-14 14:39:20 +02:00
fiaxh f44ded88c8 Fix character counting for fallbacks
fixes #1420
2023-05-14 14:39:20 +02:00
Karim Malhas a74b894147 Focus ChatInput textbox after selecting emoji
After selecting an emoji, the emoji is inserted
into the textbox, but focus remains on the emoji_button.

This causes the EmojiChooser to be opened again if a user
hits the Enter key directly, but text is inserted into the textbox
if they continue to type.

This commit just explicitely focuses on the textbox after
an emoji has been selected.
2023-05-14 14:39:20 +02:00
fiaxh f011adac37 Fix crash on NS_URI call when own server has no MAM; drop broken mam:1 "support"
fixes #1405
2023-05-14 14:39:20 +02:00
Marvin W 58379acc9f Fix empty alias being handled different than none 2023-05-14 14:39:20 +02:00
fiaxh 2fb8d29b34 Fix call window styling 2023-05-14 14:39:20 +02:00
fiaxh 7a27634732 Fix call window controlls hiding 2023-05-14 14:39:20 +02:00
Marvin W d155ec15d2 Fix video for cameras with rotated image 2023-05-14 14:39:20 +02:00
Marvin W baf96d9d9f
Check sender of bookmark:1 updates 2023-03-23 12:06:36 -06:00
Marvin W 179c766d19
Bind soup session lifetime to File provider/sender lifetime
Required since libsoup 3.4. Fixes #1395
2023-03-22 13:22:23 -06:00
Bohdan Horbeshko 004824040d
Fix a crash if a message subnode is not found in a carbon
Fixes #1392
2023-03-22 09:59:55 -06:00
Michael Vetter b6f9b54d76
Remove gspell
7e7dcedaf ported from GTK3 to GTK4.
It also removed gspell from main/CMakeLists.txt.

I assume that gspell is not needed anymore and we can thus remove the
requirement from the CI and the cmake file as well.
2023-03-22 09:59:55 -06:00
Sebastian Krzyszkowiak 1738bf8dc8
data: Set StartupNotify to true in .desktop file
GTK handles startup notifications, so advertise it in desktop
file. This allows splash screens and other startup indications
in DEs to work.
2023-03-22 09:59:54 -06:00
Marvin W 481a68fd89
Improve database performance while reconnecting and syncing
Also move some tasks to low priority idle queue so they won't block UI updates
2023-03-22 09:59:54 -06:00
Marvin W 89b9110fcb
Improve history sync
- Ensure we fully fetch desired history if possible (previously, duplicates
  from offline message queue could hinder MAM sync)
- Early drop illegal MAM messages so they don't pile up in the pending queue
  waiting for their query to end (which it never will if they were not
  requested in first place).

Fixes #1386
2023-03-22 09:59:54 -06:00
Marvin W acf9c69470
Fix C binding for gst_video_frame_get_data
Fixes #1267
2023-03-22 09:59:54 -06:00
29 changed files with 306 additions and 162 deletions

View file

@ -7,7 +7,7 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- run: sudo apt-get update - run: sudo apt-get update
- run: sudo apt-get remove libunwind-14-dev - run: sudo apt-get remove libunwind-14-dev
- run: sudo apt-get install -y build-essential gettext cmake valac libgee-0.8-dev libsqlite3-dev libgtk-4-dev libnotify-dev libgpgme-dev libsoup2.4-dev libgcrypt20-dev libqrencode-dev libgspell-1-dev libnice-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libsrtp2-dev libwebrtc-audio-processing-dev libadwaita-1-dev - run: sudo apt-get install -y build-essential gettext cmake valac libgee-0.8-dev libsqlite3-dev libgtk-4-dev libnotify-dev libgpgme-dev libsoup2.4-dev libgcrypt20-dev libqrencode-dev libnice-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libsrtp2-dev libwebrtc-audio-processing-dev libadwaita-1-dev
- run: ./configure --with-tests --with-libsignal-in-tree - run: ./configure --with-tests --with-libsignal-in-tree
- run: make - run: make
- run: build/xmpp-vala-test - run: build/xmpp-vala-test

View file

@ -1,14 +0,0 @@
include(PkgConfigWithFallback)
find_pkg_config_with_fallback(Gspell
PKG_CONFIG_NAME gspell-1
LIB_NAMES gspell-1
INCLUDE_NAMES gspell.h
INCLUDE_DIR_SUFFIXES gspell-1 gspell-1/gspell
DEPENDS GTK3
)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(Gspell
REQUIRED_VARS Gspell_LIBRARY
VERSION_VAR Gspell_VERSION)

View file

@ -7,7 +7,7 @@ using Dino.Entities;
namespace Dino { namespace Dino {
public class Database : Qlite.Database { public class Database : Qlite.Database {
private const int VERSION = 25; private const int VERSION = 26;
public class AccountTable : Table { public class AccountTable : Table {
public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true }; public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
@ -93,6 +93,11 @@ public class Database : Qlite.Database {
// deduplication // deduplication
index("message_account_counterpart_stanzaid_idx", {account_id, counterpart_id, stanza_id}); index("message_account_counterpart_stanzaid_idx", {account_id, counterpart_id, stanza_id});
index("message_account_counterpart_serverid_idx", {account_id, counterpart_id, server_id});
// message by marked
index("message_account_marked_idx", {account_id, marked});
fts({body}); fts({body});
} }
} }

View file

@ -90,11 +90,9 @@ public class Dino.HistorySync {
if (!is_muc_mam && !from_our_server) return; if (!is_muc_mam && !from_our_server) return;
// Get the server time of the message and store it in `mam_times` // Get the server time of the message and store it in `mam_times`
Xmpp.MessageArchiveManagement.Flag? mam_flag = stream != null ? stream.get_flag(Xmpp.MessageArchiveManagement.Flag.IDENTITY) : null; string? id = message.stanza.get_deep_attribute(Xmpp.MessageArchiveManagement.NS_URI + ":result", "id");
if (mam_flag == null) return;
string? id = message.stanza.get_deep_attribute(mam_flag.ns_ver + ":result", "id");
if (id == null) return; if (id == null) return;
StanzaNode? delay_node = message.stanza.get_deep_subnode(mam_flag.ns_ver + ":result", StanzaForwarding.NS_URI + ":forwarded", DelayedDelivery.NS_URI + ":delay"); StanzaNode? delay_node = message.stanza.get_deep_subnode(Xmpp.MessageArchiveManagement.NS_URI + ":result", StanzaForwarding.NS_URI + ":forwarded", DelayedDelivery.NS_URI + ":delay");
if (delay_node == null) { if (delay_node == null) {
warning("MAM result did not contain delayed time %s", message.stanza.to_string()); warning("MAM result did not contain delayed time %s", message.stanza.to_string());
return; return;
@ -104,7 +102,7 @@ public class Dino.HistorySync {
mam_times[account][id] = time; mam_times[account][id] = time;
// Check if this is the target message // Check if this is the target message
string? query_id = message.stanza.get_deep_attribute(mam_flag.ns_ver + ":result", mam_flag.ns_ver + ":queryid"); string? query_id = message.stanza.get_deep_attribute(Xmpp.MessageArchiveManagement.NS_URI + ":result", Xmpp.MessageArchiveManagement.NS_URI + ":queryid");
if (query_id != null && id == catchup_until_id[account]) { if (query_id != null && id == catchup_until_id[account]) {
debug("[%s] Hitted range (id) %s", account.bare_jid.to_string(), id); debug("[%s] Hitted range (id) %s", account.bare_jid.to_string(), id);
hitted_range[query_id] = -2; hitted_range[query_id] = -2;
@ -163,7 +161,7 @@ public class Dino.HistorySync {
if (current_row[db.mam_catchup.from_end]) return; if (current_row[db.mam_catchup.from_end]) return;
debug("[%s] Fetching between ranges %s - %s", mam_server.to_string(), previous_row[db.mam_catchup.to_time].to_string(), current_row[db.mam_catchup.from_time].to_string()); debug("[%s] Fetching between ranges %s - %s", mam_server.to_string(), previous_row[db.mam_catchup.to_time].to_string(), current_row[db.mam_catchup.from_time].to_string());
current_row = yield fetch_between_ranges(account, mam_server, previous_row, current_row); current_row = yield fetch_between_ranges(account, mam_server, previous_row, current_row, cancellable);
if (current_row == null) return; if (current_row == null) return;
RowOption previous_row_opt = db.mam_catchup.select() RowOption previous_row_opt = db.mam_catchup.select()
@ -214,13 +212,11 @@ public class Dino.HistorySync {
return null; return null;
} }
// If we get PageResult.Duplicate, we still want to update the db row to the latest message.
// Catchup finished within first page. Update latest db entry. // Catchup finished within first page. Update latest db entry.
if (latest_row_id != -1 && if (latest_row_id != -1 &&
page_result.page_result in new PageResult[] { PageResult.TargetReached, PageResult.NoMoreMessages, PageResult.Duplicate }) { page_result.page_result in new PageResult[] { PageResult.TargetReached, PageResult.NoMoreMessages }) {
if (page_result.stanzas == null || page_result.stanzas.is_empty) return null; if (page_result.stanzas == null) return null;
string latest_mam_id = page_result.query_result.last; string latest_mam_id = page_result.query_result.last;
long latest_mam_time = (long) mam_times[account][latest_mam_id].to_unix(); long latest_mam_time = (long) mam_times[account][latest_mam_id].to_unix();
@ -272,7 +268,7 @@ public class Dino.HistorySync {
** Merges the `earlier_range` db row into the `later_range` db row. ** Merges the `earlier_range` db row into the `later_range` db row.
** @return The resulting range comprising `earlier_range`, `later_rage`, and everything in between. null if fetching/merge failed. ** @return The resulting range comprising `earlier_range`, `later_rage`, and everything in between. null if fetching/merge failed.
**/ **/
private async Row? fetch_between_ranges(Account account, Jid mam_server, Row earlier_range, Row later_range) { private async Row? fetch_between_ranges(Account account, Jid mam_server, Row earlier_range, Row later_range, Cancellable? cancellable = null) {
int later_range_id = (int) later_range[db.mam_catchup.id]; int later_range_id = (int) later_range[db.mam_catchup.id];
DateTime earliest_time = new DateTime.from_unix_utc(earlier_range[db.mam_catchup.to_time]); DateTime earliest_time = new DateTime.from_unix_utc(earlier_range[db.mam_catchup.to_time]);
DateTime latest_time = new DateTime.from_unix_utc(later_range[db.mam_catchup.from_time]); DateTime latest_time = new DateTime.from_unix_utc(later_range[db.mam_catchup.from_time]);
@ -282,9 +278,9 @@ public class Dino.HistorySync {
earliest_time, earlier_range[db.mam_catchup.to_id], earliest_time, earlier_range[db.mam_catchup.to_id],
latest_time, later_range[db.mam_catchup.from_id]); latest_time, later_range[db.mam_catchup.from_id]);
PageRequestResult page_result = yield fetch_query(account, query_params, later_range_id); PageRequestResult page_result = yield fetch_query(account, query_params, later_range_id, cancellable);
if (page_result.page_result == PageResult.TargetReached) { if (page_result.page_result == PageResult.TargetReached || page_result.page_result == PageResult.NoMoreMessages) {
debug("[%s | %s] Merging range %i into %i", account.bare_jid.to_string(), mam_server.to_string(), earlier_range[db.mam_catchup.id], later_range_id); debug("[%s | %s] Merging range %i into %i", account.bare_jid.to_string(), mam_server.to_string(), earlier_range[db.mam_catchup.id], later_range_id);
// Merge earlier range into later one. // Merge earlier range into later one.
db.mam_catchup.update() db.mam_catchup.update()
@ -330,9 +326,9 @@ public class Dino.HistorySync {
PageRequestResult? page_result = null; PageRequestResult? page_result = null;
do { do {
page_result = yield get_mam_page(account, query_params, page_result, cancellable); page_result = yield get_mam_page(account, query_params, page_result, cancellable);
debug("Page result %s %b", page_result.page_result.to_string(), page_result.stanzas == null); debug("[%s | %s] Page result %s (got stanzas: %s)", account.bare_jid.to_string(), query_params.mam_server.to_string(), page_result.page_result.to_string(), (page_result.stanzas != null).to_string());
if (page_result.page_result == PageResult.Error || page_result.page_result == PageResult.Cancelled || page_result.stanzas == null) return page_result; if (page_result.page_result == PageResult.Error || page_result.page_result == PageResult.Cancelled || page_result.query_result.first == null) return page_result;
string earliest_mam_id = page_result.query_result.first; string earliest_mam_id = page_result.query_result.first;
long earliest_mam_time = (long)mam_times[account][earliest_mam_id].to_unix(); long earliest_mam_time = (long)mam_times[account][earliest_mam_id].to_unix();
@ -357,7 +353,6 @@ public class Dino.HistorySync {
MorePagesAvailable, MorePagesAvailable,
TargetReached, TargetReached,
NoMoreMessages, NoMoreMessages,
Duplicate,
Error, Error,
Cancelled Cancelled
} }
@ -399,23 +394,25 @@ public class Dino.HistorySync {
string query_id = query_params.query_id; string query_id = query_params.query_id;
string? after_id = query_params.start_id; string? after_id = query_params.start_id;
var stanzas_for_query = stanzas.has_key(query_id) && !stanzas[query_id].is_empty ? stanzas[query_id] : null;
if (cancellable != null && cancellable.is_cancelled()) { if (cancellable != null && cancellable.is_cancelled()) {
return new PageRequestResult(PageResult.Cancelled, query_result, stanzas[query_id]); stanzas.unset(query_id);
return new PageRequestResult(PageResult.Cancelled, query_result, stanzas_for_query);
} }
if (stanzas.has_key(query_id) && !stanzas[query_id].is_empty) { if (stanzas_for_query != null) {
// Check it we reached our target (from_id) // Check it we reached our target (from_id)
foreach (Xmpp.MessageStanza message in stanzas[query_id]) { foreach (Xmpp.MessageStanza message in stanzas_for_query) {
Xmpp.MessageArchiveManagement.MessageFlag? mam_message_flag = Xmpp.MessageArchiveManagement.MessageFlag.get_flag(message); Xmpp.MessageArchiveManagement.MessageFlag? mam_message_flag = Xmpp.MessageArchiveManagement.MessageFlag.get_flag(message);
if (mam_message_flag != null && mam_message_flag.mam_id != null) { if (mam_message_flag != null && mam_message_flag.mam_id != null) {
if (after_id != null && mam_message_flag.mam_id == after_id) { if (after_id != null && mam_message_flag.mam_id == after_id) {
// Successfully fetched the whole range // Successfully fetched the whole range
yield send_messages_back_into_pipeline(account, query_id, cancellable); yield send_messages_back_into_pipeline(account, query_id, cancellable);
if (cancellable != null && cancellable.is_cancelled()) { if (cancellable != null && cancellable.is_cancelled()) {
return new PageRequestResult(PageResult.Cancelled, query_result, stanzas[query_id]); return new PageRequestResult(PageResult.Cancelled, query_result, stanzas_for_query);
} }
return new PageRequestResult(PageResult.TargetReached, query_result, stanzas[query_id]); return new PageRequestResult(PageResult.TargetReached, query_result, stanzas_for_query);
} }
} }
} }
@ -423,37 +420,9 @@ public class Dino.HistorySync {
// Message got filtered out by xmpp-vala, but succesful range fetch nevertheless // Message got filtered out by xmpp-vala, but succesful range fetch nevertheless
yield send_messages_back_into_pipeline(account, query_id); yield send_messages_back_into_pipeline(account, query_id);
if (cancellable != null && cancellable.is_cancelled()) { if (cancellable != null && cancellable.is_cancelled()) {
return new PageRequestResult(PageResult.Cancelled, query_result, stanzas[query_id]); return new PageRequestResult(PageResult.Cancelled, query_result, stanzas_for_query);
} }
return new PageRequestResult(PageResult.TargetReached, query_result, stanzas[query_id]); return new PageRequestResult(PageResult.TargetReached, query_result, stanzas_for_query);
}
// Check for duplicates. Go through all messages and build a db query.
foreach (Xmpp.MessageStanza message in stanzas[query_id]) {
Xmpp.MessageArchiveManagement.MessageFlag? mam_message_flag = Xmpp.MessageArchiveManagement.MessageFlag.get_flag(message);
if (mam_message_flag != null && mam_message_flag.mam_id != null) {
if (selection == null) {
selection = @"$(db.message.server_id) = ?";
} else {
selection += @" OR $(db.message.server_id) = ?";
}
selection_args += mam_message_flag.mam_id;
}
}
var duplicates_qry = db.message.select()
.with(db.message.account_id, "=", account.id)
.where(selection, selection_args);
// We don't want messages from different MAM servers to interfere with each other.
if (!query_params.mam_server.equals_bare(account.bare_jid)) {
duplicates_qry.with(db.message.counterpart_id, "=", db.get_jid_id(query_params.mam_server));
} else {
duplicates_qry.with(db.message.type_, "=", Message.Type.CHAT);
}
var duplicates_count = duplicates_qry.count();
if (duplicates_count > 0) {
// We got a duplicate although we thought we have to catch up.
// There was a server bug where prosody would send all messages if it didn't know the after ID that was given
page_result = PageResult.Duplicate;
} }
} }
@ -461,7 +430,7 @@ public class Dino.HistorySync {
if (cancellable != null && cancellable.is_cancelled()) { if (cancellable != null && cancellable.is_cancelled()) {
page_result = PageResult.Cancelled; page_result = PageResult.Cancelled;
} }
return new PageRequestResult(page_result, query_result, stanzas.has_key(query_id) ? stanzas[query_id] : null); return new PageRequestResult(page_result, query_result, stanzas_for_query);
} }
private async void send_messages_back_into_pipeline(Account account, string query_id, Cancellable? cancellable = null) { private async void send_messages_back_into_pipeline(Account account, string query_id, Cancellable? cancellable = null) {

View file

@ -170,19 +170,21 @@ public class MessageProcessor : StreamInteractionModule, Object {
XmppStream? stream = stream_interactor.get_stream(account); XmppStream? stream = stream_interactor.get_stream(account);
Xmpp.MessageArchiveManagement.MessageFlag? mam_message_flag = Xmpp.MessageArchiveManagement.MessageFlag.get_flag(message); Xmpp.MessageArchiveManagement.MessageFlag? mam_message_flag = Xmpp.MessageArchiveManagement.MessageFlag.get_flag(message);
Xmpp.MessageArchiveManagement.Flag? mam_flag = stream != null ? stream.get_flag(Xmpp.MessageArchiveManagement.Flag.IDENTITY) : null;
EntityInfo entity_info = stream_interactor.get_module(EntityInfo.IDENTITY); EntityInfo entity_info = stream_interactor.get_module(EntityInfo.IDENTITY);
if (mam_message_flag != null && mam_flag != null && mam_flag.ns_ver == Xmpp.MessageArchiveManagement.NS_URI_2 && mam_message_flag.mam_id != null) { if (mam_message_flag != null && mam_message_flag.mam_id != null) {
bool server_does_mam = entity_info.has_feature_cached(account, account.bare_jid, Xmpp.MessageArchiveManagement.NS_URI);
if (server_does_mam) {
new_message.server_id = mam_message_flag.mam_id; new_message.server_id = mam_message_flag.mam_id;
}
} else if (message.type_ == Xmpp.MessageStanza.TYPE_GROUPCHAT) { } else if (message.type_ == Xmpp.MessageStanza.TYPE_GROUPCHAT) {
bool server_supports_sid = (yield entity_info.has_feature(account, new_message.counterpart.bare_jid, Xep.UniqueStableStanzaIDs.NS_URI)) || bool server_supports_sid = (yield entity_info.has_feature(account, new_message.counterpart.bare_jid, Xep.UniqueStableStanzaIDs.NS_URI)) ||
(yield entity_info.has_feature(account, new_message.counterpart.bare_jid, Xmpp.MessageArchiveManagement.NS_URI_2)); (yield entity_info.has_feature(account, new_message.counterpart.bare_jid, Xmpp.MessageArchiveManagement.NS_URI));
if (server_supports_sid) { if (server_supports_sid) {
new_message.server_id = Xep.UniqueStableStanzaIDs.get_stanza_id(message, new_message.counterpart.bare_jid); new_message.server_id = Xep.UniqueStableStanzaIDs.get_stanza_id(message, new_message.counterpart.bare_jid);
} }
} else if (message.type_ == Xmpp.MessageStanza.TYPE_CHAT) { } else if (message.type_ == Xmpp.MessageStanza.TYPE_CHAT) {
bool server_supports_sid = (yield entity_info.has_feature(account, account.bare_jid, Xep.UniqueStableStanzaIDs.NS_URI)) || bool server_supports_sid = (yield entity_info.has_feature(account, account.bare_jid, Xep.UniqueStableStanzaIDs.NS_URI)) ||
(yield entity_info.has_feature(account, account.bare_jid, Xmpp.MessageArchiveManagement.NS_URI_2)); (yield entity_info.has_feature(account, account.bare_jid, Xmpp.MessageArchiveManagement.NS_URI));
if (server_supports_sid) { if (server_supports_sid) {
new_message.server_id = Xep.UniqueStableStanzaIDs.get_stanza_id(message, account.bare_jid); new_message.server_id = Xep.UniqueStableStanzaIDs.get_stanza_id(message, account.bare_jid);
} }
@ -378,7 +380,7 @@ public class MessageProcessor : StreamInteractionModule, Object {
public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) { public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
bool is_mam_message = Xmpp.MessageArchiveManagement.MessageFlag.get_flag(stanza) != null; bool is_mam_message = Xmpp.MessageArchiveManagement.MessageFlag.get_flag(stanza) != null;
XmppStream? stream = stream_interactor.get_stream(conversation.account); XmppStream? stream = stream_interactor.get_stream(conversation.account);
Xmpp.MessageArchiveManagement.Flag? mam_flag = stream != null ? stream.get_flag(Xmpp.MessageArchiveManagement.Flag.IDENTITY) : null; Xmpp.MessageArchiveManagement.Flag mam_flag = Xmpp.MessageArchiveManagement.Flag.get_flag(stream);
if (is_mam_message || (mam_flag != null && mam_flag.cought_up == true)) { if (is_mam_message || (mam_flag != null && mam_flag.cought_up == true)) {
conversation.account.mam_earliest_synced = message.local_time; conversation.account.mam_earliest_synced = message.local_time;
} }
@ -494,8 +496,7 @@ public class MessageProcessor : StreamInteractionModule, Object {
string fallback = FallbackBody.get_quoted_fallback_body(content_item); string fallback = FallbackBody.get_quoted_fallback_body(content_item);
long fallback_length = fallback.length; var fallback_location = new Xep.FallbackIndication.FallbackLocation(0, (int)fallback.char_count());
var fallback_location = new Xep.FallbackIndication.FallbackLocation(0, (int)fallback_length);
Xep.FallbackIndication.set_fallback(new_stanza, new Xep.FallbackIndication.Fallback(Xep.Replies.NS_URI, new Xep.FallbackIndication.FallbackLocation[] { fallback_location })); Xep.FallbackIndication.set_fallback(new_stanza, new Xep.FallbackIndication.Fallback(Xep.Replies.NS_URI, new Xep.FallbackIndication.FallbackLocation[] { fallback_location }));
return fallback; return fallback;

View file

@ -72,7 +72,7 @@ public class MucManager : StreamInteractionModule, Object {
bool receive_history = true; bool receive_history = true;
EntityInfo entity_info = stream_interactor.get_module(EntityInfo.IDENTITY); EntityInfo entity_info = stream_interactor.get_module(EntityInfo.IDENTITY);
bool can_do_mam = yield entity_info.has_feature(account, jid, Xmpp.MessageArchiveManagement.NS_URI_2); bool can_do_mam = yield entity_info.has_feature(account, jid, Xmpp.MessageArchiveManagement.NS_URI);
if (can_do_mam) { if (can_do_mam) {
receive_history = false; receive_history = false;
history_since = null; history_since = null;

View file

@ -64,7 +64,7 @@ public class Dino.Reactions : StreamInteractionModule, Object {
// The MUC server needs to 1) support stable stanza ids 2) either support occupant ids or be a private room (where we know real jids) // The MUC server needs to 1) support stable stanza ids 2) either support occupant ids or be a private room (where we know real jids)
var entity_info = stream_interactor.get_module(EntityInfo.IDENTITY); var entity_info = stream_interactor.get_module(EntityInfo.IDENTITY);
bool server_supports_sid = (entity_info.has_feature_cached(conversation.account, conversation.counterpart.bare_jid, Xep.UniqueStableStanzaIDs.NS_URI)) || bool server_supports_sid = (entity_info.has_feature_cached(conversation.account, conversation.counterpart.bare_jid, Xep.UniqueStableStanzaIDs.NS_URI)) ||
(entity_info.has_feature_cached(conversation.account, conversation.counterpart.bare_jid, Xmpp.MessageArchiveManagement.NS_URI_2)); (entity_info.has_feature_cached(conversation.account, conversation.counterpart.bare_jid, Xmpp.MessageArchiveManagement.NS_URI));
if (!server_supports_sid) return false; if (!server_supports_sid) return false;
bool? supports_occupant_ids = entity_info.has_feature_cached(conversation.account, conversation.counterpart, Xep.OccupantIds.NS_URI); bool? supports_occupant_ids = entity_info.has_feature_cached(conversation.account, conversation.counterpart, Xep.OccupantIds.NS_URI);

View file

@ -105,7 +105,8 @@ namespace Dino {
string body = message.body; string body = message.body;
foreach (var fallback in message.get_fallbacks()) { foreach (var fallback in message.get_fallbacks()) {
if (fallback.ns_uri == Xep.Replies.NS_URI && message.quoted_item_id > 0) { if (fallback.ns_uri == Xep.Replies.NS_URI && message.quoted_item_id > 0) {
body = body[0:fallback.locations[0].from_char] + body[fallback.locations[0].to_char:body.length]; body = body[0:body.index_of_nth_char(fallback.locations[0].from_char)] +
body[body.index_of_nth_char(fallback.locations[0].to_char):body.length];
} }
} }
return body; return body;

View file

@ -34,6 +34,7 @@ namespace Dino {
if (self_word != null && (account.alias == null || account.alias.length == 0)) { if (self_word != null && (account.alias == null || account.alias.length == 0)) {
return self_word; return self_word;
} }
if (account.alias != null && account.alias.length == 0) return null;
return account.alias; return account.alias;
} }
Roster.Item roster_item = stream_interactor.get_module(RosterManager.IDENTITY).get_roster_item(account, jid); Roster.Item roster_item = stream_interactor.get_module(RosterManager.IDENTITY).get_roster_item(account, jid);

View file

@ -85,6 +85,7 @@
<property name="margin_bottom">3</property> <property name="margin_bottom">3</property>
<property name="margin_start">14</property> <property name="margin_start">14</property>
<property name="margin_end">14</property> <property name="margin_end">14</property>
<property name="wrap">True</property>
<attributes> <attributes>
<attribute name="scale" value="0.8"></attribute> <attribute name="scale" value="0.8"></attribute>
</attributes> </attributes>

View file

@ -5,9 +5,10 @@ GenericName=Jabber/XMPP Client
Keywords=chat;talk;im;message;xmpp;jabber; Keywords=chat;talk;im;message;xmpp;jabber;
Exec=dino %U Exec=dino %U
Icon=im.dino.Dino Icon=im.dino.Dino
StartupNotify=false StartupNotify=true
Terminal=false Terminal=false
Type=Application Type=Application
Categories=GTK;Network;Chat;InstantMessaging; Categories=GTK;Network;Chat;InstantMessaging;
X-GNOME-UsesNotifications=true X-GNOME-UsesNotifications=true
MimeType=x-scheme-handler/xmpp; MimeType=x-scheme-handler/xmpp;
X-Purism-FormFactor=Workstation;Mobile;

View file

@ -392,17 +392,13 @@ box.dino-input-error .chat-input-status.input-status-highlight-once {
text-shadow: 0 0 2px black; text-shadow: 0 0 2px black;
} }
.dino-call-window .call-header-background { .dino-call-window .participant-header-bar {
background: linear-gradient(rgba(0,0,0,0.4), rgba(0,0,0,0)); background: linear-gradient(rgba(0,0,0,0.4), rgba(0,0,0,0));
border: 0; border: 0;
border-radius: 0; border-radius: 0;
} }
.dino-call-window .participant-header-bar button { .dino-call-window .participant-header-bar button:hover:not(.close) {
background: none;
}
.dino-call-window .participant-header-bar button:hover {
background: rgba(255,255,255,0.15); background: rgba(255,255,255,0.15);
border-color: rgba(255,255,255,0); border-color: rgba(255,255,255,0);
box-shadow: none; box-shadow: none;

View file

@ -22,7 +22,12 @@ namespace Dino.Ui {
private HashMap<string, ParticipantWidget> participant_widgets = new HashMap<string, ParticipantWidget>(); private HashMap<string, ParticipantWidget> participant_widgets = new HashMap<string, ParticipantWidget>();
private ArrayList<string> participants = new ArrayList<string>(); private ArrayList<string> participants = new ArrayList<string>();
private EventControllerFocus this_focus_events = new EventControllerFocus();
private GestureClick this_gesture_events = new GestureClick() { touch_only=true, propagation_phase=Gtk.PropagationPhase.CAPTURE };
private EventControllerMotion this_motion_events = new EventControllerMotion(); private EventControllerMotion this_motion_events = new EventControllerMotion();
private double latest_motion_x = -1;
private double latest_motion_y = -1;
private const double MOTION_RELEVANCE_THRESHOLD = 2;
private int own_video_width = 150; private int own_video_width = 150;
private int own_video_height = 100; private int own_video_height = 100;
@ -54,9 +59,20 @@ namespace Dino.Ui {
this.bind_property("controls-active", bottom_bar_revealer, "reveal-child", BindingFlags.SYNC_CREATE); this.bind_property("controls-active", bottom_bar_revealer, "reveal-child", BindingFlags.SYNC_CREATE);
((Widget) this).add_controller(this_motion_events); ((Widget) this).add_controller(this_motion_events);
this_motion_events.motion.connect(reveal_control_elements); this_motion_events.motion.connect((x, y) => {
this_motion_events.enter.connect(reveal_control_elements); if ((latest_motion_x - x).abs() <= MOTION_RELEVANCE_THRESHOLD && (latest_motion_y - y).abs() <= MOTION_RELEVANCE_THRESHOLD) return;
this_motion_events.leave.connect(reveal_control_elements);
latest_motion_x = x;
latest_motion_y = y;
reveal_control_elements();
});
((Widget) this).add_controller(this_focus_events);
this_focus_events.enter.connect(reveal_control_elements);
this_focus_events.leave.connect(reveal_control_elements);
((Widget) this).add_controller(this_gesture_events);
this_gesture_events.pressed.connect_after(reveal_control_elements);
this.notify["default-width"].connect(reveal_control_elements); this.notify["default-width"].connect(reveal_control_elements);
this.notify["default-height"].connect(reveal_control_elements); this.notify["default-height"].connect(reveal_control_elements);

View file

@ -74,14 +74,11 @@ namespace Dino.Ui {
header_bar.show_title_buttons = is_highest_row; header_bar.show_title_buttons = is_highest_row;
if (is_highest_row) { if (is_highest_row) {
header_bar.add_css_class("call-header-background");
Gtk.Settings? gtk_settings = Gtk.Settings.get_default(); Gtk.Settings? gtk_settings = Gtk.Settings.get_default();
if (gtk_settings != null) { if (gtk_settings != null) {
string[] buttons = gtk_settings.gtk_decoration_layout.split(":"); string[] buttons = gtk_settings.gtk_decoration_layout.split(":");
header_bar.decoration_layout = (is_start ? buttons[0] : "") + ":" + (is_end && buttons.length == 2 ? buttons[1] : ""); header_bar.decoration_layout = (is_start ? buttons[0] : "") + ":" + (is_end && buttons.length == 2 ? buttons[1] : "");
} }
} else {
header_bar.remove_css_class("call-header-background");
} }
reveal_or_hide_controls(); reveal_or_hide_controls();
} }

View file

@ -94,8 +94,13 @@ public class ChatTextView : Box {
} }
} }
private bool on_text_input_key_press(uint keyval, uint keycode, Gdk.ModifierType state) { private bool on_text_input_key_press(EventControllerKey controller, uint keyval, uint keycode, Gdk.ModifierType state) {
if (keyval in new uint[]{ Key.Return, Key.KP_Enter }) { if (keyval in new uint[]{ Key.Return, Key.KP_Enter }) {
// Allow the text view to process the event. Needed for IME.
if (text_view.im_context_filter_keypress(controller.get_current_event())) {
return true;
}
if ((state & ModifierType.SHIFT_MASK) > 0) { if ((state & ModifierType.SHIFT_MASK) > 0) {
text_view.buffer.insert_at_cursor("\n", 1); text_view.buffer.insert_at_cursor("\n", 1);
} else if (text_view.buffer.text.strip() != "") { } else if (text_view.buffer.text.strip() != "") {

View file

@ -39,6 +39,8 @@ public class View : Box {
chooser.emoji_picked.connect((emoji) => { chooser.emoji_picked.connect((emoji) => {
chat_text_view.text_view.buffer.insert_at_cursor(emoji, emoji.data.length); chat_text_view.text_view.buffer.insert_at_cursor(emoji, emoji.data.length);
}); });
chooser.closed.connect(do_focus);
emoji_button.set_popover(chooser); emoji_button.set_popover(chooser);
file_button.tooltip_text = Util.string_if_tooltips_active(_("Send a file")); file_button.tooltip_text = Util.string_if_tooltips_active(_("Send a file"));

View file

@ -95,7 +95,6 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
EventControllerMotion main_wrap_motion_events = new EventControllerMotion(); EventControllerMotion main_wrap_motion_events = new EventControllerMotion();
main_wrap_box.add_controller(main_wrap_motion_events); main_wrap_box.add_controller(main_wrap_motion_events);
main_wrap_motion_events.leave.connect(on_leave_notify_event); main_wrap_motion_events.leave.connect(on_leave_notify_event);
main_wrap_motion_events.enter.connect(update_highlight);
// The buttons of the overlaying message_menu_box may partially overlap the adjacent // The buttons of the overlaying message_menu_box may partially overlap the adjacent
// conversation items. We connect to the main_event_box directly to avoid emitting // conversation items. We connect to the main_event_box directly to avoid emitting
// the pointer motion events as long as the pointer is above the message menu. // the pointer motion events as long as the pointer is above the message menu.

View file

@ -225,7 +225,21 @@ public class ConversationSelectorRow : ListBoxRow {
label.attributes = copy; label.attributes = copy;
} }
private bool update_read_pending = false;
private bool update_read_pending_force = false;
protected void update_read(bool force_update = false) { protected void update_read(bool force_update = false) {
if (force_update) update_read_pending_force = true;
if (update_read_pending) return;
update_read_pending = true;
Idle.add(() => {
update_read_pending = false;
update_read_pending_force = false;
update_read_idle(update_read_pending_force);
return Source.REMOVE;
}, Priority.LOW);
}
private void update_read_idle(bool force_update = false) {
int current_num_unread = stream_interactor.get_module(ChatInteraction.IDENTITY).get_num_unread(conversation); int current_num_unread = stream_interactor.get_module(ChatInteraction.IDENTITY).get_num_unread(conversation);
if (num_unread == current_num_unread && !force_update) return; if (num_unread == current_num_unread && !force_update) return;
num_unread = current_num_unread; num_unread = current_num_unread;

View file

@ -10,13 +10,16 @@ public class FileProvider : Dino.FileProvider, Object {
private StreamInteractor stream_interactor; private StreamInteractor stream_interactor;
private Dino.Database dino_db; private Dino.Database dino_db;
private Soup.Session session;
private static Regex http_url_regex = /^https?:\/\/([^\s#]*)$/; // Spaces are invalid in URLs and we can't use fragments for downloads private static Regex http_url_regex = /^https?:\/\/([^\s#]*)$/; // Spaces are invalid in URLs and we can't use fragments for downloads
private static Regex omemo_url_regex = /^aesgcm:\/\/(.*)#(([A-Fa-f0-9]{2}){48}|([A-Fa-f0-9]{2}){44})$/; private static Regex omemo_url_regex = /^aesgcm:\/\/(.*)#(([A-Fa-f0-9]{2}){48}|([A-Fa-f0-9]{2}){44})$/;
public FileProvider(StreamInteractor stream_interactor, Dino.Database dino_db) { public FileProvider(StreamInteractor stream_interactor, Dino.Database dino_db) {
this.stream_interactor = stream_interactor; this.stream_interactor = stream_interactor;
this.dino_db = dino_db; this.dino_db = dino_db;
this.session = new Soup.Session();
session.user_agent = @"Dino/$(Dino.get_short_version()) ";
stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(new ReceivedMessageListener(this)); stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(new ReceivedMessageListener(this));
} }
@ -114,8 +117,6 @@ public class FileProvider : Dino.FileProvider, Object {
HttpFileReceiveData? http_receive_data = receive_data as HttpFileReceiveData; HttpFileReceiveData? http_receive_data = receive_data as HttpFileReceiveData;
if (http_receive_data == null) return file_meta; if (http_receive_data == null) return file_meta;
var session = new Soup.Session();
session.user_agent = @"Dino/$(Dino.get_short_version()) ";
var head_message = new Soup.Message("HEAD", http_receive_data.url); var head_message = new Soup.Message("HEAD", http_receive_data.url);
head_message.request_headers.append("Accept-Encoding", "identity"); head_message.request_headers.append("Accept-Encoding", "identity");
@ -150,8 +151,6 @@ public class FileProvider : Dino.FileProvider, Object {
HttpFileReceiveData? http_receive_data = receive_data as HttpFileReceiveData; HttpFileReceiveData? http_receive_data = receive_data as HttpFileReceiveData;
if (http_receive_data == null) assert(false); if (http_receive_data == null) assert(false);
var session = new Soup.Session();
session.user_agent = @"Dino/$(Dino.get_short_version()) ";
var get_message = new Soup.Message("GET", http_receive_data.url); var get_message = new Soup.Message("GET", http_receive_data.url);
try { try {

View file

@ -7,12 +7,15 @@ namespace Dino.Plugins.HttpFiles {
public class HttpFileSender : FileSender, Object { public class HttpFileSender : FileSender, Object {
private StreamInteractor stream_interactor; private StreamInteractor stream_interactor;
private Database db; private Database db;
private Soup.Session session;
private HashMap<Account, long> max_file_sizes = new HashMap<Account, long>(Account.hash_func, Account.equals_func); private HashMap<Account, long> max_file_sizes = new HashMap<Account, long>(Account.hash_func, Account.equals_func);
public HttpFileSender(StreamInteractor stream_interactor, Database db) { public HttpFileSender(StreamInteractor stream_interactor, Database db) {
this.stream_interactor = stream_interactor; this.stream_interactor = stream_interactor;
this.db = db; this.db = db;
this.session = new Soup.Session();
session.user_agent = @"Dino/$(Dino.get_short_version()) ";
stream_interactor.stream_negotiated.connect(on_stream_negotiated); stream_interactor.stream_negotiated.connect(on_stream_negotiated);
stream_interactor.get_module(MessageProcessor.IDENTITY).build_message_stanza.connect(check_add_oob); stream_interactor.get_module(MessageProcessor.IDENTITY).build_message_stanza.connect(check_add_oob);
} }
@ -90,8 +93,6 @@ public class HttpFileSender : FileSender, Object {
Xmpp.XmppStream? stream = stream_interactor.get_stream(file_transfer.account); Xmpp.XmppStream? stream = stream_interactor.get_stream(file_transfer.account);
if (stream == null) return; if (stream == null) return;
var session = new Soup.Session();
session.user_agent = @"Dino/$(Dino.get_short_version()) ";
var put_message = new Soup.Message("PUT", file_send_data.url_up); var put_message = new Soup.Message("PUT", file_send_data.url_up);
#if SOUP_3_0 #if SOUP_3_0
put_message.set_request_body(file_meta.mime_type, file_transfer.input_stream, (ssize_t) file_meta.size); put_message.set_request_body(file_meta.mime_type, file_transfer.input_stream, (ssize_t) file_meta.size);

View file

@ -38,7 +38,11 @@ public class Handler {
} }
public uint8[]? process_incoming_data(uint component_id, uint8[] data) throws Crypto.Error { public uint8[]? process_incoming_data(uint component_id, uint8[] data) throws Crypto.Error {
if (srtp_session.has_decrypt) { if (data[0] >= 128) {
if (!srtp_session.has_decrypt) {
debug("Received data before SRTP session is ready, dropping.");
return null;
}
if (component_id == 1) { if (component_id == 1) {
if (data.length >= 2 && data[1] >= 192 && data[1] < 224) { if (data.length >= 2 && data[1] >= 192 && data[1] < 224) {
return srtp_session.decrypt_rtcp(data); return srtp_session.decrypt_rtcp(data);
@ -46,9 +50,12 @@ public class Handler {
return srtp_session.decrypt_rtp(data); return srtp_session.decrypt_rtp(data);
} }
if (component_id == 2) return srtp_session.decrypt_rtcp(data); if (component_id == 2) return srtp_session.decrypt_rtcp(data);
} else if (component_id == 1) {
on_data_rec(data);
} }
if (component_id == 1 && data.length >= 1 && (data[0] >= 20 && data[0] < 64)) {
on_data_rec(data);
return null;
}
debug("Dropping unknown data from component %u", component_id);
return null; return null;
} }
@ -79,7 +86,7 @@ public class Handler {
err = private_key.generate(PKAlgorithm.ECDSA, 256); err = private_key.generate(PKAlgorithm.ECDSA, 256);
throw_if_error(err); throw_if_error(err);
var start_time = new DateTime.now_local().add_days(1); var start_time = new DateTime.now_local().add_days(-1);
var end_time = start_time.add_days(2); var end_time = start_time.add_days(2);
X509.Certificate cert = X509.Certificate.create(); X509.Certificate cert = X509.Certificate.create();

View file

@ -215,7 +215,12 @@ public class Dino.Plugins.Rtp.Device : MediaDevice, Object {
} }
if (new_width == active_caps_width) return; if (new_width == active_caps_width) return;
int new_height = device_caps_height * new_width / device_caps_width; int new_height = device_caps_height * new_width / device_caps_width;
Gst.Caps new_caps = new Gst.Caps.simple("video/x-raw", "width", typeof(int), new_width, "height", typeof(int), new_height, "framerate", typeof(Gst.Fraction), device_caps_framerate_num, device_caps_framerate_den, null); Gst.Caps new_caps;
if (device_caps_framerate_den != 0) {
new_caps = new Gst.Caps.simple("video/x-raw", "width", typeof(int), new_width, "height", typeof(int), new_height, "framerate", typeof(Gst.Fraction), device_caps_framerate_num, device_caps_framerate_den, null);
} else {
new_caps = new Gst.Caps.simple("video/x-raw", "width", typeof(int), new_width, "height", typeof(int), new_height, null);
}
double required_bitrate = get_target_bitrate(new_caps); double required_bitrate = get_target_bitrate(new_caps);
debug("Changing resolution width from %d to %d (requires bitrate %f, current target is %u)", active_caps_width, new_width, required_bitrate, bitrate); debug("Changing resolution width from %d to %d (requires bitrate %f, current target is %u)", active_caps_width, new_width, required_bitrate, bitrate);
if (bitrate < required_bitrate && new_width > active_caps_width) return; if (bitrate < required_bitrate && new_width > active_caps_width) return;
@ -347,7 +352,7 @@ public class Dino.Plugins.Rtp.Device : MediaDevice, Object {
if (media == "audio") { if (media == "audio") {
return Gst.Caps.from_string("audio/x-raw,rate=48000,channels=1"); return Gst.Caps.from_string("audio/x-raw,rate=48000,channels=1");
} else if (media == "video" && device.caps.get_size() > 0) { } else if (media == "video" && device.caps.get_size() > 0) {
int best_index = 0; int best_index = -1;
Value? best_fraction = null; Value? best_fraction = null;
int best_fps = 0; int best_fps = 0;
int best_width = 0; int best_width = 0;
@ -390,10 +395,25 @@ public class Dino.Plugins.Rtp.Device : MediaDevice, Object {
best_fraction = best_fraction_now; best_fraction = best_fraction_now;
} }
} }
if (best_index == -1) {
// No caps in first round, try without framerate
for (int i = 0; i < device.caps.get_size(); i++) {
unowned Gst.Structure? that = device.caps.get_structure(i);
if (!that.has_name("video/x-raw")) continue;
int width = 0, height = 0;
if (!that.has_field("width") || !that.get_int("width", out width)) continue;
if (!that.has_field("height") || !that.get_int("height", out height)) continue;
if (best_width < width || best_width == width && best_height < height) {
best_width = width;
best_height = height;
best_index = i;
}
}
}
Gst.Caps res = caps_copy_nth(device.caps, best_index); Gst.Caps res = caps_copy_nth(device.caps, best_index);
unowned Gst.Structure? that = res.get_structure(0); unowned Gst.Structure? that = res.get_structure(0);
Value framerate = that.get_value("framerate"); Value? framerate = that.get_value("framerate");
if (framerate.type() == typeof(Gst.ValueList)) { if (framerate != null && framerate.type() == typeof(Gst.ValueList) && best_fraction != null) {
that.set_value("framerate", best_fraction); that.set_value("framerate", best_fraction);
} }
debug("Selected caps %s", res.to_string()); debug("Selected caps %s", res.to_string());

View file

@ -426,11 +426,11 @@ public class Dino.Plugins.Rtp.Plugin : RootInterface, VideoCallPlugin, Object {
if (media == "video") { if (media == "video") {
// Pick best FPS // Pick best FPS
int max_fps = 0; int max_fps = -1;
Device? max_fps_device = null; Device? max_fps_device = null;
foreach (Device device in devices) { foreach (Device device in devices) {
int fps = get_max_fps(device); int fps = get_max_fps(device);
if (fps > max_fps) { if (fps > max_fps || max_fps_device == null) {
max_fps = fps; max_fps = fps;
max_fps_device = device; max_fps_device = device;
} }

View file

@ -102,6 +102,9 @@ public class Dino.Plugins.Rtp.Stream : Xmpp.Xep.JingleRtp.Stream {
send_rtp.drop = true; send_rtp.drop = true;
send_rtp.wait_on_eos = false; send_rtp.wait_on_eos = false;
send_rtp.new_sample.connect(on_new_sample); send_rtp.new_sample.connect(on_new_sample);
#if GST_1_20
send_rtp.new_serialized_event.connect(on_new_event);
#endif
send_rtp.connect("signal::eos", on_eos_static, this); send_rtp.connect("signal::eos", on_eos_static, this);
pipe.add(send_rtp); pipe.add(send_rtp);
@ -294,6 +297,60 @@ public class Dino.Plugins.Rtp.Stream : Xmpp.Xep.JingleRtp.Stream {
} }
} }
bool flip = false;
uint8 rotation = 0;
#if GST_1_20
private bool on_new_event(Gst.App.Sink sink) {
if (sink == null || sink != send_rtp) {
return false;
}
Gst.MiniObject obj = sink.try_pull_object(0);
if (obj.type == typeof(Gst.Event)) {
unowned Gst.TagList tags;
if (((Gst.Event)obj).type == Gst.EventType.TAG) {
((Gst.Event)obj).parse_tag(out tags);
Gst.Video.OrientationMethod orientation_method;
Gst.Video.Orientation.from_tag(tags, out orientation_method);
switch (orientation_method) {
case Gst.Video.OrientationMethod.IDENTITY:
case Gst.Video.OrientationMethod.VERT:
default:
rotation = 0;
break;
case Gst.Video.OrientationMethod.@90R:
case Gst.Video.OrientationMethod.UL_LR:
rotation = 1;
break;
case Gst.Video.OrientationMethod.@180:
case Gst.Video.OrientationMethod.HORIZ:
rotation = 2;
break;
case Gst.Video.OrientationMethod.@90L:
case Gst.Video.OrientationMethod.UR_LL:
rotation = 3;
break;
}
switch (orientation_method) {
case Gst.Video.OrientationMethod.IDENTITY:
case Gst.Video.OrientationMethod.@90R:
case Gst.Video.OrientationMethod.@180:
case Gst.Video.OrientationMethod.@90L:
default:
flip = false;
break;
case Gst.Video.OrientationMethod.VERT:
case Gst.Video.OrientationMethod.UL_LR:
case Gst.Video.OrientationMethod.HORIZ:
case Gst.Video.OrientationMethod.UR_LL:
flip = true;
break;
}
}
}
return false;
}
#endif
private Gst.FlowReturn on_new_sample(Gst.App.Sink sink) { private Gst.FlowReturn on_new_sample(Gst.App.Sink sink) {
if (sink == null) { if (sink == null) {
debug("Sink is null"); debug("Sink is null");
@ -323,6 +380,24 @@ public class Dino.Plugins.Rtp.Stream : Xmpp.Xep.JingleRtp.Stream {
#endif #endif
} }
#if GST_1_20
if (sink == send_rtp) {
Xmpp.Xep.JingleRtp.HeaderExtension? ext = header_extensions.first_match((it) => it.uri == "urn:3gpp:video-orientation");
if (ext != null) {
buffer = (Gst.Buffer) buffer.make_writable();
Gst.RTP.Buffer rtp_buffer;
if (Gst.RTP.Buffer.map(buffer, Gst.MapFlags.WRITE, out rtp_buffer)) {
uint8[] extension_data = new uint8[1];
bool camera = false;
extension_data[0] = extension_data[0] | (rotation & 0x3);
if (flip) extension_data[0] = extension_data[0] | 0x4;
if (camera) extension_data[0] = extension_data[0] | 0x8;
rtp_buffer.add_extension_onebyte_header(ext.id, extension_data);
}
}
}
#endif
prepare_local_crypto(); prepare_local_crypto();
uint8[] data; uint8[] data;
@ -489,8 +564,8 @@ public class Dino.Plugins.Rtp.Stream : Xmpp.Xep.JingleRtp.Stream {
} }
} }
private uint16 previous_video_orientation_degree = uint16.MAX; private uint16 previous_incoming_video_orientation_degree = uint16.MAX;
public signal void video_orientation_changed(uint16 degree); public signal void incoming_video_orientation_changed(uint16 degree);
public override void on_recv_rtp_data(Bytes bytes) { public override void on_recv_rtp_data(Bytes bytes) {
if (rtcp_mux && bytes.length >= 2 && bytes.get(1) >= 192 && bytes.get(1) < 224) { if (rtcp_mux && bytes.length >= 2 && bytes.get(1) >= 192 && bytes.get(1) < 224) {
@ -545,9 +620,9 @@ public class Dino.Plugins.Rtp.Stream : Xmpp.Xep.JingleRtp.Stream {
case 2: rotation_degree = 180; break; case 2: rotation_degree = 180; break;
case 3: rotation_degree = 270; break; case 3: rotation_degree = 270; break;
} }
if (rotation_degree != previous_video_orientation_degree) { if (rotation_degree != previous_incoming_video_orientation_degree) {
video_orientation_changed(rotation_degree); incoming_video_orientation_changed(rotation_degree);
previous_video_orientation_degree = rotation_degree; previous_incoming_video_orientation_degree = rotation_degree;
} }
} }
} }
@ -723,7 +798,7 @@ public class Dino.Plugins.Rtp.VideoStream : Stream {
private Gee.List<Gst.Element> outputs = new ArrayList<Gst.Element>(); private Gee.List<Gst.Element> outputs = new ArrayList<Gst.Element>();
private Gst.Element output_tee; private Gst.Element output_tee;
private Gst.Element rotate; private Gst.Element rotate;
private ulong video_orientation_changed_handler; private ulong incoming_video_orientation_changed_handler;
public VideoStream(Plugin plugin, Xmpp.Xep.Jingle.Content content) { public VideoStream(Plugin plugin, Xmpp.Xep.Jingle.Content content) {
base(plugin, content); base(plugin, content);
@ -731,7 +806,7 @@ public class Dino.Plugins.Rtp.VideoStream : Stream {
} }
public override void create() { public override void create() {
video_orientation_changed_handler = video_orientation_changed.connect(on_video_orientation_changed); incoming_video_orientation_changed_handler = incoming_video_orientation_changed.connect(on_video_orientation_changed);
plugin.pause(); plugin.pause();
rotate = Gst.ElementFactory.make("videoflip", @"video_rotate_$rtpid"); rotate = Gst.ElementFactory.make("videoflip", @"video_rotate_$rtpid");
pipe.add(rotate); pipe.add(rotate);
@ -780,7 +855,7 @@ public class Dino.Plugins.Rtp.VideoStream : Stream {
output_tee.set_state(Gst.State.NULL); output_tee.set_state(Gst.State.NULL);
pipe.remove(output_tee); pipe.remove(output_tee);
output_tee = null; output_tee = null;
disconnect(video_orientation_changed_handler); disconnect(incoming_video_orientation_changed_handler);
} }
public override void add_output(Gst.Element element, Xmpp.Jid? participant) { public override void add_output(Gst.Element element, Xmpp.Jid? participant) {

View file

@ -1,4 +1,5 @@
private static extern unowned Gst.Video.Info gst_video_frame_get_video_info(Gst.Video.Frame frame); private static extern unowned Gst.Video.Info gst_video_frame_get_video_info(Gst.Video.Frame frame);
[CCode (array_length_type = "size_t", type = "void*")]
private static extern unowned uint8[] gst_video_frame_get_data(Gst.Video.Frame frame); private static extern unowned uint8[] gst_video_frame_get_data(Gst.Video.Frame frame);
public class Dino.Plugins.Rtp.Paintable : Gdk.Paintable, Object { public class Dino.Plugins.Rtp.Paintable : Gdk.Paintable, Object {
@ -196,7 +197,11 @@ public class Dino.Plugins.Rtp.VideoWidget : Gtk.Widget, Dino.Plugins.VideoCallWi
caps.get_structure(0).get_int("width", out width); caps.get_structure(0).get_int("width", out width);
caps.get_structure(0).get_int("height", out height); caps.get_structure(0).get_int("height", out height);
debug("Input resolution changed: %ix%i", width, height); debug("Input resolution changed: %ix%i", width, height);
// Invoke signal on GTK main loop as recipients are likely to use it for doing GTK operations
Idle.add(() => {
resolution_changed(width, height); resolution_changed(width, height);
return Source.REMOVE;
});
last_input_caps = caps; last_input_caps = caps;
} }
@ -226,9 +231,21 @@ public class Dino.Plugins.Rtp.VideoWidget : Gtk.Widget, Dino.Plugins.VideoCallWi
if (connected_device == null) return; if (connected_device == null) return;
plugin.pause(); plugin.pause();
pipe.add(sink); pipe.add(sink);
#if GST_1_20
prepare = Gst.parse_bin_from_description(@"videoflip video-direction=auto name=video_widget_$(id)_orientation ! videoflip method=horizontal-flip name=video_widget_$(id)_flip ! videoconvert name=video_widget_$(id)_convert", true);
#else
prepare = Gst.parse_bin_from_description(@"videoflip method=horizontal-flip name=video_widget_$(id)_flip ! videoconvert name=video_widget_$(id)_convert", true); prepare = Gst.parse_bin_from_description(@"videoflip method=horizontal-flip name=video_widget_$(id)_flip ! videoconvert name=video_widget_$(id)_convert", true);
#endif
prepare.name = @"video_widget_$(id)_prepare"; prepare.name = @"video_widget_$(id)_prepare";
#if GST_1_20
if (prepare is Gst.Bin) {
((Gst.Bin) prepare).get_by_name(@"video_widget_$(id)_flip").get_static_pad("sink").notify["caps"].connect(input_caps_changed);
} else {
#endif
prepare.get_static_pad("sink").notify["caps"].connect(input_caps_changed); prepare.get_static_pad("sink").notify["caps"].connect(input_caps_changed);
#if GST_1_20
}
#endif
pipe.add(prepare); pipe.add(prepare);
connected_device_element = connected_device.link_source(); connected_device_element = connected_device.link_source();
connected_device_element.link(prepare); connected_device_element.link(prepare);

View file

@ -58,6 +58,10 @@ public class ReceivedPipelineListener : StanzaListener<MessageStanza> {
warning("Received alleged carbon message from %s, ignoring", message.from.to_string()); warning("Received alleged carbon message from %s, ignoring", message.from.to_string());
return true; return true;
} }
if (message_node == null) {
warning("Received a carbon message with no message subnode in jabber:client namespace from %s, ignoring", message.from.to_string());
return true;
}
if (received_node != null) { if (received_node != null) {
message.add_flag(new MessageFlag(MessageFlag.TYPE_RECEIVED)); message.add_flag(new MessageFlag(MessageFlag.TYPE_RECEIVED));
} else if (sent_node != null) { } else if (sent_node != null) {

View file

@ -58,7 +58,7 @@ namespace Xmpp.MessageArchiveManagement.V2 {
fields.add(field); fields.add(field);
} }
return MessageArchiveManagement.create_base_query(stream, MessageArchiveManagement.NS_URI_2, mam_params.query_id, fields); return MessageArchiveManagement.create_base_query(stream, mam_params.query_id, fields);
} }
public async QueryResult query_archive(XmppStream stream, MamQueryParams mam_params, Cancellable? cancellable = null) { public async QueryResult query_archive(XmppStream stream, MamQueryParams mam_params, Cancellable? cancellable = null) {
@ -67,14 +67,14 @@ namespace Xmpp.MessageArchiveManagement.V2 {
query_node.put_node(ResultSetManagement.create_set_rsm_node_before(mam_params.end_id)); query_node.put_node(ResultSetManagement.create_set_rsm_node_before(mam_params.end_id));
} }
return yield MessageArchiveManagement.query_archive(stream, MessageArchiveManagement.NS_URI_2, mam_params.mam_server, query_node, cancellable); return yield MessageArchiveManagement.query_archive(stream, mam_params.mam_server, query_node, cancellable);
} }
public async QueryResult page_through_results(XmppStream stream, MamQueryParams mam_params, QueryResult prev_result, Cancellable? cancellable = null) { public async QueryResult page_through_results(XmppStream stream, MamQueryParams mam_params, QueryResult prev_result, Cancellable? cancellable = null) {
var query_node = create_base_query(stream, mam_params); var query_node = create_base_query(stream, mam_params);
query_node.put_node(ResultSetManagement.create_set_rsm_node_before(prev_result.first)); query_node.put_node(ResultSetManagement.create_set_rsm_node_before(prev_result.first));
return yield MessageArchiveManagement.query_archive(stream, MessageArchiveManagement.NS_URI_2, mam_params.mam_server, query_node, cancellable); return yield MessageArchiveManagement.query_archive(stream, mam_params.mam_server, query_node, cancellable);
} }
} }

View file

@ -4,15 +4,13 @@ using Xmpp.Xep;
namespace Xmpp.MessageArchiveManagement { namespace Xmpp.MessageArchiveManagement {
public const string NS_URI = "urn:xmpp:mam:2"; public const string NS_URI = "urn:xmpp:mam:2";
public const string NS_URI_2 = "urn:xmpp:mam:2";
public const string NS_URI_1 = "urn:xmpp:mam:1";
public class QueryResult { public class QueryResult {
public bool error { get; set; default=false; } public bool error { get; set; default=false; }
public bool malformed { get; set; default=false; } public bool malformed { get; set; default=false; }
public bool complete { get; set; default=false; } public bool complete { get; set; default=false; }
public string first { get; set; } public string? first { get; set; }
public string last { get; set; } public string? last { get; set; }
} }
public class Module : XmppStreamModule { public class Module : XmppStreamModule {
@ -36,45 +34,36 @@ public class Module : XmppStreamModule {
private async void query_availability(XmppStream stream) { private async void query_availability(XmppStream stream) {
Jid own_jid = stream.get_flag(Bind.Flag.IDENTITY).my_jid.bare_jid; Jid own_jid = stream.get_flag(Bind.Flag.IDENTITY).my_jid.bare_jid;
bool mam_available = yield stream.get_module(ServiceDiscovery.Module.IDENTITY).has_entity_feature(stream, own_jid, NS_URI);
bool ver_2_available = yield stream.get_module(ServiceDiscovery.Module.IDENTITY).has_entity_feature(stream, own_jid, NS_URI); if (mam_available) {
if (ver_2_available) {
stream.add_flag(new Flag(NS_URI));
feature_available(stream); feature_available(stream);
return;
}
bool ver_1_available = yield stream.get_module(ServiceDiscovery.Module.IDENTITY).has_entity_feature(stream, own_jid, NS_URI_1);
if (ver_1_available) {
stream.add_flag(new Flag(NS_URI_1));
feature_available(stream);
return;
} }
} }
} }
internal StanzaNode create_base_query(XmppStream stream, string ns, string? queryid, Gee.List<DataForms.DataForm.Field> fields) { internal StanzaNode create_base_query(XmppStream stream, string? queryid, Gee.List<DataForms.DataForm.Field> fields) {
DataForms.DataForm data_form = new DataForms.DataForm(); DataForms.DataForm data_form = new DataForms.DataForm();
DataForms.DataForm.HiddenField form_type_field = new DataForms.DataForm.HiddenField() { var="FORM_TYPE" }; DataForms.DataForm.HiddenField form_type_field = new DataForms.DataForm.HiddenField() { var="FORM_TYPE" };
form_type_field.set_value_string(NS_VER(stream)); form_type_field.set_value_string(NS_URI);
data_form.add_field(form_type_field); data_form.add_field(form_type_field);
foreach (var field in fields) { foreach (var field in fields) {
data_form.add_field(field); data_form.add_field(field);
} }
StanzaNode query_node = new StanzaNode.build("query", NS_VER(stream)).add_self_xmlns().put_node(data_form.get_submit_node()); StanzaNode query_node = new StanzaNode.build("query", NS_URI).add_self_xmlns().put_node(data_form.get_submit_node());
if (queryid != null) {
query_node.put_attribute("queryid", queryid); query_node.put_attribute("queryid", queryid);
}
return query_node; return query_node;
} }
internal async QueryResult query_archive(XmppStream stream, string ns, Jid? mam_server, StanzaNode query_node, Cancellable? cancellable = null) { internal async QueryResult query_archive(XmppStream stream, Jid? mam_server, StanzaNode query_node, Cancellable? cancellable = null) {
var res = new QueryResult();
if (stream.get_flag(Flag.IDENTITY) == null) { res.error = true; return res; } var res = new QueryResult();
Flag flag = Flag.get_flag(stream);
string? query_id = query_node.get_attribute("queryid");
if (flag == null || query_id == null) { res.error = true; return res; }
flag.active_query_ids.add(query_id);
// Build and send query // Build and send query
Iq.Stanza iq = new Iq.Stanza.set(query_node) { to=mam_server }; Iq.Stanza iq = new Iq.Stanza.set(query_node) { to=mam_server };
@ -82,7 +71,7 @@ public class Module : XmppStreamModule {
Iq.Stanza result_iq = yield stream.get_module(Iq.Module.IDENTITY).send_iq_async(stream, iq, Priority.LOW, cancellable); Iq.Stanza result_iq = yield stream.get_module(Iq.Module.IDENTITY).send_iq_async(stream, iq, Priority.LOW, cancellable);
// Parse the response IQ into a QueryResult. // Parse the response IQ into a QueryResult.
StanzaNode? fin_node = result_iq.stanza.get_subnode("fin", ns); StanzaNode? fin_node = result_iq.stanza.get_subnode("fin", NS_URI);
if (fin_node == null) { res.malformed = true; return res; } if (fin_node == null) { res.malformed = true; return res; }
StanzaNode? rsm_node = fin_node.get_subnode("set", Xmpp.ResultSetManagement.NS_URI); StanzaNode? rsm_node = fin_node.get_subnode("set", Xmpp.ResultSetManagement.NS_URI);
@ -91,7 +80,12 @@ public class Module : XmppStreamModule {
res.first = rsm_node.get_deep_string_content("first"); res.first = rsm_node.get_deep_string_content("first");
res.last = rsm_node.get_deep_string_content("last"); res.last = rsm_node.get_deep_string_content("last");
if ((res.first == null) != (res.last == null)) { res.malformed = true; return res; } if ((res.first == null) != (res.last == null)) { res.malformed = true; return res; }
res.complete = fin_node.get_attribute_bool("complete", false, ns); res.complete = fin_node.get_attribute_bool("complete", false, NS_URI);
Idle.add(() => {
flag.active_query_ids.remove(query_id);
return Source.REMOVE;
}, Priority.LOW);
return res; return res;
} }
@ -104,14 +98,36 @@ public class ReceivedPipelineListener : StanzaListener<MessageStanza> {
public override string[] after_actions { get { return after_actions_const; } } public override string[] after_actions { get { return after_actions_const; } }
public override async bool run(XmppStream stream, MessageStanza message) { public override async bool run(XmppStream stream, MessageStanza message) {
if (stream.get_flag(Flag.IDENTITY) == null) return false; Flag flag = Flag.get_flag(stream);
StanzaNode? message_node = message.stanza.get_deep_subnode(NS_VER(stream) + ":result", StanzaForwarding.NS_URI + ":forwarded", Xmpp.NS_URI + ":message"); StanzaNode? message_node = message.stanza.get_deep_subnode(NS_URI + ":result", StanzaForwarding.NS_URI + ":forwarded", Xmpp.NS_URI + ":message");
if (message_node != null) { if (message_node != null) {
StanzaNode? forward_node = message.stanza.get_deep_subnode(NS_VER(stream) + ":result", StanzaForwarding.NS_URI + ":forwarded", DelayedDelivery.NS_URI + ":delay"); StanzaNode? forward_node = message.stanza.get_deep_subnode(NS_URI + ":result", StanzaForwarding.NS_URI + ":forwarded", DelayedDelivery.NS_URI + ":delay");
DateTime? datetime = DelayedDelivery.get_time_for_node(forward_node); DateTime? datetime = DelayedDelivery.get_time_for_node(forward_node);
string? mam_id = message.stanza.get_deep_attribute(NS_VER(stream) + ":result", NS_VER(stream) + ":id"); string? mam_id = message.stanza.get_deep_attribute(NS_URI + ":result", NS_URI + ":id");
string? query_id = message.stanza.get_deep_attribute(NS_VER(stream) + ":result", NS_VER(stream) + ":queryid"); string? query_id = message.stanza.get_deep_attribute(NS_URI + ":result", NS_URI + ":queryid");
if (query_id == null) {
warning("Received MAM message without queryid from %s, ignoring", message.from.to_string());
return true;
}
if (!flag.active_query_ids.contains(query_id)) {
warning("Received MAM message from %s with unknown query id %s, ignoring", message.from.to_string(), query_id ?? "<none>");
return true;
}
Jid? inner_from = null;
try {
inner_from = new Jid(message_node.get_attribute("from"));
} catch (InvalidJidError e) {
warning("Received MAM message with invalid from attribute in forwarded message from %s, ignoring", message.from.to_string());
return true;
}
if (!message.from.equals(stream.get_flag(Bind.Flag.IDENTITY).my_jid.bare_jid) && !message.from.equals_bare(inner_from)) {
warning("Received MAM message from %s illegally impersonating %s, ignoring", message.from.to_string(), inner_from.to_string());
return true;
}
message.add_flag(new MessageFlag(message.from, datetime, mam_id, query_id)); message.add_flag(new MessageFlag(message.from, datetime, mam_id, query_id));
message.stanza = message_node; message.stanza = message_node;
@ -124,10 +140,15 @@ public class ReceivedPipelineListener : StanzaListener<MessageStanza> {
public class Flag : XmppStreamFlag { public class Flag : XmppStreamFlag {
public static FlagIdentity<Flag> IDENTITY = new FlagIdentity<Flag>(NS_URI, "message_archive_management"); public static FlagIdentity<Flag> IDENTITY = new FlagIdentity<Flag>(NS_URI, "message_archive_management");
public bool cought_up { get; set; default=false; } public bool cought_up { get; set; default=false; }
public string ns_ver; public Gee.Set<string> active_query_ids { get; set; default = new HashSet<string>(); }
public Flag(string ns_ver) { public static Flag get_flag(XmppStream stream) {
this.ns_ver = ns_ver; Flag? flag = stream.get_flag(Flag.IDENTITY);
if (flag == null) {
flag = new Flag();
stream.add_flag(flag);
}
return flag;
} }
public override string get_ns() { return NS_URI; } public override string get_ns() { return NS_URI; }
@ -155,8 +176,4 @@ public class MessageFlag : Xmpp.MessageFlag {
public override string get_id() { return ID; } public override string get_id() { return ID; }
} }
private static string NS_VER(XmppStream stream) {
return stream.get_flag(Flag.IDENTITY).ns_ver;
}
} }

View file

@ -68,6 +68,11 @@ public class Module : BookmarksProvider, XmppStreamModule {
} }
private void on_pupsub_item(XmppStream stream, Jid jid, string id, StanzaNode? node) { private void on_pupsub_item(XmppStream stream, Jid jid, string id, StanzaNode? node) {
if (!jid.equals(stream.get_flag(Bind.Flag.IDENTITY).my_jid.bare_jid)) {
warning("Received alleged bookmarks:1 item from %s, ignoring", jid.to_string());
return;
}
Conference conference = parse_item_node(node, id); Conference conference = parse_item_node(node, id);
Flag? flag = stream.get_flag(Flag.IDENTITY); Flag? flag = stream.get_flag(Flag.IDENTITY);
if (flag != null) { if (flag != null) {
@ -77,6 +82,11 @@ public class Module : BookmarksProvider, XmppStreamModule {
} }
private void on_pupsub_retract(XmppStream stream, Jid jid, string id) { private void on_pupsub_retract(XmppStream stream, Jid jid, string id) {
if (!jid.equals(stream.get_flag(Bind.Flag.IDENTITY).my_jid.bare_jid)) {
warning("Received alleged bookmarks:1 retract from %s, ignoring", jid.to_string());
return;
}
try { try {
Jid jid_parsed = new Jid(id); Jid jid_parsed = new Jid(id);
Flag? flag = stream.get_flag(Flag.IDENTITY); Flag? flag = stream.get_flag(Flag.IDENTITY);