dino-0.5.0/ 0000775 0000000 0000000 00000000000 14776241610 011155 5 ustar root root dino-0.5.0/.github/ 0000775 0000000 0000000 00000000000 14776241610 012515 5 ustar root root dino-0.5.0/.github/matchers/ 0000775 0000000 0000000 00000000000 14776241610 014323 5 ustar root root dino-0.5.0/.github/matchers/gcc-problem-matcher.json 0000664 0000000 0000000 00000000663 14776241610 021036 0 ustar root root {
"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
}
]
}
]
}
dino-0.5.0/.github/matchers/meson-problem-matcher.json 0000664 0000000 0000000 00000000646 14776241610 021424 0 ustar root root {
"problemMatcher": [
{
"owner": "meson-problem-matcher",
"pattern": [
{
"regexp": "^(.*?)?:(\\d+)?:(\\d+)?: (WARNING|ERROR):\\s+(.*)$",
"file": 1,
"line": 2,
"column": 3,
"severity": 4,
"message": 5
}
]
}
]
}
dino-0.5.0/.github/matchers/vala-problem-matcher.json 0000664 0000000 0000000 00000000706 14776241610 021223 0 ustar root root {
"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
}
]
}
]
}
dino-0.5.0/.github/workflows/ 0000775 0000000 0000000 00000000000 14776241610 014552 5 ustar root root dino-0.5.0/.github/workflows/build.yml 0000664 0000000 0000000 00000003230 14776241610 016372 0 ustar root root name: Build
on: [pull_request, push]
jobs:
build:
name: "Build"
runs-on: ubuntu-24.04
steps:
- name: "Checkout sources"
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: "Setup matchers"
run: |
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
dino-0.5.0/.gitignore 0000664 0000000 0000000 00000000104 14776241610 013140 0 ustar root root *.o
build/
Makefile
.vscode/
*.iml
.idea
.sqlite3
gschemas.compiled
dino-0.5.0/LICENSE 0000664 0000000 0000000 00000104515 14776241610 012170 0 ustar root root GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
Copyright (C)
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
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
Copyright (C)
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
.
dino-0.5.0/README.md 0000664 0000000 0000000 00000003723 14776241610 012441 0 ustar root root
# Dino
Dino is an XMPP messaging app for Linux using GTK and Vala.
It supports calls, encryption, file transfers, group chats and more.

Installation
------------
Have a look at the [prebuilt packages](https://github.com/dino/dino/wiki/Distribution-Packages).
Build
-----
Make sure to install all [dependencies](https://github.com/dino/dino/wiki/Build#dependencies).
meson setup build
meson compile -C build
build/main/dino
Resources
---------
- Check out the [Dino website](https://dino.im).
- Join our XMPP channel at `chat@dino.im`.
- The [wiki](https://github.com/dino/dino/wiki) provides additional information.
Contribute
----------
- Pull requests are welcome. [These](https://github.com/dino/dino/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) might be good first issues. Please discuss bigger changes in our channel first.
- Look at [how to debug](https://github.com/dino/dino/wiki/Debugging) Dino before you report a bug.
- Help [translating](https://github.com/dino/dino/wiki/Translations) Dino into your language.
- Make a [donation](https://dino.im/#donate).
License
-------
Dino - XMPP messaging app using GTK/Vala
Copyright (C) 2016-2025 Dino contributors
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
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
dino-0.5.0/VERSION 0000664 0000000 0000000 00000000016 14776241610 012222 0 ustar root root RELEASE 0.5.0
dino-0.5.0/crypto-vala/ 0000775 0000000 0000000 00000000000 14776241610 013416 5 ustar root root dino-0.5.0/crypto-vala/crypto-vala.deps 0000664 0000000 0000000 00000000021 14776241610 016525 0 ustar root root gio-2.0
glib-2.0
dino-0.5.0/crypto-vala/meson.build 0000664 0000000 0000000 00000001522 14776241610 015560 0 ustar root root 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
dino-0.5.0/crypto-vala/src/ 0000775 0000000 0000000 00000000000 14776241610 014205 5 ustar root root dino-0.5.0/crypto-vala/src/cipher.vala 0000664 0000000 0000000 00000013647 14776241610 016337 0 ustar root root namespace Crypto {
public class SymmetricCipher {
private GCrypt.Cipher.Cipher cipher;
public static bool supports(string algo_name) {
GCrypt.Cipher.Algorithm algo;
GCrypt.Cipher.Mode mode;
GCrypt.Cipher.Flag flags;
return parse(algo_name, out algo, out mode, out flags);
}
private static unowned string mode_to_string(GCrypt.Cipher.Mode mode) {
switch (mode) {
case GCrypt.Cipher.Mode.ECB: return "ECB";
case GCrypt.Cipher.Mode.CFB: return "CFB";
case GCrypt.Cipher.Mode.CBC: return "CBC";
case GCrypt.Cipher.Mode.STREAM: return "STREAM";
case GCrypt.Cipher.Mode.OFB: return "OFB";
case GCrypt.Cipher.Mode.CTR: return "CTR";
case GCrypt.Cipher.Mode.AESWRAP: return "AESWRAP";
case GCrypt.Cipher.Mode.GCM: return "GCM";
case GCrypt.Cipher.Mode.POLY1305: return "POLY1305";
case GCrypt.Cipher.Mode.OCB: return "OCB";
case GCrypt.Cipher.Mode.CFB8: return "CFB8";
// case GCrypt.Cipher.Mode.XTS: return "XTS"; // Not supported in gcrypt < 1.8
}
return "NONE";
}
private static GCrypt.Cipher.Mode mode_from_string(string name) {
switch (name) {
case "ECB": return GCrypt.Cipher.Mode.ECB;
case "CFB": return GCrypt.Cipher.Mode.CFB;
case "CBC": return GCrypt.Cipher.Mode.CBC;
case "STREAM": return GCrypt.Cipher.Mode.STREAM;
case "OFB": return GCrypt.Cipher.Mode.OFB;
case "CTR": return GCrypt.Cipher.Mode.CTR;
case "AESWRAP": return GCrypt.Cipher.Mode.AESWRAP;
case "GCM": return GCrypt.Cipher.Mode.GCM;
case "POLY1305": return GCrypt.Cipher.Mode.POLY1305;
case "OCB": return GCrypt.Cipher.Mode.OCB;
case "CFB8": return GCrypt.Cipher.Mode.CFB8;
// case "XTS": return GCrypt.Cipher.Mode.XTS; // Not supported in gcrypt < 1.8
}
return GCrypt.Cipher.Mode.NONE;
}
private static string flags_to_string(GCrypt.Cipher.Flag flags) {
string? s = null;
if ((GCrypt.Cipher.Flag.CBC_MAC & flags) != 0) s = (s == null ? "" : @"$s-") + "MAC";
if ((GCrypt.Cipher.Flag.CBC_CTS & flags) != 0) s = (s == null ? "" : @"$s-") + "CTS";
if ((GCrypt.Cipher.Flag.ENABLE_SYNC & flags) != 0) s = (s == null ? "" : @"$s-") + "SYNC";
if ((GCrypt.Cipher.Flag.SECURE & flags) != 0) s = (s == null ? "" : @"$s-") + "SECURE";
return s ?? "NONE";
}
private static GCrypt.Cipher.Flag flag_from_string(string flag_name) {
if (flag_name == "SECURE") return GCrypt.Cipher.Flag.SECURE;
if (flag_name == "SYNC") return GCrypt.Cipher.Flag.ENABLE_SYNC;
if (flag_name == "CTS") return GCrypt.Cipher.Flag.CBC_CTS;
if (flag_name == "MAC") return GCrypt.Cipher.Flag.CBC_MAC;
return 0;
}
private static GCrypt.Cipher.Flag flags_from_string(string flag_names) {
GCrypt.Cipher.Flag flags = 0;
foreach(string flag in flag_names.split("-")) {
flags |= flag_from_string(flag);
}
return flags;
}
private static bool parse(string algo_name, out GCrypt.Cipher.Algorithm algo, out GCrypt.Cipher.Mode mode, out GCrypt.Cipher.Flag flags) {
algo = GCrypt.Cipher.Algorithm.NONE;
mode = GCrypt.Cipher.Mode.NONE;
flags = 0;
string[] algo_parts = algo_name.split("-", 3);
algo = GCrypt.Cipher.Algorithm.from_string(algo_parts[0]);
if (algo_parts.length >= 2) {
mode = mode_from_string(algo_parts[1]);
}
if (algo_parts.length == 3) {
flags |= flags_from_string(algo_parts[2]);
}
return to_algo_name(algo, mode, flags) == algo_name;
}
private static string to_algo_name(GCrypt.Cipher.Algorithm algo = GCrypt.Cipher.Algorithm.NONE, GCrypt.Cipher.Mode mode = GCrypt.Cipher.Mode.NONE, GCrypt.Cipher.Flag flags = 0) {
if (flags != 0) {
return @"$algo-$(mode_to_string(mode))-$(flags_to_string(flags))";
} else if (mode != GCrypt.Cipher.Mode.NONE) {
return @"$algo-$(mode_to_string(mode))";
} else {
return algo.to_string();
}
}
public SymmetricCipher(string algo_name) throws Error {
GCrypt.Cipher.Algorithm algo;
GCrypt.Cipher.Mode mode;
GCrypt.Cipher.Flag flags;
if (parse(algo_name, out algo, out mode, out flags)) {
this.gcrypt(algo, mode, flags);
} else {
throw new Error.ILLEGAL_ARGUMENTS(@"The algorithm $algo_name is not supported");
}
}
private SymmetricCipher.gcrypt(GCrypt.Cipher.Algorithm algo, GCrypt.Cipher.Mode mode, GCrypt.Cipher.Flag flags) throws Error {
may_throw_gcrypt_error(GCrypt.Cipher.Cipher.open(out this.cipher, algo, mode, flags));
}
public void set_key(uint8[] key) throws Error {
may_throw_gcrypt_error(cipher.set_key(key));
}
public void set_iv(uint8[] iv) throws Error {
may_throw_gcrypt_error(cipher.set_iv(iv));
}
public void set_counter_vector(uint8[] ctr) throws Error {
may_throw_gcrypt_error(cipher.set_counter_vector(ctr));
}
public void reset() throws Error {
may_throw_gcrypt_error(cipher.reset());
}
public uint8[] get_tag(size_t taglen) throws Error {
uint8[] tag = new uint8[taglen];
may_throw_gcrypt_error(cipher.get_tag(tag));
return tag;
}
public void check_tag(uint8[] tag) throws Error {
may_throw_gcrypt_error(cipher.check_tag(tag));
}
public void encrypt(uint8[] output, uint8[] input) throws Error {
may_throw_gcrypt_error(cipher.encrypt(output, input));
}
public void decrypt(uint8[] output, uint8[] input) throws Error {
may_throw_gcrypt_error(cipher.decrypt(output, input));
}
public void sync() throws Error {
may_throw_gcrypt_error(cipher.sync());
}
}
}
dino-0.5.0/crypto-vala/src/cipher_converter.vala 0000664 0000000 0000000 00000010400 14776241610 020406 0 ustar root root using GLib;
namespace Crypto {
public abstract class SymmetricCipherConverter : Converter, Object {
internal SymmetricCipher cipher;
internal size_t attached_taglen;
public abstract ConverterResult convert(uint8[] inbuf, uint8[] outbuf, ConverterFlags flags, out size_t bytes_read, out size_t bytes_written) throws IOError;
public uint8[] get_tag(size_t taglen) throws Error {
return cipher.get_tag(taglen);
}
public void check_tag(uint8[] tag) throws Error {
cipher.check_tag(tag);
}
public void reset() {
try {
cipher.reset();
} catch (Crypto.Error e) {
warning(@"$(e.domain) error while resetting cipher: $(e.message)");
}
}
}
public class SymmetricCipherEncrypter : SymmetricCipherConverter {
public SymmetricCipherEncrypter(owned SymmetricCipher cipher, size_t attached_taglen = 0) {
this.cipher = (owned) cipher;
this.attached_taglen = attached_taglen;
}
public override ConverterResult convert(uint8[] inbuf, uint8[] outbuf, ConverterFlags flags, out size_t bytes_read, out size_t bytes_written) throws IOError {
if (inbuf.length > outbuf.length) {
throw new IOError.NO_SPACE("CipherConverter needs at least the size of input as output space");
}
if ((flags & ConverterFlags.INPUT_AT_END) != 0 && inbuf.length + attached_taglen > outbuf.length) {
throw new IOError.NO_SPACE("CipherConverter needs additional output space to attach tag");
}
try {
if (inbuf.length > 0) {
cipher.encrypt(outbuf, inbuf);
}
bytes_read = inbuf.length;
bytes_written = inbuf.length;
if ((flags & ConverterFlags.INPUT_AT_END) != 0) {
if (attached_taglen > 0) {
Memory.copy((uint8*)outbuf + inbuf.length, get_tag(attached_taglen), attached_taglen);
bytes_written = inbuf.length + attached_taglen;
}
return ConverterResult.FINISHED;
}
if ((flags & ConverterFlags.FLUSH) != 0) {
return ConverterResult.FLUSHED;
}
return ConverterResult.CONVERTED;
} catch (Crypto.Error e) {
throw new IOError.FAILED(@"$(e.domain) error while encrypting: $(e.message)");
}
}
}
public class SymmetricCipherDecrypter : SymmetricCipherConverter {
public SymmetricCipherDecrypter(owned SymmetricCipher cipher, size_t attached_taglen = 0) {
this.cipher = (owned) cipher;
this.attached_taglen = attached_taglen;
}
public override ConverterResult convert(uint8[] inbuf, uint8[] outbuf, ConverterFlags flags, out size_t bytes_read, out size_t bytes_written) throws IOError {
if (inbuf.length > outbuf.length + attached_taglen) {
throw new IOError.NO_SPACE("CipherConverter needs at least the size of input as output space");
}
if ((flags & ConverterFlags.INPUT_AT_END) != 0 && inbuf.length < attached_taglen) {
throw new IOError.PARTIAL_INPUT("CipherConverter needs additional input to read tag");
} else if ((flags & ConverterFlags.INPUT_AT_END) == 0 && inbuf.length < attached_taglen + 1) {
throw new IOError.PARTIAL_INPUT("CipherConverter needs additional input to make sure to not accidentally read tag");
}
try {
inbuf.length -= (int) attached_taglen;
if (inbuf.length > 0) {
cipher.decrypt(outbuf, inbuf);
}
bytes_read = inbuf.length;
bytes_written = inbuf.length;
inbuf.length += (int) attached_taglen;
if ((flags & ConverterFlags.INPUT_AT_END) != 0) {
if (attached_taglen > 0) {
check_tag(inbuf[(inbuf.length - attached_taglen):inbuf.length]);
bytes_read = inbuf.length;
}
return ConverterResult.FINISHED;
}
if ((flags & ConverterFlags.FLUSH) != 0) {
return ConverterResult.FLUSHED;
}
return ConverterResult.CONVERTED;
} catch (Crypto.Error e) {
throw new IOError.FAILED(@"$(e.domain) error while decrypting: $(e.message)");
}
}
}
}
dino-0.5.0/crypto-vala/src/error.vala 0000664 0000000 0000000 00000000430 14776241610 016200 0 ustar root root namespace Crypto {
public errordomain Error {
ILLEGAL_ARGUMENTS,
GCRYPT,
AUTHENTICATION_FAILED,
UNKNOWN
}
internal void may_throw_gcrypt_error(GCrypt.Error e) throws Error {
if (((int)e) != 0) {
throw new Crypto.Error.GCRYPT(e.to_string());
}
}
} dino-0.5.0/crypto-vala/src/random.vala 0000664 0000000 0000000 00000000152 14776241610 016330 0 ustar root root namespace Crypto {
public static void randomize(uint8[] buffer) {
GCrypt.Random.randomize(buffer);
}
} dino-0.5.0/crypto-vala/src/srtp.vala 0000664 0000000 0000000 00000010445 14776241610 016046 0 ustar root root using Srtp;
namespace Crypto.Srtp {
public const string AES_CM_128_HMAC_SHA1_80 = "AES_CM_128_HMAC_SHA1_80";
public const string AES_CM_128_HMAC_SHA1_32 = "AES_CM_128_HMAC_SHA1_32";
public const string F8_128_HMAC_SHA1_80 = "F8_128_HMAC_SHA1_80";
public class Session {
public bool has_encrypt { get; private set; default = false; }
public bool has_decrypt { get; private set; default = false; }
private Context encrypt_context;
private Context decrypt_context;
static construct {
init();
install_log_handler(log);
}
private static void log(LogLevel level, string msg) {
print(@"SRTP[$level]: $msg\n");
}
public Session() {
Context.create(out encrypt_context, null);
Context.create(out decrypt_context, null);
}
public uint8[] encrypt_rtp(uint8[] data) throws Error {
uint8[] buf = new uint8[data.length + MAX_TRAILER_LEN];
Memory.copy(buf, data, data.length);
int buf_use = data.length;
ErrorStatus res = encrypt_context.protect(buf, ref buf_use);
if (res != ErrorStatus.ok) {
throw new Error.UNKNOWN(@"SRTP encrypt failed: $res");
}
buf.length = buf_use;
return buf;
}
public uint8[] decrypt_rtp(uint8[] data) throws Error {
uint8[] buf = new uint8[data.length];
Memory.copy(buf, data, data.length);
int buf_use = data.length;
ErrorStatus res = decrypt_context.unprotect(buf, ref buf_use);
switch (res) {
case ErrorStatus.auth_fail:
throw new Error.AUTHENTICATION_FAILED("SRTP packet failed the message authentication check");
case ErrorStatus.ok:
break;
default:
throw new Error.UNKNOWN(@"SRTP decrypt failed: $res");
}
uint8[] ret = new uint8[buf_use];
GLib.Memory.copy(ret, buf, buf_use);
return ret;
}
public uint8[] encrypt_rtcp(uint8[] data) throws Error {
uint8[] buf = new uint8[data.length + MAX_TRAILER_LEN + 4];
Memory.copy(buf, data, data.length);
int buf_use = data.length;
ErrorStatus res = encrypt_context.protect_rtcp(buf, ref buf_use);
if (res != ErrorStatus.ok) {
throw new Error.UNKNOWN(@"SRTCP encrypt failed: $res");
}
buf.length = buf_use;
return buf;
}
public uint8[] decrypt_rtcp(uint8[] data) throws Error {
uint8[] buf = new uint8[data.length];
Memory.copy(buf, data, data.length);
int buf_use = data.length;
ErrorStatus res = decrypt_context.unprotect_rtcp(buf, ref buf_use);
switch (res) {
case ErrorStatus.auth_fail:
throw new Error.AUTHENTICATION_FAILED("SRTCP packet failed the message authentication check");
case ErrorStatus.ok:
break;
default:
throw new Error.UNKNOWN(@"SRTCP decrypt failed: $res");
}
uint8[] ret = new uint8[buf_use];
GLib.Memory.copy(ret, buf, buf_use);
return ret;
}
private Policy create_policy(string profile) {
Policy policy = Policy();
switch (profile) {
case AES_CM_128_HMAC_SHA1_80:
policy.rtp.set_aes_cm_128_hmac_sha1_80();
policy.rtcp.set_aes_cm_128_hmac_sha1_80();
break;
}
return policy;
}
public void set_encryption_key(string profile, uint8[] key, uint8[] salt) {
Policy policy = create_policy(profile);
policy.ssrc.type = SsrcType.any_outbound;
policy.key = new uint8[key.length + salt.length];
Memory.copy(policy.key, key, key.length);
Memory.copy(((uint8*)policy.key) + key.length, salt, salt.length);
policy.next = null;
encrypt_context.add_stream(ref policy);
has_encrypt = true;
}
public void set_decryption_key(string profile, uint8[] key, uint8[] salt) {
Policy policy = create_policy(profile);
policy.ssrc.type = SsrcType.any_inbound;
policy.key = new uint8[key.length + salt.length];
Memory.copy(policy.key, key, key.length);
Memory.copy(((uint8*)policy.key) + key.length, salt, salt.length);
policy.next = null;
decrypt_context.add_stream(ref policy);
has_decrypt = true;
}
}
} dino-0.5.0/crypto-vala/vapi/ 0000775 0000000 0000000 00000000000 14776241610 014355 5 ustar root root dino-0.5.0/crypto-vala/vapi/libgcrypt.vapi 0000664 0000000 0000000 00000046504 14776241610 017246 0 ustar root root /* gcrypt.vapi
*
* Copyright:
* 2008 Jiqing Qiang
* 2008, 2010, 2012-2013 Evan Nemerson
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* Author:
* Jiqing Qiang
* Evan Nemerson
*/
[CCode (cheader_filename = "gcrypt.h", lower_case_cprefix = "gcry_")]
namespace GCrypt {
[CCode (cname = "gpg_err_source_t", cprefix = "GPG_ERR_SOURCE_")]
public enum ErrorSource {
UNKNOWN,
GCRYPT,
GPG,
GPGSM,
GPGAGENT,
PINENTRY,
SCD,
GPGME,
KEYBOX,
KSBA,
DIRMNGR,
GSTI,
ANY,
USER_1,
USER_2,
USER_3,
USER_4,
/* This is one more than the largest allowed entry. */
DIM
}
[CCode (cname = "gpg_err_code_t", cprefix = "GPG_ERR_")]
public enum ErrorCode {
NO_ERROR,
GENERAL,
UNKNOWN_PACKET,
UNKNOWN_VERSION,
PUBKEY_ALGO,
DIGEST_ALGO,
BAD_PUBKEY,
BAD_SECKEY,
BAD_SIGNATURE,
NO_PUBKEY,
CHECKSUM,
BAD_PASSPHRASE,
CIPHER_ALGO,
KEYRING_OPEN,
INV_PACKET,
INV_ARMOR,
NO_USER_ID,
NO_SECKEY,
WRONG_SECKEY,
BAD_KEY,
COMPR_ALGO,
NO_PRIME,
NO_ENCODING_METHOD,
NO_ENCRYPTION_SCHEME,
NO_SIGNATURE_SCHEME,
INV_ATTR,
NO_VALUE,
NOT_FOUND,
VALUE_NOT_FOUND,
SYNTAX,
BAD_MPI,
INV_PASSPHRASE,
SIG_CLASS,
RESOURCE_LIMIT,
INV_KEYRING,
TRUSTDB,
BAD_CERT,
INV_USER_ID,
UNEXPECTED,
TIME_CONFLICT,
KEYSERVER,
WRONG_PUBKEY_ALGO,
TRIBUTE_TO_D_A,
WEAK_KEY,
INV_KEYLEN,
INV_ARG,
BAD_URI,
INV_URI,
NETWORK,
UNKNOWN_HOST,
SELFTEST_FAILED,
NOT_ENCRYPTED,
NOT_PROCESSED,
UNUSABLE_PUBKEY,
UNUSABLE_SECKEY,
INV_VALUE,
BAD_CERT_CHAIN,
MISSING_CERT,
NO_DATA,
BUG,
NOT_SUPPORTED,
INV_OP,
TIMEOUT,
INTERNAL,
EOF_GCRYPT,
INV_OBJ,
TOO_SHORT,
TOO_LARGE,
NO_OBJ,
NOT_IMPLEMENTED,
CONFLICT,
INV_CIPHER_MODE,
INV_FLAG,
INV_HANDLE,
TRUNCATED,
INCOMPLETE_LINE,
INV_RESPONSE,
NO_AGENT,
AGENT,
INV_DATA,
ASSUAN_SERVER_FAULT,
ASSUAN,
INV_SESSION_KEY,
INV_SEXP,
UNSUPPORTED_ALGORITHM,
NO_PIN_ENTRY,
PIN_ENTRY,
BAD_PIN,
INV_NAME,
BAD_DATA,
INV_PARAMETER,
WRONG_CARD,
NO_DIRMNGR,
DIRMNGR,
CERT_REVOKED,
NO_CRL_KNOWN,
CRL_TOO_OLD,
LINE_TOO_LONG,
NOT_TRUSTED,
CANCELED,
BAD_CA_CERT,
CERT_EXPIRED,
CERT_TOO_YOUNG,
UNSUPPORTED_CERT,
UNKNOWN_SEXP,
UNSUPPORTED_PROTECTION,
CORRUPTED_PROTECTION,
AMBIGUOUS_NAME,
CARD,
CARD_RESET,
CARD_REMOVED,
INV_CARD,
CARD_NOT_PRESENT,
NO_PKCS15_APP,
NOT_CONFIRMED,
CONFIGURATION,
NO_POLICY_MATCH,
INV_INDEX,
INV_ID,
NO_SCDAEMON,
SCDAEMON,
UNSUPPORTED_PROTOCOL,
BAD_PIN_METHOD,
CARD_NOT_INITIALIZED,
UNSUPPORTED_OPERATION,
WRONG_KEY_USAGE,
NOTHING_FOUND,
WRONG_BLOB_TYPE,
MISSING_VALUE,
HARDWARE,
PIN_BLOCKED,
USE_CONDITIONS,
PIN_NOT_SYNCED,
INV_CRL,
BAD_BER,
INV_BER,
ELEMENT_NOT_FOUND,
IDENTIFIER_NOT_FOUND,
INV_TAG,
INV_LENGTH,
INV_KEYINFO,
UNEXPECTED_TAG,
NOT_DER_ENCODED,
NO_CMS_OBJ,
INV_CMS_OBJ,
UNKNOWN_CMS_OBJ,
UNSUPPORTED_CMS_OBJ,
UNSUPPORTED_ENCODING,
UNSUPPORTED_CMS_VERSION,
UNKNOWN_ALGORITHM,
INV_ENGINE,
PUBKEY_NOT_TRUSTED,
DECRYPT_FAILED,
KEY_EXPIRED,
SIG_EXPIRED,
ENCODING_PROBLEM,
INV_STATE,
DUP_VALUE,
MISSING_ACTION,
MODULE_NOT_FOUND,
INV_OID_STRING,
INV_TIME,
INV_CRL_OBJ,
UNSUPPORTED_CRL_VERSION,
INV_CERT_OBJ,
UNKNOWN_NAME,
LOCALE_PROBLEM,
NOT_LOCKED,
PROTOCOL_VIOLATION,
INV_MAC,
INV_REQUEST,
UNKNOWN_EXTN,
UNKNOWN_CRIT_EXTN,
LOCKED,
UNKNOWN_OPTION,
UNKNOWN_COMMAND,
BUFFER_TOO_SHORT,
SEXP_INV_LEN_SPEC,
SEXP_STRING_TOO_LONG,
SEXP_UNMATCHED_PAREN,
SEXP_NOT_CANONICAL,
SEXP_BAD_CHARACTER,
SEXP_BAD_QUOTATION,
SEXP_ZERO_PREFIX,
SEXP_NESTED_DH,
SEXP_UNMATCHED_DH,
SEXP_UNEXPECTED_PUNC,
SEXP_BAD_HEX_CHAR,
SEXP_ODD_HEX_NUMBERS,
SEXP_BAD_OCT_CHAR,
ASS_GENERAL,
ASS_ACCEPT_FAILED,
ASS_CONNECT_FAILED,
ASS_INV_RESPONSE,
ASS_INV_VALUE,
ASS_INCOMPLETE_LINE,
ASS_LINE_TOO_LONG,
ASS_NESTED_COMMANDS,
ASS_NO_DATA_CB,
ASS_NO_INQUIRE_CB,
ASS_NOT_A_SERVER,
ASS_NOT_A_CLIENT,
ASS_SERVER_START,
ASS_READ_ERROR,
ASS_WRITE_ERROR,
ASS_TOO_MUCH_DATA,
ASS_UNEXPECTED_CMD,
ASS_UNKNOWN_CMD,
ASS_SYNTAX,
ASS_CANCELED,
ASS_NO_INPUT,
ASS_NO_OUTPUT,
ASS_PARAMETER,
ASS_UNKNOWN_INQUIRE,
USER_1,
USER_2,
USER_3,
USER_4,
USER_5,
USER_6,
USER_7,
USER_8,
USER_9,
USER_10,
USER_11,
USER_12,
USER_13,
USER_14,
USER_15,
USER_16,
MISSING_ERRNO,
UNKNOWN_ERRNO,
EOF,
E2BIG,
EACCES,
EADDRINUSE,
EADDRNOTAVAIL,
EADV,
EAFNOSUPPORT,
EAGAIN,
EALREADY,
EAUTH,
EBACKGROUND,
EBADE,
EBADF,
EBADFD,
EBADMSG,
EBADR,
EBADRPC,
EBADRQC,
EBADSLT,
EBFONT,
EBUSY,
ECANCELED,
ECHILD,
ECHRNG,
ECOMM,
ECONNABORTED,
ECONNREFUSED,
ECONNRESET,
ED,
EDEADLK,
EDEADLOCK,
EDESTADDRREQ,
EDIED,
EDOM,
EDOTDOT,
EDQUOT,
EEXIST,
EFAULT,
EFBIG,
EFTYPE,
EGRATUITOUS,
EGREGIOUS,
EHOSTDOWN,
EHOSTUNREACH,
EIDRM,
EIEIO,
EILSEQ,
EINPROGRESS,
EINTR,
EINVAL,
EIO,
EISCONN,
EISDIR,
EISNAM,
EL2HLT,
EL2NSYNC,
EL3HLT,
EL3RST,
ELIBACC,
ELIBBAD,
ELIBEXEC,
ELIBMAX,
ELIBSCN,
ELNRNG,
ELOOP,
EMEDIUMTYPE,
EMFILE,
EMLINK,
EMSGSIZE,
EMULTIHOP,
ENAMETOOLONG,
ENAVAIL,
ENEEDAUTH,
ENETDOWN,
ENETRESET,
ENETUNREACH,
ENFILE,
ENOANO,
ENOBUFS,
ENOCSI,
ENODATA,
ENODEV,
ENOENT,
ENOEXEC,
ENOLCK,
ENOLINK,
ENOMEDIUM,
ENOMEM,
ENOMSG,
ENONET,
ENOPKG,
ENOPROTOOPT,
ENOSPC,
ENOSR,
ENOSTR,
ENOSYS,
ENOTBLK,
ENOTCONN,
ENOTDIR,
ENOTEMPTY,
ENOTNAM,
ENOTSOCK,
ENOTSUP,
ENOTTY,
ENOTUNIQ,
ENXIO,
EOPNOTSUPP,
EOVERFLOW,
EPERM,
EPFNOSUPPORT,
EPIPE,
EPROCLIM,
EPROCUNAVAIL,
EPROGMISMATCH,
EPROGUNAVAIL,
EPROTO,
EPROTONOSUPPORT,
EPROTOTYPE,
ERANGE,
EREMCHG,
EREMOTE,
EREMOTEIO,
ERESTART,
EROFS,
ERPCMISMATCH,
ESHUTDOWN,
ESOCKTNOSUPPORT,
ESPIPE,
ESRCH,
ESRMNT,
ESTALE,
ESTRPIPE,
ETIME,
ETIMEDOUT,
ETOOMANYREFS,
ETXTBSY,
EUCLEAN,
EUNATCH,
EUSERS,
EWOULDBLOCK,
EXDEV,
EXFULL,
/* This is one more than the largest allowed entry. */
CODE_DIM
}
[CCode (cname = "gcry_error_t", cprefix = "gpg_err_")]
public struct Error : uint {
[CCode (cname = "gcry_err_make")]
public Error (ErrorSource source, ErrorCode code);
[CCode (cname = "gcry_err_make_from_errno")]
public Error.from_errno (ErrorSource source, int err);
public ErrorCode code ();
public ErrorSource source ();
[CCode (cname = "gcry_strerror")]
public unowned string to_string ();
[CCode (cname = "gcry_strsource")]
public unowned string source_to_string ();
}
[CCode (cname = "enum gcry_ctl_cmds", cprefix = "GCRYCTL_")]
public enum ControlCommand {
SET_KEY,
SET_IV,
CFB_SYNC,
RESET,
FINALIZE,
GET_KEYLEN,
GET_BLKLEN,
TEST_ALGO,
IS_SECURE,
GET_ASNOID,
ENABLE_ALGO,
DISABLE_ALGO,
DUMP_RANDOM_STATS,
DUMP_SECMEM_STATS,
GET_ALGO_NPKEY,
GET_ALGO_NSKEY,
GET_ALGO_NSIGN,
GET_ALGO_NENCR,
SET_VERBOSITY,
SET_DEBUG_FLAGS,
CLEAR_DEBUG_FLAGS,
USE_SECURE_RNDPOOL,
DUMP_MEMORY_STATS,
INIT_SECMEM,
TERM_SECMEM,
DISABLE_SECMEM_WARN,
SUSPEND_SECMEM_WARN,
RESUME_SECMEM_WARN,
DROP_PRIVS,
ENABLE_M_GUARD,
START_DUMP,
STOP_DUMP,
GET_ALGO_USAGE,
IS_ALGO_ENABLED,
DISABLE_INTERNAL_LOCKING,
DISABLE_SECMEM,
INITIALIZATION_FINISHED,
INITIALIZATION_FINISHED_P,
ANY_INITIALIZATION_P,
SET_CBC_CTS,
SET_CBC_MAC,
SET_CTR,
ENABLE_QUICK_RANDOM,
SET_RANDOM_SEED_FILE,
UPDATE_RANDOM_SEED_FILE,
SET_THREAD_CBS,
FAST_POLL
}
public Error control (ControlCommand cmd, ...);
[CCode (lower_case_cname = "cipher_")]
namespace Cipher {
[CCode (cname = "enum gcry_cipher_algos", cprefix = "GCRY_CIPHER_")]
public enum Algorithm {
NONE,
IDEA,
3DES,
CAST5,
BLOWFISH,
SAFER_SK128,
DES_SK,
AES,
AES128,
RIJNDAEL,
RIJNDAEL128,
AES192,
RIJNDAEL192,
AES256,
RIJNDAEL256,
TWOFISH,
TWOFISH128,
ARCFOUR,
DES,
SERPENT128,
SERPENT192,
SERPENT256,
RFC2268_40,
RFC2268_128,
SEED,
CAMELLIA128,
CAMELLIA192,
CAMELLIA256,
SALSA20,
SALSA20R12,
GOST28147,
CHACHA20;
[CCode (cname = "gcry_cipher_algo_info")]
public Error info (ControlCommand what, ref uchar[] buffer);
[CCode (cname = "gcry_cipher_algo_name")]
public unowned string to_string ();
[CCode (cname = "gcry_cipher_map_name")]
public static Algorithm from_string (string name);
[CCode (cname = "gcry_cipher_map_oid")]
public static Algorithm from_oid (string oid);
}
[CCode (cname = "enum gcry_cipher_modes", cprefix = "GCRY_CIPHER_MODE_")]
public enum Mode {
NONE, /* No mode specified */
ECB, /* Electronic Codebook */
CFB, /* Cipher Feedback */
CBC, /* Cipher Block Chaining */
STREAM, /* Used with stream ciphers */
OFB, /* Output Feedback */
CTR, /* Counter */
AESWRAP, /* AES-WRAP algorithm */
CCM, /* Counter with CBC-MAC */
GCM, /* Galois/Counter Mode */
POLY1305, /* Poly1305 based AEAD mode */
OCB, /* OCB3 mode */
CFB8, /* Cipher Feedback /* Poly1305 based AEAD mode. */
XTS; /* XTS mode */
public unowned string to_string () {
switch (this) {
case ECB: return "ECB";
case CFB: return "CFB";
case CBC: return "CBC";
case STREAM: return "STREAM";
case OFB: return "OFB";
case CTR: return "CTR";
case AESWRAP: return "AESWRAP";
case GCM: return "GCM";
case POLY1305: return "POLY1305";
case OCB: return "OCB";
case CFB8: return "CFB8";
case XTS: return "XTS";
}
return "NONE";
}
public static Mode from_string (string name) {
switch (name) {
case "ECB": return ECB;
case "CFB": return CFB;
case "CBC": return CBC;
case "STREAM": return STREAM;
case "OFB": return OFB;
case "CTR": return CTR;
case "AESWRAP": return AESWRAP;
case "GCM": return GCM;
case "POLY1305": return POLY1305;
case "OCB": return OCB;
case "CFB8": return CFB8;
case "XTS": return XTS;
}
return NONE;
}
}
[CCode (cname = "enum gcry_cipher_flags", cprefix = "GCRY_CIPHER_")]
public enum Flag {
SECURE, /* Allocate in secure memory. */
ENABLE_SYNC, /* Enable CFB sync mode. */
CBC_CTS, /* Enable CBC cipher text stealing (CTS). */
CBC_MAC /* Enable CBC message auth. code (MAC). */
}
[CCode (cname = "gcry_cipher_hd_t", lower_case_cprefix = "gcry_cipher_", free_function = "gcry_cipher_close")]
[SimpleType]
public struct Cipher {
public static Error open (out Cipher cipher, Algorithm algo, Mode mode, Flag flags);
public void close ();
[CCode (cname = "gcry_cipher_ctl")]
public Error control (ControlCommand cmd, uchar[] buffer);
public Error info (ControlCommand what, ref uchar[] buffer);
public Error encrypt (uchar[] out_buffer, uchar[] in_buffer);
public Error decrypt (uchar[] out_buffer, uchar[] in_buffer);
[CCode (cname = "gcry_cipher_setkey")]
public Error set_key (uchar[] key_data);
[CCode (cname = "gcry_cipher_setiv")]
public Error set_iv (uchar[] iv_data);
[CCode (cname = "gcry_cipher_setctr")]
public Error set_counter_vector (uchar[] counter_vector);
[CCode (cname = "gcry_cipher_gettag")]
public Error get_tag(uchar[] out_buffer);
[CCode (cname = "gcry_cipher_checktag")]
public Error check_tag(uchar[] in_buffer);
public Error reset ();
public Error sync ();
}
}
[Compact, CCode (cname = "struct gcry_md_handle", cprefix = "gcry_md_", free_function = "gcry_md_close")]
public class Hash {
[CCode (cname = "enum gcry_md_algos", cprefix = "GCRY_MD_")]
public enum Algorithm {
NONE,
SHA1,
RMD160,
MD5,
MD4,
MD2,
TIGER,
TIGER1,
TIGER2,
HAVAL,
SHA224,
SHA256,
SHA384,
SHA512,
SHA3_224,
SHA3_256,
SHA3_384,
SHA3_512,
SHAKE128,
SHAKE256,
CRC32,
CRC32_RFC1510,
CRC24_RFC2440,
WHIRLPOOL,
GOSTR3411_94,
STRIBOG256,
STRIBOG512;
[CCode (cname = "gcry_md_get_algo_dlen")]
public size_t get_digest_length ();
[CCode (cname = "gcry_md_algo_info")]
public Error info (ControlCommand what, ref uchar[] buffer);
[CCode (cname = "gcry_md_algo_name")]
public unowned string to_string ();
[CCode (cname = "gcry_md_map_name")]
public static Algorithm from_string (string name);
[CCode (cname = "gcry_md_test_algo")]
public Error is_available ();
[CCode (cname = "gcry_md_get_asnoid")]
public Error get_oid (uchar[] buffer);
}
[CCode (cname = "enum gcry_md_flags", cprefix = "GCRY_MD_FLAG_")]
public enum Flag {
SECURE,
HMAC,
BUGEMU1
}
public static Error open (out Hash hash, Algorithm algo, Flag flag);
public void close ();
public Error enable (Algorithm algo);
[CCode (instance_pos = -1)]
public Error copy (out Hash dst);
public void reset ();
[CCode (cname = "enum gcry_md_ctl")]
public Error control (ControlCommand cmd, uchar[] buffer);
public void write (uchar[] buffer);
[CCode (array_length = false)]
public unowned uchar[] read (Algorithm algo);
public static void hash_buffer (Algorithm algo, [CCode (array_length = false)] uchar[] digest, uchar[] buffer);
public Algorithm get_algo ();
public bool is_enabled (Algorithm algo);
public bool is_secure ();
public Error info (ControlCommand what, uchar[] buffer);
[CCode (cname = "gcry_md_setkey")]
public Error set_key (uchar[] key_data);
public void putc (char c);
public void final ();
public static Error list (ref Algorithm[] algos);
}
namespace Random {
[CCode (cname = "gcry_random_level_t")]
public enum Level {
[CCode (cname = "GCRY_WEAK_RANDOM")]
WEAK,
[CCode (cname = "GCRY_STRONG_RANDOM")]
STRONG,
[CCode (cname = "GCRY_VERY_STRONG_RANDOM")]
VERY_STRONG
}
[CCode (cname = "gcry_randomize")]
public static void randomize (uchar[] buffer, Level level = Level.VERY_STRONG);
[CCode (cname = "gcry_fast_random_poll")]
public static Error poll ();
[CCode (cname = "gcry_random_bytes", array_length = false)]
public static uchar[] random_bytes (size_t nbytes, Level level = Level.VERY_STRONG);
[CCode (cname = "gcry_random_bytes_secure")]
public static uchar[] random_bytes_secure (size_t nbytes, Level level = Level.VERY_STRONG);
[CCode (cname = "gcry_create_nonce")]
public static void nonce (uchar[] buffer);
}
[Compact, CCode (cname = "struct gcry_mpi", cprefix = "gcry_mpi_", free_function = "gcry_mpi_release")]
public class MPI {
[CCode (cname = "enum gcry_mpi_format", cprefix = "GCRYMPI_FMT_")]
public enum Format {
NONE,
STD,
PGP,
SSH,
HEX,
USG
}
[CCode (cname = "enum gcry_mpi_flag", cprefix = "GCRYMPI_FLAG_")]
public enum Flag {
SECURE,
OPAQUE
}
public MPI (uint nbits);
[CCode (cname = "gcry_mpi_snew")]
public MPI.secure (uint nbits);
public MPI copy ();
public void set (MPI u);
public void set_ui (ulong u);
public void swap ();
public int cmp (MPI v);
public int cmp_ui (ulong v);
public static Error scan (out MPI ret, MPI.Format format, [CCode (array_length = false)] uchar[] buffer, size_t buflen, out size_t nscanned);
[CCode (instance_pos = -1)]
public Error print (MPI.Format format, [CCode (array_length = false)] uchar[] buffer, size_t buflen, out size_t nwritter);
[CCode (instance_pos = -1)]
public Error aprint (MPI.Format format, out uchar[] buffer);
public void add (MPI u, MPI v);
public void add_ui (MPI u, ulong v);
public void addm (MPI u, MPI v, MPI m);
public void sub (MPI u, MPI v);
public void sub_ui (MPI u, MPI v);
public void subm (MPI u, MPI v, MPI m);
public void mul (MPI u, MPI v);
public void mul_ui (MPI u, ulong v);
public void mulm (MPI u, MPI v, MPI m);
public void mul_2exp (MPI u, ulong cnt);
public void div (MPI q, MPI r, MPI dividend, MPI divisor, int round);
public void mod (MPI dividend, MPI divisor);
public void powm (MPI b, MPI e, MPI m);
public int gcd (MPI a, MPI b);
public int invm (MPI a, MPI m);
public uint get_nbits ();
public int test_bit (uint n);
public void set_bit (uint n);
public void clear_bit (uint n);
public void set_highbit (uint n);
public void clear_highbit (uint n);
public void rshift (MPI a, uint n);
public void lshift (MPI a, uint n);
public void set_flag (MPI.Flag flag);
public void clear_flag (MPI.Flag flag);
public int get_flag (MPI.Flag flag);
}
[Compact, CCode (cname = "struct gcry_sexp", free_function = "gcry_sexp_release")]
public class SExp {
[CCode (cprefix = "GCRYSEXP_FMT_")]
public enum Format {
DEFAULT,
CANON,
BASE64,
ADVANCED
}
public static Error @new (out SExp retsexp, void * buffer, size_t length, int autodetect);
public static Error create (out SExp retsexp, void * buffer, size_t length, int autodetect, GLib.DestroyNotify free_function);
public static Error sscan (out SExp retsexp, out size_t erroff, char[] buffer);
public static Error build (out SExp retsexp, out size_t erroff, string format, ...);
public size_t sprint (Format mode, char[] buffer);
public static size_t canon_len (uchar[] buffer, out size_t erroff, out int errcode);
public SExp find_token (string token, size_t token_length = 0);
public int length ();
public SExp? nth (int number);
public SExp? car ();
public SExp? cdr ();
public unowned char[] nth_data (int number);
public gcry_string nth_string (int number);
public MPI nth_mpi (int number, MPI.Format mpifmt);
}
[CCode (cname = "char", free_function = "gcry_free")]
public class gcry_string : string { }
[CCode (lower_case_cprefix = "gcry_pk_")]
namespace PublicKey {
[CCode (cname = "enum gcry_pk_algos")]
public enum Algorithm {
RSA,
ELG_E,
DSA,
ELG,
ECDSA;
[CCode (cname = "gcry_pk_algo_name")]
public unowned string to_string ();
[CCode (cname = "gcry_pk_map_name")]
public static Algorithm map_name (string name);
}
public static Error encrypt (out SExp ciphertext, SExp data, SExp pkey);
public static Error decrypt (out SExp plaintext, SExp data, SExp skey);
public static Error sign (out SExp signature, SExp data, SExp skey);
public static Error verify (SExp signature, SExp data, SExp pkey);
public static Error testkey (SExp key);
public static Error genkey (out SExp r_key, SExp s_params);
public static uint get_nbits (SExp key);
}
[CCode (lower_case_cprefix = "gcry_kdf_")]
namespace KeyDerivation {
[CCode (cname = "gcry_kdf_algos", cprefix = "GCRY_KDF_", has_type_id = false)]
public enum Algorithm {
NONE,
SIMPLE_S2K,
SALTED_S2K,
ITERSALTED_S2K,
PBKDF1,
PBKDF2,
SCRYPT
}
public GCrypt.Error derive ([CCode (type = "const void*", array_length_type = "size_t")] uint8[] passphrasse, GCrypt.KeyDerivation.Algorithm algo, GCrypt.Hash.Algorithm subalgo, [CCode (type = "const void*", array_length_type = "size_t")] uint8[] salt, ulong iterations, [CCode (type = "void*", array_length_type = "size_t", array_length_pos = 5.5)] uint8[] keybuffer);
}
}
dino-0.5.0/crypto-vala/vapi/libsrtp2.vapi 0000664 0000000 0000000 00000010301 14776241610 016772 0 ustar root root [CCode (cheader_filename = "srtp2/srtp.h")]
namespace Srtp {
public const uint MAX_TRAILER_LEN;
public static ErrorStatus init();
public static ErrorStatus shutdown();
[Compact]
[CCode (cname = "srtp_ctx_t", cprefix = "srtp_", free_function = "srtp_dealloc")]
public class Context {
public static ErrorStatus create(out Context session, Policy? policy);
public ErrorStatus protect([CCode (type = "void*", array_length = false)] uint8[] rtp, ref int len);
public ErrorStatus unprotect([CCode (type = "void*", array_length = false)] uint8[] rtp, ref int len);
public ErrorStatus protect_rtcp([CCode (type = "void*", array_length = false)] uint8[] rtcp, ref int len);
public ErrorStatus unprotect_rtcp([CCode (type = "void*", array_length = false)] uint8[] rtcp, ref int len);
public ErrorStatus add_stream(ref Policy policy);
public ErrorStatus update_stream(ref Policy policy);
public ErrorStatus remove_stream(uint ssrc);
public ErrorStatus update(ref Policy policy);
}
[CCode (cname = "srtp_ssrc_t")]
public struct Ssrc {
public SsrcType type;
public uint value;
}
[CCode (cname = "srtp_ssrc_type_t", cprefix = "ssrc_")]
public enum SsrcType {
undefined, specific, any_inbound, any_outbound
}
[CCode (cname = "srtp_policy_t", destroy_function = "")]
public struct Policy {
public Ssrc ssrc;
public CryptoPolicy rtp;
public CryptoPolicy rtcp;
[CCode (array_length = false)]
public uint8[] key;
public ulong num_master_keys;
public ulong window_size;
[CCode (ctype = "int")]
public bool allow_repeat_tx;
[CCode (array_length_cname = "enc_xtn_hdr_count")]
public int[] enc_xtn_hdr;
public Policy* next;
}
[CCode (cname = "srtp_crypto_policy_t")]
public struct CryptoPolicy {
public CipherType cipher_type;
public int cipher_key_len;
public AuthType auth_type;
public int auth_key_len;
public int auth_tag_len;
public SecurityServices sec_serv;
public void set_aes_cm_128_hmac_sha1_80();
public void set_aes_cm_128_hmac_sha1_32();
public void set_aes_cm_128_null_auth();
public void set_aes_cm_192_hmac_sha1_32();
public void set_aes_cm_192_hmac_sha1_80();
public void set_aes_cm_192_null_auth();
public void set_aes_cm_256_hmac_sha1_32();
public void set_aes_cm_256_hmac_sha1_80();
public void set_aes_cm_256_null_auth();
public void set_aes_gcm_128_16_auth();
public void set_aes_gcm_128_8_auth();
public void set_aes_gcm_128_8_only_auth();
public void set_aes_gcm_256_16_auth();
public void set_aes_gcm_256_8_auth();
public void set_aes_gcm_256_8_only_auth();
public void set_null_cipher_hmac_null();
public void set_null_cipher_hmac_sha1_80();
public void set_rtp_default();
public void set_rtcp_default();
public void set_from_profile_for_rtp(Profile profile);
public void set_from_profile_for_rtcp(Profile profile);
}
[CCode (cname = "srtp_profile_t", cprefix = "srtp_profile_")]
public enum Profile {
reserved, aes128_cm_sha1_80, aes128_cm_sha1_32, null_sha1_80, null_sha1_32, aead_aes_128_gcm, aead_aes_256_gcm
}
[CCode (cname = "srtp_cipher_type_id_t")]
public struct CipherType : uint32 {}
[CCode (cname = "srtp_auth_type_id_t")]
public struct AuthType : uint32 {}
[CCode (cname = "srtp_sec_serv_t", cprefix = "sec_serv_")]
public enum SecurityServices {
none, conf, auth, conf_and_auth;
}
[CCode (cname = "srtp_err_status_t", cprefix = "srtp_err_status_", has_type_id = false)]
public enum ErrorStatus {
ok,
fail,
bad_param,
alloc_fail,
dealloc_fail,
init_fail,
terminus,
auth_fail,
cipher_fail,
replay_fail,
replay_old,
algo_fail,
no_such_op,
no_ctx,
cant_check,
key_expired,
socket_err,
signal_err,
nonce_bad,
read_fail,
write_fail,
parse_err,
encode_err,
semaphore_err,
pfkey_err,
bad_mki,
pkt_idx_old,
pkt_idx_adv
}
[CCode (cname = "srtp_log_level_t", cprefix = "srtp_log_level_", has_type_id = false)]
public enum LogLevel {
error, warning, info, debug
}
[CCode (cname = "srtp_log_handler_func_t")]
public delegate void LogHandler(LogLevel level, string msg);
public static ErrorStatus install_log_handler(LogHandler func);
} dino-0.5.0/dino.doap 0000664 0000000 0000000 00000120453 14776241610 012760 0 ustar root root
Dino
dino
Modern XMPP chat client
現代化的 XMPP 用戶端聊天軟件
现代 XMPP 聊天客户端
Modern XMPP Sohbet İstemcisi
Modern XMPP-chattklient
Klient Modern Fjalosjesh XMPP
Современный XMPP клиент
Client XMPP de discuții modern
Cliente de Chat XMPP Moderno
Moderno cliente de chat XMPP
Nowoczesny komunikator XMPP
Client XMPP modèrn
Modernen XMPP-chatcliënt
Een moderne XMPP-chatclient
Moderne XMPP-sludreklient
Šiuolaikinė XMPP pokalbių kliento programa
Modernen XMPP Chat Client
現代的な XMPP チャット クライアント
Client di chat moderno per XMPP
Un modern client de conversationes XMPP
Aplikasi chat XMPP modern
Modern XMPP csevegőprogram
Cliente moderno para conversas XMPP
Client de clavardage XMPP moderne
کلاینت نوین گپ XMPP
XMPP txat bezero modernoa
Un cliente de XMPP moderno
Moderna XMPP-Retebabililo
Modernes XMPP-Chat-Programm
Moderní XMPP klient
Client de xat XMPP modern
تطبيق حديث للدردشة عبر XMPP
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.
Dino fetches history from the server and synchronizes messages with other devices.
Dino 是一個爲桌面打造的現代化開放原始碼用戶端聊天軟件。它致力於提供一份簡洁而可靠的 Jabber/XMPP 體驗,同時亦尊重您的私隱。
它支持 OMEMO 和 OpenPGP 兩種端到端加密方式,並且容許設定私隱相關的特性譬如已讀回條和正在輸入提示。
Dino 從伺服器取得訊息並與其他裝置同步。
Dino 是一个现代的开源聊天桌面客户端。它致力于提供一个清爽又可靠的 Jabber/XMPP 体验,同时又保护您的隐私。
它支持 OMEMO 和 OpenPGP 端对端加密并允许配置隐私相关的特性比如已读回执和输入提醒。
Dino 从服务器获取消息并和其他设备同步。
Dino masaüstü bilgisayarlar için modern, açık kaynaklı bir sohbet uygulamasıdır. Gizlilik hassasiyetinizi göz önünde bulundurarak temiz ve güvenilir bir Jabber/XMPP deneyimi sunmaya odaklanır.
OMEMO ve OpenPGP ile baştan sona şifreleme destekler ve "okundu" bilgisi, "yazıyor..." bildirimi gibi gizlilikle alakalı özelliklerin ayarlanabilmesini sağlar.
Dino sunucudan konuşma geçmişini sunucudan çeker ve diğer cihazlara senkronize eder.
Dino är en moden chattklient för skrivbordet med öppen källkod. Den erbjuder en elegant och pålitligt upplevelse av Jabber/XMPP samtidigt som den ser efter din integritet.
Dino stödjer end-to-end-kryptering med OMEMO och OpenPGP och tillåter konfigurering av funktioner med integritetspåverkan som läsbekräftelser och skriftaviseringar.
Dino hämtar historik från servern och synkroniserar meddelanden med andra enheter.
Dino është një klient modern fjalosjesh, me burim të hapur, për desktop. Ai përqendrohet në dhënien e një mënyre funksionimi të qartë dhe të qëndrueshme për protokoll Jabber/XMPP, teksa ka në mendje privatësinë tuaj.
Mbulon fshehtëzim skaj-më-skaj me OMEMO dhe OpenPGP dhe lejon formësim veçorish të lidhura me privatësinë, bie fjala, dëshmish leximi dhe njoftime shtypjeje.
Dino e sjell historikun prej shërbyesve dhe njëkohëson mesazhet në pajisje të tjera.
Dino - это современный клиент для чатов с открытым исходным кодом, направленный на надёжное и приватное использование Jabber/XMPP на персональных компьютерах.
Он поддерживает сквозное шифрование через OMEMO и OpenPGP и позволяет настраивать такие функции, как уведомления о прочтении и наборе сообщений.
Dino загружает историю с сервера и синхронизирует сообщения с другими устройствами.
Dino este un client de chat modern, cu sursă deschisă, pentru calculatoare. Se concentrează să furnizeze o experiență Jabber/XMPP clară și fiabilă, ținând cont de confidențialitatea dumneavoastră.
Suportă criptare de la un capăt la altul prin intermediul OMEMO și OpenPGP, și permite configurarea caracteristicilor legate de confidențialitate precum trimiterea notificărilor de primire și tastare.
Dino preia istoricul discuțiilor de pe server și sincronizează mesajele cu celelalte dispozitive.
Dino é um cliente moderno de código aberto de chat para desktop. Ele foca em oferecer uma experiência Jabber/XMPP transparente e confiável levando em consideração a sua privacidade.
Ele suporta criptografia ponta-a-ponta com OMEMO e OpenPGP e, além disso, permite configurar funcionalidades relativas à privacidade como notificações de leitura, recebimento e escrita.
Dino obtém o histórico do servidor e sincroniza mensagens com outros dispositivos.
Dino é um moderno chat de código aberto para desktop. Ele é focado em prover uma transparente e confiável experiência Jabber/XMPP, tendo em mente a sua privacidade.
Suporte criptografia ponta a ponta com OMEMO e OpenPGP e permite configurar privacidade—características relacionadas às notificações de leitura, recebimento e escrita.
Dino obtém o histórico do servidor e sincroniza mensagens com outros aparelhos.
Dino jest nowoczesnym, otwartym komunikatorem. Skupia się na prostej obsłudze sieci Jabber/XMPP dbając o twoją prywatność.
Obsługuje szyfrowanie od końca do końca za pomocą OMEMO i OpenPGP, a także daje kontrolę nad funkcjami wpływającymi na prywatność, jak powiadomienia o pisaniu czy odczytaniu wiadomości.
Dino pobiera historię rozmów z serwera i synchronizuje wiadomości z innymi urządzeniami.
Dino es un client de chat liure e modèrn per l’ordenador de burèu. Ensaja de provesir una experiéncia neta e fisabla de Jabber/XMPP en téner en compte vòstra confidencialitat.
Compatible amb lo chiframent OMEMO e OpenPGP del cap a la fin e permet de configurar de foncionalitats ligadas amb la confidencialitat coma los acusats de lectura e las notificacions d’escritura.
Dino recupèra l’istoric del servidor e sincroniza los messatges amb d’autres periferics.
Dino is een moderne, vrije chattoepassing voor uw bureaublad. Ze biedt een eenvoudige en betrouwbare Jabber/XMPP-ervaring, met uw privacy in het achterhoofd.
Ze ondersteunt eind-tot-eind-versleuteling met OMEMO en OpenPGP, en laat u toe privacygerelateerde functies, gelijk leesbevestigingen en typmeldingen, in te stellen.
Dino haalt de geschiedenis op van de server en synchroniseert berichten met andere apparaten.
Dino is een moderne, vrije chattoepassing voor je computer. Dino biedt een eenvoudige en betrouwbare Jabber-/XMPP-ervaring, met privacy in het achterhoofd.
Dino ondersteunt end-to-endversleuteling met OMEMO en OpenPGP en staat je toe privacy-gerelateerde functies, zoals leesbevestigingen en aan-het-typenmeldingen, in te stellen.
Dino haalt de geschiedenis op van de server en synchroniseert berichten met andere apparaten.
Dino er en moderne friporg-sludringsklient for skrivebordet. Det fokuserer på rask og pålitelig XMPP-opplevelse, samtidig som det hegner om personvernet.
Det støtter ende-til-ende -kryptering med OMEMO og OpenPGP, og tillater oppsett av personvernsrelaterte funksjoner som meldingskvitteringer og skrivevarsling.
Dino henter historikk fra tjeneren og synkroniserer meldinger med andre enheter.
Dino yra šiuolaikinė atvirojo kodo kliento programa skirta darbalaukiui. Jos pagrindinis dėmesys yra pateikti tvarkingą ir patikimą Jabber/XMPP patyrimą nepamirštant apie jūsų privatumą.
Ji palaiko ištisinį šifravimą naudojant OMEMO ir OpenPGP bei leidžia konfigūruoti su privatumu susijusias ypatybes, tokias kaip pranešimus apie žinučių skaitymą ir rašymą.
Dino gauna istoriją iš serverio ir sinchronizuoja žinutes su kitais įrenginiais.
Dino ass e modernen, quell-offene Chat Client fir den Desktop. Hien biet eng opgeraumt a robust Jabber/XMPP Erfarung a leet ee Schwéierpunkt op Privatsphär.
Hien ënnerstëtz Enn-zu-Enn Verschlësselung mat OMEMO an OpenPGP an enthält Privatsphäre-Astellungen zu Liesbestätegungen an Tipp-Benoriichtegungen.
Dino rifft Gespréichverläf vum Server of a synchroniséiert Noriichte mat anere Geräter.
Dino はオープンソースの現代的なデスクトップ向けチャットクライアントです。プライバシーを考慮しつつ、シンプルで信頼できる Jabber/XMPP エクスペリエンスの提供を第一に考えて開発されています。
OMEMO と OpenPGP を利用したエンドツーエンド暗号化に対応しており、既読状態の送信や入力通知などのプライバシー関連の設定も可能です。
Dino はサーバーから履歴を取得し、ほかのデバイスとメッセージを同期します。
Dino è un client di chat per il desktop, moderno e open-source. Si concentra nel fornire un'esperienza Jabber/XMPP pulita e affidabile tenendo presente la tua privacy.
Support la crittografia end-to-end tramite OMEMO e OpenPGP e permette di configurare le funzioni relative alla privacy come le ricevute di lettura e le notifiche di digitazione.
Dino recupera la cronologia dal server e sincronizza i messaggi con gli altri dispositivi.
Dino es un modern cliente de conversationes con fonte apert. It foca se sur provider un nett e fidibil experientie de Jabber/XMPP con un attention a confidentialitá.
It supporta ciffration terminal per OMEMO e OpenPGP e permisse configurar sensitiv functiones quam confirmation de lectada e notificationes de tippada.
Dino obtene li diarium del servitore e sincronisa missages inter altri apparates.
Dino adalah aplikasi chat open source modern untuk PC. Menyediakan pengalaman Jabber / XMPP yang handal dengan tetap menjunjung privasi Anda.
Mendukung enkripsi end-to-end dengan OMEMO dan OpenPGP, dan memungkinkan pengaturan fitur terkait privasi seperti tanda pesan dibaca dan pemberitahuan pengetikan.
Dino mengambil riwayat pesan dari server dan menyinkronkan pesan dengan perangkat lain.
A Dino egy modern, nyílt forráskódú csevegőprogram az asztali gépekre. Arra összpontosít, hogy tiszta és megbízható Jabber/XMPP-élményt nyújtson, miközben a magánszféra megőrzését is fontosnak tartja.
Támogatja az OMEMO és az OpenPGP használatával történő végpontok közötti titkosítást, és lehetővé teszi a magánszférához kapcsolódó funkciókat, mint például az olvasási visszaigazolást és a gépelési értesítéseket.
A Dino lekéri az előzményeket a kiszolgálóról, és szinkronizálja az üzeneteket a többi eszközzel.
Dino é un cliente moderno e de código aberto para o escritorio. Orientado a fornecer unha experiencia Jabber/XMPP limpa e fiábel tendo a privacidade e seguranza presentes.
Suporta o cifrado de punto-a-punto con OMEMO e OpenPGP e permite configurar trazos orientados á privacidade tales coma confirmación de lectura e notificacións de escritura.
Dino obtén o histórico dende o servidor e sincroniza as mensaxes con outros dispositivos.
Dino est un client de clavardage libre et moderne pour le bureau. Il se concentre sur la fourniture d’une expérience XMPP simple et fiable tout en ayant toujours à l’esprit votre confidentialité.
Il prend en charge le chiffrement de bout en bout avec OMEMO et OpenPGP et permet de configurer les fonctions liées à la confidentialité telles que les accusés de réception et les notifications d’écriture.
Dino récupère l’historique du serveur et synchronise les messages avec les autres clients.
Dino on nykyaikainen avoimen lähdekoodin jutteluohjelma työpöydälle. Se keskittyy tarjoamaan selkeän ja luotettavan Jabber/XMPP-kokemuksen unohtamatta yksityisyyttäsi.
Se tukee päästä päähän -salausta OMEMO:n ja OpenPGP:n avulla ja mahdollistaa yksityisyyteen liittyvien ominaisuuksien, kuten lukukuittausten ja kirjoitusilmoitusten asetusten määrittämisen.
Dino hakee historian palvelimelta ja synkronisoi viestit muiden laitteiden kanssa.
دینو یک کلاینت چت متنباز برای دسکتاپ است. تمرکز آن بر فراهمکردن تجربهای شستهرفته و قابلاتکا از جَبِر/XMPP است درحالی که به حریم خصوصیتان اهمیت میدهد.
از رمزگذاری سرتاسر با اُمیمو و اُپنپیجیپی پشتیبانی میکند و اجازه تنظیم قابلیتهای مربوط به حریم خصوصی را میدهد، از جمله: رسید خواندهشدن پیامها و اعلان در حال نوشتن بودن.
دینو تاریخچه را از سرور دریافت میکند و پیامها را با دیگر دستگاهها همگامسازی میکند.
Dino mahaigainerako iturburu irekiko txat bezero moderno bat da. Jabber/XMPP esperientzia garbi eta fidagarri bat ematen du zure pribatutasuna kontuan hartzeaz gain.
Amaieratik amaierarako enkriptazioa onartzen du OMEMO eta OpenPGPrekin eta pribatutasun ezaugarriak konfiguratzea baimentzen du irakurtze markak eta idazketa jakinarazpenak bezala.
Dinok zerbitzaritik hartzen du historia eta beste gailuekin mezuak sinkronizatzen ditu.
Dino es un cliente de mensajería moderno y libre para escritorio y móvil. Está enfocado en proveer una experiencia Jabber/XMPP limpia y confiable teniendo la privacidad en mente.
Soporta encriptación fin-a-fin a través de OMEMO y OpenPGP y permite configurar las características relacionadas con la privacidad, como confirmaciones de lectura y notificaciones de escritura.
Dino recupera el historial de mensajes desde el servidor y sincroniza los mensajes con otros dispositivos.
Dino estas moderna malfermfonta retbabililo por la tabla komputilo. Ĝi celas provizi puran kaj fidindan sperton de Jabber/XMPP, protektante vian privatecon.
Ĝi subtenas fin-al-finan ĉifradon per OMEMO kaj OpenPGP kaj permesas agordi funkciojn pri privateco kiel kvitancojn de legiteco kaj sciigojn pri tajpado.
Dino prenas historion el la servilo kaj sinkronigas mesaĝojn kun aliaj aparatoj.
Dino ist ein modernes, quelloffenes Chat-Programm für den Desktop. Es bietet ein aufgeräumtes und robustes Jabber-/XMPP-Erlebnis und legt einen Schwerpunkt auf Privatsphäre.
Er unterstützt Ende-zu-Ende Verschlüsselung mit OMEMO und OpenPGP und enthält Privatsphäre-Einstellungen zu Lesebestätigungen und Tippbenachrichtigungen.
Dino ruft Gesprächsverläufe vom Server ab und synchronisiert Nachrichten mit anderen Geräten.
Dino je moderní open-source chatovací klient pro stolní počítače. Jeho cílem je poskytování čistého a spolehlivého prostředí Jabber/XMPP s důrazem na zachování vašeho soukromí.
Podporuje šifrování end-to-end pomocí OMEMO a OpenPGP a umožňuje konfigurovat funkce související se soukromím, jako jsou potvrzení o přečtení a oznámení o psaní.
Dino načítá historii ze serveru a synchronizuje zprávy s ostatními zařízeními.
Dino és un client de xat lliure i modern per a l'escriptori. Està centrat en proveir una experiència neta i fiable de Jabber/XMPP, sempre tenint en compte la vostra privacitat.
Implementa xifratge punt a punt amb OMEMO i OpenPGP, i permet configurar funcionalitats relacionades amb la privacitat com per exemple rebuts de lectura i notificacions d'escriptura.
Dino recupera l'historial del servidor i sincronitza els missatges amb altres dispositius.
إنّ Dino برنامج عصري ومفتوح المصدر للدردشة صُمّم لسطح المكتب. ويُركّز علي تقديم تجربة نظيفة وموثوق منها لجابر/XMPP مع أخذ خصوصيتكم بعين الإعتبار.
وهو يدعم التشفير بواسطة OMEMO و OpenPGP يسمح بإعداد ميزات الخصوصية كالإيصالات المقروءة والإخطارات عند الكتابة.
يقوم Dino بجلب السِجلّ مِن السيرفر ثم يُزامِن الرسائل مع الأجهزة الأخرى.
Vala
Linux
FreeBSD
complete
0.1
complete
0.1
complete
0.1
partial
0.1
complete
For use with XEP-0261
0.1
deprecated
Migrating to XEP-0402 if supported by server
0.1
complete
0.1
partial
Only for viewing avatars
0.1
partial
For use with XEP-0313
0.1
partial
0.1
partial
For use with XEP-0260
0.1
complete
For file transfers using XEP-0363
0.1
complete
0.1
complete
0.1
complete
0.1
complete
0.1
complete
0.1
deprecated
0.1
Only to fetch Avatars from other users
complete
0.1
partial
0.1
partial
0.3
partial
0.3
complete
0.3
complete
0.1
complete
0.1
complete
0.1
complete
0.1
complete
0.1
partial
0.3
complete
0.1
complete
0.1
partial
0.1
1.0
complete
0.1
partial
No support for sending
0.3
complete
1.0.3
0.1
complete
1.0
0.1
partial
0.2.0
0.3
complete
1.0.1
0.1
partial
1.0.2
0.3
partial
1.1.2
0.3
partial
1.0
0.1
partial
0.3
complete
1.2.0
0.2
partial
0.1
complete
1.0.0
0.3
complete
0.1
partial
0.1
complete
0.3.1
0.3
complete
0.1
complete
0.1
partial
0.3
0.5
complete
1.1.0
0.1
partial
Only for outgoing messages
0.1
complete
0.3.0
0.1
partial
0.1.2
0.1
complete
1.0.0
0.5
partial
1.1.1
0.1
partial
0.3.0
0.5
complete
0.1
complete
0.1
complete
1.2.0
0.1
complete
0.2
complete
0.2.0
0.4
complete
0.3.0
partial
0.2.1
complete
0.1.1
0.4
partial
0.2.0
0.5
partial
0.3.0
0.5
complete
0.1.2
partial
No support for embedded thumbnails
0.1
complete
0.2.0
0.4
partial
0.1.0
0.5
partial
0.1.0
dino-0.5.0/dino.doap.in 0000664 0000000 0000000 00000052642 14776241610 013371 0 ustar root root
Dino
dino
Modern XMPP chat client
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.
Dino fetches history from the server and synchronizes messages with other devices.
Vala
Linux
FreeBSD
complete
0.1
complete
0.1
complete
0.1
partial
0.1
complete
For use with XEP-0261
0.1
deprecated
Migrating to XEP-0402 if supported by server
0.1
complete
0.1
partial
Only for viewing avatars
0.1
partial
For use with XEP-0313
0.1
partial
0.1
partial
For use with XEP-0260
0.1
complete
For file transfers using XEP-0363
0.1
complete
0.1
complete
0.1
complete
0.1
complete
0.1
complete
0.1
deprecated
0.1
Only to fetch Avatars from other users
complete
0.1
partial
0.1
partial
0.3
partial
0.3
complete
0.3
complete
0.1
complete
0.1
complete
0.1
complete
0.1
complete
0.1
partial
0.3
complete
0.1
complete
0.1
partial
0.1
1.0
complete
0.1
partial
No support for sending
0.3
complete
1.0.3
0.1
complete
1.0
0.1
partial
0.2.0
0.3
complete
1.0.1
0.1
partial
1.0.2
0.3
partial
1.1.2
0.3
partial
1.0
0.1
partial
0.3
complete
1.2.0
0.2
partial
0.1
complete
1.0.0
0.3
complete
0.1
partial
0.1
complete
0.3.1
0.3
complete
0.1
complete
0.1
partial
0.3
0.5
complete
1.1.0
0.1
partial
Only for outgoing messages
0.1
complete
0.3.0
0.1
partial
0.1.2
0.1
complete
1.0.0
0.5
partial
1.1.1
0.1
partial
0.3.0
0.5
complete
0.1
complete
0.1
complete
1.2.0
0.1
complete
0.2
complete
0.2.0
0.4
complete
0.3.0
partial
0.2.1
complete
0.1.1
0.4
partial
0.2.0
0.5
partial
0.3.0
0.5
complete
0.1.2
partial
No support for embedded thumbnails
0.1
complete
0.2.0
0.4
partial
0.1.0
0.5
partial
0.1.0
dino-0.5.0/im.dino.Dino.json 0000664 0000000 0000000 00000005713 14776241610 014303 0 ustar root root {
"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": "git",
"url": "https://github.com/protocolbuffers/protobuf.git",
"tag": "v29.4"
}
]
},
{
"name": "libprotobuf-c",
"buildsystem": "autotools",
"config-opts": [
"CFLAGS=-fPIC"
],
"post-install": [
"rm /app/lib/*.so"
],
"cleanup": [
"*"
],
"sources": [
{
"type": "git",
"url": "https://github.com/protobuf-c/protobuf-c.git",
"tag": "v1.5.1"
}
]
},
{
"name": "libomemo-c",
"buildsystem": "meson",
"cleanup": [
"/lib/pkgconfig",
"/include"
],
"config-opts": [
"-Dtests=false",
"-Ddefault_library=static"
],
"sources": [
{
"type": "git",
"url": "https://github.com/dino/libomemo-c.git",
"tag": "v0.5.1"
}
]
},
{
"name": "qrencode",
"buildsystem": "cmake-ninja",
"cleanup": [
"*"
],
"config-opts": [
"-DCMAKE_C_FLAGS=-fPIC"
],
"sources": [
{
"type": "archive",
"url": "https://fukuchi.org/works/qrencode/qrencode-4.1.1.tar.gz",
"sha512": "209bb656ae3f391b03c7b3ceb03e34f7320b0105babf48b619e7a299528b8828449e0e7696f0b5db0d99170a81709d0518e34835229a748701e7df784e58a9ce"
}
]
},
{
"name": "dino",
"buildsystem": "meson",
"builddir": true,
"cleanup": [
"/include",
"/share/vala"
],
"sources": [
{
"type": "dir",
"path": "."
}
]
}
]
}
dino-0.5.0/libdino/ 0000775 0000000 0000000 00000000000 14776241610 012575 5 ustar root root dino-0.5.0/libdino/dino.deps 0000664 0000000 0000000 00000000074 14776241610 014404 0 ustar root root gdk-pixbuf-2.0
gee-0.8
glib-2.0
gmodule-2.0
qlite
xmpp-vala
dino-0.5.0/libdino/meson.build 0000664 0000000 0000000 00000007060 14776241610 014742 0 ustar root root # 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')
dino-0.5.0/libdino/src/ 0000775 0000000 0000000 00000000000 14776241610 013364 5 ustar root root dino-0.5.0/libdino/src/application.vala 0000664 0000000 0000000 00000014023 14776241610 016534 0 ustar root root using Dino.Entities;
namespace Dino {
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 abstract Database db { get; set; }
public abstract Dino.Entities.Settings settings { get; set; }
public abstract StreamInteractor stream_interactor { get; set; }
public abstract Plugins.Registry plugin_registry { get; set; }
public abstract SearchPathGenerator? search_path_generator { get; set; }
internal static string print_xmpp;
private const OptionEntry[] options = {
{ "print-xmpp", 0, 0, OptionArg.STRING, ref print_xmpp, "Print XMPP stanzas identified by DESC to stderr", "DESC" },
{ null }
};
public abstract void handle_uri(string jid, string query, Gee.Map options);
public void init() throws Error {
if (DirUtils.create_with_parents(get_storage_dir(), 0700) == -1) {
throw new Error(-1, 0, "Could not create storage dir \"%s\": %s", get_storage_dir(), FileUtils.error_from_errno(errno).to_string());
}
this.db = new Database(Path.build_filename(get_storage_dir(), "dino.db"));
this.settings = new Dino.Entities.Settings.from_db(db);
this.stream_interactor = new StreamInteractor(db);
MessageProcessor.start(stream_interactor, db);
MessageStorage.start(stream_interactor, db);
PresenceManager.start(stream_interactor);
CounterpartInteractionManager.start(stream_interactor);
BlockingManager.start(stream_interactor);
Calls.start(stream_interactor, db);
ConversationManager.start(stream_interactor, db);
MucManager.start(stream_interactor);
AvatarManager.start(stream_interactor, db);
RosterManager.start(stream_interactor, db);
FileManager.start(stream_interactor, db);
CallStore.start(stream_interactor, db);
ContentItemStore.start(stream_interactor, db);
ChatInteraction.start(stream_interactor);
NotificationEvents.start(stream_interactor);
SearchProcessor.start(stream_interactor, db);
Register.start(stream_interactor, db);
EntityInfo.start(stream_interactor, db);
MessageCorrection.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();
startup.connect(() => {
stream_interactor.connection_manager.log_options = print_xmpp;
restore();
});
shutdown.connect(() => {
stream_interactor.connection_manager.make_offline_all();
});
open.connect((files, hint) => {
if (files.length != 1) {
warning("Can't handle more than one URI at once.");
return;
}
File file = files[0];
if (!file.has_uri_scheme("xmpp")) {
warning("xmpp:-URI expected");
return;
}
string uri = file.get_uri();
if (!uri.contains(":")) {
warning("Invalid URI");
return;
}
string r = uri.split(":", 2)[1];
string[] m = r.split("?", 2);
string jid = m[0];
while (jid[0] == '/') {
jid = jid.substring(1);
}
jid = Uri.unescape_string(jid);
try {
jid = new Xmpp.Jid(jid).to_string();
} catch (Xmpp.InvalidJidError e) {
warning("Received invalid jid in xmpp:-URI: %s", e.message);
}
string query = "message";
Gee.Map options = new Gee.HashMap();
if (m.length == 2) {
string[] cmds = m[1].split(";");
query = cmds[0];
for (int i = 1; i < cmds.length; ++i) {
string[] opt = cmds[i].split("=", 2);
options[Uri.unescape_string(opt[0])] = opt.length == 2 ? Uri.unescape_string(opt[1]) : "";
}
}
activate();
handle_uri(jid, query, options);
});
add_main_option_entries(options);
}
public static string get_storage_dir() {
return Path.build_filename(Environment.get_user_data_dir(), "dino");
}
public static unowned Application get_default() {
return (Dino.Application) GLib.Application.get_default();
}
public void create_actions() {
SimpleAction accept_subscription_action = new SimpleAction("accept-subscription", VariantType.INT32);
accept_subscription_action.activate.connect((variant) => {
Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation_by_id(variant.get_int32());
if (conversation == null) return;
stream_interactor.get_module(PresenceManager.IDENTITY).approve_subscription(conversation.account, conversation.counterpart);
stream_interactor.get_module(PresenceManager.IDENTITY).request_subscription(conversation.account, conversation.counterpart);
});
add_action(accept_subscription_action);
}
protected void add_connection(Account account) {
if ((get_flags() & ApplicationFlags.IS_SERVICE) == ApplicationFlags.IS_SERVICE) hold();
stream_interactor.connect_account(account);
}
protected void remove_connection(Account account) {
if ((get_flags() & ApplicationFlags.IS_SERVICE) == ApplicationFlags.IS_SERVICE) release();
stream_interactor.disconnect_account.begin(account);
}
private void restore() {
foreach (Account account in db.get_accounts()) {
if (account.enabled) add_connection(account);
}
}
}
}
dino-0.5.0/libdino/src/dbus/ 0000775 0000000 0000000 00000000000 14776241610 014321 5 ustar root root dino-0.5.0/libdino/src/dbus/login1.vala 0000664 0000000 0000000 00000000652 14776241610 016362 0 ustar root root namespace Dino {
[DBus (name = "org.freedesktop.login1.Manager")]
public interface Login1Manager : Object {
public signal void PrepareForSleep(bool suspend);
}
public static async Login1Manager? get_login1() {
try {
return yield Bus.get_proxy(BusType.SYSTEM, "org.freedesktop.login1", "/org/freedesktop/login1");
} catch (IOError e) {
stderr.printf("%s\n", e.message);
}
return null;
}
} dino-0.5.0/libdino/src/dbus/notifications.vala 0000664 0000000 0000000 00000002375 14776241610 020046 0 ustar root root namespace Dino {
[DBus (name = "org.freedesktop.Notifications")]
public interface DBusNotifications : GLib.Object {
public signal void action_invoked(uint32 key, string action_key);
public signal void notification_closed (uint32 id, uint32 reason);
public abstract async uint32 notify(string app_name, uint32 replaces_id, string app_icon, string summary,
string body, string[] actions, HashTable hints, int32 expire_timeout) throws DBusError, IOError;
public abstract async void get_capabilities(out string[] capabilities) throws Error;
public abstract async void close_notification(uint id) throws DBusError, IOError;
public abstract async void get_server_information(out string name, out string vendor, out string version, out string spec_version) throws DBusError, IOError;
}
public static async DBusNotifications? get_notifications_dbus() {
try {
return yield Bus.get_proxy(BusType.SESSION, "org.freedesktop.Notifications", "/org/freedesktop/Notifications");
} catch (IOError e) {
warning("Couldn't get org.freedesktop.Notifications DBus instance: %s\n", e.message);
}
return null;
}
} dino-0.5.0/libdino/src/dbus/upower.vala 0000664 0000000 0000000 00000000675 14776241610 016517 0 ustar root root namespace Dino {
[DBus (name = "org.freedesktop.UPower")]
public interface UPower : Object {
public signal void Sleeping();
public signal void Resuming();
}
public static UPower? get_upower() {
UPower? upower = null;
try {
upower = Bus.get_proxy_sync(BusType.SYSTEM, "org.freedesktop.UPower", "/org/freedesktop/UPower");
} catch (IOError e) {
stderr.printf ("%s\n", e.message);
}
return upower;
}
} dino-0.5.0/libdino/src/dino_i18n.h 0000664 0000000 0000000 00000000405 14776241610 015324 0 ustar root root #ifndef __DINO_I18N_H__
#define __DINO_I18N_H__
#include
#define dino_gettext(MsgId) ((char *) dgettext (GETTEXT_PACKAGE, MsgId))
#define dino_ngettext(MsgId, MsgIdPlural, Int) ((char *) dngettext (GETTEXT_PACKAGE, MsgId, MsgIdPlural, Int))
#endif dino-0.5.0/libdino/src/entity/ 0000775 0000000 0000000 00000000000 14776241610 014700 5 ustar root root dino-0.5.0/libdino/src/entity/account.vala 0000664 0000000 0000000 00000007232 14776241610 017205 0 ustar root root using Gee;
using Xmpp;
namespace Dino.Entities {
public class Account : Object {
public int id { get; set; }
public string localpart { get { return full_jid.localpart; } }
public string domainpart { get { return full_jid.domainpart; } }
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 full_jid { get; private set; }
public string? password { get; set; }
public string display_name {
owned get { return (alias != null && alias.length > 0) ? alias.dup() : bare_jid.to_string(); }
}
public string? alias { get; set; }
public bool enabled { get; set; default = false; }
public string? roster_version { get; set; }
private Database? db;
public Account(Jid bare_jid, string password) {
this.id = -1;
try {
this.full_jid = bare_jid.with_resource(get_random_resource());
} catch (InvalidJidError e) {
error("Auto-generated resource was invalid (%s)", e.message);
}
this.password = password;
}
public Account.from_row(Database db, Qlite.Row row) throws InvalidJidError {
this.db = db;
id = row[db.account.id];
full_jid = new Jid(row[db.account.bare_jid]).with_resource(row[db.account.resourcepart]);
password = row[db.account.password];
alias = row[db.account.alias];
enabled = row[db.account.enabled];
roster_version = row[db.account.roster_version];
notify.connect(on_update);
}
public void persist(Database db) {
if (id > 0) return;
this.db = db;
id = (int) db.account.insert()
.value(db.account.bare_jid, bare_jid.to_string())
.value(db.account.resourcepart, resourcepart)
.value(db.account.password, password)
.value(db.account.alias, alias)
.value(db.account.enabled, enabled)
.value(db.account.roster_version, roster_version)
.perform();
notify.connect(on_update);
}
public void remove() {
db.account.delete().with(db.account.bare_jid, "=", bare_jid.to_string()).perform();
notify.disconnect(on_update);
id = -1;
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) {
return equals_func(this, acc);
}
public static bool equals_func(Account acc1, Account acc2) {
return acc1.bare_jid.to_string() == acc2.bare_jid.to_string();
}
public static uint hash_func(Account acc) {
return acc.bare_jid.to_string().hash();
}
private void on_update(Object o, ParamSpec sp) {
var update = db.account.update().with(db.account.id, "=", id);
switch (sp.name) {
case "bare-jid":
update.set(db.account.bare_jid, bare_jid.to_string()); break;
case "resourcepart":
update.set(db.account.resourcepart, resourcepart); break;
case "password":
update.set(db.account.password, password); break;
case "alias":
update.set(db.account.alias, alias); break;
case "enabled":
update.set(db.account.enabled, enabled); break;
case "roster-version":
update.set(db.account.roster_version, roster_version); break;
}
update.perform();
}
}
}
dino-0.5.0/libdino/src/entity/call.vala 0000664 0000000 0000000 00000015232 14776241610 016463 0 ustar root root using Xmpp;
namespace Dino.Entities {
public class Call : Object {
public const bool DIRECTION_OUTGOING = true;
public const bool DIRECTION_INCOMING = false;
public enum State {
RINGING,
ESTABLISHING,
IN_PROGRESS,
OTHER_DEVICE,
ENDED,
DECLINED,
MISSED,
FAILED
}
public int id { get; set; default=-1; }
public Account account { get; set; }
public Jid counterpart { get; set; }
public Gee.List counterparts = new Gee.ArrayList(Jid.equals_bare_func);
public Jid ourpart { get; set; }
public Jid proposer {
get { return direction == DIRECTION_OUTGOING ? ourpart : counterpart; }
}
public bool direction { get; set; }
public DateTime time { get; set; }
public DateTime local_time { get; set; }
public DateTime end_time { get; set; }
public Encryption encryption { get; set; default=Encryption.NONE; }
public State state { get; set; }
private Database? db;
public Call.from_row(Database db, Qlite.Row row) throws InvalidJidError {
this.db = db;
id = row[db.call.id];
account = db.get_account_by_id(row[db.call.account_id]);
string our_resource = row[db.call.our_resource];
if (our_resource != null) {
ourpart = account.bare_jid.with_resource(our_resource);
} else {
ourpart = account.bare_jid;
}
direction = row[db.call.direction];
time = new DateTime.from_unix_utc(row[db.call.time]);
local_time = new DateTime.from_unix_utc(row[db.call.local_time]);
end_time = new DateTime.from_unix_utc(row[db.call.end_time]);
encryption = (Encryption) row[db.call.encryption];
state = (State) row[db.call.state];
Qlite.QueryBuilder counterparts_select = db.call_counterpart.select().with(db.call_counterpart.call_id, "=", id);
foreach (Qlite.Row counterparts_row in counterparts_select) {
Jid peer = db.get_jid_by_id(counterparts_row[db.call_counterpart.jid_id]);
if (!counterparts.contains(peer)) { // Legacy: The first peer is also in the `call` table. Don't add twice.
counterparts.add(peer);
}
}
counterpart = db.get_jid_by_id(row[db.call.counterpart_id]);
string counterpart_resource = row[db.call.counterpart_resource];
if (counterpart_resource != null) counterpart = counterpart.with_resource(counterpart_resource);
if (counterparts.is_empty) {
counterparts.add(counterpart);
}
notify.connect(on_update);
}
public void persist(Database db) {
if (id != -1) return;
this.db = db;
Qlite.InsertBuilder builder = db.call.insert()
.value(db.call.account_id, account.id)
.value(db.call.our_resource, ourpart.resourcepart)
.value(db.call.direction, direction)
.value(db.call.time, (long) time.to_unix())
.value(db.call.local_time, (long) local_time.to_unix())
.value(db.call.encryption, encryption)
.value(db.call.state, State.ENDED); // No point in persisting states that can't survive a restart
if (end_time != null) {
builder.value(db.call.end_time, (long) end_time.to_unix());
} else {
builder.value(db.call.end_time, (long) local_time.to_unix());
}
if (counterpart != null) {
builder.value(db.call.counterpart_id, db.get_jid_id(counterpart))
.value(db.call.counterpart_resource, counterpart.resourcepart);
}
id = (int) builder.perform();
foreach (Jid peer in counterparts) {
db.call_counterpart.insert()
.value(db.call_counterpart.call_id, id)
.value(db.call_counterpart.jid_id, db.get_jid_id(peer))
.value(db.call_counterpart.resource, peer.resourcepart)
.perform();
}
notify.connect(on_update);
}
public void add_peer(Jid peer) {
if (counterparts.contains(peer)) return;
counterparts.add(peer);
if (db != null) {
db.call_counterpart.insert()
.value(db.call_counterpart.call_id, id)
.value(db.call_counterpart.jid_id, db.get_jid_id(peer))
.value(db.call_counterpart.resource, peer.resourcepart)
.perform();
}
}
public bool equals(Call c) {
return equals_func(this, c);
}
public static bool equals_func(Call c1, Call c2) {
if (c1.id == c2.id) {
return true;
}
return false;
}
public static uint hash_func(Call call) {
return (uint)call.id;
}
private void on_update(Object o, ParamSpec sp) {
Qlite.UpdateBuilder update_builder = db.call.update().with(db.call.id, "=", id);
switch (sp.name) {
case "counterpart":
update_builder.set(db.call.counterpart_id, db.get_jid_id(counterpart));
update_builder.set(db.call.counterpart_resource, counterpart.resourcepart); break;
case "ourpart":
update_builder.set(db.call.our_resource, ourpart.resourcepart); break;
case "direction":
update_builder.set(db.call.direction, direction); break;
case "time":
update_builder.set(db.call.time, (long) time.to_unix()); break;
case "local-time":
update_builder.set(db.call.local_time, (long) local_time.to_unix()); break;
case "end-time":
update_builder.set(db.call.end_time, (long) end_time.to_unix()); break;
case "encryption":
update_builder.set(db.call.encryption, encryption); break;
case "state":
// No point in persisting states that can't survive a restart
if (state == State.RINGING || state == State.ESTABLISHING || state == State.IN_PROGRESS) return;
update_builder.set(db.call.state, state);
break;
}
update_builder.perform();
}
}
}
dino-0.5.0/libdino/src/entity/conversation.vala 0000664 0000000 0000000 00000021166 14776241610 020265 0 ustar root root using Xmpp;
namespace Dino.Entities {
public class Conversation : Object {
public signal void object_updated(Conversation conversation);
public enum Type {
CHAT,
GROUPCHAT,
GROUPCHAT_PM;
public bool is_muc_semantic() {
return this == GROUPCHAT || this == GROUPCHAT_PM;
}
}
public int id { get; set; }
public Type type_ { get; set; }
public Account account { get; private set; }
public Jid counterpart { get; private set; }
public string? nickname { get; set; }
public bool active { get; set; default = false; }
public DateTime active_last_changed { get; private set; }
private DateTime? _last_active;
public DateTime? last_active {
get { return _last_active; }
set {
if (_last_active == null ||
(value != null && value.difference(_last_active) > 0)) {
_last_active = value;
}
}
}
public Encryption encryption { get; set; default = Encryption.UNKNOWN; }
public Message? read_up_to { get; set; }
public int read_up_to_item { get; set; default=-1; }
public enum NotifySetting { DEFAULT, ON, OFF, HIGHLIGHT }
public NotifySetting notify_setting { get; set; default = NotifySetting.DEFAULT; }
public enum Setting { DEFAULT, ON, OFF }
public Setting send_typing { get; set; default = Setting.DEFAULT; }
public Setting send_marker { get; set; default = Setting.DEFAULT; }
public int pinned { get; set; default = 0; }
private Database? db;
public Conversation(Jid jid, Account account, Type type) {
this.account = account;
this.counterpart = jid;
this.type_ = type;
}
public Conversation.from_row(Database db, Qlite.Row row) throws InvalidJidError {
this.db = db;
id = row[db.conversation.id];
type_ = (Conversation.Type) row[db.conversation.type_];
account = db.get_account_by_id(row[db.conversation.account_id]);
string? resource = row[db.conversation.resource];
counterpart = db.get_jid_by_id(row[db.conversation.jid_id]);
if (type_ == Conversation.Type.GROUPCHAT_PM) counterpart = counterpart.with_resource(resource);
nickname = type_ == Conversation.Type.GROUPCHAT ? resource : null;
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];
if (last_active != null) this.last_active = new DateTime.from_unix_utc(last_active);
encryption = (Encryption) row[db.conversation.encryption];
int? read_up_to = row[db.conversation.read_up_to];
if (read_up_to != null) this.read_up_to = db.get_message_by_id(read_up_to);
read_up_to_item = row[db.conversation.read_up_to_item];
notify_setting = (NotifySetting) row[db.conversation.notification];
send_typing = (Setting) row[db.conversation.send_typing];
send_marker = (Setting) row[db.conversation.send_marker];
pinned = row[db.conversation.pinned];
notify.connect(on_update);
}
public void persist(Database db) {
this.db = db;
this.active_last_changed = new DateTime.now_utc();
var insert = db.conversation.insert()
.value(db.conversation.account_id, account.id)
.value(db.conversation.jid_id, db.get_jid_id(counterpart))
.value(db.conversation.type_, type_)
.value(db.conversation.encryption, encryption)
.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.send_typing, send_typing)
.value(db.conversation.send_marker, send_marker)
.value(db.conversation.pinned, pinned);
if (read_up_to != null) {
insert.value(db.conversation.read_up_to, read_up_to.id);
}
if (read_up_to_item != -1) {
insert.value(db.conversation.read_up_to_item, read_up_to_item);
}
if (nickname != null) {
insert.value(db.conversation.resource, nickname);
}
if (counterpart.is_full()) {
insert.value(db.conversation.resource, counterpart.resourcepart);
}
if (last_active != null) {
insert.value(db.conversation.last_active, (long) last_active.to_unix());
}
id = (int) insert.perform();
notify.connect(on_update);
}
public NotifySetting get_notification_setting(StreamInteractor stream_interactor) {
return notify_setting != NotifySetting.DEFAULT ? notify_setting : get_notification_default_setting(stream_interactor);
}
public NotifySetting get_notification_default_setting(StreamInteractor stream_interactor) {
if (!Application.get_default().settings.notifications) return NotifySetting.OFF;
if (type_ == Type.GROUPCHAT) {
if (stream_interactor.get_module(MucManager.IDENTITY).is_private_room(this.account, this.counterpart)) {
return NotifySetting.ON;
} else {
return NotifySetting.HIGHLIGHT;
}
}
return NotifySetting.ON;
}
public Setting get_send_typing_setting(StreamInteractor stream_interactor) {
if (send_typing != Setting.DEFAULT) return send_typing;
if (stream_interactor.get_module(MucManager.IDENTITY).is_public_room(this.account, this.counterpart)) return Setting.OFF;
return Application.get_default().settings.send_typing ? Setting.ON : Setting.OFF;
}
public Setting get_send_marker_setting(StreamInteractor stream_interactor) {
if (send_marker != Setting.DEFAULT) return send_marker;
if (stream_interactor.get_module(MucManager.IDENTITY).is_public_room(this.account, this.counterpart)) return Setting.OFF;
return Application.get_default().settings.send_marker ? Setting.ON : Setting.OFF;
}
public bool equals(Conversation? conversation) {
if (conversation == null) return false;
return equals_func(this, conversation);
}
public static bool equals_func(Conversation conversation1, Conversation conversation2) {
return conversation1.counterpart.equals(conversation2.counterpart) && conversation1.account.equals(conversation2.account) && conversation1.type_ == conversation2.type_;
}
public static uint hash_func(Conversation conversation) {
return conversation.counterpart.to_string().hash() ^ conversation.account.bare_jid.to_string().hash();
}
private void on_update(Object o, ParamSpec sp) {
var update = db.conversation.update().with(db.conversation.id, "=", id);
switch (sp.name) {
case "type-":
update.set(db.conversation.type_, type_); break;
case "encryption":
update.set(db.conversation.encryption, encryption); break;
case "read-up-to":
if (read_up_to != null) {
update.set(db.conversation.read_up_to, read_up_to.id);
} else {
update.set_null(db.conversation.read_up_to);
}
break;
case "read-up-to-item":
if (read_up_to_item != -1) {
update.set(db.conversation.read_up_to_item, read_up_to_item);
} else {
update.set_null(db.conversation.read_up_to_item);
}
break;
case "nickname":
update.set(db.conversation.resource, nickname); break;
case "active":
update.set(db.conversation.active, active);
update.set(db.conversation.active_last_changed, (long) new DateTime.now_utc().to_unix());
break;
case "last-active":
if (last_active != null) {
update.set(db.conversation.last_active, (long) last_active.to_unix());
} else {
update.set_null(db.conversation.last_active);
}
break;
case "notify-setting":
update.set(db.conversation.notification, notify_setting); break;
case "send-typing":
update.set(db.conversation.send_typing, send_typing); break;
case "send-marker":
update.set(db.conversation.send_marker, send_marker); break;
case "pinned":
update.set(db.conversation.pinned, pinned); break;
}
update.perform();
}
}
}
dino-0.5.0/libdino/src/entity/encryption.vala 0000664 0000000 0000000 00000000350 14776241610 017735 0 ustar root root namespace Dino.Entities {
public enum Encryption {
NONE,
PGP,
OMEMO,
DTLS_SRTP,
SRTP,
UNKNOWN;
public bool is_some() {
return this != NONE;
}
}
} dino-0.5.0/libdino/src/entity/file_transfer.vala 0000664 0000000 0000000 00000031573 14776241610 020401 0 ustar root root using Xmpp;
namespace Dino.Entities {
public class FileTransfer : Object {
public signal void sources_changed();
public const bool DIRECTION_SENT = true;
public const bool DIRECTION_RECEIVED = false;
public enum State {
COMPLETE,
IN_PROGRESS,
NOT_STARTED,
FAILED
}
public int id { get; set; default=-1; }
public string? file_sharing_id { get; set; }
public Account account { get; set; }
public Jid counterpart { get; set; }
public Jid ourpart { get; set; }
public Jid? from {
get { return direction == DIRECTION_SENT ? ourpart : counterpart; }
}
public Jid? to {
get { return direction == DIRECTION_SENT ? counterpart : ourpart; }
}
public bool direction { get; set; }
public DateTime time { get; set; }
public DateTime? local_time { get; set; }
public Encryption encryption { get; set; default=Encryption.NONE; }
private InputStream? input_stream_ = null;
public InputStream input_stream {
get {
if (input_stream_ == null) {
File file = File.new_for_path(Path.build_filename(storage_dir, path ?? file_name));
try {
input_stream_ = file.read();
} catch (Error e) { }
}
return input_stream_;
}
set {
input_stream_ = value;
}
}
private string file_name_;
public string file_name {
get { return file_name_; }
set {
file_name_ = Path.get_basename(value);
if (file_name_ == Path.DIR_SEPARATOR_S || file_name_ == ".") {
file_name_ = "unknown filename";
} else if (file_name_.has_prefix(".")) {
file_name_ = "_" + file_name_;
}
}
}
private string? server_file_name_ = null;
public string server_file_name {
get { return server_file_name_ ?? file_name; }
set { server_file_name_ = value; }
}
public string path { get; set; }
public string? mime_type { get; set; }
public int64 size { get; set; }
public State state { get; set; default=State.NOT_STARTED; }
public int provider { 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 hashes = new Gee.ArrayList();
public Gee.List sfs_sources = new Gee.ArrayList(Xep.StatelessFileSharing.Source.equals_func);
public Gee.List thumbnails = new Gee.ArrayList();
private Database? db;
private string storage_dir;
public FileTransfer.from_row(Database db, Qlite.Row row, string storage_dir) throws InvalidJidError {
this.db = db;
this.storage_dir = storage_dir;
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 don’t have to generate acc new
counterpart = db.get_jid_by_id(row[db.file_transfer.counterpart_id]);
string counterpart_resource = row[db.file_transfer.counterpart_resource];
if (counterpart_resource != null) counterpart = counterpart.with_resource(counterpart_resource);
string our_resource = row[db.file_transfer.our_resource];
if (our_resource != null) {
ourpart = account.bare_jid.with_resource(our_resource);
} else {
ourpart = account.bare_jid;
}
direction = row[db.file_transfer.direction];
time = new DateTime.from_unix_utc(row[db.file_transfer.time]);
local_time = new DateTime.from_unix_utc(row[db.file_transfer.local_time]);
encryption = (Encryption) row[db.file_transfer.encryption];
file_name = row[db.file_transfer.file_name];
path = row[db.file_transfer.path];
mime_type = row[db.file_transfer.mime_type];
size = (int64) row[db.file_transfer.size];
state = (State) row[db.file_transfer.state];
provider = row[db.file_transfer.provider];
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.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);
}
public void persist(Database db) {
if (id != -1) return;
this.db = db;
Qlite.InsertBuilder builder = db.file_transfer.insert()
.value(db.file_transfer.account_id, account.id)
.value(db.file_transfer.counterpart_id, db.get_jid_id(counterpart))
.value(db.file_transfer.counterpart_resource, counterpart.resourcepart)
.value(db.file_transfer.our_resource, ourpart.resourcepart)
.value(db.file_transfer.direction, direction)
.value(db.file_transfer.time, (long) time.to_unix())
.value(db.file_transfer.local_time, (long) local_time.to_unix())
.value(db.file_transfer.encryption, encryption)
.value(db.file_transfer.file_name, file_name)
.value(db.file_transfer.size, (long) size)
.value(db.file_transfer.state, state)
.value(db.file_transfer.provider, provider)
.value(db.file_transfer.info, info);
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 (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();
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) {
db.file_thumbnails.insert()
.value(db.file_thumbnails.id, id)
.value(db.file_thumbnails.uri, thumbnail.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) {
add_sfs_source(source);
}
notify.connect(on_update);
}
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);
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();
}
sources_changed();
}
public File? get_file() {
if (path == null) return null;
return File.new_for_path(Path.build_filename(Dino.get_storage_dir(), "files", path));
}
private void on_update(Object o, ParamSpec sp) {
Qlite.UpdateBuilder update_builder = db.file_transfer.update().with(db.file_transfer.id, "=", id);
switch (sp.name) {
case "file-sharing-id":
update_builder.set(db.file_transfer.file_sharing_id, file_sharing_id); break;
case "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;
case "ourpart":
update_builder.set(db.file_transfer.our_resource, ourpart.resourcepart); break;
case "direction":
update_builder.set(db.file_transfer.direction, direction); break;
case "time":
update_builder.set(db.file_transfer.time, (long) time.to_unix()); break;
case "local-time":
update_builder.set(db.file_transfer.local_time, (long) local_time.to_unix()); break;
case "encryption":
update_builder.set(db.file_transfer.encryption, encryption); break;
case "file-name":
update_builder.set(db.file_transfer.file_name, file_name); break;
case "path":
update_builder.set(db.file_transfer.path, path); break;
case "mime-type":
update_builder.set(db.file_transfer.mime_type, mime_type); break;
case "size":
update_builder.set(db.file_transfer.size, (long) size); break;
case "state":
if (state == State.IN_PROGRESS) return;
update_builder.set(db.file_transfer.state, state); break;
case "provider":
update_builder.set(db.file_transfer.provider, provider); break;
case "info":
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();
}
}
}
dino-0.5.0/libdino/src/entity/message.vala 0000664 0000000 0000000 00000027551 14776241610 017203 0 ustar root root using Gee;
using Xmpp;
namespace Dino.Entities {
public class Message : Object {
public const bool DIRECTION_SENT = true;
public const bool DIRECTION_RECEIVED = false;
public enum Marked {
NONE,
RECEIVED,
READ,
ACKNOWLEDGED,
UNSENT,
WONTSEND,
SENDING,
SENT,
ERROR
}
public static Marked[] MARKED_RECEIVED = new Marked[] { Marked.READ, Marked.RECEIVED, Marked.ACKNOWLEDGED };
public enum Type {
ERROR,
CHAT,
GROUPCHAT,
GROUPCHAT_PM,
UNKNOWN;
public bool is_muc_semantic() {
return this == GROUPCHAT || this == GROUPCHAT_PM;
}
}
public int id { get; set; default = -1; }
public Account account { get; set; }
public Jid? counterpart { get; set; }
public Jid? ourpart { get; set; }
public Jid? from {
get { return direction == DIRECTION_SENT ? ourpart : counterpart; }
}
public Jid? to {
get { return direction == DIRECTION_SENT ? counterpart : ourpart; }
}
public bool direction { get; set; }
public Jid? real_jid { get; set; }
public Type type_ { get; set; default = Type.UNKNOWN; }
private string? body_;
public string? body {
get { return body_; }
set { body_ = value != null ? value.make_valid() : null; }
}
public string? stanza_id { get; set; }
public string? server_id { get; set; }
public DateTime? time { get; set; }
/** UTC **/
public DateTime? local_time { get; set; }
public Encryption encryption { get; set; default = Encryption.NONE; }
private Marked marked_ = Marked.NONE;
public Marked marked {
get { return marked_; }
set {
if (value == Marked.RECEIVED && marked == Marked.READ) return;
marked_ = value;
}
}
public string? edit_to = null;
public int quoted_item_id { get; private set; default=0; }
private Gee.List fallbacks = null;
private Gee.List markups = null;
private Database? db;
public Message(string? body) {
this.body = body;
}
public Message.from_row(Database db, Qlite.Row row) throws InvalidJidError {
this.db = db;
id = row[db.message.id];
account = db.get_account_by_id(row[db.message.account_id]);
stanza_id = row[db.message.stanza_id];
server_id = row[db.message.server_id];
type_ = (Message.Type) row[db.message.type_];
counterpart = db.get_jid_by_id(row[db.message.counterpart_id]);
string counterpart_resource = row[db.message.counterpart_resource];
if (counterpart_resource != null) counterpart = counterpart.with_resource(counterpart_resource);
string our_resource = row[db.message.our_resource];
if (type_ == Type.GROUPCHAT && our_resource != null) {
ourpart = counterpart.with_resource(our_resource);
} else if (our_resource != null) {
ourpart = account.bare_jid.with_resource(our_resource);
} else {
ourpart = account.bare_jid;
}
direction = row[db.message.direction];
time = new DateTime.from_unix_utc(row[db.message.time]);
local_time = new DateTime.from_unix_utc(row[db.message.local_time]);
body = row[db.message.body];
marked = (Message.Marked) row[db.message.marked];
encryption = (Encryption) row[db.message.encryption];
string? real_jid_str = row[db.real_jid.real_jid];
if (real_jid_str != null) real_jid = new Jid(real_jid_str);
edit_to = row[db.message_correction.to_stanza_id];
quoted_item_id = row[db.reply.quoted_content_item_id];
notify.connect(on_update);
}
public void persist(Database db) {
if (id != -1) return;
this.db = db;
Qlite.InsertBuilder builder = db.message.insert()
.value(db.message.account_id, account.id)
.value(db.message.counterpart_id, db.get_jid_id(counterpart))
.value(db.message.counterpart_resource, counterpart.resourcepart)
.value(db.message.our_resource, ourpart.resourcepart)
.value(db.message.direction, direction)
.value(db.message.type_, type_)
.value(db.message.time, (long) time.to_unix())
.value(db.message.local_time, (long) local_time.to_unix())
.value(db.message.body, body)
.value(db.message.encryption, encryption)
.value(db.message.marked, marked);
if (stanza_id != null) builder.value(db.message.stanza_id, stanza_id);
if (server_id != null) builder.value(db.message.server_id, server_id);
id = (int) builder.perform();
if (real_jid != null) {
db.real_jid.insert()
.value(db.real_jid.message_id, id)
.value(db.real_jid.real_jid, real_jid.to_string())
.perform();
}
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 get_fallbacks() {
if (fallbacks != null) return fallbacks;
fetch_body_meta();
return fallbacks;
}
public Gee.List get_markups() {
if (markups != null) return markups;
fetch_body_meta();
return markups;
}
public void persist_markups(Gee.List 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>();
var markups = new ArrayList();
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();
}
fallbacks_by_ns[ns_uri].add(new Xep.FallbackIndication.FallbackLocation(row[db.body_meta.from_char], row[db.body_meta.to_char]));
break;
case Xep.MessageMarkup.NS_URI:
var types = new ArrayList();
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();
foreach (string ns_uri in fallbacks_by_ns.keys) {
fallbacks.add(new Xep.FallbackIndication.Fallback(ns_uri, fallbacks_by_ns[ns_uri].to_array()));
}
this.fallbacks = fallbacks;
this.markups = markups;
}
public void set_fallbacks(Gee.List 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) {
switch (type) {
case Xmpp.MessageStanza.TYPE_CHAT:
type_ = Type.CHAT; break;
case Xmpp.MessageStanza.TYPE_GROUPCHAT:
type_ = Type.GROUPCHAT; break;
}
}
public new string get_type_string() {
switch (type_) {
case Type.CHAT:
return Xmpp.MessageStanza.TYPE_CHAT;
case Type.GROUPCHAT:
return Xmpp.MessageStanza.TYPE_GROUPCHAT;
default:
return Xmpp.MessageStanza.TYPE_NORMAL;
}
}
public bool equals(Message? m) {
if (m == null) return false;
return equals_func(this, m);
}
public static bool equals_func(Message m1, Message m2) {
if (m1.stanza_id == m2.stanza_id &&
m1.body == m2.body) {
return true;
}
return false;
}
public static uint hash_func(Message message) {
if (message.body == null) return 0;
return message.body.hash();
}
private void on_update(Object o, ParamSpec sp) {
Qlite.UpdateBuilder update_builder = db.message.update().with(db.message.id, "=", id);
switch (sp.name) {
case "stanza-id":
update_builder.set(db.message.stanza_id, stanza_id); break;
case "server-id":
update_builder.set(db.message.server_id, server_id); break;
case "counterpart":
update_builder.set(db.message.counterpart_id, db.get_jid_id(counterpart));
update_builder.set(db.message.counterpart_resource, counterpart.resourcepart); break;
case "ourpart":
update_builder.set(db.message.our_resource, ourpart.resourcepart); break;
case "direction":
update_builder.set(db.message.direction, direction); break;
case "type-":
update_builder.set(db.message.type_, type_); break;
case "time":
update_builder.set(db.message.time, (long) time.to_unix()); break;
case "local-time":
update_builder.set(db.message.local_time, (long) local_time.to_unix()); break;
case "body":
update_builder.set(db.message.body, body); break;
case "encryption":
update_builder.set(db.message.encryption, encryption); break;
case "marked":
update_builder.set(db.message.marked, marked); break;
}
update_builder.perform();
if (sp.get_name() == "real-jid") {
db.real_jid.upsert()
.value(db.real_jid.message_id, id, true)
.value(db.real_jid.real_jid, real_jid.to_string())
.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();
}
}
}
}
dino-0.5.0/libdino/src/entity/settings.vala 0000664 0000000 0000000 00000006477 14776241610 017423 0 ustar root root namespace Dino.Entities {
public class Settings : Object {
private Database db;
public Settings.from_db(Database db) {
this.db = db;
send_typing_ = col_to_bool_or_default("send_typing", true);
send_marker_ = col_to_bool_or_default("send_marker", true);
notifications_ = col_to_bool_or_default("notifications", true);
convert_utf8_smileys_ = col_to_bool_or_default("convert_utf8_smileys", true);
check_spelling = col_to_bool_or_default("check_spelling", true);
}
private bool col_to_bool_or_default(string key, bool def) {
string? val = db.settings.select({db.settings.value}).with(db.settings.key, "=", key)[db.settings.value];
return val != null ? bool.parse(val) : def;
}
private bool send_typing_;
public bool send_typing {
get { return send_typing_; }
set {
db.settings.upsert()
.value(db.settings.key, "send_typing", true)
.value(db.settings.value, value.to_string())
.perform();
send_typing_ = value;
}
}
private bool send_marker_;
public bool send_marker {
get { return send_marker_; }
set {
db.settings.upsert()
.value(db.settings.key, "send_marker", true)
.value(db.settings.value, value.to_string())
.perform();
send_marker_ = value;
}
}
private bool notifications_;
public bool notifications {
get { return notifications_; }
set {
db.settings.upsert()
.value(db.settings.key, "notifications", true)
.value(db.settings.value, value.to_string())
.perform();
notifications_ = value;
}
}
private bool convert_utf8_smileys_;
public bool convert_utf8_smileys {
get { return convert_utf8_smileys_; }
set {
db.settings.upsert()
.value(db.settings.key, "convert_utf8_smileys", true)
.value(db.settings.value, value.to_string())
.perform();
convert_utf8_smileys_ = value;
}
}
// There is currently no spell checking for GTK4, thus there is currently no UI for this setting.
private bool check_spelling_;
public bool check_spelling {
get { return check_spelling_; }
set {
db.settings.upsert()
.value(db.settings.key, "check_spelling", true)
.value(db.settings.value, value.to_string())
.perform();
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();
}
}
}
dino-0.5.0/libdino/src/plugin/ 0000775 0000000 0000000 00000000000 14776241610 014662 5 ustar root root dino-0.5.0/libdino/src/plugin/interfaces.vala 0000664 0000000 0000000 00000017000 14776241610 017650 0 ustar root root using Dino.Entities;
using Xmpp;
namespace Dino.Plugins {
public enum Priority {
LOWEST,
LOWER,
DEFAULT,
HIGHER,
HIGHEST
}
public enum WidgetType {
GTK3,
GTK4
}
public interface RootInterface : Object {
public abstract void registered(Dino.Application app);
public abstract void shutdown();
}
public interface EncryptionListEntry : Object {
public abstract Entities.Encryption encryption { get; }
public abstract string name { get; }
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 string? get_encryption_icon_name(Entities.Conversation conversation, ContentItem content_item);
}
public interface CallEncryptionEntry : Object {
public abstract CallEncryptionWidget? get_widget(Account account, Xmpp.Xep.Jingle.ContentEncryption encryption);
}
public interface CallEncryptionWidget : Object {
public abstract string? get_title();
public abstract bool show_keys();
public abstract string? get_icon_name();
}
public abstract class AccountSettingsEntry : Object {
public abstract string id { get; }
public virtual Priority priority { get { return Priority.DEFAULT; } }
public abstract string name { get; }
public virtual int16 label_top_padding { get { return -1; } }
public abstract signal void activated();
public abstract void deactivate();
public abstract void set_account(Account account);
public abstract Object? get_widget(WidgetType type);
}
public abstract class EncryptionPreferencesEntry : Object {
public abstract string id { get; }
public virtual Priority priority { get { return Priority.DEFAULT; } }
public abstract Object? get_widget(Account account, WidgetType type);
}
public interface ContactDetailsProvider : Object {
public abstract string id { get; }
public abstract string tab { get; }
public abstract void populate(Conversation conversation, ContactDetails contact_details, WidgetType type);
public abstract Object? get_widget(Conversation conversation);
}
public class ContactDetails : Object {
public signal void save();
public signal void add(string category, string label, string? desc, Object widget);
}
public interface TextCommand : Object {
public abstract string cmd { get; }
public abstract string? handle_command(string? text, Entities.Conversation? conversation);
}
public interface ConversationTitlebarEntry : Object {
public abstract string id { get; }
public abstract double order { get; }
public abstract Object? get_widget(WidgetType type);
public abstract void set_conversation(Conversation conversation);
public abstract void unset_conversation();
}
public abstract interface ConversationItemPopulator : Object {
public abstract string id { get; }
public abstract void init(Conversation conversation, ConversationItemCollection summary, WidgetType type);
public abstract void close(Conversation conversation);
}
public abstract interface ConversationAdditionPopulator : ConversationItemPopulator {
public virtual void populate_timespan(Conversation conversation, DateTime from, DateTime to) { }
}
public abstract interface VideoCallPlugin : Object {
public abstract bool supported();
// Video widget
public abstract VideoCallWidget? create_widget(WidgetType type);
// Devices
public signal void devices_changed(string media, bool incoming);
public abstract Gee.List get_devices(string media, bool incoming);
public abstract MediaDevice? get_preferred_device(string media, bool incoming);
public abstract MediaDevice? get_device(Xmpp.Xep.JingleRtp.Stream? stream, bool incoming);
public abstract void set_pause(Xmpp.Xep.JingleRtp.Stream? stream, bool pause);
public abstract void set_device(Xmpp.Xep.JingleRtp.Stream? stream, MediaDevice? device);
public abstract void dump_dot();
}
public abstract interface VideoCallWidget : Object {
public signal void resolution_changed(uint width, uint height);
public abstract void display_stream(Xmpp.Xep.JingleRtp.Stream? stream, Jid jid);
public abstract void display_device(MediaDevice device);
public abstract void detach();
}
public abstract interface MediaDevice : Object {
public abstract string id { owned get; }
public abstract string display_name { owned get; }
public abstract string? detail_name { owned get; }
public abstract string? media { owned get; }
public abstract bool incoming { get; }
}
public abstract interface NotificationPopulator : Object {
public abstract string id { get; }
public abstract void init(Conversation conversation, NotificationCollection summary, WidgetType type);
public abstract void close(Conversation conversation);
}
public abstract class MetaConversationItem : Object {
public virtual string populator_id { get; set; }
public virtual Jid? jid { get; set; default=null; }
public virtual DateTime time { get; set; default = new DateTime.now_utc(); }
public virtual int secondary_sort_indicator { get; set; }
public virtual Encryption encryption { get; set; default = Encryption.NONE; }
public virtual Entities.Message.Marked mark { get; set; default = Entities.Message.Marked.NONE; }
public bool can_merge { get; set; default=false; }
public bool requires_avatar { get; set; default=false; }
public bool requires_header { get; set; default=false; }
public bool in_edit_mode { get; set; default=false; }
public abstract Object? get_widget(ConversationItemWidgetInterface outer, WidgetType type);
public abstract Gee.List? get_item_actions(WidgetType type);
}
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 string name;
public bool sensitive = true;
public string icon_name;
public string? tooltip;
public Object? popover;
public MessageActionEvoked? callback;
}
public abstract class MetaConversationNotification : Object {
public abstract Object? get_widget(WidgetType type);
}
public interface ConversationItemCollection : Object {
public signal void inserted_item(MetaConversationItem item);
public signal void removed_item(MetaConversationItem item);
public abstract void insert_item(MetaConversationItem item);
public abstract void remove_item(MetaConversationItem item);
}
public interface NotificationCollection : Object {
public signal void add_meta_notification(MetaConversationNotification item);
public signal void remove_meta_notification(MetaConversationNotification item);
}
public delegate void SetInputFieldStatus(InputFieldStatus field_status);
public class InputFieldStatus : Object {
public enum MessageType {
NONE,
INFO,
WARNING,
ERROR
}
public enum InputState {
NORMAL,
DISABLED,
NO_SEND
}
public string? message;
public MessageType message_type;
public InputState input_state;
public bool contains_markup;
public InputFieldStatus(string? message, MessageType message_type, InputState input_state, bool contains_markup = false) {
this.message = message;
this.message_type = message_type;
this.input_state = input_state;
this.contains_markup = contains_markup;
}
}
}
dino-0.5.0/libdino/src/plugin/loader.vala 0000664 0000000 0000000 00000005322 14776241610 016777 0 ustar root root using Gee;
namespace Dino.Plugins {
private class Info : Object {
public Module module;
public Type gtype;
public Info(Type type, owned Module module) {
this.module = (owned) module;
this.gtype = type;
}
}
public class Loader : Object {
[CCode (has_target = false)]
private delegate Type RegisterPluginFunction(Module module);
private Application app;
private string[] search_paths;
private RootInterface[] plugins = new RootInterface[0];
private Info[] infos = new Info[0];
public Loader(Application app) {
this.app = app;
this.search_paths = app.search_path_generator.get_plugin_paths();
}
public void load_all() throws Error {
if (Module.supported() == false) {
throw new Error(-1, 0, "Plugins are not supported");
}
HashSet plugin_names = new HashSet();
foreach (string path in search_paths) {
try {
Dir dir = Dir.open(path, 0);
string? file = null;
while ((file = dir.read_name()) != null) {
if (file.has_suffix(Module.SUFFIX)) plugin_names.add(file);
}
} catch (Error e) {
// Ignore this folder
}
}
foreach (string plugin in plugin_names) {
load(plugin);
}
}
public RootInterface load(string name) throws Error {
if (Module.supported() == false) {
throw new Error(-1, 0, "Plugins are not supported");
}
Module module = null;
string path = "";
foreach (string prefix in search_paths) {
path = Path.build_filename(prefix, name);
module = Module.open(path, ModuleFlags.BIND_LAZY);
if (module != null) break;
}
if (module == null) {
throw new Error(-1, 1, "%s", Module.error().replace(path, name));
}
void* function;
module.symbol("register_plugin", out function);
if (function == null) {
throw new Error(-1, 2, "register_plugin () not found");
}
RegisterPluginFunction register_plugin = (RegisterPluginFunction) function;
Type type = register_plugin(module);
if (type.is_a(typeof(RootInterface)) == false) {
throw new Error(-1, 3, "Unexpected type");
}
Info info = new Plugins.Info(type, (owned) module);
infos += info;
RootInterface plugin = (RootInterface) Object.new (type);
plugins += plugin;
plugin.registered(app);
return plugin;
}
public void shutdown() {
foreach (RootInterface p in plugins) {
p.shutdown();
}
}
}
}
dino-0.5.0/libdino/src/plugin/registry.vala 0000664 0000000 0000000 00000010512 14776241610 017376 0 ustar root root using Gee;
namespace Dino.Plugins {
public class Registry {
public HashMap encryption_list_entries = new HashMap();
public HashMap call_encryption_entries = new HashMap();
public ArrayList account_settings_entries = new ArrayList();
public ArrayList encryption_preferences_entries = new ArrayList();
public ArrayList contact_details_entries = new ArrayList();
public Map text_commands = new HashMap();
public Gee.List conversation_addition_populators = new ArrayList();
public Gee.List notification_populators = new ArrayList();
public Gee.Collection conversation_titlebar_entries = new Gee.TreeSet((a, b) => {
return (int)(a.order - b.order);
});
public VideoCallPlugin? video_call_plugin;
public bool register_encryption_list_entry(EncryptionListEntry entry) {
lock(encryption_list_entries) {
if (encryption_list_entries.has_key(entry.encryption)) return false;
encryption_list_entries[entry.encryption] = entry;
return true;
}
}
public bool register_call_entryption_entry(string ns, CallEncryptionEntry entry) {
lock (call_encryption_entries) {
call_encryption_entries[ns] = entry;
}
return true;
}
public bool register_account_settings_entry(AccountSettingsEntry entry) {
lock(account_settings_entries) {
foreach(var e in account_settings_entries) {
if (e.id == entry.id) return false;
}
account_settings_entries.add(entry);
// TODO: Order by priority
account_settings_entries.sort((a,b) => b.name.collate(a.name));
return true;
}
}
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) {
lock(contact_details_entries) {
foreach(ContactDetailsProvider e in contact_details_entries) {
if (e.id == entry.id) return false;
}
contact_details_entries.add(entry);
return true;
}
}
public bool register_text_command(TextCommand cmd) {
lock(text_commands) {
if (text_commands.has_key(cmd.cmd)) return false;
text_commands[cmd.cmd] = cmd;
return true;
}
}
public bool register_contact_titlebar_entry(ConversationTitlebarEntry entry) {
lock(conversation_titlebar_entries) {
foreach(ConversationTitlebarEntry e in conversation_titlebar_entries) {
if (e.id == entry.id) return false;
}
conversation_titlebar_entries.add(entry);
return true;
}
}
public bool register_conversation_addition_populator(ConversationAdditionPopulator populator) {
lock (conversation_addition_populators) {
foreach(ConversationItemPopulator p in conversation_addition_populators) {
if (p.id == populator.id) return false;
}
conversation_addition_populators.add(populator);
return true;
}
}
public bool register_notification_populator(NotificationPopulator populator) {
lock (notification_populators) {
foreach(NotificationPopulator p in notification_populators) {
if (p.id == populator.id) return false;
}
notification_populators.add(populator);
return true;
}
}
}
}
dino-0.5.0/libdino/src/service/ 0000775 0000000 0000000 00000000000 14776241610 015024 5 ustar root root dino-0.5.0/libdino/src/service/avatar_manager.vala 0000664 0000000 0000000 00000032661 14776241610 020651 0 ustar root root using Gdk;
using Gee;
using Qlite;
using Xmpp;
using Dino.Entities;
namespace Dino {
public class AvatarManager : StreamInteractionModule, Object {
public static ModuleIdentity IDENTITY = new ModuleIdentity("avatar_manager");
public string id { get { return IDENTITY.id; } }
public signal void received_avatar(Jid jid, Account account);
public signal void fetched_avatar(Jid jid, Account account);
private enum Source {
USER_AVATARS,
VCARD
}
private StreamInteractor stream_interactor;
private Database db;
private string folder = null;
private HashMap user_avatars = new HashMap(Jid.hash_func, Jid.equals_func);
private HashMap vcard_avatars = new HashMap(Jid.hash_func, Jid.equals_func);
private HashMap cached_pixbuf = new HashMap();
private HashMap> pending_pixbuf = new HashMap>();
private HashSet pending_fetch = new HashSet();
private const int MAX_PIXEL = 192;
public static void start(StreamInteractor stream_interactor, Database db) {
AvatarManager m = new AvatarManager(stream_interactor, db);
stream_interactor.add_module(m);
}
private AvatarManager(StreamInteractor stream_interactor, Database db) {
this.stream_interactor = stream_interactor;
this.db = db;
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.module_manager.initialize_account_modules.connect((_, modules) => {
modules.add(new Xep.UserAvatars.Module());
modules.add(new Xep.VCard.Module());
});
}
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_) {
Jid jid = jid_;
if (!stream_interactor.get_module(MucManager.IDENTITY).is_groupchat_occupant(jid_, account)) {
jid = jid_.bare_jid;
}
if (user_avatars.has_key(jid)) {
return user_avatars[jid];
} else if (vcard_avatars.has_key(jid)) {
return vcard_avatars[jid];
} else {
return null;
}
}
[Version (deprecated = true)]
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) {
return get_avatar_hash(account, jid) != null;
}
[Version (deprecated = true)]
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;
}
[Version (deprecated = true)]
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();
Pixbuf? image = yield get_image(hash);
if (image != null) {
cached_pixbuf[hash] = image;
} else {
if (yield fetch_and_store(stream, account, jid, source, hash)) {
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) {
try {
Pixbuf pixbuf = new Pixbuf.from_file(file);
if (pixbuf.width >= pixbuf.height && pixbuf.width > MAX_PIXEL) {
int dest_height = (int) ((float) MAX_PIXEL / pixbuf.width * pixbuf.height);
pixbuf = pixbuf.scale_simple(MAX_PIXEL, dest_height, InterpType.BILINEAR);
} else if (pixbuf.height > pixbuf.width && pixbuf.width > MAX_PIXEL) {
int dest_width = (int) ((float) MAX_PIXEL / pixbuf.height * pixbuf.width);
pixbuf = pixbuf.scale_simple(dest_width, MAX_PIXEL, InterpType.BILINEAR);
}
uint8[] buffer;
pixbuf.save_to_buffer(out buffer, "png");
XmppStream stream = stream_interactor.get_stream(account);
if (stream != null) {
Xmpp.Xep.UserAvatars.publish_png(stream, buffer, pixbuf.width, pixbuf.height);
}
} catch (Error e) {
warning(e.message);
}
}
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) {
stream_interactor.module_manager.get_module(account, Xep.UserAvatars.Module.IDENTITY).received_avatar_hash.connect((stream, 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) =>
on_vcard_avatar_received(account, jid, id)
);
foreach (var entry in get_avatar_hashes(account, Source.USER_AVATARS).entries) {
on_user_avatar_received(account, entry.key, entry.value);
}
foreach (var entry in get_avatar_hashes(account, Source.VCARD).entries) {
on_vcard_avatar_received(account, entry.key, entry.value);
}
}
private void on_user_avatar_received(Account account, Jid jid_, string id) {
Jid jid = jid_.bare_jid;
if (!user_avatars.has_key(jid) || user_avatars[jid] != id) {
user_avatars[jid] = id;
set_avatar_hash(account, jid, id, Source.USER_AVATARS);
}
received_avatar(jid, account);
}
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);
Jid jid = is_gc ? jid_ : jid_.bare_jid;
if (!vcard_avatars.has_key(jid) || vcard_avatars[jid] != id) {
vcard_avatars[jid] = id;
if (jid.is_bare()) { // don't save MUC occupant avatars
set_avatar_hash(account, jid, id, Source.VCARD);
}
}
received_avatar(jid, account);
}
public void set_avatar_hash(Account account, Jid jid, string hash, int type) {
db.avatar.insert()
.value(db.avatar.jid_id, db.get_jid_id(jid))
.value(db.avatar.account_id, account.id)
.value(db.avatar.hash, hash)
.value(db.avatar.type_, type)
.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 get_avatar_hashes(Account account, int type) {
HashMap ret = new HashMap(Jid.hash_func, Jid.equals_func);
foreach (Row row in db.avatar.select({db.avatar.jid_id, db.avatar.hash})
.with(db.avatar.type_, "=", type)
.with(db.avatar.account_id, "=", account.id)) {
ret[db.get_jid_by_id(row[db.avatar.jid_id])] = row[db.avatar.hash];
}
return ret;
}
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));
try {
if (file.query_exists()) file.delete(); //TODO y?
DataOutputStream fos = new DataOutputStream(file.create(FileCreateFlags.REPLACE_DESTINATION));
yield fos.write_bytes_async(data);
} catch (Error e) {
// Ignore: we failed in storing, so we refuse to display later...
}
}
public bool has_image(string id) {
File file = File.new_for_path(Path.build_filename(folder, id));
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;
}
}
}
}
dino-0.5.0/libdino/src/service/blocking_manager.vala 0000664 0000000 0000000 00000003122 14776241610 021151 0 ustar root root using Gee;
using Xmpp;
using Dino.Entities;
namespace Dino {
public class BlockingManager : StreamInteractionModule, Object {
public static ModuleIdentity IDENTITY = new ModuleIdentity("blocking_manager");
public string id { get { return IDENTITY.id; } }
private StreamInteractor stream_interactor;
public static void start(StreamInteractor stream_interactor) {
BlockingManager m = new BlockingManager(stream_interactor);
stream_interactor.add_module(m);
}
private BlockingManager(StreamInteractor stream_interactor) {
this.stream_interactor = stream_interactor;
}
public bool is_blocked(Account account, Jid jid) {
XmppStream stream = stream_interactor.get_stream(account);
return stream != null && stream.get_module(Xmpp.Xep.BlockingCommand.Module.IDENTITY).is_blocked(stream, jid.to_string());
}
public void block(Account account, Jid jid) {
XmppStream stream = stream_interactor.get_stream(account);
stream.get_module(Xmpp.Xep.BlockingCommand.Module.IDENTITY).block(stream, { jid.to_string() });
}
public void unblock(Account account, Jid jid) {
XmppStream stream = stream_interactor.get_stream(account);
stream.get_module(Xmpp.Xep.BlockingCommand.Module.IDENTITY).unblock(stream, { jid.to_string() });
}
public bool is_supported(Account account) {
XmppStream stream = stream_interactor.get_stream(account);
return stream != null && stream.get_module(Xmpp.Xep.BlockingCommand.Module.IDENTITY).is_supported(stream);
}
}
}
dino-0.5.0/libdino/src/service/call_peer_state.vala 0000664 0000000 0000000 00000051454 14776241610 021030 0 ustar root root using Dino.Entities;
using Gee;
using Xmpp;
public class Dino.PeerState : Object {
public signal void stream_created(string media);
public signal void counterpart_sends_video_updated(bool mute);
public signal void info_received(Xep.JingleRtp.CallSessionInfo session_info);
public signal void connection_ready();
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);
public StreamInteractor stream_interactor;
public CallState call_state;
public Calls calls;
public Call call;
public Jid jid;
public Xep.Jingle.Session session;
public string sid;
public string internal_id = Xmpp.random_uuid();
public Xep.JingleRtp.Parameters? audio_content_parameter = null;
public Xep.JingleRtp.Parameters? video_content_parameter = null;
public Xep.Jingle.Content? audio_content = null;
public Xep.Jingle.Content? video_content = null;
public Xep.Jingle.ContentEncryption? video_encryption = null;
public Xep.Jingle.ContentEncryption? audio_encryption = null;
public bool encryption_keys_same = false;
public HashMap? video_encryptions = null;
public HashMap? audio_encryptions = null;
public bool first_peer = false;
public bool waiting_for_inbound_muji_connection = false;
public Xep.Muji.GroupCall? group_call { get; set; }
public bool counterpart_sends_video = false;
public bool we_should_send_audio { get; set; default=false; }
public bool we_should_send_video { get; set; default=false; }
public PeerState(Jid jid, Call call, CallState call_state, StreamInteractor stream_interactor) {
this.jid = jid;
this.call = call;
this.call_state = call_state;
this.stream_interactor = stream_interactor;
this.calls = stream_interactor.get_module(Calls.IDENTITY);
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) => {
if (this.sid != session.sid) return;
foreach (Xep.Jingle.Content content in session.contents) {
if (name == null || content.content_name == name) {
Xep.JingleRtp.Parameters? rtp_content_parameter = content.content_params as Xep.JingleRtp.Parameters;
if (rtp_content_parameter != null) {
on_counterpart_mute_update(mute, rtp_content_parameter.media);
}
}
}
});
session_info_type.info_received.connect((session, session_info) => {
if (this.sid != session.sid) return;
info_received(session_info);
});
}
public async void initiate_call(Jid counterpart) {
Gee.List call_resources = yield calls.get_call_resources(call.account, counterpart);
bool do_jmi = false;
Jid? jid_for_direct = null;
if (yield calls.contains_jmi_resources(call.account, call_resources)) {
do_jmi = true;
} else if (!call_resources.is_empty) {
jid_for_direct = call_resources[0];
} else if (calls.has_jmi_resources(jid)) {
do_jmi = true;
}
sid = Xmpp.random_uuid();
if (do_jmi) {
XmppStream? stream = stream_interactor.get_stream(call.account);
var descriptions = new ArrayList();
descriptions.add(new StanzaNode.build("description", Xep.JingleRtp.NS_URI).add_self_xmlns().put_attribute("media", "audio"));
if (we_should_send_video) {
descriptions.add(new StanzaNode.build("description", Xep.JingleRtp.NS_URI).add_self_xmlns().put_attribute("media", "video"));
}
stream.get_module(Xmpp.Xep.JingleMessageInitiation.Module.IDENTITY).send_session_propose_to_peer(stream, jid, sid, descriptions);
// Uncomment this use CIM instead of JMI
// call_state.cim_call_id = sid;
// stream.get_module(Xmpp.Xep.CallInvites.Module.IDENTITY).send_jingle_propose(stream, call_state.cim_call_id, jid, sid, we_should_send_video);
} else if (jid_for_direct != null) {
yield call_resource(jid_for_direct);
}
}
public async void call_resource(Jid full_jid) {
if (!call_state.accepted) {
warning("Tried to call resource in an unaccepted call?!");
return;
}
XmppStream? stream = stream_interactor.get_stream(call.account);
if (stream == null) return;
if (sid == null) sid = Xmpp.random_uuid();
Xep.Jingle.Session session = yield stream.get_module(Xep.JingleRtp.Module.IDENTITY).start_call(stream, full_jid, we_should_send_video, sid, group_call != null ? group_call.muc_jid : null);
set_session(session);
}
public void accept() {
if (!call_state.accepted) {
critical("Tried to accept peer in unaccepted call?! Something's fishy. Abort.");
return;
}
if (session != null) {
foreach (Xep.Jingle.Content content in session.contents) {
Xep.JingleRtp.Parameters? rtp_content_parameter = content.content_params as Xep.JingleRtp.Parameters;
if (rtp_content_parameter != null && rtp_content_parameter.media == "video") {
// We didn't accept video but our peer wants to negotiate that content
if (!we_should_send_video && session.senders_include_us(content.senders)) {
if (session.senders_include_counterpart(content.senders)) {
// If our peer wants to send, let them
content.modify(session.we_initiated ? Xep.Jingle.Senders.RESPONDER : Xep.Jingle.Senders.INITIATOR);
} else {
// If only we're supposed to send, reject
content.reject();
continue;
}
}
}
content.accept();
}
} else {
// Only a JMI so far
XmppStream stream = stream_interactor.get_stream(call.account);
if (stream == null) return;
stream.get_module(Xep.JingleMessageInitiation.Module.IDENTITY).send_session_accept_to_self(stream, sid);
stream.get_module(Xep.JingleMessageInitiation.Module.IDENTITY).send_session_proceed_to_peer(stream, jid, sid);
}
}
public void reject() {
if (session != null) {
foreach (Xep.Jingle.Content content in session.contents) {
content.reject();
}
} else {
// Only a JMI so far
XmppStream stream = stream_interactor.get_stream(call.account);
if (stream == null) return;
stream.get_module(Xep.JingleMessageInitiation.Module.IDENTITY).send_session_reject_to_peer(stream, jid, sid);
stream.get_module(Xep.JingleMessageInitiation.Module.IDENTITY).send_session_reject_to_self(stream, sid);
}
}
public void end(string terminate_reason, string? reason_text = null) {
switch (terminate_reason) {
case Xep.Jingle.ReasonElement.SUCCESS:
if (session != null) {
session.terminate(terminate_reason, reason_text, "success");
}
break;
case Xep.Jingle.ReasonElement.CANCEL:
if (session != null) {
session.terminate(terminate_reason, reason_text, "cancel");
} else if (group_call != null) {
// We don't have to do anything (?)
} else {
// Only a JMI so far
XmppStream? stream = stream_interactor.get_stream(call.account);
if (stream == null) return;
stream.get_module(Xep.JingleMessageInitiation.Module.IDENTITY).send_session_retract_to_peer(stream, jid, sid);
}
break;
}
}
internal void mute_own_audio(bool mute) {
// Call isn't fully established yet. Audio will be muted once the stream is created.
if (session == null || audio_content_parameter == null || audio_content_parameter.stream == null) return;
Xep.JingleRtp.Stream stream = audio_content_parameter.stream;
// Inform our counterpart that we (un)muted our audio
stream_interactor.module_manager.get_module(call.account, Xep.JingleRtp.Module.IDENTITY).session_info_type.send_mute(session, mute, "audio");
// Start/Stop sending audio data
Application.get_default().plugin_registry.video_call_plugin.set_pause(stream, mute);
}
internal void mute_own_video(bool mute) {
if (session == null) {
// Call hasn't been established yet
return;
}
Xep.JingleRtp.Module rtp_module = stream_interactor.module_manager.get_module(call.account, Xep.JingleRtp.Module.IDENTITY);
if (video_content_parameter != null &&
video_content_parameter.stream != null &&
session.senders_include_us(video_content.senders)) {
// A video content already exists
// Start/Stop sending video data
Xep.JingleRtp.Stream stream = video_content_parameter.stream;
if (stream != null) {
Application.get_default().plugin_registry.video_call_plugin.set_pause(stream, mute);
}
// Inform our counterpart that we started/stopped our video
rtp_module.session_info_type.send_mute(session, mute, "video");
} else if (!mute) {
// Add a new video content
XmppStream stream = stream_interactor.get_stream(call.account);
rtp_module.add_outgoing_video_content.begin(stream, session, group_call != null ? group_call.muc_jid : null, (_, res) => {
if (video_content_parameter == null) {
Xep.Jingle.Content content = rtp_module.add_outgoing_video_content.end(res);
Xep.JingleRtp.Parameters? rtp_content_parameter = content.content_params as Xep.JingleRtp.Parameters;
if (rtp_content_parameter != null) {
connect_content_signals(content, rtp_content_parameter);
}
}
});
}
// If video_content_parameter == null && !mute we're trying to mute a non-existant feed. It will be muted as soon as it is created.
}
public Xep.JingleRtp.Stream? get_video_stream() {
if (video_content_parameter != null) {
return video_content_parameter.stream;
}
return null;
}
public Xep.JingleRtp.Stream? get_audio_stream() {
if (audio_content_parameter != null) {
return audio_content_parameter.stream;
}
return null;
}
internal void set_session(Xep.Jingle.Session session) {
this.session = session;
this.sid = session.sid;
session.terminated.connect((stream, we_terminated, reason_name, reason_text) =>
session_terminated(we_terminated, reason_name, reason_text)
);
session.additional_content_add_incoming.connect((stream, content) =>
on_incoming_content_add(stream, content.session, content)
);
foreach (Xep.Jingle.Content content in session.contents) {
Xep.JingleRtp.Parameters? rtp_content_parameter = content.content_params as Xep.JingleRtp.Parameters;
if (rtp_content_parameter == null) continue;
connect_content_signals(content, rtp_content_parameter);
}
}
public PeerInfo get_info() {
var ret = new PeerInfo();
if (audio_content != null || audio_content_parameter != null) {
ret.audio = get_content_info(audio_content, audio_content_parameter);
}
if (video_content != null || video_content_parameter != null) {
ret.video = get_content_info(video_content, video_content_parameter);
}
return ret;
}
private PeerContentInfo get_content_info(Xep.Jingle.Content? content, Xep.JingleRtp.Parameters? parameter) {
PeerContentInfo ret = new PeerContentInfo();
if (parameter != null) {
ret.rtcp_ready = parameter.rtcp_ready;
ret.rtp_ready = parameter.rtp_ready;
if (parameter.agreed_payload_type != null) {
ret.codec = parameter.agreed_payload_type.name;
ret.clockrate = parameter.agreed_payload_type.clockrate;
}
if (parameter.stream != null && parameter.stream.remb_enabled) {
ret.target_receive_bytes = parameter.stream.target_receive_bitrate;
ret.target_send_bytes = parameter.stream.target_send_bitrate;
}
}
if (content != null) {
Xmpp.Xep.Jingle.ComponentConnection? component0 = content.get_transport_connection(1);
if (component0 != null) {
ret.bytes_received = component0.bytes_received;
ret.bytes_sent = component0.bytes_sent;
}
}
return ret;
}
private void connect_content_signals(Xep.Jingle.Content content, Xep.JingleRtp.Parameters rtp_content_parameter) {
if (rtp_content_parameter.media == "audio") {
audio_content = content;
audio_content_parameter = rtp_content_parameter;
} else if (rtp_content_parameter.media == "video") {
video_content = content;
video_content_parameter = rtp_content_parameter;
}
debug(@"[%s] %s connecting content signals %s", call.account.bare_jid.to_string(), jid.to_string(), rtp_content_parameter.media);
rtp_content_parameter.stream_created.connect((stream) => on_stream_created(rtp_content_parameter.media, stream));
rtp_content_parameter.connection_ready.connect((status) => {
Idle.add(() => {
on_connection_ready(content, rtp_content_parameter.media);
return false;
});
});
content.senders_modify_incoming.connect((content, proposed_senders) => {
if (content.session.senders_include_us(content.senders) != content.session.senders_include_us(proposed_senders)) {
warning("counterpart set us to (not)sending %s. ignoring", content.content_name);
return;
}
if (!content.session.senders_include_counterpart(content.senders) && content.session.senders_include_counterpart(proposed_senders)) {
// Counterpart wants to start sending. Ok.
content.accept_content_modify(proposed_senders);
on_counterpart_mute_update(false, "video");
}
});
}
private void on_incoming_content_add(XmppStream stream, Xep.Jingle.Session session, Xep.Jingle.Content content) {
Xep.JingleRtp.Parameters? rtp_content_parameter = content.content_params as Xep.JingleRtp.Parameters;
if (rtp_content_parameter == null) {
content.reject();
return;
}
// Our peer shouldn't tell us to start sending, that's for us to initiate
if (session.senders_include_us(content.senders)) {
if (session.senders_include_counterpart(content.senders)) {
// If our peer wants to send, let them
content.modify(session.we_initiated ? Xep.Jingle.Senders.RESPONDER : Xep.Jingle.Senders.INITIATOR);
} else {
// If only we're supposed to send, reject
content.reject();
}
}
connect_content_signals(content, rtp_content_parameter);
content.accept();
}
private void on_stream_created(string media, Xep.JingleRtp.Stream stream) {
if (media == "video" && stream.receiving) {
counterpart_sends_video = true;
video_content_parameter.connection_ready.connect((status) => {
Idle.add(() => {
counterpart_sends_video_updated(false);
return false;
});
});
}
// Outgoing audio/video might have been muted in the meanwhile.
if (media == "video" && !we_should_send_video) {
mute_own_video(true);
} else if (media == "audio" && !we_should_send_audio) {
mute_own_audio(true);
}
stream_created(media);
}
private void on_counterpart_mute_update(bool mute, string? media) {
if (!call.equals(call)) return;
if (media == "video") {
counterpart_sends_video = !mute;
debug(@"[%s] %s video muted %s", call.account.bare_jid.to_string(), jid.to_string(), mute.to_string());
counterpart_sends_video_updated(mute);
}
}
private void on_connection_ready(Xep.Jingle.Content content, string media) {
debug("[%s] %s on_connection_ready", call.account.bare_jid.to_string(), jid.to_string());
connection_ready();
if (call.state == Call.State.RINGING || call.state == Call.State.ESTABLISHING) {
call.state = Call.State.IN_PROGRESS;
}
if (media == "audio") {
audio_encryptions = content.encryptions;
} else if (media == "video") {
video_encryptions = content.encryptions;
}
if ((audio_encryptions != null && audio_encryptions.is_empty) || (video_encryptions != null && video_encryptions.is_empty)) {
call.encryption = Encryption.NONE;
encryption_updated(null, null);
return;
}
HashMap encryptions = audio_encryptions ?? video_encryptions;
Xep.Jingle.ContentEncryption? omemo_encryption = null, dtls_encryption = null, srtp_encryption = null;
foreach (string encr_name in encryptions.keys) {
if (video_encryptions != null && !video_encryptions.has_key(encr_name)) continue;
var encryption = encryptions[encr_name];
if (encryption.encryption_ns == "http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification") {
omemo_encryption = encryption;
} else if (encryption.encryption_ns == Xep.JingleIceUdp.DTLS_NS_URI) {
dtls_encryption = encryption;
} else if (encryption.encryption_name == "SRTP") {
srtp_encryption = encryption;
}
}
if (omemo_encryption != null && dtls_encryption != null) {
call.encryption = Encryption.OMEMO;
omemo_encryption.peer_key = dtls_encryption.peer_key;
omemo_encryption.our_key = dtls_encryption.our_key;
audio_encryption = omemo_encryption;
encryption_keys_same = true;
video_encryption = video_encryptions != null ? video_encryptions["http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification"] : null;
} else if (dtls_encryption != null) {
call.encryption = Encryption.DTLS_SRTP;
audio_encryption = dtls_encryption;
video_encryption = video_encryptions != null ? video_encryptions[Xep.JingleIceUdp.DTLS_NS_URI] : null;
encryption_keys_same = true;
if (video_encryption != null && dtls_encryption.peer_key.length == video_encryption.peer_key.length) {
for (int i = 0; i < dtls_encryption.peer_key.length; i++) {
if (dtls_encryption.peer_key[i] != video_encryption.peer_key[i]) {
encryption_keys_same = false;
break;
}
}
}
} else if (srtp_encryption != null) {
call.encryption = Encryption.SRTP;
audio_encryption = srtp_encryption;
video_encryption = video_encryptions != null ? video_encryptions["SRTP"] : null;
encryption_keys_same = false;
} else {
call.encryption = Encryption.NONE;
encryption_keys_same = true;
}
encryption_updated(audio_encryption, video_encryption);
}
}
public class Dino.PeerContentInfo {
public bool rtp_ready { get; set; }
public bool rtcp_ready { get; set; }
public ulong? bytes_sent { get; set; default=0; }
public ulong? bytes_received { get; set; default=0; }
public string? codec { get; set; }
public uint32 clockrate { get; set; }
public uint target_receive_bytes { get; set; default=-1; }
public uint target_send_bytes { get; set; default=-1; }
}
public class Dino.PeerInfo {
public PeerContentInfo? audio = null;
public PeerContentInfo? video = null;
} dino-0.5.0/libdino/src/service/call_state.vala 0000664 0000000 0000000 00000050673 14776241610 020017 0 ustar root root using Dino.Entities;
using Gee;
using Xmpp;
public class Dino.CallState : Object {
public signal void terminated(Jid who_terminated, string? reason_name, string? reason_text);
public signal void peer_joined(Jid jid, PeerState peer_state);
public signal void peer_left(Jid jid, PeerState peer_state, string? reason_name, string? reason_text);
public StreamInteractor stream_interactor;
public Plugins.VideoCallPlugin call_plugin = Dino.Application.get_default().plugin_registry.video_call_plugin;
public Call call;
public Jid? parent_muc { get; set; }
public Jid? invited_to_group_call = null;
public bool accepted { get; private set; default=false; }
public bool use_cim = false;
public string? cim_call_id = null;
public Jid? cim_counterpart = null;
public ArrayList cim_jids_to_inform = new ArrayList();
public string cim_message_type { get; set; default=Xmpp.MessageStanza.TYPE_CHAT; }
public Xep.Muji.GroupCall? group_call { get; set; }
public bool we_should_send_audio { get; set; default=false; }
public bool we_should_send_video { get; set; default=false; }
public HashMap peers = new HashMap(Jid.hash_func, Jid.equals_func);
private Plugins.MediaDevice selected_microphone_device;
private Plugins.MediaDevice selected_speaker_device;
private Plugins.MediaDevice selected_video_device;
public CallState(Call call, StreamInteractor stream_interactor) {
this.call = call;
this.stream_interactor = stream_interactor;
if (call.direction == Call.DIRECTION_OUTGOING && call.state != Call.State.OTHER_DEVICE) {
accepted = true;
Timeout.add_seconds(30, () => {
if (this == null) return false; // TODO enough?
if (call.state == Call.State.ESTABLISHING) {
call.state = Call.State.MISSED;
terminated(call.account.bare_jid, null, null);
}
return false;
});
}
}
internal async void initiate_groupchat_call(Jid muc) {
cim_jids_to_inform.add(muc);
cim_message_type = MessageStanza.TYPE_GROUPCHAT;
if (this.group_call == null) yield convert_into_group_call();
if (this.group_call == null) return;
// The user might have retracted the call in the meanwhile
if (this.call.state != Call.State.RINGING) return;
XmppStream stream = stream_interactor.get_stream(call.account);
if (stream == null) return;
Gee.List occupants = stream_interactor.get_module(MucManager.IDENTITY).get_other_occupants(muc, call.account);
foreach (Jid occupant in occupants) {
Jid? real_jid = stream_interactor.get_module(MucManager.IDENTITY).get_real_jid(occupant, call.account);
if (real_jid == null) continue;
debug(@"Adding MUC member as MUJI MUC owner %s", real_jid.bare_jid.to_string());
yield stream.get_module(Xep.Muc.Module.IDENTITY).change_affiliation(stream, group_call.muc_jid, real_jid.bare_jid, null, "owner");
}
stream.get_module(Xep.CallInvites.Module.IDENTITY).send_muji_propose(stream, cim_call_id, muc, group_call.muc_jid, we_should_send_video, cim_message_type);
}
internal PeerState set_first_peer(Jid peer) {
var peer_state = new PeerState(peer, call, this, stream_interactor);
peer_state.first_peer = true;
add_peer(peer_state);
return peer_state;
}
internal void add_peer(PeerState peer) {
call.add_peer(peer.jid.bare_jid);
connect_peer_signals(peer);
peer_joined(peer.jid, peer);
}
internal void on_peer_stream_created(PeerState peer, string media) {
if (media == "audio") {
call_plugin.set_device(peer.get_audio_stream(), get_microphone_device());
call_plugin.set_device(peer.get_audio_stream(), get_speaker_device());
} else if (media == "video") {
call_plugin.set_device(peer.get_video_stream(), get_video_device());
}
}
public void accept() {
accepted = true;
call.state = Call.State.ESTABLISHING;
XmppStream stream = stream_interactor.get_stream(call.account);
if (stream == null) return;
if (use_cim) {
if (invited_to_group_call != null) {
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) {
string sid = peers.values.to_array()[0].sid;
foreach (Jid jid_to_inform in cim_jids_to_inform) {
stream.get_module(Xep.CallInvites.Module.IDENTITY).send_jingle_accept(stream, jid_to_inform, cim_call_id, sid, cim_message_type);
}
}
} else {
foreach (PeerState peer in peers.values) {
peer.accept();
}
}
}
public void reject() {
call.state = Call.State.DECLINED;
if (use_cim) {
XmppStream stream = stream_interactor.get_stream(call.account);
if (stream == null) return;
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();
peers_cpy.add_all(peers.values);
foreach (PeerState peer in peers_cpy) {
peer.reject();
}
terminated(call.account.bare_jid, null, null);
}
public void end(string? reason_text = null) {
var peers_cpy = new ArrayList();
peers_cpy.add_all(peers.values);
// Terminate sessions, send out messages about the ended call, exit MUC if applicable
XmppStream stream = stream_interactor.get_stream(call.account);
if (stream != null) {
if (group_call != null) {
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) {
foreach (PeerState peer in peers_cpy) {
peer.end(Xep.Jingle.ReasonElement.SUCCESS, reason_text);
}
if (use_cim) {
foreach (Jid jid_to_inform in cim_jids_to_inform) {
stream.get_module(Xep.CallInvites.Module.IDENTITY).send_left(stream, jid_to_inform, cim_call_id, cim_message_type);
}
}
} else if (call.state == Call.State.RINGING) {
foreach (PeerState peer in peers_cpy) {
peer.end(Xep.Jingle.ReasonElement.CANCEL, reason_text);
}
if (call.direction == Call.DIRECTION_OUTGOING && use_cim) {
foreach (Jid jid_to_inform in cim_jids_to_inform) {
stream.get_module(Xep.CallInvites.Module.IDENTITY).send_retract(stream, jid_to_inform, 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;
} else {
return;
}
call.end_time = new DateTime.now_utc();
terminated(call.account.bare_jid, null, reason_text);
}
public void mute_own_audio(bool mute) {
we_should_send_audio = !mute;
foreach (PeerState peer in peers.values) {
peer.mute_own_audio(mute);
}
}
public void mute_own_video(bool mute) {
we_should_send_video = !mute;
foreach (PeerState peer in peers.values) {
peer.mute_own_video(mute);
}
}
public bool should_we_send_video() {
return we_should_send_video;
}
public async void invite_to_call(Jid invitee) {
if (this.group_call == null) yield convert_into_group_call();
if (this.group_call == null) return;
XmppStream stream = stream_interactor.get_stream(call.account);
if (stream == null) return;
debug("[%s] Inviting to muji call %s", call.account.bare_jid.to_string(), invitee.to_string());
yield stream.get_module(Xep.Muc.Module.IDENTITY).change_affiliation(stream, group_call.muc_jid, invitee, null, "owner");
stream.get_module(Xep.CallInvites.Module.IDENTITY).send_muji_propose(stream, cim_call_id, invitee, group_call.muc_jid, we_should_send_video, "chat");
// If the peer hasn't accepted within a minute, retract the invite
// TODO this should be unset when we retract the invite. otherwise a second invite attempt might break due to this
Timeout.add_seconds(60, () => {
if (this == null) return false;
bool contains_peer = false;
foreach (Jid peer in peers.keys) {
if (peer.equals_bare(invitee)) {
contains_peer = true;
}
}
if (!contains_peer) {
debug("[%s] Retracting invite to %s from %s", call.account.bare_jid.to_string(), group_call.muc_jid.to_string(), invitee.to_string());
// stream.get_module(Xep.CallInvites.Module.IDENTITY).send_retract(stream, invitee, invite_id);
// stream.get_module(Xep.Muc.Module.IDENTITY).change_affiliation.begin(stream, group_call.muc_jid, invitee, null, "none");
}
return false;
});
}
public Plugins.MediaDevice? get_microphone_device() {
if (selected_microphone_device == null) {
if (!peers.is_empty) {
var audio_stream = peers.values.to_array()[0].get_audio_stream();
selected_microphone_device = call_plugin.get_device(audio_stream, false);
}
if (selected_microphone_device == null) {
selected_microphone_device = call_plugin.get_preferred_device("audio", false);
}
}
return selected_microphone_device;
}
public Plugins.MediaDevice? get_speaker_device() {
if (selected_speaker_device == null) {
if (!peers.is_empty) {
var audio_stream = peers.values.to_array()[0].get_audio_stream();
selected_speaker_device = call_plugin.get_device(audio_stream, true);
}
if (selected_speaker_device == null) {
selected_speaker_device = call_plugin.get_preferred_device("audio", true);
}
}
return selected_speaker_device;
}
public Plugins.MediaDevice? get_video_device() {
if (selected_video_device == null) {
if (!peers.is_empty) {
var video_stream = peers.values.to_array()[0].get_video_stream();
selected_video_device = call_plugin.get_device(video_stream, false);
}
if (selected_video_device == null) {
selected_video_device = call_plugin.get_preferred_device("video", false);
}
}
return selected_video_device;
}
public void set_audio_device(Plugins.MediaDevice? device) {
if (device.incoming) {
selected_speaker_device = device;
} else {
selected_microphone_device = device;
}
foreach (PeerState peer_state in peers.values) {
call_plugin.set_device(peer_state.get_audio_stream(), device);
}
}
public void set_video_device(Plugins.MediaDevice? device) {
selected_video_device = device;
foreach (PeerState peer_state in peers.values) {
call_plugin.set_device(peer_state.get_video_stream(), device);
}
}
internal void rename_peer(Jid from_jid, Jid to_jid) {
debug("[%s] Renaming %s to %s exists %s", call.account.bare_jid.to_string(), from_jid.to_string(), to_jid.to_string(), peers.has_key(from_jid).to_string());
PeerState? peer_state = peers[from_jid];
if (peer_state == null) return;
// Adjust the internal mapping of this `PeerState` object
peers.unset(from_jid);
peers[to_jid] = peer_state;
peer_state.jid = to_jid;
}
private void on_call_terminated(Jid who_terminated, bool we_terminated, string? reason_name, string? reason_text) {
if (call.state == Call.State.RINGING || call.state == Call.State.IN_PROGRESS || call.state == Call.State.ESTABLISHING) {
call.end_time = new DateTime.now_utc();
}
if (call.state == Call.State.IN_PROGRESS) {
call.state = Call.State.ENDED;
} else if (call.state == Call.State.RINGING || call.state == Call.State.ESTABLISHING) {
if (reason_name == Xep.Jingle.ReasonElement.DECLINE) {
call.state = Call.State.DECLINED;
} else {
call.state = Call.State.FAILED;
}
}
terminated(who_terminated, reason_name, reason_text);
}
private void connect_peer_signals(PeerState peer_state) {
peers[peer_state.jid] = peer_state;
this.bind_property("we-should-send-audio", peer_state, "we-should-send-audio", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL);
this.bind_property("we-should-send-video", peer_state, "we-should-send-video", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL);
this.bind_property("group-call", peer_state, "group-call", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL);
peer_state.stream_created.connect((peer, media) => { on_peer_stream_created(peer, media); });
peer_state.session_terminated.connect((we_terminated, reason_name, reason_text) => {
debug("[%s] Peer left %s: %s %s (%i peers remaining)", call.account.bare_jid.to_string(), reason_text ?? "", reason_name ?? "", peer_state.jid.to_string(), peers.size);
handle_peer_left(peer_state, we_terminated, reason_name, reason_text);
});
}
public async bool can_convert_into_groupcall() {
if (peers.size == 0) return false;
Jid peer = peers.keys.to_array()[0];
bool peer_has_feature = yield stream_interactor.get_module(EntityInfo.IDENTITY).has_feature(call.account, peer, Xep.Muji.NS_URI);
bool can_initiate = stream_interactor.get_module(Calls.IDENTITY).can_initiate_groupcall(call.account);
return peer_has_feature && can_initiate;
}
public async void convert_into_group_call() {
XmppStream stream = stream_interactor.get_stream(call.account);
if (stream == null) return;
Jid? muc_jid = stream_interactor.get_module(MucManager.IDENTITY).default_muc_server[call.account];
if (muc_jid == null) {
warning("Failed to initiate group call: MUC server not known.");
return;
}
if (cim_call_id == null) cim_call_id = Xmpp.random_uuid();
muc_jid = new Jid("%08x@".printf(Random.next_int()) + muc_jid.to_string()); // TODO longer?
debug("[%s] Converting call to groupcall %s", call.account.bare_jid.to_string(), muc_jid.to_string());
yield join_group_call(muc_jid);
Xep.DataForms.DataForm? data_form = yield stream_interactor.get_module(MucManager.IDENTITY).get_config_form(call.account, muc_jid);
if (data_form == null) return;
foreach (Xep.DataForms.DataForm.Field field in data_form.fields) {
switch (field.var) {
case "muc#roomconfig_allowinvites":
if (field.type_ == Xep.DataForms.DataForm.Type.BOOLEAN) {
((Xep.DataForms.DataForm.BooleanField) field).value = true;
}
break;
case "muc#roomconfig_persistentroom":
if (field.type_ == Xep.DataForms.DataForm.Type.BOOLEAN) {
((Xep.DataForms.DataForm.BooleanField) field).value = false;
}
break;
case "muc#roomconfig_membersonly":
if (field.type_ == Xep.DataForms.DataForm.Type.BOOLEAN) {
((Xep.DataForms.DataForm.BooleanField) field).value = true;
}
break;
case "muc#roomconfig_whois":
if (field.type_ == Xep.DataForms.DataForm.Type.LIST_SINGLE) {
((Xep.DataForms.DataForm.ListSingleField) field).value = "anyone";
}
break;
}
}
yield stream_interactor.get_module(MucManager.IDENTITY).set_config_form(call.account, muc_jid, data_form);
foreach (Jid peer_jid in peers.keys) {
debug("[%s] Group call inviting %s", call.account.bare_jid.to_string(), peer_jid.to_string());
yield invite_to_call(peer_jid);
}
}
public async void join_group_call(Jid muc_jid) {
debug("[%s] Joining group call %s", call.account.bare_jid.to_string(), muc_jid.to_string());
XmppStream stream = stream_interactor.get_stream(call.account);
if (stream == null) return;
this.group_call = yield stream.get_module(Xep.Muji.Module.IDENTITY).join_call(stream, muc_jid, we_should_send_video);
if (this.group_call == null) {
warning("[%s] Couldn't join MUJI MUC", call.account.bare_jid.to_string());
return;
}
this.group_call.peer_joined.connect((jid) => {
debug("[%s] Group call peer joined: %s", call.account.bare_jid.to_string(), jid.to_string());
// Newly joined peers have to call us, not the other way round
// Maybe they called us already. Accept the call.
// (Except for the first peer, we already have a connection to that one.)
if (peers.has_key(jid)) {
if (!peers[jid].first_peer) {
peers[jid].accept();
}
// else: Connection to first peer already active
} else {
var peer_state = new PeerState(jid, call, this, stream_interactor);
peer_state.waiting_for_inbound_muji_connection = true;
debug("[%s] Waiting for call from %s", call.account.bare_jid.to_string(), jid.to_string());
add_peer(peer_state);
}
});
this.group_call.peer_left.connect((jid) => {
debug("[%s] Group call peer left: %s", call.account.bare_jid.to_string(), jid.to_string());
PeerState? peer_state = peers[jid];
if (peer_state == null) return;
peer_state.end(Xep.Jingle.ReasonElement.CANCEL, "Peer left the MUJI MUC");
handle_peer_left(peer_state, false, Xep.Jingle.ReasonElement.CANCEL, "Peer left the MUJI MUC");
});
if (group_call.peers_to_connect_to.size > 4) {
end("Call too full - P2p calls don't work well with many participants");
return;
}
// Call all peers that are in the room already
foreach (Jid peer_jid in group_call.peers_to_connect_to) {
// Don't establish connection if we have one already (the person that invited us to the call)
if (peers.has_key(peer_jid)) continue;
debug("[%s] Calling %s because they were in the MUC already", call.account.bare_jid.to_string(), peer_jid.to_string());
PeerState peer_state = new PeerState(peer_jid, call, this, stream_interactor);
add_peer(peer_state);
peer_state.call_resource.begin(peer_jid);
}
debug("[%s] Finished joining MUJI muc %s", call.account.bare_jid.to_string(), muc_jid.to_string());
}
private void handle_peer_left(PeerState peer_state, bool we_terminated, string? reason_name, string? reason_text) {
if (!peers.has_key(peer_state.jid)) return;
peers.unset(peer_state.jid);
if (peers.is_empty) {
if (group_call != null) {
group_call.leave(stream_interactor.get_stream(call.account));
on_call_terminated(peer_state.jid, we_terminated, null, "All participants have left the call");
} else {
on_call_terminated(peer_state.jid, we_terminated, reason_name, reason_text);
}
} else {
peer_left(peer_state.jid, peer_state, reason_name, reason_text);
}
}
} dino-0.5.0/libdino/src/service/call_store.vala 0000664 0000000 0000000 00000004044 14776241610 020022 0 ustar root root using Xmpp;
using Gee;
using Qlite;
using Dino.Entities;
namespace Dino {
public class CallStore : StreamInteractionModule, Object {
public static ModuleIdentity IDENTITY = new ModuleIdentity("call_store");
public string id { get { return IDENTITY.id; } }
private StreamInteractor stream_interactor;
private Database db;
private WeakMap calls_by_db_id = new WeakMap();
public static void start(StreamInteractor stream_interactor, Database db) {
CallStore m = new CallStore(stream_interactor, db);
stream_interactor.add_module(m);
}
private CallStore(StreamInteractor stream_interactor, Database db) {
this.stream_interactor = stream_interactor;
this.db = db;
}
public void add_call(Call call, Conversation conversation) {
call.persist(db);
cache_call(call);
}
public Call? get_call_by_id(int id, Conversation conversation) {
Call? call = calls_by_db_id[id];
if (call != null) {
return call;
}
RowOption row_option = db.call.select().with(db.call.id, "=", id).row();
return create_call_from_row_opt(row_option, conversation);
}
private Call? create_call_from_row_opt(RowOption row_opt, Conversation conversation) {
if (!row_opt.is_present()) return null;
try {
Call call = new Call.from_row(db, row_opt.inner);
if (conversation.type_.is_muc_semantic()) {
call.ourpart = conversation.counterpart.with_resource(call.ourpart.resourcepart);
}
cache_call(call);
return call;
} catch (InvalidJidError e) {
warning("Got message with invalid Jid: %s", e.message);
}
return null;
}
private void cache_call(Call call) {
calls_by_db_id[call.id] = call;
}
}
} dino-0.5.0/libdino/src/service/calls.vala 0000664 0000000 0000000 00000065565 14776241610 017010 0 ustar root root using Gee;
using Xmpp;
using Dino.Entities;
namespace Dino {
public class Calls : StreamInteractionModule, Object {
public signal void call_incoming(Call call, CallState state, Conversation conversation, bool video, bool multiparty);
public signal void call_outgoing(Call call, CallState state, Conversation conversation);
public signal void call_terminated(Call call, string? reason_name, string? reason_text);
public signal void conference_info_received(Call call, Xep.Coin.ConferenceInfo conference_info);
public static ModuleIdentity IDENTITY = new ModuleIdentity("calls");
public string id { get { return IDENTITY.id; } }
private StreamInteractor stream_interactor;
private Database db;
// public HashMap current_jmi_request_call = new HashMap(Account.hash_func, Account.equals_func);
public HashMap jmi_request_peer = new HashMap(Call.hash_func, Call.equals_func);
public HashMap call_states = new HashMap(Call.hash_func, Call.equals_func);
public static void start(StreamInteractor stream_interactor, Database db) {
Calls m = new Calls(stream_interactor, db);
stream_interactor.add_module(m);
}
private Calls(StreamInteractor stream_interactor, Database db) {
this.stream_interactor = stream_interactor;
this.db = db;
stream_interactor.account_added.connect(on_account_added);
}
public async CallState? initiate_call(Conversation conversation, bool video) {
Call call = new Call();
call.direction = Call.DIRECTION_OUTGOING;
call.account = conversation.account;
call.counterpart = conversation.counterpart;
call.ourpart = stream_interactor.get_module(MucManager.IDENTITY).get_own_jid(conversation.counterpart, conversation.account) ?? conversation.account.full_jid;
call.time = call.local_time = call.end_time = new DateTime.now_utc();
call.encryption = Encryption.UNKNOWN;
call.state = Call.State.RINGING;
stream_interactor.get_module(CallStore.IDENTITY).add_call(call, conversation);
var call_state = new CallState(call, stream_interactor);
connect_call_state_signals(call_state);
call_state.we_should_send_video = video;
call_state.we_should_send_audio = true;
if (conversation.type_ == Conversation.Type.CHAT) {
call.add_peer(conversation.counterpart);
PeerState peer_state = call_state.set_first_peer(conversation.counterpart);
jmi_request_peer[call] = peer_state;
yield peer_state.initiate_call(conversation.counterpart);
} else {
call_state.initiate_groupchat_call.begin(conversation.counterpart);
}
call_outgoing(call, call_state, conversation);
return call_state;
}
public bool can_we_do_calls(Account account) {
Plugins.VideoCallPlugin? plugin = Application.get_default().plugin_registry.video_call_plugin;
if (plugin == null) return false;
return plugin.supported();
}
public async bool can_conversation_do_calls(Conversation conversation) {
if (!can_we_do_calls(conversation.account)) return false;
if (conversation.type_ == Conversation.Type.CHAT) {
return !conversation.counterpart.equals_bare(conversation.account.bare_jid);
} else {
bool is_private = stream_interactor.get_module(MucManager.IDENTITY).is_private_room(conversation.account, conversation.counterpart);
return is_private && can_initiate_groupcall(conversation.account);
}
}
public bool can_initiate_groupcall(Account account) {
return stream_interactor.get_module(MucManager.IDENTITY).default_muc_server[account] != null;
}
public async Gee.List get_call_resources(Account account, Jid counterpart) {
ArrayList ret = new ArrayList();
XmppStream? stream = stream_interactor.get_stream(account);
if (stream == null) return ret;
Presence.Flag? presence_flag = stream.get_flag(Presence.Flag.IDENTITY);
if (presence_flag == null) return ret;
Gee.List? full_jids = presence_flag.get_resources(counterpart);
if (full_jids == null) return ret;
foreach (Jid full_jid in full_jids) {
var module = stream.get_module(Xep.JingleRtp.Module.IDENTITY);
if (module == null) return ret;
bool supports_rtc = yield module.is_available(stream, full_jid);
if (!supports_rtc) continue;
ret.add(full_jid);
}
return ret;
}
public async bool contains_jmi_resources(Account account, Gee.List full_jids) {
XmppStream? stream = stream_interactor.get_stream(account);
if (stream == null) return false;
foreach (Jid full_jid in full_jids) {
bool does_jmi = yield stream_interactor.get_module(EntityInfo.IDENTITY).has_feature(account, full_jid, Xep.JingleMessageInitiation.NS_URI);
if (does_jmi) return true;
}
return false;
}
public bool has_jmi_resources(Jid counterpart) {
int64 jmi_resources = db.entity.select()
.with(db.entity.jid_id, "=", db.get_jid_id(counterpart))
.join_with(db.entity_feature, db.entity.caps_hash, db.entity_feature.entity)
.with(db.entity_feature.feature, "=", Xep.JingleMessageInitiation.NS_URI)
.count();
return jmi_resources > 0;
}
public bool is_call_in_progress() {
foreach (Call call in call_states.keys) {
if (call.state == Call.State.IN_PROGRESS || call.state == Call.State.RINGING || call.state == Call.State.ESTABLISHING) {
return true;
}
}
return false;
}
private void on_incoming_call(Account account, Xep.Jingle.Session session) {
Jid? muji_room = session.muji_room;
bool counterpart_wants_video = false;
foreach (Xep.Jingle.Content content in session.contents) {
Xep.JingleRtp.Parameters? rtp_content_parameter = content.content_params as Xep.JingleRtp.Parameters;
if (rtp_content_parameter == null) continue;
if (rtp_content_parameter.media == "video" && session.senders_include_us(content.senders)) {
counterpart_wants_video = true;
}
}
// Check if this comes from a MUJI MUC => accept
if (muji_room != null) {
debug("[%s] Incoming call from %s from MUJI muc %s", account.bare_jid.to_string(), session.peer_full_jid.to_string(), muji_room.to_string());
foreach (CallState call_state in call_states.values) {
if (call_state.call.account.equals(account) && call_state.group_call != null && call_state.group_call.muc_jid.equals(muji_room)) {
if (call_state.peers.keys.contains(session.peer_full_jid)) {
PeerState peer_state = call_state.peers[session.peer_full_jid];
debug("[%s] Incoming call, we know the peer. Expected %s", account.bare_jid.to_string(), peer_state.waiting_for_inbound_muji_connection.to_string());
if (!peer_state.waiting_for_inbound_muji_connection) return;
peer_state.set_session(session);
debug(@"[%s] Accepting incoming MUJI call from %s", account.bare_jid.to_string(), session.peer_full_jid.to_string());
peer_state.accept();
} else {
debug(@"[%s] Incoming call, but didn't see peer in MUC yet", account.bare_jid.to_string());
PeerState peer_state = new PeerState(session.peer_full_jid, call_state.call, call_state, stream_interactor);
peer_state.set_session(session);
call_state.add_peer(peer_state);
}
return;
}
}
return;
}
debug(@"[%s] Incoming call from %s", account.bare_jid.to_string(), session.peer_full_jid.to_string());
// Check if we already got this call via Jingle Message Initiation => accept
// PeerState.accept() checks if the call was accepted and ensures that we don't accidentally send video
PeerState? peer_state = get_peer_by_sid(account, session.sid, session.peer_full_jid);
if (peer_state != null) {
jmi_request_peer[peer_state.call].set_session(session);
jmi_request_peer[peer_state.call].accept();
jmi_request_peer.unset(peer_state.call);
return;
}
// This is a direct call without prior JMI. Ask user.
if (stream_interactor.get_module(MucManager.IDENTITY).might_be_groupchat(session.peer_full_jid.bare_jid, account)) return;
peer_state = create_received_call(account, session.peer_full_jid, account.full_jid, counterpart_wants_video);
peer_state.set_session(session);
Conversation conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation(peer_state.call.counterpart.bare_jid, account, Conversation.Type.CHAT);
call_incoming(peer_state.call, peer_state.call_state, conversation, counterpart_wants_video, false);
stream_interactor.module_manager.get_module(account, Xep.JingleRtp.Module.IDENTITY).session_info_type.send_ringing(session);
}
private PeerState create_received_call(Account account, Jid from, Jid to, bool video_requested) {
Call call = new Call();
if (from.equals_bare(account.bare_jid)) {
// Call requested by another of our devices
call.direction = Call.DIRECTION_OUTGOING;
call.ourpart = from;
call.state = Call.State.OTHER_DEVICE;
call.counterpart = to;
} else {
call.direction = Call.DIRECTION_INCOMING;
call.ourpart = account.full_jid;
call.state = Call.State.RINGING;
call.counterpart = from;
}
call.add_peer(call.counterpart);
call.account = account;
call.time = call.local_time = call.end_time = new DateTime.now_utc();
call.encryption = Encryption.UNKNOWN;
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);
var call_state = new CallState(call, stream_interactor);
connect_call_state_signals(call_state);
PeerState peer_state = call_state.set_first_peer(call.counterpart);
call_state.we_should_send_video = video_requested;
call_state.we_should_send_audio = true;
return peer_state;
}
private CallState? get_call_state_by_call_id(Account account, string call_id, Jid? counterpart_jid = null) {
foreach (CallState call_state in call_states.values) {
if (!call_state.call.account.equals(account)) continue;
if (call_state.cim_call_id == call_id) {
if (counterpart_jid == null) return call_state;
foreach (Jid jid in call_state.peers.keys) {
if (jid.equals_bare(counterpart_jid)) {
return call_state;
}
}
}
}
return null;
}
private PeerState? get_peer_by_sid(Account account, string sid, Jid jid1, Jid? jid2 = null) {
Jid relevant_jid = jid1.equals_bare(account.bare_jid) && jid2 != null ? jid2 : jid1;
foreach (CallState call_state in call_states.values) {
if (!call_state.call.account.equals(account)) continue;
foreach (PeerState peer_state in call_state.peers.values) {
if (peer_state.sid != sid) continue;
if (peer_state.jid.equals_bare(relevant_jid)) {
return peer_state;
}
}
}
return null;
}
private CallState? create_recv_muji_call(Account account, string call_id, Jid inviter_jid, Jid muc_jid, string message_type) {
debug("[%s] Muji call received from %s for MUC %s, type %s", account.bare_jid.to_string(), inviter_jid.to_string(), muc_jid.to_string(), message_type);
foreach (Call call in call_states.keys) {
if (!call.account.equals(account)) continue;
CallState call_state = call_states[call];
if (call.counterparts.size == 1 && call.counterparts.contains(inviter_jid) && call_state.accepted) {
// A call is converted into a group call.
call_state.cim_call_id = call_id;
call_state.join_group_call.begin(muc_jid);
return null;
}
}
Call call = new Call();
call.direction = Call.DIRECTION_INCOMING;
call.ourpart = account.full_jid;
call.counterpart = inviter_jid;
call.account = account;
call.time = call.local_time = call.end_time = new DateTime.now_utc();
call.encryption = Encryption.UNKNOWN;
call.state = Call.State.RINGING;
// TODO create conv
Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation(inviter_jid.bare_jid, account);
if (conversation == null) return null;
stream_interactor.get_module(CallStore.IDENTITY).add_call(call, conversation);
CallState call_state = new CallState(call, stream_interactor);
connect_call_state_signals(call_state);
call_state.invited_to_group_call = muc_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());
return call_state;
}
private void remove_call_from_datastructures(Call call) {
jmi_request_peer.unset(call);
call_states.unset(call);
}
private void connect_call_state_signals(CallState call_state) {
call_states[call_state.call] = call_state;
ulong terminated_handler_id = -1;
terminated_handler_id = call_state.terminated.connect((who_terminated, reason_name, reason_text) => {
remove_call_from_datastructures(call_state.call);
call_terminated(call_state.call, reason_name, reason_text);
call_state.disconnect(terminated_handler_id);
});
}
private void on_account_added(Account account) {
Xep.Jingle.Module jingle_module = stream_interactor.module_manager.get_module(account, Xep.Jingle.Module.IDENTITY);
jingle_module.session_initiate_received.connect((stream, session) => {
foreach (Xep.Jingle.Content content in session.contents) {
Xep.JingleRtp.Parameters? rtp_content_parameter = content.content_params as Xep.JingleRtp.Parameters;
if (rtp_content_parameter != null) {
on_incoming_call(account, session);
break;
}
}
});
Xep.JingleMessageInitiation.Module mi_module = stream_interactor.module_manager.get_module(account, Xep.JingleMessageInitiation.Module.IDENTITY);
mi_module.session_proposed.connect((from, to, sid, descriptions) => {
if (stream_interactor.get_module(MucManager.IDENTITY).might_be_groupchat(from.bare_jid, account)) return;
bool audio_requested = descriptions.any_match((description) => description.ns_uri == Xep.JingleRtp.NS_URI && description.get_attribute("media") == "audio");
bool video_requested = descriptions.any_match((description) => description.ns_uri == Xep.JingleRtp.NS_URI && description.get_attribute("media") == "video");
if (!audio_requested && !video_requested) return;
PeerState peer_state = create_received_call(account, from, to, video_requested);
peer_state.sid = sid;
CallState call_state = call_states[peer_state.call];
jmi_request_peer[peer_state.call] = peer_state;
Conversation conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation(call_state.call.counterpart.bare_jid, account, Conversation.Type.CHAT);
if (call_state.call.direction == Call.DIRECTION_INCOMING) {
call_incoming(call_state.call, call_state, conversation, video_requested, false);
} else {
call_outgoing(call_state.call, call_state, conversation);
}
});
mi_module.session_accepted.connect((from, to, sid) => {
PeerState? peer_state = get_peer_by_sid(account, sid, from, to);
if (peer_state == null) return;
Call call = peer_state.call;
// Carboned message from our account
if (from.equals_bare(account.bare_jid)) {
// Ignore carbon from ourselves
if (from.equals(account.full_jid)) return;
call.ourpart = from;
call.state = Call.State.OTHER_DEVICE;
remove_call_from_datastructures(call);
return;
}
// We proposed the call. This is a message from our peer.
if (call.direction == Call.DIRECTION_OUTGOING &&
from.equals_bare(peer_state.jid) && to.equals(account.full_jid)) {
// We know the full jid of our peer now
call_states[call].rename_peer(jmi_request_peer[call].jid, from);
jmi_request_peer[call].call_resource.begin(from);
}
});
mi_module.session_rejected.connect((from, to, sid) => {
PeerState? peer_state = get_peer_by_sid(account, sid, from, to);
if (peer_state == null) return;
Call call = peer_state.call;
bool outgoing_reject = call.direction == Call.DIRECTION_OUTGOING && from.equals_bare(call.counterparts[0]);
bool incoming_reject = call.direction == Call.DIRECTION_INCOMING && from.equals_bare(account.bare_jid);
if (!outgoing_reject && !incoming_reject) return;
// We don't care if a single person in a group call rejected the call
if (incoming_reject && call_states[call].group_call != null) return;
call.state = Call.State.DECLINED;
call_states[call].terminated(from, Xep.Jingle.ReasonElement.DECLINE, "JMI reject");
remove_call_from_datastructures(call);
});
mi_module.session_retracted.connect((from, to, sid) => {
PeerState? peer_state = get_peer_by_sid(account, sid, from, to);
if (peer_state == null) return;
Call call = peer_state.call;
bool outgoing_retract = call.direction == Call.DIRECTION_OUTGOING && from.equals_bare(account.bare_jid);
bool incoming_retract = call.direction == Call.DIRECTION_INCOMING && from.equals_bare(call.counterpart);
if (!(outgoing_retract || incoming_retract)) return;
call.state = Call.State.MISSED;
call_states[call].terminated(from, Xep.Jingle.ReasonElement.CANCEL, "JMI retract");
remove_call_from_datastructures(call);
});
Xep.CallInvites.Module call_invites_module = stream_interactor.module_manager.get_module(account, Xep.CallInvites.Module.IDENTITY);
call_invites_module.call_proposed.connect((from_jid, to_jid, call_id, video_requested, join_methods, message_stanza) => {
if (from_jid.equals_bare(account.bare_jid)) return;
if (stream_interactor.get_module(MucManager.IDENTITY).is_own_muc_jid(from_jid, account)) return;
bool multiparty = false;
CallState? call_state = null;
foreach (StanzaNode join_method_node in join_methods) {
if (join_method_node.name == "muji" && join_method_node.ns_uri == Xep.Muji.NS_URI) {
// This is a MUJI invite
// Disregard calls from muc history
DateTime? delay = Xep.DelayedDelivery.get_time_for_message(message_stanza, from_jid.bare_jid);
if (delay != null) return;
string? room_jid_str = join_method_node.get_attribute("room");
if (room_jid_str == null) return;
Jid room_jid = new Jid(room_jid_str);
call_state = create_recv_muji_call(account, call_id, from_jid, room_jid, message_stanza.type_);
multiparty = true;
break;
} else if (join_method_node.name == "jingle" && join_method_node.ns_uri == Xep.CallInvites.NS_URI) {
// This is an invite for a direct Jingle session
if (stream_interactor.get_module(MucManager.IDENTITY).might_be_groupchat(from_jid.bare_jid, account)) return;
string? sid = join_method_node.get_attribute("sid");
if (sid == null) return;
PeerState peer_state = create_received_call(account, from_jid, to_jid, video_requested);
peer_state.sid = sid;
call_state = call_states[peer_state.call];
jmi_request_peer[call_state.call] = peer_state;
break;
}
}
if (call_state == null) return;
call_state.we_should_send_audio = true;
call_state.we_should_send_video = video_requested;
call_state.use_cim = true;
call_state.cim_call_id = call_id;
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_;
Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).approx_conversation_for_stanza(from_jid, to_jid, account, message_stanza.type_);
if (conversation == null) return;
if (call_state.call.direction == Call.DIRECTION_INCOMING) {
call_incoming(call_state.call, call_state, conversation, video_requested, multiparty);
} else {
call_outgoing(call_state.call, call_state, conversation);
}
});
call_invites_module.call_accepted.connect((from_jid, to_jid, call_id, message_type) => {
// Carboned message from our account
if (from_jid.equals_bare(account.bare_jid)) {
CallState? call_state = get_call_state_by_call_id(account, call_id);
if (call_state == null) return;
Call call = call_state.call;
// Ignore carbon from ourselves
if (from_jid.equals(account.full_jid)) return;
// We accepted the call from another device
call.ourpart = from_jid;
call.state = Call.State.OTHER_DEVICE;
remove_call_from_datastructures(call);
return;
}
CallState? call_state = get_call_state_by_call_id(account, call_id, from_jid);
if (call_state == null) return;
Call call = call_state.call;
// We proposed the call. This is a message from our peer.
if (call.direction == Call.DIRECTION_OUTGOING &&
to_jid.equals(account.full_jid)) {
// We know the full jid of our peer now
call_state.rename_peer(jmi_request_peer[call].jid, from_jid);
jmi_request_peer[call].call_resource.begin(from_jid);
}
});
call_invites_module.call_retracted.connect((from_jid, to_jid, call_id, message_type) => {
if (from_jid.equals_bare(account.bare_jid)) return;
// The call was retracted by the counterpart
CallState? call_state = get_call_state_by_call_id(account, call_id, from_jid);
if (call_state == null) return;
if (call_state.call.state != Call.State.RINGING) {
debug("%s tried to retract a call that's in state %s. Ignoring.", from_jid.to_string(), call_state.call.state.to_string());
return;
}
// TODO prevent other MUC occupants from retracting a call
call_state.call.state = Call.State.MISSED;
remove_call_from_datastructures(call_state.call);
});
call_invites_module.call_rejected.connect((from_jid, to_jid, call_id, message_type) => {
// We rejected an invite from another device
if (from_jid.equals_bare(account.bare_jid)) {
CallState? call_state = get_call_state_by_call_id(account, call_id);
if (call_state == null) return;
Call call = call_state.call;
call.state = Call.State.DECLINED;
}
if (from_jid.equals_bare(account.bare_jid)) return;
debug(@"[%s] %s rejected our MUJI invite", account.bare_jid.to_string(), from_jid.to_string());
});
stream_interactor.module_manager.get_module(account, Xep.Coin.Module.IDENTITY).coin_info_received.connect((jid, info) => {
foreach (Call call in call_states.keys) {
if (call.counterparts[0].equals_bare(jid)) {
conference_info_received(call, info);
return;
}
}
});
}
}
} dino-0.5.0/libdino/src/service/chat_interaction.vala 0000664 0000000 0000000 00000027602 14776241610 021216 0 ustar root root using Gee;
using Xmpp;
using Dino.Entities;
namespace Dino {
public class ChatInteraction : StreamInteractionModule, Object {
public static ModuleIdentity IDENTITY = new ModuleIdentity("chat_interaction");
public string id { get { return IDENTITY.id; } }
public signal void focused_in(Conversation conversation);
public signal void focused_out(Conversation conversation);
private StreamInteractor stream_interactor;
private Conversation? selected_conversation;
private HashMap last_input_interaction = new HashMap(Conversation.hash_func, Conversation.equals_func);
private HashMap last_interface_interaction = new HashMap(Conversation.hash_func, Conversation.equals_func);
private bool focus_in = false;
public static void start(StreamInteractor stream_interactor) {
ChatInteraction m = new ChatInteraction(stream_interactor);
stream_interactor.add_module(m);
}
private ChatInteraction(StreamInteractor stream_interactor) {
this.stream_interactor = stream_interactor;
Timeout.add_seconds(30, update_interactions);
stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(new ReceivedMessageListener(stream_interactor));
stream_interactor.get_module(MessageProcessor.IDENTITY).message_sent.connect(on_message_sent);
stream_interactor.get_module(ContentItemStore.IDENTITY).new_item.connect(new_item);
}
public int get_num_unread(Conversation conversation) {
Database db = Dino.Application.get_default().db;
Qlite.QueryBuilder query = db.content_item.select()
.with(db.content_item.conversation_id, "=", conversation.id)
.with(db.content_item.hide, "=", false);
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) {
string time = read_up_to_item.time.to_unix().to_string();
string id = read_up_to_item.id.to_string();
query.where(@"time > ? OR (time = ? AND id > ?)", { time, time, id });
}
// If it's a new conversation with read_up_to_item == null, all items are new.
return (int) query.count();
}
public bool is_active_focus(Conversation? conversation = null) {
if (conversation != null) {
return focus_in && conversation.equals(this.selected_conversation);
} else {
return focus_in;
}
}
public void on_window_focus_in(Conversation? conversation) {
on_conversation_focused(conversation);
}
public void on_window_focus_out(Conversation? conversation) {
on_conversation_unfocused(conversation);
}
public void on_message_entered(Conversation? conversation) {
if (!last_input_interaction.has_key(conversation)) {
send_chat_state_notification(conversation, Xep.ChatStateNotifications.STATE_COMPOSING);
}
last_input_interaction[conversation] = new DateTime.now_utc();
last_interface_interaction[conversation] = new DateTime.now_utc();
}
public void on_message_cleared(Conversation? conversation) {
if (last_input_interaction.has_key(conversation)) {
last_input_interaction.unset(conversation);
send_chat_state_notification(conversation, Xep.ChatStateNotifications.STATE_ACTIVE);
}
}
public void on_conversation_selected(Conversation conversation) {
on_conversation_unfocused(selected_conversation);
selected_conversation = conversation;
on_conversation_focused(conversation);
}
private void new_item(ContentItem item, Conversation conversation) {
bool mark_read = is_active_focus(conversation);
if (!mark_read) {
MessageItem? message_item = item as MessageItem;
if (message_item != null) {
if (message_item.message.direction == Message.DIRECTION_SENT) {
mark_read = true;
}
}
if (message_item == null) {
FileItem? file_item = item as FileItem;
if (file_item != null) {
if (file_item.file_transfer.direction == FileTransfer.DIRECTION_SENT) {
mark_read = true;
}
}
}
}
if (mark_read) {
ContentItem? read_up_to = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item_by_id(conversation, conversation.read_up_to_item);
if (read_up_to != null) {
if (read_up_to.compare(item) < 0) {
conversation.read_up_to_item = item.id;
}
} else {
conversation.read_up_to_item = item.id;
}
}
}
private void on_message_sent(Entities.Message message, Conversation conversation) {
last_input_interaction.unset(conversation);
last_interface_interaction.unset(conversation);
}
private void on_conversation_focused(Conversation? conversation) {
focus_in = true;
if (conversation == null) return;
focused_in(conversation);
check_send_read();
ContentItem? latest_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_latest(conversation);
if (latest_item != null) {
conversation.read_up_to_item = latest_item.id;
}
}
private void on_conversation_unfocused(Conversation? conversation) {
focus_in = false;
if (conversation == null) return;
focused_out(conversation);
if (last_input_interaction.has_key(conversation)) {
send_chat_state_notification(conversation, Xep.ChatStateNotifications.STATE_PAUSED);
last_input_interaction.unset(conversation);
}
}
private void check_send_read() {
if (selected_conversation == null) return;
Entities.Message? message = stream_interactor.get_module(MessageStorage.IDENTITY).get_last_message(selected_conversation);
if (message != null && message.direction == Entities.Message.DIRECTION_RECEIVED) {
send_chat_marker(message, null, selected_conversation, Xep.ChatMarkers.MARKER_DISPLAYED);
}
}
private bool update_interactions() {
for (MapIterator iter = last_input_interaction.map_iterator(); iter.has_next(); iter.next()) {
if (!iter.valid && iter.has_next()) iter.next();
Conversation conversation = iter.get_key();
if (last_input_interaction.has_key(conversation) &&
(new DateTime.now_utc()).difference(last_input_interaction[conversation]) >= 15 * TimeSpan.SECOND) {
iter.unset();
send_chat_state_notification(conversation, Xep.ChatStateNotifications.STATE_PAUSED);
}
}
for (MapIterator iter = last_interface_interaction.map_iterator(); iter.has_next(); iter.next()) {
if (!iter.valid && iter.has_next()) iter.next();
Conversation conversation = iter.get_key();
if (last_interface_interaction.has_key(conversation) &&
(new DateTime.now_utc()).difference(last_interface_interaction[conversation]) >= 1.5 * TimeSpan.MINUTE) {
iter.unset();
send_chat_state_notification(conversation, Xep.ChatStateNotifications.STATE_GONE);
}
}
return true;
}
private class ReceivedMessageListener : MessageListener {
public string[] after_actions_const = new string[]{ "DEDUPLICATE", "FILTER_EMPTY", "STORE_CONTENT_ITEM" };
public override string action_group { get { return "OTHER_NODES"; } }
public override string[] after_actions { get { return after_actions_const; } }
private StreamInteractor stream_interactor;
public ReceivedMessageListener(StreamInteractor stream_interactor) {
this.stream_interactor = stream_interactor;
}
public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
if (Xmpp.MessageArchiveManagement.MessageFlag.get_flag(stanza) != null) return false;
ChatInteraction outer = stream_interactor.get_module(ChatInteraction.IDENTITY);
outer.send_delivery_receipt(message, stanza, conversation);
// Send chat marker
if (message.direction == Entities.Message.DIRECTION_SENT) return false;
if (outer.is_active_focus(conversation)) {
outer.check_send_read();
outer.send_chat_marker(message, stanza, conversation, Xep.ChatMarkers.MARKER_DISPLAYED);
} else {
outer.send_chat_marker(message, stanza, conversation, Xep.ChatMarkers.MARKER_RECEIVED);
}
return false;
}
}
private void send_chat_marker(Entities.Message message, Xmpp.MessageStanza? stanza, Conversation conversation, string marker) {
XmppStream? stream = stream_interactor.get_stream(conversation.account);
if (stream == null) return;
switch (marker) {
case Xep.ChatMarkers.MARKER_RECEIVED:
if (stanza != null && Xep.ChatMarkers.Module.requests_marking(stanza) && message.type_ != Message.Type.GROUPCHAT) {
if (message.stanza_id == null) return;
stream.get_module(Xep.ChatMarkers.Module.IDENTITY).send_marker(stream, message.from, message.stanza_id, message.get_type_string(), Xep.ChatMarkers.MARKER_RECEIVED);
}
break;
case Xep.ChatMarkers.MARKER_DISPLAYED:
if (conversation.get_send_marker_setting(stream_interactor) == Conversation.Setting.ON) {
if (message.equals(conversation.read_up_to)) return;
conversation.read_up_to = message;
if (message.type_ == Message.Type.GROUPCHAT || message.type_ == Message.Type.GROUPCHAT_PM) {
if (message.server_id == null) return;
stream.get_module(Xep.ChatMarkers.Module.IDENTITY).send_marker(stream, message.from.bare_jid, message.server_id, message.get_type_string(), Xep.ChatMarkers.MARKER_DISPLAYED);
} else {
if (message.stanza_id == null) return;
stream.get_module(Xep.ChatMarkers.Module.IDENTITY).send_marker(stream, message.from, message.stanza_id, message.get_type_string(), Xep.ChatMarkers.MARKER_DISPLAYED);
}
}
break;
}
}
private void send_delivery_receipt(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
if (message.direction == Entities.Message.DIRECTION_SENT) return;
if (!Xep.MessageDeliveryReceipts.Module.requests_receipt(stanza)) return;
if (conversation.type_ == Conversation.Type.GROUPCHAT) return;
XmppStream? stream = stream_interactor.get_stream(conversation.account);
if (stream != null) {
stream.get_module(Xep.MessageDeliveryReceipts.Module.IDENTITY).send_received(stream, message.from, message.stanza_id);
}
}
private void send_chat_state_notification(Conversation conversation, string state) {
if (conversation.get_send_typing_setting(stream_interactor) != Conversation.Setting.ON) return;
XmppStream? stream = stream_interactor.get_stream(conversation.account);
if (stream != null) {
string message_type = conversation.type_ == Conversation.Type.GROUPCHAT ? Xmpp.MessageStanza.TYPE_GROUPCHAT : Xmpp.MessageStanza.TYPE_CHAT;
stream.get_module(Xep.ChatStateNotifications.Module.IDENTITY).send_state(stream, conversation.counterpart, message_type, state);
}
}
}
}
dino-0.5.0/libdino/src/service/connection_manager.vala 0000664 0000000 0000000 00000034050 14776241610 021524 0 ustar root root using Gee;
using Xmpp;
using Dino.Entities;
namespace Dino {
public class ConnectionManager : Object {
public signal void stream_opened(Account account, XmppStream stream);
public signal void stream_attached_modules(Account account, XmppStream stream);
public signal void connection_state_changed(Account account, ConnectionState state);
public signal void connection_error(Account account, ConnectionError error);
public enum ConnectionState {
CONNECTED,
CONNECTING,
DISCONNECTED
}
private HashMap connections = new HashMap(Account.hash_func, Account.equals_func);
private HashMap connection_errors = new HashMap(Account.hash_func, Account.equals_func);
private HashMap connection_ongoing = new HashMap(Account.hash_func, Account.equals_func);
private HashMap connection_directly_retry = new HashMap(Account.hash_func, Account.equals_func);
private NetworkMonitor? network_monitor;
private Login1Manager? login1;
private ModuleManager module_manager;
public string? log_options;
public class ConnectionError {
public enum Source {
CONNECTION,
SASL,
TLS,
STREAM_ERROR
}
public enum Reconnect {
NOW,
LATER,
NEVER
}
public Source source;
public string? identifier;
public Reconnect reconnect_recomendation { get; set; default=Reconnect.NOW; }
public ConnectionError(Source source, string? identifier) {
this.source = source;
this.identifier = identifier;
}
}
private class Connection {
public string uuid { get; set; }
public XmppStream? stream { get; set; }
public ConnectionState connection_state { get; set; default = ConnectionState.DISCONNECTED; }
public DateTime? established { get; set; }
public DateTime? last_activity { get; set; }
public Connection() {
reset();
}
public void reset() {
if (stream != null) {
stream.detach_modules();
stream.disconnect.begin();
}
stream = null;
established = last_activity = null;
uuid = Xmpp.random_uuid();
}
public void make_offline() {
Xmpp.Presence.Stanza presence = new Xmpp.Presence.Stanza();
presence.type_ = Xmpp.Presence.Stanza.TYPE_UNAVAILABLE;
if (stream != null) {
stream.get_module(Presence.Module.IDENTITY).send_presence(stream, presence);
}
}
public async void disconnect_account() {
make_offline();
if (stream != null) {
try {
yield stream.disconnect();
} catch (Error e) {
debug("Error disconnecting stream: %s", e.message);
}
}
}
}
public ConnectionManager(ModuleManager module_manager) {
this.module_manager = module_manager;
network_monitor = GLib.NetworkMonitor.get_default();
if (network_monitor != null) {
network_monitor.network_changed.connect(on_network_changed);
network_monitor.notify["connectivity"].connect(on_network_changed);
}
get_login1.begin((_, res) => {
login1 = get_login1.end(res);
if (login1 != null) {
login1.PrepareForSleep.connect(on_prepare_for_sleep);
}
});
Timeout.add_seconds(60, () => {
foreach (Account account in connections.keys) {
if (connections[account].last_activity == null ||
connections[account].last_activity.compare(new DateTime.now_utc().add_minutes(-1)) < 0) {
check_reconnect(account);
}
}
return true;
});
}
public XmppStream? get_stream(Account account) {
if (get_state(account) == ConnectionState.CONNECTED) {
return connections[account].stream;
}
return null;
}
public ConnectionState get_state(Account account) {
if (connections.has_key(account)){
return connections[account].connection_state;
}
return ConnectionState.DISCONNECTED;
}
public ConnectionError? get_error(Account account) {
if (connection_errors.has_key(account)) {
return connection_errors[account];
}
return null;
}
public Collection get_managed_accounts() {
return connections.keys;
}
public void connect_account(Account account) {
if (!connections.has_key(account)) {
connections[account] = new Connection();
connection_ongoing[account] = false;
connection_directly_retry[account] = false;
connect_stream.begin(account);
} else {
check_reconnect(account);
}
}
public void make_offline_all() {
foreach (Account account in connections.keys) {
make_offline(account);
}
}
private void make_offline(Account account) {
connections[account].make_offline();
change_connection_state(account, ConnectionState.DISCONNECTED);
}
public async void disconnect_account(Account account) {
if (connections.has_key(account)) {
make_offline(account);
connections[account].disconnect_account.begin();
connections.unset(account);
}
}
private async void connect_stream(Account account) {
if (!connections.has_key(account)) return;
debug("[%s] (Maybe) Establishing a new connection", account.bare_jid.to_string());
connection_errors.unset(account);
XmppStreamResult stream_result;
if (connection_ongoing[account]) {
debug("[%s] Connection attempt already in progress. Directly retry if it fails.", account.bare_jid.to_string());
connection_directly_retry[account] = true;
return;
} else if (connections[account].stream != null) {
debug("[%s] Cancelling connecting because there is already a stream", account.bare_jid.to_string());
return;
} else {
connection_ongoing[account] = true;
connection_directly_retry[account] = false;
change_connection_state(account, ConnectionState.CONNECTING);
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); }
);
connections[account].stream = stream_result.stream;
connection_ongoing[account] = false;
}
if (stream_result.stream == null) {
if (stream_result.tls_errors != null) {
set_connection_error(account, new ConnectionError(ConnectionError.Source.TLS, null) { reconnect_recomendation=ConnectionError.Reconnect.NEVER});
return;
}
debug("[%s] Could not connect", account.bare_jid.to_string());
change_connection_state(account, ConnectionState.DISCONNECTED);
check_reconnect(account, connection_directly_retry[account]);
return;
}
XmppStream stream = stream_result.stream;
debug("[%s] New connection: %p", account.full_jid.to_string(), stream);
connections[account].established = new DateTime.now_utc();
stream.attached_modules.connect((stream) => {
stream_attached_modules(account, stream);
change_connection_state(account, ConnectionState.CONNECTED);
// stream.get_module(Xep.Muji.Module.IDENTITY).join_call(stream, new Jid("test@muc.poez.io"), true);
});
stream.get_module(Sasl.Module.IDENTITY).received_auth_failure.connect((stream, node) => {
set_connection_error(account, new ConnectionError(ConnectionError.Source.SASL, null));
});
string connection_uuid = connections[account].uuid;
stream.received_node.connect(() => {
if (connections[account].uuid == connection_uuid) {
connections[account].last_activity = new DateTime.now_utc();
} else {
warning("Got node for outdated connection");
}
});
stream_opened(account, stream);
try {
yield stream.loop();
} catch (Error e) {
debug("[%s %p] Connection error: %s", account.bare_jid.to_string(), stream, e.message);
change_connection_state(account, ConnectionState.DISCONNECTED);
if (!connections.has_key(account)) return;
connections[account].reset();
StreamError.Flag? flag = stream.get_flag(StreamError.Flag.IDENTITY);
if (flag != null) {
warning(@"[%s %p] Stream Error: %s", account.bare_jid.to_string(), stream, flag.error_type);
set_connection_error(account, new ConnectionError(ConnectionError.Source.STREAM_ERROR, flag.error_type));
if (flag.resource_rejected) {
account.set_random_resource();
connect_stream.begin(account);
return;
}
}
ConnectionError? error = connection_errors[account];
if (error != null && error.source == ConnectionError.Source.SASL) {
return;
}
check_reconnect(account);
}
}
private void check_reconnects() {
foreach (Account account in connections.keys) {
check_reconnect(account);
}
}
private void check_reconnect(Account account, bool directly_reconnect = false) {
if (!connections.has_key(account)) return;
bool acked = false;
DateTime? last_activity_was = connections[account].last_activity;
if (connections[account].stream == null) {
Timeout.add_seconds(10, () => {
if (!connections.has_key(account)) return false;
if (connections[account].stream != null) return false;
if (connections[account].last_activity != last_activity_was) return false;
connect_stream.begin(account);
return false;
});
return;
}
XmppStream stream = connections[account].stream;
stream.get_module(Xep.Ping.Module.IDENTITY).send_ping.begin(stream, account.bare_jid.domain_jid, () => {
acked = true;
if (connections[account].stream != stream) return;
change_connection_state(account, ConnectionState.CONNECTED);
});
Timeout.add_seconds(10, () => {
if (!connections.has_key(account)) return false;
if (connections[account].stream != stream) return false;
if (acked) return false;
if (connections[account].last_activity != last_activity_was) return false;
// Reconnect. Nothing gets through the stream.
debug("[%s %p] Ping timeouted. Reconnecting", account.bare_jid.to_string(), stream);
change_connection_state(account, ConnectionState.DISCONNECTED);
connections[account].reset();
connect_stream.begin(account);
return false;
});
}
private bool network_is_online() {
/* FIXME: We should also check for connectivity eventually. For more
* details on why we don't do it for now, see:
*
* - https://github.com/dino/dino/pull/236#pullrequestreview-86851793
* - https://bugzilla.gnome.org/show_bug.cgi?id=792240
*/
return network_monitor != null && network_monitor.network_available;
}
private void on_network_changed() {
if (network_is_online()) {
debug("NetworkMonitor: Network reported online");
check_reconnects();
} else {
debug("NetworkMonitor: Network reported offline");
foreach (Account account in connections.keys) {
change_connection_state(account, ConnectionState.DISCONNECTED);
}
}
}
private async void on_prepare_for_sleep(bool suspend) {
foreach (Account account in connections.keys) {
change_connection_state(account, ConnectionState.DISCONNECTED);
}
if (suspend) {
debug("Login1: Device suspended");
foreach (Account account in connections.keys) {
try {
make_offline(account);
if (connections[account].stream != null) {
yield connections[account].stream.disconnect();
}
} catch (Error e) {
debug("Error disconnecting stream %p: %s", connections[account].stream, e.message);
}
}
} else {
debug("Login1: Device un-suspend");
check_reconnects();
}
}
private void change_connection_state(Account account, ConnectionState state) {
if (connections.has_key(account)) {
connections[account].connection_state = state;
connection_state_changed(account, state);
}
}
private void set_connection_error(Account account, ConnectionError error) {
connection_errors[account] = error;
connection_error(account, error);
}
public static bool on_invalid_certificate(string domain, TlsCertificate peer_cert, TlsCertificateFlags errors) {
if (domain.has_suffix(".onion") && errors == TlsCertificateFlags.UNKNOWN_CA) {
// It's barely possible for .onion servers to provide a non-self-signed cert.
// But that's fine because encryption is provided independently though TOR.
warning("Accepting TLS certificate from unknown CA from .onion address %s", domain);
return true;
}
return false;
}
}
}
dino-0.5.0/libdino/src/service/contact_model.vala 0000664 0000000 0000000 00000005504 14776241610 020510 0 ustar root root 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 IDENTITY = new ModuleIdentity("contact_models");
public string id { get { return IDENTITY.id; } }
private StreamInteractor stream_interactor;
private HashMap conversation_models = new HashMap(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;
}
}
} dino-0.5.0/libdino/src/service/content_item_store.vala 0000664 0000000 0000000 00000042666 14776241610 021613 0 ustar root root using Gee;
using Dino.Entities;
using Qlite;
using Xmpp;
namespace Dino {
public class ContentItemStore : StreamInteractionModule, Object {
public static ModuleIdentity IDENTITY = new ModuleIdentity("content_item_store");
public string id { get { return IDENTITY.id; } }
public signal void new_item(ContentItem item, Conversation conversation);
private StreamInteractor stream_interactor;
private Database db;
private HashMap collection_conversations = new HashMap(Conversation.hash_func, Conversation.equals_func);
public static void start(StreamInteractor stream_interactor, Database db) {
ContentItemStore m = new ContentItemStore(stream_interactor, db);
stream_interactor.add_module(m);
}
public ContentItemStore(StreamInteractor stream_interactor, Database db) {
this.stream_interactor = stream_interactor;
this.db = db;
stream_interactor.get_module(FileManager.IDENTITY).received_file.connect(insert_file_transfer);
stream_interactor.get_module(MessageProcessor.IDENTITY).message_received.connect(announce_message);
stream_interactor.get_module(MessageProcessor.IDENTITY).message_sent.connect(announce_message);
stream_interactor.get_module(Calls.IDENTITY).call_incoming.connect(insert_call);
stream_interactor.get_module(Calls.IDENTITY).call_outgoing.connect(insert_call);
}
public void init(Conversation conversation, ContentItemCollection item_collection) {
collection_conversations[conversation] = item_collection;
}
public void uninit(Conversation conversation, ContentItemCollection item_collection) {
collection_conversations.unset(conversation);
}
private Gee.List get_items_from_query(QueryBuilder select, Conversation conversation) {
Gee.TreeSet items = new Gee.TreeSet(ContentItem.compare_func);
foreach (var row in select) {
ContentItem content_item = get_item_from_row(row, conversation);
items.add(content_item);
}
Gee.List ret = new ArrayList();
foreach (ContentItem item in items) {
ret.add(item);
}
return ret;
}
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()
.with(db.content_item.content_type, "=", type)
.with(db.content_item.foreign_id, "=", foreign_id);
Gee.List item = get_items_from_query(select, conversation);
return item.size > 0 ? item[0] : null;
}
public ContentItem? get_item_by_id(Conversation conversation, int id) {
QueryBuilder select = db.content_item.select()
.with(db.content_item.id, "=", id);
Gee.List item = get_items_from_query(select, conversation);
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) {
Gee.List items = get_n_latest(conversation, 1);
if (items.size > 0) {
return items.get(0);
}
return null;
}
public Gee.List get_n_latest(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);
return get_items_from_query(select, conversation);
}
// public Gee.List 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();
// 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 get_before(Conversation conversation, ContentItem item, int count) {
long time = (long) item.time.to_unix();
QueryBuilder select = db.content_item.select()
.where(@"time < ? OR (time = ? AND id < ?)", { time.to_string(), time.to_string(), item.id.to_string() })
.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);
return get_items_from_query(select, conversation);
}
public Gee.List get_after(Conversation conversation, ContentItem item, int count) {
long time = (long) item.time.to_unix();
QueryBuilder select = db.content_item.select()
.where(@"time > ? OR (time = ? AND id > ?)", { time.to_string(), time.to_string(), item.id.to_string() })
.with(db.content_item.conversation_id, "=", conversation.id)
.with(db.content_item.hide, "=", false)
.order_by(db.content_item.time, "ASC")
.order_by(db.content_item.id, "ASC")
.limit(count);
return get_items_from_query(select, conversation);
}
public void insert_message(Message message, Conversation conversation, bool hide = false) {
MessageItem item = new MessageItem(message, conversation, -1);
item.id = db.add_content_item(conversation, message.time, message.local_time, 1, message.id, hide);
}
private void announce_message(Message message, Conversation conversation) {
QueryBuilder select = db.content_item.select();
select.with(db.content_item.foreign_id, "=", message.id);
select.with(db.content_item.content_type, "=", 1);
select.with(db.content_item.hide, "=", false);
foreach (Row row in select) {
MessageItem item = new MessageItem(message, conversation, row[db.content_item.id]);
if (collection_conversations.has_key(conversation)) {
collection_conversations.get(conversation).insert_item(item);
}
new_item(item, conversation);
break;
}
}
private void insert_file_transfer(FileTransfer file_transfer, Conversation conversation) {
FileItem item = new FileItem(file_transfer, conversation, -1);
item.id = db.add_content_item(conversation, file_transfer.time, file_transfer.local_time, 2, file_transfer.id, false);
if (collection_conversations.has_key(conversation)) {
collection_conversations.get(conversation).insert_item(item);
}
new_item(item, conversation);
}
private void insert_call(Call call, CallState call_state, Conversation conversation) {
CallItem item = new CallItem(call, conversation, -1);
item.id = db.add_content_item(conversation, call.time, call.local_time, 3, call.id, false);
if (collection_conversations.has_key(conversation)) {
collection_conversations.get(conversation).insert_item(item);
}
new_item(item, conversation);
}
public bool get_item_hide(ContentItem content_item) {
return db.content_item.row_with(db.content_item.id, content_item.id)[db.content_item.hide, false];
}
public void set_item_hide(ContentItem content_item, bool hide) {
db.content_item.update()
.with(db.content_item.id, "=", content_item.id)
.set(db.content_item.hide, hide)
.perform();
}
}
public interface ContentItemCollection : Object {
public abstract void insert_item(ContentItem item);
public abstract void remove_item(ContentItem item);
}
public abstract class ContentItem : Object {
public int id { get; set; }
public string type_ { get; set; }
public Jid jid { get; set; }
public DateTime time { get; set; }
public Encryption encryption { get; set; }
public Entities.Message.Marked mark { get; set; }
ContentItem(int id, string ty, Jid jid, DateTime time, Encryption encryption, Entities.Message.Marked mark) {
this.id = id;
this.type_ = ty;
this.jid = jid;
this.time = time;
this.encryption = encryption;
this.mark = mark;
}
public int compare(ContentItem c) {
return compare_func(this, c);
}
public static int compare_func(ContentItem a, ContentItem b) {
int res = a.time.compare(b.time);
if (res == 0) {
res = a.id - b.id > 0 ? 1 : -1;
}
return res;
}
}
public class MessageItem : ContentItem {
public const string TYPE = "message";
public Message message;
public Conversation conversation;
public MessageItem(Message message, Conversation conversation, int id) {
base(id, TYPE, message.from, message.time, message.encryption, message.marked);
this.message = message;
this.conversation = conversation;
message.bind_property("marked", this, "mark");
}
}
public class FileItem : ContentItem {
public const string TYPE = "file";
public FileTransfer file_transfer;
public Conversation conversation;
public FileItem(FileTransfer file_transfer, Conversation conversation, int id, Message? message = null) {
Entities.Message.Marked mark = Entities.Message.Marked.NONE;
if (message != null) {
mark = message.marked;
} else if (file_transfer.direction == FileTransfer.DIRECTION_SENT) {
mark = file_to_message_state(file_transfer.state);
}
base(id, TYPE, file_transfer.from, file_transfer.time, file_transfer.encryption, mark);
this.file_transfer = file_transfer;
this.conversation = conversation;
// TODO those don't work
if (message != null) {
message.bind_property("marked", this, "mark");
} else if (file_transfer.direction == FileTransfer.DIRECTION_SENT) {
file_transfer.bind_property("state", this, "mark", BindingFlags.DEFAULT, (_, from_value, ref to_value) => {
to_value = file_to_message_state((FileTransfer.State)from_value.get_enum());
return true;
});
}
}
private static Entities.Message.Marked file_to_message_state(FileTransfer.State state) {
switch (state) {
case FileTransfer.State.IN_PROGRESS:
return Entities.Message.Marked.UNSENT;
case FileTransfer.State.COMPLETE:
return Entities.Message.Marked.NONE;
case FileTransfer.State.NOT_STARTED:
return Entities.Message.Marked.UNSENT;
case FileTransfer.State.FAILED:
return Entities.Message.Marked.WONTSEND;
}
assert_not_reached();
}
}
public class CallItem : ContentItem {
public const string TYPE = "call";
public Call call;
public Conversation conversation;
public CallItem(Call call, Conversation conversation, int id) {
base(id, TYPE, call.proposer, call.time, call.encryption, Message.Marked.NONE);
this.call = call;
this.conversation = conversation;
call.bind_property("encryption", this, "encryption");
}
}
}
dino-0.5.0/libdino/src/service/conversation_manager.vala 0000664 0000000 0000000 00000023114 14776241610 022076 0 ustar root root using Gee;
using Xmpp;
using Dino.Entities;
namespace Dino {
public class ConversationManager : StreamInteractionModule, Object {
public static ModuleIdentity IDENTITY = new ModuleIdentity("conversation_manager");
public string id { get { return IDENTITY.id; } }
public signal void conversation_activated(Conversation conversation);
public signal void conversation_deactivated(Conversation conversation);
private StreamInteractor stream_interactor;
private Database db;
private HashMap>> conversations = new HashMap>>(Account.hash_func, Account.equals_func);
public static void start(StreamInteractor stream_interactor, Database db) {
ConversationManager m = new ConversationManager(stream_interactor, db);
stream_interactor.add_module(m);
}
private ConversationManager(StreamInteractor stream_interactor, Database db) {
this.db = db;
this.stream_interactor = stream_interactor;
stream_interactor.add_module(this);
stream_interactor.account_added.connect(on_account_added);
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).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) {
assert(conversations.has_key(account));
Jid store_jid = type == Conversation.Type.GROUPCHAT ? jid.bare_jid : jid;
// Do we already have a conversation for this jid?
if (conversations[account].has_key(store_jid)) {
foreach (var conversation in conversations[account][store_jid]) {
if (conversation.type_ == type) {
return conversation;
}
}
}
// Create a new converation
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);
conversation.persist(db);
return conversation;
}
public Conversation? get_conversation_for_message(Entities.Message message) {
if (conversations.has_key(message.account)) {
if (message.type_ == Entities.Message.Type.CHAT) {
return create_conversation(message.counterpart.bare_jid, message.account, Conversation.Type.CHAT);
} else if (message.type_ == Entities.Message.Type.GROUPCHAT) {
return create_conversation(message.counterpart.bare_jid, message.account, Conversation.Type.GROUPCHAT);
} else if (message.type_ == Entities.Message.Type.GROUPCHAT_PM) {
return create_conversation(message.counterpart, message.account, Conversation.Type.GROUPCHAT_PM);
}
}
return null;
}
public Gee.List get_conversations(Jid jid, Account account) {
Gee.List ret = new ArrayList(Conversation.equals_func);
Conversation? bare_conversation = get_conversation(jid, account);
if (bare_conversation != null) ret.add(bare_conversation);
Conversation? full_conversation = get_conversation(jid.bare_jid, account);
if (full_conversation != null) ret.add(full_conversation);
return ret;
}
public Conversation? get_conversation(Jid jid, Account account, Conversation.Type? type = null) {
if (conversations.has_key(account)) {
if (conversations[account].has_key(jid)) {
foreach (var conversation in conversations[account][jid]) {
if (type == null || conversation.type_ == type) {
return conversation;
}
}
}
}
return null;
}
public Conversation? approx_conversation_for_stanza(Jid from, Jid to, Account account, string msg_ty) {
if (msg_ty == Xmpp.MessageStanza.TYPE_GROUPCHAT) {
return get_conversation(from.bare_jid, account, Conversation.Type.GROUPCHAT);
}
Jid counterpart = from.equals_bare(account.bare_jid) ? to : from;
if (msg_ty == Xmpp.MessageStanza.TYPE_CHAT && counterpart.is_full() &&
get_conversation(counterpart.bare_jid, account, Conversation.Type.GROUPCHAT) != null) {
var pm = get_conversation(counterpart, account, Conversation.Type.GROUPCHAT_PM);
if (pm != null) return pm;
}
return get_conversation(counterpart.bare_jid, account, Conversation.Type.CHAT);
}
public Conversation? get_conversation_by_id(int id) {
foreach (HashMap> hm in conversations.values) {
foreach (Gee.List hm2 in hm.values) {
foreach (Conversation conversation in hm2) {
if (conversation.id == id) {
return conversation;
}
}
}
}
return null;
}
public Gee.List get_active_conversations(Account? account = null) {
Gee.List ret = new ArrayList(Conversation.equals_func);
foreach (Account account_ in conversations.keys) {
if (account != null && !account_.equals(account)) continue;
foreach (Gee.List list in conversations[account_].values) {
foreach (var conversation in list) {
if(conversation.active) ret.add(conversation);
}
}
}
return ret;
}
public void start_conversation(Conversation conversation) {
if (conversation.last_active == null) {
conversation.last_active = new DateTime.now_utc();
if (conversation.active) conversation_activated(conversation);
}
if (!conversation.active) {
conversation.active = true;
conversation_activated(conversation);
}
}
public void close_conversation(Conversation conversation) {
if (!conversation.active) return;
conversation.active = false;
conversation_deactivated(conversation);
}
private void on_account_added(Account account) {
conversations[account] = new HashMap>(Jid.hash_func, Jid.equals_func);
foreach (Conversation conversation in db.get_conversations(account)) {
add_conversation(conversation);
}
}
private void on_account_removed(Account account) {
foreach (Gee.List list in conversations[account].values) {
foreach (var conversation in list) {
if(conversation.active) conversation_deactivated(conversation);
}
}
conversations.unset(account);
}
private class MessageListener : Dino.MessageListener {
public string[] after_actions_const = new string[]{ "DEDUPLICATE", "FILTER_EMPTY" };
public override string action_group { get { return "MANAGER"; } }
public override string[] after_actions { get { return after_actions_const; } }
private StreamInteractor stream_interactor;
public MessageListener(StreamInteractor stream_interactor) {
this.stream_interactor = stream_interactor;
}
public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
conversation.last_active = message.time;
if (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;
if (is_mam_message && !is_recent) return false;
}
stream_interactor.get_module(ConversationManager.IDENTITY).start_conversation(conversation);
return false;
}
}
private void handle_sent_message(Entities.Message message, Conversation conversation) {
conversation.last_active = message.time;
bool is_recent = message.time.compare(new DateTime.now_utc().add_hours(-24)) > 0;
if (is_recent) {
start_conversation(conversation);
}
}
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) {
if (!conversations[conversation.account].has_key(conversation.counterpart)) {
conversations[conversation.account][conversation.counterpart] = new ArrayList(Conversation.equals_func);
}
conversations[conversation.account][conversation.counterpart].add(conversation);
if (conversation.active) {
conversation_activated(conversation);
}
}
}
}
dino-0.5.0/libdino/src/service/counterpart_interaction_manager.vala 0000664 0000000 0000000 00000027103 14776241610 024333 0 ustar root root using Gee;
using Xmpp;
using Dino.Entities;
namespace Dino {
public class CounterpartInteractionManager : StreamInteractionModule, Object {
public static ModuleIdentity IDENTITY = new ModuleIdentity("counterpart_interaction_manager");
public string id { get { return IDENTITY.id; } }
public signal void received_state(Conversation conversation, string state);
public signal void received_marker(Account account, Jid jid, Entities.Message message, Entities.Message.Marked marker);
public signal void received_message_received(Account account, Jid jid, Entities.Message message);
public signal void received_message_displayed(Account account, Jid jid, Entities.Message message);
private StreamInteractor stream_interactor;
private HashMap> typing_since = new HashMap>(Conversation.hash_func, Conversation.equals_func);
private HashMap marker_wo_message = new HashMap();
public static void start(StreamInteractor stream_interactor) {
CounterpartInteractionManager m = new CounterpartInteractionManager(stream_interactor);
stream_interactor.add_module(m);
}
private CounterpartInteractionManager(StreamInteractor stream_interactor) {
this.stream_interactor = stream_interactor;
stream_interactor.account_added.connect(on_account_added);
stream_interactor.get_module(MessageProcessor.IDENTITY).message_received.connect((message, conversation) => clear_chat_state(conversation, message.from));
stream_interactor.get_module(MessageProcessor.IDENTITY).message_sent_or_received.connect(check_if_got_marker);
stream_interactor.get_module(PresenceManager.IDENTITY).received_offline_presence.connect((jid, account) => {
foreach (Conversation conversation in stream_interactor.get_module(ConversationManager.IDENTITY).get_conversations(jid, account)) {
clear_chat_state(conversation, jid);
}
});
stream_interactor.stream_negotiated.connect((account) => clear_all_chat_states(account) );
Timeout.add_seconds(60, () => {
var one_min_ago = new DateTime.now_utc().add_seconds(-1);
foreach (Conversation conversation in typing_since.keys) {
ArrayList to_remove = new ArrayList();
foreach (Jid jid in typing_since[conversation].keys) {
if (typing_since[conversation][jid].compare(one_min_ago) < 0) {
to_remove.add(jid);
}
}
foreach (Jid jid in to_remove) {
clear_chat_state(conversation, jid);
}
}
return true;
});
}
public Gee.List? get_typing_jids(Conversation conversation) {
if (stream_interactor.connection_manager.get_state(conversation.account) != ConnectionManager.ConnectionState.CONNECTED) return null;
if (!typing_since.has_key(conversation) || typing_since[conversation].size == 0) return null;
var jids = new ArrayList();
foreach (Jid jid in typing_since[conversation].keys) {
jids.add(jid);
}
return jids;
}
private void on_account_added(Account account) {
stream_interactor.module_manager.get_module(account, Xep.ChatMarkers.Module.IDENTITY).marker_received.connect( (stream, jid, marker, id, message_stanza) => {
on_chat_marker_received.begin(account, jid, marker, id, message_stanza);
});
stream_interactor.module_manager.get_module(account, Xep.MessageDeliveryReceipts.Module.IDENTITY).receipt_received.connect((stream, jid, id, stanza) => {
on_receipt_received(account, jid, id, stanza);
});
stream_interactor.module_manager.get_module(account, Xep.ChatStateNotifications.Module.IDENTITY).chat_state_received.connect((stream, jid, state, stanza) => {
on_chat_state_received.begin(account, jid, state, stanza);
});
}
private void clear_chat_state(Conversation conversation, Jid jid) {
if (!(typing_since.has_key(conversation) && typing_since[conversation].has_key(jid))) return;
typing_since[conversation].unset(jid);
received_state(conversation, Xmpp.Xep.ChatStateNotifications.STATE_ACTIVE);
}
private void clear_all_chat_states(Account account) {
foreach (Conversation conversation in typing_since.keys) {
if (conversation.account.equals(account)) {
received_state(conversation, Xmpp.Xep.ChatStateNotifications.STATE_ACTIVE);
typing_since[conversation].clear();
}
}
}
private async void on_chat_state_received(Account account, Jid jid, string state, MessageStanza stanza) {
// Don't show our own (other devices) typing notification
if (jid.equals_bare(account.bare_jid)) return;
Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).approx_conversation_for_stanza(stanza.from, stanza.to, account, stanza.type_);
if (conversation == null) return;
// Don't show our own typing notification in MUCs
if (conversation.type_ == Conversation.Type.GROUPCHAT) {
Jid? own_muc_jid = stream_interactor.get_module(MucManager.IDENTITY).get_own_jid(jid.bare_jid, account);
if (own_muc_jid != null && own_muc_jid.equals(jid)) {
return;
}
}
if (!typing_since.has_key(conversation)) {
typing_since[conversation] = new HashMap(Jid.hash_func, Jid.equals_func);
}
if (state == Xmpp.Xep.ChatStateNotifications.STATE_COMPOSING) {
typing_since[conversation][jid] = new DateTime.now_utc();
received_state(conversation, state);
} else {
clear_chat_state(conversation, jid);
}
}
private async void on_chat_marker_received(Account account, Jid jid, string marker, string stanza_id, MessageStanza message_stanza) {
Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).approx_conversation_for_stanza(message_stanza.from, message_stanza.to, account, message_stanza.type_);
if (conversation == null) return;
handle_chat_marker(conversation, jid, marker, stanza_id);
}
private void handle_chat_marker(Conversation conversation, Jid jid, string marker, string stanza_id) {
// Check if the marker comes from ourselves (own jid or our jid in a MUC)
bool own_marker = false;
if (conversation.type_ == Conversation.Type.CHAT) {
own_marker = conversation.account.bare_jid.to_string() == jid.bare_jid.to_string();
} else {
Jid? own_muc_jid = stream_interactor.get_module(MucManager.IDENTITY).get_own_jid(jid.bare_jid, conversation.account);
if (own_muc_jid != null && own_muc_jid.equals(jid)) {
own_marker = true;
}
}
if (own_marker) {
// If we received a display marker from ourselves (other device), set the conversation read up to that message.
if (marker != Xep.ChatMarkers.MARKER_DISPLAYED && marker != Xep.ChatMarkers.MARKER_ACKNOWLEDGED) return;
Entities.Message? message = null;
if (conversation.type_ == Conversation.Type.GROUPCHAT || conversation.type_ == Conversation.Type.GROUPCHAT_PM) {
message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_server_id(stanza_id, conversation);
// Outdated clients might use the message id. Or in MUCs that don't send server ids.
if (message == null) {
message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_stanza_id(stanza_id, conversation);
}
} else {
message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_stanza_id(stanza_id, conversation);
}
if (message == null) return;
// Don't move read marker backwards because we get old info from another client
if (conversation.read_up_to != null && conversation.read_up_to.local_time.compare(message.local_time) > 0) return;
conversation.read_up_to = message;
// TODO: This only marks messages as read, not http file transfers.
ContentItem? content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item_by_foreign(conversation, 1, message.id);
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);
if (read_up_to_item != null && read_up_to_item.compare(content_item) > 0) return;
conversation.read_up_to_item = content_item.id;
} else {
// We can't currently handle chat markers in MUCs
if (conversation.type_ == Conversation.Type.GROUPCHAT) return;
Entities.Message? message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_stanza_id(stanza_id, conversation);
if (message != null) {
switch (marker) {
case Xep.ChatMarkers.MARKER_RECEIVED:
// If we got a received marker, mark the respective message received.
received_message_received(conversation.account, jid, message);
message.marked = Entities.Message.Marked.RECEIVED;
break;
case Xep.ChatMarkers.MARKER_DISPLAYED:
// If we got a display marker, set all messages up to that message as read (if we know they've been received).
received_message_displayed(conversation.account, jid, message);
Gee.List messages = stream_interactor.get_module(MessageStorage.IDENTITY).get_messages(conversation);
foreach (Entities.Message m in messages) {
if (m.equals(message)) break;
if (m.marked == Entities.Message.Marked.RECEIVED) m.marked = Entities.Message.Marked.READ;
}
message.marked = Entities.Message.Marked.READ;
break;
}
} else {
// We might get a marker before the actual message (on catchup). Save the marker.
if (marker_wo_message.has_key(stanza_id) &&
marker_wo_message[stanza_id] == Xep.ChatMarkers.MARKER_DISPLAYED && marker == Xep.ChatMarkers.MARKER_RECEIVED) {
return;
}
marker_wo_message[stanza_id] = marker;
}
}
}
private void check_if_got_marker(Entities.Message message, Conversation conversation) {
if (marker_wo_message.has_key(message.stanza_id)) {
handle_chat_marker(conversation, conversation.counterpart, marker_wo_message[message.stanza_id], message.stanza_id);
marker_wo_message.unset(message.stanza_id);
}
}
private void on_receipt_received(Account account, Jid jid, string id, MessageStanza stanza) {
Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).approx_conversation_for_stanza(stanza.from, stanza.to, account, stanza.type_);
if (conversation == null) return;
handle_chat_marker(conversation, jid,Xep.ChatMarkers.MARKER_RECEIVED, id);
}
}
}
dino-0.5.0/libdino/src/service/database.vala 0000664 0000000 0000000 00000114755 14776241610 017452 0 ustar root root using Gee;
using Qlite;
using Xmpp;
using Dino.Entities;
namespace Dino {
public class Database : Qlite.Database {
private const int VERSION = 29;
public class AccountTable : Table {
public Column id = new Column.Integer("id") { primary_key = true, auto_increment = true };
public Column bare_jid = new Column.Text("bare_jid") { unique = true, not_null = true };
public Column resourcepart = new Column.Text("resourcepart");
public Column password = new Column.Text("password");
public Column alias = new Column.Text("alias");
public Column enabled = new Column.BoolInt("enabled");
public Column 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 mam_earliest_synced = new Column.Long("mam_earliest_synced") { min_version=4 };
internal AccountTable(Database db) {
base(db, "account");
init({id, bare_jid, resourcepart, password, alias, enabled, roster_version, mam_earliest_synced});
}
}
public class JidTable : Table {
public Column id = new Column.Integer("id") { primary_key = true, auto_increment = true };
public Column bare_jid = new Column.Text("bare_jid") { unique = true, not_null = true };
internal JidTable(Database db) {
base(db, "jid");
init({id, bare_jid});
}
}
public class EntityTable : Table {
public Column id = new Column.Integer("id") { primary_key = true, auto_increment = true };
public Column account_id = new Column.Integer("account_id");
public Column jid_id = new Column.Integer("jid_id");
public Column resource = new Column.Text("resource");
public Column caps_hash = new Column.Text("caps_hash");
public Column last_seen = new Column.Long("last_seen");
internal EntityTable(Database db) {
base(db, "entity");
init({id, account_id, jid_id, resource, caps_hash, last_seen});
unique({account_id, jid_id, resource}, "IGNORE");
}
}
public class ContentItemTable : Table {
public Column id = new Column.Integer("id") { primary_key = true, auto_increment = true };
public Column conversation_id = new Column.Integer("conversation_id") { not_null = true };
public Column time = new Column.Long("time") { not_null = true };
public Column local_time = new Column.Long("local_time") { not_null = true };
public Column content_type = new Column.Integer("content_type") { not_null = true };
public Column foreign_id = new Column.Integer("foreign_id") { not_null = true };
public Column hide = new Column.BoolInt("hide") { default = "0", not_null = true, min_version = 9 };
internal ContentItemTable(Database db) {
base(db, "content_item");
init({id, conversation_id, time, local_time, content_type, foreign_id, hide});
index("contentitem_conversation_hide_time_idx", {conversation_id, hide, time});
unique({content_type, foreign_id}, "IGNORE");
}
}
public class MessageTable : Table {
public Column id = new Column.Integer("id") { primary_key = true, auto_increment = true };
public Column stanza_id = new Column.Text("stanza_id");
public Column server_id = new Column.Text("server_id") { min_version=10 };
public Column account_id = new Column.Integer("account_id") { not_null = true };
public Column counterpart_id = new Column.Integer("counterpart_id") { not_null = true };
public Column counterpart_resource = new Column.Text("counterpart_resource");
public Column our_resource = new Column.Text("our_resource");
public Column direction = new Column.BoolInt("direction") { not_null = true };
public Column type_ = new Column.Integer("type");
public Column time = new Column.Long("time");
public Column local_time = new Column.Long("local_time");
public Column body = new Column.Text("body");
public Column encryption = new Column.Integer("encryption");
public Column marked = new Column.Integer("marked");
internal MessageTable(Database db) {
base(db, "message");
init({id, stanza_id, server_id, account_id, counterpart_id, our_resource, counterpart_resource, direction,
type_, time, local_time, body, encryption, marked});
// get latest messages
index("message_account_counterpart_time_idx", {account_id, counterpart_id, time});
// deduplication
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});
}
}
public class BodyMeta : Table {
public Column id = new Column.Integer("id") { primary_key = true, auto_increment = true };
public Column message_id = new Column.Integer("message_id");
public Column from_char = new Column.Integer("from_char");
public Column to_char = new Column.Integer("to_char");
public Column info_type = new Column.Text("info_type");
public Column 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 Column id = new Column.Integer("id") { primary_key = true, auto_increment = true };
public Column message_id = new Column.Integer("message_id") { unique=true };
public Column to_stanza_id = new Column.Text("to_stanza_id");
internal MessageCorrectionTable(Database db) {
base(db, "message_correction");
init({id, message_id, to_stanza_id});
index("message_correction_to_stanza_id_idx", {to_stanza_id});
}
}
public class ReplyTable : Table {
public Column id = new Column.Integer("id") { primary_key = true, auto_increment = true };
public Column message_id = new Column.Integer("message_id") { not_null = true, unique=true };
public Column quoted_content_item_id = new Column.Integer("quoted_message_id");
public Column quoted_message_stanza_id = new Column.Text("quoted_message_stanza_id");
public Column 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 Column message_id = new Column.Integer("message_id") { primary_key = true };
public Column real_jid = new Column.Text("real_jid");
internal RealJidTable(Database db) {
base(db, "real_jid");
init({message_id, real_jid});
}
}
public class OccupantIdTable : Table {
public Column id = new Column.Integer("id") { primary_key = true };
public Column account_id = new Column.Integer("account_id") { not_null = true };
public Column last_nick = new Column.Text("last_nick");
public Column jid_id = new Column.Integer("jid_id");
public Column 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 Column message_id = new Column.Integer("message_id");
public Column type_ = new Column.Integer("type");
public Column data = new Column.Text("data");
internal UndecryptedTable(Database db) {
base(db, "undecrypted");
init({message_id, type_, data});
}
}
public class FileTransferTable : Table {
public Column id = new Column.Integer("id") { primary_key = true, auto_increment = true };
public Column file_sharing_id = new Column.Text("file_sharing_id") { min_version=28 };
public Column account_id = new Column.Integer("account_id") { not_null = true };
public Column counterpart_id = new Column.Integer("counterpart_id") { not_null = true };
public Column counterpart_resource = new Column.Text("counterpart_resource");
public Column our_resource = new Column.Text("our_resource");
public Column direction = new Column.BoolInt("direction") { not_null = true };
public Column time = new Column.Long("time");
public Column local_time = new Column.Long("local_time");
public Column encryption = new Column.Integer("encryption");
public Column file_name = new Column.Text("file_name");
public Column path = new Column.Text("path");
public Column mime_type = new Column.Text("mime_type");
public Column size = new Column.Long("size");
public Column state = new Column.Integer("state");
public Column provider = new Column.Integer("provider");
public Column info = new Column.Text("info");
public Column modification_date = new Column.Long("modification_date") { default = "-1", min_version=28 };
public Column width = new Column.Integer("width") { default = "-1", min_version=28 };
public Column height = new Column.Integer("height") { default = "-1", min_version=28 };
public Column length = new Column.Integer("length") { default = "-1", min_version=28 };
internal FileTransferTable(Database db) {
base(db, "file_transfer");
init({id, file_sharing_id, account_id, counterpart_id, counterpart_resource, our_resource, direction,
time, local_time, encryption, file_name, path, mime_type, size, state, provider, info, modification_date,
width, height, length});
}
}
public class FileHashesTable : Table {
public Column id = new Column.Integer("id");
public Column algo = new Column.Text("algo") { not_null = true };
public Column 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 id = new Column.Integer("id");
public Column