pax_global_header00006660000000000000000000000064141614166660014525gustar00rootroot0000000000000052 comment=0239509439e60963fdc6285cd340c79ab53889c5 toxic-0.11.3/000077500000000000000000000000001416141666600127355ustar00rootroot00000000000000toxic-0.11.3/.cirrus.yml000066400000000000000000000005231416141666600150450ustar00rootroot00000000000000--- cirrus-ci_task: container: image: toxchat/toktok-stack:0.0.31-third_party cpu: 2 memory: 2G configure_script: - /src/workspace/tools/inject-repo toxic test_all_script: - cd /src/workspace && bazel test -k --remote_http_cache=http://$CIRRUS_HTTP_CACHE_HOST --config=release //toxic/... toxic-0.11.3/.github/000077500000000000000000000000001416141666600142755ustar00rootroot00000000000000toxic-0.11.3/.github/CODEOWNERS000066400000000000000000000000311416141666600156620ustar00rootroot00000000000000/.github/ @TokTok/admins toxic-0.11.3/.github/FUNDING.yml000066400000000000000000000000301416141666600161030ustar00rootroot00000000000000--- github: [JFreegman] toxic-0.11.3/.github/settings.yml000066400000000000000000000005341416141666600166620ustar00rootroot00000000000000--- _extends: .github repository: name: toxic description: An ncurses-based Tox client topics: tox, console, chat branches: - name: "master" protection: required_status_checks: contexts: - build - cirrus-ci - Codacy Static Code Analysis - code-review/reviewable - infer toxic-0.11.3/.github/workflows/000077500000000000000000000000001416141666600163325ustar00rootroot00000000000000toxic-0.11.3/.github/workflows/ci.yml000066400000000000000000000051361416141666600174550ustar00rootroot00000000000000name: ci on: push: branches: [master] pull_request: branches: [master] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Install dependencies run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libalut-dev libconfig-dev libcurl4-gnutls-dev libnotify-dev libopenal-dev libopus-dev libqrencode-dev libsodium-dev libvpx-dev libx11-dev python3-dev pkg-config && git clone --depth=1 https://github.com/TokTok/c-toxcore && cd c-toxcore && cmake . -B_build -DBOOTSTRAP_DAEMON=OFF && cd _build && make -j4 && sudo make install - name: Build toxic run: make -j4 infer: runs-on: ubuntu-latest container: toxchat/infer steps: - name: Install git run: apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends git - name: Install dependencies run: apt-get update && apt-get install -y --no-install-recommends cmake g++ libalut-dev libconfig-dev libcurl4-gnutls-dev libncurses-dev libnotify-dev libopenal-dev libopus-dev libqrencode-dev libsodium-dev libvpx-dev libx11-dev make python3-dev pkg-config && git clone --depth=1 https://github.com/TokTok/c-toxcore && cd c-toxcore && cmake . -B_build -DBOOTSTRAP_DAEMON=OFF && cd _build && make -j4 && make install - uses: actions/checkout@v2 - name: Run infer run: infer --no-progress-bar -- cc src/*.c -fsyntax-only $(python3-config --includes --ldflags) $(pkg-config --cflags --libs freealut libconfig libcurl libnotify libpng libqrencode ncurses openal python3 toxcore vpx x11) -DAUDIO -DBOX_NOTIFY -DGAMES -DPACKAGE_DATADIR='""' -DPYTHON -DQRCODE -DSOUND_NOTIFY -DVIDEO - name: Print log run: cat /__w/toxic/toxic/infer-out/report.txt toxic-0.11.3/.gitignore000066400000000000000000000002411416141666600147220ustar00rootroot00000000000000.DS_Store *~ *.swp *.o *.lo *.a *.exe *.out *.app *.swp *.la stamp-h1 .deps .libs *.orig build/toxic build/*.o build/*.d apidoc/python/build *.vim *.tox *.nvim* toxic-0.11.3/.restyled.yaml000066400000000000000000000001031416141666600155240ustar00rootroot00000000000000--- restylers: - astyle: arguments: ["--options=astylerc"] toxic-0.11.3/BUILD.bazel000066400000000000000000000017721416141666600146220ustar00rootroot00000000000000load("@rules_cc//cc:defs.bzl", "cc_binary") load("//tools/project:build_defs.bzl", "project") project() cc_binary( name = "toxic", srcs = glob( [ "src/*.c", "src/*.h", ], exclude = ["src/video*"], ) + select({ "//tools/config:linux": glob(["src/video*"]), "//tools/config:osx": [], }), copts = [ "-std=gnu99", "-DAUDIO", "-DGAMES", "-DPACKAGE_DATADIR='\"data\"'", "-DPYTHON", "-DQRCODE", ] + select({ "//tools/config:linux": ["-DVIDEO"], "//tools/config:osx": [], }), deps = [ "//c-toxcore", "@curl", "@libconfig", "@libqrencode", "@libvpx", "@ncurses", "@openal", "@python3//:python", ] + select({ "//tools/config:linux": ["@x11"], "//tools/config:osx": [], }), ) sh_test( name = "toxic_test", size = "small", srcs = [":toxic"], args = ["--help"], ) toxic-0.11.3/INSTALL.md000066400000000000000000000100331416141666600143620ustar00rootroot00000000000000# Installation * [Dependencies](#dependencies) * [OS X Notes](#os-x-notes) * [Compiling](#compiling) * [Documentation](#documentation) * [Notes](#notes) * [Compilation variables](#compilation-variables) * [Environment variables](#environment-variables) ## Dependencies | Name | Needed by | Debian package | |------------------------------------------------------|----------------------------|---------------------| | [Tox Core](https://github.com/toktok/c-toxcore) | BASE | libtoxcore-dev | | [NCurses](https://www.gnu.org/software/ncurses) | BASE | libncursesw5-dev | | [LibConfig](http://www.hyperrealm.com/libconfig) | BASE | libconfig-dev | | [GNUmake](https://www.gnu.org/software/make) | BASE | make | | [libcurl](http://curl.haxx.se/) | BASE | libcurl4-openssl-dev| | [libqrencode](https://fukuchi.org/works/qrencode/) | QRCODE | libqrencode-dev | | [OpenAL](http://openal.org) | AUDIO, SOUND NOTIFICATIONS | libopenal-dev | | [OpenALUT](http://openal.org) | SOUND NOTIFICATIONS | libalut-dev | | [LibNotify](https://developer.gnome.org/libnotify) | DESKTOP NOTIFICATIONS | libnotify-dev | | [Python 3](http://www.python.org/) | PYTHON | python3-dev | | [AsciiDoc](http://asciidoc.org/index.html) | DOCUMENTATION1 | asciidoc | 1: see [Documentation](#documentation) #### OS X Notes Using [Homebrew](http://brew.sh): ``` brew install curl qrencode openal-soft freealut libconfig libpng brew install --HEAD https://raw.githubusercontent.com/Tox/homebrew-tox/master/Formula/libtoxcore.rb brew install libnotify export PKG_CONFIG_PATH=/usr/local/opt/openal-soft/lib/pkgconfig make ``` You can omit `libnotify` if you intend to build without desktop notifications enabled. ## Compiling ``` make sudo make install ``` #### Documentation Run `make doc` in the build directory after editing the asciidoc files to regenerate the manpages.
**Note for developers**: asciidoc files and generated manpages will need to be committed together.
**Note for everyone**: [asciidoc](http://asciidoc.org/index.html) (and this step) is only required for regenerating manpages when you modify them. ## Notes #### Compilation variables * You can add specific flags to the Makefile with `USER_CFLAGS=""` and `USER_LDFLAGS=""` passed as arguments to make, or as environment variables * Default compile options can be overridden by using special variables: * `DISABLE_X11=1` → Disable X11 support (needed for focus tracking) * `DISABLE_AV=1` → Disable audio call support * `DISABLE_SOUND_NOTIFY=1` → Disable sound notifications support * `DISABLE_QRCODE=1` → Disable QR exporting support * `DISABLE_QRPNG=1` → Disable support for exporting QR as PNG * `DISABLE_DESKTOP_NOTIFY=1` → Disable desktop notifications support * `DISABLE_GAMES=1` → Disable support for games * `ENABLE_PYTHON=1` → Build toxic with Python scripting support * `ENABLE_RELEASE=1` → Build toxic without debug symbols and with full compiler optimizations * `ENABLE_ASAN=1` → Build toxic with LLVM Address Sanitizer enabled * `DESTDIR=""` Specifies the base install directory for binaries and data files (e.g.: DESTDIR="/tmp/build/pkg") #### Environment variables * You can use the `CFLAGS` and `LDFLAGS` environment variables to add specific flags to the Makefile * The `PREFIX` environment variable specifies a base install directory for binaries and data files. This is interchangeable with the `DESTDIR` variable, and is generally used by systems that have the `PREFIX` environment variable set by default.
**Note**: `sudo` does not preserve user environment variables by default on some systems. See the `sudoers` manual for more information. toxic-0.11.3/LICENSE000066400000000000000000001045141416141666600137470ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . toxic-0.11.3/Makefile000066400000000000000000000055731416141666600144070ustar00rootroot00000000000000BASE_DIR = $(shell pwd -P) CFG_DIR = $(BASE_DIR)/cfg -include $(CFG_DIR)/global_vars.mk LIBS = toxcore ncursesw libconfig libcurl CFLAGS ?= -std=c99 -pthread -Wall -Wpedantic -Wunused -fstack-protector-all -Wvla -Wno-missing-braces CFLAGS += '-DTOXICVER="$(VERSION)"' -DHAVE_WIDECHAR -D_XOPEN_SOURCE_EXTENDED -D_FILE_OFFSET_BITS=64 CFLAGS += '-DPACKAGE_DATADIR="$(abspath $(DATADIR))"' CFLAGS += ${USER_CFLAGS} LDFLAGS ?= LDFLAGS += ${USER_LDFLAGS} OBJ = autocomplete.o avatars.o bootstrap.o chat.o chat_commands.o configdir.o curl_util.o execute.o OBJ += file_transfers.o friendlist.o global_commands.o conference_commands.o conference.o help.o input.o line_info.o OBJ += log.o message_queue.o misc_tools.o name_lookup.o notify.o prompt.o qr_code.o settings.o OBJ += term_mplex.o toxic.o toxic_strings.o windows.o # Check if debug build is enabled RELEASE := $(shell if [ -z "$(ENABLE_RELEASE)" ] || [ "$(ENABLE_RELEASE)" = "0" ] ; then echo disabled ; else echo enabled ; fi) ifneq ($(RELEASE), enabled) CFLAGS += -O0 -g -DDEBUG LDFLAGS += -O0 else CFLAGS += -O2 -flto LDFLAGS += -O2 -flto endif # Check if LLVM Address Sanitizer is enabled ASAN := $(shell if [ -z "$(ENABLE_ASAN)" ] || [ "$(ENABLE_ASAN)" = "0" ] ; then echo disabled ; else echo enabled ; fi) ifneq ($(ASAN), disabled) CFLAGS += -fsanitize=address -fno-omit-frame-pointer endif # Check on wich system we are running UNAME_S = $(shell uname -s) ifeq ($(UNAME_S), Linux) LDFLAGS += -ldl -lrt endif ifeq ($(UNAME_S), OpenBSD) LIBS := $(filter-out ncursesw, $(LIBS)) LDFLAGS += -lncursesw endif ifeq ($(UNAME_S), NetBSD) LIBS := $(filter-out ncursesw, $(LIBS)) LDFLAGS += -lncursesw endif ifeq ($(UNAME_S), Darwin) -include $(CFG_DIR)/systems/Darwin.mk endif # Check on which platform we are running UNAME_M = $(shell uname -m) ifeq ($(UNAME_M), x86_64) -include $(CFG_DIR)/platforms/x86_64.mk endif ifneq ($(filter %86, $(UNAME_M)),) -include $(CFG_DIR)/platforms/x86.mk endif ifneq ($(filter arm%, $(UNAME_M)),) -include $(CFG_DIR)/platforms/arm.mk endif # Include all needed checks -include $(CFG_DIR)/checks/check_features.mk # Fix path for object files OBJ := $(addprefix $(BUILD_DIR)/, $(OBJ)) # Targets all: $(BUILD_DIR)/toxic $(BUILD_DIR)/toxic: $(OBJ) @echo " LD $(@:$(BUILD_DIR)/%=%)" @$(CC) $(CFLAGS) -o $(BUILD_DIR)/toxic $(OBJ) $(LDFLAGS) $(BUILD_DIR)/osx_video.o: $(SRC_DIR)/$(OSX_VIDEO) @echo " CC $(@:$(BUILD_DIR)/)osx_video.o" @$(CC) $(CFLAGS) -o $(BUILD_DIR)/osx_video.o -c $(SRC_DIR)/$(OSX_VIDEO) $(BUILD_DIR)/%.o: $(SRC_DIR)/%.c @if [ ! -e $(BUILD_DIR) ]; then \ mkdir -p $(BUILD_DIR) ;\ fi @echo " CC $(@:$(BUILD_DIR)/%=%)" @$(CC) $(CFLAGS) -o $(BUILD_DIR)/$*.o -c $(SRC_DIR)/$*.c @$(CC) -MM $(CFLAGS) $(SRC_DIR)/$*.c >$(BUILD_DIR)/$*.d clean: rm -f $(BUILD_DIR)/*.d $(BUILD_DIR)/*.o $(BUILD_DIR)/toxic -include $(BUILD_DIR)/$(OBJ:.o=.d) -include $(CFG_DIR)/targets/*.mk .PHONY: clean all toxic-0.11.3/README.md000066400000000000000000000021761416141666600142220ustar00rootroot00000000000000 Coverity Scan Build Status Toxic is a [Tox](https://tox.chat)-based instant messaging and video chat client. [![Toxic Screenshot](https://i.imgur.com/TwYA8L0.png "Toxic Home Screen")](https://i.imgur.com/TwYA8L0.png) ## Installation [See the install instructions](/INSTALL.md) ## Settings Running Toxic for the first time creates an empty file called toxic.conf in your home configuration directory ("~/.config/tox" for Linux users). Adding options to this file allows you to enable auto-logging, change the time format (12/24 hour), and much more. You can view our example config file [here](misc/toxic.conf.example). ## Troubleshooting If your default prefix is "/usr/local" and you receive the following: ``` error while loading shared libraries: libtoxcore.so.0: cannot open shared object file: No such file or directory ``` you can attempt to correct it by running `sudo ldconfig`. If that doesn't work, run: ``` echo '/usr/local/lib/' | sudo tee -a /etc/ld.so.conf.d/locallib.conf sudo ldconfig ``` toxic-0.11.3/apidoc/000077500000000000000000000000001416141666600141745ustar00rootroot00000000000000toxic-0.11.3/apidoc/python/000077500000000000000000000000001416141666600155155ustar00rootroot00000000000000toxic-0.11.3/apidoc/python/Makefile000066400000000000000000000011421416141666600171530ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build SPHINXPROJ = toxic_api SOURCEDIR = source BUILDDIR = build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)toxic-0.11.3/apidoc/python/source/000077500000000000000000000000001416141666600170155ustar00rootroot00000000000000toxic-0.11.3/apidoc/python/source/conf.py000066400000000000000000000111431416141666600203140ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # toxic_api documentation build configuration file, created by # sphinx-quickstart on Tue May 16 08:58:24 2017. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # # import os # import sys # sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General information about the project. project = 'toxic_api' copyright = '2017, Jakob Kreuze' author = 'Jakob Kreuze' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '0.11.3' # The full version, including alpha/beta/rc tags. release = '0.11.3' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = [] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = 'alabaster' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # # html_theme_options = {} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # -- Options for HTMLHelp output ------------------------------------------ # Output file base name for HTML help builder. htmlhelp_basename = 'toxic_apidoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'toxic_api.tex', 'toxic\\_api Documentation', 'Jakob Kreuze', 'manual'), ] # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'toxic_api', 'toxic_api Documentation', [author], 1) ] # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'toxic_api', 'toxic_api Documentation', author, 'toxic_api', 'One line description of project.', 'Miscellaneous'), ] toxic-0.11.3/apidoc/python/source/examples.rst000066400000000000000000000001541416141666600213650ustar00rootroot00000000000000============ API Examples ============ Fortune ======= .. literalinclude:: fortune.py :language: python toxic-0.11.3/apidoc/python/source/fortune.py000066400000000000000000000020631416141666600210520ustar00rootroot00000000000000import toxic_api import random FORTUNES = [ "A bug in the code is worth two in the documentation.", "A bug in the hand is better than one as yet undetected.", "\"A debugged program is one for which you have not yet found the " "conditions that make it fail.\" -- Jerry Ogdin" ] def send_fortune(args): """Callback function that sends the contact of the current window a given number of random fortunes. """ if len(args) != 1: toxic_api.display("Only one argument allowed!") return try: count = int(args[0]) except ValueError: toxic_api.display("Argument must be a number!") return if count < 0 or count > 20: toxic_api.display("Argument is too large!") return name = toxic_api.get_nick() toxic_api.send("%s has decided to send you %d fortunes:" % (name, count)) for _ in range(count): toxic_api.send(random.choice(FORTUNES)) toxic_api.register("/fortune", "Send a fortune to the contact of the current " "window", send_fortune) toxic-0.11.3/apidoc/python/source/index.rst000066400000000000000000000002211416141666600206510ustar00rootroot00000000000000Toxic Scripting Interface Documentation ======================================= .. toctree:: :maxdepth: 2 intro reference examples toxic-0.11.3/apidoc/python/source/intro.rst000066400000000000000000000007021416141666600207010ustar00rootroot00000000000000========================= Toxic Scripting Interface ========================= A Python scripting interface to `Toxic `_. Getting Started =============== Toxic is compiled with Python support by default. To access the scripting interface, simply import "toxic_api" in your script. Scripts can be run by issuing "/run " from toxic, or placing them in the "autorun_path" from your toxic configuration file. toxic-0.11.3/apidoc/python/source/reference.rst000066400000000000000000000033451416141666600215120ustar00rootroot00000000000000============= API Reference ============= Messages ======== .. function:: display(msg) Display a message to the user through the current window. :param msg: The message to display. :type msg: string :rtype: none .. function:: send(msg) Send a message to the user specified by the currently open conversation. :param msg: The message to display. :type msg: string :rtype: none State ===== .. function:: get_nick() Return the user's current nickname. :rtype: string .. function:: get_status() Return a string representing the user's current status. Can be either "online", "away", or "busy". :rtype: string .. function:: get_status_message() Return the user's current status message. :rtype: string .. function:: get_all_friends() Return a list of all the user's friends. :rtype: list of (string, string) tuples containing the nickname followed by their public key Commands ======== .. function:: execute(command, class) Executes the given command. The API exports three constants for the class parameter; GLOBAL_COMMAND, CHAT_COMMAND, and GROUPCHAT_COMMAND. :param command: The command to execute. :type command: string :param class: The class of the command. :type class: int :rtype: none .. function:: register(command, help, callback) Register a callback to be executed whenever command is run. The callback function will be called with one argument, a list of arguments from when the user calls the command. :param command: The command to listen for. :type command: string :param help: A description of the command to be shown in the help menu. :type help: string :param callback: The function to be called. :type callback: callable :rtype: none toxic-0.11.3/astylerc000066400000000000000000000005501416141666600145060ustar00rootroot00000000000000# Bracket Style Options --style=kr # Tab Options --indent=spaces=4 # Indentation Options --indent-switches # Padding Options --pad-header --break-blocks --pad-oper --unpad-paren --align-pointer=name --align-reference=name # Formatting Options --add-brackets --convert-tabs --max-code-length=120 # Other Options --preserve-date --formatted --lineend=linux toxic-0.11.3/cfg/000077500000000000000000000000001416141666600134745ustar00rootroot00000000000000toxic-0.11.3/cfg/checks/000077500000000000000000000000001416141666600147345ustar00rootroot00000000000000toxic-0.11.3/cfg/checks/audio.mk000066400000000000000000000014721416141666600163720ustar00rootroot00000000000000# Variables for audio call support AUDIO_LIBS = openal AUDIO_CFLAGS = -DAUDIO ifneq (, $(findstring audio_device.o, $(OBJ))) AUDIO_OBJ = audio_call.o else AUDIO_OBJ = audio_call.o audio_device.o endif # Check if we can build audio support CHECK_AUDIO_LIBS := $(shell $(PKG_CONFIG) --exists $(AUDIO_LIBS) || echo -n "error") ifneq ($(CHECK_AUDIO_LIBS), error) LIBS += $(AUDIO_LIBS) LDFLAGS += -lm CFLAGS += $(AUDIO_CFLAGS) OBJ += $(AUDIO_OBJ) else ifneq ($(MAKECMDGOALS), clean) MISSING_AUDIO_LIBS := $(shell for lib in $(AUDIO_LIBS) ; do if ! $(PKG_CONFIG) --exists $$lib ; then echo $$lib ; fi ; done) $(warning WARNING -- Toxic will be compiled without audio support) $(warning WARNING -- You need these libraries for audio support) $(warning WARNING -- $(MISSING_AUDIO_LIBS)) endif toxic-0.11.3/cfg/checks/check_features.mk000066400000000000000000000055061416141666600202460ustar00rootroot00000000000000CHECKS_DIR = $(CFG_DIR)/checks # Check if we want build X11 support X11 := $(shell if [ -z "$(DISABLE_X11)" ] || [ "$(DISABLE_X11)" = "0" ] ; then echo enabled ; else echo disabled ; fi) ifneq ($(X11), disabled) -include $(CHECKS_DIR)/x11.mk endif # Check if we want build audio support AUDIO := $(shell if [ -z "$(DISABLE_AV)" ] || [ "$(DISABLE_AV)" = "0" ] ; then echo enabled ; else echo disabled ; fi) ifneq ($(AUDIO), disabled) -include $(CHECKS_DIR)/audio.mk endif # Check if we want build video support VIDEO := $(shell if [ -z "$(DISABLE_VI)" ] || [ "$(DISABLE_VI)" = "0" ] ; then echo enabled ; else echo disabled ; fi) ifneq ($(X11), disabled) ifneq ($(AUDIO), disabled) ifneq ($(VIDEO), disabled) -include $(CHECKS_DIR)/video.mk endif endif endif #check if we want to build with game support GAMES := $(shell if [ -z "$(DISABLE_GAMES)" ] || [ "$(DISABLE_GAMES)" = "0" ] ; then echo enabled ; else echo disabled ; fi) ifneq ($(GAMES), disabled) -include $(CHECKS_DIR)/games.mk endif # Check if we want build sound notifications support SND_NOTIFY := $(shell if [ -z "$(DISABLE_SOUND_NOTIFY)" ] || [ "$(DISABLE_SOUND_NOTIFY)" = "0" ] ; then echo enabled ; else echo disabled ; fi) ifneq ($(SND_NOTIFY), disabled) -include $(CHECKS_DIR)/sound_notifications.mk endif # Check if we want build desktop notifications support DESK_NOTIFY := $(shell if [ -z "$(DISABLE_DESKTOP_NOTIFY)" ] || [ "$(DISABLE_DESKTOP_NOTIFY)" = "0" ] ; then echo enabled ; else echo disabled ; fi) ifneq ($(DESK_NOTIFY), disabled) -include $(CHECKS_DIR)/desktop_notifications.mk endif # Check if we want build QR export support QR_CODE := $(shell if [ -z "$(DISABLE_QRCODE)" ] || [ "$(DISABLE_QRCODE)" = "0" ] ; then echo enabled ; else echo disabled ; fi) ifneq ($(QR_CODE), disabled) -include $(CHECKS_DIR)/qr.mk endif # Check if we want build QR exported as PNG support QR_PNG := $(shell if [ -z "$(DISABLE_QRPNG)" ] || [ "$(DISABLE_QRPNG)" = "0" ] ; then echo enabled ; else echo disabled ; fi) ifneq ($(QR_PNG), disabled) -include $(CHECKS_DIR)/qr_png.mk endif # Check if we want build Python scripting support PYTHON := $(shell if [ -z "$(ENABLE_PYTHON)" ] || [ "$(ENABLE_PYTHON)" = "0" ] ; then echo disabled ; else echo enabled ; fi) ifneq ($(PYTHON), disabled) -include $(CHECKS_DIR)/python.mk endif # Check if we can build Toxic CHECK_LIBS := $(shell $(PKG_CONFIG) --exists $(LIBS) || echo -n "error") ifneq ($(CHECK_LIBS), error) CFLAGS += $(shell $(PKG_CONFIG) --cflags $(LIBS)) LDFLAGS += $(shell $(PKG_CONFIG) --libs $(LIBS)) else ifneq ($(MAKECMDGOALS), clean) MISSING_LIBS := $(shell for lib in $(LIBS) ; do if ! $(PKG_CONFIG) --exists $$lib ; then echo $$lib ; fi ; done) $(warning ERROR -- Cannot compile Toxic) $(warning ERROR -- You need these libraries) $(warning ERROR -- $(MISSING_LIBS)) $(error ERROR) endif toxic-0.11.3/cfg/checks/desktop_notifications.mk000066400000000000000000000014131416141666600216660ustar00rootroot00000000000000# Variables for desktop notifications support DESK_NOTIFY_LIBS = libnotify DESK_NOTIFY_CFLAGS = -DBOX_NOTIFY # Check if we can build desktop notifications support CHECK_DESK_NOTIFY_LIBS := $(shell $(PKG_CONFIG) --exists $(DESK_NOTIFY_LIBS) || echo -n "error") ifneq ($(CHECK_DESK_NOTIFY_LIBS), error) LIBS += $(DESK_NOTIFY_LIBS) CFLAGS += $(DESK_NOTIFY_CFLAGS) else ifneq ($(MAKECMDGOALS), clean) MISSING_DESK_NOTIFY_LIBS := $(shell for lib in $(DESK_NOTIFY_LIBS) ; do if ! $(PKG_CONFIG) --exists $$lib ; then echo $$lib ; fi ; done) $(warning WARNING -- Toxic will be compiled without desktop notifications support) $(warning WARNING -- You need these libraries for desktop notifications support) $(warning WARNING -- $(MISSING_DESK_NOTIFY_LIBS)) endif toxic-0.11.3/cfg/checks/games.mk000066400000000000000000000002751416141666600163650ustar00rootroot00000000000000# Variables for game support GAMES_CFLAGS = -DGAMES GAMES_OBJ = game_base.o game_centipede.o game_chess.o game_life.o game_util.o game_snake.o CFLAGS += $(GAMES_CFLAGS) OBJ += $(GAMES_OBJ) toxic-0.11.3/cfg/checks/python.mk000066400000000000000000000012021416141666600166010ustar00rootroot00000000000000# Variables for Python scripting support PYTHON3_LIBS = python3 PYTHON_CFLAGS = -DPYTHON PYTHON_OBJ = api.o python_api.o # Check if we can build Python scripting support CHECK_PYTHON3_LIBS = $(shell $(PKG_CONFIG) --exists $(PYTHON3_LIBS) || echo -n "error") ifneq ($(CHECK_PYTHON3_LIBS), error) LDFLAGS += $(shell python3-config --ldflags) CFLAGS += $(PYTHON_CFLAGS) $(shell python3-config --includes) OBJ += $(PYTHON_OBJ) else ifneq ($(MAKECMDGOALS), clean) $(warning WARNING -- Toxic will be compiled without Python scripting support) $(warning WARNING -- You need python3 installed for Python scripting support) endif toxic-0.11.3/cfg/checks/qr.mk000066400000000000000000000011721416141666600157100ustar00rootroot00000000000000# Variables for QR export support QR_LIBS = libqrencode QR_CFLAGS = -DQRCODE # Check if we can build QR export support CHECK_QR_LIBS = $(shell pkg-config --exists $(QR_LIBS) || echo -n "error") ifneq ($(CHECK_QR_LIBS), error) LIBS += $(QR_LIBS) CFLAGS += $(QR_CFLAGS) else ifneq ($(MAKECMDGOALS), clean) MISSING_QR_LIBS = $(shell for lib in $(QR_LIBS) ; do if ! $(PKG_CONFIG) --exists $$lib ; then echo $$lib ; fi ; done) $(warning WARNING -- Toxic will be compiled without QR export support) $(warning WARNING -- You need these libraries for QR export support) $(warning WARNING -- $(MISSING_QR_LIBS)) endif toxic-0.11.3/cfg/checks/qr_png.mk000066400000000000000000000012421416141666600165520ustar00rootroot00000000000000# Variables for QR exported as PNG support PNG_LIBS = libpng PNG_CFLAGS = -DQRPNG # Check if we can build QR exported as PNG support CHECK_PNG_LIBS = $(shell pkg-config --exists $(PNG_LIBS) || echo -n "error") ifneq ($(CHECK_PNG_LIBS), error) LIBS += $(PNG_LIBS) CFLAGS += $(PNG_CFLAGS) else ifneq ($(MAKECMDGOALS), clean) MISSING_PNG_LIBS = $(shell for lib in $(PNG_LIBS) ; do if ! $(PKG_CONFIG) --exists $$lib ; then echo $$lib ; fi ; done) $(warning WARNING -- Toxic will be compiled without QR exported as PNG support) $(warning WARNING -- You need these libraries for QR exported as PNG support) $(warning WARNING -- $(MISSING_PNG_LIBS)) endif toxic-0.11.3/cfg/checks/sound_notifications.mk000066400000000000000000000016171416141666600213530ustar00rootroot00000000000000# Variables for sound notifications support SND_NOTIFY_LIBS = openal freealut SND_NOTIFY_CFLAGS = -DSOUND_NOTIFY ifneq (, $(findstring audio_device.o, $(OBJ))) SND_NOTIFY_OBJ = else SND_NOTIFY_OBJ = audio_device.o endif # Check if we can build sound notifications support CHECK_SND_NOTIFY_LIBS = $(shell $(PKG_CONFIG) --exists $(SND_NOTIFY_LIBS) || echo -n "error") ifneq ($(CHECK_SND_NOTIFY_LIBS), error) LIBS += $(SND_NOTIFY_LIBS) CFLAGS += $(SND_NOTIFY_CFLAGS) OBJ += $(SND_NOTIFY_OBJ) else ifneq ($(MAKECMDGOALS), clean) MISSING_SND_NOTIFY_LIBS = $(shell for lib in $(SND_NOTIFY_LIBS) ; do if ! $(PKG_CONFIG) --exists $$lib ; then echo $$lib ; fi ; done) $(warning WARNING -- Toxic will be compiled without sound notifications support) $(warning WARNING -- You need these libraries for sound notifications support) $(warning WARNING -- $(MISSING_SND_NOTIFY_LIBS)) endif toxic-0.11.3/cfg/checks/video.mk000066400000000000000000000014621416141666600163760ustar00rootroot00000000000000# Variables for video call support VIDEO_LIBS = openal vpx x11 VIDEO_CFLAGS = -DVIDEO ifneq (, $(findstring video_device.o, $(OBJ))) VIDEO_OBJ = video_call.o else VIDEO_OBJ = video_call.o video_device.o endif # Check if we can build video support CHECK_VIDEO_LIBS = $(shell $(PKG_CONFIG) --exists $(VIDEO_LIBS) || echo -n "error") ifneq ($(CHECK_VIDEO_LIBS), error) LIBS += $(VIDEO_LIBS) CFLAGS += $(VIDEO_CFLAGS) OBJ += $(VIDEO_OBJ) else ifneq ($(MAKECMDGOALS), clean) MISSING_VIDEO_LIBS = $(shell for lib in $(VIDEO_LIBS) ; do if ! $(PKG_CONFIG) --exists $$lib ; then echo $$lib ; fi ; done) $(warning WARNING -- Toxic will be compiled without video support) $(warning WARNING -- You will need these libraries for video support) $(warning WARNING -- $(MISSING_VIDEO_LIBS)) endif toxic-0.11.3/cfg/checks/x11.mk000066400000000000000000000013011416141666600156710ustar00rootroot00000000000000# Variables for X11 support X11_LIBS = x11 X11_CFLAGS = -DX11 X11_OBJ = x11focus.o # Check if we can build X11 support CHECK_X11_LIBS = $(shell $(PKG_CONFIG) --exists $(X11_LIBS) || echo -n "error") ifneq ($(CHECK_X11_LIBS), error) LIBS += $(X11_LIBS) CFLAGS += $(X11_CFLAGS) OBJ += $(X11_OBJ) else ifneq ($(MAKECMDGOALS), clean) MISSING_X11_LIBS = $(shell for lib in $(X11_LIBS) ; do if ! $(PKG_CONFIG) --exists $$lib ; then echo $$lib ; fi ; done) $(warning WARNING -- Toxic will be compiled without x11 support (needed for focus tracking and drag&drop support)) $(warning WARNING -- You need these libraries for x11 support) $(warning WARNING -- $(MISSING_X11_LIBS)) endif toxic-0.11.3/cfg/global_vars.mk000066400000000000000000000016331416141666600163230ustar00rootroot00000000000000# Version TOXIC_VERSION = 0.11.3 REV = $(shell git rev-list HEAD --count 2>/dev/null || echo -n "error") ifneq (, $(findstring error, $(REV))) VERSION = $(TOXIC_VERSION) else VERSION = $(TOXIC_VERSION)_r$(REV) endif # Project directories BUILD_DIR = $(BASE_DIR)/build DOC_DIR = $(BASE_DIR)/doc SRC_DIR = $(BASE_DIR)/src SND_DIR = $(BASE_DIR)/sounds MISC_DIR = $(BASE_DIR)/misc # Project files MANFILES = toxic.1 toxic.conf.5 DATAFILES = nameservers toxic.conf.example DESKFILE = toxic.desktop SNDFILES = ToxicContactOnline.wav ToxicContactOffline.wav ToxicError.wav SNDFILES += ToxicRecvMessage.wav ToxicOutgoingCall.wav ToxicIncomingCall.wav SNDFILES += ToxicTransferComplete.wav ToxicTransferStart.wav # Install directories PREFIX ?= /usr/local BINDIR = $(PREFIX)/bin DATADIR = $(PREFIX)/share/toxic MANDIR ?= $(PREFIX)/share/man APPDIR = $(PREFIX)/share/applications # Platform tools PKG_CONFIG = pkg-config toxic-0.11.3/cfg/platforms/000077500000000000000000000000001416141666600155035ustar00rootroot00000000000000toxic-0.11.3/cfg/platforms/.gitignore000066400000000000000000000001071416141666600174710ustar00rootroot00000000000000# Ignore everything in this directory * # Except this file !.gitignore toxic-0.11.3/cfg/systems/000077500000000000000000000000001416141666600152035ustar00rootroot00000000000000toxic-0.11.3/cfg/systems/Darwin.mk000066400000000000000000000014651416141666600167660ustar00rootroot00000000000000# Special options for OS X # This assumes the use of Homebrew. Change the paths if using MacPorts or Fink. PKG_CONFIG_PATH = $(shell export PKG_CONFIG_PATH=/usr/lib/pkgconfig:/usr/local/opt/libconfig/lib/pkgconfig:/usr/local/lib/pkgconfig:/opt/X11/lib/pkgconfig) LIBS := $(filter-out ncursesw, $(LIBS)) # OS X ships a usable, recent version of ncurses, but calls it ncurses not ncursesw. LDFLAGS += -lncurses -lalut -ltoxcore -lcurl -lconfig -lqrencode -lpng -lopenal -g CFLAGS += -I/usr/local/opt/freealut/include/AL -I/usr/local/opt/glib/include/glib-2.0 -g OSX_LIBRARIES = -lobjc -lresolv OSX_FRAMEWORKS = -framework Foundation -framework CoreFoundation -framework AVFoundation \ -framework QuartzCore -framework CoreMedia OSX_VIDEO = osx_video.m LDFLAGS += $(OSX_LIBRARIES) $(OSX_FRAMEWORKS) OBJ += osx_video.o toxic-0.11.3/cfg/targets/000077500000000000000000000000001416141666600151455ustar00rootroot00000000000000toxic-0.11.3/cfg/targets/doc.mk000066400000000000000000000004531416141666600162450ustar00rootroot00000000000000# Doc target doc: $(MANFILES:%=$(DOC_DIR)/%) $(DOC_DIR)/%: $(DOC_DIR)/%.asc @echo " MAN $(@F)" @a2x -f manpage -a revdate=$(shell git log -1 --date=short --format="%ad" $<) \ -a manmanual="Toxic Manual" -a mansource=toxic \ -a manversion=__VERSION__ -a datadir=__DATADIR__ $< .PHONY: doc toxic-0.11.3/cfg/targets/help.mk000066400000000000000000000051711416141666600164320ustar00rootroot00000000000000# Help target help: @echo "-- Targets --" @echo " all: Build toxic and documentation [DEFAULT]" @echo " toxic: Build toxic" @echo " doc: Build documentation" @echo " install: Build toxic and install it in PREFIX (default PREFIX is \"$(abspath $(PREFIX))\")" @echo " uninstall: Remove toxic from PREFIX (default PREFIX is \"$(abspath $(PREFIX))\")" @echo " clean: Remove built files" @echo " help: This help" @echo @echo "-- Variables --" @echo " DISABLE_X11: Set to \"1\" to force building without X11 support" @echo " DISABLE_AV: Set to \"1\" to force building without audio call support" @echo " DISABLE_SOUND_NOTIFY: Set to \"1\" to force building without sound notification support" @echo " DISABLE_DESKTOP_NOTIFY: Set to \"1\" to force building without desktop notifications support" @echo " DISABLE_QRCODE: Set to \"1\" to force building without QR export support" @echo " DISABLE_QRPNG: Set to \"1\" to force building without QR exported as PNG support" @echo " DISABLE_GAMES: Set to \"1\" to force building without game support" @echo " ENABLE_PYTHON: Set to \"1\" to enable building with Python scripting support" @echo " ENABLE_RELEASE: Set to \"1\" to build without debug symbols and with full compiler optimizations" @echo " ENABLE_ASAN: Set to \"1\" to build with LLVM address sanitizer enabled. @echo " USER_CFLAGS: Add custom flags to default CFLAGS" @echo " USER_LDFLAGS: Add custom flags to default LDFLAGS" @echo " PREFIX: Specify a prefix directory for binaries, data files,... (default is \"$(abspath $(PREFIX))\")" @echo " DESTDIR: Specify a directory where to store installed files (mainly for packaging purpose)" @echo " MANDIR: Specify a directory where to store man pages (default is \"$(abspath $(PREFIX)/share/man)\")" @echo @echo "-- Environment Variables --" @echo " CFLAGS: Add custom flags to default CFLAGS" @echo " LDFLAGS: Add custom flags to default LDFLAGS" @echo " USER_CFLAGS: Add custom flags to default CFLAGS" @echo " USER_LDFLAGS: Add custom flags to default LDFLAGS" @echo " PREFIX: Specify a prefix directory for binaries, data files,... (default is \"$(abspath $(PREFIX))\")" @echo " DESTDIR: Specify a directory where to store installed files (mainly for packaging purpose)" @echo " MANDIR: Specify a directory where to store man pages (default is \"$(abspath $(PREFIX)/share/man)\")" .PHONY: help toxic-0.11.3/cfg/targets/install.mk000066400000000000000000000026231416141666600171470ustar00rootroot00000000000000# Install target install: $(BUILD_DIR)/toxic @echo "Installing toxic executable" @mkdir -p $(abspath $(DESTDIR)/$(BINDIR)) @install -m 0755 $(BUILD_DIR)/toxic $(abspath $(DESTDIR)/$(BINDIR)/toxic) @echo "Installing desktop file" @mkdir -p $(abspath $(DESTDIR)/$(APPDIR)) @install -m 0644 $(MISC_DIR)/$(DESKFILE) $(abspath $(DESTDIR)/$(APPDIR)/$(DESKFILE)) @echo "Installing data files" @mkdir -p $(abspath $(DESTDIR)/$(DATADIR)) @for f in $(DATAFILES) ; do \ install -m 0644 $(MISC_DIR)/$$f $(abspath $(DESTDIR)/$(DATADIR)/$$f) ;\ file=$(abspath $(DESTDIR)/$(DATADIR)/$$f) ;\ sed -e 's:__DATADIR__:'$(abspath $(DATADIR))':g' $$file > temp_file && \ mv temp_file $$file ;\ done @mkdir -p $(abspath $(DESTDIR)/$(DATADIR))/sounds @for f in $(SNDFILES) ; do \ install -m 0644 $(SND_DIR)/$$f $(abspath $(DESTDIR)/$(DATADIR)/sounds/$$f) ;\ done @echo "Installing man pages" @mkdir -p $(abspath $(DESTDIR)/$(MANDIR)) @for f in $(MANFILES) ; do \ if [ ! -e "$(DOC_DIR)/$$f" ]; then \ continue ;\ fi ;\ section=$(abspath $(DESTDIR)/$(MANDIR))/man`echo $${f##*.}` ;\ file=$$section/$$f ;\ mkdir -p $$section ;\ install -m 0644 $(DOC_DIR)/$$f $$file ;\ sed -e 's:__VERSION__:'$(VERSION)':g' $$file > temp_file && \ mv temp_file $$file ;\ sed -e 's:__DATADIR__:'$(abspath $(DATADIR))':g' $$file > temp_file && \ mv temp_file $$file ;\ gzip -f -n -9 $$file ;\ done .PHONY: install toxic-0.11.3/cfg/targets/uninstall.mk000066400000000000000000000011351416141666600175070ustar00rootroot00000000000000# Uninstall target uninstall: @echo "Removing toxic executable" @rm -f $(abspath $(DESTDIR)/$(BINDIR)/toxic) @echo "Removing desktop file" @rm -f $(abspath $(DESTDIR)/$(APPDIR)/$(DESKFILE)) @echo "Removing data files" @for f in $(DATAFILES) ; do \ rm -f $(abspath $(DESTDIR)/$(DATADIR)/$$f) ;\ done @for f in $(SNDFILES) ; do \ rm -f $(abspath $(DESTDIR)/$(DATADIR)/sounds/$$f) ;\ done @echo "Removing man pages" @for f in $(MANFILES) ; do \ section=$(abspath $(DESTDIR)/$(MANDIR))/man`echo $${f##*.}` ;\ file=$$section/$$f ;\ rm -f $$file $$file.gz ;\ done .PHONY: uninstall toxic-0.11.3/doc/000077500000000000000000000000001416141666600135025ustar00rootroot00000000000000toxic-0.11.3/doc/toxic.1000066400000000000000000000100711416141666600147110ustar00rootroot00000000000000'\" t .\" Title: toxic .\" Author: [see the "AUTHORS" section] .\" Generator: DocBook XSL Stylesheets v1.79.1 .\" Date: 2021-05-24 .\" Manual: Toxic Manual .\" Source: toxic __VERSION__ .\" Language: English .\" .TH "TOXIC" "1" "2021\-05\-24" "toxic __VERSION__" "Toxic Manual" .\" ----------------------------------------------------------------- .\" * Define some portability stuff .\" ----------------------------------------------------------------- .\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .\" http://bugs.debian.org/507673 .\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html .\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .ie \n(.g .ds Aq \(aq .el .ds Aq ' .\" ----------------------------------------------------------------- .\" * set default formatting .\" ----------------------------------------------------------------- .\" disable hyphenation .nh .\" disable justification (adjust text to left margin only) .ad l .\" ----------------------------------------------------------------- .\" * MAIN CONTENT STARTS HERE * .\" ----------------------------------------------------------------- .SH "NAME" toxic \- CLI client for Tox .SH "SYNOPSIS" .sp \fBtoxic\fR [\-f \fIdata\-file\fR] [\-x] [\-4] [\-c \fIconfig\-file\fR] [\-n \fInodes\-file\fR] [\-h] .SH "DESCRIPTION" .sp toxic is an ncurses\-based instant messaging client for Tox which formerly resided in the Tox core repository, and is now available as a standalone application\&. .SH "OPTIONS" .PP \-4, \-\-ipv4 .RS 4 Force IPv4 connection .RE .PP \-b, \-\-debug .RS 4 Enable stderr for debugging\&. Redirect output to avoid breaking the curses interface and better capture messages\&. .RE .PP \-c, \-\-config config\-file .RS 4 Use specified \fIconfig\-file\fR instead of \fI~/\&.config/tox/toxic\&.conf\fR .RE .PP \-d, \-\-default\-locale .RS 4 Use default locale .RE .PP \-e, \-\-encrypt\-data .RS 4 Encrypt an unencrypted data file\&. An error will occur if this option is used with an encrypted data file\&. .RE .PP \-f, \-\-file data\-file .RS 4 Use specified \fIdata\-file\fR instead of \fI~/\&.config/tox/toxic_profile\&.tox\fR .RE .PP \-h, \-\-help .RS 4 Show help message .RE .PP \-l, \-\-logging .RS 4 Enable toxcore logging to stderr .RE .PP \-n, \-\-nodes nodes\-file .RS 4 Use specified \fInodes\-file\fR for DHT bootstrap nodes instead of \fI~/\&.config/tox/DHTnodes\&.json\fR .RE .PP \-o, \-\-noconnect .RS 4 Do not connect to the DHT network .RE .PP \-p, \-\-SOCKS5\-proxy .RS 4 Use a SOCKS5 proxy: Requires [IP] [port] .RE .PP \-P, \-\-HTTP\-proxy .RS 4 Use a HTTP proxy: Requires [IP] [port] .RE .PP \-r, \-\-namelist .RS 4 Use specified nameservers list .RE .PP \-t, \-\-force\-tcp .RS 4 Force TCP connection (use this with proxies) .RE .PP \-T, \-\-tcp\-relay .RS 4 Act as a TCP relay server for the network (Note: this uses significantly more bandwidth) .RE .PP \-u, \-\-unencrypt\-data .RS 4 Unencrypt a data file\&. A warning will appear if this option is used with a data file that is already unencrypted\&. .RE .SH "FILES" .PP ~/\&.config/tox/DHTnodes\&.json .RS 4 Default location for list of DHT bootstrap nodes (list obtained from https://nodes\&.tox\&.chat)\&. This list is automatically updated\&. See \fBtoxic\&.conf\fR(5) for details on controlling the update frequency\&. .RE .PP ~/\&.config/tox/toxic_profile\&.tox .RS 4 Savestate which contains your personal info (nickname, Tox ID, contacts, etc) .RE .PP ~/\&.config/tox/toxic\&.conf .RS 4 Configuration file\&. See \fBtoxic\&.conf\fR(5) for more details\&. .RE .PP __DATADIR__/toxic\&.conf\&.example .RS 4 Configuration example\&. .RE .SH "BUGS" .sp \-Unicode characters with a width larger than 1 column may cause strange behaviour\&. .sp \-Screen flickering sometimes occurs on certain terminals\&. .sp \-Resizing the terminal window when a game window is open will break things\&. .SH "AUTHORS" .sp JFreegman .SH "SEE ALSO" .sp \fBtoxic\&.conf\fR(5) .SH "LINKS" .sp Project page: https://github\&.com/JFreegman/toxic .sp IRC channel: irc\&.libera\&.chat#tox toxic-0.11.3/doc/toxic.1.asc000066400000000000000000000050731416141666600154640ustar00rootroot00000000000000toxic(1) ======== NAME ---- toxic - CLI client for Tox SYNOPSIS -------- *toxic* [-f 'data-file'] [-x] [-4] [-c 'config-file'] [-n 'nodes-file'] [-h] DESCRIPTION ----------- toxic is an ncurses-based instant messaging client for Tox which formerly resided in the Tox core repository, and is now available as a standalone application. OPTIONS ------- -4, --ipv4:: Force IPv4 connection -b, --debug:: Enable stderr for debugging. Redirect output to avoid breaking the curses interface and better capture messages. -c, --config config-file:: Use specified 'config-file' instead of '~/.config/tox/toxic.conf' -d, --default-locale:: Use default locale -e, --encrypt-data:: Encrypt an unencrypted data file. An error will occur if this option is used with an encrypted data file. -f, --file data-file:: Use specified 'data-file' instead of '~/.config/tox/toxic_profile.tox' -h, --help:: Show help message -l, --logging:: Enable toxcore logging to stderr -n, --nodes nodes-file:: Use specified 'nodes-file' for DHT bootstrap nodes instead of '~/.config/tox/DHTnodes.json' -o, --noconnect:: Do not connect to the DHT network -p, --SOCKS5-proxy:: Use a SOCKS5 proxy: Requires [IP] [port] -P, --HTTP-proxy:: Use a HTTP proxy: Requires [IP] [port] -r, --namelist:: Use specified nameservers list -t, --force-tcp:: Force TCP connection (use this with proxies) -T, --tcp-relay:: Act as a TCP relay server for the network (Note: this uses significantly more bandwidth) -u, --unencrypt-data:: Unencrypt a data file. A warning will appear if this option is used with a data file that is already unencrypted. FILES ----- ~/.config/tox/DHTnodes.json:: Default location for list of DHT bootstrap nodes (list obtained from https://nodes.tox.chat). This list is automatically updated. See *toxic.conf*(5) for details on controlling the update frequency. ~/.config/tox/toxic_profile.tox:: Savestate which contains your personal info (nickname, Tox ID, contacts, etc) ~/.config/tox/toxic.conf:: Configuration file. See *toxic.conf*(5) for more details. {datadir}/toxic.conf.example:: Configuration example. BUGS ---- -Unicode characters with a width larger than 1 column may cause strange behaviour. -Screen flickering sometimes occurs on certain terminals. -Resizing the terminal window when a game window is open will break things. AUTHORS ------- JFreegman SEE ALSO -------- *toxic.conf*(5) LINKS ----- Project page: IRC channel: irc.libera.chat#tox toxic-0.11.3/doc/toxic.conf.5000066400000000000000000000220121416141666600156370ustar00rootroot00000000000000'\" t .\" Title: toxic.conf .\" Author: [see the "AUTHORS" section] .\" Generator: DocBook XSL Stylesheets v1.79.1 .\" Date: 2021-05-24 .\" Manual: Toxic Manual .\" Source: toxic __VERSION__ .\" Language: English .\" .TH "TOXIC\&.CONF" "5" "2021\-05\-24" "toxic __VERSION__" "Toxic Manual" .\" ----------------------------------------------------------------- .\" * Define some portability stuff .\" ----------------------------------------------------------------- .\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .\" http://bugs.debian.org/507673 .\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html .\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .ie \n(.g .ds Aq \(aq .el .ds Aq ' .\" ----------------------------------------------------------------- .\" * set default formatting .\" ----------------------------------------------------------------- .\" disable hyphenation .nh .\" disable justification (adjust text to left margin only) .ad l .\" ----------------------------------------------------------------- .\" * MAIN CONTENT STARTS HERE * .\" ----------------------------------------------------------------- .SH "NAME" toxic.conf \- Configuration file for toxic .SH "SYNOPSIS" .sp ~/\&.config/tox/toxic\&.conf .SH "DESCRIPTION" .sp The \fItoxic\&.conf\fR file is the main configuration file for \fBtoxic\fR(1) client\&. It uses syntax accepted by \fBlibconfig\fR\&. Lines starting with "//" are comments and will be ignored\&. .SH "EXAMPLE" .sp .if n \{\ .RS 4 .\} .nf // Configuration for interface ui = { timestamps = true; alerts = false; }; // Configuration for audio audio = { input_device = 1; }; .fi .if n \{\ .RE .\} .SH "OPTIONS" .PP \fBui\fR .RS 4 Configuration related to interface elements\&. .PP \fBtimestamps\fR .RS 4 Enable or disable timestamps\&. true or false .RE .PP \fBtime_format\fR .RS 4 Select between 24 and 12 hour time\&. Specify 24 or 12\&. Setting timestamp_format and log_timestamp_format will override this setting\&. .RE .PP \fBtimestamp_format\fR .RS 4 Time format string for the interface enclosed by double quotes\&. See \fBdate\fR(1) .RE .PP \fBlog_timestamp_format\fR .RS 4 Time format string for logging enclosed by double quotes\&. See \fBdate\fR(1) .RE .PP \fBalerts\fR .RS 4 Enable or disable acoustic alerts on events\&. true or false .RE .PP \fBnative_colors\fR .RS 4 Select between native terminal colors and toxic color theme\&. true or false .RE .PP \fBcolor_bar_bg\fR .RS 4 set background color of chat status bars\&. (black, white, red, green, blue, cyan, yellow, magenta) .RE .PP \fBcolor_bar_fg\fR .RS 4 set foreground (text) color of chat status bars\&. (black, white, red, green, blue, cyan, yellow, magenta) .RE .PP \fBcolor_bar_accent\fR .RS 4 set foreground accent color of chat status bars\&. (black, white, red, green, blue, cyan, yellow, magenta) .RE .PP \fBcolor_bar_notify\fR .RS 4 set foreground notify (and typing) color in chat status bar\&. (black, white, red, green, blue, cyan, yellow, magenta) .RE .PP \fBautolog\fR .RS 4 Enable or disable autologging\&. true or false .RE .PP \fBshow_typing_other\fR .RS 4 Show when others are typing in a 1\-on\-1 chat\&. true or false .RE .PP \fBshow_typing_self\fR .RS 4 Show others when you\(cqre typing in a 1\-on\-1 chat\&. true or false .RE .PP \fBshow_welcome_msg\fR .RS 4 Show welcome message on startup\&. true or false .RE .PP \fBshow_connection_msg\fR .RS 4 Enable friend connection change notifications\&. true or false .RE .PP \fBnodelist_update_freq\fR .RS 4 How often in days to update the DHT nodes list\&. (integer; 0 to disable) .RE .PP \fBautosave_freq\fR .RS 4 How often in seconds to auto\-save the Tox data file\&. (integer; 0 to disable) .RE .PP \fBhistory_size\fR .RS 4 Maximum lines for chat window history\&. Integer value\&. (for example: 700) .RE .PP \fBnotification_timeout\fR .RS 4 Time in milliseconds to display a notification\&. Integer value\&. (for example: 3000) .RE .PP \fBline_join\fR .RS 4 Indicator for when someone connects or joins a group\&. Three characters max for line_ settings\&. .RE .PP \fBline_quit\fR .RS 4 Indicator for when someone disconnects or leaves a group\&. .RE .PP \fBline_alert\fR .RS 4 Indicator for alert messages\&. .RE .PP \fBline_normal\fR .RS 4 Indicator for normal messages\&. .RE .PP \fBmplex_away\fR .RS 4 Set user status when attaching and detaching from GNU screen or tmux\&. true or false .RE .PP \fBmplex_away_note\fR .RS 4 Status message to set when status is set to away due to screen/tmux detach\&. When attaching, the status message is set back to the original value\&. .sp .if n \{\ .RS 4 .\} .nf The following options control whether to output a terminal bell on certain events\&. Some terminals mark the window as urgent when a bell is received\&. Urgent windows are usually highlighted in the taskbar and some window managers even provide shortcuts to jump to the next urgent window\&. These options don\*(Aqt affect the "alerts" option\&. .fi .if n \{\ .RE .\} .RE .PP \fBbell_on_message\fR .RS 4 Enable/Disable the terminal bell when receiving a message\&. true or false .RE .PP \fBbell_on_filetrans\fR .RS 4 Enable/Disable the terminal bell when receiving a filetransfer\&. true or false .RE .PP \fBbell_on_filetrans_accept\fR .RS 4 Enable/Disable the terminal bell when a filetransfer was accepted\&. true or false .RE .PP \fBbell_on_invite\fR .RS 4 Enable/Disable the terminal bell when receiving a group/call invite\&. true or false .RE .RE .PP \fBaudio\fR .RS 4 Configuration related to audio devices\&. .PP \fBinput_device\fR .RS 4 Audio input device\&. Integer value\&. Number corresponds to /lsdev in .RE .PP \fBoutput_device\fR .RS 4 Audio output device\&. Integer value\&. Number corresponds to /lsdev out .RE .PP \fBVAD_threshold\fR .RS 4 Voice Activity Detection threshold\&. Float value\&. Recommended values are 1\&.0\-40\&.0 .RE .PP \fBconference_audio_channels\fR .RS 4 Number of channels for conference audio broadcast\&. Integer value\&. 1 (mono) or 2 (stereo) .RE .PP \fBchat_audio_channels\fR .RS 4 Number of channels for 1\-on\-1 audio broadcast\&. Integer value\&. 1 (mono) or 2 (stereo) .RE .PP \fBpush_to_talk\fR .RS 4 Enable/Disable Push\-To\-Talk for conference audio chats (active key is F2)\&. true or false .RE .RE .PP \fBtox\fR .RS 4 Configuration related to paths\&. .PP \fBdownload_path\fR .RS 4 Default path for downloads\&. String value\&. Absolute path for downloaded files\&. .RE .PP \fBavatar_path\fR .RS 4 Path for your avatar (file must be a \&.png and cannot exceed 16\&.3 KiB) .RE .PP \fBautorun_path\fR .RS 4 Path for any scripts that should be run on startup .RE .PP \fBchatlogs_path\fR .RS 4 Default path for chatlogs\&. String value\&. Absolute path for chatlog files\&. .RE .PP \fBpassword_eval\fR .RS 4 Replace password prompt by running this command and using its output as the password\&. .RE .RE .PP \fBsounds\fR .RS 4 Configuration related to notification sounds\&. Special value "silent" can be used to disable a specific notification\&. Each value is a string which corresponds to the absolute path of a wav sound file\&. .PP \fBnotif_error\fR .RS 4 Sound to play when an error occurs\&. .RE .PP \fBself_log_in\fR .RS 4 Sound to play when you log in\&. .RE .PP \fBself_log_out\fR .RS 4 Sound to play when you log out\&. .RE .PP \fBuser_log_in\fR .RS 4 Sound to play when a contact become online\&. .RE .PP \fBuser_log_out\fR .RS 4 Sound to play when a contact become offline\&. .RE .PP \fBcall_incoming\fR .RS 4 Sound to play when you receive an incoming call\&. .RE .PP \fBcall_outgoing\fR .RS 4 Sound to play when you start a call\&. .RE .PP \fBgeneric_message\fR .RS 4 Sound to play when an event occurs\&. .RE .PP \fBtransfer_pending\fR .RS 4 Sound to play when you receive a file transfer request\&. .RE .PP \fBtransfer_completed\fR .RS 4 Sound to play when a file transfer is completed\&. .RE .RE .PP \fBkeys\fR .RS 4 Configuration related to user interface interaction\&. Currently supported: Ctrl modified keys, Tab, PAGEUP and PAGEDOWN\&. Each value is a string which corresponds to a key combination\&. .PP \fBnext_tab\fR .RS 4 Key combination to switch next tab\&. .RE .PP \fBprev_tab\fR .RS 4 Key combination to switch previous tab\&. .RE .PP \fBscroll_line_up\fR .RS 4 Key combination to scroll one line up\&. .RE .PP \fBscroll_line_down\fR .RS 4 Key combination to scroll one line down\&. .RE .PP \fBhalf_page_up\fR .RS 4 Key combination to scroll half page up\&. .RE .PP \fBhalf_page_down\fR .RS 4 Key combination to scroll half page down\&. .RE .PP \fBpage_bottom\fR .RS 4 Key combination to scroll to page bottom\&. .RE .PP \fBtoggle_peerlist\fR .RS 4 Toggle the peer list on and off\&. .RE .PP \fBtoggle_paste_mode\fR .RS 4 Toggle treating linebreaks as enter key press\&. .RE .RE .SH "FILES" .PP ~/\&.config/tox/toxic\&.conf .RS 4 Main configuration file\&. .RE .PP __DATADIR__/toxic\&.conf\&.example .RS 4 Configuration example\&. .RE .SH "SEE ALSO" .sp \fBtoxic\fR(1) .SH "RESOURCES" .sp Project page: https://github\&.com/JFreegman/toxic .sp IRC channel: irc\&.libera\&.chat#tox .SH "AUTHORS" .sp JFreegman toxic-0.11.3/doc/toxic.conf.5.asc000066400000000000000000000166101416141666600164130ustar00rootroot00000000000000toxic.conf(5) ============= NAME ---- toxic.conf - Configuration file for toxic SYNOPSIS -------- ~/.config/tox/toxic.conf DESCRIPTION ----------- The 'toxic.conf' file is the main configuration file for *toxic*(1) client. It uses syntax accepted by *libconfig*. Lines starting with "//" are comments and will be ignored. EXAMPLE ------- ---- // Configuration for interface ui = { timestamps = true; alerts = false; }; // Configuration for audio audio = { input_device = 1; }; ---- OPTIONS ------- *ui*:: Configuration related to interface elements. *timestamps*;; Enable or disable timestamps. true or false *time_format*;; Select between 24 and 12 hour time. Specify 24 or 12. Setting timestamp_format and log_timestamp_format will override this setting. *timestamp_format*;; Time format string for the interface enclosed by double quotes. See *date*(1) *log_timestamp_format*;; Time format string for logging enclosed by double quotes. See *date*(1) *alerts*;; Enable or disable acoustic alerts on events. true or false *native_colors*;; Select between native terminal colors and toxic color theme. true or false *color_bar_bg*;; set background color of chat status bars. (black, white, red, green, blue, cyan, yellow, magenta) *color_bar_fg*;; set foreground (text) color of chat status bars. (black, white, red, green, blue, cyan, yellow, magenta) *color_bar_accent*;; set foreground accent color of chat status bars. (black, white, red, green, blue, cyan, yellow, magenta) *color_bar_notify*;; set foreground notify (and typing) color in chat status bar. (black, white, red, green, blue, cyan, yellow, magenta) *autolog*;; Enable or disable autologging. true or false *show_typing_other*;; Show when others are typing in a 1-on-1 chat. true or false *show_typing_self*;; Show others when you're typing in a 1-on-1 chat. true or false *show_welcome_msg*;; Show welcome message on startup. true or false *show_connection_msg*;; Enable friend connection change notifications. true or false *nodelist_update_freq*;; How often in days to update the DHT nodes list. (integer; 0 to disable) *autosave_freq*;; How often in seconds to auto-save the Tox data file. (integer; 0 to disable) *history_size*;; Maximum lines for chat window history. Integer value. (for example: 700) *notification_timeout*;; Time in milliseconds to display a notification. Integer value. (for example: 3000) *line_join*;; Indicator for when someone connects or joins a group. Three characters max for line_ settings. *line_quit*;; Indicator for when someone disconnects or leaves a group. *line_alert*;; Indicator for alert messages. *line_normal*;; Indicator for normal messages. *mplex_away*;; Set user status when attaching and detaching from GNU screen or tmux. true or false *mplex_away_note*;; Status message to set when status is set to away due to screen/tmux detach. When attaching, the status message is set back to the original value. The following options control whether to output a terminal bell on certain events. Some terminals mark the window as urgent when a bell is received. Urgent windows are usually highlighted in the taskbar and some window managers even provide shortcuts to jump to the next urgent window. These options don't affect the "alerts" option. *bell_on_message*;; Enable/Disable the terminal bell when receiving a message. true or false *bell_on_filetrans*;; Enable/Disable the terminal bell when receiving a filetransfer. true or false *bell_on_filetrans_accept*;; Enable/Disable the terminal bell when a filetransfer was accepted. true or false *bell_on_invite*;; Enable/Disable the terminal bell when receiving a group/call invite. true or false *audio*:: Configuration related to audio devices. *input_device*;; Audio input device. Integer value. Number corresponds to `/lsdev in` *output_device*;; Audio output device. Integer value. Number corresponds to `/lsdev out` *VAD_threshold*;; Voice Activity Detection threshold. Float value. Recommended values are 1.0-40.0 *conference_audio_channels*;; Number of channels for conference audio broadcast. Integer value. 1 (mono) or 2 (stereo) *chat_audio_channels*;; Number of channels for 1-on-1 audio broadcast. Integer value. 1 (mono) or 2 (stereo) *push_to_talk*;; Enable/Disable Push-To-Talk for conference audio chats (active key is F2). true or false *tox*:: Configuration related to paths. *download_path*;; Default path for downloads. String value. Absolute path for downloaded files. *avatar_path*;; Path for your avatar (file must be a .png and cannot exceed 16.3 KiB) *autorun_path*;; Path for any scripts that should be run on startup *chatlogs_path*;; Default path for chatlogs. String value. Absolute path for chatlog files. *password_eval*;; Replace password prompt by running this command and using its output as the password. *sounds*:: Configuration related to notification sounds. Special value "silent" can be used to disable a specific notification. + Each value is a string which corresponds to the absolute path of a wav sound file. *notif_error*;; Sound to play when an error occurs. *self_log_in*;; Sound to play when you log in. *self_log_out*;; Sound to play when you log out. *user_log_in*;; Sound to play when a contact become online. *user_log_out*;; Sound to play when a contact become offline. *call_incoming*;; Sound to play when you receive an incoming call. *call_outgoing*;; Sound to play when you start a call. *generic_message*;; Sound to play when an event occurs. *transfer_pending*;; Sound to play when you receive a file transfer request. *transfer_completed*;; Sound to play when a file transfer is completed. *keys*:: Configuration related to user interface interaction. Currently supported: Ctrl modified keys, Tab, PAGEUP and PAGEDOWN. + Each value is a string which corresponds to a key combination. *next_tab*;; Key combination to switch next tab. *prev_tab*;; Key combination to switch previous tab. *scroll_line_up*;; Key combination to scroll one line up. *scroll_line_down*;; Key combination to scroll one line down. *half_page_up*;; Key combination to scroll half page up. *half_page_down*;; Key combination to scroll half page down. *page_bottom*;; Key combination to scroll to page bottom. *toggle_peerlist*;; Toggle the peer list on and off. *toggle_paste_mode*;; Toggle treating linebreaks as enter key press. FILES ----- ~/.config/tox/toxic.conf:: Main configuration file. {datadir}/toxic.conf.example:: Configuration example. SEE ALSO -------- *toxic*(1) RESOURCES --------- Project page: IRC channel: irc.libera.chat#tox AUTHORS ------- JFreegman toxic-0.11.3/misc/000077500000000000000000000000001416141666600136705ustar00rootroot00000000000000toxic-0.11.3/misc/nameservers000066400000000000000000000001121416141666600161370ustar00rootroot00000000000000toxme.io 1A39E7A5D5FA9CF155C751570A32E625698A60A55F6D88028F949F66144F4F25 toxic-0.11.3/misc/toxic.conf.example000066400000000000000000000110151416141666600173150ustar00rootroot00000000000000// SAMPLE TOXIC CONFIGURATION // USES LIBCONFIG-ACCEPTED SYNTAX ui = { // true to enable timestamps, false to disable timestamps=true; // true to enable acoustic alerts on messages, false to disable alerts=true; // Output a bell when receiving a message (see manpage) bell_on_message=true; // Output a bell when receiving a filetransfer (see manpage) bell_on_filetrans=true; // Don't output a bell when a filetransfer was accepted (see manpage) bell_on_filetrans_accept=false; // Output a bell when receiving a group/call invite (see manpage) bell_on_invite=true; // true to use native terminal colours, false to use toxic default colour theme native_colors=false; // set background color of chat status bars (black, white, red, green, blue, cyan, yellow, magenta) color_bar_bg="blue"; // set foreground (text) color of chat status bars (black, white, red, green, blue, cyan, yellow, magenta) color_bar_fg="white"; // set foreground accent color of chat status bars (black, white, red, green, blue, cyan, yellow, magenta) color_bar_accent="cyan"; // set foreground notify (and typing) color in chat status bar (black, white, red, green, blue, cyan, yellow, magenta) color_bar_notify="yellow"; // true to enable autologging, false to disable autolog=false; // 24 or 12 hour time time_format=24; // Timestamp format string according to date/strftime format. Overrides time_format setting timestamp_format="%H:%M"; // true to show you when others are typing a message in 1-on-1 chats show_typing_other=true; // true to show others when you're typing a message in 1-on-1 chats show_typing_self=true; // true to show the welcome message on startup show_welcome_msg=true; // true to show friend connection change messages on the home screen show_connection_msg=true; // How often in days to update the DHT nodes list. (0 to disable updates) nodeslist_update_freq=7; // How often in seconds to auto-save the Tox data file. (0 to disable periodic auto-saves) autosave_freq=600; // maximum lines for chat window history history_size=700; // time in milliseconds to display a notification notification_timeout=6000; // Indicator for display when someone connects or joins a group line_join="-->"; // Indicator for display when someone disconnects or leaves a group line_quit="<--"; // Indicator for alert messages. line_alert="-!-"; // Indicator for normal messages. line_normal="-"; // true to change status based on screen/tmux attach/detach, false to disable mplex_away=true; // Status message to use when status set to away due to screen/tmux detach mplex_away_note="Away from keyboard, be back soon!" }; audio = { // preferred audio input device; numbers correspond to /lsdev in input_device=2; // preferred audio output device; numbers correspond to /lsdev out output_device=0; // default VAD threshold; float (recommended values are 1.0-40.0) VAD_threshold=5.0; // Number of channels to use for conference audio broadcasts; 1 for mono, 2 for stereo. conference_audio_channels=1; // Number of channels to use for 1-on-1 audio broadcasts; 1 for mono, 2 for stereo. chat_audio_channels=2; // toggle conference push-to-talk push_to_talk=false; }; tox = { // Path for downloaded files // download_path="/home/USERNAME/Downloads/"; // Path for your avatar (file must be a .png and cannot exceed 64 KiB) // avatar_path="/home/USERNAME/Pictures/youravatar.png"; // Path for scripts that should be run on startup // autorun_path="/home/USERNAME/toxic_scripts/"; // Path for chatlogs // chatlogs_path="/home/USERNAME/toxic_chatlogs/"; }; // To disable a sound set the path to "silent" sounds = { error="__DATADIR__/sounds/ToxicError.wav"; user_log_in="__DATADIR__/sounds/ToxicContactOnline.wav"; user_log_out="__DATADIR__/sounds/ToxicContactOffline.wav"; call_incoming="__DATADIR__/sounds/ToxicIncomingCall.wav"; call_outgoing="__DATADIR__/sounds/ToxicOutgoingCall.wav"; generic_message="__DATADIR__/sounds/ToxicRecvMessage.wav"; transfer_pending="__DATADIR__/sounds/ToxicTransferStart.wav"; transfer_completed="__DATADIR__/sounds/ToxicTransferComplete.wav"; }; // Currently supported: Ctrl modified keys, Tab, PAGEUP and PAGEDOWN (case insensitive) // Note: Ctrl+M does not work keys = { next_tab="Ctrl+P"; prev_tab="Ctrl+O"; scroll_line_up="PAGEUP"; scroll_line_down="PAGEDOWN"; half_page_up="Ctrl+F"; half_page_down="Ctrl+V"; page_bottom="Ctrl+H"; toggle_peerlist="Ctrl+B"; toggle_paste_mode="Ctrl+T"; }; toxic-0.11.3/misc/toxic.desktop000066400000000000000000000003451416141666600164130ustar00rootroot00000000000000[Desktop Entry] Version=1.0 Type=Application Name=Toxic Comment=A CLI based Tox client TryExec=toxic Exec=toxic Icon=utilities-terminal Categories=InstantMessaging;AudioVideo;Network; Terminal=true MimeType=x-scheme-handler/tox; toxic-0.11.3/script/000077500000000000000000000000001416141666600142415ustar00rootroot00000000000000toxic-0.11.3/script/build-minimal-static-toxic.sh000077500000000000000000000226141416141666600217410ustar00rootroot00000000000000#!/usr/bin/env sh # MIT License # # Copyright (c) 2021 Maxim Biro # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # Script for building a minimal statically compiled Toxic. While it doesn't # support X11 integration, video/audio calls, desktop & sound notifications, QR # codes and Python scripting, it is rather portable. # # Run as: # # sudo docker run -it --rm \ # -v /tmp/artifact:/artifact \ # -v /home/jfreegman/git/toxic:/toxic \ # amd64/alpine:latest \ # /bin/sh /toxic/script/build-minimal-static-toxic.sh # # that would use Toxic code from /home/jfreegman/git/toxic and place the build # artifact at /tmp/artifact. # You can change between amd64/alpine:latest and i386/alpine:latest, for 64-bit # and 32-bit builds. # # To debug, run: # # sudo docker run -it --rm \ # -v /tmp/artifact:/artifact \ # -v /home/jfreegman/git/toxic:/toxic \ # amd64/alpine:latest \ # /bin/sh # # sh /toxic/script/build-minimal-static-toxic.sh set -eu ARTIFACT_DIR="/artifact" TOXIC_SRC_DIR="/toxic" # Make sure we run in the expected environment if ! grep -q 'docker' /proc/1/cgroup then echo "Error: This script should be run inside a disposable Docker container as it might modify system files in ways that would break a real system." exit 1 fi if [ ! -f /etc/os-release ] || ! grep -qi 'Alpine Linux' /etc/os-release then echo "Error: This script expects to be run on Alpine Linux." exit 1 fi if [ ! -d "$ARTIFACT_DIR" ] || [ ! -d "$TOXIC_SRC_DIR" ] then echo "Error: At least one of $ARTIFACT_DIR or $TOXIC_SRC_DIR directories inside the container is missing." exit 1 fi if [ "$(id -u)" != "0" ] then echo "Error: This script expects to be run as root." exit 1 fi set -x # Use all cores for building MAKEFLAGS=j$(nproc) export MAKEFLAGS check_sha256() { if ! ( echo "$1 $2" | sha256sum -cs - ) then echo "Error: sha256 of $2 doesn't match the known one." echo "Expected: $1 $2" echo "Got: $(sha256sum "$2")" exit 1 else echo "sha256 matches the expected one: $1" fi } apk update apk upgrade apk add \ brotli-dev \ brotli-static \ build-base \ cmake \ git \ libconfig-dev \ libconfig-static \ libsodium-dev \ libsodium-static \ linux-headers \ ncurses-dev \ ncurses-static \ ncurses-terminfo \ ncurses-terminfo-base \ nghttp2-dev \ nghttp2-static \ openssl-dev \ openssl-libs-static \ pkgconf \ wget \ xz \ zlib-dev \ zlib-static BUILD_DIR="/tmp/build" mkdir -p "$BUILD_DIR" # Build Toxcore cd "$BUILD_DIR" # The git hash of the c-toxcore version we're using TOXCORE_VERSION="v0.2.13" # The sha256sum of the c-toxcore tarball for TOXCORE_VERSION TOXCORE_HASH="67114fa57504c58b695f5dce8ef85124d555f2c3c353d0d2615e6d4845114ab8" TOXCORE_FILENAME="c-toxcore-$TOXCORE_VERSION.tar.gz" wget --timeout=10 -O "$TOXCORE_FILENAME" "https://github.com/TokTok/c-toxcore/archive/$TOXCORE_VERSION.tar.gz" check_sha256 "$TOXCORE_HASH" "$TOXCORE_FILENAME" tar -o -xf "$TOXCORE_FILENAME" rm "$TOXCORE_FILENAME" cd c-toxcore* cmake -B_build -H. \ -DENABLE_STATIC=ON \ -DENABLE_SHARED=OFF \ -DCMAKE_BUILD_TYPE=Release \ -DBUILD_TOXAV=OFF \ -DBOOTSTRAP_DAEMON=OFF \ -DDHT_BOOTSTRAP=OFF \ -DCMAKE_INSTALL_PREFIX="$BUILD_DIR/prefix-toxcore" cmake --build _build --target install # Build cURL # While Alpine does provide a static cURL build, it's not built with # --with-ca-fallback, which is needed for better cross-distro portability. # Basically, some distros put their ca-certificates in different places, and # with --with-ca-fallback we or the user can provide the cert bundle file # location with SSL_CERT_FILE env variable. cd "$BUILD_DIR" CURL_VERSION="7.80.0" CURL_HASH="dab997c9b08cb4a636a03f2f7f985eaba33279c1c52692430018fae4a4878dc7" CURL_FILENAME="curl-$CURL_VERSION.tar.gz" wget --timeout=10 -O "$CURL_FILENAME" "https://curl.haxx.se/download/$CURL_FILENAME" check_sha256 "$CURL_HASH" "$CURL_FILENAME" tar -xf curl*.tar.gz rm curl*.tar.gz cd curl* ./configure \ --prefix="$BUILD_DIR/prefix-curl" \ --disable-shared \ --enable-static \ --without-ca-bundle \ --without-ca-path \ --with-ca-fallback \ --with-nghttp2 \ --with-brotli \ --with-openssl make make install sed -i 's|-lbrotlidec |-lbrotlidec-static -lbrotlicommon-static |g' $BUILD_DIR/prefix-curl/lib/pkgconfig/libcurl.pc # Build Toxic cd "$BUILD_DIR" cp -a "$TOXIC_SRC_DIR" toxic cd toxic if [ -z "$(git describe --tags --exact-match HEAD)" ] then set +x echo "Didn't find a git tag on the HEAD commit. You seem to be building an in-development release of Toxic rather than a release version." | fold -sw 80 printf "Do you wish to proceed? (y/N): " read -r answer if echo "$answer" | grep -v -iq "^y" ; then echo "Exiting." exit 1 fi set -x fi sed -i 's|pkg-config|pkg-config --static|' cfg/global_vars.mk sed -i 's| "$PREPARE_ARTIFACT_DIR/build_info" echo '#!/usr/bin/env sh DEBIAN_SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt RHEL_SSL_CERT_FILE=/etc/pki/tls/certs/ca-bundle.crt OPENSUSE_CERT_FILE=/etc/ssl/ca-bundle.pem if [ ! -f "$SSL_CERT_FILE" ] ; then if [ -f "$DEBIAN_SSL_CERT_FILE" ] ; then SSL_CERT_FILE="$DEBIAN_SSL_CERT_FILE" elif [ -f "$RHEL_SSL_CERT_FILE" ] ; then SSL_CERT_FILE="$RHEL_SSL_CERT_FILE" elif [ -f "$OPENSUSE_CERT_FILE" ] ; then SSL_CERT_FILE="$OPENSUSE_CERT_FILE" fi fi if [ -z "$SSL_CERT_FILE" ] ; then echo "Warning: Couldn'\''t find the SSL CA certificate store file." | fold -sw 80 echo echo "Toxic uses HTTPS to download a list of DHT bootstrap nodes in order to connect to the Tox DHT. This functionality is optional, you should be able to use Toxic without it. If you choose to use Toxic without it, you might need to manually enter DHT bootstrap node information using the '\''/connect'\'' command in order to come online." | fold -sw 80 echo echo "To fix this issue, install SSL CAs as provided by your Linux distribution, e.g. '\''ca-certificates'\'' package on Debian/Ubuntu. If it'\''s already installed and you still see this message, run this script with SSL_CERT_FILE variable set to point to the SSL CA certificate store file location. The file is usually named '\''ca-certificates.crt'\'' or '\''ca-bundle.pem'\''." | fold -sw 80 echo printf "Do you wish to run Toxic without SSL CA certificate store file found? (y/N): " read -r answer if echo "$answer" | grep -v -iq "^y" ; then echo "Exiting." exit fi fi cd "$(dirname -- $0)" SSL_CERT_FILE="$SSL_CERT_FILE" TERMINFO=./terminfo ./toxic -c toxic.conf' > "$PREPARE_ARTIFACT_DIR/run_toxic.sh" chmod a+x "$PREPARE_ARTIFACT_DIR/run_toxic.sh" # Tar it cd "$PREPARE_ARTIFACT_DIR" cd .. ARCH="$(tr '_' '-' < /etc/apk/arch)" ARTIFACT_NAME="toxic-minimal-static-musl_linux_$ARCH" mv "$PREPARE_ARTIFACT_DIR" "$PREPARE_ARTIFACT_DIR/../$ARTIFACT_NAME" tar -cJf "$ARTIFACT_NAME.tar.xz" "$ARTIFACT_NAME" mv "$ARTIFACT_NAME.tar.xz" "$ARTIFACT_DIR" chmod 777 -R "$ARTIFACT_DIR" toxic-0.11.3/sounds/000077500000000000000000000000001416141666600142505ustar00rootroot00000000000000toxic-0.11.3/sounds/ToxicContactOffline.wav000066400000000000000000003311441416141666600207020ustar00rootroot00000000000000RIFF\WAVEfmt Ddata4tn\a64UU@ A kiPTinB=)"   ) . @=SS5 8   ] b 4 8 p n }  [ [   ] \ U W " &  JD_[45LN $&&  a c : 7 ` d } w y ` _ @ > X \ 6 2   //XXHG@B# TVnnNN>;}_^D>OL$!63`a __PQQOPSb^}64:;LK55YW# QR / 2 O L ` c w v }~xv`bCA  ondg$#   KHZ^ B>  ) '  " moIN|~-,f e )(}|pp,+=>USkn "b`\[c^۷ڼ'#ڦ٩zxqq;<^]ff__ggډډڂux݃߃ߖhjlgpu}yjm/.85$(4 9 m o rqQTIKY\ !!""##$$8%5%%%&&''''''=(;(b(e(g(b(D(K(''''&&%&(&+%*%$$""0!.!'!;=edKK| { 87!uy..qpABwu)(rt10)(]]KLTQdcߟߛ߁߅TQްޯށށfg^\prޢޡxv"`\!2166jkUQ Z_ HJ "~"##N$K$$$$$j$n$$$##,#-###""""""""""""""""""""N"L"""!!A!B!{ | [Z 9> Y X ;5 XVwyHFzyyyKL><]Zpr_[ 4/TXLG&(GJb[!" $/-][nrgchk}kiIJ##   C G j g z{ ^]}|ef%%tr[X6:UU~~ff " ! )*4567yx<@nmlpz~ JEsvrs/0yxtt gh_^gh&!z~XY!" Y[aa74\XED>@dbaaij dh:6RS>>BAjmSSikxx-+!pv83   F C 8 : 9 9 l m A A     z z   m k M M f d 9 < __OL8;YXjljo10wv:;ss8?,16<5-]eCDFDxvb^./0656~#"OOyz| {wIITUMKoq0 3 ? ; |hjik EB# vy[ZGF:9 0 /  x v ' % s t fa]_#$FFvv"!LQ+& LJlq |}jd/6 be AEwtYXgfPN>=vy]_ywX X  ~ x * #  ml,.~32>AFB48 46ut z}sp\_;9ko f `   25#$,+suyuv{}.,`c74UW rqMQ=8+160>C_ZGGrqpkjnusBBLL!A>JJ9; sm|KKQT"    3 1 g g NQ*-HJppfg<8TX|{tp87   M N _ ^ w z   :<ts;:OKa[~| ;=qrKNzGD|xUXMONJ:>47ke.2EESOcb`[>7olNO*,trfbEBz{>ARMb_KG\Z- / j i   / -  : ? ] V S Z 4 / 9 8 _ ^  E I C > zx45LN]ahhkgfd^`Y[PRFHfj \\]] PT5:CIDFWUZ[+,0+\5eXDi~^U2-.6~v&}C[5RH$PX7*$bkCgy <bspe>%hJ"  * D s   K M r x & 0 8   z f V f l W L   e Q &  7@h#<2%O6K3cV368RZW({qut*9 K-WIpl-Tv<\DKrBksIS*K:\IZ_W`8K5V:UkhZC"L$U - R 2 > +   | w H Q $wN?DfWPfIx`3 < n . < { ^ t /Wt&a6-\9Az+duRv6L+bG)}< oHd )n ! >1 5yv4 57\(iejJv&8q *T\%f2O&lo%8dzXu-1?+wV !;jho l6   L = g s d Y O J  3 ; '  m C " | f B ]i0QaSk -P[%bwg`^`ag[/PzY"{t2<}i;Wq"xLyp F-dtmDF.:N&#z_U}IiM(fs J$:0jsglID06^lS; r# Q| x (   T   g ^  VCX0Up5 #[(FBB[{%1/[|+|q??;; qW.^/N/7*Iy@D!-] KL_Tq9QmePl5W.B"4\Dn2RG\%2A2Rm2Rel1Q [ @  /  f 1   H ! /  Z   ) |   < g P Q . ^ y @ g~ ^ w > < QBd^G0@[<Bud3p&3Z,1.X0jI`A*_K|3lvH{;/puS0;,z6BYaQz-;PW=-I0Y&Jn>x2c Bln;  U H r j  R  v i & Z t" BYTTyC8}\+ R  @  K(  a F [ @ 1 q(   TO  PY #N|;;vp XBFXD|W%O;{A )Lt4z#l9bw,n^8 Mt``sM1JQjX jM;" ldz]Ir<A) 8 &  ;    ^  + r  H =   P $ T  <  V$=%O  D   ^ z9l[3KG~|D%]M-weAJTxtDM)8P3Q:~Z18%-%~-"p*s&8~vaBc+A 2DJVJ<!Y{{tHn -;,bRou_>FP!czl%0irisupuvv< ) B h3   % R u e P /  W x  R r 5 f$A85G[ms}KX7pQA!X9HH?V @ 3#p>vZ)"TiF$% $_IBlDJjQA*|(G7"3%9Vs> %+y=6    01 DG Ba ?i E o +    > : b J ~  x n f o % ;   7 = F  m_  w H @w{8&*CJGkF|n.PJ9@k'WDlP fb]C(}EOQU|O"-S7K_jsNCs1oo/fO L'E%3=i0n  N % . M ] _ 1  s  W j _ AK  > b  W  B    r7  4 n & n<H%WpIJQ% b JadP @]8Qg#w>dr9jipg UdiaBM{PjY =iU+)|I/e]`>h3~c*[[@A[dq@ #`s  W o  v  2  f j 1 , w< X_{N e8ja P9 d  A O  N P   NJ hU )h bua'bp{U"OWWJK?@k; ^KcRZu]iM,REXZdiztE{xeM=U52:4 85t Onk,>|T. b~ 6?!U+gR[;*;B?b6gu  Y t \  A S O v N ( Z   G k  / N j j ] 8 Q ; [/ #  S K Y Z  ] x ( B j  # vwt,Ycp)U kA)Y`bnG%}+|{2@#W^[eRhV~A CnMk5hwK>P:y8x`8&M{L4c Wv7_#_ 54IF1i@% \ q  ~   9 u  & < !W `~    L P  I : K ] ~ |  $G ):*os_NsS `6"[@\c)*  &cM[V?%("/rAuja;Qn[l_$suKVO|*>\jkZd?"VQ]eDCwDb ) < H > e | %  3 j \ L ~ " _ x f {k 4 L V X   2d 0 b F   S Lu3s[fJ3aIqn|R?*I1Eeh*;>o{)<YB tNd!X9rukR_G"^(?|ngLsB[}?(W j X=   F  j B e e I m  p ` H i  q   _> \ Z *   U f(GQZXtM/V^ujezS%m 0w#@]D0OPP) #$(~xnTc pktBcV !9]B^cOc;p c<Ane4j(?MGUF[7;   s   S[   X % V `  9    L  + p U  R ow,vpEvGCLggl(y.#A>)Oe'iz0 D.E;+i A|@ iL9n/] vV? I3df/x`Z_RRA+{]7" 5jvJ& X 1^ `a:T[py|tlgO?:K3vk fd\nPF/ 3G(LOL@D?q>=_??^;=R=dx: )e ' y   U      M   e J WND|hIQ6y>p"UP-AokkDqs|l ;`*y1jLf6WSl"HN5K L&,W}I/Agv jwtgqUFr?nZ*ypRTq?_%pj!\k9x\   \W   [ 8 ~  E  j e %  hK  y]   =A K_S?OX/&2O?UJ']_KpF+~M HR 6<>X ?mX ;o9V^"VIx"z7?G.By T r<NK6D  UH  # $ 7  v @ f s  s G Z { ( Y z k y v }  ~  p Ga <   ' 4 Tm Z) R )h S& _X5D}+r)\d-a#CqaNq$*w/3Xm3.'>KYWj"fP8}LAc*{WhQOH}nM8l,}jzRV_"Ct, J c r   x f |R SN 6W )n 5~ G k   m H - 5955?yIYY6#_xm %/PQOCB]U jaH<HgMskyQ5>4M07/Z#1  [25*In\|1Ve~&CnS ;o%c >RXMq < -"3zF1A ;S=$fAjS8Us sM{lUXc|~7bv$UUlH 0yNC.iq8*j/f6R l9g*'sZ9o> &TwkmSxG7Uo:{+p2h@2CSL<[Ep_H0oV]hA~K7HO f a /  l   %   !  + $ ! b 0    p dt rh \ X @ & ^oP{zX2,|"(|FpKI@S"Nw&Eb%WPxGHhqR)%`*e/K.\^0?`AjVB"nE#o. #ej"nOP<'i(b ; u  @ \   C C i # m   " H x { [ 7  I  n / 7  [m |G}XL#@7"cQ'jJp Lz,o%HX1hKl7S.I76B-am6X~8nJYW&<3,Sc\i?2!k"@FsP2kb&p_2cfn)pO *b   K V   6 e       y T +Q   4 e 1< TZ30,5oj dFR)E=VX"L`iz~O> P 0o 6"t%CzeH}Fn1T 8Z/_"r$>SQJ 3gI&~TiJc :k&|Do;Ai)^B[f _&_Rr-JsYDo2V"OAIL]ftvi\b=T5w:R}g]Yb|$sE} H6 S*290Kwe z \:!?FVFK^7 rFx04MMY}kwHz8t6JXQjF5+ oE*}^=a/ZJ@% ]yyF  )s"]|+d L s?>Gq{*#k1N4]+!R1%/BIBOpo #;oR>Cg{5bgT$qH0ozb&@~ft/wC[FX _0:B+71-8(q,pTK,=oDX}-=|/`1f4M Q<J\ 'M4+#U(Rr&l4FvsFVHEHzX>k PN)9Mjfv }K>6yI-}&6Vh_Z,pC_P @)uZ A Ka wy | q Z q; A gC-#")06;[6(.k=euwxSfp]QxPwx9"x64DAu'(3p0Yq9 v^N;3' /4LTi~ QC9wyJ>i2a0&j]TL,(lJ] 5x(O{2^?{Lw8wJ?lm *E\ec;e_YWJG91  a*e*ZZjDx/z2RBe(dU#P LjR*rM$q@dOB"lYEBCwKo\qsz 6Cf`$T\0n1b9U(A a[$[`-Rm//[Ie{(@Pa7xZ~#DhvdaES(?2  ,C_O"``YwMC6q;WZv!N1eq CZ<07C,a] ,Tt]NWl2=`9o2DP]"m7ZyU$I wDMKNN&Kfttt_vTH5' hL8%k6Y5j$cJ%m9;:uC4'nD|Z*d,b!t3o?l(q8pYC4S$ |)d9WOSdUk~;4dp1w -LT|(kM3uA+g@'_>J T q)Nk*+jKKA`rwvjU>^9 oW>2z,[)I118"?@91vQ*86T~%~9.r=J&s: lD5r#5&'-,N* plv,+`h6G913EQbu 8R!tAe}*` )/L^k=Jmu1n>mI FM i=tt -mXz2g <{ ,IUl}5NY[ [ KE*k2qQ)V!`(C q:H^B@dY?0l$}*m9D^d,8yo^P<* %;b})W$IAkgM3WJ~:T]$w#i^#\U2]G^9](:EAPYQz[V\XRRB?j/M- vBe F]'rL[F(p5g6Al^):]{J/]fOk-At]F1}uyw~9_/a-] J(v\8=elCHwr CV{MKzD9md!3HUpp9[z{ts0x>NYbhb\N=sLR.u$R| `>WQ[%=_$;`:qE;w:]' }xlXH( cM+uhKu>2Vcz6vUO=t\ XIE&mZ <0aJ`o ;e5Smut`hGP.D&y\q?HzoNS6EPd)y@~ ?pjCU:"{|Qs+ki\^UlZN[*_ bfceYjUPF7:!'  3Lq0g,KnY-@Zw*N`"*ZLuNy BK_~ 1R@k\~%34DBRQ_ZdX WG 8%yD yXa6,laDD |QvH!f<jBwgIW7%cHvb\RLyLnH`IVLEN=P.P(KJC>:3#1$3558BMHW]kk|39TRpx#-\PpW_3T#u[6&jLp.Q#rCd|',6::<1<A5X8b+m'mi[N8% ^?b;pN&kP*` 7R[ Qa9^.oU8cD&xS1{lVG2 '=Q`r .Na Ff*NrAsC)rmR9h? B%oJj&4K ]s/EYr*G$Z=tRdqy}{ujZGh/DY!xcG}&A v~>\5PwZy8A Zi$=w=t @ l@T0nZ]Udq#5EYg#r4>Q[h|$M2UqJ}9?\i8A`l  >N^u1h ?"r!! &#(-17;5G/Wezui~]oLX<7#a1q[FK1vN'iAyZ0hE&}Id]Q4<,wdVIu>Y<>7=GUo 60OKhg,R!q7Uo 3X}0Vt1Hb4wa5g/Pr9`"7S`pty|x#x!o!g^O E2  hX8oU4s? kUr=D1XArS7lG* dT9*ueRC2% -Ac{/Ms7_>Q>oX* F$_2Rj 3ZCe"2:FKVZkT0CFY0=z$i > M  )?^N_DXfY"jnv3c] Z l X <  v0<*k2ETh1{Z E 5d X    $ I ( n G } G O D ? w 1 b & L  ,     _ 0 zZ.Nb2BsK9![_&SN&lhnDN(b [(w'h -  l 5  S  O $ d  ?  W 1 q ]  ~ m M  | ~ FOf|vHj0 B  1 7  t T rc<b# RNb D-w>. [l1pedh;L- rm,f~;Hhq]3uRs58  V6 {wfb]RPHLH>KITUZlo 1+VGy_8`zE@&Oap T@c6Vv]qlnyk|hybpR]/Ag!7h t E N $  k X 4  z N )r&,r1]<aw s"z&7,f,H[qw DG{)0$BM> v67   9 !b D! j! ! ! ! ! w!_ Y!) %!  )6 -E*>EQ]fSO?3nNfI  @ _  P y|}wzufH=%pP1|skhmo*EZw6Oj#zw9ݘ?ݫ܁ ܗۄۛڜ&=ںQxٚDq1دv׵6ו׊׌׋aخEw&ؗYعzؠظ "4SCug٧٣88ڪڧ!zi^?<"O']@t!v!UTY{M /,<:rN%w>JEMLPG>* z W * z L z ;Ow0Z q.[SB yA$~?&\@WTv > ]!!!q!:"!"/"#"i#"#%# $i#P$#$#$"$%]$W%$%$%$% %&E%7&p%N&%r&%~&%&%& &&!&&/&&/&|&2&V&&;& &&%%%%~%O%?%%$$$M$6$##g#C#""V""!w!! h  R3a9^ zi:\#3qv$"NIjp+4C * I M " I&oD`ZBJfS e*g:Qu%EbHp=hH-RnFBz[jA_3d2z= Z;'`Dj ߊbߟ/b3ݩ޹ދW])2ܥݱ܀݉Wi3D*۶۹ܹ8`ݢA܊@ݛ9ޣމߦߦ?M;M/Q SjL&qH i-bfjO,r>m n*:d>z n h y 8 _A>PT}l4=f% y 6!~!5"_"#=###$$%%%%q%%%)&%I&&Y&&H&&(&%%%%%z%4%&%$$$|$?$4$#####y#a#I#7###""""y""L""!""!"!z"!m"!b"!Z"~!M"l!E"W!3"?!'"!! "!! ! ! t!P (oW^(h6n(15(XSYXWX#_zCq=e3h@sKoKb exn]xE7 pIw@RT@n VN<wQ"vF_.* $$JY7s,Sbu jF\npd S  ! - `  ! b e f 4 g - C c u   ! ( . , 0 / 4 4 : 8 > D D J > F < B 9 u 4 / | * " G S  %   A 3 # 2 o / \ ! B q 1 Xft [ H S r   v  ; S  > aR*`2kd8M}$<6P^q15i.Rm*KUC$o$|:E\8B5t0d @p&[E wjBW{gqvigt=p&?8CFGNG08*F^_2LU8^-(-AU7J* 5x8^ ~Dq< Fn8T[<}NB }5DrY.lO)g! : \ v |  a ;  ' C > * }Z.h][=pe O(Pu)#2R8@CFIOU`pCs>W#pTe*z.EQd8f]f&K~`RQR}p|_L~&`g4#jdYP MvSS(E;1. :jV|w2*L|T\O<$~J W|SQ - n GG% .~[H3)($-U}eY:X5 cjO8se U 5 g A  h  R$ x"pH(Ba zzQlB' w_;`B[zl3wPXZSuh<!l/leqt{wi^a.z+26R23eT5#E Tn  <    =   ^m G "  EG  \  KO  P | Z B b J) K RF:)e{g|d5QtC3O/l%?d Q* }DjNDR(B*m#Zdd=>]h`7Q 4 2  K -  N  \ L H,-|* u R q m  C  k   j %x $ yYhv<jwqZ-W?~g~-{{z?zxk5Caq+ b95 1*xl8+@c1*{+]O*XT!(hK6VC:V?!=i,B1MBDW%CLq-+Zq=Yjw,Vb{tV*vQ ,>s|`"V/3nda%PtKR&i*Sgue(  >@y`?D}EWd26[|=\>CdQqY ^jcL7U8j9   7 2  K   4V J f {R  | 7 wDLkI:~k]wL   x  C   ;    "vxHW]rzfE= m6`frE#F   q Sg d h F` K s*  F ` * 3 A ~ D  j  x T gI "* . 6 _ F   ZC Y   m 6   M } G R  Y  > o.%pj>o$`!J[K}uGn,ip =2 rD( i vo>?Fxl-`4i0c*g0v Z2p.VZ2vrB*.V{ Hi rJO]r<{-D6Z V)^<7z4CMKr;@'XX@  =  aD 2  ^ 9. * 6  5   m V Z  f" =t]MG1?O+%*B7cEazw]g;B S ]2_'k.g"/T2jT~xkDg"NE}okX>R?{PSAp '  P ( b U   J  /_l<|H^E;}Bz5NBQ_|Q{K#^mkS Y["k""!lU!! l= s\u5Mr\   ]  4m?L>4gWy   }  $A_~f'Y49G; >dhU?zX9G#uV:wQ1 28%?Shw[!1ߠsNF/^4&=܄zeTgܩfm~~jWܡ @ݫ2ݠMz~.{[ m6i'zq4kI4) q V)&e. p!!t  wkGN K Q % p   SY 1  i  D  # JY  \ )p l   !` o F  Rh2&m5%pt?fU(V   &] T, k ~ n   `  0 @| P( V V YF Z ONNG8DjWLNGsEOE?<<M(ivwx]LR9o/tX TLm?:'z;3qk1 P  r H  "a t &$I/0& `e~rL4MK,ic+kN" s9Mni8V4*p]SB Rh k8H1o%ߜh7G݆CP ݩytXA8  :3[O܁ܯ%Q)|5mߜ}J\*gf-aJEAg;yfE"-pS zT-bkB9Vw]D,D&I:, P @~   9D#~!+, = B 8* !~   4 Z  ] J! "- "} # ^$+ % % A&Q&{'#(u()b)y)*is**+G+t++]+, 6,UH,F,L,M,_,/Q,OE, ,++i+3#+I*_i**)yg)Q)1(<('O'x&?l&%]%x$Bi$#P#"!N! r`sRaD"]{`5 J=nI*?sv X  J m - x  & y 0n-lJkJN a=bl !Tu;aufwG x0 J={%ܻ}ZڡmC Xe֭4g&ԛW$Ӎ7~lUPzFwvGϱ 0hΦhMaJυ}C߰ Lޥު5X҉߽:hԞ3Ս ~mupEcܙ_L!Cn. Uk_a`hc]cm"^-/Ocyz "YrRY3yfE 6C 8 , 8:I?}--RswmbVA  ! " " #q $M %(h&1' (( )*.+;d,L?-L .K.</6c01123"34Xr44V 4 47!4!4 "j4q"?4" 4)#3~#X3#2$v2n$ 2$1%.1U%0%]0%/&y/+&.W&p.&-&:-&,'+''Q+''*'*&x)&(&I(e&' &&%&%H%?%d$${#$"$!# ?#"t""!#! 1 4~fA= Kf 0  B'@YcfON< * ; LmfM*5~q1S .f%72ENd yU֊դ wLC{YҰ߄*щݍZX2jπBϲЖ0o[@ڗ 4ѮْшjaaWGZKMEٹ-,٥֮جֆ8oTnO]ضpPڌۼحn**e޽߄ڮڏcwp][_eYsyNߔߛe g(#e$)$)K%*%J*S&{*&*I'*'*;(*(f*@)&*))6*)*U)*)<+(+w(+(+'+I'+&+S&+%+;%+$Q+&$+#*"o*?"*!) )6 (%('^7'&KC&%!j%%$U<$#0Z#"H"u!!e U|&[  W t    2%N/jBQ`g i y w d VMT^la:( /]g/<^ 5Ud/zmY^R:&.x)Y1K^eݑޓ:Fܯ܆m7<ܮ.۬Dۀ׸%ׯ֨ۃ֥/֩ՠۊաFկ ۻܯDܬvԢ#^/ݬeՙ]ݰKI׆ޝJHߜ؇0xkٿ1[ڨaRT;܂.Sݡ`pRAv5>7F`u#@z]i[=&B{`WwC  N 9  o   cF & ] i Q&  C RPt 1N;dEY j!!nn"#V#$!s$$4%9 % % 7& !{&O!&!&!!'!G'!W'!j'!t'!x'!~'!s'!l'!W'!:'w!'N!& !& &p F&" &%h%6%$$6\$($##N#C#"p[""!#(! E' i r!P+y88WSJa4~$  ]^   23 R g e f g ] XMM|/#T|.s 7:Wl "1.wZ(%9P*tt#qW2@Dl#b^?0gG\pMD^ߴtM*!)8V4Qu#W>P\Y QUO[` q{#+E`Gv eX,,}*2YRY' w:U6i?48Om;~V7L/CTe= WYsc7`Q2  [r 2H    t\ /)   <q , y R6HV?z ,z8 .rS00GHBX"AWhs`Z9 pQ2Kt$`vD%"f=yH#@>K%tk:\!YmU=+7HtNy!} > /  & g e  U S  2  mot\ RIC6=}G[BSR0YR`,w6#qmk"Sm3~`TX3gwNA8 7ukJ7{:XG}h`RZ(hsq X*7U{^:  T0cH&/Cmf^?>@@ND%AKa7h v*dOvftM4Y8}Q1 mAhZYF"cA  ] d n  o s 0 k t W>  E)1[@n.Tcc;Mr*PF! f-FkX9 i?H]cq peYN:1#xsKg0XBk>r9L=6WS+< O,z! Z  F   +@ AV o  i g5L_~7;{^Kk QNy Q}Wk.N+il9@MI`)r%>;Zk-'p%_:^B&b;`< Z,=_ %9] |P DPvUY0+tz !Uu6;TW\U;I9aTG)cz.+Ep(wJqrw^a&-c pF0$j=I}@HJ??3|"Qg^c_c{wA7 oOja 6DVt l.xU #M{3Ei^?K1$#)BNo%D lNYEvLYpHV3)2&9(1h$w 034L/VP^4qb"p7V Ohj>2lSTCo721  + \ ) & )  ^ * ; - " #  z | Z [ .,O@B\Q0B6Q`TRK>0":=IHFT?16)^-~T.e?S.c3o@ Qw?R ;Q ) S  r \ C   O  E >  > J   z ?'  Hl  ;-?Z=%}Fm9r]e jW=65i? bL?PUq '?W{x-(YBY1}Y-xX(D;^q6Ap"YRe: "!Jbn=NJ3\aF9d_?%Y$q@e Qf3m.'Z)xz"}F> cY&ZHczsT#H5MRgCu" v M g ^ I N    / } 3 ~ M  X  5 m f  n e  b C NT7OS3d9k6V!h:Phy}o WB.#=DQNSzKLC7# HyZ-(8g/,o c*?i} 7 (  WH  _ O g $ a K T m ;  & ~MT}. C{rW:6ObqkOj*RX("ZU-B l E$5 ^.t+hTm8"Gj/GnO=|&^O90# !.8Kb ~%&Y1ETm=I?o(oXf!p\f7 8sx?(\g-4jX11}+ f#\=[Udrx "G:yWzMB8u} "D` } <  c (  pw !  k/   @  bGlTU= xYY*_.W~Cm*"F*o9?Scy.` ,T{R.qZCT$Av>D}44e>::gc|, }j  8 Q l E o  <Z } /  bOWJ@--(3DO ^B*%8{HmRZTU_^rx+5\Uy5},*Y5"[bw(tK1o 5lEtuox|l]LD<G:Oxi{]6}brQ6JGs>O+( qrHD oc5EX,o"y+ A#}4lIvg72vqfd ]WTMC1  n (  ]7  R % ^F  ^]  Pf  =`Lw(0Be/u@l&U  34ZO_z 'AVbkpiiYR<.fBqIw7{?z:C\vE#n >>ae \   :  g !  " 9 & W 0q?Rc8oQunq i[)A+"s Vh;G#% w{pm j"lKp|g X&-7=}([4'@N&zdRPT_w-De8l -!Ldw65n$u2m"~{O,ym t#y^(|_, `;gWD=87B>aL9`u*x!!h'5Yjk/kZ4O/8 x   a c  // ~  V N  q+ x ! y Y  f E  I - }  Jf.D`SAg>i(>:_<zIBG=;+xTU0-Th6L4p)SHO  q 4$  n U  : s b +  9 F > q  5=~8]F?RiUwp%]N;dojq4pktRLY@_Yfsr$I;{k QH?AQ\|c<3x\;' #0JXhvyvy^tJj1OA|wPm/^ [QXTbn}:f 5)^qn.p/1fy9 WEX4H^wud4WOQL}MDA9h:95 y0N4!87N6 d75Y627}5  [4  % O   c c  {5=  K c  K b  + oC    V3 `    >:Wev~ /GVf v    !<uUnsaRD*p9oe:O_,~.b 4 # C . U s  Z I X } T K>=`+}g"F,(;U|xsesUzS.HL8jy!/kk iWoX$}O*M%~T-h8{O:]P0"}ri[WNB:10) !" $)1:iJKU4hkhhfen:n~)_3\:6fYNvF E)>7)/xLO~tF6\h4\m*I[l8]`g*J,qIxZY:+ nD'vzru3tXwz{+`$T :;Oxt9#^^qWq~@Y"IP zHs%<^HNJ4KSU0@*zh+g<fIdR  dX  aR  ^>  T $ B  m&  : b  R.   KG *^k3Pt14kh=$rDq9Ss !  wW.sD |`\EVV8O0jEv&  7  4    d w  ,V /Z!3 w{Kl`XY/\ gyxY?!-@&e/F9jX&pV~.h-?Hso ABy,V}-]Q^'t> 1U*\/U{@z)a>y&C]E9w U:3^d.r?8|/ QDU n,p`7JYf~pw z})5AI Q}  f  z s  Q x  ^  >0:@ b[!N$EgTx$1K)R@dUlaqnysxu}uyoxhu`lPhBZ-TG>7* o=xgJ+#I\2a"W#h&x> f  E  V  H A o ) H - 0 q X  k1mI^L?C4[#s`aYEc{XQt$"5Ls124dADg+?k\.1qH1vjfaadjq~'?`~ .KC{c|G{Ew)>F\Nj{#T0/Rmz.x0fk]n(XS*/qn{!{qAmos_u?v%kbN(2DwT`LH- @  x 4 K   n|  7 Z  =X   *P q  K ; y <   K {d   %_?Yz*SvsTA#_|=rjcXO@S4*a(ekA) L z : W  m % O . 5 ~ d &  ~` 0x]`~G1<"kH'BAn KNa7Wt.R]&^L'{[M,2d/I9CL]cz&X@N`LQ[ga6/  8 o-Sx\%Afd \ Av :H_Nw1qQ|Y ;k>("C&o;`SSg;p}r:m\E<5m#1y)X%Q,sg j w         nt  *D |  M N  + } J Z2cTCj;i"5'G-Z)h%w}tq\}SY5&([!~_hE)&^>{_GW- g    B i E !_  o U   g w    ;Q  [b  c[ `?S|NVC16(t kn{zyJ#Dnd5Mj =P>AALYl$x3d"j+{JwCqoK4n'qH%Cki7 (Is 4D^lJOh8u [ZnV5slCD rm;S\8r)z`6F<* C{bBIE+ M| `hE%-V$ti[N=  7 z # :   @ s U 1 x 4  Kn  4  Z FY(pHMu#X~ #" Y}3` ?"P pRQ=h*tsT8-  x 2 [ $ d  x D )  { ! I l v 3 K  Q * ^a:Ru=Ky"EZo +2cVI/n)\M+Bh`3wd 'KG{u`haC_Wq&+MpFHhzEQ%-}y!?h1QBkp :s)Ou^4oVZ]%(ev FQ/ExOnpr3"~`7*bT1vBX  / Z h 8 \ 3 U ' O7 k    ,K W   5eA]t /9FKSWXx_u[b]]WFT6K"E6- vP.{Y-{P  e , o / m ! u 2 D a  V | ) Y   ( [jN7D[s/{-mJnj{7+kEMf!@IJ~p>+vc.ZHuj.x?-[(|-2>h,y8KKn9S+f9 {duEZ5>)   +(=8Z[rv8)gNbCn6$`+GVt8^*!RW} LF!kC a,k/"{21/Q29W18 sFJZyY7#c^/%L@HA- Sy   H ) \  H   F w * i > f @ w * Z I u   0 )X N{ t     2 (H 5_ Iu V ` g c b W U D > / !    u e S = V) 8    z Cw X 3 N  y e - 8 2 I   % k d   w> vuRr3s$-.}$HoYMp$#R3;EmO3^c{72pb$\b%Y wr>&Vf,Y$i3zHW5fQ3 mF+ tZ<" '|6kChRY\^qS|ZT_^mo/Nq 44`Nk-mH8r|^ RzJ@>rGXX&yo> []R#_W%2b6 mvZCD`" XR 6^y Y E  B  b  O A   &> n   H  Y  R  I {; p    -& KJ dw x     G _        (.:> C?83 k>    ~ F\ 3  G  X t * ( @ [ , P  q J D %  v . R  NuI5*SWK{ # O1qB X,yNvGI(z9&}C3_7:5=U#Y njl/~3S]Bf0]&rF!m9 jwNJ1wZ:! (Hf 9EPygX @KeMgEY`T' ^bz=?!  z~Oa';   lg  7 s  P T  R E @ w  X - X | K    + LS i    "Ea~xuvoukelcfViRhEg>k.iihc]TC{4O!! r 0 d / U Q B 0 ^  h P  * . Tc{@=g)o,]\*XhPRP8?'2py]VI7@y$'pK3W| eD|Y2=G#R{QmK f  0 Nw   7= ~w   X  J { ( j  ( ; G c Y p   * : I U b o {    * . 5 < : @ A B C = 5 ,   ~ R ) b G  U  j S 3  > i <  w /  [ a/  8yFcq)Knw",EZY Ee \_ ]]IS2Ho NMUE tE'iPX>t\- py7D{Fw K\,]'['vHuH$Y>"y _G2 "9Viz '8AVc)Ji )O  d  h.x!tr4dr6ao+bls&,rT8M^ "u)h,Mp6h!{A0D?V =q)z@LFUXm9b&a+a"^^("oi=<Yq9O+ yfnZ\JL@C97/5-/'1$0%8%<(G*R1a7s>KQ^js'Ge.9\V}#a*ZZ/`3,P-6mn D>vMS UO1$v&h=sg,7A yYF  im %Uz$ea8x8c@   ? |3 p   I G |   >  n I n  4  X C y g  0 < P Y g v z     f T 3  x f D / ^ : n U J - % X  O ! ^  H<XPT[EX+lT LF)tk,IS1q&v6`LPiFJG]{?C{@ONQp'<e^@.# sFm+O8|]?!~m_OI=:{5r5^5[DI8R0^,p"{ #($3B:cIVg{/^ 5I_\%^CP7 $Pz iJgQ b hX0R}EFAd?@I*xKbLOB:4$(mq ZSC& c  ( p) c  A  I  = |  C  o I { /  N H m d   * 6 E L $U V #Y X S Q F A 2 )   h J %  [ q * U B "  d , u q E 4  s 2 h A  k-]_1$P`+6h(NI[b^r,O>>P,scgr ^/cFtVz*d/<g%$LVtG*ZI#^Y7'mp8N|7(r]*d3c$d>z0~-mj9x6 jk\6Rf!<z!hk/@hG#Jj.i ' AI vl    R 7 e  0 n ! H n 0 W  +  D / W M e e v { | v p ` Y M I F @ : 3 '  w b H 0 Z . u Q R   B O  `ox9/PUx<j+~<x8Kw5K`t{),DGRd[A[n%\Q\ mg+uH-_Z?c&{Gd; ]7qJ&q_|EY5A"veS@1#  %7Rj )'7?J\[ts%4GY`y>Alp #HQGOf-mE'1m7m{ NS7*r!l8Q9o0s$QH0Kc@2wbF4so#LMs*c-Vv7i 0 )X T}      > @ e ^  *  J ! ^ 2 w E V b r z  f R 8 ~  u i W I 0 k  B  e _ / <   a0kCOj4o 6NP|gG0]W$c~E \YS\KNNt#YB"Xj-nv8?~Cm+Wm8vAkC[5jK.~^?%gJ0taI4 s\QD76.21;p=qPlZnnnvzCl$Ca 6 / # x ! u  d  ^  M  D 3 "        j M 0  zg`O69 Qb9D j.M {A7K] zR,F^(q)z>a e.g4W"Yu5s?G \!V}Iy>qA ~M"o:[p/8 srKD#{Zh:< "}h]GA1)"  +$<2\=oM[n /Rz2P=hgI@{g :oN~8oOL+a8 w=u,uD{9OaL1m6%`-s$_^(?g)Ph! _`"eE~!YDn = i   * S lE i     ' H f ! . 8 A J ! J ; V X O v X T T T Q P  K M " A # F 5 7 / 5 ; ( ,  .     l M f ) J  (  V . kH-k>oc2=NvK[ WWyKCv0b)\J9nf5 Z^.Z[$m3RT"q4],Y"wxL=#Yw&HT'oAyR.l Fu]iFM,0|{{/z@}]v+Hi(H)k;Rb}Ln#B%m\4AmpX@~U0i`,q/s=JS!m,rG9#h;~9z*i:| E%vfJ7~iG0r]*SCp ;; a`      # @7 `Q yd z       ( 4 C M W a g o s { |  s ] J 0 }  { o k ] R > m 1 L  '   eo4O-PsL"LtJwE ^Y)VM 7`]g%|2m+`{<[]k(3^]%&mb<+hO9"r^E'bY24lAiE#b:wn`vS`QJC:D&<: 9 799>?HKSZe l(|3?JVaky.?_t1/VGlTrAj4U{!YG:t|81yj?7rK[W QZI~U4mCW.i= uCs!]=m4l"Ov-V<_<X{&?_z3Xr % ; 2N Me kt                w c R ; $  n_C.wX1 yo\H@ &yG!kK+sJ|R/e8cl;2GPJ{ BH VF ^!Vo0n=Ln8~ QqHk;Y$sR-kH\pCN)iGn*[K?6(({eJ4"-(69CLWao{ 1,D;cQtcx *Pq=\}5a 6U;h9`O{4bEw IUv7&tMyU3[5l "@Irv  2Alx&cNuV$MyD~:E_s?'_?f} "BXr+<O]kx $$(&#%   qdPC*|nWyKU0;$ ^:}cF*j>|W-Q!QY%\U%PAhg06FX'f3P!f4S!X]3( p=a>[6wW6c;cJeH' y[:udQ'"m \>-m[^=F, vPr)W9k>x^3d:Xr7@cc)6 EyFi/w; Y,_+a'U#l4g <}V(iG_:b>u_>p-T 4x^oPb6K(<,%=Sl %8E Y2kG{o1X.zNfDl+PoH{+ O6xm2Len6)f\8kN8iH|0cDm#V+OBm?d!Rx 4V:l]| ("L6bJ]mz3Qm ".CH]\jlqrqmhaVL>/~ lUB(hQ{<U!3 mKyXCrOb?wR#l{DO!lhCC ]2_< mA `7{DEM^1f=V3tJ&nK#|Y;wW2x]K4hN4~hx]sIj?e-`$\X VWRZVa_koz  (8EWbw6Og 1 E=Yak} -Tw#:p^+[%N|R;iEs2b1`%T0 a7`+S2{W~= b4Ou8R x(Aaw;Mmy(@Tp #08+BBHLOeTjW[]Y\QRIA8.# ~mfmUYH?8*& bI|!j L6{\5nP7[5shF@ $sCY9`4_,~P#V*kB_6_3yW/ iC uO*_G&xS5 qcmEP41 v[Hv1jZ MB4, }z tun!q-m3qBpMuXwj{s#5Nc+FXp1Qw $Ab y3V1%UKnwIEog >.l_ ?6gdECoj&TCi-V5]<a=~^*F:]Spq- FV+p3~EL]fr}(.5;>CBGBFB?>76.*#ujX H5"}^lCQ$>$ S6 mR7oLzZZA6! nIe"Bc>d?ck8DxkOC'wXS9- sPl,T 2lU7$jV:' mc{OjDJ49% }lYO;5#  +=M.b7sJUhw9Rp0Ibx=Rx%I[)|Dc %FdDa4VA|f2:[e~=4bXz- U*xRn,Jn9Vz  @Y/sPk:/T=sJYep|+<P]lz}{ni_VOB>r/l)PH. z_C( lZG0rW1mRg6@_j?H*{[w-Q+xUd.E|bV@3 _\=?aoHU&8r^qDR5B)|n^OzBp1r(cia_`[`\b_ffmmvu 3 Gc%t0AS`w:Yt3M)iJf4T4uRm.L"lDa="[Dye58PUs}$2;Rbn} '$@A]Zyq+A0\Fo]n 0FWn |&2:CJQV_`kh-q3p>tHuJwVtRx[rVrYmWgQbPWFQCC981,%~kV{=e,S>*mV;!z^S4) w[>oZ>l"J ,tVs4W2z[k=I#qfOH,) ^nIM . wiUR9;%vXF+|m_TD=+%xulhb]\RRIKFBB<@:>==CAKKS Z%_7i ! -@S%b&z56EKZ`t x&;Um #5 P7gO}k~1O+iF`v%@W5vTp?5OSik #(;?NUmh|*B"T7tI\o~$5CV` q|+09<DFKNOSRRTNQKHE>:4)'wp\QB2&qgTF6#uaI4|]M3dL0p^Au*ZB&rYxJ^,E .wx_cCM,8" v}_gMM:3(pYA.{tg^UJC:0+|"tia Y QJE=<34,.')$'"%#&$+&2,'8.6:AAAOMUNeZq_zhqy(EUn!9BXfy%;Th ,>P0aIu_u)6:LXhj $,?>ZYvn.G$_=uSh~  1FW%j3}@NZep|+2DIX_isz}rgZL>3ymgWK@-y#hUD.saG5{l R=' vaC3{aH.tdJ8f!S 6# {mQkDX)A+{eN|:r"Z P=/ ufVE7' ~th_QN>=4,-!v&ph"_]!R"T$L)L+I4H3IBJAINOSN\UdWn\uaglry 0=N\j}2:MZmx"/KVr#=Kh x+;^m&?T%gAYt(>/QCjW{o&?T*e=L^m~,:M]l|!+9BLVafrv *.7>@LKUU\\ccfjfni|lylnkmj_j_hOeMf?_7_-[ UR KE?6.%qaN;(q_Q=u,_K8!{cPy:j$P>'scOo<\)F5vfKu;f"OA+{kUA1q eVH;-$yn`VJ?7,"  67LRen'7>RXlx,:N^q#8K^v0>Zf,=Ud'~>Sf}-C Wh0CVix#1DVb!y/>KYgr $0EUdw &+- 56;"> =/D*@5E4B8B;B<?>>?:?5?5@)>,<>78 5..(# tg]LC4) idPD5%wjOE*{jYK5v*\I2|iRw@`'M:$gX>-xm[OA2& i`I=* ypbYLC5/y"ohZ VKB=2-(  ,3HU_nz-:MXoy "2EWj~!3G[n .@O`n+EQhz '0EYe%~4AS`p}9EZl{#0?GW`mx(5COZim}wiaUH?0' vk]UF;+"|n^K>+ zgYJ7,s dQB0!udQD,"z gXF4%~n]H:$ndRG7+pdSH:,! zyomidbaY^xUqYkTdW^TXWUVLYLYD\B^>]HUclz5=P]my $-4 ;C G$P2Q4\A]DaMgSgYm`ogojrpsvqyxoyoupppjldf_^WSOGF;7/'%|wola\RMA</) }ohUP<4"}u`WE8' ~k\K;, }h\K<- xk^PE5,wjZL;0~tk\WGBp4d/U K:1 "  *-9=LO\bnw #*6AMXdqy %2BO_k|*7FPck '3DM^ew*<N^q!,9CPY+h4pD}N\fr}  &,-!8 6#=!A#C"H!K N!PVSZYZ`Y `\\]YZWRTKMDD9;.,${oh\SI@3,wnaWH>1$ vh]NC5)wgZI;-uj]PH6{2wbY H<- }voescqX^V\KNKF?BA17-7#20+ +$& "(&./68AAKNU[agnw{#*3:EMZ_pt!58HP]hu *6DP\hu !+7CNXbm"w+8GR_py (-'87=;GOLOUa\gbrkzoy{   zxpmea[WOJC;6-&} wkfYTIC71# so^ZKB7,#s e]HE0+|woc_SMuBl;`3P)J":.& tqhc]VSxLxHsDp?m@HLRZ]joz (+:4/( yund_VOLA>6,-umb WPD:3% {to{eubiZeXWPWKHEF>9;52)0%'$  ,(54;AGKSV^b kpy++>JM\`ls{&%*(-96;6J@NA[HaIoPqQZYbclnvy~!,+76=B@MEQNRVWZ_^fejmnrrttvvuxtutrrpmmhibd^\YUROIEA:91-(" |wuplha^XSOsKrEaC]:Q8I1>,6(*% vne]RPAA40&#{ yssklfea][VVNPIMDI?F;@9<87939266160535393=5=;;?>?D@GHKOPTWZ[b^ibkjn qvw"~+~/9>FNS[bmr~ &.8;H JUWccqo}"{&++2186<;A?HBMJMRNXRZVZ]XcZc``dafdfffih ji ji mi nk nn onolmnlmnjqipjpkomlnllmkkjkiiieideead_a_a[`Y[YUYSuUyRkQkRbQ[MWORKHLJJ=I=H5F1H*C*G!AEA@D < E:A<;:654.2+.*,'+$("$#!}zvpqg l ` d]\YXSUOQJNFHH@K;JA<@8>8;:8;7<::>9>=?@BBEDIGLLOQTTYY\\b`hekoovw{ "',059:C@JJQTZ\cekmrs{z   !##&%*&/*0.2053595=9?@{B|DwGuHpMoKiOjNcScR]SZTUTVVLWRUGZHUF\@W?[:Z6\6]/_/\(a'^!a a`aac` e _db_c\c[^ZYZXVXSUTPROMNHIFDCA>?;98706)1))&$"   " "%$)&-*0/043797<:;>>?A?BABBCCE D EF FIHJJIKJLJLK"M L'L#J+N(G-O,I-L/L.J1J1L0H5J2G5G8G6E=G9C?F;C@E>B?GB?@IC?DECAFAFAH@G=I@I;I>L8H<L6K;L6L9L5M7O5L3P3M2O/O2N+N1O)M/Q'J+T&G)S%I$O$L K$LK"KJJIHIDHAGADBAB? @ >? < > 9= 7;775604-/,-**('&$"$!     # # # (!''%+% ,&!- )",$,#,&-&.&.*/)1*.,3,/.3/0.51/06312436535736864:8679699979:987::96<897<7;7;8=5;7<6;3=790>591=/83;,72:+6/8+4+6+5(4(6*0"6)/"4!.$1/!/.,-+,'+$)$&"#! !          " # '" )$*(**+*.)1(2+0/.1-3,2.1/1/2/3 04 1323314.6.3/001.1/0./.,0*.-+,,*+)*&*%&&####                "$$"#!#$!$##$$#%$#&#%# %% # & #&% # &$ %#%"&%!!#"                                                                                                                                                                                                                                                        LISTVINFOIART JfreegmanICRD2014INAMSound4ISFTLMMS (libsndfile-1.0.25)id3 ID3 TALBToxic sound collectionTXXX"SoftwareLMMS (libsndfile-1.0.25)TPE1 JfreegmanTDRC2014TRCK5TIT2 Toxic-Sound4toxic-0.11.3/sounds/ToxicContactOnline.wav000066400000000000000000002510361416141666600205450ustar00rootroot00000000000000RIFFRWAVEfmt DdataPFFFGKIttjkCBkm//NI\V]]WYJJ44gb?<   z t E ?  _ `    , ) 0 0 ; = A > A E F B C G A = < @ 6 3 - / $ #   ||POSWHO"[Z/0npOOBGXY^a\_     6 8 n m 7574>>69p n 2 5 ywdd_aXWdchiJJWZHIfd]c#0FBFIHH53,.DK/0nlCEdfww~|}|y|{..]^SS()FD!HL!#ecgiljuv55[]WZ16Y_NN BBhi99 !ljHH#&7 < - % ? 7 !  z z SRZ\VS9< q u : 7  5 6  UQRVXWPR/1|}=?JH; 4 !!h!l!!!""T"R"""""""$#&#k#j###l$n$$$B%C%%%%%"&$&G&E&f&h&g&e&d&f&<&:&&&%%q%s%%%$$##L#K#""!!   33|zQQpn z{\[=?}KNut}w{~[]Y[1.nj}}13YVy|&#RV߃߻޿/.lpܯ۫;;ڍٌ20؋׌LMֵյ! ՗ԗXTVS79+,+()+43EF\]҄҂ҫҬ((xx>?Բ԰:=suױرؙٚكځsvqonnuvނ~ߏ;:kmYY/0'"tx'(ijJI)'lm8 8 t v % PS64EH]Yw} NI !! ##>$?$_%_%l&l&}'}'u(u(p)p)S*T*7+6+,,,,--1.0...]/Y///M0L00011O1K1v1w11111z11J1B11 10080<0//(/(/..--'-$-`,e,++****7)7)l(j(''&&&&^%V%$$##Q#R#""!!D!C! 9;kk )&3/(%no65CB""2321 5 6  DEPQ[YOQ@?;9ux+*;='+cg%! UQPN{zgeDI,(zvqs&'ނއ73ݜݜNMݹܺtt*)ۡ۟_a کڥjmډل ً؁ؗד11֕֒]aPKKPtpְ֭׍׊"'جٱ٨ڥڢۢ"[XroGJ2/FG po)),,nn | ~ N M RP  yx13if'*{{OQ!TT ]!\!""""~##2$1$$$%% &&&&Q'O'''k(k(((k)l)))]*Z***-+*+++++",",[,[,,,,,,,,,,,,,,,,,~,,;,7,++++:+:+**J*J*))&)&)((''&& & &%% $$""!! kh,0KFYZ]Z/2   + & HMwrYT--xxab>@|y%)NItx HJ "3/9=MIPTkgtx"fl"&#$jl&%ondedcjkzx<=yz XX ruEI.0twUVE@<9>?LNdhJHzzMJ&&db<Rjm)3A=J=G[ 6 _ B M ; ) [ R 0x5N  ~ogDP/B)R6^HkTo]~l+>\n2=dp/^D~j 11CGUP`^i]h`k[bO\GJ1>"# ?@v}  ?7VO k`   "  ; 5 R F p i gevx-3FNemBAim./`d06orUYCD?>IF`eFKHG0665WU]LzL"\;vKL9DPg !\|2ahJjF`D__pu3:}CG#tC6Om:MbC^%Kw 9AUW|x6xI1fyA@P-Hp uc@2!B0sQs'(FKlfL5zMi ?gU XE&J, $0H4n4:6+!& Ml.Qd#qg T 9 s  1 F e !  m ] B  T y  W ' R : n  ;1j]'C#iGn #C=YV`tcrgumyosv}vszv|^G8m!RC'w]c9/uu8G | x ? 6 _ {  N { = d R ~&4o&zL8#'1&+M-Vn L<f ; vc7tRJE <\!w./PkY Gi: *W j2J*?nO?X y/7IIN8Fl*O["i rEyI'L@FP[eZ H PQ a[6[;(zONm3jI *COf{9QiomRnCuQu|}seXE1aAsN]U'd8w[ksC i,}FP 1   C  rw  z $  ;  ] + ` 0 C 57|")4 x,<6[A jW,`=/f|?qI<},eY~D;z5> b+^/Nd<-v&)[34 S;&/(;Db|ߺJߔ\^_dmYu -iX:BoH_ Fs->4:m6cq48N(Jt)TQs?\9|1Eu K R { c C}%6{acFR[i!/meSUJ Rz/^sRl&Fu4z(sL3l:!fvDT)?Ng)j0xU^0*?wbS, :  f<  H ] W o | o# d l @  c h  h \K4/ E_qtg VEt) k@jv.uV%Ui~|Kg6K(;  !( wQ[EVq 0RTumd{ X}A. t,OtYpF9D3,',ChkLCF DTQGCJH]~f&0Rc^NFe/= y9iP.=`\6k&Dj'[Gj?zz,h 8GTtu9n @ R  4  T i I& NuqY  C  ' >  {u  r );%3*#pl?gT!-!4?M<5z$C,',#M  ]M (f^9 IX<E#4Si6BL}j . t 3 r 0 N x ^ - .   L  _ v )  s &;  e  X ) ?0YN1*5#Zp[NAVY4m/uH P&imFSDI#f/Ej@`{ImeV TM |Lb5&'/5GfL8Ybqzny[7T *VPCo pE9ywFASl. p!8wr hSC[-+BTrH`El0I('4gY q\BX Y h h a eQH? g t :  - P RnafF+tF&"=rW[T62h >yJ);EhUk#b9.MoVP- Go{(t[1jvwBj9pBr Z >   {i  U 6 &   m c 4  W\zWc|'zo7TUPHG=Q\mKLNO5tgVg@f v_gLBC3X0+nl3t,W7uB %:6POx|'\LD,gLo)8>@ JT$_%u0;Ph 9s6vJFY')Q{#2gV@{c$X}fj+&xcW J7+${$^"I<,%Kn _H>&9u7y1\ >t>6 tTK,{k =L%M$jfrR|m7*Y&v45'X keV3ULk[96z F};b<1(Ke*CUv84d$  r : v 2  H > mTH \C~</g?V-Krm]I 7 <X p -   f   Q4 4 E %G nQ \ z % j # Y  ) M eK    > y     u Y  z^ 2 v  ` g 1'   UO   h  _ z -' mc%+  w Mt , k  T  W0 ,X{%E*OV*VR-:`A:u2I{ n*i?l@2T {wg Rj BF9VzD<D bF     L  dm oU!,"v##q$P%W&&;Y''t((_)N)*g*~**<+4+E+?J+?+3+-+n***/w*q`*L*5*j*))&)LS)r)((^(('`''e&/+&%/%L$$u#G"("p!9 P3N\A!&6q?DaJ?K=&1-   { b  9  f  5 _0D Gp)-|m)'xxOI4.9]=0ZQ 2msO]Iܿ=۶/Oڭ(<ٚ8ؖ-ן,8\x.զKԕQD7ԘHӺy7 "Ԥ([5?Oԟbao"Ԩԋk(8D f#־ֽj%0_ۑ|Aߋ?RN tQU8:c ~q*Urc5P?"#!7]5V(  ; Z | lRBNKOQU\XWC4q5 > d !0 k" +# #~$L%%e&'q':>((^)u).Z**&+%++Z ,9,yi,,,,,,M,,_,QC,,+1+++E R+ '+!*{!*!{*N"=*"*#)|#~)#4)-$(v$($'($'.%@'\%&}%-&%%%$%I$%#x%"Z%#")%b!$ $m$$#N#."Qx"r"! m 42oeY/0 W w  e5 |Zp_K A x: K/ " k 1 (/BRizxy(HqX 46sB(s޽ YݤEۖ(:ZڎScٴ( ؈pbHm1؈ خ5MMkثc%زف2^Z2߆ ߬ٺڑ!hXEބ ޾6݂۾۩8ܧݢܣ!ݴݣ8zJ(FߡߋtbNbC6@,)# He_K6 3e"K6W"<|LxX=}zi es Z\ [@ Y) UPB5%]4bo;  ( u Z:  o oXW'M/V ab   !')!)!1-!! !y  X  + } a \P ? 17 1  3 ; @ p!C !K X"J "B T#9 #& ?$ $$L%B%`%+r%z%}|%u%j%nR%?%"%Z$$$?q$5$o##Q#""("!9!y S Oqxy9q?D YL   AJ }  3 0xUx?u%8OM {T c g n #iH]eO0,UGK3&;JlY>*bi$ASQ3qgi"j@wSK0*Fni ߿l_ ݚdG4 xWJEHJwiܺ qYeފ/߉ 3 Dp7Y[)b@C>k/mHK=+a(AXf~NgH@e95@B`OW[\VYWQ<2 o}Ou\z@ y   D  ~ - 7 { `  $AHScn[bMD,w_4y,rs6HhY+;RNV`jm?wnw}~ + M n    ~ p [ H .    w Q # r/b3=y`}$ y`L+~_Tus GD`  T O   4 `^  Y Up2_rMS2 Iu\0RucK!R/.k qB*'M%FZ9Jm>Y g|Vk3 EygBY XMRoU>cn~dQ :,,U} TPzbCO#q#v|2>~ q x}.I~;]vWb<*q6is?l J2m|$xx9yP   p  N C P  & c }I   * E=Bj?l(__-qKtSHg'YQ8$ZS&%SOwm}{kSu)T/@DZK`LVVTN'jG_-p>{;%,S9l J v Y p g ^ t K 0   t ~ E {  uku"wl`dB,mZ.ot7"vtK\3M13B= |aa1c!x$ 0+J`;^ _n'_!&+yJ~,#rCpcvOw5-[(jO1#;^-1SyytFD (!3b geO;Bx4:vU1Y\{-6y`iz1v'7NA{8\Z] gv.^  b +  ] T  L ( *SkO3i#U7b_}2X5]_gEfuU!U , R m ) H k         o O $ EyR.Y @6c95t/H&rryT[0( r  Ke  I , Z  y  0*}%uw!m{cW%[wVd#qG/5+!2 fvDoHJ 9umX5Nb\6#.~5Sߔߎߙߤߺ4X.!RfF^%1.R=_z@2 +W|)sn XAt )82^?SoH D,|6 #  s;   P 3 ~ x # ' b 9  >B{LJU< )FZ[U(: ;g,gq=)rQ'9.O1m+yF{|Dz||^}wyPok`*XKc5'L^m)S -N?vB0}fS@  v u " 5v  J R  O  N~B '8r`e HduoT(Hb lI2_a=JBLegL$XXd-VipEwvm VCcIyx2?twpR*7!]&q R30IhG%>1HZt]jH2lGB(:m sH;aB$k}EDt7=igzhp^=[tb/qM#gPy, y # /  9L  y g ) O T ^  mU  x M}uB`?>kX]jKF%bCQ)<h bEfCra=[*Pyf455e2BQ<{]R'F_U$6`^KYH2 ) i  c:  / { ` N *   Z d  =E_V:]tG47Ezu0gf;.S>cFTg_h6_>s# WE:$l@ Pj,Yws_.UMLQsXUh4u)N v-AZ.qjY7\~S *e[POH#wS!x}@I%}tyaUV;_c\T#I> 3+V$,qN1'0PULQ q3g ;x ,   } B @  ~ f   8K= Dg@[3!XMS+Jl7u&P-Le{'8JYn|(AWioeE? GV$t=U9{InT E O  u N =  Rf  3 g  %lAX,R)a*,@0t0+E|x6QMMq3!viU|A)s7%qA[k;5n /a9Eg"~sZG;(}}'2J]x8(\_z9u5^@#Huz1SS=cCyw %>Y"{= ?[  kj  m & e K Z n  G & ( M wrI*IRjN' :GkNQR@R7R9NECX6q(>{w1.Zj R=Bks)\97{e%dOKX;!f+,cFw63:zQK->wijR{M6_^2<x.R//=r[g%cehpI: 8%;BFaZ{s!=)Z's ~  k  \  F q . Z ; {  Py  > _   I Q    3'YHp/f)Lt^?=i<|Jl #0}BcHMS/Y]]ZUG_>3' g>zV){7Vk! vUM| +s   +  t   F t k W  2   o  :h/piR81ffRME7K+`)4&Movw!FG2u3/.k"~2[`/!t#qEt#BpQ+}B qLurimgspewWSSY_kul yP;\X Nh&jbw,VmW' J [  * w , f . 1 y d   \ $  R# {O s>d* tZZV7<bt {/AEg>:'bw zT:4xdc#Fo&7l[<wGgJ* Y$_P:e&]/h-l@O&mWyLeK^RXgbp"hA~n5aqV;P^PrL.TOa~Pn&&g=AR kv'aQLJuI?I OrTJX)]`ma_\TLJ0> m  ' | g / ;  2 t D 2  p , 4  , H  " yWdP c\pv/u3TK3Vt  vY7 ~T]2]k,4c>x*C.A*F15 6 1 ^ -  = X  - T t   <1 c9xy{o5TG/H9o7gZ8!uLI$[r?d-b$p(<%[a-Vpr(z'q0h(lB_7vWDh&U;)~^7yiSF_7B.%!$0=gT;e^VM^U ({X2e exsp3opVxS@4l3zghZ8WskX}  xWa"07V  5 o  >3   at  3U   1}  V p , 9  _ I (  E I+xW=P~4!~W=~&C`Cyo !"&m"P+n/hBN|,1T@u9=7b H ! p Y + 9 p . } # i R "  x *  z ' 1i>@=6i$ ~f X-M49/dm3F qIl+bk&H^} %s5)V2 W3vg9@ ~L|_Hw4;c: ~ n0ZFRZHwIJU]u-[ ?WjBsq.A/|sPKz`)[QwE^3da>L#~Y{-vfpc$TF@Y(^ U|?T( y { ] Z ? 7  z p D ; fP   kl   ]l   2g u  K  F kQ, V5HYjz5Wk$18@xF_LFM,P OMLFwDH:6("x>:gl D^ R K  8  Vx  ~ O 5  8  ; ) \ h w  @ |+BJTz d+qFz\ly&**(pq\WFA12+93TE^h!^A\wU:W'$ tUA%66y !  k j#$5:2cIyHSCB, AQs&f]F%B<$d:|psh'=`kK B f oP"xXPk/^ u  UG nnBV{c4!wm +Y  JZP \6NRo=Zߥ')׮ڰՌثӃֻϚμVμO4ɳ8r HƔáikfFŀKu3WƯKɒɽ:X0sMۛ ތZJi*Oq@K,ojAm& & }Cx8@%XX ! "#!"H!"!!(" s!y _FcF?Av6@JV  3 J  H   H < F`d{5/Ti8YhP$Q)6:nw!Ub4gD#r^_NTk`!r Wq6V&Is?j$ x ` > >E$y " #"<%#&:%f(& *(+H*-,,/-0c/[2032J546o576985:+9.;5: < ;<;}=<=;=R>=u>=m>=*>==-= =<$<; ;:9O9(87E65431f1..,+#))#&&##+ @ MwZ `7/7 W E y   Hm}^v>tN{eXI&#Zd>AGL'Ud~el߉ :ߌ}heaouUk!|zME5 o7_1p'izdbi#QGu+Z$@6;84 kZf~Gi c'oz mI&QsZ"}+ACK mikINh"kE g['?L''td$0 ]0k~56JTBGq d9* 1D+N6/-"S%Iy@BvU +    & 2 ;  " c  d    L y  8 Q L  Y  N j ) k][VW;UhZ!^2)AFbFMETng^H!AZ!2Fas.eP6U<.q d8*  $ - K  v X 8    ,2MX->-9#nQi`(vccE["K0A,2CH4#Fߗ, |r,+"`Vߗ#%-5HCQzjE&Sn~xHn C= 'M7R:~q n w ,  m R } k # u M lxjsQPAw[\&E<^8#Tzx_ ^6[c RlxmM5 1Hb!\P7 25|`CMT8z: ~ D [ ? ~  p  l./ytmT^)C)!V" $Y"t%#&%'?&(V')9(r*)*)Y+*+?*+a*~+Y*F+5**)p*)) )')q(f('''&.&%P%$g$#r#l"z"H!p!C cm7(}?})#ck'TO  + u  |rcyM6O hJdLm|O-b 5x-MZCߖFb >QZC]g>0',7J*mN>vo.u'^v!r!Rr!{2?a1 8wD[  LW T3$n"HB2Z!Y+B;=i*ZMJPoWvIa,3'"#9CX^[J+TS(A(LNx$]/5zdpTbr@D]Dk q5[m/wFi}    A `   -0n e47m FCSxQ9%#,Gu*e jJ aSAV"2SD1oP@4BlDNoine TZ/d O\]%u>Jc_6Le>|n[|03Ix/l,.D7s% ,kaUO&ALN6gppY'|2RioJI~&DNI/gE>pZ(h;K[QW*jTDRu=y$PHd}!t Rsl^0Y}Vbo)czxt_Iosu,OoA]C %=]d! V%B`e.l&g&g{$1pi,~"mkDut?g^= _ |  { X *  sA~07c6tGD<fumQC," jCa., x0bgvW30Z?5Q4 w 4(=SNR~~r}f~$F\pb2Tt~ L   ` ^ 7  /K6EUs/VySl5= @f, 1 !e"7#j#($$;%% %$%&3C&58&"&%b%$M$n#""+"! [ 9?N)j    H JD[sB7Ylg$t*xE2`7qcf[y&_wX#$KSw@ }  n o 2 V 6  dAeB+|G g#,9DBpY"y`+>  y!W!v!!Q ! " " ! "'!!!!!! p! !6 & C dB4<GO4p))R @ h | ltCz \k@2|(=(,8Hkys\cB Qf^&?+d!pU\ @e6e0HHyGr`C)tT}&aJ-KmW}db( -Zx+w1Q(b4a1J2[u02-97/IKs+SKQS"0 0Fgc*nLm% Bz$zed#*W519Fo&hXM+x%(})jL$-%C } [ o  g b \aYS_Nz]p]o>_,b7&b&T6VEqI.cSgaJNVIS}VTC<:_eC+VeL   = Z J * + qf3b>J4pP [*nLu gdF]XG(scTr"sy.]& Oj"9cUz2W,D#A  V  \ L l j S &   ]     % Z " M0;ida U3te zr_FZ%&rE Th+WBb+hWwdg/~*V(8muxw{u~o\8]^#}hXTI  o A ~  5 NH  f i?p8 8z7h$+q P)Ol{pbR9^;kJ{n1R{GXH]eE} % i>-KGwjQ>"X3Y ':IPtD-r8_7LP*)w,TuG,e B1 ),E>y>Uw jZ^&Ye 1[,w^?('6}rNl~j`O]4  mmF#)K:kWbTCl> 3!sBalt9v;^%NS~3~~:5uY#nJ9b2Jo>QNLUT[EP;t-lE*XJq ~>D+ DrCuS&< r QVW q  N Z 4 z J ! Gu?oX3Dk/0X[u}~GiVY(ZX'~T=st>O >jt  bt N !   3  }!g6>t< 8-NysTl+Qq]dKQ@4)J.|:SX2M<o5 J-`I.wc J\ZBl&~9wV6DvKT,}1[s#l_]}FMDLjzTG# ow>YJKOeureUQB:-bgAE\aJ7 LU=gOlh>M+RnS+(V7x lSC:CkP9(\XZ&q p$ r+i)K$Rg%r[< y@ +   M  / y h>   Ht  5Q|d(Jtw@_mM4 +WMzJ/ ~8sM rqglo T+  p D  } ]  %  + j;{K@jjTmS3` qte V.q 3 k+v=`248:MWwERvuIu6p%yGwyW2!tGi] 2eQv7ivvT/{PvGX WuBdV/o!h/nBy@g[n%+J225;I==O{[.s2{R-!@YpU(E}(Jj(Lx*gbqh1'd/ D$ic,OyYZnX`C  ] v  I$  )5  ?kSX[I  b6  W  * q  Tk  xIYvE u_HC_%IGbvw #-  } 1U - '  ` `   % K g y hAt_0#(0_9RGUo/VD*{Kt5 }Q fu_CXq^g > o_x9rU3PSnO.)E`& <_j?,i2{7<D|~  u5*I(~3sOsVI;0! tc[SQZ/ewz$ U_lm4o$E4Kov 7  N p # Q c  B H  % i 4 f ) { J-|v,H+rq j3@V.gpz~kS/{x$-Dg+Ij 6" M p L A  J zI?{BTL(,G2DrFwR'I1pi DJyA3=bzJ.v]:*,wHkxd_\'RfRSZQfxhA]GAe5#O\+r@)}AT|I4t 7Bd zt\_TRIT(}o5MCnO[aN3 C<3b EID}fU#PRyIZB4 o(E0bSnjYHy_S8|x#?q~FG*5Sox-NotU1vZD D<;KP= :xH/N?3ve&'aHD`#DWCYI! `PO2`NK~!cRw:R|CqLrv }YN =  q  !   * 1"  ('   w Ph-K&fb[l  ;<>,8A<XYm>p`Q o x j  L \  v  - Ed^nN|,m D{MA^M~6<=]Q5[l KRHPJj<Z8"Do`x,**A*W6%J sfj &  a 4 U C A 3 T !  W {  S=a9vIi2=OmDNB#)d e@MN i E r 6 n ) j * v 9  [ =w8s<&B!,<JT.epuKIj z]n`]_Bhy}P &.9X{+SgE$tdg,tKu!#dKs&h$k!4Ood4 @ b?04TY_cH Vl;lu6D}Na29~ I!{Pt}^u;gS@,]\c$iF01Z)}k@|rg4H#`Y&hH;=LpE{J.D>eH7ciJ<2rnaI?" !q";  z  W z : u 3 V   O- {   6 ?o e   U  + N f+X nQ,6> ?8%R,V . < c J  6 h  z FZ#GxK&y^(h|\bADU87x2 uSH|Kn$i b}!Gp(AmP7Qy&hJ<rE(p%CnDkH|K sMT!IrBHQ ~b=t(:i;w|L~2`DmA,vRymfD:>)A+|<L%:g&6}|8# i S ; 9  / % W   } x 0 ~ R  @ }  > U i%m<pWhjRu;v r^LZ+ ^ j ' X T C  r R   5  (o  1A5~^"k nPWX$vEU.R9cV{)$IU~$L{(z:WGSv+m+rKe(7 .Q{32`%r.ng ^cI4zHJMqa2K }[k=\HP$AyT;b;Z!P" , @GQTVTU|OZK3>7"oAkZHt6l!UC2f0W |>xJ#wha\\_epy 6Xu+KqIyH*<}k` gV 8GG,\54L^7'*zb  "1 x  (T   H : y  [ . ) N Z b x \  w N  ' d u 1  k % 7d/9p9_\-gMdRg-#|tBP9s.r;h ^byU&-lqLO5@+% '-3C DVW'_5eAaTiiajjxG//g!jB _Kg"]T8)eZ(9AjA   X. v  6 x@   2 l N t  % K f     ) < C J r K ; I C 3 " K w % h : y  t O1 r.r}]@*eZ.<b /F=bmF/(Vo%;]tQ akEx+V5iC'4c@D S!5J`J%J |U@D mhJ.vr$ZKv-?Hld{As3Wl  }#`%6#vI&ss`PD.)ls[cM[CV?W?]DdOo\xo 0<[uB'e^YBn(q+ VUDR4~HyHqh={ I   S C {    < V p- 2 = 9 7 1 &   & > W u k D  g o ! D  yh[+*p/`P#PO!2Qry P*:@yPXR]{ g:}hMG[XRW^*R W~faWZo]Cd.p(Lv&[5aM7td'Nf`Zt%U| q` S"i@D+sh&XI %33VCxHPPPQNN.EFP[M{[`nsz z'w0k5]/C + ih5>zN$gE/iN3wfXF;.$ #-(>4X?kLYn{&Sx'O{GJT;=v(+ycX @q"fLV3n4m#[' R z ( R w              o  Y  9   r9d9{/>3rT3[WVy @3^0t-^\&E]98As EDV0kM!`yI&~B vmgb^a\_`^3hTn} 8u 95Xw O)]+r 6Sm4!}` N,V b 25ub2j=a>[|Bc.1:(?>?K@a9q5+u^E.|jlVP<;$!|`G.'4J\u)Fc8g+L,ndQ4\ IMp8}15sWtU9]Iu!N%qAYr*Lg{ciIS++W!b?`3rQ+5/~E{"jHYcAr *~3rAgdu&OEs>4wy:B W!X7jH-~r'_A\WKrJAA@DKBRqdm F@'Fj`:7}b3n!Q,Z?w2 \@w::_g;b"*9AL=Z^ayow +0u>oAUFNI/E J@F<>68t0b2L+B,.($"# ~lft`g^TbIi9r0'*-)B5\=tAPPfez}7Rr9JW(d>fP *FTl#CAl_ 1>F^`v'?`ovhYH4 uIc G*^~|:X1MhY-BQy0;h%VP}6w-l-[fJU [UPg!3jd8F3 y\:#Js|#CJuy4hFj'WBk*U?gDhLq1 :&K;YK``mmsyyxoh_VIA$0*$}wkaWJA~3s&o#f_RO@A1"5',3080G1M:_@fLxYfw.J9hTq;].FVpBhCd6Pv(Ed-~Li#3C)ICUTYhX}]TOJ90 {\h=M.X6w]A)LYV.Ek3m'JE nT$cq&A xIzL]8a ; mH`". inIX#E2$ tZ@,#2BXq%Gi!/?PIgn;dKl([@_*}aB0qRl/!BDUeiwO\gq"{:]owg](K.@7*=?EAC:6,&ti\PD7*! }xto ii4c?f[fkfqo,Bj +.9PAnQVin$@Zr5Mhz /BM`hr{)5IWhywdY?, U,t|KgT9%j9vQ`26zEd<|KlG[$_0bC Y6iwU[=<, nAeM4'  . EW k $1:IR0dNjv}Ba *@4hOi0Nr:Po>Xz :Mc z(F`}5Ng~   + 7 >JOUZ^\`YYWPPFE>72)' .@Ocq  !9*H6b@tLUfo  ->IX `0kIr_wp~ }yu)j,e2W6K4=9-56 31//*,~'`#A  `C( rW:tP,oM9j?~a];4!rdKI&* uQ1 rbP:(nO.exJo'edacgnx0Cct40B;cMvZmz7Rm)Db{9[u2Pq"9LH{du3La~-0;?DGHHF"D%@.:194/8-5&:#9;=?B DGGOLMOGHC9;-+"    3=N[ly "2AN_l+{7GRdt  .9IWet ogO>' |pfYeK=;)X5 mU6`;v_x:S"5}[q;L2kJ(| gUF5*jU:'  }b[C&?1/=)K![j}#'/4< BG.P>QQ]a[tiiw{7Rp2Mi8Ro7Xx ..@@Q^drw(:Qd w     !#((++.+.-,0+3.0.-+)    '6AN^ix  &.5=,H4QJZQe`lozu '1@KWahqu{|~}t}j{ZvKt8o*kgc^XUKG;o4\&A- jOr/]G/}[@ ~bN1jJ- kJ-~ jL<pT2u\O3& )9FTeo 7O1cC]q" 2'NBcUxr &(49>SK`S~`gsz!2AS\nw        ",6:JL[bmx $2AN]iv "('-./30503-.)&!%&'(&$! zfXF5#lN;yhWE1  iR=%fM.|lWGh5I#/raN9(vpbaOP@<5t-i,S%I&7#0&$&./:AKX_qw '-6=@RQ^]{n{ *"3-FBTVcet|( :4O?aKuV_how{*@H`e{    #)15=CGSS_birt!)3;DLR]`imt{~pp[[DD/+{viepWaMKA=2%%vuheUPEB3*" z|heMV7D!6"taM=(ysngc_[WXOTLQKOJ&Q0O4JDZRccur 8H_p (/=BQZ eu)z1@QVmp !#'&+**+(+%(##!   !)(42<<DDL JM OKQILFEB>>!9$9'3&3./(.3*-%5#65=9BABFFGHHFFC@<902%t'maTK ;/ ze~WuD`3U%C3&xcZG9)usig^]TRLF|Fp;[=W3<5;-&/'.$+&(-+32;GGSN[]mbsoy !/EVk~ #-47>CE LNR,W2^B_DiVgZsgro}wz  xwkkc^ZRQFF;5/% ~zne\SG@2)wq i\WKD<3*'y f [I?,% ~tmf]UQFF<;52-,)*')++.24:>BI OS]!]+m/k8z@}GMV\cmqy 18KSco$' 26!>+F6J@RKVTZbcgawh~klsrxy~wxpqkgh`^]UWNMGE>;61*& |sld[UKD;2-  zxlja^ZTTMNzHmJbDWEF>A@/9%<4 5/,)%"  xzppjfd_] Y XTWSW)U&Z3\7`;eCjGoHwRzO[Xb_kgrqw} &1>JUdny  $* )/430C9H9V;]Ci?oI|ENJULZT^\bcikpsw{z}|}wvvlqgic_^XWRMIE?=53++"$|ywmle_[QLE;3-z|qzdw]vQrFnAm1i,deZ `SZNRJKEF@A==:99592:3:6};9}?}}GAyJD{MGOLQQWW[_adkhup|~ $/7FI[_py#16EK V `goy$%+*2/66:<=?>B@@C@FCGHKMPQVV\]_dbfgehehciajckbndmhjkiikejcfad\b[^~]xYz]rWv\pXnYpYiVoZiUh[jSg\hSh[eVhXgYgVhWhVhTiRhShNiNfMjGdJjEdDgEgAcAj@e=j>j<k;m9p:q5r8w6t2}6t01w3~,x1|.x,x/t*t+m)p'f'i$_#_"[UULNGFC> ?6:02+(#zrof`\RRGG<;20(&   %)*41=;EGPT[cirx "(23@@JMSV_\hemmrpwtyx{{~}  }}ywvprmiicf^$c X#`#X#Y%Z"T#Y$S#V%R$R$P)P$M+M&J,K*G-I+E1E/F1C2D3D3C4F2E4G1D3I0F4I0H5I/G7O1F6R3H6R5L6Q6M7Q6M7P6L6O7J5M4I3I5I/E6I-D3E0G0A0G.B3C,B3B-@/C1>.A1=.A0</>0:/:.6/5.1,0.+*+,#)$**'*(& +$(%#&$"! {~vwtpolkigfcdcbcabbccfgjkorszx  %&,)1.252776:9:<;;<<;=:;<9<7;8<7; 8=8 =8<;8<89!:5%9"6&6(6&4,5*2.60/163/8354=176A1<5C4>3E6A2F4D2E4G0E3H.F2G.E0G.B0G/A1C.A3?/A5=1?5;5>569>62:;9098<093=18-<48);39(:.9,;*7-;(7,9'6*7'5'4'6#1%6!0!441614517.7.4/1..**)#$!     "       $$! "!% ' )#**#.!-!1#-"1"/%3!/'5$2&3(8'1+9+6+6/:,52:/6290749145;2/4;2.681.535/3/614*6/5+8+4,:)8(7+;&7(<';%8$>$7!> 8 <9:8876817/40/0,/*-*,(+&,%+$ )& $' "&" !#!              ! $#&$)%+(+ , ,!- -#0","3&, 4(/#0'3%0'1'4'.)6&/*6%/,6%1*7'2&6(3&6%3(6"1'6#1%3%1!1%/ /#."--#+++*+)*,(.(.*,+-* ++ '- $ + $ *# (# &# & '%!"!" #$$ # %# "                    !! %#&$)&)(* ) ) - %0 % - * (+ ' * ( ) ( (' )' ') &* (& +% *('(%)$)"'$%%#%#%#$##%"% &!"%% "                            ##!"#$"$$%#&"(!( (!'!' & &%""""!!                                                                                                                                                                   LISTVINFOINAMsound3IART JfreegmanICRD2014ISFTLMMS (libsndfile-1.0.25)id3 ID3@  |dq|TALBToxic sound collectionTXXX"SoftwareLMMS (libsndfile-1.0.25)TPE1 JfreegmanTDRC2014TIT2sound3toxic-0.11.3/sounds/ToxicError.wav000066400000000000000000001501621416141666600170740ustar00rootroot00000000000000RIFFjWAVEfmt Ddata0  UP%&XTRW9 3   ~ % & ] ] | | C E  M I B @ bfW^7. 4937ws\Zz` g $ 9 < @ G { r { M H   E O / 8 w q jezr*'u|'0 vvJI !`bus\ [  ; F   K K ` c + & w s d b 71#%ge (%ws h f | x 0 . m n l f ' + n t T Y SV-'KTf^x~pq$%! KL;<hh_ ^ ) * Z Y  c a d b   U X ' [ V WY.-,*X\hefi`_{|65 : 9   U W ^ V   A G    ;=|{OULF39JERW72OL  Z Z B = } e g   @ >  &%!!00 UYSRsrsp! & "  Z \ } x 6 9 k m $ " C F )#B<kqd]7=!FG12EF l j    Y M l l * & M E JHkk88**|{MI2/^cgd)* M G ) ' s t D C v r !  ) / rqLP||us{|vsuyzuswzurvwuuvvvwvtruxtqvvptxqotw|y ./yx+({ q w & #   f c C F k p     RLus01$$|zgja_ehb_cdabba`cc^_dc]aha]aca__bb\]gaV^fca-*gk`]pq s v ' &   m i w r * . > < FCkkXWOPOMLPRMHLROHKOMKLLLNMJMPKGKNMIILJHLMFMTkjKL L L 5 4  P R  !    KK]]8:vyWRGE<=88769:7678657735:7588655587129822544@>:71<-%    Y Z { p  & ! % j h u t >?[\;;%'#!"$" !#! ! ! """#&##"#!93Z[RQ w z 9 / S 1  :  k : | P \ z O \ 8 $  W ] rL71bL(O%U4V(T3Q'I3A/9<84462*)#pqj H  )  N . j J v L p R a 8 1    _ ^ s H j =l {~HIKPNROc~+@7\gw2]NW*8 t P J ETouqZ5+ q  A } t } G 5-RqujBN oW@8<;LS`~uL'?[J]gvf r H   S C s z E k [  m   ST/0rbO*'sJ}"W2'q|@  h H y f d   ! :~X*7K3\{v4i 85*/ (.'2$iMi e 8 ( V  i 6 n 7 b + ;   ] 0 / T EwJO_>dk 3[LTSGCI*mI:L  Y p \ F  lX s { 4j 6B "  X3  x  # {GA3{WDOG=<)H#By@n+aV3.7Fa  " V  * a f f     < ' g2UO+^'OtagH0tSpD?b.q8f>{`bW    x &  4H U%0,s*Fift XJM{j ;L[BVXeNQNWP"k^+^$g4]f B   Nu.RY+ x-   b(~Cn]<%QEh?].VZ yJTfj>-_<qv- Fc   k w n5 /KEtAhr( Q$ QtKdH hN=G=][^f u0W- wrrp>B-''\W3:0R}x^#bk cA~H $f q '6 U tm ` AX T;h.s S e ?c fI{a< ;0vrV OcYW!2wWqwD;U?. )S h i M\  Z-ps8SO e% 9>nA-7Yxt HxtS,OaHb?}x4voNS)yc$aK:u }/~Fxh$J#"V\a4, w ? k " }s<eK i7cY~;9kGyl2c^e0}<}z=?y&xJ 'ue(Unj6_<vT#H .Y|,1[c,@=CO4V&Ra\tUJPFZ5?KD/6b8C404H>a .~B9oT$g X..4JpwU>~47%}s(GOBmSxzm9t!;y5P)^4zz=B4LKnT gOcJjfm1K8hqYSkg7v^ Li"46?(}_ Up')a{Ut h#j2UGnK|aE#%%>@sDdr>?`iRg4"+g, i r i@*&}[%a3~hb>w,f > n;z-Ut,azLtB6,~HX7>."{oQ |oe`ej [7 h" #@#YMP"*b$}&k5gbj\d[a`tk-~aPPLZbW^./?:,x.P01U\ylFQ/ .fT lQ70c s 9 8 p3QnwK7J `= E 5  /pF<~b6l4[]>C%8-I}}I9~0MZxn!c;Ec{dG%WvEDZyiOXE-    y Q } 6 y H ?P8Hu[+Bfz5Z m8dH@ EY89W`; VTC{eosHrRPK!+d/>o{$Nd Y]v%}}18u ~NRq~]e~Y P6hPZqfltDpv>9C!'QA tD  #q +s 3Fkm6/ B= ^Qe?3/`R0g? V T9`'w /FAg":  N 6TR -V"u {5$_w7'G5|oB~.{aZ``+?|l<Qt Isxf0 ]%oJMPaao(aMDD@}L#5+l-a0vWR42)BS0 .Gkfk8 #4Kg-}qSc >'%0O_M_!}  9G\hn^Is&^0GS`Jm3[4 35W"N8U:%`Hl Zv.;!v==y6R]jX kK V5 l`#op]Fw0 n[3EW9"._/DrH/BEp^0\ pr*.rm@7> &l|$/?FO~K/A6-.8U!G1R/? $J3FS s^?+3h0R7r /jJA9Uc s Y l W N rb i . " : B7 N_ 62  &oX a } _ o _ V Nj%/jPo"yOE u~E J {7Z`D*[ z m<E+EVgufx'%+ -iYo\R^E[, oUUP{P|\G6f1d#dDTo7{?`$rZL1 > \* l/ b \M,+vec {O?4nzvfAYu.LP0]jGK3F&7>.u*'X5 t 1 87GK$ w( & is%0<w >}[xuI|#3y7<&]UKE}& L  > " z S 3- y/XU \0BN ";Z;>h bYK [PzvT|3!48S.}?Z=K'kCq.  g  8> Y P: " 97 Z{\,W*4FJa735NLOyA0 m } Y  Eg  m  $  T ; ) pfvh7nCh}\9i$iuH1=.AJd\}ktJQP.8a e  | N  )hT "DiWvm1:w y 95 n ' ] ^bxvg1DuFio~?@<4{gV%\UKlsG (9mPbRXJDD0' = W T 7R tfh_W5P#I* a  (M   ;` . ^ \   %Vt[(X  +B | ;6 [ l qB s mB6J~qJ,4V #[\?.0"7D%$us[*m:N`mF]"G|]k;T;Ji4#j\~5s6okzb)goknN !? m} ' u * Q DinI2%@{]'y4ML{cxw*>8/H9je 0]B~ 8 J - {c.L`J" G p X+8\fMvo T^E_wx,)AY Ouptx^F,Ut\+bdinn \ ) H {* K #  b DO3zq@[5Rf%vm5)H`|^5B0"zB7)D c   V S _ANmt#E8"L\ ?6m=}5;b_B1% ABW0,q9  ["_*=(~-q+f1;h*8-s_ eZ&|"[I`rVOCHqD?J-L7Th\nECeUE  nSb$h v?Ql=3h#WgNY8-aF''XC-ib3 +uKj.7 I%pt gi(G g? AuT+Q^H#WuOY3"wZ~YA5n)Ng$3s@M=uv&x2Ax{R&2X:T{.llfO(sN) s &?4YC-2,eVC+BNc<qth 9CLS (.M_'[   q!\'WfCL7Jfa$?/ u<GX 2NX, (OM_;V( a$$fa@i2U4wgxXJ7`r0~[j4%ZjD7 l'Z$yrz&hLL_ 3m:: rUqR=n FF}h-Cq# a[;+9G5- &5P<hZMSHljl `')m%lEGbr3Tmy:5F[AGr*S,Z {*(< VJgu}vXcDnp.tw:OA]8S*C LNQT waQf?) ~Za0r=J8HoR-:?uH$]KS8DQ^9AAy !^FS~"LwCKLg_KpglO_.[Q :92C2aWYzY,BetV!?irix2h+d I=?OaAzQv ,A7y1/#"7L T\^TOCFGUqYP]+WE,s]{%S %.1"  "9] (H]]ZOAf8R05,+,((" i^J l) 2e`GAB?Tj^ R}"Rl[0.u>' \zK&;a[,pmAxGh,L~)Yb.G]t%IG:b>R9z+ RK8y:D\zmqV_ZB1< "A>~|5me0N4oUPA"yG&%  BZtti<yi9F^8q8 +P)Ut}vf*_?c[nr "$^(6")n9gU U7Q`]v\WNk8Q#`]$z5N?P,49UO hsO6S $ t=_r*vgw #%5t>; 6S! DM;t?~Mi ;j^>Vw)Bv`OM ]zAT)FC[Cf!B$s*xD"|Hg90xu *8A\6xY-n< 1:Mk*voX-Sh=iZ Zj@Ihh|Tg1@}$sO|'$=?F%QXFz;jTkv|9(4H`o.z| CdtHi4sS>'}]o;N$5 $ ' '!0==[8?,&"0@Xb~s`SJD?CBPO_peQq7 %Jh !/7&G3LFaYnv}v U 0 a'h4PyYE4p7Y,R>;8EH8BOGNAi?q<15#'4l#CGs<H^!I^"//#c/Q \$`lo:L3 "~L##''$'"'P ';0Oi L"Dbc&Xqug[F4x^H8P7m5~qvs-W xp\GzkmOU%>MFfBAA1{'^Fet#fZaPd~j9mA(&@^ )Nb:k+Lt30?3*lD#oXA6/*( suX[ZVR=`7e0,76CH$PGPeN|KMUf~rfS$Bc3 &Jh}gU/)DVp~CN"x_|Jm*bPSM^kwfXIF):U9148?QO[t 1Zu +Ksv \2<NgvM |oZKA $qN,b!* 1vV/8Rq2/Fz7a(37MdWjpVq [A:viP$i'gGvs\ckkz] '][Fw9/12;3A^NYn| %= x! -y(Y|iE"stSj@Y4E,"79XbM_0!&,-.$ &=q:ax "i X(A/39B MWeovyorpZYSR351+'>Me~ &6UkxV2 %4:BnBQE==);-"':Uy +Pi!,%4*2*& u^<$0 @MY[bZ\uNfL\4N-H:4(  %>Ri~utrmv s-BOjy"&)','-+/8<CPuSYaCe%q qwrnh_zWkN[CL8<.."{ rm4kDf^gsgoy!?_ -9NO ZYWXOL@:,g$L5 jO=&  '%;+U*i%"0Mi""&$*! ynxhZW8\AJ9BBPb{x}{|"wL|jjsaa`Ve^qzsQ3& ,CJ[e]sL0%}pYF. 8Ffy~a [0HL@i7644>@T]xi V*0 FWn||UH" 7}\avP66GeexA%2IaeK'>QmxQ8 '6SvqVF")LbxZF7 $6;Ni[Wl?)#b6OT5e.y !#,8GYk{zyrsihYH7'+5~4z8m5r/p+w)|#&"  !$,&+ !!"%$)$    &+/1,/ % "+/,0'* " ./BBNZWf^e[^MP:8)! | naOH==;CFVZns#<M\jt| &.7E{GtSgW\XIb8[#aSTC5$wr]_LQEEEBI{IXy`n~ '+@FZZm!i$t)r'r/o+g/_/W,I.C'5',#$ #".+2**)4>COMVUVSPHD;0( ~vojcaa_f gow!*&,%% (4FY]qq}{{ok[P>2keXSQO"V&],c6v6|<=9<1.)!/3FDVV_e_iSy]zCnCv0o*~{}qrj#e*h8_:jChDoI}G}FB76&" %->CMRXU^SZOQHF55"  %-'3++)                  % *  )$# ("#"  $!(**++'%!    $" '#     # &" !"  $$                                          LIST\INFOINAMToxic-Sound0IART JfreegmanICRD2014ISFTLMMS (libsndfile-1.0.25)id3 ID3@   TE_TXXX"SoftwareLMMS (libsndfile-1.0.25)TRCK1TALBToxic sound collectionTPE1 JfreegmanTDRC2014TIT2 Toxic-Sound0toxic-0.11.3/sounds/ToxicIncomingCall.wav000066400000000000000000017126121416141666600203470ustar00rootroot00000000000000RIFFWAVEfmt Ddata`DA t i q [ %  ys]`GA   \F8ZBU B l [ * ! ',)1CKDI%]i|3HGY'7&DG ghGQYkam } j c W RNKMuoJ A E 5  ~gD)S:{)  /   __\h#Tb&3Pk/zYiT\`crs>A'Yl)': * v : - 5036$% u w i @ + N7I,t` hdcg t{ e\jm%+@';B}{ TXXi  e^m ^ } rn %$95>> j Z YC9VB pc),64:9z62bc&- *5Faf!#:=bjoZn*6YYKJ r q e IBMJce _D(6 } o /&79(#SN g] '')BYn  &(2@Mb^sUdCK  y b K > `b5,!   cI  a K = 4 KGng  HDblLb?W?N3B+*v6Hrk}*|  *  e W m   |vzc][ M B 0 fI ,  ~qy oh +#nr!5+[owexNN-,y|Q`%it7LT F  B 0 U F _ M [P r `  cI qoIFZ^"<=.. ||xx*7wwzz+,IU(< `yTi`cWN  F 4 T C C 0 < . .&  ,& ^F  y 40\aDL-7^f[UMG9AZu )$(]l|%;> l X e W -    fg2/  N85YAe T p E = 69/:>H25|z%o9X7I :; pvUc[jKXzji | { g   s l ^_WRc^5 * 0 pV@)U9; ' G 6  &SZPW!nz@13 } w t YRAGYP p _B.B) H= ))KFlc ;< 4Jay&+,/JN`mctHV*2uq  y l U Q ,.  WX~y  cIs a W J >:aZ  RMwbs5Ms*:+*( $AQxiy u~fr    l Z ~ 1 " tqTHC 9 /  dJ  = ,  zcZyv 6.>I.COk\qU]7@)-cp1sm-== 3 8 ( Z J p ^  og}x k a P dK  `]ci+,|  GR iel1+Yd8J f{{J[GG<7s i 5 $ M > I 8 L = @:3^%) * z~GD + . |_g/:r~"4O`5hII"tE.$\*?ms2!X54ZkS~ :  5  T Mcr+#$ & & : 2^@ < f[)*(.~^>= :0 Nb@S._?: xB>@e@Z"I7 D A # % = 6 I T  xZ/AsK " $ W 1 S GK k XCPdnfxNwU7D}4{~nJ0 >FW*l}B^g  h b P S<38YB+QU CG89g[\ / ^   VI  ,?c+ sRcA{.P'{ lAKwG#8#fZi eRnWwo   | ]#sh+ F[ g !Vl m % neA{g>c31P ec|3wzF5"]8Nm> S3\j,~!  _ ? 6 ;GlDI*s > p q  NS r p$=c1)Um"B-6C:IJOefvM  wYg2PyRt?XIw~(3h-#!   J F X 5CjwU   u vWa 2 * `wt6|VghhWE^n0[$4">"]Ngfo tv)e~! C " k )  w`E<.%Kf! 9 K A  q v   7 {H/k j3Z V y5%_ R8i^ohSvdJgGIN  , E w u k 1)aa$@>T    ] ~  $ ? p?]0Ka'zUloF{ExOPq^fpPmw EsxP W P D G 5 @ 5 d [M1f8      @  j o<"0SYNc * wp: 96M ]BN0q; ,H7:om7zd8  Q   t =8  q Ye u?DBFcT Br 4 b `  8L #  z+s8P?>yBPB4f}P?8nJ/ZYxU_gQe61D oip)** O    I Gm   ^c   ` 2 tJ b B { b$OzC5#h[t\[ >&$ \,_:e~KOd]6bQY  {<   } c$ b * {C 0k     Sn  ] O k~iU-`*bAJaRM)&a lohzOvY6D)I"4# " ( r  O  iU N  %d,kh 6 O =  fAT  a  lMQ^}J[!cZye-7NfP`D}tCG?a v H   '    FEn -  + 'p JD  F>`QV.W5%g,:u~g k.hgYFdl_g|("/Rn]3|U_d' ` ' ! ,R f  t  e <s 2 D l O 8) N T   TFL5m9x&U.4A*|xuu1:BLM{SIm&1 fY < I W N A + +  ]l ( @X $ 9 CI i[.5 A ceZd1oiy%nAAp?Cja(UTCMmKN_ iX= f^c:A> D [ H w Q A { T a PS$ c  q ~H|v  : XE\ GF [PK<Iy~/vuuH H9 RBQ$:z^ Ebu^ > H y ~OmYSV  hs 0k ? 'F Z $50!gB.Qwh#%,`TNc@1\z`I7w$Dr r`^+ u [ 9 eaFe L  " m!Lx0ZN]!NLX2d^8`*;b@~2 ^:wGe A3 Df }J  b $ y + ! ! R* Vo  k Q9L;wbHwh1w;xrxqKol2f%$2V3 JrqRfT6Z!T2  h * ! 6 1 +b8%^_% G 8   =Y$Pd \   _   -$ 1y<G|0(Kb zZ&i9FdQGp^O*HG=3"xvD6Kqt2U t * - ~8  " wrslxZ >L T{ ' | " R;) Sn a wO -;0B$e;Lw $[A*'((wwQjq$=^3 @`v.K];1 5 RdH"oL " ] } ? E{"D <   `aA \  v5  oA Y  UU! YK'wu|vO]x2LN '/| vqqpF7EiilDWvcJ4R@B+YoeXw H vK1vj~#8X W 7(9O; z m )m   a{k~ | e ( D 3_ZA2X*AAJz'Ev0od/U;8IWNnd, :126L: H P T!  b  9  P e o   ?- #8 ~ sn;#zW x?s,6M(dv~TMX~mXodb Yu-e1Xxe[~c2#,iNyf =PRMN  x3-ZbS|v@M 6 wm{HAmU ! =  jp  5^ n H %?  x; ; z _YDV'Oh20{ Uk' 'u /8Ti~\Cpu-wc-7M!F[y 2 i  (Z Y'|yG#l7 > r1w o  " 0 P^ a 5 d &  ? $4 : #   W"%d t [Y r_4Md{? ?2YR3MTKt"ODV:jD0:>i 8 GQ{w  G -4  Gvb3:I   (Op/}Br#3 U < )  @  ^ K  > ?/Q#4?)%Sd$~+x,Mv^V.ILK{; >5g{ $CE78p   d / \ 8WSjU ab Q` oD   Z$. 9 ^?  6 L $ nzCmTwLY`zJv"YaE.~3ouQDreJ,~1d5B^/_L- c#i 5Hg%1.o {5 FG % hb= d  k Ei y   g   1t  wX [!t sTMfgZ\AUdM5 x+y8:NHkH2SW| `>/8O7'g!i A   6#0b E    } qc  C  ^ d PF   Q QIj ; W KfG|'5Vxz\,}W?[%1xqh~T4F5c!][}<J v  Y  H D 1 . gWq 1 &  `*   {_   U ~ .'  vA m ? e E   _ C ?x@%L&=>iAWXvh23 x1v#T?*CX>}D- J v \ U}  n  w=fz C S  \ v "OmP  #\   / ^ N  ><)-_&<BcREFWhvU r\w!a&Mv+hs{35]uh~ /c\.l4JE`;~  I f b i  ~ M~UVbvG\D  M  }xL~^ '  )) *MjH P?)xFEmBuQ`_;PAi9 A[P I/9 wQkAq|LV. L # % w El e o P ) s w  J4 ~ ? ? b 9 ^   h  E & = }  _ ;s s m j tXh!*v/"aTB(OKiV.P~yCdD}rK]:u,J#H F ] S  N ( ) 0      ? ` ' o@ i   # X  b, V  | ^QRg?U1 R\04odu.(ti[C '4.K*/CytJ&LaB@J$Dc1Qr1 ~  $ w k< _,  = 7 D    bQ        ( 0 h +X>HK0=rO7 :QStHCxlS2Z=dW )6])V2jO4tdH   0 u V U a ~ "  Z u Wl66 OnR g zqNkzfBZ W  G.E!1/]fowEX>P!k ]2Qg8_3o#}MjIr1N FzOqS > Wz]d9<i1 v  bnCg  U s  3  , ' O JuJ,1 ? E *2 P"2@~5n ;O+iwoJy>0gmg]$z"* j  n  8& E  }  -} ?q6@n,a#u X  nt  $ > 4 I 2 <* ~y" |nK*'JbnViz#s<wT2 Q r@@d  w fc"  NR^DjRZPh=@u_3-LRjF+N-N}Q5n9: p#X^`JgP> x 3s=:`-k|L?<N[q_G2Q$ ]~93wxmu!%%T  9  4 _sjOfvoqF Q LMasy   6O  z&$ ]X I '/+/;   ; xgp  E3 P 7 + * TT x i.A' xu +05RXg@/,;Bw>J//S* M5boDX t _ zp> 57  s z^ 65 cO#eb`*8 F K L]AtGE '=2o6 -xmdU|HGP$"`]cWXL!-0,!p5 Ez n 17v2:85w n Dg :   }.  zn=I*)|r4[FT,pU))~g7  k 9:   F N `  vi _G6  .sZ  4  V! A e{VY/(p4< D4hp{ZAoS{m3%k. 8HQ yp ; ? } y  =   X K  g 6 B]n\l 6 , ["  &(bys=VTd2W{kZ!_ _Z2k (   [o s e$Cj   ?~  ] g< S 8  1   "[3_ [}ZPq&EK]HajY_]PIznX&ukQI ^:W  K v   c m ] 0  (&b@ w  }c/ zA l lGi -*Y X`6yZ 2b J!BZB(5dnKw 9e  h @ - zD S ! M 2  - "  q4p0  .#  v{ L   '4t '`0HB2eTHXsz c4D )2 5J+ 6B Y ? F  1S n.$oO9I  o#rB F AL) 9Olm< j n   z6 a ; G]LS6uU|r|Up|]u1sB+20U $$g~5b [+   >  RE i{Ue" M LV8 g   ;  B)  g [MRh =LQz[[*Q6>?ESBln 75 62 S H ' hxMRu{"b  ?RWh.A%&L2/vj% Z c 7gg:`^*8Gl\)5'52Y+}_q Qm{n%LL   /VQ $ C  s   2  @`  i  H : l  GQx >X[4$)@ <-rWcQ#6$;'[<*`<%+E v    f c  5 ? 0 4 K ] P  T( B  4 " z O JFKx s bq~3>&[c"K;nLX)Xwo4hs=B6J.oB~rn! 4 A |    l@ ~  8D t  @ m lG  K f F D  & .*;g*I1"CCRBu5BeUySk[#&?_04s?|l\luk < 4Y T   / 5;#z0sq  _yxo   c }^|VbDV`Mt='Ei#T }?x=]0y@ dG=w}-bd  < = m " E s3[+ZS}?09 R}*d   -/V, AF>WE'xL\6t 1d}+O~$}Xk.2#O4#P5 >v l bWV '  j W|   Q & +r O % s D 6Dx(YY{eatvu`^ir;)00+F-rm"aQ &:('Vbqa:KAK>^+vi  p> ]  4  c]5N  Eu$\*8)8z Z >{ oe s^8 $! 2A IP ;Eu6:c#.UUMUe~Ia9 ~o7] m= + D 6! T lr E d  'Q$4oM]- g1 c y d . xg S 7 Wt  y   a+0ReOkor+6mMM?j[Fo?pu&/)   B < U T f n R eRgr}qvU};- E=  g(q!c#pz 868KFt ) cfo9):_XpnpS-   j  OGdikut 1 m [$eTdM)a / t \* |DpsA`SN %G~u,wEpmaZ=zR0BQ`I]g2=1 [ 9Qu_^B K e L 8 j  < K*Nz@| k  q CC vn/d@B}t"r$oK1 K$9o Fb-26   11 gU{)eH f  f TrDFlh+ n Q $O -Y/ ]( , '% T^|j)Z)`qb?@)g ";C=/!k`K `P 46 ] ZZwMk \ u0\`Oby$U{ Y P| Ab . B3   * zT ))C:)mB$=m lTdb&pu3dnRtlg9A3hI'W.&;]  Nj.&(FAu & C m a_hH| \ R 4 &t}< 2i DX3W$2cD#4!IM {?5w 7| d:|?3 x otMRC|b w P p P   g p g ut , yp4 / HR $} sI65?`\ Z%O|K`wh7z# N]7,'`AYzy: <, d t| 1  W;  I k ]= z w;~4PbpE=|L0h[O=B^/f0UG4"j,E   E  b2i,SeZ 4-\L+d#  <=5H??s^bep_VO6E&4g:p. \ bfxz wwzijX({V T   y  :$Y?t8 ~Gqz_  VY \ ,U( 5DR0 KY{#@ZmB/XETDH^ p  g vl O pLCII B ;:2evflkGs= g H p%O0 G ! W . I:r 3zGB!p; )3dW}E8XW PUX# U F md g* \   3Q Y T v : P4 S fh i=\~<6D?$E?kU{X^,0(XOY;E#d7 M  r< +E?M " i Sv6 I  ] Ml W  E#\P=Y)u%dE+)3J/XoD%m:)i5E,^ t 2  @ T WV:T `95l}] 4   QF)J~*vZFiF5<y5SB!I#M,RP 731!(   ^=1 64 ]  w / ' GI Y +Mvs9J*b;iJB;<ts|T24A3/ ~)3Ejw[I;Iot* ' Ry=   _u">)0  \BhDR 0 "LU L  -  l F=  5*No05By' euY1S!.LLc7  Ro"IR   d( 6, D4]@y G i s V6zY # P2{A H$ R- &, <pTtR3RA7%pQY6{CN2y $Y:+tM I | ' y w x B    2  =TV2bVy   "   *5  f Y:b?!{l3Hz~'ALaPaQzH,m}e7 c.eX2 Z nV2& <    j^ m:* ( ) KM(x.(.w>~c7 7{ G6 2q c  : PKn/H_eS8IzYl| 3:v R>iT<6E\M C  } 5 Kr 7t3R0@H bB@&3_.J5Gjz5 A 8 p:2gUkMiz X &tN|;VO'"  Zglz ' Y: I  _QCL\Ok)W0 3 My&NhY.OUR(V-kKrU5{(0Hjc#:@ ksXbiZq  I- 2 ( ` sn d.Q  a ^{h!?L($("> Q  5 }75C#@.XUMWHeHT/'  4 S5]Ce -`  Y  N v   =A a     J ?T"=qUZ]UjeM_%8,"w0S4\R *dFgkzA?esI+S*8=q q G g [ m  x 3  B  .p b    .BrqND,    $"o   S 2mU+m[CicaE!dz^?~C|:I8% ,V0;h:0p>! 46 */C o W g u- p`{J   & *  ;D l g m r =;I?B_<7 1 YZCE cpQhJ_/~q/ r?IXTW, $8`J Kz%A &VQO5T2 y 6 66 V I  xr  p+]gYOhc / i nldj?#,DLvlj=Rm9~ Q9)gcKQd3CMOz9GF0u"9^&S|]u _  W p 0 7w   P (4Y-^ S $ *  CMWycQ j 6 e ,QI! b+D bZ iIu`AJ)lWR1zkqMaVG3g2qaP5|0@d/! 3 [i (I M z & T w = | C ^@D y ] Y+]"1 [O ! 'NZ~en6I psc d|O<2&E#.{rm>s) X+:xy[^i|gb<i j 6 bz?c 701Qd/|X8|ha 8 F 9 x{ (  P   4m      KXT!"m-Vt8dr|5"s`Bj yjN.DSOmf72*];8  % G L 6 _i3   D   CI .   s BL   'D)XK   K  ok. _3@~D>v DsNb4{;42&UV7Ap#z>SQ;g  B     c m . n  B Lx"8V y n a   U @hQ2 F j=9S{3 .LdNy <[ &x{oiDvwZA#ngZ|p_1)r*$2:} M< o 4 bb"sR ~@`l| \ ~ t.F7f2G0W:p\c /.oDP$` 8s i27R+=""0$p$0. y lJ&Z*C?24=1TN&3hvN }Oxp@Zi8|V*m7  >  J g  Y5H' n4.^bgG % jYz/%fs4^}7Ed>hKvM_g.kqu \9  x8 ? e ^ a l F (  A f G^ye i  $~ 0 DO o&ES > TD X}8$^<|Alk.' 2#M+?HZ{"[MGu7;U5Gn/;Q`-n  ,    G %e4bEx@ D#+Mp,`G ` L w"6y Kj  Z1^Bz[5@Ft%ZifZIf1E' uc   UB OZ f$ V*xy\dh0,u\( PC6-Yve9 I u / C , Nd/U5ASUaG4%9H #x gS; 6 j8 F P ;#L#< 1C ~8bjEs?j)0@@RKY|aaS;/;@ Y3A" &S `/ex   Uaf ;  L px W6/H 3}x[-g ) ) fd Yivm4KZy]xXHz}Y i:/CLK7* s6"v   -%0 J Sw B+I!9#ctQ , @   W8 L\=_]>{3vhG5E\^^Lse ed?0@#e7we3 sOkmyL];0| h<]|a*Gz #%  D <  [ = C5A|#> I^uB(  _X / v8Y&u(<ww w'EFTll=pY+V3X &  C ?1]gvrR],po3P? @ V)*} 5+c & A  Y ?7$jv8J/ "* { *  K n  Q 6 <A  N ] e) ( Nc5  'I F])oQuFSjA5?}r>GJ2Q)$h k(* =uU  ^[oTlvr8W$  e_7  Q r _ EY 7GJt2UyI^ )v I`G7oje#Zm{0#ob\GF? n~{#$h6w/ C M 4CD3F2VmDCj+ic4Fz5(8" @!G>_'xB` /Wdx\s:)=2:$:T- NptfN{u 3xc v 7 XJ  ;0 V L ] v [ FWdUV> + =WY 0 YVqTZ ! DRNY'4 hg3SXNm\K(q*=4Q'D1 Z CY, t ^  2( M  - g 4  \ZIi 1-a ? ,{]9E7i,bOC: -(:M=^(UHU+sv; 2pr"\x|A ,7 xX } )N Iil s Foe    ? !9x%C a r6 DWFMDE:_LuugEqqX' ~Vte9_ aqZJ *^ "  + l 8 /  l +  D3 Z T<| *F  d m  1 c 0@JU,h)7r ^`8Y`~8 4eBk\ 9h]<3=v<    4w ~  & ) Q  @ H %uGs5l m ^  vK  _ S iBBYaktZ4?Eqd*ha.FoF:o7[~M Ps  1 }   y EX vu S~;  l  z C  \ W  (0I> Fzdo9Ax@ k^`%?wvp*Wz'[5  t N 8 | C O KF2Oz u ) HxY  D  T T"YP  <  yk-[W0?'SB\&6UK(fb%>Vvas{0}6;G'X #  a _ K W L r @  28b&iZ [ z4@ x |~(sH] H y ya `|_QvL7 s8o+>DB$j*Fp%+E 8  q$5  2 J - 70 M I ; M/ Q ,  uz )  +  { ]_{S sTdd%d'EVwPTNl _)*H5G{rJyV+t_  r a " = U  Z : & g 9 f P S y 1 n5 9_ r aFv @ [ b ;? qk:ri7E1`wX MG_ $ FZ !!?^ f6 3EM6;Y2T? qTs*K!) |YM!ru| ^   p L  e  GuPypG8#+sz -kFAK/[ kez/rpOLcYai #:;?^z_>iT+3COGkcM d o   * *j  O  G` W  2  " :xc A: | FlyFHi Z"-1!'> |bUO}K8gMk_p L }0*=lT4`a1 @  b.  7  dd~  H>$Y- %eOH Q. ] _;i}  bu?}~^2)>8h+xYicIGRh X ^\  -    -V  9  r hYB*X #  q  + T   {   $;"yPH@E^<y?T=6.(M^f1iM+,L0? t g 1 s s o e M 0u8?yEJ*Vm*u =vi]gjx;XNb8@P15O+)F>LU#V9v  O 8 x \"R' WO o Q X?T>< T  Z OLWo|KoYf?%L X_85g(Zc 0 _ U  2t ~c _!l4 DS:A m; ]jbZX<1@eu0T#re[b;%EWiJRt5 7A\T%]g/?nu+47:Q ' i# r & T1OI{  " w R%L_c %t ` 7 p ] Z sEEf|c8` xT>5GCE |bhxv2z^K2gX\ r9>   UNVd/ k] PEQ( C |zz  d  Q p  5p =f[di^RRu"fNIXRw44T/w-z8t<\ 68ag<,25DT _-Cny8 + < L^a+SG y R Zt } :J<79wh`b:8k$X#@{inD=TtcO j /'mTFa 'r4Ek  U   K r U a I R X | &  10.RO\`n}}MGI iA#|fMJZkL{FNL4${\ p ZOi/a !k  V ; M k 9  # yWUvAgl~++>BStz!|FP_N7ZV&{VXu SDocXa}K   )  Y.jF~~\*L }]YYv[(D+-D L v>aq;*LCf}keU9F:A1\!/D@9r. t-|$x7fc%1U(xo 2 { u e p&(]0n/ xIlz]  W` d) P /4/u,J ]Lb!GU%'Nu9&M;_AQXZo } % d p[ 2 ^5Z-]; 3 0+rJU_xCJ 6; -  u y _ ># J~ c{ :" `Mz6+D U*f 6&r?Qt=geH o2[H = ? \W _ D  r 0  I 3 q:pko 8\b * *vB[fO"X'(,< U! (  Q  mLA r+.(x !1 5% 32 ElQ1%=+{fo*1QPAr Z83iMo ' s8m@' !N  6}(& %==) e , u = 6 1R @ 'GlyGW,vKvUWW]5|%^aHD`DYV,8VdujW?"Ntu7 5 i^&  # ;aAy5A   6Rc)  a): 6  &  q [T -K<-l<=P)90E{\sNHxn>5L - 7y/--Ka   f& 3& @ +P+is + HWz uBnR  D4m> L W ( Y>jkJNg"V018[lR 1[9`e Qj,oS S  ; ,p  q G    7  ;VkW(        "_  l ^_2}M"/m!.a r&u:kQV[/Q|V\+}Rs0a;A j5XR8 H m?0! }J Z Q,  < Fb[Y & w~W"!6. Xw f/ Mn { " R gRF_ePT8N"u @Q!NMy'-;!' -  x 4 Hu 6hr)q.%,7G& ],y%h8dL F 8 7K viIj |" -["P]7M% >  raq Q - } C U J  VzT 2D_Rh/G% 9/d&2SW(NtH)no}v@61 :* ;M#PA{ v [  #Nf j X B? 7 6 60 q J N+ W ]ar >3 sW < zf X/EOuz 8WW}u={=dAFBM[;J4)tdvCm  S }  ! z mW{ 1tpa I B <. FB p[:*"yQ#MlQQdi7'omz#KZ]yTt_?M3D(DhRIV9bLZ'3;J w= C6Xm-j 7  t^ Og  bO 45xpyR Q d2Pm c 'v  0"  - efH"P5x;AhY-5C0qkRc~`g=8 e@ W%FF  E  |  c;*v * _T } 0 \ }% yZdJX    g4%hxE:-}J'9Ur6Z}ASY8h"9  G{7D5k1hTes4 x } v h1   } ]ZpoTr ~` 1 " ? % 7  6 c  Ez n ~9#jp7pisi3g}Kug@"sI8dt%kr*tcxU%o@mwx 4gT7zKH$C * vZ c'+B.=oh w ]  [ . ] T*   ? 0 W Jzdf   3$]7 .. ' kL   zh}&w=wa{/)+mb$^2<^ ! Y8W ]2/z Sh'A[N Ta W # h #" r 1HZ  , m az }HC;,'0{6   }z 7NiNdq -)f',CTZ6,>%q02zZ+~Y37}a5:Gh H0q  ^ )KrX9^  O 4 r    OF h     HH`#6fEmY;0k[d%dR%W3X<M*QYc5:` mc QGU{8 $h ^ ] v `  O 8 %8  Nk ,e    AQz_UH     =|}k   b @_9v5DEhJH `sX2n?fu2|1&D>0v/K7 T0 %1)]  5 i] 5&  ; <  UO  k h  j  0K9.0MC! / OW!Ab9`*e;8'Xfj"f c/9lL}IG\0 C;f9dT%  E "L P  1 . T   Fi   * ! PJSuw\wjBqnaLeXs p`'nap1mfHZU`]1@)\ CD  / S \ I" o{#K    J 2 Jb $ <  s Bp 8 $FDDs\   a  |s7_(8;[ T<d:z_o 9} 8g;G%^ .t ^-,O"1"`!%) m o;$G. GE%D#\~J,1_tJ L sOhn@[4p;  ?  G n $  c="T, o?'hbtQ 1 kUx'%bi-Wy0<\4[?kKRX,po{*T> }: I  h e d } Q +  E n L'  gm j . 3 H` v,SU B dT Pz:Y<|@kk (% (L 4DPsTH< h02L+Gf,5P\.p  /    J )j9eL~E H&3Ps1dN a S )8uGk \0\@wU1=?m*WcjUHb/?%lV!z  M3 G\ d UwxZ^e4 &rY& ID8+]vo@ Q &y 4 E 2$Nh0_9K^\jJ9+<N )}bV4 : l9 H  O 7"G!7 )B -[dAm7g"'?4DX\wZLod]^='=^/rya3gDpi{PW3,wf7dwa5Ny {-0  A >  d ? N;E,F Pg yM)a\ - >_){!>prz:BM^^<iTL1v{V % > D*[kvwQ])%sz;R3 $3 { 4  P n   W 1 7A  I  V Z0  Bd'   K FTh{@lJF cB.9tj;FG)R,#eo*$D||\ " TiL{v3 V3  p\E  \  f I`>QK{2Y|F_ -o IVH3dlfxZdz% pVOu9?9~]{r tza2sx/ C N 3<D2J0ZmC%Ak~0)pb4F*{B)I/ A&0LCq/|If8_gzcvA)E,?%?X" OgyeSym*kc r 1 JH  |3$ S G T n W <T[PU7 ' 6TZ 1 RYsO[ *  C RL^49gj@V_XwaU4r)G6Q.M2[A\0 w [ ~ '- O  # i '  OX>e)*S 4&uQ3A5a"_EC2%$:F 6U,ZRR% |~:4z~&]F' /< }c  -T Omn y Gwc    C %1~)F X r+ BNFBAD8ZNlmj=ynpT" |Vwe5b fm`L 0Y (  . h > 9  l 2  E= ^Y9 *H j q"  3 g /BNW{0d+6t bf7S\34e=hX6c^~:,:q9 v   9s v  " % U > J $sDq3p w [  }L ! _ Z oCF ^fr}Y6&GFni1kd0FrI>s:\J Pq  1 x   x CU vp P~6  g  v E  ] X )6I;Ovan?Gy?mbe) @yzs-^|,Y9  y L 9 } ? Q OH.W{ t ' Js[  G  T P!VM  6  vc(WS1<L;T*4UI'ga>Wxbm|34> G*W $  c e O U O x >  76g%r] a  y7< | },wI\ E # { yb ]|^UpPz2 u6 i +==;  h&Cl (> 7  l!5  3 ~I ". 52 L F ; N/ Q .  y~ )  1  y ^ ^T v Th a&e'GUzRSPlb'*F5F{uHQ+t]  n d # 7 S  U 9 ' ` ; c M Q t / n5 3] q ^Fw ; Y `BAxm7pm7 K6^uW @@P7QA SP `{' G  s   * n 3W u d )y@   ) Z + = " L +  / e  bF d n_R?< )]y!aF`+D9Jx"}7 } EnwFIo]/3&'C wfSRzF:hKn_l~L z,+=iQ5b^2 B  _0  3  a^  O= X+ $eWG P, ^ ]<l  dyA}}"`2-?@e+{[ff DJX` Z  ^U  ,    ,S  8  neXA*R #  s  ( T   t   %7"xLECC[> =Y97,+U\k0mP+,M2A t l 3 o s l" b N ,r4>}II(Tk)s 8pi^bhz 8YM\6EL28M.'E";LWX=y  O 9 {  ^(P*XP n S RDN?: R }" ] OI[l}HlVdB%I V]41h'[^ 3 Z  S  /| |_ \ !p1 AS;B l= \e_]\<1Aes.[%#r~iX h;-KZfAQw57A^Q"_j2Bkv*19;W  j( m ' V/NO{  ! } KU`~m%v b 4 q$` Z tFEi{a6a |S@3IDGvgnyx2~\O5iZ_!v8;  UJ Sg- j\ PEP' D vu{  a  L l  4s Ae_cfaOOxjNITUx36R1x.z8!{8Z>3hn5*88@Q [1Dj|9 , < K]c*QE z N~ Vq } 8I ?14uj^`72g'Y>xhoA?Ptd|P f 0%mRC\ %o7Gn  X   Q o R& g J R \  &  6-3WO[`sJ~FE e>(||eUI_eLxFNI6$wY s VTk)^ $j  S : K m 8  $ zYQwI|ki~--A?Vzx&{GRbV:V[({V}Z{ PAhfY^J   '  V2pI{[,L |\YZt\)B(*F  J u=^q7-L@gzn`T:D8C0 \"0BC6x0"~{.'|7ff!"6R.yv 0  s h w"*`2o0 xLl}Z  Wb b W-,/y*F ]I^#CT%%Mu:$J>Z@Q]an  # d q\ 6 _ 4_. ^< 5 ./pPQ[~IF:9 1  z w a># H~ ^{ > `Px=+=T*j :{%s:Qq:eaE o1VF'  = > ZX ] G  v -  I 6 x3qtp  6]b !( +x@WgK%\%'+=^^&FoQr@MN~NEm0zoU/  : /- s]1| V  /C < B  8 A=-s  5)?ry}s"5 4e J %c" 5&-DW,\C; V% +  R  lO< u.-+ ~ 4 7% .5IfN,%;*~ gp+/NQAs Y71kNm + n5m@" !O  7y(* =D, h 0 v ? 7 1P A Ig{DX+tJvTYT_4{ ]fH?^AXX( 5RisnV@!Qur7 4  gb$  " >_Gz/F  1Tc'  _(; 5  &  r XS *L=0k=:R%<5C~Y #tKGwk <1L . 5x1,+Jc  e& 3$ ?-P)gw ) IVy uCnQ  B3n? M U ) \@giOPcY-,={_ oN }5Z<[e Pl*nR T  ; ,q  v A    7  ;YjR'        $b  o Z]4zN.q1b u&w<lQV\2UzV\-}Ot5] @C g9[Q< F o@0# H X O,  8 Ia\W * xZ!"6- Xx d. Nm ~ $ V cRD] bM W7 Q"r AQ"RK{* 18$% *  w 6 Du 4hr(r/".4D% a&~+b;`G C < :M xjIj } 1_"T]6M# =  q]v P * @ X G  X{U 3C]Te,K% 82c%4RV(MuH%mut@30;( 5 8 :1 n L J, X `_w<. x[ 9 yf [5ILu{!8ZV{x={?aAAFJ\=J2*r>_4UY$rNY@"0UWBw+ s<b_yB 21 {   'L D L>byHg N |   | qWv7su_%K A ;/ GBn[=%"{P$JkRUci;%rky N\Y}Tu\ =L5D%FhVET6_OV)88J u? C4Yn(m 6  zY  Qh  fR 2;}sxR T d0Tl g )t # 1  ( heK M3{:ChY.3C-tlQ~^ ]f<9b@ U"JI  E  z  d=*w ) cR | 0 \ ~% |[dL[    l6 htI<2L& 5Yv7_y@YX:h!9Dy8B2p0 eX`p0 } } { h+   |  Y[roTw xd / $ ? ' 3  3 _  Cz p ~=iw4okwg7f|QyaEuI;es'hr*ub uY r I-NUi-6^ nc&MDU}3 !e a ] r a  Q 4 *7  Ln ,d    BS{d]@     >yd   d ?^<s6DKiFH _q [4k?iq63!&A;1v/M9 Q/ &1-]  } 6 je 9%  7 ?  SL  f j  g  3K<+4I;! 5 TR">d{;d(a<4'[ek$b e*8qH}FM\.F*|._>*T(` ' y%,=bthb,Grk\XuD.n&2 fcY4{m# D*k}tq{bRbvh]Iy+ u G s2Nq#J=;g9dT$  D M Y  / , S   Ej   * ! NKQ su_q$kBqncNeQt n~b%p_q3meG [Xa\ 2:& `=D  / Y _ F& qx%K!   M 3 Cc + >  t Ar 7 %GBDqb   a  |s7 c,;8[>rJKh  P     l 1   V ]z7IS n r   2Z Gz_D K g=4Xy1&EV?j5nvY%idn_/dsF>q{nH kuh ]++g*56 T= t 6 ih([  AZz{ U z n?ACeDO;c>{\l =}#8j>I&b /r ^,-O#0!c!/lm?"A4 NH#F#\|K+1`rK |O |vJglA]5q<  <  F q !  b!<" W-# s A'kdsO . oSy&"fh.!|Vv0;^1[>jIR] %qhw %\9 ~; E  n m h u W ,  G n O& lo n ) 9 IX w(KZ J ^O O}6Y@vBik)( 'L! 8ATtQK>h44K+Fg+8L`+l  0   H ,k6hID I(1Nn7jP c S }(:zHo  [1Z?yV1=|Co&XecWDc/@&lZ~  Q6 F[ b Wyx]Zi0(u~X% MA5/YtqB R *| 0 H 0-Om0^8M^XiN5*?P ({b\8} ; l8 E  M 7$H6 *@ ~.]d>q6i)<:RCR^WZ91=> W3E& *T $h3m Zik <  P {s]'8,S >}{T2j + " fb Ybjk-FYj~Nrq}MEwsOb8,?GF7'{ <'z   -*0 N [~ K5M%C#kuN 5 =   S9 IY<[`7},vaA3GUYx!ZNme_[@(:a2nt_5lEkouSU5-vd9e{_5Ox |02  B ?  d ; P8B)G Sd {K*bW - ~<a)x =orw7DP[a:kV J/uvV $ > D.YlwvQ^*#n}BO4 "1 ~ 1  P j   W 0 9F  G  Q `1  Bf&   J ERfxCjKG bA,9vn7HG+P+#hl- E_  ';4Q_`( V /Z -  l4  l *w by' #.c 8q B0^   n  Cx=dtJ!ecCgMrBWF[7f:cS\fmO Kj# Zm:0 "KnANj T N R5CI  ClE?k   3   ( 8 R w y:2o0z0Z[[ %%3E`FwX[Prf@`!jn , F f   g }j f cH !IrN & 8 w }%g.JKQW]nCc>\K663C~B[HC>Y=[DP@,TtWt%/ 3v-iNX)  J _ W J / )  c  BTs;J98 |Q  GD{#W l'@OM(g]CF!=h+y1# E ,4.-;XGTaK~  } s\IS7!U|O ( % D WN  ^  }Q}\^0ox"gXq(ryWOX+~ vH+1KZx}% r0 "#M&*yX 3 %1 Y :fq 5~_MNV+RfONMOnuraUi> G\s Q5RYg}Q {Tx&,Z*[T e.yqviEBR  h  zfc]y %C]T\9>NkG[yU\!(xMMF :-6g`9kY&v\% %1~pG,  i  7    <"\s%FmS uy^RB(@Z])}16;aziwU|D }LDiybHeo%R6o7  A Ya'pRAu[Z9y l +<k9 Bt7i 8zjs jA %[7{5?(Kpcb"V##T , }1 l X ^yxG]iS#XS|#nsh8HP4<i,b7#|Nh V Y9 - * }Y H0jPv!4uvXPR3>}8To]b;T.Ay&i9Ka",YL/+{"M,rj]Gg|  ;%$[)e"jbmRA CXI5x7&Mv}5#$$S9}QbI#C?:VX.T5aRCNBwVHe(TGQ;< (W # e  ]J@^;F}j#}d%|4:$ %<*4,XXA jX> 61?=" 6cz&K&<C.  FQvb$h{  g q#-bm/z 9 [ I JXIH " )H\l7YxXrWE_ ! b;5 3E  == {+':FL!f75W hrMX^RQ%Y9d%m0#3f,y' i`P:8 q-Dwk/f|oGdeK!8d!Sb bS z  W#,(=[T$eJNB^EMJ$*Vq@\OQ\7F:;EH2h1#}XL%+66XHYj M\tJ<FR17-m L  n 2 y  `S1vz & Q e  n o  hyQVb1* z,D;Ej+pwS,\&^Q"G|G w  h  9j<7e|#V9L J l5<VN  NQ _  % \ bW  d [ _,  > |Dm]O<ig%Z{^bz*DjJ{k;8re9$b- :3CvCp`0>V6{n;i([+u2kp i7 u  B : h Q k 2 V~Nl ?PO9q>WM]%TR`|4h,yZb*YOIgN`C|n|>>r0ypSj-G'ew]t3&<\A    z G 7 h&GQ8v-[^r,4 5Or }TzT`5,l$uf)DbKe["Wq8R  9 # H 5nRQVI:2    f [ ku (D   B E m "s 3E 9" V Gg^89jQI+A/XC0}(#D?/E4{3.c-'b`8O_*h`g  $D"Bz &_ >|]ARCo;y o q X K t B -2~ ERQCO }[Q5: QC]M nmz&KCgYGo @ $ G]  Fu M  w7?GJ `$@YdaR/k[&}S3Sa?ULpG    O  *]H3AsW[mxLQ-|rS_Y& K{ljs5\@HK^VQ6e{~N.D3/!:qW5}+L`J1y'r6Gs zv>`Os.zEZLncj) s"| 4i'R :KJOF3(O@,T G;:Cx c~*kl/dDzK6ja+7`{$&cO) b   1 S3  al  8 f hDL=] @XtuT,o v1vo) LvI5<&Nw/)@cXEr/*yRQ.*Zzn;NlYRv@v 6g  {_ _ q ] `  &Ou]ag]Wmf# 0 v 8 m D r?m,XVL$WS}Q 5,5LXX_dBJf9A#/8JtmHJ ]PFtRZ6?f   i Z l=/8Kb?He^P#fDEu( Py${^^ni@ m O fUZ  T@M]c_k   ? z X( RgU"T 299\ 1]9*OCR48G@@+!>k.}Bvy7I$7h@@N(HsQ=K ?> X; Qb 8x k H  8?zX i0\U 6 J8 JA Z dg?XM#iL8}cgm aU_,zc(hC+{8t yr5R ; >x & ' '% w#O dJ;x\  e  R Wo 1, z FS +i_(7\7Y~0tj%{!@Sa7%m2Q!.r,~E1;49^1p2(S-xe| ? w 6 {e^>[) 6.hZfR/Qev C ]+_a}&aVfbbk7yz7X@UI5q0x\ i p $ z kz W L Dq e$a-UR#}e G # 3 G6 # pWl<]FSnDiC,HW0!@.[`{9NM; b"o ^ < 5P    e   ?< 1 l 0  8 d   T 8 = KMPtKU ,U-&,a\Qu~f 0cA`u ` u%4Zd3a  Qe:)G i  MQ`+P Ol J = 9 . nrrnBM?VFNXPv He$XlF=5&D X BouU0B}*]Lp$241 \gu0c?D8(NZNb)Yf ==]e+?pw7xfZ V C ]   B ] _  x/zUUR "6$;W%jF=o{F?[0u?6)Rw#8zM}LoL%v8t5Y0A>f /kDkWwVV A/ f G x  n=W *o  9 M  8.n]BoErXMzM%!g$?1Q[LrS^s4s9d+m4~  biE| x ~|8u6|@ I    _T $6B8 n( %\ C$A.l9 ; julzF]H |/oOB@s~flTn0wSV&8$UO M m_*D<&1 {Q C BlA}G^We+ f }\~D']oiEHuX@|QgkhM-_X;9MbV<5;RU2-gX8]/{ 7D q ;   Z b a  g' P  $ * A *kWb5grr U L ' DM O/6A XR/P84ymY; }`0#9JnP(X7 h 2j&K- 9  x C1 !4P * y d 1 ~ L [ * %PxzfB"@N%kQoMgbK'SOcaDY&vGg^'   [$   ] DL#  P  @!^5   )O $ / H p f z QW - 7  !jc[!HcLn_9!"ne\byO,H^u!S  c 3 z 0 <u HHrIXG|6Z:@J%Jq]TRVm,Yiaj  \UTNMB   q  9 0 jz$f;`1G1B x } *bFwzD}aq Tw2p FeW4s V3/f;=>|ZU>|   FZ  @N YE!Q)9e'Tnq  H H   I $y  ;2[P'0[ @XiaaZN5iP)UoA YbyaosPYI7E#Z E^ $ VF  S _e  lt }  R Rd + E  ;   N?rrw.rVvz#J -g[[7 M@PY&.9l#p]&WY3+   }  )  ] )   ',  w   Y*3vx" @    uw }c  aa4<\A_&r~T(XXk*SXV6#pO00  '/} Y (    S g [ pI  jv}E2P?  K  ls l 3*tg_[IG#7pX.fK>>,uwhh ^c%qjv= a c [ EaILOG+ ` JP Z Hw, U % 104 X @ OLw"#n )XV>[hJ+Iu!Z 2r| 0_REmX<#s_E`xb . \I'K 5 w M,c* N5r j <    3 ^ E QFv>I6 ?xr)8V| rIURJqam)5:?vNr }VBo.F< j ^  . u o D1  I32 !a    %   S 2 7F|^l' 5ck7 J;'v\.YKQc%0Zs?\7FV+WJr6 ;  E k !D I  e%w=BI    >> `  P # W  F +b&+W5`eiZPE0a:PnTl4kr _59fe^I/g4#    s p " 17bb U   HSRGbl a i f WJ{}~0JyN]|A|l,!kLx{5U=Z`U/#6rl! =  N sB@BK v \gSe J 0 r  =Yrx/4X}%QZyOPD ?p66Komzyrh+O$2 #A  Fw j =KDD1z:r\u H + k N   K^ `;_4 ,j~*R&|j2Ie]\&;|  0 # S - Gv]GO3&  p 1K " ? : J  {5 6vV}.|TJsR{gV3Xg8Z"8j" XX0R$v5\0um (sMx7/#r\ ) E >n  u w  { j^k  3 J 0    =g    C( z , ,&BM|`K{\I>3E9[~"~*_:U Zn( a7   q :_ Hy>(. e J Q X   d rU3U \ c n Xys%\;=x]Y#{(x%6![=jyGJoSki G7WMnh!;Vf9@GiiC};EG@' bPI86+m7(=U56[xt } tdd , PA  c UR]I @#  CXH O j # @KHI])i z0E^?OfOul2  Z `  o c)}@ T0 rw0 D FlF~jC^ $" y'  " BDYEaQOV8+zMH(QC# 3F"tgmi wc  R  P TtDqyL`Is [e' B ! V 4 % 19tSl6Thg`k-^V#H^%&V-[ZCu^ ;i"{q?tA /   P g    A _ `-!bRK\[%4;BmLXbW'_z'iFD= <@p 8vgZ / B lek%E b    f*y   . 3  "WK<(h{g ;lxRHs.m[NOd !#J?gIX^f] '  vn :9    o lD3zQ  e 33  6 _ /y<  X ^  jAIk?CWa*V_2_T[D` KBZxn6u-qSbp~iY,w++(* 6 9    x"xjo   -   9b  z  D 8o"'WK" [1a^M{*5wwWIYfH#m$Ew$NJ^C9k/' }|*~ n  x'\ E Og  8iYxLsn3: 5e z L y F v u | > H m  1 /~s9]'0xMLo W B, pPR(j9M%gSC/1w&SiLz!=U3b!Owo3csN Q = S  `1 < A4shVBv]b 2} E@#G S I   G[n  L  VW* i } G g U $ )> 8  K7i_.[[iVfDd}Vw[EGQG Q\i!M}*uf5?}4-  rlCl-: Hn   lh kbS>I Z  JzY H  i9j 1 VG  fhKrV y6zexi5j&&rM,`BE, 5{ntaO0 =V  g[  v =y  : U 8\Xa vb5_ id*c1[l%Lfpqw@UgS_@lC>?sv_X5-l:>8JRrJli%=Tu3C |45  c$E N'T0&| F s t ;   z 5j  M i pk:-8c> }`gC,3rBSQi5e%$4 @`Fl rnb`787qfdF 21G/e8PMsZ `S-f t   O `d6~!'GOq & y   } r S! T C -  = BDXEb-  TV~9On!^mrSDblw}?+1FVp4e>D`|4? Xcg_  K a z  ^ M ~ ooYp' ; C $ " | d b pX   bl  W  Q3  l ` MN M\EU6kFl [/R}6EYQgQ:OMmh 4kU R .#Ld e  ,|H   `S { ?   q I X g X\ /Y' 7[0S wIp'"d1ij^we.bio;'\$a_kFmh"!3 [U]24<(   K  #    6H$  c X |1 M a  ^W-i=1  l8r'Y[Y q3~7OW 3ng2JT . p mh8=D\Y5aYyR-  =g` K J o+  b Q= xH /z8eyH  j p x E 8; 3 2 7 A r *  ? 7G H F(EBZnWo4]E=GAzRn;aR].kLiTL/ r $jw\  Bym_  [@ <z 8 , 0 Vc4+GE?b!x5 %jq4_.uLUqv_k"v fn4V-t-K~ \S8u#Id)H:mO> G5~3!*,i?%mN HC\}R*za& U   :  TyU)QM(E!02tSy{EjJ1  U    ` R  xCu  DN B Ni> >c i  )d ^ x  f>/ON~ WvW   P y /  ) [ Y   =512  >  ^  u  9V p OA7 I.9_iqZSHSWX ()do1  %   l Z   Y   b /  k Y a  } . EG4Va 7#orIYnruIw2z3 R?)X({(vd!Gtm hntn1f { | X  w / 1vWU   ?     O l "V  4x}<(mx\f-N}U~QDz-a 1  k  ^ aAHLp {p Mf t>Hp{  Ch|d!>dgfxq?v,"GJ_mUv'82t)  ~ R  L+ H `lB{* d=2 V_<v y ]?Lt62+*q }5D7B;+zZ]D:-k"%8W)>&` b [ - 5a }1 zg  `    !  T t FrK[Y;|C4]=`[;yKo@#m; 3.v*Ql [-xA{  >m1RiE~fE V \ `F ! V   B$nYJ O (D1isyRds;whH|K{I8:9}t 3i  R A ~  |  `&  ; ^}E_ u 2 #l    ` S S  {|{+F@/ l (sj: s\(HY$0dISKTs   / % {M 2 } mBW##Kc)&O+ O X :$hls\`}VqS0w9t:qXRY'9m(#4*8  k Q m y  [89-m5q+ [4c]N J   # l `Zc);z&HZv @M4d"aJ&?VRd1wlFlC02j( ^ % [ rMo Qy  * N/s@@)TPH:}#A"cSme$4`mHwM1S6(AV:dM oN) Z ^ Y:-a;Pu"(: R z F\`~$o  i  * #:,  k -aF`9F(KaQ]Gp`]| 7\Ds,#e2.( hj h O nA&Zsa I' /,B k' :>  )  X}  0 A   iE#B`! RdClOI"g=YYW[%}.u&zuHqz{.J->ImLiz Gx bRZJ!mg\E3ITp m C !o2EnY1PSyt#<VyQg^#qY9. T R u ]  `%0 Yx^ Z u  ( % { m x  ~ t 8 ^80Tf5oT +$Dg0$0Mo^Q ?|z7CQZ9u2 .Q; Xkt.w%B  #4 r @' t  ]  {`El2V$/BdJl|D Qx^YVq, LKpsPbH:'0 N  t  F`ue5fh* n ' D N{hG(>   vM*p|/sz /=1-iwA ~2 wOc*iAo.u"4:TE} 8 : 5  ~v')i,s { yov7mR Q @Z T ) VI4&x&( m d34;Z^qxU1uR&t}(%G,^?[U} `/OoQ U ikC )  d+1 4 Bx  l Z bQr k  oe+Z(ftPDzN@1@ Ai|H\}0}be{   05V: Z + .  kzs]v a Mq @ f  + N S " 8~ [v}y@7f'Tl}#x,[Y lfVR2RQ$ / 7vjrRC1 +  [L & V (&   n M QToy2f \K 3 NnBy;Uc{4+C_4m"1FN2Cv;] h "[ |I   F E N A =- U 2en:  y  F B '] A u  s}at!u3Z5ul(ULt^vCd{QO vk y i:~wO~`^  v  7 M K I Vp;m=bzR9*YC ) ) 8JWBO1N_)?):pm |PguCmdKGwh sk 1  : T zr-Jl5i$@q2*:yc@>+ +}R~HSGcLvc 5*msyP  ?MEnVzTv  eP]{q (92 ]C u $ F q}^^Zow`(|'^4 ?=`r@o#& Uf0gUR-CpvS>qX&XO@M9  ;  0 v w !   h2@  su ^ p  NUMqvCPy,  g +[8 l}"(Oil/;A.L^)L6NAY6T04 [ek(gb)Mk&9`UvmY{g?>_  . J  +n 3- ':K@{7 P  Q [ + Z  l Uh3 @ .2h ip|#1QtfEATpH,`v)Ad9!?5:&9*>/= (`Ek( pDKc ^ =  wm  \6S 9 eWl B   MQLh!o2'"K]7]?KGthmA\a,}rz:9u +K(  j 5  V!*t`+fV' be 5 i 3 ~E R1jW R.O:;e,yl@ I|dqez++iS/EMNN7 89 I g r | n  *w ( ^ 5B;Z k i 3  ') l  K w & 6  v"e%^<5KG.ku|k'*BP~Tk7C k.:I5}p8zX)huw 4f(2cz8  _ Z3;aytbj -   { V  O 4d%*z y   : 9 ~?_d  & iCC 9V ch AB d  }xR8x{)~6xl!"w_cD-T-/#./k|  N-um]9^7  H= Eo n  ap  a&>p } D +Ur\,k@: 61aj 3  %w2+I^jm&-Vw,`e0sYW$ =w, )c_#S]4<'Fb| %xt}. ; M3w * J SN/hg%  R 9 q k>  a ,R ~ q&6|#<y FT! #Yu$:n< "(1v8njyV/< o_Od8[}  \ d $ h 0 2d6s O C   )   V2   n b3_\ p  %sDfK3olX$7`q@J mYI].4"yoGr+5,+I aT_2Hc r B )eOgv D q e 5 r R % 8 ? ,   C H y K < x  N {  D)Aw3z Rz |{  S  i|XIv b,NN6I?}tH]f S-i~j`8T I9 ?Hf64bU 0 Y  A  " 07Bu2sw  |  $wp]!0?[! |6_P={7~M6Lk?69-dA${'$L:KcgHiB}Z:  %  hgm !t a-  #3rp/ V U ? F U{!_y}F > F  ` H0J:o^OGR~E$v'??@:h52[0 Y~=(gu{o\ +{;K  yv a  _ u 0   Rk M = zL" l  |g /ZU<Fh 1 < *}[a]LEYFw6GlobV@V/W.'@tV,9z 4ofJ( -x6al N S A(tiR` E  p =]~l zH ( n2 2 q    / @    F {HS(J, 4wBi?]|72oX%z4',v$;#1qr_rcj/ (T ?R j 0 -Vr e Af W Q<  ' n   l --+5a Vhb(5 i O I j -  v K 4])*v L$'a/ZN>J!H});dQ0fe|*BzM"5+n .K a \  [ E 6 h @ R .pan ,3 ( ^ L f 8  5  &zdBK Y MH?tZRR`LjLeD*`yeKt.20, m}"66A* C`zrQ` J cd/QTz}$OJx0Aat|kfCY5Ols?jrI_> -i  pHQQF5q':( +Z 1 H n r_I/> @] s 6  um\^w)')WW qtqkw5$ {qwb X C ZV2% o f RQ]r34x 4?P7D=>UsQHR[z DT{'ZsQ G nXI55 ( H   hW3\  wz0] s } YfJS*s ;'`.9;U_9?5p)v'Nq^D3fkn#g0\&xG)* p gU 4  X   m) (2~p9rkI7  S   _3~ 3 PG6< 8M*C1xc^oc//o`!n-DJ#hQ}&ngrDPa,} p' :C}  ; p Q5Q<u|cx?  Ue ,+C}` yIUK7V^MVUuDB72 \({lqq}|!wsT7w@Pll.a B PZ s: x/ 0=1"bsb-l|/#+ Yi 7BO;a>x 2  gZ { f R\L.6_Ts     2 p L S  ^  X mk Q+ } <$ " $kR K0UTSkiME=#v$]ruD ^ ?   ^7 qg g 5 v  | L  y??=8yn/E z1qU p!Vr42jvfdG(ZMUA,VvjSonhh\CwFqhC@/n42fI26K.r<@#^ }s :5UtcY+  | x n }Ui0% ~%_ &i0Y7dHa Z  9`E X ~ % |e  T 68D D ]gW[l~ T  ie~ l 6& M eOxt{`'p;%h \9W-na"q>]"I` 34 O m " -  / ; z z kg  P 6i  W4z~`2}1b !)?6O0S\B, }\Ep/jm>F{~K p87VX ?_eE R 6*7B T; z9bD,9 #  J    S_636Pp Iu%R;fUmvN!# o|%Q8Ex6d] PWeo Us Q   { n Y    i NT^4zO1 D   D E6 VJ;[b sce\Kcl7Tvv{UAe,ao8,l6 j   M)     z 2 H m k H5 q7: N .  f O xo  -S?fsH !yAVg$']y-\oo}gm 8])   A  l N A p  K O [yUIK ! Z    4 q`]a = # | M  {D [  d  ;  B]  J O  me Y T8  sNoc[ X_WO0y %e^w&hey{7* }~{M;INY Sv W$bs   &;sj\xc &  I zuxy q ~QR:**aJFe0I!Sl3q3+ r4k x  I @  P Ih ^ Y@D\JB ~  )  O& j  X : 5"6?kD1kz$r2gdN|\uknkjJC6Q`5h^(aBDu  s! EZ @ p H ka :m 0 ]b[ } ] . , 3|.E1IgnLM]Pq%\ d"5t'#[Q)yS[ 2i  ! > f  f6xeUQ  /ia1"L^  Iw Q_v=I0j). tc7MWU @~S#=f%h|x7zd$0 h > = .s  #!(0z ~E u{$P@m1dsFe c}xGIt "5F% Vj~Ov\   .Kx q  \ H@"C  =X  ^ DXJJ A ]!qAc^+ -g|m ]=AS|{Ji }6&~*A /Jx1_ + a VNqO+2J 2 gI!Y_7 T C 8   < ~ RC2DF:hQ}4jEH/z3V ]$U-bmrPk).@<Rjd C2Qsu3W1!QJP>7q=,v~  D | L? T-7]7UU9)MEAW{56X p x Lm+[B5\ Q^? L z  d e B& H _ |M@ 9 D b( 5*>K'& >{|uc a@*sD0F&XQFBZy<%zFy}CY 5 'D({~ 8 TU  |(0zce  ' T V !B):?AaQ!D(&2ca@_+^G6Xy{DGC,) I  f  MiiYpF/S * s  V&z5i*Ol7 E I |S#)jnTEssKxAGu{}qYdR2>3{-v >u  k )  Y[.xq7} u un@o(U W Td  [ eL_k:)PB !aAu_(g}%KZ\X58+M,:m}3Ok18q;Fq < 2H Z + +x Qyh2T%% w !T9$+6C 0   -++ \6 \ 1 o]\ iUz0" .jq_zC yE]p: _ \ ^O. e { w L iN q i y g n ( l[ g `v}y >K mB\l4UHvE0aO\4>_y`-)D Y d K U0) P 5 - <H % h }   s /[? 2 KTuut'[b3&O#I&@Af}Za:   O E $m  ZU ( ? 2 p 2;^{i  Kqns}5mfMWmt iRsu`HW3a5~T4O$GBWK P >h HS EQH2* " m=?  R < z! [ C OVny i ,ow"w:.M)n qfS\&&nu3s{=~]^ g  j q A H6'/jc Ks8n`Ihd A D . ]  + b Y0! ]u CT|\, x0pNL~{4I>yxSqw0 6 < qJ>9 "  V@   D v t    Jx M JAv]5E d! oCOlT#Y4C@0|IV 4;A60JI   xz G  1 ` e M d a'  % O%  Kw  Y  - %  ,2 f  y~BdDw  '~dJXR3xN14 nb! 'f9ZQ   y $h { ~ ?-q*&QD@8/ $ #LZ^LL>WQ7PN1sb a]q1C$ebFpaCFt? tC 0  E X |r LQ e&h"@}y'[(+Jjqpas?]+du3 )v P8 tF 0 F)UP| Nk   t]V|3LU g {v +? j X I y<jH[ q\Xw2-_;( l 8d`fi l M   lc (  rG: u  8s73T _ ^ $p (.)mZ8J'W)~"DB2U%75\Qn-k|Rn s1^~ 8N?)2 3  zn  RL[zBm)lu7 ky D p C W Y@#w Z G1;Ebua BNoXEJqrj>v%:TAY(z*0r2 X n    M P 4'K5dy  |{ 3  .) u  O w / 9  {%l*fC)XM3Kf :;C _Fhf6V]F!v dodu^DsSg_x '%|-n0 > ~ }NrZlfW    = Q  ;n74z  , G 8  PSs  1MM]pO _oX0x { W s .1~[]nofe"_h ^Og,k Q T * ty8rV#' B 1 } p  x  v+2Kz  O y9RkO5y2C9"cQ 4d &M;2li>~7e@w2b:;|B8^d:#*"s/ *eR$R]=G=Gb At/iE T gK{( ~ s Nx,]K 5 L Q  {=  d /Z x }@n#,b~xIU 1s,GsH 'zsa-YejM+5 eys\-Pr  v W  ; # P e+4q n ]  ! &5   +M-K   a )zQ5 [  6_/s6#wJ^gPGTl_9N, uk+V/=cVss~UCR  c J]lz A !k c /; | p 3S T A   L M ~  c 8  A q / [;+ gB  Wa a  PcW|}p0r;bnUr?<(37bi/VXFQsYtm'd| `7 T ^y;Jae F h .5 W  7 AIVC/  " 9hst5"Mg 9SL)yt1{^I c ?G&&{onN%f|@_{0Fpik>gVm=  8 -zr&' 1 t4  6EB q l  P4 Z n 6qS C R  f P7MAacSA S} };i #(smv)#V.'B >c'et|{VU t *FQ   v3  s  <3  ' _ g V 4j?  02]/5n7[y  / nJfOMxH}RA} h&1\]PC&F'E,=bk>,!:pgQ+2:t+* Y l# N7$ybj [  !Ce O F 9 O z    D = %   R >R}@")iv0O+M]*y(U~Qa(qZ,+v[ b~T~v4 :` J]3 x! A Cf n Gz Y SU  'k    m (:/*X`)Ub>G j E 2D |u > z  ; @]&-zq:p%J,mbJK|[KbGcD&`hc7l1x Ypp '42*2zbmqQ3r3. E- U B ' |  ;?V o* 8 kbC< P3 .  Bgn0t4X ( KHe)+>|A z VY,e w [HfD vb(]@V p&V,B< NI,B$`p' \%-AL[a n O,T F 7L  %K  W ` V9_45#a k{Z{p&tB -"K\y`WTKgx+@lkg5[M?a}R>ZVw|[o A  R  e > _ * ^ % 2     8 U>33Y;  " @6  a _   {f,M=^h gCc v.hS!-#d^4^@t,Z+>Ukjf2?$]^+H\d]w Vw 2 C h8 R . , $ oK(O/  Dj L le4 NSu#QLv,?\o|ujduG0D6@WyVFR_}  EZ}&brS M qcK=9 2 P   $lb8e  |{0] t z V]G}I$l{6T+3.R}S.81j}!l!GjY<)^ch!j0c'zI-.  p  kY 7 ! ^   p6 39"{8{sH8  V   d1 1 MEy820O"C+t[UlZ,'pYa.8G ^O}o ff>P[0j& 6Ru  I { Z<XBw h|D  Zl(1.Bf  OIM2[aGKNiEC+3X'ngdqnptfR0yo5Icg$] A JY l5  t+ 45/ ^pg.t88 ey AV NGoK / ( i` k XeR6;a\u     5 t P W  _ W kd K* n  /'  "ZQ}z>%OPFfiJ>;#]|P o 60+  &b(< i s ;  T  L AC=~l3!? |+qL sZ i|*9#Xg^Z>MAL@!zNtaMgcbgXAmBx}fKB6x>0gU05W;p>/A1_  F@_xid1    u ]q0) +^*j'wT1VDXNw { 5SA Q o % | pc  R |.2A C UfQWiP " h j} v 9( Y eVtm1s<' n)`@\0tf%v;^'!G_ 8, U g  ) # 4  p ai  I 4X  N0vvZ&q.S $:7E*QW=% {[Do1nl@C{ Ko<8aZ@a(iP U >+#8K ]: }@bI0; "  M    Wb;3/RgI s&|Q2cOgpI!}mx!N;Lw=fX SRnj Ww _   u `  #  n XX]:T6 K  ? K9 S|D9Sf uabv[FZl5OptxS}9e$`g;+f6 e   M%      z 2 I  m g J0q6@ R -  k P }u  4YChzL $zH]j(*a{-_pm|kp 8]&   B  k J @ m  K J \q YGD  U  ! 7 p\a[ 6   I 5l]; m44@xR`msUp~N)z!d]HN  L y w A = m* "8] "D W+pn 0    cw 4M uw $ C? 9tkZ] ox *}R q1Y;;eIH[ ~  ?6  p ' N p  &%26xb  = ujb~LSb=< H  | # } jp;1dP~M7@ocv_k{U vLdMAVM?#miF b   /jF{  ^R F  d` sa Q S  Vc b    & y x`    YD -3 kDke7%ocC vq4=FFNLt.w9,a j k4wjVT  ,h]3M^  Hv   Ucm;I.g(+v`5MSR A{S!8f'i~y2|f/ j ? D *y } $#(4| E"{|'RCk/a}D`dzCGs '8A! !Uj|NvX   .Ix s  [ I> I  BU  ` D[FK B \%pBg^*!1k|o ^=CV~Nd|;$)> +Mz2e ( d X PsO*,O * hD^Z6 O B 6   : } P>5GB:hL|3mDJ+0Y]%V1dmqPk*0B@Old A4Qws5Y/!VJN=:qA+w|  ? F@ ~Q.4X7TT6,OB?\s5=W h w  Jn)ZD5_S`@ L {  d j I& D d {KA < E c) 7/AK)& ?yb ]>,t@*L%\QD>\v<#vEw|EX 5 %A+}{ 8 US  }&.~`k # & W U # B*;@D_T#E)'4eb@a,^J6Vz|GH@-' Q  g  TdkXpD1S - p Z!w2f'Ql4 B H yQ(jnSEstLx@Dr#{sYcQ0B7{)w?s  k * ~ ]Z/xr6 w srAo'S [ U`  Z gK[k=&MC `;sZdu"GX\U56(H/InE\m4TGyF/\LX7;\ya0)D Z _ Q P.( M 7 ) :I ! l y   u -Z? 2 GWvqt(Yb4#M$I&B?f[ `=  ~ Q H  k  XP%( > - p 29[zl Jppty7kfLXjw hRot`IV3a4U4O&GARP Q ?h HR HOM/+ $ p:$A  U @ v! \ D LWs{ e .mt&|>/L&q rdS^%$ov3sw9}a[ e   n n C H8$4jd It8maKhd C > 0 ^ . c Z/" ]u GPxY's1kGL y6L={vTpx, 9 : qK=< !  WA }  G t s    Fp C QBr]6E f"  s{BQ~nU#Z4AB0{GU 5;C90MK |  vw H  5 ^ d N `| d, | )P'  Ms  [  , (  32 a  z#x=gD|u   (~bJUQ2xO25 la" })f9YO   | !i z yx x B+r'}#TG<:2  # IUZKL=WH6NR-qd caq8F)c_Ju]EKt> rD 5  B [ zx"JQ f%`?ww)[%)Jjpo`q?^+au4 &v } O6 rI / G&XN Mk  t_S|4KW h |u +D h Y K x?nH ]"q\:s:PA,'m~1$_)V_-y GC]3G w]Z|o3!w  hl  F;J G 4  .  e Q d  dLl m_I8nF  W J6P  EK^.oX(@-Nq%XZUmc.an4D*56kK}FKmO(\1H#RB{wf eK  W *2  T[Hu*^  0MdI< X *! Y  J \ E i x.%a ! Su:sK,~f@%AD{s yzLY  ',-w19M(s;Xu1+`<( s 5ibdm l L  nd &   sH= t  9r78X Z h #o )-+p^9G)[+ BF1 R"88]Np(mQou0^} 8O@%4 0  {m  RJ` xGo+kv8 k} B z E W SB%v_ G3> Dit|jMNk ZDJ ptgX)z'1q3 U q    L M 5+J5ax  z /  2( r  R z - 9   {(l-kA)ZO7Mg ;:C _Lch=YbG#t bocs`DsQc`x !"|,o+ > | }LiVodY    7 M  { Ar39!  - H 2  QPu  ,KS_nN _rW3u z U t /2[_!lvfjbd aJi/iT R ) sx=pW%* B ~6 x v  y  u+0M{  Q y;RjQ7y0F 7$dN 2f 'J=1lk?|8gAu7\:CvH6[d;$' o1-aS$O]=C=Je Bn.nD T eL|(  p Ly(aK 2 K N  y?  d -Z y |@q +)d!~zCY 2w*CvC "~r|]-]^mS'7 f{nW1Mt  v U  ; ! Of%9u r Z!  " (2   .Q-K   ` })sU- [  9`/t8$vG]gRDVm]:R( ui)X/? eXtr}UCR  b I`mu ; p a /?  q #8W T @  O L }  c 6  @ r - _7+ gD  Wd _  PdX|r.u:_nVu@=+23bi/VYFSpTxh*c}"^8 R [z;E`j C j *3 Y  9 CJYB0   " :isv2$Qc:RR*vp0\Ed @F%(|kr#I(b@_|.Gpii?iWk=  9 .zp$! . u7  4EA p n  S6 X q 8rV I N  e O8M @d_PG N|~<n &)qnw'$X-'@@_ &gw}~TSs'|IQ   s6  u  @4  & _ f V 3k?  1|,a/6p8Zv $ 1 o KkPPyHQA}~h(.^[MG(G)C*=]r9.#z 4ljS)2<p.+ X i$ !M:$}_o ]  D d R G < O w    A @ '   Q < R|?")iu4O1I^*z*STc,rX)!/yZ d}T}|u< :Y J_1 z$ = @h! l J} T XZ  + f    j +7+-X^*UcAG h G 3C |w ? { > C](,zs 8n&G-X n, 7 mdB< Q4 .  Abn1t3W + IEh*(?u&F wS\ *g u _ CjG t_'X!|CWj !T1<@ MI,C"\u( Z%*G NW^ k O1R I 4M  %M  W e Q8_4;$] l"}[l%z> -&OY{`WV Ok|/>qhg6^S:eQ>\Wy|Wr B  R  e ? ` ' \ ( 0     6 X<52Z=  ! B5  \ e   {c/LA\d jDd#u+fQ$.!e]1\?u+Y,>Xnhj3~>&[`-JUb^sYt 6 A j; O . /! " oM&P0   Cg L ij/MW~x%NKv,>[sxggu:R4Mgo 9fq?W1 { $_ lDOY@,q#2* )T / D m obP/B Df ~ 6  zq]%ax2~- /Y\ twwpv6# yqoa R F PQ3 p b LOZi10q/xAG4?67 1 N   r\3a  |~0] r  R`@|Qp{7V)5-OO3;.lz!o!Ci\>'^dkm,^!(I1-  w fX 3  `   r5 09w9{sI7  S   b3 / NK}473P F*w]Rm],#pTf-7I"]P} pfh=U ], m% 5Tt  H  u ];YAiD  Zo(/.@e  }QKI/^`BPMnDE13Y&mieoootgR0{l8Difz$] = PU r4 v* 271_qh.r7%9d{ @V MFpI / % jc m X eN4=dZv     4 o U R  ] ~ [ ne G+ q .#  $YTy>&ONHekG>8!^zO q j2N{?]  Zs & P- p` > Y  ZG A aE) w CzQ+~2R1j_~WIw2d{_}SpQNOErR1'Rh@^^Yqp olTcHGD,5  5  fE:.1%  "[r   2  b  K ? hs$ -6\J2Q-sNt:u qDY% g*Cj'.[wpy m 3  X  * G Z R#  'Xszh] V L?WMJ 3]ZT L}Hg6Xd]Il2TdnOj]dJ!{U RQ%ela=1@ X   b 0 K 1 u HR7}] $ pAJOm^Y:g&a6t^\! @vy l5rz|i6s7W>$ _xFwYy}c d .^ $I =  #  A-)uQ}V#|W) u<pNQoOudUns\phz5 5e H L .  GFrQ@)Aqu404o:A3QSqK= 3[TLht+sy/Ti<h}\c5Oh$ H I C g */YUa,)`BJ[U$PX\FD`Se|-MqY /"g^1~>2)b=> LD  (  V ' Ysfh<W7J@2aH)Mr<I 6x{ .3^Kwye|%cSq 8_)8A8R!y'vbZ#CZZ% B //wwJH~Pf-` mg GA3! |*DG't'egf<2( 2n V Z b#:W!cyu(tzA[\t5i 30C9 R dG \ M PA w(FD};4<>aCaZd)1vQq V l N  YwLGvdv<8  @Cp-7dC;>D UX2q MjWSG(VOTZ iPEb#hsu;rg-bq'q/|E|V&=MT fC*1c` s}[SJm%%nBEiK!C48 ~Wu;gVVA!jv5Tvhv_`J:g@&YJgwyk2%cYjO^W1sQi[b4_F3#xP(28__^8 H A AC bn ' w 6 d'yL0m;q::RPI2Fh_v`;ef QDv[4;.XuJj2< / 8Gd2  $$KL`I  [  1L 7" d 0V G)7sz0SXMBD7wD? }(<.Mf>D~oX3?-9n4#BhQqfTdJPp  _ bhUv E: B {gb+bZph-w|GC-ASIY;_~*iJCA6 OCC 8AF+ \.\'5lNnX?}!Qn&h 1$6LyQ3`WfR  , P -zL {Zb6-BeU9eF1(Y1u!@Ox+6+l#5G VY.F"\MM;WT8 vy-- =p"ddYqJs_y'  0 q  M ; I [ ^<d>}2 8> Bu ^  3q|0)r1ZXm?qY]qdj-4/9WRO) (N6W7wY,#b!Ps R> H :  Z _96 9 q ~s>kk]\K/ 5' Y 4~  .  p j . B KW&xP'S1_L(WP]sRBy|}pv@Z i>,-IktzsZp)Pv2{vEZB5nWLU0>L{`bL'6/ <IrTu.(a wc9 n>\Pb*t2  C  u Bk P~ra)!bTKNr:c'! /!ymyM(5B Pou42BUS8LN2_V67+E]-VD7 H L  s74\}c9E#E & }      v ?H  8 r p 5 H  ! ` q Wc I=DmU]?+u0t$+.;(]2 D4%4B0 X., h ${0AE , q" S2h|&   F Lv Y+ L % W5+V5b<_fJ8s0YjHI=N]moF4%2BNG  / E  J1  6 . RbD" s/SC^tY!1Mxxk5?2ceZI<  H c ~$#|V$ZA%S9 1 XL7KLXa%P_E=>/yUi)d1 W#lC; ] '6i;a0r\9ab {8v6.uK ?J6/,qce_78|U]92ReSi/<NQIf\@*h7P@G/9W5E_Iyjru8Q|_R.ihJzPI=qtGA ^F+9 U  H  MI 8    aidujUog[?dFh )143_z`JB(g JC I?Q;S" PZGx@W9=]]}b($(( 2 b @ 7 } B e  `{!Ik[ > }  \4U,UD[L](W9tVeP " e$0.7I@SO s " ] +u <?C>;/9* W RssuK]F==( ^,v K7}^jYyQ  Ts@13r%/F1z*$"t; n  Hq ; X1N|H3iJ$ s4x ?Szx+,j4J\|#Pv{:c-Epb+};NzkrY FB0}gTH9}z7 >. LQ -` T $ l[ !qjt O q  D o LN& wOZ x=/n _`e; dAgWQW:_b^x!l Z 9V  e }l7~ $kBX 7  $ F k 0r d ] =Wz`.[&i_,Ca-~\%}1%W@fB@C.kEqj:ktC1<|(>J6/*PK|2 . ]  .T #Rzt]6|BW4 ' Y3uYvt@ W<DH` Mmk(M:V~ZCYi5`s e= C$ _# j K " DGVIZeHtK_ s  8  (z | ]MPf F_uaH{;aRW^btpmI0aq 6JH \?qJ\ !],l]YZ*J~-L*UUK!(k|B qf j$5 AU6so* . Ti^i DPjV66 Hgf 6 k W6 u HS r i  39z  * _> { p # q ; QYwbh}i*bKd ?7MPx(476z )^@. M/2 aA r >-  z - )b c P  }8 v b_LFkrV } a 5 q 6o }d#a1[U| zVHfj,DV x}b;J]/ ZD$w \ E s  FH$'%a~+ 8:H  m""A%.u/y3'.UBDuh*,+Z=7 C~#Fm,f=(v`pYr/<b2l\ s O , bJ} OtoIyv4G+9^- 6OeC  + 8 P 4{ K 4 `> I4q._ G? a-9*JUWW%Dv~O{y-cb+ g-O n]S5AYbF.xFrxw&.wpNu\+z J %O )  ~ e87%S6LydN3/ M&`XCsd:!HO B" -  9 N ^ P0F;!>JJ "JU}J:E .:N-ZEWbJ#P<MEUW3I=R- 8UN'g5S(EjezRm! ,D l \ y bW {  j&,q}utE^"X($\|y`FA: QM-aun*r_"'aFIg{hV&~"x~Xte>V+.! l#W5mKQ&^:-K1En-  c x Y(  /j\lHtWE 3Js6~fl|aoV[~%5V>qq.h?\FoV!sMm>G#~UQo)2yc(Ud4' T@RVt$1U;WS I Q I d/Z|m0iE(vM `M*/ ]44P+sp{3@MN; ni;60 nH~ _|!,^ ;{L 1Flh: s    )]yh@@[H!'~nl#7j?RL`}6vI6MA'18vHrmzem8>o]Y(G4Zf'  Be"~+Qh<-LeDw:_hg*s\}hei[bLo<{F`r/~&q{u+?ZG>!v )P4.K^F>-C[OGcA'tm, ,tl6y3:y:HY&"wu"_!Pi}! 9FSpJ%?!95A-qyv9Fg*dz67yGP%Z?  S %/Q}0TLZ6d3x_@+O8%u5=AK01]LY'.anr]^&=5GG9tptS,@a. > A7!K9b\kh-_!Oasr|pk_'2x~5uCv+"5?UN;;}YrQymQHV#eHU4f%$S}2s@ug>o%r'`FY|k,hfqZqv{YV3!fL2TQxu"!HXIq{Ej= ~Lb9"z1&VtN$['1CL9|;"S i$w0 PooKWX|vA",K E;lU2nAN ?$Y'`VC*f;Q+YrR)^p#\xiOA(C><)WT 'sIqhKkzVp$UE5=~~VAF+dK@f{jEE|LSw7PC0"awTyx%W_yB;#~ ])tf.0)~rqk+zeP;%<o,<ct B&M` $d*c{A4a%G yn5[VV `TprtrF =9`qf= C1! DyS%k}^,  @t 9E)@%1/%4;MhQ,j/ETdtw~dtHo*]P7+|XC2'"-;Vf: <JKE4";{ \>v`>( ~R=#W($X7}O_jovxi eL,HI8a)PtJ $;lCLB/94 ) zzjks`bXVHY-bc0 1=x6?3 1.1-z:iHZpSPTTROPK@N1m x|znVE3)%+"S*|0;JMNK77*$#A g *VC4_zm;oS3|h^P_paA!=WcyreqP8}!F1_sga[WNG}DvMfSbfY|ROOQOF*3F b} %:\pg-zB'=QU]\[VNM D ;- `A+#8Mm|#k6\KD_&mnY"BA.u ;SestuknbeYVSTTNI:C(_"z,BmUAf {Q  BnXjoswvuebV?. penu{~LqxkQ: |!X3@F'Wq ''$%+09@@@C#F9EO;p#7MbswpcQ=hM87R.l*2=JZwjrs_}Hu.q _P:$ z_UJIR.VIblZWL:'pK 8Li{$ (#  (@Wq}plqx~ |u#o1X1>89?Io[:o 1*766G2M(TIC* 2MhztLiJ)#7L`x-650&$7MW dlu n*f8I\>y" +G Y q[)zcWSVZa2e:lFlEj@\3M 5$~eY~F=3$! "2~He_V9$ '8I[n',:?OViu*@Q[_YL=+,>QZmn{}yya7 zpnljpgd`NV8G':-r hZ'N8HMBcB~<>>JO`bqv #2?Vh9 GM3PCFUDo7}2().9<GDG>4# xx|xq~S5" tgYBk2R/|zzpiaYWII >)?F;a:|=BKRbk{0Y{"C]'i:qJk_dpN@% )*//!}b@(tazJm7]#O@4) %Ib ;Vz  8A MK%Q9HHAP<[0Z/Z*['Y([$]'Y$YVY]ejsz~hH0ojRE.gO3~}oxt{4W}4BTgy  '56JB^Si^njusnm`XSLG>}6m)f"YL?7,/.4p>MD5NYW\SPC9'ncKA-"$07J\y ()1/9<GQau *;K \gs { }vh`THy:n2`+K)@0+5@JY`mmrllbZLr?c+U G=4)+$.,:=MWahklppvy}~tojgddbedk$o*}37;>DGTZmu#1DR]dfgjj|hbyW}RoKmJaKYQK[>h1r$ qhU{Mo6m1fil jqqtysulh_]TxXmPd[]_WoW{ORPPW[`iin o mom#o,p<nErUv^nu 5?OQXYTYNQFDx@nE_LRYEg6y&w{orbdY_QWIUJYF[JcGeCm>t3w)x vppklmksz"-8EJ}UrXoeghau]xW~^`my %"#~n X C+!2EYlxwvdjVPFI89,:"7@GS$d#p+%/*2%& "-?JWbjwy&&1363.'kX <, ",4AHQUVRJA8+m"XI <6061@ATYfjus|xxw+;I]awv}ytrrsyy5=HTWaagf``SmQYKDE-G!B KIUS]_cgeeZWJB4,  &24875511+,+*/19#C"I+T+V.^.[.`*T(PA8+!%$ *) / .1../*+%  !  "+1>BJMRQV-R/R2J4D/8)-%%        *%%+,+/0,+5*1&77:7@?HKP RTSS"S+J+F,<).#&  &*12; 7 A @AB ;:6451 2."+&*:%>%K!QVVR LC92& #&+/2/2+)%  $&)% &!"/8HLTTQNB:-"        ! $&,0589;;= 993'/-)4"473 2 *$   %#)&&#   "#'''$$!"" $")'+/03441/,%$#" $!"    !"! #')12<:BABAA7;+.$$  "!  "!%!'"$##$#%"&#&"&" "                   #'+)/(,)&)!( %"##"$"      $''('$&!"%$'/-77:?==:6/ *  "!%"(!(#(((,,/./.--))&!!  ")).-/*/&*!'!&#'(-/4498992/)      "%$""#('00679:963++!               ()-4-6,,'             !##",'3.402+ + #      #"(%'%%#$"$#))045>;C?@=83/&"             ! !'&-0187;956,-#           !!       #"  ""        #"                        $!            "! ( #) $%$!            !!%"%$###!                                                                                                                                                                                       LISTVINFOINAMsound6IART JfreegmanICRD2014ISFTLMMS (libsndfile-1.0.25)id3 ID3@  OsTALBToxic sound collectionTXXX"SoftwareLMMS (libsndfile-1.0.25)TPE1 JfreegmanTDRC2014TIT2sound6toxic-0.11.3/sounds/ToxicOutgoingCall.wav000066400000000000000000027640121416141666600204010ustar00rootroot00000000000000RIFF WAVEfmt Ddata !"`1 U WNt]Q g7Jk_ m !HpE *:. ? ^$G] 4 {oa Fi<]H SN] y;M\JNsY eY,J/ Sr v eb yJmJ VB I H #GINnGiC<3QSCj-kd?g5T$3 H ` & H1|_4U"g>. @ R> n "MjNIExs@mZh0^QEY9T_P j3BtO2$ !,h@z2; ec2am(%,U eP $u=iPXnC)\Gzesm77 - x@14_nQOBhEj{`>MI_.Gpfo]fGmRttC>6Z:< ohSAR$\_`[18 Aq)2QJ.,V XRfl@8HTCMj2$g22~zE7 @^J? Z&)2&4K^  ,E`*xsfN Ak"rVKE&l<oq{*aR?1u >S6 $n[hrtxq&I< Mhn"`T9>Ma Cp D8fJ}DA-WC'KF$ 8ECbcA1Xh[0))l }2}P zAy-z~\7%9CW}0c\W"Ja8Qk 6p/eVp A-35Jp W>?5X daNV-IY +I^} ZO,Xqup:`OusVXdHoghsw?CneT}HC{9Ya $ZVM]j1HeyK~3[wy=N[l?18p0g(]x>=Flx=.^&hI,)6f$$( )Jk0 r  ?/Qo;!ILZ07 /i* MY' '9`)K>@{$K.m-.*`GK E:l"[\eP8:+7kotk}p/ 2ZLk_2%cDw*Xab{\KW|Y\ A7|W@G-Ld}_C+ fq8#N/kdR_`V~"./CKi{,Y/Z.AdB5gD9i1=QuUN2M1R[ .#x4!i*r([XL=lD`Ti_sD>A M>9q_X4c1#?PT9hx*gOw>&K\yirEZcm^#04b_3:Q.U:/V_ncDV%BCAsZ9:g#q_jEI<, !&f=9!IZ[izI_ IU0EGv?M.7y>v=4p%LzrLq=(GF1!\HV9k1n8Oj s e_:jW|^7k TMl: &)sJ{E h pJ!XeNtH{zGNMMaS J.!3hS^=xZrVi@\hDr0F>/*.Nv{u yDkmD*Y%S>d]"i]3%nnU]C+ z jWz xt_zk)nh#4SY`Ug\52Ya4ao1P;#YcsdCJj0hB:92A~> oj2]i-?C~GMU~0.+ 'uG0p.tksd K'{SecNLI&C`ax e:#jyfA dz-'Z yQGdK@H#te{h$xz.1{G. uK1G(Gunq2oz09D5r5U]"N"M;T@&Xt~*@6Pbs y$Zj&rJ9ED^e.2Hs1E`~p?){q<SV8XGi[o< %}iLmy0S<yuH1 <JDH{t O&By }!<|3I \}dYy:_d>.4w&xo?9yxXRk/q}tl0k\e4IGSWz`b'/&)"sM^#2FuC s-fp klM#B~:m?UZlD[dxa^s:&_Qw9; 2`p;41dMkFI64 xVAHuUJ|X $M=/i)B\`3"/<Hk.Y.4? ?o{@Rg"=e$JcZ -Y;:^ EZRMtwv Eye!LK}H)n'Wpf2d q [5:]KViD!!DeHKc}]Yh'*4]2Mjx_C'`dTFgNB}"Y d(Y>_H(>Vg  %S.C$B9O x 'U{?D-[\GrS>Wdy~-l~]KUE P?*,>*cdL1c$,7:WB3Y4T@ KO}4' ru#D[Uug12b8i \j?!~6hWlRIDI[lH/ 7=r eD<5 #H"hsQt,Tv9=k[HL\dxt:Z#i]c)%7p &ikKOnm9| / ~ RXL[?]We.8r@s&E %d[SCL..%1WCIRU4|jke[v!IWV|\`~R%]FOD<  *O\bid6,|gKnBYz%p G!'TjstbXYb\6NJdC_rPgoGPm[Y6Oe,iX'g"ZzDZ3.;K>/NY.2G'S.E.G`Wgs|G7&~nwv <4MH+HgC k`.zQlAfRpRl )sQ<5C{T0Afw_@e J_A/>4r^w!7Ja%<~p,YL+YH-Jp`\k`i<x 8M$rY 3:e5S0~ot[0i _/Ump4MD2t??9vP4= `re|4NZg<Bc7f|c>hXnRh dVg~ { KlmK'8,{|`lU+Y4wvKZ1+914cZ(  nhg6#Gh"[gdpTwr28. lLPn&Yk-v8/DsE&;ppxEW0$U&}2h(I]s1gFJ[J uPT>=a,@6(,  x72ec@oN)S|o;W3Jp}fo1j[$sDcpNhPaC a4UiX i3wK"yjZ Q7 4{6F1@7``B303KT`4l 0'Y6FJQRj"%] )HK%- :p 6&Npy1ERY3$tHbQ`25eL:U!l#:\He;ZW76mpkAqPj@:M[NpDHPg{py5$1jA[? rm fK+_QdfVpO0aZ ($-!; Jdm:,gz5@ B c O)riS| ( yHs$% 0rDhUn9S+1NlD3WS,j^ kn68;CO8Y:u.,8 e"$L-fU[A}'*?M6jYMsWl#ss_Zc?YF!)CJKF|K00<yD56r=M$A@\+`#d 2F(NB~c^o-)6/C*)j\^$B3lk7aA|1.q&xK.d^( pG>e$_!@hd`I) U5,QvY/ajJ|fj1hLd6-A ~.19*B#;;G`}<Uu1u[#8 *6}=]D}faBBi KN'ys1Ot/ p0dw]j) .~ G  %/ 2{y: KVp`^Ite\\~M@J] _/,A{VK*>!L2m5RN/ri$E!SM\[8EQ d+]eR}4xbg!Efv[>L{\|q5PD5,F \jgxqL6]Z[3+Nfz2S1YH,AAIUVKD |XOy|+Bh^{C-RjET5vH/(eh{ikpf$$x8 `}pO4Ag]%t=/s_%vVX='{+u69^yb06tVL" 3,`s}B"b:jDj4UctZWe/6REIqS#ewaBE#XNbMR0.)$py%Kd5(OzL8Qw*yRjk>gy=u@piFBYtVrqk @uh1  :VvV$|z&/oV^p G7/"yRB7;h_\y=f]7X kHx,p9wSPQGBt96"~aM{pB9%tPoQ#]%;5mKL[}D/iyKsb"W+R!KKTT%g - |)jLjY(mlgr~%Oe)vUI'73'.#/Ky6Kyf /GYDQ&a.yPEaV4*6 H@hnZe]%^5 UB3~,ee^{+~LT;3*.2Tj#iOxm}qA\ <M NPqs sm>r}y@B\:1X - 2 T8nL 9S|4N}Wss^Ha{.$.KQ  %KY)45&5obSm*0$s` i-}<~T]y} t#~ymA MH.iPAz%TW(w'3UsNK=3j:L3F2iP}WP\j~?/")GY5kp0#ow<SeD';_l"( 0yFcbgm'cV?(FbTx$:jgo=&_MdyTlu8E8x%Yg^V7O|-{g%Vine!Ae1Fh9:.Xo Y3255A?/L]kUf[U)DXm E1e@TCQibB?>Y`z]j jZAa3NY(t<_v(hFY- .gN/TjH5~TB}mDSxR7yWr/ $aH-<,"EGmRUSu8:%.PxYc"0>:ffpB5$YdXv*"45[3;7lGY2Lx-&vl= 6`f/FGA8U(Uo4!EEdH7*&ig9Qq~(EG}"0cldJE1m}WC^Z wr&}'p{{=nh1ZmA`(&yB9w}2})Dp#mcMC/s'N"] rj'#9V6|^=0~x2t0k<(FT*O%m}Nk`]ufwD^8"0n@4T uW /Y'bZgHh) O2+D g3 H_9[t1(N>s $^yH~f{fezc^?a (EJhx(\6nM\M~"$3h$o^k9Nv>^ [ kX+"_3 *B^'S+ &0 b71 Raze%sM u^J},APwr6Zy6eoz4S1y91,)>w;=l2:mn0-JpgoSF9+dtc" m MRb; 6{$,+`f1^l=a F#" O&m7kqXt3_]_${,RN*R|!WHengQ()-sV=j>Sd.'=!4IsBo^twIv {?r _v0)3uX3PHndekS6bvz{M<0`a vv :QOC&.F*BDcaTX ?'J'@$+G(x>8rqPI C(Nf$* {ZTuDEkxFcw='P]w )i(8O>^+CB2G:swjLF8GKvp#Nw{&59:58@vS)% 8]|.>`vj`_9%OY8+<j}K'e63='ixvaRj{if4 Y<6qk3e4}[#F >3Q = @ G  G*  sa+xer ? b )$? kh :?mosxg#7^ OM' 8 S.Y<ub8SMj\gia}Y bFe<=C^| _ b qF G QCFn   #<vO:  Ho  \  S/r3 {]dm^ 014s 0URmI \ PHf   #|mo #${78#[=c C P   )i{Gk dCtUb   T 3   ?  j"LfwxmX &|J  ~ 4  ]  &Tr{t'e S}uG n q TH&)|<#n:uE^  (f Nl( L* 6~  v  O-'-kjjp[F   { C Z 7 ` s|K\h} @.    b //HK; h  |e  9  '5)JbMK~&'j .O iWj)zW2[$c Z H>*<}o6 i-0DS U b \ b6 UUn00qP g x L!? K 3s u;,sE]A 1 o   K AHV7i%sHq<'Xhd $9*:!pD_RVc!>K)'gn -nsGclF@kH*0j0<UxA06<!LjBZC a`S}[l}}J8a}ekg~<L]#8-sr:J!l4Y aafx88Qx8 Hwe|;h={".hOI\1_&1zAyp _xu[&nnpFJfa0[d"Y/QY%lj+Eyq0 B^hM_xi'a#b &mY1Is1OVy ]jf;7 oQf_E&fLBV[XeI It>vU&tz0T m -r/ju3NpUy::]aWJ0-x]E^b{ g}: i<:I\M-^nRA^Z>j4(+h/V#:6>_Is2z)cy9pTKc^~X,SdNJQkuYk7 ?TS$Gtis+FE1@Ul$)Fc1 a?0{l\pMme"1KaA?7 JzKeScT+'uk!5QtV*Ys9)C9bH:J[QFg>NCa"L@[W6!)5wS5~vM ?}WQ\3%Nu&!|>}okS0 j]?t$dYv-U @]YGSW%0E`ZM/ 72W* YjCN5<,0daWXw\-8nfq'\\u[oUm`tg_f;p]]u -c{EV+7NlF>Fep+ 9u 'm*<<x %3='^?VkG d"dRf3=6+.&f@ `| [lbdZk7 3YNWW]5n JN{!K'*#X.YJB"|>lP* 2Ai~_T%gPp<4vik4:Y3;{ p$ Wo\$ <p7MT]2dL}m88x<xJY9 [E2O^%]=q n h0?@$B 3<>3)F&*%zp|cH? ..1f4wG6Wj~ \wI&fA9dl_ "TBrp!4T^; ` d  RJ_5zo8 $ p? o v"a 6pdpG\6! AH; R uJ{oU  Ek hLf" $  k $ ? [|A  c fa)0DdMXF.2fJ{H>&Q}( - { d ^ K?nI4b: BW Tzb iC| @5 j '+Sj l uRx %U PDlu<` m w ~8=$GD ]k u  U# l O ) nY Idt 9  |N +  <DkZm: L   / T  k  KuN&[6>7|$>U1h u Q    `aK=h~dbtY0F$w-L~Nrn.j)^0O* H/ q)Yhu5Y[0 Y`ku^M- t5hvdU/YhVi0X/Ff|oQ\vOONP%KO[Pd 0#|+U hDN\Nrfpf(qsYy{ kajaGn@\v!dLKz2#X#2s1hTvNysxc}T 8)td\_[dao3I>lJVX:t0?Dh(p}\q6e_o<+H  PoUw)Q"\01wCsmq}\8"L&N5$[Z1N p*r=m9 T g!3jt@]S`RbbPkuRc.e{F,FY7kJc?jw:&C#\ *#%Bue  jv&|P]#[XppFQjw aiPdM~TO.=7l MnB5YF!j?zO niEy_ 4TDifs%~l~Z^#1D4O%v"jMZSa:v 1AFn727scen9^(3XzxID$%fo&5LTtIKM&\l^{ [fXxWe|J; iy CcZD8iGBEq_5n<=W)7pL69# 2kg64! 9$d_]@2JS75[< *I}7ymx6y_tE4(kw*<0g&E3 [r   > e1f ^DE  hrAmzm )[DT]Z{/p\pXFG/bnR@(xFC.5  y4NDH3- in !q + &yD MR Tx 7 WZvAE| ; \ ~ W g L/ . T 7 t#z1i; 5 OF 1 \ 8 ! _2 ^h )rV(@]$M2bM<y FrAnV5  sO P  ixf~A [ ! ")K 2 C %9 V] ~ e . 6    y u  &/ (t F8 E >YYM W yGcGA W s nM ." ;H$WfBQ^.nl@CMO$5e|]#wz#C ' { ;J { & R " CrP% f\&!w  qt  ;P @ vM|a, Q WrD'MR p . p 15<( A c4!.;0S# B : 2 *Wb WZ0*7OjZp"%0&PZ7~gH AJ LD  t" | 5J 3 C p-<[bf 6q  + R  l f Y* ? O d a0Y{KM  [ w y I p  'e{i fB I c> &1u|v!&we!L Ct$=r$[0hP%HY}Q_TXp+va/NnlUHD?;5$]ydO r 6 tv 2^rA-.u  a F g   EcIIW;Eh.gJ(+h1 7:{@PZ39r" SftD6Fu-X>%y\$=JZ- \~L!hU?HUlPCft0liu)~)O&E3?7T- 5Hh B"dV6_M }f,^!i8j626& ?:894tQk;r ]Ly{]<Zo<y08F%7UHc5:) +Z8tk,.pXe#[2"!1egf;(?!=}8ee!-7Gw@} dkoB0x}0XR{#J5{ r hAv ;%qi ; NIfVWMI5&6KlW- 0AH1q}qFw@cT(%BVBSA^Fz1(>M_"rw~}S}ADTyqd{Kh=U/D42Qw%J| ;pT3( M4H-kHGJ.CBR(f^}{g$BkZ!Lhu(V:kG 0\?X_![(]\~EwC.{I!C%R;$4 K 6htSj.`  R/jR2zF8Cy R)6Ioi.Cmw jvw>nh!|5PrM4':U180Mq Cl3-e/d', pMc'l8puWqRqoq_ CMHXL3%m\ZYyW4x#7\uKmg _*AW[HzVn:kv(_ 2-@AT[VCempnj/ cz fz3%#|M+0)n^Z6COv A\=ac vE-Zd\Z$S06]i&Wq1YZxRRdY^D@1qXuy60 3xOB34UHx9TzdRQa} {=,MBX Bt&b9RTy@Le% G$okf#+t sQIS{{nXgJwn(C7|onif<|p 5u~$8N"379 t  1 :  FdH"QiG@XZ*;t^ U A#TDmC{ktk@&@D]ls^7nu  Y   pPOZG, qb/ q'  B a  Kg=y 3 \ LLtr&*4  j  IR- 7  eMdO9Pg]3H@ K# _ 5 hMu%.< c g  6i7skqRBN3JAz e)   t t {  I R >%6aiOo R s , k3 ` t ?bQ 8 *ofXn&V y\ U  \ vC[X. e>mXIO atF?W P  r U =K$ U7H /  D# y3eV*^90R|@^ ) _ - : PW mpX[ &/ a ~ 9_@HwH Y uKh?6b U M T+ B r I *'5  R%]DN,bdTn*xzQW C . J 4 cP lt UFyx,kMJ",^|z8:!X d$ { SN 9 k{uo o (S  % 6 3 ) c N tRB(<R& ' F 28 1 1h7> _c  pl'V&HYjC/O6_v=  P{% p RZmD!GNdda D]? ] 0? 2uJ- OYP#lk%=X^$!fPE - t * t  L![Z|m~/^,jqg>D@dR[la )wAy3b4 TfP[  %dyO L%s2Y Hd 8zgte@kpC(3orx g_STr%!\L\+3(M'Ik% (b KrDh$wy+J vA6IH| ;%/'=WX ;E <_"v,{<~<&2PA'GL7f7NxM7U?pF@8v\ wdgVE!F?m5fe]_=@rcjrUm5iF:9$/p U` %AYjvDxq(/&D3fOL|+c-9q $d9DC*');G*PE]7"H(kb}1a r-z+!iroxcr6GsQUku;-Y6J:w &(o\MMg%w~H{5oa4I,|K! ^!qsd[TI)wLF.";o/ i iMl%>+>n[VjV:K<ieYDXzy8)L2v6I#86>dmpCS Axdt[Kd za\# AFe`-@BU `4*{Qt/ k }[:3Q<},|X(;~ZddMcN?L$XJ:kiANHSVmn>{|^;E%Q /E\`])6f?L^r7$:N0]T vvOq[N1#vs')M`a1:z''Xa;)eqQ{&'L[#+]7oV7.H;L5xvy gzZB04HN}w0>Laj',|] r36peQ3qE_Y7>K v UC)\<4pkuR )_WL 2o>QT1p_1PtpG*\(l g>R*@(E-Hz@ayt\*M^9    bn:d`Lp m6+:~4u&$-/lip -]}Xz/a8{08_x7fm <   a(F&-659u~zv$Y  I S{  i.h[/'F6    1 vh T  L #PZWcMWYPTz! 3 J  /'Xe) | ~ * _ &hTuLubZh54 5 Z ; {Re 2} [iPhC+{Y h"=N1<?4 6  P ZD _!^@;Qo!  L<tks [i ` & <Oa6I| >S C6 dC   o h DVUmZ` o | E _p5Ry!zgU&Fv Y ' > J  ! sm $c& 9=o" n ^  3V| I <( n t+9'FQ n$S9-_ :  _  ;Q  y  } 1 m $ )ZKj6 `v @<8xu3.Ry 2t > ;d";L+9J07- m |@ : h #H^ p !  OT 2{  K* / }|M i c X Q h Nxb  5} T`!p9 ZV~BWak)3`M; tNTq^9 Nhyi~<S s,>3EooFMTW 9yLnE?Kbb/Qt<d/ G sn # W pZ?YwU(AI#Q0qmKq$CDK@H'fNapN?|jf[ `8Qm/7x;>4& XyKnW&@10xN6Cy4 3T(!J4@D j[J.:7gG P}3L_.,kl 9mUkC(cugAzCuiOCFv/#;0U 0o7]`=R qQBb?)iRY/8OY_bjed_>7AWwSfJpa+$1Q ktveuGxV IUU3:p^HK%F-%z}ae>9: @z13_^|b[PgW2+;pZYspHRU4slI8[Mjc<2l#.%Dyym]Y^^]JRu,ABxr{ ^L' ZuJo'F (JLl8A, |+a#yA;F-F;8O7bls{jGM7\s9QFVs >J8I^yv$wmWC:KyU&2eV|+,:doMalr@1M38vqP`_5,d3% _>m9:I{(EoSAbv|0 (YOwL. =q%CRT2&fV |&d. _V8XP%| iPUZO|(DB[q(J_wycFWtka ?Ox7ly=%=* |?>LSLQW^WVzRd@J]'Lk>J 't.8#= I>g_C:A,Nl"x+945|'h|MB|/S7khh9N!ZOhV3]h] ;0mb [PjE _dcfnP.( {g0EXUp oz / w4-?w$/UCdIZBPIYMb7/( $ ` @  d _ $ RJm;- 1~tY   +  ~  c 8 3I8k` -F _   o 5 U] +d/N_W]u % 9 \ B D ivP y%5.1u)AX+6"wpj:j~#1]e#   F *  9\cxdtkvq{ = C %:G f _I0PQC -1 B  Cc_5{gQV c # ^  } !\W-)iiJ :+A:(:M aKF&@PN/  l e j X l iMk]+ahW%~D   z6 v  [W  e .(sN 0 F V Z |x /j*HNUf~PA  3 .   @ &^uWRUhdlv/ qen*'"wY@vMB '  Rw    SZK=eJsLn   0 / ^ 1  c  Bj  N j   '<` 4 m&#Yd{%+]#2%Y1 / =! eR o@Z0RY5Q8ah+xywCHX$/ 2D"j(bqEJ%Yz2E/g  g* q &F ] 6J$Z21j  f q]+$6 "`h=\c bXJF7gN\zAF0bU3Y%&.&6LJC%$-Z 9] A _;\^s7ywWB.IX c=@GPL[mN{3 J'+Epc8c5bRe'PC"b,uJ) b^ikJNGsvW/9jZ&n*=r?8{m[oo= {a=tFz=y1k7 * g9-~V3h]b?:M^;&rj^/zP[PJK-BOa7G!N.C8 (*_''u-TxUE8$Z,Y H?+kqr7,q:`#n[358t<e'EqhoX93ysn9} q4V,,'jk&6>MZ|6>L (sc`bwT(zsmSZ}f =~wd2-?#jw%{a4#_z?4uZTzN|f: ( >l"wW6x\ff](D$C2vNrG c+KqZfU_uJ1t,\]:l,'p%(O  L[0\*8l&(K0WH{75W*}R<WL'+pgM {{.@ %,S|Y-:~06 ](z?<"0<}z<)}-# &.`Mr6n9;j Eyabpi >*GV}C1@2%4uodvhx'eQ>3_A#&]*R< g,11@a'HL:sKiBLrB,\a9"4 &l6RK'jWd%r:kn}<UIY /9S`BJc ODU_"p#*l6d9`3hf3Gp rQ9[ G27yH?%e 2 md[[0\{p6S<}8jj_DX1JZ A : r1 Z [ei7zM+ulpip}cb<3QXDx jzw/Ly pAW [J SV 7 ? '"X9fmy6Pz b;Seb _F  o%GB?-(ceScy\2/.X D  $ bj  o + Y $n Rh  # < ~9OeZhF . e   !J@ p  #IR Y Gm3 GF b\7<(;7" wb> $ = Dl  h 6 X6 Bd ]a@HSG76!~c-Uz5 f k ` k PUb[ :s  z 1 ]  <U37f{xW  ,R a  x < +/Sj ; \SAx#'<4~a P k 3   g  g_HJ[&|Mu{,BXUY J   r =  6<;% / >  2  u\KjOV9,m g x h m   Q uwBuTZ 1_&/C E'~^Ivy)$NP ~ !  E V  vxz@qYbx-=l~o9 * iY {  xX1W  l  = {  2p t#Z<\w{|1[ a "Z   7 Y) ,j : i),  c\<, <  `  z<  7bUJj()I]o    O: YU nUE_k7O_2+n.ad>hho\9Ew-3a%G[9zU^\xuS{# T?4)k@_&}ntXO$b^-hd1 Sww^ yr_|"WHYt$aF>w?V}*#2-V&2:V)Yk+h;6ix#i.; j_L4;<;Wq,]gy){M![6"RBNHP"&Q) h2#6c'GCo x">6Y7Y^lIW@)dnY6(k \A%p]r8<[c7W   )-Ec41a .t]Tc?uW ,v .$&4(+H#nzY$[ KBzAQr|mB!4:8u vm|=|\@ 0OKc wf_!sf=!ewLhe]~ oU5(j%heB',~Xq5p k>R @i,:p" 0vPNvqx]zYb;j% :fsvV]N dVi O![GReB:N?()R`r d<vr-Y~ZN_{}cx5toA$G~ @G{`jVH > o"Y=Fk}!t`gjS336wj`,Eg z_G3VD>xDHU"c\}1x50'>l/J+5q:&}3q`i4)([Gs'%-4m):wdv@d/ t(#9'W3U% IfQR(Kc(p`CME-{ &->R <1eJE; )ud!RK+Yd7@+/b% ff]AAZHt `rqWF$#07(oR8{o=VSOgY:U|Pu%1q h/=  O s2$3ZG1`v]/2TG ^s%  4 %fex' n d k   5 ! R =[?+)a+.,>G |   S #  3su,5^>_?(t2;JrNj5%:Zdz5 'u I z |-  9)((6 R40AAy4l  y]B  U*L [p  W^> A  . 5U g?Rf1w O [ \0   t  Z) L 8v ^ c y~3fN E`Qa7K/Wr"q.UI& / x K pq TC7i6#OG([s#"~('f     1  u Yu %GVz3 *dRH 9}+  F n_p F |T Yf{bs *} l I _ Z  X21^PFK},cc_*7{  ZV /-df=qT h ~  u_l!b-&n0) F   P   P Sh<" s'b;M H: B }u 42 % n R &rF+9  :  { Ek?!5&Q Z0m\1'@?p313V4G:i?F > Qjbwh@WMBF^yvV + y ` # dE@[ tl  V   % 3  w j|6Zp  + # x %c;%rH |k1PRJ)F r'8_|e\XX ([bhu}\U_w@7?1L{j[]G*!4Q`^/kl s$kH8AFLP-i%*(n$w.Tw5OG ]NmGe|@~AF2bt['RE ^o!GC.12$R Y<8..;!P*C G8h liN/+Uc;'`ZG )MH-'~ fs@TPz (]~ 2d\9Iz \wNKxG :0# ]^-b< :}tJF2ddk)eLcWq*(D(R$U K~C,$vl^_pmd\&^wRP0pg)fTCak:$9F|H0"tc\XV)NBGXUn_sh8USw27FEPS@m*murCdu t\ S.\3>'SRJu ]F 4J>jv\(G.@mqr22-O ^q[u}HfjE+vVf=* g so-p2zn8QSu>\m1rU\2<x3dUF=KZClm}Sd 2 2@B)Z{Q=d 3I%[k"N^PH3SErWwR6p`Vv}$%lJh B@"0Xn\yrr-}#"($"x4HtS*CupR.T$=hSF$d3d|Zj'xU}`]9V D6Lb1 $R)/7z[^jT@ME+oJ^2 q_h`=f97  6z,Fw I&^R&97s#~ hk JA}sl dP.C6,] 7IP a3Ci*EVb2TU;S~9?}J 1i .1#.h8AX[4.]"u5?{xV"!+ $iD7Kw '85#YTC;~+flLC~soeSR(el bB!aIC/P#>vbI,6vq$a56 3GqG/Ouodp G J %oz=PhiKw_eXPw  W  X U : hd TK*9AeO C C 3 F k  : F+[%Z +>` >Iz"0 nz   y > | %2ZKi +8;pzQSg9PL8bD _ q @  K  R s w iP)h#|m9 m; K f n%WZ " E {N5 V  / uWMDIx )H ;mV-Rz) y  7R > M PtL P}!E=(V@TrEBF )@~ B j_ 9a; t j  aO)IOK%H< ~u  ~ tl l3xh}S  & >x ,)i [md _ @ r $]A 0 /z 4 yTBx:O u\>GECbsHF, u [uHU |U-C QH'z? G8e ) PA , t n 49YC o yoyo1 F f],\% `e } ^ ZP[+fY nK U Df $s !*5U0G e{ i[ xF?I8= # B ru8@% H N1 ` * kQaXR` k k c  . B \vjM=/ } = |m U =  VKoxW:8b]&M3ePG4F#IT>S{3?w+= I"4 *\[Q GU9W pf76-lW@~bbz4(q$(6qMzr=: @!a"HrW4!Xb]Q5uN0,?npD]K,m .Qv8`#|byR3{e(@t|=@!Nmi"}{otoa/ =\5F24Vx:Jp& 58#Wd*k9,H]0NYKR_!q\CfzE 5zRu=Al+#$AQeiAvNjHc8}ijt%F[~>f S] |C_Ct79"RA^`4C0^~LNX  " :  hti&m'Is [O]~  ; WBl7P :M{d& x Y(Z{ n 9 q V} ~ g qI NwR[DJdN<b h@<vU M S    \KXHbUz d MSV s .  [  DKOl#bh-2:c|  ( D "f  A Lm  h-Z-0+ 41O."J # ! P lp *  X 2o W Z y vWkZk > e " %tBv )O#Hs[L C' <;& %v  5x  V1& Q J7P dga3 ]M H(:qppP#|y 7 q5-v5 eir;C,i g9 t P o Tx  %  *EZ -0L%7T ,f%m c' } 4t y ~"92?JH .{ O  au9"8c8x6Z% a[ g  \!5%xzNB~q526=b 1 NBw;0/L |  T IT]PfB@F : E:D@9 c?GwQ9@lWp&_;R]f,@f#LoZ{knDx$[= }q;t4-LOWvMlU1,?CHzlfHd5stB~wQ10d+pnvIWX} .fg$+8!x/21[dY#B6K}N10?RG4i~bh:aXD8" <U ;F(XEl$/NnXYA tTZe0cY#0np,wX}0S&_d=d_NM; Mr~*D"*-1ouH\=K3[h1; - $a>Zh[Jd\D7B$` }=r35^sp +s{|M({B(-v!SZzLd) f1z7n|~~F\1v7KB9{[6{<PMAdB1u*BqpI_X,Kv({T[U3 '7[$,G1{K ~mSTA AadTN3Hr[J\'x" ->cErL`q|}S?)L\9g#%@+h_] 1qT*" M/CA5D e""m XWVl<nv^O+w$ MUp952;_hGLr6 >="Nnjyb ug_/T_=Rk*}^xz[{>ZC -#gL1T J(FloTkEwg 9 4( vzBpx_<F m=m8~_?miC"qe<Q7w_m\u$/;< *}-mTBM2{Bpl`S.s<:Rq#7Mme t$NEiC ,7 `vTX (7"-?YwrW1vMpGS]\Wn<}o|'tfjO7\z){,Ik!1tG`DfwH*@m!_'bZI:1y"Gat voDo b!S{}JE=oJi&I9n`1I.{*x\SrT~k +RbKVUF\AhpYm)rfMv9Nmi5(U@\n-Vs7f TD T 4c.S" *ysh2m. { P O  C & f u  w*TpEE) \ h ' ! $ [k$ ^ , ndN-4SG  x 9 c ##f7`w4US3f}a2Hy-aGr.A~|UVA`%rw-l U : ke KtafH S %z~ fW   %)r^~K ,L0U 5 L L  :# b " aIPW 1 [` B 1   ?[umD5on<ShO([ - &WY/O=-9~xSr@q h . D \ jbJ M9N ,.~61 {:tZ  0wy  F$ZDf 0 > f  KY z % H Gdomx $ U+; 5   YP ^ s+a>Jw p 9  j!thzWw8f24 H :  q )zr;g5)  P    i  ^  "hX _ _ ,h S r l\]N~dG >  J IBw3!z B/ p] 8&i\3WEq_w z6NJ$S)pYd\Ew0r,POWAu7m4k SA }woG+ 9 \# G i P#BIE}y}  G  d ^ > z  ^cQ4 -p ?tI'XXWB.Q3 yY7=,pI_k_0y8uo+0pRRA3Y'!O41ILWNh"gQDz_qL&/l0OR1^Z; n#Y'd D^GsRFPnA;6/m~xIL$rQs4}zj5e\@h l80G KpK8 KJvqi><D/~x43^8 O=0~]0/=5> D 'E@I>FI2 9$.[\of>sUq i'B^8=C9wDpBVSX UC ~6:^Z'+[_fAvPz,t )u|_?Z`tV:GrnYTQFmK#HC,fRY{\0b R(EIxae 5,Alz@*x(33'MI~o -`EXm#<:gp fU+3$4 i>$pLzj=E/ :']+4TEY 4OUB-(CG73f[@%X&N3Fd}Xh8L9{_^~T7vBe(t[;Ii(=_O]oJ[ fKg6 d'sPHW<OeKN3*Q 2="'^N "_Z>? iohx4?9+A&?v5:Y < i3*+/C? ),0HbH8(r`4:]7\VLClJ(u:f 0~[ ltDj&/.H&b^}I 'v+H/5 1GW,I]+!x{b0jg5U|AkcYMe{bJW8LV)Y~=-zUj8T_<KJVzyI&3a68p6v0h"t&OkyO-m}$Oc1q>`p!yx9X_HElA+y=i K[v4GyS6z]#A!x(VDh=uUM=U=^- p  VwVq73 Y8k'dXN,   TTC s^Ssj lD)t9 ~YkEE& q5,):B#iTV(.HIX*YoC+wpCo0QCi 5 B 7O%ja\e^N?pt|  . {<h r J 4 8 ;  s  e $Iw0 rX8 xxw10957c@m+R+" q j -wWmpTG| nOaG ; Q V&*Pl'A^sP` ^ ' S k O%  + 5 v  g/ { W < N   DB ] "{+!0wp"    59)m0"c,r$/ DS E 1 }*Jg}74y;GM > \  F,\4d&  " oe y  7s RA aN sq 6k u1 v  -I :X gM) L  q=+_ W b \ nd%wx]Z<3 J ` ^ : >NmS(&$ H V /=  e e2DI*L-=d Sn`z } g H 6 a fO<^vB 0 hBJ  _ q Qy m >|Ze!C 'j ZP ?  =  b6{x!xl+^GwH-_ O)wlWQc<w9t \' %q!9~Q*i-Y5mv>0eW  x:7/~-lHns < Y  h?$JA  : :f7z+ *)_d 1   bS7RUrr!w[!R-YC'vZ/Tf g[%)g(YeVus(5"b",g+;m ;A25a$Hu.*2(J. "Ly9ncWq)^$?yr]oTxxLjA#gu |Z [hNUU :8$ru>\u=A`D,DAG\s/E!0l!P#QH.i/gy?QP/} sg }~ xj,<6rQlC&a #h%wVL@A?C,G^/T9Ou!tH1VUbHh)gSz1>4'zP);?Ot* FG<:_L0w|6(GUVS9B90Ji21jv-Y << XMQ" O 6qAqfO|6bT0QF=1~#DWi"ADe tJ+#+DCS^Yy]w:P8.JHmid[9 D fs.YkrGCZf W[|>j ~U:8pX}hj]nVD1D"S' ,M 2M$&`&tyPT`Ea Vo'Xi4\WIRWP,H8&e ?Kr5^B_?;"jbcn~ zaatXSe6n-i{6> b$y _$zV. FG6!w  }$7Rrg,?{<~%CkT"fpL`j  Nua'CWeZ\,KX=[ "H5h";hRl$P>--1./>:$^"ffktZ!XIBu0*r 7o'= ,$;:5cSbL'().<)-9cY$h#;2gKI .bx<x2U9gDx;$ >`H({{\2lJ4\I+2 wf#PTr{~,$!eda4D8qf(poB1VQ/0<4bZ2(i F*nKvfA3*:E29p ;}st x g@AjJWQ! a + g * O!C?sWp1xaj v uIEu#lz- - jNBS1e?i[yTOE?wDvnay:z/=TWY4I i{ ` *3"8C< =fY;];46H!:I K C . ;( I-g k &A qt $e! *2 Vs B P-Alu}'F  8 U *s|=',I8h[-g"N M  ~peDU/  Z #D $ >2FCd tDZx ! %{ wS V ^bd-pDvf#d WZ|kq #{^l R,  Y| ; Ui7j X^n f   y4 v _ C@Yru.)2  F *; e 3=4 .8MHH p> z , TXA4U^@n:gtK ? 2  1 `x>a#b#EH|+Sg| ~3s+X"c B v Z S  ?} M4d $qq6K <   sLoY >4JaQ.cI )? C &@peHwz1g:m{W X= \  VTo69Z7>v "g Q7   j g $ 8  /E[ _AOf:vp.@  5   b;Ice/% < a sw52fp.cM 9?qZy0BT:YZp?6;PSS>.v1dX_%w{deDDW ,   H+O%zooi a"X|5?5.Mc{4(:9 ZZy91tC_]v_1Il~//b5"M4|aLH!7]n$uwsPV0SKom7q<Hv>QC'FS 38SjQO^ xg6c,...!i *NmM/$oR[Q6^ =7]@RT, +&m=B 3[u}K g" }r/I7(meQ4$`X# D(I\kXw{wB*i*ET=p?A,ym< SKShp}+=P@y [$<&q47ZtdQQX\/XB(nN^A*XYOa .$t8Sf<`LJX_qmQ@F7\ !B`$z&jnbHa0!Nf ynb?=$X,c)n_OIYk{ CO8H>+z%);# IRZb|M ep0r:gl}l)Q#Z p Q9=7*3C +*<R.M-*U=4h|c8Ye  EV. /NwV>D` rjRN~]tOilC?_5m80 6 f8)HZ,.-lln*zosP$55oB(5GGS=crZpc&ke\|~* a[, FZd,FZ_d"[aK` i7;_MXH|W #lKZ``|!o/m%F?|> UN9p#9eq0@#u162J+[OPLY)Naaw ) jAZe="RxAcdPTAuIiI< rW{zHY#XXo; 4o4tOPBJ=zrosO7q -g#KlD*) i,AID]j-e'x|6%L 8 n ) pkP C{#/On4XjG o ; | 2 H UnwkUhH$KU5 8] )|}CF by ZN: =ZfkpvIV v3i?>REu_$8,a =  s?=oT>  )z v q p \J e _R 4a kg =p p$(B~6,Ao  2 6 J   G ou>7 S6 \ **uJ g \ KuE) `v-=>94f9ZH/A*BKK-f>1 #%[_m.48`6d{9*OFg ImVm!KgMgfiF:xlZrXCbuK`]{%mk~+Z\;4=H:^w fr=vSGX8/8Ipt*g a{2e5ht.b. U!Y,xI#&qQ5'Ps{D0b^8^8 JK)]FkO][4$!P7d0XO l*}*sdv+$-4H&K:gRvX85xCpYHC8tR4 d3s8 #q0 /!rB+w ,W]L&U0tpa U1A,zb"" `/u`%G^'=FwYI 2:QR[R5 03w?U\W]ffu _Ylm6v &QZ:GPA*(A_=]m\XRIz2Y&)}Zh(cLR:SJh;Cb~:f+\9UP@|R+g`?>~ e't9@85@zl2fvBq Q9| K0"(R>eIZ5+Lw5!!@_?nJ0#@ 57g]yWjh(0II#dtK/ :< Cwk,dKlZnn'a+e}|7' e9EZq&Ka-M[ 3M[8&#~IwRdojX|NFHg+aZRZ ]P   Cn W =2 DcwS8M T }j*`$MH  2$| 3 *Y ,hq4:9X D  Y a  ni 2 0Y~R(c7#tV n  p-\+i f - 9  ! ;xU^yko uj0  x V ! 3 t CS *5y V ~ s  52ZM c h \/'FAY X~d H ( / -b: ^ X6>; q*U @ \iO>gD | } $\uc  e b ,e:~)]L/@_ !Z W / Z m T5 7 k P 6 T V 0   ; x' gj > [  /Y7 x_yS4-  qM[! S M?(N21i;JUx\J  PZwFz'X[.Mr%Ii8Pw/h}vq9A`e VnwC@}^l1SPJEMQ *5Z~&m?x> a;9X` *\ ZW;Ok 78giF^l_r^~%{3*1h6uN yk^oB^f%U[oxd6n}Y,yh-V|Il/e)lU~Drz5nG%~E~<%( 6dN6WvEgCUP!p+/!iI=:c\f[1/8RGE%~"bXij%qe(YO@EP=I*W+&.u`^2 zX_ *"wIbSs4Aw*t!pGXXGR<R_AI|qs\/^Pi7(H@1^]:" Xq E7o 2Jb/PPE*yC,@U0t vk%Yusvn[w3d4m|v>X)*9 Q:!l+7x|;:g8# +'`MmSf= !@Il}$ #}n*1j1FM\1M rp trrzt2h:Z;7f^\ukyl@m2 ^28)0FpmD)K}w5c)+W5)]1}S m-j)~wM#N d, W=Sxh$SYmG+XSV9~7q5b+SY^&/p `J:>(>iG!y>%dO- >m=2 |TO 86 xvRLv6AY|WkC9CVb=6^ylG~D 'HEI&:!(Or8'{v^( }H(qn :5cAOLMS;o#0 ~S9;+`A;_!ujm*RN;B@FoW[\VD/jqE.Q%9elS2Y)'{nv wTY0yY_E~b?|;_s%] a[!skVS$4/ q/]No<b8x%0eIw|0DM 9g,FO,S$Ew5k 3 2h>zO>f2`Z  d  q_  +JFRp a` J 5% .--sDz1%G{ 6[4NV&ee >w=v 7 i 96 ) FJoPK_V ;q>$FP B  L  c Q@UNJoGp  qk U  W  FApy#,67b}  # _ 4 cp.J3k-G~5P~ 4    R FQ"se_Ys8'j1 ` . tR-+J bh1_;F5#LD o r le  c )mVz:Mg 0P  2x{'_Cvm  _ n m@ ?=Vk j' 6 F Wc .Q_mGD:^  @L   <; # [`JX+PE8,0 In% ? c S M!{1q- Q9  W  ? q_5< ,jfCkjr : :I " { (  V ,   ` ^{gB2& 2R R 0  MN[nI8yq\/ xC7+7f?XeM` =2 7 p rnm{?u|%c.3Q Y r Y 8XXK# v ! j  M  Jw~( n zRW;ukLzFW I R{ |  ?y ) X Bb ~& uC= Wd0!T'O<r{m&ajT hN+p xs|. _Jc SKbW"u9PT,**? Ar$burd~?vO0~&1g tcvYO105AR2_[J"`qGbGq1Dh2>21)k(jJ .]i,9rtroLN0Q=3}\!g*oQ|J~,2?s+3f_k }M rX!W7i;#J|LLu "&8v]Wd!P Zv ASxLq38QRxa=;gzG[4-Cd H JzJYu'7`[9 VlK9B. :%Db=*~$ Iu:<|U]7`A `+:7t9q#ftm71:c|;O 01YsZQOB43n&c>fHP~O<*Rw?k #VmSs#{[9mZ:IzEL+OOd<{8,8 867 detr'QAhJ[eb"\<dby5Wh7E'z73d'MifG\U[K%L2}=~.qz@i#Z8C1\Kk}fviMM[d`8ow#Blp!.(;VtH3NgrgXwJ@U.;MZ~_Tm+nZ> VQF X$Q!7-0B41\PO,b{5{'TFd49J< DZ?fej`&VE]8Wm fy~~ J+pkohtb Afow> +[W4WCF|8_I MQr_(s iXKunILM:$0v-25 Z)=C;B$3ll[mxc@I;pbq'0+|$@ Dbl%5GA}#`=[rC o<R8jEc  >J%AjIyd%M [+0W|!m7OU{?G'+5>j\JEUV{f)sQ=QSCN'p}F]ajw89/ x ( ~JZr6-zc PzQ'm;\: 2 Kj 1% Q W- S boll m *}+,0-F+W~ X( 0 # k >4fuN/ y3 pR w,a~|!ql5v'-+]z T  < Bo ] =. ^* dzQifx k K ^  o 3 { e V S  q Y( &Q M mD@. ]  Y V C+J>YX`5v*._ [  v ) f|]Cu+3Amj;~!=E97Ued     q 5 otQVr!r 7 M ~ 3 N k W& _ }J cq b (:G u o,on c g 2  K  ~^1Rng~?oK>+ En`f!CT+)"_G:K ' l R d`/s+0+eq9IC { 6 o V  +ir  t * SeL w  lJ9e4[^c 9I \  : a 9  " O* :S)Q $UxxTbr9hqgo;(3(="< D 9h   _ l >@a#/^"kF6 Q  " Pa  =   A TNZ & $ |  s ]   F *OuP[ X/,D8A^T -w!S]%%v}R@)T7p"L*F5</^Nrl3as3V(D%C8c, ' r e^%>`+p' ioiUEExY"k1iSwOlEP ln +L"f A6H 4Wq x=3U=M;q=(>$2YR]dkg 9of hXaw3. KR@x[ j2U.X:OO!,sNi VQF6sn$}NVLJz,r^m)3)ns,\N3 IIv p' `W M!,df9&u4{ ^r ZznB, VIOe+$DD#XSfFs 0B31X@KEQ<Z]/`oYp@s9!*"L42w0#zY4D+Wyo"2wQ8Nryhz/)f)`p<O ij[bekpmtUM|Fz*#~"hKms`c=E%gS .@'-"yt [o0p6hnTE.kBb+WBgcbrXn58 5sZ)[ Y="w7$O5D o\y!q/g2AWNad@S}24J7=?%I~ >X<f Q}4;q&]`hy(iOo$'22@-^x(j+r!*2cHb\us7B0P()=d)e& 2 AQ>Nxi(EU<{mbJKK`V+q2 M+H3rdOLg6M  H jB  tL :' * /1#Y1t|!1do 9 . i F ^h ?V B  ?d) ?CGD%xOGT v]o; % #  & < L<5F8b9vkdJalHo0h ?HV7B0-a | 1 Uq ?D. ' ; F>  U<J " /m[ECCaC/Z08 e z @  NV <j  @ C,L" kCbq ^ q5zi: x,S/9' : k M #X  K O +qr_y,*8l;F  ! S 4  @idQQs   :H?' I 6+rh)l;(h A c3% P h  v a6b g&CU|_7 QaU *z!oU3L a R    ,  D|u8U_^}42HN{|?Mc ,% &H? g .M t B Z S   p5*[{oEaM.gB\) # 6 _ p Wc/$,v @ c H}   m*.$i~[nF@D F  n  qH[4@-xsp4LI~iC w  l [ m>ny4A<j(B 'T  R  ^ I  xkDHXPa#> V"Vq Ux2); g @ s  T\ e} 11Gz m  & k v_8;_\~V`lv.!GZY\Jp:/tdu&P=f'K8k$o)y;NnIV_(+W_~V dMS^52 Uw_G&ap& >{QKzn &h@_ yi-H&G apg2@*^\Rl dg`NPjwkxeB+ W6H"NfI(I9KjgL7#z_}Ppq")1)'Z4Cn;8$zlk#'D8E(H*Kq#<(7P. n0ZV=Zd^x8 7`lm]0Yvr+|4JB4VM[fD6=JIm1Sp< &yfK~0#=s14'O a9H.2MYFVVIS&=t-FTl,7pj(XVs5/:EbRnzAM"`rdN+J^r|V#mvC .uw2?  M[czwJ.WaNn1wzSh^^Mboxv|>}T4Z zGl[  ^eG:5JD'jk=,_\Uh61?+]8Vo9s%'yd,|JprpO `.i~yqG1$E%H+q+F E],?(>O~ RG ~+,IylVOcqyg}}L[&MOQ$ $]OsMm)$$tU8:b3;3`+@O#`q[hLVg R2SgR]-Ccn1hNJbC7(|OTt%sf9jybtr~_*i}9|%D%Z_ `u vqlAlA3| Z!DB7VGIaL|bj6L9m<k-A\IrggK32+%g6X: e=@ZZ"I0P{TOA:J5asT d V?`N*&nt "So<@R+hn~zv/XK_Gz+RdG{;~>%vi22md&B9#oN=!9*!eP ?Q5* zu`@,P]Ns$)) ezZU(?~h Cd t c3%A8f?k#YRjfF~@T>'M 5pa[ q$ q  E g )K/ [(:gv!lPT=LXw  +  T `|iMln ^ 2   = I~ {I *s(rS=wQ   < "  V wu 2 ?H=xS \ #K?WJR3XC> n EP u` S t ;ZkM9bs9;t$4)N V  N S7  6 W ) '; , S UNnr# N  fMx[ , ] Xg      %  7 S |J +  +BPP-'4 h. Axs |mHxo 6 9 j8 Uy \ BHt~ u*T}oh`"<5K /d  ,f. M -,L gyl~ M>] ^hZF_j  %!& h ?  } f & cy  ,5oZ/Hp97L g qL0<&JS,"b >F D p5 ?j'A/Ze cLfm[H ]u  E  ' j & 7I n yRHfX g+k"{  C M4 x 2 1  _f`/9&q  M N j + mI\oT ;(*Ycuh$ /bRxO)IUyr'AM6Na31DY{h`5dG  2    t Uheu-Z;T@ @N z  6 wb e - H h$w] . JuiDi[KZxn-vfCnQ>2i noHC [hNAhtmt ^ f4[O PW hJ 8taP  cwvdLmd$(G4,WBE))_eb9wMZ8Z2G0JW9/j!_dY vt[,Vkaj)eSH7$R qV|N(Rp(Gt@DV Gmkbt QQWO-Q" 41c@C-7va4NGI)b(ysP-bBl]\UI -4EPkXy}[AJR^ ]&5Ts`h@:pn! 4Hi61r.Ciuc1THsaP5w\6mjB{<O'ye g2A2`_|m/NZRz cge+PBq:]h$j.ZRa+"/ #Ja%j^Bcx?RcN@m 1.=X3=&8GZgrbcf?rm@%1ocK[,OW |cr;(2I!|hPe#?6Os.:!&+`9fP \;]}NUrYJ*:GEd]p<+yFR_dpx !F4(7\42dg \9Vw!eE@CM^P9X^rRe1vw&[p+p\_FB{pJrb,>bX`|'Ysw/V+<s}poJy/ u'o[Ihgd}a3jxlBhj-e 5-AdaV1cmG7Qw=d(U_X"h>D';a^ qNtuHq ^ +B+>D\i^+a"wGA7Xu bx)P;SNML $jXm`60TReC {?YDNg 'mqKUZ:`R 1  ]oZ&T lY9+_  ,5 &&T,B4l^Z":mj :co$[@'P\N|`f $} hM D  ~=  FJg_- O{@0w!Qw   @  / / 8 ;  i q@RiC H   w D Q  @ 7D ,6 I 6y "(@Q$2 B, b   V  4   =j n3n&Xvjp]2Rp=T ^/$;s; &  r  s o  " ,;n5cTsf7~k2  E, 2 q@ ~ 9  w s vUg!E  n =fA'5~cL!V ` *  N e ? o VSwB]kyk,@:pfV}mUY=Kv U pk{:LO \jK 7vq u$ KO S/;KH gP@#$ ]jl&)D  N y!/C  \&  f%lI ; j C ~ O$/ I h~ C w#.4hT22+xE=d1@#X% f { T 3 HPpw?|B #C *  5  gv=ez  %& u lE aT%  * [ = /  /Y V |L{  c:T;W* ' 3kTD KEBjQ1 ih *( z'qpo.k^ud@i* % / _!],d X 3q QTp qT S w E{w`|  J ~< &+*eb y ~G , g i{B>~M[r{kyv6$R`7~;{r}6:FZ7XC>{``+f=#)S35[ Qc[>8 .CrdJ}OC EY>O&=$OdO#kme)~\PDp |}$2(h98d\1x,n"nsRA CVL y;$Q0guMO<$~2Q V>BK/tZJ[zo12 {xF"fFa_#.)1\QdJ.@!C~my(-S[sZ<,C-R<< XXUqX](P{1%7kYF?eUq?2Eq lY zor6@y CW8o}D -[xrM-QY @M[oc2~^`5mFS ciQX.Do3_2v*P}D=gYdf]jmnEV:w4j} 0r&#ibLA9+VR9Uh{p,awkI"67DR]qQoZ.Wt7}qK|%nnC<r?~b wx6f4k^1Z2,n$'Lk{vjtY!SN'>,S>o~/- 8ie"OX$ o;WI>yN5  g8;g  ):qNID/\;z3JsIFN+_7fk gZECxy(U+qf~&k/d/"6GvSSE{o68t<PkjGvzW6ex;4;d8UI}( [9V1 J97Wt*5g(>~)gULfVLgv  {}<%v. rNi-+ D$5YX2SH[a,a#=H_w-V^oY@(]F = N _o|b"L #L9Ww7@j7}; (% W 7P 6o%7]q-5(!Nj rwi I~Kh"15 =sN@"b'    N i eZ}N_u/q GV1 VwWJ c P   H-L ZD O B T  k8 UK7&+ZPL< x K    V 8 o oi.]<"Z1B3*!95D & Ip c[\_P? :   A N X  MyuqJ  6 5 N H Ly~ 5 RYXGOa^:Z "c, /e6( F`rl'XU$U{Un$l  l D   yc ZImxIxn%C]" pu  T L  XZcdK a ! 3e 6.S t p(vE-DS | ! # a % &;f 1 b}S_Z(0L$'t0  HJf cWz2_ tjk / '< 1 Ip   )  > ? .:/GR>8mG % F V % aE* ]r|- M j  }QO1Qs q 1 SL >2q,r[ ] OKC]8) . n/ 2 r sCSw.-rr@ly Oi  ;<9XfYF R Y 6 w &|}< m R E|ok J @ @_aIA^q o @9 | )HHyK D*  2x7/wQ\k3Y`VPTXtH#x3 ` o w Nq5*eV%2 J = trx,Hzz2v3asY> . ^>rF/!@TQR @su,'<Wq(X|r`u<x8br!rk#(z?=|l:s GG[=(itZAvB1Z8:92FVZABW~t \^pa%rw?fjFM%V">M0k"h 9C)gN>;wqUN%IyG] QknSoqlh&*mwO"td(WmZ{RUkPf}Shk6<@ Y[ G_I 8t`!3f= j HCeju%M$n%Sgq!*$ ?D}\8R 3uSv *g }d^^dT+@xv8Ta7\SU&x_\<3rvVu.|a5.Id(u<IUQ3Ucy>}qfPyZYYHYa:'Lc ~bK( j cG3tZL6 j:}]Gv %s0#<ShS_*{8i,I @Hg[go9<,%@W/ O#( sOXNR]d^SR*z-$rmXJ]08V"tCdsWr"Pnr2<]oZ*TX{:a/*l aIRhCF!gn"Cfx {F^ejt>vF=Ga'^<Gx (crM{;9E~#v( ubsa(~O 7y7/ AN1h'\Hy5@Tw/Q_ '?o x_`  9i N_ow`33^H] ? P V O:O pb Re;R , pG (  i KP  kGG /wSBRe AH O 4@  , uH omZR)hE 5u e6?   - Y< Oe oVeFpwSyTzCDtrY;s[U*N-cq@:m>  6? Ek ?a`v&p 0 0`  h \    a FP, i&YL d   M . .[)L3}tWW 0 ^d 3  ,@ A } bb>psH/Y<@n!g}XYc.3 ]D=(f }/-9 } 0{  FC ; aE A %  3[u/V# n 6  Q"7K({QAVi#L $l{,8zz|Y|r{IxRB$4`\Ix-69 2K D?n{d<2!]Lui@V jm7x" {b{=LL?G]=l0Q!v1t]&bVxu&WvB4p< 2&yszVgkH5" f"?B'TqM6A]Z;g<w;t~w)'vRh>xhZRt]r`J. {kr%5Nm1TDY*X 3N|8Cv#b8a`2eW~C5!@+h(o2S{(o'sh[2[m}{nw{o@zvmY2bUkK, N2&EJnTvKZPmM/7 \h&ND?)^D+@Xv r/Y/<*+UUSAd 4ORkt ?P'28Jw1"LXI_;)ie?1DA9wd6a'1;M'> IgNC)<Tb 7VIgA~l /h#9]B|jX:3da{o_@:X~%P|4MzL8FYcU,hhBd *8`:o>Myl_|Z1*p g3\:`VJ!a-dFz0$[*>8 Pr}oB\;2QSKrd;_Kw8&!mgG vYe?(aZa@ D4_UBv ^SFs*nYlF<5#M&RSF~vdb\r`W 0C rw~X+N;-Eu 2N;mw1CG@A?  skBSY[G9/-cven C k a* I#zzIgwO\3oSpVWmE  ^ F %g0Z4~H0  , p #0 s A E* @ 8 97(q (  k  G  Y`k}u gJ oEF[oTw F K VxXj@JC7 2 #Gsg@ e%o*W 4  & d  qH6l8OvS.,P 7! & P 3 o/ ( 5 {^ `   "hEd#) G8 6 u    $w$E} RkxXp c b Y F | ]l1c4H*:tCE e (" W :*98xzhG2>hJ_ R   $ Sy[a}zHhZ<7    |  2OXxp"$ ( Q [ <: O  $ +ch=d  Y    @0M[R$%"Ai e+ 'f kJ]v? $C.Y ;#}+ztb ,SPq<g I& Bx'l.ub$Ux +Z! X %l@*F~E Q A"=<G4xKdbk fofzt)FihYmp*0D-GCiA&M8d]&Pqfk!MT];#"JTxS .pga.%jDBin2P3E@6R%]gO35U$m:T?@Ixj>fo[8s B2c A3J3U& 5O6jGas'`? -DG{;[]SMA 1|LzH6 4{ m,i 7I3 'y?`c@fm;y`|fp7fFaEF$?PdP]CsaPj:h,P2rO =eM< Jw~3&?WO&j~%]) JvOwD<EF^Sgt~[Ob}!" e!<2ySN2]|G1dNTs]G:90-N6L^=_aPTC.;;r,am& F^Cs|1S/U(0Esc_rotVwHw?Y"1w @scv&;f EZOMGHvOz4lxKQqI !+ 5:5C 1EGIyQH:Ih?M~]|2b)Z[~eUKbJl)0;q}D&W7Ghu5JJ^H2_].7]|1byKilLE+fx'H05Etp-#,/>)jCT&L_=zSI`T6Mem,P =|^(H{X8o!bE_]V!["'pJZ1SM8>WJ n<]O(B i 6+ttpQBu4;u,EAw [_GAY}RLdg0QP c\X|/(>a  fh [XF)+>nQ5u'td C]Y1VPNm9Y3_3bu$F"gDMm4.or6}t6yPHOS)c'9yu'1lqSle wYDd?;} F(j/:mP7kA 9M:}R? ^gM&r#h<>!RSZP | m jjw;GwCIF )  wo`/cw- )*3  N;~ D 82O-5@fRTLB~l*L  F )3 rj2] 5B I ` RV?|l&2j5 E > s1\ B >#nH Z ?/1YLEP' z 4}#Zl  B,vE o a ?  E j Vn 1 H 0]+?W;iBD *w tE , 4DV ?bK z  , dm [G"tH<= Y  N,z K- ^x !] jt&V m e/"  HE+ q'-/)B c< D L 1 t} ]?UC Y p }x#Y1 G j dGxVycyi 4M 0)'.D! ? @VY+r 1 M . jjW : r 5+n65   z L{ 88=(sZ5 KHi YD    G; ja 5 99X&_3G 7& p  +  )Z _Y@W5 z MM % p&P|&M+Z9f H d W ^*%y&S)*D#rSF.PJ B F 9 nN PC` N   e 3 ? 8 e  [ _49>+5glAznHZ d ${?1r waWW< *" b|[ .rcZEKWa+Yo9eG&dl I\5 RM7nq:lpA5d*).oxL;g,Ja6kWJ0f):GE|QmYHeZ;U*F$apqP]Z>4- Z GPhHT.2GKmK[$RVt62lu) PQwl_+NMDa5u9wjfB6+ X$1t2^C :)0u(>mdhz R''zeM^Kj9 ryF+>|oOMxB5Up] qFo? ?pT, d.kOSs(lf #.7`8"z a c"wQkc.5fl**q995UymB8Ddd.M`(*I! [Q\W?%r bq0jXox&[hE=: *n`L'-[)1~6{-$,4ABIPT`ajM.l]'I UV2QCc$_y\v4J3&~6Ef#{G&{ycAB$1r$ke%=r 3dDmmn_s("c[+ 1BoUvH%Ad~=F;!MqxX"az(]p} w%#Heo]W2 3m H]fv^</L&CWl|cQ9 %Td:'U{ (d\D:X[T`9DdQpMQ:nl({)"zih A1&O19r E?`M1p<x}SEyM0lY6^M (@tkL{<8>:/ZKBh& i`Un[qEgjN&\?wlZS $Lj gU+)7z][25[Rp+3<W>w\o s9TS*9FH5rFGdCB05l5hO~YQ[7#SC&TlZ/b<StX|" ~E1bP Qz3W>L)=Q" }n6Eln+vv.n@*hJt/>)F@~_U]@(0rRm 6O1e&t;[EkWDi(|#[A$b B  I  ZD r B{'{?flt<z=# D < 2 r '  U }zcerhi,ojCv } 4  Cy_ g x Qp@J"lKaS^1-I%c; khrRBZ/S;.@@~ )!{!-e %v exC d b'6fF2?f  ? ; x M   < j9yOB o @g C . ~ H   )fsX^)iq=Z"`[=,KG$gVN2h0za}_\bn +^s';*nXuPGTY6b"NsPbxH6fYw9  T [ -KF)68eK63ZhJSGq4FunP?eck<4=Jtt I^%@p~<> /B^U}.Vt>st%h.v!O@,VVFn-5Yn&, ?t(0JxA3X'@4 Rt5v$dM6_q&6g<,ioRs0_Z'bnT%EESL-gUY*yEgjQ}*aQ  Zx|f;`?7hN/=^5z| )!h !ouf[p| MvRIT_p0:;VD!/:,|TvMr+^XXvCqPs4 :epV f^0)_'d0QlK,r01VCcv1/x',* '9OYmP \M*h1 nV9M)(!Y1szgvF AS# (JWjQuEZ; dq JVWtoB @R2W# AF7 tUM m}TqMKuC mZWP7f'1f9(e\/"DuU!w f}kU( #t1+vdp\n`9N;;X%xek^Ey+7[ ,#[f.%`byns`&T  f) 4.@n |  u x Nz" ?cldwDcc+< A %s g 1 Hk{XDNs{DE] eD ~!iY+}J#G2]]Od_` t Bf q [  hr*L }&S PA #K Q d6RjvU o } g6, #  8 : 0zTe  Nikv;eu_SHM<9h,8 Y #_ {Q - ]Mz+}v  < 0v[h_2|3ol;\v +  +q v wD F I E( 5 .q $ LeK`m P_y 9p Z ( FV>)[[" [ f O x t =PX{Du3dozeI!x(W,\gcSG#   ~tVlM[X2NK~,Eol,j-2LZMLuJrV{L]y"UVX] U!1;kb[2<[  =IBL~l|<dw5WgcCD_O%Sb ` H.c03PU-4xjDoZ'ZR"#)F }o49M)$4 vWrLrT._nH$.J 6 P!2zsWUOO~M/YP4>WWD8{hSt6m-Q=v9:`VN2^?a3 DC@vBS0#xRpuw`3B:qr/m_:m`SY Vf\, H 5Q_6mz@ D)VH}=Mw oe^Xiq +R% o9;w,Lmd{/v`l[=(Xge,i@ z=4x"} 3/\ o*BU"2>F:g-3 <$I=M~la4HQ[Fn81iH r'VPo@ 5 j uS,Bst[NB23v,I0"0)=\/Db6IfyP1 Hv1k]qOqjVPWR\mB}nq2r>zYbYgTFr/);&N*"b@}uG="<dGaUX"1R]?bXaWe:leKE 6bP^n96yC"Mm@y) W~)[4e+_s1)VeLl6sbis/P5QvttaLc`a (F\L2piq"b=39B&{ Bx9_%"}_/`< W jj$Xo -9w??0P#hW)ZS9[q>D/B1#(RO8 .|1Z $9 sZ\LXo.0;  z`TG?ECV|#Z`x,g$Jb/te )%0rH|KpbY&c6Tx[(=O uZ-B fz}_kDsxg[]bH=z#9N`5b?{S7.zW vmc  Yjjt8mIl57e!v  mb[S.{KWIoi0$hL8# <7Xjfib|`]')*n7G [E - ~$p5L4 V'ED B `9So&0hp  C ]> M  :oPT D&UP78U{}C  me c7sx@'@kAQr (C r   Ma Yn[T _  h  w,$a 45p " HR  =. i  x*Md9 s H N   G { 3ZRPFc( LD - J    \ k  ,&wDH[^|: e e 6\EZUn2{N?Wawy \  T I  X 4\fs0@:< ge  r l G% Y/}l4Sc.  E U  . u 8^u Cax26i:h9O4Rz5 P I H R ( ? JI / Q; f Hk "=sU.DpbQY3/ B.   N ]p ,1u:n:|z 1sH}] fz  5c 69aB<FN Z  G G  DA [ [nH %   1  {  }  h;9Z< J o[ Ct{2MGcH@E(a>l  } :A=1cfe$%#P .d` rC_  !,LI0LAq b Bc  ] p  vX `9_z3_D] hd Q e <je)S)1R_D{(h%& Jx<m a e4$@{oA^"1e}(NsZ2 s([/ #{HE w%JY9DcOc(7pI[wCmD9 5dL1=LXakK=O)f@qdkx&TeI7xZDL2 59M.8=):`kr1G&p,[(b27m$;kbAOo/' P"a)$\s~#H'T;J2"L~gJ8/wz )sJ  ZcCpV]J1N"<V~hkk:bXWtr,s<X&u^SB$b{Vvrpgd~K)] E='j#?^j.ii6l-z KQW+1$=nfH~3rI0f Hi~#6'! UU8&]jhIqso &kyq^G% iy"ocgfdv}"N#[XHeVG"+1;ouL`{L,%/W+O h2)yQR|m-ra`>gFYq}8~TY yrp~hbhq?f1W {\" 2i~M{n  /?Kd]@g0tL8QN ;]|~1zG`+}Y066RP8[Nay/|.muDOHZ0>j]//DgXsIC\w,/xJ16DIZq x<?8? l*DPr=NNZ1KVdek$xaB';T%\4*Mcy!,d xw@8xz!O+?Oey &D \i|#P =L~ DPJ2Hg!LM3Op7Y"5  ?3!_#$=w!9$S>Mi/\$!x1j)$"  \u yE { q5>ECw WE` Y lE CG{E ." bcle <w%cdf'Zi#N 8xCzJZ:G 8 AV yz(B M a ; b   &2 q Q, RM (q}=B@ 0v|Q5  =EdM{EVB#xEsAtm8g > zE w v Z|H s Oa `@lLb70`0   j &  4Q  vo H  % 4;c 2 q!% z D9`apM \UqN X  b 1  Hr^iS,b{`>p ]Rt9]qL 7  B  # 4  M  V Y!R& (?n`#N@ _:] H 3  N  1 } ^Ox 3YH  j  Yve J l ?{Bmg[~QT:zPIy0 w` f Y 2  >"| ;|=\ +ko_Maku /' r  9  wN Y  iT Q    9   =qf|`zr4 e >  vU KF7H "]{ytT *, E *&_aJ |>+LFy @{VHy #z K]6M2m{SZ ir v  \ ` B q6dS|:C  @  M    % H(J[66T _!@.%C>Zb&p|X)"fUTSV&%@2&o;hrw ,Ab;f5-_-Xq Q!a4X!uGdSr?u&B"9K rl8,{wR}W>jKQ0 q, [t<=#<-sN*kkOcEe4Ac*n JH j+k qh=78wn7N<yL &c[zh^Bmq(pK+A8\=|_!xW ]h&KXniGG MRF0n%k Sn9ii7 /+i%ggszxd{Dk4n))" t3me 94:<3q-<0] _+iq|b,.9.ueFig:D_|?"BTZmjgt k0jDm]*66Nr7m4Cs&f.^Az/j\%S~IDXep,#TB|O +q3[L$5,#4kFQF\C@(lNlreoUIygbb'/ cJ_Eth_*1>mohoK7zuwk= .;e>E8) :lzUiIUe ccK`o#dyOdhHG9P1heihfaoIw7|% &f!JOMZy2"'^6gM$FyqX$4B`e:'%"|0zRsI9St$qL;:*_L>I|#0k +hR^I!zrhuL'x4UN&-SHnh0~h1$3bHuNa7 49.!]Q 2;EvSI>1[XxSRVB*xC]f*F2E@=uV[CO]zkVt5?[EW;(Q:7  I @ $ 4GH:xP >o!zYwaC a+ v c0O[@QO`.N_sb/P&[1 b 8;kv. \   m- 5 7 W' { .E}83 tXO1t%{q{  ai ?]L7-=  XIw\ ) e' EOU7ALQ#yk @/ U !#eU)~,owHhPUDXyi7^=nE{n% k Z m b   zD   zL z Q 5 N,a $"., LG 1 ,| q-s L n$ - G\dH &:yA +3\%?"R e^R"E "ta c( c?A % 5 < K m y=" y%W  t8 q 2 r ,xlj M   + $~`}b& 0h+>7\ O x> 3pZuzYj~ %:h4 Rk7h@rP^53l P 1$ E \Z o * ^n%dqx&>[:>&SpP '3,l G \ j  MW P3g$C7M  W  # 3 W u70U m}22P]CPS= 1 { I  W"#{p_$N 9 rrI^ b;:{nYC-T >M~ R ' |* f5'0OW;:qAl*]|n!# Gw $ y;-B74, & K 9 3 lka2!0W.jP!ubQp/'=*4 'j- y W _*K N P]}bG"TdPm  ?J~'VF1;J6v &zlR#JUTld0NOo^Eg#QB N6V7n#kPoyd@k[XJazY8ct`M2?v>%b@f7]^="5EpL/?2+yrRfV~`3wiYM,cX?8p i=Gx#))%@T\?Y)p#mtHjNK(y@]S6k6$nMMl &)`O uKiZ[&w[Dv3U_NZ ;&OVYZ.U{/h7k `T.vr^XI\IlYnwE)/%b/1'wG^=zf|!b=W#cU3K;Gk)ptcdQ$2W?_[; _o$:Sa]2^4e^bf#eu _\Q6=F>e]DZ)(86rt+#{-3sZT!Cp~A{*MGn3l|a5^9aQ*)f-S,.<W8#g&.;>;5\xFK e*$L|,pxR^XWm^/umuJ-5Mq~Jl< ;,`<YyIfnOT\-g_>S 8`PaWAVjvE@<*X-J#uVzuMP7sx3)U/L^[$Q`;?e *bV4f40D&c]cVQFXT|I^sA! i8@0X`Gk huD4AR`s_mIt&eF7};a(F)y$7v-c6`m|`cDOQgInhm1hzx:uxVe, !T7 VbSe(QbL4 JJ2%!a0:i7^%sH/D%qss-JV!ZiF6w#^&Im_7S?m} \"+!TQ7t2X\'6N\ X }mvkH=}n vWTuJt-ir'`sP K=(dmz O^=Ab`! ",dw |m   ] j5 t6bw QvAVms   5  4c OO-gg;Qo*&7O't<$ =~u |}/rg|dqcU<uo3x G>  TPb@+>1G>% zIfp6] f~ Q ) [ {"j_j84  D W | $ M=d o5e5'c, \ j ^ k. $ g  -p ! (^l` b {FYC3v)mEt D ^ g Jtu    /r gWAw CHE:`wr- >MBH U'` d@x-X    2j_: 4a}V }eY9Rt)t-K 0<~N_?9}6|OoE/8V\^n`v;"on/QSU>QY*m(@pj'jsc?l3i3_gg/*;hO- AWEz"tgvv:&5U2AoS)G N)8&@ !]//:1CuVUt2FJo{JZ6[ysc.N_UI%$6JQC:lyJs&J"*Uh^C#`|6#e#Hb^|+R z:w)]"J]FYo[6^]z{J)dgPn) J%ATv3lwmHyZW*X#Q$~`9c* 1Dn,DD%D8( Jk/fYkZ /D=#53rpaa&|Rt[Q).q{M*j4aymVGf38Etsa%+MNL4mdt',<p701 P)Y_w7Ek 40$3i,8 Y+)/>nTvE|,Mm :4c\UKhg:N{[R\t& af9uJQR1l?wY'5@/[|7mqk( fnp9<d(oJ}&)+.I^'y[ZWAY'F+.aCvbBEBa1%`0@1`w_ Dg1|(EJ?uj#/VY`,-wO_ra+|.O_;i''Vkr5R'w&)f.:j5i"u=T) }UKx~)2v(D~TVL-FFsMS%$9 a$AECG { "  0 ** Lzbq9ggPf0|4M E J  F [ I  roi j | m %  4  ;Z 8X;;U[Wu5g T A3 Z W PK M[]zV]VZD`q\UPm=6| R C<  ;VRM K ?wJ2)zQ.9 E: Zsh A Y B J[N  3mG C <% 'I  -1[YYR) k   8 W mA`%zUA{Qd)UZ.`vrKWW s w '3 Q [  - h P &4 V5\$g8 @yN&h0  #?G6c  r  rsZJ Q $w}d*# !`oI% y k^ 1cjt  (   c $* }|2 D6? =DGBNil- C C S / 'F~K m <  ^dRm4< $ >u( ~# ! )T T 7 S 8 W , ) Up  6Ya 9bXv"4m xUuBeN[yqmn S` / g *F } 9(&Qh ajt5v+ LREE-`>0qXdqvS =zNav}8tSj3$;<Hzd6C-  E b)o)"u3YX%~ W RJCzP$TJ <8,oL5dZ5ba#p/RsTrc5 x$Nm[.+N}}u7oc e<IzY ^Xb_c6lE[Nv?kaqvgh0?}"?mP2;IG9p:[@B\dca8<yP\p;#jD "*A\a !jN]Y "4{,}sB.KS30:{i %"ig%t #E(IoA -Epq$WzYST(I>RB<UC k G1|2WRq!)^A15a++-?LOFB`]_a}Bkhr88ngc:7k/7'.4T0W 'Mo_, bv7@_hdV@(]H]TIQ]7 fxyh i+I)%[{E %(D-bS'-gr'4J?Bo9HV4;z1:Z\T/k,aK8[pL IU4;>~\G\2 LYh DSpU9- 07#NdgT}$_$ :+S8+w8D!8C7]cg&o1cn~:vvcV|=o"fIU,c)D T- 2)I/"W.A`s1MovZ-:5I{:5lx_oRBN6Ir)|JJc'OWk*ogt\$Ycb/Q ,1 (Fw, 1Z5fJBL\S?fbHR dWj.^)NaCUu[03xBLCY}Co\d 5G2RAmwhW)9a4ETQ^y?r RoP##C^CFCH;*aKdbTqvQ\ *M=gTCp]Ous"8cZDr |b  T)OqNZ^.N59 l[asbshEl2diTkie^+uUWR:; WA.3=-TuW+uZoN[S`MtKft ~ Ish/;\rW,9E1a1#b$ Zzi~qU3/*i-T# |A& / RO1mY}+f+  $ w =y -Qog F%1N 8o + @  \L U SXEK@ r$ )6$ 2  8 3   yg 3;jZZ3nN&@ u A  rH: [gdgm C=&S q0y G 6!_Hfx R EVW  "  { F  |FJMMPS4c b8 Y K  ex_eKp g"  w  `   Q 4C( /%mj6 5 ~ q1 # T  K )& )ef[RI3 $ ^FjVNZ <ms,TGg ,? u j ^   o8   K?(5g|b[X   Un  c("B 4 +  .  7 ` RD5Gg@5= 8  A% x%/  7 /l4MRB^ bT '  \;0`Vs) J h z <l[  e @ AH7rU"F D\@PV(P2?Vnw~n:2b9vqP,{'CVqq4\e lb7|tkBcULYMEL>Dd0+8pxj(^3K8Wyq1(9E43y.R*U YwuZH{ Z6udnF N !*7ue6_ 4bS&:=8>=L&r4I Zr7`j6Zo c(jokM9Jf{o'Z1K0z3z1UQ4Hn'DIGVdYeE[?2*l^I*umgd0$5yx*d?Ndye.L0A 5!(X=-G1qdX B8Z_EnJ=*i1 T8A *YS'qal/Ow'Iik_+]\>.6]#SsrkZLr.n7K <L nl&d8v5W)hwRLc_l,A\IVz ED`_gwhdw~Rjhoi'8?dIuSL6-H:@A@^\DwGX{@2$rL2jp/aNiL); ,<pvf'<L[Tu{9 .?Vvf&J # ,kJM=Vt$P;M^(eiV<1Lm j>0N1s <brMyd:U Yv s~ O-X%htCNK_w jt6 kR[HRG i/pw-~.= ",,+|S!]!'ns Yl6z|nB1Q%*-3n>WGAPyD3ra`;IF8\LFuM!C:rIA<#@LV@U@i@*^jU52v GM=]Yult! [ *;K VMr" y0*8&pfh^3Bynf(f\M?KC!2 O_J JO;G5VT\ 88Cw=xZjK?k"C#@f_jl4?rX_#aJvxxA{!|m0k*tc^W Ox 1^c}\!h+S&M[Yw5I_gQ%Ubq VT  !iDh 6" =9#  8 W = u p bwBA O_*P)_ !5 ^ M </sX*5+_L@' 0X/64o{H>^N>rm pY 1/ y 0j|9^v) VN * &t i \ Q w d b   #)]4 IW  K~ <? 6'c-6 U X $ W [a hJ( n B =CUPJ[A6Hn}!7$sRs&[?$]0 ] \ W {J@4]`WuBc"vA[  | TU m  0 9 M Q0z `WXgmL Y 9 ''` # B]Vo E |  b ) QL_#FrlX-gY@9)E'(_ a;lyi#&]w[b;  ?  B#T%_ga|,UL~1 J J~  *& K < !5s" H}R % p=     !fB)} 0/f<   q >` Es` bWT&[&Ftu >naGfj{ K,a R 4:72<4Z|tB5 ^  x l  A z . e P !:n6 6 | @ C  0  } ;|+.Z i/ , Qm   !- n gY~`BKw(4X%S X;zC $uuee 0ONY#FpqZddtR8DgWcf9{c/4U"z@9_ A6w^FA =. %>(RnEpYoN6Mf(.Gls~u3T3KKgl : )`:_lp"P>>1ud7s eyut`(.iGON[> IBp@`Oe[o mvu<A\ 3x!n %@DYFA^zw\+6aOuue)3n(kh~]Nb=Hfj,k!1#dg q^d=X`?_quz%qS|}LuL-B|f k*-TFq|Z:C6t5?\y~]P'yIW +(0zL0 ,Mk3Gq1 <&M0d_yg3'&GHt,At2gh; .pg@.K9Y&,]&-|r hcC|JfxHIW]a qiZO7  ~M'Jl<eK5I9XjkQ#~,S$sa!wlV=jlO!!\ <5a)]%{.X^0 "8 N=gRE7L6]u1Hx6:].i@gD;t"i !x^' |1MiteY*jl#gO&SLV9fmynyM?IfblJgq~ epcbK"eLEV0sAiP9fH#r:.Duw1I {M0O4HPuWAUQQhi790;r,22Y(5taD?&ig6W !$~mig$0-9f@[aWy~tELml6K1MU f4{6+Z!z~T$3&L7 5x" @%bUw8nw0r\;>-* }}I&A=c2|4<f X^]25Dc?Y0'$-j JNzPq_x3HurfUGxN_zn|)=d >[5uBV/MS T@?`rTTKgl" YLd$%S 8eUp3h0z tF~WQ9 P11/"H+Tk - $ m=  V+ l X+  EN # i ar}QK~K'  a k o]2S~,5'Q}  z k$ L 3P}(!m* a5<-VH ? >X B%ckU!JG! I 5 \1I'ow i / F ; Pz] Gh H' X  o } ] P9  g JO7W2l }bbJ>j= P@#'C|  l  e4k'Sf:"!S,VJN -z v A &5eiI@# n ! B  , i#[ ] pd S `TQAl  $ y Y)_ " [ ?   [S gQQSi3q  hc S  PCo?)@ w e s ;hD,W<(={6e x  zoR0Mhfz< " : l K oi [ 2 @ j  9AYU%. ~ 0 X 9~ Q U cWtI|Rf oX38#, ] |B;rdt4 h OC(JhzRe$Sy;  ^ k d$/d5}_C   ! v N e  JW ky8)` 7D & % - y  k W:D~_sp11]I D C   Lb!MBwk%Cn'%765 DVQlt0&J5Jp urOUJF4Cu"ecR$@Ph{8:66N8Ek!^hKT9RzP "`N%iTPgM{6N.?V_3A-L\iK57cG:>i&.~Jn*H8S7+ I4>}vW/ N9Z]bc\?w&1[}vJ0HNQM@@G$bS0Bc[`6?f- tiiZJ'PJ`2xpI^ m-%Oxz{D#EzJ3')SDqP6Df@56ohyO4`H<r.A>Ro y[#0"A?|F~( 96JXf]n8} qbTrQBB5Q`Aowis`/ERI#Jf&i_=J9d\LI|&gHTOYeX\iq0t59xk"p; Q$ @{d+bsrW0+3X%RSJc1g/ZJcpsmWqUf>ogj|$u* 6b8Z0 h^R]>[.eiw,bZWfD Tsque1BbRXRcHu]hLsJF9sRk9u}Lm2wk\"'[l[ b"~v j20?X3VqM>` 2Ue5 e z`_qG?*G\ hny!ygF>K<:kIn_)xqbEm>Nq)i8VZj&6=TBt4a7e9stgQ+d0R lJ(??[[I]h B}J(SdC<#NkV p{54#-Hh\?M}f+}2/mA~$!X.#LD&710Bi2io;7[VtWoEjy1^ #.e@UjV=mag>mi=9GJ<P YfP Kv u <nmuE8,5+i V*I{RU  Z f kE M Gz4z XE(3] j  y8 M %k+k#Yn` ! a  o9 v? k } u $E3HH6D^v A c E3uM(O 5     `~ @ue  K Fxm%u A b  f n T0+F<mR Q  \  _5p^N O@ # V  0U V UR 7 @nd]}3 3  <o-+n0$A AE L =Z b "[ L O(DI/[ > P  2 g _ lK;  ;*V`!'h P`>v4  t H lwD2= 6nq > % r x 9 Lnkx&NcC  ` Du#Oe qi K R[g5 3% Gu2|(\x h j x Z mxart0__ ?=ea1 2 [ 3  ` z 'dJj0+ f  } iQ_:qbD  ! a& dGT66nJ(0]AEAe*ebF[;wQJEs; , w&[g3bMRbH}A: U !  # & Q   B c}|)x#90qFP pdq&b}} 'gj4k~}z3^+]qcX5n|  x4K6SM/[B)Cbw(Uo?H#fv!V%tZ5f|Q dX-^'n7A3#' RnEUSbHI'"g&RhRL=&43GNe(Q 9<ZKK!xDHVHf- }ChU)aqJ U!{mNz\F 4xC, O6jeM@xZ);t2av*3,GXY<$X-LX@ix ~ adj@?EMl Da3gHGA9 2Ln$ (`\ue['pz\(^FvtM@uI#DA_vYVf %3l/z+PjwZ1/cW22Za~#+" I!!(Eg{|fjB(xZbzM)@f2&2KKsw~kK 9-3z3|"i+e^O1=.l?5F!|u?H*Qwin`ZcJ-&""u'"//! yh|Es|KVs#qqLX$|<(78?9r<:$ Ps_G]^0jrP7O'8 fQ/3~'F'cV,+Lr%!oIp[6@6 JT++SJ<9ec.@3E-Kd{vh;qq-a `Pvh@AdeF|z "$e<Xdq6!!F:,R,:3vVRriQ7gX\7/$['EZj +v:r JHJ8zg[pHaDzvTkNZ~ 3]AhCVW@r xA /Ko+;<e@Co^xNk0I|#Updh2nrX)_ wELeY$*>>" W<\| 7="V<eKl;H(5p*d[ "x$,+ raY&7Sx3ss~Bl*|65y2tE)dMjHUP]LCT>>{c  r^:[$f!0p{nUtA9dpwmCmV(3b_\T0'B([/lfWj<sgy&cM]V9 c T @  '" r : i  D > !7{9S% pn \ 4 w T b g <~r%%@4 3    W 2 <@<}fO|x)4=W1 t-'V CtUf 1 R & . ]f(Ml*khky b BHQ#Q 0 X a Z &c8d ZX     L k kw&%  M J] ` . m z N  %`*BpmD29CV@ \Wqyb?JKlJk] @6 <( 0'}f @u MVW :( ZsM:W %~ r, B } `  XPqsi V{O SXK2Gl6 b5 Sw[MJO @F PJ P0va{a@<_d% c-Xk;TmquD(> i-  K y cm  Qs G e 2qnlyU&  Bk| </ >,F D % b ? b [ 8Q BJV 6 b % rMpkKq(] l q {5`kbK B]'= =\Z&Y1SVg(iXqIt&/iF 7S;L < $W= '~ + h  cI ux g mm Qv B  ~c # `8 Q#gC|p29a\T83uC,6/VE_U@I(ery@^x?%4>SwtbK!GBO0><=C= LTh:4Tlz\%VZrT!A-gGcM;Gs8,0` +qDO]E?Z#peB.*ji+Y2H=TXi%?}~M2F3[VDtgDT2?,_N$}DXo$=oR !(R:>Ie%&g>i#oI2%Bo{m:U##`=SS[,5(+2=MLMMM7A[>L5t !i2>EPA01r v(@I]L2- @Jg&;,g@Y`u6!;y-8#<to~<:;:NAY Y+PMaYIU1"L6b)T"j X)MW{C2H0XXWKbE9/2LD%Ei+(Z {ZN}qHi*\l-Nr P#'))B_^A(A6R{U?C_9wr8r`]RQFLW}[E\RF.t nf~jO\s 3)a_sy h*"g3pz m[ojFsCH)gF*el"eHQg 5xv.HdW8PQUHn&(1+Qp{$b0#;FJPsHgOJJ7j v&*X~Y4Xt&c[Y.tmkqhz;| \^CIzPZDHquBRR5c`UL \ VBVs/&LQjFxo}XzsY0'K , a3gzMF"l7s)YLdwFz-}rm{ngOd jrz)],.]1aztMj2!fjp%E;$z d/JrQ85,yX,C/N_b^\3GFz^ h\AIb-w/?87Ux/\G}ZIwSS4[ 7 V  6 qpDeQ4 rj`a (+sY57V$14 3 "CE w+c,6x %Y-Mk;a*LPg, 1VuK77]<: T 6 , Z N Q O  8J-)B= ]  P G ] tUJ6 #:L)>g~= k k 6pQQr23W e O F , p$ R$d {3GT_  ! d  ' 4Ce5n ; 1 /p% Y ^ H t v g87ONk` |jn )n V P > FCD{-d<V. C \H23PGEyd9Nbp{/Ho A* 5 F #ba-qu HDIB] b)  * 5   & z Y[+[,  %<T].s Q L i jxmz W  Ni KP h} #E b8A7Uph#t m) ?  } : 8rE6 Q u Y | T3qL $sveYG  5 !  |  A  =IJ N 8R ! z y #i Ld ( l &N " > #67 ) SQLK>6%!\zNEP6 %> Z n5+N6b 3 y gjN&13m=vCt@TAK 5 ! y z ;QX9nV ,j otj Y  J fT /np>){ef + l  ] 5tua# b*RBogdfZU5~EZ9{Ew(+-PjCfB5V&xkaBgX1`=F^`+|IPR~},Wl|<\*xHE}hp@ }*n-ojK\q[2F^u>Oxh#Go%@>q&#nWR2?:Xb55Q<J{xvGi3Is_ };WI`;HG;:76I[Q{_F* Pdc>w"FLy#9j)=Eq*dh$!].8h&lP$\FDhEmA6=\:Vd)`b) %B`s,SyX#+Q um]`crl I-r?z>f)0 2Qv6gh@ %<QW`3uj"8bQe  }rM31p9w/p<g7^ld|FHbtdQG;==898HBN2$^ NZVNP.Y D4vi[6gCm-XJ_{>Q-l(Ur}&l?pU=g'? DpnyN>R6Fx*!d)rXQlF4?Y _R[% H.JxE\\ql)PQyy9FB|iW #M)EJfwn`)IT h*k3&txF hdP;/3=@cC|r*I$R< 1CALe<Lw9?)/zpYg#{9SLny\LO"m! I@oLOy}W-3&(C0>"{C{&?`*-.5\F>;5?5 -FS`2.HF{*h$0]]9F6q`ot<28<v "2?Z!|2tyb[~H2l .  d &B?7@Wj %Zx^H+|+b05ld1_pR2ZV}%B#X[ith'^ Mg [.|t;2|/77%J_kX $ D j\pc+id:/ B z@2 Gi  9  P 58 b ?UyoNYL6  # ibU@.$adwQ<{2*7 h  U <6]mu# L!:-  I o!   Gk7d8XXf:*(  8 B`   7V[|  7 c T M | O iODS W9vBx-/tS    @ g  pN,.[] F r! J z/9]+zHVSaZ)8V  g >   X<vq[]~_QPvx 32 { y  I t04-g 8S ]  =M  @ ' ] 2D j7 `8 a a  b W O KMp[SYh) ' 0 8 Sni23P@=8Mkw@'h_^*5  Y ` G-#&'>jf"v L0 : Z*gWZ {  Z  n zK|5h<.d1J[O f  ? y  o &f [ 2~t  R ^ X^@F~$oo .~Xb)oCBw]0v 7XP<_hC[}oI55fH `W &w G 2  I`';U99 kQ9px[#< @=OV&!*sJ09~7CaZ6!K\}F.*>zileM6y_'}'ejPP-t {^+UNC!bs3JfaSqV~pi( QO6Rc-&5j_H6Ud(7Z"z<V@44K[Ek<;5|8(Tq"=R^.+c- mZXcb#8gAaOeC"s>a ug-QY)>{JUUDY!G,wlkB LkL"3| jxh0]6|0BkVs06}SuQ:& r %p^:y_=Mh:}oLkX;.]2TY_f,@c_8JLSASC}tnV10dJ>,@?\ VQ:CQVi^N?"7?QFtY.w4*Q;rjSF?A_Q$|e `^ #9l[P9OZRY*q!IQ-J,J%E  UahGUL,?9C0&!XU%cbAOo<U:05@/K^XgUw1< Wy%); d0G4F4}7^QV " ]"; 6jF`C"_zC8$QUv8ie{5tFwo#&mh@@{ ?<HD^[:9.%> !vqb&N5a?3|oW).?4.[[=#{ # ;3BHO`A*~JO;%= '?P.'~Y<.>NCyx#|)ln,ZJu]}^*$-l63!~=9=afU~O/,x#/qdz\Z%c3<fFem.KzVo U < N  Z  U ]:+aWhn.sb *v{8c  h<J ^59oTs(=uY40#opiNlH{ 8y c[e1 ; | ~ ]iY 0vO 6Gb!%@k - L [ 2 Y ! EM c M =   }I}+6 au- $ l ^ AFkj]D(9 G $ ^} <: -4B, j >EB3HjAPhSdRV0%ATKWH/7 p] uM &P R} 2  P N= N 'oeEE|3J w Fa% 3 ` `  fI*\ 1 V $ W ?  v "H bXq|g$.KL { , q &v\y)9 ,vV\ E-"L&yfe-mqdn     w t 8 S A L,%Wp : My $|a R _ " : `  R1 X Y q8 U( :!P  ju}If  | 6 `k6R@kEUF.9X,     )#=bp3!nfb%(@_4\'@D[U]%s:h' F 3  -# m XT{Z80n   8 h  U rh3YQp _ ] (0h~ r& p n ) yN~j7Tq%*ORM}8i#xWO% t 8 59>sONDDXI{ 5Qeg@8_EJ  >NenQ> FZGQ(** 2 2 c R / =)xDH\  s$ } _G )-uf[ ot[vDC`5)!oh;nd.n[OgQ aiF/@TYjlF6bLs%:zIzRw fU&r-A<fao-@<4v]dn|hj)=2ebiH>uQ/&AZz{(JbAE[[TTH%6q</Cj 2f@_5Ha 9s}z0\fPDMg{4prA:g%[^k1l*U0hq9q\}}UGzk;Z[Z q-o4sTUUd*<nj&-*W $cWvm^2 }AZB Xx_@oM OD'$]"jKTJwPpM@p&ZFl)h\T? 8zDqt|So>Ub* 76iA `-C8e>xnyh]%X2 C|O:/300voixE|?9wZl#M+M;j;f;Ts=Y OZdyr& ~t}P*4QR 0D.{ ,/"cDA;s5QczW|  aV v Et .M%Iyk)'`Vq@    = L $Z!/M*iOR !M K}5) > I1v*C8rOd/W~pIIc*k  > GA0QhqAF2q m xNi/l<u Df!=I0YJh*X2|9.mg# _1"5_A  e5DcSis,kCq+UNjS  "sN}uA C Xi#W9Y0u,}%F.dO\o4R @]/G m(C+g/Y& pR7>Bd :$V@.Z8jDHD58Zy"Kh5SAt m_VMrpPU| u 5AQA:W?|&f[zHA;"[ dwK9 (P`Ue:RSej|$ .S$g#&6YF ^_}*hhn,)8|EfVSZZcYon{yAJ}|w#bLguIl)j *DD^Nz`rc%]zs2tlTSu)SIHgLWk|[rFTQUjRGWU1qH7k8[;o+T*iMk^ eTr|~t kkvB5hW!5J[~^N/VhHe,+T/poc]3@c|~@v4![C]]}""[A$:FONnw 3~u%LDjS[9#YV4{T4S>_{EMbG+j \kbpSqdSH)w"0xpVV;OQj7B-I0.$?fKs(/,QfkUS1% lUE;^gS=U9E.MQG'(QPg* } B@ '+HVU#!f_ }pg8h,/,0)orERRW^tbQDKN*15Gqa#(o=( ~kS.-f\r'51+@|P,Yx5O>^~Z_tzxau<{C, qnfCsfy51MSp?7GG%j++/l8~ ZzHY8NTgm}fo-kjK s|&QT i5>P1D#[=?3uk&eGb&VV+3(@o,&%cu|QB4|4TCa]@j'';n{f]Uk.\w]*|Aje|=$vbi_/55FBL+. P`VP #vJ  #"0bRzTcE 2 e + g R!/J;Zcby%  **$ E(jA 5k c4kNn$G2YiVpl3b6& s  J&  pp2-)pX(?3y Qv x yEo  c c PK  F> | ` s @0D^ ~sl iQV B 0q + Qu  }\ Rj 6j9 q\gr!5"J8jP  [      yZ ?O-yDD..md} O 7 1 ni E ' 0w   +3/ <~ul6=l Q c ]  M gtgO. Z u ! F .}\YY"`5#B! 3 J> ZF86 ^ 8BusI fG,}]HKwLM2SAao KSA 6  vo iA R9  A` :L<)>iTDb* WBP+fG o )sz { Y~ 6? S 7 @ & 3 - C, ZQ_T'c1P/}jGAI-.\Tku6H=/  ;]Qau/N2 1!*7>lQ4^btpFLL->`">&{.++kH!}~h I#h-8LUQnNZ\If]lR60,J a-u]Emzr\  /:C5%0BK#5Y7Z6}RHe_PNm> z\5sPPFvQ+KK w=q{i2SbD.2^!1;M:QT@ijqWX;rCk)  XL1]1 i(|[5  Zwxb.&)`c!~o\__A0<tqX* 15; 3v9x/7!2^~`#g=/X<0iR>Z)oWuq16SB* -@`yW6&H'q@R95C/t<Ocb]G DZP(|PKUq_]x#b@Q~\N#y7RdJ1zpj5J">.I16,|j!WS|u]=yzb8" ,oa x1hctD_+n"R KxuN)i$9yv',1/fkutrA<>I<N@->#oPI(&QJ MfvIfo10$m"O1H$&v^/b(sF=o}\&%2Qi^0+-g]Jl*o]Hk`CnCR>bCjDQiJ ~F^A7Ms3WR%g@GrT4r,6roe:co-A&4[LSSBn{u.z77K-HmfoH><OLS)|ToRDWwZNX|XOjO>S <\rYG!FMUtdUzJX=jD?Ez :W(0Q + W   % 0MA0ch+sXmp]  )  ! gd< 3 &l Xy4R *: CQ   >$=M&Wn^ N;nl! i|Jk#AE  2 U x 7 : t>_2YU>>#T<  f   @  t:% @ v Oj  O 1 c ~;f(oW7UG&   & "L  ,V k!)_MvAFKQ08.X/}[}X'R3 (  E f k g   E:P7Lw,~>`  t6  2 dP myt y5  ' |  #  IS\B^ TO.8mAyfX% o   g 8 -N n F,=o* 2^xI5loikD!  ;  GA    , oj5AkZ; x ] fng%?A 0 /BR] t 12 uz qlA G 9ltDo +0 vP~8VG ~H r 5  1KC>VMErI*JklgR|}JxOQ>B-4%FO"w5vUHD6 AVTe@M  ^2o'f QP{$rL~Vb Y?z* Jhd *.Azl"1d8Du*iaz#Mi,V<3+"x+f">O D*IlAAf/LO i #gSCYcRqjc8^_drx{iHmd9< :GR<&[lO"}RZ {)u5#ys("/"eB_"?.q:_ZkB$D4aC7l|i' mn$UEAvOa`fP6@?N_seg]|{tZz79]c )hTC RL)"Z,Y I9gm]jG%#09D]!2bM^(#&t0/ "5YW4!#Gv,SPtxg>yCkizhXgYuG8%FI,C&DFa:KaM>Up<"]$1Gd[|rqmy%Io],kw'r~d:a9||exIPX UUYH4%,(B[Y5 r|gi-Tz1^95w"[C9j4)@@3V<QtQ?=eix*/tT:>Kid<]G(Z[8$y/jgow :!SVTV .tUGKd,~1'4 wqb[Yp5> lH"\V!c-V"'QU.@&$yIUp @^eo1#98X<-`u< :.y1&-]b =1P]LRfmF)gm{Ol,9IqL'RX:RXWe%FX$J%<}rZx +)-k(w(pv!.52I0D=1g,8[Wkqd?6hLQ3wglSX0ogAM6w-{|/I7n3G}3'^aR z?GC(uJE H Hvjfs$D%} @P c   5m )mp8fwSMQ(Q,O"M i - e z-LTMbNJ /L ^l | v "N cPwq;2-%.wYi3jnml/ZeuzQ Mdc  q H   ; 5 !|J~bJelD> <p0 5  mF~K2X0 U C  "mB,dag&hh H7Dug %  y OIn]!KBC\J00^  3  kK r[XQ (Z L. o R z i ` d  :-abi/4YnN-{]  ! ` I f) ' xmu3n3G  pm Y>86zpX#aXhYX  3j  [  9 6D_Rabb-Emd|hl $ ,S 0 ? QvcfT: c P 6  1 ,  B u6ImK@TdE c  m  } 0H/ +g[I<   ~)y K\)rX cx up  >V + p  KJ J g[%"WN?u p Q ; n3 kW`XU w +( * U@zWseAT DH  a s u =p=]vc)t}[ )  c EjCXB: G )\b #X>UFok\g&3N>5yx# }gm87yy NmwJpiR>OCWDe"AwZ/hbBb),p3; aMqY%f:b<uPb~`7I~OO{JG~%& Bb1mK%GaB'=?'^{ptQ_F.yOD0vYJ H-JI6w '=2[znu 0lYOpMS_y#'EE_ L~U5E'Q] m{1~\}o^Tn(LlE_g O9uBJ` U2' BNu)FWi0`g5q|c&EK= QcXu=S aK}e`7 #I\x;K7(b rc[Lp!P& *]h 5`jIYQ_5cxyzusK~SWRu+$rx.s%Xc\4F,0&,y-l'N51C_QBCf+/(G!rz 6NH\u\ P yxA!+a ;U7;\p$hw:{VpK}A7})Z@'VCd/73 KgRZ\+-UfI7_ECt~Zp4Xd^pxDqQ@m34l< kPR"*1dVUNjs :E $b/;'23'LrEelf 4  ! aM  F7  ZK1  \ o 9 UyxN<!d` z l A 'jX T   ~- H!Oe:FaO2f;i_dz7{3Qi=&! V_ yI4fZ4I"#$4 e E c  (  [U / \6/Kp ;I @ } hF Z> 2  @ qi2m7  ) yI T &e(:J ,[ hd3B_(U tAc8t%5& [  F lgH YngH@WQ ,    A Y ^ B) }7. R C  ~> z 9 $ NM"<pc] (   o   .gZfyE3uD<c6%EKZ05s4hG [{ T c 0+S{52EZ R |YIt  ] j i w pqH\J |cSVn T YUq j F f r+ ^- F MhNpZp Z * e  ! C 6  t{7*7oiIh<U<d}i(|{2_X^o[ U! ^n-FW  HYfFXO^Ia iM  \ Y) U6  Fg  $ %J\]ms?#8 cgv Y y  s^#XLw!  G > y Si<P}2).C:%k^ ]!)<& ^P 4#C%eiH$/l,VX ;=SEi:)LY+, Le:=C4. ~ \azu4$f} y9eY:eJ1/Nci`p" Kn7mymc|`a)^nKa81zyxp1!e 4QCOLv~Ap^!OdC`h@5pGN1r1}@t&n><P S8xry' >;HZSM47$EM=+ 2l>^!Nh3bm_\[uw 0 G,Oa%%cN"%:6]o8GR0BmD(%86T%kzW5':WK55U_KuY_tqRu P_Q& ' R9s<?:guv3Sc0+!J" m }\liL_RhUe.Dr<#Dahi\QGB;* %@]SX0@D&.[Hbz)~GXAB#BE;{%bo$_bW-x_j~ ,K y(c ODh@-8pm3KQ R%#Xu#0.m9z)K\ZJsBhb$l%!.@HtjHyZzumczji-G p 1y7&{S16* A?w>3 {#'t&5`|\x@~oEJ/=w@T&_SjMF=ad +8 UkSh^X4"d p+&lF~wvf>}C=.rhcy37L9Zj7 Nsq&lRe]B g()  I NM?eVl,kc1r,yuTlf\=mR|aMZ}OcFF1KfI2$x &C[y Y%?z a^XJ[Z?6M&>,<omA,|[:K\KCU35;tZ#b0\' -Q8D#H8lv8S/BN)7KZ 3 )&EJ\ +t /O`At\I5d5 6 v   ?   V  sE g R| ] J 5> /] a JDqKm<ic-nXD, g - +j^%1UFQ*{5uk7gqD ? Q  N #  c d2P4*q8x l ' ;  u '@:   RU- h R Qt;7  Fq { ,%#gPX |r%(_ Ta . LrDIbi  h H s  ~@\/'re$?!p\Lp:w t b B "E {_-*!t h jS dbb2f fU m7 f A 5y   e  "]|$ 8 + {S[&1b3Uk 2( ]t ) K! {NQv_;}uQ    J- 9Da4?nxH2qlSu 8 I g{ gG?F6bWlStO  Ar 0 F |l> R P Y 4 /{XQxbq E H 1 '>     .KOn !l  PW  ru| Z ; /^%  ~z W sr A6RpI=C#FjFM&8QZc\  k~u;t]62XR 0eLUu 4  {v&Xv]> c & T     # bam  n~)' e S  PXT~ X@n%KRL+M}tQ|XU^Rb!nX) F|RV?W.-{:2<9p;84ROsTK_\7 [:I}1KvcJE&#1uE5%7duEby-G"Zt"@}& PS#,lLw-vAh"5\wb\CR0;c|Ww-W`we-UPRV}5~5y4UYaO"AQ:^i?EoN[L<R xZT =,j,{ ECU1vCK1Bcl#;iS[|*[*MtG8g5(kW7;. g b <Y 3VVs\6`OE027LEVoeo~=s3 B)OirJrMr*<` (-oO<#,]!e2,>\Sq/Z+}&J> Q_O4MzAk W9DTAW&2q_l"+{f> HkqmIW5avCS|UsYf+Nz\m.($]:WKiX] )(w"#{eJz{uft  ~W)%X2epr$?&eUsr30f}RBa= \5XWd>< \"~p 4_Zx  :3]o'g @\ +I5`jO )*\7u-!1Wf<x'pb1o8JV fFmL/H Ba ~`%"LB<KKco::IdU(r+P>[Sz0s{:#{urw"Z!h7KHrz%w\Bc3k=(% y.@( hV7#ot LbXPBd2eY3H{.-~q`?xn\ < ] SiOxEueR%!|IYb}9lhMQqN;5O%Qp{dly5M= % T /Z(  Sj1R$|=.a A  j  3 I + $S!9)i-Y! z V # O kr ONO; j 8 {  ]2 1/ v# )@rb=@sC@C l4@2ck E  Y 4[ =  ?h':u8 i u (7te ? L-f -ShX 6KePO'| TpI!u rKz| $k ?/ ghM k / xL c<,nr 6 L d(JT %  l ! ry2y Cy`NH/ l M 9 =Y 6 W} B[RX[ ??- tE\G& PvOs 0 e p t8 _ }@ 45{ . vs N s %^:0 & f \ZO+ } H( I ]{M+T 1  lo n  {3[p:Xz, z< . 9OwgssYS tU wy_< I % UV    }Pb2 7 e \  "*$!mp_.8a0% b ? 5 pnxnR!M8FDP0 Z ]ANdN]@0T[ 9 9%j$YSBEn[ \- y9 v  ~JB;II#g Y~?l.iFc*D I\6GI. ^P{ua{Np%@`5 MA3U;6_.'#k94*nZ*"Fsu2;at8j J89zF)o E{c8Q> #i9-Wb(te%u RbQ?`wV;1QoMY i)-]z0uDf%]_5\pEf { ^5}ajGlp(=IG>lo %XFyVh4QBM+ mL wW:jaS4-47X'2/_P:=o>*zTN PgM$DR3 +Ynp e4>80 T*qj |>VQ6c?hi105STNm+p __<*"%@~6F_#?r1~3{\?PD7Ljs9|9ypoudS90L:?U?6ms{6^SI*wm~QaS?T/mdN{/vZ1 8l'QSjk?w ]$6eAQ&|}*50[p;?4SqV~Uf(I" B%!n !oTA{|G%# |r2%'R`xbR>T aW9N%(Bbu>qJK*yVg#f/]TKu8:JS]{SQquJYD>55N`d 7MrM-xrDy:M& ;|?(m90z4)00@z}3i++6(m ajl7 ;ls6R^eY<L0<q_?['/tGKR1kVM 0Ka,UBz n3Bu>cZ(PBKwl$pD |{Ugk1E$[6!BYUqxe2A>;[Ognu1Kp!6bE} %/sknJ>h~ /  gCy(!`!p6Yb$2GXPD)0MPlO g hw*(G0#= =ElBT__g N?JoyR_ KE$ 'B1+v.=}1C%.R@[LELO >&f)  "s,1Hh^$F] 0AO )ZQb WvzozkCPe0[w#t uj %ZF<`e n    I V J q S*Z=FWRu<[ ; ' km ,  k  I7H^&:v { L  ? 6 I f p 8%"c2/YI%icPRFWQ5,=g)47 * P7 @+    JL6K` ] oHx D `r 9& + %0 6\V-5J!c  s y   H Bu rn79E [K  ag   # = EaD|tlci6T-e L -K_*U\ /GWZFrqcg G, N :^@?>FA ZB "b gE{ ~ U 8 uF % 4={F~j y , a Rn R  yj k cI?=U PW Y hKm  W [ S X* ZJ^J'^lhDehi}=0VQ_G%Ru<#r"MR- 43 $ ^9{jpQ s ) = d 6Wnt" iI T  AA O  qV    q};&r? iW D 3 = .  |F_[rf B (k T %BlnHy}h<Pk*.m6Av4$#T'~LfQg/2XmQrx;T!]m%X<RaVDT] ~ m V#7  [iVf k Z _ J\ n ! jFJ!0@iZMf>K<"WB (7HC(Rr1k/g}"E`hRfxkIaPN)v4%J /3bjK!.a|X=2= Y=yyJ,sgo}nb8q;s%]#~&68gsmR]Cx&xJx8 lVz$q4rbc.H~T`Vmh2x@]\ #5JgrR.^dt:M=jV %qvDug9=>I=re)Fz:a]o|["flu(Q,DhvEU zM>ge/U5Q?$ >; &#oa=(M\rDx~|`1{c8 TK{G]jva9Y}X%g1Z,17f&'iyHS|10+e Os#p@LsXM_#]9y*:8 Fi#R/1D}Sdts' 1h?@"*=Eu.oNR^(My @VE[)dmiZ5Ne =m"+640m~`A43<ZZN p*jzJD9)xI2U7.C ~`3tK^!gR>NiakG!nI^9% humz'15-'.&@LOE^cf)Q:#%vQ9CR,SL0.<'*A)#K1Tg2TtE##t398"zYS7&JIIO9^BzsV=,o`.:2K1` cgPwatW{5aNemjX}Ef&O"H:PagWMw4bC%BDU 9#sj*%G;~"T@gGD ;p4JQGy} \_o@; 2/H>BNgHd1TEP^H5D~47w9HC` NI0O}qFvMTH\wotA af&ghm)-ZcdQsI-S3| .> *KWA5A*A[kmT {b%p149gz  j Q di{ zWD?`iE""y cY|EZUv"Z1QrYf, Z ;  &<7fiAgd 8 V 9 p K n #t-Da_"VS4? c 6qy ~C jL9u~.%};lrA^I 2 * Qf`=gYt,y8Nx_ e ` cP 3Gp5LNNb  } = n C = , S Tkb/o=D  FI T s0> ! h  t i ;a]0+$  \ H  7 T9L|(>' _I@ r V` VQ5/T| {1 gJ W K^6DbN&eN+rs C  ^ <   P= 7A@u q 8%) | "oh 1B sR rd _ X GI \UqMz3cK #~ c  d 7u3 EnstCV9#RR; k qZ);MD(6wpA{hE' YE\X,#g9(H,0O'Wc>6X# _~XJ>Mc> Rln}M;]xe*73 6 / 3N>BN ] D\N{ 4\@5iI CB8cD -#D6,N V_17N.{ \|u3pJ}[@3s)_rv;SJj.2 (Aw"leZUQ'U|``\"gc\tok%H  UO[Su,&+#q*i,?uNVzA\fM5(d{Fry25wUNfvh9-O<,s_#rP8_DF7q~_Tr5c.e)cs8`X;tmCdhj;+-E60YvJl 85{b2 .Hj^N4$y~=-$mg7.Wi`{[uNybv-d+Bz l)Fn} 4dpgTt,4 Ke!g*0Q%8A,PDcxdhflsx`&PZl 0vojP7  i]6ZWeD}.1/wG4 T;J1s0QB2dr#LzXDpg\:@$X1{{T2Lb,n% 7^NUPVc-) c` "DItCms_=u*ln\9\+HVFY5s})](}w%T~G3$omv0]^n}e 5gEEq/^|/E9MOi@6x2NpujF#WHVP M9?px}iCTEJ ?xerzDk-d_]E{}cy(f7 K8To]_Z]*a"Fj\?}}WnSqRy2!OsdcasS `U Z~n_r9<Q%H7- `!/"m*'ENZVv]$D4\/9CNm w73qxTDJTl= _~3lHN-YtP%ipqrv&Ms(exGqT:,|X=GVikbJ?A%cgdl,Yv]>+8b4ai:Io / + d FobQ\WAn^HxRr(IN{1B u7x"x',{orF1&kNB_{ I P [ :hr ])h>E  Y  * k 3 P %2kJ0]x 'p} F !<< D +K 0 ] U lEZ9m=h4_  x(I[o- Jn q0*0;[i5 w [ @U \Z-~I@A.c s k ~ " T/om@3>BWMKb 0 8 M |  h>#2\/= &8"S 2  R j N XF'2jtWAZQv%p8 ,   : q 9 R Uyl7]R p ,G  l :Wqb(MmhkG fd,  G !   k5i  aV Tsg<} {s  < HR4hOQa 4     - XL/?". o0r(   l  R 4I` 5td\ P  `V { B$~d<W%E). }j cxj =  C0#y({SVga L_ q r ) 8.`.e3 \ @ t    2\ qk1W%^P7kH YQ    t ny<.:Hf:s w3VSo2w%Lp%ky4hK]2 lHV$RH6W 3AW92jNU6tFB}b'iIyWx&x"*_*!, rMTVV'(tmI5H[.JtkGUq%d.>,EcjR(UW ^&#C%9nkNH[+ gqrBup<b7lN5rem'<%FxY59j^EI:3(4\hR}g8(kJwOG4i\uf glQZm@/,O:'|R?+"_w|Mogvskzq=L,!(*Aez#u4>KWiNh~JX1. NCjXt5a\:;eeZB)#NG R{MC h$?H,IWHhU1s 3,cP.>"&WjS<}TF'%*8( qW@=$T kEQ$W5f;MNm']'q5) g^<V+!#M7U=EXRVO9Hbp: VH~T=H$R9}_nZ7CO XULPXO Fwmw^oEnF;"cC@J:\q(7{8C4{JH>Lv{ukH:@! a~[}w5:U5 h 9F\_$_bz A*;e!8iKwDgRkHQ jN' n4 NNn{pf:"i8+K.c=d%pjBI41tZQW[[(/J;`Hzy#_zDq"k#?Q==2a*16Pa+\+ D<7nn_HN_Jil]h W3cri[,A !}\q&7@~**P; a"wA+zr9?v   [OyE Kon C m#2Ag39# j Z 0T> Cf7lIAM]zG/4A0ohv*NGF_ Z Q  7#e @ z AJ Yl.Q{YSi `Y  b 2 A |*  :  ux u R c [] D  b8Y#p ; HF}x~7  q [  @~MH sjr,WbE~<3#:n_`Ipj4 9 / Z G  K: r zH`aQ]zrVj,'H1*oIGe@J;`EK(g'%M}\s!tZb"UtIe0 `j}| ^ cls~h<RW^t!dLluna E=^   6cVA78jp i^\vp:m L <; ;NGb6vU(JZ2D)jbN8(I[w?Pu8p7*:8QVbIlsH-Hg'+b*9 Wf*o"-j K,SFL k eOp<]U9~x+d-xp"jw(oei\%Wi! }O\NI $1pdaUr-6IU>~][mmEr[gJw{_C8s0]%\4jIvY*jj4_Ej-Gz@>kG"5m`M?IR:WwNxnvnC`c\6ibjh{T|uVsqz6e T1.Sb5y <spw*F  mLBu{rnO2 67j p"+2?cwV|M(*mvB( s'5mvGj,$! 4FXyQSQ]ScI!#_f4KL`kZlVU[a]V&D VO?0"1m L 786I' ]Y1Tr?f =f]|9wN#.F#f8ch(tGf{'(R0WO3 7Zx4i?F{Jm 9x1[] 8u]]>4 oHT/S6]@cCSXHK0VQR\JsXYIR@qB&+p ]a*0QytsSA0C#|JeDKLR`P /NZi;>R/tv+ [,*'WQ t4 \ _&{]@,e5O})q|V@-Y"2&gN.M"4Y&BPc>Rm}u  J\Yq\e&OVB&13L.M *o!;4)5 2bo>A G*}b(@/0*D#z.[BYLJoRqN'l%@@t[]%]&5TG?X19|M {oWG][>  | 9Y3DrW n_xs7'tj4i 5 < q RCM hE{y[ x{ 1 Y $ &wx5+g%swD7I|Cx! g # <0"]kH[u4hQZPkDeI Vpw9RY11_R %p  J / L x  x F^ xzM *X I .)+G0'o 9l &f &:0xK|p M  k ^(3jQM wM3/tIeaQ_V j Jw ]  i ] x l nW\yChF=qngD X W  8 ,   \4tb 0 2c y1AC Pj i-K= i W_ W%S Z C pk kS#Q> / 8KTO0]z%I2.j^Um X R!K A O$o G I `YrC_R$ KQS}   Lp RN URti a 6  _ ~  7r,AB%[ ( & G H: g +  g^2f }dz{[A ! HyI$;,~o Z#Tu/ { & ? e \ ?N=DT{SRG$ }qf3<  Wd   JqEv6J& V & T - i p S!wpX/9%o   ; t 4ohw'6G_n- c }M ; C z Ufy!4AJFw!K-(8m} O9hQv.%cj"E; ~YtwvAk.~lB49Q LJu_6~N_ bgaz |Tk+4N-PY0{iu6xC6^iKJD1!a5p)BT iWf#Y0zegXs9EgrNk(UibI8-.J.JaKz>-~Qgf"CraEe)^zm2B3e~B^f#}0!gp_w-u[ x^"!oc#wBuGB1 lEE>7dza >, <4j@o@8rMy `Q"Can#mRC8,fF=lYYrdtWA8*92SBc>kXXw"v[Bn0"S`q:Wc\s8Po_lM!gsUa8|F. 1Pzr~8#vb}s/ocNbrR!V76+e^Cg q$gxcevsjWqMFE$7/kU;uZBE(olpD e")y]hDc<J!omI[$?_ Aa^hhvl%]U ;o4IK8{|fTu:w(7WI DB2l!h/Z/bYpE'ES4\pG6R%op:Nm 7]ND ETVeJ%TQytdh'nT)q=)9fkj630NvN g#!;$|u y*(v kxlO@Cdh &/}1DJ"d e*0A((vg;;yKNHm%})@$I[B2q $I`)UODard FwC`+Ujsps/X`t@;mxT# #hAT_  5De .10HV^1% @Vm! g^`n(n4]*("{Z7Q V"? c  x>|Px+cf]JlEdAgw&*VvR:$)|-Am'"0&07v)>iyz&UHh<K,9d " Z SL+)O=.#DvNu . %NQ`/=' &  (  sTaTB7%vT`O   | R n0 F\|GB0 ;wcpTBjBDbF'&0PRX X .  - !w]lm'KSFd $  +  t!x  q T  q =    \;+i#z|~ D  C  ?   ahQUZ b B  HHwC ,s{Dx], B v  2B=3 ) S9@ OD5NmfQ Iv"vb?( IEA  >~ x ^ ]<mhgE{< [ij\X!- f rIY  u ] vj S % X 2 + b1lz(vi TaKUl=\X A P0 I | sz.4D  Pw+ : d p& &T1   ,  qKy & xqnG^P   ` I Vz  '5  uB l c lR-[Xedd Uh)y49P_}FV[f- 8/EW$oFPT Ni ;e-j;59GGQF}Fn {7V7Zy?7n$qcz gH(q&o"R~6w#Og{Oa565 X_+mBjEw8H",HmGZzn$svP 1/z65JS)GF!"k,{6ATb-nwrvC^F$)GFV!RF=E__<1kX0[J>G jfI2QTSV*z6} wa$fPh_J.Sy.WJNT[Yh9&orl;0k\m^"W{^^02k~fvhU07;T Q>zK,pL/Ed2VX{1 (A*,ygo~t5 [w2  { x5u9zPgT>LRrL!K~6>Q48?<)E$q<u-gvc` =qwwC^uF[,9fC&8G#.OU^x&EiZ<bH"s! t  ,h2aO0CUg(`-A:77?aOzb6s%Swhn| D%D\ro9;xR GU|UuDI/Kvl~OiQwhi?YB6A=,]Q   `  t vS  - .  7  Mpyb}lms- NCbD!6  ' y9 ^t0q P,J4eCs};X,KDw{ k G . q9UC#adh> Q 1  = U Jm saf',' r I," *Wb V 4 `,}7yJ v!= yQ V- {Rm$ bJyq0   ( 7 CA ,M'[~J onYZZfq#UB y G  % .  @ D dxir.OM 0^  S K a  Z d11 q  Y -2l%w1(U }h  j f Sh{S3 Obt o0@q2e - 7 z/KF53M  ZF  C u  -M  E D  x ps 3. E,   f9Ufr X= w ^[EYWD7\cO / {cVHKn'>|X$$J4`fC^9VrW0KjRz ,)(OrM,Rev1S\8|[V56RE0qCoY=3M,f^+R\hjTA?C;!>w jVs;F&K : 4+/`L`8 $|a68 *_Nai\.G*T,!c\4 p@>")!z[.w,Mv+rxqvBcW_^W8&(MkT/>B_!j0; I FWy|/t?:?+)FCR;C _.q>  ZWA2[[hP ~=8sk%&, ^<R8ZC$(k-Wq,q6(W<W e:to^OqL=3.;qTS +~:p 8.iw8l\pTw*U <B 6PWeUqT|nyCkP-!Sf~EgRk Xcf7g{l;2m krV MRedobC@2xc13x <kD5 agqA/_!5UB%e2S!F7]whEgt{4G/s $?TDhWgSuNn\CblrxmAG(asR?P;eFa7d$GY@XL,=kSLOJo"@$\bh'at#c+3qdbhb&|:9[cH.S3G[aZK{3DvUg 1;y&wt jFK]4$7uYc`z\Wa.1u-[*5eN'WO/S;}E&B%\4R8S$%'gR32Q{:n Pp=o"~nhs L8Z }CZ6u  $37EG\5y5$E%k}&9 j-@o,4& Xw*u7h[dj4Iqj' }]^v7n`HRuGNW^ QKVSWUOdS / i&xi+Ii gWb'[69aD?{X*-^7%CI oo4@S0,u,\rV=>'   32s|)<v 5@/"AARwmB;r /~ < &Mb(NCdX? l. / ; " z>*s^SKJTHxS`~ H|  (B  q  hcWEp]L I;(yM, - ]Y ?, 9#f`c==  ;  m N \Q?z![j d~Y X m O    'UNJn5le AHz ( -o"KGRE|k OYY$TI8Kn U c z Q 4 g~oOa^2NXvr`DQHnrTo (g * Jg, E~ , ] \f 1 ` Fw PI[zS] ~ +  8  zKoq>cU | N9#  q5v?M ItHq\ Z 3 E  c MZt8G_*7!A32Z (  5  !|h"q,8 v  $ } z  R Y" -:`umxbdu = l  ) # X !  \ $xLb7B<A ! <@; r dqmtP{;)S0f  ( W i  s 0  PX~ hT^U_cCUwe 5 sV8+2Z\v ' ' 7W #IQ Q  92z *Z63-W A%p, AG  :j,o%,2'n5 > ; $ Q5&GdA5;4<SO3p67^q&9fM!Zz# xuYfIJACFuwE[ W^oE[I'r5XDSMXN M5G|V4`xE"bjL=7!VP[*$8+`Y@vqT=}}VNO  JpQ*] n'.5d~e|i+F=]0YF.8E^7,3iF#k"OE FZBM\"K* v"Wl! 7BbVP6 RoHsAz+?.S8>lR"!2G mOv5<$8rr{JW39d5I Gqmz5Ze85sFYozu9{Vd:V>iE`?- y  X4sJ!Ln/XxMH>#8|Dxw L2xDk`Q38 1L "Ocl31{`5JlI`okDk2z~#'HYZ<Agz>1Y=5+<mY8V=NR-SR_M,.us }Pf>4A:]5}=Qo` Co ~dZ )M[Jg QKU{'wH-I{4 z IKR^vv`$_lmH)h l/lMMt[PvE0m#vJCQ?Q=@:qiFnuw4[_ WB[`fD uIBCNu >:8p9cvu6C,$\-8[H+uZS5RCiT&  R)G.L\6(/nn6Ym]]IXf&nF149,cI\_Z+2GNca-G0_q/KqnuNB`R kVCOVyj k[4JGB,\n3i'2C!xHahV3xpb>zD~v@ -="kM6'IqRSqEo,#0+Lq71q@ef&OSTZ".CT<`^B`q-o~=-rAZ 1CgKkcCgX5UtlO{Cnyz_ :?B:-I-!wFlN wn1HTX 9 O7Je3s;QFK L Je WI}Jc7L9. @| h44j4Q\liZj&2c[OM h A K fx`0RFp7?$IjL{ X ,G #   A ) sl  c) ( ++WoQ  7w8g  z h    9 |_3He,8BWT)@mnwH;!&aK  +m R &2l[g{3bC}yH       R 97 ;u iCM *QDLt #V { 7 / T h =0Zf-qZ [  K f $ A iM1B.aU6+aQ1N}8cq?{y"pN s d kZ334)$,+ ]H|O.   >>   r H @;K ~ BL~/ % 3  L s 5 -5kOn] )  (c O A4k<O4uFP_0[ S*h\4E '1l-,i*x2:nulSz0=)HPA4$9W{Ylg5#8%=mI2r0bSP4kUV/ouNANNXYN.Z^#3@%UbO5f9.1 |*QjW9@%  "u ,ICqjHpWs  r48bQp XF|7O*Q)4T3jvT# L;`:K5;,?9xqYg0,-:yVvI Q.q-GjG1 ?*hspwTt|RUZ_ Z$s i%QJ/ :1YL*/OrH%q i%-:F[E5zx7qG3.{ScGRc5$da-Y%{i3Q!Q%9^aqo r9a@T<V+s>{n!(W.s1Jw'1:-\~j)eY1O`@>,|3nd|(,`NWjEnfp^ I;@l=@kqMo(2~=r8K%$# @@qS?Xd?w 76OJ-NFC6/2F|[Zc}6( @mlPq@{upGP-A l/:MBuT8EfQ=hVxR4]@'3^$F;~|x1Z]bIgA|=9Nv(xSh~,JUi_V`P:/6fsG-.$ [_ wy(m:xmeU) X2^&-ol 9p4^H-eXyEY2  (^7$^{FW{}}d%cKg]AH&R XtE;gE+b[as3g0nb:=bKfY:,! &f4{ z"%1;U'$ ?u(DP_lA^s1<snCmL#qd+Sm0f )dt } J +qDm|o 6aMS[nMz ]Z l )\9r!#(Y#0) x/8pE)}J)Qy{++#+B}  d 8:> 24 9'^AGUG&P%| bw  ? [ N  ! U   NlG0 L % +Y0 N u{U> z/!"(UDn %~ PJ :  e  BqVnYf;4_-~QAJMpxgp! P 1 9 Wc5pr  - A `G /6M? + eGf$J .Z   7q[ ) j\< b^) /e  OyvS ( 88qX &P (  Mov Ze"+G ,[(=_/;w6 } G  i~E\g 51 ~_@q iy&6mjj% =kuT.g *qGQ o[N]NN3hs(?eGY4u7F-+:e#L@T8'* QuI)9ur/ dh?}f`? a#VuTb0'jSteEix5jVc*Kax<4I2J[3~k7(7x~=+S>.ea,o' ^{{+W! 4N5XA-O]o^ /YD{;M3c*05 2ORE>%<5`UNa W-+pVS6mA"B c/7uQ )8>C'D GMO.jJ(vUr 7Tn 4Nx,Sy=MmR)3`gNC%$y/!^!4[?p|7@+l`9_<xmSP}=*Kt'2XI^w\AH7*L[YB]53 C\+`^Wq+Y ;nS%~ xMje@ /8:7<O9;D:1 ru#uY&!m9:B&+7 ;"~\E0hv*V~=r}/tO"5LjE__S9k&DNRdE#wk-cwE2fvbjKOjXH+'qsX+0ClzbRE$SAkNY$M-L`y3 -Cb5)U!J),9y vK$>W<`N3On0<XI']/v>[c0vpGU;oE( TLvkMj[_B$lX\HC?|~n!kL ?O.'85%:k{~p7ezYIpdcO>=Zc%\N[3&VZG!bjqhS?+8G1q]6AbTaIfw*tkU`M#\>LwE A 6$9GL9@\-iQ2_J n'y9C>W3UF>~LY7X cWlL 4Swq7bzR]DNsm:H X3:hhye8vn~L*~IAjG~yJ N }?bf*mfaz< |PS7am L |V Zk}L*\}@w:~bU [; U us?!{Nm$AkwOTX5-I~9p  T  La4  v\t*O#^~J3uO- ?E T n  W +V( ' os:y  $7. * sZOBAOKjp<du v/ RP r { @y m  g)s FE   -4J;#rzFED i o q vW Xy F  P$ NW7J2D"(*)  ?  } O } -  0Pp _GXJ m0S>+ n  7 g, v _ |,1U|b ?(7V& b  U QMh? a/ a> x%hLla  s c  FS XOeDD@ u8QKi w41 L H_| ez nyJK B U11jr < >  K!`q 6 < w VPgl@Ez 0 8 | } :xlRz5Th  / P I :T:Z 2" 1Bcd?o 7 5 [   ! }^+ _ imqZ F6 ` A9 ~ } u"T ?l)^P7vPX\" O O 'H<41dC=~Ew 3Q 9[/Y{ 2Sz?0GgeI]OCS V@j%W8H<@ jJZe S\-{(\$, ?0aU6}:.v$ NY-mG? f]q?"ir.UW(\&w4 k=(\Chf<n Y y*@4}[_4TKVvvsW# Sv(6;+kJ9f1t|`Qqi@HS\%hY4ghj#Y-%,STyf G}f'K<[[I{<$>pO~/b@7A"HYx@ %/=9fG?xUZt|s1ZtF&D$w,!'I4C}blGP/ cmhuGHpR%6oRW@b<I hy8|AXzC#q P`*+8.hm{Hm|Q'k>fp|aNV9Q2\x@B(6qqM^Yv@mQ B|?*to rg P$ 6 2(Vm~(b9Hg&\um<"4#)+v`\? T+0V9O&_k%FQHP3xctGXDCG^dq.WaT*+DwZaY0\S:NDC 9sV a  F ea& [f-h b D L H3<($$>o of  Q"mi^" t=Sq4zLVXUIAw!q#"| `/u$~98Cx 6h c2 * }TQ  & ) h & o  H F: 0ODE, {iF dT j^X D 8 x m #/7F<}{  j 98 . c  @ } Z:6I.tF7 &qU 6&qi>#X663o$/F{C E : mG*G1KYn' H zQ~:.= u     c >"i, r3D c_ j + A N l q6ZNK c vO QY _+ Y r |h . @ W9E;k.2*s :pe-A rQ o4 d / "^ ",D'cf{#aHdUP9_C[,WfXG[-7o`K9L dwXe2(t5}gyTM4mdb%1(6-i>z9Bs!@Z5r ?>(_@`/% 0Wx!,O;1-5+o*  ksD+Q4}7G)bSJT+s<&9"\#B0D~JRB j WOlv -rt"&c|s'MX,sn;vLwt&J2GJ:qCBDWcz$wa!?^oAx`${'%YI.$ErVQMq>P.4@XnBg2 9TRIUk F90 Ymwy@vl pV=>Mx>$)1E<\sJM, a3 #C5d\}RURLb6}jm^@agch?i,jt_FF0qD[Z8&O{HQN:SSV\c+=  IJ-|5@O}<g<yi~reJ*3?0~bbKY/1 ?sY;]?x^J1')L(($x6RE _ [?:/.1N9V%(h.7C'L6E[CrTv)c1BlmwzYYm[|+Y<BpC O6(*^eQklED."I^"c k<y<1&r,OsW^Zqga >Yd~C-w: 1cPeR%7ZB>[9pt7h~z.J <FTWRTNAw%zAXW86MjO87eqOQ3!6Zm<$$f#T{b)q,UtG {aF7K=s> :1k3jY"[xE :FbLor=xk 0B^oZjCqq}~['&ms[B@ T\Zh_-V:yE "#<CFD1!n we_=5@fQq1~Bu?qjn(E|BA z%|1U~ zEs}lI_iV]SNbsf$i]~>,R_orjN>yu!J#}$d^1MPrOR)Pbp1_l<RBQ6u>bmj?]w&`V&:V:0-s\MVKHX@$E0DgFOKeL`&} YKn 78QM#: ci{[]R8h)O7 s  7l/E`Oa5mFr&n_dyt |3i,UL]CbX:+nh|eSP?G0)\b>ohjx+8T<z ^n\:W"EEt}n0U2S3T0SSdji+dTfjMm1y^qd]9]7eM-d\%HN=JLnHO+HZjSQ Ol"M@j}> vI{q/^}QLa ].=$hSiU>)T{fx\\wR{C^:M14%`@j5~}J 3e}%'x/iCB c7gfkEi::. =H~vd  c)5w8lD.s( r*9$T(D`G_W+=+n,`'epZqg|aMrvB^^D.z=mq>*QN??  +n'O6 Z$v[JCr3 nUvDTFi64{"4kA&Y7|NX.v0m!W@"VG|]he/Obs&kTm]^;'gyn>=KUFcc.T1&L:RcO  {[ao@3o{V*KL l+@[Cot  T8;N~U^g^nRc+2i${Swuljz$N1l$NAr{zF]OKFl !$N>+rY8ik1KA]CG:W"rO<`&&y58[ Y,+9*9BKfw_|.pILU&XM:M>0 ON,07`JT PF}!s^)7y% d KW]R-Z'm`7zdZ;+(jS }bR c8!m-z>,l)=V}&D^wp5 v$TOJrg,8oGB X3 h;$knh19$%&2FPWYlp_%G6Ro4|qnwua6QNu 0T84ANdUGAP}eU~uP9.<]xf~Uoa2#NW/;{)9S_;_.0r5@PEQl7M9n}G>c"'in/H}7-2Jx0l BBi]mX(8D1ov-^D`.3Ilwg_'Zv.b._5q%U>Q/)4lnN[btB2 >](<"mOPod6($I$ pswDt `BhoHN@D%|.wT)~B,mS$b` QEdKBXT8BsHCr]|a2\Xg 08W>MA3Vh2mGA_o!zyyu}\g#MIajb<\?j%)EqG"BgI)zd.drRc^C"S}kt%^y~>{H S]T~c`Lk~XHR-2NI9ZWg=SaQfq3jU?_&wVA}7'ZDyh#@WQ<z Jy? %BHGBl,EhZY-Ms@ -=-v3?~eI<<$kiR'(5NuO{Lt:w&0%5u]soRl`7oS[R/Ka\)y3G'Tf%>{iFFLp%8IK>e3e\0K:U |?9a%?d `T `eN-u)rj!t(Y>MJj @IgbhO+bbq.}q=: KTgkJhW4kBm&Y~|G!Q,m(Lt(>#6i33cPca##!'2"SZl>K}5vfjsYw~=o|6b?Hyhco;1rs ?4l& @A  SZDI:`cNVqAD Y|M!>;fQdp"4njCYw7bYouW{ t:C!#")[mAl?u$LacX<RI7' $,0M|i(!h8 sL:h 9E3eWDQ@PuN#> FH5tprm(vrY< t#:(}uf=6IA%e=z~a0$EJF(+ *9X]3B;TE8d-&691~1}6#r"@ * 9{CPfY'* &8h-Nfq ^ 55 4mf!mNI6!F} O|7OSagaK+^G+fl Viv`fd/R|kbcck.xsuB%1@0mzz}&E45Ag')3u<z E)A7Z[Jh;h7t0#iPO-O [~U)R-)m+fP0Vnf}|LgV `a7qhzdZFUZdp{yXwu3pymr$75F<>.# h0$$;%O$37Df2Twg |\!ze)YnTc?v(|vY/~ >yc}/Lb 0BfYimn`~G%23g<#d!<]UvuOKxWwu< .2Ta}_G]ZN.W@-C!/CUt(24qels4@%d:tII5~\R'e$S-0o4<8$s)IuL\S.q19w)J_C_.]Jg10.oaC8dEa4( ;1 y_g/Y}(S0 V}g<G@lvh'ExvM;Mco}+[OgR+':X+6AcK9w Mai0V@7do9"W8LtB-doI Ag$ h^CXO'6|6[*]KQm Yf?P>{uh!bsXAK: I4~_%,/&i6liPD>WQu3A{J7bT~EhW"' "k4UtD'4kW=VT5G!P3 /7O@FyBdt?hQj. aO8r*'<p|P[>wGJ/\]P(SSSSDn>wCL{Z dYSB'XS"p--mH"P+*,dJqP",z^cU-2J.{'*qv<j6[JbJ VP]Kb}.] 8}lgyg~T8  Me>=t .9wD)4eJZs'p 0?N,!U\wsS=G_YW.2DQ#J$8yz4`^FEJ }2}NZgcO3y,-|^z':4A1*aKRz1o`tDy'q9>YYyT,+whwFj.Nut-SB'hi0WibI\k] C },` jBA}68 QW1X$(G$%,le|#.Los/x)_3Ckmd.iD!KqU|GHQ]=1ItAR+t( wIr4@lB$Uj|x 4\[y4~g\{:s V&[nB'95DOw[qitemiy!s|z$bR X/t**-p  2 6%o;Cj$Sw.]GISCXCLSAY/%1Ke7/j]%j>  )Kh! "qD)|Q+*Q{~~j`^` (]f@!%Jv O)B|VLYSE90<Js$&-+&3K3_0g.]"A a$4$%   /Pr  bY/8  ;}ly7PFC#tF"_jSGCIRbv5`+D[fo\jZK9m7U9QIe`}$Ggw~ykZIx;P1;&.2 DPk->C:v'X=~c[oilST;1!'WPTNRQ]]ZghtzCl]q^gqROMXk#~?`ulKX)Lj_$_1s1NZ0Jj+I}lkJ)`rP\4NE@JWw*I>MNK[0NA)~XFR49BY{h: -0ui+fQqp +R_pbdR52 1?cxKxjMz[DJb\%%B i&i`-@!X @ 8%G>nd!BWe!b N(dl%o~9f: Z}qTq34 X2:k9./= >/xl79x]]w)mGCt\o8oZKFPbp Siv#o8G-oJS!TUI-DJn-*=bSc4rbRZt#\m8!5KzN( fJ`n`_U,{M }-QU2g)8lV|^qAfBr=&)*lt_2kAG9SP|gUXh>DrN"Pl$/eM^1"X5kS9\V+CTJ& v"rcx~OlYx'm|o\l-c3CN5~52{2k=/Oy$JCjKe*QZw}$3)%% ]ljkMg;GKP `MI%({L)22[cXd}:J\h^H[x]rZU;+BDR&Bk <9"0o)~m~@)jdT_WxCug?/KBjrI=G$db97|TDMV` 9y)^*e5 h4=9Uma(~Xr|` @uoi%^5l!(:;|f9N@q^~3{ =Qf7w O sP sK5w9Q DQzKf&Q$d ABizzdqJ@OJmA!Qw)u @QWM&.zxA'j KJ:2:|U{pr|y{|l]QA<3-#  1GeujS<  8FY^hgyh^ZNEA0(   '=JYUP=, "$#"=VmsuhU:# -@bjujk[OD%/6#7;0, '8BIMTQOJ@;,!!0 7=HFVWdcaVE(    ")/330( 0EY`\X JBD]6p+tr dF):Um~~oaO:( (>Wj|tW4 y >Z kjaJ69HM0BD,GG:*{`VLPbv1;K7KYVlLwIq5d%Dhhk.K]l v$z7}7|.qbE($@d&160! ]0 /W}]: 2NN-O?4OLQB2hWSVg2K^1g8qAg;^5L"7" z`SM\n -BR _`YG(>Sx2NehiSs7V<!yr~6_,@LOOB1^9    .E NW7jbqZ_aigyt,Uu"-"wP2zT<04G_ 3Rp!w#_A ,P<ulxJ`@vaPJXj(WRu`v?S 4p`Vdv,NY]D.-zXJ;?(.@"T.LxRBbujV,qAC`D>M-cX*!`LmLtA~3%sn cm).W({y A+pl03* qZ1CV<mn :NwA@e*K(c#aE)U=)PfQ F1wK-|f 40f+LJcB@> 44au%jWi70qG3VG8)"0,4.7M&fm-8`n ;d'"lGQ?R %-GB?<S[m6:` X/dv1*r #2)~(:6.dQeu] G:G1u20 K GkLqKac*']3 .6b$XySnBH m,@&x'eK$zR0n Uw .lO  %wK "/+ za5yn8 #aFr> .WISYJZ97! 0Rf66 P$Q~V'@VaV;@m!*fiE2+-7hL.dq?  (CrMz1StqB&tNK"w f62\elttX14g!6HHIP5$"uncTJL^t 'k/O9=*7#@W|o*YEQkRrY{jlP*}YD0<=[x#%H6dBvPz[vlgtSz<s$eO27[t{0[K<au|gQ5" ,Kk'!}^< $,+$xa\Xev" * 6 >:: +$3<%H,O4I>DC1J&OTXUWIC.!!. DS s!~  thI7  ~}   1>OQ_\ ` W PE;4.)# #&8CO ceqgaRB.$ "1BEH:*   #   '<U]mgj^UH7/!   ,8@MQTV NI9+ +).#-2;>79'#09BDB:,  ,ERcdgb[YON!E!=1$  %3:AAA,9216!+",8IHM=9# &9#K^!dj cV=( $-,*-..*&$$)4>FPS\^g&j8hCgMQGD?*' +6$?:A>B?80." '2.+%- G/R=dCiFg:a5Q< '4@+M2I6N0G*IBC?@GO\er}3NmpyW{<u` I& :Vhus cU5" ~{ mwvvs} +7.EIKYOcM^KYAE7,( ! -9A.PCXM^WiXdSrIl7x&y?|XizN2##wV8 #Bbsz ti U=%uopjoimmux^RR\s=.ZFqX}e{tosUx1naK- vmajxA"bKjeN*zbE=+g"9LZZWI}2X%xsxx Bd,ILF1h<wL=d)O&E(K/X9tL`xmV?:CSv< ^?{lwmUk2GrYRWf3\ CwinFI%- 0EUluvf;b@}`WOYrFHfnohP'qGb5_N8(7ECcZ|}}m]_FkEdx@g>]m|gqAU)l`Ze'|a/^|\y7Q62Wo-ZtW4g:wjcfxwz&DY%cR]vO+ xkR<1 !m$?:/Rv+;aroo~tWH@D#TKkc1qRWr7{Z>ua-WWey.dw]>*(mIOh-|Bpd]@pJ,-@DF? -8^zcYR8LJTczgUMP^s|o&qAtO]_[O=%% 9S_aYG*)@}Y|i}'tIklP?)#9RxbcnRm;f0U"@;&Maq|vjWA+)311"F^z~r]? {vu-2;63%'7BG=1 ".9DKQRSHC0&5Lclyy$w,p3d4W3J*:$03 9AGRNRJC;-$ ,ASbaaQA-  ! ' 2HQbeki_WFA 0*&('+..0/+( !   &69EHE?1#))) $%-(#   1<HOSLK=6-%+4=EGIE&? 6'&!# #")(/* 0(+  %979-**96<5/"    &,87850(#4> TV`[V!H$8'#($%"&&!$   )-,%+5AECC73$#04691.! (8MQ X TKC3/6A@<2(  '%"      )5=AA@ <: 54 '%  %*47:73,!&+968/+3;FGD8$*5#;:6. !  '-7@EI#P*M9P;G@<=/7)&6ALQMM;3 &(#!/= OPUI?( !& 0313/<,1( ,8DLX[d0\DXUJ[8](T I5  9H \cb^L?+) "' <JTWQN6) 5E"L&O#D:  -9FQW\`]X/MI?],owqpUC "@Wjpvl+i4O7G:'1- "3JS^[TC+6C \([2\2L4;/",%+AGWWb\_OM9 0&;O ^)e9g9]<P45+ $='K5_DcMfLeIX=M-<0! 3BPTTNC,#!2;EHLSNYHT;K+:#*AN]ab\WK=2#!83K<XB^AZ8R0B/  .$;;UMS][eLgBb,XK<, #2>FJHC4%6C,S>WHWNJK?F"6)!1:HIKHA7+  %54"532B%HP FC)6>C#F66G3Q^^ZP@2  ).15*(  ".8BGF'?86<"A=4- '$,-,-(#  " (.441($!+5HJOLB8*    #''*%*!'2 767-% #!  !$  +4BDGB61       $& . & &    #        ",399:/,     " $    $%#"  #'.,.)  +003&$!   &)0+0      ".4:961" "*9@>>2&     & + 3 2 /,       !14:94/#  4:GAD40  #$ %$ !!       ,/33.*!   26C= D68(#   !/ 130+"!# $   "&'*)&+!  $'47< >>$=+40,/ -$   &0799/)  #&,$1!4#/*  &&.,3,, %& !*14> :B-9:8D+G D94  &098;0) #/!; ;#9$4&"(-/5580."""#&,.2605-$.6$<ED D;.!   !)-.)! '&*+1/34,6(9?;A46#  %*',  )+2/1*)$ .7@DB:5 ! #!!  )-'2,:,8-@#;">43 ! "#$"  "%).)."(  '-5;57 '&   %"1084!3!3 )*  """"   +) 1 ,)$  !        ""'-**)              #"                      ! # !       !          & # * $       !%  !        # !      ! %)) ($            ($,) %"             &&+&' "")$&       !"       %#($$!')+$#        !      !&&(&              "$                                                                                                                                                                                                                                                  LISTVINFOINAMsound7IART JfreegmanICRD2014ISFTLMMS (libsndfile-1.0.25)id3 ID3@  GSyeTALBToxic sound collectionTXXX"SoftwareLMMS (libsndfile-1.0.25)TPE1 JfreegmanTDRC2014TIT2sound7toxic-0.11.3/sounds/ToxicRecvMessage.wav000066400000000000000000002035001416141666600202020ustar00rootroot00000000000000RIFF8WAVEfmt Ddatazvx}KF$"         $ " VQ^[64 """""""""""""""""""""""""""""""""""""""""""" glA>DGn j IL  l j   ` b   : ;    f m ,/AApn%' d_X]ޞgiBCEC@BGE@AEEBADEAAGE>AHE@ADEDABDCCDCACECCCBCEC?CGC݆݉MK 22\]PO B@57y{ JGfehfNLtsae**caHL!yvjnsq||QR20.-11|}  F D a d  / 2 B B / - "hf"qu;:op 31ooY\``<<LK^bc^^ a O M > >  "  |KJopAA&'0-=BQKfl,*ab31LJ?;KJqm\^IKrsqt|~0/[Y(+cg IFruCA34LK^_feQQb`ZWDJmi-/KK]] {z55fd>?  00qs&'((ZZHIvzj m ` ^ ( * MP CIjg0-V\ -,RRttqr'%ghBA31 {}$!CC_brsru\b+ .   W X { { h h O O 6 6   t u @ @   bd"!$'W[mnhgJIyv*(C?DBACVT "w{GB]a{yJH-1#%):8^]ll('Z_&)RWb`88UU{;;DHPMD@[\yw96OR  bbsr,,*)yz M M . . ` _ 5 4 : 6   w ~ ,/31_a}"bf x q b i a \ n p = : ] ] J J   P T >AXThlxv{w\`WU]]ww\\geim$!66YZ79 ge%#45RO## `]bcwxppNMlm}GJqsON)* ml=< RN`e  jiSPRO}~%"pqP P 4 3 # % , ( OM[^OS+-9=  @?pp$u| B?qrIL``mpGGedwx $ % 2 1 C E   vtKNlpxxje94DC^Zvy PT::ii _]FGrs\ZJNB>7:>#  A X   tLc&NZMIjmxt{}2F_"$NOr}ycO8$FCp$0 |OZ   ^ c   T O #Rc'5Ldl==d_XG wmHD!'{v!P?a*xi@?& 3'WSMJ-4vv^[fX$[L0$tlpnhdLA um(we Q=!dP]Lkf[\gl>6{2 q^ !  ^ j * Q q | v i I { $ a G *  } _ H +  l Y G 2   b2nV+qFo]0Q(fi|(`Jn.O h,>kHH/c~7[ 1&zm19u!@Lc=3xUV^~]bh%\OP.4t&ZvYe6K.!2,Sh'po`gBW*@WUBqd:|zvi]TRPYSOSd2xOu"dI}4|&]P2 p;g 2%Z'm*.%!     t wie,e9[J\WXj`zVOJTaompqtedK36" z_ZHB53-5::@.LUhH h&85hB] )#C7m9=L` tH()Q\+CMq P'vA*+9ANZwswQO$ZX<9kVSW kL[&\q#GuXGkJ K1e7g ,PO q'| f<,Us] R'S|h'?t(e94{8kX  M T L D F / < H `  5 D  / z Cr6"5=HD@^!  yn A6   l U  o  N U  ip  r  i  a  Qf  ) ]LREyVQ<9S }] 7SN# {d#f3h.(:j1Ny3H [dx3#lKp0q"|'X%[e47!PUovu~Ok(4z6J<i'INQd=Q tBx[&r% e\    A  h   ( L x x  `/ { L ! u l  ;,j3y/J!nErVq]HG#j:v _D,c   u - R  1 k Z  d 2 ] 3 u C d z r   C3(g2Vx7nK!=?s#9/L{fzPF=' ^_"Y1RGpax BQ3%#:iH  o3F=C3%*=<E12,0 *3/@_/k5Y Ex208)% n|UOA/ tAuH!sD Ej. i 3HGsJ+C<:A(-(X-vlD<ny4E"}?zqwj\`JfQrd+R%z=Zp~s_[[L6">Uv\'5TxRm1;d2uuihzmk={w~/yi_<M}?)DcyN4LWYL~JDD@HNU ]8kSzCk/~_5lR*>3Rpdfvr4ew`J/6]xf'iahd^e8i]_QmmybL:)Ur#ezW*!Fj73_H,n6m'~u2vp>|W~dXEB={,o2y7o>xFpZd|WB>885+Us1Lw6Wt=~-&`?4u+uf#s M_)]$ZjsCWm w 0  < }%w  D! %L$('*1)L*W)w*)*)*)*)+*&+ *h)O(f;\Mfn / 3JhEpl E%/~7z%g_TG- 75#C<C%~u5S@    $T1kI q   7   35 =  Z ' L I.  T[F>ztDU"  = cR N*vX?3@A3v'?Y we*oK4 hg ,QU `v~K s -  }   3v 5 9  P   c>    U `5 ' 8 r I   J "^ ] C   {   . Y Jl  4F `fxvbw \c kh`')9rF.0 smzm[\k[ 0`m.PT.pE CQ hc JU  t9L>5mRqyaB&^w)~%uv\w!@>9l{<]Vu9nYEp+@  9 *a  9b"Z4 A ylIK) o     = .a 4 D   +i;l1I=pR0~9= %]^/syu+zZ \ e %Sug(- + ;33GJKu*u?+Y  Q   qhSP=7 dq37 J9k448:8VNji{[pbO3bS]UM{Y}a ~e?qAMU"lhV13/CevIV?xLQ/Lr. q@% ?=._@?PT!mi]^S\5/-Y''t*,| jwwl$ [z2mB zK^0Hs?,R_vw5/dh + r  y  u l { @ A  9 : lY :AC [C [L  zA; ]Q M^R C &   % {   n_  'r ;  N  #at 4;,{cJ   3J J H P*  R;+g(4>_{gHXaY#gyewesT7HM05FzJ5oh]U `-oih'tK%M%R=|2vtKn\ g_,@lfK-w~R4mZuf#J<J^Hl ,  EB%QE;0 ? $  S <G  q. u e  * 1%`P2l*8lI VS 5 C Jg %   t4Y#,Ce%Wz#=7^;?2u:6P]PM.C|k!}*t$^8OWJ>'xP_0Q3s2Sa=:.} S:Cz. 9*#)f3.4p&i`B/  - J & <   u V    + 5r  ` ) \ e E  I \ q i A /  /% `z  O  v y J R! pR-}q<&VqC1-,FhA_Q>yR* db }  :a  E6>GVi# +`3 a 0^"*3`2mH, ">fNsK-RZ7p:v8~|4ab_2sP(I]x?9 e : k    l p H%rX0g5r/s/bf4(P>ngpyE\^'|h+6t4   Or  | F z a  - B v ] S 7 { R  ( w  s D - l xl2J$gg/ ,z*[:_(a?)~n]mYN=76 "FFN(NidL@o:wL:QD$fl?*:|H"31u%@)) G &%q%WB''Yq _P@9%02o :S'~RBoP;zj2V\ ;,RQ1 )JY}%v%U0",\t8Bc4Zt$^HI dpAh 7|*Y, {CB6n`M:y#<U:e=4by0  9 ' ( 1y   ( ^ nz  +u lO &    n )@  K D6%A01`hdF/d@0={yJ.C?&S.PC\Fp]jIsNYV4ur=g#QF<WTXQ,cv 0P&z'fD(=8>^Y"\b~1Vrr^jXg7EKb X  8 _m    8vL0JVlnbh`NpNAw'E y K  ) | ?a  x   ^ %sp-9Wgy$8y}!}wy5C2oy:J {7XtV]F=.Aeim9+(TiO&CqAESaFN@,eZP@F#V\IF,."s!(7Qopz1De'e <^eCZX4D!z@lnR(4<b?`+L N c 4   d r U' Bp9w[Pu0Z(`=wZqqSfWBNhJNe<eDg QU   % t - ' !w  # > & U < 1 s b 6  K n  i k  ` * 71RV(3hTy@;Qg2^R, s_4Ne647jJ3$b _H}? ^FTfp&1Y|`*P^: #@> )U\u`ci,Z-SF&.: A1 t!XE!s!$p6]aK*!~yHm!go 4  & - c    s  " T2 / ~  , C w N ^ h 3 }   j ) L  @C wD7e S#/%{H"X>Mz  S:/lLUuN:7'h?Yhsud;,`N"a\lf K,zV\zwJn$Df*.kD ><$K?N}J=,?=ln,?Zi+!uj# ( <    A   @ n   K %  B ML U`^IV?F ~C4Jt)!p>GklKR=DJHLDlFjW^wdm!Y5}(cCcl\P  A ,s (  3   # > R i ~   [ S} 5K    ] P  `,4- S0+FSom*HL !e.?8/%o-w^S;5,y3;"H)-i\/b)jsfBld-CE9MN_KB{2n'n"lFoL.XoV `Q pP)2`!'%|Woz2`gW#n=~E#OQ N  3:   P; a y u e /   V [  / }Os Gi$1z$^sq?)#/=<<?DMsDcLbv_?  Y - C #6   * ? 6 1 ^  ~   X 8'3K8.Sn*b0|5Mq0!thC w!*  ,     K Y    1K #hB2tNE4;Z' U8duy,fI<6k|y|Y ty8? >$zd 2 1 N nb~$ h%h5ZN,OY6mf_cC O 7  W*>z=r6D{\xr-cOaxfRdStCYjo2Z,@uB D  \  t R99Hjxo$b:  <  T  { & -#OvyUrtFT$F<>Usj; Fx c<,S^V ^1  o   ! 6 / , Q " ] sp;BEBc  UM   "  X  a WX. _7aM1W YG-8f|K1Szd WxV!e/,1f-fsL X}  X  K b  vO u`       b }  u &;  q @  8 A 2 n}\TbDkIl?[.1\cTEo|+iQ1IEwqv8"Z5aGW1@%X-C^1WkjCjHxwU cG_5/  VYj,-9&D hJT X6PrA@K)<-f89p1 [k/~L-lzx=+6z"k*|xbs0{UDXI|@z PDirt_L4V H t $ i g  D  { Cv(i - 5  3B   Q {U03  R   g b  ,/*UKQG mB+WvY=E/y,pv8yVs!/ IdmeTdDhW4 +UH9YAQ    : O Q {Y Y i ~  - c, S [ o,   [ >LP$ 9_Zu39[;,KwddN18SaDO;q[3!'WBb l l6XD: /u!Cnk_%j-  F$  aj  o  \ Y )  #Y  } Ep -o > x g 2   } 1 w 3   ='vLky6%'\IPqC/WM oDGzQH)1IP$F_EbW{mp]UNROJKw 5e5W(vv)msG8mfVGY<&O`Jrb>8s6b9zlN)w/Lu!5W0 iNz,gT- -pC-H4f+xK3;^]}`"]TMVJ(r:(&4ljXWs04HJ{X~=    | g xR ` u  r Y `< fS TH 8?     v ; @ ]W L}ZOsgF5c`8$Sh\h6hSdD"Of+U#pp&:;K`wt>e*n7fC0y9Q[/j.tTLr?"|g8 CU~!1) b3sBT!D,n+4]X lU$5@'f'ucVWE.L2d{  G 2  HO  $   & o   B }    @Y V m |V v p{ J $GJ n$ {4d5?v*l p3iH;hhOf-P/*FWjKW'ohQ~YKGTd|9[ 2YV$5w9_ v Gh9q6Xz?V,Kk<r^40 juZ2aG! ~~NdO@0 "'!gTk Q3"Q,1H_bFpzy2pmqVQI6:&  /[ -NHS{6k-Kx1o0IIWO`k(?@b/F ~x IJV[]kMuA+!$z"|/Rl   `:l%}%q#n"G{>Ck19UFd{J=83$Qnk4u\jLAn'[&D'R -Ib- ;i?](XQuDFWvjW(N x"b)&p( !%"*&rR$FJcWUUeREbU^jB^k^?DTB 53&sR /JB6 *`x2S>1Z)v)G!pJwUk8)  [ i u  L   7 c   _ 1   < jn G|7N\uHp/Lu/>tUfY0JT^HGt'n eY%bhi/>k'(T;A{( $//-7?fMQIg/f JRl2+4H1],+ IzW? z"~9dg;@ l M;TT[qu 3c},|T2*RIIrI;03~0c9IIAMGzVOT[:]U]u6z\U+O4zJ$}J%|y%^mb QUH41.>Kz8V$o>|V&QX]T)B^JvIn!^T6$(^,bn~/ZN_2 lDs$Yq{bd.Fo]sUaEJ&:8(hh  Fv] BrF$aIS(Fd4E(K^e&gWokd[YN80}7LJo{g28ES! \D5{/Mu:"5'u!8t[$u  s/FS;>;!{7532_6C5e?Y^m8zt{M2Vp8wPZb >%^T ENv7w"We|08IUp-XJG=-'-3F=Lko=a 3_B|ZYswU=S'+# 3#Ty #Jz=q7~>]ov=Z`E$%V8\58 4:&&Rsv`]2cwxY}lYg>:6_:76jy'O#q)RuH!J :!#:$aS$P N3NyY5%&z'Gj5h#nOFr VCm'l8k"g7i./0 vwE[#!.+~'c]hosgPd#3_7 rNeKw3<;Nh}|]_ <_\Bs yr "*9gz6IlUF"~S]5aD w  f  S   0 [X ( 8^Y*s2=nL&oRV& ZwaS4W^0Jx3.pTi6^#b mJ$BGBAGOZn}}qq#N\s] tJ}]0o(Q]0a6r5Edoz3 sK&Qms7m)k;% _*_ a/8ZPd4JDH$rJd?5!,;P[YI= Y <Z'F `:pX>.X7x:[By OV^#dX%/Djpsh`y2 hG~E s!2Kg6e(gF5:$rT~c.tu3g ldSKOEDZ9 (k#of`BU+N @F73c)*6JqpdN:) N,Mo$vi=m}/l=G,"[9Sj )9%UVe}~tNQ .N[5;Y'POf8[JJ%NX)Y({cG"{Cw N6 Pm1]<#[M{CQ,8$0H=Tel07d %)20)/21k,i??FqZ:D ,%W*z/tZ7P-~Pm ;jBr!g4aM K|+*K7pHQXSI+ziij}!! {l W<s4i8}<JU>vBQ{Rq2IqQ _p>,pB4b&'Ksp: q>sIy*7Si0BSl0@HSZ_\i dqh,w)x23:DFLIG0:Q.dxx $0\C2X jg?(~c@ /R Cr)nHe|:8xx ,;`ZAWkg"c-O:8FL]]dgliJa+T E ]9h.qiH) Qk>x-JvGO?Q`0}W7 4QjywiyYfOXAMCHFFUDjCEQUat ?XB S Qh 8 iX4i A_z 0Damtt+c:\O8Q(J<ajCK& <bB% &()2=DEK}IT}`ylyy{zbT4!/a,F5Ssn.R+;Vi5Tn.$NZi~:Me q}9SnyxnnlmfZlKD* bS^c 2Z`>9u$:9`!_3wnfy]EZZVXTUqXtWkktwtqqa[N(@M)j&1?ROV]UN3AV7i+#(-H aZQGv3:Ncao} &$p \H1f7d:kwIp/fel@Bw?v1JZ1iXwzN ;KY}rEoz'u+v#eVC){> Y(U\2^ 3zH!wvmhOI7h_Ka1:24GK\Z_[Q)QIKKW~\ctZJI@>E@)B=3Z0o #4sJ hbZK+^+\<1R?g@d-^ yA[x/e'1RPzs +&PBg[|itk\WIA*{S.rcNA,b!9y h[+X?NSSmT]jy Ab'0G5ZYfnpqqvuKoGk!Ff$'+ +#  {rW80Mu[:&d@!B`4k $f0xgOA&]7 mVC+*<HTcp,Ju .Ml!)/BQn#Kk0>OW eq!x(56BCRQc;hXpvpi[I4 q[B.#"*<Of}$))+(+),~2u9iGdSahav`ignt2I]t!#*3< K\=jYy}/Pdul[S=1 ud~L_5H,kDlW3RxrGU 6e?{dwAMLTPU~VxXt_o^mici`tOsG|6z-{| |~uqslhea`TbF^.lp~}njebf3hNri} Hl 0 F&OJ\a[zdepu,:JWY4dDdTh[ealco_u`}VTOA<(z n ZI0bE%jGrWh7C'jrKF " Z:vZ<#r&Y/@?+BS Woz %A"b)z4:BFOQ_5fOoq~2Nhw 1@IZ_r{ zh+W9DE1Q"b ls\=&"0?P_l |"4:NXes9xLas &0DVh (>Xgy{~xn`N6 ukZN:#r[@- gG`Ale[~NUI0; 74-/+j-G.,' ,%* *`I)(&'0/=JYt|uaVJ@:(7N5Y9|;INaiy&7Ph$;J[isy(.?HS`gryyf[SDD:;938/,4?0Q8`5t?CHTZip~'A_v 5?'L(V2[-h/n/y+.)++''  ~|qpjX\;O!@,|fN7!c G# uP7o_N9%hM3sdNA- ->O`r!3Oj8Mg{ '-8?HPYbmv );Rc|,6AFKQS^apwxuspvu~)8BOT]clt~ "$! |jM:kQ{5l_N@+xm^o[\JMF:8,0( "((d3J9(GN^j|{e^KG84*!" 2Od%+7)>2N?ZFnSY_hftu/3<;94+$ '2>GUdw/>S`s| *19DKV_gw~{ti`SG6'xmkg]ROFB50'" k\NjBY8C3/-*'+(35@HNyUo\a`XeJhCj* t|bj]_SKMBM0B%JB HGJNPUWY__}fphfq]yNI80#  ".;HTeq{zsqi*o<aIlXbbiqhyknpqqnpiojst   &3BMbl (7FVi{$2BO]hq{~}wvojleh`WYOTFP<L3G,I AC :7,& p_N@- {se_QF{>m-`.TJB;40-***+22<@JPv[m_dkZnSxJ@8.!%9O`z -AL]fs{)5?P[kx/:KTfn{ " %"%"' *&.149;<==:|:r9h4^8T1G3?/2+((% yiRB. ri[QH{;i6^)QID=>8<;==DBJJPTY_zcsidnXqLx?~2+ (9E]m0@Yd}  !% '&,2+5.A0G-O1X*`+j'p$!  (4=FRZfoz  !*(//5382|5{1r/o/g'b,YV'KI ;7.&!ngUL</"qp_^ML=8-v!g[ RGH;B7>8<<=C@JEOyPuUl]gaakYuXMMC>92.$")9 O[t%9F[es~  #+)4':3?9A@?IAQ9[7b/p(x$ '2=LTfox  #"&-,3379;:=79~6s0o3`+[+M'G#<2) zqZS<4%|rla[SID=62)&xqhg^_ZZ[X[[^bbomon`[QIB55%( -4NU m w'6KUfpz #&+36AHR\goy,2AJV^jmz~}|{y|y|y y z u vnohb_STCA3,#zgbME5%vt fdWQF=3+{vuqtsvw|~~}qmh`_VQLFB;7/*)!7AS Ymv%5BR`n|!'05=@JKUW`ems~ *0=DNX`itzzxrji^]SRID:6*&yoyh}[nUoFb;]1SKD:6+, !yzlh^YRLJB!B-<=7K7Y0i/v,'&" '9 G R ` js!#&$..8ACSTdgv|"&64EAQPX\binv~xrmc`VNI>80$ ~~nqdnTbH_06+0+,-+-/"*.1C(I.\)c(p+y')-(1057>=GJPT]`hlrv~ )/:ALQ]`jmxw{ztuopmlljidh\aXVQLFA95*' {twmvmcg\_G_@S/R$HD <6.+  ~ysqig-c:[D_XR^YsM{QKIIECD=?:7 43",)0:(A-R)[-f,u/|23:;DDNOUX\`ahfrp{}  /1>BLNZZeeopy}}}z|{x~v~wzxxwuuosimdc]YRPGC<6.*yzopcyenXeX\NOLLC<@88*5",+ !  {w's/l@mHcXeb^r]yZTQNIFE@D=C>@!A2?9CHBPFZGcKlJrO{N}SRUWY_^fgkqsy !')~42u?w=pHoIlTmVi^jdjgjnkrlrm{oxn~pnoplommpkqjqkqlnnmlihc`[TRID=9/. ~xzpqjge`[wYnQgPWDTDF7>55,+$%! "-9|@}NuWqamnhue``X^VYZV[XXZ WY$W3Y9VJXMX^W`[nYq]z__gfmmus~z~ yzss q%l*o1f5j;eAdCgM`LdW`Wa^_bbf^k`p`t_x`{``_b^``_^aZb[]^Z]YWZPWLOKHEAA9:0/*} xu{qwoqkkcdZZQPHFw>q<c5_3Q/L)A':"2*$!)04@BRTc{j{uvvprlkjehac`^a[ _\],^0^?aCaOeXf]hknlkut{n}yr~y|{tulnef`^%\!W0Y-R7W9P?RBPHPLOOOVMWO^LaPfLePnLjRpKrUqJxUtL}UyPSTQURUSTSSRQPNMIGE??861*})||wys srjnegb_\YSTJNBF==7t5u/h.e%^&UQKBC 4 ;-0*&&!" !$#',-47=@8E;G7H:N7J:S9N;V=U:XA];\@d?c?f@iAj=lBm@m?l?o?k>n@k@k=mBj<mBj=n?m;m<r7l8s1n3p+o-l$l&jiffcc b``]^Z\VWSPNGH@C7=.7*,}&|#pseh]^WRSJLHHEEFDIEKJPQT\[hert| %)38?HISUZ`afjipnstrzsyvuxrtppkkfeb^Z[RTMLJGB C=>:86#4!3&.)1)*,//)0*4)5&7)9%<'<&@%A'B%F&E'H&J(H'L(L'K*Q'J*S(M*R)Q+Q(S-R(V.R(X.T(V-X(T*X*S&V*S$S(T!R$Q SNTMQ NNMLIKEFB@A;>5=1811.,') #zx srnmikgkelgmlootuy{  "+/3=<HFPPVX[]`acechdffcdb`_^Y[VVSRNMKGFC>>;57- 1 *('&!! $$%('+*,/-2144597;9=:?9?:>:? 9B: B= A@BACBBBB@A> @ =?;>:<8:6928/5./,*''"#!  #&./87??EFJLLROSTTWVWXXXXUZRVQPOJJFEB??8<461.,*&%           " # % & ! % #%%& &) ( )+ ,* /(0) . + * . ),,).+--,0-/-.,/(/&-&*'&'%$%$   #'--55:==CBFFGIHJIKIKGKFIEDD?A;<77220*-$&#                # $ !"#!"%# "    #&%.*2/72:7:<:<<>;=;<;99865421/-++'&%!                   # *",&-).+/,./-/.,0)/)+)'&%#"                             # #!"$!%% ""!                                                                                                                                                                LISTVINFOIART JfreegmanICRD2014INAMsound2ISFTLMMS (libsndfile-1.0.25)id3 ID3 TALBToxic sound collectionTXXX"SoftwareLMMS (libsndfile-1.0.25)TPE1 JfreegmanTDRC2014TRCK3TIT2 Toxic-Sound2toxic-0.11.3/sounds/ToxicTransferComplete.wav000066400000000000000000002250101416141666600212530ustar00rootroot00000000000000RIFF*WAVEfmt Ddata$                                          % *'      !#                           !&%()''$"     -*:9BFFJGEB95+$     &.1587<7941,(!      %#0*32555626.0'%  $!'(&*$$! #$-.567:763-&  $%,-.0-/+*"#'+2;<CGGHJ=B2-$        !%!%#  $%**-,))"% //86>9=8901$"  #,098?>< ?692-**'"-*+ $' &04?=FB E%A%@'>)9)7)2(-$(#!z{x}{ !& )*%)3';$J Q\g j utxvrod]Q@8 }tlc^XVPU QXaf x *$+'&%#*:L\iyt^P2&|pgaXWTSW\bnw (.5?CIKMHJCA:4-%    ",3:DDIJHFA;/+||v #$0+;2M3O;_8`<j7f6n.e+g"_WQH;3$upb_XYV``ks~04'K.P;^<fFhBrFkDqCg@gB^>WBM=EB5@/C ?< 3+ zmc\WVX^er{ $ 3=NT^ebi\_QNB : 2 ( $     %),143;6<:<::9:67 02)('-:@KOZ[bddaaSQ=6$~ zsvrwzxutuy})-<CMV\a)f0e9eA^D\IOKII;K1H!GB?81. {tgb]\_elz#2BT_o y%*,1-/q)f&ZK< /~th_VNLHKPU b j y   /8BQXfntzwund\LA2$   '-17<@FJNKNG@;)#(3:EKU\bhfj^]GA'xnkagfo+y58B=D9=18-60<:A&?4=A9L/V+\bce``WOB7% togeec}ilhhq]o\xVxWW^_lp}*AYl~2K`u}pUA${dP1wnhc`YUW=IJ>B>HObq%J+c=Ufy'T{%w9e:OCD=+:+ uO|qe\cU:MLGNS_j{&A8l?JHJ-Ca;2& 1R_x~  u dF+zPhG" kbYX_k} @x<c{zx~,6;A94  4iLEe {q[E5"uQ= l_xbN; oJ*mjpUSV4H N JSWdly'.BPev4GZ pw#+:CQXdfmpl~mmi_bEa2XSQHHDACl;Z:N,?8 2138BFTy[jj_xRPLNSR^+WEdXZmc}`kw6|SepK4{qaZ=6^;(AZn -S|'5<?7"('/)+#lH"\<5Z|vu$xV,>NW`&\5^CTKLPCU3U TQK?:\&  |P+Hn $74G\OyYZabjpv}yncK8lM*vTE.! yn]Q<+2Gb8?jh,+@5PJZUdi`y[H- )=iQ;V[RJ:q$F'vP~8kURSc]~$5]m8Do,=Zl:Yv6m9}qX30cfB3IYehj6i[I#~mP0KCR2_ut*,\wYBOm;?1FKq Vh,:tR^B#,5Bs<f(TJMF`P V^Kt}eV$ 1<GgP?NSB?%'H(A1i"Iv" -*0K"q m#Pz\=( qR2d<5Oq #I^}/m0t2m~8qBVN?F':S?E"xT8 o _VPTU5]Wbloy}H$`(X|+BWizh4Y$tg}TSI"4)zd[N[auP:I{ :l-P l&<BHC>0";\8PkpQ|6xiC)yl|Z^OQ<8.(wT6(Kjsm5jQqZgkhigkt'GeR&YY~4U+Iu1dOA@#C%J0U7]IkRsjqO|mS4=% #`mT5wN *LuIZ0FR\oiaxTUTZ4^}adlx#%PX%$ko)I_}|H~K~Kz#!~X3 SsI@4pojt:L_jEm|h`G5 7]p2c^Ky0 kg*hZPXfzG5p.~JW(g#Q0*R y%7Q.RmYD )dy4Bd"v0Y$5LgkNkE]4180CM[hfHj- VqheV}~ws0y<`N<f99JX\yFs!Z&G']rN$AXpbOLHOPZV[5PeK:0#l"()C\]-%Yh N8}W{[+'bEQ3n5 lq1BmESs@7>!THylZ 1R.{X|@ IwaSF>=>LBMU[ZZVSMLIQ[qf%EyoL;*&0E:fM\hneVW4Fo>W1w(3BchpD1i-:5Mfx?~k-P'jbx`O8,TeVD?AAIZi x]il 9tK+pi^*BTM\fT9#@O{*F\! F>&w # s8}b9r?MrEX u!"&bdy1:u00x &@`&&x #TIzLtvWQ"aNNME8 FymCum\H90{'n#k{APTrwURk V~hIavDJ;E_o)/}mx;Ks>a4ePAn&D>>z,CmS-9&xEqD!(I%Nd %.hKCZy8!^4LfQ }v< }L }-kNB\$i8W:k(1.H3{By@"LGc8>e{WBq^Zx`)p~E T3!F*%hgC\}`4=}]N pC z+`.#t;^E*p?O bN)Fs[6s ;Qszt.xc: )C9"(vCU[?I/(y/ZMWL`)0N 3e}&UUA& Te^3V5e~ fFhhU"U~pb8+^&;Sr0Lz`T{RL#|X1 cZM]O8RRp`g]G; r# ^ ?OFA Nnp hF/'@fZUNV"UVt9qN a@hq *f9j&/#]v~Ia( |0 WsZ}yU?7[ %/^/cB$J[dWK,> r[ v95QaolHIDH[qp !>lnJ!&> N=Q{ A !LP#l<~oA0&Fa<7?NsR_jX7q  +q Mf"2XEn4XHcq>u(1Sv$$`k_w1=%E1Fm$*Xy;3,"*=bPX+S4=)>y yL$8yksgaG|.{6k0*SNz4 ?\S)bK2W@j'H~g0&Z)4?Q[V$)@P]``J$*D PFC  & v 0[    ^^)~U>_10=Ol~l&Nk*r\*(kTnJ(}:3b;HS@&Qu@  5 ; zY*II!19`\ gR1''Ggn[MPsB<tQQ,v06D`47|+$YBs! [}t E, j;=MZzk.y(zJlNzZqUR~8j 4esfUE52bw*k<P*y1HAC/I_67T6z>Ag "XqkP3]JJs8Fg?i~B<z> eX~%7ZkZ4 Np`MAYJe|Tmn~{'v=yY?q*vg0#{h=Vi~QwX;=OfCX%!Bv! ;3:~[[ `&o:b!2'*!A&l`Bi4i- != d)0U},A%^lyL=bvt|c&sV C#YWpwmIA^q)lNV?Z6`?.J~<FZ;Q `!>]! `/(6I:K#  J  z W BPq. + 0P 2 c9"l+">k{_w3 ~O_x4882Q YycCJ-1R  j 3 b r2a? 3 5 ;  *Ij]}`pP1PQ[mB0D!!? E | $   8 v} 'zLUR:YSS@W,]#WD%9 Z9A2C,-~@51yx[^K>!/ *! +Cb6g@dH s>scGb<- 7M'27[yNltpj;]=c^^/b;Q`Ek_v^K.zK"ET&47vx&_aDuC R"{ZbF$<wBuXV5.6   JkA\Mw{ \Z%cR|ZO9;cEgo,UpGw(e}l2'2DZyck_yP/  7D31) g } ,r QC  uw=By qY W O:N3_~M( @M-l>\w^ 4 Fe8MTZ#`fL'0eRfA 6 v[   -    : g  S f 4 y ""+RW1l c5>%G4Ij$hKGse@AD )jqs   3 6  y  <6 s ;s :^0RyX} /2N ^"#eapi2$>l: U;2FYk5 ^g*5C sCH#%xZTw$Iu|2@hvr0DH40;|18GqfM3" #R/;n!CW]I`F7O--M 3W.*!i`e+ ThPD_|y-finO=.~[q;/    l  6O  wV 0  e=lf"f*ACxV0 )Mn7~<YZg\tF-v)<30Bx'9L82A!Oyluc!dfsp uC`ImiO . K z z M E .' _    ,t MB -sDFfZ\sJnDwDwQ",gl) #Jyg,- X m S e A Qc J    Z bu j z7}^nzQ?uf[bZO0F-X4Xy}@ +>XRf s   8 e   + > g 7 ( C hc,tr&@O]d[i[| c$Wf8{ w(W8Va Lc8;ip Y0zr[ g : &OIRP/0O q q    s  = 0 | & Q CJ  ~pt5JT**%8s\@]? s:paxg qO/  )BnrK i\ NEyBKeBJWLf)ff'v[5#RCN ^|T!UA;51+6dq44zruV w J D j R /I!!Q;jFV\ ef /(3dbRYWp/Pgo|orfUS?3Ju1>6hWL L q `. ,  9eT}KxD"sd  W u  !cs-7QC+JgA-(j0Pu/btgxj]E8@CRK6H^3 d7 e F )8 ~ v  r i ~  " W  ` %   Fxdw}URC OH m io X . pJ(hKQ>r }/HRMY2I~eg${NYqZ=2 2pQ2jvN{!oa`~lGddK ]s3jcUUnm M>,G^+?jEuWB`*udk@n)`|7u'Z=q |1r?,#*8@SAQ"k:bp\L0Dc, fp# @)U@0(ne*H}oZ\$*<NsZ?5d<teaC?mUx6Xj_/N QS*wvHOrnVf4ae,L5 '_ER%5^$4`+x%|zOo>)<F^VP6h  g W D m 0 BkK-(Zsp?bzQ[xvH0%N =Fycwe} { o + ) @ :6  qa  b B ( b ukU|3B  :B\z# P f   CrI/XM u"z. S<*l*nU M  |( { X*  S !T5t2y1D $ 7 Y I ! =W R 6 e n  *3]2F0lI_&"24%M?yexV  p  N , ?#56)( U 7 X ) "V -:e(/]tE`wwkkk{*  d $ x L< 5 !_ > +Vo?8cg# U<@SS=h% 8rw#dz *=CB-hT6s: #T1, >k_^Q , d6 9   A5}FRvbsHX 0,vS0*ySw[J<lZQ.x m<3jV=9p ^ C 7  !  | \ `  }iR<39Ws)SNKr AN>zt2U0ngGk.2Z7ULj3(Ha&8- E V 5 HG _&}LJ c M`R{ (p^U3> Q 03Tf),%';/v>ZF{IY.jW;p L~`E|7$8E+<qMl y h >  ! [ 7]hGPgwLPz-e umiI A|x#APh/a,:exk7S~a2<WIn,UNB_t+ y } k F w  w W wo l W8 gK ~ q />fB]2Ib_r&Rm%ci>Kh=kE& D [$!Xw4|2$jM  (b  M }      K  ? K~ ]#=NWc2x&4K_B//XA'H'6W<j(9VC ^'^uP}=oeGXZ_J-!v^ G  r  j@TdjpAh'1XIqStffqce{,* 37(DSf# "KFrG"5a.dNBE  M  D {U84sg~kJE cz jd{.K/.vFcuv3 {B ;rhOM35K3zi~WWo x.u Yg2R5$A9:%StY 2%f1La=yu6 r'EBc$1e<fMk,4tmZs}Ol B8+g ~B`_1#6y7_gkyrOp*.0n G ,@4tB_{([#?bN{VW1<2}W8&1Qy5z< 1LlygJ$[=:t% 4WjvF}y!nmXw=-|$1C?v/G+G(&P.%SbgkA-!'.*Ik]G g[~!LGk~ }jyg%W@&S%2;jL ;n2Xiq; :tWV7dmpzx\:#X=o[B;7GrhGb U8vU4}n'bJI)H ZF5q0% O zWW mzJ7x>n Cz ~Id#H'1KFexNpI9853޼ܾJH "~|34*+yyХЦ[Z8: ТТцӂkp,0ڢܡ߯YY{w .0nk37.*uxGE][egliY^|w}y9=  ICzx~'$W[sq0.*%OJKKz } = ; n l   ]`L I - -     r r rspl ;9!LIXX~PRyuRK{- * ^^HH{{ ""d$e$?%?%u%t%%%%%r%p%%#%$$$$r#n#""!!!!S T .+A>RO  .,) % 9!:,0{|*(bbjhyx01ih l r > 9  LNnostlmli qrHHsvB> $.+ghZY EDII5668NR oo=? \]~}&%VR[Ysq02dbolSNAAKMpo) ( | }   n l       Z X   ] ^ *,PSWZ_d:4 OQFA32:68=dbwy3/34deTTbdjdmo2.~-/usJIrr?@RTzz+-_\U T kj~&+6038CC -!-!!!2"6"""######Z$^$$$ % %Z%[%%%%%&&*&&&&&f'b'''-(,((((((() )))((((( (''&&%%6$;$""H!J!77xyOO!"=; q o v s sqjk`a_\b`lkacuvJJ#MGJH,4ߐމSR33ܰ۰22ۿڿVV٥٣X]شسؒؑ،؎؏؎ةبوهڨ۬ۂijYYPPSS^^ww%#be/.||=AMI7;hh:9"QVqs"#JHdd  P Q USGDMJmk!#GEQQ bc  KMjf_`>=rshYDI.LVg*U3Y.& B>6R2@ZTXj _ o k u  " y   : '  g ?  ] q m R m ( G  & X $X?  n J9OFi3)q9RJce^l^`k|-+w>Y%=t>vAubp 8935J`@N QQ(KW4D5,xm=A kF z!uXKuj-RY[|\ S 4s#_<pF<3ORe 5 G o p  E _ p  9 L a m r z z p y v ~ -  e G  < G ] B qnx; m &* n     h d C 1  a "   P\TP~71XlEFC5-8HQc }y\QMtQ__&AMDE.;c<^"h0yBKPM5,d|*<>N_#gh&RreL.+ >t.BYyLg&e 4ln3 ukA/l"Td _x*vtsYSHu{ & o~R&H~kL%{nJ#XoIc3 V,YY~zTd3tMA7?Bs5      J^  ? >  I  ]  1   f  v + > E ;G < z   " Y A Hp uN B * "   ?o^ 9 n    h    Y g qhYCDzFuXk&IkT3stchvnnkOcFW1? AP_9&@)sx\ [3<R e U7jd!f!.;IoiwCjz SYR|]LXJ%q' $V U _ ` ~- ^ q   U! C e  3o 6( 3 %v  G JXdJ5pO@2\L5dM|"\w]Dtu\H%2$V1aS0  :1h&A-B[^wqgkxBwj:q"l=81 g\ Mf6  % 6  1   3 e   > K   7'7 .9)A u`-7_^N c 6iQe@-S59\hB6Yv n [ O 4 &xR,}wL #4CjQ` r j  C gn S|-3:O0z"8% 8 N I @ / :^ 3  ) p   :7jsU;Y1  2  M>  c  R   O Y  ~ A    i c uX _& 7   w6s\%^qod*#EXH?Lc./\IfV@ =U`/3a!nWD!O5,_n]v 5[O gN'VlO7OEY8gFM:uH;I8F0h?R* 9J!^0u$3\ 0%m5i"s2 0h-N*X% XBqLD%2~P=?::|ܯjؓ>Ցsۼ|׸ʢ{lG EʏJпf3eD{i4q, r^9]9 u\;wSDL: MN g<ai.GL*6EZ  # (&+ )|-*-7+,D*+c)*()',) '(&)&)~'*(,!*.+/--0~.!2/2/1./,,#)L($}#~yEGJ{M l%WfY0aP:'Mum*-5[1fr/h-zKLB?`"%S2S^s  g D @ Wbh7y  [] E? ~) j  JB a  K,!$h s'#)-&*'*(*[)#*r)_)t)(b)'(_&!($ '#%y!b$a #^ # $!@$ #"1,!z hS  1  O= 7 b  l IhnZ l&h f[admڃg׌N ֲ))._זזQ$J}C  +UOhfG$.,ysLM/\P=$rD!.M&q y " Y v  l .    7f)m|y`oV:=P-o[ 4y@SpZ 3Ulp(`E!*O@=7Wxv*Qb[P]:=L M2H h <  !y |CM Bf|}2bsfA@^MHoGZ SM:.2h`=0~6m Vqޠ2ܨSD9<ٛ]#]Ad6nP4PM'UExJ[|&k|Gs6 K k 6 : 2~&6H !"E#!#!>$"`$"-$1##@## #'"" !( =u  ,M8  .j'8 i pw E &/ +n%6 $+wH*)^+=0rmFb@Q8 y$ ]D P: #^X' e E VNZ,  D!~w"1##$\%o%%*%*&G&k%%#J" r 9e)1w]O~yho ]0 ^ b% rz \ D: U f ~ N OA e on'kdAt `H@%?Z{M0/L/*3!}:P9wvv"3Qu4OK s!oytߠ0Oy6S#4yCoQa]~Su'o??B4Er]{C'NhIYNTjFKdlo V H.p RY4,0F|F}!}2NZ__{v&^j> d@ c   4i >/0z?]HNGT&^UdyfcuCc:!|2E.E+/St0m Y~8-]UH8id+ Mf:y.&gc:j-J ^ t<i}0 e x# 7< _Z`U  HV l 5!!'". "!#"$#%$B&|%&%&&'&9'%?'%('%&3%&$&#A%"z$!# "y!O s IKd 8 g > H-cMBxTmP EB9*v ;B*o?sCd0Sg Nk#T'cFzG=cMoL-cUL@8@`V&s?C2{S C~g `Pm%wtCDQR3 f '7XaiH 7 eH W H A Uq  e Mbw-;MPg E~LeoxayQE-~T 0Q  E # ]  ZR  9 fF7yPR +RK Y ~ + > F tk 1  ?,wQ~Xm.}1D\h`Kyy.O4%t}W;GDsGH"R(} dYdP!j+9~S C!L9_i 2r:8ov&(OW"2_:i5"'jk -b-: Q + ?  ) bqS@_nUXa)1j {KYMz)lA Dm & T j: q y  Y    ~i Y =I 3 ` sea0 h qP\`|I  H7    Uc  m  j F N_!a4`*1d gjx,n_<}Sd*DFH2_XR!KlU"I,Jj;/#;AW ce:t]au*@j=QdT= zH !z  !p$oK  R N S e W H )M 5M a { 9 %n)1:)N"NUD]'k"f6I{R+JI{CuMa:|Jp"JK47a;.~R- RCu!q1I51+#  T 2 ~6G9|,te*y/2#b.;R:ik;k; G, )L!5P"#j#### ]#@ ?#f # " "~ " U" " !X V!    1  *     lk[U,2#1Y  2 w' B6EJI94> ,dQ4x f7cE\[p&h/YYWoUK(QNZncE^6:WN 8n'.Y-\8~P !xQE$Nws4/EPvQK{^WaD7*kCFHjgBbFMX5/yi b + W I  r%'iy|(chQW?  6    d6El"K1/`)5RGhz~yhE:%:VB I(uRz[fI IjV NA O < 6V  m  ^86 # '^z U  &r S q$ &  / h  e JC!C:Ef\/cq wXJ@|4_.p!W Ok ߻ߦLv;r)#1^38O j*)w_-<5<Gr ^  j P . 8 $ - 3 X t    J    g _ y^  j } B k{8?A 0 N %( Q r+@^NSl,lIs`@}GHaQtNJ)x1QCSe?DjZhbV8O8 bu(C"jb<l&U._>VCBFW]\^A .PwnN LM p  B O   e Dc.&pU*8M+v  !s! !:!%"!"$""""" #M#%#t##}#"C#""!{"C!!{ p! $ uRYA:`,(9;ZzD* 2A z ? J ( b% m+,%#$fO}&TfVcRF1Jgv7y)GKWSB~vri7+?" 3As/=d8Fr1%;cqS nLg+`2jSla3.fv6W^ct[   d  Sj  gP]Vu Yo   r -     Y    P - y [  } t  L { g  0^y0%LIyhcSj K4\GaZe$:]6iL CSY~u2!m3Cx@n   :    k" T `+   iDP}OO^g |  L f O ^e + %M Kwgn)1YR [) ?Lrv!3^mk/v3_gQ{*2o!(!RxF+4h,fa#bCap K $ YG   #   L c  L=JPiKF|34\1{ 4 uF % M\If"1-O2L#2}.;r2L:$qhg@&5sJ,`;3^DnteC4TvF$$bHwKc 2# hO%p j | ` A $ z JQ %   2 Du S Dh ! E T%  L/9s< B`72[|jYibFpb3w<Y+FT4zOoud   )  & r   n 0( |  f 9(,G{.K49^9p!m9y+j[2qj ' eI :  Q YU R 3 &g Y M / xmsym uK9 &:t'(q@YvE9nCn+x KoEf BD1kh}Ms^]_TI:W ^8Rz%E!,{C2  w A ! & 8[q n @~ A i  S7=G>.5QaO'6i@t[ ? N   <* Z 490/rU^ 7 p  v C P IWY~LQ|xQ9-3(-z:4Dov77%hd\/2_DF$YVcwR:peA pL?lp yJV]?Ov3BN8tgi8k[9K jaZ <6cHi  @  AsgAn  ' 8 X Gr    ?a J *#   , 9^ z s ]I    8 P =   ! <A q 8Ua2 LS)%-)JNk3Q*+e8sh,m];0$Do(f}It~nq=pnk7-wO5v}WHLUiI"dW UIQP9Lp:BA?cg Z L 2D 9 g tG  40)  ^   :   J> p #  2q G o   | i j r  A h l0  y f   G} pa_2d[    4 X j)YK ` ] _l*L[{amF8X1<~KG+0uV@n!`{t:Ji ~TiXi?L8c+u.DKoS$y10 V , ~ d   @ d  K  ;   - i  U ) ; 7M ^ c BH # c~^  ldIg0 `Z{!W0Lp pa<\6U^B=#*5R_AA*DP7neb'{K$h?"N c 2 9P d 1| `  >  y b3 N .l ,}P*X&G i b <   ksmNUsbh= v.=1 F 0 H K [  E | | K kF hBHeGl.>U-a4:&0v* ^!-DEu{39iXLZg/\c9n%%yJNbix`0p[2VuWkx?,{S,g^0AD_A=7zJgv 1sj7(>Nz []or2)1&XoaZ\t0s`e[ds*_.p5NC D   i    X j 1 )Jkz'?.nL*yIf>:rcVM\< ;Vqsb6~$}{WU8%]'1>:Uca~U<Zo"W~ZiL"Mgu#Q)wK8rQAQ~ >`T+Sg1<&n4BNZkuaUK IOIGEN?=*BQkWv~"c_6d:\{:f;;y!6CawWr !5YsM+9#'A^n>8   M,O)-BB~am!!dBL^8cRmabsjsT~!}O7}(s.k,q5i/u2'*,Bf$U9}C2&o)GDv)~D2Jn`_Y8 s! 5[gBD\v:aK0B4F/(e'/<Zy2-2)rIqqrz 9il5 8%:.F<-s8_vC #9y h  ;J  I Z   a [  v ' ?8 N K = !T e P 9 /  >@ v E  G B{<&ST q  |  w   X7  j  yI7"+`%_sj>O\&y[u6!w#U"Z(\@#+ [5j-.T)xin&:#Tqk U###E|;P~#3vwl%ed]UO(%Y.k#C0ZwHq\?V(WSiXA-!  eh1G(}@\B+gzI1?s? :I]bjikdDo=AR+dvLkUhLHA3Y^ U3[v#ol;zhx v$<p%q C;f$HZ5}T  _h C  M  i c< " u s 1?dOE?CpXIqhf<+f2N9&Mj-3;KX r '9Wu\+Pip4*Vd )0MlO&.ZRt<g e5=zrtG#`?W$h'9qJ10?i@Rj E;OkZ l"XOF|FLUl4Mv(o Pk@N7|2{2 IGj8{ l51D?8f+:XU b H [  ! . r b / |]  u  / o     ! 2 :o :M @, 0 -  ` 0 QS[ <w 0l   8.  K  V z /R 9vCbl:9 zJiyu96TAUQ`TurM.CL2u|7+Ld~?"JVp:kV BKtR5 @-Gxpeh%?NM__Vko3> $P:|d>\ |'08+|Xg7< 6yH~ ^Z9$LqE#3U:EF rahB/, [j8EhJ6! ">a#Xisip]UAON3U[2q8PHz B:nx+e_0]R RR ; w  5 A S8 [a nk w   o c K 4   < m   + z   q   Z [ /   0r C& JUN@P?7l(b)^3j.c(zAS$Ok0E[b1Kc>`41*jTnA33w0;ASp`A%+^G}Hy_?'+ T| $;P@ecx|dw<X3T vOZ27i{+ 2>(AMQRU\(QYxG1E/"[R3<5_T!c1L4o~%zK8"uZ Pl$B_-Vp0ypA/rp#V^D'X~C / {2< P9 Xz p  ( X    " 5 K/ cK }] p v ~ w (p QZ F #   M } U    R K$RKo@@VEVu2U9qBlr1ldR >x6? ;X\ 0|E3g.xX7X6(hg+4SpgN><-B=i8) u*B:_Z)&pWmvrTH 1!C@AH>X4_#f b\H.s Jk.}HNM"t8~Fh /xLHnf86g|%_2yG nRx2Ha(p8|==vY3%v;{bF#>1.N48,J[szcB6Xr`J Fjl<X7fJQYP]_cdk q!x.7<?B@@<;3- ~~ rwjk`]ZUTNJ@1zjcTNwDH<4/)w'4LA0,~-(>DO?\k}{C uDOx':gQ?r 1O{&@W0w@=\!r8#lyH(f)M|sL(o08<bDDF$B^70Ab{{T,yiST"<#F{^J9p'H`8p!se:Hr[R4EfOpX"o|8Ca#S ^(MQ%xj[g)[\]_^o9{sCX{E/1 xgM((Ag^{>Cf~I4x9 W!qMNS ]RgUp(f[-'m^>.db .<ISYbgjzl\l6gcZNEg::0+##a> wS1k>xqYR<.h8tYB.#bD$.>Up 7i'52EkQ_j{?^u~jlT`<J"B5-)!""#'/2==B}<d6W&=. aB{D}f$Lt,o= wY5 &&*qW. pCZD$f8nW=+/@Zp 6'S8lETar~1Op/W B%X_yY5Z)zLo%Ow"%O0z>IOP#RPHxE4&;`\@#X%^Q=#KmhI'_3 e@$";\!7K1]\syIz#Jv(U@g(/175=n8`?C;5<<687o4?;4>:M>?=@M8 6)|#5 T y/SlfQlE<0': Z=qM"t+aY!XSF t_AW*"s7`.hO0?f.a%\D~%GF&)qlm<t2*32k?P@25~Qg <@~p$U;Y :_  &!AVnmT0ezFb&S @)uN(sN*U*Z4zV-p? xoAP: [(vW[%9tQ(rU@(}pURDD@CCJKPW \e/p@zTk} .Ju /]}w\"BK'p 1mRJv%:gUIz+5Ocuxf[K<}/r!ZE'j;p`MW:0* o; pEfA|yy|wsok s#v3Oc/T0:IZ-dVyIzJ3t4a(J5~X?!vU>xnSD.* r}Q]1D"l_QE:#! qTxAi"R A-/Le{ v2kQdw_YVP L+HWAz?61* 8]"9PgrqdI;{ pbUA1 ueUD>w,W+B# s]:bQ1% zh_R$F6?J1_,r" 3Mm 3Z >p BsL7(o6>NZUey1h"&9RWwo*:P=^Smtw9\ 4Mv9{>jTUV?f(g oppmhlbPWJ=%s>y}KaB$NzY4W%at,U;"`*Z(ynd][/SNFEx?H<9725l.K31/31366wYy &D^y$06 E)IIXj[jm|=c"T  G#s-DRe=rp&<Vn!%)4+H4U;lCyNYis)8?IOSUVSU~OoOYF@B+:/)iN2mW8n!FrKy_J1yO&];d<do:Q:#yP2 vkeaG_&Y ]_gp}|dWE8* 3O o?\}+N_ (1COZmr#)=CUdr +@bv|oVD(` D(!/CXpyfQ rHPZdkv4y[{7Vkuod[ OD,6-.DHT^ailnrqwtzv{{y|{g{i~X~YLPGPNYajxyj_MG8/!  "0;HUbp~n`P=,oW>%~zrxwkdrUjAm2jljnlsqzvy_<'{oOu1qj`VPB:o1V$C -~qdVK>5'" $)5 7 CB!M)M4W>SI\WWb]tZZZUWNOE?4*   %18JShx/J`}0E`v|v%l;aX\nMG<1+ 2E`t zg0XAES6d(q   18'K8WAhTx`q{-E_v '+566;T<n:<72.#/Qg#2tC_OM[6h&m{}s\>eB$xruf[[-P=1f=ppYE;"' eN4~i]K>.ygM9# oX>) rbUI=x2c*L7${|{#9Ng8#X3|FZh 0T|Aj1ARe"uIg 0I,lCSk{#<^| &:%S-f2z589:953/+' #/<GT \jq~naPC2'taO@+lVB'pbF8n^E7 q'U+B5#;CJPX]gjsuTyJ/! o[H/xf~VtFl4^(YJ D8.&ztj`+Y>NKF`@t64..,.2!3><\={ILW^g(mK{q)LoFj4Sv (A^s.JZp!4EUfrufVD0 kS< gS4 "",6=MThxp^R8&hF*nU3cF( w[@# |sf_UQKHwGrDgCaDW?OEI@>B8A-@&A>A >?@?BBFFKKRRYt]hcWfMs|'|~bM5x`N6$ p%_2QD@N4c!ot#e.R2E;0;"B?E?B?>=:63p/e,R$K%<0'  ndOF 1 $ .6BNXit{hYM;5  ,AWk(8O_v&AZt1Nd   %#0)>)R/].p5|59>BGLQW]cjny |(*47<@BFGHHHJCJ?E<= 6)5-+7'<EK SZchpx|~m`MA*t`F,x]F(xfTA1|g[I 9+ |qfbUTIF><62{1s)i.b&X*M%F(7%0(#))0-73Rey4BWgx !&:,O6i=HR]hs,7FYaty  '.1;=EIOQ\Wd`hhiijhiefbac\d\c^e_bcac^_^tXgZYRGV?Q&QNOLNKNIMInKZJCI)NIQLSSVt[_[@`'a bgfgiffgOg1ad^_Y\UYTjX^SL[6U.\] ^dcilmstx|}ql[WHB8/& xwsmjf^_UVJL@?7.*!(-AI[ky6Jdwzvqlkdd ] ^;64-1,--+/,1.31468|:x=v?tCoEoKjKhVfRcb`^]nZpV~TPJKAA:53)+ &6AR\ny)4LZm|ule+]?VRMgKy@>71/($$$9GYhv #.6 @ D(Q+V0^8g;lEuI{STadlsx~vnfX SA:"(&"1 4@BKPV]agmspavPzB- vbQ:+icF=%f[C2 si`XxNvLl@lB_9d8X4Z1V3R+R3Q+L1R0L2O5N6M8OIGJNPXO_SjTrU{SWQVNRKMGFB=;42+& $,09<HHUV_fitvun^TG90{iXM6*teRE3#xmaWLA9,&}sslfaYXMP@G8<13,,&%#  '.?=:<7863 42'/32D+Q0`*m.}*++(,)-,//63;9@ AFIKO P W%V$^+\'c0f*k2n.u2v3~16/5.0-))!$  "'6:FNR__lktvzwk`TJ90!}n^M=-vkVK:)       zsc]PF>0* zwmia[UOHD<82,&#  $68LPbhzy}m m!a+\=VJN[Kl?z@45,)%!( 8I Siq  "2 ;MV crz!#),.56>>DGLLTRX\ Zcbhllvt|   #.{4u9fD^FRRJS<`5_'k!nt z|zpbRH6+ zu_WG;.# ~vmg]WNI?>31*$! }ytoib]SQBB4/ ' $*2t42'' *9EPahz!'26>HJU[_jktx}*)6:AKMZ[ejqu~}zxplg`\TOJ??!2#11%6 BJT]io|yh_OE6,  %|(s"g(a"T(M C)91'&#   ymh\SJ?7." v-t-i>f>[KXRPYKgEj?x77-*(#  -2AGQ^coy '(56BDKPWX` afki&p,n3r=r?sOsPt\terjrwr|ppmllem^j\b[[UXPOL DD=6,4-+:&>DPR \cfsqurcYQB:0#" -&4.669x9o<h:_?X;Q>G;C=894<+8%862 2 0,*'# |rnb`SQEB74+# !&0{6r@jEdS\SRcNfBr?y30#  "*3<DKU[fkv|((00 7 8>= B#B*F0E5I>I@IJLPJVLaLbKqKrKHJGEFAB=>6;/4++$ #'.6>GKYYgkszvmf\TLA:-(     # '{#u(p&c&b+T$S.G$E,8(8'-**&!&&"#   vke[RK@92(! (/6=}GsKpVf[bfYiTwLxE>8.+! !)59EMUaesw  "$(++)3/70@2E4N5S6[8a7j:p8y7:3;07.1,+(&$&*5;DLR]`nn}~wrk`\QKB:3*"  !#"&")$*&t)t%f)d%Z'U#L#I ><3,*!  yvogbZSMF>:-+!  "',44B=NKVt[qafgcqXrSMBC36() (,9;HLU[cjox| %%,04; ? D MOZ\ihxy    +/9@GQSabor|yvlh_ZPO@@3/%!        }urffZYQKF?<41+&# wvjg\XNHB76&* !%',.66>?HHRSZ`d{lwopwk{e^ZTRGG>?45.+'$  ! *37AGNW\diqx}!*'54<DFOSYccqp{ #*06=BLMZ[elpyzvohe[YNNAA61+$ ~ruii`\XSOHG?>:25+,(##}tv hj _ ]UT FK :>/1$$  # &(+1187B>IIOUX]aikrv|}wnld^[QRHH??9720.*''!$ $*09<FJQY]ekqv~ #%&+*2068;A@KGRQZ]bglsw "*16;CGNSX`cintx}xqoeb\URHF<92,% ~{uqpfi__[UWLSDM@C@:<5621.-+)|){&s%o&g"g&Z \$P"N#I"?%A"1'6$'))')-* 1 0275=q=k?d:c>Y9W=P9J;H:>:><3:1<+="=$??C @ GEIJLNSRXX[c]keoouv~}~|urpgj`b[ZUUNRGMCGAB<? 9;68"5%4-5.0869/@4D0I3M2S2U4\3_6c6i7k9rGBKHIGLRLPNVQ[PZVdS`[jXi^m_qcrewjyizqq}xx~''.17;ACI}MyRwWs[q^kcjidjbq^rZxT{VIPBD>970.*## ||u|tpyjykxctct[s[oTqQnOnGlHm>k?k6l7j/j,m'h#lmiok noposruvw{y|~x{uuspomnhleifeddc d bad_f"` d)a'g/a/h4c7h;i>hAjFlDmPoKqUqTuXw\w^~czbihmnptq{s}y}}}{z{u%y&r-u.n4p7m9j@i?eGeF_LaOZPZVXVQXSaMZKgIaDgBk@i:q9m6t0u0u*z&y&}~ |zztuqmofib`_X[PSMIK>E::72./$(  ! '&+,0256;;>B?IDLINOQURYV\Y_\a_cbcgbidighigkhhihhhgegcf`d_ ` ]]Y]TYS"S#R(N)O,I2L0E7H5A<C;>B>?9F;F4I6K0O1M-U+R(V'Z$V"_Za^d`h`k c memilmknnmolqlpkommkkkjiifegbca^_[[WXSSPNKKDH>B<;67/3++'%"!  ##'&),*0/0417697<:>=@?BBBFCGEHHGKGKIJLIKJKLHLGMFKFHFFDF@D?A = ?9<88743"11%.%.%*++('.'.$.#4!1585=8><A ?D ? I @LCKHJLKMMMPMQOPQQQQQQRQQQPQNRLPMLLLHLEHEECA@?>;=5<27211+0',$(!$      # #$$&%'(((,&0'--*0+/+0,./)3(0*-++***((&(%%%"###      #!$%%((+*-+0.1123363:3:6:99;8<:<:;;;=8>9;;9<9:7;6;48739/704/2-/-,+*)''$&!#                  ##"&$#!%!$##"##!%&$ #! !                                                                                                                 LISTVINFOINAMsound5IART JfreegmanICRD2014ISFTLMMS (libsndfile-1.0.25)id3 ID3@   7j;ITALBToxic sound collectionTXXX"SoftwareLMMS (libsndfile-1.0.25)TPE1 JfreegmanTDRC2014TIT2sound5toxic-0.11.3/sounds/license000066400000000000000000000004421416141666600156150ustar00rootroot00000000000000ToxicError.wav, ToxicRecvMessage.wav, ToxicContactOffline.wav, ToxicIncomingCall.wav, ToxicTransferComplete.wav, ToxicContactOnline.wav, ToxicOutgoingCall.wav and ToxicTransferStart.wav are licensed under the "Creative Commons Attribution 3.0 Unported". All credit attributed to Jfreegman. toxic-0.11.3/src/000077500000000000000000000000001416141666600135245ustar00rootroot00000000000000toxic-0.11.3/src/api.c000066400000000000000000000115261416141666600144460ustar00rootroot00000000000000/* api.c * * * Copyright (C) 2017 Jakob Kreuze * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #include #include #include #include "execute.h" #include "friendlist.h" #include "line_info.h" #include "message_queue.h" #include "misc_tools.h" #include "settings.h" #include "toxic_strings.h" #include "windows.h" #ifdef PYTHON #include "python_api.h" Tox *user_tox; static WINDOW *cur_window; static ToxWindow *self_window; void api_display(const char *const msg) { if (msg == NULL) { return; } self_window = get_active_window(); line_info_add(self_window, NULL, NULL, NULL, SYS_MSG, 0, 0, msg); } FriendsList api_get_friendslist(void) { return Friends; } char *api_get_nick(void) { size_t len = tox_self_get_name_size(user_tox); uint8_t *name = malloc(len + 1); if (name == NULL) { return NULL; } tox_self_get_name(user_tox, name); name[len] = '\0'; return (char *) name; } Tox_User_Status api_get_status(void) { return tox_self_get_status(user_tox); } char *api_get_status_message(void) { size_t len = tox_self_get_status_message_size(user_tox); uint8_t *status = malloc(len + 1); if (status == NULL) { return NULL; } tox_self_get_status_message(user_tox, status); status[len] = '\0'; return (char *) status; } void api_send(const char *msg) { if (msg == NULL || self_window->chatwin->cqueue == NULL) { return; } char *name = api_get_nick(); if (name == NULL) { return; } self_window = get_active_window(); snprintf((char *) self_window->chatwin->line, sizeof(self_window->chatwin->line), "%s", msg); add_line_to_hist(self_window->chatwin); int id = line_info_add(self_window, true, name, NULL, OUT_MSG, 0, 0, "%s", msg); cqueue_add(self_window->chatwin->cqueue, msg, strlen(msg), OUT_MSG, id); free(name); } void api_execute(const char *input, int mode) { self_window = get_active_window(); execute(cur_window, self_window, user_tox, input, mode); } int do_plugin_command(int num_args, char (*args)[MAX_STR_SIZE]) { return do_python_command(num_args, args); } int num_registered_handlers(void) { return python_num_registered_handlers(); } int help_max_width(void) { return python_help_max_width(); } void draw_handler_help(WINDOW *win) { python_draw_handler_help(win); } void cmd_run(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { FILE *fp; const char *error_str; cur_window = window; self_window = self; if (argc != 1) { if (argc < 1) { error_str = "Path must be specified."; } else { error_str = "Only one argument allowed."; } line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, error_str); return; } fp = fopen(argv[1], "r"); if (fp == NULL) { error_str = "Path does not exist."; line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, error_str); return; } run_python(fp, argv[1]); fclose(fp); } void invoke_autoruns(WINDOW *window, ToxWindow *self) { char abspath_buf[PATH_MAX + 256]; char err_buf[PATH_MAX + 128]; if (user_settings->autorun_path[0] == '\0') { return; } DIR *d = opendir(user_settings->autorun_path); if (d == NULL) { snprintf(err_buf, sizeof(err_buf), "Autorun path does not exist: %s", user_settings->autorun_path); api_display(err_buf); return; } struct dirent *dir = NULL; cur_window = window; self_window = self; while ((dir = readdir(d)) != NULL) { size_t path_len = strlen(dir->d_name); if (!strcmp(dir->d_name + path_len - 3, ".py")) { snprintf(abspath_buf, sizeof(abspath_buf), "%s%s", user_settings->autorun_path, dir->d_name); FILE *fp = fopen(abspath_buf, "r"); if (fp == NULL) { snprintf(err_buf, sizeof(err_buf), "Invalid path: %s", abspath_buf); api_display(err_buf); continue; } run_python(fp, abspath_buf); fclose(fp); } } closedir(d); } #endif /* PYTHON */ toxic-0.11.3/src/api.h000066400000000000000000000026061416141666600144520ustar00rootroot00000000000000/* api.h * * * Copyright (C) 2017 Jakob Kreuze * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef API_H #define API_H #include "friendlist.h" #include "windows.h" void api_display(const char *const msg); FriendsList api_get_friendslist(void); char *api_get_nick(void); Tox_User_Status api_get_status(void); char *api_get_status_message(void); void api_send(const char *msg); void api_execute(const char *input, int mode); int do_plugin_command(int num_args, char (*args)[MAX_STR_SIZE]); int num_registered_handlers(void); int help_max_width(void); void draw_handler_help(WINDOW *win); void invoke_autoruns(WINDOW *w, ToxWindow *self); void cmd_run(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); #endif /* API_H */ toxic-0.11.3/src/audio_call.c000066400000000000000000000621641416141666600157750ustar00rootroot00000000000000/* audio_call.c * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #include "audio_call.h" #include "audio_device.h" #include "chat_commands.h" #include "chat.h" #include "friendlist.h" #include "global_commands.h" #include "line_info.h" #include "misc_tools.h" #include "notify.h" #include "settings.h" #include "toxic.h" #include "windows.h" #ifdef AUDIO #ifdef VIDEO #include "video_call.h" #endif /* VIDEO */ #include #include #include #include #include #include #include #include #ifdef __APPLE__ #include #include #else #include #include /* compatibility with older versions of OpenAL */ #ifndef ALC_ALL_DEVICES_SPECIFIER #include #endif /* ALC_ALL_DEVICES_SPECIFIER */ #endif /* __APPLE__ */ struct CallControl CallControl; void on_call(ToxAV *av, uint32_t friend_number, bool audio_enabled, bool video_enabled, void *user_data); void on_call_state(ToxAV *av, uint32_t friend_number, uint32_t state, void *user_data); void on_audio_receive_frame(ToxAV *av, uint32_t friend_number, int16_t const *pcm, size_t sample_count, uint8_t channels, uint32_t sampling_rate, void *user_data); void callback_recv_invite(Tox *m, uint32_t friend_number); void callback_recv_ringing(uint32_t friend_number); void callback_recv_starting(uint32_t friend_number); void callback_recv_ending(uint32_t friend_number); void callback_call_started(uint32_t friend_number); void callback_call_canceled(uint32_t friend_number); void callback_call_rejected(uint32_t friend_number); void callback_call_ended(uint32_t friend_number); void write_device_callback(uint32_t friend_number, const int16_t *PCM, uint16_t sample_count, uint8_t channels, uint32_t sample_rate); void audio_bit_rate_callback(ToxAV *av, uint32_t friend_number, uint32_t audio_bit_rate, void *user_data); static void print_err(ToxWindow *self, const char *error_str) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "%s", error_str); } ToxAV *init_audio(ToxWindow *self, Tox *tox) { Toxav_Err_New error; CallControl.audio_errors = ae_None; CallControl.prompt = self; CallControl.av = toxav_new(tox, &error); CallControl.audio_enabled = true; CallControl.default_audio_bit_rate = 64; CallControl.audio_sample_rate = 48000; CallControl.audio_frame_duration = 20; CallControl.audio_channels = user_settings->chat_audio_channels; CallControl.video_enabled = false; CallControl.default_video_bit_rate = 0; CallControl.video_frame_duration = 0; if (!CallControl.av) { CallControl.audio_errors |= ae_StartingCoreAudio; line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Failed to init ToxAV"); return NULL; } if (init_devices() == de_InternalError) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Failed to init devices"); toxav_kill(CallControl.av); return CallControl.av = NULL; } toxav_callback_call(CallControl.av, on_call, tox); toxav_callback_call_state(CallControl.av, on_call_state, NULL); toxav_callback_audio_receive_frame(CallControl.av, on_audio_receive_frame, NULL); toxav_callback_audio_bit_rate(CallControl.av, audio_bit_rate_callback, NULL); return CallControl.av; } void read_device_callback(const int16_t *captured, uint32_t size, void *data) { UNUSED_VAR(size); Toxav_Err_Send_Frame error; uint32_t friend_number = *((uint32_t *)data); /* TODO: Or pass an array of call_idx's */ int64_t sample_count = ((int64_t) CallControl.audio_sample_rate) * \ ((int64_t) CallControl.audio_frame_duration) / 1000; if (sample_count <= 0) { return; } toxav_audio_send_frame(CallControl.av, friend_number, captured, sample_count, CallControl.audio_channels, CallControl.audio_sample_rate, &error); } void write_device_callback(uint32_t friend_number, const int16_t *PCM, uint16_t sample_count, uint8_t channels, uint32_t sample_rate) { if (CallControl.calls[friend_number].status == cs_Active) { write_out(CallControl.calls[friend_number].out_idx, PCM, sample_count, channels, sample_rate); } } bool init_call(Call *call) { if (call->status != cs_None) { return false; } *call = (struct Call) { 0 }; call->status = cs_Pending; call->in_idx = -1; call->out_idx = -1; call->audio_bit_rate = CallControl.default_audio_bit_rate; #ifdef VIDEO call->vin_idx = -1; call->vout_idx = -1; call->video_width = CallControl.default_video_width; call->video_height = CallControl.default_video_height; call->video_bit_rate = CallControl.default_video_bit_rate; #endif /* VIDEO */ return true; } static bool cancel_call(Call *call) { if (call->status != cs_Pending) { return false; } call->status = cs_None; return true; } static int start_transmission(ToxWindow *self, Call *call) { if (!self || !CallControl.av) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Failed to prepare audio transmission"); return -1; } DeviceError error = open_input_device(&call->in_idx, read_device_callback, &self->num, CallControl.audio_sample_rate, CallControl.audio_frame_duration, CallControl.audio_channels); if (error != de_None) { if (error == de_FailedStart) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Failed to start audio input device"); } if (error == de_InternalError) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Internal error with opening audio input device"); } } if (open_output_device(&call->out_idx, CallControl.audio_sample_rate, CallControl.audio_frame_duration, CallControl.audio_channels) != de_None) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Failed to open audio output device!"); } return 0; } static void start_call(ToxWindow *self, Call *call) { if (call->status != cs_Pending) { return; } if (start_transmission(self, call) != 0) { return; } call->status = cs_Active; #ifdef VIDEO if (call->state & TOXAV_FRIEND_CALL_STATE_SENDING_V) { callback_recv_video_starting(self->num); } if (call->video_bit_rate) { start_video_transmission(self, CallControl.av, call); } #endif } static int stop_transmission(Call *call, uint32_t friend_number) { if (call->status != cs_Active) { return -1; } call->status = cs_None; if (call->in_idx != -1) { close_device(input, call->in_idx); } if (call->out_idx != -1) { close_device(output, call->out_idx); } Toxav_Err_Call_Control error = TOXAV_ERR_CALL_CONTROL_OK; if (call->state > TOXAV_FRIEND_CALL_STATE_FINISHED) { toxav_call_control(CallControl.av, friend_number, TOXAV_CALL_CONTROL_CANCEL, &error); } if (error != TOXAV_ERR_CALL_CONTROL_OK) { return -1; } return 0; } void terminate_audio(void) { for (int i = 0; i < CallControl.max_calls; ++i) { stop_transmission(&CallControl.calls[i], i); } if (CallControl.av) { toxav_kill(CallControl.av); } terminate_devices(); } /* * End of transmission */ /* * Callbacks */ void on_call(ToxAV *av, uint32_t friend_number, bool audio_enabled, bool video_enabled, void *user_data) { UNUSED_VAR(av); Tox *m = (Tox *) user_data; Call *call = &CallControl.calls[friend_number]; init_call(call); call->state = TOXAV_FRIEND_CALL_STATE_ACCEPTING_A | TOXAV_FRIEND_CALL_STATE_ACCEPTING_V; if (audio_enabled) { call->state |= TOXAV_FRIEND_CALL_STATE_SENDING_A; } if (video_enabled) { call->state |= TOXAV_FRIEND_CALL_STATE_SENDING_V; } callback_recv_invite(m, friend_number); } void on_call_state(ToxAV *av, uint32_t friend_number, uint32_t state, void *user_data) { UNUSED_VAR(av); UNUSED_VAR(user_data); Call *call = &CallControl.calls[friend_number]; if (call->status == cs_None) { return; } call->state = state; switch (state) { case TOXAV_FRIEND_CALL_STATE_ERROR: case TOXAV_FRIEND_CALL_STATE_FINISHED: if (state == TOXAV_FRIEND_CALL_STATE_ERROR) { line_info_add(CallControl.prompt, false, NULL, NULL, SYS_MSG, 0, 0, "ToxAV callstate error!"); } if (call->status == cs_Pending) { cancel_call(call); callback_call_rejected(friend_number); } else { #ifdef VIDEO callback_recv_video_end(friend_number); callback_video_end(friend_number); #endif /* VIDEO */ stop_transmission(call, friend_number); callback_call_ended(friend_number); } break; default: if (call->status == cs_Pending) { /* Start answered call */ callback_call_started(friend_number); } #ifdef VIDEO /* Handle receiving client video call states */ if (state & TOXAV_FRIEND_CALL_STATE_SENDING_V) { callback_recv_video_starting(friend_number); } else { callback_recv_video_end(friend_number); } #endif /* VIDEO */ break; } } void on_audio_receive_frame(ToxAV *av, uint32_t friend_number, int16_t const *pcm, size_t sample_count, uint8_t channels, uint32_t sampling_rate, void *user_data) { UNUSED_VAR(av); UNUSED_VAR(user_data); write_device_callback(friend_number, pcm, sample_count, channels, sampling_rate); } void audio_bit_rate_callback(ToxAV *av, uint32_t friend_number, uint32_t audio_bit_rate, void *user_data) { UNUSED_VAR(user_data); Call *call = &CallControl.calls[friend_number]; call->audio_bit_rate = audio_bit_rate; toxav_audio_set_bit_rate(av, friend_number, audio_bit_rate, NULL); } void callback_recv_invite(Tox *m, uint32_t friend_number) { if (friend_number >= Friends.max_idx) { return; } if (Friends.list[friend_number].chatwin == -1) { if (get_num_active_windows() >= MAX_WINDOWS_NUM) { return; } Friends.list[friend_number].chatwin = add_window(m, new_chat(m, Friends.list[friend_number].num)); } const Call *call = &CallControl.calls[friend_number]; for (uint8_t i = 0; i < MAX_WINDOWS_NUM; ++i) { if (windows[i] != NULL && windows[i]->onInvite != NULL && windows[i]->num == friend_number) { windows[i]->onInvite(windows[i], CallControl.av, friend_number, call->state); } } } void callback_recv_ringing(uint32_t friend_number) { const Call *call = &CallControl.calls[friend_number]; for (uint8_t i = 0; i < MAX_WINDOWS_NUM; ++i) { if (windows[i] != NULL && windows[i]->onRinging != NULL && windows[i]->num == friend_number) { windows[i]->onRinging(windows[i], CallControl.av, friend_number, call->state); } } } void callback_recv_starting(uint32_t friend_number) { Call *call = &CallControl.calls[friend_number]; for (uint8_t i = 0; i < MAX_WINDOWS_NUM; ++i) { if (windows[i] != NULL && windows[i]->onStarting != NULL && windows[i]->num == friend_number) { windows[i]->onStarting(windows[i], CallControl.av, friend_number, call->state); start_call(windows[i], call); } } } void callback_recv_ending(uint32_t friend_number) { const Call *call = &CallControl.calls[friend_number]; for (uint8_t i = 0; i < MAX_WINDOWS_NUM; ++i) { if (windows[i] != NULL && windows[i]->onEnding != NULL && windows[i]->num == friend_number) { windows[i]->onEnding(windows[i], CallControl.av, friend_number, call->state); } } } void callback_call_started(uint32_t friend_number) { Call *call = &CallControl.calls[friend_number]; for (uint8_t i = 0; i < MAX_WINDOWS_NUM; ++i) { if (windows[i] != NULL && windows[i]->onStart != NULL && windows[i]->num == friend_number) { windows[i]->onStart(windows[i], CallControl.av, friend_number, call->state); start_call(windows[i], call); } } } void callback_call_canceled(uint32_t friend_number) { const Call *call = &CallControl.calls[friend_number]; for (uint8_t i = 0; i < MAX_WINDOWS_NUM; ++i) { if (windows[i] != NULL && windows[i]->onCancel != NULL && windows[i]->num == friend_number) { windows[i]->onCancel(windows[i], CallControl.av, friend_number, call->state); } } } void callback_call_rejected(uint32_t friend_number) { const Call *call = &CallControl.calls[friend_number]; for (uint8_t i = 0; i < MAX_WINDOWS_NUM; ++i) { if (windows[i] != NULL && windows[i]->onReject != NULL && windows[i]->num == friend_number) { windows[i]->onReject(windows[i], CallControl.av, friend_number, call->state); } } } void callback_call_ended(uint32_t friend_number) { const Call *call = &CallControl.calls[friend_number]; for (uint8_t i = 0; i < MAX_WINDOWS_NUM; ++i) { if (windows[i] != NULL && windows[i]->onEnd != NULL && windows[i]->num == friend_number) { windows[i]->onEnd(windows[i], CallControl.av, friend_number, call->state); } } } /* * End of Callbacks */ /* * Commands from chat_commands.h */ void cmd_call(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); UNUSED_VAR(m); UNUSED_VAR(argv); if (argc != 0) { print_err(self, "Unknown arguments."); return; } if (!CallControl.av) { print_err(self, "ToxAV not supported!"); return; } if (!self->stb->connection) { print_err(self, "Friend is offline."); return; } Call *call = &CallControl.calls[self->num]; if (call->status != cs_None) { print_err(self, "Already calling."); return; } init_call(call); place_call(self); } void cmd_answer(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); UNUSED_VAR(m); UNUSED_VAR(argv); Toxav_Err_Answer error; if (argc != 0) { print_err(self, "Unknown arguments."); return; } if (!CallControl.av) { print_err(self, "Audio not supported!"); return; } Call *call = &CallControl.calls[self->num]; if (call->status != cs_Pending) { print_err(self, "No incoming call!"); return; } toxav_answer(CallControl.av, self->num, call->audio_bit_rate, call->video_bit_rate, &error); if (error != TOXAV_ERR_ANSWER_OK) { if (error == TOXAV_ERR_ANSWER_FRIEND_NOT_CALLING) { print_err(self, "No incoming call!"); } else if (error == TOXAV_ERR_ANSWER_CODEC_INITIALIZATION) { print_err(self, "Failed to initialize codecs!"); } else if (error == TOXAV_ERR_ANSWER_FRIEND_NOT_FOUND) { print_err(self, "Friend not found!"); } else if (error == TOXAV_ERR_ANSWER_INVALID_BIT_RATE) { print_err(self, "Invalid bit rate!"); } else { print_err(self, "Internal error!"); } return; } /* Callback will print status... */ callback_recv_starting(self->num); } void cmd_reject(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); UNUSED_VAR(m); UNUSED_VAR(argv); if (argc != 0) { print_err(self, "Unknown arguments."); return; } if (!CallControl.av) { print_err(self, "Audio not supported!"); return; } Call *call = &CallControl.calls[self->num]; if (call->status != cs_Pending) { print_err(self, "No incoming call!"); return; } /* Manually send a cancel call control because call hasn't started */ toxav_call_control(CallControl.av, self->num, TOXAV_CALL_CONTROL_CANCEL, NULL); cancel_call(call); /* Callback will print status... */ callback_call_rejected(self->num); } void cmd_hangup(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); UNUSED_VAR(m); UNUSED_VAR(argv); if (!CallControl.av) { print_err(self, "Audio not supported!"); return; } if (argc != 0) { print_err(self, "Unknown arguments."); return; } Call *call = &CallControl.calls[self->num]; if (call->status == cs_None) { print_err(self, "Not in a call."); return; } stop_current_call(self); } void cmd_list_devices(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); UNUSED_VAR(m); if (argc != 1) { if (argc < 1) { print_err(self, "Type must be specified!"); } else { print_err(self, "Only one argument allowed!"); } return; } DeviceType type; if (strcasecmp(argv[1], "in") == 0) { /* Input devices */ type = input; } else if (strcasecmp(argv[1], "out") == 0) { /* Output devices */ type = output; } else { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Invalid type: %s", argv[1]); return; } // Refresh device list. get_al_device_names(); print_al_devices(self, type); } /* This changes primary device only */ void cmd_change_device(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); UNUSED_VAR(m); if (argc != 2) { if (argc < 1) { print_err(self, "Type must be specified!"); } else if (argc < 2) { print_err(self, "Must have id!"); } else { print_err(self, "Only two arguments allowed!"); } return; } DeviceType type; if (strcmp(argv[1], "in") == 0) { /* Input devices */ type = input; } else if (strcmp(argv[1], "out") == 0) { /* Output devices */ type = output; } else { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Invalid type: %s", argv[1]); return; } char *end; long int selection = strtol(argv[2], &end, 10); if (*end) { print_err(self, "Invalid input"); return; } if (set_al_device(type, selection) == de_InvalidSelection) { print_err(self, "Invalid selection!"); return; } } void cmd_mute(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); UNUSED_VAR(m); if (argc != 1) { print_err(self, "Specify type: \"/mute in\" or \"/mute out\"."); return; } DeviceType type; if (strcasecmp(argv[1], "in") == 0) { /* Input devices */ type = input; } else if (strcasecmp(argv[1], "out") == 0) { /* Output devices */ type = output; } else { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Invalid type: %s", argv[1]); return; } /* If call is active, use this_call values */ Call *this_call = &CallControl.calls[self->num]; if (this_call->status == cs_Active) { if (type == input) { device_mute(type, this_call->in_idx); self->chatwin->infobox.in_is_muted = device_is_muted(type, this_call->in_idx); } else { device_mute(type, this_call->out_idx); self->chatwin->infobox.out_is_muted = device_is_muted(type, this_call->out_idx); } } return; } void cmd_sense(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); UNUSED_VAR(m); if (argc != 1) { if (argc < 1) { print_err(self, "Must have value!"); } else { print_err(self, "Only two arguments allowed!"); } return; } char *end; float value = strtof(argv[1], &end); if (*end) { print_err(self, "Invalid input"); return; } const Call *call = &CallControl.calls[self->num]; /* Call must be active */ if (call->status == cs_Active) { device_set_VAD_threshold(call->in_idx, value); self->chatwin->infobox.vad_lvl = value; } return; } void cmd_bitrate(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); UNUSED_VAR(m); Call *call = &CallControl.calls[self->num]; if (call->status != cs_Active) { print_err(self, "Must be in a call"); return; } if (argc == 0) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Current audio encoding bitrate: %u", call->audio_bit_rate); return; } if (argc > 1) { print_err(self, "Too many arguments."); return; } char *end; const long int bit_rate = strtol(argv[1], &end, 10); if (*end || bit_rate < 0 || bit_rate > UINT32_MAX) { print_err(self, "Invalid input"); return; } Toxav_Err_Bit_Rate_Set error; toxav_audio_set_bit_rate(CallControl.av, self->num, bit_rate, &error); if (error != TOXAV_ERR_BIT_RATE_SET_OK) { if (error == TOXAV_ERR_BIT_RATE_SET_SYNC) { print_err(self, "Synchronization error occured"); } else if (error == TOXAV_ERR_BIT_RATE_SET_INVALID_BIT_RATE) { print_err(self, "Invalid audio bit rate value (valid is 6-510)"); } else if (error == TOXAV_ERR_BIT_RATE_SET_FRIEND_NOT_FOUND) { print_err(self, "Friend not found"); } else if (error == TOXAV_ERR_BIT_RATE_SET_FRIEND_NOT_IN_CALL) { print_err(self, "Friend is not in the call"); } else { print_err(self, "Unknown error"); } return; } call->audio_bit_rate = bit_rate; return; } void place_call(ToxWindow *self) { Call *call = &CallControl.calls[self->num]; if (call->status != cs_Pending) { return; } Toxav_Err_Call error; toxav_call(CallControl.av, self->num, call->audio_bit_rate, call->video_bit_rate, &error); if (error != TOXAV_ERR_CALL_OK) { if (error == TOXAV_ERR_CALL_FRIEND_ALREADY_IN_CALL) { print_err(self, "Already in a call!"); } else if (error == TOXAV_ERR_CALL_MALLOC) { print_err(self, "Memory allocation issue"); } else if (error == TOXAV_ERR_CALL_FRIEND_NOT_FOUND) { print_err(self, "Friend number invalid"); } else if (error == TOXAV_ERR_CALL_FRIEND_NOT_CONNECTED) { print_err(self, "Friend is valid but not currently connected"); } else { print_err(self, "Internal error!"); } cancel_call(call); return; } callback_recv_ringing(self->num); } void stop_current_call(ToxWindow *self) { Call *call = &CallControl.calls[self->num]; if (call->status == cs_Pending) { toxav_call_control(CallControl.av, self->num, TOXAV_CALL_CONTROL_CANCEL, NULL); cancel_call(call); callback_call_canceled(self->num); } else { #ifdef VIDEO callback_recv_video_end(self->num); callback_video_end(self->num); #endif /* VIDEO */ stop_transmission(call, self->num); callback_call_ended(self->num); } } /** * Reallocates the Calls list according to n. */ static void realloc_calls(uint32_t n) { if (n == 0) { free(CallControl.calls); CallControl.calls = NULL; return; } Call *temp = realloc(CallControl.calls, n * sizeof(Call)); if (temp == NULL) { exit_toxic_err("failed in realloc_calls", FATALERR_MEMORY); } CallControl.calls = temp; } /** * Inits the call structure for a given friend. Called when a friend is added to the friends list. * Index must be equivalent to the friend's friendlist index. */ void init_friend_AV(uint32_t index) { if (index == CallControl.max_calls) { realloc_calls(CallControl.max_calls + 1); CallControl.calls[CallControl.max_calls] = (Call) { 0 }; ++CallControl.max_calls; } } /** * Deletes a call structure from the Calls list. Called when a friend is deleted from the friends list. * Index must be equivalent to the size of the Calls list. */ void del_friend_AV(uint32_t index) { realloc_calls(index); CallControl.max_calls = index; } #endif /* AUDIO */ toxic-0.11.3/src/audio_call.h000066400000000000000000000055021416141666600157730ustar00rootroot00000000000000/* audio_call.h * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef AUDIO_CALL_H #define AUDIO_CALL_H #include #include "audio_device.h" typedef enum AudioError { ae_None = 0, ae_StartingCaptureDevice = 1 << 0, ae_StartingOutputDevice = 1 << 1, ae_StartingCoreAudio = 1 << 2 } AudioError; #ifdef VIDEO typedef enum VideoError { ve_None = 0, ve_StartingCaptureDevice = 1 << 0, ve_StartingOutputDevice = 1 << 1, ve_StartingCoreVideo = 1 << 2 } VideoError; #endif /* VIDEO */ /* Status transitions: * None -> Pending (call invitation made or received); * Pending -> None (invitation rejected or failed); * Pending -> Active (call starts); * Active -> None (call ends). */ typedef enum CallStatus { cs_None = 0, cs_Pending, cs_Active } CallStatus; typedef struct Call { CallStatus status; uint32_t state; /* ToxAV call state, valid when `status == cs_Active` */ uint32_t in_idx, out_idx; /* Audio device index, or -1 if not open */ uint32_t audio_bit_rate; /* Bit rate for sending audio */ uint32_t vin_idx, vout_idx; /* Video device index, or -1 if not open */ uint32_t video_width, video_height; uint32_t video_bit_rate; /* Bit rate for sending video; 0 for no video */ } Call; struct CallControl { AudioError audio_errors; #ifdef VIDEO VideoError video_errors; #endif /* VIDEO */ ToxAV *av; ToxWindow *prompt; Call *calls; uint32_t max_calls; bool audio_enabled; bool video_enabled; int32_t audio_frame_duration; uint32_t audio_sample_rate; uint8_t audio_channels; uint32_t default_audio_bit_rate; int32_t video_frame_duration; uint32_t default_video_width, default_video_height; uint32_t default_video_bit_rate; }; extern struct CallControl CallControl; /* You will have to pass pointer to first member of 'windows' declared in windows.c */ ToxAV *init_audio(ToxWindow *self, Tox *tox); void terminate_audio(void); bool init_call(Call *call); void place_call(ToxWindow *self); void stop_current_call(ToxWindow *self); void init_friend_AV(uint32_t index); void del_friend_AV(uint32_t index); #endif /* AUDIO_CALL_H */ toxic-0.11.3/src/audio_device.c000066400000000000000000000504251416141666600163160ustar00rootroot00000000000000/* audio_device.c * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #include "audio_device.h" #include "line_info.h" #include "misc_tools.h" #include "settings.h" #include #include /* compatibility with older versions of OpenAL */ #ifndef ALC_ALL_DEVICES_SPECIFIER #include #endif /* ALC_ALL_DEVICES_SPECIFIER */ #include #include #include #include #include #include #include extern struct user_settings *user_settings; extern struct Winthread Winthread; typedef struct FrameInfo { uint32_t samples_per_frame; uint32_t sample_rate; bool stereo; } FrameInfo; /* A virtual input/output device, abstracting the currently selected openal * device (which may change during the lifetime of the virtual device). * We refer to a virtual device as a "device", and refer to an underlying * openal device as an "al_device". * Multiple virtual devices may be open at once; the callback of each virtual * input device has data captured from the input al_device passed to it, and * each virtual output device acts as a source for the output al_device. */ typedef struct Device { bool active; bool muted; FrameInfo frame_info; // used only by input devices: DataHandleCallback cb; void *cb_data; float VAD_threshold; uint32_t VAD_samples_remaining; // used only by output devices: uint32_t source; uint32_t buffers[OPENAL_BUFS]; bool source_open; } Device; typedef struct AudioState { ALCdevice *al_device[2]; Device devices[2][MAX_DEVICES]; uint32_t num_devices[2]; FrameInfo capture_frame_info; float input_volume; // mutexes to prevent changes to input resp. output devices and al_devices // during poll_input iterations resp. calls to write_out; // mutex[input] also used to lock input_volume which poll_input writes to. pthread_mutex_t mutex[2]; // TODO: unused const char *default_al_device_name[2]; /* Default devices */ const char *al_device_names[2][MAX_OPENAL_DEVICES]; /* Available devices */ uint32_t num_al_devices[2]; char *current_al_device_name[2]; } AudioState; static AudioState *audio_state; static void lock(DeviceType type) { pthread_mutex_lock(&audio_state->mutex[type]); } static void unlock(DeviceType type) { pthread_mutex_unlock(&audio_state->mutex[type]); } static bool thread_running = true, thread_paused = true; /* Thread control */ #ifdef AUDIO static void *poll_input(void *); #endif static uint32_t sound_mode(bool stereo) { return stereo ? AL_FORMAT_STEREO16 : AL_FORMAT_MONO16; } static uint32_t sample_size(bool stereo) { return stereo ? 4 : 2; } DeviceError init_devices(void) { audio_state = calloc(1, sizeof(AudioState)); if (audio_state == NULL) { return de_InternalError; } get_al_device_names(); for (DeviceType type = input; type <= output; ++type) { audio_state->al_device[type] = NULL; if (pthread_mutex_init(&audio_state->mutex[type], NULL) != 0) { return de_InternalError; } } #ifdef AUDIO // Start poll thread pthread_t thread_id; if (pthread_create(&thread_id, NULL, poll_input, NULL) != 0 || pthread_detach(thread_id) != 0) { return de_InternalError; } #endif return de_None; } DeviceError terminate_devices(void) { lock(input); thread_running = false; unlock(input); sleep_thread(20000L); for (DeviceType type = input; type <= output; ++type) { if (pthread_mutex_destroy(&audio_state->mutex[type]) != 0) { return de_InternalError; } if (audio_state->current_al_device_name[type] != NULL) { free(audio_state->current_al_device_name[type]); } } free(audio_state); return de_None; } void get_al_device_names(void) { const char *stringed_device_list; for (DeviceType type = input; type <= output; ++type) { audio_state->num_al_devices[type] = 0; if (type == input) { stringed_device_list = alcGetString(NULL, ALC_CAPTURE_DEVICE_SPECIFIER); } else { if (alcIsExtensionPresent(NULL, "ALC_ENUMERATE_ALL_EXT") != AL_FALSE) { stringed_device_list = alcGetString(NULL, ALC_ALL_DEVICES_SPECIFIER); } else { stringed_device_list = alcGetString(NULL, ALC_DEVICE_SPECIFIER); } } if (stringed_device_list != NULL) { audio_state->default_al_device_name[type] = alcGetString(NULL, type == input ? ALC_CAPTURE_DEFAULT_DEVICE_SPECIFIER : ALC_DEFAULT_DEVICE_SPECIFIER); for (; *stringed_device_list != '\0' && audio_state->num_al_devices[type] < MAX_OPENAL_DEVICES; ++audio_state->num_al_devices[type]) { audio_state->al_device_names[type][audio_state->num_al_devices[type]] = stringed_device_list; stringed_device_list += strlen(stringed_device_list) + 1; } } } } DeviceError device_mute(DeviceType type, uint32_t device_idx) { if (device_idx >= MAX_DEVICES) { return de_InvalidSelection; } Device *device = &audio_state->devices[type][device_idx]; if (!device->active) { return de_DeviceNotActive; } lock(type); device->muted = !device->muted; unlock(type); return de_None; } bool device_is_muted(DeviceType type, uint32_t device_idx) { if (device_idx >= MAX_DEVICES) { return false; } Device *device = &audio_state->devices[type][device_idx]; if (!device->active) { return false; } return device->muted; } DeviceError device_set_VAD_threshold(uint32_t device_idx, float value) { if (device_idx >= MAX_DEVICES) { return de_InvalidSelection; } Device *device = &audio_state->devices[input][device_idx]; if (!device->active) { return de_DeviceNotActive; } if (value <= 0.0f) { value = 0.0f; } lock(input); device->VAD_threshold = value; unlock(input); return de_None; } float device_get_VAD_threshold(uint32_t device_idx) { if (device_idx >= MAX_DEVICES) { return 0.0; } Device *device = &audio_state->devices[input][device_idx]; if (!device->active) { return 0.0; } return device->VAD_threshold; } DeviceError set_source_position(uint32_t device_idx, float x, float y, float z) { if (device_idx >= MAX_DEVICES) { return de_InvalidSelection; } Device *device = &audio_state->devices[output][device_idx]; if (!device->active) { return de_DeviceNotActive; } lock(output); alSource3f(device->source, AL_POSITION, x, y, z); unlock(output); if (!audio_state->al_device[output] || alcGetError(audio_state->al_device[output]) != AL_NO_ERROR) { return de_AlError; } return de_None; } static DeviceError close_al_device(DeviceType type) { if (audio_state->al_device[type] == NULL) { return de_None; } if (type == input) { if (!alcCaptureCloseDevice(audio_state->al_device[type])) { return de_AlError; } thread_paused = true; } else { ALCcontext *context = alcGetCurrentContext(); alcMakeContextCurrent(NULL); alcDestroyContext(context); if (!alcCloseDevice(audio_state->al_device[type])) { return de_AlError; } } audio_state->al_device[type] = NULL; return de_None; } static DeviceError open_al_device(DeviceType type, FrameInfo frame_info) { audio_state->al_device[type] = type == input ? alcCaptureOpenDevice(audio_state->current_al_device_name[type], frame_info.sample_rate, sound_mode(frame_info.stereo), frame_info.samples_per_frame * 2) : alcOpenDevice(audio_state->current_al_device_name[type]); if (audio_state->al_device[type] == NULL) { return de_FailedStart; } if (type == input) { alcCaptureStart(audio_state->al_device[type]); thread_paused = false; audio_state->capture_frame_info = frame_info; } else { alcMakeContextCurrent(alcCreateContext(audio_state->al_device[type], NULL)); } if (alcGetError(audio_state->al_device[type]) != AL_NO_ERROR) { close_al_device(type); return de_AlError; } return de_None; } static void close_source(Device *device) { if (device->source_open) { alDeleteSources(1, &device->source); alDeleteBuffers(OPENAL_BUFS, device->buffers); device->source_open = false; } } static DeviceError open_source(Device *device) { alGenBuffers(OPENAL_BUFS, device->buffers); if (alcGetError(audio_state->al_device[output]) != AL_NO_ERROR) { return de_FailedStart; } alGenSources((uint32_t)1, &device->source); if (alcGetError(audio_state->al_device[output]) != AL_NO_ERROR) { alDeleteBuffers(OPENAL_BUFS, device->buffers); return de_FailedStart; } device->source_open = true; alSourcei(device->source, AL_LOOPING, AL_FALSE); const uint32_t frame_size = device->frame_info.samples_per_frame * sample_size(device->frame_info.stereo); size_t zeros_size = frame_size * sizeof(uint16_t); uint16_t *zeros = calloc(1, zeros_size); if (zeros == NULL) { close_source(device); return de_FailedStart; } for (int i = 0; i < OPENAL_BUFS; ++i) { alBufferData(device->buffers[i], sound_mode(device->frame_info.stereo), zeros, zeros_size, device->frame_info.sample_rate); } free(zeros); alSourceQueueBuffers(device->source, OPENAL_BUFS, device->buffers); alSourcePlay(device->source); if (alcGetError(audio_state->al_device[output]) != AL_NO_ERROR) { close_source(device); return de_FailedStart; } return de_None; } DeviceError set_al_device(DeviceType type, int32_t selection) { if (audio_state->num_al_devices[type] <= selection || selection < 0) { return de_InvalidSelection; } const char *name = audio_state->al_device_names[type][selection]; char **cur_name = &audio_state->current_al_device_name[type]; if (*cur_name != NULL) { free(*cur_name); } *cur_name = malloc(strlen(name) + 1); if (*cur_name == NULL) { return de_InternalError; } strcpy(*cur_name, name); if (audio_state->num_devices[type] > 0) { // close any existing al_device and try to open new one, reopening existing sources lock(type); if (type == output) { for (int i = 0; i < MAX_DEVICES; i++) { Device *device = &audio_state->devices[type][i]; if (device->active) { close_source(device); } } } close_al_device(type); DeviceError err = open_al_device(type, audio_state->capture_frame_info); if (err != de_None) { unlock(type); return err; } if (type == output) { for (int i = 0; i < MAX_DEVICES; i++) { Device *device = &audio_state->devices[type][i]; if (device->active) { open_source(device); } } } unlock(type); } return de_None; } static DeviceError open_device(DeviceType type, uint32_t *device_idx, DataHandleCallback cb, void *cb_data, uint32_t sample_rate, uint32_t frame_duration, uint8_t channels) { if (channels != 1 && channels != 2) { return de_UnsupportedMode; } const uint32_t samples_per_frame = (sample_rate * frame_duration / 1000); FrameInfo frame_info = {samples_per_frame, sample_rate, channels == 2}; uint32_t i; for (i = 0; i < MAX_DEVICES && audio_state->devices[type][i].active; ++i); if (i == MAX_DEVICES) { return de_AllDevicesBusy; } *device_idx = i; lock(type); if (audio_state->al_device[type] == NULL) { DeviceError err = open_al_device(type, frame_info); if (err != de_None) { unlock(type); return err; } } else if (type == input) { // Use previously set frame info on existing capture device frame_info = audio_state->capture_frame_info; } Device *device = &audio_state->devices[type][i]; device->active = true; ++audio_state->num_devices[type]; device->muted = false; device->frame_info = frame_info; if (type == input) { device->cb = cb; device->cb_data = cb_data; #ifdef AUDIO if (user_settings->VAD_threshold >= 0.0) { device->VAD_threshold = user_settings->VAD_threshold; } #else device->VAD_threshold = 0.0f; #endif } else { if (open_source(device) != de_None) { device->active = false; --audio_state->num_devices[type]; unlock(type); return de_FailedStart; } } unlock(type); return de_None; } DeviceError open_input_device(uint32_t *device_idx, DataHandleCallback cb, void *cb_data, uint32_t sample_rate, uint32_t frame_duration, uint8_t channels) { return open_device(input, device_idx, cb, cb_data, sample_rate, frame_duration, channels); } DeviceError open_output_device(uint32_t *device_idx, uint32_t sample_rate, uint32_t frame_duration, uint8_t channels) { return open_device(output, device_idx, 0, 0, sample_rate, frame_duration, channels); } DeviceError close_device(DeviceType type, uint32_t device_idx) { if (device_idx >= MAX_DEVICES) { return de_InvalidSelection; } lock(type); Device *device = &audio_state->devices[type][device_idx]; if (!device->active) { unlock(type); return de_DeviceNotActive; } if (type == output) { close_source(device); } device->active = false; --audio_state->num_devices[type]; DeviceError err = de_None; if (audio_state->num_devices[type] == 0) { err = close_al_device(type); } unlock(type); return err; } DeviceError write_out(uint32_t device_idx, const int16_t *data, uint32_t sample_count, uint8_t channels, uint32_t sample_rate) { if (device_idx >= MAX_DEVICES) { return de_InvalidSelection; } lock(output); Device *device = &audio_state->devices[output][device_idx]; if (!device->active || device->muted) { unlock(output); return de_DeviceNotActive; } ALuint bufid; ALint processed, queued; alGetSourcei(device->source, AL_BUFFERS_PROCESSED, &processed); alGetSourcei(device->source, AL_BUFFERS_QUEUED, &queued); if (audio_state->al_device[output] == NULL || alcGetError(audio_state->al_device[output]) != AL_NO_ERROR) { unlock(output); return de_AlError; } if (processed) { ALuint *bufids = malloc(processed * sizeof(ALuint)); if (bufids == NULL) { unlock(output); return de_InternalError; } alSourceUnqueueBuffers(device->source, processed, bufids); alDeleteBuffers(processed - 1, bufids + 1); bufid = bufids[0]; free(bufids); } else if (queued < 16) { alGenBuffers(1, &bufid); } else { unlock(output); return de_Busy; } const bool stereo = channels == 2; alBufferData(bufid, sound_mode(stereo), data, sample_count * sample_size(stereo), sample_rate); alSourceQueueBuffers(device->source, 1, &bufid); ALint state; alGetSourcei(device->source, AL_SOURCE_STATE, &state); if (state != AL_PLAYING) { alSourcePlay(device->source); } unlock(output); return de_None; } #ifdef AUDIO /* Adapted from qtox, * Copyright © 2014-2019 by The qTox Project Contributors * * return normalized volume of buffer in range 0.0-100.0 */ float volume(int16_t *frame, uint32_t samples) { float sum_of_squares = 0; for (uint32_t i = 0; i < samples; i++) { const float sample = (float)(frame[i]) / INT16_MAX; sum_of_squares += powf(sample, 2); } const float root_mean_square = sqrtf(sum_of_squares / samples); const float root_two = 1.414213562; // normalizedVolume == 1.0 corresponds to a sine wave of maximal amplitude const float normalized_volume = root_mean_square * root_two; return 100.0f * fminf(1.0f, normalized_volume); } // Time in ms for which we continue to capture audio after VAD is triggered: #define VAD_TIME 250 #define FRAME_BUF_SIZE 16000 static void *poll_input(void *arg) { UNUSED_VAR(arg); int16_t *frame_buf = malloc(FRAME_BUF_SIZE * sizeof(int16_t)); if (frame_buf == NULL) { exit_toxic_err("failed in thread_poll", FATALERR_MEMORY); } while (1) { lock(input); if (!thread_running) { free(frame_buf); unlock(input); break; } if (thread_paused) { unlock(input); sleep_thread(10000L); continue; } if (audio_state->al_device[input] != NULL) { int32_t available_samples; alcGetIntegerv(audio_state->al_device[input], ALC_CAPTURE_SAMPLES, sizeof(int32_t), &available_samples); const uint32_t f_size = audio_state->capture_frame_info.samples_per_frame; if (available_samples >= f_size && f_size <= FRAME_BUF_SIZE) { alcCaptureSamples(audio_state->al_device[input], frame_buf, f_size); unlock(input); pthread_mutex_lock(&Winthread.lock); lock(input); float frame_volume = volume(frame_buf, f_size); audio_state->input_volume = frame_volume; for (int i = 0; i < MAX_DEVICES; i++) { Device *device = &audio_state->devices[input][i]; if (device->VAD_threshold != 0.0f) { if (frame_volume >= device->VAD_threshold) { device->VAD_samples_remaining = VAD_TIME * (audio_state->capture_frame_info.sample_rate / 1000); } else if (device->VAD_samples_remaining < f_size) { continue; } else { device->VAD_samples_remaining -= f_size; } } if (device->active && !device->muted && device->cb) { device->cb(frame_buf, f_size, device->cb_data); } } pthread_mutex_unlock(&Winthread.lock); } } unlock(input); sleep_thread(5000L); } pthread_exit(NULL); } #endif float get_input_volume(void) { float ret = 0.0f; if (audio_state->al_device[input] != NULL) { lock(input); ret = audio_state->input_volume; unlock(input); } return ret; } void print_al_devices(ToxWindow *self, DeviceType type) { for (int i = 0; i < audio_state->num_al_devices[type]; ++i) { line_info_add(self, false, NULL, NULL, SYS_MSG, audio_state->current_al_device_name[type] && strcmp(audio_state->current_al_device_name[type], audio_state->al_device_names[type][i]) == 0 ? 1 : 0, 0, "%d: %s", i, audio_state->al_device_names[type][i]); } return; } DeviceError selection_valid(DeviceType type, int32_t selection) { return (audio_state->num_al_devices[type] <= selection || selection < 0) ? de_InvalidSelection : de_None; } toxic-0.11.3/src/audio_device.h000066400000000000000000000055111416141666600163170ustar00rootroot00000000000000/* audio_device.h * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ /* * You can have multiple sources (Input devices) but only one output device. * Pass buffers to output device via write(); * Read from running input device(s) via select()/callback combo. */ #ifndef AUDIO_DEVICE_H #define AUDIO_DEVICE_H #define OPENAL_BUFS 5 #define MAX_OPENAL_DEVICES 32 #define MAX_DEVICES 32 #include "windows.h" typedef enum DeviceType { input, output, } DeviceType; typedef enum DeviceError { de_None, de_InternalError = -1, de_InvalidSelection = -2, de_FailedStart = -3, de_Busy = -4, de_AllDevicesBusy = -5, de_DeviceNotActive = -6, de_BufferError = -7, de_UnsupportedMode = -8, de_AlError = -9, } DeviceError; typedef void (*DataHandleCallback)(const int16_t *, uint32_t size, void *data); DeviceError init_devices(void); void get_al_device_names(void); DeviceError terminate_devices(void); /* toggle device mute */ DeviceError device_mute(DeviceType type, uint32_t device_idx); bool device_is_muted(DeviceType type, uint32_t device_idx); DeviceError device_set_VAD_threshold(uint32_t device_idx, float value); float device_get_VAD_threshold(uint32_t device_idx); DeviceError set_source_position(uint32_t device_idx, float x, float y, float z); DeviceError set_al_device(DeviceType type, int32_t selection); /* Start device */ DeviceError open_input_device(uint32_t *device_idx, DataHandleCallback cb, void *cb_data, uint32_t sample_rate, uint32_t frame_duration, uint8_t channels); DeviceError open_output_device(uint32_t *device_idx, uint32_t sample_rate, uint32_t frame_duration, uint8_t channels); /* Stop device */ DeviceError close_device(DeviceType type, uint32_t device_idx); /* Write data to output device */ DeviceError write_out(uint32_t device_idx, const int16_t *data, uint32_t length, uint8_t channels, uint32_t sample_rate); /* return current input volume as float in range 0.0-100.0 */ float get_input_volume(void); void print_al_devices(ToxWindow *self, DeviceType type); DeviceError selection_valid(DeviceType type, int32_t selection); #endif /* AUDIO_DEVICE_H */ toxic-0.11.3/src/autocomplete.c000066400000000000000000000246541416141666600164040ustar00rootroot00000000000000/* autocomplete.c * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #include #include #include #ifdef __APPLE__ #include #include #else #include #endif /* __APPLE__ */ #include "configdir.h" #include "execute.h" #include "line_info.h" #include "misc_tools.h" #include "toxic.h" #include "windows.h" static void print_ac_matches(ToxWindow *self, Tox *m, char **list, size_t n_matches) { if (m) { execute(self->chatwin->history, self, m, "/clear", GLOBAL_COMMAND_MODE); } for (size_t i = 0; i < n_matches; ++i) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "%s", list[i]); } line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, ""); } /* puts match in match buffer. if more than one match, add first n chars that are identical. * e.g. if matches contains: [foo, foobar, foe] we put fo in match. * * Returns the length of the match. */ static size_t get_str_match(ToxWindow *self, char *match, size_t match_sz, const char *const *matches, size_t n_items, size_t max_size) { UNUSED_VAR(self); if (n_items == 1) { return snprintf(match, match_sz, "%s", matches[0]); } for (size_t i = 0; i < max_size; ++i) { char ch1 = matches[0][i]; for (size_t j = 0; j < n_items; ++j) { char ch2 = matches[j][i]; if (ch1 != ch2 || !ch1) { snprintf(match, match_sz, "%s", matches[0]); match[i] = '\0'; return i; } } } return snprintf(match, match_sz, "%s", matches[0]); } /* * Looks for all instances in list that begin with the last entered word in line according to pos, * then fills line with the complete word. e.g. "Hello jo" would complete the line * with "Hello john". If multiple matches, prints out all the matches and semi-completes line. * * `list` is a pointer to `n_items` strings. Each string in the list must be <= MAX_STR_SIZE. * * dir_search should be true if the line being completed is a file path. * * Returns the difference between the old len and new len of line on success. * Returns -1 on error. * * Note: This function should not be called directly. Use complete_line() and complete_path() instead. */ static int complete_line_helper(ToxWindow *self, const char *const *list, const size_t n_items, bool dir_search) { ChatContext *ctx = self->chatwin; if (ctx->pos <= 0 || ctx->len <= 0 || ctx->pos > ctx->len) { return -1; } if (ctx->len >= MAX_STR_SIZE) { return -1; } const char *endchrs = " "; char ubuf[MAX_STR_SIZE]; /* work with multibyte string copy of buf for simplicity */ if (wcs_to_mbs_buf(ubuf, ctx->line, sizeof(ubuf)) == -1) { return -1; } /* isolate substring from space behind pos to pos */ char tmp[MAX_STR_SIZE]; memcpy(tmp, ubuf, ctx->pos); tmp[ctx->pos] = 0; const char *s = dir_search ? strchr(tmp, ' ') : strrchr(tmp, ' '); char *sub = calloc(1, strlen(ubuf) + 1); if (sub == NULL) { exit_toxic_err("failed in complete_line_helper", FATALERR_MEMORY); } if (!s && !dir_search) { strcpy(sub, tmp); if (sub[0] != '/') { endchrs = ": "; } } else if (s) { strcpy(sub, &s[1]); if (dir_search) { int sub_len = strlen(sub); int si = char_rfind(sub, '/', sub_len); if (si || *sub == '/') { memmove(sub, &sub[si + 1], sub_len - si); } } } if (!sub[0] && !(dir_search && n_items == 1)) { free(sub); return 0; } int s_len = strlen(sub); size_t n_matches = 0; char **matches = (char **) malloc_ptr_array(n_items, MAX_STR_SIZE); if (matches == NULL) { free(sub); return -1; } /* put all list matches in matches array */ for (size_t i = 0; i < n_items; ++i) { if (strncmp(list[i], sub, s_len) == 0) { snprintf(matches[n_matches++], MAX_STR_SIZE, "%s", list[i]); } } free(sub); if (!n_matches) { free_ptr_array((void **) matches); return -1; } if (!dir_search && n_matches > 1) { print_ac_matches(self, NULL, matches, n_matches); } char match[MAX_STR_SIZE]; size_t match_len = get_str_match(self, match, sizeof(match), (const char *const *) matches, n_matches, MAX_STR_SIZE); free_ptr_array((void **) matches); if (match_len == 0) { return 0; } if (dir_search) { if (n_matches == 1) { endchrs = char_rfind(match, '.', match_len) ? "" : "/"; } else { endchrs = ""; } } else if (n_matches > 1) { endchrs = ""; } /* put match in correct spot in buf and append endchars */ int n_endchrs = strlen(endchrs); int strt = ctx->pos - s_len; int diff = match_len - s_len + n_endchrs; if (ctx->len + diff >= MAX_STR_SIZE) { return -1; } char tmpend[MAX_STR_SIZE]; snprintf(tmpend, sizeof(tmpend), "%s", &ubuf[ctx->pos]); if (match_len + n_endchrs + strlen(tmpend) >= sizeof(ubuf)) { return -1; } strcpy(&ubuf[strt], match); /* If path points to a file with no extension don't append a forward slash */ if (dir_search && *endchrs == '/') { const char *path_start = strchr(ubuf + 1, '/'); if (!path_start) { path_start = strchr(ubuf + 1, ' '); if (!path_start) { return -1; } } if (strlen(path_start) < 2) { return -1; } ++path_start; if (file_type(path_start) == FILE_TYPE_REGULAR) { endchrs = ""; diff -= n_endchrs; } } strcpy(&ubuf[strt + match_len], endchrs); strcpy(&ubuf[strt + match_len + n_endchrs], tmpend); /* convert to widechar and copy back to original buf */ wchar_t newbuf[MAX_STR_SIZE]; if (mbs_to_wcs_buf(newbuf, ubuf, sizeof(newbuf) / sizeof(wchar_t)) == -1) { return -1; } wcscpy(ctx->line, newbuf); ctx->len += diff; ctx->pos += diff; return diff; } int complete_line(ToxWindow *self, const char *const *list, size_t n_items) { return complete_line_helper(self, list, n_items, false); } static int complete_path(ToxWindow *self, const char *const *list, const size_t n_items) { return complete_line_helper(self, list, n_items, true); } /* Transforms a tab complete starting with the shorthand "~" into the full home directory. */ static void complete_home_dir(ToxWindow *self, char *path, int pathsize, const char *cmd, int cmdlen) { ChatContext *ctx = self->chatwin; char homedir[MAX_STR_SIZE] = {0}; get_home_dir(homedir, sizeof(homedir)); char newline[MAX_STR_SIZE + 1]; snprintf(newline, sizeof(newline), "%s %s%s", cmd, homedir, path + 1); snprintf(path, pathsize, "%s", &newline[cmdlen - 1]); wchar_t wline[MAX_STR_SIZE]; if (mbs_to_wcs_buf(wline, newline, sizeof(wline) / sizeof(wchar_t)) == -1) { return; } int newlen = wcslen(wline); if (ctx->len + newlen >= MAX_STR_SIZE) { return; } wmemcpy(ctx->line, wline, newlen + 1); ctx->pos = newlen; ctx->len = ctx->pos; } /* * Return true if the first `p_len` chars in `s` are equal to `p` and `s` is a valid directory name. */ static bool is_partial_match(const char *s, const char *p, size_t p_len) { if (s == NULL || p == NULL) { return false; } return strncmp(s, p, p_len) == 0 && strcmp(".", s) != 0 && strcmp("..", s) != 0; } /* Attempts to match /command "" line to matching directories. * If there is only one match the line is auto-completed. * * Returns the diff between old len and new len of ctx->line on success. * Returns -1 if no matches or more than one match. */ #define MAX_DIRS 75 int dir_match(ToxWindow *self, Tox *m, const wchar_t *line, const wchar_t *cmd) { char b_path[MAX_STR_SIZE + 1]; char b_name[MAX_STR_SIZE + 1]; char b_cmd[MAX_STR_SIZE]; const wchar_t *tmpline = &line[wcslen(cmd) + 1]; /* start after "/command " */ if (wcs_to_mbs_buf(b_path, tmpline, sizeof(b_path) - 1) == -1) { return -1; } if (wcs_to_mbs_buf(b_cmd, cmd, sizeof(b_cmd)) == -1) { return -1; } if (b_path[0] == '~') { complete_home_dir(self, b_path, sizeof(b_path) - 1, b_cmd, strlen(b_cmd) + 2); } int si = char_rfind(b_path, '/', strlen(b_path)); if (!b_path[0]) { /* list everything in pwd */ b_path[0] = '.'; b_path[1] = '\0'; } else if (!si && b_path[0] != '/') { /* look for matches in pwd */ memmove(b_path + 1, b_path, sizeof(b_path) - 1); b_path[0] = '.'; } snprintf(b_name, sizeof(b_name), "%s", &b_path[si + 1]); b_path[si + 1] = '\0'; size_t b_name_len = strlen(b_name); DIR *dp = opendir(b_path); if (dp == NULL) { return -1; } char **dirnames = (char **) malloc_ptr_array(MAX_DIRS, NAME_MAX + 1); if (dirnames == NULL) { closedir(dp); return -1; } struct dirent *entry; int dircount = 0; while ((entry = readdir(dp)) && dircount < MAX_DIRS) { if (is_partial_match(entry->d_name, b_name, b_name_len)) { snprintf(dirnames[dircount], NAME_MAX + 1, "%s", entry->d_name); ++dircount; } } closedir(dp); if (dircount == 0) { free_ptr_array((void **) dirnames); return -1; } if (dircount > 1) { qsort(dirnames, dircount, sizeof(char *), qsort_ptr_char_array_helper); print_ac_matches(self, m, dirnames, dircount); } int ret = complete_path(self, (const char *const *) dirnames, dircount); free_ptr_array((void **) dirnames); return ret; } toxic-0.11.3/src/autocomplete.h000066400000000000000000000035461416141666600164060ustar00rootroot00000000000000/* autocomplete.h * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef AUTOCOMPLETE_H #define AUTOCOMPLETE_H #include "windows.h" /* * Looks for all instances in list that begin with the last entered word in line according to pos, * then fills line with the complete word. e.g. "Hello jo" would complete the line * with "Hello john". If multiple matches, prints out all the matches and semi-completes line. * * `list` is a pointer to `n_items` strings. * * dir_search should be true if the line being completed is a file path. * * Returns the difference between the old len and new len of line on success. * Returns -1 on error. * * Note: This function should not be called directly. Use complete_line() and complete_path() instead. */ int complete_line(ToxWindow *self, const char **list, size_t n_items); /* Attempts to match /command "" line to matching directories. * If there is only one match the line is auto-completed. * * Returns the diff between old len and new len of ctx->line on success. * Returns -1 if no matches or more than one match. */ int dir_match(ToxWindow *self, Tox *m, const wchar_t *line, const wchar_t *cmd); #endif /* AUTOCOMPLETE_H */ toxic-0.11.3/src/avatars.c000066400000000000000000000144431416141666600153370ustar00rootroot00000000000000/* avatars.c * * * Copyright (C) 2015 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #include #include #include #include "avatars.h" #include "file_transfers.h" #include "friendlist.h" #include "misc_tools.h" extern FriendsList Friends; static struct Avatar { char name[TOX_MAX_FILENAME_LENGTH + 1]; size_t name_len; char path[PATH_MAX + 1]; size_t path_len; off_t size; } Avatar; /* Compares the first size bytes of fp to signature. * * Returns 0 if they are the same * Returns 1 if they differ * Returns -1 on error. * * On success this function will seek back to the beginning of fp. */ static int check_file_signature(const unsigned char *signature, size_t size, FILE *fp) { char *buf = malloc(size); if (buf == NULL) { return -1; } if (fread(buf, size, 1, fp) != 1) { free(buf); return -1; } int ret = memcmp(signature, buf, size); free(buf); if (fseek(fp, 0L, SEEK_SET) == -1) { return -1; } return ret == 0 ? 0 : 1; } static void avatar_clear(void) { Avatar = (struct Avatar) { 0 }; } /* Sends avatar to friendnumber. * * Returns 0 on success. * Returns -1 on failure. */ int avatar_send(Tox *m, uint32_t friendnumber) { Tox_Err_File_Send err; uint32_t filenumber = tox_file_send(m, friendnumber, TOX_FILE_KIND_AVATAR, (size_t) Avatar.size, NULL, (uint8_t *) Avatar.name, Avatar.name_len, &err); if (Avatar.size == 0) { return 0; } if (err != TOX_ERR_FILE_SEND_OK) { fprintf(stderr, "tox_file_send failed for friendnumber %u (error %d)\n", friendnumber, err); return -1; } struct FileTransfer *ft = new_file_transfer(NULL, friendnumber, filenumber, FILE_TRANSFER_SEND, TOX_FILE_KIND_AVATAR); if (!ft) { return -1; } ft->file = fopen(Avatar.path, "r"); if (ft->file == NULL) { return -1; } snprintf(ft->file_name, sizeof(ft->file_name), "%s", Avatar.name); ft->file_size = Avatar.size; return 0; } /* Sends avatar to all friends */ static void avatar_send_all(Tox *m) { for (size_t i = 0; i < Friends.max_idx; ++i) { if (Friends.list[i].connection_status != TOX_CONNECTION_NONE) { avatar_send(m, Friends.list[i].num); } } } /* Sets avatar to path and sends it to all online contacts. * * Returns 0 on success. * Returns -1 on failure. */ int avatar_set(Tox *m, const char *path, size_t path_len) { if (path_len == 0 || path_len >= sizeof(Avatar.path)) { return -1; } FILE *fp = fopen(path, "rb"); if (fp == NULL) { return -1; } unsigned char PNG_signature[8] = {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}; if (check_file_signature(PNG_signature, sizeof(PNG_signature), fp) != 0) { fclose(fp); return -1; } fclose(fp); off_t size = file_size(path); if (size == 0 || size > MAX_AVATAR_FILE_SIZE) { return -1; } get_file_name(Avatar.name, sizeof(Avatar.name), path); Avatar.name_len = strlen(Avatar.name); snprintf(Avatar.path, sizeof(Avatar.path), "%s", path); Avatar.path_len = path_len; Avatar.size = size; avatar_send_all(m); return 0; } /* Unsets avatar and sends to all online contacts. * * Returns 0 on success. * Returns -1 on failure. */ void avatar_unset(Tox *m) { avatar_clear(); avatar_send_all(m); } void on_avatar_friend_connection_status(Tox *m, uint32_t friendnumber, Tox_Connection connection_status) { if (connection_status == TOX_CONNECTION_NONE) { kill_avatar_file_transfers_friend(m, friendnumber); } } void on_avatar_file_control(Tox *m, struct FileTransfer *ft, Tox_File_Control control) { switch (control) { case TOX_FILE_CONTROL_RESUME: if (ft->state == FILE_TRANSFER_PENDING) { ft->state = FILE_TRANSFER_STARTED; } else if (ft->state == FILE_TRANSFER_PAUSED) { ft->state = FILE_TRANSFER_STARTED; } break; case TOX_FILE_CONTROL_PAUSE: ft->state = FILE_TRANSFER_PAUSED; break; case TOX_FILE_CONTROL_CANCEL: close_file_transfer(NULL, m, ft, -1, NULL, silent); break; } } void on_avatar_chunk_request(Tox *m, struct FileTransfer *ft, uint64_t position, size_t length) { if (ft->state != FILE_TRANSFER_STARTED) { return; } if (length == 0) { close_file_transfer(NULL, m, ft, -1, NULL, silent); return; } if (ft->file == NULL) { close_file_transfer(NULL, m, ft, TOX_FILE_CONTROL_CANCEL, NULL, silent); return; } if (ft->position != position) { if (fseek(ft->file, position, SEEK_SET) == -1) { close_file_transfer(NULL, m, ft, TOX_FILE_CONTROL_CANCEL, NULL, silent); return; } ft->position = position; } uint8_t *send_data = malloc(length); if (send_data == NULL) { close_file_transfer(NULL, m, ft, TOX_FILE_CONTROL_CANCEL, NULL, silent); return; } size_t send_length = fread(send_data, 1, length, ft->file); if (send_length != length) { close_file_transfer(NULL, m, ft, TOX_FILE_CONTROL_CANCEL, NULL, silent); free(send_data); return; } Tox_Err_File_Send_Chunk err; tox_file_send_chunk(m, ft->friendnumber, ft->filenumber, position, send_data, send_length, &err); free(send_data); if (err != TOX_ERR_FILE_SEND_CHUNK_OK) { fprintf(stderr, "tox_file_send_chunk failed in avatar callback (error %d)\n", err); } ft->position += send_length; } toxic-0.11.3/src/avatars.h000066400000000000000000000031171416141666600153400ustar00rootroot00000000000000/* avatars.h * * * Copyright (C) 2015 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef AVATARS_H #define AVATARS_H #include "file_transfers.h" #define MAX_AVATAR_FILE_SIZE 65536 /* Sends avatar to friendnum. * * Returns 0 on success. * Returns -1 on failure. */ int avatar_send(Tox *m, uint32_t friendnum); /* Sets avatar to path and sends it to all online contacts. * * Returns 0 on success. * Returns -1 on failure. */ int avatar_set(Tox *m, const char *path, size_t length); /* Unsets avatar and sends to all online contacts. * * Returns 0 on success. * Returns -1 on failure. */ void avatar_unset(Tox *m); void on_avatar_chunk_request(Tox *m, struct FileTransfer *ft, uint64_t position, size_t length); void on_avatar_file_control(Tox *m, struct FileTransfer *ft, Tox_File_Control control); void on_avatar_friend_connection_status(Tox *m, uint32_t friendnumber, Tox_Connection connection_status); #endif /* AVATARS_H */ toxic-0.11.3/src/bootstrap.c000066400000000000000000000416541416141666600157170ustar00rootroot00000000000000/* bootstrap.c * * * Copyright (C) 2016 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #include #include #include #include #include #include #include #include #include #include "configdir.h" #include "curl_util.h" #include "line_info.h" #include "misc_tools.h" #include "prompt.h" #include "settings.h" #include "windows.h" extern struct user_settings *user_settings; /* URL that we get the JSON encoded nodes list from. */ #define NODES_LIST_URL "https://nodes.tox.chat/json" #define DEFAULT_NODES_FILENAME "DHTnodes.json" /* Time to wait between bootstrap attempts */ #define TRY_BOOTSTRAP_INTERVAL 5 /* Number of nodes to bootstrap to per try */ #define NUM_BOOTSTRAP_NODES 5 /* Number of seconds since last successful ping before we consider a node offline */ #define NODE_OFFLINE_TIMOUT (60*60*24*2) #define IP_MAX_SIZE 45 #define IP_MIN_SIZE 7 #define LAST_SCAN_JSON_KEY "\"last_scan\":" #define LAST_SCAN_JSON_KEY_LEN (sizeof(LAST_SCAN_JSON_KEY) - 1) #define IPV4_JSON_KEY "\"ipv4\":\"" #define IPV4_JSON_KEY_LEN (sizeof(IPV4_JSON_KEY) - 1) #define IPV6_JSON_KEY "\"ipv6\":\"" #define IPV6_JSON_KEY_LEN (sizeof(IPV6_JSON_KEY) - 1) #define PORT_JSON_KEY "\"port\":" #define PORT_JSON_KEY_LEN (sizeof(PORT_JSON_KEY) - 1) #define PK_JSON_KEY "\"public_key\":\"" #define PK_JSON_KEY_LEN (sizeof(PK_JSON_KEY) - 1) #define LAST_PING_JSON_KEY "\"last_ping\":" #define LAST_PING_JSON_KEY_LEN (sizeof(LAST_PING_JSON_KEY) - 1) /* Maximum allowable size of the nodes list */ #define MAX_NODELIST_SIZE (MAX_RECV_CURL_DATA_SIZE) static struct Thread_Data { pthread_t tid; pthread_attr_t attr; pthread_mutex_t lock; volatile bool active; } thread_data; #define MAX_NODES 50 struct Node { char ip4[IP_MAX_SIZE + 1]; bool have_ip4; char ip6[IP_MAX_SIZE + 1]; bool have_ip6; char key[TOX_PUBLIC_KEY_SIZE]; uint16_t port; }; static struct DHT_Nodes { struct Node list[MAX_NODES]; size_t count; time_t last_updated; } Nodes; /* Return true if address appears to be a valid ipv4 address. */ static bool is_ip4_address(const char *address) { struct sockaddr_in s_addr; return inet_pton(AF_INET, address, &(s_addr.sin_addr)) != 0; } /* Return true if address roughly appears to be a valid ipv6 address. * * TODO: Improve this function (inet_pton behaves strangely with ipv6). * for now the only guarantee is that it won't return true if the * address is a domain or ipv4 address, and should only be used if you're * reasonably sure that the address is one of the three (ipv4, ipv6 or a domain). */ static bool is_ip6_address(const char *address) { size_t num_colons = 0; char ch = 0; for (size_t i = 0; (ch = address[i]); ++i) { if (ch == '.') { return false; } if (ch == ':') { ++num_colons; } } return num_colons > 1 && num_colons < 8; } /* Determine if a node is offline by comparing the age of the nodeslist * to the last time the node was successfully pinged. */ static bool node_is_offline(unsigned long long int last_ping) { return last_ping + NODE_OFFLINE_TIMOUT <= last_ping; } /* Return true if nodeslist pointed to by fp needs to be updated. * This will be the case if the file is empty, has an invalid format, * or if the file is older than the given timeout. */ static bool nodeslist_needs_update(const char *nodes_path) { if (user_settings->nodeslist_update_freq <= 0) { return false; } FILE *fp = fopen(nodes_path, "r+"); if (fp == NULL) { return false; } /* last_scan value should be at beginning of file */ char line[LAST_SCAN_JSON_KEY_LEN + 32]; if (fgets(line, sizeof(line), fp) == NULL) { fclose(fp); return true; } fclose(fp); const char *last_scan_val = strstr(line, LAST_SCAN_JSON_KEY); if (last_scan_val == NULL) { return true; } long long int last_scan = strtoll(last_scan_val + LAST_SCAN_JSON_KEY_LEN, NULL, 10); pthread_mutex_lock(&thread_data.lock); Nodes.last_updated = last_scan; pthread_mutex_unlock(&thread_data.lock); pthread_mutex_lock(&Winthread.lock); bool is_timeout = timed_out(last_scan, user_settings->nodeslist_update_freq * 24 * 60 * 60); pthread_mutex_unlock(&Winthread.lock); if (is_timeout) { return true; } return false; } /* Fetches the JSON encoded DHT nodeslist from NODES_LIST_URL. * * Return 0 on success. * Return -1 on failure. */ static int curl_fetch_nodes_JSON(struct Recv_Curl_Data *recv_data) { CURL *c_handle = curl_easy_init(); if (c_handle == NULL) { return -1; } int err = -1; struct curl_slist *headers = NULL; headers = curl_slist_append(headers, "Content-Type: application/json"); headers = curl_slist_append(headers, "charsets: utf-8"); curl_easy_setopt(c_handle, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(c_handle, CURLOPT_URL, NODES_LIST_URL); curl_easy_setopt(c_handle, CURLOPT_WRITEFUNCTION, curl_cb_write_data); curl_easy_setopt(c_handle, CURLOPT_WRITEDATA, recv_data); curl_easy_setopt(c_handle, CURLOPT_USERAGENT, "libcurl-agent/1.0"); curl_easy_setopt(c_handle, CURLOPT_HTTPGET, 1L); int proxy_ret = set_curl_proxy(c_handle, arg_opts.proxy_address, arg_opts.proxy_port, arg_opts.proxy_type); if (proxy_ret != 0) { fprintf(stderr, "set_curl_proxy() failed with error %d\n", proxy_ret); goto on_exit; } int ret = curl_easy_setopt(c_handle, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2); if (ret != CURLE_OK) { fprintf(stderr, "TLSv1.2 could not be set (libcurl error %d)", ret); goto on_exit; } ret = curl_easy_setopt(c_handle, CURLOPT_SSL_CIPHER_LIST, TLS_CIPHER_SUITE_LIST); if (ret != CURLE_OK) { fprintf(stderr, "Failed to set TLS cipher list (libcurl error %d)", ret); goto on_exit; } ret = curl_easy_perform(c_handle); if (ret != CURLE_OK) { /* If system doesn't support any of the specified ciphers suites, fall back to default */ if (ret == CURLE_SSL_CIPHER) { curl_easy_setopt(c_handle, CURLOPT_SSL_CIPHER_LIST, NULL); ret = curl_easy_perform(c_handle); } if (ret != CURLE_OK) { fprintf(stderr, "HTTPS lookup error (libcurl error %d)\n", ret); goto on_exit; } } err = 0; on_exit: curl_slist_free_all(headers); curl_easy_cleanup(c_handle); return err; } /* Attempts to update the DHT nodeslist. * * Return 1 if list was updated successfully. * Return 0 if list does not need to be updated. * Return -1 if file cannot be opened. * Return -2 if http lookup failed. * Return -3 if http reponse was empty. * Return -4 if data could not be written to disk. * Return -5 if memory allocation fails. */ static int update_DHT_nodeslist(const char *nodes_path) { if (!nodeslist_needs_update(nodes_path)) { return 0; } FILE *fp = fopen(nodes_path, "r+"); if (fp == NULL) { return -1; } struct Recv_Curl_Data *recv_data = calloc(1, sizeof(struct Recv_Curl_Data)); if (recv_data == NULL) { fclose(fp); return -5; } if (curl_fetch_nodes_JSON(recv_data) == -1) { free(recv_data); fclose(fp); return -2; } if (recv_data->length == 0) { free(recv_data); fclose(fp); return -3; } if (fwrite(recv_data->data, recv_data->length, 1, fp) != 1) { free(recv_data); fclose(fp); return -4; } free(recv_data); fclose(fp); return 1; } static void get_nodeslist_path(char *buf, size_t buf_size) { char *config_dir = NULL; if (arg_opts.nodes_path[0]) { snprintf(buf, buf_size, "%s", arg_opts.nodes_path); } else if ((config_dir = get_user_config_dir()) != NULL) { snprintf(buf, buf_size, "%s%s%s", config_dir, CONFIGDIR, DEFAULT_NODES_FILENAME); free(config_dir); } else { snprintf(buf, buf_size, "%s", DEFAULT_NODES_FILENAME); } } /* Return true if json encoded string s contains a valid IP address and puts address in ip_buf. * * ip_type should be set to 1 for ipv4 address, or 0 for ipv6 addresses. * ip_buf must have room for at least IP_MAX_SIZE + 1 bytes. */ static bool extract_val_ip(const char *s, char *ip_buf, unsigned short int ip_type) { int ip_len = char_find(0, s, '"'); if (ip_len < IP_MIN_SIZE || ip_len > IP_MAX_SIZE) { return false; } memcpy(ip_buf, s, ip_len); ip_buf[ip_len] = 0; return (ip_type == 1) ? is_ip4_address(ip_buf) : is_ip6_address(ip_buf); } /* Extracts the port from json encoded string s. * * Return port number on success. * Return 0 on failure. */ static uint16_t extract_val_port(const char *s) { long int port = strtol(s, NULL, 10); return (port > 0 && port <= MAX_PORT_RANGE) ? port : 0; } /* Extracts the last pinged value from json encoded string s. * * Return timestamp on success. * Return -1 on failure. */ static long long int extract_val_last_pinged(const char *s) { long long int last_pinged = strtoll(s, NULL, 10); return (last_pinged <= 0) ? -1 : last_pinged; } /* Extracts DHT public key from json encoded string s and puts key in key_buf. * key_buf must have room for at least TOX_PUBLIC_KEY_SIZE * 2 + 1 bytes. * * Return number of bytes copied to key_buf on success. * Return -1 on failure. */ static int extract_val_pk(const char *s, char *key_buf, size_t buf_length) { if (buf_length < TOX_PUBLIC_KEY_SIZE * 2 + 1) { return -1; } int key_len = char_find(0, s, '"'); if (key_len != TOX_PUBLIC_KEY_SIZE * 2) { return -1; } memcpy(key_buf, s, key_len); key_buf[key_len] = 0; return key_len; } /* Extracts values from json formatted string, validats them, and puts them in node. * * Return 0 on success. * Return -1 if line is empty. * Return -2 if line does not appear to be a valid nodes list entry. * Return -3 if node appears to be offline. * Return -4 if entry does not contain either a valid ipv4 or ipv6 address. * Return -5 if port value is invalid. * Return -6 if public key is invalid. */ static int extract_node(const char *line, struct Node *node) { if (!line) { return -1; } const char *ip4_start = strstr(line, IPV4_JSON_KEY); const char *ip6_start = strstr(line, IPV6_JSON_KEY); const char *port_start = strstr(line, PORT_JSON_KEY); const char *key_start = strstr(line, PK_JSON_KEY); const char *last_pinged_str = strstr(line, LAST_PING_JSON_KEY); if (!ip4_start || !ip6_start || !port_start || !key_start || !last_pinged_str) { return -2; } long long int last_pinged = extract_val_last_pinged(last_pinged_str + LAST_PING_JSON_KEY_LEN); if (last_pinged <= 0 || node_is_offline(last_pinged)) { return -3; } char ip4_string[IP_MAX_SIZE + 1]; bool have_ip4 = extract_val_ip(ip4_start + IPV4_JSON_KEY_LEN, ip4_string, 1); char ip6_string[IP_MAX_SIZE + 1]; bool have_ip6 = extract_val_ip(ip6_start + IPV6_JSON_KEY_LEN, ip6_string, 0); if (!have_ip6 && !have_ip4) { return -4; } uint16_t port = extract_val_port(port_start + PORT_JSON_KEY_LEN); if (port == 0) { return -5; } char key_string[TOX_PUBLIC_KEY_SIZE * 2 + 1]; int key_len = extract_val_pk(key_start + PK_JSON_KEY_LEN, key_string, sizeof(key_string)); if (key_len == -1) { return -6; } if (tox_pk_string_to_bytes(key_string, key_len, node->key, sizeof(node->key)) == -1) { return -6; } if (have_ip4) { snprintf(node->ip4, sizeof(node->ip4), "%s", ip4_string); node->have_ip4 = true; } if (have_ip6) { snprintf(node->ip6, sizeof(node->ip6), "%s", ip6_string); node->have_ip6 = true; } node->port = port; return 0; } /* Loads the DHT nodeslist to memory from json encoded nodes file. */ void *load_nodeslist_thread(void *data) { UNUSED_VAR(data); char nodes_path[PATH_MAX]; get_nodeslist_path(nodes_path, sizeof(nodes_path)); FILE *fp = NULL; if (!file_exists(nodes_path)) { if ((fp = fopen(nodes_path, "w+")) == NULL) { fprintf(stderr, "nodeslist load error: failed to create file '%s'\n", nodes_path); goto on_exit; } } else if ((fp = fopen(nodes_path, "r+")) == NULL) { fprintf(stderr, "nodeslist load error: failed to open file '%s'\n", nodes_path); goto on_exit; } int update_err = update_DHT_nodeslist(nodes_path); if (update_err < 0) { fprintf(stderr, "update_DHT_nodeslist() failed with error %d\n", update_err); } char line[MAX_NODELIST_SIZE + 1]; if (fgets(line, sizeof(line), fp) == NULL) { fclose(fp); fprintf(stderr, "nodeslist load error: file empty.\n"); goto on_exit; } size_t idx = 0; const char *line_start = line; while ((line_start = strstr(line_start + 1, IPV4_JSON_KEY))) { pthread_mutex_lock(&thread_data.lock); idx = Nodes.count; if (idx >= MAX_NODES) { pthread_mutex_unlock(&thread_data.lock); break; } if (extract_node(line_start, &Nodes.list[idx]) == 0) { ++Nodes.count; } pthread_mutex_unlock(&thread_data.lock); } /* If nodeslist does not contain any valid entries we set the last_scan value * to 0 so that it will fetch a new list the next time this function is called. */ if (Nodes.count == 0) { const char *s = "{\"last_scan\":0}"; rewind(fp); fwrite(s, strlen(s), 1, fp); // Not much we can do if it fails fclose(fp); fprintf(stderr, "nodeslist load error: List did not contain any valid entries.\n"); goto on_exit; } fclose(fp); on_exit: thread_data.active = false; pthread_attr_destroy(&thread_data.attr); pthread_exit(0); } /* Creates a new thread that will load the DHT nodeslist to memory * from json encoded nodes file obtained at NODES_LIST_URL. Only one * thread may run at a time. * * Return 0 on success. * Return -1 if a thread is already active. * Return -2 if mutex fails to init. * Return -3 if pthread attribute fails to init. * Return -4 if pthread fails to set detached state. * Return -5 if thread creation fails. */ int load_DHT_nodeslist(void) { if (thread_data.active) { return -1; } if (pthread_mutex_init(&thread_data.lock, NULL) != 0) { return -2; } if (pthread_attr_init(&thread_data.attr) != 0) { return -3; } if (pthread_attr_setdetachstate(&thread_data.attr, PTHREAD_CREATE_DETACHED) != 0) { return -4; } thread_data.active = true; if (pthread_create(&thread_data.tid, &thread_data.attr, load_nodeslist_thread, NULL) != 0) { thread_data.active = false; return -5; } return 0; } /* Connects to NUM_BOOTSTRAP_NODES random DHT nodes listed in the DHTnodes file. */ static void DHT_bootstrap(Tox *m) { pthread_mutex_lock(&thread_data.lock); size_t num_nodes = Nodes.count; pthread_mutex_unlock(&thread_data.lock); if (num_nodes == 0) { return; } size_t i; pthread_mutex_lock(&thread_data.lock); for (i = 0; i < NUM_BOOTSTRAP_NODES; ++i) { struct Node *node = &Nodes.list[rand() % Nodes.count]; const char *addr = node->have_ip4 ? node->ip4 : node->ip6; if (!addr) { continue; } Tox_Err_Bootstrap err; tox_bootstrap(m, addr, node->port, (uint8_t *) node->key, &err); if (err != TOX_ERR_BOOTSTRAP_OK) { fprintf(stderr, "Failed to bootstrap %s:%d\n", addr, node->port); } tox_add_tcp_relay(m, addr, node->port, (uint8_t *) node->key, &err); if (err != TOX_ERR_BOOTSTRAP_OK) { fprintf(stderr, "Failed to add TCP relay %s:%d\n", addr, node->port); } } pthread_mutex_unlock(&thread_data.lock); } /* Manages connection to the Tox DHT network. */ void do_tox_connection(Tox *m) { static time_t last_bootstrap_time = 0; bool connected = prompt_selfConnectionStatus() != TOX_CONNECTION_NONE; if (!connected && timed_out(last_bootstrap_time, TRY_BOOTSTRAP_INTERVAL)) { DHT_bootstrap(m); last_bootstrap_time = get_unix_time(); } } toxic-0.11.3/src/bootstrap.h000066400000000000000000000025061416141666600157150ustar00rootroot00000000000000/* bootstrap.h * * * Copyright (C) 2016 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef BOOTSTRAP_H #define BOOTSTRAP_H /* Manages connection to the Tox DHT network. */ void do_tox_connection(Tox *m); /* Creates a new thread that will load the DHT nodeslist to memory * from json encoded nodes file obtained at NODES_LIST_URL. Only one * thread may run at a time. * * Return 0 on success. * Return -1 if a thread is already active. * Return -2 if mutex fails to init. * Return -3 if pthread attribute fails to init. * Return -4 if pthread fails to set detached state. * Return -5 if thread creation fails. */ int load_DHT_nodeslist(void); #endif /* BOOTSTRAP_H */ toxic-0.11.3/src/chat.c000066400000000000000000001332461416141666600146200ustar00rootroot00000000000000/* chat.c * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef _GNU_SOURCE #define _GNU_SOURCE /* needed for wcswidth() */ #endif #include #include #include #include #include #include #include "autocomplete.h" #include "execute.h" #include "file_transfers.h" #include "friendlist.h" #include "help.h" #include "input.h" #include "line_info.h" #include "log.h" #include "message_queue.h" #include "misc_tools.h" #include "notify.h" #include "settings.h" #include "toxic.h" #include "toxic_strings.h" #include "windows.h" #ifdef GAMES #include "game_base.h" #endif #ifdef AUDIO #include "audio_call.h" #ifdef VIDEO #include "video_call.h" #endif /* VIDEO */ #endif /* AUDIO */ #ifdef AUDIO static void init_infobox(ToxWindow *self); static void kill_infobox(ToxWindow *self); #endif /* AUDIO */ /* Array of chat command names used for tab completion. */ static const char *chat_cmd_list[] = { "/accept", "/add", "/avatar", "/cancel", "/clear", "/close", "/connect", "/exit", "/conference", #ifdef GAMES "/game", "/play", #endif "/help", "/invite", "/join", "/log", "/myid", #ifdef QRCODE "/myqr", #endif /* QRCODE */ "/nick", "/note", "/nospam", "/quit", "/savefile", "/sendfile", "/status", #ifdef AUDIO "/call", "/answer", "/reject", "/hangup", "/sdev", "/mute", "/sense", "/bitrate", #endif /* AUDIO */ #ifdef VIDEO "/res", "/vcall", "/video", #endif /* VIDEO */ #ifdef PYTHON "/run", #endif /* PYTHON */ }; static void set_self_typingstatus(ToxWindow *self, Tox *m, bool is_typing) { if (user_settings->show_typing_self == SHOW_TYPING_OFF) { return; } ChatContext *ctx = self->chatwin; TOX_ERR_SET_TYPING err; tox_self_set_typing(m, self->num, is_typing, &err); if (err != TOX_ERR_SET_TYPING_OK) { fprintf(stderr, "Warning: tox_self_set_typing() failed with error %d\n", err); return; } ctx->self_is_typing = is_typing; } void kill_chat_window(ToxWindow *self, Tox *m) { ChatContext *ctx = self->chatwin; StatusBar *statusbar = self->stb; #ifdef AUDIO stop_current_call(self); #endif /* AUDIO */ kill_all_file_transfers_friend(m, self->num); log_disable(ctx->log); line_info_cleanup(ctx->hst); cqueue_cleanup(ctx->cqueue); delwin(ctx->linewin); delwin(ctx->history); delwin(statusbar->topline); free(ctx->log); free(ctx); free(self->help); free(statusbar); disable_chatwin(self->num); kill_notifs(self->active_box); del_window(self); } static void recv_message_helper(ToxWindow *self, const char *msg, const char *nick) { ChatContext *ctx = self->chatwin; line_info_add(self, true, nick, NULL, IN_MSG, 0, 0, "%s", msg); write_to_log(msg, nick, ctx->log, false); if (self->active_box != -1) { box_notify2(self, generic_message, NT_WNDALERT_1 | NT_NOFOCUS | user_settings->bell_on_message, self->active_box, "%s", msg); } else { box_notify(self, generic_message, NT_WNDALERT_1 | NT_NOFOCUS | user_settings->bell_on_message, &self->active_box, nick, "%s", msg); } } static void recv_action_helper(ToxWindow *self, const char *action, const char *nick) { ChatContext *ctx = self->chatwin; line_info_add(self, true, nick, NULL, IN_ACTION, 0, 0, "%s", action); write_to_log(action, nick, ctx->log, true); if (self->active_box != -1) { box_notify2(self, generic_message, NT_WNDALERT_1 | NT_NOFOCUS | user_settings->bell_on_message, self->active_box, "* %s %s", nick, action); } else { box_notify(self, generic_message, NT_WNDALERT_1 | NT_NOFOCUS | user_settings->bell_on_message, &self->active_box, self->name, "* %s %s", nick, action); } } static void chat_onMessage(ToxWindow *self, Tox *m, uint32_t num, Tox_Message_Type type, const char *msg, size_t len) { UNUSED_VAR(len); if (self->num != num) { return; } char nick[TOX_MAX_NAME_LENGTH]; get_nick_truncate(m, nick, num); if (type == TOX_MESSAGE_TYPE_NORMAL) { recv_message_helper(self, msg, nick); return; } if (type == TOX_MESSAGE_TYPE_ACTION) { recv_action_helper(self, msg, nick); return; } } static void chat_pause_file_transfers(uint32_t friendnum); static void chat_resume_file_senders(ToxWindow *self, Tox *m, uint32_t fnum); static void chat_onConnectionChange(ToxWindow *self, Tox *m, uint32_t num, Tox_Connection connection_status) { if (self->num != num) { return; } StatusBar *statusbar = self->stb; ChatContext *ctx = self->chatwin; const char *msg; char nick[TOX_MAX_NAME_LENGTH]; get_nick_truncate(m, nick, num); Tox_Connection prev_status = statusbar->connection; statusbar->connection = connection_status; if (user_settings->show_connection_msg == SHOW_WELCOME_MSG_OFF) { return; } if (prev_status == TOX_CONNECTION_NONE) { chat_resume_file_senders(self, m, num); file_send_queue_check(self, m, self->num); msg = "has come online"; line_info_add(self, true, nick, NULL, CONNECTION, 0, GREEN, msg); write_to_log(msg, nick, ctx->log, true); } else if (connection_status == TOX_CONNECTION_NONE) { Friends.list[num].is_typing = false; if (self->chatwin->self_is_typing) { set_self_typingstatus(self, m, false); } chat_pause_file_transfers(num); msg = "has gone offline"; line_info_add(self, true, nick, NULL, DISCONNECTION, 0, RED, msg); write_to_log(msg, nick, ctx->log, true); } } static void chat_onTypingChange(ToxWindow *self, Tox *m, uint32_t num, bool is_typing) { UNUSED_VAR(m); if (self->num != num) { return; } Friends.list[num].is_typing = is_typing; } static void chat_onNickChange(ToxWindow *self, Tox *m, uint32_t num, const char *nick, size_t length) { UNUSED_VAR(m); if (self->num != num) { return; } StatusBar *statusbar = self->stb; snprintf(statusbar->nick, sizeof(statusbar->nick), "%s", nick); length = strlen(statusbar->nick); statusbar->nick_len = length; set_window_title(self, statusbar->nick, length); } static void chat_onStatusChange(ToxWindow *self, Tox *m, uint32_t num, Tox_User_Status status) { UNUSED_VAR(m); if (self->num != num) { return; } StatusBar *statusbar = self->stb; statusbar->status = status; } static void chat_onStatusMessageChange(ToxWindow *self, uint32_t num, const char *status, size_t length) { UNUSED_VAR(length); if (self->num != num) { return; } StatusBar *statusbar = self->stb; snprintf(statusbar->statusmsg, sizeof(statusbar->statusmsg), "%s", status); statusbar->statusmsg_len = strlen(statusbar->statusmsg); } static void chat_onReadReceipt(ToxWindow *self, Tox *m, uint32_t num, uint32_t receipt) { UNUSED_VAR(num); cqueue_remove(self, m, receipt); } /* Stops active file transfers for this friend. Called when a friend goes offline */ static void chat_pause_file_transfers(uint32_t friendnum) { ToxicFriend *friend = &Friends.list[friendnum]; for (size_t i = 0; i < MAX_FILES; ++i) { struct FileTransfer *fts = &friend->file_sender[i]; if (fts->file_type == TOX_FILE_KIND_DATA && fts->state >= FILE_TRANSFER_STARTED) { fts->state = FILE_TRANSFER_PAUSED; } struct FileTransfer *ftr = &friend->file_receiver[i]; if (ftr->file_type == TOX_FILE_KIND_DATA && ftr->state >= FILE_TRANSFER_STARTED) { ftr->state = FILE_TRANSFER_PAUSED; } } } /* Tries to resume broken file senders. Called when a friend comes online */ static void chat_resume_file_senders(ToxWindow *self, Tox *m, uint32_t friendnum) { for (size_t i = 0; i < MAX_FILES; ++i) { struct FileTransfer *ft = &Friends.list[friendnum].file_sender[i]; if (ft->state != FILE_TRANSFER_PAUSED || ft->file_type != TOX_FILE_KIND_DATA) { continue; } Tox_Err_File_Send err; ft->filenumber = tox_file_send(m, friendnum, TOX_FILE_KIND_DATA, ft->file_size, ft->file_id, (uint8_t *) ft->file_name, strlen(ft->file_name), &err); if (err != TOX_ERR_FILE_SEND_OK) { char msg[MAX_STR_SIZE]; snprintf(msg, sizeof(msg), "File transfer for '%s' failed.", ft->file_name); close_file_transfer(self, m, ft, TOX_FILE_CONTROL_CANCEL, msg, notif_error); continue; } } } static void chat_onFileChunkRequest(ToxWindow *self, Tox *m, uint32_t friendnum, uint32_t filenumber, uint64_t position, size_t length) { if (friendnum != self->num) { return; } struct FileTransfer *ft = get_file_transfer_struct(friendnum, filenumber); if (!ft) { return; } if (ft->state != FILE_TRANSFER_STARTED) { return; } char msg[MAX_STR_SIZE]; if (length == 0) { snprintf(msg, sizeof(msg), "File '%s' successfully sent.", ft->file_name); print_progress_bar(self, ft->bps, 100.0, ft->line_id); close_file_transfer(self, m, ft, -1, msg, transfer_completed); return; } if (ft->file == NULL) { snprintf(msg, sizeof(msg), "File transfer for '%s' failed: Null file pointer.", ft->file_name); close_file_transfer(self, m, ft, TOX_FILE_CONTROL_CANCEL, msg, notif_error); return; } if (ft->position != position) { if (fseek(ft->file, position, SEEK_SET) == -1) { snprintf(msg, sizeof(msg), "File transfer for '%s' failed: Seek fail.", ft->file_name); close_file_transfer(self, m, ft, TOX_FILE_CONTROL_CANCEL, msg, notif_error); return; } ft->position = position; } uint8_t *send_data = malloc(length); if (send_data == NULL) { snprintf(msg, sizeof(msg), "File transfer for '%s' failed: Out of memory.", ft->file_name); close_file_transfer(self, m, ft, TOX_FILE_CONTROL_CANCEL, msg, notif_error); return; } size_t send_length = fread(send_data, 1, length, ft->file); if (send_length != length) { snprintf(msg, sizeof(msg), "File transfer for '%s' failed: Read fail.", ft->file_name); close_file_transfer(self, m, ft, TOX_FILE_CONTROL_CANCEL, msg, notif_error); free(send_data); return; } Tox_Err_File_Send_Chunk err; tox_file_send_chunk(m, ft->friendnumber, ft->filenumber, position, send_data, send_length, &err); free(send_data); if (err != TOX_ERR_FILE_SEND_CHUNK_OK) { fprintf(stderr, "tox_file_send_chunk failed in chat callback (error %d)\n", err); } ft->position += send_length; ft->bps += send_length; } static void chat_onFileRecvChunk(ToxWindow *self, Tox *m, uint32_t friendnum, uint32_t filenumber, uint64_t position, const char *data, size_t length) { UNUSED_VAR(position); if (friendnum != self->num) { return; } struct FileTransfer *ft = get_file_transfer_struct(friendnum, filenumber); if (!ft) { return; } if (ft->state != FILE_TRANSFER_STARTED) { return; } char msg[MAX_STR_SIZE]; if (length == 0) { snprintf(msg, sizeof(msg), "File '%s' successfully received.", ft->file_name); print_progress_bar(self, ft->bps, 100.0, ft->line_id); close_file_transfer(self, m, ft, -1, msg, transfer_completed); return; } if (ft->file == NULL) { snprintf(msg, sizeof(msg), "File transfer for '%s' failed: Invalid file pointer.", ft->file_name); close_file_transfer(self, m, ft, TOX_FILE_CONTROL_CANCEL, msg, notif_error); return; } if (fwrite(data, length, 1, ft->file) != 1) { snprintf(msg, sizeof(msg), "File transfer for '%s' failed: Write fail.", ft->file_name); close_file_transfer(self, m, ft, TOX_FILE_CONTROL_CANCEL, msg, notif_error); return; } ft->bps += length; ft->position += length; } static void chat_onFileControl(ToxWindow *self, Tox *m, uint32_t friendnum, uint32_t filenumber, Tox_File_Control control) { if (friendnum != self->num) { return; } struct FileTransfer *ft = get_file_transfer_struct(friendnum, filenumber); if (!ft) { return; } switch (control) { case TOX_FILE_CONTROL_RESUME: { /* transfer is accepted */ if (ft->state == FILE_TRANSFER_PENDING) { ft->state = FILE_TRANSFER_STARTED; line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "File transfer [%zu] for '%s' accepted.", ft->index, ft->file_name); char progline[MAX_STR_SIZE]; init_progress_bar(progline); line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "%s", progline); sound_notify(self, silent, NT_NOFOCUS | user_settings->bell_on_filetrans_accept | NT_WNDALERT_2, NULL); ft->line_id = self->chatwin->hst->line_end->id + 2; } else if (ft->state == FILE_TRANSFER_PAUSED) { /* transfer is resumed */ ft->state = FILE_TRANSFER_STARTED; } break; } case TOX_FILE_CONTROL_PAUSE: { ft->state = FILE_TRANSFER_PAUSED; break; } case TOX_FILE_CONTROL_CANCEL: { char msg[MAX_STR_SIZE]; snprintf(msg, sizeof(msg), "File transfer for '%s' was aborted.", ft->file_name); close_file_transfer(self, m, ft, -1, msg, notif_error); break; } } } /* Attempts to resume a broken inbound file transfer. * * Returns true if resume is successful. */ static bool chat_resume_broken_ft(ToxWindow *self, Tox *m, uint32_t friendnum, uint32_t filenumber) { char msg[MAX_STR_SIZE]; uint8_t file_id[TOX_FILE_ID_LENGTH]; if (!tox_file_get_file_id(m, friendnum, filenumber, file_id, NULL)) { return false; } bool resuming = false; struct FileTransfer *ft = NULL; size_t i; for (i = 0; i < MAX_FILES; ++i) { ft = &Friends.list[friendnum].file_receiver[i]; if (ft->state == FILE_TRANSFER_INACTIVE) { continue; } if (memcmp(ft->file_id, file_id, TOX_FILE_ID_LENGTH) == 0) { ft->filenumber = filenumber; ft->state = FILE_TRANSFER_STARTED; resuming = true; break; } } if (!resuming || !ft) { return false; } if (!tox_file_seek(m, ft->friendnumber, ft->filenumber, ft->position, NULL)) { goto on_error; } if (!tox_file_control(m, ft->friendnumber, ft->filenumber, TOX_FILE_CONTROL_RESUME, NULL)) { goto on_error; } return true; on_error: snprintf(msg, sizeof(msg), "File transfer for '%s' failed.", ft->file_name); close_file_transfer(self, m, ft, TOX_FILE_CONTROL_CANCEL, msg, notif_error); return false; } /* * Return true if file name is valid. * * A valid file name: * - cannot be empty. * - cannot contain the '/' characters. * - cannot begin with a space or hyphen. * - cannot be "." or ".." */ static bool valid_file_name(const char *filename, size_t length) { if (length == 0) { return false; } if (filename[0] == ' ' || filename[0] == '-') { return false; } if (strcmp(filename, ".") == 0 || strcmp(filename, "..") == 0) { return false; } for (size_t i = 0; i < length; ++i) { if (filename[i] == '/') { return false; } } return true; } static void chat_onFileRecv(ToxWindow *self, Tox *m, uint32_t friendnum, uint32_t filenumber, uint64_t file_size, const char *filename, size_t name_length) { if (self->num != friendnum) { return; } /* first check if we need to resume a broken transfer */ if (chat_resume_broken_ft(self, m, friendnum, filenumber)) { return; } struct FileTransfer *ft = new_file_transfer(self, friendnum, filenumber, FILE_TRANSFER_RECV, TOX_FILE_KIND_DATA); if (!ft) { tox_file_control(m, friendnum, filenumber, TOX_FILE_CONTROL_CANCEL, NULL); line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "File transfer request failed: Too many concurrent file transfers."); return; } char sizestr[32]; bytes_convert_str(sizestr, sizeof(sizestr), file_size); line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "File transfer request for '%s' (%s)", filename, sizestr); if (!valid_file_name(filename, name_length)) { close_file_transfer(self, m, ft, TOX_FILE_CONTROL_CANCEL, "File transfer failed: Invalid file name.", notif_error); return; } size_t file_path_buf_size = PATH_MAX + name_length + 1; char *file_path = malloc(file_path_buf_size); if (file_path == NULL) { close_file_transfer(self, m, ft, TOX_FILE_CONTROL_CANCEL, "File transfer failed: Out of memory.", notif_error); return; } size_t path_len = name_length; /* use specified download path in config if possible */ if (!string_is_empty(user_settings->download_path)) { snprintf(file_path, file_path_buf_size, "%s%s", user_settings->download_path, filename); path_len += strlen(user_settings->download_path); } else { snprintf(file_path, file_path_buf_size, "%s", filename); } if (path_len >= file_path_buf_size || path_len >= sizeof(ft->file_path) || name_length >= sizeof(ft->file_name)) { close_file_transfer(self, m, ft, TOX_FILE_CONTROL_CANCEL, "File transfer failed: File path too long.", notif_error); free(file_path); return; } /* Append a number to duplicate file names */ FILE *filecheck = NULL; int count = 1; while ((filecheck = fopen(file_path, "r"))) { fclose(filecheck); file_path[path_len] = '\0'; char d[5]; snprintf(d, sizeof(d), "(%d)", count); size_t d_len = strlen(d); if (path_len + d_len >= file_path_buf_size) { close_file_transfer(self, m, ft, TOX_FILE_CONTROL_CANCEL, "File transfer failed: File path too long.", notif_error); free(file_path); return; } strcat(file_path, d); file_path[path_len + d_len] = '\0'; if (++count > 99) { // If there are this many duplicate file names we should probably give up close_file_transfer(self, m, ft, TOX_FILE_CONTROL_CANCEL, "File transfer failed: invalid file path.", notif_error); free(file_path); return; } } line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Type '/savefile %zu' to accept the file transfer.", ft->index); ft->file_size = file_size; snprintf(ft->file_path, sizeof(ft->file_path), "%s", file_path); snprintf(ft->file_name, sizeof(ft->file_name), "%s", filename); tox_file_get_file_id(m, friendnum, filenumber, ft->file_id, NULL); free(file_path); if (self->active_box != -1) { box_notify2(self, transfer_pending, NT_WNDALERT_0 | NT_NOFOCUS | user_settings->bell_on_filetrans, self->active_box, "Incoming file: %s", filename); } else { box_notify(self, transfer_pending, NT_WNDALERT_0 | NT_NOFOCUS | user_settings->bell_on_filetrans, &self->active_box, self->name, "Incoming file: %s", filename); } } static void chat_onConferenceInvite(ToxWindow *self, Tox *m, int32_t friendnumber, uint8_t type, const char *conference_pub_key, uint16_t length) { if (self->num != friendnumber) { return; } if (Friends.list[friendnumber].conference_invite.key != NULL) { free(Friends.list[friendnumber].conference_invite.key); } char *k = malloc(length); if (k == NULL) { exit_toxic_err("Failed in chat_onConferenceInvite", FATALERR_MEMORY); } memcpy(k, conference_pub_key, length); Friends.list[friendnumber].conference_invite.key = k; Friends.list[friendnumber].conference_invite.pending = true; Friends.list[friendnumber].conference_invite.length = length; Friends.list[friendnumber].conference_invite.type = type; char name[TOX_MAX_NAME_LENGTH]; get_nick_truncate(m, name, friendnumber); const char *description = type == TOX_CONFERENCE_TYPE_AV ? "an audio conference" : "a conference"; if (self->active_box != -1) { box_notify2(self, generic_message, NT_WNDALERT_2 | user_settings->bell_on_invite, self->active_box, "invites you to join %s", description); } else { box_notify(self, generic_message, NT_WNDALERT_2 | user_settings->bell_on_invite, &self->active_box, name, "invites you to join %s", description); } line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "%s has invited you to %s.", name, description); line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Type \"/join\" to join the chat."); } #ifdef GAMES void chat_onGameInvite(ToxWindow *self, Tox *m, uint32_t friend_number, const uint8_t *data, size_t length) { if (!self || self->num != friend_number) { return; } if (length < GAME_PACKET_HEADER_SIZE || length > GAME_MAX_DATA_SIZE) { return; } uint8_t version = data[0]; if (version != GAME_NETWORKING_VERSION) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Game invite failed. Friend has network protocol version %d, you have version %d.", version, GAME_NETWORKING_VERSION); return; } GameType type = data[1]; if (!game_type_is_multiplayer(type)) { return; } uint32_t id; game_util_unpack_u32(data + 2, &id); const char *game_string = game_get_name_string(type); if (game_string == NULL) { return; } uint32_t data_length = length - GAME_PACKET_HEADER_SIZE; if (data_length > 0) { free(Friends.list[friend_number].game_invite.data); uint8_t *buf = calloc(1, data_length); if (buf == NULL) { return; } memcpy(buf, data + GAME_PACKET_HEADER_SIZE, data_length); Friends.list[friend_number].game_invite.data = buf; } Friends.list[friend_number].game_invite.type = type; Friends.list[friend_number].game_invite.id = id; Friends.list[friend_number].game_invite.pending = true; Friends.list[friend_number].game_invite.data_length = data_length; char name[TOX_MAX_NAME_LENGTH]; get_nick_truncate(m, name, friend_number); if (self->active_box != -1) { box_notify2(self, generic_message, NT_WNDALERT_2 | user_settings->bell_on_invite, self->active_box, "invites you to play %s", game_string); } else { box_notify(self, generic_message, NT_WNDALERT_2 | user_settings->bell_on_invite, &self->active_box, name, "invites you to play %s", game_string); } line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "%s has invited you to a game of %s.", name, game_string); line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Type \"/play\" to join the game."); } #endif // GAMES /* AV Stuff */ #ifdef AUDIO void chat_onInvite(ToxWindow *self, ToxAV *av, uint32_t friend_number, int state) { UNUSED_VAR(av); UNUSED_VAR(state); if (!self || self->num != friend_number) { return; } /* call is flagged active here */ self->is_call = true; line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Incoming audio call! Type: \"/answer\" or \"/reject\""); if (self->ringing_sound == -1) { sound_notify(self, call_incoming, NT_LOOP | user_settings->bell_on_invite, &self->ringing_sound); } if (self->active_box != -1) { box_silent_notify2(self, NT_NOFOCUS | NT_WNDALERT_0, self->active_box, "Incoming audio call!"); } else { box_silent_notify(self, NT_NOFOCUS | NT_WNDALERT_0, &self->active_box, self->name, "Incoming audio call!"); } } void chat_onRinging(ToxWindow *self, ToxAV *av, uint32_t friend_number, int state) { UNUSED_VAR(av); UNUSED_VAR(state); if (!self || self->num != friend_number) { return; } line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Ringing...type \"/hangup\" to cancel it."); #ifdef SOUND_NOTIFY if (self->ringing_sound == -1) { sound_notify(self, call_outgoing, NT_LOOP, &self->ringing_sound); } #endif /* SOUND_NOTIFY */ } void chat_onStarting(ToxWindow *self, ToxAV *av, uint32_t friend_number, int state) { UNUSED_VAR(av); UNUSED_VAR(state); if (!self || self->num != friend_number) { return; } init_infobox(self); line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Call started! Type: \"/hangup\" to end it."); /* call is flagged active here */ self->is_call = true; #ifdef SOUND_NOTIFY stop_sound(self->ringing_sound); #endif /* SOUND_NOTIFY */ } void chat_onEnding(ToxWindow *self, ToxAV *av, uint32_t friend_number, int state) { UNUSED_VAR(av); UNUSED_VAR(state); if (!self || self->num != friend_number) { return; } kill_infobox(self); line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Call ended!"); self->is_call = false; #ifdef SOUND_NOTIFY stop_sound(self->ringing_sound); #endif /* SOUND_NOTIFY */ } void chat_onError(ToxWindow *self, ToxAV *av, uint32_t friend_number, int state) { UNUSED_VAR(av); UNUSED_VAR(state); if (!self || self->num != friend_number) { return; } self->is_call = false; line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Error!"); #ifdef SOUND_NOTIFY stop_sound(self->ringing_sound); #endif /* SOUND_NOTIFY */ } void chat_onStart(ToxWindow *self, ToxAV *av, uint32_t friend_number, int state) { UNUSED_VAR(av); UNUSED_VAR(state); if (!self || self->num != friend_number) { return; } /* call is flagged active here */ self->is_call = true; init_infobox(self); line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Call started! Type: \"/hangup\" to end it."); #ifdef SOUND_NOTIFY stop_sound(self->ringing_sound); #endif /* SOUND_NOTIFY */ } void chat_onCancel(ToxWindow *self, ToxAV *av, uint32_t friend_number, int state) { UNUSED_VAR(av); UNUSED_VAR(state); if (!self || self->num != friend_number) { return; } self->is_call = false; kill_infobox(self); line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Call canceled!"); #ifdef SOUND_NOTIFY stop_sound(self->ringing_sound); #endif /* SOUND_NOTIFY */ } void chat_onReject(ToxWindow *self, ToxAV *av, uint32_t friend_number, int state) { UNUSED_VAR(av); UNUSED_VAR(state); if (!self || self->num != friend_number) { return; } line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Rejected!"); self->is_call = false; #ifdef SOUND_NOTIFY stop_sound(self->ringing_sound); #endif /* SOUND_NOTIFY */ } void chat_onEnd(ToxWindow *self, ToxAV *av, uint32_t friend_number, int state) { UNUSED_VAR(av); UNUSED_VAR(state); if (!self || self->num != friend_number) { return; } kill_infobox(self); line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Call ended!"); self->is_call = false; #ifdef SOUND_NOTIFY stop_sound(self->ringing_sound); #endif /* SOUND_NOTIFY */ } static void init_infobox(ToxWindow *self) { ChatContext *ctx = self->chatwin; int x2, y2; getmaxyx(self->window, y2, x2); if (y2 <= 0 || x2 <= 0) { return; } UNUSED_VAR(y2); ctx->infobox = (struct infobox) { 0 }; ctx->infobox.win = newwin(INFOBOX_HEIGHT, INFOBOX_WIDTH + 1, 1, x2 - INFOBOX_WIDTH); ctx->infobox.starttime = get_unix_time(); ctx->infobox.vad_lvl = user_settings->VAD_threshold; ctx->infobox.active = true; strcpy(ctx->infobox.timestr, "00"); } static void kill_infobox(ToxWindow *self) { ChatContext *ctx = self->chatwin; if (!ctx->infobox.win) { return; } delwin(ctx->infobox.win); ctx->infobox = (struct infobox) { 0 }; } /* update infobox info and draw in respective chat window */ static void draw_infobox(ToxWindow *self) { struct infobox *infobox = &self->chatwin->infobox; if (infobox->win == NULL) { return; } int x2, y2; getmaxyx(self->window, y2, x2); if (x2 < INFOBOX_WIDTH || y2 < INFOBOX_HEIGHT) { return; } time_t curtime = get_unix_time(); /* update interface once per second */ if (timed_out(infobox->lastupdate, 1)) { get_elapsed_time_str(infobox->timestr, sizeof(infobox->timestr), curtime - infobox->starttime); infobox->lastupdate = curtime; } const char *in_is_muted = infobox->in_is_muted ? "yes" : "no"; const char *out_is_muted = infobox->out_is_muted ? "yes" : "no"; wmove(infobox->win, 1, 1); wattron(infobox->win, COLOR_PAIR(RED) | A_BOLD); wprintw(infobox->win, " Call Active\n"); wattroff(infobox->win, COLOR_PAIR(RED) | A_BOLD); wattron(infobox->win, A_BOLD); wprintw(infobox->win, " Duration: "); wattroff(infobox->win, A_BOLD); wprintw(infobox->win, "%s\n", infobox->timestr); wattron(infobox->win, A_BOLD); wprintw(infobox->win, " In muted: "); wattroff(infobox->win, A_BOLD); wprintw(infobox->win, "%s\n", in_is_muted); wattron(infobox->win, A_BOLD); wprintw(infobox->win, " Out muted: "); wattroff(infobox->win, A_BOLD); wprintw(infobox->win, "%s\n", out_is_muted); wattron(infobox->win, A_BOLD); wprintw(infobox->win, " VAD level: "); wattroff(infobox->win, A_BOLD); wprintw(infobox->win, "%.2f\n", (double) infobox->vad_lvl); wborder(infobox->win, ACS_VLINE, ' ', ACS_HLINE, ACS_HLINE, ACS_ULCORNER, ' ', ACS_LLCORNER, ' '); wnoutrefresh(infobox->win); } #endif /* AUDIO */ static void send_action(ToxWindow *self, ChatContext *ctx, Tox *m, char *action) { if (action == NULL) { return; } char selfname[TOX_MAX_NAME_LENGTH]; tox_self_get_name(m, (uint8_t *) selfname); size_t len = tox_self_get_name_size(m); selfname[len] = '\0'; int id = line_info_add(self, true, selfname, NULL, OUT_ACTION, 0, 0, "%s", action); cqueue_add(ctx->cqueue, action, strlen(action), OUT_ACTION, id); } /* * Return true if input is recognized by handler */ bool chat_onKey(ToxWindow *self, Tox *m, wint_t key, bool ltr) { ChatContext *ctx = self->chatwin; StatusBar *statusbar = self->stb; int x, y, y2, x2; getyx(self->window, y, x); getmaxyx(self->window, y2, x2); UNUSED_VAR(y); if (y2 <= 0 || x2 <= 0) { return false; } if (ctx->pastemode && key == L'\r') { key = L'\n'; } if (self->help->active) { help_onKey(self, key); return true; } if (ltr || key == L'\n') { /* char is printable */ input_new_char(self, key, x, x2); if (ctx->line[0] != '/' && !ctx->self_is_typing && statusbar->connection != TOX_CONNECTION_NONE) { set_self_typingstatus(self, m, true); } return true; } if (line_info_onKey(self, key)) { return true; } int input_ret = input_handle(self, key, x, x2); if (key == L'\t' && ctx->len > 1 && ctx->line[0] == '/') { /* TAB key: auto-complete */ input_ret = true; int diff = -1; /* TODO: make this not suck */ if (wcsncmp(ctx->line, L"/sendfile ", wcslen(L"/sendfile ")) == 0) { diff = dir_match(self, m, ctx->line, L"/sendfile"); } else if (wcsncmp(ctx->line, L"/avatar ", wcslen(L"/avatar ")) == 0) { diff = dir_match(self, m, ctx->line, L"/avatar"); } #ifdef PYTHON else if (wcsncmp(ctx->line, L"/run ", wcslen(L"/run ")) == 0) { diff = dir_match(self, m, ctx->line, L"/run"); } #endif else if (wcsncmp(ctx->line, L"/status ", wcslen(L"/status ")) == 0) { const char *status_cmd_list[] = { "online", "away", "busy", }; diff = complete_line(self, status_cmd_list, sizeof(status_cmd_list) / sizeof(char *)); } else { diff = complete_line(self, chat_cmd_list, sizeof(chat_cmd_list) / sizeof(char *)); } if (diff != -1) { if (x + diff > x2 - 1) { int wlen = MAX(0, wcswidth(ctx->line, sizeof(ctx->line) / sizeof(wchar_t))); ctx->start = wlen < x2 ? 0 : wlen - x2 + 1; } } else { sound_notify(self, notif_error, 0, NULL); } } else if (key == L'\r') { input_ret = true; rm_trailing_spaces_buf(ctx); if (!wstring_is_empty(ctx->line)) { add_line_to_hist(ctx); wstrsubst(ctx->line, L'¶', L'\n'); char line[MAX_STR_SIZE]; if (wcs_to_mbs_buf(line, ctx->line, MAX_STR_SIZE) == -1) { memset(line, 0, sizeof(line)); } if (line[0] == '/') { if (strcmp(line, "/close") == 0) { kill_chat_window(self, m); return input_ret; } else if (strncmp(line, "/me ", strlen("/me ")) == 0) { send_action(self, ctx, m, line + strlen("/me ")); } else { execute(ctx->history, self, m, line, CHAT_COMMAND_MODE); } } else if (line[0]) { char selfname[TOX_MAX_NAME_LENGTH]; tox_self_get_name(m, (uint8_t *) selfname); size_t len = tox_self_get_name_size(m); selfname[len] = '\0'; int id = line_info_add(self, true, selfname, NULL, OUT_MSG, 0, 0, "%s", line); cqueue_add(ctx->cqueue, line, strlen(line), OUT_MSG, id); } else { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, RED, " * Failed to parse message."); } } wclear(ctx->linewin); wmove(self->window, y2, 0); reset_buf(ctx); } if (ctx->len <= 0 && ctx->self_is_typing) { set_self_typingstatus(self, m, false); } return input_ret; } static void chat_onDraw(ToxWindow *self, Tox *m) { int x2; int y2; getmaxyx(self->window, y2, x2); if (y2 <= 0 || x2 <= 0) { return; } ChatContext *ctx = self->chatwin; StatusBar *statusbar = self->stb; pthread_mutex_lock(&Winthread.lock); line_info_print(self); Tox_Connection connection = statusbar->connection; Tox_User_Status status = statusbar->status; const bool is_typing = Friends.list[self->num].is_typing; pthread_mutex_unlock(&Winthread.lock); wclear(ctx->linewin); if (ctx->len > 0) { mvwprintw(ctx->linewin, 0, 0, "%ls", &ctx->line[ctx->start]); } curs_set(1); wmove(statusbar->topline, 0, 0); wattron(statusbar->topline, COLOR_PAIR(BAR_ACCENT)); wprintw(statusbar->topline, " ["); wattroff(statusbar->topline, COLOR_PAIR(BAR_ACCENT)); switch (connection) { case TOX_CONNECTION_TCP: wattron(statusbar->topline, A_BOLD | COLOR_PAIR(STATUS_ONLINE)); wprintw(statusbar->topline, "TCP"); wattroff(statusbar->topline, A_BOLD | COLOR_PAIR(STATUS_ONLINE)); break; case TOX_CONNECTION_UDP: wattron(statusbar->topline, A_BOLD | COLOR_PAIR(STATUS_ONLINE)); wprintw(statusbar->topline, "UDP"); wattroff(statusbar->topline, A_BOLD | COLOR_PAIR(STATUS_ONLINE)); break; default: wattron(statusbar->topline, COLOR_PAIR(BAR_TEXT)); wprintw(statusbar->topline, "Offline"); wattroff(statusbar->topline, COLOR_PAIR(BAR_TEXT)); break; } wattron(statusbar->topline, COLOR_PAIR(BAR_ACCENT)); wprintw(statusbar->topline, "] "); wattroff(statusbar->topline, COLOR_PAIR(BAR_ACCENT)); const char *status_text = "ERROR"; int colour = BAR_TEXT; if (connection != TOX_CONNECTION_NONE) { switch (status) { case TOX_USER_STATUS_AWAY: colour = STATUS_AWAY; status_text = "Away"; break; case TOX_USER_STATUS_BUSY: colour = STATUS_BUSY; status_text = "Busy"; break; default: break; } } if (colour != BAR_TEXT) { wattron(statusbar->topline, COLOR_PAIR(BAR_ACCENT)); wprintw(statusbar->topline, "["); wattroff(statusbar->topline, COLOR_PAIR(BAR_ACCENT)); wattron(statusbar->topline, COLOR_PAIR(colour) | A_BOLD); wprintw(statusbar->topline, "%s", status_text); wattroff(statusbar->topline, COLOR_PAIR(colour) | A_BOLD); wattron(statusbar->topline, COLOR_PAIR(BAR_ACCENT)); wprintw(statusbar->topline, "] "); wattroff(statusbar->topline, COLOR_PAIR(BAR_ACCENT)); } if (is_typing) { wattron(statusbar->topline, A_BOLD | COLOR_PAIR(BAR_NOTIFY)); } else { wattron(statusbar->topline, COLOR_PAIR(BAR_TEXT)); } wprintw(statusbar->topline, "%s", statusbar->nick); if (is_typing) { wattroff(statusbar->topline, A_BOLD | COLOR_PAIR(BAR_NOTIFY)); } else { wattroff(statusbar->topline, A_BOLD | COLOR_PAIR(BAR_TEXT)); } /* Reset statusbar->statusmsg on window resize */ if (x2 != self->x) { char statusmsg[TOX_MAX_STATUS_MESSAGE_LENGTH] = {'\0'}; pthread_mutex_lock(&Winthread.lock); tox_friend_get_status_message(m, self->num, (uint8_t *) statusmsg, NULL); size_t s_len = tox_friend_get_status_message_size(m, self->num, NULL); filter_str(statusmsg, s_len); snprintf(statusbar->statusmsg, sizeof(statusbar->statusmsg), "%s", statusmsg); statusbar->statusmsg_len = strlen(statusbar->statusmsg); pthread_mutex_unlock(&Winthread.lock); } self->x = x2; /* Truncate note if it doesn't fit in statusbar */ size_t maxlen = x2 - getcurx(statusbar->topline) - (KEY_IDENT_DIGITS * 2) - 6; pthread_mutex_lock(&Winthread.lock); size_t statusmsg_len = statusbar->statusmsg_len; pthread_mutex_unlock(&Winthread.lock); if (statusmsg_len > maxlen) { pthread_mutex_lock(&Winthread.lock); statusbar->statusmsg[maxlen - 3] = 0; strcat(statusbar->statusmsg, "..."); statusbar->statusmsg_len = maxlen; pthread_mutex_unlock(&Winthread.lock); } if (statusmsg_len > 0) { wattron(statusbar->topline, COLOR_PAIR(BAR_ACCENT)); wprintw(statusbar->topline, " | "); wattroff(statusbar->topline, COLOR_PAIR(BAR_ACCENT)); wattron(statusbar->topline, COLOR_PAIR(BAR_TEXT)); pthread_mutex_lock(&Winthread.lock); wprintw(statusbar->topline, "%s ", statusbar->statusmsg); pthread_mutex_unlock(&Winthread.lock); } else { wattron(statusbar->topline, COLOR_PAIR(BAR_TEXT)); } int s_y; int s_x; getyx(statusbar->topline, s_y, s_x); mvwhline(statusbar->topline, s_y, s_x, ' ', x2 - s_x - (KEY_IDENT_DIGITS * 2) - 3); wattroff(statusbar->topline, COLOR_PAIR(BAR_TEXT)); wmove(statusbar->topline, 0, x2 - (KEY_IDENT_DIGITS * 2) - 3); wattron(statusbar->topline, COLOR_PAIR(BAR_ACCENT)); wprintw(statusbar->topline, "{"); wattroff(statusbar->topline, COLOR_PAIR(BAR_ACCENT)); wattron(statusbar->topline, COLOR_PAIR(BAR_TEXT)); for (size_t i = 0; i < KEY_IDENT_DIGITS; ++i) { wprintw(statusbar->topline, "%02X", Friends.list[self->num].pub_key[i] & 0xff); } wattroff(statusbar->topline, COLOR_PAIR(BAR_TEXT)); wattron(statusbar->topline, COLOR_PAIR(BAR_ACCENT)); wprintw(statusbar->topline, "} "); wattroff(statusbar->topline, COLOR_PAIR(BAR_ACCENT)); int y; int x; getyx(self->window, y, x); UNUSED_VAR(x); int new_x = ctx->start ? x2 - 1 : MAX(0, wcswidth(ctx->line, ctx->pos)); wmove(self->window, y, new_x); draw_window_bar(self); wnoutrefresh(self->window); #ifdef AUDIO if (ctx->infobox.active) { draw_infobox(self); } #endif if (self->help->active) { help_onDraw(self); } pthread_mutex_lock(&Winthread.lock); if (refresh_file_transfer_progress(self, self->num)) { flag_interface_refresh(); } pthread_mutex_unlock(&Winthread.lock); } static void chat_init_log(ToxWindow *self, Tox *m, const char *self_nick) { ChatContext *ctx = self->chatwin; char myid[TOX_ADDRESS_SIZE]; tox_self_get_address(m, (uint8_t *) myid); if (log_init(ctx->log, self_nick, myid, Friends.list[self->num].pub_key, LOG_TYPE_CHAT) != 0) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Failed to initialize chat log."); return; } if (load_chat_history(self, ctx->log) != 0) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Failed to load chat history."); } if (Friends.list[self->num].logging_on) { if (log_enable(ctx->log) != 0) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Failed to enable chat log."); } } } static void chat_onInit(ToxWindow *self, Tox *m) { curs_set(1); int x2; int y2; getmaxyx(self->window, y2, x2); if (y2 <= 0 || x2 <= 0) { exit_toxic_err("failed in chat_onInit", FATALERR_CURSES); } self->x = x2; /* Init statusbar info */ StatusBar *statusbar = self->stb; statusbar->status = get_friend_status(self->num); statusbar->connection = get_friend_connection_status(self->num); char statusmsg[TOX_MAX_STATUS_MESSAGE_LENGTH]; tox_friend_get_status_message(m, self->num, (uint8_t *) statusmsg, NULL); size_t s_len = tox_friend_get_status_message_size(m, self->num, NULL); statusmsg[s_len] = '\0'; filter_str(statusmsg, s_len); snprintf(statusbar->statusmsg, sizeof(statusbar->statusmsg), "%s", statusmsg); statusbar->statusmsg_len = strlen(statusbar->statusmsg); char nick[TOX_MAX_NAME_LENGTH + 1]; size_t n_len = get_nick_truncate(m, nick, self->num); memcpy(statusbar->nick, nick, n_len); statusbar->nick[n_len] = 0; statusbar->nick_len = n_len; /* Init subwindows */ ChatContext *ctx = self->chatwin; statusbar->topline = subwin(self->window, TOP_BAR_HEIGHT, x2, 0, 0); ctx->history = subwin(self->window, y2 - CHATBOX_HEIGHT - WINDOW_BAR_HEIGHT, x2, 0, 0); self->window_bar = subwin(self->window, WINDOW_BAR_HEIGHT, x2, y2 - (CHATBOX_HEIGHT + WINDOW_BAR_HEIGHT), 0); ctx->linewin = subwin(self->window, CHATBOX_HEIGHT, x2, y2 - WINDOW_BAR_HEIGHT, 0); ctx->hst = calloc(1, sizeof(struct history)); ctx->log = calloc(1, sizeof(struct chatlog)); ctx->cqueue = calloc(1, sizeof(struct chat_queue)); if (ctx->log == NULL || ctx->hst == NULL || ctx->cqueue == NULL) { exit_toxic_err("failed in chat_onInit", FATALERR_MEMORY); } line_info_init(ctx->hst); chat_init_log(self, m, nick); execute(ctx->history, self, m, "/log", GLOBAL_COMMAND_MODE); // Print log status to screen scrollok(ctx->history, 0); wmove(self->window, y2 - CURS_Y_OFFSET, 0); } ToxWindow *new_chat(Tox *m, uint32_t friendnum) { ToxWindow *ret = calloc(1, sizeof(ToxWindow)); if (ret == NULL) { exit_toxic_err("failed in new_chat", FATALERR_MEMORY); } ret->type = WINDOW_TYPE_CHAT; ret->onKey = &chat_onKey; ret->onDraw = &chat_onDraw; ret->onInit = &chat_onInit; ret->onMessage = &chat_onMessage; ret->onConnectionChange = &chat_onConnectionChange; ret->onTypingChange = & chat_onTypingChange; ret->onConferenceInvite = &chat_onConferenceInvite; ret->onNickChange = &chat_onNickChange; ret->onStatusChange = &chat_onStatusChange; ret->onStatusMessageChange = &chat_onStatusMessageChange; ret->onFileChunkRequest = &chat_onFileChunkRequest; ret->onFileRecvChunk = &chat_onFileRecvChunk; ret->onFileControl = &chat_onFileControl; ret->onFileRecv = &chat_onFileRecv; ret->onReadReceipt = &chat_onReadReceipt; #ifdef AUDIO ret->onInvite = &chat_onInvite; ret->onRinging = &chat_onRinging; ret->onStarting = &chat_onStarting; ret->onEnding = &chat_onEnding; ret->onError = &chat_onError; ret->onStart = &chat_onStart; ret->onCancel = &chat_onCancel; ret->onReject = &chat_onReject; ret->onEnd = &chat_onEnd; ret->is_call = false; ret->ringing_sound = -1; #endif /* AUDIO */ #ifdef GAMES ret->onGameInvite = &chat_onGameInvite; #endif /* GAMES */ ret->active_box = -1; char nick[TOX_MAX_NAME_LENGTH]; size_t n_len = get_nick_truncate(m, nick, friendnum); set_window_title(ret, nick, n_len); ChatContext *chatwin = calloc(1, sizeof(ChatContext)); StatusBar *stb = calloc(1, sizeof(StatusBar)); Help *help = calloc(1, sizeof(Help)); if (stb == NULL || chatwin == NULL || help == NULL) { exit_toxic_err("failed in new_chat", FATALERR_MEMORY); } ret->chatwin = chatwin; ret->stb = stb; ret->help = help; ret->num = friendnum; return ret; } toxic-0.11.3/src/chat.h000066400000000000000000000022051416141666600146130ustar00rootroot00000000000000/* chat.h * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef CHAT_H #define CHAT_H #include "toxic.h" #include "windows.h" /* set CTRL to -1 if we don't want to send a control signal. set msg to NULL if we don't want to display a message */ void chat_close_file_receiver(Tox *m, int filenum, int friendnum, int CTRL); void kill_chat_window(ToxWindow *self, Tox *m); ToxWindow *new_chat(Tox *m, int32_t friendnum); #endif /* end of include guard: CHAT_H */ toxic-0.11.3/src/chat_commands.c000066400000000000000000000332261416141666600164760ustar00rootroot00000000000000/* chat_commands.c * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #include #include #include "chat.h" #include "conference.h" #include "execute.h" #include "file_transfers.h" #include "friendlist.h" #include "line_info.h" #include "misc_tools.h" #include "toxic.h" #include "windows.h" extern ToxWindow *prompt; extern FriendsList Friends; void cmd_cancelfile(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); if (argc < 2) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Requires type in|out and the file ID."); return; } char msg[MAX_STR_SIZE]; const char *inoutstr = argv[1]; long int idx = strtol(argv[2], NULL, 10); if ((idx == 0 && strcmp(argv[2], "0")) || idx >= MAX_FILES || idx < 0) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Invalid file ID."); return; } // first check transfer queue if (file_send_queue_remove(self->num, idx) == 0) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Pending file transfer removed from queue"); return; } FileTransfer *ft = NULL; /* cancel an incoming file transfer */ if (strcasecmp(inoutstr, "in") == 0) { ft = get_file_transfer_struct_index(self->num, idx, FILE_TRANSFER_RECV); } else if (strcasecmp(inoutstr, "out") == 0) { ft = get_file_transfer_struct_index(self->num, idx, FILE_TRANSFER_SEND); } else { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Type must be 'in' or 'out'."); return; } if (!ft) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Invalid file ID."); return; } if (ft->state == FILE_TRANSFER_INACTIVE) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Invalid file ID."); return; } snprintf(msg, sizeof(msg), "File transfer for '%s' aborted.", ft->file_name); close_file_transfer(self, m, ft, TOX_FILE_CONTROL_CANCEL, msg, silent); } void cmd_conference_invite(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); if (argc < 1) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Conference number required."); return; } long int conferencenum = strtol(argv[1], NULL, 10); if ((conferencenum == 0 && strcmp(argv[1], "0")) || conferencenum < 0 || conferencenum == LONG_MAX) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Invalid conference number."); return; } Tox_Err_Conference_Invite err; if (!tox_conference_invite(m, self->num, conferencenum, &err)) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Failed to invite contact to conference (error %d)", err); return; } line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Invited contact to Conference %ld.", conferencenum); } void cmd_conference_join(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); UNUSED_VAR(argc); UNUSED_VAR(argv); if (get_num_active_windows() >= MAX_WINDOWS_NUM) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, RED, " * Warning: Too many windows are open."); return; } const char *conferencekey = Friends.list[self->num].conference_invite.key; uint16_t length = Friends.list[self->num].conference_invite.length; uint8_t type = Friends.list[self->num].conference_invite.type; if (!Friends.list[self->num].conference_invite.pending) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "No pending conference invite."); return; } uint32_t conferencenum; if (type == TOX_CONFERENCE_TYPE_TEXT) { Tox_Err_Conference_Join err; conferencenum = tox_conference_join(m, self->num, (const uint8_t *) conferencekey, length, &err); if (err != TOX_ERR_CONFERENCE_JOIN_OK) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Conference instance failed to initialize (error %d)", err); return; } } else if (type == TOX_CONFERENCE_TYPE_AV) { #ifdef AUDIO conferencenum = toxav_join_av_groupchat(m, self->num, (const uint8_t *) conferencekey, length, audio_conference_callback, NULL); if (conferencenum == (uint32_t) -1) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Audio conference instance failed to initialize"); return; } #else line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Audio support disabled by compile-time option."); return; #endif } else { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Unknown conference type %d", type); return; } if (init_conference_win(m, conferencenum, type, NULL, 0) == -1) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Conference window failed to initialize."); tox_conference_delete(m, conferencenum, NULL); return; } #ifdef AUDIO if (type == TOX_CONFERENCE_TYPE_AV) { if (!init_conference_audio_input(m, conferencenum)) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Audio capture failed; use \"/audio on\" to try again."); } } #endif } #ifdef GAMES void cmd_game_join(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); UNUSED_VAR(m); if (!Friends.list[self->num].game_invite.pending) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "No pending game invite."); return; } if (get_num_active_windows() >= MAX_WINDOWS_NUM) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, RED, " * Warning: Too many windows are open."); return; } GameType type = Friends.list[self->num].game_invite.type; uint32_t id = Friends.list[self->num].game_invite.id; uint8_t *data = Friends.list[self->num].game_invite.data; size_t length = Friends.list[self->num].game_invite.data_length; int ret = game_initialize(self, m, type, id, data, length); switch (ret) { case 0: { free(data); Friends.list[self->num].game_invite.data = NULL; Friends.list[self->num].game_invite.pending = false; break; } case -1: { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Window is too small. Try enlarging your window."); return; } case -2: { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Game failed to initialize (network error)"); return; } default: { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Game failed to initialize (error %d)", ret); return; } } } #endif // GAMES void cmd_savefile(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); if (argc < 1) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "File ID required."); return; } long int idx = strtol(argv[1], NULL, 10); if ((idx == 0 && strcmp(argv[1], "0")) || idx < 0 || idx >= MAX_FILES) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "No pending file transfers with that ID."); return; } FileTransfer *ft = get_file_transfer_struct_index(self->num, idx, FILE_TRANSFER_RECV); if (!ft) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "No pending file transfers with that ID."); return; } if (ft->state != FILE_TRANSFER_PENDING) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "No pending file transfers with that ID."); return; } if ((ft->file = fopen(ft->file_path, "a")) == NULL) { const char *msg = "File transfer failed: Invalid download path."; close_file_transfer(self, m, ft, TOX_FILE_CONTROL_CANCEL, msg, notif_error); return; } Tox_Err_File_Control err; tox_file_control(m, self->num, ft->filenumber, TOX_FILE_CONTROL_RESUME, &err); if (err != TOX_ERR_FILE_CONTROL_OK) { goto on_recv_error; } line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Saving file [%ld] as: '%s'", idx, ft->file_path); /* prep progress bar line */ char progline[MAX_STR_SIZE]; init_progress_bar(progline); line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "%s", progline); ft->line_id = self->chatwin->hst->line_end->id + 2; ft->state = FILE_TRANSFER_STARTED; return; on_recv_error: switch (err) { case TOX_ERR_FILE_CONTROL_FRIEND_NOT_FOUND: line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "File transfer failed: Friend not found."); return; case TOX_ERR_FILE_CONTROL_FRIEND_NOT_CONNECTED: line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "File transfer failed: Friend is not online."); return; case TOX_ERR_FILE_CONTROL_NOT_FOUND: line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "File transfer failed: Invalid filenumber."); return; case TOX_ERR_FILE_CONTROL_SENDQ: line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "File transfer failed: Connection error."); return; default: line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "File transfer failed (error %d)\n", err); return; } } void cmd_sendfile(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); const char *errmsg = NULL; if (argc < 1) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "File path required."); return; } char path[MAX_STR_SIZE]; snprintf(path, sizeof(path), "%s", argv[1]); int path_len = strlen(path); if (path_len >= MAX_STR_SIZE) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "File path exceeds character limit."); return; } FILE *file_to_send = fopen(path, "r"); if (file_to_send == NULL) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "File `%s` not found.", path); return; } off_t filesize = file_size(path); if (filesize == 0) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Invalid file."); fclose(file_to_send); return; } char file_name[TOX_MAX_FILENAME_LENGTH]; size_t namelen = get_file_name(file_name, sizeof(file_name), path); Tox_Err_File_Send err; uint32_t filenum = tox_file_send(m, self->num, TOX_FILE_KIND_DATA, (uint64_t) filesize, NULL, (uint8_t *) file_name, namelen, &err); if (err != TOX_ERR_FILE_SEND_OK) { goto on_send_error; } FileTransfer *ft = new_file_transfer(self, self->num, filenum, FILE_TRANSFER_SEND, TOX_FILE_KIND_DATA); if (!ft) { err = TOX_ERR_FILE_SEND_TOO_MANY; goto on_send_error; } memcpy(ft->file_name, file_name, namelen + 1); ft->file = file_to_send; ft->file_size = filesize; tox_file_get_file_id(m, self->num, filenum, ft->file_id, NULL); char sizestr[32]; bytes_convert_str(sizestr, sizeof(sizestr), filesize); line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Sending file [%d]: '%s' (%s)", filenum, file_name, sizestr); return; on_send_error: fclose(file_to_send); switch (err) { case TOX_ERR_FILE_SEND_FRIEND_NOT_FOUND: { errmsg = "File transfer failed: Invalid friend."; break; } case TOX_ERR_FILE_SEND_FRIEND_NOT_CONNECTED: { int queue_idx = file_send_queue_add(self->num, path, path_len); char msg[MAX_STR_SIZE]; switch (queue_idx) { case -1: { snprintf(msg, sizeof(msg), "Invalid file name: path is null or length is zero."); break; } case -2: { snprintf(msg, sizeof(msg), "File name is too long."); break; } case -3: { snprintf(msg, sizeof(msg), "File send queue is full."); break; } default: { snprintf(msg, sizeof(msg), "File transfer queued. Type \"/cancel out %d\" to cancel.", queue_idx); break; } } line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "%s", msg); return; } case TOX_ERR_FILE_SEND_NAME_TOO_LONG: { errmsg = "File transfer failed: Filename is too long."; break; } case TOX_ERR_FILE_SEND_TOO_MANY: { errmsg = "File transfer failed: Too many concurrent file transfers."; break; } default: { errmsg = "File transfer failed."; break; } } line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "%s", errmsg); tox_file_control(m, self->num, filenum, TOX_FILE_CONTROL_CANCEL, NULL); } toxic-0.11.3/src/chat_commands.h000066400000000000000000000047171416141666600165060ustar00rootroot00000000000000/* chat_commands.h * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef CHAT_COMMANDS_H #define CHAT_COMMANDS_H #include "toxic.h" #include "windows.h" void cmd_cancelfile(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_conference_invite(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_conference_join(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_game_join(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_savefile(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_sendfile(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); #ifdef AUDIO void cmd_call(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_answer(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_reject(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_hangup(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_cancel(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_ccur_device(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_mute(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_sense(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_bitrate(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); #endif /* AUDIO */ #ifdef VIDEO void cmd_vcall(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_video(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_res(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); #endif /* VIDEO */ #endif /* CHAT_COMMANDS_H */ toxic-0.11.3/src/conference.c000066400000000000000000001202701416141666600160010ustar00rootroot00000000000000/* conference.c * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef _GNU_SOURCE #define _GNU_SOURCE /* needed for strcasestr() and wcswidth() */ #endif #include #include #include #include #include #include #include #include #ifdef AUDIO #ifdef __APPLE__ #include #include #else #include #include /* compatibility with older versions of OpenAL */ #ifndef ALC_ALL_DEVICES_SPECIFIER #include #endif /* ALC_ALL_DEVICES_SPECIFIER */ #endif /* __APPLE__ */ #endif /* AUDIO */ #include "audio_device.h" #include "autocomplete.h" #include "conference.h" #include "execute.h" #include "help.h" #include "input.h" #include "line_info.h" #include "log.h" #include "misc_tools.h" #include "notify.h" #include "prompt.h" #include "settings.h" #include "toxic.h" #include "toxic_strings.h" #include "windows.h" #define MAX_CONFERENCE_NUM (MAX_WINDOWS_NUM - 2) #define CONFERENCE_EVENT_WAIT 30 static ConferenceChat conferences[MAX_CONFERENCE_NUM]; static int max_conference_index = 0; extern struct user_settings *user_settings; extern struct Winthread Winthread; /* Array of conference command names used for tab completion. */ static const char *conference_cmd_list[] = { "/accept", "/add", #ifdef AUDIO "/audio", #endif "/avatar", "/clear", "/close", "/connect", "/decline", "/exit", "/conference", #ifdef GAMES "/game", #endif "/help", "/log", #ifdef AUDIO "/mute", #endif "/myid", #ifdef QRCODE "/myqr", #endif /* QRCODE */ "/nick", "/note", "/nospam", "/quit", "/requests", #ifdef AUDIO "/ptt", "/sense", #endif "/status", "/title", #ifdef PYTHON "/run", #endif /* PYTHON */ }; static ToxWindow *new_conference_chat(uint32_t conferencenum); void conference_set_title(ToxWindow *self, uint32_t conferencesnum, const char *title, size_t length) { ConferenceChat *chat = &conferences[conferencesnum]; if (!chat->active) { return; } if (length > CONFERENCE_MAX_TITLE_LENGTH) { length = CONFERENCE_MAX_TITLE_LENGTH; } memcpy(chat->title, title, length); chat->title[length] = 0; chat->title_length = length; set_window_title(self, title, length); } static void kill_conference_window(ToxWindow *self) { ChatContext *ctx = self->chatwin; log_disable(ctx->log); line_info_cleanup(ctx->hst); delwin(ctx->linewin); delwin(ctx->history); delwin(ctx->sidebar); free(ctx->log); free(ctx); free(self->help); kill_notifs(self->active_box); del_window(self); } static void init_conference_logging(ToxWindow *self, Tox *m, uint32_t conferencenum) { ChatContext *ctx = self->chatwin; char my_id[TOX_ADDRESS_SIZE]; tox_self_get_address(m, (uint8_t *) my_id); char conference_id[TOX_CONFERENCE_ID_SIZE]; tox_conference_get_id(m, conferencenum, (uint8_t *) conference_id); if (log_init(ctx->log, conferences[self->num].title, my_id, conference_id, LOG_TYPE_CHAT) != 0) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Warning: Log failed to initialize."); return; } if (load_chat_history(self, ctx->log) != 0) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Failed to load chat history."); } if (user_settings->autolog == AUTOLOG_ON) { if (log_enable(ctx->log) != 0) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Failed to enable chat log."); } } execute(ctx->history, self, m, "/log", GLOBAL_COMMAND_MODE); // print log state to screen } int init_conference_win(Tox *m, uint32_t conferencenum, uint8_t type, const char *title, size_t length) { if (conferencenum > MAX_CONFERENCE_NUM) { return -1; } ToxWindow *self = new_conference_chat(conferencenum); for (int i = 0; i <= max_conference_index; ++i) { if (!conferences[i].active) { // FIXME: it is assumed at various points in the code that // toxcore's conferencenums agree with toxic's indices to conferences; // probably it so happens that this will (at least typically) be // the case, because toxic and tox maintain the indices in // parallel ways. But it isn't guaranteed by the API. conferences[i].chatwin = add_window(m, self); conferences[i].active = true; conferences[i].num_peers = 0; conferences[i].type = type; conferences[i].start_time = get_unix_time(); conferences[i].audio_enabled = false; conferences[i].last_sent_audio = 0; #ifdef AUDIO conferences[i].push_to_talk_enabled = user_settings->push_to_talk; #endif set_active_window_index(conferences[i].chatwin); conference_set_title(self, conferencenum, title, length); init_conference_logging(self, m, conferencenum); if (i == max_conference_index) { ++max_conference_index; } return conferences[i].chatwin; } } kill_conference_window(self); return -1; } static void free_peer(ConferencePeer *peer) { #ifdef AUDIO if (peer->sending_audio) { close_device(output, peer->audio_out_idx); } #endif } void free_conference(ToxWindow *self, uint32_t conferencenum) { ConferenceChat *chat = &conferences[conferencenum]; for (uint32_t i = 0; i < chat->num_peers; ++i) { ConferencePeer *peer = &chat->peer_list[i]; if (peer->active) { free_peer(peer); } } #ifdef AUDIO if (chat->audio_enabled) { close_device(input, chat->audio_in_idx); } #endif free(chat->name_list); free(chat->peer_list); conferences[conferencenum] = (ConferenceChat) { 0 }; int i; for (i = max_conference_index; i > 0; --i) { if (conferences[i - 1].active) { break; } } max_conference_index = i; kill_conference_window(self); } static void delete_conference(ToxWindow *self, Tox *m, uint32_t conferencenum) { tox_conference_delete(m, conferencenum, NULL); free_conference(self, conferencenum); } void conference_rename_log_path(Tox *m, uint32_t conferencenum, const char *new_title) { ConferenceChat *chat = &conferences[conferencenum]; if (!chat->active) { return; } char myid[TOX_ADDRESS_SIZE]; tox_self_get_address(m, (uint8_t *) myid); char conference_id[TOX_CONFERENCE_ID_SIZE]; tox_conference_get_id(m, conferencenum, (uint8_t *) conference_id); if (rename_logfile(chat->title, new_title, myid, conference_id, chat->chatwin) != 0) { fprintf(stderr, "Failed to rename conference log to `%s`\n", new_title); } } /* destroys and re-creates conference window with or without the peerlist */ void redraw_conference_win(ToxWindow *self) { ChatContext *ctx = self->chatwin; endwin(); refresh(); clear(); int x2; int y2; getmaxyx(self->window, y2, x2); if (y2 <= 0 || x2 <= 0) { return; } if (ctx->sidebar) { delwin(ctx->sidebar); ctx->sidebar = NULL; } delwin(ctx->linewin); delwin(ctx->history); delwin(self->window_bar); delwin(self->window); self->window = newwin(y2, x2, 0, 0); ctx->linewin = subwin(self->window, CHATBOX_HEIGHT, x2, y2 - CHATBOX_HEIGHT, 0); self->window_bar = subwin(self->window, WINDOW_BAR_HEIGHT, x2, y2 - (CHATBOX_HEIGHT + WINDOW_BAR_HEIGHT), 0); if (self->show_peerlist) { ctx->history = subwin(self->window, y2 - CHATBOX_HEIGHT - WINDOW_BAR_HEIGHT, x2 - SIDEBAR_WIDTH - 1, 0, 0); ctx->sidebar = subwin(self->window, y2 - CHATBOX_HEIGHT - WINDOW_BAR_HEIGHT, SIDEBAR_WIDTH, 0, x2 - SIDEBAR_WIDTH); } else { ctx->history = subwin(self->window, y2 - CHATBOX_HEIGHT - WINDOW_BAR_HEIGHT, x2, 0, 0); } scrollok(ctx->history, 0); wmove(self->window, y2 - CURS_Y_OFFSET, 0); } static void conference_onConferenceMessage(ToxWindow *self, Tox *m, uint32_t conferencenum, uint32_t peernum, Tox_Message_Type type, const char *msg, size_t len) { UNUSED_VAR(len); if (self->num != conferencenum) { return; } ChatContext *ctx = self->chatwin; char nick[TOX_MAX_NAME_LENGTH]; get_conference_nick_truncate(m, nick, peernum, conferencenum); char selfnick[TOX_MAX_NAME_LENGTH]; tox_self_get_name(m, (uint8_t *) selfnick); size_t sn_len = tox_self_get_name_size(m); selfnick[sn_len] = '\0'; int nick_clr = strcmp(nick, selfnick) == 0 ? GREEN : CYAN; /* Only play sound if mentioned by someone else */ if (strcasestr(msg, selfnick) && strcmp(selfnick, nick)) { if (self->active_box != -1) { box_notify2(self, generic_message, NT_WNDALERT_0 | NT_NOFOCUS | user_settings->bell_on_message, self->active_box, "%s %s", nick, msg); } else { box_notify(self, generic_message, NT_WNDALERT_0 | NT_NOFOCUS | user_settings->bell_on_message, &self->active_box, self->name, "%s %s", nick, msg); } nick_clr = RED; } else { sound_notify(self, silent, NT_WNDALERT_1, NULL); } line_info_add(self, true, nick, NULL, type == TOX_MESSAGE_TYPE_NORMAL ? IN_MSG : IN_ACTION, 0, nick_clr, "%s", msg); write_to_log(msg, nick, ctx->log, false); } static void conference_onConferenceTitleChange(ToxWindow *self, Tox *m, uint32_t conferencenum, uint32_t peernum, const char *title, size_t length) { ChatContext *ctx = self->chatwin; if (self->num != conferencenum) { return; } ConferenceChat *chat = &conferences[conferencenum]; if (!chat->active) { return; } conference_rename_log_path(m, conferencenum, title); // must be called first conference_set_title(self, conferencenum, title, length); /* don't announce title when we join the room */ if (!timed_out(conferences[conferencenum].start_time, CONFERENCE_EVENT_WAIT)) { return; } char nick[TOX_MAX_NAME_LENGTH]; get_conference_nick_truncate(m, nick, peernum, conferencenum); line_info_add(self, true, nick, NULL, NAME_CHANGE, 0, 0, " set the conference title to: %s", title); char tmp_event[MAX_STR_SIZE]; snprintf(tmp_event, sizeof(tmp_event), "set title to %s", title); write_to_log(tmp_event, nick, ctx->log, true); } /* Puts `(NameListEntry *)`s in `entries` for each matched peer, up to a * maximum of `maxpeers`. * Maches each peer whose name or pubkey begins with `prefix`. * If `prefix` is exactly the pubkey of a peer, matches only that peer. * return number of entries placed in `entries`. */ uint32_t get_name_list_entries_by_prefix(uint32_t conferencenum, const char *prefix, NameListEntry **entries, uint32_t maxpeers) { ConferenceChat *chat = &conferences[conferencenum]; if (!chat->active) { return 0; } const size_t len = strlen(prefix); if (len == 2 * TOX_PUBLIC_KEY_SIZE) { for (uint32_t i = 0; i < chat->num_peers; ++i) { NameListEntry *entry = &chat->name_list[i]; if (strcasecmp(prefix, entry->pubkey_str) == 0) { entries[0] = entry; return 1; } } } uint32_t n = 0; for (uint32_t i = 0; i < chat->num_peers; ++i) { NameListEntry *entry = &chat->name_list[i]; if (strncmp(prefix, entry->name, len) == 0 || strncasecmp(prefix, entry->pubkey_str, len) == 0) { entries[n] = entry; ++n; if (n == maxpeers) { return n; } } } return n; } static int compare_name_list_entries(const void *a, const void *b) { const int cmp1 = qsort_strcasecmp_hlpr( ((const NameListEntry *)a)->name, ((const NameListEntry *)b)->name); if (cmp1 == 0) { return qsort_strcasecmp_hlpr( ((const NameListEntry *)a)->pubkey_str, ((const NameListEntry *)b)->pubkey_str); } return cmp1; } static void conference_update_name_list(uint32_t conferencenum) { ConferenceChat *chat = &conferences[conferencenum]; if (!chat->active) { return; } if (chat->name_list) { free(chat->name_list); } chat->name_list = malloc(chat->num_peers * sizeof(NameListEntry)); if (chat->name_list == NULL) { exit_toxic_err("failed in conference_update_name_list", FATALERR_MEMORY); } uint32_t count = 0; for (uint32_t i = 0; i < chat->max_idx; ++i) { const ConferencePeer *peer = &chat->peer_list[i]; NameListEntry *entry = &chat->name_list[count]; if (peer->active) { memcpy(entry->name, peer->name, peer->name_length + 1); tox_pk_bytes_to_str(peer->pubkey, sizeof(peer->pubkey), entry->pubkey_str, sizeof(entry->pubkey_str)); entry->peernum = i; ++count; } } if (count != chat->num_peers) { fprintf(stderr, "WARNING: count != chat->num_peers\n"); } qsort(chat->name_list, count, sizeof(NameListEntry), compare_name_list_entries); } /* Reallocates conferencenum's peer list. * * Returns 0 on success. * Returns -1 on failure. */ static int realloc_peer_list(ConferenceChat *chat, uint32_t num_peers) { if (!chat) { return -1; } if (num_peers == 0) { free(chat->peer_list); chat->peer_list = NULL; return 0; } ConferencePeer *tmp_list = realloc(chat->peer_list, num_peers * sizeof(ConferencePeer)); if (!tmp_list) { return -1; } chat->peer_list = tmp_list; return 0; } /* return NULL if peer or conference doesn't exist */ static ConferencePeer *peer_in_conference(uint32_t conferencenum, uint32_t peernum) { if (conferencenum >= MAX_CONFERENCE_NUM) { return NULL; } const ConferenceChat *chat = &conferences[conferencenum]; if (!chat->active || peernum > chat->max_idx) { return NULL; } ConferencePeer *peer = &chat->peer_list[peernum]; if (!peer->active) { return NULL; } return peer; } #ifdef AUDIO /* Return true if ptt is disabled or enabled and active. */ static bool conference_check_push_to_talk(ConferenceChat *chat) { if (!chat->push_to_talk_enabled) { return true; } return !timed_out(chat->ptt_last_pushed, 1); } static void conference_enable_push_to_talk(ConferenceChat *chat) { chat->ptt_last_pushed = get_unix_time(); } static void set_peer_audio_position(Tox *m, uint32_t conferencenum, uint32_t peernum) { ConferenceChat *chat = &conferences[conferencenum]; ConferencePeer *peer = &chat->peer_list[peernum]; if (peer == NULL || !peer->sending_audio) { return; } // Position peers at distance 1 in front of listener, // ordered left to right by order in peerlist excluding self. uint32_t num_posns = chat->num_peers; uint32_t peer_posn = peernum; for (uint32_t i = 0; i < chat->num_peers; ++i) { if (tox_conference_peer_number_is_ours(m, conferencenum, peernum, NULL)) { if (i == peernum) { return; } --num_posns; if (i < peernum) { --peer_posn; } } } const float angle = asinf(peer_posn - (float)(num_posns - 1) / 2); set_source_position(peer->audio_out_idx, sinf(angle), cosf(angle), 0); } #endif // AUDIO static bool find_peer_by_pubkey(const ConferencePeer *list, uint32_t num_peers, uint8_t *pubkey, uint32_t *idx) { for (uint32_t i = 0; i < num_peers; ++i) { const ConferencePeer *peer = &list[i]; if (peer->active && memcmp(peer->pubkey, pubkey, TOX_PUBLIC_KEY_SIZE) == 0) { if (idx) { *idx = i; } return true; } } return false; } static void update_peer_list(ToxWindow *self, Tox *m, uint32_t conferencenum, uint32_t num_peers, uint32_t old_num_peers) { ConferenceChat *chat = &conferences[conferencenum]; if (!chat->active) { return; } ChatContext *ctx = self->chatwin; ConferencePeer *old_peer_list = malloc(old_num_peers * sizeof(ConferencePeer)); if (!old_peer_list) { exit_toxic_err("failed in update_peer_list", FATALERR_MEMORY); return; } if (chat->peer_list != NULL) { memcpy(old_peer_list, chat->peer_list, old_num_peers * sizeof(ConferencePeer)); } if (realloc_peer_list(chat, num_peers) != 0) { free(old_peer_list); fprintf(stderr, "Warning: realloc_peer_list() failed in update_peer_list()\n"); return; } for (uint32_t i = 0; i < num_peers; ++i) { ConferencePeer *peer = &chat->peer_list[i]; *peer = (struct ConferencePeer) { 0 }; Tox_Err_Conference_Peer_Query err; tox_conference_peer_get_public_key(m, conferencenum, i, peer->pubkey, &err); if (err != TOX_ERR_CONFERENCE_PEER_QUERY_OK) { continue; } bool new_peer = true; uint32_t j; if (find_peer_by_pubkey(old_peer_list, old_num_peers, peer->pubkey, &j)) { ConferencePeer *old_peer = &old_peer_list[j]; memcpy(peer, old_peer, sizeof(ConferencePeer)); old_peer->active = false; new_peer = false; } size_t length = tox_conference_peer_get_name_size(m, conferencenum, i, &err); if (err != TOX_ERR_CONFERENCE_PEER_QUERY_OK || length >= TOX_MAX_NAME_LENGTH) { // FIXME: length == TOX_MAX_NAME_LENGTH should not be an error! continue; } tox_conference_peer_get_name(m, conferencenum, i, (uint8_t *) peer->name, &err); peer->name[length] = 0; if (err != TOX_ERR_CONFERENCE_PEER_QUERY_OK) { continue; } peer->active = true; peer->name_length = length; peer->peernum = i; if (new_peer && peer->name_length > 0 && timed_out(chat->start_time, CONFERENCE_EVENT_WAIT)) { const char *msg = "has joined the conference"; line_info_add(self, true, peer->name, NULL, CONNECTION, 0, GREEN, msg); write_to_log(msg, peer->name, ctx->log, true); } #ifdef AUDIO set_peer_audio_position(m, conferencenum, i); #endif } conference_update_name_list(conferencenum); for (uint32_t i = 0; i < old_num_peers; ++i) { ConferencePeer *old_peer = &old_peer_list[i]; if (old_peer->active) { if (old_peer->name_length > 0 && !find_peer_by_pubkey(chat->peer_list, chat->num_peers, old_peer->pubkey, NULL)) { const char *msg = "has left the conference"; line_info_add(self, true, old_peer->name, NULL, DISCONNECTION, 0, RED, msg); write_to_log(msg, old_peer->name, ctx->log, true); } free_peer(old_peer); } } free(old_peer_list); } static void conference_onConferenceNameListChange(ToxWindow *self, Tox *m, uint32_t conferencenum) { if (self->num != conferencenum) { return; } if (conferencenum > max_conference_index) { return; } ConferenceChat *chat = &conferences[conferencenum]; if (!chat->active) { return; } Tox_Err_Conference_Peer_Query err; uint32_t num_peers = tox_conference_peer_count(m, conferencenum, &err); if (err != TOX_ERR_CONFERENCE_PEER_QUERY_OK) { fprintf(stderr, "conference_onConferenceNameListChange() failed with error: %d\n", err); return; } const uint32_t old_num = chat->num_peers; chat->num_peers = num_peers; chat->max_idx = num_peers; update_peer_list(self, m, conferencenum, num_peers, old_num); } static void conference_onConferencePeerNameChange(ToxWindow *self, Tox *m, uint32_t conferencenum, uint32_t peernum, const char *name, size_t length) { UNUSED_VAR(length); if (self->num != conferencenum) { return; } const ConferencePeer *peer = peer_in_conference(conferencenum, peernum); if (peer != NULL) { ChatContext *ctx = self->chatwin; if (peer->name_length > 0) { char log_event[TOXIC_MAX_NAME_LENGTH * 2 + 32]; line_info_add(self, true, peer->name, (const char *) name, NAME_CHANGE, 0, 0, " is now known as "); snprintf(log_event, sizeof(log_event), "is now known as %s", (const char *) name); write_to_log(log_event, peer->name, ctx->log, true); // this is kind of a hack; peers always join a group with no name set and then set it after } else if (timed_out(conferences[conferencenum].start_time, CONFERENCE_EVENT_WAIT)) { const char *msg = "has joined the conference"; line_info_add(self, true, name, NULL, CONNECTION, 0, GREEN, msg); write_to_log(msg, name, ctx->log, true); } } conference_onConferenceNameListChange(self, m, conferencenum); } static void send_conference_action(ToxWindow *self, ChatContext *ctx, Tox *m, char *action) { if (action == NULL) { wprintw(ctx->history, "Invalid syntax.\n"); return; } Tox_Err_Conference_Send_Message err; if (!tox_conference_send_message(m, self->num, TOX_MESSAGE_TYPE_ACTION, (uint8_t *) action, strlen(action), &err)) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, RED, " * Failed to send action (error %d)", err); } } /* Offset for the peer number box at the top of the statusbar */ static int sidebar_offset(uint32_t conferencenum) { return 2 + conferences[conferencenum].audio_enabled; } /* * Return true if input is recognized by handler */ static bool conference_onKey(ToxWindow *self, Tox *m, wint_t key, bool ltr) { ChatContext *ctx = self->chatwin; int x, y, y2, x2; getyx(self->window, y, x); getmaxyx(self->window, y2, x2); UNUSED_VAR(y); if (x2 <= 0 || y2 <= 0) { return false; } if (self->help->active) { help_onKey(self, key); return true; } if (ctx->pastemode && key == L'\r') { key = L'\n'; } if (ltr || key == L'\n') { /* char is printable */ input_new_char(self, key, x, x2); return true; } if (line_info_onKey(self, key)) { return true; } if (input_handle(self, key, x, x2)) { return true; } bool input_ret = false; ConferenceChat *chat = &conferences[self->num]; #ifdef AUDIO if (chat->audio_enabled && chat->push_to_talk_enabled && key == KEY_F(2)) { input_ret = true; conference_enable_push_to_talk(chat); } #endif // AUDIO if (key == L'\t') { /* TAB key: auto-completes peer name or command */ input_ret = true; if (ctx->len > 0) { int diff = -1; /* TODO: make this not suck */ if (ctx->line[0] != L'/' || wcscmp(ctx->line, L"/me") == 0) { const char **complete_strs = calloc(chat->num_peers, sizeof(const char *)); if (complete_strs) { for (uint32_t i = 0; i < chat->num_peers; ++i) { complete_strs[i] = (const char *) chat->name_list[i].name; } diff = complete_line(self, complete_strs, chat->num_peers); free(complete_strs); } } else if (wcsncmp(ctx->line, L"/avatar ", wcslen(L"/avatar ")) == 0) { diff = dir_match(self, m, ctx->line, L"/avatar"); } #ifdef PYTHON else if (wcsncmp(ctx->line, L"/run ", wcslen(L"/run ")) == 0) { diff = dir_match(self, m, ctx->line, L"/run"); } #endif else if (wcsncmp(ctx->line, L"/mute ", wcslen(L"/mute ")) == 0) { const char **complete_strs = calloc(chat->num_peers, sizeof(const char *)); if (complete_strs) { for (uint32_t i = 0; i < chat->num_peers; ++i) { complete_strs[i] = (const char *) chat->name_list[i].name; } diff = complete_line(self, complete_strs, chat->num_peers); if (diff == -1) { for (uint32_t i = 0; i < chat->num_peers; ++i) { complete_strs[i] = (const char *) chat->name_list[i].pubkey_str; } diff = complete_line(self, complete_strs, chat->num_peers); } free(complete_strs); } } else { diff = complete_line(self, conference_cmd_list, sizeof(conference_cmd_list) / sizeof(char *)); } if (diff != -1) { if (x + diff > x2 - 1) { int wlen = MAX(0, wcswidth(ctx->line, sizeof(ctx->line) / sizeof(wchar_t))); ctx->start = wlen < x2 ? 0 : wlen - x2 + 1; } } else { sound_notify(self, notif_error, 0, NULL); } } else { sound_notify(self, notif_error, 0, NULL); } } else if (key == T_KEY_C_DOWN) { /* Scroll peerlist up and down one position */ input_ret = true; const int L = y2 - CHATBOX_HEIGHT - sidebar_offset(self->num); if (chat->side_pos < (int64_t) chat->num_peers - L) { ++chat->side_pos; } } else if (key == T_KEY_C_UP) { input_ret = true; if (chat->side_pos > 0) { --chat->side_pos; } } else if (key == L'\r') { input_ret = true; rm_trailing_spaces_buf(ctx); if (!wstring_is_empty(ctx->line)) { add_line_to_hist(ctx); wstrsubst(ctx->line, L'¶', L'\n'); char line[MAX_STR_SIZE]; if (wcs_to_mbs_buf(line, ctx->line, MAX_STR_SIZE) == -1) { memset(line, 0, sizeof(line)); } if (line[0] == '/') { if (strcmp(line, "/close") == 0) { delete_conference(self, m, self->num); return true; } else if (strncmp(line, "/me ", strlen("/me ")) == 0) { send_conference_action(self, ctx, m, line + strlen("/me ")); } else { execute(ctx->history, self, m, line, CONFERENCE_COMMAND_MODE); } } else if (line[0]) { Tox_Err_Conference_Send_Message err; if (!tox_conference_send_message(m, self->num, TOX_MESSAGE_TYPE_NORMAL, (uint8_t *) line, strlen(line), &err)) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, RED, " * Failed to send message (error %d)", err); } } else { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, RED, " * Failed to parse message."); } } wclear(ctx->linewin); wmove(self->window, y2, 0); reset_buf(ctx); } return input_ret; } static void draw_peer(ToxWindow *self, Tox *m, ChatContext *ctx, uint32_t i) { pthread_mutex_lock(&Winthread.lock); const uint32_t peer_idx = i + conferences[self->num].side_pos; const uint32_t peernum = conferences[self->num].name_list[peer_idx].peernum; const bool is_self = tox_conference_peer_number_is_ours(m, self->num, peernum, NULL); const bool audio = conferences[self->num].audio_enabled; if (audio) { #ifdef AUDIO const ConferencePeer *peer = peer_in_conference(self->num, peernum); const bool audio_active = is_self ? !timed_out(conferences[self->num].last_sent_audio, 2) : peer != NULL && peer->sending_audio && !timed_out(peer->last_audio_time, 2); const bool mute = audio_active && (is_self ? device_is_muted(input, conferences[self->num].audio_in_idx) : peer != NULL && device_is_muted(output, peer->audio_out_idx)); pthread_mutex_unlock(&Winthread.lock); const int aud_attr = A_BOLD | COLOR_PAIR(audio_active && !mute ? GREEN : RED); wattron(ctx->sidebar, aud_attr); waddch(ctx->sidebar, audio_active ? (mute ? 'M' : '*') : '-'); wattroff(ctx->sidebar, aud_attr); waddch(ctx->sidebar, ' '); #endif } else { pthread_mutex_unlock(&Winthread.lock); } /* truncate nick to fit in side panel without modifying list */ char tmpnick[TOX_MAX_NAME_LENGTH]; int maxlen = SIDEBAR_WIDTH - 2 - 2 * audio; pthread_mutex_lock(&Winthread.lock); memcpy(tmpnick, &conferences[self->num].name_list[peer_idx].name, maxlen); pthread_mutex_unlock(&Winthread.lock); tmpnick[maxlen] = '\0'; if (is_self) { wattron(ctx->sidebar, COLOR_PAIR(GREEN)); } wprintw(ctx->sidebar, "%s\n", tmpnick); if (is_self) { wattroff(ctx->sidebar, COLOR_PAIR(GREEN)); } } static void conference_onDraw(ToxWindow *self, Tox *m) { UNUSED_VAR(m); int x2, y2; getmaxyx(self->window, y2, x2); if (x2 <= 0 || y2 <= 0) { return; } ConferenceChat *chat = &conferences[self->num]; if (!chat->active) { return; } ChatContext *ctx = self->chatwin; pthread_mutex_lock(&Winthread.lock); line_info_print(self); pthread_mutex_unlock(&Winthread.lock); wclear(ctx->linewin); curs_set(1); if (ctx->len > 0) { mvwprintw(ctx->linewin, 0, 0, "%ls", &ctx->line[ctx->start]); } wclear(ctx->sidebar); if (self->show_peerlist) { wattron(ctx->sidebar, COLOR_PAIR(BLUE)); mvwvline(ctx->sidebar, 0, 0, ACS_VLINE, y2 - CHATBOX_HEIGHT); mvwaddch(ctx->sidebar, y2 - CHATBOX_HEIGHT, 0, ACS_BTEE); wattroff(ctx->sidebar, COLOR_PAIR(BLUE)); pthread_mutex_lock(&Winthread.lock); const uint32_t num_peers = chat->num_peers; const bool audio = chat->audio_enabled; const int header_lines = sidebar_offset(self->num); pthread_mutex_unlock(&Winthread.lock); int line = 0; if (audio) { #ifdef AUDIO pthread_mutex_lock(&Winthread.lock); const bool ptt_idle = !conference_check_push_to_talk(chat) && chat->push_to_talk_enabled; const bool mic_on = !device_is_muted(input, chat->audio_in_idx); const float volume = get_input_volume(); const float threshold = device_get_VAD_threshold(chat->audio_in_idx); pthread_mutex_unlock(&Winthread.lock); wmove(ctx->sidebar, line, 1); wattron(ctx->sidebar, A_BOLD); wprintw(ctx->sidebar, "Mic: "); if (!mic_on) { wattron(ctx->sidebar, COLOR_PAIR(RED)); wprintw(ctx->sidebar, "MUTED"); wattroff(ctx->sidebar, COLOR_PAIR(RED)); } else if (ptt_idle) { wattron(ctx->sidebar, COLOR_PAIR(GREEN)); wprintw(ctx->sidebar, "PTT"); wattroff(ctx->sidebar, COLOR_PAIR(GREEN)); } else { const int color = volume > threshold ? GREEN : RED; wattron(ctx->sidebar, COLOR_PAIR(color)); float v = volume; if (v <= 0.0f) { wprintw(ctx->sidebar, "."); } while (v > 0.0f) { wprintw(ctx->sidebar, v > 10.0f ? (v > 15.0f ? "*" : "+") : (v > 5.0f ? "-" : ".")); v -= 20.0f; } wattroff(ctx->sidebar, COLOR_PAIR(color)); } wattroff(ctx->sidebar, A_BOLD); ++line; #endif // AUDIO } wmove(ctx->sidebar, line, 1); wattron(ctx->sidebar, A_BOLD); wprintw(ctx->sidebar, "Peers: %"PRIu32"\n", num_peers); wattroff(ctx->sidebar, A_BOLD); ++line; wattron(ctx->sidebar, COLOR_PAIR(BLUE)); mvwaddch(ctx->sidebar, line, 0, ACS_LTEE); mvwhline(ctx->sidebar, line, 1, ACS_HLINE, SIDEBAR_WIDTH - 1); wattroff(ctx->sidebar, COLOR_PAIR(BLUE)); ++line; for (uint32_t i = 0; i < num_peers && i < y2 - header_lines - CHATBOX_HEIGHT; ++i) { wmove(ctx->sidebar, i + header_lines, 1); draw_peer(self, m, ctx, i); } } int y, x; getyx(self->window, y, x); UNUSED_VAR(x); int new_x = ctx->start ? x2 - 1 : MAX(0, wcswidth(ctx->line, ctx->pos)); wmove(self->window, y, new_x); draw_window_bar(self); wnoutrefresh(self->window); if (self->help->active) { help_onDraw(self); } } static void conference_onInit(ToxWindow *self, Tox *m) { int x2, y2; getmaxyx(self->window, y2, x2); if (x2 <= 0 || y2 <= 0) { exit_toxic_err("failed in conference_onInit", FATALERR_CURSES); } ChatContext *ctx = self->chatwin; ctx->history = subwin(self->window, y2 - CHATBOX_HEIGHT - WINDOW_BAR_HEIGHT, x2 - SIDEBAR_WIDTH - 1, 0, 0); self->window_bar = subwin(self->window, WINDOW_BAR_HEIGHT, x2, y2 - (CHATBOX_HEIGHT + WINDOW_BAR_HEIGHT), 0); ctx->linewin = subwin(self->window, CHATBOX_HEIGHT, x2, y2 - CHATBOX_HEIGHT, 0); ctx->sidebar = subwin(self->window, y2 - CHATBOX_HEIGHT - WINDOW_BAR_HEIGHT, SIDEBAR_WIDTH, 0, x2 - SIDEBAR_WIDTH); ctx->hst = calloc(1, sizeof(struct history)); ctx->log = calloc(1, sizeof(struct chatlog)); if (ctx->log == NULL || ctx->hst == NULL) { exit_toxic_err("failed in conference_onInit", FATALERR_MEMORY); } line_info_init(ctx->hst); scrollok(ctx->history, 0); wmove(self->window, y2 - CURS_Y_OFFSET, 0); } static ToxWindow *new_conference_chat(uint32_t conferencenum) { ToxWindow *ret = calloc(1, sizeof(ToxWindow)); if (ret == NULL) { exit_toxic_err("failed in new_conference_chat", FATALERR_MEMORY); } ret->type = WINDOW_TYPE_CONFERENCE; ret->onKey = &conference_onKey; ret->onDraw = &conference_onDraw; ret->onInit = &conference_onInit; ret->onConferenceMessage = &conference_onConferenceMessage; ret->onConferenceNameListChange = &conference_onConferenceNameListChange; ret->onConferencePeerNameChange = &conference_onConferencePeerNameChange; ret->onConferenceTitleChange = &conference_onConferenceTitleChange; snprintf(ret->name, sizeof(ret->name), "Conference %u", conferencenum); ChatContext *chatwin = calloc(1, sizeof(ChatContext)); Help *help = calloc(1, sizeof(Help)); if (chatwin == NULL || help == NULL) { exit_toxic_err("failed in new_conference_chat", FATALERR_MEMORY); } ret->chatwin = chatwin; ret->help = help; ret->num = conferencenum; ret->show_peerlist = true; ret->active_box = -1; return ret; } #ifdef AUDIO #define CONFAV_SAMPLE_RATE 48000 #define CONFAV_FRAME_DURATION 20 #define CONFAV_SAMPLES_PER_FRAME (CONFAV_SAMPLE_RATE * CONFAV_FRAME_DURATION / 1000) void audio_conference_callback(void *tox, uint32_t conferencenum, uint32_t peernum, const int16_t *pcm, unsigned int samples, uint8_t channels, uint32_t sample_rate, void *userdata) { ConferencePeer *peer = peer_in_conference(conferencenum, peernum); if (peer == NULL) { return; } if (!peer->sending_audio) { if (open_output_device(&peer->audio_out_idx, sample_rate, CONFAV_FRAME_DURATION, channels) != de_None) { // TODO: error message? return; } peer->sending_audio = true; set_peer_audio_position(tox, conferencenum, peernum); } write_out(peer->audio_out_idx, pcm, samples, channels, sample_rate); peer->last_audio_time = get_unix_time(); return; } static void conference_read_device_callback(const int16_t *captured, uint32_t size, void *data) { UNUSED_VAR(size); AudioInputCallbackData *audio_input_callback_data = (AudioInputCallbackData *)data; ConferenceChat *chat = &conferences[audio_input_callback_data->conferencenum]; if (!conference_check_push_to_talk(chat)) { return; } chat->last_sent_audio = get_unix_time(); int channels = user_settings->conference_audio_channels; toxav_group_send_audio(audio_input_callback_data->tox, audio_input_callback_data->conferencenum, captured, CONFAV_SAMPLES_PER_FRAME, channels, CONFAV_SAMPLE_RATE); } bool init_conference_audio_input(Tox *tox, uint32_t conferencenum) { ConferenceChat *chat = &conferences[conferencenum]; if (!chat->active || chat->audio_enabled) { return false; } const AudioInputCallbackData audio_input_callback_data = { tox, conferencenum }; chat->audio_input_callback_data = audio_input_callback_data; int channels = user_settings->conference_audio_channels; bool success = (open_input_device(&chat->audio_in_idx, conference_read_device_callback, &chat->audio_input_callback_data, CONFAV_SAMPLE_RATE, CONFAV_FRAME_DURATION, channels) == de_None); chat->audio_enabled = success; return success; } bool toggle_conference_push_to_talk(uint32_t conferencenum, bool enabled) { ConferenceChat *chat = &conferences[conferencenum]; if (!chat->active) { return false; } chat->push_to_talk_enabled = enabled; return true; } bool enable_conference_audio(ToxWindow *self, Tox *tox, uint32_t conferencenum) { if (!toxav_groupchat_av_enabled(tox, conferencenum)) { if (toxav_groupchat_enable_av(tox, conferencenum, audio_conference_callback, NULL) != 0) { return false; } } bool success = init_conference_audio_input(tox, conferencenum); if (success) { self->is_call = true; } return success; } bool disable_conference_audio(ToxWindow *self, Tox *tox, uint32_t conferencenum) { ConferenceChat *chat = &conferences[conferencenum]; if (!chat->active) { return false; } if (chat->audio_enabled) { close_device(input, chat->audio_in_idx); chat->audio_enabled = false; } bool success = toxav_groupchat_disable_av(tox, conferencenum) == 0; if (success) { self->is_call = false; } return success; } bool conference_mute_self(uint32_t conferencenum) { const ConferenceChat *chat = &conferences[conferencenum]; if (!chat->active || !chat->audio_enabled) { return false; } device_mute(input, chat->audio_in_idx); return true; } bool conference_mute_peer(const Tox *m, uint32_t conferencenum, uint32_t peernum) { if (tox_conference_peer_number_is_ours(m, conferencenum, peernum, NULL)) { return conference_mute_self(conferencenum); } const ConferenceChat *chat = &conferences[conferencenum]; if (!chat->active || !chat->audio_enabled || peernum > chat->max_idx) { return false; } const ConferencePeer *peer = peer_in_conference(conferencenum, peernum); if (peer == NULL || !peer->sending_audio) { return false; } device_mute(output, peer->audio_out_idx); return true; } bool conference_set_VAD_threshold(uint32_t conferencenum, float threshold) { const ConferenceChat *chat = &conferences[conferencenum]; if (!chat->active || !chat->audio_enabled) { return false; } return (device_set_VAD_threshold(chat->audio_in_idx, threshold) == de_None); } float conference_get_VAD_threshold(uint32_t conferencenum) { const ConferenceChat *chat = &conferences[conferencenum]; if (!chat->active || !chat->audio_enabled) { return 0.0f; } return device_get_VAD_threshold(chat->audio_in_idx); } #endif // AUDIO toxic-0.11.3/src/conference.h000066400000000000000000000100351416141666600160030ustar00rootroot00000000000000/* conference.h * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef CONFERENCE_H #define CONFERENCE_H #include "toxic.h" #include "windows.h" #define CONFERENCE_MAX_TITLE_LENGTH TOX_MAX_NAME_LENGTH #define SIDEBAR_WIDTH 16 typedef struct ConferencePeer { bool active; uint8_t pubkey[TOX_PUBLIC_KEY_SIZE]; uint32_t peernum; /* index in chat->peer_list */ char name[TOX_MAX_NAME_LENGTH]; size_t name_length; bool sending_audio; uint32_t audio_out_idx; time_t last_audio_time; } ConferencePeer; typedef struct AudioInputCallbackData { Tox *tox; uint32_t conferencenum; } AudioInputCallbackData; #define PUBKEY_STRING_SIZE (2 * TOX_PUBLIC_KEY_SIZE + 1) typedef struct NameListEntry { char name[TOX_MAX_NAME_LENGTH]; char pubkey_str[PUBKEY_STRING_SIZE]; uint32_t peernum; } NameListEntry; typedef struct { int chatwin; bool active; uint8_t type; int side_pos; /* current position of the sidebar - used for scrolling up and down */ time_t start_time; char title[CONFERENCE_MAX_TITLE_LENGTH + 1]; size_t title_length; ConferencePeer *peer_list; uint32_t max_idx; NameListEntry *name_list; uint32_t num_peers; bool push_to_talk_enabled; time_t ptt_last_pushed; bool audio_enabled; time_t last_sent_audio; uint32_t audio_in_idx; AudioInputCallbackData audio_input_callback_data; } ConferenceChat; /* Frees all Toxic associated data structures for a conference (does not call tox_conference_delete() ) */ void free_conference(ToxWindow *self, uint32_t conferencenum); int init_conference_win(Tox *m, uint32_t conferencenum, uint8_t type, const char *title, size_t length); /* destroys and re-creates conference window with or without the peerlist */ void redraw_conference_win(ToxWindow *self); void conference_set_title(ToxWindow *self, uint32_t conferencesnum, const char *title, size_t length); void conference_rename_log_path(Tox *m, uint32_t conferencenum, const char *new_title); int conference_enable_logging(ToxWindow *self, Tox *m, uint32_t conferencenum, struct chatlog *log); /* Puts `(NameListEntry *)`s in `entries` for each matched peer, up to a maximum * of `maxpeers`. * Maches each peer whose name or pubkey begins with `prefix`. * If `prefix` is exactly the pubkey of a peer, matches only that peer. * return number of entries placed in `entries`. */ uint32_t get_name_list_entries_by_prefix(uint32_t conferencenum, const char *prefix, NameListEntry **entries, uint32_t maxpeers); bool init_conference_audio_input(Tox *tox, uint32_t conferencenum); bool enable_conference_audio(ToxWindow *self, Tox *tox, uint32_t conferencenum); bool disable_conference_audio(ToxWindow *self, Tox *tox, uint32_t conferencenum); bool toggle_conference_push_to_talk(uint32_t conferencenum, bool enabled); void audio_conference_callback(void *tox, uint32_t conferencenum, uint32_t peernum, const int16_t *pcm, unsigned int samples, uint8_t channels, uint32_t sample_rate, void *userdata); bool conference_mute_self(uint32_t conferencenum); bool conference_mute_peer(const Tox *m, uint32_t conferencenum, uint32_t peernum); bool conference_set_VAD_threshold(uint32_t conferencenum, float threshold); float conference_get_VAD_threshold(uint32_t conferencenum); #endif /* CONFERENCE_H */ toxic-0.11.3/src/conference_commands.c000066400000000000000000000144401416141666600176630ustar00rootroot00000000000000/* conference_commands.c * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #include #include #include "conference.h" #include "line_info.h" #include "log.h" #include "misc_tools.h" #include "toxic.h" #include "windows.h" static void print_err(ToxWindow *self, const char *error_str) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "%s", error_str); } void cmd_conference_set_title(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); Tox_Err_Conference_Title err; char title[CONFERENCE_MAX_TITLE_LENGTH + 1]; if (argc < 1) { size_t tlen = tox_conference_get_title_size(m, self->num, &err); if (err != TOX_ERR_CONFERENCE_TITLE_OK || tlen >= sizeof(title)) { print_err(self, "Title is not set"); return; } if (!tox_conference_get_title(m, self->num, (uint8_t *) title, &err)) { print_err(self, "Title is not set"); return; } title[tlen] = '\0'; line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Title is set to: %s", title); return; } size_t len = strlen(argv[1]); if (len >= sizeof(title)) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Failed to set title: max length exceeded."); return; } snprintf(title, sizeof(title), "%s", argv[1]); if (!tox_conference_set_title(m, self->num, (uint8_t *) title, len, &err)) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Failed to set title (error %d)", err); return; } conference_rename_log_path(m, self->num, title); // must be called first conference_set_title(self, self->num, title, len); char selfnick[TOX_MAX_NAME_LENGTH]; tox_self_get_name(m, (uint8_t *) selfnick); size_t sn_len = tox_self_get_name_size(m); selfnick[sn_len] = '\0'; line_info_add(self, true, selfnick, NULL, NAME_CHANGE, 0, 0, " set the conference title to: %s", title); char tmp_event[MAX_STR_SIZE + 20]; snprintf(tmp_event, sizeof(tmp_event), "set title to %s", title); write_to_log(tmp_event, selfnick, self->chatwin->log, true); } #ifdef AUDIO void cmd_enable_audio(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); bool enable; if (argc == 1 && !strcasecmp(argv[1], "on")) { enable = true; } else if (argc == 1 && !strcasecmp(argv[1], "off")) { enable = false; } else { print_err(self, "Please specify: on | off"); return; } if (enable ? enable_conference_audio(self, m, self->num) : disable_conference_audio(self, m, self->num)) { print_err(self, enable ? "Enabled conference audio. Use the '/ptt' command to toggle Push-To-Talk." : "Disabled conference audio"); } else { print_err(self, enable ? "Failed to enable audio" : "Failed to disable audio"); } } void cmd_conference_mute(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); if (argc < 1) { if (conference_mute_self(self->num)) { print_err(self, "Toggled self audio mute status"); } else { print_err(self, "No audio input to mute"); } } else { NameListEntry *entries[16]; uint32_t n = get_name_list_entries_by_prefix(self->num, argv[1], entries, 16); if (n == 0) { print_err(self, "No such peer"); return; } if (n > 1) { print_err(self, "Multiple matching peers (use /mute [public key] to disambiguate):"); for (uint32_t i = 0; i < n; ++i) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "%s: %s", entries[i]->pubkey_str, entries[i]->name); } return; } if (conference_mute_peer(m, self->num, entries[0]->peernum)) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Toggled audio mute status of %s", entries[0]->name); } else { print_err(self, "Peer is not on the call"); } } } void cmd_conference_sense(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); UNUSED_VAR(m); if (argc == 0) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Current VAD threshold: %.1f", (double) conference_get_VAD_threshold(self->num)); return; } if (argc > 1) { print_err(self, "Only one argument allowed."); return; } char *end; float value = strtof(argv[1], &end); if (*end) { print_err(self, "Invalid input"); return; } if (conference_set_VAD_threshold(self->num, value)) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Set VAD threshold to %.1f", (double) value); } else { print_err(self, "Failed to set conference audio input sensitivity."); } } void cmd_conference_push_to_talk(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); UNUSED_VAR(m); bool enable; if (argc == 1 && !strcasecmp(argv[1], "on")) { enable = true; } else if (argc == 1 && !strcasecmp(argv[1], "off")) { enable = false; } else { print_err(self, "Please specify: on | off"); return; } if (!toggle_conference_push_to_talk(self->num, enable)) { print_err(self, "Failed to toggle push to talk."); return; } print_err(self, enable ? "Push-To-Talk is enabled. Push F2 to activate" : "Push-To-Talk is disabled"); } #endif /* AUDIO */ toxic-0.11.3/src/conference_commands.h000066400000000000000000000026351416141666600176730ustar00rootroot00000000000000/* conference_commands.h * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef CONFERENCE_COMMANDS_H #define CONFERENCE_COMMANDS_H #include "toxic.h" #include "windows.h" void cmd_conference_set_title(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_enable_audio(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_conference_mute(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_conference_sense(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_conference_push_to_talk(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); #endif /* CONFERENCE_COMMANDS_H */ toxic-0.11.3/src/configdir.c000066400000000000000000000072171416141666600156430ustar00rootroot00000000000000/* configdir.c * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #include #include #include #include #include #include #include #include #include "configdir.h" #include "misc_tools.h" #include "toxic.h" /* get the user's home directory. */ void get_home_dir(char *home, int size) { struct passwd pwd; struct passwd *pwdbuf; const char *hmstr; char buf[NSS_BUFLEN_PASSWD]; int rc = getpwuid_r(getuid(), &pwd, buf, NSS_BUFLEN_PASSWD, &pwdbuf); if (rc == 0) { hmstr = pwd.pw_dir; } else { hmstr = getenv("HOME"); if (hmstr == NULL) { return; } snprintf(buf, sizeof(buf), "%s", hmstr); hmstr = buf; } snprintf(home, size, "%s", hmstr); } /** * @brief Get the user's config directory. * * This is without a trailing slash. Resulting string must be freed. * * @return The users config dir or NULL on error. */ char *get_user_config_dir(void) { char home[NSS_BUFLEN_PASSWD] = {0}; get_home_dir(home, sizeof(home)); char *user_config_dir = NULL; size_t len = 0; # if defined(__APPLE__) len = strlen(home) + strlen("/Library/Application Support") + 1; user_config_dir = malloc(len); if (user_config_dir == NULL) { return NULL; } snprintf(user_config_dir, len, "%s/Library/Application Support", home); # else /* __APPLE__ */ const char *tmp = getenv("XDG_CONFIG_HOME"); if (tmp == NULL) { len = strlen(home) + strlen("/.config") + 1; user_config_dir = malloc(len); if (user_config_dir == NULL) { return NULL; } snprintf(user_config_dir, len, "%s/.config", home); } else { user_config_dir = strdup(tmp); } # endif /* __APPLE__ */ return user_config_dir; } /* Creates the config and chatlog directories. * * Returns 0 on success. * Returns -1 on failure. */ int create_user_config_dirs(char *path) { struct stat buf; int mkdir_err = mkdir(path, 0700); if (mkdir_err && (errno != EEXIST || stat(path, &buf) || !S_ISDIR(buf.st_mode))) { return -1; } char *fullpath = malloc(strlen(path) + strlen(CONFIGDIR) + 1); char *logpath = malloc(strlen(path) + strlen(LOGDIR) + 1); if (fullpath == NULL || logpath == NULL) { exit_toxic_err("failed in load_data_structures", FATALERR_MEMORY); } strcpy(fullpath, path); strcat(fullpath, CONFIGDIR); strcpy(logpath, path); strcat(logpath, LOGDIR); mkdir_err = mkdir(fullpath, 0700); if (mkdir_err && (errno != EEXIST || stat(fullpath, &buf) || !S_ISDIR(buf.st_mode))) { free(fullpath); free(logpath); return -1; } mkdir_err = mkdir(logpath, 0700); if (mkdir_err && (errno != EEXIST || stat(logpath, &buf) || !S_ISDIR(buf.st_mode))) { free(fullpath); free(logpath); return -1; } free(logpath); free(fullpath); return 0; } toxic-0.11.3/src/configdir.h000066400000000000000000000026611416141666600156460ustar00rootroot00000000000000/* configdir.h * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef CONFIGDIR_H #define CONFIGDIR_H #ifndef NSS_BUFLEN_PASSWD #define NSS_BUFLEN_PASSWD 4096 #endif #define CONFIGDIR "/tox/" #define LOGDIR "/tox/chatlogs/" #ifndef S_ISDIR #define S_ISDIR(mode) (((mode) & S_IFMT) == S_IFDIR) #endif /** * @brief Get the user's config directory. * * This is without a trailing slash. Resulting string must be freed. * * @return The users config dir or NULL on error. */ char *get_user_config_dir(void); /* get the user's home directory. */ void get_home_dir(char *home, int size); /* Creates the config and chatlog directories. * * Returns 0 on success. * Returns -1 on failure. */ int create_user_config_dirs(char *path); #endif /* CONFIGDIR_H */ toxic-0.11.3/src/curl_util.c000066400000000000000000000051661416141666600157020ustar00rootroot00000000000000/* curl_util.c * * * Copyright (C) 2016 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #include #include #include #include #include "curl_util.h" /* Sets proxy info for given CURL handler. * * Returns 0 on success or if no proxy is set by the client. * Returns -1 if proxy info is invalid. * Returns an int > 0 on curl error (see: https://curl.haxx.se/libcurl/c/libcurl-errors.html) */ int set_curl_proxy(CURL *c_handle, const char *proxy_address, uint16_t port, uint8_t proxy_type) { if (proxy_type == TOX_PROXY_TYPE_NONE) { return 0; } if (proxy_address == NULL || port == 0) { return -1; } int ret = curl_easy_setopt(c_handle, CURLOPT_PROXYPORT, (long) port); if (ret != CURLE_OK) { return ret; } long int type = proxy_type == TOX_PROXY_TYPE_SOCKS5 ? CURLPROXY_SOCKS5_HOSTNAME : CURLPROXY_HTTP; ret = curl_easy_setopt(c_handle, CURLOPT_PROXYTYPE, type); if (ret != CURLE_OK) { return ret; } ret = curl_easy_setopt(c_handle, CURLOPT_PROXY, proxy_address); if (ret != CURLE_OK) { return ret; } return 0; } /* Callback function for CURL to write received data. * * This function will append data from an http request to the data buffer * until the request is complete or the buffer is full. Buffer will be null terminated. * * Returns number of bytes received from http request on success (don't change this). * Returns 0 if data exceeds buffer size. */ size_t curl_cb_write_data(void *data, size_t size, size_t nmemb, void *user_pointer) { struct Recv_Curl_Data *recv_data = (struct Recv_Curl_Data *) user_pointer; size_t length = size * nmemb; size_t total_size = length + recv_data->length; if (total_size > MAX_RECV_CURL_DATA_SIZE) { return 0; } memcpy(recv_data->data + recv_data->length, data, length); recv_data->data[total_size] = '\0'; recv_data->length += length; return length; } toxic-0.11.3/src/curl_util.h000066400000000000000000000050051416141666600156770ustar00rootroot00000000000000/* curl_util.h * * * Copyright (C) 2016 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef CURL_UTIL_H #define CURL_UTIL_H #include /* List based on Mozilla's recommended configurations for modern browsers */ #define TLS_CIPHER_SUITE_LIST "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK" /* Max size of an http response that we can store in Recv_Data */ #define MAX_RECV_CURL_DATA_SIZE 32767 /* Holds data received from curl lookup */ struct Recv_Curl_Data { char data[MAX_RECV_CURL_DATA_SIZE + 1]; /* Data received from curl write data callback */ size_t length; /* Total number of bytes written to data buffer (doesn't include null) */ }; /* Sets proxy info for given CURL handler. * * Returns 0 on success or if no proxy is set by the client. * Returns -1 if proxy info is invalid. * Returns an int > 0 on curl error (see: https://curl.haxx.se/libcurl/c/libcurl-errors.html) */ int set_curl_proxy(CURL *c_handle, const char *proxy_address, uint16_t port, uint8_t proxy_type); /* Callback function for CURL to write received data. * * This function will append data from an http request to the data buffer * until the request is complete or the buffer is full. Buffer will be null terminated. * * Returns size of bytes written to the data buffer. */ size_t curl_cb_write_data(void *data, size_t size, size_t nmemb, void *user_pointer); #endif /* CURL_UTIL_H */ toxic-0.11.3/src/execute.c000066400000000000000000000166321416141666600153420ustar00rootroot00000000000000/* execute.c * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #include #include #include #include "api.h" #include "chat_commands.h" #include "execute.h" #include "global_commands.h" #include "conference_commands.h" #include "line_info.h" #include "misc_tools.h" #include "notify.h" #include "toxic.h" #include "windows.h" struct cmd_func { const char *name; void (*func)(WINDOW *w, ToxWindow *, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); }; static struct cmd_func global_commands[] = { { "/accept", cmd_accept }, { "/add", cmd_add }, { "/avatar", cmd_avatar }, { "/clear", cmd_clear }, { "/connect", cmd_connect }, { "/decline", cmd_decline }, { "/exit", cmd_quit }, { "/conference", cmd_conference }, #ifdef GAMES { "/game", cmd_game }, #endif { "/help", cmd_prompt_help }, { "/log", cmd_log }, { "/myid", cmd_myid }, #ifdef QRCODE { "/myqr", cmd_myqr }, #endif /* QRCODE */ { "/nick", cmd_nick }, { "/note", cmd_note }, { "/nospam", cmd_nospam }, { "/q", cmd_quit }, { "/quit", cmd_quit }, { "/requests", cmd_requests }, { "/status", cmd_status }, #ifdef AUDIO { "/lsdev", cmd_list_devices }, { "/sdev", cmd_change_device }, #endif /* AUDIO */ #ifdef VIDEO { "/lsvdev", cmd_list_video_devices }, { "/svdev", cmd_change_video_device }, #endif /* VIDEO */ #ifdef PYTHON { "/run", cmd_run }, #endif /* PYTHON */ { NULL, NULL }, }; static struct cmd_func chat_commands[] = { { "/cancel", cmd_cancelfile }, { "/invite", cmd_conference_invite }, { "/join", cmd_conference_join }, #ifdef GAMES { "/play", cmd_game_join }, #endif { "/savefile", cmd_savefile }, { "/sendfile", cmd_sendfile }, #ifdef AUDIO { "/call", cmd_call }, { "/answer", cmd_answer }, { "/reject", cmd_reject }, { "/hangup", cmd_hangup }, { "/mute", cmd_mute }, { "/sense", cmd_sense }, { "/bitrate", cmd_bitrate }, #endif /* AUDIO */ #ifdef VIDEO { "/vcall", cmd_vcall }, { "/video", cmd_video }, { "/res", cmd_res }, #endif /* VIDEO */ { NULL, NULL }, }; static struct cmd_func conference_commands[] = { { "/title", cmd_conference_set_title }, #ifdef AUDIO { "/audio", cmd_enable_audio }, { "/mute", cmd_conference_mute }, { "/ptt", cmd_conference_push_to_talk }, { "/sense", cmd_conference_sense }, #endif /* AUDIO */ { NULL, NULL }, }; /* Special commands are commands that only take one argument even if it contains spaces */ static const char special_commands[][MAX_CMDNAME_SIZE] = { "/add", "/avatar", "/nick", "/note", #ifdef PYTHON "/run", #endif /* PYTHON */ "/sendfile", "/title", "/mute", "", }; /* Returns true if input command is in the special_commands array. */ static bool is_special_command(const char *input) { const int s = char_find(0, input, ' '); for (int i = 0; special_commands[i][0] != '\0'; ++i) { if (strncmp(input, special_commands[i], s) == 0) { return true; } } return false; } /* Parses commands in the special_commands array. Unlike parse_command, this function * does not split the input string at spaces. * * Returns the number of arguments. */ static int parse_special_command(const char *input, char (*args)[MAX_STR_SIZE]) { int len = strlen(input); int s = char_find(0, input, ' '); memcpy(args[0], input, s); args[0][s++] = '\0'; // increment to remove space after "/command " if (s >= len) { return 1; // No additional args } memcpy(args[1], input + s, len - s); args[1][len - s] = '\0'; return 2; } /* Parses input command and puts args into arg array. * * Returns the number of arguments. */ static int parse_command(const char *input, char (*args)[MAX_STR_SIZE]) { if (is_special_command(input)) { return parse_special_command(input, args); } char *cmd = strdup(input); if (cmd == NULL) { exit_toxic_err("failed in parse_command", FATALERR_MEMORY); } int num_args = 0; /* characters wrapped in double quotes count as one arg */ while (num_args < MAX_NUM_ARGS) { int i = char_find(0, cmd, ' '); // index of last char in an argument memcpy(args[num_args], cmd, i); args[num_args++][i] = '\0'; if (cmd[i] == '\0') { // no more args break; } char tmp[MAX_STR_SIZE]; snprintf(tmp, sizeof(tmp), "%s", &cmd[i + 1]); strcpy(cmd, tmp); // tmp will always fit inside cmd } free(cmd); return num_args; } /* Matches command to respective function. * * Returns 0 on match. * Returns 1 on no match */ static int do_command(WINDOW *w, ToxWindow *self, Tox *m, int num_args, struct cmd_func *commands, char (*args)[MAX_STR_SIZE]) { for (size_t i = 0; commands[i].name != NULL; ++i) { if (strcmp(args[0], commands[i].name) == 0) { (commands[i].func)(w, self, m, num_args - 1, args); return 0; } } return 1; } void execute(WINDOW *w, ToxWindow *self, Tox *m, const char *input, int mode) { if (string_is_empty(input)) { return; } char args[MAX_NUM_ARGS][MAX_STR_SIZE]; int num_args = parse_command(input, args); if (num_args <= 0) { return; } /* Try to match input command to command functions. If non-global command mode is specified, * try specified mode's commands first, then upon failure try global commands. * * Note: Global commands must come last in case of duplicate command names */ switch (mode) { case CHAT_COMMAND_MODE: if (do_command(w, self, m, num_args, chat_commands, args) == 0) { return; } break; case CONFERENCE_COMMAND_MODE: if (do_command(w, self, m, num_args, conference_commands, args) == 0) { return; } break; } if (do_command(w, self, m, num_args, global_commands, args) == 0) { return; } #ifdef PYTHON if (do_plugin_command(num_args, args) == 0) { return; } #endif line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Invalid command."); } toxic-0.11.3/src/execute.h000066400000000000000000000020571416141666600153430ustar00rootroot00000000000000/* execute.h * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef EXECUTE_H #define EXECUTE_H #include "toxic.h" #include "windows.h" #define MAX_NUM_ARGS 4 /* Includes command */ enum { GLOBAL_COMMAND_MODE, CHAT_COMMAND_MODE, CONFERENCE_COMMAND_MODE, }; void execute(WINDOW *w, ToxWindow *self, Tox *m, const char *input, int mode); #endif /* EXECUTE_H */ toxic-0.11.3/src/file_transfers.c000066400000000000000000000261511416141666600167030ustar00rootroot00000000000000/* file_transfers.c * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #include #include #include #include #include "execute.h" #include "file_transfers.h" #include "friendlist.h" #include "line_info.h" #include "misc_tools.h" #include "notify.h" #include "toxic.h" #include "windows.h" extern FriendsList Friends; /* number of "#"'s in file transfer progress bar. Keep well below MAX_STR_SIZE */ #define NUM_PROG_MARKS 50 #define STR_BUF_SIZE 30 /* creates initial progress line that will be updated during file transfer. Assumes progline has room for at least MAX_STR_SIZE bytes */ void init_progress_bar(char *progline) { strcpy(progline, "0% ["); int i; for (i = 0; i < NUM_PROG_MARKS; ++i) { strcat(progline, "-"); } strcat(progline, "] 0.0 B/s"); } /* prints a progress bar for file transfers. */ void print_progress_bar(ToxWindow *self, double bps, double pct_done, uint32_t line_id) { if (bps < 0 || pct_done < 0 || pct_done > 100) { return; } char pct_str[STR_BUF_SIZE]; snprintf(pct_str, sizeof(pct_str), "%.1f%%", pct_done); char bps_str[STR_BUF_SIZE]; bytes_convert_str(bps_str, sizeof(bps_str), bps); char prog_line[NUM_PROG_MARKS + 1]; prog_line[0] = 0; int n = pct_done / (100 / NUM_PROG_MARKS); int i, j; for (i = 0; i < n; ++i) { strcat(prog_line, "="); } if (pct_done < 100) { strcpy(prog_line + n, ">"); } for (j = i; j < NUM_PROG_MARKS - 1; ++j) { strcat(prog_line, "-"); } size_t line_buf_size = strlen(pct_str) + NUM_PROG_MARKS + strlen(bps_str) + 7; char *full_line = malloc(line_buf_size); if (full_line == NULL) { return; } snprintf(full_line, line_buf_size, "%s [%s] %s/s", pct_str, prog_line, bps_str); line_info_set(self, line_id, full_line); free(full_line); } static void refresh_progress_helper(ToxWindow *self, FileTransfer *ft) { if (ft->state == FILE_TRANSFER_INACTIVE) { return; } /* Timeout must be set to 1 second to show correct bytes per second */ if (!timed_out(ft->last_line_progress, 1)) { return; } double remain = ft->file_size - ft->position; double pct_done = remain > 0 ? (1 - (remain / ft->file_size)) * 100 : 100; print_progress_bar(self, ft->bps, pct_done, ft->line_id); ft->bps = 0; ft->last_line_progress = get_unix_time(); } /* refreshes active file transfer status bars. * * Return true if there is at least one active file transfer in either direction. */ bool refresh_file_transfer_progress(ToxWindow *self, uint32_t friendnumber) { bool active = false; for (size_t i = 0; i < MAX_FILES; ++i) { FileTransfer *ft_r = &Friends.list[friendnumber].file_receiver[i]; FileTransfer *ft_s = &Friends.list[friendnumber].file_sender[i]; refresh_progress_helper(self, ft_r); refresh_progress_helper(self, ft_s); if (ft_r->state != FILE_TRANSFER_INACTIVE || ft_s->state != FILE_TRANSFER_INACTIVE) { active = true; } } return active; } static void clear_file_transfer(FileTransfer *ft) { *ft = (FileTransfer) { 0 }; } /* Returns a pointer to friendnumber's FileTransfer struct associated with filenumber. * Returns NULL if filenumber is invalid. */ FileTransfer *get_file_transfer_struct(uint32_t friendnumber, uint32_t filenumber) { for (size_t i = 0; i < MAX_FILES; ++i) { FileTransfer *ft_send = &Friends.list[friendnumber].file_sender[i]; if (ft_send->state != FILE_TRANSFER_INACTIVE && ft_send->filenumber == filenumber) { return ft_send; } FileTransfer *ft_recv = &Friends.list[friendnumber].file_receiver[i]; if (ft_recv->state != FILE_TRANSFER_INACTIVE && ft_recv->filenumber == filenumber) { return ft_recv; } } return NULL; } /* Returns a pointer to the FileTransfer struct associated with index with the direction specified. * Returns NULL on failure. */ FileTransfer *get_file_transfer_struct_index(uint32_t friendnumber, uint32_t index, FILE_TRANSFER_DIRECTION direction) { if (direction != FILE_TRANSFER_RECV && direction != FILE_TRANSFER_SEND) { return NULL; } for (size_t i = 0; i < MAX_FILES; ++i) { FileTransfer *ft = direction == FILE_TRANSFER_SEND ? &Friends.list[friendnumber].file_sender[i] : &Friends.list[friendnumber].file_receiver[i]; if (ft->state != FILE_TRANSFER_INACTIVE && ft->index == index) { return ft; } } return NULL; } /* Returns a pointer to an unused file sender. * Returns NULL if all file senders are in use. */ static FileTransfer *new_file_sender(ToxWindow *window, uint32_t friendnumber, uint32_t filenumber, uint8_t type) { for (size_t i = 0; i < MAX_FILES; ++i) { FileTransfer *ft = &Friends.list[friendnumber].file_sender[i]; if (ft->state == FILE_TRANSFER_INACTIVE) { clear_file_transfer(ft); ft->window = window; ft->index = i; ft->friendnumber = friendnumber; ft->filenumber = filenumber; ft->file_type = type; ft->state = FILE_TRANSFER_PENDING; return ft; } } return NULL; } /* Returns a pointer to an unused file receiver. * Returns NULL if all file receivers are in use. */ static FileTransfer *new_file_receiver(ToxWindow *window, uint32_t friendnumber, uint32_t filenumber, uint8_t type) { for (size_t i = 0; i < MAX_FILES; ++i) { FileTransfer *ft = &Friends.list[friendnumber].file_receiver[i]; if (ft->state == FILE_TRANSFER_INACTIVE) { clear_file_transfer(ft); ft->window = window; ft->index = i; ft->friendnumber = friendnumber; ft->filenumber = filenumber; ft->file_type = type; ft->state = FILE_TRANSFER_PENDING; return ft; } } return NULL; } /* Initializes an unused file transfer and returns its pointer. * Returns NULL on failure. */ FileTransfer *new_file_transfer(ToxWindow *window, uint32_t friendnumber, uint32_t filenumber, FILE_TRANSFER_DIRECTION direction, uint8_t type) { if (direction == FILE_TRANSFER_RECV) { return new_file_receiver(window, friendnumber, filenumber, type); } if (direction == FILE_TRANSFER_SEND) { return new_file_sender(window, friendnumber, filenumber, type); } return NULL; } int file_send_queue_add(uint32_t friendnumber, const char *file_path, size_t length) { if (length == 0 || file_path == NULL) { return -1; } if (length > TOX_MAX_FILENAME_LENGTH) { return -2; } for (size_t i = 0; i < MAX_FILES; ++i) { PendingFileTransfer *pending_slot = &Friends.list[friendnumber].file_send_queue[i]; if (pending_slot->pending) { continue; } pending_slot->pending = true; memcpy(pending_slot->file_path, file_path, length); pending_slot->file_path[length] = 0; pending_slot->length = length; return i; } return -3; } #define FILE_TRANSFER_SEND_CMD "/sendfile " #define FILE_TRANSFER_SEND_LEN (sizeof(FILE_TRANSFER_SEND_CMD) - 1) void file_send_queue_check(ToxWindow *self, Tox *m, uint32_t friendnumber) { for (size_t i = 0; i < MAX_FILES; ++i) { PendingFileTransfer *pending_slot = &Friends.list[friendnumber].file_send_queue[i]; if (!pending_slot->pending) { continue; } char command[TOX_MAX_FILENAME_LENGTH + FILE_TRANSFER_SEND_LEN + 1]; snprintf(command, sizeof(command), "%s%s", FILE_TRANSFER_SEND_CMD, pending_slot->file_path); execute(self->window, self, m, command, CHAT_COMMAND_MODE); *pending_slot = (PendingFileTransfer) { 0, }; } } int file_send_queue_remove(uint32_t friendnumber, size_t index) { if (index >= MAX_FILES) { return -1; } PendingFileTransfer *pending_slot = &Friends.list[friendnumber].file_send_queue[index]; if (!pending_slot->pending) { return -1; } *pending_slot = (PendingFileTransfer) { 0, }; return 0; } /* Closes file transfer ft. * * Set CTRL to -1 if we don't want to send a control signal. * Set message or self to NULL if we don't want to display a message. */ void close_file_transfer(ToxWindow *self, Tox *m, FileTransfer *ft, int CTRL, const char *message, Notification sound_type) { if (!ft) { return; } if (ft->state == FILE_TRANSFER_INACTIVE) { return; } if (ft->file) { fclose(ft->file); } if (CTRL >= 0) { Tox_Err_File_Control err; if (!tox_file_control(m, ft->friendnumber, ft->filenumber, (Tox_File_Control) CTRL, &err)) { fprintf(stderr, "Failed to cancel file transfer: %d\n", err); } } if (message && self) { if (self->active_box != -1 && sound_type != silent) { box_notify2(self, sound_type, NT_NOFOCUS | NT_WNDALERT_2, self->active_box, "%s", message); } else { box_notify(self, sound_type, NT_NOFOCUS | NT_WNDALERT_2, &self->active_box, self->name, "%s", message); } line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "%s", message); } clear_file_transfer(ft); } /* Kills active outgoing avatar file transfers for friendnumber */ void kill_avatar_file_transfers_friend(Tox *m, uint32_t friendnumber) { for (size_t i = 0; i < MAX_FILES; ++i) { FileTransfer *ft = &Friends.list[friendnumber].file_sender[i]; if (ft->file_type == TOX_FILE_KIND_AVATAR) { close_file_transfer(NULL, m, ft, TOX_FILE_CONTROL_CANCEL, NULL, silent); } } } /* Kills all active file transfers for friendnumber */ void kill_all_file_transfers_friend(Tox *m, uint32_t friendnumber) { for (size_t i = 0; i < MAX_FILES; ++i) { close_file_transfer(NULL, m, &Friends.list[friendnumber].file_sender[i], TOX_FILE_CONTROL_CANCEL, NULL, silent); close_file_transfer(NULL, m, &Friends.list[friendnumber].file_receiver[i], TOX_FILE_CONTROL_CANCEL, NULL, silent); file_send_queue_remove(friendnumber, i); } } void kill_all_file_transfers(Tox *m) { for (size_t i = 0; i < Friends.max_idx; ++i) { kill_all_file_transfers_friend(m, Friends.list[i].num); } } toxic-0.11.3/src/file_transfers.h000066400000000000000000000117411416141666600167070ustar00rootroot00000000000000/* file_transfers.h * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef FILE_TRANSFERS_H #define FILE_TRANSFERS_H #include #include "notify.h" #include "toxic.h" #include "windows.h" #define KiB (uint32_t) 1024 #define MiB (uint32_t) (1024 << 10) /* 1024^2 */ #define GiB (uint32_t) (1024 << 20) /* 1024^3 */ #define MAX_FILES 32 typedef enum FILE_TRANSFER_STATE { FILE_TRANSFER_INACTIVE, FILE_TRANSFER_PAUSED, FILE_TRANSFER_PENDING, FILE_TRANSFER_STARTED, } FILE_TRANSFER_STATE; typedef enum FILE_TRANSFER_DIRECTION { FILE_TRANSFER_SEND, FILE_TRANSFER_RECV } FILE_TRANSFER_DIRECTION; typedef struct FileTransfer { ToxWindow *window; FILE *file; FILE_TRANSFER_STATE state; uint8_t file_type; char file_name[TOX_MAX_FILENAME_LENGTH + 1]; char file_path[PATH_MAX + 1]; /* Not used by senders */ double bps; uint32_t filenumber; uint32_t friendnumber; size_t index; uint64_t file_size; uint64_t position; time_t last_line_progress; /* The last time we updated the progress bar */ uint32_t line_id; uint8_t file_id[TOX_FILE_ID_LENGTH]; } FileTransfer; typedef struct PendingFileTransfer { char file_path[TOX_MAX_FILENAME_LENGTH + 1]; size_t length; uint32_t friendnumber; bool pending; } PendingFileTransfer; /* creates initial progress line that will be updated during file transfer. progline must be at lesat MAX_STR_SIZE bytes */ void init_progress_bar(char *progline); /* prints a progress bar for file transfers */ void print_progress_bar(ToxWindow *self, double pct_done, double bps, uint32_t line_id); /* refreshes active file transfer status bars. * * Return true if there is at least one active file transfer in either direction. */ bool refresh_file_transfer_progress(ToxWindow *self, uint32_t friendnumber); /* Returns a pointer to friendnumber's FileTransfer struct associated with filenumber. * Returns NULL if filenumber is invalid. */ struct FileTransfer *get_file_transfer_struct(uint32_t friendnumber, uint32_t filenumber); /* Returns a pointer to the FileTransfer struct associated with index with the direction specified. * Returns NULL on failure. */ struct FileTransfer *get_file_transfer_struct_index(uint32_t friendnumber, uint32_t index, FILE_TRANSFER_DIRECTION direction); /* Initializes an unused file transfer and returns its pointer. * Returns NULL on failure. */ struct FileTransfer *new_file_transfer(ToxWindow *window, uint32_t friendnumber, uint32_t filenumber, FILE_TRANSFER_DIRECTION direction, uint8_t type); /* Adds a file designated by `file_path` of length `length` to the file transfer queue. * * Items in this queue will be automatically sent to the contact designated by `friendnumber` * as soon as they appear online. The item will then be removed from the queue whether * or not the transfer successfully initiates. * * If the ToxWindow associated with this friend is closed, all queued items will be * discarded. * * Return the queue index on success. * Return -1 if the length is invalid. * Return -2 if the send queue is full. */ int file_send_queue_add(uint32_t friendnumber, const char *file_path, size_t length); /* Initiates all file transfers from the file send queue for friend designated by `friendnumber`. */ void file_send_queue_check(ToxWindow *self, Tox *m, uint32_t friendnumber); /* Removes the `index`-th item from the file send queue for `friendnumber`. * * Return 0 if a pending transfer was successfully removed * Return -1 if index does not designate a pending file transfer. */ int file_send_queue_remove(uint32_t friendnumber, size_t index); /* Closes file transfer ft. * * Set CTRL to -1 if we don't want to send a control signal. * Set message or self to NULL if we don't want to display a message. */ void close_file_transfer(ToxWindow *self, Tox *m, struct FileTransfer *ft, int CTRL, const char *message, Notification sound_type); /* Kills active outgoing avatar file transfers for friendnumber */ void kill_avatar_file_transfers_friend(Tox *m, uint32_t friendnumber); /* Kills all active file transfers for friendnumber */ void kill_all_file_transfers_friend(Tox *m, uint32_t friendnumber); void kill_all_file_transfers(Tox *m); #endif /* FILE_TRANSFERS_H */ toxic-0.11.3/src/friendlist.c000066400000000000000000001151021416141666600160330ustar00rootroot00000000000000/* friendlist.c * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #include #include #include #include #include #include #include #include "avatars.h" #include "chat.h" #include "friendlist.h" #include "help.h" #include "line_info.h" #include "log.h" #include "misc_tools.h" #include "notify.h" #include "prompt.h" #include "settings.h" #include "toxic.h" #include "windows.h" #ifdef AUDIO #include "audio_call.h" #endif static uint8_t blocklist_view = 0; /* 0 if we're in friendlist view, 1 if we're in blocklist view */ FriendsList Friends; static struct Blocked { int num_selected; int max_idx; int num_blocked; uint32_t *index; BlockedFriend *list; } Blocked; static struct PendingDel { uint32_t num; bool active; WINDOW *popup; } PendingDelete; static void realloc_friends(int n) { if (n <= 0) { free(Friends.list); free(Friends.index); Friends.list = NULL; Friends.index = NULL; return; } ToxicFriend *f = realloc(Friends.list, n * sizeof(ToxicFriend)); uint32_t *f_idx = realloc(Friends.index, n * sizeof(uint32_t)); if (f == NULL || f_idx == NULL) { exit_toxic_err("failed in realloc_friends", FATALERR_MEMORY); } Friends.list = f; Friends.index = f_idx; } static void realloc_blocklist(int n) { if (n <= 0) { free(Blocked.list); free(Blocked.index); Blocked.list = NULL; Blocked.index = NULL; return; } BlockedFriend *b = realloc(Blocked.list, n * sizeof(BlockedFriend)); uint32_t *b_idx = realloc(Blocked.index, n * sizeof(uint32_t)); if (b == NULL || b_idx == NULL) { exit_toxic_err("failed in realloc_blocklist", FATALERR_MEMORY); } Blocked.list = b; Blocked.index = b_idx; } void kill_friendlist(ToxWindow *self) { for (size_t i = 0; i < Friends.max_idx; ++i) { if (Friends.list[i].active) { free(Friends.list[i].conference_invite.key); #ifdef GAMES free(Friends.list[i].game_invite.data); #endif } } realloc_blocklist(0); realloc_friends(0); free(self->help); del_window(self); } static void clear_blocklist_index(size_t idx) { Blocked.list[idx] = (BlockedFriend) { 0 }; } static void clear_friendlist_index(size_t idx) { Friends.list[idx] = (ToxicFriend) { 0 }; } /* Saves the blocklist to path. If there are no items in the blocklist the * empty file will be removed. * * Returns 0 if stored successfully. * Returns -1 on failure. */ #define TEMP_BLOCKLIST_EXT ".tmp" static int save_blocklist(char *path) { if (path == NULL) { return -1; } int len = sizeof(BlockedFriend) * Blocked.num_blocked; char *data = malloc(len * sizeof(char)); if (data == NULL) { return -1; } int i, count = 0; for (i = 0; i < Blocked.max_idx; ++i) { if (count > Blocked.num_blocked) { free(data); return -1; } if (Blocked.list[i].active) { if (Blocked.list[i].namelength > TOXIC_MAX_NAME_LENGTH) { continue; } BlockedFriend tmp = {0}; tmp.namelength = htons(Blocked.list[i].namelength); memcpy(tmp.name, Blocked.list[i].name, Blocked.list[i].namelength + 1); // Include null byte memcpy(tmp.pub_key, Blocked.list[i].pub_key, TOX_PUBLIC_KEY_SIZE); uint8_t lastonline[sizeof(uint64_t)]; memcpy(lastonline, &Blocked.list[i].last_on, sizeof(uint64_t)); hst_to_net(lastonline, sizeof(uint64_t)); memcpy(&tmp.last_on, lastonline, sizeof(uint64_t)); memcpy(data + count * sizeof(BlockedFriend), &tmp, sizeof(BlockedFriend)); ++count; } } /* Blocklist is empty, we can remove the empty file */ if (count == 0) { free(data); if (remove(path) != 0) { return -1; } return 0; } size_t temp_buf_size = strlen(path) + strlen(TEMP_BLOCKLIST_EXT) + 1; char *temp_path = malloc(temp_buf_size); if (temp_path == NULL) { free(data); return -1; } snprintf(temp_path, temp_buf_size, "%s%s", path, TEMP_BLOCKLIST_EXT); FILE *fp = fopen(temp_path, "wb"); if (fp == NULL) { free(data); free(temp_path); return -1; } if (fwrite(data, len, 1, fp) != 1) { fprintf(stderr, "Failed to write blocklist data.\n"); fclose(fp); free(data); free(temp_path); return -1; } fclose(fp); free(data); if (rename(temp_path, path) != 0) { free(temp_path); return -1; } free(temp_path); return 0; } static void sort_blocklist_index(void); int load_blocklist(char *path) { if (path == NULL) { return -1; } FILE *fp = fopen(path, "rb"); if (fp == NULL) { return -1; } off_t len = file_size(path); if (len == 0) { fclose(fp); return -1; } char *data = malloc(len); if (data == NULL) { fclose(fp); return -1; } if (fread(data, len, 1, fp) != 1) { fclose(fp); free(data); return -1; } if (len % sizeof(BlockedFriend) != 0) { fclose(fp); free(data); return -1; } int num = len / sizeof(BlockedFriend); Blocked.max_idx = num; realloc_blocklist(num); for (int i = 0; i < num; ++i) { BlockedFriend tmp = {0}; clear_blocklist_index(i); memcpy(&tmp, data + i * sizeof(BlockedFriend), sizeof(BlockedFriend)); Blocked.list[i].namelength = ntohs(tmp.namelength); if (Blocked.list[i].namelength > TOXIC_MAX_NAME_LENGTH) { continue; } Blocked.list[i].active = true; Blocked.list[i].num = i; memcpy(Blocked.list[i].name, tmp.name, Blocked.list[i].namelength + 1); // copy null byte memcpy(Blocked.list[i].pub_key, tmp.pub_key, TOX_PUBLIC_KEY_SIZE); uint8_t lastonline[sizeof(uint64_t)]; memcpy(lastonline, &tmp.last_on, sizeof(uint64_t)); net_to_host(lastonline, sizeof(uint64_t)); memcpy(&Blocked.list[i].last_on, lastonline, sizeof(uint64_t)); ++Blocked.num_blocked; } fclose(fp); free(data); sort_blocklist_index(); return 0; } #define S_WEIGHT 100000 static int index_name_cmp(const void *n1, const void *n2) { int res = qsort_strcasecmp_hlpr(Friends.list[*(const int *) n1].name, Friends.list[*(const int *) n2].name); /* Use weight to make qsort always put online friends before offline */ res = Friends.list[*(const int *) n1].connection_status ? (res - S_WEIGHT) : (res + S_WEIGHT); res = Friends.list[*(const int *) n2].connection_status ? (res + S_WEIGHT) : (res - S_WEIGHT); return res; } /* sorts Friends.index first by connection status then alphabetically */ void sort_friendlist_index(void) { size_t i; uint32_t n = 0; for (i = 0; i < Friends.max_idx; ++i) { if (Friends.list[i].active) { Friends.index[n++] = Friends.list[i].num; } } if (Friends.num_friends > 0) { qsort(Friends.index, Friends.num_friends, sizeof(uint32_t), index_name_cmp); } } static int index_name_cmp_block(const void *n1, const void *n2) { return qsort_strcasecmp_hlpr(Blocked.list[*(const int *) n1].name, Blocked.list[*(const int *) n2].name); } static void sort_blocklist_index(void) { size_t i; uint32_t n = 0; for (i = 0; i < Blocked.max_idx; ++i) { if (Blocked.list[i].active) { Blocked.index[n++] = Blocked.list[i].num; } } qsort(Blocked.index, Blocked.num_blocked, sizeof(uint32_t), index_name_cmp_block); } static void update_friend_last_online(uint32_t num, time_t timestamp) { Friends.list[num].last_online.last_on = timestamp; Friends.list[num].last_online.tm = *localtime((const time_t *)×tamp); /* if the format changes make sure TIME_STR_SIZE is the correct size */ const char *t = user_settings->timestamp_format; strftime(Friends.list[num].last_online.hour_min_str, TIME_STR_SIZE, t, &Friends.list[num].last_online.tm); } static void friendlist_onMessage(ToxWindow *self, Tox *m, uint32_t num, Tox_Message_Type type, const char *str, size_t length) { UNUSED_VAR(self); UNUSED_VAR(type); UNUSED_VAR(length); if (num >= Friends.max_idx) { return; } if (Friends.list[num].chatwin != -1) { return; } if (get_num_active_windows() < MAX_WINDOWS_NUM) { Friends.list[num].chatwin = add_window(m, new_chat(m, Friends.list[num].num)); return; } char nick[TOX_MAX_NAME_LENGTH]; get_nick_truncate(m, nick, num); line_info_add(prompt, true, nick, NULL, IN_MSG, 0, 0, "%s", str); line_info_add(prompt, false, NULL, NULL, SYS_MSG, 0, RED, "* Warning: Too many windows are open."); sound_notify(prompt, notif_error, NT_WNDALERT_1, NULL); } static void friendlist_onConnectionChange(ToxWindow *self, Tox *m, uint32_t num, Tox_Connection connection_status) { UNUSED_VAR(self); if (num >= Friends.max_idx) { return; } if (connection_status == TOX_CONNECTION_NONE) { --Friends.num_online; } else if (Friends.list[num].connection_status == TOX_CONNECTION_NONE) { ++Friends.num_online; if (avatar_send(m, num) == -1) { fprintf(stderr, "avatar_send failed for friend %u\n", num); } } Friends.list[num].connection_status = connection_status; update_friend_last_online(num, get_unix_time()); store_data(m, DATA_FILE); sort_friendlist_index(); } static void friendlist_onNickChange(ToxWindow *self, Tox *m, uint32_t num, const char *nick, size_t length) { UNUSED_VAR(self); UNUSED_VAR(length); if (num >= Friends.max_idx) { return; } /* save old name for log renaming */ char oldname[TOXIC_MAX_NAME_LENGTH + 1]; snprintf(oldname, sizeof(oldname), "%s", Friends.list[num].name); /* update name */ snprintf(Friends.list[num].name, sizeof(Friends.list[num].name), "%s", nick); Friends.list[num].namelength = strlen(Friends.list[num].name); /* get data for chatlog renaming */ char newnamecpy[TOXIC_MAX_NAME_LENGTH + 1]; char myid[TOX_ADDRESS_SIZE]; strcpy(newnamecpy, Friends.list[num].name); tox_self_get_address(m, (uint8_t *) myid); if (strcmp(oldname, newnamecpy) != 0) { if (rename_logfile(oldname, newnamecpy, myid, Friends.list[num].pub_key, Friends.list[num].chatwin) != 0) { fprintf(stderr, "Failed to rename friend chat log from `%s` to `%s`\n", oldname, newnamecpy); } } sort_friendlist_index(); } static void friendlist_onStatusChange(ToxWindow *self, Tox *m, uint32_t num, Tox_User_Status status) { UNUSED_VAR(self); UNUSED_VAR(m); if (num >= Friends.max_idx) { return; } Friends.list[num].status = status; } static void friendlist_onStatusMessageChange(ToxWindow *self, uint32_t num, const char *note, size_t length) { UNUSED_VAR(self); if (length > TOX_MAX_STATUS_MESSAGE_LENGTH || num >= Friends.max_idx) { return; } snprintf(Friends.list[num].statusmsg, sizeof(Friends.list[num].statusmsg), "%s", note); Friends.list[num].statusmsg_len = strlen(Friends.list[num].statusmsg); } void friendlist_onFriendAdded(ToxWindow *self, Tox *m, uint32_t num, bool sort) { UNUSED_VAR(self); realloc_friends(Friends.max_idx + 1); clear_friendlist_index(Friends.max_idx); uint32_t i; for (i = 0; i <= Friends.max_idx; ++i) { if (Friends.list[i].active) { continue; } ++Friends.num_friends; Friends.list[i].num = num; Friends.list[i].active = true; Friends.list[i].chatwin = -1; Friends.list[i].connection_status = TOX_CONNECTION_NONE; Friends.list[i].status = TOX_USER_STATUS_NONE; Friends.list[i].logging_on = (bool) user_settings->autolog == AUTOLOG_ON; Tox_Err_Friend_Get_Public_Key pkerr; tox_friend_get_public_key(m, num, (uint8_t *) Friends.list[i].pub_key, &pkerr); if (pkerr != TOX_ERR_FRIEND_GET_PUBLIC_KEY_OK) { fprintf(stderr, "tox_friend_get_public_key failed (error %d)\n", pkerr); } Tox_Err_Friend_Get_Last_Online loerr; time_t t = tox_friend_get_last_online(m, num, &loerr); if (loerr != TOX_ERR_FRIEND_GET_LAST_ONLINE_OK) { t = 0; } update_friend_last_online(i, t); char tempname[TOX_MAX_NAME_LENGTH + 1]; int name_len = get_nick_truncate(m, tempname, num); memcpy(Friends.list[i].name, tempname, name_len); Friends.list[i].name[name_len] = 0; Friends.list[i].namelength = name_len; if (i == Friends.max_idx) { ++Friends.max_idx; } if (sort) { sort_friendlist_index(); } #ifdef AUDIO init_friend_AV(i); #endif return; } } /* Puts blocked friend back in friendlist. fnum is new friend number, bnum is blocked number. */ static void friendlist_add_blocked(uint32_t fnum, uint32_t bnum) { realloc_friends(Friends.max_idx + 1); clear_friendlist_index(Friends.max_idx); int i; for (i = 0; i <= Friends.max_idx; ++i) { if (Friends.list[i].active) { continue; } ++Friends.num_friends; Friends.list[i].num = fnum; Friends.list[i].active = true; Friends.list[i].chatwin = -1; Friends.list[i].status = TOX_USER_STATUS_NONE; Friends.list[i].logging_on = (bool) user_settings->autolog == AUTOLOG_ON; Friends.list[i].namelength = Blocked.list[bnum].namelength; update_friend_last_online(i, Blocked.list[bnum].last_on); memcpy(Friends.list[i].name, Blocked.list[bnum].name, Friends.list[i].namelength + 1); memcpy(Friends.list[i].pub_key, Blocked.list[bnum].pub_key, TOX_PUBLIC_KEY_SIZE); if (i == Friends.max_idx) { ++Friends.max_idx; } sort_blocklist_index(); sort_friendlist_index(); #ifdef AUDIO init_friend_AV(i); #endif return; } } #ifdef GAMES static void friendlist_onGameInvite(ToxWindow *self, Tox *m, uint32_t friend_number, const uint8_t *data, size_t length) { UNUSED_VAR(self); UNUSED_VAR(data); UNUSED_VAR(length); if (friend_number >= Friends.max_idx) { return; } if (Friends.list[friend_number].chatwin != -1) { return; } if (get_num_active_windows() < MAX_WINDOWS_NUM) { Friends.list[friend_number].chatwin = add_window(m, new_chat(m, Friends.list[friend_number].num)); return; } char nick[TOX_MAX_NAME_LENGTH]; get_nick_truncate(m, nick, friend_number); line_info_add(prompt, false, NULL, NULL, SYS_MSG, 0, RED, "* Game invite from %s failed: Too many windows are open.", nick); sound_notify(prompt, notif_error, NT_WNDALERT_1, NULL); } #endif // GAMES static void friendlist_onFileRecv(ToxWindow *self, Tox *m, uint32_t num, uint32_t filenum, uint64_t file_size, const char *filename, size_t name_length) { UNUSED_VAR(self); UNUSED_VAR(file_size); UNUSED_VAR(filename); UNUSED_VAR(name_length); if (num >= Friends.max_idx) { return; } if (Friends.list[num].chatwin != -1) { return; } if (get_num_active_windows() < MAX_WINDOWS_NUM) { Friends.list[num].chatwin = add_window(m, new_chat(m, Friends.list[num].num)); return; } tox_file_control(m, num, filenum, TOX_FILE_CONTROL_CANCEL, NULL); char nick[TOX_MAX_NAME_LENGTH]; get_nick_truncate(m, nick, num); line_info_add(prompt, false, NULL, NULL, SYS_MSG, 0, RED, "* File transfer from %s failed: too many windows are open.", nick); sound_notify(prompt, notif_error, NT_WNDALERT_1, NULL); } static void friendlist_onConferenceInvite(ToxWindow *self, Tox *m, int32_t num, uint8_t type, const char *conference_pub_key, uint16_t length) { UNUSED_VAR(self); UNUSED_VAR(type); UNUSED_VAR(conference_pub_key); UNUSED_VAR(length); if (num >= Friends.max_idx) { return; } if (Friends.list[num].chatwin != -1) { return; } if (get_num_active_windows() < MAX_WINDOWS_NUM) { Friends.list[num].chatwin = add_window(m, new_chat(m, Friends.list[num].num)); return; } char nick[TOX_MAX_NAME_LENGTH]; get_nick_truncate(m, nick, num); line_info_add(prompt, false, NULL, NULL, SYS_MSG, 0, RED, "* Conference chat invite from %s failed: too many windows are open.", nick); sound_notify(prompt, notif_error, NT_WNDALERT_1, NULL); } /* move friendlist/blocklist cursor up and down */ static void select_friend(wint_t key, int *selected, int num) { if (num <= 0) { return; } if (key == KEY_UP) { if (--(*selected) < 0) { *selected = num - 1; } } else if (key == KEY_DOWN) { *selected = (*selected + 1) % num; } } static void delete_friend(Tox *m, uint32_t f_num) { kill_all_file_transfers_friend(m, f_num); kill_avatar_file_transfers_friend(m, f_num); Tox_Err_Friend_Delete err; if (tox_friend_delete(m, f_num, &err) != true) { fprintf(stderr, "tox_friend_delete failed with error %d\n", err); return; } --Friends.num_friends; if (Friends.list[f_num].connection_status != TOX_CONNECTION_NONE) { --Friends.num_online; } /* close friend's chatwindow if it's currently open */ if (Friends.list[f_num].chatwin >= 0) { ToxWindow *toxwin = get_window_ptr(Friends.list[f_num].chatwin); if (toxwin != NULL) { kill_chat_window(toxwin, m); set_active_window_index(1); /* keep friendlist focused */ } } if (Friends.list[f_num].conference_invite.key != NULL) { free(Friends.list[f_num].conference_invite.key); } clear_friendlist_index(f_num); int i; for (i = Friends.max_idx; i > 0; --i) { if (Friends.list[i - 1].active) { break; } } Friends.max_idx = i; realloc_friends(i); #ifdef AUDIO del_friend_AV(i); #endif /* make sure num_selected stays within Friends.num_friends range */ if (Friends.num_friends && Friends.num_selected == Friends.num_friends) { --Friends.num_selected; } store_data(m, DATA_FILE); } /* activates delete friend popup */ static void del_friend_activate(uint32_t f_num) { PendingDelete.popup = newwin(3, 22 + TOXIC_MAX_NAME_LENGTH, 8, 8); PendingDelete.active = true; PendingDelete.num = f_num; } static void delete_blocked_friend(uint32_t bnum); /* deactivates delete friend popup and deletes friend if instructed */ static void del_friend_deactivate(Tox *m, wint_t key) { if (key == L'y') { if (blocklist_view == 0) { delete_friend(m, PendingDelete.num); sort_friendlist_index(); } else { delete_blocked_friend(PendingDelete.num); sort_blocklist_index(); } } delwin(PendingDelete.popup); PendingDelete = (struct PendingDel) { 0 }; clear(); refresh(); } static void draw_del_popup(void) { if (!PendingDelete.active) { return; } wattron(PendingDelete.popup, A_BOLD); box(PendingDelete.popup, ACS_VLINE, ACS_HLINE); wattroff(PendingDelete.popup, A_BOLD); wmove(PendingDelete.popup, 1, 1); wprintw(PendingDelete.popup, "Delete contact "); wattron(PendingDelete.popup, A_BOLD); pthread_mutex_lock(&Winthread.lock); if (blocklist_view == 0) { wprintw(PendingDelete.popup, "%s", Friends.list[PendingDelete.num].name); } else { wprintw(PendingDelete.popup, "%s", Blocked.list[PendingDelete.num].name); } pthread_mutex_unlock(&Winthread.lock); wattroff(PendingDelete.popup, A_BOLD); wprintw(PendingDelete.popup, "? y/n"); wnoutrefresh(PendingDelete.popup); } /* deletes contact from blocked list */ static void delete_blocked_friend(uint32_t bnum) { clear_blocklist_index(bnum); int i; for (i = Blocked.max_idx; i > 0; --i) { if (Blocked.list[i - 1].active) { break; } } --Blocked.num_blocked; Blocked.max_idx = i; realloc_blocklist(i); save_blocklist(BLOCK_FILE); if (Blocked.num_blocked && Blocked.num_selected == Blocked.num_blocked) { --Blocked.num_selected; } } /* deletes contact from friendlist and puts in blocklist */ void block_friend(Tox *m, uint32_t fnum) { if (Friends.num_friends == 0) { return; } realloc_blocklist(Blocked.max_idx + 1); clear_blocklist_index(Blocked.max_idx); for (int i = 0; i <= Blocked.max_idx; ++i) { if (Blocked.list[i].active) { continue; } Blocked.list[i].active = true; Blocked.list[i].num = i; Blocked.list[i].namelength = Friends.list[fnum].namelength; Blocked.list[i].last_on = Friends.list[fnum].last_online.last_on; memcpy(Blocked.list[i].pub_key, Friends.list[fnum].pub_key, TOX_PUBLIC_KEY_SIZE); memcpy(Blocked.list[i].name, Friends.list[fnum].name, Friends.list[fnum].namelength + 1); ++Blocked.num_blocked; if (i == Blocked.max_idx) { ++Blocked.max_idx; } delete_friend(m, fnum); save_blocklist(BLOCK_FILE); sort_blocklist_index(); sort_friendlist_index(); return; } } /* removes friend from blocklist, puts back in friendlist */ static void unblock_friend(Tox *m, uint32_t bnum) { if (Blocked.num_blocked <= 0) { return; } Tox_Err_Friend_Add err; uint32_t friendnum = tox_friend_add_norequest(m, (uint8_t *) Blocked.list[bnum].pub_key, &err); if (err != TOX_ERR_FRIEND_ADD_OK) { line_info_add(prompt, false, NULL, NULL, SYS_MSG, 0, 0, "Failed to unblock friend (error %d)", err); return; } friendlist_add_blocked(friendnum, bnum); delete_blocked_friend(bnum); sort_blocklist_index(); sort_friendlist_index(); } /* * Return true if input is recognized by handler */ static bool friendlist_onKey(ToxWindow *self, Tox *m, wint_t key, bool ltr) { if (self->help->active) { help_onKey(self, key); return true; } if (key == L'h') { help_init_menu(self); return true; } if (!blocklist_view && !Friends.num_friends && (key != KEY_RIGHT && key != KEY_LEFT)) { return true; } if (blocklist_view && !Blocked.num_blocked && (key != KEY_RIGHT && key != KEY_LEFT)) { return true; } int f = 0; if (blocklist_view == 1 && Blocked.num_blocked) { f = Blocked.index[Blocked.num_selected]; } else if (Friends.num_friends) { f = Friends.index[Friends.num_selected]; } /* lock screen and force decision on deletion popup */ if (PendingDelete.active) { if (key == L'y' || key == L'n') { del_friend_deactivate(m, key); } return true; } if (key == ltr) { return true; } switch (key) { case L'\r': if (blocklist_view) { break; } /* Jump to chat window if already open */ if (Friends.list[f].chatwin != -1) { set_active_window_index(Friends.list[f].chatwin); } else if (get_num_active_windows() < MAX_WINDOWS_NUM) { Friends.list[f].chatwin = add_window(m, new_chat(m, Friends.list[f].num)); set_active_window_index(Friends.list[f].chatwin); } else { const char *msg = "* Warning: Too many windows are open."; line_info_add(prompt, false, NULL, NULL, SYS_MSG, 0, RED, msg); sound_notify(prompt, notif_error, NT_WNDALERT_1, NULL); } break; case KEY_DC: del_friend_activate(f); break; case L'b': if (!blocklist_view) { block_friend(m, f); } else { unblock_friend(m, f); } break; case KEY_RIGHT: case KEY_LEFT: blocklist_view ^= 1; break; default: if (blocklist_view == 0) { select_friend(key, &Friends.num_selected, Friends.num_friends); } else { select_friend(key, &Blocked.num_selected, Blocked.num_blocked); } break; } return true; } #define FLIST_OFST 6 /* Accounts for space at top and bottom */ static void blocklist_onDraw(ToxWindow *self, Tox *m, int y2, int x2) { UNUSED_VAR(m); wattron(self->window, A_BOLD); wprintw(self->window, " Blocked: "); wattroff(self->window, A_BOLD); wprintw(self->window, "%d\n\n", Blocked.num_blocked); if ((y2 - FLIST_OFST) <= 0) { return; } uint32_t selected_num = 0; /* Determine which portion of friendlist to draw based on current position */ int page = Blocked.num_selected / (y2 - FLIST_OFST); int start = (y2 - FLIST_OFST) * page; int end = y2 - FLIST_OFST + start; int i; for (i = start; i < Blocked.num_blocked && i < end; ++i) { uint32_t f = Blocked.index[i]; bool f_selected = false; if (i == Blocked.num_selected) { wattron(self->window, A_BOLD); wprintw(self->window, " > "); wattroff(self->window, A_BOLD); selected_num = f; f_selected = true; } else { wprintw(self->window, " "); } wattron(self->window, COLOR_PAIR(RED)); wprintw(self->window, "x"); wattroff(self->window, COLOR_PAIR(RED)); if (f_selected) { wattron(self->window, COLOR_PAIR(BLUE)); } wattron(self->window, A_BOLD); wprintw(self->window, " %s\n", Blocked.list[f].name); wattroff(self->window, A_BOLD); if (f_selected) { wattroff(self->window, COLOR_PAIR(BLUE)); } } wprintw(self->window, "\n"); self->x = x2; if (Blocked.num_blocked) { wmove(self->window, y2 - 1, 1); wattron(self->window, A_BOLD); wprintw(self->window, "Public key: "); wattroff(self->window, A_BOLD); int i; for (i = 0; i < TOX_PUBLIC_KEY_SIZE; ++i) { wprintw(self->window, "%02X", Blocked.list[selected_num].pub_key[i] & 0xff); } } wnoutrefresh(self->window); draw_del_popup(); if (self->help->active) { help_onDraw(self); } } static void friendlist_onDraw(ToxWindow *self, Tox *m) { curs_set(0); werase(self->window); int x2, y2; getmaxyx(self->window, y2, x2); bool fix_statuses = x2 != self->x; /* true if window max x value has changed */ wattron(self->window, COLOR_PAIR(CYAN)); wprintw(self->window, " Press the"); wattron(self->window, A_BOLD); wprintw(self->window, " h "); wattroff(self->window, A_BOLD); wprintw(self->window, "key for help\n\n"); wattroff(self->window, COLOR_PAIR(CYAN)); draw_window_bar(self); if (blocklist_view == 1) { blocklist_onDraw(self, m, y2, x2); return; } time_t cur_time = get_unix_time(); struct tm cur_loc_tm = *localtime((const time_t *) &cur_time); wattron(self->window, A_BOLD); wprintw(self->window, " Online: "); wattroff(self->window, A_BOLD); wprintw(self->window, "%zu/%zu \n\n", Friends.num_online, Friends.num_friends); if ((y2 - FLIST_OFST) <= 0) { return; } uint32_t selected_num = 0; /* Determine which portion of friendlist to draw based on current position */ pthread_mutex_lock(&Winthread.lock); const int page = Friends.num_selected / (y2 - FLIST_OFST); pthread_mutex_unlock(&Winthread.lock); const int start = (y2 - FLIST_OFST) * page; const int end = y2 - FLIST_OFST + start; pthread_mutex_lock(&Winthread.lock); const size_t num_friends = Friends.num_friends; pthread_mutex_unlock(&Winthread.lock); int i; for (i = start; i < num_friends && i < end; ++i) { pthread_mutex_lock(&Winthread.lock); uint32_t f = Friends.index[i]; bool is_active = Friends.list[f].active; int num_selected = Friends.num_selected; pthread_mutex_unlock(&Winthread.lock); if (is_active) { bool f_selected = false; if (i == num_selected) { wattron(self->window, A_BOLD); wprintw(self->window, " > "); wattroff(self->window, A_BOLD); selected_num = f; f_selected = true; } else { wprintw(self->window, " "); } pthread_mutex_lock(&Winthread.lock); Tox_Connection connection_status = Friends.list[f].connection_status; Tox_User_Status status = Friends.list[f].status; pthread_mutex_unlock(&Winthread.lock); if (connection_status != TOX_CONNECTION_NONE) { int colour = MAGENTA; switch (status) { case TOX_USER_STATUS_NONE: colour = GREEN; break; case TOX_USER_STATUS_AWAY: colour = YELLOW; break; case TOX_USER_STATUS_BUSY: colour = RED; break; } wattron(self->window, COLOR_PAIR(colour) | A_BOLD); wprintw(self->window, "%s ", ONLINE_CHAR); wattroff(self->window, COLOR_PAIR(colour) | A_BOLD); if (f_selected) { wattron(self->window, COLOR_PAIR(BLUE)); } wattron(self->window, A_BOLD); pthread_mutex_lock(&Winthread.lock); wprintw(self->window, "%s", Friends.list[f].name); pthread_mutex_unlock(&Winthread.lock); wattroff(self->window, A_BOLD); if (f_selected) { wattroff(self->window, COLOR_PAIR(BLUE)); } /* Reset Friends.list[f].statusmsg on window resize */ if (fix_statuses) { char statusmsg[TOX_MAX_STATUS_MESSAGE_LENGTH]; pthread_mutex_lock(&Winthread.lock); tox_friend_get_status_message(m, Friends.list[f].num, (uint8_t *) statusmsg, NULL); size_t s_len = tox_friend_get_status_message_size(m, Friends.list[f].num, NULL); pthread_mutex_unlock(&Winthread.lock); statusmsg[s_len] = '\0'; filter_str(statusmsg, s_len); pthread_mutex_lock(&Winthread.lock); snprintf(Friends.list[f].statusmsg, sizeof(Friends.list[f].statusmsg), "%s", statusmsg); Friends.list[f].statusmsg_len = strlen(Friends.list[f].statusmsg); pthread_mutex_unlock(&Winthread.lock); } /* Truncate note if it doesn't fit on one line */ size_t maxlen = x2 - getcurx(self->window) - 2; pthread_mutex_lock(&Winthread.lock); if (Friends.list[f].statusmsg_len > maxlen) { Friends.list[f].statusmsg[maxlen - 3] = '\0'; strcat(Friends.list[f].statusmsg, "..."); Friends.list[f].statusmsg[maxlen] = '\0'; Friends.list[f].statusmsg_len = maxlen; } if (Friends.list[f].statusmsg_len > 0) { wprintw(self->window, " %s", Friends.list[f].statusmsg); } pthread_mutex_unlock(&Winthread.lock); wprintw(self->window, "\n"); } else { wprintw(self->window, "%s ", OFFLINE_CHAR); if (f_selected) { wattron(self->window, COLOR_PAIR(BLUE)); } wattron(self->window, A_BOLD); pthread_mutex_lock(&Winthread.lock); wprintw(self->window, "%s", Friends.list[f].name); pthread_mutex_unlock(&Winthread.lock); wattroff(self->window, A_BOLD); if (f_selected) { wattroff(self->window, COLOR_PAIR(BLUE)); } pthread_mutex_lock(&Winthread.lock); time_t last_seen = Friends.list[f].last_online.last_on; pthread_mutex_unlock(&Winthread.lock); if (last_seen != 0) { pthread_mutex_lock(&Winthread.lock); int day_dist = ( cur_loc_tm.tm_yday - Friends.list[f].last_online.tm.tm_yday + ((cur_loc_tm.tm_year - Friends.list[f].last_online.tm.tm_year) * 365) ); const char *hourmin = Friends.list[f].last_online.hour_min_str; pthread_mutex_unlock(&Winthread.lock); switch (day_dist) { case 0: wprintw(self->window, " Last seen: Today %s\n", hourmin); break; case 1: wprintw(self->window, " Last seen: Yesterday %s\n", hourmin); break; default: wprintw(self->window, " Last seen: %d days ago\n", day_dist); break; } } else { wprintw(self->window, " Last seen: Never\n"); } } } } self->x = x2; if (num_friends) { wmove(self->window, y2 - 1, 1); wattron(self->window, A_BOLD); wprintw(self->window, "Public key: "); wattroff(self->window, A_BOLD); int i; for (i = 0; i < TOX_PUBLIC_KEY_SIZE; ++i) { wprintw(self->window, "%02X", Friends.list[selected_num].pub_key[i] & 0xff); } } wnoutrefresh(self->window); draw_del_popup(); if (self->help->active) { help_onDraw(self); } } void friendlist_onInit(ToxWindow *self, Tox *m) { UNUSED_VAR(m); int x2; int y2; getmaxyx(self->window, y2, x2); if (y2 <= 0 || x2 <= 0) { exit_toxic_err("failed in friendlist_onInit", FATALERR_CURSES); } self->window_bar = subwin(self->window, WINDOW_BAR_HEIGHT, x2, y2 - 2, 0); } void disable_chatwin(uint32_t f_num) { Friends.list[f_num].chatwin = -1; } #ifdef AUDIO static void friendlist_onAV(ToxWindow *self, ToxAV *av, uint32_t friend_number, int state) { UNUSED_VAR(self); if (friend_number >= Friends.max_idx) { return; } Tox *m = toxav_get_tox(av); if (Friends.list[friend_number].chatwin == -1) { if (get_num_active_windows() < MAX_WINDOWS_NUM) { if (state != TOXAV_FRIEND_CALL_STATE_FINISHED) { Friends.list[friend_number].chatwin = add_window(m, new_chat(m, Friends.list[friend_number].num)); set_active_window_index(Friends.list[friend_number].chatwin); } } else { char nick[TOX_MAX_NAME_LENGTH]; get_nick_truncate(m, nick, Friends.list[friend_number].num); line_info_add(prompt, false, NULL, NULL, SYS_MSG, 0, 0, "Audio action from: %s!", nick); const char *errmsg = "* Warning: Too many windows are open."; line_info_add(prompt, false, NULL, NULL, SYS_MSG, 0, RED, errmsg); sound_notify(prompt, notif_error, NT_WNDALERT_1, NULL); } } } #endif /* AUDIO */ /* Returns a friend's status */ Tox_User_Status get_friend_status(uint32_t friendnumber) { return Friends.list[friendnumber].status; } /* Returns a friend's connection status */ Tox_Connection get_friend_connection_status(uint32_t friendnumber) { return Friends.list[friendnumber].connection_status; } /* * Returns true if friend associated with `public_key` is in the block list. * * `public_key` must be at least TOX_PUBLIC_KEY_SIZE bytes. */ bool friend_is_blocked(const char *public_key) { for (size_t i = 0; i < Blocked.max_idx; ++i) { if (!Blocked.list[i].active) { continue; } if (memcmp(public_key, Blocked.list[i].pub_key, TOX_PUBLIC_KEY_SIZE) == 0) { return true; } } return false; } ToxWindow *new_friendlist(void) { ToxWindow *ret = calloc(1, sizeof(ToxWindow)); if (ret == NULL) { exit_toxic_err("failed in new_friendlist", FATALERR_MEMORY); } ret->type = WINDOW_TYPE_FRIEND_LIST; ret->onInit = &friendlist_onInit; ret->onKey = &friendlist_onKey; ret->onDraw = &friendlist_onDraw; ret->onFriendAdded = &friendlist_onFriendAdded; ret->onMessage = &friendlist_onMessage; ret->onConnectionChange = &friendlist_onConnectionChange; ret->onNickChange = &friendlist_onNickChange; ret->onStatusChange = &friendlist_onStatusChange; ret->onStatusMessageChange = &friendlist_onStatusMessageChange; ret->onFileRecv = &friendlist_onFileRecv; ret->onConferenceInvite = &friendlist_onConferenceInvite; #ifdef AUDIO ret->onInvite = &friendlist_onAV; ret->onRinging = &friendlist_onAV; ret->onStarting = &friendlist_onAV; ret->onEnding = &friendlist_onAV; ret->onError = &friendlist_onAV; ret->onStart = &friendlist_onAV; ret->onCancel = &friendlist_onAV; ret->onReject = &friendlist_onAV; ret->onEnd = &friendlist_onAV; ret->is_call = false; #endif /* AUDIO */ #ifdef GAMES ret->onGameInvite = &friendlist_onGameInvite; #endif ret->num = -1; ret->active_box = -1; Help *help = calloc(1, sizeof(Help)); if (help == NULL) { exit_toxic_err("failed in new_friendlist", FATALERR_MEMORY); } ret->help = help; strcpy(ret->name, "Contacts"); return ret; } toxic-0.11.3/src/friendlist.h000066400000000000000000000063361416141666600160500ustar00rootroot00000000000000/* friendlist.h * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef FRIENDLIST_H #define FRIENDLIST_H #include #include "file_transfers.h" #include "toxic.h" #include "windows.h" #ifdef GAMES #include "game_base.h" #endif struct LastOnline { uint64_t last_on; struct tm tm; char hour_min_str[TIME_STR_SIZE]; /* holds 12/24-hour time string e.g. "10:43 PM" */ }; struct ConferenceInvite { char *key; uint16_t length; uint8_t type; bool pending; }; #ifdef GAMES struct GameInvite { uint8_t *data; size_t data_length; GameType type; uint32_t id; bool pending; }; #endif // GAMES typedef struct { char name[TOXIC_MAX_NAME_LENGTH + 1]; uint16_t namelength; char statusmsg[TOX_MAX_STATUS_MESSAGE_LENGTH + 1]; size_t statusmsg_len; char pub_key[TOX_PUBLIC_KEY_SIZE]; uint32_t num; int chatwin; bool active; Tox_Connection connection_status; bool is_typing; bool logging_on; /* saves preference for friend irrespective of global settings */ Tox_User_Status status; struct LastOnline last_online; struct ConferenceInvite conference_invite; #ifdef GAMES struct GameInvite game_invite; #endif FileTransfer file_receiver[MAX_FILES]; FileTransfer file_sender[MAX_FILES]; PendingFileTransfer file_send_queue[MAX_FILES]; } ToxicFriend; typedef struct { char name[TOXIC_MAX_NAME_LENGTH + 1]; uint16_t namelength; char pub_key[TOX_PUBLIC_KEY_SIZE]; uint32_t num; bool active; uint64_t last_on; } BlockedFriend; typedef struct { int num_selected; size_t num_friends; size_t num_online; size_t max_idx; /* 1 + the index of the last friend in list */ uint32_t *index; ToxicFriend *list; } FriendsList; extern FriendsList Friends; ToxWindow *new_friendlist(void); void friendlist_onInit(ToxWindow *self, Tox *m); void disable_chatwin(uint32_t f_num); int get_friendnum(uint8_t *name); int load_blocklist(char *data); void kill_friendlist(ToxWindow *self); void friendlist_onFriendAdded(ToxWindow *self, Tox *m, uint32_t num, bool sort); Tox_User_Status get_friend_status(uint32_t friendnumber); Tox_Connection get_friend_connection_status(uint32_t friendnumber); /* sorts friendlist_index first by connection status then alphabetically */ void sort_friendlist_index(void); /* * Returns true if friend associated with `public_key` is in the block list. * * `public_key` must be at least TOX_PUBLIC_KEY_SIZE bytes. */ bool friend_is_blocked(const char *public_key); #endif /* end of include guard: FRIENDLIST_H */ toxic-0.11.3/src/game_base.c000066400000000000000000000675101416141666600156040ustar00rootroot00000000000000/* game_base.c * * * Copyright (C) 2020 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #include #include #include #include #include "friendlist.h" #include "game_centipede.h" #include "game_base.h" #include "game_chess.h" #include "game_life.h" #include "game_snake.h" #include "line_info.h" #include "misc_tools.h" #include "notify.h" #include "settings.h" #include "windows.h" extern struct Winthread Winthread; extern struct user_settings *user_settings; /* * Determines the base rate at which game objects should update their state. * Inversely correlated with frame rate. */ #define GAME_OBJECT_UPDATE_INTERVAL_MULTIPLIER 25 /* Determines overall game speed; lower makes it faster and vice versa. * Inversely correlated with frame rate. */ #define GAME_DEFAULT_UPDATE_INTERVAL 10 #define GAME_MAX_UPDATE_INTERVAL 50 /* Determines if window is large enough for a respective window type */ #define WINDOW_SIZE_SQUARE_VALID(max_x, max_y)((((max_y) - 4) >= (GAME_MAX_SQUARE_Y_DEFAULT))\ && ((max_x) >= (GAME_MAX_SQUARE_X_DEFAULT))) #define WINDOW_SIZE_LARGE_SQUARE_VALID(max_x, max_y)((((max_y) - 4) >= (GAME_MAX_SQUARE_Y_LARGE))\ && ((max_x) >= (GAME_MAX_SQUARE_X_LARGE))) #define WINDOW_SIZE_RECT_VALID(max_x, max_y)((((max_y) - 4) >= (GAME_MAX_RECT_Y_DEFAULT))\ && ((max_x) >= (GAME_MAX_RECT_X_DEFAULT))) #define WINDOW_SIZE_LARGE_RECT_VALID(max_x, max_y)((((max_y) - 4) >= (GAME_MAX_RECT_Y_LARGE))\ && ((max_x) >= (GAME_MAX_RECT_X_LARGE))) static ToxWindow *game_new_window(Tox *m, GameType type, uint32_t friendnumber); struct GameList { const char *name; GameType type; }; static struct GameList game_list[] = { { "centipede", GT_Centipede }, { "chess", GT_Chess }, { "life", GT_Life }, { "snake", GT_Snake }, { NULL, GT_Invalid }, }; /* * Returns the GameType associated with `game_string`. */ GameType game_get_type(const char *game_string) { const char *match_string = NULL; for (size_t i = 0; (match_string = game_list[i].name); ++i) { if (strcasecmp(game_string, match_string) == 0) { return game_list[i].type; } } return GT_Invalid; } const char *game_get_name_string(GameType type) { GameType match_type; for (size_t i = 0; (match_type = game_list[i].type) < GT_Invalid; ++i) { if (match_type == type) { return game_list[i].name; } } return NULL; } void game_list_print(ToxWindow *self) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Available games:"); const char *name = NULL; for (size_t i = 0; (name = game_list[i].name); ++i) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "%zu: %s", i + 1, name); } } bool game_type_is_multiplayer(GameType type) { return type == GT_Chess; } /* * Sends a notification to the window associated with `game`. * * `message` - the notification message that will be displayed. */ void game_window_notify(const GameData *game, const char *message) { ToxWindow *self = get_window_ptr(game->window_id); if (self == NULL) { return; } if (self->active_box != -1) { box_notify2(self, generic_message, NT_WNDALERT_0 | NT_NOFOCUS | user_settings->bell_on_message, self->active_box, "%s", message); } else { box_notify(self, generic_message, NT_WNDALERT_0 | NT_NOFOCUS | user_settings->bell_on_message, &self->active_box, self->name, "%s", message); } } /* Returns the current wall time in milliseconds */ TIME_MS get_time_millis(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t); return ((TIME_MS) t.tv_sec) * 1000 + ((TIME_MS) t.tv_nsec) / 1000000; } void game_kill(ToxWindow *self) { GameData *game = self->game; if (game) { if (game->cb_game_kill) { game->cb_game_kill(game, game->cb_game_kill_data); } delwin(game->window); free(game->messages); free(game); } kill_notifs(self->active_box); del_window(self); if (get_num_active_windows_type(WINDOW_TYPE_GAME) == 0) { set_window_refresh_rate(NCURSES_DEFAULT_REFRESH_RATE); } } static void game_init_abort(const ToxWindow *parent, ToxWindow *self) { game_kill(self); set_active_window_index(parent->index); } static void game_toggle_pause(GameData *game) { GameStatus status = game->status; if (status == GS_Running) { game->status = GS_Paused; } else if (status == GS_Paused) { game->status = GS_Running; } else { return; } if (game->cb_game_pause) { game->cb_game_pause(game, game->status == GS_Paused, game->cb_game_pause_data); } } static int game_initialize_type(GameData *game, const uint8_t *data, size_t length) { int ret = -3; switch (game->type) { case GT_Snake: { ret = snake_initialize(game); break; } case GT_Centipede: { ret = centipede_initialize(game); break; } case GT_Chess: { ret = chess_initialize(game, data, length); break; } case GT_Life: { ret = life_initialize(game); break; } default: { break; } } return ret; } int game_initialize(const ToxWindow *parent, Tox *m, GameType type, uint32_t id, const uint8_t *multiplayer_data, size_t length) { int max_x; int max_y; getmaxyx(parent->window, max_y, max_x); max_y -= (CHATBOX_HEIGHT + WINDOW_BAR_HEIGHT); ToxWindow *self = game_new_window(m, type, parent->num); if (self == NULL) { return -4; } GameData *game = self->game; int window_id = add_window(m, self); if (window_id == -1) { free(game); free(self); return -4; } game->is_multiplayer = game_type_is_multiplayer(type); if (game->is_multiplayer) { if (parent->type != WINDOW_TYPE_CHAT) { game_init_abort(parent, self); return -3; } if (get_friend_connection_status(parent->num) == TOX_CONNECTION_NONE) { game_init_abort(parent, self); return -2; } game->is_multiplayer = true; } game->tox = m; game->window_shape = GW_ShapeSquare; game->parent_max_x = max_x; game->parent_max_y = max_y; game->update_interval = GAME_DEFAULT_UPDATE_INTERVAL; game->type = type; game->window_id = window_id; game->window = subwin(self->window, max_y, max_x, 0, 0); game->id = id; game->friend_number = parent->num; if (game->window == NULL) { game_init_abort(parent, self); return -4; } int init_ret = game_initialize_type(game, multiplayer_data, length); if (init_ret < 0) { game_init_abort(parent, self); return init_ret; } game->status = GS_Running; set_active_window_index(window_id); set_window_refresh_rate(NCURSES_GAME_REFRESH_RATE); return 0; } int game_set_window_shape(GameData *game, GameWindowShape shape) { if (shape >= GW_ShapeInvalid) { return -1; } if (game->status != GS_None) { return -2; } const int max_x = game->parent_max_x; const int max_y = game->parent_max_y; switch (shape) { case GW_ShapeSquare: { if (WINDOW_SIZE_SQUARE_VALID(max_x, max_y)) { game->game_max_x = GAME_MAX_SQUARE_X_DEFAULT; game->game_max_y = GAME_MAX_SQUARE_Y_DEFAULT; return 0; } break; } case GW_ShapeSquareLarge: { if (WINDOW_SIZE_LARGE_SQUARE_VALID(max_x, max_y)) { game->game_max_x = GAME_MAX_SQUARE_X_LARGE; game->game_max_y = GAME_MAX_SQUARE_Y_LARGE; return 0; } break; } case GW_ShapeRectangle: { if (WINDOW_SIZE_RECT_VALID(max_x, max_y)) { game->game_max_x = GAME_MAX_RECT_X_DEFAULT; game->game_max_y = GAME_MAX_RECT_Y_DEFAULT; return 0; } break; } case GW_ShapeRectangleLarge: { if (WINDOW_SIZE_LARGE_RECT_VALID(max_x, max_y)) { game->game_max_x = GAME_MAX_RECT_X_LARGE; game->game_max_y = GAME_MAX_RECT_Y_LARGE; return 0; } break; } default: { return -1; } } return -1; } static void game_fix_message_coords(const GameData *game, Direction direction, Coords *coords, size_t length) { if (direction >= INVALID_DIRECTION) { return; } if (direction == EAST || direction == WEST) { coords->y = game_coordinates_in_bounds(game, coords->x, coords->y + 2) ? coords->y + 2 : coords->y - 2; } else { coords->x = game_coordinates_in_bounds(game, coords->x + 2, coords->y) ? coords->x + 2 : coords->x - (length + 2); } if (!game_coordinates_in_bounds(game, coords->x + length, coords->y) || !game_coordinates_in_bounds(game, coords->x, coords->y)) { int max_x; int max_y; getmaxyx(game->window, max_y, max_x); const int x_left_bound = (max_x - game->game_max_x) / 2; const int x_right_bound = (max_x + game->game_max_x) / 2; const int y_top_bound = (max_y - game->game_max_y) / 2; const int y_bottom_bound = (max_y + game->game_max_y) / 2; if (coords->x + length >= x_right_bound) { coords->x -= (length + 2); } else if (coords->x <= x_left_bound) { coords->x = x_left_bound + 2; } if (coords->y >= y_bottom_bound) { coords->y -= 2; } else if (coords->y <= y_top_bound) { coords->y += 2; } } } static void game_clear_message(GameData *game, size_t index) { memset(&game->messages[index], 0, sizeof(GameMessage)); } static void game_clear_all_messages(GameData *game) { for (size_t i = 0; i < game->messages_size; ++i) { game_clear_message(game, i); } } static GameMessage *game_get_new_message_holder(GameData *game) { size_t i; for (i = 0; i < game->messages_size; ++i) { GameMessage *m = &game->messages[i]; if (m->length == 0) { break; } } if (i == game->messages_size) { GameMessage *tmp = realloc(game->messages, sizeof(GameMessage) * (i + 1)); if (tmp == NULL) { return NULL; } memset(&tmp[i], 0, sizeof(GameMessage)); game->messages = tmp; game->messages_size = i + 1; } return &game->messages[i]; } int game_set_message(GameData *game, const char *message, size_t length, Direction dir, int attributes, int colour, TIME_S timeout, const Coords *coords, bool sticky, bool priority) { if (length == 0 || length > GAME_MAX_MESSAGE_SIZE) { return -1; } if (coords == NULL) { return -1; } GameMessage *m = game_get_new_message_holder(game); if (m == NULL) { return -1; } memcpy(m->message, message, length); m->message[length] = 0; m->length = length; m->timeout = timeout > 0 ? timeout : GAME_MESSAGE_DEFAULT_TIMEOUT; m->set_time = get_unix_time(); m->attributes = attributes; m->colour = colour; m->direction = dir; m->coords = coords; m->sticky = sticky; m->priority = priority; m->original_coords = (Coords) { coords->x, coords->y }; if (GAME_UTIL_DIRECTION_VALID(dir)) { game_fix_message_coords(game, dir, &m->original_coords, length); } return 0; } static int game_restart(GameData *game) { if (game->cb_game_kill) { game->cb_game_kill(game, game->cb_game_kill_data); } game->update_interval = GAME_DEFAULT_UPDATE_INTERVAL; game->status = GS_Running; game->score = 0; game->level = 0; game->lives = 0; game->last_frame_time = 0; game_clear_all_messages(game); if (game_initialize_type(game, NULL, 0) == -1) { return -1; } return 0; } static void game_draw_help_bar(const GameData *game, WINDOW *win) { int max_x; int max_y; getmaxyx(win, max_y, max_x); UNUSED_VAR(max_x); wmove(win, max_y - 1, 1); if (!game->is_multiplayer) { wprintw(win, "Pause: "); wattron(win, A_BOLD); wprintw(win, "F2 "); wattroff(win, A_BOLD); } wprintw(win, "Quit: "); wattron(win, A_BOLD); wprintw(win, "F9"); wattroff(win, A_BOLD); } static void game_draw_border(const GameData *game, const int max_x, const int max_y) { WINDOW *win = game->window; const int game_max_x = game->game_max_x; const int game_max_y = game->game_max_y; const int x = (max_x - game_max_x) / 2; const int y = (max_y - game_max_y) / 2; wattron(win, COLOR_PAIR(GAME_BORDER_COLOUR)); mvwaddch(win, y, x, ACS_ULCORNER); mvwhline(win, y, x + 1, ACS_HLINE, game_max_x - 1); mvwvline(win, y + 1, x, ACS_VLINE, game_max_y - 1); mvwvline(win, y, x - 1, ACS_VLINE, game_max_y + 1); mvwaddch(win, y, x + game_max_x, ACS_URCORNER); mvwvline(win, y + 1, x + game_max_x, ACS_VLINE, game_max_y - 1); mvwvline(win, y, x + game_max_x + 1, ACS_VLINE, game_max_y + 1); mvwaddch(win, y + game_max_y, x, ACS_LLCORNER); mvwhline(win, y + game_max_y, x + 1, ACS_HLINE, game_max_x - 1); mvwaddch(win, y + game_max_y, x + game_max_x, ACS_LRCORNER); wattroff(win, COLOR_PAIR(GAME_BORDER_COLOUR)); } static void game_draw_status(const GameData *game, const int max_x, const int max_y) { WINDOW *win = game->window; int x = ((max_x - game->game_max_x) / 2) - 1; const int y = ((max_y - game->game_max_y) / 2) - 1; wattron(win, A_BOLD); if (game->show_score) { mvwprintw(win, y, x, "Score: %ld", game->score); } if (game->show_high_score) { mvwprintw(win, y + game->game_max_y + 2, x, "High Score: %zu", game->high_score); } x = ((max_x / 2) + (game->game_max_x / 2)) - 7; if (game->show_level) { mvwprintw(win, y, x, "Level: %zu", game->level); } if (game->show_lives) { mvwprintw(win, y + game->game_max_y + 2, x, "Lives: %d", game->lives); } wattroff(win, A_BOLD); } static void game_draw_game_over(const GameData *game) { WINDOW *win = game->window; int max_x; int max_y; getmaxyx(win, max_y, max_x); const int x = max_x / 2; const int y = max_y / 2; const char *message = "GAME OVER!"; size_t length = strlen(message); wattron(win, A_BOLD | COLOR_PAIR(RED)); mvwprintw(win, y - 1, x - (length / 2), "%s", message); wattroff(win, A_BOLD | COLOR_PAIR(RED)); message = "Press F5 to play again"; length = strlen(message); mvwprintw(win, y + 1, x - (length / 2), "%s", message); } static void game_draw_pause_screen(const GameData *game) { WINDOW *win = game->window; int max_x; int max_y; getmaxyx(win, max_y, max_x); const int x = max_x / 2; const int y = max_y / 2; wattron(win, A_BOLD | COLOR_PAIR(YELLOW)); mvwprintw(win, y, x - 3, "PAUSED"); wattroff(win, A_BOLD | COLOR_PAIR(YELLOW)); } static void game_draw_messages(GameData *game, bool priority) { WINDOW *win = game->window; for (size_t i = 0; i < game->messages_size; ++i) { GameMessage *m = &game->messages[i]; if (m->length == 0 || m->coords == NULL) { continue; } if (timed_out(m->set_time, m->timeout)) { game_clear_message(game, i); continue; } if (m->priority != priority) { continue; } if (!m->sticky) { wattron(win, m->attributes | COLOR_PAIR(m->colour)); mvwprintw(win, m->original_coords.y, m->original_coords.x, "%s", m->message); wattroff(win, m->attributes | COLOR_PAIR(m->colour)); continue; } Coords fixed_coords = { m->coords->x, m->coords->y }; // TODO: we should only have to do this if the coordinates changed game_fix_message_coords(game, m->direction, &fixed_coords, m->length); wattron(win, m->attributes | COLOR_PAIR(m->colour)); mvwprintw(win, fixed_coords.y, fixed_coords.x, "%s", m->message); wattroff(win, m->attributes | COLOR_PAIR(m->colour)); } } static void game_update_state(GameData *game) { if (!game->cb_game_update_state) { return; } TIME_MS cur_time = get_time_millis(); if (cur_time - game->last_frame_time > 500) { game->last_frame_time = cur_time; } size_t iterations = (cur_time - game->last_frame_time) / game->update_interval; for (size_t i = 0; i < iterations; ++i) { game->cb_game_update_state(game, game->cb_game_update_state_data); game->last_frame_time += game->update_interval; } } void game_onDraw(ToxWindow *self, Tox *m) { UNUSED_VAR(m); // Note: This function is not thread safe if we ever need to use `m` GameData *game = self->game; game_draw_help_bar(game, self->window); draw_window_bar(self); curs_set(0); int max_x; int max_y; getmaxyx(game->window, max_y, max_x); wclear(game->window); game_draw_border(game, max_x, max_y); game_draw_messages(game, false); if (game->cb_game_render_window) { game->cb_game_render_window(game, game->window, game->cb_game_render_window_data); } game_draw_status(game, max_x, max_y); switch (game->status) { case GS_Running: { game_update_state(game); break; } case GS_Paused: { game_draw_pause_screen(game); break; } case GS_Finished: { game_draw_game_over(game); break; } default: { break; } } game_draw_messages(game, true); } bool game_onKey(ToxWindow *self, Tox *m, wint_t key, bool is_printable) { UNUSED_VAR(is_printable); UNUSED_VAR(m); GameData *game = self->game; if (key == KEY_F(9)) { game_kill(self); return true; } if (key == KEY_F(2) && !game->is_multiplayer) { game_toggle_pause(self->game); return true; } if (!game->is_multiplayer && game->status == GS_Finished && key == KEY_F(5)) { if (game_restart(self->game) == -1) { fprintf(stderr, "Warning: game_restart() failed\n"); } return true; } if (game->cb_game_key_press) { if (game->is_multiplayer) { pthread_mutex_lock(&Winthread.lock); // we use the tox instance when we send packets } game->cb_game_key_press(game, key, game->cb_game_key_press_data); if (game->is_multiplayer) { pthread_mutex_unlock(&Winthread.lock); } } return true; } void game_onInit(ToxWindow *self, Tox *m) { UNUSED_VAR(m); int max_x; int max_y; getmaxyx(self->window, max_y, max_x); if (max_y <= 0 || max_x <= 0) { exit_toxic_err("failed in game_onInit", FATALERR_CURSES); } self->window_bar = subwin(self->window, WINDOW_BAR_HEIGHT, max_x, max_y - 2, 0); } /* * Byte 0: Version * Byte 1: Game type * Byte 2-5: Game ID * Byte 6-* Game data */ void game_onPacket(ToxWindow *self, Tox *m, uint32_t friendnumber, const uint8_t *data, size_t length) { UNUSED_VAR(m); GameData *game = self->game; if (friendnumber != self->num) { return; } if (data == NULL) { return; } if (length < GAME_PACKET_HEADER_SIZE || length > GAME_MAX_PACKET_SIZE) { return; } if (data[0] != GAME_NETWORKING_VERSION) { fprintf(stderr, "Game packet rejected: wrong networking version (got %d, expected %d)\n", data[0], GAME_NETWORKING_VERSION); return; } GameType type = (GameType)data[1]; if (game->type != type) { return; } uint32_t id; game_util_unpack_u32(data + 2, &id); if (game->id != id) { return; } data += GAME_PACKET_HEADER_SIZE; length -= GAME_PACKET_HEADER_SIZE; if (game->cb_game_on_packet) { game->cb_game_on_packet(game, data, length, game->cb_game_on_packet_data); } } static ToxWindow *game_new_window(Tox *m, GameType type, uint32_t friendnumber) { const char *window_name = game_get_name_string(type); if (window_name == NULL) { return NULL; } ToxWindow *ret = calloc(1, sizeof(ToxWindow)); if (ret == NULL) { return NULL; } ret->num = friendnumber; ret->type = WINDOW_TYPE_GAME; ret->onInit = &game_onInit; ret->onDraw = &game_onDraw; ret->onKey = &game_onKey; ret->onGameData = &game_onPacket; ret->game = calloc(1, sizeof(GameData)); if (ret->game == NULL) { free(ret); return NULL; } ret->active_box = -1; if (game_type_is_multiplayer(type)) { char nick[TOX_MAX_NAME_LENGTH]; get_nick_truncate(m, nick, friendnumber); snprintf(ret->name, sizeof(ret->name), "%s (%s)", window_name, nick); } else { snprintf(ret->name, sizeof(ret->name), "%s", window_name); } return ret; } bool game_coordinates_in_bounds(const GameData *game, int x, int y) { const int game_max_x = game->game_max_x; const int game_max_y = game->game_max_y; int max_x; int max_y; getmaxyx(game->window, max_y, max_x); const int x_left_bound = (max_x - game_max_x) / 2; const int x_right_bound = (max_x + game_max_x) / 2; const int y_top_bound = (max_y - game_max_y) / 2; const int y_bottom_bound = (max_y + game_max_y) / 2; return x > x_left_bound && x < x_right_bound && y > y_top_bound && y < y_bottom_bound; } void game_random_coords(const GameData *game, Coords *coords) { const int game_max_x = game->game_max_x; const int game_max_y = game->game_max_y; int max_x; int max_y; getmaxyx(game->window, max_y, max_x); const int x_left_bound = ((max_x - game_max_x) / 2) + 1; const int x_right_bound = ((max_x + game_max_x) / 2) - 1; const int y_top_bound = ((max_y - game_max_y) / 2) + 1; const int y_bottom_bound = ((max_y + game_max_y) / 2) - 1; coords->x = (rand() % (x_right_bound - x_left_bound + 1)) + x_left_bound; coords->y = (rand() % (y_bottom_bound - y_top_bound + 1)) + y_top_bound; } void game_max_x_y(const GameData *game, int *x, int *y) { getmaxyx(game->window, *y, *x); } int game_y_bottom_bound(const GameData *game) { int max_x; int max_y; getmaxyx(game->window, max_y, max_x); UNUSED_VAR(max_x); return ((max_y + game->game_max_y) / 2) - 1; } int game_y_top_bound(const GameData *game) { int max_x; int max_y; getmaxyx(game->window, max_y, max_x); UNUSED_VAR(max_x); return ((max_y - game->game_max_y) / 2) + 1; } int game_x_right_bound(const GameData *game) { int max_x; int max_y; getmaxyx(game->window, max_y, max_x); UNUSED_VAR(max_y); return ((max_x + game->game_max_x) / 2) - 1; } int game_x_left_bound(const GameData *game) { int max_x; int max_y; getmaxyx(game->window, max_y, max_x); UNUSED_VAR(max_y); return ((max_x - game->game_max_x) / 2) + 1; } void game_show_score(GameData *game, bool show_score) { game->show_score = show_score; } void game_show_high_score(GameData *game, bool show_high_score) { game->show_high_score = show_high_score; } void game_show_lives(GameData *game, bool show_lives) { game->show_lives = show_lives; } void game_show_level(GameData *game, bool show_level) { game->show_level = show_level; } void game_update_score(GameData *game, long int points) { game->score += points; if (game->score > game->high_score) { game->high_score = game->score; } } void game_set_score(GameData *game, long int val) { game->score = val; } long int game_get_score(const GameData *game) { return game->score; } void game_increment_level(GameData *game) { ++game->level; } void game_update_lives(GameData *game, int lives) { game->lives += lives; } int game_get_lives(const GameData *game) { return game->lives; } size_t game_get_current_level(const GameData *game) { return game->level; } void game_set_status(GameData *game, GameStatus status) { if (status < GS_Invalid) { game->status = status; } } void game_set_update_interval(GameData *game, TIME_MS update_interval) { game->update_interval = MIN(update_interval, GAME_MAX_UPDATE_INTERVAL); } bool game_do_object_state_update(const GameData *game, TIME_MS current_time, TIME_MS last_moved_time, TIME_MS speed) { TIME_MS delta = (current_time - last_moved_time) * speed; return delta > game->update_interval * GAME_OBJECT_UPDATE_INTERVAL_MULTIPLIER; } void game_set_cb_update_state(GameData *game, cb_game_update_state *func, void *cb_data) { game->cb_game_update_state = func; game->cb_game_update_state_data = cb_data; } void game_set_cb_on_keypress(GameData *game, cb_game_key_press *func, void *cb_data) { game->cb_game_key_press = func; game->cb_game_key_press_data = cb_data; } void game_set_cb_render_window(GameData *game, cb_game_render_window *func, void *cb_data) { game->cb_game_render_window = func; game->cb_game_render_window_data = cb_data; } void game_set_cb_kill(GameData *game, cb_game_kill *func, void *cb_data) { game->cb_game_kill = func; game->cb_game_kill_data = cb_data; } void game_set_cb_on_pause(GameData *game, cb_game_pause *func, void *cb_data) { game->cb_game_pause = func; game->cb_game_pause_data = cb_data; } void game_set_cb_on_packet(GameData *game, cb_game_on_packet *func, void *cb_data) { game->cb_game_on_packet = func; game->cb_game_on_packet_data = cb_data; } /* * Wraps `packet` in a header comprised of the custom packet type, game type and game id. */ static int game_packet_wrap(const GameData *game, uint8_t *packet, size_t size, GamePacketType packet_type) { if (size < GAME_PACKET_HEADER_SIZE + 1) { return -1; } if (packet_type != GP_Invite && packet_type != GP_Data) { return -1; } packet[0] = packet_type == GP_Data ? CUSTOM_PACKET_GAME_DATA : CUSTOM_PACKET_GAME_INVITE; packet[1] = GAME_NETWORKING_VERSION; packet[2] = game->type; game_util_pack_u32(packet + 3, game->id); return 0; } int game_packet_send(const GameData *game, const uint8_t *data, size_t length, GamePacketType packet_type) { if (length > GAME_MAX_DATA_SIZE) { return -1; } uint8_t packet[GAME_MAX_PACKET_SIZE]; if (game_packet_wrap(game, packet, sizeof(packet), packet_type) == -1) { return -1; } size_t packet_length = 1 + GAME_PACKET_HEADER_SIZE; // 1 extra byte for custom packet type memcpy(packet + 1 + GAME_PACKET_HEADER_SIZE, data, length); packet_length += length; TOX_ERR_FRIEND_CUSTOM_PACKET err; if (!tox_friend_send_lossless_packet(game->tox, game->friend_number, packet, packet_length, &err)) { fprintf(stderr, "failed to send game packet: error %d\n", err); return -1; } return 0; } toxic-0.11.3/src/game_base.h000066400000000000000000000264641416141666600156140ustar00rootroot00000000000000/* game_base.h * * * Copyright (C) 2020 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef GAME_BASE #define GAME_BASE #include #include #include #include "game_util.h" #include "windows.h" #define GAME_BORDER_COLOUR BAR_SOLID /* Max size of a default square game window */ #define GAME_MAX_SQUARE_Y_DEFAULT 26 #define GAME_MAX_SQUARE_X_DEFAULT (GAME_MAX_SQUARE_Y_DEFAULT * 2) /* Max size of a large square game window */ #define GAME_MAX_SQUARE_Y_LARGE 52 #define GAME_MAX_SQUARE_X_LARGE (GAME_MAX_SQUARE_Y_LARGE * 2) /* Max size of a default size rectangle game window */ #define GAME_MAX_RECT_Y_DEFAULT 24 #define GAME_MAX_RECT_X_DEFAULT (GAME_MAX_RECT_Y_DEFAULT * 4) /* Max size of a large rectangle game window */ #define GAME_MAX_RECT_Y_LARGE 52 #define GAME_MAX_RECT_X_LARGE (GAME_MAX_RECT_Y_LARGE * 4) /* Maximum length of a game message set with game_set_message() */ #define GAME_MAX_MESSAGE_SIZE 64 /* Default number of seconds a game message is displayed for */ #define GAME_MESSAGE_DEFAULT_TIMEOUT 3 /***** START NETWORKING CONSTANTS *****/ /* Header starts after custom packet type byte. Comprised of: NetworkVersion (1b) + GameType (1b) + id (4b) */ #define GAME_PACKET_HEADER_SIZE (1 + 1 + sizeof(uint32_t)) /* Max size of a game packet including the header */ #define GAME_MAX_PACKET_SIZE 1024 /* Max size of a game packet payload */ #define GAME_MAX_DATA_SIZE (GAME_MAX_PACKET_SIZE - GAME_PACKET_HEADER_SIZE - 1) /* Current version of networking protocol */ #define GAME_NETWORKING_VERSION 0x01 /***** END NETWORKING CONSTANTS *****/ typedef void cb_game_update_state(GameData *game, void *cb_data); typedef void cb_game_render_window(GameData *game, WINDOW *window, void *cb_data); typedef void cb_game_kill(GameData *game, void *cb_data); typedef void cb_game_pause(GameData *game, bool is_paused, void *cb_data); typedef void cb_game_key_press(GameData *game, int key, void *cb_data); typedef void cb_game_on_packet(GameData *game, const uint8_t *data, size_t length, void *cb_data); typedef enum GamePacketType { GP_Invite = 0u, GP_Data, } GamePacketType; typedef enum GameWindowShape { GW_ShapeSquare = 0u, GW_ShapeSquareLarge, GW_ShapeRectangle, GW_ShapeRectangleLarge, GW_ShapeInvalid, } GameWindowShape; typedef enum GameStatus { GS_None = 0u, GS_Paused, GS_Running, GS_Finished, GS_Invalid, } GameStatus; typedef enum GameType { GT_Centipede = 0u, GT_Chess, GT_Life, GT_Snake, GT_Invalid, } GameType; typedef struct GameMessage { char message[GAME_MAX_MESSAGE_SIZE + 1]; size_t length; const Coords *coords; // pointer to coords so we can track movement Coords original_coords; // static coords at time of being set time_t timeout; time_t set_time; int attributes; int colour; Direction direction; bool sticky; bool priority; } GameMessage; struct GameData { TIME_MS last_frame_time; TIME_MS update_interval; // determines the refresh rate (lower means faster) long int score; size_t high_score; int lives; size_t level; GameStatus status; GameType type; bool is_multiplayer; bool show_lives; bool show_score; bool show_high_score; bool show_level; GameMessage *messages; size_t messages_size; int game_max_x; // max dimensions of game window int game_max_y; int parent_max_x; // max dimensions of parent window int parent_max_y; int window_id; WINDOW *window; Tox *tox; // must be locked with Winthread mutex GameWindowShape window_shape; uint32_t id; // indentifies multiplayer instance uint32_t friend_number; // friendnumber associated with parent window cb_game_update_state *cb_game_update_state; void *cb_game_update_state_data; cb_game_render_window *cb_game_render_window; void *cb_game_render_window_data; cb_game_kill *cb_game_kill; void *cb_game_kill_data; cb_game_pause *cb_game_pause; void *cb_game_pause_data; cb_game_key_press *cb_game_key_press; void *cb_game_key_press_data; cb_game_on_packet *cb_game_on_packet; void *cb_game_on_packet_data; }; /* * Sets the callback for game state updates. */ void game_set_cb_update_state(GameData *game, cb_game_update_state *func, void *cb_data); /* * Sets the callback for frame rendering. */ void game_set_cb_render_window(GameData *game, cb_game_render_window *func, void *cb_data); /* * Sets the callback for game termination. */ void game_set_cb_kill(GameData *game, cb_game_kill *func, void *cb_data); /* * Sets the callback for the game pause event. */ void game_set_cb_on_pause(GameData *game, cb_game_pause *func, void *cb_data); /* * Sets the callback for the key press event. */ void game_set_cb_on_keypress(GameData *game, cb_game_key_press *func, void *cb_data); /* * Sets the callback for the game packet event. */ void game_set_cb_on_packet(GameData *game, cb_game_on_packet *func, void *cb_data); /* * Initializes game instance. * * `type` must be a valid GameType. * * `id` should be a unique integer to indentify the game instance. If we're being invited to a game * this identifier should be sent via the invite packet. * * if `multiplayer_data` is non-null this indicates that we accepted a game invite from a contact. * The data contains any information we need to initialize the game state. * * Return 0 on success. * Return -1 if screen is too small. * Return -2 on network related error. * Return -3 if multiplayer game is being initialized outside of a contact's window. * Return -4 on other failure. */ int game_initialize(const ToxWindow *self, Tox *m, GameType type, uint32_t id, const uint8_t *multiplayer_data, size_t length); /* * Sets game window to `shape`. * * This must be called on game initialization. * * Return 0 on success. * Return -1 if window is too small or shape is invalid. * Return -2 if function is called while the game state is valid. */ int game_set_window_shape(GameData *game, GameWindowShape shape); /* * Returns the GameType associated with `game_string`. */ GameType game_get_type(const char *game_string); /* * Returns the name represented as a string associated with `type`. */ const char *game_get_name_string(GameType type); /* * Prints all available games to window associated with `self`. */ void game_list_print(ToxWindow *self); /* * Return true if game `type` has a multiplayer mode. */ bool game_type_is_multiplayer(GameType type); /* * Returns true if coordinates designated by `x` and `y` are within the game window boundaries. */ bool game_coordinates_in_bounds(const GameData *game, int x, int y); /* * Put random coordinates that fit within the game window in `coords`. */ void game_random_coords(const GameData *game, Coords *coords); /* * Gets the current max dimensions of the game window. */ void game_max_x_y(const GameData *game, int *x, int *y); /* * Returns the respective coordinate boundary of the game window. */ int game_y_bottom_bound(const GameData *game); int game_y_top_bound(const GameData *game); int game_x_right_bound(const GameData *game); int game_x_left_bound(const GameData *game); /* * Toggle whether the respective game info is shown around the game window. */ void game_show_score(GameData *game, bool show_score); void game_show_high_score(GameData *game, bool show_high_score); void game_show_lives(GameData *game, bool show_lives); void game_show_level(GameData *game, bool show_level); /* * Sends a notification to the window associated with `game`. * * `message` - the notification message that will be displayed. */ void game_window_notify(const GameData *game, const char *message); /* * Updates game score. */ void game_update_score(GameData *game, long int points); /* * Sets game score to `val`. */ void game_set_score(GameData *game, long int score); /* * Returns the game's current score. */ long int game_get_score(const GameData *game); /* * Increments level. * * This function should be called on initialization if game wishes to display level. */ void game_increment_level(GameData *game); /* * Updates lives with `amount`. * * If lives becomes negative the lives counter will not be drawn. */ void game_update_lives(GameData *game, int amount); /* * Returns the remaining number of lives for the game. */ int game_get_lives(const GameData *game); /* * Returns the current level. */ size_t game_get_current_level(const GameData *game); /* * Sets the game status to `status`. */ void game_set_status(GameData *game, GameStatus status); /* * Sets the game base update interval. * * Lower values of `update_interval` make the game faster, where 1 is the fastest and 50 is slowest. * If this function is never called the game chooses a reasonable default. */ void game_set_update_interval(GameData *game, TIME_MS update_interval); /* * Creates a message `message` of size `length` to be displayed at `coords` for `timeout` seconds. * * Message must be no greater than GAME_MAX_MESSAGE_SIZE bytes in length. * * If `sticky` is true the message will follow coords if they move. * * If `dir` is a valid direction, the message will be positioned a few squares away from `coords` * so as to not overlap with its associated object. * * If `timeout` is zero, the default timeout value will be used. * * If `priority` true, messages will be printed on top of game objects. * * Return 0 on success. * Return -1 on failure. */ int game_set_message(GameData *game, const char *message, size_t length, Direction dir, int attributes, int colour, time_t timeout, const Coords *coords, bool sticky, bool priority); /* * Returns true if game should update an object's state according to its last moved time and current speed. * * This is used to independently control the speed of various game objects. */ bool game_do_object_state_update(const GameData *game, TIME_MS current_time, TIME_MS last_moved_time, TIME_MS speed); /* * Returns the current wall time in milliseconds. */ TIME_MS get_time_millis(void); /* * Ends game associated with `self` and cleans up. */ void game_kill(ToxWindow *self); /* * Sends a packet containing payload `data` of size `length` to the friendnumber associated with the game's * parent window. * * `length` must not exceed GAME_MAX_DATA_SIZE bytes. * * `packet_type` should be GP_Invite for an invite packet or GP_Data for all other game data. */ int game_packet_send(const GameData *game, const uint8_t *data, size_t length, GamePacketType packet_type); #endif // GAME_BASE toxic-0.11.3/src/game_centipede.c000066400000000000000000001345441416141666600166340ustar00rootroot00000000000000/* game_centipede.c * * * Copyright (C) 2020 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #include #include #include #include "game_centipede.h" #include "game_base.h" #include "game_util.h" #include "misc_tools.h" /* Determines how many mushrooms are spawned at the start of a game relative to window size (higher values means fewer) */ #define CENT_MUSHROOMS_POP_CONSTANT 35000 /* Max number of mushrooms */ #define CENT_MUSHROOMS_LENGTH (GAME_MAX_SQUARE_X_DEFAULT * GAME_MAX_SQUARE_X_DEFAULT) /* Max number of individual centipedes at any given time */ #define CENT_MAX_NUM_HEADS 20 /* Max number of segments that a centipede can have */ #define CENT_MAX_NUM_SEGMENTS 12 /* Get a free life every time we get this many points. Needs to be > the most points we can get in a single shot. */ #define CENT_SCORE_ONE_UP 7000 /* Max number of lives we can have */ #define CENT_MAX_LIVES 6 /* How many lives we start with */ #define CENT_START_LIVES 3 /* Max speed of an enemy agent */ #define CENT_MAX_ENEMY_AGENT_SPEED 8 /* Determines the overall speed of the game per game_set_update_interval() */ #define CENT_GAME_UPDATE_INTERVAL 14 /* How often a head that reaches the bottom can repdoduce */ #define CENT_REPRODUCE_TIMEOUT 10 #define CENT_CENTIPEDE_DEFAULT_SPEED 5 #define CENT_CENTIPEDE_ATTR A_BOLD #define CENT_CENTIPEDE_SEG_CHAR '8' #define CENT_CENTIPEDE_HEAD_CHAR '8' #define CENT_BULLET_COLOUR YELLOW #define CENT_BULLET_ATTR A_BOLD #define CENT_BULLET_CHAR '|' #define CENT_BULLET_SPEED 300 #define CENT_BLASTER_ATTR A_BOLD #define CENT_BLASTER_CHAR 'U' #define CENT_BLASTER_SPEED 10 #define CENT_MUSH_DEFAULT_HEALTH 4 #define CENT_MUSH_DEFAULT_ATTR A_BOLD #define CENT_MUSH_DEFAULT_CHAR '0' #define CENT_SPIDER_SPAWN_TIMER 7 // has a 75% chance of spawning each timeout #define CENT_SPIDER_DEFAULT_SPEED 1 #define CENT_SPIDER_DEFAULT_ATTR A_BOLD #define CENT_SPIDER_CHAR 'X' #define CENT_SPIDER_START_HEALTH 1 #define CENT_FLEA_SPAWN_TIMER 15 // has a 75% chance of spawning each timeout #define CENT_FLEA_DEFAULT_SPEED 2 #define CENT_FLEA_CHAR 'Y' #define CENT_FLEA_DEFAULT_ATTR A_BOLD #define CENT_FLEA_POINTS 200 #define CENT_FLEA_START_HEALTH 2 #define CENT_SCORPION_BASE_SPAWN_TIMER 30 #define CENT_SCORPION_DEFAULT_SPEED 2 #define CENT_SCORPION_DEFAULT_ATTR A_BOLD #define CENT_SCORPTION_CHAR '&' #define CENT_SCORPTION_POINTS 1000 #define CENT_SCORPTION_START_HEALTH 1 /* * Determines how far north on the Y axis the blaster can move, how far north centipedes can move when * moving north, and the point at which fleas will stop creating mushrooms. */ #define CENT_INVISIBLE_H_WALL 5 #define CENT_KEY_FIRE ' ' typedef struct EnemyAgent { Coords coords; Direction direction; Direction start_direction; int colour; int attributes; char display_char; size_t speed; TIME_MS last_time_moved; TIME_S last_time_despawned; bool was_killed; size_t health; } EnemyAgent; typedef struct Mushroom { Coords coords; size_t health; int colour; int attributes; char display_char; bool is_poisonous; } Mushroom; typedef struct Blaster { Coords coords; Coords start_coords; size_t speed; TIME_MS last_time_moved; int colour; int attributes; Direction direction; } Blaster; typedef struct Projectile { Coords coords; size_t speed; TIME_MS last_time_moved; int colour; int attributes; } Projectile; /* * A centipede is a doubly linked list comprised of one or more Segments. The head of the centipede * is the root. All non-head segments follow their `prev` segment. When a non-tail segment is destroyed, * its `next` segment becomes a head. */ typedef struct Segment Segment; struct Segment { Coords coords; Direction h_direction; Direction v_direction; int colour; int attributes; int display_char; size_t poison_rot; bool is_fertile; TIME_S last_time_reproduced; TIME_MS last_time_moved; Segment *prev; Segment *next; }; typedef struct Centipedes { Segment *heads[CENT_MAX_NUM_HEADS]; size_t heads_length; } Centipedes; typedef struct CentState { Centipedes centipedes; Mushroom *mushrooms; size_t mushrooms_length; EnemyAgent spider; EnemyAgent flea; EnemyAgent scorpion; Projectile bullet; Blaster blaster; TIME_S pause_time; bool game_over; } CentState; /* * Evaluates to true if vertically moving bullet (x2,y2) has hit or passed vertically moving object (x1,y1). * This should only be used if both the bullet and object never move on the x axis. */ #define CENT_VERTICAL_IMPACT(x1, y1, x2, y2)(((x1) == (x2)) && ((y1) >= (y2))) #define CENT_LEVEL_COLOURS_SIZE 19 static const int cent_level_colours[] = { RED, CYAN, MAGENTA, BLUE, BLUE, RED, YELLOW, GREEN, GREEN, CYAN, YELLOW, MAGENTA, BLUE, GREEN, RED, MAGENTA, CYAN, YELLOW, WHITE, }; static int cent_mushroom_colour(size_t level) { return cent_level_colours[level % CENT_LEVEL_COLOURS_SIZE]; } static int cent_head_colour(size_t level) { return cent_level_colours[(level + 1) % CENT_LEVEL_COLOURS_SIZE]; } static int cent_spider_colour(size_t level) { return cent_level_colours[(level + 2) % CENT_LEVEL_COLOURS_SIZE]; } static int cent_segment_colour(size_t level) { return cent_level_colours[(level + 3) % CENT_LEVEL_COLOURS_SIZE]; } static int cent_flea_colour(size_t level) { return cent_level_colours[(level + 4) % CENT_LEVEL_COLOURS_SIZE]; } static int cent_scorpion_colour(size_t level) { return cent_level_colours[(level + 5) % CENT_LEVEL_COLOURS_SIZE]; } static int cent_blaster_colour(size_t level) { return cent_level_colours[(level + 6) % CENT_LEVEL_COLOURS_SIZE]; } static int cent_poisonous_mush_colour(size_t level) { int colour = cent_mushroom_colour(level); switch (colour) { case RED: return YELLOW; case YELLOW: return RED; case CYAN: return MAGENTA; case MAGENTA: return CYAN; case BLUE: return GREEN; case GREEN: return BLUE; default: return RED; } } static size_t cent_enemy_agent_speed(size_t base_speed, size_t level) { if (level < 2) { return base_speed; } int r = rand() % (level / 2); return MIN(base_speed + r, CENT_MAX_ENEMY_AGENT_SPEED); } static void cent_update_score(GameData *game, const CentState *state, long int points, const Coords *coords) { char buf[GAME_MAX_MESSAGE_SIZE + 1]; long int prev_score = game_get_score(game); game_update_score(game, points); long int score = game_get_score(game); int lives = game_get_lives(game); if ((lives < CENT_MAX_LIVES) && ((score % CENT_SCORE_ONE_UP) < (prev_score % CENT_SCORE_ONE_UP))) { game_update_lives(game, 1); snprintf(buf, sizeof(buf), "%s", "1UP!"); if (game_set_message(game, buf, strlen(buf), NORTH, A_BOLD, WHITE, 0, &state->blaster.coords, false, false) == -1) { fprintf(stderr, "failed to set points message\n"); } } if (coords == NULL) { return; } snprintf(buf, sizeof(buf), "%ld", points); if (game_set_message(game, buf, strlen(buf), NORTH, A_BOLD, WHITE, 0, coords, false, true) == -1) { fprintf(stderr, "failed to set points message\n"); } } static void cent_enemy_despawn(EnemyAgent *agent, bool was_killed) { memset(agent, 0, sizeof(EnemyAgent)); agent->last_time_despawned = get_unix_time(); agent->was_killed = was_killed; } static void cent_bullet_reset(Projectile *bullet) { bullet->coords.x = -1; bullet->coords.y = -1; } static long int cent_spider_points(const Coords *spider_coords, const Coords *blaster_coords) { const int y_dist = blaster_coords->y - spider_coords->y; if (y_dist > 3) { return 300; } if (y_dist > 1) { return 600; } return 900; } static bool cent_centipedes_are_dead(Centipedes *centipedes) { for (size_t i = 0; i < centipedes->heads_length; ++i) { if (centipedes->heads[i] != NULL) { return false; } } return true; } static void cent_kill_centipede(Centipedes *centipedes, size_t index) { Segment *head = centipedes->heads[index]; while (head) { Segment *tmp1 = head->next; free(head); head = tmp1; } centipedes->heads[index] = NULL; } static void cent_poison_centipede(Segment *segment) { if (segment->poison_rot == 0) { while (segment != NULL) { segment->poison_rot = 1; segment = segment->next; } } } static void cent_cure_centipede(Segment *segment) { if (segment->poison_rot > 0) { while (segment != NULL) { segment->poison_rot = 0; segment = segment->next; } } } static void cent_exterminate_centipedes(Centipedes *centipedes) { for (size_t i = 0; i < centipedes->heads_length; ++i) { if (centipedes->heads[i] != NULL) { cent_kill_centipede(centipedes, i); } } centipedes->heads_length = 0; } static void cent_move_segments(Segment *head) { if (head == NULL) { return; } Segment *current = head; while (current->next) { current = current->next; } Segment *prev = current->prev; while (prev) { current->coords.x = prev->coords.x; current->coords.y = prev->coords.y; current->h_direction = prev->h_direction; current->v_direction = prev->v_direction; current = prev; prev = prev->prev; } } static int cent_new_centipede_head_index(Centipedes *centipedes) { for (size_t i = 0; i < CENT_MAX_NUM_HEADS; ++i) { if (centipedes->heads[i] != NULL) { continue; } if (i == centipedes->heads_length) { ++centipedes->heads_length; } return i; } return -1; } static int cent_birth_centipede(const GameData *game, CentState *state, size_t length, Direction direction, const Coords *coords) { if (length > CENT_MAX_NUM_SEGMENTS) { return -1; } Centipedes *centipedes = &state->centipedes; int head_idx = cent_new_centipede_head_index(centipedes); if (head_idx == -1) { return -1; } Segment *new_head = calloc(1, sizeof(Segment)); if (new_head == NULL) { return -1; } if (coords == NULL) { const int x_left = game_x_left_bound(game); const int x_right = game_x_right_bound(game); const int y_top = game_y_top_bound(game); new_head->coords.x = direction == EAST ? x_left : x_right; new_head->coords.y = y_top; } else { new_head->coords.x = coords->x; new_head->coords.y = coords->y; } size_t level = game_get_current_level(game); new_head->h_direction = direction; new_head->v_direction = SOUTH; new_head->colour = cent_head_colour(level); new_head->attributes = CENT_CENTIPEDE_ATTR; new_head->display_char = CENT_CENTIPEDE_HEAD_CHAR; new_head->prev = NULL; centipedes->heads[head_idx] = new_head; Segment *prev = new_head; for (size_t i = 0; i < length; ++i) { Segment *new_seg = calloc(1, sizeof(Segment)); if (new_seg == NULL) { cent_kill_centipede(centipedes, head_idx); return -1; } new_seg->colour = cent_segment_colour(level); new_seg->attributes = CENT_CENTIPEDE_ATTR; new_seg->display_char = CENT_CENTIPEDE_SEG_CHAR; new_seg->coords.x = -1; // coords will update as it moves new_seg->coords.y = -1; new_seg->h_direction = direction; new_seg->v_direction = SOUTH; new_seg->prev = prev; prev->next = new_seg; prev = new_seg; } return 0; } static int cent_init_level_centipedes(const GameData *game, CentState *state, size_t level) { Direction dir = rand() % 2 == 0 ? WEST : EAST; // First level we spawn one full size centipede if (level == 1) { if (cent_birth_centipede(game, state, CENT_MAX_NUM_SEGMENTS, dir, NULL) == -1) { return -1; } return 0; } const int c = ((int)level - 1); const int diff = CENT_MAX_NUM_SEGMENTS - c; const int y_top = game_y_top_bound(game); const int x_left = game_x_left_bound(game); const int x_right = game_x_right_bound(game); const int remainder = diff > 4 ? c : CENT_MAX_NUM_SEGMENTS; // for the next few levels we spawn one multi-seg centipede // decreasing in size and progressively more lone heads if (diff > 4) { if (cent_birth_centipede(game, state, diff, dir, NULL) == -1) { return -1; } } // Spawn loan heads for (size_t i = 0; i < remainder; ++i) { dir = i % 2 == 0 ? EAST : WEST; Coords coords; coords.x = dir == EAST ? x_left - i : x_right + i; coords.y = i % 2 == 0 ? y_top + 1 : y_top + 2; if (cent_birth_centipede(game, state, 0, dir, &coords) == -1) { return -1; } } return 0; } static int cent_restart_level(GameData *game, CentState *state) { Mushroom *mushrooms = state->mushrooms; for (size_t i = 0; i < state->mushrooms_length; ++i) { Mushroom *mush = &mushrooms[i]; if (mush->health > 0) { if (mush->health < CENT_MUSH_DEFAULT_HEALTH) { cent_update_score(game, state, 5, NULL); } mush->health = CENT_MUSH_DEFAULT_HEALTH; mush->display_char = CENT_MUSH_DEFAULT_CHAR; mush->attributes = CENT_MUSH_DEFAULT_ATTR; } } cent_enemy_despawn(&state->spider, false); cent_enemy_despawn(&state->flea, false); cent_enemy_despawn(&state->scorpion, false); cent_exterminate_centipedes(&state->centipedes); size_t level = game_get_current_level(game); if (cent_init_level_centipedes(game, state, level) == -1) { return -1; } state->blaster.coords.x = state->blaster.start_coords.x; state->blaster.coords.y = state->blaster.start_coords.y; cent_bullet_reset(&state->bullet); return 0; } static int cent_next_level(GameData *game, CentState *state) { game_increment_level(game); size_t level = game_get_current_level(game); Mushroom *mushrooms = state->mushrooms; for (size_t i = 0; i < state->mushrooms_length; ++i) { Mushroom *mush = &mushrooms[i]; if (mush->health > 0) { mush->colour = mush->is_poisonous ? cent_poisonous_mush_colour(level) : cent_mushroom_colour(level); } } cent_exterminate_centipedes(&state->centipedes); if (cent_init_level_centipedes(game, state, level) == -1) { return -1; } state->blaster.colour = cent_blaster_colour(level); state->spider.colour = cent_spider_colour(level); state->flea.colour = cent_flea_colour(level); state->scorpion.colour = cent_scorpion_colour(level); cent_bullet_reset(&state->bullet); return 0; } static void cent_deduct_life(GameData *game, CentState *state) { game_update_lives(game, -1); int lives = game_get_lives(game); if (lives == 0) { game_set_status(game, GS_Finished); state->game_over = true; } else { if (cent_restart_level(game, state) != 0) { fprintf(stderr, "Failed to restart level\n"); } } } static void cent_update_mush_appearance(const GameData *game, const CentState *state, Mushroom *mushroom) { if (mushroom->health > CENT_MUSH_DEFAULT_HEALTH) { return; } switch (mushroom->health) { case CENT_MUSH_DEFAULT_HEALTH: { size_t level = game_get_current_level(game); mushroom->colour = mushroom->is_poisonous ? cent_poisonous_mush_colour(level) : cent_mushroom_colour(level); mushroom->attributes = CENT_MUSH_DEFAULT_ATTR; mushroom->display_char = CENT_MUSH_DEFAULT_CHAR; break; } case 3: { mushroom->display_char = 'o'; break; } case 2: { mushroom->display_char = 'c'; break; } case 1: { mushroom->display_char = ';'; break; } default: { return; } } } static Mushroom *cent_get_mushroom_at_coords(CentState *state, const Coords *coords) { for (size_t i = 0; i < state->mushrooms_length; ++i) { Mushroom *mush = &state->mushrooms[i]; if (mush->health == 0) { continue; } if (COORDINATES_OVERLAP(coords->x, coords->y, mush->coords.x, mush->coords.y)) { return mush; } } return NULL; } static Mushroom *cent_mushroom_new(CentState *state) { Mushroom *mushrooms = state->mushrooms; for (size_t i = 0; i < CENT_MUSHROOMS_LENGTH; ++i) { Mushroom *mush = &mushrooms[i]; if (mush->health != 0) { continue; } if (i == state->mushrooms_length) { state->mushrooms_length = i + 1; } return &mushrooms[i]; } return NULL; } static void cent_mushroom_grow(const GameData *game, CentState *state, const Coords *coords, bool is_poisonous) { if (cent_get_mushroom_at_coords(state, coords) != NULL) { return; } if (!game_coordinates_in_bounds(game, coords->x, coords->y)) { return; } if (game_y_bottom_bound(game) == coords->y) { // can't be hit by blaster on the floor return; } Mushroom *mush = cent_mushroom_new(state); if (mush == NULL) { return; } mush->is_poisonous = is_poisonous; mush->health = CENT_MUSH_DEFAULT_HEALTH; mush->coords.x = coords->x; mush->coords.y = coords->y; cent_update_mush_appearance(game, state, mush); } static bool cent_blaster_enemy_collision(CentState *state, const EnemyAgent *enemy) { Blaster *blaster = &state->blaster; if (enemy->health == 0) { return false; } if (COORDINATES_OVERLAP(blaster->coords.x, blaster->coords.y, enemy->coords.x, enemy->coords.y)) { return true; } return false; } static void cent_blaster_move(GameData *game, CentState *state, TIME_MS cur_time) { Blaster *blaster = &state->blaster; if (!GAME_UTIL_DIRECTION_VALID(blaster->direction)) { return; } const TIME_MS real_speed = GAME_UTIL_REAL_SPEED(blaster->direction, blaster->speed); if (!game_do_object_state_update(game, cur_time, blaster->last_time_moved, real_speed)) { return; } blaster->last_time_moved = cur_time; Coords new_coords = (Coords) { blaster->coords.x, blaster->coords.y }; game_util_move_coords(blaster->direction, &new_coords); blaster->direction = INVALID_DIRECTION; const int y_bottom = game_y_bottom_bound(game); if (new_coords.y < y_bottom - CENT_INVISIBLE_H_WALL) { return; } if (!game_coordinates_in_bounds(game, new_coords.x, new_coords.y)) { return; } if (cent_get_mushroom_at_coords(state, &new_coords) != NULL) { return; } blaster->coords.x = new_coords.x; blaster->coords.y = new_coords.y; } static bool cent_bullet_mushroom_collision(GameData *game, CentState *state, const Coords *coords) { Mushroom *mush = cent_get_mushroom_at_coords(state, coords); if (mush == NULL) { return false; } --mush->health; cent_update_mush_appearance(game, state, mush); if (mush->health == 0) { memset(mush, 0, sizeof(Mushroom)); cent_update_score(game, state, 1, NULL); } return true; } static void cent_bullet_move(const GameData *game, CentState *state, TIME_MS cur_time) { Projectile *bullet = &state->bullet; if (!game_do_object_state_update(game, cur_time, bullet->last_time_moved, bullet->speed)) { return; } bullet->last_time_moved = cur_time; const int y_top = game_y_top_bound(game); --bullet->coords.y; if (bullet->coords.y < y_top) { cent_bullet_reset(bullet); } } void cent_bullet_spawn(CentState *state) { Projectile *bullet = &state->bullet; if (bullet->coords.x > 0) { return; } Blaster blaster = state->blaster; bullet->coords.x = blaster.coords.x; bullet->coords.y = blaster.coords.y; bullet->speed = CENT_BULLET_SPEED; } /* Destroys centipede segment `seg`. If seg is a head, its `next` segment is assigned as the new head for * the rest of the centipede. If seg is a tail, its `prev` segment is assigned as the new tail. * if seg is somewher in the middle, the centipede is split into two; its prev is assigned as a new * tail, and its `next` is assigned as a new head. * * Returns points value for the segment (100 for head, 10 for non-head). * Returns 0 if head limit has been reached. */ static long int cent_kill_centipede_segment(const GameData *game, Centipedes *centipedes, Segment *seg, size_t index) { if (seg->prev == NULL) { // head if (seg->next == NULL) { // lone head free(seg); centipedes->heads[index] = NULL; return 100; } seg->next->display_char = seg->display_char; seg->next->colour = seg->colour; seg->next->attributes = seg->attributes; seg->next->poison_rot = seg->poison_rot; seg->next->prev = NULL; centipedes->heads[index] = seg->next; free(seg); return 100; } if (seg->next == NULL) { // tail seg->prev->next = NULL; free(seg); return 10; } // somewhere in the middle int idx = cent_new_centipede_head_index(centipedes); if (idx == -1) { return 0; } seg->next->prev = NULL; // next becomes head seg->prev->next = NULL; // prev becomes tail seg->next->display_char = CENT_CENTIPEDE_HEAD_CHAR; seg->next->attributes = CENT_CENTIPEDE_ATTR; seg->next->poison_rot = seg->prev->poison_rot; size_t level = game_get_current_level(game); seg->next->colour = cent_head_colour(level); centipedes->heads[idx] = seg->next; free(seg); return 10; } static bool cent_bullet_centipede_collision(GameData *game, CentState *state) { Centipedes *centipedes = &state->centipedes; Projectile *bullet = &state->bullet; for (size_t i = 0; i < centipedes->heads_length; ++i) { Segment *seg = centipedes->heads[i]; if (seg == NULL) { continue; } while (seg) { if (COORDINATES_OVERLAP(seg->coords.x, seg->coords.y, bullet->coords.x, bullet->coords.y)) { Coords mush_coords; mush_coords.x = seg->h_direction == WEST ? seg->coords.x - 1 : seg->coords.x + 1; mush_coords.y = seg->coords.y; cent_mushroom_grow(game, state, &mush_coords, false); long int points = cent_kill_centipede_segment(game, centipedes, seg, i); cent_update_score(game, state, points, NULL); return true; } seg = seg->next; } } return false; } /* * Checks if bullet has collided with a mushroom or enemy and updates score appropriately. * If objects overlap they're all hit. */ static void cent_bullet_collision_check(GameData *game, CentState *state) { Projectile *bullet = &state->bullet; if (bullet->coords.x <= 0) { return; } bool collision = false; if (cent_bullet_mushroom_collision(game, state, &bullet->coords)) { collision = true; } EnemyAgent *spider = &state->spider; if (spider->health > 0 && COORDINATES_OVERLAP(spider->coords.x, spider->coords.y, bullet->coords.x, bullet->coords.y)) { long int points = cent_spider_points(&spider->coords, &state->blaster.coords); cent_update_score(game, state, points, &spider->coords); cent_enemy_despawn(spider, true); collision = true; } EnemyAgent *scorpion = &state->scorpion; if (scorpion->health > 0 && COORDINATES_OVERLAP(scorpion->coords.x, scorpion->coords.y, bullet->coords.x, bullet->coords.y)) { cent_update_score(game, state, CENT_SCORPTION_POINTS, NULL); cent_enemy_despawn(scorpion, true); collision = true; } EnemyAgent *flea = &state->flea; if (flea->health > 0 && CENT_VERTICAL_IMPACT(flea->coords.x, flea->coords.y, bullet->coords.x, bullet->coords.y)) { if (--flea->health == 0) { cent_update_score(game, state, CENT_FLEA_POINTS, NULL); cent_enemy_despawn(flea, true); } else { flea->speed += 5; } collision = true; } if (cent_bullet_centipede_collision(game, state)) { collision = true; if (cent_centipedes_are_dead(&state->centipedes)) { cent_next_level(game, state); } } if (collision) { cent_bullet_reset(bullet); } } static void cent_set_head_direction(CentState *state, Segment *head, int y_bottom, int x_left, int x_right) { Coords next_coords = (Coords) { head->coords.x, head->coords.y }; // Move horizontally until we hit a mushroom or a wall, at which point we move down one square // and continue in the other direction. if (head->h_direction == WEST) { next_coords.x = head->coords.x - 1; Mushroom *mush = cent_get_mushroom_at_coords(state, &next_coords); if (head->coords.x <= x_left || mush != NULL) { if (mush && mush->is_poisonous) { cent_poison_centipede(head); } head->h_direction = EAST; head->coords.y = head->v_direction == SOUTH ? head->coords.y + 1 : head->coords.y - 1; } else { --head->coords.x; } } else if (head->h_direction == EAST) { next_coords.x = head->coords.x + 1; Mushroom *mush = cent_get_mushroom_at_coords(state, &next_coords); if (head->coords.x >= x_right || mush != NULL) { if (mush && mush->is_poisonous) { cent_poison_centipede(head); } head->h_direction = WEST; head->coords.y = head->v_direction == SOUTH ? head->coords.y + 1 : head->coords.y - 1; } else { ++head->coords.x; } } // if we touched a poison mushroom we move south every two steps if (head->poison_rot == 2) { ++head->coords.y; head->h_direction = head->h_direction == EAST ? WEST : EAST; head->poison_rot = 1; } else if (head->poison_rot > 0) { ++head->poison_rot; } // if we hit the bottom boundary we turn north. if we're going north we only go up to the invisible wall // and turn back around. if (head->v_direction == SOUTH && head->coords.y > y_bottom) { head->coords.y -= 2; head->v_direction = NORTH; cent_cure_centipede(head); head->is_fertile = true; head->last_time_reproduced = get_unix_time(); } else if (head->v_direction == NORTH && head->coords.y < y_bottom - CENT_INVISIBLE_H_WALL) { head->coords.y += 2; head->v_direction = SOUTH; } } /* * If a head has reached the bottom it reproduces (spawns an additional head) on a timer. */ static void cent_do_reproduce(const GameData *game, CentState *state, Segment *head, int x_right, int x_left, int y_bottom) { if (!head->is_fertile) { return; } if (!timed_out(head->last_time_reproduced, CENT_REPRODUCE_TIMEOUT)) { return; } Direction dir = rand() % 2 == 0 ? WEST : EAST; Coords new_coords; new_coords.x = dir == EAST ? x_left : x_right; new_coords.y = y_bottom - (rand() % CENT_INVISIBLE_H_WALL); if (cent_birth_centipede(game, state, 0, dir, &new_coords) == 0) { head->last_time_reproduced = get_unix_time(); } } static void cent_do_centipede(const GameData *game, CentState *state, TIME_MS cur_time) { Centipedes *centipedes = &state->centipedes; if (centipedes->heads_length == 0) { return; } const int y_bottom = game_y_bottom_bound(game); const int x_left = game_x_left_bound(game); const int x_right = game_x_right_bound(game); for (size_t i = 0; i < centipedes->heads_length; ++i) { Segment *head = centipedes->heads[i]; if (head == NULL) { continue; } // half speed if poisoned TIME_MS real_speed = head->poison_rot > 0 ? (CENT_CENTIPEDE_DEFAULT_SPEED / 2) + 1 : CENT_CENTIPEDE_DEFAULT_SPEED; if (!game_do_object_state_update(game, cur_time, head->last_time_moved, real_speed)) { continue; } head->last_time_moved = cur_time; cent_move_segments(head); cent_set_head_direction(state, head, y_bottom, x_left, x_right); if (head->coords.x == x_left || head->coords.x == x_right) { cent_do_reproduce(game, state, head, x_right, x_left, y_bottom); } } } static void cent_try_spawn_flea(const GameData *game, EnemyAgent *flea) { if (!flea->was_killed) { if (!timed_out(flea->last_time_despawned, CENT_FLEA_SPAWN_TIMER)) { return; } } flea->was_killed = false; if (rand() % 4 == 0) { return; } size_t level = game_get_current_level(game); flea->colour = cent_flea_colour(level); flea->speed = cent_enemy_agent_speed(CENT_FLEA_DEFAULT_SPEED, level); flea->attributes = CENT_FLEA_DEFAULT_ATTR; flea->display_char = CENT_FLEA_CHAR; flea->direction = SOUTH; flea->health = CENT_FLEA_START_HEALTH; const int y_top = game_y_top_bound(game); const int x_left = game_x_left_bound(game); const int x_right = game_x_right_bound(game); flea->coords.y = y_top; flea->coords.x = (rand() % (x_right - x_left + 1)) + x_left; } static void cent_do_flea(GameData *game, CentState *state, TIME_MS cur_time) { EnemyAgent *flea = &state->flea; if (flea->health == 0) { cent_try_spawn_flea(game, flea); return; } if (!game_do_object_state_update(game, cur_time, flea->last_time_moved, flea->speed)) { return; } flea->last_time_moved = cur_time; const int y_bottom = game_y_bottom_bound(game); if (flea->coords.y < (y_bottom - 5) && rand() % 4 == 0) { cent_mushroom_grow(game, state, &flea->coords, false); } ++flea->coords.y; if (flea->coords.y > game_y_bottom_bound(game)) { cent_enemy_despawn(flea, false); } } /* * Scorption spawn timeout is reduced linearly according to the level. She has a 75% chance of * spawning when the timeout expires, at which point it's reset whether she spawns or not. */ static bool cent_scorpion_spawn_check(EnemyAgent *scorpion, size_t level) { size_t decay = level * 2; TIME_S timeout = CENT_SCORPION_BASE_SPAWN_TIMER > decay ? CENT_SCORPION_BASE_SPAWN_TIMER - decay : 1; if (!timed_out(scorpion->last_time_despawned, timeout)) { return false; } scorpion->last_time_despawned = get_unix_time(); return (rand() % 4) < 3; } static void cent_try_spawn_scorpion(const GameData *game, CentState *state, EnemyAgent *scorpion) { size_t level = game_get_current_level(game); if (level < 2) { return; } if (!cent_scorpion_spawn_check(scorpion, level)) { return; } scorpion->colour = cent_scorpion_colour(level); scorpion->speed = CENT_SCORPION_DEFAULT_SPEED; scorpion->attributes = CENT_SCORPION_DEFAULT_ATTR; scorpion->display_char = CENT_SCORPTION_CHAR; scorpion->health = CENT_SCORPTION_START_HEALTH; scorpion->direction = rand() % 2 == 0 ? WEST : EAST; const int y_bottom = game_y_bottom_bound(game); const int x_left = game_x_left_bound(game); const int x_right = game_x_right_bound(game); const int y_top = game_y_top_bound(game); const int y_mid = y_top + ((y_bottom - y_top) / 2); scorpion->coords.x = scorpion->direction == WEST ? x_right : x_left; scorpion->coords.y = (y_mid - 5) + (rand() % 5); } static void cent_do_scorpion(GameData *game, CentState *state, TIME_MS cur_time) { EnemyAgent *scorpion = &state->scorpion; if (scorpion->health == 0) { cent_try_spawn_scorpion(game, state, scorpion); return; } if (!game_do_object_state_update(game, cur_time, scorpion->last_time_moved, scorpion->speed)) { return; } scorpion->last_time_moved = cur_time; Mushroom *mush = cent_get_mushroom_at_coords(state, &scorpion->coords); if (mush != NULL) { size_t level = game_get_current_level(game); mush->is_poisonous = true; mush->colour = cent_poisonous_mush_colour(level); } const int x_left = game_x_left_bound(game); const int x_right = game_x_right_bound(game); scorpion->coords.x = scorpion->direction == WEST ? scorpion->coords.x - 1 : scorpion->coords.x + 1; if (scorpion->coords.x > x_right || scorpion->coords.x < x_left) { cent_enemy_despawn(scorpion, false); } } static void cent_try_spawn_spider(const GameData *game, EnemyAgent *spider) { if (!timed_out(spider->last_time_despawned, CENT_SPIDER_SPAWN_TIMER)) { return; } if (rand() % 4 == 0) { spider->last_time_despawned = get_unix_time(); return; } size_t level = game_get_current_level(game); spider->colour = cent_spider_colour(level); spider->speed = cent_enemy_agent_speed(CENT_SPIDER_DEFAULT_SPEED, level); spider->attributes = CENT_SPIDER_DEFAULT_ATTR; spider->display_char = CENT_SPIDER_CHAR; spider->start_direction = rand() % 2 == 0 ? WEST : EAST; spider->direction = spider->start_direction; spider->health = CENT_SPIDER_START_HEALTH; const int y_bottom = game_y_bottom_bound(game); const int x_left = game_x_left_bound(game); const int x_right = game_x_right_bound(game); spider->coords.x = spider->direction == WEST ? x_right : x_left; spider->coords.y = (rand() % (y_bottom - (y_bottom - CENT_INVISIBLE_H_WALL))) + (y_bottom - CENT_INVISIBLE_H_WALL); } static void cent_do_spider(GameData *game, CentState *state, TIME_MS cur_time) { EnemyAgent *spider = &state->spider; if (spider->health == 0) { cent_try_spawn_spider(game, spider); return; } if (!game_do_object_state_update(game, cur_time, spider->last_time_moved, spider->speed)) { return; } spider->last_time_moved = cur_time; Coords new_coords = (Coords) { spider->coords.x, spider->coords.y, }; int r = rand(); if (spider->direction == spider->start_direction) { if (r % 4 == 0) { spider->direction = r % 3 == 0 ? NORTH : SOUTH; } } else { if (r % 5 == 0) { spider->direction = spider->start_direction; } } game_util_move_coords(spider->direction, &new_coords); const int y_bottom = game_y_bottom_bound(game); const int x_left = game_x_left_bound(game); const int x_right = game_x_right_bound(game); const int top_limit = y_bottom - CENT_INVISIBLE_H_WALL; if (new_coords.x > x_right || new_coords.x < x_left) { cent_enemy_despawn(spider, false); return; } if (new_coords.y > y_bottom) { new_coords.y = y_bottom; spider->direction = NORTH; } else if (new_coords.y < top_limit) { new_coords.y = new_coords.y + 1; spider->direction = SOUTH; } spider->coords = (Coords) { new_coords.x, new_coords.y }; Mushroom *mush = cent_get_mushroom_at_coords(state, &new_coords); if (mush != NULL) { memset(mush, 0, sizeof(Mushroom)); } } static bool cent_blaster_centipede_collision(CentState *state) { Centipedes *centipedes = &state->centipedes; Blaster blaster = state->blaster; for (size_t i = 0; i < centipedes->heads_length; ++i) { Segment *seg = centipedes->heads[i]; if (seg == NULL) { continue; } while (seg) { if (COORDINATES_OVERLAP(seg->coords.x, seg->coords.y, blaster.coords.x, blaster.coords.y)) { return true; } seg = seg->next; } } return false; } static void cent_blaster_collision_check(GameData *game, CentState *state) { if (state->blaster.coords.x <= 0) { return; } if (cent_blaster_enemy_collision(state, &state->flea)) { cent_deduct_life(game, state); return; } if (cent_blaster_enemy_collision(state, &state->spider)) { cent_deduct_life(game, state); return; } if (cent_blaster_centipede_collision(state)) { cent_deduct_life(game, state); return; } } static void cent_blaster_draw(WINDOW *win, const CentState *state) { Blaster blaster = state->blaster; wattron(win, blaster.attributes | COLOR_PAIR(blaster.colour)); mvwaddch(win, blaster.coords.y, blaster.coords.x, CENT_BLASTER_CHAR); wattroff(win, blaster.attributes | COLOR_PAIR(blaster.colour)); } static void cent_projectiles_draw(WINDOW *win, const CentState *state) { Projectile bullet = state->bullet; Coords blaster_coords = state->blaster.coords; if (bullet.coords.x > 0 && bullet.coords.y != blaster_coords.y) { wattron(win, bullet.attributes | COLOR_PAIR(bullet.colour)); mvwaddch(win, bullet.coords.y, bullet.coords.x, CENT_BULLET_CHAR); wattroff(win, bullet.attributes | COLOR_PAIR(bullet.colour)); } } static void cent_enemy_draw(WINDOW *win, const CentState *state) { EnemyAgent spider = state->spider; if (spider.health > 0) { wattron(win, spider.attributes | COLOR_PAIR(spider.colour)); mvwaddch(win, spider.coords.y, spider.coords.x, spider.display_char); wattroff(win, spider.attributes | COLOR_PAIR(spider.colour)); } EnemyAgent flea = state->flea; if (flea.health > 0) { wattron(win, flea.attributes | COLOR_PAIR(flea.colour)); mvwaddch(win, flea.coords.y, flea.coords.x, CENT_FLEA_CHAR); wattroff(win, flea.attributes | COLOR_PAIR(flea.colour)); } EnemyAgent scorpion = state->scorpion; if (scorpion.health > 0) { wattron(win, scorpion.attributes | COLOR_PAIR(scorpion.colour)); mvwaddch(win, scorpion.coords.y, scorpion.coords.x, scorpion.display_char); wattroff(win, scorpion.attributes | COLOR_PAIR(scorpion.colour)); } } static void cent_centipede_draw(const GameData *game, WINDOW *win, CentState *state) { Centipedes *centipedes = &state->centipedes; for (size_t i = 0; i < centipedes->heads_length; ++i) { Segment *seg = centipedes->heads[i]; while (seg) { // sometimes we spawn heads outside of game bounds if (game_coordinates_in_bounds(game, seg->coords.x, seg->coords.y)) { wattron(win, seg->attributes | COLOR_PAIR(seg->colour)); mvwaddch(win, seg->coords.y, seg->coords.x, seg->display_char); wattroff(win, seg->attributes | COLOR_PAIR(seg->colour)); } seg = seg->next; } } } static void cent_mushrooms_draw(WINDOW *win, const CentState *state) { Mushroom *mushrooms = state->mushrooms; for (size_t i = 0; i < state->mushrooms_length; ++i) { Mushroom mush = mushrooms[i]; if (mush.health == 0) { continue; } wattron(win, mush.attributes | COLOR_PAIR(mush.colour)); mvwaddch(win, mush.coords.y, mush.coords.x, mush.display_char); wattroff(win, mush.attributes | COLOR_PAIR(mush.colour)); } } void cent_cb_update_game_state(GameData *game, void *cb_data) { if (!cb_data) { return; } CentState *state = (CentState *)cb_data; if (state->game_over) { return; } TIME_MS cur_time = get_time_millis(); cent_blaster_collision_check(game, state); cent_bullet_collision_check(game, state); cent_blaster_move(game, state, cur_time); cent_bullet_move(game, state, cur_time); cent_do_centipede(game, state, cur_time); cent_do_spider(game, state, cur_time); cent_do_flea(game, state, cur_time); cent_do_scorpion(game, state, cur_time); } void cent_cb_render_window(GameData *game, WINDOW *win, void *cb_data) { if (!cb_data) { return; } CentState *state = (CentState *)cb_data; cent_blaster_draw(win, state); cent_projectiles_draw(win, state); cent_mushrooms_draw(win, state); cent_enemy_draw(win, state); cent_centipede_draw(game, win, state); } void cent_cb_on_keypress(GameData *game, int key, void *cb_data) { if (!cb_data) { return; } CentState *state = (CentState *)cb_data; if (key == CENT_KEY_FIRE) { cent_bullet_spawn(state); return; } Direction dir = game_util_get_direction(key); if (dir < INVALID_DIRECTION) { state->blaster.direction = dir; } } void cent_cb_pause(GameData *game, bool is_paused, void *cb_data) { if (!cb_data) { return; } CentState *state = (CentState *)cb_data; TIME_S t = get_unix_time(); if (is_paused) { state->pause_time = t; } else { state->spider.last_time_despawned += (t - state->pause_time); } } void cent_cb_kill(GameData *game, void *cb_data) { if (!cb_data) { return; } CentState *state = (CentState *)cb_data; cent_exterminate_centipedes(&state->centipedes); free(state->mushrooms); free(state); game_set_cb_update_state(game, NULL, NULL); game_set_cb_render_window(game, NULL, NULL); game_set_cb_kill(game, NULL, NULL); game_set_cb_on_keypress(game, NULL, NULL); game_set_cb_on_pause(game, NULL, NULL); } /* Gives `mush` new coordinates that don't overlap with another mushroom and are within boundaries. * * Return false if we fail to find a vacant coordinate. */ static bool cent_new_mush_coordinates(const GameData *game, CentState *state, Mushroom *mush, int y_floor_bound) { size_t tries = 0; Coords new_coords; do { if (++tries > 10) { return false; } game_random_coords(game, &new_coords); } while (new_coords.y >= (y_floor_bound - 1) || cent_get_mushroom_at_coords(state, &new_coords) != NULL); mush->coords = (Coords) { new_coords.x, new_coords.y }; return true; } static void cent_populate_mushrooms(const GameData *game, CentState *state, int population_const) { int max_x; int max_y; game_max_x_y(game, &max_x, &max_y); const int y_floor_bound = game_y_bottom_bound(game); for (size_t i = 0; i < CENT_MUSHROOMS_LENGTH; ++i) { if (rand() % population_const != 0) { continue; } size_t idx = state->mushrooms_length; Mushroom *mush = &state->mushrooms[idx]; if (!cent_new_mush_coordinates(game, state, mush, y_floor_bound)) { continue; } mush->is_poisonous = false; mush->health = CENT_MUSH_DEFAULT_HEALTH; cent_update_mush_appearance(game, state, mush); ++state->mushrooms_length; } } static int cent_init_state(GameData *game, CentState *state) { game_update_lives(game, CENT_START_LIVES); const int y_bottom = game_y_bottom_bound(game); const int x_left = game_x_left_bound(game); const int x_right = game_x_right_bound(game); const int y_top = game_y_top_bound(game); Mushroom *mushrooms = calloc(1, sizeof(Mushroom) * CENT_MUSHROOMS_LENGTH); if (mushrooms == NULL) { return -1; } state->mushrooms = mushrooms; Centipedes *centipedes = &state->centipedes; memset(centipedes->heads, 0, sizeof(centipedes->heads)); Direction dir = rand() % 2 == 0 ? WEST : EAST; if (cent_birth_centipede(game, state, CENT_MAX_NUM_SEGMENTS, dir, NULL) == -1) { free(mushrooms); return -1; } state->spider.last_time_despawned = get_unix_time(); state->flea.last_time_despawned = get_unix_time(); state->scorpion.last_time_despawned = get_unix_time(); state->blaster.colour = cent_blaster_colour(0); state->blaster.attributes = CENT_BLASTER_ATTR; state->blaster.direction = INVALID_DIRECTION; state->blaster.speed = CENT_BLASTER_SPEED; state->blaster.coords = (Coords) { (x_left + x_right) / 2, y_bottom }; state->blaster.start_coords = (Coords) { (x_left + x_right) / 2, y_bottom }; state->bullet.colour = CENT_BULLET_COLOUR; state->bullet.attributes = CENT_BULLET_ATTR; const int grid_size = (y_bottom - y_top) * (x_right - x_left); if (grid_size >= CENT_MUSHROOMS_POP_CONSTANT) { return -1; } const int population = CENT_MUSHROOMS_POP_CONSTANT / grid_size; cent_populate_mushrooms(game, state, population); return 0; } int centipede_initialize(GameData *game) { // note: If this changes we must update CENT_MUSHROOMS_LENGTH if (game_set_window_shape(game, GW_ShapeSquare) == -1) { return -1; } CentState *state = calloc(1, sizeof(CentState)); if (state == NULL) { return -1; } game_show_level(game, true); game_show_score(game, true); game_show_lives(game, true); game_show_high_score(game, true); game_increment_level(game); game_set_update_interval(game, CENT_GAME_UPDATE_INTERVAL); if (cent_init_state(game, state) == -1) { free(state); return -1; } game_set_cb_update_state(game, cent_cb_update_game_state, state); game_set_cb_render_window(game, cent_cb_render_window, state); game_set_cb_on_keypress(game, cent_cb_on_keypress, state); game_set_cb_kill(game, cent_cb_kill, state); game_set_cb_on_pause(game, cent_cb_pause, state); return 0; } toxic-0.11.3/src/game_centipede.h000066400000000000000000000016021416141666600166250ustar00rootroot00000000000000/* game_centipede.h * * * Copyright (C) 2020 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef GAME_CENTIPEDE #define GAME_CENTIPEDE #include "game_base.h" int centipede_initialize(GameData *game); #endif // GAME_CENTIPEDE toxic-0.11.3/src/game_chess.c000066400000000000000000001547641416141666600160070ustar00rootroot00000000000000/* game_chess.c * * * Copyright (C) 2020 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #include #include #include #include "game_base.h" #include "game_util.h" #include "game_chess.h" #include "misc_tools.h" #define CHESS_WHITE_SQUARE_COLOUR WHITE_GREEN #define CHESS_BLACK_SQUARE_COLOUR WHITE_BLUE #define CHESS_BOARD_ROWS 8 #define CHESS_BOARD_COLUMNS 8 #define CHESS_TILE_SIZE_X 4 #define CHESS_TILE_SIZE_Y 2 #define CHESS_SQUARES (CHESS_BOARD_ROWS * CHESS_BOARD_COLUMNS) #define CHESS_MAX_MESSAGE_SIZE 64 typedef enum ChessPacketType { CHESS_PACKET_INIT_SEND_INVITE = 0x01, CHESS_PACKET_INIT_ACCEPT_INVITE = 0x02, CHESS_PACKET_MOVE_PIECE = 0xFE, CHESS_PACKET_RESIGN = 0xFF, } ChessPacketType; typedef struct ChessCoords { char L; uint8_t N; } ChessCoords; typedef enum ChessColour { White = 0u, Black, } ChessColour; typedef enum ChessStatus { Initializing = 0U, Playing, Checkmate, Stalemate, Resigned, } ChessStatus; typedef enum PieceType { Pawn = 0u, Rook, Knight, Bishop, King, Queen, NoPiece, } PieceType; typedef struct Piece { char display_char; ChessColour colour; PieceType type; } Piece; typedef struct Tile { Piece piece; Coords coords; // xy position on window ChessCoords chess_coords; // chess notation pair int colour; // display colour (not to be confused with White/Black ChessColour) } Tile; typedef struct Board { Tile tiles[CHESS_SQUARES]; int x_right_bound; int x_left_bound; int y_top_bound; int y_bottom_bound; } Board; typedef struct Player { Tile *holding_tile; ChessColour colour; bool can_castle_qs; bool can_castle_ks; bool in_check; Tile *en_passant; // the tile holding the pawn that passed us int en_passant_move_number; // the move number the last en passant was on Piece captured[CHESS_SQUARES]; size_t number_captured; int score; // total points of pieces captured } Player; typedef struct ChessState { Player self; Player other; Board board; int curs_x; int curs_y; char status_message[CHESS_MAX_MESSAGE_SIZE + 1]; size_t message_length; bool black_to_move; size_t move_number; ChessStatus status; } ChessState; static int chess_packet_send_move(const GameData *game, const Tile *from, const Tile *to); static int chess_packet_send_resign(const GameData *game); static const char Board_Letters[] = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'}; #define CHESS_NUM_BOARD_LETTERS (sizeof(Board_Letters) / sizeof(char)) static int chess_get_letter_index(char letter) { for (int i = 0; i < CHESS_NUM_BOARD_LETTERS; ++i) { if (Board_Letters[i] == letter) { return i; } } return -1; } /* * Copies `piece_b` to `piece_a`. */ static void chess_copy_piece(Piece *piece_a, Piece *piece_b) { memcpy(piece_a, piece_b, sizeof(Piece)); } static void chess_set_piece(Piece *piece, PieceType type, ChessColour colour) { piece->type = type; piece->colour = colour; switch (type) { case Pawn: piece->display_char = 'o'; break; case Bishop: piece->display_char = 'B'; break; case Rook: piece->display_char = 'R'; break; case Knight: piece->display_char = 'N'; break; case King: piece->display_char = 'K'; break; case Queen: piece->display_char = 'Q'; break; default: piece->display_char = '?'; break; } } static size_t chess_get_piece_value(PieceType type) { switch (type) { case Pawn: return 1; case Bishop: return 3; case Knight: return 3; case Rook: return 5; case Queen: return 9; default: return 0; } } /* * Puts the absolute difference between `from` and `to` chess coordinates in `l_diff` and `n_diff`. * * Return 0 on success. * Return -1 on failure. */ static int chess_get_chess_coords_diff(const Tile *from, const Tile *to, int *l_diff, int *n_diff) { int from_letter_idx = chess_get_letter_index(from->chess_coords.L); int to_letter_idx = chess_get_letter_index(to->chess_coords.L); if (from_letter_idx == -1 || to_letter_idx == -1) { return -1; } *l_diff = abs(from_letter_idx - to_letter_idx); *n_diff = abs((int)from->chess_coords.N - (int)to->chess_coords.N); return 0; } static void chess_set_status_message(ChessState *state, const char *message, size_t length) { if (length > CHESS_MAX_MESSAGE_SIZE) { return; } memcpy(state->status_message, message, length); state->status_message[length] = 0; state->message_length = length; } static void chess_print_move_notation(ChessState *state, const Tile *from, const Tile *to, bool check) { if (from->piece.type == King) { // special case if player castled int l_diff; int n_diff; if (chess_get_chess_coords_diff(from, to, &l_diff, &n_diff) == -1) { chess_set_status_message(state, "Error", strlen("Error")); return; } if (n_diff == 0 && l_diff > 1 && (to->chess_coords.L == 'c' || to->chess_coords.L == 'g')) { const char *message = to->chess_coords.L == 'c' ? "Last move: 0-0-0" : "Last move: 0-0"; chess_set_status_message(state, message, strlen(message)); return; } } char message[CHESS_MAX_MESSAGE_SIZE + 1]; bool captured = (to->piece.type != NoPiece) || (from->piece.type == Pawn && from->chess_coords.L != to->chess_coords.L); char tmp[2]; snprintf(tmp, sizeof(tmp), "%c", from->piece.display_char); const char *from_char = from->piece.type != Pawn ? tmp : ""; char pawn_capture[2] = {0}; if (strcmp(from_char, "") == 0 && captured) { snprintf(pawn_capture, sizeof(pawn_capture), "%c", from->chess_coords.L); } const char *capture = captured ? "x" : ""; const char *check_char = check ? "+" : ""; snprintf(message, sizeof(message), "Last move: %s%s%s%c%u%s", pawn_capture, from_char, capture, to->chess_coords.L, to->chess_coords.N, check_char); chess_set_status_message(state, message, strlen(message)); } /* * Return true if `pair_a` is the same as `pair_b`. */ static bool chess_chess_coords_overlap(const ChessCoords *pair_a, const ChessCoords *pair_b) { return pair_a->L == pair_b->L && pair_a->N == pair_b->N; } /* * Return the player whose turn it currently is not. */ static Player *chess_get_other_player(ChessState *state) { if (state->black_to_move) { return state->self.colour == Black ? &state->other : &state->self; } else { return state->self.colour == White ? &state->other : &state->self; } } /* * Return the player whose turn it currently is. */ static Player *chess_get_player_to_move(ChessState *state) { if (state->black_to_move) { return state->self.colour == Black ? &state->self : & state->other; } else { return state->self.colour == Black ? &state->other : & state->self; } } /* * Return true if it's `player`'s turn to move. */ static bool chess_player_to_move(const ChessState *state, const Player *player) { return (player->colour == White && !state->black_to_move) || (player->colour == Black && state->black_to_move); } /* * Removes `piece` from the board and puts it in `player`'s captured list. Also updates their score. */ static void chess_capture_piece(Player *player, Piece *piece) { size_t idx = player->number_captured; if (idx < CHESS_SQUARES) { chess_copy_piece(&player->captured[idx], piece); ++player->number_captured; } player->score += chess_get_piece_value(piece->type); piece->type = NoPiece; } /* * Puts coordinates associated with tile at x y coordinates in `chess_coords`. * * Return 0 on success. * Return -1 if coordinates are out of bounds. */ static int chess_get_chess_coords(const Board *board, int x, int y, ChessCoords *chess_coords, bool self_is_white) { if (x < board->x_left_bound || x > board->x_right_bound || y < board->y_top_bound || y > board->y_bottom_bound) { return -1; } size_t idx = (x - board->x_left_bound) / CHESS_TILE_SIZE_X; if (idx >= CHESS_NUM_BOARD_LETTERS) { return -1; } if (self_is_white) { chess_coords->L = Board_Letters[idx]; chess_coords->N = ((board->y_bottom_bound + 1) - y) / CHESS_TILE_SIZE_Y; } else { chess_coords->L = Board_Letters[7 - idx]; chess_coords->N = 8 - (((board->y_bottom_bound + 1) - y) / CHESS_TILE_SIZE_Y) + 1; } return 0; } /* * Returns the tile located at given coordinates. */ static Tile *chess_get_tile(ChessState *state, int x, int y) { Board *board = &state->board; ChessCoords pair; if (chess_get_chess_coords(board, x, y, &pair, state->self.colour == White) == -1) { return NULL; } for (size_t i = 0; i < CHESS_SQUARES; ++i) { Tile *tile = &board->tiles[i]; if (tile->chess_coords.N == pair.N && tile->chess_coords.L == pair.L) { return tile; } } return NULL; } /* * Returns tile associated with `chess_coords`. */ static Tile *chess_get_tile_at_chess_coords(Board *board, const ChessCoords *chess_coords) { for (size_t i = 0; i < CHESS_SQUARES; ++i) { Tile *tile = &board->tiles[i]; if (tile->chess_coords.N == chess_coords->N && tile->chess_coords.L == chess_coords->L) { return tile; } } return NULL; } /* * Return true if `piece` can occupy `tile`. */ static bool chess_piece_can_occupy_tile(const Piece *piece, const Tile *tile) { return tile->piece.colour != piece->colour || tile->piece.type == NoPiece; } /* * Return true if all squares in a horizontal or vertical line between `from` and `to` * are vacant, excluding each square respectively. */ static bool chess_path_line_clear(Board *board, const Tile *from, const Tile *to, int l_diff, int n_diff) { if (l_diff < 0 || n_diff < 0) { return false; } if (l_diff != 0 && n_diff != 0) { return false; } ChessCoords chess_coords = (ChessCoords) { 0, 0 }; size_t start; size_t end; if (l_diff == 0) { start = 1 + MIN(from->chess_coords.N, to->chess_coords.N); end = start + n_diff - 1; chess_coords.L = from->chess_coords.L; } else { int from_idx = chess_get_letter_index(from->chess_coords.L); int to_idx = chess_get_letter_index(to->chess_coords.L); if (to_idx == -1 || from_idx == -1) { return false; } start = 1 + MIN(from_idx, to_idx); end = start + l_diff - 1; chess_coords.N = from->chess_coords.N; } for (size_t i = start; i < end; ++i) { if (l_diff == 0) { chess_coords.N = i; } else { if (i >= CHESS_NUM_BOARD_LETTERS) { return false; } chess_coords.L = Board_Letters[i]; } Tile *tile = chess_get_tile_at_chess_coords(board, &chess_coords); if (tile == NULL) { return false; } if (tile->piece.type != NoPiece) { return false; } } return true; } /* * Return true if all tiles in a diagonal line between `from` and `to` are * unoccupied, excluding each respective tile. */ static bool chess_path_diagonal_clear(Board *board, const Tile *from, const Tile *to, int l_diff, int n_diff) { if (l_diff < 0 || n_diff < 0) { return false; } if (l_diff != n_diff || l_diff == 0) { return false; } size_t start = 1 + MIN(from->chess_coords.N, to->chess_coords.N); size_t end = start + n_diff - 1; // we're caluclating from south-east to north-west, or from south-west to north-east bool left_diag = (from->chess_coords.N > to->chess_coords.N && from->chess_coords.L < to->chess_coords.L) || (from->chess_coords.N < to->chess_coords.N && from->chess_coords.L > to->chess_coords.L); size_t from_l_idx = chess_get_letter_index(from->chess_coords.L); size_t to_l_idx = chess_get_letter_index(to->chess_coords.L); size_t start_l_idx = left_diag ? MAX(from_l_idx, to_l_idx) - 1 : MIN(from_l_idx, to_l_idx) + 1; if (start_l_idx == -1) { return -1; } ChessCoords chess_coords; for (size_t i = start; i < end; ++i) { chess_coords.N = i; if (start_l_idx >= CHESS_NUM_BOARD_LETTERS) { return false; } chess_coords.L = Board_Letters[start_l_idx]; Tile *tile = chess_get_tile_at_chess_coords(board, &chess_coords); if (tile == NULL) { return false; } if (tile->piece.type != NoPiece) { return false; } start_l_idx = left_diag ? start_l_idx - 1 : start_l_idx + 1; } return true; } /* * Removes en passant'd pawn and resets player's en passant flag. * * Should be called after every successful move. */ static void chess_player_check_en_passant(Player *player) { if (player->en_passant_move_number == -1) { chess_capture_piece(player, &player->en_passant->piece); player->en_passant = NULL; } player->en_passant_move_number = 0; } /* * Flags a pawn at `to` for a possible en passant take for other player. */ static void chess_pawn_en_passant_flag(ChessState *state, Tile *to) { Player *other = chess_get_other_player(state); other->en_passant = to; other->en_passant_move_number = state->move_number; } /* * Return true and flag opposing pawn to be removed if `to` is a valid en passant move. */ static bool chess_pawn_en_passant_move(ChessState *state, Player *player, const Tile *to) { if (player->en_passant == NULL || player->en_passant_move_number <= 0) { return false; } int delta = 0; if (player->colour == White) { if (player->en_passant_move_number != state->move_number) { return false; } delta = 1; } else { if (player->en_passant_move_number != state->move_number - 1) { return false; } delta = -1; } if (player->en_passant->piece.type == Pawn && to->chess_coords.N == player->en_passant->chess_coords.N + delta && to->chess_coords.L == player->en_passant->chess_coords.L) { player->en_passant_move_number = -1; // flag opponent's pawn to be removed after move is validated return true; } return false; } static bool chess_valid_pawn_move(ChessState *state, Tile *from, Tile *to) { Board *board = &state->board; Piece from_piece = from->piece; Piece to_piece = to->piece; // Can't go backwards if (from_piece.colour == Black && from->chess_coords.N <= to->chess_coords.N) { return false; } if (from_piece.colour == White && from->chess_coords.N >= to->chess_coords.N) { return false; } int l_diff; int n_diff; if (chess_get_chess_coords_diff(from, to, &l_diff, &n_diff) == -1) { return false; } // can't move more than two spaces forward or one space diagonally if (n_diff < 1 || n_diff > 2 || l_diff > 1) { return false; } // If moving two spaces vertically it must be from starting position if (n_diff == 2) { if (l_diff != 0) { return false; } if (from_piece.colour == Black && from->chess_coords.N != 7) { return false; } if (from_piece.colour == White && from->chess_coords.N != 2) { return false; } if (to_piece.type != NoPiece) { return false; } } // if moving diagonally, to square must contain an enemy piece or have a valid en passant if (l_diff == 1) { Player *self = chess_get_player_to_move(state); if (chess_pawn_en_passant_move(state, self, to)) { return true; } return to_piece.type != NoPiece && to_piece.colour != from->piece.colour; } if (to_piece.type != NoPiece) { return false; } bool ret = chess_path_line_clear(board, from, to, l_diff, n_diff); if (ret && n_diff == 2) { chess_pawn_en_passant_flag(state, to); } return ret; } static bool chess_valid_rook_move(Board *board, const Tile *from, const Tile *to) { int l_diff; int n_diff; if (chess_get_chess_coords_diff(from, to, &l_diff, &n_diff) == -1) { return false; } if (!chess_path_line_clear(board, from, to, l_diff, n_diff)) { return false; } if (!chess_piece_can_occupy_tile(&from->piece, to)) { return false; } if (from->chess_coords.N != to->chess_coords.N && from->chess_coords.L != to->chess_coords.L) { return false; } return true; } static bool chess_valid_knight_move(const Tile *from, const Tile *to) { if (!chess_piece_can_occupy_tile(&from->piece, to)) { return false; } int l_diff; int n_diff; if (chess_get_chess_coords_diff(from, to, &l_diff, &n_diff) == -1) { return false; } if (!((l_diff == 2 && n_diff == 1) || (l_diff == 1 && n_diff == 2))) { return false; } return true; } static bool chess_valid_bishop_move(Board *board, const Tile *from, const Tile *to) { if (!chess_piece_can_occupy_tile(&from->piece, to)) { return false; } int l_diff; int n_diff; if (chess_get_chess_coords_diff(from, to, &l_diff, &n_diff) == -1) { return false; } return chess_path_diagonal_clear(board, from, to, l_diff, n_diff); } static bool chess_valid_queen_move(Board *board, const Tile *from, const Tile *to) { if (!chess_piece_can_occupy_tile(&from->piece, to)) { return false; } int l_diff; int n_diff; if (chess_get_chess_coords_diff(from, to, &l_diff, &n_diff) == -1) { return false; } if (l_diff != 0 && n_diff != 0) { return chess_path_diagonal_clear(board, from, to, l_diff, n_diff); } return chess_path_line_clear(board, from, to, l_diff, n_diff); } static bool chess_valid_king_move(const Tile *from, const Tile *to) { if (!chess_piece_can_occupy_tile(&from->piece, to)) { return false; } int l_diff; int n_diff; if (chess_get_chess_coords_diff(from, to, &l_diff, &n_diff) == -1) { return false; } if (l_diff > 1 || n_diff > 1) { return false; } return true; } static bool chess_piece_attacking_square(ChessState *state, ChessColour colour, Tile *to); /* * Return true if `player` is in check. */ static bool chess_player_in_check(ChessState *state, const Player *player) { Board *board = &state->board; for (size_t i = 0; i < CHESS_SQUARES; ++i) { Tile *tile = &board->tiles[i]; if (tile->piece.type == King && tile->piece.colour == player->colour) { return chess_piece_attacking_square(state, player->colour == Black ? White : Black, tile); } } return false; } /* * Makes a mock move on the board and tests if it puts the player in check. * * Return true if move is valid. * * This function assumes that the legality of the move has already been checked. */ static bool chess_mock_move_valid(ChessState *state, const Player *player, Tile *from, Tile *to) { Board *board = &state->board; const bool en_passant = player->en_passant_move_number == -1; bool in_check = false; Tile *ep_tile = NULL; Piece ep_piece; memset(&ep_piece, 0, sizeof(ep_piece)); if (en_passant) { // remove piece that was captured via en passant ChessCoords ep_coords; ep_coords.N = player->colour == White ? to->chess_coords.N - 1 : to->chess_coords.N + 1; ep_coords.L = to->chess_coords.L; ep_tile = chess_get_tile_at_chess_coords(board, &ep_coords); if (ep_tile == NULL) { return false; } chess_copy_piece(&ep_piece, &ep_tile->piece); ep_tile->piece.type = NoPiece; // temporarily remove piece } Piece from_piece; Piece to_piece; chess_copy_piece(&from_piece, &from->piece); chess_copy_piece(&to_piece, &to->piece); chess_copy_piece(&to->piece, &from->piece); from->piece.type = NoPiece; in_check = chess_player_in_check(state, player); from->piece.type = from_piece.type; chess_copy_piece(&to->piece, &to_piece); if (en_passant) { ep_tile->piece.type = ep_piece.type; // reset piece to original state } return !in_check; } /* * Return 1 if we can legally move `from` to `to`. * Return 0 if move is legal but we're in check. * Return -1 if move is not legal. * * If `player` is null we don't check if move puts player in check. * * This function should not modify the game state. */ static int chess_valid_move(ChessState *state, const Player *player, Tile *from, Tile *to) { if (chess_chess_coords_overlap(&from->chess_coords, &to->chess_coords)) { return false; } bool valid = false; Board *board = &state->board; switch (from->piece.type) { case Pawn: valid = chess_valid_pawn_move(state, from, to); break; case Rook: valid = chess_valid_rook_move(board, from, to); break; case Knight: valid = chess_valid_knight_move(from, to); break; case Bishop: valid = chess_valid_bishop_move(board, from, to); break; case Queen: valid = chess_valid_queen_move(board, from, to); break; case King: valid = chess_valid_king_move(from, to); break; default: valid = false; break; } int ret = valid ? 1 : -1; if (player != NULL && valid) { if (!chess_mock_move_valid(state, player, from, to)) { ret = 0; } } return ret; } /* * Return true if any piece of `colour` is attacking tile designated by `to`. */ static bool chess_piece_attacking_square(ChessState *state, ChessColour colour, Tile *to) { Board *board = &state->board; for (size_t i = 0; i < CHESS_SQUARES; ++i) { Tile *from = &board->tiles[i]; if (from->piece.colour != colour || from->piece.type == NoPiece) { continue; } // We only need to know if a piece has line of sight so we don't // care if the move puts the player in check if (chess_valid_move(state, NULL, from, to) == 1) { return true; } } return false; } /* * Disables castling if king or rook moves. */ static void chess_player_set_can_castle(Player *player, const Tile *tile) { if (!player->can_castle_ks && !player->can_castle_qs) { return; } if (tile->piece.type == King) { player->can_castle_ks = false; player->can_castle_qs = false; return; } if (tile->piece.type != Rook) { return; } if ((player->colour == White && tile->chess_coords.N != 1) || (player->colour == Black && tile->chess_coords.N != 8)) { return; } if (tile->chess_coords.L == 'a') { player->can_castle_qs = false; } else if (tile->chess_coords.L == 'h') { player->can_castle_ks = false; } } /* * Attempts to castle king for `player`. * * Return true if successfully castled. */ static bool chess_player_castle(ChessState *state, Player *player, Tile *from, Tile *to) { if (!player->can_castle_ks && !player->can_castle_qs) { return false; } Board *board = &state->board; if (!(from->piece.type == King && to->piece.type == NoPiece)) { return false; } int l_diff; int n_diff; if (chess_get_chess_coords_diff(from, to, &l_diff, &n_diff) == -1) { return false; } if (!(l_diff == 2 && n_diff == 0)) { return false; } ChessCoords coords; coords.N = to->chess_coords.N; bool queen_side = false; Tile *rook_to_tile = NULL; if (to->chess_coords.L == 'g') { if (!player->can_castle_ks) { return false; } coords.L = 'f'; rook_to_tile = chess_get_tile_at_chess_coords(board, &coords); if (rook_to_tile == NULL) { return false; } if (rook_to_tile->piece.type != NoPiece) { return false; } } else if (to->chess_coords.L == 'c') { if (!player->can_castle_qs) { return false; } coords.L = 'd'; rook_to_tile = chess_get_tile_at_chess_coords(board, &coords); coords.L = 'b'; const Tile *tmp_b = chess_get_tile_at_chess_coords(board, &coords); if (rook_to_tile == NULL || tmp_b == NULL) { return false; } if (!(rook_to_tile->piece.type == NoPiece && tmp_b->piece.type == NoPiece)) { return false; } queen_side = true; } else { return false; } ChessColour other_colour = player->colour == Black ? White : Black; // Make sure a piece isn't attacking either square the king traverses if (chess_piece_attacking_square(state, other_colour, rook_to_tile)) { return false; } if (chess_piece_attacking_square(state, other_colour, to)) { return false; } Tile *rook_from_tile = NULL; // move rook coords.L = queen_side ? 'a' : 'h'; rook_from_tile = chess_get_tile_at_chess_coords(board, &coords); if (rook_from_tile == NULL || rook_from_tile->piece.type != Rook) { return false; } chess_copy_piece(&rook_to_tile->piece, &rook_from_tile->piece); chess_set_piece(&rook_from_tile->piece, NoPiece, White); // move king Piece old_king; chess_copy_piece(&old_king, &to->piece); chess_copy_piece(&to->piece, &from->piece); chess_set_piece(&from->piece, NoPiece, White); player->holding_tile = NULL; if (chess_player_in_check(state, player)) { chess_copy_piece(&to->piece, &old_king); chess_set_piece(&rook_to_tile->piece, NoPiece, White); chess_set_piece(&rook_from_tile->piece, Rook, player->colour); chess_set_piece(&from->piece, King, player->colour); return false; } player->can_castle_qs = false; player->can_castle_ks = false; return true; } static void chess_update_state(ChessState *state, Player *self, Player *other, const Tile *from, const Tile *to) { chess_player_check_en_passant(self); self->in_check = false; other->in_check = chess_player_in_check(state, other); state->message_length = 0; state->black_to_move ^= 1; if (state->black_to_move) { ++state->move_number; } chess_print_move_notation(state, from, to, other->in_check); } /* * Attempts to make opponent's move. * * Return 0 on success. * Return -1 on failure. * * This function shouldn't fail unless opponent is doing something fishy or there's a bug somewhere. * On failure the game will abort and the player who made an invalid move will lose the game. */ static int chess_try_move_opponent(ChessState *state, Tile *from, Tile *to) { Player *opponent = &state->other; if (!chess_player_to_move(state, opponent)) { return -1; } Tile from_orig; memcpy(&from_orig, from, sizeof(Tile)); Tile to_orig; memcpy(&to_orig, to, sizeof(Tile)); int valid = chess_valid_move(state, opponent, from, to); if (valid != 1) { if (!opponent->in_check && chess_player_castle(state, opponent, from, to)) { chess_set_piece(&from->piece, NoPiece, White); goto on_success; } return -1; } if (to->piece.type != NoPiece) { chess_capture_piece(opponent, &to->piece); } chess_copy_piece(&to->piece, &from->piece); chess_player_set_can_castle(opponent, &from_orig); chess_set_piece(&from->piece, NoPiece, White); // check if we need to promote pawn to queen if (from_orig.piece.type == Pawn) { if (to->chess_coords.N == 1 || to->chess_coords.N == 8) { chess_set_piece(&to->piece, Queen, to->piece.colour); } } on_success: chess_update_state(state, opponent, &state->self, &from_orig, &to_orig); return 0; } static void chess_try_move_self(const GameData *game, ChessState *state, Player *self) { if (!chess_player_to_move(state, self)) { return; } Tile *to_tile = chess_get_tile(state, state->curs_x, state->curs_y); if (to_tile == NULL) { return; } Tile *holding_tile = self->holding_tile; if (holding_tile == NULL) { return; } Tile from_orig; memcpy(&from_orig, holding_tile, sizeof(Tile)); Tile to_orig; memcpy(&to_orig, to_tile, sizeof(Tile)); if (chess_chess_coords_overlap(&holding_tile->chess_coords, &to_tile->chess_coords)) { state->message_length = 0; self->holding_tile = NULL; return; } int valid = chess_valid_move(state, self, holding_tile, to_tile); if (valid != 1) { if (!self->in_check && chess_player_castle(state, self, holding_tile, to_tile)) { if (chess_packet_send_move(game, &from_orig, to_tile) == -1) { const char *message = "Connection error"; chess_set_status_message(state, message, strlen(message)); return; } chess_set_piece(&holding_tile->piece, NoPiece, White); self->holding_tile = NULL; goto on_success; } self->holding_tile = NULL; const char *message = valid == -1 ? "Invalid move" : "Invalid move (check)"; chess_set_status_message(state, message, strlen(message)); return; } if (chess_packet_send_move(game, &from_orig, to_tile) == -1) { const char *message = "Failed to move: Connection error"; chess_set_status_message(state, message, strlen(message)); return; } if (to_tile->piece.type != NoPiece) { chess_capture_piece(self, &to_tile->piece); } chess_copy_piece(&to_tile->piece, &self->holding_tile->piece); self->holding_tile = NULL; chess_set_piece(&holding_tile->piece, NoPiece, White); chess_player_set_can_castle(self, &from_orig); // check if we need to promote pawn to queen if (from_orig.piece.type == Pawn) { if (to_tile->chess_coords.N == 1 || to_tile->chess_coords.N == 8) { chess_set_piece(&to_tile->piece, Queen, to_tile->piece.colour); } } on_success: chess_update_state(state, self, &state->other, &from_orig, &to_orig); } static void chess_pick_up_piece(ChessState *state, Player *player) { if (!chess_player_to_move(state, player)) { return; } Tile *tile = chess_get_tile(state, state->curs_x, state->curs_y); if (tile == NULL) { return; } if (tile->piece.type == NoPiece) { return; } if (tile->piece.colour != player->colour) { return; } player->holding_tile = tile; } /* * Return 0 if `player` does not have sufficient material to check mate his opponent. * Return 1 if `player` has sufficient material. * Return 2 if `player` has sufficient material but no major or minor pieces. */ static int chess_player_can_mate(ChessState *state, const Player *player) { Board *board = &state->board; size_t bishop_or_knight = 0; bool has_pawn = false; for (size_t i = 0; i < CHESS_SQUARES; ++i) { Tile tile = board->tiles[i]; if (tile.piece.type == NoPiece || tile.piece.colour != player->colour) { continue; } if (tile.piece.type == Queen || tile.piece.type == Rook) { return 1; } if (tile.piece.type == Bishop || tile.piece.type == Knight) { if (++bishop_or_knight > 1) { return 1; } } if (tile.piece.type == Pawn) { has_pawn = true; if (bishop_or_knight > 0) { return 1; } } } return has_pawn ? 2 : 0; } /* * Return true if piece on `from` tile can move to any other square on the board. */ static bool chess_piece_can_move(ChessState *state, const Player *player, Tile *from) { Board *board = &state->board; for (size_t i = 0; i < CHESS_SQUARES; ++i) { Tile *to = &board->tiles[i]; if (chess_valid_move(state, player, from, to) == 1) { return true; } } return false; } /* * Return true if any piece for `player` can make a legal move. */ static bool chess_any_piece_can_move(ChessState *state, const Player *player) { Board *board = &state->board; for (size_t i = 0; i < CHESS_SQUARES; ++i) { Tile *from = &board->tiles[i]; if (from->piece.colour != player->colour || from->piece.type == NoPiece) { continue; } if (chess_piece_can_move(state, player, from)) { return true; } } return false; } /* * Return true if game is in stalemate. * * A game is considered to be in stalemate if neither side has sufficient material to checkmake * the opponent's king, or if current turn's player is unable to move any pieces and is not in check. */ static bool chess_game_is_statemate(ChessState *state) { const Player *self = chess_get_player_to_move(state); const Player *other = chess_get_other_player(state); if (self->in_check || other->in_check) { return false; } int self_can_mate = chess_player_can_mate(state, self); int other_can_mate = chess_player_can_mate(state, other); if (self_can_mate == 0 && other_can_mate == 0) { return true; } if (self_can_mate == 1) { return false; } // player only has pawns and/or king, see if any remaining piece can move return !chess_any_piece_can_move(state, self); } /* * Return true if game is in checkmate. */ static bool chess_game_checkmate(ChessState *state) { const Player *player = chess_get_player_to_move(state); return !chess_any_piece_can_move(state, player); } /* * Checks if we have a checkmate or stalemate and updates game status. */ static void chess_update_status(ChessState *state) { if (chess_game_is_statemate(state)) { state->status = Stalemate; const char *message = "Stalemate"; chess_set_status_message(state, message, strlen(message)); return; } if (chess_game_checkmate(state)) { state->status = Checkmate; const char *message = "Checkmate!"; chess_set_status_message(state, message, strlen(message)); return; } } static void chess_do_input(const GameData *game, ChessState *state) { if (state->status != Playing) { return; } Player *self = &state->self; if (self->holding_tile == NULL) { chess_pick_up_piece(state, self); } else { chess_try_move_self(game, state, self); chess_update_status(state); } } static void chess_move_curs_left(ChessState *state) { Board *board = &state->board; int new_x = state->curs_x - CHESS_TILE_SIZE_X; if (new_x < board->x_left_bound) { return; } state->curs_x = new_x; } static void chess_move_curs_right(ChessState *state) { Board *board = &state->board; int new_x = state->curs_x + CHESS_TILE_SIZE_X; if (new_x > board->x_right_bound) { return; } state->curs_x = new_x; } static void chess_move_curs_up(ChessState *state) { Board *board = &state->board; int new_y = state->curs_y - CHESS_TILE_SIZE_Y; if (new_y < board->y_top_bound) { return; } state->curs_y = new_y; } static void chess_move_curs_down(ChessState *state) { Board *board = &state->board; int new_y = state->curs_y + CHESS_TILE_SIZE_Y; if (new_y >= board->y_bottom_bound) { return; } state->curs_y = new_y; } static int chess_get_display_colour(ChessColour p_colour, int tile_colour) { if (tile_colour == CHESS_WHITE_SQUARE_COLOUR) { if (p_colour == White) { return BLACK_WHITE; // white square, white piece } else { return YELLOW; // white square, black piece } } if (p_colour == White) { return BLACK_WHITE; // black square, white piece } else { return YELLOW; // black square, black piece } } void chess_move_curs_up_left(ChessState *state) { chess_move_curs_left(state); chess_move_curs_up(state); } void chess_move_curs_up_right(ChessState *state) { chess_move_curs_right(state); chess_move_curs_up(state); } void chess_move_curs_down_left(ChessState *state) { chess_move_curs_left(state); chess_move_curs_down(state); } void chess_move_curs_down_right(ChessState *state) { chess_move_curs_right(state); chess_move_curs_down(state); } static void chess_draw_board_coords_white(WINDOW *win, const Board *board) { for (size_t i = 0; i < CHESS_BOARD_COLUMNS; ++i) { mvwaddch(win, board->y_bottom_bound, board->x_left_bound + 1 + (i * CHESS_TILE_SIZE_X), Board_Letters[i]); } for (size_t i = 0; i < CHESS_BOARD_ROWS; ++i) { mvwprintw(win, board->y_bottom_bound - 1 - (i * CHESS_TILE_SIZE_Y), board->x_left_bound - 1, "%zu", i + 1); } } static void chess_draw_board_coords_black(WINDOW *win, const Board *board) { int l_idx = CHESS_NUM_BOARD_LETTERS - 1; for (size_t i = 0; i < CHESS_BOARD_COLUMNS && l_idx >= 0; ++i, --l_idx) { mvwaddch(win, board->y_bottom_bound, board->x_left_bound + 1 + (i * CHESS_TILE_SIZE_X), Board_Letters[l_idx]); } int n_idx = CHESS_BOARD_ROWS; for (size_t i = 0; i < CHESS_BOARD_ROWS && n_idx > 0; ++i, --n_idx) { mvwprintw(win, board->y_bottom_bound - 1 - (i * CHESS_TILE_SIZE_Y), board->x_left_bound - 1, "%d", n_idx); } } static void chess_draw_board(WINDOW *win, ChessState *state) { const Player *player = chess_get_player_to_move(state); Board *board = &state->board; for (size_t i = 0; i < CHESS_SQUARES; ++i) { Tile tile = board->tiles[i]; wattron(win, COLOR_PAIR(tile.colour)); for (size_t x = 0; x < CHESS_TILE_SIZE_X; ++x) { for (size_t y = 0; y < CHESS_TILE_SIZE_Y; ++y) { mvwaddch(win, tile.coords.y + y, tile.coords.x + x, ' '); } } wattroff(win, COLOR_PAIR(tile.colour)); // don't draw the piece we're currently holding if (player->holding_tile != NULL) { if (chess_chess_coords_overlap(&tile.chess_coords, &player->holding_tile->chess_coords)) { continue; } } Piece piece = tile.piece; if (piece.type != NoPiece) { int colour = chess_get_display_colour(piece.colour, tile.colour); wattron(win, A_BOLD | COLOR_PAIR(colour)); mvwaddch(win, tile.coords.y, tile.coords.x + 1, piece.display_char); wattroff(win, A_BOLD | COLOR_PAIR(colour)); } } if (state->self.colour == White) { chess_draw_board_coords_white(win, board); } else { chess_draw_board_coords_black(win, board); } // if holding a piece draw it at cursor position if (player->holding_tile != NULL) { Tile *tile = player->holding_tile; wattron(win, A_BOLD | COLOR_PAIR(BLACK)); mvwaddch(win, state->curs_y, state->curs_x, tile->piece.display_char); wattroff(win, A_BOLD | COLOR_PAIR(BLACK)); } } static void chess_print_status(WINDOW *win, ChessState *state) { const Board *board = &state->board; wattron(win, A_BOLD); const Player *player = chess_get_player_to_move(state); char message[CHESS_MAX_MESSAGE_SIZE + 1]; switch (state->status) { case Playing: { snprintf(message, sizeof(message), "%s to move %s", state->black_to_move ? "Black" : "White", player->in_check ? "(check)" : ""); break; } case Initializing: { snprintf(message, sizeof(message), "Waiting for opponent to connect"); break; } case Resigned: { snprintf(message, sizeof(message), "Opponent resigned"); break; } case Stalemate: /* fallthrough */ case Checkmate: { const char *score_str = NULL; if (state->self.in_check) { score_str = state->self.colour == White ? "0 - 1" : "1 - 0"; } else if (state->other.in_check) { score_str = state->other.colour == White ? "0 - 1" : "1 - 0"; } else { score_str = "1/2 - 1/2"; } snprintf(message, sizeof(message), "%s", score_str); break; } default: { snprintf(message, sizeof(message), "Invalid game state"); break; } } int x_mid = (board->x_left_bound + (CHESS_TILE_SIZE_X * (CHESS_BOARD_COLUMNS / 2))) - (strlen(message) / 2); mvwprintw(win, board->y_top_bound - 2, x_mid, "%s", message); if (state->message_length > 0) { x_mid = (board->x_left_bound + (CHESS_TILE_SIZE_X * (CHESS_BOARD_COLUMNS / 2))) - (state->message_length / 2); mvwprintw(win, board->y_bottom_bound + 2, x_mid, "%s", state->status_message); } wattroff(win, A_BOLD); } static void chess_print_captured(const GameData *game, WINDOW *win, ChessState *state) { const Board *board = &state->board; const Player *self = &state->self; const Player *other = &state->other; const int score_diff = self->score - other->score; const int self_top_y_start = board->y_bottom_bound - (CHESS_TILE_SIZE_Y * 3) + 1; const int other_top_y_start = board->y_top_bound + 1; const int left_x_start = board->x_right_bound + 1; const int right_x_border = game_x_right_bound(game) - 1; size_t idx = 0; int self_colour = self->colour == White ? WHITE : YELLOW; int other_colour = self_colour == YELLOW ? WHITE : YELLOW; wattron(win, A_BOLD); if (score_diff > 0) { wattron(win, COLOR_PAIR(self_colour)); mvwprintw(win, self_top_y_start - 1, left_x_start, "+%d", score_diff); wattroff(win, COLOR_PAIR(self_colour)); } wattron(win, COLOR_PAIR(other_colour)); for (size_t y = self_top_y_start; y < board->y_bottom_bound; ++y) { for (size_t x = left_x_start; x < right_x_border && idx < self->number_captured; x += 2, ++idx) { Piece piece = self->captured[idx]; mvwaddch(win, y, x, piece.display_char); } } wattroff(win, COLOR_PAIR(other_colour)); idx = 0; if (score_diff < 0) { wattron(win, COLOR_PAIR(other_colour)); mvwprintw(win, other_top_y_start - 1, left_x_start, "+%d", abs(score_diff)); wattroff(win, COLOR_PAIR(other_colour)); } wattron(win, COLOR_PAIR(self_colour)); for (size_t y = other_top_y_start; y < board->y_bottom_bound; ++y) { for (size_t x = left_x_start; x < right_x_border && idx < other->number_captured; x += 2, ++idx) { Piece piece = other->captured[idx]; mvwaddch(win, y, x, piece.display_char); } } wattroff(win, A_BOLD | COLOR_PAIR(self_colour)); } static void chess_draw_interface(const GameData *game, WINDOW *win, ChessState *state) { chess_print_status(win, state); chess_print_captured(game, win, state); } void chess_cb_render_window(GameData *game, WINDOW *win, void *cb_data) { if (!cb_data) { return; } ChessState *state = (ChessState *)cb_data; move(state->curs_y, state->curs_x); curs_set(1); chess_draw_board(win, state); chess_draw_interface(game, win, state); } void chess_cb_on_keypress(GameData *game, int key, void *cb_data) { if (!cb_data) { return; } ChessState *state = (ChessState *)cb_data; switch (key) { case KEY_LEFT: { chess_move_curs_left(state); break; } case KEY_RIGHT: { chess_move_curs_right(state); break; } case KEY_DOWN: { chess_move_curs_down(state); break; } case KEY_UP: { chess_move_curs_up(state); break; } case KEY_HOME: { chess_move_curs_up_left(state); break; } case KEY_END: { chess_move_curs_down_left(state); break; } case KEY_PPAGE: { chess_move_curs_up_right(state); break; } case KEY_NPAGE: { chess_move_curs_down_right(state); break; } case '\r': /* Intentional fallthrough */ case ' ': { chess_do_input(game, state); break; } default: { return; } } } void chess_cb_kill(GameData *game, void *cb_data) { if (!cb_data) { return; } ChessState *state = (ChessState *)cb_data; free(state); chess_packet_send_resign(game); game_set_cb_render_window(game, NULL, NULL); game_set_cb_kill(game, NULL, NULL); game_set_cb_on_keypress(game, NULL, NULL); game_set_cb_on_packet(game, NULL, NULL); } /* * Attempts to handle opponent's move. * * Return 0 on success. * Return -1 on failure. */ #define CHESS_PACKET_MOVE_SIZE 4 static int chess_handle_opponent_move_packet(const GameData *game, ChessState *state, const uint8_t *data, size_t length) { if (length < CHESS_PACKET_MOVE_SIZE || data == NULL) { return -1; } char from_l = data[0]; uint8_t from_n = data[1]; char to_l = data[2]; uint8_t to_n = data[3]; ChessCoords from_coords = (ChessCoords) { from_l, from_n, }; ChessCoords to_coords = (ChessCoords) { to_l, to_n, }; Board *board = &state->board; Tile *from_tile = chess_get_tile_at_chess_coords(board, &from_coords); if (from_tile == NULL) { return -1; } Tile *to_tile = chess_get_tile_at_chess_coords(board, &to_coords); if (to_tile == NULL) { return -1; } if (chess_try_move_opponent(state, from_tile, to_tile) != 0) { fprintf(stderr, "Chess opponent tried to make an illegal move: %c%d-%c%d\n", from_l, from_n, to_l, to_n); return -1; } return 0; } static void chess_notify(const GameData *game, ChessPacketType type) { const char *msg = NULL; switch (type) { case CHESS_PACKET_INIT_ACCEPT_INVITE: { msg = "Game on!"; break; } case CHESS_PACKET_RESIGN: { msg = "Opponent has resigned"; break; } case CHESS_PACKET_MOVE_PIECE: { msg = "Opponent has moved"; break; } default: { return; } } game_window_notify(game, msg); } static void chess_cb_on_packet(GameData *game, const uint8_t *data, size_t length, void *cb_data) { if (length == 0 || data == NULL) { return; } if (cb_data == NULL) { return; } ChessState *state = (ChessState *)cb_data; ChessPacketType type = data[0]; switch (type) { case CHESS_PACKET_INIT_ACCEPT_INVITE: { if (state->status == Initializing) { state->status = Playing; } break; } case CHESS_PACKET_RESIGN: { if (state->status == Playing) { state->status = Resigned; } break; } case CHESS_PACKET_MOVE_PIECE: { if (state->status == Playing) { int ret = chess_handle_opponent_move_packet(game, state, data + 1, length - 1); if (ret != 0) { state->status = Resigned; } chess_update_status(state); } break; } default: { fprintf(stderr, "Got unknown chess packet type: %d\n", type); return; } } chess_notify(game, type); } static int chess_init_board(GameData *game, ChessState *state, bool self_is_white) { Board *board = &state->board; const int x_left = game_x_left_bound(game); const int x_right = game_x_right_bound(game); const int y_top = game_y_top_bound(game); const int y_bottom = game_y_bottom_bound(game); const int x_mid = x_left + ((x_right - x_left) / 2); const int y_mid = y_top + ((y_bottom - y_top) / 2); const int board_width = CHESS_TILE_SIZE_X * CHESS_BOARD_COLUMNS; const int board_height = CHESS_TILE_SIZE_Y * CHESS_BOARD_ROWS; state->curs_x = x_mid + 1; state->curs_y = y_mid; board->x_left_bound = x_mid - (board_width / 2); board->x_right_bound = x_mid + (board_width / 2); board->y_bottom_bound = y_mid + (board_height / 2); board->y_top_bound = y_mid - (board_height / 2); if (board->y_bottom_bound > y_bottom || board->x_left_bound < x_left) { return -1; } size_t colour_rotation = 1; size_t board_idx = 0; size_t letter_idx = self_is_white ? 0 : 7; for (size_t i = board->x_left_bound; i < board->x_right_bound; i += CHESS_TILE_SIZE_X) { size_t number_idx = self_is_white ? 8 : 1; char letter = Board_Letters[letter_idx]; letter_idx = self_is_white ? letter_idx + 1 : letter_idx - 1; colour_rotation ^= 1; for (size_t j = board->y_top_bound; j < board->y_bottom_bound; j += CHESS_TILE_SIZE_Y) { Tile *tile = &board->tiles[board_idx]; tile->colour = (board_idx + colour_rotation) % 2 == 0 ? CHESS_WHITE_SQUARE_COLOUR : CHESS_BLACK_SQUARE_COLOUR; tile->coords.x = i; tile->coords.y = j; tile->chess_coords.L = letter; tile->chess_coords.N = number_idx; number_idx = self_is_white ? number_idx - 1 : number_idx + 1; ++board_idx; } } for (size_t i = 0; i < CHESS_SQUARES; ++i) { Tile *tile = &board->tiles[i]; chess_set_piece(&tile->piece, NoPiece, White); if (tile->chess_coords.N == 2 || tile->chess_coords.N == 7) { chess_set_piece(&tile->piece, Pawn, tile->chess_coords.N == 2 ? White : Black); continue; } if (tile->chess_coords.N == 1 || tile->chess_coords.N == 8) { PieceType type; switch (tile->chess_coords.L) { case 'a': // fallthrough case 'h': type = Rook; break; case 'b': // fallthrough case 'g': type = Knight; break; case 'c': // fallthrough case 'f': type = Bishop; break; case 'd': type = Queen; break; case 'e': type = King; break; default: type = NoPiece; break; } chess_set_piece(&tile->piece, type, tile->chess_coords.N == 1 ? White : Black); } } return 0; } static int chess_packet_send_resign(const GameData *game) { uint8_t data[1]; data[0] = CHESS_PACKET_RESIGN; if (game_packet_send(game, data, 1, GP_Data) == -1) { return -1; } return 0; } static int chess_packet_send_move(const GameData *game, const Tile *from, const Tile *to) { uint8_t data[5]; data[0] = CHESS_PACKET_MOVE_PIECE; data[1] = from->chess_coords.L; data[2] = from->chess_coords.N; data[3] = to->chess_coords.L; data[4] = to->chess_coords.N; if (game_packet_send(game, data, 5, GP_Data) == -1) { return -1; } return 0; } static int chess_packet_send_invite(const GameData *game, bool self_is_white) { uint8_t data[2]; data[0] = CHESS_PACKET_INIT_SEND_INVITE; data[1] = self_is_white ? Black : White; if (game_packet_send(game, data, 2, GP_Invite) == -1) { return -1; } return 0; } static int chess_packet_send_accept(const GameData *game) { uint8_t data[1]; data[0] = CHESS_PACKET_INIT_ACCEPT_INVITE; if (game_packet_send(game, data, 1, GP_Data) == -1) { return -1; } return 0; } int chess_initialize(GameData *game, const uint8_t *init_data, size_t length) { if (game_set_window_shape(game, GW_ShapeSquare) == -1) { return -1; } ChessState *state = calloc(1, sizeof(ChessState)); if (state == NULL) { return -3; } bool self_is_host = false; bool self_is_white = false; if (length == 0) { self_is_host = true; self_is_white = rand() % 2 == 0; } else { if (length < 2 || init_data[0] != CHESS_PACKET_INIT_SEND_INVITE) { free(state); return -2; } ChessColour colour = (ChessColour)init_data[1]; if (colour != White && colour != Black) { free(state); return -2; } self_is_white = colour == White; } state->self.colour = self_is_white ? White : Black; state->other.colour = !self_is_white ? White : Black; if (chess_init_board(game, state, self_is_white) == -1) { free(state); return -3; } state->self.can_castle_ks = true; state->self.can_castle_qs = true; state->other.can_castle_ks = true; state->other.can_castle_qs = true; if (self_is_host) { if (chess_packet_send_invite(game, self_is_white) == -1) { free(state); return -2; } } else { if (chess_packet_send_accept(game) == -1) { free(state); return -2; } state->status = Playing; } game_set_cb_render_window(game, chess_cb_render_window, state); game_set_cb_on_keypress(game, chess_cb_on_keypress, state); game_set_cb_kill(game, chess_cb_kill, state); game_set_cb_on_packet(game, chess_cb_on_packet, state); return 0; } toxic-0.11.3/src/game_chess.h000066400000000000000000000024561416141666600160020ustar00rootroot00000000000000/* game_chess.h * * * Copyright (C) 2020 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef GAME_CHESS #define GAME_CHESS #include "game_base.h" /* * Initializes chess game state. * * If `init_data` is non-null, this indicates that we were invited to the game. * * If we're the inviter, we send an invite packet after initialization. If we're the * invitee, we send a handshake response packet to the inviter. * * Return 0 on success. * Return -1 if window is too small. * Return -2 on network related error. * Return -3 on other error. */ int chess_initialize(GameData *game, const uint8_t *init_data, size_t length); #endif // GAME_CHESS toxic-0.11.3/src/game_life.c000066400000000000000000000353171416141666600156110ustar00rootroot00000000000000/* game_life.c * * * Copyright (C) 2021 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #include #include #include #include "game_life.h" #define LIFE_DEFAULT_CELL_CHAR 'o' #define LIFE_CELL_DEFAULT_COLOUR CYAN #define LIFE_DEFAULT_SPEED 25 #define LIFE_MAX_SPEED 40 /* Determines the additional size of the grid beyond the visible boundaries. * * This buffer allows cells to continue growing off-screen giving the illusion of an * infinite grid to a certain point. */ #define LIFE_BOUNDARY_BUFFER 50 typedef struct Cell { Coords coords; bool alive; bool marked; // true if cell should invert alive status at end of current cycle int display_char; size_t age; } Cell; typedef struct LifeState { TIME_MS time_last_cycle; size_t speed; size_t generation; bool paused; Cell **cells; int num_columns; int num_rows; int curs_x; int curs_y; int x_left_bound; int x_right_bound; int y_top_bound; int y_bottom_bound; short display_candy; int colour; } LifeState; static void life_increase_speed(LifeState *state) { if (state->speed < LIFE_MAX_SPEED) { ++state->speed; } } static void life_decrease_speed(LifeState *state) { if (state->speed > 1) { --state->speed; } } static int life_get_display_char(const LifeState *state, const Cell *cell) { if (state->display_candy == 1) { if (cell->age == 1) { return '.'; } return '+'; } if (state->display_candy == 2) { if (cell->age == 1) { return '.'; } if (cell->age == 2) { return '-'; } if (cell->age == 3) { return 'o'; } return 'O'; } return 'o'; } static void life_toggle_display_candy(LifeState *state) { state->display_candy = (state->display_candy + 1) % 3; // magic number depends on life_get_display_char() } static void life_cycle_colour(LifeState *state) { switch (state->colour) { case RED: { state->colour = YELLOW; break; } case YELLOW: { state->colour = GREEN; break; } case GREEN: { state->colour = CYAN; break; } case CYAN: { state->colour = BLUE; break; } case BLUE: { state->colour = MAGENTA; break; } case MAGENTA: { state->colour = RED; break; } default: { state->colour = RED; break; } } } static Cell *life_get_cell_at_coords(const LifeState *state, const int x, const int y) { const int i = y - (state->y_top_bound - (LIFE_BOUNDARY_BUFFER / 2)); const int j = x - (state->x_left_bound - (LIFE_BOUNDARY_BUFFER / 2)); if (i >= 0 && j >= 0) { return &state->cells[i][j]; } return NULL; } static void life_draw_cells(const GameData *game, WINDOW *win, LifeState *state) { wattron(win, A_BOLD | COLOR_PAIR(state->colour)); for (int i = LIFE_BOUNDARY_BUFFER / 2; i < state->num_rows - (LIFE_BOUNDARY_BUFFER / 2); ++i) { for (int j = LIFE_BOUNDARY_BUFFER / 2; j < state->num_columns + 1 - (LIFE_BOUNDARY_BUFFER / 2); ++j) { Cell *cell = &state->cells[i][j]; if (cell->alive) { Coords coords = cell->coords; mvwaddch(win, coords.y, coords.x, cell->display_char); } } } wattroff(win, A_BOLD | COLOR_PAIR(state->colour)); } static void life_toggle_cell(LifeState *state) { Cell *cell = life_get_cell_at_coords(state, state->curs_x, state->curs_y); if (cell == NULL) { return; } cell->alive ^= 1; } /* * Returns the number of live neighbours of cell at `i` `j` position. * * Returns NULL if cell is touching a border. */ static int life_get_live_neighbours(const LifeState *state, const int i, const int j) { Cell *n[8] = {0}; if (i > 0 && j > 0) { n[0] = &state->cells[i - 1][j - 1]; } if (i > 0) { n[1] = &state->cells[i - 1][j]; } if (i > 0 && j < state->num_columns - 1) { n[2] = &state->cells[i - 1][j + 1]; } if (j > 0) { n[3] = &state->cells[i][j - 1]; } if (j < state->num_columns - 1) { n[4] = &state->cells[i][j + 1]; } if (i < state->num_rows - 1 && j > 0) { n[5] = &state->cells[i + 1][j - 1]; } if (i < state->num_rows - 1) { n[6] = &state->cells[i + 1][j]; } if (i < state->num_rows - 1 && j < state->num_columns - 1) { n[7] = &state->cells[i + 1][j + 1]; } int count = 0; for (size_t i = 0; i < 8; ++i) { if (n[i] == NULL) { return 0; // If we're at a boundary kill cell } if (n[i]->alive) { ++count; } } return count; } static void life_restart(GameData *game, LifeState *state) { for (int i = 0; i < state->num_rows; ++i) { for (int j = 0; j < state->num_columns; ++j) { Cell *cell = &state->cells[i][j]; cell->alive = false; cell->marked = false; cell->display_char = LIFE_DEFAULT_CELL_CHAR; cell->age = 0; } } game_set_score(game, 0); state->generation = 0; } static void life_do_cells(LifeState *state) { for (int i = 0; i < state->num_rows; ++i) { for (int j = 0; j < state->num_columns; ++j) { Cell *cell = &state->cells[i][j]; if (cell->marked) { cell->marked = false; cell->alive ^= 1; cell->age = cell->alive; cell->display_char = life_get_display_char(state, cell); } else if (cell->alive) { ++cell->age; cell->display_char = life_get_display_char(state, cell); } } } } static void life_cycle(GameData *game, LifeState *state) { if (state->generation == 0) { return; } TIME_MS cur_time = get_time_millis(); if (!game_do_object_state_update(game, cur_time, state->time_last_cycle, state->speed)) { return; } state->time_last_cycle = get_time_millis(); ++state->generation; size_t live_cells = 0; for (int i = 0; i < state->num_rows; ++i) { for (int j = 0; j < state->num_columns; ++j) { Cell *cell = &state->cells[i][j]; int live_neighbours = life_get_live_neighbours(state, i, j); if (cell->alive) { if (!(live_neighbours == 2 || live_neighbours == 3)) { cell->marked = true; } else { ++live_cells; } } else { if (live_neighbours == 3) { cell->marked = true; ++live_cells; } } } } if (live_cells == 0) { life_restart(game, state); return; } life_do_cells(state); game_update_score(game, 1); } static void life_start(GameData *game, LifeState *state) { state->generation = 1; } void life_cb_update_game_state(GameData *game, void *cb_data) { if (!cb_data) { return; } LifeState *state = (LifeState *)cb_data; life_cycle(game, state); } void life_cb_render_window(GameData *game, WINDOW *win, void *cb_data) { if (!cb_data) { return; } LifeState *state = (LifeState *)cb_data; move(state->curs_y, state->curs_x); if (state->generation == 0 || state->paused) { curs_set(1); } life_draw_cells(game, win, state); } static void life_move_curs_left(LifeState *state) { int new_x = state->curs_x - 1; if (new_x < state->x_left_bound) { return; } state->curs_x = new_x; } static void life_move_curs_right(LifeState *state) { int new_x = state->curs_x + 1; if (new_x > state->x_right_bound) { return; } state->curs_x = new_x; } static void life_move_curs_up(LifeState *state) { int new_y = state->curs_y - 1; if (new_y < state->y_top_bound) { return; } state->curs_y = new_y; } static void life_move_curs_down(LifeState *state) { int new_y = state->curs_y + 1; if (new_y >= state->y_bottom_bound) { return; } state->curs_y = new_y; } static void life_move_curs_up_left(LifeState *state) { life_move_curs_up(state); life_move_curs_left(state); } static void life_move_curs_up_right(LifeState *state) { life_move_curs_up(state); life_move_curs_right(state); } static void life_move_curs_down_right(LifeState *state) { life_move_curs_down(state); life_move_curs_right(state); } static void life_move_curs_down_left(LifeState *state) { life_move_curs_down(state); life_move_curs_left(state); } void life_cb_on_keypress(GameData *game, int key, void *cb_data) { if (!cb_data) { return; } LifeState *state = (LifeState *)cb_data; switch (key) { case KEY_LEFT: { life_move_curs_left(state); break; } case KEY_RIGHT: { life_move_curs_right(state); break; } case KEY_DOWN: { life_move_curs_down(state); break; } case KEY_UP: { life_move_curs_up(state); break; } case KEY_HOME: { life_move_curs_up_left(state); break; } case KEY_END: { life_move_curs_down_left(state); break; } case KEY_PPAGE: { life_move_curs_up_right(state); break; } case KEY_NPAGE: { life_move_curs_down_right(state); break; } case '\r': { if (state->generation > 0) { life_restart(game, state); } else { life_start(game, state); } break; } case ' ': { life_toggle_cell(state); break; } case '=': /* intentional fallthrough */ case '+': { life_increase_speed(state); break; } case '-': /* intentional fallthrough */ case '_': { life_decrease_speed(state); break; } case '\t': { life_toggle_display_candy(state); break; } case '`': { life_cycle_colour(state); break; } default: { return; } } } static void life_free_cells(LifeState *state) { if (state->cells == NULL) { return; } for (int i = 0; i < state->num_rows; ++i) { if (state->cells[i]) { free(state->cells[i]); } } free(state->cells); } void life_cb_pause(GameData *game, bool is_paused, void *cb_data) { if (!cb_data) { return; } LifeState *state = (LifeState *)cb_data; state->paused = is_paused; } void life_cb_kill(GameData *game, void *cb_data) { if (!cb_data) { return; } LifeState *state = (LifeState *)cb_data; life_free_cells(state); free(state); game_set_cb_update_state(game, NULL, NULL); game_set_cb_render_window(game, NULL, NULL); game_set_cb_kill(game, NULL, NULL); game_set_cb_on_keypress(game, NULL, NULL); } static int life_init_state(GameData *game, LifeState *state) { const int x_left = game_x_left_bound(game) ; const int x_right = game_x_right_bound(game); const int y_top = game_y_top_bound(game); const int y_bottom = game_y_bottom_bound(game) + 1; state->x_left_bound = x_left; state->x_right_bound = x_right; state->y_top_bound = y_top; state->y_bottom_bound = y_bottom; const int x_mid = x_left + ((x_right - x_left) / 2); const int y_mid = y_top + ((y_bottom - y_top) / 2); state->curs_x = x_mid; state->curs_y = y_mid; const int num_rows = (y_bottom - y_top) + LIFE_BOUNDARY_BUFFER; const int num_columns = (x_right - x_left) + LIFE_BOUNDARY_BUFFER; if (num_rows <= 0 || num_columns <= 0) { return -1; } state->num_columns = num_columns; state->num_rows = num_rows; state->cells = calloc(1, num_rows * sizeof(Cell *)); if (state->cells == NULL) { return -1; } for (int i = 0; i < num_rows; ++i) { state->cells[i] = calloc(1, num_columns * sizeof(Cell)); if (state->cells[i] == NULL) { return -1; } for (int j = 0; j < num_columns; ++j) { state->cells[i][j].coords.y = i + (state->y_top_bound - (LIFE_BOUNDARY_BUFFER / 2)); state->cells[i][j].coords.x = j + (state->x_left_bound - (LIFE_BOUNDARY_BUFFER / 2)); } } state->speed = LIFE_DEFAULT_SPEED; state->colour = LIFE_CELL_DEFAULT_COLOUR; life_restart(game, state); return 0; } int life_initialize(GameData *game) { // Try best fit from largest to smallest before giving up if (game_set_window_shape(game, GW_ShapeRectangleLarge) == -1) { if (game_set_window_shape(game, GW_ShapeSquareLarge) == -1) { if (game_set_window_shape(game, GW_ShapeRectangle) == -1) { if (game_set_window_shape(game, GW_ShapeSquare) == -1) { return -1; } } } } LifeState *state = calloc(1, sizeof(LifeState)); if (state == NULL) { return -1; } if (life_init_state(game, state) == -1) { life_free_cells(state); free(state); return -1; } game_set_update_interval(game, 40); game_show_score(game, true); game_set_cb_update_state(game, life_cb_update_game_state, state); game_set_cb_render_window(game, life_cb_render_window, state); game_set_cb_on_keypress(game, life_cb_on_keypress, state); game_set_cb_on_pause(game, life_cb_pause, state); game_set_cb_kill(game, life_cb_kill, state); return 0; } toxic-0.11.3/src/game_life.h000066400000000000000000000015511416141666600156070ustar00rootroot00000000000000/* game_life.h * * * Copyright (C) 2021 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef GAME_LIFE #define GAME_LIFE #include "game_base.h" int life_initialize(GameData *game); #endif // GAME_LIFE toxic-0.11.3/src/game_snake.c000066400000000000000000000603731416141666600157730ustar00rootroot00000000000000/* game_snake.c * * * Copyright (C) 2020 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #include #include #include #include "game_base.h" #include "game_util.h" #include "game_snake.h" #include "misc_tools.h" #define SNAKE_MAX_SNAKE_LENGTH (GAME_MAX_SQUARE_X_DEFAULT * GAME_MAX_SQUARE_Y_DEFAULT) #define SNAKE_AGENT_MAX_LIST_SIZE (GAME_MAX_SQUARE_X_DEFAULT * GAME_MAX_SQUARE_Y_DEFAULT) #define SNAKE_DEFAULT_SNAKE_SPEED 6 #define SNAKE_DEFAULT_AGENT_SPEED 1 #define SNAKE_MAX_SNAKE_SPEED 12 #define SNAKE_MAX_AGENT_SPEED (SNAKE_MAX_SNAKE_SPEED / 2) #define SNAKE_DEFAULT_UPDATE_INTERVAL 20 /* How often we update frames independent of the speed of the game or objects */ #define SNAKE_FRAME_DRAW_SPEED 5 /* How long a regular message stays on the screen */ #define SNAKE_DEFAULT_MESSAGE_TIMER 5 /* Increment snake speed by 1 every time level increases by this amount */ #define SNAKE_LEVEL_SPEED_INTERVAL 5 /* Increment level by 1 every time snake eats this many foods */ #define SNAKE_LEVEL_UP_FOOD_LIMIT 4 /* Increment agent speed by 1 every time level increases by this amount */ #define SNAKE_AGENT_LEVEL_SPEED_INTERVAL 2 /* Points multiplier for getting a powerup */ #define SNAKE_POWERUP_BONUS_MULTIPLIER 5 /* Extra bonus for running over a glowing agent */ #define SNAKE_AGENT_GLOWING_MULTIPLIER 2 /* Agents begin glowing if their speed is greater than this */ #define SNAKE_AGENT_GLOWING_SPEED (SNAKE_DEFAULT_AGENT_SPEED + 2) /* A new powerup is placed on the board after this many seconds since last one wore off */ #define SNAKE_POWERUP_INTERVAL 45 /* How long a powerup lasts */ #define SNAKE_POWERUP_TIMER 12 /* Number of key presses to queue; one key press is retrieved per state update */ #define SNAKE_KEY_PRESS_QUEUE_SIZE 3 /* These decide how many points to decay and how often to penalize camping for powerups */ #define SNAKE_DECAY_POINTS_INTERVAL 1 #define SNAKE_DECAY_POINTS_FRACTION 10 #define SNAKE_HEAD_COLOUR GREEN #define SNAKE_DEAD_BODY_CHAR 'o' #define SNAKE_DEAD_BODY_COLOUR RED #define SNAKE_BODY_COLOUR CYAN #define SNAKE_BODY_CHAR 'o' #define SNAKE_FOOD_COLOUR YELLOW #define SNAKE_FOOD_CHAR '*' #define SNAKE_AGENT_NORMAL_COLOUR RED #define SNAKE_AGENT_GLOWING_COLOUR GREEN #define SNAKE_AGENT_NORMAL_CHAR 'x' #define SNAKE_AGENT_GLOWING_CHAR 'X' #define SNAKE_POWERUP_CHAR 'P' typedef struct NasaAgent { Coords coords; bool is_alive; bool is_glowing; TIME_MS last_time_moved; size_t speed; char display_char; int colour; int attributes; } NasaAgent; typedef struct Snake { Coords coords; char display_char; int colour; int attributes; } Snake; typedef struct SnakeState { Snake *snake; size_t snake_length; size_t snake_speed; TIME_MS snake_time_last_moved; bool has_powerup; Direction direction; Coords powerup; TIME_S powerup_timer; TIME_S last_powerup_time; Coords food; NasaAgent *agents; size_t agent_list_size; TIME_S last_time_points_decayed; TIME_S pause_time; int key_press_queue[SNAKE_KEY_PRESS_QUEUE_SIZE]; size_t keys_skip_counter; TIME_MS last_draw_update; bool game_over; } SnakeState; static void snake_create_points_message(GameData *game, Direction dir, long int points, const Coords *coords) { char buf[GAME_MAX_MESSAGE_SIZE + 1]; snprintf(buf, sizeof(buf), "%ld", points); if (game_set_message(game, buf, strlen(buf), dir, A_BOLD, WHITE, 0, coords, false, false) == -1) { fprintf(stderr, "failed to set points message\n"); } } static void snake_create_message(GameData *game, Direction dir, const char *message, int attributes, int colour, TIME_S timeout, const Coords *coords, bool priority) { if (game_set_message(game, message, strlen(message), dir, attributes, colour, timeout, coords, false, priority) == -1) { fprintf(stderr, "failed to set message\n"); } } static Coords *snake_get_head_coords(const SnakeState *state) { return &state->snake[0].coords; } static void snake_set_head_char(SnakeState *state) { Snake *snake_head = &state->snake[0]; switch (state->direction) { case NORTH: snake_head->display_char = '^'; break; case SOUTH: snake_head->display_char = 'v'; break; case EAST: snake_head->display_char = '>'; return; case WEST: snake_head->display_char = '<'; break; default: snake_head->display_char = '?'; break; } } static bool snake_validate_direction(const SnakeState *state, Direction dir) { if (!GAME_UTIL_DIRECTION_VALID(dir)) { return false; } const int diff = abs((int)state->direction - (int)dir); return diff != 1; } static void snake_update_direction(SnakeState *state) { for (size_t i = 0; i < SNAKE_KEY_PRESS_QUEUE_SIZE; ++i) { int key = state->key_press_queue[i]; if (key == 0) { continue; } Direction dir = game_util_get_direction(key); if (!GAME_UTIL_DIRECTION_VALID(dir)) { state->key_press_queue[i] = 0; continue; } if (snake_validate_direction(state, dir)) { state->direction = dir; snake_set_head_char(state); state->key_press_queue[i] = 0; state->keys_skip_counter = 0; break; } if (state->keys_skip_counter++ >= SNAKE_KEY_PRESS_QUEUE_SIZE) { state->keys_skip_counter = 0; memset(state->key_press_queue, 0, sizeof(state->key_press_queue)); } } } static void snake_set_key_press(SnakeState *state, int key) { for (size_t i = 0; i < SNAKE_KEY_PRESS_QUEUE_SIZE; ++i) { if (state->key_press_queue[i] == 0) { state->key_press_queue[i] = key; return; } } memset(state->key_press_queue, 0, sizeof(state->key_press_queue)); state->key_press_queue[0] = key; } static void snake_update_score(GameData *game, const SnakeState *state, long int points) { const Coords *head = snake_get_head_coords(state); snake_create_points_message(game, state->direction, points, head); game_update_score(game, points); } static long int snake_get_move_points(const SnakeState *state) { return state->snake_length + (2 * state->snake_speed); } /* Return true if snake body is occupying given coordinates */ static bool snake_coords_contain_body(const SnakeState *state, const Coords *coords) { for (size_t i = 1; i < state->snake_length; ++i) { Coords snake_coords = state->snake[i].coords; if (COORDINATES_OVERLAP(coords->x, coords->y, snake_coords.x, snake_coords.y)) { return true; } } return false; } static bool snake_self_consume(const SnakeState *state) { const Coords *head = snake_get_head_coords(state); return snake_coords_contain_body(state, head); } /* * Returns a pointer to the agent at x and y coordinates. * * Returns NULL if no living agent is at coords. */ static NasaAgent *snake_get_agent_at_coords(const SnakeState *state, const Coords *coords) { for (size_t i = 0; i < state->agent_list_size; ++i) { NasaAgent *agent = &state->agents[i]; if (!agent->is_alive) { continue; } if (COORDINATES_OVERLAP(coords->x, coords->y, agent->coords.x, agent->coords.y)) { return agent; } } return NULL; } /* * Return true if snake got caught by an agent and doesn't have a powerup. * * If snake runs over agent and does have a powerup the agent is killed and score updated. */ static bool snake_agent_caught(GameData *game, SnakeState *state, const Coords *coords) { NasaAgent *agent = snake_get_agent_at_coords(state, coords); if (agent == NULL) { return false; } if (!state->has_powerup) { return true; } agent->is_alive = false; long int points = snake_get_move_points(state) * (agent->speed + 1); if (agent->is_glowing) { points *= SNAKE_AGENT_GLOWING_MULTIPLIER; } snake_update_score(game, state, points); return false; } static bool snake_state_valid(GameData *game, SnakeState *state) { const Coords *head = snake_get_head_coords(state); if (!game_coordinates_in_bounds(game, head->x, head->y)) { snake_create_message(game, state->direction, "Ouch!", A_BOLD, WHITE, SNAKE_DEFAULT_MESSAGE_TIMER, head, true); return false; } if (snake_self_consume(state)) { snake_create_message(game, state->direction, "Tastes like chicken", A_BOLD, WHITE, SNAKE_DEFAULT_MESSAGE_TIMER, head, true); return false; } if (snake_agent_caught(game, state, head)) { snake_create_message(game, state->direction, "ARGH they got me!", A_BOLD, WHITE, SNAKE_DEFAULT_MESSAGE_TIMER, head, true); return false; } return true; } /* * Sets colour and attributes for entire snake except head. * * If colour is set to -1 a random colour is chosen for each body part. */ static void snake_set_body_attributes(Snake *snake, size_t length, int colour, int attributes) { for (size_t i = 1; i < length; ++i) { Snake *body = &snake[i]; if (colour == -1) { body->colour = game_util_random_colour(); } else { body->colour = colour; } body->attributes = attributes; } } static void snake_move_body(SnakeState *state) { for (size_t i = state->snake_length - 1; i > 0; --i) { Coords *curr = &state->snake[i].coords; Coords prev = state->snake[i - 1].coords; curr->x = prev.x; curr->y = prev.y; } } static void snake_move_head(SnakeState *state) { Coords *head = snake_get_head_coords(state); game_util_move_coords(state->direction, head); } static void snake_grow(SnakeState *state) { size_t index = state->snake_length; if (index >= SNAKE_MAX_SNAKE_LENGTH) { return; } state->snake[index].coords.x = -1; state->snake[index].coords.y = -1; state->snake[index].display_char = SNAKE_BODY_CHAR; state->snake[index].colour = SNAKE_BODY_COLOUR; state->snake[index].attributes = A_BOLD; state->snake_length = index + 1; } static long int snake_check_food(const GameData *game, SnakeState *state) { Coords *food = &state->food; const Coords *head = snake_get_head_coords(state); if (!COORDINATES_OVERLAP(head->x, head->y, food->x, food->y)) { return 0; } snake_grow(state); game_random_coords(game, food); return snake_get_move_points(state); } static long int snake_check_powerup(GameData *game, SnakeState *state) { Coords *powerup = &state->powerup; const Coords *head = snake_get_head_coords(state); if (!COORDINATES_OVERLAP(head->x, head->y, powerup->x, powerup->y)) { return 0; } snake_create_message(game, state->direction, "AAAAA", A_BOLD, RED, 2, head, false); TIME_S t = get_unix_time(); state->has_powerup = true; state->powerup_timer = t; powerup->x = -1; powerup->y = -1; return snake_get_move_points(state) * SNAKE_POWERUP_BONUS_MULTIPLIER; } /* * Returns the first unoccupied index in agent array. */ static size_t snake_get_empty_agent_index(const NasaAgent *agents) { for (size_t i = 0; i < SNAKE_AGENT_MAX_LIST_SIZE; ++i) { if (!agents[i].is_alive) { return i; } } fprintf(stderr, "Warning: Agent array is full. This should be impossible\n"); return 0; } static void snake_initialize_agent(SnakeState *state, const Coords *coords) { size_t idx = snake_get_empty_agent_index(state->agents); if ((idx >= state->agent_list_size) && (idx + 1 <= SNAKE_AGENT_MAX_LIST_SIZE)) { state->agent_list_size = idx + 1; } NasaAgent *agent = &state->agents[idx]; agent->coords = (Coords) { coords->x, coords->y }; agent->is_alive = true; agent->is_glowing = false; agent->display_char = SNAKE_AGENT_NORMAL_CHAR; agent->colour = SNAKE_AGENT_NORMAL_COLOUR; agent->attributes = A_BOLD; agent->last_time_moved = 0; agent->speed = SNAKE_DEFAULT_AGENT_SPEED; } static void snake_dispatch_new_agent(const GameData *game, SnakeState *state) { Coords new_coords; const Coords *head = snake_get_head_coords(state); size_t tries = 0; do { if (tries++ >= 10) { return; } game_random_coords(game, &new_coords); } while (COORDINATES_OVERLAP(new_coords.x, new_coords.y, head->x, head->y) || snake_get_agent_at_coords(state, &new_coords) != NULL); snake_initialize_agent(state, &new_coords); } static void snake_place_powerup(const GameData *game, SnakeState *state) { Coords *powerup = &state->powerup; if (powerup->x != -1) { return; } if (!timed_out(state->last_powerup_time, SNAKE_POWERUP_INTERVAL)) { return; } game_random_coords(game, powerup); } static void snake_do_powerup(const GameData *game, SnakeState *state) { if (!state->has_powerup) { snake_place_powerup(game, state); return; } if (timed_out(state->powerup_timer, SNAKE_POWERUP_TIMER)) { state->last_powerup_time = get_unix_time(); state->has_powerup = false; snake_set_body_attributes(state->snake, state->snake_length, SNAKE_BODY_COLOUR, A_BOLD); } } static void snake_decay_points(GameData *game, SnakeState *state) { long int score = game_get_score(game); long int decay = snake_get_move_points(state) / SNAKE_DECAY_POINTS_FRACTION; if (score > decay && timed_out(state->last_time_points_decayed, SNAKE_DECAY_POINTS_INTERVAL)) { game_update_score(game, -decay); state->last_time_points_decayed = get_unix_time(); } } static void snake_do_points_update(GameData *game, SnakeState *state, long int points) { snake_update_score(game, state, points); if (state->snake_length % SNAKE_LEVEL_UP_FOOD_LIMIT != 0) { return; } game_increment_level(game); size_t level = game_get_current_level(game); if (level % SNAKE_LEVEL_SPEED_INTERVAL == 0 && state->snake_speed < SNAKE_MAX_SNAKE_SPEED) { ++state->snake_speed; } if (level % SNAKE_AGENT_LEVEL_SPEED_INTERVAL == 0) { for (size_t i = 0; i < state->agent_list_size; ++i) { NasaAgent *agent = &state->agents[i]; if (!agent->is_alive) { continue; } if (agent->speed < SNAKE_MAX_AGENT_SPEED) { ++agent->speed; } if (agent->speed > SNAKE_AGENT_GLOWING_SPEED && !agent->is_glowing) { agent->is_glowing = true; agent->display_char = SNAKE_AGENT_GLOWING_CHAR; agent->colour = SNAKE_AGENT_GLOWING_COLOUR; snake_create_message(game, state->direction, "*glows*", A_BOLD, GREEN, 2, &agent->coords, false); } } } snake_dispatch_new_agent(game, state); } static void snake_game_over(SnakeState *state) { state->game_over = true; state->has_powerup = false; state->snake[0].colour = SNAKE_DEAD_BODY_COLOUR; state->snake[0].attributes = A_BOLD | A_BLINK; snake_set_body_attributes(state->snake, state->snake_length, SNAKE_DEAD_BODY_COLOUR, A_BOLD | A_BLINK); } static void snake_move(GameData *game, SnakeState *state, TIME_MS cur_time) { const TIME_MS real_speed = GAME_UTIL_REAL_SPEED(state->direction, state->snake_speed); if (!game_do_object_state_update(game, cur_time, state->snake_time_last_moved, real_speed)) { return; } state->snake_time_last_moved = cur_time; snake_update_direction(state); snake_move_body(state); snake_move_head(state); if (!snake_state_valid(game, state)) { snake_game_over(state); game_set_status(game, GS_Finished); return; } long int points = snake_check_food(game, state) + snake_check_powerup(game, state); if (points > 0) { snake_do_points_update(game, state, points); } } /* * Attempts to move every agent in list. * * If an agent is normal it will move in a random direction. If it's glowing it will * move towards the snake. */ static void snake_agent_move(GameData *game, SnakeState *state, TIME_MS cur_time) { const Coords *head = snake_get_head_coords(state); for (size_t i = 0; i < state->agent_list_size; ++i) { NasaAgent *agent = &state->agents[i]; if (!agent->is_alive) { continue; } Coords *coords = &agent->coords; Coords new_coords = (Coords) { coords->x, coords->y }; Direction dir = !agent->is_glowing ? game_util_random_direction() : game_util_move_towards(coords, head, state->has_powerup); const TIME_MS real_speed = agent->is_glowing ? GAME_UTIL_REAL_SPEED(dir, agent->speed) : agent->speed; if (!game_do_object_state_update(game, cur_time, agent->last_time_moved, real_speed)) { continue; } agent->last_time_moved = cur_time; game_util_move_coords(dir, &new_coords); if (!game_coordinates_in_bounds(game, new_coords.x, new_coords.y)) { continue; } if (snake_coords_contain_body(state, &new_coords)) { continue; } if (snake_get_agent_at_coords(state, &new_coords) != NULL) { continue; } coords->x = new_coords.x; coords->y = new_coords.y; if (!state->has_powerup && COORDINATES_OVERLAP(head->x, head->y, new_coords.x, new_coords.y)) { snake_game_over(state); game_set_status(game, GS_Finished); return; } } } static void snake_update_frames(const GameData *game, SnakeState *state, TIME_MS cur_time) { if (!game_do_object_state_update(game, cur_time, state->last_draw_update, SNAKE_FRAME_DRAW_SPEED)) { return; } state->last_draw_update = cur_time; if (state->has_powerup) { const int time_left = SNAKE_POWERUP_TIMER - (get_unix_time() - state->powerup_timer); if (time_left <= 5 && time_left % 2 == 0) { snake_set_body_attributes(state->snake, state->snake_length, SNAKE_BODY_COLOUR, A_BOLD); } else { snake_set_body_attributes(state->snake, state->snake_length, -1, A_BOLD); } } } static void snake_draw_self(WINDOW *win, const SnakeState *state) { for (size_t i = 0; i < state->snake_length; ++i) { const Snake *body = &state->snake[i]; if (body->coords.x <= 0 || body->coords.y <= 0) { continue; } wattron(win, body->attributes | COLOR_PAIR(body->colour)); mvwaddch(win, body->coords.y, body->coords.x, body->display_char); wattroff(win, body->attributes | COLOR_PAIR(body->colour)); } } static void snake_draw_food(WINDOW *win, const SnakeState *state) { wattron(win, A_BOLD | COLOR_PAIR(SNAKE_FOOD_COLOUR)); mvwaddch(win, state->food.y, state->food.x, SNAKE_FOOD_CHAR); wattroff(win, A_BOLD | COLOR_PAIR(SNAKE_FOOD_COLOUR)); } static void snake_draw_agent(WINDOW *win, const SnakeState *state) { for (size_t i = 0; i < state->agent_list_size; ++i) { NasaAgent agent = state->agents[i]; if (agent.is_alive) { Coords coords = agent.coords; wattron(win, agent.attributes | COLOR_PAIR(agent.colour)); mvwaddch(win, coords.y, coords.x, agent.display_char); wattroff(win, agent.attributes | COLOR_PAIR(agent.colour)); } } } static void snake_draw_powerup(WINDOW *win, const SnakeState *state) { Coords powerup = state->powerup; if (powerup.x != -1) { int colour = game_util_random_colour(); wattron(win, A_BOLD | COLOR_PAIR(colour)); mvwaddch(win, powerup.y, powerup.x, SNAKE_POWERUP_CHAR); wattroff(win, A_BOLD | COLOR_PAIR(colour)); } } void snake_cb_update_game_state(GameData *game, void *cb_data) { if (!cb_data) { return; } SnakeState *state = (SnakeState *)cb_data; TIME_MS cur_time = get_time_millis(); snake_do_powerup(game, state); snake_agent_move(game, state, cur_time); snake_move(game, state, cur_time); snake_decay_points(game, state); if (!state->game_over) { snake_update_frames(game, state, cur_time); } } void snake_cb_render_window(GameData *game, WINDOW *win, void *cb_data) { if (!cb_data) { return; } SnakeState *state = (SnakeState *)cb_data; snake_draw_food(win, state); snake_draw_powerup(win, state); snake_draw_agent(win, state); snake_draw_self(win, state); } void snake_cb_kill(GameData *game, void *cb_data) { if (!cb_data) { return; } SnakeState *state = (SnakeState *)cb_data; free(state->snake); free(state->agents); free(state); game_set_cb_update_state(game, NULL, NULL); game_set_cb_render_window(game, NULL, NULL); game_set_cb_kill(game, NULL, NULL); game_set_cb_on_keypress(game, NULL, NULL); game_set_cb_on_pause(game, NULL, NULL); } void snake_cb_on_keypress(GameData *game, int key, void *cb_data) { if (!cb_data) { return; } SnakeState *state = (SnakeState *)cb_data; snake_set_key_press(state, key); } void snake_cb_pause(GameData *game, bool is_paused, void *cb_data) { UNUSED_VAR(game); if (!cb_data) { return; } SnakeState *state = (SnakeState *)cb_data; TIME_S t = get_unix_time(); if (is_paused) { state->pause_time = t; } else { state->powerup_timer += (t - state->pause_time); state->last_powerup_time += (t - state->pause_time); } } static void snake_initialize_snake_head(const GameData *game, Snake *snake) { int max_x; int max_y; game_max_x_y(game, &max_x, &max_y); snake[0].coords.x = max_x / 2; snake[0].coords.y = max_y / 2; snake[0].colour = SNAKE_HEAD_COLOUR; snake[0].attributes = A_BOLD; } int snake_initialize(GameData *game) { // note: if this changes we must update SNAKE_MAX_SNAKE_LENGTH and SNAKE_AGENT_MAX_LIST_SIZE if (game_set_window_shape(game, GW_ShapeSquare) == -1) { return -1; } SnakeState *state = calloc(1, sizeof(SnakeState)); if (state == NULL) { return -1; } state->snake = calloc(1, SNAKE_MAX_SNAKE_LENGTH * sizeof(Snake)); if (state->snake == NULL) { free(state); return -1; } state->agents = calloc(1, SNAKE_AGENT_MAX_LIST_SIZE * sizeof(NasaAgent)); if (state->agents == NULL) { free(state->snake); free(state); return -1; } snake_initialize_snake_head(game, state->snake); state->snake_speed = SNAKE_DEFAULT_SNAKE_SPEED; state->snake_length = 1; state->direction = NORTH; snake_set_head_char(state); state->powerup.x = -1; state->powerup.y = -1; state->last_powerup_time = get_unix_time(); game_show_level(game, true); game_show_score(game, true); game_show_high_score(game, true); game_increment_level(game); game_set_update_interval(game, SNAKE_DEFAULT_UPDATE_INTERVAL); game_random_coords(game, &state->food); game_set_cb_update_state(game, snake_cb_update_game_state, state); game_set_cb_render_window(game, snake_cb_render_window, state); game_set_cb_on_keypress(game, snake_cb_on_keypress, state); game_set_cb_kill(game, snake_cb_kill, state); game_set_cb_on_pause(game, snake_cb_pause, state); return 0; } toxic-0.11.3/src/game_snake.h000066400000000000000000000015551416141666600157750ustar00rootroot00000000000000/* game_snake.h * * * Copyright (C) 2020 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef GAME_SNAKE #define GAME_SNAKE #include "game_base.h" int snake_initialize(GameData *game); #endif // GAME_SNAKE toxic-0.11.3/src/game_util.c000066400000000000000000000100301416141666600156300ustar00rootroot00000000000000/* game_util.c * * * Copyright (C) 2020 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #include #include #include #include "game_util.h" #include "toxic.h" #include "windows.h" Direction game_util_get_direction(int key) { switch (key) { case KEY_UP: { return NORTH; } case KEY_DOWN: { return SOUTH; } case KEY_RIGHT: { return EAST; } case KEY_LEFT: { return WEST; } default: { return INVALID_DIRECTION; } } } Direction game_util_move_towards(const Coords *coords_a, const Coords *coords_b, bool inverse) { const int x1 = coords_a->x; const int y1 = coords_a->y; const int x2 = coords_b->x; const int y2 = coords_b->y; const int x_diff = abs(x1 - x2); const int y_diff = abs(y1 - y2); if (inverse) { if (x_diff > y_diff) { return x2 >= x1 ? WEST : EAST; } else { return y2 >= y1 ? NORTH : SOUTH; } } else { if (x_diff > y_diff) { return x2 < x1 ? WEST : EAST; } else { return y2 < y1 ? NORTH : SOUTH; } } } Direction game_util_random_direction(void) { int r = rand() % 4; switch (r) { case 0: return NORTH; case 1: return SOUTH; case 2: return EAST; case 3: return WEST; default: // impossible return NORTH; } } void game_util_move_coords(Direction direction, Coords *coords) { switch (direction) { case NORTH: { if (coords->y > 0) { --(coords->y); } break; } case SOUTH: { ++(coords->y); // Will rollover if you do something stupid break; } case EAST: { ++(coords->x); // Will rollover if you do something stupid break; } case WEST: { if (coords->x > 0) { --(coords->x); } break; } default: { fprintf(stderr, "Warning: tried to move in an invalid direction\n"); return; } } } int game_util_random_colour(void) { int r = rand() % 6; switch (r) { case 0: return GREEN; case 1: return CYAN; case 2: return RED; case 3: return BLUE; case 4: return YELLOW; case 5: return MAGENTA; default: // impossible return RED; } } static size_t net_pack_u16(uint8_t *bytes, uint16_t v) { bytes[0] = (v >> 8) & 0xff; bytes[1] = v & 0xff; return sizeof(v); } static size_t net_unpack_u16(const uint8_t *bytes, uint16_t *v) { uint8_t hi = bytes[0]; uint8_t lo = bytes[1]; *v = ((uint16_t)hi << 8) | lo; return sizeof(*v); } size_t game_util_pack_u32(uint8_t *bytes, uint32_t v) { uint8_t *p = bytes; p += net_pack_u16(p, (v >> 16) & 0xffff); p += net_pack_u16(p, v & 0xffff); return p - bytes; } size_t game_util_unpack_u32(const uint8_t *bytes, uint32_t *v) { const uint8_t *p = bytes; uint16_t hi; uint16_t lo; p += net_unpack_u16(p, &hi); p += net_unpack_u16(p, &lo); *v = ((uint32_t)hi << 16) | lo; return p - bytes; } toxic-0.11.3/src/game_util.h000066400000000000000000000050171416141666600156460ustar00rootroot00000000000000/* game_util.h * * * Copyright (C) 2020 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef GAME_UTIL #define GAME_UTIL #include #include typedef struct Coords { int x; int y; } Coords; // don't change these typedef enum Direction { NORTH = 0u, SOUTH = 1, EAST = 3, WEST = 4, INVALID_DIRECTION = 5 } Direction; /* * Use these for ms and second timestamps respectively so we don't accidentally interchange them */ typedef int64_t TIME_MS; typedef time_t TIME_S; /* * Return true if coordinates x1, y1 overlap with x2, y2. */ #define COORDINATES_OVERLAP(x1, y1, x2, y2)(((x1) == (x2)) && ((y1) == (y2))) /* * Halves speed if moving north or south. This accounts for the fact that Y steps are twice as large as X steps. */ #define GAME_UTIL_REAL_SPEED(dir, speed)((((dir) == (NORTH)) || ((dir) == (SOUTH))) ? (MAX(1, ((speed) / 2))) : (speed)) /* * Return true if dir is a valid Direction. */ #define GAME_UTIL_DIRECTION_VALID(dir)((dir) < (INVALID_DIRECTION)) /* * Returns cardinal direction mapped to `key`. */ Direction game_util_get_direction(int key); /* * Returns the direction that will move `coords_a` closest to `coords_b`. * * If `inverse` is true, returns the opposite result. */ Direction game_util_move_towards(const Coords *coords_a, const Coords *coords_b, bool inverse); /* * Returns a random direction. */ Direction game_util_random_direction(void); /* * Moves `coords` one square towards `direction`. */ void game_util_move_coords(Direction direction, Coords *coords); /* * Returns a random colour. */ int game_util_random_colour(void); /* * Packs an unsigned 32 bit integer `v` into `bytes`. */ size_t game_util_pack_u32(uint8_t *bytes, uint32_t v); /* * Unpacks an unsigned 32 bit integer in `bytes` to `v`. */ size_t game_util_unpack_u32(const uint8_t *bytes, uint32_t *v); #endif // GAME_UTIL toxic-0.11.3/src/global_commands.c000066400000000000000000000541571416141666600170250ustar00rootroot00000000000000/* global_commands.c * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #include #include #include "avatars.h" #include "conference.h" #include "friendlist.h" #include "help.h" #include "line_info.h" #include "log.h" #include "misc_tools.h" #include "name_lookup.h" #include "prompt.h" #include "qr_code.h" #include "term_mplex.h" #include "toxic.h" #include "toxic_strings.h" #include "windows.h" #ifdef GAMES #include "game_base.h" #endif /* command functions */ void cmd_accept(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); if (argc < 1) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Request ID required."); return; } long int req = strtol(argv[1], NULL, 10); if ((req == 0 && strcmp(argv[1], "0")) || req < 0 || req >= MAX_FRIEND_REQUESTS) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "No pending friend request with that ID."); return; } if (!FrndRequests.request[req].active) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "No pending friend request with that ID."); return; } Tox_Err_Friend_Add err; uint32_t friendnum = tox_friend_add_norequest(m, FrndRequests.request[req].key, &err); if (err != TOX_ERR_FRIEND_ADD_OK) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Failed to add friend (error %d\n)", err); return; } else { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Friend request accepted."); on_friend_added(m, friendnum, true); } FrndRequests.request[req] = (struct friend_request) { 0 }; int i; for (i = FrndRequests.max_idx; i > 0; --i) { if (FrndRequests.request[i - 1].active) { break; } } FrndRequests.max_idx = i; --FrndRequests.num_requests; } void cmd_add_helper(ToxWindow *self, Tox *m, const char *id_bin, const char *msg) { const char *errmsg; Tox_Err_Friend_Add err; uint32_t f_num = tox_friend_add(m, (const uint8_t *) id_bin, (const uint8_t *) msg, strlen(msg), &err); switch (err) { case TOX_ERR_FRIEND_ADD_TOO_LONG: errmsg = "Message is too long."; break; case TOX_ERR_FRIEND_ADD_NO_MESSAGE: errmsg = "Please add a message to your request."; break; case TOX_ERR_FRIEND_ADD_OWN_KEY: errmsg = "That appears to be your own ID."; break; case TOX_ERR_FRIEND_ADD_ALREADY_SENT: errmsg = "Friend request has already been sent."; break; case TOX_ERR_FRIEND_ADD_BAD_CHECKSUM: errmsg = "Bad checksum in address."; break; case TOX_ERR_FRIEND_ADD_SET_NEW_NOSPAM: errmsg = "Nospam was different."; break; case TOX_ERR_FRIEND_ADD_MALLOC: errmsg = "Core memory allocation failed."; break; case TOX_ERR_FRIEND_ADD_OK: errmsg = "Friend request sent."; on_friend_added(m, f_num, true); break; case TOX_ERR_FRIEND_ADD_NULL: /* fallthrough */ default: errmsg = "Failed to add friend: Unknown error."; break; } line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, errmsg); } void cmd_add(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); if (argc < 1) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Tox ID or address required."); return; } char msg[MAX_STR_SIZE] = {0}; const char *id = argv[1]; const size_t arg_length = strlen(id); const int space_idx = char_find(0, id, ' '); // we have to manually parse the message due to this command being a special case if (space_idx > 0 && space_idx < arg_length - 1) { snprintf(msg, sizeof(msg), "%s", &id[space_idx + 1]); } else { char selfname[TOX_MAX_NAME_LENGTH]; tox_self_get_name(m, (uint8_t *) selfname); size_t n_len = tox_self_get_name_size(m); selfname[n_len] = '\0'; snprintf(msg, sizeof(msg), "Hello, my name is %s. Care to Tox?", selfname); } char id_bin[TOX_ADDRESS_SIZE] = {0}; const bool is_tox_id = (char_find(0, id, '@') == arg_length) && (arg_length >= TOX_ADDRESS_SIZE * 2); if (!is_tox_id) { name_lookup(self, m, id_bin, id, msg); } char xx[3]; uint32_t x = 0; for (size_t i = 0; i < TOX_ADDRESS_SIZE; ++i) { xx[0] = id[2 * i]; xx[1] = id[2 * i + 1]; xx[2] = 0; if (sscanf(xx, "%02x", &x) != 1) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Invalid Tox ID."); return; } id_bin[i] = x; } if (friend_is_blocked(id_bin)) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Friend is in your block list."); return; } cmd_add_helper(self, m, id_bin, msg); } void cmd_avatar(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); if (argc != 1 || strlen(argv[1]) < 3) { avatar_unset(m); line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Avatar has been unset."); return; } char path[MAX_STR_SIZE]; snprintf(path, sizeof(path), "%s", argv[1]); int len = strlen(path); if (len <= 0) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Invalid path."); return; } path[len] = '\0'; char filename[MAX_STR_SIZE]; get_file_name(filename, sizeof(filename), path); if (avatar_set(m, path, len) == -1) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Failed to set avatar. Avatars must be in PNG format and may not exceed %d bytes.", MAX_AVATAR_FILE_SIZE); return; } line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Avatar set to '%s'", filename); } void cmd_clear(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(m); UNUSED_VAR(argc); UNUSED_VAR(argv); line_info_clear(self->chatwin->hst); force_refresh(window); } void cmd_connect(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); if (argc != 3) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Require: "); return; } const char *ip = argv[1]; const char *port_str = argv[2]; const char *ascii_key = argv[3]; long int port = strtol(port_str, NULL, 10); if (port <= 0 || port > MAX_PORT_RANGE) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Invalid port."); return; } char key_binary[TOX_PUBLIC_KEY_SIZE]; if (tox_pk_string_to_bytes(ascii_key, strlen(ascii_key), key_binary, sizeof(key_binary)) == -1) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Invalid key."); return; } Tox_Err_Bootstrap err; tox_bootstrap(m, ip, port, (uint8_t *) key_binary, &err); tox_add_tcp_relay(m, ip, port, (uint8_t *) key_binary, &err); switch (err) { case TOX_ERR_BOOTSTRAP_BAD_HOST: line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Bootstrap failed: Invalid IP."); break; case TOX_ERR_BOOTSTRAP_BAD_PORT: line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Bootstrap failed: Invalid port."); break; case TOX_ERR_BOOTSTRAP_NULL: line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Bootstrap failed."); break; default: break; } } void cmd_decline(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); UNUSED_VAR(m); if (argc < 1) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Request ID required."); return; } long int req = strtol(argv[1], NULL, 10); if ((req == 0 && strcmp(argv[1], "0")) || req < 0 || req >= MAX_FRIEND_REQUESTS) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "No pending friend request with that ID."); return; } if (!FrndRequests.request[req].active) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "No pending friend request with that ID."); return; } FrndRequests.request[req] = (struct friend_request) { 0 }; int i; for (i = FrndRequests.max_idx; i > 0; --i) { if (FrndRequests.request[i - 1].active) { break; } } FrndRequests.max_idx = i; --FrndRequests.num_requests; } #ifdef GAMES void cmd_game(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); if (argc < 1) { game_list_print(self); return; } GameType type = game_get_type(argv[1]); if (type >= GT_Invalid) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Unknown game."); return; } if (get_num_active_windows() >= MAX_WINDOWS_NUM) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, RED, " * Warning: Too many windows are open."); return; } uint32_t id = rand(); int ret = game_initialize(self, m, type, id, NULL, 0); switch (ret) { case 0: { break; } case -1: { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Window is too small."); return; } case -2: { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Game failed to initialize: Network error."); return; } case -3: { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Game is multiplayer only. Try the command again in the chat window of the contact you wish to play with."); return; } default: { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Game failed to initialize (error %d)", ret); return; } } } #endif // GAMES void cmd_conference(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); if (get_num_active_windows() >= MAX_WINDOWS_NUM) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, RED, " * Warning: Too many windows are open."); return; } if (argc < 1) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Please specify conference type: text | audio"); return; } uint8_t type; if (!strcasecmp(argv[1], "audio")) { type = TOX_CONFERENCE_TYPE_AV; } else if (!strcasecmp(argv[1], "text")) { type = TOX_CONFERENCE_TYPE_TEXT; } else { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Valid conference types are: text | audio"); return; } uint32_t conferencenum = 0; if (type == TOX_CONFERENCE_TYPE_TEXT) { Tox_Err_Conference_New err; conferencenum = tox_conference_new(m, &err); if (err != TOX_ERR_CONFERENCE_NEW_OK) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Conference instance failed to initialize (error %d)", err); return; } } else if (type == TOX_CONFERENCE_TYPE_AV) { #ifdef AUDIO conferencenum = toxav_add_av_groupchat(m, audio_conference_callback, NULL); if (conferencenum == (uint32_t) -1) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Audio conference instance failed to initialize"); return; } #else line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Audio support disabled by compile-time option."); return; #endif } if (init_conference_win(m, conferencenum, type, NULL, 0) == -1) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Conference window failed to initialize."); tox_conference_delete(m, conferencenum, NULL); return; } #ifdef AUDIO if (type == TOX_CONFERENCE_TYPE_AV) { if (!init_conference_audio_input(m, conferencenum)) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Audio capture failed; use \"/audio on\" to try again."); } } #endif line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Conference [%d] created.", conferencenum); } void cmd_log(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); const char *msg; struct chatlog *log = self->chatwin->log; if (argc == 0) { if (log->log_on) { msg = "Logging for this window is ON; type \"/log off\" to disable. (Logs are not encrypted)"; } else { msg = "Logging for this window is OFF; type \"/log on\" to enable."; } line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, msg); return; } const char *swch = argv[1]; if (!strcmp(swch, "1") || !strcmp(swch, "on")) { msg = log_enable(log) == 0 ? "Logging enabled." : "Warning: Failed to enable log."; line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, msg); return; } else if (!strcmp(swch, "0") || !strcmp(swch, "off")) { if (self->type == WINDOW_TYPE_CHAT) { Friends.list[self->num].logging_on = false; } log_disable(log); msg = "Logging disabled."; line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, msg); return; } msg = "Invalid option. Use \"/log on\" and \"/log off\" to toggle logging."; line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, msg); } void cmd_myid(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); UNUSED_VAR(argc); UNUSED_VAR(argv); char id_string[TOX_ADDRESS_SIZE * 2 + 1]; char bin_id[TOX_ADDRESS_SIZE]; tox_self_get_address(m, (uint8_t *) bin_id); if (tox_id_bytes_to_str(bin_id, sizeof(bin_id), id_string, sizeof(id_string)) == -1) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Failed to print ID."); return; } line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "%s", id_string); } #ifdef QRCODE void cmd_myqr(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); char id_string[TOX_ADDRESS_SIZE * 2 + 1]; char bin_id[TOX_ADDRESS_SIZE]; tox_self_get_address(m, (uint8_t *) bin_id); if (tox_id_bytes_to_str(bin_id, sizeof(bin_id), id_string, sizeof(id_string)) == -1) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Failed to create QR code."); return; } char nick[TOX_MAX_NAME_LENGTH]; tox_self_get_name(m, (uint8_t *) nick); size_t nick_len = tox_self_get_name_size(m); nick[nick_len] = '\0'; size_t data_file_len = strlen(DATA_FILE); char *dir = malloc(data_file_len + 1); if (dir == NULL) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Failed to create QR code: Out of memory."); return; } size_t dir_len = get_base_dir(DATA_FILE, data_file_len, dir); #ifdef QRPNG if (argc == 0) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Required 'txt' or 'png'"); free(dir); return; } else if (!strcmp(argv[1], "txt")) { #endif /* QRPNG */ size_t qr_path_buf_size = dir_len + nick_len + sizeof(QRCODE_FILENAME_EXT); char *qr_path = malloc(qr_path_buf_size); if (qr_path == NULL) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Failed to create QR code: Out of memory"); free(dir); return; } snprintf(qr_path, qr_path_buf_size, "%s%s%s", dir, nick, QRCODE_FILENAME_EXT); if (ID_to_QRcode_txt(id_string, qr_path) == -1) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Failed to create QR code."); free(dir); free(qr_path); return; } line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "QR code has been printed to the file '%s'", qr_path); free(qr_path); #ifdef QRPNG } else if (!strcmp(argv[1], "png")) { size_t qr_path_buf_size = dir_len + nick_len + sizeof(QRCODE_FILENAME_EXT_PNG); char *qr_path = malloc(qr_path_buf_size); if (qr_path == NULL) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Failed to create QR code: Out of memory"); free(dir); return; } snprintf(qr_path, qr_path_buf_size, "%s%s%s", dir, nick, QRCODE_FILENAME_EXT_PNG); if (ID_to_QRcode_png(id_string, qr_path) == -1) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Failed to create QR code."); free(dir); free(qr_path); return; } line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "QR code has been printed to the file '%s'", qr_path); free(qr_path); } else { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Unknown option '%s' -- Required 'txt' or 'png'", argv[1]); free(dir); return; } #endif /* QRPNG */ free(dir); } #endif /* QRCODE */ void cmd_nick(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); if (argc < 1) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Input required."); return; } char nick[MAX_STR_SIZE]; snprintf(nick, sizeof(nick), "%s", argv[1]); size_t len = strlen(nick); if (!valid_nick(nick)) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Invalid name."); return; } len = MIN(len, TOXIC_MAX_NAME_LENGTH - 1); nick[len] = '\0'; tox_self_set_name(m, (uint8_t *) nick, len, NULL); prompt_update_nick(prompt, nick); store_data(m, DATA_FILE); } void cmd_note(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); const char *note = argc >= 1 ? argv[1] : ""; prompt_update_statusmessage(prompt, m, note); } void cmd_nospam(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { long int nospam = rand(); if (argc > 0) { nospam = strtol(argv[1], NULL, 16); if ((nospam == 0 && strcmp(argv[1], "0")) || nospam < 0) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Invalid nospam value."); return; } } uint32_t old_nospam = tox_self_get_nospam(m); tox_self_set_nospam(m, (uint32_t) nospam); line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Your new Tox ID is:"); cmd_myid(window, self, m, 0, NULL); line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, ""); line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Any services that relied on your old ID will need to be updated manually."); line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "If you ever want your old Tox ID back, type '/nospam %X'", old_nospam); } void cmd_prompt_help(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); UNUSED_VAR(m); UNUSED_VAR(argc); UNUSED_VAR(argv); help_init_menu(self); } void cmd_quit(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); UNUSED_VAR(argc); UNUSED_VAR(argv); UNUSED_VAR(self); exit_toxic_success(m); } void cmd_requests(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); UNUSED_VAR(m); UNUSED_VAR(argc); UNUSED_VAR(argv); if (FrndRequests.num_requests == 0) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "No pending friend requests."); return; } int i, j; int count = 0; for (i = 0; i < FrndRequests.max_idx; ++i) { if (!FrndRequests.request[i].active) { continue; } char id[TOX_PUBLIC_KEY_SIZE * 2 + 1] = {0}; for (j = 0; j < TOX_PUBLIC_KEY_SIZE; ++j) { char d[3]; snprintf(d, sizeof(d), "%02X", FrndRequests.request[i].key[j] & 0xff); strcat(id, d); } line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "%d : %s", i, id); line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "%s", FrndRequests.request[i].msg); if (++count < FrndRequests.num_requests) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, ""); } } } void cmd_status(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); const char *errmsg; lock_status(); if (argc < 1) { errmsg = "Require a status. Statuses are: online, busy and away."; line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, errmsg); goto finish; } const char *status_str = argv[1]; Tox_User_Status status; if (!strcasecmp(status_str, "online")) { status = TOX_USER_STATUS_NONE; } else if (!strcasecmp(status_str, "away")) { status = TOX_USER_STATUS_AWAY; } else if (!strcasecmp(status_str, "busy")) { status = TOX_USER_STATUS_BUSY; } else { errmsg = "Invalid status. Valid statuses are: online, busy and away."; line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, errmsg); goto finish; } tox_self_set_status(m, status); prompt_update_status(prompt, status); line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Your status has been changed to %s.", status_str); finish: unlock_status(); } toxic-0.11.3/src/global_commands.h000066400000000000000000000061501416141666600170200ustar00rootroot00000000000000/* global_commands.h * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef GLOBAL_COMMANDS_H #define GLOBAL_COMMANDS_H #include "toxic.h" #include "windows.h" void cmd_accept(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_add(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_avatar(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_clear(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_connect(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_decline(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_conference(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_log(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_myid(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); #ifdef QRCODE void cmd_myqr(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); #endif /* QRCODE */ void cmd_nick(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_note(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_nospam(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_prompt_help(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_quit(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_requests(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_status(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_add_helper(ToxWindow *self, Tox *m, const char *id_bin, const char *msg); #ifdef AUDIO void cmd_list_devices(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_change_device(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); #endif /* AUDIO */ #ifdef VIDEO void cmd_list_video_devices(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_change_video_device(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); #endif /* VIDEO */ #ifdef PYTHON void cmd_run(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); #endif /* PYTHON */ #ifdef GAMES void cmd_game(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); #endif /* GAMES */ #endif /* GLOBAL_COMMANDS_H */ toxic-0.11.3/src/help.c000066400000000000000000000331151416141666600146230ustar00rootroot00000000000000/* help.c * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #include #include "help.h" #include "misc_tools.h" #include "toxic.h" #include "windows.h" #ifdef PYTHON #include "api.h" #endif /* PYTHON */ #ifdef PYTHON #define HELP_MENU_HEIGHT 10 #else #define HELP_MENU_HEIGHT 9 #endif /* PYTHON */ #define HELP_MENU_WIDTH 26 void help_init_menu(ToxWindow *self) { if (self->help->win) { delwin(self->help->win); } int y2, x2; getmaxyx(self->window, y2, x2); if (y2 < HELP_MENU_HEIGHT || x2 < HELP_MENU_WIDTH) { return; } self->help->win = newwin(HELP_MENU_HEIGHT, HELP_MENU_WIDTH, 3, 3); self->help->active = true; self->help->type = HELP_MENU; } static void help_exit(ToxWindow *self) { delwin(self->help->win); *(self->help) = (struct Help) { 0 }; } static void help_init_window(ToxWindow *self, int height, int width) { if (self->help->win) { delwin(self->help->win); } int y2, x2; getmaxyx(stdscr, y2, x2); if (y2 <= 0 || x2 <= 0) { return; } height = MIN(height, y2); width = MIN(width, x2); self->help->win = newwin(height, width, 0, 0); } static void help_draw_menu(ToxWindow *self) { WINDOW *win = self->help->win; wmove(win, 1, 1); wattron(win, A_BOLD | COLOR_PAIR(RED)); wprintw(win, " Help Menu\n"); wattroff(win, A_BOLD | COLOR_PAIR(RED)); wattron(win, A_BOLD | COLOR_PAIR(BLUE)); wprintw(win, " g"); wattroff(win, A_BOLD | COLOR_PAIR(BLUE)); wprintw(win, "lobal commands\n"); wattron(win, A_BOLD | COLOR_PAIR(BLUE)); wprintw(win, " c"); wattroff(win, A_BOLD | COLOR_PAIR(BLUE)); wprintw(win, "hat commands\n"); wprintw(win, " c"); wattron(win, A_BOLD | COLOR_PAIR(BLUE)); wprintw(win, "o"); wattroff(win, A_BOLD | COLOR_PAIR(BLUE)); wprintw(win, "nference commands\n"); #ifdef PYTHON wattron(win, A_BOLD | COLOR_PAIR(BLUE)); wprintw(win, " p"); wattroff(win, A_BOLD | COLOR_PAIR(BLUE)); wprintw(win, "lugin commands\n"); #endif /* PYTHON */ wattron(win, A_BOLD | COLOR_PAIR(BLUE)); wprintw(win, " f"); wattroff(win, A_BOLD | COLOR_PAIR(BLUE)); wprintw(win, "riendlist controls\n"); wattron(win, A_BOLD | COLOR_PAIR(BLUE)); wprintw(win, " k"); wattroff(win, A_BOLD | COLOR_PAIR(BLUE)); wprintw(win, "ey bindings\n"); wprintw(win, " e"); wattron(win, A_BOLD | COLOR_PAIR(BLUE)); wprintw(win, "x"); wattroff(win, A_BOLD | COLOR_PAIR(BLUE)); wprintw(win, "it menu\n"); box(win, ACS_VLINE, ACS_HLINE); wnoutrefresh(win); } static void help_draw_bottom_menu(WINDOW *win) { int y2, x2; getmaxyx(win, y2, x2); UNUSED_VAR(x2); wmove(win, y2 - 2, 1); wattron(win, A_BOLD | COLOR_PAIR(BLUE)); wprintw(win, " m"); wattroff(win, A_BOLD | COLOR_PAIR(BLUE)); wprintw(win, "ain menu |"); wprintw(win, " e"); wattron(win, A_BOLD | COLOR_PAIR(BLUE)); wprintw(win, "x"); wattroff(win, A_BOLD | COLOR_PAIR(BLUE)); wprintw(win, "it"); } static void help_draw_global(ToxWindow *self) { WINDOW *win = self->help->win; wmove(win, 1, 1); wattron(win, A_BOLD | COLOR_PAIR(RED)); wprintw(win, "Global Commands:\n"); wattroff(win, A_BOLD | COLOR_PAIR(RED)); wprintw(win, " /add : Add contact with optional message\n"); wprintw(win, " /accept : Accept friend request\n"); wprintw(win, " /avatar : Set an avatar (leave path empty to unset)\n"); wprintw(win, " /decline : Decline friend request\n"); wprintw(win, " /requests : List pending friend requests\n"); wprintw(win, " /connect : Manually connect to a DHT node\n"); wprintw(win, " /status : Set status (Online, Busy, Away)\n"); wprintw(win, " /note : Set a personal note\n"); wprintw(win, " /nick : Set your nickname\n"); wprintw(win, " /nospam : Change part of your Tox ID to stop spam\n"); wprintw(win, " /log or : Enable/disable logging\n"); wprintw(win, " /conference : Create a conference where type: text | audio\n"); wprintw(win, " /myid : Print your Tox ID\n"); #ifdef GAMES wprintw(win, " /game : Play a game\n"); #endif /* GAMES */ #ifdef QRCODE #ifdef QRPNG wprintw(win, " /myqr or : Print your Tox ID's QR code to a file.\n"); #else wprintw(win, " /myqr : Print your Tox ID's QR code to a file.\n"); #endif /* QRPNG */ #endif /* QRCODE */ wprintw(win, " /clear : Clear window history\n"); wprintw(win, " /close : Close the current chat window\n"); wprintw(win, " /quit or /exit : Exit Toxic\n"); #ifdef AUDIO wattron(win, A_BOLD); wprintw(win, "\n Audio:\n"); wattroff(win, A_BOLD); wprintw(win, " /lsdev : List devices where type: in|out\n"); wprintw(win, " /sdev : Set active device\n"); #endif /* AUDIO */ #ifdef VIDEO wattron(win, A_BOLD); wprintw(win, "\n Video:\n"); wattroff(win, A_BOLD); wprintw(win, " /lsvdev : List video devices where type: in|out\n"); wprintw(win, " /svdev : Set active video device\n"); #endif /* VIDEO */ #ifdef PYTHON wattron(win, A_BOLD); wprintw(win, "\n Scripting:\n"); wattroff(win, A_BOLD); wprintw(win, " /run : Load and run the script at path\n"); #endif /* PYTHON */ help_draw_bottom_menu(win); box(win, ACS_VLINE, ACS_HLINE); wnoutrefresh(win); } static void help_draw_chat(ToxWindow *self) { WINDOW *win = self->help->win; wmove(win, 1, 1); wattron(win, A_BOLD | COLOR_PAIR(RED)); wprintw(win, "Chat Commands:\n"); wattroff(win, A_BOLD | COLOR_PAIR(RED)); wprintw(win, " /invite : Invite contact to a conference \n"); wprintw(win, " /join : Join a pending conference\n"); wprintw(win, " /sendfile : Send a file\n"); wprintw(win, " /savefile : Receive a file\n"); wprintw(win, " /cancel : Cancel file transfer where type: in|out\n"); #ifdef AUDIO wattron(win, A_BOLD); wprintw(win, "\n Audio:\n"); wattroff(win, A_BOLD); wprintw(win, " /call : Audio call\n"); wprintw(win, " /answer : Answer incoming call\n"); wprintw(win, " /reject : Reject incoming call\n"); wprintw(win, " /hangup : Hangup active call\n"); wprintw(win, " /sdev : Change active device\n"); wprintw(win, " /mute : Mute active device if in call\n"); wprintw(win, " /sense : VAD sensitivity threshold\n"); wprintw(win, " /bitrate : Set the audio encoding bitrate\n"); #endif /* AUDIO */ #ifdef VIDEO wattron(win, A_BOLD); wprintw(win, "\n Video:\n"); wattroff(win, A_BOLD); wprintw(win, " /res : Set video resolution\n"); wprintw(win, " /vcall : Video call\n"); wprintw(win, " /video : Toggle video in call\n"); #endif /* VIDEO */ #ifdef GAMES wprintw(win, " /game : Play a game with contact\n"); #endif /* GAMES */ help_draw_bottom_menu(win); box(win, ACS_VLINE, ACS_HLINE); wnoutrefresh(win); } static void help_draw_keys(ToxWindow *self) { WINDOW *win = self->help->win; wmove(win, 1, 1); wattron(win, A_BOLD | COLOR_PAIR(RED)); wprintw(win, "Key bindings:\n"); wattroff(win, A_BOLD | COLOR_PAIR(RED)); wprintw(win, " Ctrl+O and Ctrl+P : Navigate through the tabs\n"); wprintw(win, " Page Up and Page Down : Scroll window history one line\n"); wprintw(win, " Ctrl+F and Ctrl+V : Scroll window history half a page\n"); wprintw(win, " Ctrl+H : Move to the bottom of window history\n"); wprintw(win, " Ctrl+up and Ctrl+down : Scroll peer list in conference\n"); wprintw(win, " Ctrl+B : Toggle the conference peerlist\n"); wprintw(win, " Ctrl+J : Insert new line\n"); wprintw(win, " Ctrl+T : Toggle paste mode\n\n"); wprintw(win, " (Note: Custom keybindings override these defaults.)\n\n"); help_draw_bottom_menu(win); box(win, ACS_VLINE, ACS_HLINE); wnoutrefresh(win); } static void help_draw_conference(ToxWindow *self) { WINDOW *win = self->help->win; wmove(win, 1, 1); wattron(win, A_BOLD | COLOR_PAIR(RED)); wprintw(win, "Conference commands:\n"); wattroff(win, A_BOLD | COLOR_PAIR(RED)); wprintw(win, " /title : Show/set conference title\n"); #ifdef AUDIO wattron(win, A_BOLD); wprintw(win, "\n Audio:\n"); wattroff(win, A_BOLD); wprintw(win, " /audio or : Enable/disable audio in an audio conference\n"); wprintw(win, " /mute : Toggle self audio mute status\n"); wprintw(win, " /mute or : Toggle peer audio mute status\n"); wprintw(win, " /ptt or : Toggle audio input Push-To-Talk (F2 to activate)\n"); wprintw(win, " /sense : VAD sensitivity threshold\n\n"); #endif help_draw_bottom_menu(win); box(win, ACS_VLINE, ACS_HLINE); wnoutrefresh(win); } #ifdef PYTHON static void help_draw_plugin(ToxWindow *self) { WINDOW *win = self->help->win; wmove(win, 1, 1); wattron(win, A_BOLD | COLOR_PAIR(RED)); wprintw(win, "Plugin commands:\n"); wattroff(win, A_BOLD | COLOR_PAIR(RED)); draw_handler_help(win); help_draw_bottom_menu(win); box(win, ACS_VLINE, ACS_HLINE); wnoutrefresh(win); } #endif /* PYTHON */ static void help_draw_contacts(ToxWindow *self) { WINDOW *win = self->help->win; wmove(win, 1, 1); wattron(win, A_BOLD | COLOR_PAIR(RED)); wprintw(win, "Friendlist controls:\n"); wattroff(win, A_BOLD | COLOR_PAIR(RED)); wprintw(win, " Up and Down arrows : Scroll through list\n"); wprintw(win, " Right and Left arrows : Switch between friendlist and blocked list\n"); wprintw(win, " Enter : Open a chat window with selected contact\n"); wprintw(win, " Delete : Permanently delete a contact\n"); wprintw(win, " B : Block or unblock a contact\n"); help_draw_bottom_menu(win); box(win, ACS_VLINE, ACS_HLINE); wnoutrefresh(win); } void help_onKey(ToxWindow *self, wint_t key) { int height; switch (key) { case L'x': case T_KEY_ESC: help_exit(self); break; case L'c': #ifdef VIDEO help_init_window(self, 26, 80); #elif AUDIO help_init_window(self, 21, 80); #else help_init_window(self, 11, 80); #endif self->help->type = HELP_CHAT; break; case L'g': height = 22; #ifdef VIDEO height += 8; #elif AUDIO height += 4; #endif #ifdef PYTHON height += 2; #endif #ifdef GAMES height += 1; #endif help_init_window(self, height, 80); self->help->type = HELP_GLOBAL; break; case L'o': height = 6; #ifdef AUDIO height += 7; #endif help_init_window(self, height, 80); self->help->type = HELP_CONFERENCE; break; #ifdef PYTHON case L'p': help_init_window(self, 4 + num_registered_handlers(), help_max_width()); self->help->type = HELP_PLUGIN; break; #endif /* PYTHON */ case L'f': help_init_window(self, 10, 80); self->help->type = HELP_CONTACTS; break; case L'k': help_init_window(self, 15, 80); self->help->type = HELP_KEYS; break; case L'm': help_init_menu(self); self->help->type = HELP_MENU; break; } } void help_onDraw(ToxWindow *self) { switch (self->help->type) { case HELP_MENU: help_draw_menu(self); return; case HELP_CHAT: help_draw_chat(self); break; case HELP_GLOBAL: help_draw_global(self); break; case HELP_KEYS: help_draw_keys(self); break; case HELP_CONTACTS: help_draw_contacts(self); break; case HELP_CONFERENCE: help_draw_conference(self); break; #ifdef PYTHON case HELP_PLUGIN: help_draw_plugin(self); break; #endif /* PYTHON */ } } toxic-0.11.3/src/help.h000066400000000000000000000021521416141666600146250ustar00rootroot00000000000000/* help.h * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef HELP_H #define HELP_H #include "toxic.h" #include "windows.h" typedef enum { HELP_MENU, HELP_GLOBAL, HELP_CHAT, HELP_CONFERENCE, HELP_KEYS, HELP_CONTACTS, #ifdef PYTHON HELP_PLUGIN, #endif } HELP_TYPES; void help_onDraw(ToxWindow *self); void help_init_menu(ToxWindow *self); void help_onKey(ToxWindow *self, wint_t key); #endif /* HELP_H */ toxic-0.11.3/src/input.c000066400000000000000000000215251416141666600150340ustar00rootroot00000000000000/* input.c * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef _GNU_SOURCE #define _GNU_SOURCE /* needed for wcwidth() */ #endif #include #include "conference.h" #include "line_info.h" #include "misc_tools.h" #include "notify.h" #include "settings.h" #include "toxic.h" #include "toxic_strings.h" #include "windows.h" extern struct user_settings *user_settings; /* add a char to input field and buffer */ void input_new_char(ToxWindow *self, wint_t key, int x, int mx_x) { ChatContext *ctx = self->chatwin; /* this is the only place we need to do this check */ if (key == '\n') { key = L'¶'; } int cur_len = wcwidth(key); if (cur_len == -1) { sound_notify(self, notif_error, 0, NULL); return; } if (add_char_to_buf(ctx, key) == -1) { sound_notify(self, notif_error, 0, NULL); return; } if (x + cur_len >= mx_x) { int s_len = wcwidth(ctx->line[ctx->start]); ctx->start += 1 + MAX(0, cur_len - s_len); } } /* delete a char via backspace key from input field and buffer */ static void input_backspace(ToxWindow *self, int x, int mx_x) { ChatContext *ctx = self->chatwin; if (del_char_buf_bck(ctx) == -1) { sound_notify(self, notif_error, 0, NULL); return; } int cur_len = ctx->pos > 0 ? wcwidth(ctx->line[ctx->pos - 1]) : 0; int s_len = ctx->start > 0 ? wcwidth(ctx->line[ctx->start - 1]) : 0; if (ctx->start && (x >= mx_x - cur_len)) { ctx->start = MAX(0, ctx->start - 1 + (s_len - cur_len)); } else if (ctx->start) { ctx->start = MAX(0, ctx->start - cur_len); } } /* delete a char via delete key from input field and buffer */ static void input_delete(ToxWindow *self) { if (del_char_buf_frnt(self->chatwin) == -1) { sound_notify(self, notif_error, 0, NULL); } } /* delete last typed word */ static void input_del_word(ToxWindow *self) { ChatContext *ctx = self->chatwin; if (del_word_buf(ctx) == -1) { sound_notify(self, notif_error, 0, NULL); } } /* deletes entire line before cursor from input field and buffer */ static void input_discard(ToxWindow *self) { if (discard_buf(self->chatwin) == -1) { sound_notify(self, notif_error, 0, NULL); } } /* deletes entire line after cursor from input field and buffer */ static void input_kill(ChatContext *ctx) { if (kill_buf(ctx) == -1) { sound_notify(NULL, notif_error, NT_ALWAYS, NULL); } } static void input_yank(ToxWindow *self, int x, int mx_x) { ChatContext *ctx = self->chatwin; if (yank_buf(ctx) == -1) { sound_notify(self, notif_error, 0, NULL); return; } int yank_cols = MAX(0, wcswidth(ctx->yank, ctx->yank_len)); if (x + yank_cols >= mx_x) { int rmdr = MAX(0, (x + yank_cols) - mx_x); int s_len = MAX(0, wcswidth(&ctx->line[ctx->start], rmdr)); ctx->start += s_len + 1; } } /* moves cursor/line position to end of line in input field and buffer */ static void input_mv_end(ToxWindow *self, int mx_x) { ChatContext *ctx = self->chatwin; ctx->pos = ctx->len; int wlen = MAX(0, wcswidth(ctx->line, sizeof(ctx->line) / sizeof(wchar_t))); ctx->start = MAX(0, 1 + (mx_x * (wlen / mx_x) - mx_x) + (wlen % mx_x)); } /* moves cursor/line position to start of line in input field and buffer */ static void input_mv_home(ToxWindow *self) { ChatContext *ctx = self->chatwin; if (ctx->pos <= 0) { return; } ctx->pos = 0; ctx->start = 0; } /* moves cursor/line position left in input field and buffer */ static void input_mv_left(ToxWindow *self, int x, int mx_x) { ChatContext *ctx = self->chatwin; if (ctx->pos <= 0) { return; } int cur_len = ctx->pos > 0 ? wcwidth(ctx->line[ctx->pos - 1]) : 0; --ctx->pos; if (ctx->start > 0 && (x >= mx_x - cur_len)) { int s_len = wcwidth(ctx->line[ctx->start - 1]); ctx->start = MAX(0, ctx->start - 1 + (s_len - cur_len)); } else if (ctx->start > 0) { ctx->start = MAX(0, ctx->start - cur_len); } } /* moves the cursor to the beginning of the previous word in input field and buffer */ static void input_skip_left(ToxWindow *self, int x, int mx_x) { ChatContext *ctx = self->chatwin; if (ctx->pos <= 0) { return; } int count = 0; do { --ctx->pos; count += wcwidth(ctx->line[ctx->pos]); } while (ctx->pos > 0 && (ctx->line[ctx->pos - 1] != L' ' || ctx->line[ctx->pos] == L' ')); if (ctx->start > 0 && (x >= mx_x - count)) { int s_len = wcwidth(ctx->line[ctx->start - 1]); ctx->start = MAX(0, ctx->start - 1 + (s_len - count)); } else if (ctx->start > 0) { ctx->start = MAX(0, ctx->start - count); } } /* moves cursor/line position right in input field and buffer */ static void input_mv_right(ToxWindow *self, int x, int mx_x) { ChatContext *ctx = self->chatwin; if (ctx->pos >= ctx->len) { return; } ++ctx->pos; int cur_len = wcwidth(ctx->line[ctx->pos - 1]); if (x + cur_len >= mx_x) { int s_len = wcwidth(ctx->line[ctx->start]); ctx->start += 1 + MAX(0, cur_len - s_len); } } /* moves the cursor to the end of the next word in input field and buffer */ static void input_skip_right(ToxWindow *self, int x, int mx_x) { ChatContext *ctx = self->chatwin; if (ctx->pos >= ctx->len) { return; } int count = 0; do { count += wcwidth(ctx->line[ctx->pos]); ++ctx->pos; } while (ctx->pos < ctx->len && !(ctx->line[ctx->pos] == L' ' && ctx->line[ctx->pos - 1] != L' ')); int newpos = x + count; if (newpos >= mx_x) { ctx->start += (1 + (newpos - mx_x)); } } /* puts a line history item in input field and buffer */ static void input_history(ToxWindow *self, wint_t key, int mx_x) { ChatContext *ctx = self->chatwin; fetch_hist_item(ctx, key); int wlen = MAX(0, wcswidth(ctx->line, sizeof(ctx->line) / sizeof(wchar_t))); ctx->start = wlen < mx_x ? 0 : wlen - mx_x + 1; } /* Handles non-printable input keys that behave the same for all types of chat windows. return true if key matches a function, false otherwise */ bool input_handle(ToxWindow *self, wint_t key, int x, int mx_x) { bool match = true; switch (key) { case 0x7f: case KEY_BACKSPACE: input_backspace(self, x, mx_x); break; case KEY_DC: input_delete(self); break; case T_KEY_DISCARD: input_discard(self); break; case T_KEY_KILL: input_kill(self->chatwin); break; case T_KEY_C_Y: input_yank(self, x, mx_x); break; case T_KEY_C_W: input_del_word(self); break; case KEY_HOME: case T_KEY_C_A: input_mv_home(self); break; case KEY_END: case T_KEY_C_E: input_mv_end(self, mx_x); break; case KEY_LEFT: input_mv_left(self, x, mx_x); break; case KEY_RIGHT: input_mv_right(self, x, mx_x); break; case KEY_UP: case KEY_DOWN: input_history(self, key, mx_x); break; case T_KEY_C_L: force_refresh(self->chatwin->history); break; case T_KEY_C_LEFT: input_skip_left(self, x, mx_x); break; case T_KEY_C_RIGHT: input_skip_right(self, x, mx_x); break; default: match = false; break; } /* TODO: this special case is ugly. maybe convert entire function to if/else and make them all customizable keys? */ if (!match) { if (key == user_settings->key_toggle_peerlist) { if (self->type == WINDOW_TYPE_CONFERENCE) { self->show_peerlist ^= 1; redraw_conference_win(self); } match = true; } else if (key == user_settings->key_toggle_pastemode) { self->chatwin->pastemode ^= 1; match = true; } } if (match) { flag_interface_refresh(); } return match; } toxic-0.11.3/src/input.h000066400000000000000000000021771416141666600150430ustar00rootroot00000000000000/* input.h * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef INPUT_H #define INPUT_H /* add a char to input field and buffer for given chatcontext */ void input_new_char(ToxWindow *self, wint_t key, int x, int mx_x); /* Handles non-printable input keys that behave the same for all types of chat windows. return true if key matches a function, false otherwise */ bool input_handle(ToxWindow *self, wint_t key, int x, int mx_x); #endif /* INPUT_H */ toxic-0.11.3/src/line_info.c000066400000000000000000000541211416141666600156350ustar00rootroot00000000000000/* line_info.c * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #include #include #include #include #include "conference.h" #include "line_info.h" #include "message_queue.h" #include "misc_tools.h" #include "notify.h" #include "settings.h" #include "toxic.h" #include "windows.h" extern struct user_settings *user_settings; void line_info_init(struct history *hst) { hst->line_root = calloc(1, sizeof(struct line_info)); if (hst->line_root == NULL) { exit_toxic_err("failed in line_info_init", FATALERR_MEMORY); } hst->line_start = hst->line_root; hst->line_end = hst->line_start; hst->queue_size = 0; } /* resets line_start (moves to end of chat history) */ void line_info_reset_start(ToxWindow *self, struct history *hst) { struct line_info *line = hst->line_end; if (line == NULL || line->prev == NULL) { return; } int y2; int x2; getmaxyx(self->window, y2, x2); UNUSED_VAR(x2); int top_offst = (self->type == WINDOW_TYPE_CHAT) || (self->type == WINDOW_TYPE_PROMPT) ? TOP_BAR_HEIGHT : 0; int max_y = y2 - CHATBOX_HEIGHT - WINDOW_BAR_HEIGHT - top_offst; uint16_t curlines = 0; do { curlines += line->format_lines; line = line->prev; } while (line->prev && curlines + line->format_lines <= max_y); hst->line_start = line; self->scroll_pause = false; } void line_info_cleanup(struct history *hst) { struct line_info *tmp1 = hst->line_root; while (tmp1) { struct line_info *tmp2 = tmp1->next; free(tmp1); tmp1 = tmp2; } for (size_t i = 0; i < hst->queue_size; ++i) { if (hst->queue[i]) { free(hst->queue[i]); } } free(hst); } /* moves root forward and frees previous root */ static void line_info_root_fwd(struct history *hst) { struct line_info *tmp = hst->line_root->next; tmp->prev = NULL; if (hst->line_start->prev == NULL) { /* if line_start is root move it forward as well */ hst->line_start = hst->line_start->next; hst->line_start->prev = NULL; ++hst->start_id; } free(hst->line_root); hst->line_root = tmp; } /* returns ptr to queue item 0 and removes it from queue. Returns NULL if queue is empty. */ static struct line_info *line_info_ret_queue(struct history *hst) { if (hst->queue_size == 0) { return NULL; } struct line_info *line = hst->queue[0]; for (size_t i = 0; i < hst->queue_size; ++i) { hst->queue[i] = hst->queue[i + 1]; } --hst->queue_size; return line; } /* Prints a maximum of `n` chars from `s` to `win`. * * Return 1 if the string contains a newline byte. * Return 0 if string does not contain a newline byte. * Return -1 if printing was aborted. */ static int print_n_chars(WINDOW *win, const char *s, size_t n, int max_y) { bool newline = false; char ch; for (size_t i = 0; i < n && (ch = s[i]); ++i) { if (ch == '\n') { newline = true; int x; int y; getyx(win, y, x); UNUSED_VAR(x); // make sure cursor will wrap correctly after newline to prevent display bugs if (y + 1 >= max_y) { return -1; } } if (win) { wprintw(win, "%c", ch); } } return newline; } /* Returns the index of the last space character in `s` found before `limit`. * Returns -1 if no space is found. */ static int rspace_index(const char *s, int limit) { for (int i = limit; i >= 0; --i) { if (s[i] == ' ') { return i; } } return -1; } /* Returns the first index in `s` containing a newline byte found before `limit`. * Returns -1 if no newline is found. */ static int newline_index(const char *s, int limit) { char ch; for (int i = 0; i < limit && (ch = s[i]); ++i) { if (ch == '\n') { return i; } } return -1; } /* Returns the number of newline bytes in `s` */ static unsigned int newline_count(const char *s) { char ch; unsigned int count = 0; for (size_t i = 0; (ch = s[i]); ++i) { if (ch == '\n') { ++count; } } return count; } /* Prints `line` message to window, wrapping at the last word that fits on the current line. * This function updates the `format_lines` field of `line` according to current window dimensions. * * If `win` is null nothing will be printed to the window. This is useful to set the * `format_lines` field on initialization. * * Return 0 on success. * Return -1 if not all characters in line's message were printed to screen. */ static int print_wrap(WINDOW *win, struct line_info *line, int max_x, int max_y) { int x; int y; UNUSED_VAR(y); const char *msg = line->msg; uint16_t length = line->msg_len; uint16_t lines = 0; const int x_start = line->len - line->msg_len - 1; // manually keep track of x position because ncurses sucks int x_limit = max_x - x_start; if (x_limit <= 1) { fprintf(stderr, "Warning: x_limit <= 0 in print_wrap(): %d\n", x_limit); return -1; } while (msg) { getyx(win, y, x); // next line would print past window limit so we abort; we don't want to update format_lines if (x > x_start) { return -1; } if (length < x_limit) { int p_ret = print_n_chars(win, msg, length, max_y); if (p_ret == 1) { lines += newline_count(msg); } else if (p_ret == -1) { return -1; } ++lines; break; } int newline_idx = newline_index(msg, x_limit - 1); if (newline_idx >= 0) { if (print_n_chars(win, msg, newline_idx + 1, max_y) == -1) { return -1; } msg += (newline_idx + 1); length -= (newline_idx + 1); x_limit = max_x; // if we find a newline we stop adding column padding for rest of message ++lines; continue; } int space_idx = rspace_index(msg, x_limit - 1); if (space_idx >= 1) { if (print_n_chars(win, msg, space_idx, max_y) == -1) { return -1; } msg += space_idx + 1; length -= (space_idx + 1); if (win) { waddch(win, '\n'); } } else { if (print_n_chars(win, msg, x_limit, max_y) == -1) { return -1; } msg += x_limit; length -= x_limit; } // Add padding to the start of the next line if (win && x_limit < max_x) { for (size_t i = 0; i < x_start; ++i) { waddch(win, ' '); } } ++lines; } if (win && line->noread_flag) { getyx(win, y, x); if (x >= max_x - 1 || x == x_start) { ++lines; } wattron(win, COLOR_PAIR(RED)); wprintw(win, " x"); wattroff(win, COLOR_PAIR(RED)); } line->format_lines = lines; return 0; } static void line_info_init_line(ToxWindow *self, struct line_info *line) { int y2; int x2; getmaxyx(self->window, y2, x2); UNUSED_VAR(y2); const int max_y = y2 - CHATBOX_HEIGHT - WINDOW_BAR_HEIGHT; const int max_x = self->show_peerlist ? x2 - 1 - SIDEBAR_WIDTH : x2; print_wrap(NULL, line, max_x, max_y); } /* creates new line_info line and puts it in the queue. * * Returns the id of the new line. * Returns -1 on failure. */ int line_info_add(ToxWindow *self, bool show_timestamp, const char *name1, const char *name2, LINE_TYPE type, uint8_t bold, uint8_t colour, const char *msg, ...) { if (!self) { return -1; } struct history *hst = self->chatwin->hst; if (hst->queue_size >= MAX_LINE_INFO_QUEUE) { return -1; } struct line_info *new_line = calloc(1, sizeof(struct line_info)); if (new_line == NULL) { exit_toxic_err("failed in line_info_add", FATALERR_MEMORY); } char frmt_msg[MAX_LINE_INFO_MSG_SIZE]; frmt_msg[0] = 0; va_list args; va_start(args, msg); vsnprintf(frmt_msg, sizeof(frmt_msg), msg, args); va_end(args); int len = 1; /* there will always be a newline */ /* for type-specific formatting in print function */ switch (type) { case IN_ACTION: /* fallthrough */ case OUT_ACTION: len += strlen(user_settings->line_normal) + 2; // two spaces break; case IN_MSG: /* fallthrough */ case OUT_MSG: len += strlen(user_settings->line_normal) + 3; // two spaces and a ':' char break; case CONNECTION: len += strlen(user_settings->line_join) + 2; // two spaces break; case DISCONNECTION: len += strlen(user_settings->line_quit) + 2; // two spaces break; case SYS_MSG: break; case NAME_CHANGE: len += strlen(user_settings->line_alert) + 2; // two spaces break; case PROMPT: len += 2; // '$' char and a space break; default: len += 2; break; } uint16_t msg_len = 0; if (frmt_msg[0]) { snprintf(new_line->msg, sizeof(new_line->msg), "%s", frmt_msg); msg_len = strlen(new_line->msg); len += msg_len; } if (show_timestamp) { get_time_str(new_line->timestr, sizeof(new_line->timestr)); len += strlen(new_line->timestr) + 1; } if (name1) { snprintf(new_line->name1, sizeof(new_line->name1), "%s", name1); len += strlen(new_line->name1); } if (name2) { snprintf(new_line->name2, sizeof(new_line->name2), "%s", name2); len += strlen(new_line->name2); } new_line->id = (hst->line_end->id + 1 + hst->queue_size) % INT_MAX; new_line->len = len; new_line->msg_len = msg_len; new_line->type = type; new_line->bold = bold; new_line->colour = colour; new_line->noread_flag = false; new_line->timestamp = get_unix_time(); if (type == OUT_MSG || type == OUT_ACTION) { new_line->noread_flag = self->stb->connection == TOX_CONNECTION_NONE; } line_info_init_line(self, new_line); hst->queue[hst->queue_size++] = new_line; return new_line->id; } /* adds a single queue item to hst if possible. only called once per call to line_info_print() */ static void line_info_check_queue(ToxWindow *self) { struct history *hst = self->chatwin->hst; struct line_info *line = line_info_ret_queue(hst); if (line == NULL) { return; } if (hst->start_id > user_settings->history_size) { line_info_root_fwd(hst); } line->prev = hst->line_end; hst->line_end->next = line; hst->line_end = line; hst->line_end->id = line->id; if (!self->scroll_pause) { line_info_reset_start(self, hst); } } void line_info_print(ToxWindow *self) { ChatContext *ctx = self->chatwin; if (ctx == NULL) { return; } struct history *hst = ctx->hst; /* Only allow one new item to be added to chat window per call to this function */ line_info_check_queue(self); WINDOW *win = ctx->history; wclear(win); int y2; int x2; getmaxyx(self->window, y2, x2); if (x2 - 1 <= SIDEBAR_WIDTH) { // leave room on x axis for sidebar padding return; } if (self->type == WINDOW_TYPE_CONFERENCE) { wmove(win, 0, 0); } else { wmove(win, TOP_BAR_HEIGHT, 0); } struct line_info *line = hst->line_start->next; if (!line) { return; } const int max_y = y2 - CHATBOX_HEIGHT - WINDOW_BAR_HEIGHT; const int max_x = self->show_peerlist ? x2 - 1 - SIDEBAR_WIDTH : x2; uint16_t numlines = line->format_lines; int print_ret = 0; while (line && numlines++ <= max_y && print_ret == 0) { int y; int x; getyx(win, y, x); UNUSED_VAR(y); if (x > 0) { // Prevents us from printing off the screen break; } uint8_t type = line->type; switch (type) { case OUT_MSG: /* fallthrough */ case OUT_MSG_READ: /* fallthrough */ case IN_MSG: wattron(win, COLOR_PAIR(BLUE)); wprintw(win, "%s ", line->timestr); wattroff(win, COLOR_PAIR(BLUE)); int nameclr = GREEN; if (line->colour) { nameclr = line->colour; } else if (type == IN_MSG) { nameclr = CYAN; } wattron(win, COLOR_PAIR(nameclr)); wprintw(win, "%s %s: ", user_settings->line_normal, line->name1); wattroff(win, COLOR_PAIR(nameclr)); if (line->msg[0] == 0) { waddch(win, '\n'); break; } if (line->msg[0] == '>') { wattron(win, COLOR_PAIR(GREEN)); } else if (line->msg[0] == '<') { wattron(win, COLOR_PAIR(RED)); } print_ret = print_wrap(win, line, max_x, max_y); if (line->msg[0] == '>') { wattroff(win, COLOR_PAIR(GREEN)); } else if (line->msg[0] == '<') { wattroff(win, COLOR_PAIR(RED)); } waddch(win, '\n'); break; case OUT_ACTION_READ: /* fallthrough */ case OUT_ACTION: /* fallthrough */ case IN_ACTION: wattron(win, COLOR_PAIR(BLUE)); wprintw(win, "%s ", line->timestr); wattroff(win, COLOR_PAIR(BLUE)); wattron(win, COLOR_PAIR(YELLOW)); wprintw(win, "%s %s ", user_settings->line_normal, line->name1); print_ret = print_wrap(win, line, max_x, max_y); wattroff(win, COLOR_PAIR(YELLOW)); waddch(win, '\n'); break; case SYS_MSG: if (line->timestr[0]) { wattron(win, COLOR_PAIR(BLUE)); wprintw(win, "%s ", line->timestr); wattroff(win, COLOR_PAIR(BLUE)); } if (line->bold) { wattron(win, A_BOLD); } if (line->colour) { wattron(win, COLOR_PAIR(line->colour)); } print_ret = print_wrap(win, line, max_x, max_y); waddch(win, '\n'); if (line->bold) { wattroff(win, A_BOLD); } if (line->colour) { wattroff(win, COLOR_PAIR(line->colour)); } break; case PROMPT: wattron(win, COLOR_PAIR(GREEN)); wprintw(win, "$ "); wattroff(win, COLOR_PAIR(GREEN)); if (line->msg[0]) { print_ret = print_wrap(win, line, max_x, max_y); } waddch(win, '\n'); break; case CONNECTION: wattron(win, COLOR_PAIR(BLUE)); wprintw(win, "%s ", line->timestr); wattroff(win, COLOR_PAIR(BLUE)); wattron(win, COLOR_PAIR(line->colour)); wprintw(win, "%s ", user_settings->line_join); wattron(win, A_BOLD); wprintw(win, "%s ", line->name1); wattroff(win, A_BOLD); print_ret = print_wrap(win, line, max_x, max_y); waddch(win, '\n'); wattroff(win, COLOR_PAIR(line->colour)); break; case DISCONNECTION: wattron(win, COLOR_PAIR(BLUE)); wprintw(win, "%s ", line->timestr); wattroff(win, COLOR_PAIR(BLUE)); wattron(win, COLOR_PAIR(line->colour)); wprintw(win, "%s ", user_settings->line_quit); wattron(win, A_BOLD); wprintw(win, "%s ", line->name1); wattroff(win, A_BOLD); print_ret = print_wrap(win, line, max_x, max_y); waddch(win, '\n'); wattroff(win, COLOR_PAIR(line->colour)); break; case NAME_CHANGE: wattron(win, COLOR_PAIR(BLUE)); wprintw(win, "%s ", line->timestr); wattroff(win, COLOR_PAIR(BLUE)); wattron(win, COLOR_PAIR(MAGENTA)); wprintw(win, "%s ", user_settings->line_alert); wattron(win, A_BOLD); wprintw(win, "%s", line->name1); wattroff(win, A_BOLD); print_ret = print_wrap(win, line, max_x, max_y); wattron(win, A_BOLD); wprintw(win, "%s\n", line->name2); wattroff(win, A_BOLD); wattroff(win, COLOR_PAIR(MAGENTA)); break; } line = line->next; } flag_interface_refresh(); /* keep calling until queue is empty */ if (hst->queue_size > 0) { line_info_print(self); } } /* * Return true if all lines starting from `line` can fit on the screen. */ static bool line_info_screen_fit(ToxWindow *self, struct line_info *line) { if (!line) { return true; } int x2; int y2; getmaxyx(self->chatwin->history, y2, x2); UNUSED_VAR(x2); const int top_offset = (self->type == WINDOW_TYPE_CHAT) || (self->type == WINDOW_TYPE_PROMPT) ? TOP_BAR_HEIGHT : 0; const int max_y = y2 - top_offset; uint16_t lines = line->format_lines; while (line) { if (lines > max_y) { return false; } lines += line->format_lines; line = line->next; } return true; } /* puts msg in specified line_info msg buffer */ void line_info_set(ToxWindow *self, uint32_t id, char *msg) { flag_interface_refresh(); struct line_info *line = self->chatwin->hst->line_end; while (line) { if (line->id == id) { size_t new_len = strlen(msg); line->len = line->len - line->msg_len + new_len; line->msg_len = new_len; snprintf(line->msg, sizeof(line->msg), "%s", msg); return; } line = line->prev; } } /* Return the line_info object associated with `id`. * Return NULL if id cannot be found */ struct line_info *line_info_get(ToxWindow *self, uint32_t id) { struct line_info *line = self->chatwin->hst->line_end; while (line) { if (line->id == id) { return line; } line = line->prev; } return NULL; } static void line_info_scroll_up(ToxWindow *self, struct history *hst) { if (hst->line_start->prev) { hst->line_start = hst->line_start->prev; self->scroll_pause = true; } } static void line_info_scroll_down(ToxWindow *self, struct history *hst) { struct line_info *next = hst->line_start->next; if (next && self->scroll_pause) { if (line_info_screen_fit(self, next->next)) { line_info_reset_start(self, hst); } else { hst->line_start = next; } } else { line_info_reset_start(self, hst); } } static void line_info_page_up(ToxWindow *self, struct history *hst) { int x2; int y2; getmaxyx(self->window, y2, x2); UNUSED_VAR(x2); const int top_offset = (self->type == WINDOW_TYPE_CHAT) || (self->type == WINDOW_TYPE_PROMPT) ? TOP_BAR_HEIGHT : 0; const int max_y = y2 - top_offset; size_t jump_dist = max_y / 2; for (size_t i = 0; i < jump_dist && hst->line_start->prev; ++i) { hst->line_start = hst->line_start->prev; } self->scroll_pause = true; } static void line_info_page_down(ToxWindow *self, struct history *hst) { if (!self->scroll_pause) { return; } int x2; int y2; getmaxyx(self->chatwin->history, y2, x2); UNUSED_VAR(x2); const int top_offset = (self->type == WINDOW_TYPE_CHAT) || (self->type == WINDOW_TYPE_PROMPT) ? TOP_BAR_HEIGHT : 0; const int max_y = y2 - top_offset; size_t jump_dist = max_y / 2; struct line_info *next = hst->line_start->next; for (size_t i = 0; i < jump_dist && next; ++i) { if (line_info_screen_fit(self, next->next)) { line_info_reset_start(self, hst); break; } hst->line_start = next; next = hst->line_start->next; } } bool line_info_onKey(ToxWindow *self, wint_t key) { struct history *hst = self->chatwin->hst; bool match = true; if (key == user_settings->key_half_page_up) { line_info_page_up(self, hst); } else if (key == user_settings->key_half_page_down) { line_info_page_down(self, hst); } else if (key == user_settings->key_scroll_line_up) { line_info_scroll_up(self, hst); } else if (key == user_settings->key_scroll_line_down) { line_info_scroll_down(self, hst); } else if (key == user_settings->key_page_bottom) { line_info_reset_start(self, hst); } else { match = false; } if (match) { flag_interface_refresh(); } return match; } void line_info_clear(struct history *hst) { hst->line_start = hst->line_end; hst->start_id = hst->line_start->id; } toxic-0.11.3/src/line_info.h000066400000000000000000000070171416141666600156440ustar00rootroot00000000000000/* line_info.h * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef LINE_INFO_H #define LINE_INFO_H #include "toxic.h" #include "windows.h" #define MAX_HISTORY 100000 #define MIN_HISTORY 40 #define MAX_LINE_INFO_QUEUE 1024 #define MAX_LINE_INFO_MSG_SIZE (MAX_STR_SIZE + TOXIC_MAX_NAME_LENGTH + 32) /* needs extra room for log loading */ typedef enum LINE_TYPE { SYS_MSG, IN_MSG, OUT_MSG, OUT_MSG_READ, /* for sent messages that have received a read reply. don't set this with line_info_add */ IN_ACTION, OUT_ACTION, OUT_ACTION_READ, /* same as OUT_MSG_READ but for actions */ PROMPT, CONNECTION, DISCONNECTION, NAME_CHANGE, } LINE_TYPE; struct line_info { char timestr[TIME_STR_SIZE]; char name1[TOXIC_MAX_NAME_LENGTH + 1]; char name2[TOXIC_MAX_NAME_LENGTH + 1]; char msg[MAX_LINE_INFO_MSG_SIZE]; time_t timestamp; uint8_t type; uint8_t bold; uint8_t colour; bool noread_flag; /* true if a line should be flagged as unread */ bool read_flag; /* true if a message has been flagged as read */ uint32_t id; uint16_t len; /* combined length of entire line */ uint16_t msg_len; /* length of the message */ uint16_t format_lines; /* number of lines the combined string takes up (dynamically set) */ struct line_info *prev; struct line_info *next; }; /* Linked list containing chat history lines */ struct history { struct line_info *line_root; struct line_info *line_start; /* the first line we want to start printing at */ struct line_info *line_end; uint32_t start_id; /* keeps track of where line_start should be when at bottom of history */ struct line_info *queue[MAX_LINE_INFO_QUEUE]; size_t queue_size; }; /* creates new line_info line and puts it in the queue. * * Returns the id of the new line. * Returns -1 on failure. */ int line_info_add(ToxWindow *self, bool show_timestamp, const char *name1, const char *name2, LINE_TYPE type, uint8_t bold, uint8_t colour, const char *msg, ...); /* Prints a section of history starting at line_start */ void line_info_print(ToxWindow *self); /* frees all history lines */ void line_info_cleanup(struct history *hst); /* clears the screen (does not delete anything) */ void line_info_clear(struct history *hst); /* puts msg in specified line_info msg buffer */ void line_info_set(ToxWindow *self, uint32_t id, char *msg); /* Return the line_info object associated with `id`. * Return NULL if id cannot be found */ struct line_info *line_info_get(ToxWindow *self, uint32_t id); /* resets line_start (moves to end of chat history) */ void line_info_reset_start(ToxWindow *self, struct history *hst); void line_info_init(struct history *hst); bool line_info_onKey(ToxWindow *self, wint_t key); /* returns true if key is a match */ #endif /* LINE_INFO_H */ toxic-0.11.3/src/log.c000066400000000000000000000217531416141666600144610ustar00rootroot00000000000000/* log.c * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #include #include #include #include #include "configdir.h" #include "line_info.h" #include "log.h" #include "misc_tools.h" #include "settings.h" #include "toxic.h" #include "windows.h" extern struct user_settings *user_settings; /* Creates a log path and puts it in `dest. * * There are two types of logs: chat logs and prompt logs (see LOG_TYPE in log.h) * A prompt log is in the format: LOGDIR/selfkey-home.log * A chat log is in the format: LOGDIR/selfkey-name-otherkey.log * * For friend chats `otherkey` is the first 6 bytes of the friend's Tox ID. * For Conferences/groups `otherkey` is the first 6 bytes of the group's unique ID. * * Return path length on success. * Return -1 if the path is too long. */ static int get_log_path(char *dest, int destsize, const char *name, const char *selfkey, const char *otherkey) { if (!valid_nick(name)) { name = UNKNOWN_NAME; } const char *namedash = otherkey ? "-" : ""; const char *set_path = user_settings->chatlogs_path; char *user_config_dir = get_user_config_dir(); int path_len = strlen(name) + strlen(".log") + strlen("-") + strlen(namedash); path_len += strlen(set_path) ? *set_path : strlen(user_config_dir) + strlen(LOGDIR); /* first 6 bytes of selfkey */ char self_id[32] = {0}; path_len += KEY_IDENT_DIGITS * 2; sprintf(&self_id[0], "%02X", selfkey[0] & 0xff); sprintf(&self_id[2], "%02X", selfkey[1] & 0xff); sprintf(&self_id[4], "%02X", selfkey[2] & 0xff); self_id[KEY_IDENT_DIGITS * 2] = '\0'; char other_id[32] = {0}; if (otherkey) { /* first 6 bytes of otherkey */ path_len += KEY_IDENT_DIGITS * 2; sprintf(&other_id[0], "%02X", otherkey[0] & 0xff); sprintf(&other_id[2], "%02X", otherkey[1] & 0xff); sprintf(&other_id[4], "%02X", otherkey[2] & 0xff); other_id[KEY_IDENT_DIGITS * 2] = '\0'; } if (path_len >= destsize) { free(user_config_dir); return -1; } if (!string_is_empty(set_path)) { snprintf(dest, destsize, "%s%s-%s%s%s.log", set_path, self_id, name, namedash, other_id); } else { snprintf(dest, destsize, "%s%s%s-%s%s%s.log", user_config_dir, LOGDIR, self_id, name, namedash, other_id); } free(user_config_dir); return path_len; } /* Initializes log path for `log`. * * Return 0 on success. * Return -1 on failure. */ static int init_logging_session(const char *name, const char *selfkey, const char *otherkey, struct chatlog *log, LOG_TYPE type) { if (log == NULL) { return -1; } if (selfkey == NULL || (type == LOG_TYPE_CHAT && otherkey == NULL)) { return -1; } char log_path[MAX_STR_SIZE]; int path_len = get_log_path(log_path, sizeof(log_path), name, selfkey, otherkey); if (path_len == -1 || path_len >= sizeof(log->path)) { return -1; } memcpy(log->path, log_path, path_len); log->path[path_len] = 0; return 0; } #define LOG_FLUSH_LIMIT 1 /* limits calls to fflush to a max of one per LOG_FLUSH_LIMIT seconds */ void write_to_log(const char *msg, const char *name, struct chatlog *log, bool event) { if (log == NULL) { return; } if (!log->log_on) { return; } if (log->file == NULL) { log->log_on = false; return; } char name_frmt[TOXIC_MAX_NAME_LENGTH + 3]; if (event) { snprintf(name_frmt, sizeof(name_frmt), "* %s", name); } else { snprintf(name_frmt, sizeof(name_frmt), "%s:", name); } const char *t = user_settings->log_timestamp_format; char s[MAX_STR_SIZE]; strftime(s, MAX_STR_SIZE, t, get_time()); fprintf(log->file, "%s %s %s\n", s, name_frmt, msg); if (timed_out(log->lastwrite, LOG_FLUSH_LIMIT)) { fflush(log->file); log->lastwrite = get_unix_time(); } } void log_disable(struct chatlog *log) { if (log == NULL) { return; } if (log->file != NULL) { fclose(log->file); log->file = NULL; } log->lastwrite = 0; log->log_on = false; } int log_enable(struct chatlog *log) { if (log == NULL) { return -1; } if (log->log_on) { return 0; } if (*log->path == 0) { return -1; } if (log->file != NULL) { return -1; } log->file = fopen(log->path, "a+"); if (log->file == NULL) { return -1; } log->log_on = true; return 0; } /* Initializes a log. This function must be called before any other logging operations. * * Return 0 on success. * Return -1 on failure. */ int log_init(struct chatlog *log, const char *name, const char *selfkey, const char *otherkey, LOG_TYPE type) { if (log == NULL) { return -1; } if (log->file != NULL || log->log_on) { fprintf(stderr, "Warning: Called log_init() on an already initialized log\n"); return -1; } if (init_logging_session(name, selfkey, otherkey, log, type) == -1) { return -1; } log_disable(log); return 0; } /* Loads chat log history and prints it to `self` window. * * Return 0 on success or if log file doesn't exist. * Return -1 on failure. */ int load_chat_history(ToxWindow *self, struct chatlog *log) { if (log == NULL) { return -1; } if (*log->path == 0) { return -1; } off_t sz = file_size(log->path); if (sz <= 0) { return 0; } FILE *fp = fopen(log->path, "r"); if (fp == NULL) { return -1; } char *buf = malloc(sz + 1); if (buf == NULL) { fclose(fp); return -1; } if (fseek(fp, 0L, SEEK_SET) == -1) { free(buf); fclose(fp); return -1; } if (fread(buf, sz, 1, fp) != 1) { free(buf); fclose(fp); return -1; } fclose(fp); buf[sz] = 0; /* Number of history lines to load: must not be larger than MAX_LINE_INFO_QUEUE - 2 */ int L = MIN(MAX_LINE_INFO_QUEUE - 2, user_settings->history_size); int start = 0; int count = 0; /* start at end and backtrace L lines or to the beginning of buffer */ for (start = sz - 1; start >= 0 && count < L; --start) { if (buf[start] == '\n') { ++count; } } char *tmp = NULL; const char *line = strtok_r(&buf[start + 1], "\n", &tmp); if (line == NULL) { free(buf); return -1; } while (line != NULL && count--) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "%s", line); line = strtok_r(NULL, "\n", &tmp); } line_info_add(self, false, NULL, NULL, SYS_MSG, 0, YELLOW, "---"); free(buf); return 0; } /* Renames chatlog file `src` to `dest`. * * Return 0 on success or if no log exists. * Return -1 on failure. */ int rename_logfile(const char *src, const char *dest, const char *selfkey, const char *otherkey, int winnum) { ToxWindow *toxwin = get_window_ptr(winnum); struct chatlog *log = NULL; bool log_on = false; /* disable log if necessary and save its state */ if (toxwin != NULL) { log = toxwin->chatwin->log; if (log == NULL) { return -1; } log_on = log->log_on; } if (log_on) { log_disable(log); } char newpath[MAX_STR_SIZE]; char oldpath[MAX_STR_SIZE]; if (get_log_path(oldpath, sizeof(oldpath), src, selfkey, otherkey) == -1) { goto on_error; } if (!file_exists(oldpath)) { init_logging_session(dest, selfkey, otherkey, log, LOG_TYPE_CHAT); // still need to rename path return 0; } int new_path_len = get_log_path(newpath, sizeof(newpath), dest, selfkey, otherkey); if (new_path_len == -1 || new_path_len >= MAX_STR_SIZE) { goto on_error; } if (file_exists(newpath)) { if (remove(oldpath) != 0) { fprintf(stderr, "Warning: remove() failed to remove log path `%s`\n", oldpath); } } else if (rename(oldpath, newpath) != 0) { goto on_error; } if (log != NULL) { memcpy(log->path, newpath, new_path_len); log->path[new_path_len] = 0; if (log_on) { log_enable(log); } } return 0; on_error: if (log_on) { log_enable(log); } return -1; } toxic-0.11.3/src/log.h000066400000000000000000000040201416141666600144520ustar00rootroot00000000000000/* log.h * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef LOG_H #define LOG_H struct chatlog { FILE *file; time_t lastwrite; char path[MAX_STR_SIZE]; bool log_on; /* specific to current chat window */ }; typedef enum LOG_TYPE { LOG_TYPE_PROMPT, LOG_TYPE_CHAT, } LOG_TYPE; /* Initializes a log. This function must be called before any other logging operations. * * Return 0 on success. * Return -1 on failure. */ int log_init(struct chatlog *log, const char *name, const char *selfkey, const char *otherkey, LOG_TYPE type); /* formats/writes line to log file */ void write_to_log(const char *msg, const char *name, struct chatlog *log, bool event); /* enables logging for specified log. * * Returns 0 on success. * Returns -1 on failure. */ int log_enable(struct chatlog *log); /* disables logging for specified log and closes file */ void log_disable(struct chatlog *log); /* Loads chat log history and prints it to `self` window. * * Return 0 on success or if log file doesn't exist. * Return -1 on failure. */ int load_chat_history(ToxWindow *self, struct chatlog *log); /* Renames chatlog file `src` to `dest`. * * Return 0 on success or if no log exists. * Return -1 on failure. */ int rename_logfile(const char *src, const char *dest, const char *selfkey, const char *otherkey, int winnum); #endif /* LOG_H */ toxic-0.11.3/src/message_queue.c000066400000000000000000000134531416141666600165260ustar00rootroot00000000000000/* message_queue.c * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #include #include "line_info.h" #include "log.h" #include "message_queue.h" #include "misc_tools.h" #include "toxic.h" #include "windows.h" void cqueue_cleanup(struct chat_queue *q) { struct cqueue_msg *tmp1 = q->root; while (tmp1) { struct cqueue_msg *tmp2 = tmp1->next; free(tmp1); tmp1 = tmp2; } free(q); } void cqueue_add(struct chat_queue *q, const char *msg, size_t len, uint8_t type, int line_id) { if (line_id < 0) { return; } struct cqueue_msg *new_m = malloc(sizeof(struct cqueue_msg)); if (new_m == NULL) { exit_toxic_err("failed in cqueue_message", FATALERR_MEMORY); } snprintf(new_m->message, sizeof(new_m->message), "%s", msg); new_m->len = len; new_m->type = type; new_m->line_id = line_id; new_m->last_send_try = 0; new_m->time_added = get_unix_time(); new_m->receipt = -1; new_m->next = NULL; if (q->root == NULL) { new_m->prev = NULL; q->root = new_m; } else { new_m->prev = q->end; q->end->next = new_m; } q->end = new_m; } /* update line to show receipt was received after queue removal */ static void cqueue_mark_read(ToxWindow *self, struct cqueue_msg *msg) { struct line_info *line = line_info_get(self, msg->line_id); if (line == NULL) { return; } line->type = msg->type == OUT_ACTION ? OUT_ACTION_READ : OUT_MSG_READ; if (line->noread_flag) { line->noread_flag = false; line->read_flag = true; flag_interface_refresh(); } } /* removes message with matching receipt from queue, writes to log and updates line to show the message was received. */ void cqueue_remove(ToxWindow *self, Tox *m, uint32_t receipt) { struct chatlog *log = self->chatwin->log; struct chat_queue *q = self->chatwin->cqueue; struct cqueue_msg *msg = q->root; while (msg) { if (msg->receipt != receipt) { msg = msg->next; continue; } if (log->log_on) { char selfname[TOX_MAX_NAME_LENGTH]; tox_self_get_name(m, (uint8_t *) selfname); size_t len = tox_self_get_name_size(m); selfname[len] = 0; write_to_log(msg->message, selfname, log, msg->type == OUT_ACTION); } cqueue_mark_read(self, msg); struct cqueue_msg *next = msg->next; if (msg->prev == NULL) { /* root */ if (next) { next->prev = NULL; } q->root = next; } else { struct cqueue_msg *prev = msg->prev; prev->next = next; next->prev = prev; } free(msg); return; } } // We use knowledge of toxcore internals (bad!) to determine that if we haven't received a read receipt for a // sent packet after this amount of time, the connection has been severed and the packet needs to be re-sent. #define TRY_SEND_TIMEOUT 32 /* * Marks all timed out messages in queue as unsent. */ static void cqueue_check_timeouts(struct cqueue_msg *msg) { while (msg) { if (timed_out(msg->last_send_try, TRY_SEND_TIMEOUT)) { msg->receipt = -1; } msg = msg->next; } } /* * Sets the noread flag for messages sent to the peer associated with `self` which have not * received a receipt after a period of time. */ #define NOREAD_TIMEOUT 5 void cqueue_check_unread(ToxWindow *self) { struct chat_queue *q = self->chatwin->cqueue; struct cqueue_msg *msg = q->root; while (msg) { if (msg->noread_flag) { msg = msg->next; continue; } struct line_info *line = line_info_get(self, msg->line_id); if (line != NULL) { if (timed_out(msg->time_added, NOREAD_TIMEOUT)) { line->noread_flag = true; msg->noread_flag = true; flag_interface_refresh(); } } msg = msg->next; } } /* * Tries to send all messages in the send queue in sequential order. * If a message fails to send the function will immediately return. */ void cqueue_try_send(ToxWindow *self, Tox *m) { struct chat_queue *q = self->chatwin->cqueue; struct cqueue_msg *msg = q->root; while (msg) { if (msg->receipt != -1) { // we can no longer try to send unsent messages until we get receipts for our previous sent // messages, but we continue to iterate the list, checking timestamps for any further // successfully sent messages that have not yet gotten a receipt. cqueue_check_timeouts(msg); return; } TOX_ERR_FRIEND_SEND_MESSAGE err; Tox_Message_Type type = msg->type == OUT_MSG ? TOX_MESSAGE_TYPE_NORMAL : TOX_MESSAGE_TYPE_ACTION; uint32_t receipt = tox_friend_send_message(m, self->num, type, (uint8_t *) msg->message, msg->len, &err); if (err != TOX_ERR_FRIEND_SEND_MESSAGE_OK) { return; } msg->receipt = receipt; msg->last_send_try = get_unix_time(); msg = msg->next; } } toxic-0.11.3/src/message_queue.h000066400000000000000000000035261416141666600165330ustar00rootroot00000000000000/* message_queue.h * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef MESSAGE_QUEUE_H #define MESSAGE_QUEUE_H struct cqueue_msg { char message[MAX_STR_SIZE]; size_t len; int line_id; time_t last_send_try; time_t time_added; uint8_t type; int64_t receipt; bool noread_flag; struct cqueue_msg *next; struct cqueue_msg *prev; }; struct chat_queue { struct cqueue_msg *root; struct cqueue_msg *end; }; void cqueue_cleanup(struct chat_queue *q); void cqueue_add(struct chat_queue *q, const char *msg, size_t len, uint8_t type, int line_id); /* * Tries to send all messages in the send queue in sequential order. * If a message fails to send the function will immediately return. */ void cqueue_try_send(ToxWindow *self, Tox *m); /* * Sets the noread flag for messages sent to the peer associated with `self` which have not * received a receipt after a period of time. */ void cqueue_check_unread(ToxWindow *self); /* removes message with matching receipt from queue, writes to log and updates line to show the message was received. */ void cqueue_remove(ToxWindow *self, Tox *m, uint32_t receipt); #endif /* MESSAGE_QUEUE_H */ toxic-0.11.3/src/misc_tools.c000066400000000000000000000362731416141666600160560ustar00rootroot00000000000000/* misc_tools.c * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #include #include #include #include #include #include #include #include "file_transfers.h" #include "misc_tools.h" #include "settings.h" #include "toxic.h" #include "windows.h" extern ToxWindow *prompt; extern struct user_settings *user_settings; void clear_screen(void) { printf("\033[2J\033[1;1H"); } void hst_to_net(uint8_t *num, uint16_t numbytes) { #ifndef WORDS_BIGENDIAN uint8_t *buff = malloc(numbytes); if (buff == NULL) { return; } for (uint32_t i = 0; i < numbytes; ++i) { buff[i] = num[numbytes - i - 1]; } memcpy(num, buff, numbytes); free(buff); #endif } time_t get_unix_time(void) { return time(NULL); } /* Returns 1 if connection has timed out, 0 otherwise */ int timed_out(time_t timestamp, time_t timeout) { return timestamp + timeout <= get_unix_time(); } /* Attempts to sleep the caller's thread for `usec` microseconds */ void sleep_thread(long int usec) { struct timespec req; struct timespec rem; req.tv_sec = 0; req.tv_nsec = usec * 1000L; if (nanosleep(&req, &rem) == -1) { if (nanosleep(&rem, NULL) == -1) { fprintf(stderr, "nanosleep() returned -1\n"); } } } /* Get the current local time */ struct tm *get_time(void) { struct tm *timeinfo; time_t t = get_unix_time(); timeinfo = localtime((const time_t *) &t); return timeinfo; } /* Puts the current time in buf in the format of specified by the config */ void get_time_str(char *buf, size_t bufsize) { if (buf == NULL || bufsize == 0) { return; } *buf = 0; if (user_settings->timestamps == TIMESTAMPS_OFF) { return; } const char *t = user_settings->timestamp_format; if (strftime(buf, bufsize, t, get_time()) == 0) { strftime(buf, bufsize, TIMESTAMP_DEFAULT, get_time()); } } /* Converts seconds to string in format HH:mm:ss; truncates hours and minutes when necessary */ void get_elapsed_time_str(char *buf, int bufsize, time_t secs) { if (!secs) { return; } long int seconds = secs % 60; long int minutes = (secs % 3600) / 60; long int hours = secs / 3600; if (!minutes && !hours) { snprintf(buf, bufsize, "%.2ld", seconds); } else if (!hours) { snprintf(buf, bufsize, "%ld:%.2ld", minutes, seconds); } else { snprintf(buf, bufsize, "%ld:%.2ld:%.2ld", hours, minutes, seconds); } } /* * Converts a hexidecimal string representation of a Tox public key to binary format and puts * the result in output. * * `hex_len` must be exactly TOX_PUBLIC_KEY_SIZE * 2, and `output_size` must have room * for TOX_PUBLIC_KEY_SIZE bytes. * * Returns 0 on success. * Returns -1 on failure. */ int tox_pk_string_to_bytes(const char *hex_string, size_t hex_len, char *output, size_t output_size) { if (output_size != TOX_PUBLIC_KEY_SIZE || hex_len != output_size * 2) { return -1; } for (size_t i = 0; i < output_size; ++i) { sscanf(hex_string, "%2hhx", (unsigned char *)&output[i]); hex_string += 2; } return 0; } /* Convert a hexadecimcal string of length `size` to bytes and puts the result in `keystr`. * * Returns 0 on success. * Returns -1 on failure. */ int hex_string_to_bytes(char *buf, int size, const char *keystr) { if (size % 2 != 0) { return -1; } const char *pos = keystr; for (size_t i = 0; i < size; ++i) { int res = sscanf(pos, "%2hhx", (unsigned char *)&buf[i]); pos += 2; if (res == EOF || res < 1) { return -1; } } return 0; } /* Converts a binary representation of a Tox ID into a string. * * `bin_id_size` must be exactly TOX_ADDRESS_SIZE bytes in length, and * `output_size` must be at least TOX_ADDRESS_SIZE * 2 + 1. * * Returns 0 on success. * Returns -1 on failure. */ int tox_id_bytes_to_str(const char *bin_id, size_t bin_id_size, char *output, size_t output_size) { if (bin_id_size != TOX_ADDRESS_SIZE || output_size < (TOX_ADDRESS_SIZE * 2 + 1)) { return -1; } for (size_t i = 0; i < TOX_ADDRESS_SIZE; ++i) { snprintf(&output[i * 2], output_size - (i * 2), "%02X", bin_id[i] & 0xff); } return 0; } /* Converts a binary representation of a Tox public key into a string. * * `bin_pubkey_size` must be exactly TOX_PUBLIC_KEY_SIZE bytes in size, and * `output_size` must be at least TOX_PUBLIC_KEY_SIZE * 2 + 1. * * Returns 0 on success. * Returns -1 on failure. */ int tox_pk_bytes_to_str(const uint8_t *bin_pubkey, size_t bin_pubkey_size, char *output, size_t output_size) { if (bin_pubkey_size != TOX_PUBLIC_KEY_SIZE || output_size < (TOX_PUBLIC_KEY_SIZE * 2 + 1)) { return -1; } for (size_t i = 0; i < TOX_PUBLIC_KEY_SIZE; ++i) { snprintf(&output[i * 2], output_size - (i * 2), "%02X", bin_pubkey[i] & 0xff); } return 0; } /* Returns 1 if the string is empty, 0 otherwise */ int string_is_empty(const char *string) { if (!string) { return true; } return string[0] == '\0'; } /* Returns 1 if the string is empty, 0 otherwise */ int wstring_is_empty(const wchar_t *string) { if (!string) { return true; } return string[0] == L'\0'; } /* convert a multibyte string to a wide character string and puts in buf. */ int mbs_to_wcs_buf(wchar_t *buf, const char *string, size_t n) { size_t len = mbstowcs(NULL, string, 0) + 1; if (n < len) { return -1; } if ((len = mbstowcs(buf, string, n)) == (size_t) - 1) { return -1; } return len; } /* converts wide character string into a multibyte string and puts in buf. */ int wcs_to_mbs_buf(char *buf, const wchar_t *string, size_t n) { size_t len = wcstombs(NULL, string, 0) + 1; if (n < len) { return -1; } if ((len = wcstombs(buf, string, n)) == (size_t) - 1) { return -1; } return len; } /* case-insensitive string compare function for use with qsort */ int qsort_strcasecmp_hlpr(const void *str1, const void *str2) { return strcasecmp((const char *) str1, (const char *) str2); } /* case-insensitive string compare function for use with qsort */ int qsort_ptr_char_array_helper(const void *str1, const void *str2) { return strcasecmp(*(const char *const *)str1, *(const char *const *)str2); } static const char invalid_chars[] = {'/', '\n', '\t', '\v', '\r', '\0'}; /* * Helper function for `valid_nick()`. * * Returns true if `ch` is not in the `invalid_chars` array. */ static bool is_valid_char(char ch) { char tmp; for (size_t i = 0; (tmp = invalid_chars[i]); ++i) { if (tmp == ch) { return false; } } return true; } /* Returns true if nick is valid. * * A valid toxic nick: * - cannot be empty * - cannot start with a space * - must not contain a forward slash (for logfile naming purposes) * - must not contain contiguous spaces * - must not contain a newline or tab seqeunce */ bool valid_nick(const char *nick) { if (!nick[0] || nick[0] == ' ') { return false; } for (size_t i = 0; nick[i]; ++i) { char ch = nick[i]; if ((ch == ' ' && nick[i + 1] == ' ') || !is_valid_char(ch)) { return false; } } return true; } /* Converts all newline/tab chars to spaces (use for strings that should be contained to a single line) */ void filter_str(char *str, size_t len) { for (size_t i = 0; i < len; ++i) { char ch = str[i]; if (!is_valid_char(ch) || str[i] == '\0') { str[i] = ' '; } } } /* gets base file name from path or original file name if no path is supplied. * Returns the file name length */ size_t get_file_name(char *namebuf, size_t bufsize, const char *pathname) { int len = strlen(pathname) - 1; char *path = strdup(pathname); if (path == NULL) { exit_toxic_err("failed in get_file_name", FATALERR_MEMORY); } while (len >= 0 && pathname[len] == '/') { path[len--] = '\0'; } char *finalname = strdup(path); if (finalname == NULL) { exit_toxic_err("failed in get_file_name", FATALERR_MEMORY); } const char *basenm = strrchr(path, '/'); if (basenm != NULL) { if (basenm[1]) { strcpy(finalname, &basenm[1]); } } snprintf(namebuf, bufsize, "%s", finalname); free(finalname); free(path); return strlen(namebuf); } /* Gets the base directory of path and puts it in dir. * dir must have at least as much space as path_len + 1. * * Returns the length of the base directory. */ size_t get_base_dir(const char *path, size_t path_len, char *dir) { if (path_len == 0 || path == NULL) { return 0; } size_t dir_len = char_rfind(path, '/', path_len); if (dir_len != 0 && dir_len < path_len) { ++dir_len; /* Leave trailing slash */ } memcpy(dir, path, dir_len); dir[dir_len] = '\0'; return dir_len; } /* converts str to all lowercase */ void str_to_lower(char *str) { int i; for (i = 0; str[i]; ++i) { str[i] = tolower(str[i]); } } /* puts friendnum's nick in buf, truncating at TOXIC_MAX_NAME_LENGTH if necessary. if toxcore API call fails, put UNKNOWN_NAME in buf Returns nick len */ size_t get_nick_truncate(Tox *m, char *buf, uint32_t friendnum) { Tox_Err_Friend_Query err; size_t len = tox_friend_get_name_size(m, friendnum, &err); if (err != TOX_ERR_FRIEND_QUERY_OK) { goto on_error; } else { if (!tox_friend_get_name(m, friendnum, (uint8_t *) buf, NULL)) { goto on_error; } } len = MIN(len, TOXIC_MAX_NAME_LENGTH - 1); buf[len] = '\0'; filter_str(buf, len); return len; on_error: strcpy(buf, UNKNOWN_NAME); len = strlen(UNKNOWN_NAME); buf[len] = '\0'; return len; } /* same as get_nick_truncate but for conferences */ int get_conference_nick_truncate(Tox *m, char *buf, uint32_t peernum, uint32_t conferencenum) { Tox_Err_Conference_Peer_Query err; size_t len = tox_conference_peer_get_name_size(m, conferencenum, peernum, &err); if (err != TOX_ERR_CONFERENCE_PEER_QUERY_OK) { goto on_error; } else { if (!tox_conference_peer_get_name(m, conferencenum, peernum, (uint8_t *) buf, NULL)) { goto on_error; } } len = MIN(len, TOXIC_MAX_NAME_LENGTH - 1); buf[len] = '\0'; filter_str(buf, len); return len; on_error: strcpy(buf, UNKNOWN_NAME); len = strlen(UNKNOWN_NAME); buf[len] = '\0'; return len; } /* copies data to msg buffer, removing return characters. returns length of msg, which will be no larger than size-1 */ size_t copy_tox_str(char *msg, size_t size, const char *data, size_t length) { size_t i; size_t j = 0; for (i = 0; (i < length) && (j < size - 1); ++i) { if (data[i] != '\r') { msg[j++] = data[i]; } } msg[j] = '\0'; return j; } /* returns index of the first instance of ch in s starting at idx. returns length of s if char not found or 0 if s is NULL. */ int char_find(int idx, const char *s, char ch) { if (!s) { return 0; } int i = idx; for (i = idx; s[i]; ++i) { if (s[i] == ch) { break; } } return i; } /* returns index of the last instance of ch in s starting at len. returns 0 if char not found or s is NULL (skips 0th index). */ int char_rfind(const char *s, char ch, int len) { if (!s) { return 0; } int i = 0; for (i = len; i > 0; --i) { if (s[i] == ch) { break; } } return i; } /* Converts bytes to appropriate unit and puts in buf as a string */ void bytes_convert_str(char *buf, int size, uint64_t bytes) { double conv = bytes; const char *unit; if (conv < KiB) { unit = "Bytes"; } else if (conv < MiB) { unit = "KiB"; conv /= (double) KiB; } else if (conv < GiB) { unit = "MiB"; conv /= (double) MiB; } else { unit = "GiB"; conv /= (double) GiB; } snprintf(buf, size, "%.1f %s", conv, unit); } /* checks if a file exists. Returns true or false */ bool file_exists(const char *path) { struct stat s; return stat(path, &s) == 0; } /* * Checks the file type path points to and returns a File_Type enum value. * * Returns FILE_TYPE_DIRECTORY if path points to a directory. * Returns FILE_TYPE_REGULAR if path points to a regular file. * Returns FILE_TYPE_OTHER on any other result, including an invalid path. */ File_Type file_type(const char *path) { struct stat s; if (stat(path, &s) == -1) { return FILE_TYPE_OTHER; } switch (s.st_mode & S_IFMT) { case S_IFDIR: return FILE_TYPE_DIRECTORY; case S_IFREG: return FILE_TYPE_REGULAR; default: return FILE_TYPE_OTHER; } } /* returns file size. If file doesn't exist returns 0. */ off_t file_size(const char *path) { struct stat st; if (stat(path, &st) == -1) { return 0; } return st.st_size; } /* sets window title in tab bar. */ void set_window_title(ToxWindow *self, const char *title, int len) { if (len <= 0 || !title) { return; } char cpy[TOXIC_MAX_NAME_LENGTH + 1]; if (self->type == WINDOW_TYPE_CONFERENCE) { /* keep conferencenumber in title for invites */ snprintf(cpy, sizeof(cpy), "%u %s", self->num, title); } else { snprintf(cpy, sizeof(cpy), "%s", title); } if (len > MAX_WINDOW_NAME_LENGTH) { strcpy(&cpy[MAX_WINDOW_NAME_LENGTH - 3], "..."); cpy[MAX_WINDOW_NAME_LENGTH] = '\0'; } snprintf(self->name, sizeof(self->name), "%s", cpy); } /* * Frees all members of a pointer array plus `arr`. */ void free_ptr_array(void **arr) { if (arr == NULL) { return; } void **tmp = arr; while (*arr) { free(*arr); ++arr; } free(tmp); } /* * Returns a null terminated array of `length` pointers. Each pointer is allocated `bytes` bytes. * Returns NULL on failure. * * The caller is responsible for freeing the array with `free_ptr_array`. */ void **malloc_ptr_array(size_t length, size_t bytes) { void **arr = malloc((length + 1) * sizeof(void *)); if (arr == NULL) { return NULL; } for (size_t i = 0; i < length; ++i) { arr[i] = malloc(bytes); if (arr[i] == NULL) { free_ptr_array(arr); return NULL; } } arr[length] = NULL; return arr; } toxic-0.11.3/src/misc_tools.h000066400000000000000000000161511416141666600160540ustar00rootroot00000000000000/* misc_tools.h * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef MISC_TOOLS_H #define MISC_TOOLS_H #include #include "toxic.h" #include "windows.h" #ifndef MIN #define MIN(x, y) (((x) < (y)) ? (x) : (y)) #endif #ifndef MAX #define MAX(x, y) (((x) > (y)) ? (x) : (y)) #endif #ifndef net_to_host #define net_to_host(x, y) hst_to_net(x, y) #endif #define UNUSED_VAR(x) ((void) x) typedef enum File_Type { FILE_TYPE_REGULAR, FILE_TYPE_DIRECTORY, FILE_TYPE_OTHER, } File_Type; void clear_screen(void); void hst_to_net(uint8_t *num, uint16_t numbytes); /* * Converts a hexidecimal string representation of a Tox public key to binary format and puts * the result in output. * * `hex_len` must be exactly TOX_PUBLIC_KEY_SIZE * 2, and `output_size` must have room * for TOX_PUBLIC_KEY_SIZE bytes. * * Returns 0 on success. * Returns -1 on failure. */ int tox_pk_string_to_bytes(const char *hex_string, size_t hex_len, char *output, size_t output_size); /* Converts a binary representation of a Tox public key into a string. * * `bin_pubkey_size` must be exactly TOX_PUBLIC_KEY_SIZE bytes in size, and * `output_size` must be at least TOX_PUBLIC_KEY_SIZE * 2 + 1. * * Returns 0 on success. * Returns -1 on failure. */ int tox_pk_bytes_to_str(const uint8_t *bin_pubkey, size_t bin_pubkey_size, char *output, size_t output_size); /* Convert a hexadecimcal string of length `size` to bytes and puts the result in `keystr`. * * Returns 0 on success. * Returns -1 on failure. */ int hex_string_to_bytes(char *buf, int size, const char *keystr); /* Converts a binary representation of a Tox ID into a string. * * `bin_id_size` must be exactly TOX_ADDRESS_SIZE bytes in length, and * `output_size` must be at least TOX_ADDRESS_SIZE * 2 + 1. * * Returns 0 on success. * Returns -1 on failure. */ int tox_id_bytes_to_str(const char *bin_id, size_t bin_id_size, char *output, size_t output_size); /* get the current unix time (not thread safe) */ time_t get_unix_time(void); /* Puts the current time in buf in the format of specified by the config */ void get_time_str(char *buf, size_t bufsize); /* Converts seconds to string in format HH:mm:ss; truncates hours and minutes when necessary */ void get_elapsed_time_str(char *buf, int bufsize, time_t secs); /* get the current local time (not thread safe) */ struct tm *get_time(void); /* Returns 1 if the string is empty, 0 otherwise */ int string_is_empty(const char *string); /* Same as above but for wide character strings */ int wstring_is_empty(const wchar_t *string); /* converts a multibyte string to a wide character string (must provide buffer) */ int char_to_wcs_buf(wchar_t *buf, const char *string, size_t n); /* converts wide character string into a multibyte string and puts in buf. */ int wcs_to_mbs_buf(char *buf, const wchar_t *string, size_t n); /* converts a multibyte string to a wide character string and puts in buf) */ int mbs_to_wcs_buf(wchar_t *buf, const char *string, size_t n); /* Returns 1 if connection has timed out, 0 otherwise */ int timed_out(time_t timestamp, time_t timeout); /* Attempts to sleep the caller's thread for `usec` microseconds */ void sleep_thread(long int usec); /* Colours the window tab according to type. Beeps if is_beep is true */ void alert_window(ToxWindow *self, int type, bool is_beep); /* case-insensitive string compare function for use with qsort */ int qsort_strcasecmp_hlpr(const void *str1, const void *str2); /* case-insensitive string compare function for use with qsort */ int qsort_ptr_char_array_helper(const void *str1, const void *str2); /* Returns true if nick is valid. * * A valid toxic nick: * - cannot be empty * - cannot start with a space * - must not contain a forward slash (for logfile naming purposes) * - must not contain contiguous spaces * - must not contain a newline or tab seqeunce */ bool valid_nick(const char *nick); /* Converts all newline/tab chars to spaces (use for strings that should be contained to a single line) */ void filter_str(char *str, size_t len); /* gets base file name from path or original file name if no path is supplied */ size_t get_file_name(char *namebuf, size_t bufsize, const char *pathname); /* Gets the base directory of path and puts it in dir. * dir must have at least as much space as path_len. * * Returns the length of the base directory on success. * Returns -1 on failure. */ size_t get_base_dir(const char *path, size_t path_len, char *dir); /* converts str to all lowercase */ void str_to_lower(char *str); /* puts friendnum's nick in buf, truncating at TOXIC_MAX_NAME_LENGTH if necessary. Returns nick len on success, -1 on failure */ size_t get_nick_truncate(Tox *m, char *buf, uint32_t friendnum); /* same as get_nick_truncate but for conferences */ int get_conference_nick_truncate(Tox *m, char *buf, uint32_t peernum, uint32_t conferencenum); /* copies data to msg buffer. returns length of msg, which will be no larger than size-1 */ size_t copy_tox_str(char *msg, size_t size, const char *data, size_t length); /* returns index of the first instance of ch in s starting at idx. returns length of s if char not found or 0 if s is NULL. */ int char_find(int idx, const char *s, char ch); /* returns index of the last instance of ch in s starting at len. returns 0 if char not found or s is NULL (skips 0th index). */ int char_rfind(const char *s, char ch, int len); /* Converts bytes to appropriate unit and puts in buf as a string */ void bytes_convert_str(char *buf, int size, uint64_t bytes); /* checks if a file exists. Returns true or false */ bool file_exists(const char *path); /* * Checks the file type path points to and returns a File_Type enum value. * * Returns FILE_TYPE_DIRECTORY if path points to a directory. * Returns FILE_TYPE_REGULAR if path points to a regular file. * Returns FILE_TYPE_OTHER on any other result, including an invalid path. */ File_Type file_type(const char *path); /* returns file size. If file doesn't exist returns 0. */ off_t file_size(const char *path); /* sets window title in tab bar. */ void set_window_title(ToxWindow *self, const char *title, int len); /* * Frees all members of a pointer array plus `arr`. */ void free_ptr_array(void **arr); /* * Returns a null terminated array of `length` pointers. Each pointer is allocated `bytes` bytes. * Returns NULL on failure. * * The caller is responsible for freeing the array with `free_ptr_array`. */ void **malloc_ptr_array(size_t length, size_t bytes); #endif /* MISC_TOOLS_H */ toxic-0.11.3/src/name_lookup.c000066400000000000000000000273551416141666600162150ustar00rootroot00000000000000/* name_lookup.c * * * Copyright (C) 2015 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #include #include #include #include #include "configdir.h" #include "curl_util.h" #include "global_commands.h" #include "line_info.h" #include "misc_tools.h" #include "toxic.h" #include "windows.h" #define NAMESERVER_API_PATH "api" #define SERVER_KEY_SIZE 32 #define MAX_SERVERS 50 #define MAX_DOMAIN_SIZE 32 #define MAX_SERVER_LINE MAX_DOMAIN_SIZE + (SERVER_KEY_SIZE * 2) + 3 static struct Nameservers { int lines; char names[MAX_SERVERS][MAX_DOMAIN_SIZE]; char keys[MAX_SERVERS][SERVER_KEY_SIZE]; } Nameservers; static struct thread_data { Tox *m; ToxWindow *self; char id_bin[TOX_ADDRESS_SIZE]; char addr[MAX_STR_SIZE]; char msg[MAX_STR_SIZE]; bool disabled; volatile bool busy; } t_data; static struct lookup_thread { pthread_t tid; pthread_attr_t attr; } lookup_thread; static void clear_thread_data(void) { t_data = (struct thread_data) { 0 }; } static int lookup_error(ToxWindow *self, const char *errmsg, ...) { char frmt_msg[MAX_STR_SIZE]; va_list args; va_start(args, errmsg); vsnprintf(frmt_msg, sizeof(frmt_msg), errmsg, args); va_end(args); pthread_mutex_lock(&Winthread.lock); line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "name lookup failed: %s", frmt_msg); pthread_mutex_unlock(&Winthread.lock); return -1; } static void kill_lookup_thread(void) { clear_thread_data(); pthread_attr_destroy(&lookup_thread.attr); pthread_exit(NULL); } /* Attempts to load the nameserver list pointed at by path into the Nameservers structure. * * Returns 0 on success. * -1 is reserved. * Returns -2 if the supplied path does not exist. * Returns -3 if the list does not contain any valid entries. */ static int load_nameserver_list(const char *path) { FILE *fp = fopen(path, "r"); if (fp == NULL) { return -2; } char line[MAX_SERVER_LINE]; while (fgets(line, sizeof(line), fp) && Nameservers.lines < MAX_SERVERS) { size_t linelen = strlen(line); if (linelen < SERVER_KEY_SIZE * 2 + 5) { continue; } if (line[linelen - 1] == '\n') { --linelen; line[linelen] = '\0'; } const char *name = strtok(line, " "); const char *keystr = strtok(NULL, " "); if (name == NULL || keystr == NULL) { continue; } if (strlen(keystr) != SERVER_KEY_SIZE * 2) { continue; } const size_t idx = Nameservers.lines; snprintf(Nameservers.names[idx], sizeof(Nameservers.names[idx]), "%s", name); int res = hex_string_to_bytes(Nameservers.keys[idx], SERVER_KEY_SIZE, keystr); if (res == -1) { continue; } ++Nameservers.lines; } fclose(fp); if (Nameservers.lines < 1) { return -3; } return 0; } /* Takes address addr in the form "username@domain", puts the username in namebuf, * and the domain in dombuf. * * Returns 0 on success. * Returns -1 on failure */ static int parse_addr(const char *addr, char *namebuf, size_t namebuf_sz, char *dombuf, size_t dombuf_sz) { if (strlen(addr) >= (MAX_STR_SIZE - strlen(NAMESERVER_API_PATH))) { return -1; } char tmpaddr[MAX_STR_SIZE]; char *tmpname = NULL; char *tmpdom = NULL; snprintf(tmpaddr, sizeof(tmpaddr), "%s", addr); tmpname = strtok(tmpaddr, "@"); tmpdom = strtok(NULL, ""); if (tmpname == NULL || tmpdom == NULL) { return -1; } str_to_lower(tmpdom); snprintf(namebuf, namebuf_sz, "%s", tmpname); snprintf(dombuf, dombuf_sz, "%s", tmpdom); return 0; } /* matches input domain name with domains in list and obtains key. * Turns out_domain into the full domain we need to make a POST request. * * Return true on match. * Returns false on no match. */ static bool get_domain_match(char *pubkey, char *out_domain, size_t out_domain_size, const char *inputdomain) { int i; for (i = 0; i < Nameservers.lines; ++i) { if (strcmp(Nameservers.names[i], inputdomain) == 0) { memcpy(pubkey, Nameservers.keys[i], SERVER_KEY_SIZE); snprintf(out_domain, out_domain_size, "https://%s/%s", Nameservers.names[i], NAMESERVER_API_PATH); return true; } } return false; } /* Converts Tox ID string contained in recv_data to binary format and puts it in thread's ID buffer. * * Returns 0 on success. * Returns -1 on failure. */ #define ID_PREFIX "\"tox_id\": \"" static int process_response(struct Recv_Curl_Data *recv_data) { size_t prefix_size = strlen(ID_PREFIX); if (recv_data->length < TOX_ADDRESS_SIZE * 2 + prefix_size) { return -1; } const char *IDstart = strstr(recv_data->data, ID_PREFIX); if (IDstart == NULL) { return -1; } if (strlen(IDstart) < TOX_ADDRESS_SIZE * 2 + prefix_size) { return -1; } char ID_string[TOX_ADDRESS_SIZE * 2 + 1]; memcpy(ID_string, IDstart + prefix_size, TOX_ADDRESS_SIZE * 2); ID_string[TOX_ADDRESS_SIZE * 2] = 0; if (tox_pk_string_to_bytes(ID_string, strlen(ID_string), t_data.id_bin, sizeof(t_data.id_bin)) == -1) { return -1; } return 0; } void *lookup_thread_func(void *data) { UNUSED_VAR(data); ToxWindow *self = t_data.self; char input_domain[MAX_STR_SIZE]; char name[MAX_STR_SIZE]; if (parse_addr(t_data.addr, name, sizeof(name), input_domain, sizeof(input_domain)) == -1) { lookup_error(self, "Input must be a 76 character Tox ID or an address in the form: username@domain"); kill_lookup_thread(); } char nameserver_key[SERVER_KEY_SIZE]; char real_domain[MAX_DOMAIN_SIZE]; if (!get_domain_match(nameserver_key, real_domain, sizeof(real_domain), input_domain)) { if (!strcasecmp(input_domain, "utox.org")) { lookup_error(self, "utox.org uses deprecated DNS-based lookups and is no longer supported by Toxic."); } else { lookup_error(self, "Name server domain not found."); } kill_lookup_thread(); } CURL *c_handle = curl_easy_init(); if (!c_handle) { lookup_error(self, "curl handler error"); kill_lookup_thread(); } struct Recv_Curl_Data *recv_data = calloc(1, sizeof(struct Recv_Curl_Data)); if (recv_data == NULL) { lookup_error(self, "memory allocation error"); kill_lookup_thread(); } char post_data[MAX_STR_SIZE + 30]; snprintf(post_data, sizeof(post_data), "{\"action\": 3, \"name\": \"%s\"}", name); struct curl_slist *headers = NULL; headers = curl_slist_append(headers, "Content-Type: application/json"); headers = curl_slist_append(headers, "charsets: utf-8"); curl_easy_setopt(c_handle, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(c_handle, CURLOPT_URL, real_domain); curl_easy_setopt(c_handle, CURLOPT_WRITEFUNCTION, curl_cb_write_data); curl_easy_setopt(c_handle, CURLOPT_WRITEDATA, recv_data); curl_easy_setopt(c_handle, CURLOPT_USERAGENT, "libcurl-agent/1.0"); curl_easy_setopt(c_handle, CURLOPT_POSTFIELDS, post_data); int proxy_ret = set_curl_proxy(c_handle, arg_opts.proxy_address, arg_opts.proxy_port, arg_opts.proxy_type); if (proxy_ret != 0) { lookup_error(self, "Failed to set proxy (error %d)\n", proxy_ret); goto on_exit; } int ret = curl_easy_setopt(c_handle, CURLOPT_USE_SSL, CURLUSESSL_ALL); if (ret != CURLE_OK) { lookup_error(self, "TLS could not be enabled (libcurl error %d)", ret); goto on_exit; } ret = curl_easy_setopt(c_handle, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2); if (ret != CURLE_OK) { lookup_error(self, "TLSv1.2 could not be set (libcurl error %d)", ret); goto on_exit; } ret = curl_easy_setopt(c_handle, CURLOPT_SSL_CIPHER_LIST, TLS_CIPHER_SUITE_LIST); if (ret != CURLE_OK) { lookup_error(self, "Failed to set TLS cipher list (libcurl error %d)", ret); goto on_exit; } ret = curl_easy_perform(c_handle); if (ret != CURLE_OK) { /* If system doesn't support any of the specified ciphers suites, fall back to default */ if (ret == CURLE_SSL_CIPHER) { curl_easy_setopt(c_handle, CURLOPT_SSL_CIPHER_LIST, NULL); ret = curl_easy_perform(c_handle); } if (ret != CURLE_OK) { lookup_error(self, "HTTPS lookup error (libcurl error %d)", ret); goto on_exit; } } if (process_response(recv_data) == -1) { lookup_error(self, "Bad response."); goto on_exit; } pthread_mutex_lock(&Winthread.lock); cmd_add_helper(self, t_data.m, t_data.id_bin, t_data.msg); pthread_mutex_unlock(&Winthread.lock); on_exit: free(recv_data); curl_slist_free_all(headers); curl_easy_cleanup(c_handle); kill_lookup_thread(); return 0; } void name_lookup(ToxWindow *self, Tox *m, const char *id_bin, const char *addr, const char *message) { if (t_data.disabled) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "name lookups are disabled."); return; } if (t_data.busy) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Please wait for previous name lookup to finish."); return; } snprintf(t_data.id_bin, sizeof(t_data.id_bin), "%s", id_bin); snprintf(t_data.addr, sizeof(t_data.addr), "%s", addr); snprintf(t_data.msg, sizeof(t_data.msg), "%s", message); t_data.self = self; t_data.m = m; t_data.busy = true; if (pthread_attr_init(&lookup_thread.attr) != 0) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, RED, "Error: lookup thread attr failed to init"); clear_thread_data(); return; } if (pthread_attr_setdetachstate(&lookup_thread.attr, PTHREAD_CREATE_DETACHED) != 0) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, RED, "Error: lookup thread attr failed to set"); pthread_attr_destroy(&lookup_thread.attr); clear_thread_data(); return; } if (pthread_create(&lookup_thread.tid, &lookup_thread.attr, lookup_thread_func, NULL) != 0) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, RED, "Error: lookup thread failed to init"); pthread_attr_destroy(&lookup_thread.attr); clear_thread_data(); return; } } /* Initializes http based name lookups. Note: This function must be called only once before additional * threads are spawned. * * Returns 0 on success. * Returns -1 if curl failed to init. * Returns -2 if the nameserver list cannot be found. * Returns -3 if the nameserver list does not contain any valid entries. */ int name_lookup_init(int curl_init_status) { if (curl_init_status != 0) { t_data.disabled = true; return -1; } const char *path = arg_opts.nameserver_path[0] ? arg_opts.nameserver_path : PACKAGE_DATADIR "/nameservers"; int ret = load_nameserver_list(path); if (ret != 0) { t_data.disabled = true; return ret; } return 0; } toxic-0.11.3/src/name_lookup.h000066400000000000000000000022011416141666600162010ustar00rootroot00000000000000/* name_lookup.h * * * Copyright (C) 2015 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef NAME_LOOKUP #define NAME_LOOKUP /* Initializes http based name lookups. Note: This function must be called only once before additional * threads are spawned. * * Returns 0 on success. * Returns -1 on failure. */ int name_lookup_init(int curl_init_status); int name_lookup(ToxWindow *self, Tox *m, const char *id_bin, const char *addr, const char *message); #endif /* NAME_LOOKUP */ toxic-0.11.3/src/notify.c000066400000000000000000000540711416141666600152070ustar00rootroot00000000000000/* notify.c * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #include #include #include #include #include #include #include #include #include #include "audio_device.h" #include "line_info.h" #include "misc_tools.h" #include "notify.h" #include "settings.h" #include "x11focus.h" #if defined(AUDIO) || defined(SOUND_NOTIFY) #ifdef __APPLE__ #include #include #else #include #include /* compatibility with older versions of OpenAL */ #ifndef ALC_ALL_DEVICES_SPECIFIER #include #endif /* ALC_ALL_DEVICES_SPECIFIER */ #endif /* __APPLE__ */ #ifdef SOUND_NOTIFY #include /* freealut packet */ #endif /* SOUND_NOTIFY */ #endif /* defined(AUDIO) || defined(SOUND_NOTIFY) */ #ifdef BOX_NOTIFY #include #endif #define MAX_BOX_MSG_LEN 127 #define SOUNDS_SIZE 10 #define ACTIVE_NOTIFS_MAX 10 extern struct user_settings *user_settings; static struct Control { time_t cooldown; time_t notif_timeout; #if defined(SOUND_NOTIFY) || defined(BOX_NOTIFY) pthread_mutex_t poll_mutex[1]; bool poll_active; #endif #ifdef SOUND_NOTIFY uint32_t device_idx; /* index of output device */ char *sounds[SOUNDS_SIZE]; #endif /* SOUND_NOTIFY */ } Control = {0}; static struct _ActiveNotifications { #ifdef SOUND_NOTIFY uint32_t source; uint32_t buffer; bool looping; #endif /* SOUND_NOTIFY */ bool active; int *id_indicator; #ifdef BOX_NOTIFY NotifyNotification *box; char messages[MAX_BOX_MSG_LEN + 1][MAX_BOX_MSG_LEN + 1]; char title[64]; size_t size; time_t n_timeout; #endif /* BOX_NOTIFY */ } actives[ACTIVE_NOTIFS_MAX]; /**********************************************************************************/ /**********************************************************************************/ /**********************************************************************************/ /**********************************************************************************/ /**********************************************************************************/ static void clear_actives_index(size_t idx) { if (actives[idx].id_indicator) { *actives[idx].id_indicator = -1; } actives[idx] = (struct _ActiveNotifications) { 0 }; } /* coloured tab notifications: primary notification type */ static void tab_notify(ToxWindow *self, uint64_t flags) { if (self == NULL) { return; } if (flags & NT_WNDALERT_0) { self->alert = WINDOW_ALERT_0; } else if ((flags & NT_WNDALERT_1) && (!self->alert || self->alert > WINDOW_ALERT_0)) { self->alert = WINDOW_ALERT_1; } else if ((flags & NT_WNDALERT_2) && (!self->alert || self->alert > WINDOW_ALERT_1)) { self->alert = WINDOW_ALERT_2; } ++self->pending_messages; } static bool notifications_are_disabled(uint64_t flags) { if (user_settings->alerts != ALERTS_ENABLED) { return true; } bool res = (flags & NT_RESTOL) && (Control.cooldown > get_unix_time()); #ifdef X11 return res || ((flags & NT_NOFOCUS) && is_focused()); #else return res; #endif } static void control_lock(void) { #if defined(SOUND_NOTIFY) || defined(BOX_NOTIFY) pthread_mutex_lock(Control.poll_mutex); #endif } static void control_unlock(void) { #if defined(SOUND_NOTIFY) || defined(BOX_NOTIFY) pthread_mutex_unlock(Control.poll_mutex); #endif } #ifdef SOUND_NOTIFY bool is_playing(int source) { int ready; alGetSourcei(source, AL_SOURCE_STATE, &ready); return ready == AL_PLAYING; } /* TODO maybe find better way to do this */ /* cooldown is in seconds */ #define DEVICE_COOLDOWN 5 /* TODO perhaps load this from config? */ static bool device_opened = false; time_t last_opened_update = 0; /* Opens primary device. Returns true on succe*/ void m_open_device(void) { last_opened_update = get_unix_time(); if (device_opened) { return; } /* Blah error check */ open_output_device(&Control.device_idx, 48000, 20, 1); device_opened = true; } void m_close_device(void) { if (!device_opened) { return; } close_device(output, Control.device_idx); device_opened = false; } /* Terminate all sounds but wait for them to finish first */ void graceful_clear(void) { control_lock(); while (1) { int i; for (i = 0; i < ACTIVE_NOTIFS_MAX; ++i) { if (actives[i].active) { #ifdef BOX_NOTIFY if (actives[i].box) { GError *ignore; notify_notification_close(actives[i].box, &ignore); actives[i].box = NULL; } #endif /* BOX_NOTIFY */ if (actives[i].id_indicator) { *actives[i].id_indicator = -1; /* reset indicator value */ } if (actives[i].looping) { stop_sound(i); } else { if (!is_playing(actives[i].source)) { clear_actives_index(i); } else { break; } } } } if (i == ACTIVE_NOTIFS_MAX) { m_close_device(); /* In case it's opened */ control_unlock(); return; } sleep_thread(1000L); } control_unlock(); } void *do_playing(void *_p) { UNUSED_VAR(_p); while (true) { control_lock(); if (!Control.poll_active) { control_unlock(); break; } bool has_looping = false; bool test_active_notify = false; int i; for (i = 0; i < ACTIVE_NOTIFS_MAX; ++i) { if (actives[i].looping) { has_looping = true; } test_active_notify = actives[i].active && !actives[i].looping; #ifdef BOX_NOTIFY test_active_notify = test_active_notify && !actives[i].box; #endif if (test_active_notify) { if (actives[i].id_indicator) { *actives[i].id_indicator = -1; /* reset indicator value */ } if (!is_playing(actives[i].source)) { /* Close */ alSourceStop(actives[i].source); alDeleteSources(1, &actives[i].source); alDeleteBuffers(1, &actives[i].buffer); clear_actives_index(i); } } #ifdef BOX_NOTIFY else if (actives[i].box && time(NULL) >= actives[i].n_timeout) { GError *ignore; notify_notification_close(actives[i].box, &ignore); actives[i].box = NULL; if (actives[i].id_indicator) { *actives[i].id_indicator = -1; /* reset indicator value */ } if (!actives[i].looping && !is_playing(actives[i].source)) { /* stop source if not looping or playing, just terminate box */ alSourceStop(actives[i].source); alDeleteSources(1, &actives[i].source); alDeleteBuffers(1, &actives[i].buffer); clear_actives_index(i); } } #endif /* BOX_NOTIFY */ } /* device is opened and no activity in under DEVICE_COOLDOWN time, close device*/ if (device_opened && !has_looping && (time(NULL) - last_opened_update) > DEVICE_COOLDOWN) { m_close_device(); } has_looping = false; control_unlock(); sleep_thread(100000L); } pthread_exit(NULL); } int play_source(uint32_t source, uint32_t buffer, bool looping) { int i = 0; for (; i < ACTIVE_NOTIFS_MAX && actives[i].active; ++i); if (i == ACTIVE_NOTIFS_MAX) { return -1; /* Full */ } alSourcePlay(source); actives[i].active = 1; actives[i].source = source; actives[i].buffer = buffer; actives[i].looping = looping; return i; } #elif BOX_NOTIFY void *do_playing(void *_p) { UNUSED_VAR(_p); while (true) { control_lock(); if (!Control.poll_active) { control_unlock(); break; } for (size_t i = 0; i < ACTIVE_NOTIFS_MAX; ++i) { if (actives[i].box && time(NULL) >= actives[i].n_timeout) { GError *ignore; notify_notification_close(actives[i].box, &ignore); clear_actives_index(i); } } control_unlock(); sleep_thread(10000L); } pthread_exit(NULL); } void graceful_clear(void) { control_lock(); for (size_t i = 0; i < ACTIVE_NOTIFS_MAX; ++i) { if (actives[i].box) { GError *ignore; notify_notification_close(actives[i].box, &ignore); } clear_actives_index(i); } control_unlock(); } #endif /* SOUND_NOTIFY */ /* Kills all notifications for `id`. This must be called before freeing a ToxWindow. */ void kill_notifs(int id) { control_lock(); for (size_t i = 0; i < ACTIVE_NOTIFS_MAX; ++i) { if (!actives[i].id_indicator) { continue; } if (*actives[i].id_indicator == id) { #ifdef BOX_NOTIFY if (actives[i].box) { GError *ignore; notify_notification_close(actives[i].box, &ignore); } #endif // BOX_NOTIFY clear_actives_index(i); } } control_unlock(); } /**********************************************************************************/ /**********************************************************************************/ /**********************************************************************************/ /**********************************************************************************/ /**********************************************************************************/ /* Opens primary device */ int init_notify(int login_cooldown, int notification_timeout) { #ifdef SOUND_NOTIFY alutInitWithoutContext(NULL, NULL); #endif /* SOUND_NOTIFY */ #if defined(SOUND_NOTIFY) || defined(BOX_NOTIFY) if (pthread_mutex_init(Control.poll_mutex, NULL) != 0) { return -1; } Control.poll_active = 1; pthread_t thread; if (pthread_create(&thread, NULL, do_playing, NULL) != 0 || pthread_detach(thread) != 0) { pthread_mutex_destroy(Control.poll_mutex); Control.poll_active = 0; return -1; } #endif /* defined(SOUND_NOTIFY) || defined(BOX_NOTIFY) */ Control.cooldown = time(NULL) + login_cooldown; #ifdef BOX_NOTIFY notify_init("Toxic"); #endif Control.notif_timeout = notification_timeout; return 1; } void terminate_notify(void) { #if defined(SOUND_NOTIFY) || defined(BOX_NOTIFY) control_lock(); if (!Control.poll_active) { control_unlock(); return; } Control.poll_active = 0; control_unlock(); graceful_clear(); #endif /* defined(SOUND_NOTIFY) || defined(BOX_NOTIFY) */ #ifdef SOUND_NOTIFY int i = 0; for (; i < SOUNDS_SIZE; ++i) { free(Control.sounds[i]); } alutExit(); #endif /* SOUND_NOTIFY */ #ifdef BOX_NOTIFY notify_uninit(); #endif } #ifdef SOUND_NOTIFY /* * Sets notification sound designated by `sound` to file path `value`. * * Return true if the sound is successfully set. */ bool set_sound(Notification sound, const char *value) { if (sound == silent) { return false; } free(Control.sounds[sound]); size_t len = strlen(value) + 1; Control.sounds[sound] = calloc(len, 1); if (Control.sounds[sound] == NULL) { return false; } memcpy(Control.sounds[sound], value, len); struct stat buf; return stat(value, &buf) == 0; } int play_sound_internal(Notification what, bool loop) { uint32_t source; uint32_t buffer; m_open_device(); alGenSources(1, &source); alGenBuffers(1, &buffer); buffer = alutCreateBufferFromFile(Control.sounds[what]); alSourcei(source, AL_BUFFER, buffer); alSourcei(source, AL_LOOPING, loop); int rc = play_source(source, buffer, loop); if (rc < 0) { alSourceStop(source); alDeleteSources(1, &source); alDeleteBuffers(1, &buffer); return -1; } return rc; } int play_notify_sound(Notification notif, uint64_t flags) { int rc = -1; if (flags & NT_BEEP) { beep(); } if (notif != silent) { if (!Control.poll_active || !Control.sounds[notif]) { return -1; } rc = play_sound_internal(notif, flags & NT_LOOP ? 1 : 0); } return rc; } void stop_sound(int id) { if (id >= 0 && id < ACTIVE_NOTIFS_MAX && actives[id].looping && actives[id].active) { #ifdef BOX_NOTIFY if (actives[id].box) { GError *ignore; notify_notification_close(actives[id].box, &ignore); } #endif /* BOX_NOTIFY */ // alSourcei(actives[id].source, AL_LOOPING, false); alSourceStop(actives[id].source); alDeleteSources(1, &actives[id].source); alDeleteBuffers(1, &actives[id].buffer); clear_actives_index(id); } } #endif /* SOUND_NOTIFY */ static int m_play_sound(Notification notif, uint64_t flags) { #ifdef SOUND_NOTIFY return play_notify_sound(notif, flags); #else if (notif != silent) { beep(); } return -1; #endif /* SOUND_NOTIFY */ } int sound_notify(ToxWindow *self, Notification notif, uint64_t flags, int *id_indicator) { tab_notify(self, flags); if (notifications_are_disabled(flags)) { return -1; } int id = -1; control_lock(); if (self && (!self->stb || self->stb->status != TOX_USER_STATUS_BUSY)) { id = m_play_sound(notif, flags); } else if (flags & NT_ALWAYS) { id = m_play_sound(notif, flags); } #if defined(BOX_NOTIFY) && !defined(SOUND_NOTIFY) if (id == -1) { for (id = 0; id < ACTIVE_NOTIFS_MAX && actives[id].box; id++); if (id == ACTIVE_NOTIFS_MAX) { control_unlock(); return -1; /* Full */ } } #endif /* defined(BOX_NOTIFY) && !defined(SOUND_NOTIFY) */ if (id_indicator && id != -1) { actives[id].id_indicator = id_indicator; *id_indicator = id; } control_unlock(); return id; } int sound_notify2(ToxWindow *self, Notification notif, uint64_t flags, int id) { tab_notify(self, flags); if (notifications_are_disabled(flags)) { return -1; } if (id < 0 || id >= ACTIVE_NOTIFS_MAX) { return -1; } #ifdef SOUND_NOTIFY control_lock(); if (!actives[id].active || !Control.sounds[notif]) { control_unlock(); return -1; } m_open_device(); alSourceStop(actives[id].source); alDeleteSources(1, &actives[id].source); alDeleteBuffers(1, &actives[id].buffer); alGenSources(1, &actives[id].source); alGenBuffers(1, &actives[id].buffer); actives[id].buffer = alutCreateBufferFromFile(Control.sounds[notif]); alSourcei(actives[id].source, AL_BUFFER, actives[id].buffer); alSourcei(actives[id].source, AL_LOOPING, flags & NT_LOOP); alSourcePlay(actives[id].source); control_unlock(); return id; #else if (notif != silent) { beep(); } return 0; #endif /* SOUND_NOTIFY */ } int box_notify(ToxWindow *self, Notification notif, uint64_t flags, int *id_indicator, const char *title, const char *format, ...) { if (notifications_are_disabled(flags)) { tab_notify(self, flags); return -1; } #ifdef BOX_NOTIFY int id = sound_notify(self, notif, flags, id_indicator); control_lock(); #ifdef SOUND_NOTIFY if (id == -1) { /* Could not play */ for (id = 0; id < ACTIVE_NOTIFS_MAX && actives[id].active; id ++); if (id == ACTIVE_NOTIFS_MAX) { control_unlock(); return -1; /* Full */ } actives[id].active = 1; actives[id].id_indicator = id_indicator; if (id_indicator) { *id_indicator = id; } } #else if (id == -1) { control_unlock(); return -1; } #endif /* SOUND_NOTIFY */ snprintf(actives[id].title, sizeof(actives[id].title), "%s", title); if (strlen(title) > 23) { strcpy(actives[id].title + 20, "..."); } va_list __ARGS__; va_start(__ARGS__, format); vsnprintf(actives[id].messages[0], MAX_BOX_MSG_LEN, format, __ARGS__); va_end(__ARGS__); if (strlen(actives[id].messages[0]) > MAX_BOX_MSG_LEN - 3) { strcpy(actives[id].messages[0] + MAX_BOX_MSG_LEN - 3, "..."); } actives[id].box = notify_notification_new(actives[id].title, actives[id].messages[0], NULL); actives[id].size++; actives[id].n_timeout = get_unix_time() + Control.notif_timeout / 1000; notify_notification_set_timeout(actives[id].box, Control.notif_timeout); notify_notification_set_app_name(actives[id].box, "toxic"); /*notify_notification_add_action(actives[id].box, "lel", "default", m_notify_action, self, NULL);*/ notify_notification_show(actives[id].box, NULL); control_unlock(); return id; #else return sound_notify(self, notif, flags, id_indicator); #endif /* BOX_NOTIFY */ } int box_notify2(ToxWindow *self, Notification notif, uint64_t flags, int id, const char *format, ...) { if (notifications_are_disabled(flags)) { tab_notify(self, flags); return -1; } #ifdef BOX_NOTIFY if (sound_notify2(self, notif, flags, id) == -1) { return -1; } control_lock(); if (!actives[id].box || actives[id].size >= MAX_BOX_MSG_LEN + 1) { control_unlock(); return -1; } va_list __ARGS__; va_start(__ARGS__, format); vsnprintf(actives[id].messages[actives[id].size], MAX_BOX_MSG_LEN, format, __ARGS__); va_end(__ARGS__); if (strlen(actives[id].messages[actives[id].size]) > MAX_BOX_MSG_LEN - 3) { strcpy(actives[id].messages[actives[id].size] + MAX_BOX_MSG_LEN - 3, "..."); } actives[id].size++; actives[id].n_timeout = get_unix_time() + Control.notif_timeout / 1000; char *formatted = calloc(1, sizeof(char) * ((MAX_BOX_MSG_LEN + 1) * (MAX_BOX_MSG_LEN + 2))); for (size_t i = 0; i < actives[id].size; ++i) { strcat(formatted, actives[id].messages[i]); strcat(formatted, "\n"); } notify_notification_update(actives[id].box, actives[id].title, formatted, NULL); notify_notification_show(actives[id].box, NULL); free(formatted); control_unlock(); return id; #else return sound_notify2(self, notif, flags, id); #endif /* BOX_NOTIFY */ } int box_silent_notify(ToxWindow *self, uint64_t flags, int *id_indicator, const char *title, const char *format, ...) { tab_notify(self, flags); if (notifications_are_disabled(flags)) { return -1; } #ifdef BOX_NOTIFY control_lock(); int id; for (id = 0; id < ACTIVE_NOTIFS_MAX && actives[id].active; id ++); if (id == ACTIVE_NOTIFS_MAX) { control_unlock(); return -1; /* Full */ } if (id_indicator) { actives[id].id_indicator = id_indicator; *id_indicator = id; } snprintf(actives[id].title, sizeof(actives[id].title), "%s", title); if (strlen(title) > 23) { strcpy(actives[id].title + 20, "..."); } va_list __ARGS__; va_start(__ARGS__, format); vsnprintf(actives[id].messages[0], MAX_BOX_MSG_LEN, format, __ARGS__); va_end(__ARGS__); if (strlen(actives[id].messages[0]) > MAX_BOX_MSG_LEN - 3) { strcpy(actives[id].messages[0] + MAX_BOX_MSG_LEN - 3, "..."); } actives[id].active = 1; actives[id].box = notify_notification_new(actives[id].title, actives[id].messages[0], NULL); actives[id].size ++; actives[id].n_timeout = get_unix_time() + Control.notif_timeout / 1000; notify_notification_set_timeout(actives[id].box, Control.notif_timeout); notify_notification_set_app_name(actives[id].box, "toxic"); /*notify_notification_add_action(actives[id].box, "lel", "default", m_notify_action, self, NULL);*/ notify_notification_show(actives[id].box, NULL); control_unlock(); return id; #else return -1; #endif /* BOX_NOTIFY */ } int box_silent_notify2(ToxWindow *self, uint64_t flags, int id, const char *format, ...) { tab_notify(self, flags); if (notifications_are_disabled(flags)) { return -1; } #ifdef BOX_NOTIFY control_lock(); if (id < 0 || id >= ACTIVE_NOTIFS_MAX || !actives[id].box || actives[id].size >= MAX_BOX_MSG_LEN + 1) { control_unlock(); return -1; } va_list __ARGS__; va_start(__ARGS__, format); vsnprintf(actives[id].messages[actives[id].size], MAX_BOX_MSG_LEN, format, __ARGS__); va_end(__ARGS__); if (strlen(actives[id].messages[actives[id].size]) > MAX_BOX_MSG_LEN - 3) { strcpy(actives[id].messages[actives[id].size] + MAX_BOX_MSG_LEN - 3, "..."); } actives[id].size ++; actives[id].n_timeout = get_unix_time() + Control.notif_timeout / 1000; char *formatted = calloc(1, sizeof(char) * ((MAX_BOX_MSG_LEN + 1) * (MAX_BOX_MSG_LEN + 2))); for (size_t i = 0; i < actives[id].size; ++i) { strcat(formatted, actives[id].messages[i]); strcat(formatted, "\n"); } notify_notification_update(actives[id].box, actives[id].title, formatted, NULL); notify_notification_show(actives[id].box, NULL); free(formatted); control_unlock(); return id; #else return -1; #endif /* BOX_NOTIFY */ } toxic-0.11.3/src/notify.h000066400000000000000000000061141416141666600152070ustar00rootroot00000000000000/* notify.h * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef NOTIFY_H #define NOTIFY_H #include #include "windows.h" typedef enum _Notification { silent = -1, notif_error, self_log_in, self_log_out, user_log_in, user_log_out, call_incoming, call_outgoing, generic_message, transfer_pending, transfer_completed, } Notification; typedef enum _Flags { NT_NOFOCUS = 1 << 0, /* Notify when focus is not on this terminal. NOTE: only works with x11, * if no x11 present this flag is ignored */ NT_BEEP = 1 << 1, /* Play native sound instead: \a */ NT_LOOP = 1 << 2, /* Loop sound. If this setting active, notify() will return id of the sound * so it could be stopped. It will return 0 if error or NT_NATIVE flag is set and play \a instead */ NT_RESTOL = 1 << 3, /* Respect tolerance. Usually used to stop flood at toxic startup * Only works if login_cooldown is true when calling init_notify() */ NT_NOTIFWND = 1 << 4, /* Pop notify window. NOTE: only works(/WILL WORK) if libnotify is present */ NT_WNDALERT_0 = 1 << 5, /* Alert toxic */ NT_WNDALERT_1 = 1 << 6, /* Alert toxic */ NT_WNDALERT_2 = 1 << 7, /* Alert toxic */ NT_ALWAYS = 1 << 8, /* Force sound to play */ } Flags; int init_notify(int login_cooldown, int notification_timeout); void terminate_notify(void); /* Kills all notifications for `id`. This must be called before freeing a ToxWindow. */ void kill_notifs(int id); int sound_notify(ToxWindow *self, Notification notif, uint64_t flags, int *id_indicator); int sound_notify2(ToxWindow *self, Notification notif, uint64_t flags, int id); void stop_sound(int id); int box_notify(ToxWindow *self, Notification notif, uint64_t flags, int *id_indicator, const char *title, const char *format, ...); int box_notify2(ToxWindow *self, Notification notif, uint64_t flags, int id, const char *format, ...); int box_silent_notify(ToxWindow *self, uint64_t flags, int *id_indicator, const char *title, const char *format, ...); int box_silent_notify2(ToxWindow *self, uint64_t flags, int id, const char *format, ...); #ifdef SOUND_NOTIFY bool set_sound(Notification sound, const char *value); #endif /* SOUND_NOTIFY */ #endif /* NOTIFY_H */ toxic-0.11.3/src/osx_video.h000066400000000000000000000033051416141666600156750ustar00rootroot00000000000000/* osx_video.h * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef OSX_VIDEO_H #define OSX_VIDEO_H #include #ifdef __OBJC__ #import #import #endif /* __OBJC__ */ #define RELEASE_CHK(func, obj) if ((obj))\ func((obj)); void bgrtoyuv420(uint8_t *plane_y, uint8_t *plane_u, uint8_t *plane_v, uint8_t *rgb, uint16_t width, uint16_t height); #ifdef __OBJC__ @interface OSXVideo : NSObject - (instancetype)initWithDeviceNames: (char **)device_names AmtDevices: (int *)size; @end #endif /* __OBJC__ */ int osx_video_init(char **device_names, int *size); void osx_video_release(void); /* Start device */ int osx_video_open_device(uint32_t selection, uint16_t *width, uint16_t *height); /* Stop device */ void osx_video_close_device(uint32_t device_idx); /* Read data from device */ int osx_video_read_device(uint8_t *y, uint8_t *u, uint8_t *v, uint16_t *width, uint16_t *height); #endif /* OSX_VIDEO_H */ toxic-0.11.3/src/osx_video.m000066400000000000000000000217541416141666600157120ustar00rootroot00000000000000/* osx_video.m * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifdef __OBJC__ #include "osx_video.h" #import #import #include "line_info.h" #include "settings.h" #include #include #include #include #include #include #include /* * Helper video format functions */ static uint8_t rgb_to_y(int r, int g, int b) { int y = ((9798 * r + 19235 * g + 3736 * b) >> 15); return y > 255 ? 255 : y < 0 ? 0 : y; } static uint8_t rgb_to_u(int r, int g, int b) { int u = ((-5538 * r + -10846 * g + 16351 * b) >> 15) + 128; return u > 255 ? 255 : u < 0 ? 0 : u; } static uint8_t rgb_to_v(int r, int g, int b) { int v = ((16351 * r + -13697 * g + -2664 * b) >> 15) + 128; return v > 255 ? 255 : v < 0 ? 0 : v; } void bgrxtoyuv420(uint8_t *plane_y, uint8_t *plane_u, uint8_t *plane_v, uint8_t *rgb, uint16_t width, uint16_t height) { uint16_t x, y; uint8_t *p; uint8_t r, g, b; for (y = 0; y != height; y += 2) { p = rgb; for (x = 0; x != width; x++) { b = *rgb++; g = *rgb++; r = *rgb++; rgb++; *plane_y++ = rgb_to_y(r, g, b); } for (x = 0; x != width / 2; x++) { b = *rgb++; g = *rgb++; r = *rgb++; rgb++; *plane_y++ = rgb_to_y(r, g, b); b = *rgb++; g = *rgb++; r = *rgb++; rgb++; *plane_y++ = rgb_to_y(r, g, b); b = ((int)b + (int) * (rgb - 8) + (int) * p + (int) * (p + 4) + 2) / 4; p++; g = ((int)g + (int) * (rgb - 7) + (int) * p + (int) * (p + 4) + 2) / 4; p++; r = ((int)r + (int) * (rgb - 6) + (int) * p + (int) * (p + 4) + 2) / 4; p++; p++; *plane_u++ = rgb_to_u(r, g, b); *plane_v++ = rgb_to_v(r, g, b); p += 4; } } } /* * End of helper video format functions */ /* * Implementation for OSXVideo */ @implementation OSXVideo { dispatch_queue_t _processingQueue; AVCaptureSession *_session; AVCaptureVideoDataOutput *_linkerVideo; CVImageBufferRef _currentFrame; pthread_mutex_t _frameLock; BOOL _shouldMangleDimensions; } - (instancetype)initWithDeviceNames: (char **)device_names AmtDevices: (int *)size { _session = [[AVCaptureSession alloc] init]; NSArray *devices = [AVCaptureDevice devicesWithMediaType: AVMediaTypeVideo]; int i; for (i = 0; i < [devices count]; ++i) { AVCaptureDevice *device = [devices objectAtIndex: i]; char *video_input_name; NSString *localizedName = [device localizedName]; video_input_name = (char *)malloc(strlen([localizedName cStringUsingEncoding: NSUTF8StringEncoding]) + 1); strcpy(video_input_name, (char *)[localizedName cStringUsingEncoding: NSUTF8StringEncoding]); device_names[i] = video_input_name; } if (i <= 0) { return nil; } *size = i; return self; } - (void)dealloc { pthread_mutex_destroy(&_frameLock); [_session release]; [_linkerVideo release]; dispatch_release(_processingQueue); [super dealloc]; } - (int)openVideoDeviceIndex: (uint32_t)device_idx Width: (uint16_t *)width Height: (uint16_t *)height { pthread_mutex_init(&_frameLock, NULL); pthread_mutex_lock(&_frameLock); _processingQueue = dispatch_queue_create("Toxic processing queue", DISPATCH_QUEUE_SERIAL); NSArray *devices = [AVCaptureDevice devicesWithMediaType: AVMediaTypeVideo]; AVCaptureDevice *device = [devices objectAtIndex: device_idx]; NSError *error = NULL; AVCaptureInput *input = [[AVCaptureDeviceInput alloc] initWithDevice: device error: &error]; if (error != NULL) { [input release]; return -1; } [_session beginConfiguration]; [_session addInput: input]; //_session.sessionPreset = AVCaptureSessionPreset640x480; //*width = 640; //*height = 480; _shouldMangleDimensions = YES; [_session commitConfiguration]; [input release]; [device release]; /* Obtain device resolution */ AVCaptureInputPort *port = [input.ports objectAtIndex: 0]; CMFormatDescriptionRef format_description = port.formatDescription; if (format_description) { CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions(format_description); *width = dimensions.width; *height = dimensions.height; } else { *width = 0; *height = 0; } _linkerVideo = [[AVCaptureVideoDataOutput alloc] init]; [_linkerVideo setSampleBufferDelegate: self queue: _processingQueue]; // TODO possibly get a better pixel format if (_shouldMangleDimensions) { [_linkerVideo setVideoSettings: @ { (id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA), (id)kCVPixelBufferWidthKey: @640, (id)kCVPixelBufferHeightKey: @480 }]; } else { [_linkerVideo setVideoSettings: @ {(id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA)}]; } [_session addOutput: _linkerVideo]; [_session startRunning]; pthread_mutex_unlock(&_frameLock); return 0; } - (void)closeVideoDeviceIndex: (uint32_t)device_idx { NSArray *devices = [AVCaptureDevice devicesWithMediaType: AVMediaTypeVideo]; AVCaptureDevice *device = [devices objectAtIndex: device_idx]; NSError *error = NULL; AVCaptureInput *input = [[AVCaptureDeviceInput alloc] initWithDevice: device error: &error]; [_session stopRunning]; [_session removeOutput: _linkerVideo]; [_session removeInput: input]; [_linkerVideo release]; } - (void)captureOutput: (AVCaptureOutput *)captureOutput didOutputSampleBuffer: (CMSampleBufferRef)sampleBuffer fromConnection: (AVCaptureConnection *)connection { pthread_mutex_lock(&_frameLock); CVImageBufferRef img = CMSampleBufferGetImageBuffer(sampleBuffer); if (!img) { NSLog(@"Toxic WARNING: Bad sampleBuffer from AVfoundation!"); } else { CVPixelBufferUnlockBaseAddress(_currentFrame, kCVPixelBufferLock_ReadOnly); RELEASE_CHK(CFRelease, _currentFrame); _currentFrame = (CVImageBufferRef)CFRetain(img); // we're not going to do anything to it, so it's safe to lock it always CVPixelBufferLockBaseAddress(_currentFrame, kCVPixelBufferLock_ReadOnly); } pthread_mutex_unlock(&_frameLock); } - (int)getVideoFrameY: (uint8_t *)y U: (uint8_t *)u V: (uint8_t *)v Width: (uint16_t *)width Height: (uint16_t *)height { if (!_currentFrame) { return -1; } pthread_mutex_lock(&_frameLock); CFRetain(_currentFrame); CFTypeID imageType = CFGetTypeID(_currentFrame); if (imageType == CVPixelBufferGetTypeID()) { // TODO maybe handle other formats bgrxtoyuv420(y, u, v, CVPixelBufferGetBaseAddress(_currentFrame), *width, *height); } else if (imageType == CVOpenGLBufferGetTypeID()) { // OpenGL pbuffer } else if (imageType == CVOpenGLTextureGetTypeID()) { // OpenGL Texture (Do we need to handle these?) } CVPixelBufferRelease(_currentFrame); pthread_mutex_unlock(&_frameLock); return 0; } @end /* * End of implementation for OSXVideo */ /* * C-interface for OSXVideo */ static OSXVideo *_OSXVideo = nil; int osx_video_init(char **device_names, int *size) { _OSXVideo = [[OSXVideo alloc] initWithDeviceNames: device_names AmtDevices: size]; if (_OSXVideo == nil) { return -1; } return 0; } void osx_video_release() { [_OSXVideo release]; _OSXVideo = nil; } int osx_video_open_device(uint32_t selection, uint16_t *width, uint16_t *height) { if (_OSXVideo == nil) { return -1; } return [_OSXVideo openVideoDeviceIndex: selection Width: width Height: height]; } void osx_video_close_device(uint32_t device_idx) { [_OSXVideo closeVideoDeviceIndex: device_idx]; } int osx_video_read_device(uint8_t *y, uint8_t *u, uint8_t *v, uint16_t *width, uint16_t *height) { if (_OSXVideo == nil) { return -1; } return [_OSXVideo getVideoFrameY: y U: u V: v Width: width Height: height]; } /* * End of C-interface for OSXVideo */ #endif /* __OBJC__ */ toxic-0.11.3/src/prompt.c000066400000000000000000000476271416141666600152310ustar00rootroot00000000000000/* prompt.c * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef _GNU_SOURCE #define _GNU_SOURCE /* needed for wcswidth() */ #endif #include #include #include #include "autocomplete.h" #include "execute.h" #include "friendlist.h" #include "help.h" #include "input.h" #include "line_info.h" #include "log.h" #include "misc_tools.h" #include "notify.h" #include "prompt.h" #include "settings.h" #include "toxic.h" #include "toxic_strings.h" #include "windows.h" extern ToxWindow *prompt; extern struct user_settings *user_settings; extern struct Winthread Winthread; extern FriendsList Friends; FriendRequests FrndRequests; /* Array of global command names used for tab completion. */ static const char *glob_cmd_list[] = { "/accept", "/add", "/avatar", "/clear", "/connect", "/decline", "/exit", "/conference", #ifdef GAMES "/game", #endif "/help", "/log", "/myid", #ifdef QRCODE "/myqr", #endif /* QRCODE */ "/nick", "/note", "/nospam", "/quit", "/requests", "/status", #ifdef AUDIO "/lsdev", "/sdev", #endif /* AUDIO */ #ifdef VIDEO "/lsvdev", "/svdev", #endif /* VIDEO */ #ifdef PYTHON "/run", #endif /* PYTHON */ }; void kill_prompt_window(ToxWindow *self) { ChatContext *ctx = self->chatwin; StatusBar *statusbar = self->stb; log_disable(ctx->log); line_info_cleanup(ctx->hst); delwin(ctx->linewin); delwin(ctx->history); delwin(statusbar->topline); free(ctx->log); free(ctx); free(self->help); free(statusbar); del_window(self); } /* callback: Updates own connection status in prompt statusbar */ void on_self_connection_status(Tox *m, Tox_Connection connection_status, void *userdata) { UNUSED_VAR(m); UNUSED_VAR(userdata); StatusBar *statusbar = prompt->stb; statusbar->connection = connection_status; flag_interface_refresh(); } /* Updates own nick in prompt statusbar */ void prompt_update_nick(ToxWindow *prompt, const char *nick) { StatusBar *statusbar = prompt->stb; snprintf(statusbar->nick, sizeof(statusbar->nick), "%s", nick); statusbar->nick_len = strlen(statusbar->nick); } /* Updates own statusmessage */ void prompt_update_statusmessage(ToxWindow *prompt, Tox *m, const char *statusmsg) { StatusBar *statusbar = prompt->stb; snprintf(statusbar->statusmsg, sizeof(statusbar->statusmsg), "%s", statusmsg); size_t len = strlen(statusbar->statusmsg); statusbar->statusmsg_len = len; Tox_Err_Set_Info err; tox_self_set_status_message(m, (const uint8_t *) statusmsg, len, &err); if (err != TOX_ERR_SET_INFO_OK) { line_info_add(prompt, false, NULL, NULL, SYS_MSG, 0, 0, "Failed to set note (error %d)\n", err); } } /* Updates own status in prompt statusbar */ void prompt_update_status(ToxWindow *prompt, Tox_User_Status status) { StatusBar *statusbar = prompt->stb; statusbar->status = status; } /* Returns our own connection status */ Tox_Connection prompt_selfConnectionStatus(void) { StatusBar *statusbar = prompt->stb; return statusbar->connection; } /* Adds friend request to pending friend requests. Returns request number on success, -1 if queue is full. */ static int add_friend_request(const char *public_key, const char *data) { if (FrndRequests.max_idx >= MAX_FRIEND_REQUESTS) { return -1; } int i; for (i = 0; i <= FrndRequests.max_idx; ++i) { if (!FrndRequests.request[i].active) { FrndRequests.request[i].active = true; memcpy(FrndRequests.request[i].key, public_key, TOX_PUBLIC_KEY_SIZE); snprintf(FrndRequests.request[i].msg, sizeof(FrndRequests.request[i].msg), "%s", data); if (i == FrndRequests.max_idx) { ++FrndRequests.max_idx; } ++FrndRequests.num_requests; return i; } } return -1; } /* * Return true if input is recognized by handler */ static bool prompt_onKey(ToxWindow *self, Tox *m, wint_t key, bool ltr) { ChatContext *ctx = self->chatwin; int x, y, y2, x2; getyx(self->window, y, x); getmaxyx(self->window, y2, x2); UNUSED_VAR(y); if (x2 <= 0 || y2 <= 0) { return false; } if (ctx->pastemode && key == '\r') { key = '\n'; } /* ignore non-menu related input if active */ if (self->help->active) { help_onKey(self, key); return true; } if (ltr || key == '\n') { /* char is printable */ input_new_char(self, key, x, x2); return true; } if (line_info_onKey(self, key)) { return true; } if (input_handle(self, key, x, x2)) { return true; } int input_ret = false; if (key == '\t') { /* TAB key: auto-completes command */ input_ret = true; if (ctx->len > 1 && ctx->line[0] == '/') { int diff = -1; if (wcsncmp(ctx->line, L"/avatar ", wcslen(L"/avatar ")) == 0) { diff = dir_match(self, m, ctx->line, L"/avatar"); } #ifdef PYTHON else if (wcsncmp(ctx->line, L"/run ", wcslen(L"/run ")) == 0) { diff = dir_match(self, m, ctx->line, L"/run"); } #endif else if (wcsncmp(ctx->line, L"/status ", wcslen(L"/status ")) == 0) { const char *status_cmd_list[] = { "online", "away", "busy", }; diff = complete_line(self, status_cmd_list, sizeof(status_cmd_list) / sizeof(char *)); } else { diff = complete_line(self, glob_cmd_list, sizeof(glob_cmd_list) / sizeof(char *)); } if (diff != -1) { if (x + diff > x2 - 1) { int wlen = MAX(0, wcswidth(ctx->line, sizeof(ctx->line) / sizeof(wchar_t))); ctx->start = wlen < x2 ? 0 : wlen - x2 + 1; } } else { sound_notify(self, notif_error, 0, NULL); } } else { sound_notify(self, notif_error, 0, NULL); } } else if (key == '\r') { input_ret = true; rm_trailing_spaces_buf(ctx); if (!wstring_is_empty(ctx->line)) { add_line_to_hist(ctx); wstrsubst(ctx->line, L'¶', L'\n'); char line[MAX_STR_SIZE]; if (wcs_to_mbs_buf(line, ctx->line, MAX_STR_SIZE) == -1) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, RED, " * Failed to parse message."); } else { if (strcmp(line, "/clear") != 0) { line_info_add(self, false, NULL, NULL, PROMPT, 0, 0, "%s", line); } execute(ctx->history, self, m, line, GLOBAL_COMMAND_MODE); } } wclear(ctx->linewin); wmove(self->window, y2, 0); reset_buf(ctx); } return input_ret; } static void prompt_onDraw(ToxWindow *self, Tox *m) { int x2; int y2; getmaxyx(self->window, y2, x2); if (y2 <= 0 || x2 <= 0) { return; } ChatContext *ctx = self->chatwin; pthread_mutex_lock(&Winthread.lock); line_info_print(self); pthread_mutex_unlock(&Winthread.lock); wclear(ctx->linewin); if (ctx->len > 0) { mvwprintw(ctx->linewin, 0, 0, "%ls", &ctx->line[ctx->start]); } curs_set(1); StatusBar *statusbar = self->stb; wmove(statusbar->topline, 0, 0); pthread_mutex_lock(&Winthread.lock); Tox_Connection connection = statusbar->connection; Tox_User_Status status = statusbar->status; pthread_mutex_unlock(&Winthread.lock); wattron(statusbar->topline, COLOR_PAIR(BAR_ACCENT)); wprintw(statusbar->topline, " ["); wattroff(statusbar->topline, COLOR_PAIR(BAR_ACCENT)); switch (connection) { case TOX_CONNECTION_TCP: wattron(statusbar->topline, A_BOLD | COLOR_PAIR(STATUS_ONLINE)); wprintw(statusbar->topline, "TCP"); wattroff(statusbar->topline, A_BOLD | COLOR_PAIR(STATUS_ONLINE)); break; case TOX_CONNECTION_UDP: wattron(statusbar->topline, A_BOLD | COLOR_PAIR(STATUS_ONLINE)); wprintw(statusbar->topline, "UDP"); wattroff(statusbar->topline, A_BOLD | COLOR_PAIR(STATUS_ONLINE)); break; default: wattron(statusbar->topline, COLOR_PAIR(BAR_TEXT)); wprintw(statusbar->topline, "Offline"); wattroff(statusbar->topline, COLOR_PAIR(BAR_TEXT)); break; } wattron(statusbar->topline, COLOR_PAIR(BAR_ACCENT)); wprintw(statusbar->topline, "]"); wattroff(statusbar->topline, COLOR_PAIR(BAR_ACCENT)); if (status != TOX_USER_STATUS_NONE) { int colour = MAGENTA; const char *status_text = "ERROR"; switch (status) { case TOX_USER_STATUS_AWAY: colour = STATUS_AWAY; status_text = "Away"; break; case TOX_USER_STATUS_BUSY: colour = STATUS_BUSY; status_text = "Busy"; break; default: break; } wattron(statusbar->topline, COLOR_PAIR(BAR_ACCENT)); wprintw(statusbar->topline, " ["); wattroff(statusbar->topline, COLOR_PAIR(BAR_ACCENT)); wattron(statusbar->topline, A_BOLD | COLOR_PAIR(colour)); wprintw(statusbar->topline, "%s", status_text); wattroff(statusbar->topline, A_BOLD | COLOR_PAIR(colour)); wattron(statusbar->topline, COLOR_PAIR(BAR_ACCENT)); wprintw(statusbar->topline, "]"); wattroff(statusbar->topline, COLOR_PAIR(BAR_ACCENT)); wattron(statusbar->topline, COLOR_PAIR(BAR_TEXT)); pthread_mutex_lock(&Winthread.lock); wprintw(statusbar->topline, " %s", statusbar->nick); pthread_mutex_unlock(&Winthread.lock); } else { wattron(statusbar->topline, COLOR_PAIR(BAR_TEXT)); pthread_mutex_lock(&Winthread.lock); wprintw(statusbar->topline, " %s", statusbar->nick); pthread_mutex_unlock(&Winthread.lock); } int s_y; int s_x; getyx(statusbar->topline, s_y, s_x); mvwhline(statusbar->topline, s_y, s_x, ' ', x2 - s_x); wattroff(statusbar->topline, COLOR_PAIR(BAR_TEXT)); /* Reset statusbar->statusmsg on window resize */ if (x2 != self->x) { char statusmsg[TOX_MAX_STATUS_MESSAGE_LENGTH]; pthread_mutex_lock(&Winthread.lock); size_t slen = tox_self_get_status_message_size(m); tox_self_get_status_message(m, (uint8_t *) statusmsg); statusmsg[slen] = '\0'; snprintf(statusbar->statusmsg, sizeof(statusbar->statusmsg), "%s", statusmsg); statusbar->statusmsg_len = strlen(statusbar->statusmsg); pthread_mutex_unlock(&Winthread.lock); } self->x = x2; /* Truncate note if it doesn't fit in statusbar */ uint16_t maxlen = x2 - getcurx(statusbar->topline) - 3; pthread_mutex_lock(&Winthread.lock); size_t statusmsg_len = statusbar->statusmsg_len; pthread_mutex_unlock(&Winthread.lock); if (statusmsg_len > maxlen) { pthread_mutex_lock(&Winthread.lock); statusbar->statusmsg[maxlen - 3] = 0; strcat(statusbar->statusmsg, "..."); statusbar->statusmsg_len = maxlen; pthread_mutex_unlock(&Winthread.lock); } if (statusmsg_len) { wattron(statusbar->topline, COLOR_PAIR(BAR_ACCENT)); wprintw(statusbar->topline, " | "); wattroff(statusbar->topline, COLOR_PAIR(BAR_ACCENT)); wattron(statusbar->topline, COLOR_PAIR(BAR_TEXT)); pthread_mutex_lock(&Winthread.lock); wprintw(statusbar->topline, "%s", statusbar->statusmsg); pthread_mutex_unlock(&Winthread.lock); wattroff(statusbar->topline, COLOR_PAIR(BAR_TEXT)); } int y; int x; getyx(self->window, y, x); UNUSED_VAR(x); int new_x = ctx->start ? x2 - 1 : MAX(0, wcswidth(ctx->line, ctx->pos)); wmove(self->window, y, new_x); draw_window_bar(self); wnoutrefresh(self->window); if (self->help->active) { help_onDraw(self); } } static void prompt_onConnectionChange(ToxWindow *self, Tox *m, uint32_t friendnum, Tox_Connection connection_status) { ChatContext *ctx = self->chatwin; char nick[TOX_MAX_NAME_LENGTH] = {0}; /* stop removing this initiation */ get_nick_truncate(m, nick, friendnum); if (!nick[0]) { snprintf(nick, sizeof(nick), "%s", UNKNOWN_NAME); } const char *msg; if (user_settings->show_connection_msg == SHOW_WELCOME_MSG_OFF) { return; } if (connection_status != TOX_CONNECTION_NONE && Friends.list[friendnum].connection_status == TOX_CONNECTION_NONE) { msg = "has come online"; line_info_add(self, true, nick, NULL, CONNECTION, 0, GREEN, msg); write_to_log(msg, nick, ctx->log, true); if (self->active_box != -1) { box_notify2(self, user_log_in, NT_WNDALERT_2 | NT_NOTIFWND | NT_RESTOL, self->active_box, "%s has come online", nick); } else { box_notify(self, user_log_in, NT_WNDALERT_2 | NT_NOTIFWND | NT_RESTOL, &self->active_box, "Toxic", "%s has come online", nick); } } else if (connection_status == TOX_CONNECTION_NONE) { msg = "has gone offline"; line_info_add(self, true, nick, NULL, DISCONNECTION, 0, RED, msg); write_to_log(msg, nick, ctx->log, true); if (self->active_box != -1) { box_notify2(self, user_log_out, NT_WNDALERT_2 | NT_NOTIFWND | NT_RESTOL, self->active_box, "%s has gone offline", nick); } else { box_notify(self, user_log_out, NT_WNDALERT_2 | NT_NOTIFWND | NT_RESTOL, &self->active_box, "Toxic", "%s has gone offline", nick); } } } static void prompt_onFriendRequest(ToxWindow *self, Tox *m, const char *key, const char *data, size_t length) { UNUSED_VAR(m); UNUSED_VAR(length); ChatContext *ctx = self->chatwin; line_info_add(self, true, NULL, NULL, SYS_MSG, 0, 0, "Friend request with the message '%s'", data); write_to_log("Friend request with the message '%s'", "", ctx->log, true); int n = add_friend_request(key, data); if (n == -1) { const char *errmsg = "Friend request queue is full. Discarding request."; line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, errmsg); return; } line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Type \"/accept %d\" or \"/decline %d\"", n, n); sound_notify(self, generic_message, NT_WNDALERT_1 | NT_NOTIFWND, NULL); } void prompt_init_statusbar(ToxWindow *self, Tox *m, bool first_time_run) { int x2, y2; getmaxyx(self->window, y2, x2); if (y2 <= 0 || x2 <= 0) { exit_toxic_err("failed in prompt_init_statusbar", FATALERR_CURSES); } UNUSED_VAR(y2); /* Init statusbar info */ StatusBar *statusbar = self->stb; statusbar->status = TOX_USER_STATUS_NONE; statusbar->connection = TOX_CONNECTION_NONE; char nick[TOX_MAX_NAME_LENGTH]; char statusmsg[TOX_MAX_STATUS_MESSAGE_LENGTH]; size_t n_len = tox_self_get_name_size(m); tox_self_get_name(m, (uint8_t *) nick); size_t s_len = tox_self_get_status_message_size(m); tox_self_get_status_message(m, (uint8_t *) statusmsg); Tox_User_Status status = tox_self_get_status(m); nick[n_len] = '\0'; statusmsg[s_len] = '\0'; if (first_time_run) { snprintf(statusmsg, sizeof(statusmsg), "Toxing on Toxic"); s_len = strlen(statusmsg); statusmsg[s_len] = '\0'; } prompt_update_statusmessage(prompt, m, statusmsg); prompt_update_status(prompt, status); prompt_update_nick(prompt, nick); /* Init statusbar subwindow */ statusbar->topline = subwin(self->window, TOP_BAR_HEIGHT, x2, 0, 0); } static void print_welcome_msg(ToxWindow *self) { line_info_add(self, false, NULL, NULL, SYS_MSG, 1, BLUE, " _____ _____ _____ ____ "); line_info_add(self, false, NULL, NULL, SYS_MSG, 1, BLUE, " |_ _/ _ \\ \\/ /_ _/ ___|"); line_info_add(self, false, NULL, NULL, SYS_MSG, 1, BLUE, " | || | | \\ / | | | "); line_info_add(self, false, NULL, NULL, SYS_MSG, 1, BLUE, " | || |_| / \\ | | |___ "); line_info_add(self, false, NULL, NULL, SYS_MSG, 1, BLUE, " |_| \\___/_/\\_\\___\\____| v." TOXICVER); line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, ""); const char *msg = "Welcome to Toxic, a free, open source Tox-based instant messaging client."; line_info_add(self, false, NULL, NULL, SYS_MSG, 1, CYAN, msg); msg = "Type \"/help\" for assistance. Further help may be found via the man page."; line_info_add(self, false, NULL, NULL, SYS_MSG, 1, CYAN, msg); line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, ""); } static void prompt_init_log(ToxWindow *self, Tox *m, const char *self_name) { ChatContext *ctx = self->chatwin; char myid[TOX_ADDRESS_SIZE]; tox_self_get_address(m, (uint8_t *) myid); if (log_init(ctx->log, self->name, myid, NULL, LOG_TYPE_PROMPT) != 0) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Warning: Log failed to initialize."); return; } if (user_settings->autolog == AUTOLOG_ON) { if (log_enable(ctx->log) == -1) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Warning: Failed to enable log."); } } } static void prompt_onInit(ToxWindow *self, Tox *m) { curs_set(1); int y2; int x2; getmaxyx(self->window, y2, x2); if (y2 <= 0 || x2 <= 0) { exit_toxic_err("failed in prompt_onInit", FATALERR_CURSES); } ChatContext *ctx = self->chatwin; ctx->history = subwin(self->window, y2 - CHATBOX_HEIGHT - WINDOW_BAR_HEIGHT, x2, 0, 0); self->window_bar = subwin(self->window, WINDOW_BAR_HEIGHT, x2, y2 - (CHATBOX_HEIGHT + WINDOW_BAR_HEIGHT), 0); ctx->linewin = subwin(self->window, CHATBOX_HEIGHT, x2, y2 - WINDOW_BAR_HEIGHT, 0); ctx->log = calloc(1, sizeof(struct chatlog)); ctx->hst = calloc(1, sizeof(struct history)); if (ctx->log == NULL || ctx->hst == NULL) { exit_toxic_err("failed in prompt_onInit", FATALERR_MEMORY); } line_info_init(ctx->hst); prompt_init_log(self, m, self->name); scrollok(ctx->history, 0); wmove(self->window, y2 - CURS_Y_OFFSET, 0); if (user_settings->show_welcome_msg == SHOW_WELCOME_MSG_ON) { print_welcome_msg(self); } } ToxWindow *new_prompt(void) { ToxWindow *ret = calloc(1, sizeof(ToxWindow)); if (ret == NULL) { exit_toxic_err("failed in new_prompt", FATALERR_MEMORY); } ret->num = -1; ret->type = WINDOW_TYPE_PROMPT; ret->onKey = &prompt_onKey; ret->onDraw = &prompt_onDraw; ret->onInit = &prompt_onInit; ret->onConnectionChange = &prompt_onConnectionChange; ret->onFriendRequest = &prompt_onFriendRequest; strcpy(ret->name, "Home"); ChatContext *chatwin = calloc(1, sizeof(ChatContext)); StatusBar *stb = calloc(1, sizeof(StatusBar)); Help *help = calloc(1, sizeof(Help)); if (stb == NULL || chatwin == NULL || help == NULL) { exit_toxic_err("failed in new_prompt", FATALERR_MEMORY); } ret->chatwin = chatwin; ret->stb = stb; ret->help = help; ret->active_box = -1; return ret; } toxic-0.11.3/src/prompt.h000066400000000000000000000036451416141666600152260ustar00rootroot00000000000000/* prompt.h * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef PROMPT_H #define PROMPT_H #include "toxic.h" #include "windows.h" #define MAX_FRIEND_REQUESTS 20 struct friend_request { bool active; char msg[TOX_MAX_FRIEND_REQUEST_LENGTH + 1]; uint8_t key[TOX_PUBLIC_KEY_SIZE]; }; typedef struct FriendRequests { int max_idx; int num_requests; struct friend_request request[MAX_FRIEND_REQUESTS]; } FriendRequests; extern ToxWindow *prompt; extern FriendRequests FrndRequests; ToxWindow *new_prompt(void); void prep_prompt_win(void); void prompt_init_statusbar(ToxWindow *self, Tox *m, bool first_time_run); void prompt_update_nick(ToxWindow *prompt, const char *nick); void prompt_update_statusmessage(ToxWindow *prompt, Tox *m, const char *statusmsg); void prompt_update_status(ToxWindow *prompt, Tox_User_Status status); void prompt_update_connectionstatus(ToxWindow *prompt, bool is_connected); void kill_prompt_window(ToxWindow *self); /* callback: Updates own connection status in prompt statusbar */ void on_self_connection_status(Tox *m, Tox_Connection connection_status, void *userdata); /* Returns our own connection status */ Tox_Connection prompt_selfConnectionStatus(void); #endif /* end of include guard: PROMPT_H */ toxic-0.11.3/src/python_api.c000066400000000000000000000225131416141666600160450ustar00rootroot00000000000000/* python_api.c * * * Copyright (C) 2017 Jakob Kreuze * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifdef PYTHON #include #include "api.h" #include "execute.h" extern Tox *user_tox; static struct python_registered_func { char *name; char *help; PyObject *callback; struct python_registered_func *next; } python_commands = {0}; static PyObject *python_api_display(PyObject *self, PyObject *args) { const char *msg; if (!PyArg_ParseTuple(args, "s", &msg)) { return NULL; } api_display(msg); return Py_None; } static PyObject *python_api_get_nick(PyObject *self, PyObject *args) { char *name; PyObject *ret; if (!PyArg_ParseTuple(args, "")) { return NULL; } name = api_get_nick(); if (name == NULL) { return NULL; } ret = Py_BuildValue("s", name); free(name); return ret; } static PyObject *python_api_get_status(PyObject *self, PyObject *args) { PyObject *ret = NULL; if (!PyArg_ParseTuple(args, "")) { return NULL; } switch (api_get_status()) { case TOX_USER_STATUS_NONE: ret = Py_BuildValue("s", "online"); break; case TOX_USER_STATUS_AWAY: ret = Py_BuildValue("s", "away"); break; case TOX_USER_STATUS_BUSY: ret = Py_BuildValue("s", "busy"); break; } return ret; } static PyObject *python_api_get_status_message(PyObject *self, PyObject *args) { char *status; PyObject *ret; if (!PyArg_ParseTuple(args, "")) { return NULL; } status = api_get_status_message(); if (status == NULL) { return NULL; } ret = Py_BuildValue("s", status); free(status); return ret; } static PyObject *python_api_get_all_friends(PyObject *self, PyObject *args) { FriendsList friends; char pubkey_buf[TOX_PUBLIC_KEY_SIZE * 2 + 1]; if (!PyArg_ParseTuple(args, "")) { return NULL; } friends = api_get_friendslist(); PyObject *ret = PyList_New(0); for (size_t i = 0; i < friends.num_friends; i++) { for (size_t ii = 0; ii < TOX_PUBLIC_KEY_SIZE; ii++) { snprintf(pubkey_buf + ii * 2, 3, "%02X", friends.list[i].pub_key[ii] & 0xff); } pubkey_buf[TOX_PUBLIC_KEY_SIZE * 2] = '\0'; PyObject *cur = Py_BuildValue("(s,s)", friends.list[i].name, pubkey_buf); PyList_Append(ret, cur); } return ret; } static PyObject *python_api_send(PyObject *self, PyObject *args) { const char *msg; if (!PyArg_ParseTuple(args, "s", &msg)) { return NULL; } api_send(msg); return Py_None; } static PyObject *python_api_execute(PyObject *self, PyObject *args) { int mode; const char *command; if (!PyArg_ParseTuple(args, "si", &command, &mode)) { return NULL; } api_execute(command, mode); return Py_None; } static PyObject *python_api_register(PyObject *self, PyObject *args) { struct python_registered_func *cur; size_t command_len, help_len; const char *command, *help; PyObject *callback; if (!PyArg_ParseTuple(args, "ssO:register_command", &command, &help, &callback)) { return NULL; } if (!PyCallable_Check(callback)) { PyErr_SetString(PyExc_TypeError, "Calback parameter must be callable"); return NULL; } if (command[0] != '/') { PyErr_SetString(PyExc_TypeError, "Command must be prefixed with a '/'"); return NULL; } for (cur = &python_commands; ; cur = cur->next) { if (cur->name != NULL && !strcmp(command, cur->name)) { Py_XDECREF(cur->callback); Py_XINCREF(callback); cur->callback = callback; break; } if (cur->next == NULL) { Py_XINCREF(callback); cur->next = malloc(sizeof(struct python_registered_func)); if (cur->next == NULL) { return PyErr_NoMemory(); } command_len = strlen(command); cur->next->name = malloc(command_len + 1); if (cur->next->name == NULL) { return PyErr_NoMemory(); } strncpy(cur->next->name, command, command_len + 1); help_len = strlen(help); cur->next->help = malloc(help_len + 1); if (cur->next->help == NULL) { return PyErr_NoMemory(); } strncpy(cur->next->help, help, help_len + 1); cur->next->callback = callback; cur->next->next = NULL; break; } } Py_INCREF(Py_None); return Py_None; } static PyMethodDef ToxicApiMethods[] = { {"display", python_api_display, METH_VARARGS, "Display a message to the current prompt"}, {"get_nick", python_api_get_nick, METH_VARARGS, "Return the user's current nickname"}, {"get_status", python_api_get_status, METH_VARARGS, "Returns the user's current status"}, {"get_status_message", python_api_get_status_message, METH_VARARGS, "Return the user's current status message"}, {"get_all_friends", python_api_get_all_friends, METH_VARARGS, "Return all of the user's friends"}, {"send", python_api_send, METH_VARARGS, "Send the message to the current user"}, {"execute", python_api_execute, METH_VARARGS, "Execute a command like `/nick`"}, {"register", python_api_register, METH_VARARGS, "Register a command like `/nick` to a Python function"}, {NULL, NULL, 0, NULL}, }; static struct PyModuleDef toxic_api_module = { PyModuleDef_HEAD_INIT, "toxic_api", NULL, -1, ToxicApiMethods }; PyMODINIT_FUNC PyInit_toxic_api(void) { PyObject *m = PyModule_Create(&toxic_api_module); PyObject *global_command_const = Py_BuildValue("i", GLOBAL_COMMAND_MODE); PyObject *chat_command_const = Py_BuildValue("i", CHAT_COMMAND_MODE); PyObject *conference_command_const = Py_BuildValue("i", CONFERENCE_COMMAND_MODE); PyObject_SetAttrString(m, "GLOBAL_COMMAND", global_command_const); PyObject_SetAttrString(m, "CHAT_COMMAND", chat_command_const); PyObject_SetAttrString(m, "CONFERENCE_COMMAND", conference_command_const); Py_DECREF(global_command_const); Py_DECREF(chat_command_const); Py_DECREF(conference_command_const); return m; } void terminate_python(void) { if (python_commands.name != NULL) { free(python_commands.name); } struct python_registered_func *cur = NULL; for (cur = python_commands.next; cur != NULL;) { struct python_registered_func *old = cur; cur = cur->next; free(old->name); free(old); } Py_Finalize(); } void init_python(Tox *m) { user_tox = m; PyImport_AppendInittab("toxic_api", PyInit_toxic_api); Py_Initialize(); } void run_python(FILE *fp, char *path) { PyRun_SimpleFile(fp, path); } int do_python_command(int num_args, char (*args)[MAX_STR_SIZE]) { int i; PyObject *callback_args, *args_strings; struct python_registered_func *cur; for (cur = &python_commands; cur != NULL; cur = cur->next) { if (cur->name == NULL) { continue; } if (!strcmp(args[0], cur->name)) { args_strings = PyList_New(0); for (i = 1; i < num_args; i++) { PyList_Append(args_strings, Py_BuildValue("s", args[i])); } callback_args = PyTuple_Pack(1, args_strings); if (PyObject_CallObject(cur->callback, callback_args) == NULL) { api_display("Exception raised in callback function"); } return 0; } } return 1; } int python_num_registered_handlers(void) { int n = 0; struct python_registered_func *cur; for (cur = &python_commands; cur != NULL; cur = cur->next) { if (cur->name != NULL) { n++; } } return n; } int python_help_max_width(void) { size_t tmp; int max = 0; struct python_registered_func *cur; for (cur = &python_commands; cur != NULL; cur = cur->next) { if (cur->name != NULL) { tmp = strlen(cur->help); max = tmp > max ? tmp : max; } } max = max > 50 ? 50 : max; return 37 + max; } void python_draw_handler_help(WINDOW *win) { struct python_registered_func *cur; for (cur = &python_commands; cur != NULL; cur = cur->next) { if (cur->name != NULL) { wprintw(win, " %-29s: %.50s\n", cur->name, cur->help); } } } #endif /* PYTHON */ toxic-0.11.3/src/python_api.h000066400000000000000000000022631416141666600160520ustar00rootroot00000000000000/* python_api.h * * * Copyright (C) 2017 Jakob Kreuze * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef PYTHON_API_H #define PYTHON_API_H #ifdef PYTHON #include #endif /* PYTHON */ PyMODINIT_FUNC PyInit_toxic_api(void); void terminate_python(void); void init_python(Tox *m); void run_python(FILE *fp, char *path); int do_python_command(int num_args, char (*args)[MAX_STR_SIZE]); int python_num_registered_handlers(void); int python_help_max_width(void); void python_draw_handler_help(WINDOW *win); #endif /* PYTHON_API_H */ toxic-0.11.3/src/qr_code.c000066400000000000000000000125311416141666600153060ustar00rootroot00000000000000/* qr_obj_code.c * * * Copyright (C) 2015 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifdef QRCODE #include #include #include #include #include "qr_code.h" #include "toxic.h" #include "windows.h" #ifdef QRPNG #include #define INCHES_PER_METER (100.0/2.54) #define DPI 72 #define SQUARE_SIZE 5 #endif /* QRPNG */ #define BORDER_LEN 1 #define CHAR_1 "\342\226\210" #define CHAR_2 "\342\226\204" #define CHAR_3 "\342\226\200" /* Converts a tox ID string into a QRcode and prints it into the given filename. * * Returns 0 on success. * Returns -1 on failure. */ int ID_to_QRcode_txt(const char *tox_id, const char *outfile) { FILE *fp = fopen(outfile, "wb"); if (fp == NULL) { return -1; } QRcode *qr_obj = QRcode_encodeString(tox_id, 0, QR_ECLEVEL_L, QR_MODE_8, 0); if (qr_obj == NULL) { fclose(fp); return -1; } size_t width = qr_obj->width; size_t i, j; for (i = 0; i < width + BORDER_LEN * 2; ++i) { fprintf(fp, "%s", CHAR_1); } fprintf(fp, "\n"); for (i = 0; i < width; i += 2) { for (j = 0; j < BORDER_LEN; ++j) { fprintf(fp, "%s", CHAR_1); } const unsigned char *row_1 = qr_obj->data + width * i; const unsigned char *row_2 = row_1 + width; for (j = 0; j < width; ++j) { bool x = row_1[j] & 1; bool y = (i + 1) < width ? (row_2[j] & 1) : false; if (x && y) { fprintf(fp, " "); } else if (x) { fprintf(fp, "%s", CHAR_2); } else if (y) { fprintf(fp, "%s", CHAR_3); } else { fprintf(fp, "%s", CHAR_1); } } for (j = 0; j < BORDER_LEN; ++j) { fprintf(fp, "%s", CHAR_1); } fprintf(fp, "\n"); } fclose(fp); QRcode_free(qr_obj); return 0; } #ifdef QRPNG /* Converts a tox ID string into a QRcode and prints it into the given filename as png. * * Returns 0 on success. * Returns -1 on failure. */ int ID_to_QRcode_png(const char *tox_id, const char *outfile) { static FILE *fp; unsigned char *p; unsigned char black[4] = {0, 0, 0, 255}; size_t x, y, xx, yy, real_width; png_structp png_ptr; png_infop info_ptr; fp = fopen(outfile, "wb"); if (fp == NULL) { return -1; } QRcode *qr_obj = QRcode_encodeString(tox_id, 0, QR_ECLEVEL_L, QR_MODE_8, 0); if (qr_obj == NULL) { fclose(fp); return -1; } real_width = (qr_obj->width + BORDER_LEN * 2) * SQUARE_SIZE; size_t row_size = real_width * 4; unsigned char *row = malloc(row_size); if (row == NULL) { fclose(fp); QRcode_free(qr_obj); return -1; } png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); if (png_ptr == NULL) { fclose(fp); free(row); QRcode_free(qr_obj); return -1; } info_ptr = png_create_info_struct(png_ptr); if (info_ptr == NULL) { fclose(fp); free(row); QRcode_free(qr_obj); return -1; } if (setjmp(png_jmpbuf(png_ptr))) { fclose(fp); free(row); QRcode_free(qr_obj); png_destroy_write_struct(&png_ptr, &info_ptr); return -1; } png_init_io(png_ptr, fp); png_set_IHDR(png_ptr, info_ptr, real_width, real_width, 8, PNG_COLOR_TYPE_RGB_ALPHA, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); png_set_pHYs(png_ptr, info_ptr, DPI * INCHES_PER_METER, DPI * INCHES_PER_METER, PNG_RESOLUTION_METER); png_write_info(png_ptr, info_ptr); /* top margin */ memset(row, 0xff, row_size); for (y = 0; y < BORDER_LEN * SQUARE_SIZE; y++) { png_write_row(png_ptr, row); } /* data */ p = qr_obj->data; for (y = 0; y < qr_obj->width; y++) { memset(row, 0xff, row_size); for (x = 0; x < qr_obj->width; x++) { for (xx = 0; xx < SQUARE_SIZE; xx++) { if (*p & 1) { memcpy(&row[((BORDER_LEN + x) * SQUARE_SIZE + xx) * 4], black, 4); } } p++; } for (yy = 0; yy < SQUARE_SIZE; yy++) { png_write_row(png_ptr, row); } } /* bottom margin */ memset(row, 0xff, row_size); for (y = 0; y < BORDER_LEN * SQUARE_SIZE; y++) { png_write_row(png_ptr, row); } free(row); fclose(fp); png_write_end(png_ptr, info_ptr); png_destroy_write_struct(&png_ptr, &info_ptr); QRcode_free(qr_obj); return 0; } #endif /* QRPNG */ #endif /* QRCODE */ toxic-0.11.3/src/qr_code.h000066400000000000000000000025331416141666600153140ustar00rootroot00000000000000/* qr_code.h * * * Copyright (C) 2015 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef QR_CODE #define QR_CODE #ifdef QRCODE #define QRCODE_FILENAME_EXT ".QRcode" /* Converts a tox ID string into a QRcode and prints it into the given filename. * * Returns 0 on success. * Returns -1 on failure. */ int ID_to_QRcode_txt(const char *tox_id, const char *outfile); #ifdef QRPNG #define QRCODE_FILENAME_EXT_PNG ".QRcode.png" /* Converts a tox ID string into a QRcode and prints it into the given filename as png. * * Returns 0 on success. * Returns -1 on failure. */ int ID_to_QRcode_png(const char *tox_id, const char *outfile); #endif /* QRPNG */ #endif /* QRCODE */ #endif /* QR_CODE */ toxic-0.11.3/src/settings.c000066400000000000000000000515131416141666600155350ustar00rootroot00000000000000/* settings.c * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #include #include #include #include #include "configdir.h" #include "misc_tools.h" #include "notify.h" #include "toxic.h" #include "windows.h" #ifdef AUDIO #include "audio_device.h" #endif /* AUDIO */ #include "line_info.h" #include "settings.h" #ifndef PACKAGE_DATADIR #define PACKAGE_DATADIR "." #endif #define NO_SOUND "silent" static struct ui_strings { const char *self; const char *timestamps; const char *time_format; const char *timestamp_format; const char *log_timestamp_format; const char *alerts; const char *bell_on_message; const char *bell_on_filetrans; const char *bell_on_filetrans_accept; const char *bell_on_invite; const char *native_colors; const char *autolog; const char *history_size; const char *notification_timeout; const char *show_typing_self; const char *show_typing_other; const char *show_welcome_msg; const char *show_connection_msg; const char *nodeslist_update_freq; const char *autosave_freq; const char *line_join; const char *line_quit; const char *line_alert; const char *line_normal; const char *mplex_away; const char *mplex_away_note; const char *color_bar_bg; const char *color_bar_fg; const char *color_bar_accent; const char *color_bar_notify; } ui_strings = { "ui", "timestamps", "time_format", "timestamp_format", "log_timestamp_format", "alerts", "bell_on_message", "bell_on_filetrans", "bell_on_filetrans_accept", "bell_on_invite", "native_colors", "autolog", "history_size", "notification_timeout", "show_typing_self", "show_typing_other", "show_welcome_msg", "show_connection_msg", "nodeslist_update_freq", "autosave_freq", "line_join", "line_quit", "line_alert", "line_normal", "mplex_away", "mplex_away_note", "color_bar_bg", "color_bar_fg", "color_bar_accent", "color_bar_notify", }; static void ui_defaults(struct user_settings *settings) { settings->timestamps = TIMESTAMPS_ON; snprintf(settings->timestamp_format, sizeof(settings->timestamp_format), "%s", TIMESTAMP_DEFAULT); snprintf(settings->log_timestamp_format, sizeof(settings->log_timestamp_format), "%s", LOG_TIMESTAMP_DEFAULT); settings->autolog = AUTOLOG_OFF; settings->alerts = ALERTS_ENABLED; settings->bell_on_message = 0; settings->bell_on_filetrans = 0; settings->bell_on_filetrans_accept = 0; settings->bell_on_invite = 0; settings->colour_theme = DFLT_COLS; settings->history_size = 700; settings->notification_timeout = 6000; settings->show_typing_self = SHOW_TYPING_ON; settings->show_typing_other = SHOW_TYPING_ON; settings->show_welcome_msg = SHOW_WELCOME_MSG_ON; settings->show_connection_msg = SHOW_CONNECTION_MSG_ON; settings->nodeslist_update_freq = 7; settings->autosave_freq = 600; snprintf(settings->line_join, LINE_HINT_MAX + 1, "%s", LINE_JOIN); snprintf(settings->line_quit, LINE_HINT_MAX + 1, "%s", LINE_QUIT); snprintf(settings->line_alert, LINE_HINT_MAX + 1, "%s", LINE_ALERT); snprintf(settings->line_normal, LINE_HINT_MAX + 1, "%s", LINE_NORMAL); settings->mplex_away = MPLEX_ON; snprintf(settings->mplex_away_note, sizeof(settings->mplex_away_note), "%s", MPLEX_AWAY_NOTE); } static const struct keys_strings { const char *self; const char *next_tab; const char *prev_tab; const char *scroll_line_up; const char *scroll_line_down; const char *half_page_up; const char *half_page_down; const char *page_bottom; const char *toggle_peerlist; const char *toggle_pastemode; } key_strings = { "keys", "next_tab", "prev_tab", "scroll_line_up", "scroll_line_down", "half_page_up", "half_page_down", "page_bottom", "toggle_peerlist", "toggle_paste_mode", }; /* defines from toxic.h */ static void key_defaults(struct user_settings *settings) { settings->key_next_tab = T_KEY_NEXT; settings->key_prev_tab = T_KEY_PREV; settings->key_scroll_line_up = KEY_PPAGE; settings->key_scroll_line_down = KEY_NPAGE; settings->key_half_page_up = T_KEY_C_F; settings->key_half_page_down = T_KEY_C_V; settings->key_page_bottom = T_KEY_C_H; settings->key_toggle_peerlist = T_KEY_C_B; settings->key_toggle_pastemode = T_KEY_C_T; } static const struct tox_strings { const char *self; const char *download_path; const char *chatlogs_path; const char *avatar_path; const char *autorun_path; const char *password_eval; } tox_strings = { "tox", "download_path", "chatlogs_path", "avatar_path", "autorun_path", "password_eval", }; static void tox_defaults(struct user_settings *settings) { strcpy(settings->download_path, ""); strcpy(settings->chatlogs_path, ""); strcpy(settings->avatar_path, ""); strcpy(settings->autorun_path, ""); strcpy(settings->password_eval, ""); } #ifdef AUDIO static const struct audio_strings { const char *self; const char *input_device; const char *output_device; const char *VAD_threshold; const char *conference_audio_channels; const char *chat_audio_channels; const char *push_to_talk; } audio_strings = { "audio", "input_device", "output_device", "VAD_threshold", "conference_audio_channels", "chat_audio_channels", "push_to_talk", }; static void audio_defaults(struct user_settings *settings) { settings->audio_in_dev = 0; settings->audio_out_dev = 0; settings->VAD_threshold = 5.0; settings->conference_audio_channels = 1; settings->chat_audio_channels = 2; settings->push_to_talk = 0; } #endif #ifdef SOUND_NOTIFY static const struct sound_strings { const char *self; const char *notif_error; const char *self_log_in; const char *self_log_out; const char *user_log_in; const char *user_log_out; const char *call_incoming; const char *call_outgoing; const char *generic_message; const char *transfer_pending; const char *transfer_completed; } sound_strings = { "sounds", "notif_error", "self_log_in", "self_log_out", "user_log_in", "user_log_out", "call_incoming", "call_outgoing", "generic_message", "transfer_pending", "transfer_completed", }; #endif static int key_parse(const char **bind) { int len = strlen(*bind); if (len > 5) { if (strncasecmp(*bind, "ctrl+", 5) == 0 && toupper(bind[0][5]) != 'M') { /* ctrl+m cannot be used */ return toupper(bind[0][5]) - 'A' + 1; } } if (strncasecmp(*bind, "tab", 3) == 0) { return T_KEY_TAB; } if (strncasecmp(*bind, "page", 4) == 0) { return len == 6 ? KEY_PPAGE : KEY_NPAGE; } return -1; } static void set_key_binding(int *key, const char **bind) { int code = key_parse(bind); if (code != -1) { *key = code; } } int settings_load(struct user_settings *s, const char *patharg) { config_t cfg[1]; config_setting_t *setting; const char *str = NULL; /* Load default settings */ ui_defaults(s); tox_defaults(s); key_defaults(s); #ifdef AUDIO audio_defaults(s); #endif config_init(cfg); char path[MAX_STR_SIZE]; /* use default config file path */ if (patharg == NULL) { char *user_config_dir = get_user_config_dir(); snprintf(path, sizeof(path), "%s%stoxic.conf", user_config_dir, CONFIGDIR); free(user_config_dir); /* make sure path exists or is created on first time running */ if (!file_exists(path)) { FILE *fp = fopen(path, "w"); if (fp == NULL) { return -1; } fclose(fp); } } else { snprintf(path, sizeof(path), "%s", patharg); } if (!config_read_file(cfg, path)) { config_destroy(cfg); return -1; } /* ui */ if ((setting = config_lookup(cfg, ui_strings.self)) != NULL) { config_setting_lookup_bool(setting, ui_strings.timestamps, &s->timestamps); int time = 24; if (config_setting_lookup_int(setting, ui_strings.time_format, &time)) { if (time == 12) { snprintf(s->timestamp_format, sizeof(s->timestamp_format), "%s", "%I:%M %p"); snprintf(s->log_timestamp_format, sizeof(s->log_timestamp_format), "%s", "%Y/%m/%d [%I:%M:%S %p]"); } } if (config_setting_lookup_string(setting, ui_strings.timestamp_format, &str)) { snprintf(s->timestamp_format, sizeof(s->timestamp_format), "%s", str); } if (config_setting_lookup_string(setting, ui_strings.color_bar_bg, &str)) { snprintf(s->color_bar_bg, sizeof(s->color_bar_bg), "%s", str); } if (config_setting_lookup_string(setting, ui_strings.color_bar_fg, &str)) { snprintf(s->color_bar_fg, sizeof(s->color_bar_fg), "%s", str); } if (config_setting_lookup_string(setting, ui_strings.color_bar_accent, &str)) { snprintf(s->color_bar_accent, sizeof(s->color_bar_accent), "%s", str); } if (config_setting_lookup_string(setting, ui_strings.color_bar_notify, &str)) { snprintf(s->color_bar_notify, sizeof(s->color_bar_notify), "%s", str); } if (config_setting_lookup_string(setting, ui_strings.log_timestamp_format, &str)) { snprintf(s->log_timestamp_format, sizeof(s->log_timestamp_format), "%s", str); } config_setting_lookup_bool(setting, ui_strings.alerts, &s->alerts); if (config_setting_lookup_bool(setting, ui_strings.bell_on_message, &s->bell_on_message)) { s->bell_on_message = s->bell_on_message ? NT_BEEP : 0; } if (config_setting_lookup_bool(setting, ui_strings.bell_on_filetrans, &s->bell_on_filetrans)) { s->bell_on_filetrans = s->bell_on_filetrans ? NT_BEEP : 0; } if (config_setting_lookup_bool(setting, ui_strings.bell_on_filetrans_accept, &s->bell_on_filetrans_accept)) { s->bell_on_filetrans_accept = s->bell_on_filetrans_accept ? NT_BEEP : 0; } if (config_setting_lookup_bool(setting, ui_strings.bell_on_invite, &s->bell_on_invite)) { s->bell_on_invite = s->bell_on_invite ? NT_BEEP : 0; } config_setting_lookup_bool(setting, ui_strings.autolog, &s->autolog); config_setting_lookup_bool(setting, ui_strings.native_colors, &s->colour_theme); config_setting_lookup_bool(setting, ui_strings.show_typing_self, &s->show_typing_self); config_setting_lookup_bool(setting, ui_strings.show_typing_other, &s->show_typing_other); config_setting_lookup_bool(setting, ui_strings.show_welcome_msg, &s->show_welcome_msg); config_setting_lookup_bool(setting, ui_strings.show_connection_msg, &s->show_connection_msg); config_setting_lookup_int(setting, ui_strings.history_size, &s->history_size); config_setting_lookup_int(setting, ui_strings.notification_timeout, &s->notification_timeout); config_setting_lookup_int(setting, ui_strings.nodeslist_update_freq, &s->nodeslist_update_freq); config_setting_lookup_int(setting, ui_strings.autosave_freq, &s->autosave_freq); if (config_setting_lookup_string(setting, ui_strings.line_join, &str)) { snprintf(s->line_join, sizeof(s->line_join), "%s", str); } if (config_setting_lookup_string(setting, ui_strings.line_quit, &str)) { snprintf(s->line_quit, sizeof(s->line_quit), "%s", str); } if (config_setting_lookup_string(setting, ui_strings.line_alert, &str)) { snprintf(s->line_alert, sizeof(s->line_alert), "%s", str); } if (config_setting_lookup_string(setting, ui_strings.line_normal, &str)) { snprintf(s->line_normal, sizeof(s->line_normal), "%s", str); } config_setting_lookup_bool(setting, ui_strings.mplex_away, &s->mplex_away); if (config_setting_lookup_string(setting, ui_strings.mplex_away_note, &str)) { snprintf(s->mplex_away_note, sizeof(s->mplex_away_note), "%s", str); } } /* paths */ if ((setting = config_lookup(cfg, tox_strings.self)) != NULL) { if (config_setting_lookup_string(setting, tox_strings.download_path, &str)) { snprintf(s->download_path, sizeof(s->download_path), "%s", str); int len = strlen(s->download_path); /* make sure path ends with a '/' */ if (len >= sizeof(s->download_path) - 2) { s->download_path[0] = '\0'; } else if (s->download_path[len - 1] != '/') { strcat(&s->download_path[len - 1], "/"); } } if (config_setting_lookup_string(setting, tox_strings.chatlogs_path, &str)) { snprintf(s->chatlogs_path, sizeof(s->chatlogs_path), "%s", str); int len = strlen(s->chatlogs_path); if (len >= sizeof(s->chatlogs_path) - 2) { s->chatlogs_path[0] = '\0'; } else if (s->chatlogs_path[len - 1] != '/') { strcat(&s->chatlogs_path[len - 1], "/"); } } if (config_setting_lookup_string(setting, tox_strings.avatar_path, &str)) { snprintf(s->avatar_path, sizeof(s->avatar_path), "%s", str); int len = strlen(str); if (len >= sizeof(s->avatar_path)) { s->avatar_path[0] = '\0'; } } #ifdef PYTHON if (config_setting_lookup_string(setting, tox_strings.autorun_path, &str)) { snprintf(s->autorun_path, sizeof(s->autorun_path), "%s", str); int len = strlen(str); if (len >= sizeof(s->autorun_path) - 2) { s->autorun_path[0] = '\0'; } else if (s->autorun_path[len - 1] != '/') { strcat(&s->autorun_path[len - 1], "/"); } } #endif if (config_setting_lookup_string(setting, tox_strings.password_eval, &str)) { snprintf(s->password_eval, sizeof(s->password_eval), "%s", str); int len = strlen(str); if (len >= sizeof(s->password_eval)) { s->password_eval[0] = '\0'; } } } /* keys */ if ((setting = config_lookup(cfg, key_strings.self)) != NULL) { const char *tmp = NULL; if (config_setting_lookup_string(setting, key_strings.next_tab, &tmp)) { set_key_binding(&s->key_next_tab, &tmp); } if (config_setting_lookup_string(setting, key_strings.prev_tab, &tmp)) { set_key_binding(&s->key_prev_tab, &tmp); } if (config_setting_lookup_string(setting, key_strings.scroll_line_up, &tmp)) { set_key_binding(&s->key_scroll_line_up, &tmp); } if (config_setting_lookup_string(setting, key_strings.scroll_line_down, &tmp)) { set_key_binding(&s->key_scroll_line_down, &tmp); } if (config_setting_lookup_string(setting, key_strings.half_page_up, &tmp)) { set_key_binding(&s->key_half_page_up, &tmp); } if (config_setting_lookup_string(setting, key_strings.half_page_down, &tmp)) { set_key_binding(&s->key_half_page_down, &tmp); } if (config_setting_lookup_string(setting, key_strings.page_bottom, &tmp)) { set_key_binding(&s->key_page_bottom, &tmp); } if (config_setting_lookup_string(setting, key_strings.toggle_peerlist, &tmp)) { set_key_binding(&s->key_toggle_peerlist, &tmp); } if (config_setting_lookup_string(setting, key_strings.toggle_pastemode, &tmp)) { set_key_binding(&s->key_toggle_pastemode, &tmp); } } #ifdef AUDIO if ((setting = config_lookup(cfg, audio_strings.self)) != NULL) { config_setting_lookup_int(setting, audio_strings.input_device, &s->audio_in_dev); s->audio_in_dev = s->audio_in_dev < 0 || s->audio_in_dev > MAX_DEVICES ? 0 : s->audio_in_dev; config_setting_lookup_int(setting, audio_strings.output_device, &s->audio_out_dev); s->audio_out_dev = s->audio_out_dev < 0 || s->audio_out_dev > MAX_DEVICES ? 0 : s->audio_out_dev; config_setting_lookup_float(setting, audio_strings.VAD_threshold, &s->VAD_threshold); config_setting_lookup_int(setting, audio_strings.conference_audio_channels, &s->conference_audio_channels); s->conference_audio_channels = s->conference_audio_channels <= 0 || s->conference_audio_channels > 2 ? 1 : s->conference_audio_channels; config_setting_lookup_int(setting, audio_strings.chat_audio_channels, &s->chat_audio_channels); s->chat_audio_channels = s->chat_audio_channels <= 0 || s->chat_audio_channels > 2 ? 2 : s->chat_audio_channels; config_setting_lookup_bool(setting, audio_strings.push_to_talk, &s->push_to_talk); } #endif #ifdef SOUND_NOTIFY if ((setting = config_lookup(cfg, sound_strings.self)) != NULL) { if ((config_setting_lookup_string(setting, sound_strings.notif_error, &str) != CONFIG_TRUE) || !set_sound(notif_error, str)) { if (str && strcasecmp(str, NO_SOUND) != 0) { set_sound(notif_error, PACKAGE_DATADIR "/sounds/ToxicError.wav"); } } if (!config_setting_lookup_string(setting, sound_strings.user_log_in, &str) || !set_sound(user_log_in, str)) { if (str && strcasecmp(str, NO_SOUND) != 0) { set_sound(user_log_in, PACKAGE_DATADIR "/sounds/ToxicContactOnline.wav"); } } if (!config_setting_lookup_string(setting, sound_strings.user_log_out, &str) || !set_sound(user_log_out, str)) { if (str && strcasecmp(str, NO_SOUND) != 0) { set_sound(user_log_out, PACKAGE_DATADIR "/sounds/ToxicContactOffline.wav"); } } if (!config_setting_lookup_string(setting, sound_strings.call_incoming, &str) || !set_sound(call_incoming, str)) { if (str && strcasecmp(str, NO_SOUND) != 0) { set_sound(call_incoming, PACKAGE_DATADIR "/sounds/ToxicIncomingCall.wav"); } } if (!config_setting_lookup_string(setting, sound_strings.call_outgoing, &str) || !set_sound(call_outgoing, str)) { if (str && strcasecmp(str, NO_SOUND) != 0) { set_sound(call_outgoing, PACKAGE_DATADIR "/sounds/ToxicOutgoingCall.wav"); } } if (!config_setting_lookup_string(setting, sound_strings.generic_message, &str) || !set_sound(generic_message, str)) { if (str && strcasecmp(str, NO_SOUND) != 0) { set_sound(generic_message, PACKAGE_DATADIR "/sounds/ToxicRecvMessage.wav"); } } if (!config_setting_lookup_string(setting, sound_strings.transfer_pending, &str) || !set_sound(transfer_pending, str)) { if (str && strcasecmp(str, NO_SOUND) != 0) { set_sound(transfer_pending, PACKAGE_DATADIR "/sounds/ToxicTransferStart.wav"); } } if (!config_setting_lookup_string(setting, sound_strings.transfer_completed, &str) || !set_sound(transfer_completed, str)) { if (str && strcasecmp(str, NO_SOUND) != 0) { set_sound(transfer_completed, PACKAGE_DATADIR "/sounds/ToxicTransferComplete.wav"); } } } else { set_sound(notif_error, PACKAGE_DATADIR "/sounds/ToxicError.wav"); set_sound(user_log_in, PACKAGE_DATADIR "/sounds/ToxicContactOnline.wav"); set_sound(user_log_out, PACKAGE_DATADIR "/sounds/ToxicContactOffline.wav"); set_sound(call_incoming, PACKAGE_DATADIR "/sounds/ToxicIncomingCall.wav"); set_sound(call_outgoing, PACKAGE_DATADIR "/sounds/ToxicOutgoingCall.wav"); set_sound(generic_message, PACKAGE_DATADIR "/sounds/ToxicRecvMessage.wav"); set_sound(transfer_pending, PACKAGE_DATADIR "/sounds/ToxicTransferStart.wav"); set_sound(transfer_completed, PACKAGE_DATADIR "/sounds/ToxicTransferComplete.wav"); } #endif config_destroy(cfg); return 0; } toxic-0.11.3/src/settings.h000066400000000000000000000073171416141666600155450ustar00rootroot00000000000000/* settings.h * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef SETTINGS_H #define SETTINGS_H #include #include /* Represents line_* hints max strlen */ #define LINE_HINT_MAX 3 #define PASSWORD_EVAL_MAX 512 /* Holds user setting values defined in the toxic config file. */ struct user_settings { int autolog; /* boolean */ int alerts; /* boolean */ /* boolean (is set to NT_BEEP or 0 after loading) */ int bell_on_message; int bell_on_filetrans; int bell_on_filetrans_accept; int bell_on_invite; int timestamps; /* boolean */ char timestamp_format[TIME_STR_SIZE]; char log_timestamp_format[TIME_STR_SIZE]; int colour_theme; /* boolean (0 for default toxic colours) */ int history_size; /* int between MIN_HISTORY and MAX_HISTORY */ int notification_timeout; int show_typing_self; /* boolean */ int show_typing_other; /* boolean */ int show_welcome_msg; /* boolean */ int show_connection_msg; /* boolean */ int nodeslist_update_freq; /* int (<= 0 to disable updates) */ int autosave_freq; /* int (<= 0 to disable autosave) */ char line_join[LINE_HINT_MAX + 1]; char line_quit[LINE_HINT_MAX + 1]; char line_alert[LINE_HINT_MAX + 1]; char line_normal[LINE_HINT_MAX + 1]; char download_path[PATH_MAX]; char chatlogs_path[PATH_MAX]; char avatar_path[PATH_MAX]; char autorun_path[PATH_MAX]; char password_eval[PASSWORD_EVAL_MAX]; char color_bar_bg[COLOR_STR_SIZE]; char color_bar_fg[COLOR_STR_SIZE]; char color_bar_accent[COLOR_STR_SIZE]; char color_bar_notify[COLOR_STR_SIZE]; int key_next_tab; int key_prev_tab; int key_scroll_line_up; int key_scroll_line_down; int key_half_page_up; int key_half_page_down; int key_page_bottom; int key_toggle_peerlist; int key_toggle_pastemode; int mplex_away; /* boolean (1 for reaction to terminal attach/detach) */ char mplex_away_note [TOX_MAX_STATUS_MESSAGE_LENGTH]; #ifdef AUDIO int audio_in_dev; int audio_out_dev; double VAD_threshold; int conference_audio_channels; int chat_audio_channels; int push_to_talk; /* boolean */ #endif }; extern struct user_settings *user_settings; enum settings_values { AUTOLOG_OFF = 0, AUTOLOG_ON = 1, TIMESTAMPS_OFF = 0, TIMESTAMPS_ON = 1, ALERTS_DISABLED = 0, ALERTS_ENABLED = 1, DFLT_COLS = 0, NATIVE_COLS = 1, SHOW_TYPING_OFF = 0, SHOW_TYPING_ON = 1, SHOW_WELCOME_MSG_OFF = 0, SHOW_WELCOME_MSG_ON = 1, SHOW_CONNECTION_MSG_OFF = 0, SHOW_CONNECTION_MSG_ON = 1, DFLT_HST_SIZE = 700, MPLEX_OFF = 0, MPLEX_ON = 1, }; #define LINE_JOIN "-->" #define LINE_QUIT "<--" #define LINE_ALERT "-!-" #define LINE_NORMAL "-" #define TIMESTAMP_DEFAULT "%H:%M" #define LOG_TIMESTAMP_DEFAULT "%Y/%m/%d [%H:%M:%S]" #define MPLEX_AWAY_NOTE "Away from keyboard, be back soon!" int settings_load(struct user_settings *s, const char *patharg); #endif /* SETTINGS_H */ toxic-0.11.3/src/term_mplex.c000066400000000000000000000246731416141666600160600ustar00rootroot00000000000000/* term_mplex.c * * * Copyright (C) 2015 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #include /* PATH_MAX */ #include /* fgets, popen, pclose */ #include /* malloc, realloc, free, getenv */ #include /* strlen, strcpy, strstr, strchr, strrchr, strcat, strncmp */ #include #include #include #include #include #include #include "execute.h" #include "settings.h" #include "term_mplex.h" #include "toxic.h" #include "windows.h" extern struct ToxWindow *prompt; extern struct user_settings *user_settings; extern struct Winthread Winthread; #if defined(PATH_MAX) && PATH_MAX > 512 #define BUFFER_SIZE PATH_MAX #else #define BUFFER_SIZE 512 #endif #define PATH_SEP_S "/" #define PATH_SEP_C '/' typedef enum { MPLEX_NONE, MPLEX_SCREEN, MPLEX_TMUX, } mplex_status; /* used for: - storing screen socket name - storing tmux session number in string form */ static char mplex_data [BUFFER_SIZE]; static char buffer [BUFFER_SIZE]; /* Differentiates between mplex auto-away and manual-away */ static bool auto_away_active = false; static mplex_status mplex = MPLEX_NONE; static Tox_User_Status prev_status = TOX_USER_STATUS_NONE; static char prev_note [TOX_MAX_STATUS_MESSAGE_LENGTH] = ""; /* mutex for access to status data, for sync between: - user command /status from ncurses thread - auto-away POSIX timer, which runs from a separate thread after init, should be accessed only by cmd_status() */ static pthread_mutex_t status_lock; static pthread_t mplex_tid; void lock_status(void) { pthread_mutex_lock(&status_lock); } void unlock_status(void) { pthread_mutex_unlock(&status_lock); } static char *read_into_dyn_buffer(FILE *stream) { const char *input_ptr = NULL; char *dyn_buffer = NULL; int dyn_buffer_size = 1; /* account for the \0 */ while ((input_ptr = fgets(buffer, BUFFER_SIZE, stream)) != NULL) { int length = dyn_buffer_size + strlen(input_ptr); if (dyn_buffer) { char *tmp = realloc(dyn_buffer, length); if (tmp == NULL) { return NULL; } dyn_buffer = tmp; } else { dyn_buffer = malloc(length); if (dyn_buffer == NULL) { return NULL; } } strcpy(dyn_buffer + dyn_buffer_size - 1, input_ptr); dyn_buffer_size = length; } return dyn_buffer; } static char *extract_socket_path(const char *info) { const char *search_str = " Socket"; const char *pos = strstr(info, search_str); char *end = NULL; char *path = NULL; if (!pos) { return NULL; } pos += strlen(search_str); pos = strchr(pos, PATH_SEP_C); if (!pos) { return NULL; } end = strchr(pos, '\n'); if (!end) { return NULL; } *end = '\0'; end = strrchr(pos, '.'); if (!end) { return NULL; } path = malloc(end - pos + 1); if (path == NULL) { return NULL; } *end = '\0'; return strcpy(path, pos); } static int detect_gnu_screen(void) { FILE *session_info_stream = NULL; char *socket_name = NULL, *socket_path = NULL; char *dyn_buffer = NULL; socket_name = getenv("STY"); if (!socket_name) { goto nomplex; } session_info_stream = popen("env LC_ALL=C screen -ls", "r"); if (!session_info_stream) { goto nomplex; } dyn_buffer = read_into_dyn_buffer(session_info_stream); if (!dyn_buffer) { goto nomplex; } pclose(session_info_stream); session_info_stream = NULL; socket_path = extract_socket_path(dyn_buffer); if (!socket_path) { goto nomplex; } free(dyn_buffer); dyn_buffer = NULL; if (strlen(socket_path) + strlen(PATH_SEP_S) + strlen(socket_name) >= sizeof(mplex_data)) { goto nomplex; } strcpy(mplex_data, socket_path); strcat(mplex_data, PATH_SEP_S); strcat(mplex_data, socket_name); free(socket_path); socket_path = NULL; mplex = MPLEX_SCREEN; return 1; nomplex: if (session_info_stream) { pclose(session_info_stream); } if (dyn_buffer) { free(dyn_buffer); } if (socket_path) { free(socket_path); } return 0; } static int detect_tmux(void) { char *tmux_env = getenv("TMUX"), *pos; if (!tmux_env) { return 0; } /* find second separator */ pos = strrchr(tmux_env, ','); if (!pos) { return 0; } /* store the session id for later use */ snprintf(mplex_data, sizeof(mplex_data), "$%s", pos + 1); mplex = MPLEX_TMUX; return 1; } /* Checks whether a terminal multiplexer (mplex) is present, and finds its unix socket. GNU screen and tmux are supported. Returns 1 if present, 0 otherwise. This value can be used to determine whether an auto-away detection timer is needed. */ static int detect_mplex(void) { /* try screen, and if fails try tmux */ return detect_gnu_screen() || detect_tmux(); } /* Detects gnu screen session attached/detached by examining permissions of the session's unix socket. */ static int gnu_screen_is_detached(void) { if (mplex != MPLEX_SCREEN) { return 0; } struct stat sb; if (stat(mplex_data, &sb) != 0) { return 0; } /* execution permission (x) means attached */ return !(sb.st_mode & S_IXUSR); } /* Detects tmux attached/detached by getting session data and finding the current session's entry. */ static int tmux_is_detached(void) { if (mplex != MPLEX_TMUX) { return 0; } FILE *session_info_stream = NULL; char *dyn_buffer = NULL, *search_str = NULL; char *entry_pos; int detached; const int numstr_len = strlen(mplex_data); /* get the number of attached clients for each session */ session_info_stream = popen("tmux list-sessions -F \"#{session_id} #{session_attached}\"", "r"); if (!session_info_stream) { goto fail; } dyn_buffer = read_into_dyn_buffer(session_info_stream); if (!dyn_buffer) { goto fail; } pclose(session_info_stream); session_info_stream = NULL; /* prepare search string, for finding the current session's entry */ search_str = malloc(numstr_len + 2); if (search_str == NULL) { goto fail; } search_str[0] = '\n'; strcpy(search_str + 1, mplex_data); /* do the search */ if (strncmp(dyn_buffer, search_str + 1, numstr_len) == 0) { entry_pos = dyn_buffer; } else { entry_pos = strstr(dyn_buffer, search_str); } if (! entry_pos) { goto fail; } entry_pos = strchr(entry_pos, ' ') + 1; detached = strncmp(entry_pos, "0\n", 2) == 0; free(search_str); search_str = NULL; free(dyn_buffer); dyn_buffer = NULL; return detached; fail: if (session_info_stream) { pclose(session_info_stream); } if (dyn_buffer) { free(dyn_buffer); } if (search_str) { free(search_str); } return 0; } /* Checks whether there is a terminal multiplexer present, but in detached state. Returns 1 if detached, 0 if attached or if there is no terminal multiplexer. If detect_mplex_socket() failed to find a mplex, there is no need to call this function. If it did find one, this function can be used to periodically sample its state and update away status according to attached/detached state of the mplex. */ static int mplex_is_detached(void) { return gnu_screen_is_detached() || tmux_is_detached(); } static void mplex_timer_handler(Tox *m) { Tox_User_Status current_status, new_status; const char *new_note; if (mplex == MPLEX_NONE) { return; } int detached = mplex_is_detached(); pthread_mutex_lock(&Winthread.lock); current_status = tox_self_get_status(m); pthread_mutex_unlock(&Winthread.lock); if (auto_away_active && current_status == TOX_USER_STATUS_AWAY && !detached) { auto_away_active = false; new_status = prev_status; new_note = prev_note; } else if (current_status == TOX_USER_STATUS_NONE && detached) { auto_away_active = true; prev_status = current_status; new_status = TOX_USER_STATUS_AWAY; pthread_mutex_lock(&Winthread.lock); size_t slen = tox_self_get_status_message_size(m); tox_self_get_status_message(m, (uint8_t *) prev_note); prev_note[slen] = '\0'; pthread_mutex_unlock(&Winthread.lock); new_note = user_settings->mplex_away_note; } else { return; } char status_str[MAX_STR_SIZE]; char note_str[MAX_STR_SIZE]; const char *status = new_status == TOX_USER_STATUS_AWAY ? "away" : new_status == TOX_USER_STATUS_BUSY ? "busy" : "online"; snprintf(status_str, sizeof(status_str), "/status %s", status); snprintf(note_str, sizeof(status_str), "/note %s", new_note); pthread_mutex_lock(&Winthread.lock); execute(prompt->chatwin->history, prompt, m, status_str, GLOBAL_COMMAND_MODE); execute(prompt->chatwin->history, prompt, m, note_str, GLOBAL_COMMAND_MODE); pthread_mutex_unlock(&Winthread.lock); } /* Time in seconds between calls to mplex_timer_handler */ #define MPLEX_TIMER_INTERVAL 5 void *mplex_timer_thread(void *data) { Tox *m = (Tox *) data; while (true) { sleep(MPLEX_TIMER_INTERVAL); mplex_timer_handler(m); } } int init_mplex_away_timer(Tox *m) { if (! detect_mplex()) { return 0; } if (! user_settings->mplex_away) { return 0; } /* status access mutex */ if (pthread_mutex_init(&status_lock, NULL) != 0) { return -1; } if (pthread_create(&mplex_tid, NULL, mplex_timer_thread, (void *) m) != 0) { return -1; } return 0; } toxic-0.11.3/src/term_mplex.h000066400000000000000000000021561416141666600160550ustar00rootroot00000000000000/* term_mplex.h * * * Copyright (C) 2015 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef TERM_MPLEX_H #define TERM_MPLEX_H /* Checks if Toxic runs inside a terminal multiplexer (GNU screen or tmux). If * yes, it initializes a timer which periodically checks the attached/detached * state of the terminal and updates away status accordingly. */ int init_mplex_away_timer(Tox *m); void lock_status(void); void unlock_status(void); #endif /* TERM_MPLEX_H */ toxic-0.11.3/src/toxic.c000066400000000000000000001416471416141666600150330ustar00rootroot00000000000000/* toxic.c * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "audio_device.h" #include "bootstrap.h" #include "conference.h" #include "configdir.h" #include "execute.h" #include "file_transfers.h" #include "friendlist.h" #include "line_info.h" #include "log.h" #include "message_queue.h" #include "misc_tools.h" #include "name_lookup.h" #include "notify.h" #include "prompt.h" #include "settings.h" #include "term_mplex.h" #include "toxic.h" #include "windows.h" #ifdef X11 #include "x11focus.h" #endif #ifdef AUDIO #include "audio_call.h" #ifdef VIDEO #include "video_call.h" #endif /* VIDEO */ static ToxAV *av; #endif /* AUDIO */ #ifdef PYTHON #include "api.h" #include "python_api.h" #endif #ifndef PACKAGE_DATADIR #define PACKAGE_DATADIR "." #endif /* Export for use in Callbacks */ char *DATA_FILE = NULL; char *BLOCK_FILE = NULL; ToxWindow *prompt = NULL; #define DATANAME "toxic_profile.tox" #define BLOCKNAME "toxic_blocklist" #define MIN_PASSWORD_LEN 6 #define MAX_PASSWORD_LEN 64 struct Winthread Winthread; static struct cqueue_thread cqueue_thread; struct arg_opts arg_opts; #ifdef AUDIO static struct av_thread av_thread; #endif // This struct is not thread safe. It should only ever be written to from the main thread // before any other thread that uses it is initialized. struct user_settings *user_settings = NULL; static struct user_password { bool data_is_encrypted; char pass[MAX_PASSWORD_LEN + 1]; int len; } user_password; static time_t last_signal_time; static void catch_SIGINT(int sig) { UNUSED_VAR(sig); time_t cur_time = get_unix_time(); if (difftime(cur_time, last_signal_time) <= 1) { Winthread.sig_exit_toxic = 1; } else { last_signal_time = cur_time; } } static void catch_SIGSEGV(int sig) { UNUSED_VAR(sig); if (freopen("/dev/tty", "w", stderr)) { // make sure stderr is enabled since we may have disabled it fprintf(stderr, "Caught SIGSEGV: Aborting toxic session.\n"); } endwin(); exit(EXIT_FAILURE); } static void flag_window_resize(int sig) { UNUSED_VAR(sig); Winthread.flag_resize = 1; } static void init_signal_catchers(void) { signal(SIGWINCH, flag_window_resize); signal(SIGINT, catch_SIGINT); signal(SIGSEGV, catch_SIGSEGV); } void free_global_data(void) { if (DATA_FILE) { free(DATA_FILE); DATA_FILE = NULL; } if (BLOCK_FILE) { free(BLOCK_FILE); BLOCK_FILE = NULL; } if (user_settings) { free(user_settings); user_settings = NULL; } } void exit_toxic_success(Tox *m) { store_data(m, DATA_FILE); user_password = (struct user_password) { 0 }; terminate_notify(); kill_all_file_transfers(m); kill_all_windows(m); #ifdef AUDIO #ifdef VIDEO terminate_video(); #endif /* VIDEO */ terminate_audio(); #endif /* AUDIO */ #ifdef PYTHON terminate_python(); #endif /* PYTHON */ free_global_data(); tox_kill(m); if (arg_opts.log_fp != NULL) { fclose(arg_opts.log_fp); arg_opts.log_fp = NULL; } endwin(); curl_global_cleanup(); #ifdef X11 terminate_x11focus(); #endif /* X11 */ exit(EXIT_SUCCESS); } void exit_toxic_err(const char *errmsg, int errcode) { free_global_data(); if (freopen("/dev/tty", "w", stderr)) { fprintf(stderr, "Toxic session aborted with error code %d (%s)\n", errcode, errmsg); } endwin(); exit(EXIT_FAILURE); } void cb_toxcore_logger(Tox *m, TOX_LOG_LEVEL level, const char *file, uint32_t line, const char *func, const char *message, void *user_data) { UNUSED_VAR(user_data); UNUSED_VAR(file); UNUSED_VAR(m); if (user_data) { FILE *fp = (FILE *)user_data; fprintf(fp, "[%d] %u:%s() - %s\n", level, line, func, message); fflush(fp); } else { fprintf(stderr, "[%d] %u:%s() - %s\n", level, line, func, message); } } /* Sets ncurses refresh rate. Lower values make it refresh more often. */ void set_window_refresh_rate(size_t refresh_rate) { timeout(refresh_rate); } static void init_term(void) { #if HAVE_WIDECHAR if (!arg_opts.default_locale) { if (setlocale(LC_ALL, "") == NULL) exit_toxic_err("Could not set your locale, please check your locale settings or " "disable unicode support with the -d flag.", FATALERR_LOCALE_NOT_SET); } #endif initscr(); cbreak(); keypad(stdscr, 1); noecho(); nonl(); set_window_refresh_rate(NCURSES_DEFAULT_REFRESH_RATE); if (has_colors()) { short bg_color = COLOR_BLACK; short bar_bg_color = COLOR_BLUE; short bar_fg_color = COLOR_WHITE; short bar_accent_color = COLOR_CYAN; short bar_notify_color = COLOR_YELLOW; start_color(); if (user_settings->colour_theme == NATIVE_COLS) { if (assume_default_colors(-1, -1) == OK) { bg_color = -1; } } if (!string_is_empty(user_settings->color_bar_bg)) { if (strcmp(user_settings->color_bar_bg, "black") == 0) { bar_bg_color = COLOR_BLACK; } else if (strcmp(user_settings->color_bar_bg, "red") == 0) { bar_bg_color = COLOR_RED; } else if (strcmp(user_settings->color_bar_bg, "blue") == 0) { bar_bg_color = COLOR_BLUE; } else if (strcmp(user_settings->color_bar_bg, "cyan") == 0) { bar_bg_color = COLOR_CYAN; } else if (strcmp(user_settings->color_bar_bg, "green") == 0) { bar_bg_color = COLOR_GREEN; } else if (strcmp(user_settings->color_bar_bg, "yellow") == 0) { bar_bg_color = COLOR_YELLOW; } else if (strcmp(user_settings->color_bar_bg, "magenta") == 0) { bar_bg_color = COLOR_MAGENTA; } else if (strcmp(user_settings->color_bar_bg, "white") == 0) { bar_bg_color = COLOR_WHITE; } else { bar_bg_color = COLOR_BLUE; } } else { bar_bg_color = COLOR_BLUE; } if (!string_is_empty(user_settings->color_bar_fg)) { if (strcmp(user_settings->color_bar_fg, "black") == 0) { bar_fg_color = COLOR_BLACK; } else if (strcmp(user_settings->color_bar_fg, "red") == 0) { bar_fg_color = COLOR_RED; } else if (strcmp(user_settings->color_bar_fg, "blue") == 0) { bar_fg_color = COLOR_BLUE; } else if (strcmp(user_settings->color_bar_fg, "cyan") == 0) { bar_fg_color = COLOR_CYAN; } else if (strcmp(user_settings->color_bar_fg, "green") == 0) { bar_fg_color = COLOR_GREEN; } else if (strcmp(user_settings->color_bar_fg, "yellow") == 0) { bar_fg_color = COLOR_YELLOW; } else if (strcmp(user_settings->color_bar_fg, "magenta") == 0) { bar_fg_color = COLOR_MAGENTA; } else if (strcmp(user_settings->color_bar_fg, "white") == 0) { bar_fg_color = COLOR_WHITE; } else { bar_fg_color = COLOR_WHITE; } } else { bar_fg_color = COLOR_WHITE; } if (!string_is_empty(user_settings->color_bar_accent)) { if (strcmp(user_settings->color_bar_accent, "black") == 0) { bar_accent_color = COLOR_BLACK; } else if (strcmp(user_settings->color_bar_accent, "red") == 0) { bar_accent_color = COLOR_RED; } else if (strcmp(user_settings->color_bar_accent, "blue") == 0) { bar_accent_color = COLOR_BLUE; } else if (strcmp(user_settings->color_bar_accent, "cyan") == 0) { bar_accent_color = COLOR_CYAN; } else if (strcmp(user_settings->color_bar_accent, "green") == 0) { bar_accent_color = COLOR_GREEN; } else if (strcmp(user_settings->color_bar_accent, "yellow") == 0) { bar_accent_color = COLOR_YELLOW; } else if (strcmp(user_settings->color_bar_accent, "magenta") == 0) { bar_accent_color = COLOR_MAGENTA; } else if (strcmp(user_settings->color_bar_accent, "white") == 0) { bar_accent_color = COLOR_WHITE; } else { bar_accent_color = COLOR_CYAN; } } else { bar_accent_color = COLOR_CYAN; } if (!string_is_empty(user_settings->color_bar_notify)) { if (strcmp(user_settings->color_bar_notify, "black") == 0) { bar_notify_color = COLOR_BLACK; } else if (strcmp(user_settings->color_bar_notify, "red") == 0) { bar_notify_color = COLOR_RED; } else if (strcmp(user_settings->color_bar_notify, "blue") == 0) { bar_notify_color = COLOR_BLUE; } else if (strcmp(user_settings->color_bar_notify, "cyan") == 0) { bar_notify_color = COLOR_CYAN; } else if (strcmp(user_settings->color_bar_notify, "green") == 0) { bar_notify_color = COLOR_GREEN; } else if (strcmp(user_settings->color_bar_notify, "yellow") == 0) { bar_notify_color = COLOR_YELLOW; } else if (strcmp(user_settings->color_bar_notify, "magenta") == 0) { bar_notify_color = COLOR_MAGENTA; } else if (strcmp(user_settings->color_bar_notify, "white") == 0) { bar_notify_color = COLOR_WHITE; } else { bar_notify_color = COLOR_YELLOW; } } else { bar_notify_color = COLOR_YELLOW; } init_pair(WHITE, COLOR_WHITE, COLOR_BLACK); init_pair(GREEN, COLOR_GREEN, bg_color); init_pair(CYAN, COLOR_CYAN, bg_color); init_pair(RED, COLOR_RED, bg_color); init_pair(BLUE, COLOR_BLUE, bg_color); init_pair(YELLOW, COLOR_YELLOW, bg_color); init_pair(MAGENTA, COLOR_MAGENTA, bg_color); init_pair(BLACK, COLOR_BLACK, COLOR_BLACK); init_pair(WHITE_BLUE, COLOR_WHITE, COLOR_BLUE); init_pair(BLACK_WHITE, COLOR_BLACK, COLOR_WHITE); init_pair(WHITE_BLACK, COLOR_WHITE, COLOR_BLACK); init_pair(WHITE_GREEN, COLOR_WHITE, COLOR_GREEN); init_pair(BLACK_BG, COLOR_BLACK, bar_bg_color); init_pair(PURPLE_BG, COLOR_MAGENTA, bar_bg_color); init_pair(BAR_TEXT, bar_fg_color, bar_bg_color); init_pair(BAR_SOLID, bar_bg_color, bar_bg_color); init_pair(BAR_ACCENT, bar_accent_color, bar_bg_color); init_pair(BAR_NOTIFY, bar_notify_color, bar_bg_color); init_pair(STATUS_ONLINE, COLOR_GREEN, bar_bg_color); init_pair(STATUS_AWAY, COLOR_YELLOW, bar_bg_color); init_pair(STATUS_BUSY, COLOR_RED, bar_bg_color); } refresh(); } static struct _init_messages { char **msgs; int num; } init_messages; /* One-time queue for messages created during init. Do not use after program init. */ static void queue_init_message(const char *msg, ...) { char frmt_msg[MAX_STR_SIZE] = {0}; va_list args; va_start(args, msg); vsnprintf(frmt_msg, sizeof(frmt_msg), msg, args); va_end(args); int i = init_messages.num; ++init_messages.num; char **new_msgs = realloc(init_messages.msgs, sizeof(char *) * init_messages.num); if (new_msgs == NULL) { exit_toxic_err("Failed in queue_init_message", FATALERR_MEMORY); } new_msgs[i] = malloc(MAX_STR_SIZE); if (new_msgs[i] == NULL) { exit_toxic_err("Failed in queue_init_message", FATALERR_MEMORY); } snprintf(new_msgs[i], MAX_STR_SIZE, "%s", frmt_msg); init_messages.msgs = new_msgs; } /* called after messages have been printed to prompt and are no longer needed */ static void cleanup_init_messages(void) { if (init_messages.num <= 0) { return; } for (int i = 0; i < init_messages.num; ++i) { free(init_messages.msgs[i]); } free(init_messages.msgs); } static void print_init_messages(ToxWindow *toxwin) { for (int i = 0; i < init_messages.num; ++i) { line_info_add(toxwin, NULL, NULL, NULL, SYS_MSG, 0, 0, init_messages.msgs[i]); } } static void load_friendlist(Tox *m) { size_t numfriends = tox_self_get_friend_list_size(m); for (size_t i = 0; i < numfriends; ++i) { friendlist_onFriendAdded(NULL, m, i, false); } sort_friendlist_index(); } static void load_conferences(Tox *m) { size_t num_chats = tox_conference_get_chatlist_size(m); if (num_chats == 0) { return; } uint32_t *chatlist = malloc(num_chats * sizeof(uint32_t)); if (chatlist == NULL) { fprintf(stderr, "malloc() failed in load_conferences()\n"); return; } tox_conference_get_chatlist(m, chatlist); for (size_t i = 0; i < num_chats; ++i) { uint32_t conferencenum = chatlist[i]; if (get_num_active_windows() >= MAX_WINDOWS_NUM) { tox_conference_delete(m, conferencenum, NULL); continue; } Tox_Err_Conference_Get_Type err; Tox_Conference_Type type = tox_conference_get_type(m, conferencenum, &err); if (err != TOX_ERR_CONFERENCE_GET_TYPE_OK) { tox_conference_delete(m, conferencenum, NULL); continue; } Tox_Err_Conference_Title t_err; size_t length = tox_conference_get_title_size(m, conferencenum, &t_err); uint8_t title[MAX_STR_SIZE]; if (t_err != TOX_ERR_CONFERENCE_TITLE_OK || length >= sizeof(title)) { length = 0; } else { tox_conference_get_title(m, conferencenum, title, &t_err); if (t_err != TOX_ERR_CONFERENCE_TITLE_OK) { length = 0; } } title[length] = 0; int win_idx = init_conference_win(m, conferencenum, type, (const char *) title, length); if (win_idx == -1) { tox_conference_delete(m, conferencenum, NULL); continue; } if (type == TOX_CONFERENCE_TYPE_AV) { line_info_add(get_window_ptr(win_idx), NULL, NULL, NULL, SYS_MSG, 0, 0, #ifdef AUDIO "Use \"/audio on\" to enable audio in this conference." #else "Audio support disabled by compile-time option." #endif ); } } free(chatlist); } /* return length of password on success, 0 on failure */ static int password_prompt(char *buf, int size) { buf[0] = '\0'; /* disable terminal echo */ struct termios oflags, nflags; tcgetattr(fileno(stdin), &oflags); nflags = oflags; nflags.c_lflag &= ~ECHO; nflags.c_lflag |= ECHONL; if (tcsetattr(fileno(stdin), TCSANOW, &nflags) != 0) { return 0; } const char *p = fgets(buf, size, stdin); /* re-enable terminal echo */ tcsetattr(fileno(stdin), TCSANOW, &oflags); if (p == NULL) { return 0; } size_t len = strlen(buf); if (len <= 1) { return 0; } /* eat overflowed stdin and return error */ if (buf[--len] != '\n') { int ch; while ((ch = getchar()) != '\n' && ch > 0) { } return 0; } buf[len] = '\0'; return len; } /* Get the password from the eval command. * return length of password on success, 0 on failure */ static int password_eval(char *buf, int size) { buf[0] = '\0'; /* Run password_eval command */ FILE *f = popen(user_settings->password_eval, "r"); if (f == NULL) { fprintf(stderr, "Executing password_eval failed\n"); return 0; } /* Get output from command */ char *ret = fgets(buf, size, f); if (ret == NULL) { fprintf(stderr, "Reading password from password_eval command failed\n"); pclose(f); return 0; } /* Get exit status */ int status = pclose(f); if (status != 0) { fprintf(stderr, "password_eval command returned error %d\n", status); return 0; } /* Removez whitespace or \n at end */ int i, len = strlen(buf); for (i = len - 1; i > 0 && isspace(buf[i]); i--) { buf[i] = 0; len--; } return len; } /* Ask user if they would like to encrypt the data file and set password */ static void first_time_encrypt(const char *msg) { char ch[256] = {0}; do { clear_screen(); printf("%s ", msg); fflush(stdout); if (!strcasecmp(ch, "y\n") || !strcasecmp(ch, "n\n") || !strcasecmp(ch, "yes\n") || !strcasecmp(ch, "no\n") || !strcasecmp(ch, "q\n")) { break; } } while (fgets(ch, sizeof(ch), stdin)); printf("\n"); if (ch[0] == 'q' || ch[0] == 'Q') { exit(0); } if (ch[0] == 'y' || ch[0] == 'Y') { int len = 0; bool valid_password = false; char passconfirm[MAX_PASSWORD_LEN + 1] = {0}; printf("Enter a new password (must be at least %d characters) ", MIN_PASSWORD_LEN); while (valid_password == false) { fflush(stdout); // Flush all before user input len = password_prompt(user_password.pass, sizeof(user_password.pass)); user_password.len = len; if (strcasecmp(user_password.pass, "q") == 0) { exit(0); } if (string_is_empty(passconfirm) && (len < MIN_PASSWORD_LEN || len > MAX_PASSWORD_LEN)) { printf("Password must be between %d and %d characters long. ", MIN_PASSWORD_LEN, MAX_PASSWORD_LEN); continue; } if (string_is_empty(passconfirm)) { printf("Enter password again "); snprintf(passconfirm, sizeof(passconfirm), "%s", user_password.pass); continue; } if (strcmp(user_password.pass, passconfirm) != 0) { memset(passconfirm, 0, sizeof(passconfirm)); memset(user_password.pass, 0, sizeof(user_password.pass)); printf("Passwords don't match. Try again. "); continue; } valid_password = true; } queue_init_message("Data file '%s' is encrypted", DATA_FILE); memset(passconfirm, 0, sizeof(passconfirm)); user_password.data_is_encrypted = true; } clear_screen(); } /* Store Tox profile data to path. * * Return 0 if stored successfully. * Return -1 on error. */ #define TEMP_PROFILE_EXT ".tmp" int store_data(Tox *m, const char *path) { if (path == NULL) { return -1; } size_t temp_buf_size = strlen(path) + strlen(TEMP_PROFILE_EXT) + 1; char *temp_path = malloc(temp_buf_size); if (temp_path == NULL) { return -1; } snprintf(temp_path, temp_buf_size, "%s%s", path, TEMP_PROFILE_EXT); FILE *fp = fopen(temp_path, "wb"); if (fp == NULL) { free(temp_path); return -1; } size_t data_len = tox_get_savedata_size(m); char *data = malloc(data_len * sizeof(char)); if (data == NULL) { free(temp_path); fclose(fp); return -1; } tox_get_savedata(m, (uint8_t *) data); if (user_password.data_is_encrypted && !arg_opts.unencrypt_data) { size_t enc_len = data_len + TOX_PASS_ENCRYPTION_EXTRA_LENGTH; char *enc_data = malloc(enc_len * sizeof(char)); if (enc_data == NULL) { fclose(fp); free(temp_path); free(data); return -1; } Tox_Err_Encryption err; tox_pass_encrypt((uint8_t *) data, data_len, (uint8_t *) user_password.pass, user_password.len, (uint8_t *) enc_data, &err); if (err != TOX_ERR_ENCRYPTION_OK) { fprintf(stderr, "tox_pass_encrypt() failed with error %d\n", err); fclose(fp); free(temp_path); free(data); free(enc_data); return -1; } if (fwrite(enc_data, enc_len, 1, fp) != 1) { fprintf(stderr, "Failed to write profile data.\n"); fclose(fp); free(temp_path); free(data); free(enc_data); return -1; } free(enc_data); } else { /* data will not be encrypted */ if (fwrite(data, data_len, 1, fp) != 1) { fprintf(stderr, "Failed to write profile data.\n"); fclose(fp); free(temp_path); free(data); return -1; } } fclose(fp); free(data); if (rename(temp_path, path) != 0) { free(temp_path); return -1; } free(temp_path); return 0; } static void init_tox_callbacks(Tox *m) { tox_callback_self_connection_status(m, on_self_connection_status); tox_callback_friend_connection_status(m, on_friend_connection_status); tox_callback_friend_typing(m, on_friend_typing); tox_callback_friend_request(m, on_friend_request); tox_callback_friend_message(m, on_friend_message); tox_callback_friend_name(m, on_friend_name); tox_callback_friend_status(m, on_friend_status); tox_callback_friend_status_message(m, on_friend_status_message); tox_callback_friend_read_receipt(m, on_friend_read_receipt); tox_callback_conference_invite(m, on_conference_invite); tox_callback_conference_message(m, on_conference_message); tox_callback_conference_peer_list_changed(m, on_conference_peer_list_changed); tox_callback_conference_peer_name(m, on_conference_peer_name); tox_callback_conference_title(m, on_conference_title); tox_callback_file_recv(m, on_file_recv); tox_callback_file_chunk_request(m, on_file_chunk_request); tox_callback_file_recv_control(m, on_file_recv_control); tox_callback_file_recv_chunk(m, on_file_recv_chunk); tox_callback_friend_lossless_packet(m, on_lossless_custom_packet); } static void init_tox_options(struct Tox_Options *tox_opts) { tox_options_default(tox_opts); tox_options_set_ipv6_enabled(tox_opts, !arg_opts.use_ipv4); tox_options_set_udp_enabled(tox_opts, !arg_opts.force_tcp); tox_options_set_proxy_type(tox_opts, arg_opts.proxy_type); tox_options_set_tcp_port(tox_opts, arg_opts.tcp_port); tox_options_set_local_discovery_enabled(tox_opts, !arg_opts.disable_local_discovery); if (arg_opts.logging) { tox_options_set_log_callback(tox_opts, cb_toxcore_logger); if (arg_opts.log_fp != NULL) { tox_options_set_log_user_data(tox_opts, arg_opts.log_fp); } } if (!tox_options_get_ipv6_enabled(tox_opts)) { queue_init_message("Forcing IPv4 connection"); } if (tox_options_get_tcp_port(tox_opts)) { queue_init_message("TCP relaying enabled on port %d", tox_options_get_tcp_port(tox_opts)); } if (tox_options_get_proxy_type(tox_opts) != TOX_PROXY_TYPE_NONE) { tox_options_set_proxy_port(tox_opts, arg_opts.proxy_port); tox_options_set_proxy_host(tox_opts, arg_opts.proxy_address); const char *ps = tox_options_get_proxy_type(tox_opts) == TOX_PROXY_TYPE_SOCKS5 ? "SOCKS5" : "HTTP"; char tmp[sizeof(arg_opts.proxy_address) + MAX_STR_SIZE]; snprintf(tmp, sizeof(tmp), "Using %s proxy %s : %d", ps, arg_opts.proxy_address, arg_opts.proxy_port); queue_init_message("%s", tmp); } if (!tox_options_get_udp_enabled(tox_opts)) { queue_init_message("UDP disabled"); } else if (tox_options_get_proxy_type(tox_opts) != TOX_PROXY_TYPE_NONE) { const char *msg = "WARNING: Using a proxy without disabling UDP may leak your real IP address."; queue_init_message("%s", msg); msg = "Use the -t option to disable UDP."; queue_init_message("%s", msg); } } /* Returns a new Tox object on success. * If object fails to initialize the toxic process will terminate. */ static Tox *load_tox(char *data_path, struct Tox_Options *tox_opts, Tox_Err_New *new_err) { Tox *m = NULL; FILE *fp = fopen(data_path, "rb"); if (fp != NULL) { /* Data file exists */ off_t len = file_size(data_path); if (len == 0) { fclose(fp); exit_toxic_err("failed in load_tox", FATALERR_FILEOP); } char *data = malloc(len); if (data == NULL) { fclose(fp); exit_toxic_err("failed in load_tox", FATALERR_MEMORY); } if (fread(data, len, 1, fp) != 1) { fclose(fp); free(data); exit_toxic_err("failed in load_tox", FATALERR_FILEOP); } bool is_encrypted = tox_is_data_encrypted((uint8_t *) data); /* attempt to encrypt an already encrypted data file */ if (arg_opts.encrypt_data && is_encrypted) { fclose(fp); free(data); exit_toxic_err("failed in load_tox", FATALERR_ENCRYPT); } if (arg_opts.unencrypt_data && is_encrypted) { queue_init_message("Data file '%s' has been unencrypted", data_path); } else if (arg_opts.unencrypt_data) { queue_init_message("Warning: passed --unencrypt-data option with unencrypted data file '%s'", data_path); } if (is_encrypted) { if (!arg_opts.unencrypt_data) { user_password.data_is_encrypted = true; } size_t pwlen = 0; int pweval = user_settings->password_eval[0]; if (!pweval) { clear_screen(); printf("Enter password (q to quit) "); } size_t plain_len = len - TOX_PASS_ENCRYPTION_EXTRA_LENGTH; char *plain = malloc(plain_len); // must be freed after tox_new() if (plain == NULL) { fclose(fp); free(data); exit_toxic_err("failed in load_tox", FATALERR_MEMORY); } while (true) { fflush(stdout); // Flush before prompts so the user sees the question/message if (pweval) { pwlen = password_eval(user_password.pass, sizeof(user_password.pass)); } else { pwlen = password_prompt(user_password.pass, sizeof(user_password.pass)); } user_password.len = pwlen; if (strcasecmp(user_password.pass, "q") == 0) { fclose(fp); free(plain); free(data); exit(0); } if (pwlen < MIN_PASSWORD_LEN) { clear_screen(); sleep(1); printf("Invalid password. Try again. "); pweval = 0; continue; } Tox_Err_Decryption pwerr; tox_pass_decrypt((uint8_t *) data, len, (uint8_t *) user_password.pass, pwlen, (uint8_t *) plain, &pwerr); if (pwerr == TOX_ERR_DECRYPTION_OK) { tox_options_set_savedata_type(tox_opts, TOX_SAVEDATA_TYPE_TOX_SAVE); tox_options_set_savedata_data(tox_opts, (uint8_t *) plain, plain_len); m = tox_new(tox_opts, new_err); if (m == NULL) { fclose(fp); free(data); free(plain); return NULL; } break; } else if (pwerr == TOX_ERR_DECRYPTION_FAILED) { clear_screen(); sleep(1); printf("Invalid password. Try again. "); pweval = 0; } else { fclose(fp); free(data); free(plain); exit_toxic_err("tox_pass_decrypt() failed", pwerr); } } free(plain); } else { /* data is not encrypted */ tox_options_set_savedata_type(tox_opts, TOX_SAVEDATA_TYPE_TOX_SAVE); tox_options_set_savedata_data(tox_opts, (uint8_t *) data, len); m = tox_new(tox_opts, new_err); if (m == NULL) { fclose(fp); free(data); return NULL; } } fclose(fp); free(data); } else { /* Data file does not/should not exist */ if (file_exists(data_path)) { exit_toxic_err("failed in load_tox", FATALERR_FILEOP); } tox_options_set_savedata_type(tox_opts, TOX_SAVEDATA_TYPE_NONE); m = tox_new(tox_opts, new_err); if (m == NULL) { return NULL; } if (store_data(m, data_path) == -1) { exit_toxic_err("failed in load_tox", FATALERR_FILEOP); } } return m; } static Tox *load_toxic(char *data_path) { Tox_Err_Options_New options_new_err; struct Tox_Options *tox_opts = tox_options_new(&options_new_err); if (!tox_opts) { exit_toxic_err("tox_options_new returned fatal error", options_new_err); } init_tox_options(tox_opts); Tox_Err_New new_err; Tox *m = load_tox(data_path, tox_opts, &new_err); if (new_err == TOX_ERR_NEW_PORT_ALLOC && tox_options_get_ipv6_enabled(tox_opts)) { queue_init_message("Falling back to ipv4"); tox_options_set_ipv6_enabled(tox_opts, false); m = load_tox(data_path, tox_opts, &new_err); } if (!m) { exit_toxic_err("tox_new returned fatal error", new_err); } if (new_err != TOX_ERR_NEW_OK) { queue_init_message("tox_new returned non-fatal error %d", new_err); } init_tox_callbacks(m); load_friendlist(m); load_blocklist(BLOCK_FILE); if (tox_self_get_name_size(m) == 0) { tox_self_set_name(m, (uint8_t *) "Toxic User", strlen("Toxic User"), NULL); } tox_options_free(tox_opts); return m; } static void do_toxic(Tox *m) { pthread_mutex_lock(&Winthread.lock); if (arg_opts.no_connect) { pthread_mutex_unlock(&Winthread.lock); return; } tox_iterate(m, NULL); do_tox_connection(m); pthread_mutex_unlock(&Winthread.lock); } /* Set interface refresh flag. This should be called whenever the interface changes. * * This function is not thread safe. */ void flag_interface_refresh(void) { Winthread.flag_refresh = 1; Winthread.last_refresh_flag = get_unix_time(); } /* How long we wait to idle interface refreshing after last flag set. Should be no less than 2. */ #define ACTIVE_WIN_REFRESH_TIMEOUT 2 static void poll_interface_refresh_flag(void) { pthread_mutex_lock(&Winthread.lock); bool flag = Winthread.flag_refresh; time_t t = Winthread.last_refresh_flag; pthread_mutex_unlock(&Winthread.lock); if (flag == 1 && timed_out(t, ACTIVE_WIN_REFRESH_TIMEOUT)) { pthread_mutex_lock(&Winthread.lock); Winthread.flag_refresh = 0; pthread_mutex_unlock(&Winthread.lock); } } /* How often we refresh windows that aren't focused */ #define INACTIVE_WIN_REFRESH_RATE 10 void *thread_winref(void *data) { Tox *m = (Tox *) data; uint8_t draw_count = 0; init_signal_catchers(); while (true) { draw_count++; draw_active_window(m); if (Winthread.flag_resize) { on_window_resize(); Winthread.flag_resize = 0; } else if (draw_count >= INACTIVE_WIN_REFRESH_RATE) { refresh_inactive_windows(); draw_count = 0; } if (Winthread.sig_exit_toxic) { pthread_mutex_lock(&Winthread.lock); exit_toxic_success(m); } poll_interface_refresh_flag(); } } void *thread_cqueue(void *data) { Tox *m = (Tox *) data; while (true) { pthread_mutex_lock(&Winthread.lock); for (size_t i = 2; i < MAX_WINDOWS_NUM; ++i) { ToxWindow *toxwin = get_window_ptr(i); if ((toxwin != NULL) && (toxwin->type == WINDOW_TYPE_CHAT)) { cqueue_check_unread(toxwin); if (get_friend_connection_status(toxwin->num) != TOX_CONNECTION_NONE) { cqueue_try_send(toxwin, m); } } } pthread_mutex_unlock(&Winthread.lock); sleep_thread(750000L); // 0.75 seconds } } #ifdef AUDIO void *thread_av(void *data) { ToxAV *av = (ToxAV *) data; while (true) { pthread_mutex_lock(&Winthread.lock); toxav_iterate(av); pthread_mutex_unlock(&Winthread.lock); long int sleep_duration = toxav_iteration_interval(av) * 1000; sleep_thread(sleep_duration); } } #endif /* AUDIO */ static void print_usage(void) { fprintf(stderr, "usage: toxic [OPTION] [FILE ...]\n"); fprintf(stderr, " -4, --ipv4 Force IPv4 connection\n"); fprintf(stderr, " -b, --debug Enable stderr for debugging\n"); fprintf(stderr, " -c, --config Use specified config file\n"); fprintf(stderr, " -d, --default-locale Use default POSIX locale\n"); fprintf(stderr, " -e, --encrypt-data Encrypt an unencrypted data file\n"); fprintf(stderr, " -f, --file Use specified data file\n"); fprintf(stderr, " -h, --help Show this message and exit\n"); fprintf(stderr, " -l, --logging Enable toxcore logging: Requires [log_path | stderr]\n"); fprintf(stderr, " -L, --no-lan Disable local discovery\n"); fprintf(stderr, " -n, --nodes Use specified DHTnodes file\n"); fprintf(stderr, " -o, --noconnect Do not connect to the DHT network\n"); fprintf(stderr, " -p, --SOCKS5-proxy Use SOCKS5 proxy: Requires [IP] [port]\n"); fprintf(stderr, " -P, --HTTP-proxy Use HTTP proxy: Requires [IP] [port]\n"); fprintf(stderr, " -r, --namelist Use specified name lookup server list\n"); fprintf(stderr, " -t, --force-tcp Force toxic to use a TCP connection (use with proxies)\n"); fprintf(stderr, " -T, --tcp-server Act as a TCP relay server: Requires [port]\n"); fprintf(stderr, " -u, --unencrypt-data Unencrypt an encrypted data file\n"); fprintf(stderr, " -v, --version Print the version\n"); } static void print_version(void) { fprintf(stderr, "Toxic version %s\n", TOXICVER); fprintf(stderr, "Toxcore version %d.%d.%d\n", tox_version_major(), tox_version_minor(), tox_version_patch()); } static void set_default_opts(void) { arg_opts = (struct arg_opts) { 0 }; /* set any non-zero defaults here*/ arg_opts.proxy_type = TOX_PROXY_TYPE_NONE; } static void parse_args(int argc, char *argv[]) { set_default_opts(); static struct option long_opts[] = { {"ipv4", no_argument, 0, '4'}, {"debug", no_argument, 0, 'b'}, {"default-locale", no_argument, 0, 'd'}, {"config", required_argument, 0, 'c'}, {"encrypt-data", no_argument, 0, 'e'}, {"file", required_argument, 0, 'f'}, {"logging", required_argument, 0, 'l'}, {"no-lan", no_argument, 0, 'L'}, {"nodes", required_argument, 0, 'n'}, {"help", no_argument, 0, 'h'}, {"noconnect", no_argument, 0, 'o'}, {"namelist", required_argument, 0, 'r'}, {"force-tcp", no_argument, 0, 't'}, {"tcp-server", required_argument, 0, 'T'}, {"SOCKS5-proxy", required_argument, 0, 'p'}, {"HTTP-proxy", required_argument, 0, 'P'}, {"unencrypt-data", no_argument, 0, 'u'}, {"version", no_argument, 0, 'v'}, {NULL, no_argument, NULL, 0}, }; const char *opts_str = "4bdehLotuxvc:f:l:n:r:p:P:T:"; int opt = 0; int indexptr = 0; while ((opt = getopt_long(argc, argv, opts_str, long_opts, &indexptr)) != -1) { switch (opt) { case '4': { arg_opts.use_ipv4 = 1; break; } case 'b': { arg_opts.debug = 1; queue_init_message("stderr enabled"); break; } case 'c': { if (optarg == NULL) { queue_init_message("Invalid argument for option: %d", opt); break; } snprintf(arg_opts.config_path, sizeof(arg_opts.config_path), "%s", optarg); if (!file_exists(arg_opts.config_path)) { queue_init_message("Config file not found"); } break; } case 'd': { arg_opts.default_locale = 1; queue_init_message("Using default POSIX locale"); break; } case 'e': { arg_opts.encrypt_data = 1; break; } case 'f': { if (optarg == NULL) { queue_init_message("Invalid argument for option: %d", opt); break; } arg_opts.use_custom_data = 1; if (DATA_FILE) { free(DATA_FILE); DATA_FILE = NULL; } if (BLOCK_FILE) { free(BLOCK_FILE); BLOCK_FILE = NULL; } DATA_FILE = malloc(strlen(optarg) + 1); if (DATA_FILE == NULL) { exit_toxic_err("failed in parse_args", FATALERR_MEMORY); } strcpy(DATA_FILE, optarg); BLOCK_FILE = malloc(strlen(optarg) + strlen("-blocklist") + 1); if (BLOCK_FILE == NULL) { exit_toxic_err("failed in parse_args", FATALERR_MEMORY); } strcpy(BLOCK_FILE, optarg); strcat(BLOCK_FILE, "-blocklist"); queue_init_message("Using '%s' data file", DATA_FILE); break; } case 'l': { if (optarg) { arg_opts.logging = true; if (strcmp(optarg, "stderr") != 0) { arg_opts.log_fp = fopen(optarg, "w"); if (arg_opts.log_fp != NULL) { queue_init_message("Toxcore logging enabled to %s", optarg); } else { arg_opts.debug = true; queue_init_message("Failed to open log file %s. Falling back to stderr.", optarg); } } else { arg_opts.debug = true; queue_init_message("Toxcore logging enabled to stderr"); } } break; } case 'L': { arg_opts.disable_local_discovery = 1; queue_init_message("Local discovery disabled"); break; } case 'n': { if (optarg == NULL) { queue_init_message("Invalid argument for option: %d", opt); break; } snprintf(arg_opts.nodes_path, sizeof(arg_opts.nodes_path), "%s", optarg); break; } case 'o': { arg_opts.no_connect = 1; queue_init_message("DHT disabled"); break; } case 'p': { arg_opts.proxy_type = TOX_PROXY_TYPE_SOCKS5; } // Intentional fallthrough case 'P': { if (optarg == NULL) { queue_init_message("Invalid argument for option: %d", opt); arg_opts.proxy_type = TOX_PROXY_TYPE_NONE; break; } if (arg_opts.proxy_type == TOX_PROXY_TYPE_NONE) { arg_opts.proxy_type = TOX_PROXY_TYPE_HTTP; } snprintf(arg_opts.proxy_address, sizeof(arg_opts.proxy_address), "%s", optarg); if (++optind > argc || argv[optind - 1][0] == '-') { exit_toxic_err("Proxy error", FATALERR_PROXY); } long int port = strtol(argv[optind - 1], NULL, 10); if (port <= 0 || port > MAX_PORT_RANGE) { exit_toxic_err("Proxy error", FATALERR_PROXY); } arg_opts.proxy_port = port; break; } case 'r': { if (optarg == NULL) { queue_init_message("Invalid argument for option: %d", opt); break; } snprintf(arg_opts.nameserver_path, sizeof(arg_opts.nameserver_path), "%s", optarg); if (!file_exists(arg_opts.nameserver_path)) { queue_init_message("nameserver list not found"); } break; } case 't': { arg_opts.force_tcp = 1; break; } case 'T': { if (optarg == NULL) { queue_init_message("Invalid argument for option: %d", opt); break; } long int port = strtol(optarg, NULL, 10); if (port <= 0 || port > MAX_PORT_RANGE) { port = MAX_PORT_RANGE; } arg_opts.tcp_port = port; break; } case 'u': { arg_opts.unencrypt_data = 1; break; } case 'v': { print_version(); exit(EXIT_SUCCESS); } case 'h': // Intentional fallthrough default: { print_usage(); exit(EXIT_SUCCESS); } } } } /* Initializes the default config directory and data files used by toxic. * * Exits the process with an error on failure. */ static void init_default_data_files(void) { if (arg_opts.use_custom_data) { return; } char *user_config_dir = get_user_config_dir(); if (user_config_dir == NULL) { exit_toxic_err("failed in init_default_data_files()", FATALERR_FILEOP); } int config_err = create_user_config_dirs(user_config_dir); if (config_err == -1) { DATA_FILE = strdup(DATANAME); BLOCK_FILE = strdup(BLOCKNAME); if (DATA_FILE == NULL || BLOCK_FILE == NULL) { exit_toxic_err("failed in init_default_data_files()", FATALERR_MEMORY); } } else { DATA_FILE = malloc(strlen(user_config_dir) + strlen(CONFIGDIR) + strlen(DATANAME) + 1); BLOCK_FILE = malloc(strlen(user_config_dir) + strlen(CONFIGDIR) + strlen(BLOCKNAME) + 1); if (DATA_FILE == NULL || BLOCK_FILE == NULL) { exit_toxic_err("failed in init_default_data_files()", FATALERR_MEMORY); } strcpy(DATA_FILE, user_config_dir); strcat(DATA_FILE, CONFIGDIR); strcat(DATA_FILE, DATANAME); strcpy(BLOCK_FILE, user_config_dir); strcat(BLOCK_FILE, CONFIGDIR); strcat(BLOCK_FILE, BLOCKNAME); } free(user_config_dir); } int main(int argc, char **argv) { /* Make sure all written files are read/writeable only by the current user. */ umask(S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH); srand(time(NULL)); // We use rand() for trivial/non-security related things parse_args(argc, argv); /* Use the -b flag to enable stderr */ if (!arg_opts.debug) { if (!freopen("/dev/null", "w", stderr)) { fprintf(stderr, "Warning: failed to enable stderr\n"); } } if (arg_opts.encrypt_data && arg_opts.unencrypt_data) { arg_opts.encrypt_data = 0; arg_opts.unencrypt_data = 0; queue_init_message("Warning: Using --unencrypt-data and --encrypt-data simultaneously has no effect"); } init_default_data_files(); bool datafile_exists = file_exists(DATA_FILE); if (!datafile_exists && !arg_opts.unencrypt_data) { first_time_encrypt("Creating new data file. Would you like to encrypt it? Y/n (q to quit)"); } else if (arg_opts.encrypt_data) { first_time_encrypt("Encrypt existing data file? Y/n (q to quit)"); } /* init user_settings struct and load settings from conf file */ user_settings = calloc(1, sizeof(struct user_settings)); if (user_settings == NULL) { exit_toxic_err("failed in main", FATALERR_MEMORY); } const char *p = arg_opts.config_path[0] ? arg_opts.config_path : NULL; if (settings_load(user_settings, p) == -1) { queue_init_message("Failed to load user settings"); } int curl_init = curl_global_init(CURL_GLOBAL_ALL); int nameserver_ret = name_lookup_init(curl_init); if (nameserver_ret == -1) { queue_init_message("curl failed to initialize; name lookup service is disabled."); } else if (nameserver_ret == -2) { queue_init_message("Name lookup server list could not be found."); } else if (nameserver_ret == -3) { queue_init_message("Name lookup server list does not contain any valid entries."); } #ifdef X11 if (init_x11focus() == -1) { queue_init_message("X failed to initialize"); } #endif /* X11 */ Tox *m = load_toxic(DATA_FILE); if (arg_opts.encrypt_data && !datafile_exists) { arg_opts.encrypt_data = 0; } init_term(); prompt = init_windows(m); prompt_init_statusbar(prompt, m, !datafile_exists); load_conferences(m); set_active_window_index(0); if (pthread_mutex_init(&Winthread.lock, NULL) != 0) { exit_toxic_err("failed in main", FATALERR_MUTEX_INIT); } #ifdef AUDIO av = init_audio(prompt, m); #ifdef VIDEO init_video(prompt, m); #endif /* VIDEO */ /* AV thread */ if (pthread_create(&av_thread.tid, NULL, thread_av, (void *) av) != 0) { exit_toxic_err("failed in main", FATALERR_THREAD_CREATE); } set_al_device(input, user_settings->audio_in_dev); set_al_device(output, user_settings->audio_out_dev); #elif SOUND_NOTIFY if (init_devices() == de_InternalError) { queue_init_message("Failed to init audio devices"); } #endif /* AUDIO */ /* thread for ncurses stuff */ if (pthread_create(&Winthread.tid, NULL, thread_winref, (void *) m) != 0) { exit_toxic_err("failed in main", FATALERR_THREAD_CREATE); } /* thread for message queue */ if (pthread_create(&cqueue_thread.tid, NULL, thread_cqueue, (void *) m) != 0) { exit_toxic_err("failed in main", FATALERR_THREAD_CREATE); } #ifdef PYTHON init_python(m); invoke_autoruns(prompt->chatwin->history, prompt); #endif /* PYTHON */ init_notify(60, user_settings->notification_timeout); /* screen/tmux auto-away timer */ if (init_mplex_away_timer(m) == -1) { queue_init_message("Failed to init mplex auto-away."); } int nodeslist_ret = load_DHT_nodeslist(); if (nodeslist_ret != 0) { queue_init_message("DHT nodeslist failed to load (error %d)", nodeslist_ret); } pthread_mutex_lock(&Winthread.lock); print_init_messages(prompt); flag_interface_refresh(); pthread_mutex_unlock(&Winthread.lock); cleanup_init_messages(); /* set user avatar from config file. if no path is supplied tox_unset_avatar is called */ char avatarstr[PATH_MAX + 11]; snprintf(avatarstr, sizeof(avatarstr), "/avatar %s", user_settings->avatar_path); execute(prompt->chatwin->history, prompt, m, avatarstr, GLOBAL_COMMAND_MODE); time_t last_save = get_unix_time(); while (true) { do_toxic(m); time_t cur_time = get_unix_time(); if (user_settings->autosave_freq > 0 && timed_out(last_save, user_settings->autosave_freq)) { pthread_mutex_lock(&Winthread.lock); if (store_data(m, DATA_FILE) != 0) { line_info_add(prompt, false, NULL, NULL, SYS_MSG, 0, RED, "WARNING: Failed to save to data file"); } pthread_mutex_unlock(&Winthread.lock); last_save = cur_time; } long int sleep_duration = tox_iteration_interval(m) * 1000; sleep_thread(sleep_duration); } return 0; } toxic-0.11.3/src/toxic.h000066400000000000000000000151161416141666600150270ustar00rootroot00000000000000/* toxic.h * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef TOXIC_H #define TOXIC_H #ifndef TOXICVER #define TOXICVER "NOVERS" /* Use the -D flag to set this */ #endif #ifndef SIGWINCH #define SIGWINCH 28 #endif #ifndef SIGINT #define SIGINT 2 #endif #include #include #include #define UNKNOWN_NAME "Anonymous" #define DEFAULT_TOX_NAME "Tox User" /* should always be the same as toxcore's default name */ #define MAX_STR_SIZE TOX_MAX_MESSAGE_LENGTH /* must be >= TOX_MAX_MESSAGE_LENGTH */ #define MAX_CMDNAME_SIZE 64 #define TOXIC_MAX_NAME_LENGTH 32 /* Must be <= TOX_MAX_NAME_LENGTH */ #define KEY_IDENT_DIGITS 3 /* number of hex digits to display for the pub-key based identifier */ #define TIME_STR_SIZE 32 #define COLOR_STR_SIZE 10 /* should fit every color option */ #define NCURSES_DEFAULT_REFRESH_RATE 100 #define NCURSES_GAME_REFRESH_RATE 25 #ifndef MAX_PORT_RANGE #define MAX_PORT_RANGE 65535 #endif /* ASCII key codes */ #define T_KEY_ESC 0x1B /* ESC key */ #define T_KEY_KILL 0x0B /* ctrl-k */ #define T_KEY_DISCARD 0x15 /* ctrl-u */ #define T_KEY_NEXT 0x10 /* ctrl-p */ #define T_KEY_PREV 0x0F /* ctrl-o */ #define T_KEY_C_E 0x05 /* ctrl-e */ #define T_KEY_C_A 0x01 /* ctrl-a */ #define T_KEY_C_V 0x16 /* ctrl-v */ #define T_KEY_C_F 0x06 /* ctrl-f */ #define T_KEY_C_H 0x08 /* ctrl-h */ #define T_KEY_C_Y 0x19 /* ctrl-y */ #define T_KEY_C_L 0x0C /* ctrl-l */ #define T_KEY_C_W 0x17 /* ctrl-w */ #define T_KEY_C_B 0x02 /* ctrl-b */ #define T_KEY_C_T 0x14 /* ctrl-t */ #define T_KEY_C_LEFT 0x221 /* ctrl-left arrow */ #define T_KEY_C_RIGHT 0x230 /* ctrl-right arrow */ #define T_KEY_C_UP 0x236 /* ctrl-up arrow */ #define T_KEY_C_DOWN 0x20D /* ctrl-down arrow */ #define T_KEY_TAB 0x09 /* TAB key */ #define ONLINE_CHAR "o" #define OFFLINE_CHAR "o" typedef enum _FATAL_ERRS { FATALERR_MEMORY = -1, /* heap memory allocation failed */ FATALERR_FILEOP = -2, /* critical file operation failed */ FATALERR_THREAD_CREATE = -3, /* thread creation failed for critical thread */ FATALERR_MUTEX_INIT = -4, /* mutex init for critical thread failed */ FATALERR_THREAD_ATTR = -5, /* thread attr object init failed */ FATALERR_LOCALE_NOT_SET = -6, /* system locale not set */ FATALERR_STORE_DATA = -7, /* store_data failed in critical section */ FATALERR_INFLOOP = -8, /* infinite loop detected */ FATALERR_WININIT = -9, /* window init failed */ FATALERR_PROXY = -10, /* Tox network failed to init using a proxy */ FATALERR_ENCRYPT = -11, /* Data file encryption failure */ FATALERR_TOX_INIT = -12, /* Tox instance failed to initialize */ FATALERR_CURSES = -13, /* Unrecoverable Ncurses error */ } FATAL_ERRS; /* Fixes text color problem on some terminals. Uncomment if necessary */ /* #define URXVT_FIX */ void lock_status(void); void unlock_status(void); void flag_interface_refresh(void); /* Sets ncurses refresh rate. Lower values make it refresh more often. */ void set_window_refresh_rate(size_t refresh_rate); void exit_toxic_success(Tox *m); void exit_toxic_err(const char *errmsg, int errcode); int store_data(Tox *m, const char *path); /* callbacks */ void on_friend_request(Tox *m, const uint8_t *public_key, const uint8_t *data, size_t length, void *userdata); void on_friend_connection_status(Tox *m, uint32_t friendnumber, Tox_Connection status, void *userdata); void on_friend_message(Tox *m, uint32_t friendnumber, Tox_Message_Type type, const uint8_t *string, size_t length, void *userdata); void on_friend_name(Tox *m, uint32_t friendnumber, const uint8_t *string, size_t length, void *userdata); void on_friend_status(Tox *m, uint32_t friendnumber, Tox_User_Status status, void *userdata); void on_friend_status_message(Tox *m, uint32_t friendnumber, const uint8_t *string, size_t length, void *userdata); void on_friend_added(Tox *m, uint32_t friendnumber, bool sort); void on_conference_message(Tox *m, uint32_t conferencenumber, uint32_t peernumber, Tox_Message_Type type, const uint8_t *message, size_t length, void *userdata); void on_conference_invite(Tox *m, uint32_t friendnumber, Tox_Conference_Type type, const uint8_t *conference_pub_key, size_t length, void *userdata); void on_conference_peer_list_changed(Tox *m, uint32_t conferencenumber, void *userdata); void on_conference_peer_name(Tox *m, uint32_t conferencenumber, uint32_t peernumber, const uint8_t *name, size_t length, void *userdata); void on_conference_title(Tox *m, uint32_t conferencenumber, uint32_t peernumber, const uint8_t *title, size_t length, void *userdata); void on_file_chunk_request(Tox *m, uint32_t friendnumber, uint32_t filenumber, uint64_t position, size_t length, void *userdata); void on_file_recv_chunk(Tox *m, uint32_t friendnumber, uint32_t filenumber, uint64_t position, const uint8_t *data, size_t length, void *userdata); void on_file_recv_control(Tox *m, uint32_t friendnumber, uint32_t filenumber, Tox_File_Control control, void *userdata); void on_file_recv(Tox *m, uint32_t friendnumber, uint32_t filenumber, uint32_t kind, uint64_t file_size, const uint8_t *filename, size_t filename_length, void *userdata); void on_friend_typing(Tox *m, uint32_t friendnumber, bool is_typing, void *userdata); void on_friend_read_receipt(Tox *m, uint32_t friendnumber, uint32_t receipt, void *userdata); void on_lossless_custom_packet(Tox *m, uint32_t friendnumber, const uint8_t *data, size_t length, void *userdata); extern char *DATA_FILE; extern char *BLOCK_FILE; #endif /* TOXIC_H */ toxic-0.11.3/src/toxic_strings.c000066400000000000000000000154621416141666600165770ustar00rootroot00000000000000/* toxic_strings.c * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #include #include #include #include "misc_tools.h" #include "notify.h" #include "toxic.h" #include "toxic_strings.h" #include "windows.h" /* Adds char to line at pos. Return 0 on success, -1 if line buffer is full */ int add_char_to_buf(ChatContext *ctx, wint_t ch) { if (ctx->len >= MAX_STR_SIZE - 1) { return -1; } wmemmove(&ctx->line[ctx->pos + 1], &ctx->line[ctx->pos], ctx->len - ctx->pos); ctx->line[ctx->pos++] = ch; ctx->line[++ctx->len] = L'\0'; return 0; } /* Deletes the character before pos. Return 0 on success, -1 if nothing to delete */ int del_char_buf_bck(ChatContext *ctx) { if (ctx->pos <= 0) { return -1; } wmemmove(&ctx->line[ctx->pos - 1], &ctx->line[ctx->pos], ctx->len - ctx->pos); --ctx->pos; ctx->line[--ctx->len] = L'\0'; return 0; } /* Deletes the character at pos. Return 0 on success, -1 if nothing to delete. */ int del_char_buf_frnt(ChatContext *ctx) { if (ctx->pos >= ctx->len) { return -1; } wmemmove(&ctx->line[ctx->pos], &ctx->line[ctx->pos + 1], ctx->len - ctx->pos - 1); ctx->line[--ctx->len] = L'\0'; return 0; } /* Deletes the line from beginning to pos and puts discarded portion in yank buffer. Return 0 on success, -1 if noting to discard. */ int discard_buf(ChatContext *ctx) { if (ctx->pos <= 0) { return -1; } ctx->yank_len = ctx->pos; wmemcpy(ctx->yank, ctx->line, ctx->yank_len); ctx->yank[ctx->yank_len] = L'\0'; wmemmove(ctx->line, &ctx->line[ctx->pos], ctx->len - ctx->pos); ctx->len -= ctx->pos; ctx->pos = 0; ctx->start = 0; ctx->line[ctx->len] = L'\0'; return 0; } /* Deletes the line from pos to len and puts killed portion in yank buffer. Return 0 on success, -1 if nothing to kill. */ int kill_buf(ChatContext *ctx) { if (ctx->len <= ctx->pos) { return -1; } ctx->yank_len = ctx->len - ctx->pos; wmemcpy(ctx->yank, &ctx->line[ctx->pos], ctx->yank_len); ctx->yank[ctx->yank_len] = L'\0'; ctx->line[ctx->pos] = L'\0'; ctx->len = ctx->pos; return 0; } /* Inserts string in ctx->yank into line at pos. Return 0 on success, -1 if yank buffer is empty or too long */ int yank_buf(ChatContext *ctx) { if (!ctx->yank[0]) { return -1; } if (ctx->yank_len + ctx->len >= MAX_STR_SIZE) { return -1; } wmemmove(&ctx->line[ctx->pos + ctx->yank_len], &ctx->line[ctx->pos], ctx->len - ctx->pos); wmemcpy(&ctx->line[ctx->pos], ctx->yank, ctx->yank_len); ctx->pos += ctx->yank_len; ctx->len += ctx->yank_len; ctx->line[ctx->len] = L'\0'; return 0; } /* Deletes all characters from line starting at pos and going backwards until we find a space or run out of characters. Return 0 on success, -1 if nothing to delete */ int del_word_buf(ChatContext *ctx) { if (ctx->len == 0 || ctx->pos == 0) { return -1; } int i = ctx->pos, count = 0; /* traverse past empty space */ while (i > 0 && ctx->line[i - 1] == L' ') { ++count; --i; } /* traverse past last entered word */ while (i > 0 && ctx->line[i - 1] != L' ') { ++count; --i; } wmemmove(&ctx->line[i], &ctx->line[ctx->pos], ctx->len - ctx->pos); ctx->start = MAX(0, ctx->start - count); /* TODO: take into account widechar */ ctx->len -= count; ctx->pos -= count; ctx->line[ctx->len] = L'\0'; return 0; } /* nulls line and sets pos, len and start to 0 */ void reset_buf(ChatContext *ctx) { ctx->line[0] = L'\0'; ctx->pos = 0; ctx->len = 0; ctx->start = 0; } /* Removes trailing spaces and newlines from line. */ void rm_trailing_spaces_buf(ChatContext *ctx) { if (ctx->len <= 0) { return; } if (ctx->line[ctx->len - 1] != ' ' && ctx->line[ctx->len - 1] != L'¶') { return; } int i; for (i = ctx->len - 1; i >= 0; --i) { if (ctx->line[i] != ' ' && ctx->line[i] != L'¶') { break; } } ctx->len = i + 1; ctx->pos = MIN(ctx->pos, ctx->len); ctx->line[ctx->len] = L'\0'; } #define HIST_PURGE MAX_LINE_HIST / 4 /* shifts hist items back and makes room for HIST_PURGE new entries */ static void shift_hist_back(ChatContext *ctx) { int i; int n = MAX_LINE_HIST - HIST_PURGE; for (i = 0; i < n; ++i) { wmemcpy(ctx->ln_history[i], ctx->ln_history[i + HIST_PURGE], MAX_STR_SIZE); } ctx->hst_tot = n; } /* adds a line to the ln_history buffer at hst_pos and sets hst_pos to end of history. */ void add_line_to_hist(ChatContext *ctx) { if (ctx->len >= MAX_STR_SIZE) { return; } if (ctx->hst_tot >= MAX_LINE_HIST) { shift_hist_back(ctx); } ++ctx->hst_tot; ctx->hst_pos = ctx->hst_tot; wmemcpy(ctx->ln_history[ctx->hst_tot - 1], ctx->line, ctx->len + 1); } /* copies history item at hst_pos to line. Sets pos and len to the len of the history item. hst_pos is decremented or incremented depending on key_dir. resets line if at end of history */ void fetch_hist_item(ChatContext *ctx, int key_dir) { if (wcscmp(ctx->line, L"\0") != 0 && ctx->hst_pos == ctx->hst_tot) { add_line_to_hist(ctx); ctx->hst_pos--; } if (key_dir == KEY_UP) { if (--ctx->hst_pos < 0) { ctx->hst_pos = 0; sound_notify(NULL, notif_error, NT_ALWAYS, NULL); } } else { if (++ctx->hst_pos >= ctx->hst_tot) { ctx->hst_pos = ctx->hst_tot; reset_buf(ctx); return; } } const wchar_t *hst_line = ctx->ln_history[ctx->hst_pos]; size_t h_len = wcslen(hst_line); wmemcpy(ctx->line, hst_line, h_len + 1); ctx->pos = h_len; ctx->len = h_len; } void strsubst(char *str, char old, char new) { int i; for (i = 0; str[i] != '\0'; ++i) { if (str[i] == old) { str[i] = new; } } } void wstrsubst(wchar_t *str, wchar_t old, wchar_t new) { int i; for (i = 0; str[i] != L'\0'; ++i) { if (str[i] == old) { str[i] = new; } } } toxic-0.11.3/src/toxic_strings.h000066400000000000000000000051411416141666600165750ustar00rootroot00000000000000/* toxic_strings.h * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef TOXIC_STRINGS_H #define TOXIC_STRINGS_H #include "windows.h" /* Adds char to line at pos. Return 0 on success, -1 if line buffer is full */ int add_char_to_buf(ChatContext *ctx, wint_t ch); /* Deletes the character before pos. Return 0 on success, -1 if nothing to delete */ int del_char_buf_bck(ChatContext *ctx); /* Deletes the character at pos. Return 0 on success, -1 if nothing to delete. */ int del_char_buf_frnt(ChatContext *ctx); /* Deletes the line from beginning to pos and puts discarded portion in yank buffer. Return 0 on success, -1 if noting to discard */ int discard_buf(ChatContext *ctx); /* Deletes the line from pos to len and puts killed portion in yank buffer. Return 0 on success, -1 if nothing to kill. */ int kill_buf(ChatContext *ctx); /* nulls line and sets pos, len and start to 0 */ void reset_buf(ChatContext *ctx); /* Inserts string in ctx->yank into line at pos. Return 0 on success, -1 if yank buffer is empty or too long */ int yank_buf(ChatContext *ctx); /* Deletes all characters from line starting at pos and going backwards until we find a space or run out of characters. Return 0 on success, -1 if no line or already at the beginning */ int del_word_buf(ChatContext *ctx); /* Removes trailing spaces from line. */ void rm_trailing_spaces_buf(ChatContext *ctx); /* adds a line to the ln_history buffer at hst_pos and sets hst_pos to last history item. */ void add_line_to_hist(ChatContext *ctx); /* copies history item at hst_pos to line. Sets pos and len to the len of the history item. hst_pos is decremented or incremented depending on key_dir. resets line if at end of history */ void fetch_hist_item(ChatContext *ctx, int key_dir); /* Substitutes all occurrences of old with new. */ void strsubst(char *str, char old, char new); void wstrsubst(wchar_t *str, wchar_t old, wchar_t new); #endif /* TOXIC_STRINGS_H */ toxic-0.11.3/src/video_call.c000066400000000000000000000322261416141666600157760ustar00rootroot00000000000000/* video_call.c * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #include "chat_commands.h" #include "global_commands.h" #include "line_info.h" #include "misc_tools.h" #include "notify.h" #include "toxic.h" #include "video_call.h" #include "video_device.h" #include "windows.h" #include #include #include #include #include #include #include #ifdef VIDEO #define DEFAULT_VIDEO_BIT_RATE 5000 void on_video_receive_frame(ToxAV *av, uint32_t friend_number, uint16_t width, uint16_t height, uint8_t const *y, uint8_t const *u, uint8_t const *v, int32_t ystride, int32_t ustride, int32_t vstride, void *user_data); void on_video_bit_rate(ToxAV *av, uint32_t friend_number, uint32_t video_bit_rate, void *user_data); static void print_err(ToxWindow *self, const char *error_str) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "%s", error_str); } ToxAV *init_video(ToxWindow *self, Tox *tox) { UNUSED_VAR(tox); CallControl.video_errors = ve_None; CallControl.video_enabled = true; CallControl.default_video_bit_rate = 0; CallControl.video_frame_duration = 10; if (!CallControl.av) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Video failed to init with ToxAV instance"); return NULL; } if (init_video_devices(CallControl.av) == vde_InternalError) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Failed to init video devices"); return NULL; } toxav_callback_video_receive_frame(CallControl.av, on_video_receive_frame, &CallControl); toxav_callback_video_bit_rate(CallControl.av, on_video_bit_rate, &CallControl); return CallControl.av; } void terminate_video(void) { int i; for (i = 0; i < CallControl.max_calls; ++i) { Call *this_call = &CallControl.calls[i]; stop_video_transmission(this_call, i); if (this_call->status == cs_Active && this_call->vout_idx != -1) { close_video_device(vdt_output, this_call->vout_idx); this_call->vout_idx = -1; } } terminate_video_devices(); } void read_video_device_callback(int16_t width, int16_t height, const uint8_t *y, const uint8_t *u, const uint8_t *v, void *data) { uint32_t friend_number = *((uint32_t *)data); /* TODO: Or pass an array of call_idx's */ Call *this_call = &CallControl.calls[friend_number]; Toxav_Err_Send_Frame error; /* Drop frame if video sending is disabled */ if (this_call->video_bit_rate == 0 || this_call->status != cs_Active || this_call->vin_idx == -1) { line_info_add(CallControl.prompt, false, NULL, NULL, SYS_MSG, 0, 0, "Video frame dropped."); return; } if (toxav_video_send_frame(CallControl.av, friend_number, width, height, y, u, v, &error) == false) { line_info_add(CallControl.prompt, false, NULL, NULL, SYS_MSG, 0, 0, "Failed to send video frame"); if (error == TOXAV_ERR_SEND_FRAME_NULL) { line_info_add(CallControl.prompt, false, NULL, NULL, SYS_MSG, 0, 0, "Failed to capture video frame"); } else if (error == TOXAV_ERR_SEND_FRAME_INVALID) { line_info_add(CallControl.prompt, false, NULL, NULL, SYS_MSG, 0, 0, "Failed to prepare video frame"); } } } void write_video_device_callback(uint32_t friend_number, uint16_t width, uint16_t height, uint8_t const *y, uint8_t const *u, uint8_t const *v, int32_t ystride, int32_t ustride, int32_t vstride, void *user_data) { UNUSED_VAR(friend_number); write_video_out(width, height, y, u, v, ystride, ustride, vstride, user_data); } int start_video_transmission(ToxWindow *self, ToxAV *av, Call *call) { if (!self || !av) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Failed to prepare video transmission"); return -1; } if (open_primary_video_device(vdt_input, &call->vin_idx, &call->video_width, &call->video_height) != vde_None) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Failed to open input video device!"); return -1; } if (register_video_device_callback(self->num, call->vin_idx, read_video_device_callback, &self->num) != vde_None) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Failed to register input video handler!"); return -1; } if (!toxav_video_set_bit_rate(CallControl.av, self->num, call->video_bit_rate, NULL)) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Failed to set video bit rate"); return -1; } return 0; } int stop_video_transmission(Call *call, int friend_number) { if (call->status != cs_Active) { return -1; } call->video_bit_rate = 0; toxav_video_set_bit_rate(CallControl.av, friend_number, call->video_bit_rate, NULL); if (call->vin_idx != -1) { close_video_device(vdt_input, call->vin_idx); call->vin_idx = -1; } return 0; } /* * End of transmission */ /* * Callbacks */ void on_video_receive_frame(ToxAV *av, uint32_t friend_number, uint16_t width, uint16_t height, uint8_t const *y, uint8_t const *u, uint8_t const *v, int32_t ystride, int32_t ustride, int32_t vstride, void *user_data) { UNUSED_VAR(av); write_video_device_callback(friend_number, width, height, y, u, v, ystride, ustride, vstride, user_data); } void on_video_bit_rate(ToxAV *av, uint32_t friend_number, uint32_t video_bit_rate, void *user_data) { UNUSED_VAR(av); UNUSED_VAR(user_data); Call *call = &CallControl.calls[friend_number]; call->video_bit_rate = video_bit_rate; /* TODO: with current toxav using one-pass VP8, the value of * video_bit_rate has no effect, except to disable video if it is 0. * Automatically change resolution instead? */ toxav_video_set_bit_rate(CallControl.av, friend_number, call->video_bit_rate, NULL); } void callback_recv_video_starting(uint32_t friend_number) { Call *this_call = &CallControl.calls[friend_number]; if (this_call->status != cs_Active || this_call->vout_idx != -1) { return; } open_primary_video_device(vdt_output, &this_call->vout_idx, NULL, NULL); } void callback_recv_video_end(uint32_t friend_number) { Call *this_call = &CallControl.calls[friend_number]; if (this_call->status != cs_Active || this_call->vout_idx == -1) { return; } close_video_device(vdt_output, this_call->vout_idx); this_call->vout_idx = -1; } void callback_video_starting(uint32_t friend_number) { Call *this_call = &CallControl.calls[friend_number]; Toxav_Err_Call_Control error = TOXAV_ERR_CALL_CONTROL_OK; toxav_call_control(CallControl.av, friend_number, TOXAV_CALL_CONTROL_SHOW_VIDEO, &error); if (error == TOXAV_ERR_CALL_CONTROL_OK) { size_t i; for (i = 0; i < MAX_WINDOWS_NUM; ++i) { ToxWindow *window = get_window_ptr(i); if (window != NULL && window->is_call && window->num == friend_number) { if (start_video_transmission(window, CallControl.av, this_call) == 0) { line_info_add(window, NULL, NULL, NULL, SYS_MSG, 0, 0, "Video capture starting."); } } } } } void callback_video_end(uint32_t friend_number) { stop_video_transmission(&CallControl.calls[friend_number], friend_number); } /* * End of Callbacks */ /* * Commands from chat_commands.h */ void cmd_vcall(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); UNUSED_VAR(m); UNUSED_VAR(argv); if (argc != 0) { print_err(self, "Unknown arguments."); return; } if (!CallControl.av) { print_err(self, "ToxAV not supported!"); return; } if (!self->stb->connection) { print_err(self, "Friend is offline."); return; } Call *call = &CallControl.calls[self->num]; if (call->status != cs_None) { print_err(self, "Already calling."); return; } init_call(call); call->video_bit_rate = DEFAULT_VIDEO_BIT_RATE; place_call(self); } void cmd_video(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); UNUSED_VAR(m); UNUSED_VAR(argv); Call *this_call = &CallControl.calls[self->num]; if (argc != 0) { print_err(self, "Unknown arguments."); return; } if (!CallControl.av) { print_err(self, "ToxAV not supported!"); return; } if (!self->stb->connection) { print_err(self, "Friend is offline."); return; } if (this_call->status != cs_Active) { print_err(self, "Not in call!"); return; } if (this_call->vin_idx == -1) { this_call->video_bit_rate = DEFAULT_VIDEO_BIT_RATE; callback_video_starting(self->num); } else { callback_video_end(self->num); } } void cmd_res(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); UNUSED_VAR(m); Call *call = &CallControl.calls[self->num]; if (argc == 0) { if (call->status == cs_Active && call->vin_idx != -1) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Resolution of current call: %u x %u", call->video_width, call->video_height); } else { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Initial resolution for video calls: %u x %u", CallControl.default_video_width, CallControl.default_video_height); } return; } if (argc != 2) { print_err(self, "Require 0 or 2 arguments."); return; } char *endw, *endh; const long int width = strtol(argv[1], &endw, 10); const long int height = strtol(argv[2], &endh, 10); if (*endw || *endh || width < 0 || height < 0) { print_err(self, "Invalid input"); return; } if (call->status == cs_Active && call->vin_idx != -1) { stop_video_transmission(call, self->num); call->video_width = width; call->video_height = height; call->video_bit_rate = DEFAULT_VIDEO_BIT_RATE; start_video_transmission(self, CallControl.av, call); } else { CallControl.default_video_width = width; CallControl.default_video_height = height; } } void cmd_list_video_devices(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); UNUSED_VAR(m); if (argc != 1) { if (argc < 1) { print_err(self, "Type must be specified!"); } else { print_err(self, "Only one argument allowed!"); } return; } VideoDeviceType type; if (strcasecmp(argv[1], "in") == 0) { /* Input devices */ type = vdt_input; } else if (strcasecmp(argv[1], "out") == 0) { /* Output devices */ type = vdt_output; } else { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Invalid type: %s", argv[1]); return; } print_video_devices(self, type); } /* This changes primary video device only */ void cmd_change_video_device(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); UNUSED_VAR(m); if (argc != 2) { if (argc < 1) { print_err(self, "Type must be specified!"); } else if (argc < 2) { print_err(self, "Must have id!"); } else { print_err(self, "Only two arguments allowed!"); } return; } VideoDeviceType type; if (strcmp(argv[1], "in") == 0) { /* Input devices */ type = vdt_input; } else if (strcmp(argv[1], "out") == 0) { /* Output devices */ type = vdt_output; } else { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Invalid type: %s", argv[1]); return; } char *end; long int selection = strtol(argv[2], &end, 10); if (*end) { print_err(self, "Invalid input"); return; } if (set_primary_video_device(type, selection) == vde_InvalidSelection) { print_err(self, "Invalid selection!"); return; } } #endif /* VIDEO */ toxic-0.11.3/src/video_call.h000066400000000000000000000026001416141666600157740ustar00rootroot00000000000000/* video_call.h * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef VIDEO_CALL_H #define VIDEO_CALL_H #include #include "audio_call.h" #include "video_device.h" /* You will have to pass pointer to first member of 'windows' declared in windows.c */ ToxAV *init_video(ToxWindow *self, Tox *tox); void terminate_video(void); int start_video_transmission(ToxWindow *self, ToxAV *av, Call *call); int stop_video_transmission(Call *call, int friend_number); void callback_recv_video_starting(uint32_t friend_number); void callback_recv_video_end(uint32_t friend_number); void callback_video_starting(uint32_t friend_number); void callback_video_end(uint32_t friend_number); #endif /* VIDEO_CALL_H */ toxic-0.11.3/src/video_device.c000066400000000000000000000650031416141666600163210ustar00rootroot00000000000000/* video_device.c * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #include "video_call.h" #include "video_device.h" #include #include #if defined(__OSX__) || defined(__APPLE__) #import "osx_video.h" #else #include #include #include #include #include #include #include #if defined(__OpenBSD__) || defined(__NetBSD__) #include #else #include #endif /* defined(__OpenBSD__) || defined(__NetBSD__) */ #endif /* __OSX__ || __APPLE__ */ #include "line_info.h" #include "misc_tools.h" #include "settings.h" #include #include #include #include #include #include #ifdef VIDEO #define inline__ inline __attribute__((always_inline)) extern struct user_settings *user_settings; struct VideoBuffer { void *start; size_t length; }; typedef struct VideoDevice { VideoDataHandleCallback cb; /* Use this to handle data from input device usually */ void *cb_data; /* Data to be passed to callback */ int32_t friend_number; /* ToxAV friend number */ #if !(defined(__OSX__) || defined(__APPLE__)) int fd; /* File descriptor of video device selected/opened */ struct v4l2_format fmt; struct VideoBuffer *buffers; uint32_t n_buffers; #endif uint32_t ref_count; int32_t selection; pthread_mutex_t mutex[1]; uint16_t video_width; uint16_t video_height; vpx_image_t input; Display *x_display; Window x_window; GC x_gc; } VideoDevice; static const char *dvideo_device_names[2]; /* Default device */ static char *video_devices_names[2][MAX_DEVICES]; /* Container of available devices */ static int size[2]; /* Size of above containers */ static VideoDevice *video_devices_running[2][MAX_DEVICES] = {{NULL}}; /* Running devices */ static uint32_t primary_video_device[2]; /* Primary device */ static ToxAV *av = NULL; /* q_mutex */ #define lock pthread_mutex_lock(&video_mutex) #define unlock pthread_mutex_unlock(&video_mutex) static pthread_mutex_t video_mutex; static bool video_thread_running = true; static bool video_thread_paused = true; /* Thread control */ void *video_thread_poll(void *); static void yuv420tobgr(uint16_t width, uint16_t height, const uint8_t *y, const uint8_t *u, const uint8_t *v, unsigned int ystride, unsigned int ustride, unsigned int vstride, uint8_t *out) { unsigned long int i, j; for (i = 0; i < height; ++i) { for (j = 0; j < width; ++j) { uint8_t *point = out + 4 * ((i * width) + j); int t_y = y[((i * ystride) + j)]; int t_u = u[(((i / 2) * ustride) + (j / 2))]; int t_v = v[(((i / 2) * vstride) + (j / 2))]; t_y = t_y < 16 ? 16 : t_y; int r = (298 * (t_y - 16) + 409 * (t_v - 128) + 128) >> 8; int g = (298 * (t_y - 16) - 100 * (t_u - 128) - 208 * (t_v - 128) + 128) >> 8; int b = (298 * (t_y - 16) + 516 * (t_u - 128) + 128) >> 8; point[2] = r > 255 ? 255 : r < 0 ? 0 : r; point[1] = g > 255 ? 255 : g < 0 ? 0 : g; point[0] = b > 255 ? 255 : b < 0 ? 0 : b; point[3] = ~0; } } } #if !(defined(__OSX__) || defined(__APPLE__)) static void yuv422to420(uint8_t *plane_y, uint8_t *plane_u, uint8_t *plane_v, uint8_t *input, uint16_t width, uint16_t height) { uint8_t *end = input + width * height * 2; while (input != end) { uint8_t *line_end = input + width * 2; while (input != line_end) { *plane_y++ = *input++; *plane_u++ = *input++; *plane_y++ = *input++; *plane_v++ = *input++; } line_end = input + width * 2; while (input != line_end) { *plane_y++ = *input++; input++;//u *plane_y++ = *input++; input++;//v } } } static int xioctl(int fh, unsigned long request, void *arg) { int r; do { r = ioctl(fh, request, arg); } while (-1 == r && EINTR == errno); return r; } #endif /* Meet devices */ #ifdef VIDEO VideoDeviceError init_video_devices(ToxAV *av_) #else VideoDeviceError init_video_devices(void) #endif /* VIDEO */ { size[vdt_input] = 0; #if defined(__OSX__) || defined(__APPLE__) if (osx_video_init(&video_devices_names[vdt_input][0], &size[vdt_input]) != 0) { return vde_InternalError; } #else /* not __OSX__ || __APPLE__ */ for (; size[vdt_input] <= MAX_DEVICES; ++size[vdt_input]) { int fd; char device_address[] = "/dev/videoXX"; snprintf(device_address + 10, sizeof(char) * strlen(device_address) - 10, "%i", size[vdt_input]); fd = open(device_address, O_RDWR | O_NONBLOCK, 0); if (fd == -1) { break; } else { struct v4l2_capability cap; char *video_input_name; /* Query V4L for capture capabilities */ if (-1 != ioctl(fd, VIDIOC_QUERYCAP, &cap)) { video_input_name = (char *)malloc(strlen((const char *)cap.card) + strlen(device_address) + 4); if (video_input_name == NULL) { close(fd); return vde_InternalError; } strcpy(video_input_name, (char *)cap.card); strcat(video_input_name, " ("); strcat(video_input_name, (char *)device_address); strcat(video_input_name, ")"); } else { video_input_name = (char *)malloc(strlen(device_address) + 3); if (video_input_name == NULL) { close(fd); return vde_InternalError; } strcpy(video_input_name, "("); strcat(video_input_name, device_address); strcat(video_input_name, ")"); } video_devices_names[vdt_input][size[vdt_input]] = video_input_name; close(fd); } } #endif size[vdt_output] = 1; // TODO(iphydf): String literals are const char *. This may need to be // copied, or if we're not owning any output device names, it should be // const and video_devices_names needs to be split. char *video_output_name = "Toxic Video Receiver"; video_devices_names[vdt_output][0] = video_output_name; // Start poll thread if (pthread_mutex_init(&video_mutex, NULL) != 0) { return vde_InternalError; } pthread_t thread_id; if (pthread_create(&thread_id, NULL, video_thread_poll, NULL) != 0 || pthread_detach(thread_id) != 0) { return vde_InternalError; } #ifdef VIDEO av = av_; #endif /* VIDEO */ return (VideoDeviceError) vde_None; } VideoDeviceError terminate_video_devices(void) { /* Cleanup if needed */ lock; video_thread_running = false; unlock; sleep_thread(20000L); int i; for (i = 0; i < size[vdt_input]; ++i) { free(video_devices_names[vdt_input][i]); } if (pthread_mutex_destroy(&video_mutex) != 0) { return (VideoDeviceError) vde_InternalError; } #if defined(__OSX__) || defined(__APPLE__) osx_video_release(); #endif /* __OSX__ || __APPLE__ */ return (VideoDeviceError) vde_None; } VideoDeviceError register_video_device_callback(int32_t friend_number, uint32_t device_idx, VideoDataHandleCallback callback, void *data) { #if defined(__OSX__) || defined(__APPLE__) if (size[vdt_input] <= device_idx || !video_devices_running[vdt_input][device_idx]) { return vde_InvalidSelection; } #else /* not __OSX__ || __APPLE__ */ if (size[vdt_input] <= device_idx || !video_devices_running[vdt_input][device_idx] || !video_devices_running[vdt_input][device_idx]->fd) { return vde_InvalidSelection; } #endif lock; video_devices_running[vdt_input][device_idx]->cb = callback; video_devices_running[vdt_input][device_idx]->cb_data = data; video_devices_running[vdt_input][device_idx]->friend_number = friend_number; unlock; return vde_None; } VideoDeviceError set_primary_video_device(VideoDeviceType type, int32_t selection) { if (size[type] <= selection || selection < 0) { return vde_InvalidSelection; } primary_video_device[type] = selection; return vde_None; } VideoDeviceError open_primary_video_device(VideoDeviceType type, uint32_t *device_idx, uint32_t *width, uint32_t *height) { return open_video_device(type, primary_video_device[type], device_idx, width, height); } void get_primary_video_device_name(VideoDeviceType type, char *buf, int size) { memcpy(buf, dvideo_device_names[type], size); } VideoDeviceError open_video_device(VideoDeviceType type, int32_t selection, uint32_t *device_idx, uint32_t *width, uint32_t *height) { if (size[type] <= selection || selection < 0) { return vde_InvalidSelection; } lock; uint32_t i, temp_idx = -1; for (i = 0; i < MAX_DEVICES; ++i) { if (!video_devices_running[type][i]) { temp_idx = i; break; } } if (temp_idx == -1) { unlock; return vde_AllDevicesBusy; } for (i = 0; i < MAX_DEVICES; i ++) { /* Check if any device has the same selection */ if (video_devices_running[type][i] && video_devices_running[type][i]->selection == selection) { video_devices_running[type][temp_idx] = video_devices_running[type][i]; video_devices_running[type][i]->ref_count++; unlock; return vde_None; } } VideoDevice *device = video_devices_running[type][temp_idx] = calloc(1, sizeof(VideoDevice)); device->selection = selection; if (pthread_mutex_init(device->mutex, NULL) != 0) { free(device); unlock; return vde_InternalError; } if (type == vdt_input) { video_thread_paused = true; #if defined(__OSX__) || defined(__APPLE__) /* TODO: use requested resolution */ if (osx_video_open_device(selection, &device->video_width, &device->video_height) != 0) { free(device); unlock; return vde_FailedStart; } #else /* not __OSX__ || __APPLE__ */ /* Open selected device */ char device_address[] = "/dev/videoXX"; snprintf(device_address + 10, sizeof(device_address) - 10, "%i", selection); device->fd = open(device_address, O_RDWR); if (device->fd == -1) { unlock; return vde_FailedStart; } /* Obtain video device capabilities */ struct v4l2_capability cap; if (-1 == xioctl(device->fd, VIDIOC_QUERYCAP, &cap)) { close(device->fd); free(device); unlock; return vde_FailedStart; } /* Setup video format */ struct v4l2_format fmt = {0}; fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; fmt.fmt.pix.width = width == NULL ? 0 : *width; fmt.fmt.pix.height = height == NULL ? 0 : *height; if (-1 == xioctl(device->fd, VIDIOC_S_FMT, &fmt)) { close(device->fd); free(device); unlock; return vde_FailedStart; } device->video_width = fmt.fmt.pix.width; device->video_height = fmt.fmt.pix.height; /* Request buffers */ struct v4l2_requestbuffers req = {0}; req.count = 4; req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; req.memory = V4L2_MEMORY_MMAP; if (-1 == xioctl(device->fd, VIDIOC_REQBUFS, &req)) { close(device->fd); free(device); unlock; return vde_FailedStart; } if (req.count < 2) { close(device->fd); free(device); unlock; return vde_FailedStart; } device->buffers = calloc(req.count, sizeof(struct VideoBuffer)); for (i = 0; i < req.count; ++i) { struct v4l2_buffer buf = {0}; buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; buf.index = i; if (-1 == xioctl(device->fd, VIDIOC_QUERYBUF, &buf)) { close(device->fd); free(device); unlock; return vde_FailedStart; } device->buffers[i].length = buf.length; device->buffers[i].start = mmap(NULL /* start anywhere */, buf.length, PROT_READ | PROT_WRITE /* required */, MAP_SHARED /* recommended */, device->fd, buf.m.offset); if (MAP_FAILED == device->buffers[i].start) { for (i = 0; i < buf.index; ++i) { munmap(device->buffers[i].start, device->buffers[i].length); } close(device->fd); free(device); unlock; return vde_FailedStart; } } device->n_buffers = i; enum v4l2_buf_type type; for (i = 0; i < device->n_buffers; ++i) { struct v4l2_buffer buf = {0}; buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; buf.index = i; if (-1 == xioctl(device->fd, VIDIOC_QBUF, &buf)) { for (i = 0; i < device->n_buffers; ++i) { munmap(device->buffers[i].start, device->buffers[i].length); } close(device->fd); free(device); unlock; return vde_FailedStart; } } type = V4L2_BUF_TYPE_VIDEO_CAPTURE; /* Turn on video stream */ if (-1 == xioctl(device->fd, VIDIOC_STREAMON, &type)) { close_video_device(vdt_input, temp_idx); unlock; return vde_FailedStart; } #endif /* Create X11 window associated to device */ if ((device->x_display = XOpenDisplay(NULL)) == NULL) { close_video_device(vdt_input, temp_idx); unlock; return vde_FailedStart; } int screen = DefaultScreen(device->x_display); if (!(device->x_window = XCreateSimpleWindow(device->x_display, RootWindow(device->x_display, screen), 0, 0, device->video_width, device->video_height, 0, BlackPixel(device->x_display, screen), BlackPixel(device->x_display, screen)))) { close_video_device(vdt_input, temp_idx); unlock; return vde_FailedStart; } XStoreName(device->x_display, device->x_window, "Video Preview"); XSelectInput(device->x_display, device->x_window, ExposureMask | ButtonPressMask | KeyPressMask); if ((device->x_gc = DefaultGC(device->x_display, screen)) == NULL) { close_video_device(vdt_input, temp_idx); unlock; return vde_FailedStart; } /* Disable user from manually closing the X11 window */ Atom wm_delete_window = XInternAtom(device->x_display, "WM_DELETE_WINDOW", false); XSetWMProtocols(device->x_display, device->x_window, &wm_delete_window, 1); XMapWindow(device->x_display, device->x_window); XClearWindow(device->x_display, device->x_window); XMapRaised(device->x_display, device->x_window); XFlush(device->x_display); vpx_img_alloc(&device->input, VPX_IMG_FMT_I420, device->video_width, device->video_height, 1); if (width != NULL) { *width = device->video_width; } if (height != NULL) { *height = device->video_height; } video_thread_paused = false; } else { /* vdt_output */ /* Create X11 window associated to device */ if ((device->x_display = XOpenDisplay(NULL)) == NULL) { close_video_device(vdt_output, temp_idx); unlock; return vde_FailedStart; } int screen = DefaultScreen(device->x_display); if (!(device->x_window = XCreateSimpleWindow(device->x_display, RootWindow(device->x_display, screen), 0, 0, 100, 100, 0, BlackPixel(device->x_display, screen), BlackPixel(device->x_display, screen)))) { close_video_device(vdt_output, temp_idx); unlock; return vde_FailedStart; } XStoreName(device->x_display, device->x_window, "Video Receive"); XSelectInput(device->x_display, device->x_window, ExposureMask | ButtonPressMask | KeyPressMask); if ((device->x_gc = DefaultGC(device->x_display, screen)) == NULL) { close_video_device(vdt_output, temp_idx); unlock; return vde_FailedStart; } /* Disable user from manually closing the X11 window */ Atom wm_delete_window = XInternAtom(device->x_display, "WM_DELETE_WINDOW", false); XSetWMProtocols(device->x_display, device->x_window, &wm_delete_window, 1); XMapWindow(device->x_display, device->x_window); XClearWindow(device->x_display, device->x_window); XMapRaised(device->x_display, device->x_window); XFlush(device->x_display); vpx_img_alloc(&device->input, VPX_IMG_FMT_I420, device->video_width, device->video_height, 1); } *device_idx = temp_idx; unlock; return vde_None; } VideoDeviceError write_video_out(uint16_t width, uint16_t height, uint8_t const *y, uint8_t const *u, uint8_t const *v, int32_t ystride, int32_t ustride, int32_t vstride, void *user_data) { UNUSED_VAR(user_data); VideoDevice *device = video_devices_running[vdt_output][0]; if (!device) { return vde_DeviceNotActive; } if (!device->x_window) { return vde_DeviceNotActive; } pthread_mutex_lock(device->mutex); /* Resize X11 window to correct size */ if (device->video_width != width || device->video_height != height) { device->video_width = width; device->video_height = height; XResizeWindow(device->x_display, device->x_window, width, height); vpx_img_free(&device->input); vpx_img_alloc(&device->input, VPX_IMG_FMT_I420, width, height, 1); } /* Convert YUV420 data to BGR */ ystride = abs(ystride); ustride = abs(ustride); vstride = abs(vstride); uint8_t *img_data = malloc(width * height * 4); yuv420tobgr(width, height, y, u, v, ystride, ustride, vstride, img_data); /* Allocate image data in X11 */ XImage image = { .width = width, .height = height, .depth = 24, .bits_per_pixel = 32, .format = ZPixmap, .byte_order = LSBFirst, .bitmap_unit = 8, .bitmap_bit_order = LSBFirst, .bytes_per_line = width * 4, .red_mask = 0xFF0000, .green_mask = 0xFF00, .blue_mask = 0xFF, .data = (char *)img_data }; /* Render image data */ Pixmap pixmap = XCreatePixmap(device->x_display, device->x_window, width, height, 24); XPutImage(device->x_display, pixmap, device->x_gc, &image, 0, 0, 0, 0, width, height); XCopyArea(device->x_display, pixmap, device->x_window, device->x_gc, 0, 0, width, height, 0, 0); XFreePixmap(device->x_display, pixmap); XFlush(device->x_display); free(img_data); pthread_mutex_unlock(device->mutex); return vde_None; } void *video_thread_poll(void *arg) // TODO: maybe use thread for every input source { /* * NOTE: We only need to poll input devices for data. */ UNUSED_VAR(arg); uint32_t i; while (1) { lock; if (!video_thread_running) { unlock; break; } unlock; if (video_thread_paused) { sleep_thread(10000L); /* Wait for unpause. */ } else { for (i = 0; i < size[vdt_input]; ++i) { lock; if (video_devices_running[vdt_input][i] != NULL) { /* Obtain frame image data from device buffers */ VideoDevice *device = video_devices_running[vdt_input][i]; uint16_t video_width = device->video_width; uint16_t video_height = device->video_height; uint8_t *y = device->input.planes[0]; uint8_t *u = device->input.planes[1]; uint8_t *v = device->input.planes[2]; #if defined(__OSX__) || defined(__APPLE__) if (osx_video_read_device(y, u, v, &video_width, &video_height) != 0) { unlock; continue; } #else /* not __OSX__ || __APPLE__ */ struct v4l2_buffer buf = {0}; buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; if (-1 == ioctl(device->fd, VIDIOC_DQBUF, &buf)) { unlock; continue; } void *data = (void *)device->buffers[buf.index].start; /* Convert frame image data to YUV420 for ToxAV */ yuv422to420(y, u, v, data, video_width, video_height); #endif /* Send frame data to friend through ToxAV */ if (device->cb) { device->cb(video_width, video_height, y, u, v, device->cb_data); } /* Convert YUV420 data to BGR */ uint8_t *img_data = malloc(video_width * video_height * 4); yuv420tobgr(video_width, video_height, y, u, v, video_width, video_width / 2, video_width / 2, img_data); /* Allocate image data in X11 */ XImage image = { .width = video_width, .height = video_height, .depth = 24, .bits_per_pixel = 32, .format = ZPixmap, .byte_order = LSBFirst, .bitmap_unit = 8, .bitmap_bit_order = LSBFirst, .bytes_per_line = video_width * 4, .red_mask = 0xFF0000, .green_mask = 0xFF00, .blue_mask = 0xFF, .data = (char *)img_data }; /* Render image data */ Pixmap pixmap = XCreatePixmap(device->x_display, device->x_window, video_width, video_height, 24); XPutImage(device->x_display, pixmap, device->x_gc, &image, 0, 0, 0, 0, video_width, video_height); XCopyArea(device->x_display, pixmap, device->x_window, device->x_gc, 0, 0, video_width, video_height, 0, 0); XFreePixmap(device->x_display, pixmap); XFlush(device->x_display); free(img_data); #if !(defined(__OSX__) || defined(__APPLE__)) if (-1 == xioctl(device->fd, VIDIOC_QBUF, &buf)) { unlock; continue; } #endif } unlock; } long int sleep_duration = 1000 * 1000 / 24; sleep_thread(sleep_duration); } } pthread_exit(NULL); } VideoDeviceError close_video_device(VideoDeviceType type, uint32_t device_idx) { if (device_idx >= MAX_DEVICES) { return vde_InvalidSelection; } lock; VideoDevice *device = video_devices_running[type][device_idx]; VideoDeviceError rc = vde_None; if (!device) { unlock; return vde_DeviceNotActive; } video_devices_running[type][device_idx] = NULL; if (!device->ref_count) { if (type == vdt_input) { #if defined(__OSX__) || defined(__APPLE__) osx_video_close_device(device_idx); #else /* not __OSX__ || __APPLE__ */ enum v4l2_buf_type buf_type = V4L2_BUF_TYPE_VIDEO_CAPTURE; if (-1 == xioctl(device->fd, VIDIOC_STREAMOFF, &buf_type)) {} int i; for (i = 0; i < device->n_buffers; ++i) { if (-1 == munmap(device->buffers[i].start, device->buffers[i].length)) { } } close(device->fd); #endif vpx_img_free(&device->input); XDestroyWindow(device->x_display, device->x_window); XFlush(device->x_display); XCloseDisplay(device->x_display); pthread_mutex_destroy(device->mutex); #if !(defined(__OSX__) || defined(__APPLE__)) free(device->buffers); #endif /* not __OSX__ || __APPLE__ */ free(device); } else { vpx_img_free(&device->input); XDestroyWindow(device->x_display, device->x_window); XFlush(device->x_display); XCloseDisplay(device->x_display); pthread_mutex_destroy(device->mutex); free(device); } } else { device->ref_count--; } unlock; return rc; } void print_video_devices(ToxWindow *self, VideoDeviceType type) { int i; for (i = 0; i < size[type]; ++i) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "%d: %s", i, video_devices_names[type][i]); } return; } VideoDeviceError video_selection_valid(VideoDeviceType type, int32_t selection) { return (size[type] <= selection || selection < 0) ? vde_InvalidSelection : vde_None; } #endif /* VIDEO */ toxic-0.11.3/src/video_device.h000066400000000000000000000055031416141666600163250ustar00rootroot00000000000000/* video_device.h * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef VIDEO_DEVICE_H #define VIDEO_DEVICE_H #define MAX_DEVICES 32 #include #include "windows.h" typedef enum VideoDeviceType { vdt_input, vdt_output, } VideoDeviceType; typedef enum VideoDeviceError { vde_None, vde_InternalError = -1, vde_InvalidSelection = -2, vde_FailedStart = -3, vde_Busy = -4, vde_AllDevicesBusy = -5, vde_DeviceNotActive = -6, vde_BufferError = -7, vde_UnsupportedMode = -8, vde_CaptureError = -9, } VideoDeviceError; typedef void (*VideoDataHandleCallback)(int16_t width, int16_t height, const uint8_t *y, const uint8_t *u, const uint8_t *v, void *data); #ifdef VIDEO VideoDeviceError init_video_devices(ToxAV *av); #else VideoDeviceError init_video_devices(void); #endif /* VIDEO */ VideoDeviceError terminate_video_devices(void); /* Callback handles ready data from INPUT device */ VideoDeviceError register_video_device_callback(int32_t call_idx, uint32_t device_idx, VideoDataHandleCallback callback, void *data); void *get_video_device_callback_data(uint32_t device_idx); VideoDeviceError set_primary_video_device(VideoDeviceType type, int32_t selection); VideoDeviceError open_primary_video_device(VideoDeviceType type, uint32_t *device_idx, uint32_t *width, uint32_t *height); /* Start device */ VideoDeviceError open_video_device(VideoDeviceType type, int32_t selection, uint32_t *device_idx, uint32_t *width, uint32_t *height); /* Stop device */ VideoDeviceError close_video_device(VideoDeviceType type, uint32_t device_idx); /* Write data to device */ VideoDeviceError write_video_out(uint16_t width, uint16_t height, uint8_t const *y, uint8_t const *u, uint8_t const *v, int32_t ystride, int32_t ustride, int32_t vstride, void *user_data); void print_video_devices(ToxWindow *self, VideoDeviceType type); void get_primary_video_device_name(VideoDeviceType type, char *buf, int size); VideoDeviceError video_selection_valid(VideoDeviceType type, int32_t selection); #endif /* VIDEO_DEVICE_H */ toxic-0.11.3/src/windows.c000066400000000000000000000635051416141666600153730ustar00rootroot00000000000000/* windows.c * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #include #include #include #include #include "avatars.h" #include "chat.h" #include "conference.h" #include "file_transfers.h" #include "friendlist.h" #include "line_info.h" #include "misc_tools.h" #include "prompt.h" #include "settings.h" #include "toxic.h" #include "windows.h" #ifdef GAMES #include "game_base.h" #endif ToxWindow *windows[MAX_WINDOWS_NUM]; static uint8_t active_window_index; static int num_active_windows; /* CALLBACKS START */ void on_friend_request(Tox *m, const uint8_t *public_key, const uint8_t *data, size_t length, void *userdata) { UNUSED_VAR(userdata); char msg[MAX_STR_SIZE + 1]; length = copy_tox_str(msg, sizeof(msg), (const char *) data, length); for (uint8_t i = 0; i < MAX_WINDOWS_NUM; ++i) { if (windows[i] != NULL && windows[i]->onFriendRequest != NULL) { windows[i]->onFriendRequest(windows[i], m, (const char *) public_key, msg, length); } } } void on_friend_connection_status(Tox *m, uint32_t friendnumber, Tox_Connection connection_status, void *userdata) { UNUSED_VAR(userdata); on_avatar_friend_connection_status(m, friendnumber, connection_status); for (uint8_t i = 0; i < MAX_WINDOWS_NUM; ++i) { if (windows[i] != NULL && windows[i]->onConnectionChange != NULL) { windows[i]->onConnectionChange(windows[i], m, friendnumber, connection_status); } } flag_interface_refresh(); } void on_friend_typing(Tox *m, uint32_t friendnumber, bool is_typing, void *userdata) { UNUSED_VAR(userdata); if (user_settings->show_typing_other == SHOW_TYPING_OFF) { return; } for (uint8_t i = 0; i < MAX_WINDOWS_NUM; ++i) { if (windows[i] != NULL && windows[i]->onTypingChange != NULL) { windows[i]->onTypingChange(windows[i], m, friendnumber, is_typing); } } flag_interface_refresh(); } void on_friend_message(Tox *m, uint32_t friendnumber, Tox_Message_Type type, const uint8_t *string, size_t length, void *userdata) { UNUSED_VAR(userdata); char msg[MAX_STR_SIZE + 1]; length = copy_tox_str(msg, sizeof(msg), (const char *) string, length); for (uint8_t i = 0; i < MAX_WINDOWS_NUM; ++i) { if (windows[i] != NULL && windows[i]->onMessage != NULL) { windows[i]->onMessage(windows[i], m, friendnumber, type, msg, length); } } } void on_friend_name(Tox *m, uint32_t friendnumber, const uint8_t *string, size_t length, void *userdata) { UNUSED_VAR(userdata); char nick[TOXIC_MAX_NAME_LENGTH + 1]; length = copy_tox_str(nick, sizeof(nick), (const char *) string, length); filter_str(nick, length); for (uint8_t i = 0; i < MAX_WINDOWS_NUM; ++i) { if (windows[i] != NULL && windows[i]->onNickChange != NULL) { windows[i]->onNickChange(windows[i], m, friendnumber, nick, length); } } flag_interface_refresh(); store_data(m, DATA_FILE); } void on_friend_status_message(Tox *m, uint32_t friendnumber, const uint8_t *string, size_t length, void *userdata) { UNUSED_VAR(userdata); UNUSED_VAR(m); char msg[TOX_MAX_STATUS_MESSAGE_LENGTH + 1]; length = copy_tox_str(msg, sizeof(msg), (const char *) string, length); filter_str(msg, length); for (uint8_t i = 0; i < MAX_WINDOWS_NUM; ++i) { if (windows[i] != NULL && windows[i]->onStatusMessageChange != NULL) { windows[i]->onStatusMessageChange(windows[i], friendnumber, msg, length); } } flag_interface_refresh(); } void on_friend_status(Tox *m, uint32_t friendnumber, Tox_User_Status status, void *userdata) { UNUSED_VAR(userdata); for (uint8_t i = 0; i < MAX_WINDOWS_NUM; ++i) { if (windows[i] != NULL && windows[i]->onStatusChange != NULL) { windows[i]->onStatusChange(windows[i], m, friendnumber, status); } } flag_interface_refresh(); } void on_friend_added(Tox *m, uint32_t friendnumber, bool sort) { for (uint8_t i = 0; i < MAX_WINDOWS_NUM; ++i) { if (windows[i] != NULL && windows[i]->onFriendAdded != NULL) { windows[i]->onFriendAdded(windows[i], m, friendnumber, sort); } } store_data(m, DATA_FILE); } void on_conference_message(Tox *m, uint32_t conferencenumber, uint32_t peernumber, Tox_Message_Type type, const uint8_t *message, size_t length, void *userdata) { UNUSED_VAR(userdata); char msg[MAX_STR_SIZE + 1]; length = copy_tox_str(msg, sizeof(msg), (const char *) message, length); for (uint8_t i = 0; i < MAX_WINDOWS_NUM; ++i) { if (windows[i] != NULL && windows[i]->onConferenceMessage != NULL) { windows[i]->onConferenceMessage(windows[i], m, conferencenumber, peernumber, type, msg, length); } } } void on_conference_invite(Tox *m, uint32_t friendnumber, Tox_Conference_Type type, const uint8_t *conference_pub_key, size_t length, void *userdata) { UNUSED_VAR(userdata); for (uint8_t i = 0; i < MAX_WINDOWS_NUM; ++i) { if (windows[i] != NULL && windows[i]->onConferenceInvite != NULL) { windows[i]->onConferenceInvite(windows[i], m, friendnumber, type, (const char *) conference_pub_key, length); } } } void on_conference_peer_list_changed(Tox *m, uint32_t conferencenumber, void *userdata) { UNUSED_VAR(userdata); for (uint8_t i = 0; i < MAX_WINDOWS_NUM; ++i) { if (windows[i] != NULL && windows[i]->onConferenceNameListChange != NULL) { windows[i]->onConferenceNameListChange(windows[i], m, conferencenumber); } } flag_interface_refresh(); } void on_conference_peer_name(Tox *m, uint32_t conferencenumber, uint32_t peernumber, const uint8_t *name, size_t length, void *userdata) { UNUSED_VAR(userdata); char nick[TOXIC_MAX_NAME_LENGTH + 1]; length = copy_tox_str(nick, sizeof(nick), (const char *) name, length); filter_str(nick, length); for (uint8_t i = 0; i < MAX_WINDOWS_NUM; ++i) { if (windows[i] != NULL && windows[i]->onConferencePeerNameChange != NULL) { windows[i]->onConferencePeerNameChange(windows[i], m, conferencenumber, peernumber, nick, length); } } } void on_conference_title(Tox *m, uint32_t conferencenumber, uint32_t peernumber, const uint8_t *title, size_t length, void *userdata) { UNUSED_VAR(userdata); char data[MAX_STR_SIZE + 1]; length = copy_tox_str(data, sizeof(data), (const char *) title, length); for (uint8_t i = 0; i < MAX_WINDOWS_NUM; ++i) { if (windows[i] != NULL && windows[i]->onConferenceTitleChange != NULL) { windows[i]->onConferenceTitleChange(windows[i], m, conferencenumber, peernumber, data, length); } } } void on_file_chunk_request(Tox *m, uint32_t friendnumber, uint32_t filenumber, uint64_t position, size_t length, void *userdata) { UNUSED_VAR(userdata); FileTransfer *ft = get_file_transfer_struct(friendnumber, filenumber); if (!ft) { return; } if (ft->file_type == TOX_FILE_KIND_AVATAR) { on_avatar_chunk_request(m, ft, position, length); return; } for (uint8_t i = 0; i < MAX_WINDOWS_NUM; ++i) { if (windows[i] != NULL && windows[i]->onFileChunkRequest != NULL) { windows[i]->onFileChunkRequest(windows[i], m, friendnumber, filenumber, position, length); } } } void on_file_recv_chunk(Tox *m, uint32_t friendnumber, uint32_t filenumber, uint64_t position, const uint8_t *data, size_t length, void *userdata) { UNUSED_VAR(userdata); FileTransfer *ft = get_file_transfer_struct(friendnumber, filenumber); if (!ft) { return; } for (uint8_t i = 0; i < MAX_WINDOWS_NUM; ++i) { if (windows[i] != NULL && windows[i]->onFileRecvChunk != NULL) { windows[i]->onFileRecvChunk(windows[i], m, friendnumber, filenumber, position, (const char *) data, length); } } } void on_file_recv_control(Tox *m, uint32_t friendnumber, uint32_t filenumber, Tox_File_Control control, void *userdata) { UNUSED_VAR(userdata); FileTransfer *ft = get_file_transfer_struct(friendnumber, filenumber); if (!ft) { return; } if (ft->file_type == TOX_FILE_KIND_AVATAR) { on_avatar_file_control(m, ft, control); return; } for (uint8_t i = 0; i < MAX_WINDOWS_NUM; ++i) { if (windows[i] != NULL && windows[i]->onFileControl != NULL) { windows[i]->onFileControl(windows[i], m, friendnumber, filenumber, control); } } } void on_file_recv(Tox *m, uint32_t friendnumber, uint32_t filenumber, uint32_t kind, uint64_t file_size, const uint8_t *filename, size_t filename_length, void *userdata) { UNUSED_VAR(userdata); /* We don't care about receiving avatars */ if (kind != TOX_FILE_KIND_DATA) { tox_file_control(m, friendnumber, filenumber, TOX_FILE_CONTROL_CANCEL, NULL); return; } for (uint8_t i = 0; i < MAX_WINDOWS_NUM; ++i) { if (windows[i] != NULL && windows[i]->onFileRecv != NULL) { windows[i]->onFileRecv(windows[i], m, friendnumber, filenumber, file_size, (const char *) filename, filename_length); } } } void on_friend_read_receipt(Tox *m, uint32_t friendnumber, uint32_t receipt, void *userdata) { UNUSED_VAR(userdata); for (uint8_t i = 0; i < MAX_WINDOWS_NUM; ++i) { if (windows[i] != NULL && windows[i]->onReadReceipt != NULL) { windows[i]->onReadReceipt(windows[i], m, friendnumber, receipt); } } } void on_lossless_custom_packet(Tox *m, uint32_t friendnumber, const uint8_t *data, size_t length, void *userdata) { UNUSED_VAR(userdata); if (length == 0 || data == NULL) { return; } uint8_t type = data[0]; switch (type) { #ifdef GAMES case CUSTOM_PACKET_GAME_INVITE: { for (size_t i = 0; i < MAX_WINDOWS_NUM; ++i) { ToxWindow *window = windows[i]; if (window != NULL && window->onGameInvite != NULL) { window->onGameInvite(window, m, friendnumber, data + 1, length - 1); } } break; } case CUSTOM_PACKET_GAME_DATA: { for (size_t i = 0; i < MAX_WINDOWS_NUM; ++i) { ToxWindow *window = windows[i]; if (window != NULL && window->onGameData != NULL) { window->onGameData(window, m, friendnumber, data + 1, length - 1); } } break; } #endif // GAMES default: { fprintf(stderr, "Got unknown custom packet of type: %u\n", type); return; } } } /* CALLBACKS END */ int add_window(Tox *m, ToxWindow *w) { if (LINES < 2) { return -1; } for (uint8_t i = 0; i < MAX_WINDOWS_NUM; i++) { if (windows[i] != NULL) { continue; } w->index = i; w->window = newwin(LINES, COLS, 0, 0); if (w->window == NULL) { return -1; } #ifdef URXVT_FIX /* Fixes text color problem on some terminals. */ wbkgd(w->window, COLOR_PAIR(6)); #endif windows[i] = w; if (w->onInit) { w->onInit(w, m); } ++num_active_windows; return i; } return -1; } void set_active_window_index(uint8_t index) { if (index < MAX_WINDOWS_NUM) { active_window_index = index; } } /* Displays the next window if `ch` is equal to the next window key binding. * Otherwise displays the previous window. */ static void set_next_window(int ch) { uint8_t index = 0; if (ch == user_settings->key_next_tab) { for (uint8_t i = active_window_index + 1; i < MAX_WINDOWS_NUM; ++i) { if (windows[i] != NULL) { index = i; break; } } } else { uint8_t start = active_window_index == 0 ? MAX_WINDOWS_NUM - 1 : active_window_index - 1; for (uint8_t i = start; i > 0; --i) { if (windows[i] != NULL) { index = i; break; } } } set_active_window_index(index); } /* Deletes window w and cleans up */ void del_window(ToxWindow *w) { uint8_t idx = w->index; delwin(w->window_bar); delwin(w->window); free(w); windows[idx] = NULL; clear(); refresh(); if (num_active_windows > 0) { if (active_window_index == 2) { // if closing current window would bring us to friend list set_next_window(-1); // skip back to the home window instead. FIXME: magic numbers } set_next_window(-1); --num_active_windows; } } ToxWindow *init_windows(Tox *m) { if (COLS <= CHATBOX_HEIGHT + WINDOW_BAR_HEIGHT) { exit_toxic_err("add_window() for prompt failed in init_windows", FATALERR_WININIT); } prompt = new_prompt(); int n_prompt = add_window(m, prompt); if (n_prompt < 0) { exit_toxic_err("add_window() for prompt failed in init_windows", FATALERR_WININIT); } if (add_window(m, new_friendlist()) == -1) { exit_toxic_err("add_window() for friendlist failed in init_windows", FATALERR_WININIT); } set_active_window_index(n_prompt); return prompt; } void on_window_resize(void) { endwin(); refresh(); clear(); int x2; int y2; for (uint8_t i = 0; i < MAX_WINDOWS_NUM; ++i) { ToxWindow *w = windows[i]; if (w == NULL) { continue; } if (w->type == WINDOW_TYPE_FRIEND_LIST) { delwin(w->window_bar); delwin(w->window); w->window = newwin(LINES, COLS, 0, 0); w->window_bar = subwin(w->window, WINDOW_BAR_HEIGHT, COLS, LINES - 2, 0); continue; } #ifdef GAMES if (w->type == WINDOW_TYPE_GAME) { delwin(w->window_bar); delwin(w->window); delwin(w->game->window); w->window = newwin(LINES, COLS, 0, 0); getmaxyx(w->window, y2, x2); if (y2 <= 0 || x2 <= 0) { fprintf(stderr, "Failed to resize game window: max_x: %d, max_y: %d\n", x2, y2); delwin(w->window); continue; } w->window_bar = subwin(w->window, WINDOW_BAR_HEIGHT, COLS, LINES - 2, 0); w->game->window = subwin(w->window, y2 - CHATBOX_HEIGHT - WINDOW_BAR_HEIGHT, x2, 0, 0); continue; } #endif // GAMES if (w->help->active) { wclear(w->help->win); } if (w->type == WINDOW_TYPE_CONFERENCE) { delwin(w->chatwin->sidebar); w->chatwin->sidebar = NULL; } else { delwin(w->stb->topline); } delwin(w->chatwin->linewin); delwin(w->chatwin->history); delwin(w->window_bar); delwin(w->window); w->window = newwin(LINES, COLS, 0, 0); getmaxyx(w->window, y2, x2); if (y2 <= 0 || x2 <= 0) { fprintf(stderr, "Failed to resize window: max_x: %d, max_y: %d\n", x2, y2); delwin(w->window); continue; } if (w->show_peerlist) { w->chatwin->history = subwin(w->window, y2 - CHATBOX_HEIGHT - WINDOW_BAR_HEIGHT, x2 - SIDEBAR_WIDTH - 1, 0, 0); w->chatwin->sidebar = subwin(w->window, y2 - CHATBOX_HEIGHT - WINDOW_BAR_HEIGHT, SIDEBAR_WIDTH, 0, x2 - SIDEBAR_WIDTH); } else { w->chatwin->history = subwin(w->window, y2 - CHATBOX_HEIGHT - WINDOW_BAR_HEIGHT, x2, 0, 0); if (w->type != WINDOW_TYPE_CONFERENCE) { w->stb->topline = subwin(w->window, TOP_BAR_HEIGHT, x2, 0, 0); } } w->window_bar = subwin(w->window, WINDOW_BAR_HEIGHT, x2, y2 - (CHATBOX_HEIGHT + WINDOW_BAR_HEIGHT), 0); w->chatwin->linewin = subwin(w->window, CHATBOX_HEIGHT, x2, y2 - WINDOW_BAR_HEIGHT, 0); #ifdef AUDIO if (w->chatwin->infobox.active) { delwin(w->chatwin->infobox.win); w->chatwin->infobox.win = newwin(INFOBOX_HEIGHT, INFOBOX_WIDTH + 1, 1, x2 - INFOBOX_WIDTH); } #endif /* AUDIO */ scrollok(w->chatwin->history, 0); wmove(w->window, y2 - CURS_Y_OFFSET, 0); } } static void draw_window_tab(WINDOW *win, ToxWindow *toxwin, bool active_window) { pthread_mutex_lock(&Winthread.lock); bool has_alert = toxwin->alert != WINDOW_ALERT_NONE; unsigned int pending_messages = toxwin->pending_messages; pthread_mutex_unlock(&Winthread.lock); WINDOW_TYPE type = toxwin->type; if (active_window) { wattron(win, A_BOLD | COLOR_PAIR(BAR_ACCENT)); wprintw(win, " ["); wattroff(win, COLOR_PAIR(BAR_ACCENT)); wattron(win, COLOR_PAIR(BAR_TEXT)); } else { if (has_alert) { wattron(win, COLOR_PAIR(BAR_ACCENT)); wprintw(win, " ["); wattroff(win, COLOR_PAIR(BAR_ACCENT)); wattron(win, A_BOLD | COLOR_PAIR(toxwin->alert)); } else { wattron(win, COLOR_PAIR(BAR_ACCENT)); wprintw(win, " ["); wattroff(win, COLOR_PAIR(BAR_ACCENT)); wattron(win, COLOR_PAIR(BAR_TEXT)); } } if (active_window || (type == WINDOW_TYPE_PROMPT || type == WINDOW_TYPE_FRIEND_LIST)) { wprintw(win, "%s", toxwin->name); } else { if (pending_messages > 0) { wprintw(win, "%u", pending_messages); } else { wprintw(win, "-"); } } if (active_window) { wattroff(win, COLOR_PAIR(BAR_TEXT)); wattron(win, COLOR_PAIR(BAR_ACCENT)); wprintw(win, "]"); wattroff(win, A_BOLD | COLOR_PAIR(BAR_ACCENT)); } else { if (has_alert) { wattroff(win, A_BOLD | COLOR_PAIR(toxwin->alert)); wattron(win, COLOR_PAIR(BAR_ACCENT)); wprintw(win, "]"); wattroff(win, COLOR_PAIR(BAR_ACCENT)); } else { wattroff(win, COLOR_PAIR(BAR_TEXT)); wattron(win, COLOR_PAIR(BAR_ACCENT)); wprintw(win, "]"); wattroff(win, COLOR_PAIR(BAR_ACCENT)); } } } void draw_window_bar(ToxWindow *self) { WINDOW *win = self->window_bar; wclear(win); if (self->scroll_pause) { wattron(win, A_BLINK | A_BOLD | COLOR_PAIR(BAR_NOTIFY)); wprintw(win, "^"); wattroff(win, A_BLINK | A_BOLD | COLOR_PAIR(BAR_NOTIFY)); } else { wattron(win, COLOR_PAIR(BAR_TEXT)); wprintw(win, " "); wattroff(win, COLOR_PAIR(BAR_TEXT)); } for (uint8_t i = 0; i < MAX_WINDOWS_NUM; ++i) { if (windows[i] == NULL) { continue; } bool active_window = i == active_window_index; draw_window_tab(win, windows[i], active_window); } int cur_x; int cur_y; getyx(win, cur_y, cur_x); UNUSED_VAR(cur_y); wattron(win, COLOR_PAIR(BAR_TEXT)); mvwhline(win, 0, cur_x, ' ', COLS - cur_x); wattroff(win, COLOR_PAIR(BAR_TEXT)); } /* * Gets current char from stdscr and puts it in ch. * * Return 1 if char is printable. * Return 0 if char is not printable. * Return -1 on error. */ static int get_current_char(wint_t *ch) { wint_t tmpchar = 0; bool is_printable = false; #ifdef HAVE_WIDECHAR int status = wget_wch(stdscr, &tmpchar); if (status == ERR) { return -1; } if (status == OK) { is_printable = iswprint(tmpchar); } #else tmpchar = getch(); if (tmpchar == ERR) { return -1; } is_printable = isprint(tmpchar); #endif /* HAVE_WIDECHAR */ *ch = tmpchar; return (int) is_printable; } static struct key_sequence_codes { wchar_t *code; wint_t key; } Keys[] = { { L"[1;5A", T_KEY_C_UP }, { L"[1;5B", T_KEY_C_DOWN }, { L"[1;5C", T_KEY_C_RIGHT }, { L"[1;5D", T_KEY_C_LEFT }, { NULL, 0 } }; /* * Return key code corresponding to character sequence queued in stdscr. * Return -1 if sequence is unknown. */ #define MAX_SEQUENCE_SIZE 5 static wint_t get_input_sequence_code(void) { wchar_t code[MAX_SEQUENCE_SIZE + 1]; size_t length = 0; wint_t ch = 0; for (size_t i = 0; i < MAX_SEQUENCE_SIZE; ++i) { int res = get_current_char(&ch); if (res < 0) { break; } ++length; code[i] = (wchar_t) ch; } if (length == 0) { return -1; } code[length] = L'\0'; for (size_t i = 0; Keys[i].key != 0; ++i) { if (wcscmp(code, Keys[i].code) == 0) { return Keys[i].key; } } return -1; } void draw_active_window(Tox *m) { ToxWindow *a = windows[active_window_index]; if (a == NULL) { return; } pthread_mutex_lock(&Winthread.lock); a->alert = WINDOW_ALERT_NONE; a->pending_messages = 0; bool flag_refresh = Winthread.flag_refresh; pthread_mutex_unlock(&Winthread.lock); if (flag_refresh) { touchwin(a->window); a->onDraw(a, m); wrefresh(a->window); } #ifdef AUDIO else if (a->is_call && timed_out(a->chatwin->infobox.lastupdate, 1)) { touchwin(a->window); a->onDraw(a, m); wrefresh(a->window); } #endif // AUDIO #ifdef GAMES if (a->type == WINDOW_TYPE_GAME) { if (!flag_refresh) { // we always want to be continously refreshing game windows touchwin(a->window); a->onDraw(a, m); wrefresh(a->window); } int ch = getch(); if (ch == ERR) { return; } pthread_mutex_lock(&Winthread.lock); flag_interface_refresh(); pthread_mutex_unlock(&Winthread.lock); if (ch == user_settings->key_next_tab || ch == user_settings->key_prev_tab) { set_next_window(ch); } a->onKey(a, m, ch, false); return; } #endif // GAMES wint_t ch = 0; int printable = get_current_char(&ch); if (printable < 0) { return; } pthread_mutex_lock(&Winthread.lock); flag_interface_refresh(); pthread_mutex_unlock(&Winthread.lock); if (printable == 0 && (ch == user_settings->key_next_tab || ch == user_settings->key_prev_tab)) { set_next_window((int) ch); return; } else if ((printable == 0) && (a->type != WINDOW_TYPE_FRIEND_LIST)) { pthread_mutex_lock(&Winthread.lock); bool input_ret = a->onKey(a, m, ch, (bool) printable); pthread_mutex_unlock(&Winthread.lock); if (input_ret) { return; } // if an unprintable key code is unrecognized by input handler we attempt to manually decode char sequence wint_t tmp = get_input_sequence_code(); if (tmp != (wint_t) -1) { ch = tmp; } } pthread_mutex_lock(&Winthread.lock); a->onKey(a, m, ch, (bool) printable); pthread_mutex_unlock(&Winthread.lock); } /* Refresh inactive windows to prevent scrolling bugs. * Call at least once per second. */ void refresh_inactive_windows(void) { for (uint8_t i = 0; i < MAX_WINDOWS_NUM; ++i) { ToxWindow *toxwin = windows[i]; if (toxwin == NULL) { continue; } if ((i != active_window_index) && (toxwin->type != WINDOW_TYPE_FRIEND_LIST)) { pthread_mutex_lock(&Winthread.lock); line_info_print(toxwin); pthread_mutex_unlock(&Winthread.lock); } } } /* Returns a pointer to the ToxWindow in the ith index. * Returns NULL if no ToxWindow exists. */ ToxWindow *get_window_ptr(size_t index) { if (index >= MAX_WINDOWS_NUM) { return NULL; } return windows[index]; } /* Returns a pointer to the currently active ToxWindow. */ ToxWindow *get_active_window(void) { return windows[active_window_index]; } void force_refresh(WINDOW *w) { wclear(w); endwin(); refresh(); } int get_num_active_windows(void) { return num_active_windows; } /* Returns the number of active windows of given type. */ size_t get_num_active_windows_type(WINDOW_TYPE type) { size_t count = 0; for (size_t i = 0; i < MAX_WINDOWS_NUM; ++i) { ToxWindow *w = windows[i]; if (w == NULL) { continue; } if (w->type == type) { ++count; } } return count; } /* destroys all chat and conference windows (should only be called on shutdown) */ void kill_all_windows(Tox *m) { for (size_t i = 2; i < MAX_WINDOWS_NUM; ++i) { ToxWindow *w = windows[i]; if (w == NULL) { continue; } switch (w->type) { case WINDOW_TYPE_CHAT: { kill_chat_window(w, m); break; } case WINDOW_TYPE_CONFERENCE: { free_conference(w, w->num); break; } #ifdef GAMES case WINDOW_TYPE_GAME: { game_kill(w); break; } #endif // GAMES default: { break; } } } /* TODO: use enum instead of magic indices */ kill_friendlist(windows[1]); kill_prompt_window(windows[0]); } toxic-0.11.3/src/windows.h000066400000000000000000000216131416141666600153720ustar00rootroot00000000000000/* windows.h * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef WINDOWS_H #define WINDOWS_H #include #include #include #include #include #ifdef AUDIO #include #endif /* AUDIO */ #include "toxic.h" #define MAX_WINDOWS_NUM 20 #define MAX_WINDOW_NAME_LENGTH 22 #define CURS_Y_OFFSET 1 /* y-axis cursor offset for chat contexts */ #define CHATBOX_HEIGHT 1 #define TOP_BAR_HEIGHT 1 #define WINDOW_BAR_HEIGHT 1 typedef enum CustomPacket { CUSTOM_PACKET_GAME_INVITE = 160, CUSTOM_PACKET_GAME_DATA = 161, } CustomPacket; /* ncurses colour pairs as FOREGROUND_BACKGROUND. No background defaults to black. */ typedef enum { WHITE, GREEN, CYAN, RED, BLUE, YELLOW, MAGENTA, BLACK, BLACK_WHITE, WHITE_BLACK, WHITE_BLUE, WHITE_GREEN, BAR_TEXT, STATUS_ONLINE, BAR_ACCENT, PURPLE_BG, BLACK_BG, STATUS_BUSY, STATUS_AWAY, BAR_NOTIFY, BAR_SOLID, } C_COLOURS; /* tab alert types: lower types take priority (this relies on the order of C_COLOURS) */ typedef enum { WINDOW_ALERT_NONE = 0, WINDOW_ALERT_0 = STATUS_ONLINE, WINDOW_ALERT_1 = BAR_ACCENT, WINDOW_ALERT_2 = PURPLE_BG, } WINDOW_ALERTS; typedef enum { WINDOW_TYPE_PROMPT, WINDOW_TYPE_CHAT, WINDOW_TYPE_CONFERENCE, WINDOW_TYPE_FRIEND_LIST, #ifdef GAMES WINDOW_TYPE_GAME, #endif } WINDOW_TYPE; /* Fixes text color problem on some terminals. Uncomment if necessary */ /* #define URXVT_FIX */ /* * Used to control access to global variables via a mutex, as well as to handle signals. * Any file, variable or data structure that is used by the UI/Window thread and any other thread * must be guarded by `lock`. * * There should only ever be one instance of this struct. */ struct Winthread { pthread_t tid; pthread_mutex_t lock; volatile sig_atomic_t sig_exit_toxic; volatile sig_atomic_t flag_resize; volatile sig_atomic_t flag_refresh; volatile sig_atomic_t last_refresh_flag; }; extern struct Winthread Winthread; struct cqueue_thread { pthread_t tid; }; struct av_thread { pthread_t tid; }; struct arg_opts { bool use_ipv4; bool force_tcp; bool disable_local_discovery; bool debug; bool default_locale; bool use_custom_data; bool no_connect; bool encrypt_data; bool unencrypt_data; char nameserver_path[MAX_STR_SIZE]; char config_path[MAX_STR_SIZE]; char nodes_path[MAX_STR_SIZE]; bool logging; FILE *log_fp; char proxy_address[256]; uint8_t proxy_type; uint16_t proxy_port; uint16_t tcp_port; }; extern struct arg_opts arg_opts; typedef struct ToxWindow ToxWindow; typedef struct StatusBar StatusBar; typedef struct PromptBuf PromptBuf; typedef struct ChatContext ChatContext; typedef struct Help Help; #ifdef GAMES typedef struct GameData GameData; #endif struct ToxWindow { /* ncurses */ bool(*onKey)(ToxWindow *, Tox *, wint_t, bool); void(*onDraw)(ToxWindow *, Tox *); void(*onInit)(ToxWindow *, Tox *); /* toxcore */ void(*onFriendRequest)(ToxWindow *, Tox *, const char *, const char *, size_t); void(*onFriendAdded)(ToxWindow *, Tox *, uint32_t, bool); void(*onConnectionChange)(ToxWindow *, Tox *, uint32_t, Tox_Connection); void(*onMessage)(ToxWindow *, Tox *, uint32_t, Tox_Message_Type, const char *, size_t); void(*onNickChange)(ToxWindow *, Tox *, uint32_t, const char *, size_t); void(*onStatusChange)(ToxWindow *, Tox *, uint32_t, Tox_User_Status); void(*onStatusMessageChange)(ToxWindow *, uint32_t, const char *, size_t); void(*onConferenceMessage)(ToxWindow *, Tox *, uint32_t, uint32_t, Tox_Message_Type, const char *, size_t); void(*onConferenceInvite)(ToxWindow *, Tox *, int32_t, uint8_t, const char *, uint16_t); void(*onConferenceNameListChange)(ToxWindow *, Tox *, uint32_t); void(*onConferencePeerNameChange)(ToxWindow *, Tox *, uint32_t, uint32_t, const char *, size_t); void(*onConferenceTitleChange)(ToxWindow *, Tox *, uint32_t, uint32_t, const char *, size_t); void(*onFileChunkRequest)(ToxWindow *, Tox *, uint32_t, uint32_t, uint64_t, size_t); void(*onFileRecvChunk)(ToxWindow *, Tox *, uint32_t, uint32_t, uint64_t, const char *, size_t); void(*onFileControl)(ToxWindow *, Tox *, uint32_t, uint32_t, Tox_File_Control); void(*onFileRecv)(ToxWindow *, Tox *, uint32_t, uint32_t, uint64_t, const char *, size_t); void(*onTypingChange)(ToxWindow *, Tox *, uint32_t, bool); void(*onReadReceipt)(ToxWindow *, Tox *, uint32_t, uint32_t); #ifdef GAMES void(*onGameInvite)(ToxWindow *, Tox *, uint32_t, const uint8_t *, size_t); void(*onGameData)(ToxWindow *, Tox *, uint32_t, const uint8_t *, size_t); #endif // GAMES #ifdef AUDIO void(*onInvite)(ToxWindow *, ToxAV *, uint32_t, int); void(*onRinging)(ToxWindow *, ToxAV *, uint32_t, int); void(*onStarting)(ToxWindow *, ToxAV *, uint32_t, int); void(*onEnding)(ToxWindow *, ToxAV *, uint32_t, int); void(*onError)(ToxWindow *, ToxAV *, uint32_t, int); void(*onStart)(ToxWindow *, ToxAV *, uint32_t, int); void(*onCancel)(ToxWindow *, ToxAV *, uint32_t, int); void(*onReject)(ToxWindow *, ToxAV *, uint32_t, int); void(*onEnd)(ToxWindow *, ToxAV *, uint32_t, int); void(*onWriteDevice)(ToxWindow *, Tox *, uint32_t, int, const int16_t *, unsigned int, uint8_t, unsigned int); bool is_call; int ringing_sound; #endif /* AUDIO */ int active_box; /* For box notify */ char name[TOXIC_MAX_NAME_LENGTH + 1]; uint32_t num; /* corresponds to friendnumber in chat windows */ uint8_t index; /* This window's index in the windows array */ bool scroll_pause; /* true if this window is not scrolled to the bottom */ unsigned int pending_messages; /* # of new messages in this window since the last time it was focused */ int x; WINDOW_TYPE type; int show_peerlist; /* used to toggle conference peerlist */ WINDOW_ALERTS alert; ChatContext *chatwin; StatusBar *stb; Help *help; #ifdef GAMES GameData *game; #endif WINDOW *window; WINDOW *window_bar; }; extern ToxWindow *windows[MAX_WINDOWS_NUM]; /* statusbar info holder */ struct StatusBar { WINDOW *topline; char statusmsg[TOX_MAX_STATUS_MESSAGE_LENGTH + 1]; size_t statusmsg_len; char nick[TOXIC_MAX_NAME_LENGTH + 1]; size_t nick_len; Tox_User_Status status; Tox_Connection connection; }; #ifdef AUDIO #define INFOBOX_HEIGHT 7 #define INFOBOX_WIDTH 21 /* holds display info for audio calls */ struct infobox { float vad_lvl; bool in_is_muted; bool out_is_muted; bool hide; bool active; time_t lastupdate; time_t starttime; char timestr[TIME_STR_SIZE]; WINDOW *win; }; #endif /* AUDIO */ #define MAX_LINE_HIST 128 /* chat and conference window/buffer holder */ struct ChatContext { wchar_t line[MAX_STR_SIZE]; int pos; int len; int start; /* the position to start printing line at */ wchar_t ln_history[MAX_LINE_HIST][MAX_STR_SIZE]; /* history for input lines/commands */ int hst_pos; int hst_tot; wchar_t yank[MAX_STR_SIZE]; /* contains last killed/discarded line */ int yank_len; struct history *hst; struct chatlog *log; struct chat_queue *cqueue; #ifdef AUDIO struct infobox infobox; #endif uint8_t self_is_typing; uint8_t pastemode; /* whether to translate \r to \n */ WINDOW *history; WINDOW *linewin; WINDOW *sidebar; }; struct Help { WINDOW *win; int type; bool active; }; ToxWindow *init_windows(Tox *m); void draw_active_window(Tox *m); int add_window(Tox *m, ToxWindow *w); void del_window(ToxWindow *w); void set_active_window_index(uint8_t index); int get_num_active_windows(void); void kill_all_windows(Tox *m); /* should only be called on shutdown */ void on_window_resize(void); void force_refresh(WINDOW *w); ToxWindow *get_window_ptr(size_t i); ToxWindow *get_active_window(void); void draw_window_bar(ToxWindow *self); /* Returns the number of active windows of given type. */ size_t get_num_active_windows_type(WINDOW_TYPE type); /* refresh inactive windows to prevent scrolling bugs. call at least once per second */ void refresh_inactive_windows(void); #endif // WINDOWS_H toxic-0.11.3/src/x11focus.c000066400000000000000000000034041416141666600153420ustar00rootroot00000000000000/* x11focus.c * * * Copyright (C) 2020 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #include "x11focus.h" #ifndef __APPLE__ #include static struct Focus { Display *display; Window terminal_window; } Focus; static long unsigned int focused_window_id(void) { if (!Focus.display) { return 0; } Window focus; int revert; XLockDisplay(Focus.display); XGetInputFocus(Focus.display, &focus, &revert); XUnlockDisplay(Focus.display); return focus; } bool is_focused(void) { if (!Focus.display) { return false; } return Focus.terminal_window == focused_window_id(); } int init_x11focus(void) { if (XInitThreads() == 0) { return -1; } Focus.display = XOpenDisplay(NULL); if (!Focus.display) { return -1; } Focus.terminal_window = focused_window_id(); return 0; } void terminate_x11focus(void) { if (!Focus.display || !Focus.terminal_window) { return; } XLockDisplay(Focus.display); XCloseDisplay(Focus.display); XUnlockDisplay(Focus.display); } #endif /* !__APPLE__ */ toxic-0.11.3/src/x11focus.h000066400000000000000000000017451416141666600153550ustar00rootroot00000000000000/* xtra.h * * * Copyright (C) 2020 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #ifndef X11FOCUS_H #define X11FOCUS_H #include /* NOTE: If no xlib present don't compile */ int init_x11focus(void); void terminate_x11focus(void); bool is_focused(void); #endif /* X11FOCUS */