diff --git a/.gitignore b/.gitignore index a632a291..73091141 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,6 @@ config.log config.status configure debian -include INSTALL libtool libtool.m4 @@ -21,3 +20,5 @@ Makefile Makefile.in pidgin win32-install-dir +.idea +cmake-build-debug diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 4f72f397..00000000 --- a/.travis.yml +++ /dev/null @@ -1,69 +0,0 @@ -language: c - -dist: xenial - -notifications: - email: false - -os: - - linux - -compiler: - - gcc - -env: - global: - - secure: "de18DuRkeapGDbP+1u4+5lgUTeZeVdZVKZdeULv2PbArf+XjAGp1XIRPCLQ0tmrmGZuOb4Py8Hm+mrFgmli+maoUV8dxqN0oLMFgcRyAzwHfHohMNNLn+0VTmxc2S36EHx0iYzPcLU8xADv2fWgG008Ql093UlPivTHB26uC22hdX8HBkF2mtLtlSPrWtJk2Ojr53hCZZCiw0b5SpgFtvcPHkDbmp54XlK4fSGyX0rcLaQHcJZxe7A0hFE4tEB+exxp4UgBckeP7gEQLwmw6XXwt/vQcNXy75qtw0RvrLvnJDMZZM2cBbBcHug+9ORNS5WrSTcJ16Onjq+qZBC++hniJHPtLFqLymmvVt3+qLhSlOyFIxPCPCUMenOiXI1rHyMgAiZFJDwFeInm97rBnS0CKUuZ7/x+8kK/T8pSqjGLXczJ5SDhBZvaBrkyrqB9lGmDFPTK9+v/XwZl6FZncAT1CqlU070ZsfRQJlx8K41OJ534nUaxutPG32BY+2AODmmusYygVVXI0yF6ry5nuWsV6RqBa5wrg8ZQx64yYxk7BPsIpKvCiiBeHfkHZt7C4CDdrQD6+kQVuB13TXI74Uv2olzp3RSRmii8eCU9RtCZeF7GmYFbEG5PPYX5kFKBn4u90ctVBc7Sb5jWOqIMaYjCtmT5+B6lRPrUeemh4P2o=" - - secure: "lgKD/7KlClD77CLnQzvIuIsdDhTwACtRzL0vuqIGBlyclTO+UVC7vIQ6RyOt1Jab9Vg77xItsyRgZqbjDnE/4Vml614rrSfPAaHCBCvf3Z9j7/oqv0maYmX7bhN3/nbkru2LeQMQENZO5CyhYpHcoqPfqEwdryOx4TUj3Tt75QjCfzWr5IM4g8327M+In8NhoovwI+5FMGZUTJZxyAAD8KgdCxnRk8U2c2M096eDObilwEQoD69gM/ZJ8aAyGZkKmGbAkLP8bSx9u6DYAiT3vhRJwopX99u1Y4V8zEik9tqFZYyWlJLrd0wz4EkAN6jdG4rbhyZ33jVr2zMsAnqMeVnB0tLdw7ajWTSOHMwqyK+S8GMDZ87iwQknmE8Zn9wghtJdHTOTJGScFUkS35vOjBXPg2zB6FKP2RAioRBYQfnSV5PWWfADj2j7X6hEsCqO+rdOGq/GPQcRBiveAalq4vcciAaVr01xjU1d7dBSGiMMLWtnWWJfcCwDsJ8lhcNbDXSrfDXRDmO7OsTXzP9Dzc6moJdmOmJVCcdXPjYJGuDHPsDd9qRlx0wXMKzKrHIf/fgwx304EA68OA4j5WkEKcqYYlhzjTG83CKAwLP/8gQ8gsgOeeDeCEyKHN/bmr8Jb7kDYHat34NElEwYpW558rE1U7cNxQtyzaDN7nY/9dg=" - - secure: "WF5OAKWYgKdPO7H6UuLoDupfJyjzJlVxXd3IQR/Dq6eytDF5k5tzsRkqE8BwbpmX51bgbWAhxU+9yeyb7GDNp6koIDltgMYCqt7KZsstKjz7fHSqkjqxmzzXA3reKO/iOz7eddPPrhsliNpsx7iEC8/HWE3hXuyL68SlPzLpFK1h4o2rJMPF9VNmS1hruY21qiuEltxEmqYMR9skMKfddebNChp23pza8i6MWkBIMDsAiZ0ek5WVerWHM8+5/NZx/kKPG0wDThvKsB5lLF8OqlsbBO2AyQMxvQVxHgx6rK3y9GOPiAdVyJHPQHk66hFDkWCkak5xTcbbOJ0P8Qou5A+c/BM9Cyy2W6m4M3BboOD9oso49veAF7p6bcL/QZwEUr/MlJFGfWez9UQaRfESn3kipF/Jhct6OOk+M50DBrVsHgL5ph8y8GJiU7xpKOHeUjM5iB4GJimjotR9u3YYxszbQxjpQ0MG2vnTUNQ1YEnTJ384Y+OQI9KXCjhmxfgymZ3/UyZd9EPnfc8gi2SCr8+xtogA4Bxkz12EEM9vdS195I0dot0OQYlYpZL0HsBMEirsKr049mX6A+Ca8WKUVPqZnqNfbpg80G/F/+hToGTkFoUmOCkpIhwvJb1zMbMLHQhDv1k5YqIcIMg8dY1sxO2IvVsueDBSEqhtZUPNxgQ=" - - secure: "QOIE9TZzTYgFfMPVhwvOEo8buxU0s6/a2+vBsOprAw94juhafpPyiBEUzqyzVLJX+ZY3aVt+s6ch4y4ehEwmyWnqJzxKg6CIy3dGLaBEW+GvZDVtTj1rC3Q/bkPB/MrHkChv08o1Ysb8yQ0gF0GWS+wUQ3m58qgKYMH/ve0Tt76AMBxVU548j1CdKKF1SO6bedCDhX8txiB4MeYWV6pfripYIMcjL5w3fHN8LIw/ctc8APPr2owIJ6rxv8scQXfWWpgCJVxHCZaP7TP5HwLnjFPX+UNpxlH7ZDm+opA9clep5Q1SDmDgUcXT3aMXjRulbzJxcKXXiVNfByEm+Gu85W+c0ka0rXvcKtRZmpwprbiBnMQ+bP/1MqicNkjCLDT/EFMzX0Bo2Kzwu/9Wc/VdAohnIWq03NUsErTL8CGmkI5QhCBJ4nBY1u87wpkOghq95woqxhiC7VI+qV26u0JNBf3miUD5nUfSTS1Ym7BoeEgKFUViDeX7P7LMzKoGfM0+Clutpemq0FuLRP+Ez+4a//UhY42TxUAfdPUCPmGrcm0DlHAa1cZYa2jPcTHOB7trvk31AUgNFCTa36SbLeEym8Z68Z8/8XOLw7Hej8wYSewmc/xwXAq3jNOXh9DAKc4ktOv7EC8fmdCM8qzUDkOzkEcG+tOB7DNPHQi/ZqoNdMk=" - - MY_DEPLOY_BRANCH=master - - pidgindir=/tmp/pidgin - -before_install: - - echo "deb http://download.opensuse.org/repositories/openSUSE:/Tools/xUbuntu_$(lsb_release -rs) ./" - | sudo tee /etc/apt/sources.list.d/suse.list - - curl -s "http://download.opensuse.org/repositories/openSUSE:/Tools/xUbuntu_$(lsb_release -rs)/Release.key" - | sudo apt-key add - - - sudo apt-get update -qq - - sudo apt-get install -qq - --no-install-recommends - binutils-mingw-w64-i686 - clang - gcc-mingw-w64-i686 - libjson-glib-dev - libpurple-dev - osc - - if [ "x$TRAVIS_EVENT_TYPE" != "xpull_request" ]; then - mkdir -p ~/.ssh; - openssl aes-256-cbc - -K $encrypted_1a8668021c4a_key - -iv $encrypted_1a8668021c4a_iv - -in .travis/key -out ~/.ssh/id_rsa -d; - chmod 600 ~/.ssh/id_rsa; - fi - -install: - - .travis/win32-install.sh - -before_script: - - ./update.sh - -script: - - CFLAGS="-Werror" ./autogen.sh --enable-warnings - - make - - make clean - - scan-build -k --status-bugs make - - .travis/win32-build.sh - -after_success: - - if [ "x$TRAVIS_EVENT_TYPE" != "xpull_request" ]; then - .travis/win32-deploy.sh; - .travis/obs.sh; - fi - -after_script: - - if [ "x$TRAVIS_EVENT_TYPE" != "xpull_request" ]; then - shred ~/.ssh/id_rsa; - rm -f ~/.ssh/id_rsa; - fi diff --git a/.travis/key b/.travis/key deleted file mode 100644 index 895439cd..00000000 Binary files a/.travis/key and /dev/null differ diff --git a/.travis/obs.sh b/.travis/obs.sh deleted file mode 100755 index 197cddc7..00000000 --- a/.travis/obs.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/bash - -[ "${TRAVIS_PULL_REQUEST}" == "false" -a \ - "${TRAVIS_BRANCH}" == "${MY_DEPLOY_BRANCH}" \ -] || exit -set -e - -GITREV=$(git rev-parse --short=7 HEAD) -FULLVERS="$(date +%Y%m%d)-$(cat RELEASE_VERSION)-${GITREV}-${TRAVIS_BUILD_NUMBER}" -FULLVERS_RPM="$(echo ${FULLVERS} | sed 's/-/~/g')" -FULLDATE=$(date -R) -REPONAME=$(basename "${TRAVIS_REPO_SLUG}") - -git reset -q --hard -git clean -dfqx -./update.sh - -sed -ri \ - -e "20 s/^(\s+).*(,)\$/\1\[${FULLVERS}\]\2/" \ - configure.ac -sed -ri \ - -e "s/(^Build-Depends:.*)/\1, libzephyr4/" \ - debian/control -sed -ri \ - -e "s/(^%setup -q.*)/\1 -n %\{name\}/" \ - -e "s/(^Source0:.*)\-(.*)/\1_\2/" \ - -e "s/(^Version:).*/\1 ${FULLVERS_RPM}/" \ - dist/*.spec - -cat < debian/changelog -${REPONAME} (${FULLVERS}) UNRELEASED; urgency=medium - - * Updated to ${FULLVERS}. - - -- Travis CI ${FULLDATE} -EOF - -cat < ~/.oscrc -[general] -apiurl = https://api.opensuse.org -[https://api.opensuse.org] -user = ${OBSUSER} -pass = ${OBSPASS} -EOF - -mkdir -p m4 -osc checkout "home:${OBSUSER}" "${REPONAME}" -o /tmp/obs - -( - cd /tmp/obs - rm -f *.{dsc,tar.gz} - dpkg-source -I -b "${TRAVIS_BUILD_DIR}" - cp "${TRAVIS_BUILD_DIR}/dist/_service" . - - osc addremove -r - osc commit -m "Updated to ${FULLVERS}" -) diff --git a/.travis/win32-build.sh b/.travis/win32-build.sh deleted file mode 100755 index 992a8a2c..00000000 --- a/.travis/win32-build.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -set -e - -GITREV=$(git rev-parse --short=7 HEAD) -FULLVERS="$(date +%Y%m%d)-$(cat RELEASE_VERSION)-${GITREV}-${TRAVIS_BUILD_NUMBER}" - -CC="i686-w64-mingw32-gcc" \ -DLL_LD_FLAGS="-static-libgcc" \ -make -f Makefile.mingw \ - install \ - GLIB_GENMARSHAL="glib-genmarshal" \ - PLUGIN_VERSION="${FULLVERS}" \ - WIN32_TREE_TOP="win32-dev/pidgin-2.10.11" diff --git a/.travis/win32-deploy.sh b/.travis/win32-deploy.sh deleted file mode 100755 index 85800ac2..00000000 --- a/.travis/win32-deploy.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -[ "${TRAVIS_PULL_REQUEST}" == "false" -a \ - "${TRAVIS_BRANCH}" == "${MY_DEPLOY_BRANCH}" \ -] || exit -set -e - -sftp -qo StrictHostKeyChecking=no -P ${SSHPORT} ${SSHUSER} -b <> ${MARSHAL_H} + COMMENT "Generating marshal.h" +) + +add_custom_command( + OUTPUT ${MARSHAL_C} + COMMAND glib-genmarshal --prefix=fb_marshal --body ${CMAKE_SOURCE_DIR}/facebook/marshaller.list >> ${MARSHAL_C} + COMMENT "Generating marshal.c" +) + + +add_library(facebook SHARED + ${MARSHAL_H} + ${MARSHAL_C} + facebook/api.c + facebook/data.c + facebook/facebook.c + facebook/http.c + facebook/id.h + facebook/json.c + facebook/mqtt.c + facebook/thrift.c + facebook/util.c + purple2compat/http.c + purple2compat/purple-socket.c + ) + +add_definitions(${GLIB_CFLAGS_OTHER} + ${JSON-GLIB_CFLAGS_OTHER} + ${PURPLE_CFLAGS_OTHER} + ${ZLIB_CFLAGS_OTHER} + ) + +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DPURPLE_PLUGINS -include purple-compat.h") + +add_definitions(-DPACKAGE_VERSION=\"${PACKAGE_VERSION}\") +add_definitions(-DPACKAGE_URL=\"${PACKAGE_URL}\") + +target_link_libraries(facebook + ${GLIB_LIBRARIES} + ${JSON-GLIB_LIBRARIES} + ${GIO_LIBRARIES} + ${GOBJECT_LIBRARIES} + ${PURPLE_LIBRARIES} + ${ZLIB_LIBRARIES}) + +if(WIN32) + target_link_libraries(facebook ws2_32) +endif() \ No newline at end of file diff --git a/MANIFEST_PIDGIN b/MANIFEST_PIDGIN deleted file mode 100644 index d6d934c7..00000000 --- a/MANIFEST_PIDGIN +++ /dev/null @@ -1,25 +0,0 @@ -AUTHORS -COPYING -COPYRIGHT -libpurple/glibcompat.h -libpurple/http.c -libpurple/http.h -libpurple/protocols/facebook/api.c -libpurple/protocols/facebook/api.h -libpurple/protocols/facebook/data.c -libpurple/protocols/facebook/data.h -libpurple/protocols/facebook/facebook.c -libpurple/protocols/facebook/facebook.h -libpurple/protocols/facebook/http.c -libpurple/protocols/facebook/http.h -libpurple/protocols/facebook/id.h -libpurple/protocols/facebook/json.c -libpurple/protocols/facebook/json.h -libpurple/protocols/facebook/Makefile.am -libpurple/protocols/facebook/Makefile.mingw -libpurple/protocols/facebook/mqtt.c -libpurple/protocols/facebook/mqtt.h -libpurple/protocols/facebook/thrift.c -libpurple/protocols/facebook/thrift.h -libpurple/protocols/facebook/util.c -libpurple/protocols/facebook/util.h diff --git a/MANIFEST_VOIDS b/MANIFEST_VOIDS deleted file mode 100644 index b1b08a6e..00000000 --- a/MANIFEST_VOIDS +++ /dev/null @@ -1,12 +0,0 @@ -include/blistnode.h -include/buddy.h -include/buddylist.h -include/conversations.h -include/conversationtypes.h -include/image.h -include/image-store.h -include/message.h -include/plugins.h -include/presence.h -include/protocol.h -include/protocols.h diff --git a/Makefile.am b/Makefile.am deleted file mode 100644 index 9e638c50..00000000 --- a/Makefile.am +++ /dev/null @@ -1,17 +0,0 @@ -ACLOCAL_AMFLAGS = -Im4 -SUBDIRS = pidgin/libpurple/protocols/facebook - -EXTRA_DIST = \ - autogen.sh \ - debian \ - include \ - Makefile.mingw \ - MANIFEST_PIDGIN \ - MANIFEST_VOIDS \ - patches \ - pidgin/AUTHORS \ - pidgin/COPYING \ - pidgin/COPYRIGHT \ - update.sh \ - VERSION \ - RELEASE_VERSION diff --git a/Makefile.mingw b/Makefile.mingw deleted file mode 100644 index ecc91208..00000000 --- a/Makefile.mingw +++ /dev/null @@ -1,66 +0,0 @@ -PLUGIN_VERSION = $(shell cat RELEASE_VERSION) -PURPLE_VERSION = 2.10.11 - -TREE_TOP = ../../../.. -PLUGIN_TOP = pidgin/libpurple/protocols/facebook -WIN32_TREE_TOP := ../win32-dev/pidgin-$(PURPLE_VERSION) - -PIDGIN_TREE_TOP = $(TREE_TOP)/$(WIN32_TREE_TOP) -WIN32_DEV_TOP = $(PIDGIN_TREE_TOP)/.. -PURPLE_TOP = $(TREE_TOP)/$(WIN32_TREE_TOP)/libpurple -PURPLE_INSTALL_PLUGINS_DIR = $(TREE_TOP)/win32-install-dir/plugins - -GTK_TOP = $(WIN32_DEV_TOP)/glib-2.28.8 -GTK_BIN = $(GTK_TOP)/bin -GLIB_GENMARSHAL = $(GTK_BIN)/glib-genmarshal -JSON_GLIB_TOP = $(WIN32_DEV_TOP)/json-glib-0.14 - -CFLAGS = -O0 -g -export CFLAGS += -include purple-compat.h - -export DEFINES = $(AUTOTOOLS_DEFINES) \ - -DPACKAGE_NAME=\"purple-facebook\" \ - -DPACKAGE_TARNAME=\"purple-facebook\" \ - -DPACKAGE_VERSION=\"$(PLUGIN_VERSION)\" \ - -DPACKAGE_STRING=\"purple-facebook\ $(PLUGIN_VERSION)\" \ - -DPACKAGE_BUGREPORT=\"https://github.com/dequis/purple-facebook/issues\" \ - -DPACKAGE_URL=\"https://github.com/dequis/purple-facebook\" \ - -DPACKAGE=\"purple-facebook\" \ - -DVERSION=\"$(PLUGIN_VERSION)\" \ - -DPURPLE_PLUGINS - -export INCLUDE_PATHS = \ - -I$(TREE_TOP)/include \ - -I$(TREE_TOP)/pidgin \ - -I$(TREE_TOP)/pidgin/libpurple - -export LIB_PATHS = \ - -L$(PIDGIN_TREE_TOP)/win32-install-dir - -override PLUGIN_MAKE = \ -$(MAKE) -C $(PLUGIN_TOP) -f Makefile.mingw $(@) \ - PURPLE_VERSION="$(PURPLE_VERSION)" \ - PIDGIN_TREE_TOP="$(PIDGIN_TREE_TOP)" \ - WIN32_DEV_TOP="$(WIN32_DEV_TOP)" \ - PLUGIN_TOP="$(PLUGIN_TOP)" \ - PURPLE_TOP="$(PURPLE_TOP)" \ - PURPLE_INSTALL_PLUGINS_DIR="$(PURPLE_INSTALL_PLUGINS_DIR)" \ - GPLUGIN_TOP="." \ - GTK_TOP="$(GTK_TOP)" \ - GTK_BIN="$(GTK_BIN)" \ - GLIB_GENMARSHAL="$(GLIB_GENMARSHAL)" \ - JSON_GLIB_TOP="$(JSON_GLIB_TOP)" - -.PHONY: all install clean - -all: - $(call PLUGIN_MAKE,all) - -$(PURPLE_INSTALL_PLUGINS_DIR): - cd $(PLUGIN_TOP) && mkdir -p $(PURPLE_INSTALL_PLUGINS_DIR) - -install: $(PURPLE_INSTALL_PLUGINS_DIR) - $(call PLUGIN_MAKE,install) - -clean: - $(call PLUGIN_MAKE,clean) diff --git a/RELEASE_VERSION b/RELEASE_VERSION deleted file mode 100644 index 85b7c695..00000000 --- a/RELEASE_VERSION +++ /dev/null @@ -1 +0,0 @@ -0.9.6 diff --git a/VERSION b/VERSION deleted file mode 100644 index 7a8181f5..00000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -9ff9acf9fa14 diff --git a/autogen.sh b/autogen.sh deleted file mode 100755 index f5c04909..00000000 --- a/autogen.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh - -test -z "$srcdir" && srcdir=$(dirname "$0") -test -z "$srcdir" && srcdir=. - -cwd=$(pwd) -cd "$srcdir" - -./update.sh || exit $? - -mkdir -p m4 -autoreconf --verbose --force --install || exit $? - -cd "$cwd" -test -z "$NOCONFIGURE" && "$srcdir/configure" $@ diff --git a/configure.ac b/configure.ac deleted file mode 100644 index db3ed51e..00000000 --- a/configure.ac +++ /dev/null @@ -1,112 +0,0 @@ -# Copyright 2015-2016 James Geboski -# -# 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 2 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 . - -AC_PREREQ([2.64]) - -AC_INIT( - [purple-facebook], - m4_esyscmd_s([cat RELEASE_VERSION]), - [https://github.com/dequis/purple-facebook/issues], - [purple-facebook], - [https://github.com/dequis/purple-facebook], - [] -) - -AC_CONFIG_AUX_DIR([build-aux]) -AC_CONFIG_MACRO_DIR([m4]) -AM_INIT_AUTOMAKE([subdir-objects]) - -AC_PROG_CC -AM_PROG_CC_C_O - -AC_DISABLE_STATIC -AC_PROG_LIBTOOL - -m4_ifdef([AM_SILENT_RULES], [AM_SILENT_RULES([yes])]) - -# Define PKG_CHECK_VAR() for pkg-config < 0.28 -m4_define_default( - [PKG_CHECK_VAR], - [AC_ARG_VAR([$1], [value of $3 for $2, overriding pkg-config]) - AS_IF([test -z "$$1"], [$1=`$PKG_CONFIG --variable="$3" "$2"`]) - AS_IF([test -n "$$1"], [$4], [$5])] -) - -AC_ARG_ENABLE( - [warnings], - [AS_HELP_STRING( - [--enable-warnings], - [Enable additional compile-time (GCC) warnings] - )], - [WARNINGS="yes"], - [WARNINGS="no"] -) - -AS_IF( - [test "x$WARNINGS" == "xyes"], - [CFLAGS="$CFLAGS -Wall -Wextra \ - -Waggregate-return \ - -Wdeclaration-after-statement \ - -Wfloat-equal \ - -Wformat \ - -Winit-self \ - -Wmissing-declarations \ - -Wmissing-prototypes \ - -Wno-unused-parameter \ - -Wpointer-arith"] -) - -AC_ARG_WITH( - [plugindir], - [AS_HELP_STRING( - [--with-plugindir], - [libpurple plugin directory] - )], - [PURPLE_PLUGINDIR="$with_plugindir"] -) - -PKG_CHECK_MODULES([GLIB], [glib-2.0 >= 2.28.0 gio-2.0 gobject-2.0]) -PKG_CHECK_MODULES([JSON], [json-glib-1.0 >= 0.14.0]) -PKG_CHECK_MODULES([PURPLE], [purple < 3]) -PKG_CHECK_MODULES([ZLIB], [zlib]) - -PKG_CHECK_VAR([GLIB_GENMARSHAL], [glib-2.0], [glib_genmarshal]) -AS_IF( - [test -z "$GLIB_GENMARSHAL"], - [AC_MSG_ERROR([The `glib-genmarshal' tool is missing.])] -) - -AS_IF( - [test -z "$PURPLE_PLUGINDIR"], - [PKG_CHECK_VAR( - [PURPLE_PLUGINDIR], - [purple], - [plugindir], - [PURPLE_PLUGINDIR="$PURPLE_PLUGINDIR"], - [PURPLE_PLUGINDIR="$libdir/purple-2"] - )] -) - -PLUGIN_CFLAGS="-I`pwd`/$srcdir/include -I`pwd`/$srcdir/pidgin " -PLUGIN_CFLAGS="${PLUGIN_CFLAGS} -I`pwd`/$srcdir/pidgin/libpurple" -PLUGIN_CFLAGS="$PLUGIN_CFLAGS -DPURPLE_PLUGINS -include purple-compat.h" -AC_SUBST([PLUGIN_CFLAGS]) - -AM_CONDITIONAL([STATIC_FACEBOOK], false) -AC_SUBST([PLUGIN_LDFLAGS], [-avoid-version]) -AC_SUBST([PURPLE_LIBS], ["$GLIB_LIBS $JSON_LIBS $PURPLE_LIBS $ZLIB_LIBS"]) - -AC_CONFIG_FILES([Makefile pidgin/libpurple/protocols/facebook/Makefile]) -AC_OUTPUT diff --git a/debian/changelog b/debian/changelog deleted file mode 100644 index 3ee2b598..00000000 --- a/debian/changelog +++ /dev/null @@ -1,5 +0,0 @@ -purple-facebook (0.0.0-1) UNRELEASED; urgency=medium - - * Initial debian support. - - -- jgeboski Thu, 01 Jan 1970 00:00:00 +0000 diff --git a/debian/compat b/debian/compat deleted file mode 100644 index ec635144..00000000 --- a/debian/compat +++ /dev/null @@ -1 +0,0 @@ -9 diff --git a/debian/control b/debian/control deleted file mode 100644 index ce127ff3..00000000 --- a/debian/control +++ /dev/null @@ -1,18 +0,0 @@ -Source: purple-facebook -Maintainer: jgeboski -Section: misc -Priority: optional -Standards-Version: 3.9.6 -Build-Depends: debhelper (>= 9), dh-autoreconf, libglib2.0-dev (>= 2.28), libjson-glib-dev (>= 0.14), libpurple-dev -Homepage: https://github.com/jgeboski/purple-facebook - -Package: purple-facebook -Architecture: any -Section: misc -Priority: optional -Depends: ${shlibs:Depends}, ${misc:Depends}, libglib2.0-0 (>= 2.28), libjson-glib-1.0-0 (>= 0.14), libpurple0 -Homepage: https://github.com/jgeboski/purple-facebook -Description: Facebook protocol plugin for libpurple - Purple Facebook implements the Facebook Messenger protocol into - pidgin, finch, and libpurple. While the primary implementation is for - purple3, this plugin is back-ported for purple2. diff --git a/debian/copyright b/debian/copyright deleted file mode 100644 index 67cb7edc..00000000 --- a/debian/copyright +++ /dev/null @@ -1,27 +0,0 @@ -Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ -Upstream-Name: purple-facebook -Source: https://github.com/jgeboski/purple-facebook - -Files: * -Copyright: Copyright 2015-2016 James Geboski -License: GPL-2+ - 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 2 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 package; if not, write to the Free - Software Foundation, Inc., 51 Franklin St, Fifth Floor, - Boston, MA 02110-1301 USA - . - On Debian systems, the full text of the GNU General Public - License version 2 can be found in the file - `/usr/share/common-licenses/GPL-2'. diff --git a/debian/rules b/debian/rules deleted file mode 100755 index bd66c750..00000000 --- a/debian/rules +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/make -f - -override_dh_auto_install: - dh_auto_install - find debian -name '*.la' -print -delete - -%: - dh $@ --with autoreconf diff --git a/dist/_service b/dist/_service deleted file mode 100644 index 705265cc..00000000 --- a/dist/_service +++ /dev/null @@ -1,6 +0,0 @@ - - - *.tar.gz - */dist/*.spec - - diff --git a/dist/purple-facebook.spec b/dist/purple-facebook.spec deleted file mode 100644 index 401ef15e..00000000 --- a/dist/purple-facebook.spec +++ /dev/null @@ -1,49 +0,0 @@ -%if 0%{?suse_version} -%define _group Productivity/Networking/Instant Messenger -%else -%define _group Applications/Internet -%endif - -Name: purple-facebook -Version: 0.0.0 -Release: 0 -Summary: Facebook protocol plugin for libpurple -Group: %{_group} -License: GPL-2.0+ -URL: https://github.com/dequis/purple-facebook -Source0: %{name}-%{version}.tar.gz - -BuildRequires: autoconf >= 2.64 -BuildRequires: automake -BuildRequires: glib2-devel >= 2.28.0 -BuildRequires: json-glib-devel >= 0.14 -BuildRequires: libpurple-devel < 3 -BuildRequires: libtool -BuildRequires: pkg-config - -Requires: glib2 >= 2.28.0 -Requires: json-glib >= 0.14 -Requires: libpurple < 3 - -%description -Purple Facebook implements the Facebook Messenger protocol into pidgin, -finch, and libpurple. While the primary implementation is for purple3, -this plugin is back-ported for purple2. - -%prep -%setup -q - -%build -autoreconf -fi -%configure -make %{?_smp_mflags} - -%install -%make_install -find %{buildroot} -name '*.la' -print -delete - -%files -%doc AUTHORS COPYING ChangeLog NEWS README -%{_libdir}/purple-2/libfacebook.so - -%changelog diff --git a/facebook/Makefile.am b/facebook/Makefile.am new file mode 100644 index 00000000..87dd0825 --- /dev/null +++ b/facebook/Makefile.am @@ -0,0 +1,70 @@ +EXTRA_DIST = \ + Makefile.mingw \ + marshaller.list + +pkgdir = @PURPLE_PLUGINDIR@ + +FACEBOOKSOURCES = \ + marshal.c \ + marshal.h \ + api.c \ + api.h \ + data.c \ + data.h \ + facebook.h \ + facebook.c \ + http.c \ + http.h \ + id.h \ + json.c \ + json.h \ + mqtt.c \ + mqtt.h \ + thrift.c \ + thrift.h \ + util.c \ + util.h \ + ../../glibcompat.h \ + ../../http.c \ + ../../http.h \ + ../../purple-socket.h \ + ../../purple-socket.c + +CLEANFILES = \ + marshal.c \ + marshal.h + +marshal.c: $(srcdir)/marshaller.list marshal.h + $(AM_V_GEN)echo "#include \"marshal.h\"" > $@ + $(AM_V_at)$(GLIB_GENMARSHAL) --prefix=fb_marshal --body $(srcdir)/marshaller.list >> $@ + +marshal.h: $(srcdir)/marshaller.list + $(AM_V_GEN)$(GLIB_GENMARSHAL) --prefix=fb_marshal --header $(srcdir)/marshaller.list > $@ + +AM_CFLAGS = $(st) + +libfacebook_la_LDFLAGS = -module @PLUGIN_LDFLAGS@ + +if STATIC_FACEBOOK + +st = -DPURPLE_STATIC_PRPL +noinst_LTLIBRARIES = libfacebook.la +libfacebook_la_SOURCES = $(FACEBOOKSOURCES) +libfacebook_la_CFLAGS = $(AM_CFLAGS) + +else + +st = +pkg_LTLIBRARIES = libfacebook.la +libfacebook_la_SOURCES = $(FACEBOOKSOURCES) +libfacebook_la_LIBADD = @PURPLE_LIBS@ $(JSON_LIBS) + +endif + +AM_CPPFLAGS = \ + $(GLIB_CFLAGS) \ + $(JSON_CFLAGS) \ + $(PURPLE_CFLAGS) \ + $(ZLIB_CFLAGS) \ + $(PLUGIN_CFLAGS) \ + $(DEBUG_CFLAGS) diff --git a/facebook/Makefile.mingw b/facebook/Makefile.mingw new file mode 100644 index 00000000..181937ff --- /dev/null +++ b/facebook/Makefile.mingw @@ -0,0 +1,104 @@ +# +# Makefile.mingw +# +# Description: Makefile for win32 (mingw) version of libfacebook +# + +PIDGIN_TREE_TOP := ../../.. +include $(PIDGIN_TREE_TOP)/libpurple/win32/global.mak + +TARGET = libfacebook +TYPE = PLUGIN + +# Static or Plugin... +ifeq ($(TYPE),STATIC) + DEFINES += -DSTATIC + DLL_INSTALL_DIR = $(PURPLE_INSTALL_DIR) +else +ifeq ($(TYPE),PLUGIN) + DLL_INSTALL_DIR = $(PURPLE_INSTALL_PLUGINS_DIR) +endif +endif + +## +## INCLUDE PATHS +## +INCLUDE_PATHS += -I. \ + -I$(GTK_TOP)/include \ + -I$(GTK_TOP)/include/glib-2.0 \ + -I$(GTK_TOP)/lib/glib-2.0/include \ + -I$(JSON_GLIB_TOP)/include/json-glib-1.0 \ + -I$(PURPLE_TOP) \ + -I$(PURPLE_TOP)/win32 \ + -I$(GPLUGIN_TOP) \ + -I$(PIDGIN_TREE_TOP) + +LIB_PATHS += -L$(GTK_TOP)/lib \ + -L$(JSON_GLIB_TOP)/lib \ + -L$(GPLUGIN_TOP) \ + -L$(PURPLE_TOP) + +## +## SOURCES, OBJECTS +## +C_SRC = \ + marshal.c \ + api.c \ + data.c \ + facebook.c \ + http.c \ + json.c \ + mqtt.c \ + thrift.c \ + util.c \ + ../../http.c \ + ../../purple-socket.c + +OBJECTS = $(C_SRC:%.c=%.o) + +## +## LIBRARIES +## +LIBS = \ + -lglib-2.0 \ + -lgio-2.0 \ + -lgobject-2.0 \ + -lws2_32 \ + -lintl \ + -ljson-glib-1.0 \ + -lz \ + -lpurple + +include $(PIDGIN_COMMON_RULES) + +## +## TARGET DEFINITIONS +## +.PHONY: all install clean + +all: $(TARGET).dll + +install: all + cp $(TARGET).dll $(DLL_INSTALL_DIR) + +$(OBJECTS): $(PURPLE_CONFIG_H) + +$(TARGET).dll: $(OBJECTS) + $(CC) -shared $(OBJECTS) $(LIB_PATHS) $(LIBS) $(DLL_LD_FLAGS) -o $(TARGET).dll + +marshal.c: marshaller.list marshal.h + @echo "#include \"marshal.h\"" > $@ + @$(GLIB_GENMARSHAL) --prefix=fb_marshal --body marshaller.list >> $@ + +marshal.h: marshaller.list + @$(GLIB_GENMARSHAL) --prefix=fb_marshal --header marshaller.list > $@ + + +## +## CLEAN RULES +## +clean: + rm -f $(OBJECTS) marshal.c marshal.h + rm -f $(TARGET).dll + +include $(PIDGIN_COMMON_TARGETS) diff --git a/facebook/api.c b/facebook/api.c new file mode 100644 index 00000000..dce6afd8 --- /dev/null +++ b/facebook/api.c @@ -0,0 +1,3539 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#include +#include +#include + +#include "purple2compat/glibcompat.h" + +#include "api.h" +#include "http.h" +#include "json.h" +#include "marshal.h" +#include "thrift.h" +#include "util.h" + +typedef struct _FbApiData FbApiData; + +enum +{ + PROP_0, + + PROP_CID, + PROP_DID, + PROP_MID, + PROP_STOKEN, + PROP_TOKEN, + PROP_UID, + + PROP_N +}; + +struct _FbApiPrivate +{ + FbMqtt *mqtt; + FbHttpConns *cons; + PurpleConnection *gc; + GHashTable *data; + gboolean retrying; + + FbId uid; + gint64 sid; + guint64 mid; + gchar *cid; + gchar *did; + gchar *stoken; + gchar *token; + + GQueue *msgs; + gboolean invisible; + guint unread; + FbId lastmid; + gchar *contacts_delta; +}; + +struct _FbApiData +{ + gpointer data; + GDestroyNotify func; +}; + +static void +fb_api_attach(FbApi *api, FbId aid, const gchar *msgid, FbApiMessage *msg); + +static void +fb_api_contacts_after(FbApi *api, const gchar *cursor); + +static void +fb_api_message_send(FbApi *api, FbApiMessage *msg); + +static void +fb_api_sticker(FbApi *api, FbId sid, FbApiMessage *msg); + +void +fb_api_contacts_delta(FbApi *api, const gchar *delta_cursor); + +G_DEFINE_TYPE_WITH_CODE(FbApi, fb_api, G_TYPE_OBJECT, G_ADD_PRIVATE(FbApi)); + +static void +fb_api_set_property(GObject *obj, guint prop, const GValue *val, + GParamSpec *pspec) +{ + FbApiPrivate *priv = FB_API(obj)->priv; + + switch (prop) { + case PROP_CID: + g_free(priv->cid); + priv->cid = g_value_dup_string(val); + break; + case PROP_DID: + g_free(priv->did); + priv->did = g_value_dup_string(val); + break; + case PROP_MID: + priv->mid = g_value_get_uint64(val); + break; + case PROP_STOKEN: + g_free(priv->stoken); + priv->stoken = g_value_dup_string(val); + break; + case PROP_TOKEN: + g_free(priv->token); + priv->token = g_value_dup_string(val); + break; + case PROP_UID: + priv->uid = g_value_get_int64(val); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop, pspec); + break; + } +} + +static void +fb_api_get_property(GObject *obj, guint prop, GValue *val, GParamSpec *pspec) +{ + FbApiPrivate *priv = FB_API(obj)->priv; + + switch (prop) { + case PROP_CID: + g_value_set_string(val, priv->cid); + break; + case PROP_DID: + g_value_set_string(val, priv->did); + break; + case PROP_MID: + g_value_set_uint64(val, priv->mid); + break; + case PROP_STOKEN: + g_value_set_string(val, priv->stoken); + break; + case PROP_TOKEN: + g_value_set_string(val, priv->token); + break; + case PROP_UID: + g_value_set_int64(val, priv->uid); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop, pspec); + break; + } +} + + +static void +fb_api_dispose(GObject *obj) +{ + FbApiData *fata; + FbApiPrivate *priv = FB_API(obj)->priv; + GHashTableIter iter; + + fb_http_conns_cancel_all(priv->cons); + g_hash_table_iter_init(&iter, priv->data); + + while (g_hash_table_iter_next(&iter, NULL, (gpointer) &fata)) { + fata->func(fata->data); + g_free(fata); + } + + if (G_UNLIKELY(priv->mqtt != NULL)) { + g_object_unref(priv->mqtt); + } + + fb_http_conns_free(priv->cons); + g_hash_table_destroy(priv->data); + g_queue_free_full(priv->msgs, (GDestroyNotify) fb_api_message_free); + + g_free(priv->cid); + g_free(priv->did); + g_free(priv->stoken); + g_free(priv->token); + g_free(priv->contacts_delta); +} + +static void +fb_api_class_init(FbApiClass *klass) +{ + GObjectClass *gklass = G_OBJECT_CLASS(klass); + GParamSpec *props[PROP_N] = {NULL}; + + gklass->set_property = fb_api_set_property; + gklass->get_property = fb_api_get_property; + gklass->dispose = fb_api_dispose; + g_type_class_add_private(klass, sizeof (FbApiPrivate)); + + /** + * FbApi:cid: + * + * The client identifier for MQTT. This value should be saved + * and loaded for persistence. + */ + props[PROP_CID] = g_param_spec_string( + "cid", + "Client ID", + "Client identifier for MQTT", + NULL, + G_PARAM_READWRITE); + + /** + * FbApi:did: + * + * The device identifier for the MQTT message queue. This value + * should be saved and loaded for persistence. + */ + props[PROP_DID] = g_param_spec_string( + "did", + "Device ID", + "Device identifier for the MQTT message queue", + NULL, + G_PARAM_READWRITE); + + /** + * FbApi:mid: + * + * The MQTT identifier. This value should be saved and loaded + * for persistence. + */ + props[PROP_MID] = g_param_spec_uint64( + "mid", + "MQTT ID", + "MQTT identifier", + 0, G_MAXUINT64, 0, + G_PARAM_READWRITE); + + /** + * FbApi:stoken: + * + * The synchronization token for the MQTT message queue. This + * value should be saved and loaded for persistence. + */ + props[PROP_STOKEN] = g_param_spec_string( + "stoken", + "Sync Token", + "Synchronization token for the MQTT message queue", + NULL, + G_PARAM_READWRITE); + + /** + * FbApi:token: + * + * The access token for authentication. This value should be + * saved and loaded for persistence. + */ + props[PROP_TOKEN] = g_param_spec_string( + "token", + "Access Token", + "Access token for authentication", + NULL, + G_PARAM_READWRITE); + + /** + * FbApi:uid: + * + * The #FbId of the user of the #FbApi. + */ + props[PROP_UID] = g_param_spec_int64( + "uid", + "User ID", + "User identifier", + 0, G_MAXINT64, 0, + G_PARAM_READWRITE); + g_object_class_install_properties(gklass, PROP_N, props); + + /** + * FbApi::auth: + * @api: The #FbApi. + * + * Emitted upon the successful completion of the authentication + * process. This is emitted as a result of #fb_api_auth(). + */ + g_signal_new("auth", + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_ACTION, + 0, + NULL, NULL, + fb_marshal_VOID__VOID, + G_TYPE_NONE, + 0); + + /** + * FbApi::connect: + * @api: The #FbApi. + * + * Emitted upon the successful completion of the connection + * process. This is emitted as a result of #fb_api_connect(). + */ + g_signal_new("connect", + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_ACTION, + 0, + NULL, NULL, + fb_marshal_VOID__VOID, + G_TYPE_NONE, + 0); + + /** + * FbApi::contact: + * @api: The #FbApi. + * @user: The #FbApiUser. + * + * Emitted upon the successful reply of a contact request. This + * is emitted as a result of #fb_api_contact(). + */ + g_signal_new("contact", + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_ACTION, + 0, + NULL, NULL, + fb_marshal_VOID__POINTER, + G_TYPE_NONE, + 1, G_TYPE_POINTER); + + /** + * FbApi::contacts: + * @api: The #FbApi. + * @users: The #GSList of #FbApiUser's. + * @complete: #TRUE if the list is fetched, otherwise #FALSE. + * + * Emitted upon the successful reply of a contacts request. + * This is emitted as a result of #fb_api_contacts(). This can + * be emitted multiple times before the entire contacts list + * has been fetched. Use @complete for detecting the completion + * status of the list fetch. + */ + g_signal_new("contacts", + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_ACTION, + 0, + NULL, NULL, + fb_marshal_VOID__POINTER_BOOLEAN, + G_TYPE_NONE, + 2, G_TYPE_POINTER, G_TYPE_BOOLEAN); + + /** + * FbApi::contacts-delta: + * @api: The #FbApi. + * @added: The #GSList of added #FbApiUser's. + * @removed: The #GSList of strings with removed user ids. + * + * Like 'contacts', but only the deltas. + */ + g_signal_new("contacts-delta", + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_ACTION, + 0, + NULL, NULL, + fb_marshal_VOID__POINTER_POINTER, + G_TYPE_NONE, + 2, G_TYPE_POINTER, G_TYPE_POINTER); + + /** + * FbApi::error: + * @api: The #FbApi. + * @error: The #GError. + * + * Emitted whenever an error is hit within the #FbApi. This + * should disconnect the #FbApi with #fb_api_disconnect(). + */ + g_signal_new("error", + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_ACTION, + 0, + NULL, NULL, + fb_marshal_VOID__POINTER, + G_TYPE_NONE, + 1, G_TYPE_POINTER); + + /** + * FbApi::events: + * @api: The #FbApi. + * @events: The #GSList of #FbApiEvent's. + * + * Emitted upon incoming events from the stream. + */ + g_signal_new("events", + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_ACTION, + 0, + NULL, NULL, + fb_marshal_VOID__POINTER, + G_TYPE_NONE, + 1, G_TYPE_POINTER); + + /** + * FbApi::messages: + * @api: The #FbApi. + * @msgs: The #GSList of #FbApiMessage's. + * + * Emitted upon incoming messages from the stream. + */ + g_signal_new("messages", + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_ACTION, + 0, + NULL, NULL, + fb_marshal_VOID__POINTER, + G_TYPE_NONE, + 1, G_TYPE_POINTER); + + /** + * FbApi::presences: + * @api: The #FbApi. + * @press: The #GSList of #FbApiPresence's. + * + * Emitted upon incoming presences from the stream. + */ + g_signal_new("presences", + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_ACTION, + 0, + NULL, NULL, + fb_marshal_VOID__POINTER, + G_TYPE_NONE, + 1, G_TYPE_POINTER); + + /** + * FbApi::thread: + * @api: The #FbApi. + * @thrd: The #FbApiThread. + * + * Emitted upon the successful reply of a thread request. This + * is emitted as a result of #fb_api_thread(). + */ + g_signal_new("thread", + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_ACTION, + 0, + NULL, NULL, + fb_marshal_VOID__POINTER, + G_TYPE_NONE, + 1, G_TYPE_POINTER); + + /** + * FbApi::thread-create: + * @api: The #FbApi. + * @tid: The thread #FbId. + * + * Emitted upon the successful reply of a thread creation + * request. This is emitted as a result of + * #fb_api_thread_create(). + */ + g_signal_new("thread-create", + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_ACTION, + 0, + NULL, NULL, + fb_marshal_VOID__INT64, + G_TYPE_NONE, + 1, FB_TYPE_ID); + + /** + * FbApi::thread-kicked: + * @api: The #FbApi. + * @thrd: The #FbApiThread. + * + * Emitted upon the reply of a thread request when the user is no longer + * part of that thread. This is emitted as a result of #fb_api_thread(). + */ + g_signal_new("thread-kicked", + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_ACTION, + 0, + NULL, NULL, + fb_marshal_VOID__POINTER, + G_TYPE_NONE, + 1, G_TYPE_POINTER); + + /** + * FbApi::threads: + * @api: The #FbApi. + * @thrds: The #GSList of #FbApiThread's. + * + * Emitted upon the successful reply of a threads request. This + * is emitted as a result of #fb_api_threads(). + */ + g_signal_new("threads", + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_ACTION, + 0, + NULL, NULL, + fb_marshal_VOID__POINTER, + G_TYPE_NONE, + 1, G_TYPE_POINTER); + + /** + * FbApi::typing: + * @api: The #FbApi. + * @typg: The #FbApiTyping. + * + * Emitted upon an incoming typing state from the stream. + */ + g_signal_new("typing", + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_ACTION, + 0, + NULL, NULL, + fb_marshal_VOID__POINTER, + G_TYPE_NONE, + 1, G_TYPE_POINTER); +} + +static void +fb_api_init(FbApi *api) +{ + FbApiPrivate *priv; + + priv = G_TYPE_INSTANCE_GET_PRIVATE(api, FB_TYPE_API, FbApiPrivate); + api->priv = priv; + + priv->cons = fb_http_conns_new(); + priv->msgs = g_queue_new(); + priv->data = g_hash_table_new_full(g_direct_hash, g_direct_equal, + NULL, NULL); +} + +GQuark +fb_api_error_quark(void) +{ + static GQuark q = 0; + + if (G_UNLIKELY(q == 0)) { + q = g_quark_from_static_string("fb-api-error-quark"); + } + + return q; +} + +static void +fb_api_data_set(FbApi *api, gpointer handle, gpointer data, + GDestroyNotify func) +{ + FbApiData *fata; + FbApiPrivate *priv = api->priv; + + fata = g_new0(FbApiData, 1); + fata->data = data; + fata->func = func; + g_hash_table_replace(priv->data, handle, fata); +} + +static gpointer +fb_api_data_take(FbApi *api, gconstpointer handle) +{ + FbApiData *fata; + FbApiPrivate *priv = api->priv; + gpointer data; + + fata = g_hash_table_lookup(priv->data, handle); + + if (fata == NULL) { + return NULL; + } + + data = fata->data; + g_hash_table_remove(priv->data, handle); + g_free(fata); + return data; +} + +static gboolean +fb_api_json_chk(FbApi *api, gconstpointer data, gssize size, JsonNode **node) +{ + const gchar *str; + FbApiError errc = FB_API_ERROR_GENERAL; + FbApiPrivate *priv; + FbJsonValues *values; + gboolean success = TRUE; + gchar *msg; + GError *err = NULL; + gint64 code; + guint i; + JsonNode *root; + + static const gchar *exprs[] = { + "$.error.message", + "$.error.summary", + "$.error_msg", + "$.errorCode", + "$.failedSend.errorMessage", + }; + + g_return_val_if_fail(FB_IS_API(api), FALSE); + priv = api->priv; + + if (G_UNLIKELY(size == 0)) { + fb_api_error(api, FB_API_ERROR_GENERAL, _("Empty JSON data")); + return FALSE; + } + + fb_util_debug(FB_UTIL_DEBUG_INFO, "Parsing JSON: %.*s\n", + (gint) size, (const gchar *) data); + + root = fb_json_node_new(data, size, &err); + FB_API_ERROR_EMIT(api, err, return FALSE); + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE, "$.error_code"); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, "$.error.type"); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, "$.errorCode"); + fb_json_values_update(values, &err); + + FB_API_ERROR_EMIT(api, err, + g_object_unref(values); + json_node_free(root); + return FALSE + ); + + code = fb_json_values_next_int(values, 0); + str = fb_json_values_next_str(values, NULL); + + if (purple_strequal(str, "OAuthException") || (code == 401)) { + errc = FB_API_ERROR_AUTH; + success = FALSE; + + g_free(priv->stoken); + priv->stoken = NULL; + + g_free(priv->token); + priv->token = NULL; + } + + /* 509 is used for "invalid attachment id" */ + if (code == 509) { + errc = FB_API_ERROR_NONFATAL; + success = FALSE; + } + + str = fb_json_values_next_str(values, NULL); + + if (purple_strequal(str, "ERROR_QUEUE_NOT_FOUND") || + purple_strequal(str, "ERROR_QUEUE_LOST")) + { + errc = FB_API_ERROR_QUEUE; + success = FALSE; + + g_free(priv->stoken); + priv->stoken = NULL; + } + + g_object_unref(values); + + for (msg = NULL, i = 0; i < G_N_ELEMENTS(exprs); i++) { + msg = fb_json_node_get_str(root, exprs[i], NULL); + + if (msg != NULL) { + success = FALSE; + break; + } + } + + if (!success && (msg == NULL)) { + msg = g_strdup(_("Unknown error")); + } + + if (msg != NULL) { + fb_api_error(api, errc, "%s", msg); + json_node_free(root); + g_free(msg); + return FALSE; + } + + if (node != NULL) { + *node = root; + } else { + json_node_free(root); + } + + return TRUE; +} + +static gboolean +fb_api_http_chk(FbApi *api, PurpleHttpConnection *con, PurpleHttpResponse *res, + JsonNode **root) +{ + const gchar *data; + const gchar *msg; + FbApiPrivate *priv = api->priv; + gchar *emsg; + GError *err = NULL; + gint code; + gsize size; + + if (fb_http_conns_is_canceled(priv->cons)) { + return FALSE; + } + + msg = purple_http_response_get_error(res); + code = purple_http_response_get_code(res); + data = purple_http_response_get_data(res, &size); + fb_http_conns_remove(priv->cons, con); + + if (msg != NULL) { + emsg = g_strdup_printf("%s (%d)", msg, code); + } else { + emsg = g_strdup_printf("%d", code); + } + + fb_util_debug(FB_UTIL_DEBUG_INFO, "HTTP Response (%p):", con); + fb_util_debug(FB_UTIL_DEBUG_INFO, " Response Error: %s", emsg); + g_free(emsg); + + if (G_LIKELY(size > 0)) { + fb_util_debug(FB_UTIL_DEBUG_INFO, " Response Data: %.*s", + (gint) size, data); + } + + if (fb_http_error_chk(res, &err) && (root == NULL)) { + return TRUE; + } + + /* Rudimentary check to prevent wrongful error parsing */ + if ((size < 2) || (data[0] != '{') || (data[size - 1] != '}')) { + FB_API_ERROR_EMIT(api, err, return FALSE); + } + + if (!fb_api_json_chk(api, data, size, root)) { + if (G_UNLIKELY(err != NULL)) { + g_error_free(err); + } + + return FALSE; + } + + FB_API_ERROR_EMIT(api, err, return FALSE); + return TRUE; +} + +static PurpleHttpConnection * +fb_api_http_req(FbApi *api, const gchar *url, const gchar *name, + const gchar *method, FbHttpParams *params, + PurpleHttpCallback callback) +{ + FbApiPrivate *priv = api->priv; + gchar *data; + gchar *key; + gchar *val; + GList *keys; + GList *l; + GString *gstr; + PurpleHttpConnection *ret; + PurpleHttpRequest *req; + + fb_http_params_set_str(params, "api_key", FB_API_KEY); + fb_http_params_set_str(params, "device_id", priv->did); + fb_http_params_set_str(params, "fb_api_req_friendly_name", name); + fb_http_params_set_str(params, "format", "json"); + fb_http_params_set_str(params, "method", method); + + val = fb_util_get_locale(); + fb_http_params_set_str(params, "locale", val); + g_free(val); + + req = purple_http_request_new(url); + purple_http_request_set_max_len(req, -1); + purple_http_request_set_method(req, "POST"); + + /* Ensure an old signature is not computed */ + g_hash_table_remove(params, "sig"); + + gstr = g_string_new(NULL); + keys = g_hash_table_get_keys(params); + keys = g_list_sort(keys, (GCompareFunc) g_ascii_strcasecmp); + + for (l = keys; l != NULL; l = l->next) { + key = l->data; + val = g_hash_table_lookup(params, key); + g_string_append_printf(gstr, "%s=%s", key, val); + } + + g_string_append(gstr, FB_API_SECRET); + data = g_compute_checksum_for_string(G_CHECKSUM_MD5, gstr->str, + gstr->len); + fb_http_params_set_str(params, "sig", data); + g_string_free(gstr, TRUE); + g_list_free(keys); + g_free(data); + + if (priv->token != NULL) { + data = g_strdup_printf("OAuth %s", priv->token); + purple_http_request_header_set(req, "Authorization", data); + g_free(data); + } + + purple_http_request_header_set(req, "User-Agent", FB_API_AGENT); + purple_http_request_header_set(req, "Content-Type", "application/x-www-form-urlencoded; charset=utf-8"); + + data = fb_http_params_close(params, NULL); + purple_http_request_set_contents(req, data, -1); + ret = purple_http_request(priv->gc, req, callback, api); + fb_http_conns_add(priv->cons, ret); + purple_http_request_unref(req); + + fb_util_debug(FB_UTIL_DEBUG_INFO, "HTTP Request (%p):", ret); + fb_util_debug(FB_UTIL_DEBUG_INFO, " Request URL: %s", url); + fb_util_debug(FB_UTIL_DEBUG_INFO, " Request Data: %s", data); + + g_free(data); + return ret; +} + +static PurpleHttpConnection * +fb_api_http_query(FbApi *api, gint64 query, JsonBuilder *builder, + PurpleHttpCallback hcb) +{ + const gchar *name; + FbHttpParams *prms; + gchar *json; + + switch (query) { + case FB_API_QUERY_CONTACT: + name = "UsersQuery"; + break; + case FB_API_QUERY_CONTACTS: + name = "FetchContactsFullQuery"; + break; + case FB_API_QUERY_CONTACTS_AFTER: + name = "FetchContactsFullWithAfterQuery"; + break; + case FB_API_QUERY_CONTACTS_DELTA: + name = "FetchContactsDeltaQuery"; + break; + case FB_API_QUERY_STICKER: + name = "FetchStickersWithPreviewsQuery"; + break; + case FB_API_QUERY_THREAD: + name = "ThreadQuery"; + break; + case FB_API_QUERY_SEQ_ID: + case FB_API_QUERY_THREADS: + name = "ThreadListQuery"; + break; + case FB_API_QUERY_XMA: + name = "XMAQuery"; + break; + default: + g_return_val_if_reached(NULL); + return NULL; + } + + prms = fb_http_params_new(); + json = fb_json_bldr_close(builder, JSON_NODE_OBJECT, NULL); + + fb_http_params_set_strf(prms, "query_id", "%" G_GINT64_FORMAT, query); + fb_http_params_set_str(prms, "query_params", json); + g_free(json); + + return fb_api_http_req(api, FB_API_URL_GQL, name, "get", prms, hcb); +} + +static void +fb_api_cb_http_bool(PurpleHttpConnection *con, PurpleHttpResponse *res, + gpointer data) +{ + const gchar *hata; + FbApi *api = data; + + if (!fb_api_http_chk(api, con, res, NULL)) { + return; + } + + hata = purple_http_response_get_data(res, NULL); + + if (!purple_strequal(hata, "true")) { + fb_api_error(api, FB_API_ERROR, + _("Failed generic API operation")); + } +} + +static void +fb_api_cb_mqtt_error(FbMqtt *mqtt, GError *error, gpointer data) +{ + FbApi *api = data; + FbApiPrivate *priv = api->priv; + + if (!priv->retrying) { + priv->retrying = TRUE; + fb_util_debug_info("Attempting to reconnect the MQTT stream..."); + fb_api_connect(api, priv->invisible); + } else { + g_signal_emit_by_name(api, "error", error); + } +} + +static void +fb_api_cb_mqtt_open(FbMqtt *mqtt, gpointer data) +{ + const GByteArray *bytes; + FbApi *api = data; + FbApiPrivate *priv = api->priv; + FbThrift *thft; + GByteArray *cytes; + GError *err = NULL; + + static guint8 flags = FB_MQTT_CONNECT_FLAG_USER | + FB_MQTT_CONNECT_FLAG_PASS | + FB_MQTT_CONNECT_FLAG_CLR; + + thft = fb_thrift_new(NULL, 0); + + /* Write the client identifier */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_STRING, 1, 0); + fb_thrift_write_str(thft, priv->cid); + + fb_thrift_write_field(thft, FB_THRIFT_TYPE_STRUCT, 4, 1); + + /* Write the user identifier */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_I64, 1, 0); + fb_thrift_write_i64(thft, priv->uid); + + /* Write the information string */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_STRING, 2, 1); + fb_thrift_write_str(thft, FB_API_MQTT_AGENT); + + /* Write the UNKNOWN ("cp"?) */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_I64, 3, 2); + fb_thrift_write_i64(thft, 23); + + /* Write the UNKNOWN ("ecp"?) */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_I64, 4, 3); + fb_thrift_write_i64(thft, 26); + + /* Write the UNKNOWN */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_I32, 5, 4); + fb_thrift_write_i32(thft, 1); + + /* Write the UNKNOWN ("no_auto_fg"?) */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_BOOL, 6, 5); + fb_thrift_write_bool(thft, TRUE); + + /* Write the visibility state */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_BOOL, 7, 6); + fb_thrift_write_bool(thft, !priv->invisible); + + /* Write the device identifier */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_STRING, 8, 7); + fb_thrift_write_str(thft, priv->did); + + /* Write the UNKNOWN ("fg"?) */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_BOOL, 9, 8); + fb_thrift_write_bool(thft, TRUE); + + /* Write the UNKNOWN ("nwt"?) */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_I32, 10, 9); + fb_thrift_write_i32(thft, 1); + + /* Write the UNKNOWN ("nwst"?) */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_I32, 11, 10); + fb_thrift_write_i32(thft, 0); + + /* Write the MQTT identifier */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_I64, 12, 11); + fb_thrift_write_i64(thft, priv->mid); + + /* Write the UNKNOWN */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_LIST, 14, 12); + fb_thrift_write_list(thft, FB_THRIFT_TYPE_I32, 0); + fb_thrift_write_stop(thft); + + /* Write the token */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_STRING, 15, 14); + fb_thrift_write_str(thft, priv->token); + + /* Write the STOP for the struct */ + fb_thrift_write_stop(thft); + + bytes = fb_thrift_get_bytes(thft); + cytes = fb_util_zlib_deflate(bytes, &err); + + FB_API_ERROR_EMIT(api, err, + g_object_unref(thft); + return; + ); + + fb_util_debug_hexdump(FB_UTIL_DEBUG_INFO, bytes, "Writing connect"); + fb_mqtt_connect(mqtt, flags, cytes); + + g_byte_array_free(cytes, TRUE); + g_object_unref(thft); +} + +static void +fb_api_connect_queue(FbApi *api) +{ + FbApiMessage *msg; + FbApiPrivate *priv = api->priv; + gchar *json; + JsonBuilder *bldr; + + bldr = fb_json_bldr_new(JSON_NODE_OBJECT); + fb_json_bldr_add_int(bldr, "delta_batch_size", 125); + fb_json_bldr_add_int(bldr, "max_deltas_able_to_process", 1250); + fb_json_bldr_add_int(bldr, "sync_api_version", 3); + fb_json_bldr_add_str(bldr, "encoding", "JSON"); + + if (priv->stoken == NULL) { + fb_json_bldr_add_int(bldr, "initial_titan_sequence_id", + priv->sid); + fb_json_bldr_add_str(bldr, "device_id", priv->did); + fb_json_bldr_add_int(bldr, "entity_fbid", priv->uid); + + fb_json_bldr_obj_begin(bldr, "queue_params"); + fb_json_bldr_add_str(bldr, "buzz_on_deltas_enabled", "false"); + + fb_json_bldr_obj_begin(bldr, "graphql_query_hashes"); + fb_json_bldr_add_str(bldr, "xma_query_id", + G_STRINGIFY(FB_API_QUERY_XMA)); + fb_json_bldr_obj_end(bldr); + + fb_json_bldr_obj_begin(bldr, "graphql_query_params"); + fb_json_bldr_obj_begin(bldr, G_STRINGIFY(FB_API_QUERY_XMA)); + fb_json_bldr_add_str(bldr, "xma_id", ""); + fb_json_bldr_obj_end(bldr); + fb_json_bldr_obj_end(bldr); + fb_json_bldr_obj_end(bldr); + + json = fb_json_bldr_close(bldr, JSON_NODE_OBJECT, NULL); + fb_api_publish(api, "/messenger_sync_create_queue", "%s", + json); + g_free(json); + return; + } + + fb_json_bldr_add_int(bldr, "last_seq_id", priv->sid); + fb_json_bldr_add_str(bldr, "sync_token", priv->stoken); + + json = fb_json_bldr_close(bldr, JSON_NODE_OBJECT, NULL); + fb_api_publish(api, "/messenger_sync_get_diffs", "%s", json); + g_signal_emit_by_name(api, "connect"); + g_free(json); + + if (!g_queue_is_empty(priv->msgs)) { + msg = g_queue_peek_head(priv->msgs); + fb_api_message_send(api, msg); + } + + if (priv->retrying) { + priv->retrying = FALSE; + fb_util_debug_info("Reconnected the MQTT stream"); + } +} + +static void +fb_api_cb_seqid(PurpleHttpConnection *con, PurpleHttpResponse *res, + gpointer data) +{ + const gchar *str; + FbApi *api = data; + FbApiPrivate *priv = api->priv; + FbJsonValues *values; + GError *err = NULL; + JsonNode *root; + + if (!fb_api_http_chk(api, con, res, &root)) { + return; + } + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.viewer.message_threads.sync_sequence_id"); + fb_json_values_add(values, FB_JSON_TYPE_INT, TRUE, + "$.viewer.message_threads.unread_count"); + fb_json_values_update(values, &err); + + FB_API_ERROR_EMIT(api, err, + g_object_unref(values); + json_node_free(root); + return; + ); + + str = fb_json_values_next_str(values, "0"); + priv->sid = g_ascii_strtoll(str, NULL, 10); + priv->unread = fb_json_values_next_int(values, 0); + + if (priv->sid == 0) { + fb_api_error(api, FB_API_ERROR_GENERAL, + _("Failed to get sync_sequence_id")); + } else { + fb_api_connect_queue(api); + } + + g_object_unref(values); + json_node_free(root); +} + +static void +fb_api_cb_mqtt_connect(FbMqtt *mqtt, gpointer data) +{ + FbApi *api = data; + FbApiPrivate *priv = api->priv; + gchar *json; + JsonBuilder *bldr; + + bldr = fb_json_bldr_new(JSON_NODE_OBJECT); + fb_json_bldr_add_bool(bldr, "foreground", TRUE); + fb_json_bldr_add_int(bldr, "keepalive_timeout", FB_MQTT_KA); + + json = fb_json_bldr_close(bldr, JSON_NODE_OBJECT, NULL); + fb_api_publish(api, "/foreground_state", "%s", json); + g_free(json); + + fb_mqtt_subscribe(mqtt, + "/inbox", 0, + "/mercury", 0, + "/messaging_events", 0, + "/orca_presence", 0, + "/orca_typing_notifications", 0, + "/pp", 0, + "/t_ms", 0, + "/t_p", 0, + "/t_rtc", 0, + "/webrtc", 0, + "/webrtc_response", 0, + NULL + ); + + /* Notifications seem to lead to some sort of sending rate limit */ + fb_mqtt_unsubscribe(mqtt, "/orca_message_notifications", NULL); + + if (priv->sid == 0) { + bldr = fb_json_bldr_new(JSON_NODE_OBJECT); + fb_json_bldr_add_str(bldr, "1", "0"); + fb_api_http_query(api, FB_API_QUERY_SEQ_ID, bldr, + fb_api_cb_seqid); + } else { + fb_api_connect_queue(api); + } +} + +static void +fb_api_cb_publish_mark(FbApi *api, GByteArray *pload) +{ + FbJsonValues *values; + GError *err = NULL; + JsonNode *root; + + if (!fb_api_json_chk(api, pload->data, pload->len, &root)) { + return; + } + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_BOOL, FALSE, "$.succeeded"); + fb_json_values_update(values, &err); + + FB_API_ERROR_EMIT(api, err, + g_object_unref(values); + json_node_free(root); + return; + ); + + if (!fb_json_values_next_bool(values, TRUE)) { + fb_api_error(api, FB_API_ERROR_GENERAL, + _("Failed to mark thread as read")); + } + + g_object_unref(values); + json_node_free(root); +} + +static GSList * +fb_api_event_parse(FbApi *api, FbApiEvent *event, GSList *events, + JsonNode *root, GError **error) +{ + const gchar *str; + FbApiEvent *devent; + FbJsonValues *values; + GError *err = NULL; + guint i; + + static const struct { + FbApiEventType type; + const gchar *expr; + } evtypes[] = { + { + FB_API_EVENT_TYPE_THREAD_USER_ADDED, + "$.log_message_data.added_participants" + }, { + FB_API_EVENT_TYPE_THREAD_USER_REMOVED, + "$.log_message_data.removed_participants" + } + }; + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.log_message_type"); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, "$.author"); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.log_message_data.name"); + fb_json_values_update(values, &err); + + if (G_UNLIKELY(err != NULL)) { + g_propagate_error(error, err); + g_object_unref(values); + return events; + } + + str = fb_json_values_next_str(values, NULL); + + if (g_strcmp0(str, "log:thread-name") == 0) { + str = fb_json_values_next_str(values, ""); + str = strrchr(str, ':'); + + if (str != NULL) { + devent = fb_api_event_dup(event, FALSE); + devent->type = FB_API_EVENT_TYPE_THREAD_TOPIC; + devent->uid = FB_ID_FROM_STR(str + 1); + devent->text = fb_json_values_next_str_dup(values, NULL); + events = g_slist_prepend(events, devent); + } + } + + g_object_unref(values); + + for (i = 0; i < G_N_ELEMENTS(evtypes); i++) { + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, "$"); + fb_json_values_set_array(values, FALSE, evtypes[i].expr); + + while (fb_json_values_update(values, &err)) { + str = fb_json_values_next_str(values, ""); + str = strrchr(str, ':'); + + if (str != NULL) { + devent = fb_api_event_dup(event, FALSE); + devent->type = evtypes[i].type; + devent->uid = FB_ID_FROM_STR(str + 1); + events = g_slist_prepend(events, devent); + } + } + + g_object_unref(values); + + if (G_UNLIKELY(err != NULL)) { + g_propagate_error(error, err); + break; + } + } + + return events; +} + +static void +fb_api_cb_publish_mercury(FbApi *api, GByteArray *pload) +{ + const gchar *str; + FbApiEvent event; + FbJsonValues *values; + GError *err = NULL; + GSList *events = NULL; + JsonNode *root; + JsonNode *node; + + if (!fb_api_json_chk(api, pload->data, pload->len, &root)) { + return; + } + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, "$.thread_fbid"); + fb_json_values_set_array(values, FALSE, "$.actions"); + + while (fb_json_values_update(values, &err)) { + fb_api_event_reset(&event, FALSE); + str = fb_json_values_next_str(values, "0"); + event.tid = FB_ID_FROM_STR(str); + + node = fb_json_values_get_root(values); + events = fb_api_event_parse(api, &event, events, node, &err); + } + + if (G_LIKELY(err == NULL)) { + events = g_slist_reverse(events); + g_signal_emit_by_name(api, "events", events); + } else { + fb_api_error_emit(api, err); + } + + g_slist_free_full(events, (GDestroyNotify) fb_api_event_free); + g_object_unref(values); + json_node_free(root); + +} + +static void +fb_api_cb_publish_typing(FbApi *api, GByteArray *pload) +{ + const gchar *str; + FbApiPrivate *priv = api->priv; + FbApiTyping typg; + FbJsonValues *values; + GError *err = NULL; + JsonNode *root; + + if (!fb_api_json_chk(api, pload->data, pload->len, &root)) { + return; + } + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, "$.type"); + fb_json_values_add(values, FB_JSON_TYPE_INT, TRUE, "$.sender_fbid"); + fb_json_values_add(values, FB_JSON_TYPE_INT, TRUE, "$.state"); + fb_json_values_update(values, &err); + + FB_API_ERROR_EMIT(api, err, + g_object_unref(values); + json_node_free(root); + return; + ); + + str = fb_json_values_next_str(values, NULL); + + if (g_ascii_strcasecmp(str, "typ") == 0) { + typg.uid = fb_json_values_next_int(values, 0); + + if (typg.uid != priv->uid) { + typg.state = fb_json_values_next_int(values, 0); + g_signal_emit_by_name(api, "typing", &typg); + } + } + + g_object_unref(values); + json_node_free(root); +} + +static void +fb_api_cb_publish_ms_r(FbApi *api, GByteArray *pload) +{ + FbApiMessage *msg; + FbApiPrivate *priv = api->priv; + FbJsonValues *values; + GError *err = NULL; + JsonNode *root; + + if (!fb_api_json_chk(api, pload->data, pload->len, &root)) { + return; + } + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_BOOL, TRUE, "$.succeeded"); + fb_json_values_update(values, &err); + + FB_API_ERROR_EMIT(api, err, + g_object_unref(values); + json_node_free(root); + return; + ); + + if (fb_json_values_next_bool(values, TRUE)) { + /* Pop and free the successful message */ + msg = g_queue_pop_head(priv->msgs); + fb_api_message_free(msg); + + if (!g_queue_is_empty(priv->msgs)) { + msg = g_queue_peek_head(priv->msgs); + fb_api_message_send(api, msg); + } + } else { + fb_api_error(api, FB_API_ERROR_GENERAL, + "Failed to send message"); + } + + g_object_unref(values); + json_node_free(root); +} + +static gchar * +fb_api_xma_parse(FbApi *api, const gchar *body, JsonNode *root, GError **error) +{ + const gchar *str; + const gchar *url; + FbHttpParams *params; + FbJsonValues *values; + gchar *text; + GError *err = NULL; + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.story_attachment.target.__type__.name"); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.story_attachment.url"); + fb_json_values_update(values, &err); + + if (G_UNLIKELY(err != NULL)) { + g_propagate_error(error, err); + g_object_unref(values); + return NULL; + } + + str = fb_json_values_next_str(values, NULL); + url = fb_json_values_next_str(values, NULL); + + if ((str == NULL) || (url == NULL)) { + text = g_strdup(_("")); + g_object_unref(values); + return text; + } + + if (purple_strequal(str, "ExternalUrl")) { + params = fb_http_params_new_parse(url, TRUE); + if (g_str_has_prefix(url, FB_API_FBRPC_PREFIX)) { + text = fb_http_params_dup_str(params, "target_url", NULL); + } else { + text = fb_http_params_dup_str(params, "u", NULL); + } + fb_http_params_free(params); + } else { + text = g_strdup(url); + } + + if (fb_http_urlcmp(body, text, FALSE)) { + g_free(text); + g_object_unref(values); + return NULL; + } + + g_object_unref(values); + return text; +} + +static GSList * +fb_api_message_parse_attach(FbApi *api, const gchar *mid, FbApiMessage *msg, + GSList *msgs, const gchar *body, JsonNode *root, + GError **error) +{ + const gchar *str; + FbApiMessage *dmsg; + FbId id; + FbJsonValues *values; + gchar *xma; + GError *err = NULL; + JsonNode *node; + JsonNode *xode; + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, "$.xmaGraphQL"); + fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE, "$.fbid"); + fb_json_values_set_array(values, FALSE, "$.attachments"); + + while (fb_json_values_update(values, &err)) { + str = fb_json_values_next_str(values, NULL); + + if (str == NULL) { + id = fb_json_values_next_int(values, 0); + dmsg = fb_api_message_dup(msg, FALSE); + fb_api_attach(api, id, mid, dmsg); + continue; + } + + node = fb_json_node_new(str, -1, &err); + + if (G_UNLIKELY(err != NULL)) { + break; + } + + xode = fb_json_node_get_nth(node, 0); + xma = fb_api_xma_parse(api, body, xode, &err); + + if (xma != NULL) { + dmsg = fb_api_message_dup(msg, FALSE); + dmsg->text = xma; + msgs = g_slist_prepend(msgs, dmsg); + } + + json_node_free(node); + + if (G_UNLIKELY(err != NULL)) { + break; + } + } + + if (G_UNLIKELY(err != NULL)) { + g_propagate_error(error, err); + } + + g_object_unref(values); + return msgs; +} + + +static GSList * +fb_api_cb_publish_ms_new_message(FbApi *api, JsonNode *root, GSList *msgs, GError **error); + +static GSList * +fb_api_cb_publish_ms_event(FbApi *api, JsonNode *root, GSList *events, FbApiEventType type, GError **error); + +static void +fb_api_cb_publish_mst(FbThrift *thft, GError **error) +{ + if (fb_thrift_read_isstop(thft)) { + FB_API_TCHK(fb_thrift_read_stop(thft)); + } else { + FbThriftType type; + gint16 id; + + FB_API_TCHK(fb_thrift_read_field(thft, &type, &id, 0)); + FB_API_TCHK(type == FB_THRIFT_TYPE_STRING); + // FB_API_TCHK(id == 2); + FB_API_TCHK(fb_thrift_read_str(thft, NULL)); + FB_API_TCHK(fb_thrift_read_stop(thft)); + } +} + +static void +fb_api_cb_publish_ms(FbApi *api, GByteArray *pload) +{ + const gchar *data; + FbApiPrivate *priv = api->priv; + FbJsonValues *values; + FbThrift *thft; + gchar *stoken; + GError *err = NULL; + GList *elms, *l; + GSList *msgs = NULL; + GSList *events = NULL; + guint size; + JsonNode *root; + JsonNode *node; + JsonArray *arr; + + static const struct { + const gchar *member; + FbApiEventType type; + gboolean is_message; + } event_types[] = { + {"deltaNewMessage", 0, 1}, + {"deltaThreadName", FB_API_EVENT_TYPE_THREAD_TOPIC, 0}, + {"deltaParticipantsAddedToGroupThread", FB_API_EVENT_TYPE_THREAD_USER_ADDED, 0}, + {"deltaParticipantLeftGroupThread", FB_API_EVENT_TYPE_THREAD_USER_REMOVED, 0}, + }; + + /* Read identifier string (for Facebook employees) */ + thft = fb_thrift_new(pload, 0); + fb_api_cb_publish_mst(thft, &err); + size = fb_thrift_get_pos(thft); + g_object_unref(thft); + + FB_API_ERROR_EMIT(api, err, + return; + ); + + g_return_if_fail(size < pload->len); + data = (gchar *) pload->data + size; + size = pload->len - size; + + if (!fb_api_json_chk(api, data, size, &root)) { + return; + } + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE, + "$.lastIssuedSeqId"); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, "$.syncToken"); + fb_json_values_update(values, &err); + + FB_API_ERROR_EMIT(api, err, + g_object_unref(values); + json_node_free(root); + return; + ); + + priv->sid = fb_json_values_next_int(values, 0); + stoken = fb_json_values_next_str_dup(values, NULL); + g_object_unref(values); + + if (G_UNLIKELY(stoken != NULL)) { + g_free(priv->stoken); + priv->stoken = stoken; + g_signal_emit_by_name(api, "connect"); + json_node_free(root); + return; + } + + arr = fb_json_node_get_arr(root, "$.deltas", NULL); + elms = json_array_get_elements(arr); + + for (l = elms; l != NULL; l = l->next) { + guint i = 0; + JsonObject *o = json_node_get_object(l->data); + + for (i = 0; i < G_N_ELEMENTS(event_types); i++) { + if ((node = json_object_get_member(o, event_types[i].member))) { + if (event_types[i].is_message) { + msgs = fb_api_cb_publish_ms_new_message( + api, node, msgs, &err + ); + } else { + events = fb_api_cb_publish_ms_event( + api, node, events, event_types[i].type, &err + ); + } + } + } + + if (G_UNLIKELY(err != NULL)) { + break; + } + } + + g_list_free(elms); + json_array_unref(arr); + + if (G_LIKELY(err == NULL)) { + if (msgs) { + msgs = g_slist_reverse(msgs); + g_signal_emit_by_name(api, "messages", msgs); + } + + if (events) { + events = g_slist_reverse(events); + g_signal_emit_by_name(api, "events", events); + } + } else { + fb_api_error_emit(api, err); + } + + g_slist_free_full(msgs, (GDestroyNotify) fb_api_message_free); + g_slist_free_full(events, (GDestroyNotify) fb_api_event_free); + json_node_free(root); +} + +static GSList * +fb_api_cb_publish_ms_new_message(FbApi *api, JsonNode *root, GSList *msgs, GError **error) +{ + const gchar *body; + const gchar *str; + GError *err = NULL; + FbApiPrivate *priv = api->priv; + FbApiMessage *dmsg; + FbApiMessage msg; + FbId id; + FbId oid; + FbJsonValues *values; + JsonNode *node; + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE, + "$.messageMetadata.offlineThreadingId"); + fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE, + "$.messageMetadata.actorFbId"); + fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE, + "$.messageMetadata" + ".threadKey.otherUserFbId"); + fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE, + "$.messageMetadata" + ".threadKey.threadFbId"); + fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE, + "$.messageMetadata.timestamp"); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.body"); + fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE, + "$.stickerId"); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.messageMetadata.messageId"); + + if (fb_json_values_update(values, &err)) { + id = fb_json_values_next_int(values, 0); + + /* Ignore everything but new messages */ + if (id == 0) { + goto beach; + } + + /* Ignore sequential duplicates */ + if (id == priv->lastmid) { + fb_util_debug_info("Ignoring duplicate %" FB_ID_FORMAT, id); + goto beach; + } + + priv->lastmid = id; + fb_api_message_reset(&msg, FALSE); + msg.uid = fb_json_values_next_int(values, 0); + oid = fb_json_values_next_int(values, 0); + msg.tid = fb_json_values_next_int(values, 0); + msg.tstamp = fb_json_values_next_int(values, 0); + + if (msg.uid == priv->uid) { + msg.flags |= FB_API_MESSAGE_FLAG_SELF; + + if (msg.tid == 0) { + msg.uid = oid; + } + } + + body = fb_json_values_next_str(values, NULL); + + if (body != NULL) { + dmsg = fb_api_message_dup(&msg, FALSE); + dmsg->text = g_strdup(body); + msgs = g_slist_prepend(msgs, dmsg); + } + + id = fb_json_values_next_int(values, 0); + + if (id != 0) { + dmsg = fb_api_message_dup(&msg, FALSE); + fb_api_sticker(api, id, dmsg); + } + + str = fb_json_values_next_str(values, NULL); + + if (str == NULL) { + goto beach; + } + + node = fb_json_values_get_root(values); + msgs = fb_api_message_parse_attach(api, str, &msg, msgs, body, + node, &err); + + if (G_UNLIKELY(err != NULL)) { + g_propagate_error(error, err); + goto beach; + } + } + +beach: + g_object_unref(values); + return msgs; +} + +static GSList * +fb_api_cb_publish_ms_event(FbApi *api, JsonNode *root, GSList *events, FbApiEventType type, GError **error) +{ + FbApiEvent *event; + FbJsonValues *values = NULL; + FbJsonValues *values_inner = NULL; + GError *err = NULL; + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE, + "$.messageMetadata.threadKey.threadFbId"); + fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE, + "$.messageMetadata.actorFbId"); + + switch (type) { + case FB_API_EVENT_TYPE_THREAD_TOPIC: + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.name"); + break; + + case FB_API_EVENT_TYPE_THREAD_USER_ADDED: + values_inner = fb_json_values_new(root); + + fb_json_values_add(values_inner, FB_JSON_TYPE_INT, FALSE, + "$.userFbId"); + + /* use the text field for the full name */ + fb_json_values_add(values_inner, FB_JSON_TYPE_STR, FALSE, + "$.fullName"); + + fb_json_values_set_array(values_inner, FALSE, + "$.addedParticipants"); + break; + + case FB_API_EVENT_TYPE_THREAD_USER_REMOVED: + fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE, + "$.leftParticipantFbId"); + + /* use the text field for the kick message */ + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.messageMetadata.adminText"); + break; + } + + fb_json_values_update(values, &err); + + event = fb_api_event_dup(NULL, FALSE); + event->type = type; + event->tid = fb_json_values_next_int(values, 0); + event->uid = fb_json_values_next_int(values, 0); + + if (type == FB_API_EVENT_TYPE_THREAD_TOPIC) { + event->text = fb_json_values_next_str_dup(values, NULL); + } else if (type == FB_API_EVENT_TYPE_THREAD_USER_REMOVED) { + /* overwrite actor with subject */ + event->uid = fb_json_values_next_int(values, 0); + event->text = fb_json_values_next_str_dup(values, NULL); + } else if (type == FB_API_EVENT_TYPE_THREAD_USER_ADDED) { + + while (fb_json_values_update(values_inner, &err)) { + FbApiEvent *devent = fb_api_event_dup(event, FALSE); + + devent->uid = fb_json_values_next_int(values_inner, 0); + devent->text = fb_json_values_next_str_dup(values_inner, NULL); + + events = g_slist_prepend(events, devent); + } + fb_api_event_free(event); + event = NULL; + g_object_unref(values_inner); + } + + g_object_unref(values); + + if (G_UNLIKELY(err != NULL)) { + g_propagate_error(error, err); + } else if (event) { + events = g_slist_prepend(events, event); + } + + return events; +} + +static void +fb_api_cb_publish_pt(FbThrift *thft, GSList **press, GError **error) +{ + FbApiPresence *pres; + FbThriftType type; + gint16 id; + gint32 i32; + gint64 i64; + guint i; + guint size = 0; + + /* Read identifier string (for Facebook employees) */ + FB_API_TCHK(fb_thrift_read_str(thft, NULL)); + + /* Read the full list boolean field */ + FB_API_TCHK(fb_thrift_read_field(thft, &type, &id, 0)); + FB_API_TCHK(type == FB_THRIFT_TYPE_BOOL); + FB_API_TCHK(id == 1); + FB_API_TCHK(fb_thrift_read_bool(thft, NULL)); + + /* Read the list field */ + FB_API_TCHK(fb_thrift_read_field(thft, &type, &id, id)); + FB_API_TCHK(type == FB_THRIFT_TYPE_LIST); + FB_API_TCHK(id == 2); + + /* Read the list */ + FB_API_TCHK(fb_thrift_read_list(thft, &type, &size)); + FB_API_TCHK(type == FB_THRIFT_TYPE_STRUCT); + + for (i = 0; i < size; i++) { + /* Read the user identifier field */ + FB_API_TCHK(fb_thrift_read_field(thft, &type, &id, 0)); + FB_API_TCHK(type == FB_THRIFT_TYPE_I64); + FB_API_TCHK(id == 1); + FB_API_TCHK(fb_thrift_read_i64(thft, &i64)); + + /* Read the active field */ + FB_API_TCHK(fb_thrift_read_field(thft, &type, &id, id)); + FB_API_TCHK(type == FB_THRIFT_TYPE_I32); + FB_API_TCHK(id == 2); + FB_API_TCHK(fb_thrift_read_i32(thft, &i32)); + + pres = fb_api_presence_dup(NULL); + pres->uid = i64; + pres->active = i32 != 0; + *press = g_slist_prepend(*press, pres); + + fb_util_debug_info("Presence: %" FB_ID_FORMAT " (%d) id: %d", + i64, i32 != 0, id); + + while (id <= 6) { + if (fb_thrift_read_isstop(thft)) { + break; + } + + FB_API_TCHK(fb_thrift_read_field(thft, &type, &id, id)); + + switch (id) { + case 3: + /* Read the last active timestamp field */ + FB_API_TCHK(type == FB_THRIFT_TYPE_I64); + FB_API_TCHK(fb_thrift_read_i64(thft, NULL)); + break; + + case 4: + /* Read the active client bits field */ + FB_API_TCHK(type == FB_THRIFT_TYPE_I16); + FB_API_TCHK(fb_thrift_read_i16(thft, NULL)); + break; + + case 5: + /* Read the VoIP compatibility bits field */ + FB_API_TCHK(type == FB_THRIFT_TYPE_I64); + FB_API_TCHK(fb_thrift_read_i64(thft, NULL)); + break; + + case 6: + /* Unknown new field */ + FB_API_TCHK(type == FB_THRIFT_TYPE_I64); + FB_API_TCHK(fb_thrift_read_i64(thft, NULL)); + break; + + default: + /* Try to read unknown fields as varint */ + FB_API_TCHK(type == FB_THRIFT_TYPE_I16 || + type == FB_THRIFT_TYPE_I32 || + type == FB_THRIFT_TYPE_I64); + FB_API_TCHK(fb_thrift_read_i64(thft, NULL)); + break; + } + } + + /* Read the field stop */ + FB_API_TCHK(fb_thrift_read_stop(thft)); + } + + /* Read the field stop */ + if (fb_thrift_read_isstop(thft)) { + FB_API_TCHK(fb_thrift_read_stop(thft)); + } +} + +static void +fb_api_cb_publish_p(FbApi *api, GByteArray *pload) +{ + FbThrift *thft; + GError *err = NULL; + GSList *press = NULL; + + thft = fb_thrift_new(pload, 0); + fb_api_cb_publish_pt(thft, &press, &err); + g_object_unref(thft); + + if (G_LIKELY(err == NULL)) { + g_signal_emit_by_name(api, "presences", press); + } else { + fb_api_error_emit(api, err); + } + + g_slist_free_full(press, (GDestroyNotify) fb_api_presence_free); +} + +static void +fb_api_cb_mqtt_publish(FbMqtt *mqtt, const gchar *topic, GByteArray *pload, + gpointer data) +{ + FbApi *api = data; + gboolean comp; + GByteArray *bytes; + GError *err = NULL; + guint i; + + static const struct { + const gchar *topic; + void (*func) (FbApi *api, GByteArray *pload); + } parsers[] = { + {"/mark_thread_response", fb_api_cb_publish_mark}, + {"/mercury", fb_api_cb_publish_mercury}, + {"/orca_typing_notifications", fb_api_cb_publish_typing}, + {"/send_message_response", fb_api_cb_publish_ms_r}, + {"/t_ms", fb_api_cb_publish_ms}, + {"/t_p", fb_api_cb_publish_p} + }; + + comp = fb_util_zlib_test(pload); + + if (G_LIKELY(comp)) { + bytes = fb_util_zlib_inflate(pload, &err); + FB_API_ERROR_EMIT(api, err, return); + } else { + bytes = (GByteArray *) pload; + } + + fb_util_debug_hexdump(FB_UTIL_DEBUG_INFO, bytes, + "Reading message (topic: %s)", + topic); + + for (i = 0; i < G_N_ELEMENTS(parsers); i++) { + if (g_ascii_strcasecmp(topic, parsers[i].topic) == 0) { + parsers[i].func(api, bytes); + break; + } + } + + if (G_LIKELY(comp)) { + g_byte_array_free(bytes, TRUE); + } +} + +FbApi * +fb_api_new(PurpleConnection *gc) +{ + FbApi *api; + FbApiPrivate *priv; + + api = g_object_new(FB_TYPE_API, NULL); + priv = api->priv; + + priv->gc = gc; + priv->mqtt = fb_mqtt_new(gc); + + g_signal_connect(priv->mqtt, + "connect", + G_CALLBACK(fb_api_cb_mqtt_connect), + api); + g_signal_connect(priv->mqtt, + "error", + G_CALLBACK(fb_api_cb_mqtt_error), + api); + g_signal_connect(priv->mqtt, + "open", + G_CALLBACK(fb_api_cb_mqtt_open), + api); + g_signal_connect(priv->mqtt, + "publish", + G_CALLBACK(fb_api_cb_mqtt_publish), + api); + + return api; +} + +void +fb_api_rehash(FbApi *api) +{ + FbApiPrivate *priv; + + g_return_if_fail(FB_IS_API(api)); + priv = api->priv; + + if (priv->cid == NULL) { + priv->cid = fb_util_rand_alnum(32); + } + + if (priv->did == NULL) { + priv->did = purple_uuid_random(); + } + + if (priv->mid == 0) { + priv->mid = g_random_int(); + } + + if (strlen(priv->cid) > 20) { + priv->cid = g_realloc_n(priv->cid , 21, sizeof *priv->cid); + priv->cid[20] = 0; + } +} + +gboolean +fb_api_is_invisible(FbApi *api) +{ + FbApiPrivate *priv; + + g_return_val_if_fail(FB_IS_API(api), FALSE); + priv = api->priv; + + return priv->invisible; +} + +void +fb_api_error(FbApi *api, FbApiError error, const gchar *format, ...) +{ + GError *err; + va_list ap; + + g_return_if_fail(FB_IS_API(api)); + + va_start(ap, format); + err = g_error_new_valist(FB_API_ERROR, error, format, ap); + va_end(ap); + + fb_api_error_emit(api, err); +} + +void +fb_api_error_emit(FbApi *api, GError *error) +{ + g_return_if_fail(FB_IS_API(api)); + g_return_if_fail(error != NULL); + + g_signal_emit_by_name(api, "error", error); + g_error_free(error); +} + +static void +fb_api_cb_attach(PurpleHttpConnection *con, PurpleHttpResponse *res, + gpointer data) +{ + const gchar *str; + FbApi *api = data; + FbApiMessage *msg; + FbJsonValues *values; + gchar *name; + GError *err = NULL; + GSList *msgs = NULL; + guint i; + JsonNode *root; + + static const gchar *imgexts[] = {".jpg", ".png", ".gif"}; + + if (!fb_api_http_chk(api, con, res, &root)) { + return; + } + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, "$.filename"); + fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, "$.redirect_uri"); + fb_json_values_update(values, &err); + + FB_API_ERROR_EMIT(api, err, + g_object_unref(values); + json_node_free(root); + return; + ); + + msg = fb_api_data_take(api, con); + str = fb_json_values_next_str(values, NULL); + name = g_ascii_strdown(str, -1); + + for (i = 0; i < G_N_ELEMENTS(imgexts); i++) { + if (g_str_has_suffix(name, imgexts[i])) { + msg->flags |= FB_API_MESSAGE_FLAG_IMAGE; + break; + } + } + + g_free(name); + msg->text = fb_json_values_next_str_dup(values, NULL); + msgs = g_slist_prepend(msgs, msg); + + g_signal_emit_by_name(api, "messages", msgs); + g_slist_free_full(msgs, (GDestroyNotify) fb_api_message_free); + g_object_unref(values); + json_node_free(root); + +} + +static void +fb_api_attach(FbApi *api, FbId aid, const gchar *msgid, FbApiMessage *msg) +{ + FbHttpParams *prms; + PurpleHttpConnection *http; + + prms = fb_http_params_new(); + fb_http_params_set_str(prms, "mid", msgid); + fb_http_params_set_strf(prms, "aid", "%" FB_ID_FORMAT, aid); + + http = fb_api_http_req(api, FB_API_URL_ATTACH, "getAttachment", + "messaging.getAttachment", prms, + fb_api_cb_attach); + fb_api_data_set(api, http, msg, (GDestroyNotify) fb_api_message_free); +} + +static void +fb_api_cb_auth(PurpleHttpConnection *con, PurpleHttpResponse *res, + gpointer data) +{ + FbApi *api = data; + FbApiPrivate *priv = api->priv; + FbJsonValues *values; + GError *err = NULL; + JsonNode *root; + + if (!fb_api_http_chk(api, con, res, &root)) { + return; + } + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, "$.access_token"); + fb_json_values_add(values, FB_JSON_TYPE_INT, TRUE, "$.uid"); + fb_json_values_update(values, &err); + + FB_API_ERROR_EMIT(api, err, + g_object_unref(values); + json_node_free(root); + return; + ); + + g_free(priv->token); + priv->token = fb_json_values_next_str_dup(values, NULL); + priv->uid = fb_json_values_next_int(values, 0); + + g_signal_emit_by_name(api, "auth"); + g_object_unref(values); + json_node_free(root); +} + +void +fb_api_auth(FbApi *api, const gchar *user, const gchar *pass) +{ + FbHttpParams *prms; + + prms = fb_http_params_new(); + fb_http_params_set_str(prms, "email", user); + fb_http_params_set_str(prms, "password", pass); + fb_api_http_req(api, FB_API_URL_AUTH, "authenticate", "auth.login", + prms, fb_api_cb_auth); +} + +static gchar * +fb_api_user_icon_checksum(gchar *icon) +{ + gchar *csum; + FbHttpParams *prms; + + if (G_UNLIKELY(icon == NULL)) { + return NULL; + } + + prms = fb_http_params_new_parse(icon, TRUE); + csum = fb_http_params_dup_str(prms, "oh", NULL); + fb_http_params_free(prms); + + if (G_UNLIKELY(csum == NULL)) { + /* Revert to the icon URL as the unique checksum */ + csum = g_strdup(icon); + } + + return csum; +} + +static void +fb_api_cb_contact(PurpleHttpConnection *con, PurpleHttpResponse *res, + gpointer data) +{ + const gchar *str; + FbApi *api = data; + FbApiUser user; + FbJsonValues *values; + GError *err = NULL; + JsonNode *node; + JsonNode *root; + + if (!fb_api_http_chk(api, con, res, &root)) { + return; + } + + node = fb_json_node_get_nth(root, 0); + + if (node == NULL) { + fb_api_error(api, FB_API_ERROR_GENERAL, + _("Failed to obtain contact information")); + json_node_free(root); + return; + } + + values = fb_json_values_new(node); + fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, "$.id"); + fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, "$.name"); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.profile_pic_large.uri"); + fb_json_values_update(values, &err); + + FB_API_ERROR_EMIT(api, err, + g_object_unref(values); + json_node_free(root); + return; + ); + + fb_api_user_reset(&user, FALSE); + str = fb_json_values_next_str(values, "0"); + user.uid = FB_ID_FROM_STR(str); + user.name = fb_json_values_next_str_dup(values, NULL); + user.icon = fb_json_values_next_str_dup(values, NULL); + + user.csum = fb_api_user_icon_checksum(user.icon); + + g_signal_emit_by_name(api, "contact", &user); + fb_api_user_reset(&user, TRUE); + g_object_unref(values); + json_node_free(root); +} + +void +fb_api_contact(FbApi *api, FbId uid) +{ + JsonBuilder *bldr; + + bldr = fb_json_bldr_new(JSON_NODE_OBJECT); + fb_json_bldr_arr_begin(bldr, "0"); + fb_json_bldr_add_strf(bldr, NULL, "%" FB_ID_FORMAT, uid); + fb_json_bldr_arr_end(bldr); + + fb_json_bldr_add_str(bldr, "1", "true"); + fb_api_http_query(api, FB_API_QUERY_CONTACT, bldr, fb_api_cb_contact); +} + +static GSList * +fb_api_cb_contacts_nodes(FbApi *api, JsonNode *root, GSList *users) +{ + const gchar *str; + FbApiPrivate *priv = api->priv; + FbApiUser *user; + FbId uid; + FbJsonValues *values; + gboolean is_array; + GError *err = NULL; + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.represented_profile.id"); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.represented_profile.friendship_status"); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.structured_name.text"); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.hugePictureUrl.uri"); + + is_array = (JSON_NODE_TYPE(root) == JSON_NODE_ARRAY); + + if (is_array) { + fb_json_values_set_array(values, FALSE, "$"); + } + + while (fb_json_values_update(values, &err)) { + str = fb_json_values_next_str(values, "0"); + uid = FB_ID_FROM_STR(str); + str = fb_json_values_next_str(values, NULL); + + if ((!purple_strequal(str, "ARE_FRIENDS") && + (uid != priv->uid)) || (uid == 0)) + { + if (!is_array) { + break; + } + continue; + } + + user = fb_api_user_dup(NULL, FALSE); + user->uid = uid; + user->name = fb_json_values_next_str_dup(values, NULL); + user->icon = fb_json_values_next_str_dup(values, NULL); + + user->csum = fb_api_user_icon_checksum(user->icon); + + users = g_slist_prepend(users, user); + + if (!is_array) { + break; + } + } + + g_object_unref(values); + + return users; +} + +/* base64(contact:::) */ +static GSList * +fb_api_cb_contacts_parse_removed(FbApi *api, JsonNode *node, GSList *users) +{ + gsize len; + char **split; + char *decoded = (char *) g_base64_decode(json_node_get_string(node), &len); + + g_return_val_if_fail(decoded[len] == '\0', users); + g_return_val_if_fail(len == strlen(decoded), users); + g_return_val_if_fail(g_str_has_prefix(decoded, "contact:"), users); + + split = g_strsplit_set(decoded, ":", 4); + + g_return_val_if_fail(g_strv_length(split) == 4, users); + + users = g_slist_prepend(users, g_strdup(split[2])); + + g_strfreev(split); + g_free(decoded); + + return users; +} + +static void +fb_api_cb_contacts(PurpleHttpConnection *con, PurpleHttpResponse *res, + gpointer data) +{ + const gchar *cursor; + const gchar *delta_cursor; + FbApi *api = data; + FbApiPrivate *priv = api->priv; + FbJsonValues *values; + gboolean complete; + gboolean is_delta; + GError *err = NULL; + GList *l; + GSList *users = NULL; + JsonNode *root; + JsonNode *croot; + JsonNode *node; + + if (!fb_api_http_chk(api, con, res, &root)) { + return; + } + + croot = fb_json_node_get(root, "$.viewer.messenger_contacts.deltas", NULL); + is_delta = (croot != NULL); + + if (!is_delta) { + croot = fb_json_node_get(root, "$.viewer.messenger_contacts", NULL); + node = fb_json_node_get(croot, "$.nodes", NULL); + users = fb_api_cb_contacts_nodes(api, node, users); + json_node_free(node); + + } else { + GSList *added = NULL; + GSList *removed = NULL; + JsonArray *arr = fb_json_node_get_arr(croot, "$.nodes", NULL); + GList *elms = json_array_get_elements(arr); + + for (l = elms; l != NULL; l = l->next) { + if ((node = fb_json_node_get(l->data, "$.added", NULL))) { + added = fb_api_cb_contacts_nodes(api, node, added); + json_node_free(node); + } + + if ((node = fb_json_node_get(l->data, "$.removed", NULL))) { + removed = fb_api_cb_contacts_parse_removed(api, node, removed); + json_node_free(node); + } + } + + g_signal_emit_by_name(api, "contacts-delta", added, removed); + + g_slist_free_full(added, (GDestroyNotify) fb_api_user_free); + g_slist_free_full(removed, (GDestroyNotify) g_free); + + g_list_free(elms); + json_array_unref(arr); + } + + values = fb_json_values_new(croot); + fb_json_values_add(values, FB_JSON_TYPE_BOOL, FALSE, + "$.page_info.has_next_page"); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.page_info.delta_cursor"); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.page_info.end_cursor"); + fb_json_values_update(values, NULL); + + complete = !fb_json_values_next_bool(values, FALSE); + + delta_cursor = fb_json_values_next_str(values, NULL); + + cursor = fb_json_values_next_str(values, NULL); + + if (G_UNLIKELY(err == NULL)) { + if (is_delta || complete) { + g_free(priv->contacts_delta); + priv->contacts_delta = g_strdup(is_delta ? cursor : delta_cursor); + } + + if (users || (complete && !is_delta)) { + g_signal_emit_by_name(api, "contacts", users, complete); + } + + if (!complete) { + fb_api_contacts_after(api, cursor); + } + } else { + fb_api_error_emit(api, err); + } + + g_slist_free_full(users, (GDestroyNotify) fb_api_user_free); + g_object_unref(values); + + json_node_free(croot); + json_node_free(root); +} + +void +fb_api_contacts(FbApi *api) +{ + FbApiPrivate *priv; + JsonBuilder *bldr; + + g_return_if_fail(FB_IS_API(api)); + priv = api->priv; + + if (priv->contacts_delta) { + fb_api_contacts_delta(api, priv->contacts_delta); + return; + } + + bldr = fb_json_bldr_new(JSON_NODE_OBJECT); + fb_json_bldr_arr_begin(bldr, "0"); + fb_json_bldr_add_str(bldr, NULL, "user"); + fb_json_bldr_arr_end(bldr); + + fb_json_bldr_add_str(bldr, "1", G_STRINGIFY(FB_API_CONTACTS_COUNT)); + fb_api_http_query(api, FB_API_QUERY_CONTACTS, bldr, + fb_api_cb_contacts); +} + +static void +fb_api_contacts_after(FbApi *api, const gchar *cursor) +{ + JsonBuilder *bldr; + + bldr = fb_json_bldr_new(JSON_NODE_OBJECT); + fb_json_bldr_arr_begin(bldr, "0"); + fb_json_bldr_add_str(bldr, NULL, "user"); + fb_json_bldr_arr_end(bldr); + + fb_json_bldr_add_str(bldr, "1", cursor); + fb_json_bldr_add_str(bldr, "2", G_STRINGIFY(FB_API_CONTACTS_COUNT)); + fb_api_http_query(api, FB_API_QUERY_CONTACTS_AFTER, bldr, + fb_api_cb_contacts); +} + +void +fb_api_contacts_delta(FbApi *api, const gchar *delta_cursor) +{ + JsonBuilder *bldr; + + bldr = fb_json_bldr_new(JSON_NODE_OBJECT); + + fb_json_bldr_add_str(bldr, "0", delta_cursor); + + fb_json_bldr_arr_begin(bldr, "1"); + fb_json_bldr_add_str(bldr, NULL, "user"); + fb_json_bldr_arr_end(bldr); + + fb_json_bldr_add_str(bldr, "2", G_STRINGIFY(FB_API_CONTACTS_COUNT)); + fb_api_http_query(api, FB_API_QUERY_CONTACTS_DELTA, bldr, + fb_api_cb_contacts); +} + +void +fb_api_connect(FbApi *api, gboolean invisible) +{ + FbApiPrivate *priv; + + g_return_if_fail(FB_IS_API(api)); + priv = api->priv; + + priv->invisible = invisible; + fb_mqtt_open(priv->mqtt, FB_MQTT_HOST, FB_MQTT_PORT); +} + +void +fb_api_disconnect(FbApi *api) +{ + FbApiPrivate *priv; + + g_return_if_fail(FB_IS_API(api)); + priv = api->priv; + + fb_mqtt_disconnect(priv->mqtt); +} + +static void +fb_api_message_send(FbApi *api, FbApiMessage *msg) +{ + const gchar *tpfx; + FbApiPrivate *priv = api->priv; + FbId id; + FbId mid; + gchar *json; + JsonBuilder *bldr; + + mid = FB_API_MSGID(g_get_real_time() / 1000, g_random_int()); + priv->lastmid = mid; + + if (msg->tid != 0) { + tpfx = "tfbid_"; + id = msg->tid; + } else { + tpfx = ""; + id = msg->uid; + } + + bldr = fb_json_bldr_new(JSON_NODE_OBJECT); + fb_json_bldr_add_str(bldr, "body", msg->text); + fb_json_bldr_add_strf(bldr, "msgid", "%" FB_ID_FORMAT, mid); + fb_json_bldr_add_strf(bldr, "sender_fbid", "%" FB_ID_FORMAT, priv->uid); + fb_json_bldr_add_strf(bldr, "to", "%s%" FB_ID_FORMAT, tpfx, id); + + json = fb_json_bldr_close(bldr, JSON_NODE_OBJECT, NULL); + fb_api_publish(api, "/send_message2", "%s", json); + g_free(json); +} + +void +fb_api_message(FbApi *api, FbId id, gboolean thread, const gchar *text) +{ + FbApiMessage *msg; + FbApiPrivate *priv; + gboolean empty; + + g_return_if_fail(FB_IS_API(api)); + g_return_if_fail(text != NULL); + priv = api->priv; + + msg = fb_api_message_dup(NULL, FALSE); + msg->text = g_strdup(text); + + if (thread) { + msg->tid = id; + } else { + msg->uid = id; + } + + empty = g_queue_is_empty(priv->msgs); + g_queue_push_tail(priv->msgs, msg); + + if (empty && fb_mqtt_connected(priv->mqtt, FALSE)) { + fb_api_message_send(api, msg); + } +} + +void +fb_api_publish(FbApi *api, const gchar *topic, const gchar *format, ...) +{ + FbApiPrivate *priv; + GByteArray *bytes; + GByteArray *cytes; + gchar *msg; + GError *err = NULL; + va_list ap; + + g_return_if_fail(FB_IS_API(api)); + g_return_if_fail(topic != NULL); + g_return_if_fail(format != NULL); + priv = api->priv; + + va_start(ap, format); + msg = g_strdup_vprintf(format, ap); + va_end(ap); + + bytes = g_byte_array_new_take((guint8 *) msg, strlen(msg)); + cytes = fb_util_zlib_deflate(bytes, &err); + + FB_API_ERROR_EMIT(api, err, + g_byte_array_free(bytes, TRUE); + return; + ); + + fb_util_debug_hexdump(FB_UTIL_DEBUG_INFO, bytes, + "Writing message (topic: %s)", + topic); + + fb_mqtt_publish(priv->mqtt, topic, cytes); + g_byte_array_free(cytes, TRUE); + g_byte_array_free(bytes, TRUE); +} + +void +fb_api_read(FbApi *api, FbId id, gboolean thread) +{ + const gchar *key; + FbApiPrivate *priv; + gchar *json; + JsonBuilder *bldr; + + g_return_if_fail(FB_IS_API(api)); + priv = api->priv; + + bldr = fb_json_bldr_new(JSON_NODE_OBJECT); + fb_json_bldr_add_bool(bldr, "state", TRUE); + fb_json_bldr_add_int(bldr, "syncSeqId", priv->sid); + fb_json_bldr_add_str(bldr, "mark", "read"); + + key = thread ? "threadFbId" : "otherUserFbId"; + fb_json_bldr_add_strf(bldr, key, "%" FB_ID_FORMAT, id); + + json = fb_json_bldr_close(bldr, JSON_NODE_OBJECT, NULL); + fb_api_publish(api, "/mark_thread", "%s", json); + g_free(json); +} + +static GSList * +fb_api_cb_unread_parse_attach(FbApi *api, const gchar *mid, FbApiMessage *msg, + GSList *msgs, JsonNode *root, GError **error) +{ + const gchar *str; + FbApiMessage *dmsg; + FbId id; + FbJsonValues *values; + GError *err = NULL; + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, + "$.attachment_fbid"); + fb_json_values_set_array(values, FALSE, "$.blob_attachments"); + + while (fb_json_values_update(values, &err)) { + str = fb_json_values_next_str(values, NULL); + id = FB_ID_FROM_STR(str); + dmsg = fb_api_message_dup(msg, FALSE); + fb_api_attach(api, id, mid, dmsg); + } + + if (G_UNLIKELY(err != NULL)) { + g_propagate_error(error, err); + } + + g_object_unref(values); + return msgs; +} + +static void +fb_api_cb_unread_msgs(PurpleHttpConnection *con, PurpleHttpResponse *res, + gpointer data) +{ + const gchar *body; + const gchar *str; + FbApi *api = data; + FbApiMessage *dmsg; + FbApiMessage msg; + FbId id; + FbId tid; + FbJsonValues *values; + gchar *xma; + GError *err = NULL; + GSList *msgs = NULL; + JsonNode *node; + JsonNode *root; + JsonNode *xode; + + if (!fb_api_http_chk(api, con, res, &root)) { + return; + } + + node = fb_json_node_get_nth(root, 0); + + if (node == NULL) { + fb_api_error(api, FB_API_ERROR_GENERAL, + _("Failed to obtain unread messages")); + json_node_free(root); + return; + } + + values = fb_json_values_new(node); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.thread_key.thread_fbid"); + fb_json_values_update(values, &err); + + FB_API_ERROR_EMIT(api, err, + g_object_unref(values); + return; + ); + + fb_api_message_reset(&msg, FALSE); + str = fb_json_values_next_str(values, "0"); + tid = FB_ID_FROM_STR(str); + g_object_unref(values); + + values = fb_json_values_new(node); + fb_json_values_add(values, FB_JSON_TYPE_BOOL, TRUE, "$.unread"); + fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, + "$.message_sender.messaging_actor.id"); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, "$.message.text"); + fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, + "$.timestamp_precise"); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, "$.sticker.id"); + fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, "$.message_id"); + fb_json_values_set_array(values, FALSE, "$.messages.nodes"); + + while (fb_json_values_update(values, &err)) { + if (!fb_json_values_next_bool(values, FALSE)) { + continue; + } + + str = fb_json_values_next_str(values, "0"); + body = fb_json_values_next_str(values, NULL); + + fb_api_message_reset(&msg, FALSE); + msg.uid = FB_ID_FROM_STR(str); + msg.tid = tid; + + str = fb_json_values_next_str(values, "0"); + msg.tstamp = g_ascii_strtoll(str, NULL, 10); + + if (body != NULL) { + dmsg = fb_api_message_dup(&msg, FALSE); + dmsg->text = g_strdup(body); + msgs = g_slist_prepend(msgs, dmsg); + } + + str = fb_json_values_next_str(values, NULL); + + if (str != NULL) { + dmsg = fb_api_message_dup(&msg, FALSE); + id = FB_ID_FROM_STR(str); + fb_api_sticker(api, id, dmsg); + } + + node = fb_json_values_get_root(values); + xode = fb_json_node_get(node, "$.extensible_attachment", NULL); + + if (xode != NULL) { + xma = fb_api_xma_parse(api, body, xode, &err); + + if (xma != NULL) { + dmsg = fb_api_message_dup(&msg, FALSE); + dmsg->text = xma; + msgs = g_slist_prepend(msgs, dmsg); + } + + json_node_free(xode); + + if (G_UNLIKELY(err != NULL)) { + break; + } + } + + str = fb_json_values_next_str(values, NULL); + + if (str == NULL) { + continue; + } + + msgs = fb_api_cb_unread_parse_attach(api, str, &msg, msgs, + node, &err); + + if (G_UNLIKELY(err != NULL)) { + break; + } + } + + if (G_UNLIKELY(err == NULL)) { + msgs = g_slist_reverse(msgs); + g_signal_emit_by_name(api, "messages", msgs); + } else { + fb_api_error_emit(api, err); + } + + g_slist_free_full(msgs, (GDestroyNotify) fb_api_message_free); + g_object_unref(values); + json_node_free(root); +} + +static void +fb_api_cb_unread(PurpleHttpConnection *con, PurpleHttpResponse *res, + gpointer data) +{ + const gchar *id; + FbApi *api = data; + FbJsonValues *values; + GError *err = NULL; + gint64 count; + JsonBuilder *bldr; + JsonNode *root; + + if (!fb_api_http_chk(api, con, res, &root)) { + return; + } + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_INT, TRUE, "$.unread_count"); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.thread_key.other_user_id"); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.thread_key.thread_fbid"); + fb_json_values_set_array(values, FALSE, "$.viewer.message_threads" + ".nodes"); + + while (fb_json_values_update(values, &err)) { + count = fb_json_values_next_int(values, -5); + + if (count < 1) { + continue; + } + + id = fb_json_values_next_str(values, NULL); + + if (id == NULL) { + id = fb_json_values_next_str(values, "0"); + } + + bldr = fb_json_bldr_new(JSON_NODE_OBJECT); + fb_json_bldr_arr_begin(bldr, "0"); + fb_json_bldr_add_str(bldr, NULL, id); + fb_json_bldr_arr_end(bldr); + + fb_json_bldr_add_str(bldr, "10", "true"); + fb_json_bldr_add_str(bldr, "11", "true"); + fb_json_bldr_add_int(bldr, "12", count); + fb_json_bldr_add_str(bldr, "13", "false"); + fb_api_http_query(api, FB_API_QUERY_THREAD, bldr, + fb_api_cb_unread_msgs); + } + + if (G_UNLIKELY(err != NULL)) { + fb_api_error_emit(api, err); + } + + g_object_unref(values); + json_node_free(root); +} + +void +fb_api_unread(FbApi *api) +{ + FbApiPrivate *priv; + JsonBuilder *bldr; + + g_return_if_fail(FB_IS_API(api)); + priv = api->priv; + + if (priv->unread < 1) { + return; + } + + bldr = fb_json_bldr_new(JSON_NODE_OBJECT); + fb_json_bldr_add_str(bldr, "2", "true"); + fb_json_bldr_add_int(bldr, "1", priv->unread); + fb_json_bldr_add_str(bldr, "12", "true"); + fb_json_bldr_add_str(bldr, "13", "false"); + fb_api_http_query(api, FB_API_QUERY_THREADS, bldr, + fb_api_cb_unread); +} + +static void +fb_api_cb_sticker(PurpleHttpConnection *con, PurpleHttpResponse *res, + gpointer data) +{ + FbApi *api = data; + FbApiMessage *msg; + FbJsonValues *values; + GError *err = NULL; + GSList *msgs = NULL; + JsonNode *node; + JsonNode *root; + + if (!fb_api_http_chk(api, con, res, &root)) { + return; + } + + node = fb_json_node_get_nth(root, 0); + values = fb_json_values_new(node); + fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, + "$.thread_image.uri"); + fb_json_values_update(values, &err); + + FB_API_ERROR_EMIT(api, err, + g_object_unref(values); + json_node_free(root); + return; + ); + + msg = fb_api_data_take(api, con); + msg->flags |= FB_API_MESSAGE_FLAG_IMAGE; + msg->text = fb_json_values_next_str_dup(values, NULL); + msgs = g_slist_prepend(msgs, msg); + + g_signal_emit_by_name(api, "messages", msgs); + g_slist_free_full(msgs, (GDestroyNotify) fb_api_message_free); + g_object_unref(values); + json_node_free(root); +} + +static void +fb_api_sticker(FbApi *api, FbId sid, FbApiMessage *msg) +{ + JsonBuilder *bldr; + PurpleHttpConnection *http; + + bldr = fb_json_bldr_new(JSON_NODE_OBJECT); + fb_json_bldr_arr_begin(bldr, "0"); + fb_json_bldr_add_strf(bldr, NULL, "%" FB_ID_FORMAT, sid); + fb_json_bldr_arr_end(bldr); + + http = fb_api_http_query(api, FB_API_QUERY_STICKER, bldr, + fb_api_cb_sticker); + fb_api_data_set(api, http, msg, (GDestroyNotify) fb_api_message_free); +} + +static gboolean +fb_api_thread_parse(FbApi *api, FbApiThread *thrd, JsonNode *root, + GError **error) +{ + const gchar *str; + FbApiPrivate *priv = api->priv; + FbApiUser *user; + FbId uid; + FbJsonValues *values; + gboolean haself = FALSE; + guint num_users = 0; + GError *err = NULL; + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.thread_key.thread_fbid"); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, "$.name"); + fb_json_values_update(values, &err); + + if (G_UNLIKELY(err != NULL)) { + g_propagate_error(error, err); + g_object_unref(values); + return FALSE; + } + + str = fb_json_values_next_str(values, NULL); + + if (str == NULL) { + g_object_unref(values); + return FALSE; + } + + thrd->tid = FB_ID_FROM_STR(str); + thrd->topic = fb_json_values_next_str_dup(values, NULL); + g_object_unref(values); + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, + "$.messaging_actor.id"); + fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, + "$.messaging_actor.name"); + fb_json_values_set_array(values, TRUE, "$.all_participants.nodes"); + + while (fb_json_values_update(values, &err)) { + str = fb_json_values_next_str(values, "0"); + uid = FB_ID_FROM_STR(str); + num_users++; + + if (uid != priv->uid) { + user = fb_api_user_dup(NULL, FALSE); + user->uid = uid; + user->name = fb_json_values_next_str_dup(values, NULL); + thrd->users = g_slist_prepend(thrd->users, user); + } else { + haself = TRUE; + } + } + + if (G_UNLIKELY(err != NULL)) { + g_propagate_error(error, err); + fb_api_thread_reset(thrd, TRUE); + g_object_unref(values); + return FALSE; + } + + if (num_users < 2 || !haself) { + g_object_unref(values); + return FALSE; + } + + g_object_unref(values); + return TRUE; +} + +static void +fb_api_cb_thread(PurpleHttpConnection *con, PurpleHttpResponse *res, + gpointer data) +{ + FbApi *api = data; + FbApiThread thrd; + GError *err = NULL; + JsonNode *node; + JsonNode *root; + + if (!fb_api_http_chk(api, con, res, &root)) { + return; + } + + node = fb_json_node_get_nth(root, 0); + + if (node == NULL) { + fb_api_error(api, FB_API_ERROR_GENERAL, + _("Failed to obtain thread information")); + json_node_free(root); + return; + } + + fb_api_thread_reset(&thrd, FALSE); + + if (!fb_api_thread_parse(api, &thrd, node, &err)) { + if (G_LIKELY(err == NULL)) { + if (thrd.tid) { + g_signal_emit_by_name(api, "thread-kicked", &thrd); + } else { + fb_api_error(api, FB_API_ERROR_GENERAL, + _("Failed to parse thread information")); + } + } else { + fb_api_error_emit(api, err); + } + } else { + g_signal_emit_by_name(api, "thread", &thrd); + } + + fb_api_thread_reset(&thrd, TRUE); + json_node_free(root); +} + +void +fb_api_thread(FbApi *api, FbId tid) +{ + JsonBuilder *bldr; + + bldr = fb_json_bldr_new(JSON_NODE_OBJECT); + fb_json_bldr_arr_begin(bldr, "0"); + fb_json_bldr_add_strf(bldr, NULL, "%" FB_ID_FORMAT, tid); + fb_json_bldr_arr_end(bldr); + + fb_json_bldr_add_str(bldr, "10", "false"); + fb_json_bldr_add_str(bldr, "11", "false"); + fb_json_bldr_add_str(bldr, "13", "false"); + fb_api_http_query(api, FB_API_QUERY_THREAD, bldr, fb_api_cb_thread); +} + +static void +fb_api_cb_thread_create(PurpleHttpConnection *con, PurpleHttpResponse *res, + gpointer data) +{ + const gchar *str; + FbApi *api = data; + FbId tid; + FbJsonValues *values; + GError *err = NULL; + JsonNode *root; + + if (!fb_api_http_chk(api, con, res, &root)) { + return; + } + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, "$.id"); + fb_json_values_update(values, &err); + + FB_API_ERROR_EMIT(api, err, + g_object_unref(values); + json_node_free(root); + return; + ); + + str = fb_json_values_next_str(values, "0"); + tid = FB_ID_FROM_STR(str); + g_signal_emit_by_name(api, "thread-create", tid); + + g_object_unref(values); + json_node_free(root); +} + +void +fb_api_thread_create(FbApi *api, GSList *uids) +{ + FbApiPrivate *priv; + FbHttpParams *prms; + FbId *uid; + gchar *json; + GSList *l; + JsonBuilder *bldr; + + g_return_if_fail(FB_IS_API(api)); + g_warn_if_fail(g_slist_length(uids) > 1); + priv = api->priv; + + bldr = fb_json_bldr_new(JSON_NODE_ARRAY); + fb_json_bldr_obj_begin(bldr, NULL); + fb_json_bldr_add_str(bldr, "type", "id"); + fb_json_bldr_add_strf(bldr, "id", "%" FB_ID_FORMAT, priv->uid); + fb_json_bldr_obj_end(bldr); + + for (l = uids; l != NULL; l = l->next) { + uid = l->data; + fb_json_bldr_obj_begin(bldr, NULL); + fb_json_bldr_add_str(bldr, "type", "id"); + fb_json_bldr_add_strf(bldr, "id", "%" FB_ID_FORMAT, *uid); + fb_json_bldr_obj_end(bldr); + } + + json = fb_json_bldr_close(bldr, JSON_NODE_ARRAY, NULL); + prms = fb_http_params_new(); + fb_http_params_set_str(prms, "recipients", json); + fb_api_http_req(api, FB_API_URL_THREADS, "createGroup", "POST", + prms, fb_api_cb_thread_create); + g_free(json); +} + +void +fb_api_thread_invite(FbApi *api, FbId tid, FbId uid) +{ + FbHttpParams *prms; + gchar *json; + JsonBuilder *bldr; + + bldr = fb_json_bldr_new(JSON_NODE_ARRAY); + fb_json_bldr_obj_begin(bldr, NULL); + fb_json_bldr_add_str(bldr, "type", "id"); + fb_json_bldr_add_strf(bldr, "id", "%" FB_ID_FORMAT, uid); + fb_json_bldr_obj_end(bldr); + json = fb_json_bldr_close(bldr, JSON_NODE_ARRAY, NULL); + + prms = fb_http_params_new(); + fb_http_params_set_str(prms, "to", json); + fb_http_params_set_strf(prms, "id", "t_%" FB_ID_FORMAT, tid); + fb_api_http_req(api, FB_API_URL_PARTS, "addMembers", "POST", + prms, fb_api_cb_http_bool); + g_free(json); +} + +void +fb_api_thread_remove(FbApi *api, FbId tid, FbId uid) +{ + FbApiPrivate *priv; + FbHttpParams *prms; + gchar *json; + JsonBuilder *bldr; + + g_return_if_fail(FB_IS_API(api)); + priv = api->priv; + + prms = fb_http_params_new(); + fb_http_params_set_strf(prms, "id", "t_%" FB_ID_FORMAT, tid); + + if (uid == 0) { + uid = priv->uid; + } + + if (uid != priv->uid) { + bldr = fb_json_bldr_new(JSON_NODE_ARRAY); + fb_json_bldr_add_strf(bldr, NULL, "%" FB_ID_FORMAT, uid); + json = fb_json_bldr_close(bldr, JSON_NODE_ARRAY, NULL); + fb_http_params_set_str(prms, "to", json); + g_free(json); + } + + fb_api_http_req(api, FB_API_URL_PARTS, "removeMembers", "DELETE", + prms, fb_api_cb_http_bool); +} + +void +fb_api_thread_topic(FbApi *api, FbId tid, const gchar *topic) +{ + FbHttpParams *prms; + + prms = fb_http_params_new(); + fb_http_params_set_str(prms, "name", topic); + fb_http_params_set_int(prms, "tid", tid); + fb_api_http_req(api, FB_API_URL_TOPIC, "setThreadName", + "messaging.setthreadname", prms, + fb_api_cb_http_bool); +} + +static void +fb_api_cb_threads(PurpleHttpConnection *con, PurpleHttpResponse *res, + gpointer data) +{ + FbApi *api = data; + FbApiThread *dthrd; + FbApiThread thrd; + GError *err = NULL; + GList *elms; + GList *l; + GSList *thrds = NULL; + JsonArray *arr; + JsonNode *root; + + if (!fb_api_http_chk(api, con, res, &root)) { + return; + } + + arr = fb_json_node_get_arr(root, "$.viewer.message_threads.nodes", + &err); + FB_API_ERROR_EMIT(api, err, + json_node_free(root); + return; + ); + + elms = json_array_get_elements(arr); + + for (l = elms; l != NULL; l = l->next) { + fb_api_thread_reset(&thrd, FALSE); + + if (fb_api_thread_parse(api, &thrd, l->data, &err)) { + dthrd = fb_api_thread_dup(&thrd, FALSE); + thrds = g_slist_prepend(thrds, dthrd); + } else { + fb_api_thread_reset(&thrd, TRUE); + } + + if (G_UNLIKELY(err != NULL)) { + break; + } + } + + if (G_LIKELY(err == NULL)) { + thrds = g_slist_reverse(thrds); + g_signal_emit_by_name(api, "threads", thrds); + } else { + fb_api_error_emit(api, err); + } + + g_slist_free_full(thrds, (GDestroyNotify) fb_api_thread_free); + g_list_free(elms); + json_array_unref(arr); + json_node_free(root); +} + +void +fb_api_threads(FbApi *api) +{ + JsonBuilder *bldr; + + bldr = fb_json_bldr_new(JSON_NODE_OBJECT); + fb_json_bldr_add_str(bldr, "2", "true"); + fb_json_bldr_add_str(bldr, "12", "false"); + fb_json_bldr_add_str(bldr, "13", "false"); + fb_api_http_query(api, FB_API_QUERY_THREADS, bldr, fb_api_cb_threads); +} + +void +fb_api_typing(FbApi *api, FbId uid, gboolean state) +{ + gchar *json; + JsonBuilder *bldr; + + bldr = fb_json_bldr_new(JSON_NODE_OBJECT); + fb_json_bldr_add_int(bldr, "state", state != 0); + fb_json_bldr_add_strf(bldr, "to", "%" FB_ID_FORMAT, uid); + + json = fb_json_bldr_close(bldr, JSON_NODE_OBJECT, NULL); + fb_api_publish(api, "/typing", "%s", json); + g_free(json); +} + +FbApiEvent * +fb_api_event_dup(const FbApiEvent *event, gboolean deep) +{ + FbApiEvent *ret; + + if (event == NULL) { + return g_new0(FbApiEvent, 1); + } + + ret = g_memdup(event, sizeof *event); + + if (deep) { + ret->text = g_strdup(event->text); + } + + return ret; +} + +void +fb_api_event_reset(FbApiEvent *event, gboolean deep) +{ + g_return_if_fail(event != NULL); + + if (deep) { + g_free(event->text); + } + + memset(event, 0, sizeof *event); +} + +void +fb_api_event_free(FbApiEvent *event) +{ + if (G_LIKELY(event != NULL)) { + g_free(event->text); + g_free(event); + } +} + +FbApiMessage * +fb_api_message_dup(const FbApiMessage *msg, gboolean deep) +{ + FbApiMessage *ret; + + if (msg == NULL) { + return g_new0(FbApiMessage, 1); + } + + ret = g_memdup(msg, sizeof *msg); + + if (deep) { + ret->text = g_strdup(msg->text); + } + + return ret; +} + +void +fb_api_message_reset(FbApiMessage *msg, gboolean deep) +{ + g_return_if_fail(msg != NULL); + + if (deep) { + g_free(msg->text); + } + + memset(msg, 0, sizeof *msg); +} + +void +fb_api_message_free(FbApiMessage *msg) +{ + if (G_LIKELY(msg != NULL)) { + g_free(msg->text); + g_free(msg); + } +} + +FbApiPresence * +fb_api_presence_dup(const FbApiPresence *pres) +{ + if (pres == NULL) { + return g_new0(FbApiPresence, 1); + } + + return g_memdup(pres, sizeof *pres); +} + +void +fb_api_presence_reset(FbApiPresence *pres) +{ + g_return_if_fail(pres != NULL); + memset(pres, 0, sizeof *pres); +} + +void +fb_api_presence_free(FbApiPresence *pres) +{ + if (G_LIKELY(pres != NULL)) { + g_free(pres); + } +} + +FbApiThread * +fb_api_thread_dup(const FbApiThread *thrd, gboolean deep) +{ + FbApiThread *ret; + FbApiUser *user; + GSList *l; + + if (thrd == NULL) { + return g_new0(FbApiThread, 1); + } + + ret = g_memdup(thrd, sizeof *thrd); + + if (deep) { + ret->users = NULL; + + for (l = thrd->users; l != NULL; l = l->next) { + user = fb_api_user_dup(l->data, TRUE); + ret->users = g_slist_prepend(ret->users, user); + } + + ret->topic = g_strdup(thrd->topic); + ret->users = g_slist_reverse(ret->users); + } + + return ret; +} + +void +fb_api_thread_reset(FbApiThread *thrd, gboolean deep) +{ + g_return_if_fail(thrd != NULL); + + if (deep) { + g_slist_free_full(thrd->users, (GDestroyNotify) fb_api_user_free); + g_free(thrd->topic); + } + + memset(thrd, 0, sizeof *thrd); +} + +void +fb_api_thread_free(FbApiThread *thrd) +{ + if (G_LIKELY(thrd != NULL)) { + g_slist_free_full(thrd->users, (GDestroyNotify) fb_api_user_free); + g_free(thrd->topic); + g_free(thrd); + } +} + +FbApiTyping * +fb_api_typing_dup(const FbApiTyping *typg) +{ + if (typg == NULL) { + return g_new0(FbApiTyping, 1); + } + + return g_memdup(typg, sizeof *typg); +} + +void +fb_api_typing_reset(FbApiTyping *typg) +{ + g_return_if_fail(typg != NULL); + memset(typg, 0, sizeof *typg); +} + +void +fb_api_typing_free(FbApiTyping *typg) +{ + if (G_LIKELY(typg != NULL)) { + g_free(typg); + } +} + +FbApiUser * +fb_api_user_dup(const FbApiUser *user, gboolean deep) +{ + FbApiUser *ret; + + if (user == NULL) { + return g_new0(FbApiUser, 1); + } + + ret = g_memdup(user, sizeof *user); + + if (deep) { + ret->name = g_strdup(user->name); + ret->icon = g_strdup(user->icon); + ret->csum = g_strdup(user->csum); + } + + return ret; +} + +void +fb_api_user_reset(FbApiUser *user, gboolean deep) +{ + g_return_if_fail(user != NULL); + + if (deep) { + g_free(user->name); + g_free(user->icon); + g_free(user->csum); + } + + memset(user, 0, sizeof *user); +} + +void +fb_api_user_free(FbApiUser *user) +{ + if (G_LIKELY(user != NULL)) { + g_free(user->name); + g_free(user->icon); + g_free(user->csum); + g_free(user); + } +} diff --git a/facebook/api.h b/facebook/api.h new file mode 100644 index 00000000..63c9a1b9 --- /dev/null +++ b/facebook/api.h @@ -0,0 +1,1006 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#ifndef _FACEBOOK_API_H_ +#define _FACEBOOK_API_H_ + +/** + * SECTION:api + * @section_id: facebook-api + * @short_description: api.h + * @title: Facebook API + * + * The API for interacting with the Facebook Messenger protocol. + */ + +#include "internal.h" + +#include + +#include "connection.h" + +#include "http.h" +#include "id.h" +#include "mqtt.h" + +#define FB_TYPE_API (fb_api_get_type()) +#define FB_API(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), FB_TYPE_API, FbApi)) +#define FB_API_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), FB_TYPE_API, FbApiClass)) +#define FB_IS_API(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), FB_TYPE_API)) +#define FB_IS_API_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), FB_TYPE_API)) +#define FB_API_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), FB_TYPE_API, FbApiClass)) + +/** + * FB_API_AHOST: + * + * The HTTP host for the Facebook API. + */ +#define FB_API_AHOST "https://api.facebook.com" + +/** + * FB_API_BHOST: + * + * The HTTP host for the Facebook BAPI. + */ +#define FB_API_BHOST "https://b-api.facebook.com" + +/** + * FB_API_GHOST: + * + * The HTTP host for the Facebook Graph API. + */ +#define FB_API_GHOST "https://graph.facebook.com" + +/** + * FB_API_WHOST: + * + * The HTTP host for the Facebook website. + */ +#define FB_API_WHOST "https://www.facebook.com" + +/** + * FB_API_FBRPC_PREFIX + * + * The fbrpc URL prefix used in links shared from the mobile app. + */ +#define FB_API_FBRPC_PREFIX "fbrpc://facebook/nativethirdparty" + +/** + * FB_API_KEY: + * + * The Facebook API key. + */ +#define FB_API_KEY "256002347743983" + +/** + * FB_API_SECRET: + * + * The Facebook API secret. + */ +#define FB_API_SECRET "374e60f8b9bb6b8cbb30f78030438895" + +/** + * FB_ORCA_AGENT + * + * The part of the user agent that looks like the official client, since the + * server started checking this. + */ + +#define FB_ORCA_AGENT "[FBAN/Orca-Android;FBAV/537.0.0.31.101;FBPN/com.facebook.orca;FBLC/en_US;FBBV/52182662]" + +/** + * FB_API_AGENT: + * + * The HTTP User-Agent header. + */ +#define FB_API_AGENT "Facebook plugin / Purple / " PACKAGE_VERSION " " FB_ORCA_AGENT + +/** + * FB_API_MQTT_AGENT + * + * The client information string sent in the MQTT CONNECT message + */ + +#define FB_API_MQTT_AGENT FB_API_AGENT + +/** + * FB_API_URL_ATTACH: + * + * The URL for attachment URL requests. + */ +#define FB_API_URL_ATTACH FB_API_AHOST "/method/messaging.getAttachment" +//#define FB_API_URL_ATTACH FB_API_AHOST "/method/messaging.attachmentRedirect" + +/** + * FB_API_URL_AUTH: + * + * The URL for authentication requests. + */ +#define FB_API_URL_AUTH FB_API_BHOST "/method/auth.login" + +/** + * FB_API_URL_GQL: + * + * The URL for GraphQL requests. + */ +#define FB_API_URL_GQL FB_API_GHOST "/graphql" + +/** + * FB_API_URL_MESSAGES: + * + * The URL for linking message threads. + */ +#define FB_API_URL_MESSAGES FB_API_WHOST "/messages" + +/** + * FB_API_URL_PARTS: + * + * The URL for participant management requests. + */ +#define FB_API_URL_PARTS FB_API_GHOST "/participants" + +/** + * FB_API_URL_THREADS: + * + * The URL for thread management requests. + */ +#define FB_API_URL_THREADS FB_API_GHOST "/me/group_threads" + +/** + * FB_API_URL_TOPIC: + * + * The URL for thread topic requests. + */ +#define FB_API_URL_TOPIC FB_API_AHOST "/method/messaging.setthreadname" + +/** + * FB_API_QUERY_CONTACT: + * + * The query hash for the `UsersQuery`. + * + * Key mapping: + * 0: user_fbids + * 1: include_full_user_info + * 2: profile_pic_large_size + * 3: profile_pic_medium_size + * 4: profile_pic_small_size + */ +#define FB_API_QUERY_CONTACT 10153915107411729 + +/** + * FB_API_QUERY_CONTACTS: + * + * The query hash for the `FetchContactsFullQuery`. + * + * Key mapping: + * 0: profile_types + * 1: limit + * 2: big_img_size + * 3: huge_img_size + * 4: small_img_size + */ +#define FB_API_QUERY_CONTACTS 10154444360806729 + +/** + * FB_API_QUERY_CONTACTS_AFTER: + * + * The query hash for the `FetchContactsFullWithAfterQuery`. + * + * Key mapping: + * 0: profile_types + * 1: after + * 2: limit + * 3: big_img_size + * 4: huge_img_size + * 5: small_img_size + */ +#define FB_API_QUERY_CONTACTS_AFTER 10154444360816729 + + +/** + * FB_API_QUERY_CONTACTS_DELTA: + * + * The query hash for the `FetchContactsDeltaQuery`. + * + * Key mapping: + * 0: after + * 1: profile_types + * 2: limit + * 3: big_img_size + * 4: huge_img_size + * 5: small_img_size + */ +#define FB_API_QUERY_CONTACTS_DELTA 10154444360801729 + +/** + * FB_API_QUERY_STICKER: + * + * The query hash for the `FetchStickersWithPreviewsQuery`. + * + * Key mapping: + * 0: sticker_ids + * 1: media_type + * 2: preview_size + * 3: scaling_factor + * 4: animated_media_type + */ +#define FB_API_QUERY_STICKER 10152877994321729 + +/** + * FB_API_QUERY_THREAD: + * + * The query hash for the `ThreadQuery`. + * + * Key mapping: + * 0: thread_ids + * 1: verification_type + * 2: hash_key + * 3: small_preview_size + * 4: large_preview_size + * 5: item_count + * 6: event_count + * 7: full_screen_height + * 8: full_screen_width + * 9: medium_preview_size + * 10: fetch_users_separately + * 11: include_message_info + * 12: msg_count + * 13: include_full_user_info + * 14: profile_pic_large_size + * 15: profile_pic_medium_size + * 16: profile_pic_small_size + */ +#define FB_API_QUERY_THREAD 10153919752036729 + +/** + * FB_API_QUERY_THREADS: + * + * The query hash for the `ThreadListQuery`. + * + * Key mapping: + * 0: folder_tag + * 1: thread_count + * 2: include_thread_info + * 3: verification_type + * 4: hash_key + * 5: small_preview_size + * 6: large_preview_size + * 7: item_count + * 8: event_count + * 9: full_screen_height + * 10: full_screen_width + * 11: medium_preview_size + * 12: fetch_users_separately + * 13: include_message_info + * 14: msg_count + * 15: UNKNOWN + * 16: profile_pic_large_size + * 17: profile_pic_medium_size + * 18: profile_pic_small_size + */ +#define FB_API_QUERY_THREADS 10153919752026729 + +/** + * FB_API_QUERY_SEQ_ID: + * + * A variant of ThreadListQuery with sequence ID + * + * TODO: parameters. + */ + +#define FB_API_QUERY_SEQ_ID 10155268192741729 + +/** + * FB_API_QUERY_XMA: + * + * The query hash for the `XMAQuery`. + * + * Key mapping: + * 0: xma_id + */ +#define FB_API_QUERY_XMA 10153919431161729 + +/** + * FB_API_CONTACTS_COUNT: + * + * The maximum amount of contacts to fetch in a single request. If this + * value is set too high, HTTP request will fail. This is due to the + * request data being too large. + */ +#define FB_API_CONTACTS_COUNT 500 + +/** + * FB_API_TCHK: + * @e: The expression. + * + * Checks the Thrift related expression to ensure that it evaluates to + * #TRUE. If the expression evaluates to #FALSE, a #GError is assigned + * to the local `error` variable, then returns with no value. + * + * This macro is meant to only be used for Thrift related expressions, + * where the calling function has a `void` return type. This macro also + * requires the existence of a predefined `error` variable, which is a + * pointer of a pointer to a #GError. + */ +#define FB_API_TCHK(e) \ + G_STMT_START { \ + if (G_UNLIKELY(!(e))) { \ + g_set_error(error, FB_API_ERROR, FB_API_ERROR_GENERAL, \ + "Failed to read thrift: %s:%d " \ + "%s: assertion '%s' failed", \ + __FILE__, __LINE__, G_STRFUNC, #e); \ + return; \ + } \ + } G_STMT_END + +/** + * FB_API_MSGID: + * @m: The time in milliseconds. + * @i: The random integer. + * + * Creates a 64-bit message identifier in the Facebook format. + * + * Returns: The message identifier. + */ +#define FB_API_MSGID(m, i) ((guint64) ( \ + (((guint32) i) & 0x3FFFFF) | \ + (((guint64) m) << 22) \ + )) + +/** + * FB_API_ERROR_EMIT: + * @a: The #FbApi. + * @e: The #FbApiError. + * @c: The code to execute. + * + * Emits a #GError on behalf of the #FbApi. + */ +#define FB_API_ERROR_EMIT(a, e, c) \ + G_STMT_START { \ + if (G_UNLIKELY((e) != NULL)) { \ + fb_api_error_emit(a, e); \ + {c;} \ + } \ + } G_STMT_END + +/** + * FB_API_ERROR: + * + * The #GQuark of the domain of API errors. + */ +#define FB_API_ERROR fb_api_error_quark() + +typedef struct _FbApi FbApi; +typedef struct _FbApiClass FbApiClass; +typedef struct _FbApiPrivate FbApiPrivate; +typedef struct _FbApiEvent FbApiEvent; +typedef struct _FbApiMessage FbApiMessage; +typedef struct _FbApiPresence FbApiPresence; +typedef struct _FbApiThread FbApiThread; +typedef struct _FbApiTyping FbApiTyping; +typedef struct _FbApiUser FbApiUser; + +/** + * FbApiError: + * @FB_API_ERROR_GENERAL: General failure. + * @FB_API_ERROR_AUTH: Authentication failure. + * @FB_API_ERROR_QUEUE: Queue failure. + * @FB_API_ERROR_NONFATAL: Other non-fatal errors. + * + * The error codes for the #FB_API_ERROR domain. + */ +typedef enum +{ + FB_API_ERROR_GENERAL, + FB_API_ERROR_AUTH, + FB_API_ERROR_QUEUE, + FB_API_ERROR_NONFATAL +} FbApiError; + +/** + * FbApiEventType: + * @FB_API_EVENT_TYPE_THREAD_TOPIC: The thread topic was changed. + * @FB_API_EVENT_TYPE_THREAD_USER_ADDED: A thread user was added. + * @FB_API_EVENT_TYPE_THREAD_USER_REMOVED: A thread user was removed. + * + * The #FbApiEvent types. + */ +typedef enum +{ + FB_API_EVENT_TYPE_THREAD_TOPIC, + FB_API_EVENT_TYPE_THREAD_USER_ADDED, + FB_API_EVENT_TYPE_THREAD_USER_REMOVED +} FbApiEventType; + +/** + * FbApiMessageFlags: + * @FB_API_MESSAGE_FLAG_DONE: The text has been processed. + * @FB_API_MESSAGE_FLAG_IMAGE: The text is a URL to an image. + * @FB_API_MESSAGE_FLAG_SELF: The text is from the #FbApi user. + * + * The #FbApiMessage flags. + */ +typedef enum +{ + FB_API_MESSAGE_FLAG_DONE = 1 << 0, + FB_API_MESSAGE_FLAG_IMAGE = 1 << 1, + FB_API_MESSAGE_FLAG_SELF = 1 << 2 +} FbApiMessageFlags; + +/** + * FbApi: + * + * Represents a Facebook Messenger connection. + */ +struct _FbApi +{ + /*< private >*/ + GObject parent; + FbApiPrivate *priv; +}; + +/** + * FbApiClass: + * + * The base class for all #FbApi's. + */ +struct _FbApiClass +{ + /*< private >*/ + GObjectClass parent_class; +}; + +/** + * FbApiEvent: + * @type: The #FbApiEventType. + * @uid: The user #FbId. + * @tid: The thread #FbId. + * @text: The event text. + * + * Represents a Facebook update event. + */ +struct _FbApiEvent +{ + FbApiEventType type; + FbId uid; + FbId tid; + gchar *text; +}; + +/** + * FbApiMessage: + * @flags: The #FbApiMessageFlags. + * @uid: The user #FbId. + * @tid: The thread #FbId. + * @tstamp: The timestamp in milliseconds (UTC). + * @text: The message text. + * + * Represents a Facebook user message. + */ +struct _FbApiMessage +{ + FbApiMessageFlags flags; + FbId uid; + FbId tid; + gint64 tstamp; + gchar *text; +}; + +/** + * FbApiPresence: + * @uid: The user #FbId. + * @active: #TRUE if the user is active, otherwise #FALSE. + * + * Represents a Facebook presence message. + */ +struct _FbApiPresence +{ + FbId uid; + gboolean active; +}; + +/** + * FbApiThread: + * @tid: The thread #FbId. + * @topic: The topic. + * @users: The #GSList of #FbApiUser's. + * + * Represents a Facebook message thread. + */ +struct _FbApiThread +{ + FbId tid; + gchar *topic; + GSList *users; +}; + +/** + * FbApiTyping: + * @uid: The user #FbId. + * @state: #TRUE if the user is typing, otherwise #FALSE. + * + * Represents a Facebook typing message. + */ +struct _FbApiTyping +{ + FbId uid; + gboolean state; +}; + +/** + * FbApiUser: + * @uid: The user #FbId. + * @name: The name of the user. + * @icon: The icon URL. + * @csum: The checksum of @icon. + * + * Represents a Facebook user. + */ +struct _FbApiUser +{ + FbId uid; + gchar *name; + gchar *icon; + gchar *csum; +}; + +/** + * fb_api_get_type: + * + * Returns: The #GType for an #FbApi. + */ +GType +fb_api_get_type(void); + +/** + * fb_api_error_quark: + * + * Gets the #GQuark of the domain of API errors. + * + * Returns: The #GQuark of the domain. + */ +GQuark +fb_api_error_quark(void); + +/** + * fb_api_new: + * @gc: The #PurpleConnection. + * + * Creates a new #FbApi. The returned #FbApi should be freed with + * #g_object_unref() when no longer needed. + * + * Returns: The new #FbApi. + */ +FbApi * +fb_api_new(PurpleConnection *gc); + +/** + * fb_api_rehash: + * @api: The #FbApi. + * + * Rehashes and updates internal data of the #FbApi. This should be + * called whenever properties are modified. + */ +void +fb_api_rehash(FbApi *api); + +/** + * fb_api_is_invisible: + * @api: The #FbApi. + * + * Determines if the user of the #FbApi is invisible. + * + * Returns: #TRUE if the #FbApi user is invisible, otherwise #FALSE. + */ +gboolean +fb_api_is_invisible(FbApi *api); + +/** + * fb_api_error: + * @api: The #FbApi. + * @error: The #FbApiError. + * @format: The format string literal. + * @...: The arguments for @format. + * + * Emits an #FbApiError. + */ +void +fb_api_error(FbApi *api, FbApiError error, const gchar *format, ...) + G_GNUC_PRINTF(3, 4); + +/** + * fb_api_error_emit: + * @api: The #FbApi. + * @error: The #GError. + * + * Emits a #GError on an #FbApiError. + */ +void +fb_api_error_emit(FbApi *api, GError *error); + +/** + * fb_api_auth: + * @api: The #FbApi. + * @user: The Facebook user name, email, or phone number. + * @pass: The Facebook password. + * + * Sends an authentication request to Facebook. This will obtain + * session information, which is required for all other requests. + */ +void +fb_api_auth(FbApi *api, const gchar *user, const gchar *pass); + +/** + * fb_api_contact: + * @api: The #FbApi. + * @uid: The user #FbId. + * + * Sends a contact request. This will obtain the general information of + * a single contact. + */ +void +fb_api_contact(FbApi *api, FbId uid); + +/** + * fb_api_contacts: + * @api: The #FbApi. + * + * Sends a contacts request. This will obtain a full list of detailed + * contact information about the friends of the #FbApi user. + */ +void +fb_api_contacts(FbApi *api); + +/** + * fb_api_connect: + * @api: The #FbApi. + * @invisible: #TRUE to make the user invisible, otherwise #FALSE. + * + * Initializes and establishes the underlying MQTT connection. + */ +void +fb_api_connect(FbApi *api, gboolean invisible); + +/** + * fb_api_disconnect: + * @api: The #FbApi. + * + * Closes the underlying MQTT connection. + */ +void +fb_api_disconnect(FbApi *api); + +/** + * fb_api_message: + * @api: The #FbApi. + * @id: The user or thread #FbId. + * @thread: #TRUE if @id is a thread, otherwise #FALSE. + * @text: The message text. + * + * Sends a message as the user of the #FbApi to a user or a thread. + */ +void +fb_api_message(FbApi *api, FbId id, gboolean thread, const gchar *text); + +/** + * fb_api_publish: + * @api: The #FbApi. + * @topic: The topic. + * @format: The format string literal. + * @...: The arguments for @format. + * + * Publishes an MQTT message. + */ +void +fb_api_publish(FbApi *api, const gchar *topic, const gchar *format, ...) + G_GNUC_PRINTF(3, 4); + +/** + * fb_api_read: + * @api: The #FbApi. + * @id: The user or thread #FbId. + * @thread: #TRUE if @id is a thread, otherwise #FALSE. + * + * Marks a message thread as read. + */ +void +fb_api_read(FbApi *api, FbId id, gboolean thread); + +/** + * fb_api_unread: + * @api: The #FbApi. + * + * Sends an unread message request. + */ +void +fb_api_unread(FbApi *api); + +/** + * fb_api_thread: + * @api: The #FbApi. + * @tid: The thread #FbId. + * + * Sends a thread request. This will obtain the general information of + * a single thread. + */ +void +fb_api_thread(FbApi *api, FbId tid); + +/** + * fb_api_thread_create: + * @api: The #FbApi. + * @uids: The #GSList of #FbId's. + * + * Sends a thread creation request. In order to create a thread, there + * must be at least two other users in @uids. + */ +void +fb_api_thread_create(FbApi *api, GSList *uids); + +/** + * fb_api_thread_invite: + * @api: The #FbApi. + * @tid: The thread #FbId. + * @uid: The user #FbId. + * + * Sends a thread user invitation request. + */ +void +fb_api_thread_invite(FbApi *api, FbId tid, FbId uid); + +/** + * fb_api_thread_remove: + * @api: The #FbApi. + * @tid: The thread #FbId. + * @uid: The user #FbId. + * + * Sends a thread user removal request. + */ +void +fb_api_thread_remove(FbApi *api, FbId tid, FbId uid); + +/** + * fb_api_thread_topic: + * @api: The #FbApi. + * @tid: The thread #FbId. + * @topic: The topic. + * + * Sends a thread topic change request. + */ +void +fb_api_thread_topic(FbApi *api, FbId tid, const gchar *topic); + +/** + * fb_api_threads: + * @api: The #FbApi. + * + * Sends a threads request. This will obtain a full list of detailed + * thread information about the threads of the #FbApi user. + */ +void +fb_api_threads(FbApi *api); + +/** + * fb_api_typing: + * @api: The #FbApi. + * @uid: The user #FbId. + * @state: #TRUE if the #FbApi user is typing, otherwise #FALSE. + * + * Sends a typing state message for the user of the #FbApi. + */ +void +fb_api_typing(FbApi *api, FbId uid, gboolean state); + +/** + * fb_api_event_dup: + * @event: The #FbApiEvent or #NULL. + * @deep: #TRUE to duplicate allocated data, otherwise #FALSE. + * + * Duplicates an #FbApiEvent. If @event is #NULL, a new zero filled + * #FbApiEvent is returned. The returned #FbApiEvent should be freed + * with #fb_api_event_free() when no longer needed. + * + * Returns: The new #FbApiEvent. + */ +FbApiEvent * +fb_api_event_dup(const FbApiEvent *event, gboolean deep); + +/** + * fb_api_event_reset: + * @event: The #FbApiEvent. + * @deep: #TRUE to free allocated data, otherwise #FALSE. + * + * Resets an #FbApiEvent. + */ +void +fb_api_event_reset(FbApiEvent *event, gboolean deep); + +/** + * fb_api_event_free: + * @event: The #FbApiEvent. + * + * Frees all memory used by the #FbApiEvent. + */ +void +fb_api_event_free(FbApiEvent *event); + +/** + * fb_api_message_dup: + * @msg: The #FbApiMessage or #NULL. + * @deep: #TRUE to duplicate allocated data, otherwise #FALSE. + * + * Duplicates an #FbApiMessage. If @msg is #NULL, a new zero filled + * #FbApiMessage is returned. The returned #FbApiMessage should be + * freed with #fb_api_message_free() when no longer needed. + * + * Returns: The new #FbApiMessage. + */ +FbApiMessage * +fb_api_message_dup(const FbApiMessage *msg, gboolean deep); + +/** + * fb_api_message_reset: + * @msg: The #FbApiMessage. + * @deep: #TRUE to free allocated data, otherwise #FALSE. + * + * Resets an #FbApiMessage. + */ +void +fb_api_message_reset(FbApiMessage *msg, gboolean deep); + +/** + * fb_api_message_free: + * @msg: The #FbApiMessage. + * + * Frees all memory used by the #FbApiMessage. + */ +void +fb_api_message_free(FbApiMessage *msg); + +/** + * fb_api_presence_dup: + * @pres: The #FbApiPresence or #NULL. + * + * Duplicates an #FbApiPresence. If @pres is #NULL, a new zero filled + * #FbApiPresence is returned. The returned #FbApiPresence should be + * freed with #fb_api_presence_free() when no longer needed. + * + * Returns: The new #FbApiPresence. + */ +FbApiPresence * +fb_api_presence_dup(const FbApiPresence *pres); + +/** + * fb_api_presence_reset: + * @pres: The #FbApiPresence. + * + * Resets an #FbApiPresence. + */ +void +fb_api_presence_reset(FbApiPresence *pres); + +/** + * fb_api_presence_free: + * @pres: The #FbApiPresence. + * + * Frees all memory used by the #FbApiPresence. + */ +void +fb_api_presence_free(FbApiPresence *pres); + +/** + * fb_api_thread_dup: + * @thrd: The #FbApiThread or #NULL. + * @deep: #TRUE to duplicate allocated data, otherwise #FALSE. + * + * Duplicates an #FbApiThread. If @thrd is #NULL, a new zero filled + * #FbApiThread is returned. The returned #FbApiThread should be freed + * with #fb_api_thread_free() when no longer needed. + * + * Returns: The new #FbApiThread. + */ +FbApiThread * +fb_api_thread_dup(const FbApiThread *thrd, gboolean deep); + +/** + * fb_api_thread_reset: + * @thrd: The #FbApiThread. + * @deep: #TRUE to free allocated data, otherwise #FALSE. + * + * Resets an #FbApiThread. + */ +void +fb_api_thread_reset(FbApiThread *thrd, gboolean deep); + +/** + * fb_api_thread_free: + * @thrd: The #FbApiThread. + * + * Frees all memory used by the #FbApiThread. + */ +void +fb_api_thread_free(FbApiThread *thrd); + +/** + * fb_api_typing_dup: + * @typg: The #FbApiTyping or #NULL. + * + * Duplicates an #FbApiTyping. If @typg is #NULL, a new zero filled + * #FbApiTyping is returned. The returned #FbApiTyping should be freed + * with #fb_api_typing_free() when no longer needed. + * + * Returns: The new #FbApiTyping. + */ +FbApiTyping * +fb_api_typing_dup(const FbApiTyping *typg); + +/** + * fb_api_typing_reset: + * @typg: The #FbApiTyping. + * + * Resets an #FbApiTyping. + */ +void +fb_api_typing_reset(FbApiTyping *typg); + +/** + * fb_api_typing_free: + * @typg: The #FbApiTyping. + * + * Frees all memory used by the #FbApiTyping. + */ +void +fb_api_typing_free(FbApiTyping *typg); + +/** + * fb_api_user_dup: + * @user: The #FbApiUser or #NULL. + * @deep: #TRUE to duplicate allocated data, otherwise #FALSE. + * + * Duplicates an #FbApiUser. If @user is #NULL, a new zero filled + * #FbApiUser is returned. The returned #FbApiUser should be freed with + * #fb_api_user_free() when no longer needed. + * + * Returns: The new #FbApiUser. + */ +FbApiUser * +fb_api_user_dup(const FbApiUser *user, gboolean deep); + +/** + * fb_api_user_reset: + * @user: The #FbApiUser. + * @deep: #TRUE to free allocated data, otherwise #FALSE. + * + * Resets an #FbApiUser. + */ +void +fb_api_user_reset(FbApiUser *user, gboolean deep); + +/** + * fb_api_user_free: + * @user: The #FbApiUser. + * + * Frees all memory used by the #FbApiUser. + */ +void +fb_api_user_free(FbApiUser *user); + +#endif /* _FACEBOOK_API_H_ */ diff --git a/facebook/api.h.orig b/facebook/api.h.orig new file mode 100644 index 00000000..b0531ed2 --- /dev/null +++ b/facebook/api.h.orig @@ -0,0 +1,1006 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#ifndef _FACEBOOK_API_H_ +#define _FACEBOOK_API_H_ + +/** + * SECTION:api + * @section_id: facebook-api + * @short_description: api.h + * @title: Facebook API + * + * The API for interacting with the Facebook Messenger protocol. + */ + +#include "internal.h" + +#include + +#include "connection.h" + +#include "http.h" +#include "id.h" +#include "mqtt.h" + +#define FB_TYPE_API (fb_api_get_type()) +#define FB_API(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), FB_TYPE_API, FbApi)) +#define FB_API_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), FB_TYPE_API, FbApiClass)) +#define FB_IS_API(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), FB_TYPE_API)) +#define FB_IS_API_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), FB_TYPE_API)) +#define FB_API_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), FB_TYPE_API, FbApiClass)) + +/** + * FB_API_AHOST: + * + * The HTTP host for the Facebook API. + */ +#define FB_API_AHOST "https://api.facebook.com" + +/** + * FB_API_BHOST: + * + * The HTTP host for the Facebook BAPI. + */ +#define FB_API_BHOST "https://b-api.facebook.com" + +/** + * FB_API_GHOST: + * + * The HTTP host for the Facebook Graph API. + */ +#define FB_API_GHOST "https://graph.facebook.com" + +/** + * FB_API_WHOST: + * + * The HTTP host for the Facebook website. + */ +#define FB_API_WHOST "https://www.facebook.com" + +/** + * FB_API_FBRPC_PREFIX + * + * The fbrpc URL prefix used in links shared from the mobile app. + */ +#define FB_API_FBRPC_PREFIX "fbrpc://facebook/nativethirdparty" + +/** + * FB_API_KEY: + * + * The Facebook API key. + */ +#define FB_API_KEY "256002347743983" + +/** + * FB_API_SECRET: + * + * The Facebook API secret. + */ +#define FB_API_SECRET "374e60f8b9bb6b8cbb30f78030438895" + +/** + * FB_ORCA_AGENT + * + * The part of the user agent that looks like the official client, since the + * server started checking this. + */ + +#define FB_ORCA_AGENT "[FBAN/Orca-Android;FBAV/192.0.0.31.101;FBPN/com.facebook.orca;FBLC/en_US;FBBV/52182662]" + +/** + * FB_API_AGENT: + * + * The HTTP User-Agent header. + */ +#define FB_API_AGENT "Facebook plugin / Purple / " PACKAGE_VERSION " " FB_ORCA_AGENT + +/** + * FB_API_MQTT_AGENT + * + * The client information string sent in the MQTT CONNECT message + */ + +#define FB_API_MQTT_AGENT FB_API_AGENT + +/** + * FB_API_URL_ATTACH: + * + * The URL for attachment URL requests. + */ +#define FB_API_URL_ATTACH FB_API_AHOST "/method/messaging.getAttachment" +//#define FB_API_URL_ATTACH FB_API_AHOST "/method/messaging.attachmentRedirect" + +/** + * FB_API_URL_AUTH: + * + * The URL for authentication requests. + */ +#define FB_API_URL_AUTH FB_API_BHOST "/method/auth.login" + +/** + * FB_API_URL_GQL: + * + * The URL for GraphQL requests. + */ +#define FB_API_URL_GQL FB_API_GHOST "/graphql" + +/** + * FB_API_URL_MESSAGES: + * + * The URL for linking message threads. + */ +#define FB_API_URL_MESSAGES FB_API_WHOST "/messages" + +/** + * FB_API_URL_PARTS: + * + * The URL for participant management requests. + */ +#define FB_API_URL_PARTS FB_API_GHOST "/participants" + +/** + * FB_API_URL_THREADS: + * + * The URL for thread management requests. + */ +#define FB_API_URL_THREADS FB_API_GHOST "/me/group_threads" + +/** + * FB_API_URL_TOPIC: + * + * The URL for thread topic requests. + */ +#define FB_API_URL_TOPIC FB_API_AHOST "/method/messaging.setthreadname" + +/** + * FB_API_QUERY_CONTACT: + * + * The query hash for the `UsersQuery`. + * + * Key mapping: + * 0: user_fbids + * 1: include_full_user_info + * 2: profile_pic_large_size + * 3: profile_pic_medium_size + * 4: profile_pic_small_size + */ +#define FB_API_QUERY_CONTACT 10153915107411729 + +/** + * FB_API_QUERY_CONTACTS: + * + * The query hash for the `FetchContactsFullQuery`. + * + * Key mapping: + * 0: profile_types + * 1: limit + * 2: big_img_size + * 3: huge_img_size + * 4: small_img_size + */ +#define FB_API_QUERY_CONTACTS 10154444360806729 + +/** + * FB_API_QUERY_CONTACTS_AFTER: + * + * The query hash for the `FetchContactsFullWithAfterQuery`. + * + * Key mapping: + * 0: profile_types + * 1: after + * 2: limit + * 3: big_img_size + * 4: huge_img_size + * 5: small_img_size + */ +#define FB_API_QUERY_CONTACTS_AFTER 10154444360816729 + + +/** + * FB_API_QUERY_CONTACTS_DELTA: + * + * The query hash for the `FetchContactsDeltaQuery`. + * + * Key mapping: + * 0: after + * 1: profile_types + * 2: limit + * 3: big_img_size + * 4: huge_img_size + * 5: small_img_size + */ +#define FB_API_QUERY_CONTACTS_DELTA 10154444360801729 + +/** + * FB_API_QUERY_STICKER: + * + * The query hash for the `FetchStickersWithPreviewsQuery`. + * + * Key mapping: + * 0: sticker_ids + * 1: media_type + * 2: preview_size + * 3: scaling_factor + * 4: animated_media_type + */ +#define FB_API_QUERY_STICKER 10152877994321729 + +/** + * FB_API_QUERY_THREAD: + * + * The query hash for the `ThreadQuery`. + * + * Key mapping: + * 0: thread_ids + * 1: verification_type + * 2: hash_key + * 3: small_preview_size + * 4: large_preview_size + * 5: item_count + * 6: event_count + * 7: full_screen_height + * 8: full_screen_width + * 9: medium_preview_size + * 10: fetch_users_separately + * 11: include_message_info + * 12: msg_count + * 13: include_full_user_info + * 14: profile_pic_large_size + * 15: profile_pic_medium_size + * 16: profile_pic_small_size + */ +#define FB_API_QUERY_THREAD 10153919752036729 + +/** + * FB_API_QUERY_THREADS: + * + * The query hash for the `ThreadListQuery`. + * + * Key mapping: + * 0: folder_tag + * 1: thread_count + * 2: include_thread_info + * 3: verification_type + * 4: hash_key + * 5: small_preview_size + * 6: large_preview_size + * 7: item_count + * 8: event_count + * 9: full_screen_height + * 10: full_screen_width + * 11: medium_preview_size + * 12: fetch_users_separately + * 13: include_message_info + * 14: msg_count + * 15: UNKNOWN + * 16: profile_pic_large_size + * 17: profile_pic_medium_size + * 18: profile_pic_small_size + */ +#define FB_API_QUERY_THREADS 10153919752026729 + +/** + * FB_API_QUERY_SEQ_ID: + * + * A variant of ThreadListQuery with sequence ID + * + * TODO: parameters. + */ + +#define FB_API_QUERY_SEQ_ID 10155268192741729 + +/** + * FB_API_QUERY_XMA: + * + * The query hash for the `XMAQuery`. + * + * Key mapping: + * 0: xma_id + */ +#define FB_API_QUERY_XMA 10153919431161729 + +/** + * FB_API_CONTACTS_COUNT: + * + * The maximum amount of contacts to fetch in a single request. If this + * value is set too high, HTTP request will fail. This is due to the + * request data being too large. + */ +#define FB_API_CONTACTS_COUNT 500 + +/** + * FB_API_TCHK: + * @e: The expression. + * + * Checks the Thrift related expression to ensure that it evaluates to + * #TRUE. If the expression evaluates to #FALSE, a #GError is assigned + * to the local `error` variable, then returns with no value. + * + * This macro is meant to only be used for Thrift related expressions, + * where the calling function has a `void` return type. This macro also + * requires the existence of a predefined `error` variable, which is a + * pointer of a pointer to a #GError. + */ +#define FB_API_TCHK(e) \ + G_STMT_START { \ + if (G_UNLIKELY(!(e))) { \ + g_set_error(error, FB_API_ERROR, FB_API_ERROR_GENERAL, \ + "Failed to read thrift: %s:%d " \ + "%s: assertion '%s' failed", \ + __FILE__, __LINE__, G_STRFUNC, #e); \ + return; \ + } \ + } G_STMT_END + +/** + * FB_API_MSGID: + * @m: The time in milliseconds. + * @i: The random integer. + * + * Creates a 64-bit message identifier in the Facebook format. + * + * Returns: The message identifier. + */ +#define FB_API_MSGID(m, i) ((guint64) ( \ + (((guint32) i) & 0x3FFFFF) | \ + (((guint64) m) << 22) \ + )) + +/** + * FB_API_ERROR_EMIT: + * @a: The #FbApi. + * @e: The #FbApiError. + * @c: The code to execute. + * + * Emits a #GError on behalf of the #FbApi. + */ +#define FB_API_ERROR_EMIT(a, e, c) \ + G_STMT_START { \ + if (G_UNLIKELY((e) != NULL)) { \ + fb_api_error_emit(a, e); \ + {c;} \ + } \ + } G_STMT_END + +/** + * FB_API_ERROR: + * + * The #GQuark of the domain of API errors. + */ +#define FB_API_ERROR fb_api_error_quark() + +typedef struct _FbApi FbApi; +typedef struct _FbApiClass FbApiClass; +typedef struct _FbApiPrivate FbApiPrivate; +typedef struct _FbApiEvent FbApiEvent; +typedef struct _FbApiMessage FbApiMessage; +typedef struct _FbApiPresence FbApiPresence; +typedef struct _FbApiThread FbApiThread; +typedef struct _FbApiTyping FbApiTyping; +typedef struct _FbApiUser FbApiUser; + +/** + * FbApiError: + * @FB_API_ERROR_GENERAL: General failure. + * @FB_API_ERROR_AUTH: Authentication failure. + * @FB_API_ERROR_QUEUE: Queue failure. + * @FB_API_ERROR_NONFATAL: Other non-fatal errors. + * + * The error codes for the #FB_API_ERROR domain. + */ +typedef enum +{ + FB_API_ERROR_GENERAL, + FB_API_ERROR_AUTH, + FB_API_ERROR_QUEUE, + FB_API_ERROR_NONFATAL +} FbApiError; + +/** + * FbApiEventType: + * @FB_API_EVENT_TYPE_THREAD_TOPIC: The thread topic was changed. + * @FB_API_EVENT_TYPE_THREAD_USER_ADDED: A thread user was added. + * @FB_API_EVENT_TYPE_THREAD_USER_REMOVED: A thread user was removed. + * + * The #FbApiEvent types. + */ +typedef enum +{ + FB_API_EVENT_TYPE_THREAD_TOPIC, + FB_API_EVENT_TYPE_THREAD_USER_ADDED, + FB_API_EVENT_TYPE_THREAD_USER_REMOVED +} FbApiEventType; + +/** + * FbApiMessageFlags: + * @FB_API_MESSAGE_FLAG_DONE: The text has been processed. + * @FB_API_MESSAGE_FLAG_IMAGE: The text is a URL to an image. + * @FB_API_MESSAGE_FLAG_SELF: The text is from the #FbApi user. + * + * The #FbApiMessage flags. + */ +typedef enum +{ + FB_API_MESSAGE_FLAG_DONE = 1 << 0, + FB_API_MESSAGE_FLAG_IMAGE = 1 << 1, + FB_API_MESSAGE_FLAG_SELF = 1 << 2 +} FbApiMessageFlags; + +/** + * FbApi: + * + * Represents a Facebook Messenger connection. + */ +struct _FbApi +{ + /*< private >*/ + GObject parent; + FbApiPrivate *priv; +}; + +/** + * FbApiClass: + * + * The base class for all #FbApi's. + */ +struct _FbApiClass +{ + /*< private >*/ + GObjectClass parent_class; +}; + +/** + * FbApiEvent: + * @type: The #FbApiEventType. + * @uid: The user #FbId. + * @tid: The thread #FbId. + * @text: The event text. + * + * Represents a Facebook update event. + */ +struct _FbApiEvent +{ + FbApiEventType type; + FbId uid; + FbId tid; + gchar *text; +}; + +/** + * FbApiMessage: + * @flags: The #FbApiMessageFlags. + * @uid: The user #FbId. + * @tid: The thread #FbId. + * @tstamp: The timestamp in milliseconds (UTC). + * @text: The message text. + * + * Represents a Facebook user message. + */ +struct _FbApiMessage +{ + FbApiMessageFlags flags; + FbId uid; + FbId tid; + gint64 tstamp; + gchar *text; +}; + +/** + * FbApiPresence: + * @uid: The user #FbId. + * @active: #TRUE if the user is active, otherwise #FALSE. + * + * Represents a Facebook presence message. + */ +struct _FbApiPresence +{ + FbId uid; + gboolean active; +}; + +/** + * FbApiThread: + * @tid: The thread #FbId. + * @topic: The topic. + * @users: The #GSList of #FbApiUser's. + * + * Represents a Facebook message thread. + */ +struct _FbApiThread +{ + FbId tid; + gchar *topic; + GSList *users; +}; + +/** + * FbApiTyping: + * @uid: The user #FbId. + * @state: #TRUE if the user is typing, otherwise #FALSE. + * + * Represents a Facebook typing message. + */ +struct _FbApiTyping +{ + FbId uid; + gboolean state; +}; + +/** + * FbApiUser: + * @uid: The user #FbId. + * @name: The name of the user. + * @icon: The icon URL. + * @csum: The checksum of @icon. + * + * Represents a Facebook user. + */ +struct _FbApiUser +{ + FbId uid; + gchar *name; + gchar *icon; + gchar *csum; +}; + +/** + * fb_api_get_type: + * + * Returns: The #GType for an #FbApi. + */ +GType +fb_api_get_type(void); + +/** + * fb_api_error_quark: + * + * Gets the #GQuark of the domain of API errors. + * + * Returns: The #GQuark of the domain. + */ +GQuark +fb_api_error_quark(void); + +/** + * fb_api_new: + * @gc: The #PurpleConnection. + * + * Creates a new #FbApi. The returned #FbApi should be freed with + * #g_object_unref() when no longer needed. + * + * Returns: The new #FbApi. + */ +FbApi * +fb_api_new(PurpleConnection *gc); + +/** + * fb_api_rehash: + * @api: The #FbApi. + * + * Rehashes and updates internal data of the #FbApi. This should be + * called whenever properties are modified. + */ +void +fb_api_rehash(FbApi *api); + +/** + * fb_api_is_invisible: + * @api: The #FbApi. + * + * Determines if the user of the #FbApi is invisible. + * + * Returns: #TRUE if the #FbApi user is invisible, otherwise #FALSE. + */ +gboolean +fb_api_is_invisible(FbApi *api); + +/** + * fb_api_error: + * @api: The #FbApi. + * @error: The #FbApiError. + * @format: The format string literal. + * @...: The arguments for @format. + * + * Emits an #FbApiError. + */ +void +fb_api_error(FbApi *api, FbApiError error, const gchar *format, ...) + G_GNUC_PRINTF(3, 4); + +/** + * fb_api_error_emit: + * @api: The #FbApi. + * @error: The #GError. + * + * Emits a #GError on an #FbApiError. + */ +void +fb_api_error_emit(FbApi *api, GError *error); + +/** + * fb_api_auth: + * @api: The #FbApi. + * @user: The Facebook user name, email, or phone number. + * @pass: The Facebook password. + * + * Sends an authentication request to Facebook. This will obtain + * session information, which is required for all other requests. + */ +void +fb_api_auth(FbApi *api, const gchar *user, const gchar *pass); + +/** + * fb_api_contact: + * @api: The #FbApi. + * @uid: The user #FbId. + * + * Sends a contact request. This will obtain the general information of + * a single contact. + */ +void +fb_api_contact(FbApi *api, FbId uid); + +/** + * fb_api_contacts: + * @api: The #FbApi. + * + * Sends a contacts request. This will obtain a full list of detailed + * contact information about the friends of the #FbApi user. + */ +void +fb_api_contacts(FbApi *api); + +/** + * fb_api_connect: + * @api: The #FbApi. + * @invisible: #TRUE to make the user invisible, otherwise #FALSE. + * + * Initializes and establishes the underlying MQTT connection. + */ +void +fb_api_connect(FbApi *api, gboolean invisible); + +/** + * fb_api_disconnect: + * @api: The #FbApi. + * + * Closes the underlying MQTT connection. + */ +void +fb_api_disconnect(FbApi *api); + +/** + * fb_api_message: + * @api: The #FbApi. + * @id: The user or thread #FbId. + * @thread: #TRUE if @id is a thread, otherwise #FALSE. + * @text: The message text. + * + * Sends a message as the user of the #FbApi to a user or a thread. + */ +void +fb_api_message(FbApi *api, FbId id, gboolean thread, const gchar *text); + +/** + * fb_api_publish: + * @api: The #FbApi. + * @topic: The topic. + * @format: The format string literal. + * @...: The arguments for @format. + * + * Publishes an MQTT message. + */ +void +fb_api_publish(FbApi *api, const gchar *topic, const gchar *format, ...) + G_GNUC_PRINTF(3, 4); + +/** + * fb_api_read: + * @api: The #FbApi. + * @id: The user or thread #FbId. + * @thread: #TRUE if @id is a thread, otherwise #FALSE. + * + * Marks a message thread as read. + */ +void +fb_api_read(FbApi *api, FbId id, gboolean thread); + +/** + * fb_api_unread: + * @api: The #FbApi. + * + * Sends an unread message request. + */ +void +fb_api_unread(FbApi *api); + +/** + * fb_api_thread: + * @api: The #FbApi. + * @tid: The thread #FbId. + * + * Sends a thread request. This will obtain the general information of + * a single thread. + */ +void +fb_api_thread(FbApi *api, FbId tid); + +/** + * fb_api_thread_create: + * @api: The #FbApi. + * @uids: The #GSList of #FbId's. + * + * Sends a thread creation request. In order to create a thread, there + * must be at least two other users in @uids. + */ +void +fb_api_thread_create(FbApi *api, GSList *uids); + +/** + * fb_api_thread_invite: + * @api: The #FbApi. + * @tid: The thread #FbId. + * @uid: The user #FbId. + * + * Sends a thread user invitation request. + */ +void +fb_api_thread_invite(FbApi *api, FbId tid, FbId uid); + +/** + * fb_api_thread_remove: + * @api: The #FbApi. + * @tid: The thread #FbId. + * @uid: The user #FbId. + * + * Sends a thread user removal request. + */ +void +fb_api_thread_remove(FbApi *api, FbId tid, FbId uid); + +/** + * fb_api_thread_topic: + * @api: The #FbApi. + * @tid: The thread #FbId. + * @topic: The topic. + * + * Sends a thread topic change request. + */ +void +fb_api_thread_topic(FbApi *api, FbId tid, const gchar *topic); + +/** + * fb_api_threads: + * @api: The #FbApi. + * + * Sends a threads request. This will obtain a full list of detailed + * thread information about the threads of the #FbApi user. + */ +void +fb_api_threads(FbApi *api); + +/** + * fb_api_typing: + * @api: The #FbApi. + * @uid: The user #FbId. + * @state: #TRUE if the #FbApi user is typing, otherwise #FALSE. + * + * Sends a typing state message for the user of the #FbApi. + */ +void +fb_api_typing(FbApi *api, FbId uid, gboolean state); + +/** + * fb_api_event_dup: + * @event: The #FbApiEvent or #NULL. + * @deep: #TRUE to duplicate allocated data, otherwise #FALSE. + * + * Duplicates an #FbApiEvent. If @event is #NULL, a new zero filled + * #FbApiEvent is returned. The returned #FbApiEvent should be freed + * with #fb_api_event_free() when no longer needed. + * + * Returns: The new #FbApiEvent. + */ +FbApiEvent * +fb_api_event_dup(const FbApiEvent *event, gboolean deep); + +/** + * fb_api_event_reset: + * @event: The #FbApiEvent. + * @deep: #TRUE to free allocated data, otherwise #FALSE. + * + * Resets an #FbApiEvent. + */ +void +fb_api_event_reset(FbApiEvent *event, gboolean deep); + +/** + * fb_api_event_free: + * @event: The #FbApiEvent. + * + * Frees all memory used by the #FbApiEvent. + */ +void +fb_api_event_free(FbApiEvent *event); + +/** + * fb_api_message_dup: + * @msg: The #FbApiMessage or #NULL. + * @deep: #TRUE to duplicate allocated data, otherwise #FALSE. + * + * Duplicates an #FbApiMessage. If @msg is #NULL, a new zero filled + * #FbApiMessage is returned. The returned #FbApiMessage should be + * freed with #fb_api_message_free() when no longer needed. + * + * Returns: The new #FbApiMessage. + */ +FbApiMessage * +fb_api_message_dup(const FbApiMessage *msg, gboolean deep); + +/** + * fb_api_message_reset: + * @msg: The #FbApiMessage. + * @deep: #TRUE to free allocated data, otherwise #FALSE. + * + * Resets an #FbApiMessage. + */ +void +fb_api_message_reset(FbApiMessage *msg, gboolean deep); + +/** + * fb_api_message_free: + * @msg: The #FbApiMessage. + * + * Frees all memory used by the #FbApiMessage. + */ +void +fb_api_message_free(FbApiMessage *msg); + +/** + * fb_api_presence_dup: + * @pres: The #FbApiPresence or #NULL. + * + * Duplicates an #FbApiPresence. If @pres is #NULL, a new zero filled + * #FbApiPresence is returned. The returned #FbApiPresence should be + * freed with #fb_api_presence_free() when no longer needed. + * + * Returns: The new #FbApiPresence. + */ +FbApiPresence * +fb_api_presence_dup(const FbApiPresence *pres); + +/** + * fb_api_presence_reset: + * @pres: The #FbApiPresence. + * + * Resets an #FbApiPresence. + */ +void +fb_api_presence_reset(FbApiPresence *pres); + +/** + * fb_api_presence_free: + * @pres: The #FbApiPresence. + * + * Frees all memory used by the #FbApiPresence. + */ +void +fb_api_presence_free(FbApiPresence *pres); + +/** + * fb_api_thread_dup: + * @thrd: The #FbApiThread or #NULL. + * @deep: #TRUE to duplicate allocated data, otherwise #FALSE. + * + * Duplicates an #FbApiThread. If @thrd is #NULL, a new zero filled + * #FbApiThread is returned. The returned #FbApiThread should be freed + * with #fb_api_thread_free() when no longer needed. + * + * Returns: The new #FbApiThread. + */ +FbApiThread * +fb_api_thread_dup(const FbApiThread *thrd, gboolean deep); + +/** + * fb_api_thread_reset: + * @thrd: The #FbApiThread. + * @deep: #TRUE to free allocated data, otherwise #FALSE. + * + * Resets an #FbApiThread. + */ +void +fb_api_thread_reset(FbApiThread *thrd, gboolean deep); + +/** + * fb_api_thread_free: + * @thrd: The #FbApiThread. + * + * Frees all memory used by the #FbApiThread. + */ +void +fb_api_thread_free(FbApiThread *thrd); + +/** + * fb_api_typing_dup: + * @typg: The #FbApiTyping or #NULL. + * + * Duplicates an #FbApiTyping. If @typg is #NULL, a new zero filled + * #FbApiTyping is returned. The returned #FbApiTyping should be freed + * with #fb_api_typing_free() when no longer needed. + * + * Returns: The new #FbApiTyping. + */ +FbApiTyping * +fb_api_typing_dup(const FbApiTyping *typg); + +/** + * fb_api_typing_reset: + * @typg: The #FbApiTyping. + * + * Resets an #FbApiTyping. + */ +void +fb_api_typing_reset(FbApiTyping *typg); + +/** + * fb_api_typing_free: + * @typg: The #FbApiTyping. + * + * Frees all memory used by the #FbApiTyping. + */ +void +fb_api_typing_free(FbApiTyping *typg); + +/** + * fb_api_user_dup: + * @user: The #FbApiUser or #NULL. + * @deep: #TRUE to duplicate allocated data, otherwise #FALSE. + * + * Duplicates an #FbApiUser. If @user is #NULL, a new zero filled + * #FbApiUser is returned. The returned #FbApiUser should be freed with + * #fb_api_user_free() when no longer needed. + * + * Returns: The new #FbApiUser. + */ +FbApiUser * +fb_api_user_dup(const FbApiUser *user, gboolean deep); + +/** + * fb_api_user_reset: + * @user: The #FbApiUser. + * @deep: #TRUE to free allocated data, otherwise #FALSE. + * + * Resets an #FbApiUser. + */ +void +fb_api_user_reset(FbApiUser *user, gboolean deep); + +/** + * fb_api_user_free: + * @user: The #FbApiUser. + * + * Frees all memory used by the #FbApiUser. + */ +void +fb_api_user_free(FbApiUser *user); + +#endif /* _FACEBOOK_API_H_ */ diff --git a/facebook/data.c b/facebook/data.c new file mode 100644 index 00000000..b4ac71b3 --- /dev/null +++ b/facebook/data.c @@ -0,0 +1,608 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#include + +#include "account.h" +#include "purple2compat/glibcompat.h" + +#include "api.h" +#include "data.h" + +struct _FbDataPrivate +{ + FbApi *api; + FbHttpConns *cons; + PurpleConnection *gc; + PurpleRoomlist *roomlist; + GQueue *msgs; + GHashTable *imgs; + GHashTable *unread; + GHashTable *evs; +}; + +struct _FbDataImagePrivate +{ + FbData *fata; + gchar *url; + FbDataImageFunc func; + gpointer data; + GDestroyNotify dunc; + + gboolean active; + const guint8 *image; + gsize size; +}; + +static const gchar *fb_props_strs[] = { + "cid", + "did", + "stoken", + "token" +}; + +G_DEFINE_TYPE_WITH_CODE(FbData, fb_data, G_TYPE_OBJECT, G_ADD_PRIVATE(FbData)); +G_DEFINE_TYPE_WITH_CODE(FbDataImage, fb_data_image, G_TYPE_OBJECT, G_ADD_PRIVATE(FbDataImage)); + +static void +fb_data_dispose(GObject *obj) +{ + FbDataPrivate *priv = FB_DATA(obj)->priv; + GHashTableIter iter; + gpointer ptr; + + fb_http_conns_cancel_all(priv->cons); + g_hash_table_iter_init(&iter, priv->evs); + + while (g_hash_table_iter_next(&iter, NULL, &ptr)) { + g_source_remove(GPOINTER_TO_UINT(ptr)); + } + + if (G_LIKELY(priv->api != NULL)) { + g_object_unref(priv->api); + } + + fb_http_conns_free(priv->cons); + g_queue_free_full(priv->msgs, (GDestroyNotify) fb_api_message_free); + + g_hash_table_destroy(priv->imgs); + g_hash_table_destroy(priv->unread); + g_hash_table_destroy(priv->evs); +} + +static void +fb_data_class_init(FbDataClass *klass) +{ + GObjectClass *gklass = G_OBJECT_CLASS(klass); + + gklass->dispose = fb_data_dispose; + g_type_class_add_private(klass, sizeof (FbDataPrivate)); +} + +static void +fb_data_init(FbData *fata) +{ + FbDataPrivate *priv; + + priv = G_TYPE_INSTANCE_GET_PRIVATE(fata, FB_TYPE_DATA, FbDataPrivate); + fata->priv = priv; + + priv->cons = fb_http_conns_new(); + priv->msgs = g_queue_new(); + + priv->imgs = g_hash_table_new_full(g_direct_hash, g_direct_equal, + g_object_unref, NULL); + priv->unread = g_hash_table_new_full(fb_id_hash, fb_id_equal, + g_free, NULL); + priv->evs = g_hash_table_new_full(g_str_hash, g_str_equal, + g_free, NULL); +} + +static void +fb_data_image_dispose(GObject *obj) +{ + FbDataImage *img = FB_DATA_IMAGE(obj); + FbDataImagePrivate *priv = img->priv; + FbData *fata = priv->fata; + + if ((priv->dunc != NULL) && (priv->data != NULL)) { + priv->dunc(priv->data); + } + + g_free(priv->url); + g_hash_table_steal(fata->priv->imgs, img); +} + +static void +fb_data_image_class_init(FbDataImageClass *klass) +{ + GObjectClass *gklass = G_OBJECT_CLASS(klass); + + gklass->dispose = fb_data_image_dispose; + g_type_class_add_private(klass, sizeof (FbDataImagePrivate)); +} + +static void +fb_data_image_init(FbDataImage *img) +{ + FbDataImagePrivate *priv; + + priv = G_TYPE_INSTANCE_GET_PRIVATE(img, FB_TYPE_DATA_IMAGE, + FbDataImagePrivate); + img->priv = priv; +} + +FbData * +fb_data_new(PurpleConnection *gc) +{ + FbData *fata; + FbDataPrivate *priv; + + fata = g_object_new(FB_TYPE_DATA, NULL); + priv = fata->priv; + + priv->api = fb_api_new(gc); + priv->gc = gc; + + return fata; +} + +gboolean +fb_data_load(FbData *fata) +{ + const gchar *str; + FbDataPrivate *priv; + FbId id; + gboolean ret = TRUE; + guint i; + guint64 uint; + GValue val = G_VALUE_INIT; + PurpleAccount *acct; + + g_return_val_if_fail(FB_IS_DATA(fata), FALSE); + priv = fata->priv; + acct = purple_connection_get_account(priv->gc); + + for (i = 0; i < G_N_ELEMENTS(fb_props_strs); i++) { + str = purple_account_get_string(acct, fb_props_strs[i], NULL); + + if (str == NULL) { + ret = FALSE; + } + + g_value_init(&val, G_TYPE_STRING); + g_value_set_string(&val, str); + g_object_set_property(G_OBJECT(priv->api), fb_props_strs[i], + &val); + g_value_unset(&val); + } + + str = purple_account_get_string(acct, "mid", NULL); + + if (str != NULL) { + uint = g_ascii_strtoull(str, NULL, 10); + g_value_init(&val, G_TYPE_UINT64); + g_value_set_uint64(&val, uint); + g_object_set_property(G_OBJECT(priv->api), "mid", &val); + g_value_unset(&val); + } else { + ret = FALSE; + } + + str = purple_account_get_string(acct, "uid", NULL); + + if (str != NULL) { + id = FB_ID_FROM_STR(str); + g_value_init(&val, FB_TYPE_ID); + g_value_set_int64(&val, id); + g_object_set_property(G_OBJECT(priv->api), "uid", &val); + g_value_unset(&val); + } else { + ret = FALSE; + } + + fb_api_rehash(priv->api); + return ret; +} + +void +fb_data_save(FbData *fata) +{ + const gchar *str; + FbDataPrivate *priv; + gchar *dup; + guint i; + guint64 uint; + GValue val = G_VALUE_INIT; + PurpleAccount *acct; + + g_return_if_fail(FB_IS_DATA(fata)); + priv = fata->priv; + acct = purple_connection_get_account(priv->gc); + + for (i = 0; i < G_N_ELEMENTS(fb_props_strs); i++) { + g_value_init(&val, G_TYPE_STRING); + g_object_get_property(G_OBJECT(priv->api), fb_props_strs[i], + &val); + str = g_value_get_string(&val); + + if (purple_strequal(fb_props_strs[i], "token") && !purple_account_get_remember_password(acct)) { + str = ""; + } + purple_account_set_string(acct, fb_props_strs[i], str); + g_value_unset(&val); + } + + g_value_init(&val, G_TYPE_UINT64); + g_object_get_property(G_OBJECT(priv->api), "mid", &val); + uint = g_value_get_uint64(&val); + g_value_unset(&val); + + dup = g_strdup_printf("%" G_GINT64_FORMAT, uint); + purple_account_set_string(acct, "mid", dup); + g_free(dup); + + g_value_init(&val, G_TYPE_INT64); + g_object_get_property(G_OBJECT(priv->api), "uid", &val); + uint = g_value_get_int64(&val); + g_value_unset(&val); + + dup = g_strdup_printf("%" FB_ID_FORMAT, uint); + purple_account_set_string(acct, "uid", dup); + g_free(dup); +} + +void +fb_data_add_timeout(FbData *fata, const gchar *name, guint interval, + GSourceFunc func, gpointer data) +{ + FbDataPrivate *priv; + gchar *key; + guint id; + + g_return_if_fail(FB_IS_DATA(fata)); + priv = fata->priv; + + fb_data_clear_timeout(fata, name, TRUE); + + key = g_strdup(name); + id = g_timeout_add(interval, func, data); + g_hash_table_replace(priv->evs, key, GUINT_TO_POINTER(id)); +} + +void +fb_data_clear_timeout(FbData *fata, const gchar *name, gboolean remove) +{ + FbDataPrivate *priv; + gpointer ptr; + guint id; + + g_return_if_fail(FB_IS_DATA(fata)); + priv = fata->priv; + + ptr = g_hash_table_lookup(priv->evs, name); + id = GPOINTER_TO_UINT(ptr); + + if ((id > 0) && remove) { + g_source_remove(id); + } + + g_hash_table_remove(priv->evs, name); +} + +FbApi * +fb_data_get_api(FbData *fata) +{ + FbDataPrivate *priv; + + g_return_val_if_fail(FB_IS_DATA(fata), NULL); + priv = fata->priv; + + return priv->api; +} + +PurpleConnection * +fb_data_get_connection(FbData *fata) +{ + FbDataPrivate *priv; + + g_return_val_if_fail(FB_IS_DATA(fata), NULL); + priv = fata->priv; + + return priv->gc; +} + +PurpleRoomlist * +fb_data_get_roomlist(FbData *fata) +{ + FbDataPrivate *priv; + + g_return_val_if_fail(FB_IS_DATA(fata), NULL); + priv = fata->priv; + + return priv->roomlist; +} + +gboolean +fb_data_get_unread(FbData *fata, FbId id) +{ + FbDataPrivate *priv; + gpointer *ptr; + + g_return_val_if_fail(FB_IS_DATA(fata), FALSE); + g_return_val_if_fail(id != 0, FALSE); + priv = fata->priv; + + ptr = g_hash_table_lookup(priv->unread, &id); + return GPOINTER_TO_INT(ptr); +} + +void +fb_data_set_roomlist(FbData *fata, PurpleRoomlist *list) +{ + FbDataPrivate *priv; + + g_return_if_fail(FB_IS_DATA(fata)); + priv = fata->priv; + + priv->roomlist = list; +} + +void +fb_data_set_unread(FbData *fata, FbId id, gboolean unread) +{ + FbDataPrivate *priv; + gpointer key; + + g_return_if_fail(FB_IS_DATA(fata)); + g_return_if_fail(id != 0); + priv = fata->priv; + + if (!unread) { + g_hash_table_remove(priv->unread, &id); + return; + } + + key = g_memdup(&id, sizeof id); + g_hash_table_replace(priv->unread, key, GINT_TO_POINTER(unread)); +} + +void +fb_data_add_message(FbData *fata, FbApiMessage *msg) +{ + FbDataPrivate *priv; + + g_return_if_fail(FB_IS_DATA(fata)); + priv = fata->priv; + + g_queue_push_tail(priv->msgs, msg); +} + +void +fb_data_remove_message(FbData *fata, FbApiMessage *msg) +{ + FbDataPrivate *priv; + + g_return_if_fail(FB_IS_DATA(fata)); + priv = fata->priv; + + g_queue_remove(priv->msgs, msg); +} + +GSList * +fb_data_take_messages(FbData *fata, FbId uid) +{ + FbApiMessage *msg; + FbDataPrivate *priv; + GList *l; + GList *prev; + GSList *msgs = NULL; + + g_return_val_if_fail(FB_IS_DATA(fata), NULL); + priv = fata->priv; + l = priv->msgs->tail; + + while (l != NULL) { + msg = l->data; + prev = l->prev; + + if (msg->uid == uid) { + msgs = g_slist_prepend(msgs, msg); + g_queue_delete_link(priv->msgs, l); + } + + l = prev; + } + + return msgs; +} + +FbDataImage * +fb_data_image_add(FbData *fata, const gchar *url, FbDataImageFunc func, + gpointer data, GDestroyNotify dunc) +{ + FbDataImage *img; + FbDataImagePrivate *priv; + + g_return_val_if_fail(FB_IS_DATA(fata), NULL); + g_return_val_if_fail(url != NULL, NULL); + g_return_val_if_fail(func != NULL, NULL); + + img = g_object_new(FB_TYPE_DATA_IMAGE, NULL); + priv = img->priv; + + priv->fata = fata; + priv->url = g_strdup(url); + priv->func = func; + priv->data = data; + priv->dunc = dunc; + + g_hash_table_insert(fata->priv->imgs, img, img); + return img; +} + +gboolean +fb_data_image_get_active(FbDataImage *img) +{ + FbDataImagePrivate *priv; + + g_return_val_if_fail(FB_IS_DATA_IMAGE(img), FALSE); + priv = img->priv; + + return priv->active; +} + +gpointer +fb_data_image_get_data(FbDataImage *img) +{ + FbDataImagePrivate *priv; + + g_return_val_if_fail(FB_IS_DATA_IMAGE(img), NULL); + priv = img->priv; + + return priv->data; +} + +FbData * +fb_data_image_get_fata(FbDataImage *img) +{ + FbDataImagePrivate *priv; + + g_return_val_if_fail(FB_IS_DATA_IMAGE(img), NULL); + priv = img->priv; + + return priv->fata; +} + +const guint8 * +fb_data_image_get_image(FbDataImage *img, gsize *size) +{ + FbDataImagePrivate *priv; + + g_return_val_if_fail(FB_IS_DATA_IMAGE(img), NULL); + priv = img->priv; + + if (size != NULL) { + *size = priv->size; + } + + return priv->image; +} + +guint8 * +fb_data_image_dup_image(FbDataImage *img, gsize *size) +{ + FbDataImagePrivate *priv; + + g_return_val_if_fail(FB_IS_DATA_IMAGE(img), NULL); + priv = img->priv; + + if (size != NULL) { + *size = priv->size; + } + + if (priv->size < 1) { + return NULL; + } + + return g_memdup(priv->image, priv->size); +} + +const gchar * +fb_data_image_get_url(FbDataImage *img) +{ + FbDataImagePrivate *priv; + + g_return_val_if_fail(FB_IS_DATA_IMAGE(img), NULL); + priv = img->priv; + + return priv->url; +} + +static void +fb_data_image_cb(PurpleHttpConnection *con, PurpleHttpResponse *res, + gpointer data) +{ + FbDataImage *img = data; + FbDataImagePrivate *priv = img->priv; + FbDataPrivate *driv = priv->fata->priv; + GError *err = NULL; + + if (fb_http_conns_is_canceled(driv->cons)) { + return; + } + + fb_http_conns_remove(driv->cons, con); + fb_http_error_chk(res, &err); + + priv->image = (guint8 *) purple_http_response_get_data(res, &priv->size); + priv->func(img, err); + + if (G_LIKELY(err == NULL)) { + fb_data_image_queue(priv->fata); + } else { + g_error_free(err); + } + + g_object_unref(img); +} + +void +fb_data_image_queue(FbData *fata) +{ + const gchar *url; + FbDataImage *img; + FbDataPrivate *priv; + GHashTableIter iter; + guint active = 0; + PurpleHttpConnection *con; + + g_return_if_fail(FB_IS_DATA(fata)); + priv = fata->priv; + g_hash_table_iter_init(&iter, priv->imgs); + + while (g_hash_table_iter_next(&iter, (gpointer *) &img, NULL)) { + if (fb_data_image_get_active(img)) { + active++; + } + } + + if (active >= FB_DATA_ICON_MAX) { + return; + } + + g_hash_table_iter_init(&iter, priv->imgs); + + while (g_hash_table_iter_next(&iter, (gpointer *) &img, NULL)) { + if (fb_data_image_get_active(img)) { + continue; + } + + img->priv->active = TRUE; + url = fb_data_image_get_url(img); + con = purple_http_get(priv->gc, fb_data_image_cb, img, url); + fb_http_conns_add(priv->cons, con); + + if (++active >= FB_DATA_ICON_MAX) { + break; + } + } +} diff --git a/facebook/data.h b/facebook/data.h new file mode 100644 index 00000000..dbcac479 --- /dev/null +++ b/facebook/data.h @@ -0,0 +1,398 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#ifndef _FACEBOOK_DATA_H_ +#define _FACEBOOK_DATA_H_ + +/** + * SECTION:data + * @section_id: facebook-data + * @short_description: data.h + * @title: Connection Data + * + * The Connection Data. + */ + +#include + +#include "connection.h" +#include "roomlist.h" + +#include "api.h" +#include "http.h" +#include "id.h" + +#define FB_TYPE_DATA (fb_data_get_type()) +#define FB_DATA(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), FB_TYPE_DATA, FbData)) +#define FB_DATA_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), FB_TYPE_DATA, FbDataClass)) +#define FB_IS_DATA(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), FB_TYPE_DATA)) +#define FB_IS_DATA_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), FB_TYPE_DATA)) +#define FB_DATA_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), FB_TYPE_DATA, FbDataClass)) + +#define FB_TYPE_DATA_IMAGE (fb_data_image_get_type()) +#define FB_DATA_IMAGE(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), FB_TYPE_DATA_IMAGE, FbDataImage)) +#define FB_DATA_IMAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), FB_TYPE_DATA_IMAGE, FbDataImageClass)) +#define FB_IS_DATA_IMAGE(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), FB_TYPE_DATA_IMAGE)) +#define FB_IS_DATA_IMAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), FB_TYPE_DATA_IMAGE)) +#define FB_DATA_IMAGE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), FB_TYPE_DATA_IMAGE, FbDataImageClass)) + +/** + * FB_DATA_ICON_MAX: + * + * The maximum of number of concurrent icon fetches. + */ +#define FB_DATA_ICON_MAX 4 + +typedef struct _FbData FbData; +typedef struct _FbDataClass FbDataClass; +typedef struct _FbDataPrivate FbDataPrivate; +typedef struct _FbDataImage FbDataImage; +typedef struct _FbDataImageClass FbDataImageClass; +typedef struct _FbDataImagePrivate FbDataImagePrivate; + +/** + * FbDataImageFunc: + * @img: The #FbDataImage. + * @error: The #GError or #NULL. + * + * The callback for a fetched #FbDataImage. + */ +typedef void (*FbDataImageFunc) (FbDataImage *img, GError *error); + +/** + * FbData: + * + * Represents the connection data used by #FacebookProtocol. + */ +struct _FbData +{ + /*< private >*/ + GObject parent; + FbDataPrivate *priv; +}; + +/** + * FbDataClass: + * + * The base class for all #FbData's. + */ +struct _FbDataClass +{ + /*< private >*/ + GObjectClass parent_class; +}; + +/** + * FbDataImage: + * + * Represents the data used for fetching images. + */ +struct _FbDataImage +{ + /*< private >*/ + GObject parent; + FbDataImagePrivate *priv; +}; + +/** + * FbDataImageClass: + * + * The base class for all #FbDataImage's. + */ +struct _FbDataImageClass +{ + /*< private >*/ + GObjectClass parent_class; +}; + +/** + * fb_data_get_type: + * + * Returns: The #GType for an #FbData. + */ +GType +fb_data_get_type(void); + +/** + * fb_data_image_get_type: + * + * Returns: The #GType for an #FbDataImage. + */ +GType +fb_data_image_get_type(void); + +/** + * fb_data_new: + * @gc: The #PurpleConnection. + * + * Creates a new #FbData. The returned #FbData should be freed with + * #g_object_unref() when no longer needed. + * + * Returns: The new #FbData. + */ +FbData * +fb_data_new(PurpleConnection *gc); + +/** + * fb_data_load: + * @fata: The #FbData. + * + * Loads the internal data from the underlying #PurpleAccount. + * + * Return: #TRUE if all of the data was loaded, otherwise #FALSE. + */ +gboolean +fb_data_load(FbData *fata); + +/** + * fb_data_save: + * @fata: The #FbData. + * + * Saves the internal data to the underlying #PurpleAccount. + */ +void +fb_data_save(FbData *fata); + +/** + * fb_data_add_timeout: + * @fata: The #FbData. + * @name: The name of the timeout. + * @interval: The time, in milliseconds, between calls to @func. + * @func: The #GSourceFunc. + * @data: The data passed to @func. + * + * Adds a new callback timer. The callback is called repeatedly on the + * basis of @interval, until @func returns #FALSE. The timeout should + * be cleared with #fb_data_clear_timeout() when no longer needed. + */ +void +fb_data_add_timeout(FbData *fata, const gchar *name, guint interval, + GSourceFunc func, gpointer data); + +/** + * fb_data_clear_timeout: + * @fata: The #FbData. + * @name: The name of the timeout. + * @remove: #TRUE to remove from the event loop, otherwise #FALSE. + * + * Clears and removes a callback timer. The only time @remove should be + * #FALSE, is when being called from a #GSourceFunc, which is returning + * #FALSE. + */ +void +fb_data_clear_timeout(FbData *fata, const gchar *name, gboolean remove); + +/** + * fb_data_get_api: + * @fata: The #FbData. + * + * Gets the #FbApi from the #FbData. + * + * Return: The #FbApi. + */ +FbApi * +fb_data_get_api(FbData *fata); + +/** + * fb_data_get_connection: + * @fata: The #FbData. + * + * Gets the #PurpleConnection from the #FbData. + * + * Return: The #PurpleConnection. + */ +PurpleConnection * +fb_data_get_connection(FbData *fata); + +/** + * fb_data_get_roomlist: + * @fata: The #FbData. + * + * Gets the #PurpleRoomlist from the #FbData. + * + * Return: The #PurpleRoomlist. + */ +PurpleRoomlist * +fb_data_get_roomlist(FbData *fata); + +/** + * fb_data_get_unread: + * @fata: The #FbData. + * @id: The #FbId. + * + * Gets the unread state of an #FbId. + * + * Return: #TRUE if the #FbId is unread, otherwise #FALSE. + */ +gboolean +fb_data_get_unread(FbData *fata, FbId id); + +/** + * fb_data_set_roomlist: + * @fata: The #FbData. + * @list: The #PurpleRoomlist. + * + * Sets the #PurpleRoomlist to the #FbData. + */ +void +fb_data_set_roomlist(FbData *fata, PurpleRoomlist *list); + +/** + * fb_data_set_unread: + * @fata: The #FbData. + * @id: The #FbId. + * @unread: #TRUE if the #FbId is unread, otherwise #FALSE. + * + * Sets the unread state of an #FbId to the #FbData. + */ +void +fb_data_set_unread(FbData *fata, FbId id, gboolean unread); + +/** + * fb_data_add_message: + * @fata: The #FbData. + * @msg: The #FbApiMessage. + * + * Adds an #FbApiMessage to the #FbData. + */ +void +fb_data_add_message(FbData *fata, FbApiMessage *msg); + +/** + * fb_data_remove_message: + * @fata: The #FbData. + * @msg: The #FbApiMessage. + * + * Removes an #FbApiMessage from the #FbData. + */ +void +fb_data_remove_message(FbData *fata, FbApiMessage *msg); + +/** + * fb_data_take_messages: + * @fata: The #FbData. + * @uid: The user #FbId. + * + * Gets a #GSList of messages by the user #FbId from the #FbData. The + * #FbApiMessage's are removed from the #FbData. The returned #GSList + * and its #FbApiMessage's should be freed with #fb_api_message_free() + * and #g_slist_free_full() when no longer needed. + */ +GSList * +fb_data_take_messages(FbData *fata, FbId uid); + +/** + * fb_data_image_add: + * @fata: The #FbData. + * @url: The image URL. + * @func: The #FbDataImageFunc. + * @data: The user-defined data. + * @dunc: The #GDestroyNotify for @data or #NULL. + * + * Adds a new #FbDataImage to the #FbData. This is used to fetch images + * from HTTP sources. After calling this, #fb_data_image_queue() should + * be called to queue the fetching process. + * + * Return: The #FbDataImage. + */ +FbDataImage * +fb_data_image_add(FbData *fata, const gchar *url, FbDataImageFunc func, + gpointer data, GDestroyNotify dunc); + +/** + * fb_data_image_get_active: + * @img: The #FbDataImage. + * + * Gets the active fetching state from the #FbDataImage. + * + * Returns: #TRUE if the image is being fetched, otherwise #FALSE. + */ +gboolean +fb_data_image_get_active(FbDataImage *img); + +/** + * fb_data_image_get_data: + * @img: The #FbDataImage. + * + * Gets the user-defined data from the #FbDataImage. + * + * Returns: The user-defined data. + */ +gpointer +fb_data_image_get_data(FbDataImage *img); + +/** + * fb_data_image_get_fata: + * @img: The #FbDataImage. + * + * Gets the #FbData from the #FbDataImage. + * + * Returns: The #FbData. + */ +FbData * +fb_data_image_get_fata(FbDataImage *img); + +/** + * fb_data_image_get_image: + * @img: The #FbDataImage. + * @size: The return location for the image size or #NULL. + * + * Gets the image data from the #FbDataImage. + * + * Returns: The image data. + */ +const guint8 * +fb_data_image_get_image(FbDataImage *img, gsize *size); + +/** + * fb_data_image_dup_image: + * @img: The #FbDataImage. + * @size: The return location for the image size or #NULL. + * + * Gets the duplicated image data from the #FbDataImage. The returned + * data should be freed with #g_free() when no longer needed. + * + * Returns: The duplicated image data. + */ +guint8 * +fb_data_image_dup_image(FbDataImage *img, gsize *size); + +/** + * fb_data_image_get_url: + * @img: The #FbDataImage. + * + * Gets the image URL from the #FbDataImage. + * + * Returns: The image URL. + */ +const gchar * +fb_data_image_get_url(FbDataImage *img); + +/** + * fb_data_image_queue: + * @fata: The #FbData. + * + * Queues the next #FbDataImage fetches. + */ +void +fb_data_image_queue(FbData *fata); + +#endif /* _FACEBOOK_DATA_H_ */ diff --git a/facebook/facebook.c b/facebook/facebook.c new file mode 100644 index 00000000..2be98121 --- /dev/null +++ b/facebook/facebook.c @@ -0,0 +1,1685 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#include "internal.h" + +#include "account.h" +#include "accountopt.h" +#include "blistnode.h" +#include "buddy.h" +#include "buddyicon.h" +#include "buddylist.h" +#include "cmds.h" +#include "connection.h" +#include "conversation.h" +#include "conversations.h" +#include "conversationtypes.h" +#include "purple2compat/glibcompat.h" +#include "image.h" +#include "image-store.h" +#include "message.h" +#include "notify.h" +#include "plugins.h" +#include "presence.h" +#include "protocol.h" +#include "protocols.h" +#include "request.h" +#include "roomlist.h" +#include "server.h" +#include "signals.h" +#include "sslconn.h" +#include "status.h" +#include "util.h" +#include "version.h" + +#include "api.h" +#include "data.h" +#include "facebook.h" +#include "http.h" +#include "util.h" + +static GSList *fb_cmds = NULL; +static PurpleProtocol *fb_protocol = NULL; + +static void +fb_cb_api_messages(FbApi *api, GSList *msgs, gpointer data); + +static PurpleGroup * +fb_get_group(gboolean friend) +{ + PurpleBlistNode *n; + PurpleBlistNode *node; + PurpleGroup *grp; + const gchar *title; + + if (friend) { + title = _("Facebook Friends"); + } else { + title = _("Facebook Non-Friends"); + } + + grp = purple_blist_find_group(title); + + if (G_UNLIKELY(grp == NULL)) { + grp = purple_group_new(title); + node = NULL; + + for (n = purple_blist_get_root(); n != NULL; n = n->next) { + node = n; + } + + /* Append to the end of the buddy list */ + purple_blist_add_group(grp, node); + + if (!friend) { + node = PURPLE_BLIST_NODE(grp); + purple_blist_node_set_bool(node, "collapsed", TRUE); + } + } + + return grp; +} + +static void +fb_buddy_add_nonfriend(PurpleAccount *acct, FbApiUser *user) +{ + gchar uid[FB_ID_STRMAX]; + PurpleBuddy *bdy; + PurpleGroup *grp; + + FB_ID_TO_STR(user->uid, uid); + bdy = purple_buddy_new(acct, uid, user->name); + grp = fb_get_group(FALSE); + + purple_buddy_set_server_alias(bdy, user->name); + purple_blist_add_buddy(bdy, NULL, grp, NULL); +} + +static void +fb_cb_api_auth(FbApi *api, gpointer data) +{ + FbData *fata = data; + PurpleConnection *gc; + + gc = fb_data_get_connection(fata); + + purple_connection_update_progress(gc, _("Fetching contacts"), 2, 4); + fb_data_save(fata); + fb_api_contacts(api); +} + +static void +fb_cb_api_connect(FbApi *api, gpointer data) +{ + FbData *fata = data; + PurpleAccount *acct; + PurpleConnection *gc; + + gc = fb_data_get_connection(fata); + acct = purple_connection_get_account(gc); + + fb_data_save(fata); + purple_connection_set_state(gc, PURPLE_CONNECTION_CONNECTED); + + if (purple_account_get_bool(acct, "show-unread", TRUE)) { + fb_api_unread(api); + } +} + +static void +fb_cb_api_contact(FbApi *api, FbApiUser *user, gpointer data) +{ + FbData *fata = data; + gchar uid[FB_ID_STRMAX]; + GSList *msgs; + PurpleAccount *acct; + PurpleConnection *gc; + + gc = fb_data_get_connection(fata); + acct = purple_connection_get_account(gc); + FB_ID_TO_STR(user->uid, uid); + + if (purple_blist_find_buddy(acct, uid) == NULL) { + fb_buddy_add_nonfriend(acct, user); + } + + msgs = fb_data_take_messages(fata, user->uid); + + if (msgs != NULL) { + fb_cb_api_messages(api, msgs, fata); + g_slist_free_full(msgs, (GDestroyNotify) fb_api_message_free); + } +} + +static gboolean +fb_cb_sync_contacts(gpointer data) +{ + FbApi *api; + FbData *fata = data; + + api = fb_data_get_api(fata); + fb_data_clear_timeout(fata, "sync-contacts", FALSE); + fb_api_contacts(api); + return FALSE; +} + +static void +fb_cb_icon(FbDataImage *img, GError *error) +{ + const gchar *csum; + const gchar *name; + const gchar *str; + FbHttpParams *params; + gsize size; + guint8 *image; + PurpleAccount *acct; + PurpleBuddy *bdy; + + bdy = fb_data_image_get_data(img); + acct = purple_buddy_get_account(bdy); + name = purple_buddy_get_name(bdy); + + if (G_UNLIKELY(error != NULL)) { + fb_util_debug_warning("Failed to retrieve icon for %s: %s", + name, error->message); + return; + } + + str = fb_data_image_get_url(img); + params = fb_http_params_new_parse(str, TRUE); + csum = fb_http_params_get_str(params, "oh", NULL); + + image = fb_data_image_dup_image(img, &size); + purple_buddy_icons_set_for_user(acct, name, image, size, csum); + fb_http_params_free(params); +} + +static void +fb_sync_contacts_add_timeout(FbData *fata) +{ + gint sync; + PurpleConnection *gc; + PurpleAccount *acct; + + gc = fb_data_get_connection(fata); + acct = purple_connection_get_account(gc); + + sync = purple_account_get_int(acct, "sync-interval", 5); + + if (sync < 1) { + purple_account_set_int(acct, "sync-interval", 1); + sync = 1; + } + + sync *= 60 * 1000; + fb_data_add_timeout(fata, "sync-contacts", sync, fb_cb_sync_contacts, + fata); +} + +static void +fb_cb_api_contacts(FbApi *api, GSList *users, gboolean complete, gpointer data) +{ + const gchar *alias; + const gchar *csum; + FbApiUser *user; + FbData *fata = data; + FbId muid; + gchar uid[FB_ID_STRMAX]; + GSList *l; + GValue val = G_VALUE_INIT; + PurpleAccount *acct; + PurpleBuddy *bdy; + PurpleConnection *gc; + PurpleConnectionState state; + PurpleGroup *grp; + PurpleGroup *grpn; + PurpleStatus *status; + PurpleStatusPrimitive pstat; + PurpleStatusType *type; + + gc = fb_data_get_connection(fata); + acct = purple_connection_get_account(gc); + grp = fb_get_group(TRUE); + grpn = fb_get_group(FALSE); + alias = purple_account_get_private_alias(acct); + state = purple_connection_get_state(gc); + + g_value_init(&val, FB_TYPE_ID); + g_object_get_property(G_OBJECT(api), "uid", &val); + muid = g_value_get_int64(&val); + g_value_unset(&val); + + for (l = users; l != NULL; l = l->next) { + user = l->data; + FB_ID_TO_STR(user->uid, uid); + + if (G_UNLIKELY(user->uid == muid)) { + if (G_UNLIKELY(alias != NULL)) { + continue; + } + + purple_account_set_private_alias(acct, user->name); + continue; + } + + bdy = purple_blist_find_buddy(acct, uid); + + if ((bdy != NULL) && (purple_buddy_get_group(bdy) == grpn)) { + purple_blist_remove_buddy(bdy); + bdy = NULL; + } + + if (bdy == NULL) { + bdy = purple_buddy_new(acct, uid, NULL); + purple_blist_add_buddy(bdy, NULL, grp, NULL); + } + + purple_buddy_set_server_alias(bdy, user->name); + csum = purple_buddy_icons_get_checksum_for_user(bdy); + + if (!purple_strequal(csum, user->csum)) { + fb_data_image_add(fata, user->icon, fb_cb_icon, + bdy, NULL); + } + } + + fb_data_image_queue(fata); + + if (!complete) { + return; + } + + if (state != PURPLE_CONNECTION_CONNECTED) { + status = purple_account_get_active_status(acct); + type = purple_status_get_status_type(status); + pstat = purple_status_type_get_primitive(type); + + purple_connection_update_progress(gc, _("Connecting"), 3, 4); + fb_api_connect(api, pstat == PURPLE_STATUS_INVISIBLE); + } + + fb_sync_contacts_add_timeout(fata); +} + +static void +fb_cb_api_contacts_delta(FbApi *api, GSList *added, GSList *removed, gpointer data) +{ + FbApiUser *user; + FbData *fata = data; + gchar uid[FB_ID_STRMAX]; + GSList *l; + PurpleAccount *acct; + PurpleBuddy *bdy; + PurpleConnection *gc; + PurpleGroup *grp; + PurpleGroup *grpn; + + gc = fb_data_get_connection(fata); + acct = purple_connection_get_account(gc); + grp = fb_get_group(TRUE); + grpn = fb_get_group(FALSE); + + for (l = added; l != NULL; l = l->next) { + user = l->data; + FB_ID_TO_STR(user->uid, uid); + + bdy = purple_blist_find_buddy(acct, uid); + + if ((bdy != NULL) && (purple_buddy_get_group(bdy) == grpn)) { + purple_blist_remove_buddy(bdy); + } + + bdy = purple_buddy_new(acct, uid, NULL); + purple_blist_add_buddy(bdy, NULL, grp, NULL); + + purple_buddy_set_server_alias(bdy, user->name); + } + + for (l = removed; l != NULL; l = l->next) { + bdy = purple_blist_find_buddy(acct, l->data); + + if (bdy != NULL) { + purple_blist_remove_buddy(bdy); + } + } + + fb_sync_contacts_add_timeout(fata); +} + +static void +fb_cb_api_error(FbApi *api, GError *error, gpointer data) +{ + FbData *fata = data; + PurpleConnection *gc; + PurpleConnectionError errc; + + gc = fb_data_get_connection(fata); + + if (error->domain == FB_MQTT_SSL_ERROR) { + purple_connection_ssl_error(gc, error->code); + return; + } + + if (g_error_matches(error, FB_API_ERROR, FB_API_ERROR_QUEUE)) { + /* Save the reset data */ + fb_data_save(fata); + } + + if ((error->domain == FB_HTTP_ERROR) && + (error->code >= 400) && + (error->code <= 500)) + { + errc = PURPLE_CONNECTION_ERROR_OTHER_ERROR; + } else if (g_error_matches(error, FB_API_ERROR, FB_API_ERROR_AUTH)) { + errc = PURPLE_CONNECTION_ERROR_AUTHENTICATION_FAILED; + } else { + errc = PURPLE_CONNECTION_ERROR_NETWORK_ERROR; + } + + + if (!g_error_matches(error, FB_API_ERROR, FB_API_ERROR_NONFATAL)) { + purple_connection_error(gc, errc, error->message); + } +} + +static void +fb_cb_api_events(FbApi *api, GSList *events, gpointer data) +{ + FbData *fata = data; + FbApiEvent *event; + gchar uid[FB_ID_STRMAX]; + gchar tid[FB_ID_STRMAX]; + GHashTable *fetch; + GHashTableIter iter; + GSList *l; + PurpleAccount *acct; + PurpleChatConversation *chat; + PurpleConnection *gc; + + gc = fb_data_get_connection(fata); + acct = purple_connection_get_account(gc); + fetch = g_hash_table_new(fb_id_hash, fb_id_equal); + + for (l = events; l != NULL; l = l->next) { + event = l->data; + + FB_ID_TO_STR(event->tid, tid); + chat = purple_conversations_find_chat_with_account(tid, acct); + + if (chat == NULL) { + continue; + } + + FB_ID_TO_STR(event->uid, uid); + + switch (event->type) { + case FB_API_EVENT_TYPE_THREAD_TOPIC: + purple_chat_conversation_set_topic(chat, uid, + event->text); + break; + + case FB_API_EVENT_TYPE_THREAD_USER_ADDED: + if (purple_blist_find_buddy(acct, uid) == NULL) { + if (event->text) { + FbApiUser *user = fb_api_user_dup(NULL, FALSE); + user->uid = event->uid; + user->name = g_strdup(event->text); + + fb_buddy_add_nonfriend(acct, user); + + fb_api_user_free(user); + } else { + g_hash_table_insert(fetch, &event->tid, event); + break; + } + } + + purple_chat_conversation_add_user(chat, uid, NULL, 0, + TRUE); + break; + + case FB_API_EVENT_TYPE_THREAD_USER_REMOVED: + purple_chat_conversation_remove_user(chat, uid, event->text); + break; + } + } + + g_hash_table_iter_init(&iter, fetch); + + while (g_hash_table_iter_next(&iter, NULL, (gpointer) &event)) { + fb_api_thread(api, event->tid); + } + + g_hash_table_destroy(fetch); +} + +static void +fb_cb_image(FbDataImage *img, GError *error) +{ + const gchar *url; + FbApi *api; + FbApiMessage *msg; + FbData *fata; + gsize size; + GSList *msgs = NULL; + guint id; + guint8 *image; + PurpleImage *pimg; + + fata = fb_data_image_get_fata(img); + msg = fb_data_image_get_data(img); + + if (G_UNLIKELY(error != NULL)) { + url = fb_data_image_get_url(img); + fb_util_debug_warning("Failed to retrieve image %s: %s", + url, error->message); + return; + } + + api = fb_data_get_api(fata); + image = fb_data_image_dup_image(img, &size); + pimg = purple_image_new_from_data(image, size); + id = purple_image_store_add_weak(pimg); + + g_free(msg->text); + msg->text = g_strdup_printf("", id); + msg->flags |= FB_API_MESSAGE_FLAG_DONE; + + msgs = g_slist_prepend(msgs, msg); + fb_cb_api_messages(api, msgs, fata); + g_slist_free(msgs); +} + +static void +fb_cb_api_messages(FbApi *api, GSList *msgs, gpointer data) +{ + const gchar *text; + FbApiMessage *msg; + FbData *fata = data; + gboolean isself; + gboolean mark; + gboolean open; + gboolean self; + gchar *html; + gchar tid[FB_ID_STRMAX]; + gchar uid[FB_ID_STRMAX]; + gint id; + gint64 tstamp; + GSList *l; + PurpleAccount *acct; + PurpleChatConversation *chat; + PurpleConnection *gc; + PurpleMessageFlags flags; + + gc = fb_data_get_connection(fata); + acct = purple_connection_get_account(gc); + mark = purple_account_get_bool(acct, "mark-read", TRUE); + open = purple_account_get_bool(acct, "group-chat-open", TRUE); + self = purple_account_get_bool(acct, "show-self", TRUE); + + for (l = msgs; l != NULL; l = l->next) { + msg = l->data; + FB_ID_TO_STR(msg->uid, uid); + + if (purple_blist_find_buddy(acct, uid) == NULL) { + msg = fb_api_message_dup(msg, TRUE); + fb_data_add_message(fata, msg); + fb_api_contact(api, msg->uid); + continue; + } + + isself = (msg->flags & FB_API_MESSAGE_FLAG_SELF) != 0; + + if (isself && !self) { + continue; + } + + flags = isself ? PURPLE_MESSAGE_SEND : PURPLE_MESSAGE_RECV; + tstamp = msg->tstamp / 1000; + + if (msg->flags & FB_API_MESSAGE_FLAG_IMAGE) { + if (!(msg->flags & FB_API_MESSAGE_FLAG_DONE)) { + msg = fb_api_message_dup(msg, TRUE); + fb_data_image_add(fata, msg->text, fb_cb_image, + msg, (GDestroyNotify) + fb_api_message_free); + fb_data_image_queue(fata); + continue; + } + + flags |= PURPLE_MESSAGE_IMAGES; + text = msg->text; + html = NULL; + } else { + html = purple_markup_escape_text(msg->text, -1); + text = html; + } + + if (msg->tid == 0) { + if (mark && !isself) { + fb_data_set_unread(fata, msg->uid, TRUE); + } + + fb_util_serv_got_im(gc, uid, text, flags, tstamp); + g_free(html); + continue; + } + + FB_ID_TO_STR(msg->tid, tid); + chat = purple_conversations_find_chat_with_account(tid, acct); + + if (chat == NULL) { + if (!open) { + g_free(html); + continue; + } + + id = fb_id_hash(&msg->tid); + purple_serv_got_joined_chat(gc, id, tid); + fb_api_thread(api, msg->tid); + } else { + id = purple_chat_conversation_get_id(chat); + } + + if (mark && !isself) { + fb_data_set_unread(fata, msg->tid, TRUE); + } + + fb_util_serv_got_chat_in(gc, id, uid, text, flags, tstamp); + g_free(html); + } +} + +static void +fb_cb_api_presences(FbApi *api, GSList *press, gpointer data) +{ + const gchar *statid; + FbApiPresence *pres; + FbData *fata = data; + gchar uid[FB_ID_STRMAX]; + GSList *l; + PurpleAccount *acct; + PurpleConnection *gc; + PurpleStatusPrimitive pstat; + + gc = fb_data_get_connection(fata); + acct = purple_connection_get_account(gc); + + for (l = press; l != NULL; l = l->next) { + pres = l->data; + + if (pres->active) { + pstat = PURPLE_STATUS_AVAILABLE; + } else { + pstat = PURPLE_STATUS_OFFLINE; + } + + FB_ID_TO_STR(pres->uid, uid); + statid = purple_primitive_get_id_from_type(pstat); + purple_protocol_got_user_status(acct, uid, statid, NULL); + } +} + +static void +fb_cb_api_thread(FbApi *api, FbApiThread *thrd, gpointer data) +{ + const gchar *name; + FbApiUser *user; + FbData *fata = data; + gboolean active; + gchar tid[FB_ID_STRMAX]; + gchar uid[FB_ID_STRMAX]; + gint id; + GSList *l; + PurpleAccount *acct; + PurpleChatConversation *chat; + PurpleConnection *gc; + + gc = fb_data_get_connection(fata); + acct = purple_connection_get_account(gc); + id = fb_id_hash(&thrd->tid); + FB_ID_TO_STR(thrd->tid, tid); + + chat = purple_conversations_find_chat_with_account(tid, acct); + + if ((chat == NULL) || purple_chat_conversation_has_left(chat)) { + chat = purple_serv_got_joined_chat(gc, id, tid); + active = FALSE; + } else { + /* If there are no users in the group chat, including + * the local user, then the group chat has yet to be + * setup by this function. As a result, any group chat + * without users is inactive. + */ + active = purple_chat_conversation_get_users_count(chat) > 0; + } + + if (!active) { + name = purple_account_get_username(acct); + purple_chat_conversation_add_user(chat, name, NULL, 0, FALSE); + } + + purple_chat_conversation_set_topic(chat, NULL, thrd->topic); + + for (l = thrd->users; l != NULL; l = l->next) { + user = l->data; + FB_ID_TO_STR(user->uid, uid); + + if (purple_chat_conversation_has_user(chat, uid)) { + continue; + } + + if (purple_blist_find_buddy(acct, uid) == NULL) { + fb_buddy_add_nonfriend(acct, user); + } + + purple_chat_conversation_add_user(chat, uid, NULL, 0, active); + } +} + +static void +fb_cb_api_thread_create(FbApi *api, FbId tid, gpointer data) +{ + FbData *fata = data; + gchar sid[FB_ID_STRMAX]; + GHashTable *table; + PurpleConnection *gc; + + gc = fb_data_get_connection(fata); + FB_ID_TO_STR(tid, sid); + + table = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, g_free); + g_hash_table_insert(table, "name", g_strdup(sid)); + purple_serv_join_chat(gc, table); + g_hash_table_destroy(table); +} + +static void +fb_cb_api_thread_kicked(FbApi *api, FbApiThread *thrd, gpointer data) +{ + FbData *fata = data; + gchar tid[FB_ID_STRMAX]; + PurpleAccount *acct; + PurpleConnection *gc; + PurpleChatConversation *chat; + + FB_ID_TO_STR(thrd->tid, tid); + + gc = fb_data_get_connection(fata); + acct = purple_connection_get_account(gc); + chat = purple_conversations_find_chat_with_account(tid, acct); + + if (chat == NULL) { + PurpleRequestCommonParameters *cpar; + + cpar = purple_request_cpar_from_connection(gc); + purple_notify_error(gc, + _("Join a Chat"), + _("Failed to Join Chat"), + _("You have been removed from this chat"), + cpar); + return; + } + + purple_conversation_write_system_message(PURPLE_CONVERSATION(chat), + _("You have been removed from this chat"), 0); + + purple_serv_got_chat_left(gc, purple_chat_conversation_get_id(chat)); +} + +static void +fb_cb_api_threads(FbApi *api, GSList *thrds, gpointer data) +{ + const gchar *alias; + FbApiUser *user; + FbData *fata = data; + gchar tid[FB_ID_STRMAX]; + gchar uid[FB_ID_STRMAX]; + GSList *l; + GSList *m; + GString *gstr; + FbApiThread *thrd; + PurpleAccount *acct; + PurpleBuddy *bdy; + PurpleConnection *gc; + PurpleRoomlist *list; + PurpleRoomlistRoom *room; + + list = fb_data_get_roomlist(fata); + + if (G_UNLIKELY(list == NULL)) { + return; + } + + gc = fb_data_get_connection(fata); + acct = purple_connection_get_account(gc); + gstr = g_string_new(NULL); + + for (l = thrds; l != NULL; l = l->next) { + thrd = l->data; + FB_ID_TO_STR(thrd->tid, tid); + g_string_truncate(gstr, 0); + + for (m = thrd->users; m != NULL; m = m->next) { + user = m->data; + FB_ID_TO_STR(user->uid, uid); + bdy = purple_blist_find_buddy(acct, uid); + + if (bdy != NULL) { + alias = purple_buddy_get_alias(bdy); + } else { + alias = user->name; + } + + if (gstr->len > 0) { + g_string_append(gstr, ", "); + } + + g_string_append(gstr, alias); + } + + room = purple_roomlist_room_new(PURPLE_ROOMLIST_ROOMTYPE_ROOM, + tid, NULL); + purple_roomlist_room_add_field(list, room, thrd->topic); + purple_roomlist_room_add_field(list, room, gstr->str); + purple_roomlist_room_add(list, room); + } + + purple_roomlist_set_in_progress(list, FALSE); + fb_data_set_roomlist(fata, NULL); + g_string_free(gstr, TRUE); +} + +static void +fb_cb_api_typing(FbApi *api, FbApiTyping *typg, gpointer data) +{ + FbData *fata = data; + gchar uid[FB_ID_STRMAX]; + PurpleConnection *gc; + + gc = fb_data_get_connection(fata); + FB_ID_TO_STR(typg->uid, uid); + + if (typg->state) { + purple_serv_got_typing(gc, uid, 0, PURPLE_IM_TYPING); + } else { + purple_serv_got_typing_stopped(gc, uid); + } +} + +static void +fb_mark_read(FbData *fata, FbId id, gboolean thread) +{ + FbApi *api; + PurpleAccount *acct; + PurpleConnection *gc; + + gc = fb_data_get_connection(fata); + acct = purple_connection_get_account(gc); + api = fb_data_get_api(fata); + + if (!fb_data_get_unread(fata, id) || + (purple_account_get_bool(acct, "mark-read-available", FALSE) && + fb_api_is_invisible(api))) + { + return; + } + + fb_data_set_unread(fata, id, FALSE); + fb_api_read(api, id, thread); +} + +static gboolean +fb_cb_conv_read(gpointer data) +{ + const gchar *name; + FbData *fata; + FbId id; + gchar *tname; + PurpleConnection *gc; + PurpleConversation *conv = data; + + gc = purple_conversation_get_connection(conv); + fata = purple_connection_get_protocol_data(gc); + name = purple_conversation_get_name(conv); + id = FB_ID_FROM_STR(name); + + tname = g_strconcat("conv-read-", name, NULL); + fb_data_clear_timeout(fata, tname, FALSE); + g_free(tname); + + if (purple_conversation_has_focus(conv)) { + fb_mark_read(fata, id, PURPLE_IS_CHAT_CONVERSATION(conv)); + } + return FALSE; +} + +static void +fb_cb_conv_updated(PurpleConversation *conv, PurpleConversationUpdateType type, + gpointer data) +{ + const gchar *name; + const gchar *pid; + FbData *fata = data; + gchar *tname; + PurpleAccount *acct; + + acct = purple_conversation_get_account(conv); + pid = purple_account_get_protocol_id(acct); + + if ((type == PURPLE_CONVERSATION_UPDATE_UNSEEN) && + purple_strequal(pid, FB_PROTOCOL_ID) && + purple_account_get_bool(acct, "mark-read", TRUE)) + { + /* Use event loop for purple_conversation_has_focus() */ + name = purple_conversation_get_name(conv); + tname = g_strconcat("conv-read-", name, NULL); + fb_data_add_timeout(fata, tname, 1, fb_cb_conv_read, conv); + g_free(tname); + } +} + +static void +fb_cb_conv_deleting(PurpleConversation *conv, gpointer data) +{ + const gchar *name; + const gchar *pid; + FbData *fata = data; + gchar *tname; + PurpleAccount *acct; + + acct = purple_conversation_get_account(conv); + pid = purple_account_get_protocol_id(acct); + + if (!purple_strequal(pid, FB_PROTOCOL_ID)) { + return; + } + + name = purple_conversation_get_name(conv); + tname = g_strconcat("conv-read-", name, NULL); + fb_data_clear_timeout(fata, tname, TRUE); + g_free(tname); +} + +static void +fb_blist_chat_create(GSList *buddies, gpointer data) +{ + const gchar *name; + FbApi *api; + FbData *fata = data; + FbId *did; + FbId uid; + GSList *l; + GSList *uids = NULL; + PurpleConnection *gc; + PurpleRequestCommonParameters *cpar; + + gc = fb_data_get_connection(fata); + api = fb_data_get_api(fata); + + if (g_slist_length(buddies) < 2) { + cpar = purple_request_cpar_from_connection(gc); + purple_notify_error(gc, + _("Initiate Chat"), + _("Failed to Initiate Chat"), + _("At least two initial chat participants" + " are required."), + cpar); + return; + } + + for (l = buddies; l != NULL; l = l->next) { + name = purple_buddy_get_name(l->data); + uid = FB_ID_FROM_STR(name); + did = g_memdup(&uid, sizeof uid); + uids = g_slist_prepend(uids, did); + } + + fb_api_thread_create(api, uids); + g_slist_free_full(uids, g_free); +} + +static void +fb_blist_chat_init(PurpleBlistNode *node, gpointer data) +{ + FbData *fata = data; + GSList *select = NULL; + PurpleConnection *gc; + + if (!PURPLE_BLIST_NODE_IS_BUDDY(node)) { + return; + } + + gc = fb_data_get_connection(fata); + select = g_slist_prepend(select, PURPLE_BUDDY(node)); + + fb_util_request_buddy(gc, + _("Initiate Chat"), + _("Initial Chat Participants"), + _("Select at least two initial participants."), + select, TRUE, + G_CALLBACK(fb_blist_chat_create), NULL, + fata); + g_slist_free(select); +} + +static void +fb_login(PurpleAccount *acct) +{ + const gchar *pass; + const gchar *user; + FbApi *api; + FbData *fata; + gpointer convh; + PurpleConnection *gc; + + gc = purple_account_get_connection(acct); + + fata = fb_data_new(gc); + api = fb_data_get_api(fata); + convh = purple_conversations_get_handle(); + purple_connection_set_protocol_data(gc, fata); + + g_signal_connect(api, + "auth", + G_CALLBACK(fb_cb_api_auth), + fata); + g_signal_connect(api, + "connect", + G_CALLBACK(fb_cb_api_connect), + fata); + g_signal_connect(api, + "contact", + G_CALLBACK(fb_cb_api_contact), + fata); + g_signal_connect(api, + "contacts", + G_CALLBACK(fb_cb_api_contacts), + fata); + g_signal_connect(api, + "contacts-delta", + G_CALLBACK(fb_cb_api_contacts_delta), + fata); + g_signal_connect(api, + "error", + G_CALLBACK(fb_cb_api_error), + fata); + g_signal_connect(api, + "events", + G_CALLBACK(fb_cb_api_events), + fata); + g_signal_connect(api, + "messages", + G_CALLBACK(fb_cb_api_messages), + fata); + g_signal_connect(api, + "presences", + G_CALLBACK(fb_cb_api_presences), + fata); + g_signal_connect(api, + "thread", + G_CALLBACK(fb_cb_api_thread), + fata); + g_signal_connect(api, + "thread-create", + G_CALLBACK(fb_cb_api_thread_create), + fata); + g_signal_connect(api, + "thread-kicked", + G_CALLBACK(fb_cb_api_thread_kicked), + fata); + g_signal_connect(api, + "threads", + G_CALLBACK(fb_cb_api_threads), + fata); + g_signal_connect(api, + "typing", + G_CALLBACK(fb_cb_api_typing), + fata); + + purple_signal_connect(convh, + "conversation-updated", + gc, + G_CALLBACK(fb_cb_conv_updated), + fata); + purple_signal_connect(convh, + "deleting-conversation", + gc, + G_CALLBACK(fb_cb_conv_deleting), + fata); + + if (!fb_data_load(fata) || !purple_account_get_remember_password(acct)) { + user = purple_account_get_username(acct); + pass = purple_connection_get_password(gc); + purple_connection_update_progress(gc, _("Authenticating"), + 1, 4); + fb_api_auth(api, user, pass); + return; + } + + purple_connection_update_progress(gc, _("Fetching contacts"), 2, 4); + fb_api_contacts(api); +} + +static void +fb_close(PurpleConnection *gc) +{ + FbApi *api; + FbData *fata; + + fata = purple_connection_get_protocol_data(gc); + api = fb_data_get_api(fata); + + fb_data_save(fata); + fb_api_disconnect(api); + g_object_unref(fata); + + purple_connection_set_protocol_data(gc, NULL); + purple_signals_disconnect_by_handle(gc); +} + +static GList * +fb_status_types(PurpleAccount *acct) +{ + PurpleStatusType *type; + GList *types = NULL; + + type = purple_status_type_new(PURPLE_STATUS_AVAILABLE, + NULL, NULL, TRUE); + types = g_list_prepend(types, type); + + /* Just a NULL state (as of now) for compatibility */ + type = purple_status_type_new(PURPLE_STATUS_AWAY, + NULL, NULL, TRUE); + types = g_list_prepend(types, type); + + type = purple_status_type_new(PURPLE_STATUS_INVISIBLE, + NULL, NULL, TRUE); + types = g_list_prepend(types, type); + + type = purple_status_type_new(PURPLE_STATUS_OFFLINE, + NULL, NULL, TRUE); + types = g_list_prepend(types, type); + + return g_list_reverse(types); +} + +static const char * +fb_list_icon(PurpleAccount *account, PurpleBuddy *buddy) +{ + return "facebook"; +} + +static void +fb_client_tooltip_text(PurpleBuddy *buddy, PurpleNotifyUserInfo *info, + gboolean full) +{ + const gchar *name; + PurplePresence *pres; + PurpleStatus *status; + + pres = purple_buddy_get_presence(buddy); + status = purple_presence_get_active_status(pres); + + if (!PURPLE_BUDDY_IS_ONLINE(buddy)) { + /* Prevent doubles statues for Offline buddies */ + /* See: pidgin_get_tooltip_text() in gtkblist.c */ + purple_notify_user_info_remove_last_item(info); + } + + name = purple_status_get_name(status); + purple_notify_user_info_add_pair_plaintext(info, _("Status"), name); +} + +static GList * +fb_client_blist_node_menu(PurpleBlistNode *node) +{ + FbData *fata; + GList *acts = NULL; + PurpleAccount *acct; + PurpleConnection *gc; + PurpleMenuAction *act; + + if (!PURPLE_BLIST_NODE_IS_BUDDY(node)) { + return NULL; + } + + acct = purple_buddy_get_account(PURPLE_BUDDY(node)); + gc = purple_account_get_connection(acct); + fata = purple_connection_get_protocol_data(gc); + + act = purple_menu_action_new(_("Initiate _Chat"), + PURPLE_CALLBACK(fb_blist_chat_init), + fata, NULL); + acts = g_list_prepend(acts, act); + + return g_list_reverse(acts); +} + +static gboolean +fb_client_offline_message(const PurpleBuddy *buddy) +{ + return TRUE; +} + +static void +fb_server_set_status(PurpleAccount *acct, PurpleStatus *status) +{ + FbApi *api; + FbData *fata; + gboolean invis; + PurpleConnection *gc; + PurpleStatusPrimitive pstat; + PurpleStatusType *type; + + gc = purple_account_get_connection(acct); + fata = purple_connection_get_protocol_data(gc); + api = fb_data_get_api(fata); + + type = purple_status_get_status_type(status); + pstat = purple_status_type_get_primitive(type); + invis = fb_api_is_invisible(api); + + if ((pstat == PURPLE_STATUS_INVISIBLE) && !invis) { + fb_api_connect(api, TRUE); + } else if ((pstat != PURPLE_STATUS_OFFLINE) && invis) { + fb_api_connect(api, FALSE); + } +} + +static gint +fb_im_send(PurpleConnection *gc, const gchar *who, const gchar *tmsg, + PurpleMessageFlags flags) +{ + const gchar *name; + const gchar *text; + FbApi *api; + FbData *fata; + FbId uid; + gchar *sext; + + PurpleMessage *msg = purple_message_new_outgoing(who, tmsg, flags); + + fata = purple_connection_get_protocol_data(gc); + api = fb_data_get_api(fata); + + name = purple_message_get_recipient(msg); + uid = FB_ID_FROM_STR(name); + + text = purple_message_get_contents(msg); + sext = purple_markup_strip_html(text); + fb_api_message(api, uid, FALSE, sext); + g_free(sext); + return 1; +} + +static guint +fb_im_send_typing(PurpleConnection *gc, const gchar *name, + PurpleIMTypingState state) +{ + FbApi *api; + FbData *fata; + FbId uid; + + fata = purple_connection_get_protocol_data(gc); + api = fb_data_get_api(fata); + uid = FB_ID_FROM_STR(name); + + fb_api_typing(api, uid, state != PURPLE_IM_NOT_TYPING); + return 0; +} + +static GList * +fb_chat_info() +{ + GList *pces = NULL; + PurpleProtocolChatEntry *pce; + + pce = g_new0(PurpleProtocolChatEntry, 1); + pce->label = _("Chat _Name:"); + pce->identifier = "name"; + pce->required = TRUE; + pces = g_list_prepend(pces, pce); + + return g_list_reverse(pces); +} + +static GHashTable * +fb_chat_info_defaults(PurpleConnection *gc, const gchar *name) +{ + GHashTable *data; + + data = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, g_free); + g_hash_table_insert(data, "name", g_strdup(name)); + + return data; +} + +static void +fb_chat_join(PurpleConnection *gc, GHashTable *data) +{ + const gchar *name; + FbApi *api; + FbData *fata; + FbId tid; + gint id; + PurpleChatConversation *chat; + PurpleRequestCommonParameters *cpar; + + name = g_hash_table_lookup(data, "name"); + g_return_if_fail(name != NULL); + + if (!FB_ID_IS_STR(name)) { + cpar = purple_request_cpar_from_connection(gc); + purple_notify_error(gc, + _("Join a Chat"), + _("Failed to Join Chat"), + _("Invalid Facebook identifier."), + cpar); + return; + } + + tid = FB_ID_FROM_STR(name); + id = fb_id_hash(&tid); + chat = purple_conversations_find_chat(gc, id); + + if ((chat != NULL) && !purple_chat_conversation_has_left(chat)) { + purple_conversation_present(PURPLE_CONVERSATION(chat)); + return; + } + + fata = purple_connection_get_protocol_data(gc); + api = fb_data_get_api(fata); + fb_api_thread(api, tid); +} + +static gchar * +fb_chat_get_name(GHashTable *data) +{ + const gchar *name; + + name = g_hash_table_lookup(data, "name"); + g_return_val_if_fail(name != NULL, NULL); + + return g_strdup(name); +} + +static void +fb_chat_invite(PurpleConnection *gc, gint id, const gchar *msg, + const gchar *who) +{ + const gchar *name; + FbApi *api; + FbData *fata; + FbId tid; + FbId uid; + PurpleChatConversation *chat; + PurpleRequestCommonParameters *cpar; + + if (!FB_ID_IS_STR(who)) { + cpar = purple_request_cpar_from_connection(gc); + purple_notify_error(gc, + _("Invite Buddy Into Chat Room"), + _("Failed to Invite User"), + _("Invalid Facebook identifier."), + cpar); + return; + } + + fata = purple_connection_get_protocol_data(gc); + api = fb_data_get_api(fata); + chat = purple_conversations_find_chat(gc, id); + + name = purple_conversation_get_name(PURPLE_CONVERSATION(chat)); + tid = FB_ID_FROM_STR(name); + uid = FB_ID_FROM_STR(who); + + fb_api_thread_invite(api, tid, uid); +} + +static gint +fb_chat_send(PurpleConnection *gc, gint id, const gchar *tmsg, + PurpleMessageFlags flags) +{ + const gchar *name; + const gchar *text; + FbApi *api; + FbData *fata; + FbId tid; + gchar *sext; + PurpleAccount *acct; + PurpleChatConversation *chat; + + PurpleMessage *msg = purple_message_new_outgoing(NULL, tmsg, flags); + + acct = purple_connection_get_account(gc); + fata = purple_connection_get_protocol_data(gc); + api = fb_data_get_api(fata); + chat = purple_conversations_find_chat(gc, id); + + name = purple_conversation_get_name(PURPLE_CONVERSATION(chat)); + tid = FB_ID_FROM_STR(name); + + text = purple_message_get_contents(msg); + sext = purple_markup_strip_html(text); + fb_api_message(api, tid, TRUE, sext); + g_free(sext); + + name = purple_account_get_username(acct); + purple_serv_got_chat_in(gc, id, name, + purple_message_get_flags(msg), + purple_message_get_contents(msg), + time(NULL)); + return 0; +} + +static void +fb_chat_set_topic(PurpleConnection *gc, gint id, const gchar *topic) +{ + const gchar *name; + FbApi *api; + FbData *fata; + FbId tid; + PurpleChatConversation *chat; + + fata = purple_connection_get_protocol_data(gc); + api = fb_data_get_api(fata); + chat = purple_conversations_find_chat(gc, id); + + name = purple_conversation_get_name(PURPLE_CONVERSATION(chat)); + tid = FB_ID_FROM_STR(name); + fb_api_thread_topic(api, tid, topic); +} + +static PurpleRoomlist * +fb_roomlist_get_list(PurpleConnection *gc) +{ + FbApi *api; + FbData *fata; + GList *flds = NULL; + PurpleAccount *acct; + PurpleRoomlist *list; + PurpleRoomlistField *fld; + + fata = purple_connection_get_protocol_data(gc); + list = fb_data_get_roomlist(fata); + g_return_val_if_fail(list == NULL, NULL); + + api = fb_data_get_api(fata); + acct = purple_connection_get_account(gc); + list = purple_roomlist_new(acct); + fb_data_set_roomlist(fata, list); + + fld = purple_roomlist_field_new(PURPLE_ROOMLIST_FIELD_STRING, + _("Topic"), "topic", FALSE); + flds = g_list_prepend(flds, fld); + + fld = purple_roomlist_field_new(PURPLE_ROOMLIST_FIELD_STRING, + _("Users"), "users", FALSE); + flds = g_list_prepend(flds, fld); + + flds = g_list_reverse(flds); + purple_roomlist_set_fields(list, flds); + + purple_roomlist_set_in_progress(list, TRUE); + fb_api_threads(api); + return list; +} + +static void +fb_roomlist_cancel(PurpleRoomlist *list) +{ + FbData *fata; + PurpleAccount *acct; + PurpleConnection *gc; + PurpleRoomlist *cist; + + acct = purple_roomlist_get_account(list); + gc = purple_account_get_connection(acct); + fata = purple_connection_get_protocol_data(gc); + cist = fb_data_get_roomlist(fata); + + if (G_LIKELY(cist == list)) { + fb_data_set_roomlist(fata, NULL); + } + + purple_roomlist_set_in_progress(list, FALSE); + g_object_unref(list); +} + +static PurpleCmdRet +fb_cmd_kick(PurpleConversation *conv, const gchar *cmd, gchar **args, + gchar **error, gpointer data) +{ + const gchar *name; + FbApi *api; + FbData *fata; + FbId tid; + FbId uid; + GError *err = NULL; + PurpleAccount *acct; + PurpleBuddy *bdy; + PurpleConnection *gc; + PurpleChatConversation *chat; + + g_return_val_if_fail(PURPLE_IS_CHAT_CONVERSATION(conv), + PURPLE_CMD_RET_FAILED); + + gc = purple_conversation_get_connection(conv); + acct = purple_connection_get_account(gc); + chat = PURPLE_CHAT_CONVERSATION(conv); + bdy = fb_util_account_find_buddy(acct, chat, args[0], &err); + + if (err != NULL) { + *error = g_strdup_printf(_("%s."), err->message); + g_error_free(err); + return PURPLE_CMD_RET_FAILED; + } + + fata = purple_connection_get_protocol_data(gc); + api = fb_data_get_api(fata); + + name = purple_conversation_get_name(conv); + tid = FB_ID_FROM_STR(name); + + name = purple_buddy_get_name(bdy); + uid = FB_ID_FROM_STR(name); + + fb_api_thread_remove(api, tid, uid); + return PURPLE_CMD_RET_OK; +} + +static PurpleCmdRet +fb_cmd_leave(PurpleConversation *conv, const gchar *cmd, gchar **args, + gchar **error, gpointer data) +{ + const gchar *name; + FbApi *api; + FbData *fata; + FbId tid; + gint id; + PurpleConnection *gc; + PurpleChatConversation *chat; + + g_return_val_if_fail(PURPLE_IS_CHAT_CONVERSATION(conv), + PURPLE_CMD_RET_FAILED); + + gc = purple_conversation_get_connection(conv); + fata = purple_connection_get_protocol_data(gc); + api = fb_data_get_api(fata); + + chat = PURPLE_CHAT_CONVERSATION(conv); + id = purple_chat_conversation_get_id(chat); + + name = purple_conversation_get_name(conv); + tid = FB_ID_FROM_STR(name); + + purple_serv_got_chat_left(gc, id); + fb_api_thread_remove(api, tid, 0); + return PURPLE_CMD_RET_OK; +} + +static void +fb_cmds_register(void) +{ + PurpleCmdId id; + + static PurpleCmdFlag cflags = + PURPLE_CMD_FLAG_CHAT | + PURPLE_CMD_FLAG_PROTOCOL_ONLY; + + g_return_if_fail(fb_cmds == NULL); + + id = purple_cmd_register("kick", "s", PURPLE_CMD_P_PROTOCOL, cflags, + "prpl-facebook", fb_cmd_kick, + _("kick: Kick someone from the chat"), + NULL); + fb_cmds = g_slist_prepend(fb_cmds, GUINT_TO_POINTER(id)); + + id = purple_cmd_register("leave", "", PURPLE_CMD_P_PROTOCOL, cflags, + "prpl-facebook", fb_cmd_leave, + _("leave: Leave the chat"), + NULL); + fb_cmds = g_slist_prepend(fb_cmds, GUINT_TO_POINTER(id)); +} + +static void +fb_cmds_unregister_free(gpointer data) +{ + PurpleCmdId id = GPOINTER_TO_UINT(data); + purple_cmd_unregister(id); +} + +static void +fb_cmds_unregister(void) +{ + g_slist_free_full(fb_cmds, fb_cmds_unregister_free); +} + +static gboolean +plugin_load(PurplePlugin *plugin) +{ + fb_cmds_register(); + _purple_socket_init(); + purple_http_init(); + return TRUE; +} + +static gboolean +plugin_unload(PurplePlugin *plugin) +{ + fb_cmds_unregister(); + purple_http_uninit(); + _purple_socket_uninit(); + return TRUE; +} + +G_MODULE_EXPORT gboolean +purple_init_plugin(PurplePlugin *plugin); + +G_MODULE_EXPORT gboolean +purple_init_plugin(PurplePlugin *plugin) +{ + GList *opts = NULL; + PurpleAccountOption *opt; + + static gboolean inited = FALSE; + static PurplePluginInfo info; + static PurplePluginProtocolInfo pinfo; + + (void) fb_protocol; + plugin->info = &info; + + if (G_LIKELY(inited)) { + return purple_plugin_register(plugin); + } + + memset(&info, 0, sizeof info); + memset(&pinfo, 0, sizeof pinfo); + + info.magic = PURPLE_PLUGIN_MAGIC; + info.major_version = PURPLE_MAJOR_VERSION; + info.minor_version = PURPLE_MINOR_VERSION; + info.type = PURPLE_PLUGIN_PROTOCOL; + info.priority = PURPLE_PRIORITY_DEFAULT; + info.id = FB_PROTOCOL_ID; + info.name = "Facebook"; + info.version = PACKAGE_VERSION; + info.summary = N_("Facebook Protocol Plugin"); + info.description = N_("Facebook Protocol Plugin"); + info.homepage = PACKAGE_URL; + info.load = plugin_load; + info.unload = plugin_unload; + info.extra_info = &pinfo; + + pinfo.options = OPT_PROTO_CHAT_TOPIC; + pinfo.list_icon = fb_list_icon; + pinfo.tooltip_text = fb_client_tooltip_text; + pinfo.status_types = fb_status_types; + pinfo.blist_node_menu = fb_client_blist_node_menu; + pinfo.chat_info = fb_chat_info; + pinfo.chat_info_defaults = fb_chat_info_defaults; + pinfo.login = fb_login; + pinfo.close = fb_close; + pinfo.send_im = fb_im_send; + pinfo.send_typing = fb_im_send_typing; + pinfo.set_status = fb_server_set_status; + pinfo.join_chat = fb_chat_join; + pinfo.get_chat_name = fb_chat_get_name; + pinfo.chat_invite = fb_chat_invite; + pinfo.chat_send = fb_chat_send; + pinfo.set_chat_topic = fb_chat_set_topic; + pinfo.roomlist_get_list = fb_roomlist_get_list; + pinfo.roomlist_cancel = fb_roomlist_cancel; + pinfo.offline_message = fb_client_offline_message; + pinfo.struct_size = sizeof pinfo; + + opt = purple_account_option_int_new(_("Buddy list sync interval"), + "sync-interval", 5); + opts = g_list_prepend(opts, opt); + + opt = purple_account_option_bool_new(_("Mark messages as read on focus"), + "mark-read", TRUE); + opts = g_list_prepend(opts, opt); + + opt = purple_account_option_bool_new(_("Mark messages as read only when available"), + "mark-read-available", FALSE); + opts = g_list_prepend(opts, opt); + + opt = purple_account_option_bool_new(_("Show self messages"), + "show-self", TRUE); + opts = g_list_prepend(opts, opt); + + opt = purple_account_option_bool_new(_("Show unread messages"), + "show-unread", TRUE); + opts = g_list_prepend(opts, opt); + + opt = purple_account_option_bool_new(_("Open new group chats with " + "incoming messages"), + "group-chat-open", TRUE); + opts = g_list_prepend(opts, opt); + pinfo.protocol_options = g_list_reverse(opts); + + inited = TRUE; + return purple_plugin_register(plugin); +} diff --git a/facebook/facebook.h b/facebook/facebook.h new file mode 100644 index 00000000..aa8b3c34 --- /dev/null +++ b/facebook/facebook.h @@ -0,0 +1,34 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#ifndef _FACEBOOK_H_ +#define _FACEBOOK_H_ + +#include + +/** + * FB_PROTOCOL_ID: + * + * The Facebook protocol identifier. + */ +#define FB_PROTOCOL_ID "prpl-facebook" + +#endif /* _FACEBOOK_H_ */ diff --git a/facebook/http.c b/facebook/http.c new file mode 100644 index 00000000..cd9876cf --- /dev/null +++ b/facebook/http.c @@ -0,0 +1,438 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#include "internal.h" + +#include + +#include "http.h" + +struct _FbHttpConns +{ + GHashTable *cons; + gboolean canceled; +}; + +GQuark +fb_http_error_quark(void) +{ + static GQuark q = 0; + + if (G_UNLIKELY(q == 0)) { + q = g_quark_from_static_string("fb-http-error-quark"); + } + + return q; +} + +FbHttpConns * +fb_http_conns_new(void) +{ + FbHttpConns *cons; + + cons = g_new0(FbHttpConns, 1); + cons->cons = g_hash_table_new(g_direct_hash, g_direct_equal); + return cons; +} + +void +fb_http_conns_free(FbHttpConns *cons) +{ + g_return_if_fail(cons != NULL); + + g_hash_table_destroy(cons->cons); + g_free(cons); +} + +void +fb_http_conns_cancel_all(FbHttpConns *cons) +{ + GHashTableIter iter; + gpointer con; + + g_return_if_fail(cons != NULL); + g_return_if_fail(!cons->canceled); + + cons->canceled = TRUE; + g_hash_table_iter_init(&iter, cons->cons); + + while (g_hash_table_iter_next(&iter, &con, NULL)) { + g_hash_table_iter_remove(&iter); + purple_http_conn_cancel(con); + } +} + +gboolean +fb_http_conns_is_canceled(FbHttpConns *cons) +{ + g_return_val_if_fail(cons != NULL, TRUE); + return cons->canceled; +} + +void +fb_http_conns_add(FbHttpConns *cons, PurpleHttpConnection *con) +{ + g_return_if_fail(cons != NULL); + g_return_if_fail(!cons->canceled); + g_hash_table_replace(cons->cons, con, con); +} + +void +fb_http_conns_remove(FbHttpConns *cons, PurpleHttpConnection *con) +{ + g_return_if_fail(cons != NULL); + g_return_if_fail(!cons->canceled); + g_hash_table_remove(cons->cons, con); +} + +void +fb_http_conns_reset(FbHttpConns *cons) +{ + g_return_if_fail(cons != NULL); + cons->canceled = FALSE; + g_hash_table_remove_all(cons->cons); +} + +gboolean +fb_http_error_chk(PurpleHttpResponse *res, GError **error) +{ + const gchar *msg; + gint code; + + if (purple_http_response_is_successful(res)) { + return TRUE; + } + + msg = purple_http_response_get_error(res); + code = purple_http_response_get_code(res); + g_set_error(error, FB_HTTP_ERROR, code, "%s", msg); + return FALSE; +} + +FbHttpParams * +fb_http_params_new(void) +{ + return g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); +} + +FbHttpParams * +fb_http_params_new_parse(const gchar *data, gboolean isurl) +{ + const gchar *tail; + gchar *key; + gchar **ps; + gchar *val; + guint i; + FbHttpParams *params; + + params = fb_http_params_new(); + + if (data == NULL) { + return params; + } + + if (isurl) { + data = strchr(data, '?'); + + if (data == NULL) { + return params; + } + + tail = strchr(++data, '#'); + + if (tail != NULL) { + data = g_strndup(data, tail - data); + } else { + data = g_strdup(data); + } + } + + ps = g_strsplit(data, "&", 0); + + for (i = 0; ps[i] != NULL; i++) { + key = ps[i]; + val = strchr(ps[i], '='); + + if (val == NULL) { + continue; + } + + *(val++) = 0; + key = g_uri_unescape_string(key, NULL); + val = g_uri_unescape_string(val, NULL); + g_hash_table_replace(params, key, val); + } + + if (isurl) { + g_free((gchar *) data); + } + + g_strfreev(ps); + return params; +} + +void +fb_http_params_free(FbHttpParams *params) +{ + g_hash_table_destroy(params); +} + +gchar * +fb_http_params_close(FbHttpParams *params, const gchar *url) +{ + GHashTableIter iter; + gpointer key; + gpointer val; + GString *ret; + + g_hash_table_iter_init(&iter, params); + ret = g_string_new(NULL); + + while (g_hash_table_iter_next(&iter, &key, &val)) { + if (val == NULL) { + g_hash_table_iter_remove(&iter); + continue; + } + + if (ret->len > 0) { + g_string_append_c(ret, '&'); + } + + g_string_append_uri_escaped(ret, key, NULL, TRUE); + g_string_append_c(ret, '='); + g_string_append_uri_escaped(ret, val, NULL, TRUE); + } + + if (url != NULL) { + g_string_prepend_c(ret, '?'); + g_string_prepend(ret, url); + } + + fb_http_params_free(params); + return g_string_free(ret, FALSE); +} + +static const gchar * +fb_http_params_get(FbHttpParams *params, const gchar *name, GError **error) +{ + const gchar *ret; + + ret = g_hash_table_lookup(params, name); + + if (ret == NULL) { + g_set_error(error, FB_HTTP_ERROR, FB_HTTP_ERROR_NOMATCH, + _("No matches for %s"), name); + return NULL; + } + + return ret; +} + +gboolean +fb_http_params_get_bool(FbHttpParams *params, const gchar *name, + GError **error) +{ + const gchar *val; + + val = fb_http_params_get(params, name, error); + + if (val == NULL) { + return FALSE; + } + + return g_ascii_strcasecmp(val, "TRUE") == 0; +} + +gdouble +fb_http_params_get_dbl(FbHttpParams *params, const gchar *name, + GError **error) +{ + const gchar *val; + + val = fb_http_params_get(params, name, error); + + if (val == NULL) { + return 0.0; + } + + return g_ascii_strtod(val, NULL); +} + +gint64 +fb_http_params_get_int(FbHttpParams *params, const gchar *name, + GError **error) +{ + const gchar *val; + + val = fb_http_params_get(params, name, error); + + if (val == NULL) { + return 0; + } + + return g_ascii_strtoll(val, NULL, 10); +} + +const gchar * +fb_http_params_get_str(FbHttpParams *params, const gchar *name, + GError **error) +{ + return fb_http_params_get(params, name, error); +} + +gchar * +fb_http_params_dup_str(FbHttpParams *params, const gchar *name, + GError **error) +{ + const gchar *str; + + str = fb_http_params_get(params, name, error); + return g_strdup(str); +} + +static void +fb_http_params_set(FbHttpParams *params, const gchar *name, gchar *value) +{ + gchar *key; + + key = g_strdup(name); + g_hash_table_replace(params, key, value); +} + +void +fb_http_params_set_bool(FbHttpParams *params, const gchar *name, + gboolean value) +{ + gchar *val; + + val = g_strdup(value ? "true" : "false"); + fb_http_params_set(params, name, val); +} + +void +fb_http_params_set_dbl(FbHttpParams *params, const gchar *name, gdouble value) +{ + gchar *val; + + val = g_strdup_printf("%f", value); + fb_http_params_set(params, name, val); +} + +void +fb_http_params_set_int(FbHttpParams *params, const gchar *name, gint64 value) +{ + gchar *val; + + val = g_strdup_printf("%" G_GINT64_FORMAT, value); + fb_http_params_set(params, name, val); +} + +void +fb_http_params_set_str(FbHttpParams *params, const gchar *name, + const gchar *value) +{ + gchar *val; + + val = g_strdup(value); + fb_http_params_set(params, name, val); +} + +void +fb_http_params_set_strf(FbHttpParams *params, const gchar *name, + const gchar *format, ...) +{ + gchar *val; + va_list ap; + + va_start(ap, format); + val = g_strdup_vprintf(format, ap); + va_end(ap); + + fb_http_params_set(params, name, val); +} + +gboolean +fb_http_urlcmp(const gchar *url1, const gchar *url2, gboolean protocol) +{ + const gchar *str1; + const gchar *str2; + gboolean ret = TRUE; + gint int1; + gint int2; + guint i; + PurpleHttpURL *purl1; + PurpleHttpURL *purl2; + + static const gchar * (*funcs[]) (const PurpleHttpURL *url) = { + /* Always first so it can be skipped */ + purple_http_url_get_protocol, + + purple_http_url_get_fragment, + purple_http_url_get_host, + purple_http_url_get_password, + purple_http_url_get_path, + purple_http_url_get_username + }; + + if ((url1 == NULL) || (url2 == NULL)) { + return url1 == url2; + } + + if (strstr(url1, url2) != NULL || strstr(url2, url1) != NULL) { + return TRUE; + } + + purl1 = purple_http_url_parse(url1); + + if (purl1 == NULL) { + return g_ascii_strcasecmp(url1, url2) == 0; + } + + purl2 = purple_http_url_parse(url2); + + if (purl2 == NULL) { + purple_http_url_free(purl1); + return g_ascii_strcasecmp(url1, url2) == 0; + } + + for (i = protocol ? 0 : 1; i < G_N_ELEMENTS(funcs); i++) { + str1 = funcs[i](purl1); + str2 = funcs[i](purl2); + + if (!purple_strequal(str1, str2)) { + ret = FALSE; + break; + } + } + + if (ret && protocol) { + int1 = purple_http_url_get_port(purl1); + int2 = purple_http_url_get_port(purl2); + + if (int1 != int2) { + ret = FALSE; + } + } + + purple_http_url_free(purl1); + purple_http_url_free(purl2); + return ret; +} diff --git a/facebook/http.h b/facebook/http.h new file mode 100644 index 00000000..fdaa067c --- /dev/null +++ b/facebook/http.h @@ -0,0 +1,371 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#ifndef _FACEBOOK_HTTP_H_ +#define _FACEBOOK_HTTP_H_ + +/** + * SECTION:http + * @section_id: facebook-http + * @short_description: http.h + * @title: HTTP Utilities + * + * The HTTP utilities. + */ + +#include + +#include "purple2compat/http.h" + +/** + * FB_HTTP_ERROR: + * + * The #GQuark of the domain of HTTP errors. + */ +#define FB_HTTP_ERROR fb_http_error_quark() + +/** + * FbHttpConns: + * + * Represents a set of #PurpleHttpConnection. + */ +typedef struct _FbHttpConns FbHttpConns; + +/** + * FbHttpParams: + * + * Represents a set of key/value HTTP parameters. + */ +typedef GHashTable FbHttpParams; + +/** + * FbHttpError: + * @FB_HTTP_ERROR_SUCCESS: There is no error. + * @FB_HTTP_ERROR_NOMATCH: The name does not match anything. + * + * The error codes for the #FB_HTTP_ERROR domain. + */ +typedef enum +{ + FB_HTTP_ERROR_SUCCESS = 0, + FB_HTTP_ERROR_NOMATCH +} FbHttpError; + +/** + * fb_http_error_quark: + * + * Gets the #GQuark of the domain of HTTP errors. + * + * Returns: The #GQuark of the domain. + */ +GQuark +fb_http_error_quark(void); + +/** + * fb_http_conns_new: + * + * Creates a new #FbHttpConns. The returned #FbHttpConns should be + * freed with #fb_http_conns_free() when no longer needed. + * + * Returns: The new #FbHttpConns. + */ +FbHttpConns * +fb_http_conns_new(void); + +/** + * fb_http_conns_free: + * @cons: The #FbHttpConns. + * + * Frees all memory used by the #FbHttpConns. This will *not* cancel + * the any of the added #PurpleHttpConnection. + */ +void +fb_http_conns_free(FbHttpConns *cons); + +/** + * fb_http_conns_cancel_all: + * @cons: The #FbHttpConns. + * + * Cancels each #PurpleHttpConnection in the #FbHttpConns. + */ +void +fb_http_conns_cancel_all(FbHttpConns *cons); + +/** + * fb_http_conns_is_canceled: + * @cons: The #FbHttpConns. + * + * Determines if the #FbHttpConns has been canceled. + * + * Returns: #TRUE if it has been canceled, otherwise #FALSE. + */ +gboolean +fb_http_conns_is_canceled(FbHttpConns *cons); + +/** + * fb_http_conns_add: + * @cons: The #FbHttpConns. + * @con: The #PurpleHttpConnection. + * + * Adds a #PurpleHttpConnection to the #FbHttpConns. + */ +void +fb_http_conns_add(FbHttpConns *cons, PurpleHttpConnection *con); + +/** + * fb_http_conns_remove: + * @cons: The #FbHttpConns. + * @con: The #PurpleHttpConnection. + * + * Removes a #PurpleHttpConnection from the #FbHttpConns. + */ +void +fb_http_conns_remove(FbHttpConns *cons, PurpleHttpConnection *con); + +/** + * fb_http_conns_reset: + * @cons: The #FbHttpConns. + * + * Resets the #FbHttpConns. This removes each #PurpleHttpConnection + * from the #FbHttpConns *without* canceling it. This allows the the + * #FbHttpConns to be reused. + */ +void +fb_http_conns_reset(FbHttpConns *cons); + +/** + * fb_http_error_chk: + * @res: The #PurpleHttpResponse. + * @error: The return location for the #GError or #NULL. + * + * Checks a #PurpleHttpResponse for success. This optionally assigns an + * appropriate #GError upon failure. + * + * Returns: #TRUE if the request was successful, otherwise #FALSE. + */ +gboolean +fb_http_error_chk(PurpleHttpResponse *res, GError **error); + +/** + * fb_http_params_new: + * + * Creates a new #FbHttpParams. The returned #FbHttpParams should be + * freed with #fb_http_params_free() when no longer needed. Optionally, + * instead of freeing, the returned #FbHttpParams can be closed with + * #fb_http_params_close(). + * + * Returns: The new #FbHttpParams. + */ +FbHttpParams * +fb_http_params_new(void); + +/** + * fb_http_params_new_parse: + * @data: The string containing HTTP parameters. + * @isurl: #TRUE if @data is a URL, otherwise #FALSE. + * + * Creates a new #FbHttpParams. This parses the #FbHttpParams from a + * string, which can be a URL. The returned #FbHttpParams should be + * freed with #fb_http_params_free() when no longer needed. Optionally, + * instead of freeing, the returned #FbHttpParams can be closed with + * #fb_http_params_close(). + * + * Returns: The new #FbHttpParams. + */ +FbHttpParams * +fb_http_params_new_parse(const gchar *data, gboolean isurl); + +/** + * fb_http_params_free: + * @params: The #FbHttpParams. + * + * Frees all memory used by the #FbHttpParams. + */ +void +fb_http_params_free(FbHttpParams *params); + +/** + * fb_http_params_close: + * @params: The #FbHttpParams. + * @url: The URL or #NULL. + * + * Closes the #FbHttpParams by returning a string representing the HTTP + * parameters. If @url is non-#NULL, then the parameters are appended + * to the value of @url. This frees the #FbHttpParams. The returned + * string should be freed with #g_free() when no longer needed. + * + * Returns: The string representation of the HTTP parameters. + */ +gchar * +fb_http_params_close(FbHttpParams *params, const gchar *url); + +/** + * fb_http_params_get_bool: + * @params: The #FbHttpParams. + * @name: The parameter name. + * @error: The return location for the #GError or #NULL. + * + * Gets a boolean value from the #FbHttpParams. This optionally assigns + * an appropriate #GError upon failure. + * + * Return: The boolean value. + */ +gboolean +fb_http_params_get_bool(FbHttpParams *params, const gchar *name, + GError **error); + +/** + * fb_http_params_get_dbl: + * @params: The #FbHttpParams. + * @name: The parameter name. + * @error: The return location for the #GError or #NULL. + * + * Gets a floating point value from the #FbHttpParams. This optionally + * assigns an appropriate #GError upon failure. + * + * Return: The floating point value. + */ +gdouble +fb_http_params_get_dbl(FbHttpParams *params, const gchar *name, + GError **error); + +/** + * fb_http_params_get_int: + * @params: The #FbHttpParams. + * @name: The parameter name. + * @error: The return location for the #GError or #NULL. + * + * Gets an integer value from the #FbHttpParams. This optionally + * assigns an appropriate #GError upon failure. + * + * Return: The integer value. + */ +gint64 +fb_http_params_get_int(FbHttpParams *params, const gchar *name, + GError **error); + +/** + * fb_http_params_get_str: + * @params: The #FbHttpParams. + * @name: The parameter name. + * @error: The return location for the #GError or #NULL. + * + * Gets a string value from the #FbHttpParams. This optionally assigns + * an appropriate #GError upon failure. + * + * Return: The string value. + */ +const gchar * +fb_http_params_get_str(FbHttpParams *params, const gchar *name, + GError **error); + +/** + * fb_http_params_dup_str: + * @params: The #FbHttpParams. + * @name: The parameter name. + * @error: The return location for the #GError or #NULL. + * + * Gets a duplicated string value from the #FbHttpParams. This + * optionally assigns an appropriate #GError upon failure. The returned + * string should be freed with #g_free() when no longer needed. + * + * Return: The duplicated string value. + */ +gchar * +fb_http_params_dup_str(FbHttpParams *params, const gchar *name, + GError **error); + +/** + * fb_http_params_set_bool: + * @params: The #FbHttpParams. + * @name: The parameter name. + * @value: The value. + * + * Sets a boolean value to the #FbHttpParams. + */ +void +fb_http_params_set_bool(FbHttpParams *params, const gchar *name, + gboolean value); + +/** + * fb_http_params_set_dbl: + * @params: The #FbHttpParams. + * @name: The parameter name. + * @value: The value. + * + * Sets a floating point value to the #FbHttpParams. + */ +void +fb_http_params_set_dbl(FbHttpParams *params, const gchar *name, gdouble value); + +/** + * fb_http_params_set_int: + * @params: The #FbHttpParams. + * @name: The parameter name. + * @value: The value. + * + * Sets an integer value to the #FbHttpParams. + */ +void +fb_http_params_set_int(FbHttpParams *params, const gchar *name, gint64 value); + +/** + * fb_http_params_set_str: + * @params: The #FbHttpParams. + * @name: The parameter name. + * @value: The value. + * + * Sets a string value to the #FbHttpParams. + */ +void +fb_http_params_set_str(FbHttpParams *params, const gchar *name, + const gchar *value); + +/** + * fb_http_params_set_strf: + * @params: The #FbHttpParams. + * @name: The parameter name. + * @format: The format string literal. + * @...: The arguments for @format. + * + * Sets a formatted string value to the #FbHttpParams. + */ +void +fb_http_params_set_strf(FbHttpParams *params, const gchar *name, + const gchar *format, ...) + G_GNUC_PRINTF(3, 4); + +/** + * fb_http_urlcmp: + * @url1: The first URL. + * @url2: The second URL. + * @protocol: #TRUE to match the protocols, otherwise #FALSE. + * + * Compares two URLs. This is more reliable than just comparing two URL + * strings, as it avoids casing in some areas, while not in others. It + * can also, optionally, ignore the matching of the URL protocol. + * + * Returns: #TRUE if the URLs match, otherwise #FALSE. + */ +gboolean +fb_http_urlcmp(const gchar *url1, const gchar *url2, gboolean protocol); + +#endif /* _FACEBOOK_HTTP_H_ */ diff --git a/facebook/id.h b/facebook/id.h new file mode 100644 index 00000000..00b4bd7a --- /dev/null +++ b/facebook/id.h @@ -0,0 +1,131 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#ifndef _FACEBOOK_ID_H_ +#define _FACEBOOK_ID_H_ + +/** + * SECTION:id + * @section_id: facebook-id + * @short_description: id.h + * @title: Facebook Identifier + * + * The Facebook identifier utilities. + */ + +#include +#include + +#include "util.h" + +/** + * FB_ID_FORMAT: + * + * The format specifier for printing and scanning an #FbId. + */ +#define FB_ID_FORMAT G_GINT64_FORMAT + +/** + * FB_ID_MODIFIER: + * + * The length modifier for printing an #FbId. + */ +#define FB_ID_MODIFIER G_GINT64_MODIFIER + +/** + * FB_ID_STRMAX: + * + * The maximum length, including a null-terminating character, of the + * string representation of an #FbId. + */ +#define FB_ID_STRMAX 21 + +/** + * FB_TYPE_ID: + * + * The #GType of an #FbId. + */ +#define FB_TYPE_ID G_TYPE_INT64 + +/** + * FB_ID_CONSTANT: + * @v: The value. + * + * Inserts a literal #FbId into source code. + * + * Return: The literal #FbId value. + */ +#define FB_ID_CONSTANT(v) G_GINT64_CONSTANT(v) + +/** + * FB_ID_FROM_STR: + * @s: The string value. + * + * Converts a string to an #FbId. + * + * Return: The converted #FbId value. + */ +#define FB_ID_FROM_STR(s) g_ascii_strtoll(s, NULL, 10) + +/** + * FB_ID_IS_STR: + * @s: The string value. + * + * Determines if a string is an #FbId. + * + * Return: #TRUE if the string is an #FbId, otherwise #FALSE. + */ +#define FB_ID_IS_STR(s) fb_util_strtest(s, G_ASCII_DIGIT) + +/** + * FB_ID_TO_STR: + * @i: The #FbId. + * @s: The string buffer. + * + * Converts an #FbId to a string. The buffer should be at least the + * size of #FB_ID_STRMAX. + * + * Return: The converted string value. + */ +#define FB_ID_TO_STR(i, s) g_sprintf(s, "%" FB_ID_FORMAT, (FbId) i) + +/** + * fb_id_equal: + * + * Compares the values of two #FbId's for equality. See #g_int64_equal. + */ +#define fb_id_equal g_int64_equal + +/** + * fb_id_hash: + * + * Converts a pointer to a #FbId hash value. See #g_int64_hash. + */ +#define fb_id_hash g_int64_hash + +/** + * FbId: + * + * Represents a numeric Facebook identifier. + */ +typedef gint64 FbId; + +#endif /* _FACEBOOK_ID_H_ */ diff --git a/facebook/json.c b/facebook/json.c new file mode 100644 index 00000000..25278c31 --- /dev/null +++ b/facebook/json.c @@ -0,0 +1,677 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#include "internal.h" + +#include +#include + +#include "json.h" +#include "purple2compat/glibcompat.h" +#include "util.h" + +typedef struct _FbJsonValue FbJsonValue; + +struct _FbJsonValue +{ + const gchar *expr; + FbJsonType type; + gboolean required; + GValue value; +}; + +struct _FbJsonValuesPrivate +{ + JsonNode *root; + GQueue *queue; + GList *next; + + gboolean isarray; + JsonArray *array; + guint index; + + GError *error; +}; + +G_DEFINE_TYPE_WITH_CODE(FbJsonValues, fb_json_values, G_TYPE_OBJECT, G_ADD_PRIVATE(FbJsonValues)); + +static void +fb_json_values_dispose(GObject *obj) +{ + FbJsonValue *value; + FbJsonValuesPrivate *priv = FB_JSON_VALUES(obj)->priv; + + while (!g_queue_is_empty(priv->queue)) { + value = g_queue_pop_head(priv->queue); + + if (G_IS_VALUE(&value->value)) { + g_value_unset(&value->value); + } + + g_free(value); + } + + if (priv->array != NULL) { + json_array_unref(priv->array); + } + + if (priv->error != NULL) { + g_error_free(priv->error); + } + + g_queue_free(priv->queue); +} + +static void +fb_json_values_class_init(FbJsonValuesClass *klass) +{ + GObjectClass *gklass = G_OBJECT_CLASS(klass); + + gklass->dispose = fb_json_values_dispose; + g_type_class_add_private(klass, sizeof (FbJsonValuesPrivate)); +} + +static void +fb_json_values_init(FbJsonValues *values) +{ + FbJsonValuesPrivate *priv; + + priv = G_TYPE_INSTANCE_GET_PRIVATE(values, FB_TYPE_JSON_VALUES, + FbJsonValuesPrivate); + values->priv = priv; + + priv->queue = g_queue_new(); +} + +GQuark +fb_json_error_quark(void) +{ + static GQuark q = 0; + + if (G_UNLIKELY(q == 0)) { + q = g_quark_from_static_string("fb-json-error-quark"); + } + + return q; +} + +JsonBuilder * +fb_json_bldr_new(JsonNodeType type) +{ + JsonBuilder *bldr; + + bldr = json_builder_new(); + + switch (type) { + case JSON_NODE_ARRAY: + fb_json_bldr_arr_begin(bldr, NULL); + break; + + case JSON_NODE_OBJECT: + fb_json_bldr_obj_begin(bldr, NULL); + break; + + default: + break; + } + + return bldr; +} + +gchar * +fb_json_bldr_close(JsonBuilder *bldr, JsonNodeType type, gsize *size) +{ + gchar *ret; + JsonGenerator *genr; + JsonNode *root; + + switch (type) { + case JSON_NODE_ARRAY: + fb_json_bldr_arr_end(bldr); + break; + + case JSON_NODE_OBJECT: + fb_json_bldr_obj_end(bldr); + break; + + default: + break; + } + + genr = json_generator_new(); + root = json_builder_get_root(bldr); + + json_generator_set_root(genr, root); + ret = json_generator_to_data(genr, size); + + json_node_free(root); + g_object_unref(genr); + g_object_unref(bldr); + + return ret; +} + +void +fb_json_bldr_arr_begin(JsonBuilder *bldr, const gchar *name) +{ + if (name != NULL) { + json_builder_set_member_name(bldr, name); + } + + json_builder_begin_array(bldr); +} + +void +fb_json_bldr_arr_end(JsonBuilder *bldr) +{ + json_builder_end_array(bldr); +} + +void +fb_json_bldr_obj_begin(JsonBuilder *bldr, const gchar *name) +{ + if (name != NULL) { + json_builder_set_member_name(bldr, name); + } + + json_builder_begin_object(bldr); +} + +void +fb_json_bldr_obj_end(JsonBuilder *bldr) +{ + json_builder_end_object(bldr); +} + +void +fb_json_bldr_add_bool(JsonBuilder *bldr, const gchar *name, gboolean value) +{ + if (name != NULL) { + json_builder_set_member_name(bldr, name); + } + + json_builder_add_boolean_value(bldr, value); +} + +void +fb_json_bldr_add_dbl(JsonBuilder *bldr, const gchar *name, gdouble value) +{ + if (name != NULL) { + json_builder_set_member_name(bldr, name); + } + + json_builder_add_double_value(bldr, value); +} + +void +fb_json_bldr_add_int(JsonBuilder *bldr, const gchar *name, gint64 value) +{ + if (name != NULL) { + json_builder_set_member_name(bldr, name); + } + + json_builder_add_int_value(bldr, value); +} + +void +fb_json_bldr_add_str(JsonBuilder *bldr, const gchar *name, const gchar *value) +{ + if (name != NULL) { + json_builder_set_member_name(bldr, name); + } + + json_builder_add_string_value(bldr, value); +} + +void +fb_json_bldr_add_strf(JsonBuilder *bldr, const gchar *name, + const gchar *format, ...) +{ + gchar *value; + va_list ap; + + va_start(ap, format); + value = g_strdup_vprintf(format, ap); + va_end(ap); + + fb_json_bldr_add_str(bldr, name, value); + g_free(value); +} + +JsonNode * +fb_json_node_new(const gchar *data, gssize size, GError **error) +{ + gchar *slice; + JsonNode *root; + JsonParser *prsr; + + g_return_val_if_fail(data != NULL, NULL); + + if (size < 0) { + size = strlen(data); + } + + /* Ensure data is null terminated for json-glib < 1.0.2 */ + slice = g_strndup(data, size); + prsr = json_parser_new(); + + if (!json_parser_load_from_data(prsr, slice, size, error)) { + g_object_unref(prsr); + g_free(slice); + return NULL; + } + + root = json_parser_get_root(prsr); + root = json_node_copy(root); + + g_object_unref(prsr); + g_free(slice); + return root; +} + +JsonNode * +fb_json_node_get(JsonNode *root, const gchar *expr, GError **error) +{ + GError *err = NULL; + guint size; + JsonArray *rslt; + JsonNode *node; + JsonNode *ret; + + /* Special case for json-glib < 0.99.2 */ + if (purple_strequal(expr, "$")) { + return json_node_copy(root); + } + + node = json_path_query(expr, root, &err); + + if (err != NULL) { + g_propagate_error(error, err); + json_node_free(node); + return NULL; + } + + rslt = json_node_get_array(node); + size = json_array_get_length(rslt); + + if (size < 1) { + g_set_error(error, FB_JSON_ERROR, FB_JSON_ERROR_NOMATCH, + _("No matches for %s"), expr); + json_node_free(node); + return NULL; + } + + if (size > 1) { + g_set_error(error, FB_JSON_ERROR, FB_JSON_ERROR_AMBIGUOUS, + _("Ambiguous matches for %s"), expr); + json_node_free(node); + return NULL; + } + + if (json_array_get_null_element(rslt, 0)) { + g_set_error(error, FB_JSON_ERROR, FB_JSON_ERROR_NULL, + _("Null value for %s"), expr); + json_node_free(node); + return NULL; + } + + ret = json_array_dup_element(rslt, 0); + json_node_free(node); + return ret; +} + +JsonNode * +fb_json_node_get_nth(JsonNode *root, guint n) +{ + GList *vals; + JsonNode *ret; + JsonObject *obj; + + obj = json_node_get_object(root); + vals = json_object_get_values(obj); + ret = g_list_nth_data(vals, n); + + g_list_free(vals); + return ret; +} + +JsonArray * +fb_json_node_get_arr(JsonNode *root, const gchar *expr, GError **error) +{ + JsonArray *ret; + JsonNode *rslt; + + rslt = fb_json_node_get(root, expr, error); + + if (rslt == NULL) { + return NULL; + } + + ret = json_node_dup_array(rslt); + json_node_free(rslt); + return ret; +} + +gboolean +fb_json_node_get_bool(JsonNode *root, const gchar *expr, GError **error) +{ + gboolean ret; + JsonNode *rslt; + + rslt = fb_json_node_get(root, expr, error); + + if (rslt == NULL) { + return FALSE; + } + + ret = json_node_get_boolean(rslt); + json_node_free(rslt); + return ret; +} + +gdouble +fb_json_node_get_dbl(JsonNode *root, const gchar *expr, GError **error) +{ + gdouble ret; + JsonNode *rslt; + + rslt = fb_json_node_get(root, expr, error); + + if (rslt == NULL) { + return 0.0; + } + + ret = json_node_get_double(rslt); + json_node_free(rslt); + return ret; +} + +gint64 +fb_json_node_get_int(JsonNode *root, const gchar *expr, GError **error) +{ + gint64 ret; + JsonNode *rslt; + + rslt = fb_json_node_get(root, expr, error); + + if (rslt == NULL) { + return 0; + } + + ret = json_node_get_int(rslt); + json_node_free(rslt); + return ret; +} + +gchar * +fb_json_node_get_str(JsonNode *root, const gchar *expr, GError **error) +{ + gchar *ret; + JsonNode *rslt; + + rslt = fb_json_node_get(root, expr, error); + + if (rslt == NULL) { + return NULL; + } + + ret = json_node_dup_string(rslt); + json_node_free(rslt); + return ret; +} + +FbJsonValues * +fb_json_values_new(JsonNode *root) +{ + FbJsonValues *values; + FbJsonValuesPrivate *priv; + + g_return_val_if_fail(root != NULL, NULL); + + values = g_object_new(FB_TYPE_JSON_VALUES, NULL); + priv = values->priv; + priv->root = root; + + return values; +} + +void +fb_json_values_add(FbJsonValues *values, FbJsonType type, gboolean required, + const gchar *expr) +{ + FbJsonValue *value; + FbJsonValuesPrivate *priv; + + g_return_if_fail(values != NULL); + g_return_if_fail(expr != NULL); + priv = values->priv; + + value = g_new0(FbJsonValue, 1); + value->expr = expr; + value->type = type; + value->required = required; + + g_queue_push_tail(priv->queue, value); +} + +JsonNode * +fb_json_values_get_root(FbJsonValues *values) +{ + FbJsonValuesPrivate *priv; + guint index; + + g_return_val_if_fail(values != NULL, NULL); + priv = values->priv; + + if (priv->array == NULL) { + return priv->root; + } + + g_return_val_if_fail(priv->index > 0, NULL); + index = priv->index - 1; + + if (json_array_get_length(priv->array) <= index) { + return NULL; + } + + return json_array_get_element(priv->array, index); +} + +void +fb_json_values_set_array(FbJsonValues *values, gboolean required, + const gchar *expr) +{ + FbJsonValuesPrivate *priv; + + g_return_if_fail(values != NULL); + priv = values->priv; + + priv->array = fb_json_node_get_arr(priv->root, expr, &priv->error); + priv->isarray = TRUE; + + if ((priv->error != NULL) && !required) { + g_clear_error(&priv->error); + } +} + +gboolean +fb_json_values_update(FbJsonValues *values, GError **error) +{ + FbJsonValue *value; + FbJsonValuesPrivate *priv; + GError *err = NULL; + GList *l; + GType type; + JsonNode *root; + JsonNode *node; + + g_return_val_if_fail(values != NULL, FALSE); + priv = values->priv; + + if (G_UNLIKELY(priv->error != NULL)) { + g_propagate_error(error, priv->error); + priv->error = NULL; + return FALSE; + } + + if (priv->isarray) { + if ((priv->array == NULL) || + (json_array_get_length(priv->array) <= priv->index)) + { + return FALSE; + } + + root = json_array_get_element(priv->array, priv->index++); + } else { + root = priv->root; + } + + g_return_val_if_fail(root != NULL, FALSE); + + for (l = priv->queue->head; l != NULL; l = l->next) { + value = l->data; + node = fb_json_node_get(root, value->expr, &err); + + if (G_IS_VALUE(&value->value)) { + g_value_unset(&value->value); + } + + if (err != NULL) { + json_node_free(node); + + if (value->required) { + g_propagate_error(error, err); + return FALSE; + } + + g_clear_error(&err); + continue; + } + + type = json_node_get_value_type(node); + + if (G_UNLIKELY(type != value->type)) { + g_set_error(error, FB_JSON_ERROR, FB_JSON_ERROR_TYPE, + _("Expected a %s but got a %s for %s"), + g_type_name(value->type), + g_type_name(type), + value->expr); + json_node_free(node); + return FALSE; + } + + json_node_get_value(node, &value->value); + json_node_free(node); + } + + priv->next = priv->queue->head; + return TRUE; +} + +const GValue * +fb_json_values_next(FbJsonValues *values) +{ + FbJsonValue *value; + FbJsonValuesPrivate *priv; + + g_return_val_if_fail(values != NULL, NULL); + priv = values->priv; + + g_return_val_if_fail(priv->next != NULL, NULL); + value = priv->next->data; + priv->next = priv->next->next; + + if (!G_IS_VALUE(&value->value)) { + return NULL; + } + + return &value->value; +} + +gboolean +fb_json_values_next_bool(FbJsonValues *values, gboolean defval) +{ + const GValue *value; + + value = fb_json_values_next(values); + + if (G_UNLIKELY(value == NULL)) { + return defval; + } + + return g_value_get_boolean(value); +} + +gdouble +fb_json_values_next_dbl(FbJsonValues *values, gdouble defval) +{ + const GValue *value; + + value = fb_json_values_next(values); + + if (G_UNLIKELY(value == NULL)) { + return defval; + } + + return g_value_get_double(value); +} + +gint64 +fb_json_values_next_int(FbJsonValues *values, gint64 defval) +{ + const GValue *value; + + value = fb_json_values_next(values); + + if (G_UNLIKELY(value == NULL)) { + return defval; + } + + return g_value_get_int64(value); +} + +const gchar * +fb_json_values_next_str(FbJsonValues *values, const gchar *defval) +{ + const GValue *value; + + value = fb_json_values_next(values); + + if (G_UNLIKELY(value == NULL)) { + return defval; + } + + return g_value_get_string(value); +} + +gchar * +fb_json_values_next_str_dup(FbJsonValues *values, const gchar *defval) +{ + const GValue *value; + + value = fb_json_values_next(values); + + if (G_UNLIKELY(value == NULL)) { + return g_strdup(defval); + } + + return g_value_dup_string(value); +} diff --git a/facebook/json.h b/facebook/json.h new file mode 100644 index 00000000..2a462163 --- /dev/null +++ b/facebook/json.h @@ -0,0 +1,517 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#ifndef _FACEBOOK_JSON_H_ +#define _FACEBOOK_JSON_H_ + +/** + * SECTION:json + * @section_id: facebook-json + * @short_description: json.h + * @title: JSON Utilities + * + * The JSON utilities. + */ + +#include +#include + +#define FB_TYPE_JSON_VALUES (fb_json_values_get_type()) +#define FB_JSON_VALUES(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), FB_TYPE_JSON_VALUES, FbJsonValues)) +#define FB_JSON_VALUES_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), FB_TYPE_JSON_VALUES, FbJsonValuesClass)) +#define FB_IS_JSON_VALUES(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), FB_TYPE_JSON_VALUES)) +#define FB_IS_JSON_VALUES_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), FB_TYPE_JSON_VALUES)) +#define FB_JSON_VALUES_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), FB_TYPE_JSON_VALUES, FbJsonValuesClass)) + +/** + * FB_JSON_ERROR: + * + * The #GQuark of the domain of JSON errors. + */ +#define FB_JSON_ERROR fb_json_error_quark() + +typedef struct _FbJsonValues FbJsonValues; +typedef struct _FbJsonValuesClass FbJsonValuesClass; +typedef struct _FbJsonValuesPrivate FbJsonValuesPrivate; + +/** + * FbJsonError: + * @FB_JSON_ERROR_SUCCESS: There is no error. + * @FB_JSON_ERROR_AMBIGUOUS: The node has ambiguous matches. + * @FB_JSON_ERROR_GENERAL: General failure. + * @FB_JSON_ERROR_NOMATCH: The node does not match anything. + * @FB_JSON_ERROR_NULL: The node is of type NULL. + * @FB_JSON_ERROR_TYPE: The node has an unexpected type. + * + * The error codes for the #FB_JSON_ERROR domain. + */ +typedef enum +{ + FB_JSON_ERROR_SUCCESS = 0, + FB_JSON_ERROR_AMBIGUOUS, + FB_JSON_ERROR_GENERAL, + FB_JSON_ERROR_NOMATCH, + FB_JSON_ERROR_NULL, + FB_JSON_ERROR_TYPE +} FbJsonError; + +/** + * FbJsonType: + * @FB_JSON_TYPE_NULL: An unknown value. + * @FB_JSON_TYPE_BOOL: A boolean (#TRUE or #FALSE). + * @FB_JSON_TYPE_DBL: A floating point number. + * @FB_JSON_TYPE_INT: A signed integer. + * @FB_JSON_TYPE_STR: A string. + * + * The JSON data types. + */ +typedef enum +{ + FB_JSON_TYPE_NULL = 0, + FB_JSON_TYPE_BOOL = G_TYPE_BOOLEAN, + FB_JSON_TYPE_DBL = G_TYPE_DOUBLE, + FB_JSON_TYPE_INT = G_TYPE_INT64, + FB_JSON_TYPE_STR = G_TYPE_STRING +} FbJsonType; + +/** + * FbJsonValues: + * + * Represents a JSON value handler. + */ +struct _FbJsonValues +{ + /*< private >*/ + GObject parent; + FbJsonValuesPrivate *priv; +}; + +/** + * FbJsonValuesClass: + * + * The base class for all #FbJsonValues's. + */ +struct _FbJsonValuesClass +{ + /*< private >*/ + GObjectClass parent_class; +}; + +/** + * fb_json_values_get_type: + * + * Returns: The #GType for an #FbJsonValues. + */ +GType +fb_json_values_get_type(void); + +/** + * fb_json_error_quark: + * + * Gets the #GQuark of the domain of JSON errors. + * + * Returns: The #GQuark of the domain. + */ +GQuark +fb_json_error_quark(void); + +/** + * fb_json_bldr_new: + * @type: The starting #JsonNodeType. + * + * Creates a new #JsonBuilder. The starting #JsonNodeType is likely to + * be #JSON_NODE_OBJECT. The returned #JsonBuilder should be freed with + * #g_object_unref() when no longer needed. Optionally, instead of + * freeing, the returned #JsonBuilder can be closed with + * #fb_json_bldr_close(). + * + * Returns: The new #JsonBuilder. + */ +JsonBuilder * +fb_json_bldr_new(JsonNodeType type); + +/** + * fb_json_bldr_close: + * @bldr: The #JsonBuilder. + * @type: The ending #JsonNodeType. + * @size: The return local for the size of the returned string. + * + * Closes the #JsonBuilder by returning a string representing the + * #JsonBuilder. The ending #JsonNodeType is likely to be + * #JSON_NODE_OBJECT. This calls #g_object_unref(). The returned + * string should be freed with #g_free() when no longer needed. + * + * Returns: The string representation of the #JsonBuilder. + */ +gchar * +fb_json_bldr_close(JsonBuilder *bldr, JsonNodeType type, gsize *size); + +/** + * fb_json_bldr_arr_begin: + * @bldr: The #JsonBuilder. + * @name: The member name or #NULL. + * + * Begins an array member in the #JsonBuilder. + */ +void +fb_json_bldr_arr_begin(JsonBuilder *bldr, const gchar *name); + +/** + * fb_json_bldr_arr_end: + * @bldr: The #JsonBuilder. + * + * Ends an array member in the #JsonBuilder. + */ +void +fb_json_bldr_arr_end(JsonBuilder *bldr); + +/** + * fb_json_bldr_obj_begin: + * @bldr: The #JsonBuilder. + * @name: The member name or #NULL. + * + * Begins an object member in the #JsonBuilder. + */ +void +fb_json_bldr_obj_begin(JsonBuilder *bldr, const gchar *name); + +/** + * fb_json_bldr_obj_end: + * @bldr: The #JsonBuilder. + * + * Ends an array member in the #JsonBuilder. + */ +void +fb_json_bldr_obj_end(JsonBuilder *bldr); + +/** + * fb_json_bldr_add_bool: + * @bldr: The #JsonBuilder. + * @name: The member name or #NULL. + * @value: The value. + * + * Adds a boolean memeber to the #JsonBuilder. + */ +void +fb_json_bldr_add_bool(JsonBuilder *bldr, const gchar *name, gboolean value); + +/** + * fb_json_bldr_add_dbl: + * @bldr: The #JsonBuilder. + * @name: The member name or #NULL. + * @value: The value. + * + * Adds a floating point memeber to the #JsonBuilder. + */ +void +fb_json_bldr_add_dbl(JsonBuilder *bldr, const gchar *name, gdouble value); + +/** + * fb_json_bldr_add_int: + * @bldr: The #JsonBuilder. + * @name: The member name or #NULL. + * @value: The value. + * + * Adds a integer memeber to the #JsonBuilder. + */ +void +fb_json_bldr_add_int(JsonBuilder *bldr, const gchar *name, gint64 value); + +/** + * fb_json_bldr_add_str: + * @bldr: The #JsonBuilder. + * @name: The member name or #NULL. + * @value: The value. + * + * Adds a string memeber to the #JsonBuilder. + */ +void +fb_json_bldr_add_str(JsonBuilder *bldr, const gchar *name, const gchar *value); + +/** + * fb_json_bldr_add_strf: + * @bldr: The #JsonBuilder. + * @name: The member name or #NULL. + * @format: The format string literal. + * @...: The arguments for @format. + * + * Adds a formatted string memeber to the #JsonBuilder. + */ +void +fb_json_bldr_add_strf(JsonBuilder *bldr, const gchar *name, + const gchar *format, ...) + G_GNUC_PRINTF(3, 4); + +/** + * fb_json_node_new: + * @data: The string JSON. + * @size: The size of @json or -1 if null-terminated. + * @error: The return location for the #GError or #NULL. + * + * Creates a new #JsonNode. The returned #JsonBuilder should be freed + * wuth #json_node_free() when no longer needed. + * + * Returns: The new #JsonNode. + */ +JsonNode * +fb_json_node_new(const gchar *data, gssize size, GError **error); + +/** + * fb_json_node_get: + * @root: The root #JsonNode. + * @expr: The #JsonPath expression. + * @error: The return location for the #GError or #NULL. + * + * Gets a new #JsonNode value from a parent #JsonNode with a #JsonPath + * expression. The returned #JsonNode should be freed with + * #json_node_free() when no longer needed. + * + * Returns: The new #JsonNode. + */ +JsonNode * +fb_json_node_get(JsonNode *root, const gchar *expr, GError **error); + +/** + * fb_json_node_get_nth: + * @root: The root #JsonNode. + * @n: The index number. + * + * Gets a #JsonNode value from a parent #JsonNode by index. The + * returned #JsonNode should not be freed. + * + * Return: The #JsonNode. + */ +JsonNode * +fb_json_node_get_nth(JsonNode *root, guint n); + +/** + * fb_json_node_get_arr: + * @root: The root #JsonNode. + * @expr: The #JsonPath expression. + * @error: The return location for the #GError or #NULL. + * + * Gets a new #JsonArray value from a parent #JsonNode with a #JsonPath + * expression. The returned #JsonArray should be freed with + * #json_array_unref() when no longer needed. + * + * Returns: The new #JsonArray. + */ +JsonArray * +fb_json_node_get_arr(JsonNode *root, const gchar *expr, GError **error); + +/** + * fb_json_node_get_bool: + * @root: The root #JsonNode. + * @expr: The #JsonPath expression. + * @error: The return location for the #GError or #NULL. + * + * Gets a boolean value from a parent #JsonNode with a #JsonPath + * expression. + * + * Returns: The boolean value. + */ +gboolean +fb_json_node_get_bool(JsonNode *root, const gchar *expr, GError **error); + +/** + * fb_json_node_get_dbl: + * @root: The root #JsonNode. + * @expr: The #JsonPath expression. + * @error: The return location for the #GError or #NULL. + * + * Gets a floating point value from a parent #JsonNode with a #JsonPath + * expression. + * + * Returns: The floating point value. + */ +gdouble +fb_json_node_get_dbl(JsonNode *root, const gchar *expr, GError **error); + +/** + * fb_json_node_get_int: + * @root: The root #JsonNode. + * @expr: The #JsonPath expression. + * @error: The return location for the #GError or #NULL. + * + * Gets an integer value from a parent #JsonNode with a #JsonPath + * expression. + * + * Returns: The integer value. + */ +gint64 +fb_json_node_get_int(JsonNode *root, const gchar *expr, GError **error); + +/** + * fb_json_node_get_str: + * @root: The root #JsonNode. + * @expr: The #JsonPath expression. + * @error: The return location for the #GError or #NULL. + * + * Gets an string value from a parent #JsonNode with a #JsonPath + * expression. The returned string should be freed with #g_free() + * when no longer needed. + * + * Returns: The string value. + */ +gchar * +fb_json_node_get_str(JsonNode *root, const gchar *expr, GError **error); + +/** + * fb_json_values_new: + * @root: The root #JsonNode. + * + * Creates a new #FbJsonValues. The returned #FbJsonValues should be + * freed with #g_object_unref when no longer needed. + * + * Returns: The new #FbJsonValues. + */ +FbJsonValues * +fb_json_values_new(JsonNode *root); + +/** + * fb_json_values_add: + * @values: The #FbJsonValues. + * @type: The #FbJsonType. + * @required: #TRUE if the node is required, otherwise #FALSE. + * @expr: The #JsonPath expression. + * + * Adds a new #FbJsonValue to the #FbJsonValues. + */ +void +fb_json_values_add(FbJsonValues *values, FbJsonType type, gboolean required, + const gchar *expr); + +/** + * fb_json_values_get_root: + * @values: The #FbJsonValues. + * + * Gets the current working root #JsonNode. This is either the current + * array #JsonNode or the root #JsonNode. The returned #JsonNode should + * not be freed. + */ +JsonNode * +fb_json_values_get_root(FbJsonValues *values); + +/** + * fb_json_values_set_array: + * @values: The #FbJsonValues. + * @required: #TRUE if the node is required, otherwise #FALSE. + * @expr: The #JsonPath expression. + * + * Sets the #JsonPath for an array to base all #FbJsonValue's off. + */ +void +fb_json_values_set_array(FbJsonValues *values, gboolean required, + const gchar *expr); + +/** + * fb_json_values_update: + * @values: The #FbJsonValues. + * @error: The return location for the #GError or #NULL. + * + * Updates the current working root. This should be called after all of + * the #FbJsonValue's have been added with #fb_json_values_add(). If an + * array was set with #fb_json_values_set_array(), then this should be + * called in a while loop, until #FALSE is returned. + * + * Returns: #TRUE if the values were updated, otherwise #FALSE. + */ +gboolean +fb_json_values_update(FbJsonValues *values, GError **error); + +/** + * fb_json_values_next: + * @values: The #FbJsonValues. + * + * Gets the next #GValue from the #FbJsonValues. Before calling this + * function, #fb_json_values_update() must be called. + * + * Returns: The #GValue. + */ +const GValue * +fb_json_values_next(FbJsonValues *values); + +/** + * fb_json_values_next_bool: + * @values: The #FbJsonValues. + * @defval: The default value. + * + * Gets the next boolean value from the #FbJsonValues. Before calling + * this function, #fb_json_values_update() must be called. + * + * Returns: The boolean value. + */ +gboolean +fb_json_values_next_bool(FbJsonValues *values, gboolean defval); + +/** + * fb_json_values_next_dbl: + * @values: The #FbJsonValues. + * @defval: The default value. + * + * Gets the next floating point value from the #FbJsonValues. Before + * calling this function, #fb_json_values_update() must be called. + * + * Returns: The floating point value. + */ +gdouble +fb_json_values_next_dbl(FbJsonValues *values, gdouble defval); + +/** + * fb_json_values_next_int: + * @values: The #FbJsonValues. + * @defval: The default value. + * + * Gets the next integer value from the #FbJsonValues. Before calling + * this function, #fb_json_values_update() must be called. + * + * Returns: The integer value. + */ +gint64 +fb_json_values_next_int(FbJsonValues *values, gint64 defval); + +/** + * fb_json_values_next_str: + * @values: The #FbJsonValues. + * @defval: The default value. + * + * Gets the next string value from the #FbJsonValues. Before calling + * this function, #fb_json_values_update() must be called. + * + * Returns: The string value. + */ +const gchar * +fb_json_values_next_str(FbJsonValues *values, const gchar *defval); + +/** + * fb_json_values_next_str_dup: + * @values: The #FbJsonValues. + * @defval: The default value. + * + * Gets the next duplicate string value from the #FbJsonValues. Before + * calling this function, #fb_json_values_update() must be called. + * + * Returns: The duplicate string value. + */ +gchar * +fb_json_values_next_str_dup(FbJsonValues *values, const gchar *defval); + +#endif /* _FACEBOOK_JSON_H_ */ diff --git a/facebook/marshal.c b/facebook/marshal.c new file mode 100644 index 00000000..7c39f6fa --- /dev/null +++ b/facebook/marshal.c @@ -0,0 +1,195 @@ +/* This file is generated by glib-genmarshal, do not modify it. This code is licensed under the same license as the containing project. Note that it links to GLib, so must comply with the LGPL linking clauses. */ +#include + +#ifdef G_ENABLE_DEBUG +#define g_marshal_value_peek_boolean(v) g_value_get_boolean (v) +#define g_marshal_value_peek_char(v) g_value_get_schar (v) +#define g_marshal_value_peek_uchar(v) g_value_get_uchar (v) +#define g_marshal_value_peek_int(v) g_value_get_int (v) +#define g_marshal_value_peek_uint(v) g_value_get_uint (v) +#define g_marshal_value_peek_long(v) g_value_get_long (v) +#define g_marshal_value_peek_ulong(v) g_value_get_ulong (v) +#define g_marshal_value_peek_int64(v) g_value_get_int64 (v) +#define g_marshal_value_peek_uint64(v) g_value_get_uint64 (v) +#define g_marshal_value_peek_enum(v) g_value_get_enum (v) +#define g_marshal_value_peek_flags(v) g_value_get_flags (v) +#define g_marshal_value_peek_float(v) g_value_get_float (v) +#define g_marshal_value_peek_double(v) g_value_get_double (v) +#define g_marshal_value_peek_string(v) (char*) g_value_get_string (v) +#define g_marshal_value_peek_param(v) g_value_get_param (v) +#define g_marshal_value_peek_boxed(v) g_value_get_boxed (v) +#define g_marshal_value_peek_pointer(v) g_value_get_pointer (v) +#define g_marshal_value_peek_object(v) g_value_get_object (v) +#define g_marshal_value_peek_variant(v) g_value_get_variant (v) +#else /* !G_ENABLE_DEBUG */ +/* WARNING: This code accesses GValues directly, which is UNSUPPORTED API. + * Do not access GValues directly in your code. Instead, use the + * g_value_get_*() functions + */ +#define g_marshal_value_peek_boolean(v) (v)->data[0].v_int +#define g_marshal_value_peek_char(v) (v)->data[0].v_int +#define g_marshal_value_peek_uchar(v) (v)->data[0].v_uint +#define g_marshal_value_peek_int(v) (v)->data[0].v_int +#define g_marshal_value_peek_uint(v) (v)->data[0].v_uint +#define g_marshal_value_peek_long(v) (v)->data[0].v_long +#define g_marshal_value_peek_ulong(v) (v)->data[0].v_ulong +#define g_marshal_value_peek_int64(v) (v)->data[0].v_int64 +#define g_marshal_value_peek_uint64(v) (v)->data[0].v_uint64 +#define g_marshal_value_peek_enum(v) (v)->data[0].v_long +#define g_marshal_value_peek_flags(v) (v)->data[0].v_ulong +#define g_marshal_value_peek_float(v) (v)->data[0].v_float +#define g_marshal_value_peek_double(v) (v)->data[0].v_double +#define g_marshal_value_peek_string(v) (v)->data[0].v_pointer +#define g_marshal_value_peek_param(v) (v)->data[0].v_pointer +#define g_marshal_value_peek_boxed(v) (v)->data[0].v_pointer +#define g_marshal_value_peek_pointer(v) (v)->data[0].v_pointer +#define g_marshal_value_peek_object(v) (v)->data[0].v_pointer +#define g_marshal_value_peek_variant(v) (v)->data[0].v_pointer +#endif /* !G_ENABLE_DEBUG */ + +/* VOID:INT64 (/home/ayush/CLionProjects/purple-facebook/facebook/marshaller.list:1) */ +void +fb_marshal_VOID__INT64 (GClosure *closure, + GValue *return_value G_GNUC_UNUSED, + guint n_param_values, + const GValue *param_values, + gpointer invocation_hint G_GNUC_UNUSED, + gpointer marshal_data) +{ + typedef void (*GMarshalFunc_VOID__INT64) (gpointer data1, + gint64 arg1, + gpointer data2); + GCClosure *cc = (GCClosure *) closure; + gpointer data1, data2; + GMarshalFunc_VOID__INT64 callback; + + g_return_if_fail (n_param_values == 2); + + if (G_CCLOSURE_SWAP_DATA (closure)) + { + data1 = closure->data; + data2 = g_value_peek_pointer (param_values + 0); + } + else + { + data1 = g_value_peek_pointer (param_values + 0); + data2 = closure->data; + } + callback = (GMarshalFunc_VOID__INT64) (marshal_data ? marshal_data : cc->callback); + + callback (data1, + g_marshal_value_peek_int64 (param_values + 1), + data2); +} + +/* VOID:POINTER,BOOLEAN (/home/ayush/CLionProjects/purple-facebook/facebook/marshaller.list:4) */ +void +fb_marshal_VOID__POINTER_BOOLEAN (GClosure *closure, + GValue *return_value G_GNUC_UNUSED, + guint n_param_values, + const GValue *param_values, + gpointer invocation_hint G_GNUC_UNUSED, + gpointer marshal_data) +{ + typedef void (*GMarshalFunc_VOID__POINTER_BOOLEAN) (gpointer data1, + gpointer arg1, + gboolean arg2, + gpointer data2); + GCClosure *cc = (GCClosure *) closure; + gpointer data1, data2; + GMarshalFunc_VOID__POINTER_BOOLEAN callback; + + g_return_if_fail (n_param_values == 3); + + if (G_CCLOSURE_SWAP_DATA (closure)) + { + data1 = closure->data; + data2 = g_value_peek_pointer (param_values + 0); + } + else + { + data1 = g_value_peek_pointer (param_values + 0); + data2 = closure->data; + } + callback = (GMarshalFunc_VOID__POINTER_BOOLEAN) (marshal_data ? marshal_data : cc->callback); + + callback (data1, + g_marshal_value_peek_pointer (param_values + 1), + g_marshal_value_peek_boolean (param_values + 2), + data2); +} + +/* VOID:STRING,BOXED (/home/ayush/CLionProjects/purple-facebook/facebook/marshaller.list:5) */ +void +fb_marshal_VOID__STRING_BOXED (GClosure *closure, + GValue *return_value G_GNUC_UNUSED, + guint n_param_values, + const GValue *param_values, + gpointer invocation_hint G_GNUC_UNUSED, + gpointer marshal_data) +{ + typedef void (*GMarshalFunc_VOID__STRING_BOXED) (gpointer data1, + gpointer arg1, + gpointer arg2, + gpointer data2); + GCClosure *cc = (GCClosure *) closure; + gpointer data1, data2; + GMarshalFunc_VOID__STRING_BOXED callback; + + g_return_if_fail (n_param_values == 3); + + if (G_CCLOSURE_SWAP_DATA (closure)) + { + data1 = closure->data; + data2 = g_value_peek_pointer (param_values + 0); + } + else + { + data1 = g_value_peek_pointer (param_values + 0); + data2 = closure->data; + } + callback = (GMarshalFunc_VOID__STRING_BOXED) (marshal_data ? marshal_data : cc->callback); + + callback (data1, + g_marshal_value_peek_string (param_values + 1), + g_marshal_value_peek_boxed (param_values + 2), + data2); +} + +/* VOID:POINTER,POINTER (/home/ayush/CLionProjects/purple-facebook/facebook/marshaller.list:7) */ +void +fb_marshal_VOID__POINTER_POINTER (GClosure *closure, + GValue *return_value G_GNUC_UNUSED, + guint n_param_values, + const GValue *param_values, + gpointer invocation_hint G_GNUC_UNUSED, + gpointer marshal_data) +{ + typedef void (*GMarshalFunc_VOID__POINTER_POINTER) (gpointer data1, + gpointer arg1, + gpointer arg2, + gpointer data2); + GCClosure *cc = (GCClosure *) closure; + gpointer data1, data2; + GMarshalFunc_VOID__POINTER_POINTER callback; + + g_return_if_fail (n_param_values == 3); + + if (G_CCLOSURE_SWAP_DATA (closure)) + { + data1 = closure->data; + data2 = g_value_peek_pointer (param_values + 0); + } + else + { + data1 = g_value_peek_pointer (param_values + 0); + data2 = closure->data; + } + callback = (GMarshalFunc_VOID__POINTER_POINTER) (marshal_data ? marshal_data : cc->callback); + + callback (data1, + g_marshal_value_peek_pointer (param_values + 1), + g_marshal_value_peek_pointer (param_values + 2), + data2); +} + diff --git a/facebook/marshal.h b/facebook/marshal.h new file mode 100644 index 00000000..6aac4770 --- /dev/null +++ b/facebook/marshal.h @@ -0,0 +1,57 @@ +/* This file is generated by glib-genmarshal, do not modify it. This code is licensed under the same license as the containing project. Note that it links to GLib, so must comply with the LGPL linking clauses. */ +#ifndef __FB_MARSHAL_MARSHAL_H__ +#define __FB_MARSHAL_MARSHAL_H__ + +#include + +G_BEGIN_DECLS + +/* VOID:INT64 (/home/ayush/CLionProjects/purple-facebook/facebook/marshaller.list:1) */ +extern +void fb_marshal_VOID__INT64 (GClosure *closure, + GValue *return_value, + guint n_param_values, + const GValue *param_values, + gpointer invocation_hint, + gpointer marshal_data); + +/* VOID:OBJECT (/home/ayush/CLionProjects/purple-facebook/facebook/marshaller.list:2) */ +#define fb_marshal_VOID__OBJECT g_cclosure_marshal_VOID__OBJECT + +/* VOID:POINTER (/home/ayush/CLionProjects/purple-facebook/facebook/marshaller.list:3) */ +#define fb_marshal_VOID__POINTER g_cclosure_marshal_VOID__POINTER + +/* VOID:POINTER,BOOLEAN (/home/ayush/CLionProjects/purple-facebook/facebook/marshaller.list:4) */ +extern +void fb_marshal_VOID__POINTER_BOOLEAN (GClosure *closure, + GValue *return_value, + guint n_param_values, + const GValue *param_values, + gpointer invocation_hint, + gpointer marshal_data); + +/* VOID:STRING,BOXED (/home/ayush/CLionProjects/purple-facebook/facebook/marshaller.list:5) */ +extern +void fb_marshal_VOID__STRING_BOXED (GClosure *closure, + GValue *return_value, + guint n_param_values, + const GValue *param_values, + gpointer invocation_hint, + gpointer marshal_data); + +/* VOID:VOID (/home/ayush/CLionProjects/purple-facebook/facebook/marshaller.list:6) */ +#define fb_marshal_VOID__VOID g_cclosure_marshal_VOID__VOID + +/* VOID:POINTER,POINTER (/home/ayush/CLionProjects/purple-facebook/facebook/marshaller.list:7) */ +extern +void fb_marshal_VOID__POINTER_POINTER (GClosure *closure, + GValue *return_value, + guint n_param_values, + const GValue *param_values, + gpointer invocation_hint, + gpointer marshal_data); + + +G_END_DECLS + +#endif /* __FB_MARSHAL_MARSHAL_H__ */ diff --git a/facebook/marshaller.list b/facebook/marshaller.list new file mode 100644 index 00000000..ef5d4c9a --- /dev/null +++ b/facebook/marshaller.list @@ -0,0 +1,7 @@ +VOID:INT64 +VOID:OBJECT +VOID:POINTER +VOID:POINTER,BOOLEAN +VOID:STRING,BOXED +VOID:VOID +VOID:POINTER,POINTER diff --git a/facebook/mqtt.c b/facebook/mqtt.c new file mode 100644 index 00000000..6031dab6 --- /dev/null +++ b/facebook/mqtt.c @@ -0,0 +1,1023 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#include "internal.h" + +#include +#include +#include + +#include "account.h" +#include "eventloop.h" +#include "purple2compat/glibcompat.h" +#include "sslconn.h" + +#include "marshal.h" +#include "mqtt.h" +#include "util.h" + +struct _FbMqttPrivate +{ + PurpleConnection *gc; + PurpleSslConnection *gsc; + gboolean connected; + guint16 mid; + + GByteArray *rbuf; + GByteArray *wbuf; + gsize remz; + + gint tev; + gint rev; + gint wev; +}; + +struct _FbMqttMessagePrivate +{ + FbMqttMessageType type; + FbMqttMessageFlags flags; + + GByteArray *bytes; + guint offset; + guint pos; + + gboolean local; +}; + +G_DEFINE_TYPE_WITH_CODE(FbMqtt, fb_mqtt, G_TYPE_OBJECT, G_ADD_PRIVATE(FbMqtt)); +G_DEFINE_TYPE_WITH_CODE(FbMqttMessage, fb_mqtt_message, G_TYPE_OBJECT, G_ADD_PRIVATE(FbMqttMessage)); + +static void +fb_mqtt_dispose(GObject *obj) +{ + FbMqtt *mqtt = FB_MQTT(obj); + FbMqttPrivate *priv = mqtt->priv; + + fb_mqtt_close(mqtt); + g_byte_array_free(priv->rbuf, TRUE); + g_byte_array_free(priv->wbuf, TRUE); +} + +static void +fb_mqtt_class_init(FbMqttClass *klass) +{ + GObjectClass *gklass = G_OBJECT_CLASS(klass); + + gklass->dispose = fb_mqtt_dispose; + g_type_class_add_private(klass, sizeof (FbMqttPrivate)); + + /** + * FbMqtt::connect: + * @mqtt: The #FbMqtt. + * + * Emitted upon the successful completion of the connection + * process. This is emitted as a result of #fb_mqtt_connect(). + */ + g_signal_new("connect", + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_ACTION, + 0, + NULL, NULL, + fb_marshal_VOID__VOID, + G_TYPE_NONE, + 0); + + /** + * FbMqtt::error: + * @mqtt: The #FbMqtt. + * @error: The #GError. + * + * Emitted whenever an error is hit within the #FbMqtt. This + * should close the #FbMqtt with #fb_mqtt_close(). + */ + g_signal_new("error", + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_ACTION, + 0, + NULL, NULL, + fb_marshal_VOID__POINTER, + G_TYPE_NONE, + 1, G_TYPE_POINTER); + + /** + * FbMqtt::open: + * @mqtt: The #FbMqtt. + * + * Emitted upon the successful opening of the remote socket. + * This is emitted as a result of #fb_mqtt_open(). This should + * call #fb_mqtt_connect(). + */ + g_signal_new("open", + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_ACTION, + 0, + NULL, NULL, + fb_marshal_VOID__VOID, + G_TYPE_NONE, + 0); + + /** + * FbMqtt::publish: + * @mqtt: The #FbMqtt. + * @topic: The topic. + * @pload: The payload. + * + * Emitted upon an incoming message from the steam. + */ + g_signal_new("publish", + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_ACTION, + 0, + NULL, NULL, + fb_marshal_VOID__STRING_BOXED, + G_TYPE_NONE, + 2, G_TYPE_STRING, G_TYPE_BYTE_ARRAY); +} + +static void +fb_mqtt_init(FbMqtt *mqtt) +{ + FbMqttPrivate *priv; + + priv = G_TYPE_INSTANCE_GET_PRIVATE(mqtt, FB_TYPE_MQTT, FbMqttPrivate); + mqtt->priv = priv; + + priv->rbuf = g_byte_array_new(); + priv->wbuf = g_byte_array_new(); +} + +static void +fb_mqtt_message_dispose(GObject *obj) +{ + FbMqttMessagePrivate *priv = FB_MQTT_MESSAGE(obj)->priv; + + if ((priv->bytes != NULL) && priv->local) { + g_byte_array_free(priv->bytes, TRUE); + } +} + +static void +fb_mqtt_message_class_init(FbMqttMessageClass *klass) +{ + GObjectClass *gklass = G_OBJECT_CLASS(klass); + + gklass->dispose = fb_mqtt_message_dispose; + g_type_class_add_private(klass, sizeof (FbMqttMessagePrivate)); +} + +static void +fb_mqtt_message_init(FbMqttMessage *msg) +{ + FbMqttMessagePrivate *priv; + + priv = G_TYPE_INSTANCE_GET_PRIVATE(msg, FB_TYPE_MQTT_MESSAGE, + FbMqttMessagePrivate); + msg->priv = priv; +} + +GQuark +fb_mqtt_error_quark(void) +{ + static GQuark q = 0; + + if (G_UNLIKELY(q == 0)) { + q = g_quark_from_static_string("fb-mqtt-error-quark"); + } + + return q; +} + +GQuark +fb_mqtt_ssl_error_quark(void) +{ + static GQuark q = 0; + + if (G_UNLIKELY(q == 0)) { + q = g_quark_from_static_string("fb-mqtt-ssl-error-quark"); + } + + return q; +} + +FbMqtt * +fb_mqtt_new(PurpleConnection *gc) +{ + FbMqtt *mqtt; + FbMqttPrivate *priv; + + g_return_val_if_fail(PURPLE_IS_CONNECTION(gc), NULL); + + mqtt = g_object_new(FB_TYPE_MQTT, NULL); + priv = mqtt->priv; + priv->gc = gc; + + return mqtt; +}; + +void +fb_mqtt_close(FbMqtt *mqtt) +{ + FbMqttPrivate *priv; + + g_return_if_fail(FB_IS_MQTT(mqtt)); + priv = mqtt->priv; + + if (priv->wev > 0) { + purple_input_remove(priv->wev); + priv->wev = 0; + } + + if (priv->rev > 0) { + purple_input_remove(priv->rev); + priv->rev = 0; + } + + if (priv->tev > 0) { + purple_timeout_remove(priv->tev); + priv->tev = 0; + } + + if (priv->gsc != NULL) { + purple_ssl_close(priv->gsc); + priv->gsc = NULL; + } + + if (priv->wbuf->len > 0) { + fb_util_debug_warning("Closing with unwritten data"); + } + + priv->connected = FALSE; + g_byte_array_set_size(priv->rbuf, 0); + g_byte_array_set_size(priv->wbuf, 0); +} + +void +fb_mqtt_error(FbMqtt *mqtt, FbMqttError error, const gchar *format, ...) +{ + GError *err; + va_list ap; + + g_return_if_fail(FB_IS_MQTT(mqtt)); + + va_start(ap, format); + err = g_error_new_valist(FB_MQTT_ERROR, error, format, ap); + va_end(ap); + + g_signal_emit_by_name(mqtt, "error", err); + g_error_free(err); +} + +static gboolean +fb_mqtt_cb_timeout(gpointer data) +{ + FbMqtt *mqtt = data; + FbMqttPrivate *priv = mqtt->priv; + + priv->tev = 0; + fb_mqtt_error(mqtt, FB_MQTT_ERROR_GENERAL, _("Connection timed out")); + return FALSE; +} + +static void +fb_mqtt_timeout_clear(FbMqtt *mqtt) +{ + FbMqttPrivate *priv = mqtt->priv; + + if (priv->tev > 0) { + g_source_remove(priv->tev); + priv->tev = 0; + } +} + +static void +fb_mqtt_timeout(FbMqtt *mqtt) +{ + FbMqttPrivate *priv = mqtt->priv; + + fb_mqtt_timeout_clear(mqtt); + priv->tev = g_timeout_add(FB_MQTT_TIMEOUT_CONN, + fb_mqtt_cb_timeout, mqtt); +} + +static gboolean +fb_mqtt_cb_ping(gpointer data) +{ + FbMqtt *mqtt = data; + FbMqttMessage *msg; + FbMqttPrivate *priv = mqtt->priv; + + msg = fb_mqtt_message_new(FB_MQTT_MESSAGE_TYPE_PINGREQ, 0); + fb_mqtt_write(mqtt, msg); + g_object_unref(msg); + + priv->tev = 0; + fb_mqtt_timeout(mqtt); + return FALSE; +} + +static void +fb_mqtt_ping(FbMqtt *mqtt) +{ + FbMqttPrivate *priv = mqtt->priv; + + fb_mqtt_timeout_clear(mqtt); + priv->tev = g_timeout_add(FB_MQTT_TIMEOUT_PING, + fb_mqtt_cb_ping, mqtt); +} + +static void +fb_mqtt_cb_read(gpointer data, gint fd, PurpleInputCondition cond) +{ + FbMqtt *mqtt = data; + FbMqttMessage *msg; + FbMqttPrivate *priv = mqtt->priv; + gint res; + guint mult; + guint8 buf[1024]; + guint8 byte; + gsize size; + gssize rize; + + if (priv->remz < 1) { + /* Reset the read buffer */ + g_byte_array_set_size(priv->rbuf, 0); + + res = purple_ssl_read(priv->gsc, &byte, sizeof byte); + + if (res < 0 && errno == EAGAIN) { + return; + } else if (res != 1) { + fb_mqtt_error(mqtt, FB_MQTT_ERROR_GENERAL, + _("Failed to read fixed header")); + return; + } + + g_byte_array_append(priv->rbuf, &byte, sizeof byte); + + mult = 1; + + do { + res = purple_ssl_read(priv->gsc, &byte, sizeof byte); + + /* TODO: this case isn't handled yet */ + if (0 && res < 0 && errno == EAGAIN) { + return; + } else if (res != 1) { + fb_mqtt_error(mqtt, FB_MQTT_ERROR_GENERAL, + _("Failed to read packet size")); + return; + } + + g_byte_array_append(priv->rbuf, &byte, sizeof byte); + + priv->remz += (byte & 127) * mult; + mult *= 128; + } while ((byte & 128) != 0); + } + + if (priv->remz > 0) { + size = MIN(priv->remz, sizeof buf); + rize = purple_ssl_read(priv->gsc, buf, size); + + if (rize < 0 && errno == EAGAIN) { + return; + } else if (rize < 1) { + fb_mqtt_error(mqtt, FB_MQTT_ERROR_GENERAL, + _("Failed to read packet data")); + return; + } + + g_byte_array_append(priv->rbuf, buf, rize); + priv->remz -= rize; + } + + if (priv->remz < 1) { + msg = fb_mqtt_message_new_bytes(priv->rbuf); + priv->remz = 0; + + if (G_UNLIKELY(msg == NULL)) { + fb_mqtt_error(mqtt, FB_MQTT_ERROR_GENERAL, + _("Failed to parse message")); + return; + } + + fb_mqtt_read(mqtt, msg); + g_object_unref(msg); + } +} + +void +fb_mqtt_read(FbMqtt *mqtt, FbMqttMessage *msg) +{ + FbMqttMessage *nsg; + FbMqttPrivate *priv; + FbMqttMessagePrivate *mriv; + GByteArray *wytes; + gchar *str; + guint8 chr; + guint16 mid; + + g_return_if_fail(FB_IS_MQTT(mqtt)); + g_return_if_fail(FB_IS_MQTT_MESSAGE(msg)); + priv = mqtt->priv; + mriv = msg->priv; + + fb_util_debug_hexdump(FB_UTIL_DEBUG_INFO, mriv->bytes, + "Reading %d (flags: 0x%0X)", + mriv->type, mriv->flags); + + switch (mriv->type) { + case FB_MQTT_MESSAGE_TYPE_CONNACK: + if (!fb_mqtt_message_read_byte(msg, NULL) || + !fb_mqtt_message_read_byte(msg, &chr)) + { + break; + } + + if (chr != FB_MQTT_ERROR_SUCCESS) { + fb_mqtt_error(mqtt, chr, _("Connection failed (%u)"), + chr); + return; + } + + priv->connected = TRUE; + fb_mqtt_ping(mqtt); + g_signal_emit_by_name(mqtt, "connect"); + return; + + case FB_MQTT_MESSAGE_TYPE_PUBLISH: + if (!fb_mqtt_message_read_str(msg, &str)) { + break; + } + + if ((mriv->flags & FB_MQTT_MESSAGE_FLAG_QOS1) || + (mriv->flags & FB_MQTT_MESSAGE_FLAG_QOS2)) + { + if (mriv->flags & FB_MQTT_MESSAGE_FLAG_QOS1) { + chr = FB_MQTT_MESSAGE_TYPE_PUBACK; + } else { + chr = FB_MQTT_MESSAGE_TYPE_PUBREC; + } + + if (!fb_mqtt_message_read_mid(msg, &mid)) { + g_free(str); + break; + } + + nsg = fb_mqtt_message_new(chr, 0); + fb_mqtt_message_write_u16(nsg, mid); + fb_mqtt_write(mqtt, nsg); + g_object_unref(nsg); + } + + wytes = g_byte_array_new(); + fb_mqtt_message_read_r(msg, wytes); + g_signal_emit_by_name(mqtt, "publish", str, wytes); + g_byte_array_free(wytes, TRUE); + g_free(str); + return; + + case FB_MQTT_MESSAGE_TYPE_PUBREL: + if (!fb_mqtt_message_read_mid(msg, &mid)) { + break; + } + + nsg = fb_mqtt_message_new(FB_MQTT_MESSAGE_TYPE_PUBCOMP, 0); + fb_mqtt_message_write_u16(nsg, mid); /* Message identifier */ + fb_mqtt_write(mqtt, nsg); + g_object_unref(nsg); + return; + + case FB_MQTT_MESSAGE_TYPE_PINGRESP: + fb_mqtt_ping(mqtt); + return; + + case FB_MQTT_MESSAGE_TYPE_PUBACK: + case FB_MQTT_MESSAGE_TYPE_PUBCOMP: + case FB_MQTT_MESSAGE_TYPE_SUBACK: + case FB_MQTT_MESSAGE_TYPE_UNSUBACK: + return; + + default: + fb_mqtt_error(mqtt, FB_MQTT_ERROR_GENERAL, + _("Unknown packet (%u)"), mriv->type); + return; + } + + /* Since no case returned, there was a parse error. */ + fb_mqtt_error(mqtt, FB_MQTT_ERROR_GENERAL, + _("Failed to parse message")); +} + +static void +fb_mqtt_cb_write(gpointer data, gint fd, PurpleInputCondition cond) +{ + FbMqtt *mqtt = data; + FbMqttPrivate *priv = mqtt->priv; + gssize wize; + + wize = purple_ssl_write(priv->gsc, priv->wbuf->data, priv->wbuf->len); + + if (wize < 0) { + fb_mqtt_error(mqtt, FB_MQTT_ERROR_GENERAL, + _("Failed to write data")); + return; + } + + if (wize > 0) { + g_byte_array_remove_range(priv->wbuf, 0, wize); + } + + if (priv->wbuf->len < 1) { + priv->wev = 0; + } +} + +void +fb_mqtt_write(FbMqtt *mqtt, FbMqttMessage *msg) +{ + const GByteArray *bytes; + FbMqttMessagePrivate *mriv; + FbMqttPrivate *priv; + + g_return_if_fail(FB_IS_MQTT(mqtt)); + g_return_if_fail(FB_IS_MQTT_MESSAGE(msg)); + priv = mqtt->priv; + mriv = msg->priv; + + bytes = fb_mqtt_message_bytes(msg); + + if (G_UNLIKELY(bytes == NULL)) { + fb_mqtt_error(mqtt, FB_MQTT_ERROR_GENERAL, + _("Failed to format data")); + return; + } + + fb_util_debug_hexdump(FB_UTIL_DEBUG_INFO, mriv->bytes, + "Writing %d (flags: 0x%0X)", + mriv->type, mriv->flags); + + g_byte_array_append(priv->wbuf, bytes->data, bytes->len); + fb_mqtt_cb_write(mqtt, priv->gsc->fd, PURPLE_INPUT_WRITE); + + if (priv->wev > 0) { + priv->wev = purple_input_add(priv->gsc->fd, + PURPLE_INPUT_WRITE, + fb_mqtt_cb_write, mqtt); + } +} + +static void +fb_mqtt_cb_open(gpointer data, PurpleSslConnection *ssl, + PurpleInputCondition cond) +{ + FbMqtt *mqtt = data; + FbMqttPrivate *priv = mqtt->priv; + + fb_mqtt_timeout_clear(mqtt); + priv->rev = purple_input_add(priv->gsc->fd, PURPLE_INPUT_READ, + fb_mqtt_cb_read, mqtt); + g_signal_emit_by_name(mqtt, "open"); +} + +static void +fb_mqtt_cb_open_error(PurpleSslConnection *ssl, PurpleSslErrorType error, + gpointer data) +{ + const gchar *str; + FbMqtt *mqtt = data; + FbMqttPrivate *priv = mqtt->priv; + GError *err; + + str = purple_ssl_strerror(error); + err = g_error_new_literal(FB_MQTT_SSL_ERROR, error, str); + + /* Do not call purple_ssl_close() from the error_func */ + priv->gsc = NULL; + + g_signal_emit_by_name(mqtt, "error", err); + g_error_free(err); +} + +void +fb_mqtt_open(FbMqtt *mqtt, const gchar *host, gint port) +{ + FbMqttPrivate *priv; + PurpleAccount *acc; + + g_return_if_fail(FB_IS_MQTT(mqtt)); + priv = mqtt->priv; + + acc = purple_connection_get_account(priv->gc); + fb_mqtt_close(mqtt); + priv->gsc = purple_ssl_connect(acc, host, port, fb_mqtt_cb_open, + fb_mqtt_cb_open_error, mqtt); + + if (priv->gsc == NULL) { + fb_mqtt_cb_open_error(NULL, 0, mqtt); + return; + } + + fb_mqtt_timeout(mqtt); +} + +void +fb_mqtt_connect(FbMqtt *mqtt, guint8 flags, const GByteArray *pload) +{ + FbMqttMessage *msg; + + g_return_if_fail(!fb_mqtt_connected(mqtt, FALSE)); + g_return_if_fail(pload != NULL); + + /* Facebook always sends a CONNACK, use QoS1 */ + flags |= FB_MQTT_CONNECT_FLAG_QOS1; + + msg = fb_mqtt_message_new(FB_MQTT_MESSAGE_TYPE_CONNECT, 0); + fb_mqtt_message_write_str(msg, FB_MQTT_NAME); /* Protocol name */ + fb_mqtt_message_write_byte(msg, FB_MQTT_LEVEL); /* Protocol level */ + fb_mqtt_message_write_byte(msg, flags); /* Flags */ + fb_mqtt_message_write_u16(msg, FB_MQTT_KA); /* Keep alive */ + + fb_mqtt_message_write(msg, pload->data, pload->len); + fb_mqtt_write(mqtt, msg); + + fb_mqtt_timeout(mqtt); + g_object_unref(msg); +} + +gboolean +fb_mqtt_connected(FbMqtt *mqtt, gboolean error) +{ + FbMqttPrivate *priv; + gboolean connected; + + g_return_val_if_fail(FB_IS_MQTT(mqtt), FALSE); + priv = mqtt->priv; + connected = (priv->gsc != NULL) && priv->connected; + + if (!connected && error) { + fb_mqtt_error(mqtt, FB_MQTT_ERROR_GENERAL, + _("Not connected")); + } + + return connected; +} + +void +fb_mqtt_disconnect(FbMqtt *mqtt) +{ + FbMqttMessage *msg; + + if (G_UNLIKELY(!fb_mqtt_connected(mqtt, FALSE))) { + return; + } + + msg = fb_mqtt_message_new(FB_MQTT_MESSAGE_TYPE_DISCONNECT, 0); + fb_mqtt_write(mqtt, msg); + g_object_unref(msg); + fb_mqtt_close(mqtt); +} + +void +fb_mqtt_publish(FbMqtt *mqtt, const gchar *topic, const GByteArray *pload) +{ + FbMqttMessage *msg; + FbMqttPrivate *priv; + + g_return_if_fail(FB_IS_MQTT(mqtt)); + g_return_if_fail(fb_mqtt_connected(mqtt, FALSE)); + priv = mqtt->priv; + + /* Message identifier not required, but for consistency use QoS1 */ + msg = fb_mqtt_message_new(FB_MQTT_MESSAGE_TYPE_PUBLISH, + FB_MQTT_MESSAGE_FLAG_QOS1); + + fb_mqtt_message_write_str(msg, topic); /* Message topic */ + fb_mqtt_message_write_mid(msg, &priv->mid); /* Message identifier */ + + if (pload != NULL) { + fb_mqtt_message_write(msg, pload->data, pload->len); + } + + fb_mqtt_write(mqtt, msg); + g_object_unref(msg); +} + +void +fb_mqtt_subscribe(FbMqtt *mqtt, const gchar *topic1, guint16 qos1, ...) +{ + const gchar *topic; + FbMqttMessage *msg; + FbMqttPrivate *priv; + guint16 qos; + va_list ap; + + g_return_if_fail(FB_IS_MQTT(mqtt)); + g_return_if_fail(fb_mqtt_connected(mqtt, FALSE)); + priv = mqtt->priv; + + /* Facebook requires a message identifier, use QoS1 */ + msg = fb_mqtt_message_new(FB_MQTT_MESSAGE_TYPE_SUBSCRIBE, + FB_MQTT_MESSAGE_FLAG_QOS1); + + fb_mqtt_message_write_mid(msg, &priv->mid); /* Message identifier */ + fb_mqtt_message_write_str(msg, topic1); /* First topics */ + fb_mqtt_message_write_byte(msg, qos1); /* First QoS value */ + + va_start(ap, qos1); + + while ((topic = va_arg(ap, const gchar*)) != NULL) { + qos = va_arg(ap, guint); + fb_mqtt_message_write_str(msg, topic); /* Remaining topics */ + fb_mqtt_message_write_byte(msg, qos); /* Remaining QoS values */ + } + + va_end(ap); + + fb_mqtt_write(mqtt, msg); + g_object_unref(msg); +} + +void +fb_mqtt_unsubscribe(FbMqtt *mqtt, const gchar *topic1, ...) +{ + const gchar *topic; + FbMqttMessage *msg; + FbMqttPrivate *priv; + va_list ap; + + g_return_if_fail(FB_IS_MQTT(mqtt)); + g_return_if_fail(fb_mqtt_connected(mqtt, FALSE)); + priv = mqtt->priv; + + /* Facebook requires a message identifier, use QoS1 */ + msg = fb_mqtt_message_new(FB_MQTT_MESSAGE_TYPE_UNSUBSCRIBE, + FB_MQTT_MESSAGE_FLAG_QOS1); + + fb_mqtt_message_write_mid(msg, &priv->mid); /* Message identifier */ + fb_mqtt_message_write_str(msg, topic1); /* First topic */ + + va_start(ap, topic1); + + while ((topic = va_arg(ap, const gchar*)) != NULL) { + fb_mqtt_message_write_str(msg, topic); /* Remaining topics */ + } + + va_end(ap); + + fb_mqtt_write(mqtt, msg); + g_object_unref(msg); +} + +FbMqttMessage * +fb_mqtt_message_new(FbMqttMessageType type, FbMqttMessageFlags flags) +{ + FbMqttMessage *msg; + FbMqttMessagePrivate *priv; + + msg = g_object_new(FB_TYPE_MQTT_MESSAGE, NULL); + priv = msg->priv; + + priv->type = type; + priv->flags = flags; + priv->bytes = g_byte_array_new(); + priv->local = TRUE; + + return msg; +} + +FbMqttMessage * +fb_mqtt_message_new_bytes(GByteArray *bytes) +{ + FbMqttMessage *msg; + FbMqttMessagePrivate *priv; + guint8 *byte; + + g_return_val_if_fail(bytes != NULL, NULL); + g_return_val_if_fail(bytes->len >= 2, NULL); + + msg = g_object_new(FB_TYPE_MQTT_MESSAGE, NULL); + priv = msg->priv; + + priv->bytes = bytes; + priv->local = FALSE; + priv->type = (*bytes->data & 0xF0) >> 4; + priv->flags = *bytes->data & 0x0F; + + /* Skip the fixed header */ + for (byte = priv->bytes->data + 1; (*(byte++) & 128) != 0; ); + priv->offset = byte - bytes->data; + priv->pos = priv->offset; + + return msg; +} + +void +fb_mqtt_message_reset(FbMqttMessage *msg) +{ + FbMqttMessagePrivate *priv; + + g_return_if_fail(FB_IS_MQTT_MESSAGE(msg)); + priv = msg->priv; + + if (priv->offset > 0) { + g_byte_array_remove_range(priv->bytes, 0, priv->offset); + priv->offset = 0; + priv->pos = 0; + } +} + +const GByteArray * +fb_mqtt_message_bytes(FbMqttMessage *msg) +{ + FbMqttMessagePrivate *priv; + guint i; + guint8 byte; + guint8 sbuf[4]; + guint32 size; + + g_return_val_if_fail(FB_IS_MQTT_MESSAGE(msg), NULL); + priv = msg->priv; + + i = 0; + size = priv->bytes->len - priv->offset; + + do { + if (G_UNLIKELY(i >= G_N_ELEMENTS(sbuf))) { + return NULL; + } + + byte = size % 128; + size /= 128; + + if (size > 0) { + byte |= 128; + } + + sbuf[i++] = byte; + } while (size > 0); + + fb_mqtt_message_reset(msg); + g_byte_array_prepend(priv->bytes, sbuf, i); + + byte = ((priv->type & 0x0F) << 4) | (priv->flags & 0x0F); + g_byte_array_prepend(priv->bytes, &byte, sizeof byte); + + priv->pos = (i + 1) * (sizeof byte); + return priv->bytes; +} + +gboolean +fb_mqtt_message_read(FbMqttMessage *msg, gpointer data, guint size) +{ + FbMqttMessagePrivate *priv; + + g_return_val_if_fail(FB_IS_MQTT_MESSAGE(msg), FALSE); + priv = msg->priv; + + if ((priv->pos + size) > priv->bytes->len) { + return FALSE; + } + + if ((data != NULL) && (size > 0)) { + memcpy(data, priv->bytes->data + priv->pos, size); + } + + priv->pos += size; + return TRUE; +} + +gboolean +fb_mqtt_message_read_r(FbMqttMessage *msg, GByteArray *bytes) +{ + FbMqttMessagePrivate *priv; + guint size; + + g_return_val_if_fail(FB_IS_MQTT_MESSAGE(msg), FALSE); + priv = msg->priv; + size = priv->bytes->len - priv->pos; + + if (G_LIKELY(size > 0)) { + g_byte_array_append(bytes, priv->bytes->data + priv->pos, + size); + } + + return TRUE; +} + +gboolean +fb_mqtt_message_read_byte(FbMqttMessage *msg, guint8 *value) +{ + return fb_mqtt_message_read(msg, value, sizeof *value); +} + +gboolean +fb_mqtt_message_read_mid(FbMqttMessage *msg, guint16 *value) +{ + return fb_mqtt_message_read_u16(msg, value); +} + +gboolean +fb_mqtt_message_read_u16(FbMqttMessage *msg, guint16 *value) +{ + if (!fb_mqtt_message_read(msg, value, sizeof *value)) { + return FALSE; + } + + if (value != NULL) { + *value = g_ntohs(*value); + } + + return TRUE; +} + +gboolean +fb_mqtt_message_read_str(FbMqttMessage *msg, gchar **value) +{ + guint8 *data; + guint16 size; + + if (!fb_mqtt_message_read_u16(msg, &size)) { + return FALSE; + } + + if (value != NULL) { + data = g_new(guint8, size + 1); + data[size] = 0; + } else { + data = NULL; + } + + if (!fb_mqtt_message_read(msg, data, size)) { + g_free(data); + return FALSE; + } + + if (value != NULL) { + *value = (gchar *) data; + } + + return TRUE; +} + +void +fb_mqtt_message_write(FbMqttMessage *msg, gconstpointer data, guint size) +{ + FbMqttMessagePrivate *priv; + + g_return_if_fail(FB_IS_MQTT_MESSAGE(msg)); + priv = msg->priv; + + g_byte_array_append(priv->bytes, data, size); + priv->pos += size; +} + +void +fb_mqtt_message_write_byte(FbMqttMessage *msg, guint8 value) +{ + fb_mqtt_message_write(msg, &value, sizeof value); +} + +void +fb_mqtt_message_write_mid(FbMqttMessage *msg, guint16 *value) +{ + g_return_if_fail(value != NULL); + fb_mqtt_message_write_u16(msg, ++(*value)); +} + +void +fb_mqtt_message_write_u16(FbMqttMessage *msg, guint16 value) +{ + value = g_htons(value); + fb_mqtt_message_write(msg, &value, sizeof value); +} + +void +fb_mqtt_message_write_str(FbMqttMessage *msg, const gchar *value) +{ + gint16 size; + + g_return_if_fail(value != NULL); + + size = strlen(value); + fb_mqtt_message_write_u16(msg, size); + fb_mqtt_message_write(msg, value, size); +} diff --git a/facebook/mqtt.h b/facebook/mqtt.h new file mode 100644 index 00000000..a33b4f52 --- /dev/null +++ b/facebook/mqtt.h @@ -0,0 +1,626 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#ifndef _FACEBOOK_MQTT_H_ +#define _FACEBOOK_MQTT_H_ + +/** + * SECTION:mqtt + * @section_id: facebook-mqtt + * @short_description: mqtt.h + * @title: MQTT Connection + * + * The MQTT connection. + */ + +#include +#include + +#include "connection.h" + +#define FB_TYPE_MQTT (fb_mqtt_get_type()) +#define FB_MQTT(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), FB_TYPE_MQTT, FbMqtt)) +#define FB_MQTT_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), FB_TYPE_MQTT, FbMqttClass)) +#define FB_IS_MQTT(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), FB_TYPE_MQTT)) +#define FB_IS_MQTT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), FB_TYPE_MQTT)) +#define FB_MQTT_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), FB_TYPE_MQTT, FbMqttClass)) + +#define FB_TYPE_MQTT_MESSAGE (fb_mqtt_message_get_type()) +#define FB_MQTT_MESSAGE(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), FB_TYPE_MQTT_MESSAGE, FbMqttMessage)) +#define FB_MQTT_MESSAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), FB_TYPE_MQTT_MESSAGE, FbMqttMessageClass)) +#define FB_IS_MQTT_MESSAGE(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), FB_TYPE_MQTT_MESSAGE)) +#define FB_IS_MQTT_MESSAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), FB_TYPE_MQTT_MESSAGE)) +#define FB_MQTT_MESSAGE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), FB_TYPE_MQTT_MESSAGE, FbMqttMessageClass)) + +/** + * FB_MQTT_NAME: + * + * The name of the MQTT version. + */ +#define FB_MQTT_NAME "MQTToT" + +/** + * FB_MQTT_LEVEL: + * + * The level of the MQTT version. + */ +#define FB_MQTT_LEVEL 3 + +/** + * FB_MQTT_KA: + * + * The keep-alive timeout, in seconds, of the MQTT connection. + */ +#define FB_MQTT_KA 60 + +/** + * FB_MQTT_HOST: + * + * The MQTT host name for Facebook. + */ +#define FB_MQTT_HOST "mqtt.facebook.com" + +/** + * FB_MQTT_PORT: + * + * The MQTT host port for Facebook. + */ +#define FB_MQTT_PORT 443 + +/** + * FB_MQTT_TIMEOUT_CONN: + * + * The timeout, in milliseconds, to wait for a PING back from the + * server. + */ +#define FB_MQTT_TIMEOUT_CONN (FB_MQTT_KA * 1500) + +/** + * FB_MQTT_TIMEOUT_PING: + * + * The timeout, in milliseconds, to send a PING to the server. + */ +#define FB_MQTT_TIMEOUT_PING (FB_MQTT_KA * 1000) + +/** + * FB_MQTT_ERROR: + * + * The #GQuark of the domain of MQTT errors. + */ +#define FB_MQTT_ERROR fb_mqtt_error_quark() + +/** + * FB_MQTT_SSL_ERROR: + * + * The #GQuark of the domain of MQTT SSL errors. + */ +#define FB_MQTT_SSL_ERROR fb_mqtt_ssl_error_quark() + +typedef struct _FbMqtt FbMqtt; +typedef struct _FbMqttClass FbMqttClass; +typedef struct _FbMqttPrivate FbMqttPrivate; +typedef struct _FbMqttMessage FbMqttMessage; +typedef struct _FbMqttMessageClass FbMqttMessageClass; +typedef struct _FbMqttMessagePrivate FbMqttMessagePrivate; + +/** + * FbMqttConnectFlags: + * @FB_MQTT_CONNECT_FLAG_CLR: Clear the session. + * @FB_MQTT_CONNECT_FLAG_WILL: A will message is in the payload. + * @FB_MQTT_CONNECT_FLAG_RET: Retain the will message. + * @FB_MQTT_CONNECT_FLAG_PASS: A password is in the payload. + * @FB_MQTT_CONNECT_FLAG_USER: A user name is in the payload. + * @FB_MQTT_CONNECT_FLAG_QOS0: Use no quality of service. + * @FB_MQTT_CONNECT_FLAG_QOS1: Use level one quality of service. + * @FB_MQTT_CONNECT_FLAG_QOS2: Use level two quality of service. + * + * The #FbMqttMessage flags for the CONNECT message. + */ +typedef enum +{ + FB_MQTT_CONNECT_FLAG_CLR = 1 << 1, + FB_MQTT_CONNECT_FLAG_WILL = 1 << 2, + FB_MQTT_CONNECT_FLAG_RET = 1 << 5, + FB_MQTT_CONNECT_FLAG_PASS = 1 << 6, + FB_MQTT_CONNECT_FLAG_USER = 1 << 7, + FB_MQTT_CONNECT_FLAG_QOS0 = 0 << 3, + FB_MQTT_CONNECT_FLAG_QOS1 = 1 << 3, + FB_MQTT_CONNECT_FLAG_QOS2 = 2 << 3 +} FbMqttConnectFlags; + +/** + * FbMqttError: + * @FB_MQTT_ERROR_SUCCESS: There is no error. + * @FB_MQTT_ERROR_PRTVERS: Unacceptable protocol version. + * @FB_MQTT_ERROR_IDREJECT: Identifier rejected. + * @FB_MQTT_ERROR_SRVGONE: Server unavailable. + * @FB_MQTT_ERROR_USERPASS: Bad user name or password. + * @FB_MQTT_ERROR_UNAUTHORIZED: Not authorized. + * @FB_MQTT_ERROR_GENERAL: General failure. + * + * The error codes for the #FB_MQTT_ERROR domain. + */ +typedef enum +{ + FB_MQTT_ERROR_SUCCESS = 0, + FB_MQTT_ERROR_PRTVERS = 1, + FB_MQTT_ERROR_IDREJECT = 2, + FB_MQTT_ERROR_SRVGONE = 3, + FB_MQTT_ERROR_USERPASS = 4, + FB_MQTT_ERROR_UNAUTHORIZED = 5, + FB_MQTT_ERROR_GENERAL +} FbMqttError; + +/** + * FbMqttMessageFlags: + * @FB_MQTT_MESSAGE_FLAG_RET: Retain messages. + * @FB_MQTT_MESSAGE_FLAG_DUP: Duplicate delivery of control packet. + * @FB_MQTT_MESSAGE_FLAG_QOS0: Use no quality of service. + * @FB_MQTT_MESSAGE_FLAG_QOS1: Use level one quality of service. + * @FB_MQTT_MESSAGE_FLAG_QOS2: Use level two quality of service. + * + * The #FbMqttMessage flags. + */ +typedef enum +{ + FB_MQTT_MESSAGE_FLAG_RET = 1 << 0, + FB_MQTT_MESSAGE_FLAG_DUP = 1 << 3, + FB_MQTT_MESSAGE_FLAG_QOS0 = 0 << 1, + FB_MQTT_MESSAGE_FLAG_QOS1 = 1 << 1, + FB_MQTT_MESSAGE_FLAG_QOS2 = 2 << 1 +} FbMqttMessageFlags; + +/** + * FbMqttMessageType: + * @FB_MQTT_MESSAGE_TYPE_CONNECT: Requests a connection. + * @FB_MQTT_MESSAGE_TYPE_CONNACK: Connection acknowledgment. + * @FB_MQTT_MESSAGE_TYPE_PUBLISH: Requests a message publication. + * @FB_MQTT_MESSAGE_TYPE_PUBACK: Publication acknowledgment. + * @FB_MQTT_MESSAGE_TYPE_PUBREC: Publication received. + * @FB_MQTT_MESSAGE_TYPE_PUBREL: Publication released. + * @FB_MQTT_MESSAGE_TYPE_PUBCOMP: Publication complete. + * @FB_MQTT_MESSAGE_TYPE_SUBSCRIBE: Requests a subscription. + * @FB_MQTT_MESSAGE_TYPE_SUBACK: Subscription acknowledgment. + * @FB_MQTT_MESSAGE_TYPE_UNSUBSCRIBE: Requests an unsubscription. + * @FB_MQTT_MESSAGE_TYPE_UNSUBACK: Unsubscription acknowledgment. + * @FB_MQTT_MESSAGE_TYPE_PINGREQ: Requests a ping response. + * @FB_MQTT_MESSAGE_TYPE_PINGRESP: Ping response. + * @FB_MQTT_MESSAGE_TYPE_DISCONNECT: Requests a disconnection. + * + * The #FbMqttMessage types. + */ +typedef enum +{ + FB_MQTT_MESSAGE_TYPE_CONNECT = 1, + FB_MQTT_MESSAGE_TYPE_CONNACK = 2, + FB_MQTT_MESSAGE_TYPE_PUBLISH = 3, + FB_MQTT_MESSAGE_TYPE_PUBACK = 4, + FB_MQTT_MESSAGE_TYPE_PUBREC = 5, + FB_MQTT_MESSAGE_TYPE_PUBREL = 6, + FB_MQTT_MESSAGE_TYPE_PUBCOMP = 7, + FB_MQTT_MESSAGE_TYPE_SUBSCRIBE = 8, + FB_MQTT_MESSAGE_TYPE_SUBACK = 9, + FB_MQTT_MESSAGE_TYPE_UNSUBSCRIBE = 10, + FB_MQTT_MESSAGE_TYPE_UNSUBACK = 11, + FB_MQTT_MESSAGE_TYPE_PINGREQ = 12, + FB_MQTT_MESSAGE_TYPE_PINGRESP = 13, + FB_MQTT_MESSAGE_TYPE_DISCONNECT = 14 +} FbMqttMessageType; + +/** + * FbMqtt: + * + * Represents an MQTT connection. + */ +struct _FbMqtt +{ + /*< private >*/ + GObject parent; + FbMqttPrivate *priv; +}; + +/** + * FbMqttClass: + * + * The base class for all #FbMqtt's. + */ +struct _FbMqttClass +{ + /*< private >*/ + GObjectClass parent_class; +}; + +/** + * FbMqttMessage: + * + * Represents a reader/writer for an MQTT message. + */ +struct _FbMqttMessage +{ + /*< private >*/ + GObject parent; + FbMqttMessagePrivate *priv; +}; + +/** + * FbMqttMessageClass: + * + * The base class for all #FbMqttMessageClass's. + */ +struct _FbMqttMessageClass +{ + /*< private >*/ + GObjectClass parent_class; +}; + +/** + * fb_mqtt_get_type: + * + * Returns: The #GType for an #FbMqtt. + */ +GType +fb_mqtt_get_type(void); + +/** + * fb_mqtt_message_get_type: + * + * Returns: The #GType for an #FbMqttMessage. + */ +GType +fb_mqtt_message_get_type(void); + +/** + * fb_mqtt_error_quark: + * + * Gets the #GQuark of the domain of MQTT errors. + * + * Returns: The #GQuark of the domain. + */ +GQuark +fb_mqtt_error_quark(void); + +/** + * fb_mqtt_ssl_error_quark: + * + * Gets the #GQuark of the domain of MQTT SSL errors. + * + * Returns: The #GQuark of the domain. + */ +GQuark +fb_mqtt_ssl_error_quark(void); + +/** + * fb_mqtt_new: + * @gc: The #PurpleConnection. + * + * Creates a new #FbMqtt. The returned #FbMqtt should be freed with + * #g_object_unref() when no longer needed. + * + * Returns: The new #FbMqtt. + */ +FbMqtt * +fb_mqtt_new(PurpleConnection *gc); + +/** + * fb_mqtt_close: + * @mqtt: The #FbMqtt. + * + * Closes the MQTT without sending the `DISCONNECT` message. The #FbMqtt + * may be reopened after calling this. + */ +void +fb_mqtt_close(FbMqtt *mqtt); + +/** + * fb_mqtt_error: + * @mqtt: The #FbMqtt. + * @error: The #FbMqttError. + * @format: The format string literal. + * @...: The arguments for @format. + * + * Emits an #FbMqttError and closes the #FbMqtt. + */ +void +fb_mqtt_error(FbMqtt *mqtt, FbMqttError error, const gchar *format, ...) + G_GNUC_PRINTF(3, 4); + +/** + * fb_mqtt_read: + * @mqtt: The #FbMqtt. + * @msg: The #FbMqttMessage. + * + * Reads an #FbMqttMessage into the #FbMqtt for processing. + */ +void +fb_mqtt_read(FbMqtt *mqtt, FbMqttMessage *msg); + +/** + * fb_mqtt_write: + * @mqtt: The #FbMqtt. + * @msg: The #FbMqttMessage. + * + * Writes an #FbMqttMessage to the wire. + */ +void +fb_mqtt_write(FbMqtt *mqtt, FbMqttMessage *msg); + +/** + * fb_mqtt_open: + * @mqtt: The #FbMqtt. + * @host: The host name. + * @port: The port. + * + * Opens an SSL connection to the remote server. + */ +void +fb_mqtt_open(FbMqtt *mqtt, const gchar *host, gint port); + +/** + * fb_mqtt_connect: + * @mqtt: The #FbMqtt. + * @flags: The #FbMqttConnectFlags. + * @pload: The payload. + * + * Sends a message of type #FB_MQTT_MESSAGE_TYPE_CONNECT. + */ +void +fb_mqtt_connect(FbMqtt *mqtt, guint8 flags, const GByteArray *pload); + +/** + * fb_mqtt_connected: + * @mqtt: The #FbMqtt. + * @error: #TRUE to error with no connection, otherwise #FALSE. + * + * Determines the connection state of the #FbMqtt, and optionally emits + * an error. + * + * Returns: #TRUE if the #FbMqtt is connected, otherwise #FALSE. + */ +gboolean +fb_mqtt_connected(FbMqtt *mqtt, gboolean error); + +/** + * fb_mqtt_disconnect: + * @mqtt: The #FbMqtt. + * + * Sends a message of type #FB_MQTT_MESSAGE_TYPE_DISCONNECT, and closes + * the connection. + */ +void +fb_mqtt_disconnect(FbMqtt *mqtt); + +/** + * fb_mqtt_publish: + * @mqtt: The #FbMqtt. + * @topic: The topic. + * @pload: The payload. + * + * Sends a message of type #FB_MQTT_MESSAGE_TYPE_PUBLISH. + */ +void +fb_mqtt_publish(FbMqtt *mqtt, const gchar *topic, const GByteArray *pload); + +/** + * fb_mqtt_subscribe: + * @mqtt: The #FbMqtt. + * @topic1: The first topic. + * @qos1: The first QoS. + * @...: The %NULL-terminated list of topic/QoS pairs. + * + * Sends a message of type #FB_MQTT_MESSAGE_TYPE_SUBSCRIBE. + */ +void +fb_mqtt_subscribe(FbMqtt *mqtt, const gchar *topic1, guint16 qos1, ...) + G_GNUC_NULL_TERMINATED; + +/** + * fb_mqtt_unsubscribe: + * @mqtt: The #FbMqtt. + * @topic1: The first topic. + * @...: The %NULL-terminated list of topics. + * + * Sends a message of type #FB_MQTT_MESSAGE_TYPE_UNSUBSCRIBE. + */ +void +fb_mqtt_unsubscribe(FbMqtt *mqtt, const gchar *topic1, ...) + G_GNUC_NULL_TERMINATED; + +/** + * fb_mqtt_message_new: + * @type: The #FbMqttMessageType. + * @flags: The #FbMqttMessageFlags. + * + * Creates a new #FbMqttMessage. The returned #FbMqttMessage should be + * freed with #g_object_unref() when no longer needed. + * + * Returns: The new #FbMqttMessage. + */ +FbMqttMessage * +fb_mqtt_message_new(FbMqttMessageType type, FbMqttMessageFlags flags); + +/** + * fb_mqtt_message_new_bytes: + * @bytes: The #GByteArray. + * + * Creates a new #FbMqttMessage from a #GByteArray. The returned + * #FbMqttMessage should be freed with #g_object_unref() when no + * longer needed. + * + * Returns: The new #FbMqttMessage. + */ +FbMqttMessage * +fb_mqtt_message_new_bytes(GByteArray *bytes); + +/** + * fb_mqtt_message_reset: + * @msg: The #FbMqttMessage. + * + * Resets an #FbMqttMessage. This resets the cursor position, and + * removes any sort of fixed header. + */ +void +fb_mqtt_message_reset(FbMqttMessage *msg); + +/** + * fb_mqtt_message_bytes: + * @msg: The #FbMqttMessage. + * + * Formats the internal #GByteArray of the #FbMqttMessage with the + * required fixed header. This resets the cursor position. + * + * Returns: The internal #GByteArray. + */ +const GByteArray * +fb_mqtt_message_bytes(FbMqttMessage *msg); + +/** + * fb_mqtt_message_read: + * @msg: The #FbMqttMessage. + * @data: The data buffer. + * @size: The size of @buffer. + * + * Reads data from the #FbMqttMessage into a buffer. If @data is #NULL, + * this will simply advance the cursor position. + * + * Returns: #TRUE if the data was read, otherwise #FALSE. + */ +gboolean +fb_mqtt_message_read(FbMqttMessage *msg, gpointer data, guint size); + +/** + * fb_mqtt_message_read_r: + * @msg: The #FbMqttMessage. + * @bytes: The #GByteArray. + * + * Reads the remaining data from the #FbMqttMessage into a #GByteArray. + * This is useful for obtaining the payload of a message. + * + * Returns: #TRUE if the data was read, otherwise #FALSE. + */ +gboolean +fb_mqtt_message_read_r(FbMqttMessage *msg, GByteArray *bytes); + +/** + * fb_mqtt_message_read_byte: + * @msg: The #FbMqttMessage. + * @value: The return location for the value or #NULL. + * + * Reads an 8-bit integer value from the #FbMqttMessage. If @value is + * #NULL, this will simply advance the cursor position. + * + * Returns: #TRUE if the value was read, otherwise #FALSE. + */ +gboolean +fb_mqtt_message_read_byte(FbMqttMessage *msg, guint8 *value); + +/** + * fb_mqtt_message_read_mid: + * @msg: The #FbMqttMessage. + * @value: The return location for the value or #NULL. + * + * Reads a message identifier from the #FbMqttMessage. If @value is + * #NULL, this will simply advance the cursor position. + * + * Returns: #TRUE if the value was read, otherwise #FALSE. + */ +gboolean +fb_mqtt_message_read_mid(FbMqttMessage *msg, guint16 *value); + +/** + * fb_mqtt_message_read_u16: + * @msg: The #FbMqttMessage. + * @value: The return location for the value or #NULL. + * + * Reads a 16-bit integer value from the #FbMqttMessage. If @value is + * #NULL, this will simply advance the cursor position. + * + * Returns: #TRUE if the value was read, otherwise #FALSE. + */ +gboolean +fb_mqtt_message_read_u16(FbMqttMessage *msg, guint16 *value); + +/** + * fb_mqtt_message_read_str: + * @msg: The #FbMqttMessage. + * @value: The return location for the value or #NULL. + * + * Reads a string value from the #FbMqttMessage. The value returned to + * @value should be freed with #g_free() when no longer needed. If + * @value is #NULL, this will simply advance the cursor position. + * + * Returns: #TRUE if the value was read, otherwise #FALSE. + */ +gboolean +fb_mqtt_message_read_str(FbMqttMessage *msg, gchar **value); + +/** + * fb_mqtt_message_write: + * @msg: The #FbMqttMessage. + * @data: The data buffer. + * @size: The size of @buffer. + * + * Writes data to the #FbMqttMessage. + */ +void +fb_mqtt_message_write(FbMqttMessage *msg, gconstpointer data, guint size); + +/** + * fb_mqtt_message_write_byte: + * @msg: The #FbMqttMessage. + * @value: The value. + * + * Writes an 8-bit integer value to the #FbMqttMessage. + */ +void +fb_mqtt_message_write_byte(FbMqttMessage *msg, guint8 value); + +/** + * fb_mqtt_message_write_mid: + * @msg: The #FbMqttMessage. + * @value: The value. + * + * Writes a message identifier to the #FbMqttMessage. This increments + * @value for the next message. + */ +void +fb_mqtt_message_write_mid(FbMqttMessage *msg, guint16 *value); + +/** + * fb_mqtt_message_write_u16: + * @msg: The #FbMqttMessage. + * @value: The value. + * + * Writes a 16-bit integer value to the #FbMqttMessage. + */ +void +fb_mqtt_message_write_u16(FbMqttMessage *msg, guint16 value); + +/** + * fb_mqtt_message_write_str: + * @msg: The #FbMqttMessage. + * @value: The value. + * + * Writes a string value to the #FbMqttMessage. + */ +void +fb_mqtt_message_write_str(FbMqttMessage *msg, const gchar *value); + +#endif /* _FACEBOOK_MQTT_H_ */ diff --git a/facebook/thrift.c b/facebook/thrift.c new file mode 100644 index 00000000..9ec7bf35 --- /dev/null +++ b/facebook/thrift.c @@ -0,0 +1,700 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#include + +#include "purple2compat/glibcompat.h" +#include "thrift.h" + +struct _FbThriftPrivate +{ + GByteArray *bytes; + gboolean internal; + guint offset; + guint pos; + guint lastbool; +}; + +G_DEFINE_TYPE_WITH_CODE(FbThrift, fb_thrift, G_TYPE_OBJECT, G_ADD_PRIVATE(FbThrift)); + +static void +fb_thrift_dispose(GObject *obj) +{ + FbThriftPrivate *priv = FB_THRIFT(obj)->priv; + + if (priv->internal) { + g_byte_array_free(priv->bytes, TRUE); + } +} + +static void +fb_thrift_class_init(FbThriftClass *klass) +{ + GObjectClass *gklass = G_OBJECT_CLASS(klass); + + gklass->dispose = fb_thrift_dispose; + g_type_class_add_private(klass, sizeof (FbThriftPrivate)); +} + +static void +fb_thrift_init(FbThrift *thft) +{ + FbThriftPrivate *priv; + + priv = G_TYPE_INSTANCE_GET_PRIVATE(thft, FB_TYPE_THRIFT, + FbThriftPrivate); + thft->priv = priv; +} + +FbThrift * +fb_thrift_new(GByteArray *bytes, guint offset) +{ + FbThrift *thft; + FbThriftPrivate *priv; + + thft = g_object_new(FB_TYPE_THRIFT, NULL); + priv = thft->priv; + + if (bytes != NULL) { + priv->bytes = bytes; + priv->offset = offset; + priv->pos = offset; + } else { + priv->bytes = g_byte_array_new(); + priv->internal = TRUE; + } + + return thft; +} + +const GByteArray * +fb_thrift_get_bytes(FbThrift *thft) +{ + FbThriftPrivate *priv; + + g_return_val_if_fail(FB_IS_THRIFT(thft), NULL); + priv = thft->priv; + return priv->bytes; +} + +guint +fb_thrift_get_pos(FbThrift *thft) +{ + FbThriftPrivate *priv; + + g_return_val_if_fail(FB_IS_THRIFT(thft), 0); + priv = thft->priv; + return priv->pos; +} + +void +fb_thrift_set_pos(FbThrift *thft, guint pos) +{ + FbThriftPrivate *priv; + + g_return_if_fail(FB_IS_THRIFT(thft)); + priv = thft->priv; + priv->pos = pos; +} + +void +fb_thrift_reset(FbThrift *thft) +{ + FbThriftPrivate *priv; + + g_return_if_fail(FB_IS_THRIFT(thft)); + priv = thft->priv; + priv->pos = priv->offset; +} + +gboolean +fb_thrift_read(FbThrift *thft, gpointer data, guint size) +{ + FbThriftPrivate *priv; + + g_return_val_if_fail(FB_IS_THRIFT(thft), FALSE); + priv = thft->priv; + + if ((priv->pos + size) > priv->bytes->len) { + return FALSE; + } + + if ((data != NULL) && (size > 0)) { + memcpy(data, priv->bytes->data + priv->pos, size); + } + + priv->pos += size; + return TRUE; +} + +gboolean +fb_thrift_read_bool(FbThrift *thft, gboolean *value) +{ + FbThriftPrivate *priv; + guint8 byte; + + g_return_val_if_fail(FB_IS_THRIFT(thft), FALSE); + priv = thft->priv; + + if ((priv->lastbool & 0x03) != 0x01) { + if (!fb_thrift_read_byte(thft, &byte)) { + return FALSE; + } + + if (value != NULL) { + *value = (byte & 0x0F) == 0x01; + } + + priv->lastbool = 0; + return TRUE; + } + + if (value != NULL) { + *value = ((priv->lastbool & 0x04) >> 2) != 0; + } + + priv->lastbool = 0; + return TRUE; +} + +gboolean +fb_thrift_read_byte(FbThrift *thft, guint8 *value) +{ + return fb_thrift_read(thft, value, sizeof *value); +} + +gboolean +fb_thrift_read_dbl(FbThrift *thft, gdouble *value) +{ + gint64 i64; + + /* Almost always 8, but check anyways */ + static const gsize size = MIN(sizeof value, sizeof i64); + + if (!fb_thrift_read_i64(thft, &i64)) { + return FALSE; + } + + if (value != NULL) { + memcpy(value, &i64, size); + } + + return TRUE; +} + +gboolean +fb_thrift_read_i16(FbThrift *thft, gint16 *value) +{ + gint64 i64; + + if (!fb_thrift_read_i64(thft, &i64)) { + return FALSE; + } + + if (value != NULL) { + *value = i64; + } + + return TRUE; +} + +gboolean +fb_thrift_read_vi16(FbThrift *thft, guint16 *value) +{ + guint64 u64; + + if (!fb_thrift_read_vi64(thft, &u64)) { + return FALSE; + } + + if (value != NULL) { + *value = u64; + } + + return TRUE; +} + +gboolean +fb_thrift_read_i32(FbThrift *thft, gint32 *value) +{ + gint64 i64; + + if (!fb_thrift_read_i64(thft, &i64)) { + return FALSE; + } + + if (value != NULL) { + *value = i64; + } + + return TRUE; +} + +gboolean +fb_thrift_read_vi32(FbThrift *thft, guint32 *value) +{ + guint64 u64; + + if (!fb_thrift_read_vi64(thft, &u64)) { + return FALSE; + } + + if (value != NULL) { + *value = u64; + } + + return TRUE; +} + +gboolean +fb_thrift_read_i64(FbThrift *thft, gint64 *value) +{ + guint64 u64; + + if (!fb_thrift_read_vi64(thft, &u64)) { + return FALSE; + } + + if (value != NULL) { + /* Convert from zigzag to integer */ + *value = (u64 >> 0x01) ^ -(u64 & 0x01); + } + + return TRUE; +} + +gboolean +fb_thrift_read_vi64(FbThrift *thft, guint64 *value) +{ + guint i = 0; + guint8 byte; + guint64 u64 = 0; + + do { + if (!fb_thrift_read_byte(thft, &byte)) { + return FALSE; + } + + u64 |= ((guint64) (byte & 0x7F)) << i; + i += 7; + } while ((byte & 0x80) == 0x80); + + if (value != NULL) { + *value = u64; + } + + return TRUE; +} + +gboolean +fb_thrift_read_str(FbThrift *thft, gchar **value) +{ + guint8 *data; + guint32 size; + + if (!fb_thrift_read_vi32(thft, &size)) { + return FALSE; + } + + if (value != NULL) { + data = g_new(guint8, size + 1); + data[size] = 0; + } else { + data = NULL; + } + + if (!fb_thrift_read(thft, data, size)) { + g_free(data); + return FALSE; + } + + if (value != NULL) { + *value = (gchar *) data; + } + + return TRUE; +} + +gboolean +fb_thrift_read_field(FbThrift *thft, FbThriftType *type, gint16 *id, + gint16 lastid) +{ + FbThriftPrivate *priv; + gint16 i16; + guint8 byte; + + g_return_val_if_fail(FB_IS_THRIFT(thft), FALSE); + g_return_val_if_fail(type != NULL, FALSE); + g_return_val_if_fail(id != NULL, FALSE); + priv = thft->priv; + + if (!fb_thrift_read_byte(thft, &byte)) { + return FALSE; + } + + if (byte == FB_THRIFT_TYPE_STOP) { + *type = FB_THRIFT_TYPE_STOP; + return FALSE; + } + + *type = fb_thrift_ct2t(byte & 0x0F); + i16 = (byte & 0xF0) >> 4; + + if (i16 == 0) { + if (!fb_thrift_read_i16(thft, id)) { + return FALSE; + } + } else { + *id = lastid + i16; + } + + if (*type == FB_THRIFT_TYPE_BOOL) { + priv->lastbool = 0x01; + + if ((byte & 0x0F) == 0x01) { + priv->lastbool |= 0x01 << 2; + } + } + + return TRUE; +} + +gboolean +fb_thrift_read_stop(FbThrift *thft) +{ + guint8 byte; + + return fb_thrift_read_byte(thft, &byte) && + (byte == FB_THRIFT_TYPE_STOP); +} + +gboolean +fb_thrift_read_isstop(FbThrift *thft) +{ + FbThriftPrivate *priv; + guint8 byte; + + g_return_val_if_fail(FB_IS_THRIFT(thft), FALSE); + priv = thft->priv; + + if (!fb_thrift_read_byte(thft, &byte)) { + return FALSE; + } + + priv->pos--; + return byte == FB_THRIFT_TYPE_STOP; +} + +gboolean +fb_thrift_read_list(FbThrift *thft, FbThriftType *type, guint *size) +{ + guint8 byte; + guint32 u32; + + g_return_val_if_fail(type != NULL, FALSE); + g_return_val_if_fail(size != NULL, FALSE); + + if (!fb_thrift_read_byte(thft, &byte)) { + return FALSE; + } + + *type = fb_thrift_ct2t(byte & 0x0F); + *size = (byte & 0xF0) >> 4; + + if (*size == 0x0F) { + if (!fb_thrift_read_vi32(thft, &u32)) { + return FALSE; + } + + *size = u32; + } + + return TRUE; +} + +gboolean +fb_thrift_read_map(FbThrift *thft, FbThriftType *ktype, FbThriftType *vtype, + guint *size) +{ + gint32 i32; + guint8 byte; + + g_return_val_if_fail(ktype != NULL, FALSE); + g_return_val_if_fail(vtype != NULL, FALSE); + g_return_val_if_fail(size != NULL, FALSE); + + if (!fb_thrift_read_i32(thft, &i32)) { + return FALSE; + } + + if (i32 != 0) { + if (!fb_thrift_read_byte(thft, &byte)) { + return FALSE; + } + + *ktype = fb_thrift_ct2t((byte & 0xF0) >> 4); + *vtype = fb_thrift_ct2t(byte & 0x0F); + } else { + *ktype = 0; + *vtype = 0; + } + + *size = i32; + return TRUE; +} + +gboolean +fb_thrift_read_set(FbThrift *thft, FbThriftType *type, guint *size) +{ + return fb_thrift_read_list(thft, type, size); +} + +void +fb_thrift_write(FbThrift *thft, gconstpointer data, guint size) +{ + FbThriftPrivate *priv; + + g_return_if_fail(FB_IS_THRIFT(thft)); + priv = thft->priv; + + g_byte_array_append(priv->bytes, data, size); + priv->pos += size; +} + +void +fb_thrift_write_bool(FbThrift *thft, gboolean value) +{ + FbThriftPrivate *priv; + guint pos; + + g_return_if_fail(FB_IS_THRIFT(thft)); + priv = thft->priv; + + if ((priv->lastbool & 0x03) != 0x02) { + fb_thrift_write_byte(thft, value ? 0x01 : 0x02); + return; + } + + pos = priv->lastbool >> 3; + priv->lastbool = 0; + + if ((pos >= priv->offset) && (pos < priv->bytes->len)) { + priv->bytes->data[pos] &= ~0x0F; + priv->bytes->data[pos] |= value ? 0x01 : 0x02; + } +} + +void +fb_thrift_write_byte(FbThrift *thft, guint8 value) +{ + fb_thrift_write(thft, &value, sizeof value); +} + +void +fb_thrift_write_dbl(FbThrift *thft, gdouble value) +{ + gint64 i64; + + /* Almost always 8, but check anyways */ + static const gsize size = MIN(sizeof value, sizeof i64); + + memcpy(&i64, &value, size); + fb_thrift_write_i64(thft, i64); +} + +void +fb_thrift_write_i16(FbThrift *thft, gint16 value) +{ + fb_thrift_write_i64(thft, value); +} + +void +fb_thrift_write_vi16(FbThrift *thft, guint16 value) +{ + fb_thrift_write_vi64(thft, value); +} + +void +fb_thrift_write_i32(FbThrift *thft, gint32 value) +{ + value = (value << 1) ^ (value >> 31); + fb_thrift_write_vi64(thft, value); +} + +void +fb_thrift_write_vi32(FbThrift *thft, guint32 value) +{ + fb_thrift_write_vi64(thft, value); +} + +void +fb_thrift_write_i64(FbThrift *thft, gint64 value) +{ + value = (value << 1) ^ (value >> 63); + fb_thrift_write_vi64(thft, value); +} + +void +fb_thrift_write_vi64(FbThrift *thft, guint64 value) +{ + gboolean last; + guint8 byte; + + do { + last = (value & ~0x7F) == 0; + byte = value & 0x7F; + + if (!last) { + byte |= 0x80; + value >>= 7; + } + + fb_thrift_write_byte(thft, byte); + } while (!last); +} + +void +fb_thrift_write_str(FbThrift *thft, const gchar *value) +{ + guint32 size; + + g_return_if_fail(value != NULL); + + size = strlen(value); + fb_thrift_write_vi32(thft, size); + fb_thrift_write(thft, value, size); +} + +void +fb_thrift_write_field(FbThrift *thft, FbThriftType type, gint16 id, + gint16 lastid) +{ + FbThriftPrivate *priv; + gint16 diff; + + g_return_if_fail(FB_IS_THRIFT(thft)); + priv = thft->priv; + + if (type == FB_THRIFT_TYPE_BOOL) { + priv->lastbool = (priv->pos << 3) | 0x02; + } + + type = fb_thrift_t2ct(type); + diff = id - lastid; + + if ((id <= lastid) || (diff > 0x0F)) { + fb_thrift_write_byte(thft, type); + fb_thrift_write_i16(thft, id); + } else { + fb_thrift_write_byte(thft, (diff << 4) | type); + } +} + +void +fb_thrift_write_stop(FbThrift *thft) +{ + fb_thrift_write_byte(thft, FB_THRIFT_TYPE_STOP); +} + +void +fb_thrift_write_list(FbThrift *thft, FbThriftType type, guint size) +{ + type = fb_thrift_t2ct(type); + + if (size <= 14) { + fb_thrift_write_byte(thft, (size << 4) | type); + return; + } + + fb_thrift_write_vi32(thft, size); + fb_thrift_write_byte(thft, 0xF0 | type); +} + +void +fb_thrift_write_map(FbThrift *thft, FbThriftType ktype, FbThriftType vtype, + guint size) +{ + if (size == 0) { + fb_thrift_write_byte(thft, 0); + return; + } + + ktype = fb_thrift_t2ct(ktype); + vtype = fb_thrift_t2ct(vtype); + + fb_thrift_write_vi32(thft, size); + fb_thrift_write_byte(thft, (ktype << 4) | vtype); +} + +void +fb_thrift_write_set(FbThrift *thft, FbThriftType type, guint size) +{ + fb_thrift_write_list(thft, type, size); +} + +guint8 +fb_thrift_t2ct(FbThriftType type) +{ + static const guint8 types[] = { + [FB_THRIFT_TYPE_STOP] = 0, + [FB_THRIFT_TYPE_VOID] = 0, + [FB_THRIFT_TYPE_BOOL] = 2, + [FB_THRIFT_TYPE_BYTE] = 3, + [FB_THRIFT_TYPE_DOUBLE] = 7, + [5] = 0, + [FB_THRIFT_TYPE_I16] = 4, + [7] = 0, + [FB_THRIFT_TYPE_I32] = 5, + [9] = 0, + [FB_THRIFT_TYPE_I64] = 6, + [FB_THRIFT_TYPE_STRING] = 8, + [FB_THRIFT_TYPE_STRUCT] = 12, + [FB_THRIFT_TYPE_MAP] = 11, + [FB_THRIFT_TYPE_SET] = 10, + [FB_THRIFT_TYPE_LIST] = 9 + }; + + g_return_val_if_fail(type < G_N_ELEMENTS(types), 0); + return types[type]; +} + +FbThriftType +fb_thrift_ct2t(guint8 type) +{ + static const guint8 types[] = { + [0] = FB_THRIFT_TYPE_STOP, + [1] = FB_THRIFT_TYPE_BOOL, + [2] = FB_THRIFT_TYPE_BOOL, + [3] = FB_THRIFT_TYPE_BYTE, + [4] = FB_THRIFT_TYPE_I16, + [5] = FB_THRIFT_TYPE_I32, + [6] = FB_THRIFT_TYPE_I64, + [7] = FB_THRIFT_TYPE_DOUBLE, + [8] = FB_THRIFT_TYPE_STRING, + [9] = FB_THRIFT_TYPE_LIST, + [10] = FB_THRIFT_TYPE_SET, + [11] = FB_THRIFT_TYPE_MAP, + [12] = FB_THRIFT_TYPE_STRUCT + }; + + g_return_val_if_fail(type < G_N_ELEMENTS(types), 0); + return types[type]; +} diff --git a/facebook/thrift.h b/facebook/thrift.h new file mode 100644 index 00000000..0da34cac --- /dev/null +++ b/facebook/thrift.h @@ -0,0 +1,604 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#ifndef _FACEBOOK_THRIFT_H_ +#define _FACEBOOK_THRIFT_H_ + +/** + * SECTION:thrift + * @section_id: facebook-thrift + * @short_description: thrift.h + * @title: Thrift Reader/Writer + * + * The Thrift reader/writer. + */ + +#include +#include + +#define FB_TYPE_THRIFT (fb_thrift_get_type()) +#define FB_THRIFT(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), FB_TYPE_THRIFT, FbThrift)) +#define FB_THRIFT_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), FB_TYPE_THRIFT, FbThriftClass)) +#define FB_IS_THRIFT(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), FB_TYPE_THRIFT)) +#define FB_IS_THRIFT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), FB_TYPE_THRIFT)) +#define FB_THRIFT_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), FB_TYPE_THRIFT, FbThriftClass)) + +typedef struct _FbThrift FbThrift; +typedef struct _FbThriftClass FbThriftClass; +typedef struct _FbThriftPrivate FbThriftPrivate; + +/** + * FbThriftType: + * @FB_THRIFT_TYPE_STOP: A stopper for certain types. + * @FB_THRIFT_TYPE_VOID: A void or empty value. + * @FB_THRIFT_TYPE_BOOL: A boolean (#TRUE or #FALSE). + * @FB_THRIFT_TYPE_BYTE: A signed 8-bit integer. + * @FB_THRIFT_TYPE_DOUBLE: A 64-bit floating point number. + * @FB_THRIFT_TYPE_I16: A signed 16-bit integer. + * @FB_THRIFT_TYPE_I32: A signed 32-bit integer. + * @FB_THRIFT_TYPE_I64: A signed 64-bit integer. + * @FB_THRIFT_TYPE_STRING: A UTF-8 encoded string. + * @FB_THRIFT_TYPE_STRUCT: A set of typed fields. + * @FB_THRIFT_TYPE_MAP: A map of unique keys to values. + * @FB_THRIFT_TYPE_SET: A unique set of values. + * @FB_THRIFT_TYPE_LIST: A ordered list of values. + * @FB_THRIFT_TYPE_ENUM: A 32-bit enumerated list. + * @FB_THRIFT_TYPE_UNKNOWN: An unknown type. + * + * The Thrift data types. + */ +typedef enum +{ + FB_THRIFT_TYPE_STOP = 0, + FB_THRIFT_TYPE_VOID = 1, + FB_THRIFT_TYPE_BOOL = 2, + FB_THRIFT_TYPE_BYTE = 3, + FB_THRIFT_TYPE_DOUBLE = 4, + FB_THRIFT_TYPE_I16 = 6, + FB_THRIFT_TYPE_I32 = 8, + FB_THRIFT_TYPE_I64 = 10, + FB_THRIFT_TYPE_STRING = 11, + FB_THRIFT_TYPE_STRUCT = 12, + FB_THRIFT_TYPE_MAP = 13, + FB_THRIFT_TYPE_SET = 14, + FB_THRIFT_TYPE_LIST = 15, + FB_THRIFT_TYPE_ENUM = 16, + + FB_THRIFT_TYPE_UNKNOWN +} FbThriftType; + +/** + * FbThrift: + * + * Represents a reader/writer for compact Thrift data. + */ +struct _FbThrift +{ + /*< private >*/ + GObject parent; + FbThriftPrivate *priv; +}; + +/** + * FbThriftClass: + * + * The base class for all #FbThrift's. + */ +struct _FbThriftClass +{ + /*< private >*/ + GObjectClass parent_class; +}; + +/** + * fb_thrift_get_type: + * + * Returns: The #GType for an #FbThrift. + */ +GType +fb_thrift_get_type(void); + +/** + * fb_thrift_new: + * @bytes: The #GByteArray to read or write. + * @offset: The offset in bytes of the data in @bytes. + * + * Creates a new #FbThrift. The returned #FbThrift should be freed with + * #g_object_unref() when no longer needed. This will optionally use a + * #GByteArray at an offset, rather than a newly created and internal + * #GByteArray. + * + * Returns: The new #FbThrift. + */ +FbThrift * +fb_thrift_new(GByteArray *bytes, guint offset); + +/** + * fb_thrift_get_bytes: + * @thft: The #FbThrift. + * + * Gets the underlying #GByteArray of an #FbThrift. + * + * Returns: The #GByteArray. + */ +const GByteArray * +fb_thrift_get_bytes(FbThrift *thft); + +/** + * fb_thrift_get_pos: + * @thft: The #FbThrift. + * + * Gets the cursor position of an #FbThrift. + * + * Returns: The cursor position. + */ +guint +fb_thrift_get_pos(FbThrift *thft); + +/** + * fb_thrift_set_pos: + * @thft: The #FbThrift. + * @pos: The position. + * + * Sets the cursor position of an #FbThrift. + * + * Returns: The #GByteArray. + */ +void +fb_thrift_set_pos(FbThrift *thft, guint pos); + +/** + * fb_thrift_reset: + * @thft: The #FbThrift. + * + * Resets the cursor position of an #FbThrift. + * + * Returns: The #GByteArray. + */ +void +fb_thrift_reset(FbThrift *thft); + +/** + * fb_thrift_read: + * @thft: The #FbThrift. + * @data: The data buffer. + * @size: The size of @buffer. + * + * Reads data from the #FbThrift into a buffer. If @data is #NULL, this + * will simply advance the cursor position. + * + * Returns: #TRUE if the data was read, otherwise #FALSE. + */ +gboolean +fb_thrift_read(FbThrift *thft, gpointer data, guint size); + +/** + * fb_thrift_read_bool: + * @thft: The #FbThrift. + * @value: The return location for the value or #NULL. + * + * Reads a boolean value from the #FbThrift. If @value is #NULL, this + * will simply advance the cursor position. + * + * Returns: #TRUE if the value was read, otherwise #FALSE. + */ +gboolean +fb_thrift_read_bool(FbThrift *thft, gboolean *value); + +/** + * fb_thrift_read_byte: + * @thft: The #FbThrift. + * @value: The return location for the value or #NULL. + * + * Reads an 8-bit integer value from the #FbThrift. If @value is #NULL, + * this will simply advance the cursor position. + * + * Returns: #TRUE if the value was read, otherwise #FALSE. + */ +gboolean +fb_thrift_read_byte(FbThrift *thft, guint8 *value); + +/** + * fb_thrift_read_dbl: + * @thft: The #FbThrift. + * @value: The return location for the value or #NULL. + * + * Reads a 64-bit floating point value from the #FbThrift. If @value + * is #NULL, this will simply advance the cursor position. + * + * Returns: #TRUE if the value was read, otherwise #FALSE. + */ +gboolean +fb_thrift_read_dbl(FbThrift *thft, gdouble *value); + +/** + * fb_thrift_read_i16: + * @thft: The #FbThrift. + * @value: The return location for the value or #NULL. + * + * Reads a signed 16-bit integer value from the #FbThrift. This will + * convert the integer from the zig-zag format. If @value is #NULL, + * this will simply advance the cursor position. + * + * Returns: #TRUE if the value was read, otherwise #FALSE. + */ +gboolean +fb_thrift_read_i16(FbThrift *thft, gint16 *value); + +/** + * fb_thrift_read_vi16: + * @thft: The #FbThrift. + * @value: The return location for the value or #NULL. + * + * Reads a 16-bit integer value from the #FbThrift. This reads the raw + * integer value without converting it from the zig-zag format. If + * @value is #NULL, this will simply advance the cursor position. + * + * Returns: #TRUE if the value was read, otherwise #FALSE. + */ +gboolean +fb_thrift_read_vi16(FbThrift *thft, guint16 *value); + +/** + * fb_thrift_read_i32: + * @thft: The #FbThrift. + * @value: The return location for the value or #NULL. + * + * Reads a signed 32-bit integer value from the #FbThrift. This will + * convert the integer from the zig-zag format. If @value is #NULL, + * this will simply advance the cursor position. + * + * Returns: #TRUE if the value was read, otherwise #FALSE. + */ +gboolean +fb_thrift_read_i32(FbThrift *thft, gint32 *value); + +/** + * fb_thrift_read_vi32: + * @thft: The #FbThrift. + * @value: The return location for the value or #NULL. + * + * Reads a 32-bit integer value from the #FbThrift. This reads the raw + * integer value without converting it from the zig-zag format. If + * @value is #NULL, this will simply advance the cursor position. + * + * Returns: #TRUE if the value was read, otherwise #FALSE. + */ +gboolean +fb_thrift_read_vi32(FbThrift *thft, guint32 *value); + +/** + * fb_thrift_read_i64: + * @thft: The #FbThrift. + * @value: The return location for the value or #NULL. + * + * Reads a signed 64-bit integer value from the #FbThrift. This will + * convert the integer from the zig-zag format. If @value is #NULL, + * this will simply advance the cursor position. + * + * Returns: #TRUE if the value was read, otherwise #FALSE. + */ +gboolean +fb_thrift_read_i64(FbThrift *thft, gint64 *value); + +/** + * fb_thrift_read_vi64: + * @thft: The #FbThrift. + * @value: The return location for the value or #NULL. + * + * Reads a 64-bit integer value from the #FbThrift. This reads the raw + * integer value without converting it from the zig-zag format. If + * @value is #NULL, this will simply advance the cursor position. + * + * Returns: #TRUE if the value was read, otherwise #FALSE. + */ +gboolean +fb_thrift_read_vi64(FbThrift *thft, guint64 *value); + +/** + * fb_thrift_read_str: + * @thft: The #FbThrift. + * @value: The return location for the value or #NULL. + * + * Reads a string value from the #FbThrift. The value returned to + * @value should be freed with #g_free() when no longer needed. If + * @value is #NULL, this will simply advance the cursor position. + * + * Returns: #TRUE if the value was read, otherwise #FALSE. + */ +gboolean +fb_thrift_read_str(FbThrift *thft, gchar **value); + +/** + * fb_thrift_read_field: + * @thft: The #FbThrift. + * @type: The return location for the #FbThriftType. + * @id: The return location for the identifier. + * @lastid: The identifier of the previous field. + * + * Reads a field header from the #FbThrift. + * + * Returns: #TRUE if the field header was read, otherwise #FALSE. + */ +gboolean +fb_thrift_read_field(FbThrift *thft, FbThriftType *type, gint16 *id, + gint16 lastid); + +/** + * fb_thrift_read_stop: + * @thft: The #FbThrift. + * + * Reads a field stop from the #FbThrift. + * + * Returns: #TRUE if the field stop was read, otherwise #FALSE. + */ +gboolean +fb_thrift_read_stop(FbThrift *thft); + +/** + * fb_thrift_read_isstop: + * @thft: The #FbThrift. + * + * Determines if the next byte of the #FbThrift is a field stop. + * + * Returns: #TRUE if the next byte is a field stop, otherwise #FALSE. + */ +gboolean +fb_thrift_read_isstop(FbThrift *thft); + +/** + * fb_thrift_read_list: + * @thft: The #FbThrift. + * @type: The return location for the #FbThriftType. + * @size: The return location for the size. + * + * Reads a list header from the #FbThrift. + * + * Returns: #TRUE if the list header was read, otherwise #FALSE. + */ +gboolean +fb_thrift_read_list(FbThrift *thft, FbThriftType *type, guint *size); + +/** + * fb_thrift_read_map: + * @thft: The #FbThrift. + * @ktype: The return location for the key #FbThriftType. + * @vtype: The return location for the value #FbThriftType. + * @size: The return location for the size. + * + * Reads a map header from the #FbThrift. + * + * Returns: #TRUE if the map header was read, otherwise #FALSE. + */ +gboolean +fb_thrift_read_map(FbThrift *thft, FbThriftType *ktype, FbThriftType *vtype, + guint *size); + +/** + * fb_thrift_read_set: + * @thft: The #FbThrift. + * @type: The return location for the #FbThriftType. + * @size: The return location for the size. + * + * Reads a set header from the #FbThrift. + * + * Returns: #TRUE if the set header was read, otherwise #FALSE. + */ +gboolean +fb_thrift_read_set(FbThrift *thft, FbThriftType *type, guint *size); + +/** + * fb_thrift_write: + * @thft: The #FbThrift. + * @data: The data buffer. + * @size: The size of @buffer. + * + * Writes data to the #FbThrift. + */ +void +fb_thrift_write(FbThrift *thft, gconstpointer data, guint size); + +/** + * fb_thrift_write_bool: + * @thft: The #FbThrift. + * @value: The value. + * + * Writes a boolean value to the #FbThrift. + */ +void +fb_thrift_write_bool(FbThrift *thft, gboolean value); + +/** + * fb_thrift_write_byte: + * @thft: The #FbThrift. + * @value: The value. + * + * Writes an 8-bit integer value to the #FbThrift. + */ +void +fb_thrift_write_byte(FbThrift *thft, guint8 value); + +/** + * fb_thrift_write_dbl: + * @thft: The #FbThrift. + * @value: The value. + * + * Writes a 64-bit floating point value to the #FbThrift. + */ +void +fb_thrift_write_dbl(FbThrift *thft, gdouble value); + +/** + * fb_thrift_write_i16: + * @thft: The #FbThrift. + * @value: The value. + * + * Writes a signed 16-bit integer value to the #FbThrift. This will + * convert the integer to the zig-zag format. + */ +void +fb_thrift_write_i16(FbThrift *thft, gint16 value); + +/** + * fb_thrift_write_vi16: + * @thft: The #FbThrift. + * @value: The value. + * + * Writes a 16-bit integer value to the #FbThrift. This writes the raw + * integer value without converting it to the zig-zag format. + */ +void +fb_thrift_write_vi16(FbThrift *thft, guint16 value); + +/** + * fb_thrift_write_i32: + * @thft: The #FbThrift. + * @value: The value. + * + * Writes a signed 32-bit integer value to the #FbThrift. This will + * convert the integer to the zig-zag format. + */ +void +fb_thrift_write_i32(FbThrift *thft, gint32 value); + +/** + * fb_thrift_write_vi32: + * @thft: The #FbThrift. + * @value: The value. + * + * Writes a 32-bit integer value to the #FbThrift. This writes the raw + * integer value without converting it to the zig-zag format. + */ +void +fb_thrift_write_vi32(FbThrift *thft, guint32 value); + +/** + * fb_thrift_write_i64: + * @thft: The #FbThrift. + * @value: The value. + * + * Writes a signed 64-bit integer value to the #FbThrift. This will + * convert the integer to the zig-zag format. + */ +void +fb_thrift_write_i64(FbThrift *thft, gint64 value); + +/** + * fb_thrift_write_vi64: + * @thft: The #FbThrift. + * @value: The value. + * + * Writes a 64-bit integer value to the #FbThrift. This writes the raw + * integer value without converting it to the zig-zag format. + */ +void +fb_thrift_write_vi64(FbThrift *thft, guint64 value); + +/** + * fb_thrift_write_str: + * @thft: The #FbThrift. + * @value: The value. + * + * Writes a string value to the #FbThrift. + */ +void +fb_thrift_write_str(FbThrift *thft, const gchar *value); + +/** + * fb_thrift_write_field: + * @thft: The #FbThrift. + * @type: The #FbThriftType. + * @id: The identifier. + * @lastid: The identifier of the previous field. + * + * Writes a field header to the #FbThrift. + */ +void +fb_thrift_write_field(FbThrift *thft, FbThriftType type, gint16 id, + gint16 lastid); + +/** + * fb_thrift_write_stop: + * @thft: The #FbThrift. + * + * Writes a field stop to the #FbThrift. + */ +void +fb_thrift_write_stop(FbThrift *thft); + +/** + * fb_thrift_write_list: + * @thft: The #FbThrift. + * @type: The #FbThriftType. + * @size: The size. + * + * Writes a list header to the #FbThrift. + */ +void +fb_thrift_write_list(FbThrift *thft, FbThriftType type, guint size); + +/** + * fb_thrift_write_map: + * @thft: The #FbThrift. + * @ktype: The key #FbThriftType. + * @vtype: The value #FbThriftType. + * @size: The size. + * + * Writes a map header to the #FbThrift. + */ +void +fb_thrift_write_map(FbThrift *thft, FbThriftType ktype, FbThriftType vtype, + guint size); + +/** + * fb_thrift_write_set: + * @thft: The #FbThrift. + * @type: The #FbThriftType. + * @size: The size. + * + * Writes a set header to the #FbThrift. + */ +void +fb_thrift_write_set(FbThrift *thft, FbThriftType type, guint size); + +/** + * fb_thrift_t2ct: + * @type: The #FbThriftType. + * + * Converts a #FbThriftType to a compact type. + * + * Return: The equivalent compact type. + */ +guint8 +fb_thrift_t2ct(FbThriftType type); + +/** + * fb_thrift_ct2t: + * @type: The compact type. + * + * Converts a compact type to an #FbThriftType. + * + * Return: The equivalent #FbThriftType. + */ +FbThriftType +fb_thrift_ct2t(guint8 type); + +#endif /* _FACEBOOK_THRIFT_H_ */ diff --git a/facebook/util.c b/facebook/util.c new file mode 100644 index 00000000..bc4eea97 --- /dev/null +++ b/facebook/util.c @@ -0,0 +1,566 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#include "internal.h" + +#include +#include +#include + +#include "buddylist.h" +#include "conversations.h" +#include "purple2compat/glibcompat.h" +#include "message.h" +#include "request.h" +#include "server.h" + +#include "util.h" + +GQuark +fb_util_error_quark(void) +{ + static GQuark q = 0; + + if (G_UNLIKELY(q == 0)) { + q = g_quark_from_static_string("fb-util-error-quark"); + } + + return q; +} + +PurpleBuddy * +fb_util_account_find_buddy(PurpleAccount *acct, PurpleChatConversation *chat, + const gchar *search, GError **error) +{ + const gchar *alias; + const gchar *name; + GSList *buddies; + GSList *l; + guint retc; + PurpleBuddy *ret = NULL; + + g_return_val_if_fail(PURPLE_IS_ACCOUNT(acct), NULL); + g_return_val_if_fail(search != NULL, NULL); + + buddies = purple_blist_find_buddies(acct, NULL); + + for (retc = 0, l = buddies; l != NULL; l = l->next) { + name = purple_buddy_get_name(l->data); + alias = purple_buddy_get_alias(l->data); + + if ((chat != NULL) && + !purple_chat_conversation_has_user(chat, name)) + { + continue; + } + + if (g_ascii_strcasecmp(name, search) == 0) { + ret = l->data; + retc++; + } + + if (g_ascii_strcasecmp(alias, search) == 0) { + ret = l->data; + retc++; + } + } + + if (retc == 0) { + g_set_error(error, FB_UTIL_ERROR, FB_UTIL_ERROR_GENERAL, + _("Buddy %s not found"), search); + } else if (retc > 1) { + g_set_error(error, FB_UTIL_ERROR, FB_UTIL_ERROR_GENERAL, + _("Buddy name %s is ambiguous"), search); + ret = NULL; + } + + g_slist_free(buddies); + return ret; +} + +void +fb_util_debug(PurpleDebugLevel level, const gchar *format, ...) +{ + va_list ap; + + va_start(ap, format); + fb_util_vdebug(level, format, ap); + va_end(ap); +} + +void +fb_util_vdebug(PurpleDebugLevel level, const gchar *format, va_list ap) +{ + gboolean unsafe; + gboolean verbose; + gchar *str; + + g_return_if_fail(format != NULL); + + unsafe = (level & FB_UTIL_DEBUG_FLAG_UNSAFE) != 0; + verbose = (level & FB_UTIL_DEBUG_FLAG_VERBOSE) != 0; + + if ((unsafe && !purple_debug_is_unsafe()) || + (verbose && !purple_debug_is_verbose())) + { + return; + } + + /* Ensure all local flags are removed */ + level &= ~FB_UTIL_DEBUG_FLAG_ALL; + + str = g_strdup_vprintf(format, ap); + purple_debug(level, "facebook", "%s\n", str); + g_free(str); +} + +void +fb_util_debug_misc(const gchar *format, ...) +{ + va_list ap; + + va_start(ap, format); + fb_util_vdebug(PURPLE_DEBUG_MISC, format, ap); + va_end(ap); +} + +void +fb_util_debug_info(const gchar *format, ...) +{ + va_list ap; + + va_start(ap, format); + fb_util_vdebug(PURPLE_DEBUG_INFO, format, ap); + va_end(ap); +} + +void +fb_util_debug_warning(const gchar *format, ...) +{ + va_list ap; + + va_start(ap, format); + fb_util_vdebug(PURPLE_DEBUG_WARNING, format, ap); + va_end(ap); +} + +void +fb_util_debug_error(const gchar *format, ...) +{ + va_list ap; + + va_start(ap, format); + fb_util_vdebug(PURPLE_DEBUG_ERROR, format, ap); + va_end(ap); +} + +void +fb_util_debug_fatal(const gchar *format, ...) +{ + va_list ap; + + va_start(ap, format); + fb_util_vdebug(PURPLE_DEBUG_FATAL, format, ap); + va_end(ap); +} + +void +fb_util_debug_hexdump(PurpleDebugLevel level, const GByteArray *bytes, + const gchar *format, ...) +{ + gchar c; + guint i; + guint j; + GString *gstr; + va_list ap; + + static const gchar *indent = " "; + + g_return_if_fail(bytes != NULL); + + if (format != NULL) { + va_start(ap, format); + fb_util_vdebug(level, format, ap); + va_end(ap); + } + + gstr = g_string_sized_new(80); + + for (i = 0; i < bytes->len; i += 16) { + g_string_append_printf(gstr, "%s%08x ", indent, i); + + for (j = 0; j < 16; j++) { + if ((i + j) < bytes->len) { + g_string_append_printf(gstr, "%02x ", + bytes->data[i + j]); + } else { + g_string_append(gstr, " "); + } + + if (j == 7) { + g_string_append_c(gstr, ' '); + } + } + + g_string_append(gstr, " |"); + + for (j = 0; (j < 16) && ((i + j) < bytes->len); j++) { + c = bytes->data[i + j]; + + if (!g_ascii_isprint(c) || g_ascii_isspace(c)) { + c = '.'; + } + + g_string_append_c(gstr, c); + } + + g_string_append_c(gstr, '|'); + fb_util_debug(level, "%s", gstr->str); + g_string_erase(gstr, 0, -1); + } + + g_string_append_printf(gstr, "%s%08x", indent, i); + fb_util_debug(level, "%s", gstr->str); + g_string_free(gstr, TRUE); +} + +gchar * +fb_util_get_locale(void) +{ + const gchar * const *langs; + const gchar *lang; + gchar *chr; + guint i; + + static const gchar chrs[] = {'.', '@'}; + + langs = g_get_language_names(); + lang = langs[0]; + + if (purple_strequal(lang, "C")) { + return g_strdup("en_US"); + } + + for (i = 0; i < G_N_ELEMENTS(chrs); i++) { + chr = strchr(lang, chrs[i]); + + if (chr != NULL) { + return g_strndup(lang, chr - lang); + } + } + + return g_strdup(lang); +} + +gchar * +fb_util_rand_alnum(guint len) +{ + gchar *ret; + GRand *rand; + guint i; + guint j; + + static const gchar chars[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789"; + static const gsize charc = G_N_ELEMENTS(chars) - 1; + + g_return_val_if_fail(len > 0, NULL); + rand = g_rand_new(); + ret = g_new(gchar, len + 1); + + for (i = 0; i < len; i++) { + j = g_rand_int_range(rand, 0, charc); + ret[i] = chars[j]; + } + + ret[len] = 0; + g_rand_free(rand); + return ret; +} + +static void +fb_util_request_buddy_ok(gpointer *mata, PurpleRequestFields *fields) +{ + FbUtilRequestBuddyFunc func = mata[0]; + GList *l; + GList *select; + gpointer data = mata[2]; + GSList *ret = NULL; + PurpleBuddy *bdy; + PurpleRequestField *field; + + if (func == NULL) { + g_free(mata); + return; + } + + field = purple_request_fields_get_field(fields, "buddy"); + select = purple_request_field_list_get_selected(field); + + for (l = select; l != NULL; l = l->next) { + bdy = purple_request_field_list_get_data(field, l->data); + ret = g_slist_prepend(ret, bdy); + } + + ret = g_slist_reverse(ret); + func(ret, data); + + g_slist_free(ret); + g_free(mata); +} + +static void +fb_util_request_buddy_cancel(gpointer *mata, PurpleRequestFields *fields) +{ + FbUtilRequestBuddyFunc func = mata[1]; + gpointer data = mata[2]; + + if (func != NULL) { + func(NULL, data); + } + + g_free(mata); +} + +gpointer +fb_util_request_buddy(PurpleConnection *gc, const gchar *title, + const gchar *primary, const gchar *secondary, + GSList *select, gboolean multi, GCallback ok_cb, + GCallback cancel_cb, gpointer data) +{ + const gchar *alias; + const gchar *name; + gchar *str; + GList *items = NULL; + gpointer *mata; + GSList *buddies; + GSList *l; + PurpleAccount *acct; + PurpleRequestCommonParameters *cpar; + PurpleRequestField *field; + PurpleRequestFieldGroup *group; + PurpleRequestFields *fields; + + mata = g_new0(gpointer, 3); + mata[0] = ok_cb; + mata[1] = cancel_cb; + mata[2] = data; + + acct = purple_connection_get_account(gc); + buddies = purple_blist_find_buddies(acct, NULL); + buddies = g_slist_sort(buddies, (GCompareFunc) g_ascii_strcasecmp); + + fields = purple_request_fields_new(); + group = purple_request_field_group_new(NULL); + purple_request_fields_add_group(fields, group); + + field = purple_request_field_list_new("buddy", NULL); + purple_request_field_list_set_multi_select(field, multi); + purple_request_field_set_required(field, TRUE); + purple_request_field_group_add_field(group, field); + + for (l = buddies; l != NULL; l = l->next) { + name = purple_buddy_get_name(l->data); + alias = purple_buddy_get_alias(l->data); + str = g_strdup_printf("%s (%s)", alias, name); + purple_request_field_list_add_icon(field, str, NULL, l->data); + g_free(str); + } + + for (l = select; l != NULL; l = l->next) { + name = purple_buddy_get_name(l->data); + alias = purple_buddy_get_alias(l->data); + str = g_strdup_printf("%s (%s)", alias, name); + items = g_list_append(items, str); + } + + purple_request_field_list_set_selected(field, items); + g_slist_free(buddies); + g_list_free_full(items, g_free); + + cpar = purple_request_cpar_from_connection(gc); + return purple_request_fields(gc, title, primary, secondary, fields, + _("Ok"), + G_CALLBACK(fb_util_request_buddy_ok), + _("Cancel"), + G_CALLBACK(fb_util_request_buddy_cancel), + cpar, mata); +} + +void +fb_util_serv_got_im(PurpleConnection *gc, const gchar *who, const gchar *text, + PurpleMessageFlags flags, guint64 timestamp) +{ + const gchar *name; + PurpleAccount *acct; + PurpleIMConversation *conv; + PurpleMessage *msg; + + if (!(flags & PURPLE_MESSAGE_SEND)) { + purple_serv_got_im(gc, who, text, flags, timestamp); + return; + } + + acct = purple_connection_get_account(gc); + conv = purple_conversations_find_im_with_account(who, acct); + + if (conv == NULL) { + conv = purple_im_conversation_new(acct, who); + } + + name = purple_account_get_username(acct); + msg = purple_message_new_outgoing(name, text, flags); + purple_message_set_time(msg, timestamp); + purple_conversation_write_message(PURPLE_CONVERSATION(conv), msg); +} + +void +fb_util_serv_got_chat_in(PurpleConnection *gc, gint id, const gchar *who, + const gchar *text, PurpleMessageFlags flags, + guint64 timestamp) +{ + const gchar *name; + PurpleAccount *acct; + PurpleChatConversation *conv; + PurpleMessage *msg; + + if (!(flags & PURPLE_MESSAGE_SEND)) { + purple_serv_got_chat_in(gc, id, who, flags, text, timestamp); + return; + } + + acct = purple_connection_get_account(gc); + conv = purple_conversations_find_chat(gc, id); + + name = purple_account_get_username(acct); + msg = purple_message_new_outgoing(name, text, flags); + purple_message_set_time(msg, timestamp); + purple_conversation_write_message(PURPLE_CONVERSATION(conv), msg); +} + +gboolean +fb_util_strtest(const gchar *str, GAsciiType type) +{ + gsize i; + gsize size; + guchar c; + + g_return_val_if_fail(str != NULL, FALSE); + size = strlen(str); + + for (i = 0; i < size; i++) { + c = (guchar) str[i]; + + if ((g_ascii_table[c] & type) == 0) { + return FALSE; + } + } + + return TRUE; +} + +gboolean +fb_util_zlib_test(const GByteArray *bytes) +{ + guint8 b0; + guint8 b1; + + g_return_val_if_fail(bytes != NULL, FALSE); + + if (bytes->len < 2) { + return FALSE; + } + + b0 = *(bytes->data + 0); + b1 = *(bytes->data + 1); + + return ((((b0 << 8) | b1) % 31) == 0) && /* Check the header */ + ((b0 & 0x0F) == 8 /* Z_DEFLATED */); /* Check the method */ +} + +static GByteArray * +fb_util_zlib_conv(GConverter *conv, const GByteArray *bytes, GError **error) +{ + GByteArray *ret; + GConverterResult res; + gsize cize = 0; + gsize rize; + gsize wize; + guint8 data[1024]; + + ret = g_byte_array_new(); + + while (TRUE) { + rize = 0; + wize = 0; + + res = g_converter_convert(conv, + bytes->data + cize, + bytes->len - cize, + data, sizeof data, + G_CONVERTER_INPUT_AT_END, + &rize, &wize, error); + + switch (res) { + case G_CONVERTER_CONVERTED: + g_byte_array_append(ret, data, wize); + cize += rize; + break; + + case G_CONVERTER_ERROR: + g_byte_array_free(ret, TRUE); + return NULL; + + case G_CONVERTER_FINISHED: + g_byte_array_append(ret, data, wize); + return ret; + + default: + break; + } + } +} + +GByteArray * +fb_util_zlib_deflate(const GByteArray *bytes, GError **error) +{ + GByteArray *ret; + GZlibCompressor *conv; + + conv = g_zlib_compressor_new(G_ZLIB_COMPRESSOR_FORMAT_ZLIB, -1); + ret = fb_util_zlib_conv(G_CONVERTER(conv), bytes, error); + g_object_unref(conv); + return ret; +} + +GByteArray * +fb_util_zlib_inflate(const GByteArray *bytes, GError **error) +{ + GByteArray *ret; + GZlibDecompressor *conv; + + conv = g_zlib_decompressor_new(G_ZLIB_COMPRESSOR_FORMAT_ZLIB); + ret = fb_util_zlib_conv(G_CONVERTER(conv), bytes, error); + g_object_unref(conv); + return ret; +} diff --git a/facebook/util.h b/facebook/util.h new file mode 100644 index 00000000..2a1d5ebf --- /dev/null +++ b/facebook/util.h @@ -0,0 +1,350 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#ifndef _FACEBOOK_UTIL_H_ +#define _FACEBOOK_UTIL_H_ + +/** + * SECTION:util + * @section_id: facebook-util + * @short_description: util.h + * @title: General Utilities + * + * The general utilities. + */ + +#include + +#include + +#include "account.h" +#include "conversationtypes.h" +#include "debug.h" +#include "connection.h" +#include "conversation.h" + +/** + * FB_UTIL_DEBUG_INFO: + * + * Shortcut #PurpleDebugLevel for unsafe and verbose info messages. + */ +#define FB_UTIL_DEBUG_INFO ( \ + PURPLE_DEBUG_INFO | \ + FB_UTIL_DEBUG_FLAG_UNSAFE | \ + FB_UTIL_DEBUG_FLAG_VERBOSE \ + ) + +/** + * FB_UTIL_ERROR: + * + * The #GQuark of the domain of utility errors. + */ +#define FB_UTIL_ERROR fb_util_error_quark() + +/** + * FbUtilRequestBuddyFunc: + * @buddies: The list of #PurpleBuddy's. + * @data: The user-defined data. + * + * The callback for requested buddies. + */ +typedef void (*FbUtilRequestBuddyFunc) (GSList *buddies, gpointer data); + +/** + * FbUtilDebugFlags: + * @FB_UTIL_DEBUG_FLAG_UNSAFE: The message is unsafe. + * @FB_UTIL_DEBUG_FLAG_VERBOSE: The message is verbose. + * @FB_UTIL_DEBUG_FLAG_ALL: All of the flags. + * + * The debugging message flags. These flags are inserted on top of + * a #PurpleDebugLevel. + */ +typedef enum +{ + FB_UTIL_DEBUG_FLAG_UNSAFE = 1 << 25, + FB_UTIL_DEBUG_FLAG_VERBOSE = 1 << 26, + FB_UTIL_DEBUG_FLAG_ALL = 3 << 25 +} FbUtilDebugFlags; + +/** + * FbUtilError: + * @FB_UTIL_ERROR_GENERAL: General failure. + * + * The error codes for the #FB_UTIL_ERROR domain. + */ +typedef enum +{ + FB_UTIL_ERROR_GENERAL +} FbUtilError; + +/** + * fb_util_error_quark: + * + * Gets the #GQuark of the domain of utility errors. + * + * Returns: The #GQuark of the domain. + */ +GQuark +fb_util_error_quark(void); + +/** + * fb_util_account_find_buddy: + * @acct: The #PurpleAccount. + * @chat: The #PurpleChatConversation. + * @name: The name of the buddy. + * @error: The return location for the #GError or #NULL. + * + * Finds a buddy by their name or alias. + * + * Returns: The #PurpleBuddy if found, otherwise #NULL. + */ +PurpleBuddy * +fb_util_account_find_buddy(PurpleAccount *acct, PurpleChatConversation *chat, + const gchar *name, GError **error); + +/** + * fb_util_debug: + * @level: The #PurpleDebugLevel. + * @format: The format string literal. + * @...: The arguments for @format. + * + * Logs a debugging message. If the messages is unsafe or verbose, + * apply the appropriate #FbUtilDebugFlags. + */ +void +fb_util_debug(PurpleDebugLevel level, const gchar *format, ...) + G_GNUC_PRINTF(2, 3); + +/** + * fb_util_vdebug: + * @level: The #PurpleDebugLevel. + * @format: The format string literal. + * @ap: The #va_list. + * + * Logs a debugging message. If the messages is unsafe or verbose, + * apply the appropriate #FbUtilDebugFlags. + */ +void +fb_util_vdebug(PurpleDebugLevel level, const gchar *format, va_list ap); + +/** + * fb_util_debug_misc: + * @format: The format string literal. + * @...: The arguments for @format. + * + * Logs a debugging message with the level of #PURPLE_DEBUG_MISC. + */ +void +fb_util_debug_misc(const gchar *format, ...) + G_GNUC_PRINTF(1, 2); + +/** + * fb_util_debug_info: + * @format: The format string literal. + * @...: The arguments for @format. + * + * Logs a debugging message with the level of #PURPLE_DEBUG_INFO. + */ +void +fb_util_debug_info(const gchar *format, ...) + G_GNUC_PRINTF(1, 2); + +/** + * fb_util_debug_warning: + * @format: The format string literal. + * @...: The arguments for @format. + * + * Logs a debugging message with the level of #PURPLE_DEBUG_WARNING. + */ +void +fb_util_debug_warning(const gchar *format, ...) + G_GNUC_PRINTF(1, 2); + +/** + * fb_util_debug_error: + * @format: The format string literal. + * @...: The arguments for @format. + * + * Logs a debugging message with the level of #PURPLE_DEBUG_ERROR. + */ +void +fb_util_debug_error(const gchar *format, ...) + G_GNUC_PRINTF(1, 2); + +/** + * fb_util_debug_fatal: + * @format: The format string literal. + * @...: The arguments for @format. + * + * Logs a debugging message with the level of #PURPLE_DEBUG_FATAL. + */ +void +fb_util_debug_fatal(const gchar *format, ...) + G_GNUC_PRINTF(1, 2); + +/** + * fb_util_debug_hexdump: + * @level: The #PurpleDebugLevel. + * @bytes: The #GByteArray. + * @format: The format string literal. + * @...: The arguments for @format. + * + * Logs a hexdump of a #GByteArray. If the messages is unsafe or + * verbose, apply the appropriate #FbUtilDebugFlags. + */ +void +fb_util_debug_hexdump(PurpleDebugLevel level, const GByteArray *bytes, + const gchar *format, ...) + G_GNUC_PRINTF(3, 4); + +/** + * fb_util_get_locale: + * + * Gets the locale string (ex: en_US) from the system. The returned + * string should be freed with #g_free() when no longer needed. + * + * Returns: The locale string. + */ +gchar * +fb_util_get_locale(void); + +/** + * fb_util_rand_alnum: + * @len: The length of the string. + * + * Gets a random alphanumeric (A-Za-z0-9) string. This function should + * *not* be relied on for cryptographic operations. The returned string + * should be freed with #g_free() when no longer needed. + * + * Returns: The alphanumeric string. + */ +gchar * +fb_util_rand_alnum(guint len); + +/** + * fb_util_request_buddy: + * @gc: The #PurpleConnection. + * @title: The title of the message or #NULL. + * @primary: The main point of the message or #NULL. + * @secondary: The secondary information or #NULL. + * @select: A #GSList of selected buddies or #NULL. + * @multi: #TRUE to for multiple buddy selections, otherwise #FALSE. + * @ok_cb: The callback for the `OK` button or #NULL. + * @cancel_cb: The callback for the `Cancel` button or #NULL. + * @data: The user-defined data. + * + * Displays a buddy list selection form. + * + * Returns: The UI-specific handle. + */ +gpointer +fb_util_request_buddy(PurpleConnection *gc, const gchar *title, + const gchar *primary, const gchar *secondary, + GSList *select, gboolean multi, GCallback ok_cb, + GCallback cancel_cb, gpointer data); + +/** + * fb_util_serv_got_im: + * @gc: The #PurpleConnection. + * @who: The message sender or receiver. + * @text: The message text. + * @flags: The #PurpleMessageFlags. + * @timestamp: The message timestamp. + * + * Handles an incoming IM message. This function is special in that it + * handles self messages. This function determines the direction of the + * message from the #PurpleMessageFlags. + */ +void +fb_util_serv_got_im(PurpleConnection *gc, const gchar *who, const gchar *text, + PurpleMessageFlags flags, guint64 timestamp); + +/** + * fb_util_serv_got_chat_in: + * @gc: The #PurpleConnection. + * @id: The id of the chat. + * @who: The message sender or receiver. + * @text: The message text. + * @flags: The #PurpleMessageFlags. + * @timestamp: The message timestamp. + * + * Handles an incoming chat message. This function is special in that + * it handles self messages. This function determines the direction of + * the message from the #PurpleMessageFlags. + */ +void +fb_util_serv_got_chat_in(PurpleConnection *gc, gint id, const gchar *who, + const gchar *text, PurpleMessageFlags flags, + guint64 timestamp); + +/** + * fb_util_strtest: + * @str: The string. + * @type: The #GAsciiType. + * + * Tests if the string only contains characters allowed by the + * #GAsciiType. More than one type can be specified by ORing the types + * together. + * + * Returns: #TRUE if the string only contains characters allowed by the + * #GAsciiType, otherwise #FALSE. + */ +gboolean +fb_util_strtest(const gchar *str, GAsciiType type); + +/** + * fb_util_zlib_test: + * @bytes: The #GByteArray. + * + * Tests if the #GByteArray is zlib compressed. + * + * Returns: #TRUE if the #GByteArray is compressed, otherwise #FALSE. + */ +gboolean +fb_util_zlib_test(const GByteArray *bytes); + +/** + * fb_util_zlib_deflate: + * @bytes: The #GByteArray. + * @error: The return location for the #GError or #NULL. + * + * Deflates a #GByteArray with zlib. The returned #GByteArray should be + * freed with #g_byte_array_free() when no longer needed. + * + * Returns: The deflated #GByteArray or #NULL on error. + */ +GByteArray * +fb_util_zlib_deflate(const GByteArray *bytes, GError **error); + +/** + * fb_util_zlib_inflate: + * @bytes: The #GByteArray. + * @error: The return location for the #GError or #NULL. + * + * Inflates a #GByteArray with zlib. The returned #GByteArray should be + * freed with #g_byte_array_free() when no longer needed. + * + * Returns: The inflated #GByteArray or #NULL on error. + */ +GByteArray * +fb_util_zlib_inflate(const GByteArray *bytes, GError **error); + +#endif /* _FACEBOOK_UTIL_H_ */ diff --git a/include/blistnode.h b/include/blistnode.h new file mode 100644 index 00000000..e69de29b diff --git a/include/buddy.h b/include/buddy.h new file mode 100644 index 00000000..e69de29b diff --git a/include/buddylist.h b/include/buddylist.h new file mode 100644 index 00000000..e69de29b diff --git a/include/conversations.h b/include/conversations.h new file mode 100644 index 00000000..e69de29b diff --git a/include/conversationtypes.h b/include/conversationtypes.h new file mode 100644 index 00000000..e69de29b diff --git a/include/image-store.h b/include/image-store.h new file mode 100644 index 00000000..e69de29b diff --git a/include/image.h b/include/image.h new file mode 100644 index 00000000..e69de29b diff --git a/include/message.h b/include/message.h new file mode 100644 index 00000000..e69de29b diff --git a/include/plugins.h b/include/plugins.h new file mode 100644 index 00000000..e69de29b diff --git a/include/presence.h b/include/presence.h new file mode 100644 index 00000000..e69de29b diff --git a/include/protocol.h b/include/protocol.h new file mode 100644 index 00000000..e69de29b diff --git a/include/protocols.h b/include/protocols.h new file mode 100644 index 00000000..e69de29b diff --git a/patches/01-makefile.patch b/patches/01-makefile.patch deleted file mode 100644 index ed5a3d7e..00000000 --- a/patches/01-makefile.patch +++ /dev/null @@ -1,68 +0,0 @@ -diff -r ad2ee74b913a libpurple/protocols/facebook/Makefile.am ---- a/libpurple/protocols/facebook/Makefile.am Thu Jan 07 14:06:04 2016 -0500 -+++ b/libpurple/protocols/facebook/Makefile.am Thu Jan 07 15:45:09 2016 -0500 -@@ -20,7 +20,12 @@ - thrift.c \ - thrift.h \ - util.c \ -- util.h -+ util.h \ -+ ../../glibcompat.h \ -+ ../../http.c \ -+ ../../http.h \ -+ ../../purple-socket.h \ -+ ../../purple-socket.c - - AM_CFLAGS = $(st) - -@@ -43,10 +48,9 @@ - endif - - AM_CPPFLAGS = \ -- -I$(top_srcdir)/libpurple \ -- -I$(top_builddir)/libpurple \ -- -I$(top_srcdir) \ - $(GLIB_CFLAGS) \ - $(JSON_CFLAGS) \ -- $(GPLUGIN_CFLAGS) \ -+ $(PURPLE_CFLAGS) \ -+ $(ZLIB_CFLAGS) \ -+ $(PLUGIN_CFLAGS) \ - $(DEBUG_CFLAGS) -diff -r ad2ee74b913a libpurple/protocols/facebook/Makefile.mingw ---- a/libpurple/protocols/facebook/Makefile.mingw Thu Jan 07 14:06:04 2016 -0500 -+++ b/libpurple/protocols/facebook/Makefile.mingw Thu Jan 07 15:45:09 2016 -0500 -@@ -49,7 +49,9 @@ - json.c \ - mqtt.c \ - thrift.c \ -- util.c -+ util.c \ -+ ../../http.c \ -+ ../../purple-socket.c - - OBJECTS = $(C_SRC:%.c=%.o) - -@@ -62,7 +64,6 @@ - -lgobject-2.0 \ - -lws2_32 \ - -lintl \ -- -lgplugin \ - -ljson-glib-1.0 \ - -lz \ - -lpurple -@@ -76,12 +77,12 @@ - - all: $(TARGET).dll - --install: all $(DLL_INSTALL_DIR) -+install: all - cp $(TARGET).dll $(DLL_INSTALL_DIR) - - $(OBJECTS): $(PURPLE_CONFIG_H) - --$(TARGET).dll: $(PURPLE_DLL).a $(OBJECTS) -+$(TARGET).dll: $(OBJECTS) - $(CC) -shared $(OBJECTS) $(LIB_PATHS) $(LIBS) $(DLL_LD_FLAGS) -o $(TARGET).dll - - ## diff --git a/patches/02-glibcompat.patch b/patches/02-glibcompat.patch deleted file mode 100644 index 5e131f0f..00000000 --- a/patches/02-glibcompat.patch +++ /dev/null @@ -1,33 +0,0 @@ ---- a/libpurple/glibcompat.h 2016-11-21 09:38:39.250858177 -0300 -+++ b/libpurple/glibcompat.h 2016-11-21 09:39:44.789845560 -0300 -@@ -71,6 +71,30 @@ - /****************************************************************************** - * g_assert_* macros - *****************************************************************************/ -+ -+#if !GLIB_CHECK_VERSION(2, 32, 0) -+static inline GByteArray * g_byte_array_new_take(guint8 *data, gsize len) -+{ -+ GByteArray *array; -+ -+ array = g_byte_array_new(); -+ g_byte_array_append(array, data, len); -+ g_free(data); -+ -+ return array; -+} -+ -+static inline void g_queue_free_full(GQueue *queue, GDestroyNotify free_func) -+{ -+ g_queue_foreach(queue, (GFunc)free_func, NULL); -+ g_queue_free(queue); -+} -+#endif -+ -+#if !GLIB_CHECK_VERSION(2, 30, 0) -+#define G_VALUE_INIT {0, {{0}}} -+#endif -+ - #if !GLIB_CHECK_VERSION(2, 38, 0) - #define g_assert_true(expr) G_STMT_START { \ - if G_LIKELY (expr) ; else \ diff --git a/patches/03-plugin.patch b/patches/03-plugin.patch deleted file mode 100644 index 0964223c..00000000 --- a/patches/03-plugin.patch +++ /dev/null @@ -1,398 +0,0 @@ -diff -r 25a255f32eee libpurple/protocols/facebook/facebook.c ---- a/libpurple/protocols/facebook/facebook.c Sat Jan 16 10:01:23 2016 -0500 -+++ b/libpurple/protocols/facebook/facebook.c Mon Jan 18 09:39:59 2016 -0500 -@@ -500,7 +500,7 @@ - id = purple_image_store_add_weak(pimg); - - g_free(msg->text); -- msg->text = g_strdup_printf("text = g_strdup_printf("", id); - msg->flags |= FB_API_MESSAGE_FLAG_DONE; -@@ -966,7 +966,7 @@ - GSList *select = NULL; - PurpleConnection *gc; - -- if (!PURPLE_IS_BUDDY(node)) { -+ if (!PURPLE_BLIST_NODE_IS_BUDDY(node)) { - return; - } - -@@ -1160,7 +1160,7 @@ - PurpleConnection *gc; - PurpleMenuAction *act; - -- if (!PURPLE_IS_BUDDY(node)) { -+ if (!PURPLE_BLIST_NODE_IS_BUDDY(node)) { - return NULL; - } - -@@ -1208,7 +1208,8 @@ - } - - static gint --fb_im_send(PurpleConnection *gc, PurpleMessage *msg) -+fb_im_send(PurpleConnection *gc, const gchar *who, const gchar *tmsg, -+ PurpleMessageFlags flags) - { - const gchar *name; - const gchar *text; -@@ -1217,6 +1218,8 @@ - FbId uid; - gchar *sext; - -+ PurpleMessage *msg = purple_message_new_outgoing(who, tmsg, flags); -+ - fata = purple_connection_get_protocol_data(gc); - api = fb_data_get_api(fata); - -@@ -1355,7 +1358,8 @@ - } - - static gint --fb_chat_send(PurpleConnection *gc, gint id, PurpleMessage *msg) -+fb_chat_send(PurpleConnection *gc, gint id, const gchar *tmsg, -+ PurpleMessageFlags flags) - { - const gchar *name; - const gchar *text; -@@ -1366,6 +1370,8 @@ - PurpleAccount *acct; - PurpleChatConversation *chat; - -+ PurpleMessage *msg = purple_message_new_outgoing(NULL, tmsg, flags); -+ - acct = purple_connection_get_account(gc); - fata = purple_connection_get_protocol_data(gc); - api = fb_data_get_api(fata); -@@ -1534,107 +1540,6 @@ - } - - static void --facebook_protocol_init(PurpleProtocol *protocol) --{ -- GList *opts = NULL; -- PurpleAccountOption *opt; -- -- protocol->id = FB_PROTOCOL_ID; -- protocol->name = "Facebook"; -- protocol->options = OPT_PROTO_CHAT_TOPIC; -- -- opt = purple_account_option_int_new(_("Buddy list sync interval"), -- "sync-interval", 5); -- opts = g_list_prepend(opts, opt); -- -- opt = purple_account_option_bool_new(_("Mark messages as read on focus"), -- "mark-read", TRUE); -- opts = g_list_prepend(opts, opt); -- -- opt = purple_account_option_bool_new(_("Mark messages as read only when available"), -- "mark-read-available", FALSE); -- opts = g_list_prepend(opts, opt); -- -- opt = purple_account_option_bool_new(_("Show self messages"), -- "show-self", TRUE); -- opts = g_list_prepend(opts, opt); -- -- opt = purple_account_option_bool_new(_("Show unread messages"), -- "show-unread", TRUE); -- opts = g_list_prepend(opts, opt); -- -- opt = purple_account_option_bool_new(_("Open new group chats with " -- "incoming messages"), -- "group-chat-open", TRUE); -- opts = g_list_prepend(opts, opt); -- protocol->account_options = g_list_reverse(opts); --} -- --static void --facebook_protocol_class_init(PurpleProtocolClass *klass) --{ -- klass->login = fb_login; -- klass->close = fb_close; -- klass->status_types = fb_status_types; -- klass->list_icon = fb_list_icon; --} -- --static void --facebook_protocol_client_iface_init(PurpleProtocolClientIface *iface) --{ -- iface->tooltip_text = fb_client_tooltip_text; -- iface->blist_node_menu = fb_client_blist_node_menu; -- iface->offline_message = fb_client_offline_message; --} -- --static void --facebook_protocol_server_iface_init(PurpleProtocolServerIface *iface) --{ -- iface->set_status = fb_server_set_status; --} -- --static void --facebook_protocol_im_iface_init(PurpleProtocolIMIface *iface) --{ -- iface->send = fb_im_send; -- iface->send_typing = fb_im_send_typing; --} -- --static void --facebook_protocol_chat_iface_init(PurpleProtocolChatIface *iface) --{ -- iface->info = fb_chat_info; -- iface->info_defaults = fb_chat_info_defaults; -- iface->join = fb_chat_join; -- iface->get_name = fb_chat_get_name; -- iface->invite = fb_chat_invite; -- iface->send = fb_chat_send; -- iface->set_topic = fb_chat_set_topic; --} -- --static void --facebook_protocol_roomlist_iface_init(PurpleProtocolRoomlistIface *iface) --{ -- iface->get_list = fb_roomlist_get_list; -- iface->cancel = fb_roomlist_cancel; --} -- --PURPLE_DEFINE_TYPE_EXTENDED( -- FacebookProtocol, facebook_protocol, PURPLE_TYPE_PROTOCOL, 0, -- -- PURPLE_IMPLEMENT_INTERFACE_STATIC(PURPLE_TYPE_PROTOCOL_CLIENT_IFACE, -- facebook_protocol_client_iface_init) -- PURPLE_IMPLEMENT_INTERFACE_STATIC(PURPLE_TYPE_PROTOCOL_SERVER_IFACE, -- facebook_protocol_server_iface_init) -- PURPLE_IMPLEMENT_INTERFACE_STATIC(PURPLE_TYPE_PROTOCOL_IM_IFACE, -- facebook_protocol_im_iface_init) -- PURPLE_IMPLEMENT_INTERFACE_STATIC(PURPLE_TYPE_PROTOCOL_CHAT_IFACE, -- facebook_protocol_chat_iface_init) -- PURPLE_IMPLEMENT_INTERFACE_STATIC(PURPLE_TYPE_PROTOCOL_ROOMLIST_IFACE, -- facebook_protocol_roomlist_iface_init) --); -- --static void - fb_cmds_register(void) - { - PurpleCmdId id; -@@ -1646,13 +1551,13 @@ - g_return_if_fail(fb_cmds == NULL); - - id = purple_cmd_register("kick", "s", PURPLE_CMD_P_PROTOCOL, cflags, -- fb_protocol->id, fb_cmd_kick, -+ "prpl-facebook", fb_cmd_kick, - _("kick: Kick someone from the chat"), - NULL); - fb_cmds = g_slist_prepend(fb_cmds, GUINT_TO_POINTER(id)); - - id = purple_cmd_register("leave", "", PURPLE_CMD_P_PROTOCOL, cflags, -- fb_protocol->id, fb_cmd_leave, -+ "prpl-facebook", fb_cmd_leave, - _("leave: Leave the chat"), - NULL); - fb_cmds = g_slist_prepend(fb_cmds, GUINT_TO_POINTER(id)); -@@ -1671,43 +1576,110 @@ - g_slist_free_full(fb_cmds, fb_cmds_unregister_free); - } - --static PurplePluginInfo * --plugin_query(GError **error) --{ -- return purple_plugin_info_new( -- "id", FB_PROTOCOL_ID, -- "name", "Facebook Protocol", -- "version", DISPLAY_VERSION, -- "category", N_("Protocol"), -- "summary", N_("Facebook Protocol Plugin"), -- "description", N_("Facebook Protocol Plugin"), -- "website", PURPLE_WEBSITE, -- "abi-version", PURPLE_ABI_VERSION, -- "flags", PURPLE_PLUGIN_INFO_FLAGS_INTERNAL | -- PURPLE_PLUGIN_INFO_FLAGS_AUTO_LOAD, -- NULL -- ); --} -- - static gboolean --plugin_load(PurplePlugin *plugin, GError **error) -+plugin_load(PurplePlugin *plugin) - { -- facebook_protocol_register_type(plugin); -- fb_protocol = purple_protocols_add(FACEBOOK_TYPE_PROTOCOL, error); -- -- if (fb_protocol == NULL) { -- return FALSE; -- } -- - fb_cmds_register(); -+ _purple_socket_init(); -+ purple_http_init(); - return TRUE; - } - - static gboolean --plugin_unload(PurplePlugin *plugin, GError **error) -+plugin_unload(PurplePlugin *plugin) - { - fb_cmds_unregister(); -- return purple_protocols_remove(fb_protocol, error); -+ purple_http_uninit(); -+ _purple_socket_uninit(); -+ return TRUE; - } - --PURPLE_PLUGIN_INIT(facebook, plugin_query, plugin_load, plugin_unload); -+G_MODULE_EXPORT gboolean -+purple_init_plugin(PurplePlugin *plugin); -+ -+G_MODULE_EXPORT gboolean -+purple_init_plugin(PurplePlugin *plugin) -+{ -+ GList *opts = NULL; -+ PurpleAccountOption *opt; -+ -+ static gboolean inited = FALSE; -+ static PurplePluginInfo info; -+ static PurplePluginProtocolInfo pinfo; -+ -+ (void) fb_protocol; -+ plugin->info = &info; -+ -+ if (G_LIKELY(inited)) { -+ return purple_plugin_register(plugin); -+ } -+ -+ memset(&info, 0, sizeof info); -+ memset(&pinfo, 0, sizeof pinfo); -+ -+ info.magic = PURPLE_PLUGIN_MAGIC; -+ info.major_version = PURPLE_MAJOR_VERSION; -+ info.minor_version = PURPLE_MINOR_VERSION; -+ info.type = PURPLE_PLUGIN_PROTOCOL; -+ info.priority = PURPLE_PRIORITY_DEFAULT; -+ info.id = FB_PROTOCOL_ID; -+ info.name = "Facebook"; -+ info.version = PACKAGE_VERSION; -+ info.summary = N_("Facebook Protocol Plugin"); -+ info.description = N_("Facebook Protocol Plugin"); -+ info.homepage = PACKAGE_URL; -+ info.load = plugin_load; -+ info.unload = plugin_unload; -+ info.extra_info = &pinfo; -+ -+ pinfo.options = OPT_PROTO_CHAT_TOPIC; -+ pinfo.list_icon = fb_list_icon; -+ pinfo.tooltip_text = fb_client_tooltip_text; -+ pinfo.status_types = fb_status_types; -+ pinfo.blist_node_menu = fb_client_blist_node_menu; -+ pinfo.chat_info = fb_chat_info; -+ pinfo.chat_info_defaults = fb_chat_info_defaults; -+ pinfo.login = fb_login; -+ pinfo.close = fb_close; -+ pinfo.send_im = fb_im_send; -+ pinfo.send_typing = fb_im_send_typing; -+ pinfo.set_status = fb_server_set_status; -+ pinfo.join_chat = fb_chat_join; -+ pinfo.get_chat_name = fb_chat_get_name; -+ pinfo.chat_invite = fb_chat_invite; -+ pinfo.chat_send = fb_chat_send; -+ pinfo.set_chat_topic = fb_chat_set_topic; -+ pinfo.roomlist_get_list = fb_roomlist_get_list; -+ pinfo.roomlist_cancel = fb_roomlist_cancel; -+ pinfo.offline_message = fb_client_offline_message; -+ pinfo.struct_size = sizeof pinfo; -+ -+ opt = purple_account_option_int_new(_("Buddy list sync interval"), -+ "sync-interval", 5); -+ opts = g_list_prepend(opts, opt); -+ -+ opt = purple_account_option_bool_new(_("Mark messages as read on focus"), -+ "mark-read", TRUE); -+ opts = g_list_prepend(opts, opt); -+ -+ opt = purple_account_option_bool_new(_("Mark messages as read only when available"), -+ "mark-read-available", FALSE); -+ opts = g_list_prepend(opts, opt); -+ -+ opt = purple_account_option_bool_new(_("Show self messages"), -+ "show-self", TRUE); -+ opts = g_list_prepend(opts, opt); -+ -+ opt = purple_account_option_bool_new(_("Show unread messages"), -+ "show-unread", TRUE); -+ opts = g_list_prepend(opts, opt); -+ -+ opt = purple_account_option_bool_new(_("Open new group chats with " -+ "incoming messages"), -+ "group-chat-open", TRUE); -+ opts = g_list_prepend(opts, opt); -+ pinfo.protocol_options = g_list_reverse(opts); -+ -+ inited = TRUE; -+ return purple_plugin_register(plugin); -+} -diff -r 25a255f32eee libpurple/protocols/facebook/facebook.h ---- a/libpurple/protocols/facebook/facebook.h Sat Jan 16 10:01:23 2016 -0500 -+++ b/libpurple/protocols/facebook/facebook.h Mon Jan 18 09:39:59 2016 -0500 -@@ -22,24 +22,7 @@ - #ifndef _FACEBOOK_H_ - #define _FACEBOOK_H_ - --/** -- * SECTION:facebook -- * @section_id: facebook-plugin -- * @short_description: facebook.h -- * @title: Facebook Plugin -- * -- * The Facebook Messenger #PurpleProtocol. -- */ -- - #include --#include -- --#define FACEBOOK_TYPE_PROTOCOL (facebook_protocol_get_type()) --#define FACEBOOK_PROTOCOL(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), FACEBOOK_TYPE_PROTOCOL, FacebookProtocol)) --#define FACEBOOK_PROTOCOL_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), FACEBOOK_TYPE_PROTOCOL, FacebookProtocolClass)) --#define FACEBOOK_IS_PROTOCOL(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), FACEBOOK_TYPE_PROTOCOL)) --#define FACEBOOK_IS_PROTOCOL_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), FACEBOOK_TYPE_PROTOCOL)) --#define FACEBOOK_PROTOCOL_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), FACEBOOK_TYPE_PROTOCOL, FacebookProtocolClass)) - - /** - * FB_PROTOCOL_ID: -@@ -48,37 +31,4 @@ - */ - #define FB_PROTOCOL_ID "prpl-facebook" - --typedef struct _FacebookProtocol FacebookProtocol; --typedef struct _FacebookProtocolClass FacebookProtocolClass; -- --/** -- * FacebookProtocol: -- * -- * Represents the Facebook #PurpleProtocol. -- */ --struct _FacebookProtocol --{ -- /*< private >*/ -- PurpleProtocol parent; --}; -- --/** -- * FacebookProtocolClass: -- * -- * The base class for all #FacebookProtocol's. -- */ --struct _FacebookProtocolClass --{ -- /*< private >*/ -- PurpleProtocolClass parent_class; --}; -- --/** -- * facebook_protocol_get_type: -- * -- * Returns: The #GType for a #FacebookProtocol. -- */ --G_MODULE_EXPORT GType --facebook_protocol_get_type(void); -- - #endif /* _FACEBOOK_H_ */ diff --git a/patches/04-revert-gio.patch b/patches/04-revert-gio.patch deleted file mode 100644 index 811c3171..00000000 --- a/patches/04-revert-gio.patch +++ /dev/null @@ -1,530 +0,0 @@ -diff -r 56d191003b34 -r 5a63c26f21fd libpurple/protocols/facebook/facebook.c ---- a/libpurple/protocols/facebook/facebook.c Thu Sep 15 13:31:06 2016 -0500 -+++ b/libpurple/protocols/facebook/facebook.c Wed Sep 14 14:53:12 2016 -0500 -@@ -373,8 +373,8 @@ - - gc = fb_data_get_connection(fata); - -- if (error->domain == G_IO_ERROR) { -- purple_connection_g_error(gc, error); -+ if (error->domain == FB_MQTT_SSL_ERROR) { -+ purple_connection_ssl_error(gc, error->code); - return; - } - -diff -r 56d191003b34 -r 5a63c26f21fd libpurple/protocols/facebook/mqtt.c ---- a/libpurple/protocols/facebook/mqtt.c Thu Sep 15 13:31:06 2016 -0500 -+++ b/libpurple/protocols/facebook/mqtt.c Wed Sep 14 14:53:12 2016 -0500 -@@ -28,8 +28,7 @@ - #include "account.h" - #include "eventloop.h" - #include "glibcompat.h" --#include "purple-gio.h" --#include "queuedoutputstream.h" -+#include "sslconn.h" - - #include "mqtt.h" - #include "util.h" -@@ -37,17 +36,17 @@ - struct _FbMqttPrivate - { - PurpleConnection *gc; -- GIOStream *conn; -- GBufferedInputStream *input; -- PurpleQueuedOutputStream *output; -- GCancellable *cancellable; -+ PurpleSslConnection *gsc; - gboolean connected; - guint16 mid; - - GByteArray *rbuf; -+ GByteArray *wbuf; - gsize remz; - - gint tev; -+ gint rev; -+ gint wev; - }; - - struct _FbMqttMessagePrivate -@@ -65,8 +64,6 @@ - G_DEFINE_TYPE(FbMqtt, fb_mqtt, G_TYPE_OBJECT); - G_DEFINE_TYPE(FbMqttMessage, fb_mqtt_message, G_TYPE_OBJECT); - --static void fb_mqtt_read_packet(FbMqtt *mqtt); -- - static void - fb_mqtt_dispose(GObject *obj) - { -@@ -75,6 +72,7 @@ - - fb_mqtt_close(mqtt); - g_byte_array_free(priv->rbuf, TRUE); -+ g_byte_array_free(priv->wbuf, TRUE); - } - - static void -@@ -158,6 +156,7 @@ - mqtt->priv = priv; - - priv->rbuf = g_byte_array_new(); -+ priv->wbuf = g_byte_array_new(); - } - - static void -@@ -201,6 +200,18 @@ - return q; - } - -+GQuark -+fb_mqtt_ssl_error_quark(void) -+{ -+ static GQuark q = 0; -+ -+ if (G_UNLIKELY(q == 0)) { -+ q = g_quark_from_static_string("fb-mqtt-ssl-error-quark"); -+ } -+ -+ return q; -+} -+ - FbMqtt * - fb_mqtt_new(PurpleConnection *gc) - { -@@ -224,47 +235,33 @@ - g_return_if_fail(FB_IS_MQTT(mqtt)); - priv = mqtt->priv; - -- if (priv->tev > 0) { -- g_source_remove(priv->tev); -- priv->tev = 0; -+ if (priv->wev > 0) { -+ purple_input_remove(priv->wev); -+ priv->wev = 0; - } - -- if (priv->cancellable != NULL) { -- g_cancellable_cancel(priv->cancellable); -- g_clear_object(&priv->cancellable); -+ if (priv->rev > 0) { -+ purple_input_remove(priv->rev); -+ priv->rev = 0; - } - -- if (priv->conn != NULL) { -- purple_gio_graceful_close(priv->conn, -- G_INPUT_STREAM(priv->input), -- G_OUTPUT_STREAM(priv->output)); -- g_clear_object(&priv->input); -- g_clear_object(&priv->output); -- g_clear_object(&priv->conn); -+ if (priv->tev > 0) { -+ purple_timeout_remove(priv->tev); -+ priv->tev = 0; - } - -- priv->connected = FALSE; -- g_byte_array_set_size(priv->rbuf, 0); --} -- --static void --fb_mqtt_take_error(FbMqtt *mqtt, GError *err, const gchar *prefix) --{ -- if (g_error_matches(err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { -- /* Return as cancelled means the connection is closing */ -- g_error_free(err); -- return; -+ if (priv->gsc != NULL) { -+ purple_ssl_close(priv->gsc); -+ priv->gsc = NULL; - } - -- /* Now we can check for programming errors */ -- g_return_if_fail(FB_IS_MQTT(mqtt)); -- -- if (prefix != NULL) { -- g_prefix_error(&err, "%s: ", prefix); -+ if (priv->wbuf->len > 0) { -+ fb_util_debug_warning("Closing with unwritten data"); - } - -- g_signal_emit_by_name(mqtt, "error", err); -- g_error_free(err); -+ priv->connected = FALSE; -+ g_byte_array_set_size(priv->rbuf, 0); -+ g_byte_array_set_size(priv->wbuf, 0); - } - - void -@@ -342,127 +339,75 @@ - } - - static void --fb_mqtt_cb_fill(GObject *source, GAsyncResult *res, gpointer data) --{ -- GBufferedInputStream *input = G_BUFFERED_INPUT_STREAM(source); -- FbMqtt *mqtt = data; -- gssize ret; -- GError *err = NULL; -- -- ret = g_buffered_input_stream_fill_finish(input, res, &err); -- -- if (ret < 1) { -- if (ret == 0) { -- err = g_error_new_literal(G_IO_ERROR, -- G_IO_ERROR_CONNECTION_CLOSED, -- _("Connection closed")); -- } -- -- fb_mqtt_take_error(mqtt, err, _("Failed to read fixed header")); -- return; -- } -- -- fb_mqtt_read_packet(mqtt); --} -- --static void --fb_mqtt_cb_read_packet(GObject *source, GAsyncResult *res, gpointer data) -+fb_mqtt_cb_read(gpointer data, gint fd, PurpleInputCondition cond) - { - FbMqtt *mqtt = data; -- FbMqttPrivate *priv; -- gssize ret; - FbMqttMessage *msg; -- GError *err = NULL; -- -- ret = g_input_stream_read_finish(G_INPUT_STREAM(source), res, &err); -+ FbMqttPrivate *priv = mqtt->priv; -+ gint res; -+ guint mult; -+ guint8 buf[1024]; -+ guint8 byte; -+ gsize size; -+ gssize rize; - -- if (ret < 1) { -- if (ret == 0) { -- err = g_error_new_literal(G_IO_ERROR, -- G_IO_ERROR_CONNECTION_CLOSED, -- _("Connection closed")); -+ if (priv->remz < 1) { -+ /* Reset the read buffer */ -+ g_byte_array_set_size(priv->rbuf, 0); -+ -+ res = purple_ssl_read(priv->gsc, &byte, sizeof byte); -+ g_byte_array_append(priv->rbuf, &byte, sizeof byte); -+ -+ if (res != sizeof byte) { -+ fb_mqtt_error(mqtt, FB_MQTT_ERROR_GENERAL, -+ _("Failed to read fixed header")); -+ return; - } - -- fb_mqtt_take_error(mqtt, err, _("Failed to read packet data")); -- return; -- } -+ mult = 1; - -- priv = mqtt->priv; -- priv->remz -= ret; -+ do { -+ res = purple_ssl_read(priv->gsc, &byte, sizeof byte); -+ g_byte_array_append(priv->rbuf, &byte, sizeof byte); -+ -+ if (res != sizeof byte) { -+ fb_mqtt_error(mqtt, FB_MQTT_ERROR_GENERAL, -+ _("Failed to read packet size")); -+ return; -+ } - -- if (priv->remz > 0) { -- g_input_stream_read_async(G_INPUT_STREAM(source), -- priv->rbuf->data + -- priv->rbuf->len - priv->remz, priv->remz, -- G_PRIORITY_DEFAULT, priv->cancellable, -- fb_mqtt_cb_read_packet, mqtt); -- return; -+ priv->remz += (byte & 127) * mult; -+ mult *= 128; -+ } while ((byte & 128) != 0); - } - -- msg = fb_mqtt_message_new_bytes(priv->rbuf); -- -- if (G_UNLIKELY(msg == NULL)) { -- fb_mqtt_error(mqtt, FB_MQTT_ERROR_GENERAL, -- _("Failed to parse message")); -- return; -- } -+ if (priv->remz > 0) { -+ size = MIN(priv->remz, sizeof buf); -+ rize = purple_ssl_read(priv->gsc, buf, size); - -- fb_mqtt_read(mqtt, msg); -- g_object_unref(msg); -+ if (rize < 1) { -+ fb_mqtt_error(mqtt, FB_MQTT_ERROR_GENERAL, -+ _("Failed to read packet data")); -+ return; -+ } - -- /* Read another packet if connection wasn't reset in fb_mqtt_read() */ -- if (fb_mqtt_connected(mqtt, FALSE)) { -- fb_mqtt_read_packet(mqtt); -+ g_byte_array_append(priv->rbuf, buf, rize); -+ priv->remz -= rize; - } --} - --static void --fb_mqtt_read_packet(FbMqtt *mqtt) --{ -- FbMqttPrivate *priv = mqtt->priv; -- const guint8 const *buf; -- gsize count = 0; -- gsize pos; -- guint mult = 1; -- guint8 byte; -- gsize size = 0; -+ if (priv->remz < 1) { -+ msg = fb_mqtt_message_new_bytes(priv->rbuf); -+ priv->remz = 0; - -- buf = g_buffered_input_stream_peek_buffer(priv->input, &count); -- -- /* Start at 1 to skip the first byte */ -- pos = 1; -- -- do { -- if (pos >= count) { -- /* Not enough data yet, try again later */ -- g_buffered_input_stream_fill_async(priv->input, -1, -- G_PRIORITY_DEFAULT, priv->cancellable, -- fb_mqtt_cb_fill, mqtt); -+ if (G_UNLIKELY(msg == NULL)) { -+ fb_mqtt_error(mqtt, FB_MQTT_ERROR_GENERAL, -+ _("Failed to parse message")); - return; - } - -- byte = *(buf + pos++); -- -- size += (byte & 127) * mult; -- mult *= 128; -- } while ((byte & 128) != 0); -- -- /* Add header to size */ -- size += pos; -- -- g_byte_array_set_size(priv->rbuf, size); -- priv->remz = size; -- -- /* TODO: Use g_input_stream_read_all_async() when available. */ -- /* TODO: Alternately, it would be nice to let the -- * FbMqttMessage directly use the GBufferedInputStream -- * buffer instead of copying it, provided it's consumed -- * before the next read. -- */ -- g_input_stream_read_async(G_INPUT_STREAM(priv->input), -- priv->rbuf->data, priv->rbuf->len, -- G_PRIORITY_DEFAULT, priv->cancellable, -- fb_mqtt_cb_read_packet, mqtt); -+ fb_mqtt_read(mqtt, msg); -+ g_object_unref(msg); -+ } - } - - void -@@ -569,16 +514,27 @@ - } - - static void --fb_mqtt_cb_flush(GObject *source, GAsyncResult *res, gpointer data) -+fb_mqtt_cb_write(gpointer data, gint fd, PurpleInputCondition cond) - { - FbMqtt *mqtt = data; -- GError *err = NULL; -+ FbMqttPrivate *priv = mqtt->priv; -+ gssize wize; - -- if (!g_output_stream_flush_finish(G_OUTPUT_STREAM(source), -- res, &err)) { -- fb_mqtt_take_error(mqtt, err, _("Failed to write data")); -+ wize = purple_ssl_write(priv->gsc, priv->wbuf->data, priv->wbuf->len); -+ -+ if (wize < 0) { -+ fb_mqtt_error(mqtt, FB_MQTT_ERROR_GENERAL, -+ _("Failed to write data")); - return; - } -+ -+ if (wize > 0) { -+ g_byte_array_remove_range(priv->wbuf, 0, wize); -+ } -+ -+ if (priv->wbuf->len < 1) { -+ priv->wev = 0; -+ } - } - - void -@@ -587,7 +543,6 @@ - const GByteArray *bytes; - FbMqttMessagePrivate *mriv; - FbMqttPrivate *priv; -- GBytes *gbytes; - - g_return_if_fail(FB_IS_MQTT(mqtt)); - g_return_if_fail(FB_IS_MQTT_MESSAGE(msg)); -@@ -606,46 +561,46 @@ - "Writing %d (flags: 0x%0X)", - mriv->type, mriv->flags); - -- /* TODO: Would be nice to refactor this to not require copying bytes */ -- gbytes = g_bytes_new(bytes->data, bytes->len); -- purple_queued_output_stream_push_bytes(priv->output, gbytes); -- g_bytes_unref(gbytes); -+ g_byte_array_append(priv->wbuf, bytes->data, bytes->len); -+ fb_mqtt_cb_write(mqtt, priv->gsc->fd, PURPLE_INPUT_WRITE); - -- if (!g_output_stream_has_pending(G_OUTPUT_STREAM(priv->output))) { -- g_output_stream_flush_async(G_OUTPUT_STREAM(priv->output), -- G_PRIORITY_DEFAULT, priv->cancellable, -- fb_mqtt_cb_flush, mqtt); -+ if (priv->wev > 0) { -+ priv->wev = purple_input_add(priv->gsc->fd, -+ PURPLE_INPUT_WRITE, -+ fb_mqtt_cb_write, mqtt); - } - } - - static void --fb_mqtt_cb_open(GObject *source, GAsyncResult *res, gpointer data) -+fb_mqtt_cb_open(gpointer data, PurpleSslConnection *ssl, -+ PurpleInputCondition cond) - { - FbMqtt *mqtt = data; -- FbMqttPrivate *priv; -- GSocketConnection *conn; -- GError *err = NULL; -- -- conn = g_socket_client_connect_to_host_finish(G_SOCKET_CLIENT(source), -- res, &err); -- -- if (conn == NULL) { -- fb_mqtt_take_error(mqtt, err, NULL); -- return; -- } -+ FbMqttPrivate *priv = mqtt->priv; - - fb_mqtt_timeout_clear(mqtt); -+ priv->rev = purple_input_add(priv->gsc->fd, PURPLE_INPUT_READ, -+ fb_mqtt_cb_read, mqtt); -+ g_signal_emit_by_name(mqtt, "open"); -+} - -- priv = mqtt->priv; -- priv->conn = G_IO_STREAM(conn); -- priv->input = G_BUFFERED_INPUT_STREAM(g_buffered_input_stream_new( -- g_io_stream_get_input_stream(priv->conn))); -- priv->output = purple_queued_output_stream_new( -- g_io_stream_get_output_stream(priv->conn)); -+static void -+fb_mqtt_cb_open_error(PurpleSslConnection *ssl, PurpleSslErrorType error, -+ gpointer data) -+{ -+ const gchar *str; -+ FbMqtt *mqtt = data; -+ FbMqttPrivate *priv = mqtt->priv; -+ GError *err; - -- fb_mqtt_read_packet(mqtt); -+ str = purple_ssl_strerror(error); -+ err = g_error_new_literal(FB_MQTT_SSL_ERROR, error, str); - -- g_signal_emit_by_name(mqtt, "open"); -+ /* Do not call purple_ssl_close() from the error_func */ -+ priv->gsc = NULL; -+ -+ g_signal_emit_by_name(mqtt, "error", err); -+ g_error_free(err); - } - - void -@@ -653,29 +608,20 @@ - { - FbMqttPrivate *priv; - PurpleAccount *acc; -- GSocketClient *client; -- GError *err = NULL; - - g_return_if_fail(FB_IS_MQTT(mqtt)); - priv = mqtt->priv; - - acc = purple_connection_get_account(priv->gc); - fb_mqtt_close(mqtt); -+ priv->gsc = purple_ssl_connect(acc, host, port, fb_mqtt_cb_open, -+ fb_mqtt_cb_open_error, mqtt); - -- client = purple_gio_socket_client_new(acc, &err); -- -- if (client == NULL) { -- fb_mqtt_take_error(mqtt, err, NULL); -+ if (priv->gsc == NULL) { -+ fb_mqtt_cb_open_error(NULL, 0, mqtt); - return; - } - -- priv->cancellable = g_cancellable_new(); -- -- g_socket_client_set_tls(client, TRUE); -- g_socket_client_connect_to_host_async(client, host, port, -- priv->cancellable, fb_mqtt_cb_open, mqtt); -- g_object_unref(client); -- - fb_mqtt_timeout(mqtt); - } - -@@ -711,7 +657,7 @@ - - g_return_val_if_fail(FB_IS_MQTT(mqtt), FALSE); - priv = mqtt->priv; -- connected = (priv->conn != NULL) && priv->connected; -+ connected = (priv->gsc != NULL) && priv->connected; - - if (!connected && error) { - fb_mqtt_error(mqtt, FB_MQTT_ERROR_GENERAL, -diff -r 56d191003b34 -r 5a63c26f21fd libpurple/protocols/facebook/mqtt.h ---- a/libpurple/protocols/facebook/mqtt.h Thu Sep 15 13:31:06 2016 -0500 -+++ b/libpurple/protocols/facebook/mqtt.h Wed Sep 14 14:53:12 2016 -0500 -@@ -107,6 +107,13 @@ - */ - #define FB_MQTT_ERROR fb_mqtt_error_quark() - -+/** -+ * FB_MQTT_SSL_ERROR: -+ * -+ * The #GQuark of the domain of MQTT SSL errors. -+ */ -+#define FB_MQTT_SSL_ERROR fb_mqtt_ssl_error_quark() -+ - typedef struct _FbMqtt FbMqtt; - typedef struct _FbMqttClass FbMqttClass; - typedef struct _FbMqttPrivate FbMqttPrivate; -@@ -291,6 +298,16 @@ - fb_mqtt_error_quark(void); - - /** -+ * fb_mqtt_ssl_error_quark: -+ * -+ * Gets the #GQuark of the domain of MQTT SSL errors. -+ * -+ * Returns: The #GQuark of the domain. -+ */ -+GQuark -+fb_mqtt_ssl_error_quark(void); -+ -+/** - * fb_mqtt_new: - * @gc: The #PurpleConnection. - * diff --git a/patches/05-revert-http-callbacks.patch b/patches/05-revert-http-callbacks.patch deleted file mode 100644 index c648a7dc..00000000 --- a/patches/05-revert-http-callbacks.patch +++ /dev/null @@ -1,42 +0,0 @@ -diff --git a/libpurple/http.c b/libpurple/http.c ---- a/libpurple/http.c -+++ b/libpurple/http.c -@@ -546,11 +546,7 @@ - cb_data = g_object_steal_data(source, "cb_data"); - - if (conn == NULL) { -- if (!g_error_matches(error, -- G_IO_ERROR, G_IO_ERROR_CANCELLED)) { -- cb(hs, error->message, cb_data); -- } -- -+ cb(hs, error->message, cb_data); - g_clear_error(&error); - return; - } -@@ -1223,10 +1219,8 @@ - &error); - got_anything = (len > 0); - -- if (len < 0 && (g_error_matches(error, -- G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK) || -- g_error_matches(error, -- G_IO_ERROR, G_IO_ERROR_CANCELLED))) { -+ if (len < 0 && g_error_matches(error, -+ G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK)) { - g_clear_error(&error); - return FALSE; - } -@@ -1511,10 +1505,8 @@ - &error); - } - -- if (written < 0 && (g_error_matches(error, -- G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK) || -- g_error_matches(error, -- G_IO_ERROR, G_IO_ERROR_CANCELLED))) { -+ if (written < 0 && g_error_matches(error, -+ G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK)) { - g_clear_error(&error); - return G_SOURCE_CONTINUE; - } diff --git a/patches/06-revert-purple-socket.patch b/patches/06-revert-purple-socket.patch deleted file mode 100644 index 230ccd09..00000000 --- a/patches/06-revert-purple-socket.patch +++ /dev/null @@ -1,637 +0,0 @@ -diff --git a/libpurple/purple-socket.c b/libpurple/purple-socket.c -new file mode 100644 ---- /dev/null -+++ b/libpurple/purple-socket.c -@@ -0,0 +1,410 @@ -+/* purple -+ * -+ * Purple is the legal property of its developers, whose names are too numerous -+ * to list here. Please refer to the COPYRIGHT file distributed with this -+ * source distribution. -+ * -+ * 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 2 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, write to the Free Software -+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA -+ */ -+ -+#include "purple-socket.h" -+ -+#include "internal.h" -+ -+#include "debug.h" -+#include "proxy.h" -+#include "sslconn.h" -+ -+typedef enum { -+ PURPLE_SOCKET_STATE_DISCONNECTED = 0, -+ PURPLE_SOCKET_STATE_CONNECTING, -+ PURPLE_SOCKET_STATE_CONNECTED, -+ PURPLE_SOCKET_STATE_ERROR -+} PurpleSocketState; -+ -+struct _PurpleSocket -+{ -+ PurpleConnection *gc; -+ gchar *host; -+ int port; -+ gboolean is_tls; -+ GHashTable *data; -+ -+ PurpleSocketState state; -+ -+ PurpleSslConnection *tls_connection; -+ PurpleProxyConnectData *raw_connection; -+ int fd; -+ guint inpa; -+ -+ PurpleSocketConnectCb cb; -+ gpointer cb_data; -+}; -+ -+static GHashTable *handles = NULL; -+ -+static void -+handle_add(PurpleSocket *ps) -+{ -+ PurpleConnection *gc = ps->gc; -+ GSList *l; -+ -+ l = g_hash_table_lookup(handles, gc); -+ l = g_slist_prepend(l, ps); -+ g_hash_table_insert(handles, gc, l); -+} -+ -+static void -+handle_remove(PurpleSocket *ps) -+{ -+ PurpleConnection *gc = ps->gc; -+ GSList *l; -+ -+ l = g_hash_table_lookup(handles, gc); -+ l = g_slist_remove(l, ps); -+ g_hash_table_insert(handles, gc, l); -+} -+ -+void -+_purple_socket_init(void) -+{ -+ handles = g_hash_table_new(g_direct_hash, g_direct_equal); -+} -+ -+void -+_purple_socket_uninit(void) -+{ -+ g_hash_table_destroy(handles); -+ handles = NULL; -+} -+ -+PurpleSocket * -+purple_socket_new(PurpleConnection *gc) -+{ -+ PurpleSocket *ps = g_new0(PurpleSocket, 1); -+ -+ ps->gc = gc; -+ ps->fd = -1; -+ ps->port = -1; -+ ps->data = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); -+ -+ handle_add(ps); -+ -+ return ps; -+} -+ -+PurpleConnection * -+purple_socket_get_connection(PurpleSocket *ps) -+{ -+ g_return_val_if_fail(ps != NULL, NULL); -+ -+ return ps->gc; -+} -+ -+static gboolean -+purple_socket_check_state(PurpleSocket *ps, PurpleSocketState wanted_state) -+{ -+ g_return_val_if_fail(ps != NULL, FALSE); -+ -+ if (ps->state == wanted_state) -+ return TRUE; -+ -+ purple_debug_error("socket", "invalid state: %d (should be: %d)", -+ ps->state, wanted_state); -+ ps->state = PURPLE_SOCKET_STATE_ERROR; -+ return FALSE; -+} -+ -+void -+purple_socket_set_tls(PurpleSocket *ps, gboolean is_tls) -+{ -+ g_return_if_fail(ps != NULL); -+ -+ if (!purple_socket_check_state(ps, PURPLE_SOCKET_STATE_DISCONNECTED)) -+ return; -+ -+ ps->is_tls = is_tls; -+} -+ -+void -+purple_socket_set_host(PurpleSocket *ps, const gchar *host) -+{ -+ g_return_if_fail(ps != NULL); -+ -+ if (!purple_socket_check_state(ps, PURPLE_SOCKET_STATE_DISCONNECTED)) -+ return; -+ -+ g_free(ps->host); -+ ps->host = g_strdup(host); -+} -+ -+void -+purple_socket_set_port(PurpleSocket *ps, int port) -+{ -+ g_return_if_fail(ps != NULL); -+ g_return_if_fail(port >= 0); -+ g_return_if_fail(port <= 65535); -+ -+ if (!purple_socket_check_state(ps, PURPLE_SOCKET_STATE_DISCONNECTED)) -+ return; -+ -+ ps->port = port; -+} -+ -+static void -+_purple_socket_connected_raw(gpointer _ps, gint fd, const gchar *error_message) -+{ -+ PurpleSocket *ps = _ps; -+ -+ ps->raw_connection = NULL; -+ -+ if (!purple_socket_check_state(ps, PURPLE_SOCKET_STATE_CONNECTING)) { -+ if (fd > 0) -+ close(fd); -+ ps->cb(ps, _("Invalid socket state"), ps->cb_data); -+ return; -+ } -+ -+ if (fd <= 0 || error_message != NULL) { -+ if (error_message == NULL) -+ error_message = _("Unknown error"); -+ ps->fd = -1; -+ ps->state = PURPLE_SOCKET_STATE_ERROR; -+ ps->cb(ps, error_message, ps->cb_data); -+ return; -+ } -+ -+ ps->state = PURPLE_SOCKET_STATE_CONNECTED; -+ ps->fd = fd; -+ ps->cb(ps, NULL, ps->cb_data); -+} -+ -+static void -+_purple_socket_connected_tls(gpointer _ps, PurpleSslConnection *tls_connection, -+ PurpleInputCondition cond) -+{ -+ PurpleSocket *ps = _ps; -+ -+ if (!purple_socket_check_state(ps, PURPLE_SOCKET_STATE_CONNECTING)) { -+ purple_ssl_close(tls_connection); -+ ps->tls_connection = NULL; -+ ps->cb(ps, _("Invalid socket state"), ps->cb_data); -+ return; -+ } -+ -+ if (ps->tls_connection->fd <= 0) { -+ ps->state = PURPLE_SOCKET_STATE_ERROR; -+ purple_ssl_close(tls_connection); -+ ps->tls_connection = NULL; -+ ps->cb(ps, _("Invalid file descriptor"), ps->cb_data); -+ return; -+ } -+ -+ ps->state = PURPLE_SOCKET_STATE_CONNECTED; -+ ps->fd = ps->tls_connection->fd; -+ ps->cb(ps, NULL, ps->cb_data); -+} -+ -+static void -+_purple_socket_connected_tls_error(PurpleSslConnection *ssl_connection, -+ PurpleSslErrorType error, gpointer _ps) -+{ -+ PurpleSocket *ps = _ps; -+ -+ ps->state = PURPLE_SOCKET_STATE_ERROR; -+ ps->tls_connection = NULL; -+ ps->cb(ps, purple_ssl_strerror(error), ps->cb_data); -+} -+ -+gboolean -+purple_socket_connect(PurpleSocket *ps, PurpleSocketConnectCb cb, -+ gpointer user_data) -+{ -+ PurpleAccount *account = NULL; -+ -+ g_return_val_if_fail(ps != NULL, FALSE); -+ -+ if (ps->gc && purple_connection_is_disconnecting(ps->gc)) { -+ purple_debug_error("socket", "connection is being destroyed"); -+ ps->state = PURPLE_SOCKET_STATE_ERROR; -+ return FALSE; -+ } -+ -+ if (!purple_socket_check_state(ps, PURPLE_SOCKET_STATE_DISCONNECTED)) -+ return FALSE; -+ ps->state = PURPLE_SOCKET_STATE_CONNECTING; -+ -+ if (ps->host == NULL || ps->port < 0) { -+ purple_debug_error("socket", "Host or port is not specified"); -+ ps->state = PURPLE_SOCKET_STATE_ERROR; -+ return FALSE; -+ } -+ -+ if (ps->gc != NULL) -+ account = purple_connection_get_account(ps->gc); -+ -+ ps->cb = cb; -+ ps->cb_data = user_data; -+ -+ if (ps->is_tls) { -+ ps->tls_connection = purple_ssl_connect(account, ps->host, -+ ps->port, _purple_socket_connected_tls, -+ _purple_socket_connected_tls_error, ps); -+ } else { -+ ps->raw_connection = purple_proxy_connect(ps->gc, account, -+ ps->host, ps->port, _purple_socket_connected_raw, ps); -+ } -+ -+ if (ps->tls_connection == NULL && -+ ps->raw_connection == NULL) -+ { -+ ps->state = PURPLE_SOCKET_STATE_ERROR; -+ return FALSE; -+ } -+ -+ return TRUE; -+} -+ -+gssize -+purple_socket_read(PurpleSocket *ps, guchar *buf, size_t len) -+{ -+ g_return_val_if_fail(ps != NULL, -1); -+ g_return_val_if_fail(buf != NULL, -1); -+ -+ if (!purple_socket_check_state(ps, PURPLE_SOCKET_STATE_CONNECTED)) -+ return -1; -+ -+ if (ps->is_tls) -+ return purple_ssl_read(ps->tls_connection, buf, len); -+ else -+ return read(ps->fd, buf, len); -+} -+ -+gssize -+purple_socket_write(PurpleSocket *ps, const guchar *buf, size_t len) -+{ -+ g_return_val_if_fail(ps != NULL, -1); -+ g_return_val_if_fail(buf != NULL, -1); -+ -+ if (!purple_socket_check_state(ps, PURPLE_SOCKET_STATE_CONNECTED)) -+ return -1; -+ -+ if (ps->is_tls) -+ return purple_ssl_write(ps->tls_connection, buf, len); -+ else -+ return write(ps->fd, buf, len); -+} -+ -+void -+purple_socket_watch(PurpleSocket *ps, PurpleInputCondition cond, -+ PurpleInputFunction func, gpointer user_data) -+{ -+ g_return_if_fail(ps != NULL); -+ -+ if (!purple_socket_check_state(ps, PURPLE_SOCKET_STATE_CONNECTED)) -+ return; -+ -+ if (ps->inpa > 0) -+ purple_input_remove(ps->inpa); -+ ps->inpa = 0; -+ -+ g_return_if_fail(ps->fd > 0); -+ -+ if (func != NULL) -+ ps->inpa = purple_input_add(ps->fd, cond, func, user_data); -+} -+ -+int -+purple_socket_get_fd(PurpleSocket *ps) -+{ -+ g_return_val_if_fail(ps != NULL, -1); -+ -+ if (!purple_socket_check_state(ps, PURPLE_SOCKET_STATE_CONNECTED)) -+ return -1; -+ -+ g_return_val_if_fail(ps->fd > 0, -1); -+ -+ return ps->fd; -+} -+ -+void -+purple_socket_set_data(PurpleSocket *ps, const gchar *key, gpointer data) -+{ -+ g_return_if_fail(ps != NULL); -+ g_return_if_fail(key != NULL); -+ -+ if (data == NULL) -+ g_hash_table_remove(ps->data, key); -+ else -+ g_hash_table_insert(ps->data, g_strdup(key), data); -+} -+ -+gpointer -+purple_socket_get_data(PurpleSocket *ps, const gchar *key) -+{ -+ g_return_val_if_fail(ps != NULL, NULL); -+ g_return_val_if_fail(key != NULL, NULL); -+ -+ return g_hash_table_lookup(ps->data, key); -+} -+ -+static void -+purple_socket_cancel(PurpleSocket *ps) -+{ -+ if (ps->inpa > 0) -+ purple_input_remove(ps->inpa); -+ ps->inpa = 0; -+ -+ if (ps->tls_connection != NULL) { -+ purple_ssl_close(ps->tls_connection); -+ ps->fd = -1; -+ } -+ ps->tls_connection = NULL; -+ -+ if (ps->raw_connection != NULL) -+ purple_proxy_connect_cancel(ps->raw_connection); -+ ps->raw_connection = NULL; -+ -+ if (ps->fd > 0) -+ close(ps->fd); -+ ps->fd = 0; -+} -+ -+void -+purple_socket_destroy(PurpleSocket *ps) -+{ -+ if (ps == NULL) -+ return; -+ -+ handle_remove(ps); -+ -+ purple_socket_cancel(ps); -+ -+ g_free(ps->host); -+ g_hash_table_destroy(ps->data); -+ g_free(ps); -+} -+ -+void -+_purple_socket_cancel_with_connection(PurpleConnection *gc) -+{ -+ GSList *it; -+ -+ it = g_hash_table_lookup(handles, gc); -+ for (; it; it = g_slist_next(it)) { -+ PurpleSocket *ps = it->data; -+ purple_socket_cancel(ps); -+ } -+} -diff --git a/libpurple/purple-socket.h b/libpurple/purple-socket.h -new file mode 100644 ---- /dev/null -+++ b/libpurple/purple-socket.h -@@ -0,0 +1,217 @@ -+/* purple -+ * -+ * Purple is the legal property of its developers, whose names are too numerous -+ * to list here. Please refer to the COPYRIGHT file distributed with this -+ * source distribution. -+ * -+ * 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 2 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, write to the Free Software -+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA -+ */ -+ -+#ifndef _PURPLE_SOCKET_H_ -+#define _PURPLE_SOCKET_H_ -+/** -+ * SECTION:purple-socket -+ * @section_id: libpurple-purple-socket -+ * @short_description: purple-socket.h -+ * @title: Generic Sockets -+ */ -+ -+#include "connection.h" -+ -+/** -+ * PurpleSocket: -+ * -+ * A structure holding all resources needed for the TCP connection. -+ */ -+typedef struct _PurpleSocket PurpleSocket; -+ -+/** -+ * PurpleSocketConnectCb: -+ * @ps: The socket. -+ * @error: Error message, or NULL if connection was successful. -+ * @user_data: The user data passed with callback function. -+ * -+ * A callback fired after (successfully or not) establishing a connection. -+ */ -+typedef void (*PurpleSocketConnectCb)(PurpleSocket *ps, const gchar *error, -+ gpointer user_data); -+ -+/** -+ * purple_socket_new: -+ * @gc: The connection for which the socket is needed, or NULL. -+ * -+ * Creates new, disconnected socket. -+ * -+ * Passing a PurpleConnection allows for proper proxy handling. -+ * -+ * Returns: The new socket struct. -+ */ -+PurpleSocket * -+purple_socket_new(PurpleConnection *gc); -+ -+/** -+ * purple_socket_get_connection: -+ * @ps: The socket. -+ * -+ * Gets PurpleConnection tied with specified socket. -+ * -+ * Returns: The PurpleConnection object. -+ */ -+PurpleConnection * -+purple_socket_get_connection(PurpleSocket *ps); -+ -+/** -+ * purple_socket_set_tls: -+ * @ps: The socket. -+ * @is_tls: TRUE, if TLS should be handled transparently, FALSE otherwise. -+ * -+ * Determines, if socket should handle TLS. -+ */ -+void -+purple_socket_set_tls(PurpleSocket *ps, gboolean is_tls); -+ -+/** -+ * purple_socket_set_host: -+ * @ps: The socket. -+ * @host: The connection host. -+ * -+ * Sets connection host. -+ */ -+void -+purple_socket_set_host(PurpleSocket *ps, const gchar *host); -+ -+/** -+ * purple_socket_set_port: -+ * @ps: The socket. -+ * @port: The connection port. -+ * -+ * Sets connection port. -+ */ -+void -+purple_socket_set_port(PurpleSocket *ps, int port); -+ -+/** -+ * purple_socket_connect: -+ * @ps: The socket. -+ * @cb: The function to call after establishing a connection, or on -+ * error. -+ * @user_data: The user data to be passed to callback function. -+ * -+ * Establishes a connection. -+ * -+ * Returns: TRUE on success (this doesn't mean it's connected yet), FALSE -+ * otherwise. -+ */ -+gboolean -+purple_socket_connect(PurpleSocket *ps, PurpleSocketConnectCb cb, -+ gpointer user_data); -+ -+/** -+ * purple_socket_read: -+ * @ps: The socket. -+ * @buf: The buffer to write data to. -+ * @len: The buffer size. -+ * -+ * Reads incoming data from socket. -+ * -+ * This function deals with TLS, if the socket is configured to do it. -+ * -+ * Returns: Amount of data written, or -1 on error (errno will be also be set). -+ */ -+gssize -+purple_socket_read(PurpleSocket *ps, guchar *buf, size_t len); -+ -+/** -+ * purple_socket_write: -+ * @ps: The socket. -+ * @buf: The buffer to read data from. -+ * @len: The amount of data to read and send. -+ * -+ * Sends data through socket. -+ * -+ * This function deals with TLS, if the socket is configured to do it. -+ * -+ * Returns: Amount of data sent, or -1 on error (errno will albo be set). -+ */ -+gssize -+purple_socket_write(PurpleSocket *ps, const guchar *buf, size_t len); -+ -+/** -+ * purple_socket_watch: -+ * @ps: The socket. -+ * @cond: The condition type. -+ * @func: The callback function for data, or NULL to remove any -+ * existing callbacks. -+ * @user_data: The user data to be passed to callback function. -+ * -+ * Adds an input handler for the socket. -+ * -+ * If the specified socket had input handler already registered, it will be -+ * removed. To remove any input handlers, pass an NULL handler function. -+ */ -+void -+purple_socket_watch(PurpleSocket *ps, PurpleInputCondition cond, -+ PurpleInputFunction func, gpointer user_data); -+ -+/** -+ * purple_socket_get_fd: -+ * @ps: The socket -+ * -+ * Gets underlying file descriptor for socket. -+ * -+ * It's not meant to read/write data (use purple_socket_read/ -+ * purple_socket_write), rather for watching for changes with select(). -+ * -+ * Returns: The file descriptor, or -1 on error. -+ */ -+int -+purple_socket_get_fd(PurpleSocket *ps); -+ -+/** -+ * purple_socket_set_data: -+ * @ps: The socket. -+ * @key: The unique key. -+ * @data: The data to assign, or NULL to remove. -+ * -+ * Sets extra data for a socket. -+ */ -+void -+purple_socket_set_data(PurpleSocket *ps, const gchar *key, gpointer data); -+ -+/** -+ * purple_socket_get_data: -+ * @ps: The socket. -+ * @key: The unqiue key. -+ * -+ * Returns extra data in a socket. -+ * -+ * Returns: The data associated with the key. -+ */ -+gpointer -+purple_socket_get_data(PurpleSocket *ps, const gchar *key); -+ -+/** -+ * purple_socket_destroy: -+ * @ps: The socket. -+ * -+ * Destroys the socket, closes connection and frees all resources. -+ * -+ * If file descriptor for the socket was extracted with purple_socket_get_fd and -+ * added to event loop, it have to be removed prior this. -+ */ -+void -+purple_socket_destroy(PurpleSocket *ps); -+ -+#endif /* _PURPLE_SOCKET_H_ */ diff --git a/patches/07-revert-http-gio.patch b/patches/07-revert-http-gio.patch deleted file mode 100644 index 58370c5d..00000000 --- a/patches/07-revert-http-gio.patch +++ /dev/null @@ -1,428 +0,0 @@ -diff --git a/libpurple/http.c b/libpurple/http.c ---- a/libpurple/http.c -+++ b/libpurple/http.c -@@ -27,7 +27,7 @@ - - #include "debug.h" - #include "proxy.h" --#include "purple-gio.h" -+#include "purple-socket.h" - - #define PURPLE_HTTP_URL_CREDENTIALS_CHARS "a-z0-9.,~_/*!&%?=+\\^-" - #define PURPLE_HTTP_MAX_RECV_BUFFER_LEN 10240 -@@ -51,15 +51,9 @@ - - typedef struct _PurpleHttpGzStream PurpleHttpGzStream; - --typedef void (*PurpleHttpSocketConnectCb)(PurpleHttpSocket *hs, -- const gchar *error, gpointer _hc); -- - struct _PurpleHttpSocket - { -- GSocketConnection *conn; -- GCancellable *cancellable; -- guint input_source; -- guint output_source; -+ PurpleSocket *ps; - - gboolean is_busy; - guint use_count; -@@ -175,7 +169,7 @@ - struct _PurpleHttpKeepaliveRequest - { - PurpleConnection *gc; -- PurpleHttpSocketConnectCb cb; -+ PurpleSocketConnectCb cb; - gpointer user_data; - - PurpleHttpKeepaliveHost *host; -@@ -266,7 +260,7 @@ - static PurpleHttpKeepaliveRequest * - purple_http_keepalive_pool_request(PurpleHttpKeepalivePool *pool, - PurpleConnection *gc, const gchar *host, int port, gboolean is_ssl, -- PurpleHttpSocketConnectCb cb, gpointer user_data); -+ PurpleSocketConnectCb cb, gpointer user_data); - static void - purple_http_keepalive_pool_request_cancel(PurpleHttpKeepaliveRequest *req); - static void -@@ -529,65 +523,23 @@ - return g_strdup_printf("%c:%s:%d", (is_ssl ? 'S' : 'R'), host, port); - } - --static void --purple_http_socket_connect_new_cb(GObject *source, GAsyncResult *res, -- gpointer user_data) --{ -- PurpleHttpSocket *hs = user_data; -- GSocketConnection *conn; -- PurpleHttpSocketConnectCb cb; -- gpointer cb_data; -- GError *error = NULL; -- -- conn = g_socket_client_connect_to_host_finish(G_SOCKET_CLIENT(source), -- res, &error); -- -- cb = g_object_steal_data(source, "cb"); -- cb_data = g_object_steal_data(source, "cb_data"); -- -- if (conn == NULL) { -- cb(hs, error->message, cb_data); -- g_clear_error(&error); -- return; -- } -- -- hs->conn = conn; -- -- cb(hs, NULL, cb_data); --} -- - static PurpleHttpSocket * - purple_http_socket_connect_new(PurpleConnection *gc, const gchar *host, -- int port, gboolean is_ssl, -- PurpleHttpSocketConnectCb cb, gpointer user_data) -+ int port, gboolean is_ssl, PurpleSocketConnectCb cb, gpointer user_data) - { -- PurpleHttpSocket *hs; -- GSocketClient *client; -- GError *error = NULL; -- -- client = purple_gio_socket_client_new( -- purple_connection_get_account(gc), &error); -+ PurpleHttpSocket *hs = g_new0(PurpleHttpSocket, 1); - -- if (client == NULL) { -- purple_debug_error("http", "Error connecting to '%s:%d': %s", -- host, port, error->message); -- g_clear_error(&error); -+ hs->ps = purple_socket_new(gc); -+ purple_socket_set_data(hs->ps, "hs", hs); -+ purple_socket_set_tls(hs->ps, is_ssl); -+ purple_socket_set_host(hs->ps, host); -+ purple_socket_set_port(hs->ps, port); -+ if (!purple_socket_connect(hs->ps, cb, user_data)) { -+ purple_socket_destroy(hs->ps); -+ g_free(hs); - return NULL; - } - -- hs = g_new0(PurpleHttpSocket, 1); -- hs->cancellable = g_cancellable_new(); -- -- g_socket_client_set_tls(client, is_ssl); -- g_object_set_data(G_OBJECT(client), "cb", cb); -- g_object_set_data(G_OBJECT(client), "cb_data", user_data); -- -- g_socket_client_connect_to_host_async(client, -- host, port, hs->cancellable, -- purple_http_socket_connect_new_cb, hs); -- -- g_object_unref(client); -- - if (purple_debug_is_verbose()) - purple_debug_misc("http", "new socket created: %p\n", hs); - -@@ -603,26 +555,7 @@ - if (purple_debug_is_verbose()) - purple_debug_misc("http", "destroying socket: %p\n", hs); - -- if (hs->input_source > 0) { -- g_source_remove(hs->input_source); -- hs->input_source = 0; -- } -- -- if (hs->output_source > 0) { -- g_source_remove(hs->output_source); -- hs->output_source = 0; -- } -- -- if (hs->cancellable != NULL) { -- g_cancellable_cancel(hs->cancellable); -- g_clear_object(&hs->cancellable); -- } -- -- if (hs->conn != NULL) { -- purple_gio_graceful_close(G_IO_STREAM(hs->conn), NULL, NULL); -- g_clear_object(&hs->conn); -- } -- -+ purple_socket_destroy(hs->ps); - g_free(hs); - } - -@@ -809,9 +742,10 @@ - gboolean is_graceful); - - static void _purple_http_gen_headers(PurpleHttpConnection *hc); --static gboolean _purple_http_recv_loopbody(PurpleHttpConnection *hc); --static gboolean _purple_http_recv(GObject *source, gpointer _hc); --static gboolean _purple_http_send(GObject *source, gpointer _hc); -+static gboolean _purple_http_recv_loopbody(PurpleHttpConnection *hc, gint fd); -+static void _purple_http_recv(gpointer _hc, gint fd, -+ PurpleInputCondition cond); -+static void _purple_http_send(gpointer _hc, gint fd, PurpleInputCondition cond); - - /* closes current connection (if exists), estabilishes one and proceeds with - * request */ -@@ -1204,31 +1138,21 @@ - return _purple_http_recv_body_data(hc, buf, len); - } - --static gboolean _purple_http_recv_loopbody(PurpleHttpConnection *hc) -+static gboolean _purple_http_recv_loopbody(PurpleHttpConnection *hc, gint fd) - { - int len; - gchar buf[4096]; - gboolean got_anything; -- GError *error = NULL; - -- len = g_pollable_input_stream_read_nonblocking( -- G_POLLABLE_INPUT_STREAM( -- g_io_stream_get_input_stream( -- G_IO_STREAM(hc->socket->conn))), -- buf, sizeof(buf), hc->socket->cancellable, -- &error); -+ len = purple_socket_read(hc->socket->ps, (guchar*)buf, sizeof(buf)); - got_anything = (len > 0); - -- if (len < 0 && g_error_matches(error, -- G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK)) { -- g_clear_error(&error); -+ if (len < 0 && errno == EAGAIN) - return FALSE; -- } - - if (len < 0) { - _purple_http_error(hc, _("Error reading from %s: %s"), -- hc->url->host, error->message); -- g_clear_error(&error); -+ hc->url->host, g_strerror(errno)); - return FALSE; - } - -@@ -1407,13 +1331,11 @@ - return got_anything; - } - --static gboolean _purple_http_recv(GObject *source, gpointer _hc) -+static void _purple_http_recv(gpointer _hc, gint fd, PurpleInputCondition cond) - { - PurpleHttpConnection *hc = _hc; - -- while (_purple_http_recv_loopbody(hc)); -- -- return G_SOURCE_CONTINUE; -+ while (_purple_http_recv_loopbody(hc, fd)); - } - - static void _purple_http_send_got_data(PurpleHttpConnection *hc, -@@ -1444,19 +1366,17 @@ - hc->request->contents_length = estimated_length; - } - --static gboolean _purple_http_send(GObject *source, gpointer _hc) -+static void _purple_http_send(gpointer _hc, gint fd, PurpleInputCondition cond) - { - PurpleHttpConnection *hc = _hc; - int written, write_len; - const gchar *write_from; - gboolean writing_headers; -- GError *error = NULL; -- GSource *gsource; - - /* Waiting for data. This could be written more efficiently, by removing - * (and later, adding) hs->inpa. */ - if (hc->contents_reader_requested) -- return G_SOURCE_CONTINUE; -+ return; - - _purple_http_gen_headers(hc); - -@@ -1469,7 +1389,7 @@ - hc->request_header_written; - } else if (hc->request->contents_reader) { - if (hc->contents_reader_requested) -- return G_SOURCE_CONTINUE; /* waiting for data */ -+ return; /* waiting for data */ - if (!hc->contents_reader_buffer) - hc->contents_reader_buffer = g_string_new(""); - if (hc->contents_reader_buffer->len == 0) { -@@ -1482,7 +1402,7 @@ - PURPLE_HTTP_MAX_READ_BUFFER_LEN, - hc->request->contents_reader_data, - _purple_http_send_got_data); -- return G_SOURCE_CONTINUE; -+ return; - } - write_from = hc->contents_reader_buffer->str; - write_len = hc->contents_reader_buffer->len; -@@ -1497,19 +1417,12 @@ - purple_debug_warning("http", "Nothing to write\n"); - written = 0; - } else { -- written = g_pollable_output_stream_write_nonblocking( -- G_POLLABLE_OUTPUT_STREAM( -- g_io_stream_get_output_stream( -- G_IO_STREAM(hc->socket->conn))), -- write_from, write_len, hc->socket->cancellable, -- &error); -+ written = purple_socket_write(hc->socket->ps, -+ (const guchar*)write_from, write_len); - } - -- if (written < 0 && g_error_matches(error, -- G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK)) { -- g_clear_error(&error); -- return G_SOURCE_CONTINUE; -- } -+ if (written < 0 && errno == EAGAIN) -+ return; - - if (written < 0) { - if (hc->request_header_written == 0 && -@@ -1518,22 +1431,21 @@ - purple_debug_info("http", "Keep-alive connection " - "expired (when writing), retrying...\n"); - purple_http_conn_retry(hc); -- } else { -- _purple_http_error(hc, _("Error writing to %s: %s"), -- hc->url->host, error->message); -+ return; - } - -- g_clear_error(&error); -- return G_SOURCE_CONTINUE; -+ _purple_http_error(hc, _("Error writing to %s: %s"), -+ hc->url->host, g_strerror(errno)); -+ return; - } - - if (writing_headers) { - hc->request_header_written += written; - purple_http_conn_notify_progress_watcher(hc); - if (hc->request_header_written < hc->request_header->len) -- return G_SOURCE_CONTINUE; -+ return; - if (hc->request->contents_length > 0) -- return G_SOURCE_CONTINUE; -+ return; - } else { - hc->request_contents_written += written; - purple_http_conn_notify_progress_watcher(hc); -@@ -1543,24 +1455,14 @@ - hc->request_contents_written < - (guint)hc->request->contents_length) - { -- return G_SOURCE_CONTINUE; -+ return; - } - } - - /* request is completely written, let's read the response */ - hc->is_reading = TRUE; -- gsource = g_pollable_input_stream_create_source( -- G_POLLABLE_INPUT_STREAM( -- g_io_stream_get_input_stream( -- G_IO_STREAM(hc->socket->conn))), -- NULL); -- g_source_set_callback(gsource, -- (GSourceFunc)_purple_http_recv, hc, NULL); -- hc->socket->input_source = g_source_attach(gsource, NULL); -- g_source_unref(gsource); -- -- hc->socket->output_source = 0; -- return G_SOURCE_REMOVE; -+ purple_socket_watch(hc->socket->ps, PURPLE_INPUT_READ, -+ _purple_http_recv, hc); - } - - static void _purple_http_disconnect(PurpleHttpConnection *hc, -@@ -1585,10 +1487,13 @@ - } - - static void --_purple_http_connected(PurpleHttpSocket *hs, const gchar *error, gpointer _hc) -+_purple_http_connected(PurpleSocket *ps, const gchar *error, gpointer _hc) - { -+ PurpleHttpSocket *hs = NULL; - PurpleHttpConnection *hc = _hc; -- GSource *source; -+ -+ if (ps != NULL) -+ hs = purple_socket_get_data(ps, "hs"); - - hc->socket_request = NULL; - hc->socket = hs; -@@ -1599,14 +1504,7 @@ - return; - } - -- source = g_pollable_output_stream_create_source( -- G_POLLABLE_OUTPUT_STREAM( -- g_io_stream_get_output_stream(G_IO_STREAM(hs->conn))), -- NULL); -- g_source_set_callback(source, -- (GSourceFunc)_purple_http_send, hc, NULL); -- hc->socket->output_source = g_source_attach(source, NULL); -- g_source_unref(source); -+ purple_socket_watch(ps, PURPLE_INPUT_WRITE, _purple_http_send, hc); - } - - static gboolean _purple_http_reconnect(PurpleHttpConnection *hc) -@@ -2318,7 +2216,7 @@ - static PurpleHttpKeepaliveRequest * - purple_http_keepalive_pool_request(PurpleHttpKeepalivePool *pool, - PurpleConnection *gc, const gchar *host, int port, gboolean is_ssl, -- PurpleHttpSocketConnectCb cb, gpointer user_data) -+ PurpleSocketConnectCb cb, gpointer user_data) - { - PurpleHttpKeepaliveRequest *req; - PurpleHttpKeepaliveHost *kahost; -@@ -2361,15 +2259,19 @@ - } - - static void --_purple_http_keepalive_socket_connected(PurpleHttpSocket *hs, -+_purple_http_keepalive_socket_connected(PurpleSocket *ps, - const gchar *error, gpointer _req) - { -+ PurpleHttpSocket *hs = NULL; - PurpleHttpKeepaliveRequest *req = _req; - -+ if (ps != NULL) -+ hs = purple_socket_get_data(ps, "hs"); -+ - if (hs != NULL) - hs->use_count++; - -- req->cb(hs, error, req->user_data); -+ req->cb(ps, error, req->user_data); - g_free(req); - } - -@@ -2425,7 +2327,7 @@ - - purple_http_keepalive_host_process_queue(host); - -- req->cb(hs, NULL, req->user_data); -+ req->cb(hs->ps, NULL, req->user_data); - g_free(req); - - return FALSE; -@@ -2496,16 +2398,7 @@ - if (purple_debug_is_verbose()) - purple_debug_misc("http", "releasing a socket: %p\n", hs); - -- if (hs->input_source > 0) { -- g_source_remove(hs->input_source); -- hs->input_source = 0; -- } -- -- if (hs->output_source > 0) { -- g_source_remove(hs->output_source); -- hs->output_source = 0; -- } -- -+ purple_socket_watch(hs->ps, 0, NULL, NULL); - hs->is_busy = FALSE; - host = hs->host; - diff --git a/patches/08-revert-marshaller.patch b/patches/08-revert-marshaller.patch deleted file mode 100644 index d902a80a..00000000 --- a/patches/08-revert-marshaller.patch +++ /dev/null @@ -1,367 +0,0 @@ -diff --git a/libpurple/protocols/facebook/Makefile.am b/libpurple/protocols/facebook/Makefile.am ---- a/libpurple/protocols/facebook/Makefile.am -+++ b/libpurple/protocols/facebook/Makefile.am -@@ -1,11 +1,14 @@ - EXTRA_DIST = \ -- Makefile.mingw -+ Makefile.mingw \ -+ marshaller.list - - pkgdir = @PURPLE_PLUGINDIR@ - - FACEBOOKSOURCES = \ -+ marshal.c \ -+ marshal.h \ - api.c \ - api.h \ - data.c \ - data.h \ - facebook.h \ -@@ -25,10 +28,21 @@ - ../../http.c \ - ../../http.h \ - ../../purple-socket.h \ - ../../purple-socket.c - -+CLEANFILES = \ -+ marshal.c \ -+ marshal.h -+ -+marshal.c: $(srcdir)/marshaller.list marshal.h -+ $(AM_V_GEN)echo "#include \"marshal.h\"" > $@ -+ $(AM_V_at)$(GLIB_GENMARSHAL) --prefix=fb_marshal --body $(srcdir)/marshaller.list >> $@ -+ -+marshal.h: $(srcdir)/marshaller.list -+ $(AM_V_GEN)$(GLIB_GENMARSHAL) --prefix=fb_marshal --header $(srcdir)/marshaller.list > $@ -+ - AM_CFLAGS = $(st) - - libfacebook_la_LDFLAGS = -module @PLUGIN_LDFLAGS@ - - if STATIC_FACEBOOK -diff --git a/libpurple/protocols/facebook/Makefile.mingw b/libpurple/protocols/facebook/Makefile.mingw ---- a/libpurple/protocols/facebook/Makefile.mingw -+++ b/libpurple/protocols/facebook/Makefile.mingw -@@ -42,6 +42,7 @@ - ## SOURCES, OBJECTS - ## - C_SRC = \ -+ marshal.c \ - api.c \ - data.c \ - facebook.c \ -@@ -85,11 +86,19 @@ - $(TARGET).dll: $(OBJECTS) - $(CC) -shared $(OBJECTS) $(LIB_PATHS) $(LIBS) $(DLL_LD_FLAGS) -o $(TARGET).dll - -+marshal.c: marshaller.list marshal.h -+ @echo "#include \"marshal.h\"" > $@ -+ @$(GLIB_GENMARSHAL) --prefix=fb_marshal --body marshaller.list >> $@ -+ -+marshal.h: marshaller.list -+ @$(GLIB_GENMARSHAL) --prefix=fb_marshal --header marshaller.list > $@ -+ -+ - ## - ## CLEAN RULES - ## - clean: -- rm -f $(OBJECTS) -+ rm -f $(OBJECTS) marshal.c marshal.h - rm -f $(TARGET).dll - - include $(PIDGIN_COMMON_TARGETS) -diff --git a/libpurple/protocols/facebook/api.c b/libpurple/protocols/facebook/api.c ---- a/libpurple/protocols/facebook/api.c -+++ b/libpurple/protocols/facebook/api.c -@@ -26,10 +26,11 @@ - #include "glibcompat.h" - - #include "api.h" - #include "http.h" - #include "json.h" -+#include "marshal.h" - #include "thrift.h" - #include "util.h" - - typedef struct _FbApiData FbApiData; - -@@ -289,11 +290,12 @@ - */ - g_signal_new("auth", - G_TYPE_FROM_CLASS(klass), - G_SIGNAL_ACTION, - 0, -- NULL, NULL, NULL, -+ NULL, NULL, -+ fb_marshal_VOID__VOID, - G_TYPE_NONE, - 0); - - /** - * FbApi::connect: -@@ -304,11 +306,12 @@ - */ - g_signal_new("connect", - G_TYPE_FROM_CLASS(klass), - G_SIGNAL_ACTION, - 0, -- NULL, NULL, NULL, -+ NULL, NULL, -+ fb_marshal_VOID__VOID, - G_TYPE_NONE, - 0); - - /** - * FbApi::contact: -@@ -320,11 +323,12 @@ - */ - g_signal_new("contact", - G_TYPE_FROM_CLASS(klass), - G_SIGNAL_ACTION, - 0, -- NULL, NULL, NULL, -+ NULL, NULL, -+ fb_marshal_VOID__POINTER, - G_TYPE_NONE, - 1, G_TYPE_POINTER); - - /** - * FbApi::contacts: -@@ -340,11 +344,12 @@ - */ - g_signal_new("contacts", - G_TYPE_FROM_CLASS(klass), - G_SIGNAL_ACTION, - 0, -- NULL, NULL, NULL, -+ NULL, NULL, -+ fb_marshal_VOID__POINTER_BOOLEAN, - G_TYPE_NONE, - 2, G_TYPE_POINTER, G_TYPE_BOOLEAN); - - /** - * FbApi::contacts-delta: -@@ -356,11 +361,12 @@ - */ - g_signal_new("contacts-delta", - G_TYPE_FROM_CLASS(klass), - G_SIGNAL_ACTION, - 0, -- NULL, NULL, NULL, -+ NULL, NULL, -+ fb_marshal_VOID__POINTER_POINTER, - G_TYPE_NONE, - 2, G_TYPE_POINTER, G_TYPE_POINTER); - - /** - * FbApi::error: -@@ -372,11 +378,12 @@ - */ - g_signal_new("error", - G_TYPE_FROM_CLASS(klass), - G_SIGNAL_ACTION, - 0, -- NULL, NULL, NULL, -+ NULL, NULL, -+ fb_marshal_VOID__OBJECT, - G_TYPE_NONE, - 1, G_TYPE_POINTER); - - /** - * FbApi::events: -@@ -387,11 +394,12 @@ - */ - g_signal_new("events", - G_TYPE_FROM_CLASS(klass), - G_SIGNAL_ACTION, - 0, -- NULL, NULL, NULL, -+ NULL, NULL, -+ fb_marshal_VOID__POINTER, - G_TYPE_NONE, - 1, G_TYPE_POINTER); - - /** - * FbApi::messages: -@@ -402,11 +410,12 @@ - */ - g_signal_new("messages", - G_TYPE_FROM_CLASS(klass), - G_SIGNAL_ACTION, - 0, -- NULL, NULL, NULL, -+ NULL, NULL, -+ fb_marshal_VOID__POINTER, - G_TYPE_NONE, - 1, G_TYPE_POINTER); - - /** - * FbApi::presences: -@@ -417,11 +426,12 @@ - */ - g_signal_new("presences", - G_TYPE_FROM_CLASS(klass), - G_SIGNAL_ACTION, - 0, -- NULL, NULL, NULL, -+ NULL, NULL, -+ fb_marshal_VOID__POINTER, - G_TYPE_NONE, - 1, G_TYPE_POINTER); - - /** - * FbApi::thread: -@@ -433,11 +443,12 @@ - */ - g_signal_new("thread", - G_TYPE_FROM_CLASS(klass), - G_SIGNAL_ACTION, - 0, -- NULL, NULL, NULL, -+ NULL, NULL, -+ fb_marshal_VOID__POINTER, - G_TYPE_NONE, - 1, G_TYPE_POINTER); - - /** - * FbApi::thread-create: -@@ -450,11 +461,12 @@ - */ - g_signal_new("thread-create", - G_TYPE_FROM_CLASS(klass), - G_SIGNAL_ACTION, - 0, -- NULL, NULL, NULL, -+ NULL, NULL, -+ fb_marshal_VOID__INT64, - G_TYPE_NONE, - 1, FB_TYPE_ID); - - /** - * FbApi::thread-kicked: -@@ -466,11 +478,12 @@ - */ - g_signal_new("thread-kicked", - G_TYPE_FROM_CLASS(klass), - G_SIGNAL_ACTION, - 0, -- NULL, NULL, NULL, -+ NULL, NULL, -+ fb_marshal_VOID__POINTER, - G_TYPE_NONE, - 1, G_TYPE_POINTER); - - /** - * FbApi::threads: -@@ -482,11 +495,12 @@ - */ - g_signal_new("threads", - G_TYPE_FROM_CLASS(klass), - G_SIGNAL_ACTION, - 0, -- NULL, NULL, NULL, -+ NULL, NULL, -+ fb_marshal_VOID__POINTER, - G_TYPE_NONE, - 1, G_TYPE_POINTER); - - /** - * FbApi::typing: -@@ -497,11 +511,12 @@ - */ - g_signal_new("typing", - G_TYPE_FROM_CLASS(klass), - G_SIGNAL_ACTION, - 0, -- NULL, NULL, NULL, -+ NULL, NULL, -+ fb_marshal_VOID__POINTER, - G_TYPE_NONE, - 1, G_TYPE_POINTER); - } - - static void -diff --git a/libpurple/protocols/facebook/marshaller.list b/libpurple/protocols/facebook/marshaller.list -new file mode 100644 ---- /dev/null -+++ b/libpurple/protocols/facebook/marshaller.list -@@ -0,0 +1,7 @@ -+VOID:INT64 -+VOID:OBJECT -+VOID:POINTER -+VOID:POINTER,BOOLEAN -+VOID:STRING,BOXED -+VOID:VOID -+VOID:POINTER,POINTER -diff --git a/libpurple/protocols/facebook/mqtt.c b/libpurple/protocols/facebook/mqtt.c ---- a/libpurple/protocols/facebook/mqtt.c -+++ b/libpurple/protocols/facebook/mqtt.c -@@ -28,10 +28,11 @@ - #include "account.h" - #include "eventloop.h" - #include "glibcompat.h" - #include "sslconn.h" - -+#include "marshal.h" - #include "mqtt.h" - #include "util.h" - - struct _FbMqttPrivate - { -@@ -92,11 +93,12 @@ - */ - g_signal_new("connect", - G_TYPE_FROM_CLASS(klass), - G_SIGNAL_ACTION, - 0, -- NULL, NULL, NULL, -+ NULL, NULL, -+ fb_marshal_VOID__VOID, - G_TYPE_NONE, - 0); - - /** - * FbMqtt::error: -@@ -108,11 +110,12 @@ - */ - g_signal_new("error", - G_TYPE_FROM_CLASS(klass), - G_SIGNAL_ACTION, - 0, -- NULL, NULL, NULL, -+ NULL, NULL, -+ fb_marshal_VOID__OBJECT, - G_TYPE_NONE, - 1, G_TYPE_ERROR); - - /** - * FbMqtt::open: -@@ -124,11 +127,12 @@ - */ - g_signal_new("open", - G_TYPE_FROM_CLASS(klass), - G_SIGNAL_ACTION, - 0, -- NULL, NULL, NULL, -+ NULL, NULL, -+ fb_marshal_VOID__VOID, - G_TYPE_NONE, - 0); - - /** - * FbMqtt::publish: -@@ -140,11 +144,12 @@ - */ - g_signal_new("publish", - G_TYPE_FROM_CLASS(klass), - G_SIGNAL_ACTION, - 0, -- NULL, NULL, NULL, -+ NULL, NULL, -+ fb_marshal_VOID__STRING_BOXED, - G_TYPE_NONE, - 2, G_TYPE_STRING, G_TYPE_BYTE_ARRAY); - } - - static void diff --git a/patches/09-revert-http-zlib.patch b/patches/09-revert-http-zlib.patch deleted file mode 100644 index 90e49fd6..00000000 --- a/patches/09-revert-http-zlib.patch +++ /dev/null @@ -1,148 +0,0 @@ -diff -Naur a/libpurple/http.c b/libpurple/http.c ---- a/libpurple/http.c -+++ b/libpurple/http.c -@@ -29,6 +29,11 @@ - #include "proxy.h" - #include "purple-socket.h" - -+#include -+#ifndef z_const -+#define z_const -+#endif -+ - #define PURPLE_HTTP_URL_CREDENTIALS_CHARS "a-z0-9.,~_/*!&%?=+\\^-" - #define PURPLE_HTTP_MAX_RECV_BUFFER_LEN 10240 - #define PURPLE_HTTP_MAX_READ_BUFFER_LEN 10240 -@@ -212,7 +217,7 @@ - struct _PurpleHttpGzStream - { - gboolean failed; -- GZlibDecompressor *decompressor; -+ z_stream zs; - gsize max_output; - gsize decompressed; - GString *pending; -@@ -360,14 +365,19 @@ - purple_http_gz_new(gsize max_output, gboolean is_deflate) - { - PurpleHttpGzStream *gzs = g_new0(PurpleHttpGzStream, 1); -- GZlibCompressorFormat format; -+ int windowBits; - - if (is_deflate) -- format = G_ZLIB_COMPRESSOR_FORMAT_RAW; -+ windowBits = -MAX_WBITS; - else /* is gzip */ -- format = G_ZLIB_COMPRESSOR_FORMAT_GZIP; -+ windowBits = MAX_WBITS + 32; -+ -+ if (inflateInit2(&gzs->zs, windowBits) != Z_OK) { -+ purple_debug_error("http", "Cannot initialize zlib stream\n"); -+ g_free(gzs); -+ return NULL; -+ } - -- gzs->decompressor = g_zlib_decompressor_new(format); - gzs->max_output = max_output; - - return gzs; -@@ -379,6 +389,7 @@ - const gchar *compressed_buff; - gsize compressed_len; - GString *ret; -+ z_stream *zs; - - g_return_val_if_fail(gzs != NULL, NULL); - g_return_val_if_fail(buf != NULL, NULL); -@@ -386,6 +397,8 @@ - if (gzs->failed) - return NULL; - -+ zs = &gzs->zs; -+ - if (gzs->pending) { - g_string_append_len(gzs->pending, buf, len); - compressed_buff = gzs->pending->str; -@@ -395,26 +408,22 @@ - compressed_len = len; - } - -+ zs->next_in = (z_const Bytef*)compressed_buff; -+ zs->avail_in = compressed_len; -+ - ret = g_string_new(NULL); -- while (compressed_len > 0) { -- GConverterResult gzres; -+ while (zs->avail_in > 0) { -+ int gzres; - gchar decompressed_buff[PURPLE_HTTP_GZ_BUFF_LEN]; -- gsize decompressed_len = 0; -- gsize bytes_read = 0; -- GError *error = NULL; -- -- gzres = g_converter_convert(G_CONVERTER(gzs->decompressor), -- compressed_buff, compressed_len, -- decompressed_buff, sizeof(decompressed_buff), -- G_CONVERTER_NO_FLAGS, -- &bytes_read, -- &decompressed_len, -- &error); -+ gsize decompressed_len; - -- compressed_buff += bytes_read; -- compressed_len -= bytes_read; -+ zs->next_out = (Bytef*)decompressed_buff; -+ zs->avail_out = sizeof(decompressed_buff); -+ decompressed_len = zs->avail_out = sizeof(decompressed_buff); -+ gzres = inflate(zs, Z_FULL_FLUSH); -+ decompressed_len -= zs->avail_out; - -- if (gzres == G_CONVERTER_CONVERTED || G_CONVERTER_FINISHED) { -+ if (gzres == Z_OK || gzres == Z_STREAM_END) { - if (decompressed_len == 0) - break; - if (gzs->decompressed + decompressed_len >= -@@ -424,18 +433,17 @@ - " decompressed data is reached\n"); - decompressed_len = gzs->max_output - - gzs->decompressed; -- gzres = G_CONVERTER_FINISHED; -+ gzres = Z_STREAM_END; - } - gzs->decompressed += decompressed_len; - g_string_append_len(ret, decompressed_buff, - decompressed_len); -- if (gzres == G_CONVERTER_FINISHED) -+ if (gzres == Z_STREAM_END) - break; - } else { - purple_debug_error("http", - "Decompression failed (%d): %s\n", gzres, -- error->message); -- g_clear_error(&error); -+ zs->msg); - gzs->failed = TRUE; - return NULL; - } -@@ -446,9 +454,9 @@ - gzs->pending = NULL; - } - -- if (compressed_len > 0) { -- gzs->pending = g_string_new_len(compressed_buff, -- compressed_len); -+ if (zs->avail_in > 0) { -+ gzs->pending = g_string_new_len((gchar*)zs->next_in, -+ zs->avail_in); - } - - return ret; -@@ -459,7 +467,7 @@ - { - if (gzs == NULL) - return; -- g_object_unref(gzs->decompressor); -+ inflateEnd(&gzs->zs); - if (gzs->pending) - g_string_free(gzs->pending, TRUE); - g_free(gzs); diff --git a/patches/10-glib-compiled-with-debug.patch b/patches/10-glib-compiled-with-debug.patch deleted file mode 100644 index 8c92d6c9..00000000 --- a/patches/10-glib-compiled-with-debug.patch +++ /dev/null @@ -1,43 +0,0 @@ -From 99e31624bf9e88b3002e05514db904d5aad35db6 Mon Sep 17 00:00:00 2001 -From: dequis -Date: Sat, 25 Feb 2017 03:14:57 -0300 -Subject: [PATCH] Fix crash when the error signal is raised and glib has - G_ENABLE_DEBUG - -If glib is built with --enable-debug=yes, it will have G_ENABLE_DEBUG -set internally - -This flag changes the g_marshal_value_peek_object() macro to use -g_value_get_object(), which performs sanity checks, instead of doing a -direct offset access. - -The definition of our error signal was wrong, using a marshaller with -a GObject in the first parameter but setting the type of the GValue to -G_TYPE_ERROR. Since we have no marshaller for G_TYPE_ERROR, and that all -of those are functionally equivalent (and in fact use the exact same -code in non-debug builds), I went with POINTER for both sides. - -The actual crash happened because of a g_return_val_if_fail() in the -sanity checks which made it return NULL after throwing a warning like -this: - - g_value_get_object: assertion 'G_VALUE_HOLDS_OBJECT (value)' failed - -This was reported by a gentoo user who had the debug use flag. Thanks! ---- - facebook/facebook-api.c | 4 ++-- - 1 file changed, 2 insertions(+), 2 deletions(-) - -diff --git a/libpurple/protocols/facebook/api.c b/libpurple/protocols/facebook/api.c -index a94f9af..eb0e8b4 100644 ---- a/libpurple/protocols/facebook/api.c -+++ b/libpurple/protocols/facebook/api.c -@@ -381,7 +381,7 @@ - G_SIGNAL_ACTION, - 0, - NULL, NULL, -- fb_marshal_VOID__OBJECT, -+ fb_marshal_VOID__POINTER, - G_TYPE_NONE, - 1, G_TYPE_POINTER); - diff --git a/patches/11-contacts.patch b/patches/11-contacts.patch deleted file mode 100644 index bcebf89f..00000000 --- a/patches/11-contacts.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- a/libpurple/protocols/facebook/api.c -+++ b/libpurple/protocols/facebook/api.c -@@ -2424,7 +2424,7 @@ - priv->contacts_delta = g_strdup(is_delta ? cursor : delta_cursor); - } - -- if (users) { -+ if (users || (complete && !is_delta)) { - g_signal_emit_by_name(api, "contacts", users, complete); - } - diff --git a/patches/12-glib-error-cast-bug-2-electric-boogaloo.patch b/patches/12-glib-error-cast-bug-2-electric-boogaloo.patch deleted file mode 100644 index 08d4476d..00000000 --- a/patches/12-glib-error-cast-bug-2-electric-boogaloo.patch +++ /dev/null @@ -1,14 +0,0 @@ ---- a/libpurple/protocols/facebook/mqtt.c -+++ b/libpurple/protocols/facebook/mqtt.c -@@ -113,9 +113,9 @@ - G_SIGNAL_ACTION, - 0, - NULL, NULL, -- fb_marshal_VOID__OBJECT, -+ fb_marshal_VOID__POINTER, - G_TYPE_NONE, -- 1, G_TYPE_ERROR); -+ 1, G_TYPE_POINTER); - - /** - * FbMqtt::open: diff --git a/patches/13-async-sockets-are-hard.patch b/patches/13-async-sockets-are-hard.patch deleted file mode 100644 index 65752943..00000000 --- a/patches/13-async-sockets-are-hard.patch +++ /dev/null @@ -1,51 +0,0 @@ ---- a/libpurple/protocols/facebook/mqtt.c -+++ b/libpurple/protocols/facebook/mqtt.c -@@ -361,26 +361,33 @@ - g_byte_array_set_size(priv->rbuf, 0); - - res = purple_ssl_read(priv->gsc, &byte, sizeof byte); -- g_byte_array_append(priv->rbuf, &byte, sizeof byte); - -- if (res != sizeof byte) { -+ if (res < 0 && errno == EAGAIN) { -+ return; -+ } else if (res != 1) { - fb_mqtt_error(mqtt, FB_MQTT_ERROR_GENERAL, - _("Failed to read fixed header")); - return; - } - -+ g_byte_array_append(priv->rbuf, &byte, sizeof byte); -+ - mult = 1; - - do { - res = purple_ssl_read(priv->gsc, &byte, sizeof byte); -- g_byte_array_append(priv->rbuf, &byte, sizeof byte); - -- if (res != sizeof byte) { -+ /* TODO: this case isn't handled yet */ -+ if (0 && res < 0 && errno == EAGAIN) { -+ return; -+ } else if (res != 1) { - fb_mqtt_error(mqtt, FB_MQTT_ERROR_GENERAL, - _("Failed to read packet size")); - return; - } - -+ g_byte_array_append(priv->rbuf, &byte, sizeof byte); -+ - priv->remz += (byte & 127) * mult; - mult *= 128; - } while ((byte & 128) != 0); -@@ -390,7 +397,9 @@ - size = MIN(priv->remz, sizeof buf); - rize = purple_ssl_read(priv->gsc, buf, size); - -- if (rize < 1) { -+ if (rize < 0 && errno == EAGAIN) { -+ return; -+ } else if (rize < 1) { - fb_mqtt_error(mqtt, FB_MQTT_ERROR_GENERAL, - _("Failed to read packet data")); - return; diff --git a/patches/14-the-future.patch b/patches/14-the-future.patch deleted file mode 100644 index 6a8c7949..00000000 --- a/patches/14-the-future.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- old/libpurple/protocols/facebook/api.h 2019-01-07 10:30:23.164785430 +0100 -+++ pidgin/libpurple/protocols/facebook/api.h 2019-01-07 10:35:18.494967061 +0100 -@@ -104,7 +104,7 @@ - * server started checking this. - */ - --#define FB_ORCA_AGENT "[FBAN/Orca-Android;FBAV/109.0.0.17.70;FBPN/com.facebook.orca;FBLC/en_US;FBBV/52182662]" -+#define FB_ORCA_AGENT "[FBAN/Orca-Android;FBAV/192.0.0.31.101;FBPN/com.facebook.orca;FBLC/en_US;FBBV/52182662]" - - /** - * FB_API_AGENT: diff --git a/patches/15-glib-deprecate-g_type_class_add_private.patch b/patches/15-glib-deprecate-g_type_class_add_private.patch deleted file mode 100644 index c8709c16..00000000 --- a/patches/15-glib-deprecate-g_type_class_add_private.patch +++ /dev/null @@ -1,99 +0,0 @@ -Index: libpurple/protocols/facebook/api.c -=================================================================== ---- a/libpurple/protocols/facebook/api.c -+++ b/libpurple/protocols/facebook/api.c -@@ -92,7 +92,7 @@ fb_api_sticker(FbApi *api, FbId sid, FbA - void - fb_api_contacts_delta(FbApi *api, const gchar *delta_cursor); - --G_DEFINE_TYPE(FbApi, fb_api, G_TYPE_OBJECT); -+G_DEFINE_TYPE_WITH_CODE(FbApi, fb_api, G_TYPE_OBJECT, G_ADD_PRIVATE(FbApi)); - - static void - fb_api_set_property(GObject *obj, guint prop, const GValue *val, -Index: libpurple/protocols/facebook/data.c -=================================================================== ---- a/libpurple/protocols/facebook/data.c -+++ b/libpurple/protocols/facebook/data.c -@@ -59,8 +59,8 @@ static const gchar *fb_props_strs[] = { - "token" - }; - --G_DEFINE_TYPE(FbData, fb_data, G_TYPE_OBJECT); --G_DEFINE_TYPE(FbDataImage, fb_data_image, G_TYPE_OBJECT); -+G_DEFINE_TYPE_WITH_CODE(FbData, fb_data, G_TYPE_OBJECT, G_ADD_PRIVATE(FbData)); -+G_DEFINE_TYPE_WITH_CODE(FbDataImage, fb_data_image, G_TYPE_OBJECT, G_ADD_PRIVATE(FbDataImage)); - - static void - fb_data_dispose(GObject *obj) -Index: libpurple/protocols/facebook/json.c -=================================================================== ---- a/libpurple/protocols/facebook/json.c -+++ b/libpurple/protocols/facebook/json.c -@@ -25,6 +25,7 @@ - #include - - #include "json.h" -+#include "glibcompat.h" - #include "util.h" - - typedef struct _FbJsonValue FbJsonValue; -@@ -50,7 +51,7 @@ struct _FbJsonValuesPrivate - GError *error; - }; - --G_DEFINE_TYPE(FbJsonValues, fb_json_values, G_TYPE_OBJECT); -+G_DEFINE_TYPE_WITH_CODE(FbJsonValues, fb_json_values, G_TYPE_OBJECT, G_ADD_PRIVATE(FbJsonValues)); - - static void - fb_json_values_dispose(GObject *obj) -Index: libpurple/protocols/facebook/mqtt.c -=================================================================== ---- a/libpurple/protocols/facebook/mqtt.c -+++ b/libpurple/protocols/facebook/mqtt.c -@@ -62,8 +62,8 @@ struct _FbMqttMessagePrivate - gboolean local; - }; - --G_DEFINE_TYPE(FbMqtt, fb_mqtt, G_TYPE_OBJECT); --G_DEFINE_TYPE(FbMqttMessage, fb_mqtt_message, G_TYPE_OBJECT); -+G_DEFINE_TYPE_WITH_CODE(FbMqtt, fb_mqtt, G_TYPE_OBJECT, G_ADD_PRIVATE(FbMqtt)); -+G_DEFINE_TYPE_WITH_CODE(FbMqttMessage, fb_mqtt_message, G_TYPE_OBJECT, G_ADD_PRIVATE(FbMqttMessage)); - - static void - fb_mqtt_dispose(GObject *obj) -Index: libpurple/protocols/facebook/thrift.c -=================================================================== ---- a/libpurple/protocols/facebook/thrift.c -+++ b/libpurple/protocols/facebook/thrift.c -@@ -21,6 +21,7 @@ - - #include - -+#include "glibcompat.h" - #include "thrift.h" - - struct _FbThriftPrivate -@@ -32,7 +33,7 @@ struct _FbThriftPrivate - guint lastbool; - }; - --G_DEFINE_TYPE(FbThrift, fb_thrift, G_TYPE_OBJECT); -+G_DEFINE_TYPE_WITH_CODE(FbThrift, fb_thrift, G_TYPE_OBJECT, G_ADD_PRIVATE(FbThrift)); - - static void - fb_thrift_dispose(GObject *obj) -Index: libpurple/glibcompat.h -=================================================================== ---- a/libpurple/glibcompat.h -+++ b/libpurple/glibcompat.h -@@ -110,6 +110,9 @@ static inline void g_queue_free_full(GQu - g_assertion_message (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \ - "'" #expr "' should be NULL"); \ - } G_STMT_END -+#define G_ADD_PRIVATE(TypeName) G_STMT_START { } G_STMT_END -+#else -+#define g_type_class_add_private(k,s) G_STMT_START { } G_STMT_END - #endif - - #if !GLIB_CHECK_VERSION(2, 40, 0) diff --git a/patches/16-fix-duplicate-decl-specifier.patch b/patches/16-fix-duplicate-decl-specifier.patch deleted file mode 100644 index d3a68e98..00000000 --- a/patches/16-fix-duplicate-decl-specifier.patch +++ /dev/null @@ -1,13 +0,0 @@ -Index: libpurple/protocols/facebook/http.c -=================================================================== ---- a/libpurple/protocols/facebook/http.c -+++ b/libpurple/protocols/facebook/http.c -@@ -381,7 +381,7 @@ fb_http_urlcmp(const gchar *url1, const - PurpleHttpURL *purl1; - PurpleHttpURL *purl2; - -- static const const gchar * (*funcs[]) (const PurpleHttpURL *url) = { -+ static const gchar * (*funcs[]) (const PurpleHttpURL *url) = { - /* Always first so it can be skipped */ - purple_http_url_get_protocol, - diff --git a/patches/17-this-build-system-sucks.patch b/patches/17-this-build-system-sucks.patch deleted file mode 100644 index eb75a267..00000000 --- a/patches/17-this-build-system-sucks.patch +++ /dev/null @@ -1,12 +0,0 @@ -diff -Naur pidgin/libpurple/protocols/facebook/api.h pidgin2/libpurple/protocols/facebook/api.h ---- a/libpurple/protocols/facebook/api.h -+++ b/libpurple/protocols/facebook/api.h -@@ -111,7 +111,7 @@ - * - * The HTTP User-Agent header. - */ --#define FB_API_AGENT "Facebook plugin / Purple / 0.9.5 " FB_ORCA_AGENT -+#define FB_API_AGENT "Facebook plugin / Purple / " PACKAGE_VERSION " " FB_ORCA_AGENT - - /** - * FB_API_MQTT_AGENT diff --git a/patches/18-fix-thrift-stop-failure.patch b/patches/18-fix-thrift-stop-failure.patch deleted file mode 100644 index a8af8e42..00000000 --- a/patches/18-fix-thrift-stop-failure.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- a/libpurple/protocols/facebook/api.c -+++ b/libpurple/protocols/facebook/api.c -@@ -1847,7 +1847,7 @@ - fb_util_debug_info("Presence: %" FB_ID_FORMAT " (%d)", - i64, i32 != 0); - -- while (id <= 5) { -+ while (id <= 6) { - if (fb_thrift_read_isstop(thft)) { - break; - } diff --git a/patches/19-fix-taNewMessage-bug.patch b/patches/19-fix-taNewMessage-bug.patch deleted file mode 100644 index c2aaa9f6..00000000 --- a/patches/19-fix-taNewMessage-bug.patch +++ /dev/null @@ -1,64 +0,0 @@ ---- a/libpurple/protocols/facebook/api.c -+++ b/libpurple/protocols/facebook/api.c -@@ -1502,6 +1502,23 @@ - fb_api_cb_publish_ms_event(FbApi *api, JsonNode *root, GSList *events, FbApiEventType type, GError **error); - - static void -+fb_api_cb_publish_mst(FbThrift *thft, GError **error) -+{ -+ if (fb_thrift_read_isstop(thft)) { -+ FB_API_TCHK(fb_thrift_read_stop(thft)); -+ } else { -+ FbThriftType type; -+ gint16 id; -+ -+ FB_API_TCHK(fb_thrift_read_field(thft, &type, &id, 0)); -+ FB_API_TCHK(type == FB_THRIFT_TYPE_STRING); -+ // FB_API_TCHK(id == 2); -+ FB_API_TCHK(fb_thrift_read_str(thft, NULL)); -+ FB_API_TCHK(fb_thrift_read_stop(thft)); -+ } -+} -+ -+static void - fb_api_cb_publish_ms(FbApi *api, GByteArray *pload) - { - const gchar *data; -@@ -1531,10 +1548,14 @@ - - /* Read identifier string (for Facebook employees) */ - thft = fb_thrift_new(pload, 0); -- fb_thrift_read_str(thft, NULL); -+ fb_api_cb_publish_mst(thft, &err); - size = fb_thrift_get_pos(thft); - g_object_unref(thft); - -+ FB_API_ERROR_EMIT(api, err, -+ return; -+ ); -+ - g_return_if_fail(size < pload->len); - data = (gchar *) pload->data + size; - size = pload->len - size; -@@ -1844,8 +1865,8 @@ - pres->active = i32 != 0; - *press = g_slist_prepend(*press, pres); - -- fb_util_debug_info("Presence: %" FB_ID_FORMAT " (%d)", -- i64, i32 != 0); -+ fb_util_debug_info("Presence: %" FB_ID_FORMAT " (%d) id: %d", -+ i64, i32 != 0, id); - - while (id <= 6) { - if (fb_thrift_read_isstop(thft)) { -@@ -1894,7 +1915,9 @@ - } - - /* Read the field stop */ -- FB_API_TCHK(fb_thrift_read_stop(thft)); -+ if (fb_thrift_read_isstop(thft)) { -+ FB_API_TCHK(fb_thrift_read_stop(thft)); -+ } - } - - static void diff --git a/patches/20-bump-FB_ORCA_AGENT-version.patch b/patches/20-bump-FB_ORCA_AGENT-version.patch deleted file mode 100644 index f237bc31..00000000 --- a/patches/20-bump-FB_ORCA_AGENT-version.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- a/libpurple/protocols/facebook/api.h -+++ b/libpurple/protocols/facebook/api.h -@@ -97,7 +97,7 @@ - * server started checking this. - */ - --#define FB_ORCA_AGENT "[FBAN/Orca-Android;FBAV/192.0.0.31.101;FBPN/com.facebook.orca;FBLC/en_US;FBBV/52182662]" -+#define FB_ORCA_AGENT "[FBAN/Orca-Android;FBAV/537.0.0.31.101;FBPN/com.facebook.orca;FBLC/en_US;FBBV/52182662]" - - /** - * FB_API_AGENT: diff --git a/purple2compat/glibcompat.h b/purple2compat/glibcompat.h new file mode 100644 index 00000000..03826511 --- /dev/null +++ b/purple2compat/glibcompat.h @@ -0,0 +1,139 @@ +/* pidgin + * + * Pidgin is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#ifndef _GLIBCOMPAT_H_ +#define _GLIBCOMPAT_H_ +/* + * SECTION:glibcompat + * @section_id: libpurple-glibcompat + * @short_description: glibcompat.h + * @title: GLib version-dependent definitions + * + * This file is internal to libpurple. Do not use! + * Also, any public API should not depend on this file. + */ + +#include + +/* glib's definition of g_stat+GStatBuf seems to be broken on mingw64-w32 (and + * possibly other 32-bit windows), so instead of relying on it, + * we'll define our own. + */ +#if defined(_WIN32) && !defined(_MSC_VER) && !defined(_WIN64) +# include +typedef struct _stat GStatBufW32; +static inline int +purple_g_stat(const gchar *filename, GStatBufW32 *buf) +{ + return g_stat(filename, (GStatBuf*)buf); +} +# define GStatBuf GStatBufW32 +# define g_stat purple_g_stat +#endif + + +#ifdef __clang__ + +#undef G_GNUC_BEGIN_IGNORE_DEPRECATIONS +#define G_GNUC_BEGIN_IGNORE_DEPRECATIONS \ + _Pragma ("clang diagnostic push") \ + _Pragma ("clang diagnostic ignored \"-Wdeprecated-declarations\"") + +#undef G_GNUC_END_IGNORE_DEPRECATIONS +#define G_GNUC_END_IGNORE_DEPRECATIONS \ + _Pragma ("clang diagnostic pop") + +#endif /* __clang__ */ + + +#if !GLIB_CHECK_VERSION(2, 44, 0) +#define G_IO_ERROR_CONNECTION_CLOSED G_IO_ERROR_BROKEN_PIPE +#endif + +/****************************************************************************** + * g_assert_* macros + *****************************************************************************/ + +#if !GLIB_CHECK_VERSION(2, 32, 0) +static inline GByteArray * g_byte_array_new_take(guint8 *data, gsize len) +{ + GByteArray *array; + + array = g_byte_array_new(); + g_byte_array_append(array, data, len); + g_free(data); + + return array; +} + +static inline void g_queue_free_full(GQueue *queue, GDestroyNotify free_func) +{ + g_queue_foreach(queue, (GFunc)free_func, NULL); + g_queue_free(queue); +} +#endif + +#if !GLIB_CHECK_VERSION(2, 30, 0) +#define G_VALUE_INIT {0, {{0}}} +#endif + +#if !GLIB_CHECK_VERSION(2, 38, 0) +#define g_assert_true(expr) G_STMT_START { \ + if G_LIKELY (expr) ; else \ + g_assertion_message (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \ + "'" #expr "' should be TRUE"); \ + } G_STMT_END +#define g_assert_false(expr) G_STMT_START { \ + if G_LIKELY (!(expr)) ; else \ + g_assertion_message (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \ + "'" #expr "' should be FALSE"); \ + } G_STMT_END +#define g_assert_null(expr) G_STMT_START { if G_LIKELY ((expr) == NULL) ; else \ + g_assertion_message (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \ + "'" #expr "' should be NULL"); \ + } G_STMT_END +#define G_ADD_PRIVATE(TypeName) G_STMT_START { } G_STMT_END +#else +#define g_type_class_add_private(k,s) G_STMT_START { } G_STMT_END +#endif + +#if !GLIB_CHECK_VERSION(2, 40, 0) +#define g_assert_nonnull(expr) G_STMT_START { \ + if G_LIKELY ((expr) != NULL) ; else \ + g_assertion_message (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \ + "'" #expr "' should not be NULL"); \ + } G_STMT_END +#endif + +#if !GLIB_CHECK_VERSION(2, 46, 0) +#define g_assert_cmpmem(m1, l1, m2, l2) G_STMT_START {\ + gconstpointer __m1 = m1, __m2 = m2; \ + int __l1 = l1, __l2 = l2; \ + if (__l1 != __l2) \ + g_assertion_message_cmpnum (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \ + #l1 " (len(" #m1 ")) == " #l2 " (len(" #m2 "))", __l1, "==", __l2, 'i'); \ + else if (memcmp (__m1, __m2, __l1) != 0) \ + g_assertion_message (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \ + "assertion failed (" #m1 " == " #m2 ")"); \ + } G_STMT_END +#endif + +#endif /* _GLIBCOMPAT_H_ */ diff --git a/purple2compat/http.c b/purple2compat/http.c new file mode 100644 index 00000000..75e390ab --- /dev/null +++ b/purple2compat/http.c @@ -0,0 +1,3299 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#include "http.h" + +#include "internal.h" +#include "glibcompat.h" + + +#include "debug.h" +#include "proxy.h" +#include "purple-socket.h" + +#include +#ifndef z_const +#define z_const +#endif + +#define PURPLE_HTTP_URL_CREDENTIALS_CHARS "a-z0-9.,~_/*!&%?=+\\^-" +#define PURPLE_HTTP_MAX_RECV_BUFFER_LEN 10240 +#define PURPLE_HTTP_MAX_READ_BUFFER_LEN 10240 +#define PURPLE_HTTP_GZ_BUFF_LEN 1024 + +#define PURPLE_HTTP_REQUEST_DEFAULT_MAX_REDIRECTS 20 +#define PURPLE_HTTP_REQUEST_DEFAULT_TIMEOUT 30 +#define PURPLE_HTTP_REQUEST_DEFAULT_MAX_LENGTH 1048576 +#define PURPLE_HTTP_REQUEST_HARD_MAX_LENGTH G_MAXINT32-1 + +#define PURPLE_HTTP_PROGRESS_WATCHER_DEFAULT_INTERVAL 250000 + +typedef struct _PurpleHttpSocket PurpleHttpSocket; + +typedef struct _PurpleHttpHeaders PurpleHttpHeaders; + +typedef struct _PurpleHttpKeepaliveHost PurpleHttpKeepaliveHost; + +typedef struct _PurpleHttpKeepaliveRequest PurpleHttpKeepaliveRequest; + +typedef struct _PurpleHttpGzStream PurpleHttpGzStream; + +struct _PurpleHttpSocket +{ + PurpleSocket *ps; + + gboolean is_busy; + guint use_count; + PurpleHttpKeepaliveHost *host; +}; + +struct _PurpleHttpRequest +{ + int ref_count; + + gchar *url; + gchar *method; + PurpleHttpHeaders *headers; + PurpleHttpCookieJar *cookie_jar; + PurpleHttpKeepalivePool *keepalive_pool; + + gchar *contents; + int contents_length; + PurpleHttpContentReader contents_reader; + gpointer contents_reader_data; + PurpleHttpContentWriter response_writer; + gpointer response_writer_data; + + int timeout; + int max_redirects; + gboolean http11; + guint max_length; +}; + +struct _PurpleHttpConnection +{ + PurpleConnection *gc; + PurpleHttpCallback callback; + gpointer user_data; + gboolean is_reading; + gboolean is_keepalive; + gboolean is_cancelling; + + PurpleHttpURL *url; + PurpleHttpRequest *request; + PurpleHttpResponse *response; + + PurpleHttpKeepaliveRequest *socket_request; + PurpleHttpConnectionSet *connection_set; + PurpleHttpSocket *socket; + GString *request_header; + guint request_header_written, request_contents_written; + gboolean main_header_got, headers_got; + GString *response_buffer; + PurpleHttpGzStream *gz_stream; + + GString *contents_reader_buffer; + gboolean contents_reader_requested; + + int redirects_count; + + int length_expected; + guint length_got, length_got_decompressed; + + gboolean is_chunked, in_chunk, chunks_done; + int chunk_length, chunk_got; + + GList *link_global, *link_gc; + + guint timeout_handle; + + PurpleHttpProgressWatcher watcher; + gpointer watcher_user_data; + guint watcher_interval_threshold; + gint64 watcher_last_call; + guint watcher_delayed_handle; +}; + +struct _PurpleHttpResponse +{ + int code; + gchar *error; + + GString *contents; + PurpleHttpHeaders *headers; +}; + +struct _PurpleHttpURL +{ + gchar *protocol; + gchar *username; + gchar *password; + gchar *host; + int port; + gchar *path; + gchar *fragment; +}; + +struct _PurpleHttpHeaders +{ + GList *list; + GHashTable *by_name; +}; + +typedef struct +{ + time_t expires; + gchar *value; +} PurpleHttpCookie; + +struct _PurpleHttpCookieJar +{ + int ref_count; + + GHashTable *tab; +}; + +struct _PurpleHttpKeepaliveRequest +{ + PurpleConnection *gc; + PurpleSocketConnectCb cb; + gpointer user_data; + + PurpleHttpKeepaliveHost *host; + PurpleHttpSocket *hs; +}; + +struct _PurpleHttpKeepaliveHost +{ + PurpleHttpKeepalivePool *pool; + + gchar *host; + int port; + gboolean is_ssl; + + GSList *sockets; /* list of PurpleHttpSocket */ + + GSList *queue; /* list of PurpleHttpKeepaliveRequest */ + guint process_queue_timeout; +}; + +struct _PurpleHttpKeepalivePool +{ + gboolean is_destroying; + + int ref_count; + + guint limit_per_host; + + /* key: purple_http_socket_hash, value: PurpleHttpKeepaliveHost */ + GHashTable *by_hash; +}; + +struct _PurpleHttpConnectionSet +{ + gboolean is_destroying; + + GHashTable *connections; +}; + +struct _PurpleHttpGzStream +{ + gboolean failed; + z_stream zs; + gsize max_output; + gsize decompressed; + GString *pending; +}; + +struct _ntlm_type1_message { + guint8 protocol[8]; /* 'N', 'T', 'L', 'M', 'S', 'S', 'P', '\0' */ + guint32 type; /* 0x00000001 */ + guint32 flags; /* 0x0000b203 */ + + guint16 dom_len1; /* domain string length */ + guint16 dom_len2; /* domain string length */ + guint32 dom_off; /* domain string offset */ + + guint16 host_len1; /* host string length */ + guint16 host_len2; /* host string length */ + guint32 host_off; /* host string offset (always 0x00000020) */ + +#if 0 + guint8 host[*]; /* host string (ASCII) */ + guint8 dom[*]; /* domain string (ASCII) */ +#endif +}; + +static time_t purple_http_rfc1123_to_time(const gchar *str); + +static gboolean purple_http_request_is_method(PurpleHttpRequest *request, + const gchar *method); + +static PurpleHttpConnection * purple_http_connection_new( + PurpleHttpRequest *request, PurpleConnection *gc); +static void purple_http_connection_terminate(PurpleHttpConnection *hc); +static void purple_http_conn_notify_progress_watcher(PurpleHttpConnection *hc); +static void +purple_http_conn_retry(PurpleHttpConnection *http_conn); + +static PurpleHttpResponse * purple_http_response_new(void); +static void purple_http_response_free(PurpleHttpResponse *response); + +static void purple_http_cookie_jar_parse(PurpleHttpCookieJar *cookie_jar, + GList *values); +static gchar * purple_http_cookie_jar_gen(PurpleHttpCookieJar *cookie_jar); +gchar * purple_http_cookie_jar_dump(PurpleHttpCookieJar *cjar); + +static PurpleHttpKeepaliveRequest * +purple_http_keepalive_pool_request(PurpleHttpKeepalivePool *pool, + PurpleConnection *gc, const gchar *host, int port, gboolean is_ssl, + PurpleSocketConnectCb cb, gpointer user_data); +static void +purple_http_keepalive_pool_request_cancel(PurpleHttpKeepaliveRequest *req); +static void +purple_http_keepalive_pool_release(PurpleHttpSocket *hs, gboolean invalidate); + +static void +purple_http_connection_set_remove(PurpleHttpConnectionSet *set, + PurpleHttpConnection *http_conn); + +static GRegex *purple_http_re_url, *purple_http_re_url_host, + *purple_http_re_rfc1123; + +/* + * Values: pointers to running PurpleHttpConnection. + */ +static GList *purple_http_hc_list; + +/* + * Keys: pointers to PurpleConnection. + * Values: GList of pointers to running PurpleHttpConnection. + */ +static GHashTable *purple_http_hc_by_gc; + +/* + * Keys: pointers to PurpleConnection. + * Values: gboolean TRUE. + */ +static GHashTable *purple_http_cancelling_gc; + +/* + * Keys: pointers to PurpleHttpConnection. + * Values: pointers to links in purple_http_hc_list. + */ +static GHashTable *purple_http_hc_by_ptr; + +/*** Helper functions *********************************************************/ + +static time_t purple_http_rfc1123_to_time(const gchar *str) +{ + static const gchar *months[13] = { + "jan", "feb", "mar", "apr", "may", "jun", + "jul", "aug", "sep", "oct", "nov", "dec", NULL + }; + GMatchInfo *match_info; + gchar *d_date, *d_month, *d_year, *d_time; + int month; + gchar *iso_date; + time_t t; + + g_return_val_if_fail(str != NULL, 0); + + g_regex_match(purple_http_re_rfc1123, str, 0, &match_info); + if (!g_match_info_matches(match_info)) { + g_match_info_free(match_info); + return 0; + } + + d_date = g_match_info_fetch(match_info, 1); + d_month = g_match_info_fetch(match_info, 2); + d_year = g_match_info_fetch(match_info, 3); + d_time = g_match_info_fetch(match_info, 4); + + g_match_info_free(match_info); + + month = 0; + while (months[month] != NULL) { + if (0 == g_ascii_strcasecmp(d_month, months[month])) + break; + month++; + } + month++; + + iso_date = g_strdup_printf("%s-%02d-%sT%s+00:00", + d_year, month, d_date, d_time); + + g_free(d_date); + g_free(d_month); + g_free(d_year); + g_free(d_time); + + if (month > 12) { + purple_debug_warning("http", "Invalid month: %s\n", d_month); + g_free(iso_date); + return 0; + } + + t = purple_str_to_time(iso_date, TRUE, NULL, NULL, NULL); + + g_free(iso_date); + + return t; +} + +/*** GZip streams *************************************************************/ + +static PurpleHttpGzStream * +purple_http_gz_new(gsize max_output, gboolean is_deflate) +{ + PurpleHttpGzStream *gzs = g_new0(PurpleHttpGzStream, 1); + int windowBits; + + if (is_deflate) + windowBits = -MAX_WBITS; + else /* is gzip */ + windowBits = MAX_WBITS + 32; + + if (inflateInit2(&gzs->zs, windowBits) != Z_OK) { + purple_debug_error("http", "Cannot initialize zlib stream\n"); + g_free(gzs); + return NULL; + } + + gzs->max_output = max_output; + + return gzs; +} + +static GString * +purple_http_gz_put(PurpleHttpGzStream *gzs, const gchar *buf, gsize len) +{ + const gchar *compressed_buff; + gsize compressed_len; + GString *ret; + z_stream *zs; + + g_return_val_if_fail(gzs != NULL, NULL); + g_return_val_if_fail(buf != NULL, NULL); + + if (gzs->failed) + return NULL; + + zs = &gzs->zs; + + if (gzs->pending) { + g_string_append_len(gzs->pending, buf, len); + compressed_buff = gzs->pending->str; + compressed_len = gzs->pending->len; + } else { + compressed_buff = buf; + compressed_len = len; + } + + zs->next_in = (z_const Bytef*)compressed_buff; + zs->avail_in = compressed_len; + + ret = g_string_new(NULL); + while (zs->avail_in > 0) { + int gzres; + gchar decompressed_buff[PURPLE_HTTP_GZ_BUFF_LEN]; + gsize decompressed_len; + + zs->next_out = (Bytef*)decompressed_buff; + zs->avail_out = sizeof(decompressed_buff); + decompressed_len = zs->avail_out = sizeof(decompressed_buff); + gzres = inflate(zs, Z_FULL_FLUSH); + decompressed_len -= zs->avail_out; + + if (gzres == Z_OK || gzres == Z_STREAM_END) { + if (decompressed_len == 0) + break; + if (gzs->decompressed + decompressed_len >= + gzs->max_output) + { + purple_debug_warning("http", "Maximum amount of" + " decompressed data is reached\n"); + decompressed_len = gzs->max_output - + gzs->decompressed; + gzres = Z_STREAM_END; + } + gzs->decompressed += decompressed_len; + g_string_append_len(ret, decompressed_buff, + decompressed_len); + if (gzres == Z_STREAM_END) + break; + } else { + purple_debug_error("http", + "Decompression failed (%d): %s\n", gzres, + zs->msg); + gzs->failed = TRUE; + return NULL; + } + } + + if (gzs->pending) { + g_string_free(gzs->pending, TRUE); + gzs->pending = NULL; + } + + if (zs->avail_in > 0) { + gzs->pending = g_string_new_len((gchar*)zs->next_in, + zs->avail_in); + } + + return ret; +} + +static void +purple_http_gz_free(PurpleHttpGzStream *gzs) +{ + if (gzs == NULL) + return; + inflateEnd(&gzs->zs); + if (gzs->pending) + g_string_free(gzs->pending, TRUE); + g_free(gzs); +} + +/*** NTLM *********************************************************************/ + +/** + * purple_ntlm_gen_type1: + * @hostname: Your hostname + * @domain: The domain to authenticate to + * + * Generates the base64 encoded type 1 message needed for NTLM authentication + * + * Returns: base64 encoded string to send to the server. This should + * be g_free'd by the caller. + */ +static gchar * +purple_http_ntlm_gen_type1(const gchar *hostname, const gchar *domain) +{ + int hostnamelen,host_off; + int domainlen,dom_off; + unsigned char *msg; + struct _ntlm_type1_message *tmsg; + gchar *tmp; + + hostnamelen = strlen(hostname); + domainlen = strlen(domain); + host_off = sizeof(struct _ntlm_type1_message); + dom_off = sizeof(struct _ntlm_type1_message) + hostnamelen; + msg = g_malloc0(sizeof(struct _ntlm_type1_message) + hostnamelen + domainlen); + tmsg = (struct _ntlm_type1_message*)(gpointer)msg; + tmsg->protocol[0] = 'N'; + tmsg->protocol[1] = 'T'; + tmsg->protocol[2] = 'L'; + tmsg->protocol[3] = 'M'; + tmsg->protocol[4] = 'S'; + tmsg->protocol[5] = 'S'; + tmsg->protocol[6] = 'P'; + tmsg->protocol[7] = '\0'; + tmsg->type = GUINT32_TO_LE(0x00000001); + tmsg->flags = GUINT32_TO_LE(0x0000b203); + tmsg->dom_len1 = tmsg->dom_len2 = GUINT16_TO_LE(domainlen); + tmsg->dom_off = GUINT32_TO_LE(dom_off); + tmsg->host_len1 = tmsg->host_len2 = GUINT16_TO_LE(hostnamelen); + tmsg->host_off = GUINT32_TO_LE(host_off); + memcpy(msg + host_off, hostname, hostnamelen); + memcpy(msg + dom_off, domain, domainlen); + + tmp = g_base64_encode(msg, sizeof(struct _ntlm_type1_message) + hostnamelen + domainlen); + g_free(msg); + + return tmp; +} + +/*** HTTP Sockets *************************************************************/ + +static gchar * +purple_http_socket_hash(const gchar *host, int port, gboolean is_ssl) +{ + return g_strdup_printf("%c:%s:%d", (is_ssl ? 'S' : 'R'), host, port); +} + +static PurpleHttpSocket * +purple_http_socket_connect_new(PurpleConnection *gc, const gchar *host, + int port, gboolean is_ssl, PurpleSocketConnectCb cb, gpointer user_data) +{ + PurpleHttpSocket *hs = g_new0(PurpleHttpSocket, 1); + + hs->ps = purple_socket_new(gc); + purple_socket_set_data(hs->ps, "hs", hs); + purple_socket_set_tls(hs->ps, is_ssl); + purple_socket_set_host(hs->ps, host); + purple_socket_set_port(hs->ps, port); + if (!purple_socket_connect(hs->ps, cb, user_data)) { + purple_socket_destroy(hs->ps); + g_free(hs); + return NULL; + } + + if (purple_debug_is_verbose()) + purple_debug_misc("http", "new socket created: %p\n", hs); + + return hs; +} + +static void +purple_http_socket_close_free(PurpleHttpSocket *hs) +{ + if (hs == NULL) + return; + + if (purple_debug_is_verbose()) + purple_debug_misc("http", "destroying socket: %p\n", hs); + + purple_socket_destroy(hs->ps); + g_free(hs); +} + +/*** Headers collection *******************************************************/ + +static PurpleHttpHeaders * purple_http_headers_new(void); +static void purple_http_headers_free(PurpleHttpHeaders *hdrs); +static void purple_http_headers_add(PurpleHttpHeaders *hdrs, const gchar *key, + const gchar *value); +static const GList * purple_http_headers_get_all(PurpleHttpHeaders *hdrs); +static GList * purple_http_headers_get_all_by_name( + PurpleHttpHeaders *hdrs, const gchar *key); +static const gchar * purple_http_headers_get(PurpleHttpHeaders *hdrs, + const gchar *key); +static gboolean purple_http_headers_get_int(PurpleHttpHeaders *hdrs, + const gchar *key, int *dst); +static gboolean purple_http_headers_match(PurpleHttpHeaders *hdrs, + const gchar *key, const gchar *value); +static gchar * purple_http_headers_dump(PurpleHttpHeaders *hdrs); + +static PurpleHttpHeaders * purple_http_headers_new(void) +{ + PurpleHttpHeaders *hdrs = g_new0(PurpleHttpHeaders, 1); + + hdrs->by_name = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, + (GDestroyNotify)g_list_free); + + return hdrs; +} + +static void purple_http_headers_free_kvp(PurpleKeyValuePair *kvp) +{ + g_free(kvp->key); + g_free(kvp->value); + g_free(kvp); +} + +static void purple_http_headers_free(PurpleHttpHeaders *hdrs) +{ + if (hdrs == NULL) + return; + + g_hash_table_destroy(hdrs->by_name); + g_list_free_full(hdrs->list, + (GDestroyNotify)purple_http_headers_free_kvp); + g_free(hdrs); +} + +static void purple_http_headers_add(PurpleHttpHeaders *hdrs, const gchar *key, + const gchar *value) +{ + PurpleKeyValuePair *kvp; + GList *named_values, *new_values; + gchar *key_low; + + g_return_if_fail(hdrs != NULL); + g_return_if_fail(key != NULL); + g_return_if_fail(value != NULL); + + kvp = g_new0(PurpleKeyValuePair, 1); + kvp->key = g_strdup(key); + kvp->value = g_strdup(value); + hdrs->list = g_list_append(hdrs->list, kvp); + + key_low = g_ascii_strdown(key, -1); + named_values = g_hash_table_lookup(hdrs->by_name, key_low); + new_values = g_list_append(named_values, kvp->value); + if (named_values) + g_free(key_low); + else + g_hash_table_insert(hdrs->by_name, key_low, new_values); +} + +static void purple_http_headers_remove(PurpleHttpHeaders *hdrs, + const gchar *key) +{ + GList *it, *curr; + + g_return_if_fail(hdrs != NULL); + g_return_if_fail(key != NULL); + + if (!g_hash_table_remove(hdrs->by_name, key)) + return; + + /* Could be optimized to O(1). */ + it = g_list_first(hdrs->list); + while (it) { + PurpleKeyValuePair *kvp = it->data; + curr = it; + it = g_list_next(it); + if (g_ascii_strcasecmp(kvp->key, key) != 0) + continue; + + hdrs->list = g_list_delete_link(hdrs->list, curr); + purple_http_headers_free_kvp(kvp); + } +} + +static const GList * purple_http_headers_get_all(PurpleHttpHeaders *hdrs) +{ + g_return_val_if_fail(hdrs != NULL, NULL); + + return hdrs->list; +} + +/* return const */ +static GList * purple_http_headers_get_all_by_name( + PurpleHttpHeaders *hdrs, const gchar *key) +{ + GList *values; + gchar *key_low; + + g_return_val_if_fail(hdrs != NULL, NULL); + g_return_val_if_fail(key != NULL, NULL); + + key_low = g_ascii_strdown(key, -1); + values = g_hash_table_lookup(hdrs->by_name, key_low); + g_free(key_low); + + return values; +} + +static const gchar * purple_http_headers_get(PurpleHttpHeaders *hdrs, + const gchar *key) +{ + const GList *values = purple_http_headers_get_all_by_name(hdrs, key); + + if (!values) + return NULL; + + return values->data; +} + +static gboolean purple_http_headers_get_int(PurpleHttpHeaders *hdrs, + const gchar *key, int *dst) +{ + int val; + const gchar *str; + + str = purple_http_headers_get(hdrs, key); + if (!str) + return FALSE; + + if (1 != sscanf(str, "%d", &val)) + return FALSE; + + *dst = val; + return TRUE; +} + +static gboolean purple_http_headers_match(PurpleHttpHeaders *hdrs, + const gchar *key, const gchar *value) +{ + const gchar *str; + + str = purple_http_headers_get(hdrs, key); + if (str == NULL || value == NULL) + return str == value; + + return (g_ascii_strcasecmp(str, value) == 0); +} + +static gchar * purple_http_headers_dump(PurpleHttpHeaders *hdrs) +{ + const GList *hdr; + + GString *s = g_string_new(""); + + hdr = purple_http_headers_get_all(hdrs); + while (hdr) { + PurpleKeyValuePair *kvp = hdr->data; + hdr = g_list_next(hdr); + + g_string_append_printf(s, "%s: %s%s", kvp->key, + (gchar*)kvp->value, hdr ? "\n" : ""); + } + + return g_string_free(s, FALSE); +} + +/*** HTTP protocol backend ****************************************************/ + +static void _purple_http_disconnect(PurpleHttpConnection *hc, + gboolean is_graceful); + +static void _purple_http_gen_headers(PurpleHttpConnection *hc); +static gboolean _purple_http_recv_loopbody(PurpleHttpConnection *hc, gint fd); +static void _purple_http_recv(gpointer _hc, gint fd, + PurpleInputCondition cond); +static void _purple_http_send(gpointer _hc, gint fd, PurpleInputCondition cond); + +/* closes current connection (if exists), estabilishes one and proceeds with + * request */ +static gboolean _purple_http_reconnect(PurpleHttpConnection *hc); + +static void _purple_http_error(PurpleHttpConnection *hc, const char *format, + ...) G_GNUC_PRINTF(2, 3); + +static void _purple_http_error(PurpleHttpConnection *hc, const char *format, + ...) +{ + va_list args; + + va_start(args, format); + hc->response->error = g_strdup_vprintf(format, args); + va_end(args); + + if (purple_debug_is_verbose()) + purple_debug_warning("http", "error: %s\n", hc->response->error); + + purple_http_conn_cancel(hc); +} + +static void _purple_http_gen_headers(PurpleHttpConnection *hc) +{ + GString *h; + PurpleHttpURL *url; + const GList *hdr; + PurpleHttpRequest *req; + PurpleHttpHeaders *hdrs; + gchar *request_url, *tmp_url = NULL; + + PurpleProxyInfo *proxy; + gboolean proxy_http = FALSE; + const gchar *proxy_username, *proxy_password; + + g_return_if_fail(hc != NULL); + + if (hc->request_header != NULL) + return; + + req = hc->request; + url = hc->url; + hdrs = req->headers; + proxy = purple_proxy_get_setup(hc->gc ? + purple_connection_get_account(hc->gc) : NULL); + + proxy_http = (purple_proxy_info_get_proxy_type(proxy) == PURPLE_PROXY_HTTP || + purple_proxy_info_get_proxy_type(proxy) == PURPLE_PROXY_USE_ENVVAR); + /* this is HTTP proxy, but used with tunelling with CONNECT */ + if (proxy_http && url->port != 80) + proxy_http = FALSE; + + hc->request_header = h = g_string_new(""); + hc->request_header_written = 0; + hc->request_contents_written = 0; + + if (proxy_http) + request_url = tmp_url = purple_http_url_print(url); + else + request_url = url->path; + + g_string_append_printf(h, "%s %s HTTP/%s\r\n", + req->method ? req->method : "GET", + request_url, + req->http11 ? "1.1" : "1.0"); + + g_free(tmp_url); + + if (!purple_http_headers_get(hdrs, "host")) + g_string_append_printf(h, "Host: %s\r\n", url->host); + if (!purple_http_headers_get(hdrs, "connection")) { + g_string_append(h, "Connection: "); + g_string_append(h, hc->is_keepalive ? + "Keep-Alive\r\n" : "close\r\n"); + } + if (!purple_http_headers_get(hdrs, "accept")) + g_string_append(h, "Accept: */*\r\n"); + if (!purple_http_headers_get(hdrs, "accept-encoding")) + g_string_append(h, "Accept-Encoding: gzip, deflate\r\n"); + + if (!purple_http_headers_get(hdrs, "content-length") && ( + req->contents_length > 0 || + purple_http_request_is_method(req, "post"))) + { + g_string_append_printf(h, "Content-Length: %u\r\n", + req->contents_length); + } + + if (proxy_http) + g_string_append(h, "Proxy-Connection: close\r\n"); /* TEST: proxy+KeepAlive */ + + proxy_username = purple_proxy_info_get_username(proxy); + if (proxy_http && proxy_username != NULL && proxy_username[0] != '\0') { + gchar *proxy_auth, *ntlm_type1, *tmp; + int len; + + proxy_password = purple_proxy_info_get_password(proxy); + if (proxy_password == NULL) + proxy_password = ""; + + tmp = g_strdup_printf("%s:%s", proxy_username, proxy_password); + len = strlen(tmp); + proxy_auth = g_base64_encode((const guchar *)tmp, len); + memset(tmp, 0, len); + g_free(tmp); + + ntlm_type1 = purple_http_ntlm_gen_type1(purple_get_host_name(), + ""); + + g_string_append_printf(h, "Proxy-Authorization: Basic %s\r\n", + proxy_auth); + g_string_append_printf(h, "Proxy-Authorization: NTLM %s\r\n", + ntlm_type1); + g_string_append(h, "Proxy-Connection: close\r\n"); /* TEST: proxy+KeepAlive */ + + memset(proxy_auth, 0, strlen(proxy_auth)); + g_free(proxy_auth); + g_free(ntlm_type1); + } + + hdr = purple_http_headers_get_all(hdrs); + while (hdr) { + PurpleKeyValuePair *kvp = hdr->data; + hdr = g_list_next(hdr); + + g_string_append_printf(h, "%s: %s\r\n", + kvp->key, (gchar*)kvp->value); + } + + if (!purple_http_cookie_jar_is_empty(req->cookie_jar)) { + gchar * cookies = purple_http_cookie_jar_gen(req->cookie_jar); + g_string_append_printf(h, "Cookie: %s\r\n", cookies); + g_free(cookies); + } + + g_string_append_printf(h, "\r\n"); + + if (purple_debug_is_unsafe() && purple_debug_is_verbose()) { + purple_debug_misc("http", "Generated request headers:\n%s", + h->str); + } +} + +static gboolean _purple_http_recv_headers(PurpleHttpConnection *hc, + const gchar *buf, int len) +{ + gchar *eol, *delim; + + if (hc->headers_got) { + purple_debug_error("http", "Headers already got\n"); + _purple_http_error(hc, _("Error parsing HTTP")); + return FALSE; + } + + g_string_append_len(hc->response_buffer, buf, len); + if (hc->response_buffer->len > PURPLE_HTTP_MAX_RECV_BUFFER_LEN) { + purple_debug_error("http", + "Buffer too big when parsing headers\n"); + _purple_http_error(hc, _("Error parsing HTTP")); + return FALSE; + } + + while ((eol = strstr(hc->response_buffer->str, "\r\n")) + != NULL) + { + gchar *hdrline = hc->response_buffer->str; + int hdrline_len = eol - hdrline; + + hdrline[hdrline_len] = '\0'; + + if (hdrline[0] == '\0') { + if (!hc->main_header_got) { + if (purple_debug_is_verbose() && + hc->is_keepalive) + { + purple_debug_misc("http", "Got keep-" + "alive terminator from previous" + " request\n"); + } else { + purple_debug_warning("http", "Got empty" + " line at the beginning - this " + "may be a HTTP server quirk\n"); + } + } else /* hc->main_header_got */ { + hc->headers_got = TRUE; + if (purple_debug_is_verbose()) { + purple_debug_misc("http", "Got headers " + "end\n"); + } + } + } else if (!hc->main_header_got) { + hc->main_header_got = TRUE; + delim = strchr(hdrline, ' '); + if (delim == NULL || 1 != sscanf(delim + 1, "%d", + &hc->response->code)) + { + purple_debug_warning("http", + "Invalid response code\n"); + _purple_http_error(hc, _("Error parsing HTTP")); + return FALSE; + } + if (purple_debug_is_verbose()) + purple_debug_misc("http", + "Got main header with code %d\n", + hc->response->code); + } else { + if (purple_debug_is_verbose() && + purple_debug_is_unsafe()) + purple_debug_misc("http", "Got header: %s\n", + hdrline); + delim = strchr(hdrline, ':'); + if (delim == NULL || delim == hdrline) { + purple_debug_warning("http", + "Bad header delimiter\n"); + _purple_http_error(hc, _("Error parsing HTTP")); + return FALSE; + } + *delim++ = '\0'; + while (*delim == ' ') + delim++; + + purple_http_headers_add(hc->response->headers, hdrline, delim); + } + + g_string_erase(hc->response_buffer, 0, hdrline_len + 2); + if (hc->headers_got) + break; + } + return TRUE; +} + +static gboolean _purple_http_recv_body_data(PurpleHttpConnection *hc, + const gchar *buf, int len) +{ + GString *decompressed = NULL; + + if (hc->length_expected >= 0 && + len + hc->length_got > (guint)hc->length_expected) + { + len = hc->length_expected - hc->length_got; + } + + hc->length_got += len; + + if (hc->gz_stream != NULL) { + decompressed = purple_http_gz_put(hc->gz_stream, buf, len); + if (decompressed == NULL) { + _purple_http_error(hc, + _("Error while decompressing data")); + return FALSE; + } + buf = decompressed->str; + len = decompressed->len; + } + + g_assert(hc->request->max_length <= + PURPLE_HTTP_REQUEST_HARD_MAX_LENGTH); + if (hc->length_got_decompressed + len > hc->request->max_length) { + purple_debug_warning("http", + "Maximum length exceeded, truncating\n"); + len = hc->request->max_length - hc->length_got_decompressed; + hc->length_expected = hc->length_got; + } + hc->length_got_decompressed += len; + + if (len == 0) { + if (decompressed != NULL) + g_string_free(decompressed, TRUE); + return TRUE; + } + + if (hc->request->response_writer != NULL) { + gboolean succ; + succ = hc->request->response_writer(hc, hc->response, buf, + hc->length_got_decompressed, len, + hc->request->response_writer_data); + if (!succ) { + if (decompressed != NULL) + g_string_free(decompressed, TRUE); + purple_debug_error("http", + "Cannot write using callback\n"); + _purple_http_error(hc, + _("Error handling retrieved data")); + return FALSE; + } + } else { + if (hc->response->contents == NULL) + hc->response->contents = g_string_new(""); + g_string_append_len(hc->response->contents, buf, len); + } + + if (decompressed != NULL) + g_string_free(decompressed, TRUE); + + purple_http_conn_notify_progress_watcher(hc); + return TRUE; +} + +static gboolean _purple_http_recv_body_chunked(PurpleHttpConnection *hc, + const gchar *buf, int len) +{ + gchar *eol, *line; + int line_len; + + if (hc->chunks_done) + return FALSE; + if (!hc->response_buffer) + hc->response_buffer = g_string_new(""); + + g_string_append_len(hc->response_buffer, buf, len); + if (hc->response_buffer->len > PURPLE_HTTP_MAX_RECV_BUFFER_LEN) { + purple_debug_error("http", + "Buffer too big when searching for chunk\n"); + _purple_http_error(hc, _("Error parsing HTTP")); + return FALSE; + } + + while (hc->response_buffer->len > 0) { + if (hc->in_chunk) { + int got_now = hc->response_buffer->len; + if (hc->chunk_got + got_now > hc->chunk_length) + got_now = hc->chunk_length - hc->chunk_got; + hc->chunk_got += got_now; + + if (!_purple_http_recv_body_data(hc, + hc->response_buffer->str, got_now)) + return FALSE; + + g_string_erase(hc->response_buffer, 0, got_now); + hc->in_chunk = (hc->chunk_got < hc->chunk_length); + + continue; + } + + line = hc->response_buffer->str; + eol = strstr(line, "\r\n"); + if (eol == line) { + g_string_erase(hc->response_buffer, 0, 2); + line = hc->response_buffer->str; + eol = strstr(line, "\r\n"); + } + if (eol == NULL) { + /* waiting for more data (unlikely, but possible) */ + if (hc->response_buffer->len > 20) { + purple_debug_warning("http", "Chunk length not " + "found (buffer too large)\n"); + _purple_http_error(hc, _("Error parsing HTTP")); + return FALSE; + } + return TRUE; + } + line_len = eol - line; + + if (1 != sscanf(line, "%x", &hc->chunk_length)) { + if (purple_debug_is_unsafe()) + purple_debug_warning("http", + "Chunk length not found in [%s]\n", + line); + else + purple_debug_warning("http", + "Chunk length not found\n"); + _purple_http_error(hc, _("Error parsing HTTP")); + return FALSE; + } + hc->chunk_got = 0; + hc->in_chunk = TRUE; + + if (purple_debug_is_verbose()) + purple_debug_misc("http", "Found chunk of length %d\n", hc->chunk_length); + + g_string_erase(hc->response_buffer, 0, line_len + 2); + + if (hc->chunk_length == 0) { + hc->chunks_done = TRUE; + hc->in_chunk = FALSE; + return TRUE; + } + } + + return TRUE; +} + +static gboolean _purple_http_recv_body(PurpleHttpConnection *hc, + const gchar *buf, int len) +{ + if (hc->is_chunked) + return _purple_http_recv_body_chunked(hc, buf, len); + + return _purple_http_recv_body_data(hc, buf, len); +} + +static gboolean _purple_http_recv_loopbody(PurpleHttpConnection *hc, gint fd) +{ + int len; + gchar buf[4096]; + gboolean got_anything; + + len = purple_socket_read(hc->socket->ps, (guchar*)buf, sizeof(buf)); + got_anything = (len > 0); + + if (len < 0 && errno == EAGAIN) + return FALSE; + + if (len < 0) { + _purple_http_error(hc, _("Error reading from %s: %s"), + hc->url->host, g_strerror(errno)); + return FALSE; + } + + /* EOF */ + if (len == 0) { + if (hc->request->max_length == 0) { + /* It's definitely YHttpServer quirk. */ + purple_debug_warning("http", "Got EOF, but no data was " + "expected (this may be a server quirk)\n"); + hc->length_expected = hc->length_got; + } + if (hc->length_expected >= 0 && + hc->length_got < (guint)hc->length_expected) + { + purple_debug_warning("http", "No more data while reading" + " contents\n"); + _purple_http_error(hc, _("Error parsing HTTP")); + return FALSE; + } + if (hc->headers_got) + hc->length_expected = hc->length_got; + else if (hc->length_got == 0 && hc->socket->use_count > 1) { + purple_debug_info("http", "Keep-alive connection " + "expired (when reading), retrying...\n"); + purple_http_conn_retry(hc); + return FALSE; + } else { + const gchar *server = purple_http_headers_get( + hc->response->headers, "Server"); + if (server && + g_ascii_strcasecmp(server, "YHttpServer") == 0) + { + purple_debug_warning("http", "No more data " + "while parsing headers (YHttpServer " + "quirk)\n"); + hc->headers_got = TRUE; + hc->length_expected = hc->length_got = 0; + hc->length_got_decompressed = 0; + } else { + purple_debug_warning("http", "No more data " + "while parsing headers\n"); + _purple_http_error(hc, _("Error parsing HTTP")); + return FALSE; + } + } + } + + if (!hc->headers_got && len > 0) { + if (!_purple_http_recv_headers(hc, buf, len)) + return FALSE; + len = 0; + if (hc->headers_got) { + gboolean is_gzip, is_deflate; + if (!purple_http_headers_get_int(hc->response->headers, + "Content-Length", &hc->length_expected)) + hc->length_expected = -1; + hc->is_chunked = (purple_http_headers_match( + hc->response->headers, + "Transfer-Encoding", "chunked")); + is_gzip = purple_http_headers_match( + hc->response->headers, "Content-Encoding", + "gzip"); + is_deflate = purple_http_headers_match( + hc->response->headers, "Content-Encoding", + "deflate"); + if (is_gzip || is_deflate) { + hc->gz_stream = purple_http_gz_new( + hc->request->max_length + 1, + is_deflate); + } + } + if (hc->headers_got && hc->response_buffer && + hc->response_buffer->len > 0) + { + int buffer_len = hc->response_buffer->len; + gchar *buffer = g_string_free(hc->response_buffer, FALSE); + hc->response_buffer = NULL; + _purple_http_recv_body(hc, buffer, buffer_len); + g_free(buffer); + } + if (!hc->headers_got) + return got_anything; + } + + if (len > 0) { + if (!_purple_http_recv_body(hc, buf, len)) + return FALSE; + } + + if (hc->is_chunked && hc->chunks_done && hc->length_expected < 0) + hc->length_expected = hc->length_got; + + if (hc->length_expected >= 0 && + hc->length_got >= (guint)hc->length_expected) + { + const gchar *redirect; + + if (hc->is_chunked && !hc->chunks_done) { + if (len == 0) { + _purple_http_error(hc, _("Chunked connection terminated")); + return FALSE; + } + if (purple_debug_is_verbose()) { + purple_debug_misc("http", + "I need the terminating empty chunk\n"); + } + return TRUE; + } + + if (!hc->headers_got) { + hc->response->code = 0; + purple_debug_warning("http", "No headers got\n"); + _purple_http_error(hc, _("Error parsing HTTP")); + return FALSE; + } + + if (purple_debug_is_unsafe() && purple_debug_is_verbose()) { + gchar *hdrs = purple_http_headers_dump( + hc->response->headers); + purple_debug_misc("http", "Got response headers: %s\n", + hdrs); + g_free(hdrs); + } + + purple_http_cookie_jar_parse(hc->request->cookie_jar, + purple_http_headers_get_all_by_name( + hc->response->headers, "Set-Cookie")); + + if (purple_debug_is_unsafe() && purple_debug_is_verbose() && + !purple_http_cookie_jar_is_empty( + hc->request->cookie_jar)) + { + gchar *cookies = purple_http_cookie_jar_dump( + hc->request->cookie_jar); + purple_debug_misc("http", "Cookies: %s\n", cookies); + g_free(cookies); + } + + if (hc->response->code == 407) { + _purple_http_error(hc, _("Invalid proxy credentials")); + return FALSE; + } + + redirect = purple_http_headers_get(hc->response->headers, + "location"); + if (redirect && (hc->request->max_redirects == -1 || + hc->request->max_redirects > hc->redirects_count)) + { + PurpleHttpURL *url = purple_http_url_parse(redirect); + + hc->redirects_count++; + + if (!url) { + if (purple_debug_is_unsafe()) + purple_debug_warning("http", + "Invalid redirect to %s\n", + redirect); + else + purple_debug_warning("http", + "Invalid redirect\n"); + _purple_http_error(hc, _("Error parsing HTTP")); + } + + purple_http_url_relative(hc->url, url); + purple_http_url_free(url); + + _purple_http_reconnect(hc); + return FALSE; + } + + _purple_http_disconnect(hc, TRUE); + purple_http_connection_terminate(hc); + return FALSE; + } + + return got_anything; +} + +static void _purple_http_recv(gpointer _hc, gint fd, PurpleInputCondition cond) +{ + PurpleHttpConnection *hc = _hc; + + while (_purple_http_recv_loopbody(hc, fd)); +} + +static void _purple_http_send_got_data(PurpleHttpConnection *hc, + gboolean success, gboolean eof, size_t stored) +{ + int estimated_length; + + g_return_if_fail(hc != NULL); + + if (!success) { + _purple_http_error(hc, _("Error requesting data to write")); + return; + } + + hc->contents_reader_requested = FALSE; + g_string_set_size(hc->contents_reader_buffer, stored); + if (!eof) + return; + + estimated_length = hc->request_contents_written + stored; + + if (hc->request->contents_length != -1 && + hc->request->contents_length != estimated_length) + { + purple_debug_warning("http", + "Invalid amount of data has been written\n"); + } + hc->request->contents_length = estimated_length; +} + +static void _purple_http_send(gpointer _hc, gint fd, PurpleInputCondition cond) +{ + PurpleHttpConnection *hc = _hc; + int written, write_len; + const gchar *write_from; + gboolean writing_headers; + + /* Waiting for data. This could be written more efficiently, by removing + * (and later, adding) hs->inpa. */ + if (hc->contents_reader_requested) + return; + + _purple_http_gen_headers(hc); + + writing_headers = + (hc->request_header_written < hc->request_header->len); + if (writing_headers) { + write_from = hc->request_header->str + + hc->request_header_written; + write_len = hc->request_header->len - + hc->request_header_written; + } else if (hc->request->contents_reader) { + if (hc->contents_reader_requested) + return; /* waiting for data */ + if (!hc->contents_reader_buffer) + hc->contents_reader_buffer = g_string_new(""); + if (hc->contents_reader_buffer->len == 0) { + hc->contents_reader_requested = TRUE; + g_string_set_size(hc->contents_reader_buffer, + PURPLE_HTTP_MAX_READ_BUFFER_LEN); + hc->request->contents_reader(hc, + hc->contents_reader_buffer->str, + hc->request_contents_written, + PURPLE_HTTP_MAX_READ_BUFFER_LEN, + hc->request->contents_reader_data, + _purple_http_send_got_data); + return; + } + write_from = hc->contents_reader_buffer->str; + write_len = hc->contents_reader_buffer->len; + } else { + write_from = hc->request->contents + + hc->request_contents_written; + write_len = hc->request->contents_length - + hc->request_contents_written; + } + + if (write_len == 0) { + purple_debug_warning("http", "Nothing to write\n"); + written = 0; + } else { + written = purple_socket_write(hc->socket->ps, + (const guchar*)write_from, write_len); + } + + if (written < 0 && errno == EAGAIN) + return; + + if (written < 0) { + if (hc->request_header_written == 0 && + hc->socket->use_count > 1) + { + purple_debug_info("http", "Keep-alive connection " + "expired (when writing), retrying...\n"); + purple_http_conn_retry(hc); + return; + } + + _purple_http_error(hc, _("Error writing to %s: %s"), + hc->url->host, g_strerror(errno)); + return; + } + + if (writing_headers) { + hc->request_header_written += written; + purple_http_conn_notify_progress_watcher(hc); + if (hc->request_header_written < hc->request_header->len) + return; + if (hc->request->contents_length > 0) + return; + } else { + hc->request_contents_written += written; + purple_http_conn_notify_progress_watcher(hc); + if (hc->contents_reader_buffer) + g_string_erase(hc->contents_reader_buffer, 0, written); + if (hc->request->contents_length > 0 && + hc->request_contents_written < + (guint)hc->request->contents_length) + { + return; + } + } + + /* request is completely written, let's read the response */ + hc->is_reading = TRUE; + purple_socket_watch(hc->socket->ps, PURPLE_INPUT_READ, + _purple_http_recv, hc); +} + +static void _purple_http_disconnect(PurpleHttpConnection *hc, + gboolean is_graceful) +{ + g_return_if_fail(hc != NULL); + + if (hc->request_header) + g_string_free(hc->request_header, TRUE); + hc->request_header = NULL; + + if (hc->response_buffer) + g_string_free(hc->response_buffer, TRUE); + hc->response_buffer = NULL; + + if (hc->socket_request) + purple_http_keepalive_pool_request_cancel(hc->socket_request); + else { + purple_http_keepalive_pool_release(hc->socket, !is_graceful); + hc->socket = NULL; + } +} + +static void +_purple_http_connected(PurpleSocket *ps, const gchar *error, gpointer _hc) +{ + PurpleHttpSocket *hs = NULL; + PurpleHttpConnection *hc = _hc; + + if (ps != NULL) + hs = purple_socket_get_data(ps, "hs"); + + hc->socket_request = NULL; + hc->socket = hs; + + if (error != NULL) { + _purple_http_error(hc, _("Unable to connect to %s: %s"), + hc->url->host, error); + return; + } + + purple_socket_watch(ps, PURPLE_INPUT_WRITE, _purple_http_send, hc); +} + +static gboolean _purple_http_reconnect(PurpleHttpConnection *hc) +{ + PurpleHttpURL *url; + gboolean is_ssl = FALSE; + + g_return_val_if_fail(hc != NULL, FALSE); + g_return_val_if_fail(hc->url != NULL, FALSE); + + _purple_http_disconnect(hc, TRUE); + + if (purple_debug_is_verbose()) { + if (purple_debug_is_unsafe()) { + gchar *urlp = purple_http_url_print(hc->url); + purple_debug_misc("http", "Connecting to %s...\n", urlp); + g_free(urlp); + } else + purple_debug_misc("http", "Connecting to %s...\n", + hc->url->host); + } + + url = hc->url; + if (g_strcmp0(url->protocol, "") == 0 || + g_ascii_strcasecmp(url->protocol, "http") == 0) + { + /* do nothing */ + } else if (g_ascii_strcasecmp(url->protocol, "https") == 0) { + is_ssl = TRUE; + } else { + _purple_http_error(hc, _("Unsupported protocol: %s"), + url->protocol); + return FALSE; + } + + if (hc->request->keepalive_pool != NULL) { + hc->socket_request = purple_http_keepalive_pool_request( + hc->request->keepalive_pool, hc->gc, url->host, + url->port, is_ssl, _purple_http_connected, hc); + } else { + hc->socket = purple_http_socket_connect_new(hc->gc, url->host, + url->port, is_ssl, _purple_http_connected, hc); + } + + if (hc->socket_request == NULL && hc->socket == NULL) { + _purple_http_error(hc, _("Unable to connect to %s"), url->host); + return FALSE; + } + + purple_http_headers_free(hc->response->headers); + hc->response->headers = purple_http_headers_new(); + hc->response_buffer = g_string_new(""); + hc->main_header_got = FALSE; + hc->headers_got = FALSE; + if (hc->response->contents != NULL) + g_string_free(hc->response->contents, TRUE); + hc->response->contents = NULL; + hc->length_got = 0; + hc->length_got_decompressed = 0; + hc->length_expected = -1; + hc->is_chunked = FALSE; + hc->in_chunk = FALSE; + hc->chunks_done = FALSE; + + purple_http_conn_notify_progress_watcher(hc); + + return TRUE; +} + +/*** Performing HTTP requests *************************************************/ + +static gboolean purple_http_request_timeout(gpointer _hc) +{ + PurpleHttpConnection *hc = _hc; + + purple_debug_warning("http", "Timeout reached for request %p\n", hc); + + purple_http_conn_cancel(hc); + + return FALSE; +} + +PurpleHttpConnection * purple_http_get(PurpleConnection *gc, + PurpleHttpCallback callback, gpointer user_data, const gchar *url) +{ + PurpleHttpRequest *request; + PurpleHttpConnection *hc; + + g_return_val_if_fail(url != NULL, NULL); + + request = purple_http_request_new(url); + hc = purple_http_request(gc, request, callback, user_data); + purple_http_request_unref(request); + + return hc; +} + +PurpleHttpConnection * purple_http_get_printf(PurpleConnection *gc, + PurpleHttpCallback callback, gpointer user_data, + const gchar *format, ...) +{ + va_list args; + gchar *value; + PurpleHttpConnection *ret; + + g_return_val_if_fail(format != NULL, NULL); + + va_start(args, format); + value = g_strdup_vprintf(format, args); + va_end(args); + + ret = purple_http_get(gc, callback, user_data, value); + g_free(value); + + return ret; +} + +PurpleHttpConnection * purple_http_request(PurpleConnection *gc, + PurpleHttpRequest *request, PurpleHttpCallback callback, + gpointer user_data) +{ + PurpleHttpConnection *hc; + + g_return_val_if_fail(request != NULL, NULL); + + if (request->url == NULL) { + purple_debug_error("http", "Cannot perform new request - " + "URL is not set\n"); + return NULL; + } + + if (g_hash_table_lookup(purple_http_cancelling_gc, gc)) { + purple_debug_warning("http", "Cannot perform another HTTP " + "request while cancelling all related with this " + "PurpleConnection\n"); + return NULL; + } + + hc = purple_http_connection_new(request, gc); + hc->callback = callback; + hc->user_data = user_data; + + hc->url = purple_http_url_parse(request->url); + + if (purple_debug_is_unsafe()) + purple_debug_misc("http", "Performing new request %p for %s.\n", + hc, request->url); + else + purple_debug_misc("http", "Performing new request %p to %s.\n", + hc, hc->url ? hc->url->host : NULL); + + if (!hc->url || hc->url->host == NULL || hc->url->host[0] == '\0') { + purple_debug_error("http", "Invalid URL requested.\n"); + purple_http_connection_terminate(hc); + return NULL; + } + + _purple_http_reconnect(hc); + + hc->timeout_handle = g_timeout_add_seconds(request->timeout, + purple_http_request_timeout, hc); + + return hc; +} + +/*** HTTP connection API ******************************************************/ + +static void purple_http_connection_free(PurpleHttpConnection *hc); +static gboolean purple_http_conn_notify_progress_watcher_timeout(gpointer _hc); + +static PurpleHttpConnection * purple_http_connection_new( + PurpleHttpRequest *request, PurpleConnection *gc) +{ + PurpleHttpConnection *hc = g_new0(PurpleHttpConnection, 1); + + g_assert(request != NULL); + + hc->request = request; + purple_http_request_ref(request); + hc->response = purple_http_response_new(); + hc->is_keepalive = (request->keepalive_pool != NULL); + + hc->link_global = purple_http_hc_list = + g_list_prepend(purple_http_hc_list, hc); + g_hash_table_insert(purple_http_hc_by_ptr, hc, hc->link_global); + if (gc) { + GList *gc_list = g_hash_table_lookup(purple_http_hc_by_gc, gc); + g_hash_table_steal(purple_http_hc_by_gc, gc); + hc->link_gc = gc_list = g_list_prepend(gc_list, hc); + g_hash_table_insert(purple_http_hc_by_gc, gc, gc_list); + hc->gc = gc; + } + + return hc; +} + +static void purple_http_connection_free(PurpleHttpConnection *hc) +{ + if (hc->timeout_handle) + g_source_remove(hc->timeout_handle); + if (hc->watcher_delayed_handle) + g_source_remove(hc->watcher_delayed_handle); + + if (hc->connection_set != NULL) + purple_http_connection_set_remove(hc->connection_set, hc); + + purple_http_url_free(hc->url); + purple_http_request_unref(hc->request); + purple_http_response_free(hc->response); + + if (hc->contents_reader_buffer) + g_string_free(hc->contents_reader_buffer, TRUE); + purple_http_gz_free(hc->gz_stream); + + if (hc->request_header) + g_string_free(hc->request_header, TRUE); + + purple_http_hc_list = g_list_delete_link(purple_http_hc_list, + hc->link_global); + g_hash_table_remove(purple_http_hc_by_ptr, hc); + if (hc->gc) { + GList *gc_list, *gc_list_new; + gc_list = g_hash_table_lookup(purple_http_hc_by_gc, hc->gc); + g_assert(gc_list != NULL); + + gc_list_new = g_list_delete_link(gc_list, hc->link_gc); + if (gc_list != gc_list_new) { + g_hash_table_steal(purple_http_hc_by_gc, hc->gc); + if (gc_list_new) + g_hash_table_insert(purple_http_hc_by_gc, + hc->gc, gc_list_new); + } + } + + g_free(hc); +} + +/* call callback and do the cleanup */ +static void purple_http_connection_terminate(PurpleHttpConnection *hc) +{ + g_return_if_fail(hc != NULL); + + purple_debug_misc("http", "Request %p performed %s.\n", hc, + purple_http_response_is_successful(hc->response) ? + "successfully" : "without success"); + + if (hc->callback) + hc->callback(hc, hc->response, hc->user_data); + + purple_http_connection_free(hc); +} + +void purple_http_conn_cancel(PurpleHttpConnection *http_conn) +{ + if (http_conn == NULL) + return; + + if (http_conn->is_cancelling) + return; + http_conn->is_cancelling = TRUE; + + if (purple_debug_is_verbose()) { + purple_debug_misc("http", "Cancelling connection %p...\n", + http_conn); + } + + http_conn->response->code = 0; + _purple_http_disconnect(http_conn, FALSE); + purple_http_connection_terminate(http_conn); +} + +static void +purple_http_conn_retry(PurpleHttpConnection *http_conn) +{ + if (http_conn == NULL) + return; + + purple_debug_info("http", "Retrying connection %p...\n", http_conn); + + http_conn->response->code = 0; + _purple_http_disconnect(http_conn, FALSE); + _purple_http_reconnect(http_conn); +} + +void purple_http_conn_cancel_all(PurpleConnection *gc) +{ + GList *gc_list; + + if (purple_debug_is_verbose()) { + purple_debug_misc("http", "Cancelling all running HTTP " + "connections\n"); + } + + gc_list = g_hash_table_lookup(purple_http_hc_by_gc, gc); + + g_hash_table_insert(purple_http_cancelling_gc, gc, GINT_TO_POINTER(TRUE)); + + while (gc_list) { + PurpleHttpConnection *hc = gc_list->data; + gc_list = g_list_next(gc_list); + purple_http_conn_cancel(hc); + } + + g_hash_table_remove(purple_http_cancelling_gc, gc); + + if (NULL != g_hash_table_lookup(purple_http_hc_by_gc, gc)) + purple_debug_fatal("http", "Couldn't cancel all connections " + "related to gc=%p (it shouldn't happen)\n", gc); +} + +gboolean purple_http_conn_is_running(PurpleHttpConnection *http_conn) +{ + if (http_conn == NULL) + return FALSE; + return (NULL != g_hash_table_lookup(purple_http_hc_by_ptr, http_conn)); +} + +PurpleHttpRequest * purple_http_conn_get_request(PurpleHttpConnection *http_conn) +{ + g_return_val_if_fail(http_conn != NULL, NULL); + + return http_conn->request; +} + +PurpleHttpCookieJar * purple_http_conn_get_cookie_jar( + PurpleHttpConnection *http_conn) +{ + return purple_http_request_get_cookie_jar(purple_http_conn_get_request( + http_conn)); +} + +PurpleConnection * purple_http_conn_get_purple_connection( + PurpleHttpConnection *http_conn) +{ + g_return_val_if_fail(http_conn != NULL, NULL); + + return http_conn->gc; +} + +void purple_http_conn_set_progress_watcher(PurpleHttpConnection *http_conn, + PurpleHttpProgressWatcher watcher, gpointer user_data, + gint interval_threshold) +{ + g_return_if_fail(http_conn != NULL); + + if (interval_threshold < 0) { + interval_threshold = + PURPLE_HTTP_PROGRESS_WATCHER_DEFAULT_INTERVAL; + } + + http_conn->watcher = watcher; + http_conn->watcher_user_data = user_data; + http_conn->watcher_interval_threshold = interval_threshold; +} + +static void purple_http_conn_notify_progress_watcher( + PurpleHttpConnection *hc) +{ + gint64 now; + gboolean reading_state; + int processed, total; + + g_return_if_fail(hc != NULL); + + if (hc->watcher == NULL) + return; + + reading_state = hc->is_reading; + if (reading_state) { + total = hc->length_expected; + processed = hc->length_got; + } else { + total = hc->request->contents_length; + processed = hc->request_contents_written; + if (total == 0) + total = -1; + } + if (total != -1 && total < processed) { + purple_debug_warning("http", "Processed too much\n"); + total = processed; + } + + now = g_get_monotonic_time(); + if (hc->watcher_last_call + hc->watcher_interval_threshold + > now && processed != total) + { + if (hc->watcher_delayed_handle) + return; + hc->watcher_delayed_handle = g_timeout_add_seconds( + 1 + hc->watcher_interval_threshold / 1000000, + purple_http_conn_notify_progress_watcher_timeout, hc); + return; + } + + if (hc->watcher_delayed_handle) + g_source_remove(hc->watcher_delayed_handle); + hc->watcher_delayed_handle = 0; + + hc->watcher_last_call = now; + hc->watcher(hc, reading_state, processed, total, hc->watcher_user_data); +} + +static gboolean purple_http_conn_notify_progress_watcher_timeout(gpointer _hc) +{ + PurpleHttpConnection *hc = _hc; + + purple_http_conn_notify_progress_watcher(hc); + + return FALSE; +} + +/*** Cookie jar API ***********************************************************/ + +static PurpleHttpCookie * purple_http_cookie_new(const gchar *value); +void purple_http_cookie_free(PurpleHttpCookie *cookie); + +static void purple_http_cookie_jar_set_ext(PurpleHttpCookieJar *cookie_jar, + const gchar *name, const gchar *value, time_t expires); + +static PurpleHttpCookie * purple_http_cookie_new(const gchar *value) +{ + PurpleHttpCookie *cookie = g_new0(PurpleHttpCookie, 1); + + cookie->value = g_strdup(value); + cookie->expires = -1; + + return cookie; +} + +void purple_http_cookie_free(PurpleHttpCookie *cookie) +{ + g_free(cookie->value); + g_free(cookie); +} + +void purple_http_cookie_jar_free(PurpleHttpCookieJar *cookie_jar); + +PurpleHttpCookieJar * purple_http_cookie_jar_new(void) +{ + PurpleHttpCookieJar *cjar = g_new0(PurpleHttpCookieJar, 1); + + cjar->ref_count = 1; + cjar->tab = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, + (GDestroyNotify)purple_http_cookie_free); + + return cjar; +} + +void purple_http_cookie_jar_free(PurpleHttpCookieJar *cookie_jar) +{ + g_hash_table_destroy(cookie_jar->tab); + g_free(cookie_jar); +} + +void purple_http_cookie_jar_ref(PurpleHttpCookieJar *cookie_jar) +{ + g_return_if_fail(cookie_jar != NULL); + + cookie_jar->ref_count++; +} + +PurpleHttpCookieJar * purple_http_cookie_jar_unref( + PurpleHttpCookieJar *cookie_jar) +{ + if (cookie_jar == NULL) + return NULL; + + g_return_val_if_fail(cookie_jar->ref_count > 0, NULL); + + cookie_jar->ref_count--; + if (cookie_jar->ref_count > 0) + return cookie_jar; + + purple_http_cookie_jar_free(cookie_jar); + return NULL; +} + +static void purple_http_cookie_jar_parse(PurpleHttpCookieJar *cookie_jar, + GList *values) +{ + values = g_list_first(values); + while (values) { + const gchar *cookie = values->data; + const gchar *eqsign, *semicolon; + gchar *name, *value; + time_t expires = -1; + values = g_list_next(values); + + eqsign = strchr(cookie, '='); + semicolon = strchr(cookie, ';'); + + if (eqsign == NULL || eqsign == cookie || + (semicolon != NULL && semicolon < eqsign)) + { + if (purple_debug_is_unsafe()) + purple_debug_warning("http", + "Invalid cookie: [%s]\n", cookie); + else + purple_debug_warning("http", "Invalid cookie."); + continue; + } + + name = g_strndup(cookie, eqsign - cookie); + eqsign++; + if (semicolon != NULL) + value = g_strndup(eqsign, semicolon - eqsign); + else + value = g_strdup(eqsign); + + if (semicolon != NULL) { + GMatchInfo *match_info; + GRegex *re_expires = g_regex_new( /* XXX: make it static */ + "expires=([a-z0-9, :]+)", + G_REGEX_OPTIMIZE | G_REGEX_CASELESS, + G_REGEX_MATCH_NOTEMPTY, NULL); + + g_regex_match(re_expires, semicolon, 0, &match_info); + if (g_match_info_matches(match_info)) { + gchar *expire_date = + g_match_info_fetch(match_info, 1); + expires = purple_http_rfc1123_to_time( + expire_date); + g_free(expire_date); + } + g_match_info_free(match_info); + + g_regex_unref(re_expires); + } + + purple_http_cookie_jar_set_ext(cookie_jar, name, value, expires); + + g_free(name); + g_free(value); + } +} + +static gchar * purple_http_cookie_jar_gen(PurpleHttpCookieJar *cookie_jar) +{ + GHashTableIter it; + gchar *key; + PurpleHttpCookie *cookie; + GString *str; + time_t now = time(NULL); + + g_return_val_if_fail(cookie_jar != NULL, NULL); + + str = g_string_new(""); + + g_hash_table_iter_init(&it, cookie_jar->tab); + while (g_hash_table_iter_next(&it, (gpointer*)&key, + (gpointer*)&cookie)) + { + if (cookie->expires != -1 && cookie->expires != 0 && cookie->expires <= now) + continue; + g_string_append_printf(str, "%s=%s; ", key, cookie->value); + } + + if (str->len > 0) + g_string_truncate(str, str->len - 2); + return g_string_free(str, FALSE); +} + +void purple_http_cookie_jar_set(PurpleHttpCookieJar *cookie_jar, + const gchar *name, const gchar *value) +{ + gchar *escaped_name = g_strdup(purple_url_encode(name)); + gchar *escaped_value = NULL; + + if (value) { + escaped_value = g_strdup(purple_url_encode(value)); + } + + purple_http_cookie_jar_set_ext(cookie_jar, escaped_name, escaped_value, -1); + + g_free(escaped_name); + g_free(escaped_value); +} + +static void purple_http_cookie_jar_set_ext(PurpleHttpCookieJar *cookie_jar, + const gchar *name, const gchar *value, time_t expires) +{ + g_return_if_fail(cookie_jar != NULL); + g_return_if_fail(name != NULL); + + if (expires != -1 && expires != 0 && time(NULL) >= expires) + value = NULL; + + if (value != NULL) { + PurpleHttpCookie *cookie = purple_http_cookie_new(value); + cookie->expires = expires; + g_hash_table_insert(cookie_jar->tab, g_strdup(name), cookie); + } else + g_hash_table_remove(cookie_jar->tab, name); +} + +gchar * purple_http_cookie_jar_get(PurpleHttpCookieJar *cookie_jar, + const gchar *name) +{ + PurpleHttpCookie *cookie; + + g_return_val_if_fail(cookie_jar != NULL, NULL); + g_return_val_if_fail(name != NULL, NULL); + + cookie = g_hash_table_lookup(cookie_jar->tab, name); + if (!cookie) + return NULL; + + return g_strdup(purple_url_decode(cookie->value)); +} + +gchar * purple_http_cookie_jar_dump(PurpleHttpCookieJar *cjar) +{ + GHashTableIter it; + gchar *key; + PurpleHttpCookie *cookie; + GString *str = g_string_new(""); + + g_hash_table_iter_init(&it, cjar->tab); + while (g_hash_table_iter_next(&it, (gpointer*)&key, (gpointer*)&cookie)) + g_string_append_printf(str, "%s: %s (expires: %" G_GINT64_FORMAT + ")\n", key, cookie->value, (gint64)cookie->expires); + + if (str->len > 0) + g_string_truncate(str, str->len - 1); + return g_string_free(str, FALSE); +} + +gboolean purple_http_cookie_jar_is_empty(PurpleHttpCookieJar *cookie_jar) +{ + g_return_val_if_fail(cookie_jar != NULL, TRUE); + + return g_hash_table_size(cookie_jar->tab) == 0; +} + +/*** HTTP Keep-Alive pool API *************************************************/ + +static void +purple_http_keepalive_host_process_queue(PurpleHttpKeepaliveHost *host); + +static void +purple_http_keepalive_host_free(gpointer _host) +{ + PurpleHttpKeepaliveHost *host = _host; + + g_free(host->host); + + g_slist_free_full(host->queue, + (GDestroyNotify)purple_http_keepalive_pool_request_cancel); + g_slist_free_full(host->sockets, + (GDestroyNotify)purple_http_socket_close_free); + + if (host->process_queue_timeout > 0) { + g_source_remove(host->process_queue_timeout); + host->process_queue_timeout = 0; + } + + + g_free(host); +} + +PurpleHttpKeepalivePool * +purple_http_keepalive_pool_new(void) +{ + PurpleHttpKeepalivePool *pool = g_new0(PurpleHttpKeepalivePool, 1); + + pool->ref_count = 1; + pool->by_hash = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, + purple_http_keepalive_host_free); + + return pool; +} + +static void +purple_http_keepalive_pool_free(PurpleHttpKeepalivePool *pool) +{ + g_return_if_fail(pool != NULL); + + if (pool->is_destroying) + return; + pool->is_destroying = TRUE; + g_hash_table_destroy(pool->by_hash); + g_free(pool); +} + +void +purple_http_keepalive_pool_ref(PurpleHttpKeepalivePool *pool) +{ + g_return_if_fail(pool != NULL); + + pool->ref_count++; +} + +PurpleHttpKeepalivePool * +purple_http_keepalive_pool_unref(PurpleHttpKeepalivePool *pool) +{ + if (pool == NULL) + return NULL; + + g_return_val_if_fail(pool->ref_count > 0, NULL); + + pool->ref_count--; + if (pool->ref_count > 0) + return pool; + + purple_http_keepalive_pool_free(pool); + return NULL; +} + +static PurpleHttpKeepaliveRequest * +purple_http_keepalive_pool_request(PurpleHttpKeepalivePool *pool, + PurpleConnection *gc, const gchar *host, int port, gboolean is_ssl, + PurpleSocketConnectCb cb, gpointer user_data) +{ + PurpleHttpKeepaliveRequest *req; + PurpleHttpKeepaliveHost *kahost; + gchar *hash; + + g_return_val_if_fail(pool != NULL, NULL); + g_return_val_if_fail(host != NULL, NULL); + + if (pool->is_destroying) { + purple_debug_error("http", "pool is destroying\n"); + return NULL; + } + + hash = purple_http_socket_hash(host, port, is_ssl); + kahost = g_hash_table_lookup(pool->by_hash, hash); + + if (kahost == NULL) { + kahost = g_new0(PurpleHttpKeepaliveHost, 1); + kahost->pool = pool; + kahost->host = g_strdup(host); + kahost->port = port; + kahost->is_ssl = is_ssl; + + g_hash_table_insert(pool->by_hash, g_strdup(hash), kahost); + } + + g_free(hash); + + req = g_new0(PurpleHttpKeepaliveRequest, 1); + req->gc = gc; + req->cb = cb; + req->user_data = user_data; + req->host = kahost; + + kahost->queue = g_slist_append(kahost->queue, req); + + purple_http_keepalive_host_process_queue(kahost); + + return req; +} + +static void +_purple_http_keepalive_socket_connected(PurpleSocket *ps, + const gchar *error, gpointer _req) +{ + PurpleHttpSocket *hs = NULL; + PurpleHttpKeepaliveRequest *req = _req; + + if (ps != NULL) + hs = purple_socket_get_data(ps, "hs"); + + if (hs != NULL) + hs->use_count++; + + req->cb(ps, error, req->user_data); + g_free(req); +} + +static gboolean +_purple_http_keepalive_host_process_queue_cb(gpointer _host) +{ + PurpleHttpKeepaliveRequest *req; + PurpleHttpKeepaliveHost *host = _host; + PurpleHttpSocket *hs = NULL; + GSList *it; + guint sockets_count; + + g_return_val_if_fail(host != NULL, FALSE); + + host->process_queue_timeout = 0; + + if (host->queue == NULL) + return FALSE; + + sockets_count = 0; + it = host->sockets; + while (it != NULL) { + PurpleHttpSocket *hs_current = it->data; + + sockets_count++; + + if (!hs_current->is_busy) { + hs = hs_current; + break; + } + + it = g_slist_next(it); + } + + /* There are no free sockets and we cannot create another one. */ + if (hs == NULL && sockets_count >= host->pool->limit_per_host && + host->pool->limit_per_host > 0) + { + return FALSE; + } + + req = host->queue->data; + host->queue = g_slist_remove(host->queue, req); + + if (hs != NULL) { + if (purple_debug_is_verbose()) { + purple_debug_misc("http", "locking a (previously used) " + "socket: %p\n", hs); + } + + hs->is_busy = TRUE; + hs->use_count++; + + purple_http_keepalive_host_process_queue(host); + + req->cb(hs->ps, NULL, req->user_data); + g_free(req); + + return FALSE; + } + + hs = purple_http_socket_connect_new(req->gc, req->host->host, + req->host->port, req->host->is_ssl, + _purple_http_keepalive_socket_connected, req); + if (hs == NULL) { + purple_debug_error("http", "failed creating new socket"); + return FALSE; + } + + req->hs = hs; + hs->is_busy = TRUE; + hs->host = host; + + if (purple_debug_is_verbose()) + purple_debug_misc("http", "locking a (new) socket: %p\n", hs); + + host->sockets = g_slist_append(host->sockets, hs); + + return FALSE; +} + +static void +purple_http_keepalive_host_process_queue(PurpleHttpKeepaliveHost *host) +{ + g_return_if_fail(host != NULL); + + if (host->process_queue_timeout > 0) + return; + + host->process_queue_timeout = g_timeout_add(0, + _purple_http_keepalive_host_process_queue_cb, host); +} + +static void +purple_http_keepalive_pool_request_cancel(PurpleHttpKeepaliveRequest *req) +{ + if (req == NULL) + return; + + if (req->host != NULL) + req->host->queue = g_slist_remove(req->host->queue, req); + + if (req->hs != NULL) { + if (G_LIKELY(req->host)) { + req->host->sockets = g_slist_remove(req->host->sockets, + req->hs); + } + purple_http_socket_close_free(req->hs); + /* req should already be free'd here */ + } else { + req->cb(NULL, _("Cancelled"), req->user_data); + g_free(req); + } +} + +static void +purple_http_keepalive_pool_release(PurpleHttpSocket *hs, gboolean invalidate) +{ + PurpleHttpKeepaliveHost *host; + + if (hs == NULL) + return; + + if (purple_debug_is_verbose()) + purple_debug_misc("http", "releasing a socket: %p\n", hs); + + purple_socket_watch(hs->ps, 0, NULL, NULL); + hs->is_busy = FALSE; + host = hs->host; + + if (host == NULL) { + purple_http_socket_close_free(hs); + return; + } + + if (invalidate) { + host->sockets = g_slist_remove(host->sockets, hs); + purple_http_socket_close_free(hs); + } + + purple_http_keepalive_host_process_queue(host); +} + +void +purple_http_keepalive_pool_set_limit_per_host(PurpleHttpKeepalivePool *pool, + guint limit) +{ + g_return_if_fail(pool != NULL); + + pool->limit_per_host = limit; +} + +guint +purple_http_keepalive_pool_get_limit_per_host(PurpleHttpKeepalivePool *pool) +{ + g_return_val_if_fail(pool != NULL, 0); + + return pool->limit_per_host; +} + +/*** HTTP connection set API **************************************************/ + +PurpleHttpConnectionSet * +purple_http_connection_set_new(void) +{ + PurpleHttpConnectionSet *set; + + set = g_new0(PurpleHttpConnectionSet, 1); + set->connections = g_hash_table_new(g_direct_hash, g_direct_equal); + + return set; +} + +void +purple_http_connection_set_destroy(PurpleHttpConnectionSet *set) +{ + if (set == NULL) + return; + + set->is_destroying = TRUE; + + while (TRUE) { + GHashTableIter iter; + PurpleHttpConnection *http_conn; + + g_hash_table_iter_init(&iter, set->connections); + if (!g_hash_table_iter_next(&iter, (gpointer*)&http_conn, NULL)) + break; + + purple_http_conn_cancel(http_conn); + } + + g_hash_table_destroy(set->connections); + g_free(set); +} + +void +purple_http_connection_set_add(PurpleHttpConnectionSet *set, + PurpleHttpConnection *http_conn) +{ + if (set->is_destroying) + return; + if (http_conn->connection_set == set) + return; + if (http_conn->connection_set != NULL) { + purple_http_connection_set_remove(http_conn->connection_set, + http_conn); + } + g_hash_table_insert(set->connections, http_conn, (gpointer)TRUE); + http_conn->connection_set = set; +} + +static void +purple_http_connection_set_remove(PurpleHttpConnectionSet *set, + PurpleHttpConnection *http_conn) +{ + g_hash_table_remove(set->connections, http_conn); + if (http_conn->connection_set == set) + http_conn->connection_set = NULL; +} + +/*** Request API **************************************************************/ + +static void purple_http_request_free(PurpleHttpRequest *request); + +PurpleHttpRequest * purple_http_request_new(const gchar *url) +{ + PurpleHttpRequest *request; + + request = g_new0(PurpleHttpRequest, 1); + + request->ref_count = 1; + request->url = g_strdup(url); + request->headers = purple_http_headers_new(); + request->cookie_jar = purple_http_cookie_jar_new(); + request->keepalive_pool = purple_http_keepalive_pool_new(); + + request->timeout = PURPLE_HTTP_REQUEST_DEFAULT_TIMEOUT; + request->max_redirects = PURPLE_HTTP_REQUEST_DEFAULT_MAX_REDIRECTS; + request->http11 = TRUE; + request->max_length = PURPLE_HTTP_REQUEST_DEFAULT_MAX_LENGTH; + + return request; +} + +static void purple_http_request_free(PurpleHttpRequest *request) +{ + purple_http_headers_free(request->headers); + purple_http_cookie_jar_unref(request->cookie_jar); + purple_http_keepalive_pool_unref(request->keepalive_pool); + g_free(request->method); + g_free(request->contents); + g_free(request->url); + g_free(request); +} + +void purple_http_request_ref(PurpleHttpRequest *request) +{ + g_return_if_fail(request != NULL); + + request->ref_count++; +} + +PurpleHttpRequest * purple_http_request_unref(PurpleHttpRequest *request) +{ + if (request == NULL) + return NULL; + + g_return_val_if_fail(request->ref_count > 0, NULL); + + request->ref_count--; + if (request->ref_count > 0) + return request; + + purple_http_request_free(request); + return NULL; +} + +void purple_http_request_set_url(PurpleHttpRequest *request, const gchar *url) +{ + g_return_if_fail(request != NULL); + g_return_if_fail(url != NULL); + + g_free(request->url); + request->url = g_strdup(url); +} + +void purple_http_request_set_url_printf(PurpleHttpRequest *request, + const gchar *format, ...) +{ + va_list args; + gchar *value; + + g_return_if_fail(request != NULL); + g_return_if_fail(format != NULL); + + va_start(args, format); + value = g_strdup_vprintf(format, args); + va_end(args); + + purple_http_request_set_url(request, value); + g_free(value); +} + +const gchar * purple_http_request_get_url(PurpleHttpRequest *request) +{ + g_return_val_if_fail(request != NULL, NULL); + + return request->url; +} + +void purple_http_request_set_method(PurpleHttpRequest *request, const gchar *method) +{ + g_return_if_fail(request != NULL); + + g_free(request->method); + request->method = g_strdup(method); +} + +const gchar * purple_http_request_get_method(PurpleHttpRequest *request) +{ + g_return_val_if_fail(request != NULL, NULL); + + return request->method; +} + +static gboolean purple_http_request_is_method(PurpleHttpRequest *request, + const gchar *method) +{ + const gchar *rmethod; + + g_return_val_if_fail(request != NULL, FALSE); + g_return_val_if_fail(method != NULL, FALSE); + + rmethod = purple_http_request_get_method(request); + if (rmethod == NULL) + return (g_ascii_strcasecmp(method, "get") == 0); + return (g_ascii_strcasecmp(method, rmethod) == 0); +} + +void +purple_http_request_set_keepalive_pool(PurpleHttpRequest *request, + PurpleHttpKeepalivePool *pool) +{ + g_return_if_fail(request != NULL); + + if (pool != NULL) + purple_http_keepalive_pool_ref(pool); + + if (request->keepalive_pool != NULL) { + purple_http_keepalive_pool_unref(request->keepalive_pool); + request->keepalive_pool = NULL; + } + + if (pool != NULL) + request->keepalive_pool = pool; +} + +PurpleHttpKeepalivePool * +purple_http_request_get_keepalive_pool(PurpleHttpRequest *request) +{ + g_return_val_if_fail(request != NULL, FALSE); + + return request->keepalive_pool; +} + +void purple_http_request_set_contents(PurpleHttpRequest *request, + const gchar *contents, int length) +{ + g_return_if_fail(request != NULL); + g_return_if_fail(length >= -1); + + request->contents_reader = NULL; + request->contents_reader_data = NULL; + + g_free(request->contents); + if (contents == NULL || length == 0) { + request->contents = NULL; + request->contents_length = 0; + return; + } + + if (length == -1) + length = strlen(contents); + request->contents = g_memdup(contents, length); + request->contents_length = length; +} + +void purple_http_request_set_contents_reader(PurpleHttpRequest *request, + PurpleHttpContentReader reader, int contents_length, gpointer user_data) +{ + g_return_if_fail(request != NULL); + g_return_if_fail(reader != NULL); + g_return_if_fail(contents_length >= -1); + + g_free(request->contents); + request->contents = NULL; + request->contents_length = contents_length; + request->contents_reader = reader; + request->contents_reader_data = user_data; +} + +void purple_http_request_set_response_writer(PurpleHttpRequest *request, + PurpleHttpContentWriter writer, gpointer user_data) +{ + g_return_if_fail(request != NULL); + + if (writer == NULL) + user_data = NULL; + request->response_writer = writer; + request->response_writer_data = user_data; +} + +void purple_http_request_set_timeout(PurpleHttpRequest *request, int timeout) +{ + g_return_if_fail(request != NULL); + + if (timeout < -1) + timeout = -1; + + request->timeout = timeout; +} + +int purple_http_request_get_timeout(PurpleHttpRequest *request) +{ + g_return_val_if_fail(request != NULL, -1); + + return request->timeout; +} + +void purple_http_request_set_max_redirects(PurpleHttpRequest *request, + int max_redirects) +{ + g_return_if_fail(request != NULL); + + if (max_redirects < -1) + max_redirects = -1; + + request->max_redirects = max_redirects; +} + +int purple_http_request_get_max_redirects(PurpleHttpRequest *request) +{ + g_return_val_if_fail(request != NULL, -1); + + return request->max_redirects; +} + +void purple_http_request_set_cookie_jar(PurpleHttpRequest *request, + PurpleHttpCookieJar *cookie_jar) +{ + g_return_if_fail(request != NULL); + g_return_if_fail(cookie_jar != NULL); + + purple_http_cookie_jar_ref(cookie_jar); + purple_http_cookie_jar_unref(request->cookie_jar); + request->cookie_jar = cookie_jar; +} + +PurpleHttpCookieJar * purple_http_request_get_cookie_jar( + PurpleHttpRequest *request) +{ + g_return_val_if_fail(request != NULL, NULL); + + return request->cookie_jar; +} + +void purple_http_request_set_http11(PurpleHttpRequest *request, gboolean http11) +{ + g_return_if_fail(request != NULL); + + request->http11 = http11; +} + +gboolean purple_http_request_is_http11(PurpleHttpRequest *request) +{ + g_return_val_if_fail(request != NULL, FALSE); + + return request->http11; +} + +void purple_http_request_set_max_len(PurpleHttpRequest *request, int max_len) +{ + g_return_if_fail(request != NULL); + + if (max_len < 0 || max_len > PURPLE_HTTP_REQUEST_HARD_MAX_LENGTH) + max_len = PURPLE_HTTP_REQUEST_HARD_MAX_LENGTH; + + request->max_length = max_len; +} + +int purple_http_request_get_max_len(PurpleHttpRequest *request) +{ + g_return_val_if_fail(request != NULL, -1); + + return request->max_length; +} + +void purple_http_request_header_set(PurpleHttpRequest *request, + const gchar *key, const gchar *value) +{ + g_return_if_fail(request != NULL); + g_return_if_fail(key != NULL); + + purple_http_headers_remove(request->headers, key); + if (value) + purple_http_headers_add(request->headers, key, value); +} + +void purple_http_request_header_set_printf(PurpleHttpRequest *request, + const gchar *key, const gchar *format, ...) +{ + va_list args; + gchar *value; + + g_return_if_fail(request != NULL); + g_return_if_fail(key != NULL); + g_return_if_fail(format != NULL); + + va_start(args, format); + value = g_strdup_vprintf(format, args); + va_end(args); + + purple_http_request_header_set(request, key, value); + g_free(value); +} + +void purple_http_request_header_add(PurpleHttpRequest *request, + const gchar *key, const gchar *value) +{ + g_return_if_fail(request != NULL); + g_return_if_fail(key != NULL); + + purple_http_headers_add(request->headers, key, value); +} + +/*** HTTP response API ********************************************************/ + +static PurpleHttpResponse * purple_http_response_new(void) +{ + PurpleHttpResponse *response = g_new0(PurpleHttpResponse, 1); + + return response; +} + +static void purple_http_response_free(PurpleHttpResponse *response) +{ + if (response->contents != NULL) + g_string_free(response->contents, TRUE); + g_free(response->error); + purple_http_headers_free(response->headers); + g_free(response); +} + +gboolean purple_http_response_is_successful(PurpleHttpResponse *response) +{ + int code; + + g_return_val_if_fail(response != NULL, FALSE); + + code = response->code; + + if (code <= 0) + return FALSE; + + /* TODO: HTTP/1.1 100 Continue */ + + if (code / 100 == 2) + return TRUE; + + return FALSE; +} + +int purple_http_response_get_code(PurpleHttpResponse *response) +{ + g_return_val_if_fail(response != NULL, 0); + + return response->code; +} + +const gchar * purple_http_response_get_error(PurpleHttpResponse *response) +{ + g_return_val_if_fail(response != NULL, NULL); + + if (response->error != NULL) + return response->error; + + if (!purple_http_response_is_successful(response)) { + static gchar errmsg[200]; + if (response->code <= 0) { + g_snprintf(errmsg, sizeof(errmsg), + _("Unknown HTTP error")); + } else { + g_snprintf(errmsg, sizeof(errmsg), + _("Invalid HTTP response code (%d)"), + response->code); + } + return errmsg; + } + + return NULL; +} + +gsize purple_http_response_get_data_len(PurpleHttpResponse *response) +{ + g_return_val_if_fail(response != NULL, 0); + + if (response->contents == NULL) + return 0; + + return response->contents->len; +} + +const gchar * purple_http_response_get_data(PurpleHttpResponse *response, size_t *len) +{ + const gchar *ret = ""; + + g_return_val_if_fail(response != NULL, ""); + + if (response->contents != NULL) { + ret = response->contents->str; + if (len) + *len = response->contents->len; + } else { + if (len) + *len = 0; + } + + return ret; +} + +const GList * purple_http_response_get_all_headers(PurpleHttpResponse *response) +{ + g_return_val_if_fail(response != NULL, NULL); + + return purple_http_headers_get_all(response->headers); +} + +const GList * purple_http_response_get_headers_by_name( + PurpleHttpResponse *response, const gchar *name) +{ + g_return_val_if_fail(response != NULL, NULL); + g_return_val_if_fail(name != NULL, NULL); + + return purple_http_headers_get_all_by_name(response->headers, name); +} + +const gchar * purple_http_response_get_header(PurpleHttpResponse *response, + const gchar *name) +{ + g_return_val_if_fail(response != NULL, NULL); + g_return_val_if_fail(name != NULL, NULL); + + return purple_http_headers_get(response->headers, name); +} + +/*** URL functions ************************************************************/ + +PurpleHttpURL * +purple_http_url_parse(const char *raw_url) +{ + PurpleHttpURL *url; + GMatchInfo *match_info; + + gchar *host_full, *tmp; + + g_return_val_if_fail(raw_url != NULL, NULL); + + if (!g_regex_match(purple_http_re_url, raw_url, 0, &match_info)) { + if (purple_debug_is_verbose() && purple_debug_is_unsafe()) { + purple_debug_warning("http", + "Invalid URL provided: %s\n", + raw_url); + } + return NULL; + } + + url = g_new0(PurpleHttpURL, 1); + + url->protocol = g_match_info_fetch(match_info, 1); + host_full = g_match_info_fetch(match_info, 2); + url->path = g_match_info_fetch(match_info, 3); + url->fragment = g_match_info_fetch(match_info, 4); + g_match_info_free(match_info); + + if (g_strcmp0(url->protocol, "") == 0) { + g_free(url->protocol); + url->protocol = NULL; + } else if (url->protocol != NULL) { + tmp = url->protocol; + url->protocol = g_ascii_strdown(url->protocol, -1); + g_free(tmp); + } + if (host_full[0] == '\0') { + g_free(host_full); + host_full = NULL; + } + if (url->path[0] == '\0') { + g_free(url->path); + url->path = NULL; + } + if ((url->protocol == NULL) != (host_full == NULL)) + purple_debug_warning("http", "Protocol or host not present " + "(unlikely case)\n"); + + if (host_full) { + gchar *port_str; + + if (!g_regex_match(purple_http_re_url_host, host_full, 0, + &match_info)) + { + if (purple_debug_is_verbose() && + purple_debug_is_unsafe()) + { + purple_debug_warning("http", + "Invalid host provided for URL: %s\n", + raw_url); + } + + g_free(host_full); + purple_http_url_free(url); + return NULL; + } + + url->username = g_match_info_fetch(match_info, 1); + url->password = g_match_info_fetch(match_info, 2); + url->host = g_match_info_fetch(match_info, 3); + port_str = g_match_info_fetch(match_info, 4); + + if (port_str && port_str[0]) + url->port = atoi(port_str); + + if (url->username[0] == '\0') { + g_free(url->username); + url->username = NULL; + } + if (url->password[0] == '\0') { + g_free(url->password); + url->password = NULL; + } + if (g_strcmp0(url->host, "") == 0) { + g_free(url->host); + url->host = NULL; + } else if (url->host != NULL) { + tmp = url->host; + url->host = g_ascii_strdown(url->host, -1); + g_free(tmp); + } + + g_free(port_str); + g_match_info_free(match_info); + + g_free(host_full); + host_full = NULL; + } + + if (url->host != NULL) { + if (url->protocol == NULL) + url->protocol = g_strdup("http"); + if (url->port == 0 && 0 == strcmp(url->protocol, "http")) + url->port = 80; + if (url->port == 0 && 0 == strcmp(url->protocol, "https")) + url->port = 443; + if (url->path == NULL) + url->path = g_strdup("/"); + if (url->path[0] != '/') + purple_debug_warning("http", + "URL path doesn't start with slash\n"); + } + + return url; +} + +void +purple_http_url_free(PurpleHttpURL *parsed_url) +{ + if (parsed_url == NULL) + return; + + g_free(parsed_url->protocol); + g_free(parsed_url->username); + g_free(parsed_url->password); + g_free(parsed_url->host); + g_free(parsed_url->path); + g_free(parsed_url->fragment); + g_free(parsed_url); +} + +void +purple_http_url_relative(PurpleHttpURL *base_url, PurpleHttpURL *relative_url) +{ + g_return_if_fail(base_url != NULL); + g_return_if_fail(relative_url != NULL); + + if (relative_url->host) { + g_free(base_url->protocol); + base_url->protocol = g_strdup(relative_url->protocol); + g_free(base_url->username); + base_url->username = g_strdup(relative_url->username); + g_free(base_url->password); + base_url->password = g_strdup(relative_url->password); + g_free(base_url->host); + base_url->host = g_strdup(relative_url->host); + base_url->port = relative_url->port; + + g_free(base_url->path); + base_url->path = NULL; + } + + if (relative_url->path) { + if (relative_url->path[0] == '/' || + base_url->path == NULL) + { + g_free(base_url->path); + base_url->path = g_strdup(relative_url->path); + } else { + gchar *last_slash = strrchr(base_url->path, '/'); + gchar *tmp; + if (last_slash == NULL) + base_url->path[0] = '\0'; + else + last_slash[1] = '\0'; + tmp = base_url->path; + base_url->path = g_strconcat(base_url->path, + relative_url->path, NULL); + g_free(tmp); + } + } + + g_free(base_url->fragment); + base_url->fragment = g_strdup(relative_url->fragment); +} + +gchar * +purple_http_url_print(PurpleHttpURL *parsed_url) +{ + GString *url = g_string_new(""); + gboolean before_host_printed = FALSE, host_printed = FALSE; + gboolean port_is_default = FALSE; + + if (parsed_url->protocol) { + g_string_append_printf(url, "%s://", parsed_url->protocol); + before_host_printed = TRUE; + if (parsed_url->port == 80 && 0 == strcmp(parsed_url->protocol, + "http")) + port_is_default = TRUE; + if (parsed_url->port == 443 && 0 == strcmp(parsed_url->protocol, + "https")) + port_is_default = TRUE; + } + if (parsed_url->username || parsed_url->password) { + if (parsed_url->username) + g_string_append(url, parsed_url->username); + g_string_append_printf(url, ":%s", parsed_url->password); + g_string_append(url, "@"); + before_host_printed = TRUE; + } + if (parsed_url->host || parsed_url->port) { + if (!parsed_url->host) + g_string_append_printf(url, "{???}:%d", + parsed_url->port); + else { + g_string_append(url, parsed_url->host); + if (!port_is_default) + g_string_append_printf(url, ":%d", + parsed_url->port); + } + host_printed = TRUE; + } + if (parsed_url->path) { + if (!host_printed && before_host_printed) + g_string_append(url, "{???}"); + g_string_append(url, parsed_url->path); + } + if (parsed_url->fragment) + g_string_append_printf(url, "#%s", parsed_url->fragment); + + return g_string_free(url, FALSE); +} + +const gchar * +purple_http_url_get_protocol(const PurpleHttpURL *parsed_url) +{ + g_return_val_if_fail(parsed_url != NULL, NULL); + + return parsed_url->protocol; +} + +const gchar * +purple_http_url_get_username(const PurpleHttpURL *parsed_url) +{ + g_return_val_if_fail(parsed_url != NULL, NULL); + + return parsed_url->username; +} + +const gchar * +purple_http_url_get_password(const PurpleHttpURL *parsed_url) +{ + g_return_val_if_fail(parsed_url != NULL, NULL); + + return parsed_url->password; +} + +const gchar * +purple_http_url_get_host(const PurpleHttpURL *parsed_url) +{ + g_return_val_if_fail(parsed_url != NULL, NULL); + + return parsed_url->host; +} + +int +purple_http_url_get_port(const PurpleHttpURL *parsed_url) +{ + g_return_val_if_fail(parsed_url != NULL, 0); + + return parsed_url->port; +} + +const gchar * +purple_http_url_get_path(const PurpleHttpURL *parsed_url) +{ + g_return_val_if_fail(parsed_url != NULL, NULL); + + return parsed_url->path; +} + +const gchar * +purple_http_url_get_fragment(const PurpleHttpURL *parsed_url) +{ + g_return_val_if_fail(parsed_url != NULL, NULL); + + return parsed_url->fragment; +} + +/*** HTTP Subsystem ***********************************************************/ + +void purple_http_init(void) +{ + purple_http_re_url = g_regex_new("^" + + "(?:" /* host part beginning */ + "([a-z]+)\\:/*" /* protocol */ + "([^/]+)" /* username, password, host, port */ + ")?" /* host part ending */ + + "([^#]*)" /* path */ + + "(?:#" "(.*)" ")?" /* fragment */ + + "$", G_REGEX_OPTIMIZE | G_REGEX_CASELESS, + G_REGEX_MATCH_NOTEMPTY, NULL); + + purple_http_re_url_host = g_regex_new("^" + + "(?:" /* user credentials part beginning */ + "([" PURPLE_HTTP_URL_CREDENTIALS_CHARS "]+)" /* username */ + "(?::([" PURPLE_HTTP_URL_CREDENTIALS_CHARS "]+))" /* password */ + "@)?" /* user credentials part ending */ + + "([a-z0-9.-]+)" /* host */ + "(?::([0-9]+))?" /* port*/ + + "$", G_REGEX_OPTIMIZE | G_REGEX_CASELESS, + G_REGEX_MATCH_NOTEMPTY, NULL); + + purple_http_re_rfc1123 = g_regex_new( + "^[a-z]+, " /* weekday */ + "([0-9]+) " /* date */ + "([a-z]+) " /* month */ + "([0-9]+) " /* year */ + "([0-9]+:[0-9]+:[0-9]+) " /* time */ + "(?:GMT|UTC)$", + G_REGEX_OPTIMIZE | G_REGEX_CASELESS, + G_REGEX_MATCH_NOTEMPTY, NULL); + + purple_http_hc_list = NULL; + purple_http_hc_by_ptr = g_hash_table_new(g_direct_hash, g_direct_equal); + purple_http_hc_by_gc = g_hash_table_new_full(g_direct_hash, + g_direct_equal, NULL, (GDestroyNotify)g_list_free); + purple_http_cancelling_gc = g_hash_table_new(g_direct_hash, g_direct_equal); +} + +static void purple_http_foreach_conn_cancel(gpointer _hc, gpointer user_data) +{ + PurpleHttpConnection *hc = _hc; + purple_http_conn_cancel(hc); +} + +void purple_http_uninit(void) +{ + g_regex_unref(purple_http_re_url); + purple_http_re_url = NULL; + g_regex_unref(purple_http_re_url_host); + purple_http_re_url_host = NULL; + g_regex_unref(purple_http_re_rfc1123); + purple_http_re_rfc1123 = NULL; + + g_list_foreach(purple_http_hc_list, purple_http_foreach_conn_cancel, + NULL); + + if (purple_http_hc_list != NULL || + 0 != g_hash_table_size(purple_http_hc_by_ptr) || + 0 != g_hash_table_size(purple_http_hc_by_gc)) + purple_debug_warning("http", + "Couldn't cleanup all connections.\n"); + + g_list_free(purple_http_hc_list); + purple_http_hc_list = NULL; + g_hash_table_destroy(purple_http_hc_by_gc); + purple_http_hc_by_gc = NULL; + g_hash_table_destroy(purple_http_hc_by_ptr); + purple_http_hc_by_ptr = NULL; + g_hash_table_destroy(purple_http_cancelling_gc); + purple_http_cancelling_gc = NULL; +} diff --git a/purple2compat/http.h b/purple2compat/http.h new file mode 100644 index 00000000..870d7664 --- /dev/null +++ b/purple2compat/http.h @@ -0,0 +1,964 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#ifndef _PURPLE_HTTP_H_ +#define _PURPLE_HTTP_H_ +/** + * SECTION:http + * @section_id: libpurple-http + * @short_description: http.h + * @title: HTTP API + */ + +#include + +#include "connection.h" + +/** + * PurpleHttpRequest: + * + * A structure containing all data required to generate a single HTTP request. + */ +typedef struct _PurpleHttpRequest PurpleHttpRequest; + +/** + * PurpleHttpConnection: + * + * A representation of actually running HTTP request. Can be used to cancel the + * request. + */ +typedef struct _PurpleHttpConnection PurpleHttpConnection; + +/** + * PurpleHttpResponse: + * + * All information got with response for HTTP request. + */ +typedef struct _PurpleHttpResponse PurpleHttpResponse; + +/** + * PurpleHttpURL: + * + * Parsed representation for the URL. + */ +typedef struct _PurpleHttpURL PurpleHttpURL; + +/** + * PurpleHttpCookieJar: + * + * An collection of cookies, got from HTTP response or provided for HTTP + * request. + */ +typedef struct _PurpleHttpCookieJar PurpleHttpCookieJar; + +/** + * PurpleHttpKeepalivePool: + * + * A pool of TCP connections for HTTP Keep-Alive session. + */ +typedef struct _PurpleHttpKeepalivePool PurpleHttpKeepalivePool; + +/** + * PurpleHttpConnectionSet: + * + * A set of running HTTP requests. Can be used to cancel all of them at once. + */ +typedef struct _PurpleHttpConnectionSet PurpleHttpConnectionSet; + +/** + * PurpleHttpCallback: + * + * An callback called after performing (successfully or not) HTTP request. + */ +typedef void (*PurpleHttpCallback)(PurpleHttpConnection *http_conn, + PurpleHttpResponse *response, gpointer user_data); + +/** + * PurpleHttpContentReaderCb: + * + * An callback called after storing data requested by PurpleHttpContentReader. + */ +typedef void (*PurpleHttpContentReaderCb)(PurpleHttpConnection *http_conn, + gboolean success, gboolean eof, size_t stored); + +/** + * PurpleHttpContentReader: + * @http_conn: Connection, which requests data. + * @buffer: Buffer to store data to (with offset ignored). + * @offset: Position, from where to read data. + * @length: Length of data to read. + * @user_data: The user data passed with callback function. + * @cb: The function to call after storing data to buffer. + * + * An callback for getting large request contents (ie. from file stored on + * disk). + */ +typedef void (*PurpleHttpContentReader)(PurpleHttpConnection *http_conn, + gchar *buffer, size_t offset, size_t length, gpointer user_data, + PurpleHttpContentReaderCb cb); + +/** + * PurpleHttpContentWriter: + * @http_conn: Connection, which requests data. + * @response: Response at point got so far (may change later). + * @buffer: Buffer to read data from (with offset ignored). + * @offset: Position of data got (its value is offset + length of + * previous call), can be safely ignored. + * @length: Length of data read. + * @user_data: The user data passed with callback function. + * + * An callback for writting large response contents. + * + * Returns: TRUE, if succeeded, FALSE otherwise. + */ +typedef gboolean (*PurpleHttpContentWriter)(PurpleHttpConnection *http_conn, + PurpleHttpResponse *response, const gchar *buffer, size_t offset, + size_t length, gpointer user_data); + +/** + * PurpleHttpProgressWatcher: + * @http_conn: The HTTP Connection. + * @reading_state: FALSE, is we are sending the request, TRUE, when reading + * the response. + * @processed: The amount of data already processed. + * @total: Total amount of data (in current state). + * @user_data: The user data passed with callback function. + * + * An callback for watching HTTP connection progress. + */ +typedef void (*PurpleHttpProgressWatcher)(PurpleHttpConnection *http_conn, + gboolean reading_state, int processed, int total, gpointer user_data); + +G_BEGIN_DECLS + +/**************************************************************************/ +/* Performing HTTP requests */ +/**************************************************************************/ + +/** + * purple_http_get: + * @gc: The connection for which the request is needed, or NULL. + * @callback: (scope call): The callback function. + * @user_data: The user data to pass to the callback function. + * @url: The URL. + * + * Fetches the data from a URL with GET request, and passes it to a callback + * function. + * + * Returns: The HTTP connection struct. + */ +PurpleHttpConnection * purple_http_get(PurpleConnection *gc, + PurpleHttpCallback callback, gpointer user_data, const gchar *url); + +/** + * purple_http_get_printf: + * @gc: The connection for which the request is needed, or NULL. + * @callback: (scope call): The callback function. + * @user_data: The user data to pass to the callback function. + * @format: The format string. + * @...: The parameters to insert into the format string. + * + * Constructs an URL and fetches the data from it with GET request, then passes + * it to a callback function. + * + * Returns: The HTTP connection struct. + */ +PurpleHttpConnection * purple_http_get_printf(PurpleConnection *gc, + PurpleHttpCallback callback, gpointer user_data, + const gchar *format, ...) G_GNUC_PRINTF(4, 5); + +/** + * purple_http_request: + * @gc: The connection for which the request is needed, or NULL. + * @request: The request. + * @callback: (scope call): The callback function. + * @user_data: The user data to pass to the callback function. + * + * Fetches a HTTP request and passes the response to a callback function. + * Provided request struct can be shared by multiple http requests but can not + * be modified when any of these is running. + * + * Returns: The HTTP connection struct. + */ +PurpleHttpConnection * purple_http_request(PurpleConnection *gc, + PurpleHttpRequest *request, PurpleHttpCallback callback, + gpointer user_data); + +/**************************************************************************/ +/* HTTP connection API */ +/**************************************************************************/ + +/** + * purple_http_conn_cancel: + * @http_conn: The data returned when you initiated the HTTP request. + * + * Cancel a pending HTTP request. + */ +void purple_http_conn_cancel(PurpleHttpConnection *http_conn); + +/** + * purple_http_conn_cancel_all: + * @gc: The handle. + * + * Cancels all HTTP connections associated with the specified handle. + */ +void purple_http_conn_cancel_all(PurpleConnection *gc); + +/** + * purple_http_conn_is_running: + * @http_conn: The HTTP connection (may be invalid pointer). + * + * Checks, if provided HTTP request is running. + * + * Returns: TRUE, if provided connection is currently running. + */ +gboolean purple_http_conn_is_running(PurpleHttpConnection *http_conn); + +/** + * purple_http_conn_get_request: + * @http_conn: The HTTP connection. + * + * Gets PurpleHttpRequest used for specified HTTP connection. + * + * Returns: The PurpleHttpRequest object. + */ +PurpleHttpRequest * purple_http_conn_get_request( + PurpleHttpConnection *http_conn); + +/** + * purple_http_conn_get_cookie_jar: + * @http_conn: The HTTP connection. + * + * Gets cookie jar used within connection. + * + * Returns: The cookie jar. + */ +PurpleHttpCookieJar * purple_http_conn_get_cookie_jar( + PurpleHttpConnection *http_conn); + +/** + * purple_http_conn_get_purple_connection: + * @http_conn: The HTTP connection. + * + * Gets PurpleConnection tied with specified HTTP connection. + * + * Returns: The PurpleConnection object. + */ +PurpleConnection * purple_http_conn_get_purple_connection( + PurpleHttpConnection *http_conn); + +/** + * purple_http_conn_set_progress_watcher: + * @http_conn: The HTTP connection. + * @watcher: (scope call): The watcher. + * @user_data: The user data to pass to the callback function. + * @interval_threshold: Minimum interval (in microseconds) of calls to + * watcher, or -1 for default. + * + * Sets the watcher, called after writing or reading data to/from HTTP stream. + * May be used for updating transfer progress gauge. + */ +void purple_http_conn_set_progress_watcher(PurpleHttpConnection *http_conn, + PurpleHttpProgressWatcher watcher, gpointer user_data, + gint interval_threshold); + + +/**************************************************************************/ +/* URL processing API */ +/**************************************************************************/ + +/** + * purple_http_url_parse: + * @url: The URL to parse. + * + * Parses a URL. + * + * The returned data must be freed with purple_http_url_free. + * + * Returns: The parsed url or NULL, if the URL is invalid. + */ +PurpleHttpURL * +purple_http_url_parse(const char *url); + +/** + * purple_http_url_free: + * @parsed_url: The parsed URL struct, or NULL. + * + * Frees the parsed URL struct. + */ +void +purple_http_url_free(PurpleHttpURL *parsed_url); + +/** + * purple_http_url_relative: + * @base_url: The base URL. The result is stored here. + * @relative_url: The relative URL. + * + * Converts the base URL to the absolute form of the provided relative URL. + * + * Example: "https://example.com/path/to/file.html" + "subdir/other-file.html" = + * "https://example.com/path/to/subdir/another-file.html" + */ +void +purple_http_url_relative(PurpleHttpURL *base_url, PurpleHttpURL *relative_url); + +/** + * purple_http_url_print: + * @parsed_url: The URL struct. + * + * Converts the URL struct to the printable form. The result may not be a valid + * URL (in cases, when the struct doesn't have all fields filled properly). + * + * The result must be g_free'd. + * + * Returns: The printable form of the URL. + */ +gchar * +purple_http_url_print(PurpleHttpURL *parsed_url); + +/** + * purple_http_url_get_protocol: + * @parsed_url: The URL struct. + * + * Gets the protocol part of URL. + * + * Returns: The protocol. + */ +const gchar * +purple_http_url_get_protocol(const PurpleHttpURL *parsed_url); + +/** + * purple_http_url_get_username: + * @parsed_url: The URL struct. + * + * Gets the username part of URL. + * + * Returns: The username. + */ +const gchar * +purple_http_url_get_username(const PurpleHttpURL *parsed_url); + +/** + * purple_http_url_get_password: + * @parsed_url: The URL struct. + * + * Gets the password part of URL. + * + * Returns: The password. + */ +const gchar * +purple_http_url_get_password(const PurpleHttpURL *parsed_url); + +/** + * purple_http_url_get_host: + * @parsed_url: The URL struct. + * + * Gets the hostname part of URL. + * + * Returns: The hostname. + */ +const gchar * +purple_http_url_get_host(const PurpleHttpURL *parsed_url); + +/** + * purple_http_url_get_port: + * @parsed_url: The URL struct. + * + * Gets the port part of URL. + * + * Returns: The port number. + */ +int +purple_http_url_get_port(const PurpleHttpURL *parsed_url); + +/** + * purple_http_url_get_path: + * @parsed_url: The URL struct. + * + * Gets the path part of URL. + * + * Returns: The path. + */ +const gchar * +purple_http_url_get_path(const PurpleHttpURL *parsed_url); + +/** + * purple_http_url_get_fragment: + * @parsed_url: The URL struct. + * + * Gets the fragment part of URL. + * + * Returns: The fragment. + */ +const gchar * +purple_http_url_get_fragment(const PurpleHttpURL *parsed_url); + + +/**************************************************************************/ +/* Cookie jar API */ +/**************************************************************************/ + +/** + * purple_http_cookie_jar_new: + * + * Creates new cookie jar, + * + * Returns: empty cookie jar. + */ +PurpleHttpCookieJar * purple_http_cookie_jar_new(void); + +/** + * purple_http_cookie_jar_ref: + * @cookie_jar: The cookie jar. + * + * Increment the reference count. + */ +void purple_http_cookie_jar_ref(PurpleHttpCookieJar *cookie_jar); + +/** + * purple_http_cookie_jar_unref: + * @cookie_jar: The cookie jar. + * + * Decrement the reference count. + * + * If the reference count reaches zero, the cookie jar will be freed. + * + * Returns: @cookie_jar or %NULL if the reference count reached zero. + */ +PurpleHttpCookieJar * purple_http_cookie_jar_unref( + PurpleHttpCookieJar *cookie_jar); + +/** + * purple_http_cookie_jar_set: + * @cookie_jar: The cookie jar. + * @name: Cookie name. + * @value: Cookie contents. + * + * Sets the cookie. + */ +void purple_http_cookie_jar_set(PurpleHttpCookieJar *cookie_jar, + const gchar *name, const gchar *value); + +/** + * purple_http_cookie_jar_get: + * @cookie_jar: The cookie jar. + * @name: Cookie name. + * + * Gets the cookie. + * + * The result must be g_free'd. + * + * Returns: Cookie contents, or NULL, if cookie doesn't exists. + */ +gchar * purple_http_cookie_jar_get(PurpleHttpCookieJar *cookie_jar, + const gchar *name); + +/** + * purple_http_cookie_jar_is_empty: + * @cookie_jar: The cookie jar. + * + * Checks, if the cookie jar contains any cookies. + * + * Returns: TRUE, if cookie jar contains any cookie, FALSE otherwise. + */ +gboolean purple_http_cookie_jar_is_empty(PurpleHttpCookieJar *cookie_jar); + + +/**************************************************************************/ +/* HTTP Request API */ +/**************************************************************************/ + +/** + * purple_http_request_new: + * @url: The URL to request for, or NULL to leave empty (to be set with + * purple_http_request_set_url). + * + * Creates the new instance of HTTP request configuration. + * + * Returns: The new instance of HTTP request struct. + */ +PurpleHttpRequest * purple_http_request_new(const gchar *url); + +/** + * purple_http_request_ref: + * @request: The request. + * + * Increment the reference count. + */ +void purple_http_request_ref(PurpleHttpRequest *request); + +/** + * purple_http_request_unref: + * @request: The request. + * + * Decrement the reference count. + * + * If the reference count reaches zero, the http request struct will be freed. + * + * Returns: @request or %NULL if the reference count reached zero. + */ +PurpleHttpRequest * purple_http_request_unref(PurpleHttpRequest *request); + +/** + * purple_http_request_set_url: + * @request: The request. + * @url: The url. + * + * Sets URL for HTTP request. + */ +void purple_http_request_set_url(PurpleHttpRequest *request, const gchar *url); + +/** + * purple_http_request_set_url_printf: + * @request: The request. + * @format: The format string. + * @...: The parameters to insert into the format string. + * + * Constructs and sets an URL for HTTP request. + */ +void purple_http_request_set_url_printf(PurpleHttpRequest *request, + const gchar *format, ...) G_GNUC_PRINTF(2, 3); + +/** + * purple_http_request_get_url: + * @request: The request. + * + * Gets URL set for the HTTP request. + * + * Returns: URL set for this request. + */ +const gchar * purple_http_request_get_url(PurpleHttpRequest *request); + +/** + * purple_http_request_set_method: + * @request: The request. + * @method: The method, or NULL for default. + * + * Sets custom HTTP method used for the request. + */ +void purple_http_request_set_method(PurpleHttpRequest *request, + const gchar *method); + +/** + * purple_http_request_get_method: + * @request: The request. + * + * Gets HTTP method set for the request. + * + * Returns: The method. + */ +const gchar * purple_http_request_get_method(PurpleHttpRequest *request); + +/** + * purple_http_request_set_keepalive_pool: + * @request: The request. + * @pool: The new KeepAlive pool, or NULL to reset. + * + * Sets HTTP KeepAlive connections pool for the request. + * + * It increases pool's reference count. + */ +void +purple_http_request_set_keepalive_pool(PurpleHttpRequest *request, + PurpleHttpKeepalivePool *pool); + +/** + * purple_http_request_get_keepalive_pool: + * @request: The request. + * + * Gets HTTP KeepAlive connections pool associated with the request. + * + * It doesn't affect pool's reference count. + * + * Returns: The KeepAlive pool, used for the request. + */ +PurpleHttpKeepalivePool * +purple_http_request_get_keepalive_pool(PurpleHttpRequest *request); + +/** + * purple_http_request_set_contents: + * @request: The request. + * @contents: The contents. + * @length: The length of contents (-1 if it's a NULL-terminated string) + * + * Sets contents of HTTP request (for example, POST data). + */ +void purple_http_request_set_contents(PurpleHttpRequest *request, + const gchar *contents, int length); + +/** + * purple_http_request_set_contents_reader: + * @request: The request. + * @reader: (scope call): The reader callback. + * @contents_length: The size of all contents. + * @user_data: The user data to pass to the callback function. + * + * Sets contents reader for HTTP request, used mainly for possible large + * uploads. + */ +void purple_http_request_set_contents_reader(PurpleHttpRequest *request, + PurpleHttpContentReader reader, int contents_length, gpointer user_data); + +/** + * purple_http_request_set_response_writer: + * @request: The request. + * @writer: (scope call): The writer callback, or %NULL to remove existing. + * @user_data: The user data to pass to the callback function. + * + * Set contents writer for HTTP response. + */ +void purple_http_request_set_response_writer(PurpleHttpRequest *request, + PurpleHttpContentWriter writer, gpointer user_data); + +/** + * purple_http_request_set_timeout: + * @request: The request. + * @timeout: Time (in seconds) after that timeout will be cancelled, + * -1 for infinite time. + * + * Set maximum amount of time, that request is allowed to run. + */ +void purple_http_request_set_timeout(PurpleHttpRequest *request, int timeout); + +/** + * purple_http_request_get_timeout: + * @request: The request. + * + * Get maximum amount of time, that request is allowed to run. + * + * Returns: Timeout currently set (-1 for infinite). + */ +int purple_http_request_get_timeout(PurpleHttpRequest *request); + +/** + * purple_http_request_set_max_redirects: + * @request: The request. + * @max_redirects: Maximum amount of redirects, or -1 for unlimited. + * + * Sets maximum amount of redirects. + */ +void purple_http_request_set_max_redirects(PurpleHttpRequest *request, + int max_redirects); + +/** + * purple_http_request_get_max_redirects: + * @request: The request. + * + * Gets maximum amount of redirects. + * + * Returns: Current maximum amount of redirects (-1 for unlimited). + */ +int purple_http_request_get_max_redirects(PurpleHttpRequest *request); + +/** + * purple_http_request_set_cookie_jar: + * @request: The request. + * @cookie_jar: The cookie jar. + * + * Sets cookie jar used for the request. + */ +void purple_http_request_set_cookie_jar(PurpleHttpRequest *request, + PurpleHttpCookieJar *cookie_jar); + +/** + * purple_http_request_get_cookie_jar: + * @request: The request. + * + * Gets cookie jar used for the request. + * + * Returns: The cookie jar. + */ +PurpleHttpCookieJar * purple_http_request_get_cookie_jar( + PurpleHttpRequest *request); + +/** + * purple_http_request_set_http11: + * @request: The request. + * @http11: TRUE for HTTP/1.1, FALSE for HTTP/1.0. + * + * Sets HTTP version to use. + */ +void purple_http_request_set_http11(PurpleHttpRequest *request, + gboolean http11); + +/** + * purple_http_request_is_http11: + * @request: The request. + * + * Gets used HTTP version. + * + * Returns: TRUE, if we use HTTP/1.1, FALSE for HTTP/1.0. + */ +gboolean purple_http_request_is_http11(PurpleHttpRequest *request); + +/** + * purple_http_request_set_max_len: + * @request: The request. + * @max_len: Maximum length of response to read (-1 for the maximum + * supported amount). + * + * Sets maximum length of response content to read. + * + * Headers length doesn't count here. + * + */ +void purple_http_request_set_max_len(PurpleHttpRequest *request, int max_len); + +/** + * purple_http_request_get_max_len: + * @request: The request. + * + * Gets maximum length of response content to read. + * + * Returns: Maximum length of response to read, or -1 if unlimited. + */ +int purple_http_request_get_max_len(PurpleHttpRequest *request); + +/** + * purple_http_request_header_set: + * @request: The request. + * @key: A header to be set. + * @value: A value to set, or NULL to remove specified header. + * + * Sets (replaces, if exists) specified HTTP request header with provided value. + * + * See purple_http_request_header_add(). + */ +void purple_http_request_header_set(PurpleHttpRequest *request, + const gchar *key, const gchar *value); + +/** + * purple_http_request_header_set_printf: + * @request: The request. + * @key: A header to be set. + * @format: The format string. + * + * Constructs and sets (replaces, if exists) specified HTTP request header. + */ +void purple_http_request_header_set_printf(PurpleHttpRequest *request, + const gchar *key, const gchar *format, ...) G_GNUC_PRINTF(3, 4); + +/** + * purple_http_request_header_add: + * @request: The request. + * @key: A header to be set. + * @value: A value to set. + * + * Adds (without replacing, if exists) an HTTP request header. + * + * See purple_http_request_header_set(). + */ +void purple_http_request_header_add(PurpleHttpRequest *request, + const gchar *key, const gchar *value); + + +/**************************************************************************/ +/* HTTP Keep-Alive pool API */ +/**************************************************************************/ + +/** + * purple_http_keepalive_pool_new: + * + * Creates a new HTTP Keep-Alive pool. + */ +PurpleHttpKeepalivePool * +purple_http_keepalive_pool_new(void); + +/** + * purple_http_keepalive_pool_ref: + * @pool: The HTTP Keep-Alive pool. + * + * Increment the reference count. + */ +void +purple_http_keepalive_pool_ref(PurpleHttpKeepalivePool *pool); + +/** + * purple_http_keepalive_pool_unref: + * @pool: The HTTP Keep-Alive pool. + * + * Decrement the reference count. + * + * If the reference count reaches zero, the pool will be freed and all + * connections will be closed. + * + * Returns: @pool or %NULL if the reference count reached zero. + */ +PurpleHttpKeepalivePool * +purple_http_keepalive_pool_unref(PurpleHttpKeepalivePool *pool); + +/** + * purple_http_keepalive_pool_set_limit_per_host: + * @pool: The HTTP Keep-Alive pool. + * @limit: The new limit, 0 for unlimited. + * + * Sets maximum allowed number of connections to specific host-triple (is_ssl + + * hostname + port). + */ +void +purple_http_keepalive_pool_set_limit_per_host(PurpleHttpKeepalivePool *pool, + guint limit); + +/** + * purple_http_keepalive_pool_get_limit_per_host: + * @pool: The HTTP Keep-Alive pool. + * + * Gets maximum allowed number of connections to specific host-triple (is_ssl + + * hostname + port). + * + * Returns: The limit. + */ +guint +purple_http_keepalive_pool_get_limit_per_host(PurpleHttpKeepalivePool *pool); + + +/**************************************************************************/ +/* HTTP connection set API */ +/**************************************************************************/ + +PurpleHttpConnectionSet * +purple_http_connection_set_new(void); + +void +purple_http_connection_set_destroy(PurpleHttpConnectionSet *set); + +void +purple_http_connection_set_add(PurpleHttpConnectionSet *set, + PurpleHttpConnection *http_conn); + + +/**************************************************************************/ +/* HTTP response API */ +/**************************************************************************/ + +/** + * purple_http_response_is_successful: + * @response: The response. + * + * Checks, if HTTP request was performed successfully. + * + * Returns: TRUE, if request was performed successfully. + */ +gboolean purple_http_response_is_successful(PurpleHttpResponse *response); + +/** + * purple_http_response_get_code: + * @response: The response. + * + * Gets HTTP response code. + * + * Returns: HTTP response code. + */ +int purple_http_response_get_code(PurpleHttpResponse *response); + +/** + * purple_http_response_get_error: + * @response: The response. + * + * Gets error description. + * + * Returns: Localized error description or NULL, if there was no error. + */ +const gchar * purple_http_response_get_error(PurpleHttpResponse *response); + +/** + * purple_http_response_get_data_len: + * @response: The response. + * + * Gets HTTP response data length. + * + * Returns: Data length; + */ +gsize purple_http_response_get_data_len(PurpleHttpResponse *response); + +/** + * purple_http_response_get_data: + * @response: The response. + * @len: Return address for the size of the data. Can be NULL. + * + * Gets HTTP response data. + * + * Response data is not written, if writer callback was set for request. + * + * Returns: The data. + */ +const gchar * purple_http_response_get_data(PurpleHttpResponse *response, size_t *len); + +/** + * purple_http_response_get_all_headers: + * @response: The response. + * + * Gets all headers got with response. + * + * Returns: GList of PurpleKeyValuePair, which keys are header field + * names (gchar*) and values are its contents (gchar*). + */ +const GList * purple_http_response_get_all_headers(PurpleHttpResponse *response); + +/** + * purple_http_response_get_headers_by_name: + * @response: The response. + * @name: The name of header field. + * + * Gets all headers with specified name got with response. + * + * Returns: GList of header field records contents (gchar*). + */ +const GList * purple_http_response_get_headers_by_name( + PurpleHttpResponse *response, const gchar *name); + +/** + * purple_http_response_get_header: + * @response: The response. + * @name: The name of header field. + * + * Gets one header contents with specified name got with response. + * + * To get all headers with the same name, use + * purple_http_response_get_headers_by_name instead. + * + * Returns: Header field contents or NULL, if there is no such one. + */ +const gchar * purple_http_response_get_header(PurpleHttpResponse *response, + const gchar *name); + + +/**************************************************************************/ +/* HTTP Subsystem */ +/**************************************************************************/ + +/** + * purple_http_init: + * + * Initializes the http subsystem. + */ +void purple_http_init(void); + +/** + * purple_http_uninit: + * + * Uninitializes the http subsystem. + */ +void purple_http_uninit(void); + +G_END_DECLS + +#endif /* _PURPLE_HTTP_H_ */ diff --git a/purple2compat/purple-socket.c b/purple2compat/purple-socket.c new file mode 100644 index 00000000..72b368b9 --- /dev/null +++ b/purple2compat/purple-socket.c @@ -0,0 +1,410 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#include "purple-socket.h" + +#include "internal.h" + +#include "debug.h" +#include "proxy.h" +#include "sslconn.h" + +typedef enum { + PURPLE_SOCKET_STATE_DISCONNECTED = 0, + PURPLE_SOCKET_STATE_CONNECTING, + PURPLE_SOCKET_STATE_CONNECTED, + PURPLE_SOCKET_STATE_ERROR +} PurpleSocketState; + +struct _PurpleSocket +{ + PurpleConnection *gc; + gchar *host; + int port; + gboolean is_tls; + GHashTable *data; + + PurpleSocketState state; + + PurpleSslConnection *tls_connection; + PurpleProxyConnectData *raw_connection; + int fd; + guint inpa; + + PurpleSocketConnectCb cb; + gpointer cb_data; +}; + +static GHashTable *handles = NULL; + +static void +handle_add(PurpleSocket *ps) +{ + PurpleConnection *gc = ps->gc; + GSList *l; + + l = g_hash_table_lookup(handles, gc); + l = g_slist_prepend(l, ps); + g_hash_table_insert(handles, gc, l); +} + +static void +handle_remove(PurpleSocket *ps) +{ + PurpleConnection *gc = ps->gc; + GSList *l; + + l = g_hash_table_lookup(handles, gc); + l = g_slist_remove(l, ps); + g_hash_table_insert(handles, gc, l); +} + +void +_purple_socket_init(void) +{ + handles = g_hash_table_new(g_direct_hash, g_direct_equal); +} + +void +_purple_socket_uninit(void) +{ + g_hash_table_destroy(handles); + handles = NULL; +} + +PurpleSocket * +purple_socket_new(PurpleConnection *gc) +{ + PurpleSocket *ps = g_new0(PurpleSocket, 1); + + ps->gc = gc; + ps->fd = -1; + ps->port = -1; + ps->data = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); + + handle_add(ps); + + return ps; +} + +PurpleConnection * +purple_socket_get_connection(PurpleSocket *ps) +{ + g_return_val_if_fail(ps != NULL, NULL); + + return ps->gc; +} + +static gboolean +purple_socket_check_state(PurpleSocket *ps, PurpleSocketState wanted_state) +{ + g_return_val_if_fail(ps != NULL, FALSE); + + if (ps->state == wanted_state) + return TRUE; + + purple_debug_error("socket", "invalid state: %d (should be: %d)", + ps->state, wanted_state); + ps->state = PURPLE_SOCKET_STATE_ERROR; + return FALSE; +} + +void +purple_socket_set_tls(PurpleSocket *ps, gboolean is_tls) +{ + g_return_if_fail(ps != NULL); + + if (!purple_socket_check_state(ps, PURPLE_SOCKET_STATE_DISCONNECTED)) + return; + + ps->is_tls = is_tls; +} + +void +purple_socket_set_host(PurpleSocket *ps, const gchar *host) +{ + g_return_if_fail(ps != NULL); + + if (!purple_socket_check_state(ps, PURPLE_SOCKET_STATE_DISCONNECTED)) + return; + + g_free(ps->host); + ps->host = g_strdup(host); +} + +void +purple_socket_set_port(PurpleSocket *ps, int port) +{ + g_return_if_fail(ps != NULL); + g_return_if_fail(port >= 0); + g_return_if_fail(port <= 65535); + + if (!purple_socket_check_state(ps, PURPLE_SOCKET_STATE_DISCONNECTED)) + return; + + ps->port = port; +} + +static void +_purple_socket_connected_raw(gpointer _ps, gint fd, const gchar *error_message) +{ + PurpleSocket *ps = _ps; + + ps->raw_connection = NULL; + + if (!purple_socket_check_state(ps, PURPLE_SOCKET_STATE_CONNECTING)) { + if (fd > 0) + close(fd); + ps->cb(ps, _("Invalid socket state"), ps->cb_data); + return; + } + + if (fd <= 0 || error_message != NULL) { + if (error_message == NULL) + error_message = _("Unknown error"); + ps->fd = -1; + ps->state = PURPLE_SOCKET_STATE_ERROR; + ps->cb(ps, error_message, ps->cb_data); + return; + } + + ps->state = PURPLE_SOCKET_STATE_CONNECTED; + ps->fd = fd; + ps->cb(ps, NULL, ps->cb_data); +} + +static void +_purple_socket_connected_tls(gpointer _ps, PurpleSslConnection *tls_connection, + PurpleInputCondition cond) +{ + PurpleSocket *ps = _ps; + + if (!purple_socket_check_state(ps, PURPLE_SOCKET_STATE_CONNECTING)) { + purple_ssl_close(tls_connection); + ps->tls_connection = NULL; + ps->cb(ps, _("Invalid socket state"), ps->cb_data); + return; + } + + if (ps->tls_connection->fd <= 0) { + ps->state = PURPLE_SOCKET_STATE_ERROR; + purple_ssl_close(tls_connection); + ps->tls_connection = NULL; + ps->cb(ps, _("Invalid file descriptor"), ps->cb_data); + return; + } + + ps->state = PURPLE_SOCKET_STATE_CONNECTED; + ps->fd = ps->tls_connection->fd; + ps->cb(ps, NULL, ps->cb_data); +} + +static void +_purple_socket_connected_tls_error(PurpleSslConnection *ssl_connection, + PurpleSslErrorType error, gpointer _ps) +{ + PurpleSocket *ps = _ps; + + ps->state = PURPLE_SOCKET_STATE_ERROR; + ps->tls_connection = NULL; + ps->cb(ps, purple_ssl_strerror(error), ps->cb_data); +} + +gboolean +purple_socket_connect(PurpleSocket *ps, PurpleSocketConnectCb cb, + gpointer user_data) +{ + PurpleAccount *account = NULL; + + g_return_val_if_fail(ps != NULL, FALSE); + + if (ps->gc && purple_connection_is_disconnecting(ps->gc)) { + purple_debug_error("socket", "connection is being destroyed"); + ps->state = PURPLE_SOCKET_STATE_ERROR; + return FALSE; + } + + if (!purple_socket_check_state(ps, PURPLE_SOCKET_STATE_DISCONNECTED)) + return FALSE; + ps->state = PURPLE_SOCKET_STATE_CONNECTING; + + if (ps->host == NULL || ps->port < 0) { + purple_debug_error("socket", "Host or port is not specified"); + ps->state = PURPLE_SOCKET_STATE_ERROR; + return FALSE; + } + + if (ps->gc != NULL) + account = purple_connection_get_account(ps->gc); + + ps->cb = cb; + ps->cb_data = user_data; + + if (ps->is_tls) { + ps->tls_connection = purple_ssl_connect(account, ps->host, + ps->port, _purple_socket_connected_tls, + _purple_socket_connected_tls_error, ps); + } else { + ps->raw_connection = purple_proxy_connect(ps->gc, account, + ps->host, ps->port, _purple_socket_connected_raw, ps); + } + + if (ps->tls_connection == NULL && + ps->raw_connection == NULL) + { + ps->state = PURPLE_SOCKET_STATE_ERROR; + return FALSE; + } + + return TRUE; +} + +gssize +purple_socket_read(PurpleSocket *ps, guchar *buf, size_t len) +{ + g_return_val_if_fail(ps != NULL, -1); + g_return_val_if_fail(buf != NULL, -1); + + if (!purple_socket_check_state(ps, PURPLE_SOCKET_STATE_CONNECTED)) + return -1; + + if (ps->is_tls) + return purple_ssl_read(ps->tls_connection, buf, len); + else + return read(ps->fd, buf, len); +} + +gssize +purple_socket_write(PurpleSocket *ps, const guchar *buf, size_t len) +{ + g_return_val_if_fail(ps != NULL, -1); + g_return_val_if_fail(buf != NULL, -1); + + if (!purple_socket_check_state(ps, PURPLE_SOCKET_STATE_CONNECTED)) + return -1; + + if (ps->is_tls) + return purple_ssl_write(ps->tls_connection, buf, len); + else + return write(ps->fd, buf, len); +} + +void +purple_socket_watch(PurpleSocket *ps, PurpleInputCondition cond, + PurpleInputFunction func, gpointer user_data) +{ + g_return_if_fail(ps != NULL); + + if (!purple_socket_check_state(ps, PURPLE_SOCKET_STATE_CONNECTED)) + return; + + if (ps->inpa > 0) + purple_input_remove(ps->inpa); + ps->inpa = 0; + + g_return_if_fail(ps->fd > 0); + + if (func != NULL) + ps->inpa = purple_input_add(ps->fd, cond, func, user_data); +} + +int +purple_socket_get_fd(PurpleSocket *ps) +{ + g_return_val_if_fail(ps != NULL, -1); + + if (!purple_socket_check_state(ps, PURPLE_SOCKET_STATE_CONNECTED)) + return -1; + + g_return_val_if_fail(ps->fd > 0, -1); + + return ps->fd; +} + +void +purple_socket_set_data(PurpleSocket *ps, const gchar *key, gpointer data) +{ + g_return_if_fail(ps != NULL); + g_return_if_fail(key != NULL); + + if (data == NULL) + g_hash_table_remove(ps->data, key); + else + g_hash_table_insert(ps->data, g_strdup(key), data); +} + +gpointer +purple_socket_get_data(PurpleSocket *ps, const gchar *key) +{ + g_return_val_if_fail(ps != NULL, NULL); + g_return_val_if_fail(key != NULL, NULL); + + return g_hash_table_lookup(ps->data, key); +} + +static void +purple_socket_cancel(PurpleSocket *ps) +{ + if (ps->inpa > 0) + purple_input_remove(ps->inpa); + ps->inpa = 0; + + if (ps->tls_connection != NULL) { + purple_ssl_close(ps->tls_connection); + ps->fd = -1; + } + ps->tls_connection = NULL; + + if (ps->raw_connection != NULL) + purple_proxy_connect_cancel(ps->raw_connection); + ps->raw_connection = NULL; + + if (ps->fd > 0) + close(ps->fd); + ps->fd = 0; +} + +void +purple_socket_destroy(PurpleSocket *ps) +{ + if (ps == NULL) + return; + + handle_remove(ps); + + purple_socket_cancel(ps); + + g_free(ps->host); + g_hash_table_destroy(ps->data); + g_free(ps); +} + +void +_purple_socket_cancel_with_connection(PurpleConnection *gc) +{ + GSList *it; + + it = g_hash_table_lookup(handles, gc); + for (; it; it = g_slist_next(it)) { + PurpleSocket *ps = it->data; + purple_socket_cancel(ps); + } +} diff --git a/purple2compat/purple-socket.h b/purple2compat/purple-socket.h new file mode 100644 index 00000000..b43e512e --- /dev/null +++ b/purple2compat/purple-socket.h @@ -0,0 +1,217 @@ +/* purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ + +#ifndef _PURPLE_SOCKET_H_ +#define _PURPLE_SOCKET_H_ +/** + * SECTION:purple-socket + * @section_id: libpurple-purple-socket + * @short_description: purple-socket.h + * @title: Generic Sockets + */ + +#include "connection.h" + +/** + * PurpleSocket: + * + * A structure holding all resources needed for the TCP connection. + */ +typedef struct _PurpleSocket PurpleSocket; + +/** + * PurpleSocketConnectCb: + * @ps: The socket. + * @error: Error message, or NULL if connection was successful. + * @user_data: The user data passed with callback function. + * + * A callback fired after (successfully or not) establishing a connection. + */ +typedef void (*PurpleSocketConnectCb)(PurpleSocket *ps, const gchar *error, + gpointer user_data); + +/** + * purple_socket_new: + * @gc: The connection for which the socket is needed, or NULL. + * + * Creates new, disconnected socket. + * + * Passing a PurpleConnection allows for proper proxy handling. + * + * Returns: The new socket struct. + */ +PurpleSocket * +purple_socket_new(PurpleConnection *gc); + +/** + * purple_socket_get_connection: + * @ps: The socket. + * + * Gets PurpleConnection tied with specified socket. + * + * Returns: The PurpleConnection object. + */ +PurpleConnection * +purple_socket_get_connection(PurpleSocket *ps); + +/** + * purple_socket_set_tls: + * @ps: The socket. + * @is_tls: TRUE, if TLS should be handled transparently, FALSE otherwise. + * + * Determines, if socket should handle TLS. + */ +void +purple_socket_set_tls(PurpleSocket *ps, gboolean is_tls); + +/** + * purple_socket_set_host: + * @ps: The socket. + * @host: The connection host. + * + * Sets connection host. + */ +void +purple_socket_set_host(PurpleSocket *ps, const gchar *host); + +/** + * purple_socket_set_port: + * @ps: The socket. + * @port: The connection port. + * + * Sets connection port. + */ +void +purple_socket_set_port(PurpleSocket *ps, int port); + +/** + * purple_socket_connect: + * @ps: The socket. + * @cb: The function to call after establishing a connection, or on + * error. + * @user_data: The user data to be passed to callback function. + * + * Establishes a connection. + * + * Returns: TRUE on success (this doesn't mean it's connected yet), FALSE + * otherwise. + */ +gboolean +purple_socket_connect(PurpleSocket *ps, PurpleSocketConnectCb cb, + gpointer user_data); + +/** + * purple_socket_read: + * @ps: The socket. + * @buf: The buffer to write data to. + * @len: The buffer size. + * + * Reads incoming data from socket. + * + * This function deals with TLS, if the socket is configured to do it. + * + * Returns: Amount of data written, or -1 on error (errno will be also be set). + */ +gssize +purple_socket_read(PurpleSocket *ps, guchar *buf, size_t len); + +/** + * purple_socket_write: + * @ps: The socket. + * @buf: The buffer to read data from. + * @len: The amount of data to read and send. + * + * Sends data through socket. + * + * This function deals with TLS, if the socket is configured to do it. + * + * Returns: Amount of data sent, or -1 on error (errno will albo be set). + */ +gssize +purple_socket_write(PurpleSocket *ps, const guchar *buf, size_t len); + +/** + * purple_socket_watch: + * @ps: The socket. + * @cond: The condition type. + * @func: The callback function for data, or NULL to remove any + * existing callbacks. + * @user_data: The user data to be passed to callback function. + * + * Adds an input handler for the socket. + * + * If the specified socket had input handler already registered, it will be + * removed. To remove any input handlers, pass an NULL handler function. + */ +void +purple_socket_watch(PurpleSocket *ps, PurpleInputCondition cond, + PurpleInputFunction func, gpointer user_data); + +/** + * purple_socket_get_fd: + * @ps: The socket + * + * Gets underlying file descriptor for socket. + * + * It's not meant to read/write data (use purple_socket_read/ + * purple_socket_write), rather for watching for changes with select(). + * + * Returns: The file descriptor, or -1 on error. + */ +int +purple_socket_get_fd(PurpleSocket *ps); + +/** + * purple_socket_set_data: + * @ps: The socket. + * @key: The unique key. + * @data: The data to assign, or NULL to remove. + * + * Sets extra data for a socket. + */ +void +purple_socket_set_data(PurpleSocket *ps, const gchar *key, gpointer data); + +/** + * purple_socket_get_data: + * @ps: The socket. + * @key: The unqiue key. + * + * Returns extra data in a socket. + * + * Returns: The data associated with the key. + */ +gpointer +purple_socket_get_data(PurpleSocket *ps, const gchar *key); + +/** + * purple_socket_destroy: + * @ps: The socket. + * + * Destroys the socket, closes connection and frees all resources. + * + * If file descriptor for the socket was extracted with purple_socket_get_fd and + * added to event loop, it have to be removed prior this. + */ +void +purple_socket_destroy(PurpleSocket *ps); + +#endif /* _PURPLE_SOCKET_H_ */ diff --git a/update.sh b/update.sh deleted file mode 100755 index f2f2a381..00000000 --- a/update.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/sh - -set -e - -URL="https://keep.imfreedom.org/pidgin/pidgin" -HASHG=$(_TMP_=$(type hg 2>&1); echo $?) - -if test "$HASHG" != "0"; then - echo "hg (mercurial) not found in PATH" >&2 - exit $HASHG -fi - -test -z "$srcdir" && srcdir=$(dirname "$0") -test -z "$srcdir" && srcdir=. -test -z "$pidgindir" && pidgindir=.pidgin - -cd "$srcdir" -REVISION=$(cat VERSION) - -if ! test -d "$pidgindir/.hg"; then - rm -rf "$pidgindir" - hg clone "$URL" "$pidgindir" -fi - -hg -R "$pidgindir" -v pull -hg -R "$pidgindir" -v update -C "$REVISION" -rm -rf pidgin - -for FILE in $(cat MANIFEST_PIDGIN); do - mkdir -p $(dirname "pidgin/$FILE") - cp "$pidgindir/$FILE" "pidgin/$FILE" -done - -touch $(cat MANIFEST_VOIDS) - -patchdir="$(pwd)/patches" -cd pidgin - -for patch in $(ls -1 "$patchdir"); do - patch -p1 -i "$patchdir/$patch" -done