pax_global_header00006660000000000000000000000064147457214720014527gustar00rootroot0000000000000052 comment=1b80a5a9e3eae4cc19766ce627ea874f83a89ab4 buteo-sync-plugins-social-0.4.28/000077500000000000000000000000001474572147200166415ustar00rootroot00000000000000buteo-sync-plugins-social-0.4.28/.gitignore000066400000000000000000000001461474572147200206320ustar00rootroot00000000000000*.o moc_* Makefile *.so *.so.* *.ts *.qm *.moc *.obj *.pro.* /RPMS/ *.list *-client tests/tst_*/tst_* buteo-sync-plugins-social-0.4.28/COPYING000066400000000000000000000635271474572147200177110ustar00rootroot00000000000000GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999 Copyright (C) 1991, 1999 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, 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 this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. GNU LESSER GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), 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 distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Lesser 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 Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "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 LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY 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 LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey 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 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 Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. , 1 April 1990 Ty Coon, President of Vice That's all there is to it! buteo-sync-plugins-social-0.4.28/README000066400000000000000000000014061474572147200175220ustar00rootroot00000000000000This repository contains plugins which can provide data synchronization with various social services (such as Facebook and Twitter). It provides a basic structure which is intended to be simple to extend to allow more plugins to be written for further social services or data synchronization services. The framework supports synchronizing data of various types (contacts, calendar events, notifications, etc). The plugins are Buteo synchronization framework plugins. Each plugin can be built either in out-of-process mode or in-process mode (that is, as a shared library or standalone executable). The code in this repository is LGPLv2.1 licensed. Any contributions to this repository must be of either this license or a less restrictive license (such as BSD license). buteo-sync-plugins-social-0.4.28/buteo-sync-plugins-social.pro000066400000000000000000000001241474572147200243770ustar00rootroot00000000000000TEMPLATE = subdirs SUBDIRS = src OTHER_FILES += rpm/buteo-sync-plugins-social.spec buteo-sync-plugins-social-0.4.28/rpm/000077500000000000000000000000001474572147200174375ustar00rootroot00000000000000buteo-sync-plugins-social-0.4.28/rpm/buteo-sync-plugins-social.spec000066400000000000000000000364151474572147200253430ustar00rootroot00000000000000Name: buteo-sync-plugins-social Summary: Sync plugins for social services Version: 0.4.0 Release: 1 License: LGPLv2 URL: https://github.com/sailfishos/buteo-sync-plugins-social/ Source0: %{name}-%{version}.tar.bz2 BuildRequires: pkgconfig(Qt5Core) BuildRequires: pkgconfig(Qt5DBus) BuildRequires: pkgconfig(Qt5Sql) BuildRequires: pkgconfig(Qt5Network) BuildRequires: pkgconfig(Qt5Gui) BuildRequires: pkgconfig(Qt5Contacts) BuildRequires: qt5-qttools-linguist BuildRequires: pkgconfig(mlite5) BuildRequires: pkgconfig(buteosyncfw5) >= 0.10.0 BuildRequires: pkgconfig(libsignon-qt5) BuildRequires: pkgconfig(accounts-qt5) >= 1.13 BuildRequires: pkgconfig(socialcache) >= 0.0.48 BuildRequires: pkgconfig(libsailfishkeyprovider) BuildRequires: pkgconfig(qtcontacts-sqlite-qt5-extensions) >= 0.3.0 BuildRequires: pkgconfig(libmkcal-qt5) >= 0.5.45 BuildRequires: pkgconfig(KF5CalendarCore) >= 5.79 BuildRequires: nemo-qml-plugin-notifications-qt5-devel Requires: buteo-syncfw-qt5-msyncd Requires: systemd Requires(pre): sailfish-setup Requires(post): systemd Obsoletes: sociald < 0.4.0 Provides: sociald %description A Buteo plugin which provides data synchronization with various social services. %package facebook Summary: Provides synchronisation with Facebook Requires: %{name} = %{version}-%{release} Obsoletes: sociald-facebook-calendars < 0.4.0 Obsoletes: sociald-facebook-images < 0.4.0 Obsoletes: sociald-facebook-signon < 0.4.0 Provides: sociald-facebook-calendars Provides: sociald-facebook-images Provides: sociald-facebook-signon %description facebook %{summary}. %package google Summary: Provides synchronisation with Google Requires: %{name} = %{version}-%{release} Obsoletes: sociald-google-calendars < 0.4.0 Obsoletes: sociald-google-contacts < 0.4.0 Obsoletes: sociald-google-signon < 0.4.0 Provides: sociald-google-calendars Provides: sociald-google-contacts Provides: sociald-google-signon %description google %{summary}. %package twitter Summary: Provides synchronisation with Twitter Requires: %{name} = %{version}-%{release} Obsoletes: sociald-twitter-notifications < 0.4.0 Obsoletes: sociald-twitter-posts < 0.4.0 Provides: sociald-twitter-notifications Provides: sociald-twitter-posts %description twitter %{summary}. %package onedrive Summary: Provides synchronisation with OneDrive Requires: %{name} = %{version}-%{release} Obsoletes: sociald-onedrive-signon < 0.4.0 Obsoletes: sociald-onedrive-images < 0.4.0 Obsoletes: sociald-onedrive-backup < 0.4.0 Provides: sociald-onedrive-signon Provides: sociald-onedrive-images Provides: sociald-onedrive-backup %description onedrive %{summary}. %package vk Summary: Provides synchronisation with VK Requires: %{name} = %{version}-%{release} Obsoletes: sociald-vk-posts < 0.4.0 Obsoletes: sociald-vk-notifications < 0.4.0 Obsoletes: sociald-vk-calendars < 0.4.0 Obsoletes: sociald-vk-contacts < 0.4.0 Obsoletes: sociald-vk-images < 0.4.0 Provides: sociald-vk-posts Provides: sociald-vk-notifications Provides: sociald-vk-calendars Provides: sociald-vk-contacts Provides: sociald-vk-images %description vk %{summary}. %package dropbox Summary: Provides synchronisation with Dropbox Requires: %{name} = %{version}-%{release} Obsoletes: sociald-dropbox-images < 0.4.0 Obsoletes: sociald-dropbox-backup < 0.4.0 Provides: sociald-dropbox-images Provides: sociald-dropbox-backup %description dropbox %{summary}. %package knowncontacts Summary: Store locally created contacts Requires: %{name} = %{version}-%{release} Obsoletes: sociald-knowncontacts < 0.4.0 Provides: sociald-knowncontacts %description knowncontacts Buteo sync plugin that stores locally created contacts, such as email recipients. %package ts-devel Summary: Translation source for sociald %description ts-devel %{summary}. %prep %setup -q -n %{name}-%{version} %build %qmake5 \ "CONFIG+=dropbox" \ "CONFIG+=facebook" \ "CONFIG+=google" \ "CONFIG+=onedrive" \ "CONFIG+=twitter" \ "CONFIG+=vk" \ "CONFIG+=knowncontacts" \ "CONFIG+=calendar" %make_build %install %qmake5_install %pre USERS=$(getent group users | cut -d ":" -f 4 | tr "," "\n") for user in $USERS; do USERHOME=$(getent passwd ${user} | cut -d ":" -f 6) rm -f ${USERHOME}/.cache/msyncd/sync/client/sociald.xml || : rm -f ${USERHOME}/.cache/msyncd/sync/sociald.facebook.Calendars.xml || : rm -f ${USERHOME}/.cache/msyncd/sync/sociald.facebook.Contacts.xml || : rm -f ${USERHOME}/.cache/msyncd/sync/sociald.facebook.Images.xml || : rm -f ${USERHOME}/.cache/msyncd/sync/sociald.facebook.Notifications.xml || : rm -f ${USERHOME}/.cache/msyncd/sync/sociald.twitter.Notifications.xml || : rm -f ${USERHOME}/.cache/msyncd/sync/sociald.twitter.Posts.xml || : rm -f ${USERHOME}/.cache/msyncd/sync/sociald.google.Calendars.xml || : rm -f ${USERHOME}/.cache/msyncd/sync/sociald.google.Contacts.xml || : done %pre facebook # calendar USERS=$(getent group users | cut -d ":" -f 4 | tr "," "\n") for user in $USERS; do USERHOME=$(getent passwd ${user} | cut -d ":" -f 6) rm -f ${USERHOME}/.cache/msyncd/sync/client/facebook-calendars.xml || : rm -f ${USERHOME}/.cache/msyncd/sync/facebook.Calendars.xml || : done #images for user in $USERS; do USERHOME=$(getent passwd ${user} | cut -d ":" -f 6) rm -f ${USERHOME}/.cache/msyncd/sync/client/facebook-images.xml || : rm -f ${USERHOME}/.cache/msyncd/sync/facebook.Images.xml || : done #signon for user in $USERS; do USERHOME=$(getent passwd ${user} | cut -d ":" -f 6) rm -f ${USERHOME}/.cache/msyncd/sync/client/facebook-signon.xml || : rm -f ${USERHOME}/.cache/msyncd/sync/facebook.Signon.xml || : done %pre google USERS=$(getent group users | cut -d ":" -f 4 | tr "," "\n") # calendar for user in $USERS; do USERHOME=$(getent passwd ${user} | cut -d ":" -f 6) rm -f ${USERHOME}/.cache/msyncd/sync/client/google-calendars.xml || : rm -f ${USERHOME}/.cache/msyncd/sync/google.Calendars.xml || : done # contacts for user in $USERS; do USERHOME=$(getent passwd ${user} | cut -d ":" -f 6) rm -f ${USERHOME}/.cache/msyncd/sync/client/google-contacts.xml || : rm -f ${USERHOME}/.cache/msyncd/sync/google.Contacts.xml || : done # signon for user in $USERS; do USERHOME=$(getent passwd ${user} | cut -d ":" -f 6) rm -f ${USERHOME}/.cache/msyncd/sync/client/google-signon.xml || : rm -f ${USERHOME}/.cache/msyncd/sync/google.Signon.xml || : done %pre twitter # notifications USERS=$(getent group users | cut -d ":" -f 4 | tr "," "\n") for user in $USERS; do USERHOME=$(getent passwd ${user} | cut -d ":" -f 6) rm -f ${USERHOME}/.cache/msyncd/sync/client/twitter-notifications.xml || : rm -f ${USERHOME}/.cache/msyncd/sync/twitter.Notifications.xml || : done # posts for user in $USERS; do USERHOME=$(getent passwd ${user} | cut -d ":" -f 6) rm -f ${USERHOME}/.cache/msyncd/sync/client/twitter-posts.xml || : rm -f ${USERHOME}/.cache/msyncd/sync/twitter.Posts.xml || : done %pre onedrive USERS=$(getent group users | cut -d ":" -f 4 | tr "," "\n") # signon for user in $USERS; do USERHOME=$(getent passwd ${user} | cut -d ":" -f 6) rm -f ${USERHOME}/.cache/msyncd/sync/client/onedrive-signon.xml || : rm -f ${USERHOME}/.cache/msyncd/sync/onedrive.Signon.xml || : done # images for user in $USERS; do USERHOME=$(getent passwd ${user} | cut -d ":" -f 6) rm -f ${USERHOME}/.cache/msyncd/sync/client/onedrive-images.xml || : rm -f ${USERHOME}/.cache/msyncd/sync/onedrive.Images.xml || : done # backup for user in $USERS; do USERHOME=$(getent passwd ${user} | cut -d ":" -f 6) rm -f ${USERHOME}/.cache/msyncd/sync/client/onedrive-backup.xml || : rm -f ${USERHOME}/.cache/msyncd/sync/client/onedrive-backupquery.xml || : rm -f ${USERHOME}/.cache/msyncd/sync/client/onedrive-backuprestore.xml || : rm -f ${USERHOME}/.cache/msyncd/sync/onedrive.Backup.xml || : rm -f ${USERHOME}/.cache/msyncd/sync/onedrive.BackupQuery.xml || : rm -f ${USERHOME}/.cache/msyncd/sync/onedrive.BackupRestore.xml || : done %pre vk USERS=$(getent group users | cut -d ":" -f 4 | tr "," "\n") # posts for user in $USERS; do USERHOME=$(getent passwd ${user} | cut -d ":" -f 6) rm -f ${USERHOME}/.cache/msyncd/sync/client/vk-posts.xml || : rm -f ${USERHOME}/.cache/msyncd/sync/vk.Posts.xml || : done # notifications for user in $USERS; do USERHOME=$(getent passwd ${user} | cut -d ":" -f 6) rm -f ${USERHOME}/.cache/msyncd/sync/client/vk-notifications.xml || : rm -f ${USERHOME}/.cache/msyncd/sync/vk.Notifications.xml || : done # calendars for user in $USERS; do USERHOME=$(getent passwd ${user} | cut -d ":" -f 6) rm -f ${USERHOME}/.cache/msyncd/sync/client/vk-calendars.xml || : rm -f ${USERHOME}/.cache/msyncd/sync/vk.Calendars.xml || : done # contacts for user in $USERS; do USERHOME=$(getent passwd ${user} | cut -d ":" -f 6) rm -f ${USERHOME}/.cache/msyncd/sync/client/vk-contacts.xml || : rm -f ${USERHOME}/.cache/msyncd/sync/vk.Contacts.xml || : done # images for user in $USERS; do USERHOME=$(getent passwd ${user} | cut -d ":" -f 6) rm -f ${USERHOME}/.cache/msyncd/sync/client/vk-images.xml || : rm -f ${USERHOME}/.cache/msyncd/sync/vk.Images.xml || : done %pre dropbox USERS=$(getent group users | cut -d ":" -f 4 | tr "," "\n") # images for user in $USERS; do USERHOME=$(getent passwd ${user} | cut -d ":" -f 6) rm -f ${USERHOME}/.cache/msyncd/sync/client/dropbox-images.xml || : rm -f ${USERHOME}/.cache/msyncd/sync/dropbox.Images.xml || : done # backup for user in $USERS; do USERHOME=$(getent passwd ${user} | cut -d ":" -f 6) rm -f ${USERHOME}/.cache/msyncd/sync/client/dropbox-backup.xml || : rm -f ${USERHOME}/.cache/msyncd/sync/client/dropbox-backupquery.xml || : rm -f ${USERHOME}/.cache/msyncd/sync/client/dropbox-backuprestore.xml || : rm -f ${USERHOME}/.cache/msyncd/sync/dropbox.Backup.xml || : rm -f ${USERHOME}/.cache/msyncd/sync/dropbox.BackupQuery.xml || : rm -f ${USERHOME}/.cache/msyncd/sync/dropbox.BackupRestore.xml || : done %post -p /sbin/ldconfig %postun -p /sbin/ldconfig %files %{_libdir}/buteo-plugins-qt5/oopp/libsociald-client.so %config %{_sysconfdir}/buteo/profiles/client/sociald.xml %config %{_sysconfdir}/buteo/profiles/sync/sociald.All.xml %{_libdir}/libsyncpluginscommon.so.* %exclude %{_libdir}/libsyncpluginscommon.so %license COPYING %files facebook # calendar: %{_libdir}/buteo-plugins-qt5/oopp/libfacebook-calendars-client.so %config %{_sysconfdir}/buteo/profiles/client/facebook-calendars.xml %config %{_sysconfdir}/buteo/profiles/sync/facebook.Calendars.xml # images: %{_libdir}/buteo-plugins-qt5/oopp/libfacebook-images-client.so %config %{_sysconfdir}/buteo/profiles/client/facebook-images.xml %config %{_sysconfdir}/buteo/profiles/sync/facebook.Images.xml # signon %{_libdir}/buteo-plugins-qt5/oopp/libfacebook-signon-client.so %config %{_sysconfdir}/buteo/profiles/client/facebook-signon.xml %config %{_sysconfdir}/buteo/profiles/sync/facebook.Signon.xml %files google # calendar %{_libdir}/buteo-plugins-qt5/oopp/libgoogle-calendars-client.so %config %{_sysconfdir}/buteo/profiles/client/google-calendars.xml %config %{_sysconfdir}/buteo/profiles/sync/google.Calendars.xml # contacts %{_libdir}/buteo-plugins-qt5/oopp/libgoogle-contacts-client.so %config %{_sysconfdir}/buteo/profiles/client/google-contacts.xml %config %{_sysconfdir}/buteo/profiles/sync/google.Contacts.xml # signon %{_libdir}/buteo-plugins-qt5/oopp/libgoogle-signon-client.so %config %{_sysconfdir}/buteo/profiles/client/google-signon.xml %config %{_sysconfdir}/buteo/profiles/sync/google.Signon.xml %files twitter # notifications %{_libdir}/buteo-plugins-qt5/oopp/libtwitter-notifications-client.so %config %{_sysconfdir}/buteo/profiles/client/twitter-notifications.xml %config %{_sysconfdir}/buteo/profiles/sync/twitter.Notifications.xml %{_datadir}/translations/lipstick-jolla-home-twitter-notif_eng_en.qm # posts %{_libdir}/buteo-plugins-qt5/oopp/libtwitter-posts-client.so %config %{_sysconfdir}/buteo/profiles/client/twitter-posts.xml %config %{_sysconfdir}/buteo/profiles/sync/twitter.Posts.xml %files onedrive # signon %{_libdir}/buteo-plugins-qt5/oopp/libonedrive-signon-client.so %config %{_sysconfdir}/buteo/profiles/client/onedrive-signon.xml %config %{_sysconfdir}/buteo/profiles/sync/onedrive.Signon.xml # images %{_libdir}/buteo-plugins-qt5/oopp/libonedrive-images-client.so # backup %config %{_sysconfdir}/buteo/profiles/client/onedrive-images.xml %config %{_sysconfdir}/buteo/profiles/sync/onedrive.Images.xml %{_libdir}/buteo-plugins-qt5/oopp/libonedrive-backup-client.so %{_libdir}/buteo-plugins-qt5/oopp/libonedrive-backupquery-client.so %{_libdir}/buteo-plugins-qt5/oopp/libonedrive-backuprestore-client.so #%%{_libdir}/buteo-plugins-qt5/libonedrive-backupquery-client.so #%%{_libdir}/buteo-plugins-qt5/libonedrive-backuprestore-client.so %config %{_sysconfdir}/buteo/profiles/client/onedrive-backup.xml %config %{_sysconfdir}/buteo/profiles/client/onedrive-backupquery.xml %config %{_sysconfdir}/buteo/profiles/client/onedrive-backuprestore.xml %config %{_sysconfdir}/buteo/profiles/sync/onedrive.Backup.xml %config %{_sysconfdir}/buteo/profiles/sync/onedrive.BackupQuery.xml %config %{_sysconfdir}/buteo/profiles/sync/onedrive.BackupRestore.xml %files vk # posts %{_libdir}/buteo-plugins-qt5/oopp/libvk-posts-client.so %config %{_sysconfdir}/buteo/profiles/client/vk-posts.xml %config %{_sysconfdir}/buteo/profiles/sync/vk.Posts.xml # notifications %{_libdir}/buteo-plugins-qt5/oopp/libvk-notifications-client.so %config %{_sysconfdir}/buteo/profiles/client/vk-notifications.xml %config %{_sysconfdir}/buteo/profiles/sync/vk.Notifications.xml # calendars #out-of-proces-plugin form: %{_libdir}/buteo-plugins-qt5/oopp/libvk-calendars-client.so %config %{_sysconfdir}/buteo/profiles/client/vk-calendars.xml %config %{_sysconfdir}/buteo/profiles/sync/vk.Calendars.xml # contacts %{_libdir}/buteo-plugins-qt5/oopp/libvk-contacts-client.so %config %{_sysconfdir}/buteo/profiles/client/vk-contacts.xml %config %{_sysconfdir}/buteo/profiles/sync/vk.Contacts.xml # images %{_libdir}/buteo-plugins-qt5/oopp/libvk-images-client.so %config %{_sysconfdir}/buteo/profiles/client/vk-images.xml %config %{_sysconfdir}/buteo/profiles/sync/vk.Images.xml %files dropbox # images %{_libdir}/buteo-plugins-qt5/oopp/libdropbox-images-client.so %config %{_sysconfdir}/buteo/profiles/client/dropbox-images.xml %config %{_sysconfdir}/buteo/profiles/sync/dropbox.Images.xml # backup %{_libdir}/buteo-plugins-qt5/oopp/libdropbox-backup-client.so %{_libdir}/buteo-plugins-qt5/oopp/libdropbox-backupquery-client.so %{_libdir}/buteo-plugins-qt5/oopp/libdropbox-backuprestore-client.so #%%{_libdir}/buteo-plugins-qt5/libdropbox-backupquery-client.so #%%{_libdir}/buteo-plugins-qt5/libdropbox-backuprestore-client.so %config %{_sysconfdir}/buteo/profiles/client/dropbox-backup.xml %config %{_sysconfdir}/buteo/profiles/client/dropbox-backupquery.xml %config %{_sysconfdir}/buteo/profiles/client/dropbox-backuprestore.xml %config %{_sysconfdir}/buteo/profiles/sync/dropbox.Backup.xml %config %{_sysconfdir}/buteo/profiles/sync/dropbox.BackupQuery.xml %config %{_sysconfdir}/buteo/profiles/sync/dropbox.BackupRestore.xml %files knowncontacts %{_libdir}/buteo-plugins-qt5/oopp/libknowncontacts-client.so %{_sysconfdir}/buteo/profiles/client/knowncontacts.xml %{_sysconfdir}/buteo/profiles/sync/knowncontacts.Contacts.xml %files ts-devel %{_datadir}/translations/source/lipstick-jolla-home-twitter-notif.ts buteo-sync-plugins-social-0.4.28/src/000077500000000000000000000000001474572147200174305ustar00rootroot00000000000000buteo-sync-plugins-social-0.4.28/src/common.pri000066400000000000000000000016741474572147200214440ustar00rootroot00000000000000QMAKE_CXXFLAGS += -Werror CONFIG += link_pkgconfig PKGCONFIG += \ libsailfishkeyprovider \ libsignon-qt5 \ accounts-qt5 \ buteosyncfw5 \ socialcache QT += \ network \ dbus \ sql QT -= \ gui QMAKE_LFLAGS += $$QMAKE_LFLAGS_NOUNDEF DEFINES += 'SYNC_DATABASE_DIR=\'\"Sync\"\'' INCLUDEPATH += . $$PWD/common/ LIBS += -L$$PWD/common -lsyncpluginscommon contains(DEFINES, 'SOCIALD_USE_QTPIM') { DEFINES *= USE_CONTACTS_NAMESPACE=QTCONTACTS_USE_NAMESPACE PKGCONFIG += Qt5Contacts qtcontacts-sqlite-qt5-extensions HEADERS += $$PWD/common/constants_p.h # We need the moc output for ContactManagerEngine from sqlite-extensions extensionsIncludePath = $$system(pkg-config --cflags-only-I qtcontacts-sqlite-qt5-extensions) VPATH += $$replace(extensionsIncludePath, -I, ) HEADERS += contactmanagerengine.h } TEMPLATE = lib CONFIG += plugin target.path = $$[QT_INSTALL_LIBS]/buteo-plugins-qt5/oopp buteo-sync-plugins-social-0.4.28/src/common/000077500000000000000000000000001474572147200207205ustar00rootroot00000000000000buteo-sync-plugins-social-0.4.28/src/common/buteosyncfw_p.h000066400000000000000000000027211474572147200237620ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2014 Jolla Ltd. ** Contact: Chris Adams ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef SOCIALD_BUTEOSYNCFW_P_H #define SOCIALD_BUTEOSYNCFW_P_H #include #include #include #include #include #include #include #include #include #ifndef SOCIALD_TEST_DEFINE #define PRIVILEGED_DATA_DIR QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + QLatin1String("/.local/share/system/privileged") #endif #endif // SOCIALD_BUTEOSYNCFW_P_H buteo-sync-plugins-social-0.4.28/src/common/common.pro000066400000000000000000000011541474572147200227330ustar00rootroot00000000000000TEMPLATE = lib QT -= gui QT += network dbus CONFIG += link_pkgconfig PKGCONFIG += \ accounts-qt5 \ buteosyncfw5 \ socialcache \ TARGET = syncpluginscommon TARGET = $$qtLibraryTarget($$TARGET) HEADERS += \ $$PWD/buteosyncfw_p.h \ $$PWD/socialdbuteoplugin.h \ $$PWD/socialnetworksyncadaptor.h \ $$PWD/socialdnetworkaccessmanager_p.h \ $$PWD/trace.h SOURCES += \ $$PWD/socialdbuteoplugin.cpp \ $$PWD/socialnetworksyncadaptor.cpp \ $$PWD/socialdnetworkaccessmanager_p.cpp \ $$PWD/trace.cpp TARGETPATH = $$[QT_INSTALL_LIBS] target.path = $$TARGETPATH INSTALLS += target buteo-sync-plugins-social-0.4.28/src/common/constants_p.h000066400000000000000000000025161474572147200234300ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2013-2014 Jolla Ltd. ** Contact: Raine Makelainen ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef SOCIALD_CONSTANTS_P_H #define SOCIALD_CONSTANTS_P_H // ensure we include the extensions from qtcontacts-sqlite // to allow access to QContactAvatar__FieldAvatarMetadata etc // Note that the _impl.h files need to be included exactly once // in the project to ensure that the symbols exist. #include #include #endif buteo-sync-plugins-social-0.4.28/src/common/socialdbuteoplugin.cpp000066400000000000000000000337711474572147200253330ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2014 Jolla Ltd. ** Contact: Chris Adams ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "socialdbuteoplugin.h" #include "socialnetworksyncadaptor.h" #include "trace.h" #include #include #include #include #include #include "buteosyncfw_p.h" #include #include #include namespace { static const QString SyncProfileTemplatesKey = QStringLiteral("sync_profile_templates"); static QString SyncProfileIdKey(const QString &templateProfileName) { return QStringLiteral("%1/%2").arg(templateProfileName).arg(Buteo::KEY_PROFILE_ID); } QString createProfile(Buteo::ProfileManager *profileManager, const QString &templateProfileName, Accounts::Account *account, const Accounts::Service &srv, bool enableProfile, const QVariantMap &properties) { if (!account || !srv.isValid()) { qWarning() << "Invalid account or service"; return QString(); } if (templateProfileName.isEmpty()) { qWarning() << "Invalid templateProfileName"; return QString(); } Accounts::Service prevService = account->selectedService(); account->selectService(srv); Buteo::SyncProfile *templateProfile = profileManager->syncProfile(templateProfileName); if (!templateProfile) { account->selectService(prevService); qWarning() << "Unable to load template profile:" << templateProfileName; return QString(); } Buteo::SyncProfile *profile = templateProfile->clone(); if (!profile) { delete templateProfile; account->selectService(prevService); qWarning() << "unable to clone template profile:" << templateProfileName; return QString(); } QString accountIdStr = QString::number(account->id()); profile->setName(templateProfileName + "-" + accountIdStr); profile->setKey(Buteo::KEY_DISPLAY_NAME, templateProfileName + "-" + account->displayName().toHtmlEscaped()); profile->setKey(Buteo::KEY_ACCOUNT_ID, accountIdStr); profile->setBoolKey(Buteo::KEY_USE_ACCOUNTS, true); profile->setEnabled(enableProfile); // enable the profile schedule Buteo::SyncSchedule schedule = profile->syncSchedule(); schedule.setScheduleEnabled(true); profile->setSyncSchedule(schedule); // set custom properties; note this may override any properties already set Q_FOREACH (const QString &key, properties.keys()) { profile->setKey(key, properties[key].toString()); } QString profileId = profileManager->updateProfile(*profile); if (profileId.isEmpty()) { qWarning() << "Unable to save sync profile" << templateProfile->name(); } else { account->setValue(SyncProfileIdKey(templateProfile->name()), profile->name()); } account->selectService(prevService); delete profile; delete templateProfile; return profileId; } } SocialdButeoPlugin::SocialdButeoPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *callbackInterface, const QString &socialServiceName, const QString &dataTypeName) : ClientPlugin(pluginName, profile, callbackInterface) , m_socialNetworkSyncAdaptor(0) , m_socialServiceName(socialServiceName) , m_dataTypeName(dataTypeName) , m_profileAccountId(0) { } SocialdButeoPlugin::~SocialdButeoPlugin() { } bool SocialdButeoPlugin::init() { m_profileAccountId = profile().key(Buteo::KEY_ACCOUNT_ID).toInt(); m_socialNetworkSyncAdaptor = createSocialNetworkSyncAdaptor(); if (m_socialNetworkSyncAdaptor) { connect(m_socialNetworkSyncAdaptor, SIGNAL(statusChanged()), this, SLOT(syncStatusChanged())); return true; } return false; } bool SocialdButeoPlugin::uninit() { delete m_socialNetworkSyncAdaptor; m_socialNetworkSyncAdaptor = 0; return true; } bool SocialdButeoPlugin::startSync() { // if the profile being triggered is the template profile, then we // need to ensure that the appropriate per-account profiles exist. if (m_profileAccountId == 0) { QList perAccountProfiles = ensurePerAccountSyncProfilesExist(); m_socialNetworkSyncAdaptor->setAccountSyncProfile(NULL); // we need to trigger sync with each profile separately, // or (due to scheduling/etc) another plugin instance might // be created to sync that profile at the same time, and // we don't handle concurrency. foreach (Buteo::SyncProfile *perAccountProfile, perAccountProfiles) { QDBusMessage message = QDBusMessage::createMethodCall( "com.meego.msyncd", "/synchronizer", "com.meego.msyncd", "startSync"); message.setArguments(QVariantList() << perAccountProfile->name()); QDBusConnection::sessionBus().asyncCall(message); } } else { m_socialNetworkSyncAdaptor->setAccountSyncProfile(profile().clone()); } // now perform sync. Note that for the template profile case, this will // result in a purge operation occurring (checking for removed accounts and // purging any synced data associated with those accounts). if (m_socialNetworkSyncAdaptor && m_socialNetworkSyncAdaptor->enabled()) { if (m_socialNetworkSyncAdaptor->status() == SocialNetworkSyncAdaptor::Inactive) { qCDebug(lcSocialPlugin) << "performing sync of" << m_dataTypeName << "from" << m_socialServiceName << "for account" << m_profileAccountId; m_socialNetworkSyncAdaptor->sync(m_dataTypeName, m_profileAccountId); return true; } else { qCDebug(lcSocialPlugin) << m_socialServiceName << "sync adaptor for" << m_dataTypeName << "is still busy with last sync of account" << m_profileAccountId; } } else { qCDebug(lcSocialPlugin) << "no enabled" << m_socialServiceName << "sync adaptor for" << m_dataTypeName; } return false; } void SocialdButeoPlugin::abortSync(Sync::SyncStatus status) { // note: it seems buteo automatically calls abortSync on network connectivity loss... qCInfo(lcSocialPlugin) << "aborting sync with status:" << status; m_socialNetworkSyncAdaptor->abortSync(status); } bool SocialdButeoPlugin::cleanUp() { m_profileAccountId = profile().key(Buteo::KEY_ACCOUNT_ID).toInt(); if (!m_socialNetworkSyncAdaptor) { // might have already been initialized by the OOP framework via init(). m_socialNetworkSyncAdaptor = createSocialNetworkSyncAdaptor(); } if (m_socialNetworkSyncAdaptor && m_profileAccountId > 0) { m_socialNetworkSyncAdaptor->purgeDataForOldAccount(m_profileAccountId, SocialNetworkSyncAdaptor::CleanUpPurge); } return true; } Buteo::SyncResults SocialdButeoPlugin::getSyncResults() const { return m_syncResults; } void SocialdButeoPlugin::connectivityStateChanged(Sync::ConnectivityType type, bool state) { // See TransportTracker.cpp:149 for example // Sync::CONNECTIVITY_INTERNET, true|false qCInfo(lcSocialPlugin) << "notified of connectivity change:" << type << state; if (type == Sync::CONNECTIVITY_INTERNET && state == false) { // we lost connectivity during sync. abortSync(Sync::SYNC_CONNECTION_ERROR); } } void SocialdButeoPlugin::syncStatusChanged() { if (m_socialNetworkSyncAdaptor) { SocialNetworkSyncAdaptor::Status syncStatus = m_socialNetworkSyncAdaptor->status(); // Busy change comes when sync starts -> let's ignore that. if (syncStatus == SocialNetworkSyncAdaptor::Inactive) { updateResults(Buteo::SyncResults(QDateTime::currentDateTime(), Buteo::SyncResults::SYNC_RESULT_SUCCESS, Buteo::SyncResults::NO_ERROR)); emit success(getProfileName(), QString("%1 update succeeded").arg(getProfileName())); } else if (syncStatus != SocialNetworkSyncAdaptor::Busy) { updateResults(Buteo::SyncResults(QDateTime::currentDateTime(), Buteo::SyncResults::SYNC_RESULT_FAILED, Buteo::SyncResults::ABORTED)); emit error(getProfileName(), QString("%1 update failed").arg(getProfileName()), Buteo::SyncResults::ABORTED); } } else { updateResults(Buteo::SyncResults(QDateTime::currentDateTime(), Buteo::SyncResults::SYNC_RESULT_FAILED, Buteo::SyncResults::ABORTED)); emit error(getProfileName(), QString("%1 update failed").arg(getProfileName()), Buteo::SyncResults::ABORTED); } } void SocialdButeoPlugin::updateResults(const Buteo::SyncResults &results) { m_syncResults = results; m_syncResults.setScheduled(true); } // This function is called when the non-per-account profile is triggered. // The implementation does: // - get all profiles from the ProfileManager // - get all accounts from the AccountManager // - build a mapping of profile -> account for the current data type. (should be one-to-one for the datatype). // - any account which doesn't have a profile, print an error. // - check the enabled status of the account -> ensure that the enabled status is reflected in the profile. // It then returns a list of the appropriate (per account for this data-type) sync profiles. // The caller takes ownership of the list. QList SocialdButeoPlugin::ensurePerAccountSyncProfilesExist() { Accounts::Manager am; Accounts::AccountIdList accountIds = am.accountList(); QList syncProfiles = m_profileManager.allSyncProfiles(); QMap perAccountProfiles; Accounts::Service dataTypeSyncService = am.service(m_socialNetworkSyncAdaptor->syncServiceName()); if (!dataTypeSyncService.isValid()) { qWarning() << Q_FUNC_INFO << "Invalid data type sync service name specified:" << m_socialNetworkSyncAdaptor->syncServiceName(); return QList(); } for (int i = 0; i < accountIds.size(); ++i) { Accounts::Account *currAccount = Accounts::Account::fromId(&am, accountIds.at(i), this); if (!currAccount || currAccount->id() == 0 || m_socialNetworkSyncAdaptor->syncServiceName().split('-').first() != currAccount->providerName()) { // we only generate per-account sync profiles for accounts which // are provided by the provider which provides our sync service. continue; } // for the current account, find the associated sync profile. bool foundProfile = false; for (int j = 0; j < syncProfiles.size(); ++j) { if (syncProfiles[j]->key(Buteo::KEY_ACCOUNT_ID).toInt() == QString::number(currAccount->id()).toInt() && syncProfiles[j]->clientProfile() != NULL && syncProfiles[j]->clientProfile()->name() == profile().clientProfile()->name()) { // we have found the sync profile for this datatype for this account. foundProfile = true; perAccountProfiles.insert(currAccount, syncProfiles.takeAt(j)); break; } } if (!foundProfile) { // it should have been generated for the account when the account was added. qCInfo(lcSocialPlugin) << "no per-account" << profile().name() << "sync profile exists for account:" << currAccount->id(); // create the per-account profile... we shouldn't need to do this... QString profileName = createProfile(&m_profileManager, profile().name(), currAccount, dataTypeSyncService, true, QVariantMap()); Buteo::SyncProfile *newProfile = m_profileManager.syncProfile(profileName); if (!newProfile) { qCWarning(lcSocialPlugin) << "unable to create per-account" << profile().name() << "sync profile for account:" << currAccount->id(); } else { // enable the sync schedule for the profile. Buteo::SyncSchedule schedule = newProfile->syncSchedule(); schedule.setScheduleEnabled(true); newProfile->setSyncSchedule(schedule); m_profileManager.updateProfile(*newProfile); // and return the profile in the map. perAccountProfiles.insert(currAccount, newProfile); } } } // Every account now has the appropriate sync profile. qDeleteAll(syncProfiles); // these are for the wrong data type, ignore them. QList retn; foreach (Accounts::Account *acc, perAccountProfiles.keys()) { retn.append(perAccountProfiles[acc]); acc->deleteLater(); } return retn; } buteo-sync-plugins-social-0.4.28/src/common/socialdbuteoplugin.h000066400000000000000000000046641474572147200247770ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2013-2014 Jolla Ltd. ** Contact: Raine Makelainen ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef SOCIALDBUTEOPLUGIN_H #define SOCIALDBUTEOPLUGIN_H #include #include "buteosyncfw_p.h" /* Datatype-specific implementations of this class allow per-account sync profiles for that data type. */ class SocialNetworkSyncAdaptor; class Q_DECL_EXPORT SocialdButeoPlugin : public Buteo::ClientPlugin { Q_OBJECT protected: virtual SocialNetworkSyncAdaptor *createSocialNetworkSyncAdaptor() = 0; public: SocialdButeoPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *cbInterface, const QString &socialServiceName, const QString &dataTypeName); virtual ~SocialdButeoPlugin(); bool init(); bool uninit(); bool startSync(); void abortSync(Sync::SyncStatus status = Sync::SYNC_ABORTED); Buteo::SyncResults getSyncResults() const; bool cleanUp(); public Q_SLOTS: void connectivityStateChanged(Sync::ConnectivityType type, bool state); private Q_SLOTS: void syncStatusChanged(); protected: QList ensurePerAccountSyncProfilesExist(); private: void updateResults(const Buteo::SyncResults &results); Buteo::SyncResults m_syncResults; Buteo::ProfileManager m_profileManager; SocialNetworkSyncAdaptor *m_socialNetworkSyncAdaptor; QString m_socialServiceName; QString m_dataTypeName; int m_profileAccountId; }; #endif // SOCIALDBUTEOPLUGIN_H buteo-sync-plugins-social-0.4.28/src/common/socialdnetworkaccessmanager_p.cpp000066400000000000000000000027711474572147200275170ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2013-2014 Jolla Ltd. ** Contact: Chris Adams ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "socialdnetworkaccessmanager_p.h" /* The default implementation is just a normal QNetworkAccessManager */ SocialdNetworkAccessManager::SocialdNetworkAccessManager(QObject *parent) : QNetworkAccessManager(parent) { } QNetworkReply *SocialdNetworkAccessManager::createRequest( QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *outgoingData) { return QNetworkAccessManager::createRequest(op, req, outgoingData); } buteo-sync-plugins-social-0.4.28/src/common/socialdnetworkaccessmanager_p.h000066400000000000000000000026301474572147200271560ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2013-2014 Jolla Ltd. ** Contact: Chris Adams ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef SOCIALD_QNAMFACTORY_P_H #define SOCIALD_QNAMFACTORY_P_H #include class SocialdNetworkAccessManager : public QNetworkAccessManager { Q_OBJECT public: SocialdNetworkAccessManager(QObject *parent = 0); protected: QNetworkReply *createRequest(QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *outgoingData = 0); }; #endif buteo-sync-plugins-social-0.4.28/src/common/socialnetworksyncadaptor.cpp000066400000000000000000000403631474572147200265660ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2013-2014 Jolla Ltd. ** Contact: Chris Adams ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "socialnetworksyncadaptor.h" #include "socialdnetworkaccessmanager_p.h" #include "trace.h" #include #include #include #include #include #include #include #include #include "buteosyncfw_p.h" // libaccounts-qt5 #include #include #include // libsocialcache #include #include namespace { QStringList validDataTypesInitialiser() { return QStringList() << QStringLiteral("Contacts") << QStringLiteral("Calendars") << QStringLiteral("Notifications") << QStringLiteral("Images") << QStringLiteral("Videos") << QStringLiteral("Posts") << QStringLiteral("Messages") << QStringLiteral("Emails") << QStringLiteral("Signon") << QStringLiteral("Backup") << QStringLiteral("BackupQuery") << QStringLiteral("BackupRestore"); } } SocialNetworkSyncAdaptor::SocialNetworkSyncAdaptor(const QString &serviceName, SocialNetworkSyncAdaptor::DataType dataType, QNetworkAccessManager *qnam, QObject *parent) : QObject(parent) , m_dataType(dataType) , m_accountManager(new Accounts::Manager(this)) , m_networkAccessManager(qnam != 0 ? qnam : new SocialdNetworkAccessManager) , m_accountSyncProfile(NULL) , m_syncDb(new SocialNetworkSyncDatabase()) , m_status(SocialNetworkSyncAdaptor::Invalid) , m_enabled(false) , m_syncAborted(false) , m_serviceName(serviceName) { } SocialNetworkSyncAdaptor::~SocialNetworkSyncAdaptor() { delete m_networkAccessManager; delete m_accountSyncProfile; delete m_syncDb; } // The SocialNetworkSyncAdaptor takes ownership of the sync profiles. void SocialNetworkSyncAdaptor::setAccountSyncProfile(Buteo::SyncProfile* perAccountSyncProfile) { delete m_accountSyncProfile; m_accountSyncProfile = perAccountSyncProfile; } SocialNetworkSyncAdaptor::Status SocialNetworkSyncAdaptor::status() const { return m_status; } bool SocialNetworkSyncAdaptor::enabled() const { return m_enabled; } QString SocialNetworkSyncAdaptor::serviceName() const { return m_serviceName; } bool SocialNetworkSyncAdaptor::syncAborted() const { return m_syncAborted; } void SocialNetworkSyncAdaptor::sync(const QString &dataType, int accountId) { Q_UNUSED(dataType) Q_UNUSED(accountId) qCWarning(lcSocialPlugin) << "sync() must be overridden by derived types"; } void SocialNetworkSyncAdaptor::abortSync(Sync::SyncStatus status) { qCInfo(lcSocialPlugin) << "forcing timeout of outstanding replies due to abort:" << status; m_syncAborted = true; triggerReplyTimeouts(); } /*! * \brief SocialNetworkSyncAdaptor::checkAccount * \param account * \return true if synchronization of this adaptor's datatype is enabled for the account * * The default implementation checks that the account is enabled * with the accounts&sso service associated with this sync adaptor. */ bool SocialNetworkSyncAdaptor::checkAccount(Accounts::Account *account) { bool globallyEnabled = account->enabled(); Accounts::Service srv(m_accountManager->service(syncServiceName())); if (!srv.isValid()) { qCInfo(lcSocialPlugin) << "invalid service" << syncServiceName() << "specified, account" << account->id() << "will be disabled for" << m_serviceName << dataTypeName(m_dataType) << "sync"; return false; } account->selectService(srv); bool serviceEnabled = account->enabled(); account->selectService(Accounts::Service()); return globallyEnabled && serviceEnabled; } /*! \internal Called when the semaphores for all accounts have been decreased to zero. This is the final function which is called prior to telling buteo that the sync plugin can be destroyed. The implementation MUST be synchronous. */ void SocialNetworkSyncAdaptor::finalCleanup() { } /*! \internal Called when the semaphores decreased to 0, this method is used to finalize something, like saving all data to a database. You can call incrementSemaphore to perform asynchronous tasks in this method. finalize will then be called again when the asynchronous task is finished (and when decrementSemaphore is called), be sure to have a condition check in order not to run into an infinite loop. It is unsafe to call decrementSemaphore in this method, as the semaphore handling method will find that the semaphore went to 0 twice and will perform cleanup operations twice. Please call decrementSemaphore at the end of the asynchronous task (preferably in a slot), and only call incrementSemaphore for asynchronous tasks. */ void SocialNetworkSyncAdaptor::finalize(int accountId) { Q_UNUSED(accountId) } /*! \internal Returns the last sync timestamp for the given service, account and data type. If data from prior to this timestamp is received in subsequent requests, it does not need to be synced. This function will return an invalid QDateTime if no synchronisation has occurred. */ QDateTime SocialNetworkSyncAdaptor::lastSyncTimestamp(const QString &serviceName, const QString &dataType, int accountId) const { return m_syncDb->lastSyncTimestamp(serviceName, dataType, accountId); } /*! \internal Updates the last sync timestamp for the given service, account and data type to the given \a timestamp. */ bool SocialNetworkSyncAdaptor::updateLastSyncTimestamp(const QString &serviceName, const QString &dataType, int accountId, const QDateTime ×tamp) { // Workaround // TODO: do better, with a queue m_syncDb->addSyncTimestamp(serviceName, dataType, accountId, timestamp); m_syncDb->commit(); m_syncDb->wait(); return m_syncDb->writeStatus() == AbstractSocialCacheDatabase::Finished; } /*! \internal Returns the list of identifiers of accounts which have been synced for the given \a dataType. */ QList SocialNetworkSyncAdaptor::syncedAccounts(const QString &dataType) { return m_syncDb->syncedAccounts(m_serviceName, dataType); } /*! * \internal * Changes status if there is real change and emits statusChanged() signal. */ void SocialNetworkSyncAdaptor::setStatus(Status status) { if (m_status != status) { m_status = status; emit statusChanged(); } } /*! * \internal * Should be used in constructors to set the initial state * of enabled and status, without emitting signals * */ void SocialNetworkSyncAdaptor::setInitialActive(bool enabled) { m_enabled = enabled; if (enabled) { m_status = Inactive; } else { m_status = Invalid; } } /*! * \internal * Should be called by any specific sync adapter when * they've finished syncing data. The transition from * busy status to inactive status is what causes the * Buteo plugin to emit the sync results (and allows * subsequent syncs to occur). */ void SocialNetworkSyncAdaptor::setFinishedInactive() { finalCleanup(); qCInfo(lcSocialPlugin) << "Finished" << m_serviceName << SocialNetworkSyncAdaptor::dataTypeName(m_dataType) << "sync at:" << QDateTime::currentDateTime().toString(Qt::ISODate); setStatus(SocialNetworkSyncAdaptor::Inactive); } void SocialNetworkSyncAdaptor::incrementSemaphore(int accountId) { int semaphoreValue = m_accountSyncSemaphores.value(accountId); semaphoreValue += 1; m_accountSyncSemaphores.insert(accountId, semaphoreValue); qCDebug(lcSocialPlugin) << "incremented busy semaphore for account" << accountId << "to:" << semaphoreValue; } void SocialNetworkSyncAdaptor::decrementSemaphore(int accountId) { if (!m_accountSyncSemaphores.contains(accountId)) { qCWarning(lcSocialPlugin) << "no such semaphore for account" << accountId; return; } int semaphoreValue = m_accountSyncSemaphores.value(accountId); semaphoreValue -= 1; qCDebug(lcSocialPlugin) << "decremented busy semaphore for account" << accountId << "to:" << semaphoreValue; if (semaphoreValue < 0) { qCWarning(lcSocialPlugin) << "busy semaphore is negative for account" << accountId; return; } m_accountSyncSemaphores.insert(accountId, semaphoreValue); if (semaphoreValue == 0) { finalize(accountId); // With the newer implementation, in finalize we can raise semaphores, // so if after calling finalize, the semaphore count is not the same anymore, // we shouldn't update the sync timestamp if (m_accountSyncSemaphores.value(accountId) > 0) { return; } // finished all outstanding sync requests for this account. // update the sync time in the global sociald database. updateLastSyncTimestamp(m_serviceName, SocialNetworkSyncAdaptor::dataTypeName(m_dataType), accountId, QDateTime::currentDateTime().toTimeSpec(Qt::UTC)); // if all outstanding requests for all accounts have finished, // then update our status to Inactive / ready to handle more sync requests. bool allAreZero = true; QList semaphores = m_accountSyncSemaphores.values(); foreach (int sv, semaphores) { if (sv != 0) { allAreZero = false; break; } } if (allAreZero) { setFinishedInactive(); // Finished! } } } void SocialNetworkSyncAdaptor::timeoutReply() { QTimer *timer = qobject_cast(sender()); QNetworkReply *reply = timer->property("networkReply").value(); int accountId = timer->property("accountId").toInt(); qCWarning(lcSocialPlugin) << "network request timed out while performing sync with account" << accountId; m_networkReplyTimeouts[accountId].remove(reply); reply->setProperty("isError", QVariant::fromValue(true)); reply->finished(); // invoke finished, so that the error handling there decrements the semaphore etc. reply->disconnect(); } void SocialNetworkSyncAdaptor::setupReplyTimeout(int accountId, QNetworkReply *reply, int msecs) { // this function should be called whenever a new network request is performed. QTimer *timer = new QTimer(this); timer->setSingleShot(true); timer->setInterval(msecs); timer->setProperty("accountId", accountId); timer->setProperty("networkReply", QVariant::fromValue(reply)); connect(timer, SIGNAL(timeout()), this, SLOT(timeoutReply())); timer->start(); m_networkReplyTimeouts[accountId].insert(reply, timer); } void SocialNetworkSyncAdaptor::removeReplyTimeout(int accountId, QNetworkReply *reply) { // this function should be called by the finished() handler for the reply. QTimer *timer = m_networkReplyTimeouts[accountId].value(reply); if (!reply) { return; } delete timer; m_networkReplyTimeouts[accountId].remove(reply); } void SocialNetworkSyncAdaptor::triggerReplyTimeouts() { // if we've lost network connectivity, we should immediately timeout all replies. Q_FOREACH (int accountId, m_networkReplyTimeouts.keys()) { Q_FOREACH (QTimer *timer, m_networkReplyTimeouts[accountId]) { timer->stop(); timer->setInterval(1); timer->start(); } } } QJsonObject SocialNetworkSyncAdaptor::parseJsonObjectReplyData(const QByteArray &replyData, bool *ok) { QJsonDocument jsonDocument = QJsonDocument::fromJson(replyData); *ok = !jsonDocument.isEmpty(); if (*ok && jsonDocument.isObject()) { return jsonDocument.object(); } *ok = false; return QJsonObject(); } QJsonArray SocialNetworkSyncAdaptor::parseJsonArrayReplyData(const QByteArray &replyData, bool *ok) { QJsonDocument jsonDocument = QJsonDocument::fromJson(replyData); *ok = !jsonDocument.isEmpty(); if (*ok && jsonDocument.isArray()) { return jsonDocument.array(); } *ok = false; return QJsonArray(); } /* Valid data types are data types which are known to the API. Note that just because a data type is valid does not mean that it will necessarily be supported by a given social network sync adaptor. */ QStringList SocialNetworkSyncAdaptor::validDataTypes() { static QStringList retn(validDataTypesInitialiser()); return retn; } /* String for Enum since the DBus API uses strings */ QString SocialNetworkSyncAdaptor::dataTypeName(SocialNetworkSyncAdaptor::DataType t) { switch (t) { case SocialNetworkSyncAdaptor::Contacts: return QStringLiteral("Contacts"); case SocialNetworkSyncAdaptor::Calendars: return QStringLiteral("Calendars"); case SocialNetworkSyncAdaptor::Notifications: return QStringLiteral("Notifications"); case SocialNetworkSyncAdaptor::Images: return QStringLiteral("Images"); case SocialNetworkSyncAdaptor::Videos: return QStringLiteral("Videos"); case SocialNetworkSyncAdaptor::Posts: return QStringLiteral("Posts"); case SocialNetworkSyncAdaptor::Messages: return QStringLiteral("Messages"); case SocialNetworkSyncAdaptor::Emails: return QStringLiteral("Emails"); case SocialNetworkSyncAdaptor::Signon: return QStringLiteral("Signon"); case SocialNetworkSyncAdaptor::Backup: return QStringLiteral("Backup"); case SocialNetworkSyncAdaptor::BackupQuery: return QStringLiteral("BackupQuery"); case SocialNetworkSyncAdaptor::BackupRestore: return QStringLiteral("BackupRestore"); default: break; } return QString(); } void SocialNetworkSyncAdaptor::purgeCachedImages(SocialImagesDatabase *database, int accountId) { database->queryImages(accountId); database->wait(); QList images = database->images(); foreach (SocialImage::ConstPtr image, images) { qCDebug(lcSocialPlugin) << "Purge cached image " << image->imageFile() << " for account " << image->accountId(); QFile::remove(image->imageFile()); } database->removeImages(images); database->commit(); database->wait(); } void SocialNetworkSyncAdaptor::purgeExpiredImages(SocialImagesDatabase *database, int accountId) { database->queryExpired(accountId); database->wait(); QList images = database->images(); foreach (SocialImage::ConstPtr image, images) { qCDebug(lcSocialPlugin) << "Purge expired image " << image->imageFile() << " for account " << image->accountId(); QFile::remove(image->imageFile()); } database->removeImages(images); database->commit(); database->wait(); } buteo-sync-plugins-social-0.4.28/src/common/socialnetworksyncadaptor.h000066400000000000000000000120471474572147200262310ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2013-2014 Jolla Ltd. ** Contact: Chris Adams ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef SOCIALNETWORKSYNCADAPTOR_H #define SOCIALNETWORKSYNCADAPTOR_H #include #include #include #include #include #include #include #include "buteosyncfw_p.h" class QSqlDatabase; class QNetworkAccessManager; class QTimer; class QNetworkReply; class SocialNetworkSyncDatabase; class SocialImagesDatabase; namespace Accounts { class Account; class Manager; } class SocialNetworkSyncAdaptor : public QObject { Q_OBJECT Q_PROPERTY(Status status READ status NOTIFY statusChanged) Q_PROPERTY(bool enabled READ enabled NOTIFY enabledChanged) public: enum Status { Initializing = 0, Inactive, Busy, Error, Invalid }; enum PurgeMode { SyncPurge = 0, CleanUpPurge }; enum DataType { Contacts = 1, // "Contacts" Calendars, // "Calendars" Notifications, // "Notifications" Images, // "Images" Videos, // "Videos" Posts, // "Posts" Messages, // "Messages" Emails, // "Emails" Signon, // "Signon" -- for refreshing AccessTokens etc. Backup, // "Backup" BackupQuery, // "BackupQuery" BackupRestore // "BackupRestore" }; static QStringList validDataTypes(); static QString dataTypeName(DataType t); public: SocialNetworkSyncAdaptor(const QString &serviceName, SocialNetworkSyncAdaptor::DataType dataType, QNetworkAccessManager *qnam, QObject *parent); virtual ~SocialNetworkSyncAdaptor(); virtual QString syncServiceName() const = 0; void setAccountSyncProfile(Buteo::SyncProfile* perAccountSyncProfile); Status status() const; bool enabled() const; QString serviceName() const; virtual void sync(const QString &dataType, int accountId = 0); virtual void purgeDataForOldAccount(int accountId, PurgeMode mode = SyncPurge) = 0; virtual void abortSync(Sync::SyncStatus status); Q_SIGNALS: void statusChanged(); void enabledChanged(); protected: virtual bool checkAccount(Accounts::Account *account); virtual void finalCleanup(); virtual void finalize(int accountId); QDateTime lastSyncTimestamp(const QString &serviceName, const QString &dataType, int accountId) const; bool updateLastSyncTimestamp(const QString &serviceName, const QString &dataType, int accountId, const QDateTime ×tamp); QList syncedAccounts(const QString &dataType); void setStatus(Status status); void setInitialActive(bool enabled); void setFinishedInactive(); // whether the sync has been aborted (perhaps due to network connection loss) bool syncAborted() const; // Semaphore system void incrementSemaphore(int accountId); void decrementSemaphore(int accountId); // network reply timeouts void setupReplyTimeout(int accountId, QNetworkReply *reply, int msecs = 60000); void removeReplyTimeout(int accountId, QNetworkReply *reply); void triggerReplyTimeouts(); // Parsing methods static QJsonObject parseJsonObjectReplyData(const QByteArray &replyData, bool *ok); static QJsonArray parseJsonArrayReplyData(const QByteArray &replyData, bool *ok); // Cache management void purgeCachedImages(SocialImagesDatabase *database, int accountId); void purgeExpiredImages(SocialImagesDatabase *database, int accountId); const SocialNetworkSyncAdaptor::DataType m_dataType; Accounts::Manager * const m_accountManager; QNetworkAccessManager * const m_networkAccessManager; Buteo::SyncProfile *m_accountSyncProfile; protected Q_SLOTS: virtual void timeoutReply(); private: SocialNetworkSyncDatabase *m_syncDb; SocialNetworkSyncAdaptor::Status m_status; bool m_enabled; bool m_syncAborted; QString m_serviceName; QMap m_accountSyncSemaphores; QMap > m_networkReplyTimeouts; }; #endif // SOCIALNETWORKSYNCADAPTOR_H buteo-sync-plugins-social-0.4.28/src/common/trace.cpp000066400000000000000000000021011474572147200225140ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "trace.h" Q_LOGGING_CATEGORY(lcSocialPlugin, "buteo.plugin.social", QtWarningMsg) Q_LOGGING_CATEGORY(lcSocialPluginTrace, "buteo.plugin.social.trace", QtWarningMsg) buteo-sync-plugins-social-0.4.28/src/common/trace.h000066400000000000000000000021711474572147200221700ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2013-2014 Jolla Ltd. ** Contact: Chris Adams ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef TRACE_H #define TRACE_H #include Q_DECLARE_LOGGING_CATEGORY(lcSocialPlugin) Q_DECLARE_LOGGING_CATEGORY(lcSocialPluginTrace) #endif // TRACE_H buteo-sync-plugins-social-0.4.28/src/dropbox/000077500000000000000000000000001474572147200211055ustar00rootroot00000000000000buteo-sync-plugins-social-0.4.28/src/dropbox/dropbox-backup/000077500000000000000000000000001474572147200240255ustar00rootroot00000000000000buteo-sync-plugins-social-0.4.28/src/dropbox/dropbox-backup/dropbox-backup.pri000066400000000000000000000001601474572147200274560ustar00rootroot00000000000000SOURCES += $$PWD/dropboxbackupsyncadaptor.cpp HEADERS += $$PWD/dropboxbackupsyncadaptor.h INCLUDEPATH += $$PWD buteo-sync-plugins-social-0.4.28/src/dropbox/dropbox-backup/dropbox-backup.pro000066400000000000000000000013011474572147200274620ustar00rootroot00000000000000TARGET = dropbox-backup-client include($$PWD/../../common.pri) include($$PWD/../dropbox-common.pri) include($$PWD/../dropbox-backupoperation.pri) include($$PWD/dropbox-backup.pri) dropbox_backup_sync_profile.path = /etc/buteo/profiles/sync dropbox_backup_sync_profile.files = $$PWD/dropbox.Backup.xml dropbox_backup_client_plugin_xml.path = /etc/buteo/profiles/client dropbox_backup_client_plugin_xml.files = $$PWD/dropbox-backup.xml HEADERS += dropboxbackupplugin.h SOURCES += dropboxbackupplugin.cpp OTHER_FILES += \ dropbox_backup_sync_profile.files \ dropbox_backup_client_plugin_xml.files INSTALLS += \ target \ dropbox_backup_sync_profile \ dropbox_backup_client_plugin_xml buteo-sync-plugins-social-0.4.28/src/dropbox/dropbox-backup/dropbox-backup.xml000066400000000000000000000002051474572147200274640ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/dropbox/dropbox-backup/dropbox.Backup.xml000066400000000000000000000011311474572147200274240ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/dropbox/dropbox-backup/dropboxbackupplugin.cpp000066400000000000000000000036431474572147200306210ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2020 Open Mobile Platform LLC. ** Copyright (c) 2015 - 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "dropboxbackupplugin.h" #include "dropboxbackupsyncadaptor.h" #include "socialnetworksyncadaptor.h" DropboxBackupPlugin::DropboxBackupPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *callbackInterface) : SocialdButeoPlugin(pluginName, profile, callbackInterface, QStringLiteral("dropbox"), SocialNetworkSyncAdaptor::dataTypeName(SocialNetworkSyncAdaptor::Backup)) { } DropboxBackupPlugin::~DropboxBackupPlugin() { } SocialNetworkSyncAdaptor *DropboxBackupPlugin::createSocialNetworkSyncAdaptor() { return new DropboxBackupSyncAdaptor(this); } Buteo::ClientPlugin* DropboxBackupPluginLoader::createClientPlugin( const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) { return new DropboxBackupPlugin(pluginName, profile, cbInterface); } buteo-sync-plugins-social-0.4.28/src/dropbox/dropbox-backup/dropboxbackupplugin.h000066400000000000000000000036521474572147200302660ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2020 Open Mobile Platform LLC. ** Copyright (c) 2015 - 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef DROPBOXBACKUPPLUGIN_H #define DROPBOXBACKUPPLUGIN_H #include "socialdbuteoplugin.h" #include class Q_DECL_EXPORT DropboxBackupPlugin : public SocialdButeoPlugin { Q_OBJECT public: DropboxBackupPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *cbInterface); ~DropboxBackupPlugin(); protected: SocialNetworkSyncAdaptor *createSocialNetworkSyncAdaptor(); }; class DropboxBackupPluginLoader : public Buteo::SyncPluginLoader { Q_OBJECT Q_PLUGIN_METADATA(IID "org.sailfishos.plugins.sync.DropboxBackupPluginLoader") Q_INTERFACES(Buteo::SyncPluginLoader) public: Buteo::ClientPlugin* createClientPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) override; }; #endif // DROPBOXBACKUPPLUGIN_H buteo-sync-plugins-social-0.4.28/src/dropbox/dropbox-backup/dropboxbackupsyncadaptor.cpp000066400000000000000000000025651474572147200316540ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2015 - 2019 Jolla Ltd. ** Copyright (c) 2020 Open Mobile Platform LLC. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "dropboxbackupsyncadaptor.h" DropboxBackupSyncAdaptor::DropboxBackupSyncAdaptor(QObject *parent) : DropboxBackupOperationSyncAdaptor(SocialNetworkSyncAdaptor::Backup, parent) { setInitialActive(true); } DropboxBackupSyncAdaptor::~DropboxBackupSyncAdaptor() { } DropboxBackupOperationSyncAdaptor::Operation DropboxBackupSyncAdaptor::operation() const { return DropboxBackupOperationSyncAdaptor::Backup; } buteo-sync-plugins-social-0.4.28/src/dropbox/dropbox-backup/dropboxbackupsyncadaptor.h000066400000000000000000000025511474572147200313140ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2015 - 2019 Jolla Ltd. ** Copyright (c) 2020 Open Mobile Platform LLC. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef DROPBOXBACKUPSYNCADAPTOR_H #define DROPBOXBACKUPSYNCADAPTOR_H #include "dropboxbackupoperationsyncadaptor.h" class DropboxBackupSyncAdaptor : public DropboxBackupOperationSyncAdaptor { Q_OBJECT public: DropboxBackupSyncAdaptor(QObject *parent); ~DropboxBackupSyncAdaptor(); DropboxBackupOperationSyncAdaptor::Operation operation() const override; }; #endif // DROPBOXBACKUPSYNCADAPTOR_H buteo-sync-plugins-social-0.4.28/src/dropbox/dropbox-backupoperation.pri000066400000000000000000000002021474572147200264540ustar00rootroot00000000000000SOURCES += $$PWD/dropboxbackupoperationsyncadaptor.cpp HEADERS += $$PWD/dropboxbackupoperationsyncadaptor.h INCLUDEPATH += $$PWD buteo-sync-plugins-social-0.4.28/src/dropbox/dropbox-backupquery/000077500000000000000000000000001474572147200251135ustar00rootroot00000000000000buteo-sync-plugins-social-0.4.28/src/dropbox/dropbox-backupquery/dropbox-backupquery.pri000066400000000000000000000001721474572147200316350ustar00rootroot00000000000000SOURCES += $$PWD/dropboxbackupquerysyncadaptor.cpp HEADERS += $$PWD/dropboxbackupquerysyncadaptor.h INCLUDEPATH += $$PWD buteo-sync-plugins-social-0.4.28/src/dropbox/dropbox-backupquery/dropbox-backupquery.pro000066400000000000000000000014071474572147200316450ustar00rootroot00000000000000TARGET = dropbox-backupquery-client include($$PWD/../../common.pri) include($$PWD/../dropbox-common.pri) include($$PWD/../dropbox-backupoperation.pri) include($$PWD/dropbox-backupquery.pri) dropbox_backupquery_sync_profile.path = /etc/buteo/profiles/sync dropbox_backupquery_sync_profile.files = $$PWD/dropbox.BackupQuery.xml dropbox_backupquery_client_plugin_xml.path = /etc/buteo/profiles/client dropbox_backupquery_client_plugin_xml.files = $$PWD/dropbox-backupquery.xml HEADERS += dropboxbackupqueryplugin.h SOURCES += dropboxbackupqueryplugin.cpp OTHER_FILES += \ dropbox_backupquery_sync_profile.files \ dropbox_backupquery_client_plugin_xml.files INSTALLS += \ target \ dropbox_backupquery_sync_profile \ dropbox_backupquery_client_plugin_xml buteo-sync-plugins-social-0.4.28/src/dropbox/dropbox-backupquery/dropbox-backupquery.xml000066400000000000000000000002121474572147200316360ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/dropbox/dropbox-backupquery/dropbox.BackupQuery.xml000066400000000000000000000011311474572147200315400ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/dropbox/dropbox-backupquery/dropboxbackupqueryplugin.cpp000066400000000000000000000037311474572147200327730ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2020 Open Mobile Platform LLC. ** Copyright (c) 2015 - 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "dropboxbackupqueryplugin.h" #include "dropboxbackupquerysyncadaptor.h" #include "socialnetworksyncadaptor.h" DropboxBackupQueryPlugin::DropboxBackupQueryPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *callbackInterface) : SocialdButeoPlugin(pluginName, profile, callbackInterface, QStringLiteral("dropbox"), SocialNetworkSyncAdaptor::dataTypeName(SocialNetworkSyncAdaptor::BackupQuery)) { } DropboxBackupQueryPlugin::~DropboxBackupQueryPlugin() { } SocialNetworkSyncAdaptor *DropboxBackupQueryPlugin::createSocialNetworkSyncAdaptor() { return new DropboxBackupQuerySyncAdaptor(this); } Buteo::ClientPlugin* DropboxBackupQueryPluginLoader::createClientPlugin( const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) { return new DropboxBackupQueryPlugin(pluginName, profile, cbInterface); } buteo-sync-plugins-social-0.4.28/src/dropbox/dropbox-backupquery/dropboxbackupqueryplugin.h000066400000000000000000000037221474572147200324400ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2020 Open Mobile Platform LLC. ** Copyright (c) 2015 - 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef DROPBOXBACKUPQUERYPLUGIN_H #define DROPBOXBACKUPQUERYPLUGIN_H #include "socialdbuteoplugin.h" #include class Q_DECL_EXPORT DropboxBackupQueryPlugin : public SocialdButeoPlugin { Q_OBJECT public: DropboxBackupQueryPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *cbInterface); ~DropboxBackupQueryPlugin(); protected: SocialNetworkSyncAdaptor *createSocialNetworkSyncAdaptor(); }; class DropboxBackupQueryPluginLoader : public Buteo::SyncPluginLoader { Q_OBJECT Q_PLUGIN_METADATA(IID "org.sailfishos.plugins.sync.DropboxBackupQueryPluginLoader") Q_INTERFACES(Buteo::SyncPluginLoader) public: Buteo::ClientPlugin* createClientPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) override; }; #endif // DROPBOXBACKUPQUERYPLUGIN_H buteo-sync-plugins-social-0.4.28/src/dropbox/dropbox-backupquery/dropboxbackupquerysyncadaptor.cpp000066400000000000000000000025641474572147200340270ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2020 Open Mobile Platform LLC. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "dropboxbackupquerysyncadaptor.h" DropboxBackupQuerySyncAdaptor::DropboxBackupQuerySyncAdaptor(QObject *parent) : DropboxBackupOperationSyncAdaptor(SocialNetworkSyncAdaptor::BackupQuery, parent) { setInitialActive(true); } DropboxBackupQuerySyncAdaptor::~DropboxBackupQuerySyncAdaptor() { } DropboxBackupOperationSyncAdaptor::Operation DropboxBackupQuerySyncAdaptor::operation() const { return DropboxBackupOperationSyncAdaptor::BackupQuery; } buteo-sync-plugins-social-0.4.28/src/dropbox/dropbox-backupquery/dropboxbackupquerysyncadaptor.h000066400000000000000000000025361474572147200334730ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2020 Open Mobile Platform LLC. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef DROPBOXBACKUPQUERYSYNCADAPTOR_H #define DROPBOXBACKUPQUERYSYNCADAPTOR_H #include "dropboxbackupoperationsyncadaptor.h" class DropboxBackupQuerySyncAdaptor : public DropboxBackupOperationSyncAdaptor { Q_OBJECT public: DropboxBackupQuerySyncAdaptor(QObject *parent); ~DropboxBackupQuerySyncAdaptor(); DropboxBackupOperationSyncAdaptor::Operation operation() const override; }; #endif // DROPBOXBACKUPQUERYSYNCADAPTOR_H buteo-sync-plugins-social-0.4.28/src/dropbox/dropbox-backuprestore/000077500000000000000000000000001474572147200254315ustar00rootroot00000000000000buteo-sync-plugins-social-0.4.28/src/dropbox/dropbox-backuprestore/dropbox-backuprestore.pri000066400000000000000000000001761474572147200324750ustar00rootroot00000000000000SOURCES += $$PWD/dropboxbackuprestoresyncadaptor.cpp HEADERS += $$PWD/dropboxbackuprestoresyncadaptor.h INCLUDEPATH += $$PWD buteo-sync-plugins-social-0.4.28/src/dropbox/dropbox-backuprestore/dropbox-backuprestore.pro000066400000000000000000000014431474572147200325010ustar00rootroot00000000000000TARGET = dropbox-backuprestore-client include($$PWD/../../common.pri) include($$PWD/../dropbox-common.pri) include($$PWD/../dropbox-backupoperation.pri) include($$PWD/dropbox-backuprestore.pri) dropbox_backuprestore_sync_profile.path = /etc/buteo/profiles/sync dropbox_backuprestore_sync_profile.files = $$PWD/dropbox.BackupRestore.xml dropbox_backuprestore_client_plugin_xml.path = /etc/buteo/profiles/client dropbox_backuprestore_client_plugin_xml.files = $$PWD/dropbox-backuprestore.xml HEADERS += dropboxbackuprestoreplugin.h SOURCES += dropboxbackuprestoreplugin.cpp OTHER_FILES += \ dropbox_backuprestore_sync_profile.files \ dropbox_backuprestore_client_plugin_xml.files INSTALLS += \ target \ dropbox_backuprestore_sync_profile \ dropbox_backuprestore_client_plugin_xml buteo-sync-plugins-social-0.4.28/src/dropbox/dropbox-backuprestore/dropbox-backuprestore.xml000066400000000000000000000002141474572147200324740ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/dropbox/dropbox-backuprestore/dropbox.BackupRestore.xml000066400000000000000000000011411474572147200323750ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/dropbox/dropbox-backuprestore/dropboxbackuprestoreplugin.cpp000066400000000000000000000037571474572147200336370ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2020 Open Mobile Platform LLC. ** Copyright (c) 2015 - 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "dropboxbackuprestoreplugin.h" #include "dropboxbackuprestoresyncadaptor.h" #include "socialnetworksyncadaptor.h" DropboxBackupRestorePlugin::DropboxBackupRestorePlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *callbackInterface) : SocialdButeoPlugin(pluginName, profile, callbackInterface, QStringLiteral("dropbox"), SocialNetworkSyncAdaptor::dataTypeName(SocialNetworkSyncAdaptor::BackupRestore)) { } DropboxBackupRestorePlugin::~DropboxBackupRestorePlugin() { } SocialNetworkSyncAdaptor *DropboxBackupRestorePlugin::createSocialNetworkSyncAdaptor() { return new DropboxBackupRestoreSyncAdaptor(this); } Buteo::ClientPlugin* DropboxBackupRestorePluginLoader::createClientPlugin( const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) { return new DropboxBackupRestorePlugin(pluginName, profile, cbInterface); } buteo-sync-plugins-social-0.4.28/src/dropbox/dropbox-backuprestore/dropboxbackuprestoreplugin.h000066400000000000000000000037421474572147200332760ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2020 Open Mobile Platform LLC. ** Copyright (c) 2015 - 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef DROPBOXBACKUPRESTOREPLUGIN_H #define DROPBOXBACKUPRESTOREPLUGIN_H #include "socialdbuteoplugin.h" #include class Q_DECL_EXPORT DropboxBackupRestorePlugin : public SocialdButeoPlugin { Q_OBJECT public: DropboxBackupRestorePlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *cbInterface); ~DropboxBackupRestorePlugin(); protected: SocialNetworkSyncAdaptor *createSocialNetworkSyncAdaptor(); }; class DropboxBackupRestorePluginLoader : public Buteo::SyncPluginLoader { Q_OBJECT Q_PLUGIN_METADATA(IID "org.sailfishos.plugins.sync.DropboxBackupRestorePluginLoader") Q_INTERFACES(Buteo::SyncPluginLoader) public: Buteo::ClientPlugin* createClientPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) override; }; #endif // DROPBOXBACKUPRESTOREPLUGIN_H dropboxbackuprestoresyncadaptor.cpp000066400000000000000000000026041474572147200345770ustar00rootroot00000000000000buteo-sync-plugins-social-0.4.28/src/dropbox/dropbox-backuprestore/**************************************************************************** ** ** Copyright (c) 2020 Open Mobile Platform LLC. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "dropboxbackuprestoresyncadaptor.h" DropboxBackupRestoreSyncAdaptor::DropboxBackupRestoreSyncAdaptor(QObject *parent) : DropboxBackupOperationSyncAdaptor(SocialNetworkSyncAdaptor::BackupRestore, parent) { setInitialActive(true); } DropboxBackupRestoreSyncAdaptor::~DropboxBackupRestoreSyncAdaptor() { } DropboxBackupOperationSyncAdaptor::Operation DropboxBackupRestoreSyncAdaptor::operation() const { return DropboxBackupOperationSyncAdaptor::BackupRestore; } buteo-sync-plugins-social-0.4.28/src/dropbox/dropbox-backuprestore/dropboxbackuprestoresyncadaptor.h000066400000000000000000000025521474572147200343250ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2020 Open Mobile Platform LLC. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef DROPBOXBACKUPRESTORESYNCADAPTOR_H #define DROPBOXBACKUPRESTORESYNCADAPTOR_H #include "dropboxbackupoperationsyncadaptor.h" class DropboxBackupRestoreSyncAdaptor : public DropboxBackupOperationSyncAdaptor { Q_OBJECT public: DropboxBackupRestoreSyncAdaptor(QObject *parent); ~DropboxBackupRestoreSyncAdaptor(); DropboxBackupOperationSyncAdaptor::Operation operation() const override; }; #endif // DROPBOXBACKUPRESTORESYNCADAPTOR_H buteo-sync-plugins-social-0.4.28/src/dropbox/dropbox-common.pri000066400000000000000000000001631474572147200245640ustar00rootroot00000000000000INCLUDEPATH += $$PWD SOURCES += $$PWD/dropboxdatatypesyncadaptor.cpp HEADERS += $$PWD/dropboxdatatypesyncadaptor.h buteo-sync-plugins-social-0.4.28/src/dropbox/dropbox-images/000077500000000000000000000000001474572147200240255ustar00rootroot00000000000000buteo-sync-plugins-social-0.4.28/src/dropbox/dropbox-images/dropbox-images.pri000066400000000000000000000001561474572147200274630ustar00rootroot00000000000000SOURCES += $$PWD/dropboximagesyncadaptor.cpp HEADERS += $$PWD/dropboximagesyncadaptor.h INCLUDEPATH += $$PWD buteo-sync-plugins-social-0.4.28/src/dropbox/dropbox-images/dropbox-images.pro000066400000000000000000000013011474572147200274620ustar00rootroot00000000000000TARGET = dropbox-images-client include($$PWD/../../common.pri) include($$PWD/../dropbox-common.pri) include($$PWD/dropbox-images.pri) CONFIG += link_pkgconfig PKGCONFIG += mlite5 dropbox_images_sync_profile.path = /etc/buteo/profiles/sync dropbox_images_sync_profile.files = $$PWD/dropbox.Images.xml dropbox_images_client_plugin_xml.path = /etc/buteo/profiles/client dropbox_images_client_plugin_xml.files = $$PWD/dropbox-images.xml HEADERS += dropboximagesplugin.h SOURCES += dropboximagesplugin.cpp OTHER_FILES += \ dropbox_images_sync_profile.files \ dropbox_images_client_plugin_xml.files INSTALLS += \ target \ dropbox_images_sync_profile \ dropbox_images_client_plugin_xml buteo-sync-plugins-social-0.4.28/src/dropbox/dropbox-images/dropbox-images.xml000066400000000000000000000002051474572147200274640ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/dropbox/dropbox-images/dropbox.Images.xml000066400000000000000000000011351474572147200274300ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/dropbox/dropbox-images/dropboximagesplugin.cpp000066400000000000000000000035571474572147200306250ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2015 - 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "dropboximagesplugin.h" #include "dropboximagesyncadaptor.h" #include "socialnetworksyncadaptor.h" DropboxImagesPlugin::DropboxImagesPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *callbackInterface) : SocialdButeoPlugin(pluginName, profile, callbackInterface, QStringLiteral("dropbox"), SocialNetworkSyncAdaptor::dataTypeName(SocialNetworkSyncAdaptor::Images)) { } DropboxImagesPlugin::~DropboxImagesPlugin() { } SocialNetworkSyncAdaptor *DropboxImagesPlugin::createSocialNetworkSyncAdaptor() { return new DropboxImageSyncAdaptor(this); } Buteo::ClientPlugin* DropboxImagesPluginLoader::createClientPlugin( const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) { return new DropboxImagesPlugin(pluginName, profile, cbInterface); } buteo-sync-plugins-social-0.4.28/src/dropbox/dropbox-images/dropboximagesplugin.h000066400000000000000000000035711474572147200302660ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2015 - 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef DROPBOXIMAGESPLUGIN_H #define DROPBOXIMAGESPLUGIN_H #include "socialdbuteoplugin.h" #include class Q_DECL_EXPORT DropboxImagesPlugin : public SocialdButeoPlugin { Q_OBJECT public: DropboxImagesPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *cbInterface); ~DropboxImagesPlugin(); protected: SocialNetworkSyncAdaptor *createSocialNetworkSyncAdaptor(); }; class DropboxImagesPluginLoader : public Buteo::SyncPluginLoader { Q_OBJECT Q_PLUGIN_METADATA(IID "org.sailfishos.plugins.sync.DropboxImagesPluginLoader") Q_INTERFACES(Buteo::SyncPluginLoader) public: Buteo::ClientPlugin* createClientPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) override; }; #endif // DROPBOXIMAGESPLUGIN_H buteo-sync-plugins-social-0.4.28/src/dropbox/dropbox-images/dropboximagesyncadaptor.cpp000066400000000000000000000547241474572147200314750ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015 Jolla Ltd. ** Contact: Jonni Rainisto ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "dropboximagesyncadaptor.h" #include "trace.h" #include #include #include #include #include #include #include #include #include #include #include #include #include // Update the following version if database schema changes e.g. new // fields are added to the existing tables. // It will make old tables dropped and creates new ones. // Currently, we integrate with the device image gallery via saving thumbnails to the // ~/.config/sociald/images directory, and filling the ~/.config/sociald/images/dropbox.db // with appropriate data. namespace { bool filenameHasImageExtension(const QString &filename) { if (filename.endsWith(".jpg", Qt::CaseInsensitive) || filename.endsWith(".jpeg", Qt::CaseInsensitive) || filename.endsWith(".png", Qt::CaseInsensitive) || filename.endsWith(".tiff", Qt::CaseInsensitive) || filename.endsWith(".tif", Qt::CaseInsensitive) || filename.endsWith(".gif", Qt::CaseInsensitive) || filename.endsWith(".bmp", Qt::CaseInsensitive)) { return true; } return false; } } DropboxImageSyncAdaptor::DropboxImageSyncAdaptor(QObject *parent) : DropboxDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::Images, parent) , m_optimalThumbnailWidth(0) , m_optimalImageWidth(0) { setInitialActive(m_db.isValid()); } DropboxImageSyncAdaptor::~DropboxImageSyncAdaptor() { } QString DropboxImageSyncAdaptor::syncServiceName() const { return QStringLiteral("dropbox-images"); } void DropboxImageSyncAdaptor::sync(const QString &dataTypeString, int accountId) { // get ready for sync if (!determineOptimalDimensions()) { qCWarning(lcSocialPlugin) << "unable to determine optimal image dimensions, aborting"; setStatus(SocialNetworkSyncAdaptor::Error); return; } if (!initRemovalDetectionLists(accountId)) { qCWarning(lcSocialPlugin) << "unable to initialized cached account list for account" << accountId; setStatus(SocialNetworkSyncAdaptor::Error); return; } // call superclass impl. DropboxDataTypeSyncAdaptor::sync(dataTypeString, accountId); } void DropboxImageSyncAdaptor::purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode) { m_db.purgeAccount(oldId); m_db.commit(); m_db.wait(); // manage image cache. Gallery UI caches full size images // and maintains bindings between source and cached image in SocialImageDatabase. // purge cached images belonging to this account. purgeCachedImages(&m_imageCacheDb, oldId); } void DropboxImageSyncAdaptor::beginSync(int accountId, const QString &accessToken) { possiblyAddNewUser(QString::number(accountId), accountId, accessToken); queryCameraRollCursor(accountId, accessToken); } void DropboxImageSyncAdaptor::finalize(int accountId) { Q_UNUSED(accountId) if (syncAborted()) { qCInfo(lcSocialPlugin) << "sync aborted, won't commit database changes"; } else { // Remove albums m_db.removeAlbums(m_cachedAlbums.keys()); // Remove images m_db.removeImages(m_removedImages); m_db.commit(); m_db.wait(); // manage image cache. Gallery UI caches full size images // and maintains bindings between source and cached image in SocialImageDatabase. // purge cached images older than two weeks. purgeExpiredImages(&m_imageCacheDb, accountId); } } void DropboxImageSyncAdaptor::queryCameraRollCursor(int accountId, const QString &accessToken) { QJsonObject requestParameters; requestParameters.insert("path", "/Pictures"); requestParameters.insert("recursive", false); requestParameters.insert("include_media_info", true); requestParameters.insert("include_deleted", false); requestParameters.insert("include_has_explicit_shared_members", false); QJsonDocument doc; doc.setObject(requestParameters); QByteArray postData = doc.toJson(QJsonDocument::Compact); QUrl url(QStringLiteral("%1/2/files/list_folder/get_latest_cursor").arg(api())); QNetworkRequest req; req.setUrl(url); req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); req.setHeader(QNetworkRequest::ContentLengthHeader, postData.size()); req.setRawHeader(QString(QLatin1String("Authorization")).toUtf8(), QString(QLatin1String("Bearer ")).toUtf8() + accessToken.toUtf8()); qCDebug(lcSocialPlugin) << "querying camera roll cursor:" << url.toString(); QNetworkReply *reply = m_networkAccessManager->post(req, postData); if (reply) { reply->setProperty("accountId", accountId); reply->setProperty("accessToken", accessToken); connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(errorHandler(QNetworkReply::NetworkError))); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsHandler(QList))); connect(reply, SIGNAL(finished()), this, SLOT(cameraRollCursorFinishedHandler())); // we're requesting data. Increment the semaphore so that we know we're still busy. incrementSemaphore(accountId); setupReplyTimeout(accountId, reply); } else { qCWarning(lcSocialPlugin) << "unable to request data from Dropbox account with id" << accountId; clearRemovalDetectionLists(); // don't perform server-side removal detection during this sync run. } } void DropboxImageSyncAdaptor::cameraRollCursorFinishedHandler() { QNetworkReply *reply = qobject_cast(sender()); bool isError = reply->property("isError").toBool(); int accountId = reply->property("accountId").toInt(); QString accessToken = reply->property("accessToken").toString(); QString continuationUrl = reply->property("continuationUrl").toString(); QByteArray replyData = reply->readAll(); disconnect(reply); reply->deleteLater(); removeReplyTimeout(accountId, reply); bool ok = false; QJsonObject parsed = parseJsonObjectReplyData(replyData, &ok); if (isError || !ok || parsed.contains("error")) { qCWarning(lcSocialPlugin) << "unable to read Pictures cursor response for Dropbox account with id" << accountId; if (reply->error() == QNetworkReply::ContentNotFoundError) { qCDebug(lcSocialPlugin) << "Possibly" << reply->request().url().toString() << "is not available on server because no photos have been uploaded yet"; } QString errorResponse = QString::fromUtf8(replyData); Q_FOREACH (const QString &line, errorResponse.split('\n')) { qCDebug(lcSocialPlugin) << line; } clearRemovalDetectionLists(); // don't perform server-side removal detection during this sync run. decrementSemaphore(accountId); return; } QString cursor = parsed.value(QLatin1String("cursor")).toString(); QString userId = QString::number(accountId); QString albumId = "DropboxPictures-" + userId; // in future we might have multiple dropbox accounts const DropboxAlbum::ConstPtr &dbAlbum = m_cachedAlbums.value(albumId); m_cachedAlbums.remove(albumId); // this album exists, so remove it from the removal detection delta. if (!dbAlbum.isNull() && dbAlbum->hash() == cursor) { qCDebug(lcSocialPlugin) << "album with id" << albumId << "by user" << userId << "from Dropbox account with id" << accountId << "doesn't need sync"; decrementSemaphore(accountId); return; } // some changes have occurred, we need to sync. queryCameraRoll(accountId, accessToken, albumId, cursor, QString()); decrementSemaphore(accountId); } void DropboxImageSyncAdaptor::queryCameraRoll(int accountId, const QString &accessToken, const QString &albumId, const QString &cursor, const QString &continuationCursor) { QJsonObject requestParameters; if (continuationCursor.isEmpty()) { requestParameters.insert("path", "/Pictures"); requestParameters.insert("include_deleted", false); requestParameters.insert("include_has_explicit_shared_members", false); } else { requestParameters.insert("cursor", continuationCursor); } QJsonDocument doc; doc.setObject(requestParameters); QByteArray postData = doc.toJson(QJsonDocument::Compact); QUrl url; if (continuationCursor.isEmpty()) { url = QUrl(QStringLiteral("%1/2/files/list_folder").arg(api())); } else { url = QUrl(QStringLiteral("%1/2/files/list_folder_continue").arg(api())); } QNetworkRequest req; req.setUrl(url); req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); req.setHeader(QNetworkRequest::ContentLengthHeader, postData.size()); req.setRawHeader(QString(QLatin1String("Authorization")).toUtf8(), QString(QLatin1String("Bearer ")).toUtf8() + accessToken.toUtf8()); qCDebug(lcSocialPlugin) << "querying camera roll:" << url.toString(); QNetworkReply *reply = m_networkAccessManager->post(req, postData); if (reply) { reply->setProperty("accountId", accountId); reply->setProperty("accessToken", accessToken); reply->setProperty("albumId", albumId); reply->setProperty("cursor", cursor); connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(errorHandler(QNetworkReply::NetworkError))); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsHandler(QList))); connect(reply, SIGNAL(finished()), this, SLOT(cameraRollFinishedHandler())); // we're requesting data. Increment the semaphore so that we know we're still busy. incrementSemaphore(accountId); setupReplyTimeout(accountId, reply); } else { qCWarning(lcSocialPlugin) << "unable to request data from Dropbox account with id" << accountId; clearRemovalDetectionLists(); // don't perform server-side removal detection during this sync run. } } void DropboxImageSyncAdaptor::cameraRollFinishedHandler() { QNetworkReply *reply = qobject_cast(sender()); bool isError = reply->property("isError").toBool(); int accountId = reply->property("accountId").toInt(); QString accessToken = reply->property("accessToken").toString(); QString albumId = reply->property("albumId").toString(); QString cursor = reply->property("cursor").toString(); QByteArray replyData = reply->readAll(); disconnect(reply); reply->deleteLater(); removeReplyTimeout(accountId, reply); bool ok = false; QJsonObject parsed = parseJsonObjectReplyData(replyData, &ok); if (isError || !ok || parsed.contains("error")) { qCWarning(lcSocialPlugin) << "unable to read albums response for Dropbox account with id" << accountId; if (reply->error() == QNetworkReply::ContentNotFoundError) { qCDebug(lcSocialPlugin) << "Possibly" << reply->request().url().toString() << "is not available on server because no photos have been uploaded yet"; } QString errorResponse = QString::fromUtf8(replyData); Q_FOREACH (const QString &line, errorResponse.split('\n')) { qCDebug(lcSocialPlugin) << line; } clearRemovalDetectionLists(); // don't perform server-side removal detection during this sync run. decrementSemaphore(accountId); return; } // read the pictures information QJsonArray data = parsed.value(QLatin1String("entries")).toArray(); for (int i = 0; i < data.size(); ++i) { QJsonObject fileObject = data.at(i).toObject(); const QString &remoteFilePath = fileObject.value("path_display").toString(); if (!fileObject.isEmpty() && filenameHasImageExtension(remoteFilePath)) { m_retrievedObjects.append(fileObject); } } QString continuationCursor = parsed.value(QLatin1String("cursor")).toString(); bool hasMore = parsed.value(QLatin1String("has_more")).toBool(); if (hasMore) { queryCameraRoll(accountId, accessToken, albumId, cursor, continuationCursor); } else { // we have retrieved all of the image file objects data. QString userId = QString::number(accountId); QString albumName = "Pictures"; // TODO: do we need to translate? m_db.syncAccount(accountId, userId); m_db.addAlbum(albumId, userId, QDateTime(), QDateTime(), albumName, m_retrievedObjects.size(), cursor); // process the objects and update the database. for (int i = 0; i < m_retrievedObjects.size(); ++i) { QJsonObject fileObject = m_retrievedObjects.at(i).toObject(); const QString &remoteFilePath = fileObject.value("path_display").toString(); QString photoId = fileObject.value(QLatin1String("rev")).toString(); QString photoName = remoteFilePath.split("/").last(); // Previously, we retrieved imageWidth and imageHeight via the media_info. // Dropbox have deprecated that, so now the only way to get the information // is via per-file get_metadata requests, which are prohibitively expensive. // Instead, just define these placeholder values, as it turns out that the // values are never used in practice. int imageWidth = 1024; int imageHeight = 768; QString createdTimeStr = fileObject.value(QLatin1String("client_modified")).toString(); QDateTime createdTime = QDateTime::fromString(createdTimeStr, Qt::ISODate); QString updatedTimeStr = fileObject.value(QLatin1String("server_modified")).toString(); QDateTime updatedTime = QDateTime::fromString(updatedTimeStr, Qt::ISODate); if (!m_serverImageIds[albumId].contains(photoId)) { m_serverImageIds[albumId].insert(photoId); } QString thumbnailAPIUrl = content() + "/2/files/get_thumbnail"; QString fileAPIUrl = content() + "/2/files/download"; QString thumbnailSizeStr; if (m_optimalThumbnailWidth <= 32) { thumbnailSizeStr = "w32h32"; } else if (m_optimalThumbnailWidth <= 64) { thumbnailSizeStr = "w64h64"; } else if (m_optimalThumbnailWidth <= 128) { thumbnailSizeStr = "w128h128"; } else if (m_optimalThumbnailWidth <= 480) { thumbnailSizeStr = "w640h480"; } else { thumbnailSizeStr = "w1024h768"; } QJsonObject thumbnailQueryObject; thumbnailQueryObject.insert("path", remoteFilePath); thumbnailQueryObject.insert("format", "jpeg"); thumbnailQueryObject.insert("size", thumbnailSizeStr); QByteArray thumbnailQueryArg = QJsonDocument(thumbnailQueryObject).toJson(QJsonDocument::Compact); QString thumbnailUrl = thumbnailAPIUrl + "?arg=" + QString::fromUtf8(thumbnailQueryArg.toPercentEncoding()); QJsonObject fileQueryObject; fileQueryObject.insert("path", remoteFilePath); QByteArray fileQueryArg = QJsonDocument(fileQueryObject).toJson(QJsonDocument::Compact); QString imageSrcUrl = fileAPIUrl + "?arg=" + QString::fromUtf8(fileQueryArg.toPercentEncoding()); // check if we need to sync, and write to the database. if (haveAlreadyCachedImage(photoId, imageSrcUrl)) { qCDebug(lcSocialPlugin) << "have previously cached photo" << photoId << ":" << imageSrcUrl; } else { qCDebug(lcSocialPlugin) << "caching new photo" << photoId << ":" << imageSrcUrl << "->" << imageWidth << "x" << imageHeight; m_db.addImage(photoId, albumId, userId, createdTime, updatedTime, photoName, imageWidth, imageHeight, thumbnailUrl, imageSrcUrl, accessToken); } } checkRemovedImages(albumId); } decrementSemaphore(accountId); } bool DropboxImageSyncAdaptor::haveAlreadyCachedImage(const QString &imageId, const QString &imageUrl) { DropboxImage::ConstPtr dbImage = m_db.image(imageId); bool imagedbSynced = !dbImage.isNull(); if (!imagedbSynced) { return false; } QString dbImageUrl = dbImage->imageUrl(); if (dbImageUrl != imageUrl) { qCWarning(lcSocialPlugin) << "Image/dropbox.db has outdated data!\n" " photoId:" << imageId << "\n" " cached image url:" << dbImageUrl << "\n" " new image url:" << imageUrl; return false; } return true; } void DropboxImageSyncAdaptor::possiblyAddNewUser(const QString &userId, int accountId, const QString &accessToken) { if (!m_db.user(userId).isNull()) { return; } // We need to add the user. We call Dropbox to get the informations that we // need and then add it to the database https://api.dropboxapi.com/2/users/get_current_account QUrl url(QStringLiteral("%1/2/users/get_current_account").arg(api())); QNetworkRequest req; req.setUrl(url); req.setRawHeader(QString(QLatin1String("Authorization")).toUtf8(), QString(QLatin1String("Bearer ")).toUtf8() + accessToken.toUtf8()); qCDebug(lcSocialPlugin) << "querying Dropbox account info:" << url.toString(); QNetworkReply *reply = m_networkAccessManager->post(req, QByteArray()); if (reply) { reply->setProperty("accountId", accountId); reply->setProperty("accessToken", accessToken); connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(errorHandler(QNetworkReply::NetworkError))); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsHandler(QList))); connect(reply, SIGNAL(finished()), this, SLOT(userFinishedHandler())); incrementSemaphore(accountId); setupReplyTimeout(accountId, reply); } } void DropboxImageSyncAdaptor::userFinishedHandler() { QNetworkReply *reply = qobject_cast(sender()); QByteArray replyData = reply->readAll(); int accountId = reply->property("accountId").toInt(); disconnect(reply); reply->deleteLater(); bool ok = false; QJsonObject parsed = parseJsonObjectReplyData(replyData, &ok); if (!ok || !parsed.contains(QLatin1String("name"))) { qCWarning(lcSocialPlugin) << "unable to read user response for Dropbox account with id" << accountId; return; } // QString userId = parsed.value(QLatin1String("id")).toString(); QJsonObject name = parsed.value(QLatin1String("name")).toObject(); QString display_name = name.value(QLatin1String("display_name")).toString(); if (display_name.isEmpty()) { qCWarning(lcSocialPlugin) << "unable to read user display name for Dropbox account with id" << accountId; return; } m_db.addUser(QString::number(accountId), QDateTime::currentDateTime(), display_name); decrementSemaphore(accountId); } bool DropboxImageSyncAdaptor::initRemovalDetectionLists(int accountId) { // This function should be called as part of the ::sync() preamble. // Clear our internal state variables which we use to track server-side deletions. // We have to do it this way, as results can be spread across multiple requests // if Dropbox returns results in paginated form. clearRemovalDetectionLists(); bool ok = false; QMap accounts = m_db.accounts(&ok); if (!ok) { return false; } if (accounts.contains(accountId)) { QString userId = accounts.value(accountId); QStringList allAlbumIds = m_db.allAlbumIds(); foreach (const QString& albumId, allAlbumIds) { DropboxAlbum::ConstPtr album = m_db.album(albumId); if (album->userId() == userId) { m_cachedAlbums.insert(albumId, album); } } } return true; } void DropboxImageSyncAdaptor::clearRemovalDetectionLists() { m_cachedAlbums.clear(); m_serverImageIds.clear(); m_removedImages.clear(); } void DropboxImageSyncAdaptor::checkRemovedImages(const QString &albumId) { const QSet &serverImageIds = m_serverImageIds.value(albumId); QSet cachedImageIds = m_db.imageIds(albumId).toSet(); foreach (const QString &imageId, serverImageIds) { cachedImageIds.remove(imageId); } m_removedImages.append(cachedImageIds.toList()); } bool DropboxImageSyncAdaptor::determineOptimalDimensions() { int width = 0, height = 0; const int defaultValue = 0; MDConfItem widthConf("/lipstick/screen/primary/width"); if (widthConf.value(defaultValue).toInt() != defaultValue) { width = widthConf.value(defaultValue).toInt(); } MDConfItem heightConf("/lipstick/screen/primary/height"); if (heightConf.value(defaultValue).toInt() != defaultValue) { height = heightConf.value(defaultValue).toInt(); } // we want to use the largest of these dimensions as the "optimal" int maxDimension = qMax(width, height); if (maxDimension % 3 == 0) { m_optimalThumbnailWidth = maxDimension / 3; } else { m_optimalThumbnailWidth = (maxDimension / 2); } m_optimalImageWidth = maxDimension; qCDebug(lcSocialPlugin) << "Determined optimal image dimension:" << m_optimalImageWidth << ", thumbnail:" << m_optimalThumbnailWidth; return true; } buteo-sync-plugins-social-0.4.28/src/dropbox/dropbox-images/dropboximagesyncadaptor.h000066400000000000000000000061111474572147200311250ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015 Jolla Ltd. ** Contact: Jonni Rainisto ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef DROPBOXIMAGESYNCADAPTOR_H #define DROPBOXIMAGESYNCADAPTOR_H #include "dropboxdatatypesyncadaptor.h" #include #include #include #include #include #include #include #include #include #include #include #include class DropboxImageSyncAdaptor : public DropboxDataTypeSyncAdaptor { Q_OBJECT public: DropboxImageSyncAdaptor(QObject *parent); ~DropboxImageSyncAdaptor(); QString syncServiceName() const; void sync(const QString &dataTypeString, int accountId); protected: // implementing DropboxDataTypeSyncAdaptor interface void purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode mode); void beginSync(int accountId, const QString &accessToken); void finalize(int accountId); private: void queryCameraRollCursor(int accountId, const QString &accessToken); void queryCameraRoll(int accountId, const QString &accessToken, const QString &albumId, const QString &cursor, const QString &continuationCursor); bool haveAlreadyCachedImage(const QString &fbImageId, const QString &imageUrl); void possiblyAddNewUser(const QString &fbUserId, int accountId, const QString &accessToken); private Q_SLOTS: void cameraRollCursorFinishedHandler(); void cameraRollFinishedHandler(); void userFinishedHandler(); private: // for server-side removal detection. bool initRemovalDetectionLists(int accountId); void clearRemovalDetectionLists(); void checkRemovedImages(const QString &fbAlbumId); QMap m_cachedAlbums; QMap > m_serverImageIds; QStringList m_removedImages; DropboxImagesDatabase m_db; SocialImagesDatabase m_imageCacheDb; QJsonArray m_retrievedObjects; bool determineOptimalDimensions(); int m_optimalThumbnailWidth; int m_optimalImageWidth; }; #endif // DROPBOXIMAGESYNCADAPTOR_H buteo-sync-plugins-social-0.4.28/src/dropbox/dropbox.pro000066400000000000000000000002251474572147200233030ustar00rootroot00000000000000TEMPLATE = subdirs SUBDIRS = \ $$PWD/dropbox-backup \ $$PWD/dropbox-backupquery \ $$PWD/dropbox-backuprestore \ $$PWD/dropbox-images buteo-sync-plugins-social-0.4.28/src/dropbox/dropboxbackupoperationsyncadaptor.cpp000066400000000000000000000777751474572147200306740ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2020 Open Mobile Platform LLC. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "dropboxbackupoperationsyncadaptor.h" #include "trace.h" #include #include #include #include #include #include #include #include #include namespace { void debugDumpResponse(const QByteArray &data) { QStringList lines = QString::fromUtf8(data).split('\n'); Q_FOREACH (const QString &line, lines) { qCDebug(lcSocialPlugin) << line; } } } DropboxBackupOperationSyncAdaptor::DropboxBackupOperationSyncAdaptor(SocialNetworkSyncAdaptor::DataType dataType, QObject *parent) : DropboxDataTypeSyncAdaptor(dataType, parent) , m_sailfishBackup(new QDBusInterface("org.sailfishos.backup", "/sailfishbackup", "org.sailfishos.backup", QDBusConnection::sessionBus(), this)) { m_sailfishBackup->connection().connect( m_sailfishBackup->service(), m_sailfishBackup->path(), m_sailfishBackup->interface(), "cloudBackupStatusChanged", this, SLOT(cloudBackupStatusChanged(int,QString))); m_sailfishBackup->connection().connect( m_sailfishBackup->service(), m_sailfishBackup->path(), m_sailfishBackup->interface(), "cloudBackupError", this, SLOT(cloudBackupError(int,QString,QString))); m_sailfishBackup->connection().connect( m_sailfishBackup->service(), m_sailfishBackup->path(), m_sailfishBackup->interface(), "cloudRestoreStatusChanged", this, SLOT(cloudRestoreStatusChanged(int,QString))); m_sailfishBackup->connection().connect( m_sailfishBackup->service(), m_sailfishBackup->path(), m_sailfishBackup->interface(), "cloudRestoreError", this, SLOT(cloudRestoreError(int,QString,QString))); } DropboxBackupOperationSyncAdaptor::~DropboxBackupOperationSyncAdaptor() { } QString DropboxBackupOperationSyncAdaptor::syncServiceName() const { // this service covers all of these sync profiles: backup, backup query and restore. return QStringLiteral("dropbox-backup"); } void DropboxBackupOperationSyncAdaptor::sync(const QString &dataTypeString, int accountId) { DropboxDataTypeSyncAdaptor::sync(dataTypeString, accountId); } void DropboxBackupOperationSyncAdaptor::purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode) { purgeAccount(oldId); } void DropboxBackupOperationSyncAdaptor::beginSync(int accountId, const QString &accessToken) { QDBusReply backupDeviceIdReply = m_sailfishBackup->call("backupFileDeviceId"); if (backupDeviceIdReply.value().isEmpty()) { qCWarning(lcSocialPlugin) << "Backup device ID is invalid!"; setStatus(SocialNetworkSyncAdaptor::Error); return; } m_remoteDirPath = QString::fromLatin1("/Backups/%1").arg(backupDeviceIdReply.value()); m_accountId = accountId; m_accessToken = accessToken; switch (operation()) { case Backup: { QDBusReply createBackupReply = m_sailfishBackup->call("createBackupForSyncProfile", m_accountSyncProfile->name()); if (!createBackupReply.isValid() || createBackupReply.value().isEmpty()) { qCWarning(lcSocialPlugin) << "Call to createBackupForSyncProfile() failed:" << createBackupReply.error().name() << createBackupReply.error().message(); setStatus(SocialNetworkSyncAdaptor::Error); return; } // Save the file path, then wait for org.sailfish.backup service to finish creating the // backup before continuing in cloudBackupStatusChanged(). // we're requesting data. Increment the semaphore so that we know we're still busy. incrementSemaphore(accountId); m_localFileInfo = QFileInfo(createBackupReply.value()); break; } case BackupQuery: { requestList(accountId, accessToken, m_remoteDirPath, QString(), QVariantMap()); break; } case BackupRestore: { const QString filePath = m_accountSyncProfile->key(QStringLiteral("sfos-backuprestore-file")); if (filePath.isEmpty()) { qCWarning(lcSocialPlugin) << "No remote file has been set!"; setStatus(SocialNetworkSyncAdaptor::Error); return; } m_localFileInfo = QFileInfo(filePath); QDir localDir; if (!localDir.mkpath(m_localFileInfo.absolutePath())) { qCWarning(lcSocialPlugin) << "Could not create local backup directory:" << m_localFileInfo.absolutePath() << "for Dropbox account:" << accountId; setStatus(SocialNetworkSyncAdaptor::Error); return; } beginSyncOperation(accountId, accessToken); break; } default: qCWarning(lcSocialPlugin) << "Unrecognized sync operation: " + operation(); setStatus(SocialNetworkSyncAdaptor::Error); break; } } void DropboxBackupOperationSyncAdaptor::beginSyncOperation(int accountId, const QString &accessToken) { // dropbox requestData() function takes remoteFile param which has a fully specified path. QString remoteFile = QStringLiteral("%1/%2").arg(m_remoteDirPath).arg(m_localFileInfo.fileName()); // either upsync or downsync as required. if (operation() == Backup) { uploadData(accountId, accessToken, m_localFileInfo.absolutePath(), m_remoteDirPath, m_localFileInfo.fileName()); } else if (operation() == BackupRestore) { // step one: get the remote path and its children metadata. // step two: for each (non-folder) child in metadata, download it. QVariantMap properties = { { QStringLiteral("localPath"), m_localFileInfo.absolutePath() }, { QStringLiteral("remoteFile"), remoteFile }, }; requestList(accountId, accessToken, m_remoteDirPath, QString(), properties); } else { qCWarning(lcSocialPlugin) << "No direction set for Dropbox Backup sync with account:" << accountId; setStatus(SocialNetworkSyncAdaptor::Error); return; } } void DropboxBackupOperationSyncAdaptor::cloudBackupStatusChanged(int accountId, const QString &status) { if (accountId != m_accountId) { return; } qCDebug(lcSocialPlugin) << "Backup status changed:" << status << "for file:" << m_localFileInfo.absoluteFilePath(); if (status == QLatin1String("UploadingBackup")) { if (!m_localFileInfo.exists()) { qCWarning(lcSocialPlugin) << "Backup finished, but cannot find the backup file:" << m_localFileInfo.absoluteFilePath(); setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(m_accountId); return; } beginSyncOperation(m_accountId, m_accessToken); decrementSemaphore(m_accountId); } else if (status == QLatin1String("Canceled")) { qCWarning(lcSocialPlugin) << "Cloud backup was canceled"; setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(m_accountId); } else if (status == QLatin1String("Error")) { qCWarning(lcSocialPlugin) << "Failed to create backup file:" << m_localFileInfo.absoluteFilePath(); setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(m_accountId); } } void DropboxBackupOperationSyncAdaptor::cloudBackupError(int accountId, const QString &error, const QString &errorString) { if (accountId != m_accountId) { return; } qCWarning(lcSocialPlugin) << "Cloud backup error was:" << error << errorString; setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(m_accountId); } void DropboxBackupOperationSyncAdaptor::cloudRestoreStatusChanged(int accountId, const QString &status) { if (accountId != m_accountId) { return; } qCDebug(lcSocialPlugin) << "Backup restore status changed:" << status << "for file:" << m_localFileInfo.absoluteFilePath(); if (status == QLatin1String("Canceled")) { qCWarning(lcSocialPlugin) << "Cloud backup restore was canceled"; setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(m_accountId); } else if (status == QLatin1String("Error")) { qCWarning(lcSocialPlugin) << "Cloud backup restore failed"; setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(m_accountId); } } void DropboxBackupOperationSyncAdaptor::cloudRestoreError(int accountId, const QString &error, const QString &errorString) { if (accountId != m_accountId) { return; } qCWarning(lcSocialPlugin) << "Cloud backup restore error was:" << error << errorString; } void DropboxBackupOperationSyncAdaptor::requestList(int accountId, const QString &accessToken, const QString &remotePath, const QString &continuationCursor, const QVariantMap &extraProperties) { QJsonObject requestParameters; if (continuationCursor.isEmpty()) { requestParameters.insert("path", remotePath); requestParameters.insert("recursive", false); requestParameters.insert("include_deleted", false); requestParameters.insert("include_has_explicit_shared_members", false); } else { if (!continuationCursor.isEmpty()) { requestParameters.insert("cursor", continuationCursor); } } QJsonDocument doc; doc.setObject(requestParameters); QByteArray postData = doc.toJson(QJsonDocument::Compact); QUrl url; if (continuationCursor.isEmpty()) { url = QUrl(QStringLiteral("%1/2/files/list_folder").arg(api())); } else { url = QUrl(QStringLiteral("%1/2/files/list_folder_continue").arg(api())); } QNetworkRequest req; req.setUrl(url); req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); req.setHeader(QNetworkRequest::ContentLengthHeader, postData.size()); req.setRawHeader(QString(QLatin1String("Authorization")).toUtf8(), QString(QLatin1String("Bearer ")).toUtf8() + accessToken.toUtf8()); qCDebug(lcSocialPlugin) << "performing directory request:" << url.toString() << ":" << remotePath << continuationCursor; QNetworkReply *reply = m_networkAccessManager->post(req, postData); if (reply) { reply->setProperty("accountId", accountId); reply->setProperty("accessToken", accessToken); reply->setProperty("remotePath", remotePath); for (QVariantMap::const_iterator it = extraProperties.constBegin(); it != extraProperties.constEnd(); ++it) { reply->setProperty(it.key().toUtf8().constData(), it.value()); } connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(errorHandler(QNetworkReply::NetworkError))); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsHandler(QList))); connect(reply, SIGNAL(finished()), this, SLOT(remotePathFinishedHandler())); // we're requesting data. Increment the semaphore so that we know we're still busy. incrementSemaphore(accountId); setupReplyTimeout(accountId, reply, 10 * 60 * 1000); // 10 minutes } else { qCWarning(lcSocialPlugin) << "unable to request data from Dropbox account with id" << accountId; } } void DropboxBackupOperationSyncAdaptor::remotePathFinishedHandler() { QNetworkReply *reply = qobject_cast(sender()); QByteArray data = reply->readAll(); int accountId = reply->property("accountId").toInt(); QString accessToken = reply->property("accessToken").toString(); QString remotePath = reply->property("remotePath").toString(); int httpCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); bool isError = reply->property("isError").toBool(); reply->deleteLater(); removeReplyTimeout(accountId, reply); if (isError) { // Show error but don't set error status until error code is checked more thoroughly. qCWarning(lcSocialPlugin) << "error occurred when performing Backup remote path request for Dropbox account" << accountId; debugDumpResponse(data); } bool ok = false; const QJsonObject parsed = parseJsonObjectReplyData(data, &ok); const QJsonArray entries = parsed.value("entries").toArray(); if (!ok || entries.isEmpty()) { QString errorMessage = parsed.value("error_summary").toString(); if (!errorMessage.isEmpty()) { qCWarning(lcSocialPlugin) << "Dropbox returned error message:" << errorMessage; errorMessage.clear(); } // Directory may be not found or be empty if user has deleted backups. Only set the error // status if parsing failed or if there was an unexpected error code. if (!ok) { errorMessage = QStringLiteral("Failed to parse directory listing at %1 for account %2").arg(remotePath).arg(accountId); } else if (httpCode != 200 && httpCode != 404 && httpCode != 409 // Dropbox error when requested path is not found && httpCode != 410) { errorMessage = QStringLiteral("Directory listing request at %1 for account %2 failed").arg(remotePath).arg(accountId); } if (errorMessage.isEmpty()) { qCDebug(lcSocialPlugin) << "Completed directory listing for account:" << accountId; } else { qCWarning(lcSocialPlugin) << errorMessage; setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(accountId); return; } } QString continuationCursor = parsed.value("cursor").toString(); bool hasMore = parsed.value("has_more").toBool(); for (const QJsonValue &child : entries) { const QString tag = child.toObject().value(".tag").toString(); const QString childPath = child.toObject().value("path_display").toString(); if (tag.compare("folder", Qt::CaseInsensitive) == 0) { qCDebug(lcSocialPlugin) << "ignoring folder:" << childPath << "under remote backup path:" << remotePath << "for Dropbox account:" << accountId; } else if (tag.compare("file", Qt::CaseInsensitive) == 0){ qCDebug(lcSocialPlugin) << "found remote backup object:" << childPath << "for Dropbox account:" << accountId; m_backupFiles.insert(childPath); } } if (entries.isEmpty()) { qCDebug(lcSocialPlugin) << "No entries found in dir listing, but not an error (e.g. maybe file was deleted on server)"; debugDumpResponse(data); } else { qCDebug(lcSocialPlugin) << "Parsed dir listing entries:" << entries; } switch (operation()) { case BackupQuery: { if (hasMore) { requestList(accountId, accessToken, remotePath, continuationCursor, QVariantMap()); } else { QDBusReply setCloudBackupsReply = m_sailfishBackup->call("setCloudBackups", m_accountSyncProfile->name(), QVariant(m_backupFiles.toList())); if (!setCloudBackupsReply.isValid()) { qCDebug(lcSocialPlugin) << "Call to setCloudBackups() failed:" << setCloudBackupsReply.error().name() << setCloudBackupsReply.error().message(); } else { qCDebug(lcSocialPlugin) << "Wrote directory listing for" << m_accountSyncProfile->name(); } } break; } case Backup: case BackupRestore: { QString localPath = reply->property("localPath").toString(); QString remoteFile = reply->property("remoteFile").toString(); if (hasMore) { QVariantMap properties = { { QStringLiteral("localPath"), localPath }, { QStringLiteral("remoteFile"), remoteFile }, }; requestList(accountId, accessToken, remotePath, continuationCursor, properties); } else { bool fileFound = false; for (QSet::const_iterator it = m_backupFiles.constBegin(); it != m_backupFiles.constEnd(); it++) { if ((*it).endsWith(remoteFile)) { requestData(accountId, accessToken, localPath, remotePath, *it); fileFound = true; break; } } if (!fileFound) { qCWarning(lcSocialPlugin) << "Cannot find requested file on remote server:" << remoteFile; setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(accountId); return; } } break; } default: qCWarning(lcSocialPlugin) << "Unrecognized sync operation: " << operation(); setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(accountId); return; } decrementSemaphore(accountId); } void DropboxBackupOperationSyncAdaptor::requestData(int accountId, const QString &accessToken, const QString &localPath, const QString &remotePath, const QString &remoteFile) { // file download request QJsonObject fileQueryObject; fileQueryObject.insert("path", remoteFile); QByteArray fileQueryArg = QJsonDocument(fileQueryObject).toJson(QJsonDocument::Compact); QUrl url(QStringLiteral("%1/2/files/download?arg=%2").arg(content(), QString::fromUtf8(fileQueryArg.toPercentEncoding()))); QNetworkRequest req; req.setUrl(url); req.setHeader(QNetworkRequest::ContentTypeHeader, "application/octet-stream"); req.setRawHeader(QString(QLatin1String("Authorization")).toUtf8(), QString(QLatin1String("Bearer ")).toUtf8() + accessToken.toUtf8()); qCDebug(lcSocialPlugin) << "performing file download request:" << url.toString() << ":" << remoteFile; QNetworkReply *reply = m_networkAccessManager->post(req, QByteArray()); if (reply) { reply->setProperty("accountId", accountId); reply->setProperty("accessToken", accessToken); reply->setProperty("localPath", localPath); reply->setProperty("remotePath", remotePath); reply->setProperty("remoteFile", remoteFile); connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(errorHandler(QNetworkReply::NetworkError))); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsHandler(QList))); connect(reply, SIGNAL(downloadProgress(qint64,qint64)), this, SLOT(downloadProgressHandler(qint64,qint64))); connect(reply, SIGNAL(finished()), this, SLOT(remoteFileFinishedHandler())); // we're requesting data. Increment the semaphore so that we know we're still busy. incrementSemaphore(accountId); setupReplyTimeout(accountId, reply, 10 * 60 * 1000); // 10 minutes } else { qCWarning(lcSocialPlugin) << "unable to create download request:" << remotePath << remoteFile << "for Dropbox account with id" << accountId; } } void DropboxBackupOperationSyncAdaptor::remoteFileFinishedHandler() { QNetworkReply *reply = qobject_cast(sender()); QByteArray data = reply->readAll(); int accountId = reply->property("accountId").toInt(); QString localPath = reply->property("localPath").toString(); QString remotePath = reply->property("remotePath").toString(); QString remoteFile = reply->property("remoteFile").toString(); bool isError = reply->property("isError").toBool(); reply->deleteLater(); removeReplyTimeout(accountId, reply); if (isError) { qCWarning(lcSocialPlugin) << "error occurred when performing Backup remote file request for Dropbox account" << accountId; debugDumpResponse(data); setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(accountId); return; } if (data.isEmpty()) { qCInfo(lcSocialPlugin) << "remote file:" << remoteFile << "from" << remotePath << "is empty; ignoring"; } else { // create local directory if it doesn't exist QFileInfo fileInfo(QStringLiteral("%1/%2").arg(localPath).arg(QFileInfo(remoteFile).fileName())); QDir localDir; if (!localDir.mkpath(fileInfo.absolutePath())) { qCWarning(lcSocialPlugin) << "Could not create local backup directory:" << fileInfo.absolutePath() << "for Dropbox account:" << accountId; setStatus(SocialNetworkSyncAdaptor::Error); return; } QFile file(fileInfo.absoluteFilePath()); if (!file.open(QIODevice::WriteOnly)) { qCWarning(lcSocialPlugin) << "could not open" << file.fileName() << "locally for writing!"; setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(accountId); } else if (!file.write(data)) { qCWarning(lcSocialPlugin) << "could not write data to" << file.fileName() << "locally from" << remotePath << remoteFile << "for Dropbox account:" << accountId; setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(accountId); } else { qCDebug(lcSocialPlugin) << "successfully wrote" << data.size() << "bytes to:" << file.fileName() << "from:" << remoteFile; } file.close(); } decrementSemaphore(accountId); } void DropboxBackupOperationSyncAdaptor::uploadData(int accountId, const QString &accessToken, const QString &localPath, const QString &remotePath, const QString &localFile) { // step one: ensure the remote path exists (and if not, create it) // step two: upload every single file from the local path to the remote path. QNetworkReply *reply = 0; if (localFile.isEmpty()) { // attempt to create the remote path directory. QJsonObject requestParameters; requestParameters.insert("path", remotePath.startsWith(QLatin1String("/")) ? remotePath : QStringLiteral("/%1").arg(remotePath)); requestParameters.insert("autorename", false); QJsonDocument doc; doc.setObject(requestParameters); QByteArray postData = doc.toJson(QJsonDocument::Compact); QUrl url = QUrl(QStringLiteral("%1/2/files/create_folder_v2").arg(api())); QNetworkRequest req; req.setUrl(url); req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); req.setHeader(QNetworkRequest::ContentLengthHeader, postData.size()); req.setRawHeader(QString(QLatin1String("Authorization")).toUtf8(), QString(QLatin1String("Bearer ")).toUtf8() + accessToken.toUtf8()); qCDebug(lcSocialPlugin) << "Attempting to create the remote directory:" << remotePath << "via request:" << url.toString(); reply = m_networkAccessManager->post(req, postData); } else { // attempt to create a remote file. const QString filePath = remotePath.startsWith(QLatin1String("/")) ? QStringLiteral("%1/%2").arg(remotePath, localFile) : QStringLiteral("/%1/%2").arg(remotePath, localFile); QJsonObject requestParameters; requestParameters.insert("path", filePath); requestParameters.insert("mode", "overwrite"); QJsonDocument doc; doc.setObject(requestParameters); QByteArray requestParamData = doc.toJson(QJsonDocument::Compact); QUrl url = QUrl(QStringLiteral("%1/2/files/upload").arg(content())); QNetworkRequest req; req.setUrl(url); QString localFileName = QStringLiteral("%1/%2").arg(localPath).arg(localFile); QFile f(localFileName, this); if(!f.open(QIODevice::ReadOnly)){ qCWarning(lcSocialPlugin) << "unable to open local file:" << localFileName << "for upload to Dropbox Backup with account:" << accountId; } else { QByteArray data(f.readAll()); f.close(); QNetworkRequest req(url); req.setRawHeader(QString(QLatin1String("Authorization")).toUtf8(), QString(QLatin1String("Bearer ")).toUtf8() + accessToken.toUtf8()); req.setRawHeader(QString(QLatin1String("Dropbox-API-Arg")).toUtf8(), requestParamData); req.setHeader(QNetworkRequest::ContentLengthHeader, data.size()); req.setHeader(QNetworkRequest::ContentTypeHeader, "application/octet-stream"); qCDebug(lcSocialPlugin) << "Attempting to create the remote file:" << QStringLiteral("%1/%2").arg(remotePath).arg(localFile) << "via request:" << url.toString(); reply = m_networkAccessManager->post(req, data); } } if (reply) { reply->setProperty("accountId", accountId); reply->setProperty("accessToken", accessToken); reply->setProperty("localPath", localPath); reply->setProperty("remotePath", remotePath); reply->setProperty("localFile", localFile); connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(errorHandler(QNetworkReply::NetworkError))); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsHandler(QList))); if (localFile.isEmpty()) { connect(reply, SIGNAL(finished()), this, SLOT(createRemotePathFinishedHandler())); } else { connect(reply, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(uploadProgressHandler(qint64,qint64))); connect(reply, SIGNAL(finished()), this, SLOT(createRemoteFileFinishedHandler())); } // we're requesting data. Increment the semaphore so that we know we're still busy. incrementSemaphore(accountId); setupReplyTimeout(accountId, reply, 10 * 60 * 1000); // 10 minutes } else { qCWarning(lcSocialPlugin) << "unable to create upload request:" << localPath << localFile << "->" << remotePath << "for Dropbox account with id" << accountId; } } void DropboxBackupOperationSyncAdaptor::createRemotePathFinishedHandler() { QNetworkReply *reply = qobject_cast(sender()); QByteArray data = reply->readAll(); int accountId = reply->property("accountId").toInt(); QString accessToken = reply->property("accessToken").toString(); QString localPath = reply->property("localPath").toString(); QString remotePath = reply->property("remotePath").toString(); bool isError = reply->property("isError").toBool(); int httpCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); reply->deleteLater(); removeReplyTimeout(accountId, reply); if (isError) { // we actually expect a conflict error if the folder already existed, which is fine. if (httpCode != 409) { // this must be a real error. qCWarning(lcSocialPlugin) << "remote path creation failed:" << httpCode << QString::fromUtf8(data); debugDumpResponse(data); setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(accountId); return; } else { qCDebug(lcSocialPlugin) << "remote path creation had conflict: already exists:" << remotePath << ". Continuing."; } } // upload all files from the local path to the remote server. qCDebug(lcSocialPlugin) << "now uploading files from" << localPath << "to Dropbox folder:" << remotePath; QDir dir(localPath); QStringList localFiles = dir.entryList(QDir::Files); Q_FOREACH (const QString &localFile, localFiles) { qCDebug(lcSocialPlugin) << "about to upload:" << localFile << "to Dropbox folder:" << remotePath; uploadData(accountId, accessToken, localPath, remotePath, localFile); } decrementSemaphore(accountId); } void DropboxBackupOperationSyncAdaptor::createRemoteFileFinishedHandler() { QNetworkReply *reply = qobject_cast(sender()); QByteArray data = reply->readAll(); int accountId = reply->property("accountId").toInt(); QString localPath = reply->property("localPath").toString(); QString remotePath = reply->property("remotePath").toString(); QString localFile = reply->property("localFile").toString(); bool ok = true; QJsonObject parsed = parseJsonObjectReplyData(data, &ok); bool isError = reply->property("isError").toBool() || !ok || !parsed.value("error_summary").toString().isEmpty(); int httpCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); reply->deleteLater(); removeReplyTimeout(accountId, reply); if (isError) { qCWarning(lcSocialPlugin) << "failed to backup file:" << localPath << localFile << "to:" << remotePath << "for Dropbox account:" << accountId << ", code:" << httpCode << ":" << parsed.value("error_summary").toString(); debugDumpResponse(data); setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(accountId); return; } qCDebug(lcSocialPlugin) << "successfully uploaded backup of file:" << localPath << localFile << "to:" << remotePath << "for Dropbox account:" << accountId; decrementSemaphore(accountId); } void DropboxBackupOperationSyncAdaptor::downloadProgressHandler(qint64 bytesReceived, qint64 bytesTotal) { QNetworkReply *reply = qobject_cast(sender()); int accountId = reply->property("accountId").toInt(); QString localPath = reply->property("localPath").toString(); QString remotePath = reply->property("remotePath").toString(); QString localFile = reply->property("localFile").toString(); qCDebug(lcSocialPlugin) << "Have download progress: bytesReceived:" << bytesReceived << "of" << bytesTotal << ", for" << localPath << localFile << "from" << remotePath << "with account:" << accountId; } void DropboxBackupOperationSyncAdaptor::uploadProgressHandler(qint64 bytesSent, qint64 bytesTotal) { QNetworkReply *reply = qobject_cast(sender()); int accountId = reply->property("accountId").toInt(); QString localPath = reply->property("localPath").toString(); QString remotePath = reply->property("remotePath").toString(); QString localFile = reply->property("localFile").toString(); qCDebug(lcSocialPlugin) << "Have upload progress: bytesSent:" << bytesSent << "of" << bytesTotal << ", for" << localPath << localFile << "to" << remotePath << "with account:" << accountId; } void DropboxBackupOperationSyncAdaptor::finalize(int accountId) { qCDebug(lcSocialPlugin) << "Finalize Dropbox backup sync for account" << accountId; if (operation() == Backup) { qCDebug(lcSocialPlugin) << "Deleting created backup file" << m_localFileInfo.absoluteFilePath(); QFile::remove(m_localFileInfo.absoluteFilePath()); QDir().rmdir(m_localFileInfo.absolutePath()); } } void DropboxBackupOperationSyncAdaptor::purgeAccount(int) { // TODO: delete the contents of the localPath directory? probably not, could be shared between dropbox+onedrive } void DropboxBackupOperationSyncAdaptor::finalCleanup() { // nothing to do? } buteo-sync-plugins-social-0.4.28/src/dropbox/dropboxbackupoperationsyncadaptor.h000066400000000000000000000072421474572147200303170ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2020 Open Mobile Platform LLC. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef DROPBOXBACKUPOPERATIONSYNCADAPTOR_H #define DROPBOXBACKUPOPERATIONSYNCADAPTOR_H #include "dropboxdatatypesyncadaptor.h" #include #include #include #include #include class QDBusInterface; class DropboxBackupOperationSyncAdaptor : public DropboxDataTypeSyncAdaptor { Q_OBJECT public: enum Operation { Backup, BackupQuery, BackupRestore }; DropboxBackupOperationSyncAdaptor(SocialNetworkSyncAdaptor::DataType dataType, QObject *parent); ~DropboxBackupOperationSyncAdaptor(); QString syncServiceName() const override; void sync(const QString &dataTypeString, int accountId) override; virtual DropboxBackupOperationSyncAdaptor::Operation operation() const = 0; protected: // implementing DropboxDataTypeSyncAdaptor interface void purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode mode); void beginSync(int accountId, const QString &accessToken); void finalize(int accountId); void finalCleanup(); private: void requestList(int accountId, const QString &accessToken, const QString &remotePath, const QString &continuationCursor, const QVariantMap &extraProperties); void requestData(int accountId, const QString &accessToken, const QString &localPath, const QString &remotePath, const QString &remoteFile); void uploadData(int accountId, const QString &accessToken, const QString &localPath, const QString &remotePath, const QString &localFile); void purgeAccount(int accountId); void beginSyncOperation(int accountId, const QString &accessToken); private Q_SLOTS: void cloudBackupStatusChanged(int accountId, const QString &status); void cloudBackupError(int accountId, const QString &error, const QString &errorString); void cloudRestoreStatusChanged(int accountId, const QString &status); void cloudRestoreError(int accountId, const QString &error, const QString &errorString); void remotePathFinishedHandler(); void remoteFileFinishedHandler(); void createRemotePathFinishedHandler(); void createRemoteFileFinishedHandler(); void downloadProgressHandler(qint64 bytesReceived, qint64 bytesTotal); void uploadProgressHandler(qint64 bytesSent, qint64 bytesTotal); private: QDBusInterface *m_sailfishBackup = nullptr; QFileInfo m_localFileInfo; QSet m_backupFiles; QString m_remoteDirPath; QString m_accessToken; int m_accountId = 0; }; #endif // DropboxBackupOperationSyncAdaptor_H buteo-sync-plugins-social-0.4.28/src/dropbox/dropboxdatatypesyncadaptor.cpp000066400000000000000000000264661474572147200273100ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2015 - 2019 Jolla Ltd. ** Copyright (c) 2020 Open Mobile Platform LLC. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "dropboxdatatypesyncadaptor.h" #include "trace.h" #include #include #include #include #include //libsailfishkeyprovider #include // libaccounts-qt5 #include #include #include #include //libsignon-qt: SignOn::NoUserInteractionPolicy #include #include #include DropboxDataTypeSyncAdaptor::DropboxDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::DataType dataType, QObject *parent) : SocialNetworkSyncAdaptor("dropbox", dataType, 0, parent), m_triedLoading(false) { } DropboxDataTypeSyncAdaptor::~DropboxDataTypeSyncAdaptor() { } void DropboxDataTypeSyncAdaptor::sync(const QString &dataTypeString, int accountId) { if (dataTypeString != SocialNetworkSyncAdaptor::dataTypeName(m_dataType)) { qCWarning(lcSocialPlugin) << "Dropbox" << SocialNetworkSyncAdaptor::dataTypeName(m_dataType) << "sync adaptor was asked to sync" << dataTypeString; setStatus(SocialNetworkSyncAdaptor::Error); return; } if (clientId().isEmpty()) { qCWarning(lcSocialPlugin) << "client id couldn't be retrieved for Dropbox account" << accountId; setStatus(SocialNetworkSyncAdaptor::Error); return; } if (clientSecret().isEmpty()) { qCWarning(lcSocialPlugin) << "client secret couldn't be retrieved for Dropbox account" << accountId; setStatus(SocialNetworkSyncAdaptor::Error); return; } setStatus(SocialNetworkSyncAdaptor::Busy); updateDataForAccount(accountId); qCDebug(lcSocialPlugin) << "successfully triggered sync with profile:" << m_accountSyncProfile->name(); } void DropboxDataTypeSyncAdaptor::updateDataForAccount(int accountId) { Accounts::Account *account = Accounts::Account::fromId(m_accountManager, accountId, this); if (!account) { qCWarning(lcSocialPlugin) << "existing account with id" << accountId << "couldn't be retrieved"; setStatus(SocialNetworkSyncAdaptor::Error); return; } // will be decremented by either signOnError or signOnResponse. incrementSemaphore(accountId); signIn(account); } void DropboxDataTypeSyncAdaptor::finalCleanup() { } void DropboxDataTypeSyncAdaptor::errorHandler(QNetworkReply::NetworkError err) { // Dropbox sends error code 204 (HTTP code 401) for Unauthorized Error QNetworkReply *reply = qobject_cast(sender()); int httpCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (err == QNetworkReply::AuthenticationRequiredError) { qCInfo(lcSocialPlugin) << "sociald:Dropbox: would normally set CredentialsNeedUpdate for account" << reply->property("accountId").toInt() << "but could be spurious. Http code:" << httpCode; } qCWarning(lcSocialPlugin) << SocialNetworkSyncAdaptor::dataTypeName(m_dataType) << "request with account" << sender()->property("accountId").toInt() << "experienced error:" << httpCode; // set "isError" on the reply so that adapters know to ignore the result in the finished() handler reply->setProperty("isError", QVariant::fromValue(true)); // Note: not all errors are "unrecoverable" errors, so we don't change the status here. } void DropboxDataTypeSyncAdaptor::sslErrorsHandler(const QList &errs) { QString sslerrs; foreach (const QSslError &e, errs) { sslerrs += e.errorString() + "; "; } if (errs.size() > 0) { sslerrs.chop(2); } qCWarning(lcSocialPlugin) << SocialNetworkSyncAdaptor::dataTypeName(m_dataType) << "request with account" << sender()->property("accountId").toInt() << "experienced ssl errors:" << sslerrs; // set "isError" on the reply so that adapters know to ignore the result in the finished() handler sender()->setProperty("isError", QVariant::fromValue(true)); // Note: not all errors are "unrecoverable" errors, so we don't change the status here. } QString DropboxDataTypeSyncAdaptor::api() const { return m_api; } QString DropboxDataTypeSyncAdaptor::content() const { return m_content; } QString DropboxDataTypeSyncAdaptor::clientId() { if (!m_triedLoading) { loadClientIdAndSecret(); } return m_clientId; } QString DropboxDataTypeSyncAdaptor::clientSecret() { if (!m_triedLoading) { loadClientIdAndSecret(); } return m_clientSecret; } void DropboxDataTypeSyncAdaptor::loadClientIdAndSecret() { m_triedLoading = true; char *cClientId = NULL; char *cClientSecret = NULL; int cSuccess = SailfishKeyProvider_storedKey("dropbox", "dropbox-sync", "client_id", &cClientId); if (cClientId == NULL) { return; } else if (cSuccess != 0) { free(cClientId); return; } m_clientId = QLatin1String(cClientId); free(cClientId); cSuccess = SailfishKeyProvider_storedKey("dropbox", "dropbox-sync", "client_secret", &cClientSecret); if (cClientSecret == NULL) { return; } else if (cSuccess != 0) { free(cClientSecret); return; } m_clientSecret = QLatin1String(cClientSecret); free(cClientSecret); } void DropboxDataTypeSyncAdaptor::setCredentialsNeedUpdate(Accounts::Account *account) { qWarning() << "sociald:Dropbox: setting CredentialsNeedUpdate to true for account:" << account->id(); Accounts::Service srv(m_accountManager->service(syncServiceName())); account->selectService(srv); account->setValue(QStringLiteral("CredentialsNeedUpdate"), QVariant::fromValue(true)); account->setValue(QStringLiteral("CredentialsNeedUpdateFrom"), QVariant::fromValue(QString::fromLatin1("sociald-dropbox"))); account->selectService(Accounts::Service()); account->syncAndBlock(); } void DropboxDataTypeSyncAdaptor::signIn(Accounts::Account *account) { // Fetch consumer key and secret from keyprovider int accountId = account->id(); if (!checkAccount(account) || clientId().isEmpty() || clientSecret().isEmpty()) { decrementSemaphore(accountId); return; } // grab out a valid identity for the sync service. Accounts::Service srv(m_accountManager->service(syncServiceName())); account->selectService(srv); SignOn::Identity *identity = account->credentialsId() > 0 ? SignOn::Identity::existingIdentity(account->credentialsId()) : 0; if (!identity) { qCWarning(lcSocialPlugin) << "account" << accountId << "has no valid credentials; cannot sign in"; decrementSemaphore(accountId); return; } Accounts::AccountService accSrv(account, srv); QString method = accSrv.authData().method(); QString mechanism = accSrv.authData().mechanism(); SignOn::AuthSession *session = identity->createSession(method); if (!session) { qCWarning(lcSocialPlugin) << "could not create signon session for account" << accountId; identity->deleteLater(); decrementSemaphore(accountId); return; } QVariantMap signonSessionData = accSrv.authData().parameters(); signonSessionData.insert("ClientId", clientId()); signonSessionData.insert("ClientSecret", clientSecret()); signonSessionData.insert("UiPolicy", SignOn::NoUserInteractionPolicy); connect(session, SIGNAL(response(SignOn::SessionData)), this, SLOT(signOnResponse(SignOn::SessionData)), Qt::UniqueConnection); connect(session, SIGNAL(error(SignOn::Error)), this, SLOT(signOnError(SignOn::Error)), Qt::UniqueConnection); session->setProperty("account", QVariant::fromValue(account)); session->setProperty("identity", QVariant::fromValue(identity)); session->process(SignOn::SessionData(signonSessionData), mechanism); } void DropboxDataTypeSyncAdaptor::signOnError(const SignOn::Error &error) { SignOn::AuthSession *session = qobject_cast(sender()); Accounts::Account *account = session->property("account").value(); SignOn::Identity *identity = session->property("identity").value(); int accountId = account->id(); qCWarning(lcSocialPlugin) << "credentials for account with id" << accountId << "couldn't be retrieved:" << error.type() << error.message(); // if the error is because credentials have expired, we // set the CredentialsNeedUpdate key. if (error.type() == SignOn::Error::UserInteraction) { setCredentialsNeedUpdate(account); } session->disconnect(this); identity->destroySession(session); identity->deleteLater(); account->deleteLater(); // if we couldn't sign in, we can't sync with this account. setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(accountId); } void DropboxDataTypeSyncAdaptor::signOnResponse(const SignOn::SessionData &responseData) { QVariantMap data; foreach (const QString &key, responseData.propertyNames()) { data.insert(key, responseData.getProperty(key)); } QString accessToken; SignOn::AuthSession *session = qobject_cast(sender()); Accounts::Account *account = session->property("account").value(); SignOn::Identity *identity = session->property("identity").value(); int accountId = account->id(); if (data.contains(QLatin1String("AccessToken"))) { accessToken = data.value(QLatin1String("AccessToken")).toString(); } else { qCInfo(lcSocialPlugin) << "signon response for account with id" << accountId << "contained no access token"; } m_api = account->value(QStringLiteral("hosts/ApiHost")).toString(); if (m_api.isEmpty()) { m_api = QStringLiteral("https://api.dropboxapi.com"); } m_content = account->value(QStringLiteral("hosts/ContentHost")).toString(); if (m_content.isEmpty()) { m_content = QStringLiteral("https://content.dropboxapi.com"); } session->disconnect(this); identity->destroySession(session); identity->deleteLater(); account->deleteLater(); if (!accessToken.isEmpty()) { beginSync(accountId, accessToken); // call the derived-class sync entrypoint. } decrementSemaphore(accountId); } buteo-sync-plugins-social-0.4.28/src/dropbox/dropboxdatatypesyncadaptor.h000066400000000000000000000051761474572147200267500ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2015 - 2019 Jolla Ltd. ** Copyright (c) 2020 Open Mobile Platform LLC. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef DROPBOXDATATYPESYNCADAPTOR_H #define DROPBOXDATATYPESYNCADAPTOR_H #include "socialnetworksyncadaptor.h" #include #include #include #include #include namespace Accounts { class Account; } namespace SignOn { class Error; class SessionData; } /* Abstract interface for all of the data-specific sync adaptors which pull data from Dropbox's online services. */ class DropboxDataTypeSyncAdaptor : public SocialNetworkSyncAdaptor { Q_OBJECT public: DropboxDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::DataType dataType, QObject *parent); virtual ~DropboxDataTypeSyncAdaptor(); virtual void sync(const QString &dataTypeString, int accountId); protected: QString api() const; QString content() const; QString clientId(); QString clientSecret(); virtual void updateDataForAccount(int accountId); virtual void beginSync(int accountId, const QString &accessToken) = 0; virtual void finalCleanup(); protected Q_SLOTS: virtual void errorHandler(QNetworkReply::NetworkError err); virtual void sslErrorsHandler(const QList &errs); private Q_SLOTS: void signOnError(const SignOn::Error &error); void signOnResponse(const SignOn::SessionData &responseData); private: void loadClientIdAndSecret(); void setCredentialsNeedUpdate(Accounts::Account *account); void signIn(Accounts::Account *account); bool m_triedLoading; // Is true if we tried to load (even if we failed) QString m_clientId; QString m_clientSecret; QString m_api; QString m_content; }; #endif // DROPBOXDATATYPESYNCADAPTOR_H buteo-sync-plugins-social-0.4.28/src/facebook/000077500000000000000000000000001474572147200212015ustar00rootroot00000000000000buteo-sync-plugins-social-0.4.28/src/facebook/facebook-calendars/000077500000000000000000000000001474572147200247045ustar00rootroot00000000000000buteo-sync-plugins-social-0.4.28/src/facebook/facebook-calendars/facebook-calendars.pri000066400000000000000000000002711474572147200311230ustar00rootroot00000000000000CONFIG += link_pkgconfig PKGCONFIG += libmkcal-qt5 KF5CalendarCore SOURCES += $$PWD/facebookcalendarsyncadaptor.cpp HEADERS += $$PWD/facebookcalendarsyncadaptor.h INCLUDEPATH += $$PWD buteo-sync-plugins-social-0.4.28/src/facebook/facebook-calendars/facebook-calendars.pro000066400000000000000000000013141474572147200311300ustar00rootroot00000000000000TARGET = facebook-calendars-client include($$PWD/../../common.pri) include($$PWD/../facebook-common.pri) include($$PWD/facebook-calendars.pri) facebook_calendars_sync_profile.path = /etc/buteo/profiles/sync facebook_calendars_sync_profile.files = $$PWD/facebook.Calendars.xml facebook_calendars_client_plugin_xml.path = /etc/buteo/profiles/client facebook_calendars_client_plugin_xml.files = $$PWD/facebook-calendars.xml HEADERS += facebookcalendarsplugin.h SOURCES += facebookcalendarsplugin.cpp OTHER_FILES += \ facebook_calendars_sync_profile.files \ facebook_calendars_client_plugin_xml.files INSTALLS += \ target \ facebook_calendars_sync_profile \ facebook_calendars_client_plugin_xml buteo-sync-plugins-social-0.4.28/src/facebook/facebook-calendars/facebook-calendars.xml000066400000000000000000000002111474572147200311230ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/facebook/facebook-calendars/facebook.Calendars.xml000066400000000000000000000011551474572147200310740ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/facebook/facebook-calendars/facebookcalendarsplugin.cpp000066400000000000000000000036341474572147200322630ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2014 - 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "facebookcalendarsplugin.h" #include "facebookcalendarsyncadaptor.h" #include "socialnetworksyncadaptor.h" FacebookCalendarsPlugin::FacebookCalendarsPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *callbackInterface) : SocialdButeoPlugin(pluginName, profile, callbackInterface, QStringLiteral("facebook"), SocialNetworkSyncAdaptor::dataTypeName(SocialNetworkSyncAdaptor::Calendars)) { } FacebookCalendarsPlugin::~FacebookCalendarsPlugin() { } SocialNetworkSyncAdaptor *FacebookCalendarsPlugin::createSocialNetworkSyncAdaptor() { return new FacebookCalendarSyncAdaptor(this); } Buteo::ClientPlugin* FacebookCalendarsPluginLoader::createClientPlugin( const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) { return new FacebookCalendarsPlugin(pluginName, profile, cbInterface); } buteo-sync-plugins-social-0.4.28/src/facebook/facebook-calendars/facebookcalendarsplugin.h000066400000000000000000000036311474572147200317250ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2014 - 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef FACEBOOKCALENDARSPLUGIN_H #define FACEBOOKCALENDARSPLUGIN_H #include "socialdbuteoplugin.h" #include class Q_DECL_EXPORT FacebookCalendarsPlugin : public SocialdButeoPlugin { Q_OBJECT public: FacebookCalendarsPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *cbInterface); ~FacebookCalendarsPlugin(); protected: SocialNetworkSyncAdaptor *createSocialNetworkSyncAdaptor(); }; class FacebookCalendarsPluginLoader : public Buteo::SyncPluginLoader { Q_OBJECT Q_PLUGIN_METADATA(IID "org.sailfishos.plugins.sync.FacebookCalendarsPluginLoader") Q_INTERFACES(Buteo::SyncPluginLoader) public: Buteo::ClientPlugin* createClientPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) override; }; #endif // FACEBOOKCALENDARSPLUGIN_H buteo-sync-plugins-social-0.4.28/src/facebook/facebook-calendars/facebookcalendarsyncadaptor.cpp000066400000000000000000000530461474572147200331330ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2013-2014 Jolla Ltd. ** Contact: Lucien Xu ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "facebookcalendarsyncadaptor.h" #include "trace.h" #include #include #include #include #include #include #include #include #include #include #include #include static const char *FACEBOOK = "Facebook"; static const char *FACEBOOK_COLOR = "#3B5998"; FacebookParsedEvent::FacebookParsedEvent() : m_isDateOnly(false) , m_endExists(false) { } FacebookParsedEvent::FacebookParsedEvent(const FacebookParsedEvent &e) { m_id = e.m_id; m_isDateOnly = e.m_isDateOnly; m_endExists = e.m_endExists; m_startTime = e.m_startTime; m_endTime = e.m_endTime; m_summary = e.m_summary; m_description = e.m_description; } namespace { // returns true if the ghost-event cleanup sync has been performed. bool ghostEventCleanupPerformed() { QString settingsFileName = QString::fromLatin1("%1/%2/fbcal.ini") .arg(PRIVILEGED_DATA_DIR) .arg(QString::fromLatin1(SYNC_DATABASE_DIR)); QSettings settingsFile(settingsFileName, QSettings::IniFormat); return settingsFile.value(QString::fromLatin1("cleaned"), QVariant::fromValue(false)).toBool(); } void setGhostEventCleanupPerformed() { QString settingsFileName = QString::fromLatin1("%1/%2/fbcal.ini") .arg(PRIVILEGED_DATA_DIR) .arg(QString::fromLatin1(SYNC_DATABASE_DIR)); QSettings settingsFile(settingsFileName, QSettings::IniFormat); settingsFile.setValue(QString::fromLatin1("cleaned"), QVariant::fromValue(true)); settingsFile.sync(); } } FacebookCalendarSyncAdaptor::FacebookCalendarSyncAdaptor(QObject *parent) : FacebookDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::Calendars, parent) , m_calendar(mKCal::ExtendedCalendar::Ptr(new mKCal::ExtendedCalendar(QTimeZone::utc()))) , m_storage(mKCal::ExtendedCalendar::defaultStorage(m_calendar)) , m_storageNeedsSave(false) { setInitialActive(true); } FacebookCalendarSyncAdaptor::~FacebookCalendarSyncAdaptor() { } QString FacebookCalendarSyncAdaptor::syncServiceName() const { return QStringLiteral("facebook-calendars"); } void FacebookCalendarSyncAdaptor::sync(const QString &dataTypeString, int accountId) { m_storageNeedsSave = false; m_parsedEvents.clear(); m_storage->open(); // we close it in finalCleanup() FacebookDataTypeSyncAdaptor::sync(dataTypeString, accountId); } void FacebookCalendarSyncAdaptor::finalCleanup() { if (syncAborted()) { qCInfo(lcSocialPlugin) << "sync aborted, won't commit database changes"; m_storage->close(); return; } // commit changes to db if (m_storageNeedsSave) { // apply changes from sync m_storage->save(); // set the facebook notebook back to read-only Q_FOREACH (mKCal::Notebook::Ptr notebook, m_storage->notebooks()) { if (notebook->pluginName() == QLatin1String(FACEBOOK) && !notebook->isReadOnly()) { notebook->setIsReadOnly(true); m_storage->updateNotebook(notebook); break; } } // done. m_storageNeedsSave = false; } if (!ghostEventCleanupPerformed()) { // Delete any events which are not associated with a notebook. // These events are ghost events, caused by a bug which previously // existed in the purgeDataForOldAccount code. // The mkcal API doesn't allow us to determine which notebook a // given incidence belongs to, so we have to instead load // everything and then find the ones which are ophaned. m_storage->load(); KCalendarCore::Incidence::List allIncidences = m_calendar->incidences(); mKCal::Notebook::List allNotebooks = m_storage->notebooks(); QSet notebookIncidenceUids; foreach (mKCal::Notebook::Ptr notebook, allNotebooks) { KCalendarCore::Incidence::List currNbIncidences; m_storage->allIncidences(&currNbIncidences, notebook->uid()); foreach (KCalendarCore::Incidence::Ptr incidence, currNbIncidences) { notebookIncidenceUids.insert(incidence->uid()); } } foreach (const KCalendarCore::Incidence::Ptr incidence, allIncidences) { if (!notebookIncidenceUids.contains(incidence->uid())) { // orphan/ghost incidence. must be deleted. qCDebug(lcSocialPlugin) << "deleting orphan event with uid:" << incidence->uid(); m_calendar->deleteIncidence(incidence); m_storageNeedsSave = true; } } if (!m_storageNeedsSave || m_storage->save()) { setGhostEventCleanupPerformed(); } } // done. m_storage->close(); } void FacebookCalendarSyncAdaptor::purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode mode) { if (mode == SocialNetworkSyncAdaptor::CleanUpPurge) { // we need to initialise the storage m_storageNeedsSave = false; m_storage->open(); // we close it in finalCleanup() } // We clean all the entries in the calendar foreach (mKCal::Notebook::Ptr notebook, m_storage->notebooks()) { if (notebook->pluginName() == QLatin1String(FACEBOOK) && notebook->account() == QString::number(oldId)) { m_storage->deleteNotebook(notebook); } } if (mode == SocialNetworkSyncAdaptor::CleanUpPurge) { // and commit any changes made. finalCleanup(); } } void FacebookCalendarSyncAdaptor::beginSync(int accountId, const QString &accessToken) { qCDebug(lcSocialPlugin) << "beginning Calendar sync for Facebook account" << accountId; requestEvents(accountId, accessToken); } void FacebookCalendarSyncAdaptor::requestEvents(int accountId, const QString &accessToken, const QString &batchRequest) { QString batch = batchRequest; if (batch.isEmpty()) { // Create batch query of following format: // [{ "method":"GET","relative_url":"me/events?type=created&include_headers=false&limit=200&fields=..."}, // { "method":"GET","relative_url":"me/events?type=attending&include_headers=false&limit=200&fields=..."}, // { "method":"GET","relative_url":"me/events?type=maybe&include_headers=false&limit=200&fields=..."}, // { "method":"GET","relative_url":"me/events?type=not_replied&include_headers=false&limit=200&fields=..."}] int sinceSpan = m_accountSyncProfile ? m_accountSyncProfile->key(Buteo::KEY_SYNC_SINCE_DAYS_PAST, QStringLiteral("30")).toInt() : 30; uint startTime = QDateTime::currentDateTimeUtc().addDays(sinceSpan * -1).toTime_t(); QString since = QStringLiteral("since=") + QString::number(startTime); QString calendarQuery = QStringLiteral("{\"method\":\"GET\",\"relative_url\":\"me/events?type=%1&include_headers=false&limit=200&fields=id,name,start_time,end_time,description,place&") + since + QStringLiteral("\"}"); batch = QStringLiteral("[") + calendarQuery.arg(QStringLiteral("created")) + QStringLiteral(",") + calendarQuery.arg(QStringLiteral("attending")) + QStringLiteral(",") + calendarQuery.arg(QStringLiteral("maybe")) + QStringLiteral(",") + calendarQuery.arg(QStringLiteral("not_replied")) + QStringLiteral("]"); } QUrl url(graphAPI()); QNetworkRequest request(url); QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); multiPart->setBoundary("-------Sska2129ifcalksmqq3"); QHttpPart accessTokenPart; accessTokenPart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"access_token\"")); accessTokenPart.setBody(accessToken.toUtf8()); QHttpPart batchPart; batchPart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"batch\"")); batchPart.setBody(batch.toUtf8()); multiPart->append(accessTokenPart); multiPart->append(batchPart); request.setRawHeader("Content-Type", "multipart/form-data; boundary="+multiPart->boundary()); QNetworkReply *reply = m_networkAccessManager->post(request, multiPart); if (reply) { multiPart->setParent(reply); reply->setProperty("accountId", accountId); reply->setProperty("accessToken", accessToken); connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(errorHandler(QNetworkReply::NetworkError))); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsHandler(QList))); connect(reply, SIGNAL(finished()), this, SLOT(finishedHandler())); if (batchRequest.isEmpty()) { // we're requesting data. Increment the semaphore so that we know we're still busy. incrementSemaphore(accountId); } setupReplyTimeout(accountId, reply); } else { delete multiPart; qCWarning(lcSocialPlugin) << "unable to request events from Facebook account" << accountId; } } void FacebookCalendarSyncAdaptor::finishedHandler() { QNetworkReply *reply = qobject_cast(sender()); int accountId = reply->property("accountId").toInt(); QString accessToken = reply->property("accessToken").toString(); QByteArray replyData = reply->readAll(); bool isError = reply->property("isError").toBool(); disconnect(reply); reply->deleteLater(); removeReplyTimeout(accountId, reply); qCDebug(lcSocialPluginTrace) << "request finished, got response:"; Q_FOREACH (const QString &line, QString::fromUtf8(replyData).split('\n', QString::SkipEmptyParts)) { qCDebug(lcSocialPluginTrace) << line; } QStringList ongoingRequests; QJsonArray array; bool ok = false; QJsonDocument jsonDocument = QJsonDocument::fromJson(replyData); if (!jsonDocument.isEmpty() && jsonDocument.isArray()) { array = jsonDocument.array(); if (array.count() > 0) { ok = true; } } if (!isError && ok) { foreach (QJsonValue value, array) { // Go through each entry in batch reply and process the events it contains if (!value.isObject()) { qCWarning(lcSocialPlugin) << "Facebook calendar batch reply entry is not an object for account " << accountId; continue; } QJsonObject entry = value.toObject(); if (entry.value(QLatin1String("code")).toInt() != 200) { qCWarning(lcSocialPlugin) << "Facebook calendar batch request for account " << accountId << " failed with " << entry.value("code").toInt(); continue; } if (!entry.contains(QLatin1String("body"))) { qCWarning(lcSocialPlugin) << "Facebook calendar batch reply entry doesn't contain body field for account " << accountId; continue; } QJsonDocument bodyDocument = QJsonDocument::fromJson(entry.value(QLatin1String("body")).toString().toUtf8()); if (bodyDocument.isEmpty()) { qCWarning(lcSocialPlugin) << "Facebook calendar batch reply body is empty for account " << accountId; continue; } QJsonObject parsed = bodyDocument.object(); if (!parsed.contains(QLatin1String("data"))) { qCWarning(lcSocialPlugin) << "Facebook calendar batch reply entry doesn't contain data for account " << accountId; continue; } if (parsed.contains(QLatin1String("paging"))) { QJsonObject paging = parsed.value(QLatin1String("paging")).toObject(); if (paging.contains(QLatin1String("next"))) { QString nextQuery = paging.value(QLatin1String("next")).toString(); ongoingRequests.append(nextQuery); } } // Parse the event list QJsonArray dataList = parsed.value(QLatin1String("data")).toArray(); foreach (QJsonValue data, dataList) { QJsonObject dataMap = data.toObject(); QString eventId = dataMap.value(QLatin1String("id")).toVariant().toString(); if (m_parsedEvents.contains(eventId)) { // event was already handled by this batch request continue; } FacebookParsedEvent parsedEvent; parsedEvent.m_id = eventId; QString startTimeString = dataMap.value(QLatin1String("start_time")).toString(); QString endTimeString = dataMap.value(QLatin1String("end_time")).toString(); if (endTimeString.isEmpty()) { // workaround for empty ET events endTimeString = startTimeString; } QDateTime parsedStartTime = QDateTime::fromString(startTimeString, Qt::ISODate); QDateTime parsedEndTime = QDateTime::fromString(endTimeString, Qt::ISODate); parsedEvent.m_startTime = parsedStartTime.toTimeZone(QTimeZone::systemTimeZone()); parsedEvent.m_endTime = parsedEndTime.toTimeZone(QTimeZone::systemTimeZone()); parsedEvent.m_summary = dataMap.value(QLatin1String("name")).toString(); parsedEvent.m_description = dataMap.value(QLatin1String("description")).toString(); parsedEvent.m_location = dataMap.value(QLatin1String("place")).toObject().toVariantMap().value("name").toString(); m_parsedEvents[eventId] = parsedEvent; } } if (ongoingRequests.count() > 0) { // Form next batch request for still ongoing requests QString nextBatch("["); foreach (const QString next, ongoingRequests) { QUrl nextUrl(next); nextBatch.append(QStringLiteral("{\"method\":\"GET\",\"relative_url\":\"me/events?include_headers=false&")); nextBatch.append(nextUrl.query()); nextBatch.append(QStringLiteral("\"},")); } nextBatch.chop(1); // remove last comma nextBatch.append(QStringLiteral("]")); requestEvents(accountId, accessToken, nextBatch); } else { qCDebug(lcSocialPlugin) << "finished all requests, about to perform database update"; processParsedEvents(accountId); decrementSemaphore(accountId); } } else { // Error occurred during request. qCWarning(lcSocialPlugin) << "unable to parse calendar data from request with account" << accountId << ", got:" << QString::fromLatin1(replyData.constData()); decrementSemaphore(accountId); } } void FacebookCalendarSyncAdaptor::processParsedEvents(int accountId) { // Search for the Facebook Notebook qCDebug(lcSocialPlugin) << "Received" << m_parsedEvents.size() << "events from server; determining delta"; mKCal::Notebook::Ptr fbNotebook; Q_FOREACH (mKCal::Notebook::Ptr notebook, m_storage->notebooks()) { if (notebook->pluginName() == QLatin1String(FACEBOOK) && notebook->account() == QString::number(accountId)) { fbNotebook = notebook; } } if (!fbNotebook) { // create the notebook if required fbNotebook = mKCal::Notebook::Ptr(new mKCal::Notebook); fbNotebook->setName(QLatin1String(FACEBOOK)); fbNotebook->setPluginName(QLatin1String(FACEBOOK)); fbNotebook->setAccount(QString::number(accountId)); fbNotebook->setColor(QLatin1String(FACEBOOK_COLOR)); fbNotebook->setDescription(m_accountManager->account(accountId)->displayName()); fbNotebook->setIsReadOnly(true); m_storage->addNotebook(fbNotebook); } else { // update the notebook details if required bool changed = false; if (fbNotebook->description().isEmpty()) { fbNotebook->setDescription(m_accountManager->account(accountId)->displayName()); changed = true; } if (changed) { m_storage->updateNotebook(fbNotebook); } } // We load incidences that are associated to Facebook into memory KCalendarCore::Incidence::List dbEvents; m_storage->loadNotebookIncidences(fbNotebook->uid()); if (!m_storage->allIncidences(&dbEvents, fbNotebook->uid())) { qCWarning(lcSocialPlugin) << "unable to load Facebook events from database"; return; } // Now determine the delta to the events received from server. QSet seenLocalEvents; Q_FOREACH (const QString &fbId, m_parsedEvents.keys()) { // find the local event associated with this event. bool foundLocal = false; const FacebookParsedEvent &parsedEvent = m_parsedEvents[fbId]; Q_FOREACH (KCalendarCore::Incidence::Ptr incidence, dbEvents) { if (incidence->uid().endsWith(QStringLiteral(":%1").arg(fbId))) { KCalendarCore::Event::Ptr event = m_calendar->event(incidence->uid()); if (!event) continue; // not a valid event incidence. // found. If it has been modified remotely, then modify locally. foundLocal = true; seenLocalEvents.insert(incidence->uid()); if (event->summary() != parsedEvent.m_summary || event->description() != parsedEvent.m_description || event->location() != parsedEvent.m_location || event->dtStart() != parsedEvent.m_startTime || (parsedEvent.m_endExists && event->dtEnd() != parsedEvent.m_endTime)) { // the event has been changed remotely. event->startUpdates(); event->setSummary(parsedEvent.m_summary); event->setDescription(parsedEvent.m_description); event->setLocation(parsedEvent.m_location); event->setDtStart(parsedEvent.m_startTime); if (parsedEvent.m_endExists) { event->setDtEnd(parsedEvent.m_endTime); } if (parsedEvent.m_isDateOnly) { event->setAllDay(true); } event->setReadOnly(true); event->endUpdates(); m_storageNeedsSave = true; qCDebug(lcSocialPlugin) << "Facebook event" << event->uid() << "was modified on server"; } else { qCDebug(lcSocialPlugin) << "Facebook event" << event->uid() << "is unchanged on server"; } } } // if not found locally, it must be a new addition. if (!foundLocal) { KCalendarCore::Event::Ptr event = KCalendarCore::Event::Ptr(new KCalendarCore::Event); QString eventUid = QUuid::createUuid().toString(); eventUid = eventUid.mid(1); // remove leading { eventUid.chop(1); // remove trailing } eventUid = QStringLiteral("%1:%2").arg(eventUid).arg(fbId); event->setUid(eventUid); event->setSummary(parsedEvent.m_summary); event->setDescription(parsedEvent.m_description); event->setLocation(parsedEvent.m_location); event->setDtStart(parsedEvent.m_startTime); if (parsedEvent.m_endExists) { event->setDtEnd(parsedEvent.m_endTime); } if (parsedEvent.m_isDateOnly) { event->setAllDay(true); } event->setReadOnly(true); m_calendar->addEvent(event, fbNotebook->uid()); m_storageNeedsSave = true; qCDebug(lcSocialPlugin) << "Facebook event" << event->uid() << "was added on server"; } } // Any local events which were not seen, must have been removed remotely. Q_FOREACH (KCalendarCore::Incidence::Ptr incidence, dbEvents) { if (!seenLocalEvents.contains(incidence->uid())) { // note: have to delete from calendar after loaded from calendar. m_calendar->deleteIncidence(m_calendar->incidence(incidence->uid())); m_storageNeedsSave = true; qCDebug(lcSocialPlugin) << "Facebook event" << incidence->uid() << "was deleted on server"; } } } buteo-sync-plugins-social-0.4.28/src/facebook/facebook-calendars/facebookcalendarsyncadaptor.h000066400000000000000000000047411474572147200325760ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2013-2014 Jolla Ltd. ** Contact: Lucien Xu ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef FACEBOOKCALENDARSYNCADAPTOR_H #define FACEBOOKCALENDARSYNCADAPTOR_H #include "facebookdatatypesyncadaptor.h" #include #include class FacebookParsedEvent { public: FacebookParsedEvent(); FacebookParsedEvent(const FacebookParsedEvent &e); FacebookParsedEvent &operator=(const FacebookParsedEvent &) = default; public: QString m_id; bool m_isDateOnly; bool m_endExists; QDateTime m_startTime; QDateTime m_endTime; QString m_summary; QString m_description; QString m_location; }; class FacebookCalendarSyncAdaptor : public FacebookDataTypeSyncAdaptor { Q_OBJECT public: FacebookCalendarSyncAdaptor(QObject *parent); ~FacebookCalendarSyncAdaptor(); QString syncServiceName() const; void sync(const QString &dataTypeString, int accountId); protected: // implementing FacebookDataTypeSyncAdaptor interface void purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode mode); void beginSync(int accountId, const QString &accessToken); void finalCleanup(); private: void requestEvents(int accountId, const QString &accessToken, const QString &batchRequest = QString()); void processParsedEvents(int accountId); private Q_SLOTS: void finishedHandler(); private: mKCal::ExtendedCalendar::Ptr m_calendar; mKCal::ExtendedStorage::Ptr m_storage; bool m_storageNeedsSave; QMap m_parsedEvents; }; #endif // FACEBOOKCALENDARSYNCADAPTOR_H buteo-sync-plugins-social-0.4.28/src/facebook/facebook-common.pri000066400000000000000000000001651474572147200247560ustar00rootroot00000000000000INCLUDEPATH += $$PWD SOURCES += $$PWD/facebookdatatypesyncadaptor.cpp HEADERS += $$PWD/facebookdatatypesyncadaptor.h buteo-sync-plugins-social-0.4.28/src/facebook/facebook-images/000077500000000000000000000000001474572147200242155ustar00rootroot00000000000000buteo-sync-plugins-social-0.4.28/src/facebook/facebook-images/facebook-images.pri000066400000000000000000000001601474572147200277420ustar00rootroot00000000000000SOURCES += $$PWD/facebookimagesyncadaptor.cpp HEADERS += $$PWD/facebookimagesyncadaptor.h INCLUDEPATH += $$PWD buteo-sync-plugins-social-0.4.28/src/facebook/facebook-images/facebook-images.pro000066400000000000000000000013201474572147200277470ustar00rootroot00000000000000TARGET = facebook-images-client include($$PWD/../../common.pri) include($$PWD/../facebook-common.pri) include($$PWD/facebook-images.pri) CONFIG += link_pkgconfig PKGCONFIG += mlite5 facebook_images_sync_profile.path = /etc/buteo/profiles/sync facebook_images_sync_profile.files = $$PWD/facebook.Images.xml facebook_images_client_plugin_xml.path = /etc/buteo/profiles/client facebook_images_client_plugin_xml.files = $$PWD/facebook-images.xml HEADERS += facebookimagesplugin.h SOURCES += facebookimagesplugin.cpp OTHER_FILES += \ facebook_images_sync_profile.files \ facebook_images_client_plugin_xml.files INSTALLS += \ target \ facebook_images_sync_profile \ facebook_images_client_plugin_xml buteo-sync-plugins-social-0.4.28/src/facebook/facebook-images/facebook-images.xml000066400000000000000000000002061474572147200277510ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/facebook/facebook-images/facebook.Images.xml000066400000000000000000000011411474572147200277110ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/facebook/facebook-images/facebookimagesplugin.cpp000066400000000000000000000035721474572147200311060ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2014 - 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "facebookimagesplugin.h" #include "facebookimagesyncadaptor.h" #include "socialnetworksyncadaptor.h" FacebookImagesPlugin::FacebookImagesPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *callbackInterface) : SocialdButeoPlugin(pluginName, profile, callbackInterface, QStringLiteral("facebook"), SocialNetworkSyncAdaptor::dataTypeName(SocialNetworkSyncAdaptor::Images)) { } FacebookImagesPlugin::~FacebookImagesPlugin() { } SocialNetworkSyncAdaptor *FacebookImagesPlugin::createSocialNetworkSyncAdaptor() { return new FacebookImageSyncAdaptor(this); } Buteo::ClientPlugin* FacebookImagesPluginLoader::createClientPlugin( const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) { return new FacebookImagesPlugin(pluginName, profile, cbInterface); } buteo-sync-plugins-social-0.4.28/src/facebook/facebook-images/facebookimagesplugin.h000066400000000000000000000036011474572147200305440ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2014 - 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef FACEBOOKIMAGESPLUGIN_H #define FACEBOOKIMAGESPLUGIN_H #include "socialdbuteoplugin.h" #include class Q_DECL_EXPORT FacebookImagesPlugin : public SocialdButeoPlugin { Q_OBJECT public: FacebookImagesPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *cbInterface); ~FacebookImagesPlugin(); protected: SocialNetworkSyncAdaptor *createSocialNetworkSyncAdaptor(); }; class FacebookImagesPluginLoader : public Buteo::SyncPluginLoader { Q_OBJECT Q_PLUGIN_METADATA(IID "org.sailfishos.plugins.sync.FacebookImagesPluginLoader") Q_INTERFACES(Buteo::SyncPluginLoader) public: Buteo::ClientPlugin* createClientPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) override; }; #endif // FACEBOOKIMAGESPLUGIN_H buteo-sync-plugins-social-0.4.28/src/facebook/facebook-images/facebookimagesyncadaptor.cpp000066400000000000000000000562371474572147200317620ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2013-2014 Jolla Ltd. ** Contact: Chris Adams ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "facebookimagesyncadaptor.h" #include "trace.h" #include #include #include #include #include #include #include #include #include #include #include #include #include // Update the following version if database schema changes e.g. new // fields are added to the existing tables. // It will make old tables dropped and creates new ones. // Currently, we integrate with the device image gallery via saving thumbnails to the // ~/.config/sociald/images directory, and filling the ~/.config/sociald/images/facebook.db // with appropriate data. // TODO: there is still issues with multiaccount, if an user adds two times the same // account, it might have some problems, like data being removed while it shouldn't. FacebookImageSyncAdaptor::FacebookImageSyncAdaptor(QObject *parent) : FacebookDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::Images, parent) , m_optimalThumbnailWidth(0) , m_optimalImageWidth(0) { setInitialActive(m_db.isValid()); } FacebookImageSyncAdaptor::~FacebookImageSyncAdaptor() { } QString FacebookImageSyncAdaptor::syncServiceName() const { return QStringLiteral("facebook-images"); } void FacebookImageSyncAdaptor::sync(const QString &dataTypeString, int accountId) { // get ready for sync if (!determineOptimalDimensions()) { qCWarning(lcSocialPlugin) << "unable to determine optimal image dimensions, aborting"; setStatus(SocialNetworkSyncAdaptor::Error); return; } if (!initRemovalDetectionLists(accountId)) { qCWarning(lcSocialPlugin) << "unable to initialized cached account list for account" << accountId; setStatus(SocialNetworkSyncAdaptor::Error); return; } // call superclass impl. FacebookDataTypeSyncAdaptor::sync(dataTypeString, accountId); } void FacebookImageSyncAdaptor::purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode) { m_db.purgeAccount(oldId); m_db.commit(); m_db.wait(); } void FacebookImageSyncAdaptor::beginSync(int accountId, const QString &accessToken) { // XXX TODO: use a sync queue. One for accounts + one for albums. // Finish all images from a single album, etc on down. // That way we don't request anything "out of order" which can screw up Facebook's paging etc stuff. // Downside: much slower, since more (network) IO bound than previously. requestData(accountId, accessToken, QString(), QString(), QString()); } void FacebookImageSyncAdaptor::finalize(int accountId) { Q_UNUSED(accountId) if (syncAborted()) { qCInfo(lcSocialPlugin) << "sync aborted, won't commit database changes"; } else { // Remove albums m_db.removeAlbums(m_cachedAlbums.keys()); // Remove images m_db.removeImages(m_removedImages); m_db.commit(); m_db.wait(); } } void FacebookImageSyncAdaptor::requestData(int accountId, const QString &accessToken, const QString &continuationUrl, const QString &fbUserId, const QString &fbAlbumId) { if (syncAborted()) { qCDebug(lcSocialPlugin) << "skipping data request due to sync abort"; clearRemovalDetectionLists(); // don't perform server-side removal detection during this sync run. return; } QUrl url; if (!continuationUrl.isEmpty()) { // fetch the next page. url = QUrl(continuationUrl); } else { // build the request, depending on whether we're fetching albums or images. if (fbAlbumId.isEmpty()) { // fetching all albums from the me user. url = QUrl(graphAPI(QLatin1String("/me/albums"))); } else { // fetching images from a particular album. url = QUrl(graphAPI(QString(QLatin1String("/%1/photos")).arg(fbAlbumId))); } } // if the url already contains query part (in which case it is continuationUrl), don't overwrite it. if (!url.hasQuery()) { QList > queryItems; QUrlQuery query(url); queryItems.append(QPair(QString(QLatin1String("access_token")), accessToken)); queryItems.append(QPair(QString(QLatin1String("limit")), QString(QLatin1String("2000")))); if (fbAlbumId.isEmpty()) { queryItems.append(QPair(QString(QLatin1String("fields")), QString(QLatin1String("id,from,name,created_time,updated_time,count")))); } else { queryItems.append(QPair(QString(QLatin1String("fields")), QString(QLatin1String("id,picture,source,images,width,height,created_time,updated_time,name")))); } query.setQueryItems(queryItems); url.setQuery(query); } QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(url)); if (reply) { reply->setProperty("accountId", accountId); reply->setProperty("accessToken", accessToken); reply->setProperty("fbUserId", fbUserId); reply->setProperty("fbAlbumId", fbAlbumId); reply->setProperty("continuationUrl", continuationUrl); connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(errorHandler(QNetworkReply::NetworkError))); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsHandler(QList))); if (fbAlbumId.isEmpty()) { connect(reply, SIGNAL(finished()), this, SLOT(albumsFinishedHandler())); } else { connect(reply, SIGNAL(finished()), this, SLOT(imagesFinishedHandler())); } // we're requesting data. Increment the semaphore so that we know we're still busy. incrementSemaphore(accountId); setupReplyTimeout(accountId, reply); } else { qCWarning(lcSocialPlugin) << "unable to request data from Facebook account with id" << accountId; clearRemovalDetectionLists(); // don't perform server-side removal detection during this sync run. } } void FacebookImageSyncAdaptor::albumsFinishedHandler() { QNetworkReply *reply = qobject_cast(sender()); bool isError = reply->property("isError").toBool(); int accountId = reply->property("accountId").toInt(); QString accessToken = reply->property("accessToken").toString(); QString fbUserId = reply->property("fbUserId").toString(); QString fbAlbumId = reply->property("fbAlbumId").toString(); QString continuationUrl = reply->property("continuationUrl").toString(); QByteArray replyData = reply->readAll(); disconnect(reply); reply->deleteLater(); removeReplyTimeout(accountId, reply); bool ok = false; QJsonObject parsed = parseJsonObjectReplyData(replyData, &ok); if (isError || !ok || !parsed.contains(QLatin1String("data"))) { qCWarning(lcSocialPlugin) << "unable to read albums response for Facebook account with id" << accountId; clearRemovalDetectionLists(); // don't perform server-side removal detection during this sync run. decrementSemaphore(accountId); return; } QJsonArray data = parsed.value(QLatin1String("data")).toArray(); if (data.size() == 0) { qCDebug(lcSocialPlugin) << "Facebook account with id" << accountId << "has no albums"; decrementSemaphore(accountId); return; } // read the albums information for (int i = 0; i < data.size(); ++i) { QJsonObject albumObject = data.at(i).toObject(); if (albumObject.isEmpty()) { continue; } QString albumId = albumObject.value(QLatin1String("id")).toString(); QString userId = albumObject.value(QLatin1String("from")).toObject().value(QLatin1String("id")).toString(); if (!userId.isEmpty() && userId != fbUserId) { // probably because the fbUserId hasn't been filled yet. fbUserId = userId; m_db.syncAccount(accountId, fbUserId); } if (!albumId.isEmpty() && albumId != fbAlbumId) { // probably because the fbAlbumId hasn't been filled yet. fbAlbumId = albumId; } QString albumName = albumObject.value(QLatin1String("name")).toString(); QString createdTimeStr = albumObject.value(QLatin1String("created_time")).toString(); QString updatedTimeStr = albumObject.value(QLatin1String("updated_time")).toString(); int imageCount = static_cast(albumObject.value(QLatin1String("count")).toDouble()); qCDebug(lcSocialPlugin) << "Got album information:" << userId << albumId << albumName << createdTimeStr << updatedTimeStr << imageCount; // check to see whether we need to sync (any changes since last sync) // Note that we also check if the image count is the same, since, when // removing an image, the updatedTime is not changed QDateTime createdTime = QDateTime::fromString(createdTimeStr, Qt::ISODate); QDateTime updatedTime = QDateTime::fromString(updatedTimeStr, Qt::ISODate); const FacebookAlbum::ConstPtr &dbAlbum = m_cachedAlbums.value(fbAlbumId); m_cachedAlbums.remove(fbAlbumId); // Removal detection if (!dbAlbum.isNull() && (dbAlbum->updatedTime() >= updatedTime && dbAlbum->imageCount() == imageCount)) { qCDebug(lcSocialPlugin) << "album with id" << albumId << "by user" << userId << "from Facebook account with id" << accountId << "doesn't need sync"; continue; } // We need to sync. We save the album entry, and request the images for the album. // When saving the album, we might need to add a new user possiblyAddNewUser(userId, accountId, accessToken); // We then save the album m_db.addAlbum(albumId, userId, createdTime, updatedTime, albumName, imageCount); // TODO: After successfully added an album, we should begin a new query to get the image // information (based on cover image id). requestData(accountId, accessToken, QString(), fbUserId, fbAlbumId); } // Perform a continuation request if required. QJsonObject paging = parsed.value(QLatin1String("paging")).toObject(); QString nextUrl = paging.value(QLatin1String("next")).toString(); if (!nextUrl.isEmpty() && nextUrl != continuationUrl) { // note: we check equality because fb can return spurious paging data... qCDebug(lcSocialPlugin) << "performing continuation request for more albums for Facebook account with id" << accountId << ":" << nextUrl; requestData(accountId, accessToken, nextUrl, fbUserId, QString()); } // Finally, reduce our semaphore. decrementSemaphore(accountId); } void FacebookImageSyncAdaptor::imagesFinishedHandler() { QNetworkReply *reply = qobject_cast(sender()); bool isError = reply->property("isError").toBool(); int accountId = reply->property("accountId").toInt(); QString accessToken = reply->property("accessToken").toString(); QString fbUserId = reply->property("fbUserId").toString(); QString fbAlbumId = reply->property("fbAlbumId").toString(); QString continuationUrl = reply->property("continuationUrl").toString(); QByteArray replyData = reply->readAll(); disconnect(reply); reply->deleteLater(); removeReplyTimeout(accountId, reply); bool ok = false; QJsonObject parsed = parseJsonObjectReplyData(replyData, &ok); if (isError || !ok || !parsed.contains(QLatin1String("data"))) { qCWarning(lcSocialPlugin) << "unable to read photos response for Facebook account with id" << accountId; qCDebug(lcSocialPlugin) << replyData; clearRemovalDetectionLists(); // don't perform server-side removal detection during this sync run. decrementSemaphore(accountId); return; } QJsonArray data = parsed.value(QLatin1String("data")).toArray(); if (data.size() == 0) { qCDebug(lcSocialPlugin) << "album with id" << fbAlbumId << "from Facebook account with id" << accountId << "has no photos"; checkRemovedImages(fbAlbumId); decrementSemaphore(accountId); return; } // read the photos information foreach (const QJsonValue &photoValue, data) { QJsonObject imageObject = photoValue.toObject(); if (imageObject.isEmpty()) { continue; } QString photoId = imageObject.value(QLatin1String("id")).toString(); QString thumbnailUrl = imageObject.value(QLatin1String("picture")).toString(); QString imageSrcUrl = imageObject.value(QLatin1String("source")).toString(); QString createdTimeStr = imageObject.value(QLatin1String("created_time")).toString(); QString updatedTimeStr = imageObject.value(QLatin1String("updated_time")).toString(); QString photoName = imageObject.value(QLatin1String("name")).toString(); int imageWidth = 0; int imageHeight = 0; if (photoId.isEmpty()) { qCWarning(lcSocialPlugin) << "Unable to parse photo id from data:" << photoValue.toObject().toVariantMap(); continue; } // Find optimal thumbnail and image source urls based on dimensions. QList imageSources; QJsonArray images = imageObject.value(QLatin1String("images")).toArray(); foreach (const QJsonValue &imageValue, images) { QJsonObject image = imageValue.toObject(); imageSources << ImageSource(static_cast(image.value(QLatin1String("width")).toDouble()), static_cast(image.value(QLatin1String("height")).toDouble()), image.value(QLatin1String("source")).toString()); } bool foundOptimalThumbnail = false, foundOptimalImage = false; std::sort(imageSources.begin(), imageSources.end()); Q_FOREACH (const ImageSource &img, imageSources) { if (!foundOptimalThumbnail && qMin(img.width, img.height) >= m_optimalThumbnailWidth) { foundOptimalThumbnail = true; thumbnailUrl = img.sourceUrl; } if (!foundOptimalImage && qMin(img.width, img.height) >= m_optimalImageWidth) { foundOptimalImage = true; imageWidth = img.width; imageHeight = img.height; imageSrcUrl = img.sourceUrl; } } if (!foundOptimalThumbnail && imageSources.size()) { // just choose the largest one. thumbnailUrl = imageSources.last().sourceUrl; } if (!foundOptimalImage && imageSources.size()) { // just choose the largest one. imageSrcUrl = imageSources.last().sourceUrl; imageWidth = imageSources.last().width; imageHeight = imageSources.last().height; } QDateTime createdTime = QDateTime::fromString(createdTimeStr, Qt::ISODate); QDateTime updatedTime = QDateTime::fromString(updatedTimeStr, Qt::ISODate); if (!m_serverImageIds[fbAlbumId].contains(photoId)) { m_serverImageIds[fbAlbumId].insert(photoId); } // check if we need to sync, and write to the database. if (!imageSrcUrl.isEmpty()) { if (haveAlreadyCachedImage(photoId, imageSrcUrl)) { qCDebug(lcSocialPlugin) << "have previously cached photo" << photoId << ":" << imageSrcUrl; } else { qCDebug(lcSocialPlugin) << "caching new photo" << photoId << ":" << imageSrcUrl << "->" << imageWidth << "x" << imageHeight; m_db.addImage(photoId, fbAlbumId, fbUserId, createdTime, updatedTime, photoName, imageWidth, imageHeight, thumbnailUrl, imageSrcUrl); } } else { qCWarning(lcSocialPlugin) << "Cannot add photo to database:" << photoId << "- empty image source url!"; } } // perform a continuation request if required. QJsonObject paging = parsed.value(QLatin1String("paging")).toObject(); QString nextUrl = paging.value(QLatin1String("next")).toString(); if (!nextUrl.isEmpty() && nextUrl != continuationUrl) { qCDebug(lcSocialPlugin) << "performing continuation request for more photos for Facebook account with id" << accountId << ":" << nextUrl; requestData(accountId, accessToken, nextUrl, fbUserId, fbAlbumId); } else { // this was the laste page, check removed images checkRemovedImages(fbAlbumId); } // we're finished this request. Decrement our busy semaphore. decrementSemaphore(accountId); } bool FacebookImageSyncAdaptor::haveAlreadyCachedImage(const QString &fbImageId, const QString &imageUrl) { FacebookImage::ConstPtr dbImage = m_db.image(fbImageId); bool imagedbSynced = !dbImage.isNull(); if (!imagedbSynced) { return false; } QString dbImageUrl = dbImage->imageUrl(); if (dbImageUrl != imageUrl) { qCWarning(lcSocialPlugin) << "Image/facebook.db has outdated data!\n" " fbPhotoId:" << fbImageId << "\n" " cached image url:" << dbImageUrl << "\n" " new image url:" << imageUrl; return false; } return true; } void FacebookImageSyncAdaptor::possiblyAddNewUser(const QString &fbUserId, int accountId, const QString &accessToken) { if (!m_db.user(fbUserId).isNull()) { return; } // We need to add the user. We call Facebook to get the informations that we // need and then add it to the database // me?fields=updated_time,name,picture QUrl url(graphAPI(QLatin1String("/me"))); QList > queryItems; queryItems.append(QPair(QString(QLatin1String("access_token")), accessToken)); queryItems.append(QPair(QString(QLatin1String("fields")), QLatin1String("id,updated_time,name,picture"))); QUrlQuery query(url); query.setQueryItems(queryItems); url.setQuery(query); QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(url)); if (reply) { reply->setProperty("accountId", accountId); reply->setProperty("accessToken", accessToken); connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(errorHandler(QNetworkReply::NetworkError))); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsHandler(QList))); connect(reply, SIGNAL(finished()), this, SLOT(userFinishedHandler())); incrementSemaphore(accountId); setupReplyTimeout(accountId, reply); } } void FacebookImageSyncAdaptor::userFinishedHandler() { QNetworkReply *reply = qobject_cast(sender()); QByteArray replyData = reply->readAll(); int accountId = reply->property("accountId").toInt(); disconnect(reply); reply->deleteLater(); bool ok = false; QJsonObject parsed = parseJsonObjectReplyData(replyData, &ok); if (!ok || !parsed.contains(QLatin1String("id"))) { qCWarning(lcSocialPlugin) << "unable to read user response for Facebook account with id" << accountId; return; } QString fbUserId = parsed.value(QLatin1String("id")).toString(); QString fbName = parsed.value(QLatin1String("name")).toString(); QString updatedStr = parsed.value(QLatin1String("updated_time")).toString(); m_db.addUser(fbUserId, QDateTime::fromString(updatedStr, Qt::ISODate), fbName); decrementSemaphore(accountId); } bool FacebookImageSyncAdaptor::initRemovalDetectionLists(int accountId) { // This function should be called as part of the ::sync() preamble. // Clear our internal state variables which we use to track server-side deletions. // We have to do it this way, as results can be spread across multiple requests // if Facebook returns results in paginated form. clearRemovalDetectionLists(); bool ok = false; QMap accounts = m_db.accounts(&ok); if (!ok) { return false; } if (accounts.contains(accountId)) { QString userId = accounts.value(accountId); QStringList allAlbumIds = m_db.allAlbumIds(); foreach (const QString& albumId, allAlbumIds) { FacebookAlbum::ConstPtr album = m_db.album(albumId); if (album->fbUserId() == userId) { m_cachedAlbums.insert(albumId, album); } } } return true; } void FacebookImageSyncAdaptor::clearRemovalDetectionLists() { m_cachedAlbums.clear(); m_serverImageIds.clear(); m_removedImages.clear(); } void FacebookImageSyncAdaptor::checkRemovedImages(const QString &fbAlbumId) { const QSet &serverImageIds = m_serverImageIds.value(fbAlbumId); QSet cachedImageIds = m_db.imageIds(fbAlbumId).toSet(); foreach (const QString &fbImageId, serverImageIds) { cachedImageIds.remove(fbImageId); } m_removedImages.append(cachedImageIds.toList()); } bool FacebookImageSyncAdaptor::determineOptimalDimensions() { int width = 0, height = 0; const int defaultValue = 0; MDConfItem widthConf("/lipstick/screen/primary/width"); if (widthConf.value(defaultValue).toInt() != defaultValue) { width = widthConf.value(defaultValue).toInt(); } MDConfItem heightConf("/lipstick/screen/primary/height"); if (heightConf.value(defaultValue).toInt() != defaultValue) { height = heightConf.value(defaultValue).toInt(); } // we want to use the largest of these dimensions as the "optimal" int maxDimension = qMax(width, height); if (maxDimension % 3 == 0) { m_optimalThumbnailWidth = maxDimension / 3; } else { m_optimalThumbnailWidth = (maxDimension / 2); } m_optimalImageWidth = maxDimension; qCDebug(lcSocialPlugin) << "Determined optimal image dimension:" << m_optimalImageWidth << ", thumbnail:" << m_optimalThumbnailWidth; return true; } buteo-sync-plugins-social-0.4.28/src/facebook/facebook-images/facebookimagesyncadaptor.h000066400000000000000000000063551474572147200314230ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2013-2014 Jolla Ltd. ** Contact: Chris Adams ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef FACEBOOKIMAGESYNCADAPTOR_H #define FACEBOOKIMAGESYNCADAPTOR_H #include "facebookdatatypesyncadaptor.h" #include #include #include #include #include #include #include #include #include #include class FacebookImageSyncAdaptor : public FacebookDataTypeSyncAdaptor { Q_OBJECT public: FacebookImageSyncAdaptor(QObject *parent); ~FacebookImageSyncAdaptor(); QString syncServiceName() const; void sync(const QString &dataTypeString, int accountId); protected: // implementing FacebookDataTypeSyncAdaptor interface void purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode mode); void beginSync(int accountId, const QString &accessToken); void finalize(int accountId); private: void requestData(int accountId, const QString &accessToken, const QString &continuationUrl, const QString &fbUserId, const QString &fbAlbumId); bool haveAlreadyCachedImage(const QString &fbImageId, const QString &imageUrl); void possiblyAddNewUser(const QString &fbUserId, int accountId, const QString &accessToken); private Q_SLOTS: void albumsFinishedHandler(); void imagesFinishedHandler(); void userFinishedHandler(); private: // for server-side removal detection. bool initRemovalDetectionLists(int accountId); void clearRemovalDetectionLists(); void checkRemovedImages(const QString &fbAlbumId); QMap m_cachedAlbums; QMap > m_serverImageIds; QStringList m_removedImages; FacebookImagesDatabase m_db; // image variants with different dimentions class ImageSource { public: ImageSource(int width, int height, const QString &sourceUrl) : width(width), height(height), sourceUrl(sourceUrl) {} bool operator<(const ImageSource &other) const { return this->width < other.width; } int width; int height; QString sourceUrl; }; bool determineOptimalDimensions(); int m_optimalThumbnailWidth; int m_optimalImageWidth; }; #endif // FACEBOOKIMAGESYNCADAPTOR_H buteo-sync-plugins-social-0.4.28/src/facebook/facebook-signon/000077500000000000000000000000001474572147200242455ustar00rootroot00000000000000buteo-sync-plugins-social-0.4.28/src/facebook/facebook-signon/facebook-signon.pri000066400000000000000000000001621474572147200300240ustar00rootroot00000000000000SOURCES += $$PWD/facebooksignonsyncadaptor.cpp HEADERS += $$PWD/facebooksignonsyncadaptor.h INCLUDEPATH += $$PWD buteo-sync-plugins-social-0.4.28/src/facebook/facebook-signon/facebook-signon.pro000066400000000000000000000012421474572147200300320ustar00rootroot00000000000000TARGET = facebook-signon-client include($$PWD/../../common.pri) include($$PWD/../facebook-common.pri) include($$PWD/facebook-signon.pri) facebook_signon_sync_profile.path = /etc/buteo/profiles/sync facebook_signon_sync_profile.files = $$PWD/facebook.Signon.xml facebook_signon_client_plugin_xml.path = /etc/buteo/profiles/client facebook_signon_client_plugin_xml.files = $$PWD/facebook-signon.xml HEADERS += facebooksignonplugin.h SOURCES += facebooksignonplugin.cpp OTHER_FILES += \ facebook_signon_sync_profile.files \ facebook_signon_client_plugin_xml.files INSTALLS += \ target \ facebook_signon_sync_profile \ facebook_signon_client_plugin_xml buteo-sync-plugins-social-0.4.28/src/facebook/facebook-signon/facebook-signon.xml000066400000000000000000000002061474572147200300310ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/facebook/facebook-signon/facebook.Signon.xml000066400000000000000000000011221474572147200277700ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/facebook/facebook-signon/facebooksignonplugin.cpp000066400000000000000000000035741474572147200311700ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2014 - 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "facebooksignonplugin.h" #include "facebooksignonsyncadaptor.h" #include "socialnetworksyncadaptor.h" FacebookSignonPlugin::FacebookSignonPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *callbackInterface) : SocialdButeoPlugin(pluginName, profile, callbackInterface, QStringLiteral("facebook"), SocialNetworkSyncAdaptor::dataTypeName(SocialNetworkSyncAdaptor::Signon)) { } FacebookSignonPlugin::~FacebookSignonPlugin() { } SocialNetworkSyncAdaptor *FacebookSignonPlugin::createSocialNetworkSyncAdaptor() { return new FacebookSignonSyncAdaptor(this); } Buteo::ClientPlugin* FacebookSignonPluginLoader::createClientPlugin( const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) { return new FacebookSignonPlugin(pluginName, profile, cbInterface); } buteo-sync-plugins-social-0.4.28/src/facebook/facebook-signon/facebooksignonplugin.h000066400000000000000000000036011474572147200306240ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2014 - 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef FACEBOOKSIGNONPLUGIN_H #define FACEBOOKSIGNONPLUGIN_H #include "socialdbuteoplugin.h" #include class Q_DECL_EXPORT FacebookSignonPlugin : public SocialdButeoPlugin { Q_OBJECT public: FacebookSignonPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *cbInterface); ~FacebookSignonPlugin(); protected: SocialNetworkSyncAdaptor *createSocialNetworkSyncAdaptor(); }; class FacebookSignonPluginLoader : public Buteo::SyncPluginLoader { Q_OBJECT Q_PLUGIN_METADATA(IID "org.sailfishos.plugins.sync.FacebookSignonPluginLoader") Q_INTERFACES(Buteo::SyncPluginLoader) public: Buteo::ClientPlugin* createClientPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) override; }; #endif // FACEBOOKSIGNONPLUGIN_H buteo-sync-plugins-social-0.4.28/src/facebook/facebook-signon/facebooksignonsyncadaptor.cpp000066400000000000000000000332221474572147200322120ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2013-2014 Jolla Ltd. ** Contact: Chris Adams ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "facebooksignonsyncadaptor.h" #include "trace.h" #include #include #include #include #include #include #include FacebookSignonSyncAdaptor::FacebookSignonSyncAdaptor(QObject *parent) : FacebookDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::Signon, parent) { setInitialActive(true); } FacebookSignonSyncAdaptor::~FacebookSignonSyncAdaptor() { } QString FacebookSignonSyncAdaptor::syncServiceName() const { return QStringLiteral("facebook-sync"); // TODO: change name of service to facebook-signon! } void FacebookSignonSyncAdaptor::sync(const QString &dataTypeString, int accountId) { // call superclass impl. FacebookDataTypeSyncAdaptor::sync(dataTypeString, accountId); } void FacebookSignonSyncAdaptor::purgeDataForOldAccount(int, SocialNetworkSyncAdaptor::PurgeMode) { // Nothing to do. } void FacebookSignonSyncAdaptor::finalize(int) { // nothing to do } void FacebookSignonSyncAdaptor::beginSync(int accountId, const QString &accessToken) { // We can't do a verify token request (debug_token) as that requires an app_access_token // which Facebook states should only be retrieved from a server-to-server request as // it involves the clientSecret. // Similarly, we can't exchange a short-lived token for a long-lived token, as that // flow also requires a clientSecret and thus needs a server-to-server request. // In short, we have to use a "normal" request using the access token and hope that // that is sufficient to refresh the existing short-lived token. // If it is not (for example, if the user turns their phone off over night) then we // need to raise the CredentialsNeedUpdate flag and have the user log in again. // Perform a "get the current user" request with the specified authorization token. QList > queryItems; queryItems.append(QPair(QString(QLatin1String("access_token")), accessToken)); queryItems.append(QPair(QString(QLatin1String("fields")), QLatin1String("id"))); QUrl url(graphAPI(QLatin1String("/me"))); QUrlQuery query(url); query.setQueryItems(queryItems); url.setQuery(query); QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(url)); if (reply) { reply->setProperty("accountId", accountId); reply->setProperty("accessToken", accessToken); connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(errorHandler(QNetworkReply::NetworkError))); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsHandler(QList))); connect(reply, SIGNAL(finished()), this, SLOT(requestFinishedHandler())); // we're requesting data. Increment the semaphore so that we know we're still busy. setupReplyTimeout(accountId, reply); incrementSemaphore(accountId); } else { qCWarning(lcSocialPlugin) << "unable to verify access token via network request for Facebook account:" << accountId; } } void FacebookSignonSyncAdaptor::requestFinishedHandler() { QNetworkReply *reply = qobject_cast(sender()); bool isError = reply->property("isError").toBool(); int accountId = reply->property("accountId").toInt(); QString accessToken = reply->property("accessToken").toString(); QByteArray replyData = reply->readAll(); reply->disconnect(this); reply->deleteLater(); removeReplyTimeout(accountId, reply); if (syncAborted()) { qCInfo(lcSocialPlugin) << "sync aborted, skipping signon sync reply handling"; decrementSemaphore(accountId); return; } bool ok = false; QJsonObject parsed = parseJsonObjectReplyData(replyData, &ok); if (!isError && ok && parsed.contains(QStringLiteral("id"))) { // Request was successful. the token expiry will be refreshed server-side. // We need to manually extend the device-side expiry time. // We don't know how long it'll be extended server-side for, // and they don't tell us, so let's assume it's 7 days. forceTokenExpiry(7 * 86400, accountId, accessToken); // 7 days } else if (isError && ok) { if (parsed.contains("error")) { QJsonObject errorObj = parsed.value(QStringLiteral("error")).toObject(); QString errorType = errorObj.value(QStringLiteral("type")).toString(); double errorCode = errorObj.value(QStringLiteral("code")).toDouble(); QString errorMessage = errorObj.value(QStringLiteral("message")).toString(); if (errorType == QStringLiteral("OAuthException") || errorCode == 190 || errorCode == 102 || errorCode == 10 || (errorCode >= 200 && errorCode <= 299)) { // the account is in a state which requires user intervention qCWarning(lcSocialPlugin) << "access token has expired for Facebook account" << accountId << ":" << errorCode << "," << errorType << "," << errorMessage; forceTokenExpiry(0, accountId, accessToken); } else { // other error (downtime / service disruption / etc) // ignore this one. } } else { // unknown response from server. Probably a networking error or similar. // ignore this one. } } else if (reply->error() == QNetworkReply::UnknownContentError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 400) { // for some strange reason Facebook will return this error // if the token has been manually revoked. There is no // response data in this case. // the account is in a state which requires user intervention qCWarning(lcSocialPlugin) << "access token has presumably been revoked for Facebook account" << accountId; forceTokenExpiry(0, accountId, accessToken); } else { // could have been a network error, or something. // we treat it as a sync error, but not a signon error. qCWarning(lcSocialPlugin) << "unable to parse response information for verification request for Facebook account:" << accountId; } decrementSemaphore(accountId); } Accounts::Account *FacebookSignonSyncAdaptor::loadAccount(int accountId) { Accounts::Account *acc = 0; if (m_accounts.contains(accountId)) { acc = m_accounts[accountId]; } else { acc = Accounts::Account::fromId(&m_accountManager, accountId, this); if (!acc) { qCWarning(lcSocialPlugin) << "Facebook account" << accountId << "was deleted during signon refresh sync"; return 0; } else { m_accounts.insert(accountId, acc); } } Accounts::Service srv = m_accountManager.service(syncServiceName()); if (!srv.isValid()) { qCWarning(lcSocialPlugin) << "invalid service" << syncServiceName() << "specified for refresh sync with Facebook account" << accountId; return 0; } return acc; } void FacebookSignonSyncAdaptor::raiseCredentialsNeedUpdateFlag(int accountId) { Accounts::Account *acc = loadAccount(accountId); if (acc) { qCWarning(lcSocialPlugin) << "FBSSA: raising CredentialsNeedUpdate flag"; Accounts::Service srv = m_accountManager.service(syncServiceName()); acc->selectService(srv); acc->setValue(QStringLiteral("CredentialsNeedUpdate"), QVariant::fromValue(true)); acc->setValue(QStringLiteral("CredentialsNeedUpdateFrom"), QVariant::fromValue(QString::fromLatin1("sociald-facebook-signon"))); acc->selectService(Accounts::Service()); acc->syncAndBlock(); } } void FacebookSignonSyncAdaptor::lowerCredentialsNeedUpdateFlag(int accountId) { Accounts::Account *acc = loadAccount(accountId); if (acc) { qCInfo(lcSocialPlugin) << "FBSSA: lowering CredentialsNeedUpdate flag"; Accounts::Service srv = m_accountManager.service(syncServiceName()); acc->selectService(srv); acc->setValue(QStringLiteral("CredentialsNeedUpdate"), QVariant::fromValue(false)); acc->remove(QStringLiteral("CredentialsNeedUpdateFrom")); acc->selectService(Accounts::Service()); acc->syncAndBlock(); } } void FacebookSignonSyncAdaptor::forceTokenExpiry(int seconds, int accountId, const QString &accessToken) { Accounts::Account *acc = loadAccount(accountId); if (acc) { // force expiry of cached tokens to signon db via ProvidedTokens hook Accounts::Service srv(m_accountManager.service(syncServiceName())); acc->selectService(srv); SignOn::Identity *identity = acc->credentialsId() > 0 ? SignOn::Identity::existingIdentity(acc->credentialsId()) : 0; if (!identity) { qCWarning(lcSocialPlugin) << "Facebook account" << accountId << "has no valid credentials, cannot perform refresh sync"; return; } Accounts::AccountService *accSrv = new Accounts::AccountService(acc, srv); if (!accSrv) { qCWarning(lcSocialPlugin) << "Facebook account" << accountId << "has no valid account service, cannot perform refresh sync"; identity->deleteLater(); return; } QString method = accSrv->authData().method(); QString mechanism = accSrv->authData().mechanism(); SignOn::AuthSession *session = identity->createSession(method); if (!session) { qCWarning(lcSocialPlugin) << "could not create signon session for Facebook account" << accountId << "cannot perform refresh sync"; accSrv->deleteLater(); identity->deleteLater(); return; } QVariantMap providedTokens; providedTokens.insert("AccessToken", accessToken); providedTokens.insert("RefreshToken", QString()); providedTokens.insert("ExpiresIn", seconds); QVariantMap signonSessionData = accSrv->authData().parameters(); signonSessionData.insert("ClientId", clientId()); signonSessionData.insert("UiPolicy", SignOn::NoUserInteractionPolicy); signonSessionData.insert("ProvidedTokens", providedTokens); connect(session, SIGNAL(response(SignOn::SessionData)), this, SLOT(forceTokenExpiryResponse(SignOn::SessionData)), Qt::UniqueConnection); connect(session, SIGNAL(error(SignOn::Error)), this, SLOT(forceTokenExpiryError(SignOn::Error)), Qt::UniqueConnection); incrementSemaphore(accountId); session->setProperty("accountId", accountId); session->setProperty("seconds", seconds); session->process(SignOn::SessionData(signonSessionData), mechanism); } } void FacebookSignonSyncAdaptor::forceTokenExpiryResponse(const SignOn::SessionData &responseData) { SignOn::AuthSession *session = qobject_cast(sender()); int accountId = session->property("accountId").toInt(); int seconds = session->property("seconds").toInt(); QVariantMap vmrd; foreach (const QString &key, responseData.propertyNames()) { vmrd.insert(key, responseData.getProperty(key)); } qCDebug(lcSocialPlugin) << "forcibly updated cache for Facebook account" << accountId << "," << "ExpiresIn now:" << vmrd.value("ExpiresIn").toInt() << ", expected" << seconds; if (seconds == 0) { // successfully forced expiry qCWarning(lcSocialPlugin) << "forced expiry for reportedly invalid token"; raiseCredentialsNeedUpdateFlag(accountId); } else { // successfully forced new ExpiresIn value lowerCredentialsNeedUpdateFlag(accountId); } decrementSemaphore(accountId); } void FacebookSignonSyncAdaptor::forceTokenExpiryError(const SignOn::Error &error) { SignOn::AuthSession *session = qobject_cast(sender()); int accountId = session->property("accountId").toInt(); int seconds = session->property("seconds").toInt(); qCInfo(lcSocialPlugin) << "got signon error when performing force-expire for Facebook account" << accountId << ":" << error.type() << "," << error.message(); if (seconds == 0) { // we treat the error as if it was a success, since we need to update the credentials anyway. qCWarning(lcSocialPlugin) << "forced expiry for reportedly invalid token failed"; raiseCredentialsNeedUpdateFlag(accountId); } else { // don't raise or lower the flag. If was previously not raised, // presumably it's because ExpiresIn hadn't reached zero. } decrementSemaphore(accountId); } buteo-sync-plugins-social-0.4.28/src/facebook/facebook-signon/facebooksignonsyncadaptor.h000066400000000000000000000050361474572147200316610ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2013-2014 Jolla Ltd. ** Contact: Chris Adams ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef FACEBOOKSIGNONSYNCADAPTOR_H #define FACEBOOKSIGNONSYNCADAPTOR_H #include "facebookdatatypesyncadaptor.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include class FacebookSignonSyncAdaptor : public FacebookDataTypeSyncAdaptor { Q_OBJECT public: FacebookSignonSyncAdaptor(QObject *parent); ~FacebookSignonSyncAdaptor(); QString syncServiceName() const; void sync(const QString &dataTypeString, int accountId); protected: // implementing FacebookDataTypeSyncAdaptor interface void purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode mode); void beginSync(int accountId, const QString &accessToken); void finalize(int accountId); private Q_SLOTS: void requestFinishedHandler(); void forceTokenExpiryResponse(const SignOn::SessionData &responseData); void forceTokenExpiryError(const SignOn::Error &error); private: Accounts::Account *loadAccount(int accountId); void raiseCredentialsNeedUpdateFlag(int accountId); void lowerCredentialsNeedUpdateFlag(int accountId); void forceTokenExpiry(int seconds, int accountId, const QString &accessToken); Accounts::Manager m_accountManager; QMap m_accounts; }; #endif // FACEBOOKSIGNONSYNCADAPTOR_H buteo-sync-plugins-social-0.4.28/src/facebook/facebook.pro000066400000000000000000000001641474572147200234750ustar00rootroot00000000000000TEMPLATE = subdirs SUBDIRS = \ $$PWD/facebook-signon \ $$PWD/facebook-calendars \ $$PWD/facebook-images buteo-sync-plugins-social-0.4.28/src/facebook/facebookdatatypesyncadaptor.cpp000066400000000000000000000245751474572147200274770ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2013-2014 Jolla Ltd. ** Contact: Chris Adams ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "facebookdatatypesyncadaptor.h" #include "trace.h" #include #include #include #include #include //libsailfishkeyprovider #include // libaccounts-qt5 #include #include #include #include //libsignon-qt: SignOn::NoUserInteractionPolicy #include #include #include FacebookDataTypeSyncAdaptor::FacebookDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::DataType dataType, QObject *parent) : SocialNetworkSyncAdaptor("facebook", dataType, 0, parent), m_triedLoading(false) { } FacebookDataTypeSyncAdaptor::~FacebookDataTypeSyncAdaptor() { } void FacebookDataTypeSyncAdaptor::sync(const QString &dataTypeString, int accountId) { if (dataTypeString != SocialNetworkSyncAdaptor::dataTypeName(m_dataType)) { qCWarning(lcSocialPlugin) << "Facebook" << SocialNetworkSyncAdaptor::dataTypeName(m_dataType) << "sync adaptor was asked to sync" << dataTypeString; setStatus(SocialNetworkSyncAdaptor::Error); return; } if (clientId().isEmpty()) { qCWarning(lcSocialPlugin) << "client id couldn't be retrieved for Facebook account" << accountId; setStatus(SocialNetworkSyncAdaptor::Error); return; } setStatus(SocialNetworkSyncAdaptor::Busy); updateDataForAccount(accountId); qCDebug(lcSocialPlugin) << "successfully triggered sync with profile:" << m_accountSyncProfile->name(); } void FacebookDataTypeSyncAdaptor::updateDataForAccount(int accountId) { Accounts::Account *account = Accounts::Account::fromId(m_accountManager, accountId, this); if (!account) { qCWarning(lcSocialPlugin) << "existing account with id" << accountId << "couldn't be retrieved"; setStatus(SocialNetworkSyncAdaptor::Error); return; } // will be decremented by either signOnError or signOnResponse. incrementSemaphore(accountId); signIn(account); } void FacebookDataTypeSyncAdaptor::errorHandler(QNetworkReply::NetworkError err) { QNetworkReply *reply = qobject_cast(sender()); QByteArray replyData = reply->readAll(); int accountId = reply->property("accountId").toInt(); qCWarning(lcSocialPlugin) << SocialNetworkSyncAdaptor::dataTypeName(m_dataType) << "request with account" << accountId << "experienced error:" << err; // set "isError" on the reply so that adapters know to ignore the result in the finished() handler reply->setProperty("isError", QVariant::fromValue(true)); // Note: not all errors are "unrecoverable" errors, so we don't change the status here. bool ok = false; QJsonObject parsed = parseJsonObjectReplyData(replyData, &ok); if (ok && parsed.contains(QLatin1String("error"))) { QJsonObject errorReply = parsed.value("error").toObject(); // Password Changed on server side if (errorReply.value("code").toDouble() == 190 && errorReply.value("error_subcode").toDouble() == 460) { int accountId = reply->property("accountId").toInt(); Accounts::Account *account = Accounts::Account::fromId(m_accountManager, accountId, this); if (account) { setCredentialsNeedUpdate(account); } } } } void FacebookDataTypeSyncAdaptor::sslErrorsHandler(const QList &errs) { QString sslerrs; foreach (const QSslError &e, errs) { sslerrs += e.errorString() + "; "; } if (errs.size() > 0) { sslerrs.chop(2); } qCWarning(lcSocialPlugin) << SocialNetworkSyncAdaptor::dataTypeName(m_dataType) << "request with account" << sender()->property("accountId").toInt() << "experienced ssl errors:" << sslerrs; // set "isError" on the reply so that adapters know to ignore the result in the finished() handler sender()->setProperty("isError", QVariant::fromValue(true)); // Note: not all errors are "unrecoverable" errors, so we don't change the status here. } QString FacebookDataTypeSyncAdaptor::clientId() { if (!m_triedLoading) { loadClientId(); } return m_clientId; } QString FacebookDataTypeSyncAdaptor::graphAPI(const QString &request) const { return m_graphAPI + request; } void FacebookDataTypeSyncAdaptor::loadClientId() { m_triedLoading = true; char *cClientId = NULL; int cSuccess = SailfishKeyProvider_storedKey("facebook", "facebook-sync", "client_id", &cClientId); if (cSuccess != 0 || cClientId == NULL) { return; } m_clientId = QLatin1String(cClientId); free(cClientId); return; } void FacebookDataTypeSyncAdaptor::setCredentialsNeedUpdate(Accounts::Account *account) { qWarning() << "sociald:Facebook: setting CredentialsNeedUpdate to true for account:" << account->id(); Accounts::Service srv(m_accountManager->service(syncServiceName())); account->selectService(srv); account->setValue(QStringLiteral("CredentialsNeedUpdate"), QVariant::fromValue(true)); account->setValue(QStringLiteral("CredentialsNeedUpdateFrom"), QVariant::fromValue(QString::fromLatin1("sociald-facebook"))); account->selectService(Accounts::Service()); account->syncAndBlock(); } void FacebookDataTypeSyncAdaptor::signIn(Accounts::Account *account) { // Fetch consumer key and secret from keyprovider int accountId = account->id(); if (!checkAccount(account) || clientId().isEmpty()) { decrementSemaphore(accountId); return; } // grab out a valid identity for the sync service. Accounts::Service srv(m_accountManager->service(syncServiceName())); account->selectService(srv); SignOn::Identity *identity = account->credentialsId() > 0 ? SignOn::Identity::existingIdentity(account->credentialsId()) : 0; if (!identity) { qCWarning(lcSocialPlugin) << "account" << accountId << "has no valid credentials, cannot sign in"; decrementSemaphore(accountId); return; } Accounts::AccountService accSrv(account, srv); QString method = accSrv.authData().method(); QString mechanism = accSrv.authData().mechanism(); SignOn::AuthSession *session = identity->createSession(method); if (!session) { qCWarning(lcSocialPlugin) << "could not create signon session for account" << accountId; identity->deleteLater(); decrementSemaphore(accountId); return; } QVariantMap signonSessionData = accSrv.authData().parameters(); signonSessionData.insert("ClientId", clientId()); signonSessionData.insert("UiPolicy", SignOn::NoUserInteractionPolicy); connect(session, SIGNAL(response(SignOn::SessionData)), this, SLOT(signOnResponse(SignOn::SessionData)), Qt::UniqueConnection); connect(session, SIGNAL(error(SignOn::Error)), this, SLOT(signOnError(SignOn::Error)), Qt::UniqueConnection); session->setProperty("account", QVariant::fromValue(account)); session->setProperty("identity", QVariant::fromValue(identity)); session->process(SignOn::SessionData(signonSessionData), mechanism); } void FacebookDataTypeSyncAdaptor::signOnError(const SignOn::Error &error) { SignOn::AuthSession *session = qobject_cast(sender()); Accounts::Account *account = session->property("account").value(); SignOn::Identity *identity = session->property("identity").value(); int accountId = account->id(); qCWarning(lcSocialPlugin) << "credentials for account with id" << accountId << "couldn't be retrieved:" << error.type() << error.message(); // if the error is because credentials have expired, we // set the CredentialsNeedUpdate key. if (error.type() == SignOn::Error::UserInteraction) { setCredentialsNeedUpdate(account); } session->disconnect(this); identity->destroySession(session); identity->deleteLater(); account->deleteLater(); // if we couldn't sign in, we can't sync with this account. setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(accountId); } void FacebookDataTypeSyncAdaptor::signOnResponse(const SignOn::SessionData &responseData) { QVariantMap data; foreach (const QString &key, responseData.propertyNames()) { data.insert(key, responseData.getProperty(key)); } QString accessToken; SignOn::AuthSession *session = qobject_cast(sender()); Accounts::Account *account = session->property("account").value(); SignOn::Identity *identity = session->property("identity").value(); int accountId = account->id(); if (data.contains(QLatin1String("AccessToken"))) { accessToken = data.value(QLatin1String("AccessToken")).toString(); } else { qCInfo(lcSocialPlugin) << "signon response for account with id" << accountId << "contained no access token"; } m_graphAPI = account->value(QStringLiteral("graph_api/Host")).toString(); session->disconnect(this); identity->destroySession(session); identity->deleteLater(); account->deleteLater(); if (!accessToken.isEmpty()) { beginSync(accountId, accessToken); // call the derived-class sync entrypoint. } decrementSemaphore(accountId); } buteo-sync-plugins-social-0.4.28/src/facebook/facebookdatatypesyncadaptor.h000066400000000000000000000050411474572147200271270ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2013-2014 Jolla Ltd. ** Contact: Chris Adams ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef FACEBOOKDATATYPESYNCADAPTOR_H #define FACEBOOKDATATYPESYNCADAPTOR_H #include "socialnetworksyncadaptor.h" #include #include #include #include #include namespace Accounts { class Account; } namespace SignOn { class Error; class SessionData; } /* Abstract interface for all of the data-specific sync adaptors which pull data from the Facebook social network. */ class FacebookDataTypeSyncAdaptor : public SocialNetworkSyncAdaptor { Q_OBJECT public: FacebookDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::DataType dataType, QObject *parent); virtual ~FacebookDataTypeSyncAdaptor(); virtual void sync(const QString &dataTypeString, int accountId); protected: QString clientId(); QString graphAPI(const QString &request = QString()) const; virtual void updateDataForAccount(int accountIds); virtual void beginSync(int accountId, const QString &accessToken) = 0; protected Q_SLOTS: virtual void errorHandler(QNetworkReply::NetworkError err); virtual void sslErrorsHandler(const QList &errs); private Q_SLOTS: void signOnError(const SignOn::Error &error); void signOnResponse(const SignOn::SessionData &responseData); private: void loadClientId(); void setCredentialsNeedUpdate(Accounts::Account *account); void signIn(Accounts::Account *account); bool m_triedLoading; // Is true if we tried to load (even if we failed) QString m_clientId; QString m_graphAPI; }; #endif // FACEBOOKDATATYPESYNCADAPTOR_H buteo-sync-plugins-social-0.4.28/src/google/000077500000000000000000000000001474572147200207045ustar00rootroot00000000000000buteo-sync-plugins-social-0.4.28/src/google/google-calendars/000077500000000000000000000000001474572147200241125ustar00rootroot00000000000000buteo-sync-plugins-social-0.4.28/src/google/google-calendars/google-calendars.pri000066400000000000000000000003611474572147200300340ustar00rootroot00000000000000CONFIG += link_pkgconfig PKGCONFIG += libmkcal-qt5 KF5CalendarCore SOURCES += \ $$PWD/googlecalendarsyncadaptor.cpp HEADERS += \ $$PWD/googlecalendarsyncadaptor.h \ $$PWD/googlecalendarincidencecomparator.h INCLUDEPATH += $$PWD buteo-sync-plugins-social-0.4.28/src/google/google-calendars/google-calendars.pro000066400000000000000000000012561474572147200300460ustar00rootroot00000000000000TARGET = google-calendars-client include($$PWD/../../common.pri) include($$PWD/../google-common.pri) include($$PWD/google-calendars.pri) google_calendars_sync_profile.path = /etc/buteo/profiles/sync google_calendars_sync_profile.files = $$PWD/google.Calendars.xml google_calendars_client_plugin_xml.path = /etc/buteo/profiles/client google_calendars_client_plugin_xml.files = $$PWD/google-calendars.xml HEADERS += googlecalendarsplugin.h SOURCES += googlecalendarsplugin.cpp OTHER_FILES += \ google_calendars_sync_profile.files \ google_calendars_client_plugin_xml.files INSTALLS += \ target \ google_calendars_sync_profile \ google_calendars_client_plugin_xml buteo-sync-plugins-social-0.4.28/src/google/google-calendars/google-calendars.xml000066400000000000000000000002071474572147200300410ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/google/google-calendars/google.Calendars.xml000066400000000000000000000011411474572147200300000ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/google/google-calendars/googlecalendarincidencecomparator.h000066400000000000000000000247051474572147200331730ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015 Jolla Ltd. ** Contact: Chris Adams ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef GOOGLECALENDARINCIDENCECOMPARATOR_H #define GOOGLECALENDARINCIDENCECOMPARATOR_H #include #include #include #include #include #include #include #include #include #include #include "trace.h" #define GIC_RETURN_FALSE_IF_NOT_EQUAL(a, b, func, desc) {\ if (a->func != b->func) {\ qCDebug(lcSocialPlugin) << "Incidence" << desc << "" << "properties are not equal:" << a->func << b->func; \ return false;\ }\ } #define GIC_RETURN_FALSE_IF_NOT_EQUAL_CUSTOM(failureCheck, desc, debug) {\ if (failureCheck) {\ qCDebug(lcSocialPlugin) << "Incidence" << desc << "properties are not equal:" << desc << debug; \ return false;\ }\ } namespace GoogleCalendarIncidenceComparator { void normalizePersonEmail(KCalendarCore::Person *p) { QString email = p->email().replace(QStringLiteral("mailto:"), QString(), Qt::CaseInsensitive); if (email != p->email()) { p->setEmail(email); } } template bool pointerDataEqual(const QVector > &vectorA, const QVector > &vectorB) { if (vectorA.count() != vectorB.count()) { return false; } for (int i=0; idateEnd() != b->dateEnd(), "dateEnd", (a->dateEnd().toString() + " != " + b->dateEnd().toString())); GIC_RETURN_FALSE_IF_NOT_EQUAL(a, b, transparency(), "transparency"); // some special handling for dtEnd() depending on whether it's an all-day event or not. if (a->allDay() && b->allDay()) { GIC_RETURN_FALSE_IF_NOT_EQUAL_CUSTOM(a->dtEnd().date() != b->dtEnd().date(), "dtEnd", (a->dtEnd().toString() + " != " + b->dtEnd().toString())); } else { GIC_RETURN_FALSE_IF_NOT_EQUAL_CUSTOM(a->dtEnd() != b->dtEnd(), "dtEnd", (a->dtEnd().toString() + " != " + b->dtEnd().toString())); } // some special handling for isMultiday() depending on whether it's an all-day event or not. if (a->allDay() && b->allDay()) { // here we assume that both events are in "export form" (that is, exclusive DTEND) if (a->dtEnd().date() != b->dtEnd().date()) { qCDebug(lcSocialPlugin) << "have a->dtStart()" << a->dtStart().toString() << ", a->dtEnd()" << a->dtEnd().toString(); qCDebug(lcSocialPlugin) << "have b->dtStart()" << b->dtStart().toString() << ", b->dtEnd()" << b->dtEnd().toString(); qCDebug(lcSocialPlugin) << "have a->isMultiDay()" << a->isMultiDay() << ", b->isMultiDay()" << b->isMultiDay(); return false; } } else { GIC_RETURN_FALSE_IF_NOT_EQUAL(a, b, isMultiDay(), "multiday"); } // Don't compare hasEndDate() as Event(Event*) does not initialize it based on the validity of // dtEnd(), so it could be false when dtEnd() is valid. The dtEnd comparison above is sufficient. return true; } bool todosEqual(const KCalendarCore::Todo::Ptr &a, const KCalendarCore::Todo::Ptr &b) { GIC_RETURN_FALSE_IF_NOT_EQUAL(a, b, hasCompletedDate(), "hasCompletedDate"); GIC_RETURN_FALSE_IF_NOT_EQUAL_CUSTOM(a->dtRecurrence() != b->dtRecurrence(), "dtRecurrence", (a->dtRecurrence().toString() + " != " + b->dtRecurrence().toString())); GIC_RETURN_FALSE_IF_NOT_EQUAL(a, b, hasDueDate(), "hasDueDate"); GIC_RETURN_FALSE_IF_NOT_EQUAL_CUSTOM(a->dtDue() != b->dtDue(), "dtDue", (a->dtDue().toString() + " != " + b->dtDue().toString())); GIC_RETURN_FALSE_IF_NOT_EQUAL(a, b, hasStartDate(), "hasStartDate"); GIC_RETURN_FALSE_IF_NOT_EQUAL(a, b, isCompleted(), "isCompleted"); GIC_RETURN_FALSE_IF_NOT_EQUAL_CUSTOM(a->completed() != b->completed(), "completed", (a->completed().toString() + " != " + b->completed().toString())); GIC_RETURN_FALSE_IF_NOT_EQUAL(a, b, isOpenEnded(), "isOpenEnded"); GIC_RETURN_FALSE_IF_NOT_EQUAL(a, b, percentComplete(), "percentComplete"); return true; } bool journalsEqual(const KCalendarCore::Journal::Ptr &, const KCalendarCore::Journal::Ptr &) { // no journal-specific properties; it only uses the base incidence properties return true; } // Checks whether a specific set of properties are equal. bool incidencesEqual(const KCalendarCore::Incidence::Ptr &a, const KCalendarCore::Incidence::Ptr &b) { if (!a || !b) { qWarning() << "Invalid parameters! a:" << a << "b:" << b; return false; } // Do not compare created() or lastModified() because we don't update these fields when // an incidence is updated by copyIncidenceProperties(), so they are guaranteed to be unequal. // TODO compare deref alarms and attachment lists to compare them also. // Don't compare resources() for now because KCalendarCore may insert QStringList("") as the resources // when in fact it should be QStringList(), which causes the comparison to fail. GIC_RETURN_FALSE_IF_NOT_EQUAL(a, b, type(), "type"); GIC_RETURN_FALSE_IF_NOT_EQUAL(a, b, duration(), "duration"); GIC_RETURN_FALSE_IF_NOT_EQUAL(a, b, hasDuration(), "hasDuration"); GIC_RETURN_FALSE_IF_NOT_EQUAL(a, b, isReadOnly(), "isReadOnly"); GIC_RETURN_FALSE_IF_NOT_EQUAL(a, b, comments(), "comments"); GIC_RETURN_FALSE_IF_NOT_EQUAL(a, b, contacts(), "contacts"); GIC_RETURN_FALSE_IF_NOT_EQUAL(a, b, altDescription(), "altDescription"); GIC_RETURN_FALSE_IF_NOT_EQUAL(a, b, categories(), "categories"); GIC_RETURN_FALSE_IF_NOT_EQUAL(a, b, customStatus(), "customStatus"); GIC_RETURN_FALSE_IF_NOT_EQUAL(a, b, description(), "description"); GIC_RETURN_FALSE_IF_NOT_EQUAL_CUSTOM(!qFuzzyCompare(a->geoLatitude(), b->geoLatitude()), "geoLatitude", (QString("%1 != %2").arg(a->geoLatitude()).arg(b->geoLatitude()))); GIC_RETURN_FALSE_IF_NOT_EQUAL_CUSTOM(!qFuzzyCompare(a->geoLongitude(), b->geoLongitude()), "geoLongitude", (QString("%1 != %2").arg(a->geoLongitude()).arg(b->geoLongitude()))); GIC_RETURN_FALSE_IF_NOT_EQUAL(a, b, hasGeo(), "hasGeo"); GIC_RETURN_FALSE_IF_NOT_EQUAL(a, b, location(), "location"); GIC_RETURN_FALSE_IF_NOT_EQUAL(a, b, secrecy(), "secrecy"); GIC_RETURN_FALSE_IF_NOT_EQUAL(a, b, status(), "status"); GIC_RETURN_FALSE_IF_NOT_EQUAL(a, b, summary(), "summary"); // check recurrence information. Note that we only need to check the recurrence rules for equality if they both recur. GIC_RETURN_FALSE_IF_NOT_EQUAL_CUSTOM(a->recurs() != b->recurs(), "recurs", a->recurs() + " != " + b->recurs()); GIC_RETURN_FALSE_IF_NOT_EQUAL_CUSTOM(a->recurs() && *(a->recurrence()) != *(b->recurrence()), "recurrence", "..."); // some special handling for dtStart() depending on whether it's an all-day event or not. if (a->allDay() && b->allDay()) { GIC_RETURN_FALSE_IF_NOT_EQUAL_CUSTOM(a->dtStart().date() != b->dtStart().date(), "dtStart", (a->dtStart().toString() + " != " + b->dtStart().toString())); } else { GIC_RETURN_FALSE_IF_NOT_EQUAL_CUSTOM(a->dtStart() != b->dtStart(), "dtStart", (a->dtStart().toString() + " != " + b->dtStart().toString())); } // Some servers insert a mailto: in the organizer email address, so ignore this when comparing organizers KCalendarCore::Person personA(a->organizer()); KCalendarCore::Person personB(b->organizer()); normalizePersonEmail(&personA); normalizePersonEmail(&personB); const QString aEmail = personA.email(); const QString bEmail = personB.email(); // If the aEmail is empty, the local event doesn't have organizer info. // That's ok - Google will add organizer/creator info when we upsync, // so don't treat it as a local modification. // Otherwise, it is a "real" change. if (aEmail != bEmail && !aEmail.isEmpty()) { GIC_RETURN_FALSE_IF_NOT_EQUAL_CUSTOM(personA != personB, "organizer", (personA.fullName() + " != " + personB.fullName())); } switch (a->type()) { case KCalendarCore::IncidenceBase::TypeEvent: if (!eventsEqual(a.staticCast(), b.staticCast())) { return false; } break; case KCalendarCore::IncidenceBase::TypeTodo: if (!todosEqual(a.staticCast(), b.staticCast())) { return false; } break; case KCalendarCore::IncidenceBase::TypeJournal: if (!journalsEqual(a.staticCast(), b.staticCast())) { return false; } break; case KCalendarCore::IncidenceBase::TypeFreeBusy: case KCalendarCore::IncidenceBase::TypeUnknown: qCDebug(lcSocialPlugin) << "Unable to compare FreeBusy or Unknown incidence, assuming equal"; break; } return true; } } #endif // GOOGLECALENDARINCIDENCECOMPARATOR_H buteo-sync-plugins-social-0.4.28/src/google/google-calendars/googlecalendarsplugin.cpp000066400000000000000000000036061474572147200311730ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2014 - 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "googlecalendarsplugin.h" #include "googlecalendarsyncadaptor.h" #include "socialnetworksyncadaptor.h" GoogleCalendarsPlugin::GoogleCalendarsPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *callbackInterface) : SocialdButeoPlugin(pluginName, profile, callbackInterface, QStringLiteral("google"), SocialNetworkSyncAdaptor::dataTypeName(SocialNetworkSyncAdaptor::Calendars)) { } GoogleCalendarsPlugin::~GoogleCalendarsPlugin() { } SocialNetworkSyncAdaptor *GoogleCalendarsPlugin::createSocialNetworkSyncAdaptor() { return new GoogleCalendarSyncAdaptor(this); } Buteo::ClientPlugin* GoogleCalendarPluginLoader::createClientPlugin( const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) { return new GoogleCalendarsPlugin(pluginName, profile, cbInterface); } buteo-sync-plugins-social-0.4.28/src/google/google-calendars/googlecalendarsplugin.h000066400000000000000000000036071474572147200306410ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2014 - 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef GOOGLECALENDARSPLUGIN_H #define GOOGLECALENDARSPLUGIN_H #include "socialdbuteoplugin.h" #include class Q_DECL_EXPORT GoogleCalendarsPlugin : public SocialdButeoPlugin { Q_OBJECT public: GoogleCalendarsPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *cbInterface); ~GoogleCalendarsPlugin(); protected: SocialNetworkSyncAdaptor *createSocialNetworkSyncAdaptor(); }; class GoogleCalendarPluginLoader : public Buteo::SyncPluginLoader { Q_OBJECT Q_PLUGIN_METADATA(IID "org.sailfishos.plugins.sync.GoogleCalendarPluginLoader") Q_INTERFACES(Buteo::SyncPluginLoader) public: Buteo::ClientPlugin* createClientPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) override; }; #endif // GOOGLECALENDARSPLUGIN_H buteo-sync-plugins-social-0.4.28/src/google/google-calendars/googlecalendarsyncadaptor.cpp000066400000000000000000004713701474572147200320500ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2013 - 2019 Jolla Ltd. ** Copyright (C) 2020 Open Mobile Platform LLC. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "googlecalendarsyncadaptor.h" #include "googlecalendarincidencecomparator.h" #include "trace.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include //---------------------------------------------- #define QDATEONLY_FORMAT "yyyy-MM-dd" #define RFC5545_FORMAT "yyyyMMddThhmmssZ" #define RFC5545_FORMAT_NTZC "yyyyMMddThhmmss" #define RFC5545_QDATE_FORMAT "yyyyMMdd" namespace { const int GOOGLE_CAL_SYNC_PLUGIN_VERSION = 3; const QByteArray NOTEBOOK_SERVER_SYNC_TOKEN_PROPERTY = QByteArrayLiteral("syncToken"); const QByteArray NOTEBOOK_SERVER_ID_PROPERTY = QByteArrayLiteral("calendarServerId"); const QByteArray NOTEBOOK_EMAIL_PROPERTY = QByteArrayLiteral("userPrincipalEmail"); const QByteArray SERVER_COLOR_PROPERTY = QByteArrayLiteral("serverColor"); const int COLLISION_ERROR_MAX_CONSECUTIVE = 8; const QByteArray VOLATILE_APP = QByteArrayLiteral("VOLATILE"); const QByteArray VOLATILE_NAME = QByteArrayLiteral("SYNC-FAILURE"); const QString ERROR_REASON_NON_ORGANIZER = QStringLiteral("forbiddenForNonOrganizer"); const QString ERROR_REASON_UPDATE_MIN_TOO_OLD = QStringLiteral("updatedMinTooLongAgo"); void errorDumpStr(const QString &str) { // Dump the entire string to the log. // Note that the log cannot handle newlines, // so we separate the string into chunks. Q_FOREACH (const QString &chunk, str.split('\n', QString::SkipEmptyParts)) { qCWarning(lcSocialPlugin) << chunk; } } void traceDumpStr(const QString &str) { if (!lcSocialPluginTrace().isDebugEnabled()) { return; } // Dump the entire string to the log. // Note that the log cannot handle newlines, // so we separate the string into chunks. Q_FOREACH (const QString &chunk, str.split('\n', QString::SkipEmptyParts)) { qCDebug(lcSocialPluginTrace) << chunk; } } // returns true if the ghost-event cleanup sync has been performed. bool ghostEventCleanupPerformed() { QString settingsFileName = QString::fromLatin1("%1/%2/gcal.ini") .arg(PRIVILEGED_DATA_DIR) .arg(QString::fromLatin1(SYNC_DATABASE_DIR)); QSettings settingsFile(settingsFileName, QSettings::IniFormat); return settingsFile.value(QString::fromLatin1("cleaned"), QVariant::fromValue(false)).toBool(); } void setGhostEventCleanupPerformed() { QString settingsFileName = QString::fromLatin1("%1/%2/gcal.ini") .arg(PRIVILEGED_DATA_DIR) .arg(QString::fromLatin1(SYNC_DATABASE_DIR)); QSettings settingsFile(settingsFileName, QSettings::IniFormat); settingsFile.setValue(QString::fromLatin1("cleaned"), QVariant::fromValue(true)); settingsFile.sync(); } void uniteIncidenceLists(const KCalendarCore::Incidence::List &first, KCalendarCore::Incidence::List *second) { int originalSecondSize = second->size(); bool foundMatch = false; Q_FOREACH (KCalendarCore::Incidence::Ptr inc, first) { foundMatch = false; for (int i = 0; i < originalSecondSize; ++i) { if (inc->uid() == second->at(i)->uid() && inc->recurrenceId() == second->at(i)->recurrenceId()) { // found a match foundMatch = true; break; } } if (!foundMatch) { second->append(inc); } } } QString gCalEventId(KCalendarCore::Incidence::Ptr event) { // we abuse the comments field to store our gcal-id. // we should use a custom property, but those are deleted on incidence deletion. const QStringList &comments(event->comments()); Q_FOREACH (const QString &comment, comments) { if (comment.startsWith("jolla-sociald:gcal-id:")) { return comment.mid(22); } } return QString(); } void setGCalEventId(KCalendarCore::Incidence::Ptr event, const QString &id) { // we abuse the comments field to store our gcal-id. // we should use a custom property, but those are deleted on incidence deletion. const QStringList &comments(event->comments()); Q_FOREACH (const QString &comment, comments) { if (comment.startsWith("jolla-sociald:gcal-id:")) { // remove any existing gcal-id comment. if (!event->removeComment(comment)) { qCDebug(lcSocialPlugin) << "Unable to remove comment:" << comment; } break; } } event->addComment(QStringLiteral("jolla-sociald:gcal-id:%1").arg(id)); } void setRemoteUidCustomField(KCalendarCore::Incidence::Ptr event, const QString &uid, const QString &id) { // store it also in a custom property purely for invitation lookup purposes. if (!uid.isEmpty()) { event->setNonKDECustomProperty("X-SAILFISHOS-REMOTE-UID", uid.toUtf8()); } else { // Google Calendar invites are sent as invitations with uid suffixed with @google.com. if (id.endsWith(QLatin1String("@google.com"), Qt::CaseInsensitive)) { event->setNonKDECustomProperty("X-SAILFISHOS-REMOTE-UID", id.toUtf8()); } else { QString suffixedId = id; suffixedId.append(QLatin1String("@google.com")); event->setNonKDECustomProperty("X-SAILFISHOS-REMOTE-UID", suffixedId.toUtf8()); } } } QString gCalETag(KCalendarCore::Incidence::Ptr event) { return event->customProperty("jolla-sociald", "gcal-etag"); } void setGCalETag(KCalendarCore::Incidence::Ptr event, const QString &etag) { // note: custom properties are purged on incidence deletion. event->setCustomProperty("jolla-sociald", "gcal-etag", etag); } QList datetimesFromExRDateStr(const QString &exrdatestr, bool *isDateOnly) { // possible forms: // RDATE:19970714T123000Z // RDATE;VALUE=DATE-TIME:19970714T123000Z // RDATE;VALUE=DATE-TIME:19970714T123000Z,19970715T123000Z // RDATE;TZID=America/New_York:19970714T083000 // RDATE;VALUE=PERIOD:19960403T020000Z/19960403T040000Z,19960404T010000Z/PT3H // RDATE;VALUE=DATE:19970101,19970120 QList retn; QString str = exrdatestr; *isDateOnly = false; // by default. if (str.startsWith(QStringLiteral("exdate"), Qt::CaseInsensitive)) { str.remove(0, 6); } else if (str.startsWith(QStringLiteral("rdate"), Qt::CaseInsensitive)) { str.remove(0, 5); } else { qCWarning(lcSocialPlugin) << "not an ex/rdate string:" << exrdatestr; return retn; } if (str.startsWith(';')) { str.remove(0,1); if (str.startsWith("VALUE=DATE-TIME:", Qt::CaseInsensitive)) { str.remove(0, 16); QStringList dts = str.split(','); Q_FOREACH (const QString &dtstr, dts) { if (dtstr.endsWith('Z')) { // UTC QDateTime dt = QDateTime::fromString(dtstr, RFC5545_FORMAT); dt.setTimeSpec(Qt::UTC); retn.append(dt); } else { // Floating time QDateTime dt = QDateTime::fromString(dtstr, RFC5545_FORMAT_NTZC); dt.setTimeSpec(Qt::LocalTime); retn.append(dt); } } } else if (str.startsWith("VALUE=DATE:", Qt::CaseInsensitive)) { str.remove(0, 11); QStringList dts = str.split(','); Q_FOREACH(const QString &dstr, dts) { QDate date = QLocale::c().toDate(dstr, RFC5545_QDATE_FORMAT); retn.append(QDateTime(date)); } } else if (str.startsWith("VALUE=PERIOD:", Qt::CaseInsensitive)) { qCWarning(lcSocialPlugin) << "unsupported parameter in ex/rdate string:" << exrdatestr; // TODO: support PERIOD formats, or just switch to CalDAV for Google sync... } else if (str.startsWith("TZID=") && str.contains(':')) { str.remove(0, 5); QString tzidstr = str.mid(0, str.indexOf(':')); // something like: "Australia/Brisbane" QTimeZone tz(tzidstr.toUtf8()); str.remove(0, tzidstr.size()+1); QStringList dts = str.split(','); Q_FOREACH (const QString &dtstr, dts) { QDateTime dt = QDateTime::fromString(dtstr, RFC5545_FORMAT_NTZC); if (!dt.isValid()) { // try parsing from alternate formats dt = QDateTime::fromString(dtstr, Qt::ISODate); } if (!dt.isValid()) { qCWarning(lcSocialPlugin) << "unable to parse datetime from ex/rdate string:" << exrdatestr; } else { if (tz.isValid()) { dt.setTimeZone(tz); } else { dt.setTimeSpec(Qt::LocalTime); qCInfo(lcSocialPlugin) << "WARNING: unknown tzid:" << tzidstr << "; assuming clock-time instead!"; } retn.append(dt); } } } else { qCWarning(lcSocialPlugin) << "invalid parameter in ex/rdate string:" << exrdatestr; } } else if (str.startsWith(':')) { str.remove(0,1); QStringList dts = str.split(','); Q_FOREACH (const QString &dtstr, dts) { if (dtstr.endsWith('Z')) { // UTC QDateTime dt = QDateTime::fromString(dtstr, RFC5545_FORMAT); if (!dt.isValid()) { // try parsing from alternate formats dt = QDateTime::fromString(dtstr, Qt::ISODate); } if (!dt.isValid()) { qCWarning(lcSocialPlugin) << "unable to parse datetime from ex/rdate string:" << exrdatestr; } else { // parsed successfully dt.setTimeSpec(Qt::UTC); retn.append(dt); } } else { // Floating time QDateTime dt = QDateTime::fromString(dtstr, RFC5545_FORMAT_NTZC); if (!dt.isValid()) { // try parsing from alternate formats dt = QDateTime::fromString(dtstr, Qt::ISODate); } if (!dt.isValid()) { qCWarning(lcSocialPlugin) << "unable to parse datetime from ex/rdate string:" << exrdatestr; } else { // parsed successfully dt.setTimeSpec(Qt::LocalTime); retn.append(dt); } } } } else { qCWarning(lcSocialPlugin) << "not a valid ex/rdate string:" << exrdatestr; } return retn; } QJsonArray recurrenceArray(KCalendarCore::Event::Ptr event, KCalendarCore::ICalFormat &icalFormat, const QList &exceptions) { QJsonArray retn; // RRULE KCalendarCore::Recurrence *kcalRecurrence = event->recurrence(); Q_FOREACH (KCalendarCore::RecurrenceRule *rrule, kcalRecurrence->rRules()) { QString rruleStr = icalFormat.toString(rrule); rruleStr.replace("\r\n", ""); retn.append(QJsonValue(rruleStr)); } // EXRULE Q_FOREACH (KCalendarCore::RecurrenceRule *exrule, kcalRecurrence->exRules()) { QString exruleStr = icalFormat.toString(exrule); exruleStr.replace("RRULE", "EXRULE"); exruleStr.replace("\r\n", ""); retn.append(QJsonValue(exruleStr)); } // RDATE (date) QString rdates; Q_FOREACH (const QDate &rdate, kcalRecurrence->rDates()) { rdates.append(QLocale::c().toString(rdate, RFC5545_QDATE_FORMAT)); rdates.append(','); } if (rdates.size()) { rdates.chop(1); // trailing comma retn.append(QJsonValue(QString::fromLatin1("RDATE;VALUE=DATE:%1").arg(rdates))); } // RDATE (date-time) QString rdatetimes; Q_FOREACH (const QDateTime &rdatetime, kcalRecurrence->rDateTimes()) { if (rdatetime.timeSpec() == Qt::LocalTime) { rdatetimes.append(rdatetime.toString(RFC5545_FORMAT_NTZC)); } else { rdatetimes.append(rdatetime.toUTC().toString(RFC5545_FORMAT)); } rdatetimes.append(','); } if (rdatetimes.size()) { rdatetimes.chop(1); // trailing comma retn.append(QJsonValue(QString::fromLatin1("RDATE;VALUE=DATE-TIME:%1").arg(rdatetimes))); } // EXDATE (date) QString exdates; Q_FOREACH (const QDate &exdate, kcalRecurrence->exDates()) { // mkcal adds an EXDATE for each exception event, whereas Google does not // So we only include the EXDATE if there's no exception associated with it if (!exceptions.contains(QDateTime(exdate))) { exdates.append(QLocale::c().toString(exdate, RFC5545_QDATE_FORMAT)); exdates.append(','); } } if (exdates.size()) { exdates.chop(1); // trailing comma retn.append(QJsonValue(QString::fromLatin1("EXDATE;VALUE=DATE:%1").arg(exdates))); } // EXDATE (date-time) QString exdatetimes; Q_FOREACH (const QDateTime &exdatetime, kcalRecurrence->exDateTimes()) { // mkcal adds an EXDATE for each exception event, whereas Google does not // So we only include the EXDATE if there's no exception associated with it if (!exceptions.contains(exdatetime)) { if (exdatetime.timeSpec() == Qt::LocalTime) { exdatetimes.append(exdatetime.toString(RFC5545_FORMAT_NTZC)); } else { exdatetimes.append(exdatetime.toUTC().toString(RFC5545_FORMAT)); } exdatetimes.append(','); } } if (exdatetimes.size()) { exdatetimes.chop(1); // trailing comma retn.append(QJsonValue(QString::fromLatin1("EXDATE;VALUE=DATE-TIME:%1").arg(exdatetimes))); } return retn; } QDateTime parseRecurrenceId(const QJsonObject &originalStartTime) { QString recurrenceIdStr; QString recurrenceIdTzStr; if (originalStartTime.contains(QLatin1String("date"))) { recurrenceIdStr = originalStartTime.value(QLatin1String("date")).toVariant().toString(); } else { recurrenceIdStr = originalStartTime.value(QLatin1String("dateTime")).toVariant().toString(); recurrenceIdTzStr = originalStartTime.value(QLatin1String("timeZone")).toVariant().toString(); } QDateTime recurrenceId = QDateTime::fromString(recurrenceIdStr, Qt::ISODate); if (!recurrenceIdTzStr.isEmpty()) { recurrenceId = recurrenceId.toTimeZone(QTimeZone(recurrenceIdTzStr.toLatin1())); } return recurrenceId; } QDateTime parseDateTimeString(const QString &dateTimeStr) { QDateTime parsedTime = QDateTime::fromString(dateTimeStr, Qt::ISODate); if (parsedTime.isNull()) { qWarning() << "Unable to parse date time from string:" << dateTimeStr; return QDateTime(); } return parsedTime.toTimeZone(QTimeZone::systemTimeZone()); } void extractCreatedAndUpdated(const QJsonObject &eventData, QDateTime *created, QDateTime *updated) { const QString createdStr = eventData.value(QLatin1String("created")).toVariant().toString(); const QString updatedStr = eventData.value(QLatin1String("updated")).toVariant().toString(); if (!createdStr.isEmpty()) { *created = parseDateTimeString(createdStr); } if (!updatedStr.isEmpty()) { *updated = parseDateTimeString(updatedStr); } } void extractStartAndEnd(const QJsonObject &eventData, bool *startExists, bool *endExists, bool *startIsDateOnly, bool *endIsDateOnly, bool *isAllDay, QDateTime *start, QDateTime *end) { *startIsDateOnly = false, *endIsDateOnly = false; QString startTimeString, endTimeString; QJsonObject startTimeData = eventData.value(QLatin1String("start")).toObject(); QJsonObject endTimeData = eventData.value(QLatin1String("end")).toObject(); if (!startTimeData.value(QLatin1String("date")).toVariant().toString().isEmpty()) { *startExists = true; *startIsDateOnly = true; // all-day event. startTimeString = startTimeData.value(QLatin1String("date")).toVariant().toString(); } else if (!startTimeData.value(QLatin1String("dateTime")).toVariant().toString().isEmpty()) { *startExists = true; startTimeString = startTimeData.value(QLatin1String("dateTime")).toVariant().toString(); } else { *startExists = false; } if (!endTimeData.value(QLatin1String("date")).toVariant().toString().isEmpty()) { *endExists = true; *endIsDateOnly = true; // all-day event. endTimeString = endTimeData.value(QLatin1String("date")).toVariant().toString(); } else if (!endTimeData.value(QLatin1String("dateTime")).toVariant().toString().isEmpty()) { *endExists = true; endTimeString = endTimeData.value(QLatin1String("dateTime")).toVariant().toString(); } else { *endExists = false; } if (*startExists) { if (!*startIsDateOnly) { *start = parseDateTimeString(startTimeString); } else { *start = QDateTime(QLocale::c().toDate(startTimeString, QDATEONLY_FORMAT)); } } if (*endExists) { if (!*endIsDateOnly) { *end = parseDateTimeString(endTimeString); } else { // Special handling for all-day events is required. if (*startExists && *startIsDateOnly) { if (QLocale::c().toDate(startTimeString, QDATEONLY_FORMAT) == QLocale::c().toDate(endTimeString, QDATEONLY_FORMAT)) { // single-day all-day event *endExists = false; *isAllDay = true; } else if (QLocale::c().toDate(startTimeString, QDATEONLY_FORMAT) == QLocale::c().toDate(endTimeString, QDATEONLY_FORMAT).addDays(-1)) { // Google will send a single-day all-day event has having an end-date // of startDate+1 to conform to iCal spec. Hence, this is actually // a single-day all-day event, despite the difference in end-date. *endExists = false; *isAllDay = true; } else { // multi-day all-day event. // as noted above, Google will send all-day events as having an end-date // of real-end-date+1 in order to conform to iCal spec (exclusive end dt). *end = QDateTime(QLocale::c().toDate(endTimeString, QDATEONLY_FORMAT).addDays(-1)); *isAllDay = true; } } else { *end = QDateTime(QLocale::c().toDate(endTimeString, QDATEONLY_FORMAT)); *isAllDay = false; } } } } void extractRecurrence(const QJsonArray &recurrence, KCalendarCore::Event::Ptr event, KCalendarCore::ICalFormat &icalFormat) { KCalendarCore::Recurrence *kcalRecurrence = event->recurrence(); kcalRecurrence->clear(); // avoid adding duplicate recurrence information for (int i = 0; i < recurrence.size(); ++i) { QString ruleStr = recurrence.at(i).toString(); if (ruleStr.startsWith(QString::fromLatin1("rrule"), Qt::CaseInsensitive)) { KCalendarCore::RecurrenceRule *rrule = new KCalendarCore::RecurrenceRule; if (!icalFormat.fromString(rrule, ruleStr.mid(6))) { qCDebug(lcSocialPlugin) << "unable to parse RRULE information:" << ruleStr; traceDumpStr(QString::fromUtf8(QJsonDocument(recurrence).toJson())); } else { // Set the recurrence start to be the event start rrule->setStartDt(event->dtStart()); kcalRecurrence->addRRule(rrule); } } else if (ruleStr.startsWith(QString::fromLatin1("exrule"), Qt::CaseInsensitive)) { KCalendarCore::RecurrenceRule *exrule = new KCalendarCore::RecurrenceRule; if (!icalFormat.fromString(exrule, ruleStr.mid(7))) { qCDebug(lcSocialPlugin) << "unable to parse EXRULE information:" << ruleStr; traceDumpStr(QString::fromUtf8(QJsonDocument(recurrence).toJson())); } else { kcalRecurrence->addExRule(exrule); } } else if (ruleStr.startsWith(QString::fromLatin1("rdate"), Qt::CaseInsensitive)) { bool isDateOnly = false; QList rdatetimes = datetimesFromExRDateStr(ruleStr, &isDateOnly); if (!rdatetimes.size()) { qCDebug(lcSocialPlugin) << "unable to parse RDATE information:" << ruleStr; traceDumpStr(QString::fromUtf8(QJsonDocument(recurrence).toJson())); } else { Q_FOREACH (const QDateTime &dt, rdatetimes) { if (isDateOnly) { kcalRecurrence->addRDate(dt.date()); } else { kcalRecurrence->addRDateTime(dt); } } } } else if (ruleStr.startsWith(QString::fromLatin1("exdate"), Qt::CaseInsensitive)) { bool isDateOnly = false; QList exdatetimes = datetimesFromExRDateStr(ruleStr, &isDateOnly); if (!exdatetimes.size()) { qCDebug(lcSocialPlugin) << "unable to parse EXDATE information:" << ruleStr; traceDumpStr(QString::fromUtf8(QJsonDocument(recurrence).toJson())); } else { Q_FOREACH (const QDateTime &dt, exdatetimes) { if (isDateOnly) { kcalRecurrence->addExDate(dt.date()); } else { kcalRecurrence->addExDateTime(dt); } } } } else { qCDebug(lcSocialPlugin) << "unknown recurrence information:" << ruleStr; traceDumpStr(QString::fromUtf8(QJsonDocument(recurrence).toJson())); } } } void extractOrganizer(const QJsonObject &creatorObj, const QJsonObject &organizerObj, KCalendarCore::Event::Ptr event) { if (!organizerObj.value(QLatin1String("displayName")).toVariant().toString().isEmpty() || !organizerObj.value(QLatin1String("email")).toVariant().toString().isEmpty()) { KCalendarCore::Person organizer( organizerObj.value(QLatin1String("displayName")).toVariant().toString(), organizerObj.value(QLatin1String("email")).toVariant().toString()); event->setOrganizer(organizer); } else if (!creatorObj.value(QLatin1String("displayName")).toVariant().toString().isEmpty() || !creatorObj.value(QLatin1String("email")).toVariant().toString().isEmpty()) { KCalendarCore::Person organizer( creatorObj.value(QLatin1String("displayName")).toVariant().toString(), creatorObj.value(QLatin1String("email")).toVariant().toString()); event->setOrganizer(organizer); } } void extractAttendees(const QJsonArray &attendees, KCalendarCore::Event::Ptr event) { event->clearAttendees(); for (int i = 0; i < attendees.size(); ++i) { QJsonObject attendeeObj = attendees.at(i).toObject(); KCalendarCore::Attendee attendee( attendeeObj.value(QLatin1String("displayName")).toVariant().toString(), attendeeObj.value(QLatin1String("email")).toVariant().toString()); if (attendeeObj.find(QLatin1String("optional")) != attendeeObj.end()) { if (attendeeObj.value(QLatin1String("optional")).toVariant().toBool()) { attendee.setRole(KCalendarCore::Attendee::OptParticipant); } else { attendee.setRole(KCalendarCore::Attendee::ReqParticipant); } } if (attendeeObj.find(QLatin1String("responseStatus")) != attendeeObj.end()) { const QString &responseValue = attendeeObj.value(QLatin1String("responseStatus")).toVariant().toString(); if (responseValue == "needsAction") { attendee.setStatus(KCalendarCore::Attendee::NeedsAction); } else if (responseValue == "accepted") { attendee.setStatus(KCalendarCore::Attendee::Accepted); } else if (responseValue == "declined") { attendee.setStatus(KCalendarCore::Attendee::Declined); } else { attendee.setStatus(KCalendarCore::Attendee::Tentative); } } attendee.setRSVP(true); event->addAttendee(attendee); } } #define START_EVENT_UPDATES_IF_REQUIRED(event, changed) \ if (*changed == false) { \ event->startUpdates(); \ } \ *changed = true; #define UPDATE_EVENT_PROPERTY_IF_REQUIRED(event, getter, setter, newValue, changed) \ if (event->getter() != newValue) { \ START_EVENT_UPDATES_IF_REQUIRED(event, changed) \ event->setter(newValue); \ } #define END_EVENT_UPDATES_IF_REQUIRED(event, changed, startedUpdates) \ if (*changed == false) { \ qCDebug(lcSocialPlugin) << "Ignoring spurious change reported for:" << \ event->uid() << event->revision() << event->summary(); \ } else if (startedUpdates) { \ event->endUpdates(); \ } void extractAlarms(const QJsonObject &json, KCalendarCore::Event::Ptr event, int defaultReminderStartOffset, bool *changed) { QSet startOffsets; if (json.contains(QStringLiteral("reminders"))) { QJsonObject reminders = json.value(QStringLiteral("reminders")).toObject(); if (reminders.value(QStringLiteral("useDefault")).toBool()) { if (defaultReminderStartOffset > 0) { startOffsets.insert(defaultReminderStartOffset); } else { qCDebug(lcSocialPlugin) << "not adding default reminder even though requested: not popup or invalid start offset."; } } else { QJsonArray overrides = reminders.value(QStringLiteral("overrides")).toArray(); for (int i = 0; i < overrides.size(); ++i) { QJsonObject override = overrides.at(i).toObject(); if (override.value(QStringLiteral("method")).toString() == QStringLiteral("popup")) { startOffsets.insert(override.value(QStringLiteral("minutes")).toInt()); } } } // search for all reminders to see if they are represented by an alarm. bool needRemoveAndRecreate = false; for (QSet::const_iterator it = startOffsets.constBegin(); it != startOffsets.constEnd(); it++) { const int startOffset = (*it) * -60; // convert minutes to seconds (before event) qCDebug(lcSocialPlugin) << "event needs reminder with start offset (seconds):" << startOffset; KCalendarCore::Alarm::List alarms = event->alarms(); int alarmsCount = 0; for (int i = 0; i < alarms.count(); ++i) { // we don't count Procedure type alarms. if (alarms.at(i)->type() != KCalendarCore::Alarm::Procedure) { alarmsCount += 1; if (alarms.at(i)->startOffset().asSeconds() == startOffset) { qCDebug(lcSocialPlugin) << "event already has reminder with start offset (seconds):" << startOffset; } else { qCDebug(lcSocialPlugin) << "event is missing reminder with start offset (seconds):" << startOffset; needRemoveAndRecreate = true; } } } if (alarmsCount != startOffsets.count()) { qCDebug(lcSocialPlugin) << "event has too many reminders, recreating alarms."; needRemoveAndRecreate = true; } } if (needRemoveAndRecreate) { START_EVENT_UPDATES_IF_REQUIRED(event, changed); KCalendarCore::Alarm::List alarms = event->alarms(); for (int i = 0; i < alarms.count(); ++i) { if (alarms.at(i)->type() != KCalendarCore::Alarm::Procedure) { event->removeAlarm(alarms.at(i)); } } for (QSet::const_iterator it = startOffsets.constBegin(); it != startOffsets.constEnd(); it++) { const int startOffset = (*it) * -60; // convert minutes to seconds (before event) qCDebug(lcSocialPlugin) << "setting event reminder with start offset (seconds):" << startOffset; KCalendarCore::Alarm::Ptr alarm = event->newAlarm(); alarm->setEnabled(true); alarm->setStartOffset(KCalendarCore::Duration(startOffset)); } } } if (startOffsets.isEmpty()) { // no reminders were defined in the json received from Google. // remove any alarms as required from the local event. KCalendarCore::Alarm::List alarms = event->alarms(); for (int i = 0; i < alarms.count(); ++i) { if (alarms.at(i)->type() != KCalendarCore::Alarm::Procedure) { qCDebug(lcSocialPlugin) << "removing event reminder with start offset (seconds):" << alarms.at(i)->startOffset().asSeconds(); START_EVENT_UPDATES_IF_REQUIRED(event, changed); event->removeAlarm(alarms.at(i)); } } } } void jsonToKCal(const QJsonObject &json, KCalendarCore::Event::Ptr event, int defaultReminderStartOffset, KCalendarCore::ICalFormat &icalFormat, bool *changed) { Q_ASSERT(!event.isNull()); bool alreadyStarted = *changed; // if this is true, we don't need to call startUpdates/endUpdates() in this function. const QString eventGCalETag(gCalETag(event)); const QString jsonGCalETag(json.value(QLatin1String("etag")).toVariant().toString()); if (!alreadyStarted && eventGCalETag == jsonGCalETag) { qCDebug(lcSocialPlugin) << "Ignoring non-remote-changed:" << event->uid() << "," << eventGCalETag << "==" << jsonGCalETag; return; // this event has not changed server-side since we last saw it. } QDateTime createdTimestamp, updatedTimestamp, start, end; bool startExists = false, endExists = false; bool startIsDateOnly = false, endIsDateOnly = false; bool isAllDay = false; extractCreatedAndUpdated(json, &createdTimestamp, &updatedTimestamp); extractStartAndEnd(json, &startExists, &endExists, &startIsDateOnly, &endIsDateOnly, &isAllDay, &start, &end); if (gCalEventId(event) != json.value(QLatin1String("id")).toVariant().toString()) { START_EVENT_UPDATES_IF_REQUIRED(event, changed); setGCalEventId(event, json.value(QLatin1String("id")).toVariant().toString()); } if (eventGCalETag != jsonGCalETag) { START_EVENT_UPDATES_IF_REQUIRED(event, changed); setGCalETag(event, jsonGCalETag); } setRemoteUidCustomField(event, json.value(QLatin1String("iCalUID")).toVariant().toString(), json.value(QLatin1String("id")).toVariant().toString()); extractOrganizer(json.value(QLatin1String("creator")).toObject(), json.value(QLatin1String("organizer")).toObject(), event); extractAttendees(json.value(QLatin1String("attendees")).toArray(), event); UPDATE_EVENT_PROPERTY_IF_REQUIRED(event, isReadOnly, setReadOnly, json.value(QLatin1String("locked")).toVariant().toBool(), changed) UPDATE_EVENT_PROPERTY_IF_REQUIRED(event, summary, setSummary, json.value(QLatin1String("summary")).toVariant().toString(), changed) UPDATE_EVENT_PROPERTY_IF_REQUIRED(event, description, setDescription, json.value(QLatin1String("description")).toVariant().toString(), changed) UPDATE_EVENT_PROPERTY_IF_REQUIRED(event, location, setLocation, json.value(QLatin1String("location")).toVariant().toString(), changed) UPDATE_EVENT_PROPERTY_IF_REQUIRED(event, revision, setRevision, json.value(QLatin1String("sequence")).toVariant().toInt(), changed) if (createdTimestamp.isValid()) { UPDATE_EVENT_PROPERTY_IF_REQUIRED(event, created, setCreated, createdTimestamp, changed) } if (updatedTimestamp.isValid()) { UPDATE_EVENT_PROPERTY_IF_REQUIRED(event, lastModified, setLastModified, updatedTimestamp, changed) } if (startExists) { UPDATE_EVENT_PROPERTY_IF_REQUIRED(event, dtStart, setDtStart, start, changed) } if (endExists) { if (!event->hasEndDate() || event->dtEnd() != end) { START_EVENT_UPDATES_IF_REQUIRED(event, changed); event->setDtEnd(end); } } // Recurrence rules use the event start time, so must be set after it extractRecurrence(json.value(QLatin1String("recurrence")).toArray(), event, icalFormat); if (isAllDay) { UPDATE_EVENT_PROPERTY_IF_REQUIRED(event, allDay, setAllDay, true, changed) } extractAlarms(json, event, defaultReminderStartOffset, changed); END_EVENT_UPDATES_IF_REQUIRED(event, changed, !alreadyStarted); } bool remoteModificationIsReal(const QJsonObject &json, KCalendarCore::Event::Ptr event) { if (gCalEventId(event) != json.value(QLatin1String("id")).toVariant().toString()) { return true; // this event is either a partial-upsync-artifact or a new remote addition. } if (gCalETag(event) != json.value(QLatin1String("etag")).toVariant().toString()) { return true; // this event has changed server-side since we last saw it. } return false; // this event has not changed server-side since we last saw it. } bool localModificationIsReal(const QJsonObject &local, const QJsonObject &remote, int defaultReminderStartOffset, KCalendarCore::ICalFormat &icalFormat) { bool changed = true; KCalendarCore::Event::Ptr localEvent = KCalendarCore::Event::Ptr(new KCalendarCore::Event); KCalendarCore::Event::Ptr remoteEvent = KCalendarCore::Event::Ptr(new KCalendarCore::Event); jsonToKCal(local, localEvent, defaultReminderStartOffset, icalFormat, &changed); jsonToKCal(remote, remoteEvent, defaultReminderStartOffset, icalFormat, &changed); if (GoogleCalendarIncidenceComparator::incidencesEqual(localEvent, remoteEvent)) { return false; // they're equal, so the local modification is not real. } return true; } // returns true if the last sync was marked as successful, and then marks the current // sync as being unsuccessful. The sync adapter should set it to true manually // once sync succeeds. bool wasLastSyncSuccessful(int accountId, bool *needCleanSync) { QString settingsFileName = QString::fromLatin1("%1/%2/gcal.ini") .arg(PRIVILEGED_DATA_DIR) .arg(QString::fromLatin1(SYNC_DATABASE_DIR)); if (!QFile::exists(settingsFileName)) { qCDebug(lcSocialPlugin) << "gcal.ini settings file does not exist, triggering clean sync"; *needCleanSync = true; return false; } QSettings settingsFile(settingsFileName, QSettings::IniFormat); // needCleanSync will be true if and only if an unrecoverable error occurred during the previous sync. *needCleanSync = settingsFile.value(QString::fromLatin1("%1-needCleanSync").arg(accountId), QVariant::fromValue(false)).toBool(); bool retn = settingsFile.value(QString::fromLatin1("%1-success").arg(accountId), QVariant::fromValue(false)).toBool(); settingsFile.setValue(QString::fromLatin1("%1-success").arg(accountId), QVariant::fromValue(false)); int pluginVersion = settingsFile.value(QString::fromLatin1("%1-pluginVersion").arg(accountId), QVariant::fromValue(GOOGLE_CAL_SYNC_PLUGIN_VERSION)).toInt(); if (pluginVersion != GOOGLE_CAL_SYNC_PLUGIN_VERSION) { qCDebug(lcSocialPlugin) << "Google cal sync plugin version mismatch, force clean sync"; retn = false; } return retn; } void setLastSyncSuccessful(int accountId) { QString settingsFileName = QString::fromLatin1("%1/%2/gcal.ini") .arg(PRIVILEGED_DATA_DIR) .arg(QString::fromLatin1(SYNC_DATABASE_DIR)); QSettings settingsFile(settingsFileName, QSettings::IniFormat); settingsFile.setValue(QString::fromLatin1("%1-needCleanSync").arg(accountId), QVariant::fromValue(false)); settingsFile.setValue(QString::fromLatin1("%1-success").arg(accountId), QVariant::fromValue(true)); settingsFile.setValue(QString::fromLatin1("%1-pluginVersion").arg(accountId), GOOGLE_CAL_SYNC_PLUGIN_VERSION); } // Move all items with a recurrenceId after those without // Retain the same order within the two groups void reorderAdditions(KCalendarCore::Incidence::List &addedList) { // This is a QVector so we want to avoid expensive insertions/deletions int nextSwap = 0; for (int pos = 0; pos < addedList.size() - 1; ++pos) { const KCalendarCore::Incidence::Ptr from = addedList[pos]; if (from->hasRecurrenceId()) { // Need to swap if (nextSwap <= pos) { nextSwap = pos + 1; } // Find the next sensible swap position while (nextSwap < addedList.size() - 1 && addedList[nextSwap]->hasRecurrenceId()) { ++nextSwap; } // Swap the item positions addedList[pos] = addedList[nextSwap]; addedList[nextSwap] = from; } } } QString toBase32hex(QByteArray bytes) { // See RFC2938 section 3.1.2 static const char convert[] = "0123456789abcdefghijklmnopqrstuv"; int const length = bytes.length(); QString result; // pos represents the bit position, taking 5 at a time for (int pos = 0; pos < length * 8; pos += 5) { quint8 const startbyte = pos / 8; quint8 const underflowstart = pos % 8; quint8 const underflowbits = underflowstart > 3 ? 8 - underflowstart : 5; quint8 const overflowend = 5 - underflowbits; quint8 const underflowmask = ((2 << (underflowbits - 1)) - 1) << underflowstart; int val = (bytes[startbyte] & underflowmask) >> underflowstart; if ((overflowend > 0) && (startbyte < (length - 1))) { quint8 const overflowmask = ((2 << (overflowend - 1)) - 1); val += (bytes[startbyte + 1] & overflowmask) << underflowbits; } Q_ASSERT(val < 32); result += convert[val]; } return result; } QString percentEnc(const QString &str) { return QString::fromUtf8(QUrl::toPercentEncoding(str)); } QString generate_uuid() { // UUID documentation here: // https://developers.google.com/calendar/v3/reference/events#id // Generate using RFC4122 return toBase32hex(QUuid::createUuid().toRfc4122()); } QByteArray jsonReplaceValue(const QByteArray &json, const QString &key, const QJsonValue &value) { // This is expensive, so should be used sparingly QJsonObject object = QJsonDocument::fromJson(json).object(); object.insert(key, value); return QJsonDocument(object).toJson(); } struct ErrorDetails { QString reason; QString message; }; QString getErrorReason(const QByteArray &replyData) { QString reason; QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(replyData, &error); if (error.error == QJsonParseError::NoError) { QJsonObject object = doc.object().value(QStringLiteral("error")).toObject();; reason = object.value(QStringLiteral("errors")).toArray().first().toObject().value(QStringLiteral("reason")).toString(); } else { qCDebug(lcSocialPlugin) << "Json parse error:" << error.errorString(); } return reason; } KCalendarCore::Event::Ptr dissociateSingleOccurrence(const KCalendarCore::Event::Ptr &event, const QDateTime &dateTime) { // Create a new incidence KCalendarCore::Event::Ptr newEvent = KCalendarCore::Event::Ptr(event->clone()); newEvent->setCreated(QDateTime::currentDateTimeUtc()); newEvent->setSchedulingID(QString()); if (newEvent->recurs()) { newEvent->clearRecurrence(); } // Add a recurrence rule to the parent if it needs it if (event->allDay()) { if (!event->recursOn(dateTime.date(), dateTime.timeZone())) { event->recurrence()->addRDate(dateTime.date()); } } else { if (!event->recursAt(dateTime)) { event->recurrence()->addRDateTime(dateTime); } } // Set the recurrenceId for the new incidence // Don't save milliseconds QDateTime recId(dateTime); recId.setTime(QTime(recId.time().hour(), recId.time().minute(), recId.time().second())); newEvent->setRecurrenceId(recId); return newEvent; } } GoogleCalendarSyncAdaptor::GoogleCalendarSyncAdaptor(QObject *parent) : GoogleDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::Calendars, parent) , m_syncSucceeded(false) , m_accountId(0) , m_calendar(mKCal::ExtendedCalendar::Ptr(new mKCal::ExtendedCalendar(QTimeZone::utc()))) , m_storage(mKCal::ExtendedCalendar::defaultStorage(m_calendar)) , m_storageNeedsSave(false) { m_calendar->setUpdateLastModifiedOnChange(false); setInitialActive(true); } GoogleCalendarSyncAdaptor::~GoogleCalendarSyncAdaptor() { } QString GoogleCalendarSyncAdaptor::syncServiceName() const { return QStringLiteral("google-calendars"); } void GoogleCalendarSyncAdaptor::sync(const QString &dataTypeString, int accountId) { m_storage->open(); // we close it in finalCleanup() m_accountId = accountId; // needed by finalCleanup() GoogleDataTypeSyncAdaptor::sync(dataTypeString, accountId); } void GoogleCalendarSyncAdaptor::finalCleanup() { if (m_accountId != 0) { if (!m_syncSucceeded) { qCDebug(lcSocialPluginTrace) << "Sync failed, configuring for next sync"; // sync failed. check to see if we need to apply any changes to the database. const QSet calendarsRequiringChange = m_timeMinFailure + m_syncTokenFailure; const QDateTime yesterdayDate = QDateTime::currentDateTimeUtc().addDays(-1); for (const QString &calendarId : calendarsRequiringChange) { // this codepath is hit if the server replied with HTTP 410 for the sync token or timeMin value. if (mKCal::Notebook::Ptr notebook = notebookForCalendarId(calendarId)) { if (m_syncTokenFailure.contains(calendarId)) { // this sync cycle failed due to the sync token being invalidated server-side. // trigger clean sync with wide time span on next sync. qCInfo(lcSocialPlugin) << "Clearing sync time for calendar:" << calendarId; notebook->setSyncDate(QDateTime()); } else if (m_timeMinFailure.contains(calendarId)) { // this sync cycle failed due to the timeMin value being too far in the past. // trigger clean sync with short time span on next sync. qCInfo(lcSocialPlugin) << "Setting sync time to yesterday for calendar:" << calendarId; notebook->setSyncDate(yesterdayDate); } notebook->setCustomProperty(NOTEBOOK_SERVER_SYNC_TOKEN_PROPERTY, QString()); m_storage->updateNotebook(notebook); // Notebook operations are immediate so no need to amend m_storageNeedsSave } } } else { // sync succeeded. apply the changes to the database. applyRemoteChangesLocally(); if (!m_syncSucceeded) { qCInfo(lcSocialPlugin) << "Error occurred while applying remote changes locally"; } else { Q_FOREACH (const QString &updatedCalendarId, m_calendarsFinishedRequested) { // Update the sync date for the notebook, to the timestamp reported by Google // in the calendar request for the remote calendar associated with the notebook, // if that timestamp is recent (within the last week). If it is older than that, // update it to the current date minus one day, otherwise Google will return // 410 GONE "UpdatedMin too old" error on subsequent requests. mKCal::Notebook::Ptr notebook = notebookForCalendarId(updatedCalendarId); if (!notebook) { // may have been deleted due to a purge operation. continue; } // Google doesn't use the sync date (synchronisation is handled by the token), it's // only used by us to figure out what has changed since this sync, using either the // lastModified or dateDeleted, both of which are set based on the client's time. We // should therefore set the local synchronisation date to the client's time too. // The "modified by" test inequality is inclusive, so changes from the sync have // timestamp clamped to a second before the sync time using clampEventTimeToSync(). qCDebug(lcSocialPlugin) << "Latest sync date set to: " << m_syncedDateTime.toString(); notebook->setSyncDate(m_syncedDateTime); // also update the remote sync token in each notebook. notebook->setCustomProperty(NOTEBOOK_SERVER_SYNC_TOKEN_PROPERTY, m_calendarsNextSyncTokens.value(updatedCalendarId)); m_storage->updateNotebook(notebook); // Notebook operations are immediate so no need to amend m_storageNeedsSave } } } // Flag any errors applySyncFailureFlags(); } qCDebug(lcSocialPlugin) << "Saving:" << m_storageNeedsSave; if (m_storageNeedsSave) { m_storage->save(mKCal::ExtendedStorage::PurgeDeleted); } m_storageNeedsSave = false; if (!m_purgeList.isEmpty()) { for (QMap::ConstIterator it = m_purgeList.constBegin(); it != m_purgeList.constEnd(); it++) { if (!m_storage->purgeDeletedIncidences(it.value(), it.key())) { // Silently ignore failed purge action in database. qCWarning(lcSocialPlugin) << "Cannot purge from database the marked as deleted incidences."; } } } // set the success status for each of our account settings. if (m_syncSucceeded) { setLastSyncSuccessful(m_accountId); } if (!ghostEventCleanupPerformed()) { // Delete any events which are not associated with a notebook. // These events are ghost events, caused by a bug which previously // existed in the sync adapter code due to mkcal deleteNotebook semantics. // The mkcal API doesn't allow us to determine which notebook a // given incidence belongs to, so we have to instead load // everything and then find the ones which are ophaned. // Note: we do this separately / after the commit above, because // loading all events from the database is expensive. qCInfo(lcSocialPlugin) << "performing ghost event cleanup"; m_storage->load(); KCalendarCore::Incidence::List allIncidences; m_storage->allIncidences(&allIncidences); mKCal::Notebook::List allNotebooks = m_storage->notebooks(); QSet notebookIncidenceUids; foreach (mKCal::Notebook::Ptr notebook, allNotebooks) { KCalendarCore::Incidence::List currNbIncidences; m_storage->allIncidences(&currNbIncidences, notebook->uid()); foreach (KCalendarCore::Incidence::Ptr incidence, currNbIncidences) { notebookIncidenceUids.insert(incidence->uid()); } } int foundOrphans = 0; foreach (const KCalendarCore::Incidence::Ptr incidence, allIncidences) { if (!notebookIncidenceUids.contains(incidence->uid())) { // orphan/ghost incidence. must be deleted. qCDebug(lcSocialPlugin) << "deleting local orphan event with uid:" << incidence->uid(); m_calendar->deleteIncidence(m_calendar->incidence(incidence->uid(), incidence->recurrenceId())); foundOrphans++; } } if (foundOrphans == 0) { setGhostEventCleanupPerformed(); qCInfo(lcSocialPlugin) << "orphan cleanup completed without finding orphans!"; } else if (m_storage->save(mKCal::ExtendedStorage::PurgeDeleted)) { setGhostEventCleanupPerformed(); qCInfo(lcSocialPlugin) << "orphan cleanup deleted" << foundOrphans << "; storage save completed!"; } else { qCWarning(lcSocialPlugin) << "orphan cleanup found" << foundOrphans << "; but storage save failed!"; } } m_storage->close(); qCInfo(lcSocialPlugin) << "Sync completed"; } void GoogleCalendarSyncAdaptor::purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode mode) { if (mode == SocialNetworkSyncAdaptor::CleanUpPurge) { // need to initialise the database m_storage->open(); // we close it in finalCleanup() } // We clean all the entries in the calendar // Delete the notebooks from the storage foreach (mKCal::Notebook::Ptr notebook, m_storage->notebooks()) { if (notebook->pluginName().startsWith(QStringLiteral("google")) && notebook->account() == QString::number(oldId)) { // remove the incidences and delete the notebook m_storage->deleteNotebook(notebook); } } if (mode == SocialNetworkSyncAdaptor::CleanUpPurge) { // and commit any changes made. finalCleanup(); } } void GoogleCalendarSyncAdaptor::beginSync(int accountId, const QString &accessToken) { qCDebug(lcSocialPlugin) << "beginning Calendar sync for Google, account" << accountId; Q_ASSERT(accountId == m_accountId); bool needCleanSync = false; bool lastSyncSuccessful = wasLastSyncSuccessful(accountId, &needCleanSync); if (needCleanSync) { qCInfo(lcSocialPlugin) << "performing clean sync"; } else if (!lastSyncSuccessful) { qCInfo(lcSocialPlugin) << "last sync was not successful, attempting to recover without clean sync"; } m_serverCalendarIdToCalendarInfo.clear(); m_calendarIdToEventObjects.clear(); m_purgeList.clear(); m_deletedGcalIdToIncidence.clear(); m_sequenced.clear(); m_eventSyncFlags.clear(); m_syncSucceeded = true; // set to false on error m_syncedDateTime = QDateTime::currentDateTimeUtc(); m_collisionErrorCount = 0; requestCalendars(accessToken, needCleanSync); } void GoogleCalendarSyncAdaptor::requestCalendars(const QString &accessToken, bool needCleanSync, const QString &pageToken) { QList > queryItems; if (!pageToken.isEmpty()) { // continuation request queryItems.append(QPair(QString::fromLatin1("pageToken"), pageToken)); } QUrl url(QLatin1String("https://www.googleapis.com/calendar/v3/users/me/calendarList")); QUrlQuery query(url); query.setQueryItems(queryItems); url.setQuery(query); QNetworkRequest request(url); request.setRawHeader("GData-Version", "3.0"); request.setRawHeader(QString(QLatin1String("Authorization")).toUtf8(), QString(QLatin1String("Bearer ") + accessToken).toUtf8()); QNetworkReply *reply = m_networkAccessManager->get(request); // we're requesting data. Increment the semaphore so that we know we're still busy. incrementSemaphore(m_accountId); if (reply) { reply->setProperty("accountId", m_accountId); reply->setProperty("accessToken", accessToken); reply->setProperty("needCleanSync", QVariant::fromValue(needCleanSync)); connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(errorHandler(QNetworkReply::NetworkError))); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsHandler(QList))); connect(reply, SIGNAL(finished()), this, SLOT(calendarsFinishedHandler())); setupReplyTimeout(m_accountId, reply); } else { qCWarning(lcSocialPlugin) << "unable to request calendars from Google account with id" << m_accountId; m_syncSucceeded = false; decrementSemaphore(m_accountId); } } void GoogleCalendarSyncAdaptor::calendarsFinishedHandler() { QNetworkReply *reply = qobject_cast(sender()); Q_ASSERT(reply->property("accountId").toInt() == m_accountId); QString accessToken = reply->property("accessToken").toString(); bool needCleanSync = reply->property("needCleanSync").toBool(); QByteArray replyData = reply->readAll(); bool isError = reply->property("isError").toBool(); disconnect(reply); reply->deleteLater(); removeReplyTimeout(m_accountId, reply); // parse the calendars' metadata from the response. bool fetchingNextPage = false; bool ok = false; QJsonObject parsed = parseJsonObjectReplyData(replyData, &ok); if (!isError && ok) { // first, check to see if there are more pages of calendars to fetch if (parsed.find(QLatin1String("nextPageToken")) != parsed.end() && !parsed.value(QLatin1String("nextPageToken")).toVariant().toString().isEmpty()) { fetchingNextPage = true; requestCalendars(accessToken, needCleanSync, parsed.value(QLatin1String("nextPageToken")).toVariant().toString()); } // second, parse the calendars' metadata QJsonArray items = parsed.value(QStringLiteral("items")).toArray(); for (int i = 0; i < items.count(); ++i) { QJsonObject currCalendar = items.at(i).toObject(); if (!currCalendar.isEmpty() && currCalendar.find(QStringLiteral("id")) != currCalendar.end()) { QString accessRole = currCalendar.value(QStringLiteral("accessRole")).toString(); AccessRole access = NoAccess; if (accessRole == QStringLiteral("owner")) { access = Owner; } else if (accessRole == QStringLiteral("writer")) { access = Writer; } else if (accessRole == QStringLiteral("reader")) { access = Reader; } else if (accessRole == QStringLiteral("freeBusyReader")) { access = FreeBusyReader; } if (access != NoAccess) { GoogleCalendarSyncAdaptor::CalendarInfo currCalendarInfo; currCalendarInfo.color = currCalendar.value(QStringLiteral("backgroundColor")).toString(); currCalendarInfo.summary = currCalendar.value(QStringLiteral("summary")).toString(); currCalendarInfo.description = currCalendar.value(QStringLiteral("description")).toString(); currCalendarInfo.change = NoChange; // we detect the appropriate change type (if required) later. currCalendarInfo.access = access; QString currCalendarId = currCalendar.value(QStringLiteral("id")).toString(); m_serverCalendarIdToCalendarInfo.insert(currCalendarId, currCalendarInfo); } } } } else { // error occurred during request. qCWarning(lcSocialPlugin) << "unable to parse calendar data from request with account" << m_accountId << "; got:"; errorDumpStr(QString::fromLatin1(replyData.constData())); m_syncSucceeded = false; } if (!fetchingNextPage) { // we've finished loading all pages of calendar information // we now need to process the loaded information to determine // which calendars need to be added/updated/removed locally. updateLocalCalendarNotebooks(accessToken, needCleanSync); } // we're finished with this request. decrementSemaphore(m_accountId); } void GoogleCalendarSyncAdaptor::updateLocalCalendarNotebooks(const QString &accessToken, bool needCleanSync) { if (syncAborted()) { qCDebug(lcSocialPlugin) << "sync aborted, skipping updating local calendar notebooks"; return; } QMap &calendars = m_serverCalendarIdToCalendarInfo; QMap serverCalendarIdToSyncToken; // any calendars which exist on the device but not the server need to be purged. QStringList calendarsToDelete; QStringList deviceCalendarIds; foreach (mKCal::Notebook::Ptr notebook, m_storage->notebooks()) { if (notebook->pluginName().startsWith(QStringLiteral("google")) && notebook->account() == QString::number(m_accountId)) { // back compat: notebook pluginName used to be of form: google-calendarId const QString currDeviceCalendarId = notebook->pluginName().startsWith(QStringLiteral("google-")) ? notebook->pluginName().mid(7) : notebook->customProperty(NOTEBOOK_SERVER_ID_PROPERTY); if (calendars.contains(currDeviceCalendarId)) { // the server-side calendar exists on the device. const QString notebookNextSyncToken = notebook->customProperty(NOTEBOOK_SERVER_SYNC_TOKEN_PROPERTY); if (!notebookNextSyncToken.isEmpty()) { serverCalendarIdToSyncToken.insert(currDeviceCalendarId, notebookNextSyncToken); } // check to see if we need to perform a clean sync cycle with this notebook. // if the sync token is empty and the syncTime is invalid, perform a clean sync anyway const bool effectiveCleanSync = (notebookNextSyncToken.isEmpty() && (!notebook->syncDate().isValid() || notebook->syncDate().toMSecsSinceEpoch() == 0)); if (needCleanSync || effectiveCleanSync) { // we are performing a clean sync cycle. // we will eventually delete and then insert this notebook. qCDebug(lcSocialPlugin) << "queueing clean sync of local calendar" << notebook->name() << currDeviceCalendarId << "for Google account:" << m_accountId; deviceCalendarIds.append(currDeviceCalendarId); calendars[currDeviceCalendarId].change = GoogleCalendarSyncAdaptor::CleanSync; } else { // we don't need to purge it, but we may need to update its summary/color details. deviceCalendarIds.append(currDeviceCalendarId); if (notebook->name() != calendars.value(currDeviceCalendarId).summary || notebook->color() != calendars.value(currDeviceCalendarId).color || notebook->description() != calendars.value(currDeviceCalendarId).description || notebook->sharedWith() != QStringList(currDeviceCalendarId) || notebook->isReadOnly()) { // calendar information changed server-side. qCDebug(lcSocialPlugin) << "queueing modification of local calendar" << notebook->name() << currDeviceCalendarId << "for Google account:" << m_accountId; calendars[currDeviceCalendarId].change = GoogleCalendarSyncAdaptor::Modify; } else { // the calendar information is unchanged server-side. // no need to change anything locally. qCDebug(lcSocialPlugin) << "No modification required for local calendar" << notebook->name() << currDeviceCalendarId << "for Google account:" << m_accountId; calendars[currDeviceCalendarId].change = GoogleCalendarSyncAdaptor::NoChange; } } } else { // the calendar has been removed from the server. // we need to purge it from the device. qCDebug(lcSocialPlugin) << "queueing removal of local calendar" << notebook->name() << currDeviceCalendarId << "for Google account:" << m_accountId; calendarsToDelete.append(currDeviceCalendarId); } } } // any calendarIds which exist on the server but not the device need to be created. foreach (const QString &serverCalendarId, calendars.keys()) { if (!deviceCalendarIds.contains(serverCalendarId)) { qCDebug(lcSocialPlugin) << "queueing addition of local calendar" << serverCalendarId << calendars.value(serverCalendarId).summary << "for Google account:" << m_accountId; calendars[serverCalendarId].change = GoogleCalendarSyncAdaptor::Insert; } } qCDebug(lcSocialPlugin) << "Syncing calendar events for Google account: " << m_accountId << " CleanSync: " << needCleanSync; foreach (const QString &calendarId, calendars.keys()) { const QString syncToken = isCleanSync(calendarId) ? QString() : serverCalendarIdToSyncToken.value(calendarId); requestEvents(accessToken, calendarId, syncToken); m_calendarsBeingRequested.append(calendarId); } // now we can queue the calendars which need deletion. // note: we have to do it after the previous foreach loop, otherwise we'd attempt to retrieve events for them. foreach (const QString &currDeviceCalendarId, calendarsToDelete) { calendars[currDeviceCalendarId].change = GoogleCalendarSyncAdaptor::Delete; } } void GoogleCalendarSyncAdaptor::requestEvents(const QString &accessToken, const QString &calendarId, const QString &syncToken, const QString &pageToken) { mKCal::Notebook::Ptr notebook = notebookForCalendarId(calendarId); // get the last sync date stored into the notebook (if it exists). // we need to perform a "clean" sync if we don't have a valid sync date // or if we don't have a valid syncToken. // The mKCal API doesn't provide a way to get all deleted/modified incidences // for a notebook, as it implements the SQL query using an inequality on both modifiedAfter // and createdBefore; so instead we have to build a datetime which "should" satisfy // the inequality for all possible local modifications detectable since the last sync. QDateTime syncDate = notebook ? notebook->syncDate().addSecs(1) : QDateTime(); const bool needCleanSync = isCleanSync(calendarId); if (!needCleanSync) { qCDebug(lcSocialPlugin) << "Previous sync time for Google account:" << m_accountId << "Calendar Id:" << calendarId << "- Times:" << syncDate.toString() << "- SyncToken:" << syncToken; } else if (syncDate.isValid() && syncToken.isEmpty()) { qCDebug(lcSocialPlugin) << "Clean sync required for Google account:" << m_accountId << "Calendar Id:" << calendarId << "- Ignoring last sync time:" << syncDate.toString(); syncDate = QDateTime(); } else { qCDebug(lcSocialPlugin) << "Invalid previous sync time for Google account:" << m_accountId << "Calendar Id:" << calendarId << "- Time:" << syncDate.toString() << "- SyncToken:" << syncToken; } QList > queryItems; // we don't care about focusTime, outOfOffice or workingLocation queryItems.append(QPair(QStringLiteral("eventTypes"), QStringLiteral("default"))); if (!needCleanSync) { // delta update request queryItems.append(QPair(QString::fromLatin1("syncToken"), syncToken)); } else { // clean sync request // Note: if the syncDate is valid, that should be because we previously // suffered from a 410 error due to the timeMin value being too long ago, // and we detected that case and wrote the next sync date value to use here. const QDateTime clampMin = QDateTime::currentDateTimeUtc().addYears(-1); const QDateTime timeMax = QDateTime::currentDateTimeUtc().addYears(2); syncDate = (!syncDate.isValid() || (syncDate < clampMin)) ? clampMin : syncDate; queryItems.append(QPair(QString::fromLatin1("timeMin"), syncDate.toString(Qt::ISODate))); queryItems.append(QPair(QString::fromLatin1("timeMax"), timeMax.toString(Qt::ISODate))); } if (!pageToken.isEmpty()) { // continuation request queryItems.append(QPair(QString::fromLatin1("pageToken"), pageToken)); } QUrl url(QString::fromLatin1("https://www.googleapis.com/calendar/v3/calendars/%1/events").arg(percentEnc(calendarId))); QUrlQuery query(url); query.setQueryItems(queryItems); url.setQuery(query); QNetworkRequest request(url); request.setRawHeader("GData-Version", "3.0"); request.setRawHeader(QString(QLatin1String("Authorization")).toUtf8(), QString(QLatin1String("Bearer ") + accessToken).toUtf8()); QNetworkReply *reply = m_networkAccessManager->get(request); // we're requesting data. Increment the semaphore so that we know we're still busy. incrementSemaphore(m_accountId); if (reply) { reply->setProperty("accountId", m_accountId); reply->setProperty("accessToken", accessToken); reply->setProperty("calendarId", calendarId); reply->setProperty("syncToken", needCleanSync ? QString() : syncToken); reply->setProperty("since", syncDate); connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(errorHandler(QNetworkReply::NetworkError))); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsHandler(QList))); connect(reply, SIGNAL(finished()), this, SLOT(eventsFinishedHandler())); qCDebug(lcSocialPlugin) << "requesting calendar events for Google account:" << m_accountId << ":" << url.toString(); setupReplyTimeout(m_accountId, reply); } else { qCWarning(lcSocialPlugin) << "unable to request events for calendar" << calendarId << "from Google account with id" << m_accountId; m_syncSucceeded = false; decrementSemaphore(m_accountId); } } void GoogleCalendarSyncAdaptor::eventsFinishedHandler() { QNetworkReply *reply = qobject_cast(sender()); Q_ASSERT(reply->property("accountId").toInt() == m_accountId); QString calendarId = reply->property("calendarId").toString(); QString accessToken = reply->property("accessToken").toString(); QString syncToken = reply->property("syncToken").toString(); QDateTime since = reply->property("since").toDateTime(); QByteArray replyData = reply->readAll(); bool isError = reply->property("isError").toBool(); int httpCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); QString replyString = QString::fromUtf8(replyData); qCDebug(lcSocialPluginTrace) << "-------------------------------"; qCDebug(lcSocialPluginTrace) << "Events response for calendar:" << calendarId << "from account:" << m_accountId; qCDebug(lcSocialPluginTrace) << "HTTP CODE:" << httpCode; Q_FOREACH (QString line, replyString.split('\n', QString::SkipEmptyParts)) { qCDebug(lcSocialPluginTrace) << line.replace('\r', ' '); } qCDebug(lcSocialPluginTrace) << "-------------------------------"; disconnect(reply); reply->deleteLater(); removeReplyTimeout(m_accountId, reply); bool fetchingNextPage = false; bool ok = false; QString nextSyncToken; const QJsonObject parsed = parseJsonObjectReplyData(replyData, &ok); if (!isError && ok) { // If there are more pages of results to fetch, ensure we fetch them if (parsed.find(QLatin1String("nextPageToken")) != parsed.end() && !parsed.value(QLatin1String("nextPageToken")).toVariant().toString().isEmpty()) { fetchingNextPage = true; requestEvents(accessToken, calendarId, syncToken, parsed.value(QLatin1String("nextPageToken")).toVariant().toString()); } // Otherwise, if we get a new sync token, ensure we store that for next sync nextSyncToken = parsed.value(QLatin1String("nextSyncToken")).toVariant().toString(); // parse the default reminders data to find the default popup reminder start offset. if (parsed.find(QStringLiteral("defaultReminders")) != parsed.end()) { const QJsonArray defaultReminders = parsed.value(QStringLiteral("defaultReminders")).toArray(); for (int i = 0; i < defaultReminders.size(); ++i) { QJsonObject defaultReminder = defaultReminders.at(i).toObject(); if (defaultReminder.value(QStringLiteral("method")).toString() == QStringLiteral("popup")) { m_serverCalendarIdToDefaultReminderTimes[calendarId] = defaultReminder.value(QStringLiteral("minutes")).toInt(); } } } // Parse the event list const QJsonArray dataList = parsed.value(QLatin1String("items")).toArray(); foreach (const QJsonValue &item, dataList) { QJsonObject eventData = item.toObject(); // otherwise, we queue the event for insertion into the database. m_calendarIdToEventObjects.insertMulti(calendarId, eventData); } } else { // error occurred during request. if (httpCode == 410) { // HTTP 410 GONE is emitted if the syncToken or timeMin parameters are invalid. // We should trigger a clean sync with this notebook if we hit this error. const QString reason = getErrorReason(replyData); qCWarning(lcSocialPlugin) << "received 410 GONE" << reason << "from server; marking calendar" << calendarId << "from account" << m_accountId << "for clean sync"; nextSyncToken.clear(); if (reason == ERROR_REASON_UPDATE_MIN_TOO_OLD) { m_timeMinFailure.insert(calendarId); } else { m_syncTokenFailure.insert(calendarId); } } else { qCWarning(lcSocialPlugin) << "unable to parse event data from request with account" << m_accountId << "; got:"; errorDumpStr(QString::fromUtf8(replyData.constData())); } m_syncSucceeded = false; } if (!fetchingNextPage) { // we've finished loading all pages of event information // we now need to process the loaded information to determine // which events need to be added/updated/removed locally. finishedRequestingRemoteEvents(accessToken, calendarId, syncToken, nextSyncToken, since); } // we're finished this request. Decrement our busy semaphore. decrementSemaphore(m_accountId); } mKCal::Notebook::Ptr GoogleCalendarSyncAdaptor::notebookForCalendarId(const QString &calendarId) const { foreach (mKCal::Notebook::Ptr notebook, m_storage->notebooks()) { if (notebook->account() == QString::number(m_accountId) && (notebook->customProperty(NOTEBOOK_SERVER_ID_PROPERTY) == calendarId // for backward compatibility with old accounts / notebooks: || notebook->pluginName() == QString::fromLatin1("google-%1").arg(calendarId))) { return notebook; } } return mKCal::Notebook::Ptr(); } void GoogleCalendarSyncAdaptor::finishedRequestingRemoteEvents(const QString &accessToken, const QString &calendarId, const QString &syncToken, const QString &nextSyncToken, const QDateTime &since) { m_calendarsBeingRequested.removeAll(calendarId); m_calendarsFinishedRequested.append(calendarId); m_calendarsThisSyncTokens.insert(calendarId, syncToken); m_calendarsNextSyncTokens.insert(calendarId, nextSyncToken); m_calendarsSyncDate.insert(calendarId, since); if (!m_calendarsBeingRequested.isEmpty()) { return; // still waiting for more requests to finish. } if (syncAborted() || !m_syncSucceeded) { return; // sync was aborted or failed before we received all remote data, and before we could upsync local changes. } // We're about to perform the delta, so record the time to use for the next sync m_syncedDateTime = QDateTime::currentDateTimeUtc(); // determine local changes to upsync. Q_FOREACH (const QString &finishedCalendarId, m_calendarsFinishedRequested) { // now upsync the local changes to the remote server QList changesToUpsync = determineSyncDelta(accessToken, finishedCalendarId, m_calendarsSyncDate.value(finishedCalendarId)); if (changesToUpsync.size()) { if (syncAborted()) { qCDebug(lcSocialPlugin) << "skipping upsync of queued upsync changes due to sync being aborted"; } else if (m_syncSucceeded == false) { qCDebug(lcSocialPlugin) << "skipping upsync of queued upsync changes due to previous error during sync"; } else { qCDebug(lcSocialPlugin) << "upsyncing" << changesToUpsync.size() << "local changes to the remote server"; for (int i = 0; i < changesToUpsync.size(); ++i) { upsyncChanges(changesToUpsync[i]); } } } else { // no local changes to upsync. // we can apply the remote changes and we are finished. } } } // Return a list of all dates in the recurrence pattern that have an exception event associated with them // Events must be loaded into memeory first before calling this method const QList GoogleCalendarSyncAdaptor::getExceptionInstanceDates(const KCalendarCore::Event::Ptr event) const { QList exceptions; // Get all the instances of the event KCalendarCore::Incidence::List instances = m_calendar->instances(event); for (const KCalendarCore::Incidence::Ptr &incidence : instances) { if (incidence->hasRecurrenceId()) { // Record its recurrence Id exceptions += incidence->recurrenceId(); } } return exceptions; } // Determine the sync delta, and then cache the required downsynced changes and return the required changes to upsync. QList GoogleCalendarSyncAdaptor::determineSyncDelta(const QString &accessToken, const QString &calendarId, const QDateTime &since) { Q_UNUSED(accessToken) // in the future, we might need it to download images/data associated with the event. QList changesToUpsync; // Search for the device Notebook matching this CalendarId. // Only upsync changes if we're doing a delta sync, and upsync is enabled. bool upsyncEnabled = true; mKCal::Notebook::Ptr googleNotebook = notebookForCalendarId(calendarId); if (googleNotebook.isNull()) { // this is a new, never before seen calendar. qCInfo(lcSocialPlugin) << "No local calendar exists for:" << calendarId << "for account:" << m_accountId << ". No upsync possible."; upsyncEnabled = false; } else if (!m_accountSyncProfile || m_accountSyncProfile->syncDirection() == Buteo::SyncProfile::SYNC_DIRECTION_FROM_REMOTE) { qCInfo(lcSocialPlugin) << "skipping upload of local calendar changes to" << calendarId << "due to profile direction setting for account" << m_accountId; upsyncEnabled = false; } else if (!since.isValid()) { qCInfo(lcSocialPlugin) << "Delta upsync with Google calendar" << calendarId << "for account" << m_accountId << "not required due to clean sync"; upsyncEnabled = false; } else { qCInfo(lcSocialPlugin) << "Delta upsync with Google calendar" << calendarId << "for account" << m_accountId << "is enabled."; upsyncEnabled = true; } // re-order the list of remote events so that base recurring events will precede occurrences. QList eventObjects; foreach (const QJsonObject &eventData, m_calendarIdToEventObjects.values(calendarId)) { if (eventData.value(QLatin1String("recurringEventId")).toVariant().toString().isEmpty()) { // base event; prepend to list. eventObjects.prepend(eventData); } else { // occurrence; append to list. eventObjects.append(eventData); } } // parse that list to look for partial-upsync-artifacts. // if we upsynced some local addition, and then lost connectivity, // the remote server-side will report that as a remote addition upon // the next sync - but really it isn't. QHash upsyncedUidMapping; QSet partialUpsyncArtifactsNeedingUpdate; // set of gcalIds foreach (const QJsonObject &eventData, eventObjects) { const QString eventId = eventData.value(QLatin1String("id")).toVariant().toString(); const QString upsyncedUid = eventData.value(QLatin1String("extendedProperties")).toObject() .value(QLatin1String("private")).toObject() .value(QLatin1String("x-jolla-sociald-mkcal-uid")).toVariant().toString(); if (!upsyncedUid.isEmpty() && !eventId.isEmpty()) { upsyncedUidMapping.insert(upsyncedUid, eventId); } } // load local events from the database. KCalendarCore::Incidence::List deletedList, extraDeletedList, addedList, updatedList, allList; QMap allMap, updatedMap; QMap > deletedMap; // gcalId to incidenceUid,recurrenceId QSet cleanSyncDeletionAdditions; // gcalIds if (!isCleanSync(calendarId) && !googleNotebook.isNull()) { // delta sync, not a clean sync. populate our lists of local changes. qCDebug(lcSocialPluginTrace) << "Loading existing data given delta sync method"; m_storage->loadNotebookIncidences(googleNotebook->uid()); m_storage->allIncidences(&allList, googleNotebook->uid()); m_storage->insertedIncidences(&addedList, QDateTime(since), googleNotebook->uid()); m_storage->modifiedIncidences(&updatedList, QDateTime(since), googleNotebook->uid()); // mkcal's implementation of deletedIncidences() is unusual. It returns any event // which was deleted after the second (datetime) parameter, IF AND ONLY IF // it was created before that same datetime. // Unfortunately, mkcal also only supports second-resolution datetimes, which means // that the "last sync timestamp" cannot effectively be used as the parameter, since // any events which were added to the database due to the previous sync cycle // will (most likely) have been added within 1 second of the sync anchor timestamp. // To work around this, we need to retrieve deleted incidences twice, and unite them. m_storage->deletedIncidences(&deletedList, QDateTime(since), googleNotebook->uid()); m_storage->deletedIncidences(&extraDeletedList, QDateTime(since).addSecs(1), googleNotebook->uid()); uniteIncidenceLists(extraDeletedList, &deletedList); Q_FOREACH(const KCalendarCore::Incidence::Ptr incidence, allList) { if (incidence.isNull()) { qCDebug(lcSocialPlugin) << "Ignoring null incidence returned from allIncidences()"; continue; } KCalendarCore::Event::Ptr eventPtr = m_calendar->event(incidence->uid(), incidence->recurrenceId()); QString gcalId = gCalEventId(incidence); if (gcalId.isEmpty() && upsyncedUidMapping.contains(incidence->uid())) { // partially upsynced artifact. It may need to be updated with gcalId comment field. gcalId = upsyncedUidMapping.value(incidence->uid()); partialUpsyncArtifactsNeedingUpdate.insert(gcalId); } if (gcalId.size() && eventPtr) { qCDebug(lcSocialPluginTrace) << "Have local event:" << gcalId << "," << eventPtr->uid() << ":" << eventPtr->recurrenceId().toString(); allMap.insert(gcalId, eventPtr); } // else, newly added locally, no gcalId yet. } Q_FOREACH(const KCalendarCore::Incidence::Ptr incidence, updatedList) { if (incidence.isNull()) { qCDebug(lcSocialPlugin) << "Ignoring null incidence returned from modifiedIncidences()"; continue; } KCalendarCore::Event::Ptr eventPtr = m_calendar->event(incidence->uid(), incidence->recurrenceId()); QString gcalId = gCalEventId(incidence); if (gcalId.isEmpty() && upsyncedUidMapping.contains(incidence->uid())) { // TODO: can this codepath be hit? If it was a partial upsync artifact, // shouldn't it be reported as a local+remote addition, not local modification? // partially upsynced artifact gcalId = upsyncedUidMapping.value(incidence->uid()); partialUpsyncArtifactsNeedingUpdate.remove(gcalId); // will already update due to local change. } if (gcalId.size() && eventPtr) { qCDebug(lcSocialPlugin) << "Have local modification:" << incidence->uid() << "in" << calendarId; updatedMap.insert(gcalId, eventPtr); } // else, newly added+updated locally, no gcalId yet. } Q_FOREACH(const KCalendarCore::Incidence::Ptr incidence, deletedList) { if (incidence.isNull()) { qCDebug(lcSocialPlugin) << "Ignoring null incidence returned from deletedIncidences()"; continue; } QString gcalId = gCalEventId(incidence); if (gcalId.isEmpty() && upsyncedUidMapping.contains(incidence->uid())) { // TODO: can this codepath be hit? If it was a partial upsync artifact, // shouldn't it be reported as a local+remote addition, not local deletion? // partially upsynced artifact gcalId = upsyncedUidMapping.value(incidence->uid()); partialUpsyncArtifactsNeedingUpdate.remove(gcalId); // doesn't need update due to deletion. } if (gcalId.size()) { // Now we check to see whether this event was deleted due to a clean-sync (notebook removal). // If so, then another event (with the same gcalId association) should have been ADDED at the // same time, to fulfil clean-sync semantics (because the notebook uid is maintained). // If so, we treat it as a modification rather than delete+add pair. if (allMap.contains(gcalId)) { // note: this works because gcalId is different for base series vs persistent occurrence of series. qCDebug(lcSocialPlugin) << "Have local deletion+addition from cleansync:" << gcalId << "in" << calendarId; cleanSyncDeletionAdditions.insert(gcalId); } else { // otherwise, it's a real local deletion. qCDebug(lcSocialPlugin) << "Have local deletion:" << incidence->uid() << "in" << calendarId; deletedMap.insert(gcalId, qMakePair(incidence->uid(), incidence->recurrenceId())); updatedMap.remove(gcalId); // don't upsync updates to deleted events. m_deletedGcalIdToIncidence.insert(gcalId, incidence); } } // else, newly added+deleted locally, no gcalId yet. } } else { if (googleNotebook.isNull()) { qCDebug(lcSocialPluginTrace) << "No local notebook exists for remote; no existing data to load."; } } // apply the conflict resolution strategy to remove any local or remote changes which should be dropped. int discardedLocalAdditions = 0, discardedLocalModifications = 0, discardedLocalRemovals = 0; int remoteAdditions = 0, remoteModifications = 0, remoteRemovals = 0, discardedRemoteModifications = 0, discardedRemoteRemovals = 0; QHash unchangedRemoteModifications; // gcalId to eventData. QStringList remoteAdditionIds; // For each each of the events downloaded from the server, determine // if the remote change invalidates a local change, or if a local // deletion invalidates the remote change. // Otherwise, cache the remote change for later storage to local db. foreach (const QJsonObject &eventData, eventObjects) { QString eventId = eventData.value(QLatin1String("id")).toVariant().toString(); QString parentId = eventData.value(QLatin1String("recurringEventId")).toVariant().toString(); bool eventWasDeletedRemotely = eventData.value(QLatin1String("status")).toVariant().toString() == QString::fromLatin1("cancelled"); if (eventWasDeletedRemotely) { // if modified locally and deleted on server side, don't upsync modifications if (allMap.contains(eventId)) { // currently existing base event or persistent occurrence which was deleted server-side remoteRemovals++; qCDebug(lcSocialPlugin) << "Have remote series deletion:" << eventId << "in" << calendarId; m_changesFromDownsync.insertMulti(calendarId, qMakePair(GoogleCalendarSyncAdaptor::Delete, eventData)); if (updatedMap.contains(eventId)) { qCDebug(lcSocialPlugin) << "Discarding local event modification:" << eventId << "due to remote deletion"; updatedMap.remove(eventId); // discard any local modifications to this event, don't upsync. discardedLocalModifications++; } // also discard the event from the locally added list if it is reported there. // this can happen due to cleansync, or the overlap in the sync date due to mkcal resolution issue. for (int i = 0; i < addedList.size(); ++i) { KCalendarCore::Incidence::Ptr addedEvent = addedList[i]; if (addedEvent.isNull()) { qCDebug(lcSocialPlugin) << "Disregarding local event addition due to null state"; continue; } const QString &gcalId(gCalEventId(addedEvent)); if (gcalId == eventId) { qCDebug(lcSocialPlugin) << "Discarding local event addition:" << addedEvent->uid() << "due to remote deletion"; addedList.remove(i); discardedLocalAdditions++; break; } } } else if (!parentId.isEmpty() && (allMap.contains(parentId) || remoteAdditionIds.contains(parentId))) { // this is a non-persistent occurrence deletion, we need to add an EXDATE to the base event. // we treat this as a remote modification of the base event (ie, the EXDATE addition) // and thus will discard any local modifications to the base event, and not upsync them. // TODO: use a more optimal conflict resolution strategy for this case! remoteRemovals++; qCDebug(lcSocialPlugin) << "Have remote occurrence deletion:" << eventId << "in" << calendarId; m_changesFromDownsync.insertMulti(calendarId, qMakePair(GoogleCalendarSyncAdaptor::DeleteOccurrence, eventData)); if (updatedMap.contains(parentId)) { qCDebug(lcSocialPlugin) << "Discarding local modification to recurrence series:" << parentId << "due to remote EXDATE addition. Sub-optimal resolution strategy!"; updatedMap.remove(parentId); discardedLocalModifications++; } // also discard the event from the locally added list if it is reported there. // this can happen due to cleansync, or the overlap in the sync date due to mkcal resolution issue. for (int i = 0; i < addedList.size(); ++i) { KCalendarCore::Incidence::Ptr addedEvent = addedList[i]; if (addedEvent.isNull()) { qCDebug(lcSocialPlugin) << "Disregarding local event addition due to null state"; continue; } const QString &gcalId(gCalEventId(addedEvent)); if (gcalId == eventId) { qCDebug(lcSocialPlugin) << "Discarding local event addition:" << addedEvent->uid() << "due to remote EXDATE addition. Sub-optimal resolution strategy!"; addedList.remove(i); discardedLocalAdditions++; break; } } } else { // !allMap.contains(parentId) if (deletedMap.contains(eventId)) { // remote deleted event was also deleted locally, can ignore. qCDebug(lcSocialPlugin) << "Event deleted remotely:" << eventId << "was already deleted locally; discarding both local and remote deletion"; deletedMap.remove(eventId); // discard local deletion. discardedLocalRemovals++; discardedRemoteRemovals++; } else { // remote deleted event never existed locally. // this can happen due to the increased timeMin window // extending to prior to the account existing on the device. qCDebug(lcSocialPlugin) << "Event deleted remotely:" << eventId << "was never downsynced to device; discarding"; discardedRemoteRemovals++; } } } else if (deletedMap.contains(eventId)) { // remote change will be discarded due to local deletion. qCDebug(lcSocialPlugin) << "Discarding remote event modification:" << eventId << "due to local deletion"; discardedRemoteModifications++; } else if (allMap.contains(eventId)) { // remote modification of an existing event. KCalendarCore::Event::Ptr event = allMap.value(eventId); bool changed = false; if (partialUpsyncArtifactsNeedingUpdate.contains(eventId)) { // This event was partially upsynced and then connectivity died before we committed // and updated its comment field with the gcalId it was given by the remote server. // During this sync cycle, we will update it by assuming remote modification. // Note: this will lose any local changes made since it was partially-upsynced, // however the alternative is to lose remote changes made since then... // So we stick with our "prefer-remote" conflict resolution strategy here. qCDebug(lcSocialPlugin) << "Reloading partial upsync artifact:" << eventId << "from server as a modification"; changed = true; } else { qCDebug(lcSocialPlugin) << "Determining if remote data differs from local data for event" << eventId << "in" << calendarId; if (event.isNull()) { qCDebug(lcSocialPlugin) << "Unable to find local event:" << eventId << ", marking as changed."; changed = true; } else { changed = remoteModificationIsReal(eventData, event); } } if (!changed) { // Not a real change. We discard this remote modification, // but we track it so that we can detect spurious local modifications. qCDebug(lcSocialPlugin) << "Discarding remote event modification:" << eventId << "in" << calendarId << "as spurious"; unchangedRemoteModifications.insert(eventId, eventData); discardedRemoteModifications++; } else { qCDebug(lcSocialPlugin) << "Have remote modification:" << eventId << "in" << calendarId; remoteModifications++; m_changesFromDownsync.insertMulti(calendarId, qMakePair(GoogleCalendarSyncAdaptor::Modify, eventData)); if (updatedMap.contains(eventId)) { // if both local and server were modified, prefer server. qCDebug(lcSocialPlugin) << "Discarding local event modification:" << eventId << "due to remote modification"; updatedMap.remove(eventId); discardedLocalModifications++; } // also discard the event from the locally added list if it is reported there. // this can happen due to cleansync, or the overlap in the sync date due to mkcal resolution issue. for (int i = 0; i < addedList.size(); ++i) { KCalendarCore::Incidence::Ptr addedEvent = addedList[i]; if (addedEvent.isNull()) { qCDebug(lcSocialPlugin) << "Disregarding local event addition due to null state"; continue; } const QString &gcalId(gCalEventId(addedEvent)); if (gcalId == eventId) { qCDebug(lcSocialPlugin) << "Discarding local event addition:" << addedEvent->uid() << "due to remote modification"; addedList.remove(i); discardedLocalAdditions++; break; } } } } else { // pure remote addition. remote additions cannot invalidate local changes. // note that we have already detected (and dealt with) partial-upsync-artifacts // which would have been reported from the remote server as additions. qCDebug(lcSocialPlugin) << "Have remote addition:" << eventId << "in" << calendarId; remoteAdditions++; m_changesFromDownsync.insertMulti(calendarId, qMakePair(GoogleCalendarSyncAdaptor::Insert, eventData)); remoteAdditionIds.append(eventId); } } qCInfo(lcSocialPlugin) << "Delta downsync from Google calendar" << calendarId << "for account" << m_accountId << ":" << "remote A/M/R: " << remoteAdditions << "/" << remoteModifications << "/" << remoteRemovals << "after discarding M/R:" << discardedRemoteModifications << "/" << discardedRemoteRemovals << "due to local deletions or identical data"; if (upsyncEnabled) { // Now build the local-changes-to-upsync data structures. int localAdded = 0, localModified = 0, localRemoved = 0; // first, queue up deletions. Q_FOREACH (const QString &deletedGcalId, deletedMap.keys()) { QString incidenceUid = deletedMap.value(deletedGcalId).first; QDateTime recurrenceId = deletedMap.value(deletedGcalId).second; localRemoved++; qCDebug(lcSocialPluginTrace) << "queueing upsync deletion for gcal id:" << deletedGcalId; UpsyncChange deletion; deletion.accessToken = accessToken; deletion.upsyncType = GoogleCalendarSyncAdaptor::Delete; deletion.kcalNotebookId = googleNotebook->uid(); deletion.kcalEventId = incidenceUid; deletion.recurrenceId = recurrenceId; deletion.calendarId = calendarId; deletion.eventId = deletedGcalId; changesToUpsync.append(deletion); } // second, queue up modifications. Q_FOREACH (const QString &updatedGcalId, updatedMap.keys()) { KCalendarCore::Event::Ptr event = updatedMap.value(updatedGcalId); if (event) { QJsonObject localEventData = kCalToJson(event, m_icalFormat); if (unchangedRemoteModifications.contains(updatedGcalId) && !localModificationIsReal(localEventData, unchangedRemoteModifications.value(updatedGcalId), m_serverCalendarIdToDefaultReminderTimes.value(calendarId), m_icalFormat)) { // this local modification is spurious. It may have been reported // due to the timestamp resolution issue, but in any case the // event does not differ from the remote one. qCDebug(lcSocialPlugin) << "Discarding local event modification:" << event->uid() << event->recurrenceId().toString() << "as spurious, for gcalId:" << updatedGcalId; discardedLocalModifications++; continue; } localModified++; QByteArray eventBlob = QJsonDocument(localEventData).toJson(); qCDebug(lcSocialPluginTrace) << "queueing upsync modification for gcal id:" << updatedGcalId; traceDumpStr(QString::fromUtf8(eventBlob)); UpsyncChange modification; modification.accessToken = accessToken; modification.upsyncType = GoogleCalendarSyncAdaptor::Modify; modification.kcalNotebookId = googleNotebook->uid(); modification.kcalEventId = event->uid(); modification.recurrenceId = event->recurrenceId(); modification.calendarId = calendarId; modification.eventId = updatedGcalId; modification.eventData = eventBlob; changesToUpsync.append(modification); } } // move parent insertions before recurrence exclusion insertions reorderAdditions(addedList); // finally, queue up insertions. Q_FOREACH (KCalendarCore::Incidence::Ptr incidence, addedList) { KCalendarCore::Event::Ptr event = m_calendar->event(incidence->uid(), incidence->recurrenceId()); if (event) { if (upsyncedUidMapping.contains(incidence->uid())) { const QString &eventId(upsyncedUidMapping.value(incidence->uid())); if (partialUpsyncArtifactsNeedingUpdate.contains(eventId)) { // We have already handled this one, by treating it as a remote modification, above. qCDebug(lcSocialPlugin) << "Discarding partial upsync artifact local addition:" << eventId; discardedLocalAdditions++; continue; } // should never be hit. bug in plugin code. qCWarning(lcSocialPlugin) << "Not discarding partial upsync artifact local addition due to data inconsistency:" << eventId; } const QString gcalId = gCalEventId(event); if (!gcalId.isEmpty() && !event->hasRecurrenceId()) { if (cleanSyncDeletionAdditions.contains(gcalId)) { // this event was deleted+re-added due to clean sync. treat it as a local modification // of the remote event. Note: we cannot update the extended UID property in the remote // event, because multiple other devices may depend on it. When we downsynced the event // for the re-add, we should have re-used the old uid. qCDebug(lcSocialPlugin) << "Converting local addition to modification due to clean-sync semantics"; } else { // this event was previously downsynced from the remote in the last sync cycle. // check to see whether it has changed locally since we downsynced it. if (event->lastModified() < since) { qCDebug(lcSocialPlugin) << "Discarding local event addition:" << event->uid() << event->recurrenceId().toString() << "as spurious due to downsync, for gcalId:" << gcalId; qCDebug(lcSocialPlugin) << "Last modified:" << event->lastModified() << "<" << since; discardedLocalModifications++; continue; } // we treat it as a local modification (as it has changed locally since it was downsynced). qCDebug(lcSocialPlugin) << "Converting local addition to modification due to it being a previously downsynced event"; } // convert the local event to a JSON object. QJsonObject localEventData = kCalToJson(event, m_icalFormat); // check to see if this differs from some discarded remote modification. // if it does not, then the remote and local are identical, and it's only // being reported as a local addition/modification due to the "since" timestamp // overlap. if (unchangedRemoteModifications.contains(gcalId) && !localModificationIsReal(localEventData, unchangedRemoteModifications.value(gcalId), m_serverCalendarIdToDefaultReminderTimes.value(calendarId), m_icalFormat)) { // this local addition is spurious. It may have been reported // due to the timestamp resolution issue, but in any case the // event does not differ from the remote one which is already updated. qCDebug(lcSocialPlugin) << "Discarding local event modification:" << event->uid() << event->recurrenceId().toString() << "as spurious, for gcalId:" << gcalId; discardedLocalModifications++; continue; } localModified++; QByteArray eventBlob = QJsonDocument(localEventData).toJson(); qCDebug(lcSocialPluginTrace) << "queueing upsync modification for gcal id:" << gcalId; traceDumpStr(QString::fromUtf8(eventBlob)); UpsyncChange modification; modification.accessToken = accessToken; modification.upsyncType = GoogleCalendarSyncAdaptor::Modify; modification.kcalNotebookId = googleNotebook->uid(); modification.kcalEventId = event->uid(); modification.recurrenceId = event->recurrenceId(); modification.calendarId = calendarId; modification.eventId = gcalId; modification.eventData = eventBlob; changesToUpsync.append(modification); } else { localAdded++; // This is an insertion, so we may need it to run after a parent insertion completed queueSequencedInsertion(changesToUpsync, event, calendarId, accessToken); } } } qCInfo(lcSocialPlugin) << "Delta upsync with Google calendar" << calendarId << "for account" << m_accountId << ":" << "local A/M/R:" << localAdded << "/" << localModified << "/" << localRemoved << "after discarding A/M/R:" << discardedLocalAdditions << "/" << discardedLocalModifications << "/" << discardedLocalRemovals << "due to remote changes or identical data"; } return changesToUpsync; } void GoogleCalendarSyncAdaptor::queueSequencedInsertion(QList &changesToUpsync, const KCalendarCore::Event::Ptr event, const QString &calendarId, const QString &accessToken) { // Last parameter true -> "insert extended UID property" QJsonObject eventJson = kCalToJson(event, m_icalFormat, true); // Check whether the insertion needs to be sequenced after its parent QString insertionGcalId; UpsyncChange *parentChange = nullptr; if (event->hasRecurrenceId()) { // This event has a parent, so we must check if it needs to be sequenced for (UpsyncChange &change : changesToUpsync) { if ((change.upsyncType == Insert) && (change.kcalEventId == event->uid()) && (change.recurrenceId.isNull())) { parentChange = &change; break; } } // Use our generated GCalId to refer to the parent if (parentChange && !parentChange->eventId.isEmpty()) { // Replace the recurringEventId eventJson.insert(QLatin1String("recurringEventId"), parentChange->eventId); } } else { // There will be no gcalId yet, so we can generate and add our own insertionGcalId = generate_uuid(); qCDebug(lcSocialPlugin) << "Generated id for new event:" << insertionGcalId; eventJson.insert(QLatin1String("id"), insertionGcalId); } qCDebug(lcSocialPluginTrace) << "queueing up insertion for local id:" << event->uid(); UpsyncChange insertion; insertion.accessToken = accessToken; insertion.upsyncType = GoogleCalendarSyncAdaptor::Insert; insertion.kcalEventId = event->uid(); insertion.recurrenceId = event->recurrenceId(); insertion.calendarId = calendarId; insertion.eventId = insertionGcalId; insertion.eventData = QJsonDocument(eventJson).toJson(); traceDumpStr(QString::fromUtf8(insertion.eventData)); // At this point we either add the upsync change to the default queue, or to the sequenced queue if (parentChange && !parentChange->eventId.isEmpty()) { qCDebug(lcSocialPlugin) << "Sequencing event after its parent:" << parentChange->eventId; // Add the insertion to the appropriate sublist m_sequenced.insertMulti(parentChange->eventId, insertion); } else { qCDebug(lcSocialPlugin) << "Upsyncing event in first round without sequencing:" << event->uid() << event->summary(); changesToUpsync.append(insertion); } } void GoogleCalendarSyncAdaptor::upsyncChanges(const UpsyncChange &changeToUpsync) { const QString &accessToken = changeToUpsync.accessToken; GoogleCalendarSyncAdaptor::ChangeType upsyncType = changeToUpsync.upsyncType; const QString &kcalNotebookId = changeToUpsync.kcalNotebookId; const QString &kcalEventId = changeToUpsync.kcalEventId; const QDateTime &recurrenceId = changeToUpsync.recurrenceId; const QString &calendarId = changeToUpsync.calendarId; const QString &eventId = changeToUpsync.eventId; const QByteArray &eventData = changeToUpsync.eventData; QUrl requestUrl = upsyncType == GoogleCalendarSyncAdaptor::Insert ? QUrl(QString::fromLatin1("https://www.googleapis.com/calendar/v3/calendars/%1/events").arg(percentEnc(calendarId))) : QUrl(QString::fromLatin1("https://www.googleapis.com/calendar/v3/calendars/%1/events/%2").arg(percentEnc(calendarId)).arg(eventId)); QNetworkRequest request(requestUrl); request.setRawHeader("GData-Version", "3.0"); request.setRawHeader(QString(QLatin1String("Authorization")).toUtf8(), QString(QLatin1String("Bearer ") + accessToken).toUtf8()); request.setHeader(QNetworkRequest::ContentTypeHeader, QVariant::fromValue(QString::fromLatin1("application/json"))); QNetworkReply *reply = 0; QString upsyncTypeStr; switch (upsyncType) { case GoogleCalendarSyncAdaptor::Insert: upsyncTypeStr = QString::fromLatin1("Insert"); reply = m_networkAccessManager->post(request, eventData); break; case GoogleCalendarSyncAdaptor::Modify: upsyncTypeStr = QString::fromLatin1("Modify"); reply = m_networkAccessManager->put(request, eventData); break; case GoogleCalendarSyncAdaptor::Delete: upsyncTypeStr = QString::fromLatin1("Delete"); reply = m_networkAccessManager->deleteResource(request); break; default: qCWarning(lcSocialPlugin) << "UNREACHBLE - upsyncing non-change"; // always an error. m_syncSucceeded = false; return; } // we're performing a request. Increment the semaphore so that we know we're still busy. incrementSemaphore(m_accountId); if (reply) { reply->setProperty("accountId", m_accountId); reply->setProperty("accessToken", accessToken); reply->setProperty("upsyncType", static_cast(upsyncType)); reply->setProperty("kcalNotebookId", kcalNotebookId); reply->setProperty("kcalEventId", kcalEventId); reply->setProperty("recurrenceId", recurrenceId); reply->setProperty("calendarId", calendarId); reply->setProperty("eventId", eventId); reply->setProperty("eventData", eventData); connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(errorHandler(QNetworkReply::NetworkError))); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsHandler(QList))); connect(reply, SIGNAL(finished()), this, SLOT(upsyncFinishedHandler())); setupReplyTimeout(m_accountId, reply); qCDebug(lcSocialPlugin) << "upsyncing change:" << upsyncTypeStr << "to calendarId:" << calendarId << "of account" << m_accountId << "to" << request.url().toString(); traceDumpStr(QString::fromUtf8(eventData)); } else { qCWarning(lcSocialPlugin) << "unable to request upsync for calendar" << calendarId << "from Google account with id" << m_accountId; m_syncSucceeded = false; decrementSemaphore(m_accountId); } } void GoogleCalendarSyncAdaptor::reInsertWithRandomId(const QNetworkReply *reply) { Q_ASSERT(reply->property("accountId").toInt() == m_accountId); QString accessToken = reply->property("accessToken").toString(); ChangeType upsyncType = ChangeType(reply->property("upsyncType").toInt()); QString kcalEventId = reply->property("kcalEventId").toString(); QDateTime recurrenceId = reply->property("recurrenceId").toDateTime(); QString calendarId = reply->property("calendarId").toString(); QString eventId = reply->property("eventId").toString(); QByteArray eventData = reply->property("eventData").toByteArray(); // The gcalId we chose randomly collided, so we should try with another qCDebug(lcSocialPluginTrace) << "GCalId collision, try with something different"; QString insertionGcalId = generate_uuid(); qCDebug(lcSocialPlugin) << "Generated id for new event:" << insertionGcalId; // Update the parent's sequenced events if (m_sequenced.contains(eventId)) { // Moving values in-place is problemmatic in case (eventId == insertionGuid) so copy them QList changesToUpsync = m_sequenced.values(eventId); m_sequenced.remove(eventId); for (UpsyncChange &changeToUpsync : changesToUpsync) { qCDebug(lcSocialPlugin) << "Updating sequenced gcalId for event" << changeToUpsync.kcalEventId << "recurrenceId" << changeToUpsync.recurrenceId; changeToUpsync.eventData = jsonReplaceValue(changeToUpsync.eventData, "recurringEventId", insertionGcalId); m_sequenced.insertMulti(insertionGcalId, changeToUpsync); } } UpsyncChange changeToUpsync; changeToUpsync.accessToken = accessToken; changeToUpsync.upsyncType = upsyncType; changeToUpsync.kcalEventId = kcalEventId; changeToUpsync.recurrenceId = recurrenceId; changeToUpsync.calendarId = calendarId; changeToUpsync.eventId = insertionGcalId; changeToUpsync.eventData = jsonReplaceValue(eventData, "id", insertionGcalId); upsyncChanges(changeToUpsync); } void GoogleCalendarSyncAdaptor::handleErrorReply(QNetworkReply *reply) { Q_ASSERT(reply->property("accountId").toInt() == m_accountId); ChangeType upsyncType = ChangeType(reply->property("upsyncType").toInt()); QDateTime recurrenceId = reply->property("recurrenceId").toDateTime(); const QByteArray &replyData = reply->readAll(); int httpCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); const QString kcalEventId = reply->property("kcalEventId").toString(); // error occurred during request. qCWarning(lcSocialPlugin) << "error: calendarId:" << reply->property("calendarId").toString(); qCWarning(lcSocialPlugin) << "error: eventId:" << reply->property("eventId").toString(); qCWarning(lcSocialPlugin) << "error: summary:" << reply->property("summary").toString(); qCWarning(lcSocialPlugin) << "error" << httpCode << "occurred upsyncing Google account" << m_accountId << "; got:"; errorDumpStr(QString::fromUtf8(replyData)); if (reply->error() == QNetworkReply::ContentOperationNotPermittedError) { const QString reason = getErrorReason(replyData); if (reason == ERROR_REASON_NON_ORGANIZER) { // This is an attempt to modify a shared event, and Google prevents // any user other than the original creator of the event from modifying those. // Such errors should not prevent the rest of the sync cycle from succeeding. qCDebug(lcSocialPluginTrace) << "Ignoring 403 due to shared calendar resource"; } else { qCWarning(lcSocialPlugin) << "Usage limit reached. Please try syncing again later."; m_syncSucceeded = false; } flagUploadFailure(kcalEventId); } else if (httpCode == 410) { // HTTP 410 GONE "deleted" // The event was already deleted on the server, so continue as normal qCDebug(lcSocialPluginTrace) << "Event already deleted on the server, so we're now in sync"; } else if ((httpCode == 404) && (upsyncType == ChangeType::Delete)) { // HTTP 404 NOT FOUND on deletion // The event doesn't now exist on client or server, so continue as normal qCDebug(lcSocialPluginTrace) << "Event deleted doesn't exist on the server, so we're now in sync"; } else if (httpCode == 409) { // HTTP 409 CONFLICT "The requested identifier already exists" // This should be a super-rare occurrence // We only generate gcalIds for insertions without recurrenceIds Q_ASSERT(upsyncType == GoogleCalendarSyncAdaptor::Insert); Q_ASSERT(recurrenceId.isNull()); ++m_collisionErrorCount; if (m_collisionErrorCount < COLLISION_ERROR_MAX_CONSECUTIVE) { reInsertWithRandomId(reply); } else { qCDebug(lcSocialPluginTrace) << "Reached" << m_collisionErrorCount << "id collisions; giving up"; flagUploadFailure(kcalEventId); m_syncSucceeded = false; } } else { flagUploadFailure(kcalEventId); m_syncSucceeded = false; } } void GoogleCalendarSyncAdaptor::handleDeleteReply(QNetworkReply *reply) { Q_ASSERT(reply->property("accountId").toInt() == m_accountId); const QString kcalNotebookId = reply->property("kcalNotebookId").toString(); QString kcalEventId = reply->property("kcalEventId").toString(); QString eventId = reply->property("eventId").toString(); const QByteArray &replyData = reply->readAll(); int httpCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); // we expect an empty response body on success for Delete operations // the only exception is if there's an error, in which case this should have been // picked up by the "isError" clause. if (replyData.isEmpty()) { KCalendarCore::Incidence::Ptr incidence = m_deletedGcalIdToIncidence.value(eventId); qCDebug(lcSocialPluginTrace) << "Deletion confirmed, purging event: " << kcalEventId; const QMap::Iterator it = m_purgeList.find(kcalNotebookId); if (it == m_purgeList.end()) { m_purgeList.insert(kcalNotebookId, KCalendarCore::Incidence::List() << incidence); } else { it.value().append(incidence); } } else { // This path should never be taken qCWarning(lcSocialPlugin) << "error" << httpCode << "occurred while upsyncing calendar event deletion to Google account" << m_accountId << "; got:"; errorDumpStr(QString::fromUtf8(replyData)); m_syncSucceeded = false; } } void GoogleCalendarSyncAdaptor::handleInsertModifyReply(QNetworkReply *reply) { ChangeType upsyncType = ChangeType(reply->property("upsyncType").toInt()); QString kcalEventId = reply->property("kcalEventId").toString(); QDateTime recurrenceId = reply->property("recurrenceId").toDateTime(); QString calendarId = reply->property("calendarId").toString(); const QByteArray &replyData = reply->readAll(); // we expect an event resource body on success for Insert/Modify requests. bool ok = false; QJsonObject parsed = parseJsonObjectReplyData(replyData, &ok); if (!ok) { QString typeStr = upsyncType == GoogleCalendarSyncAdaptor::Insert ? QString::fromLatin1("insertion") : QString::fromLatin1("modification"); qCWarning(lcSocialPlugin) << "error occurred while upsyncing calendar event" << typeStr << "to Google account" << m_accountId << "; got:"; errorDumpStr(QString::fromUtf8(replyData)); flagUploadFailure(kcalEventId); m_syncSucceeded = false; } else { // No collision for this upsync, so reset the collision error count m_collisionErrorCount = 0; // TODO: reduce code duplication between here and the other function. // Search for the device Notebook matching this CalendarId mKCal::Notebook::Ptr googleNotebook = notebookForCalendarId(calendarId); if (googleNotebook.isNull()) { qCWarning(lcSocialPlugin) << "calendar" << calendarId << "doesn't have a notebook for Google account with id" << m_accountId; m_syncSucceeded = false; } else { // cache the update to this event in the local calendar m_storage->loadNotebookIncidences(googleNotebook->uid()); KCalendarCore::Event::Ptr event = m_calendar->event(kcalEventId, recurrenceId); if (!event) { qCWarning(lcSocialPlugin) << "event" << kcalEventId << recurrenceId.toString() << "was deleted locally during sync of Google account with id" << m_accountId; m_syncSucceeded = false; } else { qCDebug(lcSocialPluginTrace) << "Local upsync response json:"; traceDumpStr(QString::fromUtf8(replyData)); m_changesFromUpsync.insertMulti(calendarId, qMakePair(event, parsed)); flagUploadSuccess(kcalEventId); } } } } void GoogleCalendarSyncAdaptor::performSequencedUpsyncs(const QNetworkReply *reply) { QString eventId = reply->property("eventId").toString(); qCDebug(lcSocialPlugin) << "Performing sequenced upsyncs"; // Trigger any sequenced upsyncs before we decrement the semaphore QMultiHash::const_iterator iter = m_sequenced.find(eventId); while (iter != m_sequenced.end() && iter.key() == eventId) { const UpsyncChange &changeToUpsync = iter.value(); qCDebug(lcSocialPlugin) << "Sequenced upsync for event" << changeToUpsync.kcalEventId << "recurrenceId" << changeToUpsync.recurrenceId; upsyncChanges(changeToUpsync); ++iter; } } void GoogleCalendarSyncAdaptor::upsyncFinishedHandler() { QNetworkReply *reply = qobject_cast(sender()); Q_ASSERT(reply->property("accountId").toInt() == m_accountId); ChangeType upsyncType = ChangeType(reply->property("upsyncType").toInt()); bool isError = reply->property("isError").toBool(); // QNetworkReply can report an error even if there isn't one... if (isError && reply->error() == QNetworkReply::UnknownContentError && upsyncType == GoogleCalendarSyncAdaptor::Delete) { isError = false; // not a real error; Google returns an empty response. } disconnect(reply); reply->deleteLater(); removeReplyTimeout(m_accountId, reply); // parse the calendars' metadata from the response. if (isError) { handleErrorReply(reply); } else if (upsyncType == GoogleCalendarSyncAdaptor::Delete) { handleDeleteReply(reply); } else { // upsyncType == GoogleCalendarSyncAdaptor::Insert // upsyncType == GoogleCalendarSyncAdaptor::Modify handleInsertModifyReply(reply); } if (!isError) { performSequencedUpsyncs(reply); } // we're finished with this request. decrementSemaphore(m_accountId); } void GoogleCalendarSyncAdaptor::setCalendarProperties( mKCal::Notebook::Ptr notebook, const CalendarInfo &calendarInfo, const QString &serverCalendarId, int accountId, const QString &syncProfile, const QString &ownerEmail) { notebook->setIsReadOnly(calendarInfo.access == GoogleCalendarSyncAdaptor::Reader || calendarInfo.access == GoogleCalendarSyncAdaptor::FreeBusyReader); notebook->setName(calendarInfo.summary); notebook->setDescription(calendarInfo.description); notebook->setPluginName(QStringLiteral("google")); notebook->setSyncProfile(syncProfile); notebook->setCustomProperty(NOTEBOOK_SERVER_ID_PROPERTY, serverCalendarId); notebook->setCustomProperty(NOTEBOOK_EMAIL_PROPERTY, ownerEmail); // extra calendars have their own email addresses. using this property to pass it forward. notebook->setSharedWith(QStringList() << serverCalendarId); notebook->setAccount(QString::number(accountId)); if (!calendarInfo.color.isEmpty() && notebook->customProperty(SERVER_COLOR_PROPERTY) != calendarInfo.color) { if (!notebook->customProperty(SERVER_COLOR_PROPERTY).isEmpty()) { // Override user-selected notebook color only on each server change // and not if there was no server color saved. notebook->setColor(calendarInfo.color); } notebook->setCustomProperty(SERVER_COLOR_PROPERTY, calendarInfo.color); } } void GoogleCalendarSyncAdaptor::applyRemoteChangesLocally() { qCDebug(lcSocialPlugin) << "applying all remote changes to local database"; QString emailAddress; QString syncProfile; Accounts::Account *account = Accounts::Account::fromId(m_accountManager, m_accountId, Q_NULLPTR); if (!account) { qCWarning(lcSocialPlugin) << "unable to load Google account" << m_accountId << "to retrieve settings"; } else { account->selectService(m_accountManager->service(QStringLiteral("google-gmail"))); emailAddress = account->valueAsString(QStringLiteral("emailaddress")); account->selectService(m_accountManager->service(QStringLiteral("google-calendars"))); syncProfile = account->valueAsString(QStringLiteral("google.Calendars/profile_id")); account->deleteLater(); } foreach (const QString &serverCalendarId, m_serverCalendarIdToCalendarInfo.keys()) { const CalendarInfo calendarInfo = m_serverCalendarIdToCalendarInfo.value(serverCalendarId); const QString ownerEmail = (calendarInfo.access == GoogleCalendarSyncAdaptor::Owner) ? emailAddress : QString(); switch (calendarInfo.change) { case GoogleCalendarSyncAdaptor::NoChange: { // No changes required. Note that this just applies to the notebook metadata; // there may be incidences belonging to this notebook which need modification. qCDebug(lcSocialPlugin) << "No metadata changes required for local notebook for server calendar:" << serverCalendarId; mKCal::Notebook::Ptr notebook = notebookForCalendarId(serverCalendarId); // We ensure anyway property values for notebooks created without. if (notebook && notebook->syncProfile() != syncProfile) { qCDebug(lcSocialPlugin) << "Adding missing sync profile label."; notebook->setSyncProfile(syncProfile); m_storage->updateNotebook(notebook); // Actually, we don't need to flag m_storageNeedsSave since // notebook operations are immediate on storage. } } break; case GoogleCalendarSyncAdaptor::Insert: { qCDebug(lcSocialPlugin) << "Adding local notebook for new server calendar:" << serverCalendarId; mKCal::Notebook::Ptr notebook = mKCal::Notebook::Ptr(new mKCal::Notebook); setCalendarProperties(notebook, calendarInfo, serverCalendarId, m_accountId, syncProfile, ownerEmail); m_storage->addNotebook(notebook); } break; case GoogleCalendarSyncAdaptor::Modify: { qCDebug(lcSocialPlugin) << "Modifications required for local notebook for server calendar:" << serverCalendarId; mKCal::Notebook::Ptr notebook = notebookForCalendarId(serverCalendarId); if (notebook.isNull()) { qCWarning(lcSocialPlugin) << "unable to modify non-existent calendar:" << serverCalendarId << "for account:" << m_accountId; m_syncSucceeded = false; // we don't return immediately, as we want to at least attempt to // apply other database modifications if possible, in order to leave // the local database in a usable state even after failed sync. } else { setCalendarProperties(notebook, calendarInfo, serverCalendarId, m_accountId, syncProfile, ownerEmail); m_storage->updateNotebook(notebook); } } break; case GoogleCalendarSyncAdaptor::Delete: { qCDebug(lcSocialPlugin) << "Deleting local notebook for deleted server calendar:" << serverCalendarId; mKCal::Notebook::Ptr notebook = notebookForCalendarId(serverCalendarId); if (notebook.isNull()) { qCWarning(lcSocialPlugin) << "unable to delete non-existent calendar:" << serverCalendarId << "for account:" << m_accountId; // m_syncSucceeded = false; // don't mark as failed, since the outcome is identical. } else { m_storage->deleteNotebook(notebook); } } break; case GoogleCalendarSyncAdaptor::DeleteOccurrence: { // this codepath should never be hit. qCWarning(lcSocialPlugin) << "invalid DeleteOccurrence change reported for calendar:" << serverCalendarId << "from account:" << m_accountId; } break; case GoogleCalendarSyncAdaptor::CleanSync: { qCDebug(lcSocialPlugin) << "Deleting and recreating local notebook for clean-sync server calendar:" << serverCalendarId; QString notebookUid; // reuse the old notebook Uid after recreating it due to clean sync. // delete mKCal::Notebook::Ptr notebook = notebookForCalendarId(serverCalendarId); if (!notebook.isNull()) { qCDebug(lcSocialPlugin) << "deleting notebook:" << notebook->uid() << "due to clean sync"; notebookUid = notebook->uid(); m_storage->deleteNotebook(notebook); } else { qCDebug(lcSocialPlugin) << "could not find local notebook corresponding to server calendar:" << serverCalendarId; } // and then recreate. qCDebug(lcSocialPlugin) << "recreating notebook:" << notebook->uid() << "due to clean sync"; notebook = mKCal::Notebook::Ptr(new mKCal::Notebook); if (!notebookUid.isEmpty()) { notebook->setUid(notebookUid); } setCalendarProperties(notebook, calendarInfo, serverCalendarId, m_accountId, syncProfile, ownerEmail); m_storage->addNotebook(notebook); } break; } } qCDebug(lcSocialPlugin) << "finished updating local notebooks, about to apply remote event delta locally"; QStringList calendarsNeedingLocalChanges = m_changesFromDownsync.keys() + m_changesFromUpsync.keys(); calendarsNeedingLocalChanges.removeDuplicates(); Q_FOREACH (const QString &updatedCalendarId, calendarsNeedingLocalChanges) { // save any required changes to the local database updateLocalCalendarNotebookEvents(updatedCalendarId); m_storageNeedsSave = true; } } KCalendarCore::Event::Ptr GoogleCalendarSyncAdaptor::addDummyParent(const QJsonObject &eventData, const QString &parentId, const mKCal::Notebook::Ptr googleNotebook) { if (!googleNotebook) { qCWarning(lcSocialPlugin) << "No google Notebook for calendar inserting:" << parentId; return KCalendarCore::Event::Ptr(); } KCalendarCore::Event::Ptr parentEvent = KCalendarCore::Event::Ptr(new KCalendarCore::Event); bool changed = true; jsonToKCal(eventData, parentEvent, 0, m_icalFormat, &changed); // Clear the recurrence rule, to avoid issues when one gets added during dissociation parentEvent->clearRecurrence(); clampEventTimeToSync(parentEvent); qCDebug(lcSocialPlugin) << "Inserting parent event with new lastModified time: " << parentEvent->lastModified().toString(); setGCalEventId(parentEvent, parentId); if (!m_calendar->addEvent(parentEvent, googleNotebook->uid())) { qCWarning(lcSocialPlugin) << "Could not add parent occurrence to calendar:" << parentId; return KCalendarCore::Event::Ptr(); } return parentEvent; } bool GoogleCalendarSyncAdaptor::applyRemoteDelete(const QString &eventId, QMap &allLocalEventsMap) { qCDebug(lcSocialPlugin) << "Event deleted remotely:" << eventId; KCalendarCore::Event::Ptr doomed = allLocalEventsMap.value(eventId); if (!m_calendar->deleteEvent(doomed)) { qCWarning(lcSocialPlugin) << "Unable to delete incidence: " << doomed->uid() << doomed->recurrenceId().toString(); flagDeleteFailure(doomed->uid()); return false; } return true; } bool GoogleCalendarSyncAdaptor::applyRemoteDeleteOccurence(const QString &eventId, const QJsonObject &eventData, QMap &allLocalEventsMap) { const QString parentId = eventData.value(QLatin1String("recurringEventId")).toVariant().toString(); const QDateTime recurrenceId = parseRecurrenceId(eventData.value("originalStartTime").toObject()); qCDebug(lcSocialPlugin) << "Occurrence deleted remotely:" << eventId << "for recurrenceId:" << recurrenceId.toString(); KCalendarCore::Event::Ptr event = allLocalEventsMap.value(parentId); if (event) { if (recurrenceId.isValid()) { event->startUpdates(); if (event->allDay()) { event->recurrence()->addExDate(recurrenceId.date()); } else { event->recurrence()->addExDateTime(recurrenceId); } event->endUpdates(); } else { flagDeleteFailure(event->uid()); } } else { // The parent event should never be null by this point, but we guard against it just in case qCWarning(lcSocialPlugin) << "Deletion failed as the parent event" << parentId << "couldn't be found"; } return true; } bool GoogleCalendarSyncAdaptor::applyRemoteModify(const QString &eventId, const QJsonObject &eventData, const QString &calendarId, QMap &allLocalEventsMap) { qCDebug(lcSocialPlugin) << "Event modified remotely:" << eventId; KCalendarCore::Event::Ptr event = allLocalEventsMap.value(eventId); if (event.isNull()) { qCWarning(lcSocialPlugin) << "Cannot find modified event:" << eventId << "in local calendar!"; return false; } bool changed = false; // modification, not insert, so initially changed = "false". jsonToKCal(eventData, event, m_serverCalendarIdToDefaultReminderTimes.value(calendarId), m_icalFormat, &changed); clampEventTimeToSync(event); qCDebug(lcSocialPlugin) << "Modified event with new lastModified time: " << event->lastModified().toString(); return true; } bool GoogleCalendarSyncAdaptor::applyRemoteInsert(const QString &eventId, const QJsonObject &eventData, const QString &calendarId, const QHash &upsyncedUidMapping, QMap &allLocalEventsMap) { QDateTime recurrenceId = parseRecurrenceId(eventData.value("originalStartTime").toObject()); QString parentId = eventData.value(QLatin1String("recurringEventId")).toVariant().toString(); mKCal::Notebook::Ptr googleNotebook = notebookForCalendarId(calendarId); if (!googleNotebook) { qCWarning(lcSocialPlugin) << "No google Notebook for calendar:" << calendarId; return false; } KCalendarCore::Event::Ptr event; if (recurrenceId.isValid()) { // this is a persistent occurrence for an already-existing series. qCDebug(lcSocialPlugin) << "Persistent occurrence added remotely:" << eventId; KCalendarCore::Event::Ptr parentEvent = allLocalEventsMap.value(parentId); if (parentEvent.isNull()) { // it might have been newly added in this sync cycle. Look for it from the calendar. QString parentEventUid = m_recurringEventIdToKCalUid.value(parentId); parentEvent = parentEventUid.isEmpty() ? parentEvent : m_calendar->event(parentEventUid, QDateTime()); } if (parentEvent.isNull()) { // construct a recurring parent series for this orphan qCInfo(lcSocialPlugin) << "Creating parent:" << parentId << "for orphaned event:" << eventId; parentEvent = addDummyParent(eventData, parentId, googleNotebook); if (!parentEvent) { return false; } m_recurringEventIdToKCalUid.insert(parentId, parentEvent->uid()); // Add to the local events map, in case there are future modifications in the same sync if (parentId.size()) { allLocalEventsMap.insert(parentId, parentEvent); } } // dissociate the persistent occurrence qCDebug(lcSocialPlugin) << "Dissociating exception from" << parentEvent->uid(); event = dissociateSingleOccurrence(parentEvent, recurrenceId); if (event.isNull()) { qCWarning(lcSocialPlugin) << "Could not dissociate occurrence from recurring event:" << parentId << recurrenceId.toString(); return false; } } else { // this is a new event in its own right. qCDebug(lcSocialPlugin) << "Event added remotely:" << eventId; event = KCalendarCore::Event::Ptr(new KCalendarCore::Event); // check to see if another Sailfish OS device uploaded this event. // if so, we want to use the same local UID it did. QString localUid = eventData.value(QLatin1String("extendedProperties")).toObject() .value(QLatin1String("private")).toObject() .value("x-jolla-sociald-mkcal-uid").toVariant().toString(); if (localUid.size()) { // either this event was uploaded by a different Sailfish OS device, // in which case we should re-use the uid it used; // or it was uploaded by this device from a different notebook, // and then the event was copied to a different calendar via // the Google web UI - in which case we need to use a different // uid as mkcal doesn't support a single event being stored in // multiple notebooks. m_storage->load(localUid); // the return value is useless, returns true even if count == 0 KCalendarCore::Event::Ptr checkLocalUidEvent = m_calendar->event(localUid, QDateTime()); if (!checkLocalUidEvent) { qCDebug(lcSocialPlugin) << "Event" << eventId << "was synced by another Sailfish OS device, reusing local uid:" << localUid; event->setUid(localUid); } } } bool changed = true; // set to true as it's an addition, no need to check for delta. jsonToKCal(eventData, event, m_serverCalendarIdToDefaultReminderTimes.value(calendarId), m_icalFormat, &changed); // direct conversion clampEventTimeToSync(event); qCDebug(lcSocialPlugin) << "Inserting event with new lastModified time: " << event->lastModified().toString(); if (!m_calendar->addEvent(event, googleNotebook->uid())) { qCWarning(lcSocialPlugin) << "Could not add dissociated occurrence to calendar:" << parentId << recurrenceId.toString(); return false; } m_recurringEventIdToKCalUid.insert(eventId, event->uid()); // Add to the local events map, in case there are future modifications in the same sync QString gcalId = gCalEventId(event); if (gcalId.isEmpty()) { gcalId = upsyncedUidMapping.value(event->uid()); } if (gcalId.size() && event) { allLocalEventsMap.insert(gcalId, event); } return true; } void GoogleCalendarSyncAdaptor::updateLocalCalendarNotebookEvents(const QString &calendarId) { QList > changesFromDownsyncForCalendar = m_changesFromDownsync.values(calendarId); QList > changesFromUpsyncForCalendar = m_changesFromUpsync.values(calendarId); if (changesFromDownsyncForCalendar.isEmpty() && changesFromUpsyncForCalendar.isEmpty()) { qCDebug(lcSocialPlugin) << "No remote changes to apply for calendar:" << calendarId << "for Google account:" << m_accountId; return; // no remote changes to apply. } // Set notebook writeable locally. mKCal::Notebook::Ptr googleNotebook = notebookForCalendarId(calendarId); if (!googleNotebook) { qCWarning(lcSocialPlugin) << "no local notebook associated with calendar:" << calendarId << "from account:" << m_accountId << "to update!"; m_syncSucceeded = false; return; } KCalendarCore::Incidence::List allLocalEventsList; m_storage->loadNotebookIncidences(googleNotebook->uid()); m_storage->allIncidences(&allLocalEventsList, googleNotebook->uid()); // write changes required to complete downsync to local database if (!changesFromDownsyncForCalendar.isEmpty()) { // build the partial-upsync-artifact mapping for this set of changes. QHash upsyncedUidMapping; for (int i = 0; i < changesFromDownsyncForCalendar.size(); ++i) { const QPair &remoteChange(changesFromDownsyncForCalendar[i]); QString gcalId = remoteChange.second.value(QLatin1String("id")).toVariant().toString(); QString upsyncedUid = remoteChange.second.value(QLatin1String("extendedProperties")).toObject() .value(QLatin1String("private")).toObject() .value("x-jolla-sociald-mkcal-uid").toVariant().toString(); if (!upsyncedUid.isEmpty() && !gcalId.isEmpty()) { upsyncedUidMapping.insert(upsyncedUid, gcalId); } } // build up map of gcalIds to local events for this change set QMap allLocalEventsMap; Q_FOREACH(const KCalendarCore::Incidence::Ptr incidence, allLocalEventsList) { if (incidence.isNull()) { continue; } KCalendarCore::Event::Ptr eventPtr = m_calendar->event(incidence->uid(), incidence->recurrenceId()); QString gcalId = gCalEventId(incidence); if (gcalId.isEmpty()) { gcalId = upsyncedUidMapping.value(incidence->uid()); } if (gcalId.size() && eventPtr) { allLocalEventsMap.insert(gcalId, eventPtr); } } // re-order remote changes so that additions of recurring series happen before additions of exception occurrences. // otherwise, the parent event may not exist when we attempt to insert the exception. // similarly, re-order remote deletions of exceptions so that they occur before remote deletions of series. // So we will have this ordering: // 1. Remote parent additions // 2. Remote exception deletions // 3. Remote exception additions // 4. Remote parent deletions QList > reorderedChangesFromDownsyncForCalendar; for (int i = 0; i < changesFromDownsyncForCalendar.size(); ++i) { const QPair &remoteChange(changesFromDownsyncForCalendar[i]); QString parentId = remoteChange.second.value(QLatin1String("recurringEventId")).toVariant().toString(); if (!parentId.isEmpty()) { if (remoteChange.first == GoogleCalendarSyncAdaptor::Delete || remoteChange.first == GoogleCalendarSyncAdaptor::DeleteOccurrence) { reorderedChangesFromDownsyncForCalendar.prepend(remoteChange); } else { reorderedChangesFromDownsyncForCalendar.append(remoteChange); } } } for (int i = 0; i < changesFromDownsyncForCalendar.size(); ++i) { const QPair &remoteChange(changesFromDownsyncForCalendar[i]); QString parentId = remoteChange.second.value(QLatin1String("recurringEventId")).toVariant().toString(); if (parentId.isEmpty()) { if (remoteChange.first == GoogleCalendarSyncAdaptor::Delete) { reorderedChangesFromDownsyncForCalendar.append(remoteChange); } else { reorderedChangesFromDownsyncForCalendar.prepend(remoteChange); } } } // apply the remote changes locally. for (int i = 0; i < reorderedChangesFromDownsyncForCalendar.size(); ++i) { const QPair &remoteChange(reorderedChangesFromDownsyncForCalendar[i]); const QJsonObject eventData(remoteChange.second); const QString eventId = eventData.value(QLatin1String("id")).toVariant().toString(); bool success = true; switch (remoteChange.first) { case GoogleCalendarSyncAdaptor::Delete: { // currently existing base event or persistent occurrence which needs deletion success = applyRemoteDelete(eventId, allLocalEventsMap); } break; case GoogleCalendarSyncAdaptor::DeleteOccurrence: { // this is a non-persistent occurrence, we need to add an EXDATE to the base event. success = applyRemoteDeleteOccurence(eventId, eventData, allLocalEventsMap); } break; case GoogleCalendarSyncAdaptor::Modify: { // An existing event was modified remotely success = applyRemoteModify(eventId, eventData, calendarId, allLocalEventsMap); } break; case GoogleCalendarSyncAdaptor::Insert: { // add a new local event for the remote addition. success = applyRemoteInsert(eventId, eventData, calendarId, upsyncedUidMapping, allLocalEventsMap); } break; default: break; } if (success) { flagUpdateSuccess(eventId); } else { m_syncSucceeded = false; } } } // write changes required to complete upsync to the local database for (int i = 0; i < changesFromUpsyncForCalendar.size(); ++i) { const QPair &remoteChange(changesFromUpsyncForCalendar[i]); KCalendarCore::Event::Ptr event(remoteChange.first); const QJsonObject eventData(remoteChange.second); // all changes are modifications to existing events, since it was an upsync response. bool changed = false; qCDebug(lcSocialPlugin) << "Updating event:" << event->summary(); jsonToKCal(eventData, event, m_serverCalendarIdToDefaultReminderTimes.value(calendarId), m_icalFormat, &changed); if (changed) { flagUpdateSuccess(event->uid()); qCDebug(lcSocialPlugin) << "Two-way calendar sync with account" << m_accountId << ": re-updating event:" << event->summary(); } } } void GoogleCalendarSyncAdaptor::clampEventTimeToSync(KCalendarCore::Event::Ptr event) const { if (event) { // Don't allow the event created time to fall after the sync time if (event->created() > m_syncedDateTime) { event->setCreated(m_syncedDateTime.addSecs(-1)); } // Don't allow the event last modified time to fall after the sync time if (event->lastModified() > m_syncedDateTime) { event->setLastModified(event->created()); } } } bool GoogleCalendarSyncAdaptor::isCleanSync(const QString &calendarId) const { if (m_serverCalendarIdToCalendarInfo.contains(calendarId)) { return (m_serverCalendarIdToCalendarInfo[calendarId].change == GoogleCalendarSyncAdaptor::CleanSync); } else { return false; } } QJsonObject GoogleCalendarSyncAdaptor::kCalToJson(KCalendarCore::Event::Ptr event, KCalendarCore::ICalFormat &icalFormat, bool setUidProperty) const { QString eventId = gCalEventId(event); QJsonObject start, end, originalStartTime; QString recurrenceId; QJsonArray attendees; const KCalendarCore::Attendee::List attendeesList = event->attendees(); if (!attendeesList.isEmpty()) { Q_FOREACH (auto att, attendeesList) { if (att.email().isEmpty()) { continue; } QJsonObject attendee; attendee.insert("email", att.email()); if (att.role() == KCalendarCore::Attendee::OptParticipant) { attendee.insert("optional", true); } const QString &name = att.name(); if (!name.isEmpty()) { attendee.insert("displayName", name); } attendees.append(attendee); } } // insert the date/time and timeZone information into the Json object. // note that timeZone is required for recurring events, for some reason. if (event->dtStart().time().isNull() || (event->allDay() && event->dtStart().time() == QTime(0,0,0))) { start.insert(QLatin1String("date"), QLocale::c().toString(event->dtStart().date(), QDATEONLY_FORMAT)); } else { start.insert(QLatin1String("dateTime"), event->dtStart().toString(Qt::ISODate)); start.insert(QLatin1String("timeZone"), QJsonValue(QString::fromUtf8(event->dtStart().timeZone().id()))); } if (event->dtEnd().time().isNull() || (event->allDay() && event->dtEnd().time() == QTime(0,0,0))) { // For all day events, the end date is exclusive, so we need to add 1 end.insert(QLatin1String("date"), QLocale::c().toString(event->dateEnd().addDays(1), QDATEONLY_FORMAT)); } else { end.insert(QLatin1String("dateTime"), event->dtEnd().toString(Qt::ISODate)); end.insert(QLatin1String("timeZone"), QJsonValue(QString::fromUtf8(event->dtEnd().timeZone().id()))); } if (event->hasRecurrenceId()) { // Kcal recurrence events share their parent's id, whereas Google gives them their own id // and stores the parent's id in the recurrenceId field. So we must find the parent's gCalId KCalendarCore::Event::Ptr parent = m_calendar->event(event->uid()); if (parent) { recurrenceId = gCalEventId(parent); } else { recurrenceId = eventId.contains('_') ? eventId.left(eventId.indexOf("_")) : eventId; qCDebug(lcSocialPlugin) << "Guessing recurrence gCalId" << recurrenceId << "from gCalId" << eventId; } originalStartTime.insert(QLatin1String("dateTime"), event->recurrenceId().toString(Qt::ISODate)); originalStartTime.insert(QLatin1String("timeZone"), QJsonValue(QString::fromUtf8(event->recurrenceId().timeZone().id()))); } QJsonObject retn; if (!eventId.isEmpty() && (eventId != recurrenceId)) { retn.insert(QLatin1String("id"), eventId); } if (event->recurrence()) { const QList exceptions = getExceptionInstanceDates(event); QJsonArray recArray = recurrenceArray(event, icalFormat, exceptions); if (recArray.size()) { retn.insert(QLatin1String("recurrence"), recArray); } } retn.insert(QLatin1String("summary"), event->summary()); retn.insert(QLatin1String("description"), event->description()); retn.insert(QLatin1String("location"), event->location()); retn.insert(QLatin1String("start"), start); retn.insert(QLatin1String("end"), end); retn.insert(QLatin1String("sequence"), QString::number(event->revision()+1)); if (!attendees.isEmpty()) { retn.insert(QLatin1String("attendees"), attendees); } if (!originalStartTime.isEmpty()) { retn.insert(QLatin1String("recurringEventId"), recurrenceId); retn.insert(QLatin1String("originalStartTime"), originalStartTime); } //retn.insert(QLatin1String("locked"), event->readOnly()); // only allow locking server-side. // we may wish to support locking/readonly from local side also, in the future. // if the event has no alarms associated with it, don't let Google add one automatically // otherwise, attempt to upsync the alarms as popup reminders. QJsonObject reminders; if (event->alarms().count()) { QJsonArray overrides; KCalendarCore::Alarm::List alarms = event->alarms(); for (int i = 0; i < alarms.count(); ++i) { // only upsync non-procedure alarms as popup reminders. QSet seenMinutes; if (alarms.at(i)->type() != KCalendarCore::Alarm::Procedure) { const int minutes = (alarms.at(i)->startOffset().asSeconds() / 60) * -1; if (!seenMinutes.contains(minutes)) { QJsonObject override; override.insert(QLatin1String("method"), QLatin1String("popup")); override.insert(QLatin1String("minutes"), minutes); overrides.append(override); seenMinutes.insert(minutes); } } } reminders.insert(QLatin1String("overrides"), overrides); } reminders.insert(QLatin1String("useDefault"), false); retn.insert(QLatin1String("reminders"), reminders); if (setUidProperty) { // now we store private extended properties: local uid. // this allows us to detect partially-upsynced artifacts during subsequent syncs. // usually this codepath will be hit for localAdditions being upsynced, // but sometimes also if we need to update the mapping due to clean-sync. QJsonObject privateExtendedProperties; privateExtendedProperties.insert(QLatin1String("x-jolla-sociald-mkcal-uid"), event->uid()); QJsonObject extendedProperties; extendedProperties.insert(QLatin1String("private"), privateExtendedProperties); retn.insert(QLatin1String("extendedProperties"), extendedProperties); } return retn; } void GoogleCalendarSyncAdaptor::flagUploadFailure(const QString &kcalEventId) { qCDebug(lcSocialPlugin) << "Setting upsync failure flag for:" << kcalEventId; m_eventSyncFlags.insert(kcalEventId, UploadFailure); } void GoogleCalendarSyncAdaptor::flagUploadSuccess(const QString &kcalEventId) { // Errors take precedence if (!m_eventSyncFlags.contains(kcalEventId)) { qCDebug(lcSocialPlugin) << "Setting upsync success flag for:" << kcalEventId; m_eventSyncFlags.insert(kcalEventId, NoSyncFailure); } } void GoogleCalendarSyncAdaptor::flagUpdateSuccess(const QString &kcalEventId) { // Errors take precedence if (!m_eventSyncFlags.contains(kcalEventId)) { qCDebug(lcSocialPlugin) << "Setting update success flag for:" << kcalEventId; m_eventSyncFlags.insert(kcalEventId, NoSyncFailure); } } void GoogleCalendarSyncAdaptor::flagDeleteFailure(const QString &kcalEventId) { qCDebug(lcSocialPlugin) << "Setting delete failure flag for:" << kcalEventId; m_eventSyncFlags.insert(kcalEventId, DeleteFailure); } void GoogleCalendarSyncAdaptor::applySyncFailureFlag(KCalendarCore::Event::Ptr event, SyncFailure flag) { const QString current = event->customProperty(VOLATILE_APP, VOLATILE_NAME); QString updated; switch (flag) { case UploadFailure: updated = QStringLiteral("upload"); break; case UpdateFailure: updated = QStringLiteral("update"); break; case DeleteFailure: updated = QStringLiteral("delete"); break; case NoSyncFailure: default: updated = QString(); break; } if (current != updated) { qCDebug(lcSocialPlugin) << "Changing flag from" << current << "to" << updated << "for" << event->uid(); if (!updated.isEmpty()) { event->setCustomProperty(VOLATILE_APP, VOLATILE_NAME, updated); } else { event->removeCustomProperty(VOLATILE_APP, VOLATILE_NAME); } m_storageNeedsSave = true; } } void GoogleCalendarSyncAdaptor::applySyncFailureFlags() { qCDebug(lcSocialPlugin) << "Applying sync failure flags for calendar"; // Iterate over the incidences with flags associated with them QMap::const_iterator iter = m_eventSyncFlags.constBegin(); while (iter != m_eventSyncFlags.constEnd()) { const QString &uid = iter.key(); const SyncFailure flag = iter.value(); KCalendarCore::Event::Ptr event = m_calendar->event(uid); if (!event) { // Load it if it wasn't already m_storage->load(uid); event = m_calendar->event(uid); } // Find the parent event if (event) { applySyncFailureFlag(event, flag); // Iterate over the exception instances of the parent const KCalendarCore::Event::List instances = m_calendar->eventInstances(event); for (KCalendarCore::Event::Ptr instance : instances) { applySyncFailureFlag(instance, flag); } } ++iter; } } buteo-sync-plugins-social-0.4.28/src/google/google-calendars/googlecalendarsyncadaptor.h000066400000000000000000000213141474572147200315020ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2013-2014 Jolla Ltd. ** Contact: Chris Adams ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef GOOGLECALENDARSYNCADAPTOR_H #define GOOGLECALENDARSYNCADAPTOR_H #include "googledatatypesyncadaptor.h" #include #include #include #include #include #include #include class GoogleCalendarSyncAdaptor : public GoogleDataTypeSyncAdaptor { Q_OBJECT public: GoogleCalendarSyncAdaptor(QObject *parent); ~GoogleCalendarSyncAdaptor(); QString syncServiceName() const; void sync(const QString &dataTypeString, int accountId); protected: // implementing GoogleDataTypeSyncAdaptor interface void purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode mode); void beginSync(int accountId, const QString &accessToken); void finalCleanup(); private: enum ChangeType { NoChange = 0, Insert = 1, Modify = 2, Delete = 3, DeleteOccurrence = 4, // used to identify downsynced status->CANCELLED changes only CleanSync = 5 // delete followed by insert. }; enum AccessRole { NoAccess = 0, FreeBusyReader = 1, Reader = 2, Writer = 3, Owner = 4 }; enum SyncFailure { NoSyncFailure, UploadFailure, UpdateFailure, DeleteFailure }; struct UpsyncChange { UpsyncChange() : upsyncType(NoChange) {} QString accessToken; ChangeType upsyncType; QString kcalNotebookId; QString kcalEventId; QDateTime recurrenceId; QString calendarId; QString eventId; QByteArray eventData; }; struct CalendarInfo { CalendarInfo() : change(NoChange), access(NoAccess) {} QString summary; QString description; QString color; ChangeType change; AccessRole access; }; void requestCalendars(const QString &accessToken, bool needCleanSync, const QString &pageToken = QString()); void requestEvents(const QString &accessToken, const QString &calendarId, const QString &syncToken, const QString &pageToken = QString()); void updateLocalCalendarNotebooks(const QString &accessToken, bool needCleanSync); QList determineSyncDelta(const QString &accessToken, const QString &calendarId, const QDateTime &since); void queueSequencedInsertion(QList &changesToUpsync, const KCalendarCore::Event::Ptr event, const QString &calendarId, const QString &accessToken); void reInsertWithRandomId(const QNetworkReply *reply); void upsyncChanges(const UpsyncChange &changeToUpsync); void applyRemoteChangesLocally(); void updateLocalCalendarNotebookEvents(const QString &calendarId); mKCal::Notebook::Ptr notebookForCalendarId(const QString &calendarId) const; void finishedRequestingRemoteEvents(const QString &accessToken, const QString &calendarId, const QString &syncToken, const QString &nextSyncToken, const QDateTime &since); void clampEventTimeToSync(KCalendarCore::Event::Ptr event) const; bool isCleanSync(const QString &calendarId) const; static void setCalendarProperties(mKCal::Notebook::Ptr notebook, const CalendarInfo &calendarInfo, const QString &serverCalendarId, int accountId, const QString &syncProfile, const QString &ownerEmail); const QList getExceptionInstanceDates(const KCalendarCore::Event::Ptr event) const; QJsonObject kCalToJson(KCalendarCore::Event::Ptr event, KCalendarCore::ICalFormat &icalFormat, bool setUidProperty = false) const; void handleErrorReply(QNetworkReply *reply); void handleDeleteReply(QNetworkReply *reply); void handleInsertModifyReply(QNetworkReply *reply); void performSequencedUpsyncs(const QNetworkReply *reply); KCalendarCore::Event::Ptr addDummyParent(const QJsonObject &eventData, const QString &parentId, const mKCal::Notebook::Ptr googleNotebook); bool applyRemoteDelete(const QString &eventId, QMap &allLocalEventsMap); bool applyRemoteDeleteOccurence(const QString &eventId, const QJsonObject &eventData, QMap &allLocalEventsMap); bool applyRemoteModify(const QString &eventId, const QJsonObject &eventData, const QString &calendarId, QMap &allLocalEventsMap); bool applyRemoteInsert(const QString &eventId, const QJsonObject &eventData, const QString &calendarId, const QHash &upsyncedUidMapping, QMap &allLocalEventsMap); void flagUploadFailure(const QString &kcalEventId); void flagUploadSuccess(const QString &kcalEventId); void flagUpdateSuccess(const QString &kcalEventId); void flagDeleteFailure(const QString &kcalEventId); void applySyncFailureFlag(KCalendarCore::Event::Ptr event, SyncFailure flag); void applySyncFailureFlags(); private Q_SLOTS: void calendarsFinishedHandler(); void eventsFinishedHandler(); void upsyncFinishedHandler(); private: QMap m_serverCalendarIdToCalendarInfo; QMap m_serverCalendarIdToDefaultReminderTimes; QMultiMap m_calendarIdToEventObjects; QMap m_recurringEventIdToKCalUid; bool m_syncSucceeded; int m_accountId; QStringList m_calendarsBeingRequested; // calendarIds QStringList m_calendarsFinishedRequested; // calendarId to updated timestamp string QMap m_calendarsThisSyncTokens; // calendarId to sync token used during this sync cycle QMap m_calendarsNextSyncTokens; // calendarId to sync token to use during next sync cycle QMap m_calendarsSyncDate; // calendarId to since date to use when determining delta QMultiMap > m_changesFromDownsync; // calendarId to change QMultiMap > m_changesFromUpsync; // calendarId to event+upsyncResponse QSet m_syncTokenFailure; // calendarIds suffering from 410 error due to invalid sync token QSet m_timeMinFailure; // calendarIds suffering from 410 error due to invalid timeMin value QMap m_purgeList; // notebookIds to local deleted incidences that can be purged QMap m_deletedGcalIdToIncidence; mKCal::ExtendedCalendar::Ptr m_calendar; mKCal::ExtendedStorage::Ptr m_storage; mutable KCalendarCore::ICalFormat m_icalFormat; bool m_storageNeedsSave; QDateTime m_syncedDateTime; // Sequenced upsync changes are referenced by the gcalId of the // parent upsync, as recorded in UpsyncChange::eventId QMultiHash m_sequenced; int m_collisionErrorCount; QMap m_eventSyncFlags; }; #endif // GOOGLECALENDARSYNCADAPTOR_H buteo-sync-plugins-social-0.4.28/src/google/google-common.pri000066400000000000000000000001611474572147200241600ustar00rootroot00000000000000INCLUDEPATH += $$PWD SOURCES += $$PWD/googledatatypesyncadaptor.cpp HEADERS += $$PWD/googledatatypesyncadaptor.h buteo-sync-plugins-social-0.4.28/src/google/google-contacts/000077500000000000000000000000001474572147200237745ustar00rootroot00000000000000buteo-sync-plugins-social-0.4.28/src/google/google-contacts/google-contacts.pri000066400000000000000000000006761474572147200276110ustar00rootroot00000000000000CONFIG += link_pkgconfig PKGCONFIG += Qt5Contacts qtcontacts-sqlite-qt5-extensions QT += gui SOURCES += \ $$PWD/googletwowaycontactsyncadaptor.cpp \ $$PWD/googlepeopleapi.cpp \ $$PWD/googlepeoplejson.cpp \ $$PWD/googlecontactimagedownloader.cpp HEADERS += \ $$PWD/googletwowaycontactsyncadaptor.h \ $$PWD/googlepeopleapi.h \ $$PWD/googlepeoplejson.h \ $$PWD/googlecontactimagedownloader.h INCLUDEPATH += $$PWD buteo-sync-plugins-social-0.4.28/src/google/google-contacts/google-contacts.pro000066400000000000000000000012761474572147200276140ustar00rootroot00000000000000TARGET = google-contacts-client DEFINES += SOCIALD_USE_QTPIM include($$PWD/../../common.pri) include($$PWD/../google-common.pri) include($$PWD/google-contacts.pri) google_contacts_sync_profile.path = /etc/buteo/profiles/sync google_contacts_sync_profile.files = $$PWD/google.Contacts.xml google_contacts_client_plugin_xml.path = /etc/buteo/profiles/client google_contacts_client_plugin_xml.files = $$PWD/google-contacts.xml HEADERS += googlecontactsplugin.h SOURCES += googlecontactsplugin.cpp OTHER_FILES += \ google_contacts_sync_profile.files \ google_contacts_client_plugin_xml.files INSTALLS += \ target \ google_contacts_sync_profile \ google_contacts_client_plugin_xml buteo-sync-plugins-social-0.4.28/src/google/google-contacts/google-contacts.xml000066400000000000000000000002061474572147200276040ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/google/google-contacts/google.Contacts.xml000066400000000000000000000011351474572147200275470ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/google/google-contacts/googlecontactimagedownloader.cpp000066400000000000000000000046371474572147200324240ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2014 Jolla Ltd. ** Contact: Chris Adams ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "googlecontactimagedownloader.h" #include #include #include static const char *IMAGE_DOWNLOADER_TOKEN_KEY = "url"; static const char *IMAGE_DOWNLOADER_IDENTIFIER_KEY = "identifier"; GoogleContactImageDownloader::GoogleContactImageDownloader() : AbstractImageDownloader() { } QString GoogleContactImageDownloader::staticOutputFile(const QString &identifier, const QUrl &url) { return makeUrlOutputFile(SocialSyncInterface::Google, SocialSyncInterface::Contacts, identifier, url.toString(), QString()); } QNetworkReply * GoogleContactImageDownloader::createReply(const QString &url, const QVariantMap &metadata) { Q_D(AbstractImageDownloader); QString accessToken = metadata.value(IMAGE_DOWNLOADER_TOKEN_KEY).toString(); QNetworkRequest request(url); request.setRawHeader(QString(QLatin1String("Authorization")).toUtf8(), QString(QLatin1String("Bearer ") + accessToken).toUtf8()); return d->networkAccessManager->get(request); } QString GoogleContactImageDownloader::outputFile(const QString &url, const QVariantMap &data, const QString &mimeType) const { Q_UNUSED(mimeType); // TODO: use return staticOutputFile(data.value(IMAGE_DOWNLOADER_IDENTIFIER_KEY).toString(), url); } buteo-sync-plugins-social-0.4.28/src/google/google-contacts/googlecontactimagedownloader.h000066400000000000000000000034561474572147200320670ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2014 Jolla Ltd. ** Contact: Chris Adams ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef GOOGLECONTACTIMAGEDOWNLOADER_H #define GOOGLECONTACTIMAGEDOWNLOADER_H #include #include #include #include #include #include class QNetworkReply; class GoogleContactImageDownloader: public AbstractImageDownloader { Q_OBJECT public: explicit GoogleContactImageDownloader(); static QString staticOutputFile(const QString &identifier, const QUrl &url); protected: QNetworkReply * createReply(const QString &url, const QVariantMap &metadata); // This is a reimplemented method, used by AbstractImageDownloader QString outputFile(const QString &url, const QVariantMap &data, const QString &mimeType) const override; private: Q_DECLARE_PRIVATE(AbstractImageDownloader) }; #endif // GOOGLECONTACTIMAGEDOWNLOADER_H buteo-sync-plugins-social-0.4.28/src/google/google-contacts/googlecontactsplugin.cpp000066400000000000000000000040061474572147200307320ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2014 - 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "constants_p.h" #include #include #include "googlecontactsplugin.h" #include "googletwowaycontactsyncadaptor.h" #include "socialnetworksyncadaptor.h" #include GoogleContactsPlugin::GoogleContactsPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *callbackInterface) : SocialdButeoPlugin(pluginName, profile, callbackInterface, QStringLiteral("google"), SocialNetworkSyncAdaptor::dataTypeName(SocialNetworkSyncAdaptor::Contacts)) { } GoogleContactsPlugin::~GoogleContactsPlugin() { } SocialNetworkSyncAdaptor *GoogleContactsPlugin::createSocialNetworkSyncAdaptor() { return new GoogleTwoWayContactSyncAdaptor(this); } Buteo::ClientPlugin* GoogleContactsPluginLoader::createClientPlugin( const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) { return new GoogleContactsPlugin(pluginName, profile, cbInterface); } buteo-sync-plugins-social-0.4.28/src/google/google-contacts/googlecontactsplugin.h000066400000000000000000000036011474572147200303770ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2014 - 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef GOOGLECONTACTSPLUGIN_H #define GOOGLECONTACTSPLUGIN_H #include "socialdbuteoplugin.h" #include class Q_DECL_EXPORT GoogleContactsPlugin : public SocialdButeoPlugin { Q_OBJECT public: GoogleContactsPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *cbInterface); ~GoogleContactsPlugin(); protected: SocialNetworkSyncAdaptor *createSocialNetworkSyncAdaptor(); }; class GoogleContactsPluginLoader : public Buteo::SyncPluginLoader { Q_OBJECT Q_PLUGIN_METADATA(IID "org.sailfishos.plugins.sync.GoogleContactsPluginLoader") Q_INTERFACES(Buteo::SyncPluginLoader) public: Buteo::ClientPlugin* createClientPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) override; }; #endif // GOOGLECONTACTSPLUGIN_H buteo-sync-plugins-social-0.4.28/src/google/google-contacts/googlepeopleapi.cpp000066400000000000000000000465471474572147200276730ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "googlepeopleapi.h" #include "trace.h" #include #include #include #include #include #include #include #include #include #include #include namespace { const QString ContentIdCreateContact = QStringLiteral("CreateContact:"); const QString ContentIdUpdateContact = QStringLiteral("UpdateContact:"); const QString ContentIdDeleteContact = QStringLiteral("DeleteContact:"); const QString ContentIdAddContactPhoto = QStringLiteral("AddContactPhoto:"); const QString ContentIdUpdateContactPhoto = QStringLiteral("UpdateContactPhoto:"); const QString ContentIdDeleteContactPhoto = QStringLiteral("DeleteContactPhoto:"); const int MaximumAvatarWidth = 512; template QList jsonArrayToList(const QJsonArray &array) { QList values; for (auto it = array.constBegin(); it != array.constEnd(); ++it) { values.append(T::fromJsonObject(it->toObject())); } return values; } QJsonObject parseJsonObject(const QByteArray &data) { QJsonParseError err; QJsonDocument doc = QJsonDocument::fromJson(data, &err); if (err.error != QJsonParseError::NoError) { qCWarning(lcSocialPlugin) << "JSON parse error:" << err.errorString(); return QJsonObject(); } return doc.object(); } QFile *newResizedImageFile(const QString &imagePath, int maxWidth) { QImage image; if (!image.load(imagePath)) { qCWarning(lcSocialPlugin) << "Unable to load image file:" << imagePath; return nullptr; } const QByteArray fileSuffix = QFileInfo(imagePath).suffix().toUtf8(); if (image.size().width() < maxWidth) { return nullptr; } QTemporaryFile *temp = new QTemporaryFile; image = image.scaledToWidth(maxWidth); temp->setFileTemplate(imagePath); if (temp->open() && image.save(temp->fileName(), fileSuffix.data())) { temp->seek(0); return temp; } else { qCWarning(lcSocialPlugin) << "Unable to save resized image to file:" << temp->fileName(); } delete temp; return nullptr; } bool writePhotoUpdateBody(QJsonObject *jsonObject, const QContactAvatar &avatar) { if (!avatar.imageUrl().isLocalFile()) { qCWarning(lcSocialPlugin) << "Cannot open non-local avatar file:" << avatar.imageUrl(); return false; } // Reduce the avatar size to minimize the uploaded data. const QString avatarFileName = avatar.imageUrl().toLocalFile(); QFile *imageFile = newResizedImageFile(avatarFileName, MaximumAvatarWidth); if (!imageFile) { imageFile = new QFile(avatarFileName); if (!imageFile->open(QFile::ReadOnly)) { qCWarning(lcSocialPlugin) << "Unable to open avatar file:" << imageFile->fileName(); delete imageFile; return false; } } jsonObject->insert("photoBytes", QString::fromLatin1(imageFile->readAll().toBase64())); delete imageFile; return true; } QString contentIdForContactOperation(GooglePeopleApi::OperationType operationType, const QContact &contact) { static const QMap contentIdPrefixes = { { GooglePeopleApi::CreateContact, ContentIdCreateContact }, { GooglePeopleApi::UpdateContact, ContentIdUpdateContact }, { GooglePeopleApi::DeleteContact, ContentIdDeleteContact }, { GooglePeopleApi::AddContactPhoto, ContentIdAddContactPhoto }, { GooglePeopleApi::UpdateContactPhoto, ContentIdUpdateContactPhoto }, { GooglePeopleApi::DeleteContactPhoto, ContentIdDeleteContactPhoto }, }; const QString idPrefix = contentIdPrefixes.value(operationType); if (idPrefix.isEmpty()) { qCWarning(lcSocialPlugin) << "contentIdForOperationType(): invalid operation type!"; return QString(); } return QString("Content-ID: %1%2\n").arg(idPrefix).arg(contact.id().toString()); } void addPartHeaderForContactOperation(QByteArray *bytes, GooglePeopleApi::OperationType operationType, const QContact &contact) { bytes->append("\n" "--batch_people\n" "Content-Type: application/http\n" "Content-Transfer-Encoding: binary\n"); bytes->append(contentIdForContactOperation(operationType, contact).toUtf8()); bytes->append("\n"); } } GooglePeopleApiRequest::GooglePeopleApiRequest(const QString &accessToken) : m_accessToken(accessToken) { } GooglePeopleApiRequest::~GooglePeopleApiRequest() { } QByteArray GooglePeopleApiRequest::writeMultiPartRequest(const QMap > &batch) { QByteArray bytes; bool hasContent = false; static const QString supportedPersonFieldList = GooglePeople::Person::supportedPersonFields().join(','); // Encode each multi-part request into the overall request. // Each part contains a Content-ID that indicates the request type, to assist in parsing the // response when it is received. for (auto it = batch.constBegin(); it != batch.constEnd(); ++it) { switch (it.key()) { case GooglePeopleApi::UnsupportedOperation: qCWarning(lcSocialPlugin) << "Invalid operation type in multi-part batch"; break; case GooglePeopleApi::CreateContact: { for (const QContact &contact : it.value()) { const QJsonObject jsonObject = GooglePeople::Person::contactToJsonObject(contact); if (jsonObject.isEmpty()) { qCWarning(lcSocialPlugin) << "No contact data found for contact:" << contact.id(); } else { addPartHeaderForContactOperation(&bytes, it.key(), contact); const QByteArray body = "\n" + QJsonDocument(jsonObject).toJson(); bytes += QString("POST /v1/people:createContact?personFields=%1 HTTP/1.1\n") .arg(supportedPersonFieldList); bytes += "Content-Type: application/json\n"; bytes += QString("Content-Length: %1\n").arg(body.size()).toLatin1(); bytes += "Accept: application/json\n"; bytes += body; bytes += "\n"; hasContent = true; } } break; } case GooglePeopleApi::UpdateContact: { for (const QContact &contact : it.value()) { QStringList updatedPersonFieldList; const QJsonObject jsonObject = GooglePeople::Person::contactToJsonObject( contact, &updatedPersonFieldList); if (updatedPersonFieldList.isEmpty()) { qCInfo(lcSocialPlugin) << "No non-avatar fields have changed in contact:" << contact.id(); } else if (jsonObject.isEmpty()) { qCWarning(lcSocialPlugin) << "No contact data found for contact:" << contact.id(); } else { addPartHeaderForContactOperation(&bytes, it.key(), contact); const QByteArray body = "\n" + QJsonDocument(jsonObject).toJson(); bytes += QString("PATCH /v1/%1:updateContact?updatePersonFields=%2&personFields=%3 HTTP/1.1\n") .arg(GooglePeople::Person::personResourceName(contact)) .arg(updatedPersonFieldList.join(',')) .arg(supportedPersonFieldList); bytes += "Content-Type: application/json\n"; bytes += QString("Content-Length: %1\n").arg(body.size()).toLatin1(); bytes += "Accept: application/json\n"; bytes += body; bytes += "\n"; hasContent = true; } } break; } case GooglePeopleApi::DeleteContact: { for (const QContact &contact : it.value()) { addPartHeaderForContactOperation(&bytes, it.key(), contact); bytes += QString("DELETE /v1/%1:deleteContact HTTP/1.1\n") .arg(GooglePeople::Person::personResourceName(contact)); bytes += "Content-Type: application/json\n"; bytes += "Accept: application/json\n"; bytes += "\n"; hasContent = true; } break; } case GooglePeopleApi::AddContactPhoto: case GooglePeopleApi::UpdateContactPhoto: { for (const QContact &contact : it.value()) { const QContactAvatar avatar = GooglePeople::Photo::getPrimaryPhoto(contact); if (avatar.imageUrl().isEmpty()) { qCWarning(lcSocialPlugin) << "No avatar found in contact:" << contact; continue; } QJsonObject jsonObject; if (!writePhotoUpdateBody(&jsonObject, avatar)) { qCWarning(lcSocialPlugin) << "Failed to write avatar update details:" << avatar.imageUrl(); continue; } jsonObject.insert("personFields", supportedPersonFieldList); addPartHeaderForContactOperation(&bytes, it.key(), contact); const QByteArray body = "\n" + QJsonDocument(jsonObject).toJson(); bytes += QString("PATCH /v1/%1:updateContactPhoto HTTP/1.1\n") .arg(GooglePeople::Person::personResourceName(contact)); bytes += "Content-Type: application/json\n"; bytes += QString("Content-Length: %1\n").arg(body.size()).toLatin1(); bytes += "Accept: application/json\n"; bytes += body; bytes += "\n"; hasContent = true; } break; } case GooglePeopleApi::DeleteContactPhoto: { for (const QContact &contact : it.value()) { addPartHeaderForContactOperation(&bytes, it.key(), contact); bytes += QString("DELETE /v1/%1:deleteContactPhoto?personFields=%2 HTTP/1.1\n") .arg(GooglePeople::Person::personResourceName(contact)) .arg(supportedPersonFieldList); bytes += QString("Content-ID: %1%2\n") .arg(ContentIdDeleteContactPhoto) .arg(contact.id().toString()).toUtf8(); bytes += "Accept: application/json\n"; bytes += "\n"; hasContent = true; } break; } } } if (!hasContent) { return QByteArray(); } bytes += "--batch_people--\n\n"; return bytes; } //----------- void GooglePeopleApiResponse::BatchResponsePart::reset() { contentType.clear(); contentId.clear(); bodyStatusLine.clear(); bodyContentType.clear(); body.clear(); } void GooglePeopleApiResponse::BatchResponsePart::parse( GooglePeopleApi::OperationType *operationType, QString *contactId, GooglePeople::Person *person, Error *error) const { static const QString responseToken = QStringLiteral("response-"); if (!responseToken.startsWith(responseToken)) { qCWarning(lcSocialPlugin) << "Unexpected content ID in response:" << contentId; return; } const QString operationInfo = contentId.mid(responseToken.length()); static const QMap operationTypes = { { ContentIdCreateContact, GooglePeopleApi::CreateContact }, { ContentIdUpdateContact, GooglePeopleApi::UpdateContact }, { ContentIdDeleteContact, GooglePeopleApi::DeleteContact }, { ContentIdAddContactPhoto, GooglePeopleApi::AddContactPhoto }, { ContentIdUpdateContactPhoto, GooglePeopleApi::UpdateContactPhoto }, { ContentIdDeleteContactPhoto, GooglePeopleApi::DeleteContactPhoto }, }; *operationType = GooglePeopleApi::UnsupportedOperation; for (auto it = operationTypes.constBegin(); it != operationTypes.constEnd(); ++it) { if (operationInfo.startsWith(it.key())) { *operationType = it.value(); *contactId = operationInfo.mid(it.key().length()); break; } } const QJsonObject jsonBody = parseJsonObject(body); const QJsonObject errorObject = jsonBody.value("error").toObject(); if (!errorObject.isEmpty()) { error->code = errorObject.value("code").toInt(); error->message = errorObject.value("message").toString(); error->status = errorObject.value("status").toString(); } else { switch (*operationType) { case GooglePeopleApi::CreateContact: case GooglePeopleApi::UpdateContact: // The JSON response is a Person object *person = GooglePeople::Person::fromJsonObject(jsonBody); break; case GooglePeopleApi::AddContactPhoto: case GooglePeopleApi::UpdateContactPhoto: case GooglePeopleApi::DeleteContactPhoto: { // The JSON response contains a "person" value that is a Person object *person = GooglePeople::Person::fromJsonObject(jsonBody.value("person").toObject()); break; } case GooglePeopleApi::DeleteContact: // JSON response is empty. break; case GooglePeopleApi::UnsupportedOperation: break; } } } //----------- void GooglePeopleApiResponse::PeopleConnectionsListResponse::getContacts( int accountId, const QList &candidateCollections, QList *addedOrModified, QList *deleted) const { for (const GooglePeople::Person &person : connections) { if (person.metadata.deleted) { if (deleted) { deleted->append(person.toContact(accountId, candidateCollections)); } } else if (addedOrModified) { addedOrModified->append(person.toContact(accountId, candidateCollections)); } } } bool GooglePeopleApiResponse::readResponse( const QByteArray &data, GooglePeopleApiResponse::ContactGroupsResponse *response) { if (!response) { return false; } const QJsonObject object = parseJsonObject(data); response->contactGroups = jsonArrayToList(object.value("contactGroups").toArray()); response->totalItems = object.value("totalItems").toString().toInt(); response->nextPageToken = object.value("nextPageToken").toString(); response->nextSyncToken = object.value("nextSyncToken").toString(); return true; } bool GooglePeopleApiResponse::readResponse( const QByteArray &data, GooglePeopleApiResponse::PeopleConnectionsListResponse *response) { if (!response) { return false; } const QJsonObject object = parseJsonObject(data); response->connections = jsonArrayToList(object.value("connections").toArray()); response->nextPageToken = object.value("nextPageToken").toString(); response->nextSyncToken = object.value("nextSyncToken").toString(); response->totalPeople = object.value("totalPeople").toString().toInt(); response->totalItems = object.value("totalItems").toString().toInt(); return true; } bool GooglePeopleApiResponse::readMultiPartResponse( const QByteArray &data, QList *responseParts) { if (!responseParts) { return false; } QBuffer buffer; buffer.setData(data); if (!buffer.open(QIODevice::ReadOnly)) { return false; } /* Example multi-part response body: --batch_izedEXuDWnLH5_41NeoKptxfL5sqA2K6 Content-Type: application/http Content-ID: response- HTTP/1.1 200 OK Content-Type: application/json; charset=UTF-8 Vary: Origin Vary: X-Origin Vary: Referer { // json body of created contact } --batch_izedEXuDWnLH5_41NeoKptxfL5sqA2K6 Content-Type: application/http Content-ID: response- HTTP/1.1 400 Bad Request Vary: Origin Vary: X-Origin Vary: Referer Content-Type: application/json; charset=UTF-8 { "error": { "code": 400, "message": "Request person.etag is different than the current person.etag. Clear local cache and get the latest person.", "status": "FAILED_PRECONDITION" } } --batch_izedEXuDWnLH5_41NeoKptxfL5sqA2K6-- */ enum PartParseStatus { ParseHeaders, ParseBodyHeaders, ParseBody }; BatchResponsePart currentPart; PartParseStatus parseStatus = ParseHeaders; static const QByteArray contentTypeToken = "Content-Type:"; static const QByteArray contentIdToken = "Content-ID:"; while (!buffer.atEnd()) { const QByteArray line = buffer.readLine(); const bool isSeparator = line.startsWith("--batch_"); if (parseStatus == ParseHeaders) { // Parse the headers for this part. if (isSeparator) { continue; } else if (line.startsWith(contentTypeToken)) { currentPart.contentType = QString::fromUtf8(line.mid(contentTypeToken.length() + 1).trimmed()); } else if (line.startsWith(contentIdToken)) { currentPart.contentId = QString::fromUtf8(line.mid(contentIdToken.length() + 1).trimmed()); } else if (line.trimmed().isEmpty() && !currentPart.contentType.isEmpty()) { parseStatus = ParseBodyHeaders; } } else if (parseStatus == ParseBodyHeaders) { // Parse the body of this part, which itself contains a separate HTTP response with // headers and body. if (line.startsWith("HTTP/")) { currentPart.bodyStatusLine = line.trimmed(); } else if (line.startsWith(contentTypeToken)) { currentPart.bodyContentType = QString::fromUtf8(line.mid(contentTypeToken.length() + 1).trimmed()); } else if (line.trimmed().isEmpty() && !currentPart.bodyContentType.isEmpty()) { parseStatus = ParseBody; } } else if (parseStatus == ParseBody) { if (isSeparator) { // This is the start of another part, or the end of the batch. responseParts->append(currentPart); currentPart.reset(); parseStatus = ParseHeaders; if (line.endsWith("--")) { break; } } else { currentPart.body += line; } } } return true; } buteo-sync-plugins-social-0.4.28/src/google/google-contacts/googlepeopleapi.h000066400000000000000000000062671474572147200273330ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef GOOGLEPEOPLEAPI_H #define GOOGLEPEOPLEAPI_H #include "googlepeoplejson.h" #include #include QTCONTACTS_USE_NAMESPACE namespace GooglePeopleApi { enum OperationType { UnsupportedOperation, CreateContact, UpdateContact, DeleteContact, AddContactPhoto, UpdateContactPhoto, DeleteContactPhoto }; } class GooglePeopleApiRequest { public: GooglePeopleApiRequest(const QString &accessToken); ~GooglePeopleApiRequest(); static QByteArray writeMultiPartRequest(const QMap > &batch); private: QString m_accessToken; }; class GooglePeopleApiResponse { public: class PeopleConnectionsListResponse { public: QList connections; QString nextPageToken; QString nextSyncToken; int totalPeople = 0; int totalItems = 0; void getContacts(int accountId, const QList &candidateCollections, QList *addedOrModified, QList *deleted) const; }; class ContactGroupsResponse { public: // Note: for this response, memberResourceNames of each group are not populated QList contactGroups; int totalItems = 0; QString nextPageToken; QString nextSyncToken; }; class BatchResponsePart { public: struct Error { int code; QString message; QString status; }; QString contentType; QString contentId; QString bodyStatusLine; QString bodyContentType; QByteArray body; void reset(); void parse(GooglePeopleApi::OperationType *operationType, QString *contactId, GooglePeople::Person *person, Error *error) const; }; static bool readResponse(const QByteArray &data, ContactGroupsResponse *response); static bool readResponse(const QByteArray &data, PeopleConnectionsListResponse *response); static bool readMultiPartResponse(const QByteArray &data, QList *responseParts); }; #endif // GOOGLEPEOPLEAPI_H buteo-sync-plugins-social-0.4.28/src/google/google-contacts/googlepeoplejson.cpp000066400000000000000000001431611474572147200300610ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "googlepeoplejson.h" #include "googlecontactimagedownloader.h" #include "trace.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { const QString StarredContactGroupName = QStringLiteral("contactGroups/starred"); QDate jsonObjectToDate(const QJsonObject &object, bool *ok) { const int year = object.value("year").toInt(); const int month = object.value("month").toInt(); const int day = object.value("day").toInt(); QDate date(year, month, day); if (!date.isValid()) { qCWarning(lcSocialPlugin) << "Cannot read date from JSON:" << object; } if (ok) { *ok = date.isValid(); } return date; } QJsonObject jsonObjectFromDate(const QDate &date) { QJsonObject object; if (date.isValid()) { object.insert("year", date.year()); object.insert("month", date.month()); object.insert("day", date.day()); } return object; } template QList jsonArrayToList(const QJsonArray &array) { QList values; for (auto it = array.constBegin(); it != array.constEnd(); ++it) { bool error = false; const T &value = T::fromJsonObject(it->toObject(), &error); if (!error) { values.append(value); } } return values; } template void addJsonValuesForContact(const QString &propertyName, const QContact &contact, QJsonObject *object, QStringList *addedFields) { bool hasChanges = false; const QJsonArray array = T::jsonValuesForContact(contact, &hasChanges); if (!hasChanges) { return; } object->insert(propertyName, array); if (addedFields) { addedFields->append(propertyName); } } bool saveContactExtendedDetail(QContact *contact, const QString &detailName, const QVariant &detailData) { QContactExtendedDetail matchedDetail; for (const QContactExtendedDetail &detail : contact->details()) { if (detail.name() == detailName) { matchedDetail = detail; break; } } if (matchedDetail.name().isEmpty()) { matchedDetail.setName(detailName); } matchedDetail.setData(detailData); return contact->saveDetail(&matchedDetail, QContact::IgnoreAccessConstraints); } QVariant contactExtendedDetail(const QContact &contact, const QString &detailName) { for (const QContactExtendedDetail &detail : contact.details()) { if (detail.name() == detailName) { return detail.data(); } } return QVariant(); } bool saveContactDetail(QContact *contact, QContactDetail *detail) { detail->setValue(QContactDetail__FieldModifiable, true); return contact->saveDetail(detail, QContact::IgnoreAccessConstraints); } template bool removeDetails(QContact *contact) { QList details = contact->details(); for (int i = 0; i < details.count(); ++i) { T *detail = &details[i]; if (!contact->removeDetail(detail)) { qCWarning(lcSocialPlugin) << "Unable to remove detail:" << detail; return false; } } return true; } bool shouldAddDetailChanges(const QContactDetail &detail, bool *hasChanges) { const int changeFlags = detail.value(QContactDetail__FieldChangeFlags).toInt(); if (changeFlags == 0) { return false; } *hasChanges = true; if (changeFlags & QContactDetail__ChangeFlag_IsDeleted) { return false; } // Detail was added or modified return true; } } GooglePeople::Source GooglePeople::Source::fromJsonObject(const QJsonObject &object, bool *) { Source ret; ret.type = object.value("type").toString(); ret.id = object.value("id").toString(); ret.etag = object.value("etag").toString(); return ret; } GooglePeople::FieldMetadata GooglePeople::FieldMetadata::fromJsonObject(const QJsonObject &object) { FieldMetadata ret; ret.primary = object.value("primary").toBool(); ret.verified = object.value("verified").toBool(); ret.source = Source::fromJsonObject(object.value("source").toObject()); return ret; } bool GooglePeople::Address::saveContactDetails(QContact *contact, const QList
&values) { removeDetails(contact); for (const Address &address : values) { QList contexts; if (address.type == QStringLiteral("home")) { contexts.append(QContactDetail::ContextHome); } else if (address.type == QStringLiteral("work")) { contexts.append(QContactDetail::ContextWork); } else if (address.type == QStringLiteral("other")) { contexts.append(QContactDetail::ContextOther); } else { // address.type is a custom type, so ignore it. If the user does not change it to a // known type, the type will not be upsynced and the custom type will be preserved. } QContactAddress detail; if (!contexts.isEmpty()) { detail.setContexts(contexts); } detail.setStreet(address.streetAddress); detail.setPostOfficeBox(address.poBox); detail.setLocality(address.city); detail.setRegion(address.region); detail.setPostcode(address.postalCode); detail.setCountry(address.country); if (!saveContactDetail(contact, &detail)) { return false; } } return true; } GooglePeople::Address GooglePeople::Address::fromJsonObject(const QJsonObject &obj, bool *) { Address ret; ret.metadata = FieldMetadata::fromJsonObject(obj.value("metadata").toObject()); ret.formattedValue = obj.value("formattedValue").toString(); ret.type = obj.value("type").toString(); ret.formattedType = obj.value("formattedType").toString(); ret.poBox = obj.value("poBox").toString(); ret.streetAddress = obj.value("streetAddress").toString(); ret.extendedAddress = obj.value("extendedAddress").toString(); ret.city = obj.value("city").toString(); ret.region = obj.value("region").toString(); ret.postalCode = obj.value("postalCode").toString(); ret.country = obj.value("country").toString(); ret.countryCode = obj.value("countryCode").toString(); return ret; } QJsonArray GooglePeople::Address::jsonValuesForContact(const QContact &contact, bool *hasChanges) { QJsonArray array; const QList details = contact.details(); for (int i = 0; i < details.count(); ++i) { const QContactAddress &detail = details.at(i); if (!shouldAddDetailChanges(detail, hasChanges)) { continue; } const int context = detail.contexts().value(0, -1); QString type; switch (context) { case QContactDetail::ContextHome: type = QStringLiteral("home"); break; case QContactDetail::ContextWork: type = QStringLiteral("work"); break; case QContactDetail::ContextOther: type = QStringLiteral("other"); break; } QJsonObject address; if (type.isEmpty()) { // No type set, or the Google field had a custom type set, so don't overwrite it. } else { address.insert("type", type); } address.insert("poBox", detail.postOfficeBox()); address.insert("streetAddress", detail.street()); address.insert("city", detail.locality()); address.insert("region", detail.region()); address.insert("postalCode", detail.postcode()); address.insert("country", detail.country()); array.append(address); } return array; } bool GooglePeople::Biography::saveContactDetails(QContact *contact, const QList &values) { // Only one biography allowed in a Google contact. if (values.isEmpty()) { return true; } QContactNote detail = contact->detail(); detail.setNote(values.at(0).value); return saveContactDetail(contact, &detail); } GooglePeople::Biography GooglePeople::Biography::fromJsonObject(const QJsonObject &object, bool *) { Biography ret; ret.metadata = FieldMetadata::fromJsonObject(object.value("metadata").toObject()); ret.value = object.value("value").toString(); return ret; } QJsonArray GooglePeople::Biography::jsonValuesForContact(const QContact &contact, bool *hasChanges) { // Only one biography allowed in a Google contact. QJsonArray array; const QContactNote &detail = contact.detail(); if (!shouldAddDetailChanges(detail, hasChanges)) { return array; } QJsonObject note; note.insert("value", detail.note()); array.append(note); return array; } bool GooglePeople::Birthday::saveContactDetails(QContact *contact, const QList &values) { // Only one birthday allowed in a Google contact. if (values.isEmpty()) { return true; } QContactBirthday detail = contact->detail(); detail.setDate(values.at(0).date); return saveContactDetail(contact, &detail); } GooglePeople::Birthday GooglePeople::Birthday::fromJsonObject(const QJsonObject &object, bool *error) { bool dateOk = false; const QDate date = jsonObjectToDate(object.value("date").toObject(), &dateOk); if (error) { *error = !dateOk; } if (!dateOk) { return Birthday(); } Birthday ret; ret.metadata = FieldMetadata::fromJsonObject(object.value("metadata").toObject()); ret.date = date; return ret; } QJsonArray GooglePeople::Birthday::jsonValuesForContact(const QContact &contact, bool *hasChanges) { // Only one birthday allowed in a Google contact. QJsonArray array; const QContactBirthday &detail = contact.detail(); if (!shouldAddDetailChanges(detail, hasChanges)) { return array; } QJsonObject birthday; birthday.insert("date", jsonObjectFromDate(detail.date())); array.append(birthday); return array; } bool GooglePeople::EmailAddress::saveContactDetails(QContact *contact, const QList &values) { removeDetails(contact); QStringList types; for (const EmailAddress &emailAddress : values) { QList contexts; if (emailAddress.type == QStringLiteral("home")) { contexts.append(QContactDetail::ContextHome); } else if (emailAddress.type == QStringLiteral("work")) { contexts.append(QContactDetail::ContextWork); } else if (emailAddress.type == QStringLiteral("other")) { contexts.append(QContactDetail::ContextOther); } else { // emailAddress.type is a custom type, so ignore it. If the user does not change it to a // known type, the type will not be upsynced and the custom type will be preserved. } QContactEmailAddress detail; if (!contexts.isEmpty()) { detail.setContexts(contexts); } detail.setEmailAddress(emailAddress.value); if (!saveContactDetail(contact, &detail)) { return false; } types.append(emailAddress.type); } return true; } GooglePeople::EmailAddress GooglePeople::EmailAddress::fromJsonObject(const QJsonObject &object, bool *) { EmailAddress ret; ret.metadata = FieldMetadata::fromJsonObject(object.value("metadata").toObject()); ret.value = object.value("value").toString(); ret.type = object.value("type").toString(); ret.formattedType = object.value("formattedType").toString(); ret.displayName = object.value("displayName").toString(); return ret; } QJsonArray GooglePeople::EmailAddress::jsonValuesForContact(const QContact &contact, bool *hasChanges) { QJsonArray array; const QList details = contact.details(); for (int i = 0; i < details.count(); ++i) { const QContactEmailAddress &detail = details.at(i); if (!shouldAddDetailChanges(detail, hasChanges)) { continue; } const int context = detail.contexts().value(0, -1); QString type; switch (context) { case QContactDetail::ContextHome: type = QStringLiteral("home"); break; case QContactDetail::ContextWork: type = QStringLiteral("work"); break; case QContactDetail::ContextOther: type = QStringLiteral("other"); break; } QJsonObject email; if (type.isEmpty()) { // No type set, or the Google field had a custom type set, so don't overwrite it. } else { email.insert("type", type); } email.insert("value", detail.emailAddress()); array.append(email); } return array; } bool GooglePeople::Event::saveContactDetails(QContact *contact, const QList &values) { removeDetails(contact); for (const Event &event : values) { QContactAnniversary detail; detail.setOriginalDateTime(QDateTime(event.date)); if (event.type == QStringLiteral("Wedding")) { detail.setSubType(QContactAnniversary::SubTypeWedding); } else if (event.type == QStringLiteral("Engagement")) { detail.setSubType(QContactAnniversary::SubTypeEngagement); } else if (event.type == QStringLiteral("House")) { detail.setSubType(QContactAnniversary::SubTypeHouse); } else if (event.type == QStringLiteral("Employment")) { detail.setSubType(QContactAnniversary::SubTypeEmployment); } else if (event.type == QStringLiteral("Memorial")) { detail.setSubType(QContactAnniversary::SubTypeMemorial); } else { // event.type is a custom type, so ignore it. If the user does not change it to a // known type, the type will not be upsynced and the custom type will be preserved. } if (!saveContactDetail(contact, &detail)) { return false; } } return true; } GooglePeople::Event GooglePeople::Event::fromJsonObject(const QJsonObject &object, bool *error) { bool dateOk = false; const QDate date = jsonObjectToDate(object.value("date").toObject(), &dateOk); if (error) { *error = !dateOk; } if (!dateOk) { return Event(); } Event ret; ret.metadata = FieldMetadata::fromJsonObject(object.value("metadata").toObject()); ret.date = date; ret.type = object.value("type").toString(); return ret; } QJsonArray GooglePeople::Event::jsonValuesForContact(const QContact &contact, bool *hasChanges) { QJsonArray array; const QList details = contact.details(); for (int i = 0; i < details.count(); ++i) { const QContactAnniversary &detail = details.at(i); if (!shouldAddDetailChanges(detail, hasChanges)) { continue; } QString type; switch (detail.subType()) { case QContactAnniversary::SubTypeWedding: type = QStringLiteral("Wedding"); break; case QContactAnniversary::SubTypeEngagement: type = QStringLiteral("Engagement"); break; case QContactAnniversary::SubTypeHouse: type = QStringLiteral("House"); break; case QContactAnniversary::SubTypeEmployment: type = QStringLiteral("Employment"); break; case QContactAnniversary::SubTypeMemorial: type = QStringLiteral("Memorial"); break; default: break; } QJsonObject event; if (type.isEmpty()) { // No type set, or the Google field had a custom type set, so don't overwrite it. } else { event.insert("type", type); } event.insert("date", jsonObjectFromDate(detail.originalDateTime().date())); array.append(event); } return array; } bool GooglePeople::Membership::matchesCollection(const QContactCollection &collection, int accountId) const { return collection.extendedMetaData(QStringLiteral("resourceName")).toString() == contactGroupResourceName && collection.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID).toInt() == accountId; } bool GooglePeople::Membership::saveContactDetails( QContact *contact, const QList &values, int accountId, const QList &candidateCollections) { contact->setCollectionId(QContactCollectionId()); QStringList contactGroupResourceNames; bool isFavorite = false; for (const Membership &membership : values) { if (contact->collectionId().isNull()) { for (const QContactCollection &collection : candidateCollections) { if (membership.matchesCollection(collection, accountId)) { contact->setCollectionId(collection.id()); break; } } } if (membership.contactGroupResourceName == StarredContactGroupName) { isFavorite = true; } contactGroupResourceNames.append(membership.contactGroupResourceName); } QContactFavorite favoriteDetail = contact->detail(); favoriteDetail.setFavorite(isFavorite); if (!saveContactDetail(contact, &favoriteDetail)) { return false; } // Preserve contactGroupResourceName values since a Person can belong to multiple contact // groups but a QContact can only belong to one collection. if (!saveContactExtendedDetail(contact, QStringLiteral("contactGroupResourceNames"), contactGroupResourceNames)) { return false; } return true; } GooglePeople::Membership GooglePeople::Membership::fromJsonObject(const QJsonObject &object, bool *) { Membership ret; ret.metadata = FieldMetadata::fromJsonObject(object.value("metadata").toObject()); const QJsonObject contactGroupMembership = object.value("contactGroupMembership").toObject(); ret.contactGroupResourceName = contactGroupMembership.value("contactGroupResourceName").toString(); return ret; } QJsonArray GooglePeople::Membership::jsonValuesForContact(const QContact &contact, bool *hasChanges) { QJsonArray array; QStringList contactGroupResourceNames = contactExtendedDetail( contact, QStringLiteral("contactGroupResourceNames")).toStringList(); const QContactFavorite favoriteDetail = contact.detail(); if (shouldAddDetailChanges(favoriteDetail, hasChanges)) { const bool isFavorite = favoriteDetail.isFavorite(); if (isFavorite && contactGroupResourceNames.indexOf(StarredContactGroupName) < 0) { contactGroupResourceNames.append(StarredContactGroupName); } else if (!isFavorite) { contactGroupResourceNames.removeOne(StarredContactGroupName); } } if (contact.id().isNull()) { // This is a new contact, so add its collection into the list of memberships. *hasChanges = true; } if (*hasChanges) { // Add the list of all known memberships of this contact. for (const QString &contactGroupResourceName : contactGroupResourceNames) { QJsonObject membership; // Add the nested contactGroupMembership object. Don't need to add "contactGroupId" // property as that is deprecated. QJsonObject contactGroupMembershipObject; contactGroupMembershipObject.insert("contactGroupResourceName", contactGroupResourceName); membership.insert("contactGroupMembership", contactGroupMembershipObject); array.append(membership); } } return array; } bool GooglePeople::Name::saveContactDetails(QContact *contact, const QList &values) { // Only one name allowed in a Google contact. if (values.isEmpty()) { return true; } const Name &name = values.at(0); QContactName detail = contact->detail(); detail.setFirstName(name.givenName); detail.setMiddleName(name.middleName); detail.setLastName(name.familyName); return saveContactDetail(contact, &detail); } GooglePeople::Name GooglePeople::Name::fromJsonObject(const QJsonObject &object, bool *) { Name ret; ret.metadata = FieldMetadata::fromJsonObject(object.value("metadata").toObject()); ret.familyName = object.value("familyName").toString(); ret.givenName = object.value("givenName").toString(); ret.middleName = object.value("middleName").toString(); return ret; } QJsonArray GooglePeople::Name::jsonValuesForContact(const QContact &contact, bool *hasChanges) { // Only one name allowed in a Google contact. QJsonArray array; const QContactName &detail = contact.detail(); if (!shouldAddDetailChanges(detail, hasChanges)) { return array; } QJsonObject name; name.insert("familyName", detail.lastName()); name.insert("givenName", detail.firstName()); name.insert("middleName", detail.middleName()); name.insert("honorificPrefix", detail.prefix()); name.insert("honorificSuffix", detail.suffix()); array.append(name); return array; } bool GooglePeople::Nickname::saveContactDetails(QContact *contact, const QList &values) { removeDetails(contact); for (const Nickname &nickName : values) { QContactNickname detail; detail.setNickname(nickName.value); if (!saveContactDetail(contact, &detail)) { return false; } } return true; } GooglePeople::Nickname GooglePeople::Nickname::fromJsonObject(const QJsonObject &object, bool *) { Nickname ret; ret.metadata = FieldMetadata::fromJsonObject(object.value("metadata").toObject()); ret.value = object.value("value").toString(); return ret; } QJsonArray GooglePeople::Nickname::jsonValuesForContact(const QContact &contact, bool *hasChanges) { QJsonArray array; const QList details = contact.details(); for (const QContactNickname &detail : details) { if (!shouldAddDetailChanges(detail, hasChanges)) { continue; } QJsonObject nickName; nickName.insert("value", detail.nickname()); array.append(nickName); } return array; } bool GooglePeople::Organization::saveContactDetails(QContact *contact, const QList &values) { removeDetails(contact); for (const Organization &organization : values) { QContactOrganization detail; detail.setName(organization.name); detail.setTitle(organization.title); detail.setRole(organization.jobDescription); detail.setDepartment(QStringList(organization.department)); if (!saveContactDetail(contact, &detail)) { return false; } } return true; } GooglePeople::Organization GooglePeople::Organization::fromJsonObject(const QJsonObject &object, bool *) { Organization ret; ret.metadata = FieldMetadata::fromJsonObject(object.value("metadata").toObject()); ret.name = object.value("name").toString(); ret.title = object.value("title").toString(); ret.jobDescription = object.value("jobDescription").toString(); ret.department = object.value("department").toString(); return ret; } QJsonArray GooglePeople::Organization::jsonValuesForContact(const QContact &contact, bool *hasChanges) { QJsonArray array; const QList details = contact.details(); for (const QContactOrganization &detail : details) { if (!shouldAddDetailChanges(detail, hasChanges)) { continue; } QJsonObject org; org.insert("name", detail.name()); org.insert("title", detail.title()); org.insert("jobDescription", detail.role()); org.insert("department", detail.department().value(0)); array.append(org); } return array; } bool GooglePeople::PhoneNumber::saveContactDetails(QContact *contact, const QList &values) { removeDetails(contact); for (const PhoneNumber &phoneNumber : values) { QContactPhoneNumber detail; detail.setNumber(phoneNumber.value); if (phoneNumber.type == QStringLiteral("home")) { detail.setContexts(QContactDetail::ContextHome); } else if (phoneNumber.type == QStringLiteral("work")) { detail.setContexts(QContactDetail::ContextWork); } else if (phoneNumber.type == QStringLiteral("mobile")) { detail.setSubTypes(QList() << QContactPhoneNumber::SubTypeMobile); } else if (phoneNumber.type == QStringLiteral("workMobile")) { detail.setContexts(QContactDetail::ContextWork); detail.setSubTypes(QList() << QContactPhoneNumber::SubTypeMobile); } else if (phoneNumber.type == QStringLiteral("homeFax")) { detail.setContexts(QContactDetail::ContextHome); detail.setSubTypes(QList() << QContactPhoneNumber::SubTypeFax); } else if (phoneNumber.type == QStringLiteral("workFax")) { detail.setContexts(QContactDetail::ContextWork); detail.setSubTypes(QList() << QContactPhoneNumber::SubTypeFax); } else if (phoneNumber.type == QStringLiteral("otherFax")) { detail.setContexts(QContactDetail::ContextOther); detail.setSubTypes(QList() << QContactPhoneNumber::SubTypeFax); } else if (phoneNumber.type == QStringLiteral("pager")) { detail.setSubTypes(QList() << QContactPhoneNumber::SubTypePager); } else if (phoneNumber.type == QStringLiteral("workPager")) { detail.setContexts(QContactDetail::ContextWork); detail.setSubTypes(QList() << QContactPhoneNumber::SubTypePager); } else if (phoneNumber.type == QStringLiteral("other")) { detail.setContexts(QContactDetail::ContextOther); } else { // phoneNumber.type is a custom type, so ignore it. If the user does not change it to a // known type, the type will not be upsynced and the custom type will be preserved. } if (!saveContactDetail(contact, &detail)) { return false; } } return true; } GooglePeople::PhoneNumber GooglePeople::PhoneNumber::fromJsonObject(const QJsonObject &object, bool *) { PhoneNumber ret; ret.metadata = FieldMetadata::fromJsonObject(object.value("metadata").toObject()); ret.value = object.value("value").toString(); ret.type = object.value("type").toString(); return ret; } QJsonArray GooglePeople::PhoneNumber::jsonValuesForContact(const QContact &contact, bool *hasChanges) { QJsonArray array; const QList details = contact.details(); for (int i = 0; i < details.count(); ++i) { const QContactPhoneNumber &detail = details.at(i); if (!shouldAddDetailChanges(detail, hasChanges)) { continue; } QString type; const int context = detail.contexts().value(0, -1); if (detail.subTypes().isEmpty()) { if (context == QContactDetail::ContextHome) { type = QStringLiteral("home"); } else if (context == QContactDetail::ContextWork) { type = QStringLiteral("work"); } } else { const int subType = detail.subTypes().at(0); switch (subType) { case QContactPhoneNumber::SubTypeMobile: type = QStringLiteral("mobile"); break; case QContactPhoneNumber::SubTypeFax: if (context == QContactDetail::ContextHome) { type = QStringLiteral("homeFax"); } else if (context == QContactDetail::ContextWork) { type = QStringLiteral("workFax"); } else if (context == QContactDetail::ContextOther) { type = QStringLiteral("otherFax"); } break; case QContactPhoneNumber::SubTypePager: if (context == QContactDetail::ContextWork) { type = QStringLiteral("workPager"); } else { type = QStringLiteral("pager"); } break; default: break; } } QJsonObject phone; if (type.isEmpty()) { // No type set, or the Google field had a custom type set, so don't overwrite it. } else { phone.insert("type", type); } phone.insert("value", detail.number()); array.append(phone); } return array; } bool GooglePeople::PersonMetadata::saveContactDetails(QContact *contact, const PersonMetadata &metadata) { for (const Source &source : metadata.sources) { QVariantMap sourceInfo; sourceInfo.insert("type", source.type); sourceInfo.insert("id", source.id); sourceInfo.insert("etag", source.etag); if (!saveContactExtendedDetail(contact, QStringLiteral("source_%1").arg(source.type), sourceInfo)) { return false; } } return true; } QString GooglePeople::PersonMetadata::etag(const QContact &contact) { const QVariantMap sourceInfo = contactExtendedDetail(contact, QStringLiteral("source_CONTACT")).toMap(); return sourceInfo.value("etag").toString(); } GooglePeople::PersonMetadata GooglePeople::PersonMetadata::fromJsonObject(const QJsonObject &object, bool *) { PersonMetadata ret; ret.sources = jsonArrayToList(object.value("sources").toArray()); ret.previousResourceNames = object.value("previousResourceNames").toVariant().toStringList(); ret.linkedPeopleResourceNames = object.value("linkedPeopleResourceNames").toVariant().toStringList(); ret.deleted = object.value("deleted").toBool(); return ret; } QJsonObject GooglePeople::PersonMetadata::toJsonObject(const QContact &contact) { // Only need to add the details for the "CONTACT" source. QJsonObject metadataObject; const QVariantMap sourceInfo = contactExtendedDetail( contact, QStringLiteral("source_CONTACT")).toMap(); if (!sourceInfo.isEmpty()) { QJsonObject sourceObject; sourceObject.insert("type", sourceInfo.value("type").toString()); sourceObject.insert("id", sourceInfo.value("id").toString()); sourceObject.insert("etag", sourceInfo.value("etag").toString()); QJsonArray sourcesArray; sourcesArray.append(QJsonValue(sourceObject)); metadataObject.insert("sources", sourcesArray); } return metadataObject; } QContactAvatar GooglePeople::Photo::getPrimaryPhoto(const QContact &contact, QString *remoteAvatarUrl, QString *localAvatarFile) { // Use the first avatar as the the primary photo for the contact. const QContactAvatar avatar = contact.detail(); if (localAvatarFile) { *localAvatarFile = avatar.imageUrl().toString(); } if (remoteAvatarUrl) { *remoteAvatarUrl = avatar.videoUrl().toString(); } return avatar; } bool GooglePeople::Photo::saveContactDetails(QContact *contact, const QList &values) { removeDetails(contact); const QString guid = contact->detail().guid(); for (const Photo &photo : values) { if (photo.default_) { // Ignore the Google-generated avatar that simply shows the contact's initials. continue; } QContactAvatar avatar; const QString localFilePath = GoogleContactImageDownloader::staticOutputFile(guid, photo.url); if (localFilePath.isEmpty()) { qCWarning(lcSocialPlugin) << "Cannot generate local file name for avatar url:" << photo.url << "for contact:" << guid; continue; } avatar.setImageUrl(QUrl(localFilePath)); avatar.setVideoUrl(QUrl(photo.url)); // ugly hack to store the remote url separately to the local path if (!saveContactDetail(contact, &avatar)) { return false; } } return true; } GooglePeople::Photo GooglePeople::Photo::fromJsonObject(const QJsonObject &object, bool *) { Photo ret; ret.metadata = FieldMetadata::fromJsonObject(object.value("metadata").toObject()); ret.url = object.value("url").toString(); ret.default_ = object.value("default").toBool(); return ret; } QJsonArray GooglePeople::Photo::jsonValuesForContact(const QContact &contact, bool *hasChanges) { QJsonArray array; const QList details = contact.details(); for (const QContactAvatar &detail : details) { if (!shouldAddDetailChanges(detail, hasChanges)) { continue; } QJsonObject photo; photo.insert("url", detail.imageUrl().toString()); array.append(photo); } return array; } bool GooglePeople::Url::saveContactDetails(QContact *contact, const QList &values) { removeDetails(contact); for (const Url &url : values) { QContactUrl detail; detail.setUrl(url.value); if (url.type == QStringLiteral("homePage")) { detail.setSubType(QContactUrl::SubTypeHomePage); } else if (url.type == QStringLiteral("blog")) { detail.setSubType(QContactUrl::SubTypeBlog); } if (!saveContactDetail(contact, &detail)) { return false; } } return true; } GooglePeople::Url GooglePeople::Url::fromJsonObject(const QJsonObject &object, bool *) { Url ret; ret.metadata = FieldMetadata::fromJsonObject(object.value("metadata").toObject()); ret.value = object.value("value").toString(); ret.type = object.value("type").toString(); ret.formattedType = object.value("formattedType").toString(); return ret; } QJsonArray GooglePeople::Url::jsonValuesForContact(const QContact &contact, bool *hasChanges) { QJsonArray array; const QList details = contact.details(); for (const QContactUrl &detail : details) { if (!shouldAddDetailChanges(detail, hasChanges)) { continue; } QJsonObject url; switch (detail.subType()) { case QContactUrl::SubTypeHomePage: url.insert("type", QStringLiteral("homePage")); break; case QContactUrl::SubTypeBlog: url.insert("type", QStringLiteral("blog")); break; default: // No type set, or the Google field had a custom type set, so don't overwrite it. break; } url.insert("value", detail.url()); array.append(url); } return array; } QContact GooglePeople::Person::toContact(int accountId, const QList &candidateCollections) const { QContact contact; saveToContact(&contact, accountId, candidateCollections); return contact; } bool GooglePeople::Person::saveToContact(QContact *contact, int accountId, const QList &candidateCollections) const { if (!contact) { qCWarning(lcSocialPlugin) << "saveToContact() failed: invalid contact!"; return false; } QContactGuid guid = contact->detail(); if (guid.guid().isEmpty()) { guid.setGuid(guidForPerson(accountId, resourceName)); if (!contact->saveDetail(&guid, QContact::IgnoreAccessConstraints)) { return false; } } PersonMetadata::saveContactDetails(contact, metadata); Address::saveContactDetails(contact, addresses); Biography::saveContactDetails(contact, biographies); Birthday::saveContactDetails(contact, birthdays); EmailAddress::saveContactDetails(contact, emailAddresses); Event::saveContactDetails(contact, events); Membership::saveContactDetails(contact, memberships, accountId, candidateCollections); Name::saveContactDetails(contact, names); Nickname::saveContactDetails(contact, nicknames); Organization::saveContactDetails(contact, organizations); PhoneNumber::saveContactDetails(contact, phoneNumbers); Photo::saveContactDetails(contact, photos); Url::saveContactDetails(contact, urls); return true; } GooglePeople::Person GooglePeople::Person::fromJsonObject(const QJsonObject &object) { Person ret; ret.resourceName = object.value("resourceName").toString(); ret.metadata = PersonMetadata::fromJsonObject(object.value("metadata").toObject(), nullptr); ret.addresses = jsonArrayToList
(object.value("addresses").toArray()); ret.biographies = jsonArrayToList(object.value("biographies").toArray()); ret.birthdays = jsonArrayToList(object.value("birthdays").toArray()); ret.emailAddresses = jsonArrayToList(object.value("emailAddresses").toArray()); ret.events = jsonArrayToList(object.value("events").toArray()); ret.memberships = jsonArrayToList(object.value("memberships").toArray()); ret.names = jsonArrayToList(object.value("names").toArray()); ret.nicknames = jsonArrayToList(object.value("nicknames").toArray()); ret.organizations = jsonArrayToList(object.value("organizations").toArray()); ret.phoneNumbers = jsonArrayToList(object.value("phoneNumbers").toArray()); ret.photos = jsonArrayToList(object.value("photos").toArray()); ret.urls = jsonArrayToList(object.value("urls").toArray()); return ret; } GooglePeople::ContactGroupMetadata GooglePeople::ContactGroupMetadata::fromJsonObject(const QJsonObject &obj) { ContactGroupMetadata ret; const QString updateTime = obj.value("updateTime").toString(); if (!updateTime.isEmpty()) { ret.updateTime = QDateTime::fromString(updateTime, Qt::ISODate); } ret.deleted = obj.value("deleted").toBool(); return ret; } QJsonObject GooglePeople::Person::contactToJsonObject(const QContact &contact, QStringList *addedFields) { QJsonObject person; // Add resourceName QString resourceName = personResourceName(contact); if (!resourceName.isEmpty()) { person.insert("resourceName", resourceName); } // Add metadata including etag QJsonObject metadataObject = PersonMetadata::toJsonObject(contact); if (!metadataObject.isEmpty()) { person.insert("metadata", metadataObject); } // Add other fields. // photos are not added here, as they can only modified in the Google People API via // updateContactPhoto(), and cannot be passed in createContact() and updateContact(). addJsonValuesForContact
(QStringLiteral("addresses"), contact, &person, addedFields); addJsonValuesForContact(QStringLiteral("biographies"), contact, &person, addedFields); addJsonValuesForContact(QStringLiteral("birthdays"), contact, &person, addedFields); addJsonValuesForContact(QStringLiteral("emailAddresses"), contact, &person, addedFields); addJsonValuesForContact(QStringLiteral("events"), contact, &person, addedFields); addJsonValuesForContact(QStringLiteral("memberships"), contact, &person, addedFields); addJsonValuesForContact(QStringLiteral("names"), contact, &person, addedFields); addJsonValuesForContact(QStringLiteral("nicknames"), contact, &person, addedFields); addJsonValuesForContact(QStringLiteral("organizations"), contact, &person, addedFields); addJsonValuesForContact(QStringLiteral("phoneNumbers"), contact, &person, addedFields); addJsonValuesForContact(QStringLiteral("urls"), contact, &person, addedFields); return person; } QString GooglePeople::Person::personResourceName(const QContact &contact) { const QString guid = contact.detail().guid(); if (!guid.isEmpty()) { const int index = guid.indexOf(':'); if (index >= 0) { return guid.mid(index + 1); } } return QString(); } QStringList GooglePeople::Person::supportedPersonFields() { static QStringList fields; if (fields.isEmpty()) { fields << QStringLiteral("metadata"); fields << QStringLiteral("addresses"); fields << QStringLiteral("biographies"); fields << QStringLiteral("birthdays"); fields << QStringLiteral("emailAddresses"); fields << QStringLiteral("events"); fields << QStringLiteral("memberships"); fields << QStringLiteral("names"); fields << QStringLiteral("nicknames"); fields << QStringLiteral("organizations"); fields << QStringLiteral("phoneNumbers"); fields << QStringLiteral("photos"); fields << QStringLiteral("urls"); } return fields; } QString GooglePeople::Person::guidForPerson(int accountId, const QString &resourceName) { return QStringLiteral("%1:%2").arg(accountId).arg(resourceName); } bool GooglePeople::ContactGroup::isMyContactsGroup() const { return resourceName == QStringLiteral("contactGroups/myContacts"); } QContactCollection GooglePeople::ContactGroup::toCollection(int accountId) const { QContactCollection collection; collection.setMetaData(QContactCollection::KeyName, formattedName); collection.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, QCoreApplication::applicationName()); collection.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, accountId); collection.setExtendedMetaData(QStringLiteral("resourceName"), resourceName); collection.setExtendedMetaData(QStringLiteral("groupType"), groupType); return collection; } bool GooglePeople::ContactGroup::isMyContactsCollection(const QContactCollection &collection, int accountId) { return collection.extendedMetaData("resourceName").toString() == QStringLiteral("contactGroups/myContacts") && (accountId == 0 || collection.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID).toInt() == accountId); } GooglePeople::ContactGroup GooglePeople::ContactGroup::fromJsonObject(const QJsonObject &obj) { ContactGroup ret; ret.resourceName = obj.value("resourceName").toString(); ret.etag = obj.value("etag").toString(); ret.contactGroupMetadata = ContactGroupMetadata::fromJsonObject(obj.value("contactGroupMetadata").toObject()); ret.groupType = obj.value("groupType").toString(); ret.name = obj.value("name").toString(); ret.formattedName = obj.value("formattedName").toString(); ret.memberResourceNames = obj.value("memberResourceNames").toVariant().toStringList(); ret.memberCount = obj.value("memberCount").toInt(); return ret; } #define DEBUG_VALUE_ONLY(propertyName) \ debug.nospace() << #propertyName << "=" << value.propertyName #define DEBUG_VALUE(propertyName) \ DEBUG_VALUE_ONLY(propertyName) << ", "; #define DEBUG_VALUE_LAST(propertyName) \ DEBUG_VALUE_ONLY(propertyName) << ")"; #define DEBUG_VALUE_INDENT(propertyName) \ debug.nospace() << "\n ";\ DEBUG_VALUE(propertyName); #define DEBUG_VALUE_INDENT_LAST(propertyName) \ debug.nospace() << "\n ";\ DEBUG_VALUE_LAST(propertyName); QDebug operator<<(QDebug debug, const GooglePeople::Source &value) { debug.nospace() << "Source("; DEBUG_VALUE(type) DEBUG_VALUE_LAST(id); return debug.maybeSpace(); } QDebug operator<<(QDebug debug, const GooglePeople::FieldMetadata &value) { debug.nospace() << "FieldMetadata("; DEBUG_VALUE(primary) DEBUG_VALUE(verified) DEBUG_VALUE_LAST(source) return debug.maybeSpace(); } QDebug operator<<(QDebug debug, const GooglePeople::Address &value) { debug.nospace() << "Address("; DEBUG_VALUE(metadata) DEBUG_VALUE(formattedValue) DEBUG_VALUE(type) DEBUG_VALUE(formattedType) DEBUG_VALUE(poBox) DEBUG_VALUE(streetAddress) DEBUG_VALUE(extendedAddress) DEBUG_VALUE(city) DEBUG_VALUE(region) DEBUG_VALUE(postalCode) DEBUG_VALUE(country) DEBUG_VALUE_LAST(countryCode) return debug.maybeSpace(); } QDebug operator<<(QDebug debug, const GooglePeople::Biography &value) { debug.nospace() << "Biography("; DEBUG_VALUE(metadata) DEBUG_VALUE(value) return debug.maybeSpace(); } QDebug operator<<(QDebug debug, const GooglePeople::Birthday &value) { debug.nospace() << "Birthday("; DEBUG_VALUE(metadata) DEBUG_VALUE(date) return debug.maybeSpace(); } QDebug operator<<(QDebug debug, const GooglePeople::EmailAddress &value) { debug.nospace() << "EmailAddress("; DEBUG_VALUE(metadata) DEBUG_VALUE(value) DEBUG_VALUE(type) DEBUG_VALUE(formattedType) DEBUG_VALUE_LAST(displayName) return debug.maybeSpace(); } QDebug operator<<(QDebug debug, const GooglePeople::Event &value) { debug.nospace() << "Event("; DEBUG_VALUE(metadata) DEBUG_VALUE(date) DEBUG_VALUE_LAST(type) return debug.maybeSpace(); } QDebug operator<<(QDebug debug, const GooglePeople::Membership &value) { debug.nospace() << "Membership("; DEBUG_VALUE(metadata) DEBUG_VALUE(contactGroupResourceName) return debug.maybeSpace(); } QDebug operator<<(QDebug debug, const GooglePeople::Name &value) { debug.nospace() << "Name("; DEBUG_VALUE(metadata) DEBUG_VALUE(familyName) DEBUG_VALUE(givenName) DEBUG_VALUE_LAST(middleName) return debug.maybeSpace(); } QDebug operator<<(QDebug debug, const GooglePeople::Nickname &value) { debug.nospace() << "Nickname("; DEBUG_VALUE(metadata) DEBUG_VALUE(value) return debug.maybeSpace(); } QDebug operator<<(QDebug debug, const GooglePeople::Organization &value) { debug.nospace() << "Organization("; DEBUG_VALUE(metadata) DEBUG_VALUE(name) DEBUG_VALUE(title) DEBUG_VALUE(jobDescription) DEBUG_VALUE_LAST(department) return debug.maybeSpace(); } QDebug operator<<(QDebug debug, const GooglePeople::PhoneNumber &value) { debug.nospace() << "PhoneNumber("; DEBUG_VALUE(metadata) DEBUG_VALUE(value) DEBUG_VALUE(type) return debug.maybeSpace(); } QDebug operator<<(QDebug debug, const GooglePeople::PersonMetadata &value) { debug.nospace() << "PersonMetadata("; DEBUG_VALUE(sources) DEBUG_VALUE(previousResourceNames) DEBUG_VALUE(linkedPeopleResourceNames) DEBUG_VALUE_LAST(deleted) return debug.maybeSpace(); } QDebug operator<<(QDebug debug, const GooglePeople::Photo &value) { debug.nospace() << "Photo("; DEBUG_VALUE(metadata) DEBUG_VALUE(url) DEBUG_VALUE_LAST(default_) return debug.maybeSpace(); } QDebug operator<<(QDebug debug, const GooglePeople::Url &value) { debug.nospace() << "Url("; DEBUG_VALUE(metadata) DEBUG_VALUE(value) DEBUG_VALUE(type) DEBUG_VALUE_LAST(formattedType) return debug.maybeSpace(); } QDebug operator<<(QDebug debug, const GooglePeople::Person &value) { debug.nospace() << "\nPerson("; DEBUG_VALUE_INDENT(resourceName) DEBUG_VALUE_INDENT(metadata) DEBUG_VALUE_INDENT(addresses) DEBUG_VALUE_INDENT(biographies) DEBUG_VALUE_INDENT(birthdays) DEBUG_VALUE_INDENT(emailAddresses) DEBUG_VALUE_INDENT(memberships) DEBUG_VALUE_INDENT(names) DEBUG_VALUE_INDENT(nicknames) DEBUG_VALUE_INDENT(organizations) DEBUG_VALUE_INDENT(phoneNumbers) DEBUG_VALUE_INDENT(photos) DEBUG_VALUE_INDENT_LAST(urls) return debug.maybeSpace(); } QDebug operator<<(QDebug debug, const GooglePeople::ContactGroupMetadata &value) { debug.nospace() << "ContactGroupMetadata("; DEBUG_VALUE(updateTime) DEBUG_VALUE_LAST(deleted) return debug.maybeSpace(); } QDebug operator<<(QDebug debug, const GooglePeople::ContactGroup &value) { debug.nospace() << "\nContactGroup("; DEBUG_VALUE_INDENT(resourceName) DEBUG_VALUE_INDENT(etag) DEBUG_VALUE_INDENT(contactGroupMetadata) DEBUG_VALUE_INDENT(groupType) DEBUG_VALUE_INDENT(name) DEBUG_VALUE_INDENT(formattedName) DEBUG_VALUE_INDENT(memberResourceNames) DEBUG_VALUE_INDENT_LAST(memberCount) return debug.maybeSpace(); } buteo-sync-plugins-social-0.4.28/src/google/google-contacts/googlepeoplejson.h000066400000000000000000000312401474572147200275200ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef GOOGLEPEOPLEJSON_H #define GOOGLEPEOPLEJSON_H #include #include #include #include #include #include QTCONTACTS_USE_NAMESPACE namespace GooglePeople { class Source { public: QString type; QString id; QString etag; /* Ignored fields: QString updateTime; ProfileMetadata profileMetadata; */ static Source fromJsonObject(const QJsonObject &obj, bool *error = nullptr); }; class FieldMetadata { public: bool primary = false; bool verified = false; Source source; static FieldMetadata fromJsonObject(const QJsonObject &obj); }; class Address { public: FieldMetadata metadata; QString formattedValue; QString type; QString formattedType; QString poBox; QString streetAddress; QString extendedAddress; QString city; QString region; QString postalCode; QString country; QString countryCode; static bool saveContactDetails(QContact *contact, const QList
&values); static Address fromJsonObject(const QJsonObject &obj, bool *error = nullptr); static QJsonArray jsonValuesForContact(const QContact &contact, bool *hasChanges); }; class Biography { public: FieldMetadata metadata; QString value; /* Ignored fields: QString contentType; */ static bool saveContactDetails(QContact *contact, const QList &values); static Biography fromJsonObject(const QJsonObject &obj, bool *error = nullptr); static QJsonArray jsonValuesForContact(const QContact &contact, bool *hasChanges); }; class Birthday { public: FieldMetadata metadata; QDate date; /* Ignored fields: QString text; */ static bool saveContactDetails(QContact *contact, const QList &values); static Birthday fromJsonObject(const QJsonObject &obj, bool *error = nullptr); static QJsonArray jsonValuesForContact(const QContact &contact, bool *hasChanges); }; class EmailAddress { public: FieldMetadata metadata; QString value; QString type; QString formattedType; QString displayName; static bool saveContactDetails(QContact *contact, const QList &values); static EmailAddress fromJsonObject(const QJsonObject &obj, bool *error = nullptr); static QJsonArray jsonValuesForContact(const QContact &contact, bool *hasChanges); }; class Event { public: FieldMetadata metadata; QDate date; QString type; /* Ignored fields: QString formattedType; */ static bool saveContactDetails(QContact *contact, const QList &values); static Event fromJsonObject(const QJsonObject &obj, bool *error = nullptr); static QJsonArray jsonValuesForContact(const QContact &contact, bool *hasChanges); }; class Membership { public: FieldMetadata metadata; QString contactGroupResourceName; /* Ignored fields: DomainMembership domainMembership; */ bool matchesCollection(const QContactCollection &collection, int accountId) const; static bool saveContactDetails(QContact *contact, const QList &values, int accountId, const QList &candidateCollections); static Membership fromJsonObject(const QJsonObject &obj, bool *error = nullptr); static QJsonArray jsonValuesForContact(const QContact &contact, bool *hasChanges); }; class Name { public: FieldMetadata metadata; QString familyName; QString givenName; QString middleName; /* Ignored fields: QString displayName; QString displayNameLastFirst; QString unstructuredName; QString phoneticFullName; QString phoneticFamilyName; QString phoneticGivenName; QString phoneticMiddleName; QString honorificPrefix; QString honorificSuffix; QString phoneticHonorificPrefix; QString phoneticHonorificSuffix; */ static bool saveContactDetails(QContact *contact, const QList &values); static Name fromJsonObject(const QJsonObject &obj, bool *error = nullptr); static QJsonArray jsonValuesForContact(const QContact &contact, bool *hasChanges); }; class Nickname { public: FieldMetadata metadata; QString value; /* Ignored fields: QString type; */ static bool saveContactDetails(QContact *contact, const QList &values); static Nickname fromJsonObject(const QJsonObject &obj, bool *error = nullptr); static QJsonArray jsonValuesForContact(const QContact &contact, bool *hasChanges); }; class Organization { public: FieldMetadata metadata; QString name; QString title; QString jobDescription; QString department; /* Ignored fields: QString type; QString formattedType; QDate startDate; QDate endDate; QString phoneticName; QString symbol; QString domain; QString location; */ static bool saveContactDetails(QContact *contact, const QList &values); static Organization fromJsonObject(const QJsonObject &obj, bool *error = nullptr); static QJsonArray jsonValuesForContact(const QContact &contact, bool *hasChanges); }; class PhoneNumber { public: FieldMetadata metadata; QString value; QString type; /* Ignored fields: QString canonicalForm; QString formattedType; */ static bool saveContactDetails(QContact *contact, const QList &values); static PhoneNumber fromJsonObject(const QJsonObject &obj, bool *error = nullptr); static QJsonArray jsonValuesForContact(const QContact &contact, bool *hasChanges); }; class PersonMetadata { public: QList sources; QStringList previousResourceNames; QStringList linkedPeopleResourceNames; bool deleted = false; static QString etag(const QContact &contact); static bool saveContactDetails(QContact *contact, const PersonMetadata &value); static PersonMetadata fromJsonObject(const QJsonObject &obj, bool *error = nullptr); static QJsonObject toJsonObject(const QContact &contact); }; class Photo { public: FieldMetadata metadata; QString url; bool default_ = false; static QContactAvatar getPrimaryPhoto(const QContact &contact, QString *remoteAvatarUrl = nullptr, QString *localAvatarFile = nullptr); static bool saveContactDetails(QContact *contact, const QList &values); static Photo fromJsonObject(const QJsonObject &obj, bool *error = nullptr); static QJsonArray jsonValuesForContact(const QContact &contact, bool *hasChanges); }; class Url { public: FieldMetadata metadata; QString value; QString type; QString formattedType; static bool saveContactDetails(QContact *contact, const QList &values); static Url fromJsonObject(const QJsonObject &obj, bool *error = nullptr); static QJsonArray jsonValuesForContact(const QContact &contact, bool *hasChanges); }; class Person { public: QString resourceName; PersonMetadata metadata; QList
addresses; QList biographies; QList birthdays; QList emailAddresses; QList events; QList memberships; QList names; QList nicknames; QList organizations; QList phoneNumbers; QList photos; QList urls; /* Ignored fields: QString etag; QList ageRanges; QList calendarUrls; QList clientData; QList coverPhotos; QList externalIds; QList fileAses; QList genders; QList imClients; QList interests; QList locales; QList locations; QList miscKeywords; QList occupations; QList relations; QList sipAddresses; QList skills; QList userDefined; */ inline bool isValid() const { return !resourceName.isEmpty(); } QContact toContact(int accountId, const QList &candidateCollections) const; bool saveToContact(QContact *contact, int accountId, const QList &candidateCollections) const; static Person fromJsonObject(const QJsonObject &obj); static QJsonObject contactToJsonObject(const QContact &contact, QStringList *updatedFields = nullptr); static QString personResourceName(const QContact &contact); static QStringList supportedPersonFields(); private: static QString guidForPerson(int accountId, const QString &resourceName); }; class ContactGroupMetadata { public: QDateTime updateTime; bool deleted = false; static ContactGroupMetadata fromJsonObject(const QJsonObject &obj); }; class ContactGroup { public: QString resourceName; QString etag; ContactGroupMetadata contactGroupMetadata; QString groupType; QString name; QString formattedName; QStringList memberResourceNames; int memberCount = 0; bool isMyContactsGroup() const; QContactCollection toCollection(int accountId) const; static bool isMyContactsCollection(const QContactCollection &collection, int accountId = 0); static ContactGroup fromJsonObject(const QJsonObject &obj); }; } QDebug operator<<(QDebug debug, const GooglePeople::Source &value); QDebug operator<<(QDebug debug, const GooglePeople::FieldMetadata &value); QDebug operator<<(QDebug debug, const GooglePeople::Address &value); QDebug operator<<(QDebug debug, const GooglePeople::Biography &value); QDebug operator<<(QDebug debug, const GooglePeople::Birthday &value); QDebug operator<<(QDebug debug, const GooglePeople::EmailAddress &value); QDebug operator<<(QDebug debug, const GooglePeople::Event &value); QDebug operator<<(QDebug debug, const GooglePeople::Membership &value); QDebug operator<<(QDebug debug, const GooglePeople::Name &value); QDebug operator<<(QDebug debug, const GooglePeople::Nickname &value); QDebug operator<<(QDebug debug, const GooglePeople::Organization &value); QDebug operator<<(QDebug debug, const GooglePeople::PhoneNumber &value); QDebug operator<<(QDebug debug, const GooglePeople::PersonMetadata &value); QDebug operator<<(QDebug debug, const GooglePeople::Photo &value); QDebug operator<<(QDebug debug, const GooglePeople::Url &value); QDebug operator<<(QDebug debug, const GooglePeople::Person &value); QDebug operator<<(QDebug debug, const GooglePeople::ContactGroupMetadata &value); QDebug operator<<(QDebug debug, const GooglePeople::ContactGroup &value); #endif // GOOGLEPEOPLEJSON_H buteo-sync-plugins-social-0.4.28/src/google/google-contacts/googletwowaycontactsyncadaptor.cpp000066400000000000000000001523261474572147200330640ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2014 - 2019 Jolla Ltd. ** Copyright (c) 2020 Open Mobile Platform LLC. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "googletwowaycontactsyncadaptor.h" #include "googlecontactimagedownloader.h" #include "constants_p.h" #include "trace.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include static const char *IMAGE_DOWNLOADER_TOKEN_KEY = "url"; static const char *IMAGE_DOWNLOADER_IDENTIFIER_KEY = "identifier"; namespace { const QString CollectionKeySyncToken = QStringLiteral("syncToken"); const QString CollectionKeySyncTokenDate = QStringLiteral("syncTokenDate"); QContactCollection findCollection(const QContactManager &contactManager, int accountId) { const QList collections = contactManager.collections(); for (const QContactCollection &collection : collections) { if (GooglePeople::ContactGroup::isMyContactsCollection(collection, accountId)) { return collection; } } return QContactCollection(); } int indexOfContact(const QList &contacts, const QContactId &contactId) { for (int i = 0; i < contacts.count(); ++i) { if (contacts.at(i).id() == contactId) { return i; } } return -1; } } //------------------------- GoogleContactSqliteSyncAdaptor::GoogleContactSqliteSyncAdaptor(int accountId, GoogleTwoWayContactSyncAdaptor *parent) : QtContactsSqliteExtensions::TwoWayContactSyncAdaptor(accountId, qAppName(), *parent->m_contactManager) , q(parent) { } GoogleContactSqliteSyncAdaptor::~GoogleContactSqliteSyncAdaptor() { } bool GoogleContactSqliteSyncAdaptor::isLocallyDeletedGuid(const QString &guid) const { if (guid.isEmpty()) { return false; } const TwoWayContactSyncAdaptorPrivate::ContactChanges &localChanges(d->m_localContactChanges[q->m_collection.id()]); for (const QContact &removedContact : localChanges.removedContacts) { if (guid == removedContact.detail().guid()) { return true; } } return false; } bool GoogleContactSqliteSyncAdaptor::determineRemoteCollections() { if (q->m_collection.id().isNull()) { qCDebug(lcSocialPluginTrace) << "performing request to find My Contacts group with account" << q->m_accountId; q->requestData(GoogleTwoWayContactSyncAdaptor::ContactGroupRequest); } else { // we can just sync changes immediately qCDebug(lcSocialPluginTrace) << "requesting contact sync deltas with account" << q->m_accountId << "for collection" << q->m_collection.id(); remoteCollectionsDetermined(QList() << q->m_collection); } return true; } bool GoogleContactSqliteSyncAdaptor::deleteRemoteCollection(const QContactCollection &collection) { qCWarning(lcSocialPlugin) << "Ignoring request to delete My Contacts collection" << collection.id(); return true; } bool GoogleContactSqliteSyncAdaptor::determineRemoteContacts(const QContactCollection &collection) { Q_UNUSED(collection) q->requestData(GoogleTwoWayContactSyncAdaptor::ContactRequest, GoogleTwoWayContactSyncAdaptor::DetermineRemoteContacts); return true; } bool GoogleContactSqliteSyncAdaptor::determineRemoteContactChanges(const QContactCollection &collection, const QList &localAddedContacts, const QList &localModifiedContacts, const QList &localDeletedContacts, const QList &localUnmodifiedContacts, QContactManager::Error *error) { Q_UNUSED(collection) Q_UNUSED(localAddedContacts) Q_UNUSED(localModifiedContacts) Q_UNUSED(localDeletedContacts) Q_UNUSED(localUnmodifiedContacts) Q_UNUSED(error) if (q->m_connectionsListParams.syncToken.isEmpty()) { // Notify the two-way sync adaptor that this is a full sync rather than a delta sync, so // that it will call determineRemoteContacts() to fetch all contacts for the collection. *error = QContactManager::NotSupportedError; return false; } q->requestData(GoogleTwoWayContactSyncAdaptor::ContactRequest, GoogleTwoWayContactSyncAdaptor::DetermineRemoteContactChanges); return true; } bool GoogleContactSqliteSyncAdaptor::storeLocalChangesRemotely(const QContactCollection &collection, const QList &addedContacts, const QList &modifiedContacts, const QList &deletedContacts) { Q_UNUSED(collection) q->upsyncLocalChanges(addedContacts, modifiedContacts, deletedContacts); return true; } void GoogleContactSqliteSyncAdaptor::storeRemoteChangesLocally(const QContactCollection &collection, const QList &addedContacts, const QList &modifiedContacts, const QList &deletedContacts) { Q_UNUSED(collection) TwoWayContactSyncAdaptor::storeRemoteChangesLocally(q->m_collection, addedContacts, modifiedContacts, deletedContacts); } void GoogleContactSqliteSyncAdaptor::syncFinishedSuccessfully() { qCInfo(lcSocialPlugin) << "Sync finished OK"; q->syncFinished(); } void GoogleContactSqliteSyncAdaptor::syncFinishedWithError() { qCWarning(lcSocialPlugin) << "Sync finished with error"; if (q->m_collection.id().isNull()) { return; } // If sync fails, clear the sync token and date for the collection, so that the next sync // requests a full contact listing, to ensure we are up-to-date with the server. q->m_collection.setExtendedMetaData(CollectionKeySyncToken, QString()); q->m_collection.setExtendedMetaData(CollectionKeySyncTokenDate, QString()); QHash* > modifiedCollections; QList emptyContacts; modifiedCollections.insert(&q->m_collection, &emptyContacts); QtContactsSqliteExtensions::ContactManagerEngine *cme = QtContactsSqliteExtensions::contactManagerEngine(*q->m_contactManager); QContactManager::Error error = QContactManager::NoError; if (!cme->storeChanges(nullptr, &modifiedCollections, QList(), QtContactsSqliteExtensions::ContactManagerEngine::PreserveLocalChanges, true, &error)) { qCWarning(lcSocialPlugin) << "Failed to clear sync token for account:" << q->m_accountId << "due to error:" << error; } } //------------------------------------- GoogleTwoWayContactSyncAdaptor::GoogleTwoWayContactSyncAdaptor(QObject *parent) : GoogleDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::Contacts, parent) , m_contactManager(new QContactManager(QStringLiteral("org.nemomobile.contacts.sqlite"))) , m_workerObject(new GoogleContactImageDownloader()) { connect(m_workerObject, &AbstractImageDownloader::imageDownloaded, this, &GoogleTwoWayContactSyncAdaptor::imageDownloaded); // can sync, enabled setInitialActive(true); } GoogleTwoWayContactSyncAdaptor::~GoogleTwoWayContactSyncAdaptor() { delete m_workerObject; } QString GoogleTwoWayContactSyncAdaptor::syncServiceName() const { return QStringLiteral("google-contacts"); } void GoogleTwoWayContactSyncAdaptor::sync(const QString &dataTypeString, int accountId) { m_accountId = accountId; // Detect if this account was previously synced with the legacy Google Contacts API. If so, // remove all contacts and do a fresh sync with the Google People API. const QList collections = m_contactManager->collections(); for (const QContactCollection &collection : collections) { if (collection.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID).toInt() == accountId && collection.extendedMetaData(QStringLiteral("atom-id")).isValid()) { qCInfo(lcSocialPlugin) << "Removing contacts synced with legacy Google Contacts API"; purgeAccount(accountId); } } // Remove legacy settings file QString settingsFileName = QString::fromLatin1("%1/%2/gcontacts.ini") .arg(PRIVILEGED_DATA_DIR) .arg(QString::fromLatin1(SYNC_DATABASE_DIR)); QFile::remove(settingsFileName); m_sqliteSync = new GoogleContactSqliteSyncAdaptor(accountId, this); // assume we can make up to 99 requests per sync, before being throttled. m_apiRequestsRemaining = 99; // call superclass impl. GoogleDataTypeSyncAdaptor::sync(dataTypeString, accountId); } void GoogleTwoWayContactSyncAdaptor::purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode ) { purgeAccount(oldId); } void GoogleTwoWayContactSyncAdaptor::beginSync(int accountId, const QString &accessToken) { if (accountId != m_accountId) { qCWarning(lcSocialPlugin) << "Cannot begin sync, expected account id" << m_accountId << "but got" << m_accountId; setStatus(SocialNetworkSyncAdaptor::Error); return; } m_accessToken = accessToken; // Find the Google contacts collection, if previously synced. m_collection = findCollection(*m_contactManager, accountId); if (m_collection.id().isNull()) { qCDebug(lcSocialPlugin) << "No MyContacts collection saved yet for account:" << accountId; } else { loadCollection(m_collection); qCDebug(lcSocialPlugin) << "Found MyContacts collection" << m_collection.id() << "for account:" << accountId; } // Initialize the people.connections.list() parameters QString syncToken; if (!m_collection.id().isNull()) { syncToken = m_collection.extendedMetaData(CollectionKeySyncToken).toString(); const QDateTime syncTokenDate = QDateTime::fromString( m_collection.extendedMetaData(CollectionKeySyncTokenDate).toString(), Qt::ISODate); // Google sync token expires after 7 days. If it's almost expired, request a new sync token // during this sync session. if (syncTokenDate.isValid() && syncTokenDate.daysTo(QDateTime::currentDateTimeUtc()) >= 6) { qCInfo(lcSocialPlugin) << "Will request new syncToken during this sync session"; syncToken.clear(); } } m_connectionsListParams.requestSyncToken = true; m_connectionsListParams.syncToken = syncToken; m_connectionsListParams.personFields = GooglePeople::Person::supportedPersonFields().join(','); // Start the sync if (!m_sqliteSync->startSync()) { m_sqliteSync->deleteLater(); qCWarning(lcSocialPlugin) << "unable to start sync - aborting sync contacts with account:" << m_accountId; setStatus(SocialNetworkSyncAdaptor::Error); } } void GoogleTwoWayContactSyncAdaptor::requestData( DataRequestType requestType, ContactChangeNotifier contactChangeNotifier, const QString &pageToken) { QUrl requestUrl; QUrlQuery urlQuery; if (requestType == ContactGroupRequest) { requestUrl = QUrl(QStringLiteral("https://people.googleapis.com/v1/contactGroups")); // Currently we do not add a syncToken for group requests, as we always fetch the complete // list. } else { requestUrl = QUrl(QStringLiteral("https://people.googleapis.com/v1/people/me/connections")); if (m_connectionsListParams.requestSyncToken) { urlQuery.addQueryItem(QStringLiteral("requestSyncToken"), QStringLiteral("true")); } if (!m_connectionsListParams.syncToken.isEmpty()) { urlQuery.addQueryItem(QStringLiteral("syncToken"), m_connectionsListParams.syncToken); } urlQuery.addQueryItem(QStringLiteral("personFields"), m_connectionsListParams.personFields); } if (!pageToken.isEmpty()) { urlQuery.addQueryItem(QStringLiteral("pageToken"), pageToken); } requestUrl.setQuery(urlQuery); QNetworkRequest req(requestUrl); req.setRawHeader(QString(QLatin1String("Authorization")).toUtf8(), QString(QLatin1String("Bearer ") + m_accessToken).toUtf8()); qCDebug(lcSocialPluginTrace) << "requesting" << requestUrl << "with account" << m_accountId; // we're requesting data. Increment the semaphore so that we know we're still busy. incrementSemaphore(m_accountId); QNetworkReply *reply = m_networkAccessManager->get(req); if (reply) { reply->setProperty("requestType", requestType); reply->setProperty("contactChangeNotifier", contactChangeNotifier); reply->setProperty("accountId", m_accountId); if (requestType == ContactGroupRequest) { connect(reply, &QNetworkReply::finished, this, &GoogleTwoWayContactSyncAdaptor::groupsFinishedHandler); } else { connect(reply, &QNetworkReply::finished, this, &GoogleTwoWayContactSyncAdaptor::contactsFinishedHandler); } connect(reply, static_cast(&QNetworkReply::error), this, &GoogleTwoWayContactSyncAdaptor::errorHandler); connect(reply, &QNetworkReply::sslErrors, this, &GoogleTwoWayContactSyncAdaptor::sslErrorsHandler); m_apiRequestsRemaining -= 1; setupReplyTimeout(m_accountId, reply); } else { qCWarning(lcSocialPlugin) << "unable to request data from Google account with id" << m_accountId; setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(m_accountId); } } void GoogleTwoWayContactSyncAdaptor::groupsFinishedHandler() { QNetworkReply *reply = qobject_cast(sender()); QByteArray data = reply->readAll(); bool isError = reply->property("isError").toBool(); reply->deleteLater(); removeReplyTimeout(m_accountId, reply); if (isError) { qCWarning(lcSocialPlugin) << "error occurred when performing groups request for Google account" << m_accountId; setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(m_accountId); return; } else if (data.isEmpty()) { qCWarning(lcSocialPlugin) << "no groups data in reply from Google with account" << m_accountId; setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(m_accountId); return; } GooglePeopleApiResponse::ContactGroupsResponse response; if (!GooglePeopleApiResponse::readResponse(data, &response)) { qCWarning(lcSocialPlugin) << "unable to parse groups data from reply from Google using account with id" << m_accountId; setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(m_accountId); return; } qCDebug(lcSocialPluginTrace) << "received information about" << response.contactGroups.size() << "groups for account" << m_accountId; GooglePeople::ContactGroup myContactsGroup; for (auto it = response.contactGroups.constBegin(); it != response.contactGroups.constEnd(); ++it) { if (it->isMyContactsGroup()) { myContactsGroup = *it; break; } } if (!myContactsGroup.resourceName.isEmpty()) { // we can now continue with contact sync. m_collection = myContactsGroup.toCollection(m_accountId); m_sqliteSync->remoteCollectionsDetermined(QList() << m_collection); } else if (!response.nextPageToken.isEmpty()) { // request more groups if they exist. requestData(ContactGroupRequest, NoContactChangeNotifier, response.nextPageToken); } else { qCInfo(lcSocialPlugin) << "Cannot find My Contacts group when syncing Google contacts for account:" << m_accountId; m_sqliteSync->remoteCollectionsDetermined(QList()); } decrementSemaphore(m_accountId); } void GoogleTwoWayContactSyncAdaptor::contactsFinishedHandler() { QNetworkReply *reply = qobject_cast(sender()); if (reply->error() == QNetworkReply::ProtocolInvalidOperationError) { QNetworkReply *reply = qobject_cast(sender()); if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 400 && !m_retriedConnectionsList) { qCInfo(lcSocialPlugin) << "Will request new sync token, got error from server:" << reply->readAll(); DataRequestType requestType = static_cast( reply->property("requestType").toInt()); ContactChangeNotifier contactChangeNotifier = static_cast( reply->property("contactChangeNotifier").toInt()); m_connectionsListParams.requestSyncToken = true; m_connectionsListParams.syncToken.clear(); m_retriedConnectionsList = true; requestData(requestType, contactChangeNotifier); decrementSemaphore(m_accountId); return; } } QByteArray data = reply->readAll(); ContactChangeNotifier contactChangeNotifier = static_cast(reply->property("contactChangeNotifier").toInt()); bool isError = reply->property("isError").toBool(); reply->deleteLater(); removeReplyTimeout(m_accountId, reply); if (isError) { qCWarning(lcSocialPlugin) << "error occurred when performing contacts request for Google account" << m_accountId << ", network error was:" << reply->error() << reply->errorString() << "HTTP code:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(m_accountId); return; } else if (data.isEmpty()) { qCWarning(lcSocialPlugin) << "no contact data in reply from Google with account" << m_accountId; setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(m_accountId); return; } GooglePeopleApiResponse::PeopleConnectionsListResponse response; if (!GooglePeopleApiResponse::readResponse(data, &response)) { qCWarning(lcSocialPlugin) << "unable to parse contacts data from reply from Google using account with id" << m_accountId; setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(m_accountId); return; } if (!response.nextSyncToken.isEmpty()) { qCInfo(lcSocialPlugin) << "Received sync token for people.connections.list():" << response.nextSyncToken; const QString dateString = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); m_collection.setExtendedMetaData(CollectionKeySyncToken, response.nextSyncToken); m_collection.setExtendedMetaData(CollectionKeySyncTokenDate, dateString); } QList remoteAddModContacts; QList remoteDelContacts; response.getContacts(m_accountId, QList() << m_collection, &remoteAddModContacts, &remoteDelContacts); qCDebug(lcSocialPluginTrace) << "received information about" << remoteAddModContacts.size() << "add/mod contacts and " << remoteDelContacts.size() << "del contacts" << "for account" << m_accountId; for (QContact c : remoteAddModContacts) { const QString guid = c.detail().guid(); // get the saved etag const QString newEtag = GooglePeople::PersonMetadata::etag(c); if (newEtag.isEmpty()) { qCWarning(lcSocialPlugin) << "No etag found for contact:" << guid; } else if (newEtag == m_contactEtags.value(guid)) { // the etags match, so no remote changes have occurred. // most likely this is a spurious change, however it // may be the case that we have not yet downloaded the // avatar for this contact. Check this. QString remoteAvatarUrl; QString localAvatarFile; const QContactAvatar avatar = GooglePeople::Photo::getPrimaryPhoto(c, &remoteAvatarUrl, &localAvatarFile); if (!localAvatarFile.isEmpty() && !QFile::exists(localAvatarFile)) { // the avatar image has not yet been downloaded. qCDebug(lcSocialPlugin) << "Remote modification spurious except for missing avatar" << guid; m_contactAvatars.insert(guid, remoteAvatarUrl); // enqueue outstanding avatar. } if (m_connectionsListParams.syncToken.isEmpty()) { // This is a fresh sync, so keep the modification. qCDebug(lcSocialPlugin) << "Remote modification for contact:" << guid << "is not spurious, keeping it (this is a fresh sync)"; } else { // This is a delta sync and the modification is spurious, so discard the contact. qCDebug(lcSocialPlugin) << "Disregarding spurious remote modification for contact:" << guid; continue; } } // put contact into added or modified list const QHash::iterator contactIdIter = m_contactIds.find(guid); if (contactIdIter == m_contactIds.end()) { if (m_sqliteSync->isLocallyDeletedGuid(guid)) { qCDebug(lcSocialPluginTrace) << "New remote contact" << guid << "was locally deleted, ignoring"; } else { m_remoteAdds.append(c); qCDebug(lcSocialPluginTrace) << "New remote contact" << guid; } } else { c.setId(QContactId::fromString(contactIdIter.value())); m_remoteMods.append(c); qCDebug(lcSocialPluginTrace) << "Found modified contact " << guid << ", etag now" << newEtag; } } for (auto it = remoteDelContacts.begin(); it != remoteDelContacts.end(); ++it) { QContact c = *it; const QString guid = c.detail().guid(); const QString idStr = m_contactIds.value(guid); if (idStr.isEmpty()) { qCWarning(lcSocialPlugin) << "Unable to find deleted contact with guid: " << guid; } else { c.setId(QContactId::fromString(idStr)); m_contactAvatars.remove(guid); // just in case the avatar was outstanding. m_remoteDels.append(c); } } if (!response.nextPageToken.isEmpty()) { // request more if they exist. qCDebug(lcSocialPluginTrace) << "more contact sync information is available server-side; performing another request with account" << m_accountId; requestData(ContactRequest, contactChangeNotifier, response.nextPageToken); } else { // we're finished downloading the remote changes - we should sync local changes up. qCInfo(lcSocialPlugin) << "Google contact sync with account" << m_accountId << "got remote changes: A/M/R:" << m_remoteAdds.count() << m_remoteMods.count() << m_remoteDels.count(); continueSync(contactChangeNotifier); } decrementSemaphore(m_accountId); } void GoogleTwoWayContactSyncAdaptor::continueSync(ContactChangeNotifier contactChangeNotifier) { // early out in case we lost connectivity if (syncAborted()) { qCWarning(lcSocialPlugin) << "aborting sync of account" << m_accountId; setStatus(SocialNetworkSyncAdaptor::Error); // note: don't decrement here - it's done by contactsFinishedHandler(). return; } // avatars of the added and modified contacts will need to be downloaded for (int i = 0; i < m_remoteAdds.size(); ++i) { addAvatarToDownload(&m_remoteAdds[i]); } for (int i = 0; i < m_remoteMods.size(); ++i) { addAvatarToDownload(&m_remoteMods[i]); } // now store the changes locally qCDebug(lcSocialPluginTrace) << "storing remote changes locally for account" << m_accountId; if (contactChangeNotifier == DetermineRemoteContactChanges) { m_sqliteSync->remoteContactChangesDetermined(m_collection, m_remoteAdds, m_remoteMods, m_remoteDels); } else { m_sqliteSync->remoteContactsDetermined(m_collection, m_remoteAdds + m_remoteMods); } } void GoogleTwoWayContactSyncAdaptor::upsyncLocalChanges(const QList &locallyAdded, const QList &locallyModified, const QList &locallyDeleted) { QSet alreadyEncoded; // shouldn't be necessary, as determineLocalChanges should already ensure distinct result sets. for (const QContact &c : locallyDeleted) { const QString &guid = c.detail().guid(); if (!guid.isEmpty()) { m_localDels.append(c); m_contactAvatars.remove(guid); // just in case the avatar was outstanding. alreadyEncoded.insert(guid); } else { qCInfo(lcSocialPlugin) << "Ignore locally-deleted contact" << c.id() << ", was not uploaded to server prior to local deletion"; } } for (const QContact &c : locallyAdded) { const QString guid = c.detail().guid(); if (!alreadyEncoded.contains(guid)) { m_localAdds.append(c); if (!guid.isEmpty()) { alreadyEncoded.insert(guid); } QString remoteAvatarUrl; QString localAvatarFile; GooglePeople::Photo::getPrimaryPhoto(c, &remoteAvatarUrl, &localAvatarFile); if (remoteAvatarUrl.isEmpty() && !localAvatarFile.isEmpty()) { // The avatar was created locally and needs to be uploaded. qCDebug(lcSocialPluginTrace) << "Will upsync avatar for new contact" << guid; m_localAvatarAdds.append(c); } } } for (const QContact &c : locallyModified) { const QString guid = c.detail().guid(); if (!alreadyEncoded.contains(guid)) { m_localMods.append(c); // Determine the type of avatar change to be uploaded. QString remoteAvatarUrl; QString localAvatarFile; const QContactAvatar avatar = GooglePeople::Photo::getPrimaryPhoto(c, &remoteAvatarUrl, &localAvatarFile); const int changeFlag = avatar.value(QContactDetail__FieldChangeFlags).toInt(); if (changeFlag & QContactDetail__ChangeFlag_IsDeleted) { qCDebug(lcSocialPluginTrace) << "Will upsync avatar deletion for contact" << guid; m_localAvatarDels.append(c); } else if ((changeFlag & QContactDetail__ChangeFlag_IsAdded) || (changeFlag & QContactDetail__ChangeFlag_IsModified)) { if (localAvatarFile.isEmpty()) { qCDebug(lcSocialPluginTrace) << "Will upsync avatar deletion for contact" << guid; m_localAvatarDels.append(c); } else { qCDebug(lcSocialPluginTrace) << "Will upsync avatar modification for contact" << guid; // This is a local file, so upload it. The server will generate a remote image // url for it and provide the url in the response, that we then can download. // Note that the contact is added to m_localAvatarMods and not m_localAvatarAdds // even if it is a new avatar file, because this is for an existing contact, // not a new contact. m_localAvatarMods.append(c); } } } } m_batchUpdateIndexes.clear(); qCInfo(lcSocialPlugin) << "Google account:" << m_accountId << "upsyncing local contact A/M/R:" << m_localAdds.count() << "/" << m_localMods.count() << "/" << m_localDels.count() << "and local avatar A/M/R:" << m_localAvatarAdds.count() << "/" << m_localAvatarMods.count() << "/" << m_localAvatarDels.count(); upsyncLocalChangesList(); } bool GoogleTwoWayContactSyncAdaptor::batchRemoteChanges(BatchedUpdate *batchedUpdate, QList *contacts, GooglePeopleApi::OperationType updateType) { int batchUpdateIndex = m_batchUpdateIndexes.value(updateType, contacts->count() - 1); while (batchUpdateIndex >= 0 && batchUpdateIndex < contacts->count()) { const QContact &contact = contacts->at(batchUpdateIndex--); m_batchUpdateIndexes[updateType] = batchUpdateIndex; batchedUpdate->batch[updateType].append(contact); batchedUpdate->batchCount++; if (batchUpdateIndex <= 0) { const QByteArray encodedContactUpdates = GooglePeopleApiRequest::writeMultiPartRequest(batchedUpdate->batch); if (encodedContactUpdates.isEmpty()) { qCInfo(lcSocialPlugin) << "No data changes found, no non-avatar changes to upsync for contact" << contact.id() << "guid" << contact.detail().guid(); } else { qCDebug(lcSocialPluginTrace) << "storing a batch of" << batchedUpdate->batchCount << "local changes to remote server for account" << m_accountId; } batchedUpdate->batch.clear(); batchedUpdate->batchCount = 0; if (!encodedContactUpdates.isEmpty()) { storeToRemote(encodedContactUpdates); return true; } } } return false; } void GoogleTwoWayContactSyncAdaptor::upsyncLocalChangesList() { bool postedData = false; if (!m_accountSyncProfile || m_accountSyncProfile->syncDirection() != Buteo::SyncProfile::SYNC_DIRECTION_FROM_REMOTE) { // two-way sync is the default setting. Upsync the changes. BatchedUpdate batch; if (!postedData) { postedData = batchRemoteChanges(&batch, &m_localAdds, GooglePeopleApi::CreateContact); } if (!postedData) { postedData = batchRemoteChanges(&batch, &m_localMods, GooglePeopleApi::UpdateContact); } if (!postedData) { postedData = batchRemoteChanges(&batch, &m_localDels, GooglePeopleApi::DeleteContact); } if (!postedData) { // The avatar additions must be sent after the CreateContact calls, so that we have a // valid Person resourceName to attach to the UpdateContactPhoto call. postedData = batchRemoteChanges(&batch, &m_localAvatarAdds, GooglePeopleApi::AddContactPhoto); } if (!postedData) { postedData = batchRemoteChanges(&batch, &m_localAvatarMods, GooglePeopleApi::UpdateContactPhoto); } if (!postedData) { postedData = batchRemoteChanges(&batch, &m_localAvatarDels, GooglePeopleApi::DeleteContactPhoto); } } else { qCInfo(lcSocialPlugin) << "skipping upload of local contacts changes due to profile direction setting for account" << m_accountId; } if (!postedData) { qCInfo(lcSocialPlugin) << "All upsync requests sent"; // Nothing left to upsync. // notify TWCSA that the upsync is complete. m_sqliteSync->localChangesStoredRemotely(m_collection, m_localAdds, m_localMods); } } void GoogleTwoWayContactSyncAdaptor::storeToRemote(const QByteArray &encodedContactUpdates) { QUrl requestUrl(QLatin1String("https://people.googleapis.com/batch")); QNetworkRequest req(requestUrl); req.setRawHeader(QString(QLatin1String("Authorization")).toUtf8(), QString(QLatin1String("Bearer ") + m_accessToken).toUtf8()); req.setRawHeader(QString(QLatin1String("Authorization")).toUtf8(), QString(QLatin1String("Bearer ") + m_accessToken).toUtf8()); req.setRawHeader(QString(QLatin1String("Content-Type")).toUtf8(), QString(QLatin1String("multipart/mixed; boundary=\"batch_people\"")).toUtf8()); req.setHeader(QNetworkRequest::ContentLengthHeader, encodedContactUpdates.size()); // we're posting data. Increment the semaphore so that we know we're still busy. incrementSemaphore(m_accountId); QNetworkReply *reply = m_networkAccessManager->post(req, encodedContactUpdates); if (reply) { connect(reply, &QNetworkReply::finished, this, &GoogleTwoWayContactSyncAdaptor::postFinishedHandler); connect(reply, static_cast(&QNetworkReply::error), this, &GoogleTwoWayContactSyncAdaptor::postErrorHandler); connect(reply, &QNetworkReply::sslErrors, this, &GoogleTwoWayContactSyncAdaptor::postErrorHandler); m_apiRequestsRemaining -= 1; setupReplyTimeout(m_accountId, reply); } else { qCWarning(lcSocialPlugin) << "unable to post contacts to Google account with id" << m_accountId; setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(m_accountId); } } void GoogleTwoWayContactSyncAdaptor::postFinishedHandler() { QNetworkReply *reply = qobject_cast(sender()); QByteArray response = reply->readAll(); reply->deleteLater(); removeReplyTimeout(m_accountId, reply); if (reply->property("isError").toBool()) { qCWarning(lcSocialPlugin) << "error occurred posting contact data to google with account" << m_accountId << "," << "got response:" << QString::fromUtf8(response); setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(m_accountId); return; } QList operationResponses; if (!GooglePeopleApiResponse::readMultiPartResponse(response, &operationResponses)) { qCWarning(lcSocialPlugin) << "unable to read response for batch operation with Google account" << m_accountId; setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(m_accountId); return; } const QList collections { m_collection }; bool errorOccurredInBatch = false; for (const GooglePeopleApiResponse::BatchResponsePart &response : operationResponses) { GooglePeopleApi::OperationType operationType; QString contactIdString; GooglePeople::Person person; GooglePeopleApiResponse::BatchResponsePart::Error error; response.parse(&operationType, &contactIdString, &person, &error); if (!error.status.isEmpty()) { if (error.code == 404 && (operationType == GooglePeopleApi::DeleteContact || operationType == GooglePeopleApi::DeleteContactPhoto)) { // Couldn't find the remote contact or photo to be deleted; perhaps some previous // change was not synced as expected. This is not a problem as we will just delete // it locally. qCInfo(lcSocialPlugin) << "Unable to delete contact or photo on the server, will just delete it locally." << "id:" << contactIdString << "resource:" << person.resourceName; } else { errorOccurredInBatch = true; qCWarning(lcSocialPlugin) << "batch operation error:\n" " contentId: " << response.contentId << "\n" " error.code: " << error.code << "\n" " error.message: " << error.message << "\n" " error.status: " << error.status << "\n"; } } if (errorOccurredInBatch) { // The sync will finish with an error. Keep looking for other possible errors, but // don't process any more responses. continue; } qCDebug(lcSocialPluginTrace) << "Process response for batched request" << response.contentId << "status =" << response.bodyStatusLine << "body len =" << response.body.length(); if (!person.resourceName.isEmpty()) { qCDebug(lcSocialPlugin) << "Batched response contains Person(resourceName =" << person.resourceName << ")"; } // Save contact etag and other details into the added/modified lists so that the // updated details are saved into the database later. QList *contactList = nullptr; switch (operationType) { case GooglePeopleApi::CreateContact: case GooglePeopleApi::AddContactPhoto: contactList = &m_localAdds; break; case GooglePeopleApi::UpdateContact: case GooglePeopleApi::UpdateContactPhoto: case GooglePeopleApi::DeleteContactPhoto: contactList = &m_localMods; break; case GooglePeopleApi::DeleteContact: // Nothing to do, the response body will be empty. break; case GooglePeopleApi::UnsupportedOperation: break; } if (contactList) { if (!person.isValid()) { qCWarning(lcSocialPlugin) << "Cannot read Person object!"; qCDebug(lcSocialPluginTrace) << "Response data was:" << response.body; continue; } const QContactId contactId = QContactId::fromString(contactIdString); const int listIndex = indexOfContact(*contactList, contactId); if (listIndex < 0) { qCWarning(lcSocialPlugin) << "Cannot save details, contact" << contactId.toString() << " not found in added/modified contacts"; continue; } QContact *contact = &((*contactList)[listIndex]); if (!person.saveToContact(contact, m_accountId, collections)) { qCWarning(lcSocialPlugin) << "Cannot save added/modified details for contact" << contactId.toString(); continue; } if (operationType == GooglePeopleApi::CreateContact) { // The contact has now been assigned a resourceName from the Google server. // If the contact has an avatar to be uploaded in a later batch, update the // guid for the contact in m_localAvatarAdds to ensure the resourceName is // valid when the avatar is uploaded. const int avatarAddIndex = indexOfContact(m_localAvatarAdds, contact->id()); if (avatarAddIndex >= 0) { QContactGuid guid = contact->detail(); m_localAvatarAdds[avatarAddIndex].saveDetail(&guid); } } else if (operationType == GooglePeopleApi::AddContactPhoto || operationType == GooglePeopleApi::UpdateContactPhoto) { // When a contact photo is uploaded to the server, the person's "photos" is // updated with a new remote url for the avatar; add this url to the list of // avatars to be downloaded later. addAvatarToDownload(contact); } } } if (errorOccurredInBatch) { qCWarning(lcSocialPlugin) << "error occurred during batch operation with Google account" << m_accountId; setStatus(SocialNetworkSyncAdaptor::Error); } else { // continue with more, if there were more than one page of updates to post. upsyncLocalChangesList(); } // finished with this request, so decrementing semaphore. decrementSemaphore(m_accountId); } void GoogleTwoWayContactSyncAdaptor::postErrorHandler() { sender()->setProperty("isError", QVariant::fromValue(true)); } void GoogleTwoWayContactSyncAdaptor::syncFinished() { // If this is the first sync, TWCSA will have saved the collection and given it a valid id, so // update collection so that any post-sync operations (e.g. saving of queued avatar downloads) // will refer to a valid collection. if (m_collection.id().isNull()) { const QContactCollection savedCollection = findCollection(*m_contactManager, m_accountId); if (savedCollection.id().isNull()) { qCWarning(lcSocialPlugin) << "Error: cannot find saved My Contacts collection!"; } else { m_collection.setId(savedCollection.id()); } } // Attempt to download any outstanding avatars. queueOutstandingAvatars(); } void GoogleTwoWayContactSyncAdaptor::queueOutstandingAvatars() { int queuedCount = 0; for (QHash::const_iterator it = m_contactAvatars.constBegin(); it != m_contactAvatars.constEnd(); ++it) { if (!it.value().isEmpty() && queueAvatarForDownload(it.key(), it.value())) { queuedCount++; } } qCDebug(lcSocialPluginTrace) << "queued" << queuedCount << "outstanding avatars for download for account" << m_accountId; } bool GoogleTwoWayContactSyncAdaptor::queueAvatarForDownload(const QString &contactGuid, const QString &imageUrl) { if (m_apiRequestsRemaining > 0 && !m_queuedAvatarsForDownload.contains(contactGuid)) { m_apiRequestsRemaining -= 1; m_queuedAvatarsForDownload[contactGuid] = imageUrl; QVariantMap metadata; metadata.insert(IMAGE_DOWNLOADER_TOKEN_KEY, m_accessToken); metadata.insert(IMAGE_DOWNLOADER_IDENTIFIER_KEY, contactGuid); incrementSemaphore(m_accountId); QMetaObject::invokeMethod(m_workerObject, "queue", Qt::QueuedConnection, Q_ARG(QString, imageUrl), Q_ARG(QVariantMap, metadata)); return true; } return false; } bool GoogleTwoWayContactSyncAdaptor::addAvatarToDownload(QContact *contact) { // The avatar detail from the remote contact will be of the form: // https://.googleusercontent.com//photo.jpg" // (The server will generate a new URL whenever the photo content changes, so there is no need // to store a photo etag to track changes.) // If the remote URL has changed, or the file has not been downloaded, then add it to the // list of pending avatar downloads. if (!contact) { return false; } const QString contactGuid = contact->detail().guid(); if (contactGuid.isEmpty()) { return false; } QString remoteAvatarUrl; QString localAvatarFile; const QContactAvatar avatar = GooglePeople::Photo::getPrimaryPhoto( *contact, &remoteAvatarUrl, &localAvatarFile); const QPair prevAvatar = m_previousAvatarUrls.value(contactGuid); const QString prevRemoteAvatarUrl = prevAvatar.first; const QString prevLocalAvatarFile = prevAvatar.second; const bool isNewAvatar = prevRemoteAvatarUrl.isEmpty(); const bool isModifiedAvatar = !isNewAvatar && prevRemoteAvatarUrl != remoteAvatarUrl; const bool isMissingFile = !QFile::exists(localAvatarFile); if (!isNewAvatar && !isModifiedAvatar && !isMissingFile) { // No need to download the file. return false; } if (!prevLocalAvatarFile.isEmpty()) { QFile::remove(prevLocalAvatarFile); } // queue outstanding avatar for download once all upsyncs are complete m_contactAvatars.insert(contactGuid, remoteAvatarUrl); return true; } void GoogleTwoWayContactSyncAdaptor::imageDownloaded(const QString &url, const QString &path, const QVariantMap &metadata) { // Load finished, update the avatar, decrement semaphore QString contactGuid = metadata.value(IMAGE_DOWNLOADER_IDENTIFIER_KEY).toString(); // Empty path signifies that an error occurred. if (path.isEmpty()) { qCWarning(lcSocialPlugin) << "Unable to download avatar" << url; } else { // no longer outstanding. m_contactAvatars.remove(contactGuid); m_queuedAvatarsForDownload.remove(contactGuid); } decrementSemaphore(m_accountId); } void GoogleTwoWayContactSyncAdaptor::purgeAccount(int pid) { QtContactsSqliteExtensions::ContactManagerEngine *cme = QtContactsSqliteExtensions::contactManagerEngine(*m_contactManager); QContactManager::Error error = QContactManager::NoError; QList addedCollections; QList modifiedCollections; QList deletedCollections; QList unmodifiedCollections; if (!cme->fetchCollectionChanges(pid, qAppName(), &addedCollections, &modifiedCollections, &deletedCollections, &unmodifiedCollections, &error)) { qCWarning(lcSocialPlugin) << "Cannot find collection for account" << pid << "error:" << error; return; } const QList collections = addedCollections + modifiedCollections + deletedCollections + unmodifiedCollections; if (collections.isEmpty()) { qCInfo(lcSocialPlugin) << "Nothing to purge, no collection has been saved for account" << pid; return; } for (const QContactCollection &collection : collections) { // Delete local avatar image files. QContactCollectionFilter collectionFilter; collectionFilter.setCollectionId(collection.id()); QContactFetchHint fetchHint; fetchHint.setOptimizationHints(QContactFetchHint::NoRelationships); fetchHint.setDetailTypesHint(QList() << QContactDetail::TypeGuid << QContactDetail::TypeAvatar); const QList savedContacts = m_contactManager->contacts(collectionFilter, QList(), fetchHint); for (const QContact &contact : savedContacts) { const QList avatars = contact.details(); for (const QContactAvatar &avatar : avatars) { const QString localFilePath = avatar.imageUrl().toString(); if (!localFilePath.isEmpty() && !QFile::remove(localFilePath)) { qCWarning(lcSocialPlugin) << "Failed to remove avatar:" << localFilePath; } } } } QList collectionIds; for (const QContactCollection &collection : collections) { collectionIds.append(collection.id()); } // Delete the collection and its contacts. if (cme->storeChanges(nullptr, nullptr, collectionIds, QtContactsSqliteExtensions::ContactManagerEngine::PreserveLocalChanges, true, &error)) { qCInfo(lcSocialPlugin) << "purged account" << pid << "and successfully removed collections" << collectionIds; } else { qCWarning(lcSocialPlugin) << "Failed to remove My Contacts collection during purge of account" << pid << "error:" << error; } } void GoogleTwoWayContactSyncAdaptor::finalize(int accountId) { if (syncAborted()|| status() == SocialNetworkSyncAdaptor::Error) { m_sqliteSync->syncFinishedWithError(); return; } if (accountId != m_accountId || m_accessToken.isEmpty()) { // account failure occurred before sync process was started, // in this case we have nothing left to do except cleanup. return; } // sync was successful, allow cleaning up contacts from removed accounts. m_allowFinalCleanup = true; } void GoogleTwoWayContactSyncAdaptor::finalCleanup() { // Only perform the cleanup if the sync cycle was successful. // Note: purgeDataForOldAccount() will still be invoked by Buteo // in response to the account being deleted when restoring the // backup, so we cannot avoid the problem of "lost contacts" // completely. See JB#38210 for more information. if (!m_allowFinalCleanup) { return; } // Synchronously find any contacts which need to be removed, // which were somehow "left behind" by the sync process. // first, get a list of all existing google account ids QList googleAccountIds; QList purgeAccountIds; QList currentAccountIds; QList uaids = m_accountManager->accountList(); Q_FOREACH (uint uaid, uaids) { currentAccountIds.append(static_cast(uaid)); } for (int currId : currentAccountIds) { Accounts::Account *act = Accounts::Account::fromId(m_accountManager, currId, this); if (act) { if (act->providerName() == QString(QLatin1String("google"))) { // this account still exists, no need to purge its content. googleAccountIds.append(currId); } act->deleteLater(); } } // find all account ids from which contacts have been synced const QList collections = m_contactManager->collections(); for (const QContactCollection &collection : collections) { if (GooglePeople::ContactGroup::isMyContactsCollection(collection)) { const int purgeId = collection.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID).toInt(); if (purgeId && !googleAccountIds.contains(purgeId) && !purgeAccountIds.contains(purgeId)) { // this account no longer exists, and needs to be purged. purgeAccountIds.append(purgeId); } } } // purge all data for those account ids which no longer exist. if (purgeAccountIds.size()) { qCInfo(lcSocialPlugin) << "finalCleanup() purging contacts from" << purgeAccountIds.size() << "non-existent Google accounts"; for (int purgeId : purgeAccountIds) { purgeAccount(purgeId); } } } void GoogleTwoWayContactSyncAdaptor::loadCollection(const QContactCollection &collection) { QContactCollectionFilter collectionFilter; collectionFilter.setCollectionId(collection.id()); QContactFetchHint noRelationships; noRelationships.setOptimizationHints(QContactFetchHint::NoRelationships); QList savedContacts = m_contactManager->contacts(collectionFilter, QList(), noRelationships); for (const QContact &contact : savedContacts) { const QString contactGuid = contact.detail().guid(); if (contactGuid.isEmpty()) { qCDebug(lcSocialPlugin) << "No guid found for saved contact, must be new:" << contact.id(); continue; } // m_contactEtags const QString etag = GooglePeople::PersonMetadata::etag(contact); if (!etag.isEmpty()) { m_contactEtags[contactGuid] = etag; } // m_contactIds m_contactIds[contactGuid] = contact.id().toString(); // m_avatarImageUrls QString remoteAvatarUrl; QString localAvatarFile; GooglePeople::Photo::getPrimaryPhoto(contact, &remoteAvatarUrl, &localAvatarFile); m_previousAvatarUrls.insert(contactGuid, qMakePair(remoteAvatarUrl,localAvatarFile)); } } buteo-sync-plugins-social-0.4.28/src/google/google-contacts/googletwowaycontactsyncadaptor.h000066400000000000000000000157251474572147200325320ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2014 - 2019 Jolla Ltd. ** Copyright (c) 2020 Open Mobile Platform LLC. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef GOOGLETWOWAYCONTACTSYNCADAPTOR_H #define GOOGLETWOWAYCONTACTSYNCADAPTOR_H #include "googledatatypesyncadaptor.h" #include "googlepeopleapi.h" #include #include #include #include #include #include #include QTCONTACTS_USE_NAMESPACE class GoogleContactImageDownloader; class GoogleTwoWayContactSyncAdaptor; class GoogleContactSqliteSyncAdaptor : public QObject, public QtContactsSqliteExtensions::TwoWayContactSyncAdaptor { Q_OBJECT public: GoogleContactSqliteSyncAdaptor(int accountId, GoogleTwoWayContactSyncAdaptor *parent); ~GoogleContactSqliteSyncAdaptor(); bool isLocallyDeletedGuid(const QString &guid) const; virtual bool determineRemoteCollections() override; virtual bool deleteRemoteCollection(const QContactCollection &collection) override; virtual bool determineRemoteContacts(const QContactCollection &collection) override; virtual bool determineRemoteContactChanges(const QContactCollection &collection, const QList &localAddedContacts, const QList &localModifiedContacts, const QList &localDeletedContacts, const QList &localUnmodifiedContacts, QContactManager::Error *error) override; virtual bool storeLocalChangesRemotely(const QContactCollection &collection, const QList &addedContacts, const QList &modifiedContacts, const QList &deletedContacts) override; virtual void storeRemoteChangesLocally(const QContactCollection &collection, const QList &addedContacts, const QList &modifiedContacts, const QList &deletedContacts) override; virtual void syncFinishedSuccessfully() override; virtual void syncFinishedWithError() override; private: GoogleTwoWayContactSyncAdaptor *q; }; class GoogleTwoWayContactSyncAdaptor : public GoogleDataTypeSyncAdaptor { Q_OBJECT public: enum DataRequestType { ContactRequest, ContactGroupRequest }; enum ContactChangeNotifier { NoContactChangeNotifier, DetermineRemoteContacts, DetermineRemoteContactChanges }; Q_ENUM(ContactChangeNotifier) GoogleTwoWayContactSyncAdaptor(QObject *parent); ~GoogleTwoWayContactSyncAdaptor(); virtual QString syncServiceName() const override; virtual void sync(const QString &dataTypeString, int accountId) override; void requestData(DataRequestType requestType, ContactChangeNotifier contactChangeNotifier = NoContactChangeNotifier, const QString &pageToken = QString()); void upsyncLocalChanges(const QList &locallyAdded, const QList &locallyModified, const QList &locallyDeleted); void syncFinished(); protected: // implementing GoogleDataTypeSyncAdaptor interface void purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode mode) override; void beginSync(int accountId, const QString &accessToken) override; void finalize(int accountId) override; void finalCleanup() override; private: friend class GoogleContactSqliteSyncAdaptor; class BatchedUpdate { public: QMap > batch; int batchCount = 0; }; void groupsFinishedHandler(); void contactsFinishedHandler(); void continueSync(GoogleTwoWayContactSyncAdaptor::ContactChangeNotifier contactChangeNotifier); void upsyncLocalChangesList(); bool batchRemoteChanges(BatchedUpdate *batchedUpdate, QList *contacts, GooglePeopleApi::OperationType updateType); void storeToRemote(const QByteArray &encodedContactUpdates); void queueOutstandingAvatars(); bool queueAvatarForDownload(const QString &contactGuid, const QString &imageUrl); bool addAvatarToDownload(QContact *contact); void imageDownloaded(const QString &url, const QString &path, const QVariantMap &metadata); void loadCollection(const QContactCollection &collection); void purgeAccount(int pid); void postFinishedHandler(); void postErrorHandler(); QList m_remoteAdds; QList m_remoteMods; QList m_remoteDels; QList m_localAdds; QList m_localMods; QList m_localDels; QList m_localAvatarAdds; QList m_localAvatarMods; QList m_localAvatarDels; QHash m_contactEtags; // contact guid -> contact etag QHash m_contactIds; // contact guid -> contact id QHash m_contactAvatars; // contact guid -> remote avatar path QHash > m_previousAvatarUrls; QHash m_batchUpdateIndexes; QHash m_queuedAvatarsForDownload; // contact guid -> remote avatar path QContactManager *m_contactManager = nullptr; GoogleContactSqliteSyncAdaptor *m_sqliteSync = nullptr; GoogleContactImageDownloader *m_workerObject = nullptr; QContactCollection m_collection; QString m_accessToken; struct PeopleConnectionsListParameters { bool requestSyncToken; QString syncToken; QString personFields; } m_connectionsListParams; int m_accountId = 0; int m_apiRequestsRemaining = 0; bool m_retriedConnectionsList = false; bool m_allowFinalCleanup = false; }; #endif // GOOGLETWOWAYCONTACTSYNCADAPTOR_H buteo-sync-plugins-social-0.4.28/src/google/google-signon/000077500000000000000000000000001474572147200234535ustar00rootroot00000000000000buteo-sync-plugins-social-0.4.28/src/google/google-signon/google-signon.pri000066400000000000000000000001561474572147200267400ustar00rootroot00000000000000SOURCES += $$PWD/googlesignonsyncadaptor.cpp HEADERS += $$PWD/googlesignonsyncadaptor.h INCLUDEPATH += $$PWD buteo-sync-plugins-social-0.4.28/src/google/google-signon/google-signon.pro000066400000000000000000000012041474572147200267410ustar00rootroot00000000000000TARGET = google-signon-client include($$PWD/../../common.pri) include($$PWD/../google-common.pri) include($$PWD/google-signon.pri) google_signon_sync_profile.path = /etc/buteo/profiles/sync google_signon_sync_profile.files = $$PWD/google.Signon.xml google_signon_client_plugin_xml.path = /etc/buteo/profiles/client google_signon_client_plugin_xml.files = $$PWD/google-signon.xml HEADERS += googlesignonplugin.h SOURCES += googlesignonplugin.cpp OTHER_FILES += \ google_signon_sync_profile.files \ google_signon_client_plugin_xml.files INSTALLS += \ target \ google_signon_sync_profile \ google_signon_client_plugin_xml buteo-sync-plugins-social-0.4.28/src/google/google-signon/google-signon.xml000066400000000000000000000002041474572147200267400ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/google/google-signon/google.Signon.xml000066400000000000000000000011141474572147200267020ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/google/google-signon/googlesignonplugin.cpp000066400000000000000000000035461474572147200301000ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2014 - 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "googlesignonplugin.h" #include "googlesignonsyncadaptor.h" #include "socialnetworksyncadaptor.h" GoogleSignonPlugin::GoogleSignonPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *callbackInterface) : SocialdButeoPlugin(pluginName, profile, callbackInterface, QStringLiteral("google"), SocialNetworkSyncAdaptor::dataTypeName(SocialNetworkSyncAdaptor::Signon)) { } GoogleSignonPlugin::~GoogleSignonPlugin() { } SocialNetworkSyncAdaptor *GoogleSignonPlugin::createSocialNetworkSyncAdaptor() { return new GoogleSignonSyncAdaptor(this); } Buteo::ClientPlugin* GoogleSignonPluginLoader::createClientPlugin( const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) { return new GoogleSignonPlugin(pluginName, profile, cbInterface); } buteo-sync-plugins-social-0.4.28/src/google/google-signon/googlesignonplugin.h000066400000000000000000000035611474572147200275420ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2014 - 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef GOOGLESIGNONPLUGIN_H #define GOOGLESIGNONPLUGIN_H #include "socialdbuteoplugin.h" #include class Q_DECL_EXPORT GoogleSignonPlugin : public SocialdButeoPlugin { Q_OBJECT public: GoogleSignonPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *cbInterface); ~GoogleSignonPlugin(); protected: SocialNetworkSyncAdaptor *createSocialNetworkSyncAdaptor(); }; class GoogleSignonPluginLoader : public Buteo::SyncPluginLoader { Q_OBJECT Q_PLUGIN_METADATA(IID "org.sailfishos.plugins.sync.GoogleSignonPluginLoader") Q_INTERFACES(Buteo::SyncPluginLoader) public: Buteo::ClientPlugin* createClientPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) override; }; #endif // GOOGLESIGNONPLUGIN_H buteo-sync-plugins-social-0.4.28/src/google/google-signon/googlesignonsyncadaptor.cpp000066400000000000000000000264561474572147200311360ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2013-2014 Jolla Ltd. ** Contact: Chris Adams ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "googlesignonsyncadaptor.h" #include "trace.h" #include #include #include #include #include #include #include GoogleSignonSyncAdaptor::GoogleSignonSyncAdaptor(QObject *parent) : GoogleDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::Signon, parent) { setInitialActive(true); } GoogleSignonSyncAdaptor::~GoogleSignonSyncAdaptor() { } QString GoogleSignonSyncAdaptor::syncServiceName() const { return QStringLiteral("google-sync"); // TODO: change name of service to google-signon! } void GoogleSignonSyncAdaptor::sync(const QString &dataTypeString, int accountId) { // call superclass impl. GoogleDataTypeSyncAdaptor::sync(dataTypeString, accountId); } void GoogleSignonSyncAdaptor::purgeDataForOldAccount(int, SocialNetworkSyncAdaptor::PurgeMode) { // Nothing to do. } void GoogleSignonSyncAdaptor::finalize(int) { // nothing to do } void GoogleSignonSyncAdaptor::beginSync(int accountId, const QString &accessToken) { Q_UNUSED(accessToken); refreshTokens(accountId); } Accounts::Account *GoogleSignonSyncAdaptor::loadAccount(int accountId) { Accounts::Account *acc = 0; if (m_accounts.contains(accountId)) { acc = m_accounts[accountId]; } else { acc = Accounts::Account::fromId(&m_accountManager, accountId, this); if (!acc) { qCWarning(lcSocialPlugin) << QString(QLatin1String("error: Google account %1 was deleted during signon refresh sync")) .arg(accountId); return 0; } else { m_accounts.insert(accountId, acc); } } Accounts::Service srv = m_accountManager.service(syncServiceName()); if (!srv.isValid()) { qCWarning(lcSocialPlugin) << QString(QLatin1String("error: invalid service %1 specified for refresh sync with Google account: %2")) .arg(syncServiceName()).arg(accountId); return 0; } return acc; } void GoogleSignonSyncAdaptor::raiseCredentialsNeedUpdateFlag(int accountId) { Accounts::Account *acc = loadAccount(accountId); if (acc) { qCWarning(lcSocialPlugin) << "GSSA: raising CredentialsNeedUpdate flag"; Accounts::Service srv = m_accountManager.service(syncServiceName()); acc->selectService(srv); acc->setValue(QStringLiteral("CredentialsNeedUpdate"), QVariant::fromValue(true)); acc->setValue(QStringLiteral("CredentialsNeedUpdateFrom"), QVariant::fromValue(QString::fromLatin1("sociald-google-signon"))); acc->selectService(Accounts::Service()); acc->syncAndBlock(); } } void GoogleSignonSyncAdaptor::lowerCredentialsNeedUpdateFlag(int accountId) { Accounts::Account *acc = loadAccount(accountId); if (acc) { qCInfo(lcSocialPlugin) << "GSSA: lowering CredentialsNeedUpdate flag"; Accounts::Service srv = m_accountManager.service(syncServiceName()); acc->selectService(srv); acc->setValue(QStringLiteral("CredentialsNeedUpdate"), QVariant::fromValue(false)); acc->remove(QStringLiteral("CredentialsNeedUpdateFrom")); acc->selectService(Accounts::Service()); acc->syncAndBlock(); } } void GoogleSignonSyncAdaptor::refreshTokens(int accountId) { Accounts::Account *acc = loadAccount(accountId); if (!acc) { return; } // First perform a "normal" signon. Then force token expiry. Then signon to refresh the tokens. Accounts::Service srv(m_accountManager.service(syncServiceName())); acc->selectService(srv); SignOn::Identity *identity = acc->credentialsId() > 0 ? SignOn::Identity::existingIdentity(acc->credentialsId()) : 0; if (!identity) { qCWarning(lcSocialPlugin) << QString(QLatin1String("error: Google account %1 has no valid credentials, cannot perform refresh sync")) .arg(accountId); return; } Accounts::AccountService *accSrv = new Accounts::AccountService(acc, srv); if (!accSrv) { qCWarning(lcSocialPlugin) << QString(QLatin1String("error: Google account %1 has no valid account service, cannot perform refresh sync")) .arg(accountId); identity->deleteLater(); return; } QString method = accSrv->authData().method(); QString mechanism = accSrv->authData().mechanism(); SignOn::AuthSession *session = identity->createSession(method); if (!session) { qCWarning(lcSocialPlugin) << QString(QLatin1String("error: could not create signon session for Google account %1, cannot perform refresh sync")) .arg(accountId); accSrv->deleteLater(); identity->deleteLater(); return; } QVariantMap signonSessionData = accSrv->authData().parameters(); signonSessionData.insert("ClientId", clientId()); signonSessionData.insert("ClientSecret", clientSecret()); signonSessionData.insert("UiPolicy", SignOn::NoUserInteractionPolicy); connect(session, SIGNAL(response(SignOn::SessionData)), this, SLOT(initialSignonResponse(SignOn::SessionData)), Qt::UniqueConnection); connect(session, SIGNAL(error(SignOn::Error)), this, SLOT(signonError(SignOn::Error)), Qt::UniqueConnection); incrementSemaphore(accountId); session->setProperty("accountId", accountId); session->setProperty("mechanism", mechanism); session->setProperty("signonSessionData", signonSessionData); m_idents.insert(accountId, identity); session->process(SignOn::SessionData(signonSessionData), mechanism); } void GoogleSignonSyncAdaptor::initialSignonResponse(const SignOn::SessionData &responseData) { SignOn::AuthSession *session = qobject_cast(sender()); session->disconnect(this); if (syncAborted()) { // don't expire the tokens - we may have lost network connectivity // while we were attempting to perform signon sync, and that would // leave us in a position where we're unable to automatically recover. int accountId = session->property("accountId").toInt(); qCInfo(lcSocialPlugin) << "aborting signon sync refresh"; decrementSemaphore(accountId); return; } connect(session, SIGNAL(response(SignOn::SessionData)), this, SLOT(forceTokenExpiryResponse(SignOn::SessionData)), Qt::UniqueConnection); connect(session, SIGNAL(error(SignOn::Error)), this, SLOT(signonError(SignOn::Error)), Qt::UniqueConnection); QString mechanism = session->property("mechanism").toString(); QVariantMap signonSessionData = session->property("signonSessionData").toMap(); // Now expire the tokens. QVariantMap providedTokens; providedTokens.insert("AccessToken", responseData.getProperty(QStringLiteral("AccessToken")).toString()); providedTokens.insert("RefreshToken", responseData.getProperty(QStringLiteral("RefreshToken")).toString()); providedTokens.insert("ExpiresIn", 2); signonSessionData.insert("ProvidedTokens", providedTokens); session->process(SignOn::SessionData(signonSessionData), mechanism); } void GoogleSignonSyncAdaptor::forceTokenExpiryResponse(const SignOn::SessionData &) { SignOn::AuthSession *session = qobject_cast(sender()); session->disconnect(this); QString mechanism = session->property("mechanism").toString(); QVariantMap signonSessionData = session->property("signonSessionData").toMap(); QTimer *timer = new QTimer(this); timer->setInterval(4000); timer->setSingleShot(true); timer->setProperty("mechanism", mechanism); timer->setProperty("signonSessionData", signonSessionData); timer->setProperty("session", QVariant::fromValue(session)); connect(timer, SIGNAL(timeout()), this, SLOT(triggerRefresh())); timer->start(); } void GoogleSignonSyncAdaptor::triggerRefresh() { QTimer *timer = qobject_cast(sender()); timer->deleteLater(); QString mechanism = timer->property("mechanism").toString(); QVariantMap signonSessionData = timer->property("signonSessionData").toMap(); SignOn::AuthSession *session = timer->property("session").value(); connect(session, SIGNAL(response(SignOn::SessionData)), this, SLOT(refreshTokenResponse(SignOn::SessionData)), Qt::UniqueConnection); connect(session, SIGNAL(error(SignOn::Error)), this, SLOT(signonError(SignOn::Error)), Qt::UniqueConnection); session->process(SignOn::SessionData(signonSessionData), mechanism); } void GoogleSignonSyncAdaptor::refreshTokenResponse(const SignOn::SessionData &responseData) { SignOn::AuthSession *session = qobject_cast(sender()); int accountId = session->property("accountId").toInt(); session->disconnect(this); SignOn::Identity *identity = m_idents.take(accountId); if (identity) { identity->destroySession(session); identity->deleteLater(); } else { session->deleteLater(); } qCInfo(lcSocialPlugin) << QString(QLatin1String("successfully performed signon refresh for Google account %1: new ExpiresIn: %3")) .arg(accountId).arg(responseData.getProperty("ExpiresIn").toInt()); lowerCredentialsNeedUpdateFlag(accountId); decrementSemaphore(accountId); } void GoogleSignonSyncAdaptor::signonError(const SignOn::Error &error) { SignOn::AuthSession *session = qobject_cast(sender()); int accountId = session->property("accountId").toInt(); session->disconnect(this); SignOn::Identity *identity = m_idents.take(accountId); if (identity) { identity->destroySession(session); identity->deleteLater(); } else { session->deleteLater(); } bool raiseFlag = error.type() == SignOn::Error::UserInteraction; qCInfo(lcSocialPlugin) << QString(QLatin1String("got signon error when performing signon refresh for Google account %1: %2: %3. Raising flag? %4")) .arg(accountId).arg(error.type()).arg(error.message()).arg(raiseFlag); if (raiseFlag) { // UserInteraction error is returned if user interaction is required. raiseCredentialsNeedUpdateFlag(accountId); } decrementSemaphore(accountId); } buteo-sync-plugins-social-0.4.28/src/google/google-signon/googlesignonsyncadaptor.h000066400000000000000000000052171474572147200305730ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2013-2014 Jolla Ltd. ** Contact: Chris Adams ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef GOOGLESIGNONSYNCADAPTOR_H #define GOOGLESIGNONSYNCADAPTOR_H #include "googledatatypesyncadaptor.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include class GoogleSignonSyncAdaptor : public GoogleDataTypeSyncAdaptor { Q_OBJECT public: GoogleSignonSyncAdaptor(QObject *parent); ~GoogleSignonSyncAdaptor(); QString syncServiceName() const; void sync(const QString &dataTypeString, int accountId = 0); protected: // implementing GoogleDataTypeSyncAdaptor interface void purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode mode); void beginSync(int accountId, const QString &accessToken); void finalize(int accountId); private Q_SLOTS: void initialSignonResponse(const SignOn::SessionData &responseData); void forceTokenExpiryResponse(const SignOn::SessionData &responseData); void refreshTokenResponse(const SignOn::SessionData &responseData); void signonError(const SignOn::Error &error); void triggerRefresh(); private: Accounts::Account *loadAccount(int accountId); void raiseCredentialsNeedUpdateFlag(int accountId); void lowerCredentialsNeedUpdateFlag(int accountId); void refreshTokens(int accountId); Accounts::Manager m_accountManager; QMap m_accounts; QMap m_idents; }; #endif // GOOGLESIGNONSYNCADAPTOR_H buteo-sync-plugins-social-0.4.28/src/google/google.pro000066400000000000000000000002101474572147200226730ustar00rootroot00000000000000TEMPLATE = subdirs SUBDIRS = \ $$PWD/google-contacts \ $$PWD/google-signon CONFIG(calendar): SUBDIRS += $$PWD/google-calendars buteo-sync-plugins-social-0.4.28/src/google/googledatatypesyncadaptor.cpp000066400000000000000000000277751474572147200267120ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2013-2014 Jolla Ltd. ** Contact: Chris Adams ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "googledatatypesyncadaptor.h" #include "trace.h" #include #include #include #include #include //libsailfishkeyprovider #include // libaccounts-qt5 #include #include #include #include //libsignon-qt: SignOn::NoUserInteractionPolicy #include #include #include GoogleDataTypeSyncAdaptor::GoogleDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::DataType dataType, QObject *parent) : SocialNetworkSyncAdaptor("google", dataType, 0, parent), m_triedLoading(false) { } GoogleDataTypeSyncAdaptor::~GoogleDataTypeSyncAdaptor() { } void GoogleDataTypeSyncAdaptor::sync(const QString &dataTypeString, int accountId) { if (dataTypeString != SocialNetworkSyncAdaptor::dataTypeName(m_dataType)) { qCWarning(lcSocialPlugin) << "Google" << SocialNetworkSyncAdaptor::dataTypeName(m_dataType) << "sync adaptor was asked to sync" << dataTypeString; setStatus(SocialNetworkSyncAdaptor::Error); return; } #ifdef USE_SAILFISHKEYPROVIDER if (clientId().isEmpty()) { qCWarning(lcSocialPlugin) << "client id couldn't be retrieved for Google account" << accountId; setStatus(SocialNetworkSyncAdaptor::Error); return; } if (clientSecret().isEmpty()) { qCWarning(lcSocialPlugin) << "client secret couldn't be retrieved for Google account" << accountId; setStatus(SocialNetworkSyncAdaptor::Error); return; } #endif setStatus(SocialNetworkSyncAdaptor::Busy); updateDataForAccount(accountId); qCDebug(lcSocialPlugin) << "successfully triggered sync with profile:" << m_accountSyncProfile->name(); } void GoogleDataTypeSyncAdaptor::updateDataForAccount(int accountId) { Accounts::Account *account = Accounts::Account::fromId(m_accountManager, accountId, this); if (!account) { qCWarning(lcSocialPlugin) << "existing account with id" << accountId << "couldn't be retrieved"; setStatus(SocialNetworkSyncAdaptor::Error); return; } // will be decremented by either signOnError or signOnResponse. incrementSemaphore(accountId); signIn(account); } void GoogleDataTypeSyncAdaptor::finalCleanup() { } void GoogleDataTypeSyncAdaptor::errorHandler(QNetworkReply::NetworkError err) { // Google sends error code 204 (HTTP code 401) for Unauthorized Error // Note: sometimes it sends it spuriously // For now, don't raise the flag, until we can solve // any API rate limit issues associated with avatars // which might cause this (if multiple accounts are involved). // Another possible cause might be: if the ExpiresIn time // is small (less than 30 seconds, say) it's possible that the // access token will expire _during_ the sync process. // XXX TODO: check expires time, force refresh if < 30. QNetworkReply *reply = qobject_cast(sender()); if (err == QNetworkReply::AuthenticationRequiredError) { //int accountId = sender()->property("accountId").toInt(); //Account *account = m_accountManager->account(accountId); //if (account->status() == Account::Initialized) { // setCredentialsNeedUpdate(account); //} else { // connect(account, SIGNAL(statusChanged()), this, SLOT(accountCredentialsChangeHandler())); //} // instead of triggering CredentialsNeedUpdate, print some debugging. int httpCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); QByteArray jsonBody = reply->readAll(); qWarning() << "sociald:Google: would normally set CredentialsNeedUpdate for account" << reply->property("accountId").toInt() << "but could be spurious"; qWarning() << " Http code:" << httpCode; qWarning() << " Json body:" << QString::fromUtf8(jsonBody).replace('\r', ' ').replace('\n', ' '); } qCWarning(lcSocialPlugin) << SocialNetworkSyncAdaptor::dataTypeName(m_dataType) << "request with account" << sender()->property("accountId").toInt() << "experienced error:" << err; // set "isError" on the reply so that adapters know to ignore the result in the finished() handler reply->setProperty("isError", QVariant::fromValue(true)); // Note: not all errors are "unrecoverable" errors, so we don't change the status here. } void GoogleDataTypeSyncAdaptor::sslErrorsHandler(const QList &errs) { QString sslerrs; foreach (const QSslError &e, errs) { sslerrs += e.errorString() + "; "; } if (errs.size() > 0) { sslerrs.chop(2); } qCWarning(lcSocialPlugin) << SocialNetworkSyncAdaptor::dataTypeName(m_dataType) << "request with account" << sender()->property("accountId").toInt() << "experienced ssl errors:" << sslerrs; // set "isError" on the reply so that adapters know to ignore the result in the finished() handler sender()->setProperty("isError", QVariant::fromValue(true)); // Note: not all errors are "unrecoverable" errors, so we don't change the status here. } QString GoogleDataTypeSyncAdaptor::clientId() { if (!m_triedLoading) { loadClientIdAndSecret(); } return m_clientId; } QString GoogleDataTypeSyncAdaptor::clientSecret() { if (!m_triedLoading) { loadClientIdAndSecret(); } return m_clientSecret; } void GoogleDataTypeSyncAdaptor::loadClientIdAndSecret() { m_triedLoading = true; char *cClientId = NULL; char *cClientSecret = NULL; int cSuccess = SailfishKeyProvider_storedKey("google", "google-sync", "client_id", &cClientId); if (cClientId == NULL) { return; } else if (cSuccess != 0) { free(cClientId); return; } m_clientId = QLatin1String(cClientId); free(cClientId); cSuccess = SailfishKeyProvider_storedKey("google", "google-sync", "client_secret", &cClientSecret); if (cClientSecret == NULL) { return; } else if (cSuccess != 0) { free(cClientSecret); return; } m_clientSecret = QLatin1String(cClientSecret); free(cClientSecret); } void GoogleDataTypeSyncAdaptor::setCredentialsNeedUpdate(Accounts::Account *account) { qWarning() << "sociald:Google: setting CredentialsNeedUpdate to true for account:" << account->id(); Accounts::Service srv(m_accountManager->service(syncServiceName())); account->selectService(srv); account->setValue(QStringLiteral("CredentialsNeedUpdate"), QVariant::fromValue(true)); account->setValue(QStringLiteral("CredentialsNeedUpdateFrom"), QVariant::fromValue(QString::fromLatin1("sociald-google"))); account->selectService(Accounts::Service()); account->syncAndBlock(); } void GoogleDataTypeSyncAdaptor::signIn(Accounts::Account *account) { // Fetch consumer key and secret from keyprovider int accountId = account->id(); if (!checkAccount(account)) { decrementSemaphore(accountId); return; } #ifdef USE_SAILFISHKEYPROVIDER if (clientId().isEmpty() || clientSecret().isEmpty()) { decrementSemaphore(accountId); return; } #endif // grab out a valid identity for the sync service. Accounts::Service srv(m_accountManager->service(syncServiceName())); account->selectService(srv); SignOn::Identity *identity = account->credentialsId() > 0 ? SignOn::Identity::existingIdentity(account->credentialsId()) : 0; if (!identity) { qCWarning(lcSocialPlugin) << "account" << accountId << "has no valid credentials; cannot sign in"; decrementSemaphore(accountId); return; } Accounts::AccountService accSrv(account, srv); QString method = accSrv.authData().method(); QString mechanism = accSrv.authData().mechanism(); SignOn::AuthSession *session = identity->createSession(method); if (!session) { qCWarning(lcSocialPlugin) << "could not create signon session for account" << accountId; identity->deleteLater(); decrementSemaphore(accountId); return; } QVariantMap signonSessionData = accSrv.authData().parameters(); signonSessionData.insert("ClientId", clientId()); signonSessionData.insert("ClientSecret", clientSecret()); signonSessionData.insert("UiPolicy", SignOn::NoUserInteractionPolicy); connect(session, SIGNAL(response(SignOn::SessionData)), this, SLOT(signOnResponse(SignOn::SessionData)), Qt::UniqueConnection); connect(session, SIGNAL(error(SignOn::Error)), this, SLOT(signOnError(SignOn::Error)), Qt::UniqueConnection); session->setProperty("account", QVariant::fromValue(account)); session->setProperty("identity", QVariant::fromValue(identity)); session->process(SignOn::SessionData(signonSessionData), mechanism); } void GoogleDataTypeSyncAdaptor::signOnError(const SignOn::Error &error) { SignOn::AuthSession *session = qobject_cast(sender()); Accounts::Account *account = session->property("account").value(); SignOn::Identity *identity = session->property("identity").value(); int accountId = account->id(); qCWarning(lcSocialPlugin) << "credentials for account with id" << accountId << "couldn't be retrieved:" << error.type() << error.message(); // if the error is because credentials have expired, we // set the CredentialsNeedUpdate key. if (error.type() == SignOn::Error::UserInteraction) { setCredentialsNeedUpdate(account); } session->disconnect(this); identity->destroySession(session); identity->deleteLater(); account->deleteLater(); // if we couldn't sign in, we can't sync with this account. setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(accountId); } void GoogleDataTypeSyncAdaptor::signOnResponse(const SignOn::SessionData &responseData) { QVariantMap data; foreach (const QString &key, responseData.propertyNames()) { data.insert(key, responseData.getProperty(key)); } QString accessToken; SignOn::AuthSession *session = qobject_cast(sender()); Accounts::Account *account = session->property("account").value(); SignOn::Identity *identity = session->property("identity").value(); int accountId = account->id(); if (data.contains(QLatin1String("AccessToken"))) { accessToken = data.value(QLatin1String("AccessToken")).toString(); } else { qCInfo(lcSocialPlugin) << "signon response for account with id" << accountId << "contained no access token"; } session->disconnect(this); identity->destroySession(session); identity->deleteLater(); account->deleteLater(); if (!accessToken.isEmpty()) { beginSync(accountId, accessToken); // call the derived-class sync entrypoint. } decrementSemaphore(accountId); } buteo-sync-plugins-social-0.4.28/src/google/googledatatypesyncadaptor.h000066400000000000000000000050331474572147200263360ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2013-2014 Jolla Ltd. ** Contact: Chris Adams ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef GOOGLEDATATYPESYNCADAPTOR_H #define GOOGLEDATATYPESYNCADAPTOR_H #include "socialnetworksyncadaptor.h" #include #include #include #include #include namespace Accounts { class Account; } namespace SignOn { class Error; class SessionData; } /* Abstract interface for all of the data-specific sync adaptors which pull data from Google's online services. */ class GoogleDataTypeSyncAdaptor : public SocialNetworkSyncAdaptor { Q_OBJECT public: GoogleDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::DataType dataType, QObject *parent); virtual ~GoogleDataTypeSyncAdaptor(); virtual void sync(const QString &dataTypeString, int accountId); protected: QString clientId(); QString clientSecret(); virtual void updateDataForAccount(int accountId); virtual void beginSync(int accountId, const QString &accessToken) = 0; virtual void finalCleanup(); protected Q_SLOTS: virtual void errorHandler(QNetworkReply::NetworkError err); virtual void sslErrorsHandler(const QList &errs); private Q_SLOTS: void signOnError(const SignOn::Error &error); void signOnResponse(const SignOn::SessionData &responseData); private: void loadClientIdAndSecret(); void setCredentialsNeedUpdate(Accounts::Account *account); void signIn(Accounts::Account *account); bool m_triedLoading; // Is true if we tried to load (even if we failed) QString m_clientId; QString m_clientSecret; }; #endif // GOOGLEDATATYPESYNCADAPTOR_H buteo-sync-plugins-social-0.4.28/src/knowncontacts/000077500000000000000000000000001474572147200223235ustar00rootroot00000000000000buteo-sync-plugins-social-0.4.28/src/knowncontacts/README.md000066400000000000000000000020221474572147200235760ustar00rootroot00000000000000Knowncontacts Sync Plugin ========================= This is a sync plugin for Buteo framework. It stores locally created contacts, such as email recipients that are not to be synced elsewhere and only ever syncs to the device side. In this case the "server" means QSettings files containing contacts. On sync this plugin reads the files and creates local contacts from the information. Contact file format ------------------- A contact in QSettings file must have an id as group name and contact information as key value pairs. There are no requirements for the id but it must be consistent between syncs to avoid duplicating contact information. All keys are optional. A file may have as many contacts as needed. The file must end with file extension `ini` and they are stored in `~/.local/share/system/privileged/Contacts/knowncontacts`. ```ini [john.doe.example] FirstName=John LastName=Doe EmailAddress=john@example.com ``` Supported keys: - FirstName, LastName - EmailAddress - Phone, HomePhone, MobilePhone - Company, Title, Office buteo-sync-plugins-social-0.4.28/src/knowncontacts/knowncontacts.Contacts.xml000066400000000000000000000010561474572147200275170ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/knowncontacts/knowncontacts.pro000066400000000000000000000015731474572147200257460ustar00rootroot00000000000000TARGET = knowncontacts-client include($$PWD/../common.pri) knowncontacts_sync_profile.path = /etc/buteo/profiles/sync knowncontacts_sync_profile.files = knowncontacts.Contacts.xml knowncontacts_client_plugin_xml.path = /etc/buteo/profiles/client knowncontacts_client_plugin_xml.files = knowncontacts.xml HEADERS += \ knowncontactsplugin.h \ knowncontactssyncer.h SOURCES += \ knowncontactsplugin.cpp \ knowncontactssyncer.cpp OTHER_FILES = \ knowncontacts.Contacts.xml knowncontacts.xml QT -= gui QT += dbus TEMPLATE = lib CONFIG += plugin link_pkgconfig PKGCONFIG += buteosyncfw5 Qt5Contacts qtcontacts-sqlite-qt5-extensions QMAKE_CXXFLAGS = -Wall \ -g \ -Wno-cast-align \ -O2 -finline-functions target.path = $$[QT_INSTALL_LIBS]/buteo-plugins-qt5/oopp INSTALLS += target \ knowncontacts_sync_profile \ knowncontacts_client_plugin_xml buteo-sync-plugins-social-0.4.28/src/knowncontacts/knowncontacts.xml000066400000000000000000000003701474572147200257400ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/knowncontacts/knowncontactsplugin.cpp000066400000000000000000000127061474572147200271470ustar00rootroot00000000000000/* * Buteo sync plugin that stores locally created contacts * Copyright (C) 2020 Open Mobile Platform LLC. ** Copyright (c) 2019 - 2021 Jolla Ltd. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License version 2.1 as published by the Free Software Foundation * and appearing in the file LICENSE.LGPL included in the packaging * of this file. * * 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 */ #include #include #include #include #include "knowncontactsplugin.h" #include "knowncontactssyncer.h" #include "trace.h" const auto KnownContactsSyncFolder = QStringLiteral("system/privileged/Contacts/knowncontacts"); KnownContactsPlugin::KnownContactsPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *cbInterface) : Buteo::ClientPlugin(pluginName, profile, cbInterface) , m_syncer(nullptr) { FUNCTION_CALL_TRACE(lcSocialPluginTrace); } KnownContactsPlugin::~KnownContactsPlugin() { FUNCTION_CALL_TRACE(lcSocialPluginTrace); } /** * \!brief Initialize the plugin for the actual sync to happen * This method will be invoked by the framework */ bool KnownContactsPlugin::init() { FUNCTION_CALL_TRACE(lcSocialPluginTrace); if (!m_syncer) { auto path = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QDir::separator() + KnownContactsSyncFolder; m_syncer = new KnownContactsSyncer(path, this); qCDebug(lcSocialPlugin) << "KnownContacts plugin initialized for path" << path; } return true; } /** * \!brief Uninitializes the plugin * This method will be invoked by the framework */ bool KnownContactsPlugin::uninit() { FUNCTION_CALL_TRACE(lcSocialPluginTrace); delete m_syncer; m_syncer = nullptr; qCDebug(lcSocialPlugin) << "KnownContacts plugin uninitialized"; return true; } /** * \!brief Start the actual sync. This method will be invoked by the * framework */ bool KnownContactsPlugin::startSync() { FUNCTION_CALL_TRACE(lcSocialPluginTrace); if (!m_syncer) return false; connect(m_syncer, &KnownContactsSyncer::syncSucceeded, this, &KnownContactsPlugin::syncSucceeded); connect(m_syncer, &KnownContactsSyncer::syncFailed, this, &KnownContactsPlugin::syncFailed); qCDebug(lcSocialPlugin) << "Starting sync"; // Start the actual sync return m_syncer->startSync(); } /** * \!brief Aborts sync. An abort can happen due to protocol errors, * connection failures or by the user (via a UI) */ void KnownContactsPlugin::abortSync(Sync::SyncStatus status) { Q_UNUSED(status) FUNCTION_CALL_TRACE(lcSocialPluginTrace); qCDebug(lcSocialPlugin) << "Aborting is not supported"; // Not supported, syncing usually takes very little time // and there is not much to abort } Buteo::SyncResults KnownContactsPlugin::getSyncResults() const { FUNCTION_CALL_TRACE(lcSocialPluginTrace); return m_results; } /** * This method is required if a profile has been deleted. The plugin * has to implement the necessary cleanup (like temporary data, anchors etc.) */ bool KnownContactsPlugin::cleanUp() { FUNCTION_CALL_TRACE(lcSocialPluginTrace); bool success; init(); // Ensure that syncer exists success = m_syncer->purgeData(profile().key(Buteo::KEY_ACCOUNT_ID).toInt()); uninit(); // Destroy syncer return success; } void KnownContactsPlugin::syncSucceeded() { FUNCTION_CALL_TRACE(lcSocialPluginTrace); qCDebug(lcSocialPlugin) << "Sync successful"; m_results = Buteo::SyncResults(QDateTime::currentDateTimeUtc(), Buteo::SyncResults::SYNC_RESULT_SUCCESS, Buteo::SyncResults::NO_ERROR); emit success(getProfileName(), QStringLiteral("Success")); } void KnownContactsPlugin::syncFailed() { FUNCTION_CALL_TRACE(lcSocialPluginTrace); qCDebug(lcSocialPlugin) << "Sync failed"; m_results = Buteo::SyncResults(iProfile.lastSuccessfulSyncTime(), Buteo::SyncResults::SYNC_RESULT_FAILED, Buteo::SyncResults::INTERNAL_ERROR); emit error(getProfileName(), QStringLiteral("Failure"), Buteo::SyncResults::INTERNAL_ERROR); } /** * Signal from the protocol engine about connectivity state changes */ void KnownContactsPlugin::connectivityStateChanged(Sync::ConnectivityType type, bool state) { Q_UNUSED(type) Q_UNUSED(state) FUNCTION_CALL_TRACE(lcSocialPluginTrace); // Stub } Buteo::ClientPlugin* KnownContactsPluginLoader::createClientPlugin( const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) { return new KnownContactsPlugin(pluginName, profile, cbInterface); } buteo-sync-plugins-social-0.4.28/src/knowncontacts/knowncontactsplugin.h000066400000000000000000000057351474572147200266200ustar00rootroot00000000000000/* * Buteo sync plugin that stores locally created contacts * Copyright (C) 2020 Open Mobile Platform LLC. * Copyright (c) 2019 - 2021 Jolla Ltd. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License version 2.1 as published by the Free Software Foundation * and appearing in the file LICENSE.LGPL included in the packaging * of this file. * * 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 */ #ifndef KNOWNCONTACTSPLUGIN_H #define KNOWNCONTACTSPLUGIN_H #include #include #include class KnownContactsSyncer; /*! \brief Implementation for client plugin * */ class KnownContactsPlugin : public Buteo::ClientPlugin { Q_OBJECT; public: /*! \brief Constructor * * @param pluginName Name of this client plugin * @param profile Sync profile * @param cbInterface Pointer to the callback interface */ KnownContactsPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *cbInterface); /*! \brief Destructor * * Call uninit before destroying the object. */ virtual ~KnownContactsPlugin(); //! @see SyncPluginBase::init virtual bool init(); //! @see SyncPluginBase::uninit virtual bool uninit(); //! @see ClientPlugin::startSync virtual bool startSync(); //! @see SyncPluginBase::abortSync virtual void abortSync(Sync::SyncStatus status = Sync::SYNC_ABORTED); //! @see SyncPluginBase::getSyncResults virtual Buteo::SyncResults getSyncResults() const; //! @see SyncPluginBase::cleanUp virtual bool cleanUp(); public slots: //! @see SyncPluginBase::connectivityStateChanged virtual void connectivityStateChanged(Sync::ConnectivityType type, bool state); protected slots: void syncSucceeded(); void syncFailed(); private: Buteo::SyncResults m_results; KnownContactsSyncer *m_syncer; }; class KnownContactsPluginLoader : public Buteo::SyncPluginLoader { Q_OBJECT Q_PLUGIN_METADATA(IID "org.sailfishos.plugins.sync.KnownContactsPluginLoader") Q_INTERFACES(Buteo::SyncPluginLoader) public: Buteo::ClientPlugin* createClientPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) override; }; #endif // KNOWNCONTACTSPLUGIN_H buteo-sync-plugins-social-0.4.28/src/knowncontacts/knowncontactssyncer.cpp000066400000000000000000000364711474572147200271610ustar00rootroot00000000000000/* * Buteo sync plugin that stores locally created contacts * Copyright (C) 2020 Open Mobile Platform LLC. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License version 2.1 as published by the Free Software Foundation * and appearing in the file LICENSE.LGPL included in the packaging * of this file. * * 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 */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "knowncontactssyncer.h" #include "trace.h" /* This performs a one-way sync to read contacts from a specified .ini file and write them to the contacts database. The .ini files are populated from a GAL address book linked to ActiveSync Exchange accounts. */ const auto GalCollectionName = QLatin1String("GAL"); const auto CollectionKeyLastSync = QLatin1String("last-sync-time"); static void setGuid(QContact *contact, const QString &id); static void setNames(QContact *contact, const QSettings &file); static void setPhoneNumbers(QContact *contact, const QSettings &file); static void setEmailAddress(QContact *contact, const QSettings &file); static void setCompanyInfo(QContact *contact, const QSettings &file); namespace { QContactCollection collectionForAccount(int accountId) { QContactCollection collection; collection.setMetaData(QContactCollection::KeyName, GalCollectionName); collection.setMetaData(QContactCollection::KeyDescription, QStringLiteral("Global Address List contacts")); collection.setMetaData(QContactCollection::KeyColor, QStringLiteral("yellow")); collection.setMetaData(QContactCollection::KeySecondaryColor, QStringLiteral("lightyellow")); collection.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, QCoreApplication::applicationName()); collection.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_READONLY, true); collection.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, accountId); return collection; } QMap managerParameters() { QMap rv; // Report presence changes independently from other contact changes rv.insert(QString::fromLatin1("mergePresenceChanges"), QString::fromLatin1("false")); return rv; } } KnownContactsSyncer::KnownContactsSyncer(QString path, QObject *parent) : QObject(parent) , QtContactsSqliteExtensions::TwoWayContactSyncAdaptor(0, qAppName(), managerParameters()) , m_syncFolder(path) { FUNCTION_CALL_TRACE(lcSocialPluginTrace); } KnownContactsSyncer::~KnownContactsSyncer() { FUNCTION_CALL_TRACE(lcSocialPluginTrace); } bool KnownContactsSyncer::determineRemoteCollections() { FUNCTION_CALL_TRACE(lcSocialPluginTrace); m_collections.clear(); const QList allCollections = contactManager().collections(); for (const QContactCollection &collection : allCollections) { if (collection.metaData(QContactCollection::KeyName).toString() == GalCollectionName && collection.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME).toString() == qAppName()) { m_collections.append(collection); } } qCInfo(lcSocialPlugin) << "Found" << m_collections.count() << "existing collections"; QDir syncDir(m_syncFolder); const QStringList fileNames = syncDir.entryList(QStringList() << "*-contacts-*.ini", QDir::Files); for (const QString &fileName : fileNames) { const int accountId = fileName.left(fileName.indexOf('-')).toInt(); if (accountId == 0) { qWarning() << "No account id in .ini file name:" << fileName; continue; } QContactCollection accountCollection; for (const QContactCollection &c : m_collections) { if (c.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID).toInt() == accountId) { accountCollection = c; break; } } if (accountCollection.id().isNull()) { accountCollection = collectionForAccount(accountId); m_collections.append(accountCollection); } m_updatedCollectionFileNames[accountCollection.id()].append(fileName); } qCInfo(lcSocialPlugin) << "Synced ini files" << fileNames << "now total collection count is:" << m_collections.count(); remoteCollectionsDetermined(m_collections); return true; } bool KnownContactsSyncer::deleteRemoteCollection(const QContactCollection &collection) { Q_UNUSED(collection) qCWarning(lcSocialPlugin) << "Collection deletion not supported, ignoring request to delete collection" << collection.id(); return true; } bool KnownContactsSyncer::determineRemoteContacts(const QContactCollection &collection) { FUNCTION_CALL_TRACE(lcSocialPluginTrace); QContactCollectionFilter collectionFilter; collectionFilter.setCollectionId(collection.id()); QContactFetchHint noRelationships; noRelationships.setOptimizationHints(QContactFetchHint::NoRelationships); const QList existingContacts = contactManager().contacts(collectionFilter, QList(), noRelationships); qCDebug(lcSocialPlugin) << "Found" << existingContacts.size() << "existing contacts"; QHash existingContactsHash; for (const QContact &contact : existingContacts) { existingContactsHash.insert(contact.detail().guid(), contact); } const QDateTime remoteSince = collection.extendedMetaData(CollectionKeyLastSync).toDateTime(); const QStringList updatedFiles = m_updatedCollectionFileNames.take(collection.id()); QDir syncDir(m_syncFolder); for (const QString &file : updatedFiles) { const QString path = syncDir.absoluteFilePath(file); QFileInfo info(path); if (info.lastModified() >= remoteSince) { QSettings settings(info.absoluteFilePath(), QSettings::IniFormat); readContacts(&settings, &existingContactsHash); } if (!QLockFile(path + QStringLiteral(".lock")).tryLock()) { qCDebug(lcSocialPlugin) << "File in use, not removing" << path; } else if (!QFile::remove(path)) { qCWarning(lcSocialPlugin) << "Could not remove" << path; } } const QList updatedContacts = existingContactsHash.values(); qCDebug(lcSocialPlugin) << "Reporting" << updatedContacts.size() << "contacts in total"; remoteContactsDetermined(collection, updatedContacts); return true; } bool KnownContactsSyncer::storeLocalChangesRemotely(const QContactCollection &collection, const QList &addedContacts, const QList &modifiedContacts, const QList &deletedContacts) { Q_UNUSED(collection) Q_UNUSED(addedContacts) Q_UNUSED(modifiedContacts) Q_UNUSED(deletedContacts) qCDebug(lcSocialPlugin) << "Sync is one-way, ignoring local changes for" << collection.id(); return true; } void KnownContactsSyncer::storeRemoteChangesLocally(const QContactCollection &collection, const QList &addedContacts, const QList &modifiedContacts, const QList &deletedContacts) { for (int i = 0; i < m_collections.count(); ++i) { if (m_collections[i] == collection) { m_collections[i].setExtendedMetaData(CollectionKeyLastSync, QDateTime::currentDateTimeUtc()); QtContactsSqliteExtensions::TwoWayContactSyncAdaptor::storeRemoteChangesLocally( m_collections[i], addedContacts, modifiedContacts, deletedContacts); return; } } QtContactsSqliteExtensions::TwoWayContactSyncAdaptor::storeRemoteChangesLocally( collection, addedContacts, modifiedContacts, deletedContacts); } template static inline T findDetail(QContact &contact, int field, const QString &value) { T result; QList details = contact.details(); for (T &detail : details) { if (detail.value(field) == value) { result = detail; break; } } return result; } static void setGuid(QContact *contact, const QString &id) { Q_ASSERT(contact); auto detail = contact->detail(); detail.setGuid(id); contact->saveDetail(&detail, QContact::IgnoreAccessConstraints); } static void setNames(QContact *contact, const QSettings &file) { Q_ASSERT(contact); const auto firstName = file.value("FirstName").toString(); const auto lastName = file.value("LastName").toString(); if (!firstName.isEmpty() || !lastName.isEmpty()) { auto detail = contact->detail(); if (!firstName.isEmpty()) detail.setFirstName(firstName); if (!lastName.isEmpty()) detail.setLastName(lastName); contact->saveDetail(&detail, QContact::IgnoreAccessConstraints); } } // Using QVariant as optional (aka 'maybe') type static inline void addPhoneNumberDetail(QContact *contact, const QString &value, const QVariant subType, const QVariant context) { Q_ASSERT(contact); if (!value.isEmpty()) { auto detail = findDetail(*contact, QContactPhoneNumber::FieldNumber, value); detail.setValue(QContactPhoneNumber::FieldNumber, value); if (subType.isValid()) detail.setSubTypes({subType.value()}); if (context.isValid()) detail.setContexts({context.value()}); contact->saveDetail(&detail, QContact::IgnoreAccessConstraints); } } static void setPhoneNumbers(QContact *contact, const QSettings &file) { Q_ASSERT(contact); addPhoneNumberDetail(contact, file.value("Phone").toString(), QContactPhoneNumber::SubTypeLandline, QVariant()); addPhoneNumberDetail(contact, file.value("HomePhone").toString(), QContactPhoneNumber::SubTypeLandline, QContactDetail::ContextHome); addPhoneNumberDetail(contact, file.value("MobilePhone").toString(), QContactPhoneNumber::SubTypeMobile, QVariant()); } static void setEmailAddress(QContact *contact, const QSettings &file) { Q_ASSERT(contact); const auto emailAddress = file.value("EmailAddress").toString(); if (!emailAddress.isEmpty()) { auto detail = findDetail( *contact, QContactEmailAddress::FieldEmailAddress, emailAddress); detail.setValue(QContactEmailAddress::FieldEmailAddress, emailAddress); contact->saveDetail(&detail, QContact::IgnoreAccessConstraints); } } static void setCompanyInfo(QContact *contact, const QSettings &file) { Q_ASSERT(contact); const auto company = file.value("Company").toString(); const auto title = file.value("Title").toString(); const auto office = file.value("Office").toString(); if (!title.isEmpty() || !office.isEmpty()) { auto detail = contact->detail(); if (!company.isEmpty()) detail.setName(company); if (!title.isEmpty()) detail.setTitle(title); if (!office.isEmpty()) detail.setLocation(office); contact->saveDetail(&detail, QContact::IgnoreAccessConstraints); } } void KnownContactsSyncer::readContacts(QSettings *file, QHash *contacts) { FUNCTION_CALL_TRACE(lcSocialPluginTrace); /* * This was implemented to support certain subset of contact fields * but can be extended to support more as long as the fields are * kept optional. */ for (const auto &id : file->childGroups()) { file->beginGroup(id); auto it = contacts->find(id); if (it == contacts->end()) { it = contacts->insert(id, QContact()); setGuid(&it.value(), id); } QContact &contact = it.value(); setNames(&contact, *file); setPhoneNumbers(&contact, *file); setEmailAddress(&contact, *file); setCompanyInfo(&contact, *file); file->endGroup(); } } void KnownContactsSyncer::syncFinishedSuccessfully() { qCDebug(lcSocialPlugin) << "Sync finished OK"; emit syncSucceeded(); } void KnownContactsSyncer::syncFinishedWithError() { qCWarning(lcSocialPlugin) << "Sync finished with error"; emit syncFailed(); } bool KnownContactsSyncer::purgeData(int accountId) { if (accountId <= 0) { qCWarning(lcSocialPlugin) << "Cannot purge data, invalid account id!"; return false; } QtContactsSqliteExtensions::ContactManagerEngine *cme = QtContactsSqliteExtensions::contactManagerEngine(contactManager()); QContactManager::Error error = QContactManager::NoError; QList addedCollections; QList modifiedCollections; QList deletedCollections; QList unmodifiedCollections; if (!cme->fetchCollectionChanges(accountId, qAppName(), &addedCollections, &modifiedCollections, &deletedCollections, &unmodifiedCollections, &error)) { qCWarning(lcSocialPlugin) << "Cannot find collections for account" << accountId << "app" << qAppName() << "error:" << error; return false; } const QList collections = addedCollections + modifiedCollections + deletedCollections + unmodifiedCollections; if (collections.isEmpty()) { qCInfo(lcSocialPlugin) << "Nothing to purge, no collection has been saved for account" << accountId; return false; } QList collectionIds; for (const QContactCollection &collection : collections) { collectionIds.append(collection.id()); } if (cme->storeChanges(nullptr, nullptr, collectionIds, QtContactsSqliteExtensions::ContactManagerEngine::PreserveLocalChanges, true, &error)) { qCInfo(lcSocialPlugin) << "Successfully removed contact collections" << collectionIds; return true; } qCWarning(lcSocialPlugin) << "Failed to remove contact collections:" << collectionIds << "error:" << error; return false; } buteo-sync-plugins-social-0.4.28/src/knowncontacts/knowncontactssyncer.h000066400000000000000000000052151474572147200266160ustar00rootroot00000000000000/* * Buteo sync plugin that stores locally created contacts * Copyright (C) 2020 Open Mobile Platform LLC. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License version 2.1 as published by the Free Software Foundation * and appearing in the file LICENSE.LGPL included in the packaging * of this file. * * 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 */ #ifndef KNOWNCONTACTS_SYNCER_H #define KNOWNCONTACTS_SYNCER_H #include #include #include #include #include QTCONTACTS_USE_NAMESPACE class KnownContactsSyncer : public QObject, public QtContactsSqliteExtensions::TwoWayContactSyncAdaptor { Q_OBJECT public: KnownContactsSyncer(QString path, QObject *parent = nullptr); ~KnownContactsSyncer(); bool purgeData(int accountId); virtual bool determineRemoteCollections() override; virtual bool deleteRemoteCollection(const QContactCollection &collection) override; virtual bool determineRemoteContacts(const QContactCollection &collection) override; virtual bool storeLocalChangesRemotely(const QContactCollection &collection, const QList &addedContacts, const QList &modifiedContacts, const QList &deletedContacts) override; virtual void storeRemoteChangesLocally(const QContactCollection &collection, const QList &addedContacts, const QList &modifiedContacts, const QList &deletedContacts) override; virtual void syncFinishedSuccessfully() override; virtual void syncFinishedWithError() override; signals: void syncSucceeded(); void syncFailed(); private: void readContacts(QSettings *file, QHash *contacts); QList m_collections; QMap m_updatedCollectionFileNames; QString m_syncFolder; }; #endif // KNOWNCONTACTS_SYNCER_H buteo-sync-plugins-social-0.4.28/src/onedrive/000077500000000000000000000000001474572147200212435ustar00rootroot00000000000000buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-backup/000077500000000000000000000000001474572147200243215ustar00rootroot00000000000000buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-backup/onedrive-backup.pri000066400000000000000000000001621474572147200301120ustar00rootroot00000000000000SOURCES += $$PWD/onedrivebackupsyncadaptor.cpp HEADERS += $$PWD/onedrivebackupsyncadaptor.h INCLUDEPATH += $$PWD buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-backup/onedrive-backup.pro000066400000000000000000000013211474572147200301160ustar00rootroot00000000000000TARGET = onedrive-backup-client include($$PWD/../../common.pri) include($$PWD/../onedrive-common.pri) include($$PWD/../onedrive-backupoperation.pri) include($$PWD/onedrive-backup.pri) onedrive_backup_sync_profile.path = /etc/buteo/profiles/sync onedrive_backup_sync_profile.files = $$PWD/onedrive.Backup.xml onedrive_backup_client_plugin_xml.path = /etc/buteo/profiles/client onedrive_backup_client_plugin_xml.files = $$PWD/onedrive-backup.xml HEADERS += onedrivebackupplugin.h SOURCES += onedrivebackupplugin.cpp OTHER_FILES += \ onedrive_backup_sync_profile.files \ onedrive_backup_client_plugin_xml.files INSTALLS += \ target \ onedrive_backup_sync_profile \ onedrive_backup_client_plugin_xml buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-backup/onedrive-backup.xml000066400000000000000000000002061474572147200301170ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-backup/onedrive.Backup.xml000066400000000000000000000011351474572147200300620ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-backup/onedrivebackupplugin.cpp000066400000000000000000000036551474572147200312560ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2020 Open Mobile Platform LLC. ** Copyright (c) 2015 - 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "onedrivebackupplugin.h" #include "onedrivebackupsyncadaptor.h" #include "socialnetworksyncadaptor.h" OneDriveBackupPlugin::OneDriveBackupPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *callbackInterface) : SocialdButeoPlugin(pluginName, profile, callbackInterface, QStringLiteral("onedrive"), SocialNetworkSyncAdaptor::dataTypeName(SocialNetworkSyncAdaptor::Backup)) { } OneDriveBackupPlugin::~OneDriveBackupPlugin() { } SocialNetworkSyncAdaptor *OneDriveBackupPlugin::createSocialNetworkSyncAdaptor() { return new OneDriveBackupSyncAdaptor(this); } Buteo::ClientPlugin* OneDriveBackupPluginLoader::createClientPlugin( const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) { return new OneDriveBackupPlugin(pluginName, profile, cbInterface); } buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-backup/onedrivebackupplugin.h000066400000000000000000000036621474572147200307210ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2020 Open Mobile Platform LLC. ** Copyright (c) 2015 - 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef ONEDRIVEBACKUPPLUGIN_H #define ONEDRIVEBACKUPPLUGIN_H #include "socialdbuteoplugin.h" #include class Q_DECL_EXPORT OneDriveBackupPlugin : public SocialdButeoPlugin { Q_OBJECT public: OneDriveBackupPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *cbInterface); ~OneDriveBackupPlugin(); protected: SocialNetworkSyncAdaptor *createSocialNetworkSyncAdaptor(); }; class OneDriveBackupPluginLoader : public Buteo::SyncPluginLoader { Q_OBJECT Q_PLUGIN_METADATA(IID "org.sailfishos.plugins.sync.OneDriveBackupPluginLoader") Q_INTERFACES(Buteo::SyncPluginLoader) public: Buteo::ClientPlugin* createClientPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) override; }; #endif // ONEDRIVEBACKUPPLUGIN_H buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-backup/onedrivebackupsyncadaptor.cpp000066400000000000000000000025761474572147200323100ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2015 - 2019 Jolla Ltd. ** Copyright (c) 2020 Open Mobile Platform LLC. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "onedrivebackupsyncadaptor.h" OneDriveBackupSyncAdaptor::OneDriveBackupSyncAdaptor(QObject *parent) : OneDriveBackupOperationSyncAdaptor(SocialNetworkSyncAdaptor::Backup, parent) { setInitialActive(true); } OneDriveBackupSyncAdaptor::~OneDriveBackupSyncAdaptor() { } OneDriveBackupOperationSyncAdaptor::Operation OneDriveBackupSyncAdaptor::operation() const { return OneDriveBackupOperationSyncAdaptor::Backup; } buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-backup/onedrivebackupsyncadaptor.h000066400000000000000000000025621474572147200317500ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2015 - 2019 Jolla Ltd. ** Copyright (c) 2020 Open Mobile Platform LLC. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef ONEDRIVEBACKUPSYNCADAPTOR_H #define ONEDRIVEBACKUPSYNCADAPTOR_H #include "onedrivebackupoperationsyncadaptor.h" class OneDriveBackupSyncAdaptor : public OneDriveBackupOperationSyncAdaptor { Q_OBJECT public: OneDriveBackupSyncAdaptor(QObject *parent); ~OneDriveBackupSyncAdaptor(); OneDriveBackupOperationSyncAdaptor::Operation operation() const override; }; #endif // ONEDRIVEBACKUPSYNCADAPTOR_H buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-backupoperation.pri000066400000000000000000000002041474572147200267520ustar00rootroot00000000000000SOURCES += $$PWD/onedrivebackupoperationsyncadaptor.cpp HEADERS += $$PWD/onedrivebackupoperationsyncadaptor.h INCLUDEPATH += $$PWD buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-backupquery/000077500000000000000000000000001474572147200254075ustar00rootroot00000000000000buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-backupquery/onedrive-backupquery.pri000066400000000000000000000001741474572147200322710ustar00rootroot00000000000000SOURCES += $$PWD/onedrivebackupquerysyncadaptor.cpp HEADERS += $$PWD/onedrivebackupquerysyncadaptor.h INCLUDEPATH += $$PWD buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-backupquery/onedrive-backupquery.pro000066400000000000000000000014271474572147200323010ustar00rootroot00000000000000TARGET = onedrive-backupquery-client include($$PWD/../../common.pri) include($$PWD/../onedrive-common.pri) include($$PWD/../onedrive-backupoperation.pri) include($$PWD/onedrive-backupquery.pri) onedrive_backupquery_sync_profile.path = /etc/buteo/profiles/sync onedrive_backupquery_sync_profile.files = $$PWD/onedrive.BackupQuery.xml onedrive_backupquery_client_plugin_xml.path = /etc/buteo/profiles/client onedrive_backupquery_client_plugin_xml.files = $$PWD/onedrive-backupquery.xml HEADERS += onedrivebackupqueryplugin.h SOURCES += onedrivebackupqueryplugin.cpp OTHER_FILES += \ onedrive_backupquery_sync_profile.files \ onedrive_backupquery_client_plugin_xml.files INSTALLS += \ target \ onedrive_backupquery_sync_profile \ onedrive_backupquery_client_plugin_xml buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-backupquery/onedrive-backupquery.xml000066400000000000000000000002131474572147200322710ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-backupquery/onedrive.BackupQuery.xml000066400000000000000000000011521474572147200321750ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-backupquery/onedrivebackupqueryplugin.cpp000066400000000000000000000037451474572147200334320ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2020 Open Mobile Platform LLC. ** Copyright (c) 2015 - 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "onedrivebackupqueryplugin.h" #include "onedrivebackupquerysyncadaptor.h" #include "socialnetworksyncadaptor.h" OneDriveBackupQueryPlugin::OneDriveBackupQueryPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *callbackInterface) : SocialdButeoPlugin(pluginName, profile, callbackInterface, QStringLiteral("onedrive"), SocialNetworkSyncAdaptor::dataTypeName(SocialNetworkSyncAdaptor::BackupQuery)) { } OneDriveBackupQueryPlugin::~OneDriveBackupQueryPlugin() { } SocialNetworkSyncAdaptor *OneDriveBackupQueryPlugin::createSocialNetworkSyncAdaptor() { return new OneDriveBackupQuerySyncAdaptor(this); } Buteo::ClientPlugin* OneDriveBackupQueryPluginLoader::createClientPlugin( const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) { return new OneDriveBackupQueryPlugin(pluginName, profile, cbInterface); } buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-backupquery/onedrivebackupqueryplugin.h000066400000000000000000000037321474572147200330730ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2020 Open Mobile Platform LLC. ** Copyright (c) 2015 - 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef ONEDRIVEBACKUPQUERYPLUGIN_H #define ONEDRIVEBACKUPQUERYPLUGIN_H #include "socialdbuteoplugin.h" #include class Q_DECL_EXPORT OneDriveBackupQueryPlugin : public SocialdButeoPlugin { Q_OBJECT public: OneDriveBackupQueryPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *cbInterface); ~OneDriveBackupQueryPlugin(); protected: SocialNetworkSyncAdaptor *createSocialNetworkSyncAdaptor(); }; class OneDriveBackupQueryPluginLoader : public Buteo::SyncPluginLoader { Q_OBJECT Q_PLUGIN_METADATA(IID "org.sailfishos.plugins.sync.OneDriveBackupQueryPluginLoader") Q_INTERFACES(Buteo::SyncPluginLoader) public: Buteo::ClientPlugin* createClientPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) override; }; #endif // ONEDRIVEBACKUPQUERYPLUGIN_H onedrivebackupquerysyncadaptor.cpp000066400000000000000000000025751474572147200344040ustar00rootroot00000000000000buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-backupquery/**************************************************************************** ** ** Copyright (c) 2020 Open Mobile Platform LLC. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "onedrivebackupquerysyncadaptor.h" OneDriveBackupQuerySyncAdaptor::OneDriveBackupQuerySyncAdaptor(QObject *parent) : OneDriveBackupOperationSyncAdaptor(SocialNetworkSyncAdaptor::BackupQuery, parent) { setInitialActive(true); } OneDriveBackupQuerySyncAdaptor::~OneDriveBackupQuerySyncAdaptor() { } OneDriveBackupOperationSyncAdaptor::Operation OneDriveBackupQuerySyncAdaptor::operation() const { return OneDriveBackupOperationSyncAdaptor::BackupQuery; } buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-backupquery/onedrivebackupquerysyncadaptor.h000066400000000000000000000025471474572147200341270ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2020 Open Mobile Platform LLC. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef ONEDRIVEBACKUPQUERYSYNCADAPTOR_H #define ONEDRIVEBACKUPQUERYSYNCADAPTOR_H #include "onedrivebackupoperationsyncadaptor.h" class OneDriveBackupQuerySyncAdaptor : public OneDriveBackupOperationSyncAdaptor { Q_OBJECT public: OneDriveBackupQuerySyncAdaptor(QObject *parent); ~OneDriveBackupQuerySyncAdaptor(); OneDriveBackupOperationSyncAdaptor::Operation operation() const override; }; #endif // ONEDRIVEBACKUPQUERYSYNCADAPTOR_H buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-backuprestore/000077500000000000000000000000001474572147200257255ustar00rootroot00000000000000buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-backuprestore/onedrive-backuprestore.pri000066400000000000000000000002001474572147200331130ustar00rootroot00000000000000SOURCES += $$PWD/onedrivebackuprestoresyncadaptor.cpp HEADERS += $$PWD/onedrivebackuprestoresyncadaptor.h INCLUDEPATH += $$PWD buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-backuprestore/onedrive-backuprestore.pro000066400000000000000000000014631474572147200331350ustar00rootroot00000000000000TARGET = onedrive-backuprestore-client include($$PWD/../../common.pri) include($$PWD/../onedrive-common.pri) include($$PWD/../onedrive-backupoperation.pri) include($$PWD/onedrive-backuprestore.pri) onedrive_backuprestore_sync_profile.path = /etc/buteo/profiles/sync onedrive_backuprestore_sync_profile.files = $$PWD/onedrive.BackupRestore.xml onedrive_backuprestore_client_plugin_xml.path = /etc/buteo/profiles/client onedrive_backuprestore_client_plugin_xml.files = $$PWD/onedrive-backuprestore.xml HEADERS += onedrivebackuprestoreplugin.h SOURCES += onedrivebackuprestoreplugin.cpp OTHER_FILES += \ onedrive_backuprestore_sync_profile.files \ onedrive_backuprestore_client_plugin_xml.files INSTALLS += \ target \ onedrive_backuprestore_sync_profile \ onedrive_backuprestore_client_plugin_xml buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-backuprestore/onedrive-backuprestore.xml000066400000000000000000000002151474572147200331270ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-backuprestore/onedrive.BackupRestore.xml000066400000000000000000000011621474572147200330320ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-backuprestore/onedrivebackuprestoreplugin.cpp000066400000000000000000000037731474572147200342670ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2020 Open Mobile Platform LLC. ** Copyright (c) 2015 - 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "onedrivebackuprestoreplugin.h" #include "onedrivebackuprestoresyncadaptor.h" #include "socialnetworksyncadaptor.h" OneDriveBackupRestorePlugin::OneDriveBackupRestorePlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *callbackInterface) : SocialdButeoPlugin(pluginName, profile, callbackInterface, QStringLiteral("onedrive"), SocialNetworkSyncAdaptor::dataTypeName(SocialNetworkSyncAdaptor::BackupRestore)) { } OneDriveBackupRestorePlugin::~OneDriveBackupRestorePlugin() { } SocialNetworkSyncAdaptor *OneDriveBackupRestorePlugin::createSocialNetworkSyncAdaptor() { return new OneDriveBackupRestoreSyncAdaptor(this); } Buteo::ClientPlugin* OneDriveBackupRestorePluginLoader::createClientPlugin( const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) { return new OneDriveBackupRestorePlugin(pluginName, profile, cbInterface); } buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-backuprestore/onedrivebackuprestoreplugin.h000066400000000000000000000037521474572147200337310ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2020 Open Mobile Platform LLC. ** Copyright (c) 2015 - 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef ONEDRIVEBACKUPRESTOREPLUGIN_H #define ONEDRIVEBACKUPRESTOREPLUGIN_H #include "socialdbuteoplugin.h" #include class Q_DECL_EXPORT OneDriveBackupRestorePlugin : public SocialdButeoPlugin { Q_OBJECT public: OneDriveBackupRestorePlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *cbInterface); ~OneDriveBackupRestorePlugin(); protected: SocialNetworkSyncAdaptor *createSocialNetworkSyncAdaptor(); }; class OneDriveBackupRestorePluginLoader : public Buteo::SyncPluginLoader { Q_OBJECT Q_PLUGIN_METADATA(IID "org.sailfishos.plugins.sync.OneDriveBackupRestorePluginLoader") Q_INTERFACES(Buteo::SyncPluginLoader) public: Buteo::ClientPlugin* createClientPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) override; }; #endif // ONEDRIVEBACKUPRESTOREPLUGIN_H onedrivebackuprestoresyncadaptor.cpp000066400000000000000000000026151474572147200352330ustar00rootroot00000000000000buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-backuprestore/**************************************************************************** ** ** Copyright (c) 2020 Open Mobile Platform LLC. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "onedrivebackuprestoresyncadaptor.h" OneDriveBackupRestoreSyncAdaptor::OneDriveBackupRestoreSyncAdaptor(QObject *parent) : OneDriveBackupOperationSyncAdaptor(SocialNetworkSyncAdaptor::BackupRestore, parent) { setInitialActive(true); } OneDriveBackupRestoreSyncAdaptor::~OneDriveBackupRestoreSyncAdaptor() { } OneDriveBackupOperationSyncAdaptor::Operation OneDriveBackupRestoreSyncAdaptor::operation() const { return OneDriveBackupOperationSyncAdaptor::BackupRestore; } onedrivebackuprestoresyncadaptor.h000066400000000000000000000025631474572147200347020ustar00rootroot00000000000000buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-backuprestore/**************************************************************************** ** ** Copyright (c) 2020 Open Mobile Platform LLC. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef ONEDRIVEBACKUPRESTORESYNCADAPTOR_H #define ONEDRIVEBACKUPRESTORESYNCADAPTOR_H #include "onedrivebackupoperationsyncadaptor.h" class OneDriveBackupRestoreSyncAdaptor : public OneDriveBackupOperationSyncAdaptor { Q_OBJECT public: OneDriveBackupRestoreSyncAdaptor(QObject *parent); ~OneDriveBackupRestoreSyncAdaptor(); OneDriveBackupOperationSyncAdaptor::Operation operation() const override; }; #endif // ONEDRIVEBACKUPRESTORESYNCADAPTOR_H buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-common.pri000066400000000000000000000001651474572147200250620ustar00rootroot00000000000000INCLUDEPATH += $$PWD SOURCES += $$PWD/onedrivedatatypesyncadaptor.cpp HEADERS += $$PWD/onedrivedatatypesyncadaptor.h buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-images/000077500000000000000000000000001474572147200243215ustar00rootroot00000000000000buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-images/onedrive-images.pri000066400000000000000000000001601474572147200301100ustar00rootroot00000000000000SOURCES += $$PWD/onedriveimagesyncadaptor.cpp HEADERS += $$PWD/onedriveimagesyncadaptor.h INCLUDEPATH += $$PWD buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-images/onedrive-images.pro000066400000000000000000000013201474572147200301150ustar00rootroot00000000000000TARGET = onedrive-images-client include($$PWD/../../common.pri) include($$PWD/../onedrive-common.pri) include($$PWD/onedrive-images.pri) CONFIG += link_pkgconfig PKGCONFIG += mlite5 onedrive_images_sync_profile.path = /etc/buteo/profiles/sync onedrive_images_sync_profile.files = $$PWD/onedrive.Images.xml onedrive_images_client_plugin_xml.path = /etc/buteo/profiles/client onedrive_images_client_plugin_xml.files = $$PWD/onedrive-images.xml HEADERS += onedriveimagesplugin.h SOURCES += onedriveimagesplugin.cpp OTHER_FILES += \ onedrive_images_sync_profile.files \ onedrive_images_client_plugin_xml.files INSTALLS += \ target \ onedrive_images_sync_profile \ onedrive_images_client_plugin_xml buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-images/onedrive-images.xml000066400000000000000000000002061474572147200301170ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-images/onedrive.Images.xml000066400000000000000000000011411474572147200300570ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-images/onedriveimagesplugin.cpp000066400000000000000000000035721474572147200312540ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2015 - 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "onedriveimagesplugin.h" #include "onedriveimagesyncadaptor.h" #include "socialnetworksyncadaptor.h" OneDriveImagesPlugin::OneDriveImagesPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *callbackInterface) : SocialdButeoPlugin(pluginName, profile, callbackInterface, QStringLiteral("onedrive"), SocialNetworkSyncAdaptor::dataTypeName(SocialNetworkSyncAdaptor::Images)) { } OneDriveImagesPlugin::~OneDriveImagesPlugin() { } SocialNetworkSyncAdaptor *OneDriveImagesPlugin::createSocialNetworkSyncAdaptor() { return new OneDriveImageSyncAdaptor(this); } Buteo::ClientPlugin* OneDriveImagesPluginLoader::createClientPlugin( const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) { return new OneDriveImagesPlugin(pluginName, profile, cbInterface); } buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-images/onedriveimagesplugin.h000066400000000000000000000036031474572147200307140ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2015 - 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef ONEDRIVEIMAGESPLUGIN_H #define ONEDRIVEIMAGESPLUGIN_H #include "socialdbuteoplugin.h" #include class Q_DECL_EXPORT OneDriveImagesPlugin : public SocialdButeoPlugin { Q_OBJECT public: OneDriveImagesPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *cbInterface); ~OneDriveImagesPlugin(); protected: SocialNetworkSyncAdaptor *createSocialNetworkSyncAdaptor(); }; class OneDriveImagesPluginLoader : public Buteo::SyncPluginLoader { Q_OBJECT Q_PLUGIN_METADATA(IID "org.sailfishos.plugins.sync.OneDriveImagesPluginLoader") Q_INTERFACES(Buteo::SyncPluginLoader) public: Buteo::ClientPlugin* createClientPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) override; }; #endif // ONEDRIVEIMAGESPLUGIN_H buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-images/onedriveimagesyncadaptor.cpp000066400000000000000000000440421474572147200321170ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015 Jolla Ltd. ** Contact: Antti Seppälä ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "onedriveimagesyncadaptor.h" #include "trace.h" #include #include #include #include #include #include #include #include #include #include #include #include OneDriveImageSyncAdaptor::AlbumData::AlbumData() { } OneDriveImageSyncAdaptor::AlbumData::AlbumData( const QString &albumId, const QString &userId, const QDateTime &createdTime, const QDateTime &updatedTime, const QString &albumName, int imageCount) : albumId(albumId), userId(userId), createdTime(createdTime) , updatedTime(updatedTime), albumName(albumName), imageCount(imageCount) { } OneDriveImageSyncAdaptor::AlbumData::AlbumData(const AlbumData &other) { albumId = other.albumId; userId = other.userId; createdTime = other.createdTime; updatedTime = other.updatedTime; albumName = other.albumName; imageCount = other.imageCount; } OneDriveImageSyncAdaptor::ImageData::ImageData() : imageWidth(0), imageHeight(0) { } OneDriveImageSyncAdaptor::ImageData::ImageData( const QString &photoId, const QString &albumId, const QString &userId, const QDateTime &createdTime, const QDateTime &updatedTime, const QString &photoName, int imageWidth, int imageHeight, const QString &thumbnailUrl, const QString &imageSourceUrl, const QString &description) : photoId(photoId), albumId(albumId), userId(userId) , createdTime(createdTime), updatedTime(updatedTime), photoName(photoName) , imageWidth(imageWidth), imageHeight(imageHeight) , thumbnailUrl(thumbnailUrl), imageSourceUrl(imageSourceUrl) , description(description) { } OneDriveImageSyncAdaptor::ImageData::ImageData(const ImageData &other) { photoId = other.photoId; albumId = other.albumId; userId = other.userId; createdTime = other.createdTime; updatedTime = other.updatedTime; photoName = other.photoName; imageWidth = other.imageWidth; imageHeight = other.imageHeight; thumbnailUrl = other.thumbnailUrl; imageSourceUrl = other.imageSourceUrl; description = other.description; } // Update the following version if database schema changes e.g. new // fields are added to the existing tables. // It will make old tables dropped and creates new ones. // Currently, we integrate with the device image gallery via saving thumbnails to the // ~/.config/sociald/images directory, and filling the ~/.config/sociald/images/onedrive.db // with appropriate data. OneDriveImageSyncAdaptor::OneDriveImageSyncAdaptor(QObject *parent) : OneDriveDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::Images, parent) { setInitialActive(m_db.isValid()); } OneDriveImageSyncAdaptor::~OneDriveImageSyncAdaptor() { } QString OneDriveImageSyncAdaptor::syncServiceName() const { return QStringLiteral("onedrive-images"); } void OneDriveImageSyncAdaptor::sync(const QString &dataTypeString, int accountId) { if (!initRemovalDetectionLists(accountId)) { qCWarning(lcSocialPlugin) << "unable to initialized cached account list for account" << accountId; setStatus(SocialNetworkSyncAdaptor::Error); return; } // call superclass impl. OneDriveDataTypeSyncAdaptor::sync(dataTypeString, accountId); } void OneDriveImageSyncAdaptor::purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode) { m_db.purgeAccount(oldId); m_db.commit(); m_db.wait(); // manage image cache. Gallery UI caches full size images // and maintains bindings between source and cached image in SocialImageDatabase. // purge cached images belonging to this account. purgeCachedImages(&m_imageCacheDb, oldId); } void OneDriveImageSyncAdaptor::beginSync(int accountId, const QString &accessToken) { requestResource(accountId, accessToken); } void OneDriveImageSyncAdaptor::finalize(int accountId) { if (syncAborted()) { qCInfo(lcSocialPlugin) << "sync aborted, won't commit database changes"; } else if (m_userId.isEmpty()) { qCWarning(lcSocialPlugin) << "no user id determined during sync, aborting"; } else { // Add user if (m_db.user(m_userId).isNull()) { m_db.addUser(m_userId, QDateTime::currentDateTime(), m_userDisplayName, accountId); } // Add/update albums QMap::const_iterator i; for (i = m_albumData.constBegin(); i != m_albumData.constEnd(); ++i) { const AlbumData &data = i.value(); m_db.addAlbum(data.albumId, data.userId, data.createdTime, data.updatedTime, data.albumName, data.imageCount); } // Remove albums m_db.removeAlbums(m_cachedAlbums.keys()); // determine whether any images have been removed server-side. Q_FOREACH (const QString &albumId, m_seenAlbums) { checkRemovedImages(albumId); } // Remove images m_db.removeImages(m_removedImages); // Add/update images QMap::const_iterator j; for (j = m_imageData.constBegin(); j != m_imageData.constEnd(); ++j) { const ImageData &data = j.value(); m_db.addImage(data.photoId, data.albumId, data.userId, data.createdTime, data.updatedTime, data.photoName, data.imageWidth, data.imageHeight, data.thumbnailUrl, data.imageSourceUrl, data.description, accountId); } m_db.commit(); m_db.wait(); // manage image cache. Gallery UI caches full size images // and maintains bindings between source and cached image in SocialImageDatabase. // purge cached images older than two weeks. purgeExpiredImages(&m_imageCacheDb, accountId); } } void OneDriveImageSyncAdaptor::requestResource(int accountId, const QString &accessToken, const QString &resourceTarget) { // TODO: in future, do a "first pass" WITHOUT child expansion to detect changed ctag/etag. // then, do a "second pass" only for the albums which have changed ctag. const QString defaultResourceTarget = QStringLiteral("/drive/special/photos"); const QUrl url(QStringLiteral("%1%2%3?expand=children(expand=thumbnails)") .arg(api()).arg("/me").arg(resourceTarget.isEmpty() ? defaultResourceTarget : resourceTarget)); qCDebug(lcSocialPlugin) << "OneDrive image sync requesting resource:" << url.toString(); QNetworkRequest req(url); req.setRawHeader(QString(QLatin1String("Authorization")).toUtf8(), QString(QLatin1String("Bearer ")).toUtf8() + accessToken.toUtf8()); QNetworkReply *reply = m_networkAccessManager->get(req); if (reply) { reply->setProperty("accountId", accountId); reply->setProperty("accessToken", accessToken); reply->setProperty("defaultResource", resourceTarget.isEmpty()); connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(errorHandler(QNetworkReply::NetworkError))); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsHandler(QList))); connect(reply, SIGNAL(finished()), this, SLOT(resourceFinishedHandler())); // we're requesting data. Increment the semaphore so that we know we're still busy. incrementSemaphore(accountId); setupReplyTimeout(accountId, reply); } else { qCWarning(lcSocialPlugin) << "unable to request data from OneDrive account with id" << accountId; clearRemovalDetectionLists(); // don't perform server-side removal detection during this sync run. } } void OneDriveImageSyncAdaptor::requestNextLink(int accountId, const QString &accessToken, const QString &nextLink, bool isDefaultResource) { qCDebug(lcSocialPlugin) << "OneDrive image sync requesting nextlink resources:" << nextLink; QUrl nextLinkUrl(nextLink); QNetworkRequest req(nextLinkUrl); req.setRawHeader(QString(QLatin1String("Authorization")).toUtf8(), QString(QLatin1String("Bearer ")).toUtf8() + accessToken.toUtf8()); QNetworkReply *reply = m_networkAccessManager->get(req); if (reply) { reply->setProperty("accountId", accountId); reply->setProperty("accessToken", accessToken); reply->setProperty("defaultResource", isDefaultResource); connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(errorHandler(QNetworkReply::NetworkError))); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsHandler(QList))); connect(reply, SIGNAL(finished()), this, SLOT(resourceFinishedHandler())); // we're requesting data. Increment the semaphore so that we know we're still busy. incrementSemaphore(accountId); setupReplyTimeout(accountId, reply); } else { qCWarning(lcSocialPlugin) << "unable to request data from OneDrive account with id" << accountId; clearRemovalDetectionLists(); // don't perform server-side removal detection during this sync run. } } void OneDriveImageSyncAdaptor::resourceFinishedHandler() { QNetworkReply *reply = qobject_cast(sender()); const bool isError = reply->property("isError").toBool(); const int accountId = reply->property("accountId").toInt(); const QString accessToken = reply->property("accessToken").toString(); const bool defaultResource = reply->property("defaultResource").toBool(); const QByteArray replyData = reply->readAll(); disconnect(reply); reply->deleteLater(); removeReplyTimeout(accountId, reply); bool ok = false; const QJsonObject parsed = parseJsonObjectReplyData(replyData, &ok); if (isError || !ok || !parsed.contains(QLatin1String("id"))) { qCWarning(lcSocialPlugin) << "Unable to parse query response for OneDrive account with id" << accountId; qCDebug(lcSocialPlugin) << "Received response data:" << replyData; clearRemovalDetectionLists(); // don't perform server-side removal detection during this sync run. decrementSemaphore(accountId); return; } const QJsonObject userObj = parsed.value("createdBy").toObject().value("user").toObject(); if (defaultResource) { m_userDisplayName = userObj.value("displayName").toString(); m_userId = userObj.value("id").toString(); if (m_userId.isEmpty()) { qCDebug(lcSocialPlugin) << "Unable to determine user id for default resource, aborting"; decrementSemaphore(accountId); return; } m_db.syncAccount(accountId, m_userId); } else if (m_userId != userObj.value("id").toString()) { // ignore this album, not created by the current user. qCDebug(lcSocialPlugin) << "Ignoring album" << parsed.value("name").toString() << " - different user."; decrementSemaphore(accountId); return; } const QJsonObject fileSystemInfo = parsed.value("fileSystemInfo").toObject(); const QString albumId = parsed.value("id").toString(); const QDateTime createdTime = QDateTime::fromString(fileSystemInfo.value("createdDateTime").toString(), Qt::ISODate); const QDateTime updatedTime = QDateTime::fromString(fileSystemInfo.value("lastModifiedDateTime").toString(), Qt::ISODate); const QString albumName = parsed.value("name").toString(); int photoCount = 0; const QJsonArray children = parsed.value("children").toArray(); for (int i = 0; i < children.size(); ++i) { const QJsonObject child = children.at(i).toObject(); if (child.contains("folder")) { const QJsonObject parentReference = child.value("parentReference").toObject(); const QString onedriveResourceTarget = QStringLiteral("%1/%2").arg(parentReference.value("path").toString(), child.value("name").toString()); qCDebug(lcSocialPlugin) << "Found subfolder:" << child.value("name").toString() << "of folder:" << albumName; requestResource(accountId, accessToken, onedriveResourceTarget); } else if (child.contains("image") && child.contains("@microsoft.graph.downloadUrl")) { photoCount++; const QJsonArray thumbnails = child.value("thumbnails").toArray(); const QJsonObject thumbnail = thumbnails.size() ? thumbnails.at(0).toObject() : QJsonObject(); const QString photoId = child.value("id").toString(); const QDateTime photoCreatedTime = QDateTime::fromString(child.value("createdDateTime").toString(), Qt::ISODate); const QDateTime photoUpdatedTime = QDateTime::fromString(child.value("lastModifiedDateTime").toString(), Qt::ISODate); const QString photoName = child.value("name").toString(); const int imageWidth = child.value("image").toObject().value("width").toInt(); const int imageHeight = child.value("image").toObject().value("height").toInt(); const QString photoThumbnailUrl = thumbnail.value("medium").toObject().value("url").toString(); const QString photoImageSrcUrl = QStringLiteral("%1%2%3%4%5") .arg(api()).arg("/me").arg("/drive/items/").arg(photoId).arg("/content"); const QString photoDescription = child.value("description").toString(); const ImageData image(photoId, albumId, m_userId, photoCreatedTime, photoUpdatedTime, photoName, imageWidth, imageHeight, photoThumbnailUrl, photoImageSrcUrl, photoDescription); // record the fact that we've seen this photo in this album m_serverAlbumImageIds[albumId].insert(photoId); // now check to see if this image has changed server-side const OneDriveImage::ConstPtr &dbImage = m_db.image(photoId); if (dbImage.isNull() || dbImage->imageId() != photoId || dbImage->createdTime().toTime_t() < photoCreatedTime.toTime_t() || dbImage->updatedTime().toTime_t() < photoUpdatedTime.toTime_t()) { // changed, need to update in our local db. qCDebug(lcSocialPlugin) << "Image:" << photoName << "in folder:" << albumName << "added or changed on server"; m_imageData.insert(photoId, image); } } } const OneDriveAlbum::ConstPtr &dbAlbum = m_cachedAlbums.value(albumId); m_cachedAlbums.remove(albumId); // Removal detection m_seenAlbums.insert(albumId); if (!dbAlbum.isNull() && (dbAlbum->updatedTime().toTime_t() >= updatedTime.toTime_t())) { qCDebug(lcSocialPlugin) << "album with id" << albumId << "by user" << m_userId << "from OneDrive account with id" << accountId << "doesn't need update"; } else { qCDebug(lcSocialPlugin) << "Album:" << albumName << "added or changed on server"; const AlbumData album(albumId, m_userId, createdTime, updatedTime, albumName, photoCount); if (m_albumData.contains(albumId)) { // updating due to nextlink / continuation request m_albumData[albumId].imageCount += photoCount; } else { // new unseen album. m_albumData.insert(albumId, album); } } // if more than 200 children exist, the result will include a // continuation request / pagination request next-link URL. const QString nextLink = parsed.value("@odata.nextLink").toString(); if (!nextLink.isEmpty()) { requestNextLink(accountId, accessToken, nextLink, defaultResource); } decrementSemaphore(accountId); } bool OneDriveImageSyncAdaptor::initRemovalDetectionLists(int accountId) { // This function should be called as part of the ::sync() preamble. // Clear our internal state variables which we use to track server-side deletions. // We have to do it this way, as results can be spread across multiple requests // if OneDrive returns results in paginated form. clearRemovalDetectionLists(); bool ok = false; QMap accounts = m_db.accounts(&ok); if (!ok) { return false; } if (accounts.contains(accountId)) { QString userId = accounts.value(accountId); QStringList allAlbumIds = m_db.allAlbumIds(); foreach (const QString& albumId, allAlbumIds) { OneDriveAlbum::ConstPtr album = m_db.album(albumId); if (album->userId() == userId) { m_cachedAlbums.insert(albumId, album); } } } return true; } void OneDriveImageSyncAdaptor::clearRemovalDetectionLists() { m_cachedAlbums.clear(); m_serverAlbumImageIds.clear(); m_removedImages.clear(); } void OneDriveImageSyncAdaptor::checkRemovedImages(const QString &albumId) { const QSet &serverImageIds = m_serverAlbumImageIds.value(albumId); QSet dbImageIds = m_db.imageIds(albumId).toSet(); foreach (const QString &imageId, serverImageIds) { dbImageIds.remove(imageId); } m_removedImages.append(dbImageIds.toList()); } buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-images/onedriveimagesyncadaptor.h000066400000000000000000000103311474572147200315560ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015 Jolla Ltd. ** Contact: Antti Seppälä ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef ONEDRIVEIMAGESYNCADAPTOR_H #define ONEDRIVEIMAGESYNCADAPTOR_H #include "onedrivedatatypesyncadaptor.h" #include #include #include #include #include #include #include #include #include #include #include class OneDriveImageSyncAdaptor : public OneDriveDataTypeSyncAdaptor { Q_OBJECT public: OneDriveImageSyncAdaptor(QObject *parent); ~OneDriveImageSyncAdaptor(); QString syncServiceName() const; void sync(const QString &dataTypeString, int accountId); protected: // implementing OneDriveDataTypeSyncAdaptor interface void purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode mode); void beginSync(int accountId, const QString &accessToken); void finalize(int accountId); private: void requestResource(int accountId, const QString &accessToken, const QString &onedriveResource = QString()); void requestNextLink(int accountId, const QString &accessToken, const QString &nextLink, bool isDefaultResource); private Q_SLOTS: void resourceFinishedHandler(); private: struct AlbumData { AlbumData(); AlbumData(const QString &albumId, const QString &userId, const QDateTime &createdTime, const QDateTime &updatedTime, const QString &albumName, int imageCount); AlbumData(const AlbumData &other); QString albumId; QString userId; QDateTime createdTime; QDateTime updatedTime; QString albumName; int imageCount; }; struct ImageData { ImageData(); ImageData(const QString &photoId, const QString &albumId, const QString &userId, const QDateTime &createdTime, const QDateTime &updatedTime, const QString &photoName, int imageWidth, int imageHeight, const QString &thumbnailUrl, const QString &imageSourceUrl, const QString &description); ImageData(const ImageData &other); QString photoId; QString albumId; QString userId; QDateTime createdTime; QDateTime updatedTime; QString photoName; int imageWidth; int imageHeight; QString thumbnailUrl; QString imageSourceUrl; QString description; }; private: // for server-side removal detection. bool initRemovalDetectionLists(int accountId); void clearRemovalDetectionLists(); void checkRemovedImages(const QString &fbAlbumId); QMap m_cachedAlbums; QSet m_seenAlbums; QMap m_albumData; QMap m_imageData; QMap > m_serverAlbumImageIds; QStringList m_removedImages; QString m_userId; QString m_userDisplayName; OneDriveImagesDatabase m_db; SocialImagesDatabase m_imageCacheDb; }; #endif // ONEDRIVEIMAGESYNCADAPTOR_H buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-signon/000077500000000000000000000000001474572147200243515ustar00rootroot00000000000000buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-signon/onedrive-signon.pri000066400000000000000000000001621474572147200301720ustar00rootroot00000000000000SOURCES += $$PWD/onedrivesignonsyncadaptor.cpp HEADERS += $$PWD/onedrivesignonsyncadaptor.h INCLUDEPATH += $$PWD buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-signon/onedrive-signon.pro000066400000000000000000000012421474572147200302000ustar00rootroot00000000000000TARGET = onedrive-signon-client include($$PWD/../../common.pri) include($$PWD/../onedrive-common.pri) include($$PWD/onedrive-signon.pri) onedrive_signon_sync_profile.path = /etc/buteo/profiles/sync onedrive_signon_sync_profile.files = $$PWD/onedrive.Signon.xml onedrive_signon_client_plugin_xml.path = /etc/buteo/profiles/client onedrive_signon_client_plugin_xml.files = $$PWD/onedrive-signon.xml HEADERS += onedrivesignonplugin.h SOURCES += onedrivesignonplugin.cpp OTHER_FILES += \ onedrive_signon_sync_profile.files \ onedrive_signon_client_plugin_xml.files INSTALLS += \ target \ onedrive_signon_sync_profile \ onedrive_signon_client_plugin_xml buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-signon/onedrive-signon.xml000066400000000000000000000002061474572147200301770ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-signon/onedrive.Signon.xml000066400000000000000000000011221474572147200301360ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-signon/onedrivesignonplugin.cpp000066400000000000000000000035741474572147200313360ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2015 - 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "onedrivesignonplugin.h" #include "onedrivesignonsyncadaptor.h" #include "socialnetworksyncadaptor.h" OneDriveSignonPlugin::OneDriveSignonPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *callbackInterface) : SocialdButeoPlugin(pluginName, profile, callbackInterface, QStringLiteral("onedrive"), SocialNetworkSyncAdaptor::dataTypeName(SocialNetworkSyncAdaptor::Signon)) { } OneDriveSignonPlugin::~OneDriveSignonPlugin() { } SocialNetworkSyncAdaptor *OneDriveSignonPlugin::createSocialNetworkSyncAdaptor() { return new OneDriveSignonSyncAdaptor(this); } Buteo::ClientPlugin* OneDriveSignonPluginLoader::createClientPlugin( const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) { return new OneDriveSignonPlugin(pluginName, profile, cbInterface); } buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-signon/onedrivesignonplugin.h000066400000000000000000000036021474572147200307730ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2015 - 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef ONEDRIVESIGNONPLUGIN_H #define ONEDRIVESIGNONPLUGIN_H #include "socialdbuteoplugin.h" #include class Q_DECL_EXPORT OneDriveSignonPlugin : public SocialdButeoPlugin { Q_OBJECT public: OneDriveSignonPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *cbInterface); ~OneDriveSignonPlugin(); protected: SocialNetworkSyncAdaptor *createSocialNetworkSyncAdaptor(); }; class OneDriveSignonPluginLoader : public Buteo::SyncPluginLoader { Q_OBJECT Q_PLUGIN_METADATA(IID "org.sailfishos.plugins.sync.OneDriveSignonPluginLoader") Q_INTERFACES(Buteo::SyncPluginLoader) public: Buteo::ClientPlugin* createClientPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) override; }; #endif // ONEDRIVESIGNONPLUGIN_H buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-signon/onedrivesignonsyncadaptor.cpp000066400000000000000000000264531474572147200323700ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015 Jolla Ltd. ** Contact: Chris Adams ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "onedrivesignonsyncadaptor.h" #include "trace.h" #include #include #include #include #include #include #include OneDriveSignonSyncAdaptor::OneDriveSignonSyncAdaptor(QObject *parent) : OneDriveDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::Signon, parent) { setInitialActive(true); } OneDriveSignonSyncAdaptor::~OneDriveSignonSyncAdaptor() { } QString OneDriveSignonSyncAdaptor::syncServiceName() const { return QStringLiteral("onedrive-sync"); // TODO: change name of service to onedrive-signon! } void OneDriveSignonSyncAdaptor::sync(const QString &dataTypeString, int accountId) { // call superclass impl. OneDriveDataTypeSyncAdaptor::sync(dataTypeString, accountId); } void OneDriveSignonSyncAdaptor::purgeDataForOldAccount(int, SocialNetworkSyncAdaptor::PurgeMode) { // Nothing to do. } void OneDriveSignonSyncAdaptor::finalize(int) { // nothing to do } void OneDriveSignonSyncAdaptor::beginSync(int accountId, const QString &accessToken) { Q_UNUSED(accessToken); refreshTokens(accountId); } Accounts::Account *OneDriveSignonSyncAdaptor::loadAccount(int accountId) { Accounts::Account *acc = 0; if (m_accounts.contains(accountId)) { acc = m_accounts[accountId]; } else { acc = Accounts::Account::fromId(&m_accountManager, accountId, this); if (!acc) { qCWarning(lcSocialPlugin) << QString(QLatin1String("error: OneDrive account %1 was deleted during signon refresh sync")) .arg(accountId); return 0; } else { m_accounts.insert(accountId, acc); } } Accounts::Service srv = m_accountManager.service(syncServiceName()); if (!srv.isValid()) { qCWarning(lcSocialPlugin) << QString(QLatin1String("error: invalid service %1 specified for refresh sync with OneDrive account: %2")) .arg(syncServiceName()).arg(accountId); return 0; } return acc; } void OneDriveSignonSyncAdaptor::raiseCredentialsNeedUpdateFlag(int accountId) { Accounts::Account *acc = loadAccount(accountId); if (acc) { qCWarning(lcSocialPlugin) << "ODSSA: raising CredentialsNeedUpdate flag"; Accounts::Service srv = m_accountManager.service(syncServiceName()); acc->selectService(srv); acc->setValue(QStringLiteral("CredentialsNeedUpdate"), QVariant::fromValue(true)); acc->setValue(QStringLiteral("CredentialsNeedUpdateFrom"), QVariant::fromValue(QString::fromLatin1("sociald-onedrive-signon"))); acc->selectService(Accounts::Service()); acc->syncAndBlock(); } } void OneDriveSignonSyncAdaptor::lowerCredentialsNeedUpdateFlag(int accountId) { Accounts::Account *acc = loadAccount(accountId); if (acc) { qCInfo(lcSocialPlugin) << "ODSSA: lowering CredentialsNeedUpdate flag"; Accounts::Service srv = m_accountManager.service(syncServiceName()); acc->selectService(srv); acc->setValue(QStringLiteral("CredentialsNeedUpdate"), QVariant::fromValue(false)); acc->remove(QStringLiteral("CredentialsNeedUpdateFrom")); acc->selectService(Accounts::Service()); acc->syncAndBlock(); } } void OneDriveSignonSyncAdaptor::refreshTokens(int accountId) { Accounts::Account *acc = loadAccount(accountId); if (!acc) { return; } // First perform a "normal" signon. Then force token expiry. Then signon to refresh the tokens. Accounts::Service srv(m_accountManager.service(syncServiceName())); acc->selectService(srv); SignOn::Identity *identity = acc->credentialsId() > 0 ? SignOn::Identity::existingIdentity(acc->credentialsId()) : 0; if (!identity) { qCWarning(lcSocialPlugin) << QString(QLatin1String("error: OneDrive account %1 has no valid credentials, cannot perform refresh sync")) .arg(accountId); return; } Accounts::AccountService *accSrv = new Accounts::AccountService(acc, srv); if (!accSrv) { qCWarning(lcSocialPlugin) << QString(QLatin1String("error: OneDrive account %1 has no valid account service, cannot perform refresh sync")) .arg(accountId); identity->deleteLater(); return; } QString method = accSrv->authData().method(); QString mechanism = accSrv->authData().mechanism(); SignOn::AuthSession *session = identity->createSession(method); if (!session) { qCWarning(lcSocialPlugin) << QString(QLatin1String("error: could not create signon session for OneDrive account %1, cannot perform refresh sync")) .arg(accountId); accSrv->deleteLater(); identity->deleteLater(); return; } QVariantMap signonSessionData = accSrv->authData().parameters(); signonSessionData.insert("ClientId", clientId()); signonSessionData.insert("UiPolicy", SignOn::NoUserInteractionPolicy); connect(session, SIGNAL(response(SignOn::SessionData)), this, SLOT(initialSignonResponse(SignOn::SessionData)), Qt::UniqueConnection); connect(session, SIGNAL(error(SignOn::Error)), this, SLOT(signonError(SignOn::Error)), Qt::UniqueConnection); incrementSemaphore(accountId); session->setProperty("accountId", accountId); session->setProperty("mechanism", mechanism); session->setProperty("signonSessionData", signonSessionData); m_idents.insert(accountId, identity); session->process(SignOn::SessionData(signonSessionData), mechanism); } void OneDriveSignonSyncAdaptor::initialSignonResponse(const SignOn::SessionData &responseData) { SignOn::AuthSession *session = qobject_cast(sender()); session->disconnect(this); if (syncAborted()) { // don't expire the tokens - we may have lost network connectivity // while we were attempting to perform signon sync, and that would // leave us in a position where we're unable to automatically recover. int accountId = session->property("accountId").toInt(); qCInfo(lcSocialPlugin) << "aborting signon sync refresh"; decrementSemaphore(accountId); return; } connect(session, SIGNAL(response(SignOn::SessionData)), this, SLOT(forceTokenExpiryResponse(SignOn::SessionData)), Qt::UniqueConnection); connect(session, SIGNAL(error(SignOn::Error)), this, SLOT(signonError(SignOn::Error)), Qt::UniqueConnection); QString mechanism = session->property("mechanism").toString(); QVariantMap signonSessionData = session->property("signonSessionData").toMap(); // Now expire the tokens. QVariantMap providedTokens; providedTokens.insert("AccessToken", responseData.getProperty(QStringLiteral("AccessToken")).toString()); providedTokens.insert("RefreshToken", responseData.getProperty(QStringLiteral("RefreshToken")).toString()); providedTokens.insert("ExpiresIn", 2); signonSessionData.insert("ProvidedTokens", providedTokens); session->process(SignOn::SessionData(signonSessionData), mechanism); } void OneDriveSignonSyncAdaptor::forceTokenExpiryResponse(const SignOn::SessionData &) { SignOn::AuthSession *session = qobject_cast(sender()); session->disconnect(this); QString mechanism = session->property("mechanism").toString(); QVariantMap signonSessionData = session->property("signonSessionData").toMap(); QTimer *timer = new QTimer(this); timer->setInterval(4000); timer->setSingleShot(true); timer->setProperty("mechanism", mechanism); timer->setProperty("signonSessionData", signonSessionData); timer->setProperty("session", QVariant::fromValue(session)); connect(timer, SIGNAL(timeout()), this, SLOT(triggerRefresh())); timer->start(); } void OneDriveSignonSyncAdaptor::triggerRefresh() { QTimer *timer = qobject_cast(sender()); timer->deleteLater(); QString mechanism = timer->property("mechanism").toString(); QVariantMap signonSessionData = timer->property("signonSessionData").toMap(); SignOn::AuthSession *session = timer->property("session").value(); connect(session, SIGNAL(response(SignOn::SessionData)), this, SLOT(refreshTokenResponse(SignOn::SessionData)), Qt::UniqueConnection); connect(session, SIGNAL(error(SignOn::Error)), this, SLOT(signonError(SignOn::Error)), Qt::UniqueConnection); session->process(SignOn::SessionData(signonSessionData), mechanism); } void OneDriveSignonSyncAdaptor::refreshTokenResponse(const SignOn::SessionData &responseData) { SignOn::AuthSession *session = qobject_cast(sender()); int accountId = session->property("accountId").toInt(); session->disconnect(this); SignOn::Identity *identity = m_idents.take(accountId); if (identity) { identity->destroySession(session); identity->deleteLater(); } else { session->deleteLater(); } qCInfo(lcSocialPlugin) << QString(QLatin1String("successfully performed signon refresh for OneDrive account %1: new ExpiresIn: %3")) .arg(accountId).arg(responseData.getProperty("ExpiresIn").toInt()); lowerCredentialsNeedUpdateFlag(accountId); decrementSemaphore(accountId); } void OneDriveSignonSyncAdaptor::signonError(const SignOn::Error &error) { SignOn::AuthSession *session = qobject_cast(sender()); int accountId = session->property("accountId").toInt(); session->disconnect(this); SignOn::Identity *identity = m_idents.take(accountId); if (identity) { identity->destroySession(session); identity->deleteLater(); } else { session->deleteLater(); } bool raiseFlag = error.type() == SignOn::Error::UserInteraction; qCInfo(lcSocialPlugin) << QString(QLatin1String("got signon error when performing signon refresh for OneDrive account %1: %2: %3. Raising flag? %4")) .arg(accountId).arg(error.type()).arg(error.message()).arg(raiseFlag); if (raiseFlag) { // UserInteraction error is returned if user interaction is required. raiseCredentialsNeedUpdateFlag(accountId); } decrementSemaphore(accountId); } buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive-signon/onedrivesignonsyncadaptor.h000066400000000000000000000052341474572147200320270ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015 Jolla Ltd. ** Contact: Chris Adams ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef ONEDRIVESIGNONSYNCADAPTOR_H #define ONEDRIVESIGNONSYNCADAPTOR_H #include "onedrivedatatypesyncadaptor.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include class OneDriveSignonSyncAdaptor : public OneDriveDataTypeSyncAdaptor { Q_OBJECT public: OneDriveSignonSyncAdaptor(QObject *parent); ~OneDriveSignonSyncAdaptor(); QString syncServiceName() const; void sync(const QString &dataTypeString, int accountId = 0); protected: // implementing OneDriveDataTypeSyncAdaptor interface void purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode mode); void beginSync(int accountId, const QString &accessToken); void finalize(int accountId); private Q_SLOTS: void initialSignonResponse(const SignOn::SessionData &responseData); void forceTokenExpiryResponse(const SignOn::SessionData &responseData); void refreshTokenResponse(const SignOn::SessionData &responseData); void signonError(const SignOn::Error &error); void triggerRefresh(); private: Accounts::Account *loadAccount(int accountId); void raiseCredentialsNeedUpdateFlag(int accountId); void lowerCredentialsNeedUpdateFlag(int accountId); void refreshTokens(int accountId); Accounts::Manager m_accountManager; QMap m_accounts; QMap m_idents; }; #endif // ONEDRIVESIGNONSYNCADAPTOR_H buteo-sync-plugins-social-0.4.28/src/onedrive/onedrive.pro000066400000000000000000000002651474572147200236030ustar00rootroot00000000000000TEMPLATE = subdirs SUBDIRS = \ $$PWD/onedrive-signon \ $$PWD/onedrive-images \ $$PWD/onedrive-backup \ $$PWD/onedrive-backupquery \ $$PWD/onedrive-backuprestore buteo-sync-plugins-social-0.4.28/src/onedrive/onedrivebackupoperationsyncadaptor.cpp000066400000000000000000001500621474572147200311450ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2020 Open Mobile Platform LLC. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "onedrivebackupoperationsyncadaptor.h" #include "trace.h" #include #include #include #include #include #include #include #include #include #include #include namespace { // OneDrive upload fragments must be multiple of 320kb static const qint64 UploadChunkSize = 327680; void debugDumpResponse(const QByteArray &data) { QString alldata = QString::fromUtf8(data); QStringList alldatasplit = alldata.split('\n'); Q_FOREACH (const QString &s, alldatasplit) { qCDebug(lcSocialPlugin) << s; } } void debugDumpJsonResponse(const QByteArray &data) { if (!lcSocialPluginTrace().isDebugEnabled()) { return; } // Prettify the json for outputting line-by-line. QString output; QString json = QString::fromUtf8(data); QString leadingSpace = ""; for (int i = 0; i < json.size(); ++i) { if (json[i] == '{') { leadingSpace = leadingSpace + " "; output = output + json[i] + '\n' + leadingSpace; } else if (json[i] == '}') { if (leadingSpace.size() >= 4) { leadingSpace.chop(4); } output = output + '\n' + leadingSpace + json[i]; } else if (json[i] == ',') { output = output + json[i] + '\n' + leadingSpace; } else if (json[i] == '\n' || json[i] == '\r') { // ignore newlines/carriage returns } else { output = output + json[i]; } } debugDumpResponse(output.toUtf8()); } } OneDriveBackupOperationSyncAdaptor::OneDriveBackupOperationSyncAdaptor(SocialNetworkSyncAdaptor::DataType dataType, QObject *parent) : OneDriveDataTypeSyncAdaptor(dataType, parent) , m_sailfishBackup(new QDBusInterface("org.sailfishos.backup", "/sailfishbackup", "org.sailfishos.backup", QDBusConnection::sessionBus(), this)) , m_remoteAppDir(QStringLiteral("drive/special/approot")) { m_sailfishBackup->connection().connect( m_sailfishBackup->service(), m_sailfishBackup->path(), m_sailfishBackup->interface(), "cloudBackupStatusChanged", this, SLOT(cloudBackupStatusChanged(int,QString))); m_sailfishBackup->connection().connect( m_sailfishBackup->service(), m_sailfishBackup->path(), m_sailfishBackup->interface(), "cloudBackupError", this, SLOT(cloudBackupError(int,QString,QString))); m_sailfishBackup->connection().connect( m_sailfishBackup->service(), m_sailfishBackup->path(), m_sailfishBackup->interface(), "cloudRestoreStatusChanged", this, SLOT(cloudRestoreStatusChanged(int,QString))); m_sailfishBackup->connection().connect( m_sailfishBackup->service(), m_sailfishBackup->path(), m_sailfishBackup->interface(), "cloudRestoreError", this, SLOT(cloudRestoreError(int,QString,QString))); } OneDriveBackupOperationSyncAdaptor::~OneDriveBackupOperationSyncAdaptor() { } QString OneDriveBackupOperationSyncAdaptor::syncServiceName() const { // this service covers all of these sync profiles: backup, backup query and restore. return QStringLiteral("onedrive-backup"); } void OneDriveBackupOperationSyncAdaptor::sync(const QString &dataTypeString, int accountId) { OneDriveDataTypeSyncAdaptor::sync(dataTypeString, accountId); } void OneDriveBackupOperationSyncAdaptor::purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode) { purgeAccount(oldId); } void OneDriveBackupOperationSyncAdaptor::beginSync(int accountId, const QString &accessToken) { QDBusReply backupDeviceIdReply = m_sailfishBackup->call("backupFileDeviceId"); if (backupDeviceIdReply.value().isEmpty()) { qCWarning(lcSocialPlugin) << "Backup device ID is invalid!"; setStatus(SocialNetworkSyncAdaptor::Error); return; } m_remoteDirPath = QString::fromLatin1("Backups/%1").arg(backupDeviceIdReply.value()); m_accountId = accountId; m_accessToken = accessToken; switch (operation()) { case Backup: { QDBusReply createBackupReply = m_sailfishBackup->call("createBackupForSyncProfile", m_accountSyncProfile->name()); if (!createBackupReply.isValid() || createBackupReply.value().isEmpty()) { qCWarning(lcSocialPlugin) << "Call to createBackupForSyncProfile() failed:" << createBackupReply.error().name() << createBackupReply.error().message(); setStatus(SocialNetworkSyncAdaptor::Error); return; } // Wait for org.sailfish.backup service to finish creating the backup. // we're requesting data. Increment the semaphore so that we know we're still busy. incrementSemaphore(accountId); m_localFileInfo = QFileInfo(createBackupReply.value()); break; } case BackupQuery: { beginListOperation(accountId, accessToken, m_remoteDirPath); break; } case BackupRestore: { const QString filePath = m_accountSyncProfile->key(QStringLiteral("sfos-backuprestore-file")); if (filePath.isEmpty()) { qCWarning(lcSocialPlugin) << "No remote file has been set!"; setStatus(SocialNetworkSyncAdaptor::Error); return; } m_localFileInfo = QFileInfo(filePath); QDir localDir; if (!localDir.mkpath(m_localFileInfo.absolutePath())) { qCWarning(lcSocialPlugin) << "Could not create local backup directory:" << m_localFileInfo.absolutePath() << "for OneDrive account:" << accountId; setStatus(SocialNetworkSyncAdaptor::Error); return; } beginSyncOperation(accountId, accessToken); break; } default: qCWarning(lcSocialPlugin) << "Unrecognized sync operation: " + operation(); setStatus(SocialNetworkSyncAdaptor::Error); break; } } void OneDriveBackupOperationSyncAdaptor::cloudBackupStatusChanged(int accountId, const QString &status) { if (accountId != m_accountId) { return; } qCDebug(lcSocialPlugin) << "Backup status changed:" << status << "for file:" << m_localFileInfo.absoluteFilePath(); if (status == QLatin1String("UploadingBackup")) { if (!m_localFileInfo.exists()) { qCWarning(lcSocialPlugin) << "Backup finished, but cannot find the backup file:" << m_localFileInfo.absoluteFilePath(); setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(m_accountId); return; } beginSyncOperation(m_accountId, m_accessToken); decrementSemaphore(m_accountId); } else if (status == QLatin1String("Canceled")) { qCWarning(lcSocialPlugin) << "Cloud backup was canceled"; setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(m_accountId); } else if (status == QLatin1String("Error")) { qCWarning(lcSocialPlugin) << "Failed to create backup file:" << m_localFileInfo.absoluteFilePath(); setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(m_accountId); } } void OneDriveBackupOperationSyncAdaptor::cloudBackupError(int accountId, const QString &error, const QString &errorString) { if (accountId != m_accountId) { return; } qCWarning(lcSocialPlugin) << "Cloud backup error was:" << error << errorString; setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(m_accountId); } void OneDriveBackupOperationSyncAdaptor::cloudRestoreStatusChanged(int accountId, const QString &status) { if (accountId != m_accountId) { return; } qCDebug(lcSocialPlugin) << "Backup restore status changed:" << status << "for file:" << m_localFileInfo.absoluteFilePath(); if (status == QLatin1String("Canceled")) { qCWarning(lcSocialPlugin) << "Cloud backup restore was canceled"; setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(m_accountId); } else if (status == QLatin1String("Error")) { qCWarning(lcSocialPlugin) << "Cloud backup restore failed"; setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(m_accountId); } } void OneDriveBackupOperationSyncAdaptor::cloudRestoreError(int accountId, const QString &error, const QString &errorString) { if (accountId != m_accountId) { return; } qCWarning(lcSocialPlugin) << "Cloud backup restore error was:" << error << errorString; } void OneDriveBackupOperationSyncAdaptor::beginListOperation(int accountId, const QString &accessToken, const QString &remoteDirPath) { if (remoteDirPath.isEmpty()) { qCWarning(lcSocialPlugin) << "Cannot fetch directory listing, remote path path set"; setStatus(SocialNetworkSyncAdaptor::Error); return; } QUrl url(QStringLiteral("%1/%2:/%3:/").arg(api(), QStringLiteral("drive/special/approot"), remoteDirPath)); QUrlQuery query(url); QList > queryItems; queryItems.append(QPair(QStringLiteral("expand"), QStringLiteral("children"))); query.setQueryItems(queryItems); url.setQuery(query); QNetworkRequest req(url); req.setRawHeader(QString(QLatin1String("Authorization")).toUtf8(), QString(QLatin1String("Bearer ")).toUtf8() + accessToken.toUtf8()); QNetworkReply *reply = m_networkAccessManager->get(req); if (reply) { reply->setProperty("accountId", accountId); reply->setProperty("accessToken", accessToken); reply->setProperty("remotePath", remoteDirPath); connect(reply, &QNetworkReply::finished, this, &OneDriveBackupOperationSyncAdaptor::listOperationFinished); // we're requesting data. Increment the semaphore so that we know we're still busy. incrementSemaphore(accountId); setupReplyTimeout(accountId, reply, 10 * 60 * 1000); // 10 minutes } else { qCWarning(lcSocialPlugin) << "unable to start directory listing request for OneDrive account with id" << accountId; } } void OneDriveBackupOperationSyncAdaptor::listOperationFinished() { QNetworkReply *reply = qobject_cast(sender()); QByteArray data = reply->readAll(); int accountId = reply->property("accountId").toInt(); QString remotePath = reply->property("remotePath").toString(); int httpCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); bool isError = reply->property("isError").toBool(); reply->deleteLater(); removeReplyTimeout(accountId, reply); if (isError) { // Show error but don't set error status until error code is checked more thoroughly. qCWarning(lcSocialPlugin) << "error occurred when performing Backup remote path request for OneDrive account" << accountId; debugDumpResponse(data); } bool ok = false; const QJsonObject parsed = parseJsonObjectReplyData(data, &ok); const QJsonArray entries = parsed.value("children").toArray(); if (!ok || entries.isEmpty()) { QString errorMessage = parsed.value("error").toString(); if (!errorMessage.isEmpty()) { qCWarning(lcSocialPlugin) << "OneDrive returned error message:" << errorMessage; errorMessage.clear(); } // Directory may be not found or be empty if user has deleted backups. Only emit the error // signal if parsing failed or there was an unexpected error code. if (!ok) { errorMessage = QStringLiteral("Failed to parse directory listing at %1 for account %2").arg(remotePath).arg(accountId); } else if (httpCode != 200 && httpCode != 404 && httpCode != 410) { errorMessage = QStringLiteral("Directory listing request at %1 for account %2 failed").arg(remotePath).arg(accountId); } if (errorMessage.isEmpty()) { qCDebug(lcSocialPlugin) << "Completed directory listing for account:" << accountId; } else { qCWarning(lcSocialPlugin) << errorMessage; setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(accountId); return; } } if (entries.isEmpty()) { qCDebug(lcSocialPlugin) << "No entries found in dir listing, but not an error (e.g. maybe file was deleted on server)"; debugDumpResponse(data); } else { qCDebug(lcSocialPlugin) << "Parsed dir listing entries:" << entries; } QStringList dirListing; for (const QJsonValue &child : entries) { const QString childName = child.toObject().value("name").toString(); if (child.toObject().keys().contains("folder")) { qCDebug(lcSocialPlugin) << "ignoring folder:" << childName << "under remote backup path:" << remotePath << "for account:" << accountId; } else { qCDebug(lcSocialPlugin) << "found remote backup object:" << childName << "for account:" << accountId << "under remote backup path:" << remotePath; dirListing.append(remotePath + '/' + childName); } } QDBusReply setCloudBackupsReply = m_sailfishBackup->call("setCloudBackups", m_accountSyncProfile->name(), dirListing); if (!setCloudBackupsReply.isValid()) { qCDebug(lcSocialPlugin) << "Call to setCloudBackups() failed:" << setCloudBackupsReply.error().name() << setCloudBackupsReply.error().message(); } else { qCDebug(lcSocialPlugin) << "Wrote directory listing for profile:" << m_accountSyncProfile->name() << dirListing; } decrementSemaphore(accountId); } void OneDriveBackupOperationSyncAdaptor::beginSyncOperation(int accountId, const QString &accessToken) { QString direction = operation() == Backup ? Buteo::VALUE_TO_REMOTE : (operation() == BackupRestore ? Buteo::VALUE_FROM_REMOTE : QString()); if (direction.isEmpty()) { qCWarning(lcSocialPlugin) << "Invalid sync operation" << operation() << "for OneDrive account:" << accountId; setStatus(SocialNetworkSyncAdaptor::Error); return; } // either upsync or downsync as required. if (direction == Buteo::VALUE_TO_REMOTE || direction == Buteo::VALUE_FROM_REMOTE) { // Perform an initial app folder request before upload/download. initialiseAppFolderRequest(accountId, accessToken, m_localFileInfo.absolutePath(), m_remoteDirPath, m_localFileInfo.fileName(), direction); } else { qCWarning(lcSocialPlugin) << "No direction set for OneDrive Backup sync with account:" << accountId; setStatus(SocialNetworkSyncAdaptor::Error); return; } } void OneDriveBackupOperationSyncAdaptor::initialiseAppFolderRequest(int accountId, const QString &accessToken, const QString &localPath, const QString &remotePath, const QString &remoteFile, const QString &syncDirection) { // initialise the app folder and get the remote id of the drive/special/approot path. // e.g., let's say we have a final path like: drive/special/approot/Backups/ABCDEFG/backup.tar // this request will get us the id of the drive/special/approot bit. QUrl url = QUrl(QStringLiteral("%1/%2").arg(api(), QStringLiteral("drive/special/approot"))); QNetworkRequest req(url); req.setRawHeader(QString(QLatin1String("Authorization")).toUtf8(), QString(QLatin1String("Bearer ")).toUtf8() + accessToken.toUtf8()); QNetworkReply *reply = m_networkAccessManager->get(req); if (reply) { reply->setProperty("accountId", accountId); reply->setProperty("accessToken", accessToken); reply->setProperty("localPath", localPath); reply->setProperty("remotePath", remotePath); reply->setProperty("remoteFile", remoteFile); reply->setProperty("syncDirection", syncDirection); connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(errorHandler(QNetworkReply::NetworkError))); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsHandler(QList))); connect(reply, SIGNAL(finished()), this, SLOT(initialiseAppFolderFinishedHandler())); // we're requesting data. Increment the semaphore so that we know we're still busy. incrementSemaphore(accountId); setupReplyTimeout(accountId, reply, 10 * 60 * 1000); // 10 minutes } else { qCWarning(lcSocialPlugin) << "unable to create app folder initialisation request for OneDrive account with id" << accountId; } } void OneDriveBackupOperationSyncAdaptor::initialiseAppFolderFinishedHandler() { QNetworkReply *reply = qobject_cast(sender()); QByteArray data = reply->readAll(); int accountId = reply->property("accountId").toInt(); QString accessToken = reply->property("accessToken").toString(); QString localPath = reply->property("localPath").toString(); QString remotePath = reply->property("remotePath").toString(); QString remoteFile = reply->property("remoteFile").toString(); QString syncDirection = reply->property("syncDirection").toString(); bool isError = reply->property("isError").toBool(); reply->deleteLater(); removeReplyTimeout(accountId, reply); bool ok = false; QJsonObject parsed = parseJsonObjectReplyData(data, &ok); if (isError || !ok) { qCWarning(lcSocialPlugin) << "error occurred when performing initialiseAppFolder request with OneDrive account:" << accountId; debugDumpJsonResponse(data); setStatus(SocialNetworkSyncAdaptor::Error); } else { qCDebug(lcSocialPlugin) << "initialiseAppFolder request succeeded with OneDrive account:" << accountId; qCDebug(lcSocialPlugin) << "app folder has remote ID:" << parsed.value("id").toString(); // Initialize our list of remote directories to create if (syncDirection == Buteo::VALUE_TO_REMOTE) { QString remoteParentPath = m_remoteAppDir; Q_FOREACH (const QString &dir, remotePath.split('/', QString::SkipEmptyParts)) { OneDriveBackupOperationSyncAdaptor::RemoteDirectory remoteDir; remoteDir.dirName = dir; remoteDir.remoteId = QString(); remoteDir.parentPath = remoteParentPath; remoteDir.parentId = QString(); remoteDir.created = false; m_remoteDirectories.append(remoteDir); remoteParentPath = QStringLiteral("%1/%2").arg(remoteParentPath).arg(dir); } // Read out the app folder remote ID from the response and set it as the parent ID of the first remote dir. m_remoteDirectories[0].parentId = parsed.value("id").toString(); qCDebug(lcSocialPlugin) << "Set the parentId of the first subfolder:" << m_remoteDirectories[0].dirName << "to:" << m_remoteDirectories[0].parentId; // and begin creating the remote directory structure as required, prior to uploading the files. // We will create the first (intermediate) remote directory // e.g. if final path is: drive/special/approot/Backups/ABCDEFG/backup.tar // then the first intermediate remote directory is "Backups" // Once that is complete, we will request the folder metadata for drive/special/approot // and from that, parse the children array to get the remote ID of the "Backups" dir. // Then, we can create the next intermediate remote directory "ABCDEFG" etc. uploadData(accountId, accessToken, localPath, remotePath); } else if (syncDirection == Buteo::VALUE_FROM_REMOTE) { // download the required data. requestData(accountId, accessToken, localPath, remotePath, remoteFile); } else { qCWarning(lcSocialPlugin) << "invalid syncDirection specified to initialiseAppFolder request with OneDrive account:" << accountId << ":" << syncDirection; setStatus(SocialNetworkSyncAdaptor::Error); } } decrementSemaphore(accountId); } void OneDriveBackupOperationSyncAdaptor::getRemoteFolderMetadata(int accountId, const QString &accessToken, const QString &localPath, const QString &remotePath, const QString &parentId, const QString &remoteDirName) { // we request the parent folder metadata // e.g., let's say we have a final path like: drive/special/approot/Backups/ABCDEFG/backup.tar // this request will, when first called, be passed the remote id of the drive/special/approot bit // so we request the metadata for that (expanding children) QUrl url = QUrl(QStringLiteral("%1/%2/%3").arg(api(), QStringLiteral("drive/items"), parentId)); QUrlQuery query(url); QList > queryItems; queryItems.append(QPair(QStringLiteral("expand"), QStringLiteral("children"))); query.setQueryItems(queryItems); url.setQuery(query); QNetworkRequest req(url); req.setRawHeader(QString(QLatin1String("Authorization")).toUtf8(), QString(QLatin1String("Bearer ")).toUtf8() + accessToken.toUtf8()); QNetworkReply *reply = m_networkAccessManager->get(req); if (reply) { reply->setProperty("accountId", accountId); reply->setProperty("accessToken", accessToken); reply->setProperty("localPath", localPath); reply->setProperty("remotePath", remotePath); reply->setProperty("parentId", parentId); // the id of the parent folder containing the remote folder we're interested in reply->setProperty("remoteDirName", remoteDirName); // the name of the remote folder we're interested in connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(errorHandler(QNetworkReply::NetworkError))); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsHandler(QList))); connect(reply, SIGNAL(finished()), this, SLOT(getRemoteFolderMetadataFinishedHandler())); // we're requesting data. Increment the semaphore so that we know we're still busy. incrementSemaphore(accountId); setupReplyTimeout(accountId, reply, 10 * 60 * 1000); // 10 minutes } else { qCWarning(lcSocialPlugin) << "unable to perform remote folder metadata request for OneDrive account with id" << accountId; } } void OneDriveBackupOperationSyncAdaptor::getRemoteFolderMetadataFinishedHandler() { QNetworkReply *reply = qobject_cast(sender()); QByteArray data = reply->readAll(); int accountId = reply->property("accountId").toInt(); QString accessToken = reply->property("accessToken").toString(); QString localPath = reply->property("localPath").toString(); QString remotePath = reply->property("remotePath").toString(); QString parentId = reply->property("parentId").toString(); QString remoteDirName = reply->property("remoteDirName").toString(); bool isError = reply->property("isError").toBool(); reply->deleteLater(); removeReplyTimeout(accountId, reply); bool ok = false; QJsonObject parsed = parseJsonObjectReplyData(data, &ok); if (isError || !ok) { qCWarning(lcSocialPlugin) << "error occurred when performing remote folder metadata request with OneDrive account:" << accountId; debugDumpJsonResponse(data); setStatus(SocialNetworkSyncAdaptor::Error); } else { qCDebug(lcSocialPlugin) << "remote folder metadata request succeeded with OneDrive account:" << accountId; qCDebug(lcSocialPlugin) << "remote folder:" << parsed.value("name").toString() << "has remote ID:" << parsed.value("id").toString(); debugDumpJsonResponse(data); if (!parsed.contains("children")) { qCWarning(lcSocialPlugin) << "folder metadata request result had no children!"; setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(accountId); return; } // parse the response, and find the child folder which we're interested in. // once we've found it, store the remote id associated with that child folder in our folder metadata list. // then, trigger creation of the next child subfolder, now we know the id of that subfolder's parent folder. bool foundChildFolder = false; QJsonArray children = parsed.value("children").toArray(); Q_FOREACH (const QJsonValue &child, children) { const QJsonObject childObject = child.toObject(); const QString childName = childObject.value("name").toString(); const QString childId = childObject.value("id").toString(); const bool isDir = childObject.keys().contains("folder"); qCDebug(lcSocialPlugin) << "Looking for:" << remoteDirName << ", checking child object:" << childName << "with id:" << childId << ", isDir?" << isDir; if (isDir && childName.compare(remoteDirName, Qt::CaseInsensitive) == 0) { qCDebug(lcSocialPlugin) << "found folder:" << childName << "with remote id:" << childId << "for OneDrive account:" << accountId; foundChildFolder = true; bool updatedMetadata = false; for (int i = 0; i < m_remoteDirectories.size(); i++) { if (m_remoteDirectories[i].parentId == parentId && m_remoteDirectories[i].dirName.compare(remoteDirName, Qt::CaseInsensitive) == 0) { // found the directory whose metadata we should update m_remoteDirectories[i].remoteId = childId; m_remoteDirectories[i].dirName = childName; if ((i+1) < (m_remoteDirectories.size())) { // also, this directory will be the parent of the next directory // so set the parentId of the next directory to be this directory's id. m_remoteDirectories[i+1].parentId = childId; } updatedMetadata = true; break; } } if (!updatedMetadata) { qCWarning(lcSocialPlugin) << "could not find remote dir in directory metadata:" << remoteDirName; setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(accountId); return; } // we now know the remote id of this folder, so we can trigger creation of the next subfolder. uploadData(accountId, accessToken, localPath, remotePath); break; } } if (!foundChildFolder) { qCWarning(lcSocialPlugin) << "could not find remote dir in folder metadata response:" << remoteDirName; setStatus(SocialNetworkSyncAdaptor::Error); } } decrementSemaphore(accountId); } void OneDriveBackupOperationSyncAdaptor::requestData(int accountId, const QString &accessToken, const QString &localPath, const QString &remotePath, const QString &remoteFile, const QString &redirectUrl) { // step one: get the remote path and its children metadata. // step two: for each (non-folder) child in metadata, download it. QUrl url; if (accessToken.isEmpty()) { // content request to a temporary URL, since it doesn't require access token. url = QUrl(redirectUrl); } else { // directory or file info request. We use the path and sign with access token. if (remoteFile.isEmpty()) { // directory request. expand the children. url = QUrl(QStringLiteral("%1/%2:/%3:/").arg(api(), m_remoteAppDir, remotePath)); QUrlQuery query(url); QList > queryItems; queryItems.append(QPair(QStringLiteral("expand"), QStringLiteral("children"))); query.setQueryItems(queryItems); url.setQuery(query); qCDebug(lcSocialPlugin) << "performing directory request:" << url.toString(); } else { // file request, download its metadata. That will contain a content URL which we will redirect to. url = QUrl(QStringLiteral("%1/%2:/%3/%4").arg(api(), m_remoteAppDir, remotePath, remoteFile)); qCDebug(lcSocialPlugin) << "performing file request:" << url.toString(); } } QNetworkRequest req(url); req.setRawHeader(QString(QLatin1String("Authorization")).toUtf8(), QString(QLatin1String("Bearer ")).toUtf8() + accessToken.toUtf8()); QNetworkReply *reply = m_networkAccessManager->get(req); if (reply) { reply->setProperty("accountId", accountId); reply->setProperty("accessToken", accessToken); reply->setProperty("localPath", localPath); reply->setProperty("remotePath", remotePath); reply->setProperty("remoteFile", remoteFile); reply->setProperty("redirectUrl", redirectUrl); connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(errorHandler(QNetworkReply::NetworkError))); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsHandler(QList))); if (remoteFile.isEmpty()) { connect(reply, SIGNAL(finished()), this, SLOT(remotePathFinishedHandler())); } else { connect(reply, SIGNAL(downloadProgress(qint64,qint64)), this, SLOT(downloadProgressHandler(qint64,qint64))); connect(reply, SIGNAL(finished()), this, SLOT(remoteFileFinishedHandler())); } // we're requesting data. Increment the semaphore so that we know we're still busy. incrementSemaphore(accountId); setupReplyTimeout(accountId, reply, 10 * 60 * 1000); // 10 minutes } else { qCWarning(lcSocialPlugin) << "unable to create download request:" << remotePath << remoteFile << redirectUrl << "for OneDrive account with id" << accountId; } } void OneDriveBackupOperationSyncAdaptor::remotePathFinishedHandler() { QNetworkReply *reply = qobject_cast(sender()); QByteArray data = reply->readAll(); int accountId = reply->property("accountId").toInt(); QString accessToken = reply->property("accessToken").toString(); QString localPath = reply->property("localPath").toString(); QString remotePath = reply->property("remotePath").toString(); bool isError = reply->property("isError").toBool(); reply->deleteLater(); removeReplyTimeout(accountId, reply); if (isError) { qCWarning(lcSocialPlugin) << "error occurred when performing Backup remote path request for OneDrive account" << accountId << ":"; debugDumpJsonResponse(data); setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(accountId); return; } bool ok = false; QJsonObject parsed = parseJsonObjectReplyData(data, &ok); if (!ok || !parsed.contains("children")) { qCWarning(lcSocialPlugin) << "no backup data exists in reply from OneDrive with account" << accountId << ", got:"; debugDumpJsonResponse(data); setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(accountId); return; } QJsonArray children = parsed.value("children").toArray(); Q_FOREACH (const QJsonValue &child, children) { const QString childName = child.toObject().value("name").toString(); if (child.toObject().keys().contains("folder")) { qCDebug(lcSocialPlugin) << "ignoring folder:" << childName << "under remote backup path:" << remotePath << "for OneDrive account:" << accountId; } else { qCDebug(lcSocialPlugin) << "found remote backup object:" << childName << "for OneDrive account:" << accountId; requestData(accountId, accessToken, localPath, remotePath, childName); } } decrementSemaphore(accountId); } void OneDriveBackupOperationSyncAdaptor::remoteFileFinishedHandler() { QNetworkReply *reply = qobject_cast(sender()); QByteArray data = reply->readAll(); int accountId = reply->property("accountId").toInt(); QString localPath = reply->property("localPath").toString(); QString remotePath = reply->property("remotePath").toString(); QString remoteFile = reply->property("remoteFile").toString(); QString redirectUrl = reply->property("redirectUrl").toString(); bool isError = reply->property("isError").toBool(); QString remoteFileName = QStringLiteral("%1/%2").arg(remotePath).arg(remoteFile); reply->deleteLater(); removeReplyTimeout(accountId, reply); if (isError) { qCWarning(lcSocialPlugin) << "error occurred when performing Backup remote file request for OneDrive account" << accountId << ", got:"; debugDumpJsonResponse(data); setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(accountId); return; } // if it was a file metadata request, then parse the content url location and redirect to that. // otherwise it was a file content request and we should save the data. if (redirectUrl.isEmpty()) { // we expect to be redirected from the file path to a temporary url to GET content/data. // note: no access token is required to access the content redirect url. bool ok = false; QJsonObject parsed = parseJsonObjectReplyData(data, &ok); redirectUrl = parsed.value("@microsoft.graph.downloadUrl").toString(); if (!ok || redirectUrl.isEmpty()) { qCWarning(lcSocialPlugin) << "no content redirect url exists in file metadata for file:" << remoteFile; debugDumpJsonResponse(data); setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(accountId); return; } qCDebug(lcSocialPlugin) << "redirected from:" << remoteFileName << "to:" << redirectUrl; requestData(accountId, QString(), localPath, remotePath, remoteFile, redirectUrl); } else { if (data.isEmpty()) { qCInfo(lcSocialPlugin) << "remote file:" << remoteFileName << "is empty; ignoring"; } else { const QString filename = QStringLiteral("%1/%2").arg(localPath).arg(remoteFile); QFile file(filename); file.open(QIODevice::WriteOnly); // TODO: error checking file.write(data); file.close(); qCDebug(lcSocialPlugin) << "successfully wrote" << data.size() << "bytes to:" << filename << "from:" << remoteFileName; } } decrementSemaphore(accountId); } void OneDriveBackupOperationSyncAdaptor::uploadData(int accountId, const QString &accessToken, const QString &localPath, const QString &remotePath, const QString &localFile) { // step one: ensure the remote path exists (and if not, create it) // step two: upload every single file from the local path to the remote path. QNetworkReply *reply = 0; QString intermediatePath; if (localFile.isEmpty()) { // attempt to create the remote path directory. QString remoteDir; QString remoteParentId; QString remoteParentPath; for (int i = 0; i < m_remoteDirectories.size(); ++i) { if (m_remoteDirectories[i].created == false) { m_remoteDirectories[i].created = true; // we're creating this intermediate dir this time. remoteDir = m_remoteDirectories[i].dirName; remoteParentId = m_remoteDirectories[i].parentId; remoteParentPath = m_remoteDirectories[i].parentPath; break; } } if (remoteDir.isEmpty()) { qCWarning(lcSocialPlugin) << "No remote directory to create, but no file specified for upload - aborting"; return; } if (remoteParentId.isEmpty()) { qCWarning(lcSocialPlugin) << "No remote parent id known for directory:" << remoteDir << "- aborting"; return; } QString createFolderJson = QStringLiteral( "{" "\"name\": \"%1\"," "\"folder\": { }" "}").arg(remoteDir); QByteArray data = createFolderJson.toUtf8(); QUrl url = QUrl(QStringLiteral("%1/drive/items/%2/children").arg(api(), remoteParentId)); intermediatePath = QStringLiteral("%1/%2").arg(remoteParentPath).arg(remoteDir); QNetworkRequest request(url); request.setHeader(QNetworkRequest::ContentLengthHeader, data.size()); request.setHeader(QNetworkRequest::ContentTypeHeader, QVariant::fromValue(QString::fromLatin1("application/json"))); request.setRawHeader(QString(QLatin1String("Authorization")).toUtf8(), QString(QLatin1String("Bearer ")).toUtf8() + accessToken.toUtf8()); qCDebug(lcSocialPlugin) << "Attempting to create the remote directory:" << intermediatePath << "via request:" << url.toString(); qCDebug(lcSocialPlugin) << "with data:" << createFolderJson; reply = m_networkAccessManager->post(request, data); } else if (m_uploadSessionUrl.isEmpty()) { // Create an upload session const QString createUploadSessionJson = QStringLiteral( "{" "\"name\": \"%1\"" "}").arg(m_localFileInfo.fileName()); const QByteArray data = createUploadSessionJson.toUtf8(); const QUrl url = QUrl(QStringLiteral("%1/%2:/%3/%4:/createUploadSession").arg(api(), m_remoteAppDir, remotePath, localFile)); QNetworkRequest request(url); request.setHeader(QNetworkRequest::ContentLengthHeader, data.size()); request.setHeader(QNetworkRequest::ContentTypeHeader, QVariant::fromValue(QString::fromLatin1("application/json"))); request.setRawHeader(QString(QLatin1String("Authorization")).toUtf8(), QString(QLatin1String("Bearer ")).toUtf8() + accessToken.toUtf8()); qCDebug(lcSocialPlugin) << "Creating upload session for remote file:" << QStringLiteral("%1/%2").arg(remotePath).arg(localFile) << "via request:" << url.toString(); reply = m_networkAccessManager->post(request, data); } else { // attempt to create a remote file. QUrl url(m_uploadSessionUrl); if (!m_uploadFile) { m_uploadFile = new QFile(m_localFileInfo.filePath()); m_nextFileUploadPos = 0; if (!m_uploadFile->open(QIODevice::ReadOnly)){ qCWarning(lcSocialPlugin) << "unable to open local file:" << m_localFileInfo.filePath() << "for upload to OneDrive Backup with account:" << accountId; return; } } const qint64 readSize = qMin(UploadChunkSize, m_uploadFile->size() - m_nextFileUploadPos); m_uploadFile->seek(m_nextFileUploadPos); const QByteArray data(m_uploadFile->read(readSize)); const QString contentRange = QStringLiteral("bytes %1-%2/%3") // e.g. "bytes 0-25/128" .arg(m_nextFileUploadPos) .arg(m_nextFileUploadPos + data.size() - 1) // -1 because range is inclusive .arg(m_uploadFile->size()); QNetworkRequest req(url); req.setHeader(QNetworkRequest::ContentLengthHeader, data.size()); req.setRawHeader(QByteArrayLiteral("Content-Range"), contentRange.toLatin1()); req.setRawHeader(QByteArrayLiteral("Authorization"), QByteArrayLiteral("Bearer ") + accessToken.toUtf8()); qCDebug(lcSocialPlugin) << "Attempting to upload" << contentRange << "of file:" << m_localFileInfo.filePath() << "via request:" << url.toString(); reply = m_networkAccessManager->put(req, data); } if (reply) { reply->setProperty("accountId", accountId); reply->setProperty("accessToken", accessToken); reply->setProperty("localPath", localPath); reply->setProperty("remotePath", remotePath); reply->setProperty("intermediatePath", intermediatePath); reply->setProperty("localFile", localFile); connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(errorHandler(QNetworkReply::NetworkError))); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsHandler(QList))); if (localFile.isEmpty()) { connect(reply, &QNetworkReply::finished, this, &OneDriveBackupOperationSyncAdaptor::createRemotePathFinishedHandler); } else if (m_uploadSessionUrl.isEmpty()) { connect(reply, &QNetworkReply::finished, this, &OneDriveBackupOperationSyncAdaptor::createUploadSessionFinishedHandler); } else { connect(reply, &QNetworkReply::uploadProgress, this, &OneDriveBackupOperationSyncAdaptor::uploadProgressHandler); connect(reply, &QNetworkReply::finished, this, &OneDriveBackupOperationSyncAdaptor::filePartUploadFinishedHandler); } // we're requesting data. Increment the semaphore so that we know we're still busy. incrementSemaphore(accountId); setupReplyTimeout(accountId, reply, 10 * 60 * 1000); // 10 minutes } else { qCWarning(lcSocialPlugin) << "unable to create upload request:" << localPath << localFile << "->" << remotePath << "for OneDrive account with id" << accountId; } } void OneDriveBackupOperationSyncAdaptor::createRemotePathFinishedHandler() { QNetworkReply *reply = qobject_cast(sender()); QByteArray data = reply->readAll(); int accountId = reply->property("accountId").toInt(); QString accessToken = reply->property("accessToken").toString(); QString localPath = reply->property("localPath").toString(); QString remotePath = reply->property("remotePath").toString(); QString intermediatePath = reply->property("intermediatePath").toString(); bool isError = reply->property("isError").toBool(); int httpCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); reply->deleteLater(); removeReplyTimeout(accountId, reply); if (isError) { if (httpCode == 409) { // we actually expect a conflict error if the folder already existed, which is fine. qCDebug(lcSocialPlugin) << "remote path creation had conflict: already exists:" << intermediatePath << ". Continuing."; } else { // this must be a real error. qCWarning(lcSocialPlugin) << "remote path creation failed:" << httpCode; debugDumpJsonResponse(data); setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(accountId); return; } } // Check to see if we need to create any more intermediate directories QString createdDirectoryParentFolderId; QString createdDirectoryName; for (int i = 0; i < m_remoteDirectories.size(); ++i) { if (m_remoteDirectories[i].created) { // the last one of these will be the one for which this response was received. createdDirectoryParentFolderId = m_remoteDirectories[i].parentId; createdDirectoryName = m_remoteDirectories[i].dirName; } else { qCDebug(lcSocialPlugin) << "successfully created folder:" << createdDirectoryName << ", now performing parent request to get its remote id"; qCDebug(lcSocialPlugin) << "need to create another remote directory:" << m_remoteDirectories[i].dirName << "with parent:" << m_remoteDirectories[i].parentPath; // first, get the metadata for the most recently created path's parent, to get its remote ID. // after that's done, the next intermediate directory can be created. // NOTE: we do this (rather than attempting to parse the folder id from the response in this function) // because in the case where the remote path creation failed due to conflict, the response doesn't // contain the remote id. So, better to have uniform code to handle all cases. getRemoteFolderMetadata(accountId, accessToken, localPath, remotePath, createdDirectoryParentFolderId, createdDirectoryName); decrementSemaphore(accountId); return; } } // upload all files from the local path to the remote server. qCDebug(lcSocialPlugin) << "remote path now exists, attempting to upload local files"; QDir dir(localPath); QStringList localFiles = dir.entryList(QDir::Files); Q_FOREACH (const QString &localFile, localFiles) { qCDebug(lcSocialPlugin) << "uploading file:" << localFile << "from" << localPath << "to:" << remotePath; uploadData(accountId, accessToken, localPath, remotePath, localFile); } decrementSemaphore(accountId); } void OneDriveBackupOperationSyncAdaptor::createUploadSessionFinishedHandler() { QNetworkReply *reply = qobject_cast(sender()); const QByteArray data = reply->readAll(); const int accountId = reply->property("accountId").toInt(); const QString localPath = reply->property("localPath").toString(); const QString remotePath = reply->property("remotePath").toString(); const QString localFile = reply->property("localFile").toString(); const QString accessToken = reply->property("accessToken").toString(); const bool isError = reply->property("isError").toBool(); const int httpCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); reply->deleteLater(); removeReplyTimeout(accountId, reply); bool ok = false; const QJsonObject parsed = parseJsonObjectReplyData(data, &ok); const QString uploadSessionUrl = parsed.value("uploadUrl").toString(); if (isError || !ok || uploadSessionUrl.isEmpty()) { qCWarning(lcSocialPlugin) << "failed to read uploadUrl from createUploadSessionRequest for path" << remotePath << "to upload file" << localPath << localFile << "for OneDrive account:" << accountId << ", code:" << httpCode << "response:" << data; debugDumpJsonResponse(data); setStatus(SocialNetworkSyncAdaptor::Error); } else { m_uploadSessionUrl = uploadSessionUrl; qCDebug(lcSocialPlugin) << "successfully created upload session to upload to:" << m_uploadSessionUrl << localPath << localFile << "to:" << remotePath << "for OneDrive account:" << accountId; uploadData(accountId, accessToken, localPath, remotePath, localFile); } decrementSemaphore(accountId); } void OneDriveBackupOperationSyncAdaptor::filePartUploadFinishedHandler() { QNetworkReply *reply = qobject_cast(sender()); const QByteArray data = reply->readAll(); const int accountId = reply->property("accountId").toInt(); const QString localPath = reply->property("localPath").toString(); const QString remotePath = reply->property("remotePath").toString(); const QString localFile = reply->property("localFile").toString(); const QString accessToken = reply->property("accessToken").toString(); const int httpCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); reply->deleteLater(); removeReplyTimeout(accountId, reply); int nextRangeStart = 0; if (httpCode == 200 || httpCode == 201) { // OK or Created qCDebug(lcSocialPlugin) << "successfully uploaded backup of file:" << localPath << localFile << "to:" << remotePath << "for OneDrive account:" << accountId; } else if (httpCode == 202) { // Accepted bool ok = false; const QJsonObject parsed = parseJsonObjectReplyData(data, &ok); const QJsonArray nextExpectedRanges = parsed.value("nextExpectedRanges").toArray(); for (const QJsonValue &value : nextExpectedRanges) { const QString range = value.toString(); const int sepIndex = range.indexOf('-'); if (sepIndex > 0) { nextRangeStart = range.mid(0, sepIndex).toInt(); break; } } if (nextRangeStart == 0) { qCWarning(lcSocialPlugin) << "Cannot find nextExpectedRanges data to upload next part of" << m_localFileInfo.filePath() << "to" << remotePath << "for OneDrive account:" << accountId << ", code:" << httpCode << "response:" << data; debugDumpJsonResponse(data); setStatus(SocialNetworkSyncAdaptor::Error); } } else { qCWarning(lcSocialPlugin) << "failed to backup file:" << localPath << localFile << "to:" << remotePath << "for OneDrive account:" << accountId << ", code:" << httpCode << "response:" << data; debugDumpJsonResponse(data); setStatus(SocialNetworkSyncAdaptor::Error); } if (nextRangeStart > 0) { m_nextFileUploadPos = nextRangeStart; uploadData(accountId, accessToken, localPath, remotePath, localFile); } else { m_uploadFile->close(); delete m_uploadFile; m_uploadFile = nullptr; } decrementSemaphore(accountId); } void OneDriveBackupOperationSyncAdaptor::downloadProgressHandler(qint64 bytesReceived, qint64 bytesTotal) { QNetworkReply *reply = qobject_cast(sender()); int accountId = reply->property("accountId").toInt(); QString localPath = reply->property("localPath").toString(); QString remotePath = reply->property("remotePath").toString(); QString localFile = reply->property("localFile").toString(); qCDebug(lcSocialPlugin) << "Have download progress: bytesReceived:" << bytesReceived << "of" << bytesTotal << ", for" << localPath << localFile << "from" << remotePath << "with account:" << accountId; } void OneDriveBackupOperationSyncAdaptor::uploadProgressHandler(qint64 bytesSent, qint64 bytesTotal) { QNetworkReply *reply = qobject_cast(sender()); int accountId = reply->property("accountId").toInt(); QString localPath = reply->property("localPath").toString(); QString remotePath = reply->property("remotePath").toString(); QString localFile = reply->property("localFile").toString(); qCDebug(lcSocialPlugin) << "Have upload progress: bytesSent:" << bytesSent << "of" << bytesTotal << ", for" << localPath << localFile << "to" << remotePath << "with account:" << accountId; } void OneDriveBackupOperationSyncAdaptor::finalize(int accountId) { qCDebug(lcSocialPlugin) << "Finalize OneDrive backup sync for account" << accountId; if (operation() == Backup) { qCDebug(lcSocialPlugin) << "Deleting created backup file" << m_localFileInfo.absoluteFilePath(); QFile::remove(m_localFileInfo.absoluteFilePath()); QDir().rmdir(m_localFileInfo.absolutePath()); } } void OneDriveBackupOperationSyncAdaptor::purgeAccount(int) { // TODO: delete the contents of the localPath directory? probably not, could be shared between dropbox+onedrive } void OneDriveBackupOperationSyncAdaptor::finalCleanup() { // nothing to do? } buteo-sync-plugins-social-0.4.28/src/onedrive/onedrivebackupoperationsyncadaptor.h000066400000000000000000000106551474572147200306150ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2020 Open Mobile Platform LLC. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef ONEDRIVEBACKUPOPERATIONSYNCADAPTOR_H #define ONEDRIVEBACKUPOPERATIONSYNCADAPTOR_H #include "onedrivedatatypesyncadaptor.h" #include #include #include #include class OneDriveBackupOperationSyncAdaptor : public OneDriveDataTypeSyncAdaptor { Q_OBJECT public: enum Operation { Backup, BackupQuery, BackupRestore }; OneDriveBackupOperationSyncAdaptor(DataType dataType, QObject *parent); ~OneDriveBackupOperationSyncAdaptor(); QString syncServiceName() const override; void sync(const QString &dataTypeString, int accountId) override; virtual OneDriveBackupOperationSyncAdaptor::Operation operation() const = 0; protected: // implementing OneDriveDataTypeSyncAdaptor interface void purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode mode); void beginSync(int accountId, const QString &accessToken); void finalize(int accountId); void finalCleanup(); private: void initialiseAppFolderRequest(int accountId, const QString &accessToken, const QString &localPath, const QString &remotePath, const QString &remoteFile, const QString &syncDirection); void getRemoteFolderMetadata(int accountId, const QString &accessToken, const QString &localPath, const QString &remotePath, const QString &parentId, const QString &remoteDirName); void requestData(int accountId, const QString &accessToken, const QString &localPath, const QString &remotePath, const QString &remoteFile = QString(), const QString &redirectUrl = QString()); void uploadData(int accountId, const QString &accessToken, const QString &localPath, const QString &remotePath, const QString &localFile = QString()); void purgeAccount(int accountId); private Q_SLOTS: void cloudBackupStatusChanged(int accountId, const QString &status); void cloudBackupError(int accountId, const QString &error, const QString &errorString); void cloudRestoreStatusChanged(int accountId, const QString &status); void cloudRestoreError(int accountId, const QString &error, const QString &errorString); void initialiseAppFolderFinishedHandler(); void getRemoteFolderMetadataFinishedHandler(); void remotePathFinishedHandler(); void remoteFileFinishedHandler(); void createRemotePathFinishedHandler(); void filePartUploadFinishedHandler(); void createUploadSessionFinishedHandler(); void downloadProgressHandler(qint64 bytesReceived, qint64 bytesTotal); void uploadProgressHandler(qint64 bytesSent, qint64 bytesTotal); private: void beginListOperation(int accountId, const QString &accessToken, const QString &remoteDirPath); void beginSyncOperation(int accountId, const QString &accessToken); void listOperationFinished(); QDBusInterface *m_sailfishBackup = nullptr; QString m_remoteAppDir; struct RemoteDirectory { QString dirName; QString remoteId; QString parentPath; QString parentId; bool created; }; QList m_remoteDirectories; QString m_accessToken; QString m_remoteDirPath; QString m_uploadSessionUrl; QFileInfo m_localFileInfo; QFile *m_uploadFile = nullptr; qint64 m_nextFileUploadPos = 0; int m_accountId = 0; }; #endif // ONEDRIVEBACKUPOPERATIONSYNCADAPTOR_H buteo-sync-plugins-social-0.4.28/src/onedrive/onedrivedatatypesyncadaptor.cpp000066400000000000000000000246011474572147200275710ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2015 - 2019 Jolla Ltd. ** Copyright (c) 2020 Open Mobile Platform LLC. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "onedrivedatatypesyncadaptor.h" #include "trace.h" #include #include #include #include #include //libsailfishkeyprovider #include // libaccounts-qt5 #include #include #include #include //libsignon-qt: SignOn::NoUserInteractionPolicy #include #include #include OneDriveDataTypeSyncAdaptor::OneDriveDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::DataType dataType, QObject *parent) : SocialNetworkSyncAdaptor("onedrive", dataType, 0, parent), m_triedLoading(false) { } OneDriveDataTypeSyncAdaptor::~OneDriveDataTypeSyncAdaptor() { } void OneDriveDataTypeSyncAdaptor::sync(const QString &dataTypeString, int accountId) { if (dataTypeString != SocialNetworkSyncAdaptor::dataTypeName(m_dataType)) { qCWarning(lcSocialPlugin) << "OneDrive" << SocialNetworkSyncAdaptor::dataTypeName(m_dataType) << "sync adaptor was asked to sync" << dataTypeString; setStatus(SocialNetworkSyncAdaptor::Error); return; } if (clientId().isEmpty()) { qCWarning(lcSocialPlugin) << "client id couldn't be retrieved for OneDrive account" << accountId; setStatus(SocialNetworkSyncAdaptor::Error); return; } setStatus(SocialNetworkSyncAdaptor::Busy); updateDataForAccount(accountId); qCDebug(lcSocialPlugin) << "successfully triggered sync with profile:" << m_accountSyncProfile->name(); } void OneDriveDataTypeSyncAdaptor::updateDataForAccount(int accountId) { Accounts::Account *account = Accounts::Account::fromId(m_accountManager, accountId, this); if (!account) { qCWarning(lcSocialPlugin) << "existing account with id" << accountId << "couldn't be retrieved"; setStatus(SocialNetworkSyncAdaptor::Error); return; } // will be decremented by either signOnError or signOnResponse. incrementSemaphore(accountId); signIn(account); } void OneDriveDataTypeSyncAdaptor::finalCleanup() { } void OneDriveDataTypeSyncAdaptor::errorHandler(QNetworkReply::NetworkError err) { // OneDrive sends error code 204 (HTTP code 401) for Unauthorized Error QNetworkReply *reply = qobject_cast(sender()); const int httpCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (err == QNetworkReply::AuthenticationRequiredError) { int httpCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); qCInfo(lcSocialPlugin) << "sociald:OneDrive: received:" << httpCode << "would normally set CredentialsNeedUpdate for account" << reply->property("accountId").toInt() << "but could be spurious"; } qCWarning(lcSocialPlugin) << SocialNetworkSyncAdaptor::dataTypeName(m_dataType) << "request with account" << sender()->property("accountId").toInt() << "experienced error:" << err << "HTTP code:" << httpCode << "data:" << reply->readAll(); // set "isError" on the reply so that adapters know to ignore the result in the finished() handler reply->setProperty("isError", QVariant::fromValue(true)); // Note: not all errors are "unrecoverable" errors, so we don't change the status here. } void OneDriveDataTypeSyncAdaptor::sslErrorsHandler(const QList &errs) { QString sslerrs; foreach (const QSslError &e, errs) { sslerrs += e.errorString() + "; "; } if (errs.size() > 0) { sslerrs.chop(2); } qCWarning(lcSocialPlugin) << SocialNetworkSyncAdaptor::dataTypeName(m_dataType) << "request with account" << sender()->property("accountId").toInt() << "experienced ssl errors:" << sslerrs; // set "isError" on the reply so that adapters know to ignore the result in the finished() handler sender()->setProperty("isError", QVariant::fromValue(true)); // Note: not all errors are "unrecoverable" errors, so we don't change the status here. } QString OneDriveDataTypeSyncAdaptor::api() const { return m_api; } QString OneDriveDataTypeSyncAdaptor::clientId() { if (!m_triedLoading) { loadClientId(); } return m_clientId; } void OneDriveDataTypeSyncAdaptor::loadClientId() { m_triedLoading = true; char *cClientId = NULL; int cSuccess = SailfishKeyProvider_storedKey("onedrive", "onedrive-sync", "client_id", &cClientId); if (cClientId == NULL) { return; } else if (cSuccess != 0) { free(cClientId); return; } m_clientId = QLatin1String(cClientId); free(cClientId); } void OneDriveDataTypeSyncAdaptor::setCredentialsNeedUpdate(Accounts::Account *account) { qWarning() << "sociald:OneDrive: setting CredentialsNeedUpdate to true for account:" << account->id(); Accounts::Service srv(m_accountManager->service(syncServiceName())); account->selectService(srv); account->setValue(QStringLiteral("CredentialsNeedUpdate"), QVariant::fromValue(true)); account->setValue(QStringLiteral("CredentialsNeedUpdateFrom"), QVariant::fromValue(QString::fromLatin1("sociald-onedrive"))); account->selectService(Accounts::Service()); account->syncAndBlock(); } void OneDriveDataTypeSyncAdaptor::signIn(Accounts::Account *account) { // Fetch consumer key from keyprovider int accountId = account->id(); if (!checkAccount(account) || clientId().isEmpty()) { decrementSemaphore(accountId); return; } // grab out a valid identity for the sync service. Accounts::Service srv(m_accountManager->service(syncServiceName())); account->selectService(srv); SignOn::Identity *identity = account->credentialsId() > 0 ? SignOn::Identity::existingIdentity(account->credentialsId()) : 0; if (!identity) { qCWarning(lcSocialPlugin) << "account" << accountId << "has no valid credentials; cannot sign in"; decrementSemaphore(accountId); return; } Accounts::AccountService accSrv(account, srv); QString method = accSrv.authData().method(); QString mechanism = accSrv.authData().mechanism(); SignOn::AuthSession *session = identity->createSession(method); if (!session) { qCWarning(lcSocialPlugin) << "could not create signon session for account" << accountId; identity->deleteLater(); decrementSemaphore(accountId); return; } QVariantMap signonSessionData = accSrv.authData().parameters(); signonSessionData.insert("ClientId", clientId()); signonSessionData.insert("UiPolicy", SignOn::NoUserInteractionPolicy); connect(session, SIGNAL(response(SignOn::SessionData)), this, SLOT(signOnResponse(SignOn::SessionData)), Qt::UniqueConnection); connect(session, SIGNAL(error(SignOn::Error)), this, SLOT(signOnError(SignOn::Error)), Qt::UniqueConnection); session->setProperty("account", QVariant::fromValue(account)); session->setProperty("identity", QVariant::fromValue(identity)); session->process(SignOn::SessionData(signonSessionData), mechanism); } void OneDriveDataTypeSyncAdaptor::signOnError(const SignOn::Error &error) { SignOn::AuthSession *session = qobject_cast(sender()); Accounts::Account *account = session->property("account").value(); SignOn::Identity *identity = session->property("identity").value(); int accountId = account->id(); qCWarning(lcSocialPlugin) << "credentials for account with id" << accountId << "couldn't be retrieved:" << error.type() << error.message(); // if the error is because credentials have expired, we // set the CredentialsNeedUpdate key. if (error.type() == SignOn::Error::UserInteraction) { setCredentialsNeedUpdate(account); } session->disconnect(this); identity->destroySession(session); identity->deleteLater(); account->deleteLater(); // if we couldn't sign in, we can't sync with this account. setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(accountId); } void OneDriveDataTypeSyncAdaptor::signOnResponse(const SignOn::SessionData &responseData) { QVariantMap data; foreach (const QString &key, responseData.propertyNames()) { data.insert(key, responseData.getProperty(key)); } QString accessToken; SignOn::AuthSession *session = qobject_cast(sender()); Accounts::Account *account = session->property("account").value(); SignOn::Identity *identity = session->property("identity").value(); int accountId = account->id(); if (data.contains(QLatin1String("AccessToken"))) { accessToken = data.value(QLatin1String("AccessToken")).toString(); } else { qCInfo(lcSocialPlugin) << "signon response for account with id" << accountId << "contained no access token"; } m_api = account->value(QStringLiteral("api/Host")).toString(); session->disconnect(this); identity->destroySession(session); identity->deleteLater(); account->deleteLater(); if (!accessToken.isEmpty()) { beginSync(accountId, accessToken); // call the derived-class sync entrypoint. } decrementSemaphore(accountId); } buteo-sync-plugins-social-0.4.28/src/onedrive/onedrivedatatypesyncadaptor.h000066400000000000000000000050241474572147200272340ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2015 - 2019 Jolla Ltd. ** Copyright (c) 2020 Open Mobile Platform LLC. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef ONEDRIVEDATATYPESYNCADAPTOR_H #define ONEDRIVEDATATYPESYNCADAPTOR_H #include "socialnetworksyncadaptor.h" #include #include #include #include #include namespace Accounts { class Account; } namespace SignOn { class Error; class SessionData; } /* Abstract interface for all of the data-specific sync adaptors which pull data from OneDrive's online services. */ class OneDriveDataTypeSyncAdaptor : public SocialNetworkSyncAdaptor { Q_OBJECT public: OneDriveDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::DataType dataType, QObject *parent); virtual ~OneDriveDataTypeSyncAdaptor(); virtual void sync(const QString &dataTypeString, int accountId); protected: QString api() const; QString clientId(); virtual void updateDataForAccount(int accountId); virtual void beginSync(int accountId, const QString &accessToken) = 0; virtual void finalCleanup(); protected Q_SLOTS: virtual void errorHandler(QNetworkReply::NetworkError err); virtual void sslErrorsHandler(const QList &errs); private Q_SLOTS: void signOnError(const SignOn::Error &error); void signOnResponse(const SignOn::SessionData &responseData); private: void loadClientId(); void setCredentialsNeedUpdate(Accounts::Account *account); void signIn(Accounts::Account *account); bool m_triedLoading; // Is true if we tried to load (even if we failed) QString m_clientId; QString m_api; }; #endif // ONEDRIVEDATATYPESYNCADAPTOR_H buteo-sync-plugins-social-0.4.28/src/sociald/000077500000000000000000000000001474572147200210465ustar00rootroot00000000000000buteo-sync-plugins-social-0.4.28/src/sociald/sociald.All.xml000066400000000000000000000011171474572147200237150ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/sociald/sociald.pro000066400000000000000000000007641474572147200232150ustar00rootroot00000000000000TARGET = sociald-client include($$PWD/../common.pri) HEADERS += socialdplugin.h SOURCES += socialdplugin.cpp sociald_sync_profile.path = /etc/buteo/profiles/sync sociald_sync_profile.files = $$PWD/sociald.All.xml sociald_client_plugin_xml.path = /etc/buteo/profiles/client sociald_client_plugin_xml.files = $$PWD/sociald.xml OTHER_FILES += \ sociald_sync_profile.files \ sociald_client_plugin_xml.files INSTALLS += \ target \ sociald_sync_profile \ sociald_client_plugin_xml buteo-sync-plugins-social-0.4.28/src/sociald/sociald.xml000066400000000000000000000001761474572147200232120ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/sociald/socialdplugin.cpp000066400000000000000000000104211474572147200244050ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2013 - 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "socialdplugin.h" #include "trace.h" #include #include #include #include #include #include #include SocialdPlugin::SocialdPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *callbackInterface) : ClientPlugin(pluginName, profile, callbackInterface) { } SocialdPlugin::~SocialdPlugin() { } bool SocialdPlugin::init() { // sociald plugin profiles are either sociald.All.xml or // of the form sociald...xml QString profile = getProfileName(); if (profile == QStringLiteral("sociald.All")) { m_dataType.clear(); m_serviceName.clear(); return true; } // specific datatype sync. QStringList servicePlusDataType = profile.split("."); if (servicePlusDataType.length() == 3 && servicePlusDataType.at(0) == QStringLiteral("sociald")) { m_serviceName = servicePlusDataType.at(1); m_dataType = servicePlusDataType.at(2); return true; } return false; } bool SocialdPlugin::uninit() { return true; } bool SocialdPlugin::startSync() { QStringList startSyncParams; if (!m_dataType.isEmpty() && !m_serviceName.isEmpty()) { // trigger sync of specific data type with all accounts. startSyncParams.append(QStringLiteral("%1.%2").arg(m_serviceName, m_dataType)); } else { // trigger sync of all known data types with all accounts. startSyncParams << "google.Calendars"; startSyncParams << "google.Contacts"; startSyncParams << "facebook.Calendars"; startSyncParams << "facebook.Contacts"; startSyncParams << "facebook.Images"; startSyncParams << "facebook.Notifications"; //startSyncParams << "facebook.Posts"; startSyncParams << "twitter.Notifications"; startSyncParams << "twitter.Posts"; startSyncParams << "vk.Posts"; startSyncParams << "vk.Notifications"; startSyncParams << "vk.Calendars"; startSyncParams << "vk.Contacts"; startSyncParams << "vk.Images"; } foreach (const QString ¶m, startSyncParams) { QDBusMessage message = QDBusMessage::createMethodCall( "com.meego.msyncd", "/synchronizer", "com.meego.msyncd", "startSync"); message.setArguments(QVariantList() << param); QDBusConnection::sessionBus().asyncCall(message); } // always "succeed" even though the actual sync may fail. updateResults(Buteo::SyncResults(QDateTime::currentDateTime(), Buteo::SyncResults::SYNC_RESULT_SUCCESS, Buteo::SyncResults::NO_ERROR)); return true; } void SocialdPlugin::abortSync(Sync::SyncStatus) { } bool SocialdPlugin::cleanUp() { return true; } Buteo::SyncResults SocialdPlugin::getSyncResults() const { return m_syncResults; } void SocialdPlugin::connectivityStateChanged(Sync::ConnectivityType, bool) { // TODO, see TransportTracker.cpp:149 // Sync::CONNECTIVITY_INTERNET, true|false // Kill all ongoing on false // "Free" single shot sync on wlan? } void SocialdPlugin::updateResults(const Buteo::SyncResults &results) { m_syncResults = results; m_syncResults.setScheduled(true); } buteo-sync-plugins-social-0.4.28/src/sociald/socialdplugin.h000066400000000000000000000050031474572147200240520ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2013 - 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef SOCIALDPLUGIN_H #define SOCIALDPLUGIN_H #include #include #include "buteosyncfw_p.h" /* This plugin implementation provides a simple way to trigger syncs of all datatypes for all accounts, as opposed to a sync for only a specific datatype of a specific account, via the following profile: sociald.All.xml It also allows triggering syncs of a specific data type for all accounts, via the following profiles: sociald.google.Calendars.xml sociald.google.Contacts.xml sociald.facebook.Calendars.xml sociald.facebook.Contacts.xml sociald.facebook.Images.xml sociald.facebook.Notifications.xml sociald.facebook.Posts.xml sociald.twitter.Notifications.xml sociald.twitter.Posts.xml Note that it does not extend SocialdButeoPlugin (from common.pri) as it uses a different mechanism. */ class Q_DECL_EXPORT SocialdPlugin : public Buteo::ClientPlugin { Q_OBJECT public: SocialdPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *cbInterface); ~SocialdPlugin(); bool init(); bool uninit(); bool startSync(); void abortSync(Sync::SyncStatus status = Sync::SYNC_ABORTED); Buteo::SyncResults getSyncResults() const; bool cleanUp(); public slots: void connectivityStateChanged(Sync::ConnectivityType type, bool state); private: void updateResults(const Buteo::SyncResults &results); Buteo::SyncResults m_syncResults; QString m_dataType; QString m_serviceName; }; #endif // SOCIALDPLUGIN_H buteo-sync-plugins-social-0.4.28/src/src.pro000066400000000000000000000011361474572147200207420ustar00rootroot00000000000000TEMPLATE = subdirs SUBDIRS = \ common \ sociald sociald.depends = common CONFIG(google): { SUBDIRS += google google.depends = common } CONFIG(facebook): { SUBDIRS += facebook facebook.depends = common } CONFIG(twitter): { SUBDIRS += twitter twitter.depends = common } CONFIG(onedrive): { SUBDIRS += onedrive onedrive.depends = common } CONFIG(dropbox): { SUBDIRS += dropbox dropbox.depends = common } CONFIG(vk): { SUBDIRS += vk vk.depends = common } CONFIG(knowncontacts): { SUBDIRS += knowncontacts knowncontacts.depends = common } buteo-sync-plugins-social-0.4.28/src/twitter/000077500000000000000000000000001474572147200211325ustar00rootroot00000000000000buteo-sync-plugins-social-0.4.28/src/twitter/twitter-common.pri000066400000000000000000000001631474572147200246360ustar00rootroot00000000000000INCLUDEPATH += $$PWD SOURCES += $$PWD/twitterdatatypesyncadaptor.cpp HEADERS += $$PWD/twitterdatatypesyncadaptor.h buteo-sync-plugins-social-0.4.28/src/twitter/twitter-notifications/000077500000000000000000000000001474572147200255035ustar00rootroot00000000000000buteo-sync-plugins-social-0.4.28/src/twitter/twitter-notifications/twitter-notifications.pri000066400000000000000000000002701474572147200325670ustar00rootroot00000000000000CONFIG += link_pkgconfig PKGCONFIG += nemonotifications-qt5 SOURCES += $$PWD/twitternotificationsyncadaptor.cpp HEADERS += $$PWD/twitternotificationsyncadaptor.h INCLUDEPATH += $$PWD buteo-sync-plugins-social-0.4.28/src/twitter/twitter-notifications/twitter-notifications.pro000066400000000000000000000036771474572147200326130ustar00rootroot00000000000000TARGET = twitter-notifications-client include($$PWD/../../common.pri) include($$PWD/../twitter-common.pri) include($$PWD/twitter-notifications.pri) twitter_notifications_sync_profile.path = /etc/buteo/profiles/sync twitter_notifications_sync_profile.files = $$PWD/twitter.Notifications.xml twitter_notifications_client_plugin_xml.path = /etc/buteo/profiles/client twitter_notifications_client_plugin_xml.files = $$PWD/twitter-notifications.xml HEADERS += twitternotificationsplugin.h SOURCES += twitternotificationsplugin.cpp OTHER_FILES += \ twitter_notifications_sync_profile.files \ twitter_notifications_client_plugin_xml.files # translations TWITTER_TS_FILE = $$OUT_PWD/lipstick-jolla-home-twitter-notif.ts TWITTER_EE_QM = $$OUT_PWD/lipstick-jolla-home-twitter-notif_eng_en.qm twitter_ts.commands += lupdate $$PWD -ts $$TWITTER_TS_FILE twitter_ts.CONFIG += no_check_exist twitter_ts.output = $$TWITTER_TS_FILE twitter_ts.input = $$PWD twitter_ts_install.files = $$TWITTER_TS_FILE twitter_ts_install.path = /usr/share/translations/source twitter_ts_install.CONFIG += no_check_exist # should add -markuntranslated "-" when proper translations are in place (or for testing) twitter_engineering_english.commands += lrelease -idbased $$TWITTER_TS_FILE -qm $$TWITTER_EE_QM twitter_engineering_english.CONFIG += no_check_exist twitter_engineering_english.depends = twitter_ts twitter_engineering_english.input = $$TWITTER_TS_FILE twitter_engineering_english.output = $$TWITTER_EE_QM twitter_engineering_english_install.path = /usr/share/translations twitter_engineering_english_install.files = $$TWITTER_EE_QM twitter_engineering_english_install.CONFIG += no_check_exist QMAKE_EXTRA_TARGETS += twitter_ts twitter_engineering_english PRE_TARGETDEPS += twitter_ts twitter_engineering_english INSTALLS += \ target \ twitter_notifications_sync_profile \ twitter_notifications_client_plugin_xml \ twitter_ts_install \ twitter_engineering_english_install buteo-sync-plugins-social-0.4.28/src/twitter/twitter-notifications/twitter-notifications.xml000066400000000000000000000002141474572147200325730ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/twitter/twitter-notifications/twitter.Notifications.xml000066400000000000000000000011471474572147200325420ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/twitter/twitter-notifications/twitternotificationsplugin.cpp000066400000000000000000000050651474572147200337300ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2014 - 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "twitternotificationsplugin.h" #include "twitternotificationsyncadaptor.h" #include "socialnetworksyncadaptor.h" #include #include TwitterNotificationsPlugin::TwitterNotificationsPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *callbackInterface) : SocialdButeoPlugin(pluginName, profile, callbackInterface, QStringLiteral("twitter"), SocialNetworkSyncAdaptor::dataTypeName(SocialNetworkSyncAdaptor::Notifications)) { QString translationPath("/usr/share/translations/"); // QTranslator life-cycle: owned by ButeoSocial and removed by its own destructor QTranslator *engineeringEnglish = new QTranslator(this); engineeringEnglish->load("lipstick-jolla-home-twitter-notif_eng_en", translationPath); QCoreApplication::instance()->installTranslator(engineeringEnglish); QTranslator *translator = new QTranslator(this); translator->load(QLocale(), "lipstick-jolla-home-twitter-notif", "-", translationPath); QCoreApplication::instance()->installTranslator(translator); } TwitterNotificationsPlugin::~TwitterNotificationsPlugin() { } SocialNetworkSyncAdaptor *TwitterNotificationsPlugin::createSocialNetworkSyncAdaptor() { return new TwitterNotificationSyncAdaptor(this); } Buteo::ClientPlugin* TwitterNotificationsPluginLoader::createClientPlugin( const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) { return new TwitterNotificationsPlugin(pluginName, profile, cbInterface); } buteo-sync-plugins-social-0.4.28/src/twitter/twitter-notifications/twitternotificationsplugin.h000066400000000000000000000036611474572147200333750ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2014 - 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef TWITTERNOTIFICATIONSPLUGIN_H #define TWITTERNOTIFICATIONSPLUGIN_H #include "socialdbuteoplugin.h" #include class Q_DECL_EXPORT TwitterNotificationsPlugin : public SocialdButeoPlugin { Q_OBJECT public: TwitterNotificationsPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *cbInterface); ~TwitterNotificationsPlugin(); protected: SocialNetworkSyncAdaptor *createSocialNetworkSyncAdaptor(); }; class TwitterNotificationsPluginLoader : public Buteo::SyncPluginLoader { Q_OBJECT Q_PLUGIN_METADATA(IID "org.sailfishos.plugins.sync.TwitterNotificationsPluginLoader") Q_INTERFACES(Buteo::SyncPluginLoader) public: Buteo::ClientPlugin* createClientPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) override; }; #endif // TWITTERNOTIFICATIONSPLUGIN_H twitternotificationsyncadaptor.cpp000066400000000000000000000744261474572147200345260ustar00rootroot00000000000000buteo-sync-plugins-social-0.4.28/src/twitter/twitter-notifications/**************************************************************************** ** ** Copyright (C) 2013-2015 Jolla Ltd. ** Contact: Chris Adams ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "twitternotificationsyncadaptor.h" #include "trace.h" #include #include #include //nemo-qml-plugins/notifications #include // currently, we integrate with the device notifications via nemo-qml-plugin-notification #define OPEN_BROWSER_ACTION(openUrlArgs) \ Notification::remoteAction( \ "default", \ "", \ "org.sailfishos.browser", \ "/", \ "org.sailfishos.browser", \ "openUrl", \ QVariantList() << openUrlArgs \ ) TwitterNotificationSyncAdaptor::TwitterNotificationSyncAdaptor(QObject *parent) : TwitterDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::Notifications, parent) , m_firstTimeSync(false) { setInitialActive(m_db.isValid()); } TwitterNotificationSyncAdaptor::~TwitterNotificationSyncAdaptor() { } QString TwitterNotificationSyncAdaptor::syncServiceName() const { return QStringLiteral("twitter-microblog"); } void TwitterNotificationSyncAdaptor::purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode) { Notification *notification = findNotification(oldId, Mention); if (notification) { notification->close(); notification->deleteLater(); } notification = findNotification(oldId, Retweet); if (notification) { notification->close(); notification->deleteLater(); } notification = findNotification(oldId, Follower); if (notification) { notification->close(); notification->deleteLater(); } m_db.setFollowerIds(oldId, QSet()); m_db.setRetweetedTweetCounts(oldId, QHash()); m_db.sync(); m_db.wait(); } void TwitterNotificationSyncAdaptor::beginSync(int accountId, const QString &oauthToken, const QString &oauthTokenSecret) { m_lastSyncTimestamp = lastSyncTimestamp(QLatin1String("twitter"), SocialNetworkSyncAdaptor::dataTypeName(SocialNetworkSyncAdaptor::Notifications), accountId); if (!m_lastSyncTimestamp.isValid()) { m_firstTimeSync = true; } qCDebug(lcSocialPlugin) << "last sync of Twitter notifications was at:" << m_lastSyncTimestamp.toString(Qt::ISODate); requestNotifications(accountId, oauthToken, oauthTokenSecret); } void TwitterNotificationSyncAdaptor::finalize(int accountId) { if (syncAborted()) { qCInfo(lcSocialPlugin) << "sync aborted, won't commit notification database changes for Twitter account:" << accountId; } else { m_db.sync(); m_db.wait(); } } void TwitterNotificationSyncAdaptor::requestNotifications(int accountId, const QString &oauthToken, const QString &oauthTokenSecret, const QString &sinceTweetId, const QString &followersCursor) { if (followersCursor.isEmpty()) { // request mentions QList > queryItems; queryItems.append(QPair(QString(QLatin1String("count")), QString(QLatin1String("50")))); if (!sinceTweetId.isEmpty()) { queryItems.append(QPair(QString(QLatin1String("since_id")), sinceTweetId)); } QString baseUrl(QLatin1String("https://api.twitter.com/1.1/statuses/mentions_timeline.json")); QUrl url(baseUrl); QUrlQuery query(url); query.setQueryItems(queryItems); url.setQuery(query); QNetworkRequest mentionsRequest(url); mentionsRequest.setRawHeader("Authorization", authorizationHeader( accountId, oauthToken, oauthTokenSecret, QLatin1String("GET"), baseUrl, queryItems).toLatin1()); QNetworkReply *mreply = m_networkAccessManager->get(mentionsRequest); if (mreply) { mreply->setProperty("accountId", accountId); mreply->setProperty("oauthToken", oauthToken); mreply->setProperty("oauthTokenSecret", oauthTokenSecret); connect(mreply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(errorHandler(QNetworkReply::NetworkError))); connect(mreply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsHandler(QList))); connect(mreply, SIGNAL(finished()), this, SLOT(finishedMentionsHandler())); // we're requesting data. Increment the semaphore so that we know we're still busy. incrementSemaphore(accountId); setupReplyTimeout(accountId, mreply); } else { qCWarning(lcSocialPlugin) << "unable to request mention timeline notifications from Twitter account with id" << accountId; } // request retweets queryItems.clear(); queryItems.append(QPair(QString(QLatin1String("count")), QString(QLatin1String("40")))); queryItems.append(QPair(QString(QLatin1String("trim_user")), QString(QLatin1String("false")))); queryItems.append(QPair(QString(QLatin1String("include_entities")), QString(QLatin1String("false")))); if (!sinceTweetId.isEmpty()) { queryItems.append(QPair(QString(QLatin1String("since_id")), sinceTweetId)); } baseUrl = QString(QLatin1String("https://api.twitter.com/1.1/statuses/retweets_of_me.json")); url = QUrl(baseUrl); QUrlQuery retweetsQuery(url); retweetsQuery.setQueryItems(queryItems); url.setQuery(retweetsQuery); QNetworkRequest retweetsRequest(url); retweetsRequest.setRawHeader("Authorization", authorizationHeader( accountId, oauthToken, oauthTokenSecret, QLatin1String("GET"), baseUrl, queryItems).toLatin1()); QNetworkReply *rreply = m_networkAccessManager->get(retweetsRequest); if (rreply) { rreply->setProperty("accountId", accountId); rreply->setProperty("oauthToken", oauthToken); rreply->setProperty("oauthTokenSecret", oauthTokenSecret); connect(rreply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(errorHandler(QNetworkReply::NetworkError))); connect(rreply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsHandler(QList))); connect(rreply, SIGNAL(finished()), this, SLOT(finishedRetweetsHandler())); // we're requesting data. Increment the semaphore so that we know we're still busy. incrementSemaphore(accountId); setupReplyTimeout(accountId, rreply); } else { qCWarning(lcSocialPlugin) << "unable to request retweet notifications from Twitter account with id" << accountId; } } // request followers QList > queryItems; queryItems.append(QPair(QString(QLatin1String("count")), QString(QLatin1String("5000")))); queryItems.append(QPair(QString(QLatin1String("stringify_ids")), QString(QLatin1String("true")))); if (!followersCursor.isEmpty()) { queryItems.append(QPair(QString(QLatin1String("cursor")), followersCursor)); } QString baseUrl(QLatin1String("https://api.twitter.com/1.1/followers/ids.json")); QUrl url(baseUrl); QUrlQuery followersQuery(url); followersQuery.setQueryItems(queryItems); url.setQuery(followersQuery); QNetworkRequest followersRequest(url); followersRequest.setRawHeader("Authorization", authorizationHeader( accountId, oauthToken, oauthTokenSecret, QLatin1String("GET"), baseUrl, queryItems).toLatin1()); QNetworkReply *freply = m_networkAccessManager->get(followersRequest); if (freply) { freply->setProperty("accountId", accountId); freply->setProperty("oauthToken", oauthToken); freply->setProperty("oauthTokenSecret", oauthTokenSecret); connect(freply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(errorHandler(QNetworkReply::NetworkError))); connect(freply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsHandler(QList))); connect(freply, SIGNAL(finished()), this, SLOT(finishedFollowersHandler())); // we're requesting data. Increment the semaphore so that we know we're still busy. incrementSemaphore(accountId); setupReplyTimeout(accountId, freply); } else { qCWarning(lcSocialPlugin) << "unable to request followers from Twitter account with id" << accountId; } } void TwitterNotificationSyncAdaptor::finishedMentionsHandler() { QNetworkReply *reply = qobject_cast(sender()); int accountId = reply->property("accountId").toInt(); QByteArray replyData = reply->readAll(); disconnect(reply); reply->deleteLater(); removeReplyTimeout(accountId, reply); if (syncAborted()) { qCInfo(lcSocialPlugin) << "sync aborted, ignoring request response"; decrementSemaphore(accountId); return; } bool ok = false; QJsonArray tweets = parseJsonArrayReplyData(replyData, &ok); if (ok) { if (!tweets.size()) { qCDebug(lcSocialPlugin) << "no mentions received for account" << accountId; decrementSemaphore(accountId); return; } int mentionsCount = 0; QString body; QString summary; QDateTime timestamp; QString link; foreach (const QJsonValue &tweetValue, tweets) { QJsonObject tweet = tweetValue.toObject(); QDateTime createdTime = parseTwitterDateTime(tweet.value(QLatin1String("created_at")).toString()); QString mentionId = tweet.value(QLatin1String("id_str")).toString(); QString text = tweet.value(QLatin1String("text")).toString(); QJsonObject user = tweet.value(QLatin1String("user")).toObject(); QString userName = user.value(QLatin1String("name")).toString(); QString userScreenName = user.value(QLatin1String("screen_name")).toString(); // check to see if we need to post it to the notifications feed int sinceSpan = m_accountSyncProfile ? m_accountSyncProfile->key(Buteo::KEY_SYNC_SINCE_DAYS_PAST, QStringLiteral("7")).toInt() : 7; if (!createdTime.isValid()) { qCInfo(lcSocialPlugin) << "ignoring Twitter mention due to invalid createdTime parsed from:" << tweet.value(QLatin1String("created_at")).toString(); } else if (m_lastSyncTimestamp.isValid() && createdTime < m_lastSyncTimestamp) { qCDebug(lcSocialPlugin) << "mention notification for account" << accountId << "is older than last sync:" << createdTime << ":" << text; break; // all subsequent notifications will be even older. } else if (qAbs(createdTime.daysTo(QDateTime::currentDateTimeUtc())) > sinceSpan) { qCDebug(lcSocialPlugin) << "mention for account" << accountId << "is more than" << sinceSpan << "days old:" << createdTime << ":" << text; } else { summary = userName; //: Label telling the user that someone mentioned them. e.g: "John Smith" + "mentioned you in a Tweet" //% "mentioned you in a Tweet" body = qtTrId("qtn_social_notifications_twitter_mentioned_you"); timestamp = createdTime; link = QLatin1String("https://mobile.twitter.com/") + userScreenName + QLatin1String("/status/") + mentionId; mentionsCount+=1; } } // if this is the first sync, don't post any notifications (we don't know which are "new" or not) if (!m_firstTimeSync && mentionsCount > 0) { // Search if we already have a notification Notification *notification = createNotification(accountId, Mention); // Set properties of the notification notification->setItemCount(qMax(notification->itemCount(), 0) + mentionsCount); QStringList openUrlArgs; if (notification->itemCount() == 1) { notification->setTimestamp(timestamp); notification->setSummary(summary); notification->setBody(body); openUrlArgs << link; } else { notification->setTimestamp(QDateTime::currentDateTimeUtc()); //: This label refers to the user of the device, eg "You" + "received 5 new mentions" //% "You" notification->setSummary(qtTrId("qtn_social_notifications_twitter_you_received_new_mentions")); //: The number of tweets (n) by other people which mention this user. Include n. //% "received %n mentions" notification->setBody(qtTrId("qtn_social_notifications_twitter_n_mentions_include_n", notification->itemCount())); openUrlArgs << QLatin1String("https://mobile.twitter.com/i/connect"); } notification->setUrgency(Notification::Low); notification->setRemoteAction(OPEN_BROWSER_ACTION(openUrlArgs)); notification->publish(); if (notification->replacesId() == 0) { // failed. qCWarning(lcSocialPlugin) << "failed to publish mention notification:" << body; } } } else { // error occurred during request. qCWarning(lcSocialPlugin) << "unable to parse mention notification data from request with account" << accountId << "," << "got:" << QString::fromLatin1(replyData.constData()); } // we're finished this request. Decrement our busy semaphore. decrementSemaphore(accountId); } void TwitterNotificationSyncAdaptor::finishedRetweetsHandler() { QNetworkReply *reply = qobject_cast(sender()); int accountId = reply->property("accountId").toInt(); QByteArray replyData = reply->readAll(); disconnect(reply); reply->deleteLater(); removeReplyTimeout(accountId, reply); if (syncAborted()) { qCInfo(lcSocialPlugin) << "sync aborted, ignoring request response"; decrementSemaphore(accountId); return; } bool ok = false; QJsonArray tweets = parseJsonArrayReplyData(replyData, &ok); if (ok) { if (!tweets.size()) { qCDebug(lcSocialPlugin) << "no retweets received for account" << accountId; decrementSemaphore(accountId); return; } QString selfUserScreenName; QHash dbRetweetCounts = m_db.retweetedTweetCounts(accountId); QHash retweetCounts; QStringList newlyRetweetedTweets; int delta = 0; foreach (const QJsonValue &tweetValue, tweets) { QJsonObject tweet = tweetValue.toObject(); selfUserScreenName = tweet.value(QLatin1String("user")).toObject().value(QLatin1String("screen_name")).toString(); QString retweetId = tweet.value(QLatin1String("id_str")).toString(); int retweetsCount = tweet.value(QLatin1String("retweet_count")).toInt(); QDateTime createdTime = parseTwitterDateTime(tweet.value(QLatin1String("created_at")).toString()); // check to see if we need to post it to the notifications feed int sinceSpan = m_accountSyncProfile ? m_accountSyncProfile->key(Buteo::KEY_SYNC_SINCE_DAYS_PAST, QStringLiteral("7")).toInt() : 7; if (!createdTime.isValid() || qAbs(createdTime.daysTo(QDateTime::currentDateTimeUtc())) > sinceSpan) { qCDebug(lcSocialPlugin) << "retweet for account" << accountId << "is for tweet more than" << sinceSpan << "days old:" << createdTime << ", ignoring."; } else if (retweetsCount > 0) { retweetCounts.insert(retweetId, retweetsCount); if (!dbRetweetCounts.contains(retweetId) || dbRetweetCounts.value(retweetId) < retweetsCount) { delta += retweetsCount - dbRetweetCounts.value(retweetId); newlyRetweetedTweets.append(retweetId); } } } m_db.setRetweetedTweetCounts(accountId, retweetCounts); // won't get committed until finalize(); // if this is the first sync, don't post any notifications (we don't know which are "new" or not) if (!m_firstTimeSync && newlyRetweetedTweets.size() > 0) { // Search if we already have a notification Notification *notification = createNotification(accountId, Retweet); // Set properties of the notification QStringList openUrlArgs; notification->setItemCount(newlyRetweetedTweets.size()); notification->setTimestamp(QDateTime::currentDateTimeUtc()); if (newlyRetweetedTweets.size() == 1) { //: This label refers to a single Tweet by the user, eg: "Your Tweet" + "has been retweeted 5 times" //% "Your Tweet" notification->setSummary(qtTrId("qtn_social_notifications_twitter_your_tweet")); //: This label tells the user how many times (n) a single Tweet has been retweeted. Include n. //% "has been retweeted %n times" notification->setBody(qtTrId("qtn_social_notifications_twitter_1_n_retweets_include_n", delta)); openUrlArgs << QLatin1String("https://mobile.twitter.com/") + selfUserScreenName + QLatin1String("/status/") + newlyRetweetedTweets.first(); } else { //: This label refers to multiple Tweets by the user, eg: "Your Tweets" + "have been retweeted 5 times" //% "Your Tweets" notification->setSummary(qtTrId("qtn_social_notifications_twitter_your_tweets")); //: This label tells the user how many times (n) multiple Tweets have been retweeted. Include n. //% "have been retweeted %n times" notification->setBody(qtTrId("qtn_social_notifications_twitter_m_n_retweets_include_n", delta)); openUrlArgs << QLatin1String("https://mobile.twitter.com/i/connect"); } notification->setUrgency(Notification::Low); notification->setRemoteAction(OPEN_BROWSER_ACTION(openUrlArgs)); notification->publish(); if (notification->replacesId() == 0) { // failed. qCWarning(lcSocialPlugin) << "failed to publish retweet notification:" << newlyRetweetedTweets << delta; } } } else { // error occurred during request. qCWarning(lcSocialPlugin) << "unable to parse retweet notification data from request with account" << accountId << "," << "got:" << QString::fromLatin1(replyData.constData()); } // we're finished this request. Decrement our busy semaphore. decrementSemaphore(accountId); } void TwitterNotificationSyncAdaptor::finishedFollowersHandler() { QNetworkReply *reply = qobject_cast(sender()); int accountId = reply->property("accountId").toInt(); QString oauthToken = reply->property("oauthToken").toString(); QString oauthTokenSecret = reply->property("oauthTokenSecret").toString(); QByteArray replyData = reply->readAll(); disconnect(reply); reply->deleteLater(); removeReplyTimeout(accountId, reply); if (syncAborted()) { qCInfo(lcSocialPlugin) << "sync aborted, ignoring request response"; decrementSemaphore(accountId); return; } bool ok = false; QJsonObject response = parseJsonObjectReplyData(replyData, &ok); if (ok && response.contains("ids")) { QJsonArray ids = response.value("ids").toArray(); while (ids.size()) { m_followerIds.insert(ids.takeAt(0).toString()); } // if next_cursor exists, we have more followers we need to request. if (response.contains("next_cursor_str") && response.value("next_cursor_str").toString() != QStringLiteral("0")) { requestNotifications(accountId, oauthToken, oauthTokenSecret, QString(), response.value("next_cursor_str").toString()); } else { // finished requesting all followers. now calculate the delta to database data. QSet dbFollowerIds = m_db.followerIds(accountId); QSet differenceSet = m_followerIds; QList newFollowers = differenceSet.subtract(dbFollowerIds).toList(); bool needMultipleNotification = false; if (m_firstTimeSync || newFollowers.size() == 0) { // If this is the first sync, don't post any notifications (we don't know which are "new" or not). // Also, if we have no new followers, then no need to raise a notification. } else if (newFollowers.size() == 1) { // exactly one new follower. Possibly need to request detailed information. Notification *notification = findNotification(accountId, Follower); if (notification && notification->itemCount()) { // don't need to request detailed information, as we have multiple new followers // ie, exactly one since last sync + N from the sync before. needMultipleNotification = true; } else { // do need to request detailed information. QList > queryItems; queryItems.append(QPair(QString(QLatin1String("user_id")), newFollowers.first())); QString baseUrl(QLatin1String("https://api.twitter.com/1.1/users/show.json")); QUrl url(baseUrl); QUrlQuery query(url); query.setQueryItems(queryItems); url.setQuery(query); QNetworkRequest showRequest(url); showRequest.setRawHeader("Authorization", authorizationHeader( accountId, oauthToken, oauthTokenSecret, QLatin1String("GET"), baseUrl, queryItems).toLatin1()); QNetworkReply *sreply = m_networkAccessManager->get(showRequest); if (sreply) { sreply->setProperty("accountId", accountId); sreply->setProperty("oauthToken", oauthToken); sreply->setProperty("oauthTokenSecret", oauthTokenSecret); connect(sreply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(errorHandler(QNetworkReply::NetworkError))); connect(sreply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsHandler(QList))); connect(sreply, SIGNAL(finished()), this, SLOT(finishedUserShowHandler())); // we're requesting data. Increment the semaphore so that we know we're still busy. incrementSemaphore(accountId); setupReplyTimeout(accountId, sreply); } else { qCWarning(lcSocialPlugin) << "unable to request user information from Twitter account with id" << accountId; } } } else { // multiple new followers. needMultipleNotification = true; } if (needMultipleNotification) { Notification *notification = createNotification(accountId, Follower); notification->setItemCount(notification->itemCount() + newFollowers.size()); notification->setTimestamp(QDateTime::currentDateTimeUtc()); //: This label refers to the user of the device, eg "You" + "have 5 new followers" //% "You" notification->setSummary(qtTrId("qtn_social_notifications_twitter_you_have_new_followers")); //: The number of new followers (n) the user has on Twitter. Include n. e.g. "You" + "have 5 new followers". //% "have %n new followers" notification->setBody(qtTrId("qtn_social_notifications_n_followers_include_n", notification->itemCount())); notification->setUrgency(Notification::Low); QStringList openUrlArgs; openUrlArgs << QLatin1String("https://mobile.twitter.com/i/connect"); notification->setRemoteAction(OPEN_BROWSER_ACTION(openUrlArgs)); notification->publish(); if (notification->replacesId() == 0) { // failed. qCWarning(lcSocialPlugin) << "failed to publish followers notification:" << notification->itemCount(); } } // now update our database. Note that this doesn't get synced until finalize(). m_db.setFollowerIds(accountId, m_followerIds); } } else { // error occurred during request. qCWarning(lcSocialPlugin) << "unable to parse mention notification data from request with account" << accountId << "," << "got:" << QString::fromLatin1(replyData.constData()); } // we're finished this request. Decrement our busy semaphore. decrementSemaphore(accountId); } void TwitterNotificationSyncAdaptor::finishedUserShowHandler() { QNetworkReply *reply = qobject_cast(sender()); int accountId = reply->property("accountId").toInt(); QByteArray replyData = reply->readAll(); disconnect(reply); reply->deleteLater(); removeReplyTimeout(accountId, reply); if (syncAborted()) { qCInfo(lcSocialPlugin) << "sync aborted, ignoring request response"; decrementSemaphore(accountId); return; } bool ok = false; QJsonObject response = parseJsonObjectReplyData(replyData, &ok); if (ok && (response.contains("name") || response.contains("screen_name"))) { QString name = response.value("name").toString(); QString screenName = response.value("screen_name").toString(); Notification *notification = createNotification(accountId, Retweet); notification->setItemCount(1); notification->setTimestamp(QDateTime::currentDateTimeUtc()); notification->setSummary(name.isEmpty() ? screenName : name); //: Text telling the user that another user has followed them, e.g.: "John Smith" + "followed you". //% "followed you" notification->setBody(qtTrId("qtn_social_notifications_twitter_followed_you")); notification->setUrgency(Notification::Low); QStringList openUrlArgs; openUrlArgs << QLatin1String("https://mobile.twitter.com/i/connect"); notification->setRemoteAction(OPEN_BROWSER_ACTION(openUrlArgs)); notification->publish(); if (notification->replacesId() == 0) { // failed. qCWarning(lcSocialPlugin) << "failed to publish follower notification:" << name << screenName; } } else { qCWarning(lcSocialPlugin) << "unable to parse user information response:" << replyData; } // we're finished this request. Decrement our busy semaphore. decrementSemaphore(accountId); } Notification *TwitterNotificationSyncAdaptor::createNotification(int accountId, TwitterNotificationType ntype) { Notification *notification = findNotification(accountId, ntype); if (notification) { return notification; } notification = new Notification(this); //% "Twitter" notification->setAppName(qtTrId("qtn_social_notifications_twitter")); notification->setAppIcon("icon-lock-twitter"); notification->setHintValue("x-nemo.sociald.account-id", accountId); notification->setHintValue("x-nemo-feedback", "social"); if (ntype == TwitterNotificationSyncAdaptor::Mention) { notification->setCategory(QLatin1String("x-nemo.social.twitter.mention")); } else if (ntype == TwitterNotificationSyncAdaptor::Retweet) { notification->setCategory(QLatin1String("x-nemo.social.twitter.retweet")); } else { notification->setCategory(QLatin1String("x-nemo.social.twitter.follower")); } return notification; } Notification * TwitterNotificationSyncAdaptor::findNotification(int accountId, TwitterNotificationType ntype) { QString ntypeCategory; if (ntype == TwitterNotificationSyncAdaptor::Mention) { ntypeCategory = QLatin1String("x-nemo.social.twitter.mention"); } else if (ntype == TwitterNotificationSyncAdaptor::Retweet) { ntypeCategory = QLatin1String("x-nemo.social.twitter.retweet"); } else { ntypeCategory = QLatin1String("x-nemo.social.twitter.follower"); } Notification *notification = 0; QList notifications = Notification::notifications(); foreach (QObject *object, notifications) { Notification *castedNotification = static_cast(object); if (castedNotification->category() == ntypeCategory && castedNotification->hintValue("x-nemo.sociald.account-id").toInt() == accountId) { notification = castedNotification; break; } } if (notification) { notifications.removeAll(notification); } qDeleteAll(notifications); return notification; } buteo-sync-plugins-social-0.4.28/src/twitter/twitter-notifications/twitternotificationsyncadaptor.h000066400000000000000000000053451474572147200342440ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2013-2015 Jolla Ltd. ** Contact: Chris Adams ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef TWITTERNOTIFICATIONSYNCADAPTOR_H #define TWITTERNOTIFICATIONSYNCADAPTOR_H #include "twitterdatatypesyncadaptor.h" #include #include #include #include #include #include #include #include class Notification; class TwitterNotificationSyncAdaptor : public TwitterDataTypeSyncAdaptor { Q_OBJECT public: TwitterNotificationSyncAdaptor(QObject *parent); ~TwitterNotificationSyncAdaptor(); QString syncServiceName() const; protected: // implementing TwitterDataTypeSyncAdaptor interface void purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode mode); void beginSync(int accountId, const QString &oauthToken, const QString &oauthTokenSecret); void finalize(int accountId); private: void requestNotifications(int accountId, const QString &oauthToken, const QString &oauthTokenSecret, const QString &sinceTweetId = QString(), const QString &followersCursor = QString()); private Q_SLOTS: void finishedMentionsHandler(); void finishedRetweetsHandler(); void finishedFollowersHandler(); void finishedUserShowHandler(); private: enum TwitterNotificationType { Mention = 0, Retweet, Follower }; Notification * createNotification(int accountId, TwitterNotificationType ntype); Notification * findNotification(int accountId, TwitterNotificationType ntype); TwitterNotificationsDatabase m_db; QDateTime m_lastSyncTimestamp; QSet m_followerIds; bool m_firstTimeSync; }; #endif // TWITTERNOTIFICATIONSYNCADAPTOR_H buteo-sync-plugins-social-0.4.28/src/twitter/twitter-posts/000077500000000000000000000000001474572147200240025ustar00rootroot00000000000000buteo-sync-plugins-social-0.4.28/src/twitter/twitter-posts/twitter-posts.pri000066400000000000000000000002701474572147200273650ustar00rootroot00000000000000CONFIG += link_pkgconfig PKGCONFIG += nemonotifications-qt5 SOURCES += $$PWD/twitterhometimelinesyncadaptor.cpp HEADERS += $$PWD/twitterhometimelinesyncadaptor.h INCLUDEPATH += $$PWD buteo-sync-plugins-social-0.4.28/src/twitter/twitter-posts/twitter-posts.pro000066400000000000000000000012051474572147200273720ustar00rootroot00000000000000TARGET = twitter-posts-client include($$PWD/../../common.pri) include($$PWD/../twitter-common.pri) include($$PWD/twitter-posts.pri) twitter_posts_sync_profile.path = /etc/buteo/profiles/sync twitter_posts_sync_profile.files = $$PWD/twitter.Posts.xml twitter_posts_client_plugin_xml.path = /etc/buteo/profiles/client twitter_posts_client_plugin_xml.files = $$PWD/twitter-posts.xml HEADERS += twitterpostsplugin.h SOURCES += twitterpostsplugin.cpp OTHER_FILES += \ twitter_posts_sync_profile.files \ twitter_posts_client_plugin_xml.files INSTALLS += \ target \ twitter_posts_sync_profile \ twitter_posts_client_plugin_xml buteo-sync-plugins-social-0.4.28/src/twitter/twitter-posts/twitter-posts.xml000066400000000000000000000002041474572147200273700ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/twitter/twitter-posts/twitter.Posts.xml000066400000000000000000000011171474572147200273350ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/twitter/twitter-posts/twitterhometimelinesyncadaptor.cpp000066400000000000000000000313251474572147200330640ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2013-2014 Jolla Ltd. ** Contact: Chris Adams ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "twitterhometimelinesyncadaptor.h" #include "trace.h" #include #include #include TwitterHomeTimelineSyncAdaptor::TwitterHomeTimelineSyncAdaptor(QObject *parent) : TwitterDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::Posts, parent) { setInitialActive(m_db.isValid()); } TwitterHomeTimelineSyncAdaptor::~TwitterHomeTimelineSyncAdaptor() { } void TwitterHomeTimelineSyncAdaptor::purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode) { m_db.removePosts(oldId); m_db.commit(); m_db.wait(); // manage image cache. Social media feed UI caches feed images // and maintains bindings between source and cached image in SocialImageDatabase. // purge cached images belonging to this account. purgeCachedImages(&m_imageCacheDb, oldId); } QString TwitterHomeTimelineSyncAdaptor::syncServiceName() const { return QStringLiteral("twitter-microblog"); } void TwitterHomeTimelineSyncAdaptor::beginSync(int accountId, const QString &oauthToken, const QString &oauthTokenSecret) { requestMe(accountId, oauthToken, oauthTokenSecret); } void TwitterHomeTimelineSyncAdaptor::finalize(int accountId) { Q_UNUSED(accountId) if (syncAborted()) { qCInfo(lcSocialPlugin) << "sync aborted, won't commit database changes"; } else { m_db.commit(); m_db.wait(); // manage image cache. Social media feed UI caches feed images // and maintains bindings between source and cached image in SocialImageDatabase. // purge cached images older than four weeks. purgeExpiredImages(&m_imageCacheDb, accountId); } } void TwitterHomeTimelineSyncAdaptor::requestMe(int accountId, const QString &oauthToken, const QString &oauthTokenSecret) { QList > queryItems; queryItems.append(QPair(QString(QLatin1String("skip_status")), QString(QLatin1String("true")))); QString baseUrl = QLatin1String("https://api.twitter.com/1.1/account/verify_credentials.json"); QUrl url(baseUrl); QUrlQuery query(url); query.setQueryItems(queryItems); url.setQuery(query); QNetworkRequest nreq(url); nreq.setRawHeader("Authorization", authorizationHeader( accountId, oauthToken, oauthTokenSecret, QLatin1String("GET"), baseUrl, queryItems).toLatin1()); QNetworkReply *reply = m_networkAccessManager->get(nreq); if (reply) { reply->setProperty("accountId", accountId); reply->setProperty("oauthToken", oauthToken); reply->setProperty("oauthTokenSecret", oauthTokenSecret); connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(errorHandler(QNetworkReply::NetworkError))); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsHandler(QList))); connect(reply, SIGNAL(finished()), this, SLOT(finishedMeHandler())); // we're requesting data. Increment the semaphore so that we know we're still busy. incrementSemaphore(accountId); setupReplyTimeout(accountId, reply); } else { qCWarning(lcSocialPlugin) << "unable to request user verification from Twitter account with id" << accountId; } } void TwitterHomeTimelineSyncAdaptor::requestPosts(int accountId, const QString &oauthToken, const QString &oauthTokenSecret, const QString &sinceTweetId, const QString &fromUserId) { QList > queryItems; queryItems.append(QPair(QString(QLatin1String("count")), QString(QLatin1String("10")))); // limit to 10 Tweets. if (!sinceTweetId.isEmpty()) { queryItems.append(QPair(QString(QLatin1String("since_id")), sinceTweetId)); } if (!fromUserId.isEmpty()) { queryItems.append(QPair(QString(QLatin1String("user_id")), fromUserId)); } QString baseUrl = QLatin1String("https://api.twitter.com/1.1/statuses/home_timeline.json"); QUrl url(baseUrl); QUrlQuery query(url); query.setQueryItems(queryItems); url.setQuery(query); QNetworkRequest nreq(url); nreq.setRawHeader("Authorization", authorizationHeader( accountId, oauthToken, oauthTokenSecret, QLatin1String("GET"), baseUrl, queryItems).toLatin1()); QNetworkReply *reply = m_networkAccessManager->get(nreq); if (reply) { reply->setProperty("accountId", accountId); reply->setProperty("oauthToken", oauthToken); reply->setProperty("oauthTokenSecret", oauthTokenSecret); reply->setProperty("selfUserId", fromUserId); connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(errorHandler(QNetworkReply::NetworkError))); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsHandler(QList))); connect(reply, SIGNAL(finished()), this, SLOT(finishedPostsHandler())); // we're requesting data. Increment the semaphore so that we know we're still busy. incrementSemaphore(accountId); setupReplyTimeout(accountId, reply); } else { qCWarning(lcSocialPlugin) << "unable to request user timeline posts from Twitter account with id" << accountId; } } void TwitterHomeTimelineSyncAdaptor::finishedMeHandler() { QNetworkReply *reply = qobject_cast(sender()); int accountId = reply->property("accountId").toInt(); QString oauthToken = reply->property("oauthToken").toString(); QString oauthTokenSecret = reply->property("oauthTokenSecret").toString(); QByteArray replyData = reply->readAll(); disconnect(reply); reply->deleteLater(); removeReplyTimeout(accountId, reply); bool ok = false; QJsonObject parsed = parseJsonObjectReplyData(replyData, &ok); if (ok && parsed.contains(QLatin1String("id_str"))) { QString selfUserId = parsed.value(QLatin1String("id_str")).toString(); QString selfScreenName = parsed.value(QLatin1String("screen_name")).toString(); QString profileImage = parsed.value(QLatin1String("profile_image_url")).toString(); if (!m_selfTuids.contains(selfUserId)) { m_selfTuids.append(selfUserId); m_selfTScreenNames.insert(selfUserId, selfScreenName); m_accountProfileImage.insert(accountId, profileImage); } requestPosts(accountId, oauthToken, oauthTokenSecret, QString(), QString()); } else { qCWarning(lcSocialPlugin) << "unable to parse self user id from me request for account" << accountId << "," << "got:" << replyData; } decrementSemaphore(accountId); } void TwitterHomeTimelineSyncAdaptor::finishedPostsHandler() { QNetworkReply *reply = qobject_cast(sender()); int accountId = reply->property("accountId").toInt(); QDateTime lastSync = lastSyncTimestamp(QLatin1String("twitter"), SocialNetworkSyncAdaptor::dataTypeName(SocialNetworkSyncAdaptor::Posts), accountId); QByteArray replyData = reply->readAll(); disconnect(reply); reply->deleteLater(); removeReplyTimeout(accountId, reply); bool ok = false; QJsonArray tweets = parseJsonArrayReplyData(replyData, &ok); if (ok) { if (!tweets.size()) { qCDebug(lcSocialPlugin) << "no feed posts received for account" << accountId; decrementSemaphore(accountId); return; } m_db.removePosts(accountId); // purge old tweets. foreach (const QJsonValue &tweetValue, tweets) { // these are the fields we eventually need to fill out: QList > imageList; QString retweeter; // grab the data from the current post QJsonObject tweet = tweetValue.toObject(); // Just to be sure to get the time of the current (re)tweet QDateTime eventTimestamp = parseTwitterDateTime(tweet.value(QLatin1String("created_at")).toString()); // We should get data for the retweeted tweet instead of // getting the (often partial) retweeted tweet. if (tweet.contains(QLatin1String("retweeted_status"))) { retweeter = tweet.value(QLatin1String("user")).toObject().value("name").toString(); tweet = tweet.value(QLatin1String("retweeted_status")).toObject(); } QString postId = tweet.value(QLatin1String("id_str")).toString(); QString body = tweet.value(QLatin1String("text")).toString(); QJsonObject user = tweet.value(QLatin1String("user")).toObject(); QString name = user.value("name").toString(); QString screenName = user.value("screen_name").toString(); QString icon = user.value(QLatin1String("profile_image_url")).toString(); // Twitter does some HTML substitutions in their content // in JSON feeds, to prevent issues with JSONP formatting. body.replace(QStringLiteral("<"), QStringLiteral("<")); body.replace(QStringLiteral(">"), QStringLiteral(">")); body.replace(QStringLiteral("&"), QStringLiteral("&")); QJsonObject entities = tweet.value(QLatin1String("entities")).toObject(); QJsonArray mediaList = entities.value(QLatin1String("media")).toArray(); if (!mediaList.isEmpty()) { foreach (const QJsonValue &mediaValue, mediaList) { QJsonObject mediaObject = mediaValue.toObject(); if (mediaObject.contains(QLatin1String("media_url_https"))) { QString imageUrl = mediaObject.value(QLatin1String("media_url_https")).toString(); imageList.append(qMakePair(imageUrl, SocialPostImage::Photo)); } } } QJsonArray urlList = entities.value(QLatin1String("urls")).toArray(); foreach (const QJsonValue &urlValue, urlList) { // Right now, we use a slightly inefficient algorithm, that is error-proof // we just replace the old URL by the new one QJsonObject urlObject = urlValue.toObject(); QString shortUrl = urlObject.value(QLatin1String("url")).toString(); QString expandedUrl = urlObject.value(QLatin1String("expanded_url")).toString(); body.replace(shortUrl, expandedUrl); } // We always purge, so even if we've synced it in the past, we need it. // Check to see if we need to post it to the events feed int sinceSpan = m_accountSyncProfile ? m_accountSyncProfile->key(Buteo::KEY_SYNC_SINCE_DAYS_PAST, QStringLiteral("7")).toInt() : 7; if (eventTimestamp.daysTo(QDateTime::currentDateTime()) > sinceSpan) { qCDebug(lcSocialPlugin) << "tweet for account" << accountId << "is more than" << sinceSpan << "days old:" << eventTimestamp.toString(Qt::ISODate) << body; } else { m_db.addTwitterPost(postId, name, body, eventTimestamp, icon, imageList, screenName, retweeter, consumerKey(), consumerSecret(), accountId); } } } else { // error occurred during request. qCWarning(lcSocialPlugin) << "unable to parse event feed data from request with account" << accountId << "," << "got:" << QString::fromLatin1(replyData.constData()); } // we're finished this request. Decrement our busy semaphore. decrementSemaphore(accountId); } buteo-sync-plugins-social-0.4.28/src/twitter/twitter-posts/twitterhometimelinesyncadaptor.h000066400000000000000000000052771474572147200325400ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2013-2014 Jolla Ltd. ** Contact: Chris Adams ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef TWITTERHOMETIMELINESYNCADAPTOR_H #define TWITTERHOMETIMELINESYNCADAPTOR_H #include "twitterdatatypesyncadaptor.h" #include #include #include #include #include #include #include #include #include #include #include class TwitterHomeTimelineSyncAdaptor : public TwitterDataTypeSyncAdaptor { Q_OBJECT public: TwitterHomeTimelineSyncAdaptor(QObject *parent); ~TwitterHomeTimelineSyncAdaptor(); QString syncServiceName() const; protected: // implementing TwitterDataTypeSyncAdaptor interface void purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode mode); void beginSync(int accountId, const QString &oauthToken, const QString &oauthTokenSecret); void finalize(int accountId); private: void requestMe(int accountId, const QString &oauthToken, const QString &oauthTokenSecret); void requestPosts(int accountId, const QString &oauthToken, const QString &oauthTokenSecret, const QString &sinceId = QString(), const QString &fromUserId = QString()); bool fromIsSelfContact(const QString &fromName, const QString &fromTwUid) const; private Q_SLOTS: void finishedMeHandler(); void finishedPostsHandler(); private: TwitterPostsDatabase m_db; SocialImagesDatabase m_imageCacheDb; QMap m_accountProfileImage; QStringList m_selfTuids; // twitter user id strings of "me" objects QMap m_selfTScreenNames; // map of user id string to screen name }; #endif // TWITTERHOMETIMELINESYNCADAPTOR_H buteo-sync-plugins-social-0.4.28/src/twitter/twitter-posts/twitterpostsplugin.cpp000066400000000000000000000035651474572147200305310ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2014 - 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "twitterpostsplugin.h" #include "twitterhometimelinesyncadaptor.h" #include "socialnetworksyncadaptor.h" TwitterPostsPlugin::TwitterPostsPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *callbackInterface) : SocialdButeoPlugin(pluginName, profile, callbackInterface, QStringLiteral("twitter"), SocialNetworkSyncAdaptor::dataTypeName(SocialNetworkSyncAdaptor::Posts)) { } TwitterPostsPlugin::~TwitterPostsPlugin() { } SocialNetworkSyncAdaptor *TwitterPostsPlugin::createSocialNetworkSyncAdaptor() { return new TwitterHomeTimelineSyncAdaptor(this); } Buteo::ClientPlugin* TwitterPostsPluginLoader::createClientPlugin( const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) { return new TwitterPostsPlugin(pluginName, profile, cbInterface); } buteo-sync-plugins-social-0.4.28/src/twitter/twitter-posts/twitterpostsplugin.h000066400000000000000000000035621474572147200301730ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2014 - 2021 Jolla Ltd. ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef TWITTERPOSTSPLUGIN_H #define TWITTERPOSTSPLUGIN_H #include "socialdbuteoplugin.h" #include class Q_DECL_EXPORT TwitterPostsPlugin : public SocialdButeoPlugin { Q_OBJECT public: TwitterPostsPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *cbInterface); ~TwitterPostsPlugin(); protected: SocialNetworkSyncAdaptor *createSocialNetworkSyncAdaptor(); }; class TwitterPostsPluginLoader : public Buteo::SyncPluginLoader { Q_OBJECT Q_PLUGIN_METADATA(IID "org.sailfishos.plugins.sync.TwitterPostsPluginLoader") Q_INTERFACES(Buteo::SyncPluginLoader) public: Buteo::ClientPlugin* createClientPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) override; }; #endif // TWITTERPOSTSPLUGIN_H buteo-sync-plugins-social-0.4.28/src/twitter/twitter.pro000066400000000000000000000001311474572147200233510ustar00rootroot00000000000000TEMPLATE = subdirs SUBDIRS = \ $$PWD/twitter-notifications \ $$PWD/twitter-posts buteo-sync-plugins-social-0.4.28/src/twitter/twitterdatatypesyncadaptor.cpp000066400000000000000000000427131474572147200273530ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2013-2014 Jolla Ltd. ** Contact: Chris Adams ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "twitterdatatypesyncadaptor.h" #include "trace.h" #include #include #include #include #include #include #include #include #include #include //libsailfishkeyprovider #include // libaccounts-qt5 #include #include #include #include //libsignon-qt: SignOn::NoUserInteractionPolicy #include #include #include TwitterDataTypeSyncAdaptor::TwitterDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::DataType dataType, QObject *parent) : SocialNetworkSyncAdaptor("twitter", dataType, 0, parent), m_triedLoading(false) { } TwitterDataTypeSyncAdaptor::~TwitterDataTypeSyncAdaptor() { } void TwitterDataTypeSyncAdaptor::sync(const QString &dataTypeString, int accountId) { if (dataTypeString != SocialNetworkSyncAdaptor::dataTypeName(m_dataType)) { qCWarning(lcSocialPlugin) << "Twitter" << SocialNetworkSyncAdaptor::dataTypeName(m_dataType) << "sync adaptor was asked to sync" << dataTypeString; setStatus(SocialNetworkSyncAdaptor::Error); return; } if (consumerKey().isEmpty() || consumerSecret().isEmpty()) { qCWarning(lcSocialPlugin) << "secrets could not be retrieved for twitter account" << accountId; setStatus(SocialNetworkSyncAdaptor::Error); return; } setStatus(SocialNetworkSyncAdaptor::Busy); updateDataForAccount(accountId); qCDebug(lcSocialPlugin) << "successfully triggered sync with profile:" << m_accountSyncProfile->name(); } void TwitterDataTypeSyncAdaptor::updateDataForAccount(int accountId) { Accounts::Account *account = Accounts::Account::fromId(m_accountManager, accountId, this); if (!account) { qCWarning(lcSocialPlugin) << "existing account with id" << accountId << "couldn't be retrieved"; setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(accountId); return; } // will be decremented by either signOnError or signOnResponse. incrementSemaphore(accountId); signIn(account); } QString TwitterDataTypeSyncAdaptor::consumerKey() { if (!m_triedLoading) { loadConsumerKeyAndSecret(); } return m_consumerKey; } QString TwitterDataTypeSyncAdaptor::consumerSecret() { if (!m_triedLoading) { loadConsumerKeyAndSecret(); } return m_consumerSecret; } void TwitterDataTypeSyncAdaptor::errorHandler(QNetworkReply::NetworkError err) { QNetworkReply *reply = qobject_cast(sender()); QByteArray replyData = reply->readAll(); int accountId = reply->property("accountId").toInt(); qCWarning(lcSocialPlugin) << SocialNetworkSyncAdaptor::dataTypeName(m_dataType) << "request with account" << accountId << "experienced error:" << err << "HTTP:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); // set "isError" on the reply so that adapters know to ignore the result in the finished() handler reply->setProperty("isError", QVariant::fromValue(true)); // Note: not all errors are "unrecoverable" errors, so we don't change the status here. bool ok = false; QJsonObject parsed = parseJsonObjectReplyData(replyData, &ok); if (ok && parsed.contains(QLatin1String("errors"))) { QJsonArray dataList = parsed.value(QLatin1String("errors")).toArray(); // API v1.1 returns only one element in the array, but looks like these // are constantly updated: https://dev.twitter.com/docs/error-codes-responses foreach (QJsonValue data, dataList) { QJsonObject dataMap = data.toObject(); if (dataMap.value("code").toDouble() == 32 || dataMap.value("code").toDouble() == 89) { Accounts::Account *account = Accounts::Account::fromId(m_accountManager, accountId, this); if (account) { setCredentialsNeedUpdate(account); } } } } } void TwitterDataTypeSyncAdaptor::sslErrorsHandler(const QList &errs) { QString sslerrs; foreach (const QSslError &e, errs) { sslerrs += e.errorString() + "; "; } if (errs.size() > 0) { sslerrs.chop(2); } qCWarning(lcSocialPlugin) << SocialNetworkSyncAdaptor::dataTypeName(m_dataType) << "request with account" << sender()->property("accountId").toInt() << "experienced ssl errors:" << sslerrs; // set "isError" on the reply so that adapters know to ignore the result in the finished() handler sender()->setProperty("isError", QVariant::fromValue(true)); // Note: not all errors are "unrecoverable" errors, so we don't change the status here. } // This function taken from http://qt-project.org/wiki/HMAC-SHA1 which is in the public domain // and carries no licensing requirements (as at 2013-05-09) static QString hmacSha1(const QString &signingKey, const QString &baseString) { QByteArray key = signingKey.toLatin1(); QByteArray baseArray = baseString.toLatin1(); int blockSize = 64; // HMAC-SHA-1 block size, defined in SHA-1 standard if (key.length() > blockSize) { // if key is longer than block size (64), reduce key length with SHA-1 compression key = QCryptographicHash::hash(key, QCryptographicHash::Sha1); } QByteArray innerPadding(blockSize, char(0x36)); // initialize inner padding with char "6" QByteArray outerPadding(blockSize, char(0x5c)); // initialize outer padding with char "\" // ascii characters 0x36 ("6") and 0x5c ("\") are selected because they have large // Hamming distance (http://en.wikipedia.org/wiki/Hamming_distance) for (int i = 0; i < key.length(); i++) { innerPadding[i] = innerPadding[i] ^ key.at(i); // XOR operation between every byte in key and innerpadding, of key length outerPadding[i] = outerPadding[i] ^ key.at(i); // XOR operation between every byte in key and outerpadding, of key length } // result = hash ( outerPadding CONCAT hash ( innerPadding CONCAT baseArray ) ).toBase64 QByteArray total = outerPadding; QByteArray part = innerPadding; part.append(baseArray); total.append(QCryptographicHash::hash(part, QCryptographicHash::Sha1)); QByteArray hashed = QCryptographicHash::hash(total, QCryptographicHash::Sha1); return hashed.toBase64(); } QString TwitterDataTypeSyncAdaptor::authorizationHeader(int accountId, const QString &oauthToken, const QString &oauthTokenSecret, const QString &requestMethod, const QString &requestUrl, const QList > ¶meters) { Q_UNUSED(accountId); // Twitter requires all requests to be signed with an authorization header. QString key = consumerKey(); QString secret = consumerSecret(); if (key.isEmpty() || secret.isEmpty()) { return QString(); } QString oauthNonce = QString::fromLatin1(QUuid::createUuid().toByteArray().toBase64()); QString oauthSignature; QString oauthSigMethod = QLatin1String("HMAC-SHA1"); QString oauthTimestamp = QString::number(qFloor(QDateTime::currentMSecsSinceEpoch() / 1000.0)); //QString oauthToken; // already passed in as parameter. QString oauthVersion = QLatin1String("1.0"); // now build up the encoded parameters map. We use a map to perform alphabetical sorting. QMap encodedParams; encodedParams.insert(QUrl::toPercentEncoding(QLatin1String("oauth_consumer_key")), QUrl::toPercentEncoding(key)); encodedParams.insert(QUrl::toPercentEncoding(QLatin1String("oauth_nonce")), QUrl::toPercentEncoding(oauthNonce)); encodedParams.insert(QUrl::toPercentEncoding(QLatin1String("oauth_signature_method")), QUrl::toPercentEncoding(oauthSigMethod)); encodedParams.insert(QUrl::toPercentEncoding(QLatin1String("oauth_timestamp")), QUrl::toPercentEncoding(oauthTimestamp)); encodedParams.insert(QUrl::toPercentEncoding(QLatin1String("oauth_token")), QUrl::toPercentEncoding(oauthToken)); encodedParams.insert(QUrl::toPercentEncoding(QLatin1String("oauth_version")), QUrl::toPercentEncoding(oauthVersion)); for (int i = 0; i < parameters.size(); ++i) { QPair param = parameters.at(i); encodedParams.insert(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second)); } QString parametersString; QStringList keys = encodedParams.keys(); foreach (const QString &key, keys) { parametersString += key + QLatin1String("=") + encodedParams.value(key) + QLatin1String("&"); } parametersString.chop(1); QString signatureBaseString = requestMethod.toUpper() + QLatin1String("&") + QUrl::toPercentEncoding(requestUrl) + QLatin1String("&") + QUrl::toPercentEncoding(parametersString); QString signingKey = QUrl::toPercentEncoding(secret) + QLatin1String("&") + QUrl::toPercentEncoding(oauthTokenSecret); oauthSignature = hmacSha1(signingKey, signatureBaseString); encodedParams.insert(QUrl::toPercentEncoding(QLatin1String("oauth_signature")), QUrl::toPercentEncoding(oauthSignature)); // now generate the Authorization header from the encoded parameters map. // we need to remove the query items from the encoded parameters map first. QString authHeader = QLatin1String("OAuth "); for (int i = 0; i < parameters.size(); ++i) { QPair param = parameters.at(i); encodedParams.remove(QUrl::toPercentEncoding(param.first)); } keys = encodedParams.keys(); foreach (const QString &key, keys) { authHeader += key + QLatin1String("=\"") + encodedParams.value(key) + QLatin1String("\", "); } authHeader.chop(2); return authHeader; } QDateTime TwitterDataTypeSyncAdaptor::parseTwitterDateTime(const QString &tdt) { // Twitter use the following format ddd MMM dd hh:mm:ss +0000 yyyy // The +0000 should always be +0000 since it relates to UTC time // We are using it like that but it might break if Twitter change their // API or if +0000 is not constant. // Twitter use english in their date, so we need to use an english // locale to parse the date QLocale locale (QLocale::English, QLocale::UnitedStates); QDateTime time = locale.toDateTime(tdt, "ddd MMM dd HH:mm:ss +0000 yyyy"); time.setTimeSpec(Qt::UTC); return time; } void TwitterDataTypeSyncAdaptor::loadConsumerKeyAndSecret() { m_triedLoading = true; char *cConsumerKey = NULL; char *cConsumerSecret = NULL; int ckSuccess = SailfishKeyProvider_storedKey("twitter", "twitter-sync", "consumer_key", &cConsumerKey); int csSuccess = SailfishKeyProvider_storedKey("twitter", "twitter-sync", "consumer_secret", &cConsumerSecret); if (ckSuccess != 0 || cConsumerKey == NULL || csSuccess != 0 || cConsumerSecret == NULL) { qCInfo(lcSocialPlugin) << "No valid OAuth2 keys found"; return; } m_consumerKey = QLatin1String(cConsumerKey); m_consumerSecret = QLatin1String(cConsumerSecret); free(cConsumerKey); free(cConsumerSecret); } void TwitterDataTypeSyncAdaptor::setCredentialsNeedUpdate(Accounts::Account *account) { qWarning() << "sociald:Twitter: setting CredentialsNeedUpdate to true for account:" << account->id(); Accounts::Service srv(m_accountManager->service(syncServiceName())); account->selectService(srv); account->setValue(QStringLiteral("CredentialsNeedUpdate"), QVariant::fromValue(true)); account->setValue(QStringLiteral("CredentialsNeedUpdateFrom"), QVariant::fromValue(QString::fromLatin1("sociald-twitter"))); account->selectService(Accounts::Service()); account->syncAndBlock(); } void TwitterDataTypeSyncAdaptor::signIn(Accounts::Account *account) { // Fetch consumer key and secret from keyprovider QString key = consumerKey(); QString secret = consumerSecret(); int accountId = account->id(); if (!checkAccount(account) || key.isEmpty() || secret.isEmpty()) { decrementSemaphore(accountId); return; } // grab out a valid identity for the sync service. Accounts::Service srv(m_accountManager->service(syncServiceName())); account->selectService(srv); SignOn::Identity *identity = account->credentialsId() > 0 ? SignOn::Identity::existingIdentity(account->credentialsId()) : 0; if (!identity) { qCWarning(lcSocialPlugin) << "account" << accountId << "has no valid credentials, cannot sign in"; decrementSemaphore(accountId); return; } Accounts::AccountService accSrv(account, srv); QString method = accSrv.authData().method(); QString mechanism = accSrv.authData().mechanism(); SignOn::AuthSession *session = identity->createSession(method); if (!session) { qCWarning(lcSocialPlugin) << "could not create signon session for account" << accountId; identity->deleteLater(); decrementSemaphore(accountId); return; } QVariantMap signonSessionData = accSrv.authData().parameters(); signonSessionData.insert("ConsumerKey", key); signonSessionData.insert("ConsumerSecret", secret); signonSessionData.insert("UiPolicy", SignOn::NoUserInteractionPolicy); connect(session, SIGNAL(response(SignOn::SessionData)), this, SLOT(signOnResponse(SignOn::SessionData)), Qt::UniqueConnection); connect(session, SIGNAL(error(SignOn::Error)), this, SLOT(signOnError(SignOn::Error)), Qt::UniqueConnection); session->setProperty("account", QVariant::fromValue(account)); session->setProperty("identity", QVariant::fromValue(identity)); session->process(SignOn::SessionData(signonSessionData), mechanism); } void TwitterDataTypeSyncAdaptor::signOnError(const SignOn::Error &error) { SignOn::AuthSession *session = qobject_cast(sender()); Accounts::Account *account = session->property("account").value(); SignOn::Identity *identity = session->property("identity").value(); int accountId = account->id(); qCWarning(lcSocialPlugin) << "credentials for account with id" << accountId << "couldn't be retrieved:" << error.type() << "," << error.message(); // if the error is because credentials have expired, we // set the CredentialsNeedUpdate key. if (error.type() == SignOn::Error::UserInteraction) { setCredentialsNeedUpdate(account); } session->disconnect(this); identity->destroySession(session); identity->deleteLater(); account->deleteLater(); // if we couldn't sign in, we can't sync with this account. setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(accountId); } void TwitterDataTypeSyncAdaptor::signOnResponse(const SignOn::SessionData &responseData) { QVariantMap data; foreach (const QString &key, responseData.propertyNames()) { data.insert(key, responseData.getProperty(key)); } QString oauthToken; QString oauthTokenSecret; SignOn::AuthSession *session = qobject_cast(sender()); Accounts::Account *account = session->property("account").value(); SignOn::Identity *identity = session->property("identity").value(); int accountId = account->id(); if (data.contains(QLatin1String("AccessToken"))) { oauthToken = data.value(QLatin1String("AccessToken")).toString(); } else { qCInfo(lcSocialPlugin) << "signon response for account with id" << accountId << "contained no oauth token"; } if (data.contains(QLatin1String("TokenSecret"))) { oauthTokenSecret = data.value(QLatin1String("TokenSecret")).toString(); } else { qCInfo(lcSocialPlugin) << "signon response for account with id" << accountId << "contained no oauth token secret"; } session->disconnect(this); identity->destroySession(session); identity->deleteLater(); account->deleteLater(); if (!oauthToken.isEmpty() && !oauthTokenSecret.isEmpty()) { beginSync(accountId, oauthToken, oauthTokenSecret); // call the derived-class sync entrypoint. } decrementSemaphore(accountId); } buteo-sync-plugins-social-0.4.28/src/twitter/twitterdatatypesyncadaptor.h000066400000000000000000000054561474572147200270230ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2013-2014 Jolla Ltd. ** Contact: Chris Adams ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef TWITTERDATATYPESYNCADAPTOR_H #define TWITTERDATATYPESYNCADAPTOR_H #include "socialnetworksyncadaptor.h" #include #include #include #include namespace Accounts { class Account; } namespace SignOn { class Error; class SessionData; } /* Abstract interface for all of the data-specific sync adaptors which pull data from the Twitter social network. */ class TwitterDataTypeSyncAdaptor : public SocialNetworkSyncAdaptor { Q_OBJECT public: TwitterDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::DataType dataType, QObject *parent); virtual ~TwitterDataTypeSyncAdaptor(); virtual void sync(const QString &dataTypeString, int accountId); protected: static QDateTime parseTwitterDateTime(const QString &tdt); virtual QString authorizationHeader(int accountId, const QString &oauthToken, const QString &oauthTokenSecret, const QString &requestMethod, const QString &requestUrl, const QList > ¶meters); virtual void updateDataForAccount(int accountId); virtual void beginSync(int accountId, const QString &oauthToken, const QString &oauthTokenSecret) = 0; QString consumerKey(); QString consumerSecret(); protected Q_SLOTS: virtual void errorHandler(QNetworkReply::NetworkError err); virtual void sslErrorsHandler(const QList &errs); private Q_SLOTS: void signOnError(const SignOn::Error &error); void signOnResponse(const SignOn::SessionData &sessionData); private: void loadConsumerKeyAndSecret(); void setCredentialsNeedUpdate(Accounts::Account *account); void signIn(Accounts::Account *account); bool m_triedLoading; // Is true if we tried to load (even if we failed) QString m_consumerKey; QString m_consumerSecret; }; #endif // TWITTERDATATYPESYNCADAPTOR_H buteo-sync-plugins-social-0.4.28/src/vk/000077500000000000000000000000001474572147200200505ustar00rootroot00000000000000buteo-sync-plugins-social-0.4.28/src/vk/vk-calendars/000077500000000000000000000000001474572147200224225ustar00rootroot00000000000000buteo-sync-plugins-social-0.4.28/src/vk/vk-calendars/vk-calendars.pri000066400000000000000000000002551474572147200255120ustar00rootroot00000000000000CONFIG += link_pkgconfig PKGCONFIG += libmkcal-qt5 KF5CalendarCore SOURCES += $$PWD/vkcalendarsyncadaptor.cpp HEADERS += $$PWD/vkcalendarsyncadaptor.h INCLUDEPATH += $$PWD buteo-sync-plugins-social-0.4.28/src/vk/vk-calendars/vk-calendars.pro000066400000000000000000000011171474572147200255160ustar00rootroot00000000000000TARGET = vk-calendars-client include($$PWD/../../common.pri) include($$PWD/../vk-common.pri) include($$PWD/vk-calendars.pri) vk_calendars_sync_profile.path = /etc/buteo/profiles/sync vk_calendars_sync_profile.files = $$PWD/vk.Calendars.xml vk_calendars_client_plugin_xml.path = /etc/buteo/profiles/client vk_calendars_client_plugin_xml.files = $$PWD/vk-calendars.xml HEADERS += vkcalendarsplugin.h SOURCES += vkcalendarsplugin.cpp OTHER_FILES += \ vk.Calendars.xml \ vk-calendars.xml INSTALLS += \ target \ vk_calendars_sync_profile \ vk_calendars_client_plugin_xml buteo-sync-plugins-social-0.4.28/src/vk/vk-calendars/vk-calendars.xml000066400000000000000000000002031474572147200255110ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/vk/vk-calendars/vk.Calendars.xml000066400000000000000000000011251474572147200254560ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/vk/vk-calendars/vkcalendarsplugin.cpp000066400000000000000000000022211474572147200266370ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2014 - 2021 Jolla Ltd. ** ****************************************************************************/ #include "vkcalendarsplugin.h" #include "vkcalendarsyncadaptor.h" #include "socialnetworksyncadaptor.h" VKCalendarsPlugin::VKCalendarsPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *callbackInterface) : SocialdButeoPlugin(pluginName, profile, callbackInterface, QStringLiteral("vk"), SocialNetworkSyncAdaptor::dataTypeName(SocialNetworkSyncAdaptor::Calendars)) { } VKCalendarsPlugin::~VKCalendarsPlugin() { } SocialNetworkSyncAdaptor *VKCalendarsPlugin::createSocialNetworkSyncAdaptor() { return new VKCalendarSyncAdaptor(this); } Buteo::ClientPlugin* VKCalendarsPluginLoader::createClientPlugin( const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) { return new VKCalendarsPlugin(pluginName, profile, cbInterface); } buteo-sync-plugins-social-0.4.28/src/vk/vk-calendars/vkcalendarsplugin.h000066400000000000000000000022411474572147200263060ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2014 - 2021 Jolla Ltd. ** ****************************************************************************/ #ifndef VKCALENDARSPLUGIN_H #define VKCALENDARSPLUGIN_H #include "socialdbuteoplugin.h" #include class Q_DECL_EXPORT VKCalendarsPlugin : public SocialdButeoPlugin { Q_OBJECT public: VKCalendarsPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *cbInterface); ~VKCalendarsPlugin(); protected: SocialNetworkSyncAdaptor *createSocialNetworkSyncAdaptor(); }; class VKCalendarsPluginLoader : public Buteo::SyncPluginLoader { Q_OBJECT Q_PLUGIN_METADATA(IID "org.sailfishos.plugins.sync.VKCalendarsPluginLoader") Q_INTERFACES(Buteo::SyncPluginLoader) public: Buteo::ClientPlugin* createClientPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) override; }; #endif // VKCALENDARSPLUGIN_H buteo-sync-plugins-social-0.4.28/src/vk/vk-calendars/vkcalendarsyncadaptor.cpp000066400000000000000000000364171474572147200275230ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2014 Jolla Ltd. ** Contact: Chris Adams ** ****************************************************************************/ #include "vkcalendarsyncadaptor.h" #include "trace.h" #include #include #include #include #include #include #include #include #define SOCIALD_VK_NAME "VK" #define SOCIALD_VK_COLOR "#45668e" #define SOCIALD_VK_MAX_CALENDAR_ENTRY_RESULTS 100 namespace { bool eventNeedsUpdate(KCalendarCore::Event::Ptr event, const QJsonObject &json) { // TODO: compare data, determine if we need to update Q_UNUSED(event) Q_UNUSED(json) return true; } void jsonToKCal(const QString &vkId, const QJsonObject &json, KCalendarCore::Event::Ptr event, bool isUpdate) { qCDebug(lcSocialPlugin) << "Converting group event JSON to calendar event:" << json; if (!isUpdate) { QString eventUid = QUuid::createUuid().toString().mid(1); eventUid.chop(1); eventUid += QStringLiteral(":%1").arg(vkId); event->setUid(eventUid); } event->setSummary(json.value(QStringLiteral("name")).toString()); event->setDescription(json.value(QStringLiteral("description")).toString()); QString eventAddress = json.value(QStringLiteral("place")).toObject().value(QStringLiteral("address")).toString(); QString addressTitle = json.value(QStringLiteral("place")).toObject().value(QStringLiteral("title")).toString(); event->setLocation(eventAddress.isEmpty() ? addressTitle : eventAddress); if (json.contains(QStringLiteral("start_date"))) { uint startTime = json.value(QStringLiteral("start_date")).toDouble(); event->setDtStart(QDateTime::fromTime_t(startTime)); if (json.contains(QStringLiteral("end_date"))) { uint endTime = json.value(QStringLiteral("end_date")).toDouble(); event->setDtEnd(QDateTime::fromTime_t(endTime)); } } } } VKCalendarSyncAdaptor::VKCalendarSyncAdaptor(QObject *parent) : VKDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::Calendars, parent) , m_calendar(mKCal::ExtendedCalendar::Ptr(new mKCal::ExtendedCalendar(QTimeZone::utc()))) , m_storage(mKCal::ExtendedCalendar::defaultStorage(m_calendar)) , m_storageNeedsSave(false) { setInitialActive(true); } VKCalendarSyncAdaptor::~VKCalendarSyncAdaptor() { } QString VKCalendarSyncAdaptor::syncServiceName() const { return QStringLiteral("vk-calendars"); } void VKCalendarSyncAdaptor::sync(const QString &dataTypeString, int accountId) { m_storageNeedsSave = false; m_storage->open(); // we close it in finalCleanup() VKDataTypeSyncAdaptor::sync(dataTypeString, accountId); } void VKCalendarSyncAdaptor::finalize(int accountId) { if (syncAborted()) { qCDebug(lcSocialPlugin) << "sync aborted, skipping finalize of VK calendar events from account:" << accountId; } else { qCDebug(lcSocialPlugin) << "finalizing VK calendar sync with account:" << accountId; // convert the m_eventObjects to mkcal events, store in db or remove as required. bool foundVkNotebook = false; Q_FOREACH (mKCal::Notebook::Ptr notebook, m_storage->notebooks()) { if (notebook->pluginName() == QStringLiteral(SOCIALD_VK_NAME) && notebook->account() == QString::number(accountId)) { foundVkNotebook = true; m_vkNotebook = notebook; } } if (!foundVkNotebook) { m_vkNotebook = mKCal::Notebook::Ptr(new mKCal::Notebook); m_vkNotebook->setUid(QUuid::createUuid().toString()); m_vkNotebook->setName(QStringLiteral("VKontakte")); m_vkNotebook->setColor(QStringLiteral(SOCIALD_VK_COLOR)); m_vkNotebook->setPluginName(QStringLiteral(SOCIALD_VK_NAME)); m_vkNotebook->setAccount(QString::number(accountId)); m_vkNotebook->setIsReadOnly(true); m_storage->addNotebook(m_vkNotebook); } // We've found the notebook for this account. // Build up a map of existing events, then determine A/M/R delta. int addedCount = 0, modifiedCount = 0, removedCount = 0; m_storage->loadNotebookIncidences(m_vkNotebook->uid()); KCalendarCore::Incidence::List allIncidences; m_storage->allIncidences(&allIncidences, m_vkNotebook->uid()); QSet serverSideEventIds = m_eventObjects[accountId].keys().toSet(); Q_FOREACH (const KCalendarCore::Incidence::Ptr incidence, allIncidences) { KCalendarCore::Event::Ptr event = m_calendar->event(incidence->uid()); // when we add new events, we generate the uid like QUUID:vkId // to ensure that even after removal/re-add, the uid is unique. const QString &eventUid = event->uid(); int vkIdIdx = eventUid.indexOf(':') + 1; QString vkId = (vkIdIdx > 0 && eventUid.size() > vkIdIdx) ? eventUid.mid(eventUid.indexOf(':') + 1) : QString(); if (!m_eventObjects[accountId].contains(vkId)) { // this event was removed server-side since last sync. m_storageNeedsSave = true; m_calendar->deleteIncidence(event); removedCount += 1; qCDebug(lcSocialPluginTrace) << "deleted existing event:" << event->summary() << ":" << event->dtStart().toString(); } else { // this event was possibly modified server-side. const QJsonObject &eventObject(m_eventObjects[accountId][vkId]); if (eventNeedsUpdate(event, eventObject)) { event->startUpdates(); event->setReadOnly(false); jsonToKCal(vkId, eventObject, event, true); event->setReadOnly(true); event->endUpdates(); m_storageNeedsSave = true; modifiedCount += 1; qCDebug(lcSocialPluginTrace) << "modified existing event:" << event->summary() << ":" << event->dtStart().toString(); } else { qCDebug(lcSocialPluginTrace) << "no modificiation necessary for existing event:" << event->summary() << ":" << event->dtStart().toString(); } serverSideEventIds.remove(vkId); } } // if we have any left over, they're additions. Q_FOREACH (const QString &vkId, serverSideEventIds) { const QJsonObject &eventObject(m_eventObjects[accountId][vkId]); KCalendarCore::Event::Ptr event = KCalendarCore::Event::Ptr(new KCalendarCore::Event); jsonToKCal(vkId, eventObject, event, false); // direct conversion event->setReadOnly(true); if (!m_calendar->addEvent(event, m_vkNotebook->uid())) { qCDebug(lcSocialPluginTrace) << "failed to add new event:" << event->summary() << ":" << event->dtStart().toString() << "to notebook:" << m_vkNotebook->uid(); continue; } m_storageNeedsSave = true; addedCount += 1; qCDebug(lcSocialPluginTrace) << "added new event:" << event->summary() << ":" << event->dtStart().toString() << "to notebook:" << m_vkNotebook->uid(); } // finished! qCInfo(lcSocialPlugin) << "finished calendars sync with VK account" << accountId << ": got A/M/R:" << addedCount << "/" << modifiedCount << "/" << removedCount; } } void VKCalendarSyncAdaptor::finalCleanup() { // commit changes to db if (m_storageNeedsSave && !syncAborted()) { qCDebug(lcSocialPlugin) << "saving changes in VK calendar to storage"; m_storage->save(); } else { qCDebug(lcSocialPlugin) << "no changes to VK calendar - not saving storage"; } m_calendar->close(); m_storage->close(); } void VKCalendarSyncAdaptor::purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode mode) { // Delete the notebook and all events in it from the storage qCDebug(lcSocialPlugin) << "Purging calendar data for account:" << oldId; if (mode == SocialNetworkSyncAdaptor::CleanUpPurge) { // need to initialise the database m_storage->open(); } Q_FOREACH (mKCal::Notebook::Ptr notebook, m_storage->notebooks()) { if (notebook->pluginName() == QStringLiteral(SOCIALD_VK_NAME) && notebook->account() == QString::number(oldId)) { qCDebug(lcSocialPlugin) << "Purging notebook:" << notebook->uid() << "associated with account:" << oldId; m_storage->deleteNotebook(notebook); } } if (mode == SocialNetworkSyncAdaptor::CleanUpPurge) { // and commit any changes made. finalCleanup(); } } void VKCalendarSyncAdaptor::beginSync(int accountId, const QString &accessToken) { qCDebug(lcSocialPlugin) << "Beginning Calendar sync for VK, account:" << accountId; m_eventObjects[accountId].clear(); requestEvents(accountId, accessToken); } void VKCalendarSyncAdaptor::retryThrottledRequest(const QString &request, const QVariantList &args, bool retryLimitReached) { int accountId = args[0].toInt(); if (retryLimitReached) { qCWarning(lcSocialPlugin) << "hit request retry limit! unable to request data from VK account with id" << accountId; setStatus(SocialNetworkSyncAdaptor::Error); } else { qCDebug(lcSocialPlugin) << "retrying Calendars" << request << "request for VK account:" << accountId; requestEvents(accountId, args[1].toString(), args[2].toInt()); } decrementSemaphore(accountId); // finished waiting for the request. } void VKCalendarSyncAdaptor::requestEvents(int accountId, const QString &accessToken, int offset) { QUrlQuery urlQuery; QUrl requestUrl = QUrl(QStringLiteral("https://api.vk.com/method/groups.get")); urlQuery.addQueryItem("v", QStringLiteral("5.21")); // version urlQuery.addQueryItem("access_token", accessToken); if (offset >= 1) urlQuery.addQueryItem ("offset", QString::number(offset)); urlQuery.addQueryItem("count", QString::number(SOCIALD_VK_MAX_CALENDAR_ENTRY_RESULTS)); urlQuery.addQueryItem("extended", QStringLiteral("1")); urlQuery.addQueryItem("fields", QStringLiteral("start_date,end_date,place,description")); // theoretically, could use filter=events but this always returns zero results. requestUrl.setQuery(urlQuery); QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(requestUrl)); if (reply) { reply->setProperty("accountId", accountId); reply->setProperty("accessToken", accessToken); reply->setProperty("offset", offset); connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(errorHandler(QNetworkReply::NetworkError))); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsHandler(QList))); connect(reply, SIGNAL(finished()), this, SLOT(finishedHandler())); // we're requesting data. Increment the semaphore so that we know we're still busy. incrementSemaphore(accountId); setupReplyTimeout(accountId, reply); } else { // request was throttled by VKNetworkAccessManager QVariantList args; args << accountId << accessToken << offset; enqueueThrottledRequest(QStringLiteral("requestEvents"), args); // we are waiting to request data. Increment the semaphore so that we know we're still busy. incrementSemaphore(accountId); // decremented in retryThrottledRequest(). } } void VKCalendarSyncAdaptor::finishedHandler() { QNetworkReply *reply = qobject_cast(sender()); int accountId = reply->property("accountId").toInt(); QString accessToken = reply->property("accessToken").toString(); int offset = reply->property("offset").toInt(); QByteArray replyData = reply->readAll(); bool isError = reply->property("isError").toBool(); disconnect(reply); reply->deleteLater(); removeReplyTimeout(accountId, reply); bool ok = false; QJsonObject parsed = parseJsonObjectReplyData(replyData, &ok); if (!isError && ok) { // the zeroth index contains the count of response items QJsonArray items = parsed.value("response").toObject().value("items").toArray(); int count = parsed.value("response").toObject().value("count").toInt(); qCDebug(lcSocialPlugin) << "total communities returned in request with account" << accountId << ":" << count; bool needMorePages = false; if (count == 0 || count < SOCIALD_VK_MAX_CALENDAR_ENTRY_RESULTS) { // finished retrieving events. } else { needMorePages = true; } // parse the data in this page of results. for (int i = 1; i < items.size(); ++i) { QJsonObject currEvent = items.at(i).toObject(); if (currEvent.isEmpty() || currEvent.value("type").toString() != QStringLiteral("event")) { qCDebug(lcSocialPlugin) << "ignoring community:" << currEvent.value("name").toString() << "as it is not an event"; continue; } int gid = 0; if (currEvent.value("id").toDouble() > 0) { gid = currEvent.value("id").toInt(); } else if (currEvent.value("gid").toInt() > 0) { gid = currEvent.value("gid").toInt(); } if (gid > 0) { // we just cache them in memory here; we store them only if all // events are retrieved without sync being aborted / connection loss. QString gidstr = QString::number(gid); m_eventObjects[accountId].insert(gidstr, currEvent); qCDebug(lcSocialPlugin) << "Have found event with id:" << gid << ":" << currEvent.value("name").toString(); } else { qWarning() << "event has no id:" << currEvent; } } // if we need to request more data, do so. otherwise, parse all of the results into mkcal events. if (needMorePages) { qCDebug(lcSocialPlugin) << "need to fetch more pages of calendar results"; requestEvents(accountId, accessToken, offset + SOCIALD_VK_MAX_CALENDAR_ENTRY_RESULTS); } else { qCDebug(lcSocialPlugin) << "done fetching calendar results"; } } else { QVariantList args; args << accountId << accessToken << offset; if (enqueueServerThrottledRequestIfRequired(parsed, QStringLiteral("requestEvents"), args)) { // we hit the throttle limit, let throttle timer repeat the request // don't decrement semaphore yet as we're still waiting for it. // it will be decremented in retryThrottledRequest(). return; } // error occurred during request. qCWarning(lcSocialPlugin) << "unable to parse calendar data from request with account" << accountId << "; got:" << QString::fromUtf8(replyData); } // we're finished this request. Decrement our busy semaphore. decrementSemaphore(accountId); } buteo-sync-plugins-social-0.4.28/src/vk/vk-calendars/vkcalendarsyncadaptor.h000066400000000000000000000027241474572147200271620ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2014 Jolla Ltd. ** Contact: Chris Adams ** ****************************************************************************/ #ifndef VKCALENDARSYNCADAPTOR_H #define VKCALENDARSYNCADAPTOR_H #include "vkdatatypesyncadaptor.h" #include #include #include #include class VKCalendarSyncAdaptor : public VKDataTypeSyncAdaptor { Q_OBJECT public: VKCalendarSyncAdaptor(QObject *parent); ~VKCalendarSyncAdaptor(); QString syncServiceName() const; void sync(const QString &dataTypeString, int accountId = 0); protected: // implementing VKDataTypeSyncAdaptor interface void purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode mode); void beginSync(int accountId, const QString &accessToken); void finalize(int accountId); void finalCleanup(); void retryThrottledRequest(const QString &request, const QVariantList &args, bool retryLimitReached); private: void requestEvents(int accountId, const QString &accessToken, int offset = 0); private Q_SLOTS: void finishedHandler(); private: QMap > m_eventObjects; mKCal::ExtendedCalendar::Ptr m_calendar; mKCal::ExtendedStorage::Ptr m_storage; mKCal::Notebook::Ptr m_vkNotebook; bool m_storageNeedsSave; }; #endif // VKCALENDARSYNCADAPTOR_H buteo-sync-plugins-social-0.4.28/src/vk/vk-common.pri000066400000000000000000000002531474572147200224720ustar00rootroot00000000000000INCLUDEPATH += $$PWD SOURCES += $$PWD/vkdatatypesyncadaptor.cpp $$PWD/vknetworkaccessmanager.cpp HEADERS += $$PWD/vkdatatypesyncadaptor.h $$PWD/vknetworkaccessmanager_p.h buteo-sync-plugins-social-0.4.28/src/vk/vk-contacts/000077500000000000000000000000001474572147200223045ustar00rootroot00000000000000buteo-sync-plugins-social-0.4.28/src/vk/vk-contacts/vk-contacts.pri000066400000000000000000000004321474572147200252530ustar00rootroot00000000000000CONFIG += link_pkgconfig PKGCONFIG += Qt5Contacts qtcontacts-sqlite-qt5-extensions QT += contacts-private gui SOURCES += $$PWD/vkcontactsyncadaptor.cpp $$PWD/vkcontactimagedownloader.cpp HEADERS += $$PWD/vkcontactsyncadaptor.h $$PWD/vkcontactimagedownloader.h INCLUDEPATH += $$PWD buteo-sync-plugins-social-0.4.28/src/vk/vk-contacts/vk-contacts.pro000066400000000000000000000011361474572147200252630ustar00rootroot00000000000000TARGET = vk-contacts-client DEFINES += SOCIALD_USE_QTPIM include($$PWD/../../common.pri) include($$PWD/../vk-common.pri) include($$PWD/vk-contacts.pri) vk_contacts_sync_profile.path = /etc/buteo/profiles/sync vk_contacts_sync_profile.files = $$PWD/vk.Contacts.xml vk_contacts_client_plugin_xml.path = /etc/buteo/profiles/client vk_contacts_client_plugin_xml.files = $$PWD/vk-contacts.xml HEADERS += vkcontactsplugin.h SOURCES += vkcontactsplugin.cpp OTHER_FILES += \ vk.Contacts.xml \ vk-contacts.xml INSTALLS += \ target \ vk_contacts_sync_profile \ vk_contacts_client_plugin_xml buteo-sync-plugins-social-0.4.28/src/vk/vk-contacts/vk-contacts.xml000066400000000000000000000002021474572147200252540ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/vk/vk-contacts/vk.Contacts.xml000066400000000000000000000011211474572147200252160ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/vk/vk-contacts/vkcontactimagedownloader.cpp000066400000000000000000000025621474572147200300730ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2014 Jolla Ltd. ** Contact: Chris Adams ** ****************************************************************************/ #include "vkcontactimagedownloader.h" #include #include #include static const char *IMAGE_DOWNLOADER_IDENTIFIER_KEY = "identifier"; VKContactImageDownloader::VKContactImageDownloader() : AbstractImageDownloader() { } QString VKContactImageDownloader::staticOutputFile(const QString &identifier, const QUrl &url) { return makeUrlOutputFile(SocialSyncInterface::VK, SocialSyncInterface::Contacts, identifier, url.toString(), QString()); } QNetworkReply * VKContactImageDownloader::createReply(const QString &url, const QVariantMap &metadata) { Q_D(AbstractImageDownloader); Q_UNUSED(metadata) QNetworkRequest request(url); return d->networkAccessManager->get(request); } QString VKContactImageDownloader::outputFile(const QString &url, const QVariantMap &data, const QString &mimeType) const { Q_UNUSED(mimeType); // TODO: use return staticOutputFile(data.value(IMAGE_DOWNLOADER_IDENTIFIER_KEY).toString(), url); } buteo-sync-plugins-social-0.4.28/src/vk/vk-contacts/vkcontactimagedownloader.h000066400000000000000000000021211474572147200275270ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2014 Jolla Ltd. ** Contact: Chris Adams ** ****************************************************************************/ #ifndef VKCONTACTIMAGEDOWNLOADER_H #define VKCONTACTIMAGEDOWNLOADER_H #include #include #include #include #include #include class QNetworkReply; class VKContactImageDownloader: public AbstractImageDownloader { Q_OBJECT public: explicit VKContactImageDownloader(); static QString staticOutputFile(const QString &identifier, const QUrl &url); protected: QNetworkReply * createReply(const QString &url, const QVariantMap &metadata); // This is a reimplemented method, used by AbstractImageDownloader QString outputFile(const QString &url, const QVariantMap &data, const QString &mimeType) const override; private: Q_DECLARE_PRIVATE(AbstractImageDownloader) }; #endif // VKCONTACTIMAGEDOWNLOADER_H buteo-sync-plugins-social-0.4.28/src/vk/vk-contacts/vkcontactsplugin.cpp000066400000000000000000000024321474572147200264070ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2014 Jolla Ltd. ** Contact: Chris Adams ** ****************************************************************************/ #include "constants_p.h" #include #include #include "vkcontactsplugin.h" #include "vkcontactsyncadaptor.h" #include "socialnetworksyncadaptor.h" VKContactsPlugin::VKContactsPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *callbackInterface) : SocialdButeoPlugin(pluginName, profile, callbackInterface, QStringLiteral("vk"), SocialNetworkSyncAdaptor::dataTypeName(SocialNetworkSyncAdaptor::Contacts)) { } VKContactsPlugin::~VKContactsPlugin() { } SocialNetworkSyncAdaptor *VKContactsPlugin::createSocialNetworkSyncAdaptor() { return new VKContactSyncAdaptor(this); } Buteo::ClientPlugin* VKContactsPluginLoader::createClientPlugin( const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) { return new VKContactsPlugin(pluginName, profile, cbInterface); } buteo-sync-plugins-social-0.4.28/src/vk/vk-contacts/vkcontactsplugin.h000066400000000000000000000023021474572147200260500ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2014 Jolla Ltd. ** Contact: Chris Adams ** ****************************************************************************/ #ifndef VKCONTACTSPLUGIN_H #define VKCONTACTSPLUGIN_H #include "socialdbuteoplugin.h" #include class Q_DECL_EXPORT VKContactsPlugin : public SocialdButeoPlugin { Q_OBJECT public: VKContactsPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *cbInterface); ~VKContactsPlugin(); protected: SocialNetworkSyncAdaptor *createSocialNetworkSyncAdaptor(); }; class VKContactsPluginLoader : public Buteo::SyncPluginLoader { Q_OBJECT Q_PLUGIN_METADATA(IID "org.sailfishos.plugins.sync.VKContactsPluginLoader") Q_INTERFACES(Buteo::SyncPluginLoader) public: Buteo::ClientPlugin* createClientPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) override; }; #endif // VKCONTACTSPLUGIN_H buteo-sync-plugins-social-0.4.28/src/vk/vk-contacts/vkcontactsyncadaptor.cpp000066400000000000000000000767121474572147200272710ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2014 - 2019 Jolla Ltd. ** Copyright (c) 2020 Open Mobile Platform LLC. ** ****************************************************************************/ #include "vkcontactsyncadaptor.h" #include "vkcontactimagedownloader.h" #include "constants_p.h" #include "trace.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include //libaccounts-qt5 #include #include #define SOCIALD_VK_MAX_CONTACT_ENTRY_RESULTS 200 static const char *IMAGE_DOWNLOADER_TOKEN_KEY = "token"; static const char *IMAGE_DOWNLOADER_ACCOUNT_ID_KEY = "account_id"; static const char *IMAGE_DOWNLOADER_IDENTIFIER_KEY = "identifier"; namespace { const QString FriendCollectionName = QStringLiteral("VK"); bool saveNonexportableDetail(QContact &c, QContactDetail &d) { d.setValue(QContactDetail__FieldNonexportable, QVariant::fromValue(true)); return c.saveDetail(&d, QContact::IgnoreAccessConstraints); } QContactCollection findCollection(const QContactManager &contactManager, const QString &name, int accountId) { const QList collections = contactManager.collections(); for (const QContactCollection &collection : collections) { if (collection.metaData(QContactCollection::KeyName).toString() == name && collection.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID).toInt() == accountId) { return collection; } } return QContactCollection(); } QList findAllCollections(QContactManager &contactManager, int accountId) { QtContactsSqliteExtensions::ContactManagerEngine *cme = QtContactsSqliteExtensions::contactManagerEngine(contactManager); QContactManager::Error error = QContactManager::NoError; QList addedCollections; QList modifiedCollections; QList deletedCollections; QList unmodifiedCollections; if (!cme->fetchCollectionChanges(accountId, qAppName(), &addedCollections, &modifiedCollections, &deletedCollections, &unmodifiedCollections, &error)) { qCWarning(lcSocialPlugin) << "Cannot find collections for account" << accountId << "app" << qAppName() << "error:" << error; return QList(); } return addedCollections + modifiedCollections + deletedCollections + unmodifiedCollections; } QContact findContact(const QList &contacts, const QString &guid) { for (const QContact &contact : contacts) { if (contact.detail().guid() == guid) { return contact; } } return QContact(); } } //--------------- VKContactSqliteSyncAdaptor::VKContactSqliteSyncAdaptor(int accountId, VKContactSyncAdaptor *parent) : QtContactsSqliteExtensions::TwoWayContactSyncAdaptor(accountId, qAppName(), *parent->m_contactManager) , q(parent) , m_accountId(accountId) { m_collection = findCollection(contactManager(), FriendCollectionName, m_accountId); if (m_collection.id().isNull()) { qCDebug(lcSocialPlugin) << "No friends collection saved yet for account:" << m_accountId; m_collection.setMetaData(QContactCollection::KeyName, FriendCollectionName); m_collection.setMetaData(QContactCollection::KeyDescription, QStringLiteral("VK friend contacts")); m_collection.setMetaData(QContactCollection::KeyColor, QStringLiteral("steelblue")); m_collection.setMetaData(QContactCollection::KeySecondaryColor, QStringLiteral("lightsteelblue")); m_collection.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, QCoreApplication::applicationName()); m_collection.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, m_accountId); m_collection.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_READONLY, true); } else { qCDebug(lcSocialPlugin) << "Found friends collection" << m_collection.id() << "for account:" << m_accountId; } } VKContactSqliteSyncAdaptor::~VKContactSqliteSyncAdaptor() { } bool VKContactSqliteSyncAdaptor::determineRemoteCollections() { remoteCollectionsDetermined(QList() << m_collection); return true; } bool VKContactSqliteSyncAdaptor::deleteRemoteCollection(const QContactCollection &collection) { qCWarning(lcSocialPlugin) << "Upsync to remote not supported, not deleting collection" << collection.id(); return true; } bool VKContactSqliteSyncAdaptor::determineRemoteContacts(const QContactCollection &collection) { Q_UNUSED(collection) q->requestData(accountIdForCollection(collection), 0); return true; } bool VKContactSqliteSyncAdaptor::storeLocalChangesRemotely(const QContactCollection &collection, const QList &addedContacts, const QList &modifiedContacts, const QList &deletedContacts) { Q_UNUSED(collection) Q_UNUSED(addedContacts) Q_UNUSED(modifiedContacts) for (const QContact &contact : deletedContacts) { q->deleteDownloadedAvatar(contact); } qCDebug(lcSocialPlugin) << "Upsync to remote not supported, ignoring remote changes for" << collection.id(); return true; } void VKContactSqliteSyncAdaptor::storeRemoteChangesLocally(const QContactCollection &collection, const QList &addedContacts, const QList &modifiedContacts, const QList &deletedContacts) { Q_UNUSED(addedContacts) Q_UNUSED(modifiedContacts) for (const QContact &contact : deletedContacts) { q->deleteDownloadedAvatar(contact); } QtContactsSqliteExtensions::TwoWayContactSyncAdaptor::storeRemoteChangesLocally(collection, addedContacts, modifiedContacts, deletedContacts); } void VKContactSqliteSyncAdaptor::syncFinishedSuccessfully() { qCDebug(lcSocialPlugin) << "Sync finished OK"; // If this is the first sync, TWCSA will have saved the collection and given it a valid id, so // update m_collection so that any post-sync operations (e.g. saving of queued avatar downloads) // will refer to a valid collection. const QContactCollection savedCollection = findCollection(contactManager(), FriendCollectionName, m_accountId); if (savedCollection.id().isNull()) { qCDebug(lcSocialPlugin) << "Error: cannot find saved friends collection!"; } else { m_collection.setId(savedCollection.id()); } } void VKContactSqliteSyncAdaptor::syncFinishedWithError() { qCDebug(lcSocialPlugin) << "Sync finished with error"; } int VKContactSqliteSyncAdaptor::accountIdForCollection(const QContactCollection &collection) { return collection.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID).toInt(); } //---------------------------------- VKContactSyncAdaptor::VKContactSyncAdaptor(QObject *parent) : VKDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::Contacts, parent) , m_contactManager(new QContactManager(QStringLiteral("org.nemomobile.contacts.sqlite"))) , m_workerObject(new VKContactImageDownloader()) { connect(m_workerObject, &AbstractImageDownloader::imageDownloaded, this, &VKContactSyncAdaptor::imageDownloaded); // can sync, enabled setInitialActive(true); } VKContactSyncAdaptor::~VKContactSyncAdaptor() { delete m_workerObject; } QString VKContactSyncAdaptor::syncServiceName() const { return QStringLiteral("vk-contacts"); } void VKContactSyncAdaptor::sync(const QString &dataTypeString, int accountId) { m_apiRequestsRemaining[accountId] = 99; // assume we can make up to 99 requests per sync, before being throttled. // call superclass impl. VKDataTypeSyncAdaptor::sync(dataTypeString, accountId); } void VKContactSyncAdaptor::purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode) { const QList collections = findAllCollections(*m_contactManager, oldId); if (collections.isEmpty()) { qCWarning(lcSocialPlugin) << "Nothing to purge, no collection has been saved for account" << oldId; return; } for (const QContactCollection &collection : collections) { // Delete local avatar image files. QContactCollectionFilter collectionFilter; collectionFilter.setCollectionId(collection.id()); QContactFetchHint fetchHint; fetchHint.setOptimizationHints(QContactFetchHint::NoRelationships); fetchHint.setDetailTypesHint(QList() << QContactDetail::TypeGuid << QContactDetail::TypeAvatar); const QList savedContacts = m_contactManager->contacts(collectionFilter, QList(), fetchHint); for (const QContact &contact : savedContacts) { const QContactAvatar avatar = contact.detail(); const QString imageUrl = avatar.imageUrl().toString(); if (!imageUrl.isEmpty()) { if (!QFile::remove(imageUrl)) { qCWarning(lcSocialPlugin) << "Failed to remove avatar:" << imageUrl; } } } } QList collectionIds; for (const QContactCollection &collection : collections) { collectionIds.append(collection.id()); } // Delete the collections and their contacts. QtContactsSqliteExtensions::ContactManagerEngine *cme = QtContactsSqliteExtensions::contactManagerEngine(*m_contactManager); QContactManager::Error error = QContactManager::NoError; if (cme->storeChanges(nullptr, nullptr, collectionIds, QtContactsSqliteExtensions::ContactManagerEngine::PreserveLocalChanges, true, &error)) { qCInfo(lcSocialPlugin) << "purged account" << oldId << "and successfully removed collections"; } else { qCWarning(lcSocialPlugin) << "Failed to remove collection during purge of account" << oldId << "error:" << error; } } void VKContactSyncAdaptor::retryThrottledRequest(const QString &request, const QVariantList &args, bool retryLimitReached) { int accountId = args[0].toInt(); if (retryLimitReached) { qCWarning(lcSocialPlugin) << "hit request retry limit! unable to request data from VK account with id" << accountId; setStatus(SocialNetworkSyncAdaptor::Error); } else { qCDebug(lcSocialPlugin) << "retrying Contacts" << request << "request for VK account:" << accountId; requestData(accountId, args[1].toInt()); } decrementSemaphore(accountId); // finished waiting for the request. } void VKContactSyncAdaptor::beginSync(int accountId, const QString &accessToken) { // clear our cache lists if necessary. m_remoteContacts[accountId].clear(); m_accessTokens[accountId] = accessToken; VKContactSqliteSyncAdaptor *sqliteSync = m_sqliteSync.value(accountId); if (sqliteSync) { delete sqliteSync; } sqliteSync = new VKContactSqliteSyncAdaptor(accountId, this); if (!sqliteSync->startSync()) { sqliteSync->deleteLater(); qCWarning(lcSocialPlugin) << "unable to init sync adapter - aborting sync VK contacts with account:" << accountId; setStatus(SocialNetworkSyncAdaptor::Error); return; } m_sqliteSync[accountId] = sqliteSync; } void VKContactSyncAdaptor::requestData(int accountId, int startIndex) { const QString accessToken = m_accessTokens[accountId]; QUrl requestUrl; QUrlQuery urlQuery; requestUrl = QUrl(QStringLiteral("https://api.vk.com/method/friends.get")); if (startIndex >= 1) { urlQuery.addQueryItem ("offset", QString::number(startIndex)); } urlQuery.addQueryItem("count", QString::number(SOCIALD_VK_MAX_CONTACT_ENTRY_RESULTS)); urlQuery.addQueryItem("fields", QStringLiteral("uid,first_name,last_name,sex,screen_name,bdate,photo_max,contacts,city,country")); urlQuery.addQueryItem("access_token", accessToken); urlQuery.addQueryItem("v", QStringLiteral("5.21")); // version requestUrl.setQuery(urlQuery); QNetworkRequest req(requestUrl); // we're requesting data. Increment the semaphore so that we know we're still busy. incrementSemaphore(accountId); QNetworkReply *reply = m_networkAccessManager->get(req); if (reply) { reply->setProperty("accountId", accountId); reply->setProperty("accessToken", accessToken); reply->setProperty("startIndex", startIndex); connect(reply, &QNetworkReply::finished, this, &VKContactSyncAdaptor::contactsFinishedHandler); connect(reply, static_cast(&QNetworkReply::error), this, &VKContactSyncAdaptor::errorHandler); connect(reply, &QNetworkReply::sslErrors, this, &VKContactSyncAdaptor::sslErrorsHandler); m_apiRequestsRemaining[accountId] = m_apiRequestsRemaining[accountId] - 1; setupReplyTimeout(accountId, reply); } else { // request was throttled by VKNetworkAccessManager QVariantList args; args << accountId << startIndex; enqueueThrottledRequest(QStringLiteral("requestData"), args); // we are waiting to request data. Increment the semaphore so that we know we're still busy. incrementSemaphore(accountId); // decremented in retryThrottledRequest(). } } void VKContactSyncAdaptor::contactsFinishedHandler() { QNetworkReply *reply = qobject_cast(sender()); QByteArray data = reply->readAll(); int accountId = reply->property("accountId").toInt(); QString accessToken = reply->property("accessToken").toString(); int startIndex = reply->property("startIndex").toInt(); QDateTime lastSyncTimestamp = reply->property("lastSyncTimestamp").toDateTime(); bool isError = reply->property("isError").toBool(); reply->deleteLater(); removeReplyTimeout(accountId, reply); qCDebug(lcSocialPluginTrace) << "received VK friends data for account:" << accountId << ":"; Q_FOREACH (const QString &line, QString::fromUtf8(data).split('\n', QString::SkipEmptyParts)) { qCDebug(lcSocialPluginTrace) << line; } if (isError) { QVariantList args; args << accountId << accessToken << startIndex << lastSyncTimestamp; bool ok = true; QJsonObject parsed = parseJsonObjectReplyData(data, &ok); if (enqueueServerThrottledRequestIfRequired(parsed, QStringLiteral("requestData"), args)) { // we hit the throttle limit, let throttle timer repeat the request // don't decrement semaphore yet as we're still waiting for it. // it will be decremented in retryThrottledRequest(). return; } qCWarning(lcSocialPlugin) << "error occurred when performing contacts request for VK account:" << accountId; setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(accountId); return; } else if (data.isEmpty()) { qCWarning(lcSocialPlugin) << "no contact data in reply from VK with account:" << accountId; setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(accountId); return; } // parse the remote contact information from the response QJsonObject obj = QJsonDocument::fromJson(data).object(); QJsonObject response = obj.value("response").toObject(); m_remoteContacts[accountId].append(parseContacts(response.value("items").toArray(), accountId, accessToken)); int totalCount = response.value("count").toInt(); int seenCount = startIndex + SOCIALD_VK_MAX_CONTACT_ENTRY_RESULTS; if (syncAborted()) { qCInfo(lcSocialPlugin) << "sync aborted, not continuing sync of contacts from VK with account:" << accountId; } else if (totalCount > seenCount) { qCDebug(lcSocialPluginTrace) << "Have received" << seenCount << "contacts, now requesting:" << (seenCount+1) << "through to" << (seenCount+1+SOCIALD_VK_MAX_CONTACT_ENTRY_RESULTS); startIndex = seenCount; requestData(accountId, startIndex); } else { // We've finished downloading the remote changes VKContactSqliteSyncAdaptor *sqliteSync = m_sqliteSync[accountId]; sqliteSync->remoteContactsDetermined(sqliteSync->m_collection, m_remoteContacts[accountId]); } decrementSemaphore(accountId); } bool VKContactSyncAdaptor::queueAvatarForDownload(int accountId, const QString &accessToken, const QString &contactGuid, const QString &imageUrl) { if (m_apiRequestsRemaining[accountId] > 0 && !m_queuedAvatarsForDownload[accountId].contains(contactGuid)) { m_apiRequestsRemaining[accountId] = m_apiRequestsRemaining[accountId] - 1; m_queuedAvatarsForDownload[accountId][contactGuid] = imageUrl; QVariantMap metadata; metadata.insert(IMAGE_DOWNLOADER_ACCOUNT_ID_KEY, accountId); metadata.insert(IMAGE_DOWNLOADER_TOKEN_KEY, accessToken); metadata.insert(IMAGE_DOWNLOADER_IDENTIFIER_KEY, contactGuid); incrementSemaphore(accountId); m_workerObject->queue(imageUrl, metadata); return true; } return false; } QList VKContactSyncAdaptor::parseContacts(const QJsonArray &json, int accountId, const QString &accessToken) { QList retn; QJsonArray::const_iterator it = json.constBegin(); for ( ; it != json.constEnd(); ++it) { const QJsonObject &obj((*it).toObject()); if (obj.isEmpty()) continue; QString mobilePhone = obj.value("mobile_phone").toString(); QString homePhone = obj.value("home_phone").toString(); // build the contact. QContact c; QContactName name; name.setFirstName(obj.value("first_name").toString()); name.setLastName(obj.value("last_name").toString()); saveNonexportableDetail(c, name); QContactGuid guid; int idint = static_cast(obj.value("id").toDouble()); // horrible hack. int uidint = static_cast(obj.value("uid").toDouble()); // horrible hack. if (idint > 0) { guid.setGuid(QStringLiteral("%1:%2").arg(accountId).arg(QString::number(idint))); } else if (uidint > 0) { guid.setGuid(QStringLiteral("%1:%2").arg(accountId).arg(QString::number(uidint))); } else { qCWarning(lcSocialPlugin) << "unable to parse id from VK friend, skipping:" << name; continue; } saveNonexportableDetail(c, guid); if (obj.value("sex").toDouble() > 0) { double genderVal = obj.value("sex").toDouble(); QContactGender gender; if (genderVal == 1.0) { gender.setGender(QContactGender::GenderFemale); } else { gender.setGender(QContactGender::GenderMale); } saveNonexportableDetail(c, gender); } if (!obj.value("bdate").toString().isEmpty() && obj.value("bdate").toString().length() > 5) { // DD.MM.YYYY form, we ignore DD.MM (yearless) form response. QContactBirthday birthday; birthday.setDateTime(QLocale::c().toDateTime(obj.value("bdate").toString(), "dd.MM.yyyy")); saveNonexportableDetail(c, birthday); } if (!obj.value("screen_name").toString().isEmpty() && obj.value("screen_name").toString() != QStringLiteral("id%1").arg(c.detail().guid())) { QContactNickname nickname; nickname.setNickname(obj.value("screen_name").toString()); saveNonexportableDetail(c, nickname); } if (!obj.value("photo_max").toString().isEmpty()) { QContactAvatar avatar; avatar.setImageUrl(QUrl(obj.value("photo_max").toString())); avatar.setValue(QContactAvatar::FieldMetaData, QStringLiteral("picture")); saveNonexportableDetail(c, avatar); } if ((!obj.value("city").toObject().isEmpty() && !obj.value("city").toObject().value("title").toString().isEmpty()) || (!obj.value("country").toObject().isEmpty() && !obj.value("country").toObject().value("title").toString().isEmpty())) { QContactAddress addr; addr.setLocality(obj.value("city").toObject().value("title").toString()); addr.setCountry(obj.value("country").toObject().value("title").toString()); saveNonexportableDetail(c, addr); } if (!mobilePhone.isEmpty()) { QContactPhoneNumber num; num.setSubTypes(QList() << QContactPhoneNumber::SubTypeMobile); num.setNumber(obj.value("mobile_phone").toString()); saveNonexportableDetail(c, num); } if (!homePhone.isEmpty()) { QContactPhoneNumber num; num.setContexts(QContactDetail::ContextHome); num.setSubTypes(QList() << QContactPhoneNumber::SubTypeLandline); num.setNumber(obj.value("mobile_phone").toString()); saveNonexportableDetail(c, num); } QContactUrl url; if (idint > 0) { url.setUrl(QUrl(QStringLiteral("https://m.vk.com/id%1").arg(idint))); } else if (uidint > 0) { url.setUrl(QUrl(QStringLiteral("https://m.vk.com/id%1").arg(uidint))); } url.setSubType(QContactUrl::SubTypeHomePage); saveNonexportableDetail(c, url); retn.append(c); } // fixup the contact avatars. transformContactAvatars(retn, accountId, accessToken); return retn; } void VKContactSyncAdaptor::transformContactAvatars(QList &remoteContacts, int accountId, const QString &accessToken) { // The avatar detail from the remote contact will be some remote URL. // We need to: // 1) transform this to a local filename. // 2) determine if the local file exists. // 3) if not, trigger downloading the avatar. for (int i = 0; i < remoteContacts.size(); ++i) { QContact &curr(remoteContacts[i]); // We only deal with the first avatar from the contact. If it has multiple, // then later avatars will not be transformed. TODO: fix this. // We also only bother to do this for contacts with a GUID, as we don't // store locally any contact without one. QString contactGuid = curr.detail().guid(); if (curr.details().size() && !contactGuid.isEmpty()) { // we have a remote avatar which we need to transform. QContactAvatar avatar = curr.detail(); Q_FOREACH (const QContactAvatar &av, curr.details()) { if (av.value(QContactAvatar::FieldMetaData).toString() == QStringLiteral("picture")) { avatar = av; break; } } QString remoteImageUrl = avatar.imageUrl().toString(); if (!remoteImageUrl.isEmpty() && !avatar.imageUrl().isLocalFile()) { // transform to a local file name. QString localFileName = VKContactImageDownloader::staticOutputFile( contactGuid, remoteImageUrl); // and trigger downloading the image, if it doesn't already exist. // this means that we shouldn't download images needlessly after // first sync, but it also means that if it updates/changes on the // server side, we also won't retrieve any updated image. if (QFile::exists(localFileName)) { QImageReader reader(localFileName); if (reader.canRead()) { // avatar image already exists, update the detail in the contact. avatar.setImageUrl(localFileName); saveNonexportableDetail(curr, avatar); } else { // not a valid image file. Could be artifact from an error. QFile::remove(localFileName); } } if (!QFile::exists(localFileName)) { // temporarily remove the avatar from the contact curr.removeDetail(&avatar); // then trigger the download queueAvatarForDownload(accountId, accessToken, contactGuid, remoteImageUrl); } } } } } void VKContactSyncAdaptor::imageDownloaded(const QString &url, const QString &path, const QVariantMap &metadata) { Q_UNUSED(url) // Load finished, update the avatar, decrement semaphore int accountId = metadata.value(IMAGE_DOWNLOADER_ACCOUNT_ID_KEY).toInt(); QString contactGuid = metadata.value(IMAGE_DOWNLOADER_IDENTIFIER_KEY).toString(); // Empty path signifies that an error occurred. if (!path.isEmpty()) { // no longer outstanding. m_queuedAvatarsForDownload[accountId].remove(contactGuid); m_downloadedContactAvatars[accountId].insert(contactGuid, path); } decrementSemaphore(accountId); } void VKContactSyncAdaptor::deleteDownloadedAvatar(const QContact &contact) { const QString contactGuid = contact.detail().guid(); if (contactGuid.isEmpty()) { return; } const QContactAvatar avatar = contact.detail(); if (avatar.isEmpty()) { return; } const QString localFileName = VKContactImageDownloader::staticOutputFile( contactGuid, avatar.imageUrl().toString()); if (!localFileName.isEmpty() && QFile::remove(localFileName)) { qCDebug(lcSocialPlugin) << "Removed avatar" << localFileName << "of deleted contact" << contact.id(); } } void VKContactSyncAdaptor::finalize(int accountId) { if (syncAborted()) { qCDebug(lcSocialPlugin) << "sync aborted, skipping finalize of VK contacts from account:" << accountId; m_sqliteSync[accountId]->syncFinishedWithError(); } else { qCDebug(lcSocialPlugin) << "finalizing VK contacts sync with account:" << accountId; // first, ensure we update any avatars required. if (m_downloadedContactAvatars[accountId].size()) { // load all VK contacts from the database. We need all details, to avoid clobber. QContactCollectionFilter collectionFilter; collectionFilter.setCollectionId(m_sqliteSync[accountId]->m_collection.id()); QList VKContacts = m_contactManager->contacts(collectionFilter); // find the contacts we need to update. QMap contactsToSave; for (auto it = m_downloadedContactAvatars[accountId].constBegin(); it != m_downloadedContactAvatars[accountId].constEnd(); ++it) { QContact c = findContact(VKContacts, it.key()); if (c.isEmpty()) { c = findContact(m_remoteContacts[accountId], it.key()); } if (c.isEmpty()) { qCWarning(lcSocialPlugin) << "Not saving avatar, cannot find contact with guid" << it.key(); } else { // we have downloaded the avatar for this contact, and need to update it. QContactAvatar a; Q_FOREACH (const QContactAvatar &av, c.details()) { if (av.value(QContactAvatar::FieldMetaData).toString() == QStringLiteral("picture")) { a = av; break; } } a.setValue(QContactAvatar::FieldMetaData, QVariant::fromValue(QStringLiteral("picture"))); a.setImageUrl(it.value()); saveNonexportableDetail(c, a); contactsToSave[c.detail().guid()] = c; } } QList saveList = contactsToSave.values(); if (m_contactManager->saveContacts(&saveList)) { qCInfo(lcSocialPlugin) << "finalize: added avatars for" << saveList.size() << "VK contacts from account" << accountId; } else { qCWarning(lcSocialPlugin) << "finalize: error adding avatars for" << saveList.size() << "VK contacts from account" << accountId; } } m_sqliteSync[accountId]->syncFinishedSuccessfully(); } } void VKContactSyncAdaptor::finalCleanup() { // Synchronously find any contacts which need to be removed, // which were somehow "left behind" by the sync process. // first, get a list of all existing VK account ids QList VKAccountIds; QList purgeAccountIds; QList currentAccountIds; QList uaids = m_accountManager->accountList(); foreach (uint uaid, uaids) { currentAccountIds.append(static_cast(uaid)); } foreach (int currId, currentAccountIds) { Accounts::Account *act = Accounts::Account::fromId(m_accountManager, currId, this); if (act) { if (act->providerName() == QString(QLatin1String("vk"))) { // this account still exists, no need to purge its content VKAccountIds.append(currId); } act->deleteLater(); } } // find all account ids from which contacts have been synced const QList collections = findAllCollections(*m_contactManager, 0); for (const QContactCollection &collection : collections) { if (collection.metaData(QContactCollection::KeyName).toString() == FriendCollectionName) { const int purgeId = collection.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID).toInt(); if (purgeId && !VKAccountIds.contains(purgeId) && !purgeAccountIds.contains(purgeId)) { // this account no longer exists, and needs to be purged. purgeAccountIds.append(purgeId); } } } // purge all data for those account ids which no longer exist. if (purgeAccountIds.size()) { qCInfo(lcSocialPlugin) << "finalCleanup() purging contacts from" << purgeAccountIds.size() << "non-existent VK accounts"; for (int purgeId : purgeAccountIds) { purgeDataForOldAccount(purgeId, SocialNetworkSyncAdaptor::SyncPurge); } } qDeleteAll(m_sqliteSync.values()); m_sqliteSync.clear(); } buteo-sync-plugins-social-0.4.28/src/vk/vk-contacts/vkcontactsyncadaptor.h000066400000000000000000000075151474572147200267310ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2014 - 2019 Jolla Ltd. ** Copyright (c) 2020 Open Mobile Platform LLC. ** ****************************************************************************/ #ifndef VKCONTACTSYNCADAPTOR_H #define VKCONTACTSYNCADAPTOR_H #include "vkdatatypesyncadaptor.h" #include #include #include #include #include QTCONTACTS_USE_NAMESPACE class VKContactImageDownloader; class VKContactSyncAdaptor; class VKContactSqliteSyncAdaptor : public QObject, public QtContactsSqliteExtensions::TwoWayContactSyncAdaptor { Q_OBJECT public: VKContactSqliteSyncAdaptor(int accountId, VKContactSyncAdaptor *parent); ~VKContactSqliteSyncAdaptor(); virtual bool determineRemoteCollections() override; virtual bool deleteRemoteCollection(const QContactCollection &collection) override; virtual bool determineRemoteContacts(const QContactCollection &collection) override; virtual bool storeLocalChangesRemotely(const QContactCollection &collection, const QList &addedContacts, const QList &modifiedContacts, const QList &deletedContacts) override; virtual void storeRemoteChangesLocally(const QContactCollection &collection, const QList &addedContacts, const QList &modifiedContacts, const QList &deletedContacts) override; virtual void syncFinishedSuccessfully() override; virtual void syncFinishedWithError() override; static int accountIdForCollection(const QContactCollection &collection); QContactCollection m_collection; private: VKContactSyncAdaptor *q; int m_accountId = 0; }; class VKContactSyncAdaptor : public VKDataTypeSyncAdaptor { Q_OBJECT public: VKContactSyncAdaptor(QObject *parent); ~VKContactSyncAdaptor(); virtual QString syncServiceName() const override; virtual void sync(const QString &dataTypeString, int accountId = 0) override; void requestData(int accountId, int startIndex = 0); void deleteDownloadedAvatar(const QContact &contact); QContactManager *m_contactManager = nullptr; protected: // implementing VKDataTypeSyncAdaptor interface virtual void purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode mode) override; virtual void beginSync(int accountId, const QString &accessToken) override; virtual void finalize(int accountId) override; virtual void finalCleanup() override; virtual void retryThrottledRequest(const QString &request, const QVariantList &args, bool retryLimitReached) override; private: void contactsFinishedHandler(); QList parseContacts(const QJsonArray &json, int accountId, const QString &accessToken); void transformContactAvatars(QList &remoteContacts, int accountId, const QString &accessToken); bool queueAvatarForDownload(int accountId, const QString &accessToken, const QString &contactGuid, const QString &imageUrl); void imageDownloaded(const QString &url, const QString &path, const QVariantMap &metadata); VKContactImageDownloader *m_workerObject = nullptr; QMap m_sqliteSync; QMap m_accessTokens; QMap > m_remoteContacts; QMap m_apiRequestsRemaining; QMap > m_queuedAvatarsForDownload; // contact guid -> remote avatar path QMap > m_downloadedContactAvatars; // contact guid -> local file path }; #endif // VKCONTACTSYNCADAPTOR_H buteo-sync-plugins-social-0.4.28/src/vk/vk-images/000077500000000000000000000000001474572147200217335ustar00rootroot00000000000000buteo-sync-plugins-social-0.4.28/src/vk/vk-images/vk-images.pri000066400000000000000000000001441474572147200243310ustar00rootroot00000000000000SOURCES += $$PWD/vkimagesyncadaptor.cpp HEADERS += $$PWD/vkimagesyncadaptor.h INCLUDEPATH += $$PWD buteo-sync-plugins-social-0.4.28/src/vk/vk-images/vk-images.pro000066400000000000000000000011101474572147200243310ustar00rootroot00000000000000TARGET = vk-images-client include($$PWD/../../common.pri) include($$PWD/../vk-common.pri) include($$PWD/vk-images.pri) vk_images_sync_profile.path = /etc/buteo/profiles/sync vk_images_sync_profile.files = $$PWD/vk.Images.xml vk_images_client_plugin_xml.path = /etc/buteo/profiles/client vk_images_client_plugin_xml.files = $$PWD/vk-images.xml HEADERS += vkimagesplugin.h SOURCES += vkimagesplugin.cpp OTHER_FILES += \ vk_images_sync_profile.files \ vk_images_client_plugin_xml.files INSTALLS += \ target \ vk_images_sync_profile \ vk_images_client_plugin_xml buteo-sync-plugins-social-0.4.28/src/vk/vk-images/vk-images.xml000066400000000000000000000002001474572147200243300ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/vk/vk-images/vk.Images.xml000066400000000000000000000011201474572147200242730ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/vk/vk-images/vkimagesplugin.cpp000066400000000000000000000035421474572147200254700ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015 Jolla Ltd. ** Contact: Chris Adams ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "vkimagesplugin.h" #include "vkimagesyncadaptor.h" #include "socialnetworksyncadaptor.h" VKImagesPlugin::VKImagesPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *callbackInterface) : SocialdButeoPlugin(pluginName, profile, callbackInterface, QStringLiteral("vk"), SocialNetworkSyncAdaptor::dataTypeName(SocialNetworkSyncAdaptor::Images)) { } VKImagesPlugin::~VKImagesPlugin() { } SocialNetworkSyncAdaptor *VKImagesPlugin::createSocialNetworkSyncAdaptor() { return new VKImageSyncAdaptor(this); } Buteo::ClientPlugin* VKImagesPluginLoader::createClientPlugin( const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) { return new VKImagesPlugin(pluginName, profile, cbInterface); } buteo-sync-plugins-social-0.4.28/src/vk/vk-images/vkimagesplugin.h000066400000000000000000000035751474572147200251430ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015 Jolla Ltd. ** Contact: Chris Adams ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef VKIMAGESPLUGIN_H #define VKIMAGESPLUGIN_H #include "socialdbuteoplugin.h" #include class Q_DECL_EXPORT VKImagesPlugin : public SocialdButeoPlugin { Q_OBJECT public: VKImagesPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *cbInterface); ~VKImagesPlugin(); protected: SocialNetworkSyncAdaptor *createSocialNetworkSyncAdaptor(); }; class VKImagesPluginLoader : public Buteo::SyncPluginLoader { Q_OBJECT Q_PLUGIN_METADATA(IID "org.sailfishos.plugins.sync.VKImagesPluginLoader") Q_INTERFACES(Buteo::SyncPluginLoader) public: Buteo::ClientPlugin* createClientPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) override; }; #endif // VKIMAGESPLUGIN_H buteo-sync-plugins-social-0.4.28/src/vk/vk-images/vkimagesyncadaptor.cpp000066400000000000000000000646201474572147200263420ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015 Jolla Ltd. ** Contact: Chris Adams ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "vkimagesyncadaptor.h" #include "trace.h" #include #include #include #include #include #include #define VK_IMAGES_MAX_COUNT 1000 /* maximum images returned per request */ // Currently, we integrate with the device image gallery via saving thumbnails to the // ~/.local/share/system/privileged/Images directory, and filling the // ~/.local/share/system/privileged/Images/vk.db with appropriate data // via libsocialcache. VKImageSyncAdaptor::VKImageSyncAdaptor(QObject *parent) : VKDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::Images, parent) , m_syncError(false) , m_currentAlbumIndex(0) { setInitialActive(m_db.isValid()); } VKImageSyncAdaptor::~VKImageSyncAdaptor() { } QString VKImageSyncAdaptor::syncServiceName() const { return QStringLiteral("vk-images"); } void VKImageSyncAdaptor::sync(const QString &dataTypeString, int accountId) { // call superclass impl. VKDataTypeSyncAdaptor::sync(dataTypeString, accountId); } void VKImageSyncAdaptor::purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode) { m_db.purgeAccount(oldId); m_db.commit(); m_db.wait(); } void VKImageSyncAdaptor::beginSync(int accountId, const QString &accessToken) { requestData(accountId, accessToken, QString(), QString(), QString()); } void VKImageSyncAdaptor::finalize(int accountId) { if (syncAborted()) { qCInfo(lcSocialPlugin) << "sync aborted, won't commit database changes"; } else if (m_syncError) { qCInfo(lcSocialPlugin) << "sync error, won't commit database changes"; setStatus(SocialNetworkSyncAdaptor::Error); } else { // Determine album delta. QHash > deletedAlbumIds; // user to deleted album ids QHash > emptiedAlbumIds; // user to empty album ids QList deletedAlbums; QList addedAlbums; QList modifiedAlbums; QList unmodifiedAlbums; // first, find the albums which are new and need to be added to the db. bool found = false; QList accountAlbums = m_db.albums(accountId, QString()); Q_FOREACH (const VKAlbum::ConstPtr &receivedAlbum, m_receivedAlbums) { found = false; Q_FOREACH (const VKAlbum::ConstPtr &album, accountAlbums) { if (album->id() == receivedAlbum->id() && album->ownerId() == receivedAlbum->ownerId()) { found = true; } } if (!found) { addedAlbums.append(receivedAlbum); } } // then, find the albums which need to be removed or updated in the db. Q_FOREACH (const VKAlbum::ConstPtr &album, accountAlbums) { found = false; Q_FOREACH (const VKAlbum::ConstPtr &receivedAlbum, m_receivedAlbums) { if (album->id() == receivedAlbum->id() && album->ownerId() == receivedAlbum->ownerId()) { found = true; if (*album != *receivedAlbum) { modifiedAlbums.append(receivedAlbum); } else { unmodifiedAlbums.append(receivedAlbum); } } } if (!found) { deletedAlbums.append(album); deletedAlbumIds[album->ownerId()].insert(album->id()); } } // and find the albums which are empty server side. // these will be unmodified but the photos will need to be removed. Q_FOREACH (const VKAlbum::ConstPtr &album, m_emptyAlbums) { emptiedAlbumIds[album->ownerId()].insert(album->id()); } // Determine photo delta. QList deletedPhotos; QList addedPhotos; QList modifiedPhotos; QList unmodifiedPhotos; // first, find the photos which need to be removed or updated in the db. QList accountPhotos = m_db.images(accountId, QString(), QString()); Q_FOREACH (const VKImage::ConstPtr &photo, accountPhotos) { if (deletedAlbumIds[photo->ownerId()].contains(photo->albumId())) { // the entire album has been deleted. every photo in it needs to be deleted. deletedPhotos.append(photo); } else if (emptiedAlbumIds[photo->ownerId()].contains(photo->albumId())) { // the album has been emptied server-side. every photo in it needs to be deleted. deletedPhotos.append(photo); } else if (!m_requestedPhotosForOwnerAndAlbum.contains(QStringLiteral("%1:%2:%3").arg(photo->ownerId()).arg(photo->albumId()).arg(accountId))) { // this album wasn't modified, so we didn't request photos from it. // that is, every photo in it is unchanged. unmodifiedPhotos.append(photo); } else { // this album was modified, so we need to perform delta detection. found = false; Q_FOREACH (const VKImage::ConstPtr &receivedPhoto, m_receivedPhotos) { if (photo->id() == receivedPhoto->id() && photo->ownerId() == receivedPhoto->ownerId()) { found = true; if (*photo != *receivedPhoto) { modifiedPhotos.append(receivedPhoto); } else { unmodifiedPhotos.append(receivedPhoto); } } } if (!found) { deletedPhotos.append(photo); } } } // then find the photos which are new and need to be added to the db. Q_FOREACH (const VKImage::ConstPtr &receivedPhoto, m_receivedPhotos) { found = false; Q_FOREACH (const VKImage::ConstPtr &photo, accountPhotos) { if (photo->id() == receivedPhoto->id() && photo->ownerId() == receivedPhoto->ownerId()) { found = true; } } if (!found) { addedPhotos.append(receivedPhoto); } } qCDebug(lcSocialPlugin) << "Have finished Images sync for VK account:" << accountId; qCDebug(lcSocialPlugin) << " with Users added: " << m_receivedUsers.size(); qCDebug(lcSocialPlugin) << " with Albums A/M/R/U:" << addedAlbums.size() << "/" << modifiedAlbums.size() << "/" << deletedAlbums.size() << "/" << unmodifiedAlbums.size(); qCDebug(lcSocialPlugin) << " with Photos A/M/R/U:" << addedPhotos.size() << "/" << modifiedPhotos.size() << "/" << deletedPhotos.size() << "/" << unmodifiedPhotos.size(); // write changes to database. Q_FOREACH (const VKUser::ConstPtr &user, m_receivedUsers) { m_db.addUser(user); } m_db.addAlbums(addedAlbums+modifiedAlbums); m_db.removeAlbums(deletedAlbums); m_db.addImages(addedPhotos + modifiedPhotos); m_db.removeImages(deletedPhotos); // and commit the changes to disk. m_db.commit(); m_db.wait(); } } void VKImageSyncAdaptor::retryThrottledRequest(const QString &request, const QVariantList &args, bool retryLimitReached) { int accountId = args[0].toInt(); if (retryLimitReached) { qCWarning(lcSocialPlugin) << "hit request retry limit! unable to request data from VK account with id" << accountId; m_syncError = true; } else { qCDebug(lcSocialPlugin) << "retrying Images" << request << "request for VK account:" << accountId; if (request == QStringLiteral("requestData")) { requestData(accountId, args[1].toString(), args[2].toString(), args[3].toString(), args[4].toString()); } else { possiblyAddNewUser(accountId, args[1].toString(), args[2].toString()); } } decrementSemaphore(accountId); // finished waiting for the request. } void VKImageSyncAdaptor::requestData(int accountId, const QString &accessToken, const QString &continuationUrl, const QString &vkUserId, const QString &vkAlbumId) { if (syncAborted()) { qCDebug(lcSocialPlugin) << "skipping data request due to sync abort"; m_syncError = true; return; } QUrl url; if (!continuationUrl.isEmpty()) { // fetch the next page. url = QUrl(continuationUrl); } else { // build the request, depending on whether we're fetching albums or images. if (vkAlbumId.isEmpty()) { // fetching all albums from the user. url = QUrl(QStringLiteral("https://api.vk.com/method/photos.getAlbums")); } else { // fetching images from a particular album. url = QUrl(QStringLiteral("https://api.vk.com/method/photos.get")); } } // if the url already contains query part (in which case it is continuationUrl), don't overwrite it. if (!url.hasQuery()) { QList > queryItems; QUrlQuery query(url); queryItems.append(QPair(QStringLiteral("access_token"), accessToken)); if (vkAlbumId.isEmpty()) { queryItems.append(QPair(QStringLiteral("need_system"), QStringLiteral("1"))); queryItems.append(QPair(QStringLiteral("need_covers"), QStringLiteral("1"))); } else { queryItems.append(QPair(QStringLiteral("album_id"), vkAlbumId)); queryItems.append(QPair(QStringLiteral("extended"), QStringLiteral("1"))); queryItems.append(QPair(QStringLiteral("photo_sizes"), QStringLiteral("1"))); queryItems.append(QPair(QStringLiteral("count"), QString::number(VK_IMAGES_MAX_COUNT))); } queryItems.append(QPair(QStringLiteral("v"), QStringLiteral("5.33"))); // version query.setQueryItems(queryItems); url.setQuery(query); } QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(url)); if (reply) { reply->setProperty("accountId", accountId); reply->setProperty("accessToken", accessToken); reply->setProperty("vkUserId", vkUserId); // only valid for photos request reply->setProperty("vkAlbumId", vkAlbumId); // only valid for photos request reply->setProperty("continuationUrl", continuationUrl); // only valid for photos request connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(errorHandler(QNetworkReply::NetworkError))); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsHandler(QList))); if (vkAlbumId.isEmpty()) { qCDebug(lcSocialPlugin) << "Requesting albums for VK account:" << accountId << ":" << url.toString(); connect(reply, SIGNAL(finished()), this, SLOT(albumsFinishedHandler())); } else { qCDebug(lcSocialPlugin) << "Requesting photos from album:" << vkAlbumId << "for VK account:" << accountId << ":" << url.toString(); connect(reply, SIGNAL(finished()), this, SLOT(imagesFinishedHandler())); } // we're requesting data. Increment the semaphore so that we know we're still busy. incrementSemaphore(accountId); setupReplyTimeout(accountId, reply); } else { // request was throttled by VKNetworkAccessManager QVariantList args; args << accountId << accessToken << continuationUrl << vkUserId << vkAlbumId; enqueueThrottledRequest(QStringLiteral("requestData"), args); // we are waiting to request data. Increment the semaphore so that we know we're still busy. incrementSemaphore(accountId); // decremented in retryThrottledRequest(). } } void VKImageSyncAdaptor::albumsFinishedHandler() { QNetworkReply *reply = qobject_cast(sender()); bool isError = reply->property("isError").toBool(); int accountId = reply->property("accountId").toInt(); QString accessToken = reply->property("accessToken").toString(); QByteArray replyData = reply->readAll(); disconnect(reply); reply->deleteLater(); removeReplyTimeout(accountId, reply); qCDebug(lcSocialPluginTrace) << QString::fromUtf8(replyData); bool ok = false; QJsonObject parsed = parseJsonObjectReplyData(replyData, &ok); if (isError || !ok || !parsed.contains(QLatin1String("response"))) { QVariantList args; args << accountId << accessToken << QString() << QString() << QString(); if (enqueueServerThrottledRequestIfRequired(parsed, QStringLiteral("requestData"), args)) { // we hit the throttle limit, let throttle timer repeat the request. // don't decrement semaphore yet as we're still waiting for it. // it will be decremented in retryThrottledRequest(). return; } qCWarning(lcSocialPlugin) << "unable to read albums response for VK account with id" << accountId; m_syncError = true; decrementSemaphore(accountId); return; } QJsonArray items = parsed.value(QLatin1String("response")).toObject().value(QLatin1String("items")).toArray(); if (items.size() == 0) { qCDebug(lcSocialPlugin) << "VK account with id" << accountId << "has no albums"; decrementSemaphore(accountId); return; } // read the albums information for (int i = 0; i < items.size(); ++i) { QJsonObject albumObject = items.at(i).toObject(); // ignore empty album objects. if (albumObject.isEmpty()) { continue; } // parse the album info. QString id = QString::number(albumObject.value("id").toDouble(), 'g', 13); QString ownerId = QString::number(albumObject.value("owner_id").toDouble(), 'g', 13); QString title = albumObject.value("title").toString(); QString description = albumObject.value("description").toString(); int created = albumObject.value("created").toInt(); int updated = albumObject.value("updated").toInt(); QString thumbSrc = albumObject.value("thumb_src").toString(); int size = albumObject.value("size").toInt(); m_receivedAlbums.append(VKAlbum::create(id, ownerId, title, description, thumbSrc, QString(), size, created, updated, accountId)); // request the photos from this album if necessary int lastSyncTimestampForAlbum = 0; VKAlbum::ConstPtr dbAlbum = m_db.album(accountId, ownerId, id); if (dbAlbum) { lastSyncTimestampForAlbum = qMax(dbAlbum->created(), dbAlbum->updated()); } if (created > lastSyncTimestampForAlbum || updated > lastSyncTimestampForAlbum || (created == 0 && updated == 0)) { qCDebug(lcSocialPlugin) << "Need to request photos for album:" << id << title << "with timestamps:" << created << "+" << updated << ">" << lastSyncTimestampForAlbum; m_requestedPhotosForOwnerAndAlbum.append(QStringLiteral("%1:%2:%3").arg(ownerId).arg(id).arg(accountId)); } else { qCDebug(lcSocialPlugin) << "No need to request photos for album:" << id << title << "with timestamps:" << created << "+" << updated << "<=" << lastSyncTimestampForAlbum; } // request the information for user who owns this album if necessary possiblyAddNewUser(accountId, accessToken, ownerId); } // start downloading album content m_currentAlbumIndex = 0; requestQueuedAlbum(accessToken); // Finally, reduce our semaphore. decrementSemaphore(accountId); } void VKImageSyncAdaptor::imagesFinishedHandler() { QNetworkReply *reply = qobject_cast(sender()); bool isError = reply->property("isError").toBool(); int accountId = reply->property("accountId").toInt(); QString accessToken = reply->property("accessToken").toString(); QString vkUserId = reply->property("vkUserId").toString(); QString vkAlbumId = reply->property("vkAlbumId").toString(); QString continuationUrl = reply->property("continuationUrl").toString(); QByteArray replyData = reply->readAll(); disconnect(reply); reply->deleteLater(); removeReplyTimeout(accountId, reply); qCDebug(lcSocialPluginTrace) << QString::fromUtf8(replyData); bool ok = false; QJsonObject parsed = parseJsonObjectReplyData(replyData, &ok); if (isError || !ok || !parsed.contains(QLatin1String("response"))) { QVariantList args; args << accountId << accessToken << vkUserId << vkAlbumId << continuationUrl; if (enqueueServerThrottledRequestIfRequired(parsed, QStringLiteral("requestData"), args)) { // we hit the throttle limit, let throttle timer repeat the request // don't decrement semaphore yet as we're still waiting for it. // it will be decremented in retryThrottledRequest(). return; } qCWarning(lcSocialPlugin) << "unable to read photos response for VK account with id" << accountId; m_syncError = true; decrementSemaphore(accountId); return; } QJsonArray items = parsed.value(QLatin1String("response")).toObject().value(QLatin1String("items")).toArray(); if (items.size() == 0) { qCDebug(lcSocialPlugin) << "album with id" << vkAlbumId << "from VK account with id" << accountId << "has no photos"; VKAlbum::Ptr emptyAlbum = VKAlbum::create(vkAlbumId, vkUserId, QString(), QString(), QString(), QString(), 0, 0, 0, accountId); m_emptyAlbums.append(emptyAlbum); } // read the photos information int requestImagesCount = 0; foreach (const QJsonValue imageValue, items) { // ignore empty image objects QJsonObject imageObject = imageValue.toObject(); if (imageObject.isEmpty()) { continue; } // parse image info. QString id = QString::number(imageObject.value("id").toDouble(), 'g', 13); QString text = imageObject.value("text").toString(); int date = imageObject.value("date").toInt(); int height = 0; int width = 0; QString src; QString thumbSrc; QJsonArray sizedPhotos = imageObject.value("sizes").toArray(); for (int psi = 0; psi < sizedPhotos.size(); ++psi) { const QJsonObject &sizedImage(sizedPhotos[psi].toObject()); int currHeight = sizedImage.value("height").toInt(); if (currHeight > height) { height = currHeight; width = sizedImage.value("width").toInt(); src = sizedImage.value("src").toString(); } if (thumbSrc.isEmpty() && sizedImage.value("type").toString() == QStringLiteral("s")) { thumbSrc = sizedImage.value("src").toString(); } else if (sizedImage.value("type").toString() == QStringLiteral("m")) { thumbSrc = sizedImage.value("src").toString(); } } if (thumbSrc.isEmpty()) { thumbSrc = src; } // append the photo to our internal list. qCDebug(lcSocialPlugin) << "have new photo:" << id << src << height << width << date; m_receivedPhotos.append(VKImage::create(id, vkAlbumId, vkUserId, text, thumbSrc, src, QString(), QString(), width, height, date, accountId)); requestImagesCount += 1; } // perform a continuation request if required. set offset in url + 1000 to current offset. if (requestImagesCount == VK_IMAGES_MAX_COUNT) { QUrl continuation = QUrl(continuationUrl); QUrlQuery queryItems(continuation); int offset = queryItems.hasQueryItem("offset") ? queryItems.queryItemValue("offset").toInt() + VK_IMAGES_MAX_COUNT : VK_IMAGES_MAX_COUNT; queryItems.removeAllQueryItems("offset"); queryItems.addQueryItem("offset", QString::number(offset)); continuation.setQuery(queryItems); qCDebug(lcSocialPlugin) << "performing continuation request for album:" << vkAlbumId << ":" << continuation.toString(); requestData(accountId, accessToken, continuation.toString(), vkUserId, vkAlbumId); } else { // Load next album if there are unhandled ones in the queue requestQueuedAlbum(accessToken); } // we're finished this request. Decrement our busy semaphore. decrementSemaphore(accountId); } void VKImageSyncAdaptor::possiblyAddNewUser(int accountId, const QString &accessToken, const QString &vkUserId) { QString dbUserId; VKUser::ConstPtr dbUser = m_db.user(accountId); if (dbUser) { dbUserId = dbUser->id(); } if (m_requestedUsers.contains(vkUserId) || !dbUserId.isEmpty()) { return; // already requested or db already contains the user, no need to request. } // We need to add the user. We call VK to get the informations that we // need and then add it to the database m_requestedUsers.insert(vkUserId); QUrl url(QStringLiteral("https://api.vk.com/method/users.get")); QList > queryItems; queryItems.append(QPair(QStringLiteral("access_token"), accessToken)); queryItems.append(QPair(QStringLiteral("fields"), QStringLiteral("id,photo_medium,first_name,last_name"))); queryItems.append(QPair(QStringLiteral("v"), QStringLiteral("5.33"))); // version QUrlQuery query(url); query.setQueryItems(queryItems); url.setQuery(query); QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(url)); if (reply) { reply->setProperty("accountId", accountId); reply->setProperty("accessToken", accessToken); reply->setProperty("vkUserId", vkUserId); connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(errorHandler(QNetworkReply::NetworkError))); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsHandler(QList))); connect(reply, SIGNAL(finished()), this, SLOT(userFinishedHandler())); incrementSemaphore(accountId); setupReplyTimeout(accountId, reply); } else { // request was throttled by VKNetworkAccessManager QVariantList args; args << accountId << accessToken << vkUserId; enqueueThrottledRequest(QStringLiteral("possiblyAddNewUser"), args); // we are waiting to request data. Increment the semaphore so that we know we're still busy. incrementSemaphore(accountId); // decremented in retryThrottledRequest(). } } void VKImageSyncAdaptor::userFinishedHandler() { QNetworkReply *reply = qobject_cast(sender()); QByteArray replyData = reply->readAll(); int accountId = reply->property("accountId").toInt(); QString accessToken = reply->property("accessToken").toString(); QString vkUserId = reply->property("vkUserId").toString(); disconnect(reply); reply->deleteLater(); bool ok = false; QJsonObject parsed = parseJsonObjectReplyData(replyData, &ok); if (!ok || !parsed.contains(QLatin1String("response")) || !parsed.value(QLatin1String("response")).toArray().size()) { QVariantList args; args << accountId << accessToken << vkUserId; if (enqueueServerThrottledRequestIfRequired(parsed, QStringLiteral("possiblyAddNewUser"), args)) { // we hit the throttle limit, let throttle timer repeat the request // don't decrement semaphore yet as we're still waiting for it. // it will be decremented in retryThrottledRequest(). return; } qCWarning(lcSocialPlugin) << "unable to read users.get response for VK account with id" << accountId; return; } QJsonObject userObject = parsed.value(QLatin1String("response")).toArray().first().toObject(); QString id = QString::number(userObject.value(QLatin1String("id")).toDouble(), 'g', 13); QString firstName = userObject.value(QLatin1String("first_name")).toString(); QString lastName = userObject.value(QLatin1String("last_name")).toString(); QString photoSrc = userObject.value(QLatin1String("photo_medium")).toString(); m_receivedUsers.append(VKUser::create(id, firstName, lastName, photoSrc, QString(), accountId)); decrementSemaphore(accountId); } void VKImageSyncAdaptor::requestQueuedAlbum(const QString &accessToken) { // take next album from the queue and load it if (m_currentAlbumIndex < m_requestedPhotosForOwnerAndAlbum.count()) { QStringList parts = m_requestedPhotosForOwnerAndAlbum.at(m_currentAlbumIndex).split(":", QString::SkipEmptyParts); QString ownerId = parts.at(0); QString id = parts.at(1); int accountId = parts.at(2).toInt(); qCDebug(lcSocialPlugin) << "start loading VK album:" << id << ownerId << accountId; m_currentAlbumIndex++; requestData(accountId, accessToken, QString(), ownerId, id); } } buteo-sync-plugins-social-0.4.28/src/vk/vk-images/vkimagesyncadaptor.h000066400000000000000000000055321474572147200260040ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015 Jolla Ltd. ** Contact: Chris Adams ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef VKIMAGESYNCADAPTOR_H #define VKIMAGESYNCADAPTOR_H #include "vkdatatypesyncadaptor.h" #include #include #include #include #include #include #include #include #include #include class VKImageSyncAdaptor : public VKDataTypeSyncAdaptor { Q_OBJECT public: VKImageSyncAdaptor(QObject *parent); ~VKImageSyncAdaptor(); QString syncServiceName() const; void sync(const QString &dataTypeString, int accountId); protected: // implementing VKDataTypeSyncAdaptor interface void purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode mode); void beginSync(int accountId, const QString &accessToken); void finalize(int accountId); void retryThrottledRequest(const QString &request, const QVariantList &args, bool retryLimitReached); private: void requestData(int accountId, const QString &accessToken, const QString &continuationUrl, const QString &vkUserId, const QString &vkAlbumId); void possiblyAddNewUser(int accountId, const QString &accessToken, const QString &vkUserId); void requestQueuedAlbum(const QString &accessToken); private Q_SLOTS: void albumsFinishedHandler(); void imagesFinishedHandler(); void userFinishedHandler(); private: QList m_receivedAlbums; QList m_receivedPhotos; QList m_receivedUsers; QSet m_requestedUsers; // only want to request the user information once. QList m_requestedPhotosForOwnerAndAlbum; // owner_id:album_id:account_id QList m_emptyAlbums; VKImagesDatabase m_db; bool m_syncError; int m_currentAlbumIndex; }; #endif // VKIMAGESYNCADAPTOR_H buteo-sync-plugins-social-0.4.28/src/vk/vk-notifications/000077500000000000000000000000001474572147200233375ustar00rootroot00000000000000buteo-sync-plugins-social-0.4.28/src/vk/vk-notifications/vk-notifications.pri000066400000000000000000000002561474572147200273450ustar00rootroot00000000000000CONFIG += link_pkgconfig PKGCONFIG += nemonotifications-qt5 SOURCES += $$PWD/vknotificationsyncadaptor.cpp HEADERS += $$PWD/vknotificationsyncadaptor.h INCLUDEPATH += $$PWD buteo-sync-plugins-social-0.4.28/src/vk/vk-notifications/vk-notifications.pro000066400000000000000000000012071474572147200273500ustar00rootroot00000000000000TARGET = vk-notifications-client include($$PWD/../../common.pri) include($$PWD/../vk-common.pri) include($$PWD/vk-notifications.pri) vk_notifications_sync_profile.path = /etc/buteo/profiles/sync vk_notifications_sync_profile.files = $$PWD/vk.Notifications.xml vk_notifications_client_plugin_xml.path = /etc/buteo/profiles/client vk_notifications_client_plugin_xml.files = $$PWD/vk-notifications.xml HEADERS += vknotificationsplugin.h SOURCES += vknotificationsplugin.cpp OTHER_FILES += \ vk.Notifications.xml \ vk-notifications.xml INSTALLS += \ target \ vk_notifications_sync_profile \ vk_notifications_client_plugin_xml buteo-sync-plugins-social-0.4.28/src/vk/vk-notifications/vk-notifications.xml000066400000000000000000000002071474572147200273470ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/vk/vk-notifications/vk.Notifications.xml000066400000000000000000000011301474572147200273040ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/vk/vk-notifications/vknotificationsplugin.cpp000066400000000000000000000023461474572147200305010ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2014 Jolla Ltd. ** Contact: Chris Adams ** ****************************************************************************/ #include "vknotificationsplugin.h" #include "vknotificationsyncadaptor.h" #include "socialnetworksyncadaptor.h" VKNotificationsPlugin::VKNotificationsPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *callbackInterface) : SocialdButeoPlugin(pluginName, profile, callbackInterface, QStringLiteral("vk"), SocialNetworkSyncAdaptor::dataTypeName(SocialNetworkSyncAdaptor::Notifications)) { } VKNotificationsPlugin::~VKNotificationsPlugin() { } SocialNetworkSyncAdaptor *VKNotificationsPlugin::createSocialNetworkSyncAdaptor() { return new VKNotificationSyncAdaptor(this); } Buteo::ClientPlugin* VKNotificationsPluginLoader::createClientPlugin( const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) { return new VKNotificationsPlugin(pluginName, profile, cbInterface); } buteo-sync-plugins-social-0.4.28/src/vk/vk-notifications/vknotificationsplugin.h000066400000000000000000000023531474572147200301440ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2014 Jolla Ltd. ** Contact: Chris Adams ** ****************************************************************************/ #ifndef VKNOTIFICATIONSPLUGIN_H #define VKNOTIFICATIONSPLUGIN_H #include "socialdbuteoplugin.h" #include class Q_DECL_EXPORT VKNotificationsPlugin : public SocialdButeoPlugin { Q_OBJECT public: VKNotificationsPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *cbInterface); ~VKNotificationsPlugin(); protected: SocialNetworkSyncAdaptor *createSocialNetworkSyncAdaptor(); }; class VKNotificationsPluginLoader : public Buteo::SyncPluginLoader { Q_OBJECT Q_PLUGIN_METADATA(IID "org.sailfishos.plugins.sync.VKNotificationsPluginLoader") Q_INTERFACES(Buteo::SyncPluginLoader) public: Buteo::ClientPlugin* createClientPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) override; }; #endif // VKNOTIFICATIONSPLUGIN_H buteo-sync-plugins-social-0.4.28/src/vk/vk-notifications/vknotificationsyncadaptor.cpp000066400000000000000000000160601474572147200313450ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2013 Jolla Ltd. ** Contact: Chris Adams ** ****************************************************************************/ #include "vknotificationsyncadaptor.h" #include "trace.h" #include #include #include #include #include //static const int OLD_NOTIFICATION_LIMIT_IN_DAYS = 21; // TODO VKNotificationSyncAdaptor::VKNotificationSyncAdaptor(QObject *parent) : VKDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::Notifications, parent) { setInitialActive(true); } VKNotificationSyncAdaptor::~VKNotificationSyncAdaptor() { } QString VKNotificationSyncAdaptor::syncServiceName() const { return QStringLiteral("vk-microblog"); } void VKNotificationSyncAdaptor::purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode) { m_db.removeNotifications(oldId); m_db.sync(); m_db.wait(); } void VKNotificationSyncAdaptor::beginSync(int accountId, const QString &accessToken) { requestNotifications(accountId, accessToken); } void VKNotificationSyncAdaptor::finalize(int accountId) { Q_UNUSED(accountId); if (syncAborted()) { qCDebug(lcSocialPlugin) << "sync aborted, skipping finalize of VK Notifications from account:" << accountId; } else { qCDebug(lcSocialPlugin) << "finalizing VK Notifications sync with account:" << accountId; //m_db.purgeOldNotifications(OLD_NOTIFICATION_LIMIT_IN_DAYS); // TODO Q_FOREACH (const NotificationData ¬ification, m_notificationsToAdd) { QList userProfiles; foreach (const QJsonValue &entry, notification.profiles) { userProfiles << UserProfile::fromJsonObject(entry.toObject()); } saveVKNotificationFromObject(notification.accountId, notification.notification, userProfiles); } m_db.sync(); m_db.wait(); } } void VKNotificationSyncAdaptor::retryThrottledRequest(const QString &request, const QVariantList &args, bool retryLimitReached) { int accountId = args[0].toInt(); if (retryLimitReached) { qCWarning(lcSocialPlugin) << "hit request retry limit! unable to request data from VK account with id" << accountId; setStatus(SocialNetworkSyncAdaptor::Error); } else { qCDebug(lcSocialPlugin) << "retrying Notifications" << request << "request for VK account:" << accountId; requestNotifications(accountId, args[1].toString(), args[2].toString(), args[3].toString()); } decrementSemaphore(accountId); // finished waiting for the request. } void VKNotificationSyncAdaptor::requestNotifications(int accountId, const QString &accessToken, const QString &until, const QString &pagingToken) { // TODO: result paging Q_UNUSED(until); Q_UNUSED(pagingToken); QList > queryItems; queryItems.append(QPair(QString(QLatin1String("access_token")), accessToken)); queryItems.append(QPair(QString(QLatin1String("v")), QStringLiteral("5.21"))); // version QUrl url(QStringLiteral("https://api.vk.com/method/notifications.get")); QUrlQuery query(url); query.setQueryItems(queryItems); url.setQuery(query); QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(url)); if (reply) { reply->setProperty("accountId", accountId); reply->setProperty("accessToken", accessToken); connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(errorHandler(QNetworkReply::NetworkError))); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsHandler(QList))); connect(reply, SIGNAL(finished()), this, SLOT(finishedHandler())); // we're requesting data. Increment the semaphore so that we know we're still busy. incrementSemaphore(accountId); setupReplyTimeout(accountId, reply); } else { // request was throttled by VKNetworkAccessManager QVariantList args; args << accountId << accessToken << until << pagingToken; enqueueThrottledRequest(QStringLiteral("requestNotifications"), args); // we are waiting to request data. Increment the semaphore so that we know we're still busy. incrementSemaphore(accountId); // decremented in retryThrottledRequest(). } } void VKNotificationSyncAdaptor::finishedHandler() { QNetworkReply *reply = qobject_cast(sender()); bool isError = reply->property("isError").toBool(); int accountId = reply->property("accountId").toInt(); QByteArray replyData = reply->readAll(); disconnect(reply); reply->deleteLater(); removeReplyTimeout(accountId, reply); bool ok = false; QJsonObject parsed = parseJsonObjectReplyData(replyData, &ok); if (!isError && ok && parsed.contains(QLatin1String("response"))) { QJsonObject responseObj = parsed.value(QStringLiteral("response")).toObject(); QJsonArray profileValues = responseObj.value(QStringLiteral("profiles")).toArray(); QJsonArray items = responseObj.value(QLatin1String("items")).toArray(); foreach (const QJsonValue &entry, items) { QJsonObject object = entry.toObject(); if (!object.isEmpty()) { m_notificationsToAdd.append(NotificationData(accountId, object, profileValues)); } else { qCDebug(lcSocialPlugin) << "notification object empty; skipping"; } } } else { // error occurred during request. qCWarning(lcSocialPlugin) << "error: unable to parse notification data from request with account:" << accountId << "got:" << QString::fromUtf8(replyData); } // we're finished this request. Decrement our busy semaphore. decrementSemaphore(accountId); } void VKNotificationSyncAdaptor::saveVKNotificationFromObject(int accountId, const QJsonObject ¬if, const QList &userProfiles) { QString type = notif.value("type").toString(); QDateTime timestamp = parseVKDateTime(notif.value(QStringLiteral("date"))); QJsonObject feedback = notif.value(QStringLiteral("feedback")).toObject(); QJsonArray feedbackItems = feedback.value(QStringLiteral("items")).toArray(); Q_FOREACH (const QJsonValue &feedbackItem, feedbackItems) { QJsonObject feedbackItemObj = feedbackItem.toObject(); int fromId = int(feedbackItemObj.value(QStringLiteral("from_id")).toDouble()); int toId = int(feedbackItemObj.value(QStringLiteral("to_id")).toDouble()); UserProfile profile = findUserProfile(userProfiles, fromId); if (profile.uid != 0) { qCDebug(lcSocialPluginTrace) << "adding VK notification:" << type; m_db.addVKNotification(accountId, type, QString::number(fromId), profile.name(), profile.icon, QString::number(toId), timestamp); } else { qCDebug(lcSocialPlugin) << "no user profile found for owner" << fromId << "of notification from account" << accountId; } } } buteo-sync-plugins-social-0.4.28/src/vk/vk-notifications/vknotificationsyncadaptor.h000066400000000000000000000035621474572147200310150ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2013 Jolla Ltd. ** Contact: Chris Adams ** ****************************************************************************/ #ifndef VKNOTIFICATIONSYNCADAPTOR_H #define VKNOTIFICATIONSYNCADAPTOR_H #include "vkdatatypesyncadaptor.h" #include #include #include class Notification; class VKNotificationSyncAdaptor : public VKDataTypeSyncAdaptor { Q_OBJECT public: VKNotificationSyncAdaptor(QObject *parent); ~VKNotificationSyncAdaptor(); QString syncServiceName() const; protected: // implementing VKDataTypeSyncAdaptor interface void purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode mode); void beginSync(int accountId, const QString &accessToken); void finalize(int accountId); void retryThrottledRequest(const QString &request, const QVariantList &args, bool retryLimitReached); private: void requestNotifications(int accountId, const QString &accessToken, const QString &until = QString(), const QString &pagingToken = QString()); private Q_SLOTS: void finishedHandler(); private: void saveVKNotificationFromObject(int accountId, const QJsonObject ¬if, const QList &userProfiles); struct NotificationData { NotificationData() : accountId(0) {} NotificationData(int accountId, const QJsonObject ¬ification, const QJsonArray &profiles) : accountId(accountId), notification(notification), profiles(profiles) {} int accountId; QJsonObject notification; QJsonArray profiles; }; QList m_notificationsToAdd; VKNotificationsDatabase m_db; }; #endif // VKNOTIFICATIONSYNCADAPTOR_H buteo-sync-plugins-social-0.4.28/src/vk/vk-posts/000077500000000000000000000000001474572147200216365ustar00rootroot00000000000000buteo-sync-plugins-social-0.4.28/src/vk/vk-posts/vk-posts.pri000066400000000000000000000002611474572147200241370ustar00rootroot00000000000000PKGCONFIG += nemonotifications-qt5 Qt5Contacts qtcontacts-sqlite-qt5-extensions SOURCES += $$PWD/vkpostsyncadaptor.cpp HEADERS += $$PWD/vkpostsyncadaptor.h INCLUDEPATH += $$PWD buteo-sync-plugins-social-0.4.28/src/vk/vk-posts/vk-posts.pro000066400000000000000000000011111474572147200241400ustar00rootroot00000000000000TARGET = vk-posts-client DEFINES += SOCIALD_USE_QTPIM include($$PWD/../../common.pri) include($$PWD/../vk-common.pri) include($$PWD/vk-posts.pri) PKGCONFIG += mlite5 vk_posts_sync_profile.path = /etc/buteo/profiles/sync vk_posts_sync_profile.files = $$PWD/vk.Posts.xml vk_posts_client_plugin_xml.path = /etc/buteo/profiles/client vk_posts_client_plugin_xml.files = $$PWD/vk-posts.xml HEADERS += vkpostsplugin.h SOURCES += vkpostsplugin.cpp OTHER_FILES += \ vk.Posts.xml \ vk-posts.xml INSTALLS += \ target \ vk_posts_sync_profile \ vk_posts_client_plugin_xml buteo-sync-plugins-social-0.4.28/src/vk/vk-posts/vk-posts.xml000066400000000000000000000001771474572147200241530ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/vk/vk-posts/vk.Posts.xml000066400000000000000000000010771474572147200241140ustar00rootroot00000000000000 buteo-sync-plugins-social-0.4.28/src/vk/vk-posts/vkpostsplugin.cpp000066400000000000000000000023711474572147200252750ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2014 Jolla Ltd. ** Contact: Chris Adams ** ****************************************************************************/ #include "constants_p.h" #include #include #include "vkpostsplugin.h" #include "vkpostsyncadaptor.h" #include "socialnetworksyncadaptor.h" VKPostsPlugin::VKPostsPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *callbackInterface) : SocialdButeoPlugin(pluginName, profile, callbackInterface, QStringLiteral("vk"), SocialNetworkSyncAdaptor::dataTypeName(SocialNetworkSyncAdaptor::Posts)) { } VKPostsPlugin::~VKPostsPlugin() { } SocialNetworkSyncAdaptor *VKPostsPlugin::createSocialNetworkSyncAdaptor() { return new VKPostSyncAdaptor(this); } Buteo::ClientPlugin* VKPostsPluginLoader::createClientPlugin( const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) { return new VKPostsPlugin(pluginName, profile, cbInterface); } buteo-sync-plugins-social-0.4.28/src/vk/vk-posts/vkpostsplugin.h000066400000000000000000000022521474572147200247400ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2014 Jolla Ltd. ** Contact: Chris Adams ** ****************************************************************************/ #ifndef VKPOSTSPLUGIN_H #define VKPOSTSPLUGIN_H #include "socialdbuteoplugin.h" #include class Q_DECL_EXPORT VKPostsPlugin : public SocialdButeoPlugin { Q_OBJECT public: VKPostsPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface *cbInterface); ~VKPostsPlugin(); protected: SocialNetworkSyncAdaptor *createSocialNetworkSyncAdaptor(); }; class VKPostsPluginLoader : public Buteo::SyncPluginLoader { Q_OBJECT Q_PLUGIN_METADATA(IID "org.sailfishos.plugins.sync.VKPostsPluginLoader") Q_INTERFACES(Buteo::SyncPluginLoader) public: Buteo::ClientPlugin* createClientPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) override; }; #endif // VKPOSTSPLUGIN_H buteo-sync-plugins-social-0.4.28/src/vk/vk-posts/vkpostsyncadaptor.cpp000066400000000000000000000557241474572147200261550ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2014-15 Jolla Ltd. ** Contact: Bea Lam ** ****************************************************************************/ #include "vkpostsyncadaptor.h" #include "trace.h" #include "constants_p.h" #include #include #include #include #include #include #include #define SOCIALD_VK_POSTS_ID_PREFIX QStringLiteral("vk-posts-") #define SOCIALD_VK_POSTS_GROUPNAME QStringLiteral("vk") VKPostSyncAdaptor::VKPostSyncAdaptor(QObject *parent) : VKDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::Posts, parent) { setInitialActive(m_db.isValid()); } VKPostSyncAdaptor::~VKPostSyncAdaptor() { } QString VKPostSyncAdaptor::syncServiceName() const { return QStringLiteral("vk-microblog"); } void VKPostSyncAdaptor::sync(const QString &dataTypeString, int accountId) { // call superclass impl. VKDataTypeSyncAdaptor::sync(dataTypeString, accountId); } void VKPostSyncAdaptor::purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode) { m_db.removePosts(oldId); m_db.commit(); m_db.wait(); // social media feed UI caches feed images and maintains bindings between // source and cached image in SocialImageDatabase. // purge cached images belonging to this account. purgeCachedImages(&m_imageCacheDb, oldId); } void VKPostSyncAdaptor::beginSync(int accountId, const QString &accessToken) { qCDebug(lcSocialPlugin) << "beginning VK posts sync with account:" << accountId; requestPosts(accountId, accessToken); } void VKPostSyncAdaptor::finalize(int accountId) { if (syncAborted()) { qCDebug(lcSocialPlugin) << "sync aborted, skipping finalize of VK Posts from account:" << accountId; } else { qCDebug(lcSocialPlugin) << "finalizing VK posts sync with account:" << accountId; determineOptimalImageSize(); Q_FOREACH (const PostData &post, m_postsToAdd) { saveVKPostFromObject(post.accountId, post.post, post.userProfiles, post.groupProfiles); } Q_FOREACH (const PostData &photoPost, m_photoPostsToAdd) { saveVKPhotoPostFromObject(photoPost.accountId, photoPost.post, photoPost.userProfiles, photoPost.groupProfiles); } m_db.commit(); m_db.wait(); // manage image cache. Social media feed UI caches feed images // and maintains bindings between source and cached image in SocialImageDatabase. // purge cached images older than four weeks. purgeExpiredImages(&m_imageCacheDb, accountId); setLastSuccessfulSyncTime(accountId); } } void VKPostSyncAdaptor::retryThrottledRequest(const QString &request, const QVariantList &args, bool retryLimitReached) { int accountId = args[0].toInt(); if (retryLimitReached) { qCWarning(lcSocialPlugin) << "hit request retry limit! unable to request data from VK account with id" << accountId; setStatus(SocialNetworkSyncAdaptor::Error); } else { qCDebug(lcSocialPlugin) << "retrying Posts" << request << "request for VK account:" << accountId; requestPosts(accountId, args[1].toString()); } decrementSemaphore(accountId); // finished waiting for the request. } void VKPostSyncAdaptor::requestPosts(int accountId, const QString &accessToken) { QDateTime since = lastSuccessfulSyncTime(accountId); if (!since.isValid() || since > QDateTime::currentDateTimeUtc()) { int sinceSpan = m_accountSyncProfile ? m_accountSyncProfile->key(Buteo::KEY_SYNC_SINCE_DAYS_PAST, QStringLiteral("7")).toInt() : 7; since = QDateTime::currentDateTime().addDays(-1 * sinceSpan).toUTC(); } QList > queryItems; queryItems.append(QPair(QStringLiteral("access_token"), accessToken)); queryItems.append(QPair(QStringLiteral("extended"), QStringLiteral("1"))); queryItems.append(QPair(QStringLiteral("v"), QStringLiteral("5.37"))); // version queryItems.append(QPair(QStringLiteral("filters"), QStringLiteral("post,photo,photo_tag,wall_photo,note"))); queryItems.append(QPair(QStringLiteral("start_time"), QString::number(since.toTime_t()))); QUrl url(QStringLiteral("https://api.vk.com/method/newsfeed.get")); QUrlQuery query(url); query.setQueryItems(queryItems); url.setQuery(query); QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(url)); if (reply) { reply->setProperty("accountId", accountId); reply->setProperty("accessToken", accessToken); connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(errorHandler(QNetworkReply::NetworkError))); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsHandler(QList))); connect(reply, SIGNAL(finished()), this, SLOT(finishedPostsHandler())); // we're requesting data. Increment the semaphore so that we know we're still busy. incrementSemaphore(accountId); setupReplyTimeout(accountId, reply); } else { // request was throttled by VKNetworkAccessManager QVariantList args; args << accountId << accessToken; enqueueThrottledRequest(QStringLiteral("requestPosts"), args); // we are waiting to request data. Increment the semaphore so that we know we're still busy. incrementSemaphore(accountId); // decremented in retryThrottledRequest(). } } void VKPostSyncAdaptor::finishedPostsHandler() { QNetworkReply *reply = qobject_cast(sender()); bool isError = reply->property("isError").toBool(); int accountId = reply->property("accountId").toInt(); QString accessToken = reply->property("accessToken").toString(); QByteArray replyData = reply->readAll(); disconnect(reply); reply->deleteLater(); removeReplyTimeout(accountId, reply); bool ok = false; QJsonObject parsed = parseJsonObjectReplyData(replyData, &ok); if (!isError && ok && parsed.contains(QStringLiteral("response"))) { QJsonObject responseObj = parsed.value(QStringLiteral("response")).toObject(); QJsonArray items = responseObj.value(QStringLiteral("items")).toArray(); if (!items.size()) { qCDebug(lcSocialPlugin) << "no feed posts received for account:" << accountId; decrementSemaphore(accountId); return; } QJsonArray profileValues = responseObj.value(QStringLiteral("profiles")).toArray(); QList userProfiles; foreach (const QJsonValue &entry, profileValues) { UserProfile profile = UserProfile::fromJsonObject(entry.toObject()); userProfiles << profile; } QJsonArray groupValues = responseObj.value(QStringLiteral("groups")).toArray(); QList groupProfiles; foreach (const QJsonValue &entry, groupValues) { GroupProfile profile = GroupProfile::fromJsonObject(entry.toObject()); groupProfiles << profile; } foreach (const QJsonValue &entry, items) { QJsonObject object = entry.toObject(); if (!object.isEmpty()) { if (object.value(QStringLiteral("type")).toString() == QStringLiteral("post")) { m_postsToAdd.append(PostData(accountId, object, userProfiles, groupProfiles)); } else if (object.value(QStringLiteral("type")).toString() == QStringLiteral("photo")) { m_photoPostsToAdd.append(PostData(accountId, object, userProfiles, groupProfiles)); } else if (object.value(QStringLiteral("type")).toString() == QStringLiteral("photo_tag")) { qCDebug(lcSocialPlugin) << "TODO: unhandled newsfeed item type:" << object.value(QStringLiteral("type")).toString() << ", skipping."; } else if (object.value(QStringLiteral("type")).toString() == QStringLiteral("wall_photo")) { qCDebug(lcSocialPlugin) << "TODO: unhandled newsfeed item type:" << object.value(QStringLiteral("type")).toString() << ", skipping."; } else if (object.value(QStringLiteral("type")).toString() == QStringLiteral("note")) { qCDebug(lcSocialPlugin) << "TODO: unhandled newsfeed item type:" << object.value(QStringLiteral("type")).toString() << ", skipping."; } else { qCDebug(lcSocialPlugin) << "unhandled newsfeed item type:" << object.value(QStringLiteral("type")).toString() << ", skipping."; } } else { qCDebug(lcSocialPlugin) << "post object empty; skipping"; } } } else { QVariantList args; args << accountId << accessToken; if (enqueueServerThrottledRequestIfRequired(parsed, QStringLiteral("requestPosts"), args)) { // we hit the throttle limit, let throttle timer repeat the request. // don't decrement semaphore yet as we're still waiting for it. // it will be decremented in retryThrottledRequest(). return; } // error occurred during request. qCWarning(lcSocialPlugin) << "error: unable to parse event feed data from request with account" << accountId << "got:" << QString::fromUtf8(replyData); } // we're finished this request. Decrement our busy semaphore. decrementSemaphore(accountId); } void VKPostSyncAdaptor::saveVKPostFromObject(int accountId, const QJsonObject &post, const QList &userProfiles, const QList &groupProfiles) { bool hasValidContent = false; VKPostsDatabase::Post newPost; newPost.fromId = post.contains(QStringLiteral("from_id")) ? int(post.value(QStringLiteral("from_id")).toDouble()) : int(post.value(QStringLiteral("source_id")).toDouble()); newPost.toId = int(post.value(QStringLiteral("to_id")).toDouble()); newPost.postType = post.value(QStringLiteral("post_type")).toString(); newPost.replyOwnerId = int(post.value(QStringLiteral("reply_owner_id")).toDouble()); newPost.replyPostId = int(post.value(QStringLiteral("reply_post_id")).toDouble()); newPost.signerId = int(post.value(QStringLiteral("signer_id")).toDouble()); newPost.friendsOnly = int(post.value(QStringLiteral("friends_only")).toDouble()) == 1; QJsonObject commentInfo = post.value(QStringLiteral("comments")).toObject(); VKPostsDatabase::Comments comments; comments.count = int(commentInfo.value(QStringLiteral("count")).toDouble()); comments.userCanComment = int(commentInfo.value(QStringLiteral("count")).toDouble()) == 1; newPost.comments = comments; QJsonObject likeInfo = post.value(QStringLiteral("likes")).toObject(); VKPostsDatabase::Likes likes; likes.count = int(likeInfo.value(QStringLiteral("count")).toDouble()); likes.userLikes = int(likeInfo.value(QStringLiteral("user_likes")).toDouble()) == 1; likes.userCanLike = int(likeInfo.value(QStringLiteral("can_like")).toDouble()) == 1; likes.userCanPublish = int(likeInfo.value(QStringLiteral("can_publish")).toDouble()) == 1; newPost.likes = likes; QJsonObject repostInfo = post.value(QStringLiteral("reposts")).toObject(); VKPostsDatabase::Reposts reposts; reposts.count = int(repostInfo.value(QStringLiteral("count")).toDouble()); reposts.userReposted = int(repostInfo.value(QStringLiteral("user_reposted")).toDouble()) == 1; newPost.reposts = reposts; QJsonObject postSourceInfo = post.value(QStringLiteral("post_source")).toObject(); VKPostsDatabase::PostSource postSource; postSource.type = postSourceInfo.value(QStringLiteral("type")).toString(); postSource.data = postSourceInfo.value(QStringLiteral("data")).toString(); newPost.postSource = postSource; QList > images; QJsonArray attachmentsInfo = post.value(QStringLiteral("attachments")).toArray(); Q_FOREACH (const QJsonValue &attValue, attachmentsInfo) { QJsonObject attObject = attValue.toObject(); QString type = attObject.value(QStringLiteral("type")).toString(); QJsonObject typedValue = attObject.value(type).toObject(); if (type == QStringLiteral("photo") || type == QStringLiteral("posted_photo") || type == QStringLiteral("graffiti")) { QString src = typedValue.value(m_optimalImageSize).toString(); if (!src.isEmpty()) { images.append(qMakePair(src, SocialPostImage::Photo)); } hasValidContent = true; } } VKPostsDatabase::GeoLocation geo; QJsonObject geoInfo = post.value(QStringLiteral("geo")).toObject(); if (!geoInfo.isEmpty()) { geo.placeId = int(geoInfo.value(QStringLiteral("place_id")).toDouble()); geo.title = geoInfo.value(QStringLiteral("title")).toString(); geo.type = geoInfo.value(QStringLiteral("type")).toString(); geo.countryId = int(geoInfo.value(QStringLiteral("country_id")).toDouble()); geo.cityId = int(geoInfo.value(QStringLiteral("city_id")).toDouble()); geo.address = geoInfo.value(QStringLiteral("address")).toString(); geo.showMap = int(geoInfo.value(QStringLiteral("showmap")).toDouble()) == 1; // type???? newPost.geo = geo; } VKPostsDatabase::CopyPost copyPost; if (post.contains(QStringLiteral("copy_history")) && copyPost.type.isEmpty()) { QJsonArray copyHistory = post.value(QStringLiteral("copy_history")).toArray(); foreach (const QJsonValue historyValue, copyHistory) { QJsonObject object = historyValue.toObject(); QStringList keys = object.keys(); if (keys.contains(QStringLiteral("owner_id"))) { QStringList images; int ownerId = (int)object.value(QStringLiteral("owner_id")).toDouble(); if (ownerId != 0) { if (ownerId > 0) { const UserProfile &user(VKDataTypeSyncAdaptor::findUserProfile(userProfiles, ownerId)); copyPost.ownerName = user.name(); copyPost.ownerAvatar = user.icon; } else { // it was posted by a group const GroupProfile &group(VKDataTypeSyncAdaptor::findGroupProfile(groupProfiles, ownerId)); copyPost.ownerName = group.name; copyPost.ownerAvatar = group.icon; ownerId = -ownerId; } copyPost.ownerId = ownerId; copyPost.createdTime = VKDataTypeSyncAdaptor::parseVKDateTime(object.value(QStringLiteral("date"))); if (keys.contains(QStringLiteral("attachments"))) { QJsonArray attachments = object.value(QStringLiteral("attachments")).toArray(); foreach (const QJsonValue attachment, attachments) { QJsonObject attachmentObject = attachment.toObject(); copyPost.type = attachmentObject.value(QStringLiteral("type")).toString(); if (copyPost.type == QStringLiteral("photo")) { QJsonObject photoObject = attachmentObject.value(QStringLiteral("photo")).toObject(); QString photo = photoObject.value(m_optimalImageSize).toString(); if (!photo.isEmpty()) { images.append(photo); } } } } if (keys.contains(QStringLiteral("text"))) { copyPost.text = object.value(QStringLiteral("text")).toString(); hasValidContent = true; } if (images.count() > 0) { copyPost.photo = images.join(QStringLiteral(",")); hasValidContent = true; } } } } if (copyPost.type.isEmpty()) { copyPost.type = QStringLiteral("post"); } } newPost.copyPost = copyPost; QDateTime createdTime = VKDataTypeSyncAdaptor::parseVKDateTime(post.value(QStringLiteral("date"))); QString body = post.value(QStringLiteral("text")).toString(); if (!body.isEmpty()) { hasValidContent = true; } QString posterName, posterIcon, screenName; int fromId = newPost.fromId; if (newPost.fromId < 0) { // it was posted by a group const GroupProfile &group(VKDataTypeSyncAdaptor::findGroupProfile(groupProfiles, newPost.fromId)); posterName = group.name; posterIcon = group.icon; screenName = group.screenName; fromId = -fromId; } else { // it was posted by a user const UserProfile &user(VKDataTypeSyncAdaptor::findUserProfile(userProfiles, newPost.fromId)); posterName = user.name(); posterIcon = user.icon; } // VK post indentifier is just index number and not globally unique. To make // it unique we combine it with fromId QString identifier = QString::number(fromId) + QStringLiteral("_") + (post.contains(QStringLiteral("id")) ? QString::number(post.value(QStringLiteral("id")).toDouble()) : QString::number(post.value(QStringLiteral("post_id")).toDouble())); if (newPost.fromId < 0) { // construct group post url newPost.link = QStringLiteral("https://vk.com/%1?w=wall-%2").arg(screenName).arg(identifier); } else { // construct wall post url newPost.link = QStringLiteral("https://m.vk.com/wall") + identifier; } Q_FOREACH (const QString &line, body.split('\n')) { qCDebug(lcSocialPluginTrace) << line; } if (hasValidContent) { m_db.addVKPost(identifier, createdTime, body, newPost, images, posterName, posterIcon, accountId); } else { qCDebug(lcSocialPlugin) << "VK post without valid content, skipping"; } } void VKPostSyncAdaptor::saveVKPhotoPostFromObject(int accountId, const QJsonObject &post, const QList &userProfiles, const QList &groupProfiles) { bool hasValidContent = false; VKPostsDatabase::Post newPost; newPost.fromId = int(post.value(QStringLiteral("source_id")).toDouble()); int photoOwnerId = 0; int photoAlbumId = 0; QList > images; QJsonObject photosObject = post.value(QStringLiteral("photos")).toObject(); if (!photosObject.isEmpty()) { QJsonArray photoArray = photosObject.value(QStringLiteral("items")).toArray(); Q_FOREACH (const QJsonValue &photoValue, photoArray) { QJsonObject photo = photoValue.toObject(); QString src = photo.value(m_optimalImageSize).toString(); if (!src.isEmpty()) { photoOwnerId = (int)photo.value(QStringLiteral("owner_id")).toDouble(); photoAlbumId = (int)photo.value(QStringLiteral("album_id")).toDouble(); images.append(qMakePair(src, SocialPostImage::Photo)); hasValidContent = true; } } } QDateTime createdTime = VKDataTypeSyncAdaptor::parseVKDateTime(post.value(QStringLiteral("date"))); QString posterName, posterIcon; int fromId = newPost.fromId; if (newPost.fromId < 0) { // it was posted by a group const GroupProfile &group(VKDataTypeSyncAdaptor::findGroupProfile(groupProfiles, newPost.fromId)); posterName = group.name; posterIcon = group.icon; fromId = -fromId; } else { // it was posted by a user const UserProfile &user(VKDataTypeSyncAdaptor::findUserProfile(userProfiles, newPost.fromId)); posterName = user.name(); posterIcon = user.icon; } // VK post indentifier is just index number and not globally unique. To make // it unique we combine it with fromId QString identifier = QString::number(fromId) + QStringLiteral("_") + (post.contains(QStringLiteral("id")) ? QString::number((int)post.value(QStringLiteral("id")).toDouble()) : QString::number((int)post.value(QStringLiteral("post_id")).toDouble())); newPost.link = QStringLiteral("https://m.vk.com/album") + QString::number(photoOwnerId) + QStringLiteral("_") + QString::number(photoAlbumId); if (hasValidContent) { m_db.addVKPost(identifier, createdTime, QString(), newPost, images, posterName, posterIcon, accountId); } else { qCDebug(lcSocialPlugin) << "VK photo post without valid content, skipping"; } } void VKPostSyncAdaptor::determineOptimalImageSize() { int width = 0, height = 0; const int defaultValue = 0; MDConfItem widthConf("/lipstick/screen/primary/width"); if (widthConf.value(defaultValue).toInt() != defaultValue) { width = widthConf.value(defaultValue).toInt(); } MDConfItem heightConf("/lipstick/screen/primary/height"); if (heightConf.value(defaultValue).toInt() != defaultValue) { height = heightConf.value(defaultValue).toInt(); } // we want to use the largest of these dimensions as the "optimal" int maxDimension = qMax(width, height); if (maxDimension >= 2048) { m_optimalImageSize = "photo_1280"; } else if (maxDimension >= 960) { m_optimalImageSize = "photo_604"; } else { m_optimalImageSize = "photo_75"; } qCDebug(lcSocialPlugin) << "Determined optimal image size for dimension " << maxDimension << " as " << m_optimalImageSize; } // TODO: this is also in Facebook notifications adapter. Move to base class. QDateTime VKPostSyncAdaptor::lastSuccessfulSyncTime(int accountId) { QDateTime result; QString settingsFileName = QString::fromLatin1("%1/%2/vkposts.ini") .arg(PRIVILEGED_DATA_DIR) .arg(QString::fromLatin1(SYNC_DATABASE_DIR)); QSettings settingsFile(settingsFileName, QSettings::IniFormat); uint timestamp = settingsFile.value(QString::fromLatin1("%1-last-successful-sync-time").arg(accountId)).toUInt(); if (timestamp > 0) { result = QDateTime::fromTime_t(timestamp); } return result; } void VKPostSyncAdaptor::setLastSuccessfulSyncTime(int accountId) { QDateTime currentTime = QDateTime::currentDateTime().toUTC(); QString settingsFileName = QString::fromLatin1("%1/%2/vkposts.ini") .arg(PRIVILEGED_DATA_DIR) .arg(QString::fromLatin1(SYNC_DATABASE_DIR)); QSettings settingsFile(settingsFileName, QSettings::IniFormat); settingsFile.setValue(QString::fromLatin1("%1-last-successful-sync-time").arg(accountId), QVariant::fromValue(currentTime.toTime_t())); settingsFile.sync(); } buteo-sync-plugins-social-0.4.28/src/vk/vk-posts/vkpostsyncadaptor.h000066400000000000000000000047111474572147200256100ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2014-15 Jolla Ltd. ** Contact: Bea Lam ** ****************************************************************************/ #ifndef VKPOSTSYNCADAPTOR_H #define VKPOSTSYNCADAPTOR_H #include "vkdatatypesyncadaptor.h" #include #include #include #include #include #include #include #include #include class VKPostSyncAdaptor : public VKDataTypeSyncAdaptor { Q_OBJECT public: VKPostSyncAdaptor(QObject *parent); ~VKPostSyncAdaptor(); QString syncServiceName() const; void sync(const QString &dataTypeString, int accountId = 0); protected: // implementing VKDataTypeSyncAdaptor interface void purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode mode); void beginSync(int accountId, const QString &accessToken); void finalize(int accountId); void retryThrottledRequest(const QString &request, const QVariantList &args, bool retryLimitReached); private: void requestPosts(int accountId, const QString &accessToken); void determineOptimalImageSize(); QDateTime lastSuccessfulSyncTime(int accountId); void setLastSuccessfulSyncTime(int accountId); private Q_SLOTS: void finishedPostsHandler(); private: void saveVKPostFromObject(int accountId, const QJsonObject &post, const QList &userProfiles, const QList &groupProfiles); void saveVKPhotoPostFromObject(int accountId, const QJsonObject &post, const QList &userProfiles, const QList &groupProfiles); struct PostData { PostData() : accountId(0) {} PostData(int accountId, const QJsonObject &object, const QList &userProfiles, const QList &groupProfiles) : accountId(accountId), post(object) , userProfiles(userProfiles), groupProfiles(groupProfiles) {} int accountId; QJsonObject post; QList userProfiles; QList groupProfiles; }; QList m_postsToAdd; QList m_photoPostsToAdd; VKPostsDatabase m_db; QString m_optimalImageSize; SocialImagesDatabase m_imageCacheDb; }; #endif // VKPOSTSYNCADAPTOR_H buteo-sync-plugins-social-0.4.28/src/vk/vk.pro000066400000000000000000000002261474572147200212120ustar00rootroot00000000000000TEMPLATE = subdirs SUBDIRS = \ $$PWD/vk-posts \ $$PWD/vk-notifications \ $$PWD/vk-calendars \ $$PWD/vk-contacts \ $$PWD/vk-images buteo-sync-plugins-social-0.4.28/src/vk/vkdatatypesyncadaptor.cpp000066400000000000000000000363521474572147200252110ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2013 Jolla Ltd. ** Contact: Chris Adams ** ****************************************************************************/ #include "vkdatatypesyncadaptor.h" #include "vknetworkaccessmanager_p.h" #include "trace.h" #include #include #include #include #include //libsailfishkeyprovider #include //libaccounts-qt #include #include #include #include //libsignon-qt #include #include //----------------- VKDataTypeSyncAdaptor::UserProfile::UserProfile() { } VKDataTypeSyncAdaptor::UserProfile::~UserProfile() { } VKDataTypeSyncAdaptor::UserProfile::UserProfile(const UserProfile &other) { operator=(other); } VKDataTypeSyncAdaptor::UserProfile &VKDataTypeSyncAdaptor::UserProfile::operator=(const UserProfile &other) { if (&other == this) { return *this; } uid = other.uid; firstName = other.firstName; lastName = other.lastName; icon = other.icon; return *this; } VKDataTypeSyncAdaptor::UserProfile VKDataTypeSyncAdaptor::UserProfile::fromJsonObject(const QJsonObject &object) { UserProfile user; user.uid = int(object.value(QStringLiteral("id")).toDouble()); user.firstName = object.value(QStringLiteral("first_name")).toString(); user.lastName = object.value(QStringLiteral("last_name")).toString(); user.icon = object.value(QStringLiteral("photo_50")).toString(); return user; } QString VKDataTypeSyncAdaptor::UserProfile::name() const { // TODO locale-specific joining of names QString personName; if (!firstName.isEmpty()) { personName += firstName; } if (!lastName.isEmpty()) { if (!firstName.isEmpty()) { personName += ' '; } personName += lastName; } return personName; } //----------------- VKDataTypeSyncAdaptor::GroupProfile::GroupProfile() { } VKDataTypeSyncAdaptor::GroupProfile::~GroupProfile() { } VKDataTypeSyncAdaptor::GroupProfile::GroupProfile(const GroupProfile &other) { operator=(other); } VKDataTypeSyncAdaptor::GroupProfile &VKDataTypeSyncAdaptor::GroupProfile::operator=(const GroupProfile &other) { if (&other == this) { return *this; } uid = other.uid; name = other.name; screenName = other.screenName; icon = other.icon; return *this; } VKDataTypeSyncAdaptor::GroupProfile VKDataTypeSyncAdaptor::GroupProfile::fromJsonObject(const QJsonObject &object) { GroupProfile user; user.uid = int(object.value(QStringLiteral("id")).toDouble()); user.name = object.value(QStringLiteral("name")).toString(); user.screenName = object.value(QStringLiteral("screen_name")).toString(); user.icon = object.value(QStringLiteral("photo_50")).toString(); return user; } //----------------- VKDataTypeSyncAdaptor::UserProfile VKDataTypeSyncAdaptor::findUserProfile(const QList &profiles, int uid) { Q_FOREACH (const UserProfile &user, profiles) { if (user.uid == uid) { return user; } } return UserProfile(); } VKDataTypeSyncAdaptor::GroupProfile VKDataTypeSyncAdaptor::findGroupProfile(const QList &profiles, int uid) { // sometimes the uid is given as negative to signify a group, eg in newsfeed.get int positiveUid = uid < 0 ? uid*-1 : uid; Q_FOREACH (const GroupProfile &group, profiles) { if (group.uid == positiveUid) { return group; } } return GroupProfile(); } //----------------- QDateTime VKDataTypeSyncAdaptor::parseVKDateTime(const QJsonValue &v) { if (v.type() != QJsonValue::Double) { return QDateTime(); } int t = int(v.toDouble()); return QDateTime::fromTime_t(t); } VKDataTypeSyncAdaptor::VKDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::DataType dataType, QObject *parent) : SocialNetworkSyncAdaptor("vk", dataType, new VKNetworkAccessManager, parent), m_triedLoading(false) { m_throttleTimer.setSingleShot(true); connect(&m_throttleTimer, &QTimer::timeout, this, &VKDataTypeSyncAdaptor::throttleTimerTimeout); } VKDataTypeSyncAdaptor::~VKDataTypeSyncAdaptor() { } void VKDataTypeSyncAdaptor::enqueueThrottledRequest(const QString &request, const QVariantList &args, int interval) { m_throttledRequestQueue.append(qMakePair(request, args)); if (!m_throttleTimer.isActive() || m_throttleTimer.interval() < interval) { // start the timer if it is inactive, or if we are requested to // enqueue a request with a larger interval (e.g., if the server // throttled us, hence we are using VK_THROTTLE_EXTRA_INTERVAL). m_throttleTimer.setInterval(interval ? interval : VK_THROTTLE_INTERVAL); m_throttleTimer.start(); } } bool VKDataTypeSyncAdaptor::enqueueServerThrottledRequestIfRequired(const QJsonObject &parsed, const QString &request, const QVariantList &args) { if (parsed.contains(QLatin1String("error"))) { QJsonObject error = parsed.value(QLatin1String("error")).toObject(); int errorCode = error.value(QLatin1String("error_code")).toInt(); if (errorCode == VK_THROTTLE_ERROR_CODE) { // we have hit the server rate limit. // wait a few of seconds and try again. qCDebug(lcSocialPlugin) << "VK server rate limit exceeded, start throttle timer"; enqueueThrottledRequest(request, args, VK_THROTTLE_EXTRA_INTERVAL); return true; } } return false; } void VKDataTypeSyncAdaptor::throttleTimerTimeout() { if (m_throttledRequestQueue.isEmpty()) { return; } QPair request = m_throttledRequestQueue.takeFirst(); static int totalRetryCount; totalRetryCount += 1; bool retryLimitReached = totalRetryCount > VK_THROTTLE_RETRY_LIMIT; // even if the retry limit has been reached, we still call the derived-type function. // this is because they may have special handling (e.g., cleanup / error conditions). retryThrottledRequest(request.first, request.second, retryLimitReached); // we still handle every queued request even if the limit has been reached, as each // request will have a semaphore associated with it which will need to be decremented. if (!m_throttledRequestQueue.isEmpty()) { m_throttleTimer.setInterval(retryLimitReached ? 0 : VK_THROTTLE_INTERVAL); m_throttleTimer.start(); } } void VKDataTypeSyncAdaptor::sync(const QString &dataTypeString, int accountId) { if (dataTypeString != SocialNetworkSyncAdaptor::dataTypeName(m_dataType)) { qCWarning(lcSocialPlugin) << "VK" << SocialNetworkSyncAdaptor::dataTypeName(m_dataType) << "sync adaptor was asked to sync" << dataTypeString; setStatus(SocialNetworkSyncAdaptor::Error); return; } if (clientId().isEmpty()) { qCWarning(lcSocialPlugin) << "clientId could not be retrieved for VK account" << accountId; setStatus(SocialNetworkSyncAdaptor::Error); return; } setStatus(SocialNetworkSyncAdaptor::Busy); updateDataForAccount(accountId); qCDebug(lcSocialPlugin) << "successfully triggered sync with profile:" << m_accountSyncProfile->name(); } void VKDataTypeSyncAdaptor::updateDataForAccount(int accountId) { Accounts::Account *account = Accounts::Account::fromId(m_accountManager, accountId, this); if (!account) { qCWarning(lcSocialPlugin) << "existing account with id" << accountId << "couldn't be retrieved"; setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(accountId); return; } // will be decremented by either signOnError or signOnResponse. incrementSemaphore(accountId); signIn(account); } void VKDataTypeSyncAdaptor::errorHandler(QNetworkReply::NetworkError err) { QNetworkReply *reply = qobject_cast(sender()); QByteArray replyData = reply->readAll(); int accountId = reply->property("accountId").toInt(); qCWarning(lcSocialPlugin) << SocialNetworkSyncAdaptor::dataTypeName(m_dataType) << "request with account" << accountId << "experienced error:" << err << "HTTP:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); // set "isError" on the reply so that adapters know to ignore the result in the finished() handler reply->setProperty("isError", QVariant::fromValue(true)); // Note: not all errors are "unrecoverable" errors, so we don't change the status here. bool ok = false; QJsonObject parsed = parseJsonObjectReplyData(replyData, &ok); if (ok && parsed.contains(QLatin1String("error"))) { QJsonObject errorReply = parsed.value("error").toObject(); // Password Changed on server side if (errorReply.value("code").toDouble() == 190 && errorReply.value("error_subcode").toDouble() == 460) { int accountId = reply->property("accountId").toInt(); Accounts::Account *account = Accounts::Account::fromId(m_accountManager, accountId, this); if (account) { setCredentialsNeedUpdate(account); } } } } void VKDataTypeSyncAdaptor::sslErrorsHandler(const QList &errs) { QString sslerrs; foreach (const QSslError &e, errs) { sslerrs += e.errorString() + "; "; } if (errs.size() > 0) { sslerrs.chop(2); } qCWarning(lcSocialPlugin) << SocialNetworkSyncAdaptor::dataTypeName(m_dataType) << "request with account" << sender()->property("accountId").toInt() << "experienced ssl errors:" << sslerrs; // set "isError" on the reply so that adapters know to ignore the result in the finished() handler sender()->setProperty("isError", QVariant::fromValue(true)); // Note: not all errors are "unrecoverable" errors, so we don't change the status here. } QString VKDataTypeSyncAdaptor::clientId() { if (!m_triedLoading) { loadClientId(); } return m_clientId; } void VKDataTypeSyncAdaptor::loadClientId() { m_triedLoading = true; char *cClientId = NULL; int cSuccess = SailfishKeyProvider_storedKey("vk", "vk-sync", "client_id", &cClientId); if (cSuccess != 0 || cClientId == NULL) { return; } m_clientId = QLatin1String(cClientId); free(cClientId); return; } void VKDataTypeSyncAdaptor::setCredentialsNeedUpdate(Accounts::Account *account) { qCInfo(lcSocialPlugin) << "sociald:VKontakte: setting CredentialsNeedUpdate to true for account:" << account->id(); Accounts::Service srv(m_accountManager->service(syncServiceName())); account->selectService(srv); account->setValue(QStringLiteral("CredentialsNeedUpdate"), QVariant::fromValue(true)); account->setValue(QStringLiteral("CredentialsNeedUpdateFrom"), QVariant::fromValue(QString::fromLatin1("sociald-vkontakte"))); account->selectService(Accounts::Service()); account->syncAndBlock(); } void VKDataTypeSyncAdaptor::signIn(Accounts::Account *account) { // Fetch clientId from keyprovider int accountId = account->id(); if (!checkAccount(account) || clientId().isEmpty()) { decrementSemaphore(accountId); return; } // grab out a valid identity for the sync service. Accounts::Service srv(m_accountManager->service(syncServiceName())); account->selectService(srv); SignOn::Identity *identity = account->credentialsId() > 0 ? SignOn::Identity::existingIdentity(account->credentialsId()) : 0; if (!identity) { qCWarning(lcSocialPlugin) << "error: account has no valid credentials, cannot sign in:" << accountId; decrementSemaphore(accountId); return; } Accounts::AccountService accSrv(account, srv); QString method = accSrv.authData().method(); QString mechanism = accSrv.authData().mechanism(); SignOn::AuthSession *session = identity->createSession(method); if (!session) { qCWarning(lcSocialPlugin) << "error: could not create signon session for account:" << accountId; identity->deleteLater(); decrementSemaphore(accountId); return; } QVariantMap signonSessionData = accSrv.authData().parameters(); signonSessionData.insert("ClientId", clientId()); signonSessionData.insert("UiPolicy", SignOn::NoUserInteractionPolicy); connect(session, SIGNAL(response(SignOn::SessionData)), this, SLOT(signOnResponse(SignOn::SessionData)), Qt::UniqueConnection); connect(session, SIGNAL(error(SignOn::Error)), this, SLOT(signOnError(SignOn::Error)), Qt::UniqueConnection); session->setProperty("account", QVariant::fromValue(account)); session->setProperty("identity", QVariant::fromValue(identity)); session->process(SignOn::SessionData(signonSessionData), mechanism); } void VKDataTypeSyncAdaptor::signOnError(const SignOn::Error &error) { SignOn::AuthSession *session = qobject_cast(sender()); Accounts::Account *account = session->property("account").value(); SignOn::Identity *identity = session->property("identity").value(); int accountId = account->id(); qCWarning(lcSocialPlugin) << "credentials for account with id" << accountId << "couldn't be retrieved:" << error.type() << "," << error.message(); // if the error is because credentials have expired, we // set the CredentialsNeedUpdate key. if (error.type() == SignOn::Error::UserInteraction) { setCredentialsNeedUpdate(account); } session->disconnect(this); identity->destroySession(session); identity->deleteLater(); account->deleteLater(); // if we couldn't sign in, we can't sync with this account. setStatus(SocialNetworkSyncAdaptor::Error); decrementSemaphore(accountId); } void VKDataTypeSyncAdaptor::signOnResponse(const SignOn::SessionData &responseData) { QVariantMap data; foreach (const QString &key, responseData.propertyNames()) { data.insert(key, responseData.getProperty(key)); } QString accessToken; SignOn::AuthSession *session = qobject_cast(sender()); Accounts::Account *account = session->property("account").value(); SignOn::Identity *identity = session->property("identity").value(); int accountId = account->id(); if (data.contains(QLatin1String("AccessToken"))) { accessToken = data.value(QLatin1String("AccessToken")).toString(); } else { qCInfo(lcSocialPlugin) << "signon response for account with id" << accountId << "contained no oauth token"; } session->disconnect(this); identity->destroySession(session); identity->deleteLater(); account->deleteLater(); if (!accessToken.isEmpty()) { beginSync(accountId, accessToken); // call the derived-class sync entrypoint. } decrementSemaphore(accountId); } buteo-sync-plugins-social-0.4.28/src/vk/vkdatatypesyncadaptor.h000066400000000000000000000064471474572147200246600ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2013 Jolla Ltd. ** Contact: Chris Adams ** ****************************************************************************/ #ifndef VKDATATYPESYNCADAPTOR_H #define VKDATATYPESYNCADAPTOR_H #include "socialnetworksyncadaptor.h" #include #include #include #include #include #include namespace Accounts { class Account; } namespace SignOn { class Error; class SessionData; } class QJsonObject; /* Abstract interface for all of the data-specific sync adaptors which pull data from the VK social network. */ class VKDataTypeSyncAdaptor : public SocialNetworkSyncAdaptor { Q_OBJECT public: class UserProfile { public: UserProfile(); ~UserProfile(); UserProfile(const UserProfile &other); UserProfile &operator=(const UserProfile &other); static UserProfile fromJsonObject(const QJsonObject &object); QString name() const; int uid; QString firstName; QString lastName; QString icon; }; class GroupProfile { public: GroupProfile(); ~GroupProfile(); GroupProfile(const GroupProfile &other); GroupProfile &operator=(const GroupProfile &other); static GroupProfile fromJsonObject(const QJsonObject &object); int uid; QString name; QString screenName; QString icon; }; VKDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::DataType dataType, QObject *parent); virtual ~VKDataTypeSyncAdaptor(); virtual void sync(const QString &dataTypeString, int accountId); protected: QString clientId(); virtual void updateDataForAccount(int accountId); virtual void beginSync(int accountId, const QString &accessToken) = 0; static QDateTime parseVKDateTime(const QJsonValue &v); static UserProfile findUserProfile(const QList &profiles, int uid); static GroupProfile findGroupProfile(const QList &profiles, int uid); void enqueueThrottledRequest(const QString &request, const QVariantList &args, int interval = 0); bool enqueueServerThrottledRequestIfRequired(const QJsonObject &parsed, const QString &request, const QVariantList &args); virtual void retryThrottledRequest(const QString &request, const QVariantList &args, bool retryLimitReached) = 0; protected Q_SLOTS: virtual void errorHandler(QNetworkReply::NetworkError err); virtual void sslErrorsHandler(const QList &errs); private Q_SLOTS: void signOnError(const SignOn::Error &error); void signOnResponse(const SignOn::SessionData &responseData); void throttleTimerTimeout(); private: void loadClientId(); void setCredentialsNeedUpdate(Accounts::Account *account); void signIn(Accounts::Account *account); bool m_triedLoading; // Is true if we tried to load (even if we failed) QString m_clientId; QTimer m_throttleTimer; QList > m_throttledRequestQueue; }; #endif // VKDATATYPESYNCADAPTOR_H buteo-sync-plugins-social-0.4.28/src/vk/vknetworkaccessmanager.cpp000066400000000000000000000071071474572147200253300ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015 Jolla Ltd. ** Contact: Chris Adams ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #include "vknetworkaccessmanager_p.h" #include "buteosyncfw_p.h" #include "trace.h" #include #include #include #include #include #include #include #include #include #include namespace { bool touchTimestampFile() { static const QString timestampFileName = QString::fromLatin1("%1/%2/vktimestamp") .arg(PRIVILEGED_DATA_DIR) .arg(QString::fromLatin1(SYNC_DATABASE_DIR)); QByteArray tsfnba = timestampFileName.toUtf8(); int fd = open(tsfnba.constData(), O_WRONLY|O_CREAT|O_NOCTTY|O_NONBLOCK, 0666); if (fd < 0) { return false; } int rv = utimensat(AT_FDCWD, tsfnba.constData(), 0, 0); close(fd); if (rv != 0) { return false; } return true; } qint64 readTimestampFile() { static const QString timestampFileName = QString::fromLatin1("%1/%2/vktimestamp") .arg(PRIVILEGED_DATA_DIR) .arg(QString::fromLatin1(SYNC_DATABASE_DIR)); QByteArray tsfnba = timestampFileName.toUtf8(); struct stat buf; if (stat(tsfnba.constData(), &buf) < 0) { return 0; } time_t tvsec = buf.st_mtim.tv_sec; long nanosec = buf.st_mtim.tv_nsec; qint64 msecs = (tvsec*1000) + (nanosec/1000000); return msecs; } } VKNetworkAccessManager::VKNetworkAccessManager(QObject *parent) : SocialdNetworkAccessManager(parent) { } QNetworkReply *VKNetworkAccessManager::createRequest( QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *outgoingData) { // VK throttles requests. We want to wait at least 550 ms between each request. // To do this properly, we need to protect the file access with a semaphore // or link-lock to prevent concurrent process access. For now, we use the // naive approach. qint64 currTime = QDateTime::currentDateTimeUtc().toMSecsSinceEpoch(); qint64 lastRequestTime = readTimestampFile(); qint64 delta = currTime - lastRequestTime; if (lastRequestTime == 0 || delta > VK_THROTTLE_INTERVAL) { touchTimestampFile(); return SocialdNetworkAccessManager::createRequest(op, req, outgoingData); } qCDebug(lcSocialPlugin) << "Throttling request! lastRequestTime:" << lastRequestTime << ", currTime:" << currTime << ", so delta:" << delta; return 0; // tell the client to resubmit their request, it was throttled. } buteo-sync-plugins-social-0.4.28/src/vk/vknetworkaccessmanager_p.h000066400000000000000000000031411474572147200253060ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015 Jolla Ltd. ** Contact: Chris Adams ** ** This program/library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License ** version 2.1 as published by the Free Software Foundation. ** ** This program/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 program/library; if not, write to the Free ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA ** 02110-1301 USA ** ****************************************************************************/ #ifndef SOCIALD_VK_QNAMFACTORY_P_H #define SOCIALD_VK_QNAMFACTORY_P_H #include "socialdnetworkaccessmanager_p.h" #define VK_THROTTLE_INTERVAL 550 /* msec */ #define VK_THROTTLE_EXTRA_INTERVAL 3000 /* msec */ #define VK_THROTTLE_ERROR_CODE 6 #define VK_THROTTLE_RETRY_LIMIT 30 class VKNetworkAccessManager : public SocialdNetworkAccessManager { Q_OBJECT public: VKNetworkAccessManager(QObject *parent = 0); protected: QNetworkReply *createRequest(QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *outgoingData = 0); }; #endif // SOCIALD_VK_QNAMFACTORY_P_H