Compare commits

...

347 Commits

Author SHA1 Message Date
fiaxh
dbedfe3f49 Conversation details: Allow topic edits for moderators 2025-06-15 11:59:23 +02:00
fiaxh
b2163a7ca5 File Send Dialog: Make file widget static 2025-06-14 21:48:14 +02:00
fiaxh
bd8f277b91 Add basic support for BOB thumbnails in SFS 2025-06-14 17:50:07 +02:00
fiaxh
1dae4e1abd Support media elements in data forms 2025-06-14 17:06:45 +02:00
fiaxh
1254a753eb Support media elements in data forms 2025-06-14 17:02:01 +02:00
fiaxh
5eabd7ac38 xmpp-vala: Add basic BOB support 2025-06-14 17:00:24 +02:00
fiaxh
2956401e48 xmpp-vala: Fix SFS mime_type wire-format 2025-06-14 16:59:18 +02:00
fiaxh
915b18789f Disregard SFS fallback messages even without source attachment given a full-body fallback indication 2025-06-13 19:40:09 +02:00
fiaxh
2e6454a92e Add support for whole body fallbacks 2025-06-13 19:37:27 +02:00
Jacques Comeaux
715b874801 Voice processor: add back call to set stream delay 2025-05-23 15:08:28 -06:00
fabrixxm
22616c7458 FileTransfer fix store source
Store source in db and add sources to `sfs_sources` is divided
in two functions. `persists()` now calls `store_source`, because
the sources it want to persist are already added to `sfs_sources`.
2025-05-23 19:05:53 +02:00
fabrixxm
0a564f80ab Fix parse incoming stateless file sharing sources 2025-05-23 18:18:50 +02:00
fiaxh
c40577ed76 Conversation details: Show used account if multiple are active 2025-05-05 14:32:17 +02:00
fiaxh
953b384ef7 Account details: Make own jid selectable 2025-05-05 14:12:30 +02:00
fiaxh
c8da1cfb53 Remove pixbuf from libdino api surface and xmpp-vala 2025-04-18 16:52:46 +02:00
Marvin W
5af1b2cd0d
AppData: Set display_length to 360
"small" was removed about a year ago.
2025-04-11 21:46:32 +02:00
Marvin W
af420fe443
Update flatpak dependencies 2025-04-11 20:50:52 +02:00
Marvin W
618af16bc7
Strip leading "v" from tag when generating display version 2025-04-11 19:22:51 +02:00
Marvin W
e23f2d8a68
Prepare for 0.5 release 2025-04-11 18:30:23 +02:00
Marvin W
f6815def35
Use debug instead of print 2025-04-11 18:30:23 +02:00
Translations
010d0c40ef Update translations 2025-04-11 18:09:39 +02:00
Marvin W
f5fde137bb
Fix potential crashes during account disabling 2025-04-11 11:51:36 +02:00
Marvin W
643a31535b
Clamp color channel values before converting to hex
This is needed as `GdkRGBA` may return out of range values when doing
colorspace conversion.

See https://gitlab.gnome.org/GNOME/gtk/-/issues/7488
2025-04-10 13:05:20 +02:00
Marvin W
e6e1d4aef4
xmpp: Remove connection timeout before trying a new connection
Fixes #1666
2025-04-09 20:44:51 +02:00
Marvin W
50eeb2f16d
RTP: Tear down most of GStreamer while not in a call.
This includes #1679 and works around an upstream bug in pipewire causing #1685
2025-04-09 20:05:13 +02:00
Translations
75b5aa9171 Update translations 2025-04-09 19:31:04 +02:00
fiaxh
50351d63c6 Fix locale search path finding on meson builds 2025-04-09 19:21:19 +02:00
fiaxh
5024f7c815 Fix corner cases in file transmission progress widget 2025-04-09 18:25:33 +02:00
fiaxh
f0e64ff96d Fix call window button style 2025-04-08 13:09:32 +02:00
fiaxh
fc6447c56e Set OMEMO as default encryption 2025-04-08 12:16:07 +02:00
fiaxh
a21800860e Show call button regardless of contact online status 2025-04-08 12:15:12 +02:00
fiaxh
ea54d659e5 Fix sending images with unknown formats 2025-04-07 18:12:33 +02:00
Marvin W
d621c96539
Use deprecated StyleContext.get_color() on GTK<4.14 2025-04-07 16:13:51 +02:00
Marvin W
5bf4223aca
Add Cairo fallback for GTK<4.14 2025-04-07 15:28:51 +02:00
Marvin W
7dc815ac4d
Fix unexpected behavior of get_attribute_uint on systems with 32-bit long 2025-04-07 14:11:05 +02:00
Marvin W
db1830c54e
Improve UI for sfs image progress indicator 2025-04-07 14:11:04 +02:00
Translations
97e7d4c18c Update translations 2025-04-06 21:31:38 +02:00
Marvin W
5b4d301d18
MUC: Close chat when it was closed elsewhere while disconnected 2025-04-06 18:11:38 +02:00
Marvin W
4234dce595
Flatpak: Update to use libomemo-c 2025-04-06 13:58:06 +02:00
Marvin W
7fa44a6ff6
OMEMO: Use libomemo-c instead of libsignal-protocol-c 2025-04-06 13:58:06 +02:00
Marvin W
12c0d27814
Drop CMake support 2025-04-06 13:58:06 +02:00
Marvin W
e434f51103
Meson: Add devel tag to installed VAPI .deps files 2025-04-04 21:00:19 +02:00
Marvin W
01cf823855
Meson: Add option to set rpath on installed binaries 2025-04-04 20:58:31 +02:00
Marvin W
87dfa8e670
Meson: Make plugindir relative to libdir 2025-04-04 20:14:49 +02:00
Marvin W
4b1be6f88f
Improve 24h time format detection 2025-04-04 20:14:45 +02:00
fiaxh
b979436069 Muji calls: Send accept/left to parent MUC 2025-03-27 18:41:18 +01:00
Metalhearf
aa59553c00 Update copyright year in about window 2025-03-27 18:41:00 +01:00
Translations
8320d5a357 Update translations 2025-03-12 16:43:59 +01:00
fiaxh
4cde7809d1 Fix try reconnecting account that hasn't been connected before 2025-03-12 16:13:00 +01:00
Marvin W
9a2ef6ba50
Update Flatpak runtime 2025-03-11 13:54:23 +01:00
fiaxh
fa3a77958a Fix subscription-missing notification appearing in self-chat 2025-02-27 20:56:11 +01:00
Marvin W
e2872d8bcc
Voice processor: merge init and setup methods
This ensures we are never in an uninitialized state when processing audio data
2025-02-24 21:26:04 +01:00
eerielili
76261a2ac5 Update README.md with meson build instructions 2025-02-24 18:12:42 +01:00
Marvin W
a2768774e3
Fix build with webrtc-audio-processing 2.x 2025-02-23 19:31:09 +01:00
Marvin W
1c365daa14
Voice processor: add support for webrtc-audio-processing 1.x and 2.x 2025-02-22 21:06:26 +01:00
Ferdinand Stehle
f796286650
Apply image orientation in conversation view (#1672)
* Apply image orientation in conversation

* Load the full image into pixbuf

---------

Co-authored-by: fiaxh <git@lightrise.org>
2025-02-22 09:32:33 -06:00
Marvin W
fc42c657ee
Fix vertical picture padding for certain aspect ratios 2025-02-22 16:15:33 +01:00
Marvin W
834b1336db
Fix various sizing issues 2025-02-22 11:27:48 +01:00
Marvin W
efe39b8ea0
Fix compatibility with GTK 4.17+ 2025-02-22 11:27:48 +01:00
fiaxh
21869c92a2 SFS: Fix video metadata querying not returning 2025-02-21 15:05:55 +01:00
Marvin W
0b17090231
CMake: Mark as deprecated 2025-02-12 21:23:53 -06:00
Marvin W
b65120373c
Update GitHub actions 2025-02-12 21:23:53 -06:00
Marvin W
f99f0dee52
Flatpak: Use Meson build 2025-02-12 21:23:53 -06:00
Marvin W
901dfff081
DOAP: Fix XML 2025-01-27 11:17:53 +01:00
Miquel Lionel
2ce0d9d3e2 fix: Query duplication in global message search (#1636) 2025-01-24 10:44:47 +01:00
Marvin W
fd915ac76f
Fix build for Vala < 0.56.5 2025-01-18 20:05:46 +01:00
Marvin W
1e5bb8b7e2
Fix build with GTK<4.8 or Meson<1.1.0 2025-01-18 19:49:09 +01:00
Marvin W
b4241eb94a
Meson: Add build option summary 2025-01-18 19:39:25 +01:00
Marvin W
73e28e6c61
Meson: Add all defines to handle dependency versions 2025-01-18 19:39:13 +01:00
Marvin W
b98b597c92
Meson: Add tests 2025-01-12 20:00:51 +01:00
Marvin W
fb26066be4
Meson: Install icons to correct location 2025-01-12 20:00:51 +01:00
Marvin W
613cb5e2d7
Meson: Adjust version generation to match CMake
and be compatible with vala-language-server
2025-01-12 20:00:51 +01:00
Marvin W
a63b5ccb9a
RTP: gtksink no longer needed 2025-01-12 20:00:51 +01:00
fiaxh
6322ed819e Show banner when contact is not yet subscribed 2025-01-12 19:35:58 +01:00
fiaxh
5ee322cbd9 Contact details dialog: Add encryption tab 2025-01-12 19:35:58 +01:00
fiaxh
d78ec05622 Show file upload/download progress 2025-01-12 19:35:06 +01:00
Marvin W
d5c2804769
Search: Build preview string based on chars not bytes 2025-01-03 17:44:43 +01:00
Marvin W
398c52e1b9
Work around pango bug
See https://gitlab.gnome.org/GNOME/pango/-/issues/798 and https://gitlab.gnome.org/GNOME/pango/-/issues/832
2025-01-03 17:44:43 +01:00
fiaxh
b490eb662f
Update README.md 2025-01-02 08:01:45 -06:00
Marvin W
b3976246ec
Fix memory leak when displaying image files 2024-12-27 16:44:22 +01:00
Marvin W
045e555357
Update DOAP 2024-12-27 16:44:21 +01:00
mesonium
eb827175ae
fix: Apply CSS with toolkit and platform version checks (#1643) 2024-12-18 12:51:39 +01:00
fiaxh
79f792e090 Fix and improve stateless file-sharing 2024-11-15 14:40:08 -06:00
Patiga
aaf4542e62 Implement XEP-0447: Stateless file sharing 2024-11-14 10:20:12 -06:00
fiaxh
909f569318 xmpp-vala: StanzaNode.get_attribute_int: Return default value if not parsable as int 2024-11-14 10:20:12 -06:00
Marvin W
a6554e81c5
Fix 0482 call initiation in groups 2024-10-30 11:45:09 +01:00
rodneyrod
f4df3f9a7a Update build.yml to use Gnome 46
Fixes #1596
2024-10-25 15:43:14 +02:00
fiaxh
1e9a45f481 Use XEP-0482 for multi-party call invites 2024-10-18 14:16:33 +02:00
fiaxh
3abe9fdb9e Fix crash on account creation (resource generation) 2024-10-18 14:16:19 +02:00
fiaxh
4d4ba7dde6 Generate new resource on resource conflict 2024-09-29 16:57:31 +02:00
fiaxh
64e4fb0037 Handle TlsError when reading from stream (caused uncatched Exception) 2024-09-29 16:57:31 +02:00
eerielili
439c375393
Store avatars in the user's cache directory. (#1621)
* Store avatars in the user's cache directory.

	- Not anymore in ~/.local/share, where media files are stored.
	- Already existing ~/.local/share/dino/avatars directory will be
	  moved to ~/.cache/dino/avatars
	- If both directories already exists, the old one (in
	  ~/.local/share) is removed.

* Simplify old-to-new-location logic

---------

Co-authored-by: fiaxh <git@lightrise.org>
2024-09-28 16:40:03 +02:00
Linux in a Bit
8853ffead3
Change message padding/margins (#1564)
3px padding on top and bottom of all messages
10px margin on top of messages with usernames
This improves readability among other things.

Co-authored-by: Linux in a Bit <105567407+RageGamerBoi@users.noreply.github.com>
2024-09-23 14:27:34 +02:00
eerielili
d20553a111
Add keyboard shortcut to show preferences (Ctrl+,) (#1435) 2024-09-22 13:27:55 +02:00
fiaxh
3a4ac7c11d XEP-0215: Remove timeout from previous connection 2024-09-22 11:39:13 +01:00
Alexandre Jousset
63439cd206 XEP-0215: management of the field expires
Add a field in `Xmpp.Xep.ExternalServiceDiscovery` to keep track
of the `expires` TURN service value and use it (divided by 2) to
restart periodically the external services discovery.
2024-09-22 11:39:13 +01:00
fiaxh
a7f7b1ceeb Ignore bad stun/turn IP addresses 2024-09-22 11:39:13 +01:00
fiaxh
f8c004630f Add change password functionality
Co-authored-by: Stanislav Malishevskiy <stanislav.malishevskiy@gmail.com>
2024-09-21 17:06:20 +01:00
fiaxh
b09556f033 Preferences Window: Small improvements 2024-09-16 22:45:47 +02:00
hrxi
65404b2442 Discover plugins in Meson build directories
If the environment looks like a Meson build dir (parent directory is
called "main"), then also look for plugins in `../plugins/*`.

Fixes #1591.
2024-09-15 21:07:59 +02:00
fiaxh
228640a881 Code cleanup: Remove remnants of previous accounts dialog 2024-09-15 21:01:24 +02:00
fiaxh
e8f82fd328 Code cleanup: Remove broken disable CSD code 2024-09-15 20:15:14 +02:00
fiaxh
63ba0bc936 Refresh "Add account" UI 2024-09-15 19:35:00 +02:00
fiaxh
13123dced1 Use Adwaita 1.2, bump CI to Ubuntu 24.04 2024-09-15 19:35:00 +02:00
Pigpog
91c8c8eb49 Use new call.incoming notification category
This notification category is pending merge in FreeDesktop. See https://gitlab.freedesktop.org/xdg/xdg-specs/-/merge_requests/50
2024-09-14 22:08:49 +02:00
Translations
a9a21a2dc8 Update translations 2024-09-14 21:55:16 +02:00
Translations
5a4e31e888 Update translations 2024-09-14 20:43:55 +02:00
Translations
51252f74c9 Update translations 2024-08-22 19:52:09 +02:00
fiaxh
1431426581 Omemo: Connect listener only once on account added 2024-08-19 12:28:45 +02:00
fiaxh
dc57561dcf Add cancellable to stream connect 2024-08-19 12:28:45 +02:00
fiaxh
88376cd6f7 Conversation details: Make block button a stateful menu button 2024-08-19 11:36:17 +02:00
Miquel Lionel
8e7dedcaa6 Allow blocking entire domain from conversation details
- the block domain option is in a drop down of the block button
	- when blocking the domain, the "Blocked domain" button appears
	  and block button disappears and vice versa.
2024-08-19 11:36:17 +02:00
Matthew Fennell
cfb0ca2a64
Add omemo key management tooltip (#1603) 2024-08-18 20:40:55 +02:00
fiaxh
b0ff90a14a Add initial message markup (XEP-0394) support 2024-07-29 13:16:54 +02:00
fiaxh
ceb921a014 Store reply message as sent, with fallback 2024-07-29 13:15:14 +02:00
fiaxh
ca980ea409 Fix shift+enter in chat input not scrolling down 2024-07-20 19:15:13 +02:00
Marvin W
c95b65e5b4
OMEMO: Do not show message for OMEMO messages without payload 2024-07-19 18:26:18 +02:00
Translations
3497b3898c Update translations 2024-07-01 12:55:23 +02:00
Translations
7374218ece Update translations 2024-07-01 12:11:43 +02:00
Marvin W
5cf8809e3b
Update appdata 2024-06-30 22:33:06 +02:00
Matthew Fennell
f55b27716a Allow self-signed .onion file transfer certs (#1149)
Most Certificate Authorities don't support issuing X.509 certificates for onion
sites. However, it can still be useful to provide a certificate over Tor in
some circumstances, for instance to tie your alphanumeric Tor address to your
site's main identity.

Therefore, many Tor services provide self-signed certificates. This is OK,
since the onion service itself guarantees that you are connecting to the entity
you think you are.

Dino already allows self-signed certs when communicating over Tor (see
81a5505). However, the same exception does not exist yet for HTTP uploads and
downloads - causing these to fail over Tor.

Therefore, in this commit, we add the same exception for uploads/downloads, by
passing the host of the upload/download urls to the already existing invalid
certificate connection handler.

Note that this handler only allows certificates with type
TlsCertificateFlags.UNKNOWN_CA. This means the certificate of your server must
also include the onion http upload and download URLs in its certificate -
otherwise, the file transfer will fail with TlsCertificateFlags.BAD_IDENTITY.
2024-06-29 11:54:08 +02:00
Matthew Fennell
da4ded964f
Self-ping with server-given roomnick (#1594)
* Self-ping with server-given roomnick (#1467)

XEP-0045 describes how a server can modify a user's roomnick when joining a
room. To do this, it assigns status code 210, and sets the "from" attribute to
a JID with thew newly assigned nickname when sending the user's presence back
to the client.

This is used in IRC gateways such as biboumi. Since you can only have one nick
per IRC server, if you try and join multiple IRC-bridged MUCs on the same IRC
server, it will modify your nick to match the existing one you have on that
server.

Currently, when Dino performs a self-ping after joining a room, it uses the
nick that the client first requested, instead of the nick returned from the
server. This can lead to incorrect self-pings being sent out (until the client
is restarted, and populates mucs_todo correctly). This happens because it adds
the requested Jid to mucs_todo, before getting the given nick from the server.

Therefore, this commit adds the jid that has been given from the server
instead. If there is any error in requesting to join the MUC, it adds the
requested nick, to match existing behaviour and allow the request to be retried
via future self-pings.

fixes #1467

* Minor coding style changes

---------

Co-authored-by: fiaxh <git@lightrise.org>
2024-06-28 16:33:59 +02:00
fiaxh
749a0f1fae Join Channel dialog: Fix displaying of bookmark details 2024-06-23 23:02:27 +02:00
fiaxh
90ea9c4da4 Preferences dialog: Various improvements
- Only show avatar remove button if one is set, fixes #1589
- Only show account picker if user has more than one account
2024-06-21 17:58:04 +02:00
fiaxh
8b15417e0f Start chat: Adjust status icon position, prioritize statuses, update status 2024-06-21 14:09:44 +02:00
Miquel Lionel
00188bdf9b Show contact status in "Start chat" window
should close #139
- adds svg dino-status-offline.svg
2024-06-21 13:08:28 +02:00
fiaxh
984ae3f5b8 Settings dialog: Fix account subpage for Adwaita < 1.4, other fixes
fixes #1592
2024-06-20 18:57:24 +02:00
fiaxh
680f0dd0a7 Fix meson build files 2024-06-20 14:03:14 +02:00
fiaxh
21ae42762d Redesign and rewrite accounts and settings dialog into a combined one 2024-06-20 12:14:46 +02:00
fiaxh
c8b20d0f5f Store requested disco results with computed hash, use for offline determining of private MUCs 2024-06-19 20:08:07 +02:00
fiaxh
f1be90c02f Add logic for OMEMO by default setting 2024-05-26 17:28:28 +02:00
fiaxh
fe45ab575c Support avatar deletion 2024-05-26 17:21:04 +02:00
Marvin W
8be4f02723
AppData/DOAP: Use sentence case for summary.
The suggestion is to not use title case which we did before.
2024-05-09 16:06:32 +02:00
Marvin W
6eeac8a5a1
Flatpak: Bump runtime version to 46 2024-05-09 14:02:14 +02:00
Marvin W
20d84a4945
AppData: Add brand color 2024-05-09 12:48:11 +02:00
Christopher Vollick
d473efcbfe
Add WeakTimeout Pattern to Prevent Leaks
While doing testing I noticed that skeletons were being leaked, and
eventually tracked it down to the timer that updates the time label
closing over "this" and then keeping the reference alive, potentially
for 24 hours.

I noticed a few other places in the code doing some version of this, and
one of them had the "static and weak pointer" approach, which I pulled
out into a util class. Now, we still have to make sure we're passing it
a static method instead of a lambda, as that would also close over
"this" and render the whole thing useless, but at least most of the
annoying parts live in the util class now.

Also the call_widget version was doing a weird thing where it was
removing itself, but then returning "true"? I'm not sure what that
accomplishes, because returning "false" means to not run this again. So
I think my new version is the same in practice, but simpler...

There are other timeouts in the code that I briefly looked over, but all
of them seemed to be relatively short hard-coded durations, so I left
them alone.

But if any of them are long-lived, it's possible they could also benefit
from this class in the future.

Closes #1480

Co-Authored-By: Marvin W <git@larma.de>
2024-04-28 21:36:27 +02:00
Marvin W
ba83a4ba3d
Calls: Correctly display information in partially encrypted calls
This should never happen in practice, but now we will correctly display
if a call has encrypted audio, but unencrypted video, or vice-versa.
2024-04-28 21:35:07 +02:00
Rico Tzschichholz
6575029555
Annotating with array_null_terminated doesn't imply a missing array_length
Taken from 6b8a3e4faa
2024-04-28 21:35:07 +02:00
eerielili
d9fa4daa6a
Fix message stanza with body changing MUC subject (#1569)
- fixes https://github.com/dino/dino/issues/1542
            - more consistent with
              https://xmpp.org/extensions/xep-0045.html#enter-subject:
                    "Note: In accordance with the core definition of XML stanzas,
                    any message can contain a <subject/> element; only a message that
                    contains a <subject/> but no <body/> element shall be considered a
                    subject change for MUC purposes."
2024-04-01 13:33:11 +02:00
fiaxh
bd8e03b7ea Remove conversation closing via hover button 2024-03-21 13:52:39 +01:00
fiaxh
2f1b806dfe Introduce conversation menu, add close option 2024-03-21 13:52:39 +01:00
fiaxh
37e81a86dc Conversation details dialog: Fix runtime critical 2024-03-21 13:34:20 +01:00
Alexandre Jousset
bf9f401743
configure: fix typo (VALACFLAGS) (#1550) 2024-03-02 13:30:25 +01:00
eerielili
c8700b44f4
Fix poor contrast of highlight in search results with dark theme (#1557)
- fixes #1308
2024-03-02 13:27:44 +01:00
fiaxh
4cc7e076e6 Add unread indicator
Co-authored-by: Alexandre Jousset <mid@gtmp.org>
Co-authored-by: Aidan Epstein <aidan@jmad.org>
2024-03-02 13:18:53 +01:00
eerielili
7e3cedaf3f
Enable hyperlinks in topic text to be clicked (#1523)
fixes #1042
2024-01-13 14:27:30 +01:00
Teemu Ikonen
732d3a9814
Change select contact dialog container to AdwClamp (#1533) 2024-01-13 13:56:13 +01:00
eerielili
22516c1862
Fix crash on removing conference not in roster (#1516) 2024-01-10 21:20:50 +01:00
fiaxh
384ef1d3f1 Conversation details dialog: Fix notification+block icons 2023-12-10 15:03:02 +01:00
eerielili
4689fcb53c
Fix segfault opening conversation details when no XEP-0191 support (#1513)
fixes #1508
2023-12-10 13:28:22 +01:00
eerielili
85ea7e5008
Fix http upload for servers without file size limit (#1512)
* Fix for ejabberd XMPP server 'infinity' http upload file size announce

	- fixes https://github.com/dino/dino/issues/1222

* Update 0363_http_file_upload.vala
2023-11-24 22:13:57 +01:00
Alexandre Jousset
cb78cec9e2 main/meson.build:121: fix typo 2023-11-13 22:45:40 +01:00
hrxi
e93e14b12c rtp plugin doesn't depend on GnuTLS 2023-11-13 22:27:50 +01:00
eerielili
86b101900c
Start conversation if closed when receiving an audio or video call (#1485)
* Start conversation if closed when receiving an audio or video call

* Fix starting conversation on new calls, move setting conversation.last_active

---------

Co-authored-by: fiaxh <git@lightrise.org>
2023-10-08 13:51:30 +02:00
fiaxh
8cb195a274 Fix crash due to gpg binding issue 2023-10-07 16:54:09 +02:00
fiaxh
1e167eeea6 Fix some compiler warnings 2023-10-07 14:34:23 +02:00
fiaxh
0c45387bf9 Fix implicit-function-declaration compiler warnings 2023-10-07 13:56:38 +02:00
hrxi
c312fb282f meson: Add version detection for some dependencies 2023-10-06 15:25:12 +02:00
hrxi
a55a10e88f meson: Add RTP options that are also present in the CMakeLists.txt 2023-10-06 15:25:12 +02:00
hrxi
bfc1962f70 meson: Allow enabling/disabling plugins 2023-10-06 15:25:12 +02:00
hrxi
e6938c2965 meson: Add rtp plugin 2023-10-06 15:25:12 +02:00
hrxi
715fabb5bb meson: Add omemo plugin 2023-10-06 15:25:12 +02:00
hrxi
3edda368f3 meson: Add ice plugin 2023-10-06 15:25:12 +02:00
hrxi
7dd0e0aa4a meson: Add crypto-vala library 2023-10-06 15:25:12 +02:00
hrxi
7dd12e7dec meson: Add notification-sound plugin 2023-10-06 15:25:12 +02:00
hrxi
7326ca4d1b meson: Add openpgp plugin 2023-10-06 15:25:12 +02:00
hrxi
6d838c1c31 meson: Add http-files plugin 2023-10-06 15:25:12 +02:00
hrxi
62ed82a495 meson: Install more stuff
Install .vapi, .deps, .h files for the Vala libraries. Also install the
data files. .deps files have to be manually generated, there's a feature
request for automated generation at
https://github.com/mesonbuild/meson/issues/9756.

Import the gnome module globally.

Install dependencies on Meson CI.
2023-10-06 15:25:12 +02:00
hrxi
6eb1b53e60 Merge signal-protocol into omemo plugin
Same reasoning as for the `openpgp` plugin.
2023-10-06 15:25:12 +02:00
hrxi
e2d801b5f7 Merge gpgme-vala into openpgp plugin
There's no reason for it to be a statically linked library anymore, it
can be directly compiled into the plugin.
2023-10-06 15:25:12 +02:00
hrxi
dd0038f5e2 Fix every inclusion of gpgme_fix.h getting their own mutex 2023-10-06 15:25:12 +02:00
fiaxh
c2efb214af conversation details: Fix for libadwaita < 1.4 2023-09-25 15:02:03 +02:00
fiaxh
e2c34bf223 Rewrite contact details dialog 2023-09-24 19:54:04 +02:00
Marvin W
9eafe4139d Fix build on some Vala compiler versions
See https://gitlab.gnome.org/GNOME/vala/-/issues/1474 and https://gitlab.gnome.org/GNOME/vala/-/issues/1478
2023-09-24 19:51:33 +02:00
fiaxh
2fba24ccae Fix subscription notification clearing 2023-09-07 21:30:47 +02:00
mesonium
bc5a1d35cb
fix: Add x node to MUC PM stanza (#1462)
Add <x/> tag in MUC-PMs to support better Carbon delivery in
compliance with XEP-0045 v1.28 and above.

Fixes #1306
2023-07-29 14:02:38 +02:00
Kim Alvefur
d0fca291ac Fix showing the kick option to owners
Missing case in the switch defaulted to returning false for Owners, thus
preventing they with the most privileges from using those privileges.
2023-07-29 13:52:11 +02:00
Marvin W
8c8c2dc4b0
Fix potential crash in video calls 2023-07-09 15:32:53 +02:00
Marvin W
7357b7ecfb Fix certificate start time
I doubt anyone ever looked at it, but it shouldn't be 1 day in the future ;)
2023-07-09 14:32:33 +02:00
Marvin W
1bf57a42fa Do not send DTLS datagrams to RTP even after handshake
Also post debug message in case we drop datagrams
2023-07-09 14:32:33 +02:00
Stephen Paul Weber
f82f788f43 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 14:32:33 +02:00
Robert Mader
35163f08f9 data: Set X-Purism-FormFactor in .desktop file
So the app is detected as mobile-friendly on Phosh.
2023-07-08 11:23:44 +02:00
fiaxh
b830b796a5 Cleanup automatically loaded help overlay 2023-07-08 11:20:51 +02:00
Christopher Davis
3d5dad25d8 application: Load help overlay automatically
GTK automatically loads and sets up the action
and keyboard shortcut for the Keyboard Shortcuts
dialog. We don't need to manually do it as long as
we put everything in the right place.

See https://docs.gtk.org/gtk4/class.Application.html#automatic-resources
2023-07-08 11:20:51 +02:00
Christopher Davis
a36a63d7e4 main_window: Use AdwApplicationWindow
The main window of an app should be an ApplicationWindow.
These windows provide nicer APIs for actions and more.
2023-07-08 11:20:51 +02:00
eerielili
da7be50f05
Add a keyboard shortcut to show keyboard shortcuts (#1432)
Add a keyboard shortcut to show keyboard shortcuts

    - It's Ctrl+?
2023-06-25 13:39:07 +02:00
Tobias Bernard
4bb0c465fc
icons: Refresh some symbolic icons (#1444) 2023-05-29 22:01:33 +02:00
Marvin W
9a04573fdc Fix reactions being made to the wrong message
fixes #1426
2023-05-14 16:44:37 +02:00
fiaxh
9e4def221f Fix chat input for IME
fixes #1419

Co-authored-by: Marvin W <git@larma.de>
2023-05-14 14:12:05 +02:00
fiaxh
287d5bee6e Fix chat input status having a fixed width requirement
fixes #1439
2023-05-13 14:45:37 +02:00
fiaxh
4dfe853fbf Fix xml output intendation 2023-05-13 14:45:37 +02:00
fiaxh
0bddf9f3da Fix character counting for fallbacks
fixes #1420
2023-05-01 19:21:05 +02:00
Karim Malhas
ec6c24c2b4 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-04-23 11:53:57 +02:00
fiaxh
10315a245d Code cleanup: Remove left-over usages of mam_earliest_synced 2023-04-23 11:48:29 +02:00
fiaxh
2b9a0ccf7e Fix crash on NS_URI call when own server has no MAM; drop broken mam:1 "support"
fixes #1405
2023-04-23 11:40:06 +02:00
Marvin W
6e60cfcbbe
Fix empty alias being handled different than none 2023-04-22 20:08:49 +02:00
fiaxh
03e367ecb8 Fix call window styling 2023-04-22 19:52:28 +02:00
Marvin W
83476d1cad
Fix Flatpak pipewire socket access 2023-04-22 17:19:40 +02:00
fiaxh
5815e757b7 Fix call window controlls hiding 2023-04-22 17:07:29 +02:00
Marvin W
dbb8abc117
Fix video for cameras with rotated image 2023-04-22 17:04:28 +02:00
Marvin W
cad066628a
Build: Adjust to never build with libsignal-protocol-c 2023-04-22 17:03:22 +02:00
Marvin W
bc3738aba1
Fix GitHub CI build-flatpak 2023-04-22 17:03:21 +02:00
Sonny Piers
9b83e5ccc9 Add Github CI job for Flatpak 2023-04-21 00:41:52 +02:00
Sonny Piers
d2ac7a8aeb Add Flatpak manifest 2023-04-21 00:41:52 +02:00
Klemens Nanni
b75b6062ab Always export symbols to fix startup on BSDs
```
$ dino
(dino:38515): Gtk-ERROR **: 15:38:38.538: failed to add UI from resource /im/dino/Dino/unified_main_content.ui: .:26:1 Invalid object type 'DinoUiConversationSelector'
Trace/BPT trap (core dumped)
```

This works on Linux because CMake itself links with `-rdynamic` by default
as per its `Modules/Platform/Linux-*.cmake`.

OpenBSD carries this as local patch, FreeBSD links with `--export-dynamics`.
Just linking with `-rdynamic` also fixes it on OpenBSD, as expected.

https://cmake.org/cmake/help/latest/prop_tgt/ENABLE_EXPORTS.html

Fix #438.
2023-03-24 19:36:32 +01:00
hrxi
32e535a79c Add CI for the meson build 2023-03-24 19:32:50 +01:00
hrxi
5a90e793dd First steps of meson support
Basic configuration of qlite, xmpp-vala, the Dino library and the Dino
application are supported. There's no support for the plugins.

This e.g. enables using the Vala language server.
2023-03-24 19:32:50 +01:00
hrxi
b617bf7cc4 Make members of Plugins.Registry public instead of internal
They are being used from outside the library.
2023-03-24 19:32:50 +01:00
fiaxh
65efaca6fd
Fix images from another client in our account not being displayed right away 2023-03-23 12:14:22 -06:00
Marvin W
ef8fb0e94c
Check sender of bookmark:1 updates 2023-03-23 11:37:47 -06:00
Marvin W
6690d8e4a4
Bind soup session lifetime to File provider/sender lifetime
Required since libsoup 3.4. Fixes #1395
2023-03-22 12:35:13 -06:00
Bohdan Horbeshko
adb2b58b61 Fix a crash if a message subnode is not found in a carbon
Fixes #1392
2023-03-21 17:57:56 -06:00
Sebastian Krzyszkowiak
444275a99d FreeDesktopNotifier: Set notification categories
This provides notifications servers some context on how to handle
the notification.
2023-03-21 17:57:33 -06:00
Michael Vetter
ecf94dd2e6 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-21 17:57:07 -06:00
Sebastian Krzyszkowiak
57d47b9575 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-21 17:56:53 -06:00
Marvin W
4e1311dfa9
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-21 17:35:58 -06:00
Marvin W
3721027edb
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-21 17:35:58 -06:00
Marvin W
cb10110c57
Fix C binding for gst_video_frame_get_data
Fixes #1267
2023-03-20 15:53:53 -06:00
Marvin W
748d507a3e
Add missing since to DOAP 2023-03-20 15:53:53 -06:00
Marvin W
47a066987d
DOAP: Add first supported version for more XEPs 2023-03-07 09:47:29 -06:00
Marvin W
56195fd2b0
Update XEPs in DOAP
Fixes #1376
2023-03-06 19:38:22 +01:00
Marvin W
db3b0d5f23
New Avatar UI 2023-03-05 16:47:46 +01:00
Marvin W
d818296520
Implement XEP-0392: Consistent Color Generation 2023-03-05 16:47:46 +01:00
Marvin W
503de303d7
Consider stream readable when EOS is reached.
Fixes #1373
2023-03-02 00:02:35 +01:00
Marvin W
74ca991ddf
Fix critical warnings after DTLS-SRTP calls without OMEMO verification
libdino-CRITICAL **: dino_plugins_encryption_list_entry_get_encryption_icon_name: assertion 'self != NULL' failed
2023-03-02 00:02:35 +01:00
Marvin W
76e1410c2a
Fix typing notifications in groupchats 2023-03-02 00:02:32 +01:00
fiaxh
fb799e3ba8 Fix some memory leaks 2023-02-27 23:38:31 +01:00
fiaxh
c526848098 Stop regenerating message menu buttons
mitigates #1343
2023-02-16 12:51:33 +01:00
fiaxh
f7750c548a Clear chat input after /command
fixes #1359
2023-02-10 14:25:04 +01:00
fiaxh
9bf304095c Remove spell check setting
As there is currently no spell check support for GTK4
2023-02-09 12:35:50 +01:00
fiaxh
e554f90ff9 Make the symbolic app icon square 2023-02-09 12:11:09 +01:00
fiaxh
634302217c Settings: Connect to Switch.notify-active instead of to activate
As per the documentation of the activate signal "Applications should never connect to this signal, but use the notify:active signal."
https://valadoc.org/gtk+-3.0/Gtk.Switch.activate.html
2023-02-09 11:36:33 +01:00
Christopher Davis
430a8df4f6 menu_app: Move preferences to last group
Per
https://developer.gnome.org/hig/patterns/controls/menus.html#standard-primary-menu-items, preferences
should be at the top of the last group in the menu.
2023-02-09 11:36:33 +01:00
Christopher Davis
5eab892a24 menu_app: Rename Settings to Preferences
The standard name in GNOME applications is Preferences.
2023-02-09 11:36:33 +01:00
Christopher Davis
b19986a685 settings_dialog: Use AdwPreferencesWindow and AdwActionRow
AdwPreferencesWindow contains a nice API for preferences
windows, and AdwActionRow is the widget to use for rows
of preferences.
2023-02-09 11:36:33 +01:00
Marvin W
5568bbc6bf
Prepare for 0.4 release 2023-02-07 21:44:15 +01:00
Translations
e73b556a1a Update translations 2023-02-07 21:40:04 +01:00
Marvin W
9c5e36020d
Don't accept corrections from MUC MAM
We don't know if they're from the same user as someone else could have joined with the same nickname after sender left
2023-02-07 21:36:33 +01:00
Marvin W
116682e311
Fix various date/time stamps not updated or wrong time zone 2023-02-07 20:23:52 +01:00
Marvin W
32ae87a3c4
Fix some form entries not updating properly 2023-02-07 20:23:51 +01:00
Marvin W
1559a7a603
Show "Me" instead of bare jid when no local alias is set 2023-02-07 20:23:51 +01:00
Marvin W
d092473fe4
Improve history sync under load 2023-02-07 10:50:45 +01:00
Marvin W
1d123c7e66
Fix label attributes updated with delay 2023-02-07 10:50:45 +01:00
Marvin W
f74c1f18b1
Deduplicate messages before storing in database 2023-02-07 10:50:45 +01:00
Marvin W
d76e12b215
Add priority for and allow cancellation of outgoing stanzas 2023-02-07 10:50:45 +01:00
Marvin W
18321ed15c
Collapse most stream releated errors into IOError 2023-02-07 10:50:43 +01:00
fiaxh
95fefaff51 OMEMO: Make device list public 2023-02-05 18:49:32 +01:00
fiaxh
e0ece2aa62 Fix placeholder being shown on startup, fix missing vertical line in no-conversations placeholder 2023-01-31 20:16:23 +01:00
Marvin W
3aa3912dc3
New Date Separator 2023-01-31 15:15:55 +01:00
Marvin W
921f28c84b
Fix reaction display in private MUCs 2023-01-31 15:14:55 +01:00
Marvin W
9e11bef219
Fix critical warning due to tooltip issue 2023-01-31 15:14:47 +01:00
fiaxh
1e23b7bbd2 Fix reading reactions in private groups w/o occupant ids 2023-01-31 11:06:53 +01:00
Prashant Kumar
e3c833bce0
Add tooltips to the delivered and read icons (#1341) 2023-01-30 23:19:53 +01:00
fiaxh
b0b81b88c6 Always display reaction+reply buttons, disable if not possible 2023-01-30 22:54:55 +01:00
fiaxh
10a2bce512 Fix build 2023-01-28 15:58:33 +01:00
fiaxh
1dbacbbcab Remove nl_BE from LINGUAS files and appdata file (fixup d0a00e1) 2023-01-28 15:44:25 +01:00
Translations
d0a00e1e75 Remove Dutch (Belgium) translation, fallback to Dutch
fixes #1231
2023-01-28 15:04:48 +01:00
Translations
04eb0e763b Update translations 2023-01-28 14:56:04 +01:00
fiaxh
e833a924b5 Update appdata file with content_rating, releases, update image tags 2023-01-28 01:38:51 +01:00
fiaxh
c813a6d240 Fix QR code being displayed way too small
fixes #1278
2023-01-28 00:52:38 +01:00
fiaxh
5d9978b38b Reactions: Fix fallback bodies being displayed as messages
fixes #1352
2023-01-27 21:55:51 +01:00
fiaxh
26be9d4bb4 Fix reactions from MAM getting attributed to the wrong message 2023-01-25 19:42:19 +01:00
Marvin W
e35df88d4a
Fix UI for libadwaita 2023-01-25 11:02:02 +01:00
Marvin W
cc7db3b85f
Fix scaling image for GTK4 2023-01-24 19:21:25 +01:00
Marvin W
99d9cb383a
Small UI fixes for libAdwaita 2023-01-24 19:21:25 +01:00
fiaxh
6a182ba313
Only use Adw.AboutWindow for Adwaita >= 1.2 2023-01-24 19:21:25 +01:00
fiaxh
ef98adb18a
Add libadwaita dependency to gitlab CI 2023-01-24 19:21:25 +01:00
fiaxh
4b391f3f31
Use Adw.StatusPage instead of custom placeholders 2023-01-24 19:20:43 +01:00
fiaxh
92aca5672d
Improve libadwaita integration 2023-01-24 19:20:43 +01:00
Teemu Ikonen
0d7c8bb6e1
Change Gtk.Paned to Adw.Leaflet in MainWindow 2023-01-24 19:20:43 +01:00
Teemu Ikonen
e934a76a11
Add back button to ConversationTitlebar
Add a bool 'back_button_visible' and a signal 'back_pressed' to the
ConversationTitlebar interface.

Also add implementations to both ConversationTitlebarNoCsd and
ConversationTitlebarCsd.
2023-01-24 19:20:43 +01:00
Teemu Ikonen
04acab82c9
Remove set_window_buttons() from MainWindow 2023-01-24 19:20:43 +01:00
Teemu Ikonen
ba9462503c
Use Adw.HeaderBar for CSD header bars
This allows showing the correct buttons with 'show-start-title-buttons'
and 'show-end-title-buttons' properties when folding.
2023-01-24 19:20:42 +01:00
Teemu Ikonen
2741bf21ae
Convert main window layout to 2 vertical boxes
Use Adw.Window as main window widget, add the now missing HeaderBars to
MainWindowPlaceholder and MainWindow in the NoCSD case.
2023-01-24 19:20:42 +01:00
Teemu Ikonen
1ef42b47d2
Use Adw.Application, make about dialog an Adw.AboutWindow 2023-01-24 19:20:42 +01:00
Teemu Ikonen
f6e73d85c0
Add libadwaita to build system 2023-01-24 19:20:42 +01:00
fiaxh
7e0d1db196 MAM: Fix latest range not being stored in db if it contained a duplicate 2023-01-17 19:56:43 +01:00
fiaxh
7da79864b3 Fix pin setting switch displaying 2023-01-16 18:37:11 +01:00
fiaxh
05289e0b4d Fix reply cancelling
fixes #1340
2023-01-16 17:39:22 +01:00
fiaxh
73c0263f35 Add debug outputs to summarize_whitespaces_to_space and don't assert_not_reached
related #1335
2023-01-13 11:44:28 +01:00
fiaxh
860c72bfc9 Fix crash when removing jid from roster
fixes #1332
2023-01-11 19:54:17 +01:00
fiaxh
75500dc767 Support pinning of conversations (locally)
fixes #290
fixes #1330
2023-01-11 19:54:02 +01:00
fiaxh
cb3b19b01d Support replies and reactions to files 2023-01-11 19:49:03 +01:00
fiaxh
0c4aea96ff Replies: Fix fallback bodies with multi-line quotes 2023-01-08 12:34:25 +01:00
fiaxh
424a429062 Reactions: Fix wrong time being stored for outgoing reactions (micro sec teated as milli sec)
fixes #1296
2023-01-07 23:44:43 +01:00
fiaxh
60371331e0 Replies: Fix quoted message not being reset after sending
fixes #1334
2023-01-07 23:35:39 +01:00
fiaxh
dc52e7595c Add support for XEP-0461 replies (with fallback) 2023-01-06 14:03:54 +01:00
fiaxh
4d7809bb12 Fix compiler warnings 2022-12-30 21:34:40 +01:00
fiaxh
799d09a4c9 MAM: Fix processing after range was fetched completely, fix merging of ranges 2022-12-20 19:51:38 +01:00
fiaxh
30f99d1347 Fix connecting to jingle file provider multiple times 2022-12-20 19:49:03 +01:00
fiaxh
4d50c51a75 Fix some MAM issues
- Messages from MUCs weren't added to their respective MUC MAM ranges, thus re-fetched on rejoin
- The earliest ('first') message of a mam page was used to update the to_id, but it should have been 'last'; also the other way around.
- Duplicates weren't detected properly
2022-11-20 22:18:22 +01:00
fiaxh
d1fb22ebed Reactions: Fix reactions being differentiated by resource on first displaying
fixes #1297
2022-11-20 22:18:22 +01:00
Marvin W
cdd22e404e
Fix build with Vala < 0.52
MenuButton.set_child was only added to VAPIs of 0.52 and later.
Even if GTK4 is new enough, they wouldn't be available.
2022-11-10 22:32:19 +01:00
Teemu Ikonen
a2e894dda1
Parse conference.password from XEP-0402 bookmarks (#1310) 2022-11-10 19:23:29 +01:00
fiaxh
7a19a25156 Clean up log outputs 2022-11-04 15:57:58 -06:00
Marvin W
e62955d3cf
HTTP: Make LimitInputStream pollable for better async compatibility
Fixes #1307
2022-11-04 15:45:48 -06:00
Marvin W
6e37f3fe3f Automatically select appropriate libsoup version 2022-11-03 14:10:57 -06:00
Marvin W
809c1579e4 Don't use splice when handling input streams from libsoup-3 2022-11-02 09:57:03 -06:00
Marvin W
a2f2224781
DTLS: Use ECDSA key 2022-10-28 18:36:46 +02:00
fiaxh
2ab7374aa5 Fix segfault if reaction message has type normal
fixes #1294
2022-10-20 19:27:01 +02:00
fiaxh
d4c674284e Reactions: Fix xml attribute name 2022-10-12 19:23:12 +02:00
fiaxh
a45280f8df Reactions: Improve style 2022-10-11 18:55:33 +02:00
fiaxh
09829b3382 Fix message server_id getting overwritten with null on reconnect 2022-10-11 18:55:33 +02:00
fiaxh
11b6e615b7 Don't require use of MenuButton.set_child introduced with GTK 4.6 2022-10-11 17:50:54 +02:00
fiaxh
80258a874d Add support for reactions 2022-10-11 13:37:48 +02:00
fiaxh
6c6e7e3aa7 Rewrite MAM logic and add MUC MAM 2022-10-10 21:55:15 +02:00
fiaxh
9c736af765 Fix regression with channel join button not getting sensitive
fixes #1284
2022-10-09 11:48:58 +02:00
fiaxh
7d8b08deca Small fixes 2022-10-09 11:48:58 +02:00
fiaxh
03878eee49 Add account dialog: Reenable Next-button activation on enter 2022-10-09 11:48:58 +02:00
fiaxh
85342ee2eb Fix drag and drop uploading 2022-10-09 11:48:58 +02:00
fiaxh
21ab48e09a Fix channel join button not getting sensitive 2022-10-03 19:14:45 +02:00
fiaxh
146af31524 Move icons out of scalable/ui/ since it's not allowed by icon naming spec 2022-09-18 20:30:24 +02:00
fiaxh
7ad52d9335 OMEMO QR code: Switch to paintable, fix css 2022-09-18 20:30:24 +02:00
rim
b8e84c8326
Fix crash when calling contact from Conversations (#1259)
fixes #1227
2022-08-22 20:39:34 +02:00
fiaxh
117f193812 Fix crashes and warning in Join Conference dialog
fixes #1262
2022-08-22 13:16:46 +02:00
fiaxh
14bc3d6717 Fix crash on call window closing 2022-08-22 13:16:46 +02:00
Marvin W
0aa73c4569
RTP: Use gstreamers new VideoFrame.map 2022-08-21 20:02:48 +02:00
Marvin W
e85477bb19
RTP: Use latest gstreamer vapi if vala version older than 0.56.1 2022-08-21 19:33:20 +02:00
fiaxh
054d3fec16 Fix encryption button update and reduce its required GTK version
MenuButton.activate only exists since 4.4
2022-08-21 14:16:31 +02:00
Marvin W
d6afa6e8ff
GTK4: Don't require use of MenuButton.set_child introduced with 4.6 2022-08-21 00:57:46 +02:00
Marvin W
7b252d040a
CMake: Fix version checks 2022-08-21 00:41:27 +02:00
Marvin W
5103a7fb7b
Move all icons in respective paths 2022-08-20 21:23:35 +02:00
fiaxh
6bfa70fc70 Disable tooltips for GTK 4.6.4 - 4.6.6
A bug in GTK caused the application to crash in some tooltip-related conditions
https://gitlab.gnome.org/GNOME/gtk/-/issues/4941
2022-08-20 21:09:42 +02:00
Marvin W
517363dfc9
GTK4: Fix theme blue highlight 2022-08-20 20:46:03 +02:00
fiaxh
0af92393f1 Switch CI to GTK4 dependencies 2022-07-27 20:56:24 +02:00
Marvin W
e51b55432f Gtk4 bug fixes and improvements 2022-07-27 20:55:54 +02:00
fiaxh
f44cbe02c1 Improve Gtk4 port 2022-07-27 20:34:20 +02:00
Marvin W
2b3ce5fc95 Video for GTK4 2022-07-27 20:34:20 +02:00
fiaxh
7e7dcedaf3 Port from GTK3 to GTK4 2022-07-27 20:34:20 +02:00
Matthew Egeler
f25bfb0096
Support devices with multiple framerate options in get_max_fps (#1224) 2022-05-17 14:12:32 +02:00
foucault
186361fd8a Fix calculation of best camera framerate
When the algorithm iterates over all the available v4l2 capabilities it tries to determine the best framerate for each one of YUYV (video/x-raw) modes presented by the hardware (best_fraction, line 357 in device.vala). Regardless of what's determined to be the "best" YUYV mode from within the conditional right after (line 385) the best fractional framerate will always point to the last iterated framerate, which might be an extremely low one, like 7 or 5 FPS. When the framerate is then stored in the Gst.Structure (line 394) the fractional framerate will always be that last value which might be different than the correct one as calculated by best_fps (line 386). This workaround solves this issue by only updating best_fraction when the conditional in line 385 is satisfied.

from issue #1195
2022-05-17 14:08:22 +02:00
Xavi92
99c076254a
Inhibit idle and suspend during calls (#1233) 2022-05-11 11:12:30 +02:00
Marvin W
baa4a6a1eb
Prepare http-files plugin for libsoup-3 support
Note: ice plugin still depends on libsoup-2.4 and one can't have both
in the same process, so this remains disabled by default
2022-04-09 00:28:42 +02:00
Marvin W
a0eac798cd
Fix build of tests 2022-04-08 22:03:31 +02:00
Marvin W
193bf38a79
Allow cancellation of file transfers 2022-03-30 10:36:52 -06:00
Marvin W
339d1d8f55
Fix Version handling, add function to retrieve short version without git commit 2022-03-30 10:36:07 -06:00
Nkwuda Sunday Cletus
c5ed719b66
set a generic MIME type for encrypted file (#1213) 2022-03-23 20:42:43 +01:00
Marvin W
f0ed11ec49
Fix build on Vala < 0.50 and pre-release Vala versions 2022-03-16 08:41:51 -06:00
Marvin W
ee4fbf160d
Add XEP implementation status to DOAP 2022-03-16 08:20:43 -06:00
fiaxh
5f04a6eb09 QR-code: Actually use Uri.join if glib version supports it 2022-03-09 07:56:14 -06:00
mjk
d1c8284964 OMEMO QR code: URI-escape the JID 2022-02-26 00:08:00 +00:00
mjk
98adfa332a OMEMO: Make QR code "scalable" in accordance with GDK_SCALE
The QR code is now generated at the required resolution instead of being
stretched for display with linear filtering.
2022-02-25 23:20:05 +00:00
mjk
d3ae541673 Qrencode: Break out upsampling into a separate function 2022-02-25 22:43:02 +00:00
mjk
855a98c045 OMEMO: Make QR code fixed-resolution and the quiet zone ISO-conformant 2022-02-25 22:26:43 +00:00
fiaxh
1309d7e2e4 Fix quote formating to not (partially) expect a space after > 2022-02-14 23:49:45 +01:00
Marvin W
7b9e62b8dd
Update DOAP file 2022-02-14 01:24:04 +01:00
mjk
3719fcbefb RTP: clarify codec support warning 2022-02-13 21:32:28 +01:00
618 changed files with 74417 additions and 46827 deletions

View File

@ -0,0 +1,17 @@
{
"problemMatcher": [
{
"owner": "gcc-problem-matcher",
"pattern": [
{
"regexp": "^(.*?):(\\d+):(\\d*):?\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$",
"file": 1,
"line": 2,
"column": 3,
"severity": 4,
"message": 5
}
]
}
]
}

View File

@ -0,0 +1,17 @@
{
"problemMatcher": [
{
"owner": "meson-problem-matcher",
"pattern": [
{
"regexp": "^(.*?)?:(\\d+)?:(\\d+)?: (WARNING|ERROR):\\s+(.*)$",
"file": 1,
"line": 2,
"column": 3,
"severity": 4,
"message": 5
}
]
}
]
}

View File

@ -0,0 +1,17 @@
{
"problemMatcher": [
{
"owner": "vala-problem-matcher",
"pattern": [
{
"regexp": "^(?:../)?(.*?):(\\d+).(\\d+)-\\d+.\\d+:?\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$",
"file": 1,
"line": 2,
"column": 3,
"severity": 4,
"message": 5
}
]
}
]
}

View File

@ -2,12 +2,42 @@ name: Build
on: [pull_request, push] on: [pull_request, push]
jobs: jobs:
build: build:
runs-on: ubuntu-20.04 name: "Build"
runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v2 - name: "Checkout sources"
- run: sudo apt-get update uses: actions/checkout@v4
- run: sudo apt-get install -y build-essential gettext cmake valac libgee-0.8-dev libsqlite3-dev libgtk-3-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 with:
- run: ./configure --with-tests --with-libsignal-in-tree fetch-depth: 0
- run: make - name: "Setup matchers"
- run: build/xmpp-vala-test run: |
- run: build/signal-protocol-vala-test echo '::add-matcher::${{ github.workspace }}/.github/matchers/gcc-problem-matcher.json'
echo '::add-matcher::${{ github.workspace }}/.github/matchers/vala-problem-matcher.json'
echo '::add-matcher::${{ github.workspace }}/.github/matchers/meson-problem-matcher.json'
- name: "Setup dependencies"
run: |
sudo apt-get update
sudo apt-get remove libunwind-14-dev
sudo apt-get install -y build-essential gettext libadwaita-1-dev libcanberra-dev libgcrypt20-dev libgee-0.8-dev libgpgme-dev libgstreamer-plugins-base1.0-dev libgstreamer1.0-dev libgtk-4-dev libnice-dev libnotify-dev libqrencode-dev libsignal-protocol-c-dev libsoup-3.0-dev libsqlite3-dev libsrtp2-dev libwebrtc-audio-processing-dev meson valac
- name: "Configure"
run: meson setup build
- name: "Build"
run: meson compile -C build
- name: "Test"
run: meson test -C build
build-flatpak:
name: "Build flatpak"
runs-on: ubuntu-24.04
container:
image: bilelmoussaoui/flatpak-github-actions:gnome-46
options: --privileged
steps:
- name: "Checkout sources"
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: "Build"
uses: flathub-infra/flatpak-github-actions/flatpak-builder@master
with:
manifest-path: im.dino.Dino.json
bundle: im.dino.Dino.flatpak

4
.gitmodules vendored
View File

@ -1,4 +0,0 @@
[submodule "libsignal-protocol-c"]
path = plugins/signal-protocol/libsignal-protocol-c
url = https://github.com/WhisperSystems/libsignal-protocol-c.git
branch = v2.3.3

View File

@ -1,212 +0,0 @@
cmake_minimum_required(VERSION 3.3)
list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake)
include(ComputeVersion)
if (NOT VERSION_FOUND)
project(Dino LANGUAGES C CXX)
elseif (VERSION_IS_RELEASE)
project(Dino VERSION ${VERSION_FULL} LANGUAGES C CXX)
else ()
project(Dino LANGUAGES C CXX)
set(PROJECT_VERSION ${VERSION_FULL})
endif ()
# Prepare Plugins
set(DEFAULT_PLUGINS omemo;openpgp;http-files;ice;rtp)
foreach (plugin ${DEFAULT_PLUGINS})
if ("$CACHE{DINO_PLUGIN_ENABLED_${plugin}}" STREQUAL "")
if (NOT DEFINED DINO_PLUGIN_ENABLED_${plugin}})
set(DINO_PLUGIN_ENABLED_${plugin} "yes" CACHE BOOL "Enable plugin ${plugin}")
else ()
set(DINO_PLUGIN_ENABLED_${plugin} "${DINO_PLUGIN_ENABLED_${plugin}}" CACHE BOOL "Enable plugin ${plugin}" FORCE)
endif ()
if (DINO_PLUGIN_ENABLED_${plugin})
message(STATUS "Enabled plugin: ${plugin}")
else ()
message(STATUS "Disabled plugin: ${plugin}")
endif ()
endif ()
endforeach (plugin)
if (DISABLED_PLUGINS)
foreach(plugin ${DISABLED_PLUGINS})
set(DINO_PLUGIN_ENABLED_${plugin} "no" CACHE BOOL "Enable plugin ${plugin}" FORCE)
message(STATUS "Disabled plugin: ${plugin}")
endforeach(plugin)
endif (DISABLED_PLUGINS)
if (ENABLED_PLUGINS)
foreach(plugin ${ENABLED_PLUGINS})
set(DINO_PLUGIN_ENABLED_${plugin} "yes" CACHE BOOL "Enable plugin ${plugin}" FORCE)
message(STATUS "Enabled plugin: ${plugin}")
endforeach(plugin)
endif (ENABLED_PLUGINS)
set(PLUGINS "")
get_cmake_property(all_variables VARIABLES)
foreach (variable_name ${all_variables})
if (variable_name MATCHES "^DINO_PLUGIN_ENABLED_(.+)$" AND ${variable_name})
list(APPEND PLUGINS ${CMAKE_MATCH_1})
endif()
endforeach ()
list(SORT PLUGINS)
string(REPLACE ";" ", " PLUGINS_TEXT "${PLUGINS}")
message(STATUS "Configuring Dino ${PROJECT_VERSION} with plugins: ${PLUGINS_TEXT}")
# Prepare instal paths
macro(set_path what val desc)
if (NOT ${what})
unset(${what} CACHE)
set(${what} ${val})
endif ()
if (NOT "${${what}}" STREQUAL "${_${what}_SET}")
message(STATUS "${desc}: ${${what}}")
set(_${what}_SET ${${what}} CACHE INTERNAL ${desc})
endif()
endmacro(set_path)
string(REGEX REPLACE "^liblib" "lib" LIBDIR_NAME "lib${LIB_SUFFIX}")
set_path(CMAKE_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}" "Installation directory for architecture-independent files")
set_path(EXEC_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}" "Installation directory for architecture-dependent files")
set_path(SHARE_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}/share" "Installation directory for read-only architecture-independent data")
set_path(BIN_INSTALL_DIR "${EXEC_INSTALL_PREFIX}/bin" "Installation directory for user executables")
set_path(DATA_INSTALL_DIR "${SHARE_INSTALL_PREFIX}/dino" "Installation directory for dino-specific data")
set_path(APPDATA_FILE_INSTALL_DIR "${SHARE_INSTALL_PREFIX}/metainfo" "Installation directory for .appdata.xml files")
set_path(DESKTOP_FILE_INSTALL_DIR "${SHARE_INSTALL_PREFIX}/applications" "Installation directory for .desktop files")
set_path(SERVICE_FILE_INSTALL_DIR "${SHARE_INSTALL_PREFIX}/dbus-1/services" "Installation directory for .service files")
set_path(ICON_INSTALL_DIR "${SHARE_INSTALL_PREFIX}/icons" "Installation directory for icons")
set_path(INCLUDE_INSTALL_DIR "${EXEC_INSTALL_PREFIX}/include" "Installation directory for C header files")
set_path(LIB_INSTALL_DIR "${EXEC_INSTALL_PREFIX}/${LIBDIR_NAME}" "Installation directory for object code libraries")
set_path(LOCALE_INSTALL_DIR "${SHARE_INSTALL_PREFIX}/locale" "Installation directory for locale files")
set_path(PLUGIN_INSTALL_DIR "${LIB_INSTALL_DIR}/dino/plugins" "Installation directory for dino plugin object code files")
set_path(VAPI_INSTALL_DIR "${SHARE_INSTALL_PREFIX}/vala/vapi" "Installation directory for Vala API files")
set(TARGET_INSTALL LIBRARY DESTINATION ${LIB_INSTALL_DIR} RUNTIME DESTINATION ${BIN_INSTALL_DIR} PUBLIC_HEADER DESTINATION ${INCLUDE_INSTALL_DIR} ARCHIVE DESTINATION ${LIB_INSTALL_DIR})
set(PLUGIN_INSTALL LIBRARY DESTINATION ${PLUGIN_INSTALL_DIR} RUNTIME DESTINATION ${PLUGIN_INSTALL_DIR})
include(CheckCCompilerFlag)
include(CheckCSourceCompiles)
macro(AddCFlagIfSupported list flag)
string(REGEX REPLACE "[^a-z^A-Z^_^0-9]+" "_" flag_name ${flag})
check_c_compiler_flag(${flag} COMPILER_SUPPORTS${flag_name})
if (${COMPILER_SUPPORTS${flag_name}})
set(${list} "${${list}} ${flag}")
endif ()
endmacro()
if ("Ninja" STREQUAL ${CMAKE_GENERATOR})
AddCFlagIfSupported(CMAKE_C_FLAGS -fdiagnostics-color)
endif ()
# Flags for all C files
AddCFlagIfSupported(CMAKE_C_FLAGS -Wall)
AddCFlagIfSupported(CMAKE_C_FLAGS -Wextra)
AddCFlagIfSupported(CMAKE_C_FLAGS -Werror=format-security)
AddCFlagIfSupported(CMAKE_C_FLAGS -Wno-duplicate-decl-specifier)
AddCFlagIfSupported(CMAKE_C_FLAGS -fno-omit-frame-pointer)
if (NOT VALA_WARN)
set(VALA_WARN "conversion")
endif ()
set(VALA_WARN "${VALA_WARN}" CACHE STRING "Which warnings to show when invoking C compiler on Vala compiler output")
set_property(CACHE VALA_WARN PROPERTY STRINGS "all;unused;qualifier;conversion;deprecated;format;none")
# Vala generates some unused stuff
if (NOT ("all" IN_LIST VALA_WARN OR "unused" IN_LIST VALA_WARN))
AddCFlagIfSupported(VALA_CFLAGS -Wno-unused-but-set-variable)
AddCFlagIfSupported(VALA_CFLAGS -Wno-unused-function)
AddCFlagIfSupported(VALA_CFLAGS -Wno-unused-label)
AddCFlagIfSupported(VALA_CFLAGS -Wno-unused-parameter)
AddCFlagIfSupported(VALA_CFLAGS -Wno-unused-value)
AddCFlagIfSupported(VALA_CFLAGS -Wno-unused-variable)
endif ()
if (NOT ("all" IN_LIST VALA_WARN OR "qualifier" IN_LIST VALA_WARN))
AddCFlagIfSupported(VALA_CFLAGS -Wno-discarded-qualifiers)
AddCFlagIfSupported(VALA_CFLAGS -Wno-discarded-array-qualifiers)
AddCFlagIfSupported(VALA_CFLAGS -Wno-incompatible-pointer-types-discards-qualifiers)
endif ()
if (NOT ("all" IN_LIST VALA_WARN OR "deprecated" IN_LIST VALA_WARN))
AddCFlagIfSupported(VALA_CFLAGS -Wno-deprecated-declarations)
endif ()
if (NOT ("all" IN_LIST VALA_WARN OR "format" IN_LIST VALA_WARN))
AddCFlagIfSupported(VALA_CFLAGS -Wno-missing-braces)
endif ()
if (NOT ("all" IN_LIST VALA_WARN OR "conversion" IN_LIST VALA_WARN))
AddCFlagIfSupported(VALA_CFLAGS -Wno-int-conversion)
AddCFlagIfSupported(VALA_CFLAGS -Wno-pointer-sign)
AddCFlagIfSupported(VALA_CFLAGS -Wno-incompatible-pointer-types)
endif ()
try_compile(__WITHOUT_FILE_OFFSET_BITS_64 ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_SOURCE_DIR}/cmake/LargeFileOffsets.c COMPILE_DEFINITIONS ${CMAKE_REQUIRED_DEFINITIONS})
if (NOT __WITHOUT_FILE_OFFSET_BITS_64)
try_compile(__WITH_FILE_OFFSET_BITS_64 ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_SOURCE_DIR}/cmake/LargeFileOffsets.c COMPILE_DEFINITIONS ${CMAKE_REQUIRED_DEFINITIONS} -D_FILE_OFFSET_BITS=64)
if (__WITH_FILE_OFFSET_BITS_64)
AddCFlagIfSupported(CMAKE_C_FLAGS -D_FILE_OFFSET_BITS=64)
message(STATUS "Enabled large file support using _FILE_OFFSET_BITS=64")
else (__WITH_FILE_OFFSET_BITS_64)
message(STATUS "Large file support not available")
endif (__WITH_FILE_OFFSET_BITS_64)
unset(__WITH_FILE_OFFSET_BITS_64)
endif (NOT __WITHOUT_FILE_OFFSET_BITS_64)
unset(__WITHOUT_FILE_OFFSET_BITS_64)
if ($ENV{USE_CCACHE})
# Configure CCache if available
find_program(CCACHE_BIN ccache)
mark_as_advanced(CCACHE_BIN)
if (CCACHE_BIN)
message(STATUS "Using ccache")
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ${CCACHE_BIN})
set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK ${CCACHE_BIN})
else (CCACHE_BIN)
message(STATUS "USE_CCACHE was set but ccache was not found")
endif (CCACHE_BIN)
endif ($ENV{USE_CCACHE})
if (NOT NO_DEBUG)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -g")
set(CMAKE_VALA_FLAGS "${CMAKE_VALA_FLAGS} -g")
endif (NOT NO_DEBUG)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
set(GTK3_GLOBAL_VERSION 3.22)
set(GLib_GLOBAL_VERSION 2.38)
set(ICU_GLOBAL_VERSION 57)
if (NOT VALA_EXECUTABLE)
unset(VALA_EXECUTABLE CACHE)
endif ()
find_package(Vala 0.34 REQUIRED)
if (VALA_VERSION VERSION_GREATER "0.34.90" AND VALA_VERSION VERSION_LESS "0.36.1" OR # Due to a bug on 0.36.0 (and pre-releases), we need to disable FAST_VAPI
VALA_VERSION VERSION_EQUAL "0.44.10" OR VALA_VERSION VERSION_EQUAL "0.46.4" OR VALA_VERSION VERSION_EQUAL "0.47.1" OR # See Dino issue #646
VALA_VERSION VERSION_EQUAL "0.40.21" OR VALA_VERSION VERSION_EQUAL "0.46.8" OR VALA_VERSION VERSION_EQUAL "0.48.4") # See Dino issue #816
set(DISABLE_FAST_VAPI yes)
endif ()
include(${VALA_USE_FILE})
include(MultiFind)
include(GlibCompileResourcesSupport)
set(CMAKE_VALA_FLAGS "${CMAKE_VALA_FLAGS} --target-glib=${GLib_GLOBAL_VERSION}")
add_subdirectory(qlite)
add_subdirectory(xmpp-vala)
add_subdirectory(libdino)
add_subdirectory(main)
add_subdirectory(crypto-vala)
add_subdirectory(plugins)
# uninstall target
configure_file("${CMAKE_SOURCE_DIR}/cmake/cmake_uninstall.cmake.in" "${CMAKE_BINARY_DIR}/cmake_uninstall.cmake" IMMEDIATE @ONLY)
add_custom_target(uninstall COMMAND ${CMAKE_COMMAND} -P ${CMAKE_BINARY_DIR}/cmake_uninstall.cmake COMMENT "Uninstall the project...")

View File

@ -1,7 +1,11 @@
![Dino](https://dino.im/img/readme_header.svg) <img src="https://dino.im/img/logo.svg" width="80">
=======
![screenshots](https://dino.im/img/screenshot-main.png) # Dino
Dino is an XMPP messaging app for Linux using GTK and Vala.
It supports calls, encryption, file transfers, group chats and more.
![screenshot](https://dino.im/img/appdata/screenshot-dino-0.4-main-2244x1644@2.png)
Installation Installation
------------ ------------
@ -11,9 +15,9 @@ Build
----- -----
Make sure to install all [dependencies](https://github.com/dino/dino/wiki/Build#dependencies). Make sure to install all [dependencies](https://github.com/dino/dino/wiki/Build#dependencies).
./configure meson setup build
make meson compile -C build
build/dino build/main/dino
Resources Resources
--------- ---------
@ -30,8 +34,8 @@ Contribute
License License
------- -------
Dino - Modern Jabber/XMPP Client using GTK+/Vala Dino - XMPP messaging app using GTK/Vala
Copyright (C) 2016-2022 Dino contributors Copyright (C) 2016-2025 Dino contributors
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by

View File

@ -1,57 +0,0 @@
# This file is used to be invoked at build time. It generates the needed
# resource XML file.
# Input variables that need to provided when invoking this script:
# GXML_OUTPUT The output file path where to save the XML file.
# GXML_COMPRESS_ALL Sets all COMPRESS flags in all resources in resource
# list.
# GXML_NO_COMPRESS_ALL Removes all COMPRESS flags in all resources in
# resource list.
# GXML_STRIPBLANKS_ALL Sets all STRIPBLANKS flags in all resources in
# resource list.
# GXML_NO_STRIPBLANKS_ALL Removes all STRIPBLANKS flags in all resources in
# resource list.
# GXML_TOPIXDATA_ALL Sets all TOPIXDATA flags i nall resources in resource
# list.
# GXML_NO_TOPIXDATA_ALL Removes all TOPIXDATA flags in all resources in
# resource list.
# GXML_PREFIX Overrides the resource prefix that is prepended to
# each relative name in registered resources.
# GXML_RESOURCES The list of resource files. Whether absolute or
# relative path is equal.
# Include the GENERATE_GXML() function.
include(${CMAKE_CURRENT_LIST_DIR}/GenerateGXML.cmake)
# Set flags to actual invocation flags.
if(GXML_COMPRESS_ALL)
set(GXML_COMPRESS_ALL COMPRESS_ALL)
endif()
if(GXML_NO_COMPRESS_ALL)
set(GXML_NO_COMPRESS_ALL NO_COMPRESS_ALL)
endif()
if(GXML_STRIPBLANKS_ALL)
set(GXML_STRIPBLANKS_ALL STRIPBLANKS_ALL)
endif()
if(GXML_NO_STRIPBLANKS_ALL)
set(GXML_NO_STRIPBLANKS_ALL NO_STRIPBLANKS_ALL)
endif()
if(GXML_TOPIXDATA_ALL)
set(GXML_TOPIXDATA_ALL TOPIXDATA_ALL)
endif()
if(GXML_NO_TOPIXDATA_ALL)
set(GXML_NO_TOPIXDATA_ALL NO_TOPIXDATA_ALL)
endif()
# Replace " " with ";" to import the list over the command line. Otherwise
# CMake would interprete the passed resources as a whole string.
string(REPLACE " " ";" GXML_RESOURCES ${GXML_RESOURCES})
# Invoke the gresource XML generation function.
generate_gxml(${GXML_OUTPUT}
${GXML_COMPRESS_ALL} ${GXML_NO_COMPRESS_ALL}
${GXML_STRIPBLANKS_ALL} ${GXML_NO_STRIPBLANKS_ALL}
${GXML_TOPIXDATA_ALL} ${GXML_NO_TOPIXDATA_ALL}
PREFIX ${GXML_PREFIX}
RESOURCES ${GXML_RESOURCES})

View File

@ -1,221 +0,0 @@
include(CMakeParseArguments)
# Path to this file.
set(GCR_CMAKE_MACRO_DIR ${CMAKE_CURRENT_LIST_DIR})
# Compiles a gresource resource file from given resource files. Automatically
# creates the XML controlling file.
# The type of resource to generate (header, c-file or bundle) is automatically
# determined from TARGET file ending, if no TYPE is explicitly specified.
# The output file is stored in the provided variable "output".
# "xml_out" contains the variable where to output the XML path. Can be used to
# create custom targets or doing postprocessing.
# If you want to use preprocessing, you need to manually check the existence
# of the tools you use. This function doesn't check this for you, it just
# generates the XML file. glib-compile-resources will then throw a
# warning/error.
function(COMPILE_GRESOURCES output xml_out)
# Available options:
# COMPRESS_ALL, NO_COMPRESS_ALL Overrides the COMPRESS flag in all
# registered resources.
# STRIPBLANKS_ALL, NO_STRIPBLANKS_ALL Overrides the STRIPBLANKS flag in all
# registered resources.
# TOPIXDATA_ALL, NO_TOPIXDATA_ALL Overrides the TOPIXDATA flag in all
# registered resources.
set(CG_OPTIONS COMPRESS_ALL NO_COMPRESS_ALL
STRIPBLANKS_ALL NO_STRIPBLANKS_ALL
TOPIXDATA_ALL NO_TOPIXDATA_ALL)
# Available one value options:
# TYPE Type of resource to create. Valid options are:
# EMBED_C: A C-file that can be compiled with your project.
# EMBED_H: A header that can be included into your project.
# BUNDLE: Generates a resource bundle file that can be loaded
# at runtime.
# AUTO: Determine from target file ending. Need to specify
# target argument.
# PREFIX Overrides the resource prefix that is prepended to each
# relative file name in registered resources.
# SOURCE_DIR Overrides the resources base directory to search for resources.
# Normally this is set to the source directory with that CMake
# was invoked (CMAKE_SOURCE_DIR).
# TARGET Overrides the name of the output file/-s. Normally the output
# names from glib-compile-resources tool is taken.
set(CG_ONEVALUEARGS TYPE PREFIX SOURCE_DIR TARGET)
# Available multi-value options:
# RESOURCES The list of resource files. Whether absolute or relative path is
# equal, absolute paths are stripped down to relative ones. If the
# absolute path is not inside the given base directory SOURCE_DIR
# or CMAKE_SOURCE_DIR (if SOURCE_DIR is not overriden), this
# function aborts.
# OPTIONS Extra command line options passed to glib-compile-resources.
set(CG_MULTIVALUEARGS RESOURCES OPTIONS)
# Parse the arguments.
cmake_parse_arguments(CG_ARG
"${CG_OPTIONS}"
"${CG_ONEVALUEARGS}"
"${CG_MULTIVALUEARGS}"
"${ARGN}")
# Variable to store the double-quote (") string. Since escaping
# double-quotes in strings is not possible we need a helper variable that
# does this job for us.
set(Q \")
# Check invocation validity with the <prefix>_UNPARSED_ARGUMENTS variable.
# If other not recognized parameters were passed, throw error.
if (CG_ARG_UNPARSED_ARGUMENTS)
set(CG_WARNMSG "Invocation of COMPILE_GRESOURCES with unrecognized")
set(CG_WARNMSG "${CG_WARNMSG} parameters. Parameters are:")
set(CG_WARNMSG "${CG_WARNMSG} ${CG_ARG_UNPARSED_ARGUMENTS}.")
message(WARNING ${CG_WARNMSG})
endif()
# Check invocation validity depending on generation mode (EMBED_C, EMBED_H
# or BUNDLE).
if ("${CG_ARG_TYPE}" STREQUAL "EMBED_C")
# EMBED_C mode, output compilable C-file.
set(CG_GENERATE_COMMAND_LINE "--generate-source")
set(CG_TARGET_FILE_ENDING "c")
elseif ("${CG_ARG_TYPE}" STREQUAL "EMBED_H")
# EMBED_H mode, output includable header file.
set(CG_GENERATE_COMMAND_LINE "--generate-header")
set(CG_TARGET_FILE_ENDING "h")
elseif ("${CG_ARG_TYPE}" STREQUAL "BUNDLE")
# BUNDLE mode, output resource bundle. Don't do anything since
# glib-compile-resources outputs a bundle when not specifying
# something else.
set(CG_TARGET_FILE_ENDING "gresource")
else()
# Everything else is AUTO mode, determine from target file ending.
if (CG_ARG_TARGET)
set(CG_GENERATE_COMMAND_LINE "--generate")
else()
set(CG_ERRMSG "AUTO mode given, but no target specified. Can't")
set(CG_ERRMSG "${CG_ERRMSG} determine output type. In function")
set(CG_ERRMSG "${CG_ERRMSG} COMPILE_GRESOURCES.")
message(FATAL_ERROR ${CG_ERRMSG})
endif()
endif()
# Check flag validity.
if (CG_ARG_COMPRESS_ALL AND CG_ARG_NO_COMPRESS_ALL)
set(CG_ERRMSG "COMPRESS_ALL and NO_COMPRESS_ALL simultaneously set. In")
set(CG_ERRMSG "${CG_ERRMSG} function COMPILE_GRESOURCES.")
message(FATAL_ERROR ${CG_ERRMSG})
endif()
if (CG_ARG_STRIPBLANKS_ALL AND CG_ARG_NO_STRIPBLANKS_ALL)
set(CG_ERRMSG "STRIPBLANKS_ALL and NO_STRIPBLANKS_ALL simultaneously")
set(CG_ERRMSG "${CG_ERRMSG} set. In function COMPILE_GRESOURCES.")
message(FATAL_ERROR ${CG_ERRMSG})
endif()
if (CG_ARG_TOPIXDATA_ALL AND CG_ARG_NO_TOPIXDATA_ALL)
set(CG_ERRMSG "TOPIXDATA_ALL and NO_TOPIXDATA_ALL simultaneously set.")
set(CG_ERRMSG "${CG_ERRMSG} In function COMPILE_GRESOURCES.")
message(FATAL_ERROR ${CG_ERRMSG})
endif()
# Check if there are any resources.
if (NOT CG_ARG_RESOURCES)
set(CG_ERRMSG "No resource files to process. In function")
set(CG_ERRMSG "${CG_ERRMSG} COMPILE_GRESOURCES.")
message(FATAL_ERROR ${CG_ERRMSG})
endif()
# Extract all dependencies for targets from resource list.
foreach(res ${CG_ARG_RESOURCES})
if (NOT(("${res}" STREQUAL "COMPRESS") OR
("${res}" STREQUAL "STRIPBLANKS") OR
("${res}" STREQUAL "TOPIXDATA")))
add_custom_command(
OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/resources/${res}"
COMMAND ${CMAKE_COMMAND} -E copy "${CG_ARG_SOURCE_DIR}/${res}" "${CMAKE_CURRENT_BINARY_DIR}/resources/${res}"
MAIN_DEPENDENCY "${CG_ARG_SOURCE_DIR}/${res}")
list(APPEND CG_RESOURCES_DEPENDENCIES "${CMAKE_CURRENT_BINARY_DIR}/resources/${res}")
endif()
endforeach()
# Construct .gresource.xml path.
set(CG_XML_FILE_PATH "${CMAKE_CURRENT_BINARY_DIR}/resources/.gresource.xml")
# Generate gresources XML target.
list(APPEND CG_CMAKE_SCRIPT_ARGS "-D")
list(APPEND CG_CMAKE_SCRIPT_ARGS "GXML_OUTPUT=${Q}${CG_XML_FILE_PATH}${Q}")
if(CG_ARG_COMPRESS_ALL)
list(APPEND CG_CMAKE_SCRIPT_ARGS "-D")
list(APPEND CG_CMAKE_SCRIPT_ARGS "GXML_COMPRESS_ALL")
endif()
if(CG_ARG_NO_COMPRESS_ALL)
list(APPEND CG_CMAKE_SCRIPT_ARGS "-D")
list(APPEND CG_CMAKE_SCRIPT_ARGS "GXML_NO_COMPRESS_ALL")
endif()
if(CG_ARG_STRPIBLANKS_ALL)
list(APPEND CG_CMAKE_SCRIPT_ARGS "-D")
list(APPEND CG_CMAKE_SCRIPT_ARGS "GXML_STRIPBLANKS_ALL")
endif()
if(CG_ARG_NO_STRIPBLANKS_ALL)
list(APPEND CG_CMAKE_SCRIPT_ARGS "-D")
list(APPEND CG_CMAKE_SCRIPT_ARGS "GXML_NO_STRIPBLANKS_ALL")
endif()
if(CG_ARG_TOPIXDATA_ALL)
list(APPEND CG_CMAKE_SCRIPT_ARGS "-D")
list(APPEND CG_CMAKE_SCRIPT_ARGS "GXML_TOPIXDATA_ALL")
endif()
if(CG_ARG_NO_TOPIXDATA_ALL)
list(APPEND CG_CMAKE_SCRIPT_ARGS "-D")
list(APPEND CG_CMAKE_SCRIPT_ARGS "GXML_NO_TOPIXDATA_ALL")
endif()
list(APPEND CG_CMAKE_SCRIPT_ARGS "-D")
list(APPEND CG_CMAKE_SCRIPT_ARGS "GXML_PREFIX=${Q}${CG_ARG_PREFIX}${Q}")
list(APPEND CG_CMAKE_SCRIPT_ARGS "-D")
list(APPEND CG_CMAKE_SCRIPT_ARGS
"GXML_RESOURCES=${Q}${CG_ARG_RESOURCES}${Q}")
list(APPEND CG_CMAKE_SCRIPT_ARGS "-P")
list(APPEND CG_CMAKE_SCRIPT_ARGS
"${Q}${GCR_CMAKE_MACRO_DIR}/BuildTargetScript.cmake${Q}")
get_filename_component(CG_XML_FILE_PATH_ONLY_NAME
"${CG_XML_FILE_PATH}" NAME)
set(CG_XML_CUSTOM_COMMAND_COMMENT
"Creating gresources XML file (${CG_XML_FILE_PATH_ONLY_NAME})")
add_custom_command(OUTPUT ${CG_XML_FILE_PATH}
COMMAND ${CMAKE_COMMAND}
ARGS ${CG_CMAKE_SCRIPT_ARGS}
DEPENDS ${CG_RESOURCES_DEPENDENCIES}
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMENT ${CG_XML_CUSTOM_COMMAND_COMMENT})
# Create target manually if not set (to make sure glib-compile-resources
# doesn't change behaviour with it's naming standards).
if (NOT CG_ARG_TARGET)
set(CG_ARG_TARGET "${CMAKE_CURRENT_BINARY_DIR}/resources")
set(CG_ARG_TARGET "${CG_ARG_TARGET}.${CG_TARGET_FILE_ENDING}")
endif()
# Create source directory automatically if not set.
if (NOT CG_ARG_SOURCE_DIR)
set(CG_ARG_SOURCE_DIR "${CMAKE_SOURCE_DIR}")
endif()
# Add compilation target for resources.
add_custom_command(OUTPUT ${CG_ARG_TARGET}
COMMAND ${GLIB_COMPILE_RESOURCES_EXECUTABLE}
ARGS
${OPTIONS}
"--target=${Q}${CG_ARG_TARGET}${Q}"
"--sourcedir=${Q}${CG_ARG_SOURCE_DIR}${Q}"
${CG_GENERATE_COMMAND_LINE}
${CG_XML_FILE_PATH}
MAIN_DEPENDENCY ${CG_XML_FILE_PATH}
DEPENDS ${CG_RESOURCES_DEPENDENCIES}
WORKING_DIRECTORY ${CMAKE_BUILD_DIR})
# Set output and XML_OUT to parent scope.
set(${xml_out} ${CG_XML_FILE_PATH} PARENT_SCOPE)
set(${output} ${CG_ARG_TARGET} PARENT_SCOPE)
endfunction()

View File

@ -1,105 +0,0 @@
include(CMakeParseArguments)
function(_compute_version_from_file)
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${CMAKE_SOURCE_DIR}/VERSION)
if (NOT EXISTS ${CMAKE_SOURCE_DIR}/VERSION)
set(VERSION_FOUND 0 PARENT_SCOPE)
return()
endif ()
file(STRINGS ${CMAKE_SOURCE_DIR}/VERSION VERSION_FILE)
string(REPLACE " " ";" VERSION_FILE "${VERSION_FILE}")
cmake_parse_arguments(VERSION_FILE "" "RELEASE;PRERELEASE" "" ${VERSION_FILE})
if (DEFINED VERSION_FILE_RELEASE)
string(STRIP "${VERSION_FILE_RELEASE}" VERSION_FILE_RELEASE)
set(VERSION_IS_RELEASE 1 PARENT_SCOPE)
set(VERSION_FULL "${VERSION_FILE_RELEASE}" PARENT_SCOPE)
set(VERSION_FOUND 1 PARENT_SCOPE)
elseif (DEFINED VERSION_FILE_PRERELEASE)
string(STRIP "${VERSION_FILE_PRERELEASE}" VERSION_FILE_PRERELEASE)
set(VERSION_IS_RELEASE 0 PARENT_SCOPE)
set(VERSION_FULL "${VERSION_FILE_PRERELEASE}" PARENT_SCOPE)
set(VERSION_FOUND 1 PARENT_SCOPE)
else ()
set(VERSION_FOUND 0 PARENT_SCOPE)
endif ()
endfunction(_compute_version_from_file)
function(_compute_version_from_git)
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${CMAKE_SOURCE_DIR}/.git)
if (NOT GIT_EXECUTABLE)
find_package(Git QUIET)
if (NOT GIT_FOUND)
return()
endif ()
endif (NOT GIT_EXECUTABLE)
# Git tag
execute_process(
COMMAND "${GIT_EXECUTABLE}" describe --tags --abbrev=0
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
RESULT_VARIABLE git_result
OUTPUT_VARIABLE git_tag
ERROR_VARIABLE git_error
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_STRIP_TRAILING_WHITESPACE
)
if (NOT git_result EQUAL 0)
return()
endif (NOT git_result EQUAL 0)
if (git_tag MATCHES "^v?([0-9]+[.]?[0-9]*[.]?[0-9]*)(-[.0-9A-Za-z-]+)?([+][.0-9A-Za-z-]+)?$")
set(VERSION_LAST_RELEASE "${CMAKE_MATCH_1}")
else ()
return()
endif ()
# Git describe
execute_process(
COMMAND "${GIT_EXECUTABLE}" describe --tags
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
RESULT_VARIABLE git_result
OUTPUT_VARIABLE git_describe
ERROR_VARIABLE git_error
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_STRIP_TRAILING_WHITESPACE
)
if (NOT git_result EQUAL 0)
return()
endif (NOT git_result EQUAL 0)
if ("${git_tag}" STREQUAL "${git_describe}")
set(VERSION_IS_RELEASE 1)
else ()
set(VERSION_IS_RELEASE 0)
if (git_describe MATCHES "-([0-9]+)-g([0-9a-f]+)$")
set(VERSION_TAG_OFFSET "${CMAKE_MATCH_1}")
set(VERSION_COMMIT_HASH "${CMAKE_MATCH_2}")
endif ()
execute_process(
COMMAND "${GIT_EXECUTABLE}" show --format=%cd --date=format:%Y%m%d -s
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
RESULT_VARIABLE git_result
OUTPUT_VARIABLE git_time
ERROR_VARIABLE git_error
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_STRIP_TRAILING_WHITESPACE
)
if (NOT git_result EQUAL 0)
return()
endif (NOT git_result EQUAL 0)
set(VERSION_COMMIT_DATE "${git_time}")
endif ()
if (NOT VERSION_IS_RELEASE)
set(VERSION_SUFFIX "~git${VERSION_TAG_OFFSET}.${VERSION_COMMIT_DATE}.${VERSION_COMMIT_HASH}")
else (NOT VERSION_IS_RELEASE)
set(VERSION_SUFFIX "")
endif (NOT VERSION_IS_RELEASE)
set(VERSION_IS_RELEASE ${VERSION_IS_RELEASE} PARENT_SCOPE)
set(VERSION_FULL "${VERSION_LAST_RELEASE}${VERSION_SUFFIX}" PARENT_SCOPE)
set(VERSION_FOUND 1 PARENT_SCOPE)
endfunction(_compute_version_from_git)
_compute_version_from_file()
if (NOT VERSION_FOUND)
_compute_version_from_git()
endif (NOT VERSION_FOUND)

View File

@ -1,31 +0,0 @@
include(PkgConfigWithFallback)
find_pkg_config_with_fallback(ATK
PKG_CONFIG_NAME atk
LIB_NAMES atk-1.0
INCLUDE_NAMES atk/atk.h
INCLUDE_DIR_SUFFIXES atk-1.0 atk-1.0/include
DEPENDS GObject
)
if(ATK_FOUND AND NOT ATK_VERSION)
find_file(ATK_VERSION_HEADER "atk/atkversion.h" HINTS ${ATK_INCLUDE_DIRS})
mark_as_advanced(ATK_VERSION_HEADER)
if(ATK_VERSION_HEADER)
file(STRINGS "${ATK_VERSION_HEADER}" ATK_MAJOR_VERSION REGEX "^#define ATK_MAJOR_VERSION +\\(?([0-9]+)\\)?$")
string(REGEX REPLACE "^#define ATK_MAJOR_VERSION \\(?([0-9]+)\\)?$" "\\1" ATK_MAJOR_VERSION "${ATK_MAJOR_VERSION}")
file(STRINGS "${ATK_VERSION_HEADER}" ATK_MINOR_VERSION REGEX "^#define ATK_MINOR_VERSION +\\(?([0-9]+)\\)?$")
string(REGEX REPLACE "^#define ATK_MINOR_VERSION \\(?([0-9]+)\\)?$" "\\1" ATK_MINOR_VERSION "${ATK_MINOR_VERSION}")
file(STRINGS "${ATK_VERSION_HEADER}" ATK_MICRO_VERSION REGEX "^#define ATK_MICRO_VERSION +\\(?([0-9]+)\\)?$")
string(REGEX REPLACE "^#define ATK_MICRO_VERSION \\(?([0-9]+)\\)?$" "\\1" ATK_MICRO_VERSION "${ATK_MICRO_VERSION}")
set(ATK_VERSION "${ATK_MAJOR_VERSION}.${ATK_MINOR_VERSION}.${ATK_MICRO_VERSION}")
unset(ATK_MAJOR_VERSION)
unset(ATK_MINOR_VERSION)
unset(ATK_MICRO_VERSION)
endif()
endif()
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(ATK
REQUIRED_VARS ATK_LIBRARY
VERSION_VAR ATK_VERSION)

View File

@ -1,30 +0,0 @@
include(PkgConfigWithFallback)
find_pkg_config_with_fallback(Cairo
PKG_CONFIG_NAME cairo
LIB_NAMES cairo
INCLUDE_NAMES cairo.h
INCLUDE_DIR_SUFFIXES cairo cairo/include
)
if(Cairo_FOUND AND NOT Cairo_VERSION)
find_file(Cairo_VERSION_HEADER "cairo-version.h" HINTS ${Cairo_INCLUDE_DIRS})
mark_as_advanced(Cairo_VERSION_HEADER)
if(Cairo_VERSION_HEADER)
file(STRINGS "${Cairo_VERSION_HEADER}" Cairo_MAJOR_VERSION REGEX "^#define CAIRO_VERSION_MAJOR +\\(?([0-9]+)\\)?$")
string(REGEX REPLACE "^#define CAIRO_VERSION_MAJOR \\(?([0-9]+)\\)?$" "\\1" Cairo_MAJOR_VERSION "${Cairo_MAJOR_VERSION}")
file(STRINGS "${Cairo_VERSION_HEADER}" Cairo_MINOR_VERSION REGEX "^#define CAIRO_VERSION_MINOR +\\(?([0-9]+)\\)?$")
string(REGEX REPLACE "^#define CAIRO_VERSION_MINOR \\(?([0-9]+)\\)?$" "\\1" Cairo_MINOR_VERSION "${Cairo_MINOR_VERSION}")
file(STRINGS "${Cairo_VERSION_HEADER}" Cairo_MICRO_VERSION REGEX "^#define CAIRO_VERSION_MICRO +\\(?([0-9]+)\\)?$")
string(REGEX REPLACE "^#define CAIRO_VERSION_MICRO \\(?([0-9]+)\\)?$" "\\1" Cairo_MICRO_VERSION "${Cairo_MICRO_VERSION}")
set(Cairo_VERSION "${Cairo_MAJOR_VERSION}.${Cairo_MINOR_VERSION}.${Cairo_MICRO_VERSION}")
unset(Cairo_MAJOR_VERSION)
unset(Cairo_MINOR_VERSION)
unset(Cairo_MICRO_VERSION)
endif()
endif()
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(Cairo
REQUIRED_VARS Cairo_LIBRARY
VERSION_VAR Cairo_VERSION)

View File

@ -1,10 +0,0 @@
include(PkgConfigWithFallback)
find_pkg_config_with_fallback(Canberra
PKG_CONFIG_NAME libcanberra
LIB_NAMES canberra
INCLUDE_NAMES canberra.h
)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(Canberra
REQUIRED_VARS Canberra_LIBRARY)

View File

@ -1,10 +0,0 @@
include(PkgConfigWithFallbackOnConfigScript)
find_pkg_config_with_fallback_on_config_script(GCrypt
PKG_CONFIG_NAME libgcrypt
CONFIG_SCRIPT_NAME libgcrypt
)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(GCrypt
REQUIRED_VARS GCrypt_LIBRARY
VERSION_VAR GCrypt_VERSION)

View File

@ -1,38 +0,0 @@
include(PkgConfigWithFallback)
find_pkg_config_with_fallback(GDK3
PKG_CONFIG_NAME gdk-3.0
LIB_NAMES gdk-3
INCLUDE_NAMES gdk/gdk.h
INCLUDE_DIR_SUFFIXES gtk-3.0 gtk-3.0/include gtk+-3.0 gtk+-3.0/include
DEPENDS Pango Cairo GDKPixbuf2
)
if(GDK3_FOUND AND NOT GDK3_VERSION)
find_file(GDK3_VERSION_HEADER "gdk/gdkversionmacros.h" HINTS ${GDK3_INCLUDE_DIRS})
mark_as_advanced(GDK3_VERSION_HEADER)
if(GDK3_VERSION_HEADER)
file(STRINGS "${GDK3_VERSION_HEADER}" GDK3_MAJOR_VERSION REGEX "^#define GDK_MAJOR_VERSION +\\(?([0-9]+)\\)?$")
string(REGEX REPLACE "^#define GDK_MAJOR_VERSION \\(?([0-9]+)\\)?$" "\\1" GDK3_MAJOR_VERSION "${GDK3_MAJOR_VERSION}")
file(STRINGS "${GDK3_VERSION_HEADER}" GDK3_MINOR_VERSION REGEX "^#define GDK_MINOR_VERSION +\\(?([0-9]+)\\)?$")
string(REGEX REPLACE "^#define GDK_MINOR_VERSION \\(?([0-9]+)\\)?$" "\\1" GDK3_MINOR_VERSION "${GDK3_MINOR_VERSION}")
file(STRINGS "${GDK3_VERSION_HEADER}" GDK3_MICRO_VERSION REGEX "^#define GDK_MICRO_VERSION +\\(?([0-9]+)\\)?$")
string(REGEX REPLACE "^#define GDK_MICRO_VERSION \\(?([0-9]+)\\)?$" "\\1" GDK3_MICRO_VERSION "${GDK3_MICRO_VERSION}")
set(GDK3_VERSION "${GDK3_MAJOR_VERSION}.${GDK3_MINOR_VERSION}.${GDK3_MICRO_VERSION}")
unset(GDK3_MAJOR_VERSION)
unset(GDK3_MINOR_VERSION)
unset(GDK3_MICRO_VERSION)
endif()
endif()
if (GDK3_FOUND)
find_file(GDK3_WITH_X11 "gdk/gdkx.h" HINTS ${GDK3_INCLUDE_DIRS})
if (GDK3_WITH_X11)
set(GDK3_WITH_X11 yes CACHE INTERNAL "Does GDK3 support X11")
endif (GDK3_WITH_X11)
endif ()
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(GDK3
REQUIRED_VARS GDK3_LIBRARY
VERSION_VAR GDK3_VERSION)

View File

@ -1,23 +0,0 @@
include(PkgConfigWithFallback)
find_pkg_config_with_fallback(GDKPixbuf2
PKG_CONFIG_NAME gdk-pixbuf-2.0
LIB_NAMES gdk_pixbuf-2.0
INCLUDE_NAMES gdk-pixbuf/gdk-pixbuf.h
INCLUDE_DIR_SUFFIXES gdk-pixbuf-2.0 gdk-pixbuf-2.0/include
DEPENDS GLib
)
if(GDKPixbuf2_FOUND AND NOT GDKPixbuf2_VERSION)
find_file(GDKPixbuf2_FEATURES_HEADER "gdk-pixbuf/gdk-pixbuf-features.h" HINTS ${GDKPixbuf2_INCLUDE_DIRS})
mark_as_advanced(GDKPixbuf2_FEATURES_HEADER)
if(GDKPixbuf2_FEATURES_HEADER)
file(STRINGS "${GDKPixbuf2_FEATURES_HEADER}" GDKPixbuf2_VERSION REGEX "^#define GDK_PIXBUF_VERSION \\\"[^\\\"]+\\\"")
string(REGEX REPLACE "^#define GDK_PIXBUF_VERSION \\\"([0-9]+)\\.([0-9]+)\\.([0-9]+)\\\"$" "\\1.\\2.\\3" GDKPixbuf2_VERSION "${GDKPixbuf2_VERSION}")
endif()
endif()
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(GDKPixbuf2
REQUIRED_VARS GDKPixbuf2_LIBRARY
VERSION_VAR GDKPixbuf2_VERSION)

View File

@ -1,18 +0,0 @@
include(PkgConfigWithFallback)
find_pkg_config_with_fallback(GIO
PKG_CONFIG_NAME gio-2.0
LIB_NAMES gio-2.0
INCLUDE_NAMES gio/gio.h
INCLUDE_DIR_SUFFIXES glib-2.0 glib-2.0/include
DEPENDS GObject
)
if(GIO_FOUND AND NOT GIO_VERSION)
find_package(GLib ${GLib_GLOBAL_VERSION})
set(GIO_VERSION ${GLib_VERSION})
endif()
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(GIO
REQUIRED_VARS GIO_LIBRARY
VERSION_VAR GIO_VERSION)

View File

@ -1,32 +0,0 @@
include(PkgConfigWithFallback)
find_pkg_config_with_fallback(GLib
PKG_CONFIG_NAME glib-2.0
LIB_NAMES glib-2.0
INCLUDE_NAMES glib.h glibconfig.h
INCLUDE_DIR_HINTS ${CMAKE_LIBRARY_PATH} ${CMAKE_SYSTEM_LIBRARY_PATH}
INCLUDE_DIR_PATHS ${CMAKE_PREFIX_PATH}/lib64 ${CMAKE_PREFIX_PATH}/lib
INCLUDE_DIR_SUFFIXES glib-2.0 glib-2.0/include
)
if(GLib_FOUND AND NOT GLib_VERSION)
find_file(GLib_CONFIG_HEADER "glibconfig.h" HINTS ${GLib_INCLUDE_DIRS})
mark_as_advanced(GLib_CONFIG_HEADER)
if(GLib_CONFIG_HEADER)
file(STRINGS "${GLib_CONFIG_HEADER}" GLib_MAJOR_VERSION REGEX "^#define GLIB_MAJOR_VERSION +([0-9]+)")
string(REGEX REPLACE "^#define GLIB_MAJOR_VERSION ([0-9]+)$" "\\1" GLib_MAJOR_VERSION "${GLib_MAJOR_VERSION}")
file(STRINGS "${GLib_CONFIG_HEADER}" GLib_MINOR_VERSION REGEX "^#define GLIB_MINOR_VERSION +([0-9]+)")
string(REGEX REPLACE "^#define GLIB_MINOR_VERSION ([0-9]+)$" "\\1" GLib_MINOR_VERSION "${GLib_MINOR_VERSION}")
file(STRINGS "${GLib_CONFIG_HEADER}" GLib_MICRO_VERSION REGEX "^#define GLIB_MICRO_VERSION +([0-9]+)")
string(REGEX REPLACE "^#define GLIB_MICRO_VERSION ([0-9]+)$" "\\1" GLib_MICRO_VERSION "${GLib_MICRO_VERSION}")
set(GLib_VERSION "${GLib_MAJOR_VERSION}.${GLib_MINOR_VERSION}.${GLib_MICRO_VERSION}")
unset(GLib_MAJOR_VERSION)
unset(GLib_MINOR_VERSION)
unset(GLib_MICRO_VERSION)
endif()
endif()
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(GLib
REQUIRED_VARS GLib_LIBRARY
VERSION_VAR GLib_VERSION)

View File

@ -1,19 +0,0 @@
include(PkgConfigWithFallback)
find_pkg_config_with_fallback(GModule
PKG_CONFIG_NAME gmodule-2.0
LIB_NAMES gmodule-2.0
INCLUDE_NAMES gmodule.h
INCLUDE_DIR_SUFFIXES glib-2.0 glib-2.0/include
DEPENDS GLib
)
if(GModule_FOUND AND NOT GModule_VERSION)
# TODO
find_package(GLib ${GLib_GLOBAL_VERSION})
set(GModule_VERSION ${GLib_VERSION})
endif()
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(GModule
REQUIRED_VARS GModule_LIBRARY
VERSION_VAR GModule_VERSION)

View File

@ -1,19 +0,0 @@
include(PkgConfigWithFallback)
find_pkg_config_with_fallback(GObject
PKG_CONFIG_NAME gobject-2.0
LIB_NAMES gobject-2.0
INCLUDE_NAMES gobject/gobject.h
INCLUDE_DIR_SUFFIXES glib-2.0 glib-2.0/include
DEPENDS GLib
)
if(GObject_FOUND AND NOT GObject_VERSION)
# TODO
find_package(GLib ${GLib_GLOBAL_VERSION})
set(GObject_VERSION ${GLib_VERSION})
endif()
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(GObject
REQUIRED_VARS GObject_LIBRARY
VERSION_VAR GObject_VERSION)

View File

@ -1,10 +0,0 @@
include(PkgConfigWithFallbackOnConfigScript)
find_pkg_config_with_fallback_on_config_script(GPGME
PKG_CONFIG_NAME gpgme
CONFIG_SCRIPT_NAME gpgme
)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(GPGME
REQUIRED_VARS GPGME_LIBRARY
VERSION_VAR GPGME_VERSION)

View File

@ -1,31 +0,0 @@
include(PkgConfigWithFallback)
find_pkg_config_with_fallback(GTK3
PKG_CONFIG_NAME gtk+-3.0
LIB_NAMES gtk-3
INCLUDE_NAMES gtk/gtk.h
INCLUDE_DIR_SUFFIXES gtk-3.0 gtk-3.0/include gtk+-3.0 gtk+-3.0/include
DEPENDS GDK3 ATK
)
if(GTK3_FOUND AND NOT GTK3_VERSION)
find_file(GTK3_VERSION_HEADER "gtk/gtkversion.h" HINTS ${GTK3_INCLUDE_DIRS})
mark_as_advanced(GTK3_VERSION_HEADER)
if(GTK3_VERSION_HEADER)
file(STRINGS "${GTK3_VERSION_HEADER}" GTK3_MAJOR_VERSION REGEX "^#define GTK_MAJOR_VERSION +\\(?([0-9]+)\\)?$")
string(REGEX REPLACE "^#define GTK_MAJOR_VERSION \\(?([0-9]+)\\)?$" "\\1" GTK3_MAJOR_VERSION "${GTK3_MAJOR_VERSION}")
file(STRINGS "${GTK3_VERSION_HEADER}" GTK3_MINOR_VERSION REGEX "^#define GTK_MINOR_VERSION +\\(?([0-9]+)\\)?$")
string(REGEX REPLACE "^#define GTK_MINOR_VERSION \\(?([0-9]+)\\)?$" "\\1" GTK3_MINOR_VERSION "${GTK3_MINOR_VERSION}")
file(STRINGS "${GTK3_VERSION_HEADER}" GTK3_MICRO_VERSION REGEX "^#define GTK_MICRO_VERSION +\\(?([0-9]+)\\)?$")
string(REGEX REPLACE "^#define GTK_MICRO_VERSION \\(?([0-9]+)\\)?$" "\\1" GTK3_MICRO_VERSION "${GTK3_MICRO_VERSION}")
set(GTK3_VERSION "${GTK3_MAJOR_VERSION}.${GTK3_MINOR_VERSION}.${GTK3_MICRO_VERSION}")
unset(GTK3_MAJOR_VERSION)
unset(GTK3_MINOR_VERSION)
unset(GTK3_MICRO_VERSION)
endif()
endif()
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(GTK3
REQUIRED_VARS GTK3_LIBRARY
VERSION_VAR GTK3_VERSION)

View File

@ -1,13 +0,0 @@
include(PkgConfigWithFallback)
find_pkg_config_with_fallback(Gee
PKG_CONFIG_NAME gee-0.8
LIB_NAMES gee-0.8
INCLUDE_NAMES gee.h
INCLUDE_DIR_SUFFIXES gee-0.8 gee-0.8/include
DEPENDS GObject
)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(Gee
REQUIRED_VARS Gee_LIBRARY
VERSION_VAR Gee_VERSION)

View File

@ -1,20 +0,0 @@
find_program(XGETTEXT_EXECUTABLE xgettext)
find_program(MSGMERGE_EXECUTABLE msgmerge)
find_program(MSGFMT_EXECUTABLE msgfmt)
find_program(MSGCAT_EXECUTABLE msgcat)
mark_as_advanced(XGETTEXT_EXECUTABLE MSGMERGE_EXECUTABLE MSGFMT_EXECUTABLE MSGCAT_EXECUTABLE)
if(XGETTEXT_EXECUTABLE)
execute_process(COMMAND ${XGETTEXT_EXECUTABLE} "--version"
OUTPUT_VARIABLE Gettext_VERSION
OUTPUT_STRIP_TRAILING_WHITESPACE)
string(REGEX REPLACE "xgettext \\(GNU gettext-tools\\) ([0-9\\.]*).*" "\\1" Gettext_VERSION "${Gettext_VERSION}")
endif(XGETTEXT_EXECUTABLE)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(Gettext
FOUND_VAR Gettext_FOUND
REQUIRED_VARS XGETTEXT_EXECUTABLE MSGMERGE_EXECUTABLE MSGFMT_EXECUTABLE MSGCAT_EXECUTABLE
VERSION_VAR Gettext_VERSION)
set(GETTEXT_USE_FILE "${CMAKE_CURRENT_LIST_DIR}/UseGettext.cmake")

View File

@ -1,13 +0,0 @@
include(PkgConfigWithFallback)
find_pkg_config_with_fallback(GnuTLS
PKG_CONFIG_NAME gnutls
LIB_NAMES gnutls
INCLUDE_NAMES gnutls/gnutls.h
INCLUDE_DIR_SUFFIXES gnutls gnutls/include
DEPENDS GLib
)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(GnuTLS
REQUIRED_VARS GnuTLS_LIBRARY
VERSION_VAR GnuTLS_VERSION)

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 Gtk
)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(Gspell
REQUIRED_VARS Gspell_LIBRARY
VERSION_VAR Gspell_VERSION)

View File

@ -1,12 +0,0 @@
include(PkgConfigWithFallback)
find_pkg_config_with_fallback(Gst
PKG_CONFIG_NAME gstreamer-1.0
LIB_NAMES gstreamer-1.0
INCLUDE_NAMES gst/gst.h
INCLUDE_DIR_SUFFIXES gstreamer-1.0 gstreamer-1.0/include
)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(Gst
REQUIRED_VARS Gst_LIBRARY
VERSION_VAR Gst_VERSION)

View File

@ -1,14 +0,0 @@
include(PkgConfigWithFallback)
find_pkg_config_with_fallback(GstApp
PKG_CONFIG_NAME gstreamer-app-1.0
LIB_NAMES gstapp
LIB_DIR_HINTS gstreamer-1.0
INCLUDE_NAMES gst/app/app.h
INCLUDE_DIR_SUFFIXES gstreamer-1.0 gstreamer-1.0/include gstreamer-app-1.0 gstreamer-app-1.0/include
DEPENDS Gst
)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(GstApp
REQUIRED_VARS GstApp_LIBRARY
VERSION_VAR GstApp_VERSION)

View File

@ -1,14 +0,0 @@
include(PkgConfigWithFallback)
find_pkg_config_with_fallback(GstAudio
PKG_CONFIG_NAME gstreamer-audio-1.0
LIB_NAMES gstaudio
LIB_DIR_HINTS gstreamer-1.0
INCLUDE_NAMES gst/audio/audio.h
INCLUDE_DIR_SUFFIXES gstreamer-1.0 gstreamer-1.0/include gstreamer-audio-1.0 gstreamer-audio-1.0/include
DEPENDS Gst
)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(GstAudio
REQUIRED_VARS GstAudio_LIBRARY
VERSION_VAR GstAudio_VERSION)

View File

@ -1,19 +0,0 @@
include(PkgConfigWithFallback)
find_pkg_config_with_fallback(GstRtp
PKG_CONFIG_NAME gstreamer-rtp-1.0
LIB_NAMES gstrtp
LIB_DIR_HINTS gstreamer-1.0
INCLUDE_NAMES gst/rtp/rtp.h
INCLUDE_DIR_SUFFIXES gstreamer-1.0 gstreamer-1.0/include gstreamer-rtp-1.0 gstreamer-rtp-1.0/include
DEPENDS Gst
)
if(GstRtp_FOUND AND NOT GstRtp_VERSION)
find_package(Gst)
set(GstRtp_VERSION ${Gst_VERSION})
endif()
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(GstRtp
REQUIRED_VARS GstRtp_LIBRARY
VERSION_VAR GstRtp_VERSION)

View File

@ -1,14 +0,0 @@
include(PkgConfigWithFallback)
find_pkg_config_with_fallback(GstVideo
PKG_CONFIG_NAME gstreamer-video-1.0
LIB_NAMES gstvideo
LIB_DIR_HINTS gstreamer-1.0
INCLUDE_NAMES gst/video/video.h
INCLUDE_DIR_SUFFIXES gstreamer-1.0 gstreamer-1.0/include gstreamer-video-1.0 gstreamer-video-1.0/include
DEPENDS Gst
)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(GstVideo
REQUIRED_VARS GstVideo_LIBRARY
VERSION_VAR GstVideo_VERSION)

View File

@ -1,11 +0,0 @@
include(PkgConfigWithFallback)
find_pkg_config_with_fallback(ICU
PKG_CONFIG_NAME icu-uc
LIB_NAMES icuuc icudata
INCLUDE_NAMES unicode/umachine.h
)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(ICU
REQUIRED_VARS ICU_LIBRARY
VERSION_VAR ICU_VERSION)

View File

@ -1,13 +0,0 @@
include(PkgConfigWithFallback)
find_pkg_config_with_fallback(Nice
PKG_CONFIG_NAME nice
LIB_NAMES nice
INCLUDE_NAMES nice.h
INCLUDE_DIR_SUFFIXES nice nice/include
DEPENDS GIO
)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(Nice
REQUIRED_VARS Nice_LIBRARY
VERSION_VAR Nice_VERSION)

View File

@ -1,33 +0,0 @@
include(PkgConfigWithFallback)
find_pkg_config_with_fallback(Pango
PKG_CONFIG_NAME pango
LIB_NAMES pango-1.0
INCLUDE_NAMES pango/pango.h
INCLUDE_DIR_SUFFIXES pango-1.0 pango-1.0/include
DEPENDS GObject
)
if(Pango_FOUND AND NOT Pango_VERSION)
find_file(Pango_FEATURES_HEADER "pango/pango-features.h" HINTS ${Pango_INCLUDE_DIRS})
mark_as_advanced(Pango_FEATURES_HEADER)
if(Pango_FEATURES_HEADER)
file(STRINGS "${Pango_FEATURES_HEADER}" Pango_MAJOR_VERSION REGEX "^#define PANGO_VERSION_MAJOR +\\(?([0-9]+)\\)?$")
string(REGEX REPLACE "^#define PANGO_VERSION_MAJOR \\(?([0-9]+)\\)?$" "\\1" Pango_MAJOR_VERSION "${Pango_MAJOR_VERSION}")
file(STRINGS "${Pango_FEATURES_HEADER}" Pango_MINOR_VERSION REGEX "^#define PANGO_VERSION_MINOR +\\(?([0-9]+)\\)?$")
string(REGEX REPLACE "^#define PANGO_VERSION_MINOR \\(?([0-9]+)\\)?$" "\\1" Pango_MINOR_VERSION "${Pango_MINOR_VERSION}")
file(STRINGS "${Pango_FEATURES_HEADER}" Pango_MICRO_VERSION REGEX "^#define PANGO_VERSION_MICRO +\\(?([0-9]+)\\)?$")
string(REGEX REPLACE "^#define PANGO_VERSION_MICRO \\(?([0-9]+)\\)?$" "\\1" Pango_MICRO_VERSION "${Pango_MICRO_VERSION}")
set(Pango_VERSION "${Pango_MAJOR_VERSION}.${Pango_MINOR_VERSION}.${Pango_MICRO_VERSION}")
unset(Pango_MAJOR_VERSION)
unset(Pango_MINOR_VERSION)
unset(Pango_MICRO_VERSION)
endif()
endif()
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(Pango
FOUND_VAR Pango_FOUND
REQUIRED_VARS Pango_LIBRARY
VERSION_VAR Pango_VERSION
)

View File

@ -1,11 +0,0 @@
include(PkgConfigWithFallback)
find_pkg_config_with_fallback(Qrencode
PKG_CONFIG_NAME libqrencode
LIB_NAMES qrencode
INCLUDE_NAMES qrencode.h
)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(Qrencode
REQUIRED_VARS Qrencode_LIBRARY
VERSION_VAR Qrencode_VERSION)

View File

@ -1,21 +0,0 @@
include(PkgConfigWithFallback)
find_pkg_config_with_fallback(SQLite3
PKG_CONFIG_NAME sqlite3
LIB_NAMES sqlite3
INCLUDE_NAMES sqlite3.h
)
if(SQLite3_FOUND AND NOT SQLite3_VERSION)
find_file(SQLite3_HEADER "sqlite3.h" HINTS ${SQLite3_INCLUDE_DIRS})
mark_as_advanced(SQLite3_HEADER)
if(SQLite3_HEADER)
file(STRINGS "${SQLite3_HEADER}" SQLite3_VERSION REGEX "^#define SQLITE_VERSION +\\\"[^\\\"]+\\\"")
string(REGEX REPLACE "^#define SQLITE_VERSION +\\\"([0-9]+)\\.([0-9]+)\\.([0-9]+)\\\"$" "\\1.\\2.\\3" SQLite3_VERSION "${SQLite3_VERSION}")
endif()
endif()
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(SQLite3
REQUIRED_VARS SQLite3_LIBRARY
VERSION_VAR SQLite3_VERSION)

View File

@ -1,11 +0,0 @@
include(PkgConfigWithFallback)
find_pkg_config_with_fallback(SignalProtocol
PKG_CONFIG_NAME libsignal-protocol-c
LIB_NAMES signal-protocol-c
INCLUDE_NAMES signal/signal_protocol.h
)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(SignalProtocol
REQUIRED_VARS SignalProtocol_LIBRARY
VERSION_VAR SignalProtocol_VERSION)

View File

@ -1,31 +0,0 @@
include(PkgConfigWithFallback)
find_pkg_config_with_fallback(Soup
PKG_CONFIG_NAME libsoup-2.4
LIB_NAMES soup-2.4
INCLUDE_NAMES libsoup/soup.h
INCLUDE_DIR_SUFFIXES libsoup-2.4 libsoup-2.4/include libsoup libsoup/include
DEPENDS GIO
)
if(Soup_FOUND AND NOT Soup_VERSION)
find_file(Soup_VERSION_HEADER "libsoup/soup-version.h" HINTS ${Soup_INCLUDE_DIRS})
mark_as_advanced(Soup_VERSION_HEADER)
if(Soup_VERSION_HEADER)
file(STRINGS "${Soup_VERSION_HEADER}" Soup_MAJOR_VERSION REGEX "^#define SOUP_MAJOR_VERSION +\\(?([0-9]+)\\)?$")
string(REGEX REPLACE "^#define SOUP_MAJOR_VERSION \\(?([0-9]+)\\)?$" "\\1" Soup_MAJOR_VERSION "${Soup_MAJOR_VERSION}")
file(STRINGS "${Soup_VERSION_HEADER}" Soup_MINOR_VERSION REGEX "^#define SOUP_MINOR_VERSION +\\(?([0-9]+)\\)?$")
string(REGEX REPLACE "^#define SOUP_MINOR_VERSION \\(?([0-9]+)\\)?$" "\\1" Soup_MINOR_VERSION "${Soup_MINOR_VERSION}")
file(STRINGS "${Soup_VERSION_HEADER}" Soup_MICRO_VERSION REGEX "^#define SOUP_MICRO_VERSION +\\(?([0-9]+)\\)?$")
string(REGEX REPLACE "^#define SOUP_MICRO_VERSION \\(?([0-9]+)\\)?$" "\\1" Soup_MICRO_VERSION "${Soup_MICRO_VERSION}")
set(Soup_VERSION "${Soup_MAJOR_VERSION}.${Soup_MINOR_VERSION}.${Soup_MICRO_VERSION}")
unset(Soup_MAJOR_VERSION)
unset(Soup_MINOR_VERSION)
unset(Soup_MICRO_VERSION)
endif()
endif()
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(Soup
REQUIRED_VARS Soup_LIBRARY
VERSION_VAR Soup_VERSION)

View File

@ -1,12 +0,0 @@
include(PkgConfigWithFallback)
find_pkg_config_with_fallback(Srtp2
PKG_CONFIG_NAME libsrtp2
LIB_NAMES srtp2
INCLUDE_NAMES srtp2/srtp.h
INCLUDE_DIR_SUFFIXES srtp2 srtp2/include
)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(Srtp2
REQUIRED_VARS Srtp2_LIBRARY
VERSION_VAR Srtp2_VERSION)

View File

@ -1,73 +0,0 @@
##
# Find module for the Vala compiler (valac)
#
# This module determines wheter a Vala compiler is installed on the current
# system and where its executable is.
#
# Call the module using "find_package(Vala) from within your CMakeLists.txt.
#
# The following variables will be set after an invocation:
#
# VALA_FOUND Whether the vala compiler has been found or not
# VALA_EXECUTABLE Full path to the valac executable if it has been found
# VALA_VERSION Version number of the available valac
# VALA_USE_FILE Include this file to define the vala_precompile function
##
##
# Copyright 2009-2010 Jakob Westhoff. All rights reserved.
# Copyright 2010-2011 Daniel Pfeifer
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY JAKOB WESTHOFF ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
# EVENT SHALL JAKOB WESTHOFF OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and documentation are those
# of the authors and should not be interpreted as representing official policies,
# either expressed or implied, of Jakob Westhoff
##
# Search for the valac executable in the usual system paths
# Some distributions rename the valac to contain the major.minor in the binary name
find_package(GObject REQUIRED)
find_program(VALA_EXECUTABLE NAMES valac valac-0.38 valac-0.36 valac-0.34 valac-0.32)
mark_as_advanced(VALA_EXECUTABLE)
# Determine the valac version
if(VALA_EXECUTABLE)
file(TO_NATIVE_PATH "${VALA_EXECUTABLE}" VALA_EXECUTABLE)
execute_process(COMMAND ${VALA_EXECUTABLE} "--version"
OUTPUT_VARIABLE VALA_VERSION
OUTPUT_STRIP_TRAILING_WHITESPACE)
string(REPLACE "Vala " "" VALA_VERSION "${VALA_VERSION}")
endif(VALA_EXECUTABLE)
# Handle the QUIETLY and REQUIRED arguments, which may be given to the find call.
# Furthermore set VALA_FOUND to TRUE if Vala has been found (aka.
# VALA_EXECUTABLE is set)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(Vala
FOUND_VAR VALA_FOUND
REQUIRED_VARS VALA_EXECUTABLE
VERSION_VAR VALA_VERSION)
set(VALA_USE_FILE "${CMAKE_CURRENT_LIST_DIR}/UseVala.cmake")

View File

@ -1,12 +0,0 @@
include(PkgConfigWithFallback)
find_pkg_config_with_fallback(WebRTCAudioProcessing
PKG_CONFIG_NAME webrtc-audio-processing
LIB_NAMES webrtc_audio_processing
INCLUDE_NAMES webrtc/modules/audio_processing/include/audio_processing.h
INCLUDE_DIR_SUFFIXES webrtc-audio-processing webrtc_audio_processing
)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(WebRTCAudioProcessing
REQUIRED_VARS WebRTCAudioProcessing_LIBRARY
VERSION_VAR WebRTCAudioProcessing_VERSION)

View File

@ -1,124 +0,0 @@
include(CMakeParseArguments)
# Generates the resource XML controlling file from resource list (and saves it
# to xml_path). It's not recommended to use this function directly, since it
# doesn't handle invalid arguments. It is used by the function
# COMPILE_GRESOURCES() to create a custom command, so that this function is
# invoked at build-time in script mode from CMake.
function(GENERATE_GXML xml_path)
# Available options:
# COMPRESS_ALL, NO_COMPRESS_ALL Overrides the COMPRESS flag in all
# registered resources.
# STRIPBLANKS_ALL, NO_STRIPBLANKS_ALL Overrides the STRIPBLANKS flag in all
# registered resources.
# TOPIXDATA_ALL, NO_TOPIXDATA_ALL Overrides the TOPIXDATA flag in all
# registered resources.
set(GXML_OPTIONS COMPRESS_ALL NO_COMPRESS_ALL
STRIPBLANKS_ALL NO_STRIPBLANKS_ALL
TOPIXDATA_ALL NO_TOPIXDATA_ALL)
# Available one value options:
# PREFIX Overrides the resource prefix that is prepended to each
# relative file name in registered resources.
set(GXML_ONEVALUEARGS PREFIX)
# Available multi-value options:
# RESOURCES The list of resource files. Whether absolute or relative path is
# equal, absolute paths are stripped down to relative ones. If the
# absolute path is not inside the given base directory SOURCE_DIR
# or CMAKE_SOURCE_DIR (if SOURCE_DIR is not overriden), this
# function aborts.
set(GXML_MULTIVALUEARGS RESOURCES)
# Parse the arguments.
cmake_parse_arguments(GXML_ARG
"${GXML_OPTIONS}"
"${GXML_ONEVALUEARGS}"
"${GXML_MULTIVALUEARGS}"
"${ARGN}")
# Variable to store the double-quote (") string. Since escaping
# double-quotes in strings is not possible we need a helper variable that
# does this job for us.
set(Q \")
# Process resources and generate XML file.
# Begin with the XML header and header nodes.
set(GXML_XML_FILE "<?xml version=${Q}1.0${Q} encoding=${Q}UTF-8${Q}?>")
set(GXML_XML_FILE "${GXML_XML_FILE}<gresources><gresource prefix=${Q}")
# Set the prefix for the resources. Depending on the user-override we choose
# the standard prefix "/" or the override.
if (GXML_ARG_PREFIX)
set(GXML_XML_FILE "${GXML_XML_FILE}${GXML_ARG_PREFIX}")
else()
set(GXML_XML_FILE "${GXML_XML_FILE}/")
endif()
set(GXML_XML_FILE "${GXML_XML_FILE}${Q}>")
# Process each resource.
foreach(res ${GXML_ARG_RESOURCES})
if ("${res}" STREQUAL "COMPRESS")
set(GXML_COMPRESSION_FLAG ON)
elseif ("${res}" STREQUAL "STRIPBLANKS")
set(GXML_STRIPBLANKS_FLAG ON)
elseif ("${res}" STREQUAL "TOPIXDATA")
set(GXML_TOPIXDATA_FLAG ON)
else()
# The file name.
set(GXML_RESOURCE_PATH "${res}")
# Append to real resource file dependency list.
list(APPEND GXML_RESOURCES_DEPENDENCIES ${GXML_RESOURCE_PATH})
# Assemble <file> node.
set(GXML_RES_LINE "<file")
if ((GXML_ARG_COMPRESS_ALL OR GXML_COMPRESSION_FLAG) AND NOT
GXML_ARG_NO_COMPRESS_ALL)
set(GXML_RES_LINE "${GXML_RES_LINE} compressed=${Q}true${Q}")
endif()
# Check preprocess flag validity.
if ((GXML_ARG_STRIPBLANKS_ALL OR GXML_STRIPBLANKS_FLAG) AND
(GXML_ARG_TOPIXDATA_ALL OR GXML_TOPIXDATA_FLAG))
set(GXML_ERRMSG "Resource preprocessing option conflict. Tried")
set(GXML_ERRMSG "${GXML_ERRMSG} to specify both, STRIPBLANKS")
set(GXML_ERRMSG "${GXML_ERRMSG} and TOPIXDATA. In resource")
set(GXML_ERRMSG "${GXML_ERRMSG} ${GXML_RESOURCE_PATH} in")
set(GXML_ERRMSG "${GXML_ERRMSG} function COMPILE_GRESOURCES.")
message(FATAL_ERROR ${GXML_ERRMSG})
endif()
if ((GXML_ARG_STRIPBLANKS_ALL OR GXML_STRIPBLANKS_FLAG) AND NOT
GXML_ARG_NO_STRIPBLANKS_ALL)
set(GXML_RES_LINE "${GXML_RES_LINE} preprocess=")
set(GXML_RES_LINE "${GXML_RES_LINE}${Q}xml-stripblanks${Q}")
elseif((GXML_ARG_TOPIXDATA_ALL OR GXML_TOPIXDATA_FLAG) AND NOT
GXML_ARG_NO_TOPIXDATA_ALL)
set(GXML_RES_LINE "${GXML_RES_LINE} preprocess=")
set(GXML_RES_LINE "${GXML_RES_LINE}${Q}to-pixdata${Q}")
endif()
set(GXML_RES_LINE "${GXML_RES_LINE}>${GXML_RESOURCE_PATH}</file>")
# Append to file string.
set(GXML_XML_FILE "${GXML_XML_FILE}${GXML_RES_LINE}")
# Unset variables.
unset(GXML_COMPRESSION_FLAG)
unset(GXML_STRIPBLANKS_FLAG)
unset(GXML_TOPIXDATA_FLAG)
endif()
endforeach()
# Append closing nodes.
set(GXML_XML_FILE "${GXML_XML_FILE}</gresource></gresources>")
# Use "file" function to generate XML controlling file.
get_filename_component(xml_path_only_name "${xml_path}" NAME)
file(WRITE ${xml_path} ${GXML_XML_FILE})
endfunction()

View File

@ -1,11 +0,0 @@
# Path to this file.
set(GCR_CMAKE_MACRO_DIR ${CMAKE_CURRENT_LIST_DIR})
# Finds the glib-compile-resources executable.
find_program(GLIB_COMPILE_RESOURCES_EXECUTABLE glib-compile-resources)
mark_as_advanced(GLIB_COMPILE_RESOURCES_EXECUTABLE)
# Include the cmake files containing the functions.
include(${GCR_CMAKE_MACRO_DIR}/CompileGResources.cmake)
include(${GCR_CMAKE_MACRO_DIR}/GenerateGXML.cmake)

View File

@ -1,11 +0,0 @@
#include <sys/types.h>
#define _K ((off_t)1024)
#define _M ((off_t)1024 * _K)
#define _G ((off_t)1024 * _M)
#define _T ((off_t)1024 * _G)
int test[(((64 * _G -1) % 671088649) == 268434537) && (((_T - (64 * _G -1) + 255) % 1792151290) == 305159546)? 1: -1];
int main() {
return 0;
}

View File

@ -1,45 +0,0 @@
include(CMakeParseArguments)
function(find_packages result)
cmake_parse_arguments(ARGS "" "" "REQUIRED;OPTIONAL" ${ARGN})
set(_res "")
set(_res_libs "")
foreach(pkg ${ARGS_REQUIRED})
string(REPLACE ">=" ";" pkg_ ${pkg})
list(GET pkg_ "0" pkg)
list(LENGTH pkg_ pkg_has_version)
if(pkg_has_version GREATER 1)
list(GET pkg_ "1" pkg_version)
else()
if(${pkg}_GLOBAL_VERSION)
set(pkg_version ${${pkg}_GLOBAL_VERSION})
else()
unset(pkg_version)
endif()
endif()
find_package(${pkg} ${pkg_version} REQUIRED)
list(APPEND _res ${${pkg}_PKG_CONFIG_NAME})
list(APPEND _res_libs ${${pkg}_LIBRARIES})
endforeach(pkg)
foreach(pkg ${ARGS_OPTIONAL})
string(REPLACE ">=" ";" pkg_ ${pkg})
list(GET pkg_ "0" pkg)
list(LENGTH pkg_ pkg_has_version)
if(pkg_has_version GREATER 1)
list(GET pkg_ "1" pkg_version)
else()
if(${pkg}_GLOBAL_VERSION)
set(pkg_version ${${pkg}_GLOBAL_VERSION})
else()
unset(pkg_version)
endif()
endif()
find_package(${pkg} ${pkg_version})
if(${pkg}_FOUND)
list(APPEND _res ${${pkg}_PKG_CONFIG_NAME})
list(APPEND _res_libs ${${pkg}_LIBRARIES})
endif()
endforeach(pkg)
set(${result} "${_res}" PARENT_SCOPE)
set(${result}_LIBS "${_res_libs}" PARENT_SCOPE)
endfunction()

View File

@ -1,102 +0,0 @@
include(CMakeParseArguments)
function(find_pkg_config_with_fallback name)
cmake_parse_arguments(ARGS "" "PKG_CONFIG_NAME" "LIB_NAMES;LIB_DIR_HINTS;INCLUDE_NAMES;INCLUDE_DIR_PATHS;INCLUDE_DIR_HINTS;INCLUDE_DIR_SUFFIXES;DEPENDS" ${ARGN})
set(${name}_PKG_CONFIG_NAME ${ARGS_PKG_CONFIG_NAME} PARENT_SCOPE)
find_package(PkgConfig)
if(PKG_CONFIG_FOUND)
pkg_search_module(${name}_PKG_CONFIG QUIET ${ARGS_PKG_CONFIG_NAME})
endif(PKG_CONFIG_FOUND)
if (${name}_PKG_CONFIG_FOUND)
# Found via pkg-config, using its result values
set(${name}_FOUND ${${name}_PKG_CONFIG_FOUND})
# Try to find real file name of libraries
foreach(lib ${${name}_PKG_CONFIG_LIBRARIES})
find_library(${name}_${lib}_LIBRARY ${lib} HINTS ${${name}_PKG_CONFIG_LIBRARY_DIRS})
mark_as_advanced(${name}_${lib}_LIBRARY)
if(NOT ${name}_${lib}_LIBRARY)
unset(${name}_FOUND)
endif(NOT ${name}_${lib}_LIBRARY)
endforeach(lib)
if(${name}_FOUND)
set(${name}_LIBRARIES "")
foreach(lib ${${name}_PKG_CONFIG_LIBRARIES})
list(APPEND ${name}_LIBRARIES ${${name}_${lib}_LIBRARY})
endforeach(lib)
list(REMOVE_DUPLICATES ${name}_LIBRARIES)
set(${name}_LIBRARIES ${${name}_LIBRARIES} PARENT_SCOPE)
list(GET ${name}_LIBRARIES "0" ${name}_LIBRARY)
set(${name}_FOUND ${${name}_FOUND} PARENT_SCOPE)
set(${name}_INCLUDE_DIRS ${${name}_PKG_CONFIG_INCLUDE_DIRS} PARENT_SCOPE)
set(${name}_LIBRARIES ${${name}_PKG_CONFIG_LIBRARIES} PARENT_SCOPE)
set(${name}_LIBRARY ${${name}_LIBRARY} PARENT_SCOPE)
set(${name}_VERSION ${${name}_PKG_CONFIG_VERSION} PARENT_SCOPE)
if(NOT TARGET ${ARGS_PKG_CONFIG_NAME})
add_library(${ARGS_PKG_CONFIG_NAME} INTERFACE IMPORTED)
set_property(TARGET ${ARGS_PKG_CONFIG_NAME} PROPERTY INTERFACE_COMPILE_OPTIONS "${${name}_PKG_CONFIG_CFLAGS_OTHER}")
set_property(TARGET ${ARGS_PKG_CONFIG_NAME} PROPERTY INTERFACE_INCLUDE_DIRECTORIES "${${name}_PKG_CONFIG_INCLUDE_DIRS}")
set_property(TARGET ${ARGS_PKG_CONFIG_NAME} PROPERTY INTERFACE_LINK_LIBRARIES "${${name}_LIBRARIES}")
endif(NOT TARGET ${ARGS_PKG_CONFIG_NAME})
endif(${name}_FOUND)
else(${name}_PKG_CONFIG_FOUND)
# No success with pkg-config, try via find_library on all lib_names
set(${name}_FOUND "1")
foreach(lib ${ARGS_LIB_NAMES})
find_library(${name}_${lib}_LIBRARY ${ARGS_LIB_NAMES} HINTS ${ARGS_LIB_DIR_HINTS})
mark_as_advanced(${name}_${lib}_LIBRARY)
if(NOT ${name}_${lib}_LIBRARY)
unset(${name}_FOUND)
endif(NOT ${name}_${lib}_LIBRARY)
endforeach(lib)
foreach(inc ${ARGS_INCLUDE_NAMES})
find_path(${name}_${inc}_INCLUDE_PATH ${inc} HINTS ${ARGS_INCLUDE_DIR_HINTS} PATHS ${ARGS_INCLUDE_DIR_PATHS} PATH_SUFFIXES ${ARGS_INCLUDE_DIR_SUFFIXES})
mark_as_advanced(${name}_${inc}_INCLUDE_PATH)
if(NOT ${name}_${inc}_INCLUDE_PATH)
unset(${name}_FOUND)
endif(NOT ${name}_${inc}_INCLUDE_PATH)
endforeach(inc)
if(${name}_FOUND)
set(${name}_LIBRARIES "")
set(${name}_INCLUDE_DIRS "")
foreach(lib ${ARGS_LIB_NAMES})
list(APPEND ${name}_LIBRARIES ${${name}_${lib}_LIBRARY})
endforeach(lib)
foreach(inc ${ARGS_INCLUDE_NAMES})
list(APPEND ${name}_INCLUDE_DIRS ${${name}_${inc}_INCLUDE_PATH})
endforeach(inc)
list(GET ${name}_LIBRARIES "0" ${name}_LIBRARY)
foreach(dep ${ARGS_DEPENDS})
find_package(${dep} ${${dep}_GLOBAL_VERSION} QUIET)
if(${dep}_FOUND)
list(APPEND ${name}_INCLUDE_DIRS ${${dep}_INCLUDE_DIRS})
list(APPEND ${name}_LIBRARIES ${${dep}_LIBRARIES})
else(${dep}_FOUND)
unset(${name}_FOUND)
endif(${dep}_FOUND)
endforeach(dep)
set(${name}_FOUND ${${name}_FOUND} PARENT_SCOPE)
set(${name}_INCLUDE_DIRS ${${name}_INCLUDE_DIRS} PARENT_SCOPE)
set(${name}_LIBRARIES ${${name}_LIBRARIES} PARENT_SCOPE)
set(${name}_LIBRARY ${${name}_LIBRARY} PARENT_SCOPE)
unset(${name}_VERSION PARENT_SCOPE)
if(NOT TARGET ${ARGS_PKG_CONFIG_NAME})
add_library(${ARGS_PKG_CONFIG_NAME} INTERFACE IMPORTED)
set_property(TARGET ${ARGS_PKG_CONFIG_NAME} PROPERTY INTERFACE_INCLUDE_DIRECTORIES "${${name}_INCLUDE_DIRS}")
set_property(TARGET ${ARGS_PKG_CONFIG_NAME} PROPERTY INTERFACE_LINK_LIBRARIES "${${name}_LIBRARIES}")
endif(NOT TARGET ${ARGS_PKG_CONFIG_NAME})
endif(${name}_FOUND)
endif(${name}_PKG_CONFIG_FOUND)
endfunction()

View File

@ -1,103 +0,0 @@
include(CMakeParseArguments)
function(find_pkg_config_with_fallback_on_config_script name)
cmake_parse_arguments(ARGS "" "PKG_CONFIG_NAME" "CONFIG_SCRIPT_NAME" ${ARGN})
set(${name}_PKG_CONFIG_NAME ${ARGS_PKG_CONFIG_NAME} PARENT_SCOPE)
find_package(PkgConfig)
if(PKG_CONFIG_FOUND)
pkg_search_module(${name}_PKG_CONFIG QUIET ${ARGS_PKG_CONFIG_NAME})
endif(PKG_CONFIG_FOUND)
if (${name}_PKG_CONFIG_FOUND)
# Found via pkg-config, using it's result values
set(${name}_FOUND ${${name}_PKG_CONFIG_FOUND})
# Try to find real file name of libraries
foreach(lib ${${name}_PKG_CONFIG_LIBRARIES})
find_library(${name}_${lib}_LIBRARY ${lib} HINTS ${${name}_PKG_CONFIG_LIBRARY_DIRS})
mark_as_advanced(${name}_${lib}_LIBRARY)
if(NOT ${name}_${lib}_LIBRARY)
unset(${name}_FOUND)
endif(NOT ${name}_${lib}_LIBRARY)
endforeach(lib)
if(${name}_FOUND)
set(${name}_LIBRARIES "")
foreach(lib ${${name}_PKG_CONFIG_LIBRARIES})
list(APPEND ${name}_LIBRARIES ${${name}_${lib}_LIBRARY})
endforeach(lib)
list(REMOVE_DUPLICATES ${name}_LIBRARIES)
set(${name}_LIBRARIES ${${name}_LIBRARIES} PARENT_SCOPE)
list(GET ${name}_LIBRARIES "0" ${name}_LIBRARY)
set(${name}_FOUND ${${name}_FOUND} PARENT_SCOPE)
set(${name}_INCLUDE_DIRS ${${name}_PKG_CONFIG_INCLUDE_DIRS} PARENT_SCOPE)
set(${name}_LIBRARIES ${${name}_PKG_CONFIG_LIBRARIES} PARENT_SCOPE)
set(${name}_LIBRARY ${${name}_LIBRARY} PARENT_SCOPE)
set(${name}_VERSION ${${name}_PKG_CONFIG_VERSION} PARENT_SCOPE)
if(NOT TARGET ${ARGS_PKG_CONFIG_NAME})
add_library(${ARGS_PKG_CONFIG_NAME} INTERFACE IMPORTED)
set_property(TARGET ${ARGS_PKG_CONFIG_NAME} PROPERTY INTERFACE_COMPILE_OPTIONS "${${name}_PKG_CONFIG_CFLAGS_OTHER}")
set_property(TARGET ${ARGS_PKG_CONFIG_NAME} PROPERTY INTERFACE_INCLUDE_DIRECTORIES "${${name}_PKG_CONFIG_INCLUDE_DIRS}")
set_property(TARGET ${ARGS_PKG_CONFIG_NAME} PROPERTY INTERFACE_LINK_LIBRARIES "${${name}_LIBRARIES}")
endif(NOT TARGET ${ARGS_PKG_CONFIG_NAME})
endif(${name}_FOUND)
else(${name}_PKG_CONFIG_FOUND)
# No success with pkg-config, try via custom a *-config script
find_program(${name}_CONFIG_EXECUTABLE NAMES ${ARGS_CONFIG_SCRIPT_NAME}-config)
mark_as_advanced(${name}_CONFIG_EXECUTABLE)
find_program(${name}_SH_EXECUTABLE NAMES sh)
mark_as_advanced(${name}_SH_EXECUTABLE)
if(${name}_CONFIG_EXECUTABLE)
macro(config_script_fail errcode)
if(${errcode})
message(FATAL_ERROR "Error invoking ${ARGS_CONFIG_SCRIPT_NAME}-config: ${errcode}")
endif(${errcode})
endmacro(config_script_fail)
file(TO_NATIVE_PATH "${${name}_CONFIG_EXECUTABLE}" ${name}_CONFIG_EXECUTABLE)
file(TO_NATIVE_PATH "${${name}_SH_EXECUTABLE}" ${name}_SH_EXECUTABLE)
execute_process(COMMAND "${${name}_SH_EXECUTABLE}" "${${name}_CONFIG_EXECUTABLE}" --version
OUTPUT_VARIABLE ${name}_VERSION
RESULT_VARIABLE ERRCODE
OUTPUT_STRIP_TRAILING_WHITESPACE)
config_script_fail(${ERRCODE})
execute_process(COMMAND "${${name}_SH_EXECUTABLE}" "${${name}_CONFIG_EXECUTABLE}" --api-version
OUTPUT_VARIABLE ${name}_API_VERSION
RESULT_VARIABLE ERRCODE
OUTPUT_STRIP_TRAILING_WHITESPACE)
config_script_fail(${ERRCODE})
execute_process(COMMAND "${${name}_SH_EXECUTABLE}" "${${name}_CONFIG_EXECUTABLE}" --cflags
OUTPUT_VARIABLE ${name}_CFLAGS
RESULT_VARIABLE ERRCODE
OUTPUT_STRIP_TRAILING_WHITESPACE)
config_script_fail(${ERRCODE})
execute_process(COMMAND "${${name}_SH_EXECUTABLE}" "${${name}_CONFIG_EXECUTABLE}" --libs
OUTPUT_VARIABLE ${name}_LDFLAGS
RESULT_VARIABLE ERRCODE
OUTPUT_STRIP_TRAILING_WHITESPACE)
config_script_fail(${ERRCODE})
string(TOLOWER ${name} "${name}_LOWER")
string(REGEX REPLACE "^(.* |)-l([^ ]*${${name}_LOWER}[^ ]*)( .*|)$" "\\2" ${name}_LIBRARY_NAME "${${name}_LDFLAGS}")
string(REGEX REPLACE "^(.* |)-L([^ ]*)( .*|)$" "\\2" ${name}_LIBRARY_DIRS "${${name}_LDFLAGS}")
find_library(${name}_LIBRARY ${${name}_LIBRARY_NAME} HINTS ${${name}_LIBRARY_DIRS})
mark_as_advanced(${name}_LIBRARY)
set(${name}_LIBRARY ${${name}_LIBRARY} PARENT_SCOPE)
set(${name}_VERSION ${${name}_VERSION} PARENT_SCOPE)
unset(${name}_LIBRARY_NAME)
unset(${name}_LIBRARY_DIRS)
if(NOT TARGET ${name}_LOWER)
add_library(${name}_LOWER INTERFACE IMPORTED)
set_property(TARGET ${name}_LOWER PROPERTY INTERFACE_LINK_LIBRARIES "${${name}_LDFLAGS}")
set_property(TARGET ${name}_LOWER PROPERTY INTERFACE_COMPILE_OPTIONS "${${name}_CFLAGS}")
endif(NOT TARGET ${name}_LOWER)
endif(${name}_CONFIG_EXECUTABLE)
endif(${name}_PKG_CONFIG_FOUND)
endfunction()

View File

@ -1,28 +0,0 @@
function(_gettext_mkdir_for_file file)
get_filename_component(dir "${file}" DIRECTORY)
file(MAKE_DIRECTORY "${dir}")
endfunction()
function(gettext_compile project_name)
cmake_parse_arguments(ARGS "" "MO_FILES_NAME;TARGET_NAME;SOURCE_DIR;PROJECT_NAME" "" ${ARGN})
if(NOT ARGS_SOURCE_DIR)
set(ARGS_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR})
endif(NOT ARGS_SOURCE_DIR)
file(STRINGS "${ARGS_SOURCE_DIR}/LINGUAS" LINGUAS)
set(target_files)
foreach(lang ${LINGUAS})
set(source_file ${ARGS_SOURCE_DIR}/${lang}.po)
set(target_file ${CMAKE_BINARY_DIR}/locale/${lang}/LC_MESSAGES/${project_name}.mo)
_gettext_mkdir_for_file(${target_file})
list(APPEND target_files ${target_file})
add_custom_command(OUTPUT ${target_file} COMMAND ${MSGFMT_EXECUTABLE} --check-format -o ${target_file} ${source_file} DEPENDS ${source_file})
install(FILES ${target_file} DESTINATION ${LOCALE_INSTALL_DIR}/${lang}/LC_MESSAGES)
endforeach(lang)
if(ARGS_MO_FILES_NAME)
set(${ARGS_MO_FILES_NAME} ${target_files} PARENT_SCOPE)
endif(ARGS_MO_FILES_NAME)
if(ARGS_TARGET_NAME)
add_custom_target(${ARGS_TARGET_NAME} DEPENDS ${target_files})
endif(ARGS_TARGET_NAME)
endfunction(gettext_compile)

View File

@ -1,337 +0,0 @@
##
# Compile vala files to their c equivalents for further processing.
#
# The "vala_precompile" function takes care of calling the valac executable on
# the given source to produce c files which can then be processed further using
# default cmake functions.
#
# The first parameter provided is a variable, which will be filled with a list
# of c files outputted by the vala compiler. This list can than be used in
# conjuction with functions like "add_executable" or others to create the
# neccessary compile rules with CMake.
#
# The following sections may be specified afterwards to provide certain options
# to the vala compiler:
#
# SOURCES
# A list of .vala files to be compiled. Please take care to add every vala
# file belonging to the currently compiled project or library as Vala will
# otherwise not be able to resolve all dependencies.
#
# PACKAGES
# A list of vala packages/libraries to be used during the compile cycle. The
# package names are exactly the same, as they would be passed to the valac
# "--pkg=" option.
#
# OPTIONS
# A list of optional options to be passed to the valac executable. This can be
# used to pass "--thread" for example to enable multi-threading support.
#
# DEFINITIONS
# A list of symbols to be used for conditional compilation. They are the same
# as they would be passed using the valac "--define=" option.
#
# CUSTOM_VAPIS
# A list of custom vapi files to be included for compilation. This can be
# useful to include freshly created vala libraries without having to install
# them in the system.
#
# GENERATE_VAPI
# Pass all the needed flags to the compiler to create a vapi for
# the compiled library. The provided name will be used for this and a
# <provided_name>.vapi file will be created.
#
# GENERATE_HEADER
# Let the compiler generate a header file for the compiled code. There will
# be a header file as well as an internal header file being generated called
# <provided_name>.h and <provided_name>_internal.h
#
# The following call is a simple example to the vala_precompile macro showing
# an example to every of the optional sections:
#
# find_package(Vala "0.12" REQUIRED)
# include(${VALA_USE_FILE})
#
# vala_precompile(VALA_C
# SOURCES
# source1.vala
# source2.vala
# source3.vala
# PACKAGES
# gtk+-2.0
# gio-1.0
# posix
# DIRECTORY
# gen
# OPTIONS
# --thread
# CUSTOM_VAPIS
# some_vapi.vapi
# GENERATE_VAPI
# myvapi
# GENERATE_HEADER
# myheader
# )
#
# Most important is the variable VALA_C which will contain all the generated c
# file names after the call.
##
##
# Copyright 2009-2010 Jakob Westhoff. All rights reserved.
# Copyright 2010-2011 Daniel Pfeifer
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY JAKOB WESTHOFF ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
# EVENT SHALL JAKOB WESTHOFF OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and documentation are those
# of the authors and should not be interpreted as representing official policies,
# either expressed or implied, of Jakob Westhoff
##
include(CMakeParseArguments)
function(_vala_mkdir_for_file file)
get_filename_component(dir "${file}" DIRECTORY)
file(MAKE_DIRECTORY "${dir}")
endfunction()
function(vala_precompile output)
cmake_parse_arguments(ARGS "FAST_VAPI" "DIRECTORY;GENERATE_HEADER;GENERATE_VAPI;EXPORTS_DIR"
"SOURCES;PACKAGES;OPTIONS;DEFINITIONS;CUSTOM_VAPIS;CUSTOM_DEPS;GRESOURCES" ${ARGN})
# Header and internal header is needed to generate internal vapi
if (ARGS_GENERATE_VAPI AND NOT ARGS_GENERATE_HEADER)
set(ARGS_GENERATE_HEADER ${ARGS_GENERATE_VAPI})
endif(ARGS_GENERATE_VAPI AND NOT ARGS_GENERATE_HEADER)
if("Ninja" STREQUAL ${CMAKE_GENERATOR} AND NOT DISABLE_FAST_VAPI AND NOT ARGS_GENERATE_HEADER)
set(ARGS_FAST_VAPI true)
endif()
if(ARGS_DIRECTORY)
get_filename_component(DIRECTORY ${ARGS_DIRECTORY} ABSOLUTE)
else(ARGS_DIRECTORY)
set(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
endif(ARGS_DIRECTORY)
if(ARGS_EXPORTS_DIR)
set(ARGS_EXPORTS_DIR ${CMAKE_BINARY_DIR}/${ARGS_EXPORTS_DIR})
else(ARGS_EXPORTS_DIR)
set(ARGS_EXPORTS_DIR ${CMAKE_BINARY_DIR}/exports)
endif(ARGS_EXPORTS_DIR)
file(MAKE_DIRECTORY "${ARGS_EXPORTS_DIR}")
include_directories(${DIRECTORY} ${ARGS_EXPORTS_DIR})
set(vala_pkg_opts "")
foreach(pkg ${ARGS_PACKAGES})
list(APPEND vala_pkg_opts "--pkg=${pkg}")
endforeach(pkg ${ARGS_PACKAGES})
set(vala_define_opts "")
foreach(def ${ARGS_DEFINITIONS})
list(APPEND vala_define_opts "--define=${def}")
endforeach(def ${ARGS_DEFINITIONS})
set(custom_vapi_arguments "")
if(ARGS_CUSTOM_VAPIS)
foreach(vapi ${ARGS_CUSTOM_VAPIS})
if(${vapi} MATCHES ${CMAKE_SOURCE_DIR} OR ${vapi} MATCHES ${CMAKE_BINARY_DIR})
list(APPEND custom_vapi_arguments ${vapi})
else (${vapi} MATCHES ${CMAKE_SOURCE_DIR} OR ${vapi} MATCHES ${CMAKE_BINARY_DIR})
list(APPEND custom_vapi_arguments ${CMAKE_CURRENT_SOURCE_DIR}/${vapi})
endif(${vapi} MATCHES ${CMAKE_SOURCE_DIR} OR ${vapi} MATCHES ${CMAKE_BINARY_DIR})
endforeach(vapi ${ARGS_CUSTOM_VAPIS})
endif(ARGS_CUSTOM_VAPIS)
set(gresources_args "")
if(ARGS_GRESOURCES)
set(gresources_args --gresources "${ARGS_GRESOURCES}")
endif(ARGS_GRESOURCES)
set(in_files "")
set(fast_vapi_files "")
set(out_files "")
set(out_extra_files "")
set(out_deps_files "")
set(vapi_arguments "")
if(ARGS_GENERATE_VAPI)
list(APPEND out_extra_files "${ARGS_EXPORTS_DIR}/${ARGS_GENERATE_VAPI}.vapi")
list(APPEND out_extra_files "${ARGS_EXPORTS_DIR}/${ARGS_GENERATE_VAPI}_internal.vapi")
set(vapi_arguments "--vapi=${ARGS_EXPORTS_DIR}/${ARGS_GENERATE_VAPI}.vapi" "--internal-vapi=${ARGS_EXPORTS_DIR}/${ARGS_GENERATE_VAPI}_internal.vapi")
if(ARGS_PACKAGES)
string(REPLACE ";" "\\n" pkgs "${ARGS_PACKAGES};${ARGS_CUSTOM_DEPS}")
add_custom_command(OUTPUT "${ARGS_EXPORTS_DIR}/${ARGS_GENERATE_VAPI}.deps" COMMAND echo -e "\"${pkgs}\"" > "${ARGS_EXPORTS_DIR}/${ARGS_GENERATE_VAPI}.deps" COMMENT "Generating ${ARGS_GENERATE_VAPI}.deps")
endif(ARGS_PACKAGES)
endif(ARGS_GENERATE_VAPI)
set(header_arguments "")
if(ARGS_GENERATE_HEADER)
list(APPEND out_extra_files "${ARGS_EXPORTS_DIR}/${ARGS_GENERATE_HEADER}.h")
list(APPEND out_extra_files "${ARGS_EXPORTS_DIR}/${ARGS_GENERATE_HEADER}_internal.h")
list(APPEND header_arguments "--header=${ARGS_EXPORTS_DIR}/${ARGS_GENERATE_HEADER}.h")
list(APPEND header_arguments "--internal-header=${ARGS_EXPORTS_DIR}/${ARGS_GENERATE_HEADER}_internal.h")
endif(ARGS_GENERATE_HEADER)
string(REPLACE " " ";" VALAC_FLAGS ${CMAKE_VALA_FLAGS})
if (VALA_VERSION VERSION_GREATER "0.38")
set(VALAC_COLORS "--color=always")
endif ()
if(ARGS_FAST_VAPI)
foreach(src ${ARGS_SOURCES} ${ARGS_UNPARSED_ARGUMENTS})
set(in_file "${CMAKE_CURRENT_SOURCE_DIR}/${src}")
list(APPEND in_files "${in_file}")
string(REPLACE ".vala" ".c" src ${src})
string(REPLACE ".gs" ".c" src ${src})
string(REPLACE ".c" ".vapi" fast_vapi ${src})
set(fast_vapi_file "${DIRECTORY}/${fast_vapi}")
list(APPEND fast_vapi_files "${fast_vapi_file}")
list(APPEND out_files "${DIRECTORY}/${src}")
_vala_mkdir_for_file("${fast_vapi_file}")
add_custom_command(OUTPUT ${fast_vapi_file}
COMMAND
${VALA_EXECUTABLE}
ARGS
${VALAC_COLORS}
--fast-vapi ${fast_vapi_file}
${vala_define_opts}
${ARGS_OPTIONS}
${VALAC_FLAGS}
${in_file}
DEPENDS
${in_file}
COMMENT
"Generating fast VAPI ${fast_vapi}"
)
endforeach(src ${ARGS_SOURCES} ${ARGS_UNPARSED_ARGUMENTS})
foreach(src ${ARGS_SOURCES} ${ARGS_UNPARSED_ARGUMENTS})
set(in_file "${CMAKE_CURRENT_SOURCE_DIR}/${src}")
string(REPLACE ".vala" ".c" c_code ${src})
string(REPLACE ".gs" ".c" c_code ${c_code})
string(REPLACE ".c" ".vapi" fast_vapi ${c_code})
set(my_fast_vapi_file "${DIRECTORY}/${fast_vapi}")
set(c_code_file "${DIRECTORY}/${c_code}")
set(fast_vapi_flags "")
set(fast_vapi_stamp "")
foreach(fast_vapi_file ${fast_vapi_files})
if(NOT "${fast_vapi_file}" STREQUAL "${my_fast_vapi_file}")
list(APPEND fast_vapi_flags --use-fast-vapi "${fast_vapi_file}")
list(APPEND fast_vapi_stamp "${fast_vapi_file}")
endif()
endforeach(fast_vapi_file)
_vala_mkdir_for_file("${fast_vapi_file}")
get_filename_component(dir "${c_code_file}" DIRECTORY)
add_custom_command(OUTPUT ${c_code_file}
COMMAND
${VALA_EXECUTABLE}
ARGS
${VALAC_COLORS}
"-C"
"-d" ${dir}
${vala_pkg_opts}
${vala_define_opts}
${gresources_args}
${ARGS_OPTIONS}
${VALAC_FLAGS}
${fast_vapi_flags}
${in_file}
${custom_vapi_arguments}
DEPENDS
${fast_vapi_stamp}
${in_file}
${ARGS_CUSTOM_VAPIS}
${ARGS_GRESOURCES}
COMMENT
"Generating C source ${c_code}"
)
endforeach(src)
if(NOT "${out_extra_files}" STREQUAL "")
add_custom_command(OUTPUT ${out_extra_files}
COMMAND
${VALA_EXECUTABLE}
ARGS
${VALAC_COLORS}
-C -q --disable-warnings
${header_arguments}
${vapi_arguments}
"-b" ${CMAKE_CURRENT_SOURCE_DIR}
"-d" ${DIRECTORY}
${vala_pkg_opts}
${vala_define_opts}
${gresources_args}
${ARGS_OPTIONS}
${VALAC_FLAGS}
${in_files}
${custom_vapi_arguments}
DEPENDS
${in_files}
${ARGS_CUSTOM_VAPIS}
${ARGS_GRESOURCES}
COMMENT
"Generating VAPI and headers for target ${output}"
)
endif()
else(ARGS_FAST_VAPI)
foreach(src ${ARGS_SOURCES} ${ARGS_UNPARSED_ARGUMENTS})
set(in_file "${CMAKE_CURRENT_SOURCE_DIR}/${src}")
list(APPEND in_files "${in_file}")
string(REPLACE ".vala" ".c" src ${src})
string(REPLACE ".gs" ".c" src ${src})
list(APPEND out_files "${DIRECTORY}/${src}")
_vala_mkdir_for_file("${fast_vapi_file}")
endforeach(src ${ARGS_SOURCES} ${ARGS_UNPARSED_ARGUMENTS})
add_custom_command(OUTPUT ${out_files} ${out_extra_files}
COMMAND
${VALA_EXECUTABLE}
ARGS
${VALAC_COLORS}
-C
${header_arguments}
${vapi_arguments}
"-b" ${CMAKE_CURRENT_SOURCE_DIR}
"-d" ${DIRECTORY}
${vala_pkg_opts}
${vala_define_opts}
${gresources_args}
${ARGS_OPTIONS}
${VALAC_FLAGS}
${in_files}
${custom_vapi_arguments}
DEPENDS
${in_files}
${ARGS_CUSTOM_VAPIS}
${ARGS_GRESOURCES}
COMMENT
"Generating C code for target ${output}"
)
endif(ARGS_FAST_VAPI)
set(${output} ${out_files} PARENT_SCOPE)
endfunction(vala_precompile)

View File

@ -1,21 +0,0 @@
if(NOT EXISTS "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt")
message(FATAL_ERROR "Cannot find install manifest: @CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt")
endif(NOT EXISTS "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt")
file(READ "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt" files)
string(REGEX REPLACE "\n" ";" files "${files}")
foreach(file ${files})
message(STATUS "Uninstalling: $ENV{DESTDIR}${file}")
if(IS_SYMLINK "$ENV{DESTDIR}${file}" OR EXISTS "$ENV{DESTDIR}${file}")
exec_program(
"@CMAKE_COMMAND@" ARGS "-E remove \"$ENV{DESTDIR}${file}\""
OUTPUT_VARIABLE rm_out
RETURN_VALUE rm_retval
)
if(NOT "${rm_retval}" STREQUAL 0)
message(FATAL_ERROR "Problem when removing $ENV{DESTDIR}${file}")
endif(NOT "${rm_retval}" STREQUAL 0)
else(IS_SYMLINK "$ENV{DESTDIR}${file}" OR EXISTS "$ENV{DESTDIR}${file}")
message(STATUS "File $ENV{DESTDIR}${file} does not exist.")
endif(IS_SYMLINK "$ENV{DESTDIR}${file}" OR EXISTS "$ENV{DESTDIR}${file}")
endforeach(file)

294
configure vendored
View File

@ -1,294 +0,0 @@
#!/bin/sh
OPTS=`getopt -o "h" --long \
help,fetch-only,no-debug,disable-fast-vapi,with-tests,release,with-libsignal-in-tree,\
enable-plugin:,disable-plugin:,\
prefix:,program-prefix:,exec-prefix:,lib-suffix:,\
bindir:,libdir:,includedir:,datadir:,\
host:,build:,\
sbindir:,sysconfdir:,libexecdir:,localstatedir:,sharedstatedir:,mandir:,infodir:,\
-n './configure' -- "$@"`
if [ $? != 0 ] ; then echo "-- Ignoring unrecognized options." >&2 ; fi
eval set -- "$OPTS"
PREFIX=${PREFIX:-/usr/local}
ENABLED_PLUGINS=
DISABLED_PLUGINS=
BUILD_LIBSIGNAL_IN_TREE=
BUILD_TESTS=
BUILD_TYPE=Debug
DISABLE_FAST_VAPI=
LIB_SUFFIX=
NO_DEBUG=
FETCH_ONLY=
EXEC_PREFIX=
BINDIR=
SBINDIR=n
SYSCONFDIR=
DATADIR=
INCLUDEDIR=
LIBDIR=
LIBEXECDIR=
LOCALSTATEDIR=
SHAREDSTATEDIR=
MANDIR=
INFODIR=
help() {
cat << EOF
Usage:
./configure [OPTION]...
Defaults for the options (based on current environment) are specified in
brackets.
Configuration:
-h, --help Print this help and exit
--disable-fast-vapi Disable the usage of Vala compilers fast-vapi
feature. fast-vapi mode is slower when doing
clean builds, but faster when doing incremental
builds (during development).
--fetch-only Only fetch the files required to run ./configure
without network access later and exit.
--no-debug Build without debug symbols
--release Configure to build an optimized release version
--with-libsignal-in-tree Build libsignal-protocol-c in tree and link it
statically.
--with-tests Also build tests.
Plugin configuration:
--enable-plugin=PLUGIN Enable compilation of plugin PLUGIN.
--disable-plugin=PLUGIN Disable compilation of plugin PLUGIN.
Installation directories:
--prefix=PREFIX Install architecture-independent files in PREFIX
[$PREFIX]
--program-prefix=PREFIX Same as --prefix
--exec-prefix= Install architecture-dependent files in EPREFIX
[PREFIX]
--lib-suffix=SUFFIX Append SUFFIX to the directory name for libraries
By default, \`make install' will install all the files in
\`/usr/local/bin', \`/usr/local/lib' etc. You can specify
an installation prefix other than \`/usr/local' using \`--prefix',
for instance \`--prefix=\$HOME'.
For better control, use the options below.
Fine tuning of the installation directories:
--bindir=DIR user executables [EPREFIX/bin]
--libdir=DIR object code libraries [EPREFIX/lib]
--includedir=DIR C header files [PREFIX/include]
--datadir=DIR read-only data [PREFIX/share]
For compatibility with autotools, these options will be silently ignored:
--host, --build, --sbindir, --sysconfdir, --libexecdir, --sharedstatedir,
--localstatedir, --mandir, --infodir
Some influential environment variables:
CC C compiler command
CFLAGS C compiler flags
PKG_CONFIG_PATH directories to add to pkg-config's search path
PKG_CONFIG_LIBDIR path overriding pkg-config's built-in search path
USE_CCACHE decide to use ccache when compiling C objects
VALAC Vala compiler command
VALACFLAGS Vala compiler flags
Use these variables to override the choices made by \`configure' or to help
it to find libraries and programs with nonstandard names/locations.
EOF
}
while true; do
case "$1" in
--prefix ) PREFIX="$2"; shift; shift ;;
--enable-plugin ) if [ -z "$ENABLED_PLUGINS" ]; then ENABLED_PLUGINS="$2"; else ENABLED_PLUGINS="$ENABLED_PLUGINS;$2"; fi; shift; shift ;;
--disable-plugin ) if [ -z "$DISABLED_PLUGINS" ]; then DISABLED_PLUGINS="$2"; else DISABLED_PLUGINS="$DISABLED_PLUGINS;$2"; fi; shift; shift ;;
--valac ) VALA_EXECUTABLE="$2"; shift; shift ;;
--valac-flags ) VALAC_FLAGS="$2"; shift; shift ;;
--lib-suffix ) LIB_SUFFIX="$2"; shift; shift ;;
--with-libsignal-in-tree ) BUILD_LIBSIGNAL_IN_TREE=yes; shift ;;
--disable-fast-vapi ) DISABLE_FAST_VAPI=yes; shift ;;
--no-debug ) NO_DEBUG=yes; shift ;;
--fetch-only ) FETCH_ONLY=yes; shift ;;
--release ) BUILD_TYPE=RelWithDebInfo; shift ;;
--with-tests ) BUILD_TESTS=yes; shift ;;
# Autotools paths
--program-prefix ) PREFIX="$2"; shift; shift ;;
--exec-prefix ) EXEC_PREFIX="$2"; shift; shift ;;
--bindir ) BINDIR="$2"; shift; shift ;;
--datadir ) DATADIR="$2"; shift; shift ;;
--includedir ) INCLUDEDIR="$2"; shift; shift ;;
--libdir ) LIBDIR="$2"; shift; shift ;;
# Autotools paths not used
--sbindir ) SBINDIR="$2"; shift; shift ;;
--sysconfdir ) SYSCONFDIR="$2"; shift; shift ;;
--libexecdir ) LIBEXECDIR="$2"; shift; shift ;;
--localstatedir ) LOCALSTATEDIR="$2"; shift; shift ;;
--sharedstatedir ) SHAREDSTATEDIR="$2"; shift; shift ;;
--mandir ) MANDIR="$2"; shift; shift ;;
--infodir ) INFODIR="$2"; shift; shift ;;
--host | --build ) shift; shift ;;
-h | --help ) help; exit 0 ;;
-- ) shift; break ;;
* ) break ;;
esac
done
if [ "$BUILD_LIBSIGNAL_IN_TREE" = "yes" ] || [ "$FETCH_ONLY" = "yes" ]; then
if [ -d ".git" ]; then
git submodule update --init 2>/dev/null
else
tmp=0
for i in $(cat .gitmodules | grep -n submodule | awk -F ':' '{print $1}') $(wc -l .gitmodules | awk '{print $1}'); do
if ! [ $tmp -eq 0 ]; then
name=$(cat .gitmodules | head -n $tmp | tail -n 1 | awk -F '"' '{print $2}')
def=$(cat .gitmodules | head -n $i | tail -n $(expr "$i" - "$tmp") | awk -F ' ' '{print $1 $2 $3}')
path=$(echo "$def" | grep '^path=' | awk -F '=' '{print $2}')
url=$(echo "$def" | grep '^url=' | awk -F '=' '{print $2}')
branch=$(echo "$def" | grep '^branch=' | awk -F '=' '{print $2}')
if ! ls "$path"/* >/dev/null 2>/dev/null; then
git=$(which git)
if ! [ $? -eq 0 ] || ! [ -x $git ]; then
echo "Failed retrieving missing files"
exit 5
fi
res=$(git clone "$url" "$path" 2>&1)
if ! [ $? -eq 0 ] || ! [ -d $path ]; then
echo "Failed retrieving missing files: $res"
exit 5
fi
if [ -n "$branch" ]; then
olddir="$(pwd)"
cd "$path"
res=$(git checkout "$branch" 2>&1)
if ! [ $? -eq 0 ]; then
echo "Failed retrieving missing files: $res"
exit 5
fi
cd "$olddir"
fi
echo "Submodule path '$path': checked out '$branch' (via git clone)"
fi
fi
tmp=$i
done
fi
fi
if [ "$FETCH_ONLY" = "yes" ]; then exit 0; fi
if [ ! -x "$(which cmake 2>/dev/null)" ]
then
echo "-!- CMake required."
exit 1
fi
ninja_bin="$(which ninja-build 2>/dev/null)"
if ! [ -x "$ninja_bin" ]; then
ninja_bin="$(which ninja 2>/dev/null)"
fi
if [ -x "$ninja_bin" ]; then
ninja_version=`$ninja_bin --version 2>/dev/null`
if [ $? -eq 0 ]; then
if [ -d build ]; then
last_ninja_version=`cat build/.ninja_version 2>/dev/null`
else
last_ninja_version=0
fi
if [ "$ninja_version" != "$last_ninja_version" ]; then
echo "-- Found Ninja: $ninja_bin (found version \"$ninja_version\")"
fi
cmake_type="Ninja"
exec_bin="$ninja_bin"
exec_command="$exec_bin"
elif [ "/usr/sbin/ninja" = "$ninja_bin" ]; then
echo "-- Ninja at $ninja_bin is not usable. Did you install 'ninja' instead of 'ninja-build'?"
fi
fi
if ! [ -x "$exec_bin" ]; then
make_bin="$(which make 2>/dev/null)"
if [ -x "$make_bin" ]; then
echo "-- Found Make: $make_bin"
cmake_type="Unix Makefiles"
exec_bin="$make_bin"
exec_command="$exec_bin"
echo "-- Running with make. Using Ninja (ninja-build) might improve build experience."
fi
fi
if ! [ -x "$exec_bin" ]; then
echo "-!- No compatible build system (Ninja, Make) found."
exit 4
fi
if [ -f ./build ]; then
echo "-!- ./build file exists. ./configure can't continue"
exit 2
fi
if [ -d build ]; then
last_type=`cat build/.cmake_type`
if [ "$cmake_type" != "$last_type" ]
then
echo "-- Using different build system, cleaning build system files"
cd build
rm -r CMakeCache.txt CMakeFiles
cd ..
fi
fi
mkdir -p build
cd build
echo "$cmake_type" > .cmake_type
echo "$ninja_version" > .ninja_version
cmake -G "$cmake_type" \
-DCMAKE_INSTALL_PREFIX="$PREFIX" \
-DCMAKE_BUILD_TYPE="$BUILD_TYPE" \
-DENABLED_PLUGINS="$ENABLED_PLUGINS" \
-DDISABLED_PLUGINS="$DISABLED_PLUGINS" \
-DBUILD_TESTS="$BUILD_TESTS" \
-DBUILD_LIBSIGNAL_IN_TREE="$BUILD_LIBSIGNAL_IN_TREE" \
-DVALA_EXECUTABLE="$VALAC" \
-DCMAKE_VALA_FLAGS="$VALACFLAGS" \
-DDISABLE_FAST_VAPI="$DISABLE_FAST_VAPI" \
-DLIB_SUFFIX="$LIB_SUFFIX" \
-DNO_DEBUG="$NO_DEBUG" \
-DEXEC_INSTALL_PREFIX="$EXEC_PREFIX" \
-DSHARE_INSTALL_PREFIX="$DATADIR" \
-DBIN_INSTALL_DIR="$BINDIR" \
-DINCLUDE_INSTALL_DIR="$INCLUDEDIR" \
-DLIB_INSTALL_DIR="$LIBDIR" \
-Wno-dev \
.. || exit 9
if [ "$cmake_type" = "Ninja" ]; then
cat << EOF > Makefile
default:
@sh -c "$exec_command"
%:
@sh -c "$exec_command \"\$@\""
EOF
fi
cd ..
cat << EOF > Makefile
default:
@sh -c "cd build; $exec_command"
distclean: clean uninstall
test: default
echo "make test not yet supported"
%:
@sh -c "cd build; $exec_command \"\$@\""
EOF
echo "-- Configured. Type 'make' to build, 'make install' to install."

View File

@ -1,41 +0,0 @@
find_package(GCrypt REQUIRED)
find_package(Srtp2 REQUIRED)
find_packages(CRYPTO_VALA_PACKAGES REQUIRED
GLib
GObject
GIO
)
vala_precompile(CRYPTO_VALA_C
SOURCES
"src/cipher.vala"
"src/cipher_converter.vala"
"src/error.vala"
"src/random.vala"
"src/srtp.vala"
CUSTOM_VAPIS
"${CMAKE_CURRENT_SOURCE_DIR}/vapi/gcrypt.vapi"
"${CMAKE_CURRENT_SOURCE_DIR}/vapi/libsrtp2.vapi"
PACKAGES
${CRYPTO_VALA_PACKAGES}
GENERATE_VAPI
crypto-vala
GENERATE_HEADER
crypto-vala
)
add_custom_target(crypto-vala-vapi
DEPENDS
${CMAKE_BINARY_DIR}/exports/crypto-vala.vapi
${CMAKE_BINARY_DIR}/exports/crypto-vala.deps
)
add_definitions(${VALA_CFLAGS} -DG_LOG_DOMAIN="crypto-vala")
add_library(crypto-vala SHARED ${CRYPTO_VALA_C})
add_dependencies(crypto-vala crypto-vala-vapi)
target_link_libraries(crypto-vala ${CRYPTO_VALA_PACKAGES} gcrypt libsrtp2)
set_target_properties(crypto-vala PROPERTIES VERSION 0.0 SOVERSION 0)
install(TARGETS crypto-vala ${TARGET_INSTALL})
install(FILES ${CMAKE_BINARY_DIR}/exports/crypto-vala.vapi ${CMAKE_BINARY_DIR}/exports/crypto-vala.deps DESTINATION ${VAPI_INSTALL_DIR})
install(FILES ${CMAKE_BINARY_DIR}/exports/crypto-vala.h DESTINATION ${INCLUDE_INSTALL_DIR})

View File

@ -0,0 +1,2 @@
gio-2.0
glib-2.0

23
crypto-vala/meson.build Normal file
View File

@ -0,0 +1,23 @@
dependencies = [
dep_gio,
dep_glib,
dep_libgcrypt,
dep_libsrtp2,
]
sources = files(
'src/cipher.vala',
'src/cipher_converter.vala',
'src/error.vala',
'src/random.vala',
'src/srtp.vala',
)
c_args = [
'-DG_LOG_DOMAIN="crypto-vala"',
]
vala_args = [
'--vapidir', meson.current_source_dir() / 'vapi',
]
lib_crypto_vala = library('crypto-vala', sources, c_args: c_args, vala_args: vala_args, dependencies: dependencies, version: '0.0', install: true, install_dir: [true, true, true], install_rpath: default_install_rpath)
dep_crypto_vala = declare_dependency(link_with: lib_crypto_vala, include_directories: include_directories('.'))
install_data('crypto-vala.deps', install_dir: get_option('datadir') / 'vala/vapi', install_tag: 'devel') # TODO: workaround for https://github.com/mesonbuild/meson/issues/9756

View File

@ -55,7 +55,7 @@ public class SymmetricCipherEncrypter : SymmetricCipherConverter {
} }
return ConverterResult.CONVERTED; return ConverterResult.CONVERTED;
} catch (Crypto.Error e) { } catch (Crypto.Error e) {
throw new IOError.FAILED(@"$(e.domain) error while decrypting: $(e.message)"); throw new IOError.FAILED(@"$(e.domain) error while encrypting: $(e.message)");
} }
} }
} }

274
dino.doap
View File

@ -3,7 +3,7 @@
<Project> <Project>
<name>Dino</name> <name>Dino</name>
<short-name>dino</short-name> <short-name>dino</short-name>
<shortdesc xml:lang="en">Modern XMPP Chat Client</shortdesc> <shortdesc xml:lang="en">Modern XMPP chat client</shortdesc>
<shortdesc xml:lang="zh-TW">現代化的 XMPP 用戶端聊天軟件</shortdesc> <shortdesc xml:lang="zh-TW">現代化的 XMPP 用戶端聊天軟件</shortdesc>
<shortdesc xml:lang="zh-CN">现代 XMPP 聊天客户端</shortdesc> <shortdesc xml:lang="zh-CN">现代 XMPP 聊天客户端</shortdesc>
<shortdesc xml:lang="tr">Modern XMPP Sohbet İstemcisi</shortdesc> <shortdesc xml:lang="tr">Modern XMPP Sohbet İstemcisi</shortdesc>
@ -227,40 +227,51 @@
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0004.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0004.html"/>
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0027.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0027.html"/>
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0030.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0030.html"/>
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0045.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0045.html"/>
<xmpp:status>partial</xmpp:status> <xmpp:status>partial</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0047.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0047.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:note>For use with XEP-0261</xmpp:note>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0048.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0048.html"/>
<xmpp:status>deprecated</xmpp:status>
<xmpp:note>Migrating to XEP-0402 if supported by server</xmpp:note>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0049.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0049.html"/>
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
@ -268,119 +279,179 @@
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0054.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0054.html"/>
<xmpp:status>partial</xmpp:status> <xmpp:status>partial</xmpp:status>
<xmpp:note>Only for viewing avatars</xmpp:note> <xmpp:note>Only for viewing avatars</xmpp:note>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0059.html"/>
<xmpp:status>partial</xmpp:status>
<xmpp:note>For use with XEP-0313</xmpp:note>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0060.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0060.html"/>
<xmpp:status>partial</xmpp:status> <xmpp:status>partial</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0065.html"/>
<xmpp:status>partial</xmpp:status>
<xmpp:note>For use with XEP-0260</xmpp:note>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0066.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0066.html"/>
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:note>For file transfers using XEP-0363</xmpp:note>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0077.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0077.html"/>
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0082.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0082.html"/>
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0084.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0084.html"/>
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0085.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0085.html"/>
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0115.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0115.html"/>
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0153.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0153.html"/>
<xmpp:status>deprecated</xmpp:status>
<xmpp:since>0.1</xmpp:since>
<xmpp:note>Only to fetch Avatars from other users</xmpp:note>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0163.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0163.html"/>
<xmpp:status>partial</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0166.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0166.html"/>
<xmpp:status>partial</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0167.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0167.html"/>
<xmpp:status>partial</xmpp:status>
<xmpp:since>0.3</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0176.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0176.html"/>
<xmpp:status>partial</xmpp:status>
<xmpp:since>0.3</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0177.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:since>0.3</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0184.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0184.html"/>
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0191.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0191.html"/>
<xmpp:status>partial</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0198.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0198.html"/>
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0199.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0199.html"/>
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0203.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0203.html"/>
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0215.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0215.html"/>
<xmpp:status>partial</xmpp:status>
<xmpp:since>0.3</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0222.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0222.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0223.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0234.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0234.html"/>
<xmpp:status>partial</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
@ -388,43 +459,84 @@
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0245.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0245.html"/>
<xmpp:version>1.0</xmpp:version> <xmpp:version>1.0</xmpp:version>
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0249.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0249.html"/>
<xmpp:status>partial</xmpp:status>
<xmpp:note>No support for sending</xmpp:note>
<xmpp:since>0.3</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0260.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0260.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.0.3</xmpp:version>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0261.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0261.html"/>
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:version>1.0</xmpp:version>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0272.html"/>
<xmpp:status>partial</xmpp:status>
<xmpp:version>0.2.0</xmpp:version>
<xmpp:since>0.3</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0280.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0280.html"/>
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:version>1.0.1</xmpp:version>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0293.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0293.html"/>
<xmpp:status>partial</xmpp:status>
<xmpp:version>1.0.2</xmpp:version>
<xmpp:since>0.3</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0294.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0294.html"/>
<xmpp:status>partial</xmpp:status>
<xmpp:version>1.1.2</xmpp:version>
<xmpp:since>0.3</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0297.html"/>
<xmpp:status>partial</xmpp:status>
<xmpp:version>1.0</xmpp:version>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0298.html"/>
<xmpp:status>partial</xmpp:status>
<xmpp:since>0.3</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0308.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0308.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.2.0</xmpp:version> <xmpp:version>1.2.0</xmpp:version>
<xmpp:since>0.2</xmpp:since> <xmpp:since>0.2</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
@ -433,45 +545,67 @@
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0313.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0313.html"/>
<xmpp:status>partial</xmpp:status> <xmpp:status>partial</xmpp:status>
<xmpp:note>Not for MUCs</xmpp:note> <xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0320.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0320.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.0.0</xmpp:version>
<xmpp:since>0.3</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0333.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0333.html"/>
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0334.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0334.html"/>
<xmpp:status>partial</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0353.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0353.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.3.1</xmpp:version>
<xmpp:since>0.3</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0359.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0359.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0363.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0363.html"/>
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0367.html"/>
<xmpp:status>partial</xmpp:status>
<xmpp:version>0.3</xmpp:version>
<xmpp:since>0.5</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0368.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0368.html"/>
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:version>1.1.0</xmpp:version>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
@ -479,28 +613,160 @@
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0380.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0380.html"/>
<xmpp:status>partial</xmpp:status> <xmpp:status>partial</xmpp:status>
<xmpp:note>Only for outgoing messages</xmpp:note> <xmpp:note>Only for outgoing messages</xmpp:note>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0384.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0384.html"/>
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:version>0.3.0</xmpp:version>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0391.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0391.html"/>
<xmpp:status>partial</xmpp:status>
<xmpp:version>0.1.2</xmpp:version>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0392.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.0.0</xmpp:version>
<xmpp:since>0.5</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0393.html"/>
<xmpp:status>partial</xmpp:status>
<xmpp:version>1.1.1</xmpp:version>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0394.html"/>
<xmpp:status>partial</xmpp:status>
<xmpp:version>0.3.0</xmpp:version>
<xmpp:since>0.5</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0396.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0398.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0398.html"/>
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0402.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.2.0</xmpp:version>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0410.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0410.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:since>0.2</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0421.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.2.0</xmpp:version>
<xmpp:since>0.4</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0426.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.3.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0428.html"/>
<xmpp:status>partial</xmpp:status>
<xmpp:version>0.2.1</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0444.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.1.1</xmpp:version>
<xmpp:since>0.4</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0446.html"/>
<xmpp:status>partial</xmpp:status>
<xmpp:version>0.2.0</xmpp:version>
<xmpp:since>0.5</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0447.html"/>
<xmpp:status>partial</xmpp:status>
<xmpp:version>0.3.0</xmpp:version>
<xmpp:since>0.5</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0453.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.1.2</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0454.html"/>
<xmpp:status>partial</xmpp:status>
<xmpp:note>No support for embedded thumbnails</xmpp:note>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0461.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.2.0</xmpp:version>
<xmpp:since>0.4</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0482.html"/>
<xmpp:status>partial</xmpp:status>
<xmpp:version>0.1.0</xmpp:version>
<xmpp:since>0.5</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0486.html"/>
<xmpp:status>partial</xmpp:status>
<xmpp:version>0.1.0</xmpp:version>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
</Project> </Project>

View File

@ -9,7 +9,7 @@
<name>Dino</name> <name>Dino</name>
<short-name>dino</short-name> <short-name>dino</short-name>
<shortdesc xml:lang="en">Modern XMPP Chat Client</shortdesc> <shortdesc xml:lang="en">Modern XMPP chat client</shortdesc>
<description xml:lang="en"> <description xml:lang="en">
Dino is a modern open-source chat client for the desktop. It focuses on providing a clean and reliable Jabber/XMPP experience while having your privacy in mind. Dino is a modern open-source chat client for the desktop. It focuses on providing a clean and reliable Jabber/XMPP experience while having your privacy in mind.
It supports end-to-end encryption with OMEMO and OpenPGP and allows configuring privacy-related features such as read receipts and typing notifications. It supports end-to-end encryption with OMEMO and OpenPGP and allows configuring privacy-related features such as read receipts and typing notifications.
@ -47,40 +47,51 @@
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0004.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0004.html" />
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0027.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0027.html" />
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0030.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0030.html" />
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0045.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0045.html" />
<xmpp:status>partial</xmpp:status> <xmpp:status>partial</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0047.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0047.html" />
<xmpp:status>complete</xmpp:status>
<xmpp:note>For use with XEP-0261</xmpp:note>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0048.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0048.html" />
<xmpp:status>deprecated</xmpp:status>
<xmpp:note>Migrating to XEP-0402 if supported by server</xmpp:note>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0049.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0049.html" />
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
@ -88,119 +99,179 @@
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0054.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0054.html" />
<xmpp:status>partial</xmpp:status> <xmpp:status>partial</xmpp:status>
<xmpp:note>Only for viewing avatars</xmpp:note> <xmpp:note>Only for viewing avatars</xmpp:note>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0059.html" />
<xmpp:status>partial</xmpp:status>
<xmpp:note>For use with XEP-0313</xmpp:note>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0060.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0060.html" />
<xmpp:status>partial</xmpp:status> <xmpp:status>partial</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0065.html" />
<xmpp:status>partial</xmpp:status>
<xmpp:note>For use with XEP-0260</xmpp:note>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0066.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0066.html" />
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:note>For file transfers using XEP-0363</xmpp:note>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0077.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0077.html" />
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0082.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0082.html" />
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0084.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0084.html" />
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0085.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0085.html" />
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0115.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0115.html" />
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0153.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0153.html" />
<xmpp:status>deprecated</xmpp:status>
<xmpp:since>0.1</xmpp:since>
<xmpp:note>Only to fetch Avatars from other users</xmpp:note>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0163.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0163.html" />
<xmpp:status>partial</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0166.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0166.html" />
<xmpp:status>partial</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0167.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0167.html" />
<xmpp:status>partial</xmpp:status>
<xmpp:since>0.3</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0176.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0176.html" />
<xmpp:status>partial</xmpp:status>
<xmpp:since>0.3</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0177.html" />
<xmpp:status>complete</xmpp:status>
<xmpp:since>0.3</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0184.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0184.html" />
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0191.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0191.html" />
<xmpp:status>partial</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0198.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0198.html" />
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0199.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0199.html" />
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0203.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0203.html" />
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0215.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0215.html" />
<xmpp:status>partial</xmpp:status>
<xmpp:since>0.3</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0222.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0222.html" />
<xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0223.html" />
<xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0234.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0234.html" />
<xmpp:status>partial</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
@ -208,43 +279,84 @@
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0245.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0245.html" />
<xmpp:version>1.0</xmpp:version> <xmpp:version>1.0</xmpp:version>
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0249.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0249.html" />
<xmpp:status>partial</xmpp:status>
<xmpp:note>No support for sending</xmpp:note>
<xmpp:since>0.3</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0260.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0260.html" />
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.0.3</xmpp:version>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0261.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0261.html" />
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:version>1.0</xmpp:version>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0272.html" />
<xmpp:status>partial</xmpp:status>
<xmpp:version>0.2.0</xmpp:version>
<xmpp:since>0.3</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0280.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0280.html" />
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:version>1.0.1</xmpp:version>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0293.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0293.html" />
<xmpp:status>partial</xmpp:status>
<xmpp:version>1.0.2</xmpp:version>
<xmpp:since>0.3</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0294.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0294.html" />
<xmpp:status>partial</xmpp:status>
<xmpp:version>1.1.2</xmpp:version>
<xmpp:since>0.3</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0297.html" />
<xmpp:status>partial</xmpp:status>
<xmpp:version>1.0</xmpp:version>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0298.html" />
<xmpp:status>partial</xmpp:status>
<xmpp:since>0.3</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0308.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0308.html" />
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.2.0</xmpp:version> <xmpp:version>1.2.0</xmpp:version>
<xmpp:since>0.2</xmpp:since> <xmpp:since>0.2</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
@ -253,45 +365,67 @@
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0313.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0313.html" />
<xmpp:status>partial</xmpp:status> <xmpp:status>partial</xmpp:status>
<xmpp:note>Not for MUCs</xmpp:note> <xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0320.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0320.html" />
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.0.0</xmpp:version>
<xmpp:since>0.3</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0333.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0333.html" />
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0334.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0334.html" />
<xmpp:status>partial</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0353.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0353.html" />
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.3.1</xmpp:version>
<xmpp:since>0.3</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0359.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0359.html" />
<xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0363.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0363.html" />
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0367.html" />
<xmpp:status>partial</xmpp:status>
<xmpp:version>0.3</xmpp:version>
<xmpp:since>0.5</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0368.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0368.html" />
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:version>1.1.0</xmpp:version>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
@ -299,28 +433,160 @@
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0380.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0380.html" />
<xmpp:status>partial</xmpp:status> <xmpp:status>partial</xmpp:status>
<xmpp:note>Only for outgoing messages</xmpp:note> <xmpp:note>Only for outgoing messages</xmpp:note>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0384.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0384.html" />
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:version>0.3.0</xmpp:version>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0391.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0391.html" />
<xmpp:status>partial</xmpp:status>
<xmpp:version>0.1.2</xmpp:version>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0392.html" />
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.0.0</xmpp:version>
<xmpp:since>0.5</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0393.html" />
<xmpp:status>partial</xmpp:status>
<xmpp:version>1.1.1</xmpp:version>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0394.html" />
<xmpp:status>partial</xmpp:status>
<xmpp:version>0.3.0</xmpp:version>
<xmpp:since>0.5</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0396.html" />
<xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0398.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0398.html" />
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0402.html" />
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.2.0</xmpp:version>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0410.html" /> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0410.html" />
<xmpp:status>complete</xmpp:status>
<xmpp:since>0.2</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0421.html" />
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.2.0</xmpp:version>
<xmpp:since>0.4</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0426.html" />
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.3.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0428.html" />
<xmpp:status>partial</xmpp:status>
<xmpp:version>0.2.1</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0444.html" />
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.1.1</xmpp:version>
<xmpp:since>0.4</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0446.html" />
<xmpp:status>partial</xmpp:status>
<xmpp:version>0.2.0</xmpp:version>
<xmpp:since>0.5</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0447.html" />
<xmpp:status>partial</xmpp:status>
<xmpp:version>0.3.0</xmpp:version>
<xmpp:since>0.5</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0453.html" />
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.1.2</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0454.html" />
<xmpp:status>partial</xmpp:status>
<xmpp:note>No support for embedded thumbnails</xmpp:note>
<xmpp:since>0.1</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0461.html" />
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.2.0</xmpp:version>
<xmpp:since>0.4</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0482.html" />
<xmpp:status>partial</xmpp:status>
<xmpp:version>0.1.0</xmpp:version>
<xmpp:since>0.5</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0486.html" />
<xmpp:status>partial</xmpp:status>
<xmpp:version>0.1.0</xmpp:version>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
</Project> </Project>

133
im.dino.Dino.json Normal file
View File

@ -0,0 +1,133 @@
{
"id": "im.dino.Dino",
"runtime": "org.gnome.Platform",
"runtime-version": "48",
"sdk": "org.gnome.Sdk",
"command": "dino",
"finish-args": [
"--share=ipc",
"--socket=fallback-x11",
"--socket=wayland",
"--socket=pulseaudio",
"--socket=gpg-agent",
"--filesystem=xdg-run/pipewire-0",
"--share=network",
"--device=dri",
"--talk-name=org.freedesktop.Notifications"
],
"modules": [
{
"name": "protobuf",
"buildsystem": "cmake-ninja",
"cleanup": [
"*"
],
"config-opts": [
"-Dprotobuf_BUILD_TESTS=OFF",
"-Dprotobuf_BUILD_LIBUPB=OFF"
],
"sources": [
{
"type": "archive",
"url": "https://github.com/protocolbuffers/protobuf/releases/download/v30.2/protobuf-30.2.tar.gz",
"sha512": "555d1b18d175eeaf17f3879f124d33080f490367840d35b34bfc4e4a5b383bf6a1d09f1570acb6af9c53ac4940a14572d46423b6e3dd0c712e7802c986fb6be6",
"x-checker-data": {
"type": "anitya",
"project-id": 3715,
"stable-only": true,
"url-template": "https://github.com/protocolbuffers/protobuf/releases/download/v$version/protobuf-$version.tar.gz"
}
}
]
},
{
"name": "libprotobuf-c",
"buildsystem": "autotools",
"config-opts": [
"CFLAGS=-fPIC"
],
"post-install": [
"rm /app/lib/*.so"
],
"cleanup": [
"*"
],
"sources": [
{
"type": "archive",
"url": "https://github.com/protobuf-c/protobuf-c/releases/download/v1.5.2/protobuf-c-1.5.2.tar.gz",
"sha512": "78dc72988d7e8232c1b967849aa00939bc05ab7d39b86a8e2af005e38aa4ef4c9b03920d51fb5337399d980e65f35d11bd4742bea745a893ecc909f56a51c9ac",
"x-checker-data": {
"type": "anitya",
"project-id": 3716,
"stable-only": true,
"url-template": "https://github.com/protobuf-c/protobuf-c/releases/download/v$version/protobuf-c-$version.tar.gz"
}
}
]
},
{
"name": "libomemo-c",
"buildsystem": "meson",
"cleanup": [
"/lib/pkgconfig",
"/include"
],
"config-opts": [
"-Dtests=false",
"-Ddefault_library=static"
],
"sources": [
{
"type": "archive",
"url": "https://github.com/dino/libomemo-c/releases/download/v0.5.1/libomemo-c-0.5.1.tar.gz",
"sha512": "ff59565406c51663f2944e9a7c12c5b0e3fa01073039f5161472dd81f59194b1cf2685bc1e0cc930a141bc409b965c5d93313cfc3e0e237250102af3b5e88826",
"x-checker-data": {
"type": "anitya",
"project-id": 359676,
"stable-only": true,
"url-template": "https://github.com/dino/libomemo-c/releases/download/v$version/libomemo-c-$version.tar.gz"
}
}
]
},
{
"name": "qrencode",
"buildsystem": "cmake-ninja",
"cleanup": [
"*"
],
"config-opts": [
"-DCMAKE_C_FLAGS=-fPIC"
],
"sources": [
{
"type": "archive",
"url": "https://github.com/fukuchi/libqrencode/archive/refs/tags/v4.1.1.tar.gz",
"sha512": "584106e7bcaaa1ef2efe63d653daad38d4ff436eb4b185a1db3c747169c1ffa74149c3b1329bb0b8ae007903db0a7034aabf135cc196d91a37b5c61348154a65",
"x-checker-data": {
"type": "anitya",
"project-id": 12834,
"stable-only": true,
"url-template": "https://github.com/fukuchi/libqrencode/archive/refs/tags/v$version.tar.gz"
}
}
]
},
{
"name": "dino",
"buildsystem": "meson",
"builddir": true,
"cleanup": [
"/include",
"/share/vala"
],
"sources": [
{
"type": "dir",
"path": "."
}
]
}
]
}

View File

@ -1,121 +0,0 @@
find_packages(LIBDINO_PACKAGES REQUIRED
GDKPixbuf2
Gee
GLib
GModule
GObject
)
vala_precompile(LIBDINO_VALA_C
SOURCES
src/application.vala
src/dbus/login1.vala
src/dbus/notifications.vala
src/dbus/upower.vala
src/entity/account.vala
src/entity/call.vala
src/entity/conversation.vala
src/entity/encryption.vala
src/entity/file_transfer.vala
src/entity/message.vala
src/entity/settings.vala
src/plugin/interfaces.vala
src/plugin/loader.vala
src/plugin/registry.vala
src/service/avatar_manager.vala
src/service/blocking_manager.vala
src/service/call_store.vala
src/service/call_state.vala
src/service/call_peer_state.vala
src/service/calls.vala
src/service/chat_interaction.vala
src/service/connection_manager.vala
src/service/content_item_store.vala
src/service/conversation_manager.vala
src/service/counterpart_interaction_manager.vala
src/service/database.vala
src/service/entity_capabilities_storage.vala
src/service/entity_info.vala
src/service/file_manager.vala
src/service/file_transfer_storage.vala
src/service/jingle_file_transfers.vala
src/service/message_correction.vala
src/service/message_processor.vala
src/service/message_storage.vala
src/service/module_manager.vala
src/service/muc_manager.vala
src/service/notification_events.vala
src/service/presence_manager.vala
src/service/registration.vala
src/service/roster_manager.vala
src/service/search_processor.vala
src/service/stream_interactor.vala
src/service/util.vala
src/util/display_name.vala
src/util/util.vala
src/util/weak_map.vala
CUSTOM_VAPIS
"${CMAKE_BINARY_DIR}/exports/xmpp-vala.vapi"
"${CMAKE_BINARY_DIR}/exports/qlite.vapi"
CUSTOM_DEPS
xmpp-vala
qlite
PACKAGES
${LIBDINO_PACKAGES}
GENERATE_VAPI
dino
GENERATE_HEADER
dino
)
add_custom_command(OUTPUT "${CMAKE_BINARY_DIR}/exports/dino_i18n.h"
COMMAND
cp "${CMAKE_CURRENT_SOURCE_DIR}/src/dino_i18n.h" "${CMAKE_BINARY_DIR}/exports/dino_i18n.h"
DEPENDS
"${CMAKE_CURRENT_SOURCE_DIR}/src/dino_i18n.h"
COMMENT
Copy header file dino_i18n.h
)
add_custom_target(dino-vapi
DEPENDS
${CMAKE_BINARY_DIR}/exports/dino.vapi
${CMAKE_BINARY_DIR}/exports/dino.deps
${CMAKE_BINARY_DIR}/exports/dino_i18n.h
)
add_definitions(${VALA_CFLAGS} -DDINO_SYSTEM_PLUGIN_DIR="${PLUGIN_INSTALL_DIR}" -DDINO_SYSTEM_LIBDIR_NAME="${LIBDIR_NAME}" -DG_LOG_DOMAIN="libdino")
add_library(libdino SHARED ${LIBDINO_VALA_C} ${CMAKE_BINARY_DIR}/exports/dino_i18n.h)
add_dependencies(libdino dino-vapi)
target_link_libraries(libdino xmpp-vala qlite ${LIBDINO_PACKAGES} m)
set_target_properties(libdino PROPERTIES PREFIX "" VERSION 0.0 SOVERSION 0)
install(TARGETS libdino ${TARGET_INSTALL})
install(FILES ${CMAKE_BINARY_DIR}/exports/dino.vapi ${CMAKE_BINARY_DIR}/exports/dino.deps DESTINATION ${VAPI_INSTALL_DIR})
install(FILES ${CMAKE_BINARY_DIR}/exports/dino.h ${CMAKE_BINARY_DIR}/exports/dino_i18n.h DESTINATION ${INCLUDE_INSTALL_DIR})
if(BUILD_TESTS)
vala_precompile(LIBDINO_TEST_VALA_C
SOURCES
"tests/weak_map.vala"
"tests/testcase.vala"
"tests/common.vala"
CUSTOM_VAPIS
${CMAKE_BINARY_DIR}/exports/dino_internal.vapi
${CMAKE_BINARY_DIR}/exports/xmpp-vala.vapi
${CMAKE_BINARY_DIR}/exports/qlite.vapi
PACKAGES
${LIBDINO_PACKAGES}
OPTIONS
${LIBDINO_EXTRA_OPTIONS}
)
add_definitions(${VALA_CFLAGS})
add_executable(libdino-test ${LIBDINO_TEST_VALA_C})
target_link_libraries(libdino-test libdino)
endif(BUILD_TESTS)

6
libdino/dino.deps Normal file
View File

@ -0,0 +1,6 @@
gdk-pixbuf-2.0
gee-0.8
glib-2.0
gmodule-2.0
qlite
xmpp-vala

92
libdino/meson.build Normal file
View File

@ -0,0 +1,92 @@
# version_vala
dot_git = meson.current_source_dir() / '../.git'
version_file = meson.current_source_dir() / '../VERSION'
command = [prog_python, files('version.py'), version_file, '--git-repo', meson.current_source_dir()]
if prog_git.found()
command += ['--git', prog_git]
endif
version_vala = vcs_tag(command: command, input: 'src/version.vala.in', output: 'version.vala', replace_string: '%VERSION%')
# libdino
dependencies = [
dep_gdk_pixbuf,
dep_gee,
dep_gio,
dep_glib,
dep_gmodule,
dep_qlite,
dep_xmpp_vala
]
sources = files(
'src/application.vala',
'src/dbus/login1.vala',
'src/dbus/notifications.vala',
'src/dbus/upower.vala',
'src/entity/account.vala',
'src/entity/call.vala',
'src/entity/conversation.vala',
'src/entity/encryption.vala',
'src/entity/file_transfer.vala',
'src/entity/message.vala',
'src/entity/settings.vala',
'src/plugin/interfaces.vala',
'src/plugin/loader.vala',
'src/plugin/registry.vala',
'src/service/avatar_manager.vala',
'src/service/blocking_manager.vala',
'src/service/call_store.vala',
'src/service/call_state.vala',
'src/service/call_peer_state.vala',
'src/service/calls.vala',
'src/service/chat_interaction.vala',
'src/service/connection_manager.vala',
'src/service/contact_model.vala',
'src/service/content_item_store.vala',
'src/service/conversation_manager.vala',
'src/service/counterpart_interaction_manager.vala',
'src/service/database.vala',
'src/service/entity_capabilities_storage.vala',
'src/service/entity_info.vala',
'src/service/fallback_body.vala',
'src/service/file_manager.vala',
'src/service/file_transfer_storage.vala',
'src/service/history_sync.vala',
'src/service/jingle_file_transfers.vala',
'src/service/message_correction.vala',
'src/service/message_processor.vala',
'src/service/message_storage.vala',
'src/service/module_manager.vala',
'src/service/muc_manager.vala',
'src/service/notification_events.vala',
'src/service/presence_manager.vala',
'src/service/replies.vala',
'src/service/reactions.vala',
'src/service/registration.vala',
'src/service/roster_manager.vala',
'src/service/search_processor.vala',
'src/service/sfs_metadata.vala',
'src/service/stateless_file_sharing.vala',
'src/service/stream_interactor.vala',
'src/service/util.vala',
'src/util/display_name.vala',
'src/util/limit_input_stream.vala',
'src/util/send_message.vala',
'src/util/util.vala',
'src/util/weak_map.vala',
'src/util/weak_timeout.vala',
)
sources += [version_vala]
c_args = [
'-DDINO_SYSTEM_LIBDIR_NAME="@0@"'.format(get_option('prefix') / get_option('libdir')),
'-DDINO_SYSTEM_PLUGIN_DIR="@0@"'.format(get_option('prefix') / get_option('libdir') / get_option('plugindir')),
'-DG_LOG_DOMAIN="libdino"',
]
vala_args = []
if meson.get_compiler('vala').version().version_compare('=0.56.11')
vala_args += ['-D', 'VALA_0_56_11']
endif
lib_dino = library('libdino', sources, c_args: c_args, vala_args: vala_args, include_directories: include_directories('src'), dependencies: dependencies, name_prefix: '', version: '0.0', install: true, install_dir: [true, true, true], install_rpath: default_install_rpath)
dep_dino = declare_dependency(link_with: lib_dino, include_directories: include_directories('.', 'src'))
install_data('dino.deps', install_dir: get_option('datadir') / 'vala/vapi', install_tag: 'devel') # TODO: workaround for https://github.com/mesonbuild/meson/issues/9756
install_headers('src/dino_i18n.h')

View File

@ -1,7 +1,12 @@
using Dino.Entities; using Dino.Entities;
namespace Dino { namespace Dino {
extern const string VERSION;
public string get_version() { return VERSION; }
public string get_short_version() {
if (!VERSION.contains("~")) return VERSION;
return VERSION.split("~")[0] + "+";
}
public interface Application : GLib.Application { public interface Application : GLib.Application {
@ -34,12 +39,12 @@ public interface Application : GLib.Application {
PresenceManager.start(stream_interactor); PresenceManager.start(stream_interactor);
CounterpartInteractionManager.start(stream_interactor); CounterpartInteractionManager.start(stream_interactor);
BlockingManager.start(stream_interactor); BlockingManager.start(stream_interactor);
Calls.start(stream_interactor, db);
ConversationManager.start(stream_interactor, db); ConversationManager.start(stream_interactor, db);
MucManager.start(stream_interactor); MucManager.start(stream_interactor);
AvatarManager.start(stream_interactor, db); AvatarManager.start(stream_interactor, db);
RosterManager.start(stream_interactor, db); RosterManager.start(stream_interactor, db);
FileManager.start(stream_interactor, db); FileManager.start(stream_interactor, db);
Calls.start(stream_interactor, db);
CallStore.start(stream_interactor, db); CallStore.start(stream_interactor, db);
ContentItemStore.start(stream_interactor, db); ContentItemStore.start(stream_interactor, db);
ChatInteraction.start(stream_interactor); ChatInteraction.start(stream_interactor);
@ -49,15 +54,17 @@ public interface Application : GLib.Application {
EntityInfo.start(stream_interactor, db); EntityInfo.start(stream_interactor, db);
MessageCorrection.start(stream_interactor, db); MessageCorrection.start(stream_interactor, db);
FileTransferStorage.start(stream_interactor, db); FileTransferStorage.start(stream_interactor, db);
Reactions.start(stream_interactor, db);
Replies.start(stream_interactor, db);
FallbackBody.start(stream_interactor, db);
ContactModels.start(stream_interactor);
StatelessFileSharing.start(stream_interactor, db);
create_actions(); create_actions();
startup.connect(() => { startup.connect(() => {
stream_interactor.connection_manager.log_options = print_xmpp; stream_interactor.connection_manager.log_options = print_xmpp;
Idle.add(() => {
restore(); restore();
return false;
});
}); });
shutdown.connect(() => { shutdown.connect(() => {
stream_interactor.connection_manager.make_offline_all(); stream_interactor.connection_manager.make_offline_all();

View File

@ -8,38 +8,30 @@ public class Account : Object {
public int id { get; set; } public int id { get; set; }
public string localpart { get { return full_jid.localpart; } } public string localpart { get { return full_jid.localpart; } }
public string domainpart { get { return full_jid.domainpart; } } public string domainpart { get { return full_jid.domainpart; } }
public string resourcepart { get { return full_jid.resourcepart;} } public string resourcepart {
get { return full_jid.resourcepart; }
private set { full_jid.resourcepart = value; }
}
public Jid bare_jid { owned get { return full_jid.bare_jid; } } public Jid bare_jid { owned get { return full_jid.bare_jid; } }
public Jid full_jid { get; private set; } public Jid full_jid { get; private set; }
public string? password { get; set; } public string? password { get; set; }
public string display_name { public string display_name {
owned get { return alias ?? bare_jid.to_string(); } owned get { return (alias != null && alias.length > 0) ? alias.dup() : bare_jid.to_string(); }
} }
public string? alias { get; set; } public string? alias { get; set; }
public bool enabled { get; set; default = false; } public bool enabled { get; set; default = false; }
public string? roster_version { get; set; } public string? roster_version { get; set; }
public DateTime mam_earliest_synced { get; set; default=new DateTime.from_unix_utc(0); }
private Database? db; private Database? db;
public Account(Jid bare_jid, string? resourcepart, string? password, string? alias) { public Account(Jid bare_jid, string password) {
this.id = -1; this.id = -1;
if (resourcepart != null) {
try { try {
this.full_jid = bare_jid.with_resource(resourcepart); this.full_jid = bare_jid.with_resource(get_random_resource());
} catch (InvalidJidError e) {
warning("Tried to create account with invalid resource (%s), defaulting to auto generated", e.message);
}
}
if (this.full_jid == null) {
try {
this.full_jid = bare_jid.with_resource("dino." + Random.next_int().to_string("%x"));
} catch (InvalidJidError e) { } catch (InvalidJidError e) {
error("Auto-generated resource was invalid (%s)", e.message); error("Auto-generated resource was invalid (%s)", e.message);
} }
}
this.password = password; this.password = password;
this.alias = alias;
} }
public Account.from_row(Database db, Qlite.Row row) throws InvalidJidError { public Account.from_row(Database db, Qlite.Row row) throws InvalidJidError {
@ -50,7 +42,6 @@ public class Account : Object {
alias = row[db.account.alias]; alias = row[db.account.alias];
enabled = row[db.account.enabled]; enabled = row[db.account.enabled];
roster_version = row[db.account.roster_version]; roster_version = row[db.account.roster_version];
mam_earliest_synced = new DateTime.from_unix_utc(row[db.account.mam_earliest_synced]);
notify.connect(on_update); notify.connect(on_update);
} }
@ -66,7 +57,6 @@ public class Account : Object {
.value(db.account.alias, alias) .value(db.account.alias, alias)
.value(db.account.enabled, enabled) .value(db.account.enabled, enabled)
.value(db.account.roster_version, roster_version) .value(db.account.roster_version, roster_version)
.value(db.account.mam_earliest_synced, (long)mam_earliest_synced.to_unix())
.perform(); .perform();
notify.connect(on_update); notify.connect(on_update);
@ -79,6 +69,14 @@ public class Account : Object {
db = null; db = null;
} }
public void set_random_resource() {
this.resourcepart = get_random_resource();
}
private static string get_random_resource() {
return "dino." + Random.next_int().to_string("%x");
}
public bool equals(Account acc) { public bool equals(Account acc) {
return equals_func(this, acc); return equals_func(this, acc);
} }
@ -106,8 +104,6 @@ public class Account : Object {
update.set(db.account.enabled, enabled); break; update.set(db.account.enabled, enabled); break;
case "roster-version": case "roster-version":
update.set(db.account.roster_version, roster_version); break; update.set(db.account.roster_version, roster_version); break;
case "mam-earliest-synced":
update.set(db.account.mam_earliest_synced, (long)mam_earliest_synced.to_unix()); break;
} }
update.perform(); update.perform();
} }

View File

@ -22,6 +22,7 @@ public class Conversation : Object {
public Jid counterpart { get; private set; } public Jid counterpart { get; private set; }
public string? nickname { get; set; } public string? nickname { get; set; }
public bool active { get; set; default = false; } public bool active { get; set; default = false; }
public DateTime active_last_changed { get; private set; }
private DateTime? _last_active; private DateTime? _last_active;
public DateTime? last_active { public DateTime? last_active {
get { return _last_active; } get { return _last_active; }
@ -32,7 +33,7 @@ public class Conversation : Object {
} }
} }
} }
public Encryption encryption { get; set; default = Encryption.NONE; } public Encryption encryption { get; set; default = Encryption.UNKNOWN; }
public Message? read_up_to { get; set; } public Message? read_up_to { get; set; }
public int read_up_to_item { get; set; default=-1; } public int read_up_to_item { get; set; default=-1; }
@ -41,9 +42,10 @@ public class Conversation : Object {
public enum Setting { DEFAULT, ON, OFF } public enum Setting { DEFAULT, ON, OFF }
public Setting send_typing { get; set; default = Setting.DEFAULT; } public Setting send_typing { get; set; default = Setting.DEFAULT; }
public Setting send_marker { get; set; default = Setting.DEFAULT; } public Setting send_marker { get; set; default = Setting.DEFAULT; }
public int pinned { get; set; default = 0; }
private Database? db; private Database? db;
public Conversation(Jid jid, Account account, Type type) { public Conversation(Jid jid, Account account, Type type) {
@ -63,6 +65,7 @@ public class Conversation : Object {
if (type_ == Conversation.Type.GROUPCHAT_PM) counterpart = counterpart.with_resource(resource); if (type_ == Conversation.Type.GROUPCHAT_PM) counterpart = counterpart.with_resource(resource);
nickname = type_ == Conversation.Type.GROUPCHAT ? resource : null; nickname = type_ == Conversation.Type.GROUPCHAT ? resource : null;
active = row[db.conversation.active]; active = row[db.conversation.active];
active_last_changed = new DateTime.from_unix_utc(row[db.conversation.active_last_changed]);
int64? last_active = row[db.conversation.last_active]; int64? last_active = row[db.conversation.last_active];
if (last_active != null) this.last_active = new DateTime.from_unix_utc(last_active); if (last_active != null) this.last_active = new DateTime.from_unix_utc(last_active);
encryption = (Encryption) row[db.conversation.encryption]; encryption = (Encryption) row[db.conversation.encryption];
@ -72,21 +75,26 @@ public class Conversation : Object {
notify_setting = (NotifySetting) row[db.conversation.notification]; notify_setting = (NotifySetting) row[db.conversation.notification];
send_typing = (Setting) row[db.conversation.send_typing]; send_typing = (Setting) row[db.conversation.send_typing];
send_marker = (Setting) row[db.conversation.send_marker]; send_marker = (Setting) row[db.conversation.send_marker];
pinned = row[db.conversation.pinned];
notify.connect(on_update); notify.connect(on_update);
} }
public void persist(Database db) { public void persist(Database db) {
this.db = db; this.db = db;
this.active_last_changed = new DateTime.now_utc();
var insert = db.conversation.insert() var insert = db.conversation.insert()
.value(db.conversation.account_id, account.id) .value(db.conversation.account_id, account.id)
.value(db.conversation.jid_id, db.get_jid_id(counterpart)) .value(db.conversation.jid_id, db.get_jid_id(counterpart))
.value(db.conversation.type_, type_) .value(db.conversation.type_, type_)
.value(db.conversation.encryption, encryption) .value(db.conversation.encryption, encryption)
.value(db.conversation.active, active) .value(db.conversation.active, active)
.value(db.conversation.active_last_changed, (long) active_last_changed.to_unix())
.value(db.conversation.notification, notify_setting) .value(db.conversation.notification, notify_setting)
.value(db.conversation.send_typing, send_typing) .value(db.conversation.send_typing, send_typing)
.value(db.conversation.send_marker, send_marker); .value(db.conversation.send_marker, send_marker)
.value(db.conversation.pinned, pinned);
if (read_up_to != null) { if (read_up_to != null) {
insert.value(db.conversation.read_up_to, read_up_to.id); insert.value(db.conversation.read_up_to, read_up_to.id);
} }
@ -176,7 +184,9 @@ public class Conversation : Object {
case "nickname": case "nickname":
update.set(db.conversation.resource, nickname); break; update.set(db.conversation.resource, nickname); break;
case "active": case "active":
update.set(db.conversation.active, active); break; update.set(db.conversation.active, active);
update.set(db.conversation.active_last_changed, (long) new DateTime.now_utc().to_unix());
break;
case "last-active": case "last-active":
if (last_active != null) { if (last_active != null) {
update.set(db.conversation.last_active, (long) last_active.to_unix()); update.set(db.conversation.last_active, (long) last_active.to_unix());
@ -190,6 +200,8 @@ public class Conversation : Object {
update.set(db.conversation.send_typing, send_typing); break; update.set(db.conversation.send_typing, send_typing); break;
case "send-marker": case "send-marker":
update.set(db.conversation.send_marker, send_marker); break; update.set(db.conversation.send_marker, send_marker); break;
case "pinned":
update.set(db.conversation.pinned, pinned); break;
} }
update.perform(); update.perform();
} }

View File

@ -1,12 +1,16 @@
namespace Dino.Entities { namespace Dino.Entities {
public enum Encryption { public enum Encryption {
NONE, NONE,
PGP, PGP,
OMEMO, OMEMO,
DTLS_SRTP, DTLS_SRTP,
SRTP, SRTP,
UNKNOWN, UNKNOWN;
}
public bool is_some() {
return this != NONE;
}
}
} }

View File

@ -4,6 +4,8 @@ namespace Dino.Entities {
public class FileTransfer : Object { public class FileTransfer : Object {
public signal void sources_changed();
public const bool DIRECTION_SENT = true; public const bool DIRECTION_SENT = true;
public const bool DIRECTION_RECEIVED = false; public const bool DIRECTION_RECEIVED = false;
@ -15,6 +17,7 @@ public class FileTransfer : Object {
} }
public int id { get; set; default=-1; } public int id { get; set; default=-1; }
public string? file_sharing_id { get; set; }
public Account account { get; set; } public Account account { get; set; }
public Jid counterpart { get; set; } public Jid counterpart { get; set; }
public Jid ourpart { get; set; } public Jid ourpart { get; set; }
@ -64,12 +67,51 @@ public class FileTransfer : Object {
} }
public string path { get; set; } public string path { get; set; }
public string? mime_type { get; set; } public string? mime_type { get; set; }
// TODO(hrxi): expand to 64 bit public int64 size { get; set; }
public int size { get; set; default=-1; }
public State state { get; set; default=State.NOT_STARTED; } public State state { get; set; default=State.NOT_STARTED; }
public int provider { get; set; } public int provider { get; set; }
public string info { get; set; } public string info { get; set; }
public Cancellable cancellable { get; default=new Cancellable(); }
// This value is not persisted
public int64 transferred_bytes { get; set; }
public Xep.FileMetadataElement.FileMetadata file_metadata {
owned get {
return new Xep.FileMetadataElement.FileMetadata() {
name = this.file_name,
mime_type = this.mime_type,
size = this.size,
desc = this.desc,
date = this.modification_date,
width = this.width,
height = this.height,
length = this.length,
hashes = this.hashes,
thumbnails = this.thumbnails
};
}
set {
this.file_name = value.name;
this.mime_type = value.mime_type;
this.size = value.size;
this.desc = value.desc;
this.modification_date = value.date;
this.width = value.width;
this.height = value.height;
this.length = value.length;
this.hashes = value.hashes;
this.thumbnails = value.thumbnails;
}
}
public string? desc { get; set; }
public DateTime? modification_date { get; set; }
public int width { get; set; default=-1; }
public int height { get; set; default=-1; }
public int64 length { get; set; default=-1; }
public Gee.List<Xep.CryptographicHashes.Hash> hashes = new Gee.ArrayList<Xep.CryptographicHashes.Hash>();
public Gee.List<Xep.StatelessFileSharing.Source> sfs_sources = new Gee.ArrayList<Xep.StatelessFileSharing.Source>(Xep.StatelessFileSharing.Source.equals_func);
public Gee.List<Xep.JingleContentThumbnails.Thumbnail> thumbnails = new Gee.ArrayList<Xep.JingleContentThumbnails.Thumbnail>();
private Database? db; private Database? db;
private string storage_dir; private string storage_dir;
@ -79,6 +121,7 @@ public class FileTransfer : Object {
this.storage_dir = storage_dir; this.storage_dir = storage_dir;
id = row[db.file_transfer.id]; id = row[db.file_transfer.id];
file_sharing_id = row[db.file_transfer.file_sharing_id];
account = db.get_account_by_id(row[db.file_transfer.account_id]); // TODO dont have to generate acc new account = db.get_account_by_id(row[db.file_transfer.account_id]); // TODO dont have to generate acc new
counterpart = db.get_jid_by_id(row[db.file_transfer.counterpart_id]); counterpart = db.get_jid_by_id(row[db.file_transfer.counterpart_id]);
@ -98,10 +141,37 @@ public class FileTransfer : Object {
file_name = row[db.file_transfer.file_name]; file_name = row[db.file_transfer.file_name];
path = row[db.file_transfer.path]; path = row[db.file_transfer.path];
mime_type = row[db.file_transfer.mime_type]; mime_type = row[db.file_transfer.mime_type];
size = row[db.file_transfer.size]; size = (int64) row[db.file_transfer.size];
state = (State) row[db.file_transfer.state]; state = (State) row[db.file_transfer.state];
provider = row[db.file_transfer.provider]; provider = row[db.file_transfer.provider];
info = row[db.file_transfer.info]; info = row[db.file_transfer.info];
modification_date = new DateTime.from_unix_utc(row[db.file_transfer.modification_date]);
width = row[db.file_transfer.width];
height = row[db.file_transfer.height];
length = (int64) row[db.file_transfer.length];
// TODO put those into the initial query
foreach(var hash_row in db.file_hashes.select().with(db.file_hashes.id, "=", id)) {
Xep.CryptographicHashes.Hash hash = new Xep.CryptographicHashes.Hash();
hash.algo = hash_row[db.file_hashes.algo];
hash.val = hash_row[db.file_hashes.value];
hashes.add(hash);
}
foreach(var thumbnail_row in db.file_thumbnails.select().with(db.file_thumbnails.id, "=", id)) {
Xep.JingleContentThumbnails.Thumbnail thumbnail = new Xep.JingleContentThumbnails.Thumbnail();
thumbnail.data = Xmpp.get_data_for_uri(thumbnail_row[db.file_thumbnails.uri]);
thumbnail.media_type = thumbnail_row[db.file_thumbnails.mime_type];
thumbnail.width = thumbnail_row[db.file_thumbnails.width];
thumbnail.height = thumbnail_row[db.file_thumbnails.height];
thumbnails.add(thumbnail);
}
foreach(Qlite.Row source_row in db.sfs_sources.select().with(db.sfs_sources.file_transfer_id, "=", id)) {
if (source_row[db.sfs_sources.type] == "http") {
sfs_sources.add(new Xep.StatelessFileSharing.HttpSource() { url=source_row[db.sfs_sources.data] });
}
}
notify.connect(on_update); notify.connect(on_update);
} }
@ -120,26 +190,79 @@ public class FileTransfer : Object {
.value(db.file_transfer.local_time, (long) local_time.to_unix()) .value(db.file_transfer.local_time, (long) local_time.to_unix())
.value(db.file_transfer.encryption, encryption) .value(db.file_transfer.encryption, encryption)
.value(db.file_transfer.file_name, file_name) .value(db.file_transfer.file_name, file_name)
.value(db.file_transfer.size, size) .value(db.file_transfer.size, (long) size)
.value(db.file_transfer.state, state) .value(db.file_transfer.state, state)
.value(db.file_transfer.provider, provider) .value(db.file_transfer.provider, provider)
.value(db.file_transfer.info, info); .value(db.file_transfer.info, info);
if (file_name != null) builder.value(db.file_transfer.file_name, file_name); if (file_sharing_id != null) builder.value(db.file_transfer.file_sharing_id, file_sharing_id);
if (path != null) builder.value(db.file_transfer.path, path); if (path != null) builder.value(db.file_transfer.path, path);
if (mime_type != null) builder.value(db.file_transfer.mime_type, mime_type); if (mime_type != null) builder.value(db.file_transfer.mime_type, mime_type);
if (path != null) builder.value(db.file_transfer.path, path);
if (modification_date != null) builder.value(db.file_transfer.modification_date, (long) modification_date.to_unix());
if (width != -1) builder.value(db.file_transfer.width, width);
if (height != -1) builder.value(db.file_transfer.height, height);
if (length != -1) builder.value(db.file_transfer.length, (long) length);
id = (int) builder.perform(); id = (int) builder.perform();
foreach (Xep.CryptographicHashes.Hash hash in hashes) {
db.file_hashes.insert()
.value(db.file_hashes.id, id)
.value(db.file_hashes.algo, hash.algo)
.value(db.file_hashes.value, hash.val)
.perform();
}
foreach (Xep.JingleContentThumbnails.Thumbnail thumbnail in thumbnails) {
string data_uri = "data:image/png;base64," + Base64.encode(thumbnail.data.get_data());
db.file_thumbnails.insert()
.value(db.file_thumbnails.id, id)
.value(db.file_thumbnails.uri, data_uri)
.value(db.file_thumbnails.mime_type, thumbnail.media_type)
.value(db.file_thumbnails.width, thumbnail.width)
.value(db.file_thumbnails.height, thumbnail.height)
.perform();
}
foreach (Xep.StatelessFileSharing.Source source in sfs_sources) {
persist_source(source);
}
notify.connect(on_update); notify.connect(on_update);
} }
public File get_file() { public void add_sfs_source(Xep.StatelessFileSharing.Source source) {
if (sfs_sources.contains(source)) return; // Don't add the same source twice. Might happen due to MAM and lacking deduplication.
sfs_sources.add(source);
if (id != -1) {
persist_source(source);
}
sources_changed();
}
private void persist_source(Xep.StatelessFileSharing.Source source) {
Xep.StatelessFileSharing.HttpSource? http_source = source as Xep.StatelessFileSharing.HttpSource;
if (http_source != null) {
db.sfs_sources.insert()
.value(db.sfs_sources.file_transfer_id, id)
.value(db.sfs_sources.type, "http")
.value(db.sfs_sources.data, http_source.url)
.perform();
}
}
public File? get_file() {
if (path == null) return null;
return File.new_for_path(Path.build_filename(Dino.get_storage_dir(), "files", path)); return File.new_for_path(Path.build_filename(Dino.get_storage_dir(), "files", path));
} }
private void on_update(Object o, ParamSpec sp) { private void on_update(Object o, ParamSpec sp) {
Qlite.UpdateBuilder update_builder = db.file_transfer.update().with(db.file_transfer.id, "=", id); Qlite.UpdateBuilder update_builder = db.file_transfer.update().with(db.file_transfer.id, "=", id);
switch (sp.name) { switch (sp.name) {
case "file-sharing-id":
update_builder.set(db.file_transfer.file_sharing_id, file_sharing_id); break;
case "counterpart": case "counterpart":
update_builder.set(db.file_transfer.counterpart_id, db.get_jid_id(counterpart)); update_builder.set(db.file_transfer.counterpart_id, db.get_jid_id(counterpart));
update_builder.set(db.file_transfer.counterpart_resource, counterpart.resourcepart); break; update_builder.set(db.file_transfer.counterpart_resource, counterpart.resourcepart); break;
@ -160,7 +283,7 @@ public class FileTransfer : Object {
case "mime-type": case "mime-type":
update_builder.set(db.file_transfer.mime_type, mime_type); break; update_builder.set(db.file_transfer.mime_type, mime_type); break;
case "size": case "size":
update_builder.set(db.file_transfer.size, size); break; update_builder.set(db.file_transfer.size, (long) size); break;
case "state": case "state":
if (state == State.IN_PROGRESS) return; if (state == State.IN_PROGRESS) return;
update_builder.set(db.file_transfer.state, state); break; update_builder.set(db.file_transfer.state, state); break;
@ -168,6 +291,14 @@ public class FileTransfer : Object {
update_builder.set(db.file_transfer.provider, provider); break; update_builder.set(db.file_transfer.provider, provider); break;
case "info": case "info":
update_builder.set(db.file_transfer.info, info); break; update_builder.set(db.file_transfer.info, info); break;
case "modification-date":
update_builder.set(db.file_transfer.modification_date, (long) modification_date.to_unix()); break;
case "width":
update_builder.set(db.file_transfer.width, width); break;
case "height":
update_builder.set(db.file_transfer.height, height); break;
case "length":
update_builder.set(db.file_transfer.length, (long) length); break;
} }
update_builder.perform(); update_builder.perform();
} }

View File

@ -67,6 +67,10 @@ public class Message : Object {
} }
} }
public string? edit_to = null; public string? edit_to = null;
public int quoted_item_id { get; private set; default=0; }
private Gee.List<Xep.FallbackIndication.Fallback> fallbacks = null;
private Gee.List<Xep.MessageMarkup.Span> markups = null;
private Database? db; private Database? db;
@ -105,6 +109,7 @@ public class Message : Object {
if (real_jid_str != null) real_jid = new Jid(real_jid_str); if (real_jid_str != null) real_jid = new Jid(real_jid_str);
edit_to = row[db.message_correction.to_stanza_id]; edit_to = row[db.message_correction.to_stanza_id];
quoted_item_id = row[db.reply.quoted_content_item_id];
notify.connect(on_update); notify.connect(on_update);
} }
@ -138,6 +143,103 @@ public class Message : Object {
notify.connect(on_update); notify.connect(on_update);
} }
public void set_quoted_item(int quoted_content_item_id) {
if (id == -1) {
warning("Message needs to be persisted before setting quoted item");
return;
}
this.quoted_item_id = quoted_content_item_id;
db.reply.upsert()
.value(db.reply.message_id, id, true)
.value(db.reply.quoted_content_item_id, quoted_content_item_id)
.value_null(db.reply.quoted_message_stanza_id)
.value_null(db.reply.quoted_message_from)
.perform();
}
public Gee.List<Xep.FallbackIndication.Fallback> get_fallbacks() {
if (fallbacks != null) return fallbacks;
fetch_body_meta();
return fallbacks;
}
public Gee.List<Xep.MessageMarkup.Span> get_markups() {
if (markups != null) return markups;
fetch_body_meta();
return markups;
}
public void persist_markups(Gee.List<Xep.MessageMarkup.Span> markups, int message_id) {
this.markups = markups;
foreach (var span in markups) {
foreach (var ty in span.types) {
db.body_meta.insert()
.value(db.body_meta.info_type, Xep.MessageMarkup.NS_URI)
.value(db.body_meta.message_id, message_id)
.value(db.body_meta.info, Xep.MessageMarkup.span_type_to_str(ty))
.value(db.body_meta.from_char, span.start_char)
.value(db.body_meta.to_char, span.end_char)
.perform();
}
}
}
private void fetch_body_meta() {
var fallbacks_by_ns = new HashMap<string, ArrayList<Xep.FallbackIndication.FallbackLocation>>();
var markups = new ArrayList<Xep.MessageMarkup.Span>();
foreach (Qlite.Row row in db.body_meta.select().with(db.body_meta.message_id, "=", id)) {
switch (row[db.body_meta.info_type]) {
case Xep.FallbackIndication.NS_URI:
string ns_uri = row[db.body_meta.info];
if (!fallbacks_by_ns.has_key(ns_uri)) {
fallbacks_by_ns[ns_uri] = new ArrayList<Xep.FallbackIndication.FallbackLocation>();
}
fallbacks_by_ns[ns_uri].add(new Xep.FallbackIndication.FallbackLocation.partial_body(row[db.body_meta.from_char], row[db.body_meta.to_char]));
break;
case Xep.MessageMarkup.NS_URI:
var types = new ArrayList<Xep.MessageMarkup.SpanType>();
types.add(Xep.MessageMarkup.str_to_span_type(row[db.body_meta.info]));
markups.add(new Xep.MessageMarkup.Span() { types=types, start_char=row[db.body_meta.from_char], end_char=row[db.body_meta.to_char] });
break;
}
}
var fallbacks = new ArrayList<Xep.FallbackIndication.Fallback>();
foreach (string ns_uri in fallbacks_by_ns.keys) {
fallbacks.add(new Xep.FallbackIndication.Fallback(ns_uri, fallbacks_by_ns[ns_uri]));
}
this.fallbacks = fallbacks;
this.markups = markups;
}
public void set_fallbacks(Gee.List<Xep.FallbackIndication.Fallback> fallbacks) {
if (id == -1) {
warning("Message needs to be persisted before setting fallbacks");
return;
}
this.fallbacks = fallbacks;
foreach (var fallback in fallbacks) {
foreach (var location in fallback.locations) {
db.body_meta.insert()
.value(db.body_meta.message_id, id)
.value(db.body_meta.info_type, Xep.FallbackIndication.NS_URI)
.value(db.body_meta.info, fallback.ns_uri)
.value(db.body_meta.from_char, location.from_char)
.value(db.body_meta.to_char, location.to_char)
.perform();
}
}
}
public void set_type_string(string type) { public void set_type_string(string type) {
switch (type) { switch (type) {
case Xmpp.MessageStanza.TYPE_CHAT: case Xmpp.MessageStanza.TYPE_CHAT:
@ -172,6 +274,7 @@ public class Message : Object {
} }
public static uint hash_func(Message message) { public static uint hash_func(Message message) {
if (message.body == null) return 0;
return message.body.hash(); return message.body.hash();
} }
@ -210,6 +313,13 @@ public class Message : Object {
.value(db.real_jid.real_jid, real_jid.to_string()) .value(db.real_jid.real_jid, real_jid.to_string())
.perform(); .perform();
} }
if (sp.get_name() == "quoted-item-id") {
db.reply.upsert()
.value(db.reply.message_id, id, true)
.value(db.reply.quoted_content_item_id, quoted_item_id)
.perform();
}
} }
} }

View File

@ -67,6 +67,7 @@ public class Settings : Object {
} }
} }
// There is currently no spell checking for GTK4, thus there is currently no UI for this setting.
private bool check_spelling_; private bool check_spelling_;
public bool check_spelling { public bool check_spelling {
get { return check_spelling_; } get { return check_spelling_; }
@ -78,6 +79,24 @@ public class Settings : Object {
check_spelling_ = value; check_spelling_ = value;
} }
} }
public Encryption get_default_encryption(Account account) {
string? setting = db.account_settings.get_value(account.id, "default-encryption");
if (setting != null) {
return (Encryption) int.parse(setting);
}
return Encryption.OMEMO;
}
public void set_default_encryption(Account account, Encryption encryption) {
db.account_settings.upsert()
.value(db.account_settings.key, "default-encryption", true)
.value(db.account_settings.account_id, account.id, true)
.value(db.account_settings.value, ((int)encryption).to_string())
.perform();
}
} }
} }

View File

@ -12,7 +12,8 @@ public enum Priority {
} }
public enum WidgetType { public enum WidgetType {
GTK GTK3,
GTK4
} }
public interface RootInterface : Object { public interface RootInterface : Object {
@ -27,6 +28,8 @@ public interface EncryptionListEntry : Object {
public abstract void encryption_activated(Entities.Conversation conversation, Plugins.SetInputFieldStatus callback); public abstract void encryption_activated(Entities.Conversation conversation, Plugins.SetInputFieldStatus callback);
public abstract Object? get_encryption_icon(Entities.Conversation conversation, ContentItem content_item); public abstract Object? get_encryption_icon(Entities.Conversation conversation, ContentItem content_item);
public abstract string? get_encryption_icon_name(Entities.Conversation conversation, ContentItem content_item);
} }
public interface CallEncryptionEntry : Object { public interface CallEncryptionEntry : Object {
@ -45,21 +48,26 @@ public abstract class AccountSettingsEntry : Object {
public abstract string name { get; } public abstract string name { get; }
public virtual int16 label_top_padding { get { return -1; } } public virtual int16 label_top_padding { get { return -1; } }
public abstract AccountSettingsWidget? get_widget(WidgetType type); public abstract signal void activated();
public abstract void deactivate();
public abstract void set_account(Account account);
public abstract Object? get_widget(WidgetType type);
} }
public interface AccountSettingsWidget : Object { public abstract class EncryptionPreferencesEntry : Object {
public abstract void set_account(Account account); public abstract string id { get; }
public virtual Priority priority { get { return Priority.DEFAULT; } }
public abstract signal void activated(); public abstract Object? get_widget(Account account, WidgetType type);
public abstract void deactivate();
} }
public interface ContactDetailsProvider : Object { public interface ContactDetailsProvider : Object {
public abstract string id { get; } public abstract string id { get; }
public abstract string tab { get; }
public abstract void populate(Conversation conversation, ContactDetails contact_details, WidgetType type); public abstract void populate(Conversation conversation, ContactDetails contact_details, WidgetType type);
public abstract Object? get_widget(Conversation conversation);
} }
public class ContactDetails : Object { public class ContactDetails : Object {
@ -76,10 +84,8 @@ public interface TextCommand : Object {
public interface ConversationTitlebarEntry : Object { public interface ConversationTitlebarEntry : Object {
public abstract string id { get; } public abstract string id { get; }
public abstract double order { get; } public abstract double order { get; }
public abstract ConversationTitlebarWidget? get_widget(WidgetType type); public abstract Object? get_widget(WidgetType type);
}
public interface ConversationTitlebarWidget : Object {
public abstract void set_conversation(Conversation conversation); public abstract void set_conversation(Conversation conversation);
public abstract void unset_conversation(); public abstract void unset_conversation();
} }
@ -96,7 +102,7 @@ public abstract interface ConversationAdditionPopulator : ConversationItemPopula
public abstract interface VideoCallPlugin : Object { public abstract interface VideoCallPlugin : Object {
public abstract bool supports(string? media); public abstract bool supported();
// Video widget // Video widget
public abstract VideoCallWidget? create_widget(WidgetType type); public abstract VideoCallWidget? create_widget(WidgetType type);
@ -146,14 +152,22 @@ public abstract class MetaConversationItem : Object {
public bool requires_header { get; set; default=false; } public bool requires_header { get; set; default=false; }
public bool in_edit_mode { get; set; default=false; } public bool in_edit_mode { get; set; default=false; }
public abstract Object? get_widget(WidgetType type); public abstract Object? get_widget(ConversationItemWidgetInterface outer, WidgetType type);
public abstract Gee.List<MessageAction>? get_item_actions(WidgetType type); public abstract Gee.List<MessageAction>? get_item_actions(WidgetType type);
} }
public delegate void MessageActionEvoked(Object button, Plugins.MetaConversationItem evoked_on, Object widget); public interface ConversationItemWidgetInterface: Object {
public abstract void set_widget(Object object, WidgetType type, int priority);
}
public delegate void MessageActionEvoked(Variant? variant);
public class MessageAction : Object { public class MessageAction : Object {
public string name;
public bool sensitive = true;
public string icon_name; public string icon_name;
public MessageActionEvoked callback; public string? tooltip;
public Object? popover;
public MessageActionEvoked? callback;
} }
public abstract class MetaConversationNotification : Object { public abstract class MetaConversationNotification : Object {

View File

@ -3,25 +3,24 @@ using Gee;
namespace Dino.Plugins { namespace Dino.Plugins {
public class Registry { public class Registry {
internal ArrayList<EncryptionListEntry> encryption_list_entries = new ArrayList<EncryptionListEntry>(); public HashMap<Entities.Encryption, EncryptionListEntry> encryption_list_entries = new HashMap<Entities.Encryption, EncryptionListEntry>();
internal HashMap<string, CallEncryptionEntry> call_encryption_entries = new HashMap<string, CallEncryptionEntry>(); public HashMap<string, CallEncryptionEntry> call_encryption_entries = new HashMap<string, CallEncryptionEntry>();
internal ArrayList<AccountSettingsEntry> account_settings_entries = new ArrayList<AccountSettingsEntry>(); public ArrayList<AccountSettingsEntry> account_settings_entries = new ArrayList<AccountSettingsEntry>();
internal ArrayList<ContactDetailsProvider> contact_details_entries = new ArrayList<ContactDetailsProvider>(); public ArrayList<EncryptionPreferencesEntry> encryption_preferences_entries = new ArrayList<EncryptionPreferencesEntry>();
internal Map<string, TextCommand> text_commands = new HashMap<string, TextCommand>(); public ArrayList<ContactDetailsProvider> contact_details_entries = new ArrayList<ContactDetailsProvider>();
internal Gee.List<ConversationAdditionPopulator> conversation_addition_populators = new ArrayList<ConversationAdditionPopulator>(); public Map<string, TextCommand> text_commands = new HashMap<string, TextCommand>();
internal Gee.List<NotificationPopulator> notification_populators = new ArrayList<NotificationPopulator>(); public Gee.List<ConversationAdditionPopulator> conversation_addition_populators = new ArrayList<ConversationAdditionPopulator>();
internal Gee.Collection<ConversationTitlebarEntry> conversation_titlebar_entries = new Gee.TreeSet<ConversationTitlebarEntry>((a, b) => { public Gee.List<NotificationPopulator> notification_populators = new ArrayList<NotificationPopulator>();
public Gee.Collection<ConversationTitlebarEntry> conversation_titlebar_entries = new Gee.TreeSet<ConversationTitlebarEntry>((a, b) => {
return (int)(a.order - b.order); return (int)(a.order - b.order);
}); });
public VideoCallPlugin? video_call_plugin; public VideoCallPlugin? video_call_plugin;
public bool register_encryption_list_entry(EncryptionListEntry entry) { public bool register_encryption_list_entry(EncryptionListEntry entry) {
lock(encryption_list_entries) { lock(encryption_list_entries) {
foreach(var e in encryption_list_entries) { if (encryption_list_entries.has_key(entry.encryption)) return false;
if (e.encryption == entry.encryption) return false;
} encryption_list_entries[entry.encryption] = entry;
encryption_list_entries.add(entry);
encryption_list_entries.sort((a,b) => b.name.collate(a.name));
return true; return true;
} }
} }
@ -45,6 +44,18 @@ public class Registry {
} }
} }
public bool register_encryption_preferences_entry(EncryptionPreferencesEntry entry) {
lock(encryption_preferences_entries) {
foreach(var e in encryption_preferences_entries) {
if (e.id == entry.id) return false;
}
encryption_preferences_entries.add(entry);
// TODO: Order by priority
// encryption_preferences_entries.sort((a,b) => b.name.collate(a.name));
return true;
}
}
public bool register_contact_details_entry(ContactDetailsProvider entry) { public bool register_contact_details_entry(ContactDetailsProvider entry) {
lock(contact_details_entries) { lock(contact_details_entries) {
foreach(ContactDetailsProvider e in contact_details_entries) { foreach(ContactDetailsProvider e in contact_details_entries) {

View File

@ -12,6 +12,7 @@ public class AvatarManager : StreamInteractionModule, Object {
public string id { get { return IDENTITY.id; } } public string id { get { return IDENTITY.id; } }
public signal void received_avatar(Jid jid, Account account); public signal void received_avatar(Jid jid, Account account);
public signal void fetched_avatar(Jid jid, Account account);
private enum Source { private enum Source {
USER_AVATARS, USER_AVATARS,
@ -23,8 +24,7 @@ public class AvatarManager : StreamInteractionModule, Object {
private string folder = null; private string folder = null;
private HashMap<Jid, string> user_avatars = new HashMap<Jid, string>(Jid.hash_func, Jid.equals_func); private HashMap<Jid, string> user_avatars = new HashMap<Jid, string>(Jid.hash_func, Jid.equals_func);
private HashMap<Jid, string> vcard_avatars = new HashMap<Jid, string>(Jid.hash_func, Jid.equals_func); private HashMap<Jid, string> vcard_avatars = new HashMap<Jid, string>(Jid.hash_func, Jid.equals_func);
private HashMap<string, Pixbuf> cached_pixbuf = new HashMap<string, Pixbuf>(); private HashSet<string> pending_fetch = new HashSet<string>();
private HashMap<string, Gee.List<SourceFuncWrapper>> pending_pixbuf = new HashMap<string, Gee.List<SourceFuncWrapper>>();
private const int MAX_PIXEL = 192; private const int MAX_PIXEL = 192;
public static void start(StreamInteractor stream_interactor, Database db) { public static void start(StreamInteractor stream_interactor, Database db) {
@ -35,8 +35,39 @@ public class AvatarManager : StreamInteractionModule, Object {
private AvatarManager(StreamInteractor stream_interactor, Database db) { private AvatarManager(StreamInteractor stream_interactor, Database db) {
this.stream_interactor = stream_interactor; this.stream_interactor = stream_interactor;
this.db = db; this.db = db;
this.folder = Path.build_filename(Dino.get_storage_dir(), "avatars");
DirUtils.create_with_parents(this.folder, 0700); File old_avatars = File.new_build_filename(Dino.get_storage_dir(), "avatars");
File new_avatars = File.new_build_filename(Dino.get_cache_dir(), "avatars");
this.folder = new_avatars.get_path();
// Move old avatar location to new one
if (old_avatars.query_exists()) {
if (!new_avatars.query_exists()) {
// Move old avatars folder (~/.local/share/dino) to new location (~/.cache/dino)
try {
new_avatars.get_parent().make_directory_with_parents();
} catch (Error e) { }
try {
old_avatars.move(new_avatars, FileCopyFlags.NONE);
debug("Avatars directory %s moved to %s", old_avatars.get_path(), new_avatars.get_path());
} catch (Error e) { }
} else {
// If both old and new folders exist, remove the old one
try {
FileEnumerator enumerator = old_avatars.enumerate_children("standard::*", FileQueryInfoFlags.NOFOLLOW_SYMLINKS);
FileInfo info = null;
while ((info = enumerator.next_file()) != null) {
FileUtils.remove(old_avatars.get_path() + "/" + info.get_name());
}
DirUtils.remove(old_avatars.get_path());
} catch (Error e) { }
}
}
// Create avatar folder
try {
new_avatars.make_directory_with_parents();
} catch (Error e) { }
stream_interactor.account_added.connect(on_account_added); stream_interactor.account_added.connect(on_account_added);
stream_interactor.module_manager.initialize_account_modules.connect((_, modules) => { stream_interactor.module_manager.initialize_account_modules.connect((_, modules) => {
@ -45,6 +76,18 @@ public class AvatarManager : StreamInteractionModule, Object {
}); });
} }
public File? get_avatar_file(Account account, Jid jid_) {
string? hash = get_avatar_hash(account, jid_);
if (hash == null) return null;
File file = File.new_for_path(Path.build_filename(folder, hash));
if (!file.query_exists()) {
fetch_and_store_for_jid.begin(account, jid_);
return null;
} else {
return file;
}
}
private string? get_avatar_hash(Account account, Jid jid_) { private string? get_avatar_hash(Account account, Jid jid_) {
Jid jid = jid_; Jid jid = jid_;
if (!stream_interactor.get_module(MucManager.IDENTITY).is_groupchat_occupant(jid_, account)) { if (!stream_interactor.get_module(MucManager.IDENTITY).is_groupchat_occupant(jid_, account)) {
@ -59,79 +102,10 @@ public class AvatarManager : StreamInteractionModule, Object {
} }
} }
public bool has_avatar_cached(Account account, Jid jid) {
string? hash = get_avatar_hash(account, jid);
return hash != null && cached_pixbuf.has_key(hash);
}
public bool has_avatar(Account account, Jid jid) { public bool has_avatar(Account account, Jid jid) {
return get_avatar_hash(account, jid) != null; return get_avatar_hash(account, jid) != null;
} }
public Pixbuf? get_cached_avatar(Account account, Jid jid_) {
string? hash = get_avatar_hash(account, jid_);
if (hash == null) return null;
if (cached_pixbuf.has_key(hash)) return cached_pixbuf[hash];
return null;
}
public async Pixbuf? get_avatar(Account account, Jid jid_) {
Jid jid = jid_;
if (!stream_interactor.get_module(MucManager.IDENTITY).is_groupchat_occupant(jid_, account)) {
jid = jid_.bare_jid;
}
int source = -1;
string? hash = null;
if (user_avatars.has_key(jid)) {
hash = user_avatars[jid];
source = 1;
} else if (vcard_avatars.has_key(jid)) {
hash = vcard_avatars[jid];
source = 2;
}
if (hash == null) return null;
if (cached_pixbuf.has_key(hash)) {
return cached_pixbuf[hash];
}
XmppStream? stream = stream_interactor.get_stream(account);
if (stream == null || !stream.negotiation_complete) return null;
if (pending_pixbuf.has_key(hash)) {
pending_pixbuf[hash].add(new SourceFuncWrapper(get_avatar.callback));
yield;
return cached_pixbuf[hash];
}
pending_pixbuf[hash] = new ArrayList<SourceFuncWrapper>();
Pixbuf? image = yield get_image(hash);
if (image != null) {
cached_pixbuf[hash] = image;
} else {
Bytes? bytes = null;
if (source == 1) {
bytes = yield Xmpp.Xep.UserAvatars.fetch_image(stream, jid, hash);
} else if (source == 2) {
bytes = yield Xmpp.Xep.VCard.fetch_image(stream, jid, hash);
if (bytes == null && jid.is_bare()) {
db.avatar.delete().with(db.avatar.jid_id, "=", db.get_jid_id(jid)).perform();
}
}
if (bytes != null) {
store_image(hash, bytes);
image = yield get_image(hash);
}
cached_pixbuf[hash] = image;
}
foreach (SourceFuncWrapper sfw in pending_pixbuf[hash]) {
sfw.sfun();
}
return image;
}
public void publish(Account account, string file) { public void publish(Account account, string file) {
try { try {
Pixbuf pixbuf = new Pixbuf.from_file(file); Pixbuf pixbuf = new Pixbuf.from_file(file);
@ -153,30 +127,32 @@ public class AvatarManager : StreamInteractionModule, Object {
} }
} }
public void unset_avatar(Account account) {
XmppStream stream = stream_interactor.get_stream(account);
if (stream == null) return;
Xmpp.Xep.UserAvatars.unset_avatar(stream);
}
private void on_account_added(Account account) { private void on_account_added(Account account) {
stream_interactor.module_manager.get_module(account, Xep.UserAvatars.Module.IDENTITY).received_avatar_hash.connect((stream, jid, id) => stream_interactor.module_manager.get_module(account, Xep.UserAvatars.Module.IDENTITY).received_avatar_hash.connect((stream, jid, id) =>
on_user_avatar_received.begin(account, jid, id) on_user_avatar_received(account, jid, id)
); );
stream_interactor.module_manager.get_module(account, Xep.UserAvatars.Module.IDENTITY).avatar_removed.connect((stream, jid) => {
on_user_avatar_removed(account, jid);
});
stream_interactor.module_manager.get_module(account, Xep.VCard.Module.IDENTITY).received_avatar_hash.connect((stream, jid, id) => stream_interactor.module_manager.get_module(account, Xep.VCard.Module.IDENTITY).received_avatar_hash.connect((stream, jid, id) =>
on_vcard_avatar_received.begin(account, jid, id) on_vcard_avatar_received(account, jid, id)
); );
foreach (var entry in get_avatar_hashes(account, Source.USER_AVATARS).entries) { foreach (var entry in get_avatar_hashes(account, Source.USER_AVATARS).entries) {
user_avatars[entry.key] = entry.value; on_user_avatar_received(account, entry.key, entry.value);
} }
foreach (var entry in get_avatar_hashes(account, Source.VCARD).entries) { foreach (var entry in get_avatar_hashes(account, Source.VCARD).entries) {
on_vcard_avatar_received(account, entry.key, entry.value);
// FIXME: remove. temporary to remove falsely saved avatars.
if (stream_interactor.get_module(MucManager.IDENTITY).is_groupchat(entry.key, account)) {
db.avatar.delete().with(db.avatar.jid_id, "=", db.get_jid_id(entry.key)).perform();
continue;
}
vcard_avatars[entry.key] = entry.value;
} }
} }
private async void on_user_avatar_received(Account account, Jid jid_, string id) { private void on_user_avatar_received(Account account, Jid jid_, string id) {
Jid jid = jid_.bare_jid; Jid jid = jid_.bare_jid;
if (!user_avatars.has_key(jid) || user_avatars[jid] != id) { if (!user_avatars.has_key(jid) || user_avatars[jid] != id) {
@ -186,7 +162,14 @@ public class AvatarManager : StreamInteractionModule, Object {
received_avatar(jid, account); received_avatar(jid, account);
} }
private async void on_vcard_avatar_received(Account account, Jid jid_, string id) { private void on_user_avatar_removed(Account account, Jid jid_) {
Jid jid = jid_.bare_jid;
user_avatars.unset(jid);
remove_avatar_hash(account, jid, Source.USER_AVATARS);
received_avatar(jid, account);
}
private void on_vcard_avatar_received(Account account, Jid jid_, string id) {
bool is_gc = stream_interactor.get_module(MucManager.IDENTITY).might_be_groupchat(jid_.bare_jid, account); bool is_gc = stream_interactor.get_module(MucManager.IDENTITY).might_be_groupchat(jid_.bare_jid, account);
Jid jid = is_gc ? jid_ : jid_.bare_jid; Jid jid = is_gc ? jid_ : jid_.bare_jid;
@ -208,6 +191,14 @@ public class AvatarManager : StreamInteractionModule, Object {
.perform(); .perform();
} }
public void remove_avatar_hash(Account account, Jid jid, int type) {
db.avatar.delete()
.with(db.avatar.jid_id, "=", db.get_jid_id(jid))
.with(db.avatar.account_id, "=", account.id)
.with(db.avatar.type_, "=", type)
.perform();
}
public HashMap<Jid, string> get_avatar_hashes(Account account, int type) { public HashMap<Jid, string> get_avatar_hashes(Account account, int type) {
HashMap<Jid, string> ret = new HashMap<Jid, string>(Jid.hash_func, Jid.equals_func); HashMap<Jid, string> ret = new HashMap<Jid, string>(Jid.hash_func, Jid.equals_func);
foreach (Row row in db.avatar.select({db.avatar.jid_id, db.avatar.hash}) foreach (Row row in db.avatar.select({db.avatar.jid_id, db.avatar.hash})
@ -218,12 +209,53 @@ public class AvatarManager : StreamInteractionModule, Object {
return ret; return ret;
} }
public void store_image(string id, Bytes data) { public async bool fetch_and_store_for_jid(Account account, Jid jid) {
int source = -1;
string? hash = null;
if (user_avatars.has_key(jid)) {
hash = user_avatars[jid];
source = 1;
} else if (vcard_avatars.has_key(jid)) {
hash = vcard_avatars[jid];
source = 2;
} else {
return false;
}
XmppStream? stream = stream_interactor.get_stream(account);
if (stream == null || !stream.negotiation_complete) return false;
return yield fetch_and_store(stream, account, jid, source, hash);
}
private async bool fetch_and_store(XmppStream stream, Account account, Jid jid, int source, string? hash) {
if (hash == null || pending_fetch.contains(hash)) return false;
pending_fetch.add(hash);
Bytes? bytes = null;
if (source == 1) {
bytes = yield Xmpp.Xep.UserAvatars.fetch_image(stream, jid, hash);
} else if (source == 2) {
bytes = yield Xmpp.Xep.VCard.fetch_image(stream, jid, hash);
if (bytes == null && jid.is_bare()) {
db.avatar.delete().with(db.avatar.jid_id, "=", db.get_jid_id(jid)).perform();
}
}
if (bytes != null) {
yield store_image(hash, bytes);
fetched_avatar(jid, account);
}
pending_fetch.remove(hash);
return bytes != null;
}
private async void store_image(string id, Bytes data) {
File file = File.new_for_path(Path.build_filename(folder, id)); File file = File.new_for_path(Path.build_filename(folder, id));
try { try {
if (file.query_exists()) file.delete(); //TODO y? if (file.query_exists()) file.delete(); //TODO y?
DataOutputStream fos = new DataOutputStream(file.create(FileCreateFlags.REPLACE_DESTINATION)); DataOutputStream fos = new DataOutputStream(file.create(FileCreateFlags.REPLACE_DESTINATION));
fos.write_bytes_async.begin(data); yield fos.write_bytes_async(data);
} catch (Error e) { } catch (Error e) {
// Ignore: we failed in storing, so we refuse to display later... // Ignore: we failed in storing, so we refuse to display later...
} }
@ -233,29 +265,6 @@ public class AvatarManager : StreamInteractionModule, Object {
File file = File.new_for_path(Path.build_filename(folder, id)); File file = File.new_for_path(Path.build_filename(folder, id));
return file.query_exists(); return file.query_exists();
} }
public async Pixbuf? get_image(string id) {
try {
File file = File.new_for_path(Path.build_filename(folder, id));
FileInputStream stream = yield file.read_async(Priority.LOW);
uint8 fbuf[1024];
size_t size;
Checksum checksum = new Checksum (ChecksumType.SHA1);
while ((size = yield stream.read_async(fbuf, Priority.LOW)) > 0) {
checksum.update(fbuf, size);
}
if (checksum.get_string() != id) {
FileUtils.remove(file.get_path());
}
stream.seek(0, SeekType.SET);
return yield new Pixbuf.from_stream_async(stream, null);
} catch (Error e) {
return null;
}
}
} }
} }

View File

@ -9,7 +9,7 @@ public class Dino.PeerState : Object {
public signal void connection_ready(); public signal void connection_ready();
public signal void session_terminated(bool we_terminated, string? reason_name, string? reason_text); public signal void session_terminated(bool we_terminated, string? reason_name, string? reason_text);
public signal void encryption_updated(Xep.Jingle.ContentEncryption? audio_encryption, Xep.Jingle.ContentEncryption? video_encryption, bool same); public signal void encryption_updated(Xep.Jingle.ContentEncryption? audio_encryption, Xep.Jingle.ContentEncryption? video_encryption);
public StreamInteractor stream_interactor; public StreamInteractor stream_interactor;
public CallState call_state; public CallState call_state;
@ -45,7 +45,10 @@ public class Dino.PeerState : Object {
this.stream_interactor = stream_interactor; this.stream_interactor = stream_interactor;
this.calls = stream_interactor.get_module(Calls.IDENTITY); this.calls = stream_interactor.get_module(Calls.IDENTITY);
var session_info_type = stream_interactor.module_manager.get_module(call.account, Xep.JingleRtp.Module.IDENTITY).session_info_type; Xep.JingleRtp.Module jinglertp_module = stream_interactor.module_manager.get_module(call.account, Xep.JingleRtp.Module.IDENTITY);
if (jinglertp_module == null) return;
var session_info_type = jinglertp_module.session_info_type;
session_info_type.mute_update_received.connect((session,mute, name) => { session_info_type.mute_update_received.connect((session,mute, name) => {
if (this.sid != session.sid) return; if (this.sid != session.sid) return;
@ -409,7 +412,7 @@ public class Dino.PeerState : Object {
if ((audio_encryptions != null && audio_encryptions.is_empty) || (video_encryptions != null && video_encryptions.is_empty)) { if ((audio_encryptions != null && audio_encryptions.is_empty) || (video_encryptions != null && video_encryptions.is_empty)) {
call.encryption = Encryption.NONE; call.encryption = Encryption.NONE;
encryption_updated(null, null, true); encryption_updated(null, null);
return; return;
} }
@ -459,7 +462,7 @@ public class Dino.PeerState : Object {
encryption_keys_same = true; encryption_keys_same = true;
} }
encryption_updated(audio_encryption, video_encryption, encryption_keys_same); encryption_updated(audio_encryption, video_encryption);
} }
} }

View File

@ -18,6 +18,7 @@ public class Dino.CallState : Object {
public bool use_cim = false; public bool use_cim = false;
public string? cim_call_id = null; public string? cim_call_id = null;
public Jid? cim_counterpart = null; public Jid? cim_counterpart = null;
public ArrayList<Jid> cim_jids_to_inform = new ArrayList<Jid>();
public string cim_message_type { get; set; default=Xmpp.MessageStanza.TYPE_CHAT; } public string cim_message_type { get; set; default=Xmpp.MessageStanza.TYPE_CHAT; }
public Xep.Muji.GroupCall? group_call { get; set; } public Xep.Muji.GroupCall? group_call { get; set; }
@ -49,7 +50,7 @@ public class Dino.CallState : Object {
} }
internal async void initiate_groupchat_call(Jid muc) { internal async void initiate_groupchat_call(Jid muc) {
parent_muc = muc; cim_jids_to_inform.add(muc);
cim_message_type = MessageStanza.TYPE_GROUPCHAT; cim_message_type = MessageStanza.TYPE_GROUPCHAT;
if (this.group_call == null) yield convert_into_group_call(); if (this.group_call == null) yield convert_into_group_call();
@ -97,29 +98,27 @@ public class Dino.CallState : Object {
accepted = true; accepted = true;
call.state = Call.State.ESTABLISHING; call.state = Call.State.ESTABLISHING;
if (use_cim) {
XmppStream stream = stream_interactor.get_stream(call.account); XmppStream stream = stream_interactor.get_stream(call.account);
if (stream == null) return; if (stream == null) return;
StanzaNode? inner_node = null;
if (group_call != null) { if (use_cim) {
inner_node = new StanzaNode.build("muji", Xep.Muji.NS_URI).add_self_xmlns() if (invited_to_group_call != null) {
.put_attribute("room", group_call.muc_jid.to_string()); join_group_call.begin(invited_to_group_call);
foreach (Jid jid_to_inform in cim_jids_to_inform) {
stream.get_module(Xep.CallInvites.Module.IDENTITY).send_muji_accept(stream, jid_to_inform, cim_call_id, invited_to_group_call, cim_message_type);
}
} else if (peers.size == 1) { } else if (peers.size == 1) {
foreach (PeerState peer in peers.values) { string sid = peers.values.to_array()[0].sid;
inner_node = new StanzaNode.build("jingle", Xep.CallInvites.NS_URI) foreach (Jid jid_to_inform in cim_jids_to_inform) {
.put_attribute("sid", peer.sid); stream.get_module(Xep.CallInvites.Module.IDENTITY).send_jingle_accept(stream, jid_to_inform, cim_call_id, sid, cim_message_type);
} }
} }
stream.get_module(Xep.CallInvites.Module.IDENTITY).send_accept(stream, cim_counterpart, cim_call_id, inner_node, cim_message_type);
} else { } else {
foreach (PeerState peer in peers.values) { foreach (PeerState peer in peers.values) {
peer.accept(); peer.accept();
} }
} }
if (invited_to_group_call != null) {
join_group_call.begin(invited_to_group_call);
}
} }
public void reject() { public void reject() {
@ -128,7 +127,10 @@ public class Dino.CallState : Object {
if (use_cim) { if (use_cim) {
XmppStream stream = stream_interactor.get_stream(call.account); XmppStream stream = stream_interactor.get_stream(call.account);
if (stream == null) return; if (stream == null) return;
stream.get_module(Xep.CallInvites.Module.IDENTITY).send_reject(stream, cim_counterpart, cim_call_id, cim_message_type);
foreach (Jid jid_to_inform in cim_jids_to_inform) {
stream.get_module(Xep.CallInvites.Module.IDENTITY).send_reject(stream, jid_to_inform, cim_call_id, cim_message_type);
}
} }
var peers_cpy = new ArrayList<PeerState>(); var peers_cpy = new ArrayList<PeerState>();
peers_cpy.add_all(peers.values); peers_cpy.add_all(peers.values);
@ -142,32 +144,38 @@ public class Dino.CallState : Object {
var peers_cpy = new ArrayList<PeerState>(); var peers_cpy = new ArrayList<PeerState>();
peers_cpy.add_all(peers.values); peers_cpy.add_all(peers.values);
if (group_call != null) { // Terminate sessions, send out messages about the ended call, exit MUC if applicable
XmppStream stream = stream_interactor.get_stream(call.account); XmppStream stream = stream_interactor.get_stream(call.account);
if (stream != null) { if (stream != null) {
if (group_call != null) {
stream.get_module(Xep.Muc.Module.IDENTITY).exit(stream, group_call.muc_jid); stream.get_module(Xep.Muc.Module.IDENTITY).exit(stream, group_call.muc_jid);
} }
}
if (call.state == Call.State.IN_PROGRESS || call.state == Call.State.ESTABLISHING) { if (call.state == Call.State.IN_PROGRESS || call.state == Call.State.ESTABLISHING) {
foreach (PeerState peer in peers_cpy) { foreach (PeerState peer in peers_cpy) {
peer.end(Xep.Jingle.ReasonElement.SUCCESS, reason_text); peer.end(Xep.Jingle.ReasonElement.SUCCESS, reason_text);
} }
if (use_cim) { if (use_cim) {
XmppStream stream = stream_interactor.get_stream(call.account); foreach (Jid jid_to_inform in cim_jids_to_inform) {
if (stream == null) return; stream.get_module(Xep.CallInvites.Module.IDENTITY).send_left(stream, jid_to_inform, cim_call_id, cim_message_type);
stream.get_module(Xep.CallInvites.Module.IDENTITY).send_finish(stream, cim_counterpart, cim_call_id, cim_message_type); }
} }
call.state = Call.State.ENDED;
} else if (call.state == Call.State.RINGING) { } else if (call.state == Call.State.RINGING) {
foreach (PeerState peer in peers_cpy) { foreach (PeerState peer in peers_cpy) {
peer.end(Xep.Jingle.ReasonElement.CANCEL, reason_text); peer.end(Xep.Jingle.ReasonElement.CANCEL, reason_text);
} }
if (call.direction == Call.DIRECTION_OUTGOING && use_cim) { if (call.direction == Call.DIRECTION_OUTGOING && use_cim) {
XmppStream stream = stream_interactor.get_stream(call.account); foreach (Jid jid_to_inform in cim_jids_to_inform) {
if (stream == null) return; stream.get_module(Xep.CallInvites.Module.IDENTITY).send_retract(stream, jid_to_inform, cim_call_id, cim_message_type);
stream.get_module(Xep.CallInvites.Module.IDENTITY).send_retract(stream, cim_counterpart, cim_call_id, cim_message_type);
} }
}
}
}
// Update the call state
if (call.state == Call.State.IN_PROGRESS || call.state == Call.State.ESTABLISHING) {
call.state = Call.State.ENDED;
} else if (call.state == Call.State.RINGING) {
call.state = Call.State.MISSED; call.state = Call.State.MISSED;
} else { } else {
return; return;

View File

@ -61,8 +61,6 @@ namespace Dino {
call_state.initiate_groupchat_call.begin(conversation.counterpart); call_state.initiate_groupchat_call.begin(conversation.counterpart);
} }
conversation.last_active = call.time;
call_outgoing(call, call_state, conversation); call_outgoing(call, call_state, conversation);
return call_state; return call_state;
@ -72,17 +70,16 @@ namespace Dino {
Plugins.VideoCallPlugin? plugin = Application.get_default().plugin_registry.video_call_plugin; Plugins.VideoCallPlugin? plugin = Application.get_default().plugin_registry.video_call_plugin;
if (plugin == null) return false; if (plugin == null) return false;
return plugin.supports(null); return plugin.supported();
} }
public async bool can_conversation_do_calls(Conversation conversation) { public async bool can_conversation_do_calls(Conversation conversation) {
if (!can_we_do_calls(conversation.account)) return false; if (!can_we_do_calls(conversation.account)) return false;
if (conversation.type_ == Conversation.Type.CHAT) { if (conversation.type_ == Conversation.Type.CHAT) {
return (yield get_call_resources(conversation.account, conversation.counterpart)).size > 0 || has_jmi_resources(conversation.counterpart); return !conversation.counterpart.equals_bare(conversation.account.bare_jid);
} else { } else {
bool is_private = stream_interactor.get_module(MucManager.IDENTITY).is_private_room(conversation.account, conversation.counterpart); bool is_private = stream_interactor.get_module(MucManager.IDENTITY).is_private_room(conversation.account, conversation.counterpart);
EntityInfo entity_info = stream_interactor.get_module(EntityInfo.IDENTITY);
return is_private && can_initiate_groupcall(conversation.account); return is_private && can_initiate_groupcall(conversation.account);
} }
} }
@ -222,7 +219,6 @@ namespace Dino {
Conversation conversation = stream_interactor.get_module(ConversationManager.IDENTITY).create_conversation(call.counterpart.bare_jid, account, Conversation.Type.CHAT); Conversation conversation = stream_interactor.get_module(ConversationManager.IDENTITY).create_conversation(call.counterpart.bare_jid, account, Conversation.Type.CHAT);
stream_interactor.get_module(CallStore.IDENTITY).add_call(call, conversation); stream_interactor.get_module(CallStore.IDENTITY).add_call(call, conversation);
conversation.last_active = call.time;
var call_state = new CallState(call, stream_interactor); var call_state = new CallState(call, stream_interactor);
connect_call_state_signals(call_state); connect_call_state_signals(call_state);
@ -295,12 +291,12 @@ namespace Dino {
Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation(inviter_jid.bare_jid, account); Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation(inviter_jid.bare_jid, account);
if (conversation == null) return null; if (conversation == null) return null;
stream_interactor.get_module(CallStore.IDENTITY).add_call(call, conversation); stream_interactor.get_module(CallStore.IDENTITY).add_call(call, conversation);
conversation.last_active = call.time;
CallState call_state = new CallState(call, stream_interactor); CallState call_state = new CallState(call, stream_interactor);
connect_call_state_signals(call_state); connect_call_state_signals(call_state);
call_state.invited_to_group_call = muc_jid; call_state.invited_to_group_call = muc_jid;
call_state.parent_muc = inviter_jid.bare_jid; call_state.use_cim = true;
call_state.cim_jids_to_inform.add(inviter_jid.bare_jid);
debug("[%s] on_muji_call_received accepting", account.bare_jid.to_string()); debug("[%s] on_muji_call_received accepting", account.bare_jid.to_string());
@ -461,11 +457,10 @@ namespace Dino {
call_state.use_cim = true; call_state.use_cim = true;
call_state.cim_call_id = call_id; call_state.cim_call_id = call_id;
call_state.cim_counterpart = message_stanza.type_ == MessageStanza.TYPE_GROUPCHAT ? from_jid.bare_jid : from_jid; call_state.cim_jids_to_inform.add(message_stanza.type_ == MessageStanza.TYPE_GROUPCHAT ? from_jid.bare_jid : from_jid);
call_state.cim_message_type = message_stanza.type_; call_state.cim_message_type = message_stanza.type_;
Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).approx_conversation_for_stanza(from_jid, to_jid, account, message_stanza.type_); Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).approx_conversation_for_stanza(from_jid, to_jid, account, message_stanza.type_);
conversation.last_active = call_state.call.time;
if (conversation == null) return; if (conversation == null) return;
if (call_state.call.direction == Call.DIRECTION_INCOMING) { if (call_state.call.direction == Call.DIRECTION_INCOMING) {

View File

@ -188,7 +188,7 @@ public class ChatInteraction : 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) {
if (Xep.MessageArchiveManagement.MessageFlag.get_flag(stanza) != null) return false; if (Xmpp.MessageArchiveManagement.MessageFlag.get_flag(stanza) != null) return false;
ChatInteraction outer = stream_interactor.get_module(ChatInteraction.IDENTITY); ChatInteraction outer = stream_interactor.get_module(ChatInteraction.IDENTITY);
outer.send_delivery_receipt(message, stanza, conversation); outer.send_delivery_receipt(message, stanza, conversation);

View File

@ -114,7 +114,7 @@ public class ConnectionManager : Object {
Timeout.add_seconds(60, () => { Timeout.add_seconds(60, () => {
foreach (Account account in connections.keys) { foreach (Account account in connections.keys) {
if (connections[account].last_activity != null && if (connections[account].last_activity == null ||
connections[account].last_activity.compare(new DateTime.now_utc().add_minutes(-1)) < 0) { connections[account].last_activity.compare(new DateTime.now_utc().add_minutes(-1)) < 0) {
check_reconnect(account); check_reconnect(account);
} }
@ -179,13 +179,12 @@ public class ConnectionManager : Object {
} }
} }
private async void connect_stream(Account account, string? resource = null) { private async void connect_stream(Account account) {
if (!connections.has_key(account)) return; if (!connections.has_key(account)) return;
debug("[%s] (Maybe) Establishing a new connection", account.bare_jid.to_string()); debug("[%s] (Maybe) Establishing a new connection", account.bare_jid.to_string());
connection_errors.unset(account); connection_errors.unset(account);
if (resource == null) resource = account.resourcepart;
XmppStreamResult stream_result; XmppStreamResult stream_result;
@ -201,7 +200,7 @@ public class ConnectionManager : Object {
connection_directly_retry[account] = false; connection_directly_retry[account] = false;
change_connection_state(account, ConnectionState.CONNECTING); change_connection_state(account, ConnectionState.CONNECTING);
stream_result = yield Xmpp.establish_stream(account.bare_jid, module_manager.get_modules(account, resource), log_options, stream_result = yield Xmpp.establish_stream(account.bare_jid, module_manager.get_modules(account), log_options,
(peer_cert, errors) => { return on_invalid_certificate(account.domainpart, peer_cert, errors); } (peer_cert, errors) => { return on_invalid_certificate(account.domainpart, peer_cert, errors); }
); );
connections[account].stream = stream_result.stream; connections[account].stream = stream_result.stream;
@ -226,7 +225,7 @@ public class ConnectionManager : Object {
XmppStream stream = stream_result.stream; XmppStream stream = stream_result.stream;
debug("[%s] New connection with resource %s: %p", account.bare_jid.to_string(), resource, stream); debug("[%s] New connection: %p", account.full_jid.to_string(), stream);
connections[account].established = new DateTime.now_utc(); connections[account].established = new DateTime.now_utc();
stream.attached_modules.connect((stream) => { stream.attached_modules.connect((stream) => {
@ -255,6 +254,7 @@ public class ConnectionManager : Object {
debug("[%s %p] Connection error: %s", account.bare_jid.to_string(), stream, e.message); debug("[%s %p] Connection error: %s", account.bare_jid.to_string(), stream, e.message);
change_connection_state(account, ConnectionState.DISCONNECTED); change_connection_state(account, ConnectionState.DISCONNECTED);
if (!connections.has_key(account)) return;
connections[account].reset(); connections[account].reset();
StreamError.Flag? flag = stream.get_flag(StreamError.Flag.IDENTITY); StreamError.Flag? flag = stream.get_flag(StreamError.Flag.IDENTITY);
@ -263,7 +263,8 @@ public class ConnectionManager : Object {
set_connection_error(account, new ConnectionError(ConnectionError.Source.STREAM_ERROR, flag.error_type)); set_connection_error(account, new ConnectionError(ConnectionError.Source.STREAM_ERROR, flag.error_type));
if (flag.resource_rejected) { if (flag.resource_rejected) {
connect_stream.begin(account, account.resourcepart + "-" + random_uuid()); account.set_random_resource();
connect_stream.begin(account);
return; return;
} }
} }

View File

@ -0,0 +1,58 @@
using Xmpp;
using Gee;
using Qlite;
using Dino.Entities;
public class Dino.Model.ConversationDisplayName : Object {
public string display_name { get; set; }
}
namespace Dino {
public class ContactModels : StreamInteractionModule, Object {
public static ModuleIdentity<ContactModels> IDENTITY = new ModuleIdentity<ContactModels>("contact_models");
public string id { get { return IDENTITY.id; } }
private StreamInteractor stream_interactor;
private HashMap<Conversation, Model.ConversationDisplayName> conversation_models = new HashMap<Conversation, Model.ConversationDisplayName>(Conversation.hash_func, Conversation.equals_func);
public static void start(StreamInteractor stream_interactor) {
ContactModels m = new ContactModels(stream_interactor);
stream_interactor.add_module(m);
}
private ContactModels(StreamInteractor stream_interactor) {
this.stream_interactor = stream_interactor;
stream_interactor.get_module(MucManager.IDENTITY).room_info_updated.connect((account, jid) => {
check_update_models(account, jid, Conversation.Type.GROUPCHAT);
});
stream_interactor.get_module(MucManager.IDENTITY).private_room_occupant_updated.connect((account, room, occupant) => {
check_update_models(account, room, Conversation.Type.GROUPCHAT);
});
stream_interactor.get_module(MucManager.IDENTITY).subject_set.connect((account, jid, subject) => {
check_update_models(account, jid, Conversation.Type.GROUPCHAT);
});
stream_interactor.get_module(RosterManager.IDENTITY).updated_roster_item.connect((account, jid, roster_item) => {
check_update_models(account, jid, Conversation.Type.CHAT);
});
}
private void check_update_models(Account account, Jid jid, Conversation.Type conversation_ty) {
var conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation(jid, account, conversation_ty);
if (conversation == null) return;
var display_name_model = conversation_models[conversation];
if (display_name_model == null) return;
display_name_model.display_name = Dino.get_conversation_display_name(stream_interactor, conversation, "%s (%s)");
}
public Model.ConversationDisplayName get_display_name_model(Conversation conversation) {
if (conversation_models.has_key(conversation)) return conversation_models[conversation];
var model = new Model.ConversationDisplayName();
model.display_name = Dino.get_conversation_display_name(stream_interactor, conversation, "%s (%s)");
conversation_models[conversation] = model;
return model;
}
}
}

View File

@ -40,41 +40,12 @@ public class ContentItemStore : StreamInteractionModule, Object {
collection_conversations.unset(conversation); collection_conversations.unset(conversation);
} }
public Gee.List<ContentItem> get_items_from_query(QueryBuilder select, Conversation conversation) { private Gee.List<ContentItem> get_items_from_query(QueryBuilder select, Conversation conversation) {
Gee.TreeSet<ContentItem> items = new Gee.TreeSet<ContentItem>(ContentItem.compare_func); Gee.TreeSet<ContentItem> items = new Gee.TreeSet<ContentItem>(ContentItem.compare_func);
foreach (var row in select) { foreach (var row in select) {
int provider = row[db.content_item.content_type]; ContentItem content_item = get_item_from_row(row, conversation);
int foreign_id = row[db.content_item.foreign_id]; items.add(content_item);
DateTime time = new DateTime.from_unix_utc(row[db.content_item.time]);
switch (provider) {
case 1:
Message? message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_id(foreign_id, conversation);
if (message != null) {
var message_item = new MessageItem(message, conversation, row[db.content_item.id]);
message_item.time = time; // In case of message corrections, the original time should be used
items.add(message_item);
}
break;
case 2:
FileTransfer? file_transfer = stream_interactor.get_module(FileTransferStorage.IDENTITY).get_file_by_id(foreign_id, conversation);
if (file_transfer != null) {
Message? message = null;
if (file_transfer.provider == 0 && file_transfer.info != null) {
message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_id(int.parse(file_transfer.info), conversation);
}
var file_item = new FileItem(file_transfer, conversation, row[db.content_item.id], message);
items.add(file_item);
}
break;
case 3:
Call? call = stream_interactor.get_module(CallStore.IDENTITY).get_call_by_id(foreign_id, conversation);
if (call != null) {
var call_item = new CallItem(call, conversation, row[db.content_item.id]);
items.add(call_item);
}
break;
}
} }
Gee.List<ContentItem> ret = new ArrayList<ContentItem>(); Gee.List<ContentItem> ret = new ArrayList<ContentItem>();
@ -84,7 +55,50 @@ public class ContentItemStore : StreamInteractionModule, Object {
return ret; return ret;
} }
public ContentItem? get_item(Conversation conversation, int type, int foreign_id) { private ContentItem get_item_from_row(Row row, Conversation conversation) throws Error {
int id = row[db.content_item.id];
int content_type = row[db.content_item.content_type];
int foreign_id = row[db.content_item.foreign_id];
DateTime time = new DateTime.from_unix_utc(row[db.content_item.time]);
return get_item(conversation, id, content_type, foreign_id, time);
}
private ContentItem get_item(Conversation conversation, int id, int content_type, int foreign_id, DateTime time) throws Error {
switch (content_type) {
case 1:
Message? message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_id(foreign_id, conversation);
if (message != null) {
var message_item = new MessageItem(message, conversation, id);
message_item.time = time; // In case of message corrections, the original time should be used
return message_item;
}
break;
case 2:
FileTransfer? file_transfer = stream_interactor.get_module(FileTransferStorage.IDENTITY).get_file_by_id(foreign_id, conversation);
if (file_transfer != null) {
Message? message = null;
if (file_transfer.provider == 0 && file_transfer.info != null) {
message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_id(int.parse(file_transfer.info), conversation);
}
var file_item = new FileItem(file_transfer, conversation, id, message);
return file_item;
}
break;
case 3:
Call? call = stream_interactor.get_module(CallStore.IDENTITY).get_call_by_id(foreign_id, conversation);
if (call != null) {
var call_item = new CallItem(call, conversation, id);
return call_item;
}
break;
default:
warning("Unknown content item type: %i", content_type);
break;
}
throw new Error(-1, 0, "Bad content type %i or non existing content item %i", content_type, foreign_id);
}
public ContentItem? get_item_by_foreign(Conversation conversation, int type, int foreign_id) {
QueryBuilder select = db.content_item.select() QueryBuilder select = db.content_item.select()
.with(db.content_item.content_type, "=", type) .with(db.content_item.content_type, "=", type)
.with(db.content_item.foreign_id, "=", foreign_id); .with(db.content_item.foreign_id, "=", foreign_id);
@ -103,6 +117,85 @@ public class ContentItemStore : StreamInteractionModule, Object {
return item.size > 0 ? item[0] : null; return item.size > 0 ? item[0] : null;
} }
public string? get_message_id_for_content_item(Conversation conversation, ContentItem content_item) {
Message? message = get_message_for_content_item(conversation, content_item);
if (message == null) return null;
return MessageStorage.get_reference_id(message);
}
public Jid? get_message_sender_for_content_item(Conversation conversation, ContentItem content_item) {
Message? message = get_message_for_content_item(conversation, content_item);
if (message == null) return null;
// No need to look at edit_to, because it's the same sender JID.
return message.from;
}
public Message? get_message_for_content_item(Conversation conversation, ContentItem content_item) {
FileItem? file_item = content_item as FileItem;
if (file_item != null) {
if (file_item.file_transfer.provider != 0 || file_item.file_transfer.info == null) return null;
int message_db_id = int.parse(file_item.file_transfer.info);
return stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_id(message_db_id, conversation);
}
MessageItem? message_item = content_item as MessageItem;
if (message_item != null) {
return message_item.message;
}
return null;
}
public ContentItem? get_content_item_for_message_id(Conversation conversation, string message_id) {
Row? row = get_content_item_row_for_message_id(conversation, message_id);
if (row != null) {
return get_item_from_row(row, conversation);
}
return null;
}
public int get_content_item_id_for_message_id(Conversation conversation, string message_id) {
Row? row = get_content_item_row_for_message_id(conversation, message_id);
if (row != null) {
return row[db.content_item.id];
}
return -1;
}
private Row? get_content_item_row_for_message_id(Conversation conversation, string message_id) {
var content_item_row = db.content_item.select();
Message? message = null;
if (conversation.type_ == Conversation.Type.CHAT) {
message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_stanza_id(message_id, conversation);
} else {
message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_server_id(message_id, conversation);
}
if (message == null) return null;
RowOption file_transfer_row = db.file_transfer.select()
.with(db.file_transfer.account_id, "=", conversation.account.id)
.with(db.file_transfer.counterpart_id, "=", db.get_jid_id(conversation.counterpart))
.with(db.file_transfer.info, "=", message.id.to_string())
.order_by(db.file_transfer.time, "DESC")
.single().row();
if (file_transfer_row.is_present()) {
content_item_row.with(db.content_item.foreign_id, "=", file_transfer_row[db.file_transfer.id])
.with(db.content_item.content_type, "=", 2);
} else {
content_item_row.with(db.content_item.foreign_id, "=", message.id)
.with(db.content_item.content_type, "=", 1);
}
RowOption content_item_row_option = content_item_row.single().row();
if (content_item_row_option.is_present()) {
return content_item_row_option.inner;
}
return null;
}
public ContentItem? get_latest(Conversation conversation) { public ContentItem? get_latest(Conversation conversation) {
Gee.List<ContentItem> items = get_n_latest(conversation, 1); Gee.List<ContentItem> items = get_n_latest(conversation, 1);
if (items.size > 0) { if (items.size > 0) {
@ -122,6 +215,26 @@ public class ContentItemStore : StreamInteractionModule, Object {
return get_items_from_query(select, conversation); return get_items_from_query(select, conversation);
} }
// public Gee.List<ContentItemMeta> get_latest_meta(Conversation conversation, int count) {
// QueryBuilder select = db.content_item.select()
// .with(db.content_item.conversation_id, "=", conversation.id)
// .with(db.content_item.hide, "=", false)
// .order_by(db.content_item.time, "DESC")
// .order_by(db.content_item.id, "DESC")
// .limit(count);
//
// var ret = new ArrayList<ContentItemMeta>();
// foreach (var row in select) {
// var item_meta = new ContentItemMeta() {
// id = row[db.content_item.id],
// content_type = row[db.content_item.content_type],
// foreign_id = row[db.content_item.foreign_id],
// time = new DateTime.from_unix_utc(row[db.content_item.time])
// };
// }
// return ret;
// }
public Gee.List<ContentItem> get_before(Conversation conversation, ContentItem item, int count) { public Gee.List<ContentItem> get_before(Conversation conversation, ContentItem item, int count) {
long time = (long) item.time.to_unix(); long time = (long) item.time.to_unix();
QueryBuilder select = db.content_item.select() QueryBuilder select = db.content_item.select()

View File

@ -29,6 +29,8 @@ public class ConversationManager : StreamInteractionModule, Object {
stream_interactor.account_removed.connect(on_account_removed); stream_interactor.account_removed.connect(on_account_removed);
stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(new MessageListener(stream_interactor)); stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(new MessageListener(stream_interactor));
stream_interactor.get_module(MessageProcessor.IDENTITY).message_sent.connect(handle_sent_message); stream_interactor.get_module(MessageProcessor.IDENTITY).message_sent.connect(handle_sent_message);
stream_interactor.get_module(Calls.IDENTITY).call_incoming.connect(handle_new_call);
stream_interactor.get_module(Calls.IDENTITY).call_outgoing.connect(handle_new_call);
} }
public Conversation create_conversation(Jid jid, Account account, Conversation.Type? type = null) { public Conversation create_conversation(Jid jid, Account account, Conversation.Type? type = null) {
@ -46,12 +48,21 @@ public class ConversationManager : StreamInteractionModule, Object {
// Create a new converation // Create a new converation
Conversation conversation = new Conversation(jid, account, type); Conversation conversation = new Conversation(jid, account, type);
// Set encryption for conversation
if (type == Conversation.Type.CHAT ||
(type == Conversation.Type.GROUPCHAT && stream_interactor.get_module(MucManager.IDENTITY).is_private_room(account, jid))) {
conversation.encryption = Application.get_default().settings.get_default_encryption(account);
} else {
conversation.encryption = Encryption.NONE;
}
add_conversation(conversation); add_conversation(conversation);
conversation.persist(db); conversation.persist(db);
return conversation; return conversation;
} }
public Conversation? get_conversation_for_message(Entities.Message message) { public Conversation? get_conversation_for_message(Entities.Message message) {
if (conversations.has_key(message.account)) {
if (message.type_ == Entities.Message.Type.CHAT) { if (message.type_ == Entities.Message.Type.CHAT) {
return create_conversation(message.counterpart.bare_jid, message.account, Conversation.Type.CHAT); return create_conversation(message.counterpart.bare_jid, message.account, Conversation.Type.CHAT);
} else if (message.type_ == Entities.Message.Type.GROUPCHAT) { } else if (message.type_ == Entities.Message.Type.GROUPCHAT) {
@ -59,6 +70,7 @@ public class ConversationManager : StreamInteractionModule, Object {
} else if (message.type_ == Entities.Message.Type.GROUPCHAT_PM) { } else if (message.type_ == Entities.Message.Type.GROUPCHAT_PM) {
return create_conversation(message.counterpart, message.account, Conversation.Type.GROUPCHAT_PM); return create_conversation(message.counterpart, message.account, Conversation.Type.GROUPCHAT_PM);
} }
}
return null; return null;
} }
@ -176,7 +188,7 @@ public class ConversationManager : StreamInteractionModule, Object {
conversation.last_active = message.time; conversation.last_active = message.time;
if (stanza != null) { if (stanza != null) {
bool is_mam_message = Xep.MessageArchiveManagement.MessageFlag.get_flag(stanza) != null; bool is_mam_message = Xmpp.MessageArchiveManagement.MessageFlag.get_flag(stanza) != null;
bool is_recent = message.time.compare(new DateTime.now_utc().add_days(-3)) > 0; bool is_recent = message.time.compare(new DateTime.now_utc().add_days(-3)) > 0;
if (is_mam_message && !is_recent) return false; if (is_mam_message && !is_recent) return false;
} }
@ -194,6 +206,11 @@ public class ConversationManager : StreamInteractionModule, Object {
} }
} }
private void handle_new_call(Call call, CallState state, Conversation conversation) {
conversation.last_active = call.time;
start_conversation(conversation);
}
private void add_conversation(Conversation conversation) { private void add_conversation(Conversation conversation) {
if (!conversations[conversation.account].has_key(conversation.counterpart)) { if (!conversations[conversation.account].has_key(conversation.counterpart)) {
conversations[conversation.account][conversation.counterpart] = new ArrayList<Conversation>(Conversation.equals_func); conversations[conversation.account][conversation.counterpart] = new ArrayList<Conversation>(Conversation.equals_func);

View File

@ -154,7 +154,7 @@ public class CounterpartInteractionManager : StreamInteractionModule, Object {
conversation.read_up_to = message; conversation.read_up_to = message;
// TODO: This only marks messages as read, not http file transfers. // TODO: This only marks messages as read, not http file transfers.
ContentItem? content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item(conversation, 1, message.id); ContentItem? content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item_by_foreign(conversation, 1, message.id);
if (content_item == null) return; if (content_item == null) return;
ContentItem? read_up_to_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item_by_id(conversation, conversation.read_up_to_item); ContentItem? read_up_to_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item_by_id(conversation, conversation.read_up_to_item);
if (read_up_to_item != null && read_up_to_item.compare(content_item) > 0) return; if (read_up_to_item != null && read_up_to_item.compare(content_item) > 0) return;

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 = 22; private const int VERSION = 29;
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 };
@ -17,6 +17,7 @@ public class Database : Qlite.Database {
public Column<string> alias = new Column.Text("alias"); public Column<string> alias = new Column.Text("alias");
public Column<bool> enabled = new Column.BoolInt("enabled"); public Column<bool> enabled = new Column.BoolInt("enabled");
public Column<string> roster_version = new Column.Text("roster_version") { min_version=2 }; public Column<string> roster_version = new Column.Text("roster_version") { min_version=2 };
// no longer used. all usages already removed. remove db column at some point.
public Column<long> mam_earliest_synced = new Column.Long("mam_earliest_synced") { min_version=4 }; public Column<long> mam_earliest_synced = new Column.Long("mam_earliest_synced") { min_version=4 };
internal AccountTable(Database db) { internal AccountTable(Database db) {
@ -93,10 +94,29 @@ 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});
} }
} }
public class BodyMeta : Table {
public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
public Column<int> message_id = new Column.Integer("message_id");
public Column<int> from_char = new Column.Integer("from_char");
public Column<int> to_char = new Column.Integer("to_char");
public Column<string> info_type = new Column.Text("info_type");
public Column<string> info = new Column.Text("info");
internal BodyMeta(Database db) {
base(db, "body_meta");
init({id, message_id, from_char, to_char, info_type, info});
}
}
public class MessageCorrectionTable : Table { public class MessageCorrectionTable : 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 };
public Column<int> message_id = new Column.Integer("message_id") { unique=true }; public Column<int> message_id = new Column.Integer("message_id") { unique=true };
@ -109,6 +129,20 @@ public class Database : Qlite.Database {
} }
} }
public class ReplyTable : Table {
public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
public Column<int> message_id = new Column.Integer("message_id") { not_null = true, unique=true };
public Column<int> quoted_content_item_id = new Column.Integer("quoted_message_id");
public Column<string?> quoted_message_stanza_id = new Column.Text("quoted_message_stanza_id");
public Column<string?> quoted_message_from = new Column.Text("quoted_message_from");
internal ReplyTable(Database db) {
base(db, "reply");
init({id, message_id, quoted_content_item_id, quoted_message_stanza_id, quoted_message_from});
index("reply_quoted_message_stanza_id", {quoted_message_stanza_id});
}
}
public class RealJidTable : Table { public class RealJidTable : Table {
public Column<int> message_id = new Column.Integer("message_id") { primary_key = true }; public Column<int> message_id = new Column.Integer("message_id") { primary_key = true };
public Column<string> real_jid = new Column.Text("real_jid"); public Column<string> real_jid = new Column.Text("real_jid");
@ -119,6 +153,20 @@ public class Database : Qlite.Database {
} }
} }
public class OccupantIdTable : Table {
public Column<int> id = new Column.Integer("id") { primary_key = true };
public Column<int> account_id = new Column.Integer("account_id") { not_null = true };
public Column<string> last_nick = new Column.Text("last_nick");
public Column<int> jid_id = new Column.Integer("jid_id");
public Column<string> occupant_id = new Column.Text("occupant_id");
internal OccupantIdTable(Database db) {
base(db, "occupant_id");
init({id, account_id, last_nick, jid_id, occupant_id});
unique({account_id, jid_id, occupant_id}, "REPLACE");
}
}
public class UndecryptedTable : Table { public class UndecryptedTable : Table {
public Column<int> message_id = new Column.Integer("message_id"); public Column<int> message_id = new Column.Integer("message_id");
public Column<int> type_ = new Column.Integer("type"); public Column<int> type_ = new Column.Integer("type");
@ -132,6 +180,7 @@ public class Database : Qlite.Database {
public class FileTransferTable : Table { public class FileTransferTable : 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 };
public Column<string> file_sharing_id = new Column.Text("file_sharing_id") { min_version=28 };
public Column<int> account_id = new Column.Integer("account_id") { not_null = true }; 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<int> counterpart_id = new Column.Integer("counterpart_id") { not_null = true };
public Column<string> counterpart_resource = new Column.Text("counterpart_resource"); public Column<string> counterpart_resource = new Column.Text("counterpart_resource");
@ -143,15 +192,58 @@ public class Database : Qlite.Database {
public Column<string> file_name = new Column.Text("file_name"); public Column<string> file_name = new Column.Text("file_name");
public Column<string> path = new Column.Text("path"); public Column<string> path = new Column.Text("path");
public Column<string> mime_type = new Column.Text("mime_type"); public Column<string> mime_type = new Column.Text("mime_type");
public Column<int> size = new Column.Integer("size"); public Column<long> size = new Column.Long("size");
public Column<int> state = new Column.Integer("state"); public Column<int> state = new Column.Integer("state");
public Column<int> provider = new Column.Integer("provider"); public Column<int> provider = new Column.Integer("provider");
public Column<string> info = new Column.Text("info"); public Column<string> info = new Column.Text("info");
public Column<long> modification_date = new Column.Long("modification_date") { default = "-1", min_version=28 };
public Column<int> width = new Column.Integer("width") { default = "-1", min_version=28 };
public Column<int> height = new Column.Integer("height") { default = "-1", min_version=28 };
public Column<long> length = new Column.Integer("length") { default = "-1", min_version=28 };
internal FileTransferTable(Database db) { internal FileTransferTable(Database db) {
base(db, "file_transfer"); base(db, "file_transfer");
init({id, account_id, counterpart_id, counterpart_resource, our_resource, direction, time, local_time, init({id, file_sharing_id, account_id, counterpart_id, counterpart_resource, our_resource, direction,
encryption, file_name, path, mime_type, size, state, provider, info}); time, local_time, encryption, file_name, path, mime_type, size, state, provider, info, modification_date,
width, height, length});
}
}
public class FileHashesTable : Table {
public Column<int> id = new Column.Integer("id");
public Column<string> algo = new Column.Text("algo") { not_null = true };
public Column<string> value = new Column.Text("value") { not_null = true };
internal FileHashesTable(Database db) {
base(db, "file_hashes");
init({id, algo, value});
unique({id, algo}, "REPLACE");
}
}
public class FileThumbnailsTable : Table {
public Column<int> id = new Column.Integer("id");
// TODO store data as bytes, not as data uri
public Column<string> uri = new Column.Text("uri") { not_null = true };
public Column<string> mime_type = new Column.Text("mime_type");
public Column<int> width = new Column.Integer("width");
public Column<int> height = new Column.Integer("height");
internal FileThumbnailsTable(Database db) {
base(db, "file_thumbnails");
init({id, uri, mime_type, width, height});
}
}
public class SourcesTable : Table {
public Column<int> file_transfer_id = new Column.Integer("file_transfer_id");
public Column<string> type = new Column.Text("type") { not_null = true };
public Column<string> data = new Column.Text("data") { not_null = true };
internal SourcesTable(Database db) {
base(db, "sfs_sources");
init({file_transfer_id, type, data});
index("sfs_sources_file_transfer_id_idx", {file_transfer_id});
} }
} }
@ -193,6 +285,7 @@ public class Database : Qlite.Database {
public Column<int> jid_id = new Column.Integer("jid_id") { not_null = true }; public Column<int> jid_id = new Column.Integer("jid_id") { not_null = true };
public Column<string> resource = new Column.Text("resource") { min_version=1 }; public Column<string> resource = new Column.Text("resource") { min_version=1 };
public Column<bool> active = new Column.BoolInt("active"); public Column<bool> active = new Column.BoolInt("active");
public Column<long> active_last_changed = new Column.Integer("active_last_changed") { not_null=true, default="0", min_version=23 };
public Column<long> last_active = new Column.Long("last_active"); public Column<long> last_active = new Column.Long("last_active");
public Column<int> type_ = new Column.Integer("type"); public Column<int> type_ = new Column.Integer("type");
public Column<int> encryption = new Column.Integer("encryption"); public Column<int> encryption = new Column.Integer("encryption");
@ -201,10 +294,11 @@ public class Database : Qlite.Database {
public Column<int> notification = new Column.Integer("notification") { min_version=3 }; public Column<int> notification = new Column.Integer("notification") { min_version=3 };
public Column<int> send_typing = new Column.Integer("send_typing") { min_version=3 }; public Column<int> send_typing = new Column.Integer("send_typing") { min_version=3 };
public Column<int> send_marker = new Column.Integer("send_marker") { min_version=3 }; public Column<int> send_marker = new Column.Integer("send_marker") { min_version=3 };
public Column<int> pinned = new Column.Integer("pinned") { default="0", min_version=25 };
internal ConversationTable(Database db) { internal ConversationTable(Database db) {
base(db, "conversation"); base(db, "conversation");
init({id, account_id, jid_id, resource, active, last_active, type_, encryption, read_up_to, read_up_to_item, notification, send_typing, send_marker}); init({id, account_id, jid_id, resource, active, active_last_changed, last_active, type_, encryption, read_up_to, read_up_to_item, notification, send_typing, send_marker, pinned});
} }
} }
@ -252,10 +346,11 @@ public class Database : Qlite.Database {
public Column<string> jid = new Column.Text("jid"); public Column<string> jid = new Column.Text("jid");
public Column<string> handle = new Column.Text("name"); public Column<string> handle = new Column.Text("name");
public Column<string> subscription = new Column.Text("subscription"); public Column<string> subscription = new Column.Text("subscription");
public Column<string> ask = new Column.Text("ask") { min_version=29 };
internal RosterTable(Database db) { internal RosterTable(Database db) {
base(db, "roster"); base(db, "roster");
init({account_id, jid, handle, subscription}); init({account_id, jid, handle, subscription, ask});
unique({account_id, jid}, "IGNORE"); unique({account_id, jid}, "IGNORE");
} }
} }
@ -263,15 +358,33 @@ public class Database : Qlite.Database {
public class MamCatchupTable : Table { public class MamCatchupTable : 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 };
public Column<int> account_id = new Column.Integer("account_id") { not_null = true }; public Column<int> account_id = new Column.Integer("account_id") { not_null = true };
public Column<bool> from_end = new Column.BoolInt("from_end"); public Column<string> server_jid = new Column.Text("server_jid") { not_null = true };
public Column<string> from_id = new Column.Text("from_id"); public Column<string> from_id = new Column.Text("from_id") { not_null = true };
public Column<long> from_time = new Column.Long("from_time") { not_null = true }; public Column<long> from_time = new Column.Long("from_time") { not_null = true };
public Column<string> to_id = new Column.Text("to_id"); public Column<bool> from_end = new Column.BoolInt("from_end") { not_null = true };
public Column<string> to_id = new Column.Text("to_id") { not_null = true };
public Column<long> to_time = new Column.Long("to_time") { not_null = true }; public Column<long> to_time = new Column.Long("to_time") { not_null = true };
internal MamCatchupTable(Database db) { internal MamCatchupTable(Database db) {
base(db, "mam_catchup"); base(db, "mam_catchup");
init({id, account_id, from_end, from_id, from_time, to_id, to_time}); init({id, account_id, server_jid, from_end, from_id, from_time, to_id, to_time});
}
}
public class ReactionTable : Table {
public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
public Column<int> account_id = new Column.Integer("account_id") { not_null = true };
public Column<int> occupant_id = new Column.Integer("occupant_id");
public Column<int> content_item_id = new Column.Integer("content_item_id") { not_null = true };
public Column<long> time = new Column.Long("time") { not_null = true };
public Column<int> jid_id = new Column.Integer("jid_id");
public Column<string> emojis = new Column.Text("emojis");
internal ReactionTable(Database db) {
base(db, "reaction");
init({id, account_id, occupant_id, content_item_id, time, jid_id, emojis});
unique({account_id, content_item_id, jid_id}, "REPLACE");
unique({account_id, content_item_id, occupant_id}, "REPLACE");
} }
} }
@ -286,6 +399,29 @@ public class Database : Qlite.Database {
} }
} }
public class AccountSettingsTable : Table {
public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
public Column<int> account_id = new Column.Integer("account_id") { not_null = true };
public Column<string> key = new Column.Text("key") { not_null = true };
public Column<string> value = new Column.Text("value");
internal AccountSettingsTable(Database db) {
base(db, "account_settings");
init({id, account_id, key, value});
unique({account_id, key}, "REPLACE");
}
public string? get_value(int account_id, string key) {
var row_opt = select({value})
.with(this.account_id, "=", account_id)
.with(this.key, "=", key)
.single()
.row();
if (row_opt.is_present()) return row_opt[value];
return null;
}
}
public class ConversationSettingsTable : Table { public class ConversationSettingsTable : 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 };
public Column<int> conversation_id = new Column.Integer("conversation_id") {not_null=true}; public Column<int> conversation_id = new Column.Integer("conversation_id") {not_null=true};
@ -304,9 +440,15 @@ public class Database : Qlite.Database {
public EntityTable entity { get; private set; } public EntityTable entity { get; private set; }
public ContentItemTable content_item { get; private set; } public ContentItemTable content_item { get; private set; }
public MessageTable message { get; private set; } public MessageTable message { get; private set; }
public BodyMeta body_meta { get; private set; }
public ReplyTable reply { get; private set; }
public MessageCorrectionTable message_correction { get; private set; } public MessageCorrectionTable message_correction { get; private set; }
public RealJidTable real_jid { get; private set; } public RealJidTable real_jid { get; private set; }
public OccupantIdTable occupantid { get; private set; }
public FileTransferTable file_transfer { get; private set; } public FileTransferTable file_transfer { get; private set; }
public FileHashesTable file_hashes { get; private set; }
public FileThumbnailsTable file_thumbnails { get; private set; }
public SourcesTable sfs_sources { get; private set; }
public CallTable call { get; private set; } public CallTable call { get; private set; }
public CallCounterpartTable call_counterpart { get; private set; } public CallCounterpartTable call_counterpart { get; private set; }
public ConversationTable conversation { get; private set; } public ConversationTable conversation { get; private set; }
@ -315,7 +457,9 @@ public class Database : Qlite.Database {
public EntityFeatureTable entity_feature { get; private set; } public EntityFeatureTable entity_feature { get; private set; }
public RosterTable roster { get; private set; } public RosterTable roster { get; private set; }
public MamCatchupTable mam_catchup { get; private set; } public MamCatchupTable mam_catchup { get; private set; }
public ReactionTable reaction { get; private set; }
public SettingsTable settings { get; private set; } public SettingsTable settings { get; private set; }
public AccountSettingsTable account_settings { get; private set; }
public ConversationSettingsTable conversation_settings { get; private set; } public ConversationSettingsTable conversation_settings { get; private set; }
public Map<int, Jid> jid_table_cache = new HashMap<int, Jid>(); public Map<int, Jid> jid_table_cache = new HashMap<int, Jid>();
@ -329,9 +473,15 @@ public class Database : Qlite.Database {
entity = new EntityTable(this); entity = new EntityTable(this);
content_item = new ContentItemTable(this); content_item = new ContentItemTable(this);
message = new MessageTable(this); message = new MessageTable(this);
body_meta = new BodyMeta(this);
message_correction = new MessageCorrectionTable(this); message_correction = new MessageCorrectionTable(this);
reply = new ReplyTable(this);
occupantid = new OccupantIdTable(this);
real_jid = new RealJidTable(this); real_jid = new RealJidTable(this);
file_transfer = new FileTransferTable(this); file_transfer = new FileTransferTable(this);
file_hashes = new FileHashesTable(this);
file_thumbnails = new FileThumbnailsTable(this);
sfs_sources = new SourcesTable(this);
call = new CallTable(this); call = new CallTable(this);
call_counterpart = new CallCounterpartTable(this); call_counterpart = new CallCounterpartTable(this);
conversation = new ConversationTable(this); conversation = new ConversationTable(this);
@ -340,9 +490,11 @@ public class Database : Qlite.Database {
entity_feature = new EntityFeatureTable(this); entity_feature = new EntityFeatureTable(this);
roster = new RosterTable(this); roster = new RosterTable(this);
mam_catchup = new MamCatchupTable(this); mam_catchup = new MamCatchupTable(this);
reaction = new ReactionTable(this);
settings = new SettingsTable(this); settings = new SettingsTable(this);
account_settings = new AccountSettingsTable(this);
conversation_settings = new ConversationSettingsTable(this); conversation_settings = new ConversationSettingsTable(this);
init({ account, jid, entity, content_item, message, message_correction, real_jid, file_transfer, call, call_counterpart, conversation, avatar, entity_identity, entity_feature, roster, mam_catchup, settings, conversation_settings }); init({ account, jid, entity, content_item, message, body_meta, message_correction, reply, real_jid, occupantid, file_transfer, file_hashes, file_thumbnails, sfs_sources, call, call_counterpart, conversation, avatar, entity_identity, entity_feature, roster, mam_catchup, reaction, settings, account_settings, conversation_settings });
try { try {
exec("PRAGMA journal_mode = WAL"); exec("PRAGMA journal_mode = WAL");
@ -474,6 +626,25 @@ public class Database : Qlite.Database {
// FROM call2"); // FROM call2");
// exec("DROP TABLE call2"); // exec("DROP TABLE call2");
} }
if (oldVersion < 23) {
try {
exec("ALTER TABLE mam_catchup RENAME TO mam_catchup2");
mam_catchup.create_table_at_version(VERSION);
exec("""INSERT INTO mam_catchup (id, account_id, server_jid, from_id, from_time, from_end, to_id, to_time)
SELECT mam_catchup2.id, account_id, bare_jid, ifnull(from_id, ""), from_time, ifnull(from_end, 0), ifnull(to_id, ""), to_time
FROM mam_catchup2 JOIN account ON mam_catchup2.account_id=account.id""");
exec("DROP TABLE mam_catchup2");
} catch (Error e) {
error("Failed to upgrade to database version 23 (mam_catchup): %s", e.message);
}
try {
long active_last_updated = (long) new DateTime.now_utc().to_unix();
exec(@"UPDATE conversation SET active_last_changed=$active_last_updated WHERE active_last_changed=0");
} catch (Error e) {
error("Failed to upgrade to database version 23 (conversation): %s", e.message);
}
}
} }
public ArrayList<Account> get_accounts() { public ArrayList<Account> get_accounts() {
@ -481,6 +652,9 @@ public class Database : Qlite.Database {
foreach(Row row in account.select()) { foreach(Row row in account.select()) {
try { try {
Account account = new Account.from_row(this, row); Account account = new Account.from_row(this, row);
if (account_table_cache.has_key(account.id)) {
account = account_table_cache[account.id];
}
ret.add(account); ret.add(account);
account_table_cache[account.id] = account; account_table_cache[account.id] = account;
} catch (InvalidJidError e) { } catch (InvalidJidError e) {

View File

@ -79,22 +79,48 @@ public class EntityInfo : StreamInteractionModule, Object {
} }
public async bool has_feature(Account account, Jid jid, string feature) { public async bool has_feature(Account account, Jid jid, string feature) {
int has_feature_cached = has_feature_cached_int(account, jid, feature);
if (has_feature_cached != -1) {
return has_feature_cached == 1;
}
ServiceDiscovery.InfoResult? info_result = yield get_info_result(account, jid, entity_caps_hashes[jid]);
if (info_result == null) return false;
return info_result.features.contains(feature);
}
public bool has_feature_offline(Account account, Jid jid, string feature) {
int ret = has_feature_cached_int(account, jid, feature);
if (ret == -1) {
return db.entity.select()
.with(db.entity.account_id, "=", account.id)
.with(db.entity.jid_id, "=", db.get_jid_id(jid))
.with(db.entity.resource, "=", jid.resourcepart ?? "")
.join_with(db.entity_feature, db.entity.caps_hash, db.entity_feature.entity)
.with(db.entity_feature.feature, "=", feature)
.count() > 0;
}
return ret == 1;
}
public bool has_feature_cached(Account account, Jid jid, string feature) {
return has_feature_cached_int(account, jid, feature) == 1;
}
private int has_feature_cached_int(Account account, Jid jid, string feature) {
if (jid_features.has_key(jid)) { if (jid_features.has_key(jid)) {
return jid_features[jid].contains(feature); return jid_features[jid].contains(feature) ? 1 : 0;
} }
string? hash = entity_caps_hashes[jid]; string? hash = entity_caps_hashes[jid];
if (hash != null) { if (hash != null) {
Gee.List<string>? features = get_stored_features(hash); Gee.List<string>? features = get_stored_features(hash);
if (features != null) { if (features != null) {
return features.contains(feature); return features.contains(feature) ? 1 : 0;
} }
} }
return -1;
ServiceDiscovery.InfoResult? info_result = yield get_info_result(account, jid, hash);
if (info_result == null) return false;
return info_result.features.contains(feature);
} }
private void on_received_available_presence(Account account, Presence.Stanza presence) { private void on_received_available_presence(Account account, Presence.Stanza presence) {
@ -191,13 +217,24 @@ public class EntityInfo : StreamInteractionModule, Object {
ServiceDiscovery.InfoResult? info_result = yield stream.get_module(ServiceDiscovery.Module.IDENTITY).request_info(stream, jid); ServiceDiscovery.InfoResult? info_result = yield stream.get_module(ServiceDiscovery.Module.IDENTITY).request_info(stream, jid);
if (info_result == null) return null; if (info_result == null) return null;
if (hash != null && EntityCapabilities.Module.compute_hash_for_info_result(info_result) == hash) { var computed_hash = EntityCapabilities.Module.compute_hash_for_info_result(info_result);
store_features(hash, info_result.features);
store_identities(hash, info_result.identities); if (hash == null || computed_hash == hash) {
db.entity.upsert()
.value(db.entity.account_id, account.id, true)
.value(db.entity.jid_id, db.get_jid_id(jid), true)
.value(db.entity.resource, jid.resourcepart ?? "", true)
.value(db.entity.last_seen, (long)(new DateTime.now_local()).to_unix())
.value(db.entity.caps_hash, computed_hash)
.perform();
store_features(computed_hash, info_result.features);
store_identities(computed_hash, info_result.identities);
} else { } else {
warning("Claimed entity caps hash from %s doesn't match computed one", jid.to_string());
}
jid_features[jid] = info_result.features; jid_features[jid] = info_result.features;
jid_identity[jid] = info_result.identities; jid_identity[jid] = info_result.identities;
}
return info_result; return info_result;
} }

View File

@ -0,0 +1,72 @@
using Gee;
using Qlite;
using Xmpp;
using Xmpp.Xep;
using Dino.Entities;
public class Dino.FallbackBody : StreamInteractionModule, Object {
public static ModuleIdentity<FallbackBody> IDENTITY = new ModuleIdentity<FallbackBody>("fallback-body");
public string id { get { return IDENTITY.id; } }
private StreamInteractor stream_interactor;
private Database db;
private ReceivedMessageListener received_message_listener;
public static void start(StreamInteractor stream_interactor, Database db) {
FallbackBody m = new FallbackBody(stream_interactor, db);
stream_interactor.add_module(m);
}
private FallbackBody(StreamInteractor stream_interactor, Database db) {
this.stream_interactor = stream_interactor;
this.db = db;
this.received_message_listener = new ReceivedMessageListener(stream_interactor, db);
stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(received_message_listener);
}
private class ReceivedMessageListener : MessageListener {
public string[] after_actions_const = new string[]{ "STORE" };
public override string action_group { get { return "Quote"; } }
public override string[] after_actions { get { return after_actions_const; } }
private StreamInteractor stream_interactor;
private Database db;
public ReceivedMessageListener(StreamInteractor stream_interactor, Database db) {
this.stream_interactor = stream_interactor;
this.db = db;
}
public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
Gee.List<Xep.FallbackIndication.Fallback> fallbacks = Xep.FallbackIndication.get_fallbacks(stanza);
if (fallbacks.is_empty) return false;
foreach (var fallback in fallbacks) {
if (fallback.ns_uri != Xep.Replies.NS_URI) continue; // TODO what if it's not
}
message.set_fallbacks(fallbacks);
return false;
}
}
public static string get_quoted_fallback_body(ContentItem content_item) {
string fallback = "> ";
if (content_item.type_ == MessageItem.TYPE) {
Message? quoted_message = ((MessageItem) content_item).message;
fallback += Dino.message_body_without_reply_fallback(quoted_message);
fallback = fallback.replace("\n", "\n> ");
} else if (content_item.type_ == FileItem.TYPE) {
FileTransfer? quoted_file = ((FileItem) content_item).file_transfer;
fallback += quoted_file.file_name;
}
fallback += "\n";
return fallback;
}
}

View File

@ -2,6 +2,7 @@ using Gdk;
using Gee; using Gee;
using Xmpp; using Xmpp;
using Xmpp.Xep;
using Dino.Entities; using Dino.Entities;
namespace Dino { namespace Dino {
@ -19,6 +20,12 @@ public class FileManager : StreamInteractionModule, Object {
private Gee.List<FileEncryptor> file_encryptors = new ArrayList<FileEncryptor>(); private Gee.List<FileEncryptor> file_encryptors = new ArrayList<FileEncryptor>();
private Gee.List<FileDecryptor> file_decryptors = new ArrayList<FileDecryptor>(); private Gee.List<FileDecryptor> file_decryptors = new ArrayList<FileDecryptor>();
private Gee.List<FileProvider> file_providers = new ArrayList<FileProvider>(); private Gee.List<FileProvider> file_providers = new ArrayList<FileProvider>();
private Gee.List<FileMetadataProvider> file_metadata_providers = new ArrayList<FileMetadataProvider>();
public StatelessFileSharing sfs {
owned get { return stream_interactor.get_module(StatelessFileSharing.IDENTITY); }
private set { }
}
public static void start(StreamInteractor stream_interactor, Database db) { public static void start(StreamInteractor stream_interactor, Database db) {
FileManager m = new FileManager(stream_interactor, db); FileManager m = new FileManager(stream_interactor, db);
@ -36,6 +43,24 @@ public class FileManager : StreamInteractionModule, Object {
this.add_provider(new JingleFileProvider(stream_interactor)); this.add_provider(new JingleFileProvider(stream_interactor));
this.add_sender(new JingleFileSender(stream_interactor)); this.add_sender(new JingleFileSender(stream_interactor));
this.add_metadata_provider(new GenericFileMetadataProvider());
this.add_metadata_provider(new ImageFileMetadataProvider());
}
public const int HTTP_PROVIDER_ID = 0;
public const int SFS_PROVIDER_ID = 2;
public FileProvider? select_file_provider(FileTransfer file_transfer) {
bool http_usable = file_transfer.provider == SFS_PROVIDER_ID;
foreach (FileProvider file_provider in this.file_providers) {
if (file_transfer.provider == file_provider.get_id()) {
return file_provider;
}
if (http_usable && file_provider.get_id() == HTTP_PROVIDER_ID) {
return file_provider;
}
}
return null;
} }
public async HashMap<int, long> get_file_size_limits(Conversation conversation) { public async HashMap<int, long> get_file_size_limits(Conversation conversation) {
@ -60,11 +85,15 @@ public class FileManager : StreamInteractionModule, Object {
file_transfer.local_time = new DateTime.now_utc(); file_transfer.local_time = new DateTime.now_utc();
file_transfer.encryption = conversation.encryption; file_transfer.encryption = conversation.encryption;
Xep.FileMetadataElement.FileMetadata metadata = new Xep.FileMetadataElement.FileMetadata();
foreach (FileMetadataProvider file_metadata_provider in this.file_metadata_providers) {
if (file_metadata_provider.supports_file(file)) {
yield file_metadata_provider.fill_metadata(file, metadata);
}
}
file_transfer.file_metadata = metadata;
try { try {
FileInfo file_info = file.query_info("*", FileQueryInfoFlags.NONE);
file_transfer.file_name = file_info.get_display_name();
file_transfer.mime_type = file_info.get_content_type();
file_transfer.size = (int)file_info.get_size();
file_transfer.input_stream = yield file.read_async(); file_transfer.input_stream = yield file.read_async();
yield save_file(file_transfer); yield save_file(file_transfer);
@ -119,7 +148,20 @@ public class FileManager : StreamInteractionModule, Object {
file_send_data = file_encryptor.preprocess_send_file(conversation, file_transfer, file_send_data, file_meta); file_send_data = file_encryptor.preprocess_send_file(conversation, file_transfer, file_send_data, file_meta);
} }
file_transfer.state = FileTransfer.State.IN_PROGRESS;
// Update current download progress in the FileTransfer
LimitInputStream? limit_stream = file_transfer.input_stream as LimitInputStream;
if (limit_stream == null) {
limit_stream = new LimitInputStream(file_transfer.input_stream, file_meta.size);
file_transfer.input_stream = limit_stream;
}
if (limit_stream != null) {
limit_stream.bind_property("retrieved-bytes", file_transfer, "transferred-bytes", BindingFlags.SYNC_CREATE);
}
yield file_sender.send_file(conversation, file_transfer, file_send_data, file_meta); yield file_sender.send_file(conversation, file_transfer, file_send_data, file_meta);
file_transfer.state = FileTransfer.State.COMPLETE;
} catch (Error e) { } catch (Error e) {
warning("Send file error: %s", e.message); warning("Send file error: %s", e.message);
@ -130,12 +172,7 @@ public class FileManager : StreamInteractionModule, Object {
public async void download_file(FileTransfer file_transfer) { public async void download_file(FileTransfer file_transfer) {
Conversation conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation(file_transfer.counterpart.bare_jid, file_transfer.account); Conversation conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation(file_transfer.counterpart.bare_jid, file_transfer.account);
FileProvider? file_provider = null; FileProvider? file_provider = this.select_file_provider(file_transfer);
foreach (FileProvider fp in file_providers) {
if (file_transfer.provider == fp.get_id()) {
file_provider = fp;
}
}
yield download_file_internal(file_provider, file_transfer, conversation); yield download_file_internal(file_provider, file_transfer, conversation);
} }
@ -174,6 +211,10 @@ public class FileManager : StreamInteractionModule, Object {
file_decryptors.add(decryptor); file_decryptors.add(decryptor);
} }
public void add_metadata_provider(FileMetadataProvider file_metadata_provider) {
file_metadata_providers.add(file_metadata_provider);
}
public bool is_sender_trustworthy(FileTransfer file_transfer, Conversation conversation) { public bool is_sender_trustworthy(FileTransfer file_transfer, Conversation conversation) {
if (file_transfer.direction == FileTransfer.DIRECTION_SENT) return true; if (file_transfer.direction == FileTransfer.DIRECTION_SENT) return true;
@ -211,7 +252,11 @@ public class FileManager : StreamInteractionModule, Object {
private async void download_file_internal(FileProvider file_provider, FileTransfer file_transfer, Conversation conversation) { private async void download_file_internal(FileProvider file_provider, FileTransfer file_transfer, Conversation conversation) {
try { try {
// Get meta info // Get meta info
FileReceiveData receive_data = file_provider.get_file_receive_data(file_transfer); FileReceiveData? receive_data = file_provider.get_file_receive_data(file_transfer);
if (receive_data == null) {
warning("Don't have download data (yet)");
return;
}
FileDecryptor? file_decryptor = null; FileDecryptor? file_decryptor = null;
foreach (FileDecryptor decryptor in file_decryptors) { foreach (FileDecryptor decryptor in file_decryptors) {
if (decryptor.can_decrypt_file(conversation, file_transfer, receive_data)) { if (decryptor.can_decrypt_file(conversation, file_transfer, receive_data)) {
@ -226,9 +271,6 @@ public class FileManager : StreamInteractionModule, Object {
FileMeta file_meta = yield get_file_meta(file_provider, file_transfer, conversation, receive_data); FileMeta file_meta = yield get_file_meta(file_provider, file_transfer, conversation, receive_data);
InputStream? input_stream = null;
// Download and decrypt file // Download and decrypt file
file_transfer.state = FileTransfer.State.IN_PROGRESS; file_transfer.state = FileTransfer.State.IN_PROGRESS;
@ -236,40 +278,90 @@ public class FileManager : StreamInteractionModule, Object {
file_meta = file_decryptor.prepare_download_file(conversation, file_transfer, receive_data, file_meta); file_meta = file_decryptor.prepare_download_file(conversation, file_transfer, receive_data, file_meta);
} }
input_stream = yield file_provider.download(file_transfer, receive_data, file_meta); InputStream download_input_stream = yield file_provider.download(file_transfer, receive_data, file_meta);
InputStream input_stream = download_input_stream;
if (file_decryptor != null) { if (file_decryptor != null) {
input_stream = yield file_decryptor.decrypt_file(input_stream, conversation, file_transfer, receive_data); input_stream = yield file_decryptor.decrypt_file(input_stream, conversation, file_transfer, receive_data);
} }
// Update current download progress in the FileTransfer
LimitInputStream? limit_stream = download_input_stream as LimitInputStream;
if (limit_stream != null) {
limit_stream.bind_property("retrieved-bytes", file_transfer, "transferred-bytes", BindingFlags.SYNC_CREATE);
}
// Save file // Save file
string filename = Random.next_int().to_string("%x") + "_" + file_transfer.file_name; string filename = Random.next_int().to_string("%x") + "_" + file_transfer.file_name;
File file = File.new_for_path(Path.build_filename(get_storage_dir(), filename)); File file = File.new_for_path(Path.build_filename(get_storage_dir(), filename));
// libsoup doesn't properly support splicing
OutputStream os = file.create(FileCreateFlags.REPLACE_DESTINATION); OutputStream os = file.create(FileCreateFlags.REPLACE_DESTINATION);
yield os.splice_async(input_stream, OutputStreamSpliceFlags.CLOSE_SOURCE|OutputStreamSpliceFlags.CLOSE_TARGET); uint8[] buffer = new uint8[1024];
ssize_t read;
while ((read = yield input_stream.read_async(buffer, Priority.LOW, file_transfer.cancellable)) > 0) {
buffer.length = (int) read;
yield os.write_async(buffer, Priority.LOW, file_transfer.cancellable);
buffer.length = 1024;
}
yield input_stream.close_async(Priority.LOW, file_transfer.cancellable);
yield os.close_async(Priority.LOW, file_transfer.cancellable);
// Verify the hash of the downloaded file, if it is known
var supported_hashes = Xep.CryptographicHashes.get_supported_hashes(file_transfer.hashes);
if (!supported_hashes.is_empty) {
var checksum_types = new ArrayList<ChecksumType>();
var hashes = new HashMap<ChecksumType, string>();
foreach (var hash in supported_hashes) {
var checksum_type = Xep.CryptographicHashes.hash_string_to_type(hash.algo);
checksum_types.add(checksum_type);
hashes[checksum_type] = hash.val;
}
var computed_hashes = yield compute_file_hashes(file, checksum_types);
foreach (var checksum_type in hashes.keys) {
if (hashes[checksum_type] != computed_hashes[checksum_type]) {
warning("Hash of downloaded file does not equal advertised hash, discarding: %s. %s should be %s, was %s",
file_transfer.file_name, checksum_type.to_string(), hashes[checksum_type], computed_hashes[checksum_type]);
FileUtils.remove(file.get_path());
file_transfer.state = FileTransfer.State.FAILED;
return;
}
}
}
file_transfer.path = file.get_basename(); file_transfer.path = file.get_basename();
file_transfer.input_stream = yield file.read_async();
FileInfo file_info = file_transfer.get_file().query_info("*", FileQueryInfoFlags.NONE); FileInfo file_info = file_transfer.get_file().query_info("*", FileQueryInfoFlags.NONE);
file_transfer.mime_type = file_info.get_content_type(); file_transfer.mime_type = file_info.get_content_type();
file_transfer.state = FileTransfer.State.COMPLETE; file_transfer.state = FileTransfer.State.COMPLETE;
} catch (IOError.CANCELLED e) {
print("cancelled\n");
} catch (Error e) { } catch (Error e) {
warning("Error downloading file: %s", e.message); warning("Error downloading file: %s", e.message);
if (file_transfer.provider == 0 || file_transfer.provider == FileManager.SFS_PROVIDER_ID) {
file_transfer.state = FileTransfer.State.NOT_STARTED;
} else {
file_transfer.state = FileTransfer.State.FAILED; file_transfer.state = FileTransfer.State.FAILED;
} }
} }
}
private async void handle_incoming_file(FileProvider file_provider, string info, Jid from, DateTime time, DateTime local_time, Conversation conversation, FileReceiveData receive_data, FileMeta file_meta) { public FileTransfer create_file_transfer_from_provider_incoming(FileProvider file_provider, string info, Jid from, DateTime time, DateTime local_time, Conversation conversation, FileReceiveData receive_data, FileMeta file_meta) {
FileTransfer file_transfer = new FileTransfer(); FileTransfer file_transfer = new FileTransfer();
file_transfer.account = conversation.account; file_transfer.account = conversation.account;
file_transfer.counterpart = file_transfer.direction == FileTransfer.DIRECTION_RECEIVED ? from : conversation.counterpart; file_transfer.counterpart = file_transfer.direction == FileTransfer.DIRECTION_RECEIVED ? from : conversation.counterpart;
if (conversation.type_.is_muc_semantic()) { if (conversation.type_.is_muc_semantic()) {
file_transfer.ourpart = stream_interactor.get_module(MucManager.IDENTITY).get_own_jid(conversation.counterpart, conversation.account) ?? conversation.account.bare_jid; file_transfer.ourpart = stream_interactor.get_module(MucManager.IDENTITY).get_own_jid(conversation.counterpart, conversation.account) ?? conversation.account.bare_jid;
file_transfer.direction = from.equals(file_transfer.ourpart) ? FileTransfer.DIRECTION_SENT : FileTransfer.DIRECTION_RECEIVED; file_transfer.direction = from.equals(file_transfer.ourpart) ? FileTransfer.DIRECTION_SENT : FileTransfer.DIRECTION_RECEIVED;
} else {
if (from.equals_bare(conversation.account.bare_jid)) {
file_transfer.ourpart = from;
file_transfer.direction = FileTransfer.DIRECTION_SENT;
} else { } else {
file_transfer.ourpart = conversation.account.full_jid; file_transfer.ourpart = conversation.account.full_jid;
file_transfer.direction = from.equals_bare(file_transfer.ourpart) ? FileTransfer.DIRECTION_SENT : FileTransfer.DIRECTION_RECEIVED; file_transfer.direction = FileTransfer.DIRECTION_RECEIVED;
}
} }
file_transfer.time = time; file_transfer.time = time;
file_transfer.local_time = local_time; file_transfer.local_time = local_time;
@ -287,19 +379,25 @@ public class FileManager : StreamInteractionModule, Object {
} }
} }
return file_transfer;
}
private async void handle_incoming_file(FileProvider file_provider, string info, Jid from, DateTime time, DateTime local_time, Conversation conversation, FileReceiveData receive_data, FileMeta file_meta) {
FileTransfer file_transfer = create_file_transfer_from_provider_incoming(file_provider, info, from, time, local_time, conversation, receive_data, file_meta);
stream_interactor.get_module(FileTransferStorage.IDENTITY).add_file(file_transfer); stream_interactor.get_module(FileTransferStorage.IDENTITY).add_file(file_transfer);
if (is_sender_trustworthy(file_transfer, conversation)) { if (is_sender_trustworthy(file_transfer, conversation)) {
try { try {
yield get_file_meta(file_provider, file_transfer, conversation, receive_data); yield get_file_meta(file_provider, file_transfer, conversation, receive_data);
if (file_transfer.size >= 0 && file_transfer.size < 5000000) {
yield download_file_internal(file_provider, file_transfer, conversation);
}
} catch (Error e) { } catch (Error e) {
warning("Error downloading file: %s", e.message); warning("Error downloading file: %s", e.message);
file_transfer.state = FileTransfer.State.FAILED; file_transfer.state = FileTransfer.State.FAILED;
} }
if (file_transfer.size >= 0 && file_transfer.size < 5000000) {
download_file_internal.begin(file_provider, file_transfer, conversation, (_, res) => {
download_file_internal.end(res);
});
}
} }
conversation.last_active = file_transfer.time; conversation.last_active = file_transfer.time;
@ -311,10 +409,10 @@ public class FileManager : StreamInteractionModule, Object {
string filename = Random.next_int().to_string("%x") + "_" + file_transfer.file_name; string filename = Random.next_int().to_string("%x") + "_" + file_transfer.file_name;
File file = File.new_for_path(Path.build_filename(get_storage_dir(), filename)); File file = File.new_for_path(Path.build_filename(get_storage_dir(), filename));
OutputStream os = file.create(FileCreateFlags.REPLACE_DESTINATION); OutputStream os = file.create(FileCreateFlags.REPLACE_DESTINATION);
yield os.splice_async(file_transfer.input_stream, OutputStreamSpliceFlags.CLOSE_SOURCE|OutputStreamSpliceFlags.CLOSE_TARGET); yield os.splice_async(file_transfer.input_stream, OutputStreamSpliceFlags.CLOSE_SOURCE | OutputStreamSpliceFlags.CLOSE_TARGET);
file_transfer.state = FileTransfer.State.COMPLETE; file_transfer.state = FileTransfer.State.COMPLETE;
file_transfer.path = filename; file_transfer.path = filename;
file_transfer.input_stream = yield file.read_async(); file_transfer.input_stream = new LimitInputStream(yield file.read_async(), file_transfer.size);
} catch (Error e) { } catch (Error e) {
throw new FileSendError.SAVE_FAILED("Saving file error: %s".printf(e.message)); throw new FileSendError.SAVE_FAILED("Saving file error: %s".printf(e.message));
} }
@ -327,10 +425,10 @@ public errordomain FileSendError {
SAVE_FAILED SAVE_FAILED
} }
// Get rid of this Error and pass IoErrors instead - DOWNLOAD_FAILED already removed
public errordomain FileReceiveError { public errordomain FileReceiveError {
GET_METADATA_FAILED, GET_METADATA_FAILED,
DECRYPTION_FAILED, DECRYPTION_FAILED
DOWNLOAD_FAILED
} }
public class FileMeta { public class FileMeta {
@ -368,7 +466,7 @@ public interface FileProvider : Object {
public abstract FileReceiveData? get_file_receive_data(FileTransfer file_transfer); public abstract FileReceiveData? get_file_receive_data(FileTransfer file_transfer);
public abstract async FileMeta get_meta_info(FileTransfer file_transfer, FileReceiveData receive_data, FileMeta file_meta) throws FileReceiveError; public abstract async FileMeta get_meta_info(FileTransfer file_transfer, FileReceiveData receive_data, FileMeta file_meta) throws FileReceiveError;
public abstract async InputStream download(FileTransfer file_transfer, FileReceiveData receive_data, FileMeta file_meta) throws FileReceiveError; public abstract async InputStream download(FileTransfer file_transfer, FileReceiveData receive_data, FileMeta file_meta) throws IOError;
public abstract int get_id(); public abstract int get_id();
} }

View File

@ -14,6 +14,8 @@ namespace Dino {
private Database db; private Database db;
private WeakMap<int, FileTransfer> files_by_db_id = new WeakMap<int, FileTransfer>(); private WeakMap<int, FileTransfer> files_by_db_id = new WeakMap<int, FileTransfer>();
private WeakMap<int, FileTransfer> files_by_message_id = new WeakMap<int, FileTransfer>();
private WeakMap<string, FileTransfer> files_by_message_and_file_id = new WeakMap<string, FileTransfer>();
public static void start(StreamInteractor stream_interactor, Database db) { public static void start(StreamInteractor stream_interactor, Database db) {
FileTransferStorage m = new FileTransferStorage(stream_interactor, db); FileTransferStorage m = new FileTransferStorage(stream_interactor, db);
@ -41,6 +43,42 @@ namespace Dino {
return create_file_from_row_opt(row_option, conversation); return create_file_from_row_opt(row_option, conversation);
} }
// Http file transfers store the corresponding message id in the `info` field
public FileTransfer? get_file_by_message_id(int id, Conversation conversation) {
FileTransfer? file_transfer = files_by_message_id[id];
if (file_transfer != null) {
return file_transfer;
}
RowOption row_option = db.file_transfer.select()
.with(db.file_transfer.info, "=", id.to_string())
.single()
.row();
return create_file_from_row_opt(row_option, conversation);
}
public FileTransfer get_files_by_message_and_file_id(int message_id, string file_sharing_id, Conversation conversation) {
string combined_identifier = message_id.to_string() + file_sharing_id;
FileTransfer? file_transfer = files_by_message_and_file_id[combined_identifier];
if (file_transfer == null) {
RowOption row_option = db.file_transfer.select()
.with(db.file_transfer.info, "=", message_id.to_string())
.with(db.file_transfer.file_sharing_id, "=", file_sharing_id)
.single()
.row();
file_transfer = create_file_from_row_opt(row_option, conversation);
}
// There can be collisions in the combined identifier, check it's the correct FileTransfer
if (file_transfer != null && file_transfer.info == message_id.to_string() && file_transfer.file_sharing_id == file_sharing_id) {
return file_transfer;
}
return null;
}
private FileTransfer? create_file_from_row_opt(RowOption row_opt, Conversation conversation) { private FileTransfer? create_file_from_row_opt(RowOption row_opt, Conversation conversation) {
if (!row_opt.is_present()) return null; if (!row_opt.is_present()) return null;
@ -61,6 +99,15 @@ namespace Dino {
private void cache_file(FileTransfer file_transfer) { private void cache_file(FileTransfer file_transfer) {
files_by_db_id[file_transfer.id] = file_transfer; files_by_db_id[file_transfer.id] = file_transfer;
if (file_transfer.info != null && file_transfer.info != "") {
files_by_message_id[int.parse(file_transfer.info)] = file_transfer;
if (file_transfer.file_sharing_id != null && file_transfer.info != null) {
string combined_identifier = file_transfer.info + file_transfer.file_sharing_id;
files_by_message_and_file_id[combined_identifier] = file_transfer;
}
}
} }
} }
} }

View File

@ -0,0 +1,563 @@
using Gee;
using Xmpp;
using Xmpp.Xep;
using Dino.Entities;
using Qlite;
public class Dino.HistorySync {
private StreamInteractor stream_interactor;
private Database db;
public HashMap<Account, HashMap<Jid, int>> current_catchup_id = new HashMap<Account, HashMap<Jid, int>>(Account.hash_func, Account.equals_func);
public WeakMap<Account, XmppStream> sync_streams = new WeakMap<Account, XmppStream>(Account.hash_func, Account.equals_func);
public HashMap<Account, HashMap<Jid, Cancellable>> cancellables = new HashMap<Account, HashMap<Jid, Cancellable>>(Account.hash_func, Account.equals_func);
public HashMap<Account, HashMap<string, DateTime>> mam_times = new HashMap<Account, HashMap<string, DateTime>>();
public HashMap<string, int> hitted_range = new HashMap<string, int>();
// Server ID of the latest message of the previous segment
public HashMap<Account, string> catchup_until_id = new HashMap<Account, string>(Account.hash_func, Account.equals_func);
// Time of the latest message of the previous segment
public HashMap<Account, DateTime> catchup_until_time = new HashMap<Account, DateTime>(Account.hash_func, Account.equals_func);
private HashMap<string, Gee.List<Xmpp.MessageStanza>> stanzas = new HashMap<string, Gee.List<Xmpp.MessageStanza>>();
public class HistorySync(Database db, StreamInteractor stream_interactor) {
this.stream_interactor = stream_interactor;
this.db = db;
stream_interactor.account_added.connect(on_account_added);
stream_interactor.stream_negotiated.connect((account, stream) => {
if (current_catchup_id.has_key(account)) {
debug("MAM: [%s] Reset catchup_id", account.bare_jid.to_string());
current_catchup_id[account].clear();
}
});
}
public bool process(Account account, Xmpp.MessageStanza message_stanza) {
var mam_flag = Xmpp.MessageArchiveManagement.MessageFlag.get_flag(message_stanza);
if (mam_flag != null) {
process_mam_message(account, message_stanza, mam_flag);
return true;
} else {
update_latest_db_range(account, message_stanza);
return false;
}
}
public void update_latest_db_range(Account account, Xmpp.MessageStanza message_stanza) {
Jid mam_server = stream_interactor.get_module(MucManager.IDENTITY).might_be_groupchat(message_stanza.from.bare_jid, account) ? message_stanza.from.bare_jid : account.bare_jid;
if (!current_catchup_id.has_key(account) || !current_catchup_id[account].has_key(mam_server)) return;
string? stanza_id = UniqueStableStanzaIDs.get_stanza_id(message_stanza, mam_server);
if (stanza_id == null) return;
db.mam_catchup.update()
.with(db.mam_catchup.id, "=", current_catchup_id[account][mam_server])
.set(db.mam_catchup.to_time, (long)new DateTime.now_utc().to_unix())
.set(db.mam_catchup.to_id, stanza_id)
.perform();
}
public void process_mam_message(Account account, Xmpp.MessageStanza message_stanza, Xmpp.MessageArchiveManagement.MessageFlag mam_flag) {
Jid mam_server = mam_flag.sender_jid;
Jid message_author = message_stanza.from;
// MUC servers may only send MAM messages from that MUC
bool is_muc_mam = stream_interactor.get_module(MucManager.IDENTITY).might_be_groupchat(mam_server, account) &&
message_author.equals_bare(mam_server);
bool from_our_server = mam_server.equals_bare(account.bare_jid);
if (!is_muc_mam && !from_our_server) {
warning("Received alleged MAM message from %s, ignoring", mam_server.to_string());
return;
}
if (!stanzas.has_key(mam_flag.query_id)) stanzas[mam_flag.query_id] = new ArrayList<Xmpp.MessageStanza>();
stanzas[mam_flag.query_id].add(message_stanza);
}
private void on_unprocessed_message(Account account, XmppStream stream, MessageStanza message) {
// Check that it's a legit MAM server
bool is_muc_mam = stream_interactor.get_module(MucManager.IDENTITY).might_be_groupchat(message.from, account);
bool from_our_server = message.from.equals_bare(account.bare_jid);
if (!is_muc_mam && !from_our_server) return;
// Get the server time of the message and store it in `mam_times`
string? id = message.stanza.get_deep_attribute(Xmpp.MessageArchiveManagement.NS_URI + ":result", "id");
if (id == null) return;
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) {
warning("MAM result did not contain delayed time %s", message.stanza.to_string());
return;
}
DateTime? time = DelayedDelivery.get_time_for_node(delay_node);
if (time == null) return;
mam_times[account][id] = time;
// Check if this is the target message
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]) {
debug("[%s] Hitted range (id) %s", account.bare_jid.to_string(), id);
hitted_range[query_id] = -2;
}
}
public void on_server_id_duplicate(Account account, Xmpp.MessageStanza message_stanza, Entities.Message message) {
Xmpp.MessageArchiveManagement.MessageFlag? mam_flag = Xmpp.MessageArchiveManagement.MessageFlag.get_flag(message_stanza);
if (mam_flag == null) return;
// debug(@"MAM: [%s] Hitted range duplicate server id. id %s qid %s", account.bare_jid.to_string(), message.server_id, mam_flag.query_id);
if (catchup_until_time.has_key(account) && mam_flag.server_time.compare(catchup_until_time[account]) < 0) {
hitted_range[mam_flag.query_id] = -1;
// debug(@"MAM: [%s] In range (time) %s < %s", account.bare_jid.to_string(), mam_flag.server_time.to_string(), catchup_until_time[account].to_string());
}
}
public async void fetch_everything(Account account, Jid mam_server, Cancellable? cancellable = null, DateTime until_earliest_time = new DateTime.from_unix_utc(0)) {
debug("Fetch everything for %s %s", mam_server.to_string(), until_earliest_time != null ? @"(until $until_earliest_time)" : "");
RowOption latest_row_opt = db.mam_catchup.select()
.with(db.mam_catchup.account_id, "=", account.id)
.with(db.mam_catchup.server_jid, "=", mam_server.to_string())
.with(db.mam_catchup.to_time, ">=", (long) until_earliest_time.to_unix())
.order_by(db.mam_catchup.to_time, "DESC")
.single().row();
Row? latest_row = latest_row_opt.is_present() ? latest_row_opt.inner : null;
Row? new_row = yield fetch_latest_page(account, mam_server, latest_row, until_earliest_time, cancellable);
if (new_row != null) {
current_catchup_id[account][mam_server] = new_row[db.mam_catchup.id];
} else if (latest_row != null) {
current_catchup_id[account][mam_server] = latest_row[db.mam_catchup.id];
}
// Set the previous and current row
Row? previous_row = null;
Row? current_row = null;
if (new_row != null) {
current_row = new_row;
previous_row = latest_row;
} else if (latest_row != null) {
current_row = latest_row;
RowOption previous_row_opt = db.mam_catchup.select()
.with(db.mam_catchup.account_id, "=", account.id)
.with(db.mam_catchup.server_jid, "=", mam_server.to_string())
.with(db.mam_catchup.to_time, "<", current_row[db.mam_catchup.from_time])
.with(db.mam_catchup.to_time, ">=", (long) until_earliest_time.to_unix())
.order_by(db.mam_catchup.to_time, "DESC")
.single().row();
previous_row = previous_row_opt.is_present() ? previous_row_opt.inner : null;
}
// Fetch messages between two db ranges and merge them
while (current_row != null && previous_row != null) {
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());
current_row = yield fetch_between_ranges(account, mam_server, previous_row, current_row, cancellable);
if (current_row == null) return;
RowOption previous_row_opt = db.mam_catchup.select()
.with(db.mam_catchup.account_id, "=", account.id)
.with(db.mam_catchup.server_jid, "=", mam_server.to_string())
.with(db.mam_catchup.to_time, "<", current_row[db.mam_catchup.from_time])
.with(db.mam_catchup.to_time, ">=", (long) until_earliest_time.to_unix())
.order_by(db.mam_catchup.to_time, "DESC")
.single().row();
previous_row = previous_row_opt.is_present() ? previous_row_opt.inner : null;
}
// We're at the earliest range. Try to expand it even further back.
if (current_row == null || current_row[db.mam_catchup.from_end]) return;
// We don't want to fetch before the earliest range over and over again in MUCs if it's after until_earliest_time.
// For now, don't query if we are within a week of until_earliest_time
if (until_earliest_time != null &&
current_row[db.mam_catchup.from_time] > until_earliest_time.add(-TimeSpan.DAY * 7).to_unix()) return;
yield fetch_before_range(account, mam_server, current_row, until_earliest_time);
}
// Fetches the latest page (up to previous db row). Extends the previous db row if it was reached, creates a new row otherwise.
public async Row? fetch_latest_page(Account account, Jid mam_server, Row? latest_row, DateTime? until_earliest_time, Cancellable? cancellable = null) {
debug("[%s | %s] Fetching latest page", account.bare_jid.to_string(), mam_server.to_string());
int latest_row_id = -1;
DateTime latest_message_time = until_earliest_time;
string? latest_message_id = null;
if (latest_row != null) {
latest_row_id = latest_row[db.mam_catchup.id];
latest_message_time = (new DateTime.from_unix_utc(latest_row[db.mam_catchup.to_time])).add_minutes(-5);
latest_message_id = latest_row[db.mam_catchup.to_id];
// Make sure we only fetch to until_earliest_time if latest_message_time is further back
if (until_earliest_time != null && latest_message_time.compare(until_earliest_time) < 0) {
latest_message_time = until_earliest_time.add_minutes(-5);
latest_message_id = null;
}
}
var query_params = new Xmpp.MessageArchiveManagement.V2.MamQueryParams.query_latest(mam_server, latest_message_time, latest_message_id);
PageRequestResult page_result = yield get_mam_page(account, query_params, null, cancellable);
debug("[%s | %s] Latest page result: %s", account.bare_jid.to_string(), mam_server.to_string(), page_result.page_result.to_string());
if (page_result.page_result == PageResult.Error || page_result.page_result == PageResult.Cancelled) {
return null;
}
// Catchup finished within first page. Update latest db entry.
if (latest_row_id != -1 &&
page_result.page_result in new PageResult[] { PageResult.TargetReached, PageResult.NoMoreMessages }) {
if (page_result.stanzas == null) return null;
string latest_mam_id = page_result.query_result.last;
long latest_mam_time = (long) mam_times[account][latest_mam_id].to_unix();
var query = db.mam_catchup.update()
.with(db.mam_catchup.id, "=", latest_row_id)
.set(db.mam_catchup.to_time, latest_mam_time)
.set(db.mam_catchup.to_id, latest_mam_id);
if (page_result.page_result == PageResult.NoMoreMessages) {
// If the server doesn't have more messages, store that this range is at its end.
query.set(db.mam_catchup.from_end, true);
}
query.perform();
return null;
}
if (page_result.query_result.first == null || page_result.query_result.last == null) {
return null;
}
// Either we need to fetch more pages or this is the first db entry ever
debug("[%s | %s] Creating new db range for latest page", account.bare_jid.to_string(), mam_server.to_string());
string from_id = page_result.query_result.first;
string to_id = page_result.query_result.last;
if (!mam_times[account].has_key(from_id) || !mam_times[account].has_key(to_id)) {
debug("Missing from/to id %s %s", from_id, to_id);
return null;
}
long from_time = (long) mam_times[account][from_id].to_unix();
long to_time = (long) mam_times[account][to_id].to_unix();
int new_row_id = (int) db.mam_catchup.insert()
.value(db.mam_catchup.account_id, account.id)
.value(db.mam_catchup.server_jid, mam_server.to_string())
.value(db.mam_catchup.from_id, from_id)
.value(db.mam_catchup.from_time, from_time)
.value(db.mam_catchup.from_end, page_result.page_result == PageResult.NoMoreMessages)
.value(db.mam_catchup.to_id, to_id)
.value(db.mam_catchup.to_time, to_time)
.perform();
return db.mam_catchup.select().with(db.mam_catchup.id, "=", new_row_id).single().row().inner;
}
/** Fetches messages between the end of `earlier_range` and start of `later_range`
** 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.
**/
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];
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]);
debug("[%s | %s] Fetching between %s (%s) and %s (%s)", account.bare_jid.to_string(), mam_server.to_string(), earliest_time.to_string(), earlier_range[db.mam_catchup.to_id], latest_time.to_string(), later_range[db.mam_catchup.from_id]);
var query_params = new Xmpp.MessageArchiveManagement.V2.MamQueryParams.query_between(mam_server,
earliest_time, earlier_range[db.mam_catchup.to_id],
latest_time, later_range[db.mam_catchup.from_id]);
PageRequestResult page_result = yield fetch_query(account, query_params, later_range_id, cancellable);
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);
// Merge earlier range into later one.
db.mam_catchup.update()
.with(db.mam_catchup.id, "=", later_range_id)
.set(db.mam_catchup.from_time, earlier_range[db.mam_catchup.from_time])
.set(db.mam_catchup.from_id, earlier_range[db.mam_catchup.from_id])
.set(db.mam_catchup.from_end, earlier_range[db.mam_catchup.from_end])
.perform();
db.mam_catchup.delete().with(db.mam_catchup.id, "=", earlier_range[db.mam_catchup.id]).perform();
// Return the updated version of the later range
return db.mam_catchup.select().with(db.mam_catchup.id, "=", later_range_id).single().row().inner;
}
return null;
}
private async void fetch_before_range(Account account, Jid mam_server, Row range, DateTime? until_earliest_time, Cancellable? cancellable = null) {
DateTime latest_time = new DateTime.from_unix_utc(range[db.mam_catchup.from_time]);
string latest_id = range[db.mam_catchup.from_id];
debug("[%s | %s] Fetching before range < %s, %s", account.bare_jid.to_string(), mam_server.to_string(), latest_time.to_string(), latest_id);
Xmpp.MessageArchiveManagement.V2.MamQueryParams query_params;
if (until_earliest_time == null) {
query_params = new Xmpp.MessageArchiveManagement.V2.MamQueryParams.query_before(mam_server, latest_time, latest_id);
} else {
query_params = new Xmpp.MessageArchiveManagement.V2.MamQueryParams.query_between(
mam_server,
until_earliest_time, null,
latest_time, latest_id
);
}
yield fetch_query(account, query_params, range[db.mam_catchup.id], cancellable);
}
/**
* Iteratively fetches all pages returned for a query (until a PageResult other than MorePagesAvailable is returned)
* @return The last PageRequestResult result
**/
private async PageRequestResult fetch_query(Account account, Xmpp.MessageArchiveManagement.V2.MamQueryParams query_params, int db_id, Cancellable? cancellable = null) {
debug("[%s | %s] Fetch query %s - %s", account.bare_jid.to_string(), query_params.mam_server.to_string(), query_params.start != null ? query_params.start.to_string() : "", query_params.end != null ? query_params.end.to_string() : "");
PageRequestResult? page_result = null;
do {
page_result = yield get_mam_page(account, query_params, page_result, cancellable);
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.query_result.first == null) return page_result;
string earliest_mam_id = page_result.query_result.first;
long earliest_mam_time = (long)mam_times[account][earliest_mam_id].to_unix();
debug("Updating %s to %s, %s", query_params.mam_server.to_string(), earliest_mam_time.to_string(), earliest_mam_id);
var query = db.mam_catchup.update()
.with(db.mam_catchup.id, "=", db_id)
.set(db.mam_catchup.from_time, earliest_mam_time)
.set(db.mam_catchup.from_id, earliest_mam_id);
if (page_result.page_result == PageResult.NoMoreMessages) {
// If the server doesn't have more messages, store that this range is at its end.
query.set(db.mam_catchup.from_end, true);
}
query.perform();
} while (page_result.page_result == PageResult.MorePagesAvailable);
return page_result;
}
enum PageResult {
MorePagesAvailable,
TargetReached,
NoMoreMessages,
Error,
Cancelled
}
/**
* prev_page_result: null if this is the first page request
**/
private async PageRequestResult get_mam_page(Account account, Xmpp.MessageArchiveManagement.V2.MamQueryParams query_params, PageRequestResult? prev_page_result, Cancellable? cancellable = null) {
XmppStream stream = stream_interactor.get_stream(account);
Xmpp.MessageArchiveManagement.QueryResult query_result = null;
if (prev_page_result == null) {
query_result = yield Xmpp.MessageArchiveManagement.V2.query_archive(stream, query_params, cancellable);
} else {
query_result = yield Xmpp.MessageArchiveManagement.V2.page_through_results(stream, query_params, prev_page_result.query_result, cancellable);
}
return yield process_query_result(account, query_params, query_result, cancellable);
}
private async PageRequestResult process_query_result(Account account, Xmpp.MessageArchiveManagement.V2.MamQueryParams query_params, Xmpp.MessageArchiveManagement.QueryResult query_result, Cancellable? cancellable = null) {
PageResult page_result = PageResult.MorePagesAvailable;
if (query_result.malformed || query_result.error) {
page_result = PageResult.Error;
}
// We wait until all the messages from the page are processed (and we got the `mam_times` from them)
Idle.add(process_query_result.callback, Priority.LOW);
yield;
// We might have successfully reached the target or the server doesn't have all messages stored anymore
// If it's the former, we'll overwrite the value with PageResult.MorePagesAvailable below.
if (query_result.complete) {
page_result = PageResult.NoMoreMessages;
}
string query_id = query_params.query_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()) {
stanzas.unset(query_id);
return new PageRequestResult(PageResult.Cancelled, query_result, stanzas_for_query);
}
if (stanzas_for_query != null) {
// Check it we reached our target (from_id)
foreach (Xmpp.MessageStanza message in stanzas_for_query) {
Xmpp.MessageArchiveManagement.MessageFlag? mam_message_flag = Xmpp.MessageArchiveManagement.MessageFlag.get_flag(message);
if (mam_message_flag != null && mam_message_flag.mam_id != null) {
if (after_id != null && mam_message_flag.mam_id == after_id) {
// Successfully fetched the whole range
yield send_messages_back_into_pipeline(account, query_id, cancellable);
if (cancellable != null && cancellable.is_cancelled()) {
return new PageRequestResult(PageResult.Cancelled, query_result, stanzas_for_query);
}
return new PageRequestResult(PageResult.TargetReached, query_result, stanzas_for_query);
}
}
}
if (hitted_range.has_key(query_id) && hitted_range[query_id] == -2) {
// Message got filtered out by xmpp-vala, but succesful range fetch nevertheless
yield send_messages_back_into_pipeline(account, query_id);
if (cancellable != null && cancellable.is_cancelled()) {
return new PageRequestResult(PageResult.Cancelled, query_result, stanzas_for_query);
}
return new PageRequestResult(PageResult.TargetReached, query_result, stanzas_for_query);
}
}
yield send_messages_back_into_pipeline(account, query_id);
if (cancellable != null && cancellable.is_cancelled()) {
page_result = PageResult.Cancelled;
}
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) {
if (!stanzas.has_key(query_id)) return;
foreach (Xmpp.MessageStanza message in stanzas[query_id]) {
if (cancellable != null && cancellable.is_cancelled()) break;
yield stream_interactor.get_module(MessageProcessor.IDENTITY).run_pipeline_announce(account, message);
}
stanzas.unset(query_id);
}
private void on_account_added(Account account) {
cleanup_db_ranges(db, account);
mam_times[account] = new HashMap<string, DateTime>();
stream_interactor.connection_manager.stream_attached_modules.connect((account, stream) => {
if (!current_catchup_id.has_key(account)) {
current_catchup_id[account] = new HashMap<Jid, int>(Jid.hash_func, Jid.equals_func);
} else {
current_catchup_id[account].clear();
}
});
stream_interactor.module_manager.get_module(account, Xmpp.MessageArchiveManagement.Module.IDENTITY).feature_available.connect((stream) => {
consider_fetch_everything(account, stream);
});
stream_interactor.module_manager.get_module(account, Xmpp.MessageModule.IDENTITY).received_message_unprocessed.connect((stream, message) => {
on_unprocessed_message(account, stream, message);
});
}
private void consider_fetch_everything(Account account, XmppStream stream) {
if (sync_streams.has(account, stream)) return;
debug("[%s] MAM available", account.bare_jid.to_string());
sync_streams[account] = stream;
if (!cancellables.has_key(account)) {
cancellables[account] = new HashMap<Jid, Cancellable>();
}
if (cancellables[account].has_key(account.bare_jid)) {
cancellables[account][account.bare_jid].cancel();
}
cancellables[account][account.bare_jid] = new Cancellable();
fetch_everything.begin(account, account.bare_jid, cancellables[account][account.bare_jid], new DateTime.from_unix_utc(0), (_, res) => {
fetch_everything.end(res);
cancellables[account].unset(account.bare_jid);
});
}
public static void cleanup_db_ranges(Database db, Account account) {
var ranges = new HashMap<Jid, ArrayList<MamRange>>(Jid.hash_func, Jid.equals_func);
foreach (Row row in db.mam_catchup.select().with(db.mam_catchup.account_id, "=", account.id)) {
var mam_range = new MamRange();
mam_range.id = row[db.mam_catchup.id];
mam_range.server_jid = new Jid(row[db.mam_catchup.server_jid]);
mam_range.from_time = row[db.mam_catchup.from_time];
mam_range.from_id = row[db.mam_catchup.from_id];
mam_range.from_end = row[db.mam_catchup.from_end];
mam_range.to_time = row[db.mam_catchup.to_time];
mam_range.to_id = row[db.mam_catchup.to_id];
if (!ranges.has_key(mam_range.server_jid)) ranges[mam_range.server_jid] = new ArrayList<MamRange>();
ranges[mam_range.server_jid].add(mam_range);
}
var to_delete = new ArrayList<MamRange>();
foreach (Jid server_jid in ranges.keys) {
foreach (var range1 in ranges[server_jid]) {
if (to_delete.contains(range1)) continue;
foreach (MamRange range2 in ranges[server_jid]) {
debug("[%s | %s] | %s - %s vs %s - %s", account.bare_jid.to_string(), server_jid.to_string(), range1.from_time.to_string(), range1.to_time.to_string(), range2.from_time.to_string(), range2.to_time.to_string());
if (range1 == range2 || to_delete.contains(range2)) continue;
// Check if range2 is a subset of range1
// range1: #####################
// range2: ######
if (range1.from_time <= range2.from_time && range1.to_time >= range2.to_time) {
warning("Removing db range which is a subset of %li-%li", range1.from_time, range1.to_time);
to_delete.add(range2);
continue;
}
// Check if range2 is an extension of range1 (towards earlier)
// range1: #####################
// range2: ###############
if (range1.from_time <= range2.to_time <= range1.to_time && range2.from_time <= range1.from_time) {
warning("Removing db range that overlapped %li-%li (towards earlier)", range1.from_time, range1.to_time);
db.mam_catchup.update()
.with(db.mam_catchup.id, "=", range1.id)
.set(db.mam_catchup.from_id, range2.from_id)
.set(db.mam_catchup.from_time, range2.from_time)
.set(db.mam_catchup.from_end, range2.from_end)
.perform();
to_delete.add(range2);
continue;
}
}
}
}
foreach (MamRange row in to_delete) {
db.mam_catchup.delete().with(db.mam_catchup.id, "=", row.id).perform();
warning("Removing db range %s %li-%li", row.server_jid.to_string(), row.from_time, row.to_time);
}
}
class MamRange {
public int id;
public Jid server_jid;
public long from_time;
public string from_id;
public bool from_end;
public long to_time;
public string to_id;
}
class PageRequestResult {
public Gee.List<MessageStanza> stanzas { get; set; }
public PageResult page_result { get; set; }
public Xmpp.MessageArchiveManagement.QueryResult query_result { get; set; }
public PageRequestResult(PageResult page_result, Xmpp.MessageArchiveManagement.QueryResult query_result, Gee.List<MessageStanza>? stanzas) {
this.page_result = page_result;
this.query_result = query_result;
this.stanzas = stanzas;
}
}
}

View File

@ -64,7 +64,7 @@ public class JingleFileProvider : FileProvider, Object {
public JingleFileProvider(StreamInteractor stream_interactor) { public JingleFileProvider(StreamInteractor stream_interactor) {
this.stream_interactor = stream_interactor; this.stream_interactor = stream_interactor;
stream_interactor.stream_negotiated.connect(on_stream_negotiated); stream_interactor.account_added.connect(on_account_added);
} }
public FileMeta get_file_meta(FileTransfer file_transfer) throws FileReceiveError { public FileMeta get_file_meta(FileTransfer file_transfer) throws FileReceiveError {
@ -95,34 +95,27 @@ public class JingleFileProvider : FileProvider, Object {
return Encryption.NONE; return Encryption.NONE;
} }
public async InputStream download(FileTransfer file_transfer, FileReceiveData receive_data, FileMeta file_meta) throws FileReceiveError { public async InputStream download(FileTransfer file_transfer, FileReceiveData receive_data, FileMeta file_meta) throws IOError {
// TODO(hrxi) What should happen if `stream == null`? // TODO(hrxi) What should happen if `stream == null`?
XmppStream? stream = stream_interactor.get_stream(file_transfer.account); XmppStream? stream = stream_interactor.get_stream(file_transfer.account);
Xmpp.Xep.JingleFileTransfer.FileTransfer? jingle_file_transfer = file_transfers[file_transfer.info]; Xmpp.Xep.JingleFileTransfer.FileTransfer? jingle_file_transfer = file_transfers[file_transfer.info];
if (jingle_file_transfer == null) { if (jingle_file_transfer == null) {
throw new FileReceiveError.DOWNLOAD_FAILED("Transfer data not available anymore"); throw new IOError.FAILED("Transfer data not available anymore");
} }
try {
yield jingle_file_transfer.accept(stream); yield jingle_file_transfer.accept(stream);
} catch (IOError e) { return new LimitInputStream(jingle_file_transfer.stream, file_meta.size);
throw new FileReceiveError.DOWNLOAD_FAILED("Establishing connection did not work");
}
return jingle_file_transfer.stream;
} }
public int get_id() { public int get_id() {
return 1; return 1;
} }
private void on_stream_negotiated(Account account, XmppStream stream) { private void on_account_added(Account account) {
stream_interactor.module_manager.get_module(account, Xmpp.Xep.JingleFileTransfer.Module.IDENTITY).file_incoming.connect((stream, jingle_file_transfer) => { stream_interactor.module_manager.get_module(account, Xmpp.Xep.JingleFileTransfer.Module.IDENTITY).file_incoming.connect((stream, jingle_file_transfer) => {
Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation(jingle_file_transfer.peer.bare_jid, account); Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation(jingle_file_transfer.peer.bare_jid, account);
if (conversation == null) { if (conversation == null) return;
// TODO(hrxi): What to do?
return;
}
string id = random_uuid();
string id = random_uuid();
file_transfers[id] = jingle_file_transfer; file_transfers[id] = jingle_file_transfer;
FileMeta file_meta = new FileMeta(); FileMeta file_meta = new FileMeta();

View File

@ -39,26 +39,21 @@ public class MessageCorrection : StreamInteractionModule, MessageListener {
}); });
} }
public void send_correction(Conversation conversation, Message old_message, string correction_text) { public void set_correction(Conversation conversation, Message message, Message old_message) {
string stanza_id = old_message.edit_to ?? old_message.stanza_id; string reference_stanza_id = old_message.edit_to ?? old_message.stanza_id;
Message out_message = stream_interactor.get_module(MessageProcessor.IDENTITY).create_out_message(correction_text, conversation); outstanding_correction_nodes[message.stanza_id] = reference_stanza_id;
out_message.edit_to = stanza_id;
outstanding_correction_nodes[out_message.stanza_id] = stanza_id;
stream_interactor.get_module(MessageProcessor.IDENTITY).send_xmpp_message(out_message, conversation);
db.message_correction.insert() db.message_correction.insert()
.value(db.message_correction.message_id, out_message.id) .value(db.message_correction.message_id, message.id)
.value(db.message_correction.to_stanza_id, stanza_id) .value(db.message_correction.to_stanza_id, reference_stanza_id)
.perform(); .perform();
db.content_item.update() db.content_item.update()
.with(db.content_item.foreign_id, "=", old_message.id) .with(db.content_item.foreign_id, "=", old_message.id)
.with(db.content_item.content_type, "=", 1) .with(db.content_item.content_type, "=", 1)
.set(db.content_item.foreign_id, out_message.id) .set(db.content_item.foreign_id, message.id)
.perform(); .perform();
on_received_correction(conversation, out_message.id);
} }
public bool is_own_correction_allowed(Conversation conversation, Message message) { public bool is_own_correction_allowed(Conversation conversation, Message message) {
@ -96,9 +91,10 @@ public class MessageCorrection : StreamInteractionModule, MessageListener {
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) {
if (conversation.type_ != Conversation.Type.CHAT) { if (conversation.type_ != Conversation.Type.CHAT) {
// Don't process messages or corrections from MUC history // Don't process messages or corrections from MUC history or MUC MAM
DateTime? mam_delay = Xep.DelayedDelivery.get_time_for_message(stanza, message.from.bare_jid); DateTime? mam_delay = Xep.DelayedDelivery.get_time_for_message(stanza, message.from.bare_jid);
if (mam_delay != null) return false; if (mam_delay != null) return false;
if (Xmpp.MessageArchiveManagement.MessageFlag.get_flag(stanza) != null) return false;
} }
string? replace_id = Xep.LastMessageCorrection.get_replace_id(stanza); string? replace_id = Xep.LastMessageCorrection.get_replace_id(stanza);
@ -143,8 +139,8 @@ public class MessageCorrection : StreamInteractionModule, MessageListener {
return false; return false;
} }
private void on_received_correction(Conversation conversation, int message_id) { public void on_received_correction(Conversation conversation, int message_id) {
ContentItem? content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item(conversation, 1, message_id); ContentItem? content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item_by_foreign(conversation, 1, message_id);
if (content_item != null) { if (content_item != null) {
received_correction(content_item); received_correction(content_item);
} }

View File

@ -18,15 +18,11 @@ public class MessageProcessor : StreamInteractionModule, Object {
public signal void message_sent_or_received(Entities.Message message, Conversation conversation); public signal void message_sent_or_received(Entities.Message message, Conversation conversation);
public signal void history_synced(Account account); public signal void history_synced(Account account);
public HistorySync history_sync;
public MessageListenerHolder received_pipeline = new MessageListenerHolder(); public MessageListenerHolder received_pipeline = new MessageListenerHolder();
private StreamInteractor stream_interactor; private StreamInteractor stream_interactor;
private Database db; private Database db;
private HashMap<Account, int> current_catchup_id = new HashMap<Account, int>(Account.hash_func, Account.equals_func);
private HashMap<Account, HashMap<string, DateTime>> mam_times = new HashMap<Account, HashMap<string, DateTime>>();
public HashMap<string, int> hitted_range = new HashMap<string, int>();
public HashMap<Account, string> catchup_until_id = new HashMap<Account, string>(Account.hash_func, Account.equals_func);
public HashMap<Account, DateTime> catchup_until_time = new HashMap<Account, DateTime>(Account.hash_func, Account.equals_func);
public static void start(StreamInteractor stream_interactor, Database db) { public static void start(StreamInteractor stream_interactor, Database db) {
MessageProcessor m = new MessageProcessor(stream_interactor, db); MessageProcessor m = new MessageProcessor(stream_interactor, db);
@ -36,34 +32,18 @@ public class MessageProcessor : StreamInteractionModule, Object {
private MessageProcessor(StreamInteractor stream_interactor, Database db) { private MessageProcessor(StreamInteractor stream_interactor, Database db) {
this.stream_interactor = stream_interactor; this.stream_interactor = stream_interactor;
this.db = db; this.db = db;
this.history_sync = new HistorySync(db, stream_interactor);
received_pipeline.connect(new DeduplicateMessageListener(this, db)); received_pipeline.connect(new DeduplicateMessageListener(this));
received_pipeline.connect(new FilterMessageListener()); received_pipeline.connect(new FilterMessageListener());
received_pipeline.connect(new StoreMessageListener(stream_interactor)); received_pipeline.connect(new StoreMessageListener(this, stream_interactor));
received_pipeline.connect(new StoreContentItemListener(stream_interactor)); received_pipeline.connect(new StoreContentItemListener(stream_interactor));
received_pipeline.connect(new MamMessageListener(stream_interactor)); received_pipeline.connect(new MarkupListener(stream_interactor));
stream_interactor.account_added.connect(on_account_added); stream_interactor.account_added.connect(on_account_added);
stream_interactor.stream_negotiated.connect(send_unsent_chat_messages); stream_interactor.stream_negotiated.connect(send_unsent_chat_messages);
stream_interactor.stream_resumed.connect(send_unsent_chat_messages); stream_interactor.stream_resumed.connect(send_unsent_chat_messages);
stream_interactor.connection_manager.stream_opened.connect((account, stream) => {
debug("MAM: [%s] Reset catchup_id", account.bare_jid.to_string());
current_catchup_id.unset(account);
});
}
public Entities.Message send_text(string text, Conversation conversation) {
Entities.Message message = create_out_message(text, conversation);
return send_message(message, conversation);
}
public Entities.Message send_message(Entities.Message message, Conversation conversation) {
stream_interactor.get_module(ContentItemStore.IDENTITY).insert_message(message, conversation);
send_xmpp_message(message, conversation);
message_sent(message, conversation);
return message;
} }
private void convert_sending_to_unsent_msgs(Account account) { private void convert_sending_to_unsent_msgs(Account account) {
@ -106,43 +86,10 @@ public class MessageProcessor : StreamInteractionModule, Object {
} }
private void on_account_added(Account account) { private void on_account_added(Account account) {
mam_times[account] = new HashMap<string, DateTime>();
stream_interactor.module_manager.get_module(account, Xmpp.MessageModule.IDENTITY).received_message.connect( (stream, message) => { stream_interactor.module_manager.get_module(account, Xmpp.MessageModule.IDENTITY).received_message.connect( (stream, message) => {
on_message_received.begin(account, message); on_message_received.begin(account, message);
}); });
XmppStream? stream_bak = null;
stream_interactor.module_manager.get_module(account, Xmpp.Xep.MessageArchiveManagement.Module.IDENTITY).feature_available.connect( (stream) => {
if (stream == stream_bak) return;
current_catchup_id.unset(account);
stream_bak = stream;
debug("MAM: [%s] MAM available", account.bare_jid.to_string());
do_mam_catchup.begin(account);
});
stream_interactor.module_manager.get_module(account, Xmpp.MessageModule.IDENTITY).received_message_unprocessed.connect((stream, message) => {
if (!message.from.equals(account.bare_jid)) return;
Xep.MessageArchiveManagement.Flag? mam_flag = stream != null ? stream.get_flag(Xep.MessageArchiveManagement.Flag.IDENTITY) : null;
if (mam_flag == null) return;
string? id = message.stanza.get_deep_attribute(mam_flag.ns_ver + ":result", "id");
if (id == null) return;
StanzaNode? delay_node = message.stanza.get_deep_subnode(mam_flag.ns_ver + ":result", "urn:xmpp:forward:0:forwarded", "urn:xmpp:delay:delay");
if (delay_node == null) {
warning("MAM result did not contain delayed time %s", message.stanza.to_string());
return;
}
DateTime? time = DelayedDelivery.get_time_for_node(delay_node);
if (time == null) return;
mam_times[account][id] = time;
string? query_id = message.stanza.get_deep_attribute(mam_flag.ns_ver + ":result", mam_flag.ns_ver + ":queryid");
if (query_id != null && id == catchup_until_id[account]) {
debug("MAM: [%s] Hitted range (id) %s", account.bare_jid.to_string(), id);
hitted_range[query_id] = -2;
}
});
stream_interactor.module_manager.get_module(account, Xmpp.MessageModule.IDENTITY).received_error.connect((stream, message_stanza, error_stanza) => { stream_interactor.module_manager.get_module(account, Xmpp.MessageModule.IDENTITY).received_error.connect((stream, message_stanza, error_stanza) => {
Message? message = null; Message? message = null;
@ -164,203 +111,20 @@ public class MessageProcessor : StreamInteractionModule, Object {
convert_sending_to_unsent_msgs(account); convert_sending_to_unsent_msgs(account);
} }
private async void do_mam_catchup(Account account) {
debug("MAM: [%s] Start catchup", account.bare_jid.to_string());
string? earliest_id = null;
DateTime? earliest_time = null;
bool continue_sync = true;
while (continue_sync) {
continue_sync = false;
// Get previous row
var previous_qry = db.mam_catchup.select().with(db.mam_catchup.account_id, "=", account.id).order_by(db.mam_catchup.to_time, "DESC");
if (current_catchup_id.has_key(account)) {
previous_qry.with(db.mam_catchup.id, "!=", current_catchup_id[account]);
}
RowOption previous_row = previous_qry.single().row();
if (previous_row.is_present()) {
catchup_until_id[account] = previous_row[db.mam_catchup.to_id];
catchup_until_time[account] = (new DateTime.from_unix_utc(previous_row[db.mam_catchup.to_time])).add_minutes(-5);
debug("MAM: [%s] Previous entry exists", account.bare_jid.to_string());
} else {
catchup_until_id.unset(account);
catchup_until_time.unset(account);
}
string query_id = Xmpp.random_uuid();
yield get_mam_range(account, query_id, null, null, earliest_time, earliest_id);
if (!hitted_range.has_key(query_id)) {
debug("MAM: [%s] Set catchup end reached", account.bare_jid.to_string());
db.mam_catchup.update()
.set(db.mam_catchup.from_end, true)
.with(db.mam_catchup.id, "=", current_catchup_id[account])
.perform();
}
if (hitted_range.has_key(query_id)) {
if (merge_ranges(account, null)) {
RowOption current_row = db.mam_catchup.row_with(db.mam_catchup.id, current_catchup_id[account]);
bool range_from_complete = current_row[db.mam_catchup.from_end];
if (!range_from_complete) {
continue_sync = true;
earliest_id = current_row[db.mam_catchup.from_id];
earliest_time = (new DateTime.from_unix_utc(current_row[db.mam_catchup.from_time])).add_seconds(1);
}
}
}
}
}
/*
* Merges the row with `current_catchup_id` with the previous range (optional: with `earlier_id`)
* Changes `current_catchup_id` to the previous range
*/
private bool merge_ranges(Account account, int? earlier_id) {
RowOption current_row = db.mam_catchup.row_with(db.mam_catchup.id, current_catchup_id[account]);
RowOption previous_row = null;
if (earlier_id != null) {
previous_row = db.mam_catchup.row_with(db.mam_catchup.id, earlier_id);
} else {
previous_row = db.mam_catchup.select()
.with(db.mam_catchup.account_id, "=", account.id)
.with(db.mam_catchup.id, "!=", current_catchup_id[account])
.order_by(db.mam_catchup.to_time, "DESC").single().row();
}
if (!previous_row.is_present()) {
debug("MAM: [%s] Merging: No previous row", account.bare_jid.to_string());
return false;
}
var qry = db.mam_catchup.update().with(db.mam_catchup.id, "=", previous_row[db.mam_catchup.id]);
debug("MAM: [%s] Merging %ld-%ld with %ld- %ld", account.bare_jid.to_string(), previous_row[db.mam_catchup.from_time], previous_row[db.mam_catchup.to_time], current_row[db.mam_catchup.from_time], current_row[db.mam_catchup.to_time]);
if (current_row[db.mam_catchup.from_time] < previous_row[db.mam_catchup.from_time]) {
qry.set(db.mam_catchup.from_id, current_row[db.mam_catchup.from_id])
.set(db.mam_catchup.from_time, current_row[db.mam_catchup.from_time]);
}
if (current_row[db.mam_catchup.to_time] > previous_row[db.mam_catchup.to_time]) {
qry.set(db.mam_catchup.to_id, current_row[db.mam_catchup.to_id])
.set(db.mam_catchup.to_time, current_row[db.mam_catchup.to_time]);
}
qry.perform();
current_catchup_id[account] = previous_row[db.mam_catchup.id];
db.mam_catchup.delete().with(db.mam_catchup.id, "=", current_row[db.mam_catchup.id]).perform();
return true;
}
private async bool get_mam_range(Account account, string? query_id, DateTime? from_time, string? from_id, DateTime? to_time, string? to_id) {
debug("MAM: [%s] Get range %s - %s", account.bare_jid.to_string(), from_time != null ? from_time.to_string() : "", to_time != null ? to_time.to_string() : "");
XmppStream stream = stream_interactor.get_stream(account);
Iq.Stanza? iq = yield stream.get_module(Xep.MessageArchiveManagement.Module.IDENTITY).query_archive(stream, null, query_id, from_time, from_id, to_time, to_id);
if (iq == null) {
debug(@"MAM: [%s] IQ null", account.bare_jid.to_string());
return true;
}
if (iq.stanza.get_deep_string_content("urn:xmpp:mam:2:fin", "http://jabber.org/protocol/rsm" + ":set", "first") == null) {
return true;
}
while (iq != null) {
string? earliest_id = iq.stanza.get_deep_string_content("urn:xmpp:mam:2:fin", "http://jabber.org/protocol/rsm" + ":set", "first");
if (earliest_id == null) return true;
string? latest_id = iq.stanza.get_deep_string_content("urn:xmpp:mam:2:fin", "http://jabber.org/protocol/rsm" + ":set", "last");
// We wait until all the messages from the page are processed (and we got the `mam_times` from them)
Idle.add(get_mam_range.callback, Priority.LOW);
yield;
int wait_ms = 1000;
if (mam_times[account].has_key(earliest_id) && (current_catchup_id.has_key(account) || mam_times[account].has_key(latest_id))) {
debug("MAM: [%s] Update from_id %s", account.bare_jid.to_string(), earliest_id);
if (!current_catchup_id.has_key(account)) {
debug("MAM: [%s] We get our first MAM page", account.bare_jid.to_string());
current_catchup_id[account] = (int) db.mam_catchup.insert()
.value(db.mam_catchup.account_id, account.id)
.value(db.mam_catchup.from_id, earliest_id)
.value(db.mam_catchup.from_time, (long)mam_times[account][earliest_id].to_unix())
.value(db.mam_catchup.to_id, latest_id)
.value(db.mam_catchup.to_time, (long)mam_times[account][latest_id].to_unix())
.perform();
} else {
// Update existing id
db.mam_catchup.update()
.set(db.mam_catchup.from_id, earliest_id)
.set(db.mam_catchup.from_time, (long)mam_times[account][earliest_id].to_unix())
.with(db.mam_catchup.id, "=", current_catchup_id[account])
.perform();
}
TimeSpan catchup_time_ago = (new DateTime.now_utc()).difference(mam_times[account][earliest_id]);
if (catchup_time_ago > 14 * TimeSpan.DAY) {
wait_ms = 2000;
} else if (catchup_time_ago > 5 * TimeSpan.DAY) {
wait_ms = 1000;
} else if (catchup_time_ago > 2 * TimeSpan.DAY) {
wait_ms = 200;
} else if (catchup_time_ago > TimeSpan.DAY) {
wait_ms = 50;
} else {
wait_ms = 10;
}
} else {
warning("Didn't have time for MAM id; earliest_id:%s latest_id:%s", mam_times[account].has_key(earliest_id).to_string(), mam_times[account].has_key(latest_id).to_string());
}
mam_times[account] = new HashMap<string, DateTime>();
Timeout.add(wait_ms, () => {
if (hitted_range.has_key(query_id)) {
debug(@"MAM: [%s] Hitted contains key %s", account.bare_jid.to_string(), query_id);
iq = null;
Idle.add(get_mam_range.callback);
return false;
}
stream.get_module(Xep.MessageArchiveManagement.Module.IDENTITY).page_through_results.begin(stream, null, query_id, from_time, to_time, iq, (_, res) => {
iq = stream.get_module(Xep.MessageArchiveManagement.Module.IDENTITY).page_through_results.end(res);
Idle.add(get_mam_range.callback);
});
return false;
});
yield;
}
return false;
}
private async void on_message_received(Account account, Xmpp.MessageStanza message_stanza) { private async void on_message_received(Account account, Xmpp.MessageStanza message_stanza) {
// If it's a message from MAM, it's going to be processed by HistorySync which calls run_pipeline_announce later.
if (history_sync.process(account, message_stanza)) return;
run_pipeline_announce.begin(account, message_stanza);
}
public async void run_pipeline_announce(Account account, Xmpp.MessageStanza message_stanza) {
Entities.Message message = yield parse_message_stanza(account, message_stanza); Entities.Message message = yield parse_message_stanza(account, message_stanza);
Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation_for_message(message); Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation_for_message(message);
if (conversation == null) return; if (conversation == null) return;
// MAM state database update
Xep.MessageArchiveManagement.MessageFlag? mam_flag = Xep.MessageArchiveManagement.MessageFlag.get_flag(message_stanza);
if (mam_flag == null) {
if (current_catchup_id.has_key(account)) {
string? stanza_id = UniqueStableStanzaIDs.get_stanza_id(message_stanza, account.bare_jid);
if (stanza_id != null) {
db.mam_catchup.update()
.with(db.mam_catchup.id, "=", current_catchup_id[account])
.set(db.mam_catchup.to_time, (long)message.local_time.to_unix())
.set(db.mam_catchup.to_id, stanza_id)
.perform();
}
}
}
bool abort = yield received_pipeline.run(message, message_stanza, conversation); bool abort = yield received_pipeline.run(message, message_stanza, conversation);
if (abort) return; if (abort) return;
@ -373,7 +137,7 @@ public class MessageProcessor : StreamInteractionModule, Object {
message_sent_or_received(message, conversation); message_sent_or_received(message, conversation);
} }
private async Entities.Message parse_message_stanza(Account account, Xmpp.MessageStanza message) { public async Entities.Message parse_message_stanza(Account account, Xmpp.MessageStanza message) {
string? body = message.body; string? body = message.body;
if (body != null) body = body.strip(); if (body != null) body = body.strip();
Entities.Message new_message = new Entities.Message(body); Entities.Message new_message = new Entities.Message(body);
@ -392,21 +156,22 @@ public class MessageProcessor : StreamInteractionModule, Object {
new_message.counterpart = counterpart_override ?? (new_message.direction == Entities.Message.DIRECTION_SENT ? message.to : message.from); new_message.counterpart = counterpart_override ?? (new_message.direction == Entities.Message.DIRECTION_SENT ? message.to : message.from);
new_message.ourpart = new_message.direction == Entities.Message.DIRECTION_SENT ? message.from : message.to; new_message.ourpart = new_message.direction == Entities.Message.DIRECTION_SENT ? message.from : message.to;
XmppStream? stream = stream_interactor.get_stream(account); Xmpp.MessageArchiveManagement.MessageFlag? mam_message_flag = Xmpp.MessageArchiveManagement.MessageFlag.get_flag(message);
Xep.MessageArchiveManagement.MessageFlag? mam_message_flag = Xep.MessageArchiveManagement.MessageFlag.get_flag(message);
Xep.MessageArchiveManagement.Flag? mam_flag = stream != null ? stream.get_flag(Xep.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 == Xep.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, Xep.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, Xep.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);
} }
@ -457,42 +222,22 @@ public class MessageProcessor : StreamInteractionModule, Object {
return Entities.Message.Type.CHAT; return Entities.Message.Type.CHAT;
} }
private class DeduplicateMessageListener : MessageListener { private bool is_duplicate(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
public string[] after_actions_const = new string[]{ "FILTER_EMPTY", "MUC" };
public override string action_group { get { return "DEDUPLICATE"; } }
public override string[] after_actions { get { return after_actions_const; } }
private MessageProcessor outer;
private Database db;
public DeduplicateMessageListener(MessageProcessor outer, Database db) {
this.outer = outer;
this.db = db;
}
public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
Account account = conversation.account; Account account = conversation.account;
Xep.MessageArchiveManagement.MessageFlag? mam_flag = Xep.MessageArchiveManagement.MessageFlag.get_flag(stanza);
// Deduplicate by server_id // Deduplicate by server_id
if (message.server_id != null) { if (message.server_id != null) {
QueryBuilder builder = db.message.select() QueryBuilder builder = db.message.select()
.with(db.message.server_id, "=", message.server_id) .with(db.message.server_id, "=", message.server_id)
.with(db.message.counterpart_id, "=", db.get_jid_id(message.counterpart)) .with(db.message.counterpart_id, "=", db.get_jid_id(message.counterpart))
.with(db.message.account_id, "=", account.id); .with(db.message.account_id, "=", account.id);
bool duplicate = builder.count() > 0;
if (duplicate && mam_flag != null) { // If the message is a duplicate
debug(@"MAM: [%s] Hitted range duplicate server id. id %s qid %s", account.bare_jid.to_string(), message.server_id, mam_flag.query_id); if (builder.count() > 0) {
if (outer.catchup_until_time.has_key(account) && mam_flag.server_time.compare(outer.catchup_until_time[account]) < 0) { history_sync.on_server_id_duplicate(account, stanza, message);
outer.hitted_range[mam_flag.query_id] = -1; return true;
debug(@"MAM: [%s] In range (time) %s < %s", account.bare_jid.to_string(), mam_flag.server_time.to_string(), outer.catchup_until_time[account].to_string());
} }
} }
if (duplicate) return true;
}
// Deduplicate messages by uuid // Deduplicate messages by uuid
bool is_uuid = message.stanza_id != null && Regex.match_simple("""[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}""", message.stanza_id); bool is_uuid = message.stanza_id != null && Regex.match_simple("""[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}""", message.stanza_id);
@ -514,14 +259,7 @@ public class MessageProcessor : StreamInteractionModule, Object {
builder.with_null(db.message.our_resource); builder.with_null(db.message.our_resource);
} }
} }
RowOption row_opt = builder.single().row(); bool duplicate = builder.single().row().is_present();
bool duplicate = row_opt.is_present();
if (duplicate && mam_flag != null && row_opt[db.message.server_id] == null &&
outer.catchup_until_time.has_key(account) && mam_flag.server_time.compare(outer.catchup_until_time[account]) > 0) {
outer.hitted_range[mam_flag.query_id] = -1;
debug(@"MAM: [%s] Hitted range duplicate message id. id %s qid %s", account.bare_jid.to_string(), message.stanza_id, mam_flag.query_id);
}
return duplicate; return duplicate;
} }
@ -544,6 +282,22 @@ public class MessageProcessor : StreamInteractionModule, Object {
} }
return builder.count() > 0; return builder.count() > 0;
} }
private class DeduplicateMessageListener : MessageListener {
public string[] after_actions_const = new string[]{ "FILTER_EMPTY", "MUC" };
public override string action_group { get { return "DEDUPLICATE"; } }
public override string[] after_actions { get { return after_actions_const; } }
private MessageProcessor outer;
public DeduplicateMessageListener(MessageProcessor outer) {
this.outer = outer;
}
public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
return outer.is_duplicate(message, stanza, conversation);
}
} }
private class FilterMessageListener : MessageListener { private class FilterMessageListener : MessageListener {
@ -553,7 +307,8 @@ public class MessageProcessor : StreamInteractionModule, Object {
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(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) { public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
return (message.body == null); return message.body == null &&
Xep.StatelessFileSharing.get_file_shares(stanza) == null;
} }
} }
@ -563,19 +318,39 @@ public class MessageProcessor : StreamInteractionModule, Object {
public override string action_group { get { return "STORE"; } } public override string action_group { get { return "STORE"; } }
public override string[] after_actions { get { return after_actions_const; } } public override string[] after_actions { get { return after_actions_const; } }
private MessageProcessor outer;
private StreamInteractor stream_interactor; private StreamInteractor stream_interactor;
public StoreMessageListener(StreamInteractor stream_interactor) { public StoreMessageListener(MessageProcessor outer, StreamInteractor stream_interactor) {
this.outer = outer;
this.stream_interactor = stream_interactor; this.stream_interactor = stream_interactor;
} }
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) {
if (message.body == null) return true;
stream_interactor.get_module(MessageStorage.IDENTITY).add_message(message, conversation); stream_interactor.get_module(MessageStorage.IDENTITY).add_message(message, conversation);
return false; return false;
} }
} }
private class MarkupListener : MessageListener {
public string[] after_actions_const = new string[]{ "STORE" };
public override string action_group { get { return "Markup"; } }
public override string[] after_actions { get { return after_actions_const; } }
private StreamInteractor stream_interactor;
public MarkupListener(StreamInteractor stream_interactor) {
this.stream_interactor = stream_interactor;
}
public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
Gee.List<MessageMarkup.Span> markups = MessageMarkup.get_spans(stanza);
message.persist_markups(markups, message.id);
return false;
}
}
private class StoreContentItemListener : MessageListener { private class StoreContentItemListener : MessageListener {
public string[] after_actions_const = new string[]{ "DEDUPLICATE", "DECRYPT", "FILTER_EMPTY", "STORE", "CORRECTION", "MESSAGE_REINTERPRETING" }; public string[] after_actions_const = new string[]{ "DEDUPLICATE", "DECRYPT", "FILTER_EMPTY", "STORE", "CORRECTION", "MESSAGE_REINTERPRETING" };
@ -595,30 +370,7 @@ public class MessageProcessor : StreamInteractionModule, Object {
} }
} }
private class MamMessageListener : MessageListener { public Entities.Message create_out_message(string? text, Conversation conversation) {
public string[] after_actions_const = new string[]{ "DEDUPLICATE" };
public override string action_group { get { return "MAM_NODE"; } }
public override string[] after_actions { get { return after_actions_const; } }
private StreamInteractor stream_interactor;
public MamMessageListener(StreamInteractor stream_interactor) {
this.stream_interactor = stream_interactor;
}
public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
bool is_mam_message = Xep.MessageArchiveManagement.MessageFlag.get_flag(stanza) != null;
XmppStream? stream = stream_interactor.get_stream(conversation.account);
Xep.MessageArchiveManagement.Flag? mam_flag = stream != null ? stream.get_flag(Xep.MessageArchiveManagement.Flag.IDENTITY) : null;
if (is_mam_message || (mam_flag != null && mam_flag.cought_up == true)) {
conversation.account.mam_earliest_synced = message.local_time;
}
return false;
}
}
public Entities.Message create_out_message(string text, Conversation conversation) {
Entities.Message message = new Entities.Message(text); Entities.Message message = new Entities.Message(text);
message.type_ = Util.get_message_type_for_conversation(conversation); message.type_ = Util.get_message_type_for_conversation(conversation);
message.stanza_id = random_uuid(); message.stanza_id = random_uuid();
@ -660,6 +412,24 @@ public class MessageProcessor : StreamInteractionModule, Object {
} else { } else {
new_message.type_ = MessageStanza.TYPE_CHAT; new_message.type_ = MessageStanza.TYPE_CHAT;
} }
if (message.quoted_item_id != 0) {
ContentItem? quoted_content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item_by_id(conversation, message.quoted_item_id);
if (quoted_content_item != null) {
Jid? quoted_sender = message.from;
string? quoted_stanza_id = stream_interactor.get_module(ContentItemStore.IDENTITY).get_message_id_for_content_item(conversation, quoted_content_item);
if (quoted_sender != null && quoted_stanza_id != null) {
Xep.Replies.set_reply_to(new_message, new Xep.Replies.ReplyTo(quoted_sender, quoted_stanza_id));
}
foreach (var fallback in message.get_fallbacks()) {
Xep.FallbackIndication.set_fallback(new_message, fallback);
}
}
}
MessageMarkup.add_spans(new_message, message.get_markups());
build_message_stanza(message, new_message, conversation); build_message_stanza(message, new_message, conversation);
pre_message_send(message, new_message, conversation); pre_message_send(message, new_message, conversation);
if (message.marked == Entities.Message.Marked.UNSENT || message.marked == Entities.Message.Marked.WONTSEND) return; if (message.marked == Entities.Message.Marked.UNSENT || message.marked == Entities.Message.Marked.WONTSEND) return;
@ -679,6 +449,10 @@ public class MessageProcessor : StreamInteractionModule, Object {
} }
} }
if (conversation.get_send_typing_setting(stream_interactor) == Conversation.Setting.ON) {
ChatStateNotifications.add_state_to_message(new_message, ChatStateNotifications.STATE_ACTIVE);
}
stream.get_module(MessageModule.IDENTITY).send_message.begin(stream, new_message, (_, res) => { stream.get_module(MessageModule.IDENTITY).send_message.begin(stream, new_message, (_, res) => {
try { try {
stream.get_module(MessageModule.IDENTITY).send_message.end(res); stream.get_module(MessageModule.IDENTITY).send_message.end(res);
@ -691,7 +465,7 @@ public class MessageProcessor : StreamInteractionModule, Object {
if (!conversation.type_.is_muc_semantic() && current_own_jid != null && !current_own_jid.equals(message.ourpart)) { if (!conversation.type_.is_muc_semantic() && current_own_jid != null && !current_own_jid.equals(message.ourpart)) {
message.ourpart = current_own_jid; message.ourpart = current_own_jid;
} }
} catch (IOStreamError e) { } catch (IOError e) {
message.marked = Entities.Message.Marked.UNSENT; message.marked = Entities.Message.Marked.UNSENT;
if (stream != stream_interactor.get_stream(conversation.account)) { if (stream != stream_interactor.get_stream(conversation.account)) {

View File

@ -42,6 +42,7 @@ public class MessageStorage : StreamInteractionModule, Object {
.with(db.message.type_, "=", (int) Util.get_message_type_for_conversation(conversation)) .with(db.message.type_, "=", (int) Util.get_message_type_for_conversation(conversation))
.order_by(db.message.time, "DESC") .order_by(db.message.time, "DESC")
.outer_join_with(db.message_correction, db.message_correction.message_id, db.message.id) .outer_join_with(db.message_correction, db.message_correction.message_id, db.message.id)
.outer_join_with(db.reply, db.reply.message_id, db.message.id)
.limit(count); .limit(count);
Gee.List<Message> ret = new LinkedList<Message>(Message.equals_func); Gee.List<Message> ret = new LinkedList<Message>(Message.equals_func);
@ -92,11 +93,20 @@ public class MessageStorage : StreamInteractionModule, Object {
RowOption row_option = db.message.select().with(db.message.id, "=", id) RowOption row_option = db.message.select().with(db.message.id, "=", id)
.outer_join_with(db.message_correction, db.message_correction.message_id, db.message.id) .outer_join_with(db.message_correction, db.message_correction.message_id, db.message.id)
.outer_join_with(db.reply, db.reply.message_id, db.message.id)
.row(); .row();
return create_message_from_row_opt(row_option, conversation); return create_message_from_row_opt(row_option, conversation);
} }
public Message? get_message_by_referencing_id(string id, Conversation conversation) {
if (conversation.type_ == Conversation.Type.CHAT) {
return stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_stanza_id(id, conversation);
} else {
return stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_server_id(id, conversation);
}
}
public Message? get_message_by_stanza_id(string stanza_id, Conversation conversation) { public Message? get_message_by_stanza_id(string stanza_id, Conversation conversation) {
if (messages_by_stanza_id.has_key(conversation)) { if (messages_by_stanza_id.has_key(conversation)) {
Message? message = messages_by_stanza_id[conversation][stanza_id]; Message? message = messages_by_stanza_id[conversation][stanza_id];
@ -111,11 +121,10 @@ public class MessageStorage : StreamInteractionModule, Object {
.with(db.message.type_, "=", (int) Util.get_message_type_for_conversation(conversation)) .with(db.message.type_, "=", (int) Util.get_message_type_for_conversation(conversation))
.with(db.message.stanza_id, "=", stanza_id) .with(db.message.stanza_id, "=", stanza_id)
.order_by(db.message.time, "DESC") .order_by(db.message.time, "DESC")
.outer_join_with(db.message_correction, db.message_correction.message_id, db.message.id); .outer_join_with(db.message_correction, db.message_correction.message_id, db.message.id)
.outer_join_with(db.reply, db.reply.message_id, db.message.id);
if (conversation.counterpart.resourcepart == null) { if (conversation.counterpart.resourcepart != null) {
query.with_null(db.message.counterpart_resource);
} else {
query.with(db.message.counterpart_resource, "=", conversation.counterpart.resourcepart); query.with(db.message.counterpart_resource, "=", conversation.counterpart.resourcepart);
} }
@ -138,7 +147,8 @@ public class MessageStorage : StreamInteractionModule, Object {
.with(db.message.type_, "=", (int) Util.get_message_type_for_conversation(conversation)) .with(db.message.type_, "=", (int) Util.get_message_type_for_conversation(conversation))
.with(db.message.server_id, "=", server_id) .with(db.message.server_id, "=", server_id)
.order_by(db.message.time, "DESC") .order_by(db.message.time, "DESC")
.outer_join_with(db.message_correction, db.message_correction.message_id, db.message.id); .outer_join_with(db.message_correction, db.message_correction.message_id, db.message.id)
.outer_join_with(db.reply, db.reply.message_id, db.message.id);
if (conversation.counterpart.resourcepart == null) { if (conversation.counterpart.resourcepart == null) {
query.with_null(db.message.counterpart_resource); query.with_null(db.message.counterpart_resource);
@ -189,6 +199,16 @@ public class MessageStorage : StreamInteractionModule, Object {
message_refs.remove_at(message_refs.size - 1); message_refs.remove_at(message_refs.size - 1);
} }
} }
public static string? get_reference_id(Message message) {
if (message.edit_to != null) return message.edit_to;
if (message.type_ == Message.Type.CHAT) {
return message.stanza_id;
} else {
return message.server_id;
}
}
} }
} }

View File

@ -24,7 +24,7 @@ public class ModuleManager {
return null; return null;
} }
public ArrayList<XmppStreamModule> get_modules(Account account, string? resource = null) { public ArrayList<XmppStreamModule> get_modules(Account account) {
ArrayList<XmppStreamModule> modules = new ArrayList<XmppStreamModule>(); ArrayList<XmppStreamModule> modules = new ArrayList<XmppStreamModule>();
lock (module_map) { lock (module_map) {
@ -34,7 +34,7 @@ public class ModuleManager {
foreach (XmppStreamModule module in module_map[account]) { foreach (XmppStreamModule module in module_map[account]) {
if (module.get_id() == Bind.Module.IDENTITY.id) { if (module.get_id() == Bind.Module.IDENTITY.id) {
((Bind.Module) module).requested_resource = resource ?? account.resourcepart; ((Bind.Module) module).requested_resource = account.resourcepart;
} else if (module.get_id() == Sasl.Module.IDENTITY.id) { } else if (module.get_id() == Sasl.Module.IDENTITY.id) {
((Sasl.Module) module).password = account.password; ((Sasl.Module) module).password = account.password;
} }
@ -57,8 +57,9 @@ public class ModuleManager {
module_map[account].add(new Xep.Bookmarks2.Module()); module_map[account].add(new Xep.Bookmarks2.Module());
module_map[account].add(new Presence.Module()); module_map[account].add(new Presence.Module());
module_map[account].add(new Xmpp.MessageModule()); module_map[account].add(new Xmpp.MessageModule());
module_map[account].add(new Xep.MessageArchiveManagement.Module()); module_map[account].add(new Xmpp.MessageArchiveManagement.Module());
module_map[account].add(new Xep.MessageCarbons.Module()); module_map[account].add(new Xep.MessageCarbons.Module());
module_map[account].add(new Xep.BitsOfBinary.Module());
module_map[account].add(new Xep.Muc.Module()); module_map[account].add(new Xep.Muc.Module());
module_map[account].add(new Xep.Pubsub.Module()); module_map[account].add(new Xep.Pubsub.Module());
module_map[account].add(new Xep.MessageDeliveryReceipts.Module()); module_map[account].add(new Xep.MessageDeliveryReceipts.Module());
@ -70,6 +71,7 @@ public class ModuleManager {
module_map[account].add(new StreamError.Module()); module_map[account].add(new StreamError.Module());
module_map[account].add(new Xep.InBandRegistration.Module()); module_map[account].add(new Xep.InBandRegistration.Module());
module_map[account].add(new Xep.HttpFileUpload.Module()); module_map[account].add(new Xep.HttpFileUpload.Module());
module_map[account].add(new Xep.Reactions.Module());
module_map[account].add(new Xep.Socks5Bytestreams.Module()); module_map[account].add(new Xep.Socks5Bytestreams.Module());
module_map[account].add(new Xep.InBandBytestreams.Module()); module_map[account].add(new Xep.InBandBytestreams.Module());
module_map[account].add(new Xep.Jingle.Module()); module_map[account].add(new Xep.Jingle.Module());
@ -80,6 +82,7 @@ public class ModuleManager {
module_map[account].add(new Xep.LastMessageCorrection.Module()); module_map[account].add(new Xep.LastMessageCorrection.Module());
module_map[account].add(new Xep.DirectMucInvitations.Module()); module_map[account].add(new Xep.DirectMucInvitations.Module());
module_map[account].add(new Xep.JingleMessageInitiation.Module()); module_map[account].add(new Xep.JingleMessageInitiation.Module());
module_map[account].add(new Xep.OccupantIds.Module());
module_map[account].add(new Xep.JingleRawUdp.Module()); module_map[account].add(new Xep.JingleRawUdp.Module());
module_map[account].add(new Xep.Muji.Module()); module_map[account].add(new Xep.Muji.Module());
module_map[account].add(new Xep.CallInvites.Module()); module_map[account].add(new Xep.CallInvites.Module());

View File

@ -21,13 +21,15 @@ public class MucManager : StreamInteractionModule, Object {
public signal void conference_removed(Account account, Jid jid); public signal void conference_removed(Account account, Jid jid);
private StreamInteractor stream_interactor; private StreamInteractor stream_interactor;
private HashMap<Account, HashSet<Jid>> mucs_todo = new HashMap<Account, HashSet<Jid>>(Account.hash_func, Account.equals_func); private HashMap<Account, HashSet<Jid>> mucs_joined = new HashMap<Account, HashSet<Jid>>(Account.hash_func, Account.equals_func);
private HashMap<Account, HashSet<Jid>> mucs_joining = new HashMap<Account, HashSet<Jid>>(Account.hash_func, Account.equals_func); private HashMap<Account, HashSet<Jid>> mucs_joining = new HashMap<Account, HashSet<Jid>>(Account.hash_func, Account.equals_func);
private HashMap<Account, HashMap<Jid, Cancellable>> mucs_sync_cancellables = new HashMap<Account, HashMap<Jid, Cancellable>>(Account.hash_func, Account.equals_func);
private HashMap<Jid, Xep.Muc.MucEnterError> enter_errors = new HashMap<Jid, Xep.Muc.MucEnterError>(Jid.hash_func, Jid.equals_func); private HashMap<Jid, Xep.Muc.MucEnterError> enter_errors = new HashMap<Jid, Xep.Muc.MucEnterError>(Jid.hash_func, Jid.equals_func);
private ReceivedMessageListener received_message_listener; private ReceivedMessageListener received_message_listener;
private HashMap<Account, BookmarksProvider> bookmarks_provider = new HashMap<Account, BookmarksProvider>(Account.hash_func, Account.equals_func); private HashMap<Account, BookmarksProvider> bookmarks_provider = new HashMap<Account, BookmarksProvider>(Account.hash_func, Account.equals_func);
private HashMap<Account, Gee.List<Jid>> invites = new HashMap<Account, Gee.List<Jid>>(Account.hash_func, Account.equals_func); private HashMap<Account, Gee.List<Jid>> invites = new HashMap<Account, Gee.List<Jid>>(Account.hash_func, Account.equals_func);
public HashMap<Account, Jid> default_muc_server = new HashMap<Account, Jid>(Account.hash_func, Account.equals_func); public HashMap<Account, Jid> default_muc_server = new HashMap<Account, Jid>(Account.hash_func, Account.equals_func);
private HashMap<Account, HashMap<Jid, string>> own_occupant_ids = new HashMap<Account, HashMap<Jid, string>>(Account.hash_func, Account.equals_func);
public static void start(StreamInteractor stream_interactor) { public static void start(StreamInteractor stream_interactor) {
MucManager m = new MucManager(stream_interactor); MucManager m = new MucManager(stream_interactor);
@ -52,10 +54,11 @@ public class MucManager : StreamInteractionModule, Object {
} }
return true; return true;
}); });
stream_interactor.get_module(MessageProcessor.IDENTITY).build_message_stanza.connect(on_build_message_stanza);
} }
// already_autojoin: Without this flag we'd be retrieving bookmarks (to check for autojoin) from the sender on every join // already_autojoin: Without this flag we'd be retrieving bookmarks (to check for autojoin) from the sender on every join
public async Muc.JoinResult? join(Account account, Jid jid, string? nick, string? password, bool already_autojoin = false) { public async Muc.JoinResult? join(Account account, Jid jid, string? nick, string? password, bool already_autojoin = false, Cancellable? cancellable = null) {
XmppStream? stream = stream_interactor.get_stream(account); XmppStream? stream = stream_interactor.get_stream(account);
if (stream == null) return null; if (stream == null) return null;
@ -68,17 +71,20 @@ public class MucManager : StreamInteractionModule, Object {
if (last_message != null) history_since = last_message.time; if (last_message != null) history_since = last_message.time;
} }
bool receive_history = true;
EntityInfo entity_info = stream_interactor.get_module(EntityInfo.IDENTITY);
bool can_do_mam = yield entity_info.has_feature(account, jid, Xmpp.MessageArchiveManagement.NS_URI);
if (can_do_mam) {
receive_history = false;
history_since = null;
}
if (!mucs_joining.has_key(account)) { if (!mucs_joining.has_key(account)) {
mucs_joining[account] = new HashSet<Jid>(Jid.hash_bare_func, Jid.equals_bare_func); mucs_joining[account] = new HashSet<Jid>(Jid.hash_bare_func, Jid.equals_bare_func);
} }
mucs_joining[account].add(jid); mucs_joining[account].add(jid);
if (!mucs_todo.has_key(account)) { Muc.JoinResult? res = yield stream.get_module(Xep.Muc.Module.IDENTITY).enter(stream, jid.bare_jid, nick_, password, history_since, receive_history, null);
mucs_todo[account] = new HashSet<Jid>(Jid.hash_bare_func, Jid.equals_bare_func);
}
mucs_todo[account].add(jid.with_resource(nick_));
Muc.JoinResult? res = yield stream.get_module(Xep.Muc.Module.IDENTITY).enter(stream, jid.bare_jid, nick_, password, history_since, null);
mucs_joining[account].remove(jid); mucs_joining[account].remove(jid);
@ -91,26 +97,60 @@ public class MucManager : StreamInteractionModule, Object {
Conversation joined_conversation = stream_interactor.get_module(ConversationManager.IDENTITY).create_conversation(jid, account, Conversation.Type.GROUPCHAT); Conversation joined_conversation = stream_interactor.get_module(ConversationManager.IDENTITY).create_conversation(jid, account, Conversation.Type.GROUPCHAT);
joined_conversation.nickname = nick; joined_conversation.nickname = nick;
stream_interactor.get_module(ConversationManager.IDENTITY).start_conversation(joined_conversation); stream_interactor.get_module(ConversationManager.IDENTITY).start_conversation(joined_conversation);
if (can_do_mam) {
var history_sync = stream_interactor.get_module(MessageProcessor.IDENTITY).history_sync;
if (conversation == null) {
// We never joined the conversation before, just fetch the latest MAM page
yield history_sync.fetch_latest_page(account, jid.bare_jid, null, new DateTime.from_unix_utc(0), cancellable);
} else {
// Fetch everything up to the last time the user actively joined
if (!mucs_sync_cancellables.has_key(account)) {
mucs_sync_cancellables[account] = new HashMap<Jid, Cancellable>();
}
if (!mucs_sync_cancellables[account].has_key(jid.bare_jid)) {
mucs_sync_cancellables[account][jid.bare_jid] = new Cancellable();
history_sync.fetch_everything.begin(account, jid.bare_jid, mucs_sync_cancellables[account][jid.bare_jid], conversation.active_last_changed, (_, res) => {
history_sync.fetch_everything.end(res);
mucs_sync_cancellables[account].unset(jid.bare_jid);
});
}
}
}
} else if (res.muc_error != null) { } else if (res.muc_error != null) {
// Join failed // Join failed
enter_errors[jid] = res.muc_error; enter_errors[jid] = res.muc_error;
} }
if (!mucs_joined.has_key(account)) {
mucs_joined[account] = new HashSet<Jid>(Jid.hash_bare_func, Jid.equals_bare_func);
}
mucs_joined[account].add(jid.with_resource(res.nick ?? nick_));
return res; return res;
} }
public void part(Account account, Jid jid) { public void part(Account account, Jid jid) {
if (!mucs_todo.has_key(account) || !mucs_todo[account].contains(jid)) return; if (mucs_joined.has_key(account) && mucs_joined[account].contains(jid)) {
mucs_joined[account].remove(jid);
mucs_todo[account].remove(jid); }
XmppStream? stream = stream_interactor.get_stream(account); XmppStream? stream = stream_interactor.get_stream(account);
if (stream == null) return; if (stream != null) {
unset_autojoin(account, stream, jid); unset_autojoin(account, stream, jid);
stream.get_module(Xep.Muc.Module.IDENTITY).exit(stream, jid.bare_jid); stream.get_module(Xep.Muc.Module.IDENTITY).exit(stream, jid.bare_jid);
}
Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation(jid, account); Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation(jid, account, Conversation.Type.GROUPCHAT);
if (conversation != null) stream_interactor.get_module(ConversationManager.IDENTITY).close_conversation(conversation); if (conversation != null) stream_interactor.get_module(ConversationManager.IDENTITY).close_conversation(conversation);
cancel_sync(account, jid);
}
private void cancel_sync(Account account, Jid jid) {
if (mucs_sync_cancellables.has_key(account) && mucs_sync_cancellables[account].has_key(jid.bare_jid) && !mucs_sync_cancellables[account][jid.bare_jid].is_cancelled()) {
mucs_sync_cancellables[account][jid.bare_jid].cancel();
}
} }
public async DataForms.DataForm? get_config_form(Account account, Jid jid) { public async DataForms.DataForm? get_config_form(Account account, Jid jid) {
@ -143,9 +183,9 @@ public class MucManager : StreamInteractionModule, Object {
conversation.nickname = new_nick; conversation.nickname = new_nick;
if (mucs_todo.has_key(conversation.account)) { if (mucs_joined.has_key(conversation.account)) {
mucs_todo[conversation.account].remove(conversation.counterpart); mucs_joined[conversation.account].remove(conversation.counterpart);
mucs_todo[conversation.account].add(conversation.counterpart.with_resource(new_nick)); mucs_joined[conversation.account].add(conversation.counterpart.with_resource(new_nick));
} }
// Update nick in bookmark // Update nick in bookmark
@ -193,15 +233,8 @@ public class MucManager : StreamInteractionModule, Object {
//the term `private room` is a short hand for members-only+non-anonymous rooms //the term `private room` is a short hand for members-only+non-anonymous rooms
public bool is_private_room(Account account, Jid jid) { public bool is_private_room(Account account, Jid jid) {
XmppStream? stream = stream_interactor.get_stream(account); var entity_info = stream_interactor.get_module(EntityInfo.IDENTITY);
if (stream == null) { return entity_info.has_feature_offline(account, jid, "muc_membersonly") && entity_info.has_feature_offline(account, jid, "muc_nonanonymous");
return false;
}
Xep.Muc.Flag? flag = stream.get_flag(Xep.Muc.Flag.IDENTITY);
if (flag == null) {
return false;
}
return flag.has_room_feature(jid, Xep.Muc.Feature.NON_ANONYMOUS) && flag.has_room_feature(jid, Xep.Muc.Feature.MEMBERS_ONLY);
} }
public bool is_moderated_room(Account account, Jid jid) { public bool is_moderated_room(Account account, Jid jid) {
@ -302,6 +335,14 @@ public class MucManager : StreamInteractionModule, Object {
return null; return null;
} }
public Jid? get_occupant_jid(Account account, Jid room, Jid occupant_real_jid) {
Xep.Muc.Flag? flag = get_muc_flag(account);
if (flag != null) {
return flag.get_occupant_jid(occupant_real_jid, room);
}
return null;
}
public Xep.Muc.Role? get_role(Jid jid, Account account) { public Xep.Muc.Role? get_role(Jid jid, Account account) {
Xep.Muc.Flag? flag = get_muc_flag(account); Xep.Muc.Flag? flag = get_muc_flag(account);
if (flag != null) { if (flag != null) {
@ -365,8 +406,16 @@ public class MucManager : StreamInteractionModule, Object {
return get_own_jid(jid, account) != null; return get_own_jid(jid, account) != null;
} }
public string? get_own_occupant_id(Account account, Jid muc_jid) {
if (own_occupant_ids.has_key(account) && own_occupant_ids[account].has_key(muc_jid)) {
return own_occupant_ids[account][muc_jid];
}
return null;
}
private void on_account_added(Account account) { private void on_account_added(Account account) {
stream_interactor.module_manager.get_module(account, Xep.Muc.Module.IDENTITY).self_removed_from_room.connect( (stream, jid, code) => { stream_interactor.module_manager.get_module(account, Xep.Muc.Module.IDENTITY).self_removed_from_room.connect( (stream, jid, code) => {
cancel_sync(account, jid);
left(account, jid); left(account, jid);
}); });
stream_interactor.module_manager.get_module(account, Xep.Muc.Module.IDENTITY).subject_set.connect( (stream, subject, jid) => { stream_interactor.module_manager.get_module(account, Xep.Muc.Module.IDENTITY).subject_set.connect( (stream, subject, jid) => {
@ -392,6 +441,12 @@ public class MucManager : StreamInteractionModule, Object {
private_room_occupant_updated(account, room, occupant); private_room_occupant_updated(account, room, occupant);
} }
}); });
stream_interactor.module_manager.get_module(account, Xep.OccupantIds.Module.IDENTITY).received_own_occupant_id.connect( (stream, jid, occupant_id) => {
if (!own_occupant_ids.has_key(account)) {
own_occupant_ids[account] = new HashMap<Jid, string>(Jid.hash_bare_func, Jid.equals_bare_func);
}
own_occupant_ids[account][jid] = occupant_id;
});
} }
private async void search_default_muc_server(Account account) { private async void search_default_muc_server(Account account) {
@ -425,6 +480,14 @@ public class MucManager : StreamInteractionModule, Object {
} }
private async void on_stream_negotiated(Account account, XmppStream stream) { private async void on_stream_negotiated(Account account, XmppStream stream) {
if (mucs_sync_cancellables.has_key(account)) {
foreach (Cancellable cancellable in mucs_sync_cancellables[account].values) {
if (!cancellable.is_cancelled()) {
cancellable.cancel();
}
}
}
yield initialize_bookmarks_provider(account); yield initialize_bookmarks_provider(account);
Set<Conference>? conferences = yield bookmarks_provider[account].get_conferences(stream); Set<Conference>? conferences = yield bookmarks_provider[account].get_conferences(stream);
@ -568,8 +631,7 @@ public class MucManager : StreamInteractionModule, Object {
} else if (conversation.active && !conference.autojoin) { } else if (conversation.active && !conference.autojoin) {
part(account, conference.jid); part(account, conference.jid);
} }
} } else if (conference.autojoin) {
if (conference.autojoin) {
join.begin(account, conference.jid, conference.nick, conference.password); join.begin(account, conference.jid, conference.nick, conference.password);
} }
conference_added(account, conference); conference_added(account, conference);
@ -583,13 +645,19 @@ public class MucManager : StreamInteractionModule, Object {
conference_removed(account, jid); conference_removed(account, jid);
} }
private void on_build_message_stanza(Entities.Message message, Xmpp.MessageStanza message_stanza, Conversation conversation) {
if (conversation.type_ == Conversation.Type.GROUPCHAT_PM) {
Xmpp.Xep.Muc.add_muc_pm_message_stanza_x_node(message_stanza);
}
}
private void self_ping(Account account) { private void self_ping(Account account) {
XmppStream? stream = stream_interactor.get_stream(account); XmppStream? stream = stream_interactor.get_stream(account);
if (stream == null) return; if (stream == null) return;
if (!mucs_todo.has_key(account)) return; if (!mucs_joined.has_key(account)) return;
foreach (Jid jid in mucs_todo[account]) { foreach (Jid jid in mucs_joined[account]) {
bool joined = false; bool joined = false;
@ -598,7 +666,7 @@ public class MucManager : StreamInteractionModule, Object {
}); });
Timeout.add_seconds(10, () => { Timeout.add_seconds(10, () => {
if (joined || !mucs_todo.has_key(account) || stream_interactor.get_stream(account) != stream) return false; if (joined || !mucs_joined.has_key(account) || stream_interactor.get_stream(account) != stream) return false;
join.begin(account, jid.bare_jid, jid.resourcepart, null, true); join.begin(account, jid.bare_jid, jid.resourcepart, null, true);
return false; return false;
@ -634,6 +702,10 @@ public class MucManager : StreamInteractionModule, Object {
if (m != null) { if (m != null) {
// For own messages from this device (msg is a duplicate) // For own messages from this device (msg is a duplicate)
m.marked = Message.Marked.RECEIVED; m.marked = Message.Marked.RECEIVED;
string? server_id = Xep.UniqueStableStanzaIDs.get_stanza_id(stanza, m.counterpart.bare_jid);
if (server_id != null) {
m.server_id = server_id;
}
} }
// For own messages from other devices (msg is not a duplicate msg) // For own messages from other devices (msg is not a duplicate msg)
message.marked = Message.Marked.RECEIVED; message.marked = Message.Marked.RECEIVED;

View File

@ -0,0 +1,464 @@
using Gee;
using Qlite;
using Xmpp;
using Xmpp.Xep;
using Dino.Entities;
public class Dino.Reactions : StreamInteractionModule, Object {
public static ModuleIdentity<Reactions> IDENTITY = new ModuleIdentity<Reactions>("reactions");
public string id { get { return IDENTITY.id; } }
public signal void reaction_added(Account account, int content_item_id, Jid jid, string reaction);
public signal void reaction_removed(Account account, int content_item_id, Jid jid, string reaction);
private StreamInteractor stream_interactor;
private Database db;
private HashMap<string, Gee.List<ReactionInfo>> reaction_infos = new HashMap<string, Gee.List<ReactionInfo>>();
public static void start(StreamInteractor stream_interactor, Database database) {
Reactions m = new Reactions(stream_interactor, database);
stream_interactor.add_module(m);
}
private Reactions(StreamInteractor stream_interactor, Database database) {
this.stream_interactor = stream_interactor;
this.db = database;
stream_interactor.account_added.connect(on_account_added);
stream_interactor.get_module(ContentItemStore.IDENTITY).new_item.connect(on_new_item);
}
public void add_reaction(Conversation conversation, ContentItem content_item, string reaction) {
Gee.List<string> reactions = get_own_reactions(conversation, content_item);
if (!reactions.contains(reaction)) {
reactions.add(reaction);
}
try {
send_reactions(conversation, content_item, reactions);
reaction_added(conversation.account, content_item.id, conversation.account.bare_jid, reaction);
} catch (IOError e) {}
}
public void remove_reaction(Conversation conversation, ContentItem content_item, string reaction) {
Gee.List<string> reactions = get_own_reactions(conversation, content_item);
reactions.remove(reaction);
try {
send_reactions(conversation, content_item, reactions);
reaction_removed(conversation.account, content_item.id, conversation.account.bare_jid, reaction);
} catch (IOError e) {}
}
public Gee.List<ReactionUsers> get_item_reactions(Conversation conversation, ContentItem content_item) {
if (conversation.type_ == Conversation.Type.CHAT) {
return get_chat_message_reactions(conversation.account, content_item);
} else {
return get_muc_message_reactions(conversation.account, content_item);
}
}
public bool conversation_supports_reactions(Conversation conversation) {
if (conversation.type_ == Conversation.Type.CHAT) {
return true;
} else {
// 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);
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));
if (!server_supports_sid) return false;
bool? supports_occupant_ids = entity_info.has_feature_cached(conversation.account, conversation.counterpart, Xep.OccupantIds.NS_URI);
if (supports_occupant_ids) return true;
return stream_interactor.get_module(MucManager.IDENTITY).is_private_room(conversation.account, conversation.counterpart);
}
}
private void send_reactions(Conversation conversation, ContentItem content_item, Gee.List<string> reactions) throws IOError {
string? message_id = stream_interactor.get_module(ContentItemStore.IDENTITY).get_message_id_for_content_item(conversation, content_item);
if (message_id == null) throw new IOError.FAILED("No message for content_item");
XmppStream? stream = stream_interactor.get_stream(conversation.account);
if (stream == null) throw new IOError.NOT_CONNECTED("No stream");
var reactions_module = stream.get_module(Xmpp.Xep.Reactions.Module.IDENTITY);
if (conversation.type_ == Conversation.Type.GROUPCHAT) {
reactions_module.send_reaction.begin(stream, conversation.counterpart, "groupchat", message_id, reactions);
// We save the reaction when it gets reflected back to us
} else if (conversation.type_ == Conversation.Type.GROUPCHAT_PM) {
reactions_module.send_reaction.begin(stream, conversation.counterpart, "chat", message_id, reactions);
} else if (conversation.type_ == Conversation.Type.CHAT) {
int64 now_millis = GLib.get_real_time () / 1000;
reactions_module.send_reaction.begin(stream, conversation.counterpart, "chat", message_id, reactions, (_, res) => {
try {
reactions_module.send_reaction.end(res);
save_chat_reactions(conversation.account, conversation.account.bare_jid, content_item.id, now_millis, reactions);
} catch (IOError e) {}
});
}
}
private Gee.List<string> get_own_reactions(Conversation conversation, ContentItem content_item) {
if (conversation.type_ == Conversation.Type.CHAT) {
return get_chat_user_reactions(conversation.account, content_item.id, conversation.account.bare_jid)
.emojis;
} else if (conversation.type_ == Conversation.Type.GROUPCHAT) {
string own_occupant_id = stream_interactor.get_module(MucManager.IDENTITY).get_own_occupant_id(conversation.account, content_item.jid);
return get_muc_user_reactions(conversation.account, content_item.id, own_occupant_id, conversation.account.bare_jid)
.emojis;
}
return new ArrayList<string>();
}
private class ReactionsTime {
public Gee.List<string>? emojis = null;
public int64 time = -1;
}
private ReactionsTime get_chat_user_reactions(Account account, int content_item_id, Jid jid) {
int jid_id = db.get_jid_id(jid);
QueryBuilder query = db.reaction.select()
.with(db.reaction.account_id, "=", account.id)
.with(db.reaction.content_item_id, "=", content_item_id)
.with(db.reaction.jid_id, "=", jid_id);
RowOption row = query.single().row();
ReactionsTime ret = new ReactionsTime();
if (row.is_present()) {
ret.emojis = string_to_emoji_list(row[db.reaction.emojis]);
ret.time = row[db.reaction.time];
} else {
ret.emojis = new ArrayList<string>();
ret.time = -1;
}
return ret;
}
private ReactionsTime get_muc_user_reactions(Account account, int content_item_id, string? occupant_id, Jid? real_jid) {
if (occupant_id == null && real_jid == null) critical("Need occupant id or real jid of a reaction");
QueryBuilder query = db.reaction.select()
.with(db.reaction.account_id, "=", account.id)
.with(db.reaction.content_item_id, "=", content_item_id)
.outer_join_with(db.occupantid, db.occupantid.id, db.reaction.occupant_id);
if (occupant_id != null) {
query.with(db.occupantid.occupant_id, "=", occupant_id);
} else if (real_jid != null) {
query.with(db.reaction.jid_id, "=", db.get_jid_id(real_jid));
}
RowOption row = query.single().row();
ReactionsTime ret = new ReactionsTime();
if (row.is_present()) {
ret.emojis = string_to_emoji_list(row[db.reaction.emojis]);
ret.time = row[db.reaction.time];
} else {
ret.emojis = new ArrayList<string>();
ret.time = -1;
}
return ret;
}
private Gee.List<string> string_to_emoji_list(string emoji_str) {
ArrayList<string> ret = new ArrayList<string>();
foreach (string emoji in emoji_str.split(",")) {
if (emoji.length != 0)
ret.add(emoji);
}
return ret;
}
public Gee.List<ReactionUsers> get_chat_message_reactions(Account account, ContentItem content_item) {
QueryBuilder select = db.reaction.select()
.with(db.reaction.account_id, "=", account.id)
.with(db.reaction.content_item_id, "=", content_item.id)
.order_by(db.reaction.time, "DESC");
var ret = new ArrayList<ReactionUsers>();
var index = new HashMap<string, ReactionUsers>();
foreach (Row row in select) {
string emoji_str = row[db.reaction.emojis];
Jid jid = db.get_jid_by_id(row[db.reaction.jid_id]);
foreach (string emoji in emoji_str.split(",")) {
if (!index.has_key(emoji)) {
index[emoji] = new ReactionUsers() { reaction=emoji, jids=new ArrayList<Jid>(Jid.equals_func) };
ret.add(index[emoji]);
}
index[emoji].jids.add(jid);
}
}
return ret;
}
public Gee.List<ReactionUsers> get_muc_message_reactions(Account account, ContentItem content_item) {
QueryBuilder select = db.reaction.select()
.with(db.reaction.account_id, "=", account.id)
.with(db.reaction.content_item_id, "=", content_item.id)
.outer_join_with(db.occupantid, db.occupantid.id, db.reaction.occupant_id)
.outer_join_with(db.jid, db.jid.id, db.reaction.jid_id)
.order_by(db.reaction.time, "DESC");
string? own_occupant_id = stream_interactor.get_module(MucManager.IDENTITY).get_own_occupant_id(account, content_item.jid);
var ret = new ArrayList<ReactionUsers>();
var index = new HashMap<string, ReactionUsers>();
foreach (Row row in select) {
string emoji_str = row[db.reaction.emojis];
Jid jid = null;
if (!db.jid.bare_jid.is_null(row)) {
jid = new Jid(row[db.jid.bare_jid]);
} else if (!db.occupantid.occupant_id.is_null(row)) {
if (row[db.occupantid.occupant_id] == own_occupant_id) {
jid = account.bare_jid;
} else {
string nick = row[db.occupantid.last_nick];
jid = content_item.jid.with_resource(nick);
}
} else {
warning("Reaction with neither JID nor occupant id");
}
foreach (string emoji in emoji_str.split(",")) {
if (!index.has_key(emoji)) {
index[emoji] = new ReactionUsers() { reaction=emoji, jids=new ArrayList<Jid>(Jid.equals_func) };
ret.add(index[emoji]);
}
index[emoji].jids.add(jid);
}
}
return ret;
}
private void on_account_added(Account account) {
// TODO get time from delays
stream_interactor.module_manager.get_module(account, Xmpp.Xep.Reactions.Module.IDENTITY).received_reactions.connect((stream, from_jid, message_id, reactions, stanza) => {
on_reaction_received.begin(account, from_jid, message_id, reactions, stanza);
});
}
private async void on_reaction_received(Account account, Jid from_jid, string message_id, Gee.List<string> reactions, MessageStanza stanza) {
if (stanza.type_ == MessageStanza.TYPE_GROUPCHAT) {
// Apply the same restrictions for incoming reactions as we do on sending them
Conversation muc_conversation = stream_interactor.get_module(ConversationManager.IDENTITY).approx_conversation_for_stanza(from_jid, account.bare_jid, account, MessageStanza.TYPE_GROUPCHAT);
bool muc_supports_reactions = conversation_supports_reactions(muc_conversation);
if (!muc_supports_reactions) return;
}
Message reaction_message = yield stream_interactor.get_module(MessageProcessor.IDENTITY).parse_message_stanza(account, stanza);
Conversation conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation_for_message(reaction_message);
int content_item_id = stream_interactor.get_module(ContentItemStore.IDENTITY).get_content_item_id_for_message_id(conversation, message_id);
var reaction_info = new ReactionInfo() { conversation=conversation, from_jid=from_jid, reactions=reactions, stanza=stanza, received_time=new DateTime.now() };
if (content_item_id != -1) {
process_reaction_for_message(content_item_id, reaction_info);
return;
}
// Store reaction infos for later processing after we got the message
debug("Got reaction for %s but dont have message yet %s", message_id, db.get_jid_id(stanza.from.bare_jid).to_string());
if (!reaction_infos.has_key(message_id)) {
reaction_infos[message_id] = new ArrayList<ReactionInfo>();
}
reaction_infos[message_id].add(reaction_info);
}
/*
* When we get a new ContentItem, check if we have any reactions cached that apply to it.
* If so, process the reactions, map and store them.
*/
private void on_new_item(ContentItem item, Conversation conversation) {
string? stanza_id = stream_interactor.get_module(ContentItemStore.IDENTITY).get_message_id_for_content_item(conversation, item);
if (stanza_id == null) return;
Gee.List<ReactionInfo>? reaction_info_list = reaction_infos[stanza_id];
if (reaction_info_list == null) return;
Message? message = stream_interactor.get_module(ContentItemStore.IDENTITY).get_message_for_content_item(conversation, item);
if (message == null) return;
// Check if the (or potentially which) reaction fits the message
var applicable_reactions = new ArrayList<ReactionInfo>();
applicable_reactions.add_all_iterator(reaction_info_list.filter(info => info.conversation.equals(conversation)));
foreach (ReactionInfo applicable_reaction in applicable_reactions) {
reaction_info_list.remove(applicable_reaction);
if (reaction_info_list.is_empty) {
reaction_infos.unset(stanza_id);
}
debug("Got ContentItem for reaction %s", stanza_id);
process_reaction_for_message(item.id, applicable_reaction);
}
}
private Message? get_message_for_reaction(Conversation conversation, string message_id) {
// Query message from a specific account and counterpart. This also makes sure it's a valid reaction for the message.
if (conversation.type_ == Conversation.Type.CHAT) {
return stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_stanza_id(message_id, conversation);
} else {
return stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_server_id(message_id, conversation);
}
}
private void process_reaction_for_message(int content_item_id, ReactionInfo reaction_info) {
Account account = reaction_info.conversation.account;
MessageStanza stanza = reaction_info.stanza;
Jid from_jid = reaction_info.from_jid;
Gee.List<string> reactions = reaction_info.reactions;
// Get reaction time
DateTime? reaction_time = null;
DelayedDelivery.MessageFlag? delayed_message_flag = DelayedDelivery.MessageFlag.get_flag(stanza);
if (delayed_message_flag != null) {
reaction_time = delayed_message_flag.datetime;
}
if (reaction_time == null) {
MessageArchiveManagement.MessageFlag? mam_message_flag = MessageArchiveManagement.MessageFlag.get_flag(stanza);
if (mam_message_flag != null) reaction_time = mam_message_flag.server_time;
}
var time_now = new DateTime.now_local();
if (reaction_time == null) reaction_time = time_now;
if (reaction_time.compare(time_now) > 0) {
reaction_time = reaction_info.received_time;
}
int64 reaction_time_long = (int64) (reaction_time.to_unix() * 1000 + reaction_time.get_microsecond() / 1000);
// Get current reactions
string? occupant_id = OccupantIds.get_occupant_id(stanza.stanza);
Jid? real_jid = stream_interactor.get_module(MucManager.IDENTITY).get_real_jid(from_jid, account);
if (stanza.type_ == MessageStanza.TYPE_GROUPCHAT && occupant_id == null && real_jid == null) {
warning("Attempting to add reaction to message w/o knowing occupant id or real jid");
return;
}
ReactionsTime reactions_time = null;
if (stanza.type_ == MessageStanza.TYPE_GROUPCHAT) {
reactions_time = get_muc_user_reactions(account, content_item_id, occupant_id, real_jid);
} else {
reactions_time = get_chat_user_reactions(account, content_item_id, from_jid);
}
if (reaction_time_long <= reactions_time.time) {
// We already have a more recent reaction
return;
}
// Save reactions
if (stanza.type_ == MessageStanza.TYPE_GROUPCHAT) {
save_muc_reactions(account, content_item_id, from_jid, occupant_id, real_jid, reaction_time_long, reactions);
} else {
save_chat_reactions(account, from_jid, content_item_id, reaction_time_long, reactions);
}
// Notify about reaction changes
Gee.List<string>? current_reactions = reactions_time.emojis;
Jid signal_jid = from_jid;
if (stanza.type_ == MessageStanza.TYPE_GROUPCHAT &&
signal_jid.equals(stream_interactor.get_module(MucManager.IDENTITY).get_own_jid(from_jid, account))) {
signal_jid = account.bare_jid;
} else if (stanza.type_ == MessageStanza.TYPE_CHAT) {
signal_jid = signal_jid.bare_jid;
}
foreach (string current_reaction in current_reactions) {
if (!reactions.contains(current_reaction)) {
reaction_removed(account, content_item_id, signal_jid, current_reaction);
}
}
foreach (string new_reaction in reactions) {
if (!current_reactions.contains(new_reaction)) {
reaction_added(account, content_item_id, signal_jid, new_reaction);
}
}
debug("reactions were: ");
foreach (string reac in current_reactions) {
debug(reac);
}
debug("reactions new : ");
foreach (string reac in reactions) {
debug(reac);
}
}
private void save_chat_reactions(Account account, Jid jid, int content_item_id, int64 reaction_time, Gee.List<string> reactions) {
var emoji_builder = new StringBuilder();
for (int i = 0; i < reactions.size; i++) {
if (i != 0) emoji_builder.append(",");
emoji_builder.append(reactions[i]);
}
db.reaction.upsert()
.value(db.reaction.account_id, account.id, true)
.value(db.reaction.content_item_id, content_item_id, true)
.value(db.reaction.jid_id, db.get_jid_id(jid), true)
.value(db.reaction.emojis, emoji_builder.str, false)
.value(db.reaction.time, (long)reaction_time, false)
.perform();
}
private void save_muc_reactions(Account account, int content_item_id, Jid jid, string? occupant_id, Jid? real_jid, int64 reaction_time, Gee.List<string> reactions) {
assert(occupant_id != null || real_jid != null);
int jid_id = db.get_jid_id(jid);
var emoji_builder = new StringBuilder();
for (int i = 0; i < reactions.size; i++) {
if (i != 0) emoji_builder.append(",");
emoji_builder.append(reactions[i]);
}
var builder = db.reaction.upsert()
.value(db.reaction.account_id, account.id, true)
.value(db.reaction.content_item_id, content_item_id, true)
.value(db.reaction.emojis, emoji_builder.str, false)
.value(db.reaction.time, (long)reaction_time, false);
if (real_jid != null) {
builder.value(db.reaction.jid_id, db.get_jid_id(real_jid), occupant_id == null);
}
if (occupant_id != null) {
RowOption row = db.occupantid.select()
.with(db.occupantid.account_id, "=", account.id)
.with(db.occupantid.jid_id, "=", jid_id)
.with(db.occupantid.occupant_id, "=", occupant_id)
.single().row();
int occupant_db_id = -1;
if (row.is_present()) {
occupant_db_id = row[db.occupantid.id];
} else {
occupant_db_id = (int)db.occupantid.upsert()
.value(db.occupantid.account_id, account.id, true)
.value(db.occupantid.jid_id, jid_id, true)
.value(db.occupantid.occupant_id, occupant_id, true)
.value(db.occupantid.last_nick, jid.resourcepart, false)
.perform();
}
builder.value(db.reaction.occupant_id, occupant_db_id, true);
}
builder.perform();
}
}
public class Dino.ReactionUsers {
public string reaction { get; set; }
public Gee.List<Jid> jids { get; set; }
}
public class Dino.ReactionInfo {
public Conversation conversation { get; set; }
public Jid from_jid { get; set; }
public Gee.List<string> reactions { get; set; }
public MessageStanza stanza { get; set; }
public DateTime received_time { get; set; }
}

View File

@ -71,6 +71,12 @@ public class Register : StreamInteractionModule, Object{
return ret; return ret;
} }
public async string? change_password(Account account, string new_pw){
XmppStream stream = stream_interactor.get_stream(account);
if (stream == null) return null;
return (yield stream.get_module(Xep.InBandRegistration.Module.IDENTITY).change_password(stream, account.full_jid, new_pw)).condition;
}
public class ServerAvailabilityReturn { public class ServerAvailabilityReturn {
public bool available { get; set; } public bool available { get; set; }
public TlsCertificateFlags? error_flags { get; set; } public TlsCertificateFlags? error_flags { get; set; }
@ -135,6 +141,7 @@ public class Register : StreamInteractionModule, Object{
Gee.List<XmppStreamModule> list = new ArrayList<XmppStreamModule>(); Gee.List<XmppStreamModule> list = new ArrayList<XmppStreamModule>();
list.add(new Iq.Module()); list.add(new Iq.Module());
list.add(new Xep.InBandRegistration.Module()); list.add(new Xep.InBandRegistration.Module());
list.add(new Xep.BitsOfBinary.Module());
XmppStreamResult stream_result = yield Xmpp.establish_stream(jid.domain_jid, list, Application.print_xmpp, XmppStreamResult stream_result = yield Xmpp.establish_stream(jid.domain_jid, list, Application.print_xmpp,
(peer_cert, errors) => { return ConnectionManager.on_invalid_certificate(jid.domainpart, peer_cert, errors); } (peer_cert, errors) => { return ConnectionManager.on_invalid_certificate(jid.domainpart, peer_cert, errors); }

View File

@ -0,0 +1,103 @@
using Gee;
using Qlite;
using Xmpp;
using Xmpp.Xep;
using Dino.Entities;
public class Dino.Replies : StreamInteractionModule, Object {
public static ModuleIdentity<Replies> IDENTITY = new ModuleIdentity<Replies>("reply");
public string id { get { return IDENTITY.id; } }
private StreamInteractor stream_interactor;
private Database db;
private HashMap<Conversation, HashMap<string, Gee.List<Message>>> unmapped_replies = new HashMap<Conversation, HashMap<string, Gee.List<Message>>>();
private ReceivedMessageListener received_message_listener;
public static void start(StreamInteractor stream_interactor, Database db) {
Replies m = new Replies(stream_interactor, db);
stream_interactor.add_module(m);
}
private Replies(StreamInteractor stream_interactor, Database db) {
this.stream_interactor = stream_interactor;
this.db = db;
this.received_message_listener = new ReceivedMessageListener(stream_interactor, this);
stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(received_message_listener);
}
public ContentItem? get_quoted_content_item(Message message, Conversation conversation) {
if (message.quoted_item_id == 0) return null;
RowOption row_option = db.reply.select().with(db.reply.message_id, "=", message.id).row();
if (row_option.is_present()) {
return stream_interactor.get_module(ContentItemStore.IDENTITY).get_item_by_id(conversation, row_option[db.reply.quoted_content_item_id]);
}
return null;
}
private void on_incoming_message(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
// Check if a previous message was in reply to this one
var reply_qry = db.reply.select();
if (conversation.type_ == Conversation.Type.GROUPCHAT) {
reply_qry.with(db.reply.quoted_message_stanza_id, "=", message.server_id);
} else {
reply_qry.with(db.reply.quoted_message_stanza_id, "=", message.stanza_id);
}
reply_qry.join_with(db.message, db.reply.message_id, db.message.id)
.with(db.message.account_id, "=", conversation.account.id)
.with(db.message.counterpart_id, "=", db.get_jid_id(conversation.counterpart))
.with(db.message.time, ">", (long)message.time.to_unix())
.order_by(db.message.time, "DESC");
foreach (Row reply_row in reply_qry) {
ContentItem? message_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item_by_foreign(conversation, 1, message.id);
Message? reply_message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_id(reply_row[db.message.id], conversation);
if (message_item != null && reply_message != null) {
reply_message.set_quoted_item(message_item.id);
}
}
// Handle if this message is a reply
Xep.Replies.ReplyTo? reply_to = Xep.Replies.get_reply_to(stanza);
if (reply_to == null) return;
ContentItem? quoted_content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_content_item_for_message_id(conversation, reply_to.to_message_id);
if (quoted_content_item == null) return;
message.set_quoted_item(quoted_content_item.id);
}
private class ReceivedMessageListener : MessageListener {
public string[] after_actions_const = new string[]{ "STORE", "STORE_CONTENT_ITEM" };
public override string action_group { get { return "Quote"; } }
public override string[] after_actions { get { return after_actions_const; } }
private Replies outer;
public ReceivedMessageListener(StreamInteractor stream_interactor, Replies outer) {
this.outer = outer;
}
public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
outer.on_incoming_message(message, stanza, conversation);
return false;
}
}
}
namespace Dino {
public string message_body_without_reply_fallback(Message message) {
string body = message.body;
foreach (var fallback in message.get_fallbacks()) {
if (fallback.ns_uri == Xep.Replies.NS_URI && message.quoted_item_id > 0) {
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;
}
}

View File

@ -133,6 +133,7 @@ public class RosterStoreImpl : Roster.Storage, Object {
.value(db.roster.jid, item.jid.to_string(), true) .value(db.roster.jid, item.jid.to_string(), true)
.value(db.roster.handle, item.name) .value(db.roster.handle, item.name)
.value(db.roster.subscription, item.subscription) .value(db.roster.subscription, item.subscription)
.value(db.roster.ask, item.ask)
.perform(); .perform();
} }

View File

@ -0,0 +1,80 @@
using Gdk;
using GLib;
using Gee;
using Xmpp;
using Xmpp.Xep;
using Dino.Entities;
namespace Dino {
public interface FileMetadataProvider : Object {
public abstract bool supports_file(File file);
public abstract async void fill_metadata(File file, Xep.FileMetadataElement.FileMetadata metadata);
}
class GenericFileMetadataProvider: Dino.FileMetadataProvider, Object {
public bool supports_file(File file) {
return true;
}
public async void fill_metadata(File file, Xep.FileMetadataElement.FileMetadata metadata) {
FileInfo info = file.query_info("*", FileQueryInfoFlags.NONE);
metadata.name = info.get_display_name();
metadata.mime_type = info.get_content_type();
metadata.size = info.get_size();
metadata.date = info.get_modification_date_time();
var checksum_types = new ArrayList<ChecksumType>.wrap(new ChecksumType[] { ChecksumType.SHA256, ChecksumType.SHA512 });
var file_hashes = yield compute_file_hashes(file, checksum_types);
metadata.hashes.add(new CryptographicHashes.Hash.with_checksum(ChecksumType.SHA256, file_hashes[ChecksumType.SHA256]));
metadata.hashes.add(new CryptographicHashes.Hash.with_checksum(ChecksumType.SHA512, file_hashes[ChecksumType.SHA512]));
}
}
public class ImageFileMetadataProvider: Dino.FileMetadataProvider, Object {
public bool supports_file(File file) {
string mime_type = file.query_info("*", FileQueryInfoFlags.NONE).get_content_type();
return Dino.Util.is_pixbuf_supported_mime_type(mime_type);
}
private const int[] THUMBNAIL_DIMS = { 1, 2, 3, 4, 8 };
private const string IMAGE_TYPE = "png";
private const string MIME_TYPE = "image/png";
public async void fill_metadata(File file, Xep.FileMetadataElement.FileMetadata metadata) {
Pixbuf pixbuf = new Pixbuf.from_stream(yield file.read_async());
metadata.width = pixbuf.get_width();
metadata.height = pixbuf.get_height();
float ratio = (float)metadata.width / (float) metadata.height;
int thumbnail_width = -1;
int thumbnail_height = -1;
float diff = float.INFINITY;
for (int i = 0; i < THUMBNAIL_DIMS.length; i++) {
int test_width = THUMBNAIL_DIMS[i];
int test_height = THUMBNAIL_DIMS[THUMBNAIL_DIMS.length - 1 - i];
float test_ratio = (float)test_width / (float)test_height;
float test_diff = (test_ratio - ratio).abs();
if (test_diff < diff) {
thumbnail_width = test_width;
thumbnail_height = test_height;
diff = test_diff;
}
}
Pixbuf thumbnail_pixbuf = pixbuf.scale_simple(thumbnail_width, thumbnail_height, InterpType.BILINEAR);
uint8[] buffer;
thumbnail_pixbuf.save_to_buffer(out buffer, IMAGE_TYPE);
var thumbnail = new Xep.JingleContentThumbnails.Thumbnail();
thumbnail.data = new Bytes.take(buffer);
thumbnail.media_type = MIME_TYPE;
thumbnail.width = thumbnail_width;
thumbnail.height = thumbnail_height;
metadata.thumbnails.add(thumbnail);
}
}
}

View File

@ -0,0 +1,168 @@
using Gdk;
using Gee;
using Xmpp;
using Xmpp.Xep;
using Dino.Entities;
public class Dino.StatelessFileSharing : StreamInteractionModule, Object {
public static ModuleIdentity<StatelessFileSharing> IDENTITY = new ModuleIdentity<StatelessFileSharing>("sfs");
public string id { get { return IDENTITY.id; } }
public const int SFS_PROVIDER_ID = 2;
public StreamInteractor stream_interactor {
owned get { return Application.get_default().stream_interactor; }
private set { }
}
public FileManager file_manager {
owned get { return stream_interactor.get_module(FileManager.IDENTITY); }
private set { }
}
public Database db {
owned get { return Application.get_default().db; }
private set { }
}
private StatelessFileSharing(StreamInteractor stream_interactor, Database db) {
this.stream_interactor = stream_interactor;
this.db = db;
stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(new ReceivedMessageListener(this));
}
public static void start(StreamInteractor stream_interactor, Database db) {
StatelessFileSharing m = new StatelessFileSharing(stream_interactor, db);
stream_interactor.add_module(m);
}
public async void create_file_transfer(Conversation conversation, Message message, string? file_sharing_id, Xep.FileMetadataElement.FileMetadata metadata, Gee.List<Xep.StatelessFileSharing.Source>? sources) {
FileTransfer file_transfer = new FileTransfer();
file_transfer.file_sharing_id = file_sharing_id;
file_transfer.account = message.account;
file_transfer.counterpart = message.counterpart;
file_transfer.ourpart = message.ourpart;
file_transfer.direction = message.direction;
file_transfer.time = message.time;
file_transfer.local_time = message.local_time;
file_transfer.provider = SFS_PROVIDER_ID;
file_transfer.file_metadata = metadata;
file_transfer.info = message.id.to_string();
if (sources != null) {
file_transfer.sfs_sources = sources;
}
stream_interactor.get_module(FileTransferStorage.IDENTITY).add_file(file_transfer);
conversation.last_active = file_transfer.time;
file_manager.received_file(file_transfer, conversation);
}
public void on_received_sources(Jid from, Conversation conversation, string attach_to_message_id, string? attach_to_file_id, Gee.List<Xep.StatelessFileSharing.Source> sources) {
Message? message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_referencing_id(attach_to_message_id, conversation);
if (message == null) return;
FileTransfer? file_transfer = null;
if (attach_to_file_id != null) {
file_transfer = stream_interactor.get_module(FileTransferStorage.IDENTITY).get_files_by_message_and_file_id(message.id, attach_to_file_id, conversation);
} else {
file_transfer = stream_interactor.get_module(FileTransferStorage.IDENTITY).get_file_by_message_id(message.id, conversation);
}
if (file_transfer == null) return;
// "If no <hash/> is provided or the <hash/> elements provided use unsupported algorithms, receiving clients MUST ignore
// any attached sources from other senders and only obtain the file from the sources announced by the original sender."
// For now we only allow the original sender
if (from.equals(file_transfer.from) && Xep.CryptographicHashes.get_supported_hashes(file_transfer.hashes).is_empty) {
warning("Ignoring sfs source: Not from original sender or no known file hashes");
return;
}
foreach (var source in sources) {
file_transfer.add_sfs_source(source);
}
if (file_manager.is_sender_trustworthy(file_transfer, conversation) && file_transfer.state == FileTransfer.State.NOT_STARTED && file_transfer.size >= 0 && file_transfer.size < 5000000) {
file_manager.download_file(file_transfer);
}
}
/*
public async void create_sfs_for_legacy_transfer(FileProvider file_provider, string info, Jid from, DateTime time, DateTime local_time, Conversation conversation, FileReceiveData receive_data, FileMeta file_meta) {
FileTransfer file_transfer = file_manager.create_file_transfer_from_provider_incoming(file_provider, info, from, time, local_time, conversation, receive_data, file_meta);
HttpFileReceiveData? http_receive_data = receive_data as HttpFileReceiveData;
if (http_receive_data == null) return;
var sources = new ArrayList<Xep.StatelessFileSharing.Source>();
Xep.StatelessFileSharing.HttpSource source = new Xep.StatelessFileSharing.HttpSource();
source.url = http_receive_data.url;
sources.add(source);
if (file_manager.is_jid_trustworthy(from, conversation)) {
try {
file_meta = yield file_provider.get_meta_info(file_transfer, http_receive_data, file_meta);
} catch (Error e) {
warning("Http meta request failed: %s", e.message);
}
}
var metadata = new Xep.FileMetadataElement.FileMetadata();
metadata.size = file_meta.size;
metadata.name = file_meta.file_name;
metadata.mime_type = file_meta.mime_type;
file_transfer.provider = SFS_PROVIDER_ID;
file_transfer.file_metadata = metadata;
file_transfer.sfs_sources = sources;
}
*/
private class ReceivedMessageListener : MessageListener {
public string[] after_actions_const = new string[]{ "STORE" };
public override string action_group { get { return "MESSAGE_REINTERPRETING"; } }
public override string[] after_actions { get { return after_actions_const; } }
private StatelessFileSharing outer;
private StreamInteractor stream_interactor;
public ReceivedMessageListener(StatelessFileSharing outer) {
this.outer = outer;
this.stream_interactor = outer.stream_interactor;
}
public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
Gee.List<Xep.StatelessFileSharing.FileShare> file_shares = Xep.StatelessFileSharing.get_file_shares(stanza);
if (file_shares != null) {
// For now, only accept file shares that have at least one supported hash
foreach (Xep.StatelessFileSharing.FileShare file_share in file_shares) {
if (!Xep.CryptographicHashes.has_supported_hashes(file_share.metadata.hashes)) {
return false;
}
}
foreach (Xep.StatelessFileSharing.FileShare file_share in file_shares) {
outer.create_file_transfer(conversation, message, file_share.id, file_share.metadata, file_share.sources);
}
return true;
}
var source_attachments = Xep.StatelessFileSharing.get_source_attachments(stanza);
if (source_attachments != null) {
foreach (var source_attachment in source_attachments) {
outer.on_received_sources(stanza.from, conversation, source_attachment.to_message_id, source_attachment.to_file_transfer_id, source_attachment.sources);
return true;
}
}
// Don't process messages that are fallback for legacy clients
if (Xep.StatelessFileSharing.is_sfs_fallback_message(stanza)) {
return true;
}
return false;
}
}
}

View File

@ -89,7 +89,12 @@ public class ModuleIdentity<T> : Object {
} }
public T? cast(StreamInteractionModule module) { public T? cast(StreamInteractionModule module) {
#if VALA_0_56_11
// We can't typecheck due to compiler bug
return (T) module;
#else
return module.get_type().is_a(typeof(T)) ? (T?) module : null; return module.get_type().is_a(typeof(T)) ? (T?) module : null;
#endif
} }
public bool matches(StreamInteractionModule module) { public bool matches(StreamInteractionModule module) {

Some files were not shown because too many files have changed in this diff Show More