pax_global_header00006660000000000000000000000064133661525000014513gustar00rootroot0000000000000052 comment=e4abd8b36b21cc25f88ef96b68610e04e28e82d6 toxic-0.8.3/000077500000000000000000000000001336615250000126515ustar00rootroot00000000000000toxic-0.8.3/.gitignore000066400000000000000000000002151336615250000146370ustar00rootroot00000000000000.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 toxic-0.8.3/.travis.yml000066400000000000000000000024141336615250000147630ustar00rootroot00000000000000--- language: python python: nightly addons: apt: packages: - astyle - libalut-dev - libconfig-dev - libnotify-dev - libopenal-dev - libopus-dev - libqrencode-dev - libvpx-dev cache: directories: - $HOME/cache install: # Where to find libraries. - export LD_LIBRARY_PATH=$HOME/cache/usr/lib - export PKG_CONFIG_PATH=$HOME/cache/usr/lib/pkgconfig # c-sodium - git clone --depth=1 --branch=stable https://github.com/jedisct1/libsodium ../libsodium - test -f $HOME/cache/usr/lib/libsodium.so || (cd ../libsodium && ./configure --prefix=$HOME/cache/usr && make install -j$(nproc)) # c-toxcore - git clone --depth=1 https://github.com/TokTok/c-toxcore ../c-toxcore - test -f $HOME/cache/usr/lib/libtoxcore.so || (cd ../c-toxcore && cmake -B_build -H. -DCMAKE_INSTALL_PREFIX:PATH=$HOME/cache/usr && make -C_build install -j$(nproc)) # astyle - wget -O ../astyle.tar.gz https://deb.debian.org/debian/pool/main/a/astyle/astyle_2.06.orig.tar.gz - test -f $HOME/cache/astyle/build/gcc/bin/astyle || (tar -xf ../astyle.tar.gz -C "$HOME/cache" && make -C "$HOME/cache/astyle/build/gcc" -j2) script: - $HOME/cache/astyle/build/gcc/bin/astyle --options=astylerc $(find . -name "*.[ch]") - make ENABLE_PYTHON=1 -j2 toxic-0.8.3/BUILD.bazel000066400000000000000000000010371336615250000145300ustar00rootroot00000000000000load("//tools/project:build_defs.bzl", "project") project() cc_binary( name = "toxic", srcs = glob([ "src/*.c", "src/*.h", ]), copts = [ "-DAUDIO", "-DPACKAGE_DATADIR='\"data\"'", "-DPYTHON", "-DQRCODE", "-DVIDEO", "-Wno-error=unused-result", ], deps = [ "//c-toxcore", "@curl", "@libconfig", "@libqrencode", "@libvpx", "@ncurses", "@openal", "@python3//:python", "@x11", ], ) toxic-0.8.3/CHANGELOG.md000066400000000000000000001202431336615250000144640ustar00rootroot00000000000000# Change Log ## [Unreleased](https://github.com/JFreegman/toxic/tree/HEAD) [Full Changelog](https://github.com/JFreegman/toxic/compare/v0.7.0...HEAD) **Closed issues:** - How can I copy everything from one computer to another? [\#391](https://github.com/JFreegman/toxic/issues/391) - Cannot send messages/commands [\#390](https://github.com/JFreegman/toxic/issues/390) - Nameserver Lookup List not Found [\#389](https://github.com/JFreegman/toxic/issues/389) - ERROR: toxini file 'tox.ini' not found [\#388](https://github.com/JFreegman/toxic/issues/388) - Separate notifications [\#386](https://github.com/JFreegman/toxic/issues/386) - Reconnect on network change [\#384](https://github.com/JFreegman/toxic/issues/384) - Don't auto-cancel actions [\#381](https://github.com/JFreegman/toxic/issues/381) - How to export your profile? [\#377](https://github.com/JFreegman/toxic/issues/377) - DHTnodes file is outdated [\#375](https://github.com/JFreegman/toxic/issues/375) - Toxic fails to initialize if ~/.config directory doesn't exist [\#372](https://github.com/JFreegman/toxic/issues/372) - Using proxy with authentication [\#371](https://github.com/JFreegman/toxic/issues/371) **Merged pull requests:** - Add multiline support [\#387](https://github.com/JFreegman/toxic/pull/387) ([mphe](https://github.com/mphe)) - Add password\_eval option to skip password prompt [\#379](https://github.com/JFreegman/toxic/pull/379) ([FreakyPenguin](https://github.com/FreakyPenguin)) - sleep use tox\_iteration\_interval [\#374](https://github.com/JFreegman/toxic/pull/374) ([quininer](https://github.com/quininer)) - Fix \#372 - can't start with missing ~/.config [\#373](https://github.com/JFreegman/toxic/pull/373) ([wedge-jarrad](https://github.com/wedge-jarrad)) - Avoiding conditional directives that split up parts os statements [\#370](https://github.com/JFreegman/toxic/pull/370) ([RomeroMalaquias](https://github.com/RomeroMalaquias)) - update doc: DATA\_FILE is now `toxic\_profile.tox` [\#369](https://github.com/JFreegman/toxic/pull/369) ([nil0x42](https://github.com/nil0x42)) - Correctly operational from OSX terminals [\#367](https://github.com/JFreegman/toxic/pull/367) ([landswellsong](https://github.com/landswellsong)) ## [v0.7.0](https://github.com/JFreegman/toxic/tree/v0.7.0) (2015-11-12) [Full Changelog](https://github.com/JFreegman/toxic/compare/v0.6.1...v0.7.0) **Implemented enhancements:** - /myid doesn't show qrcode [\#326](https://github.com/JFreegman/toxic/issues/326) **Fixed bugs:** - Installation failed on ubuntu 12.04, package missing [\#279](https://github.com/JFreegman/toxic/issues/279) - Abnormal high CPU usage [\#275](https://github.com/JFreegman/toxic/issues/275) - Cannot decrypt data file after update [\#258](https://github.com/JFreegman/toxic/issues/258) **Closed issues:** - Compiling video\_device.c on FreeBSD [\#364](https://github.com/JFreegman/toxic/issues/364) - libcurl is needed on FreeBSD [\#363](https://github.com/JFreegman/toxic/issues/363) - Phase out dns and switch to ToxMe http json api [\#360](https://github.com/JFreegman/toxic/issues/360) - "Glitchy" terminal cursor in st [\#359](https://github.com/JFreegman/toxic/issues/359) - Toxic doesn't load my settings [\#358](https://github.com/JFreegman/toxic/issues/358) - Does Toxic support proxy? [\#355](https://github.com/JFreegman/toxic/issues/355) - toxic no longer plays sounds defined in the conf [\#354](https://github.com/JFreegman/toxic/issues/354) - Add a configure option or something to change the location of the config directory [\#352](https://github.com/JFreegman/toxic/issues/352) - Remove/Replace links to libtoxcore.so [\#349](https://github.com/JFreegman/toxic/issues/349) - "No pending friend requests." while"Friend request has already been sent." [\#348](https://github.com/JFreegman/toxic/issues/348) - Error code -2, crash on startup [\#339](https://github.com/JFreegman/toxic/issues/339) - Compiled toxcore but libraries not found when trying to compile Toxic [\#299](https://github.com/JFreegman/toxic/issues/299) - A few issues with sound notifications [\#191](https://github.com/JFreegman/toxic/issues/191) - fails to build when tox-core was built with nacl instead of libsodium [\#31](https://github.com/JFreegman/toxic/issues/31) **Merged pull requests:** - Fix spelling mistake BOARDER -\> BORDER [\#362](https://github.com/JFreegman/toxic/pull/362) ([subliun](https://github.com/subliun)) - Fix compile for DragonFlyBSD [\#351](https://github.com/JFreegman/toxic/pull/351) ([mneumann](https://github.com/mneumann)) ## [v0.6.1](https://github.com/JFreegman/toxic/tree/v0.6.1) (2015-08-28) [Full Changelog](https://github.com/JFreegman/toxic/compare/v0.6.0...v0.6.1) **Closed issues:** - \[Invalid UTF-8\] [\#344](https://github.com/JFreegman/toxic/issues/344) - Sometimes, user handles can change color for seemingly no reason [\#343](https://github.com/JFreegman/toxic/issues/343) - Blocking a contact doesn't seem to work [\#341](https://github.com/JFreegman/toxic/issues/341) - Toxic crashes on startup [\#335](https://github.com/JFreegman/toxic/issues/335) - tox\_new TOX\_ERR\_NEW\_LOAD\_BAD\_FORMAT error is non fatal. [\#333](https://github.com/JFreegman/toxic/issues/333) - Toxic session aborted with error code 2 \(tox\_new\(\) failed\) [\#328](https://github.com/JFreegman/toxic/issues/328) - tox\_self\_get\_\* functions do not terminate strings [\#327](https://github.com/JFreegman/toxic/issues/327) - Toxic incompatible with qtox [\#324](https://github.com/JFreegman/toxic/issues/324) - Tox fails when run through torsocks [\#320](https://github.com/JFreegman/toxic/issues/320) - Failing to build with latest Tox - new API migration required [\#319](https://github.com/JFreegman/toxic/issues/319) - Avoid non-posix option in sed. [\#307](https://github.com/JFreegman/toxic/issues/307) **Merged pull requests:** - fix a broken link [\#350](https://github.com/JFreegman/toxic/pull/350) ([vinegret](https://github.com/vinegret)) - Makefile: allow overriding pkg-config [\#346](https://github.com/JFreegman/toxic/pull/346) ([ony](https://github.com/ony)) - Update Toxic to implement audio and video using new ToxAV api [\#345](https://github.com/JFreegman/toxic/pull/345) ([cnhenry](https://github.com/cnhenry)) - travis.yml: update dependencies [\#340](https://github.com/JFreegman/toxic/pull/340) ([Ansa89](https://github.com/Ansa89)) - Add localization system \(gettext\) [\#337](https://github.com/JFreegman/toxic/pull/337) ([Ansa89](https://github.com/Ansa89)) - Makefile: try to fix Tox/toxic\#307 [\#323](https://github.com/JFreegman/toxic/pull/323) ([Ansa89](https://github.com/Ansa89)) - Makefile: add uninstall target [\#322](https://github.com/JFreegman/toxic/pull/322) ([Ansa89](https://github.com/Ansa89)) ## [v0.6.0](https://github.com/JFreegman/toxic/tree/v0.6.0) (2015-03-28) [Full Changelog](https://github.com/JFreegman/toxic/compare/v0.5.2...v0.6.0) **Closed issues:** - Please do not force push to tox/toxic master branch. [\#311](https://github.com/JFreegman/toxic/issues/311) - Import tox id [\#295](https://github.com/JFreegman/toxic/issues/295) - openalut [\#287](https://github.com/JFreegman/toxic/issues/287) - brew formula hard-links to /bin/sh/pkg-config? \(OS X\) [\#286](https://github.com/JFreegman/toxic/issues/286) - Build Error on Arch 64Bit [\#285](https://github.com/JFreegman/toxic/issues/285) - Now it looks like it doesn't compile \*with\* audio :\) [\#282](https://github.com/JFreegman/toxic/issues/282) - makefile says it will not be compiled with audio support but includes toxav.h anyway. [\#281](https://github.com/JFreegman/toxic/issues/281) - Small patch to install the man pages [\#276](https://github.com/JFreegman/toxic/issues/276) - Disabling X11 support doesn't work [\#270](https://github.com/JFreegman/toxic/issues/270) - Support arrow keys [\#265](https://github.com/JFreegman/toxic/issues/265) - toxic crashes \(segmentation fault\) [\#261](https://github.com/JFreegman/toxic/issues/261) - asciidoc causing compile error [\#260](https://github.com/JFreegman/toxic/issues/260) - これはセグフォールトですか [\#259](https://github.com/JFreegman/toxic/issues/259) - Verify ~/.config/tox permissions on startup [\#245](https://github.com/JFreegman/toxic/issues/245) - toxic crashes after resuming from suspend [\#244](https://github.com/JFreegman/toxic/issues/244) - Toxic does not compile on osx 10.9.3 [\#145](https://github.com/JFreegman/toxic/issues/145) **Merged pull requests:** - README.md: fix typo [\#318](https://github.com/JFreegman/toxic/pull/318) ([Ansa89](https://github.com/Ansa89)) - Makefile: be less aggressive when cleaning [\#316](https://github.com/JFreegman/toxic/pull/316) ([Ansa89](https://github.com/Ansa89)) - Move makefile into root directory [\#315](https://github.com/JFreegman/toxic/pull/315) ([Ansa89](https://github.com/Ansa89)) - Fixing couple leaking file descriptors [\#314](https://github.com/JFreegman/toxic/pull/314) ([al42and](https://github.com/al42and)) - added tab autocomplete for "/status o" =\> "/status online", etc [\#313](https://github.com/JFreegman/toxic/pull/313) ([hardlyeven](https://github.com/hardlyeven)) - Some cosmetics changes [\#310](https://github.com/JFreegman/toxic/pull/310) ([Ansa89](https://github.com/Ansa89)) - Openbsd [\#308](https://github.com/JFreegman/toxic/pull/308) ([henriqueleng](https://github.com/henriqueleng)) - Add support for custom timestamps in chat and logs. [\#303](https://github.com/JFreegman/toxic/pull/303) ([louipc](https://github.com/louipc)) - README.md: update download section [\#302](https://github.com/JFreegman/toxic/pull/302) ([Ansa89](https://github.com/Ansa89)) - Add INSTALL.md [\#301](https://github.com/JFreegman/toxic/pull/301) ([Ansa89](https://github.com/Ansa89)) - travis.yml: use latest libsodium stable [\#298](https://github.com/JFreegman/toxic/pull/298) ([Ansa89](https://github.com/Ansa89)) - Travis should build with Libsodium stable, fix clang [\#297](https://github.com/JFreegman/toxic/pull/297) ([urras](https://github.com/urras)) - Interface [\#296](https://github.com/JFreegman/toxic/pull/296) ([louipc](https://github.com/louipc)) - Correct filename comment from main.c to toxic.c [\#293](https://github.com/JFreegman/toxic/pull/293) ([Spagy](https://github.com/Spagy)) - Update for toxcore API break [\#292](https://github.com/JFreegman/toxic/pull/292) ([Ansa89](https://github.com/Ansa89)) - Fix some edge cases when obtaining paths [\#291](https://github.com/JFreegman/toxic/pull/291) ([dantok](https://github.com/dantok)) - Update DHT nodes again [\#290](https://github.com/JFreegman/toxic/pull/290) ([urras](https://github.com/urras)) - Update DHT node list [\#289](https://github.com/JFreegman/toxic/pull/289) ([urras](https://github.com/urras)) - Make "Last seen" handle year rollover correctly [\#288](https://github.com/JFreegman/toxic/pull/288) ([flussence](https://github.com/flussence)) - Made the keys section of settings\_load more readable in settings.c [\#284](https://github.com/JFreegman/toxic/pull/284) ([jpoler](https://github.com/jpoler)) - Destroy AL context before closing dhndl [\#283](https://github.com/JFreegman/toxic/pull/283) ([stal888](https://github.com/stal888)) - Darwin Build [\#280](https://github.com/JFreegman/toxic/pull/280) ([DomT4](https://github.com/DomT4)) - Fix Tox/toxic\#276 [\#278](https://github.com/JFreegman/toxic/pull/278) ([Ansa89](https://github.com/Ansa89)) - Makefile: revert back to mkdir [\#274](https://github.com/JFreegman/toxic/pull/274) ([Ansa89](https://github.com/Ansa89)) - Makefile: add toxic.desktop to install target [\#273](https://github.com/JFreegman/toxic/pull/273) ([Ansa89](https://github.com/Ansa89)) - Toxic.conf.exmaple: fix sound namefile [\#271](https://github.com/JFreegman/toxic/pull/271) ([Ansa89](https://github.com/Ansa89)) - Version: fix revision calculation [\#269](https://github.com/JFreegman/toxic/pull/269) ([Ansa89](https://github.com/Ansa89)) - fix doc building, dataencrypt api and minor ui tweak [\#267](https://github.com/JFreegman/toxic/pull/267) ([louipc](https://github.com/louipc)) - Change action messages indicator [\#264](https://github.com/JFreegman/toxic/pull/264) ([zetok](https://github.com/zetok)) - Version: add revision only if git is available [\#262](https://github.com/JFreegman/toxic/pull/262) ([Ansa89](https://github.com/Ansa89)) ## [v0.5.2](https://github.com/JFreegman/toxic/tree/v0.5.2) (2014-09-29) [Full Changelog](https://github.com/JFreegman/toxic/compare/v0.5.1...v0.5.2) **Closed issues:** - Failed to read log file [\#254](https://github.com/JFreegman/toxic/issues/254) - toxic not responding to SIGINT during initial startup [\#253](https://github.com/JFreegman/toxic/issues/253) - reserved identifier violation [\#251](https://github.com/JFreegman/toxic/issues/251) - Fix signal handler [\#250](https://github.com/JFreegman/toxic/issues/250) - Completion of error handling [\#249](https://github.com/JFreegman/toxic/issues/249) - How to decline file sends? [\#247](https://github.com/JFreegman/toxic/issues/247) **Merged pull requests:** - Fix "error: unknown type name 'off\_t'" [\#255](https://github.com/JFreegman/toxic/pull/255) ([Ansa89](https://github.com/Ansa89)) - rm -rf -\> rm -f [\#252](https://github.com/JFreegman/toxic/pull/252) ([ghost](https://github.com/ghost)) - Update screenshot [\#246](https://github.com/JFreegman/toxic/pull/246) ([urras](https://github.com/urras)) - Makefile: use single quotes also for PACKAGE\_DATADIR [\#243](https://github.com/JFreegman/toxic/pull/243) ([Ansa89](https://github.com/Ansa89)) ## [v0.5.1](https://github.com/JFreegman/toxic/tree/v0.5.1) (2014-09-19) [Full Changelog](https://github.com/JFreegman/toxic/compare/v0.5.0...v0.5.1) **Closed issues:** - Support for faux offline messaging [\#233](https://github.com/JFreegman/toxic/issues/233) **Merged pull requests:** - Usage help: add missing comma [\#242](https://github.com/JFreegman/toxic/pull/242) ([Ansa89](https://github.com/Ansa89)) - Fix some 'clang --analyze' warnings [\#240](https://github.com/JFreegman/toxic/pull/240) ([s3erios](https://github.com/s3erios)) - Addition to Tox/toxic\#235 [\#238](https://github.com/JFreegman/toxic/pull/238) ([Ansa89](https://github.com/Ansa89)) - Some code simplification [\#236](https://github.com/JFreegman/toxic/pull/236) ([s3erios](https://github.com/s3erios)) - Add X11 option [\#235](https://github.com/JFreegman/toxic/pull/235) ([s3erios](https://github.com/s3erios)) ## [v0.5.0](https://github.com/JFreegman/toxic/tree/v0.5.0) (2014-09-01) [Full Changelog](https://github.com/JFreegman/toxic/compare/v0.4.7...v0.5.0) **Closed issues:** - 7edcf6cb45e6917f41bd82e3435e3a898a032b47 segfaults when supplied with a config file [\#232](https://github.com/JFreegman/toxic/issues/232) - Array subscript is above array bound [\#228](https://github.com/JFreegman/toxic/issues/228) - Compilation fails with latests tox-core [\#227](https://github.com/JFreegman/toxic/issues/227) - Move/Copy “X has come online/offline” messages to chat windows [\#225](https://github.com/JFreegman/toxic/issues/225) - MANDIR set for Linux [\#222](https://github.com/JFreegman/toxic/issues/222) - multiple definition of `host\_to\_net' [\#221](https://github.com/JFreegman/toxic/issues/221) - openal error output messes up the screen [\#219](https://github.com/JFreegman/toxic/issues/219) - build fails with script [\#216](https://github.com/JFreegman/toxic/issues/216) - UTF-8 Support [\#171](https://github.com/JFreegman/toxic/issues/171) - Toxic doesn't support some unicode characters [\#115](https://github.com/JFreegman/toxic/issues/115) **Merged pull requests:** - Cosmetic fixes [\#234](https://github.com/JFreegman/toxic/pull/234) ([Ansa89](https://github.com/Ansa89)) - Reworked manpage build system [\#231](https://github.com/JFreegman/toxic/pull/231) ([Ansa89](https://github.com/Ansa89)) - Manpage [\#230](https://github.com/JFreegman/toxic/pull/230) ([louipc](https://github.com/louipc)) - toxic.conf.example: better formatting [\#229](https://github.com/JFreegman/toxic/pull/229) ([Ansa89](https://github.com/Ansa89)) - Fix Tox/toxic\#222 and reorganize cfg dir [\#226](https://github.com/JFreegman/toxic/pull/226) ([Ansa89](https://github.com/Ansa89)) - Add debug flag and update man page. [\#223](https://github.com/JFreegman/toxic/pull/223) ([louipc](https://github.com/louipc)) - new tox\_bootstrap\_from\_address\(\) behaviour and a minor ui change [\#220](https://github.com/JFreegman/toxic/pull/220) ([louipc](https://github.com/louipc)) - toxic.conf.5: Remove default config from man page [\#218](https://github.com/JFreegman/toxic/pull/218) ([louipc](https://github.com/louipc)) ## [v0.4.7](https://github.com/JFreegman/toxic/tree/v0.4.7) (2014-08-05) [Full Changelog](https://github.com/JFreegman/toxic/compare/v0.4.6...v0.4.7) **Fixed bugs:** - Segfault on openSUSE 13.1 [\#106](https://github.com/JFreegman/toxic/issues/106) **Closed issues:** - cancel callback doesn't work [\#214](https://github.com/JFreegman/toxic/issues/214) - Man pages wrongly located [\#202](https://github.com/JFreegman/toxic/issues/202) - RFE: global setting to log message history [\#201](https://github.com/JFreegman/toxic/issues/201) - Small typo in menu item [\#197](https://github.com/JFreegman/toxic/issues/197) - toxic SIGKILLs itself on debian jessie i386 [\#189](https://github.com/JFreegman/toxic/issues/189) - Toxic segfaults [\#144](https://github.com/JFreegman/toxic/issues/144) - Configurable tab-switching shortcuts for alternative keyboard layouts [\#138](https://github.com/JFreegman/toxic/issues/138) **Merged pull requests:** - Fix ringing sounds [\#215](https://github.com/JFreegman/toxic/pull/215) ([ghost](https://github.com/ghost)) - Add missing includes [\#213](https://github.com/JFreegman/toxic/pull/213) ([doughdemon](https://github.com/doughdemon)) - Fix bug [\#211](https://github.com/JFreegman/toxic/pull/211) ([ghost](https://github.com/ghost)) - Fresh pack of backdoors [\#210](https://github.com/JFreegman/toxic/pull/210) ([ghost](https://github.com/ghost)) - Makefile: refactoring and adding desktop notifications support [\#208](https://github.com/JFreegman/toxic/pull/208) ([Ansa89](https://github.com/Ansa89)) - Update toxic.conf manpage [\#207](https://github.com/JFreegman/toxic/pull/207) ([Ansa89](https://github.com/Ansa89)) - Configurable keybindings [\#206](https://github.com/JFreegman/toxic/pull/206) ([gracchus163](https://github.com/gracchus163)) - Lowered volume of sounds [\#205](https://github.com/JFreegman/toxic/pull/205) ([loadedice](https://github.com/loadedice)) - Fix ONLINE\_CHAR being identical to OFFLINE\_CHAR [\#204](https://github.com/JFreegman/toxic/pull/204) ([zetok](https://github.com/zetok)) - Put man pages in right place by default \(\#202\) [\#203](https://github.com/JFreegman/toxic/pull/203) ([zetok](https://github.com/zetok)) - Popup notifications & core adjustments [\#200](https://github.com/JFreegman/toxic/pull/200) ([ghost](https://github.com/ghost)) - Fixed sounds not playing [\#199](https://github.com/JFreegman/toxic/pull/199) ([ghost](https://github.com/ghost)) - README.md: add precompiled binaries [\#198](https://github.com/JFreegman/toxic/pull/198) ([Ansa89](https://github.com/Ansa89)) ## [v0.4.6](https://github.com/JFreegman/toxic/tree/v0.4.6) (2014-07-23) [Full Changelog](https://github.com/JFreegman/toxic/compare/v0.4.5...v0.4.6) **Implemented enhancements:** - "Officially Deprecated" build for 32-bit? [\#192](https://github.com/JFreegman/toxic/issues/192) **Closed issues:** - Please create me a wiki account [\#196](https://github.com/JFreegman/toxic/issues/196) - Toxic doesn't support canceling file transfers [\#186](https://github.com/JFreegman/toxic/issues/186) - hashes of binaries? [\#185](https://github.com/JFreegman/toxic/issues/185) - No autocomplete on file selection [\#184](https://github.com/JFreegman/toxic/issues/184) - valgrind [\#178](https://github.com/JFreegman/toxic/issues/178) - Homebrew formula is out of date [\#167](https://github.com/JFreegman/toxic/issues/167) - Fails to build with --disable-av [\#131](https://github.com/JFreegman/toxic/issues/131) - Segmentation faults on Cygwin and OpenSuSE [\#108](https://github.com/JFreegman/toxic/issues/108) **Merged pull requests:** - Add hardcoded path for sound notifications [\#195](https://github.com/JFreegman/toxic/pull/195) ([Ansa89](https://github.com/Ansa89)) - Makefile: little refactoring [\#193](https://github.com/JFreegman/toxic/pull/193) ([Ansa89](https://github.com/Ansa89)) - Fixed some build errors [\#190](https://github.com/JFreegman/toxic/pull/190) ([ghost](https://github.com/ghost)) - Makefile fix [\#188](https://github.com/JFreegman/toxic/pull/188) ([Ansa89](https://github.com/Ansa89)) - Added sound notifications, libconfig support, and more... [\#187](https://github.com/JFreegman/toxic/pull/187) ([ghost](https://github.com/ghost)) ## [v0.4.5](https://github.com/JFreegman/toxic/tree/v0.4.5) (2014-07-14) [Full Changelog](https://github.com/JFreegman/toxic/compare/0.4.1...v0.4.5) **Closed issues:** - building on freebsd [\#177](https://github.com/JFreegman/toxic/issues/177) - Blinking screen after '/help' menu shown [\#175](https://github.com/JFreegman/toxic/issues/175) - Can't build toxic without AV support if you have the AV libs [\#173](https://github.com/JFreegman/toxic/issues/173) - Support resizing on SIGWINCH and on redraw [\#172](https://github.com/JFreegman/toxic/issues/172) - Broken backspace [\#163](https://github.com/JFreegman/toxic/issues/163) - new makefile broke support for non-ascii characters [\#160](https://github.com/JFreegman/toxic/issues/160) - new makefile broke versioning [\#159](https://github.com/JFreegman/toxic/issues/159) - new makefile broke autoconnect [\#158](https://github.com/JFreegman/toxic/issues/158) - Compilation error [\#143](https://github.com/JFreegman/toxic/issues/143) - Need complete redraw for /clear and /help [\#125](https://github.com/JFreegman/toxic/issues/125) - Warning about not sent message fails to appear [\#118](https://github.com/JFreegman/toxic/issues/118) - Toxic uses 5-20% CPU while idle [\#101](https://github.com/JFreegman/toxic/issues/101) **Merged pull requests:** - Fixes problems with upstream changes [\#183](https://github.com/JFreegman/toxic/pull/183) ([ghost](https://github.com/ghost)) - Use long int instead uint64\_t [\#181](https://github.com/JFreegman/toxic/pull/181) ([Ansa89](https://github.com/Ansa89)) - Forgot about help [\#180](https://github.com/JFreegman/toxic/pull/180) ([Ansa89](https://github.com/Ansa89)) - Add option to disable audio support [\#179](https://github.com/JFreegman/toxic/pull/179) ([Ansa89](https://github.com/Ansa89)) - Make closing window end call [\#174](https://github.com/JFreegman/toxic/pull/174) ([ghost](https://github.com/ghost)) - Manpage fix [\#170](https://github.com/JFreegman/toxic/pull/170) ([Ansa89](https://github.com/Ansa89)) - Add help target and toxic.conf manpage [\#169](https://github.com/JFreegman/toxic/pull/169) ([Ansa89](https://github.com/Ansa89)) - Fixed setting buffer to half of the size [\#165](https://github.com/JFreegman/toxic/pull/165) ([ghost](https://github.com/ghost)) - Add manpage [\#164](https://github.com/JFreegman/toxic/pull/164) ([Ansa89](https://github.com/Ansa89)) - Try to fix autoconnect [\#161](https://github.com/JFreegman/toxic/pull/161) ([Ansa89](https://github.com/Ansa89)) - Wide characters support [\#157](https://github.com/JFreegman/toxic/pull/157) ([Ansa89](https://github.com/Ansa89)) - Polishing README.md [\#155](https://github.com/JFreegman/toxic/pull/155) ([theGeekPirate](https://github.com/theGeekPirate)) - README.md: add build status [\#153](https://github.com/JFreegman/toxic/pull/153) ([Ansa89](https://github.com/Ansa89)) - Update readme instructions [\#152](https://github.com/JFreegman/toxic/pull/152) ([Ansa89](https://github.com/Ansa89)) - Forgot to set index in some callbacks [\#151](https://github.com/JFreegman/toxic/pull/151) ([ghost](https://github.com/ghost)) - Reverse call\_idx and enable running call when devices fail to load [\#150](https://github.com/JFreegman/toxic/pull/150) ([ghost](https://github.com/ghost)) - Remove autotools dependency [\#149](https://github.com/JFreegman/toxic/pull/149) ([Ansa89](https://github.com/Ansa89)) - Cast localtime [\#147](https://github.com/JFreegman/toxic/pull/147) ([Ansa89](https://github.com/Ansa89)) - Changed code a bit and added new features [\#146](https://github.com/JFreegman/toxic/pull/146) ([ghost](https://github.com/ghost)) ## [0.4.1](https://github.com/JFreegman/toxic/tree/0.4.1) (2014-06-19) [Full Changelog](https://github.com/JFreegman/toxic/compare/0.4.0...0.4.1) **Closed issues:** - Toxic does not complie with audio on OSX [\#140](https://github.com/JFreegman/toxic/issues/140) - compiling error [\#139](https://github.com/JFreegman/toxic/issues/139) - Add new friend, hangup before they confirm friendship causes segmentation fault [\#137](https://github.com/JFreegman/toxic/issues/137) - build fail [\#124](https://github.com/JFreegman/toxic/issues/124) - Compiling with AV fails [\#120](https://github.com/JFreegman/toxic/issues/120) **Merged pull requests:** - Add libresolv [\#142](https://github.com/JFreegman/toxic/pull/142) ([jin-eld](https://github.com/jin-eld)) - Search for OpenAL framework on OSX [\#141](https://github.com/JFreegman/toxic/pull/141) ([jin-eld](https://github.com/jin-eld)) ## [0.4.0](https://github.com/JFreegman/toxic/tree/0.4.0) (2014-06-01) [Full Changelog](https://github.com/JFreegman/toxic/compare/0.3.0.1...0.4.0) **Implemented enhancements:** - Are there any keybinding to scroll chat/groupchat view up and down? [\#74](https://github.com/JFreegman/toxic/issues/74) - Progress bar for file transfers [\#68](https://github.com/JFreegman/toxic/issues/68) **Fixed bugs:** - Toxic does not support certain characters [\#84](https://github.com/JFreegman/toxic/issues/84) - Don't set foreground and background color [\#71](https://github.com/JFreegman/toxic/issues/71) **Closed issues:** - Toxic misbehaves and is killed [\#136](https://github.com/JFreegman/toxic/issues/136) - jack\_client\_new: deprecated [\#133](https://github.com/JFreegman/toxic/issues/133) - build error on os x 10.9 [\#129](https://github.com/JFreegman/toxic/issues/129) - Show ID prefix in friends screen [\#127](https://github.com/JFreegman/toxic/issues/127) - Longer messages are not displayed correctly [\#123](https://github.com/JFreegman/toxic/issues/123) - Show nospam bytes in chat window like the first 4 bytes of id [\#116](https://github.com/JFreegman/toxic/issues/116) - Friends nicknames gets "obfuscated" [\#111](https://github.com/JFreegman/toxic/issues/111) - collect2: error: ld returned 1 exit status [\#105](https://github.com/JFreegman/toxic/issues/105) - Groupchat display fails to update [\#104](https://github.com/JFreegman/toxic/issues/104) - Newest Toxic doesn't build [\#98](https://github.com/JFreegman/toxic/issues/98) **Merged pull requests:** - Update README.md [\#134](https://github.com/JFreegman/toxic/pull/134) ([zetok](https://github.com/zetok)) - Update audio\_call.c [\#132](https://github.com/JFreegman/toxic/pull/132) ([Impyy](https://github.com/Impyy)) - Not done yet. [\#130](https://github.com/JFreegman/toxic/pull/130) ([ghost](https://github.com/ghost)) - Fix file sender null terminator. [\#128](https://github.com/JFreegman/toxic/pull/128) ([aitjcize](https://github.com/aitjcize)) - Drop typedef redeclarations [\#122](https://github.com/JFreegman/toxic/pull/122) ([czarkoff](https://github.com/czarkoff)) - Include "pthread.h" [\#121](https://github.com/JFreegman/toxic/pull/121) ([czarkoff](https://github.com/czarkoff)) - Wow [\#119](https://github.com/JFreegman/toxic/pull/119) ([ghost](https://github.com/ghost)) - Use default terminal fg/bg colors when we can. [\#117](https://github.com/JFreegman/toxic/pull/117) ([ooesili](https://github.com/ooesili)) - Fixed support for wide characters [\#113](https://github.com/JFreegman/toxic/pull/113) ([graboy](https://github.com/graboy)) - Mention av [\#110](https://github.com/JFreegman/toxic/pull/110) ([stqism](https://github.com/stqism)) - allow history scrolling [\#109](https://github.com/JFreegman/toxic/pull/109) ([JFreegman](https://github.com/JFreegman)) - Only those who appreciate small things [\#107](https://github.com/JFreegman/toxic/pull/107) ([ghost](https://github.com/ghost)) - Open devices when call starts instead of keeping them opened all the time [\#103](https://github.com/JFreegman/toxic/pull/103) ([ghost](https://github.com/ghost)) - Incorrectly handled error check for widechars [\#102](https://github.com/JFreegman/toxic/pull/102) ([graboy](https://github.com/graboy)) - Fix toxic build when toxav is not available [\#100](https://github.com/JFreegman/toxic/pull/100) ([jin-eld](https://github.com/jin-eld)) - Add checks for pthreads to the build system [\#99](https://github.com/JFreegman/toxic/pull/99) ([jin-eld](https://github.com/jin-eld)) - Fixes and stuff... [\#97](https://github.com/JFreegman/toxic/pull/97) ([ghost](https://github.com/ghost)) ## [0.3.0.1](https://github.com/JFreegman/toxic/tree/0.3.0.1) (2014-03-12) [Full Changelog](https://github.com/JFreegman/toxic/compare/0.3.0...0.3.0.1) **Merged pull requests:** - SPELLING IS FOR FOOLS [\#94](https://github.com/JFreegman/toxic/pull/94) ([lehitoskin](https://github.com/lehitoskin)) ## [0.3.0](https://github.com/JFreegman/toxic/tree/0.3.0) (2014-03-12) [Full Changelog](https://github.com/JFreegman/toxic/compare/0.2.7...0.3.0) **Fixed bugs:** - SIGSEVG upon friend hanging up [\#89](https://github.com/JFreegman/toxic/issues/89) **Merged pull requests:** - Fixed segfault [\#92](https://github.com/JFreegman/toxic/pull/92) ([ghost](https://github.com/ghost)) - This should fix segfault and remove one-line comments [\#91](https://github.com/JFreegman/toxic/pull/91) ([ghost](https://github.com/ghost)) - Fixed another clang issue with bools that broek file sending. [\#90](https://github.com/JFreegman/toxic/pull/90) ([Jman012](https://github.com/Jman012)) - Toxic audio support [\#88](https://github.com/JFreegman/toxic/pull/88) ([ghost](https://github.com/ghost)) - Fixed clang error, disabling the execute module. [\#87](https://github.com/JFreegman/toxic/pull/87) ([Jman012](https://github.com/Jman012)) - Issue \#84 fixed [\#86](https://github.com/JFreegman/toxic/pull/86) ([thevar1able](https://github.com/thevar1able)) - Fixing fall-back from IPv6 to IPv4 [\#85](https://github.com/JFreegman/toxic/pull/85) ([micrictor](https://github.com/micrictor)) ## [0.2.7](https://github.com/JFreegman/toxic/tree/0.2.7) (2014-03-01) [Full Changelog](https://github.com/JFreegman/toxic/compare/0.2.6.1...0.2.7) **Closed issues:** - Toxic segfault when window is closed [\#81](https://github.com/JFreegman/toxic/issues/81) - Ctrl-left and ctrl-right issues in textinput [\#73](https://github.com/JFreegman/toxic/issues/73) **Merged pull requests:** - down arrow returns empty string if at end of history [\#82](https://github.com/JFreegman/toxic/pull/82) ([kl4ng](https://github.com/kl4ng)) - Fallback to loading /usr/share/toxic/DHTservers. [\#80](https://github.com/JFreegman/toxic/pull/80) ([viric](https://github.com/viric)) ## [0.2.6.1](https://github.com/JFreegman/toxic/tree/0.2.6.1) (2014-02-23) [Full Changelog](https://github.com/JFreegman/toxic/compare/0.2.6...0.2.6.1) ## [0.2.6](https://github.com/JFreegman/toxic/tree/0.2.6) (2014-02-23) [Full Changelog](https://github.com/JFreegman/toxic/compare/0.2.5...0.2.6) ## [0.2.5](https://github.com/JFreegman/toxic/tree/0.2.5) (2014-02-22) [Full Changelog](https://github.com/JFreegman/toxic/compare/prealpha_win32_r8...0.2.5) **Fixed bugs:** - Back space leaves ć character [\#44](https://github.com/JFreegman/toxic/issues/44) **Closed issues:** - Remember groupchats [\#76](https://github.com/JFreegman/toxic/issues/76) - Segfault [\#75](https://github.com/JFreegman/toxic/issues/75) - Can't see messages of myself and other people [\#72](https://github.com/JFreegman/toxic/issues/72) - binary blob in source [\#66](https://github.com/JFreegman/toxic/issues/66) - symbol lookup error [\#54](https://github.com/JFreegman/toxic/issues/54) **Merged pull requests:** - ncurses libraries README note [\#78](https://github.com/JFreegman/toxic/pull/78) ([kl4ng](https://github.com/kl4ng)) - umask such that stored files are u+rw only [\#77](https://github.com/JFreegman/toxic/pull/77) ([alevy](https://github.com/alevy)) - Fix groupchat cursor movement. [\#63](https://github.com/JFreegman/toxic/pull/63) ([aitjcize](https://github.com/aitjcize)) - Fix wchar cursor movement. [\#62](https://github.com/JFreegman/toxic/pull/62) ([aitjcize](https://github.com/aitjcize)) - api update [\#61](https://github.com/JFreegman/toxic/pull/61) ([naxuroqa](https://github.com/naxuroqa)) - Add option to switch off ipv6. [\#60](https://github.com/JFreegman/toxic/pull/60) ([aitjcize](https://github.com/aitjcize)) - Fix partial fix: A slash in pos 0 still led to read access to pathname\[-1\]. [\#59](https://github.com/JFreegman/toxic/pull/59) ([FullName](https://github.com/FullName)) - Fix corresponding API name changes in toxcore. [\#58](https://github.com/JFreegman/toxic/pull/58) ([aitjcize](https://github.com/aitjcize)) - Fix API ret code changes of ToxCore [\#57](https://github.com/JFreegman/toxic/pull/57) ([aitjcize](https://github.com/aitjcize)) ## [prealpha_win32_r8](https://github.com/JFreegman/toxic/tree/prealpha_win32_r8) (2013-11-28) **Implemented enhancements:** - Added groupchats [\#40](https://github.com/JFreegman/toxic/pull/40) ([JFreegman](https://github.com/JFreegman)) - Adapted to ipv6-enabled tox [\#38](https://github.com/JFreegman/toxic/pull/38) ([FullName](https://github.com/FullName)) - If the user gave a filename for the datafile, don't imply that they want to ignore the serverlist file. [\#37](https://github.com/JFreegman/toxic/pull/37) ([FullName](https://github.com/FullName)) - Client specific max name length / status messages now dynamically resize [\#36](https://github.com/JFreegman/toxic/pull/36) ([JFreegman](https://github.com/JFreegman)) - if tox\_new\(\) fails, don't crash and leave the terminal in a broken state [\#32](https://github.com/JFreegman/toxic/pull/32) ([FullName](https://github.com/FullName)) - truncate friends' notes if they're too long [\#30](https://github.com/JFreegman/toxic/pull/30) ([JFreegman](https://github.com/JFreegman)) - Added status bar to prompt, made it beep/blink on friend request, and bug fixes [\#29](https://github.com/JFreegman/toxic/pull/29) ([JFreegman](https://github.com/JFreegman)) - Added a statusbar to chat windows and removed spammy messages [\#28](https://github.com/JFreegman/toxic/pull/28) ([JFreegman](https://github.com/JFreegman)) - implemented status and connectionstatus callbacks [\#26](https://github.com/JFreegman/toxic/pull/26) ([JFreegman](https://github.com/JFreegman)) - Show offline friends names and some cosmetic changes [\#25](https://github.com/JFreegman/toxic/pull/25) ([JFreegman](https://github.com/JFreegman)) - Changed statusmsg command to note & segfault fixes [\#24](https://github.com/JFreegman/toxic/pull/24) ([JFreegman](https://github.com/JFreegman)) - refactor command argument parsing [\#23](https://github.com/JFreegman/toxic/pull/23) ([lukechampine](https://github.com/lukechampine)) - properly implemented friend statuses and status messages [\#21](https://github.com/JFreegman/toxic/pull/21) ([JFreegman](https://github.com/JFreegman)) - implemented friend deletion [\#15](https://github.com/JFreegman/toxic/pull/15) ([JFreegman](https://github.com/JFreegman)) - Fix configure for Free BSD [\#11](https://github.com/JFreegman/toxic/pull/11) ([jin-eld](https://github.com/jin-eld)) - Add check for setlocale\(\) [\#10](https://github.com/JFreegman/toxic/pull/10) ([manuel-arguelles](https://github.com/manuel-arguelles)) - Update build system [\#7](https://github.com/JFreegman/toxic/pull/7) ([jin-eld](https://github.com/jin-eld)) - Added travis integration [\#6](https://github.com/JFreegman/toxic/pull/6) ([stqism](https://github.com/stqism)) - Use new public api [\#5](https://github.com/JFreegman/toxic/pull/5) ([fhahn](https://github.com/fhahn)) - Add widechar checks [\#2](https://github.com/JFreegman/toxic/pull/2) ([jin-eld](https://github.com/jin-eld)) **Fixed bugs:** - Let windows.c actually get the tox \*m. [\#41](https://github.com/JFreegman/toxic/pull/41) ([Jman012](https://github.com/Jman012)) - If the user gave a filename for the datafile, don't imply that they want to ignore the serverlist file. [\#37](https://github.com/JFreegman/toxic/pull/37) ([FullName](https://github.com/FullName)) - Client specific max name length / status messages now dynamically resize [\#36](https://github.com/JFreegman/toxic/pull/36) ([JFreegman](https://github.com/JFreegman)) - Merged pr6 [\#34](https://github.com/JFreegman/toxic/pull/34) ([stqism](https://github.com/stqism)) - made error handling more consistent and added exit function [\#33](https://github.com/JFreegman/toxic/pull/33) ([JFreegman](https://github.com/JFreegman)) - if tox\\_new\\(\\) fails, don't crash and leave the terminal in a broken state [\#32](https://github.com/JFreegman/toxic/pull/32) ([FullName](https://github.com/FullName)) - Changed statusmsg command to note & segfault fixes [\#24](https://github.com/JFreegman/toxic/pull/24) ([JFreegman](https://github.com/JFreegman)) - fix buffer overflows and format issues [\#20](https://github.com/JFreegman/toxic/pull/20) ([JFreegman](https://github.com/JFreegman)) - Fix blocking while waiting for key [\#17](https://github.com/JFreegman/toxic/pull/17) ([manuel-arguelles](https://github.com/manuel-arguelles)) - fixed "free\(\): invalid pointer" when XDG\_CONFIG\_HOME is set [\#16](https://github.com/JFreegman/toxic/pull/16) ([gs93](https://github.com/gs93)) - Make sure toxic compiles on MinGW/Win32 again [\#14](https://github.com/JFreegman/toxic/pull/14) ([jin-eld](https://github.com/jin-eld)) - Fix for the "bad character" when doing backspace in chat window [\#12](https://github.com/JFreegman/toxic/pull/12) ([jin-eld](https://github.com/jin-eld)) - Fix configure for Free BSD [\#11](https://github.com/JFreegman/toxic/pull/11) ([jin-eld](https://github.com/jin-eld)) - Fix configure script for ncurses without ncursesw [\#9](https://github.com/JFreegman/toxic/pull/9) ([manuel-arguelles](https://github.com/manuel-arguelles)) - Fix configure script for mingw32 [\#8](https://github.com/JFreegman/toxic/pull/8) ([jin-eld](https://github.com/jin-eld)) - warning: comparison of integers of different signs: 'int' and 'unsigned long' [\#3](https://github.com/JFreegman/toxic/pull/3) ([1100110](https://github.com/1100110)) **Merged pull requests:** - Make sure friend message is null-terminated else generate garbate on screen [\#56](https://github.com/JFreegman/toxic/pull/56) ([aitjcize](https://github.com/aitjcize)) - Fix trailing slashes which leads to segfault. [\#55](https://github.com/JFreegman/toxic/pull/55) ([aitjcize](https://github.com/aitjcize)) - fix cflags [\#53](https://github.com/JFreegman/toxic/pull/53) ([JFreegman](https://github.com/JFreegman)) - Fix 93ab16c [\#52](https://github.com/JFreegman/toxic/pull/52) ([urras](https://github.com/urras)) - Offer solution for "error while loading shared libraries: libtoxcore.so.... [\#51](https://github.com/JFreegman/toxic/pull/51) ([urras](https://github.com/urras)) - Implemented file transfers [\#50](https://github.com/JFreegman/toxic/pull/50) ([JFreegman](https://github.com/JFreegman)) - Fix check for toxcore by linking sodium in the correct place [\#47](https://github.com/JFreegman/toxic/pull/47) ([devurandom](https://github.com/devurandom)) - Changed order of servers [\#46](https://github.com/JFreegman/toxic/pull/46) ([grimd34th](https://github.com/grimd34th)) - set friendnames properly and some fixes [\#45](https://github.com/JFreegman/toxic/pull/45) ([JFreegman](https://github.com/JFreegman)) - moved misc helper functions to separate file and removed redundant includes [\#43](https://github.com/JFreegman/toxic/pull/43) ([JFreegman](https://github.com/JFreegman)) - Refactored prompt command parser to work with all window types and moved command stuff to separate files [\#42](https://github.com/JFreegman/toxic/pull/42) ([JFreegman](https://github.com/JFreegman)) - Ipv6.init connection [\#39](https://github.com/JFreegman/toxic/pull/39) ([FullName](https://github.com/FullName)) - Remove DHT window [\#13](https://github.com/JFreegman/toxic/pull/13) ([JFreegman](https://github.com/JFreegman)) - Update README.md [\#4](https://github.com/JFreegman/toxic/pull/4) ([notadecent](https://github.com/notadecent)) - Toxic standalone [\#1](https://github.com/JFreegman/toxic/pull/1) ([jin-eld](https://github.com/jin-eld)) \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)*toxic-0.8.3/INSTALL.md000066400000000000000000000073031336615250000143040ustar00rootroot00000000000000# 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 | *None* | | [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_DESKTOP_NOTIFY=1` → Disable desktop notifications support * `ENABLE_PYTHON=1` → Build toxic with Python scripting support * `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.8.3/LICENSE000066400000000000000000001045141336615250000136630ustar00rootroot00000000000000 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.8.3/Makefile000066400000000000000000000044731336615250000143210ustar00rootroot00000000000000BASE_DIR = $(shell pwd -P) CFG_DIR = $(BASE_DIR)/cfg -include $(CFG_DIR)/global_vars.mk LIBS = toxcore ncursesw libconfig libcurl CFLAGS ?= -g CFLAGS += -std=gnu99 -pthread -Wall -fstack-protector-all 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 group_commands.o groupchat.o help.o input.o OBJ += line_info.o 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 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.8.3/README.md000066400000000000000000000021711336615250000141310ustar00rootroot00000000000000 Coverity Scan Build Status Toxic is a [Tox](https://tox.chat)-based instant messenging and video chat client. [![Toxic Screenshot](https://i.imgur.com/san99Z2.png "Home Screen")](https://i.imgur.com/san99Z2.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.8.3/apidoc/000077500000000000000000000000001336615250000141105ustar00rootroot00000000000000toxic-0.8.3/apidoc/python/000077500000000000000000000000001336615250000154315ustar00rootroot00000000000000toxic-0.8.3/apidoc/python/Makefile000066400000000000000000000011421336615250000170670ustar00rootroot00000000000000# 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.8.3/apidoc/python/source/000077500000000000000000000000001336615250000167315ustar00rootroot00000000000000toxic-0.8.3/apidoc/python/source/conf.py000066400000000000000000000111441336615250000202310ustar00rootroot00000000000000#!/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.8.3' # The full version, including alpha/beta/rc tags. release = '0.8.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.8.3/apidoc/python/source/examples.rst000066400000000000000000000001541336615250000213010ustar00rootroot00000000000000============ API Examples ============ Fortune ======= .. literalinclude:: fortune.py :language: python toxic-0.8.3/apidoc/python/source/fortune.py000066400000000000000000000020631336615250000207660ustar00rootroot00000000000000import 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.8.3/apidoc/python/source/index.rst000066400000000000000000000002211336615250000205650ustar00rootroot00000000000000Toxic Scripting Interface Documentation ======================================= .. toctree:: :maxdepth: 2 intro reference examples toxic-0.8.3/apidoc/python/source/intro.rst000066400000000000000000000007021336615250000206150ustar00rootroot00000000000000========================= 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.8.3/apidoc/python/source/reference.rst000066400000000000000000000033451336615250000214260ustar00rootroot00000000000000============= 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.8.3/astylerc000066400000000000000000000005501336615250000144220ustar00rootroot00000000000000# 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.8.3/cfg/000077500000000000000000000000001336615250000134105ustar00rootroot00000000000000toxic-0.8.3/cfg/checks/000077500000000000000000000000001336615250000146505ustar00rootroot00000000000000toxic-0.8.3/cfg/checks/audio.mk000066400000000000000000000014451336615250000163060ustar00rootroot00000000000000# 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) 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.8.3/cfg/checks/check_features.mk000066400000000000000000000051141336615250000201550ustar00rootroot00000000000000CHECKS_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 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.8.3/cfg/checks/desktop_notifications.mk000066400000000000000000000014111336615250000216000ustar00rootroot00000000000000# 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.8.3/cfg/checks/python.mk000066400000000000000000000012021336615250000165150ustar00rootroot00000000000000# 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.8.3/cfg/checks/qr.mk000066400000000000000000000011721336615250000156240ustar00rootroot00000000000000# 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.8.3/cfg/checks/qr_png.mk000066400000000000000000000012421336615250000164660ustar00rootroot00000000000000# 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.8.3/cfg/checks/sound_notifications.mk000066400000000000000000000016171336615250000212670ustar00rootroot00000000000000# 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.8.3/cfg/checks/video.mk000066400000000000000000000014531336615250000163120ustar00rootroot00000000000000# Variables for video call support VIDEO_LIBS = 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.8.3/cfg/checks/x11.mk000066400000000000000000000012751336615250000156170ustar00rootroot00000000000000# Variables for X11 support X11_LIBS = x11 X11_CFLAGS = -DX11 X11_OBJ = xtra.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.8.3/cfg/global_vars.mk000066400000000000000000000016321336615250000162360ustar00rootroot00000000000000# Version TOXIC_VERSION = 0.8.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.8.3/cfg/platforms/000077500000000000000000000000001336615250000154175ustar00rootroot00000000000000toxic-0.8.3/cfg/platforms/.gitignore000066400000000000000000000001071336615250000174050ustar00rootroot00000000000000# Ignore everything in this directory * # Except this file !.gitignore toxic-0.8.3/cfg/systems/000077500000000000000000000000001336615250000151175ustar00rootroot00000000000000toxic-0.8.3/cfg/systems/Darwin.mk000066400000000000000000000014651336615250000167020ustar00rootroot00000000000000# 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.8.3/cfg/targets/000077500000000000000000000000001336615250000150615ustar00rootroot00000000000000toxic-0.8.3/cfg/targets/doc.mk000066400000000000000000000004551336615250000161630ustar00rootroot00000000000000# 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.8.3/cfg/targets/help.mk000066400000000000000000000045211336615250000163440ustar00rootroot00000000000000# 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 " ENABLE_PYTHON: Set to \"1\" to enable building with Python scripting support" @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.8.3/cfg/targets/install.mk000066400000000000000000000026231336615250000170630ustar00rootroot00000000000000# 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.8.3/cfg/targets/uninstall.mk000066400000000000000000000011351336615250000174230ustar00rootroot00000000000000# 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.8.3/doc/000077500000000000000000000000001336615250000134165ustar00rootroot00000000000000toxic-0.8.3/doc/toxic.1000066400000000000000000000077471336615250000146450ustar00rootroot00000000000000'\" t .\" Title: toxic .\" Author: [see the "AUTHORS" section] .\" Generator: DocBook XSL Stylesheets v1.78.1 .\" Date: 2016-09-20 .\" Manual: Toxic Manual .\" Source: toxic __VERSION__ .\" Language: English .\" .TH "TOXIC" "1" "2016\-09\-20" "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 \-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 \-Text occasionally fails to auto\-scroll to the bottom\&. .sp \-Screen flickering sometimes occurs on certain terminals\&. .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: chat\&.freenode\&.net#tox toxic-0.8.3/doc/toxic.1.asc000066400000000000000000000047621336615250000154040ustar00rootroot00000000000000toxic(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 -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. -Text occasionally fails to auto-scroll to the bottom. -Screen flickering sometimes occurs on certain terminals. AUTHORS ------- JFreegman SEE ALSO -------- *toxic.conf*(5) LINKS ----- Project page: IRC channel: chat.freenode.net#tox toxic-0.8.3/doc/toxic.conf.5000066400000000000000000000177601336615250000155710ustar00rootroot00000000000000'\" t .\" Title: toxic.conf .\" Author: [see the "AUTHORS" section] .\" Generator: DocBook XSL Stylesheets v1.79.1 .\" Date: 2016-09-20 .\" Manual: Toxic Manual .\" Source: toxic __VERSION__ .\" Language: English .\" .TH "TOXIC\&.CONF" "5" "2016\-09\-20" "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 \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\&. (0 to disable updates) .RE .PP \fBhistory_size\fR .RS 4 Maximum lines for chat window history\&. Integer value\&. (for example: 700) .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_treshold\fR .RS 4 Voice Activity Detection treshold\&. Float value\&. Recommended values are around 40\&.0 .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 \fBpeer_list_up\fR .RS 4 Key combination to scroll contacts list up\&. .RE .PP \fBpeer_list_down\fR .RS 4 Key combination to scroll contacts list down\&. .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: chat\&.freenode\&.net#tox .SH "AUTHORS" .sp JFreegman toxic-0.8.3/doc/toxic.conf.5.asc000066400000000000000000000146371336615250000163360ustar00rootroot00000000000000toxic.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 *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. (0 to disable updates) *history_size*;; Maximum lines for chat window history. Integer value. (for example: 700) *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_treshold*;; Voice Activity Detection treshold. Float value. Recommended values are around 40.0 *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. *peer_list_up*;; Key combination to scroll contacts list up. *peer_list_down*;; Key combination to scroll contacts list down. *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: chat.freenode.net#tox AUTHORS ------- JFreegman toxic-0.8.3/misc/000077500000000000000000000000001336615250000136045ustar00rootroot00000000000000toxic-0.8.3/misc/nameservers000066400000000000000000000001121336615250000160530ustar00rootroot00000000000000toxme.io 1A39E7A5D5FA9CF155C751570A32E625698A60A55F6D88028F949F66144F4F25 toxic-0.8.3/misc/toxic.conf.example000066400000000000000000000070661336615250000172440ustar00rootroot00000000000000// 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; // 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:%S"; // 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; // maximum lines for chat window history history_size=700; // 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 treshold; float (recommended values are around 40) VAD_treshold=40.0; }; 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"; peer_list_up="Ctrl+["; peer_list_down="Ctrl+]"; toggle_peerlist="Ctrl+b"; toggle_paste_mode="Ctrl+T"; }; toxic-0.8.3/misc/toxic.desktop000066400000000000000000000003451336615250000163270ustar00rootroot00000000000000[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.8.3/sounds/000077500000000000000000000000001336615250000141645ustar00rootroot00000000000000toxic-0.8.3/sounds/ToxicContactOffline.wav000066400000000000000000003311441336615250000206160ustar00rootroot00000000000000RIFF\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.8.3/sounds/ToxicContactOnline.wav000066400000000000000000002510361336615250000204610ustar00rootroot00000000000000RIFFRWAVEfmt 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.8.3/sounds/ToxicError.wav000066400000000000000000001501621336615250000170100ustar00rootroot00000000000000RIFFjWAVEfmt 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.8.3/sounds/ToxicIncomingCall.wav000066400000000000000000017126121336615250000202630ustar00rootroot00000000000000RIFFWAVEfmt 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.8.3/sounds/ToxicOutgoingCall.wav000066400000000000000000027640121336615250000203150ustar00rootroot00000000000000RIFF 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.8.3/sounds/ToxicRecvMessage.wav000066400000000000000000002035001336615250000201160ustar00rootroot00000000000000RIFF8WAVEfmt 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.8.3/sounds/ToxicTransferComplete.wav000066400000000000000000002250101336615250000211670ustar00rootroot00000000000000RIFF*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.8.3/sounds/license000066400000000000000000000004421336615250000155310ustar00rootroot00000000000000ToxicError.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.8.3/src/000077500000000000000000000000001336615250000134405ustar00rootroot00000000000000toxic-0.8.3/src/api.c000066400000000000000000000117561336615250000143670ustar00rootroot00000000000000/* 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; extern FriendsList Friends; extern struct user_settings *user_settings; 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(); char timefrmt[TIME_STR_SIZE]; if (name == NULL) { return; } self_window = get_active_window(); get_time_str(timefrmt, sizeof(timefrmt)); strncpy((char *) self_window->chatwin->line, msg, sizeof(self_window->chatwin->line)); add_line_to_hist(self_window->chatwin); int id = line_info_add(self_window, timefrmt, 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, NULL, 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, NULL, NULL, NULL, SYS_MSG, 0, 0, error_str); return; } run_python(fp, argv[1]); fclose(fp); } void invoke_autoruns(WINDOW *window, ToxWindow *self) { struct dirent *dir; char abspath_buf[PATH_MAX + 1], err_buf[PATH_MAX + 1]; size_t path_len; DIR *d; FILE *fp; if (user_settings->autorun_path[0] == '\0') { return; } d = opendir(user_settings->autorun_path); if (d == NULL) { snprintf(err_buf, PATH_MAX + 1, "Autorun path does not exist: %s", user_settings->autorun_path); api_display(err_buf); return; } cur_window = window; self_window = self; while ((dir = readdir(d)) != NULL) { path_len = strlen(dir->d_name); if (!strcmp(dir->d_name + path_len - 3, ".py")) { snprintf(abspath_buf, PATH_MAX + 1, "%s%s", user_settings->autorun_path, dir->d_name); fp = fopen(abspath_buf, "r"); if (fp == NULL) { snprintf(err_buf, PATH_MAX + 1, "Invalid path: %s", abspath_buf); api_display(err_buf); continue; } run_python(fp, abspath_buf); fclose(fp); } } closedir(d); } #endif /* PYTHON */ toxic-0.8.3/src/api.h000066400000000000000000000026061336615250000143660ustar00rootroot00000000000000/* 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.8.3/src/audio_call.c000066400000000000000000000651561336615250000157150ustar00rootroot00000000000000/* 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 "toxic.h" #include "windows.h" #include "audio_call.h" #include "audio_device.h" #include "chat_commands.h" #include "global_commands.h" #include "line_info.h" #include "notify.h" #include "friendlist.h" #include "chat.h" #ifdef AUDIO #ifdef VIDEO #include "video_call.h" #endif /* VIDEO */ #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__ */ extern FriendsList Friends; extern ToxWindow *windows[MAX_WINDOWS_NUM]; struct CallControl CallControl; #define cbend pthread_exit(NULL) #define frame_size (CallControl.audio_sample_rate * CallControl.audio_frame_duration / 1000) static int set_call(Call *call, bool start) { call->in_idx = -1; call->out_idx = -1; #ifdef VIDEO call->vin_idx = -1; call->vout_idx = -1; #endif /* VIDEO */ if (start) { call->ttas = true; if (pthread_mutex_init(&call->mutex, NULL) != 0) { return -1; } } else { call->ttid = 0; if (pthread_mutex_destroy(&call->mutex) != 0) { return -1; } } return 0; } 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); static void print_err(ToxWindow *self, const char *error_str) { line_info_add(self, NULL, 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.pending_call = false; CallControl.av = toxav_new(tox, &error); CallControl.audio_enabled = true; CallControl.audio_bit_rate = 64; CallControl.audio_sample_rate = 48000; CallControl.audio_frame_duration = 20; CallControl.audio_channels = 1; #ifndef VIDEO CallControl.video_enabled = false; CallControl.video_bit_rate = 0; CallControl.video_frame_duration = 0; #endif /* VIDEO */ if (!CallControl.av) { CallControl.audio_errors |= ae_StartingCoreAudio; line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to init ToxAV"); return NULL; } if (init_devices(CallControl.av) == de_InternalError) { line_info_add(self, NULL, 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); return CallControl.av; } void terminate_audio(void) { int i; for (i = 0; i < CallControl.max_calls; ++i) { stop_transmission(&CallControl.calls[i], i); } if (CallControl.av) { toxav_kill(CallControl.av); } terminate_devices(); } void read_device_callback(const int16_t *captured, uint32_t size, void *data) { 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 || toxav_audio_send_frame(CallControl.av, friend_number, captured, sample_count, CallControl.audio_channels, CallControl.audio_sample_rate, &error) == false) { } } 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].ttas) { write_out(CallControl.calls[friend_number].out_idx, PCM, sample_count, channels, sample_rate); } } int start_transmission(ToxWindow *self, Call *call) { if (!self || !CallControl.av) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to prepare transmission"); return -1; } if (set_call(call, true) == -1) { return -1; } DeviceError error = open_primary_device(input, &call->in_idx, CallControl.audio_sample_rate, CallControl.audio_frame_duration, CallControl.audio_channels); if (error != de_None) { if (error == de_FailedStart) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to start input device"); } if (error == de_InternalError) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Internal error with opening input device"); } } if (register_device_callback(self->num, call->in_idx, read_device_callback, &self->num, true) != de_None) /* Set VAD as true for all; TODO: Make it more dynamic */ { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to register input handler!"); } if (open_primary_device(output, &call->out_idx, CallControl.audio_sample_rate, CallControl.audio_frame_duration, CallControl.audio_channels) != de_None) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to open output device!"); call->has_output = 0; } return 0; } int stop_transmission(Call *call, uint32_t friend_number) { if (call->ttas) { Toxav_Err_Call_Control error = TOXAV_ERR_CALL_CONTROL_OK; if (CallControl.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) { call->ttas = false; if (call->in_idx != -1) { close_device(input, call->in_idx); } if (call->out_idx != -1) { close_device(output, call->out_idx); } if (set_call(call, false) == -1) { return -1; } return 0; } else { return -1; } } return -1; } /* * End of transmission */ /* * Callbacks */ void on_call(ToxAV *av, uint32_t friend_number, bool audio_enabled, bool video_enabled, void *user_data) { Tox *m = (Tox *) user_data; CallControl.pending_call = true; callback_recv_invite(m, friend_number); } void on_call_state(ToxAV *av, uint32_t friend_number, uint32_t state, void *user_data) { CallControl.call_state = state; switch (state) { case (TOXAV_FRIEND_CALL_STATE_ERROR): line_info_add(CallControl.prompt, NULL, NULL, NULL, SYS_MSG, 0, 0, "ToxAV callstate error!"); #ifdef VIDEO callback_video_end(friend_number); #endif /* VIDEO */ stop_transmission(&CallControl.calls[friend_number], friend_number); callback_call_ended(friend_number); CallControl.pending_call = false; break; case (TOXAV_FRIEND_CALL_STATE_FINISHED): if (CallControl.pending_call) { callback_call_rejected(friend_number); } else { callback_call_ended(friend_number); } #ifdef VIDEO callback_recv_video_end(friend_number); callback_video_end(friend_number); #endif /* VIDEO */ stop_transmission(&CallControl.calls[friend_number], friend_number); /* Reset stored call state after finishing */ CallControl.call_state = 0; CallControl.pending_call = false; break; default: if (CallControl.pending_call) { /* Start answered call */ callback_call_started(friend_number); CallControl.pending_call = false; } else { #ifdef VIDEO /* Handle receiving client video call states */ if (state & TOXAV_FRIEND_CALL_STATE_SENDING_V) { callback_recv_video_starting(friend_number); } else if (state & ~TOXAV_FRIEND_CALL_STATE_SENDING_V) { 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) { write_device_callback(friend_number, pcm, sample_count, channels, sampling_rate); } void audio_bit_rate_status_cb(ToxAV *av, uint32_t friend_number, uint32_t audio_bit_rate, void *user_data) { CallControl.audio_bit_rate = audio_bit_rate; toxav_audio_set_bit_rate(av, friend_number, audio_bit_rate, user_data); } 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)); } 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, CallControl.call_state); } } } void callback_recv_ringing(uint32_t 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, CallControl.call_state); } } } void callback_recv_starting(uint32_t 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, CallControl.call_state); if (0 != start_transmission(windows[i], &CallControl.calls[friend_number])) { /* YEAH! */ line_info_add(windows[i], NULL, NULL, NULL, SYS_MSG, 0, 0, "Error starting transmission!"); } return; } } } void callback_recv_ending(uint32_t 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, CallControl.call_state); } } } void callback_call_started(uint32_t 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, CallControl.call_state); if (0 != start_transmission(windows[i], &CallControl.calls[friend_number])) { /* YEAH! */ line_info_add(windows[i], NULL, NULL, NULL, SYS_MSG, 0, 0, "Error starting transmission!"); return; } } } } void callback_call_canceled(uint32_t 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, CallControl.call_state); } } } void callback_call_rejected(uint32_t 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, CallControl.call_state); } } } void callback_call_ended(uint32_t 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, CallControl.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]) { Toxav_Err_Call error; const char *error_str; if (argc != 0) { error_str = "Unknown arguments."; goto on_error; } if (!CallControl.av) { error_str = "ToxAV not supported!"; goto on_error; } if (!self->stb->connection) { error_str = "Friend is offline."; goto on_error; } if (CallControl.pending_call) { error_str = "Already a pending call!"; goto on_error; } toxav_call(CallControl.av, self->num, CallControl.audio_bit_rate, CallControl.video_bit_rate, &error); if (error != TOXAV_ERR_CALL_OK) { if (error == TOXAV_ERR_CALL_FRIEND_ALREADY_IN_CALL) { error_str = "Already in a call!"; } else if (error == TOXAV_ERR_CALL_MALLOC) { error_str = "Memory allocation issue"; } else if (error == TOXAV_ERR_CALL_FRIEND_NOT_FOUND) { error_str = "Friend number invalid"; } else if (error == TOXAV_ERR_CALL_FRIEND_NOT_CONNECTED) { error_str = "Friend is valid but not currently connected"; } else { error_str = "Internal error!"; } goto on_error; } CallControl.pending_call = true; callback_recv_ringing(self->num); return; on_error: print_err(self, error_str); } void cmd_answer(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { Toxav_Err_Answer error; const char *error_str; if (argc != 0) { error_str = "Unknown arguments."; goto on_error; } if (!CallControl.av) { error_str = "Audio not supported!"; goto on_error; } if (!CallControl.pending_call) { error_str = "No incoming call!"; goto on_error; } toxav_answer(CallControl.av, self->num, CallControl.audio_bit_rate, CallControl.video_bit_rate, &error); if (error != TOXAV_ERR_ANSWER_OK) { if (error == TOXAV_ERR_ANSWER_FRIEND_NOT_CALLING) { error_str = "No incoming call!"; } else if (error == TOXAV_ERR_ANSWER_CODEC_INITIALIZATION) { error_str = "Failed to initialize codecs!"; } else if (error == TOXAV_ERR_ANSWER_FRIEND_NOT_FOUND) { error_str = "Friend not found!"; } else if (error == TOXAV_ERR_ANSWER_INVALID_BIT_RATE) { error_str = "Invalid bit rate!"; } else { error_str = "Internal error!"; } goto on_error; } /* Callback will print status... */ callback_recv_starting(self->num); CallControl.pending_call = false; return; on_error: print_err(self, error_str); } void cmd_reject(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { const char *error_str; if (argc != 0) { error_str = "Unknown arguments."; goto on_error; } if (!CallControl.av) { error_str = "Audio not supported!"; goto on_error; } if (!CallControl.pending_call) { error_str = "No incoming call!"; goto on_error; } /* Manually send a cancel call control because call hasn't started */ toxav_call_control(CallControl.av, self->num, TOXAV_CALL_CONTROL_CANCEL, NULL); CallControl.pending_call = false; /* Callback will print status... */ callback_call_rejected(self->num); return; on_error: print_err(self, error_str); } void cmd_hangup(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { const char *error_str = NULL; if (!CallControl.av) { error_str = "Audio not supported!"; goto on_error; } if (argc != 0) { error_str = "Unknown arguments."; goto on_error; } if (!self->is_call && !CallControl.pending_call) { error_str = "Not in a call."; goto on_error; } #ifdef VIDEO callback_video_end(self->num); #endif /* VIDEO */ stop_current_call(self); return; on_error: print_err(self, error_str); } void cmd_list_devices(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { const char *error_str; if (argc != 1) { if (argc < 1) { error_str = "Type must be specified!"; } else { error_str = "Only one argument allowed!"; } goto on_error; } 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, NULL, NULL, NULL, SYS_MSG, 0, 0, "Invalid type: %s", argv[1]); return; } // Refresh device list. get_devices_names(); print_devices(self, type); return; on_error: print_err(self, error_str); } /* This changes primary device only */ void cmd_change_device(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { const char *error_str; if (argc != 2) { if (argc < 1) { error_str = "Type must be specified!"; } else if (argc < 2) { error_str = "Must have id!"; } else { error_str = "Only two arguments allowed!"; } goto on_error; } 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, NULL, NULL, NULL, SYS_MSG, 0, 0, "Invalid type: %s", argv[1]); return; } char *end; long int selection = strtol(argv[2], &end, 10); if (*end) { error_str = "Invalid input"; goto on_error; } if (set_primary_device(type, selection) == de_InvalidSelection) { error_str = "Invalid selection!"; goto on_error; } return; on_error: print_err(self, error_str); } void cmd_ccur_device(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { const char *error_str; if (argc != 2) { if (argc < 1) { error_str = "Type must be specified!"; } else if (argc < 2) { error_str = "Must have id!"; } else { error_str = "Only two arguments allowed!"; } goto on_error; } 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, NULL, NULL, NULL, SYS_MSG, 0, 0, "Invalid type: %s", argv[1]); return; } char *end; long int selection = strtol(argv[2], &end, 10); if (*end) { error_str = "Invalid input"; goto on_error; } if (selection_valid(type, selection) == de_InvalidSelection) { error_str = "Invalid selection!"; goto on_error; } /* If call is active, change device */ if (self->is_call) { Call *this_call = &CallControl.calls[self->num]; if (this_call->ttas) { if (type == output) { pthread_mutex_lock(&this_call->mutex); close_device(output, this_call->out_idx); this_call->has_output = open_device(output, selection, &this_call->out_idx, CallControl.audio_sample_rate, CallControl.audio_frame_duration, CallControl.audio_channels) == de_None ? 1 : 0; pthread_mutex_unlock(&this_call->mutex); } else { /* TODO: check for failure */ close_device(input, this_call->in_idx); open_device(input, selection, &this_call->in_idx, CallControl.audio_sample_rate, CallControl.audio_frame_duration, CallControl.audio_channels); /* Set VAD as true for all; TODO: Make it more dynamic */ register_device_callback(self->num, this_call->in_idx, read_device_callback, &self->num, true); } } } self->device_selection[type] = selection; return; on_error: print_err(self, error_str); } void cmd_mute(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { const char *error_str; if (argc != 1) { if (argc < 1) { error_str = "Type must be specified!"; } else { error_str = "Only two arguments allowed!"; } goto on_error; } 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, NULL, NULL, NULL, SYS_MSG, 0, 0, "Invalid type: %s", argv[1]); return; } /* If call is active, use this_call values */ if (self->is_call) { Call *this_call = &CallControl.calls[self->num]; pthread_mutex_lock(&this_call->mutex); if (type == input) { device_mute(type, this_call->in_idx); self->chatwin->infobox.in_is_muted ^= 1; } else { device_mute(type, this_call->out_idx); self->chatwin->infobox.out_is_muted ^= 1; } pthread_mutex_unlock(&this_call->mutex); } return; on_error: print_err(self, error_str); } void cmd_sense(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { const char *error_str; if (argc != 1) { if (argc < 1) { error_str = "Must have value!"; } else { error_str = "Only two arguments allowed!"; } goto on_error; } char *end; float value = strtof(argv[1], &end); if (*end) { error_str = "Invalid input"; goto on_error; } /* Call must be active */ if (self->is_call) { device_set_VAD_treshold(CallControl.calls[self->num].in_idx, value); self->chatwin->infobox.vad_lvl = value; } return; on_error: print_err(self, error_str); } void cmd_bitrate(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { char *error_str; if (argc != 1) { error_str = "Must have value!"; goto on_error; } if (self->is_call == false) { error_str = "Must be in a call"; goto on_error; } const uint32_t bitrate = strtol(argv[1], NULL, 10); Toxav_Err_Bit_Rate_Set error; audio_bit_rate_status_cb(CallControl.av, self->num, bitrate, &error); if (error != TOXAV_ERR_BIT_RATE_SET_OK) { switch (error) { case TOXAV_ERR_BIT_RATE_SET_SYNC: error_str = "Syncronization error occured"; break; case TOXAV_ERR_BIT_RATE_SET_INVALID_BIT_RATE: error_str = "Invalid audio bit rate value (valid is 6-510)"; break; case TOXAV_ERR_BIT_RATE_SET_FRIEND_NOT_FOUND: error_str = "Friend not found"; break; case TOXAV_ERR_BIT_RATE_SET_FRIEND_NOT_IN_CALL: error_str = "Friend is not in the call"; break; default: error_str = "Unknown error"; } goto on_error; } return; on_error: print_err(self, error_str); } void stop_current_call(ToxWindow *self) { if (CallControl.pending_call) { toxav_call_control(CallControl.av, self->num, TOXAV_CALL_CONTROL_CANCEL, NULL); callback_call_canceled(self->num); } else { stop_transmission(&CallControl.calls[self->num], self->num); callback_call_ended(self->num); } CallControl.pending_call = false; } /** * 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) { realloc_calls(CallControl.max_calls + 1); memset(&CallControl.calls[CallControl.max_calls], 0, sizeof(Call)); if (index == CallControl.max_calls) { ++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.8.3/src/audio_call.h000066400000000000000000000046671336615250000157220ustar00rootroot00000000000000/* 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 */ typedef struct Call { pthread_t ttid; /* Transmission thread id */ bool ttas, has_output; /* Transmission thread active status (0 - stopped, 1- running) */ uint32_t in_idx, out_idx; /* Audio Index */ #ifdef VIDEO uint32_t vin_idx, vout_idx; /* Video Index */ #endif /* VIDEO */ pthread_mutex_t mutex; } Call; struct CallControl { AudioError audio_errors; #ifdef VIDEO VideoError video_errors; #endif /* VIDEO */ ToxAV *av; ToxWindow *prompt; Call *calls; uint32_t max_calls; uint32_t call_state; bool pending_call; bool audio_enabled; bool video_enabled; uint32_t audio_bit_rate; int32_t audio_frame_duration; uint32_t audio_sample_rate; uint8_t audio_channels; uint32_t video_bit_rate; int32_t video_frame_duration; }; 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); int start_transmission(ToxWindow *self, Call *call); int stop_transmission(Call *call, uint32_t friend_number); 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.8.3/src/audio_device.c000066400000000000000000000340101336615250000162220ustar00rootroot00000000000000/* 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" #ifdef AUDIO #include "audio_call.h" #endif #include "line_info.h" #include "settings.h" #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__ */ #include #include #include #include #include #include #define inline__ inline __attribute__((always_inline)) extern struct user_settings *user_settings; typedef struct Device { ALCdevice *dhndl; /* Handle of device selected/opened */ ALCcontext *ctx; /* Device context */ DataHandleCallback 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 */ uint32_t source, buffers[OPENAL_BUFS]; /* Playback source/buffers */ uint32_t ref_count; int32_t selection; bool enable_VAD; bool muted; pthread_mutex_t mutex[1]; uint32_t sample_rate; uint32_t frame_duration; int32_t sound_mode; #ifdef AUDIO float VAD_treshold; /* 40 is usually recommended value */ #endif } Device; const char *ddevice_names[2]; /* Default device */ const char *devices_names[2][MAX_DEVICES]; /* Container of available devices */ static int size[2]; /* Size of above containers */ Device *running[2][MAX_DEVICES] = {{NULL}}; /* Running devices */ uint32_t primary_device[2]; /* Primary device */ #ifdef AUDIO static ToxAV *av = NULL; #endif /* AUDIO */ /* q_mutex */ #define lock pthread_mutex_lock(&mutex) #define unlock pthread_mutex_unlock(&mutex) pthread_mutex_t mutex; bool thread_running = true, thread_paused = true; /* Thread control */ void *thread_poll(void *); /* Meet devices */ #ifdef AUDIO DeviceError init_devices(ToxAV *av_) #else DeviceError init_devices(void) #endif /* AUDIO */ { get_devices_names(); // Start poll thread if (pthread_mutex_init(&mutex, NULL) != 0) { return de_InternalError; } pthread_t thread_id; if (pthread_create(&thread_id, NULL, thread_poll, NULL) != 0 || pthread_detach(thread_id) != 0) { return de_InternalError; } #ifdef AUDIO av = av_; #endif /* AUDIO */ return (DeviceError) de_None; } DeviceError terminate_devices(void) { /* Cleanup if needed */ lock; thread_running = false; unlock; usleep(20000); if (pthread_mutex_destroy(&mutex) != 0) { return (DeviceError) de_InternalError; } return (DeviceError) de_None; } void get_devices_names(void) { const char *stringed_device_list; size[input] = 0; if ((stringed_device_list = alcGetString(NULL, ALC_CAPTURE_DEVICE_SPECIFIER))) { ddevice_names[input] = alcGetString(NULL, ALC_CAPTURE_DEFAULT_DEVICE_SPECIFIER); for (; *stringed_device_list && size[input] < MAX_DEVICES; ++size[input]) { devices_names[input][size[input]] = stringed_device_list; stringed_device_list += strlen(stringed_device_list) + 1; } } size[output] = 0; 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) { ddevice_names[output] = alcGetString(NULL, ALC_DEFAULT_DEVICE_SPECIFIER); for (; *stringed_device_list && size[output] < MAX_DEVICES; ++size[output]) { devices_names[output][size[output]] = 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; } lock; Device *device = running[type][device_idx]; if (!device) { unlock; return de_DeviceNotActive; } device->muted = !device->muted; unlock; return de_None; } #ifdef AUDIO DeviceError device_set_VAD_treshold(uint32_t device_idx, float value) { if (device_idx >= MAX_DEVICES) { return de_InvalidSelection; } lock; Device *device = running[input][device_idx]; if (!device) { unlock; return de_DeviceNotActive; } device->VAD_treshold = value; unlock; return de_None; } #endif DeviceError set_primary_device(DeviceType type, int32_t selection) { if (size[type] <= selection || selection < 0) { return de_InvalidSelection; } primary_device[type] = selection; return de_None; } DeviceError open_primary_device(DeviceType type, uint32_t *device_idx, uint32_t sample_rate, uint32_t frame_duration, uint8_t channels) { return open_device(type, primary_device[type], device_idx, sample_rate, frame_duration, channels); } void get_primary_device_name(DeviceType type, char *buf, int size) { memcpy(buf, ddevice_names[type], size); } // TODO: generate buffers separately DeviceError open_device(DeviceType type, int32_t selection, uint32_t *device_idx, uint32_t sample_rate, uint32_t frame_duration, uint8_t channels) { if (size[type] <= selection || selection < 0) { return de_InvalidSelection; } if (channels != 1 && channels != 2) { return de_UnsupportedMode; } lock; const uint32_t frame_size = (sample_rate * frame_duration / 1000); uint32_t i; for (i = 0; i < MAX_DEVICES && running[type][i] != NULL; ++i); if (i == MAX_DEVICES) { unlock; return de_AllDevicesBusy; } else { *device_idx = i; } for (i = 0; i < MAX_DEVICES; i ++) { /* Check if any device has the same selection */ if (running[type][i] && running[type][i]->selection == selection) { // printf("a%d-%d:%p ", selection, i, running[type][i]->dhndl); running[type][*device_idx] = running[type][i]; running[type][i]->ref_count ++; unlock; return de_None; } } Device *device = running[type][*device_idx] = calloc(1, sizeof(Device)); device->selection = selection; device->sample_rate = sample_rate; device->frame_duration = frame_duration; device->sound_mode = channels == 1 ? AL_FORMAT_MONO16 : AL_FORMAT_STEREO16; if (pthread_mutex_init(device->mutex, NULL) != 0) { free(device); unlock; return de_InternalError; } if (type == input) { device->dhndl = alcCaptureOpenDevice(devices_names[type][selection], sample_rate, device->sound_mode, frame_size * 2); #ifdef AUDIO device->VAD_treshold = user_settings->VAD_treshold; #endif } else { device->dhndl = alcOpenDevice(devices_names[type][selection]); if (!device->dhndl) { free(device); running[type][*device_idx] = NULL; unlock; return de_FailedStart; } device->ctx = alcCreateContext(device->dhndl, NULL); alcMakeContextCurrent(device->ctx); alGenBuffers(OPENAL_BUFS, device->buffers); alGenSources((uint32_t)1, &device->source); alSourcei(device->source, AL_LOOPING, AL_FALSE); uint16_t zeros[frame_size]; memset(zeros, 0, frame_size * 2); for (i = 0; i < OPENAL_BUFS; ++i) { alBufferData(device->buffers[i], device->sound_mode, zeros, frame_size * 2, sample_rate); } alSourceQueueBuffers(device->source, OPENAL_BUFS, device->buffers); alSourcePlay(device->source); } if (alcGetError(device->dhndl) != AL_NO_ERROR) { free(device); running[type][*device_idx] = NULL; unlock; return de_FailedStart; } if (type == input) { alcCaptureStart(device->dhndl); thread_paused = false; } unlock; return de_None; } DeviceError close_device(DeviceType type, uint32_t device_idx) { if (device_idx >= MAX_DEVICES) { return de_InvalidSelection; } lock; Device *device = running[type][device_idx]; DeviceError rc = de_None; if (!device) { unlock; return de_DeviceNotActive; } running[type][device_idx] = NULL; if (!device->ref_count) { if (type == input) { if (!alcCaptureCloseDevice(device->dhndl)) { rc = de_AlError; } } else { if (alcGetCurrentContext() != device->ctx) { alcMakeContextCurrent(device->ctx); } alDeleteSources(1, &device->source); alDeleteBuffers(OPENAL_BUFS, device->buffers); alcMakeContextCurrent(NULL); if (device->ctx) { alcDestroyContext(device->ctx); } if (!alcCloseDevice(device->dhndl)) { rc = de_AlError; } } free(device); } else { device->ref_count--; } unlock; return rc; } DeviceError register_device_callback(int32_t friend_number, uint32_t device_idx, DataHandleCallback callback, void *data, bool enable_VAD) { if (size[input] <= device_idx || !running[input][device_idx] || running[input][device_idx]->dhndl == NULL) { return de_InvalidSelection; } lock; running[input][device_idx]->cb = callback; running[input][device_idx]->cb_data = data; running[input][device_idx]->enable_VAD = enable_VAD; running[input][device_idx]->friend_number = friend_number; unlock; return de_None; } inline__ 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; } Device *device = running[output][device_idx]; if (!device || device->muted) { return de_DeviceNotActive; } pthread_mutex_lock(device->mutex); ALuint bufid; ALint processed, queued; alGetSourcei(device->source, AL_BUFFERS_PROCESSED, &processed); alGetSourcei(device->source, AL_BUFFERS_QUEUED, &queued); if (processed) { ALuint bufids[processed]; alSourceUnqueueBuffers(device->source, processed, bufids); alDeleteBuffers(processed - 1, bufids + 1); bufid = bufids[0]; } else if (queued < 16) { alGenBuffers(1, &bufid); } else { pthread_mutex_unlock(device->mutex); return de_Busy; } alBufferData(bufid, channels == 1 ? AL_FORMAT_MONO16 : AL_FORMAT_STEREO16, data, sample_count * 2 * channels, sample_rate); alSourceQueueBuffers(device->source, 1, &bufid); ALint state; alGetSourcei(device->source, AL_SOURCE_STATE, &state); if (state != AL_PLAYING) { alSourcePlay(device->source); } pthread_mutex_unlock(device->mutex); return de_None; } void *thread_poll(void *arg) // TODO: maybe use thread for every input source { /* * NOTE: We only need to poll input devices for data. */ (void)arg; uint32_t i; int32_t sample = 0; while (1) { lock; if (!thread_running) { unlock; break; } bool paused = thread_paused; unlock; /* Wait for unpause. */ if (paused) { usleep(10000); } else { for (i = 0; i < size[input]; ++i) { lock; if (running[input][i] != NULL) { alcGetIntegerv(running[input][i]->dhndl, ALC_CAPTURE_SAMPLES, sizeof(int32_t), &sample); int f_size = (running[input][i]->sample_rate * running[input][i]->frame_duration / 1000); if (sample < f_size) { unlock; continue; } Device *device = running[input][i]; int16_t frame[16000]; alcCaptureSamples(device->dhndl, frame, f_size); if (device->muted) { unlock; continue; } if (device->cb) { device->cb(frame, f_size, device->cb_data); } } unlock; } usleep(5000); } } pthread_exit(NULL); } void print_devices(ToxWindow *self, DeviceType type) { int i; for (i = 0; i < size[type]; ++i) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, i == primary_device[type] ? 1 : 0, 0, "%d: %s", i, devices_names[type][i]); } return; } DeviceError selection_valid(DeviceType type, int32_t selection) { return (size[type] <= selection || selection < 0) ? de_InvalidSelection : de_None; } void *get_device_callback_data(uint32_t device_idx) { if (size[input] <= device_idx || !running[input][device_idx] || running[input][device_idx]->dhndl == NULL) { return NULL; } return running[input][device_idx]->cb_data; } toxic-0.8.3/src/audio_device.h000066400000000000000000000057661336615250000162470ustar00rootroot00000000000000/* 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_DEVICES 32 #include #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); #ifdef AUDIO DeviceError init_devices(ToxAV *av); #else DeviceError init_devices(void); #endif /* AUDIO */ void get_devices_names(void); DeviceError terminate_devices(void); /* Callback handles ready data from INPUT device */ DeviceError register_device_callback(int32_t friend_number, uint32_t device_idx, DataHandleCallback callback, void *data, bool enable_VAD); void *get_device_callback_data(uint32_t device_idx); /* toggle device mute */ DeviceError device_mute(DeviceType type, uint32_t device_idx); #ifdef AUDIO DeviceError device_set_VAD_treshold(uint32_t device_idx, float value); #endif DeviceError set_primary_device(DeviceType type, int32_t selection); DeviceError open_primary_device(DeviceType type, uint32_t *device_idx, uint32_t sample_rate, uint32_t frame_duration, uint8_t channels); /* Start device */ DeviceError open_device(DeviceType type, int32_t selection, 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 device */ DeviceError write_out(uint32_t device_idx, const int16_t *data, uint32_t length, uint8_t channels, uint32_t sample_rate); void print_devices(ToxWindow *self, DeviceType type); void get_primary_device_name(DeviceType type, char *buf, int size); DeviceError selection_valid(DeviceType type, int32_t selection); #endif /* AUDIO_DEVICE_H */ toxic-0.8.3/src/autocomplete.c000066400000000000000000000234551336615250000163160ustar00rootroot00000000000000/* 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 "windows.h" #include "toxic.h" #include "misc_tools.h" #include "line_info.h" #include "execute.h" #include "configdir.h" static void print_matches(ToxWindow *self, Tox *m, const void *list, size_t n_items, size_t size) { if (m) { execute(self->chatwin->history, self, m, "/clear", GLOBAL_COMMAND_MODE); } const char *L = (char *) list; int i; for (i = 0; i < n_items; ++i) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "%s", &L[i * size]); } line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, ""); /* formatting */ } /* 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, char (*matches)[MAX_STR_SIZE], int n) { if (n == 1) { return snprintf(match, match_sz, "%s", matches[0]); } int i; for (i = 0; i < MAX_STR_SIZE; ++i) { char ch1 = matches[0][i]; int j; for (j = 0; j < n; ++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 the list of strings being compared, n_items is the number of items * in the list, and size is the size of each item in the list. * * 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 void *list, size_t n_items, size_t size, 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 || size > MAX_STR_SIZE) { return -1; } const char *L = (const char *) list; const char *endchrs = " "; char ubuf[MAX_STR_SIZE] = {0}; /* 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]; snprintf(tmp, sizeof(tmp), "%s", ubuf); tmp[ctx->pos] = '\0'; const char *s = 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]) { free(sub); return 0; } int s_len = strlen(sub); size_t n_matches = 0; char matches[n_items][MAX_STR_SIZE]; int i = 0; /* put all list matches in matches array */ for (i = 0; i < n_items; ++i) { char str[MAX_CMDNAME_SIZE + 1]; snprintf(str, sizeof(str), "%s", &L[i * size]); if (strncasecmp(str, sub, s_len) == 0) { strcpy(matches[n_matches++], str); } } free(sub); if (!n_matches) { return -1; } if (!dir_search && n_matches > 1) { print_matches(self, NULL, matches, n_matches, MAX_STR_SIZE); } char match[MAX_STR_SIZE]; size_t match_len = get_str_match(self, match, sizeof(match), matches, n_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) { // should never happen return -1; } 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 void *list, size_t n_items, size_t size) { return complete_line_helper(self, list, n_items, size, false); } static int complete_path(ToxWindow *self, const void *list, size_t n_items, size_t size) { return complete_line_helper(self, list, n_items, size, 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]; 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; } /* 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 512 int dir_match(ToxWindow *self, Tox *m, const wchar_t *line, const wchar_t *cmd) { char b_path[MAX_STR_SIZE]; char b_name[MAX_STR_SIZE]; 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) { 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), 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 */ char tmp[MAX_STR_SIZE]; snprintf(tmp, sizeof(tmp), ".%s", b_path); snprintf(b_path, sizeof(b_path), "%s", tmp); } snprintf(b_name, sizeof(b_name), "%s", &b_path[si + 1]); b_path[si + 1] = '\0'; int b_name_len = strlen(b_name); DIR *dp = opendir(b_path); if (dp == NULL) { return -1; } char dirnames[MAX_DIRS][NAME_MAX + 1]; struct dirent *entry; int dircount = 0; while ((entry = readdir(dp)) && dircount < MAX_DIRS) { if (strncmp(entry->d_name, b_name, b_name_len) == 0 && strcmp(".", entry->d_name) && strcmp("..", entry->d_name)) { snprintf(dirnames[dircount], sizeof(dirnames[dircount]), "%s", entry->d_name); ++dircount; } } closedir(dp); if (dircount == 0) { return -1; } if (dircount > 1) { qsort(dirnames, dircount, NAME_MAX + 1, qsort_strcasecmp_hlpr); print_matches(self, m, dirnames, dircount, NAME_MAX + 1); } return complete_path(self, dirnames, dircount, NAME_MAX + 1); } toxic-0.8.3/src/autocomplete.h000066400000000000000000000034241336615250000163150ustar00rootroot00000000000000/* 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 /* * 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 the list of strings being compared, n_items is the number of items * in the list, and size is the size of each item in the list. * * Returns the difference between the old len and new len of line on success. * Returns -1 on error. */ int complete_line(ToxWindow *self, const void *list, size_t n_items, size_t size); /* 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.8.3/src/avatars.c000066400000000000000000000124411336615250000152470ustar00rootroot00000000000000/* 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 "misc_tools.h" #include "file_transfers.h" #include "friendlist.h" #include "avatars.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; static void avatar_clear(void) { memset(&Avatar, 0, sizeof(struct Avatar)); } /* Sends avatar to friendnum. * * Returns 0 on success. * Returns -1 on failure. */ int avatar_send(Tox *m, uint32_t friendnum) { Tox_Err_File_Send err; uint32_t filenum = tox_file_send(m, friendnum, 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 %d (error %d)\n", friendnum, err); return -1; } struct FileTransfer *ft = new_file_transfer(NULL, friendnum, filenum, 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) { size_t i; for (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; } 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_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[length]; size_t send_length = fread(send_data, 1, sizeof(send_data), ft->file); if (send_length != length) { close_file_transfer(NULL, m, ft, TOX_FILE_CONTROL_CANCEL, NULL, silent); return; } Tox_Err_File_Send_Chunk err; tox_file_send_chunk(m, ft->friendnum, ft->filenum, position, send_data, send_length, &err); 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; ft->last_keep_alive = get_unix_time(); } toxic-0.8.3/src/avatars.h000066400000000000000000000027101336615250000152520ustar00rootroot00000000000000/* 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 #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); #endif /* AVATARS_H */ toxic-0.8.3/src/bootstrap.c000066400000000000000000000373411336615250000156310ustar00rootroot00000000000000/* 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 "line_info.h" #include "windows.h" #include "misc_tools.h" #include "configdir.h" #include "curl_util.h" #include "settings.h" #include "prompt.h" extern struct arg_opts arg_opts; extern struct user_settings *user_settings; extern struct Winthread Winthread; /* 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 PORT_MAX_SIZE 5 #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; /* 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. */ 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; memset(&recv_data, 0, sizeof(struct Recv_Curl_Data)); if (curl_fetch_nodes_JSON(&recv_data) == -1) { fclose(fp); return -2; } if (recv_data.length == 0) { fclose(fp); return -3; } if (fwrite(recv_data.data, recv_data.length, 1, fp) != 1) { fclose(fp); return -4; } 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) { 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); if (key_len == -1) { return -6; } if (hex_string_to_bin(key_string, key_len, node->key, TOX_PUBLIC_KEY_SIZE) == -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) { 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.8.3/src/bootstrap.h000066400000000000000000000025061336615250000156310ustar00rootroot00000000000000/* 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.8.3/src/chat.c000066400000000000000000001161051336615250000145270ustar00rootroot00000000000000/* 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 "toxic.h" #include "windows.h" #include "execute.h" #include "misc_tools.h" #include "file_transfers.h" #include "friendlist.h" #include "toxic_strings.h" #include "log.h" #include "line_info.h" #include "settings.h" #include "input.h" #include "help.h" #include "autocomplete.h" #include "notify.h" #include "message_queue.h" #ifdef AUDIO #include "audio_call.h" #ifdef VIDEO #include "video_call.h" #endif /* VIDEO */ #endif /* AUDIO */ extern char *DATA_FILE; extern FriendsList Friends; extern struct Winthread Winthread; extern struct user_settings *user_settings; #ifdef AUDIO static void init_infobox(ToxWindow *self); static void kill_infobox(ToxWindow *self); #endif /* AUDIO */ #ifdef AUDIO #define AC_NUM_CHAT_COMMANDS_AUDIO 9 #else #define AC_NUM_CHAT_COMMANDS_AUDIO 0 #endif /* AUDIO */ #ifdef PYTHON #define AC_NUM_CHAT_COMMANDS_PYTHON 1 #else #define AC_NUM_CHAT_COMMANDS_PYTHON 0 #endif /* PYTHON */ #ifdef QRCODE #define AC_NUM_CHAT_COMMANDS_QRCODE 1 #else #define AC_NUM_CHAT_COMMANDS_QRCODE 0 #endif /* QRCODE */ #define AC_NUM_CHAT_COMMANDS (21 + AC_NUM_CHAT_COMMANDS_AUDIO + AC_NUM_CHAT_COMMANDS_PYTHON + AC_NUM_CHAT_COMMANDS_QRCODE) /* Array of chat command names used for tab completion. */ static const char chat_cmd_list[AC_NUM_CHAT_COMMANDS][MAX_CMDNAME_SIZE] = { { "/accept" }, { "/add" }, { "/avatar" }, { "/cancel" }, { "/clear" }, { "/close" }, { "/connect" }, { "/exit" }, { "/group" }, { "/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" }, { "/video" }, { "/bitrate" }, #endif /* AUDIO */ #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_self_set_typing(m, self->num, is_typing, NULL); ctx->self_is_typing = is_typing; } void kill_chat_window(ToxWindow *self, Tox *m) { ChatContext *ctx = self->chatwin; StatusBar *statusbar = self->stb; #ifdef AUDIO #ifdef VIDEO stop_video_stream(self); #endif /* VIDEO */ 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); del_window(self); } static void recv_message_helper(ToxWindow *self, Tox *m, uint32_t num, const char *msg, size_t len, const char *nick, const char *timefrmt) { ChatContext *ctx = self->chatwin; line_info_add(self, timefrmt, 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, Tox *m, uint32_t num, const char *action, size_t len, const char *nick, const char *timefrmt) { ChatContext *ctx = self->chatwin; line_info_add(self, timefrmt, 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) { if (self->num != num) { return; } char nick[TOX_MAX_NAME_LENGTH]; get_nick_truncate(m, nick, num); char timefrmt[TIME_STR_SIZE]; get_time_str(timefrmt, sizeof(timefrmt)); if (type == TOX_MESSAGE_TYPE_NORMAL) { recv_message_helper(self, m, num, msg, len, nick, timefrmt); return; } if (type == TOX_MESSAGE_TYPE_ACTION) { recv_action_helper(self, m, num, msg, len, nick, timefrmt); return; } } static void chat_pause_file_transfers(Tox *m, 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 timefrmt[TIME_STR_SIZE]; get_time_str(timefrmt, sizeof(timefrmt)); 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); msg = "has come online"; line_info_add(self, timefrmt, 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, 0); } chat_pause_file_transfers(m, num); msg = "has gone offline"; line_info_add(self, timefrmt, 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) { 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) { 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) { 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) { 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) { cqueue_remove(self, m, receipt); } /* Stops active file transfers for this friend. Called when a friend goes offline */ static void chat_pause_file_transfers(Tox *m, uint32_t friendnum) { ToxicFriend *friend = &Friends.list[friendnum]; size_t i; for (i = 0; i < MAX_FILES; ++i) { if (friend->file_sender[i].state >= FILE_TRANSFER_STARTED) { friend->file_sender[i].state = FILE_TRANSFER_PAUSED; } if (friend->file_receiver[i].state >= FILE_TRANSFER_STARTED) { friend->file_receiver[i].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) { size_t i; for (i = 0; i < MAX_FILES; ++i) { struct FileTransfer *ft = &Friends.list[friendnum].file_sender[i]; if (ft->state != FILE_TRANSFER_PAUSED) { continue; } Tox_Err_File_Send err; ft->filenum = 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 filenum, uint64_t position, size_t length) { if (friendnum != self->num) { return; } struct FileTransfer *ft = get_file_transfer_struct(friendnum, filenum); 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[length]; size_t send_length = fread(send_data, 1, sizeof(send_data), 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); return; } Tox_Err_File_Send_Chunk err; tox_file_send_chunk(m, ft->friendnum, ft->filenum, position, send_data, send_length, &err); 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; ft->last_keep_alive = get_unix_time(); } static void chat_onFileRecvChunk(ToxWindow *self, Tox *m, uint32_t friendnum, uint32_t filenum, uint64_t position, const char *data, size_t length) { if (friendnum != self->num) { return; } struct FileTransfer *ft = get_file_transfer_struct(friendnum, filenum); 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; ft->last_keep_alive = get_unix_time(); } static void chat_onFileControl(ToxWindow *self, Tox *m, uint32_t friendnum, uint32_t filenum, Tox_File_Control control) { if (friendnum != self->num) { return; } struct FileTransfer *ft = get_file_transfer_struct(friendnum, filenum); if (!ft) { return; } char msg[MAX_STR_SIZE]; switch (control) { case TOX_FILE_CONTROL_RESUME: { ft->last_keep_alive = get_unix_time(); /* transfer is accepted */ if (ft->state == FILE_TRANSFER_PENDING) { ft->state = FILE_TRANSFER_STARTED; line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "File transfer [%d] for '%s' accepted.", ft->index, ft->file_name); char progline[MAX_STR_SIZE]; init_progress_bar(progline); line_info_add(self, NULL, 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: { 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 filenum) { char msg[MAX_STR_SIZE]; uint8_t file_id[TOX_FILE_ID_LENGTH]; if (!tox_file_get_file_id(m, friendnum, filenum, 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->filenum = filenum; ft->state = FILE_TRANSFER_STARTED; ft->last_keep_alive = get_unix_time(); resuming = true; break; } } if (!resuming || !ft) { return false; } if (!tox_file_seek(m, ft->friendnum, ft->filenum, ft->position, NULL)) { goto on_error; } if (!tox_file_control(m, ft->friendnum, ft->filenum, 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; } static void chat_onFileRecv(ToxWindow *self, Tox *m, uint32_t friendnum, uint32_t filenum, 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, filenum)) { return; } struct FileTransfer *ft = new_file_transfer(self, friendnum, filenum, FILE_TRANSFER_RECV, TOX_FILE_KIND_DATA); if (!ft) { tox_file_control(m, friendnum, filenum, TOX_FILE_CONTROL_CANCEL, NULL); line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "File transfer failed: Too many concurrent file transfers."); return; } char sizestr[32]; bytes_convert_str(sizestr, sizeof(sizestr), file_size); line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "File transfer request for '%s' (%s)", filename, sizestr); char file_path[PATH_MAX + name_length + 1]; 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, sizeof(file_path), "%s%s", user_settings->download_path, filename); path_len += strlen(user_settings->download_path); } else { snprintf(file_path, sizeof(file_path), "%s", filename); } if (path_len >= sizeof(file_path) || path_len >= sizeof(ft->file_path) || name_length >= sizeof(ft->file_name)) { tox_file_control(m, friendnum, filenum, TOX_FILE_CONTROL_CANCEL, NULL); line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "File transfer faield: File path too long."); 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[9]; sprintf(d, "(%d)", count); ++count; size_t d_len = strlen(d); if (path_len + d_len >= sizeof(file_path)) { path_len -= d_len; file_path[path_len] = '\0'; } strcat(file_path, d); file_path[path_len + d_len] = '\0'; if (count > 999) { tox_file_control(m, friendnum, filenum, TOX_FILE_CONTROL_CANCEL, NULL); line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "File transfer failed: invalid file path."); return; } } line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Type '/savefile %d' 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, filenum, ft->file_id, NULL); 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_onGroupInvite(ToxWindow *self, Tox *m, int32_t friendnumber, uint8_t type, const char *group_pub_key, uint16_t length) { if (self->num != friendnumber) { return; } if (Friends.list[friendnumber].group_invite.key != NULL) { free(Friends.list[friendnumber].group_invite.key); } char *k = malloc(length); if (k == NULL) { exit_toxic_err("Failed in chat_onGroupInvite", FATALERR_MEMORY); } memcpy(k, group_pub_key, length); Friends.list[friendnumber].group_invite.key = k; Friends.list[friendnumber].group_invite.pending = true; Friends.list[friendnumber].group_invite.length = length; Friends.list[friendnumber].group_invite.type = type; sound_notify(self, generic_message, NT_WNDALERT_2 | user_settings->bell_on_invite, NULL); char name[TOX_MAX_NAME_LENGTH]; get_nick_truncate(m, name, friendnumber); if (self->active_box != -1) { box_silent_notify2(self, NT_WNDALERT_2 | NT_NOFOCUS, self->active_box, "invites you to join group chat"); } else { box_silent_notify(self, NT_WNDALERT_2 | NT_NOFOCUS, &self->active_box, name, "invites you to join group chat"); } line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "%s has invited you to a group chat.", name); line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Type \"/join\" to join the chat."); } /* AV Stuff */ #ifdef AUDIO void chat_onInvite(ToxWindow *self, ToxAV *av, uint32_t friend_number, int state) { if (!self || self->num != friend_number) { return; } /* call is flagged active here */ self->is_call = true; line_info_add(self, NULL, 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) { if (!self || self->num != friend_number) { return; } line_info_add(self, NULL, 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) { if (!self || self->num != friend_number) { return; } init_infobox(self); line_info_add(self, NULL, 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) { if (!self || self->num != friend_number) { return; } kill_infobox(self); line_info_add(self, NULL, 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) { if (!self || self->num != friend_number) { return; } self->is_call = false; line_info_add(self, NULL, 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) { if (!self || self->num != friend_number) { return; } /* call is flagged active here */ self->is_call = true; init_infobox(self); line_info_add(self, NULL, 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) { if (!self || self->num != friend_number) { return; } self->is_call = false; kill_infobox(self); line_info_add(self, NULL, 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) { if (!self || self->num != friend_number) { return; } line_info_add(self, NULL, 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) { if (!self || self->num != friend_number) { return; } kill_infobox(self); line_info_add(self, NULL, 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; } (void) y2; memset(&ctx->infobox, 0, sizeof(struct infobox)); 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_treshold; 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); memset(&ctx->infobox, 0, sizeof(struct infobox)); } /* 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 elapsed time string once per second */ if (curtime > infobox->lastupdate) { 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_TTEE, ' ', 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'; char timefrmt[TIME_STR_SIZE]; get_time_str(timefrmt, sizeof(timefrmt)); int id = line_info_add(self, timefrmt, selfname, NULL, OUT_ACTION, 0, 0, "%s", action); cqueue_add(ctx->cqueue, action, strlen(action), OUT_ACTION, id); } static void 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); if (y2 <= 0 || x2 <= 0) { return; } if (ctx->pastemode && key == '\r') { key = '\n'; } if (self->help->active) { help_onKey(self, key); return; } if (ltr || key == '\n') { /* char is printable */ input_new_char(self, key, x, y, x2, y2); if (ctx->line[0] != '/' && !ctx->self_is_typing && statusbar->connection != TOX_CONNECTION_NONE) { set_self_typingstatus(self, m, 1); } return; } if (line_info_onKey(self, key)) { return; } input_handle(self, key, x, y, x2, y2); if (key == '\t' && ctx->len > 1 && ctx->line[0] == '/') { /* TAB key: auto-complete */ 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[3][8] = { {"online"}, {"away"}, {"busy"}, }; diff = complete_line(self, status_cmd_list, 3, 8); } else { diff = complete_line(self, chat_cmd_list, AC_NUM_CHAT_COMMANDS, MAX_CMDNAME_SIZE); } 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 == '\r') { 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] = {0}; 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; } 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 { 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'; char timefrmt[TIME_STR_SIZE]; get_time_str(timefrmt, sizeof(timefrmt)); int id = line_info_add(self, timefrmt, selfname, NULL, OUT_MSG, 0, 0, "%s", line); cqueue_add(ctx->cqueue, line, strlen(line), OUT_MSG, id); } } wclear(ctx->linewin); wmove(self->window, y2 - CURS_Y_OFFSET, 0); reset_buf(ctx); } if (ctx->len <= 0 && ctx->self_is_typing) { set_self_typingstatus(self, m, 0); } } static void chat_onDraw(ToxWindow *self, Tox *m) { int x2, 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); curs_set(1); if (ctx->len > 0) { mvwprintw(ctx->linewin, 1, 0, "%ls", &ctx->line[ctx->start]); } /* Draw status bar */ StatusBar *statusbar = self->stb; mvwhline(statusbar->topline, 1, 0, ACS_HLINE, x2); wmove(statusbar->topline, 0, 0); /* Draw name, status and note in statusbar */ if (statusbar->connection != TOX_CONNECTION_NONE) { int colour = MAGENTA; Tox_User_Status status = statusbar->status; 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(statusbar->topline, COLOR_PAIR(colour) | A_BOLD); wprintw(statusbar->topline, " %s", ONLINE_CHAR); wattroff(statusbar->topline, COLOR_PAIR(colour) | A_BOLD); if (Friends.list[self->num].is_typing) { wattron(statusbar->topline, COLOR_PAIR(YELLOW)); } wattron(statusbar->topline, A_BOLD); wprintw(statusbar->topline, " %s ", statusbar->nick); wattroff(statusbar->topline, A_BOLD); if (Friends.list[self->num].is_typing) { wattroff(statusbar->topline, COLOR_PAIR(YELLOW)); } } else { wprintw(statusbar->topline, " %s", OFFLINE_CHAR); wattron(statusbar->topline, A_BOLD); wprintw(statusbar->topline, " %s ", statusbar->nick); wattroff(statusbar->topline, A_BOLD); } /* 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); pthread_mutex_unlock(&Winthread.lock); filter_str(statusmsg, s_len); snprintf(statusbar->statusmsg, sizeof(statusbar->statusmsg), "%s", statusmsg); statusbar->statusmsg_len = strlen(statusbar->statusmsg); } self->x = x2; /* Truncate note if it doesn't fit in statusbar */ size_t maxlen = x2 - getcurx(statusbar->topline) - (KEY_IDENT_DIGITS * 2) - 6; if (statusbar->statusmsg_len > maxlen) { statusbar->statusmsg[maxlen - 3] = '\0'; strcat(statusbar->statusmsg, "..."); statusbar->statusmsg_len = maxlen; } if (statusbar->statusmsg[0]) { wprintw(statusbar->topline, ": %s ", statusbar->statusmsg); } wclrtoeol(statusbar->topline); wmove(statusbar->topline, 0, x2 - (KEY_IDENT_DIGITS * 2) - 3); wprintw(statusbar->topline, "{"); size_t i; for (i = 0; i < KEY_IDENT_DIGITS; ++i) { wprintw(statusbar->topline, "%02X", Friends.list[self->num].pub_key[i] & 0xff); } wprintw(statusbar->topline, "}\n"); mvwhline(self->window, y2 - CHATBOX_HEIGHT, 0, ACS_HLINE, x2); int y, x; getyx(self->window, y, x); (void) x; int new_x = ctx->start ? x2 - 1 : MAX(0, wcswidth(ctx->line, ctx->pos)); wmove(self->window, y + 1, new_x); 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); refresh_file_transfer_progress(self, m, self->num); pthread_mutex_unlock(&Winthread.lock); } static void chat_onInit(ToxWindow *self, Tox *m) { curs_set(1); int x2, 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); snprintf(statusbar->nick, sizeof(statusbar->nick), "%s", nick); statusbar->nick_len = n_len; /* Init subwindows */ ChatContext *ctx = self->chatwin; statusbar->topline = subwin(self->window, 2, x2, 0, 0); ctx->history = subwin(self->window, y2 - CHATBOX_HEIGHT + 1, x2, 0, 0); ctx->linewin = subwin(self->window, CHATBOX_HEIGHT, x2, y2 - CHATBOX_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); char myid[TOX_ADDRESS_SIZE]; tox_self_get_address(m, (uint8_t *) myid); int log_ret = log_enable(nick, myid, Friends.list[self->num].pub_key, ctx->log, LOG_CHAT); load_chat_history(self, ctx->log); if (!Friends.list[self->num].logging_on) { log_disable(ctx->log); } else if (log_ret == -1) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Warning: Log failed to initialize."); } execute(ctx->history, self, m, "/log", GLOBAL_COMMAND_MODE); 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->is_chat = true; 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->onGroupInvite = &chat_onGroupInvite; 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->device_selection[0] = ret->device_selection[1] = -1; ret->ringing_sound = -1; #endif /* AUDIO */ 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.8.3/src/chat.h000066400000000000000000000022051336615250000145270ustar00rootroot00000000000000/* 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 "windows.h" #include "toxic.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.8.3/src/chat_commands.c000066400000000000000000000235471336615250000164170ustar00rootroot00000000000000/* 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 "toxic.h" #include "windows.h" #include "misc_tools.h" #include "friendlist.h" #include "execute.h" #include "line_info.h" #include "groupchat.h" #include "chat.h" #include "file_transfers.h" extern ToxWindow *prompt; extern FriendsList Friends; void cmd_cancelfile(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { if (argc < 2) { line_info_add(self, NULL, 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, NULL, NULL, NULL, SYS_MSG, 0, 0, "Invalid file ID."); return; } struct 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, NULL, NULL, NULL, SYS_MSG, 0, 0, "Type must be 'in' or 'out'."); return; } if (!ft) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Invalid file ID."); return; } if (ft->state == FILE_TRANSFER_INACTIVE) { line_info_add(self, NULL, 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_groupinvite(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { if (argc < 1) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Group number required."); return; } long int groupnum = strtol(argv[1], NULL, 10); if ((groupnum == 0 && strcmp(argv[1], "0")) || groupnum < 0 || groupnum == LONG_MAX) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Invalid group number."); return; } Tox_Err_Conference_Invite err; if (!tox_conference_invite(m, self->num, groupnum, &err)) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to invite contact to group (error %d)", err); return; } line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Invited contact to Group %d.", groupnum); } void cmd_join_group(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { if (get_num_active_windows() >= MAX_WINDOWS_NUM) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, RED, " * Warning: Too many windows are open."); return; } const char *groupkey = Friends.list[self->num].group_invite.key; uint16_t length = Friends.list[self->num].group_invite.length; uint8_t type = Friends.list[self->num].group_invite.type; if (!Friends.list[self->num].group_invite.pending) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "No pending group chat invite."); return; } if (type != TOX_CONFERENCE_TYPE_TEXT) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Toxic does not support audio groups."); return; } Tox_Err_Conference_Join err; uint32_t groupnum = tox_conference_join(m, self->num, (const uint8_t *) groupkey, length, &err); if (err != TOX_ERR_CONFERENCE_JOIN_OK) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Group chat instance failed to initialize (error %d)", err); return; } if (init_groupchat_win(prompt, m, groupnum, type) == -1) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Group chat window failed to initialize."); tox_conference_delete(m, groupnum, NULL); return; } } void cmd_savefile(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { if (argc < 1) { line_info_add(self, NULL, 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, NULL, NULL, NULL, SYS_MSG, 0, 0, "No pending file transfers with that ID."); return; } struct FileTransfer *ft = get_file_transfer_struct_index(self->num, idx, FILE_TRANSFER_RECV); if (!ft) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "No pending file transfers with that ID."); return; } if (ft->state != FILE_TRANSFER_PENDING) { line_info_add(self, NULL, 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->filenum, TOX_FILE_CONTROL_RESUME, &err); if (err != TOX_ERR_FILE_CONTROL_OK) { goto on_recv_error; } line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Saving file [%d] as: '%s'", idx, ft->file_path); /* prep progress bar line */ char progline[MAX_STR_SIZE]; init_progress_bar(progline); line_info_add(self, NULL, 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, NULL, 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, NULL, 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, NULL, NULL, NULL, SYS_MSG, 0, 0, "File transfer failed: Invalid filenumber."); return; case TOX_ERR_FILE_CONTROL_SENDQ: line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "File transfer failed: Connection error."); return; default: line_info_add(self, NULL, 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]) { const char *errmsg = NULL; if (argc < 1) { line_info_add(self, NULL, 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, NULL, 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, NULL, NULL, NULL, SYS_MSG, 0, 0, "File not found."); return; } off_t filesize = file_size(path); if (filesize == 0) { line_info_add(self, NULL, 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; } struct 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, NULL, NULL, NULL, SYS_MSG, 0, 0, "Sending file [%d]: '%s' (%s)", filenum, file_name, sizestr); return; on_send_error: 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: errmsg = "File transfer failed: Friend is offline."; break; 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, NULL, NULL, NULL, SYS_MSG, 0, 0, "%s", errmsg); tox_file_control(m, self->num, filenum, TOX_FILE_CONTROL_CANCEL, NULL); fclose(file_to_send); } toxic-0.8.3/src/chat_commands.h000066400000000000000000000044461336615250000164210ustar00rootroot00000000000000/* 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 "windows.h" #include "toxic.h" void cmd_cancelfile(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_groupinvite(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_join_group(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_video(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_ccur_video_device(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); #endif /* VIDEO */ #endif /* CHAT_COMMANDS_H */ toxic-0.8.3/src/configdir.c000066400000000000000000000072171336615250000155570ustar00rootroot00000000000000/* 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 "toxic.h" #include "configdir.h" #include "misc_tools.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.8.3/src/configdir.h000066400000000000000000000026611336615250000155620ustar00rootroot00000000000000/* 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.8.3/src/curl_util.c000066400000000000000000000051661336615250000156160ustar00rootroot00000000000000/* 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.8.3/src/curl_util.h000066400000000000000000000047601336615250000156220ustar00rootroot00000000000000/* 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 /* 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.8.3/src/execute.c000066400000000000000000000161751336615250000152600ustar00rootroot00000000000000/* 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 "toxic.h" #include "windows.h" #include "execute.h" #include "chat_commands.h" #include "global_commands.h" #include "group_commands.h" #include "line_info.h" #include "misc_tools.h" #include "notify.h" #include "api.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 }, { "/group", cmd_groupchat }, { "/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_groupinvite }, { "/join", cmd_join_group }, { "/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 { "/video", cmd_video }, #endif /* VIDEO */ { NULL, NULL }, }; static struct cmd_func group_commands[] = { { "/title", cmd_set_title }, #ifdef AUDIO { "/mute", cmd_mute }, { "/sense", cmd_sense }, #endif /* AUDIO */ { NULL, NULL }, }; #ifdef PYTHON #define SPECIAL_COMMANDS 6 #else #define SPECIAL_COMMANDS 5 #endif /* PYTHON */ /* Special commands are commands that only take one argument even if it contains spaces */ static const char special_commands[SPECIAL_COMMANDS][MAX_CMDNAME_SIZE] = { "/avatar", "/nick", "/note", #ifdef PYTHON "/run", #endif /* PYTHON */ "/title", "/sendfile", }; /* 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; i < SPECIAL_COMMANDS; ++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(WINDOW *w, ToxWindow *self, 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(WINDOW *w, ToxWindow *self, const char *input, char (*args)[MAX_STR_SIZE]) { if (is_special_command(input)) { return parse_special_command(w, self, 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]) { int i; for (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(w, self, 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 GROUPCHAT_COMMAND_MODE: if (do_command(w, self, m, num_args, group_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, NULL, NULL, NULL, SYS_MSG, 0, 0, "Invalid command."); } toxic-0.8.3/src/execute.h000066400000000000000000000020561336615250000152560ustar00rootroot00000000000000/* 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, GROUPCHAT_COMMAND_MODE, }; void execute(WINDOW *w, ToxWindow *self, Tox *m, const char *input, int mode); #endif /* EXECUTE_H */ toxic-0.8.3/src/file_transfers.c000066400000000000000000000207411336615250000166160ustar00rootroot00000000000000/* 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 "toxic.h" #include "windows.h" #include "friendlist.h" #include "file_transfers.h" #include "line_info.h" #include "misc_tools.h" #include "notify.h" extern FriendsList Friends; /* number of "#"'s in file transfer progress bar. Keep well below MAX_STR_SIZE */ #define NUM_PROG_MARKS 50 /* 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[24]; snprintf(pct_str, sizeof(pct_str), "%.1f%%", pct_done); char bps_str[24]; bytes_convert_str(bps_str, sizeof(bps_str), bps); char prog_line[NUM_PROG_MARKS + 1] = {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, "-"); } char full_line[strlen(pct_str) + NUM_PROG_MARKS + strlen(bps_str) + 7]; snprintf(full_line, sizeof(full_line), "%s [%s] %s/s", pct_str, prog_line, bps_str); line_info_set(self, line_id, full_line); } static void refresh_progress_helper(ToxWindow *self, Tox *m, struct 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. */ void refresh_file_transfer_progress(ToxWindow *self, Tox *m, uint32_t friendnum) { size_t i; for (i = 0; i < MAX_FILES; ++i) { refresh_progress_helper(self, m, &Friends.list[friendnum].file_receiver[i]); refresh_progress_helper(self, m, &Friends.list[friendnum].file_sender[i]); } } /* Returns a pointer to friendnum's FileTransfer struct associated with filenum. * Returns NULL if filenum is invalid. */ struct FileTransfer *get_file_transfer_struct(uint32_t friendnum, uint32_t filenum) { size_t i; for (i = 0; i < MAX_FILES; ++i) { struct FileTransfer *ft_send = &Friends.list[friendnum].file_sender[i]; if (ft_send->state != FILE_TRANSFER_INACTIVE && ft_send->filenum == filenum) { return ft_send; } struct FileTransfer *ft_recv = &Friends.list[friendnum].file_receiver[i]; if (ft_recv->state != FILE_TRANSFER_INACTIVE && ft_recv->filenum == filenum) { return ft_recv; } } return NULL; } /* 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 friendnum, uint32_t index, FILE_TRANSFER_DIRECTION direction) { if (direction != FILE_TRANSFER_RECV && direction != FILE_TRANSFER_SEND) { return NULL; } size_t i; for (i = 0; i < MAX_FILES; ++i) { struct FileTransfer *ft = direction == FILE_TRANSFER_SEND ? &Friends.list[friendnum].file_sender[i] : &Friends.list[friendnum].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 struct FileTransfer *new_file_sender(ToxWindow *window, uint32_t friendnum, uint32_t filenum, uint8_t type) { size_t i; for (i = 0; i < MAX_FILES; ++i) { struct FileTransfer *ft = &Friends.list[friendnum].file_sender[i]; if (ft->state == FILE_TRANSFER_INACTIVE) { memset(ft, 0, sizeof(struct FileTransfer)); ft->window = window; ft->index = i; ft->friendnum = friendnum; ft->filenum = filenum; ft->file_type = type; ft->last_keep_alive = get_unix_time(); ft->state = FILE_TRANSFER_PENDING; ft->direction = FILE_TRANSFER_SEND; return ft; } } return NULL; } /* Returns a pointer to an unused file receiver. * Returns NULL if all file receivers are in use. */ static struct FileTransfer *new_file_receiver(ToxWindow *window, uint32_t friendnum, uint32_t filenum, uint8_t type) { size_t i; for (i = 0; i < MAX_FILES; ++i) { struct FileTransfer *ft = &Friends.list[friendnum].file_receiver[i]; if (ft->state == FILE_TRANSFER_INACTIVE) { memset(ft, 0, sizeof(struct FileTransfer)); ft->window = window; ft->index = i; ft->friendnum = friendnum; ft->filenum = filenum; ft->file_type = type; ft->last_keep_alive = get_unix_time(); ft->state = FILE_TRANSFER_PENDING; ft->direction = FILE_TRANSFER_RECV; return ft; } } return NULL; } /* Initializes an unused file transfer and returns its pointer. * Returns NULL on failure. */ struct FileTransfer *new_file_transfer(ToxWindow *window, uint32_t friendnum, uint32_t filenum, FILE_TRANSFER_DIRECTION direction, uint8_t type) { if (direction == FILE_TRANSFER_RECV) { return new_file_receiver(window, friendnum, filenum, type); } if (direction == FILE_TRANSFER_SEND) { return new_file_sender(window, friendnum, filenum, type); } return NULL; } /* 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) { if (!ft) { return; } if (ft->state == FILE_TRANSFER_INACTIVE) { return; } if (ft->file) { fclose(ft->file); } if (CTRL >= 0) { tox_file_control(m, ft->friendnum, ft->filenum, (Tox_File_Control) CTRL, NULL); } 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, NULL, NULL, NULL, SYS_MSG, 0, 0, "%s", message); } memset(ft, 0, sizeof(struct FileTransfer)); } /* Kills all active file transfers for friendnum */ void kill_all_file_transfers_friend(Tox *m, uint32_t friendnum) { size_t i; for (i = 0; i < MAX_FILES; ++i) { close_file_transfer(NULL, m, &Friends.list[friendnum].file_sender[i], TOX_FILE_CONTROL_CANCEL, NULL, silent); close_file_transfer(NULL, m, &Friends.list[friendnum].file_receiver[i], TOX_FILE_CONTROL_CANCEL, NULL, silent); } } void kill_all_file_transfers(Tox *m) { size_t i; for (i = 0; i < Friends.max_idx; ++i) { kill_all_file_transfers_friend(m, Friends.list[i].num); } } toxic-0.8.3/src/file_transfers.h000066400000000000000000000070451336615250000166250ustar00rootroot00000000000000/* 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 "toxic.h" #include "windows.h" #include "notify.h" #define KiB 1024 #define MiB 1048576 /* 1024^2 */ #define GiB 1073741824 /* 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; struct FileTransfer { ToxWindow *window; FILE *file; FILE_TRANSFER_STATE state; FILE_TRANSFER_DIRECTION direction; 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 filenum; uint32_t friendnum; size_t index; uint64_t file_size; uint64_t position; time_t last_line_progress; /* The last time we updated the progress bar */ time_t last_keep_alive; /* The last time we sent or received data */ uint32_t line_id; uint8_t file_id[TOX_FILE_ID_LENGTH]; }; /* 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. */ void refresh_file_transfer_progress(ToxWindow *self, Tox *m, uint32_t friendnum); /* Returns a pointer to friendnum's FileTransfer struct associated with filenum. * Returns NULL if filenum is invalid. */ struct FileTransfer *get_file_transfer_struct(uint32_t friendnum, uint32_t filenum); /* 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 friendnum, 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 friendnum, uint32_t filenum, FILE_TRANSFER_DIRECTION direction, uint8_t type); /* 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 all active file transfers for friendnum */ void kill_all_file_transfers_friend(Tox *m, uint32_t friendnum); void kill_all_file_transfers(Tox *m); #endif /* FILE_TRANSFERS_H */ toxic-0.8.3/src/friendlist.c000066400000000000000000001077221336615250000157600ustar00rootroot00000000000000/* 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 "toxic.h" #include "windows.h" #include "chat.h" #include "friendlist.h" #include "misc_tools.h" #include "line_info.h" #include "settings.h" #include "notify.h" #include "help.h" #include "log.h" #include "avatars.h" #ifdef AUDIO #include "audio_call.h" #endif extern char *DATA_FILE; extern char *BLOCK_FILE; extern ToxWindow *prompt; extern struct Winthread Winthread; extern struct user_settings *user_settings; extern struct arg_opts arg_opts; 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 && Friends.list[i].group_invite.key != NULL) { free(Friends.list[i].group_invite.key); } } realloc_blocklist(0); realloc_friends(0); free(self->help); del_window(self); } /* 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; memset(&tmp, 0, sizeof(BlockedFriend)); 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; } char temp_path[strlen(path) + strlen(TEMP_BLOCKLIST_EXT) + 1]; snprintf(temp_path, sizeof(temp_path), "%s%s", path, TEMP_BLOCKLIST_EXT); FILE *fp = fopen(temp_path, "wb"); if (fp == NULL) { free(data); return -1; } if (fwrite(data, len, 1, fp) != 1) { fprintf(stderr, "Failed to write blocklist data.\n"); fclose(fp); free(data); return -1; } fclose(fp); free(data); if (rename(temp_path, path) != 0) { return -1; } 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[len]; if (fread(data, len, 1, fp) != 1) { fclose(fp); return -1; } if (len % sizeof(BlockedFriend) != 0) { fclose(fp); return -1; } int num = len / sizeof(BlockedFriend); Blocked.max_idx = num; realloc_blocklist(num); int i; for (i = 0; i < num; ++i) { BlockedFriend tmp; memset(&tmp, 0, sizeof(BlockedFriend)); memset(&Blocked.list[i], 0, sizeof(BlockedFriend)); 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); 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[*(int *) n1].name, Friends.list[*(int *) n2].name); /* Use weight to make qsort always put online friends before offline */ res = Friends.list[*(int *) n1].connection_status ? (res - S_WEIGHT) : (res + S_WEIGHT); res = Friends.list[*(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; } } 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[*(int *) n1].name, Blocked.list[*(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) { 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); char timefrmt[TIME_STR_SIZE]; get_time_str(timefrmt, sizeof(timefrmt)); line_info_add(prompt, timefrmt, nick, NULL, IN_MSG, 0, 0, "%s", str); line_info_add(prompt, NULL, 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) { 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 %d\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) { 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) { rename_logfile(oldname, newnamecpy, myid, Friends.list[num].pub_key, Friends.list[num].chatwin); } sort_friendlist_index(); } static void friendlist_onStatusChange(ToxWindow *self, Tox *m, uint32_t num, Tox_User_Status status) { 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) { 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) { realloc_friends(Friends.max_idx + 1); memset(&Friends.list[Friends.max_idx], 0, sizeof(ToxicFriend)); 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] = {0}; get_nick_truncate(m, tempname, num); snprintf(Friends.list[i].name, sizeof(Friends.list[i].name), "%s", tempname); Friends.list[i].namelength = strlen(Friends.list[i].name); 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(Tox *m, uint32_t fnum, uint32_t bnum) { realloc_friends(Friends.max_idx + 1); memset(&Friends.list[Friends.max_idx], 0, sizeof(ToxicFriend)); 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; } } 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) { 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, NULL, 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_onGroupInvite(ToxWindow *self, Tox *m, int32_t num, uint8_t type, const char *group_pub_key, uint16_t 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, NULL, NULL, NULL, SYS_MSG, 0, RED, "* Group 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(ToxWindow *self, 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) { 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].group_invite.key != NULL) { free(Friends.list[f_num].group_invite.key); } memset(&Friends.list[f_num], 0, sizeof(ToxicFriend)); 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(ToxWindow *self, Tox *m, 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(ToxWindow *self, Tox *m, wint_t key) { if (key == '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); memset(&PendingDelete, 0, sizeof(PendingDelete)); 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) { memset(&Blocked.list[bnum], 0, sizeof(BlockedFriend)); 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); memset(&Blocked.list[Blocked.max_idx], 0, sizeof(BlockedFriend)); int i; for (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, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to unblock friend (error %d)", err); return; } friendlist_add_blocked(m, friendnum, bnum); delete_blocked_friend(bnum); sort_blocklist_index(); sort_friendlist_index(); } static void friendlist_onKey(ToxWindow *self, Tox *m, wint_t key, bool ltr) { if (self->help->active) { help_onKey(self, key); return; } if (key == 'h') { help_init_menu(self); return; } if (!blocklist_view && !Friends.num_friends && (key != KEY_RIGHT && key != KEY_LEFT)) { return; } if (blocklist_view && !Blocked.num_blocked && (key != KEY_RIGHT && key != KEY_LEFT)) { return; } 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 == 'y' || key == 'n') { del_friend_deactivate(self, m, key); } return; } if (key == ltr) { return; } switch (key) { case '\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, NULL, NULL, NULL, SYS_MSG, 0, RED, msg); sound_notify(prompt, notif_error, NT_WNDALERT_1, NULL); } break; case KEY_DC: del_friend_activate(self, m, f); break; case '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(self, key, &Friends.num_selected, Friends.num_friends); } else { select_friend(self, key, &Blocked.num_selected, Blocked.num_blocked); } break; } } #define FLIST_OFST 6 /* Accounts for space at top and bottom */ static void blocklist_onDraw(ToxWindow *self, Tox *m, int y2, int x2) { 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, "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)); 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, "%d/%d \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); int page = Friends.num_selected / (y2 - FLIST_OFST); pthread_mutex_unlock(&Winthread.lock); int start = (y2 - FLIST_OFST) * page; int end = y2 - FLIST_OFST + start; pthread_mutex_lock(&Winthread.lock); 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); bool f_selected = false; if (is_active) { 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, "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 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) { assert(0); if (friend_number >= Friends.max_idx) { return; } assert(0); 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, NULL, NULL, NULL, SYS_MSG, 0, 0, "Audio action from: %s!", nick); const char *errmsg = "* Warning: Too many windows are open."; line_info_add(prompt, NULL, 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; } ToxWindow *new_friendlist(void) { ToxWindow *ret = calloc(1, sizeof(ToxWindow)); if (ret == NULL) { exit_toxic_err("failed in new_friendlist", FATALERR_MEMORY); } ret->is_friendlist = true; 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->onGroupInvite = &friendlist_onGroupInvite; #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; ret->device_selection[0] = ret->device_selection[1] = -1; #endif /* AUDIO */ 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.8.3/src/friendlist.h000066400000000000000000000052431336615250000157600ustar00rootroot00000000000000/* 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 "toxic.h" #include "windows.h" #include "file_transfers.h" 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 GroupChatInvite { char *key; uint16_t length; uint8_t type; bool pending; }; 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 GroupChatInvite group_invite; struct FileTransfer file_receiver[MAX_FILES]; struct FileTransfer file_sender[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; ToxWindow *new_friendlist(void); 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); #endif /* end of include guard: FRIENDLIST_H */ toxic-0.8.3/src/global_commands.c000066400000000000000000000461701336615250000167350ustar00rootroot00000000000000/* 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 "toxic.h" #include "windows.h" #include "misc_tools.h" #include "friendlist.h" #include "log.h" #include "line_info.h" #include "groupchat.h" #include "prompt.h" #include "help.h" #include "term_mplex.h" #include "avatars.h" #include "name_lookup.h" #include "qr_code.h" #include "toxic_strings.h" extern char *DATA_FILE; extern ToxWindow *prompt; extern FriendsList Friends; /* command functions */ void cmd_accept(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { if (argc < 1) { line_info_add(self, NULL, 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, NULL, NULL, NULL, SYS_MSG, 0, 0, "No pending friend request with that ID."); return; } if (!FrndRequests.request[req].active) { line_info_add(self, NULL, 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, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to add friend (error %d\n)", err); return; } else { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Friend request accepted."); on_friend_added(m, friendnum, true); } memset(&FrndRequests.request[req], 0, sizeof(struct friend_request)); 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 = "Faile to add friend: Unknown error."; break; } line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, errmsg); } void cmd_add(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { if (argc < 1) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Tox ID or address required."); return; } const char *id = argv[1]; char msg[MAX_STR_SIZE]; if (argc > 1) { if (argv[2][0] != '\"') { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Message must be enclosed in quotes."); return; } /* remove opening and closing quotes */ char tmp[MAX_STR_SIZE]; snprintf(tmp, sizeof(tmp), "%s", &argv[2][1]); int len = strlen(tmp) - 1; tmp[len] = '\0'; snprintf(msg, sizeof(msg), "%s", tmp); } 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}; uint16_t id_len = (uint16_t) strlen(id); /* try to add tox ID */ if (id_len == 2 * TOX_ADDRESS_SIZE) { size_t i; char xx[3]; uint32_t x; for (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, NULL, NULL, NULL, SYS_MSG, 0, 0, "Invalid Tox ID."); return; } id_bin[i] = x; } cmd_add_helper(self, m, id_bin, msg); } else { /* assume id is a username@domain address and do http name server lookup */ name_lookup(self, m, id_bin, id, msg); } } void cmd_avatar(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { if (argc != 1 || strlen(argv[1]) < 3) { avatar_unset(m); line_info_add(self, NULL, 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, NULL, 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, NULL, 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, NULL, 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]) { 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]) { if (argc != 3) { line_info_add(self, NULL, 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, NULL, NULL, NULL, SYS_MSG, 0, 0, "Invalid port."); return; } char key_binary[TOX_PUBLIC_KEY_SIZE * 2 + 1]; if (hex_string_to_bin(ascii_key, strlen(ascii_key), key_binary, TOX_PUBLIC_KEY_SIZE) == -1) { line_info_add(self, NULL, 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, NULL, NULL, NULL, SYS_MSG, 0, 0, "Bootstrap failed: Invalid IP."); break; case TOX_ERR_BOOTSTRAP_BAD_PORT: line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Bootstrap failed: Invalid port."); break; case TOX_ERR_BOOTSTRAP_NULL: line_info_add(self, NULL, 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]) { if (argc < 1) { line_info_add(self, NULL, 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, NULL, NULL, NULL, SYS_MSG, 0, 0, "No pending friend request with that ID."); return; } if (!FrndRequests.request[req].active) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "No pending friend request with that ID."); return; } memset(&FrndRequests.request[req], 0, sizeof(struct friend_request)); 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_groupchat(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { if (get_num_active_windows() >= MAX_WINDOWS_NUM) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, RED, " * Warning: Too many windows are open."); return; } if (argc < 1) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Please specify group 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, NULL, NULL, NULL, SYS_MSG, 0, 0, "Valid group types are: text | audio"); return; } if (type != TOX_CONFERENCE_TYPE_TEXT) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Toxic does not support audio groups."); return; } Tox_Err_Conference_New err; uint32_t groupnum = tox_conference_new(m, &err); if (err != TOX_ERR_CONFERENCE_NEW_OK) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Group chat instance failed to initialize (error %d)", err); return; } if (init_groupchat_win(prompt, m, groupnum, type) == -1) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Group chat window failed to initialize."); tox_conference_delete(m, groupnum, NULL); return; } line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Group chat [%d] created.", groupnum); } void cmd_log(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { 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, NULL, NULL, NULL, SYS_MSG, 0, 0, msg); return; } const char *swch = argv[1]; if (!strcmp(swch, "1") || !strcmp(swch, "on")) { char myid[TOX_ADDRESS_SIZE]; tox_self_get_address(m, (uint8_t *) myid); int log_ret = -1; if (self->is_chat) { Friends.list[self->num].logging_on = true; log_ret = log_enable(self->name, myid, Friends.list[self->num].pub_key, log, LOG_CHAT); } else if (self->is_prompt) { log_ret = log_enable(self->name, myid, NULL, log, LOG_PROMPT); } else if (self->is_groupchat) { log_ret = log_enable(self->name, myid, NULL, log, LOG_GROUP); } msg = log_ret == 0 ? "Logging enabled." : "Warning: Log failed to initialize."; line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, msg); return; } else if (!strcmp(swch, "0") || !strcmp(swch, "off")) { if (self->is_chat) { Friends.list[self->num].logging_on = false; } log_disable(log); msg = "Logging disabled."; line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, msg); return; } msg = "Invalid option. Use \"/log on\" and \"/log off\" to toggle logging."; line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, msg); } void cmd_myid(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { char id_string[TOX_ADDRESS_SIZE * 2 + 1]; char bin_id[TOX_ADDRESS_SIZE]; tox_self_get_address(m, (uint8_t *) bin_id); if (bin_id_to_string(bin_id, sizeof(bin_id), id_string, sizeof(id_string)) == -1) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to print ID."); return; } line_info_add(self, NULL, 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]) { char id_string[TOX_ADDRESS_SIZE * 2 + 1]; char bin_id[TOX_ADDRESS_SIZE]; tox_self_get_address(m, (uint8_t *) bin_id); if (bin_id_to_string(bin_id, sizeof(bin_id), id_string, sizeof(id_string)) == -1) { line_info_add(self, NULL, 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[data_file_len + 1]; size_t dir_len = get_base_dir(DATA_FILE, data_file_len, dir); #ifdef QRPNG if (argc == 0) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Required 'txt' or 'png'"); return; } else if (!strcmp(argv[1], "txt")) { #endif /* QRPNG */ char qr_path[dir_len + nick_len + strlen(QRCODE_FILENAME_EXT) + 1]; snprintf(qr_path, sizeof(qr_path), "%s%s%s", dir, nick, QRCODE_FILENAME_EXT); if (ID_to_QRcode_txt(id_string, qr_path) == -1) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to create QR code."); return; } line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "QR code has been printed to the file '%s'", qr_path); #ifdef QRPNG } else if (!strcmp(argv[1], "png")) { char qr_path[dir_len + nick_len + strlen(QRCODE_FILENAME_EXT_PNG) + 1]; snprintf(qr_path, sizeof(qr_path), "%s%s%s", dir, nick, QRCODE_FILENAME_EXT_PNG); if (ID_to_QRcode_png(id_string, qr_path) == -1) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to create QR code."); return; } line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "QR code has been printed to the file '%s'", qr_path); } else { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Unknown option '%s' -- Required 'txt' or 'png'", argv[1]); return; } #endif /* QRPNG */ } #endif /* QRCODE */ void cmd_nick(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { if (argc < 1) { line_info_add(self, NULL, 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, NULL, 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]) { if (argc < 1) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Input required."); return; } prompt_update_statusmessage(prompt, m, argv[1]); } 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, NULL, 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, NULL, NULL, NULL, SYS_MSG, 0, 0, "Your new Tox ID is:"); cmd_myid(window, self, m, 0, NULL); line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, ""); line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Any services that relied on your old ID will need to be updated manually."); line_info_add(self, NULL, 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]) { help_init_menu(self); } void cmd_quit(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { exit_toxic_success(m); } void cmd_requests(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { if (FrndRequests.num_requests == 0) { line_info_add(self, NULL, 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, NULL, NULL, NULL, SYS_MSG, 0, 0, "%d : %s", i, id); line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "%s", FrndRequests.request[i].msg); if (++count < FrndRequests.num_requests) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, ""); } } } void cmd_status(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { const char *errmsg; lock_status(); if (argc < 1) { errmsg = "Require a status. Statuses are: online, busy and away."; line_info_add(self, NULL, 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, NULL, NULL, NULL, SYS_MSG, 0, 0, errmsg); goto finish; } tox_self_set_status(m, status); prompt_update_status(prompt, status); line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Your status has been changed to %s.", status_str); finish: unlock_status(); } toxic-0.8.3/src/global_commands.h000066400000000000000000000057501336615250000167410ustar00rootroot00000000000000/* 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 "windows.h" #include "toxic.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_groupchat(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 */ #endif /* GLOBAL_COMMANDS_H */ toxic-0.8.3/src/group_commands.c000066400000000000000000000047111336615250000166240ustar00rootroot00000000000000/* group_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 "toxic.h" #include "windows.h" #include "line_info.h" #include "misc_tools.h" #include "log.h" void cmd_set_title(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { Tox_Err_Conference_Title err; char title[MAX_STR_SIZE]; if (argc < 1) { size_t tlen = tox_conference_get_title_size(m, self->num, &err); if (err != TOX_ERR_CONFERENCE_TITLE_OK) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Title is not set"); return; } if (!tox_conference_get_title(m, self->num, (uint8_t *) title, &err)) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Title is not set"); return; } title[tlen] = '\0'; line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Title is set to: %s", title); return; } snprintf(title, sizeof(title), "%s", argv[1]); int len = strlen(title); if (!tox_conference_set_title(m, self->num, (uint8_t *) title, len, &err)) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to set title (error %d)", err); return; } set_window_title(self, title, len); char timefrmt[TIME_STR_SIZE]; char selfnick[TOX_MAX_NAME_LENGTH]; get_time_str(timefrmt, sizeof(timefrmt)); 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, timefrmt, selfnick, NULL, NAME_CHANGE, 0, 0, " set the group 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, selfnick, self->chatwin->log, true); } toxic-0.8.3/src/group_commands.h000066400000000000000000000017241336615250000166320ustar00rootroot00000000000000/* group_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 GROUP_COMMANDS_H #define GROUP_COMMANDS_H #include "windows.h" #include "toxic.h" void cmd_set_title(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); #endif /* GROUP_COMMANDS_H */ toxic-0.8.3/src/groupchat.c000066400000000000000000000501651336615250000156070ustar00rootroot00000000000000/* groupchat.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 #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 "windows.h" #include "toxic.h" #include "execute.h" #include "misc_tools.h" #include "groupchat.h" #include "prompt.h" #include "toxic_strings.h" #include "log.h" #include "line_info.h" #include "settings.h" #include "input.h" #include "help.h" #include "notify.h" #include "autocomplete.h" #include "audio_device.h" extern char *DATA_FILE; static GroupChat groupchats[MAX_GROUPCHAT_NUM]; static int max_groupchat_index = 0; extern struct user_settings *user_settings; extern struct Winthread Winthread; #ifdef PYTHON #define AC_NUM_GROUP_COMMANDS_PYTHON 1 #else #define AC_NUM_GROUP_COMMANDS_PYTHON 0 #endif /* PYTHON */ #ifdef QRCODE #define AC_NUM_GROUP_COMMANDS_QRCODE 1 #else #define AC_NUM_GROUP_COMMANDS_QRCODE 0 #endif /* QRCODE */ #define AC_NUM_GROUP_COMMANDS (19 + AC_NUM_GROUP_COMMANDS_PYTHON + AC_NUM_GROUP_COMMANDS_QRCODE) /* Array of groupchat command names used for tab completion. */ static const char group_cmd_list[AC_NUM_GROUP_COMMANDS][MAX_CMDNAME_SIZE] = { { "/accept" }, { "/add" }, { "/avatar" }, { "/clear" }, { "/close" }, { "/connect" }, { "/decline" }, { "/exit" }, { "/group" }, { "/help" }, { "/log" }, { "/myid" }, #ifdef QRCODE { "/myqr" }, #endif /* QRCODE */ { "/nick" }, { "/note" }, { "/nospam" }, { "/quit" }, { "/requests" }, { "/status" }, { "/title" }, #ifdef PYTHON { "/run" }, #endif /* PYTHON */ }; static void kill_groupchat_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); del_window(self); } int init_groupchat_win(ToxWindow *prompt, Tox *m, uint32_t groupnum, uint8_t type) { if (groupnum > MAX_GROUPCHAT_NUM) { return -1; } ToxWindow *self = new_group_chat(m, groupnum); for (int i = 0; i <= max_groupchat_index; ++i) { if (!groupchats[i].active) { groupchats[i].chatwin = add_window(m, self); groupchats[i].active = true; groupchats[i].num_peers = 0; groupchats[i].type = type; groupchats[i].start_time = get_unix_time(); set_active_window_index(groupchats[i].chatwin); if (i == max_groupchat_index) { ++max_groupchat_index; } return 0; } } kill_groupchat_window(self); return -1; } void free_groupchat(ToxWindow *self, Tox *m, uint32_t groupnum) { free(groupchats[groupnum].name_list); free(groupchats[groupnum].peer_list); memset(&groupchats[groupnum], 0, sizeof(GroupChat)); int i; for (i = max_groupchat_index; i > 0; --i) { if (groupchats[i - 1].active) { break; } } max_groupchat_index = i; kill_groupchat_window(self); } static void delete_groupchat(ToxWindow *self, Tox *m, uint32_t groupnum) { tox_conference_delete(m, groupnum, NULL); free_groupchat(self, m, groupnum); } /* destroys and re-creates groupchat window with or without the peerlist */ void redraw_groupchat_win(ToxWindow *self) { ChatContext *ctx = self->chatwin; endwin(); refresh(); clear(); int x2, y2; getmaxyx(stdscr, y2, x2); y2 -= 2; if (y2 <= 0 || x2 <= 0) { return; } if (ctx->sidebar) { delwin(ctx->sidebar); ctx->sidebar = NULL; } delwin(ctx->linewin); delwin(ctx->history); delwin(self->window); self->window = newwin(y2, x2, 0, 0); ctx->linewin = subwin(self->window, CHATBOX_HEIGHT, x2, y2 - CHATBOX_HEIGHT, 0); if (self->show_peerlist) { ctx->history = subwin(self->window, y2 - CHATBOX_HEIGHT + 1, x2 - SIDEBAR_WIDTH - 1, 0, 0); ctx->sidebar = subwin(self->window, y2 - CHATBOX_HEIGHT + 1, SIDEBAR_WIDTH, 0, x2 - SIDEBAR_WIDTH); } else { ctx->history = subwin(self->window, y2 - CHATBOX_HEIGHT + 1, x2, 0, 0); } scrollok(ctx->history, 0); } static void groupchat_onGroupMessage(ToxWindow *self, Tox *m, uint32_t groupnum, uint32_t peernum, Tox_Message_Type type, const char *msg, size_t len) { if (self->num != groupnum) { return; } ChatContext *ctx = self->chatwin; char nick[TOX_MAX_NAME_LENGTH]; get_group_nick_truncate(m, nick, peernum, groupnum); 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)) { sound_notify(self, generic_message, NT_WNDALERT_0 | user_settings->bell_on_message, NULL); if (self->active_box != -1) { box_silent_notify2(self, NT_NOFOCUS, self->active_box, "%s %s", nick, msg); } else { box_silent_notify(self, NT_NOFOCUS, &self->active_box, self->name, "%s %s", nick, msg); } nick_clr = RED; } else { sound_notify(self, silent, NT_WNDALERT_1, NULL); } char timefrmt[TIME_STR_SIZE]; get_time_str(timefrmt, sizeof(timefrmt)); line_info_add(self, timefrmt, 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 groupchat_onGroupTitleChange(ToxWindow *self, Tox *m, uint32_t groupnum, uint32_t peernum, const char *title, size_t length) { ChatContext *ctx = self->chatwin; if (self->num != groupnum) { return; } set_window_title(self, title, length); char timefrmt[TIME_STR_SIZE]; get_time_str(timefrmt, sizeof(timefrmt)); /* don't announce title when we join the room */ if (!timed_out(groupchats[self->num].start_time, GROUP_EVENT_WAIT)) { return; } char nick[TOX_MAX_NAME_LENGTH]; get_group_nick_truncate(m, nick, peernum, groupnum); line_info_add(self, timefrmt, nick, NULL, NAME_CHANGE, 0, 0, " set the group 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); } static void group_update_name_list(uint32_t groupnum) { GroupChat *chat = &groupchats[groupnum]; if (!chat) { return; } if (chat->name_list) { free(chat->name_list); } chat->name_list = malloc(sizeof(char *) * chat->num_peers * TOX_MAX_NAME_LENGTH); if (chat->name_list == NULL) { exit_toxic_err("failed in group_update_name_list", FATALERR_MEMORY); } uint32_t i, count = 0; for (i = 0; i < chat->max_idx; ++i) { if (chat->peer_list[i].active) { memcpy(&chat->name_list[count * TOX_MAX_NAME_LENGTH], chat->peer_list[i].name, chat->peer_list[i].name_length + 1); ++count; } } qsort(chat->name_list, count, TOX_MAX_NAME_LENGTH, qsort_strcasecmp_hlpr); } /* Reallocates groupnum's peer list. Increase is true if the list needs to grow. * * Returns 0 on success. * Returns -1 on failure. */ static int realloc_peer_list(GroupChat *chat, uint32_t num_peers) { if (!chat) { return -1; } if (num_peers == 0) { free(chat->peer_list); chat->peer_list = NULL; return 0; } struct GroupPeer *tmp_list = realloc(chat->peer_list, num_peers * sizeof(struct GroupPeer)); if (!tmp_list) { return -1; } chat->peer_list = tmp_list; return 0; } static void update_peer_list(Tox *m, uint32_t groupnum, uint32_t num_peers) { GroupChat *chat = &groupchats[groupnum]; if (!chat) { return; } realloc_peer_list(chat, num_peers); uint32_t i; for (i = 0; i < num_peers; ++i) { GroupPeer *peer = &chat->peer_list[i]; Tox_Err_Conference_Peer_Query err; size_t length = tox_conference_peer_get_name_size(m, groupnum, i, &err); if (err != TOX_ERR_CONFERENCE_PEER_QUERY_OK || length >= TOX_MAX_NAME_LENGTH) { continue; } tox_conference_peer_get_name(m, groupnum, 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->peernumber = i; } group_update_name_list(groupnum); } static void groupchat_onGroupNameListChange(ToxWindow *self, Tox *m, uint32_t groupnum) { if (self->num != groupnum) { return; } if (groupnum > max_groupchat_index) { return; } GroupChat *chat = &groupchats[groupnum]; Tox_Err_Conference_Peer_Query err; uint32_t num_peers = tox_conference_peer_count(m, groupnum, &err); uint32_t old_num = chat->num_peers; if (err == TOX_ERR_CONFERENCE_PEER_QUERY_OK) { chat->num_peers = num_peers; } else { num_peers = old_num; } chat->max_idx = num_peers; update_peer_list(m, groupnum, num_peers); } static void groupchat_onGroupPeerNameChange(ToxWindow *self, Tox *m, uint32_t groupnum, uint32_t peernum, const char *name, size_t length) { if (self->num != groupnum) { return; } GroupChat *chat = &groupchats[groupnum]; if (!chat) { return; } size_t i; for (i = 0; i < chat->max_idx; ++i) { GroupPeer *peer = &chat->peer_list[i]; // Test against default tox name to prevent nick change spam on initial join (TODO: this is disgusting) if (peer->active && peer->peernumber == peernum && peer->name_length > 0) { ChatContext *ctx = self->chatwin; char timefrmt[TIME_STR_SIZE]; get_time_str(timefrmt, sizeof(timefrmt)); char tmp_event[TOXIC_MAX_NAME_LENGTH * 2 + 32]; snprintf(tmp_event, sizeof(tmp_event), "is now known as %s", (const char *) name); write_to_log(tmp_event, peer->name, ctx->log, true); line_info_add(self, timefrmt, peer->name, (const char *) name, NAME_CHANGE, 0, 0, " is now known as "); break; } } groupchat_onGroupNameListChange(self, m, groupnum); } static void send_group_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, NULL, NULL, NULL, SYS_MSG, 0, RED, " * Failed to send action (error %d)", err); } } static void groupchat_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); if (x2 <= 0 || y2 <= 0) { return; } if (self->help->active) { help_onKey(self, key); return; } if (ctx->pastemode && key == '\r') { key = '\n'; } if (ltr || key == '\n') { /* char is printable */ input_new_char(self, key, x, y, x2, y2); return; } if (line_info_onKey(self, key)) { return; } if (input_handle(self, key, x, y, x2, y2)) { return; } if (key == '\t') { /* TAB key: auto-completes peer name or command */ if (ctx->len > 0) { int diff; /* TODO: make this not suck */ if (ctx->line[0] != L'/' || wcscmp(ctx->line, L"/me") == 0) { diff = complete_line(self, groupchats[self->num].name_list, groupchats[self->num].num_peers, TOX_MAX_NAME_LENGTH); } 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 { diff = complete_line(self, group_cmd_list, AC_NUM_GROUP_COMMANDS, MAX_CMDNAME_SIZE); } 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 == user_settings->key_peer_list_down) { /* Scroll peerlist up and down one position */ int L = y2 - CHATBOX_HEIGHT - SDBAR_OFST; if (groupchats[self->num].side_pos < groupchats[self->num].num_peers - L) { ++groupchats[self->num].side_pos; } } else if (key == user_settings->key_peer_list_up) { if (groupchats[self->num].side_pos > 0) { --groupchats[self->num].side_pos; } } else if (key == '\r') { 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_groupchat(self, m, self->num); return; } else if (strncmp(line, "/me ", strlen("/me ")) == 0) { send_group_action(self, ctx, m, line + strlen("/me ")); } else { execute(ctx->history, self, m, line, GROUPCHAT_COMMAND_MODE); } } else { 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, NULL, NULL, NULL, SYS_MSG, 0, RED, " * Failed to send message (error %d)", err); } } } wclear(ctx->linewin); wmove(self->window, y2 - CURS_Y_OFFSET, 0); reset_buf(ctx); } } static void groupchat_onDraw(ToxWindow *self, Tox *m) { int x2, y2; getmaxyx(self->window, y2, x2); if (x2 <= 0 || y2 <= 0) { 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, 1, 0, "%ls", &ctx->line[ctx->start]); } wclear(ctx->sidebar); mvwhline(self->window, y2 - CHATBOX_HEIGHT, 0, ACS_HLINE, x2); if (self->show_peerlist) { mvwvline(ctx->sidebar, 0, 0, ACS_VLINE, y2 - CHATBOX_HEIGHT); mvwaddch(ctx->sidebar, y2 - CHATBOX_HEIGHT, 0, ACS_BTEE); pthread_mutex_lock(&Winthread.lock); int num_peers = groupchats[self->num].num_peers; pthread_mutex_unlock(&Winthread.lock); wmove(ctx->sidebar, 0, 1); wattron(ctx->sidebar, A_BOLD); wprintw(ctx->sidebar, "Peers: %d\n", num_peers); wattroff(ctx->sidebar, A_BOLD); mvwaddch(ctx->sidebar, 1, 0, ACS_LTEE); mvwhline(ctx->sidebar, 1, 1, ACS_HLINE, SIDEBAR_WIDTH - 1); int maxlines = y2 - SDBAR_OFST - CHATBOX_HEIGHT; int i; for (i = 0; i < num_peers && i < maxlines; ++i) { wmove(ctx->sidebar, i + 2, 1); pthread_mutex_lock(&Winthread.lock); uint32_t peer = i + groupchats[self->num].side_pos; pthread_mutex_unlock(&Winthread.lock); /* truncate nick to fit in side panel without modifying list */ char tmpnck[TOX_MAX_NAME_LENGTH]; int maxlen = SIDEBAR_WIDTH - 2; pthread_mutex_lock(&Winthread.lock); memcpy(tmpnck, &groupchats[self->num].name_list[peer * TOX_MAX_NAME_LENGTH], maxlen); pthread_mutex_unlock(&Winthread.lock); tmpnck[maxlen] = '\0'; wprintw(ctx->sidebar, "%s\n", tmpnck); } } int y, x; getyx(self->window, y, x); (void) x; int new_x = ctx->start ? x2 - 1 : MAX(0, wcswidth(ctx->line, ctx->pos)); wmove(self->window, y + 1, new_x); wnoutrefresh(self->window); if (self->help->active) { help_onDraw(self); } } static void groupchat_onInit(ToxWindow *self, Tox *m) { int x2, y2; getmaxyx(self->window, y2, x2); if (x2 <= 0 || y2 <= 0) { exit_toxic_err("failed in groupchat_onInit", FATALERR_CURSES); } ChatContext *ctx = self->chatwin; ctx->history = subwin(self->window, y2 - CHATBOX_HEIGHT + 1, x2 - SIDEBAR_WIDTH - 1, 0, 0); ctx->linewin = subwin(self->window, CHATBOX_HEIGHT, x2, y2 - CHATBOX_HEIGHT, 0); ctx->sidebar = subwin(self->window, y2 - CHATBOX_HEIGHT + 1, 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 groupchat_onInit", FATALERR_MEMORY); } line_info_init(ctx->hst); if (user_settings->autolog == AUTOLOG_ON) { char myid[TOX_ADDRESS_SIZE]; tox_self_get_address(m, (uint8_t *) myid); if (log_enable(self->name, myid, NULL, ctx->log, LOG_GROUP) == -1) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Warning: Log failed to initialize."); } } execute(ctx->history, self, m, "/log", GLOBAL_COMMAND_MODE); scrollok(ctx->history, 0); wmove(self->window, y2 - CURS_Y_OFFSET, 0); } ToxWindow *new_group_chat(Tox *m, uint32_t groupnum) { ToxWindow *ret = calloc(1, sizeof(ToxWindow)); if (ret == NULL) { exit_toxic_err("failed in new_group_chat", FATALERR_MEMORY); } ret->is_groupchat = true; ret->onKey = &groupchat_onKey; ret->onDraw = &groupchat_onDraw; ret->onInit = &groupchat_onInit; ret->onGroupMessage = &groupchat_onGroupMessage; ret->onGroupNameListChange = &groupchat_onGroupNameListChange; ret->onGroupPeerNameChange = &groupchat_onGroupPeerNameChange; ret->onGroupTitleChange = &groupchat_onGroupTitleChange; snprintf(ret->name, sizeof(ret->name), "Group %u", groupnum); ChatContext *chatwin = calloc(1, sizeof(ChatContext)); Help *help = calloc(1, sizeof(Help)); if (chatwin == NULL || help == NULL) { exit_toxic_err("failed in new_group_chat", FATALERR_MEMORY); } ret->chatwin = chatwin; ret->help = help; ret->num = groupnum; ret->show_peerlist = true; ret->active_box = -1; return ret; } toxic-0.8.3/src/groupchat.h000066400000000000000000000036141336615250000156110ustar00rootroot00000000000000/* groupchat.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 GROUPCHAT_H #define GROUPCHAT_H #include "toxic.h" #include "windows.h" #define SIDEBAR_WIDTH 16 #define SDBAR_OFST 2 /* Offset for the peer number box at the top of the statusbar */ #define MAX_GROUPCHAT_NUM MAX_WINDOWS_NUM - 2 #define GROUP_EVENT_WAIT 3 typedef struct GroupPeer { bool active; char name[TOX_MAX_NAME_LENGTH]; size_t name_length; uint32_t peernumber; } GroupPeer; 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; GroupPeer *peer_list; size_t max_idx; char *name_list; size_t num_peers; } GroupChat; /* Frees all Toxic associated data structures for a groupchat (does not call tox_conference_delete() ) */ void free_groupchat(ToxWindow *self, Tox *m, uint32_t groupnum); int init_groupchat_win(ToxWindow *prompt, Tox *m, uint32_t groupnum, uint8_t type); /* destroys and re-creates groupchat window with or without the peerlist */ void redraw_groupchat_win(ToxWindow *self); ToxWindow *new_group_chat(Tox *m, uint32_t groupnum); #endif /* GROUPCHAT_H */ toxic-0.8.3/src/help.c000066400000000000000000000310411336615250000145330ustar00rootroot00000000000000/* 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 "windows.h" #include "toxic.h" #include "help.h" #include "misc_tools.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); memset(self->help, 0, sizeof(Help)); } 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, " g"); wattron(win, A_BOLD | COLOR_PAIR(BLUE)); wprintw(win, "r"); wattroff(win, A_BOLD | COLOR_PAIR(BLUE)); wprintw(win, "oup 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); (void) 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 with optional note\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, " /group : Create a group chat where type: text | audio\n"); wprintw(win, " /myid : Print your Tox ID\n"); #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 group chat\n"); wprintw(win, " /join : Join a pending group chat\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, " /video : Toggle video call\n"); #endif /* VIDEO */ 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+[ and Ctrl+] : Scroll peer list in groupchats\n"); wprintw(win, " Ctrl+B : Toggle the groupchat 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_group(ToxWindow *self) { WINDOW *win = self->help->win; wmove(win, 1, 1); wattron(win, A_BOLD | COLOR_PAIR(RED)); wprintw(win, "Group commands:\n"); wattroff(win, A_BOLD | COLOR_PAIR(RED)); wprintw(win, " /title : Set group title (show current title if no msg)\n\n"); 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 'x': case T_KEY_ESC: help_exit(self); break; case 'c': #ifdef VIDEO help_init_window(self, 23, 80); #elif AUDIO help_init_window(self, 20, 80); #else help_init_window(self, 10, 80); #endif self->help->type = HELP_CHAT; break; case 'g': height = 22; #ifdef VIDEO height += 8; #elif AUDIO height += 4; #endif #ifdef PYTHON height += 2; #endif help_init_window(self, height, 80); self->help->type = HELP_GLOBAL; break; case 'r': help_init_window(self, 6, 80); self->help->type = HELP_GROUP; break; #ifdef PYTHON case 'p': help_init_window(self, 4 + num_registered_handlers(), help_max_width()); self->help->type = HELP_PLUGIN; break; #endif /* PYTHON */ case 'f': help_init_window(self, 10, 80); self->help->type = HELP_CONTACTS; break; case 'k': help_init_window(self, 15, 80); self->help->type = HELP_KEYS; break; case '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_GROUP: help_draw_group(self); break; #ifdef PYTHON case HELP_PLUGIN: help_draw_plugin(self); break; #endif /* PYTHON */ } } toxic-0.8.3/src/help.h000066400000000000000000000021451336615250000145430ustar00rootroot00000000000000/* 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_GROUP, 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.8.3/src/input.c000066400000000000000000000167321336615250000147540ustar00rootroot00000000000000/* 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 "toxic.h" #include "windows.h" #include "misc_tools.h" #include "toxic_strings.h" #include "line_info.h" #include "notify.h" #include "groupchat.h" #include "settings.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 y, int mx_x, int mx_y) { 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, int x, int mx_x) { ChatContext *ctx = self->chatwin; if (del_word_buf(ctx) == -1) { sound_notify(self, notif_error, 0, NULL); return; } } /* 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 y, 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; int s_len = ctx->start > 0 ? wcwidth(ctx->line[ctx->start - 1]) : 0; --ctx->pos; 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); } } /* 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); } } /* 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 y, int mx_x, int mx_y) { 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, x, mx_x); 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, y, 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; 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->is_groupchat) { self->show_peerlist ^= 1; redraw_groupchat_win(self); } match = true; } else if (key == user_settings->key_toggle_pastemode) { self->chatwin->pastemode ^= 1; match = true; } } return match; } toxic-0.8.3/src/input.h000066400000000000000000000022411336615250000147470ustar00rootroot00000000000000/* 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 y, int mx_x, int mx_y); /* 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 y, int mx_x, int mx_y); #endif /* INPUT_H */ toxic-0.8.3/src/line_info.c000066400000000000000000000402271336615250000155530ustar00rootroot00000000000000/* 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 "toxic.h" #include "windows.h" #include "line_info.h" #include "groupchat.h" #include "settings.h" #include "notify.h" #include "message_queue.h" #include "misc_tools.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_sz = 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->prev == NULL) { return; } int y2, x2; getmaxyx(self->window, y2, x2); int side_offst = self->show_peerlist ? SIDEBAR_WIDTH : 0; int top_offst = self->is_chat || self->is_prompt ? 2 : 0; int max_y = (y2 - CHATBOX_HEIGHT - top_offst); int curlines = 0; int nxtlines = line->newlines + (line->len / (x2 - side_offst)); do { curlines += 1 + nxtlines; line = line->prev; nxtlines = line->newlines + (line->len / (x2 - side_offst)); } while (line->prev && curlines + nxtlines < max_y); hst->line_start = line; } 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; } int i; for (i = 0; i < hst->queue_sz; ++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_sz <= 0) { return NULL; } struct line_info *line = hst->queue[0]; int i; for (i = 0; i < hst->queue_sz; ++i) { hst->queue[i] = hst->queue[i + 1]; } --hst->queue_sz; return line; } /* 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, const char *timestr, const char *name1, const char *name2, uint8_t type, uint8_t bold, uint8_t colour, const char *msg, ...) { if (!self) { return -1; } struct history *hst = self->chatwin->hst; if (hst->queue_sz >= 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] = {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; break; case IN_MSG: /* fallthrough */ case OUT_MSG: len += strlen(user_settings->line_normal) + 3; break; case CONNECTION: len += strlen(user_settings->line_join) + 2; break; case DISCONNECTION: len += strlen(user_settings->line_quit) + 2; break; case SYS_MSG: break; case NAME_CHANGE: len += strlen(user_settings->line_alert) + 1; break; case PROMPT: ++len; break; default: len += 2; break; } if (frmt_msg[0]) { snprintf(new_line->msg, sizeof(new_line->msg), "%s", frmt_msg); len += strlen(new_line->msg); int i; for (i = 0; frmt_msg[i]; ++i) { if (frmt_msg[i] == '\n') { ++new_line->newlines; } } } if (timestr) { snprintf(new_line->timestr, sizeof(new_line->timestr), "%s", 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_sz) % INT_MAX; new_line->len = len; new_line->type = type; new_line->bold = bold; new_line->colour = colour; new_line->noread_flag = false; new_line->timestamp = get_unix_time(); hst->queue[hst->queue_sz++] = 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; int y, y2, x, x2; getmaxyx(self->window, y2, x2); getyx(self->chatwin->history, y, x); (void) x; if (x2 <= SIDEBAR_WIDTH) { return; } int offst = self->show_peerlist ? SIDEBAR_WIDTH : 0; /* offset width of groupchat sidebar */ int lines = 1 + line->newlines + (line->len / (x2 - offst)); int max_y = y2 - CHATBOX_HEIGHT; /* move line_start forward proportionate to the number of new lines */ if (y + lines - 1 >= max_y) { while (lines > 0 && hst->line_start->next) { lines -= 1 + hst->line_start->next->newlines + (hst->line_start->next->len / (x2 - offst)); hst->line_start = hst->line_start->next; ++hst->start_id; } } } #define NOREAD_FLAG_TIMEOUT 5 /* seconds before a sent message with no read receipt is flagged as unread */ 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, x2; getmaxyx(self->window, y2, x2); if (x2 <= SIDEBAR_WIDTH) { return; } if (self->is_groupchat) { wmove(win, 0, 0); } else { wmove(win, 2, 0); } struct line_info *line = hst->line_start->next; int numlines = 0; while (line && numlines++ <= y2) { 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)); char *msg = line->msg; while (msg) { char *line = strsep(&msg, "\n"); if (line[0] == '>') { wattron(win, COLOR_PAIR(GREEN)); } else if (line[0] == '<') { wattron(win, COLOR_PAIR(RED)); } wprintw(win, "%s%c", line, msg ? '\n' : '\0'); if (line[0] == '>') { wattroff(win, COLOR_PAIR(GREEN)); } else if (line[0] == '<') { wattroff(win, COLOR_PAIR(RED)); } // change the \0 set by strsep back to \n if (msg) { msg[-1] = '\n'; } } if (type == OUT_MSG && timed_out(line->timestamp, NOREAD_FLAG_TIMEOUT)) { wattron(win, COLOR_PAIR(RED)); wprintw(win, " x", line->msg); wattroff(win, COLOR_PAIR(RED)); if (line->noread_flag == false) { line->noread_flag = true; line->len += 2; } } wprintw(win, "\n", line->msg); 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 %s", user_settings->line_normal, line->name1, line->msg); wattroff(win, COLOR_PAIR(YELLOW)); if (type == OUT_ACTION && timed_out(line->timestamp, NOREAD_FLAG_TIMEOUT)) { wattron(win, COLOR_PAIR(RED)); wprintw(win, " x", line->msg); wattroff(win, COLOR_PAIR(RED)); if (line->noread_flag == false) { line->noread_flag = true; line->len += 2; } } wprintw(win, "\n", line->msg); 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)); } wprintw(win, "%s\n", line->msg); 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]) { wprintw(win, "%s", line->msg); } wprintw(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); wprintw(win, "%s\n", line->msg); 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); wprintw(win, "%s\n", line->msg); 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); wprintw(win, "%s", line->msg); wattron(win, A_BOLD); wprintw(win, "%s\n", line->name2); wattroff(win, A_BOLD); wattroff(win, COLOR_PAIR(MAGENTA)); break; } line = line->next; } /* keep calling until queue is empty */ if (hst->queue_sz > 0) { line_info_print(self); } } /* puts msg in specified line_info msg buffer */ void line_info_set(ToxWindow *self, uint32_t id, char *msg) { struct line_info *line = self->chatwin->hst->line_end; while (line) { if (line->id == id) { snprintf(line->msg, sizeof(line->msg), "%s", msg); return; } line = line->prev; } } /* static void line_info_goto_root(struct history *hst) { hst->line_start = hst->line_root; } */ static void line_info_scroll_up(struct history *hst) { if (hst->line_start->prev) { hst->line_start = hst->line_start->prev; } else { sound_notify(NULL, notif_error, NT_ALWAYS, NULL); } } static void line_info_scroll_down(struct history *hst) { if (hst->line_start->next) { hst->line_start = hst->line_start->next; } else { sound_notify(NULL, notif_error, NT_ALWAYS, NULL); } } static void line_info_page_up(ToxWindow *self, struct history *hst) { int x2, y2; getmaxyx(self->window, y2, x2); (void) x2; int jump_dist = y2 / 2; int i; for (i = 0; i < jump_dist && hst->line_start->prev; ++i) { hst->line_start = hst->line_start->prev; } } static void line_info_page_down(ToxWindow *self, struct history *hst) { int x2, y2; getmaxyx(self->window, y2, x2); (void) x2; int jump_dist = y2 / 2; int i; for (i = 0; i < jump_dist && hst->line_start->next; ++i) { hst->line_start = 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(hst); } else if (key == user_settings->key_scroll_line_down) { line_info_scroll_down(hst); } else if (key == user_settings->key_page_bottom) { line_info_reset_start(self, hst); } else { match = false; } return match; } void line_info_clear(struct history *hst) { hst->line_start = hst->line_end; hst->start_id = hst->line_start->id; } toxic-0.8.3/src/line_info.h000066400000000000000000000062151336615250000155570ustar00rootroot00000000000000/* 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 "windows.h" #include "toxic.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 { 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; uint8_t noread_flag; /* true if a line should be flagged as unread */ uint32_t id; uint16_t len; /* combined len of entire line */ uint8_t newlines; 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]; int queue_sz; }; /* 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, const char *timestr, const char *name1, const char *name2, uint8_t 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); /* 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.8.3/src/log.c000066400000000000000000000172731336615250000143770ustar00rootroot00000000000000/* 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 "toxic.h" #include "windows.h" #include "misc_tools.h" #include "log.h" #include "settings.h" #include "line_info.h" extern struct user_settings *user_settings; /* There are three types of logs: chat logs, groupchat 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-friendname-otherkey.log A groupchat log is in the format: LOGDIR/selfkey-groupname-date[time].log Only the first (KEY_IDENT_DIGITS * 2) numbers of the key are used. Returns 0 on success, -1 if the path is too long */ static int get_log_path(char *dest, int destsize, char *name, const char *selfkey, const char *otherkey, int logtype) { if (!valid_nick(name)) { name = UNKNOWN_NAME; } const char *namedash = logtype == LOG_PROMPT ? "" : "-"; 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 digits of selfkey */ char self_id[32]; 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}; switch (logtype) { case LOG_CHAT: 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'; break; case LOG_GROUP: strftime(other_id, sizeof(other_id), "%Y-%m-%d[%H:%M:%S]", get_time()); path_len += strlen(other_id); break; } 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 0; } /* Opens log file or creates a new one */ static int init_logging_session(char *name, const char *selfkey, const char *otherkey, struct chatlog *log, int logtype) { if (selfkey == NULL || (logtype == LOG_CHAT && otherkey == NULL)) { return -1; } char log_path[MAX_STR_SIZE]; if (get_log_path(log_path, sizeof(log_path), name, selfkey, otherkey, logtype) == -1) { return -1; } log->file = fopen(log_path, "a+"); snprintf(log->path, sizeof(log->path), "%s", log_path); if (log->file == NULL) { return -1; } 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->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->file != NULL) { fclose(log->file); } memset(log, 0, sizeof(struct chatlog)); } int log_enable(char *name, const char *selfkey, const char *otherkey, struct chatlog *log, int logtype) { log->log_on = true; if (log->file != NULL) { return 0; } if (init_logging_session(name, selfkey, otherkey, log, logtype) == -1) { log_disable(log); return -1; } return 0; } /* Loads previous history from chat log */ void load_chat_history(ToxWindow *self, struct chatlog *log) { if (log->file == NULL) { return; } off_t sz = file_size(log->path); if (sz <= 0) { return; } char *hstbuf = malloc(sz + 1); if (hstbuf == NULL) { exit_toxic_err("failed in load_chat_history", FATALERR_MEMORY); } if (fseek(log->file, 0L, SEEK_SET) == -1) { free(hstbuf); line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, RED, " * Failed to read log file"); return; } if (fread(hstbuf, sz, 1, log->file) != 1) { free(hstbuf); line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, RED, " * Failed to read log file"); return; } hstbuf[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, 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 (hstbuf[start] == '\n') { ++count; } } const char *line = strtok(&hstbuf[start + 1], "\n"); if (line == NULL) { free(hstbuf); return; } while (line != NULL && count--) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "%s", line); line = strtok(NULL, "\n"); } line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, ""); free(hstbuf); } /* renames chatlog file replacing src with dest. Returns 0 on success or if no log exists, -1 on failure. */ int rename_logfile(char *src, 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; 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, LOG_CHAT) == -1) { goto on_error; } if (!file_exists(oldpath)) { return 0; } if (get_log_path(newpath, sizeof(newpath), dest, selfkey, otherkey, LOG_CHAT) == -1) { goto on_error; } if (rename(oldpath, newpath) != 0) { goto on_error; } if (log_on) { log_enable(dest, selfkey, otherkey, log, LOG_CHAT); } return 0; on_error: if (log_on) { log_enable(src, selfkey, otherkey, log, LOG_CHAT); } return -1; } toxic-0.8.3/src/log.h000066400000000000000000000034021336615250000143710ustar00rootroot00000000000000/* 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_GROUP, LOG_PROMPT, LOG_CHAT, } LOG_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 and creates/fetches file if necessary. * * Returns 0 on success. * Returns -1 on failure. */ int log_enable(char *name, const char *selfkey, const char *otherkey, struct chatlog *log, int logtype); /* disables logging for specified log and closes file */ void log_disable(struct chatlog *log); /* Loads previous history from chat log */ void load_chat_history(ToxWindow *self, struct chatlog *log); /* renames chatlog file replacing src with dest. Returns 0 on success or if no log exists, -1 on failure. */ int rename_logfile(char *src, char *dest, const char *selfkey, const char *otherkey, int winnum); #endif /* LOG_H */ toxic-0.8.3/src/message_queue.c000066400000000000000000000077671336615250000164550ustar00rootroot00000000000000/* 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 "toxic.h" #include "windows.h" #include "message_queue.h" #include "misc_tools.h" #include "line_info.h" #include "log.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->receipt = 0; 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 = self->chatwin->hst->line_end; while (line) { if (line->id != msg->line_id) { line = line->prev; continue; } line->type = msg->type == OUT_ACTION ? OUT_ACTION_READ : OUT_MSG_READ; if (line->noread_flag == true) { line->len -= 2; line->noread_flag = false; } return; } } /* 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 chat_queue *q = self->chatwin->cqueue; struct cqueue_msg *msg = q->root; while (msg) { if (msg->receipt != receipt) { msg = msg->next; continue; } 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, self->chatwin->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; } free(msg); q->root = next; } else { struct cqueue_msg *prev = msg->prev; free(msg); prev->next = next; } return; } } #define CQUEUE_TRY_SEND_INTERVAL 10 /* Tries to send the oldest unsent message in queue. */ void cqueue_try_send(ToxWindow *self, Tox *m) { struct chat_queue *q = self->chatwin->cqueue; struct cqueue_msg *msg = q->root; if (!msg) { return; } if (msg->receipt != 0 && !timed_out(msg->last_send_try, CQUEUE_TRY_SEND_INTERVAL)) { return; } Tox_Message_Type type = msg->type == OUT_MSG ? TOX_MESSAGE_TYPE_NORMAL : TOX_MESSAGE_TYPE_ACTION; msg->receipt = tox_friend_send_message(m, self->num, type, (uint8_t *) msg->message, msg->len, NULL); msg->last_send_try = get_unix_time(); } toxic-0.8.3/src/message_queue.h000066400000000000000000000030251336615250000164410ustar00rootroot00000000000000/* 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; uint8_t type; uint32_t receipt; time_t last_send_try; 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 the oldest unsent message in queue. */ void cqueue_try_send(ToxWindow *self, Tox *m); /* 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.8.3/src/misc_tools.c000066400000000000000000000332261336615250000157650ustar00rootroot00000000000000/* 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 #include #include #include "toxic.h" #include "windows.h" #include "misc_tools.h" #include "settings.h" #include "file_transfers.h" extern ToxWindow *prompt; extern struct user_settings *user_settings; void hst_to_net(uint8_t *num, uint16_t numbytes) { #ifndef WORDS_BIGENDIAN uint32_t i; uint8_t buff[numbytes]; for (i = 0; i < numbytes; ++i) { buff[i] = num[numbytes - i - 1]; } memcpy(num, buff, numbytes); #endif return; } 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(); } /* 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 [HH:mm:ss] */ void get_time_str(char *buf, int bufsize) { if (user_settings->timestamps == TIMESTAMPS_OFF) { buf[0] = '\0'; 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 of length hex_len to binary format and puts the result in output. * output_size must be exactly half of hex_len. * * Returns 0 on success. * Returns -1 on failure. */ int hex_string_to_bin(const char *hex_string, size_t hex_len, char *output, size_t output_size) { if (output_size == 0 || 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; } int hex_string_to_bytes(char *buf, int size, const char *keystr) { if (size % 2 != 0) { return -1; } int i, res; const char *pos = keystr; for (i = 0; i < size; ++i) { 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. * * Returns 0 on success. * Returns -1 on failure. */ int bin_id_to_string(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; } size_t i; for (i = 0; i < TOX_ADDRESS_SIZE; ++i) { snprintf(&output[i * 2], output_size - (i * 2), "%02X", bin_id[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); } /* Returns 1 if nick is valid, 0 if not. 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 */ int valid_nick(const char *nick) { if (!nick[0] || nick[0] == ' ') { return 0; } int i; for (i = 0; nick[i]; ++i) { if ((nick[i] == ' ' && nick[i + 1] == ' ') || nick[i] == '/' || nick[i] == '\n' || nick[i] == '\t' || nick[i] == '\v' || nick[i] == '\r') { return 0; } } return 1; } /* 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) { size_t i; for (i = 0; i < len; ++i) { if (str[i] == '\n' || str[i] == '\r' || str[i] == '\t' || str[i] == '\v' || 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 groupchats */ int get_group_nick_truncate(Tox *m, char *buf, uint32_t peernum, uint32_t groupnum) { Tox_Err_Conference_Peer_Query err; size_t len = tox_conference_peer_get_name_size(m, groupnum, peernum, &err); if (err != TOX_ERR_CONFERENCE_PEER_QUERY_OK) { goto on_error; } else { if (!tox_conference_peer_get_name(m, groupnum, 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; stat(path, &s); 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; } /* compares the first size bytes of fp to signature. Returns 0 if they are the same, 1 if they differ, and -1 on error. On success this function will seek back to the beginning of fp */ int check_file_signature(const char *signature, size_t size, FILE *fp) { char buf[size]; if (fread(buf, size, 1, fp) != 1) { return -1; } int ret = memcmp(signature, buf, size); if (fseek(fp, 0L, SEEK_SET) == -1) { return -1; } return ret == 0 ? 0 : 1; } /* sets window title in tab bar. */ void set_window_title(ToxWindow *self, const char *title, int len) { char cpy[TOXIC_MAX_NAME_LENGTH + 1]; if (self->is_groupchat) { /* keep groupnumber in title */ snprintf(cpy, sizeof(cpy), "%d %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); } /* Return true if address appears to be a valid ipv4 address. */ 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). */ bool is_ip6_address(const char *address) { size_t i; size_t num_colons = 0; char ch = 0; for (i = 0; (ch = address[i]); ++i) { if (ch == '.') { return false; } if (ch == ':') { ++num_colons; } } return num_colons > 1 && num_colons < 8; } toxic-0.8.3/src/misc_tools.h000066400000000000000000000150661336615250000157740ustar00rootroot00000000000000/* 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 "windows.h" #include "toxic.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 typedef enum File_Type { FILE_TYPE_REGULAR, FILE_TYPE_DIRECTORY, FILE_TYPE_OTHER, } File_Type; void hst_to_net(uint8_t *num, uint16_t numbytes); /* * Converts a hexidecimal string of length hex_len to binary format and puts the result in output. * output_size must be exactly half of hex_len. * * Returns 0 on success. * Returns -1 on failure. */ int hex_string_to_bin(const char *hex_string, size_t hex_len, char *output, size_t output_size); /* convert a hex string to bytes. returns 0 on success, -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. * * Returns 0 on success. * Returns -1 on failure. */ int bin_id_to_string(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 [HH:mm:ss] (not thread safe) */ void get_time_str(char *buf, int 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); /* 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); /* Returns 1 if nick is valid, 0 if not. 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 */ int 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 groupchats */ int get_group_nick_truncate(Tox *m, char *buf, uint32_t peernum, uint32_t groupnum); /* 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); /* compares the first size bytes of fp and signature. Returns 0 if they are the same, 1 if they differ, and -1 on error. On success this function will seek back to the beginning of fp */ int check_file_signature(const char *signature, size_t size, FILE *fp); /* sets window title in tab bar. */ void set_window_title(ToxWindow *self, const char *title, int len); /* Return true if address appears to be a valid ipv4 address. */ bool is_ip4_address(const char *address); /* 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). */ bool is_ip6_address(const char *address); #endif /* MISC_TOOLS_H */ toxic-0.8.3/src/name_lookup.c000066400000000000000000000271641336615250000161270ustar00rootroot00000000000000/* 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 "toxic.h" #include "windows.h" #include "line_info.h" #include "global_commands.h" #include "misc_tools.h" #include "configdir.h" #include "curl_util.h" extern struct arg_opts arg_opts; extern struct Winthread Winthread; #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 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, NULL, 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) { memset(&t_data, 0, sizeof(struct 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) { int linelen = strlen(line); if (linelen < SERVER_KEY_SIZE * 2 + 5) { continue; } if (line[linelen - 1] == '\n') { 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; } snprintf(Nameservers.names[Nameservers.lines], sizeof(Nameservers.names[Nameservers.lines]), "%s", name); int res = hex_string_to_bytes(Nameservers.keys[Nameservers.lines], 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 (hex_string_to_bin(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) { 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; memset(&recv_data, 0, sizeof(struct Recv_Curl_Data)); char post_data[MAX_STR_SIZE]; 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"); 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: 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, NULL, NULL, NULL, SYS_MSG, 0, 0, "name lookups are disabled."); return; } if (t_data.busy) { line_info_add(self, NULL, 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, NULL, NULL, NULL, SYS_MSG, 0, RED, "Error: lookup thread attr failed to init"); memset(&t_data, 0, sizeof(struct thread_data)); return; } if (pthread_attr_setdetachstate(&lookup_thread.attr, PTHREAD_CREATE_DETACHED) != 0) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, RED, "Error: lookup thread attr failed to set"); pthread_attr_destroy(&lookup_thread.attr); memset(&t_data, 0, sizeof(struct thread_data)); return; } if (pthread_create(&lookup_thread.tid, &lookup_thread.attr, lookup_thread_func, NULL) != 0) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, RED, "Error: lookup thread failed to init"); pthread_attr_destroy(&lookup_thread.attr); memset(&t_data, 0, sizeof(struct 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.8.3/src/name_lookup.h000066400000000000000000000022011336615250000161150ustar00rootroot00000000000000/* 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.8.3/src/notify.c000066400000000000000000000531621336615250000151230ustar00rootroot00000000000000/* 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 "notify.h" #include "audio_device.h" #include "settings.h" #include "line_info.h" #include "misc_tools.h" #include "xtra.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 50 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]; /**********************************************************************************/ /**********************************************************************************/ /**********************************************************************************/ /**********************************************************************************/ /**********************************************************************************/ /* 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; } } 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_primary_device(output, &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)) { memset(&actives[i], 0, sizeof(struct _ActiveNotifications)); } else { break; } } } } if (i == ACTIVE_NOTIFS_MAX) { m_close_device(); /* In case it's opened */ control_unlock(); return; } usleep(1000); } control_unlock(); } void *do_playing(void *_p) { (void)_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); memset(&actives[i], 0, sizeof(struct _ActiveNotifications)); } } #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); memset(&actives[i], 0, sizeof(struct _ActiveNotifications)); } } #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(); usleep(10000); } 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) { (void)_p; while (true) { control_lock(); if (!Control.poll_active) { control_unlock(); break; } int i; for (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); actives[i].box = NULL; if (actives[i].id_indicator) { *actives[i].id_indicator = -1; /* reset indicator value */ } memset(&actives[i], 0, sizeof(struct _ActiveNotifications)); } } control_unlock(); usleep(10000); } pthread_exit(NULL); } void graceful_clear(void) { int i; control_lock(); for (i = 0; i < ACTIVE_NOTIFS_MAX; i ++) { if (actives[i].box) { 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 */ } memset(&actives[i], 0, sizeof(struct _ActiveNotifications)); } control_unlock(); } #endif /* SOUND_NOTIFY */ /**********************************************************************************/ /**********************************************************************************/ /**********************************************************************************/ /**********************************************************************************/ /**********************************************************************************/ /* 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 int set_sound(Notification sound, const char *value) { if (sound == silent) { return 0; } free(Control.sounds[sound]); size_t len = strlen(value) + 1; Control.sounds[sound] = calloc(len, 1); 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 */ if (actives[id].id_indicator) { *actives[id].id_indicator = -1; } // alSourcei(actives[id].source, AL_LOOPING, false); alSourceStop(actives[id].source); alDeleteSources(1, &actives[id].source); alDeleteBuffers(1, &actives[id].buffer); memset(&actives[id], 0, sizeof(struct _ActiveNotifications)); } } #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) { 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 formated[128 * 129] = {'\0'}; int i = 0; for (; i < actives[id].size; i ++) { strcat(formated, actives[id].messages[i]); strcat(formated, "\n"); } formated[strlen(formated) - 1] = '\0'; notify_notification_update(actives[id].box, actives[id].title, formated, NULL); notify_notification_show(actives[id].box, NULL); 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 formated[128 * 129] = {'\0'}; int i = 0; for (; i < actives[id].size; i ++) { strcat(formated, actives[id].messages[i]); strcat(formated, "\n"); } formated[strlen(formated) - 1] = '\0'; notify_notification_update(actives[id].box, actives[id].title, formated, NULL); notify_notification_show(actives[id].box, NULL); control_unlock(); return id; #else return -1; #endif /* BOX_NOTIFY */ } toxic-0.8.3/src/notify.h000066400000000000000000000057321336615250000151300ustar00rootroot00000000000000/* 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); 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 int set_sound(Notification sound, const char *value); #endif /* SOUND_NOTIFY */ #endif /* NOTIFY_H */ toxic-0.8.3/src/osx_video.h000066400000000000000000000033051336615250000156110ustar00rootroot00000000000000/* 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.8.3/src/osx_video.m000066400000000000000000000217531336615250000156250ustar00rootroot00000000000000/* 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.8.3/src/prompt.c000066400000000000000000000443351336615250000151360ustar00rootroot00000000000000/* 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 "toxic.h" #include "windows.h" #include "prompt.h" #include "friendlist.h" #include "execute.h" #include "misc_tools.h" #include "toxic_strings.h" #include "log.h" #include "line_info.h" #include "settings.h" #include "input.h" #include "help.h" #include "notify.h" #include "autocomplete.h" extern ToxWindow *prompt; extern struct user_settings *user_settings; extern struct Winthread Winthread; extern FriendsList Friends; FriendRequests FrndRequests; #ifdef AUDIO #define AC_NUM_GLOB_COMMANDS_AUDIO 2 #else #define AC_NUM_GLOB_COMMANDS_AUDIO 0 #endif /* AUDIO */ #ifdef VIDEO #define AC_NUM_GLOB_COMMANDS_VIDEO 2 #else #define AC_NUM_GLOB_COMMANDS_VIDEO 0 #endif /* VIDEO */ #ifdef PYTHON #define AC_NUM_GLOB_COMMANDS_PYTHON 1 #else #define AC_NUM_GLOB_COMMANDS_PYTHON 0 #endif /* PYTHON */ #ifdef QRCODE #define AC_NUM_GLOB_COMMANDS_QRCODE 1 #else #define AC_NUM_GLOB_COMMANDS_QRCODE 0 #endif /* QRCODE */ #define AC_NUM_GLOB_COMMANDS (17 + AC_NUM_GLOB_COMMANDS_AUDIO + AC_NUM_GLOB_COMMANDS_VIDEO + AC_NUM_GLOB_COMMANDS_PYTHON + AC_NUM_GLOB_COMMANDS_QRCODE) /* Array of global command names used for tab completion. */ static const char glob_cmd_list[AC_NUM_GLOB_COMMANDS][MAX_CMDNAME_SIZE] = { { "/accept" }, { "/add" }, { "/avatar" }, { "/clear" }, { "/connect" }, { "/decline" }, { "/exit" }, { "/group" }, { "/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) { StatusBar *statusbar = prompt->stb; statusbar->connection = connection_status; } /* 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, NULL, 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; } static void 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); if (x2 <= 0 || y2 <= 0) { return; } if (ctx->pastemode && key == '\r') { key = '\n'; } /* ignore non-menu related input if active */ if (self->help->active) { help_onKey(self, key); return; } if (ltr || key == '\n') { /* char is printable */ input_new_char(self, key, x, y, x2, y2); return; } if (line_info_onKey(self, key)) { return; } input_handle(self, key, x, y, x2, y2); if (key == '\t') { /* TAB key: auto-completes command */ 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[3][8] = { {"online"}, {"away"}, {"busy"}, }; diff = complete_line(self, status_cmd_list, 3, 8); } else { diff = complete_line(self, glob_cmd_list, AC_NUM_GLOB_COMMANDS, MAX_CMDNAME_SIZE); } 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') { 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] = {0}; if (wcs_to_mbs_buf(line, ctx->line, MAX_STR_SIZE) == -1) { memset(&line, 0, sizeof(line)); } line_info_add(self, NULL, NULL, NULL, PROMPT, 0, 0, "%s", line); execute(ctx->history, self, m, line, GLOBAL_COMMAND_MODE); } wclear(ctx->linewin); wmove(self->window, y2 - CURS_Y_OFFSET, 0); reset_buf(ctx); } } static void prompt_onDraw(ToxWindow *self, Tox *m) { int x2, 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); curs_set(1); if (ctx->len > 0) { mvwprintw(ctx->linewin, 1, 0, "%ls", &ctx->line[ctx->start]); } StatusBar *statusbar = self->stb; mvwhline(statusbar->topline, 1, 0, ACS_HLINE, x2); wmove(statusbar->topline, 0, 0); pthread_mutex_lock(&Winthread.lock); Tox_Connection connection = statusbar->connection; pthread_mutex_unlock(&Winthread.lock); if (connection != TOX_CONNECTION_NONE) { int colour = MAGENTA; const char *status_text = "ERROR"; pthread_mutex_lock(&Winthread.lock); Tox_User_Status status = statusbar->status; pthread_mutex_unlock(&Winthread.lock); switch (status) { case TOX_USER_STATUS_NONE: status_text = "Online"; colour = GREEN; break; case TOX_USER_STATUS_AWAY: status_text = "Away"; colour = YELLOW; break; case TOX_USER_STATUS_BUSY: status_text = "Busy"; colour = RED; break; } 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, A_BOLD); pthread_mutex_lock(&Winthread.lock); wprintw(statusbar->topline, " %s", statusbar->nick); pthread_mutex_unlock(&Winthread.lock); wattroff(statusbar->topline, A_BOLD); } else { wprintw(statusbar->topline, " [Offline]"); wattron(statusbar->topline, A_BOLD); pthread_mutex_lock(&Winthread.lock); wprintw(statusbar->topline, " %s", statusbar->nick); pthread_mutex_unlock(&Winthread.lock); wattroff(statusbar->topline, A_BOLD); } /* 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); if (statusbar->statusmsg_len > maxlen) { statusbar->statusmsg[maxlen - 3] = '\0'; strcat(statusbar->statusmsg, "..."); statusbar->statusmsg_len = maxlen; } if (statusbar->statusmsg[0]) { wprintw(statusbar->topline, " : %s", statusbar->statusmsg); } pthread_mutex_unlock(&Winthread.lock); mvwhline(self->window, y2 - CHATBOX_HEIGHT, 0, ACS_HLINE, x2); int y, x; getyx(self->window, y, x); (void) x; int new_x = ctx->start ? x2 - 1 : MAX(0, wcswidth(ctx->line, ctx->pos)); wmove(self->window, y + 1, new_x); 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); } char timefrmt[TIME_STR_SIZE]; get_time_str(timefrmt, sizeof(timefrmt)); 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, timefrmt, 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, timefrmt, 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) { ChatContext *ctx = self->chatwin; char timefrmt[TIME_STR_SIZE]; get_time_str(timefrmt, sizeof(timefrmt)); line_info_add(self, timefrmt, 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, NULL, NULL, NULL, SYS_MSG, 0, 0, errmsg); return; } line_info_add(self, NULL, 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); } (void) 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, 2, x2, 0, 0); } static void print_welcome_msg(ToxWindow *self) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 1, BLUE, " _____ _____ _____ ____ "); line_info_add(self, NULL, NULL, NULL, SYS_MSG, 1, BLUE, " |_ _/ _ \\ \\/ /_ _/ ___|"); line_info_add(self, NULL, NULL, NULL, SYS_MSG, 1, BLUE, " | || | | \\ / | | | "); line_info_add(self, NULL, NULL, NULL, SYS_MSG, 1, BLUE, " | || |_| / \\ | | |___ "); line_info_add(self, NULL, NULL, NULL, SYS_MSG, 1, BLUE, " |_| \\___/_/\\_\\___\\____| v." TOXICVER); line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, ""); const char *msg = "Welcome to Toxic, a free, open source Tox-based instant messenging client."; line_info_add(self, NULL, 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, NULL, NULL, NULL, SYS_MSG, 1, CYAN, msg); line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, ""); } static void prompt_onInit(ToxWindow *self, Tox *m) { curs_set(1); int y2, 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 + 1, x2, 0, 0); ctx->linewin = subwin(self->window, CHATBOX_HEIGHT, x2, y2 - CHATBOX_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); if (user_settings->autolog == AUTOLOG_ON) { char myid[TOX_ADDRESS_SIZE]; tox_self_get_address(m, (uint8_t *) myid); if (log_enable(self->name, myid, NULL, ctx->log, LOG_PROMPT) == -1) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Warning: Log failed to initialize."); } } 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->is_prompt = true; 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.8.3/src/prompt.h000066400000000000000000000035471336615250000151430ustar00rootroot00000000000000/* 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 32 struct friend_request { bool active; char msg[MAX_STR_SIZE]; uint8_t key[TOX_PUBLIC_KEY_SIZE]; }; typedef struct { int max_idx; int num_requests; struct friend_request request[MAX_FRIEND_REQUESTS]; } FriendRequests; 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.8.3/src/python_api.c000066400000000000000000000225041336615250000157610ustar00rootroot00000000000000/* 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) { size_t i, ii; FriendsList friends; PyObject *cur, *ret; char pubkey_buf[TOX_PUBLIC_KEY_SIZE * 2 + 1]; if (!PyArg_ParseTuple(args, "")) { return NULL; } friends = api_get_friendslist(); ret = PyList_New(0); for (i = 0; i < friends.num_friends; i++) { for (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'; 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 *groupchat_command_const = Py_BuildValue("i", GROUPCHAT_COMMAND_MODE); PyObject_SetAttrString(m, "GLOBAL_COMMAND", global_command_const); PyObject_SetAttrString(m, "CHAT_COMMAND", chat_command_const); PyObject_SetAttrString(m, "GROUPCHAT_COMMAND", groupchat_command_const); Py_DECREF(global_command_const); Py_DECREF(chat_command_const); Py_DECREF(groupchat_command_const); return m; } void terminate_python(void) { struct python_registered_func *cur, *old; if (python_commands.name != NULL) { free(python_commands.name); } for (cur = python_commands.next; cur != NULL;) { 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.8.3/src/python_api.h000066400000000000000000000022631336615250000157660ustar00rootroot00000000000000/* 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.8.3/src/qr_code.c000066400000000000000000000122441336615250000152230ustar00rootroot00000000000000/* 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 "toxic.h" #include "windows.h" #include "qr_code.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[row_size]; png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); if (png_ptr == NULL) { fclose(fp); QRcode_free(qr_obj); return -1; } info_ptr = png_create_info_struct(png_ptr); if (info_ptr == NULL) { fclose(fp); QRcode_free(qr_obj); return -1; } if (setjmp(png_jmpbuf(png_ptr))) { fclose(fp); 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); } png_write_end(png_ptr, info_ptr); png_destroy_write_struct(&png_ptr, &info_ptr); fclose(fp); QRcode_free(qr_obj); return 0; } #endif /* QRPNG */ #endif /* QRCODE */ toxic-0.8.3/src/qr_code.h000066400000000000000000000025331336615250000152300ustar00rootroot00000000000000/* 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.8.3/src/settings.c000066400000000000000000000462611336615250000154550ustar00rootroot00000000000000/* 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 "toxic.h" #include "windows.h" #include "configdir.h" #include "notify.h" #include "misc_tools.h" #ifdef AUDIO #include "audio_device.h" #endif /* AUDIO */ #include "settings.h" #include "line_info.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 *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 *line_join; const char *line_quit; const char *line_alert; const char *line_normal; const char *mplex_away; const char *mplex_away_note; } 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", "show_typing_self", "show_typing_other", "show_welcome_msg", "show_connection_msg", "nodeslist_update_freq", "line_join", "line_quit", "line_alert", "line_normal", "mplex_away", "mplex_away_note", }; 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->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; 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 *peer_list_up; const char *peer_list_down; 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", "peer_list_up", "peer_list_down", "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_peer_list_up = T_KEY_C_LB; settings->key_peer_list_down = T_KEY_C_RB; 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_treshold; } audio_strings = { "audio", "input_device", "output_device", "VAD_treshold", }; static void audio_defaults(struct user_settings *settings) { settings->audio_in_dev = 0; settings->audio_out_dev = 0; settings->VAD_treshold = 40.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:%S %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.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.nodeslist_update_freq, &s->nodeslist_update_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.peer_list_up, &tmp)) { set_key_binding(&s->key_peer_list_up, &tmp); } if (config_setting_lookup_string(setting, key_strings.peer_list_down, &tmp)) { set_key_binding(&s->key_peer_list_down, &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_treshold, &s->VAD_treshold); } #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.8.3/src/settings.h000066400000000000000000000065051336615250000154570ustar00rootroot00000000000000/* 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 */ 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 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) */ 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]; 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_peer_list_up; int key_peer_list_down; 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_treshold; #endif }; 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:%S" #define LOG_TIMESTAMP_DEFAULT "%Y/%m/%d [%H:%M:%S]" #define MPLEX_AWAY_NOTE "Detached from screen" int settings_load(struct user_settings *s, const char *patharg); #endif /* SETTINGS_H */ toxic-0.8.3/src/term_mplex.c000066400000000000000000000242611336615250000157650ustar00rootroot00000000000000/* 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 "windows.h" #include "term_mplex.h" #include "toxic.h" #include "settings.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) { dyn_buffer = (char *) realloc(dyn_buffer, length); } else { dyn_buffer = (char *) malloc(length); } 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 = (char *) malloc(end - pos + 1); *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 = (char *) malloc(numstr_len + 2); 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.8.3/src/term_mplex.h000066400000000000000000000021561336615250000157710ustar00rootroot00000000000000/* 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.8.3/src/toxic.c000066400000000000000000001142011336615250000147310ustar00rootroot00000000000000/* 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 "configdir.h" #include "toxic.h" #include "windows.h" #include "friendlist.h" #include "prompt.h" #include "misc_tools.h" #include "groupchat.h" #include "file_transfers.h" #include "line_info.h" #include "settings.h" #include "log.h" #include "notify.h" #include "audio_device.h" #include "message_queue.h" #include "execute.h" #include "term_mplex.h" #include "name_lookup.h" #include "bootstrap.h" #ifdef X11 #include "xtra.h" #endif #ifdef AUDIO #include "audio_call.h" #ifdef VIDEO #include "video_call.h" #endif /* VIDEO */ 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 AUTOSAVE_FREQ 600 #define MIN_PASSWORD_LEN 6 #define MAX_PASSWORD_LEN 64 struct Winthread Winthread; struct cqueue_thread cqueue_thread; struct av_thread av_thread; struct arg_opts arg_opts; 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) { 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) { freopen("/dev/tty", "w", stderr); // make sure stderr is enabled since we may have disabled it endwin(); fprintf(stderr, "Caught SIGSEGV: Aborting toxic session.\n"); exit(EXIT_FAILURE); } static void flag_window_resize(int 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); memset(&user_password, 0, sizeof(struct user_password)); kill_all_file_transfers(m); kill_all_windows(m); terminate_notify(); #ifdef AUDIO #ifdef VIDEO terminate_video(); #endif /* VIDEO */ terminate_audio(); #endif /* AUDIO */ #ifdef PYTHON terminate_python(); #endif /* PYTHON */ free_global_data(); tox_kill(m); endwin(); curl_global_cleanup(); #ifdef X11 /* We have to terminate xtra last coz reasons * Please don't call this anywhere else coz trust me */ terminate_xtra(); #endif /* X11 */ exit(EXIT_SUCCESS); } void exit_toxic_err(const char *errmsg, int errcode) { free_global_data(); freopen("/dev/tty", "w", stderr); endwin(); fprintf(stderr, "Toxic session aborted with error code %d (%s)\n", errcode, errmsg); exit(EXIT_FAILURE); } 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(); timeout(100); if (has_colors()) { short bg_color = COLOR_BLACK; start_color(); if (user_settings->colour_theme == NATIVE_COLS) { if (assume_default_colors(-1, -1) == OK) { bg_color = -1; } } init_pair(0, COLOR_WHITE, COLOR_BLACK); init_pair(1, COLOR_GREEN, bg_color); init_pair(2, COLOR_CYAN, bg_color); init_pair(3, COLOR_RED, bg_color); init_pair(4, COLOR_BLUE, bg_color); init_pair(5, COLOR_YELLOW, bg_color); init_pair(6, COLOR_MAGENTA, bg_color); init_pair(7, COLOR_BLACK, COLOR_BLACK); init_pair(8, COLOR_BLACK, COLOR_WHITE); } 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; } int i; for (i = 0; i < init_messages.num; ++i) { free(init_messages.msgs[i]); } free(init_messages.msgs); } static void print_init_messages(ToxWindow *toxwin) { int i; for (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 i; size_t numfriends = tox_self_get_friend_list_size(m); for (i = 0; i < numfriends; ++i) { friendlist_onFriendAdded(NULL, m, i, false); } sort_friendlist_index(); } static void load_groups(ToxWindow *prompt, Tox *m) { size_t i; size_t num_chats = tox_conference_get_chatlist_size(m); uint32_t chatlist[num_chats]; if (num_chats) { tox_conference_get_chatlist(m, chatlist); } for (i = 0; i < num_chats; ++i) { uint32_t groupnum = chatlist[i]; if (get_num_active_windows() >= MAX_WINDOWS_NUM) { tox_conference_delete(m, groupnum, NULL); continue; } Tox_Err_Conference_Get_Type err; Tox_Conference_Type type = tox_conference_get_type(m, groupnum, &err); if (err != TOX_ERR_CONFERENCE_GET_TYPE_OK) { tox_conference_delete(m, groupnum, NULL); continue; } if (init_groupchat_win(prompt, m, groupnum, type) == -1) { tox_conference_delete(m, groupnum, NULL); continue; } } } /* 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); int len = strlen(buf); /* re-enable terminal echo */ tcsetattr(fileno(stdin), TCSANOW, &oflags); if (p == NULL || 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 { system("clear"); 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; } system("clear"); } /* 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; } char temp_path[strlen(path) + strlen(TEMP_PROFILE_EXT) + 1]; snprintf(temp_path, sizeof(temp_path), "%s%s", path, TEMP_PROFILE_EXT); FILE *fp = fopen(temp_path, "wb"); if (fp == NULL) { return -1; } size_t data_len = tox_get_savedata_size(m); char *data = malloc(data_len * sizeof(char)); if (data == NULL) { 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(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(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(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(data); return -1; } } fclose(fp); free(data); if (rename(temp_path, path) != 0) { return -1; } 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); } 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); 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[len]; if (fread(data, sizeof(data), 1, fp) != 1) { fclose(fp); 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); 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) { system("clear"); // TODO: is this portable? printf("Enter password (q to quit) "); } size_t plain_len = len - TOX_PASS_ENCRYPTION_EXTRA_LENGTH; char plain[plain_len]; 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); exit(0); } if (pwlen < MIN_PASSWORD_LEN) { system("clear"); 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); return NULL; } break; } else if (pwerr == TOX_ERR_DECRYPTION_FAILED) { system("clear"); sleep(1); printf("Invalid password. Try again. "); pweval = 0; } else { fclose(fp); exit_toxic_err("tox_pass_decrypt() failed", pwerr); } } } 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); return NULL; } } fclose(fp); } 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); } #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_active_window(m); draw_count++; 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); } } } void *thread_cqueue(void *data) { Tox *m = (Tox *) data; while (true) { pthread_mutex_lock(&Winthread.lock); size_t i; for (i = 2; i < MAX_WINDOWS_NUM; ++i) { ToxWindow *toxwin = get_window_ptr(i); if (toxwin != NULL && toxwin->is_chat && get_friend_connection_status(toxwin->num) != TOX_CONNECTION_NONE) { cqueue_try_send(toxwin, m); } } pthread_mutex_unlock(&Winthread.lock); usleep(4000); } } #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); usleep(toxav_iteration_interval(av) * 1000); } } #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, " -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) { memset(&arg_opts, 0, sizeof(struct arg_opts)); /* 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[] = { {"file", required_argument, 0, 'f'}, {"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'}, {"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 = "4bdehotuxvc:f:n:r:p:P:T:"; int opt, indexptr; long int port = 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': 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': arg_opts.use_custom_data = 1; if (DATA_FILE) { free(DATA_FILE); } if (BLOCK_FILE) { free(BLOCK_FILE); } DATA_FILE = malloc(strlen(optarg) + 1); strcpy(DATA_FILE, optarg); if (DATA_FILE == NULL) { exit_toxic_err("failed in parse_args", FATALERR_MEMORY); } 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 'n': 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; 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); } 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 'P': 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); } 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': 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': port = strtol(optarg, NULL, 10); if (port <= 0 || port > MAX_PORT_RANGE) { port = 14191; } arg_opts.tcp_port = port; break; case 'u': arg_opts.unencrypt_data = 1; break; case 'v': print_version(); exit(EXIT_SUCCESS); case 'h': default: print_usage(); exit(EXIT_SUCCESS); } } } /* Looks for an old default profile data file and blocklist, and renames them to the new default names. * * Returns 0 on success. * Returns -1 on failure. */ #define OLD_DATA_NAME "data" #define OLD_DATA_BLOCKLIST_NAME "data-blocklist" static int rename_old_profile(const char *user_config_dir) { char old_data_file[strlen(user_config_dir) + strlen(CONFIGDIR) + strlen(OLD_DATA_NAME) + 1]; snprintf(old_data_file, sizeof(old_data_file), "%s%s%s", user_config_dir, CONFIGDIR, OLD_DATA_NAME); if (!file_exists(old_data_file)) { return 0; } if (file_exists(DATA_FILE)) { return 0; } if (rename(old_data_file, DATA_FILE) != 0) { return -1; } queue_init_message("Data file has been moved to %s", DATA_FILE); char old_data_blocklist[strlen(user_config_dir) + strlen(CONFIGDIR) + strlen(OLD_DATA_BLOCKLIST_NAME) + 1]; snprintf(old_data_blocklist, sizeof(old_data_blocklist), "%s%s%s", user_config_dir, CONFIGDIR, OLD_DATA_BLOCKLIST_NAME); if (!file_exists(old_data_blocklist)) { return 0; } if (file_exists(BLOCK_FILE)) { return 0; } if (rename(old_data_blocklist, BLOCK_FILE) != 0) { return -1; } return 0; } /* 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); } /* For backwards compatibility with old toxic profile names. TODO: remove this some day */ if (rename_old_profile(user_config_dir) == -1) { queue_init_message("Warning: Profile backwards compatibility failed."); } free(user_config_dir); } // this doesn't do anything (yet) #ifdef X11 void DnD_callback(const char *asdv, DropType dt) { // if (dt != DT_plain) // return; // pthread_mutex_lock(&Winthread.lock); // line_info_add(prompt, NULL, NULL, NULL, SYS_MSG, 0, 0, asdv); // pthread_mutex_unlock(&Winthread.lock); } #endif /* X11 */ int main(int argc, char **argv) { parse_args(argc, argv); /* Use the -b flag to enable stderr */ if (!arg_opts.debug) { freopen("/dev/null", "w", stderr); } 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"); } /* Make sure all written files are read/writeable only by the current user. */ umask(S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH); 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_xtra(DnD_callback) == -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_groups(prompt, m); /* thread for ncurses stuff */ if (pthread_mutex_init(&Winthread.lock, NULL) != 0) { exit_toxic_err("failed in main", FATALERR_MUTEX_INIT); } 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 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_primary_device(input, user_settings->audio_in_dev); set_primary_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 */ #ifdef PYTHON init_python(m); invoke_autoruns(prompt->chatwin->history, prompt); #endif /* PYTHON */ init_notify(60, 3000); /* 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); 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 (timed_out(last_save, AUTOSAVE_FREQ)) { pthread_mutex_lock(&Winthread.lock); if (store_data(m, DATA_FILE) != 0) { line_info_add(prompt, NULL, NULL, NULL, SYS_MSG, 0, RED, "WARNING: Failed to save to data file"); } pthread_mutex_unlock(&Winthread.lock); last_save = cur_time; } usleep(tox_iteration_interval(m) * 1000); } return 0; } toxic-0.8.3/src/toxic.h000066400000000000000000000137371336615250000147520ustar00rootroot00000000000000/* 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 #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_RB 0x1D /* ctrl-] */ #define T_KEY_C_LB 0x1B /* ctrl-[ */ #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_TAB 0x09 /* TAB key */ #define ONLINE_CHAR "*" #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 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 groupnumber, 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 *group_pub_key, size_t length, void *userdata); void on_conference_peer_list_changed(Tox *m, uint32_t groupnumber, void *userdata); void on_conference_peer_name(Tox *m, uint32_t groupnumber, uint32_t peernumber, const uint8_t *name, size_t length, void *userdata); void on_conference_title(Tox *m, uint32_t groupnumber, 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); #endif /* TOXIC_H */ toxic-0.8.3/src/toxic_strings.c000066400000000000000000000152371336615250000165130ustar00rootroot00000000000000/* 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 "toxic.h" #include "windows.h" #include "misc_tools.h" #include "toxic_strings.h" #include "notify.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 (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.8.3/src/toxic_strings.h000066400000000000000000000051411336615250000165110ustar00rootroot00000000000000/* 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.8.3/src/video_call.c000066400000000000000000000305431336615250000157120ustar00rootroot00000000000000/* 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 "toxic.h" #include "windows.h" #include "video_call.h" #include "video_device.h" #include "chat_commands.h" #include "global_commands.h" #include "line_info.h" #include "notify.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, NULL, NULL, NULL, SYS_MSG, 0, 0, "%s", error_str); } ToxAV *init_video(ToxWindow *self, Tox *tox) { CallControl.video_errors = ve_None; CallControl.video_enabled = true; CallControl.video_bit_rate = 0; CallControl.video_frame_duration = 10; if (!CallControl.av) { line_info_add(self, NULL, 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, NULL, 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->vout_idx != -1) { close_video_device(vdt_output, this_call->vout_idx); } } 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 (CallControl.video_bit_rate == 0 || this_call->vin_idx == -1) { line_info_add(CallControl.prompt, NULL, 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, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to send video frame"); if (error == TOXAV_ERR_SEND_FRAME_NULL) { line_info_add(CallControl.prompt, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to capture video frame"); } else if (error == TOXAV_ERR_SEND_FRAME_INVALID) { line_info_add(CallControl.prompt, NULL, 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) { 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, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to prepare transmission"); return -1; } CallControl.video_bit_rate = default_video_bit_rate; if (toxav_video_set_bit_rate(CallControl.av, self->num, CallControl.video_bit_rate, NULL) == false) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to set video bit rate"); return -1; } if (open_primary_video_device(vdt_input, &call->vin_idx) != vde_None) { line_info_add(self, NULL, 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, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to register input video handler!"); return -1; } return 0; } int stop_video_transmission(Call *call, int friend_number) { CallControl.video_bit_rate = 0; toxav_video_set_bit_rate(CallControl.av, friend_number, CallControl.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) { 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) { CallControl.video_bit_rate = video_bit_rate; toxav_video_set_bit_rate(CallControl.av, friend_number, CallControl.video_bit_rate, NULL); } void callback_recv_video_starting(uint32_t friend_number) { Call *this_call = &CallControl.calls[friend_number]; if (this_call->vout_idx != -1) { return; } open_primary_video_device(vdt_output, &this_call->vout_idx); } void callback_recv_video_end(uint32_t friend_number) { Call *this_call = &CallControl.calls[friend_number]; close_video_device(vdt_output, this_call->vout_idx); this_call->vout_idx = -1; } void callback_video_starting(uint32_t friend_number) { ToxWindow *windows = CallControl.prompt; 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) { if (windows[i].is_call && windows[i].num == friend_number) { if (0 != start_video_transmission(&windows[i], CallControl.av, this_call)) { line_info_add(&windows[i], NULL, NULL, NULL, SYS_MSG, 0, 0, "Error starting transmission!"); return; } line_info_add(&windows[i], 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_video(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { const char *error_str; Call *this_call = &CallControl.calls[self->num]; if (argc != 0) { error_str = "Unknown arguments."; goto on_error; } if (!CallControl.av) { error_str = "ToxAV not supported!"; goto on_error; } if (!self->stb->connection) { error_str = "Friend is offline."; goto on_error; } if (!self->is_call) { error_str = "Not in call!"; goto on_error; } if (this_call->vin_idx == -1) { callback_video_starting(self->num); } else { callback_video_end(self->num); } return; on_error: print_err(self, error_str); } void cmd_list_video_devices(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { const char *error_str; if (argc != 1) { if (argc < 1) { error_str = "Type must be specified!"; } else { error_str = "Only one argument allowed!"; } goto on_error; } 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, NULL, NULL, NULL, SYS_MSG, 0, 0, "Invalid type: %s", argv[1]); return; } print_video_devices(self, type); return; on_error: print_err(self, error_str); } /* This changes primary video device only */ void cmd_change_video_device(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { const char *error_str; if (argc != 2) { if (argc < 1) { error_str = "Type must be specified!"; } else if (argc < 2) { error_str = "Must have id!"; } else { error_str = "Only two arguments allowed!"; } goto on_error; } 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, NULL, NULL, NULL, SYS_MSG, 0, 0, "Invalid type: %s", argv[1]); return; } char *end; long int selection = strtol(argv[2], &end, 10); if (*end) { error_str = "Invalid input"; goto on_error; } if (set_primary_video_device(type, selection) == vde_InvalidSelection) { error_str = "Invalid selection!"; goto on_error; } return; on_error: print_err(self, error_str); } void cmd_ccur_video_device(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { const char *error_str; if (argc != 2) { if (argc < 1) { error_str = "Type must be specified!"; } else if (argc < 2) { error_str = "Must have id!"; } else { error_str = "Only two arguments allowed!"; } goto on_error; } 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, NULL, NULL, NULL, SYS_MSG, 0, 0, "Invalid type: %s", argv[1]); return; } char *end; long int selection = strtol(argv[2], &end, 10); if (*end) { error_str = "Invalid input"; goto on_error; } if (video_selection_valid(type, selection) == vde_InvalidSelection) { error_str = "Invalid selection!"; goto on_error; } /* If call is active, change device */ if (self->is_call) { Call *this_call = &CallControl.calls[self->num]; if (this_call->ttas) { if (type == vdt_output) { } else { /* TODO: check for failure */ close_video_device(vdt_input, this_call->vin_idx); open_video_device(vdt_input, selection, &this_call->vin_idx); register_video_device_callback(self->num, this_call->vin_idx, read_video_device_callback, &self->num); } } } self->video_device_selection[type] = selection; return; on_error: print_err(self, error_str); } void stop_video_stream(ToxWindow *self) { Call *this_call = &CallControl.calls[self->num]; if (this_call && this_call->vin_idx != -1) { stop_video_transmission(this_call, self->num); } } #endif /* VIDEO */ toxic-0.8.3/src/video_call.h000066400000000000000000000026511336615250000157160ustar00rootroot00000000000000/* 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 stop_video_stream(ToxWindow *self); 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.8.3/src/video_device.c000066400000000000000000000623501336615250000162370ustar00rootroot00000000000000/* 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_device.h" #include "video_call.h" #include #include #include #include #include #if defined(__OSX__) #import "osx_video.h" #else #include #include #include #include #if defined(__OpenBSD__) || defined(__NetBSD__) #include #else #include #endif /* defined(__OpenBSD__) || defined(__NetBSD__) */ #endif /* __OSX__ */ #include "line_info.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__) 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; const char *dvideo_device_names[2]; /* Default device */ const char *video_devices_names[2][MAX_DEVICES]; /* Container of available devices */ static int size[2]; /* Size of above containers */ VideoDevice *video_devices_running[2][MAX_DEVICES] = {{NULL}}; /* Running devices */ 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); pthread_mutex_t video_mutex; bool video_thread_running = true, 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__) 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() #endif /* VIDEO */ { size[vdt_input] = 0; #if defined(__OSX__) if (osx_video_init((char **)video_devices_names[vdt_input], &size[vdt_input]) != 0) { return vde_InternalError; } #else /* not __OSX__*/ 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); 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); 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; 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; usleep(20000); int i; for (i = 0; i < size[vdt_input]; ++i) { free((void *)video_devices_names[vdt_input][i]); } if (pthread_mutex_destroy(&video_mutex) != 0) { return (VideoDeviceError) vde_InternalError; } #ifdef __OSX__ osx_video_release(); #endif /* __OSX__ */ return (VideoDeviceError) vde_None; } VideoDeviceError register_video_device_callback(int32_t friend_number, uint32_t device_idx, VideoDataHandleCallback callback, void *data) { #if defined(__OSX__) if (size[vdt_input] <= device_idx || !video_devices_running[vdt_input][device_idx]) { return vde_InvalidSelection; } #else /* not __OSX__ */ 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) { return open_video_device(type, primary_video_device[type], device_idx); } 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) { 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__) if (osx_video_open_device(selection, &device->video_width, &device->video_height) != 0) { free(device); unlock; return vde_FailedStart; } #else /* not __OSX__*/ /* 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; memset(&(fmt), 0, sizeof(fmt)); fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; 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; memset(&(req), 0, sizeof(req)); 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; memset(&(buf), 0, sizeof(buf)); 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; memset(&(buf), 0, sizeof(buf)); 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); 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) { 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. */ (void)arg; uint32_t i; while (1) { lock; if (!video_thread_running) { unlock; break; } unlock; if (video_thread_paused) { usleep(10000); /* 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__) if (osx_video_read_device(y, u, v, &video_width, &video_height) != 0) { unlock; continue; } #else /* not __OSX__*/ struct v4l2_buffer buf; memset(&(buf), 0, sizeof(buf)); 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__) if (-1 == xioctl(device->fd, VIDIOC_QBUF, &buf)) { unlock; continue; } #endif } unlock; } usleep(1000 * 1000 / 24); } } 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__) osx_video_close_device(device_idx); #else /* not __OSX__ */ 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__) free(device->buffers); #endif /* not __OSX__ */ 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, NULL, 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.8.3/src/video_device.h000066400000000000000000000053221336615250000162400ustar00rootroot00000000000000/* 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); /* Start device */ VideoDeviceError open_video_device(VideoDeviceType type, int32_t selection, uint32_t *device_idx); /* 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.8.3/src/windows.c000066400000000000000000000426441336615250000153100ustar00rootroot00000000000000/* 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 "friendlist.h" #include "prompt.h" #include "toxic.h" #include "windows.h" #include "groupchat.h" #include "chat.h" #include "line_info.h" #include "misc_tools.h" #include "avatars.h" #include "settings.h" #include "file_transfers.h" extern char *DATA_FILE; extern struct Winthread Winthread; ToxWindow *windows[MAX_WINDOWS_NUM]; static uint8_t active_window_index; static int num_active_windows; extern ToxWindow *prompt; extern struct user_settings *user_settings; /* CALLBACKS START */ void on_friend_request(Tox *m, const uint8_t *public_key, const uint8_t *data, size_t length, void *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) { 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); } } } void on_friend_typing(Tox *m, uint32_t friendnumber, bool is_typing, void *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); } } } void on_friend_message(Tox *m, uint32_t friendnumber, Tox_Message_Type type, const uint8_t *string, size_t length, void *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) { 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); } } store_data(m, DATA_FILE); } void on_friend_status_message(Tox *m, uint32_t friendnumber, const uint8_t *string, size_t length, void *userdata) { 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); } } } void on_friend_status(Tox *m, uint32_t friendnumber, Tox_User_Status status, void *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); } } } 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 groupnumber, uint32_t peernumber, Tox_Message_Type type, const uint8_t *message, size_t length, void *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]->onGroupMessage != NULL) { windows[i]->onGroupMessage(windows[i], m, groupnumber, peernumber, type, msg, length); } } } void on_conference_invite(Tox *m, uint32_t friendnumber, Tox_Conference_Type type, const uint8_t *group_pub_key, size_t length, void *userdata) { for (uint8_t i = 0; i < MAX_WINDOWS_NUM; ++i) { if (windows[i] != NULL && windows[i]->onGroupInvite != NULL) { windows[i]->onGroupInvite(windows[i], m, friendnumber, type, (char *) group_pub_key, length); } } } void on_conference_peer_list_changed(Tox *m, uint32_t groupnumber, void *userdata) { for (uint8_t i = 0; i < MAX_WINDOWS_NUM; ++i) { if (windows[i] != NULL && windows[i]->onGroupNameListChange != NULL) { windows[i]->onGroupNameListChange(windows[i], m, groupnumber); } } } void on_conference_peer_name(Tox *m, uint32_t groupnumber, uint32_t peernumber, const uint8_t *name, size_t length, void *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]->onGroupPeerNameChange != NULL) { windows[i]->onGroupPeerNameChange(windows[i], m, groupnumber, peernumber, nick, length); } } } void on_conference_title(Tox *m, uint32_t groupnumber, uint32_t peernumber, const uint8_t *title, size_t length, void *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]->onGroupTitleChange != NULL) { windows[i]->onGroupTitleChange(windows[i], m, groupnumber, peernumber, data, length); } } } void on_file_chunk_request(Tox *m, uint32_t friendnumber, uint32_t filenumber, uint64_t position, size_t length, void *userdata) { struct 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 *user_data) { struct 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, (char *) data, length); } } } void on_file_recv_control(Tox *m, uint32_t friendnumber, uint32_t filenumber, Tox_File_Control control, void *userdata) { struct 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) { /* 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, (char *) filename, filename_length); } } } void on_friend_read_receipt(Tox *m, uint32_t friendnumber, uint32_t receipt, void *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); } } } /* 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 - 2, 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; } } /* Shows next window when tab or back-tab is pressed */ void set_next_window(int ch) { if (ch == user_settings->key_next_tab) { for (uint8_t i = active_window_index + 1; i < MAX_WINDOWS_NUM; ++i) { if (windows[i] != NULL) { set_active_window_index(i); return; } } } 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) { set_active_window_index(i); return; } } } set_active_window_index(0); } /* Deletes window w and cleans up */ void del_window(ToxWindow *w) { set_active_window_index(0); uint8_t idx = w->index; delwin(w->window); free(windows[idx]); windows[idx] = NULL; clear(); refresh(); --num_active_windows; } ToxWindow *init_windows(Tox *m) { 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(); /* equivalent to LINES and COLS */ int x2, y2; getmaxyx(stdscr, y2, x2); y2 -= 2; if (y2 <= 0 || x2 <= 0) { return; } for (uint8_t i = 0; i < MAX_WINDOWS_NUM; ++i) { if (windows[i] == NULL) { continue; } ToxWindow *w = windows[i]; if (windows[i]->is_friendlist) { delwin(w->window); w->window = newwin(y2, x2, 0, 0); continue; } if (w->help->active) { wclear(w->help->win); } if (w->is_groupchat) { delwin(w->chatwin->sidebar); w->chatwin->sidebar = NULL; } else { delwin(w->stb->topline); } delwin(w->chatwin->linewin); delwin(w->chatwin->history); delwin(w->window); w->window = newwin(y2, x2, 0, 0); w->chatwin->linewin = subwin(w->window, CHATBOX_HEIGHT, x2, y2 - CHATBOX_HEIGHT, 0); if (w->show_peerlist) { w->chatwin->history = subwin(w->window, y2 - CHATBOX_HEIGHT + 1, x2 - SIDEBAR_WIDTH - 1, 0, 0); w->chatwin->sidebar = subwin(w->window, y2 - CHATBOX_HEIGHT + 1, SIDEBAR_WIDTH, 0, x2 - SIDEBAR_WIDTH); } else { w->chatwin->history = subwin(w->window, y2 - CHATBOX_HEIGHT + 1, x2, 0, 0); if (!w->is_groupchat) { w->stb->topline = subwin(w->window, 2, x2, 0, 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); } } static void draw_window_tab(ToxWindow *toxwin) { pthread_mutex_lock(&Winthread.lock); if (toxwin->alert != WINDOW_ALERT_NONE) { attron(COLOR_PAIR(toxwin->alert)); } pthread_mutex_unlock(&Winthread.lock); clrtoeol(); printw(" [%s]", toxwin->name); pthread_mutex_lock(&Winthread.lock); if (toxwin->alert != WINDOW_ALERT_NONE) { attroff(COLOR_PAIR(toxwin->alert)); } pthread_mutex_unlock(&Winthread.lock); } static void draw_bar(void) { int y, x; ToxWindow *w = windows[active_window_index]; if (w == NULL) { return; } // save current cursor position getyx(w->window, y, x); attron(COLOR_PAIR(BLUE)); mvhline(LINES - 2, 0, '_', COLS); attroff(COLOR_PAIR(BLUE)); move(LINES - 1, 0); for (uint8_t i = 0; i < MAX_WINDOWS_NUM; ++i) { if (windows[i] == NULL) { continue; } if (i == active_window_index) { #ifdef URXVT_FIX attron(A_BOLD | COLOR_PAIR(GREEN)); } else { #endif attron(A_BOLD); } draw_window_tab(windows[i]); if (i == active_window_index) { #ifdef URXVT_FIX attroff(A_BOLD | COLOR_PAIR(GREEN)); } else { #endif attroff(A_BOLD); } } // restore cursor position after drawing move(y, x); refresh(); } 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; pthread_mutex_unlock(&Winthread.lock); wint_t ch = 0; draw_bar(); touchwin(a->window); a->onDraw(a, m); wrefresh(a->window); /* Handle input */ bool ltr; #ifdef HAVE_WIDECHAR int status = wget_wch(stdscr, &ch); if (status == ERR) { return; } if (status == OK) { ltr = iswprint(ch); } else { /* if (status == KEY_CODE_YES) */ ltr = false; } #else ch = getch(); if (ch == ERR) { return; } /* TODO verify if this works */ ltr = isprint(ch); #endif /* HAVE_WIDECHAR */ if (!ltr && (ch == user_settings->key_next_tab || ch == user_settings->key_prev_tab)) { set_next_window((int) ch); } else { pthread_mutex_lock(&Winthread.lock); a->onKey(a, m, ch, ltr); 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->is_friendlist) { 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; } /* destroys all chat and groupchat windows (should only be called on shutdown) */ void kill_all_windows(Tox *m) { for (uint8_t i = 2; i < MAX_WINDOWS_NUM; ++i) { if (windows[i] == NULL) { continue; } if (windows[i]->is_chat) { kill_chat_window(windows[i], m); } else if (windows[i]->is_groupchat) { free_groupchat(windows[i], m, windows[i]->num); } } /* TODO: use enum instead of magic indices */ kill_friendlist(windows[1]); kill_prompt_window(windows[0]); } toxic-0.8.3/src/windows.h000066400000000000000000000166321336615250000153130ustar00rootroot00000000000000/* 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 16 #define MAX_WINDOW_NAME_LENGTH 22 #define CURS_Y_OFFSET 1 /* y-axis cursor offset for chat contexts */ #define CHATBOX_HEIGHT 2 /* Curses foreground colours (background is black) */ typedef enum { WHITE, GREEN, CYAN, RED, BLUE, YELLOW, MAGENTA, BLACK, } 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 = GREEN, WINDOW_ALERT_1 = RED, WINDOW_ALERT_2 = MAGENTA, } WINDOW_ALERTS; /* Fixes text color problem on some terminals. Uncomment if necessary */ /* #define URXVT_FIX */ struct Winthread { pthread_t tid; pthread_mutex_t lock; volatile sig_atomic_t sig_exit_toxic; volatile sig_atomic_t flag_resize; }; struct cqueue_thread { pthread_t tid; }; struct av_thread { pthread_t tid; }; struct arg_opts { bool use_ipv4; bool force_tcp; 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]; char proxy_address[256]; uint8_t proxy_type; uint16_t proxy_port; uint16_t tcp_port; }; typedef struct ToxWindow ToxWindow; typedef struct StatusBar StatusBar; typedef struct PromptBuf PromptBuf; typedef struct ChatContext ChatContext; typedef struct Help Help; struct ToxWindow { /* ncurses */ void(*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(*onGroupMessage)(ToxWindow *, Tox *, uint32_t, uint32_t, Tox_Message_Type, const char *, size_t); void(*onGroupInvite)(ToxWindow *, Tox *, int32_t, uint8_t, const char *, uint16_t); void(*onGroupNameListChange)(ToxWindow *, Tox *, uint32_t); void(*onGroupPeerNameChange)(ToxWindow *, Tox *, uint32_t, uint32_t, const char *, size_t); void(*onGroupTitleChange)(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 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); int device_selection[2]; /* -1 if not set, if set uses these selections instead of primary device */ bool is_call; int ringing_sound; #ifdef VIDEO int video_device_selection[2]; /* -1 if not set, if set uses these selections instead of primary video device */ #endif /* VIDEO */ #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 */ int x; bool is_chat; bool is_prompt; bool is_friendlist; bool is_groupchat; int show_peerlist; /* used to toggle groupchat peerlist */ WINDOW_ALERTS alert; ChatContext *chatwin; StatusBar *stb; Help *help; WINDOW *window; }; /* 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 groupchat 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); /* refresh inactive windows to prevent scrolling bugs. call at least once per second */ void refresh_inactive_windows(void); #endif /* WINDOWS_H */ toxic-0.8.3/src/xtra.c000066400000000000000000000273051336615250000145710ustar00rootroot00000000000000/* xtra.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 "xtra.h" #include #include #include #include #include #include #include const Atom XtraTerminate = 1; const Atom XtraNil = 0; static Atom XdndAware; static Atom XdndEnter; static Atom XdndLeave; static Atom XdndPosition; static Atom XdndStatus; static Atom XdndDrop; static Atom XdndSelection; static Atom XdndDATA; static Atom XdndTypeList; static Atom XdndActionCopy; static Atom XdndFinished; struct Xtra { drop_callback on_drop; Display *display; Window terminal_window; Window proxy_window; Window source_window; /* When we have a drop */ Atom handling_version; Atom expecting_type; } Xtra; typedef struct Property { unsigned char *data; int read_format; unsigned long read_num; Atom read_type; } Property; static Property read_property(Window s, Atom p) { Atom read_type; int read_format; unsigned long read_num; unsigned long left_bytes; unsigned char *data = NULL; int read_bytes = 1024; /* Keep trying to read the property until there are no bytes unread */ do { if (data) { XFree(data); } XGetWindowProperty(Xtra.display, s, p, 0, read_bytes, False, AnyPropertyType, &read_type, &read_format, &read_num, &left_bytes, &data); read_bytes *= 2; } while (left_bytes != 0); Property property = {data, read_format, read_num, read_type}; return property; } static Atom get_dnd_type(long *a, int l) { int i = 0; for (; i < l; i ++) { if (a[i] != XtraNil) { return a[i]; /* Get first valid */ } } return XtraNil; } /* TODO maybe support only certain types in the future */ static void handle_xdnd_enter(XClientMessageEvent *e) { Xtra.handling_version = (e->data.l[1] >> 24); if ((e->data.l[1] & 1)) { // Fetch the list of possible conversions Property p = read_property(e->data.l[0], XdndTypeList); Xtra.expecting_type = get_dnd_type((long *)p.data, p.read_num); XFree(p.data); } else { // Use the available list Xtra.expecting_type = get_dnd_type(e->data.l + 2, 3); } } static void handle_xdnd_position(XClientMessageEvent *e) { XEvent ev = { .xclient = { .type = ClientMessage, .display = e->display, .window = e->data.l[0], .message_type = XdndStatus, .format = 32, .data = { .l = { Xtra.proxy_window, (Xtra.expecting_type != XtraNil), 0, 0, XdndActionCopy } } } }; XSendEvent(Xtra.display, e->data.l[0], False, NoEventMask, &ev); XFlush(Xtra.display); } static void handle_xdnd_drop(XClientMessageEvent *e) { /* Not expecting any type */ if (Xtra.expecting_type == XtraNil) { XEvent ev = { .xclient = { .type = ClientMessage, .display = e->display, .window = e->data.l[0], .message_type = XdndFinished, .format = 32, .data = { .l = {Xtra.proxy_window, 0, 0} } } }; XSendEvent(Xtra.display, e->data.l[0], False, NoEventMask, &ev); } else { Xtra.source_window = e->data.l[0]; XConvertSelection(Xtra.display, XdndSelection, Xtra.expecting_type, XdndSelection, Xtra.proxy_window, Xtra.handling_version >= 1 ? e->data.l[2] : CurrentTime); } } static void handle_xdnd_selection(XSelectionEvent *e) { /* DnD succesfully finished, send finished and call callback */ XEvent ev = { .xclient = { .type = ClientMessage, .display = Xtra.display, .window = Xtra.source_window, .message_type = XdndFinished, .format = 32, .data = { .l = {Xtra.proxy_window, 1, XdndActionCopy} } } }; XSendEvent(Xtra.display, Xtra.source_window, False, NoEventMask, &ev); Property p = read_property(Xtra.proxy_window, XdndSelection); DropType dt; if (strcmp(XGetAtomName(Xtra.display, p.read_type), "text/uri-list") == 0) { dt = DT_file_list; } else { /* text/uri-list */ dt = DT_plain; } /* Call callback for every entry */ if (Xtra.on_drop && p.read_num) { char *sptr; char *str = strtok_r((char *) p.data, "\n\r", &sptr); if (str) { Xtra.on_drop(str, dt); } while ((str = strtok_r(NULL, "\n\r", &sptr))) { Xtra.on_drop(str, dt); } } if (p.data) { XFree(p.data); } } void *event_loop(void *p) { /* Handle events like a real nigga */ (void) p; /* DINDUNOTHIN */ XEvent event; int pending; while (Xtra.display) { /* NEEDMOEVENTSFODEMPROGRAMS */ XLockDisplay(Xtra.display); if ((pending = XPending(Xtra.display))) { XNextEvent(Xtra.display, &event); } if (!pending) { XUnlockDisplay(Xtra.display); usleep(10000); continue; } if (event.type == ClientMessage) { Atom type = event.xclient.message_type; if (type == XdndEnter) { handle_xdnd_enter(&event.xclient); } else if (type == XdndPosition) { handle_xdnd_position(&event.xclient); } else if (type == XdndDrop) { handle_xdnd_drop(&event.xclient); } else if (type == XtraTerminate) { break; } } else if (event.type == SelectionNotify) { handle_xdnd_selection(&event.xselection); } /* AINNOBODYCANHANDLEDEMEVENTS*/ else { XSendEvent(Xtra.display, Xtra.terminal_window, 0, 0, &event); } XUnlockDisplay(Xtra.display); } /* Actual XTRA termination * Please call xtra_terminate() at exit * otherwise HEWUSAGUDBOI happens */ if (Xtra.display) { XCloseDisplay(Xtra.display); } return (Xtra.display = NULL); } static long unsigned int focused_window_id(void) { if (!Xtra.display) { return 0; } Window focus; int revert; XLockDisplay(Xtra.display); XGetInputFocus(Xtra.display, &focus, &revert); XUnlockDisplay(Xtra.display); return focus; } int is_focused(void) { return Xtra.proxy_window == focused_window_id() || Xtra.terminal_window == focused_window_id(); } int init_xtra(drop_callback d) { if (!d) { return -1; } else { Xtra.on_drop = d; } XInitThreads(); if (!(Xtra.display = XOpenDisplay(NULL))) { return -1; } Xtra.terminal_window = focused_window_id(); /* OSX: if focused window is 0, it means toxic is ran from * native terminal and not X11 terminal window, silently exit */ if (!Xtra.terminal_window) { return 0; } { /* Create an invisible window which will act as proxy for the DnD operation. */ XSetWindowAttributes attr = {0}; attr.event_mask = EnterWindowMask | LeaveWindowMask | ButtonMotionMask | ButtonPressMask | ButtonReleaseMask | ResizeRedirectMask; attr.do_not_propagate_mask = NoEventMask; Window root; int x, y; unsigned int wht, hht, b, d; /* Since we cannot capture resize events for parent window we will have to create * this window to have maximum size as defined in root window */ XGetGeometry(Xtra.display, XDefaultRootWindow(Xtra.display), &root, &x, &y, &wht, &hht, &b, &d); if (!(Xtra.proxy_window = XCreateWindow (Xtra.display, Xtra.terminal_window, /* Parent */ 0, 0, /* Position */ wht, hht, /* Width + height */ 0, /* Border width */ CopyFromParent, /* Depth */ InputOnly, /* Class */ CopyFromParent, /* Visual */ CWEventMask | CWCursor, /* Value mask */ &attr))) { /* Attributes for value mask */ return -1; } } XMapWindow(Xtra.display, Xtra.proxy_window); /* Show window (sandwich) */ XLowerWindow(Xtra.display, Xtra.proxy_window); /* Don't interfere with parent lmao */ XdndAware = XInternAtom(Xtra.display, "XdndAware", False); XdndEnter = XInternAtom(Xtra.display, "XdndEnter", False); XdndLeave = XInternAtom(Xtra.display, "XdndLeave", False); XdndPosition = XInternAtom(Xtra.display, "XdndPosition", False); XdndStatus = XInternAtom(Xtra.display, "XdndStatus", False); XdndDrop = XInternAtom(Xtra.display, "XdndDrop", False); XdndSelection = XInternAtom(Xtra.display, "XdndSelection", False); XdndDATA = XInternAtom(Xtra.display, "XdndDATA", False); XdndTypeList = XInternAtom(Xtra.display, "XdndTypeList", False); XdndActionCopy = XInternAtom(Xtra.display, "XdndActionCopy", False); XdndFinished = XInternAtom(Xtra.display, "XdndFinished", False); /* Inform my nigga windows that we are aware of dnd */ Atom XdndVersion = 3; XChangeProperty(Xtra.display, Xtra.proxy_window, XdndAware, XA_ATOM, 32, PropModeReplace, (unsigned char *)&XdndVersion, 1); pthread_t id; if (pthread_create(&id, NULL, event_loop, NULL) != 0) { return -1; } pthread_detach(id); return 0; } void terminate_xtra(void) { if (!Xtra.display || !Xtra.terminal_window) { return; } XEvent terminate = { .xclient = { .type = ClientMessage, .display = Xtra.display, .message_type = XtraTerminate, } }; XLockDisplay(Xtra.display); XDeleteProperty(Xtra.display, Xtra.proxy_window, XdndAware); XSendEvent(Xtra.display, Xtra.proxy_window, 0, NoEventMask, &terminate); XUnlockDisplay(Xtra.display); while (Xtra.display); /* Wait for termination */ } toxic-0.8.3/src/xtra.h000066400000000000000000000021161336615250000145670ustar00rootroot00000000000000/* xtra.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 XTRA_H #define XTRA_H /* NOTE: If no xlib present don't compile */ typedef enum { DT_plain, DT_file_list } DropType; typedef void (*drop_callback)(const char *, DropType); int init_xtra(drop_callback d); void terminate_xtra(void); int is_focused(void); /* returns bool */ #endif /* XTRA_H */