├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── LICENSE ├── README.md ├── VERSION ├── doc ├── groupchat.png ├── instant_message.png └── reply.png ├── pixmaps └── pidgin │ └── protocols │ ├── 16 │ └── signal.png │ ├── 22 │ └── signal.png │ └── 48 │ └── signal.png └── src ├── CMakeLists.txt ├── admin.c ├── admin.h ├── attachments.c ├── attachments.h ├── comms.c ├── comms.h ├── contacts.c ├── contacts.h ├── defines.h ├── groups.c ├── groups.h ├── input.c ├── input.h ├── interface.c ├── interface.h ├── json-utils.h ├── libsignald.c ├── libsignald.h ├── link.c ├── link.h ├── login.c ├── login.h ├── message.c ├── message.h ├── options.c ├── options.h ├── purple_compat.h ├── receipt.c ├── receipt.h ├── reply.c ├── reply.h ├── signald_procmgmt.c ├── signald_procmgmt.h ├── status.c ├── status.h └── structs.h /.gitignore: -------------------------------------------------------------------------------- 1 | config 2 | ignore 3 | build 4 | *.so 5 | *.o 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "submodules/MegaMimes"] 2 | path = submodules/MegaMimes 3 | url = https://github.com/trumpowen/MegaMimes 4 | [submodule "submodules/QR-Code-generator"] 5 | path = submodules/QR-Code-generator 6 | url = https://github.com/nayuki/QR-Code-generator.git 7 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) # lowest version this was tested with 2 | 3 | project("purple-signald" LANGUAGES C) 4 | 5 | find_package(PkgConfig QUIET) 6 | if (${PKG_CONFIG_FOUND}) 7 | pkg_check_modules(PURPLE REQUIRED purple) 8 | pkg_get_variable(PURPLE_PLUGIN_DIR purple plugindir) 9 | pkg_get_variable(PURPLE_DATA_DIR purple datarootdir) 10 | pkg_check_modules(JSON REQUIRED json-glib-1.0) 11 | pkg_check_modules(PIXBUF gdk-pixbuf-2.0) 12 | elseif(${Purple_FOUND}) 13 | message(STATUS "Purple was configured manually. Proceeding without checks.") 14 | else() 15 | message(FATAL "pkg-config not found. Please configure manually and set Purple_FOUND to YES.") 16 | endif() 17 | find_package(Threads REQUIRED) 18 | 19 | add_subdirectory(src) 20 | 21 | install(DIRECTORY "pixmaps" DESTINATION "${PURPLE_DATA_DIR}" FILES_MATCHING PATTERN "*.png") 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WARNING 2 | 3 | Currently, due to issues within signald, incoming messages can get lost. Since the issue has been persisting for months, the author of purple-signald developed an alternative: 4 | 5 | → [purple-presage](https://github.com/hoehermann/purple-presage) 6 | 7 | ## purple-signald 8 | 9 | A libpurple/Pidgin plugin for [signald](https://gitlab.com/signald/signald) (signal, formerly textsecure). 10 | 11 | signald is written by Finn Herzfeld. 12 | 13 | An unofficial IRC channel exists on Libera.chat called `##purple-signald` for those who use it. 14 | 15 | ![Instant Message](/doc/instant_message.png?raw=true "Instant Message Screenshot") 16 | 17 | ### Features 18 | 19 | * Core features: 20 | 21 | * Receive messages 22 | * Send messages 23 | 24 | * Additional features: 25 | 26 | * Emoji reactions are displayed. 27 | * Messages can be marked as "read". 28 | * Stickers can be displayed if GDK headers were available at build-time and a [GDK webp pixbuf loader](https://github.com/aruiz/webp-pixbuf-loader) is present in the system at run-time. Stickers are not animated. 29 | * It is possible to leave a Signal group by leaving the Pidgin chat (close the window) after removing it from the Pidgin buddy list. 30 | * The plug-in can cache a user-defined number of incoming messages so you can reply to them by starting the message with "@needle:" (read "at character followed by a text followed by a colon"). The most recent cached message containing the needle will be replied to. 31 | ![Reply](/doc/reply.png?raw=true "Screenshot showcasing reply feature") 32 | 33 | * Additional features contributed by [Hermann Kraus](https://github.com/herm/): 34 | 35 | * Receive files 36 | * Receive images 37 | * Send images 38 | * Receive buddy list from server 39 | 40 | Note: When signald is being run as a system service, downloaded files may not be accessible directly to the user. Do not forget to add yourself to the `signald` group. 41 | 42 | * Additional features contributed by [Torsten](https://github.com/ttlmax/): 43 | 44 | * Link with the master device 45 | * Automatically start signald 46 | Note: For automatically starting signald as a child proces, `signald` needs to be in `$PATH`. 47 | * Group Avatars 48 | 49 | * Additional features contributed by [Brett Kosinski](https://github.com/fancypantalons/): 50 | 51 | * Fine-grained attachment handling (for bitlbee). 52 | 53 | ### Known Issues 54 | 55 | * Sometimes, group chats are added to the buddy list more than once. 56 | * In group chats, on outgoing messages the sender name may have a different color than displayed in the list of chat participants. 57 | * When using send acknowledgements, the text is displayed "as transmitted" rather than "as typed". 58 | * Sending out read receipts on group chats do not work util the list of participants has been loaded. This usually affects only the first message of a chat. 59 | * Read receipts of messages sent to groups are displayed in the receivers' conversation – and only if the conversation is currently active. 60 | * After linking, contacts are not synced and may appear offline. Reconnecting helps. 61 | * After a disconnect, Pidgin will not re-join group chats (must close window and re-join). 62 | 63 | These issues may or may not be worked on since they are hard to reproduce, non-trivial to resolve, and/or not a big problem. Open an issue if they impede your experience. 64 | 65 | ### Missing Features 66 | 67 | * signald configuration 68 | * Sending Mentions 69 | * Registering a new number 70 | * Deleting buddies from the server 71 | * Updating contact details 72 | * Contact colors 73 | * Expiring messages 74 | * Support for old-style groups 75 | 76 | ### Security Considerations 77 | 78 | UUIDs are used for local file access. If someone manages to forge UUIDs, bypassing all checks in Signal and signald, the wrong local files might be accessed, but I do not see that happening realistically. 79 | 80 | ### Building the plug-in 81 | 82 | sudo apt install libpurple-dev libjson-glib-dev 83 | git clone --recurse-submodules https://github.com/hoehermann/purple-signald.git purple-signald 84 | mkdir -p purple-signald/build 85 | cd purple-signald/build 86 | cmake .. 87 | make 88 | sudo make install 89 | 90 | ### Getting Started with signald 91 | 92 | 1. Install [signald](https://gitlab.com/signald/signald). 93 | 1. Add your user to the `signald` group: `sudo usermod -a -G signald $USER` 94 | 1. Logout and log-in again. Or restart your computer just to be sure. Alternatively, `su` to the current account again. The reason is that `adduser` does not change existing sessions, and *only within the su shell* you're in a new session, and you need to be in the `signald` group. 95 | 1. Restart Pidgin 96 | 1. Add your a new account by selecting "signald" as the protocol. For the username, you *must* enter your full international telephone number formatted like `+12223334444`. Alternatively, you may enter your UUID. 97 | 1. Scan the generated QR code with signal on your phone to link your account. The dialog tells you where to find this option. 98 | 1. Chat someone up using your phone to verify it's working. 99 | 1. In case it is not working, you can unlink the plug-in "device" via your main device. The plug-in will ask to scan the QR code again. In extreme cases, you may need to remove the account from purple and signald manually. The latter can be achieved via `signaldctl account delete +12223334444`. 100 | 101 | ### Working with Bitlbee 102 | 103 | Note: Compatibility with Bitlbee has been provided by contributors. The main author does not offer direct support. 104 | 105 | Setup the account first as described above. Once that is successful, in the `&root` channel of Bitlbee, add the same phone number you authenticated via Signald: 106 | ``` 107 | account add hehoe-signald +12223334444 108 | rename _12223334444 name-sig 109 | account hehoe-signald set tag signal 110 | account signal set auto-accept-invitations true 111 | account signal set nick_format %full_name-sig 112 | account signal on 113 | ``` 114 | To create a channel for Signal, auto join it and generally manage your contacts see - https://wiki.bitlbee.org/ManagingContactList 115 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.14.0 -------------------------------------------------------------------------------- /doc/groupchat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoehermann/purple-signald/aa48b7e681d1bac7cb0069bffebb278d95a3044b/doc/groupchat.png -------------------------------------------------------------------------------- /doc/instant_message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoehermann/purple-signald/aa48b7e681d1bac7cb0069bffebb278d95a3044b/doc/instant_message.png -------------------------------------------------------------------------------- /doc/reply.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoehermann/purple-signald/aa48b7e681d1bac7cb0069bffebb278d95a3044b/doc/reply.png -------------------------------------------------------------------------------- /pixmaps/pidgin/protocols/16/signal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoehermann/purple-signald/aa48b7e681d1bac7cb0069bffebb278d95a3044b/pixmaps/pidgin/protocols/16/signal.png -------------------------------------------------------------------------------- /pixmaps/pidgin/protocols/22/signal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoehermann/purple-signald/aa48b7e681d1bac7cb0069bffebb278d95a3044b/pixmaps/pidgin/protocols/22/signal.png -------------------------------------------------------------------------------- /pixmaps/pidgin/protocols/48/signal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoehermann/purple-signald/aa48b7e681d1bac7cb0069bffebb278d95a3044b/pixmaps/pidgin/protocols/48/signal.png -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(TARGET_NAME signald) 2 | add_library(${TARGET_NAME} SHARED 3 | attachments.c 4 | comms.c 5 | contacts.c 6 | groups.c 7 | libsignald.c 8 | link.c 9 | login.c 10 | message.c 11 | signald_procmgmt.c 12 | attachments.h 13 | comms.h 14 | contacts.h 15 | groups.h 16 | link.h 17 | login.h 18 | message.h 19 | purple_compat.h 20 | signald_procmgmt.h 21 | input.h 22 | input.c 23 | options.h 24 | options.c 25 | receipt.h 26 | receipt.c 27 | defines.h 28 | structs.h 29 | admin.h 30 | admin.c 31 | status.c 32 | status.h 33 | interface.c 34 | interface.h 35 | reply.h 36 | reply.c 37 | json-utils.h 38 | ../submodules/MegaMimes/src/MegaMimes.c 39 | ../submodules/QR-Code-generator/c/qrcodegen.c 40 | ) 41 | 42 | file(READ "${CMAKE_SOURCE_DIR}/VERSION" PLUGIN_VERSION) 43 | target_compile_definitions(${TARGET_NAME} PRIVATE SIGNALD_PLUGIN_VERSION="${PLUGIN_VERSION}") 44 | target_include_directories(${TARGET_NAME} PRIVATE ${PURPLE_INCLUDE_DIRS} ${JSON_INCLUDE_DIRS} ${PIXBUF_INCLUDE_DIRS} ../submodules/MegaMimes/src/ ../submodules/QR-Code-generator/c/) 45 | target_link_libraries(${TARGET_NAME} PRIVATE ${PURPLE_LIBRARIES} ${JSON_LIBRARIES} ${PIXBUF_LIBRARIES} Threads::Threads) 46 | set_target_properties(${TARGET_NAME} PROPERTIES PREFIX "lib") 47 | install(TARGETS ${TARGET_NAME} DESTINATION "${PURPLE_PLUGIN_DIR}") 48 | -------------------------------------------------------------------------------- /src/admin.c: -------------------------------------------------------------------------------- 1 | #include "admin.h" 2 | #include "comms.h" 3 | 4 | #include 5 | 6 | void 7 | signald_request_sync(SignaldAccount *sa) 8 | { 9 | g_return_if_fail(sa->uuid); 10 | 11 | JsonObject *data = json_object_new(); 12 | 13 | json_object_set_string_member(data, "type", "request_sync"); 14 | json_object_set_string_member(data, "account", sa->uuid); 15 | json_object_set_boolean_member(data, "contacts", TRUE); 16 | json_object_set_boolean_member(data, "groups", TRUE); 17 | json_object_set_boolean_member(data, "configuration", FALSE); 18 | json_object_set_boolean_member(data, "blocked", FALSE); 19 | 20 | signald_send_json_or_display_error(sa, data); 21 | 22 | json_object_unref(data); 23 | } 24 | -------------------------------------------------------------------------------- /src/admin.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "structs.h" 4 | 5 | void signald_request_sync(SignaldAccount *sa); 6 | -------------------------------------------------------------------------------- /src/attachments.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "defines.h" 4 | #include "structs.h" 5 | #include "attachments.h" 6 | #include 7 | 8 | #if !(GLIB_CHECK_VERSION(2, 67, 3)) 9 | #define g_memdup2 g_memdup 10 | #endif 11 | 12 | #if __has_include("gdk-pixbuf/gdk-pixbuf.h") 13 | #include 14 | static int 15 | pixbuf_format_mimetype_comparator(GdkPixbufFormat *format, const char *type) { 16 | int cmp = 1; 17 | gchar **mime_types = gdk_pixbuf_format_get_mime_types(format); 18 | for (gchar **mime_type = mime_types; mime_type != NULL && *mime_type != NULL && cmp != 0; mime_type++) { 19 | cmp = g_strcmp0(type, *mime_type); 20 | } 21 | g_strfreev(mime_types); 22 | return cmp; 23 | } 24 | 25 | static gboolean 26 | is_loadable_image_mimetype(const char *mimetype) { 27 | // check if mimetype is among the formats supported by pixbuf 28 | GSList *pixbuf_formats = gdk_pixbuf_get_formats(); 29 | GSList *pixbuf_format = g_slist_find_custom(pixbuf_formats, mimetype, (GCompareFunc)pixbuf_format_mimetype_comparator); 30 | g_slist_free(pixbuf_formats); 31 | return pixbuf_format != NULL; 32 | } 33 | #else 34 | static gboolean 35 | is_loadable_image_mimetype(const char *mimetype) { 36 | // blindly assume frontend can handle jpeg and png 37 | return purple_strequal(mimetype, "image/jpeg") || purple_strequal(mimetype, "image/png"); 38 | } 39 | #endif 40 | 41 | int 42 | signald_get_external_attachment_settings(SignaldAccount *sa, const char **path, const char **url) 43 | { 44 | *path = purple_account_get_string(sa->account, SIGNALD_ACCOUNT_OPT_EXT_ATTACHMENTS_DIR, ""); 45 | *url = purple_account_get_string(sa->account, SIGNALD_ACCOUNT_OPT_EXT_ATTACHMENTS_URL, ""); 46 | 47 | if (strlen(*path) == 0) { 48 | purple_debug_error(SIGNALD_PLUGIN_ID, "External attachments configured but no attachment path set."); 49 | 50 | return -1; 51 | } 52 | 53 | GFile *f = g_file_new_for_path(*path); 54 | GFileType type = g_file_query_file_type(f, G_FILE_QUERY_INFO_NONE, NULL); 55 | 56 | g_object_unref(f); 57 | 58 | if (type != G_FILE_TYPE_DIRECTORY) { 59 | purple_debug_error(SIGNALD_PLUGIN_ID, "External attachments path is not a valid directory: '%s'", *path); 60 | 61 | return -1; 62 | } 63 | 64 | if (strlen(*url) == 0) { 65 | purple_debug_error(SIGNALD_PLUGIN_ID, "External attachments configured but no attachment url set."); 66 | 67 | return -1; 68 | } 69 | 70 | return 0; 71 | } 72 | 73 | void 74 | signald_parse_attachment(SignaldAccount *sa, JsonObject *obj, GString *message) 75 | { 76 | const char *type = json_object_get_string_member(obj, "contentType"); 77 | const char *fn = json_object_get_string_member(obj, "storedFilename"); 78 | 79 | if (purple_account_get_bool(sa->account, SIGNALD_ACCOUNT_OPT_EXT_ATTACHMENTS, FALSE)) { 80 | gchar *url = signald_write_external_attachment(sa, fn, type); 81 | 82 | if (url != NULL) { 83 | g_string_append_printf(message, "Attachment (type %s): %s
", url, type, url); 84 | g_free(url); 85 | } else { 86 | g_string_append_printf(message, "An error occurred processing an attachment. Enable debug logging for more information."); 87 | } 88 | 89 | return; 90 | } 91 | 92 | if (is_loadable_image_mimetype(type)) { 93 | PurpleStoredImage *img = purple_imgstore_new_from_file(fn); // TODO: forward "access denied" error to UI 94 | size_t size = purple_imgstore_get_size(img); 95 | int img_id = purple_imgstore_add_with_id(g_memdup2(purple_imgstore_get_data(img), size), size, NULL); 96 | 97 | g_string_append_printf(message, "
", img_id); 98 | g_string_append_printf(message, "Image (type: %s)
", fn, type); 99 | } else { 100 | //TODO: Receive file using libpurple's file transfer API 101 | g_string_append_printf(message, "Attachment (type: %s)
", fn, type); 102 | } 103 | 104 | purple_debug_info(SIGNALD_PLUGIN_ID, "Attachment: %s", message->str); 105 | } 106 | 107 | GString * 108 | signald_prepare_attachments_message(SignaldAccount *sa, JsonObject *obj) { 109 | GString *attachments_message = g_string_new(""); 110 | 111 | if (json_object_has_member(obj, "attachments")) { 112 | JsonArray *attachments = json_object_get_array_member(obj, "attachments"); 113 | guint len = json_array_get_length(attachments); 114 | for (guint i=0; i < len; i++) { 115 | signald_parse_attachment(sa, json_array_get_object_element(attachments, i), attachments_message); 116 | } 117 | } 118 | 119 | return attachments_message; 120 | } 121 | 122 | // Search for embedded images and save them to files. 123 | // Remove the tags. 124 | char * 125 | signald_detach_images(const char *message, JsonArray *attachments) { 126 | GString *msg = g_string_new(""); // this shall hold the actual message body (without the tags) 127 | GData *attribs; 128 | const char *start, *end, *last; 129 | 130 | last = message; 131 | 132 | /* for each valid IMG tag... */ 133 | while (last && *last && purple_markup_find_tag("img", last, &start, &end, &attribs)) 134 | { 135 | PurpleStoredImage *image = NULL; 136 | const char *id; 137 | 138 | if (start - last) { 139 | g_string_append_len(msg, last, start - last); 140 | } 141 | 142 | id = g_datalist_get_data(&attribs, "id"); 143 | 144 | /* ... if it refers to a valid purple image ... */ 145 | if (id && (image = purple_imgstore_find_by_id(atoi(id)))) { 146 | unsigned long size = purple_imgstore_get_size(image); 147 | gconstpointer imgdata = purple_imgstore_get_data(image); 148 | gchar *tmp_fn = NULL; 149 | GError *error = NULL; 150 | //TODO: This is not very secure. But attachment handling should be reworked in signald to allow sending them in the same stream as the message 151 | //Signal requires the filename to end with a known image extension. However it does not care if the extension matches the image format. 152 | //contentType is ignored completely. 153 | //https://gitlab.com/thefinn93/signald/issues/11 154 | 155 | gint file = g_file_open_tmp("XXXXXX.png", &tmp_fn, &error); 156 | if (file == -1) { 157 | purple_debug_error(SIGNALD_PLUGIN_ID, "Error: %s\n", error->message); 158 | // TODO: show this error to the user 159 | } else { 160 | close(file); // will be re-opened by g_file_set_contents 161 | error = NULL; 162 | if (!g_file_set_contents(tmp_fn, imgdata, size, &error)) { 163 | purple_debug_error(SIGNALD_PLUGIN_ID, "Error: %s\n", error->message); 164 | } else { 165 | chmod(tmp_fn, 0644); 166 | JsonObject *attachment = json_object_new(); 167 | json_object_set_string_member(attachment, "filename", tmp_fn); 168 | // json_object_set_string_member(attachment, "caption", "Caption"); 169 | // json_object_set_string_member(attachment, "contentType", "image/png"); 170 | // json_object_set_int_member(attachment, "width", 150); 171 | // json_object_set_int_member(attachment, "height", 150); 172 | json_array_add_object_element(attachments, attachment); 173 | } 174 | g_free(tmp_fn); 175 | //TODO: Check for memory leaks 176 | //TODO: Delete file when response from signald is received 177 | } 178 | } 179 | /* If the tag is invalid, skip it, thus no else here */ 180 | 181 | g_datalist_clear(&attribs); 182 | 183 | /* continue from the end of the tag */ 184 | last = end + 1; 185 | } 186 | 187 | /* append any remaining message data */ 188 | if (last && *last) { 189 | g_string_append(msg, last); 190 | } 191 | 192 | return g_string_free(msg, FALSE); 193 | } 194 | 195 | gchar * 196 | signald_write_external_attachment(SignaldAccount *sa, const char *filename, const char *mimetype_remote) 197 | { 198 | const char *path; 199 | const char *baseurl; 200 | gchar *url = NULL; 201 | 202 | if (signald_get_external_attachment_settings(sa, &path, &baseurl) != 0) { 203 | return NULL; 204 | } 205 | 206 | GFile *f = g_file_new_for_path(filename); 207 | GFileType type = g_file_query_file_type(f, G_FILE_QUERY_INFO_NONE, NULL); 208 | 209 | g_object_unref(f); 210 | 211 | if (type == G_FILE_TYPE_UNKNOWN) { 212 | purple_debug_error(SIGNALD_PLUGIN_ID, "Error accessing file (permission issue?)"); 213 | 214 | return NULL; 215 | } else if (type != G_FILE_TYPE_REGULAR) { 216 | purple_debug_error(SIGNALD_PLUGIN_ID, "File is not a regular file... that's odd."); 217 | 218 | return NULL; 219 | } 220 | 221 | GFile *source = g_file_new_for_path(filename); 222 | char *basename = g_file_get_basename(source); // TODO: stickers are using "hash/id" – do not simply use the basename or stickers will get overwritten 223 | 224 | gchar * ext = "unknown"; 225 | char ** extensions = (char **)getMegaMimeExtensions(mimetype_remote); 226 | if (extensions && extensions[0]) { 227 | ext = extensions[0]+2; 228 | } else { 229 | purple_debug_error(SIGNALD_PLUGIN_ID, "Sender supplied mime-type %s. No extensions are known for this mime-type.", mimetype_remote); 230 | } 231 | gchar *destpath = g_strconcat(path, "/", basename, ".", ext, NULL); 232 | 233 | GFile *destination = g_file_new_for_path(destpath); 234 | 235 | purple_debug_info(SIGNALD_PLUGIN_ID, "Copying attachment from '%s' to '%s'...\n", filename, destpath); 236 | 237 | GError *gerror = NULL; 238 | if (g_file_copy(source, 239 | destination, 240 | G_FILE_COPY_NONE, 241 | NULL /* cancellable */, 242 | NULL /* progress cb */, 243 | NULL /* progress cb data */, 244 | &gerror)) { 245 | 246 | url = g_strconcat(baseurl, "/", basename, ".", ext, NULL); 247 | } else { 248 | // TODO: print this in conversation window 249 | purple_debug_error(SIGNALD_PLUGIN_ID, "Error saving attachment to '%s': %s\n", destpath, gerror->message); 250 | 251 | g_error_free(gerror); 252 | } 253 | 254 | g_object_unref(source); 255 | g_object_unref(destination); 256 | 257 | g_free(destpath); 258 | 259 | freeMegaStringArray(extensions); 260 | 261 | return url; 262 | } 263 | -------------------------------------------------------------------------------- /src/attachments.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | GString * 6 | signald_prepare_attachments_message(SignaldAccount *sa, JsonObject *obj); 7 | 8 | void 9 | signald_parse_attachment(SignaldAccount *sa, JsonObject *obj, GString *message); 10 | 11 | char * 12 | signald_detach_images(const char *message, JsonArray *attachments); 13 | 14 | gchar * 15 | signald_write_external_attachment(SignaldAccount *sa, const char *filename, const char *mimetype_remote); 16 | -------------------------------------------------------------------------------- /src/comms.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include // for socket and read 3 | #include "purple_compat.h" 4 | #include "structs.h" 5 | #include "defines.h" 6 | #include "comms.h" 7 | #include "input.h" 8 | #include 9 | 10 | /* 11 | * Implements the read callback. 12 | * Called when data has been sent by signald and is ready to be handled. 13 | */ 14 | void 15 | signald_read_cb(gpointer data, gint source, PurpleInputCondition cond) 16 | { 17 | SignaldAccount *sa = data; 18 | // this function essentially just reads bytes into a buffer until a newline is reached 19 | // apparently, this callback is executed every 8k butes. therefore, input_buffer must be persistent accross calls 20 | // using getline would be cool, but I do not want to find out what happens if I wrap this fd into a FILE* while the purple handle is connected to it 21 | gssize read = recv(sa->fd, sa->input_buffer_position, 1, sa->readflags); // read one byte at a time (sometimes blocking according to sa->readflags) 22 | while (read > 0) { 23 | sa->input_buffer_position += read; 24 | if(sa->input_buffer_position[-1] == '\n') { 25 | *sa->input_buffer_position = 0; 26 | purple_debug_info(SIGNALD_PLUGIN_ID, "got newline delimited message: %s", sa->input_buffer); 27 | signald_parse_input(sa, sa->input_buffer, sa->input_buffer_position - sa->input_buffer - 1); 28 | // reset buffer write pointer 29 | sa->input_buffer_position = sa->input_buffer; 30 | } 31 | if (sa->input_buffer_position - sa->input_buffer + 1 == SIGNALD_INPUT_BUFSIZE) { 32 | purple_connection_error(sa->pc, PURPLE_CONNECTION_ERROR_NETWORK_ERROR, "message exceeded buffer size"); 33 | // reset buffer write pointer 34 | // should not have any effect since the connection will be destroyed, but better safe than sorry 35 | sa->input_buffer_position = sa->input_buffer; 36 | return; 37 | } 38 | read = recv(sa->fd, sa->input_buffer_position, 1, MSG_DONTWAIT); // try to read another byte (continue the while loop) 39 | } 40 | if (read == 0) { 41 | purple_connection_error(sa->pc, PURPLE_CONNECTION_ERROR_NETWORK_ERROR, "Connection to signald lost."); 42 | } 43 | if (read < 0) { 44 | if ((errno == EAGAIN) || (errno == EWOULDBLOCK)) { 45 | // assume the message is complete and was probably handled 46 | } else { 47 | // TODO: error out? 48 | purple_debug_error(SIGNALD_PLUGIN_ID, "recv error is %s\n",strerror(errno)); 49 | return; 50 | } 51 | } 52 | } 53 | 54 | gboolean 55 | signald_send_str(SignaldAccount *sa, char *s) 56 | { 57 | int l = strlen(s); 58 | int w = write(sa->fd, s, l); 59 | if (w != l) { 60 | purple_debug_info(SIGNALD_PLUGIN_ID, "wrote %d, wanted %d, error is %s\n", w, l, strerror(errno)); 61 | return 0; 62 | } 63 | return 1; 64 | } 65 | 66 | gboolean 67 | signald_send_json(SignaldAccount *sa, JsonObject *data) 68 | { 69 | // Set version to v1 70 | json_object_set_string_member(data, "version", "v1"); 71 | 72 | gboolean success; 73 | char *json = json_object_to_string(data); 74 | purple_debug_info(SIGNALD_PLUGIN_ID, "Sending: %s\n", json); 75 | success = signald_send_str(sa, json); 76 | if (success) { 77 | success = signald_send_str(sa, "\n"); 78 | } 79 | g_free(json); 80 | return success; 81 | } 82 | 83 | gboolean 84 | signald_send_json_or_display_error(SignaldAccount *sa, JsonObject *data) 85 | { 86 | if (!signald_send_json(sa, data)) { 87 | const gchar *type = json_object_get_string_member(data, "type"); 88 | char *error_message = g_strdup_printf("Could not write %s message.", type); 89 | purple_connection_error(sa->pc, PURPLE_CONNECTION_ERROR_NETWORK_ERROR, error_message); 90 | g_free(error_message); 91 | } 92 | } 93 | 94 | gchar * 95 | json_object_to_string(JsonObject *obj) 96 | { 97 | JsonNode *node; 98 | gchar *str; 99 | JsonGenerator *generator; 100 | 101 | node = json_node_new(JSON_NODE_OBJECT); 102 | json_node_set_object(node, obj); 103 | 104 | // a json string ... 105 | generator = json_generator_new(); 106 | json_generator_set_root(generator, node); 107 | str = json_generator_to_data(generator, NULL); 108 | g_object_unref(generator); 109 | json_node_free(node); 110 | 111 | return str; 112 | } 113 | -------------------------------------------------------------------------------- /src/comms.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | gchar * 7 | json_object_to_string(JsonObject *obj); 8 | 9 | gboolean 10 | signald_send_json(SignaldAccount *sa, JsonObject *data); 11 | 12 | gboolean 13 | signald_send_json_or_display_error(SignaldAccount *sa, JsonObject *data); 14 | 15 | void 16 | signald_read_cb(gpointer data, gint source, PurpleInputCondition cond); 17 | -------------------------------------------------------------------------------- /src/contacts.c: -------------------------------------------------------------------------------- 1 | #include "contacts.h" 2 | #include "defines.h" 3 | #include "comms.h" 4 | #include "json-utils.h" 5 | #include "groups.h" 6 | 7 | void 8 | signald_assume_buddy_state(PurpleAccount *account, PurpleBuddy *buddy) 9 | { 10 | g_return_if_fail(buddy != NULL); 11 | 12 | const gchar *status_str = purple_account_get_string(account, "fake-status", SIGNALD_STATUS_STR_ONLINE); 13 | purple_prpl_got_user_status(account, buddy->name, status_str, NULL); 14 | purple_prpl_got_user_status(account, buddy->name, SIGNALD_STATUS_STR_MOBILE, NULL); 15 | } 16 | 17 | void 18 | signald_assume_all_buddies_state(SignaldAccount *sa) 19 | { 20 | GSList *buddies = purple_find_buddies(sa->account, NULL); 21 | while (buddies != NULL) { 22 | signald_assume_buddy_state(sa->account, buddies->data); 23 | buddies = g_slist_delete_link(buddies, buddies); 24 | } 25 | } 26 | 27 | static void 28 | signald_add_purple_buddy(SignaldAccount *sa, const char *number, const char *name, const char *uuid, const char *avatar) 29 | { 30 | g_return_if_fail(uuid && uuid[0]); 31 | 32 | const char *alias = NULL; 33 | // try number for an alias 34 | if (number && number[0]) { 35 | alias = number; 36 | } 37 | // prefer name over number 38 | if (name && name[0]) { 39 | alias = name; 40 | } 41 | // special case: contact to self 42 | if (!alias && purple_strequal(sa->uuid, uuid)) { 43 | alias = purple_account_get_alias(sa->account); 44 | if (!alias) { 45 | alias = purple_account_get_username(sa->account); 46 | } 47 | } 48 | 49 | // default: buddy identified by UUID 50 | PurpleBuddy *buddy = purple_find_buddy(sa->account, uuid); 51 | 52 | // however, ... 53 | if (number && number[0]) { 54 | // ...if the contact's number is known... 55 | PurpleBuddy *number_buddy = purple_find_buddy(sa->account, number); 56 | if (number_buddy) { 57 | // ...and the number identifies a buddy... 58 | purple_blist_rename_buddy(number_buddy, uuid); // rename (not alias) the buddy 59 | // remove superflous UUID from the buddy if set 60 | gpointer data = purple_buddy_get_protocol_data(buddy); 61 | if (data) { 62 | g_free(data); 63 | purple_buddy_set_protocol_data(number_buddy, NULL); 64 | } 65 | buddy = number_buddy; // continue with the renamed buddy 66 | purple_debug_info(SIGNALD_PLUGIN_ID, "Migrated %s to %s.\n", number, uuid); 67 | } 68 | } 69 | 70 | if (!buddy) { 71 | // new buddy 72 | PurpleGroup *g = purple_find_group(SIGNAL_DEFAULT_GROUP); 73 | if (!g) { 74 | g = purple_group_new(SIGNAL_DEFAULT_GROUP); 75 | purple_blist_add_group(g, NULL); 76 | } 77 | buddy = purple_buddy_new(sa->account, uuid, alias); 78 | purple_blist_add_buddy(buddy, NULL, g, NULL); 79 | signald_assume_buddy_state(sa->account, buddy); 80 | } 81 | if (number && number[0]) { 82 | // add/update number 83 | // NOTE: the number is currently never used except for displaying in the buddy list tooltip text 84 | purple_buddy_set_protocol_data(buddy, g_strdup(number)); 85 | } 86 | if (alias) { 87 | //purple_blist_alias_buddy(buddy, alias); // this overrides the alias set by the local user 88 | serv_got_alias(sa->pc, uuid, alias); 89 | } 90 | 91 | // Set or update avatar 92 | gchar* imgdata = NULL; 93 | gsize imglen = 0; 94 | if (avatar && avatar[0] && g_file_get_contents(avatar, &imgdata, &imglen, NULL)) { 95 | purple_buddy_icons_set_for_user(sa->account, uuid, imgdata, imglen, NULL); 96 | } 97 | } 98 | 99 | void 100 | signald_process_contact(SignaldAccount *sa, JsonNode *node) 101 | { 102 | JsonObject *obj = json_node_get_object(node); 103 | const char *name = json_object_get_string_member_or_null(obj, "name"); 104 | if (name == NULL || name[0] == 0) { 105 | name = json_object_get_string_member_or_null(obj, "contact_name"); 106 | } 107 | if (name == NULL || name[0] == 0) { 108 | name = json_object_get_string_member_or_null(obj, "profile_name"); 109 | } 110 | const char *avatar = json_object_get_string_member_or_null(obj, "avatar"); 111 | JsonObject *address = json_object_get_object_member(obj, "address"); 112 | const char *number = json_object_get_string_member_or_null(address, "number"); 113 | const char *uuid = json_object_get_string_member(address, "uuid"); 114 | signald_add_purple_buddy(sa, number, name, uuid, avatar); 115 | } 116 | 117 | void 118 | signald_parse_contact_list(SignaldAccount *sa, JsonArray *profiles) 119 | { 120 | for (guint i = 0; i < json_array_get_length(profiles); i++) { 121 | signald_process_contact(sa, json_array_get_element(profiles, i)); 122 | } 123 | //TODO: mark buddies not in contact list but in buddy list as "deleted" 124 | } 125 | 126 | /* 127 | * Purple UI function: Request information about a contact for showing it to the user. 128 | * 129 | * See @signald_request_profile for details. 130 | */ 131 | void signald_get_info(PurpleConnection *pc, const char *who) { 132 | SignaldAccount *sa = purple_connection_get_protocol_data(pc); 133 | sa->show_profile = g_strdup(who); 134 | signald_request_profile(pc, who); 135 | } 136 | 137 | /* 138 | * Request information about a contact for internal use (e.g. display of friendly name in group chat). 139 | * 140 | * See @signald_process_profile on how the answer is used. 141 | */ 142 | void signald_request_profile(PurpleConnection *pc, const char *who) { 143 | SignaldAccount *sa = purple_connection_get_protocol_data(pc); 144 | g_return_if_fail(sa->uuid); 145 | JsonObject *data = json_object_new(); 146 | json_object_set_string_member(data, "type", "get_profile"); 147 | json_object_set_string_member(data, "account", sa->uuid); 148 | JsonObject *address = json_object_new(); 149 | json_object_set_string_member(address, "uuid", who); 150 | json_object_set_object_member(data, "address", address); 151 | signald_send_json_or_display_error(sa, data); 152 | json_object_unref(data); 153 | } 154 | 155 | /* 156 | * Helper function for @signald_process_profile. 157 | */ 158 | static void 159 | signald_process_profile_info_member(JsonObject *object, const gchar *member_name, JsonNode *member_node, gpointer user_data) { 160 | if (JSON_NODE_HOLDS_OBJECT(member_node)) { 161 | JsonObject *obj = json_node_get_object(member_node); 162 | json_object_foreach_member(obj, signald_process_profile_info_member, user_data); 163 | } 164 | if (JSON_NODE_HOLDS_VALUE(member_node)) { 165 | PurpleNotifyUserInfo *user_info = user_data; 166 | GValue value = G_VALUE_INIT; 167 | json_node_get_value(member_node, &value); 168 | GValue string = G_VALUE_INIT; 169 | g_value_init(&string, G_TYPE_STRING); 170 | g_value_transform(&value, &string); 171 | purple_notify_user_info_add_pair_plaintext(user_info, member_name, g_value_get_string(&string)); 172 | } 173 | } 174 | 175 | /* 176 | * Process contact profile information. 177 | * 178 | * May be either displayed to the user via @signald_show_profile or used to update non-buddy group chat participant name, see @signald_update_participant_name. 179 | */ 180 | void signald_process_profile(SignaldAccount *sa, JsonObject *obj) { 181 | JsonObject *address = json_object_get_object_member(obj, "address"); 182 | g_return_if_fail(address); 183 | const char *uuid = json_object_get_string_member(address, "uuid"); 184 | g_return_if_fail(uuid && uuid[0]); 185 | 186 | if (sa->show_profile) { 187 | g_free(sa->show_profile); 188 | sa->show_profile = NULL; 189 | signald_show_profile(sa->pc, uuid, obj); 190 | } else { 191 | signald_update_participant_name(uuid, obj); 192 | } 193 | } 194 | 195 | /* 196 | * Recursively flattens profile information into a sequence of key-value pairs for information display. 197 | */ 198 | void signald_show_profile(PurpleConnection *pc, const char *uuid, JsonObject *obj) { 199 | PurpleNotifyUserInfo *user_info = purple_notify_user_info_new(); 200 | json_object_foreach_member(obj, signald_process_profile_info_member, (gpointer) user_info); 201 | purple_notify_userinfo(pc, uuid, user_info, NULL, NULL); 202 | purple_notify_user_info_destroy(user_info); 203 | } 204 | 205 | void 206 | signald_add_buddy(PurpleConnection *pc, PurpleBuddy *buddy, PurpleGroup *group) 207 | { 208 | SignaldAccount *sa = purple_connection_get_protocol_data(pc); 209 | signald_assume_buddy_state(sa->account, buddy); 210 | // does not actually do anything. buddy is added to pidgin's local list and is usable from there. 211 | // TODO: if buddy name is a number (very likely), try to get their uuid 212 | } 213 | 214 | void 215 | signald_list_contacts(SignaldAccount *sa) 216 | { 217 | g_return_if_fail(sa->uuid); 218 | 219 | JsonObject *data = json_object_new(); 220 | 221 | json_object_set_string_member(data, "type", "list_contacts"); 222 | json_object_set_string_member(data, "account", sa->uuid); 223 | 224 | signald_send_json_or_display_error(sa, data); 225 | 226 | json_object_unref(data); 227 | 228 | signald_assume_all_buddies_state(sa); 229 | } 230 | 231 | -------------------------------------------------------------------------------- /src/contacts.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "structs.h" 4 | #include 5 | 6 | void signald_assume_all_buddies_state(SignaldAccount *sa); 7 | 8 | void signald_parse_contact_list(SignaldAccount *sa, JsonArray *profiles); 9 | 10 | void signald_get_info(PurpleConnection *pc, const char *who); 11 | 12 | void signald_request_profile(PurpleConnection *pc, const char *who); 13 | 14 | void signald_process_profile(SignaldAccount *sa, JsonObject *obj); 15 | 16 | void signald_show_profile(PurpleConnection *pc, const char *uuid, JsonObject *obj); 17 | 18 | void signald_add_buddy(PurpleConnection *pc, PurpleBuddy *buddy, PurpleGroup *group); 19 | 20 | void signald_list_contacts(SignaldAccount *sa); 21 | -------------------------------------------------------------------------------- /src/defines.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define SIGNALD_PLUGIN_ID "prpl-hehoe-signald" 4 | #ifndef SIGNALD_PLUGIN_VERSION 5 | #error Must set SIGNALD_PLUGIN_VERSION in Makefile 6 | #endif 7 | #define SIGNALD_PLUGIN_WEBSITE "https://github.com/hoehermann/libpurple-signald" 8 | #define SIGNAL_DEFAULT_GROUP "Signal" 9 | 10 | #define SIGNALD_DIALOG_TITLE "Signal Protocol" 11 | #define SIGNALD_DIALOG_LINK "Link to Signal App" 12 | #define SIGNALD_DEFAULT_DEVICENAME "Signal-Purple-Plugin" // must fit in HOST_NAME_MAX 13 | 14 | #define SIGNALD_TIMEOUT_SECONDS 10 15 | #define SIGNALD_GLOBAL_SOCKET_FILE "signald/signald.sock" 16 | #define SIGNALD_GLOBAL_SOCKET_PATH_VAR "/var/run" 17 | 18 | #define SIGNALD_DATA_PATH "%s/signald" 19 | #define SIGNALD_AVATARS_SIGNALD_DATA_PATH "/avatars/" 20 | #define SIGNALD_AVATAR_FILE_NAME "contact-%s" 21 | #define SIGNALD_PID_FILE SIGNALD_DATA_PATH "/pid" 22 | 23 | #define SIGNALD_STATUS_STR_ONLINE "online" 24 | #define SIGNALD_STATUS_STR_AWAY "away" 25 | #define SIGNALD_STATUS_STR_OFFLINE "offline" 26 | #define SIGNALD_STATUS_STR_MOBILE "mobile" 27 | 28 | #define SIGNALD_ACCOUNT_OPT_EXT_ATTACHMENTS "external-attachments" 29 | #define SIGNALD_ACCOUNT_OPT_EXT_ATTACHMENTS_DIR "external-attachments-dir" 30 | #define SIGNALD_ACCOUNT_OPT_EXT_ATTACHMENTS_URL "external-attachments-url" 31 | 32 | #define SIGNALD_OPTION_WAIT_SEND_ACKNOWLEDEMENT "wait-send-acknowledgement" 33 | #define SIGNALD_OPTION_MARK_READ "mark-read" 34 | #define SIGNALD_OPTION_DISPLAY_RECEIPTS "display-receipts" 35 | #define SIGNALD_OPTION_REPLY_CACHE "reply-cache-capacity" 36 | -------------------------------------------------------------------------------- /src/groups.c: -------------------------------------------------------------------------------- 1 | #include "groups.h" 2 | #include "purple_compat.h" 3 | #include "defines.h" 4 | #include "comms.h" 5 | #include "message.h" 6 | #include "json-utils.h" 7 | #include "contacts.h" 8 | 9 | PurpleGroup * signald_get_purple_group() { 10 | PurpleGroup *group = purple_blist_find_group("Signal"); 11 | if (!group) { 12 | group = purple_group_new("Signal"); 13 | purple_blist_add_group(group, NULL); 14 | } 15 | return group; 16 | } 17 | 18 | /* 19 | * Add group chat to blist. Updates existing group chat if found. 20 | */ 21 | PurpleChat * signald_ensure_group_chat_in_blist( 22 | PurpleAccount *account, const char *groupId, const char *title, const char *avatar 23 | ) { 24 | gboolean fetch_contacts = TRUE; 25 | 26 | PurpleChat *chat = purple_blist_find_chat(account, groupId); 27 | 28 | if (chat == NULL && fetch_contacts) { 29 | GHashTable *comp = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, g_free); 30 | g_hash_table_insert(comp, "name", g_strdup(groupId)); 31 | g_hash_table_insert(comp, "title", g_strdup(title)); 32 | chat = purple_chat_new(account, groupId, comp); 33 | PurpleGroup *group = signald_get_purple_group(); 34 | purple_blist_add_chat(chat, group, NULL); 35 | // TODO: find out if purple_serv_got_joined_chat(gc, …) should be called 36 | } 37 | 38 | // if gtk-persistent is set, the user may close the gtk conversation window without leaving the chat 39 | // however, the window remains hidden even if new messages arrive 40 | //purple_blist_node_set_bool(&chat->node, "gtk-persistent", TRUE); 41 | 42 | if (title != NULL && fetch_contacts) { 43 | purple_blist_alias_chat(chat, title); 44 | } 45 | 46 | // set or update avatar 47 | if ((avatar != NULL) && (chat != NULL) && 48 | ((! purple_buddy_icons_node_has_custom_icon ((PurpleBlistNode*)chat)) 49 | || purple_account_get_bool(account, "use-group-avatar", TRUE))) { 50 | purple_buddy_icons_node_set_custom_icon_from_file ((PurpleBlistNode*)chat, avatar); 51 | purple_blist_update_node_icon ((PurpleBlistNode*)chat); 52 | } 53 | 54 | return chat; 55 | } 56 | 57 | /* 58 | * Given a JsonNode for a group member, get the uuid. 59 | */ 60 | char * 61 | signald_get_group_member_uuid(JsonNode *node) 62 | { 63 | JsonObject *member = json_node_get_object(node); 64 | return (char *)json_object_get_string_member(member, "uuid"); 65 | } 66 | 67 | /* 68 | * Given a list of members, get back the list of corresponding uuid. 69 | * This function makes a copy of the uuids so they must be freed via g_list_free_full. 70 | */ 71 | GList * 72 | signald_members_to_uuids(JsonArray *members) 73 | { 74 | GList *uuids = NULL; 75 | 76 | for ( 77 | GList *this_member = json_array_get_elements(members); 78 | this_member != NULL; 79 | this_member = this_member->next 80 | ) { 81 | JsonNode *element = (JsonNode *)(this_member->data); 82 | char *uuid = signald_get_group_member_uuid(element); 83 | uuids = g_list_append(uuids, g_strdup(uuid)); 84 | } 85 | return uuids; 86 | } 87 | 88 | gboolean 89 | signald_members_contains_uuid(JsonArray *members, char *uuid) 90 | { 91 | GList *uuids = signald_members_to_uuids(members); 92 | gboolean result = g_list_find_custom(uuids, uuid, (GCompareFunc)g_strcmp0) != NULL; 93 | g_list_free_full(uuids, g_free); 94 | return result; 95 | } 96 | 97 | void 98 | signald_accept_groupV2_invitation(SignaldAccount *sa, const char *groupId, JsonArray *pendingMembers) 99 | { 100 | g_return_if_fail(sa->uuid); 101 | 102 | // See if we're a pending member of the group v2. 103 | gboolean pending = signald_members_contains_uuid(pendingMembers, sa->uuid); 104 | if (pending) { 105 | // we are a pending member, join it 106 | JsonObject *data = json_object_new(); 107 | json_object_set_string_member(data, "type", "accept_invitation"); 108 | json_object_set_string_member(data, "account", sa->uuid); 109 | json_object_set_string_member(data, "groupID", groupId); 110 | signald_send_json_or_display_error(sa, data); 111 | json_object_unref(data); 112 | } 113 | } 114 | 115 | /* 116 | * This handles incoming Signal group information, 117 | * overwriting participant lists where appropriate. 118 | */ 119 | // TODO: a soft "remove who left, add who was added" would be nicer than 120 | // this blunt-force "remove everyone and re-add" approach 121 | // TODO: use purple_conv_chat_add_users since purple_conv_chat_add_user uses it anyway 122 | void 123 | signald_chat_set_participants(PurpleAccount *account, const char *groupId, JsonArray *members) { 124 | GList *uuids = signald_members_to_uuids(members); 125 | PurpleConvChat *conv_chat = purple_conversations_find_chat_with_account(groupId, account); 126 | if (conv_chat != NULL) { // only consider active chats 127 | purple_conv_chat_clear_users(conv_chat); 128 | for (GList * uuid_elem = uuids; uuid_elem != NULL; uuid_elem = uuid_elem->next) { 129 | const char* uuid = uuid_elem->data; 130 | PurpleConvChatBuddyFlags flags = 0; 131 | purple_conv_chat_add_user(conv_chat, uuid, NULL, flags, FALSE); 132 | 133 | if (!purple_find_buddy(account, uuid)) { 134 | // this UUID is not known – request the profile for display of friendly name 135 | PurpleConnection *pc = purple_account_get_connection(account); 136 | signald_request_profile(pc, uuid); 137 | } 138 | } 139 | } 140 | g_list_free_full(uuids, g_free); 141 | } 142 | 143 | void 144 | signald_process_groupV2_obj(SignaldAccount *sa, JsonObject *obj) 145 | { 146 | const char *groupId = json_object_get_string_member(obj, "id"); 147 | const char *title = json_object_get_string_member(obj, "title"); 148 | const char *avatar = json_object_get_string_member_or_null(obj, "avatar"); 149 | 150 | purple_debug_info(SIGNALD_PLUGIN_ID, "Processing group ID %s, %s\n", groupId, title); 151 | 152 | if (purple_account_get_bool(sa->account, "auto-accept-invitations", FALSE)) { 153 | signald_accept_groupV2_invitation(sa, groupId, json_object_get_array_member(obj, "pendingMembers")); 154 | } 155 | 156 | signald_ensure_group_chat_in_blist(sa->account, groupId, title, avatar); // for joining later 157 | 158 | // update participants 159 | signald_chat_set_participants(sa->account, groupId, json_object_get_array_member(obj, "members")); 160 | 161 | // set title as topic 162 | PurpleConversation *conv = purple_find_chat(sa->pc, g_str_hash(groupId)); 163 | if (conv != NULL) { 164 | purple_conv_chat_set_topic(PURPLE_CONV_CHAT(conv), groupId, title); 165 | } 166 | 167 | // the user might have requested a room list, fill it 168 | if (sa->roomlist) { 169 | PurpleRoomlistRoom *room = purple_roomlist_room_new(PURPLE_ROOMLIST_ROOMTYPE_ROOM, groupId, NULL); // this sets the room's name 170 | purple_roomlist_room_add_field(sa->roomlist, room, title); // this sets the room's title 171 | purple_roomlist_room_add(sa->roomlist, room); 172 | } 173 | } 174 | 175 | void 176 | signald_process_groupV2(JsonArray *array, guint index_, JsonNode *element_node, gpointer user_data) 177 | { 178 | SignaldAccount *sa = (SignaldAccount *)user_data; 179 | JsonObject *obj = json_node_get_object(element_node); 180 | signald_process_groupV2_obj(sa, obj); 181 | } 182 | 183 | void 184 | signald_parse_groupV2_list(SignaldAccount *sa, JsonArray *groups) 185 | { 186 | json_array_foreach_element(groups, signald_process_groupV2, sa); 187 | 188 | if (sa->roomlist) { 189 | // in case the user explicitly requested a room list, the query is finished now 190 | purple_roomlist_set_in_progress(sa->roomlist, FALSE); 191 | purple_roomlist_unref(sa->roomlist); // unref here, roomlist may remain in ui 192 | sa->roomlist = NULL; 193 | } 194 | } 195 | 196 | void 197 | signald_request_group_info(SignaldAccount *sa, const char *groupId) 198 | { 199 | return; // this currently fails. 200 | g_return_if_fail(sa->uuid); 201 | 202 | JsonObject *data = json_object_new(); 203 | 204 | json_object_set_string_member(data, "type", "get_group"); 205 | json_object_set_string_member(data, "account", sa->uuid); 206 | json_object_set_string_member(data, "groupID", groupId); 207 | 208 | signald_send_json_or_display_error(sa, data); 209 | 210 | json_object_unref(data); 211 | } 212 | 213 | void 214 | signald_request_group_list(SignaldAccount *sa) 215 | { 216 | g_return_if_fail(sa->uuid); 217 | JsonObject *data = json_object_new(); 218 | json_object_set_string_member(data, "type", "list_groups"); 219 | json_object_set_string_member(data, "account", sa->uuid); 220 | signald_send_json_or_display_error(sa, data); 221 | json_object_unref(data); 222 | } 223 | 224 | PurpleConversation * signald_enter_group_chat(PurpleConnection *pc, const char *groupId, const char *title) { 225 | SignaldAccount *sa = purple_connection_get_protocol_data(pc); 226 | // use hash of groupId for chat id number 227 | PurpleConversation *conv = purple_find_chat(pc, g_str_hash(groupId)); 228 | if (conv == NULL || (conv != NULL && purple_conversation_get_data(conv, "want-to-rejoin"))) { 229 | conv = serv_got_joined_chat(pc, g_str_hash(groupId), groupId); 230 | if (purple_conversation_get_data(conv, "want-to-rejoin")) { 231 | // now that we did rejoin, remove the flag 232 | // directly accessing conv->data feels wrong, but there is no interface to do so 233 | g_hash_table_remove(conv->data, "want-to-rejoin"); 234 | } 235 | purple_conversation_set_data(conv, "name", g_strdup(groupId)); 236 | purple_conv_chat_set_nick(PURPLE_CONV_CHAT(conv), sa->uuid); // identify ourselves in this chat 237 | if (title != NULL) { 238 | purple_conv_chat_set_topic(PURPLE_CONV_CHAT(conv), groupId, title); 239 | } 240 | signald_request_group_info(sa, groupId); 241 | } 242 | return conv; 243 | } 244 | 245 | void 246 | signald_join_chat(PurpleConnection *pc, GHashTable *data) 247 | { 248 | const char *groupId = g_hash_table_lookup(data, "name"); 249 | const char *title = g_hash_table_lookup(data, "title"); 250 | if (groupId != NULL) { 251 | signald_enter_group_chat(pc, groupId, title); 252 | } 253 | } 254 | 255 | /* 256 | * Information identifying a chat. 257 | */ 258 | GList * 259 | signald_chat_info(PurpleConnection *pc) 260 | { 261 | GList *infos = NULL; 262 | 263 | struct proto_chat_entry *pce; 264 | 265 | pce = g_new0(struct proto_chat_entry, 1); 266 | pce->label = "Group ID"; 267 | pce->identifier = "name"; 268 | pce->required = TRUE; 269 | infos = g_list_append(infos, pce); 270 | 271 | return infos; 272 | } 273 | 274 | GHashTable 275 | *signald_chat_info_defaults(PurpleConnection *pc, const char *chat_name) 276 | { 277 | GHashTable *defaults; 278 | 279 | defaults = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, g_free); 280 | 281 | if (chat_name != NULL) { 282 | g_hash_table_insert(defaults, "name", g_strdup(chat_name)); 283 | } 284 | 285 | return defaults; 286 | } 287 | 288 | void 289 | signald_set_chat_topic(PurpleConnection *pc, int id, const char *topic) 290 | { 291 | // Nothing to do here. For some reason this callback has to be 292 | // registered if Pidgin is going to enable the "Alias..." menu 293 | // option in the conversation. 294 | } 295 | 296 | /* 297 | * Get the identifying aspect of a chat (as passed to serv_got_joined_chat) 298 | * given the chat_info entries. In Signal, this is the groupId. 299 | * 300 | * Borrowed from: 301 | * https://github.com/matrix-org/purple-matrix/blob/master/libmatrix.c 302 | */ 303 | char *signald_get_chat_name(GHashTable *components) 304 | { 305 | const char *groupId = g_hash_table_lookup(components, "name"); 306 | return g_strdup(groupId); 307 | } 308 | 309 | /* 310 | * This requests a list of rooms representing the Signal group chats. 311 | * The request is asynchronous. Responses are handled by signald_roomlist_add_room. 312 | * 313 | * A purple room has an identifying name – for Signal that is the UUID. 314 | * A purple room has a list of fields – in our case only Signal group name. 315 | * 316 | * Some services like spectrum expect the human readable group name field key to be "topic", 317 | * see RoomlistProgress in https://github.com/SpectrumIM/spectrum2/blob/518ba5a/backends/libpurple/main.cpp#L1997 318 | * In purple, the roomlist field "name" gets overwritten in purple_roomlist_room_join, see libpurple/roomlist.c. 319 | */ 320 | PurpleRoomlist * 321 | signald_roomlist_get_list(PurpleConnection *pc) { 322 | SignaldAccount *sa = purple_connection_get_protocol_data(pc); 323 | if (sa->roomlist != NULL) { 324 | purple_debug_info(SIGNALD_PLUGIN_ID, "Already getting roomlist."); 325 | } else { 326 | sa->roomlist = purple_roomlist_new(sa->account); 327 | purple_roomlist_set_in_progress(sa->roomlist, TRUE); 328 | GList *fields = NULL; 329 | fields = g_list_append(fields, purple_roomlist_field_new( 330 | PURPLE_ROOMLIST_FIELD_STRING, "Group Name", "topic", FALSE 331 | )); 332 | purple_roomlist_set_fields(sa->roomlist, fields); 333 | signald_request_group_list(sa); 334 | } 335 | return sa->roomlist; 336 | } 337 | 338 | 339 | static void signald_leave_group(SignaldAccount *sa, const char *groupID) 340 | { 341 | g_return_if_fail(groupID != NULL); 342 | g_return_if_fail(sa->uuid != NULL); 343 | 344 | JsonObject *data = json_object_new(); 345 | json_object_set_string_member(data, "type", "leave_group"); 346 | json_object_set_string_member(data, "account", sa->uuid); 347 | json_object_set_string_member(data, "groupID", groupID); 348 | signald_send_json_or_display_error(sa, data); 349 | json_object_unref(data); 350 | } 351 | 352 | static void 353 | signald_leave_chat(PurpleBlistNode *node, gpointer userdata) 354 | { 355 | g_return_if_fail(node != NULL); 356 | PurpleChat *chat = (PurpleChat *)node; 357 | 358 | PurpleAccount *account = purple_chat_get_account(chat); 359 | PurpleConnection *pc = purple_account_get_connection(account); 360 | SignaldAccount *sa = purple_connection_get_protocol_data(pc); 361 | GHashTable *components = purple_chat_get_components(chat); 362 | const char * groupID = g_hash_table_lookup(components, "name"); 363 | 364 | signald_leave_group(sa, groupID); 365 | } 366 | 367 | static GList * 368 | signald_chat_menu(PurpleChat *chat) 369 | { 370 | GList *menu = NULL; 371 | 372 | PurpleMenuAction * act = purple_menu_action_new( 373 | "Leave Group", 374 | PURPLE_CALLBACK(signald_leave_chat), 375 | NULL, /* userdata passed to the callback */ 376 | NULL /* child menu items */ 377 | ); 378 | menu = g_list_append(menu, act); 379 | 380 | return menu; 381 | } 382 | 383 | GList *signald_blist_node_menu(PurpleBlistNode *node) { 384 | if (PURPLE_BLIST_NODE_IS_CHAT(node)) { 385 | return signald_chat_menu((PurpleChat *) node); 386 | } else { 387 | return NULL; 388 | } 389 | } 390 | 391 | void signald_process_leave_group(SignaldAccount *sa, JsonObject *data) { 392 | JsonObject *v2 = json_object_get_object_member(data, "v2"); 393 | const gchar *id = json_object_get_string_member(v2, "id"); 394 | PurpleChat * chat = purple_blist_find_chat(sa->account, id); 395 | purple_blist_remove_chat(chat); 396 | } 397 | 398 | static PurpleChat * signald_blist_find_chat(PurpleAccount *account, int id) { 399 | for (PurpleBlistNode *group = purple_blist_get_root(); group != NULL; group = group->next) { 400 | for (PurpleBlistNode *node = group->child; node != NULL; node = node->next) { 401 | if (PURPLE_BLIST_NODE_IS_CHAT(node)) { 402 | PurpleChat *chat = (PurpleChat*)node; 403 | if (account == chat->account) { 404 | const gchar *groupId = g_hash_table_lookup(chat->components, "name"); 405 | if (id == g_str_hash(groupId)) { 406 | return chat; 407 | } 408 | } 409 | } 410 | } 411 | } 412 | return NULL; 413 | } 414 | 415 | /* 416 | * The user wants to leave a chat. 417 | * 418 | * Unfortunately, this is called whenever a Pidgin chat window is closed 419 | * unless gtk-persistent is set. 420 | * 421 | * This leaves the chat iff it was not added to the buddy list. 422 | */ 423 | void 424 | signald_chat_leave(PurpleConnection *pc, int id) { 425 | SignaldAccount *sa = purple_connection_get_protocol_data(pc); 426 | PurpleChat * chat = signald_blist_find_chat(sa->account, id); 427 | if (!chat) { 428 | PurpleConversation *conv = purple_find_chat(pc, id); 429 | const gchar * groupID = purple_conversation_get_name(conv); 430 | signald_leave_group(sa, groupID); 431 | } 432 | } 433 | 434 | /* 435 | * Updates a non-buddy group chat participant's name. 436 | */ 437 | // note: with get_cb_alias, libpurple defines a function for this feature, but I could not figure out how to make Pidgin invoke it 438 | void signald_update_participant_name(const char *uuid, JsonObject *obj) { 439 | const char *alias = json_object_get_string_member_or_null(obj, "name"); 440 | // TODO: consider other name-like fields 441 | if (alias && alias[0]) { 442 | // iterate over all conversations 443 | for (GList * conversations_elem = purple_get_conversations(); conversations_elem != NULL; conversations_elem = conversations_elem->next) { 444 | PurpleConversation *conv = conversations_elem->data; 445 | PurpleConversationUiOps *ops = purple_conversation_get_ui_ops(conv); 446 | if (ops != NULL && ops->chat_update_user != NULL && purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) { 447 | // this conversation is a group chat and the UI can update users 448 | PurpleConvChat *chat = purple_conversation_get_chat_data(conv); 449 | PurpleConvChatBuddy *cbuddy = purple_conv_chat_cb_find(chat, uuid); 450 | if (cbuddy && !cbuddy->buddy) { 451 | g_free(cbuddy->alias); 452 | cbuddy->alias = g_strdup(alias); 453 | ops->chat_update_user(conv, uuid); // notify UI about change 454 | } 455 | } 456 | } 457 | } 458 | } 459 | -------------------------------------------------------------------------------- /src/groups.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "structs.h" 4 | #include 5 | 6 | void 7 | signald_process_groupV2_obj(SignaldAccount *sa, JsonObject *obj); 8 | 9 | void 10 | signald_parse_groupV2_list(SignaldAccount *sa, JsonArray *groups); 11 | 12 | GList * 13 | signald_chat_info(PurpleConnection *pc); 14 | 15 | GHashTable 16 | *signald_chat_info_defaults(PurpleConnection *pc, const char *chat_name); 17 | 18 | void 19 | signald_join_chat(PurpleConnection *pc, GHashTable *data); 20 | 21 | void 22 | signald_set_chat_topic(PurpleConnection *pc, int id, const char *topic); 23 | 24 | void 25 | signald_request_group_list(SignaldAccount *sa); 26 | 27 | PurpleConversation * signald_enter_group_chat(PurpleConnection *pc, const char *groupId, const char *title); 28 | 29 | char *signald_get_chat_name(GHashTable *components); 30 | 31 | PurpleRoomlist *signald_roomlist_get_list(PurpleConnection *pc); 32 | 33 | GList *signald_blist_node_menu(PurpleBlistNode *node); 34 | 35 | void signald_process_leave_group(SignaldAccount *sa, JsonObject *data); 36 | 37 | void signald_chat_leave(PurpleConnection *pc, int id); 38 | 39 | void signald_request_group_info(SignaldAccount *sa, const char *groupId); 40 | 41 | void signald_update_participant_name(const char *uuid, JsonObject *obj); 42 | -------------------------------------------------------------------------------- /src/input.c: -------------------------------------------------------------------------------- 1 | #include "purple_compat.h" 2 | #include "input.h" 3 | #include "defines.h" 4 | #include "structs.h" 5 | #include "link.h" 6 | #include "admin.h" 7 | #include "groups.h" 8 | #include "contacts.h" 9 | #include "message.h" 10 | #include "login.h" 11 | #include "receipt.h" 12 | #include "json-utils.h" 13 | 14 | static void 15 | signald_handle_input(SignaldAccount *sa, JsonNode *root) 16 | { 17 | JsonObject *obj = json_node_get_object(root); 18 | const gchar *type = json_object_get_string_member(obj, "type"); 19 | purple_debug_info(SIGNALD_PLUGIN_ID, "received type: %s\n", type); 20 | 21 | // catch and display errors 22 | JsonObject * error_object = NULL; 23 | if (json_object_has_member(obj, "error")) { 24 | // error key is set 25 | JsonNode * error_node = json_object_get_member(obj, "error"); 26 | // check whether error is a JSON object 27 | // it can also be a boolean value, see handling below 28 | if (JSON_NODE_HOLDS_OBJECT(error_node)) { 29 | error_object = json_object_get_object_member(obj, "error"); 30 | } 31 | } 32 | if (error_object != NULL) { 33 | const char *error_type = json_object_get_string_member(obj, "error_type"); 34 | const char *error_message = json_object_get_string_member(error_object, "message"); 35 | if (strstr(error_message, "AuthorizationFailedException")) { 36 | // TODO: rather check json array error.exceptions for "org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException" 37 | signald_link_or_register(sa); 38 | return; 39 | } else if (purple_strequal(type, "subscribe")) { 40 | // error while subscribing 41 | signald_link_or_register(sa); 42 | return; 43 | } else if (strstr(error_message, "SQLITE_BUSY")) { 44 | purple_connection_error(sa->pc, PURPLE_CONNECTION_ERROR_NETWORK_ERROR, "SQLite database busy."); 45 | return; 46 | } else { 47 | const char *message = json_object_get_string_member(error_object, "message"); 48 | char *error_message = g_strdup_printf("%s occurred on %s: %s\n", error_type, type, message); 49 | purple_connection_error(sa->pc, PURPLE_CONNECTION_ERROR_OTHER_ERROR, error_message); 50 | g_free(error_message); 51 | return; 52 | } 53 | } 54 | 55 | // no error, actions depending on type 56 | if (purple_strequal(type, "version")) { 57 | obj = json_object_get_object_member(obj, "data"); 58 | purple_debug_info(SIGNALD_PLUGIN_ID, "signald version: %s\n", json_object_get_string_member(obj, "version")); 59 | signald_request_accounts(sa); // Request information on accounts, including our own UUID. 60 | 61 | } else if (purple_strequal(type, "list_accounts")) { 62 | JsonObject *data = json_object_get_object_member(obj, "data"); 63 | signald_parse_account_list(sa, json_object_get_array_member(data, "accounts")); 64 | 65 | } else if (purple_strequal(type, "subscribe")) { 66 | purple_debug_info(SIGNALD_PLUGIN_ID, "Subscribed!\n"); 67 | // request a sync from other devices 68 | signald_request_sync(sa); 69 | 70 | } else if (purple_strequal(type, "unsubscribe")) { 71 | purple_connection_set_state(sa->pc, PURPLE_CONNECTION_DISCONNECTED); 72 | 73 | } else if (purple_strequal(type, "request_sync")) { 74 | // sync from other devices completed, 75 | // now pull contacts and groups 76 | signald_list_contacts(sa); 77 | signald_request_group_list(sa); 78 | 79 | } else if (purple_strequal(type, "list_contacts")) { 80 | obj = json_object_get_object_member(obj, "data"); 81 | signald_parse_contact_list(sa, json_object_get_array_member(obj,"profiles")); 82 | 83 | } else if (purple_strequal(type, "InternalError")) { 84 | // TODO: find out which messages do have a "data" object and which do not 85 | const char * message = json_object_get_string_member_or_null(obj, "message"); 86 | if (message == NULL) { 87 | obj = json_object_get_object_member(obj, "data"); 88 | message = json_object_get_string_member_or_null(obj, "message"); 89 | } 90 | if (purple_strequal(message, "org.whispersystems.signalservice.api.InvalidMessageStructureException: SyncMessage missing destination, group ID, and recipient manifest!")) { 91 | // TODO: remove this special case after https://gitlab.com/signald/signald/-/issues/363 has been resolved 92 | purple_debug_warning(SIGNALD_PLUGIN_ID, "Ignoring InvalidMessageStructureException.\n"); 93 | } else if (purple_strequal(message, "org.signal.libsignal.metadata.InvalidMetadataMessageException: org.signal.libsignal.protocol.InvalidMessageException: invalid sealed sender message: derived ephemeral key did not match key provided in message")) { 94 | // this means a message could not be fully processed and henceforth will not be displayed 95 | // the connection should not be terminated, though 96 | //purple_debug_warning(SIGNALD_PLUGIN_ID, "Ignoring InvalidMetadataMessageException.\n"); 97 | const gchar * username = purple_account_get_username(sa->account); 98 | PurpleConversation * conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, username, sa->account); 99 | if (conv == NULL) { 100 | conv = purple_conversation_new(PURPLE_CONV_TYPE_IM, sa->account, username); 101 | } 102 | purple_conversation_write(conv, NULL, "InvalidMessageException happened in signald. Please check your primary device if you have one. The message is lost for this client. I am sorry.", PURPLE_MESSAGE_ERROR, time(NULL)); 103 | } else { 104 | purple_connection_error(sa->pc, PURPLE_CONNECTION_ERROR_OTHER_ERROR, message); 105 | } 106 | 107 | } else if (purple_strequal(type, "get_profile")) { 108 | obj = json_object_get_object_member(obj, "data"); 109 | signald_process_profile(sa, obj); 110 | 111 | } else if (purple_strequal(type, "get_group")) { 112 | obj = json_object_get_object_member(obj, "data"); 113 | signald_process_groupV2_obj(sa, obj); 114 | 115 | } else if (purple_strequal(type, "list_groups")) { 116 | obj = json_object_get_object_member(obj, "data"); 117 | signald_parse_groupV2_list(sa, json_object_get_array_member(obj, "groups")); 118 | 119 | } else if (purple_strequal(type, "leave_group")) { 120 | obj = json_object_get_object_member(obj, "data"); 121 | signald_process_leave_group(sa, obj); 122 | 123 | } else if (purple_strequal(type, "IncomingMessage")) { 124 | obj = json_object_get_object_member(obj, "data"); 125 | if (json_object_has_member(obj, "receipt_message")) { 126 | signald_process_receipt(sa, obj); 127 | } else if (json_object_has_member(obj, "typing_message")) { 128 | signald_process_typing(sa, obj); 129 | } else { 130 | signald_process_message(sa, obj); 131 | } 132 | 133 | } else if (purple_strequal(type, "generate_linking_uri")) { 134 | signald_parse_linking_uri(sa, obj); 135 | 136 | } else if (purple_strequal (type, "finish_link")) { 137 | signald_process_finish_link(sa, obj); 138 | 139 | } else if (purple_strequal (type, "set_device_name")) { 140 | purple_debug_info(SIGNALD_PLUGIN_ID, "Device name set successfully.\n"); 141 | 142 | } else if (purple_strequal(type, "send")) { 143 | JsonObject *data = json_object_get_object_member(obj, "data"); 144 | signald_send_acknowledged(sa, data); 145 | 146 | } else if (purple_strequal(type, "mark_read")) { 147 | // I do not really care if sending read receipts succeed. 148 | 149 | } else if (purple_strequal(type, "WebSocketConnectionState")) { 150 | JsonObject *data = json_object_get_object_member(obj, "data"); 151 | const gchar *state = json_object_get_string_member(data, "state"); 152 | if (purple_strequal(state, "CONNECTED") && sa->uuid) { 153 | purple_connection_set_state(sa->pc, PURPLE_CONNECTION_CONNECTED); 154 | } else if (purple_strequal(state, "CONNECTING") && sa->uuid) { 155 | purple_connection_set_state(sa->pc, PURPLE_CONNECTION_CONNECTING); 156 | } else if (purple_strequal(state, "DISCONNECTED") && sa->uuid) { 157 | // setting the connection state to DISCONNECTED invokes the destruction of the instance 158 | // we probably do not want that (signald might already be doing a reconnect) 159 | purple_connection_set_state(sa->pc, PURPLE_CONNECTION_CONNECTING); 160 | //purple_connection_error(sa->pc, PURPLE_CONNECTION_ERROR_NETWORK_ERROR, "Disconnected."); 161 | } 162 | 163 | } else if (purple_strequal(type, "ListenerState")) { 164 | // obsolete variant of WebSocketConnectionState. ignore silently. 165 | 166 | } else if (purple_strequal(type, "ProtocolInvalidKeyIdError")) { 167 | // this one has boolean "error": true 168 | // TODO: get sender, show notification 169 | 170 | } else { 171 | purple_debug_warning(SIGNALD_PLUGIN_ID, "Ignored message of unknown type '%s'.\n", type); 172 | } 173 | } 174 | 175 | void 176 | signald_parse_input(SignaldAccount *sa, const char * json, gssize length) 177 | { 178 | JsonParser *parser = json_parser_new(); 179 | if (!json_parser_load_from_data(parser, json, length, NULL)) { 180 | purple_connection_error(sa->pc, PURPLE_CONNECTION_ERROR_OTHER_ERROR, "Error parsing input."); 181 | return; 182 | } else { 183 | JsonNode *root = json_parser_get_root(parser); 184 | if (root == NULL) { 185 | purple_connection_error(sa->pc, PURPLE_CONNECTION_ERROR_OTHER_ERROR, "root node is NULL."); 186 | } else { 187 | signald_handle_input(sa, root); 188 | } 189 | g_object_unref(parser); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/input.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "structs.h" 4 | 5 | void signald_parse_input(SignaldAccount *sa, const char * json, gssize length); 6 | -------------------------------------------------------------------------------- /src/interface.c: -------------------------------------------------------------------------------- 1 | #include "interface.h" 2 | 3 | const char * signald_list_icon(PurpleAccount *account, PurpleBuddy *buddy) { 4 | return "signal"; 5 | } 6 | 7 | void signald_tooltip_text(PurpleBuddy *buddy, PurpleNotifyUserInfo *user_info, gboolean full) { 8 | char * number = purple_buddy_get_protocol_data(buddy); 9 | if (number && number[0]) { 10 | purple_notify_user_info_add_pair_plaintext(user_info, "Number", number); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/interface.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | const char * signald_list_icon(PurpleAccount *account, PurpleBuddy *buddy); 6 | void signald_tooltip_text(PurpleBuddy *buddy, PurpleNotifyUserInfo *user_info, gboolean full); 7 | -------------------------------------------------------------------------------- /src/json-utils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Suppress overzealous json-glib 'critical errors' 4 | #define json_object_has_member(JSON_OBJECT, MEMBER) \ 5 | (JSON_OBJECT ? json_object_has_member(JSON_OBJECT, MEMBER) : FALSE) 6 | #define json_object_get_object_member_or_null(JSON_OBJECT, MEMBER) \ 7 | (json_object_has_member(JSON_OBJECT, MEMBER) ? json_object_get_object_member(JSON_OBJECT, MEMBER) : NULL) 8 | #define json_object_get_string_member_or_null(JSON_OBJECT, MEMBER) \ 9 | (json_object_has_member(JSON_OBJECT, MEMBER) ? json_object_get_string_member(JSON_OBJECT, MEMBER) : NULL) 10 | #define json_object_get_array_member_or_null(JSON_OBJECT, MEMBER) \ 11 | (json_object_has_member(JSON_OBJECT, MEMBER) ? json_object_get_array_member(JSON_OBJECT, MEMBER) : NULL) 12 | -------------------------------------------------------------------------------- /src/libsignald.c: -------------------------------------------------------------------------------- 1 | /* 2 | * signald plugin for libpurple 3 | * Copyright (C) 2016 Hermann Höhne 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include "structs.h" 25 | #include "defines.h" 26 | #include "comms.h" 27 | #include "login.h" 28 | #include "message.h" 29 | #include "contacts.h" 30 | #include "groups.h" 31 | #include "options.h" 32 | #include "signald_procmgmt.h" 33 | #include "interface.h" 34 | #include "status.h" 35 | #include "reply.h" 36 | 37 | static void 38 | signald_update_contacts (PurplePluginAction* action) 39 | { 40 | PurpleConnection* pc = action->context; 41 | SignaldAccount *sa = purple_connection_get_protocol_data(pc); 42 | 43 | signald_list_contacts(sa); 44 | } 45 | 46 | static void 47 | signald_update_groups (PurplePluginAction* action) 48 | { 49 | PurpleConnection* pc = action->context; 50 | SignaldAccount *sa = purple_connection_get_protocol_data(pc); 51 | 52 | signald_request_group_list(sa); 53 | } 54 | 55 | static GList * 56 | signald_actions(PurplePlugin *plugin, gpointer context) 57 | { 58 | GList* acts = NULL; 59 | { 60 | PurplePluginAction *act = purple_plugin_action_new("Update Contacts", &signald_update_contacts); 61 | acts = g_list_append(acts, act); 62 | } 63 | { 64 | PurplePluginAction *act = purple_plugin_action_new("Update Groups", &signald_update_groups); 65 | acts = g_list_append(acts, act); 66 | } 67 | return acts; 68 | } 69 | 70 | static gboolean 71 | plugin_load(PurplePlugin *plugin, GError **error) 72 | { 73 | return TRUE; 74 | } 75 | 76 | static gboolean 77 | plugin_unload(PurplePlugin *plugin, GError **error) 78 | { 79 | purple_signals_disconnect_by_handle(plugin); 80 | return TRUE; 81 | } 82 | 83 | /* Purple2 Plugin Load Functions */ 84 | static gboolean 85 | libpurple2_plugin_load(PurplePlugin *plugin) 86 | { 87 | return plugin_load(plugin, NULL); 88 | } 89 | 90 | static gboolean 91 | libpurple2_plugin_unload(PurplePlugin *plugin) 92 | { 93 | return plugin_unload(plugin, NULL); 94 | } 95 | 96 | static PurplePluginProtocolInfo prpl_info = { 97 | .struct_size = sizeof(PurplePluginProtocolInfo), // must be set for PURPLE_PROTOCOL_PLUGIN_HAS_FUNC to work across versions 98 | // base protocol information 99 | .options = OPT_PROTO_NO_PASSWORD | OPT_PROTO_IM_IMAGE, 100 | .list_icon = signald_list_icon, 101 | .status_types = signald_status_types, // this actually needs to exist, else the protocol cannot be set to "online" 102 | .login = signald_login, 103 | .close = signald_close, 104 | .send_im = signald_send_im, 105 | .add_buddy = signald_add_buddy, 106 | // extra contact information 107 | .tooltip_text = signald_tooltip_text, 108 | .get_info = signald_get_info, 109 | // group-chat related functions 110 | .chat_info = signald_chat_info, 111 | .chat_info_defaults = signald_chat_info_defaults, 112 | .join_chat = signald_join_chat, 113 | .chat_leave = signald_chat_leave, 114 | .get_chat_name = signald_get_chat_name, 115 | .chat_send = signald_send_chat, 116 | .set_chat_topic = signald_set_chat_topic, 117 | .roomlist_get_list = signald_roomlist_get_list, 118 | .blist_node_menu = signald_blist_node_menu, 119 | #if PURPLE_VERSION_CHECK(2,14,0) 120 | //.get_cb_alias // TODO: find out how to use this 121 | //.chat_send_file 122 | #else 123 | #pragma message "Warning: libpurple is too old. Group chat participants may appear without friendly names." 124 | #endif 125 | }; 126 | 127 | static void plugin_init(PurplePlugin *plugin) { 128 | prpl_info.protocol_options = signald_add_account_options(prpl_info.protocol_options); 129 | } 130 | 131 | static PurplePluginInfo info = { 132 | .magic = PURPLE_PLUGIN_MAGIC, 133 | .major_version = PURPLE_MAJOR_VERSION, 134 | .minor_version = PURPLE_MINOR_VERSION, 135 | .type = PURPLE_PLUGIN_PROTOCOL, 136 | .priority = PURPLE_PRIORITY_DEFAULT, 137 | .id = SIGNALD_PLUGIN_ID, 138 | .name = "signald", 139 | .version = SIGNALD_PLUGIN_VERSION, 140 | .author = "Hermann Hoehne ", 141 | .homepage = SIGNALD_PLUGIN_WEBSITE, 142 | .load = libpurple2_plugin_load, 143 | .unload = libpurple2_plugin_unload, 144 | .extra_info = &prpl_info, 145 | .actions = signald_actions 146 | }; 147 | 148 | PURPLE_INIT_PLUGIN(signald, plugin_init, info); 149 | -------------------------------------------------------------------------------- /src/libsignald.h: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/link.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "defines.h" 3 | #include "purple_compat.h" 4 | #include "structs.h" 5 | #include "comms.h" 6 | #include "login.h" 7 | #include "qrcodegen.h" // TODO: better use libqrencode (available in Debian) 8 | #include 9 | 10 | void 11 | signald_set_device_name (SignaldAccount *sa) 12 | { 13 | g_return_if_fail(sa->uuid != NULL); 14 | 15 | JsonObject *data = json_object_new(); 16 | json_object_set_string_member(data, "type", "set_device_name"); 17 | json_object_set_string_member(data, "account", sa->uuid); 18 | const char * device_name = purple_account_get_string(sa->account, "device-name", SIGNALD_DEFAULT_DEVICENAME); 19 | json_object_set_string_member(data, "device_name", device_name); 20 | 21 | signald_send_json_or_display_error(sa, data); 22 | json_object_unref(data); 23 | } 24 | 25 | static void 26 | signald_scan_qrcode_done(SignaldAccount *sa , PurpleRequestFields *fields) 27 | { 28 | // Send finish link 29 | JsonObject *data = json_object_new(); 30 | json_object_set_string_member(data, "type", "finish_link"); 31 | json_object_set_string_member(data, "session_id", sa->session_id); 32 | json_object_set_boolean_member(data, "overwrite", TRUE); // TODO: ask user before overwrting 33 | 34 | signald_send_json_or_display_error(sa, data); 35 | json_object_unref(data); 36 | } 37 | 38 | static void 39 | signald_scan_qrcode_cancel(SignaldAccount *sa , PurpleRequestFields *fields) 40 | { 41 | purple_connection_error(sa->pc, PURPLE_CONNECTION_ERROR_OTHER_ERROR, "Linking was cancelled."); 42 | } 43 | 44 | void 45 | signald_scan_qrcode(SignaldAccount *sa, gchar* qrimgdata, gsize qrimglen) 46 | { 47 | // Dispalay it for scanning 48 | PurpleRequestFields* fields = purple_request_fields_new(); 49 | PurpleRequestFieldGroup* group = purple_request_field_group_new(NULL); 50 | PurpleRequestField* field; 51 | 52 | purple_request_fields_add_group(fields, group); 53 | 54 | field = purple_request_field_image_new( 55 | "qr_code", "QR code", 56 | qrimgdata, qrimglen); 57 | purple_request_field_group_add_field(group, field); 58 | 59 | purple_request_fields( 60 | sa->pc, "Signal Protocol", "Link to master device", 61 | "For linking this account to a Signal master device, " 62 | "please scan the QR code below. In the Signal App, " 63 | "go to \"Preferences\" and \"Linked devices\".", 64 | fields, 65 | "Done", G_CALLBACK(signald_scan_qrcode_done), 66 | "Cancel", G_CALLBACK(signald_scan_qrcode_cancel), 67 | sa->account, 68 | purple_account_get_username(sa->account), 69 | NULL, 70 | sa); 71 | } 72 | 73 | void 74 | signald_parse_linking_uri(SignaldAccount *sa, JsonObject *obj) 75 | { 76 | // Linking uri is provided, create the qr-code 77 | JsonObject *data = json_object_get_object_member(obj, "data"); 78 | const gchar *uri = json_object_get_string_member(data, "uri"); 79 | const gchar *session_id = json_object_get_string_member(data, "session_id"); 80 | sa->session_id = g_strdup(session_id); 81 | purple_debug_info(SIGNALD_PLUGIN_ID, "Link URI = '%s'\n", uri); 82 | purple_debug_info(SIGNALD_PLUGIN_ID, "Sesison ID = '%s'\n", session_id); 83 | 84 | enum qrcodegen_Ecc errCorLvl = qrcodegen_Ecc_LOW; 85 | uint8_t qrcode[qrcodegen_BUFFER_LEN_MAX]; 86 | uint8_t tempBuffer[qrcodegen_BUFFER_LEN_MAX]; 87 | bool ok = qrcodegen_encodeText(uri, tempBuffer, qrcode, errCorLvl, qrcodegen_VERSION_MIN, qrcodegen_VERSION_MAX, qrcodegen_Mask_AUTO, true); 88 | if (ok) { 89 | int border = 4; 90 | int zoom = 4; 91 | int qrcodesize = qrcodegen_getSize(qrcode); 92 | int imgwidth = (border*2+qrcodesize)*zoom; 93 | // poor man's PBM encoder 94 | gchar *head = g_strdup_printf("P1 %d %d ", imgwidth, imgwidth); 95 | int headlen = strlen(head); 96 | gsize qrimglen = headlen+imgwidth*2*imgwidth*2; 97 | gchar qrimgdata[qrimglen]; 98 | strncpy(qrimgdata, head, headlen+1); 99 | g_free(head); 100 | gchar *qrimgptr = qrimgdata+headlen; 101 | // from https://github.com/nayuki/QR-Code-generator/blob/master/c/qrcodegen-demo.c 102 | for (int y = 0; y/zoom < qrcodesize + border*2; y++) { 103 | for (int x = 0; x/zoom < qrcodesize + border*2; x++) { 104 | *qrimgptr++ = qrcodegen_getModule(qrcode, x/zoom - border, y/zoom - border) ? '1' : '0'; 105 | *qrimgptr++ = ' '; 106 | } 107 | } 108 | signald_scan_qrcode(sa, qrimgdata, qrimglen); 109 | } else { 110 | purple_debug_info(SIGNALD_PLUGIN_ID, "qrcodegen failed.\n"); 111 | } 112 | 113 | } 114 | 115 | void 116 | signald_verify_ok_cb(SignaldAccount *sa, const char* input) 117 | { 118 | JsonObject *data = json_object_new(); 119 | json_object_set_string_member(data, "type", "verify"); 120 | json_object_set_string_member(data, "username", purple_account_get_username(sa->account)); 121 | json_object_set_string_member(data, "code", input); 122 | signald_send_json_or_display_error(sa, data); 123 | json_object_unref(data); 124 | 125 | // TODO: Is there an acknowledge on successful registration? If yes, 126 | // subscribe afterwards or display an error otherwise 127 | // signald_subscribe(sa); 128 | purple_connection_error(sa->pc, PURPLE_CONNECTION_ERROR_NETWORK_ERROR, "Verification code was sent. Reconnect needed."); 129 | } 130 | 131 | void 132 | signald_link_or_register(SignaldAccount *sa) 133 | { 134 | // TODO: split the function into link and register 135 | const char *username = purple_account_get_username(sa->account); 136 | JsonObject *data = json_object_new(); 137 | 138 | if (purple_account_get_bool(sa->account, "link", TRUE)) { 139 | // Link Pidgin to the master device. 140 | JsonObject *data = json_object_new(); 141 | json_object_set_string_member(data, "type", "generate_linking_uri"); 142 | signald_send_json_or_display_error(sa, data); 143 | } else { 144 | // Register username (phone number) as new signal account, which 145 | // requires a registration. From the signald readme: 146 | // {"type": "register", "username": "+12024561414"} 147 | // TODO: test this for v1 148 | 149 | json_object_set_string_member(data, "type", "register"); 150 | json_object_set_string_member(data, "username", username); 151 | signald_send_json_or_display_error(sa, data); 152 | 153 | // TODO: Test registering thoroughly 154 | purple_request_input (sa->pc, SIGNALD_DIALOG_TITLE, "Verify registration", 155 | "Please enter the code that you have received for\n" 156 | "verifying the registration", 157 | "000-000", FALSE, FALSE, NULL, 158 | "OK", G_CALLBACK (signald_verify_ok_cb), 159 | "Cancel", NULL, 160 | sa->account, username, NULL, sa); 161 | } 162 | 163 | json_object_unref(data); 164 | } 165 | 166 | void 167 | signald_process_account(JsonArray *array, guint index_, JsonNode *element_node, gpointer user_data) 168 | { 169 | SignaldAccount *sa = (SignaldAccount *)user_data; 170 | JsonObject *obj = json_node_get_object(element_node); 171 | 172 | const char *username = purple_account_get_username(sa->account); 173 | JsonObject *address = json_object_get_object_member(obj, "address"); 174 | const char *uuid = json_object_get_string_member(address, "uuid"); 175 | const char *number = json_object_get_string_member(address, "number"); 176 | const char *account_id = json_object_get_string_member(obj, "account_id"); 177 | if (purple_strequal(account_id, username) || purple_strequal(number, username) || purple_strequal(uuid, username)) { 178 | // this is the current account 179 | sa->account_exists = TRUE; 180 | 181 | sa->uuid = g_strdup(uuid); 182 | purple_debug_info(SIGNALD_PLUGIN_ID, "Account uuid: %s\n", sa->uuid); 183 | 184 | gboolean pending = json_object_has_member(obj, "pending") && json_object_get_boolean_member(obj, "pending"); 185 | purple_debug_info(SIGNALD_PLUGIN_ID, "Account %s pending: %d\n", account_id, pending); 186 | if (pending) { 187 | // account is pending verification, try to link again 188 | signald_link_or_register(sa); 189 | } else { 190 | // account allegedly is ready for usage 191 | signald_subscribe(sa); 192 | } 193 | } 194 | } 195 | 196 | void 197 | signald_parse_account_list(SignaldAccount *sa, JsonArray *data) 198 | { 199 | sa->account_exists = FALSE; 200 | json_array_foreach_element(data, signald_process_account, sa); // lookup signald account 201 | 202 | // if Purple account does not exist in signald, link or register 203 | if (!sa->account_exists) { 204 | signald_link_or_register(sa); 205 | } 206 | } 207 | 208 | /* 209 | * Request information on accounts, including our own UUID. 210 | * This should be the first request after making a connection. 211 | */ 212 | void 213 | signald_request_accounts(SignaldAccount *sa) { 214 | JsonObject *data = json_object_new(); 215 | json_object_set_string_member(data, "type", "list_accounts"); 216 | signald_send_json_or_display_error(sa, data); 217 | json_object_unref(data); 218 | } 219 | 220 | void 221 | signald_process_finish_link(SignaldAccount *sa, JsonObject *obj) { 222 | // sync account's UUID with the one reported by signal 223 | obj = json_object_get_object_member(obj, "data"); 224 | if (obj) { 225 | obj = json_object_get_object_member(obj, "address"); 226 | if (obj) { 227 | sa->uuid = g_strdup(json_object_get_string_member(obj, "uuid")); 228 | } 229 | } 230 | // publish device name 231 | signald_set_device_name(sa); 232 | signald_subscribe(sa); 233 | } 234 | -------------------------------------------------------------------------------- /src/link.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | void 4 | signald_request_accounts(SignaldAccount *sa); 5 | 6 | void 7 | signald_set_device_name (SignaldAccount *sa); 8 | 9 | void 10 | signald_scan_qrcode (SignaldAccount *sa, gchar* qrimgdata, gsize qrimglen); 11 | 12 | void 13 | signald_parse_linking_uri (SignaldAccount *sa, JsonObject *obj); 14 | 15 | void 16 | signald_link_or_register (SignaldAccount *sa); 17 | 18 | void 19 | signald_parse_account_list (SignaldAccount *sa, JsonArray *data); 20 | 21 | void 22 | signald_process_finish_link(SignaldAccount *sa, JsonObject *obj); 23 | -------------------------------------------------------------------------------- /src/login.c: -------------------------------------------------------------------------------- 1 | #include // for sockaddr_un 2 | #include // for socket and read 3 | #include 4 | #include "purple_compat.h" 5 | #include "structs.h" 6 | #include "defines.h" 7 | #include "comms.h" 8 | #include "signald_procmgmt.h" 9 | #include "input.h" 10 | #include "reply.h" 11 | #include "receipt.h" 12 | 13 | #if !(GLIB_CHECK_VERSION(2, 67, 3)) 14 | #define g_memdup2 g_memdup 15 | #endif 16 | 17 | /* 18 | * This struct exchanges data between threads, see @try_connect. 19 | */ 20 | typedef struct { 21 | SignaldAccount *sa; 22 | gchar *socket_path; 23 | gchar *message; 24 | } SignaldConnectionAttempt; 25 | 26 | /* 27 | * See @execute_on_main_thread. 28 | */ 29 | static gboolean 30 | display_connection_error(void *data) { 31 | SignaldConnectionAttempt *sc = data; 32 | purple_connection_error(sc->sa->pc, PURPLE_CONNECTION_ERROR_NETWORK_ERROR, sc->message); 33 | g_free(sc->message); 34 | g_free(sc); 35 | return FALSE; 36 | } 37 | 38 | /* 39 | * See @execute_on_main_thread. 40 | */ 41 | static gboolean 42 | display_debug_info(void *data) { 43 | SignaldConnectionAttempt *sc = data; 44 | purple_debug_info(SIGNALD_PLUGIN_ID, "%s", sc->message); 45 | g_free(sc->message); 46 | g_free(sc); 47 | return FALSE; 48 | } 49 | 50 | /* 51 | * Every function writing to the GTK UI must be executed from the GTK main thread. 52 | * This function is a crutch for wrapping some purple functions: 53 | * 54 | * * purple_debug_info in display_debug_info 55 | * * purple_connection_error in display_connection_error 56 | * 57 | * Can only handle one message string instead of variardic arguments. 58 | */ 59 | static void 60 | execute_on_main_thread(GSourceFunc function, SignaldConnectionAttempt *sc, gchar * message) { 61 | sc->message = message; 62 | purple_timeout_add(0, function, g_memdup2(sc, sizeof *sc)); 63 | } 64 | 65 | /* 66 | * Tries to connect to a socket at a given location. 67 | * It is ought to be executed in a thread. 68 | * Only in case it does noes not succeed AND is the last thread to stop trying, 69 | * the situation is considered a connection failure. 70 | */ 71 | static void * 72 | do_try_connect(void * arg) { 73 | SignaldConnectionAttempt * sc = arg; 74 | 75 | struct sockaddr_un address = {.sun_family = AF_UNIX}; 76 | if (strlen(sc->socket_path)-1 > sizeof address.sun_path) { 77 | execute_on_main_thread(display_connection_error, sc, g_strdup_printf("socket path %s exceeds maximum length %lu!\n", sc->socket_path, sizeof address.sun_path)); 78 | } else { 79 | // fill path into sockaddr 80 | strcpy(address.sun_path, sc->socket_path); 81 | 82 | // create a socket 83 | int fd = socket(AF_UNIX, SOCK_STREAM, 0); 84 | if (fd < 0) { 85 | execute_on_main_thread(display_connection_error, sc, g_strdup_printf("Could not create socket: %s", strerror(errno))); 86 | } else { 87 | int32_t err = -1; 88 | 89 | // connect our socket to signald socket 90 | for(int try = 1; try <= SIGNALD_TIMEOUT_SECONDS && err != 0 && sc->sa->fd < 0; try++) { 91 | err = connect(fd, (struct sockaddr *) &address, sizeof(struct sockaddr_un)); 92 | execute_on_main_thread(display_debug_info, sc, g_strdup_printf("Connecting to %s (try #%d)...\n", address.sun_path, try)); 93 | sleep(1); // altogether wait SIGNALD_TIMEOUT_SECONDS 94 | } 95 | 96 | if (err == 0) { 97 | // successfully connected, tell purple to use our socket 98 | execute_on_main_thread(display_debug_info, sc, g_strdup_printf("Connected to %s.\n", address.sun_path)); 99 | sc->sa->fd = fd; 100 | sc->sa->readflags = MSG_DONTWAIT; 101 | sc->sa->watcher = purple_input_add(fd, PURPLE_INPUT_READ, signald_read_cb, sc->sa); 102 | } 103 | if (sc->sa->fd < 0) { 104 | // no concurrent connection attempt has been successful by now 105 | execute_on_main_thread(display_debug_info, sc, g_strdup_printf("No connection to %s after %d tries.\n", address.sun_path, SIGNALD_TIMEOUT_SECONDS)); 106 | 107 | sc->sa->socket_paths_count--; // this tread gives up trying 108 | // NOTE: although unlikely, it is possible that above decrement and other modifications or checks happen concurrently. 109 | // TODO: use a mutex where appropriate. 110 | if (sc->sa->socket_paths_count == 0) { 111 | // no trying threads are remaining 112 | execute_on_main_thread(display_connection_error, sc, sc->message = g_strdup("Unable to connect to any socket location.")); 113 | } 114 | } 115 | } 116 | } 117 | g_free(sc->socket_path); 118 | g_free(sc); 119 | return NULL; 120 | } 121 | 122 | /* 123 | * Starts a connection attempt in background. 124 | */ 125 | static void 126 | try_connect(SignaldAccount *sa, gchar *socket_path) { 127 | SignaldConnectionAttempt *sc = g_new0(SignaldConnectionAttempt, 1); 128 | sc->sa = sa; 129 | sc->socket_path = socket_path; 130 | pthread_t try_connect_thread; 131 | int err = pthread_create(&try_connect_thread, NULL, do_try_connect, (void*)sc); 132 | if (err == 0) { 133 | // detach thread so it is "free'd" as soon it terminates 134 | pthread_detach(try_connect_thread); 135 | } else { 136 | gchar *errmsg = g_strdup_printf("Could not create thread for connecting in background: %s", strerror(err)); 137 | purple_connection_error(sa->pc, PURPLE_CONNECTION_ERROR_NETWORK_ERROR, errmsg); 138 | g_free(errmsg); 139 | } 140 | } 141 | 142 | /* 143 | * Connect to signald socket. 144 | * Tries multiple possible default socket location at once in background. 145 | * In case the user has explicitly defined a socket location, only that one is considered. 146 | */ 147 | // TODO: find out how purple does connections in the gevent loop. use that instead of explicit threads. 148 | void 149 | signald_connect_socket(SignaldAccount *sa) { 150 | purple_connection_set_state(sa->pc, PURPLE_CONNECTION_CONNECTING); 151 | sa->fd = -1; // socket is not connected, no valid value for fd, yet 152 | sa->socket_paths_count = 1; // there is one path to try to connect to 153 | 154 | const gchar * user_socket_path = purple_account_get_string(sa->account, "socket", ""); 155 | if (user_socket_path && user_socket_path[0]) { 156 | try_connect(sa, g_strdup(user_socket_path)); 157 | } else { 158 | const gchar *xdg_runtime_dir = g_getenv("XDG_RUNTIME_DIR"); 159 | if (xdg_runtime_dir) { 160 | sa->socket_paths_count++; // there is another path to try to connect to 161 | gchar *xdg_socket_path = g_strdup_printf("%s/%s", xdg_runtime_dir, SIGNALD_GLOBAL_SOCKET_FILE); 162 | try_connect(sa, xdg_socket_path); 163 | } else { 164 | purple_debug_warning(SIGNALD_PLUGIN_ID, "Unable to read environment variable XDG_RUNTIME_DIR. Skipping the related socket location.\n"); 165 | } 166 | 167 | gchar * var_socket_path = g_strdup_printf("%s/%s", SIGNALD_GLOBAL_SOCKET_PATH_VAR, SIGNALD_GLOBAL_SOCKET_FILE); 168 | try_connect(sa, var_socket_path); 169 | } 170 | } 171 | 172 | /* 173 | * Connects to signald. 174 | * 175 | * The overall procedure is implemented in a rather convoluted manner. 176 | * For enhanced user experience, multiple possible socket paths are tried in parallel. 177 | * The easy way would be connecting to /run/signald/signald.sock on the main thread and tell users to adjust their set-ups. 178 | */ 179 | void signald_login(PurpleAccount *account) { 180 | PurpleConnection *pc = purple_account_get_connection(account); 181 | 182 | // this protocol does not support anything special right now 183 | PurpleConnectionFlags pc_flags; 184 | pc_flags = purple_connection_get_flags(pc); 185 | pc_flags |= PURPLE_CONNECTION_NO_FONTSIZE; 186 | pc_flags |= PURPLE_CONNECTION_NO_BGCOLOR; 187 | pc_flags |= PURPLE_CONNECTION_ALLOW_CUSTOM_SMILEY; 188 | purple_connection_set_flags(pc, pc_flags); 189 | 190 | SignaldAccount *sa = g_new0(SignaldAccount, 1); 191 | 192 | purple_connection_set_protocol_data(pc, sa); 193 | 194 | sa->account = account; 195 | sa->pc = pc; 196 | sa->input_buffer_position = sa->input_buffer; 197 | 198 | sa->replycache = signald_replycache_init(); 199 | signald_receipts_init(sa); 200 | 201 | // Check account settings whether signald is globally running 202 | // (controlled by the system or the user) or whether it should 203 | // be controlled by the plugin. 204 | if (purple_account_get_bool(sa->account, "handle-signald", FALSE)) { 205 | signald_signald_start(sa->account); 206 | } 207 | 208 | signald_connect_socket(sa); 209 | } 210 | 211 | 212 | void signald_subscribe(SignaldAccount *sa) 213 | { 214 | // subscribe to the configured number 215 | JsonObject *data = json_object_new(); 216 | json_object_set_string_member(data, "type", "subscribe"); 217 | // TODO: subscribe with uuid 218 | json_object_set_string_member(data, "account", purple_account_get_username(sa->account)); 219 | signald_send_json_or_display_error(sa, data); 220 | json_object_unref(data); 221 | } 222 | 223 | void signald_close (PurpleConnection *pc) { 224 | SignaldAccount *sa = purple_connection_get_protocol_data(pc); 225 | 226 | // stop sending receipts 227 | signald_receipts_destroy(sa); 228 | 229 | // free reply cache 230 | signald_replycache_free(sa->replycache); 231 | 232 | // remove input watcher 233 | purple_input_remove(sa->watcher); 234 | sa->watcher = 0; 235 | 236 | if (sa->uuid) { 237 | // own UUID is kown, unsubscribe account 238 | JsonObject *data = json_object_new(); 239 | json_object_set_string_member(data, "type", "unsubscribe"); 240 | json_object_set_string_member(data, "account", sa->uuid); 241 | if (purple_connection_get_state(pc) == PURPLE_CONNECTION_CONNECTED) { 242 | if (signald_send_json(sa, data)) { 243 | // read one last time for acknowledgement of unsubscription 244 | // NOTE: this will block forever in case signald stalls 245 | sa->readflags = 0; 246 | signald_read_cb(sa, 0, 0); 247 | } else { 248 | purple_connection_error(sa->pc, PURPLE_CONNECTION_ERROR_NETWORK_ERROR, "Could not write message for unsubscribing."); 249 | purple_debug_error(SIGNALD_PLUGIN_ID, "Could not write message for unsubscribing: %s", strerror(errno)); 250 | } 251 | } 252 | json_object_unref(data); 253 | // now free UUID 254 | g_free(sa->uuid); 255 | sa->uuid = NULL; 256 | } 257 | 258 | close(sa->fd); 259 | sa->fd = 0; 260 | 261 | g_free(sa); 262 | 263 | signald_connection_closed(); 264 | } 265 | -------------------------------------------------------------------------------- /src/login.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "structs.h" 5 | 6 | void signald_login(PurpleAccount *account); 7 | void signald_subscribe(SignaldAccount *sa); 8 | void signald_close(PurpleConnection *pc); 9 | -------------------------------------------------------------------------------- /src/message.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "message.h" 5 | #include "defines.h" 6 | #include "attachments.h" 7 | #include "comms.h" 8 | #include "groups.h" 9 | #include "purple_compat.h" 10 | #include "receipt.h" 11 | #include "reply.h" 12 | #include "groups.h" 13 | #include "json-utils.h" 14 | 15 | const char * 16 | signald_get_uuid_from_address(JsonObject *obj, const char *address_key) 17 | { 18 | JsonObject *address = json_object_get_object_member(obj, address_key); 19 | if (address == NULL) { 20 | return NULL; 21 | } else { 22 | return (const char *)json_object_get_string_member(address, "uuid"); 23 | } 24 | } 25 | 26 | static gboolean 27 | signald_is_uuid(const gchar *identifier) { 28 | if (identifier) { 29 | return strlen(identifier) == 36; 30 | } else { 31 | return FALSE; 32 | } 33 | } 34 | 35 | static gboolean 36 | signald_is_number(const gchar *identifier) { 37 | return identifier && identifier[0] == '+'; 38 | } 39 | 40 | void 41 | signald_set_recipient(JsonObject *obj, const gchar *key, const gchar *recipient) 42 | { 43 | g_return_if_fail(recipient); 44 | g_return_if_fail(obj); 45 | // if contact was added manually and not yet migrated, the recipient might still be a number, not a UUID 46 | char * address_type = NULL; 47 | if (signald_is_number(recipient)) { 48 | address_type = "number"; 49 | } else if (signald_is_uuid(recipient)) { 50 | address_type = "uuid"; 51 | } 52 | g_return_if_fail(obj); 53 | JsonObject *address = json_object_new(); 54 | json_object_set_string_member(address, address_type, recipient); 55 | json_object_set_object_member(obj, key, address); 56 | } 57 | 58 | gboolean 59 | signald_format_message(SignaldAccount *sa, JsonObject *data, GString **target, gboolean *has_attachment) 60 | { 61 | // handle attachments, creating appropriate message content (always allocates *target) 62 | *target = signald_prepare_attachments_message(sa, data); 63 | 64 | if (json_object_has_member(data, "sticker")) { 65 | JsonObject *sticker = json_object_get_object_member(data, "sticker"); 66 | JsonObject *attachment = json_object_get_object_member(sticker, "attachment"); 67 | signald_parse_attachment(sa, attachment, *target); 68 | } 69 | 70 | if ((*target)->len > 0) { 71 | *has_attachment = TRUE; 72 | } else { 73 | *has_attachment = FALSE; 74 | } 75 | 76 | if (json_object_has_member(data, "quote")) { 77 | JsonObject *quote = json_object_get_object_member(data, "quote"); 78 | JsonObject *author = json_object_get_object_member(quote, "author"); 79 | const char *uuid = json_object_get_string_member(author, "uuid"); 80 | PurpleBuddy *buddy = purple_find_buddy(sa->account, uuid); 81 | const char *alias = purple_buddy_get_alias(buddy); 82 | if (alias && alias[0]) { 83 | g_string_append_printf(*target, "%s wrote:\n", alias); 84 | } 85 | const char *text = json_object_get_string_member(quote, "text"); 86 | // TODO: quoted text can have metions. resolve them. 87 | gchar **lines = g_strsplit(text, "\n", 0); 88 | for (int i = 0; lines[i] != NULL; i++) { 89 | g_string_append_printf(*target, "> %s\n", lines[i]); 90 | } 91 | g_strfreev(lines); 92 | } 93 | 94 | if (json_object_has_member(data, "reaction")) { 95 | JsonObject *reaction = json_object_get_object_member(data, "reaction"); 96 | const char *emoji = json_object_get_string_member(reaction, "emoji"); 97 | const gboolean remove = json_object_get_boolean_member(reaction, "remove"); 98 | const time_t targetSentTimestamp = json_object_get_int_member(reaction, "targetSentTimestamp") / 1000; 99 | struct tm *tm = localtime(&targetSentTimestamp); 100 | if (remove) { 101 | g_string_printf(*target, "removed their %s reaction.", emoji); 102 | } else { 103 | g_string_printf(*target, "reacted with %s (to message from %s).", emoji, purple_date_format_long(tm)); 104 | } 105 | } 106 | 107 | if (json_object_has_member(data, "groupV2")) { 108 | JsonObject *groupV2 = json_object_get_object_member(data, "groupV2"); 109 | if (json_object_has_member(groupV2, "group_change")) { 110 | g_string_append(*target, "made changes to this group (settings, permissions, members). This plug-in cannot show the details."); 111 | // TODO: actually process the change 112 | // just update the entire group info for now 113 | signald_request_group_info(sa, json_object_get_string_member(groupV2, "id")); 114 | } 115 | } 116 | 117 | // append actual message text 118 | const char *body = json_object_get_string_member(data, "body"); 119 | if (body != NULL && body[0]) { 120 | // TODO: move this into a separate function. use it here and on quoted text as well 121 | JsonArray *mentions = json_object_get_array_member_or_null(data, "mentions"); 122 | if (mentions == NULL) { 123 | g_string_append(*target, body); 124 | } else { 125 | const char mention_glyph[] = {0xEF, 0xBF, 0xBC, 0x00}; //"" 126 | gchar **bodyparts = g_strsplit(body, mention_glyph, -1); 127 | if (bodyparts[0] != NULL) { 128 | g_string_append(*target, bodyparts[0]); 129 | for(int i = 0; bodyparts[i+1] != NULL; i++) { 130 | JsonObject *mention = json_array_get_object_element(mentions, i); // NOTE: this assumes mentions are sorted by their start property 131 | const char *uuid = json_object_get_string_member(mention, "uuid"); 132 | const char *alias = NULL; 133 | if (purple_strequal(uuid, sa->uuid)) { 134 | alias = purple_account_get_alias(sa->account); 135 | // TODO: add flag PURPLE_MESSAGE_NICK 136 | } else { 137 | PurpleBuddy *buddy = purple_find_buddy(sa->account, uuid); 138 | alias = purple_buddy_get_alias(buddy); 139 | } 140 | if (alias == NULL || alias[0] == 0) { 141 | alias = uuid; 142 | } 143 | const char thin_space[] = {0xE2, 0x80, 0x89, 0x00}; 144 | g_string_append(*target, thin_space); 145 | g_string_append(*target, "@"); 146 | g_string_append(*target, alias); 147 | g_string_append(*target, thin_space); 148 | g_string_append(*target, bodyparts[i+1]); 149 | } 150 | } 151 | g_strfreev(bodyparts); 152 | } 153 | } 154 | 155 | // TODO: Pidgin uses HTML to style messages, but signald is plain-text-ish. Use purple_markup_escape_text somewhere. 156 | 157 | return (*target)->len > 0; // if message is not empty, formatting was successful 158 | } 159 | 160 | void 161 | signald_process_message(SignaldAccount *sa, JsonObject *obj) 162 | { 163 | // timestamp and data_message.timestamp seem to always be the same value (message sent time) 164 | // server_receiver_timestamp is when the server received the message 165 | // server_deliver_timestamp is when the server delivered the message 166 | gint64 timestamp = json_object_get_int_member(obj, "timestamp"); 167 | 168 | const gchar * sender_uuid = NULL; 169 | JsonObject *message_data = NULL; 170 | JsonObject * sent = NULL; 171 | 172 | // source is always the author of the message 173 | JsonObject * source = json_object_get_object_member(obj, "source"); 174 | sender_uuid = json_object_get_string_member(source, "uuid"); 175 | 176 | if (json_object_has_member(obj, "sync_message")) { 177 | JsonObject * sync_message = json_object_get_object_member(obj, "sync_message"); 178 | if (json_object_has_member(sync_message, "sent")) { 179 | sent = json_object_get_object_member(sync_message, "sent"); 180 | if (json_object_has_member(sent, "destination")) { 181 | // for synced messages, purple does not need the author, 182 | // but rather the conversation defined by recipient 183 | // this does it for direc messages, chats are handled below 184 | sender_uuid = signald_get_uuid_from_address(sent, "destination"); 185 | } 186 | message_data = json_object_get_object_member(sent, "message"); 187 | } 188 | } else if (json_object_has_member(obj, "data_message")) { 189 | message_data = json_object_get_object_member(obj, "data_message"); 190 | } 191 | 192 | if (message_data == NULL) { 193 | purple_debug_warning(SIGNALD_PLUGIN_ID, "Ignoring message without usable payload.\n"); 194 | } else { 195 | const gchar *groupId = NULL; 196 | if (json_object_has_member(message_data, "groupV2")) { 197 | JsonObject *groupInfo = json_object_get_object_member(message_data, "groupV2"); 198 | groupId = json_object_get_string_member(groupInfo, "id"); 199 | } 200 | signald_display_message(sa, sender_uuid, groupId, timestamp, sent != NULL, message_data); 201 | } 202 | } 203 | 204 | void 205 | signald_process_typing(SignaldAccount *sa, JsonObject *obj) 206 | { 207 | JsonObject *message_data = json_object_get_object_member(obj, "typing_message"); 208 | JsonObject *source = json_object_get_object_member(obj, "source"); 209 | const gchar *sender_uuid = json_object_get_string_member(source, "uuid"); 210 | 211 | if (message_data == NULL) { 212 | purple_debug_warning(SIGNALD_PLUGIN_ID, "Ignoring typing message without usable payload.\n"); 213 | } else if (json_object_has_member(message_data, "action")) { 214 | const gchar *action = json_object_get_string_member(message_data, "action"); 215 | if (strcmp(action, "STARTED") == 0) { 216 | purple_serv_got_typing(sa->pc, sender_uuid, 0, PURPLE_IM_TYPING); 217 | } else if (strcmp(action, "STOPPED") == 0) { 218 | purple_serv_got_typing(sa->pc, sender_uuid, 0, PURPLE_IM_NOT_TYPING); 219 | } 220 | } 221 | } 222 | 223 | int 224 | signald_send_message(SignaldAccount *sa, const gchar *who, gboolean is_chat, const char *message) 225 | { 226 | JsonObject *data = json_object_new(); 227 | 228 | json_object_set_string_member(data, "type", "send"); 229 | json_object_set_string_member(data, "account", sa->uuid); 230 | 231 | if (is_chat) { 232 | json_object_set_string_member(data, "recipientGroupId", who); 233 | } else { 234 | signald_set_recipient(data, "recipientAddress", who); 235 | } 236 | 237 | SignaldMessage *reply_message = signald_replycache_check(sa, message); 238 | if (reply_message != NULL) { 239 | signald_replycache_apply(data, reply_message); 240 | message = signald_replycache_strip_needle(message); 241 | } 242 | JsonArray *attachments = json_array_new(); 243 | char *textonly = signald_detach_images(message, attachments); 244 | json_object_set_array_member(data, "attachments", attachments); 245 | char *plain = purple_unescape_html(textonly); 246 | g_free(textonly); 247 | json_object_set_string_member(data, "messageBody", plain); 248 | 249 | int ret = !purple_account_get_bool(sa->account, SIGNALD_OPTION_WAIT_SEND_ACKNOWLEDEMENT, FALSE); 250 | if (!signald_send_json(sa, data)) { 251 | ret = -errno; 252 | } 253 | json_object_unref(data); 254 | 255 | // wait for signald to acknowledge the message has been sent 256 | // for displaying the outgoing message later, it is stored locally 257 | if (ret == 0) { 258 | // free last message just in case it still lingers in memory 259 | g_free(sa->last_message); 260 | // store message for later echo 261 | // NOTE: this stores the message "as sent" (without markup, without images) 262 | sa->last_message = g_strdup(plain); 263 | // store this as the currently active conversation 264 | sa->last_conversation = purple_find_conversation_with_account(PURPLE_CONV_TYPE_ANY, who, sa->account); 265 | if (sa->last_conversation == NULL) { 266 | // no appropriate conversation was found. maybe it is a group? 267 | PurpleConvChat *conv_chat = purple_conversations_find_chat_with_account(who, sa->account); 268 | if (conv_chat != NULL) { 269 | sa->last_conversation = conv_chat->conv; 270 | } 271 | } 272 | } 273 | 274 | g_free(plain); 275 | return ret; 276 | } 277 | 278 | struct SignaldSendResult { 279 | SignaldAccount *sa; 280 | int devices_count; 281 | }; 282 | 283 | static void 284 | signald_send_check_result(JsonArray* results, guint i, JsonNode* result_node, gpointer user_data) { 285 | struct SignaldSendResult * sr = (struct SignaldSendResult *)user_data; 286 | JsonObject * result = json_node_get_object(result_node); 287 | JsonObject * success = json_object_get_object_member(result, "success"); 288 | if (success) { 289 | JsonArray * devices = json_object_get_array_member(success, "devices"); 290 | if (devices) { 291 | sr->devices_count += json_array_get_length(devices); 292 | } 293 | } 294 | 295 | const gchar * failure = NULL; 296 | // NOTE: These failures might actually be orthogonal. This only regards the first one. 297 | if (json_object_has_member(result, "identityFailure")) { 298 | failure = "identityFailure"; 299 | } else if (json_object_get_boolean_member(result, "networkFailure")) { 300 | failure = "networkFailure"; 301 | } else if (json_object_has_member(result, "proof_required_failure")) { 302 | failure = "proof_required_failure"; 303 | } else if (json_object_get_boolean_member(result, "unregisteredFailure")) { 304 | failure = "unregisteredFailure"; 305 | } 306 | if (failure) { 307 | JsonObject * address = json_object_get_object_member(result, "address"); 308 | const gchar * number = json_object_get_string_member(address, "number"); 309 | const gchar * uuid = json_object_get_string_member(address, "uuid"); 310 | gchar * errmsg = g_strdup_printf("Message was not delivered to %s (%s) due to %s.", number, uuid, failure); 311 | purple_conversation_write(sr->sa->last_conversation, NULL, errmsg, PURPLE_MESSAGE_ERROR, time(NULL)); 312 | g_free(errmsg); 313 | } 314 | } 315 | 316 | void 317 | signald_send_acknowledged(SignaldAccount *sa, JsonObject *data) { 318 | struct SignaldSendResult sr; 319 | sr.sa = sa; 320 | sr.devices_count = 0; 321 | JsonArray * results = json_object_get_array_member(data, "results"); 322 | if (results) { 323 | if (json_array_get_length(results) == 0) { 324 | // when sending message to self, the results array is empty 325 | // TODO: check if recipient actually was sa->uuid 326 | sr.devices_count = 1; 327 | } else { 328 | json_array_foreach_element(results, signald_send_check_result, &sr); 329 | } 330 | } 331 | if (sa->last_conversation && sa->uuid && sa->last_message) { 332 | if (sr.devices_count > 0) { 333 | const guint64 timestamp_micro = json_object_get_int_member(data, "timestamp"); 334 | PurpleMessageFlags flags = PURPLE_MESSAGE_SEND | PURPLE_MESSAGE_REMOTE_SEND | PURPLE_MESSAGE_DELAYED; 335 | purple_conversation_write(sa->last_conversation, sa->uuid, sa->last_message, flags, timestamp_micro / 1000); 336 | signald_replycache_add_message(sa, sa->last_conversation, sa->uuid, timestamp_micro, sa->last_message); 337 | g_free(sa->last_message); 338 | sa->last_message = NULL; 339 | } else { 340 | // form purple_conv_present_error() 341 | purple_conversation_write(sa->last_conversation, NULL, "Message was not delivered to any devices.", PURPLE_MESSAGE_ERROR, time(NULL)); 342 | } 343 | } else if (sr.devices_count == 0) { 344 | purple_debug_error(SIGNALD_PLUGIN_ID, "A message was not delivered to any devices.\n"); 345 | } 346 | } 347 | 348 | void 349 | signald_display_message(SignaldAccount *sa, const char *who, const char *groupId, gint64 timestamp_micro, gboolean is_sync_message, JsonObject *message_data) 350 | { 351 | // Signal's integer timestamps are in microseconds, but purple time_t in milliseconds. 352 | time_t timestamp_milli = timestamp_micro / 1000; 353 | PurpleMessageFlags flags = 0; 354 | GString *content = NULL; 355 | gboolean has_attachment = FALSE; 356 | if (signald_format_message(sa, message_data, &content, &has_attachment)) { 357 | if (has_attachment) { 358 | flags |= PURPLE_MESSAGE_IMAGES; 359 | } 360 | if (is_sync_message) { 361 | flags |= PURPLE_MESSAGE_SEND | PURPLE_MESSAGE_REMOTE_SEND | PURPLE_MESSAGE_DELAYED; 362 | } else { 363 | flags |= PURPLE_MESSAGE_RECV; 364 | } 365 | PurpleConversation * conv = NULL; 366 | if (groupId) { 367 | conv = signald_enter_group_chat(sa->pc, groupId, NULL); 368 | purple_conv_chat_write(PURPLE_CONV_CHAT(conv), who, content->str, flags, timestamp_milli); 369 | // TODO: use serv_got_chat_in for more traditonal behaviour 370 | // though it compares who against chat->nick and sets the SEND/RECV flags itself 371 | signald_mark_read_chat(sa, timestamp_micro, PURPLE_CONV_CHAT(conv)->users); 372 | } else { 373 | if (flags & PURPLE_MESSAGE_RECV) { 374 | // incoming message 375 | purple_serv_got_im(sa->pc, who, content->str, flags, timestamp_milli); 376 | // although purple_serv_got_im did most of the work, we still need to fill conv for populating the message cache 377 | conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, who, sa->account); 378 | } else { 379 | // synced message (sent by ourselves via other device) 380 | conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, who, sa->account); 381 | if (conv == NULL) { 382 | conv = purple_conversation_new(PURPLE_CONV_TYPE_IM, sa->account, who); 383 | } 384 | purple_conv_im_write(PURPLE_CONV_IM(conv), who, content->str, flags, timestamp_milli); 385 | } 386 | signald_mark_read(sa, timestamp_micro, who); 387 | } 388 | signald_replycache_add_message(sa, conv, who, timestamp_micro, json_object_get_string_member_or_null(message_data, "body")); 389 | } else { 390 | purple_debug_warning(SIGNALD_PLUGIN_ID, "signald_format_message returned false.\n"); 391 | } 392 | g_string_free(content, TRUE); 393 | } 394 | 395 | int 396 | signald_send_im(PurpleConnection *pc, const gchar *who, const gchar *message, PurpleMessageFlags flags) 397 | { 398 | SignaldAccount *sa = purple_connection_get_protocol_data(pc); 399 | return signald_send_message(sa, who, FALSE, message); 400 | } 401 | 402 | int 403 | signald_send_chat(PurpleConnection *pc, int id, const char *message, PurpleMessageFlags flags) 404 | { 405 | SignaldAccount *sa = purple_connection_get_protocol_data(pc); 406 | PurpleConversation *conv = purple_find_chat(pc, id); 407 | if (conv != NULL) { 408 | gchar *groupId = (gchar *)purple_conversation_get_data(conv, "name"); 409 | if (groupId != NULL) { 410 | int ret = signald_send_message(sa, groupId, TRUE, message); 411 | if (ret > 0) { 412 | // immediate local echo (ret == 0 indicates delayed local echo) 413 | purple_conversation_write(conv, sa->uuid, message, flags, time(NULL)); 414 | } 415 | return ret; 416 | } 417 | return -6; // a negative value to indicate failure. chose ENXIO "no such address" 418 | } 419 | return -6; // a negative value to indicate failure. chose ENXIO "no such address" 420 | } 421 | -------------------------------------------------------------------------------- /src/message.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "structs.h" 4 | 5 | const char * 6 | signald_get_uuid_from_address(JsonObject *obj, const char *address_key); 7 | 8 | gboolean 9 | signald_format_message(SignaldAccount *sa, JsonObject *data, GString **target, gboolean *has_attachment); 10 | 11 | void 12 | signald_process_message(SignaldAccount *sa, JsonObject *obj); 13 | 14 | void 15 | signald_process_typing(SignaldAccount *sa, JsonObject *obj); 16 | 17 | int 18 | signald_send_message(SignaldAccount *sa, const gchar *who, gboolean is_chat, const char *message); 19 | 20 | void 21 | signald_send_acknowledged(SignaldAccount *sa, JsonObject *data); 22 | 23 | void 24 | signald_display_message(SignaldAccount *sa, const char *who, const char *groupId, gint64 timestamp, gboolean is_sync_message, JsonObject *message_data); 25 | 26 | int 27 | signald_send_im(PurpleConnection *pc, const gchar *who, const gchar *message, PurpleMessageFlags flags); 28 | 29 | int 30 | signald_send_chat(PurpleConnection *pc, int id, const char *message, PurpleMessageFlags flags); 31 | 32 | void 33 | signald_set_recipient(JsonObject *obj, const gchar *key, const gchar *recipient); 34 | -------------------------------------------------------------------------------- /src/options.c: -------------------------------------------------------------------------------- 1 | #define _DEFAULT_SOURCE // for gethostname in unistd.h 2 | #include 3 | #include 4 | #include "defines.h" 5 | #include "options.h" 6 | 7 | // from https://github.com/ars3niy/tdlib-purple/blob/master/tdlib-purple.cpp 8 | static GList * add_choice(GList *choices, const char *description, const char *value) 9 | { 10 | PurpleKeyValuePair *kvp = g_new0(PurpleKeyValuePair, 1); 11 | kvp->key = g_strdup(description); 12 | kvp->value = g_strdup(value); 13 | return g_list_append(choices, kvp); 14 | } 15 | 16 | GList * 17 | signald_add_account_options(GList *account_options) 18 | { 19 | PurpleAccountOption *option; 20 | 21 | /* 22 | option = purple_account_option_bool_new( 23 | "Link to an existing account", 24 | "link", 25 | TRUE 26 | ); 27 | account_options = g_list_append(account_options, option); 28 | */ 29 | 30 | char hostname[HOST_NAME_MAX + 1]; 31 | if (gethostname(hostname, HOST_NAME_MAX)) { 32 | strcpy(hostname, SIGNALD_DEFAULT_DEVICENAME); 33 | } 34 | 35 | option = purple_account_option_string_new( 36 | "Name of the device for linking", 37 | "device-name", 38 | hostname // strdup happens internally 39 | ); 40 | account_options = g_list_append(account_options, option); 41 | 42 | option = purple_account_option_bool_new( 43 | "Daemon signald is controlled by pidgin, not globally or by the user", 44 | "handle-signald", 45 | FALSE 46 | ); 47 | account_options = g_list_append(account_options, option); 48 | 49 | option = purple_account_option_string_new( 50 | "Custom socket location", 51 | "socket", 52 | "" 53 | ); 54 | account_options = g_list_append(account_options, option); 55 | 56 | { 57 | GList *choices = NULL; 58 | choices = add_choice(choices, "Online", SIGNALD_STATUS_STR_ONLINE); 59 | choices = add_choice(choices, "Away", SIGNALD_STATUS_STR_AWAY); 60 | choices = add_choice(choices, "Offline", SIGNALD_STATUS_STR_OFFLINE); 61 | option = purple_account_option_list_new( 62 | "Contacts' online state", 63 | "fake-status", 64 | choices 65 | ); 66 | account_options = g_list_append(account_options, option); 67 | } 68 | 69 | option = purple_account_option_int_new( 70 | "Number of incoming messages to cache for replying", 71 | SIGNALD_OPTION_REPLY_CACHE, 72 | 0 73 | ); 74 | account_options = g_list_append(account_options, option); 75 | 76 | option = purple_account_option_bool_new( 77 | "Mark messages as read", 78 | SIGNALD_OPTION_MARK_READ, 79 | FALSE 80 | ); 81 | account_options = g_list_append(account_options, option); 82 | 83 | option = purple_account_option_bool_new( 84 | "Display receipts in conversation", 85 | SIGNALD_OPTION_DISPLAY_RECEIPTS, 86 | FALSE 87 | ); 88 | account_options = g_list_append(account_options, option); 89 | 90 | option = purple_account_option_bool_new( 91 | "Wait for send acknowledgement", 92 | SIGNALD_OPTION_WAIT_SEND_ACKNOWLEDEMENT, 93 | FALSE 94 | ); 95 | account_options = g_list_append(account_options, option); 96 | 97 | option = purple_account_option_bool_new( 98 | "Automatically accept invitations to groups", 99 | "auto-accept-invitations", 100 | FALSE 101 | ); 102 | account_options = g_list_append(account_options, option); 103 | 104 | option = purple_account_option_bool_new( 105 | "Overwrite custom group icons by group avatar", 106 | "use-group-avatar", 107 | TRUE 108 | ); 109 | account_options = g_list_append(account_options, option); 110 | 111 | option = purple_account_option_bool_new( 112 | "Serve attachments from external location", 113 | SIGNALD_ACCOUNT_OPT_EXT_ATTACHMENTS, 114 | FALSE 115 | ); 116 | account_options = g_list_append(account_options, option); 117 | 118 | option = purple_account_option_string_new( 119 | "External attachment storage directory", 120 | SIGNALD_ACCOUNT_OPT_EXT_ATTACHMENTS_DIR, 121 | "" 122 | ); 123 | account_options = g_list_append(account_options, option); 124 | 125 | option = purple_account_option_string_new( 126 | "External attachment URL", 127 | SIGNALD_ACCOUNT_OPT_EXT_ATTACHMENTS_URL, 128 | "" 129 | ); 130 | account_options = g_list_append(account_options, option); 131 | 132 | return account_options; 133 | } 134 | -------------------------------------------------------------------------------- /src/options.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | GList * signald_add_account_options(GList *account_options); 6 | -------------------------------------------------------------------------------- /src/purple_compat.h: -------------------------------------------------------------------------------- 1 | /* 2 | * signald plugin for libpurple 3 | * Copyright (C) 2016-2017 Eion Robb 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #include 20 | #if PURPLE_VERSION_CHECK(3, 0, 0) 21 | #include 22 | #endif 23 | 24 | #ifndef PURPLE_PLUGINS 25 | #define PURPLE_PLUGINS 26 | #endif 27 | 28 | #ifdef _WIN32 29 | #include 30 | #endif 31 | 32 | // Purple2 compat functions 33 | #if !PURPLE_VERSION_CHECK(3, 0, 0) 34 | 35 | #define purple_connection_error purple_connection_error_reason 36 | #define purple_connection_get_protocol purple_connection_get_prpl 37 | #define PURPLE_CONNECTION_CONNECTING PURPLE_CONNECTING 38 | #define PURPLE_CONNECTION_CONNECTED PURPLE_CONNECTED 39 | #define PURPLE_CONNECTION_DISCONNECTED PURPLE_DISCONNECTED 40 | #define PURPLE_CONNECTION_FLAG_HTML PURPLE_CONNECTION_HTML 41 | #define PURPLE_CONNECTION_FLAG_NO_BGCOLOR PURPLE_CONNECTION_NO_BGCOLOR 42 | #define PURPLE_CONNECTION_FLAG_NO_FONTSIZE PURPLE_CONNECTION_NO_FONTSIZE 43 | #define PURPLE_CONNECTION_FLAG_NO_IMAGES PURPLE_CONNECTION_NO_IMAGES 44 | #define purple_connection_set_flags(pc, f) ((pc)->flags = (f)) 45 | #define purple_connection_get_flags(pc) ((pc)->flags) 46 | #define purple_blist_find_group purple_find_group 47 | #define purple_protocol_action_get_connection(action) ((PurpleConnection *) (action)->context) 48 | #define purple_protocol_action_new purple_plugin_action_new 49 | #define purple_protocol_get_id purple_plugin_get_id 50 | #define PurpleProtocolAction PurplePluginAction 51 | #define PurpleProtocolChatEntry struct proto_chat_entry 52 | #define PurpleChatConversation PurpleConvChat 53 | #define PurpleIMConversation PurpleConvIm 54 | #define purple_conversations_find_chat_with_account(id, account) \ 55 | PURPLE_CONV_CHAT(purple_find_conversation_with_account(PURPLE_CONV_TYPE_CHAT, id, account)) 56 | #define purple_chat_conversation_has_left purple_conv_chat_has_left 57 | #define PurpleConversationUpdateType PurpleConvUpdateType 58 | #define PURPLE_CONVERSATION_UPDATE_UNSEEN PURPLE_CONV_UPDATE_UNSEEN 59 | #define PURPLE_IS_IM_CONVERSATION(conv) (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) 60 | #define PURPLE_IS_CHAT_CONVERSATION(conv) (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) 61 | #define PURPLE_CONVERSATION(chatorim) ((chatorim) == NULL ? NULL : (chatorim)->conv) 62 | #define PURPLE_IM_CONVERSATION(conv) PURPLE_CONV_IM(conv) 63 | #define PURPLE_CHAT_CONVERSATION(conv) PURPLE_CONV_CHAT(conv) 64 | #define purple_conversation_present_error purple_conv_present_error 65 | #define purple_serv_got_joined_chat(pc, id, name) PURPLE_CONV_CHAT(serv_got_joined_chat(pc, id, name)) 66 | static inline PurpleConvChat * 67 | purple_conversations_find_chat(PurpleConnection *pc, int id) 68 | { 69 | PurpleConversation *conv = purple_find_chat(pc, id); 70 | 71 | if (conv != NULL) { 72 | return PURPLE_CONV_CHAT(conv); 73 | } 74 | 75 | return NULL; 76 | } 77 | #define purple_serv_got_chat_in serv_got_chat_in 78 | #define purple_chat_conversation_add_user purple_conv_chat_add_user 79 | #define purple_chat_conversation_add_users purple_conv_chat_add_users 80 | #define purple_chat_conversation_remove_user purple_conv_chat_remove_user 81 | #define purple_chat_conversation_clear_users purple_conv_chat_clear_users 82 | #define purple_chat_conversation_has_user purple_conv_chat_find_user 83 | #define purple_chat_conversation_rename_user purple_conv_chat_rename_user 84 | #define purple_chat_conversation_get_topic purple_conv_chat_get_topic 85 | #define purple_chat_conversation_set_topic purple_conv_chat_set_topic 86 | #define purple_chat_conversation_set_nick purple_conv_chat_set_nick 87 | #define PurpleChatUserFlags PurpleConvChatBuddyFlags 88 | #define PURPLE_CHAT_USER_NONE PURPLE_CBFLAGS_NONE 89 | #define PURPLE_CHAT_USER_OP PURPLE_CBFLAGS_OP 90 | #define PURPLE_CHAT_USER_FOUNDER PURPLE_CBFLAGS_FOUNDER 91 | #define PURPLE_CHAT_USER_TYPING PURPLE_CBFLAGS_TYPING 92 | #define PURPLE_CHAT_USER_AWAY PURPLE_CBFLAGS_AWAY 93 | #define PURPLE_CHAT_USER_HALFOP PURPLE_CBFLAGS_HALFOP 94 | #define PURPLE_CHAT_USER_VOICE PURPLE_CBFLAGS_VOICE 95 | #define PURPLE_CHAT_USER_TYPING PURPLE_CBFLAGS_TYPING 96 | #define PurpleChatUser PurpleConvChatBuddy 97 | static inline PurpleChatUser * 98 | purple_chat_conversation_find_user(PurpleChatConversation *chat, const char *name) 99 | { 100 | PurpleChatUser *cb = purple_conv_chat_cb_find(chat, name); 101 | 102 | if (cb != NULL) { 103 | g_dataset_set_data(cb, "chat", chat); 104 | } 105 | 106 | return cb; 107 | } 108 | #define purple_chat_user_get_flags(cb) purple_conv_chat_user_get_flags(g_dataset_get_data((cb), "chat"), (cb)->name) 109 | #define purple_chat_user_set_flags(cb, f) purple_conv_chat_user_set_flags(g_dataset_get_data((cb), "chat"), (cb)->name, (f)) 110 | #define purple_chat_user_set_alias(cb, a) (g_free((cb)->alias), (cb)->alias = g_strdup(a)) 111 | #define PurpleIMTypingState PurpleTypingState 112 | #define PURPLE_IM_NOT_TYPING PURPLE_NOT_TYPING 113 | #define PURPLE_IM_TYPING PURPLE_TYPING 114 | #define PURPLE_IM_TYPED PURPLE_TYPED 115 | #define purple_conversation_get_connection purple_conversation_get_gc 116 | #define purple_conversation_write_system_message(conv, message, flags) purple_conversation_write((conv), NULL, (message), ((flags) | PURPLE_MESSAGE_SYSTEM), time(NULL)) 117 | #define purple_chat_conversation_get_id purple_conv_chat_get_id 118 | #define PURPLE_CMD_FLAG_PROTOCOL_ONLY PURPLE_CMD_FLAG_PRPL_ONLY 119 | #define PURPLE_IS_BUDDY PURPLE_BLIST_NODE_IS_BUDDY 120 | #define PURPLE_IS_CHAT PURPLE_BLIST_NODE_IS_CHAT 121 | #define purple_chat_get_name_only purple_chat_get_name 122 | #define purple_blist_find_buddy purple_find_buddy 123 | #define purple_serv_got_alias serv_got_alias 124 | #define purple_account_set_private_alias purple_account_set_alias 125 | #define purple_account_get_private_alias purple_account_get_alias 126 | #define purple_protocol_got_user_status purple_prpl_got_user_status 127 | #define purple_protocol_got_user_idle purple_prpl_got_user_idle 128 | #define purple_serv_got_im serv_got_im 129 | #define purple_serv_got_typing serv_got_typing 130 | #define purple_conversations_find_im_with_account(name, account) \ 131 | PURPLE_CONV_IM(purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, name, account)) 132 | #define purple_im_conversation_new(account, from) PURPLE_CONV_IM(purple_conversation_new(PURPLE_CONV_TYPE_IM, account, from)) 133 | #define PurpleMessage PurpleConvMessage 134 | #define purple_message_set_time(msg, time) ((msg)->when = (time)) 135 | #define purple_conversation_write_message(conv, msg) purple_conversation_write(conv, msg->who, msg->what, msg->flags, msg->when) 136 | static inline PurpleMessage * 137 | purple_message_new_outgoing(const gchar *who, const gchar *contents, PurpleMessageFlags flags) 138 | { 139 | PurpleMessage *message = g_new0(PurpleMessage, 1); 140 | 141 | message->who = g_strdup(who); 142 | message->what = g_strdup(contents); 143 | message->flags = flags; 144 | message->when = time(NULL); 145 | 146 | return message; 147 | } 148 | static inline void 149 | purple_message_destroy(PurpleMessage *message) 150 | { 151 | g_free(message->who); 152 | g_free(message->what); 153 | g_free(message); 154 | } 155 | 156 | #define purple_message_get_recipient(message) (message->who) 157 | #define purple_message_get_contents(message) (message->what) 158 | #if !PURPLE_VERSION_CHECK(2, 12, 0) 159 | #define PURPLE_MESSAGE_REMOTE_SEND 0x10000 160 | #endif 161 | 162 | #define purple_account_privacy_deny_add purple_privacy_deny_add 163 | #define purple_account_privacy_deny_remove purple_privacy_deny_remove 164 | #define PurpleHttpConnection PurpleUtilFetchUrlData 165 | #define purple_buddy_set_name purple_blist_rename_buddy 166 | #define purple_request_cpar_from_connection(a) purple_connection_get_account(a), NULL, NULL 167 | #define purple_notify_user_info_add_pair_html purple_notify_user_info_add_pair 168 | 169 | #ifdef purple_notify_error 170 | #undef purple_notify_error 171 | #endif 172 | #define purple_notify_error(handle, title, primary, secondary) \ 173 | purple_notify_message((handle), PURPLE_NOTIFY_MSG_ERROR, (title), \ 174 | (primary), (secondary), NULL, NULL) 175 | 176 | // Kinda gross, since we can technically use the glib mainloop from purple2 177 | #define g_timeout_add_seconds purple_timeout_add_seconds 178 | #define g_timeout_add purple_timeout_add 179 | #define g_source_remove purple_timeout_remove 180 | 181 | #else 182 | // Purple3 helper functions 183 | #define purple_conversation_set_data(conv, key, value) g_object_set_data(G_OBJECT(conv), key, value) 184 | #define purple_conversation_get_data(conv, key) g_object_get_data(G_OBJECT(conv), key) 185 | #define purple_message_destroy g_object_unref 186 | #define purple_chat_user_set_alias(cb, alias) g_object_set((cb), "alias", (alias), NULL) 187 | #define purple_chat_get_alias(chat) g_object_get_data(G_OBJECT(chat), "alias") 188 | #define purple_protocol_action_get_connection(action) ((action)->connection) 189 | #define PURPLE_TYPE_STRING G_TYPE_STRING 190 | #endif 191 | -------------------------------------------------------------------------------- /src/receipt.c: -------------------------------------------------------------------------------- 1 | #include "receipt.h" 2 | #include "structs.h" 3 | #include "comms.h" 4 | #include "defines.h" 5 | #include "message.h" 6 | #include 7 | 8 | //static int signald_send_receipt(char * uuid, JsonArray * timestamps, SignaldAccount * sa) 9 | static int signald_send_receipt(void * vuuid, void * vtimestamps, void * vsa) { 10 | char * uuid = vuuid; 11 | JsonArray * timestamps = vtimestamps; 12 | SignaldAccount * sa = vsa; 13 | JsonObject *data = json_object_new(); 14 | json_object_set_string_member(data, "type", "mark_read"); 15 | json_object_set_string_member(data, "account", sa->uuid); 16 | signald_set_recipient(data, "to", uuid); 17 | json_object_set_array_member(data, "timestamps", timestamps); 18 | if (!signald_send_json(sa, data)) { 19 | purple_debug_error(SIGNALD_PLUGIN_ID, "Unable to send receipt to %s.\n", uuid); 20 | } 21 | json_array_ref(timestamps); // increase ref counter so the JsonArray still exists when g_hash_table_foreach_remove calls json_array_unref 22 | json_object_unref(data); 23 | return TRUE; 24 | } 25 | 26 | //static int signald_send_receipts(SignaldAccount * sa) 27 | static int signald_send_receipts(void * vsa) { 28 | SignaldAccount * sa = vsa; 29 | g_hash_table_foreach_remove(sa->outgoing_receipts, signald_send_receipt, sa); 30 | return TRUE; 31 | } 32 | 33 | void signald_receipts_init(SignaldAccount * sa) { 34 | sa->outgoing_receipts = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)json_array_unref); 35 | sa->receipts_timer = purple_timeout_add_seconds(10, signald_send_receipts, sa); 36 | } 37 | 38 | void signald_receipts_destroy(SignaldAccount *sa) { 39 | purple_timeout_remove(sa->receipts_timer); 40 | g_hash_table_unref(sa->outgoing_receipts); 41 | } 42 | 43 | void signald_mark_read(SignaldAccount * sa, gint64 timestamp_micro, const char *uuid) { 44 | g_return_if_fail(uuid != NULL); 45 | PurpleStatus *status = purple_account_get_active_status(sa->account); 46 | const char *status_id = purple_status_get_id(status); 47 | gboolean is_online = purple_strequal(status_id, SIGNALD_STATUS_STR_ONLINE); 48 | gboolean receipts_enabled = purple_account_get_bool(sa->account, SIGNALD_OPTION_MARK_READ, FALSE); 49 | if (receipts_enabled && is_online) { 50 | JsonArray * timestamps = g_hash_table_lookup(sa->outgoing_receipts, uuid); 51 | if (timestamps == NULL) { 52 | timestamps = json_array_new(); 53 | g_hash_table_insert(sa->outgoing_receipts, g_strdup(uuid), timestamps); 54 | } 55 | json_array_add_int_element(timestamps, timestamp_micro); 56 | } 57 | } 58 | 59 | void signald_mark_read_chat(SignaldAccount * sa, gint64 timestamp_micro, GHashTable *users) { 60 | GList * uuids = g_hash_table_get_keys(users); 61 | for (const GList *elem = uuids; elem != NULL; elem = elem->next) { 62 | signald_mark_read(sa, timestamp_micro, elem->data); 63 | } 64 | g_list_free(uuids); 65 | } 66 | 67 | void signald_process_receipt(SignaldAccount *sa, JsonObject *obj) { 68 | if (purple_account_get_bool(sa->account, SIGNALD_OPTION_DISPLAY_RECEIPTS, FALSE)) { 69 | // receipts carry no groupV2 information 70 | // source is always the reader 71 | JsonObject * source = json_object_get_object_member(obj, "source"); 72 | const gchar * who = json_object_get_string_member(source, "uuid"); 73 | PurpleConversation * conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, who, sa->account); 74 | if (conv) { 75 | // only display receipt if the conversation is currently open 76 | 77 | JsonObject * receipt_message = json_object_get_object_member(obj, "receipt_message"); 78 | const gchar * type = json_object_get_string_member(receipt_message, "type"); 79 | gint64 when = json_object_get_int_member(receipt_message, "when"); 80 | time_t timestamp = when / 1000; 81 | 82 | // concatenate list of timestamps into one message 83 | GString *message = g_string_new(type); 84 | message = g_string_append(message, " receipt for message from "); 85 | JsonArray * timestamps = json_object_get_array_member(receipt_message, "timestamps"); 86 | GList * timestamp_list = json_array_get_elements(timestamps); 87 | for (GList * elem = timestamp_list; elem != NULL ; elem = elem->next) { 88 | guint64 timestamp_micro = json_node_get_int(elem->data); 89 | time_t timestamp = timestamp_micro / 1000; 90 | struct tm *tm = localtime(×tamp); 91 | message = g_string_append(message, purple_date_format_long(tm)); 92 | if (elem->next) { 93 | message = g_string_append(message, ", "); 94 | } 95 | } 96 | g_list_free(timestamp_list); 97 | 98 | PurpleMessageFlags flags = PURPLE_MESSAGE_NO_LOG; 99 | purple_conv_im_write(PURPLE_CONV_IM(conv), who, message->str, flags, timestamp); 100 | 101 | g_string_free(message, TRUE); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/receipt.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "structs.h" 5 | 6 | void signald_receipts_init(SignaldAccount *sa); 7 | 8 | void signald_receipts_destroy(SignaldAccount *sa); 9 | 10 | void signald_mark_read(SignaldAccount *sa, gint64 timestamp_micro, const char *uuid); 11 | 12 | void signald_mark_read_chat(SignaldAccount *sa, gint64 timestamp_micro, GHashTable *users); 13 | 14 | void signald_process_receipt(SignaldAccount *sa, JsonObject *obj); 15 | -------------------------------------------------------------------------------- /src/reply.c: -------------------------------------------------------------------------------- 1 | #include "reply.h" 2 | #include "defines.h" 3 | #include "message.h" 4 | 5 | GQueue * signald_replycache_init() { 6 | return g_queue_new(); 7 | } 8 | 9 | static void signald_replycache_message_free(SignaldMessage *message) { 10 | g_return_if_fail(message != NULL); 11 | g_free(message->author_uuid); 12 | g_free(message->text); 13 | g_free(message); 14 | } 15 | 16 | void signald_replycache_free(GQueue *queue) { 17 | g_queue_free_full(queue, (GDestroyNotify)signald_replycache_message_free); 18 | } 19 | 20 | void signald_replycache_add_message(SignaldAccount *sa, PurpleConversation *conv, const char *author_uuid, guint64 timestamp_micro, const char *body) { 21 | if (body != NULL) { 22 | const int capacity = purple_account_get_int(sa->account, SIGNALD_OPTION_REPLY_CACHE, 0); 23 | if (capacity > 0) { 24 | SignaldMessage *msg = g_new0(SignaldMessage, 1); 25 | msg->conversation = conv; 26 | msg->author_uuid = g_strdup(author_uuid); 27 | msg->text = g_strdup(body); 28 | msg->id = timestamp_micro; 29 | g_queue_push_head(sa->replycache, msg); 30 | } 31 | while (g_queue_get_length(sa->replycache) > capacity) { 32 | g_queue_pop_tail(sa->replycache); 33 | } 34 | } 35 | } 36 | 37 | static int signald_replycache_predicate(SignaldMessage * msg, const char* needle) { 38 | return !(strstr(msg->text, needle) != NULL); 39 | } 40 | 41 | SignaldMessage * signald_replycache_check(SignaldAccount *sa, const gchar *message) { 42 | g_return_val_if_fail(message != NULL, NULL); 43 | if (message[0] == '@') { 44 | char * colon = strchr(message, ':'); 45 | if (colon != NULL) { 46 | const size_t needle_len = colon-message-1; 47 | char * needle = g_new0(char, needle_len); 48 | strncpy(needle, message+1, needle_len); 49 | GList * elem = g_queue_find_custom(sa->replycache, needle, (GCompareFunc)signald_replycache_predicate); 50 | if (elem != NULL) { 51 | return (SignaldMessage *)elem->data; 52 | } 53 | } 54 | } 55 | return NULL; 56 | } 57 | 58 | const gchar * signald_replycache_strip_needle(const gchar * message) { 59 | // find separator 60 | message = strchr(message, ':') + 1; 61 | // strip leading spaces 62 | while (*message != 0 && *message == ' ') { 63 | message++; 64 | } 65 | return message; 66 | } 67 | 68 | void signald_replycache_apply(JsonObject *data, const SignaldMessage * msg) { 69 | g_return_if_fail(data != NULL); 70 | g_return_if_fail(msg != NULL); 71 | JsonObject *quote = json_object_new(); 72 | json_object_set_int_member(quote, "id", msg->id); 73 | signald_set_recipient(quote, "author", msg->author_uuid); 74 | json_object_set_string_member(quote, "text", msg->text); 75 | json_object_set_object_member(data, "quote", quote); 76 | } 77 | -------------------------------------------------------------------------------- /src/reply.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "structs.h" 6 | 7 | typedef struct { 8 | PurpleConversation *conversation; 9 | char *author_uuid; 10 | gint64 id; 11 | char *text; 12 | } SignaldMessage; 13 | 14 | GQueue * signald_replycache_init(); 15 | 16 | void signald_replycache_free(GQueue *queue); 17 | 18 | void signald_replycache_add_message(SignaldAccount *sa, PurpleConversation *conv, const char *author_uuid, guint64 timestamp_micro, const char *body); 19 | 20 | SignaldMessage * signald_replycache_check(SignaldAccount *sa, const gchar *message); 21 | 22 | const gchar * signald_replycache_strip_needle(const gchar * message); 23 | 24 | void signald_replycache_apply(JsonObject *data, const SignaldMessage * msg); 25 | -------------------------------------------------------------------------------- /src/signald_procmgmt.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "signald_procmgmt.h" 3 | #include "defines.h" 4 | 5 | static int signald_usages = 0; 6 | 7 | void 8 | signald_save_pidfile (const char *pid_file_name) 9 | { 10 | int pid = getpid (); 11 | FILE *pid_file = fopen (pid_file_name, "w"); 12 | if (pid_file) { 13 | fprintf (pid_file, "%d\n", pid); 14 | fclose (pid_file); 15 | } 16 | } 17 | 18 | void 19 | signald_kill_process (const char *pid_file_name) 20 | { 21 | pid_t pid; 22 | FILE *pid_file = fopen (pid_file_name, "r"); 23 | if (pid_file) { 24 | if (fscanf (pid_file, "%d\n", &pid)) { 25 | fclose (pid_file); 26 | } else { 27 | purple_debug_info(SIGNALD_PLUGIN_ID, "Failed to read signald pid from file."); 28 | } 29 | } else { 30 | purple_debug_info(SIGNALD_PLUGIN_ID, "Failed to access signald pidfile."); 31 | } 32 | kill(pid, SIGTERM); 33 | remove(pid_file_name); 34 | } 35 | 36 | void 37 | signald_signald_start(PurpleAccount *account) 38 | { 39 | // Controlled by plugin. 40 | // We need to start signald if it not already running 41 | 42 | purple_debug_info (SIGNALD_PLUGIN_ID, "signald handled by plugin\n"); 43 | 44 | if (0 == signald_usages) { 45 | purple_debug_info (SIGNALD_PLUGIN_ID, "starting signald\n"); 46 | 47 | // Start signald daemon as forked process for killing it when closing 48 | int pid = fork(); 49 | if (pid == 0) { 50 | // The child, redirect it to signald 51 | 52 | // Save pid for later killing the daemon 53 | gchar *pid_file = g_strdup_printf(SIGNALD_PID_FILE, purple_user_dir()); 54 | signald_save_pidfile (pid_file); 55 | g_free(pid_file); 56 | 57 | // Start the daemon 58 | gchar *data = g_strdup_printf(SIGNALD_DATA_PATH, purple_user_dir()); 59 | int signald_ok; 60 | const gchar * user_socket = purple_account_get_string(account, "socket", ""); 61 | if (!user_socket[0]) { 62 | user_socket = SIGNALD_DEFAULT_SOCKET; 63 | } 64 | 65 | if (purple_debug_is_enabled ()) { 66 | signald_ok = execlp("signald", "signald", "-v", "-s", user_socket, "-d", data, NULL); 67 | } else { 68 | signald_ok = execlp("signald", "signald", "-s", user_socket, "-d", data, NULL); 69 | } 70 | g_free(data); 71 | 72 | // Error starting the daemon? (execlp only returns on error) 73 | purple_debug_info (SIGNALD_PLUGIN_ID, "return code starting signald: %d\n", signald_ok); 74 | abort(); // Stop child 75 | } 76 | } 77 | 78 | signald_usages++; 79 | purple_debug_info(SIGNALD_PLUGIN_ID, "signald used %d times\n", signald_usages); 80 | } 81 | 82 | 83 | void 84 | signald_connection_closed() { 85 | // Kill signald daemon and remove its pid file if this was the last 86 | // account using the daemon. There is no need to check the option for 87 | // controlling signald again, since usage count is only greater 0 if 88 | // controlled by the plugin. 89 | if (signald_usages) { 90 | signald_usages--; 91 | if (0 == signald_usages) { 92 | // This was the last instance, kill daemon and remove pid file 93 | gchar *pid_file = g_strdup_printf(SIGNALD_PID_FILE, purple_user_dir()); 94 | signald_kill_process(pid_file); 95 | g_free(pid_file); 96 | } 97 | purple_debug_info(SIGNALD_PLUGIN_ID, "signald used %d times after closing\n", signald_usages); 98 | } 99 | } 100 | 101 | -------------------------------------------------------------------------------- /src/signald_procmgmt.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #define SIGNALD_DEFAULT_SOCKET "/var/run/signald.sock" 6 | 7 | void signald_save_pidfile (const char *pid_file_name); 8 | void signald_kill_process (const char *pid_file_name); 9 | void signald_connection_closed(); 10 | void signald_signald_start(PurpleAccount *account); 11 | -------------------------------------------------------------------------------- /src/status.c: -------------------------------------------------------------------------------- 1 | #include "status.h" 2 | #include "defines.h" 3 | 4 | GList * signald_status_types(PurpleAccount *account) { 5 | GList *types = NULL; 6 | PurpleStatusType *status; 7 | 8 | status = purple_status_type_new_full(PURPLE_STATUS_AVAILABLE, SIGNALD_STATUS_STR_ONLINE, NULL, TRUE, TRUE, FALSE); 9 | types = g_list_append(types, status); 10 | 11 | status = purple_status_type_new_full(PURPLE_STATUS_AWAY, SIGNALD_STATUS_STR_AWAY, NULL, FALSE, FALSE, FALSE); 12 | types = g_list_prepend(types, status); 13 | 14 | status = purple_status_type_new_full(PURPLE_STATUS_OFFLINE, SIGNALD_STATUS_STR_OFFLINE, NULL, TRUE, TRUE, FALSE); 15 | types = g_list_append(types, status); 16 | 17 | status = purple_status_type_new_full(PURPLE_STATUS_MOBILE, SIGNALD_STATUS_STR_MOBILE, NULL, FALSE, FALSE, TRUE); 18 | types = g_list_prepend(types, status); 19 | 20 | return types; 21 | } 22 | -------------------------------------------------------------------------------- /src/status.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | GList * signald_status_types(PurpleAccount *account); 6 | -------------------------------------------------------------------------------- /src/structs.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #define SIGNALD_INPUT_BUFSIZE 500000 // TODO: research actual maximum message size 7 | 8 | typedef struct { 9 | PurpleAccount *account; 10 | PurpleConnection *pc; 11 | char *session_id; 12 | char *uuid; // own uuid, might be NULL – always check before use 13 | 14 | gboolean account_exists; // whether account exists in signald 15 | 16 | int socket_paths_count; 17 | int fd; 18 | int readflags; 19 | guint watcher; 20 | char input_buffer[SIGNALD_INPUT_BUFSIZE]; 21 | char * input_buffer_position; 22 | 23 | char *last_message; // the last message which has been sent to signald 24 | PurpleConversation *last_conversation; // the conversation the message is relevant to 25 | 26 | GQueue *replycache; // cache of messages for "reply to" function 27 | 28 | guint receipts_timer; // handler for timer which sends receipts 29 | GHashTable *outgoing_receipts; // buffer for receipts 30 | 31 | PurpleRoomlist *roomlist; 32 | 33 | char *show_profile; // name of the user-requested profile (NULL in case of system-requested profile) 34 | } SignaldAccount; 35 | --------------------------------------------------------------------------------