diff --git a/README.md b/README.md index e47f17e..a4e1a83 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ## 部分功能 -- SDK源码100%开源,[下载](https://pub.dev/packages/bytedesk_kefu/versions) +- SDK源码100%开源 - 支持安卓、iOS、Web - 机器人对话 - 技能组客服 diff --git a/bytedesk_kefu/.gitignore b/bytedesk_kefu/.gitignore new file mode 100644 index 0000000..e9dc58d --- /dev/null +++ b/bytedesk_kefu/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +.dart_tool/ + +.packages +.pub/ + +build/ diff --git a/bytedesk_kefu/.metadata b/bytedesk_kefu/.metadata new file mode 100644 index 0000000..0d35783 --- /dev/null +++ b/bytedesk_kefu/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 1d9032c7e1d867f071f2277eb1673e8f9b0274e3 + channel: stable + +project_type: plugin diff --git a/bytedesk_kefu/CHANGELOG.md b/bytedesk_kefu/CHANGELOG.md new file mode 100644 index 0000000..35fba8e --- /dev/null +++ b/bytedesk_kefu/CHANGELOG.md @@ -0,0 +1,211 @@ +# Upgrade Log + +## 1.2.4 + +* optimize user experience + +## 1.2.3 + +* optimize user experience + +## 1.2.2 + +* add unreadcount api + +## 1.2.1 + +* optimize user experience + +## 1.2.0 + +* add setDescription interface + +## 1.1.9 + +* optimize user experience + +## 1.1.8 + +* optimize user experience + +## 1.1.7 + +* optimize user experience + +## 1.1.6 + +* optimize user experience + +## 1.1.5 + +* optimize user experience + +## 1.1.4 + +* optimize user experience + +## 1.1.3 + +* optimize user experience + +## 1.1.2 + +* optimize user experience + +## 1.1.1 + +* optimize user experience + +## 1.1.0 + +* optimize user experience + +## 1.0.6 + +* optimize user experience + +## 1.0.5 + +* optimize user experience + +## 1.0.4 + +* optimize user experience + +## 1.0.3 + +* optimize user experience + +## 1.0.2 + +* bug fix + +## 1.0.1 + +* begion to update to null-safty +* add web support + +## 1.0.0 + +* update to flutter2.0 + +## 0.6.2 + +* optimize message send and receive + +## 0.6.1 + +* optimize message send and receive + +## 0.5.8 + +* optimize user experience + +## 0.5.6 + +* compress image and video to upload + +## 0.5.5 + +* compress image and video to upload + +## 0.5.4 + +* optimize user experience + +## 0.5.3 + +* optimize user experience + +## 0.5.2 + +* change upload domain + +## 0.5.1 + +* switch to websocket channel && support cdn + +## 0.5.0 + +* switch to websocket channel && support cdn + +## 0.4.0 + +* optimize user experience + +## 0.3.8 + +* optimize user experience + +## 0.3.6 + +* optimize user experience + +## 0.3.5 + +* optimize user experience + +## 0.3.2 + +* optimize user experience + +## 0.3.1 + +* open chat by click notification + +## 0.3.0 + +* send and play video message + +## 0.2.6 + +* optimize robot and notice + +## 0.2.5 + +* optimize robot + +## 0.1.5 + +* optimize user experience + +## 0.1.2 + +* optimize long connection + +## 0.1.1 + +* optimize user experience + +## 0.1.0 + +* add more functions +* optimize long connection + +## 0.0.8 + +* update to flutter 1.22.0 +* support show big image +* support camera + album send image +* support delete message + +## 0.0.5 + +* optimize user experience + +## 0.0.4 + +* optimize user experience + +## 0.0.3 + +* optimize user experience + +## 0.0.2 + +* optimize long connection + +## 0.0.1 + +* support login +* support agent chat diff --git a/bytedesk_kefu/LICENSE b/bytedesk_kefu/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/bytedesk_kefu/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/bytedesk_kefu/README.md b/bytedesk_kefu/README.md new file mode 100644 index 0000000..4990fcf --- /dev/null +++ b/bytedesk_kefu/README.md @@ -0,0 +1,88 @@ +# bytedesk helpdesk system + +- [萝卜丝-智能客服-中文文档](https://git.oschina.net/270580156/bytedesk-flutter) +- [Push](https://pub.dev/packages/bytedesk_push) + +bytedesk flutter helpdesk sdk + +- [Website](https://www.bytedesk.com) +- [Download Gitee Demo](https://git.oschina.net/270580156/bytedesk-flutter) +- [Download Github Demo](https://github.com/Bytedesk/bytedesk-flutter) +- [Download ApkDemo](https://bytedesk.oss-cn-shenzhen.aliyuncs.com/apk/bytedesk-android-sdk-demo.apk) + +## Features + +- support andorid/ios/web +- chat with agent +- shopping chat, send commodity info +- send post script message +- check online status +- get history thread +- message voice && vibrate setting +- chat with robot +- send and play video message +- chat notification + + + +## Getting Started + +### First Step: Register Account + +- [Register](https://www.bytedesk.com/admin) +- [Docs](https://github.com/pengjinning/bytedesk-android) + +### Second Step:Login + +```dart +// appkey和subDomain请替换为真实值 +// 获取appkey,登录后台->渠道管理->Flutter->添加应用->获取appkey +String _appKey = '81f427ea-4467-4c7c-b0cd-5c0e4b51456f'; +// 获取subDomain,也即企业号:登录后台->客服管理->客服账号->企业号 +String _subDomain = "vip"; +BytedeskKefu.init(_appKey, _subDomain); +``` + +### Third Step:Contact + +```dart +BytedeskKefu.startWorkGroupChat(context, workGroupWid, "title"); +``` + +### Completed + +| image1 | image2 | image3 | +| :---------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------: | +| | | | +| | | | +| | | | + +### Change UI + +- create new folder: vendors +- [Download](https://pub.dev/packages/bytedesk_kefu/versions) latest source code, put into vendors folder +- integrate source in pubspect.yaml + +```dart +bytedesk_kefu: + path: ./vendors/bytedesk +``` + +### Support + +- [官网](https://www.bytedesk.com/) +- QQ 3Group: 825257535 +- Follow Us: +- + +### Other + +- [Flutter Push SDK](https://pub.dev/packages/bytedesk_push) +- [Flutter SDK](https://github.com/bytedesk/bytedesk-flutter) +- [UniApp SDK](https://github.com/bytedesk/bytedesk-uniapp) +- [iOS SDK](https://github.com/bytedesk/bytedesk-ios) +- [Android SDK](https://github.com/bytedesk/bytedesk-android) +- [Web](https://github.com/bytedesk/bytedesk-web) +- [微信公众号/小程序接口](https://github.com/bytedesk/bytedesk-wechat) +- [服务器端接口](https://github.com/bytedesk/bytedesk-server) +- [机器人](https://github.com/bytedesk/bytedesk-chatbot) diff --git a/bytedesk_kefu/android/.gitignore b/bytedesk_kefu/android/.gitignore new file mode 100644 index 0000000..c6cbe56 --- /dev/null +++ b/bytedesk_kefu/android/.gitignore @@ -0,0 +1,8 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures diff --git a/bytedesk_kefu/android/.settings/org.eclipse.buildship.core.prefs b/bytedesk_kefu/android/.settings/org.eclipse.buildship.core.prefs new file mode 100644 index 0000000..99fd5b0 --- /dev/null +++ b/bytedesk_kefu/android/.settings/org.eclipse.buildship.core.prefs @@ -0,0 +1,13 @@ +arguments= +auto.sync=false +build.scans.enabled=false +connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER) +connection.project.dir= +eclipse.preferences.version=1 +gradle.user.home= +java.home=/Library/Java/JavaVirtualMachines/jdk1.8.0_202.jdk/Contents/Home +jvm.arguments= +offline.mode=false +override.workspace.settings=true +show.console.view=true +show.executions.view=true diff --git a/bytedesk_kefu/android/build.gradle b/bytedesk_kefu/android/build.gradle new file mode 100644 index 0000000..16de8ed --- /dev/null +++ b/bytedesk_kefu/android/build.gradle @@ -0,0 +1,38 @@ +group 'com.bytedesk.bytedesk_kefu' +version '1.0' + +buildscript { + repositories { + maven { url 'https://maven.aliyun.com/repository/public/' } + maven { url 'https://maven.aliyun.com/repository/google/'} + maven { url 'https://maven.aliyun.com/repository/jcenter/'} + // maven { url "https://maven.google.com" } + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.1.0' + } +} + +rootProject.allprojects { + repositories { + maven { url 'https://maven.aliyun.com/repository/public/' } + maven { url 'https://maven.aliyun.com/repository/google/'} + maven { url 'https://maven.aliyun.com/repository/jcenter/'} + // maven { url "https://maven.google.com" } + google() + jcenter() + } +} + +apply plugin: 'com.android.library' + +android { + compileSdkVersion 30 + + defaultConfig { + minSdkVersion 16 + } +} diff --git a/bytedesk_kefu/android/gradle.properties b/bytedesk_kefu/android/gradle.properties new file mode 100644 index 0000000..cfa9ec1 --- /dev/null +++ b/bytedesk_kefu/android/gradle.properties @@ -0,0 +1,8 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true +# +# systemProp.http.proxyHost=127.0.0.1 +# systemProp.http.proxyPort=10818 +# systemProp.https.proxyHost=127.0.0.1 +# systemProp.https.proxyPort=10818 \ No newline at end of file diff --git a/bytedesk_kefu/android/gradle/wrapper/gradle-wrapper.jar b/bytedesk_kefu/android/gradle/wrapper/gradle-wrapper.jar new file mode 100755 index 0000000..13372ae Binary files /dev/null and b/bytedesk_kefu/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/bytedesk_kefu/android/gradle/wrapper/gradle-wrapper.properties b/bytedesk_kefu/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..3c9d085 --- /dev/null +++ b/bytedesk_kefu/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/bytedesk_kefu/android/gradlew b/bytedesk_kefu/android/gradlew new file mode 100755 index 0000000..9d82f78 --- /dev/null +++ b/bytedesk_kefu/android/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/bytedesk_kefu/android/gradlew.bat b/bytedesk_kefu/android/gradlew.bat new file mode 100755 index 0000000..aec9973 --- /dev/null +++ b/bytedesk_kefu/android/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/bytedesk_kefu/android/settings.gradle b/bytedesk_kefu/android/settings.gradle new file mode 100644 index 0000000..f447531 --- /dev/null +++ b/bytedesk_kefu/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'bytedesk_kefu' diff --git a/bytedesk_kefu/android/src/main/AndroidManifest.xml b/bytedesk_kefu/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..90222fe --- /dev/null +++ b/bytedesk_kefu/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/bytedesk_kefu/android/src/main/java/com/bytedesk/bytedesk_kefu/BytedeskKefuPlugin.java b/bytedesk_kefu/android/src/main/java/com/bytedesk/bytedesk_kefu/BytedeskKefuPlugin.java new file mode 100644 index 0000000..1a0bdca --- /dev/null +++ b/bytedesk_kefu/android/src/main/java/com/bytedesk/bytedesk_kefu/BytedeskKefuPlugin.java @@ -0,0 +1,39 @@ +package com.bytedesk.bytedesk_kefu; + +import androidx.annotation.NonNull; + +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.MethodChannel.MethodCallHandler; +import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.plugin.common.PluginRegistry.Registrar; + +/** BytedeskKefuPlugin */ +public class BytedeskKefuPlugin implements FlutterPlugin, MethodCallHandler { + /// The MethodChannel that will the communication between Flutter and native Android + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private MethodChannel channel; + + @Override + public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) { + channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "bytedesk_kefu"); + channel.setMethodCallHandler(this); + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { + if (call.method.equals("getPlatformVersion")) { + result.success("Android " + android.os.Build.VERSION.RELEASE); + } else { + result.notImplemented(); + } + } + + @Override + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { + channel.setMethodCallHandler(null); + } +} diff --git a/bytedesk_kefu/assets/audio/appkefu_alarm.wav b/bytedesk_kefu/assets/audio/appkefu_alarm.wav new file mode 100755 index 0000000..61894f4 Binary files /dev/null and b/bytedesk_kefu/assets/audio/appkefu_alarm.wav differ diff --git a/bytedesk_kefu/assets/audio/appkefu_dingdong.wav b/bytedesk_kefu/assets/audio/appkefu_dingdong.wav new file mode 100755 index 0000000..4ca952e Binary files /dev/null and b/bytedesk_kefu/assets/audio/appkefu_dingdong.wav differ diff --git a/bytedesk_kefu/assets/audio/appkefu_newmsg.wav b/bytedesk_kefu/assets/audio/appkefu_newmsg.wav new file mode 100755 index 0000000..1657d35 Binary files /dev/null and b/bytedesk_kefu/assets/audio/appkefu_newmsg.wav differ diff --git a/bytedesk_kefu/assets/audio/appkefu_waiting.wav b/bytedesk_kefu/assets/audio/appkefu_waiting.wav new file mode 100755 index 0000000..3e24674 Binary files /dev/null and b/bytedesk_kefu/assets/audio/appkefu_waiting.wav differ diff --git a/bytedesk_kefu/assets/images/feedback/mine_feedback_add_image.png b/bytedesk_kefu/assets/images/feedback/mine_feedback_add_image.png new file mode 100755 index 0000000..3e7c5d2 Binary files /dev/null and b/bytedesk_kefu/assets/images/feedback/mine_feedback_add_image.png differ diff --git a/bytedesk_kefu/assets/images/feedback/mine_feedback_ic_del.png b/bytedesk_kefu/assets/images/feedback/mine_feedback_ic_del.png new file mode 100755 index 0000000..3d5e595 Binary files /dev/null and b/bytedesk_kefu/assets/images/feedback/mine_feedback_ic_del.png differ diff --git a/bytedesk_kefu/chat.jpeg b/bytedesk_kefu/chat.jpeg new file mode 100755 index 0000000..0a70f8a Binary files /dev/null and b/bytedesk_kefu/chat.jpeg differ diff --git a/bytedesk_kefu/chat.png b/bytedesk_kefu/chat.png new file mode 100755 index 0000000..e69189c Binary files /dev/null and b/bytedesk_kefu/chat.png differ diff --git a/bytedesk_kefu/chat_type.jpeg b/bytedesk_kefu/chat_type.jpeg new file mode 100755 index 0000000..748df87 Binary files /dev/null and b/bytedesk_kefu/chat_type.jpeg differ diff --git a/bytedesk_kefu/example/.gitignore b/bytedesk_kefu/example/.gitignore new file mode 100644 index 0000000..0fa6b67 --- /dev/null +++ b/bytedesk_kefu/example/.gitignore @@ -0,0 +1,46 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/bytedesk_kefu/example/.metadata b/bytedesk_kefu/example/.metadata new file mode 100644 index 0000000..c1ee81b --- /dev/null +++ b/bytedesk_kefu/example/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 1d9032c7e1d867f071f2277eb1673e8f9b0274e3 + channel: stable + +project_type: app diff --git a/bytedesk_kefu/example/README.md b/bytedesk_kefu/example/README.md new file mode 100644 index 0000000..cd4b64f --- /dev/null +++ b/bytedesk_kefu/example/README.md @@ -0,0 +1,16 @@ +# bytedesk_kefu_example + +Demonstrates how to use the bytedesk_kefu plugin. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/bytedesk_kefu/example/android/.gitignore b/bytedesk_kefu/example/android/.gitignore new file mode 100644 index 0000000..0a741cb --- /dev/null +++ b/bytedesk_kefu/example/android/.gitignore @@ -0,0 +1,11 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties diff --git a/bytedesk_kefu/example/android/.settings/org.eclipse.buildship.core.prefs b/bytedesk_kefu/example/android/.settings/org.eclipse.buildship.core.prefs new file mode 100644 index 0000000..5fc6763 --- /dev/null +++ b/bytedesk_kefu/example/android/.settings/org.eclipse.buildship.core.prefs @@ -0,0 +1,13 @@ +arguments= +auto.sync=false +build.scans.enabled=false +connection.gradle.distribution=GRADLE_DISTRIBUTION(VERSION(7.1.1)) +connection.project.dir= +eclipse.preferences.version=1 +gradle.user.home= +java.home=/Library/Java/JavaVirtualMachines/jdk1.8.0_202.jdk/Contents/Home +jvm.arguments= +offline.mode=false +override.workspace.settings=true +show.console.view=true +show.executions.view=true diff --git a/bytedesk_kefu/example/android/app/build.gradle b/bytedesk_kefu/example/android/app/build.gradle new file mode 100644 index 0000000..8f5d910 --- /dev/null +++ b/bytedesk_kefu/example/android/app/build.gradle @@ -0,0 +1,50 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 30 + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.bytedesk.bytedesk_kefu_example" + minSdkVersion 19 + targetSdkVersion 30 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} diff --git a/bytedesk_kefu/example/android/app/src/debug/AndroidManifest.xml b/bytedesk_kefu/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..096a44f --- /dev/null +++ b/bytedesk_kefu/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/bytedesk_kefu/example/android/app/src/main/AndroidManifest.xml b/bytedesk_kefu/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..3815747 --- /dev/null +++ b/bytedesk_kefu/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bytedesk_kefu/example/android/app/src/main/java/com/bytedesk/bytedesk_kefu_example/MainActivity.java b/bytedesk_kefu/example/android/app/src/main/java/com/bytedesk/bytedesk_kefu_example/MainActivity.java new file mode 100644 index 0000000..9fe0283 --- /dev/null +++ b/bytedesk_kefu/example/android/app/src/main/java/com/bytedesk/bytedesk_kefu_example/MainActivity.java @@ -0,0 +1,6 @@ +package com.bytedesk.bytedesk_kefu_example; + +import io.flutter.embedding.android.FlutterActivity; + +public class MainActivity extends FlutterActivity { +} diff --git a/bytedesk_kefu/example/android/app/src/main/res/drawable-v21/launch_background.xml b/bytedesk_kefu/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/bytedesk_kefu/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/bytedesk_kefu/example/android/app/src/main/res/drawable/launch_background.xml b/bytedesk_kefu/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100755 index 0000000..304732f --- /dev/null +++ b/bytedesk_kefu/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/bytedesk_kefu/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/bytedesk_kefu/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100755 index 0000000..d2f7c74 Binary files /dev/null and b/bytedesk_kefu/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/bytedesk_kefu/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/bytedesk_kefu/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100755 index 0000000..d2f7c74 Binary files /dev/null and b/bytedesk_kefu/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/bytedesk_kefu/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/bytedesk_kefu/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100755 index 0000000..6fd8bf4 Binary files /dev/null and b/bytedesk_kefu/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/bytedesk_kefu/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/bytedesk_kefu/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100755 index 0000000..6fd8bf4 Binary files /dev/null and b/bytedesk_kefu/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/bytedesk_kefu/example/android/app/src/main/res/mipmap-xhdpi/ic_arrow_back_white_24dp.png b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xhdpi/ic_arrow_back_white_24dp.png new file mode 100755 index 0000000..8214d9d Binary files /dev/null and b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xhdpi/ic_arrow_back_white_24dp.png differ diff --git a/bytedesk_kefu/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100755 index 0000000..ce9321e Binary files /dev/null and b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/bytedesk_kefu/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100755 index 0000000..ce9321e Binary files /dev/null and b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/bytedesk_kefu/example/android/app/src/main/res/mipmap-xhdpi/icon_tabbar_component.png b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xhdpi/icon_tabbar_component.png new file mode 100755 index 0000000..4e69b4c Binary files /dev/null and b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xhdpi/icon_tabbar_component.png differ diff --git a/bytedesk_kefu/example/android/app/src/main/res/mipmap-xhdpi/icon_tabbar_component_selected.png b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xhdpi/icon_tabbar_component_selected.png new file mode 100755 index 0000000..4da085e Binary files /dev/null and b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xhdpi/icon_tabbar_component_selected.png differ diff --git a/bytedesk_kefu/example/android/app/src/main/res/mipmap-xhdpi/icon_tabbar_lab.png b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xhdpi/icon_tabbar_lab.png new file mode 100755 index 0000000..8c211a3 Binary files /dev/null and b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xhdpi/icon_tabbar_lab.png differ diff --git a/bytedesk_kefu/example/android/app/src/main/res/mipmap-xhdpi/icon_tabbar_lab_selected.png b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xhdpi/icon_tabbar_lab_selected.png new file mode 100755 index 0000000..19be002 Binary files /dev/null and b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xhdpi/icon_tabbar_lab_selected.png differ diff --git a/bytedesk_kefu/example/android/app/src/main/res/mipmap-xhdpi/icon_tabbar_util.png b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xhdpi/icon_tabbar_util.png new file mode 100755 index 0000000..2e80b79 Binary files /dev/null and b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xhdpi/icon_tabbar_util.png differ diff --git a/bytedesk_kefu/example/android/app/src/main/res/mipmap-xhdpi/icon_tabbar_util_selected.png b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xhdpi/icon_tabbar_util_selected.png new file mode 100755 index 0000000..567f543 Binary files /dev/null and b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xhdpi/icon_tabbar_util_selected.png differ diff --git a/bytedesk_kefu/example/android/app/src/main/res/mipmap-xhdpi/icon_topbar_about.png b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xhdpi/icon_topbar_about.png new file mode 100755 index 0000000..02773b9 Binary files /dev/null and b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xhdpi/icon_topbar_about.png differ diff --git a/bytedesk_kefu/example/android/app/src/main/res/mipmap-xhdpi/icon_topbar_overflow.png b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xhdpi/icon_topbar_overflow.png new file mode 100755 index 0000000..f92d3a1 Binary files /dev/null and b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xhdpi/icon_topbar_overflow.png differ diff --git a/bytedesk_kefu/example/android/app/src/main/res/mipmap-xxhdpi/ic_arrow_back_white_24dp.png b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xxhdpi/ic_arrow_back_white_24dp.png new file mode 100755 index 0000000..0e43ff9 Binary files /dev/null and b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xxhdpi/ic_arrow_back_white_24dp.png differ diff --git a/bytedesk_kefu/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100755 index 0000000..29e12d5 Binary files /dev/null and b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/bytedesk_kefu/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100755 index 0000000..29e12d5 Binary files /dev/null and b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/bytedesk_kefu/example/android/app/src/main/res/mipmap-xxhdpi/icon_tabbar_component.png b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xxhdpi/icon_tabbar_component.png new file mode 100755 index 0000000..5cc1315 Binary files /dev/null and b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xxhdpi/icon_tabbar_component.png differ diff --git a/bytedesk_kefu/example/android/app/src/main/res/mipmap-xxhdpi/icon_tabbar_component_selected.png b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xxhdpi/icon_tabbar_component_selected.png new file mode 100755 index 0000000..6eb0b0c Binary files /dev/null and b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xxhdpi/icon_tabbar_component_selected.png differ diff --git a/bytedesk_kefu/example/android/app/src/main/res/mipmap-xxhdpi/icon_tabbar_lab.png b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xxhdpi/icon_tabbar_lab.png new file mode 100755 index 0000000..5c8e923 Binary files /dev/null and b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xxhdpi/icon_tabbar_lab.png differ diff --git a/bytedesk_kefu/example/android/app/src/main/res/mipmap-xxhdpi/icon_tabbar_lab_selected.png b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xxhdpi/icon_tabbar_lab_selected.png new file mode 100755 index 0000000..30a59df Binary files /dev/null and b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xxhdpi/icon_tabbar_lab_selected.png differ diff --git a/bytedesk_kefu/example/android/app/src/main/res/mipmap-xxhdpi/icon_tabbar_util.png b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xxhdpi/icon_tabbar_util.png new file mode 100755 index 0000000..fc251e3 Binary files /dev/null and b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xxhdpi/icon_tabbar_util.png differ diff --git a/bytedesk_kefu/example/android/app/src/main/res/mipmap-xxhdpi/icon_tabbar_util_selected.png b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xxhdpi/icon_tabbar_util_selected.png new file mode 100755 index 0000000..712a213 Binary files /dev/null and b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xxhdpi/icon_tabbar_util_selected.png differ diff --git a/bytedesk_kefu/example/android/app/src/main/res/mipmap-xxhdpi/icon_topbar_about.png b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xxhdpi/icon_topbar_about.png new file mode 100755 index 0000000..4313791 Binary files /dev/null and b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xxhdpi/icon_topbar_about.png differ diff --git a/bytedesk_kefu/example/android/app/src/main/res/mipmap-xxhdpi/icon_topbar_overflow.png b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xxhdpi/icon_topbar_overflow.png new file mode 100755 index 0000000..1b6d918 Binary files /dev/null and b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xxhdpi/icon_topbar_overflow.png differ diff --git a/bytedesk_kefu/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100755 index 0000000..663f287 Binary files /dev/null and b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/bytedesk_kefu/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100755 index 0000000..663f287 Binary files /dev/null and b/bytedesk_kefu/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/bytedesk_kefu/example/android/app/src/main/res/values-night/styles.xml b/bytedesk_kefu/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..449a9f9 --- /dev/null +++ b/bytedesk_kefu/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/bytedesk_kefu/example/android/app/src/main/res/values/styles.xml b/bytedesk_kefu/example/android/app/src/main/res/values/styles.xml new file mode 100755 index 0000000..1f83a33 --- /dev/null +++ b/bytedesk_kefu/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/bytedesk_kefu/example/android/app/src/main/res/xml/file_paths.xml b/bytedesk_kefu/example/android/app/src/main/res/xml/file_paths.xml new file mode 100755 index 0000000..fb3af1e --- /dev/null +++ b/bytedesk_kefu/example/android/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/bytedesk_kefu/example/android/app/src/main/res/xml/network_security_config.xml b/bytedesk_kefu/example/android/app/src/main/res/xml/network_security_config.xml new file mode 100755 index 0000000..dca93c0 --- /dev/null +++ b/bytedesk_kefu/example/android/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/bytedesk_kefu/example/android/app/src/profile/AndroidManifest.xml b/bytedesk_kefu/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..096a44f --- /dev/null +++ b/bytedesk_kefu/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/bytedesk_kefu/example/android/build.gradle b/bytedesk_kefu/example/android/build.gradle new file mode 100644 index 0000000..2993024 --- /dev/null +++ b/bytedesk_kefu/example/android/build.gradle @@ -0,0 +1,38 @@ +buildscript { + repositories { + maven { url 'https://maven.aliyun.com/repository/public/' } + maven { url 'https://maven.aliyun.com/repository/google/'} + maven { url 'https://maven.aliyun.com/repository/jcenter/'} + // maven { url "https://maven.google.com" } + // google() + // jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.1.0' + // classpath 'com.android.tools.build:gradle:3.5.4' + } +} + +allprojects { + repositories { + maven { url 'https://maven.aliyun.com/repository/public/' } + maven { url 'https://maven.aliyun.com/repository/google/'} + maven { url 'https://maven.aliyun.com/repository/jcenter/'} + // maven { url "https://maven.google.com" } + // google() + // jcenter() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/bytedesk_kefu/example/android/gradle.properties b/bytedesk_kefu/example/android/gradle.properties new file mode 100644 index 0000000..5911b3e --- /dev/null +++ b/bytedesk_kefu/example/android/gradle.properties @@ -0,0 +1,8 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true +# +# systemProp.http.proxyHost=127.0.0.1 +# systemProp.http.proxyPort=10818 +# systemProp.https.proxyHost=127.0.0.1 +# systemProp.https.proxyPort=10818 diff --git a/bytedesk_kefu/example/android/gradle/wrapper/gradle-wrapper.properties b/bytedesk_kefu/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..014fd12 --- /dev/null +++ b/bytedesk_kefu/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +# distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip diff --git a/bytedesk_kefu/example/android/settings.gradle b/bytedesk_kefu/example/android/settings.gradle new file mode 100644 index 0000000..44e62bc --- /dev/null +++ b/bytedesk_kefu/example/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/bytedesk_kefu/example/android/settings_aar.gradle b/bytedesk_kefu/example/android/settings_aar.gradle new file mode 100644 index 0000000..e7b4def --- /dev/null +++ b/bytedesk_kefu/example/android/settings_aar.gradle @@ -0,0 +1 @@ +include ':app' diff --git a/bytedesk_kefu/example/assets/audio/bytedesk_alarm.wav b/bytedesk_kefu/example/assets/audio/bytedesk_alarm.wav new file mode 100755 index 0000000..61894f4 Binary files /dev/null and b/bytedesk_kefu/example/assets/audio/bytedesk_alarm.wav differ diff --git a/bytedesk_kefu/example/assets/audio/bytedesk_dingdong.wav b/bytedesk_kefu/example/assets/audio/bytedesk_dingdong.wav new file mode 100755 index 0000000..4ca952e Binary files /dev/null and b/bytedesk_kefu/example/assets/audio/bytedesk_dingdong.wav differ diff --git a/bytedesk_kefu/example/assets/audio/bytedesk_newmsg.wav b/bytedesk_kefu/example/assets/audio/bytedesk_newmsg.wav new file mode 100755 index 0000000..1657d35 Binary files /dev/null and b/bytedesk_kefu/example/assets/audio/bytedesk_newmsg.wav differ diff --git a/bytedesk_kefu/example/assets/audio/bytedesk_waiting.wav b/bytedesk_kefu/example/assets/audio/bytedesk_waiting.wav new file mode 100755 index 0000000..3e24674 Binary files /dev/null and b/bytedesk_kefu/example/assets/audio/bytedesk_waiting.wav differ diff --git a/bytedesk_kefu/example/assets/images/feedback/mine_feedback_add_image.png b/bytedesk_kefu/example/assets/images/feedback/mine_feedback_add_image.png new file mode 100755 index 0000000..3e7c5d2 Binary files /dev/null and b/bytedesk_kefu/example/assets/images/feedback/mine_feedback_add_image.png differ diff --git a/bytedesk_kefu/example/assets/images/feedback/mine_feedback_ic_del.png b/bytedesk_kefu/example/assets/images/feedback/mine_feedback_ic_del.png new file mode 100755 index 0000000..3d5e595 Binary files /dev/null and b/bytedesk_kefu/example/assets/images/feedback/mine_feedback_ic_del.png differ diff --git a/bytedesk_kefu/example/ios/.gitignore b/bytedesk_kefu/example/ios/.gitignore new file mode 100644 index 0000000..e96ef60 --- /dev/null +++ b/bytedesk_kefu/example/ios/.gitignore @@ -0,0 +1,32 @@ +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/bytedesk_kefu/example/ios/Flutter/AppFrameworkInfo.plist b/bytedesk_kefu/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..8d4492f --- /dev/null +++ b/bytedesk_kefu/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 9.0 + + diff --git a/bytedesk_kefu/example/ios/Flutter/Debug.xcconfig b/bytedesk_kefu/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/bytedesk_kefu/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/bytedesk_kefu/example/ios/Flutter/Release.xcconfig b/bytedesk_kefu/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/bytedesk_kefu/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/bytedesk_kefu/example/ios/Podfile b/bytedesk_kefu/example/ios/Podfile new file mode 100644 index 0000000..e93971e --- /dev/null +++ b/bytedesk_kefu/example/ios/Podfile @@ -0,0 +1,39 @@ +# Uncomment this line to define a global platform for your project +platform :ios, '10.0' +# added by jackning, 20200929 +use_frameworks! +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/bytedesk_kefu/example/ios/Podfile.lock b/bytedesk_kefu/example/ios/Podfile.lock new file mode 100644 index 0000000..274b3c0 --- /dev/null +++ b/bytedesk_kefu/example/ios/Podfile.lock @@ -0,0 +1,157 @@ +PODS: + - bytedesk_kefu (0.0.1): + - Flutter + - device_info (0.0.1): + - Flutter + - devicelocale (0.0.1): + - Flutter + - DKImagePickerController/Core (4.3.2): + - DKImagePickerController/ImageDataManager + - DKImagePickerController/Resource + - DKImagePickerController/ImageDataManager (4.3.2) + - DKImagePickerController/PhotoGallery (4.3.2): + - DKImagePickerController/Core + - DKPhotoGallery + - DKImagePickerController/Resource (4.3.2) + - DKPhotoGallery (0.0.17): + - DKPhotoGallery/Core (= 0.0.17) + - DKPhotoGallery/Model (= 0.0.17) + - DKPhotoGallery/Preview (= 0.0.17) + - DKPhotoGallery/Resource (= 0.0.17) + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Core (0.0.17): + - DKPhotoGallery/Model + - DKPhotoGallery/Preview + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Model (0.0.17): + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Preview (0.0.17): + - DKPhotoGallery/Model + - DKPhotoGallery/Resource + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Resource (0.0.17): + - SDWebImage + - SwiftyGif + - file_picker (0.0.1): + - DKImagePickerController/PhotoGallery + - Flutter + - Flutter (1.0.0) + - fluttertoast (0.0.2): + - Flutter + - Toast + - FMDB (2.7.5): + - FMDB/standard (= 2.7.5) + - FMDB/standard (2.7.5) + - image_gallery_saver (1.5.0): + - Flutter + - image_picker_ios (0.0.1): + - Flutter + - package_info (0.0.1): + - Flutter + - path_provider_ios (0.0.1): + - Flutter + - SDWebImage (5.12.5): + - SDWebImage/Core (= 5.12.5) + - SDWebImage/Core (5.12.5) + - shared_preferences_ios (0.0.1): + - Flutter + - sqflite (0.0.2): + - Flutter + - FMDB (>= 2.7.5) + - SwiftyGif (5.4.3) + - Toast (4.0.0) + - video_player_avfoundation (0.0.1): + - Flutter + - wakelock (0.0.1): + - Flutter + - webview_flutter_wkwebview (0.0.1): + - Flutter + +DEPENDENCIES: + - bytedesk_kefu (from `.symlinks/plugins/bytedesk_kefu/ios`) + - device_info (from `.symlinks/plugins/device_info/ios`) + - devicelocale (from `.symlinks/plugins/devicelocale/ios`) + - file_picker (from `.symlinks/plugins/file_picker/ios`) + - Flutter (from `Flutter`) + - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) + - image_gallery_saver (from `.symlinks/plugins/image_gallery_saver/ios`) + - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) + - package_info (from `.symlinks/plugins/package_info/ios`) + - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) + - shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`) + - sqflite (from `.symlinks/plugins/sqflite/ios`) + - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`) + - wakelock (from `.symlinks/plugins/wakelock/ios`) + - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`) + +SPEC REPOS: + trunk: + - DKImagePickerController + - DKPhotoGallery + - FMDB + - SDWebImage + - SwiftyGif + - Toast + +EXTERNAL SOURCES: + bytedesk_kefu: + :path: ".symlinks/plugins/bytedesk_kefu/ios" + device_info: + :path: ".symlinks/plugins/device_info/ios" + devicelocale: + :path: ".symlinks/plugins/devicelocale/ios" + file_picker: + :path: ".symlinks/plugins/file_picker/ios" + Flutter: + :path: Flutter + fluttertoast: + :path: ".symlinks/plugins/fluttertoast/ios" + image_gallery_saver: + :path: ".symlinks/plugins/image_gallery_saver/ios" + image_picker_ios: + :path: ".symlinks/plugins/image_picker_ios/ios" + package_info: + :path: ".symlinks/plugins/package_info/ios" + path_provider_ios: + :path: ".symlinks/plugins/path_provider_ios/ios" + shared_preferences_ios: + :path: ".symlinks/plugins/shared_preferences_ios/ios" + sqflite: + :path: ".symlinks/plugins/sqflite/ios" + video_player_avfoundation: + :path: ".symlinks/plugins/video_player_avfoundation/ios" + wakelock: + :path: ".symlinks/plugins/wakelock/ios" + webview_flutter_wkwebview: + :path: ".symlinks/plugins/webview_flutter_wkwebview/ios" + +SPEC CHECKSUMS: + bytedesk_kefu: ca69f7b243932a665dc7001be5a8e04fe7f30c57 + device_info: d7d233b645a32c40dfdc212de5cf646ca482f175 + devicelocale: b22617f40038496deffba44747101255cee005b0 + DKImagePickerController: b5eb7f7a388e4643264105d648d01f727110fc3d + DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 + file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1 + Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a + fluttertoast: 16fbe6039d06a763f3533670197d01fc73459037 + FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a + image_gallery_saver: 259eab68fb271cfd57d599904f7acdc7832e7ef2 + image_picker_ios: b786a5dcf033a8336a657191401bfdf12017dabb + package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62 + path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 + SDWebImage: 0905f1b7760fc8ac4198cae0036600d67478751e + shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad + sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 + SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 + Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 + video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff + wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f + webview_flutter_wkwebview: 005fbd90c888a42c5690919a1527ecc6649e1162 + +PODFILE CHECKSUM: 3992017b8e295cef2daf25d4508056e8c5de3123 + +COCOAPODS: 1.11.3 diff --git a/bytedesk_kefu/example/ios/Runner.xcodeproj/project.pbxproj b/bytedesk_kefu/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..b4052dc --- /dev/null +++ b/bytedesk_kefu/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,550 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 6A722177A8132FF40F6CF720 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ACD77D260F2F57A568414FB9 /* Pods_Runner.framework */; }; + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; + 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 483DAFF376EA336E70A1AF95 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 72FBC4611027C1F583CD56F7 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + ACD77D260F2F57A568414FB9 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + DB686996A5E8B0AF86E5C84F /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 6A722177A8132FF40F6CF720 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 2A6E5A3D8C1F9F80E2BD0282 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ACD77D260F2F57A568414FB9 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 614C55E38B713AD40D4F5ECD /* Pods */ = { + isa = PBXGroup; + children = ( + 483DAFF376EA336E70A1AF95 /* Pods-Runner.debug.xcconfig */, + DB686996A5E8B0AF86E5C84F /* Pods-Runner.release.xcconfig */, + 72FBC4611027C1F583CD56F7 /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 614C55E38B713AD40D4F5ECD /* Pods */, + 2A6E5A3D8C1F9F80E2BD0282 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 97C146F21CF9000F007C117D /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 119C728BDA8833CEAEEB1252 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 5F993E3220C72A9F6B99ABE4 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 119C728BDA8833CEAEEB1252 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 5F993E3220C72A9F6B99ABE4 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, + 97C146F31CF9000F007C117D /* main.m in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = L5F47963M2; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.bytedesk.bytedeskKefuExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = L5F47963M2; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.bytedesk.bytedeskKefuExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = L5F47963M2; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.bytedesk.bytedeskKefuExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/bytedesk_kefu/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/bytedesk_kefu/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/bytedesk_kefu/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/bytedesk_kefu/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/bytedesk_kefu/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/bytedesk_kefu/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/bytedesk_kefu/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/bytedesk_kefu/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/bytedesk_kefu/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/bytedesk_kefu/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/bytedesk_kefu/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..3db53b6 --- /dev/null +++ b/bytedesk_kefu/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bytedesk_kefu/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/bytedesk_kefu/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/bytedesk_kefu/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/bytedesk_kefu/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/bytedesk_kefu/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/bytedesk_kefu/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/bytedesk_kefu/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/bytedesk_kefu/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/bytedesk_kefu/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/bytedesk_kefu/example/ios/Runner/AppDelegate.h b/bytedesk_kefu/example/ios/Runner/AppDelegate.h new file mode 100644 index 0000000..36e21bb --- /dev/null +++ b/bytedesk_kefu/example/ios/Runner/AppDelegate.h @@ -0,0 +1,6 @@ +#import +#import + +@interface AppDelegate : FlutterAppDelegate + +@end diff --git a/bytedesk_kefu/example/ios/Runner/AppDelegate.m b/bytedesk_kefu/example/ios/Runner/AppDelegate.m new file mode 100644 index 0000000..70e8393 --- /dev/null +++ b/bytedesk_kefu/example/ios/Runner/AppDelegate.m @@ -0,0 +1,13 @@ +#import "AppDelegate.h" +#import "GeneratedPluginRegistrant.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + [GeneratedPluginRegistrant registerWithRegistry:self]; + // Override point for customization after application launch. + return [super application:application didFinishLaunchingWithOptions:launchOptions]; +} + +@end diff --git a/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100755 index 0000000..193d0dc --- /dev/null +++ b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,158 @@ +{ + "images": [ + { + "size": "20x20", + "idiom": "iphone", + "filename": "icon-20@2x.png", + "scale": "2x" + }, + { + "size": "20x20", + "idiom": "iphone", + "filename": "icon-20@3x.png", + "scale": "3x" + }, + { + "size": "29x29", + "idiom": "iphone", + "filename": "icon-29.png", + "scale": "1x" + }, + { + "size": "29x29", + "idiom": "iphone", + "filename": "icon-29@2x.png", + "scale": "2x" + }, + { + "size": "29x29", + "idiom": "iphone", + "filename": "icon-29@3x.png", + "scale": "3x" + }, + { + "size": "40x40", + "idiom": "iphone", + "filename": "icon-40@2x.png", + "scale": "2x" + }, + { + "size": "40x40", + "idiom": "iphone", + "filename": "icon-40@3x.png", + "scale": "3x" + }, + { + "size": "57x57", + "idiom": "iphone", + "filename": "icon-57.png", + "scale": "1x" + }, + { + "size": "57x57", + "idiom": "iphone", + "filename": "icon-57@2x.png", + "scale": "2x" + }, + { + "size": "60x60", + "idiom": "iphone", + "filename": "icon-60@2x.png", + "scale": "2x" + }, + { + "size": "60x60", + "idiom": "iphone", + "filename": "icon-60@3x.png", + "scale": "3x" + }, + { + "size": "20x20", + "idiom": "ipad", + "filename": "icon-20-ipad.png", + "scale": "1x" + }, + { + "size": "20x20", + "idiom": "ipad", + "filename": "icon-20@2x-ipad.png", + "scale": "2x" + }, + { + "size": "29x29", + "idiom": "ipad", + "filename": "icon-29-ipad.png", + "scale": "1x" + }, + { + "size": "29x29", + "idiom": "ipad", + "filename": "icon-29@2x-ipad.png", + "scale": "2x" + }, + { + "size": "40x40", + "idiom": "ipad", + "filename": "icon-40.png", + "scale": "1x" + }, + { + "size": "40x40", + "idiom": "ipad", + "filename": "icon-40@2x.png", + "scale": "2x" + }, + { + "size": "50x50", + "idiom": "ipad", + "filename": "icon-50.png", + "scale": "1x" + }, + { + "size": "50x50", + "idiom": "ipad", + "filename": "icon-50@2x.png", + "scale": "2x" + }, + { + "size": "72x72", + "idiom": "ipad", + "filename": "icon-72.png", + "scale": "1x" + }, + { + "size": "72x72", + "idiom": "ipad", + "filename": "icon-72@2x.png", + "scale": "2x" + }, + { + "size": "76x76", + "idiom": "ipad", + "filename": "icon-76.png", + "scale": "1x" + }, + { + "size": "76x76", + "idiom": "ipad", + "filename": "icon-76@2x.png", + "scale": "2x" + }, + { + "size": "83.5x83.5", + "idiom": "ipad", + "filename": "icon-83.5@2x.png", + "scale": "2x" + }, + { + "size": "1024x1024", + "idiom": "ios-marketing", + "filename": "icon-1024.png", + "scale": "1x" + } + ], + "info": { + "version": 1, + "author": "icon.wuruihong.com" + } +} \ No newline at end of file diff --git a/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-1024.png b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-1024.png new file mode 100755 index 0000000..6b9c5d0 Binary files /dev/null and b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-1024.png differ diff --git a/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20-ipad.png b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20-ipad.png new file mode 100755 index 0000000..684fee1 Binary files /dev/null and b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20-ipad.png differ diff --git a/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x-ipad.png b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x-ipad.png new file mode 100755 index 0000000..66908ba Binary files /dev/null and b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x-ipad.png differ diff --git a/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png new file mode 100755 index 0000000..c85d39b Binary files /dev/null and b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png differ diff --git a/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png new file mode 100755 index 0000000..6d072eb Binary files /dev/null and b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png differ diff --git a/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29-ipad.png b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29-ipad.png new file mode 100755 index 0000000..50dc4c7 Binary files /dev/null and b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29-ipad.png differ diff --git a/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29.png b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29.png new file mode 100755 index 0000000..335469c Binary files /dev/null and b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29.png differ diff --git a/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x-ipad.png b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x-ipad.png new file mode 100755 index 0000000..e585977 Binary files /dev/null and b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x-ipad.png differ diff --git a/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png new file mode 100755 index 0000000..5dfbe16 Binary files /dev/null and b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png differ diff --git a/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png new file mode 100755 index 0000000..bd2376d Binary files /dev/null and b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png differ diff --git a/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40.png b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40.png new file mode 100755 index 0000000..66908ba Binary files /dev/null and b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40.png differ diff --git a/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png new file mode 100755 index 0000000..5672877 Binary files /dev/null and b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png differ diff --git a/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png new file mode 100755 index 0000000..e22345e Binary files /dev/null and b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png differ diff --git a/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-50.png b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-50.png new file mode 100755 index 0000000..870c163 Binary files /dev/null and b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-50.png differ diff --git a/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-50@2x.png b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-50@2x.png new file mode 100755 index 0000000..8c6ff14 Binary files /dev/null and b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-50@2x.png differ diff --git a/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-57.png b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-57.png new file mode 100755 index 0000000..bd97d1a Binary files /dev/null and b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-57.png differ diff --git a/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-57@2x.png b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-57@2x.png new file mode 100755 index 0000000..b8a20f7 Binary files /dev/null and b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-57@2x.png differ diff --git a/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png new file mode 100755 index 0000000..e22345e Binary files /dev/null and b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png differ diff --git a/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png new file mode 100755 index 0000000..50e31f4 Binary files /dev/null and b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png differ diff --git a/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-72.png b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-72.png new file mode 100755 index 0000000..62997ee Binary files /dev/null and b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-72.png differ diff --git a/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-72@2x.png b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-72@2x.png new file mode 100755 index 0000000..2bf71ff Binary files /dev/null and b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-72@2x.png differ diff --git a/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76.png b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76.png new file mode 100755 index 0000000..b6fd014 Binary files /dev/null and b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76.png differ diff --git a/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png new file mode 100755 index 0000000..949426c Binary files /dev/null and b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png differ diff --git a/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png new file mode 100755 index 0000000..b445a7f Binary files /dev/null and b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png differ diff --git a/bytedesk_kefu/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100755 index 0000000..0bedcf2 --- /dev/null +++ b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/bytedesk_kefu/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100755 index 0000000..9da19ea Binary files /dev/null and b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/bytedesk_kefu/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100755 index 0000000..9da19ea Binary files /dev/null and b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/bytedesk_kefu/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100755 index 0000000..9da19ea Binary files /dev/null and b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/bytedesk_kefu/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100755 index 0000000..89c2725 --- /dev/null +++ b/bytedesk_kefu/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/bytedesk_kefu/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/bytedesk_kefu/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/bytedesk_kefu/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bytedesk_kefu/example/ios/Runner/Base.lproj/Main.storyboard b/bytedesk_kefu/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/bytedesk_kefu/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bytedesk_kefu/example/ios/Runner/Info.plist b/bytedesk_kefu/example/ios/Runner/Info.plist new file mode 100644 index 0000000..935ec6f --- /dev/null +++ b/bytedesk_kefu/example/ios/Runner/Info.plist @@ -0,0 +1,74 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + 萝卜丝Demo + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + NSAppleMusicUsageDescription + $(PRODUCT_NAME) 需要读取音乐 + NSCalendarsUsageDescription + $(PRODUCT_NAME) 需要读取日历 + NSCameraUsageDescription + $(PRODUCT_NAME) 拍照并发送图片 + NSContactsUsageDescription + $(PRODUCT_NAME) 需要读取联系人推荐好友 + NSLocationAlwaysAndWhenInUseUsageDescription + 地图功能需要您的定位服务,否则无法使用,如果您需要使用后台定位功能请选择“始终允许”。 + NSLocationAlwaysUsageDescription + 地图功能需要您的定位服务,否则无法使用。 + NSLocationWhenInUseUsageDescription + 地图功能需要您的定位服务,否则无法使用。 + NSMicrophoneUsageDescription + $(PRODUCT_NAME) 发送语音 + NSMotionUsageDescription + $(PRODUCT_NAME) 需要读取运动健身 + NSPhotoLibraryAddUsageDescription + $(PRODUCT_NAME) 发送图片 + NSPhotoLibraryUsageDescription + $(PRODUCT_NAME) 发送图片 + NSSpeechRecognitionUsageDescription + $(PRODUCT_NAME) 需要读取语音识别 + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/bytedesk_kefu/example/ios/Runner/main.m b/bytedesk_kefu/example/ios/Runner/main.m new file mode 100644 index 0000000..dff6597 --- /dev/null +++ b/bytedesk_kefu/example/ios/Runner/main.m @@ -0,0 +1,9 @@ +#import +#import +#import "AppDelegate.h" + +int main(int argc, char* argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/bytedesk_kefu/example/lib/main.dart b/bytedesk_kefu/example/lib/main.dart new file mode 100644 index 0000000..34cc0c1 --- /dev/null +++ b/bytedesk_kefu/example/lib/main.dart @@ -0,0 +1,270 @@ +import 'package:bytedesk_kefu/bytedesk_kefu.dart'; +import 'package:bytedesk_kefu/util/bytedesk_constants.dart'; +import 'package:bytedesk_kefu/util/bytedesk_events.dart'; +import 'package:bytedesk_kefu/util/bytedesk_utils.dart'; +import 'package:bytedesk_kefu_example/notification/custom_notification.dart'; +import 'package:bytedesk_kefu_example/page/chat_type_page.dart'; +import 'package:bytedesk_kefu_example/page/history_thread_page.dart'; +import 'package:bytedesk_kefu_example/page/online_status_page.dart'; +import 'package:bytedesk_kefu_example/page/setting_page.dart'; +import 'package:bytedesk_kefu_example/page/user_info_page.dart'; +import 'package:overlay_support/overlay_support.dart'; +import 'package:flutter/material.dart'; +// import 'package:vibration/vibration.dart'; +// import 'package:audioplayers/audioplayers.dart'; + +void main() { + // runApp(MyApp()); + runApp(OverlaySupport( + child: MaterialApp( + debugShowCheckedModeBanner: false, // 去除右上角debug的标签 + home: MyApp(), + ))); + + // 参考文档:https://github.com/Bytedesk/bytedesk-flutter + // 管理后台:https://www.bytedesk.com/admin + // appkey和subDomain请替换为真实值 + // 获取appkey,登录后台->渠道管理->Flutter->添加应用->获取appkey + String _appKey = '81f427ea-4467-4c7c-b0cd-5c0e4b51456f'; + // 获取subDomain,也即企业号:登录后台->客服管理->客服账号->企业号 + String _subDomain = "vip"; + // 第一步:初始化 + BytedeskKefu.init(_appKey, _subDomain); + // 注:如果需要多平台统一用户(用于同步聊天记录等),可使用: + // BytedeskKefu.initWithUsernameAndNicknameAndAvatar('myuniappusername', '我是美女', 'https://bytedesk.oss-cn-shenzhen.aliyuncs.com/avatars/girl.png', subDomain, appKey); + // BytedeskKefu.initWithUsername('myuniappusername',subDomain, appKey); // 其中:username为自定义用户名,可与开发者所在用户系统对接 + // 如果还需要自定义昵称/头像,可以使用 initWithUsernameAndNickname或initWithUsernameAndNicknameAndAvatar, + // 具体参数可以参考 bytedesk_kefu/bytedesk_kefu.dart 文件 +} + +class MyApp extends StatefulWidget { + @override + _MyAppState createState() => _MyAppState(); +} + +class _MyAppState extends State with WidgetsBindingObserver { + // + String _title = '萝卜丝客服Demo(连接中...)'; + // AudioCache audioCache = AudioCache(); + // bool _isConnected = false; + // + @override + void initState() { + WidgetsBinding.instance?.addObserver(this); + super.initState(); + _listener(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(_title), + elevation: 0, + ), + body: ListView( + children: ListTile.divideTiles( + context: context, + tiles: [ + ListTile( + title: Text('联系客服'), + trailing: Icon(Icons.keyboard_arrow_right), + onTap: () { + // 第二步:联系客服,完毕 + Navigator.of(context) + .push(new MaterialPageRoute(builder: (context) { + return new ChatTypePage(); + })); + }, + ), + ListTile( + title: Text('用户信息'), // 自定义用户资料,设置 + trailing: Icon(Icons.keyboard_arrow_right), + onTap: () { + // 需要首先调用anonymousLogin之后,再调用设置用户信息接口 + Navigator.of(context) + .push(new MaterialPageRoute(builder: (context) { + return new UserInfoPage(); + })); + }, + ), + ListTile( + title: Text('在线状态'), // 技能组或客服账号 在线状态 + trailing: Icon(Icons.keyboard_arrow_right), + onTap: () { + Navigator.of(context) + .push(new MaterialPageRoute(builder: (context) { + return new OnlineStatusPage(); + })); + }, + ), + ListTile( + title: Text('历史会话'), // 会话记录 + trailing: Icon(Icons.keyboard_arrow_right), + onTap: () { + Navigator.of(context) + .push(new MaterialPageRoute(builder: (context) { + return new HistoryThreadPage(); + })); + }, + ), + // ListTile( + // title: Text('TODO:提交工单'), + // trailing: Icon(Icons.keyboard_arrow_right), + // onTap: () { + // print('ticket'); + // // TODO: 提交工单 + // }, + // ), + // ListTile( + // title: Text('TODO:意见反馈'), + // trailing: Icon(Icons.keyboard_arrow_right), + // onTap: () { + // print('feedback'); + // // TODO: 意见反馈 + // }, + // ), + ListTile( + title: Text('消息提示'), + trailing: Icon(Icons.keyboard_arrow_right), + onTap: () { + Navigator.of(context) + .push(new MaterialPageRoute(builder: (context) { + return new SettingPage(); + })); + }, + ), + ListTile( + title: Text('技术支持: QQ-3群: 825257535'), + ) + ], + ).toList()), + // floatingActionButton: FloatingActionButton( + // onPressed: () { + // // 第二步:到 客服管理->技能组-有一列 ‘唯一ID(wId)’, 默认设置工作组wid + // // 说明:一个技能组可以分配多个客服,访客会按照一定的规则分配给组内的各个客服账号 + // String _workGroupWid = "201807171659201"; // 默认人工 + // BytedeskKefu.startWorkGroupChat(context, _workGroupWid, "技能组客服-默认人工"); + // }, + // tooltip: '客服', + // child: Icon(Icons.message), + // ), // Th + ); + } + + // 监听状态 + _listener() { + // 监听连接状态 + bytedeskEventBus.on().listen((event) { + print('长连接状态:' + event.content); + if (event.content == BytedeskConstants.USER_STATUS_CONNECTING) { + setState(() { + _title = "萝卜丝客服Demo(连接中...)"; + }); + } else if (event.content == BytedeskConstants.USER_STATUS_CONNECTED) { + setState(() { + _title = "萝卜丝客服Demo(连接成功)"; + }); + } else if (event.content == BytedeskConstants.USER_STATUS_DISCONNECTED) { + setState(() { + _title = "萝卜丝客服Demo(连接断开)"; + }); + } + }); + // 监听消息,开发者可在此决定是否振动或播放提示音声音 + bytedeskEventBus.on().listen((event) { + // print('receive message:' + event.message.content); + // 1. 首先将example/assets/audio文件夹中文件拷贝到自己项目;2.在自己项目pubspec.yaml中添加assets + // 播放发送消息提示音 + if (BytedeskKefu.getPlayAudioOnSendMessage()! && + event.message.isSend == 1) { + print('play send audio'); + // 修改为自己项目中语音文件路径 + // audioCache.play('audio/bytedesk_dingdong.wav'); + } + if (event.message.isSend == 1) { + // 自己发送的消息,直接返回 + return; + } + // 接收消息播放提示音 + if (BytedeskKefu.getPlayAudioOnReceiveMessage()! && + event.message.isSend == 0) { + print('play receive audio'); + // audioCache.play('audio/bytedesk_dingdong.wav'); + } + // 振动 + if (BytedeskKefu.getVibrateOnReceiveMessage()! && + event.message.isSend == 0) { + print('should vibrate'); + vibrate(); + } + if (event.message.type == BytedeskConstants.MESSAGE_TYPE_TEXT) { + print('文字消息: ' + event.message.content!); + // 判断当前是否客服页面,如否,则显示顶部通知栏 + if (!BytedeskUtils.isCurrentChatKfPage()!) { + // https://github.com/boyan01/overlay_support + showOverlayNotification((context) { + return MessageNotification( + avatar: event.message.user!.avatar!, + nickname: event.message.user!.nickname!, + content: event.message.content!, + onReply: () { + // + OverlaySupportEntry.of(context)!.dismiss(); + // 进入客服页面,支持自定义页面标题 + BytedeskKefu.startChatThread(context, event.message.thread!, + title: '客服会话'); + }, + ); + }, duration: Duration(milliseconds: 4000)); + } + } else if (event.message.type == BytedeskConstants.MESSAGE_TYPE_IMAGE) { + print('图片消息:' + event.message.imageUrl!); + } else if (event.message.type == BytedeskConstants.MESSAGE_TYPE_VOICE) { + print('语音消息:' + event.message.voiceUrl!); + } else if (event.message.type == BytedeskConstants.MESSAGE_TYPE_VIDEO) { + print('视频消息:' + event.message.videoUrl!); + } else if (event.message.type == BytedeskConstants.MESSAGE_TYPE_FILE) { + print('文件消息:' + event.message.fileUrl!); + } else { + print('其他类型消息'); + } + }); + // token过期 + // bytedeskEventBus.on().listen((event) { + // // 执行重新初始化 + // print('InvalidTokenEventBus, token过期'); + // }); + } + + // @override + // void didChangeAppLifecycleState(AppLifecycleState state) { + // print("main didChangeAppLifecycleState:" + state.toString()); + // switch (state) { + // case AppLifecycleState.inactive: // 处于这种状态的应用程序应该假设它们可能在任何时候暂停。 + // break; + // case AppLifecycleState.paused: // 应用程序不可见,后台 + // break; + // case AppLifecycleState.resumed: // 应用程序可见,前台 + // // APP切换到前台之后,重连 + // // BytedeskUtils.mqttReConnect(); + // break; + // case AppLifecycleState.detached: // 申请将暂时暂停 + // break; + // } + // } + + // 振动 + void vibrate() async { + // if (await Vibration.hasVibrator()) { + // Vibration.vibrate(); + // } + } + + @override + void dispose() { + WidgetsBinding.instance?.removeObserver(this); + super.dispose(); + // audioCache? + } +} diff --git a/bytedesk_kefu/example/lib/notification/custom_animation.dart b/bytedesk_kefu/example/lib/notification/custom_animation.dart new file mode 100755 index 0000000..ca83cba --- /dev/null +++ b/bytedesk_kefu/example/lib/notification/custom_animation.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'ios_toast.dart'; + +/// Example to show how to popup overlay with custom animation. +class CustomAnimationToast extends StatelessWidget { + final double? value; + + static final Tween tweenOffset = + Tween(begin: Offset(0, 40), end: Offset(0, 0)); + + static final Tween tweenOpacity = Tween(begin: 0, end: 1); + + const CustomAnimationToast({Key? key, @required this.value}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Transform.translate( + offset: tweenOffset.transform(value!), + child: Opacity( + child: IosStyleToast(), + opacity: tweenOpacity.transform(value!), + ), + ); + } +} diff --git a/bytedesk_kefu/example/lib/notification/custom_notification.dart b/bytedesk_kefu/example/lib/notification/custom_notification.dart new file mode 100755 index 0000000..8a0dede --- /dev/null +++ b/bytedesk_kefu/example/lib/notification/custom_notification.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +class MessageNotification extends StatelessWidget { + // + final VoidCallback? onReply; + final String? avatar; + final String? nickname; + final String? content; + + const MessageNotification({ + Key? key, + @required this.onReply, + @required this.avatar, + @required this.nickname, + @required this.content, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Card( + margin: const EdgeInsets.symmetric(horizontal: 4), + child: SafeArea( + child: ListTile( + leading: SizedBox.fromSize( + size: const Size(40, 40), + child: ClipOval(child: Image.network(avatar!))), + title: Text(nickname!), + subtitle: Text(content!), + trailing: IconButton( + icon: Icon(Icons.reply), + onPressed: () { + if (onReply != null) onReply!(); + }), + ), + ), + ); + } +} diff --git a/bytedesk_kefu/example/lib/notification/ios_toast.dart b/bytedesk_kefu/example/lib/notification/ios_toast.dart new file mode 100755 index 0000000..0cb13cd --- /dev/null +++ b/bytedesk_kefu/example/lib/notification/ios_toast.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; + +class IosStyleToast extends StatelessWidget { + @override + Widget build(BuildContext context) { + return SafeArea( + child: DefaultTextStyle( + style: Theme.of(context) + .textTheme + .bodyText2! + .copyWith(color: Colors.white), + child: Padding( + padding: const EdgeInsets.all(16), + child: Center( + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: Container( + color: Colors.black87, + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 16, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.check, + color: Colors.white, + ), + Text('Succeed') + ], + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/bytedesk_kefu/example/lib/page/chat_type_page.dart b/bytedesk_kefu/example/lib/page/chat_type_page.dart new file mode 100755 index 0000000..6d85b05 --- /dev/null +++ b/bytedesk_kefu/example/lib/page/chat_type_page.dart @@ -0,0 +1,229 @@ +import 'dart:convert'; + +import 'package:bytedesk_kefu/bytedesk_kefu.dart'; +import 'package:bytedesk_kefu/util/bytedesk_constants.dart'; +import 'package:flutter/material.dart'; + +// 多种客服对话类型列表页面 +class ChatTypePage extends StatefulWidget { + ChatTypePage({Key? key}) : super(key: key); + + @override + _ChatTypePageState createState() => _ChatTypePageState(); +} + +class _ChatTypePageState extends State { + // 第二步:到 客服管理->技能组-有一列 ‘唯一ID(wId)’, 默认设置工作组wid + // 说明:一个技能组可以分配多个客服,访客会按照一定的规则分配给组内的各个客服账号 + String _workGroupWid = "201807171659201"; // 默认人工 + String _workGroupWidRobot = "201809061716221"; // 默认机器人, 在管理后台开启或关闭机器人 + // 说明:直接发送给此一个客服账号,一对一会话 + String _agentUid = "201808221551193"; + // 未读消息数目 + String _unreadMessageCount = "0"; + // + @override + void initState() { + // 加载未读消息数目 + _getUnreadCountVisitor(); + // + super.initState(); + } + + // + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('对话类型'), + elevation: 0, + ), + body: ListView( + children: ListTile.divideTiles(context: context, tiles: [ + ListTile( + title: Text('未读消息数目:' + _unreadMessageCount), + // trailing: Icon(Icons.keyboard_arrow_right), + onTap: () { + // 加载未读消息数目 + _getUnreadCountVisitor(); + }, + ), + Container( + height: 20, + ), + ListTile( + title: Text('技能组客服'), + trailing: Icon(Icons.keyboard_arrow_right), + onTap: () { + BytedeskKefu.startWorkGroupChat( + context, _workGroupWid, "技能组客服-默认人工"); + }, + ), + ListTile( + title: Text('技能组客服-机器人'), + trailing: Icon(Icons.keyboard_arrow_right), + onTap: () { + BytedeskKefu.startWorkGroupChat( + context, _workGroupWidRobot, "技能组客服-默认机器人"); + }, + ), + ListTile( + title: Text('技能组客服-电商'), + trailing: Icon(Icons.keyboard_arrow_right), + onTap: () { + // 商品信息,type/title/content/price/url/imageUrl/id/categoryCode + // 注意:长度不能大于500字符 + var custom = json.encode({ + "type": BytedeskConstants.MESSAGE_TYPE_COMMODITY, // 不能修改 + "title": "商品标题", // 可自定义, 类型为字符串 + "content": "商品详情", // 可自定义, 类型为字符串 + "price": "9.99", // 可自定义, 类型为字符串 + "url": + "https://item.m.jd.com/product/12172344.html", // 必须为url网址, 类型为字符串 + "imageUrl": + "https://bytedesk.oss-cn-shenzhen.aliyuncs.com/images/123.webp", //必须为图片网址, 类型为字符串 + "id": 123, // 可自定义 + "categoryCode": "100010003", // 可自定义, 类型为字符串 + "client": "flutter" // 可自定义, 类型为字符串 + }); + BytedeskKefu.startWorkGroupChatShop( + context, _workGroupWid, "技能组客服-电商", custom); + }, + ), + ListTile( + title: Text('技能组客服-电商-回调'), + trailing: Icon(Icons.keyboard_arrow_right), + onTap: () { + // 商品信息,type/title/content/price/url/imageUrl/id/categoryCode + // 注意:长度不能大于500字符 + var custom = json.encode({ + "type": BytedeskConstants.MESSAGE_TYPE_COMMODITY, // 不能修改 + "title": "商品标题", // 可自定义, 类型为字符串 + "content": "商品详情", // 可自定义, 类型为字符串 + "price": "9.99", // 可自定义, 类型为字符串 + "url": + "https://item.m.jd.com/product/12172344.html", // 必须为url网址, 类型为字符串 + "imageUrl": + "https://bytedesk.oss-cn-shenzhen.aliyuncs.com/images/123.webp", //必须为图片网址, 类型为字符串 + "id": 123, // 可自定义 + "categoryCode": "100010003", // 可自定义, 类型为字符串 + "client": "flutter", // 可自定义, 类型为字符串 + // 可自定义添加key:value, 客服端不可见,可用于回调原样返回 + "other1": "", // 可另外添加自定义字段,客服端不可见,可用于回调原样返回 + "other2": "", // 可另外添加自定义字段,客服端不可见,可用于回调原样返回 + "other3": "", // 可另外添加自定义字段,客服端不可见,可用于回调原样返回 + }); + BytedeskKefu.startWorkGroupChatShopCallback( + context, _workGroupWid, "技能组客服-电商-回调", custom, (value) { + print('value为custom参数原样返回 $value'); + // 主要用途:用户在聊天页面点击商品消息,回调此接口,开发者可在此打开进入商品详情页 + }); + }, + ), + ListTile( + title: Text('技能组客服-附言'), + trailing: Icon(Icons.keyboard_arrow_right), + onTap: () { + BytedeskKefu.startWorkGroupChatPostscript( + context, _workGroupWid, "技能组客服-附言", "随便说点什么吧,我会自动发送给客服"); + }, + ), + Container( + height: 20, + ), + ListTile( + title: Text('指定一对一客服'), + trailing: Icon(Icons.keyboard_arrow_right), + onTap: () { + BytedeskKefu.startAppointedChat(context, _agentUid, "指定一对一客服"); + }, + ), + ListTile( + title: Text('指定一对一客服-电商'), + trailing: Icon(Icons.keyboard_arrow_right), + onTap: () { + // 商品信息,type/title/content/price/url/imageUrl/id/categoryCode + // 注意:长度不能大于500字符 + var custom = json.encode({ + "type": BytedeskConstants.MESSAGE_TYPE_COMMODITY, + "title": "商品标题", + "content": "商品详情", + "price": "9.99", + "url": "https://item.m.jd.com/product/12172344.html", + "imageUrl": + "https://bytedesk.oss-cn-shenzhen.aliyuncs.com/images/123.webp", + "id": 123, + "categoryCode": "100010003", + "client": "flutter" + }); + BytedeskKefu.startAppointedChatShop( + context, _agentUid, "指定一对一客服-电商", custom); + }, + ), + ListTile( + title: Text('指定一对一客服-电商-回调'), + trailing: Icon(Icons.keyboard_arrow_right), + onTap: () { + // 商品信息,type/title/content/price/url/imageUrl/id/categoryCode + // 注意:长度不能大于500字符 + var custom = json.encode({ + "type": BytedeskConstants.MESSAGE_TYPE_COMMODITY, + "title": "商品标题", + "content": "商品详情", + "price": "9.99", + "url": "https://item.m.jd.com/product/12172344.html", + "imageUrl": + "https://bytedesk.oss-cn-shenzhen.aliyuncs.com/images/123.webp", + "id": 123, + "categoryCode": "100010003", + "client": "flutter", + // 可自定义添加key:value, 客服端不可见,可用于回调原样返回 + "other1": "", // 可另外添加自定义字段,客服端不可见,可用于回调原样返回 + "other2": "", // 可另外添加自定义字段,客服端不可见,可用于回调原样返回 + "other3": "", // 可另外添加自定义字段,客服端不可见,可用于回调原样返回 + }); + BytedeskKefu.startAppointedChatShopCallback( + context, _agentUid, "指定一对一客服-电商-回调", custom, (value) { + print('value为custom参数原样返回 $value'); + // 主要用途:用户在聊天页面点击商品消息,回调此接口,开发者可在此打开进入商品详情页 + }); + }, + ), + ListTile( + title: Text('指定一对一客服-附言'), + trailing: Icon(Icons.keyboard_arrow_right), + onTap: () { + BytedeskKefu.startAppointedChatPostscript( + context, _agentUid, "指定一对一客服-附言", "随便说点什么吧,我会自动发送给客服"); + }, + ), + Container( + height: 20, + ), + ListTile( + title: Text('H5网页会话'), + trailing: Icon(Icons.keyboard_arrow_right), + onTap: () { + print('h5 chat'); + // 注意: 登录后台->客服管理->技能组(或客服账号)->获取客服代码 获取相应URL + String url = + "https://h1.kefux.cn/chat/h5/index.html?sub=vip&uid=201808221551193&wid=201807171659201&type=workGroup&aid=&hidenav=1&ph=ph"; + String title = 'H5在线客服演示'; + BytedeskKefu.startH5Chat(context, url, title); + }, + ), + ]).toList(), + ), + ); + } + + void _getUnreadCountVisitor() { + // 获取指定客服在线状态 + BytedeskKefu.getUnreadCountVisitor().then((count) => { + print('unreadcount:' + count), + setState(() { + _unreadMessageCount = count; + }) + }); + } +} diff --git a/bytedesk_kefu/example/lib/page/history_thread_page.dart b/bytedesk_kefu/example/lib/page/history_thread_page.dart new file mode 100755 index 0000000..339e099 --- /dev/null +++ b/bytedesk_kefu/example/lib/page/history_thread_page.dart @@ -0,0 +1,66 @@ +import 'package:bytedesk_kefu/bytedesk_kefu.dart'; +import 'package:bytedesk_kefu/model/thread.dart'; +import 'package:flutter/material.dart'; + +// 历史会话列表 +class HistoryThreadPage extends StatefulWidget { + HistoryThreadPage({Key? key}) : super(key: key); + + @override + _HistoryThreadPageState createState() => _HistoryThreadPageState(); +} + +// TODO: 点击thread会话直接进入对话页面 +class _HistoryThreadPageState extends State { + // + int _page = 0; + int _size = 20; + List _historyThreadList = []; + // + @override + void initState() { + _getVisitorThreads(); + super.initState(); + } + + // + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('历史会话'), + elevation: 0, + ), + body: RefreshIndicator( + child: ListView.builder( + padding: EdgeInsets.all(8.0), + itemBuilder: (_, int index) => ListTile( + leading: Image.network(_historyThreadList[index].avatar!), + title: Text( + '${_historyThreadList[index].nickname}, ${_historyThreadList[index].timestamp}'), + subtitle: Text('${_historyThreadList[index].content}'), + onTap: () { + // 进入客服页面 + BytedeskKefu.startChatThread( + context, _historyThreadList[index]); + }, + ), + itemCount: _historyThreadList.length, + ), + onRefresh: _onRefresh, + )); + } + + void _getVisitorThreads() { + BytedeskKefu.getVisitorThreads(_page, _size).then((value) => { + setState(() { + _historyThreadList = value; + }) + }); + } + + Future _onRefresh() async { + _getVisitorThreads(); + _page++; + } +} diff --git a/bytedesk_kefu/example/lib/page/online_status_page.dart b/bytedesk_kefu/example/lib/page/online_status_page.dart new file mode 100755 index 0000000..59f7c61 --- /dev/null +++ b/bytedesk_kefu/example/lib/page/online_status_page.dart @@ -0,0 +1,80 @@ +import 'package:bytedesk_kefu/bytedesk_kefu.dart'; +import 'package:flutter/material.dart'; + +// 查询技能组和指定客服账号的在线状态 +class OnlineStatusPage extends StatefulWidget { + OnlineStatusPage({Key? key}) : super(key: key); + + @override + _OnlineStatusPageState createState() => _OnlineStatusPageState(); +} + +class _OnlineStatusPageState extends State { + // 到 客服管理->技能组-有一列 ‘唯一ID(wId)’ + String _workGroupWid = "201807171659201"; + // 到 客服管理->客服账号-有一列 ‘唯一ID(uId)’ + String _agentUid = "201808221551193"; + // + String _workGroupStatus = ''; // 注:online 代表在线,offline 代表离线 + String _agentStatus = ''; // 注:online 代表在线,offline 代表离线 + // + @override + void initState() { + // 获取技能组在线状态 + _getWorkGroupStatus(); + // 获取指定客服在线状态 + _getAgentStatus(); + super.initState(); + } + + // + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('在线状态'), + elevation: 0, + ), + body: ListView( + children: ListTile.divideTiles( + context: context, + tiles: [ + ListTile( + title: Text('技能组在线状态'), + subtitle: Text(_workGroupStatus), + onTap: () { + _getWorkGroupStatus(); + }, + ), + ListTile( + title: Text('客服在线状态'), + subtitle: Text(_agentStatus), + onTap: () { + _getAgentStatus(); + }, + ), + ], + ).toList()), + ); + } + + void _getWorkGroupStatus() { + // 获取技能组在线状态:当技能组中至少有一个客服在线时,显示在线 + BytedeskKefu.getWorkGroupStatus(_workGroupWid).then((status) => { + print(status), + setState(() { + _workGroupStatus = status; + }) + }); + } + + void _getAgentStatus() { + // 获取指定客服在线状态 + BytedeskKefu.getAgentStatus(_agentUid).then((status) => { + print(status), + setState(() { + _agentStatus = status; + }) + }); + } +} diff --git a/bytedesk_kefu/example/lib/page/setting_page.dart b/bytedesk_kefu/example/lib/page/setting_page.dart new file mode 100755 index 0000000..d189717 --- /dev/null +++ b/bytedesk_kefu/example/lib/page/setting_page.dart @@ -0,0 +1,72 @@ +import 'package:bytedesk_kefu/bytedesk_kefu.dart'; +import 'package:flutter/material.dart'; +import 'package:list_tile_switch/list_tile_switch.dart'; + +// 消息声音、振动设置页面 +class SettingPage extends StatefulWidget { + SettingPage({Key? key}) : super(key: key); + + @override + _SettingPageState createState() => _SettingPageState(); +} + +class _SettingPageState extends State { + bool _playAudioOnSendMessage = false; + bool _playAudioOnReceiveMessage = false; + bool _vibrateOnReceiveMessage = false; + // + @override + void initState() { + _playAudioOnSendMessage = BytedeskKefu.getPlayAudioOnSendMessage()!; + _playAudioOnReceiveMessage = BytedeskKefu.getPlayAudioOnReceiveMessage()!; + _vibrateOnReceiveMessage = BytedeskKefu.getVibrateOnReceiveMessage()!; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('消息设置'), + elevation: 0, + ), + body: ListView( + children: ListTile.divideTiles( + context: context, + tiles: [ + ListTileSwitch( + value: _playAudioOnSendMessage, + onChanged: (value) { + setState(() { + _playAudioOnSendMessage = value; + }); + BytedeskKefu.setPlayAudioOnSendMessage(value); + }, + title: Text('发送消息时播放声音'), + ), + ListTileSwitch( + value: _playAudioOnReceiveMessage, + onChanged: (value) { + setState(() { + _playAudioOnReceiveMessage = value; + }); + BytedeskKefu.setPlayAudioOnReceiveMessage(value); + }, + title: Text('收到消息时播放声音'), + ), + ListTileSwitch( + value: _vibrateOnReceiveMessage, + onChanged: (value) { + setState(() { + _vibrateOnReceiveMessage = value; + }); + // 注意:需要在安卓AndroidManifest.xml添加权限 + BytedeskKefu.setVibrateOnReceiveMessage(value); + }, + title: Text('收到消息时振动'), + ), + ], + ).toList()), + ); + } +} diff --git a/bytedesk_kefu/example/lib/page/user_info_page.dart b/bytedesk_kefu/example/lib/page/user_info_page.dart new file mode 100755 index 0000000..a8739c8 --- /dev/null +++ b/bytedesk_kefu/example/lib/page/user_info_page.dart @@ -0,0 +1,125 @@ +import 'package:bytedesk_kefu/bytedesk_kefu.dart'; +import 'package:bytedesk_kefu/util/bytedesk_constants.dart'; +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; + +// 需要首先调用anonymousLogin之后,再调用此接口 +// 自定义用户信息接口-对接APP用户信息 +class UserInfoPage extends StatefulWidget { + UserInfoPage({Key? key}) : super(key: key); + + @override + _UserInfoPageState createState() => _UserInfoPageState(); +} + +class _UserInfoPageState extends State { + String _uid = ''; // 用户唯一uid + String _username = ''; // 用户唯一用户名 + String _nickname = ''; // 用户昵称 + String _avatar = BytedeskConstants.DEFAULT_AVATA; // 用户头像 + String _description = ''; // 用户备注 + @override + void initState() { + _getProfile(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('用户信息'), + elevation: 0, + ), + body: ListView( + children: ListTile.divideTiles( + context: context, + tiles: [ + ListTile( + title: Text('唯一uid'), + subtitle: Text(_uid), + ), + ListTile( + title: Text('用户名'), + subtitle: Text(_username), + ), + ListTile( + title: Text('设置昵称(见代码)'), + subtitle: Text(_nickname), + onTap: () { + // + _setNickname(); + }, + ), + ListTile( + leading: Image.network( + _avatar, + height: 30, + width: 30, + ), + title: Text('设置头像(见代码)'), + onTap: () { + // + _setAvatar(); + }, + ), + ListTile( + title: Text('设置备注(见代码)'), + subtitle: Text(_description), + onTap: () { + // + _setDescription(); + }, + ), + ], + ).toList()), + ); + } + + void _getProfile() { + // 查询当前用户信息:昵称、头像 + BytedeskKefu.getProfile().then((user) => { + setState(() { + _uid = user.uid!; + _username = user.username!; + _nickname = user.nickname!; + _avatar = user.avatar!; + _description = user.description!; + }) + }); + } + + void _setNickname() { + // 可自定义用户昵称-客服端可见 + String mynickname = '自定义APP昵称flutter'; + BytedeskKefu.updateNickname(mynickname).then((user) => { + setState(() { + _nickname = mynickname; + }), + Fluttertoast.showToast(msg: "设置昵称成功") + }); + } + + void _setAvatar() { + // 可自定义用户头像url-客服端可见,注意:是头像网址,非本地图片路径 + String myavatarurl = + 'https://chainsnow.oss-cn-shenzhen.aliyuncs.com/avatars/visitor_default_avatar.png'; // 头像网址url + BytedeskKefu.updateAvatar(myavatarurl).then((user) => { + setState(() { + _avatar = myavatarurl; + }), + Fluttertoast.showToast(msg: "设置头像成功") + }); + } + + void _setDescription() { + // 可自定义用户昵称-客服端可见 + String description = '自定义用户备注'; + BytedeskKefu.updateDescription(description).then((user) => { + setState(() { + _description = description; + }), + Fluttertoast.showToast(msg: "设置备注成功") + }); + } +} diff --git a/bytedesk_kefu/example/macos/.gitignore b/bytedesk_kefu/example/macos/.gitignore new file mode 100644 index 0000000..d2fd377 --- /dev/null +++ b/bytedesk_kefu/example/macos/.gitignore @@ -0,0 +1,6 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/xcuserdata/ diff --git a/bytedesk_kefu/example/macos/Flutter/Flutter-Debug.xcconfig b/bytedesk_kefu/example/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..4b81f9b --- /dev/null +++ b/bytedesk_kefu/example/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/bytedesk_kefu/example/macos/Flutter/Flutter-Release.xcconfig b/bytedesk_kefu/example/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..5caa9d1 --- /dev/null +++ b/bytedesk_kefu/example/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/bytedesk_kefu/example/macos/Flutter/GeneratedPluginRegistrant.swift b/bytedesk_kefu/example/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..b7296b3 --- /dev/null +++ b/bytedesk_kefu/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,24 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import audioplayers +import bytedesk_kefu +import package_info +import path_provider_macos +import shared_preferences_macos +import sqflite +import wakelock_macos + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AudioplayersPlugin.register(with: registry.registrar(forPlugin: "AudioplayersPlugin")) + BytedeskKefuPlugin.register(with: registry.registrar(forPlugin: "BytedeskKefuPlugin")) + FLTPackageInfoPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + WakelockMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockMacosPlugin")) +} diff --git a/bytedesk_kefu/example/macos/Podfile b/bytedesk_kefu/example/macos/Podfile new file mode 100644 index 0000000..dade8df --- /dev/null +++ b/bytedesk_kefu/example/macos/Podfile @@ -0,0 +1,40 @@ +platform :osx, '10.11' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/bytedesk_kefu/example/macos/Runner.xcodeproj/project.pbxproj b/bytedesk_kefu/example/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..de45192 --- /dev/null +++ b/bytedesk_kefu/example/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,632 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + B116FB6D6F7C17AD14838202 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 92112FB12B6A4D12E1F8251F /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 2922F96554178579FF237F02 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* bytedesk_kefu_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bytedesk_kefu_example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 5C2970447709BFF43B13A2DC /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 92112FB12B6A4D12E1F8251F /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + DD709F0552D4479886D058C8 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B116FB6D6F7C17AD14838202 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 7C73FB1E0B9D9A366BA37E4C /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* bytedesk_kefu_example.app */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + 7C73FB1E0B9D9A366BA37E4C /* Pods */ = { + isa = PBXGroup; + children = ( + DD709F0552D4479886D058C8 /* Pods-Runner.debug.xcconfig */, + 2922F96554178579FF237F02 /* Pods-Runner.release.xcconfig */, + 5C2970447709BFF43B13A2DC /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 92112FB12B6A4D12E1F8251F /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 88DE65861EE7BB7782A96423 /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 929100175D5BE2BD14AA7CC6 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* bytedesk_kefu_example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 0930; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 88DE65861EE7BB7782A96423 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 929100175D5BE2BD14AA7CC6 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/bytedesk_kefu/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/bytedesk_kefu/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/bytedesk_kefu/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/bytedesk_kefu/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/bytedesk_kefu/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..2da6296 --- /dev/null +++ b/bytedesk_kefu/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bytedesk_kefu/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/bytedesk_kefu/example/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/bytedesk_kefu/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/bytedesk_kefu/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/bytedesk_kefu/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/bytedesk_kefu/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/bytedesk_kefu/example/macos/Runner/AppDelegate.swift b/bytedesk_kefu/example/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..d53ef64 --- /dev/null +++ b/bytedesk_kefu/example/macos/Runner/AppDelegate.swift @@ -0,0 +1,9 @@ +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/bytedesk_kefu/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/bytedesk_kefu/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/bytedesk_kefu/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/bytedesk_kefu/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/bytedesk_kefu/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..3c4935a Binary files /dev/null and b/bytedesk_kefu/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/bytedesk_kefu/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/bytedesk_kefu/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..ed4cc16 Binary files /dev/null and b/bytedesk_kefu/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/bytedesk_kefu/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/bytedesk_kefu/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..483be61 Binary files /dev/null and b/bytedesk_kefu/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/bytedesk_kefu/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/bytedesk_kefu/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..bcbf36d Binary files /dev/null and b/bytedesk_kefu/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/bytedesk_kefu/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/bytedesk_kefu/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..9c0a652 Binary files /dev/null and b/bytedesk_kefu/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/bytedesk_kefu/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/bytedesk_kefu/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..e71a726 Binary files /dev/null and b/bytedesk_kefu/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/bytedesk_kefu/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/bytedesk_kefu/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..8a31fe2 Binary files /dev/null and b/bytedesk_kefu/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/bytedesk_kefu/example/macos/Runner/Base.lproj/MainMenu.xib b/bytedesk_kefu/example/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..537341a --- /dev/null +++ b/bytedesk_kefu/example/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,339 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bytedesk_kefu/example/macos/Runner/Configs/AppInfo.xcconfig b/bytedesk_kefu/example/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..ffa6e69 --- /dev/null +++ b/bytedesk_kefu/example/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = bytedesk_kefu_example + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.bytedesk.bytedeskKefuExample + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2021 com.bytedesk. All rights reserved. diff --git a/bytedesk_kefu/example/macos/Runner/Configs/Debug.xcconfig b/bytedesk_kefu/example/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/bytedesk_kefu/example/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/bytedesk_kefu/example/macos/Runner/Configs/Release.xcconfig b/bytedesk_kefu/example/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/bytedesk_kefu/example/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/bytedesk_kefu/example/macos/Runner/Configs/Warnings.xcconfig b/bytedesk_kefu/example/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/bytedesk_kefu/example/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/bytedesk_kefu/example/macos/Runner/DebugProfile.entitlements b/bytedesk_kefu/example/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..dddb8a3 --- /dev/null +++ b/bytedesk_kefu/example/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/bytedesk_kefu/example/macos/Runner/Info.plist b/bytedesk_kefu/example/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/bytedesk_kefu/example/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/bytedesk_kefu/example/macos/Runner/MainFlutterWindow.swift b/bytedesk_kefu/example/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..2722837 --- /dev/null +++ b/bytedesk_kefu/example/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController.init() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/bytedesk_kefu/example/macos/Runner/Release.entitlements b/bytedesk_kefu/example/macos/Runner/Release.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/bytedesk_kefu/example/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/bytedesk_kefu/example/pubspec.yaml b/bytedesk_kefu/example/pubspec.yaml new file mode 100644 index 0000000..fcd0f95 --- /dev/null +++ b/bytedesk_kefu/example/pubspec.yaml @@ -0,0 +1,87 @@ +name: bytedesk_kefu_example +description: Demonstrates how to use the bytedesk_kefu plugin. + +# The following line prevents the package from being accidentally published to +# pub.dev using `pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +environment: + sdk: ">=2.12.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + + # 事件通知总线 + # https://pub.dev/packages/event_bus + event_bus: ^2.0.0 + # toast https://pub.dev/packages/fluttertoast + fluttertoast: ^8.0.9 + # 消息设置switch https://pub.dev/packages/list_tile_switch + list_tile_switch: ^1.0.0 + # 应用内-顶部通知栏 https://pub.dev/packages/overlay_support/ + overlay_support: ^1.2.1 + # 播放提示音 https://pub.dev/packages/audioplayers + # audioplayers: ^0.20.1 + # 振动 https://pub.dev/packages/vibration + # 针对报错fatal error: 'vibration/vibration-Swift.h' file not found #import , ld: library not found for -lvibration + # 请在ios/Podfile中添加:use_frameworks! + # vibration: ^1.7.3 + + bytedesk_kefu: + # When depending on this package from a real application you should use: + # bytedesk_kefu: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + assets: + - assets/audio/ + - assets/images/feedback/ + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/bytedesk_kefu/example/test/widget_test.dart b/bytedesk_kefu/example/test/widget_test.dart new file mode 100644 index 0000000..eec833e --- /dev/null +++ b/bytedesk_kefu/example/test/widget_test.dart @@ -0,0 +1,27 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility that Flutter provides. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:bytedesk_kefu_example/main.dart'; + +void main() { + testWidgets('Verify Platform version', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(MyApp()); + + // Verify that platform version is retrieved. + expect( + find.byWidgetPredicate( + (Widget widget) => + widget is Text && widget.data!.startsWith('Running on:'), + ), + findsOneWidget, + ); + }); +} diff --git a/bytedesk_kefu/example/web/favicon.png b/bytedesk_kefu/example/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/bytedesk_kefu/example/web/favicon.png differ diff --git a/bytedesk_kefu/example/web/icons/Icon-192.png b/bytedesk_kefu/example/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/bytedesk_kefu/example/web/icons/Icon-192.png differ diff --git a/bytedesk_kefu/example/web/icons/Icon-512.png b/bytedesk_kefu/example/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/bytedesk_kefu/example/web/icons/Icon-512.png differ diff --git a/bytedesk_kefu/example/web/index.html b/bytedesk_kefu/example/web/index.html new file mode 100644 index 0000000..c919473 --- /dev/null +++ b/bytedesk_kefu/example/web/index.html @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + bytedesk_kefu_example + + + + + + + + diff --git a/bytedesk_kefu/example/web/manifest.json b/bytedesk_kefu/example/web/manifest.json new file mode 100644 index 0000000..0dba733 --- /dev/null +++ b/bytedesk_kefu/example/web/manifest.json @@ -0,0 +1,23 @@ +{ + "name": "bytedesk_kefu_example", + "short_name": "bytedesk_kefu_example", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "Demonstrates how to use the bytedesk_kefu plugin.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} diff --git a/bytedesk_kefu/home.jpeg b/bytedesk_kefu/home.jpeg new file mode 100755 index 0000000..94c81f2 Binary files /dev/null and b/bytedesk_kefu/home.jpeg differ diff --git a/bytedesk_kefu/ios/.gitignore b/bytedesk_kefu/ios/.gitignore new file mode 100644 index 0000000..aa479fd --- /dev/null +++ b/bytedesk_kefu/ios/.gitignore @@ -0,0 +1,37 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig +/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/bytedesk_kefu/ios/Assets/.gitkeep b/bytedesk_kefu/ios/Assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/bytedesk_kefu/ios/Classes/BytedeskKefuPlugin.h b/bytedesk_kefu/ios/Classes/BytedeskKefuPlugin.h new file mode 100644 index 0000000..6c50d5c --- /dev/null +++ b/bytedesk_kefu/ios/Classes/BytedeskKefuPlugin.h @@ -0,0 +1,4 @@ +#import + +@interface BytedeskKefuPlugin : NSObject +@end diff --git a/bytedesk_kefu/ios/Classes/BytedeskKefuPlugin.m b/bytedesk_kefu/ios/Classes/BytedeskKefuPlugin.m new file mode 100644 index 0000000..268a740 --- /dev/null +++ b/bytedesk_kefu/ios/Classes/BytedeskKefuPlugin.m @@ -0,0 +1,20 @@ +#import "BytedeskKefuPlugin.h" + +@implementation BytedeskKefuPlugin ++ (void)registerWithRegistrar:(NSObject*)registrar { + FlutterMethodChannel* channel = [FlutterMethodChannel + methodChannelWithName:@"bytedesk_kefu" + binaryMessenger:[registrar messenger]]; + BytedeskKefuPlugin* instance = [[BytedeskKefuPlugin alloc] init]; + [registrar addMethodCallDelegate:instance channel:channel]; +} + +- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { + if ([@"getPlatformVersion" isEqualToString:call.method]) { + result([@"iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion]]); + } else { + result(FlutterMethodNotImplemented); + } +} + +@end diff --git a/bytedesk_kefu/ios/bytedesk_kefu.podspec b/bytedesk_kefu/ios/bytedesk_kefu.podspec new file mode 100644 index 0000000..346efda --- /dev/null +++ b/bytedesk_kefu/ios/bytedesk_kefu.podspec @@ -0,0 +1,23 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint bytedesk_kefu.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'bytedesk_kefu' + s.version = '0.0.1' + s.summary = 'A new flutter plugin project.' + s.description = <<-DESC +A new flutter plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.public_header_files = 'Classes/**/*.h' + s.dependency 'Flutter' + s.platform = :ios, '8.0' + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } +end diff --git a/bytedesk_kefu/lib/blocs/contact_bloc/bloc.dart b/bytedesk_kefu/lib/blocs/contact_bloc/bloc.dart new file mode 100755 index 0000000..54d62d3 --- /dev/null +++ b/bytedesk_kefu/lib/blocs/contact_bloc/bloc.dart @@ -0,0 +1,3 @@ +export 'contact_bloc.dart'; +export 'contact_event.dart'; +export 'contact_state.dart'; diff --git a/bytedesk_kefu/lib/blocs/contact_bloc/contact_bloc.dart b/bytedesk_kefu/lib/blocs/contact_bloc/contact_bloc.dart new file mode 100755 index 0000000..8ad73ee --- /dev/null +++ b/bytedesk_kefu/lib/blocs/contact_bloc/contact_bloc.dart @@ -0,0 +1,36 @@ +import 'dart:async'; +import 'package:bloc/bloc.dart'; +import 'package:bytedesk_kefu/blocs/contact_bloc/bloc.dart'; +import 'package:bytedesk_kefu/repositories/contact_repository.dart'; + +class ContactBloc extends Bloc { + final ContactRepository contactRepository = new ContactRepository(); + + // ContactBloc({@required this.contactRepository}); + ContactBloc() : super(ContactUninitialized()); + + // @override + // ContactState get initialState => ContactUninitialized(); + + @override + Stream mapEventToState(ContactEvent event) async* { + // + if (event is RefreshContactEvent) { + yield* _mapRefreshContactToState(event); + } else { + // + } + } + + Stream _mapRefreshContactToState( + RefreshContactEvent event) async* { + yield ContactLoading(); + try { + // final List contactList = await contactRepository.getContacts(); + // yield ContactLoadSuccess(contactList); + } catch (error) { + print(error); + yield ContactLoadError(); + } + } +} diff --git a/bytedesk_kefu/lib/blocs/contact_bloc/contact_event.dart b/bytedesk_kefu/lib/blocs/contact_bloc/contact_event.dart new file mode 100755 index 0000000..c41be0f --- /dev/null +++ b/bytedesk_kefu/lib/blocs/contact_bloc/contact_event.dart @@ -0,0 +1,29 @@ +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; + +@immutable +abstract class ContactEvent extends Equatable { + // ContactEvent([List props = const []]) : super(props); + const ContactEvent(); + + @override + List get props => []; +} + +class RefreshContactEvent extends ContactEvent {} + +class UpdateContactEvent extends ContactEvent { + final String? tid; + + UpdateContactEvent({@required this.tid}) + : assert(tid != null), + super(); +} + +class DeleteContactEvent extends ContactEvent { + final String? tid; + + DeleteContactEvent({@required this.tid}) + : assert(tid != null), + super(); +} diff --git a/bytedesk_kefu/lib/blocs/contact_bloc/contact_state.dart b/bytedesk_kefu/lib/blocs/contact_bloc/contact_state.dart new file mode 100755 index 0000000..ab9f047 --- /dev/null +++ b/bytedesk_kefu/lib/blocs/contact_bloc/contact_state.dart @@ -0,0 +1,45 @@ +import 'package:bytedesk_kefu/model/contact.dart'; +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; + +@immutable +abstract class ContactState extends Equatable { + // ContactState([List props = const []]) : super(); + const ContactState(); + + @override + List get props => []; +} + +class ContactUninitialized extends ContactState { + @override + String toString() => 'ContactUninitialized'; +} + +class ContactEmpty extends ContactState { + @override + String toString() => 'ContactEmpty'; +} + +class ContactLoading extends ContactState { + @override + String toString() => 'ContactLoading'; +} + +class ContactLoadError extends ContactState { + @override + String toString() => 'ContactLoadError'; +} + +class ContactLoadSuccess extends ContactState { + final List contactList; + + const ContactLoadSuccess(this.contactList); + + @override + List get props => [contactList]; + + @override + String toString() => + 'contactLoadSuccess { contactList: ${contactList.length} }'; +} diff --git a/bytedesk_kefu/lib/blocs/feedback_bloc/bloc.dart b/bytedesk_kefu/lib/blocs/feedback_bloc/bloc.dart new file mode 100755 index 0000000..d81fe5c --- /dev/null +++ b/bytedesk_kefu/lib/blocs/feedback_bloc/bloc.dart @@ -0,0 +1,3 @@ +export 'feedback_bloc.dart'; +export 'feedback_event.dart'; +export 'feedback_state.dart'; diff --git a/bytedesk_kefu/lib/blocs/feedback_bloc/feedback_bloc.dart b/bytedesk_kefu/lib/blocs/feedback_bloc/feedback_bloc.dart new file mode 100755 index 0000000..ff3cf2c --- /dev/null +++ b/bytedesk_kefu/lib/blocs/feedback_bloc/feedback_bloc.dart @@ -0,0 +1,63 @@ +import 'dart:async'; +import 'package:bytedesk_kefu/blocs/feedback_bloc/bloc.dart'; +import 'package:bytedesk_kefu/model/helpCategory.dart'; +// import 'package:bytedesk_kefu/model/jsonResult.dart'; +import 'package:bytedesk_kefu/repositories/feedback_repository.dart'; +import 'package:bloc/bloc.dart'; + +class FeedbackBloc extends Bloc { + // + final FeedbackRepository feedbackRepository = new FeedbackRepository(); + + FeedbackBloc() : super(new UnFeedbackState()); + + @override + Stream mapEventToState( + FeedbackEvent event, + ) async* { + if (event is GetFeedbackCategoryEvent) { + yield* _mapGetFeedbackCategoryToState(event); + } else if (event is SubmitFeedbackEvent) { + yield* _mapSubmitFeedbackToState(event); + } else if (event is UploadImageEvent) { + yield* _mapUploadImageToState(event); + } + } + + Stream _mapGetFeedbackCategoryToState( + GetFeedbackCategoryEvent event) async* { + yield FeedbackLoading(); + try { + final List categoryList = + await feedbackRepository.getHelpFeedbackCategories(event.uid); + yield FeedbackCategoryState(categoryList); + } catch (error) { + print(error); + yield FeedbackLoadError(); + } + } + + Stream _mapSubmitFeedbackToState( + SubmitFeedbackEvent event) async* { + yield FeedbackSubmiting(); + try { + // final JsonResult jsonResult = + await feedbackRepository.submitFeedback(event.content, event.imageUrls); + yield FeedbackSubmitSuccess(); + } catch (error) { + print(error); + yield FeedbackSubmitError(); + } + } + + Stream _mapUploadImageToState(UploadImageEvent event) async* { + yield ImageUploading(); + try { + final String url = await feedbackRepository.upload(event.filePath); + yield UploadImageSuccess(url); + } catch (error) { + print(error); + yield UpLoadImageError(); + } + } +} diff --git a/bytedesk_kefu/lib/blocs/feedback_bloc/feedback_event.dart b/bytedesk_kefu/lib/blocs/feedback_bloc/feedback_event.dart new file mode 100755 index 0000000..36c02a7 --- /dev/null +++ b/bytedesk_kefu/lib/blocs/feedback_bloc/feedback_event.dart @@ -0,0 +1,29 @@ +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; + +@immutable +abstract class FeedbackEvent extends Equatable { + const FeedbackEvent(); + + @override + List get props => []; +} + +class GetFeedbackCategoryEvent extends FeedbackEvent { + final String? uid; + GetFeedbackCategoryEvent({@required this.uid}) : super(); +} + +class SubmitFeedbackEvent extends FeedbackEvent { + final List? imageUrls; + final String? content; + + SubmitFeedbackEvent({@required this.content, @required this.imageUrls}) + : super(); +} + +class UploadImageEvent extends FeedbackEvent { + final String? filePath; + + UploadImageEvent({@required this.filePath}) : super(); +} diff --git a/bytedesk_kefu/lib/blocs/feedback_bloc/feedback_state.dart b/bytedesk_kefu/lib/blocs/feedback_bloc/feedback_state.dart new file mode 100755 index 0000000..73ae645 --- /dev/null +++ b/bytedesk_kefu/lib/blocs/feedback_bloc/feedback_state.dart @@ -0,0 +1,77 @@ +import 'package:bytedesk_kefu/model/helpCategory.dart'; +import 'package:equatable/equatable.dart'; + +abstract class FeedbackState extends Equatable { + FeedbackState(); + + @override + List get props => []; +} + +/// UnInitialized +class UnFeedbackState extends FeedbackState { + UnFeedbackState(); + + @override + String toString() => 'UnFeedbackState'; +} + +class FeedbackEmpty extends FeedbackState { + @override + String toString() => 'FeedbackEmpty'; +} + +class FeedbackLoading extends FeedbackState { + @override + String toString() => 'FeedbackLoading'; +} + +class FeedbackLoadError extends FeedbackState { + @override + String toString() => 'FeedbackLoadError'; +} + +/// Initialized +class FeedbackCategoryState extends FeedbackState { + final List categoryList; + + FeedbackCategoryState(this.categoryList) : super(); + + @override + String toString() => 'GetFeedbackCategoryState'; +} + +class FeedbackSubmiting extends FeedbackState { + @override + String toString() => 'FeedbackSubmiting'; +} + +class FeedbackSubmitSuccess extends FeedbackState { + @override + String toString() => 'FeedbackSubmitSuccess'; +} + +class FeedbackSubmitError extends FeedbackState { + @override + String toString() => 'FeedbackSubmitError'; +} + +class ImageUploading extends FeedbackState { + @override + String toString() => 'ImageUploading'; +} + +class UploadImageSuccess extends FeedbackState { + // + final String url; + UploadImageSuccess(this.url); + @override + List get props => [url]; + @override + String toString() => 'UploadImageSuccess { logo: $url }'; +} + +class UpLoadImageError extends FeedbackState { + @override + String toString() => 'UpLoadImageError'; +} diff --git a/bytedesk_kefu/lib/blocs/friend_bloc/bloc.dart b/bytedesk_kefu/lib/blocs/friend_bloc/bloc.dart new file mode 100755 index 0000000..b89bc0e --- /dev/null +++ b/bytedesk_kefu/lib/blocs/friend_bloc/bloc.dart @@ -0,0 +1,3 @@ +export 'friend_bloc.dart'; +export 'friend_event.dart'; +export 'friend_state.dart'; diff --git a/bytedesk_kefu/lib/blocs/friend_bloc/friend_bloc.dart b/bytedesk_kefu/lib/blocs/friend_bloc/friend_bloc.dart new file mode 100755 index 0000000..1ed659b --- /dev/null +++ b/bytedesk_kefu/lib/blocs/friend_bloc/friend_bloc.dart @@ -0,0 +1,91 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:bytedesk_kefu/blocs/friend_bloc/bloc.dart'; +import 'package:bytedesk_kefu/model/friend.dart'; +import 'package:bytedesk_kefu/repositories/friend_repository.dart'; + +class FriendBloc extends Bloc { + final FriendRepository friendRepository = new FriendRepository(); + + FriendBloc() : super(UnFriendState()); + + @override + Stream mapEventToState(FriendEvent event) async* { + // + if (event is QueryFriendEvent) { + yield* _mapQueryFriendToState(event); + } else if (event is UploadFriendAddressEvent) { + yield* _mapUploadFriendAddressToState(event); + } else if (event is QueryFriendAddressEvent) { + yield* _mapQueryFriendAddressToState(event); + } else if (event is QueryFriendNearbyEvent) { + yield* _mapQueryFriendNearbyToState(event); + } else if (event is UpdateFriendNearbyEvent) { + yield* _mapUpdateFriendNearbyToState(event); + } + } + + Stream _mapQueryFriendToState(QueryFriendEvent event) async* { + yield FriendLoading(); + try { + final List friendList = + await friendRepository.getFriends(event.page, event.size); + yield FriendLoadSuccess(friendList); + } catch (error) { + print(error); + yield ErrorFriendState('friend error'); + } + } + + Stream _mapQueryFriendAddressToState( + QueryFriendAddressEvent event) async* { + yield FriendLoading(); + try { + final List friendList = + await friendRepository.getFriendsAddress(event.page, event.size); + yield FriendLoadSuccess(friendList); + } catch (error) { + print(error); + yield ErrorFriendState('friend error'); + } + } + + Stream _mapUploadFriendAddressToState( + UploadFriendAddressEvent event) async* { + yield FriendLoading(); + try { + final Friend friend = + await friendRepository.uploadAddress(event.nickname, event.mobile); + yield FriendCreateSuccess(friend: friend); + } catch (error) { + print(error); + yield ErrorFriendState('friend error'); + } + } + + Stream _mapQueryFriendNearbyToState( + QueryFriendNearbyEvent event) async* { + yield FriendLoading(); + try { + final List friendList = + await friendRepository.getFriendsNearby(event.page, event.size); + yield FriendLoadSuccess(friendList); + } catch (error) { + print(error); + yield ErrorFriendState('friend error'); + } + } + + Stream _mapUpdateFriendNearbyToState( + UpdateFriendNearbyEvent event) async* { + yield FriendLoading(); + try { + await friendRepository.updateLocation(event.latitude, event.longtitude); + yield FriendUpdateSuccess(); + } catch (error) { + print(error); + yield ErrorFriendState('friend error'); + } + } +} diff --git a/bytedesk_kefu/lib/blocs/friend_bloc/friend_event.dart b/bytedesk_kefu/lib/blocs/friend_bloc/friend_event.dart new file mode 100755 index 0000000..ebb9daa --- /dev/null +++ b/bytedesk_kefu/lib/blocs/friend_bloc/friend_event.dart @@ -0,0 +1,45 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; + +abstract class FriendEvent extends Equatable { + const FriendEvent(); + @override + List get props => []; +} + +class InitFriendEvent extends FriendEvent {} + +class QueryFriendEvent extends FriendEvent { + final int? page; + final int? size; + + QueryFriendEvent({@required this.page, @required this.size}); +} + +class UploadFriendAddressEvent extends FriendEvent { + final String? nickname; + final String? mobile; + + UploadFriendAddressEvent({@required this.nickname, @required this.mobile}); +} + +class QueryFriendAddressEvent extends FriendEvent { + final int? page; + final int? size; + + QueryFriendAddressEvent({@required this.page, @required this.size}); +} + +class QueryFriendNearbyEvent extends FriendEvent { + final int? page; + final int? size; + + QueryFriendNearbyEvent({@required this.page, @required this.size}); +} + +class UpdateFriendNearbyEvent extends FriendEvent { + final double? latitude; + final double? longtitude; + + UpdateFriendNearbyEvent({@required this.latitude, @required this.longtitude}); +} diff --git a/bytedesk_kefu/lib/blocs/friend_bloc/friend_state.dart b/bytedesk_kefu/lib/blocs/friend_bloc/friend_state.dart new file mode 100755 index 0000000..90f3263 --- /dev/null +++ b/bytedesk_kefu/lib/blocs/friend_bloc/friend_state.dart @@ -0,0 +1,60 @@ +import 'package:bytedesk_kefu/model/friend.dart'; +import 'package:equatable/equatable.dart'; + +abstract class FriendState extends Equatable { + const FriendState(); + @override + List get props => []; +} + +/// UnInitialized +class UnFriendState extends FriendState { + UnFriendState(); + + @override + String toString() => 'UnFriendState'; +} + +/// Initialized +class FriendLoading extends FriendState { + FriendLoading() : super(); + + @override + String toString() => 'FriendLoading'; +} + +class FriendUpdateSuccess extends FriendState { + FriendUpdateSuccess() : super(); + + @override + String toString() => 'FriendUpdateSuccess'; +} + +class FriendCreateSuccess extends FriendState { + final Friend? friend; + FriendCreateSuccess({this.friend}); + + @override + String toString() => 'FriendCreateSuccess'; +} + +class FriendLoadSuccess extends FriendState { + final List friendList; + + FriendLoadSuccess(this.friendList); + + @override + List get props => [friendList]; + + @override + String toString() => 'FriendLoadSuccess { FriendList: ${friendList.length} }'; +} + +class ErrorFriendState extends FriendState { + final String errorMessage; + + ErrorFriendState(this.errorMessage); + + @override + String toString() => 'ErrorFriendState'; +} diff --git a/bytedesk_kefu/lib/blocs/global_bloc/bloc.dart b/bytedesk_kefu/lib/blocs/global_bloc/bloc.dart new file mode 100755 index 0000000..34070f4 --- /dev/null +++ b/bytedesk_kefu/lib/blocs/global_bloc/bloc.dart @@ -0,0 +1,2 @@ +export './settings_bloc.dart'; +export './theme_bloc.dart'; diff --git a/bytedesk_kefu/lib/blocs/global_bloc/settings_bloc.dart b/bytedesk_kefu/lib/blocs/global_bloc/settings_bloc.dart new file mode 100755 index 0000000..5877293 --- /dev/null +++ b/bytedesk_kefu/lib/blocs/global_bloc/settings_bloc.dart @@ -0,0 +1,45 @@ +// import 'dart:async'; + +// import 'package:meta/meta.dart'; +// import 'package:bloc/bloc.dart'; +// import 'package:equatable/equatable.dart'; + +// abstract class SettingsEvent extends Equatable { +// const SettingsEvent(); + +// @override +// List get props => []; +// } + +// class TemperatureUnitsToggled extends SettingsEvent {} + +// enum TemperatureUnits { fahrenheit, celsius } + +// class SettingsState extends Equatable { +// final TemperatureUnits temperatureUnits; + +// @override +// List get props => []; + +// SettingsState({@required this.temperatureUnits}) +// : assert(temperatureUnits != null), +// super(); +// } + +// class SettingsBloc extends Bloc { +// @override +// SettingsState get initialState => +// SettingsState(temperatureUnits: TemperatureUnits.celsius); + +// @override +// Stream mapEventToState(SettingsEvent event) async* { +// if (event is TemperatureUnitsToggled) { +// // yield SettingsState( +// // temperatureUnits: +// // currentState.temperatureUnits == TemperatureUnits.celsius +// // ? TemperatureUnits.fahrenheit +// // : TemperatureUnits.celsius, +// // ); +// } +// } +// } diff --git a/bytedesk_kefu/lib/blocs/global_bloc/theme_bloc.dart b/bytedesk_kefu/lib/blocs/global_bloc/theme_bloc.dart new file mode 100755 index 0000000..c75fea6 --- /dev/null +++ b/bytedesk_kefu/lib/blocs/global_bloc/theme_bloc.dart @@ -0,0 +1,42 @@ +// import 'dart:async'; + +// import 'package:flutter/material.dart'; + +// import 'package:meta/meta.dart'; +// import 'package:bloc/bloc.dart'; +// import 'package:equatable/equatable.dart'; + +// class ThemeState extends Equatable { +// final ThemeData theme; +// final MaterialColor color; + +// @override +// List get props => []; + +// ThemeState({@required this.theme, @required this.color}) +// : assert(theme != null), +// assert(color != null), +// super(); +// } + +// abstract class ThemeEvent extends Equatable { +// // ThemeEvent([List props = const []]) : super(props); +// const ThemeEvent(); + +// @override +// List get props => []; +// } + +// class ThemeBloc extends Bloc { + +// @override +// ThemeState get initialState => ThemeState( +// theme: ThemeData.light(), +// color: Colors.lightBlue, +// ); + +// @override +// Stream mapEventToState(ThemeEvent event) async* { + +// } +// } diff --git a/bytedesk_kefu/lib/blocs/group_bloc/bloc.dart b/bytedesk_kefu/lib/blocs/group_bloc/bloc.dart new file mode 100755 index 0000000..19c3ef1 --- /dev/null +++ b/bytedesk_kefu/lib/blocs/group_bloc/bloc.dart @@ -0,0 +1,3 @@ +export 'group_bloc.dart'; +export 'group_event.dart'; +export 'group_state.dart'; diff --git a/bytedesk_kefu/lib/blocs/group_bloc/group_bloc.dart b/bytedesk_kefu/lib/blocs/group_bloc/group_bloc.dart new file mode 100755 index 0000000..1ff9bc5 --- /dev/null +++ b/bytedesk_kefu/lib/blocs/group_bloc/group_bloc.dart @@ -0,0 +1,14 @@ +import 'dart:async'; +import 'package:bloc/bloc.dart'; +import './bloc.dart'; + +class GroupBloc extends Bloc { + GroupBloc() : super(InitialGroupState()); + + @override + Stream mapEventToState( + GroupEvent event, + ) async* { + // TODO: Add Logic + } +} diff --git a/bytedesk_kefu/lib/blocs/group_bloc/group_event.dart b/bytedesk_kefu/lib/blocs/group_bloc/group_event.dart new file mode 100755 index 0000000..5f804fd --- /dev/null +++ b/bytedesk_kefu/lib/blocs/group_bloc/group_event.dart @@ -0,0 +1,11 @@ +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; + +@immutable +abstract class GroupEvent extends Equatable { + // GroupEvent([List props = const []]) : super(props); + const GroupEvent(); + + @override + List get props => []; +} diff --git a/bytedesk_kefu/lib/blocs/group_bloc/group_state.dart b/bytedesk_kefu/lib/blocs/group_bloc/group_state.dart new file mode 100755 index 0000000..85c4f39 --- /dev/null +++ b/bytedesk_kefu/lib/blocs/group_bloc/group_state.dart @@ -0,0 +1,13 @@ +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; + +@immutable +abstract class GroupState extends Equatable { + // GroupState([List props = const []]) : super(props); + const GroupState(); + + @override + List get props => []; +} + +class InitialGroupState extends GroupState {} diff --git a/bytedesk_kefu/lib/blocs/help_bloc/bloc.dart b/bytedesk_kefu/lib/blocs/help_bloc/bloc.dart new file mode 100755 index 0000000..2a2afbf --- /dev/null +++ b/bytedesk_kefu/lib/blocs/help_bloc/bloc.dart @@ -0,0 +1,3 @@ +export 'help_bloc.dart'; +export 'help_event.dart'; +export 'help_state.dart'; diff --git a/bytedesk_kefu/lib/blocs/help_bloc/help_bloc.dart b/bytedesk_kefu/lib/blocs/help_bloc/help_bloc.dart new file mode 100755 index 0000000..38e0d1e --- /dev/null +++ b/bytedesk_kefu/lib/blocs/help_bloc/help_bloc.dart @@ -0,0 +1,47 @@ +import 'dart:async'; +import 'package:bytedesk_kefu/blocs/help_bloc/bloc.dart'; +import 'package:bytedesk_kefu/model/helpArticle.dart'; +import 'package:bytedesk_kefu/model/helpCategory.dart'; +import 'package:bytedesk_kefu/repositories/help_repository.dart'; +import 'package:bloc/bloc.dart'; + +class HelpBloc extends Bloc { + // + final HelpRepository helpRepository = new HelpRepository(); + + HelpBloc() : super(new UnHelpState()); + + @override + Stream mapEventToState(HelpEvent event) async* { + if (event is GetHelpCategoryEvent) { + yield* _mapGetHelpCategoryToState(event); + } else if (event is GetHelpArticleEvent) { + yield* _mapGetHelpArticleState(event); + } + } + + Stream _mapGetHelpCategoryToState( + GetHelpCategoryEvent event) async* { + yield HelpLoading(); + try { + final List categoryList = + await helpRepository.getHelpCategories(event.uid); + yield HelpCategoryState(categoryList); + } catch (error) { + print(error); + yield HelpLoadError(); + } + } + + Stream _mapGetHelpArticleState(GetHelpArticleEvent event) async* { + yield HelpLoading(); + try { + final List categoryList = + await helpRepository.getCategoryArticles(event.categoryId); + yield HelpArticleState(categoryList); + } catch (error) { + print(error); + yield HelpLoadError(); + } + } +} diff --git a/bytedesk_kefu/lib/blocs/help_bloc/help_event.dart b/bytedesk_kefu/lib/blocs/help_bloc/help_event.dart new file mode 100755 index 0000000..45357c9 --- /dev/null +++ b/bytedesk_kefu/lib/blocs/help_bloc/help_event.dart @@ -0,0 +1,21 @@ +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; + +@immutable +abstract class HelpEvent extends Equatable { + const HelpEvent(); + + @override + List get props => []; +} + +class GetHelpCategoryEvent extends HelpEvent { + final String? uid; + GetHelpCategoryEvent({@required this.uid}) : super(); +} + +class GetHelpArticleEvent extends HelpEvent { + final int? categoryId; + + GetHelpArticleEvent({@required this.categoryId}) : super(); +} diff --git a/bytedesk_kefu/lib/blocs/help_bloc/help_state.dart b/bytedesk_kefu/lib/blocs/help_bloc/help_state.dart new file mode 100755 index 0000000..259dbe5 --- /dev/null +++ b/bytedesk_kefu/lib/blocs/help_bloc/help_state.dart @@ -0,0 +1,52 @@ +import 'package:bytedesk_kefu/model/helpArticle.dart'; +import 'package:bytedesk_kefu/model/helpCategory.dart'; +import 'package:equatable/equatable.dart'; + +abstract class HelpState extends Equatable { + HelpState(); + + @override + List get props => []; +} + +/// UnInitialized +class UnHelpState extends HelpState { + UnHelpState(); + + @override + String toString() => 'UnHelpState'; +} + +class HelpEmpty extends HelpState { + @override + String toString() => 'HelpEmpty'; +} + +class HelpLoading extends HelpState { + @override + String toString() => 'HelpLoading'; +} + +class HelpLoadError extends HelpState { + @override + String toString() => 'HelpLoadError'; +} + +/// Initialized +class HelpCategoryState extends HelpState { + final List categoryList; + + HelpCategoryState(this.categoryList) : super(); + + @override + String toString() => 'GetHelpCategoryState'; +} + +class HelpArticleState extends HelpState { + final List articleList; + + HelpArticleState(this.articleList) : super(); + + @override + String toString() => 'GetHelpArticleState'; +} diff --git a/bytedesk_kefu/lib/blocs/leavemsg_bloc/bloc.dart b/bytedesk_kefu/lib/blocs/leavemsg_bloc/bloc.dart new file mode 100755 index 0000000..51b2375 --- /dev/null +++ b/bytedesk_kefu/lib/blocs/leavemsg_bloc/bloc.dart @@ -0,0 +1,3 @@ +export 'leavemsg_bloc.dart'; +export 'leavemsg_event.dart'; +export 'leavemsg_state.dart'; diff --git a/bytedesk_kefu/lib/blocs/leavemsg_bloc/leavemsg_bloc.dart b/bytedesk_kefu/lib/blocs/leavemsg_bloc/leavemsg_bloc.dart new file mode 100755 index 0000000..ade9d52 --- /dev/null +++ b/bytedesk_kefu/lib/blocs/leavemsg_bloc/leavemsg_bloc.dart @@ -0,0 +1,62 @@ +import 'dart:async'; +import 'package:bytedesk_kefu/blocs/leavemsg_bloc/bloc.dart'; +import 'package:bloc/bloc.dart'; +import 'package:bytedesk_kefu/repositories/leavemsg_repository.dart'; + +class LeaveMsgBloc extends Bloc { + // + final LeaveMsgRepository leaveMsgRepository = new LeaveMsgRepository(); + + LeaveMsgBloc() : super(new UnLeaveMsgState()); + + @override + Stream mapEventToState( + LeaveMsgEvent event, + ) async* { + // if (event is GetLeaveMsgCategoryEvent) { + // yield* _mapGetLeaveMsgCategoryToState(event); + // } else + if (event is SubmitLeaveMsgEvent) { + yield* _mapSubmitLeaveMsgToState(event); + } else if (event is UploadImageEvent) { + yield* _mapUploadImageToState(event); + } + } + + // Stream _mapGetLeaveMsgCategoryToState( + // GetLeaveMsgCategoryEvent event) async* { + // yield LeaveMsgLoading(); + // try { + // final List categoryList = + // await leaveMsgRepository.getHelpLeaveMsgCategories(event.uid); + // yield LeaveMsgCategoryState(categoryList); + // } catch (error) { + // print(error); + // yield LeaveMsgLoadError(); + // } + // } + + Stream _mapSubmitLeaveMsgToState( + SubmitLeaveMsgEvent event) async* { + yield LeaveMsgSubmiting(); + try { + // final JsonResult jsonResult = + await leaveMsgRepository.submitLeaveMsg(event.content, event.imageUrls); + yield LeaveMsgSubmitSuccessState(); + } catch (error) { + print(error); + yield LeaveMsgSubmitError(); + } + } + + Stream _mapUploadImageToState(UploadImageEvent event) async* { + yield ImageUploading(); + try { + final String url = await leaveMsgRepository.upload(event.filePath); + yield UploadImageSuccess(url); + } catch (error) { + print(error); + yield UpLoadImageError(); + } + } +} diff --git a/bytedesk_kefu/lib/blocs/leavemsg_bloc/leavemsg_event.dart b/bytedesk_kefu/lib/blocs/leavemsg_bloc/leavemsg_event.dart new file mode 100755 index 0000000..d976578 --- /dev/null +++ b/bytedesk_kefu/lib/blocs/leavemsg_bloc/leavemsg_event.dart @@ -0,0 +1,29 @@ +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; + +@immutable +abstract class LeaveMsgEvent extends Equatable { + const LeaveMsgEvent(); + + @override + List get props => []; +} + +class GetLeaveMsgCategoryEvent extends LeaveMsgEvent { + final String? uid; + GetLeaveMsgCategoryEvent({@required this.uid}) : super(); +} + +class SubmitLeaveMsgEvent extends LeaveMsgEvent { + final List? imageUrls; + final String? content; + + SubmitLeaveMsgEvent({@required this.content, @required this.imageUrls}) + : super(); +} + +class UploadImageEvent extends LeaveMsgEvent { + final String? filePath; + + UploadImageEvent({@required this.filePath}) : super(); +} diff --git a/bytedesk_kefu/lib/blocs/leavemsg_bloc/leavemsg_state.dart b/bytedesk_kefu/lib/blocs/leavemsg_bloc/leavemsg_state.dart new file mode 100755 index 0000000..a06254d --- /dev/null +++ b/bytedesk_kefu/lib/blocs/leavemsg_bloc/leavemsg_state.dart @@ -0,0 +1,60 @@ +// import 'package:bytedesk_kefu/model/helpCategory.dart'; +import 'package:equatable/equatable.dart'; + +abstract class LeaveMsgState extends Equatable { + LeaveMsgState(); + + @override + List get props => []; +} + +/// UnInitialized +class UnLeaveMsgState extends LeaveMsgState { + UnLeaveMsgState(); + + @override + String toString() => 'UnLeaveMsgState'; +} + +class LeaveMsgEmpty extends LeaveMsgState { + @override + String toString() => 'LeaveMsgEmpty'; +} + +class LeaveMsgSubmiting extends LeaveMsgState { + @override + String toString() => 'LeaveMsgSubmiting'; +} + +class LeaveMsgSubmitError extends LeaveMsgState { + @override + String toString() => 'LeaveMsgSubmitError'; +} + +/// Initialized +class LeaveMsgSubmitSuccessState extends LeaveMsgState { + LeaveMsgSubmitSuccessState() : super(); + + @override + String toString() => 'LeaveMsgSubmitSuccessState'; +} + +class ImageUploading extends LeaveMsgState { + @override + String toString() => 'ImageUploading'; +} + +class UploadImageSuccess extends LeaveMsgState { + // + final String url; + UploadImageSuccess(this.url); + @override + List get props => [url]; + @override + String toString() => 'UploadImageSuccess { logo: $url }'; +} + +class UpLoadImageError extends LeaveMsgState { + @override + String toString() => 'UpLoadImageError'; +} diff --git a/bytedesk_kefu/lib/blocs/login_bloc/bloc.dart b/bytedesk_kefu/lib/blocs/login_bloc/bloc.dart new file mode 100755 index 0000000..7aff76e --- /dev/null +++ b/bytedesk_kefu/lib/blocs/login_bloc/bloc.dart @@ -0,0 +1,3 @@ +export 'login_bloc.dart'; +export 'login_event.dart'; +export 'login_state.dart'; diff --git a/bytedesk_kefu/lib/blocs/login_bloc/login_bloc.dart b/bytedesk_kefu/lib/blocs/login_bloc/login_bloc.dart new file mode 100755 index 0000000..5a0c3a4 --- /dev/null +++ b/bytedesk_kefu/lib/blocs/login_bloc/login_bloc.dart @@ -0,0 +1,181 @@ +import 'dart:async'; +import 'package:bytedesk_kefu/model/codeResult.dart'; +import 'package:bytedesk_kefu/model/jsonResult.dart'; +import 'package:bytedesk_kefu/model/oauth.dart'; +import 'package:bloc/bloc.dart'; +import 'package:bytedesk_kefu/blocs/login_bloc/bloc.dart'; +import 'package:bytedesk_kefu/repositories/repositories.dart'; + +class LoginBloc extends Bloc { + // + final UserRepository _userRepository = new UserRepository(); + + LoginBloc() : super(LoginInitial()); + + @override + Stream mapEventToState(LoginEvent event) async* { + // + if (event is LoginButtonPressed) { + yield* _mapLoginState(event); + } else if (event is SMSLoginButtonPressed) { + yield* _mapSMSLoginState(event); + } else if (event is RegisterButtonPressed) { + yield* _mapRegisterState(event); + } else if (event is RequestCodeButtonPressed) { + yield* _mapRequestCodeState(event); + } else if (event is BindMobileEvent) { + yield* _mapBindMobileState(event); + } else if (event is UnionidOAuthEvent) { + yield* _mapUnionidOAuthState(event); + } else if (event is ResetPasswordButtonPressed) { + yield* _mapResetPasswordState(event); + } else if (event is UpdatePasswordButtonPressed) { + yield* _mapUpdatePasswordState(event); + } + } + + Stream _mapLoginState(LoginButtonPressed event) async* { + yield LoginInProgress(); + try { + OAuth oauth = await _userRepository.login(event.username, event.password); + if (oauth.statusCode == 200) { + // 用户名密码正确,登录成功 + yield LoginSuccess(); + } else { + // 用户名密码错误,登录失败 + yield LoginError(); + } + } catch (error) { + // 网络或其他错误 + yield LoginFailure(error: error.toString()); + } + } + + Stream _mapSMSLoginState(SMSLoginButtonPressed event) async* { + yield LoginInProgress(); + try { + // + OAuth oauth = await _userRepository.smsOAuth(event.mobile, event.code); + if (oauth.statusCode == 200) { + // 用户名密码正确,登录成功 + yield SMSLoginSuccess(); + } else { + // 用户名密码错误,登录失败 + yield LoginError(); + } + } catch (error) { + // 网络或其他错误 + yield LoginFailure(error: error.toString()); + } + } + + Stream _mapRegisterState(RegisterButtonPressed event) async* { + yield RegisterInProgress(); + try { + // + JsonResult jsonResult = + await _userRepository.register(event.mobile, event.password); + if (jsonResult.statusCode == 200) { + yield RegisterSuccess(); + } else { + yield RegisterError( + message: jsonResult.message, statusCode: jsonResult.statusCode); + } + } catch (error) { + // 网络或其他错误 + yield LoginFailure(error: error.toString()); + } + } + + Stream _mapRequestCodeState( + RequestCodeButtonPressed event) async* { + yield RequestCodeInProgress(); + try { + // + CodeResult codeResult = await _userRepository.requestCode(event.mobile); + if (codeResult.statusCode == 200) { + yield RequestCodeSuccess(codeResult: codeResult); + } else { + yield RequestCodeError( + message: codeResult.message, statusCode: codeResult.statusCode); + } + } catch (error) { + // 网络或其他错误 + yield LoginFailure(error: error.toString()); + } + } + + Stream _mapBindMobileState(BindMobileEvent event) async* { + yield BindMobileInProgress(); + try { + // + JsonResult jsonResult = await _userRepository.bindMobile(event.mobile); + if (jsonResult.statusCode == 200) { + yield BindMobileSuccess(jsonResult: jsonResult); + } else { + yield BindMobileError( + message: jsonResult.message, statusCode: jsonResult.statusCode); + } + } catch (error) { + // 网络或其他错误 + yield LoginFailure(error: error.toString()); + } + } + + Stream _mapUnionidOAuthState(UnionidOAuthEvent event) async* { + yield UnionidOAuthInProgress(); + try { + // + OAuth oauth = await _userRepository.unionIdOAuth(event.unionid); + if (oauth.statusCode == 200) { + // 登录成功 + yield UnionidLoginSuccess(); + } else { + // 登录失败 + yield LoginError(); + } + } catch (error) { + // 网络或其他错误 + yield LoginFailure(error: error.toString()); + } + } + + Stream _mapResetPasswordState( + ResetPasswordButtonPressed event) async* { + yield ResetPasswordInProgress(); + try { + // + JsonResult jsonResult = + await _userRepository.changePassword(event.mobile, event.password); + if (jsonResult.statusCode == 200) { + yield ResetPasswordSuccess(); + } else { + yield ResetPasswordError( + message: jsonResult.message, statusCode: jsonResult.statusCode); + } + } catch (error) { + // 网络或其他错误 + yield LoginFailure(error: error.toString()); + } + } + + Stream _mapUpdatePasswordState( + UpdatePasswordButtonPressed event) async* { + // + yield UpdatePasswordInProgress(); + try { + // + JsonResult jsonResult = + await _userRepository.changePassword(event.mobile, event.password); + if (jsonResult.statusCode == 200) { + yield UpdatePasswordSuccess(); + } else { + yield UpdatePasswordError( + message: jsonResult.message, statusCode: jsonResult.statusCode); + } + } catch (error) { + // 网络或其他错误 + yield LoginFailure(error: error.toString()); + } + } +} diff --git a/bytedesk_kefu/lib/blocs/login_bloc/login_event.dart b/bytedesk_kefu/lib/blocs/login_bloc/login_event.dart new file mode 100755 index 0000000..728c65c --- /dev/null +++ b/bytedesk_kefu/lib/blocs/login_bloc/login_event.dart @@ -0,0 +1,122 @@ +import 'package:meta/meta.dart'; +import 'package:equatable/equatable.dart'; + +@immutable +abstract class LoginEvent extends Equatable { + const LoginEvent(); + + @override + List get props => []; +} + +class LoginButtonPressed extends LoginEvent { + // + final String? username; + final String? password; + + LoginButtonPressed({@required this.username, @required this.password}) + : super(); + + @override + String toString() { + return 'LoginButtonPressed { username: $username, password: $password }'; + } +} + +class SMSLoginButtonPressed extends LoginEvent { + // + final String? mobile; + final String? code; + + SMSLoginButtonPressed({@required this.mobile, @required this.code}) : super(); + + @override + String toString() { + return 'SMSLoginButtonPressed { username: $mobile, password: $code }'; + } +} + +class RegisterButtonPressed extends LoginEvent { + // + final String? mobile; + final String? password; + // + RegisterButtonPressed({@required this.mobile, @required this.password}) + : super(); + @override + String toString() { + return 'RegisterButtonPressed { mobile: $mobile, password: $password }'; + } +} + +class RequestCodeButtonPressed extends LoginEvent { + // + final String? mobile; + RequestCodeButtonPressed({@required this.mobile}) : super(); + @override + String toString() { + return 'RequestCodeButtonPressed { mobile: $mobile}'; + } +} + +class BindMobileEvent extends LoginEvent { + // + final String? mobile; + BindMobileEvent({@required this.mobile}) : super(); + @override + String toString() { + return 'BindMobileEvent { mobile: $mobile}'; + } +} + +class UnionidOAuthEvent extends LoginEvent { + // + final String? unionid; + UnionidOAuthEvent({@required this.unionid}) : super(); + // + @override + String toString() { + return 'UnionidOAuthEvent { unionid: $unionid}'; + } +} + +class ResetPasswordButtonPressed extends LoginEvent { + // + final String? mobile; + final String? password; + ResetPasswordButtonPressed({@required this.mobile, @required this.password}) + : super(); + @override + String toString() { + return 'ResetPasswordButtonPressed { mobile: $mobile, password: $password }'; + } +} + +class UpdatePasswordButtonPressed extends LoginEvent { + // + final String? mobile; + final String? password; + + UpdatePasswordButtonPressed({@required this.mobile, @required this.password}) + : super(); + + @override + String toString() { + return 'UpdatePasswordButtonPressed { mobile: $mobile, password: $password }'; + } +} + +// 将unionid绑定到已经存在的手机账号 +class BindWechatMobileEvent extends LoginEvent { + // + final String? mobile; + final String? unionid; + // + BindWechatMobileEvent({@required this.mobile, @required this.unionid}) + : super(); + // + @override + String toString() { + return 'BindWechatMobileEvent { username: $mobile, password: $unionid }'; + } +} diff --git a/bytedesk_kefu/lib/blocs/login_bloc/login_state.dart b/bytedesk_kefu/lib/blocs/login_bloc/login_state.dart new file mode 100755 index 0000000..e4b1650 --- /dev/null +++ b/bytedesk_kefu/lib/blocs/login_bloc/login_state.dart @@ -0,0 +1,174 @@ +import 'package:bytedesk_kefu/model/codeResult.dart'; +import 'package:bytedesk_kefu/model/jsonResult.dart'; +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; + +abstract class LoginState extends Equatable { + const LoginState(); + + @override + List get props => []; +} + +class LoginInitial extends LoginState { + @override + String toString() => 'LoginInitial'; +} + +class LoginInProgress extends LoginState { + @override + String toString() => 'LoginInProgress'; +} + +class LoginSuccess extends LoginState { + @override + String toString() => 'LoginSuccess'; +} + +class SMSLoginSuccess extends LoginState { + @override + String toString() => 'SMSLoginSuccess'; +} + +class UnionidLoginSuccess extends LoginState { + @override + String toString() => 'UnionidLoginSuccess'; +} + +class LoginError extends LoginState { + @override + String toString() => 'LoginError'; +} + +class LoginFailure extends LoginState { + final String? error; + + const LoginFailure({@required this.error}); + + @override + List get props => [error!]; + + @override + String toString() => 'LoginFailure { error: $error }'; +} + +class ResultSuccess extends LoginState { + @override + String toString() => 'ResultSuccess'; +} + +class ResultError extends LoginState { + final String? message; + final int? statusCode; + + const ResultError({@required this.message, @required this.statusCode}); + + @override + String toString() => 'ResultError { error: $message }'; +} + +class RegisterInProgress extends LoginState { + @override + String toString() => 'RegisterInProgress'; +} + +class RegisterSuccess extends LoginState { + @override + String toString() => 'RegisterSuccess'; +} + +class RegisterError extends LoginState { + final String? message; + final int? statusCode; + + const RegisterError({@required this.message, @required this.statusCode}); + + @override + String toString() => 'RegisterError { error: $message }'; +} + +class RequestCodeInProgress extends LoginState { + @override + String toString() => 'RequestCodeInProgress'; +} + +class RequestCodeSuccess extends LoginState { + final CodeResult? codeResult; + RequestCodeSuccess({@required this.codeResult}) : super(); + @override + String toString() => 'RequestCodeSuccess'; +} + +class RequestCodeError extends LoginState { + final String? message; + final int? statusCode; + const RequestCodeError({@required this.message, @required this.statusCode}); + @override + String toString() => 'RequestCodeError { error: $message }'; +} + +class ResetPasswordInProgress extends LoginState { + @override + String toString() => 'ResetPasswordInProgress'; +} + +class ResetPasswordSuccess extends LoginState { + @override + String toString() => 'ResetPasswordSuccess'; +} + +class ResetPasswordError extends LoginState { + final String? message; + final int? statusCode; + + const ResetPasswordError({@required this.message, @required this.statusCode}); + + @override + String toString() => 'ResetPasswordError { error: $message }'; +} + +class UpdatePasswordInProgress extends LoginState { + @override + String toString() => 'UpdatePasswordInProgress'; +} + +class UpdatePasswordSuccess extends LoginState { + @override + String toString() => 'UpdatePasswordSuccess'; +} + +class UpdatePasswordError extends LoginState { + final String? message; + final int? statusCode; + + const UpdatePasswordError( + {@required this.message, @required this.statusCode}); + + @override + String toString() => 'UpdatePasswordError { error: $message }'; +} + +class BindMobileInProgress extends LoginState { + @override + String toString() => 'BindMobileInProgress'; +} + +class BindMobileSuccess extends LoginState { + final JsonResult? jsonResult; + BindMobileSuccess({@required this.jsonResult}) : super(); + @override + String toString() => 'BindMobileSuccess'; +} + +class BindMobileError extends LoginState { + final String? message; + final int? statusCode; + const BindMobileError({@required this.message, @required this.statusCode}); + @override + String toString() => 'BindMobileError { error: $message }'; +} + +class UnionidOAuthInProgress extends LoginState { + @override + String toString() => 'UnionidOAuthInProgress'; +} diff --git a/bytedesk_kefu/lib/blocs/message_bloc/bloc.dart b/bytedesk_kefu/lib/blocs/message_bloc/bloc.dart new file mode 100755 index 0000000..88484d8 --- /dev/null +++ b/bytedesk_kefu/lib/blocs/message_bloc/bloc.dart @@ -0,0 +1,3 @@ +export 'message_bloc.dart'; +export 'message_event.dart'; +export 'message_state.dart'; diff --git a/bytedesk_kefu/lib/blocs/message_bloc/message_bloc.dart b/bytedesk_kefu/lib/blocs/message_bloc/message_bloc.dart new file mode 100755 index 0000000..890d396 --- /dev/null +++ b/bytedesk_kefu/lib/blocs/message_bloc/message_bloc.dart @@ -0,0 +1,164 @@ +import 'dart:async'; +import 'package:bytedesk_kefu/model/jsonResult.dart'; +import 'package:bytedesk_kefu/model/message.dart'; +import 'package:bytedesk_kefu/model/requestAnswer.dart'; +import 'package:bytedesk_kefu/model/uploadJsonResult.dart'; +import 'package:bytedesk_kefu/repositories/message_repository.dart'; +import 'package:bloc/bloc.dart'; +import './bloc.dart'; + +class MessageBloc extends Bloc { + // + final MessageRepository messageRepository = new MessageRepository(); + + MessageBloc() : super(InitialMessageState()); + + @override + Stream mapEventToState(MessageEvent event) async* { + if (event is ReceiveMessageEvent) { + yield* _mapRefreshCourseToState(event); + } else if (event is UploadImageEvent) { + yield* _mapUploadImageToState(event); + } else if (event is UploadVideoEvent) { + yield* _mapUploadVideoToState(event); + } else if (event is QueryAnswerEvent) { + yield* _mapQueryAnswerToState(event); + } else if (event is MessageAnswerEvent) { + yield* _mapMessageAnswerToState(event); + } else if (event is RateAnswerEvent) { + yield* _mapRateAnswerToState(event); + } else if (event is LoadHistoryMessageEvent) { + yield* _mapLoadHistoryMessageToState(event); + } else if (event is LoadTopicMessageEvent) { + yield* _mapLoadTopicMessageToState(event); + } else if (event is LoadChannelMessageEvent) { + yield* _mapLoadChannelMessageToState(event); + } else if (event is SendMessageRestEvent) { + yield* _mapSendMessageRestToState(event); + } + } + + Stream _mapRefreshCourseToState( + ReceiveMessageEvent event) async* { + try { + yield ReceiveMessageState(message: event.message); + } catch (error) { + print(error); + } + } + + Stream _mapUploadImageToState(UploadImageEvent event) async* { + yield MessageUpLoading(); + try { + final UploadJsonResult uploadJsonResult = + await messageRepository.uploadImage(event.filePath); + yield UploadImageSuccess(uploadJsonResult); + } catch (error) { + print(error); + yield UpLoadImageError(); + } + } + + Stream _mapUploadVideoToState(UploadVideoEvent event) async* { + yield MessageUpLoading(); + try { + final UploadJsonResult uploadJsonResult = + await messageRepository.uploadVideo(event.filePath); + yield UploadVideoSuccess(uploadJsonResult); + } catch (error) { + print(error); + yield UpLoadImageError(); + } + } + + Stream _mapSendMessageRestToState( + SendMessageRestEvent event) async* { + yield RestMessageSending(); + try { + final JsonResult jsonResult = + await messageRepository.sendMessageRest(event.json); + yield SendMessageRestSuccess(jsonResult); + } catch (error) { + print(error); + yield SendMessageRestError(); + } + } + + Stream _mapLoadHistoryMessageToState( + LoadHistoryMessageEvent event) async* { + yield MessageLoading(); + try { + final List messageList = await messageRepository + .loadHistoryMessages(event.uid, event.page, event.size); + yield LoadHistoryMessageSuccess(messageList: messageList); + } catch (error) { + print(error); + yield LoadHistoryMessageError(); + } + } + + Stream _mapLoadTopicMessageToState( + LoadTopicMessageEvent event) async* { + yield MessageLoading(); + try { + final List messageList = await messageRepository + .loadTopicMessages(event.topic, event.page, event.size); + yield LoadTopicMessageSuccess(messageList: messageList); + } catch (error) { + print(error); + yield LoadTopicMessageError(); + } + } + + Stream _mapLoadChannelMessageToState( + LoadChannelMessageEvent event) async* { + yield MessageLoading(); + try { + final List messageList = await messageRepository + .loadChannelMessages(event.cid, event.page, event.size); + yield LoadChannelMessageSuccess(messageList: messageList); + } catch (error) { + print(error); + yield LoadChannelMessageError(); + } + } + + Stream _mapQueryAnswerToState(QueryAnswerEvent event) async* { + yield MessageLoading(); + try { + final RequestAnswerResult requestAnswerResult = + await messageRepository.queryAnswer(event.tid, event.aid); + yield QueryAnswerSuccess( + query: requestAnswerResult.query, answer: requestAnswerResult.anwser); + } catch (error) { + print(error); + yield UpLoadImageError(); + } + } + + Stream _mapMessageAnswerToState( + MessageAnswerEvent event) async* { + yield MessageLoading(); + try { + final RequestAnswerResult requestAnswerResult = await messageRepository + .messageAnswer(event.type, event.wid, event.aid, event.content); + yield MessageAnswerSuccess( + query: requestAnswerResult.query, answer: requestAnswerResult.anwser); + } catch (error) { + print(error); + yield UpLoadImageError(); + } + } + + Stream _mapRateAnswerToState(RateAnswerEvent event) async* { + yield MessageLoading(); + try { + final RequestAnswerResult requestAnswerResult = + await messageRepository.rateAnswer(event.aid, event.mid, event.rate); + yield RateAnswerSuccess(result: requestAnswerResult.anwser); + } catch (error) { + print(error); + yield UpLoadImageError(); + } + } +} diff --git a/bytedesk_kefu/lib/blocs/message_bloc/message_event.dart b/bytedesk_kefu/lib/blocs/message_bloc/message_event.dart new file mode 100755 index 0000000..e4cec48 --- /dev/null +++ b/bytedesk_kefu/lib/blocs/message_bloc/message_event.dart @@ -0,0 +1,95 @@ +import 'package:bytedesk_kefu/model/message.dart'; +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; + +@immutable +abstract class MessageEvent extends Equatable { + const MessageEvent(); + + @override + List get props => []; +} + +class ReceiveMessageEvent extends MessageEvent { + final Message? message; + + ReceiveMessageEvent({@required this.message}) : super(); +} + +class UploadImageEvent extends MessageEvent { + final String? filePath; + + UploadImageEvent({@required this.filePath}) : super(); +} + +class UploadVideoEvent extends MessageEvent { + final String? filePath; + + UploadVideoEvent({@required this.filePath}) : super(); +} + +class SendMessageRestEvent extends MessageEvent { + final String? json; + + SendMessageRestEvent({@required this.json}) : super(); +} + +class LoadHistoryMessageEvent extends MessageEvent { + final String? uid; + final int? page; + final int? size; + + LoadHistoryMessageEvent( + {@required this.uid, @required this.page, @required this.size}) + : super(); +} + +class LoadTopicMessageEvent extends MessageEvent { + final String? topic; + final int? page; + final int? size; + + LoadTopicMessageEvent( + {@required this.topic, @required this.page, @required this.size}) + : super(); +} + +class LoadChannelMessageEvent extends MessageEvent { + final String? cid; + final int? page; + final int? size; + + LoadChannelMessageEvent( + {@required this.cid, @required this.page, @required this.size}) + : super(); +} + +class QueryAnswerEvent extends MessageEvent { + final String? tid; + final String? aid; + + QueryAnswerEvent({@required this.tid, @required this.aid}) : super(); +} + +class MessageAnswerEvent extends MessageEvent { + final String? type; + final String? wid; + final String? aid; + final String? content; + + MessageAnswerEvent( + {@required this.type, + @required this.wid, + @required this.aid, + @required this.content}) + : super(); +} + +class RateAnswerEvent extends MessageEvent { + final String? aid; + final String? mid; + final bool? rate; + + RateAnswerEvent({@required this.aid, @required this.mid, @required this.rate}) + : super(); +} diff --git a/bytedesk_kefu/lib/blocs/message_bloc/message_state.dart b/bytedesk_kefu/lib/blocs/message_bloc/message_state.dart new file mode 100755 index 0000000..0a2d1f5 --- /dev/null +++ b/bytedesk_kefu/lib/blocs/message_bloc/message_state.dart @@ -0,0 +1,140 @@ +import 'package:bytedesk_kefu/model/jsonResult.dart'; +import 'package:bytedesk_kefu/model/message.dart'; +import 'package:bytedesk_kefu/model/uploadJsonResult.dart'; +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; + +@immutable +abstract class MessageState extends Equatable { + const MessageState(); + + @override + List get props => []; +} + +class InitialMessageState extends MessageState {} + +class MessageLoading extends MessageState { + @override + String toString() => 'MessageLoading'; +} + +class MessageUpLoading extends MessageState { + @override + String toString() => 'MessageUpLoading'; +} + +class RestMessageSending extends MessageState { + @override + String toString() => 'RestMessageSending'; +} + +class ReceiveMessageState extends MessageState { + final Message? message; + + ReceiveMessageState({@required this.message}) : super(); +} + +class SendMessageRestSuccess extends MessageState { + final JsonResult jsonResult; + + const SendMessageRestSuccess(this.jsonResult); + + @override + List get props => [jsonResult]; + + @override + String toString() => 'SendMessageRestSuccess'; +} + +class UploadImageSuccess extends MessageState { + final UploadJsonResult uploadJsonResult; + + const UploadImageSuccess(this.uploadJsonResult); + + @override + List get props => [uploadJsonResult]; + + @override + String toString() => 'UploadImageSuccess { logo: ${uploadJsonResult.url} }'; +} + +class UpLoadImageError extends MessageState { + @override + String toString() => 'UpLoadImageError'; +} + +class SendMessageRestError extends MessageState { + @override + String toString() => 'SendMessageRestError'; +} + +class LoadHistoryMessageError extends MessageState { + @override + String toString() => 'LoadHistoryMessageError'; +} + +class LoadTopicMessageError extends MessageState { + @override + String toString() => 'LoadTopicMessageError'; +} + +class LoadChannelMessageError extends MessageState { + @override + String toString() => 'LoadChannelMessageError'; +} + +class UploadVideoSuccess extends MessageState { + final UploadJsonResult uploadJsonResult; + + const UploadVideoSuccess(this.uploadJsonResult); + + @override + List get props => [uploadJsonResult]; + + @override + String toString() => 'UploadVideoSuccess { logo: ${uploadJsonResult.url} }'; +} + +class UpLoadVideoError extends MessageState { + @override + String toString() => 'UpLoadVideoError'; +} + +class LoadHistoryMessageSuccess extends MessageState { + final List? messageList; + + LoadHistoryMessageSuccess({@required this.messageList}) : super(); +} + +class LoadTopicMessageSuccess extends MessageState { + final List? messageList; + + LoadTopicMessageSuccess({@required this.messageList}) : super(); +} + +class LoadChannelMessageSuccess extends MessageState { + final List? messageList; + + LoadChannelMessageSuccess({@required this.messageList}) : super(); +} + +class QueryAnswerSuccess extends MessageState { + final Message? query; + final Message? answer; + + QueryAnswerSuccess({@required this.query, @required this.answer}) : super(); +} + +class MessageAnswerSuccess extends MessageState { + final Message? query; + final Message? answer; + + MessageAnswerSuccess({@required this.query, @required this.answer}) : super(); +} + +class RateAnswerSuccess extends MessageState { + final Message? result; + + RateAnswerSuccess({@required this.result}) : super(); +} diff --git a/bytedesk_kefu/lib/blocs/profile_bloc/bloc.dart b/bytedesk_kefu/lib/blocs/profile_bloc/bloc.dart new file mode 100755 index 0000000..a642f12 --- /dev/null +++ b/bytedesk_kefu/lib/blocs/profile_bloc/bloc.dart @@ -0,0 +1,3 @@ +export 'profile_bloc.dart'; +export 'profile_event.dart'; +export 'profile_state.dart'; diff --git a/bytedesk_kefu/lib/blocs/profile_bloc/profile_bloc.dart b/bytedesk_kefu/lib/blocs/profile_bloc/profile_bloc.dart new file mode 100755 index 0000000..a1015ce --- /dev/null +++ b/bytedesk_kefu/lib/blocs/profile_bloc/profile_bloc.dart @@ -0,0 +1,180 @@ +import 'dart:async'; +import 'package:bytedesk_kefu/model/jsonResult.dart'; +import 'package:bytedesk_kefu/model/user.dart'; +import 'package:bytedesk_kefu/repositories/user_repository.dart'; +import 'package:bloc/bloc.dart'; +import './bloc.dart'; + +class ProfileBloc extends Bloc { + // + final UserRepository userRepository = new UserRepository(); + + ProfileBloc() : super(InitialProfileState()); + + @override + Stream mapEventToState(ProfileEvent event) async* { + // + if (event is GetProfileEvent) { + yield* _mapProfileState(); + } else if (event is UploadImageEvent) { + yield* _mapUploadImageToState(event); + } else if (event is UpdateAvatarEvent) { + yield* _mapUpdateAvatarToState(event); + } else if (event is UpdateNicknameEvent) { + yield* _mapUpdateNicknameToState(event); + } else if (event is UpdateDescriptionEvent) { + yield* _mapUpdateDescriptionToState(event); + } else if (event is UpdateMobileEvent) { + yield* _mapUpdateMobileToState(event); + } else if (event is UpdateSexEvent) { + yield* _mapUpdateSexToState(event); + } else if (event is UpdateLocationEvent) { + yield* _mapUpdateLocationToState(event); + } else if (event is UpdateBirthdayEvent) { + yield* _mapUpdateBirthdayToState(event); + } else if (event is QueryFollowEvent) { + yield* _mapQueryFollowToState(event); + } else if (event is UserFollowEvent) { + yield* _mapUserFollowToState(event); + } else if (event is UserUnfollowEvent) { + yield* _mapUserUnfollowToState(event); + } + } + + Stream _mapProfileState() async* { + yield ProfileInProgress(); + try { + User user = await userRepository.getProfile(); + yield ProfileSuccess(user: user); + } catch (error) { + // 网络或其他错误 + yield ProfileFailure(error: error.toString()); + } + } + + Stream _mapUploadImageToState(UploadImageEvent event) async* { + yield ProfileInProgress(); + try { + final String url = await userRepository.upload(event.filePath); + yield UploadImageSuccess(url); + } catch (error) { + print(error); + yield UpLoadImageError(); + } + } + + Stream _mapUpdateAvatarToState(UpdateAvatarEvent event) async* { + yield ProfileInProgress(); + try { + final User user = await userRepository.updateAvatar(event.avatar); + yield UpdateAvatarSuccess(user); + } catch (error) { + print(error); + yield UpLoadImageError(); + } + } + + Stream _mapUpdateNicknameToState( + UpdateNicknameEvent event) async* { + yield ProfileInProgress(); + try { + final User user = await userRepository.updateNickname(event.nickname); + yield UpdateNicknameSuccess(user); + } catch (error) { + print(error); + yield UpLoadImageError(); + } + } + + Stream _mapUpdateDescriptionToState( + UpdateDescriptionEvent event) async* { + yield ProfileInProgress(); + try { + final User user = + await userRepository.updateDescription(event.description); + yield UpdateDescriptionSuccess(user); + } catch (error) { + print(error); + yield UpLoadImageError(); + } + } + + Stream _mapUpdateMobileToState(UpdateMobileEvent event) async* { + yield ProfileInProgress(); + try { + final User user = await userRepository.updateMobile(event.mobile); + yield UpdateMobileSuccess(user); + } catch (error) { + print(error); + yield UpLoadImageError(); + } + } + + Stream _mapUpdateSexToState(UpdateSexEvent event) async* { + yield ProfileInProgress(); + try { + final User user = await userRepository.updateSex(event.sex); + yield UpdateSexSuccess(user); + } catch (error) { + print(error); + yield UpLoadImageError(); + } + } + + Stream _mapUpdateLocationToState( + UpdateLocationEvent event) async* { + yield ProfileInProgress(); + try { + final User user = await userRepository.updateLocation(event.location); + yield UpdateLocationSuccess(user); + } catch (error) { + print(error); + yield UpLoadImageError(); + } + } + + Stream _mapUpdateBirthdayToState( + UpdateBirthdayEvent event) async* { + yield ProfileInProgress(); + try { + final User user = await userRepository.updateBirthday(event.birthday); + yield UpdateBirthdaySuccess(user); + } catch (error) { + print(error); + yield UpLoadImageError(); + } + } + + Stream _mapQueryFollowToState(QueryFollowEvent event) async* { + yield QueryFollowing(); + try { + final bool isFollowed = await userRepository.isFollowed(event.uid); + yield QueryFollowSuccess(isFollowed); + } catch (error) { + print(error); + yield QueryFollowError(); + } + } + + Stream _mapUserFollowToState(UserFollowEvent event) async* { + yield Following(); + try { + final JsonResult jsonResult = await userRepository.follow(event.uid); + yield FollowResultSuccess(jsonResult); + } catch (error) { + print(error); + yield FollowError(); + } + } + + Stream _mapUserUnfollowToState(UserUnfollowEvent event) async* { + yield Following(); + try { + final JsonResult jsonResult = await userRepository.unfollow(event.uid); + yield UnfollowResultSuccess(jsonResult); + } catch (error) { + print(error); + yield UnFollowError(); + } + } +} diff --git a/bytedesk_kefu/lib/blocs/profile_bloc/profile_event.dart b/bytedesk_kefu/lib/blocs/profile_bloc/profile_event.dart new file mode 100755 index 0000000..bc730f1 --- /dev/null +++ b/bytedesk_kefu/lib/blocs/profile_bloc/profile_event.dart @@ -0,0 +1,91 @@ +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; + +@immutable +abstract class ProfileEvent extends Equatable { + const ProfileEvent(); + + @override + List get props => []; +} + +class GetProfileEvent extends ProfileEvent {} + +class UpdateProfileEvent extends ProfileEvent {} + +// 上传头像 +class UploadImageEvent extends ProfileEvent { + final String? filePath; + + UploadImageEvent({@required this.filePath}) : super(); +} + +// 更新头像 +class UpdateAvatarEvent extends ProfileEvent { + final String? avatar; + + UpdateAvatarEvent({@required this.avatar}) : super(); +} + +// 更新昵称 +class UpdateNicknameEvent extends ProfileEvent { + final String? nickname; + + UpdateNicknameEvent({@required this.nickname}) : super(); +} + +// 更新个性签名 +class UpdateDescriptionEvent extends ProfileEvent { + final String? description; + + UpdateDescriptionEvent({@required this.description}) : super(); +} + +// 更新手机号 +class UpdateMobileEvent extends ProfileEvent { + final String? mobile; + + UpdateMobileEvent({@required this.mobile}) : super(); +} + +// 更新性别 +class UpdateSexEvent extends ProfileEvent { + final bool? sex; + + UpdateSexEvent({@required this.sex}) : super(); +} + +// 更新地区 +class UpdateLocationEvent extends ProfileEvent { + final String? location; + + UpdateLocationEvent({@required this.location}) : super(); +} + +// 更新生日 +class UpdateBirthdayEvent extends ProfileEvent { + final String? birthday; + + UpdateBirthdayEvent({@required this.birthday}) : super(); +} + +// 查询是否关注 +class QueryFollowEvent extends ProfileEvent { + final String? uid; + + QueryFollowEvent({@required this.uid}) : super(); +} + +// 关注用户 +class UserFollowEvent extends ProfileEvent { + final String? uid; + + UserFollowEvent({@required this.uid}) : super(); +} + +// 取消关注用户 +class UserUnfollowEvent extends ProfileEvent { + final String? uid; + + UserUnfollowEvent({@required this.uid}) : super(); +} diff --git a/bytedesk_kefu/lib/blocs/profile_bloc/profile_state.dart b/bytedesk_kefu/lib/blocs/profile_bloc/profile_state.dart new file mode 100755 index 0000000..8eadb08 --- /dev/null +++ b/bytedesk_kefu/lib/blocs/profile_bloc/profile_state.dart @@ -0,0 +1,182 @@ +import 'package:bytedesk_kefu/model/jsonResult.dart'; +import 'package:bytedesk_kefu/model/user.dart'; +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; + +@immutable +abstract class ProfileState extends Equatable { + // ProfileState([List props = const []]) : super(props); + const ProfileState(); + + @override + List get props => []; +} + +class InitialProfileState extends ProfileState {} + +class ProfileInProgress extends ProfileState { + @override + String toString() => 'ProfileInProgress'; +} + +class ProfileSuccess extends ProfileState { + final User? user; + + ProfileSuccess({@required this.user}); + + @override + String toString() => 'ProfileSuccess'; +} + +class ProfileError extends ProfileState { + @override + String toString() => 'ProfileError'; +} + +class ProfileFailure extends ProfileState { + final String? error; + + const ProfileFailure({@required this.error}); + + @override + String toString() => 'ProfileFailure { error: $error }'; +} + +class UploadImageSuccess extends ProfileState { + final String? url; + + const UploadImageSuccess(this.url); + + @override + String toString() => 'UploadImageSuccess { logo: $url }'; +} + +class UpLoadImageError extends ProfileState { + @override + String toString() => 'UpLoadImageError'; +} + +class UpdateAvatarSuccess extends ProfileState { + final User user; + + const UpdateAvatarSuccess(this.user); + + @override + String toString() => 'UpdateAvatarSuccess'; +} + +class UpdateNicknameSuccess extends ProfileState { + final User user; + + const UpdateNicknameSuccess(this.user); + + @override + String toString() => 'UpdateNicknameSuccess'; +} + +class UpdateDescriptionSuccess extends ProfileState { + final User user; + + const UpdateDescriptionSuccess(this.user); + + @override + String toString() => 'UpdateDescriptionSuccess'; +} + +class UpdateMobileSuccess extends ProfileState { + final User user; + + const UpdateMobileSuccess(this.user); + + @override + String toString() => 'UpdateMobileSuccess'; +} + +class UpdateSexSuccess extends ProfileState { + final User user; + + const UpdateSexSuccess(this.user); + + @override + String toString() => 'UpdateSexSuccess'; +} + +class UpdateLocationSuccess extends ProfileState { + final User user; + + const UpdateLocationSuccess(this.user); + + @override + String toString() => 'UpdateLocationSuccess'; +} + +class UpdateBirthdaySuccess extends ProfileState { + final User user; + + const UpdateBirthdaySuccess(this.user); + + @override + String toString() => 'UpdateBirthdaySuccess'; +} + +class UpdateError extends ProfileState { + @override + String toString() => 'UpdateError'; +} + +class QueryFollowing extends ProfileState { + @override + String toString() => 'QueryFollowing'; +} + +class QueryFollowSuccess extends ProfileState { + final bool isFollowed; + + const QueryFollowSuccess(this.isFollowed); + + @override + String toString() => 'QueryFollowSuccess'; +} + +class QueryFollowError extends ProfileState { + @override + String toString() => 'QueryFollowError'; +} + +class Following extends ProfileState { + @override + String toString() => 'Following'; +} + +class FollowResultSuccess extends ProfileState { + final JsonResult jsonResult; + + const FollowResultSuccess(this.jsonResult); + + @override + String toString() => 'FollowResultSuccess'; +} + +class FollowError extends ProfileState { + @override + String toString() => 'FollowError'; +} + +class Unfollowing extends ProfileState { + @override + String toString() => 'Unfollowing'; +} + +class UnfollowResultSuccess extends ProfileState { + final JsonResult jsonResult; + + const UnfollowResultSuccess(this.jsonResult); + + @override + String toString() => 'UnfollowResultSuccess'; +} + +class UnFollowError extends ProfileState { + @override + String toString() => 'UnFollowError'; +} diff --git a/bytedesk_kefu/lib/blocs/thread_bloc/bloc.dart b/bytedesk_kefu/lib/blocs/thread_bloc/bloc.dart new file mode 100755 index 0000000..9ed3536 --- /dev/null +++ b/bytedesk_kefu/lib/blocs/thread_bloc/bloc.dart @@ -0,0 +1,3 @@ +export 'thread_bloc.dart'; +export 'thread_event.dart'; +export 'thread_state.dart'; diff --git a/bytedesk_kefu/lib/blocs/thread_bloc/thread_bloc.dart b/bytedesk_kefu/lib/blocs/thread_bloc/thread_bloc.dart new file mode 100755 index 0000000..4342704 --- /dev/null +++ b/bytedesk_kefu/lib/blocs/thread_bloc/thread_bloc.dart @@ -0,0 +1,242 @@ +import 'dart:async'; +import 'package:bytedesk_kefu/model/markThread.dart'; +import 'package:bloc/bloc.dart'; +import './bloc.dart'; +import 'package:bytedesk_kefu/repositories/repositories.dart'; +import 'package:bytedesk_kefu/model/model.dart'; + +class ThreadBloc extends Bloc { + // + final ThreadRepository threadRepository = new ThreadRepository(); + + ThreadBloc() : super(ThreadEmpty()); + + @override + Stream mapEventToState(ThreadEvent event) async* { + // + if (event is InitThreadEvent) { + yield InitialThreadState(); + } else if (event is RefreshThreadEvent) { + yield* _mapRefreshThreadToState(event); + } else if (event is RefreshHistoryThreadEvent) { + yield* _mapRefreshHistoryThreadToState(event); + } else if (event is RefreshVisitorThreadEvent) { + yield* _mapRefreshVisitorThreadToState(event); + } else if (event is RefreshVisitorThreadAllEvent) { + yield* _mapRefreshVisitorThreadAllToState(event); + } else if (event is RequestThreadEvent) { + yield* _mapRequestThreadToState(event); + } else if (event is RequestAgentEvent) { + yield* _mapRequestAgentToState(event); + } else if (event is RequestContactThreadEvent) { + yield* _mapRequestContactThreadToState(event); + } else if (event is RequestGroupThreadEvent) { + yield* _mapRequestGroupThreadToState(event); + } else if (event is MarkTopThreadEvent) { + yield* _mapMarkTopThreadEventToState(event); + } else if (event is UnMarkTopThreadEvent) { + yield* _mapUnMarkTopThreadEventToState(event); + } else if (event is MarkNodisturbThreadEvent) { + yield* _mapMarkNodisturbThreadEventToState(event); + } else if (event is UnMarkNodisturbThreadEvent) { + yield* _mapUnMarkNodisturbThreadEventToState(event); + } else if (event is MarkUnreadThreadEvent) { + yield* _mapMarkUnreadThreadEventToState(event); + } else if (event is UnMarkUnreadThreadEvent) { + yield* _mapUnMarkUnreadThreadEventToState(event); + } else if (event is DeleteThreadEvent) { + yield* _mapDeleteThreadEventToState(event); + } + } + + Stream _mapRefreshThreadToState( + RefreshThreadEvent event) async* { + yield ThreadLoading(); + try { + final List threadList = await threadRepository.getThreads(); + yield ThreadLoadSuccess(threadList); + } catch (error) { + print(error); + yield ThreadLoadError(); + } + } + + Stream _mapRefreshHistoryThreadToState( + RefreshHistoryThreadEvent event) async* { + yield ThreadHistoryLoading(); + try { + final List threadList = + await threadRepository.getHistoryThreads(event.page, event.size); + yield ThreadLoadSuccess(threadList); + } catch (error) { + print(error); + yield ThreadLoadError(); + } + } + + Stream _mapRefreshVisitorThreadToState( + RefreshVisitorThreadEvent event) async* { + yield ThreadVisitorLoading(); + try { + final List threadList = + await threadRepository.getVisitorThreads(event.page, event.size); + yield ThreadLoadSuccess(threadList); + } catch (error) { + print(error); + yield ThreadLoadError(); + } + } + + Stream _mapRefreshVisitorThreadAllToState( + RefreshVisitorThreadAllEvent event) async* { + yield ThreadLoading(); + try { + final List threadList = + await threadRepository.getVisitorThreadsAll(); + yield ThreadLoadSuccess(threadList); + } catch (error) { + print(error); + yield ThreadLoadError(); + } + } + + Stream _mapRequestThreadToState( + RequestThreadEvent event) async* { + yield RequestThreading(); + try { + final RequestThreadResult thread = await threadRepository.requestThread( + event.wid, event.type, event.aid); + yield RequestThreadSuccess(thread); + } catch (error) { + print(error); + yield RequestThreadError(); + } + } + + Stream _mapRequestAgentToState(RequestAgentEvent event) async* { + yield RequestAgentThreading(); + try { + final RequestThreadResult thread = + await threadRepository.requestAgent(event.wid, event.type, event.aid); + yield RequestAgentSuccess(thread); + } catch (error) { + print(error); + yield RequestAgentThreadError(); + } + } + + Stream _mapRequestContactThreadToState( + RequestContactThreadEvent event) async* { + yield ThreadLoading(); + try { + final RequestThreadResult thread = + await threadRepository.requestContactThread(event.cid); + yield RequestContactThreadSuccess(thread); + } catch (error) { + print(error); + yield ThreadLoadError(); + } + } + + Stream _mapRequestGroupThreadToState( + RequestGroupThreadEvent event) async* { + yield ThreadLoading(); + try { + final RequestThreadResult thread = + await threadRepository.requestGroupThread(event.gid); + yield RequestGroupThreadSuccess(thread); + } catch (error) { + print(error); + yield ThreadLoadError(); + } + } + + Stream _mapMarkTopThreadEventToState( + MarkTopThreadEvent event) async* { + yield ThreadLoading(); + try { + final MarkThreadResult thread = await threadRepository.markTop(event.tid); + yield MarkTopThreadSuccess(thread); + } catch (error) { + print(error); + yield ThreadLoadError(); + } + } + + Stream _mapUnMarkTopThreadEventToState( + UnMarkTopThreadEvent event) async* { + yield ThreadLoading(); + try { + final MarkThreadResult thread = + await threadRepository.unmarkTop(event.tid); + yield UnMarkTopThreadSuccess(thread); + } catch (error) { + print(error); + yield ThreadLoadError(); + } + } + + Stream _mapMarkNodisturbThreadEventToState( + MarkNodisturbThreadEvent event) async* { + yield ThreadLoading(); + try { + final MarkThreadResult thread = + await threadRepository.markNodisturb(event.tid); + yield MarkNodisturbThreadSuccess(thread); + } catch (error) { + print(error); + yield ThreadLoadError(); + } + } + + Stream _mapUnMarkNodisturbThreadEventToState( + UnMarkNodisturbThreadEvent event) async* { + yield ThreadLoading(); + try { + final MarkThreadResult thread = + await threadRepository.unmarkNodisturb(event.tid); + yield UnMarkNodisturbThreadSuccess(thread); + } catch (error) { + print(error); + yield ThreadLoadError(); + } + } + + Stream _mapMarkUnreadThreadEventToState( + MarkUnreadThreadEvent event) async* { + yield ThreadLoading(); + try { + final MarkThreadResult thread = + await threadRepository.markUnread(event.tid); + yield MarkUnreadThreadSuccess(thread); + } catch (error) { + print(error); + yield ThreadLoadError(); + } + } + + Stream _mapUnMarkUnreadThreadEventToState( + UnMarkUnreadThreadEvent event) async* { + yield ThreadLoading(); + try { + final MarkThreadResult thread = + await threadRepository.unmarkUnread(event.tid); + yield UnMarkUnreadThreadSuccess(thread); + } catch (error) { + print(error); + yield ThreadLoadError(); + } + } + + Stream _mapDeleteThreadEventToState( + DeleteThreadEvent event) async* { + yield ThreadLoading(); + try { + final MarkThreadResult thread = await threadRepository.delete(event.tid); + yield DeleteThreadSuccess(thread); + } catch (error) { + print(error); + yield ThreadLoadError(); + } + } +} diff --git a/bytedesk_kefu/lib/blocs/thread_bloc/thread_event.dart b/bytedesk_kefu/lib/blocs/thread_bloc/thread_event.dart new file mode 100755 index 0000000..283f157 --- /dev/null +++ b/bytedesk_kefu/lib/blocs/thread_bloc/thread_event.dart @@ -0,0 +1,121 @@ +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; + +@immutable +abstract class ThreadEvent extends Equatable { + const ThreadEvent(); + + @override + List get props => []; +} + +class InitThreadEvent extends ThreadEvent {} + +class RefreshThreadEvent extends ThreadEvent {} + +class RefreshHistoryThreadEvent extends ThreadEvent { + final int? page; + final int? size; + // + RefreshHistoryThreadEvent({@required this.page, @required this.size}) + : super(); +} + +class RefreshVisitorThreadEvent extends ThreadEvent { + final int? page; + final int? size; + // + RefreshVisitorThreadEvent({@required this.page, @required this.size}) + : super(); +} + +class RefreshVisitorThreadAllEvent extends ThreadEvent { + // + RefreshVisitorThreadAllEvent() : super(); +} + +class UpdateThreadEvent extends ThreadEvent { + final String? tid; + + UpdateThreadEvent({@required this.tid}) + : assert(tid != null), + super(); +} + +class DeleteThreadEvent extends ThreadEvent { + final String? tid; + + DeleteThreadEvent({@required this.tid}) + : assert(tid != null), + super(); +} + +// 请求客服会话 +class RequestThreadEvent extends ThreadEvent { + final String? wid; + final String? type; + final String? aid; + + RequestThreadEvent( + {@required this.wid, @required this.type, @required this.aid}) + : super(); +} + +// 请求人工客服,不管此工作组是否设置为默认机器人,只要有人工客服在线,则可以直接对接人工 +class RequestAgentEvent extends ThreadEvent { + final String? wid; + final String? type; + final String? aid; + + RequestAgentEvent( + {@required this.wid, @required this.type, @required this.aid}) + : super(); +} + +class RequestContactThreadEvent extends ThreadEvent { + final String? cid; + + RequestContactThreadEvent({@required this.cid}) : super(); +} + +class RequestGroupThreadEvent extends ThreadEvent { + final String? gid; + + RequestGroupThreadEvent({@required this.gid}) : super(); +} + +class MarkTopThreadEvent extends ThreadEvent { + final String? tid; + + MarkTopThreadEvent({@required this.tid}) : super(); +} + +class UnMarkTopThreadEvent extends ThreadEvent { + final String? tid; + + UnMarkTopThreadEvent({@required this.tid}) : super(); +} + +class MarkNodisturbThreadEvent extends ThreadEvent { + final String? tid; + + MarkNodisturbThreadEvent({@required this.tid}) : super(); +} + +class UnMarkNodisturbThreadEvent extends ThreadEvent { + final String? tid; + + UnMarkNodisturbThreadEvent({@required this.tid}) : super(); +} + +class MarkUnreadThreadEvent extends ThreadEvent { + final String? tid; + + MarkUnreadThreadEvent({@required this.tid}) : super(); +} + +class UnMarkUnreadThreadEvent extends ThreadEvent { + final String? tid; + + UnMarkUnreadThreadEvent({@required this.tid}) : super(); +} diff --git a/bytedesk_kefu/lib/blocs/thread_bloc/thread_state.dart b/bytedesk_kefu/lib/blocs/thread_bloc/thread_state.dart new file mode 100755 index 0000000..8749a3c --- /dev/null +++ b/bytedesk_kefu/lib/blocs/thread_bloc/thread_state.dart @@ -0,0 +1,220 @@ +import 'package:bytedesk_kefu/model/markThread.dart'; +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; +import 'package:bytedesk_kefu/model/model.dart'; + +@immutable +abstract class ThreadState extends Equatable { + // ThreadState([List props = const []]) : super(props); + const ThreadState(); + + @override + List get props => []; +} + +class ThreadUninitialized extends ThreadState { + @override + String toString() => 'ThreadUninitialized'; +} + +class InitialThreadState extends ThreadState { + @override + List get props => []; + + @override + String toString() => 'InitialThreadState'; +} + +class ThreadEmpty extends ThreadState { + @override + String toString() => 'ThreadEmpty'; +} + +class ThreadLoading extends ThreadState { + @override + String toString() => 'ThreadLoading'; +} + +class ThreadVisitorLoading extends ThreadState { + @override + String toString() => 'ThreadVisitorLoading'; +} + +class ThreadHistoryLoading extends ThreadState { + @override + String toString() => 'ThreadHistoryLoading'; +} + +class ThreadLoadError extends ThreadState { + @override + String toString() => 'ThreadLoadError'; +} + +class ThreadLoadSuccess extends ThreadState { + final List? threadList; + + const ThreadLoadSuccess(this.threadList); + + ThreadLoadSuccess copyWith({List? threadList}) { + return ThreadLoadSuccess(threadList); + } + + @override + List get props => [threadList!]; + + @override + String toString() => + 'ThreadLoadSuccess { threadList: ${threadList!.length} }'; +} + +class RequestThreading extends ThreadState { + @override + String toString() => 'RequestThreading'; +} + +class RequestThreadSuccess extends ThreadState { + final RequestThreadResult threadResult; + + const RequestThreadSuccess(this.threadResult); + + @override + List get props => [threadResult]; + + @override + String toString() => 'RequestThreadSuccess'; +} + +class RequestThreadError extends ThreadState { + @override + String toString() => 'RequestThreadError'; +} + +class RequestAgentThreading extends ThreadState { + @override + String toString() => 'RequestAgentThreading'; +} + +class RequestAgentSuccess extends ThreadState { + final RequestThreadResult threadResult; + + const RequestAgentSuccess(this.threadResult); + + @override + List get props => [threadResult]; + + @override + String toString() => 'RequestAgentSuccess'; +} + +class RequestAgentThreadError extends ThreadState { + @override + String toString() => 'RequestAgentThreadError'; +} + +class RequestContactThreadSuccess extends ThreadState { + final RequestThreadResult threadResult; + + const RequestContactThreadSuccess(this.threadResult); + + @override + List get props => [threadResult]; + + @override + String toString() => 'RequestContactThreadSuccess'; +} + +class RequestGroupThreadSuccess extends ThreadState { + final RequestThreadResult threadResult; + + const RequestGroupThreadSuccess(this.threadResult); + + @override + List get props => [threadResult]; + + @override + String toString() => 'RequestGroupThreadSuccess'; +} + +class MarkTopThreadSuccess extends ThreadState { + final MarkThreadResult threadResult; + + const MarkTopThreadSuccess(this.threadResult); + + @override + List get props => [threadResult]; + + @override + String toString() => 'MarkTopThreadSuccess'; +} + +class UnMarkTopThreadSuccess extends ThreadState { + final MarkThreadResult threadResult; + + const UnMarkTopThreadSuccess(this.threadResult); + + @override + List get props => [threadResult]; + + @override + String toString() => 'UnMarkTopThreadSuccess'; +} + +class MarkUnreadThreadSuccess extends ThreadState { + final MarkThreadResult threadResult; + + const MarkUnreadThreadSuccess(this.threadResult); + + @override + List get props => [threadResult]; + + @override + String toString() => 'MarkUnreadThreadSuccess'; +} + +class UnMarkUnreadThreadSuccess extends ThreadState { + final MarkThreadResult threadResult; + + const UnMarkUnreadThreadSuccess(this.threadResult); + + @override + List get props => [threadResult]; + + @override + String toString() => 'UnMarkUnreadThreadSuccess'; +} + +class MarkNodisturbThreadSuccess extends ThreadState { + final MarkThreadResult threadResult; + + const MarkNodisturbThreadSuccess(this.threadResult); + + @override + List get props => [threadResult]; + + @override + String toString() => 'MarkNodisturbThreadSuccess'; +} + +class UnMarkNodisturbThreadSuccess extends ThreadState { + final MarkThreadResult threadResult; + + const UnMarkNodisturbThreadSuccess(this.threadResult); + + @override + List get props => [threadResult]; + + @override + String toString() => 'UnMarkNodisturbThreadSuccess'; +} + +class DeleteThreadSuccess extends ThreadState { + final MarkThreadResult threadResult; + + const DeleteThreadSuccess(this.threadResult); + + @override + List get props => [threadResult]; + + @override + String toString() => 'DeleteThreadSuccess'; +} diff --git a/bytedesk_kefu/lib/blocs/ticket_bloc/bloc.dart b/bytedesk_kefu/lib/blocs/ticket_bloc/bloc.dart new file mode 100755 index 0000000..087a909 --- /dev/null +++ b/bytedesk_kefu/lib/blocs/ticket_bloc/bloc.dart @@ -0,0 +1,3 @@ +export 'ticket_bloc.dart'; +export 'ticket_event.dart'; +export 'ticket_state.dart'; diff --git a/bytedesk_kefu/lib/blocs/ticket_bloc/ticket_bloc.dart b/bytedesk_kefu/lib/blocs/ticket_bloc/ticket_bloc.dart new file mode 100755 index 0000000..3e21f90 --- /dev/null +++ b/bytedesk_kefu/lib/blocs/ticket_bloc/ticket_bloc.dart @@ -0,0 +1,60 @@ +import 'dart:async'; +import 'package:bytedesk_kefu/blocs/ticket_bloc/bloc.dart'; +import 'package:bloc/bloc.dart'; +import 'package:bytedesk_kefu/repositories/ticket_repository.dart'; + +class TicketBloc extends Bloc { + // + final TicketRepository feedbackRepository = new TicketRepository(); + + TicketBloc() : super(new UnTicketState()); + + @override + Stream mapEventToState( + TicketEvent event, + ) async* { + if (event is GetTicketCategoryEvent) { + yield* _mapGetTicketCategoryToState(event); + } else if (event is SubmitTicketEvent) { + yield* _mapSubmitTicketToState(event); + } else if (event is UploadImageEvent) { + yield* _mapUploadImageToState(event); + } + } + + Stream _mapGetTicketCategoryToState( + GetTicketCategoryEvent event) async* { + yield TicketLoading(); + try { + // final List categoryList = + // await feedbackRepository.getHelpTicketCategories(); + // yield TicketCategoryState(categoryList); + } catch (error) { + print(error); + yield TicketLoadError(); + } + } + + Stream _mapSubmitTicketToState(SubmitTicketEvent event) async* { + yield TicketLoading(); + try { + // final List categoryList = + // await feedbackRepository.getHelpTicketCategories(); + // yield TicketCategoryState(categoryList); + } catch (error) { + print(error); + yield TicketLoadError(); + } + } + + Stream _mapUploadImageToState(UploadImageEvent event) async* { + yield TicketLoading(); + try { + final String url = await feedbackRepository.upload(event.filePath); + yield UploadImageSuccess(url); + } catch (error) { + print(error); + yield UpLoadImageError(); + } + } +} diff --git a/bytedesk_kefu/lib/blocs/ticket_bloc/ticket_event.dart b/bytedesk_kefu/lib/blocs/ticket_bloc/ticket_event.dart new file mode 100755 index 0000000..3d96752 --- /dev/null +++ b/bytedesk_kefu/lib/blocs/ticket_bloc/ticket_event.dart @@ -0,0 +1,26 @@ +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; + +@immutable +abstract class TicketEvent extends Equatable { + const TicketEvent(); + + @override + List get props => []; +} + +class GetTicketCategoryEvent extends TicketEvent {} + +class SubmitTicketEvent extends TicketEvent { + final List? imageUrls; + final String? content; + + SubmitTicketEvent({@required this.content, @required this.imageUrls}) + : super(); +} + +class UploadImageEvent extends TicketEvent { + final String? filePath; + + UploadImageEvent({@required this.filePath}) : super(); +} diff --git a/bytedesk_kefu/lib/blocs/ticket_bloc/ticket_state.dart b/bytedesk_kefu/lib/blocs/ticket_bloc/ticket_state.dart new file mode 100755 index 0000000..ee7b4ff --- /dev/null +++ b/bytedesk_kefu/lib/blocs/ticket_bloc/ticket_state.dart @@ -0,0 +1,57 @@ +import 'package:bytedesk_kefu/model/helpCategory.dart'; +import 'package:equatable/equatable.dart'; + +abstract class TicketState extends Equatable { + TicketState(); + + @override + List get props => []; +} + +/// UnInitialized +class UnTicketState extends TicketState { + UnTicketState(); + + @override + String toString() => 'UnTicketState'; +} + +class TicketEmpty extends TicketState { + @override + String toString() => 'TicketEmpty'; +} + +class TicketLoading extends TicketState { + @override + String toString() => 'TicketLoading'; +} + +class TicketLoadError extends TicketState { + @override + String toString() => 'TicketLoadError'; +} + +/// Initialized +class TicketCategoryState extends TicketState { + final List categoryList; + + TicketCategoryState(this.categoryList) : super(); + + @override + String toString() => 'GetTicketCategoryState'; +} + +class UploadImageSuccess extends TicketState { + // + final String url; + UploadImageSuccess(this.url); + @override + List get props => [url]; + @override + String toString() => 'UploadImageSuccess { logo: $url }'; +} + +class UpLoadImageError extends TicketState { + @override + String toString() => 'UpLoadImageError'; +} diff --git a/bytedesk_kefu/lib/bytedesk_kefu.dart b/bytedesk_kefu/lib/bytedesk_kefu.dart new file mode 100644 index 0000000..4103425 --- /dev/null +++ b/bytedesk_kefu/lib/bytedesk_kefu.dart @@ -0,0 +1,524 @@ +import 'dart:async'; +// import 'dart:io'; + +import 'package:bytedesk_kefu/http/bytedesk_device_api.dart'; +import 'package:bytedesk_kefu/http/bytedesk_thread_api.dart'; +import 'package:bytedesk_kefu/model/app.dart'; +import 'package:bytedesk_kefu/model/jsonResult.dart'; +import 'package:bytedesk_kefu/model/thread.dart'; +import 'package:bytedesk_kefu/model/userJsonResult.dart'; +import 'package:bytedesk_kefu/model/wechatResult.dart'; +import 'package:bytedesk_kefu/ui/channel/provider/channel_provider.dart'; +import 'package:bytedesk_kefu/ui/chat/page/chat_webview_page.dart'; +import 'package:bytedesk_kefu/ui/chat/provider/chat_im_provider.dart'; +import 'package:bytedesk_kefu/ui/chat/provider/chat_thread_provider.dart'; +import 'package:bytedesk_kefu/ui/faq/provider/help_provider.dart'; +import 'package:bytedesk_kefu/ui/feedback/provider/feedback_provider.dart'; +import 'package:bytedesk_kefu/ui/leavemsg/provider/leavemsg_provider.dart'; +import 'package:bytedesk_kefu/ui/ticket/provider/ticket_provider.dart'; +import 'package:flutter/services.dart'; + +import 'package:bytedesk_kefu/http/bytedesk_user_api.dart'; +import 'package:bytedesk_kefu/ui/chat/provider/chat_kf_provider.dart'; +import 'package:bytedesk_kefu/util/bytedesk_utils.dart'; +import 'package:bytedesk_kefu/util/bytedesk_constants.dart'; +import 'package:sp_util/sp_util.dart'; +import 'package:flutter/material.dart'; + +import 'model/user.dart'; + +class BytedeskKefu { + // + static const MethodChannel _channel = const MethodChannel('bytedesk_kefu'); + static Future get platformVersion async { + final String version = await _channel.invokeMethod('getPlatformVersion'); + return version; + } + + // 下面为 自定义接口 + static void init(String appKey, String subDomain) async { + anonymousLogin(appKey, subDomain); + } + + // 支持自定义用户名,方便跟APP业务系统对接 + static void initWithUsername( + String username, String appKey, String subDomain) async { + initWithUsernameAndNickname(username, "", appKey, subDomain); + } + + static void initWithUsernameAndNickname( + String username, String nickname, String appKey, String subDomain) async { + initWithUsernameAndNicknameAndAvatar( + username, nickname, "", appKey, subDomain); + } + + static void initWithUsernameAndNicknameAndAvatar(String username, + String nickname, String avatar, String appKey, String subDomain) async { + // anonymousLogin(appKey, subDomain); + /// sp初始化 + await SpUtil.getInstance(); + // 首先检测是否是第一次,如果是第一此启动 + String? spusername = SpUtil.getString(BytedeskConstants.username); + if (spusername!.isEmpty) { + // 第一此启动, 则调用注册接口,否则调用登录接口 + String password = username; + await BytedeskUserHttpApi() + .registerUser(username, nickname, password, avatar, subDomain); + username = username + "@" + subDomain; + userLogin(username, password, appKey, subDomain); + } else if (spusername == username) { + // 同一个账号,直接登录 + String password = username; + username = username + "@" + subDomain; + userLogin(username, password, appKey, subDomain); + } else { + // TODO: 切换新用户登录, 首先判断用户是否已经存在,如不存在着注册;如存在则直接登录 + // String password = username; + // username = username + "@" + subDomain; + // userLogin(username, password, appKey, subDomain); + } + } + + // 访客匿名登录接口 + static void anonymousLogin(String appKey, String subDomain) async { + /// sp初始化 + await SpUtil.getInstance(); + // 首先检测是否是第一次,如果是第一此启动 + String? username = SpUtil.getString(BytedeskConstants.username); + String? unionid = SpUtil.getString(BytedeskConstants.unionid); + if (username!.isEmpty && unionid!.isEmpty) { + // 第一此启动, 则调用注册接口,否则调用登录接口 + User user = await BytedeskUserHttpApi().registerAnonymous(subDomain); + visitorLogin(user.username!, appKey, subDomain); + } else if (username.isNotEmpty) { + // 用户名登录,非微信登录 + visitorLogin(username, appKey, subDomain); + } else if (unionid!.isNotEmpty) { + // 微信登录 + unionidOAuth(unionid); + } else { + // 验证码登录其他账号,非当前匿名登录账户 + otherOAuth(); + } + } + + // 匿名访客登录 + static void visitorLogin(String username, String appkey, String subDomain) { + String password = username; + login( + username, password, appkey, subDomain, BytedeskConstants.ROLE_VISITOR); + } + + // 开发者自定义用户名登录接口 + static void userLogin( + String username, String password, String appkey, String subDomain) { + login( + username, password, appkey, subDomain, BytedeskConstants.ROLE_VISITOR); + } + + // 用户名登录 + static void login(String username, String password, String appkey, + String subDomain, String role) async { + // + SpUtil.putString(BytedeskConstants.role, role); + if (role == BytedeskConstants.ROLE_ADMIN) { + if (!username.contains("@")) { + username = username + "@" + subDomain; + } + } + await BytedeskUserHttpApi().oauth(username, password); + // 登录成功之后,建立长连接 + BytedeskUtils.mqttConnect(); + // if (role == BytedeskConstants.ROLE_ADMIN) { + // // TODO: 如果是客服账号,加载个人信息 + // } + // 上传设备信息 + await BytedeskDeviceHttpApi().setDeviceInfo(); + } + + // 微信unionid登录 + static void unionidOAuth(String unionid) async { + // + await BytedeskUserHttpApi().unionIdOAuth(unionid); + // 登录成功之后,建立长连接 + BytedeskUtils.mqttConnect(); + // 上传设备信息 + await BytedeskDeviceHttpApi().setDeviceInfo(); + } + + // 直接建立长连接 + static void otherOAuth() async { + // 登录成功之后,建立长连接 + BytedeskUtils.mqttConnect(); + // 上传设备信息 + await BytedeskDeviceHttpApi().setDeviceInfo(); + } + + // 建立长连接 + static bool connect() { + return BytedeskUtils.mqttConnect(); + } + + // 重连 + static bool reconnect() { + return BytedeskUtils.mqttReConnect(); + } + + // 判断长连接状态 + static bool isConnected() { + return BytedeskUtils.isMqttConnected(); + } + + // 技能组客服会话 + static void startWorkGroupChat( + BuildContext context, String wid, String title) { + startChatDefault( + context, wid, BytedeskConstants.CHAT_TYPE_WORKGROUP, title); + } + + // 发送附言消息 + static void startWorkGroupChatPostscript( + BuildContext context, String wid, String title, String postScript) { + startChat(context, wid, BytedeskConstants.CHAT_TYPE_WORKGROUP, title, '', + postScript, null); + } + + // 电商接口,携带商品参数 + // type/title/content/price/url/imageUrl/id/categoryCode + static void startWorkGroupChatShop( + BuildContext context, String wid, String title, String commodity) { + startChatShop(context, wid, BytedeskConstants.CHAT_TYPE_WORKGROUP, title, + commodity, null); + } + + static void startWorkGroupChatShopCallback(BuildContext context, String wid, + String title, String commodity, ValueSetter customCallback) { + startChatShop(context, wid, BytedeskConstants.CHAT_TYPE_WORKGROUP, title, + commodity, customCallback); + } + + // 指定客服会话 + static void startAppointedChat( + BuildContext context, String uid, String title) { + startChatDefault( + context, uid, BytedeskConstants.CHAT_TYPE_APPOINTED, title); + } + + // 发送附言消息 + static void startAppointedChatPostscript( + BuildContext context, String uid, String title, String postScript) { + startChat(context, uid, BytedeskConstants.CHAT_TYPE_APPOINTED, title, '', + postScript, null); + } + + // 电商接口,携带商品参数 + static void startAppointedChatShop( + BuildContext context, String uid, String title, String commodity) { + startChatShop(context, uid, BytedeskConstants.CHAT_TYPE_APPOINTED, title, + commodity, null); + } + + static void startAppointedChatShopCallback(BuildContext context, String uid, + String title, String commodity, ValueSetter customCallback) { + startChatShop(context, uid, BytedeskConstants.CHAT_TYPE_APPOINTED, title, + commodity, customCallback); + } + + // 默认设置商品信息和附言为空 + static void startChatDefault( + BuildContext context, String uuid, String type, String title) { + startChat(context, uuid, type, title, '', '', null); + } + + // 电商对话-自定义类型(技能组、指定客服) + static void startChatShop(BuildContext context, String uuid, String type, + String title, String commodity, ValueSetter? customCallback) { + startChat(context, uuid, type, title, commodity, '', customCallback); + } + + // 发送附言消息-自定义类型(技能组、指定客服) + static void startChatPostscript(BuildContext context, String uuid, + String type, String title, String postScript) { + startChat(context, uuid, type, title, '', postScript, null); + } + + // 客服会话-自定义类型(技能组、指定客服) + static void startChat( + BuildContext context, + String uuid, + String type, + String title, + String commodity, + String postScript, + ValueSetter? customCallback) { + Navigator.of(context).push(new MaterialPageRoute(builder: (context) { + return new ChatKFProvider( + wid: uuid, + aid: uuid, + type: type, + title: title, + custom: commodity, + postscript: postScript, + customCallback: customCallback, + ); + })); + } + + // 从历史会话或者点击通知栏进入 + static void startChatThread(BuildContext context, Thread thread, + {String title = ''}) { + Navigator.of(context).push(new MaterialPageRoute(builder: (context) { + return new ChatThreadProvider( + thread: thread, + title: title, + ); + })); + } + + // 应用内打开H5客服页面 + static void startH5Chat(BuildContext context, String url, String title) { + Navigator.of(context).push(new MaterialPageRoute(builder: (context) { + return new ChatWebViewPage( + url: url, + title: title, + ); + })); + } + + // 应用内打开网页 + static void openWebView(BuildContext context, String url, String title) { + Navigator.of(context).push(new MaterialPageRoute(builder: (context) { + return new ChatWebViewPage( + url: url, + title: title, + ); + })); + } + + // TODO: 好友一对一聊天 + static void startContactChat(BuildContext context, String cid, String title) { + Navigator.of(context).push(new MaterialPageRoute(builder: (context) { + return new ChatIMProvider( + // cid: cid, + title: title, + ); + })); + } + + // TODO: 好友一对一,携带商品参数 + // static void startContactChatShop( + // BuildContext context, String cid, String title, String commodity) { + // Navigator.of(context).push(new MaterialPageRoute(builder: (context) { + // return new ChatIMProvider( + // // cid: cid, + // title: title, + // custom: commodity, + // ); + // })); + // } + + // TODO: 客服端-进入接待访客对话页面 + static void startChatThreadIM(BuildContext context, Thread thread) { + Navigator.of(context).push(new MaterialPageRoute(builder: (context) { + return new ChatIMProvider( + thread: thread, + isThread: true, + ); + })); + } + + // 常见问题列表 + static void showFaq(BuildContext context, String uid) { + Navigator.of(context).push(new MaterialPageRoute(builder: (context) { + return new HelpProvider(uid: uid); + })); + } + + // 意见反馈 + static void showFeedback(BuildContext context, String uid) { + Navigator.of(context).push(new MaterialPageRoute(builder: (context) { + return new FeedbackProvider(uid: uid); + })); + } + + // TODO: 提交工单 + static void showTicket(BuildContext context, String uid) { + Navigator.of(context).push(new MaterialPageRoute(builder: (context) { + return new TicketProvider(uid: uid); + })); + } + + // TODO: 留言 + static void showLeaveMessage( + BuildContext context, String uuid, String type, String tip) { + Navigator.of(context).push(new MaterialPageRoute(builder: (context) { + return new LeaveMsgProvider( + wid: uuid, + aid: uuid, + type: type, + tip: tip, + ); + })); + } + + // 频道消息 + static void showChannel(BuildContext context, Thread thread) { + Navigator.of(context).push(new MaterialPageRoute(builder: (context) { + return new ChannelProvider( + thread: thread, + ); + })); + } + + // 获取个人资料 + static Future getProfile() async { + return BytedeskUserHttpApi().getProfile(); + } + + // 设置用户昵称 + static Future updateNickname(String nickname) async { + return BytedeskUserHttpApi().updateNickname(nickname); + } + + // 设置用户头像 + static Future updateAvatar(String avatar) async { + return BytedeskUserHttpApi().updateAvatar(avatar); + } + + // 设置用户备注 + static Future updateDescription(String avatar) async { + return BytedeskUserHttpApi().updateDescription(avatar); + } + + // 查询技能组在线状态 + static Future getWorkGroupStatus(String workGroupWid) async { + return BytedeskUserHttpApi().getWorkGroupStatus(workGroupWid); + } + + // 查询客服在线状态 + static Future getAgentStatus(String agentUid) async { + return BytedeskUserHttpApi().getAgentStatus(agentUid); + } + + // 查询当前用户-某技能组wid或指定客服未读消息数目 + static Future getUnreadCount(String wid) async { + return BytedeskUserHttpApi().getUnreadCount(wid); + } + + // 访客端-查询访客所有未读消息数目 + static Future getUnreadCountVisitor() async { + return BytedeskUserHttpApi().getUnreadCountVisitor(); + } + + // 客服端-查询客服所有未读消息数目 + static Future getUnreadCountAgent() async { + return BytedeskUserHttpApi().getUnreadCountAgent(); + } + + // 访客历史会话 + static Future> getVisitorThreads(int page, int size) async { + return BytedeskThreadHttpApi().getVisitorThreads(page, size); + } + + // 消息提示设置 + static bool? getPlayAudioOnSendMessage() { + return SpUtil.getBool(BytedeskConstants.PLAY_AUDIO_ON_SEND_MESSAGE, + defValue: true); + } + + static void setPlayAudioOnSendMessage(bool flag) { + SpUtil.putBool(BytedeskConstants.PLAY_AUDIO_ON_SEND_MESSAGE, flag); + } + + static bool? getPlayAudioOnReceiveMessage() { + return SpUtil.getBool(BytedeskConstants.PLAY_AUDIO_ON_RECEIVE_MESSAGE, + defValue: true); + } + + static void setPlayAudioOnReceiveMessage(bool flag) { + SpUtil.putBool(BytedeskConstants.PLAY_AUDIO_ON_RECEIVE_MESSAGE, flag); + } + + static bool? getVibrateOnReceiveMessage() { + return SpUtil.getBool(BytedeskConstants.VIBRATE_ON_RECEIVE_MESSAGE, + defValue: true); + } + + static void setVibrateOnReceiveMessage(bool flag) { + SpUtil.putBool(BytedeskConstants.VIBRATE_ON_RECEIVE_MESSAGE, flag); + } + + // 上传ios devicetoken + static Future uploadIOSDeviceToken( + String appkey, String deviceToken) async { + BytedeskDeviceHttpApi() + .updateIOSDeviceToken(appkey, BytedeskConstants.build, deviceToken); + } + + // build的值只有两种情况,测试设置为:debug 或 线上设置为:release + // static Future uploadIOSDeviceToken( + // String appkey, String build, String deviceToken) async { + // BytedeskDeviceHttpApi().updateIOSDeviceToken(appkey, build, deviceToken); + // } + + // build的值只有两种情况,测试设置为:debug 或 线上设置为:release + static Future deleteIOSDeviceToken(String build) async { + BytedeskDeviceHttpApi().deleteIOSDeviceToken(build); + } + + // The package version. `CFBundleShortVersionString` on iOS, `versionName` on Android. + static Future getAppVersion() { + return BytedeskDeviceHttpApi().getAppVersion(); + } + + // The build number. `CFBundleVersion` on iOS, `versionCode` on Android. + static Future getAppBuildNumber() { + return BytedeskDeviceHttpApi().getAppBuildNumber(); + } + + // 从服务器检测当前APP是否有新版 + static Future checkAppVersion(String androidKey, String iosKey) { + if (BytedeskUtils.isWeb) { + // FIXME: 仅用于占位,待修改 + return BytedeskUserHttpApi().checkAppVersion(iosKey); + } else if (BytedeskUtils.isAndroid) { + return BytedeskUserHttpApi().checkAppVersion(androidKey); + } + return BytedeskUserHttpApi().checkAppVersion(iosKey); + } + + // 退出登录 + static Future logout() { + return BytedeskUserHttpApi().logout(); + } + + // 以下为专用接口,普通用户请忽略 + + // 通过token获取手机号-良师app-专用 + static Future getAliyunOneKeyLoginMobile(String token) async { + return BytedeskUserHttpApi().getAliyunOneKeyLoginMobile(token); + } + + // 匿名登录之后,绑定手机号-良师app-专用 + static Future bindMobile(String mobile) async { + return BytedeskUserHttpApi().bindMobile(mobile); + } + + // 微信登录之后,获取微信用户信息-良师app-专用 + static Future getWechatUserinfo(String code) async { + return BytedeskUserHttpApi().getWechatUserinfo(code); + } + + // 手机端注册微信登录用户-并绑定手机号-良师app-专用 + static Future registerWechatMobile(String mobile, + String nickname, String avatar, String unionid, String openid) async { + return BytedeskUserHttpApi() + .registerWechatMobile(mobile, nickname, avatar, unionid, openid); + } + + // 将unionid绑定到已经存在的手机账号-良师app-专用 + static Future bindWeChatMobile( + String mobile, String unionid) async { + return BytedeskUserHttpApi().bindWeChatMobile(mobile, unionid); + } +} diff --git a/bytedesk_kefu/lib/bytedesk_kefu_web.dart b/bytedesk_kefu/lib/bytedesk_kefu_web.dart new file mode 100644 index 0000000..060c084 --- /dev/null +++ b/bytedesk_kefu/lib/bytedesk_kefu_web.dart @@ -0,0 +1,72 @@ +import 'dart:async'; +// import 'dart:io'; +// In order to *not* need this ignore, consider extracting the "web" version +// of your plugin as a separate package, instead of inlining it in the same +// package as the core of your plugin. +// ignore: avoid_web_libraries_in_flutter +import 'dart:html' as html show window; + +import 'package:flutter/services.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; + +// import 'package:bytedesk_kefu/http/bytedesk_device_api.dart'; +// import 'package:bytedesk_kefu/http/bytedesk_thread_api.dart'; +// import 'package:bytedesk_kefu/model/app.dart'; +// import 'package:bytedesk_kefu/model/jsonResult.dart'; +// import 'package:bytedesk_kefu/model/thread.dart'; +// import 'package:bytedesk_kefu/model/userJsonResult.dart'; +// import 'package:bytedesk_kefu/model/wechatResult.dart'; +// import 'package:bytedesk_kefu/ui/channel/provider/channel_provider.dart'; +// import 'package:bytedesk_kefu/ui/chat/page/chat_webview_page.dart'; +// import 'package:bytedesk_kefu/ui/chat/provider/chat_im_provider.dart'; +// import 'package:bytedesk_kefu/ui/chat/provider/chat_thread_provider.dart'; +// import 'package:bytedesk_kefu/ui/faq/provider/help_provider.dart'; +// import 'package:bytedesk_kefu/ui/feedback/provider/feedback_provider.dart'; +// import 'package:bytedesk_kefu/ui/leavemsg/provider/leavemsg_provider.dart'; +// import 'package:bytedesk_kefu/ui/ticket/provider/ticket_provider.dart'; +// import 'package:flutter/services.dart'; + +// import 'package:bytedesk_kefu/http/bytedesk_user_api.dart'; +// import 'package:bytedesk_kefu/ui/chat/provider/chat_kf_provider.dart'; +// import 'package:bytedesk_kefu/util/bytedesk_utils.dart'; +// import 'package:bytedesk_kefu/util/bytedesk_constants.dart'; +// import 'package:flutter/material.dart'; + +// import 'model/user.dart'; + +/// A web implementation of the BytedeskKefu plugin. +class BytedeskKefuWeb { + static void registerWith(Registrar registrar) { + final MethodChannel channel = MethodChannel( + 'bytedesk_kefu', + const StandardMethodCodec(), + registrar, + ); + + final pluginInstance = BytedeskKefuWeb(); + channel.setMethodCallHandler(pluginInstance.handleMethodCall); + } + + /// Handles method calls over the MethodChannel of this plugin. + /// Note: Check the "federated" architecture for a new way of doing this: + /// https://flutter.dev/go/federated-plugins + Future handleMethodCall(MethodCall call) async { + switch (call.method) { + case 'getPlatformVersion': + return getPlatformVersion(); + // break; + default: + throw PlatformException( + code: 'Unimplemented', + details: + 'bytedesk_kefu for web doesn\'t implement \'${call.method}\'', + ); + } + } + + /// Returns a [String] containing the version of the platform. + Future getPlatformVersion() { + final version = html.window.navigator.userAgent; + return Future.value(version); + } +} diff --git a/bytedesk_kefu/lib/http/bytedesk_base_api.dart b/bytedesk_kefu/lib/http/bytedesk_base_api.dart new file mode 100755 index 0000000..e6aa796 --- /dev/null +++ b/bytedesk_kefu/lib/http/bytedesk_base_api.dart @@ -0,0 +1,25 @@ +import 'dart:io'; + +import 'package:bytedesk_kefu/util/bytedesk_constants.dart'; +import 'package:bytedesk_kefu/util/bytedesk_utils.dart'; +import 'package:sp_util/sp_util.dart'; +import 'package:http/http.dart' as http; + +class BytedeskBaseHttpApi { + // + String client = BytedeskUtils.getClient(); + String baseUrl = BytedeskUtils.getBaseUrl(); + // + final http.Client httpClient = http.Client(); + + BytedeskBaseHttpApi(); + // + Map getHeaders() { + String? accessToken = SpUtil.getString(BytedeskConstants.accessToken); + Map headers = { + HttpHeaders.contentTypeHeader: "application/json", + HttpHeaders.authorizationHeader: "Bearer $accessToken" + }; + return headers; + } +} diff --git a/bytedesk_kefu/lib/http/bytedesk_device_api.dart b/bytedesk_kefu/lib/http/bytedesk_device_api.dart new file mode 100755 index 0000000..7971404 --- /dev/null +++ b/bytedesk_kefu/lib/http/bytedesk_device_api.dart @@ -0,0 +1,167 @@ +import 'dart:convert'; +// import 'dart:io'; + +import 'package:bytedesk_kefu/http/bytedesk_base_api.dart'; +import 'package:bytedesk_kefu/util/bytedesk_constants.dart'; +import 'package:bytedesk_kefu/util/bytedesk_utils.dart'; +import 'package:device_info/device_info.dart'; +import 'package:devicelocale/devicelocale.dart'; +import 'package:package_info/package_info.dart'; + +class BytedeskDeviceHttpApi extends BytedeskBaseHttpApi { + // + static final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); + // + Future setDeviceInfo() async { + // + if (BytedeskUtils.isWeb) { + // + } else if (BytedeskUtils.isAndroid) { + updateAndroidDeviceInfo(); + } else if (BytedeskUtils.isIOS) { + updateIOSDeviceInfo(); + } else { + // TODO: web/windows/mac + } + } + + // APP当前版本 + // The package version. `CFBundleShortVersionString` on iOS, `versionName` on Android. + Future getAppVersion() async { + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + return packageInfo.version; + } + + // The build number. `CFBundleVersion` on iOS, `versionCode` on Android. + Future getAppBuildNumber() async { + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + return packageInfo.buildNumber; + } + + // 上传安卓设备信息 + Future updateAndroidDeviceInfo() async { + // + Map deviceData = + _readAndroidBuildData(await deviceInfoPlugin.androidInfo); + // + String? locale = await Devicelocale.currentLocale; + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + var body = json.encode({ + "sdkVersion": deviceData['version.sdkInt'], + "osVersion": deviceData['version.release'], + "deviceModel": deviceData['model'], + "brand": deviceData['brand'], + "language": locale, + "appVersion": packageInfo.version, + "appVersionName": packageInfo.buildNumber, + "client": client + }); + // + // final initUrl = '$baseUrl/api/fingerprint2/android/deviceInfo'; + final initUrl = Uri.http( + BytedeskConstants.host, '/api/fingerprint2/android/deviceInfo'); + await this.httpClient.post(initUrl, headers: getHeaders(), body: body); + } + + // 上传苹果设备信息 + Future updateIOSDeviceInfo() async { + // + Map deviceData = + _readIosDeviceInfo(await deviceInfoPlugin.iosInfo); + // + String? locale = await Devicelocale.currentLocale; + // Locale locale2 = await Devicelocale.currentAsLocale; + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + var body = json.encode({ + "os": deviceData['systemName'], + "osVersion": deviceData['systemVersion'], + "deviceName": deviceData['name'], + "deviceModel": deviceData['utsname.machine'], + "appName": packageInfo.appName, + "appVersion": packageInfo.version, + "language": locale, + "appCountry": deviceData[''], // TODO: 获取国家 + "client": client + }); + // + // final initUrl = '$baseUrl/api/fingerprint2/ios/deviceInfo'; + final initUrl = + Uri.http(BytedeskConstants.host, '/api/fingerprint2/ios/deviceInfo'); + await this.httpClient.post(initUrl, headers: getHeaders(), body: body); + } + + // 上传苹果deviceToken + Future updateIOSDeviceToken( + String appkey, String build, String deviceToken) async { + // + var body = json.encode({ + "appkey": appkey, + "build": build, + "token": deviceToken, + "client": client + }); + // + // final initUrl = '$baseUrl/api/push/update/token'; + final initUrl = Uri.http(BytedeskConstants.host, '/api/push/update/token'); + await this.httpClient.post(initUrl, headers: getHeaders(), body: body); + } + + // 删除设备信息 + Future deleteIOSDeviceToken(String build) async { + // + var body = json.encode({"build": build, "client": client}); + // + // final initUrl = '$baseUrl/api/push/delete/token'; + final initUrl = Uri.http(BytedeskConstants.host, '/api/push/delete/token'); + await this.httpClient.post(initUrl, headers: getHeaders(), body: body); + } + + Map _readAndroidBuildData(AndroidDeviceInfo build) { + return { + 'version.securityPatch': build.version.securityPatch, + 'version.sdkInt': build.version.sdkInt, + 'version.release': build.version.release, + 'version.previewSdkInt': build.version.previewSdkInt, + 'version.incremental': build.version.incremental, + 'version.codename': build.version.codename, + 'version.baseOS': build.version.baseOS, + 'board': build.board, + 'bootloader': build.bootloader, + 'brand': build.brand, + 'device': build.device, + 'display': build.display, + 'fingerprint': build.fingerprint, + 'hardware': build.hardware, + 'host': build.host, + 'id': build.id, + 'manufacturer': build.manufacturer, + 'model': build.model, + 'product': build.product, + 'supported32BitAbis': build.supported32BitAbis, + 'supported64BitAbis': build.supported64BitAbis, + 'supportedAbis': build.supportedAbis, + 'tags': build.tags, + 'type': build.type, + 'isPhysicalDevice': build.isPhysicalDevice, + 'androidId': build.androidId, + 'systemFeatures': build.systemFeatures, + }; + } + + Map _readIosDeviceInfo(IosDeviceInfo data) { + return { + 'name': data.name, + 'systemName': data.systemName, + 'systemVersion': data.systemVersion, + 'model': data.model, + 'localizedModel': data.localizedModel, + 'identifierForVendor': data.identifierForVendor, + 'isPhysicalDevice': data.isPhysicalDevice, + 'utsname.sysname:': data.utsname.sysname, + 'utsname.nodename:': data.utsname.nodename, + 'utsname.release:': data.utsname.release, + 'utsname.version:': data.utsname.version, + 'utsname.machine:': data.utsname.machine, + }; + } +} diff --git a/bytedesk_kefu/lib/http/bytedesk_faq_api.dart b/bytedesk_kefu/lib/http/bytedesk_faq_api.dart new file mode 100755 index 0000000..c36c39c --- /dev/null +++ b/bytedesk_kefu/lib/http/bytedesk_faq_api.dart @@ -0,0 +1,57 @@ +import 'dart:convert'; + +import 'package:bytedesk_kefu/http/bytedesk_base_api.dart'; +import 'package:bytedesk_kefu/model/helpArticle.dart'; +import 'package:bytedesk_kefu/model/helpCategory.dart'; +import 'package:bytedesk_kefu/util/bytedesk_constants.dart'; + +class BytedeskFaqHttpApi extends BytedeskBaseHttpApi { + // + // 常见问题分类 + Future> getHelpSupportCategories(String? uid) async { + // + final categoriesUrl = Uri.http(BytedeskConstants.host, + '/visitor/api/category/support', {'uid': uid, 'client': client}); + print("categories Url $categoriesUrl"); + final initResponse = await this.httpClient.get(categoriesUrl); + // + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + print("responseJson $responseJson"); + // + List categories = (responseJson['data'] as List) + .map((item) => HelpCategory.fromJson(item)) + .toList(); + + return categories; + } + + // 常见问题分类-所属文章 + Future> getHelpSupportArticles(int? categoryId) async { + // + // final categoriesUrl = + // '$baseUrl/visitor/api/category/articles?categoryId=$categoryId&client=$client'; + final categoriesUrl = Uri.http( + BytedeskConstants.host, + '/visitor/api/category/articles', + {'categoryId': categoryId, 'client': client}); + print("categories Url $categoriesUrl"); + final initResponse = await this.httpClient.get(categoriesUrl); + // + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + print("responseJson $responseJson"); + // + List articles = (responseJson['data'] as List) + .map((item) => HelpArticle.fromJson(item)) + .toList(); + // + return articles; + } +} diff --git a/bytedesk_kefu/lib/http/bytedesk_feedback_api.dart b/bytedesk_kefu/lib/http/bytedesk_feedback_api.dart new file mode 100755 index 0000000..74dd00e --- /dev/null +++ b/bytedesk_kefu/lib/http/bytedesk_feedback_api.dart @@ -0,0 +1,90 @@ +import 'dart:convert'; + +import 'package:bytedesk_kefu/http/bytedesk_base_api.dart'; +import 'package:bytedesk_kefu/model/helpCategory.dart'; +import 'package:bytedesk_kefu/model/jsonResult.dart'; +import 'package:bytedesk_kefu/util/bytedesk_constants.dart'; +import 'package:bytedesk_kefu/util/bytedesk_events.dart'; +import 'package:sp_util/sp_util.dart'; +import 'package:http/http.dart' as http; + +class BytedeskFeedbackHttpApi extends BytedeskBaseHttpApi { + // + // 获取意见反馈分类 + Future> getHelpFeedbackCategories(String? uid) async { + // + // final categoriesUrl = + // '$baseUrl/visitor/api/category/feedback?uid=$uid&client=$client'; + final categoriesUrl = Uri.http(BytedeskConstants.host, + '/visitor/api/category/feedback', {'uid': uid, 'client': client}); + print("categories Url $categoriesUrl"); + final initResponse = await this.httpClient.get(categoriesUrl); + // + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + print("responseJson $responseJson"); + // + List categories = (responseJson['data'] as List) + .map((item) => HelpCategory.fromJson(item)) + .toList(); + + return categories; + } + + // TODO: 提交意见反馈 + Future submitFeedback( + String? content, List? imageUrls) async { + // + var body = json + .encode({"content": content, "images": imageUrls, "client": client}); + // final initUrl = '$baseUrl/api/feedback/create'; + final initUrl = Uri.http(BytedeskConstants.host, '/api/feedback/create'); + final initResponse = + await this.httpClient.post(initUrl, headers: getHeaders(), body: body); + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + print("responseJson $responseJson"); + // 判断token是否过期 + if (responseJson.toString().contains('invalid_token')) { + bytedeskEventBus.fire(InvalidTokenEventBus()); + } + + return JsonResult.fromJson(responseJson); + } + + // https://pub.dev/documentation/http/latest/http/MultipartRequest-class.html + Future upload(String? filePath) async { + // + String fileName = filePath!.split("/").last; + String? username = SpUtil.getString(BytedeskConstants.uid); + + final uploadUrl = '$baseUrl/visitor/api/upload/image'; + print("fileName $fileName, username $username, upload Url $uploadUrl"); + + var uri = Uri.parse(uploadUrl); + var request = http.MultipartRequest('POST', uri) + ..fields['file_name'] = fileName + ..fields['username'] = username! + ..files.add(await http.MultipartFile.fromPath('file', filePath)); + + http.Response response = + await http.Response.fromStream(await request.send()); + // print("Result: ${response.body}"); + + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = json.decode(utf8decoder.convert(response.bodyBytes)); + print("responseJson $responseJson"); + + String url = responseJson['data']; + print('url:' + url); + return url; + } +} diff --git a/bytedesk_kefu/lib/http/bytedesk_friend_api.dart b/bytedesk_kefu/lib/http/bytedesk_friend_api.dart new file mode 100755 index 0000000..ca72fab --- /dev/null +++ b/bytedesk_kefu/lib/http/bytedesk_friend_api.dart @@ -0,0 +1,147 @@ +import 'dart:convert'; + +import 'package:bytedesk_kefu/http/bytedesk_base_api.dart'; +import 'package:bytedesk_kefu/model/friend.dart'; +import 'package:bytedesk_kefu/util/bytedesk_events.dart'; +import 'package:bytedesk_kefu/util/bytedesk_utils.dart'; +import 'package:bytedesk_kefu/util/bytedesk_constants.dart'; + +class BytedeskFriendHttpApi extends BytedeskBaseHttpApi { + // + Future> getFriends(int? page, int? size) async { + // + // final friendsUrl = + // '$baseUrl/api/friend/query?page=$page&size=$size&client=$client'; + final friendsUrl = Uri.http(BytedeskConstants.host, '/api/friend/query', + {'page': page.toString(), 'size': size.toString(), 'client': client}); + print("get friends Url $friendsUrl"); + final initResponse = + await this.httpClient.get(friendsUrl, headers: getHeaders()); + + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + print("responseJson $responseJson"); + // 判断token是否过期 + if (responseJson.toString().contains('invalid_token')) { + bytedeskEventBus.fire(InvalidTokenEventBus()); + } + + List orders = (responseJson['data']['content'] as List) + .map((item) => Friend.fromJson(item)) + .toList(); + + return orders; + } + + // 加载通讯录列表 + Future> getFriendsAddress(int? page, int? size) async { + // + // final addressUrl = + // '$baseUrl/api/friend/address/query?page=$page&size=$size&client=$client'; + final addressUrl = Uri.http( + BytedeskConstants.host, + '/api/friend/address/query', + {'page': page.toString(), 'size': size.toString(), 'client': client}); + print("address Url $addressUrl"); + final initResponse = + await this.httpClient.get(addressUrl, headers: getHeaders()); + + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + print("responseJson $responseJson"); + // 判断token是否过期 + if (responseJson.toString().contains('invalid_token')) { + bytedeskEventBus.fire(InvalidTokenEventBus()); + } + + List orders = (responseJson['data']['content'] as List) + .map((item) => Friend.fromJson(item)) + .toList(); + + return orders; + } + + // + Future uploadAddress(String? nickname, String? mobile) async { + // + var body = + json.encode({"nickname": nickname, "mobile": mobile, "client": client}); + // + // final initUrl = '$baseUrl/api/friend/address/create'; + final initUrl = + Uri.http(BytedeskConstants.host, '/api/friend/address/create'); + final initResponse = + await this.httpClient.post(initUrl, headers: getHeaders(), body: body); + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + print("responseJson $responseJson"); + // 判断token是否过期 + if (responseJson.toString().contains('invalid_token')) { + bytedeskEventBus.fire(InvalidTokenEventBus()); + } + + return Friend.fromJson(responseJson['data']); + } + + // 加载附近列表 + Future> getFriendsNearby(int? page, int? size) async { + // + double? latitude = BytedeskUtils.getLatitude(); + double? longtitude = BytedeskUtils.getLongtitude(); + // + // final addressUrl = + // '$baseUrl/api/friend/nearby/query?lat=$latitude&lng=$longtitude&page=$page&size=$size&client=$client'; + final addressUrl = + Uri.http(BytedeskConstants.host, '/api/friend/nearby/query', { + 'lat': latitude, + 'lng': longtitude, + 'page': page.toString(), + 'size': size.toString(), + 'client': client + }); + print("address Url $addressUrl"); + final initResponse = + await this.httpClient.get(addressUrl, headers: getHeaders()); + + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + print("responseJson $responseJson"); + // 判断token是否过期 + if (responseJson.toString().contains('invalid_token')) { + bytedeskEventBus.fire(InvalidTokenEventBus()); + } + + List friends = (responseJson['data']['searchHits'] as List) + .map((item) => Friend.fromElasticJson(item['content'])) + .toList(); + + // List friends = (responseJson['data']['content'] as List) + // .map((item) => Friend.fromJson(item)) + // .toList(); + + return friends; + } + + // + Future updateLocation(double? latitude, double? longtitude) async { + // + var body = + json.encode({"lat": latitude, "lng": longtitude, "client": client}); + // final initUrl = '$baseUrl/api/friend/nearby/update'; + final initUrl = + Uri.http(BytedeskConstants.host, '/api/friend/nearby/update'); + await this.httpClient.post(initUrl, headers: getHeaders(), body: body); + } +} diff --git a/bytedesk_kefu/lib/http/bytedesk_leavemsg_api.dart b/bytedesk_kefu/lib/http/bytedesk_leavemsg_api.dart new file mode 100755 index 0000000..bc4cadc --- /dev/null +++ b/bytedesk_kefu/lib/http/bytedesk_leavemsg_api.dart @@ -0,0 +1,90 @@ +import 'dart:convert'; + +import 'package:bytedesk_kefu/http/bytedesk_base_api.dart'; +import 'package:bytedesk_kefu/model/helpCategory.dart'; +import 'package:bytedesk_kefu/model/jsonResult.dart'; +import 'package:bytedesk_kefu/util/bytedesk_constants.dart'; +import 'package:bytedesk_kefu/util/bytedesk_events.dart'; +// import 'package:bytedesk_kefu/util/bytedesk_utils.dart'; +import 'package:http/http.dart' as http; +import 'package:sp_util/sp_util.dart'; + +class BytedeskLeaveMsgHttpApi extends BytedeskBaseHttpApi { + // 获取意见反馈分类 + Future> getHelpLeaveMsgCategories(String? uid) async { + // + // final categoriesUrl = + // '$baseUrl/visitor/api/category/feedback?uid=$uid&client=$client'; + final categoriesUrl = Uri.http(BytedeskConstants.host, + '/visitor/api/category/feedback', {'uid': uid, 'client': client}); + print("categories Url $categoriesUrl"); + final initResponse = await this.httpClient.get(categoriesUrl); + // + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + // + List categories = (responseJson['data'] as List) + .map((item) => HelpCategory.fromJson(item)) + .toList(); + + return categories; + } + + // TODO: 提交意见反馈 + Future submitLeaveMsg( + String? content, List? imageUrls) async { + // + var body = json.encode({"content": content, "client": client}); + // + // final initUrl = '$baseUrl/api/feedback/create'; + final initUrl = Uri.http(BytedeskConstants.host, '/api/leavemsg/create'); + final initResponse = + await this.httpClient.post(initUrl, headers: getHeaders(), body: body); + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + print("responseJson $responseJson"); + // 判断token是否过期 + if (responseJson.toString().contains('invalid_token')) { + bytedeskEventBus.fire(InvalidTokenEventBus()); + } + + // return User.fromJson(responseJson); + return JsonResult(); + } + + // https://pub.dev/documentation/http/latest/http/MultipartRequest-class.html + Future upload(String? filePath) async { + // + String fileName = filePath!.split("/").last; + String? username = SpUtil.getString(BytedeskConstants.uid); + + final uploadUrl = '$baseUrl/visitor/api/upload/image'; + print("fileName $fileName, username $username, upload Url $uploadUrl"); + + var uri = Uri.parse(uploadUrl); + var request = http.MultipartRequest('POST', uri) + ..fields['file_name'] = fileName + ..fields['username'] = username! + ..files.add(await http.MultipartFile.fromPath('file', filePath)); + + http.Response response = + await http.Response.fromStream(await request.send()); + // print("Result: ${response.body}"); + + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = json.decode(utf8decoder.convert(response.bodyBytes)); + print("responseJson $responseJson"); + + String url = responseJson['data']; + print('url:' + url); + return url; + } +} diff --git a/bytedesk_kefu/lib/http/bytedesk_message_api.dart b/bytedesk_kefu/lib/http/bytedesk_message_api.dart new file mode 100755 index 0000000..0a6ee69 --- /dev/null +++ b/bytedesk_kefu/lib/http/bytedesk_message_api.dart @@ -0,0 +1,288 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:bytedesk_kefu/http/bytedesk_base_api.dart'; +import 'package:bytedesk_kefu/model/jsonResult.dart'; +import 'package:bytedesk_kefu/model/message.dart'; +import 'package:bytedesk_kefu/model/requestAnswer.dart'; +import 'package:bytedesk_kefu/model/uploadJsonResult.dart'; +import 'package:bytedesk_kefu/util/bytedesk_constants.dart'; +import 'package:bytedesk_kefu/util/bytedesk_events.dart'; +import 'package:sp_util/sp_util.dart'; +import 'package:http/http.dart' as http; +import 'package:http_parser/http_parser.dart'; + +class BytedeskMessageHttpApi extends BytedeskBaseHttpApi { + // + Future sendMessageRest(String? jsonString) async { + // + var body = json.encode({"json": jsonString, "client": client}); + // + final sendMessageUrl = + Uri.http(BytedeskConstants.host, '/api/messages/send'); + // final sendMessageUrl = '$baseUrl/api/messages/send'; + final sendMessageResponse = await this + .httpClient + .post(sendMessageUrl, headers: getHeaders(), body: body); + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(sendMessageResponse.bodyBytes)); + print("responseJson $responseJson"); + // 判断token是否过期 + if (responseJson.toString().contains('invalid_token')) { + bytedeskEventBus.fire(InvalidTokenEventBus()); + } + // + return JsonResult(message: "发送消息成功", statusCode: 200, data: jsonString); + } + + Future> loadHistoryMessages( + String? uid, int? page, int? size) async { + // + final loadHistoryMessagesUrl = Uri.http( + BytedeskConstants.host, '/api/messages/user', { + 'page': page.toString(), + 'size': size.toString(), + 'uid': uid, + 'client': client + }); + // print("loadHistoryMessages Url $loadHistoryMessages"); + final initResponse = await this + .httpClient + .get(loadHistoryMessagesUrl, headers: getHeaders()); + // + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + // 判断token是否过期 + if (responseJson.toString().contains('invalid_token')) { + bytedeskEventBus.fire(InvalidTokenEventBus()); + } + // + List messageList = + (responseJson['data']['content'] as List) + .map((item) => Message.fromJson(item)) + .toList(); + + return messageList; + } + + Future> loadTopicMessages( + String? topic, int? page, int? size) async { + // + // final loadTopicMessagesUrl = + // '$baseUrl/api/messages/topic?topic=$topic&page=$page&size=$size&client=$client'; + final loadTopicMessagesUrl = + Uri.http(BytedeskConstants.host, '/api/messages/topic', { + 'page': page.toString(), + 'size': size.toString(), + 'topic': topic, + 'client': client + }); + // print("loadHistoryMessages Url $loadHistoryMessages"); + final initResponse = + await this.httpClient.get(loadTopicMessagesUrl, headers: getHeaders()); + // + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + // 判断token是否过期 + if (responseJson.toString().contains('invalid_token')) { + bytedeskEventBus.fire(InvalidTokenEventBus()); + } + // + List messageList = + (responseJson['data']['content'] as List) + .map((item) => Message.fromJson(item)) + .toList(); + + return messageList; + } + + Future> loadChannelMessages( + String? cid, int? page, int? size) async { + // + // final loadChannelMessagesUrl = + // '$baseUrl/api/messages/channel?cid=$cid&page=$page&size=$size&client=$client'; + final loadChannelMessagesUrl = Uri.http( + BytedeskConstants.host, '/api/messages/channel', { + 'page': page.toString(), + 'size': size.toString(), + 'cid': cid, + 'client': client + }); + // print("loadChannelMessagesUrl Url $loadHistoryMessages"); + final initResponse = await this + .httpClient + .get(loadChannelMessagesUrl, headers: getHeaders()); + + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + // 判断token是否过期 + if (responseJson.toString().contains('invalid_token')) { + bytedeskEventBus.fire(InvalidTokenEventBus()); + } + // + List messageList = + (responseJson['data']['content'] as List) + .map((item) => Message.fromJson(item)) + .toList(); + + return messageList; + } + + // + Future queryAnswer(String? tid, String? aid) async { + // + // final queryAnswerUrl = + // '$baseUrl/api/answer/query?tid=$tid&aid=$aid&client=$client'; + final queryAnswerUrl = Uri.http(BytedeskConstants.host, '/api/answer/query', + {'tid': tid, 'aid': aid, 'client': client}); + print("query Url $queryAnswerUrl"); + final initResponse = + await this.httpClient.get(queryAnswerUrl, headers: getHeaders()); + // + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + // 判断token是否过期 + if (responseJson.toString().contains('invalid_token')) { + bytedeskEventBus.fire(InvalidTokenEventBus()); + } + // + return RequestAnswerResult.fromJson(responseJson); + } + + // + Future messageAnswer( + String? type, String? wid, String? aid, String? content) async { + // + // final messageAnswerUrl = + // '$baseUrl/api/v2/answer/message?type=$type&wid=$wid&aid=$aid&content=$content&client=$client'; + final messageAnswerUrl = Uri.http( + BytedeskConstants.host, '/api/v2/answer/message', { + 'type': type, + 'wid': wid, + 'aid': aid, + 'content': content, + 'client': client + }); + print("message Url $messageAnswerUrl"); + final initResponse = + await this.httpClient.get(messageAnswerUrl, headers: getHeaders()); + // + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + print("messageAnswer responseJson $responseJson"); + // 判断token是否过期 + if (responseJson.toString().contains('invalid_token')) { + bytedeskEventBus.fire(InvalidTokenEventBus()); + } + // + return RequestAnswerResult.fromJson(responseJson); + } + + // + Future rateAnswer( + String? aid, String? mid, bool? rate) async { + // + // final rateAnswerUrl = + // '$baseUrl/api/answer/rate?aid=$aid&mid=$mid&rate=$rate&client=$client'; + final rateAnswerUrl = Uri.http(BytedeskConstants.host, '/api/answer/rate', + {'aid': aid, 'mid': mid, 'rate': rate, 'client': client}); + print("rate Url $rateAnswerUrl"); + final initResponse = + await this.httpClient.get(rateAnswerUrl, headers: getHeaders()); + // + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + // 判断token是否过期 + if (responseJson.toString().contains('invalid_token')) { + bytedeskEventBus.fire(InvalidTokenEventBus()); + } + // + return RequestAnswerResult.fromRateJson(responseJson); + } + + // https://pub.dev/documentation/http/latest/http/MultipartRequest-class.html + Future uploadImage(String? filePath) async { + // + String? fileName = filePath!.split("/").last; + String? username = SpUtil.getString(BytedeskConstants.uid); + + final uploadUrl = + '${BytedeskConstants.httpUploadUrl}/visitor/api/upload/image'; + print( + "uploadImage fileName $fileName, username $username, upload Url $uploadUrl"); + + var uri = Uri.parse(uploadUrl); + var request = http.MultipartRequest('POST', uri) + ..fields['file_name'] = username! + "_" + fileName + ..fields['username'] = username + ..files.add(await http.MultipartFile.fromPath('file', filePath)); + + http.Response response = + await http.Response.fromStream(await request.send()); + // print("Result: ${response.body}"); + + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = json.decode(utf8decoder.convert(response.bodyBytes)); + print("upload image responseJson $responseJson"); + // + return UploadJsonResult.fromJson(responseJson); + } + + // https://pub.dev/documentation/http/latest/http/MultipartRequest-class.html + Future uploadVideo(String? filePath) async { + // FIXME: image_picker有bug,选择视频后缀为.jpg,此处替换一下 + String? fileName = filePath!.split("/").last.replaceAll(".jpg", ".mp4"); + String? username = SpUtil.getString(BytedeskConstants.uid); + final uploadUrl = + '${BytedeskConstants.httpUploadUrl}/visitor/api/upload/video'; + print( + "uploadVideo fileName $fileName, username $username, upload Url $uploadUrl"); + // + Map headers = { + HttpHeaders.contentTypeHeader: "video/mp4", + }; + var uri = Uri.parse(uploadUrl); + var request = http.MultipartRequest('POST', uri) + ..fields['file_name'] = username! + "_" + fileName + ..fields['username'] = username + ..headers.addAll(headers) + ..files.add(await http.MultipartFile.fromPath('file', filePath, + // FIXME: 设置不起作用? + contentType: MediaType('video', 'mp4'))); + + http.Response response = + await http.Response.fromStream(await request.send()); + // print("Result: ${response.body}"); + + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = json.decode(utf8decoder.convert(response.bodyBytes)); + print("upload Video responseJson $responseJson"); + // + return UploadJsonResult.fromJson(responseJson); + } +} diff --git a/bytedesk_kefu/lib/http/bytedesk_thread_api.dart b/bytedesk_kefu/lib/http/bytedesk_thread_api.dart new file mode 100755 index 0000000..43ffd7d --- /dev/null +++ b/bytedesk_kefu/lib/http/bytedesk_thread_api.dart @@ -0,0 +1,432 @@ +import 'dart:convert'; +// import 'dart:io'; + +import 'package:bytedesk_kefu/http/bytedesk_base_api.dart'; +import 'package:bytedesk_kefu/model/markThread.dart'; +import 'package:bytedesk_kefu/model/requestThread.dart'; +import 'package:bytedesk_kefu/model/thread.dart'; +import 'package:bytedesk_kefu/util/bytedesk_constants.dart'; +import 'package:bytedesk_kefu/util/bytedesk_events.dart'; +import 'package:sp_util/sp_util.dart'; + +class BytedeskThreadHttpApi extends BytedeskBaseHttpApi { + // + // 客服端-加载会话列表 + Future> getThreads() async { + // final threadUrl = '$baseUrl/api/thread/get?client=$client'; + final threadUrl = + Uri.http(BytedeskConstants.host, '/api/thread/get', {'client': client}); + // print("thread Url $threadUrl"); + final initResponse = + await this.httpClient.get(threadUrl, headers: getHeaders()); + + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + // print("responseJson $responseJson"); + // 判断token是否过期 + if (responseJson.toString().contains('invalid_token')) { + bytedeskEventBus.fire(InvalidTokenEventBus()); + } + + List threadList = []; + + List agentThreadList = + (responseJson['data']['agentThreads'] as List) + .map((item) => Thread.fromWorkGroupJson(item)) + .toList(); + threadList.addAll(agentThreadList); + + List contactThreadList = + (responseJson['data']['contactThreads'] as List) + .map((item) => Thread.fromContactJson(item)) + .toList(); + threadList.addAll(contactThreadList); + + List groupThreadList = + (responseJson['data']['groupThreads'] as List) + .map((item) => Thread.fromGroupJson(item)) + .toList(); + threadList.addAll(groupThreadList); + + return threadList; + } + + // 客服端-历史客服会话 + Future> getHistoryThreads(int? page, int? size) async { + // + // final threadUrl = + // '$baseUrl/api/thread/history/records?page=$page&size=$size&client=$client'; + final threadUrl = Uri.http( + BytedeskConstants.host, + '/api/thread/history/records', + {'page': page.toString(), 'size': size.toString(), 'client': client}); + // print("thread Url $threadUrl"); + final initResponse = + await this.httpClient.get(threadUrl, headers: getHeaders()); + + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + print("responseJson $responseJson"); + // 判断token是否过期 + if (responseJson.toString().contains('invalid_token')) { + bytedeskEventBus.fire(InvalidTokenEventBus()); + } + + List threadList = []; + + List agentThreadList = + (responseJson['data']['content'] as List) + .map((item) => Thread.fromHistoryJson(item)) + .toList(); + threadList.addAll(agentThreadList); + + return threadList; + } + + // 访客端-加载访客会话列表-分页 + Future> getVisitorThreads(int? page, int? size) async { + // + final threadUrl = Uri.http( + BytedeskConstants.host, + '/api/thread/visitor/history', + {'page': page.toString(), 'size': size.toString(), 'client': client}); + final initResponse = + await this.httpClient.get(threadUrl, headers: getHeaders()); + // + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + // print("getVisitorThreads responseJson $responseJson"); + // 判断token是否过期 + if (responseJson.toString().contains('invalid_token')) { + bytedeskEventBus.fire(InvalidTokenEventBus()); + } + + List threadList = (responseJson['data']["content"] as List) + .map((item) => Thread.fromWorkGroupJson2(item)) + .toList(); + // + return threadList; + } + + // 访客端-加载访客会话列表-全部 + Future> getVisitorThreadsAll() async { + // + final threadUrl = Uri.http(BytedeskConstants.host, + '/api/thread/visitor/history/all', {'client': client}); + final initResponse = + await this.httpClient.get(threadUrl, headers: getHeaders()); + // + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + print("getVisitorThreadsAll responseJson $responseJson"); + // 判断token是否过期 + if (responseJson.toString().contains('invalid_token')) { + bytedeskEventBus.fire(InvalidTokenEventBus()); + } + + List threadList = (responseJson['data'] as List) + .map((item) => Thread.fromWorkGroupJson2(item)) + .toList(); + // + return threadList; + } + + // 请求客服会话 + Future requestThread( + String? wid, String? type, String? aid) async { + // + final threadUrl = Uri.http(BytedeskConstants.host, '/api/thread/request', + {'wId': wid, 'type': type, 'aId': aid, 'client': client}); + // print("request thread Url $threadUrl"); + final initResponse = + await this.httpClient.get(threadUrl, headers: getHeaders()); + + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + // print("responseJson $responseJson"); + // 判断token是否过期 + if (responseJson.toString().contains('invalid_token')) { + bytedeskEventBus.fire(InvalidTokenEventBus()); + } + + return RequestThreadResult.fromJson(responseJson); + } + + // 请求人工客服,不管此工作组是否设置为默认机器人,只要有人工客服在线,则可以直接对接人工 + Future requestAgent( + String? wid, String? type, String? aid) async { + // + final threadUrl = Uri.http( + BytedeskConstants.host, + '/api/thread/request/agent', + {'wId': wid, 'type': type, 'aId': aid, 'client': client}); + print("request agent Url $threadUrl"); + final initResponse = + await this.httpClient.get(threadUrl, headers: getHeaders()); + + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + // print("responseJson $responseJson"); + // 判断token是否过期 + if (responseJson.toString().contains('invalid_token')) { + bytedeskEventBus.fire(InvalidTokenEventBus()); + } + + return RequestThreadResult.fromJson(responseJson); + } + + // 请求一对一会话 + Future requestContactThread(String? cid) async { + // + final threadUrl = Uri.http(BytedeskConstants.host, '/api/thread/contact', + {'cid': cid, 'client': client}); + print("request contact thread Url $threadUrl"); + final initResponse = + await this.httpClient.get(threadUrl, headers: getHeaders()); + + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + // print("responseJson $responseJson"); + // 判断token是否过期 + if (responseJson.toString().contains('invalid_token')) { + bytedeskEventBus.fire(InvalidTokenEventBus()); + } + + return RequestThreadResult.fromJson(responseJson); + } + + // 请求群组会话 + Future requestGroupThread(String? gid) async { + // + final threadUrl = Uri.http(BytedeskConstants.host, '/api/thread/group', + {'gid': gid, 'client': client}); + print("request contact thread Url $threadUrl"); + final initResponse = + await this.httpClient.get(threadUrl, headers: getHeaders()); + + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + // print("responseJson $responseJson"); + // 判断token是否过期 + if (responseJson.toString().contains('invalid_token')) { + bytedeskEventBus.fire(InvalidTokenEventBus()); + } + + return RequestThreadResult.fromJson(responseJson); + } + + // 会话置顶 + Future markTop(String? tid) async { + // + String? uid = SpUtil.getString(BytedeskConstants.uid); + // + var body = json.encode({'tid': tid, 'uid': uid, 'client': client}); + final threadUrl = Uri.http( + BytedeskConstants.host, + '/api/v2/thread/mark/top', + ); + final initResponse = await this + .httpClient + .post(threadUrl, headers: getHeaders(), body: body); + + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + // print("responseJson $responseJson"); + // 判断token是否过期 + if (responseJson.toString().contains('invalid_token')) { + bytedeskEventBus.fire(InvalidTokenEventBus()); + } + + return MarkThreadResult.fromJson(responseJson); + } + + // 取消会话置顶 + Future unmarkTop(String? tid) async { + // + String? uid = SpUtil.getString(BytedeskConstants.uid); + // + var body = json.encode({'tid': tid, 'uid': uid, 'client': client}); + final threadUrl = Uri.http( + BytedeskConstants.host, + '/api/v2/thread/unmark/top', + ); + final initResponse = await this + .httpClient + .post(threadUrl, headers: getHeaders(), body: body); + + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + // print("responseJson $responseJson"); + // 判断token是否过期 + if (responseJson.toString().contains('invalid_token')) { + bytedeskEventBus.fire(InvalidTokenEventBus()); + } + + return MarkThreadResult.fromJson(responseJson); + } + + // 会话免打扰 + Future markNodisturb(String? tid) async { + // + String? uid = SpUtil.getString(BytedeskConstants.uid); + // + var body = json.encode({'tid': tid, 'uid': uid, 'client': client}); + final threadUrl = Uri.http( + BytedeskConstants.host, + '/api/v2/thread/mark/nodisturb', + ); + final initResponse = await this + .httpClient + .post(threadUrl, headers: getHeaders(), body: body); + + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + // print("responseJson $responseJson"); + // 判断token是否过期 + if (responseJson.toString().contains('invalid_token')) { + bytedeskEventBus.fire(InvalidTokenEventBus()); + } + + return MarkThreadResult.fromJson(responseJson); + } + + // 取消会话免打扰 + Future unmarkNodisturb(String? tid) async { + // + String? uid = SpUtil.getString(BytedeskConstants.uid); + // + var body = json.encode({'tid': tid, 'uid': uid, 'client': client}); + final threadUrl = Uri.http( + BytedeskConstants.host, + '/api/v2/thread/unmark/nodisturb', + ); + final initResponse = await this + .httpClient + .post(threadUrl, headers: getHeaders(), body: body); + + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + // print("responseJson $responseJson"); + // 判断token是否过期 + if (responseJson.toString().contains('invalid_token')) { + bytedeskEventBus.fire(InvalidTokenEventBus()); + } + + return MarkThreadResult.fromJson(responseJson); + } + + // 会话未读 + Future markUnread(String? tid) async { + // + String? uid = SpUtil.getString(BytedeskConstants.uid); + // + var body = json.encode({'tid': tid, 'uid': uid, 'client': client}); + final threadUrl = Uri.http( + BytedeskConstants.host, + '/api/v2/thread/mark/unread', + ); + final initResponse = await this + .httpClient + .post(threadUrl, headers: getHeaders(), body: body); + + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + // print("responseJson $responseJson"); + // 判断token是否过期 + if (responseJson.toString().contains('invalid_token')) { + bytedeskEventBus.fire(InvalidTokenEventBus()); + } + + return MarkThreadResult.fromJson(responseJson); + } + + // 取消会话未读 + Future unmarkUnread(String? tid) async { + // + String? uid = SpUtil.getString(BytedeskConstants.uid); + // + var body = json.encode({'tid': tid, 'uid': uid, 'client': client}); + final threadUrl = Uri.http( + BytedeskConstants.host, + '/api/v2/thread/unmark/unread', + ); + final initResponse = await this + .httpClient + .post(threadUrl, headers: getHeaders(), body: body); + + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + // print("responseJson $responseJson"); + // 判断token是否过期 + if (responseJson.toString().contains('invalid_token')) { + bytedeskEventBus.fire(InvalidTokenEventBus()); + } + + return MarkThreadResult.fromJson(responseJson); + } + + // 删除会话 + Future delete(String? tid) async { + // + var body = json.encode({"tid": tid, "client": client}); + final initUrl = Uri.http(BytedeskConstants.host, '/api/thread/delete'); + final initResponse = + await this.httpClient.post(initUrl, headers: getHeaders(), body: body); + + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + print("responseJson $responseJson"); + + // 判断token是否过期 + if (responseJson.toString().contains('invalid_token')) { + bytedeskEventBus.fire(InvalidTokenEventBus()); + } + + return MarkThreadResult.fromJson(responseJson); + } +} diff --git a/bytedesk_kefu/lib/http/bytedesk_ticket_api.dart b/bytedesk_kefu/lib/http/bytedesk_ticket_api.dart new file mode 100755 index 0000000..8668b17 --- /dev/null +++ b/bytedesk_kefu/lib/http/bytedesk_ticket_api.dart @@ -0,0 +1,11 @@ +// import 'dart:io'; + +import 'package:bytedesk_kefu/http/bytedesk_base_api.dart'; +// import 'package:bytedesk_kefu/util/bytedesk_constants.dart'; +// import 'package:bytedesk_kefu/util/bytedesk_utils.dart'; +// import 'package:http/http.dart' as http; + +class BytedeskTicketHttpApi extends BytedeskBaseHttpApi { + // + +} diff --git a/bytedesk_kefu/lib/http/bytedesk_user_api.dart b/bytedesk_kefu/lib/http/bytedesk_user_api.dart new file mode 100755 index 0000000..c067912 --- /dev/null +++ b/bytedesk_kefu/lib/http/bytedesk_user_api.dart @@ -0,0 +1,806 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:bytedesk_kefu/http/bytedesk_base_api.dart'; +import 'package:bytedesk_kefu/model/app.dart'; +import 'package:bytedesk_kefu/model/model.dart'; +import 'package:bytedesk_kefu/model/userJsonResult.dart'; +import 'package:bytedesk_kefu/model/wechatResult.dart'; +import 'package:bytedesk_kefu/util/bytedesk_constants.dart'; +import 'package:bytedesk_kefu/util/bytedesk_events.dart'; +import 'package:bytedesk_kefu/util/bytedesk_utils.dart'; +// import 'package:bytedesk_kefu/util/bytedesk_utils.dart'; +import 'package:sp_util/sp_util.dart'; +import 'package:http/http.dart' as http; + +// +class BytedeskUserHttpApi extends BytedeskBaseHttpApi { + // 授权 + Future oauth(String? username, String? password) async { + // final oauthUrl = '$baseUrl/oauth/token'; + var oauthUrl = Uri.http(BytedeskConstants.host, '/oauth/token'); + // print("http api client: oauthUrl $oauthUrl"); + Map headers = { + "Authorization": "Basic Y2xpZW50OnNlY3JldA==" + }; + Map bodyMap = { + "username": "$username", + "password": "$password", + "grant_type": "password", + "scope": "all" + }; + final oauthResponse = + await this.httpClient.post(oauthUrl, headers: headers, body: bodyMap); + // print('oauth result: $oauthResponse'); + // check the status code for the result + int statusCode = oauthResponse.statusCode; + // print("statusCode $statusCode"); + // 200: 授权成功,否则授权失败 + final oauthJson = jsonDecode(oauthResponse.body); + SpUtil.putBool(BytedeskConstants.isLogin, true); + SpUtil.putString(BytedeskConstants.accessToken, oauthJson['access_token']); + // + return OAuth.fromJson(statusCode, oauthJson); + } + + // 验证码登录 + Future smsOAuth(String? mobile, String? code) async { + // + // final oauthUrl = '$baseUrl/mobile/token'; + // final oauthUrl = Uri.http(BytedeskConstants.host, '/mobile/token'); + final oauthUrl = Uri.http(BytedeskConstants.host, '/mobile/token'); + // print("http api client: oauthUrl $oauthUrl"); + Map headers = { + "Authorization": "Basic Y2xpZW50OnNlY3JldA==" + }; + Map bodyMap = { + "mobile": "$mobile", + "code": "$code", + "grant_type": "mobile", + "scope": "all" + }; + // + final oauthResponse = + await this.httpClient.post(oauthUrl, headers: headers, body: bodyMap); + // print('oauth result: $oauthResponse'); + int statusCode = oauthResponse.statusCode; + // 200: 授权成功,否则授权失败 + final oauthJson = jsonDecode(oauthResponse.body); + print("oauthJson $oauthJson"); + if (statusCode == 200) { + SpUtil.putBool(BytedeskConstants.isLogin, true); + SpUtil.putBool(BytedeskConstants.isAuthenticated, true); + SpUtil.putString(BytedeskConstants.mobile, mobile!); + SpUtil.putString( + BytedeskConstants.accessToken, oauthJson['access_token']); + } + return OAuth.fromJson(statusCode, oauthJson); + } + + // 通过微信unionId登录 + Future unionIdOAuth(String? unionid) async { + // + // final oauthUrl = '$baseUrl/wechat/token'; + final oauthUrl = Uri.http(BytedeskConstants.host, '/wechat/token'); + // print("http api client: oauthUrl $oauthUrl"); + Map headers = { + "Authorization": "Basic Y2xpZW50OnNlY3JldA==" + }; + Map bodyMap = { + "unionid": "$unionid", + "grant_type": "wechat", + "scope": "all" + }; + // + final oauthResponse = + await this.httpClient.post(oauthUrl, headers: headers, body: bodyMap); + // print('oauth result: $oauthResponse'); + // check the status code for the result + int statusCode = oauthResponse.statusCode; + // print("statusCode $statusCode"); + // 200: 授权成功,否则授权失败 + final oauthJson = jsonDecode(oauthResponse.body); + print("oauthJson $oauthJson"); + if (statusCode == 200) { + SpUtil.putBool(BytedeskConstants.isLogin, true); + SpUtil.putBool(BytedeskConstants.isAuthenticated, true); + SpUtil.putString(BytedeskConstants.unionid, unionid!); + SpUtil.putString( + BytedeskConstants.accessToken, oauthJson['access_token']); + } + return OAuth.fromJson(statusCode, oauthJson); + } + + // 良师-手机号注册 + Future register(String? mobile, String? password) async { + // + Map headers = {"Content-Type": "application/json"}; + + var body = json.encode({ + "mobile": mobile, + "password": password, + "admin": false, // 学校端时,修改为true + "client": client + }); + + // final initUrl = '$baseUrl/visitors/api/v1/register/mobile'; + final initUrl = + Uri.http(BytedeskConstants.host, '/visitors/api/v1/register/mobile'); + final initResponse = + await this.httpClient.post(initUrl, headers: headers, body: body); + + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + // final responseJson = json.decode(initResponse.body); + // print("responseJson $responseJson"); + + return JsonResult.fromJson(responseJson); + } + + // 萝卜丝-访客端-注册匿名用户 + Future registerAnonymous(String? subDomain) async { + // + Map headers = {"Content-Type": "application/json"}; + // + final initUrl = Uri.http(BytedeskConstants.host, '/visitor/api/username', + {'subDomain': subDomain, 'client': client}); + final initResponse = await this.httpClient.get(initUrl, headers: headers); + + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + // print("responseJson $responseJson"); + // + User user = User.fromJson(responseJson['data']); + // + SpUtil.putString(BytedeskConstants.uid, user.uid!); + SpUtil.putString(BytedeskConstants.username, user.username!); + SpUtil.putString(BytedeskConstants.nickname, user.nickname!); + SpUtil.putString(BytedeskConstants.avatar, user.avatar!); + SpUtil.putString(BytedeskConstants.description, user.description!); + SpUtil.putString(BytedeskConstants.subDomain, user.subDomain!); + SpUtil.putString(BytedeskConstants.role, BytedeskConstants.ROLE_VISITOR); + // 解析用户资料 + return user; + } + + // 注册自定义普通用户:用于IM, + Future registerUser(String? username, String? nickname, + String? password, String? avatar, String? subDomain) async { + // + Map headers = {"Content-Type": "application/json"}; + var body = json.encode({ + "username": username, + "nickname": nickname, + "password": password, + "avatar": avatar, + "subDomain": subDomain, + "client": client + }); + // + final initUrl = + Uri.http(BytedeskConstants.host, '/visitor/api/register/user'); + final initResponse = + await this.httpClient.post(initUrl, headers: headers, body: body); + + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + // final responseJson = json.decode(initResponse.body); + print("responseJson $responseJson"); + // return JsonResult.fromJson(responseJson); + int statusCode = responseJson['status_code']; + if (statusCode == 200) { + // + User user = User.fromJson(responseJson['data']); + // + SpUtil.putString(BytedeskConstants.uid, user.uid!); + SpUtil.putString(BytedeskConstants.username, user.username!); + SpUtil.putString(BytedeskConstants.nickname, user.nickname!); + SpUtil.putString(BytedeskConstants.avatar, user.avatar!); + SpUtil.putString(BytedeskConstants.description, user.description!); + SpUtil.putString(BytedeskConstants.subDomain, user.subDomain!); + // 解析用户资料 + return user; + } + return new User(); + } + + // 修改密码 + Future changePassword(String? mobile, String? password) async { + Map headers = {"Content-Type": "application/json"}; + + var body = + json.encode({"mobile": mobile, "password": password, "client": client}); + + // final initUrl = '$baseUrl/visitors/api/v1/change'; + final initUrl = Uri.http(BytedeskConstants.host, '/visitors/api/v1/change'); + final initResponse = + await this.httpClient.post(initUrl, headers: headers, body: body); + + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + // final responseJson = json.decode(initResponse.body); + print("responseJson $responseJson"); + + return JsonResult.fromJson(responseJson); + } + + // 请求验证码 + Future requestCode(String? mobile) async { + // + Map headers = {"Content-Type": "application/json"}; + // + // final initUrl = + // '$baseUrl/sms/api/send/liangshibao?mobile=$mobile&client=$client'; + final initUrl = Uri.http(BytedeskConstants.host, + '/sms/api/send/liangshibao', {'mobile': mobile, 'client': client}); + final initResponse = await this.httpClient.get(initUrl, headers: headers); + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + print("responseJson $responseJson"); + + SpUtil.putBool(BytedeskConstants.exist, responseJson['data']['exist']); + SpUtil.putString(BytedeskConstants.code, responseJson['data']['code']); + + return CodeResult.fromJson(responseJson); + } + + // 绑定手机号 + Future bindMobile(String? mobile) async { + // + String? uid = SpUtil.getString(BytedeskConstants.uid); + // + var body = json.encode({"uid": uid, "mobile": mobile, "client": client}); + // + // final initUrl = '$baseUrl/api/user/bind/mobile'; + final initUrl = Uri.http(BytedeskConstants.host, '/api/user/bind/mobile'); + final initResponse = + await this.httpClient.post(initUrl, headers: getHeaders(), body: body); + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + // print("responseJson $responseJson"); + int statusCode = responseJson['status_code']; + if (statusCode == 200) { + SpUtil.putBool(BytedeskConstants.isAuthenticated, true); + SpUtil.putString(BytedeskConstants.mobile, mobile!); + SpUtil.putString(BytedeskConstants.nickname, '用户${mobile.substring(7)}'); + } + return JsonResult.fromJson(responseJson); + } + + /// 初始化 + Future getProfile() async { + // + // final initUrl = '$baseUrl/api/user/profile?client=$client'; + final initUrl = Uri.http( + BytedeskConstants.host, '/api/user/profile/simple', {'client': client}); + final initResponse = + await this.httpClient.get(initUrl, headers: getHeaders()); + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + print("responseJson $responseJson"); + // + User user = User.fromJson(responseJson['data']); + // + SpUtil.putString(BytedeskConstants.uid, user.uid!); + SpUtil.putString(BytedeskConstants.nickname, user.nickname!); + SpUtil.putString(BytedeskConstants.avatar, user.avatar!); + SpUtil.putString(BytedeskConstants.mobile, user.mobile ?? ''); + SpUtil.putString(BytedeskConstants.description, user.description!); + SpUtil.putString(BytedeskConstants.subDomain, user.subDomain!); + // TODO: 通知前端更新 + // 解析用户资料 + return user; + } + + // 更新昵称 + Future updateNickname(String? nickname) async { + // + var body = json.encode({"nickname": nickname, "client": client}); + // + // final initUrl = '$baseUrl/api/user/nickname'; + final initUrl = Uri.http(BytedeskConstants.host, '/api/user/nickname'); + final initResponse = + await this.httpClient.post(initUrl, headers: getHeaders(), body: body); + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + // print("responseJson $responseJson"); + // 更新本地数据 + SpUtil.putString(BytedeskConstants.nickname, nickname!); + + return User.fromJson(responseJson['data']); + } + + // 更新头像 + Future updateAvatar(String? avatar) async { + // + var body = json.encode({"avatar": avatar, "client": client}); + // + // final initUrl = '$baseUrl/api/user/avatar'; + final initUrl = Uri.http(BytedeskConstants.host, '/api/user/avatar'); + final initResponse = + await this.httpClient.post(initUrl, headers: getHeaders(), body: body); + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + // print("responseJson $responseJson"); + // 更新本地数据 + SpUtil.putString(BytedeskConstants.avatar, avatar!); + + return User.fromJson(responseJson['data']); + } + + // 更新个性签名 + Future updateDescription(String? description) async { + // + var body = json.encode({"description": description, "client": client}); + // + // final initUrl = '$baseUrl/api/user/description'; + final initUrl = Uri.http(BytedeskConstants.host, '/api/user/description'); + final initResponse = + await this.httpClient.post(initUrl, headers: getHeaders(), body: body); + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + // print("updateDescription $responseJson"); + // 更新本地数据 + SpUtil.putString(BytedeskConstants.description, description!); + + return User.fromJson(responseJson['data']); + } + + // 更新性别 + Future updateSex(bool? sex) async { + // + var body = json.encode({"sex": sex, "client": client}); + // + // final initUrl = '$baseUrl/api/user/sex'; + final initUrl = Uri.http(BytedeskConstants.host, '/api/user/sex'); + final initResponse = + await this.httpClient.post(initUrl, headers: getHeaders(), body: body); + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + // print("updateSex $responseJson"); + // 更新本地数据 + SpUtil.putBool(BytedeskConstants.sex, sex!); + + return User.fromJson(responseJson['data']); + } + + // 更新地区 + Future updateLocation(String? location) async { + // + var body = json.encode({"location": location, "client": client}); + // + // final initUrl = '$baseUrl/api/user/location'; + final initUrl = Uri.http(BytedeskConstants.host, '/api/user/location'); + final initResponse = + await this.httpClient.post(initUrl, headers: getHeaders(), body: body); + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + // print("updateLocation $responseJson"); + // 更新本地数据 + SpUtil.putString(BytedeskConstants.location, location!); + + return User.fromJson(responseJson['data']); + } + + // 更新生日 + Future updateBirthday(String? birthday) async { + // + var body = json.encode({"birthday": birthday, "client": client}); + // + // final initUrl = '$baseUrl/api/user/birthday'; + final initUrl = Uri.http(BytedeskConstants.host, '/api/user/birthday'); + final initResponse = + await this.httpClient.post(initUrl, headers: getHeaders(), body: body); + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + // print("updateBirthday $responseJson"); + // 更新本地数据 + SpUtil.putString(BytedeskConstants.birthday, birthday!); + + return User.fromJson(responseJson['data']); + } + + // 更新手机号 + Future updateMobile(String? mobile) async { + // + var body = json.encode({"mobile": mobile, "client": client}); + // + // final initUrl = '$baseUrl/api/user/mobile'; + final initUrl = Uri.http(BytedeskConstants.host, '/api/user/mobile'); + final initResponse = + await this.httpClient.post(initUrl, headers: getHeaders(), body: body); + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + // print("updateMobile $responseJson"); + // 更新本地数据 + SpUtil.putString(BytedeskConstants.mobile, mobile!); + + return User.fromJson(responseJson['data']); + } + + // https://pub.dev/documentation/http/latest/http/MultipartRequest-class.html + Future upload(String? filePath) async { + // + String? fileName = filePath!.split("/").last; + String? username = SpUtil.getString(BytedeskConstants.uid); + + final uploadUrl = '$baseUrl/visitor/api/upload/image'; + print("fileName $fileName, username $username, upload Url $uploadUrl"); + + var uri = Uri.parse(uploadUrl); + var request = http.MultipartRequest('POST', uri) + ..fields['file_name'] = fileName + ..fields['username'] = username! + ..files.add(await http.MultipartFile.fromPath('file', filePath)); + + http.Response response = + await http.Response.fromStream(await request.send()); + // print("Result: ${response.body}"); + + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = json.decode(utf8decoder.convert(response.bodyBytes)); + // print("responseJson $responseJson"); + // TODO: 根据status_code判断结果,并解析 + + String? url = responseJson['data']; + // print('url:' + url); + return url!; + } + + // 获取技能组在线状态 + Future getWorkGroupStatus(String? workGroupWid) async { + // + // final initUrl = + // '$baseUrl/api/status/workGroup?wid=$workGroupWid&client=$client'; + final initUrl = Uri.http(BytedeskConstants.host, '/api/status/workGroup', + {'wid': workGroupWid, 'client': client}); + final initResponse = + await this.httpClient.get(initUrl, headers: getHeaders()); + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + // print("responseJson $responseJson"); + // TODO: 根据status_code判断结果,并解析 + // 解析 + return responseJson['data']['status'].toString(); + } + + // 获取客服在线状态 + Future getAgentStatus(String? agentUid) async { + // + final initUrl = Uri.http(BytedeskConstants.host, '/api/status/agent', + {'uid': agentUid, 'client': client}); + final initResponse = + await this.httpClient.get(initUrl, headers: getHeaders()); + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + // print("responseJson $responseJson"); + // TODO: 根据status_code判断结果,并解析 + // 解析 + return responseJson['data']['status'].toString(); + } + + // 查询当前用户-某技能组wid或指定客服未读消息数目 + // 注意:技能组wid或指定客服唯一id + // 适用于 访客 和 客服 + Future getUnreadCount(String? wid) async { + // + final initUrl = Uri.http(BytedeskConstants.host, + '/api/messages/unreadCount', {'wid': wid, 'client': client}); + final initResponse = + await this.httpClient.get(initUrl, headers: getHeaders()); + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + // print("responseJson $responseJson"); + // TODO: 根据status_code判断结果,并解析 + // 解析 + return responseJson['data'].toString(); + } + + // 访客端-查询访客所有未读消息数目 + Future getUnreadCountVisitor() async { + // + final initUrl = Uri.http(BytedeskConstants.host, + '/api/messages/unreadCount/visitor', {'client': client}); + final initResponse = + await this.httpClient.get(initUrl, headers: getHeaders()); + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + // print("responseJson $responseJson"); + // TODO: 根据status_code判断结果,并解析 + // 解析 + return responseJson['data'].toString(); + } + + // 客服端-查询客服所有未读消息数目 + Future getUnreadCountAgent() async { + // + final initUrl = Uri.http(BytedeskConstants.host, + '/api/messages/unreadCount/agent', {'client': client}); + final initResponse = + await this.httpClient.get(initUrl, headers: getHeaders()); + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + // print("responseJson $responseJson"); + // TODO: 根据status_code判断结果,并解析 + // 解析 + return responseJson['data'].toString(); + } + + // 检测是否有新版本 + Future checkAppVersion(String? appkey) async { + // + final initUrl = Uri.http(BytedeskConstants.host, '/api/app/version', + {'key': appkey, 'client': client}); + final initResponse = + await this.httpClient.get(initUrl, headers: getHeaders()); + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + print("responseJson $responseJson"); + // 判断token是否过期 + if (responseJson.toString().contains('invalid_token')) { + bytedeskEventBus.fire(InvalidTokenEventBus()); + } + // TODO: 根据status_code判断结果,并解析 + // 解析 + return App.fromJson(responseJson['data']); + } + + // 通过token获取手机号 + Future getAliyunOneKeyLoginMobile(String? token) async { + // + final initUrl = Uri.http(BytedeskConstants.host, '/aliyun/mobile', + {'token': token, 'client': client}); + final initResponse = + await this.httpClient.get(initUrl, headers: getHeaders()); + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + // print("responseJson $responseJson"); + // 解析 + return responseJson['data'].toString(); + } + + // 微信登录之后,获取微信用户信息 + Future getWechatUserinfo(String? code) async { + // + Map headers = { + HttpHeaders.contentTypeHeader: "application/json", + }; + final initUrl = Uri.http(BytedeskConstants.host, + '/visitor/api/lsb/app/wechat/info', {'code': code, 'client': client}); + final initResponse = await this.httpClient.get(initUrl, headers: headers); + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + print("responseJson $responseJson"); + // 解析 + return WeChatResult.fromJson(responseJson); + } + + // 手机端注册微信登录用户-绑定手机号 + Future registerWechatMobile(String? mobile, String? nickname, + String? avatar, String? unionid, String? openid) async { + // + Map headers = { + HttpHeaders.contentTypeHeader: "application/json" + }; + // + var body = json.encode({ + "mobile": mobile, + "nickname": nickname, + "avatar": avatar, + "unionid": unionid, + "openid": openid, + "admin": false, + "client": client + }); + // + final initUrl = + Uri.http(BytedeskConstants.host, '/visitor/api/register/wechat/mobile'); + final initResponse = + await this.httpClient.post(initUrl, headers: headers, body: body); + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + // print("responseJson $responseJson"); + // + UserJsonResult userJsonResult = UserJsonResult.fromJson(responseJson); + if (userJsonResult.statusCode == 200) { + SpUtil.putString(BytedeskConstants.mobile, mobile!); + SpUtil.putString(BytedeskConstants.nickname, nickname!); + SpUtil.putString(BytedeskConstants.avatar, avatar!); + SpUtil.putString(BytedeskConstants.unionid, unionid!); + SpUtil.putString(BytedeskConstants.openid, openid!); + } + // 新账号,mqtt需要重连 + return userJsonResult; + } + + // 将unionid绑定到已经存在的手机账号 + Future bindWeChatMobile( + String? mobile, String? unionid) async { + // + Map headers = { + HttpHeaders.contentTypeHeader: "application/json" + }; + // + var body = + json.encode({"mobile": mobile, "unionid": unionid, "client": client}); + // + // final initUrl = '$baseUrl/visitor/api/bind/wechat/mobile'; + final initUrl = + Uri.http(BytedeskConstants.host, '/visitor/api/bind/wechat/mobile'); + final initResponse = + await this.httpClient.post(initUrl, headers: headers, body: body); + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + print("responseJson $responseJson"); + // + UserJsonResult userJsonResult = UserJsonResult.fromJson(responseJson); + if (userJsonResult.statusCode == 200) { + SpUtil.putString(BytedeskConstants.mobile, mobile!); + SpUtil.putString(BytedeskConstants.unionid, unionid!); + SpUtil.putBool(BytedeskConstants.isAuthenticated, true); + } + // + return userJsonResult; + } + + // 查询是否已经关注 + Future isFollowed(String? uid) async { + // + final initUrl = Uri.http(BytedeskConstants.host, '/api/user/isfollowed', + {'uid': uid, 'client': client}); + final initResponse = + await this.httpClient.get(initUrl, headers: getHeaders()); + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + // print("responseJson $responseJson"); + // 解析 + return responseJson['data']; + } + + // 关注 + Future follow(String? uid) async { + // + var body = json.encode({"uid": uid, "client": client}); + final initUrl = Uri.http(BytedeskConstants.host, '/api/user/follow'); + final initResponse = + await this.httpClient.post(initUrl, headers: getHeaders(), body: body); + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + print("responseJson $responseJson"); + // + return JsonResult.fromJson(responseJson); + } + + // 取消关注 + Future unfollow(String? uid) async { + // + var body = json.encode({"uid": uid, "client": client}); + final initUrl = Uri.http(BytedeskConstants.host, '/api/user/unfollow'); + final initResponse = + await this.httpClient.post(initUrl, headers: getHeaders(), body: body); + //解决json解析中的乱码问题 + Utf8Decoder utf8decoder = Utf8Decoder(); // fix 中文乱码 + //将string类型数据 转换为json类型的数据 + final responseJson = + json.decode(utf8decoder.convert(initResponse.bodyBytes)); + print("responseJson $responseJson"); + // + return JsonResult.fromJson(responseJson); + } + + // 退出登录 + Future logout() async { + String? accessToken = SpUtil.getString(BytedeskConstants.accessToken); + Map headers = {"Content-Type": "application/json"}; + + var body = json.encode({"client": client}); + + // final initUrl = '$baseUrl/api/user/logout?access_token=$accessToken'; + final initUrl = Uri.http(BytedeskConstants.host, '/api/user/logout', + {'access_token': accessToken}); + final initResponse = + await this.httpClient.post(initUrl, headers: headers, body: body); + + final responseJson = json.decode(initResponse.body); + print("responseJson $responseJson"); + // + // Preference.clearAccessToken(); + BytedeskUtils.clearUserCache(); + // + // SpUtil.putString(BytedeskConstants.uid, ''); + // SpUtil.putString(BytedeskConstants.username, ''); + // SpUtil.putString(BytedeskConstants.nickname, ''); + // SpUtil.putString(BytedeskConstants.avatar, ''); + // SpUtil.putString(BytedeskConstants.description, ''); + // SpUtil.putString(BytedeskConstants.subDomain, ''); + // SpUtil.putString(BytedeskConstants.role, ''); + // // + // SpUtil.putString(BytedeskConstants.unionid, ''); + // SpUtil.putString(BytedeskConstants.openid, ''); + // // + // SpUtil.putBool(BytedeskConstants.isLogin, false); + // SpUtil.putBool(BytedeskConstants.isAuthenticated, false); + // SpUtil.putString(BytedeskConstants.mobile, ''); + // SpUtil.putString(BytedeskConstants.accessToken, ''); + } +} diff --git a/bytedesk_kefu/lib/model/answer.dart b/bytedesk_kefu/lib/model/answer.dart new file mode 100755 index 0000000..781d02b --- /dev/null +++ b/bytedesk_kefu/lib/model/answer.dart @@ -0,0 +1,21 @@ +import 'package:equatable/equatable.dart'; + +class Answer extends Equatable { + // + final String? aid; + final String? question; + final String? answer; + // + Answer({this.aid, this.question, this.answer}) : super(); + // + static Answer fromJson(dynamic json) { + // print('aid:' + json['aid']); + // print('question:' + json['question']); + return Answer( + aid: json['aid'], question: json['question'], answer: json['answer']); + } + + // + @override + List get props => [aid!]; +} diff --git a/bytedesk_kefu/lib/model/app.dart b/bytedesk_kefu/lib/model/app.dart new file mode 100755 index 0000000..49fa15f --- /dev/null +++ b/bytedesk_kefu/lib/model/app.dart @@ -0,0 +1,56 @@ +class App { + // 唯一数字id,保证唯一性 + String? aid; + // 名称 + String? name; + // + int? versionCode; + // 版本 + String? version; + // 头像 + String? avatar; + // 网站url,或者app下载url + String? url; + // key为关键字 + // String? key; + // 类型:网站、App + // String? type; + // android、ios 或者 both + // String? platform; + // 上线、测试、新版本 + String? status; + // 升级新版tip + String? tip; + + /// 是否强制升级 + bool? forceUpgrade; + // 描述,介绍 + // String? description; + + // + App( + {this.aid, + this.name, + this.versionCode, + this.version, + this.avatar, + this.url, + this.status, + this.tip, + this.forceUpgrade}) + : super(); + + // + static App fromJson(dynamic app) { + return App( + aid: app['aid'], + name: app['name'], + versionCode: app['versionCode'], + version: app['version'], + avatar: app['avatar'], + url: app['url'], + status: app['status'], + tip: app['tip'], + forceUpgrade: app['forceUpgrade']); + } +} diff --git a/bytedesk_kefu/lib/model/codeResult.dart b/bytedesk_kefu/lib/model/codeResult.dart new file mode 100755 index 0000000..9fd4364 --- /dev/null +++ b/bytedesk_kefu/lib/model/codeResult.dart @@ -0,0 +1,23 @@ +import 'package:equatable/equatable.dart'; + +class CodeResult extends Equatable { + // + final String? message; + final int? statusCode; + final bool? exist; + final String? code; + + CodeResult({this.message, this.statusCode, this.exist, this.code}) : super(); + + static CodeResult fromJson(dynamic json) { + return CodeResult( + message: json["message"], + statusCode: json["status_code"], + exist: json["data"]["exist"], + code: json["data"]["code"]); + } + + @override + List get props => + [this.message!, this.statusCode!, this.exist!, this.code!]; +} diff --git a/bytedesk_kefu/lib/model/contact.dart b/bytedesk_kefu/lib/model/contact.dart new file mode 100755 index 0000000..7b40790 --- /dev/null +++ b/bytedesk_kefu/lib/model/contact.dart @@ -0,0 +1,39 @@ +// import 'package:equatable/equatable.dart'; +// import 'package:azlistview/azlistview.dart'; + +class Contact { + // extends ISuspensionBean + + final String? uid; + final String? username; + final String? nickname; + final String? avatar; + final String? description; + String? tagIndex; + String? namePinyin; + + Contact( + {this.uid, + this.username, + this.nickname, + this.avatar, + this.description, + this.tagIndex, + this.namePinyin}); + + // @override + // List get props => [uid, username, nickname, avatar, description, tagIndex, namePinyin]; + + static Contact fromJson(dynamic json) { + return Contact( + uid: json['uid'], + username: json['username'], + nickname: json['nickname'], + avatar: json['avatar'], + description: json['description']); + } + + // @override + // String? getSuspensionTag() => tagIndex; + +} diff --git a/bytedesk_kefu/lib/model/friend.dart b/bytedesk_kefu/lib/model/friend.dart new file mode 100755 index 0000000..bac8498 --- /dev/null +++ b/bytedesk_kefu/lib/model/friend.dart @@ -0,0 +1,59 @@ +// import 'package:equatable/equatable.dart'; +// import 'package:azlistview/azlistview.dart'; + +class Friend { + // extends ISuspensionBean + + final String? uid; + final String? username; + final String? nickname; + final String? avatar; + final String? description; + final String? mobile; + final double? latitude; + final double? longtitude; + String? tagIndex; + String? namePinyin; + + Friend( + {this.uid, + this.username, + this.nickname, + this.avatar, + this.description, + this.mobile, + this.latitude, + this.longtitude, + this.tagIndex, + this.namePinyin}); + + // @override + // List get props => [uid, username, nickname, avatar, description, tagIndex, namePinyin]; + + static Friend fromJson(dynamic json) { + return Friend( + uid: json['uid'], + username: json['username'], + nickname: json['nickname'], + avatar: json['avatar'], + description: json['description'], + mobile: json['mobile']); + } + + // TODO: 未显示距离 + static Friend fromElasticJson(dynamic json) { + return Friend( + uid: json['uid'], + username: json['username'], + nickname: json['nickname'], + avatar: json['avatar'], + description: json['description'], + mobile: json['mobile'], + latitude: json['location']['lat'], + longtitude: json['location']['lon']); + } + + // @override + // String? getSuspensionTag() => tagIndex; + +} diff --git a/bytedesk_kefu/lib/model/group.dart b/bytedesk_kefu/lib/model/group.dart new file mode 100755 index 0000000..f68c653 --- /dev/null +++ b/bytedesk_kefu/lib/model/group.dart @@ -0,0 +1,19 @@ +import 'package:equatable/equatable.dart'; + +class Group extends Equatable { + final String? gid; + final String? username; + final String? nickname; + + Group({this.gid, this.username, this.nickname}) : super(); + + static Group fromJson(dynamic json) { + return Group( + gid: json['gid'], + username: json['username'], + nickname: json['nickname']); + } + + @override + List get props => []; +} diff --git a/bytedesk_kefu/lib/model/helpArticle.dart b/bytedesk_kefu/lib/model/helpArticle.dart new file mode 100755 index 0000000..5c5a5f2 --- /dev/null +++ b/bytedesk_kefu/lib/model/helpArticle.dart @@ -0,0 +1,24 @@ +import 'package:equatable/equatable.dart'; + +class HelpArticle extends Equatable { + final int? id; + final String? aid; + final String? title; + final String? type; + final String? content; + + HelpArticle({this.id, this.aid, this.title, this.type, this.content}) + : super(); + + static HelpArticle fromJson(dynamic json) { + return HelpArticle( + id: json['id'], + aid: json['aid'], + title: json['title'], + type: json['type'], + content: json['content']); + } + + @override + List get props => [aid!]; +} diff --git a/bytedesk_kefu/lib/model/helpCategory.dart b/bytedesk_kefu/lib/model/helpCategory.dart new file mode 100755 index 0000000..68ef13f --- /dev/null +++ b/bytedesk_kefu/lib/model/helpCategory.dart @@ -0,0 +1,21 @@ +import 'package:equatable/equatable.dart'; + +class HelpCategory extends Equatable { + final int? id; + final String? cid; + final String? name; + final String? type; + + HelpCategory({this.id, this.cid, this.name, this.type}) : super(); + + static HelpCategory fromJson(dynamic json) { + return HelpCategory( + id: json['id'], + cid: json['cid'], + name: json['name'], + type: json['type']); + } + + @override + List get props => [cid!]; +} diff --git a/bytedesk_kefu/lib/model/jsonResult.dart b/bytedesk_kefu/lib/model/jsonResult.dart new file mode 100755 index 0000000..138a1d5 --- /dev/null +++ b/bytedesk_kefu/lib/model/jsonResult.dart @@ -0,0 +1,17 @@ +import 'package:equatable/equatable.dart'; + +class JsonResult extends Equatable { + final String? message; + final int? statusCode; + final String? data; + + JsonResult({this.message, this.statusCode, this.data}) : super(); + + static JsonResult fromJson(dynamic json) { + return JsonResult( + message: json["message"], statusCode: json["status_code"]); + } + + @override + List get props => [this.message!, this.statusCode!]; +} diff --git a/bytedesk_kefu/lib/model/markThread.dart b/bytedesk_kefu/lib/model/markThread.dart new file mode 100755 index 0000000..b51d39b --- /dev/null +++ b/bytedesk_kefu/lib/model/markThread.dart @@ -0,0 +1,20 @@ +import 'package:equatable/equatable.dart'; + +class MarkThreadResult extends Equatable { + // + final String? message; + final int? statusCode; + final String? tid; + + MarkThreadResult({this.message, this.statusCode, this.tid}) : super(); + + static MarkThreadResult fromJson(dynamic json) { + return MarkThreadResult( + message: json["message"], + statusCode: json["status_code"], + tid: json["data"]); + } + + @override + List get props => [this.tid!]; +} diff --git a/bytedesk_kefu/lib/model/message.dart b/bytedesk_kefu/lib/model/message.dart new file mode 100755 index 0000000..1f5a1eb --- /dev/null +++ b/bytedesk_kefu/lib/model/message.dart @@ -0,0 +1,175 @@ +import 'dart:convert'; + +import 'package:bytedesk_kefu/model/answer.dart'; +import 'package:bytedesk_kefu/model/thread.dart'; +import 'package:bytedesk_kefu/model/user.dart'; +import 'package:bytedesk_kefu/util/bytedesk_constants.dart'; +import 'package:sp_util/sp_util.dart'; +// import 'package:equatable/equatable.dart'; + +class Message { + // + String? mid; + String? content; + String? imageUrl; + String? voiceUrl; + String? videoUrl; + String? fileUrl; + String? nickname; + String? avatar; + String? type; + String? topic; + String? timestamp; + String? status; + int? isSend; + String? currentUid; + String? client; + // + Thread? thread; + User? user; + // + List? answers; + String? answersJson; + + Message( + {this.mid, + this.content, + this.imageUrl, + this.voiceUrl, + this.videoUrl, + this.fileUrl, + this.nickname, + this.avatar, + this.type, + this.topic, + this.timestamp, + this.isSend, + this.thread, + this.user, + this.status, + this.currentUid, + this.client, + this.answers, + this.answersJson}) + : super(); + + // + static Message fromJsonThread(dynamic json) { + // + // String? content = json['content']; + List robotQaList = []; + if (json['type'] == BytedeskConstants.MESSAGE_TYPE_ROBOT) { + robotQaList = json['answers'] == null + ? [] + : (json['answers'] as List) + .map((item) => Answer.fromJson(item)) + .toList(); + // for (var i = 0; i < robotQaList.length; i++) { + // Answer answer = robotQaList[i]; + // content += '\n\n' + answer.aid + ':' + answer.question; + // } + } + // + return Message( + mid: json['mid'], + content: json['content'], + imageUrl: json['imageUrl'], + voiceUrl: json['voiceUrl'], + fileUrl: json['fileUrl'], + videoUrl: json['videoOrShortUrl'], + nickname: json['user']['nickname'], + avatar: json['user']['avatar'], + type: json['type'], + timestamp: json['createdAt'], + status: 'stored', + isSend: 0, + currentUid: SpUtil.getString(BytedeskConstants.uid), + client: json['client'], + thread: Thread.fromVisitorJson(json['thread']), + user: User.fromJson(json['user']), + answers: robotQaList, + answersJson: json['answers'].toString()); + } + + static Message fromJson(dynamic json) { + List robotQaList = []; + if (json['type'] == BytedeskConstants.MESSAGE_TYPE_ROBOT) { + robotQaList = json['answers'] == null + ? [] + : (json['answers'] as List) + .map((item) => Answer.fromJson(item)) + .toList(); + } + return Message( + mid: json['mid'], + content: json['content'], + imageUrl: json['imageUrl'], + voiceUrl: json['voiceUrl'], + fileUrl: json['fileUrl'], + videoUrl: json['videoOrShortUrl'], + nickname: json['user']['nickname'], + avatar: json['user']['avatar'], + type: json['type'], + timestamp: json['createdAt'], + client: json['client'], + currentUid: SpUtil.getString(BytedeskConstants.uid), + answers: robotQaList, + answersJson: json['answers'].toString()); + } + + // @override + // List get props => [mid]; + + // Convert a Message into a Map. The keys must correspond to the names of the + // columns in the database. + Map toMap() { + return { + 'mid': mid, + 'content': content, + 'imageUrl': imageUrl, + 'voiceUrl': voiceUrl, + 'videoUrl': videoUrl, + 'fileUrl': fileUrl, + 'nickname': nickname, + 'avatar': avatar, + 'type': type, + 'topic': thread?.topic, + 'status': status, + 'timestamp': timestamp, + 'isSend': isSend, + 'currentUid': currentUid, + 'client': client, + 'answers': answersJson + }; + } + + Message.fromMap(Map map) { + mid = map['mid']; + content = map['content']; + imageUrl = map['imageUrl']; + voiceUrl = map['voiceUrl']; + videoUrl = map['videoUrl']; + fileUrl = map['fileUrl']; + nickname = map['nickname']; + avatar = map['avatar']; + type = map['type']; + topic = map['topic']; + status = map['status']; + timestamp = map['timestamp']; + isSend = map['isSend']; + client = map['client']; + currentUid = SpUtil.getString(BytedeskConstants.uid); + } + + String? channelTitle() { + return json.decode(content!)['title']; + } + + String? channelType() { + return json.decode(content!)['type']; + } + + String? channelContent() { + return json.decode(content!)['content']; + } +} diff --git a/bytedesk_kefu/lib/model/messageProvider.dart b/bytedesk_kefu/lib/model/messageProvider.dart new file mode 100755 index 0000000..ed13da1 --- /dev/null +++ b/bytedesk_kefu/lib/model/messageProvider.dart @@ -0,0 +1,168 @@ +import 'dart:async'; + +import 'package:bytedesk_kefu/model/answer.dart'; +import 'package:bytedesk_kefu/util/bytedesk_constants.dart'; +import 'package:bytedesk_kefu/util/bytedesk_utils.dart'; +import 'package:path/path.dart'; +import 'package:sqflite/sqflite.dart'; +import 'package:bytedesk_kefu/model/model.dart'; + +// https://pub.dev/packages/sqflite +// FIXME: 不支持web +class MessageProvider { + // + static final MessageProvider _singleton = MessageProvider._internal(); + factory MessageProvider() { + return _singleton; + } + MessageProvider._internal() { + // FIXME: 暂不支持web + if (BytedeskUtils.isWeb) { + return; + } + open(); + } + // + final String? tableMessage = 'messages'; + final String? columnId = '_id'; + final String? columnMid = 'mid'; + final String? columnType = 'type'; + final String? columnTopic = 'topic'; + final String? columnContent = 'content'; + final String? columnImageUrl = 'imageUrl'; + final String? columnVoiceUrl = 'voiceUrl'; + final String? columnVideoUrl = 'videoUrl'; + final String? columnFileUrl = 'fileUrl'; + + final String? columnNickname = 'nickname'; + final String? columnAvatar = 'avatar'; + + final String? columnStatus = 'status'; + final String? columnIsSend = 'isSend'; + final String? columnTimestamp = 'timestamp'; + final String? columnCurrentUid = 'currentUid'; + final String? columnClient = 'client'; + final String? columnAnswers = 'answers'; + // + Database? database; + + Future open() async { + // Open the database and store the reference. + database = await openDatabase( + // Set the path to the database. Note: Using the `join` function from the + // `path` package is best practice to ensure the path is correctly + // constructed for each platform. + join(await getDatabasesPath(), 'bytedesk-message-v9.db'), + // When the database is first created, create a table to store dogs. + onCreate: (db, version) { + // Run the CREATE TABLE statement on the database. autoincrement + return db.execute( + "CREATE TABLE $tableMessage($columnId INTEGER PRIMARY KEY AUTOINCREMENT, " + + "$columnMid TEXT, $columnType TEXT, $columnTopic TEXT, $columnContent TEXT, $columnImageUrl TEXT, $columnVoiceUrl TEXT,$columnVideoUrl TEXT, $columnFileUrl TEXT, $columnNickname TEXT, $columnAvatar TEXT, $columnStatus TEXT, " + + "$columnIsSend INTEGER, $columnTimestamp TEXT, $columnCurrentUid TEXT, $columnClient TEXT, $columnAnswers TEXT)", + ); + }, + // Set the version. This executes the onCreate function and provides a + // path to perform database upgrades and downgrades. + version: 9, + ); + // database path:/Users/ningjinpeng/Library/Developer/CoreSimulator/Devices/715CBA02-A602-4DE1-8C57-75A64B53BF03/data/Containers/Data/Application/8F46273D-9492-4C42-A618-4DF3815562BA/Documents/bytedesk-message-v9.db + // print('database path:' + database!.path); + } + + Future insert(Message message) async { + // FIXME: 暂不支持web + if (BytedeskUtils.isWeb) { + return 0; + } + // print('insert avatar:' + message.avatar + ' conten:' + message.content + ' timestamp:' + message.timestamp); + return await database!.insert(tableMessage!, message.toMap()); + } + + // + Future> getTopicMessages( + String? topic, String? currentUid, int? page, int? size) async { + // print('1: ' + topic! + ' currentUid:' + currentUid!); + // FIXME: 暂不支持web + if (BytedeskUtils.isWeb) { + return []; + } + // print('2'); + // + List maps = await database!.query(tableMessage!, + columns: [ + columnMid!, + columnContent!, + columnImageUrl!, + columnVoiceUrl!, + columnVideoUrl!, + columnFileUrl!, + columnType!, + columnTopic!, + columnStatus!, + columnTimestamp!, + columnNickname!, + columnAvatar!, + columnIsSend!, + columnClient! + ], + where: '$columnTopic = ? and $columnCurrentUid = ?', + whereArgs: [topic, currentUid], + orderBy: '$columnTimestamp DESC, $columnIsSend ASC', + limit: size, + offset: page! * size!); + // + // print('3'); + // print(maps.length); + // + return List.generate(maps.length, (i) { + // + List robotQaList = []; + if (maps[i]['type'] == BytedeskConstants.MESSAGE_TYPE_ROBOT) { + robotQaList = maps[i]['answers'] == null + ? [] + : (maps[i]['answers'] as List) + .map((item) => Answer.fromJson(item)) + .toList(); + } + // print('4'); + return Message( + mid: maps[i]['mid'], + content: maps[i]['content'], + imageUrl: maps[i]['imageUrl'], + voiceUrl: maps[i]['voiceUrl'], + videoUrl: maps[i]['videoUrl'], + fileUrl: maps[i]['fileUrl'], + type: maps[i]['type'], + topic: maps[i]['topic'], + status: maps[i]['status'], + timestamp: maps[i]['timestamp'], + nickname: maps[i]['nickname'], + avatar: maps[i]['avatar'], + isSend: maps[i]['isSend'], + client: maps[i]['client'], + answers: robotQaList); + }); + } + + Future delete(String? mid) async { + // FIXME: 暂不支持web + if (BytedeskUtils.isWeb) { + return 0; + } + return await database! + .delete(tableMessage!, where: '$columnMid = ?', whereArgs: [mid]); + } + + Future update(String? mid, String? status) async { + // FIXME: 暂不支持web + if (BytedeskUtils.isWeb) { + return 0; + } + return await database!.rawUpdate( + 'UPDATE $tableMessage SET $columnStatus = ? WHERE $columnMid = ?', + [status, mid]); + } + + Future close() async => database!.close(); +} diff --git a/bytedesk_kefu/lib/model/model.dart b/bytedesk_kefu/lib/model/model.dart new file mode 100755 index 0000000..fa35188 --- /dev/null +++ b/bytedesk_kefu/lib/model/model.dart @@ -0,0 +1,13 @@ +export './oauth.dart'; +export './user.dart'; +export './group.dart'; +export './message.dart'; +export './queue.dart'; +export './thread.dart'; +export './workGroup.dart'; +export './codeResult.dart'; +export './jsonResult.dart'; +export './requestAnswer.dart'; +export './requestThread.dart'; +export './helpArticle.dart'; +export './helpCategory.dart'; diff --git a/bytedesk_kefu/lib/model/oauth.dart b/bytedesk_kefu/lib/model/oauth.dart new file mode 100755 index 0000000..504aeee --- /dev/null +++ b/bytedesk_kefu/lib/model/oauth.dart @@ -0,0 +1,35 @@ +import 'package:equatable/equatable.dart'; + +class OAuth extends Equatable { + final int? statusCode; + final String? accessToken; + final int? expiresIn; + final String? jti; + final String? refreshToken; + final String? scope; + final String? tokenType; + + OAuth( + {this.statusCode, + this.accessToken, + this.expiresIn, + this.jti, + this.refreshToken, + this.scope, + this.tokenType}) + : super(); + + static OAuth fromJson(int? statusCode, dynamic json) { + return OAuth( + statusCode: statusCode, + accessToken: json['access_token'], + expiresIn: json['expires_in'], + jti: json['jti'], + refreshToken: json['refresh_token'], + scope: json['scope'], + tokenType: json['token_type']); + } + + @override + List get props => [accessToken!]; +} diff --git a/bytedesk_kefu/lib/model/queue.dart b/bytedesk_kefu/lib/model/queue.dart new file mode 100755 index 0000000..2560dcf --- /dev/null +++ b/bytedesk_kefu/lib/model/queue.dart @@ -0,0 +1,17 @@ +import 'package:equatable/equatable.dart'; + +class Queue extends Equatable { + final String? qid; + final String? nickname; + final String? avatar; + + Queue({this.qid, this.nickname, this.avatar}) : super(); + + static Queue fromJson(dynamic json) { + return Queue( + qid: json['qid'], nickname: json['nickname'], avatar: json['avatar']); + } + + @override + List get props => []; +} diff --git a/bytedesk_kefu/lib/model/requestAnswer.dart b/bytedesk_kefu/lib/model/requestAnswer.dart new file mode 100755 index 0000000..c0a69c1 --- /dev/null +++ b/bytedesk_kefu/lib/model/requestAnswer.dart @@ -0,0 +1,31 @@ +import 'package:equatable/equatable.dart'; +import 'message.dart'; + +class RequestAnswerResult extends Equatable { + final String? message; + final int? statusCode; + // + final Message? query; + final Message? anwser; + + RequestAnswerResult({this.message, this.statusCode, this.query, this.anwser}) + : super(); + + static RequestAnswerResult fromJson(dynamic json) { + return RequestAnswerResult( + message: json["message"], + statusCode: json["status_code"], + query: Message.fromJsonThread(json["data"]["query"]), + anwser: Message.fromJsonThread(json["data"]["reply"])); + } + + static RequestAnswerResult fromRateJson(dynamic json) { + return RequestAnswerResult( + message: json["message"], + statusCode: json["status_code"], + anwser: Message.fromJsonThread(json["data"])); + } + + @override + List get props => [this.statusCode!]; +} diff --git a/bytedesk_kefu/lib/model/requestThread.dart b/bytedesk_kefu/lib/model/requestThread.dart new file mode 100755 index 0000000..d987942 --- /dev/null +++ b/bytedesk_kefu/lib/model/requestThread.dart @@ -0,0 +1,21 @@ +import 'package:equatable/equatable.dart'; +import 'message.dart'; + +class RequestThreadResult extends Equatable { + final String? message; + final int? statusCode; + // + final Message? msg; + + RequestThreadResult({this.message, this.statusCode, this.msg}) : super(); + + static RequestThreadResult fromJson(dynamic json) { + return RequestThreadResult( + message: json["message"], + statusCode: json["status_code"], + msg: Message.fromJsonThread(json["data"])); + } + + @override + List get props => [this.msg!.mid!]; +} diff --git a/bytedesk_kefu/lib/model/thread.dart b/bytedesk_kefu/lib/model/thread.dart new file mode 100755 index 0000000..a730fe7 --- /dev/null +++ b/bytedesk_kefu/lib/model/thread.dart @@ -0,0 +1,276 @@ +import 'package:bytedesk_kefu/util/bytedesk_constants.dart'; +import 'package:bytedesk_kefu/util/bytedesk_utils.dart'; +import 'package:equatable/equatable.dart'; + +class Thread extends Equatable { + // + String? tid; + String? topic; + String? wid; + String? uid; + String? nickname; + String? avatar; + String? content; + String? timestamp; + int? unreadCount; + String? type; + // + bool? current; + // + bool? top; + bool? topVisitor; + // + bool? nodisturb; + bool? nodisturbVisitor; + // + bool? unread; + bool? unreadVisitor; + // + String? client; + String? currentUid; + + Thread( + {this.tid, + this.topic, + this.wid, + this.uid, + this.nickname, + this.avatar, + this.content, + this.timestamp, + this.unreadCount, + this.type, + this.current, + this.top, + this.topVisitor, + this.nodisturb, + this.nodisturbVisitor, + this.unread, + this.unreadVisitor, + this.client}); + + static Thread fromWorkGroupJson(dynamic json) { + return Thread( + tid: json['tid'], + topic: json['topic'], + wid: json['workGroup']['wid'], + nickname: json['workGroup']['nickname'], + avatar: json['workGroup']['avatar'], + content: json['content'], + timestamp: BytedeskUtils.getTimeDuration(json['timestamp']), + unreadCount: json['unreadCount'], + type: json['type'], + current: json['current'], + top: json['top'], + topVisitor: json['topVisitor'], + nodisturb: json['nodisturb'], + nodisturbVisitor: json['nodisturbVisitor'], + unread: json['unread'], + unreadVisitor: json['unreadVisitor']); + } + + static Thread fromWorkGroupJson2(dynamic json) { + if (json['type'] == BytedeskConstants.THREAD_TYPE_WORKGROUP) { + return Thread( + tid: json['tid'], + topic: json['topic'], + wid: json['workGroup']['wid'], + nickname: json['workGroup']['nickname'], + avatar: json['workGroup']['avatar'], + content: json['content'], + timestamp: json['timestamp'], + unreadCount: json['unreadCount'], + type: json['type'], + current: json['current'], + top: json['top'], + topVisitor: json['topVisitor'], + nodisturb: json['nodisturb'], + nodisturbVisitor: json['nodisturbVisitor'], + unread: json['unread'], + unreadVisitor: json['unreadVisitor']); + } else if (json['type'] == BytedeskConstants.THREAD_TYPE_APPOINTED) { + return Thread( + tid: json['tid'], + topic: json['topic'], + wid: json['agent']['uid'], + nickname: json['agent']['nickname'], + avatar: json['agent']['avatar'], + content: json['content'], + timestamp: json['timestamp'], + unreadCount: json['unreadCount'], + type: json['type'], + current: json['current'], + top: json['top'], + topVisitor: json['topVisitor'], + nodisturb: json['nodisturb'], + nodisturbVisitor: json['nodisturbVisitor'], + unread: json['unread'], + unreadVisitor: json['unreadVisitor']); + } else if (json['type'] == BytedeskConstants.THREAD_TYPE_CHANNEL) { + return Thread( + tid: json['tid'], + topic: json['topic'], + wid: json['channel']['cid'], + nickname: json['channel']['nickname'], + avatar: json['channel']['avatar'], + content: json['content'], + timestamp: json['timestamp'], + unreadCount: json['unreadCount'], + type: json['type'], + current: json['current'], + top: json['top'], + topVisitor: json['topVisitor'], + nodisturb: json['nodisturb'], + nodisturbVisitor: json['nodisturbVisitor'], + unread: json['unread'], + unreadVisitor: json['unreadVisitor']); + } + // 其他类型 + return Thread( + tid: json['tid'], + topic: json['topic'], + wid: json['admin']['uid'], + nickname: json['admin']['nickname'], + avatar: json['admin']['avatar'], + content: json['content'], + timestamp: json['timestamp'], + unreadCount: json['unreadCount'], + type: json['type'], + current: json['current'], + top: json['top'], + topVisitor: json['topVisitor'], + nodisturb: json['nodisturb'], + nodisturbVisitor: json['nodisturbVisitor'], + unread: json['unread'], + unreadVisitor: json['unreadVisitor']); + } + + static Thread fromHistoryJson(dynamic json) { + return Thread( + tid: json['tid'], + topic: json['topic'], + // wid: json['wid'], + nickname: json['nickname'], + avatar: json['avatar'], + content: json['content'], + timestamp: BytedeskUtils.getTimeDuration(json['timestamp']), + unreadCount: json['unreadCount'], + type: json['type'], + current: json['current'], + top: json['top'], + topVisitor: json['topVisitor'], + nodisturb: json['nodisturb'], + nodisturbVisitor: json['nodisturbVisitor'], + unread: json['unread'], + unreadVisitor: json['unreadVisitor']); + } + + static Thread fromVisitorJson(dynamic json) { + return Thread( + tid: json['tid'], + topic: json['topic'], + uid: json['visitor']['uid'], + nickname: json['visitor']['nickname'], + avatar: json['visitor']['avatar'], + content: json['content'], + timestamp: BytedeskUtils.getTimeDuration(json['timestamp']), + unreadCount: json['unreadCount'], + type: json['type'], + current: json['current'], + top: json['top'], + topVisitor: json['topVisitor'], + nodisturb: json['nodisturb'], + nodisturbVisitor: json['nodisturbVisitor'], + unread: json['unread'], + unreadVisitor: json['unreadVisitor']); + } + + static Thread fromContactJson(dynamic json) { + return Thread( + tid: json['tid'], + topic: json['topic'], + nickname: json['contact']['nickname'], + avatar: json['contact']['avatar'], + content: json['content'], + timestamp: BytedeskUtils.getTimeDuration(json['timestamp']), + unreadCount: json['unreadCount'], + type: json['type'], + current: json['current'], + top: json['top'], + topVisitor: json['topVisitor'], + nodisturb: json['nodisturb'], + nodisturbVisitor: json['nodisturbVisitor'], + unread: json['unread'], + unreadVisitor: json['unreadVisitor']); + } + + static Thread fromGroupJson(dynamic json) { + return Thread( + tid: json['tid'], + topic: json['topic'], + nickname: json['group']['nickname'], + avatar: json['group']['avatar'], + content: json['content'], + timestamp: BytedeskUtils.getTimeDuration(json['timestamp']), + unreadCount: json['unreadCount'], + type: json['type'], + current: json['current'], + top: json['top'], + topVisitor: json['topVisitor'], + nodisturb: json['nodisturb'], + nodisturbVisitor: json['nodisturbVisitor'], + unread: json['unread'], + unreadVisitor: json['unreadVisitor']); + } + + @override + List get props => [topic!]; + + // Convert a Thread into a Map. The keys must correspond to the names of the + // columns in the database. + Map toMap() { + return { + 'tid': tid, + 'topic': topic, + 'wid': wid, + 'uid': uid, + 'nickname': nickname, + 'avatar': avatar, + 'content': content, + 'timestamp': timestamp, + 'unreadCount': unreadCount, + 'type': type, + 'currentUid': currentUid, + 'client': client, + 'current': current, + 'top': top, + 'topVisitor': topVisitor, + 'nodisturb': nodisturb, + 'nodisturbVisitor': nodisturbVisitor, + 'unread': unread, + 'unreadVisitor': unreadVisitor + }; + } + + Thread.fromMap(Map map) { + tid = map['tid']; + topic = map['topic']; + wid = map['wid']; + uid = map['uid']; + nickname = map['nickname']; + avatar = map['avatar']; + content = map['content']; + timestamp = map['timestamp']; + unreadCount = map['unreadCount']; + type = map['type']; + current = map['current']; + top = map['top']; + topVisitor = map['topVisitor']; + nodisturb = map['nodisturb']; + nodisturbVisitor = map['nodisturbVisitor']; + unread = map['unread']; + unreadVisitor = map['unreadVisitor']; + currentUid = map['currentUid']; + client = map['client']; + } +} diff --git a/bytedesk_kefu/lib/model/threadProvider.dart b/bytedesk_kefu/lib/model/threadProvider.dart new file mode 100644 index 0000000..2edc3df --- /dev/null +++ b/bytedesk_kefu/lib/model/threadProvider.dart @@ -0,0 +1,119 @@ +import 'dart:async'; + +// import 'package:bytedesk_kefu/util/bytedesk_constants.dart'; +import 'package:path/path.dart'; +import 'package:sqflite/sqflite.dart'; +import 'package:bytedesk_kefu/model/model.dart'; + +// https://pub.dev/packages/sqflite +class ThreadProvider { + // + static final ThreadProvider _singleton = ThreadProvider._internal(); + factory ThreadProvider() { + return _singleton; + } + + ThreadProvider._internal() { + open(); + } + + // + final String? tableThread = 'threads'; + final String? columnId = '_id'; + final String? columnTid = 'tid'; + final String? columnTopic = 'topic'; + final String? columnWid = 'wid'; + final String? columnUid = 'uid'; + final String? columnNickname = 'nickname'; + final String? columnAvatar = 'avatar'; + final String? columnContent = 'content'; + final String? columnTimestamp = 'timestamp'; + final String? columnUnreadCount = 'unreadCount'; + final String? columnType = 'type'; + final String? columnClient = 'client'; + final String? columnCurrentUid = 'currentUid'; + // + Database? database; + + Future open() async { + // Open the database and store the reference. + database = await openDatabase( + // Set the path to the database. Note: Using the `join` function from the + // `path` package is best practice to ensure the path is correctly + // constructed for each platform. + join(await getDatabasesPath(), 'bytedesk-thread-v1.db'), + // When the database is first created, create a table to store dogs. + onCreate: (db, version) { + // Run the CREATE TABLE statement on the database. + return db.execute( + "CREATE TABLE $tableThread($columnId INTEGER PRIMARY KEY autoincrement, " + + "$columnTid TEXT, $columnTopic TEXT, $columnWid TEXT, $columnUid TEXT, $columnNickname TEXT, $columnAvatar TEXT, $columnContent TEXT, $columnTimestamp TEXT, " + + "$columnUnreadCount integer, $columnType TEXT, $columnClient TEXT, $columnCurrentUid TEXT)", + ); + }, + // Set the version. This executes the onCreate function and provides a + // path to perform database upgrades and downgrades. + version: 1, + ); + } + + Future insert(Thread thread) async { + // print('insert avatar:' + message.avatar + ' conten:' + message.content + ' timestamp:' + message.timestamp); + return await database!.insert(tableThread!, thread.toMap()); + } + + // + Future> getThreads(String? currentUid) async { + // + List maps = await database!.query( + tableThread!, + columns: [ + columnTid!, + columnTopic!, + columnWid!, + columnUid!, + columnNickname!, + columnAvatar!, + columnContent!, + columnTimestamp!, + columnType!, + columnUnreadCount!, + columnClient!, + ], + where: '$columnCurrentUid = ?', + whereArgs: [currentUid], + orderBy: '$columnTimestamp DESC', + // limit: size, + // offset: page * size + ); + // + return List.generate(maps.length, (i) { + // + return Thread( + tid: maps[i]['tid'], + topic: maps[i]['topic'], + wid: maps[i]['wid'], + uid: maps[i]['uid'], + nickname: maps[i]['nickname'], + avatar: maps[i]['avatar'], + content: maps[i]['content'], + timestamp: maps[i]['timestamp'], + type: maps[i]['type'], + unreadCount: maps[i]['unreadCount'], + client: maps[i]['client']); + }); + } + + Future delete(String? tid) async { + return await database! + .delete(tableThread!, where: '$columnTid = ?', whereArgs: [tid]); + } + + Future updateUnreadCount(String? tid) async { + return await database!.rawUpdate( + 'UPDATE $tableThread SET $columnUnreadCount = $columnUnreadCount + 1 WHERE $columnTid = ?', + [tid]); + } + + Future close() async => database!.close(); +} diff --git a/bytedesk_kefu/lib/model/uploadJsonResult.dart b/bytedesk_kefu/lib/model/uploadJsonResult.dart new file mode 100755 index 0000000..e6d01ed --- /dev/null +++ b/bytedesk_kefu/lib/model/uploadJsonResult.dart @@ -0,0 +1,19 @@ +import 'package:equatable/equatable.dart'; + +class UploadJsonResult extends Equatable { + final String? message; + final int? statusCode; + final String? url; + + UploadJsonResult({this.message, this.statusCode, this.url}) : super(); + + static UploadJsonResult fromJson(dynamic json) { + return UploadJsonResult( + message: json["message"], + statusCode: json["status_code"], + url: json['data']); + } + + @override + List get props => [this.url!]; +} diff --git a/bytedesk_kefu/lib/model/user.dart b/bytedesk_kefu/lib/model/user.dart new file mode 100755 index 0000000..f65e9c6 --- /dev/null +++ b/bytedesk_kefu/lib/model/user.dart @@ -0,0 +1,47 @@ +import 'package:equatable/equatable.dart'; + +class User extends Equatable { + final String? uid; + final String? username; + final String? nickname; + final String? avatar; + final String? mobile; + final String? description; + final bool? sex; + final String? location; + final String? birthday; + final String? subDomain; + + User( + {this.uid, + this.username, + this.nickname, + this.avatar, + this.mobile, + this.description, + this.sex, + this.location, + this.birthday, + this.subDomain}); + + @override + List get props => [uid!]; + + static User fromJson(dynamic json) { + return User( + uid: json['uid'], + username: json['username'], + nickname: json['nickname'], + avatar: json['avatar'], + mobile: json['mobile'], + description: json['description'], + sex: json['sex'], + location: json['location'], + birthday: json['birthday'], + subDomain: json['subDomain']); + } + + // static User fromProperties(String? uid, String? nickname, String? avatar) { + // return User() + // } +} diff --git a/bytedesk_kefu/lib/model/userJsonResult.dart b/bytedesk_kefu/lib/model/userJsonResult.dart new file mode 100755 index 0000000..58ab493 --- /dev/null +++ b/bytedesk_kefu/lib/model/userJsonResult.dart @@ -0,0 +1,21 @@ +import 'package:bytedesk_kefu/model/user.dart'; +import 'package:equatable/equatable.dart'; + +class UserJsonResult extends Equatable { + // + final String? message; + final int? statusCode; + final User? user; + + UserJsonResult({this.message, this.statusCode, this.user}) : super(); + + static UserJsonResult fromJson(dynamic json) { + return UserJsonResult( + message: json["message"], + statusCode: json["status_code"], + user: json["status_code"] == 200 ? User.fromJson(json['data']) : null); + } + + @override + List get props => [this.user!.uid!]; +} diff --git a/bytedesk_kefu/lib/model/wechatId.dart b/bytedesk_kefu/lib/model/wechatId.dart new file mode 100755 index 0000000..0cefc5e --- /dev/null +++ b/bytedesk_kefu/lib/model/wechatId.dart @@ -0,0 +1,39 @@ +import 'package:equatable/equatable.dart'; + +// {"access_token":"32_Lzn24923t_quQsCfKy7sl1kwbprKGI2kEYcvlBC5pANPOuiqLqF1S7L3Oeuj5nzP2CP4oSeNMlDmzDqEJsfpplRXku9CetoWx-MNaR1Pgxs", +// "expires_in":7200, +// "refresh_token":"32_r1s5XqFYRo0UTjcpPy5kS2pHvKV3KhQimK4v7aXhkO0_Vx94dGRS2vKrZXVtBtLy0fZkg_pUQQ7cIDZaYeSxJogoWoaXzDkyt9SxPXRI3nc", +// "openid":"oFyip1EJwsOQjXpsYWddVQh-LReM", +// "scope":"snsapi_login", +// "unionid":"os9j41CZ6hPITe_P9rIXwsLuR0JM"} +class WechatId extends Equatable { + // + final String? accessToken; + final int? expiresIn; + final String? refreshToken; + final String? openid; + final String? scope; + final String? unionid; + + WechatId( + {this.accessToken, + this.expiresIn, + this.refreshToken, + this.openid, + this.scope, + this.unionid}) + : super(); + + static WechatId fromJson(dynamic json) { + return WechatId( + accessToken: json['access_token'], + expiresIn: json['expires_in'], + refreshToken: json['refresh_token'], + openid: json['openid'], + scope: json['scope'], + unionid: json['unionid']); + } + + @override + List get props => [unionid!]; +} diff --git a/bytedesk_kefu/lib/model/wechatResult.dart b/bytedesk_kefu/lib/model/wechatResult.dart new file mode 100755 index 0000000..03d4670 --- /dev/null +++ b/bytedesk_kefu/lib/model/wechatResult.dart @@ -0,0 +1,31 @@ +import 'package:bytedesk_kefu/model/wechatId.dart'; +import 'package:bytedesk_kefu/model/wechatUserinfo.dart'; +import 'package:equatable/equatable.dart'; + +class WeChatResult extends Equatable { + // + final String? message; + final int? statusCode; + final WechatId? wechatId; + final WechatUserinfo? wechatUserinfo; + + WeChatResult( + {this.message, this.statusCode, this.wechatId, this.wechatUserinfo}) + : super(); + + static WeChatResult fromJson(dynamic json) { + return WeChatResult( + message: json["message"], + statusCode: json["status_code"], + wechatId: json["status_code"] == 200 + ? WechatId.fromJson(json['data']) + : null, // 此微信已经绑定 + wechatUserinfo: json["status_code"] == 201 + ? WechatUserinfo.fromJson(json['data']) + : null // 此微信首次登录 + ); + } + + @override + List get props => [this.wechatUserinfo!.openid!]; +} diff --git a/bytedesk_kefu/lib/model/wechatUserinfo.dart b/bytedesk_kefu/lib/model/wechatUserinfo.dart new file mode 100755 index 0000000..7e5823c --- /dev/null +++ b/bytedesk_kefu/lib/model/wechatUserinfo.dart @@ -0,0 +1,52 @@ +import 'package:equatable/equatable.dart'; + +// {"openid":"oFyip1EJwsOQjXpsYWddVQh-LReM", +// "nickname":"宁金鹏", +// "sex":1, +// "language":"zh_CN", +// "city":"Haidian", +// "province":"Beijing", +// "country":"CN", +// "headimgurl":"http:\/\/thirdwx.qlogo.cn\/mmopen\/vi_32\/DYAIOgq83erzHNgbAaT1qkWe2lUicBdAmqrSmOceA6eD2RFSUEV546ibt7SgHiaew3IxLGWFjzm8icB4wNXe5sCzBA\/132", +// "privilege":[], +// "unionid":"os9j41CZ6hPITe_P9rIXwsLuR0JM"} +class WechatUserinfo extends Equatable { + // + final String? openid; + final String? nickname; + final int? sex; + final String? language; + final String? city; + final String? province; + final String? country; + final String? headimgurl; + final String? unionid; + + WechatUserinfo( + {this.openid, + this.nickname, + this.sex, + this.language, + this.city, + this.province, + this.country, + this.headimgurl, + this.unionid}) + : super(); + + static WechatUserinfo fromJson(dynamic json) { + return WechatUserinfo( + openid: json['openid'], + nickname: json['nickname'], + sex: json['sex'], + language: json['language'], + city: json['city'], + province: json['province'], + country: json['country'], + headimgurl: json['headimgurl'], + unionid: json['unionid']); + } + + @override + List get props => [unionid!]; +} diff --git a/bytedesk_kefu/lib/model/workGroup.dart b/bytedesk_kefu/lib/model/workGroup.dart new file mode 100755 index 0000000..561b275 --- /dev/null +++ b/bytedesk_kefu/lib/model/workGroup.dart @@ -0,0 +1,15 @@ +import 'package:equatable/equatable.dart'; + +class WorkGroup extends Equatable { + final String? wid; + final String? nickname; + + WorkGroup({this.wid, this.nickname}) : super(); + + static WorkGroup fromJson(dynamic json) { + return WorkGroup(wid: json['wid'], nickname: json['nickname']); + } + + @override + List get props => []; +} diff --git a/bytedesk_kefu/lib/mqtt/bytedesk_mqtt.dart b/bytedesk_kefu/lib/mqtt/bytedesk_mqtt.dart new file mode 100755 index 0000000..39cc64a --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/bytedesk_mqtt.dart @@ -0,0 +1,1045 @@ +import 'dart:convert'; + +import 'package:bytedesk_kefu/bytedesk_kefu.dart'; +import 'package:bytedesk_kefu/model/message.dart'; +import 'package:bytedesk_kefu/model/thread.dart'; +import 'package:bytedesk_kefu/model/user.dart'; +// TODO: 条件引入web,格式类似:import 'src/configure_imp.dart' if (dart.library.html) 'src/configure_web.dart' as conf; +// ignore: uri_does_not_exist +import 'package:bytedesk_kefu/mqtt/lib/mqtt_browser_client.dart'; +import 'package:bytedesk_kefu/mqtt/lib/mqtt_client.dart'; +import 'package:bytedesk_kefu/mqtt/lib/mqtt_server_client.dart'; +import 'package:bytedesk_kefu/util/bytedesk_constants.dart'; +import 'package:bytedesk_kefu/model/messageProvider.dart'; +import 'package:bytedesk_kefu/util/bytedesk_events.dart'; +import 'package:bytedesk_kefu/util/bytedesk_extraparam.dart'; +import 'package:bytedesk_kefu/util/bytedesk_utils.dart'; +import 'package:bytedesk_kefu/util/bytedesk_uuid.dart'; +import 'package:sp_util/sp_util.dart'; +// FIXME: proto文件重新生成兼容null-safty报错 +// ignore: import_of_legacy_library_into_null_safe +import 'package:bytedesk_kefu/protobuf/message.pb.dart' as protomsg; +// ignore: import_of_legacy_library_into_null_safe +import 'package:bytedesk_kefu/protobuf/thread.pb.dart' as protothread; +// ignore: import_of_legacy_library_into_null_safe +import 'package:bytedesk_kefu/protobuf/user.pb.dart' as protouser; +import 'package:flutter/services.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +// import 'package:vibration/vibration.dart'; + +class BytedeskMqtt { + // + var mqttClient; + String? clientId; + int keepAlivePeriod = 20; + // final key = Key.fromUtf8('16BytesLengthKey'); + // final iv = IV.fromUtf8('A-16-Byte-String'); + MessageProvider messageProvider = new MessageProvider(); + List midList = []; + // bool _isConnected = false; + String? currentUid; + String? client; + + // 单例模式 + static final BytedeskMqtt _singleton = BytedeskMqtt._internal(); + factory BytedeskMqtt() { + return _singleton; + } + BytedeskMqtt._internal() { + _connect(); + } + // + void _connect() async { + // + reconnect(); + } + + void reconnect() async { + // eventbus发送广播,连接中... + bytedeskEventBus + .fire(ConnectionEventBus(BytedeskConstants.USER_STATUS_CONNECTING)); + // + currentUid = SpUtil.getString(BytedeskConstants.uid); + // String role = SpUtil.getString(BytedeskConstants.role, + // defValue: BytedeskConstants.ROLE_VISITOR); + client = BytedeskUtils.getClient(); + clientId = "$currentUid/$client"; + + //注意:必须要先判断web,否则在web运行会报错: + // Unsupported operation: Platform._operatingSystem + if (BytedeskUtils.isWeb) { + if (BytedeskConstants.isDebug) { + mqttClient = MqttBrowserClient.withPort( + 'ws://127.0.0.1/websocket', clientId!, 3885); + } else { + mqttClient = MqttBrowserClient.withPort( + BytedeskConstants.webSocketWssUrl, clientId!, 443); + } + } else { + if (BytedeskConstants.isWebSocketWss) { + mqttClient = + MqttServerClient(BytedeskConstants.webSocketWssUrl, clientId!); + mqttClient.useWebSocket = true; + mqttClient.port = 443; + } else { + mqttClient = MqttServerClient(BytedeskConstants.mqttHost, clientId!); + mqttClient.port = BytedeskConstants.mqttPort; + mqttClient.secure = BytedeskConstants.isSecure; + } + } + + // 启用3.1.1版本协议,否则clientId限制最大长度为23 + mqttClient.setProtocolV311(); + + /// Set logging on if needed, defaults to off + mqttClient.logging(on: false); // BytedeskConstants.isDebug + /// If you intend to use a keep alive value in your connect message that is not the default(60s) + /// you must set it here + mqttClient.keepAlivePeriod = keepAlivePeriod; + mqttClient.autoReconnect = true; // FIXME: + // mqttClient.onAutoReconnect = _onAutoReconnect; // FIXME: + mqttClient.onDisconnected = _onDisconnected; + mqttClient.onConnected = _onConnected; + mqttClient.onSubscribed = _onSubscribed; + mqttClient.onUnsubscribed = _onUnSubscribed; + mqttClient.onSubscribeFail = _onSubscribeFailed; + + /// Set a ping received callback if needed, called whenever a ping response(pong) is received from the broker. + mqttClient.pongCallback = _onPong; + + /// Create a connection message to use or use the default one. The default one sets the + /// client identifier, any supplied username/password, the default keepalive interval(60s) + /// and clean session, an example of a specific one below. + + final MqttConnectMessage connMess = MqttConnectMessage() + .withClientIdentifier(clientId!) + .authenticateAs('username', 'password'); // TODO: 服务器暂时不需要auth,随便填写 + // .keepAliveFor(keepAlivePeriod); // Must agree with the keep alive set above or not set + // 取消客户端设置,直接在服务器端统一内容格式推送 + // .withWillTopic('protobuf/lastWill/mqtt') // If you set this you must set a will message + // .withWillMessage('My Will message') + // .startClean() // Non persistent session for testing + // .withWillQos(MqttQos.atLeastOnce); + if (BytedeskConstants.isDebug) { + print('mqttClient connecting....'); + } + mqttClient.connectionMessage = connMess; + + /// Connect the client, any errors here are communicated by raising of the appropriate exception. Note + /// in some circumstances the broker will just disconnect us, see the spec about this, we however eill + /// never send malformed messages. + try { + await mqttClient.connect(); + } on Exception catch (e) { + print('mqttClient exception - $e'); + mqttClient.disconnect(); + } + + /// Check we are connected + if (mqttClient.connectionStatus.state == MqttConnectionState.connected) { + print('mqttClient connected'); + } else { + /// Use status here rather than state if you also want the broker return code. + print( + 'ERROR mqttClient connection failed - disconnecting, status is ${mqttClient.connectionStatus}'); + mqttClient.disconnect(); + // exit(-1); + } + + /// The client has a change notifier object(see the Observable class) which we then listen to to get + /// notifications of published updates to each subscribed topic. + // mqttClient.updates!.listen((List> c) { + // final MqttPublishMessage recMess = c[0].payload; + // final String pt = + // MqttPublishPayload.bytesToStringAsString(recMess.payload.message); + // // print('Change notification:: topic is <${c[0].topic}>, payload is <-- $pt -->'); + // }); + + /// 收到的消息 + // if (mqttClient != null && mqttClient.published != null) { + // + mqttClient.published!.listen((MqttPublishMessage messageBinary) { + // print('Published notification:: topic is ${messageBinary.variableHeader.topicName}, with Qos ${messageBinary.header.qos}'); + // + protomsg.Message messageProto = + protomsg.Message.fromBuffer(messageBinary.payload.message!); + // FIXME: 自己发送的消息显示两条?此处根据mid去个重 + var mid = messageProto.mid; + if (midList.contains(mid)) { + return; + } + midList.add(mid); + // + var uid = messageProto.user.uid; + var username = messageProto.user.username; + var nickname = messageProto.user.nickname; + var avatar = messageProto.user.avatar; + // + var content = ''; + var type = messageProto.type; + var timestamp = messageProto.timestamp; + var client = messageProto.client; + // + if (BytedeskConstants.isDebug) { + print('bytedesk_mqtt.dart receive type:' + + type + + ' client:' + + client + + ' mid:' + + mid); + } + // 非会话消息,如:会议通知等, 另行处理 + if (type == BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_NOTICE) { + // TODO: 待处理 + return; + } + // + User user = new User( + uid: uid, username: username, nickname: nickname, avatar: avatar); + // + Thread thread = new Thread( + tid: messageProto.thread.tid, + type: messageProto.thread.type, + // uid: '', + nickname: messageProto.thread.nickname, + avatar: messageProto.thread.avatar, + content: messageProto.thread.content, + timestamp: messageProto.thread.timestamp, + unreadCount: messageProto.thread.unreadCount, + topic: messageProto.thread.topic, + client: client); + Message message = new Message( + mid: mid, + content: content, + imageUrl: content, + nickname: nickname, + avatar: avatar, + type: type, + timestamp: timestamp, + status: 'stored', + isSend: uid == currentUid ? 1 : 0, + currentUid: currentUid, + thread: thread, + user: user); + // 是否发送消息回执 + // var autoReply = false; + var sendReceipt = false; + // var webRTCVideoInvite = false; + // var webRTCAudioInvite = false; + switch (type) { + case BytedeskConstants.MESSAGE_TYPE_TEXT: + { + // + // autoReply = true; + sendReceipt = true; + // TODO: 判断是否加密,暂时不需要 + message.content = messageProto.text.content; + break; + } + case BytedeskConstants.MESSAGE_TYPE_IMAGE: + { + // + // autoReply = true; + sendReceipt = true; + message.imageUrl = messageProto.image.imageUrl; + break; + } + case BytedeskConstants.MESSAGE_TYPE_VOICE: + { + // + // autoReply = true; + sendReceipt = true; + message.voiceUrl = messageProto.voice.voiceUrl; + break; + } + case BytedeskConstants.MESSAGE_TYPE_FILE: + { + // + // autoReply = true; + sendReceipt = true; + message.fileUrl = messageProto.file.fileUrl; + break; + } + case BytedeskConstants.MESSAGE_TYPE_VIDEO: + case BytedeskConstants.MESSAGE_TYPE_SHORT_VIDEO: + { + // + // autoReply = true; + sendReceipt = true; + message.videoUrl = messageProto.video.videoOrShortUrl; + break; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_THREAD: + { + message.content = messageProto.text.content; + break; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_THREAD_REENTRY: + case BytedeskConstants.MESSAGE_TYPE_COMMODITY: + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_CONNECT: + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_DISCONNECT: + { + message.content = messageProto.text.content; + break; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_QUEUE_ACCEPT: + { + // 替换 'joinQueueThread' + message.content = '接入队列会话'; + break; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_AGENT_CLOSE: + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_VISITOR_CLOSE: + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_AUTO_CLOSE: + { + message.content = messageProto.text.content; + break; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_TICKET: + { + // TODO: 工单消息 + message.content = messageProto.text.content; + break; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_FEEDBACK: + { + // TODO: 意见反馈 + message.content = messageProto.text.content; + break; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_CHANNEL: + { + // TODO: 渠道消息 + message.content = messageProto.text.content; + break; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_PREVIEW: + { + // 对方正在输入 + String uid = messageProto.user.uid; + String previewContent = messageProto.preview.content; + if (uid != currentUid) { + bytedeskEventBus + .fire(ReceiveMessagePreviewEventBus(previewContent)); + } + return; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_RECEIPT: + { + // 消息回执 + var midR = messageProto.receipt.mid; + var statusR = messageProto.receipt.status; + messageProvider.update(midR, statusR); + // 通知界面更新聊天记录状态 + bytedeskEventBus.fire(ReceiveMessageReceiptEventBus(midR, statusR)); + return; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_RECALL: + { + // 消息撤回 + String mid = messageProto.recall.mid; + bytedeskEventBus.fire(DeleteMessageEventBus(mid)); + return; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_TRANSFER: + { + // + return; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_TRANSFER_ACCEPT: + { + // + return; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_TRANSFER_REJECT: + { + // + return; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_INVITE: + { + // + return; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_INVITE_ACCEPT: + { + // + return; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_INVITE_REJECT: + { + // + return; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_INVITE_RATE: + { + // + return; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_RATE_RESULT: + { + // + return; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_WEBRTC_INVITE_VIDEO: + { + // + return; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_WEBRTC_INVITE_AUDIO: + { + // + return; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_WEBRTC_CANCEL: + { + // + return; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_WEBRTC_OFFER_VIDEO: + { + // + return; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_WEBRTC_OFFER_AUDIO: + { + // + return; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_WEBRTC_ANSWER: + { + // + return; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_WEBRTC_CANDIDATE: + { + // + return; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_WEBRTC_ACCEPT: + { + // + return; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_WEBRTC_REJECT: + { + // + return; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_WEBRTC_READY: + { + // + return; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_WEBRTC_BUSY: + { + // + return; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_WEBRTC_CLOSE: + { + // + return; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_GROUP_CREATE: + { + // + return; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_GROUP_UPDATE: + { + // + return; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_GROUP_ANNOUNCEMENT: + { + // + return; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_GROUP_INVITE: + { + // + return; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_GROUP_INVITE_ACCEPT: + { + // + return; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_GROUP_INVITE_REJECT: + { + // + return; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_GROUP_APPLY: + { + // + return; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_GROUP_APPLY_APPROVE: + { + // + return; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_GROUP_APPLY_DENY: + { + // + return; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_GROUP_KICK: + { + // + return; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_GROUP_MUTE: + { + // + return; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_GROUP_TRANSFER: + { + // + return; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_GROUP_TRANSFER_ACCEPT: + { + // + return; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_GROUP_TRANSFER_REJECT: + { + // + return; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_GROUP_WITHDRAW: + { + // + return; + } + case BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_GROUP_DISMISS: + { + // + return; + } + case BytedeskConstants.MESSAGE_TYPE_LOCATION: + { + // + return; + } + case BytedeskConstants.MESSAGE_TYPE_LINK: + { + // + return; + } + case BytedeskConstants.MESSAGE_TYPE_EVENT: + { + // + return; + } + case BytedeskConstants.MESSAGE_TYPE_CUSTOM: + { + // + return; + } + case BytedeskConstants.MESSAGE_TYPE_RED_PACKET: + { + // + return; + } + default: + print('other message type:' + type); + } + // 客服端使用 + // if (autoReply) { + // // + // } + // + if (sendReceipt && message.isSend == 0) { + // 发送送达回执 + sendReceiptReceivedMessage(mid, thread); + } + // + // final encrypter = Encrypter(AES(key, mode: AESMode.cbc)); + // final decrypted = encrypter.decrypt64(messageProto.text.content, iv: iv); + // print('content: ' + decrypted); + // + // 忽略连接信息 + if (message.content == 'visitorConnect' || + message.content == 'visitorDisconnect') { + return; + } + // 插入本地数据库 + // if (messageProvider != null) { + messageProvider.insert(message); + // } + // 通知界面显示聊天记录 + bytedeskEventBus.fire(ReceiveMessageEventBus(message)); + // 接收消息播放提示音,放到SDK外实现,迁移到demo中 + if (BytedeskKefu.getPlayAudioOnReceiveMessage()! && message.isSend == 0) { + // print('play audio'); + SystemSound.play(SystemSoundType.click); + } + // 振动,放到SDK外实现,迁移到demo中 + if (BytedeskKefu.getVibrateOnReceiveMessage()! && message.isSend == 0) { + // print('should vibrate'); + vibrate(); + } + }); + // + // } else { + // print('mqttClient.published is null'); + // } + } + + // FIXME: ld: library not found for -lvibration + void vibrate() async { + // if (await Vibration.hasVibrator()) { + // Vibration.vibrate(); + // } + } + + void subscribe(String topic) { + // print('Subscribing to the hello topic'); + mqttClient.subscribe(topic, MqttQos.exactlyOnce); + } + + void unsubscribe(String topic) { + mqttClient.unsubscribe(topic); + } + + void sendTextMessage(String content, Thread currentThread) { + publish(content, BytedeskConstants.MESSAGE_TYPE_TEXT, currentThread, null); + } + + void sendImageMessage(String content, Thread currentThread) { + publish(content, BytedeskConstants.MESSAGE_TYPE_IMAGE, currentThread, null); + } + + void sendFileMessage(String content, Thread currentThread) { + publish(content, BytedeskConstants.MESSAGE_TYPE_FILE, currentThread, null); + } + + void sendVoiceMessage(String content, Thread currentThread) { + publish(content, BytedeskConstants.MESSAGE_TYPE_VOICE, currentThread, null); + } + + void sendVideoMessage(String content, Thread currentThread) { + publish(content, BytedeskConstants.MESSAGE_TYPE_VIDEO, currentThread, null); + } + + // 商品消息 + void sendCommodityMessage(String content, Thread currentThread) { + publish( + content, BytedeskConstants.MESSAGE_TYPE_COMMODITY, currentThread, null); + } + + // 消息预知 + void sendPreviewMessage(String previewContent, Thread currentThread) { + publish(previewContent, BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_PREVIEW, + currentThread, null); + } + + // 消息撤回 + void sendRecallMessage(String mid, Thread currentThread) { + ExtraParam extraParam = new ExtraParam(); + extraParam.recallMid = mid; + publish(mid, BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_RECALL, + currentThread, extraParam); + } + + // 送达回执 + void sendReceiptReceivedMessage(String mid, Thread currentThread) { + sendReceiptMessage( + mid, BytedeskConstants.MESSAGE_STATUS_RECEIVED, currentThread); + } + + // 已读回执 + void sendReceiptReadMessage(String mid, Thread currentThread) { + sendReceiptMessage( + mid, BytedeskConstants.MESSAGE_STATUS_READ, currentThread); + } + + void sendReceiptMessage(String mid, String status, Thread currentThread) { + ExtraParam extraParam = new ExtraParam(); + extraParam.receiptMid = mid; + extraParam.receiptStatus = status; + publish('content', BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_RECEIPT, + currentThread, extraParam); + } + + void publish(String content, String type, Thread currentThread, + ExtraParam? extraParam) { + // if (currentThread == null) { + // print('连接客服失败,请退出页面重新进入。注意: 请在App启动的时候,调用init接口'); + // Fluttertoast.showToast(msg: '连接客服失败,请退出页面重新进入'); + // return; + // } + // https://pub.dev/packages/encrypt#aes + // final encrypter = Encrypter(AES(key, mode: AESMode.cbc)); + // final encrypted = encrypter.encrypt(content, iv: iv); + // print(encrypted.base64); + // final decrypted = encrypter.decrypt(encrypted, iv: iv); + // print(decrypted); + // thread + protothread.Thread thread = new protothread.Thread(); + thread.tid = currentThread.tid!; + thread.type = currentThread.type!; + thread.topic = currentThread.topic!; + thread.nickname = currentThread.nickname!; + thread.avatar = currentThread.avatar!; + thread.timestamp = BytedeskUtils.formatedDateNow(); // 没有必要填写,服务器端会填充 + thread.unreadCount = 0; + var extra = {'top': false, 'undisturb': false}; + thread.extra = jsonEncode(extra); + // user + protouser.User user = new protouser.User(); + user.uid = SpUtil.getString(BytedeskConstants.uid)!; + user.username = SpUtil.getString(BytedeskConstants.username)!; + user.nickname = SpUtil.getString(BytedeskConstants.nickname)!; + user.avatar = SpUtil.getString(BytedeskConstants.avatar)!; + // msg + protomsg.Message messageProto = new protomsg.Message(); + messageProto.mid = BytedeskUuid.generateV4(); + messageProto.type = type; + messageProto.timestamp = BytedeskUtils.formatedDateNow(); + messageProto.client = BytedeskUtils.getClient(); //BytedeskConstants.client; + messageProto.version = '1'; + messageProto.encrypted = false; + // 用来在发送之前显示到界面 + Message message = new Message(); + message.mid = messageProto.mid; + message.type = messageProto.type; + message.timestamp = messageProto.timestamp; + // message.client + message.nickname = user.nickname; + message.avatar = user.avatar; + message.topic = thread.topic; + message.status = BytedeskConstants.MESSAGE_STATUS_SENDING; + message.isSend = 1; + message.currentUid = currentUid; + message.answersJson = ''; + message.thread = currentThread; + message.user = + User(uid: user.uid, avatar: user.avatar, nickname: user.nickname); + // 判断是否应该插入本地并显示 + bool shouldInsertLocal = false; + // 发送protobuf + if (type == BytedeskConstants.MESSAGE_TYPE_TEXT) { + protomsg.Text text = new protomsg.Text(); + text.content = content; + messageProto.text = text; + // + thread.content = content; + // + shouldInsertLocal = true; + message.content = content; + } else if (type == BytedeskConstants.MESSAGE_TYPE_IMAGE) { + protomsg.Image image = new protomsg.Image(); + image.imageUrl = content; + messageProto.image = image; + // + thread.content = '[图片]'; + // + shouldInsertLocal = true; + message.imageUrl = content; + } else if (type == BytedeskConstants.MESSAGE_TYPE_VOICE) { + protomsg.Voice voice = new protomsg.Voice(); + voice.voiceUrl = content; + // voice.length + // voice.format + messageProto.voice = voice; + // + thread.content = '[语音]'; + // + shouldInsertLocal = true; + message.voiceUrl = content; + } else if (type == BytedeskConstants.MESSAGE_TYPE_FILE) { + protomsg.File file = new protomsg.File(); + file.fileUrl = content; + messageProto.file = file; + // + thread.content = '[文件]'; + // + shouldInsertLocal = true; + message.fileUrl = content; + } else if (type == BytedeskConstants.MESSAGE_TYPE_VIDEO || + type == BytedeskConstants.MESSAGE_TYPE_SHORT_VIDEO) { + protomsg.Video video = new protomsg.Video(); + video.videoOrShortUrl = content; + messageProto.video = video; + // + thread.content = '[视频]'; + // + shouldInsertLocal = true; + message.videoUrl = content; + } else if (type == BytedeskConstants.MESSAGE_TYPE_COMMODITY) { + protomsg.Text text = new protomsg.Text(); + text.content = content; + messageProto.text = text; + // + thread.content = '[商品]'; + // + shouldInsertLocal = true; + message.content = content; + } else if (type == BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_PREVIEW) { + protomsg.Preview preview = new protomsg.Preview(); + preview.content = content; + messageProto.preview = preview; + } else if (type == BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_RECEIPT) { + // 发送消息回执 + protomsg.Receipt receipt = new protomsg.Receipt(); + receipt.mid = extraParam!.receiptMid!; + receipt.status = extraParam.receiptStatus!; + // + messageProto.receipt = receipt; + } else if (type == BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_RECALL) { + // + protomsg.Recall recall = new protomsg.Recall(); + recall.mid = extraParam!.recallMid!; + // + messageProto.recall = recall; + } else if (type == BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_TRANSFER) { + // + return; + } else if (type == + BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_TRANSFER_ACCEPT) { + // + return; + } else if (type == + BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_TRANSFER_REJECT) { + // + return; + } else if (type == BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_INVITE) { + // + return; + } else if (type == + BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_INVITE_ACCEPT) { + // + return; + } else if (type == + BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_INVITE_REJECT) { + // + return; + } else if (type == + BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_INVITE_RATE) { + // + return; + } else if (type == + BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_RATE_RESULT) { + // + return; + } else if (type == + BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_WEBRTC_INVITE_VIDEO) { + // + return; + } else if (type == + BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_WEBRTC_INVITE_AUDIO) { + // + return; + } else if (type == + BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_WEBRTC_CANCEL) { + // + return; + } else if (type == + BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_WEBRTC_OFFER_VIDEO) { + // + return; + } else if (type == + BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_WEBRTC_OFFER_AUDIO) { + // + return; + } else if (type == + BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_WEBRTC_ANSWER) { + // + return; + } else if (type == + BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_WEBRTC_CANDIDATE) { + // + return; + } else if (type == + BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_WEBRTC_ACCEPT) { + // + return; + } else if (type == + BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_WEBRTC_REJECT) { + // + return; + } else if (type == + BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_WEBRTC_READY) { + // + return; + } else if (type == + BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_WEBRTC_BUSY) { + // + return; + } else if (type == + BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_WEBRTC_CLOSE) { + // + return; + } else if (type == + BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_GROUP_CREATE) { + // + return; + } else if (type == + BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_GROUP_UPDATE) { + // + return; + } else if (type == + BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_GROUP_ANNOUNCEMENT) { + // + return; + } else if (type == + BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_GROUP_INVITE) { + // + return; + } else if (type == + BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_GROUP_INVITE_ACCEPT) { + // + return; + } else if (type == + BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_GROUP_INVITE_REJECT) { + // + return; + } else if (type == + BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_GROUP_APPLY) { + // + return; + } else if (type == + BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_GROUP_APPLY_APPROVE) { + // + return; + } else if (type == + BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_GROUP_APPLY_DENY) { + // + return; + } else if (type == BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_GROUP_KICK) { + // + return; + } else if (type == BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_GROUP_MUTE) { + // + return; + } else if (type == + BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_GROUP_TRANSFER) { + // + return; + } else if (type == + BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_GROUP_TRANSFER_ACCEPT) { + // + return; + } else if (type == + BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_GROUP_TRANSFER_REJECT) { + // + return; + } else if (type == + BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_GROUP_WITHDRAW) { + // + return; + } else if (type == + BytedeskConstants.MESSAGE_TYPE_NOTIFICATION_GROUP_DISMISS) { + // + return; + } else if (type == BytedeskConstants.MESSAGE_TYPE_LOCATION) { + // + return; + } else if (type == BytedeskConstants.MESSAGE_TYPE_LINK) { + // + return; + } else if (type == BytedeskConstants.MESSAGE_TYPE_EVENT) { + // + return; + } else if (type == BytedeskConstants.MESSAGE_TYPE_CUSTOM) { + // + return; + } else if (type == BytedeskConstants.MESSAGE_TYPE_RED_PACKET) { + // + return; + } else { + // TODO: 其他类型消息 + print('other type:' + type); + } + // + messageProto.user = user; + messageProto.thread = thread; + // 先通知界面本地显示聊天记录,然后再发送 + if (shouldInsertLocal) { + // 插入本地数据库 + // if (messageProvider != null) { + messageProvider.insert(message); + // } + bytedeskEventBus.fire(ReceiveMessageEventBus(message)); + midList.add(message.mid!); + } + // + final MqttClientPayloadBuilder builder = MqttClientPayloadBuilder(); + // 注意:此函数为自行添加,原先库中没有 + builder.addProtobuf(messageProto.writeToBuffer()); + mqttClient.publishMessage( + currentThread.topic, MqttQos.exactlyOnce, builder.payload); + // 播放发送消息提示音,放到SDK外实现,迁移到demo中 + if (BytedeskKefu.getPlayAudioOnSendMessage()!) { + // + } + } + + // 断开长连接 + void disconnect() { + mqttClient.disconnect(); + } + + /// The subscribed callback + void _onSubscribed(String? topic) { + // print('Subscription confirmed for topic $topic'); + } + + /// The unsubscribed callback + void _onUnSubscribed(String? topic) { + // print('UnSubscription confirmed for topic $topic'); + } + + /// The subscribed callback + void _onSubscribeFailed(String? topic) { + // print('Subscribe Failed confirmed for topic $topic'); + } + + /// The unsolicited disconnect callback + void _onDisconnected() { + // _isConnected = false; + // print('OnDisconnected client callback - Client disconnection'); + // eventbus发广播,通知长连接断开 + bytedeskEventBus + .fire(ConnectionEventBus(BytedeskConstants.USER_STATUS_DISCONNECTED)); + // if (mqttClient.connectionStatus.returnCode == MqttConnectReturnCode.solicited) { + // print('OnDisconnected callback is solicited, this is correct'); + // } + // 延时10s执行重连 + Future.delayed(Duration(seconds: 10), () { + // print('start reconnecting'); + // reconnect(); + }); + } + + /// The unsolicited disconnect callback + // void _onAutoReconnect() { + // // print('EXAMPLE::onAutoReconnect client callback - Client auto reconnection sequence will start'); + // // connect(); + // } + + /// The successful connect callback + void _onConnected() { + // _isConnected = true; + // print('OnConnected client callback - Client connection was sucessful'); + // TODO: eventbus发广播,通知长连接建立 + bytedeskEventBus + .fire(ConnectionEventBus(BytedeskConstants.USER_STATUS_CONNECTED)); + } + + /// Pong callback + void _onPong() { + // print('Ping response client callback invoked'); + } + + bool isConnected() { + // return _isConnected; + return mqttClient.connectionStatus.state == MqttConnectionState.connected; + } +} diff --git a/bytedesk_kefu/lib/mqtt/bytedesk_payload_builder.dart b/bytedesk_kefu/lib/mqtt/bytedesk_payload_builder.dart new file mode 100755 index 0000000..cd46606 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/bytedesk_payload_builder.dart @@ -0,0 +1,18 @@ +import 'dart:typed_data'; + +import 'package:bytedesk_kefu/mqtt/lib/mqtt_client.dart'; + +// TODO: 搞定继承之后,将mqtt包改为package依赖 +class BytedeskPayloadBuilder extends MqttClientPayloadBuilder { + /// Construction + BytedeskPayloadBuilder() { + // _payload = typed.Uint8Buffer(); + } + + // typed.Uint8Buffer _payload; + + // added by jackning, 2019/12/03 + void addProtobuf(Uint8List val) { + // super._payload.addAll(val); + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/mqtt_browser_client.dart b/bytedesk_kefu/lib/mqtt/lib/mqtt_browser_client.dart new file mode 100755 index 0000000..278faed --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/mqtt_browser_client.dart @@ -0,0 +1,23 @@ +/* + * Package : mqtt_browser_client + * Author : S. Hamblett + * Date : 20/01/2020 + * Copyright : S.Hamblett + */ + +library mqtt_browser_client; + +import 'dart:async'; +// import 'dart:html'; +// 替代 dart:html 以便于在APP上运行 +import 'package:universal_html/html.dart'; +import 'dart:typed_data'; +import 'package:event_bus/event_bus.dart' as events; +import 'package:typed_data/typed_data.dart' as typed; +import 'mqtt_client.dart'; + +part 'src/mqtt_browser_client.dart'; +part 'src/connectionhandling/browser/mqtt_client_mqtt_browser_connection_handler.dart'; +part 'src/connectionhandling/browser/mqtt_client_synchronous_mqtt_browser_connection_handler.dart'; +part 'src/connectionhandling/browser/mqtt_client_mqtt_browser_ws_connection.dart'; +part 'src/connectionhandling/browser/mqtt_client_mqtt_browser_connection.dart'; diff --git a/bytedesk_kefu/lib/mqtt/lib/mqtt_client.dart b/bytedesk_kefu/lib/mqtt/lib/mqtt_client.dart new file mode 100755 index 0000000..43e526e --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/mqtt_client.dart @@ -0,0 +1,171 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 31/05/2017 + * Copyright : S.Hamblett + */ + +library mqtt_client; + +import 'dart:async'; +import 'dart:convert'; +import 'dart:typed_data'; +import 'package:meta/meta.dart'; +import 'package:typed_data/typed_data.dart' as typed; +import 'package:event_bus/event_bus.dart' as events; +import 'src/observable/observable.dart' as observe; + +/// The mqtt_client package exported interface +part 'src/mqtt_client.dart'; + +part 'src/mqtt_client_constants.dart'; + +part 'src/mqtt_client_protocol.dart'; + +part 'src/mqtt_client_events.dart'; + +part 'src/exception/mqtt_client_client_identifier_exception.dart'; + +part 'src/exception/mqtt_client_connection_exception.dart'; + +part 'src/exception/mqtt_client_noconnection_exception.dart'; + +part 'src/exception/mqtt_client_invalid_header_exception.dart'; + +part 'src/exception/mqtt_client_invalid_message_exception.dart'; + +part 'src/exception/mqtt_client_invalid_payload_size_exception.dart'; + +part 'src/exception/mqtt_client_invalid_topic_exception.dart'; + +part 'src/exception/mqtt_client_incorrect_instantiation_exception.dart'; + +part 'src/connectionhandling/mqtt_client_connection_state.dart'; + +part 'src/connectionhandling/mqtt_client_mqtt_connection_base.dart'; + +part 'src/connectionhandling/mqtt_client_mqtt_connection_handler_base.dart'; + +part 'src/connectionhandling/mqtt_client_imqtt_connection_handler.dart'; + +part 'src/mqtt_client_topic.dart'; + +part 'src/mqtt_client_connection_status.dart'; + +part 'src/mqtt_client_publication_topic.dart'; + +part 'src/mqtt_client_subscription_topic.dart'; + +part 'src/mqtt_client_subscription_status.dart'; + +part 'src/mqtt_client_mqtt_qos.dart'; + +part 'src/mqtt_client_mqtt_received_message.dart'; + +part 'src/mqtt_client_publishing_manager.dart'; + +part 'src/mqtt_client_ipublishing_manager.dart'; + +part 'src/mqtt_client_subscription.dart'; + +part 'src/mqtt_client_subscriptions_manager.dart'; + +part 'src/mqtt_client_message_identifier_dispenser.dart'; + +part 'src/dataconvertors/mqtt_client_payload_convertor.dart'; + +part 'src/dataconvertors/mqtt_client_passthru_payload_convertor.dart'; + +part 'src/encoding/mqtt_client_mqtt_encoding.dart'; + +part 'src/dataconvertors/mqtt_client_ascii_payload_convertor.dart'; + +part 'src/utility/mqtt_client_byte_buffer.dart'; + +part 'src/utility/mqtt_client_logger.dart'; + +part 'src/utility/mqtt_client_payload_builder.dart'; + +part 'src/messages/mqtt_client_mqtt_header.dart'; + +part 'src/messages/mqtt_client_mqtt_variable_header.dart'; + +part 'src/messages/mqtt_client_mqtt_message.dart'; + +part 'src/messages/connect/mqtt_client_mqtt_connect_return_code.dart'; + +part 'src/messages/connect/mqtt_client_mqtt_connect_flags.dart'; + +part 'src/messages/connect/mqtt_client_mqtt_connect_payload.dart'; + +part 'src/messages/connect/mqtt_client_mqtt_connect_variable_header.dart'; + +part 'src/messages/connect/mqtt_client_mqtt_connect_message.dart'; + +part 'src/messages/connectack/mqtt_client_mqtt_connect_ack_variable_header.dart'; + +part 'src/messages/connectack/mqtt_client_mqtt_connect_ack_message.dart'; + +part 'src/messages/disconnect/mqtt_client_mqtt_disconnect_message.dart'; + +part 'src/messages/pingrequest/mqtt_client_mqtt_ping_request_message.dart'; + +part 'src/messages/pingresponse/mqtt_client_mqtt_ping_response_message.dart'; + +part 'src/messages/publish/mqtt_client_mqtt_publish_message.dart'; + +part 'src/messages/publish/mqtt_client_mqtt_publish_variable_header.dart'; + +part 'src/messages/publishack/mqtt_client_mqtt_publish_ack_message.dart'; + +part 'src/messages/publishack/mqtt_client_mqtt_publish_ack_variable_header.dart'; + +part 'src/messages/publishcomplete/mqtt_client_mqtt_publish_complete_message.dart'; + +part 'src/messages/publishcomplete/mqtt_client_mqtt_publish_complete_variable_header.dart'; + +part 'src/messages/publishreceived/mqtt_client_mqtt_publish_received_message.dart'; + +part 'src/messages/publishreceived/mqtt_client_mqtt_publish_received_variable_header.dart'; + +part 'src/messages/publishrelease/mqtt_client_mqtt_publish_release_message.dart'; + +part 'src/messages/publishrelease/mqtt_client_mqtt_publish_release_variable_header.dart'; + +part 'src/messages/subscribe/mqtt_client_mqtt_subscribe_variable_header.dart'; + +part 'src/messages/subscribe/mqtt_client_mqtt_subscribe_payload.dart'; + +part 'src/messages/subscribe/mqtt_client_mqtt_subscribe_message.dart'; + +part 'src/messages/subscribeack/mqtt_client_mqtt_subscribe_ack_variable_header.dart'; + +part 'src/messages/subscribeack/mqtt_client_mqtt_subscribe_ack_message.dart'; + +part 'src/messages/subscribeack/mqtt_client_mqtt_subscribe_ack_payload.dart'; + +part 'src/messages/unsubscribe/mqtt_client_mqtt_unsubscribe_variable_header.dart'; + +part 'src/messages/unsubscribe/mqtt_client_mqtt_unsubscribe_payload.dart'; + +part 'src/messages/unsubscribe/mqtt_client_mqtt_unsubscribe_message.dart'; + +part 'src/messages/unsubscribeack/mqtt_client_mqtt_unsubscribe_ack_variable_header.dart'; + +part 'src/messages/unsubscribeack/mqtt_client_mqtt_unsubscribe_ack_message.dart'; + +part 'src/messages/publish/mqtt_client_mqtt_publish_payload.dart'; + +part 'src/messages/mqtt_client_mqtt_message_type.dart'; + +part 'src/messages/mqtt_client_mqtt_message_factory.dart'; + +part 'src/messages/mqtt_client_mqtt_payload.dart'; + +part 'src/management/mqtt_client_topic_filter.dart'; + +part 'src/utility/mqtt_client_utilities.dart'; + +part 'src/connectionhandling/mqtt_client_mqtt_connection_keep_alive.dart'; + +part 'src/connectionhandling/mqtt_client_read_wrapper.dart'; diff --git a/bytedesk_kefu/lib/mqtt/lib/mqtt_server_client.dart b/bytedesk_kefu/lib/mqtt/lib/mqtt_server_client.dart new file mode 100755 index 0000000..4f38ea4 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/mqtt_server_client.dart @@ -0,0 +1,26 @@ +/* + * Package : mqtt_server_client + * Author : S. Hamblett + * Date : 20/01/2020 + * Copyright : S.Hamblett + */ + +library mqtt_server_client; + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; +import 'package:crypto/crypto.dart'; +import 'package:event_bus/event_bus.dart' as events; +import 'package:typed_data/typed_data.dart' as typed; +import 'mqtt_client.dart'; + +part 'src/connectionhandling/server/mqtt_client_mqtt_server_connection_handler.dart'; +part 'src/connectionhandling/server/mqtt_client_mqtt_server_normal_connection.dart'; +part 'src/connectionhandling/server/mqtt_client_mqtt_server_secure_connection.dart'; +part 'src/connectionhandling/server/mqtt_client_mqtt_server_ws2_connection.dart'; +part 'src/connectionhandling/server/mqtt_client_mqtt_server_ws_connection.dart'; +part 'src/connectionhandling/server/mqtt_client_synchronous_mqtt_server_connection_handler.dart'; +part 'src/connectionhandling/server/mqtt_client_mqtt_server_connection.dart'; +part 'src/mqtt_server_client.dart'; diff --git a/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/browser/mqtt_client_mqtt_browser_connection.dart b/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/browser/mqtt_client_mqtt_browser_connection.dart new file mode 100755 index 0000000..4d1a9a0 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/browser/mqtt_client_mqtt_browser_connection.dart @@ -0,0 +1,113 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 22/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_browser_client; + +/// The MQTT browser connection base class +class MqttBrowserConnection extends MqttConnectionBase { + /// Default constructor + MqttBrowserConnection(clientEventBus) : super(clientEventBus); + + /// Initializes a new instance of the MqttBrowserConnection class. + MqttBrowserConnection.fromConnect(server, port, clientEventBus) + : super(clientEventBus) { + connect(server, port); + } + + /// Connect, must be overridden in connection classes + @override + Future connect(String server, int port) { + final completer = Completer(); + return completer.future; + } + + /// Connect for auto reconnect , must be overridden in connection classes + @override + Future connectAuto(String server, int port) { + final completer = Completer(); + return completer.future; + } + + /// Create the listening stream subscription and subscribe the callbacks + void _startListening() { + MqttLogger.log('MqttBrowserConnection::_startListening'); + try { + client.onClose.listen((e) { + MqttLogger.log( + 'MqttBrowserConnection::_startListening - websocket is closed'); + onDone(); + }); + client.onMessage.listen((MessageEvent e) { + _onData(e.data); + }); + client.onError.listen((e) { + MqttLogger.log( + 'MqttBrowserConnection::_startListening - websocket has errored'); + onError(e); + }); + } on Exception catch (e) { + MqttLogger.log( + 'MqttBrowserConnection::_startListening - exception raised $e'); + } + } + + /// OnData listener callback + void _onData(dynamic byteData) { + MqttLogger.log('MqttBrowserConnection::_onData'); + // Protect against 0 bytes but should never happen. + var data = Uint8List.view(byteData); + if (data.isEmpty) { + MqttLogger.log('MqttBrowserConnection::_ondata - Error - 0 byte message'); + return; + } + + messageStream.addAll(data); + + while (messageStream.isMessageAvailable()) { + var messageIsValid = true; + MqttMessage? msg; + + try { + msg = MqttMessage.createFrom(messageStream); + } on Exception { + MqttLogger.log( + 'MqttBrowserConnection::_ondata - message is not yet valid, ' + 'waiting for more data ...'); + messageIsValid = false; + } + if (!messageIsValid) { + messageStream.reset(); + return; + } + if (messageIsValid) { + messageStream.shrink(); + MqttLogger.log( + 'MqttBrowserConnection::_onData - message received ', msg); + if (!clientEventBus!.streamController.isClosed) { + if (msg!.header!.messageType == MqttMessageType.connectAck) { + clientEventBus!.fire(ConnectAckMessageAvailable(msg)); + } else { + clientEventBus!.fire(MessageAvailable(msg)); + } + MqttLogger.log( + 'MqttBrowserConnection::_onData - message available event fired'); + } else { + MqttLogger.log( + 'MqttBrowserConnection::_onData - WARN - message available event not fired, event bus is closed'); + } + } + } + } + + /// Sends the message in the stream to the broker. + void send(MqttByteBuffer message) { + final messageBytes = message.read(message.length); + var buffer = messageBytes.buffer; + var bData = ByteData.view(buffer); + client?.sendTypedData(bData); + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/browser/mqtt_client_mqtt_browser_connection_handler.dart b/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/browser/mqtt_client_mqtt_browser_connection_handler.dart new file mode 100755 index 0000000..aac0391 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/browser/mqtt_client_mqtt_browser_connection_handler.dart @@ -0,0 +1,17 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 22/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_browser_client; + +/// This class provides specific connection functionality +/// for browser based connection handler implementations. +abstract class MqttBrowserConnectionHandler extends MqttConnectionHandlerBase { + /// Initializes a new instance of the [MqttBrowserConnectionHandler] class. + MqttBrowserConnectionHandler(var clientEventBus, + {required int maxConnectionAttempts}) + : super(clientEventBus, maxConnectionAttempts: maxConnectionAttempts); +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/browser/mqtt_client_mqtt_browser_ws_connection.dart b/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/browser/mqtt_client_mqtt_browser_ws_connection.dart new file mode 100755 index 0000000..ccf50d7 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/browser/mqtt_client_mqtt_browser_ws_connection.dart @@ -0,0 +1,184 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 14/08/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_browser_client; + +/// The MQTT connection class for the browser websocket interface +class MqttBrowserWsConnection extends MqttBrowserConnection { + /// Default constructor + MqttBrowserWsConnection(events.EventBus? eventBus) : super(eventBus); + + /// Initializes a new instance of the MqttConnection class. + MqttBrowserWsConnection.fromConnect( + String server, int port, events.EventBus eventBus) + : super(eventBus) { + connect(server, port); + } + + /// The websocket subprotocol list + List protocols = MqttClientConstants.protocolsMultipleDefault; + + /// Connect + @override + Future connect(String server, int port) { + final completer = Completer(); + MqttLogger.log('MqttBrowserWsConnection::connect - entered'); + // Add the port if present + Uri uri; + try { + uri = Uri.parse(server); + } on Exception { + final message = + 'MqttBrowserWsConnection::connect - The URI supplied for the WS ' + 'connection is not valid - $server'; + throw NoConnectionException(message); + } + if (uri.scheme != 'ws' && uri.scheme != 'wss') { + final message = + 'MqttBrowserWsConnection::connect - The URI supplied for the WS has ' + 'an incorrect scheme - $server'; + throw NoConnectionException(message); + } + uri = uri.replace(port: port); + + final uriString = uri.toString(); + MqttLogger.log('MqttBrowserWsConnection::connect - WS URL is $uriString'); + try { + // Connect and save the socket. + client = WebSocket(uriString, protocols); + client.binaryType = 'arraybuffer'; + messageStream = MqttByteBuffer(typed.Uint8Buffer()); + var closeEvents; + var errorEvents; + client.onOpen.listen((e) { + MqttLogger.log('MqttBrowserWsConnection::connect - websocket is open'); + closeEvents.cancel(); + errorEvents.cancel(); + _startListening(); + return completer.complete(); + }); + + closeEvents = client.onClose.listen((e) { + MqttLogger.log( + 'MqttBrowserWsConnection::connect - websocket is closed'); + closeEvents.cancel(); + errorEvents.cancel(); + return completer.complete(MqttClientConnectionStatus()); + }); + errorEvents = client.onError.listen((e) { + MqttLogger.log( + 'MqttBrowserWsConnection::connect - websocket has erred'); + closeEvents.cancel(); + errorEvents.cancel(); + return completer.complete(MqttClientConnectionStatus()); + }); + } on Exception { + final message = + 'MqttBrowserWsConnection::connect - The connection to the message broker ' + '{$uriString} could not be made.'; + throw NoConnectionException(message); + } + MqttLogger.log('MqttBrowserWsConnection::connect - connection is waiting'); + return completer.future; + } + + /// Connect Auto + @override + Future connectAuto(String server, int port) { + final completer = Completer(); + MqttLogger.log('MqttBrowserWsConnection::connectAuto - entered'); + // Add the port if present + Uri uri; + try { + uri = Uri.parse(server); + } on Exception { + final message = + 'MqttBrowserWsConnection::connectAuto - The URI supplied for the WS ' + 'connection is not valid - $server'; + throw NoConnectionException(message); + } + if (uri.scheme != 'ws' && uri.scheme != 'wss') { + final message = + 'MqttBrowserWsConnection::connectAuto - The URI supplied for the WS has ' + 'an incorrect scheme - $server'; + throw NoConnectionException(message); + } + + uri = uri.replace(port: port); + final uriString = uri.toString(); + MqttLogger.log( + 'MqttBrowserWsConnection::connectAuto - WS URL is $uriString'); + try { + // Connect and save the socket. + client = WebSocket(uriString, protocols); + client.binaryType = 'arraybuffer'; + messageStream = MqttByteBuffer(typed.Uint8Buffer()); + var closeEvents; + var errorEvents; + client.onOpen.listen((e) { + MqttLogger.log( + 'MqttBrowserWsConnection::connectAuto - websocket is open'); + closeEvents.cancel(); + errorEvents.cancel(); + _startListening(); + return completer.complete(); + }); + + closeEvents = client.onClose.listen((e) { + MqttLogger.log( + 'MqttBrowserWsConnection::connectAuto - websocket is closed'); + closeEvents.cancel(); + errorEvents.cancel(); + return completer.complete(MqttClientConnectionStatus()); + }); + errorEvents = client.onError.listen((e) { + MqttLogger.log( + 'MqttBrowserWsConnection::connectAuto - websocket has errored'); + closeEvents.cancel(); + errorEvents.cancel(); + return completer.complete(MqttClientConnectionStatus()); + }); + } on Exception { + final message = + 'MqttBrowserWsConnection::connectAuto - The connection to the message broker ' + '{$uriString} could not be made.'; + throw NoConnectionException(message); + } + MqttLogger.log( + 'MqttBrowserWsConnection::connectAuto - connection is waiting'); + return completer.future; + } + + /// OnError listener callback + @override + void onError(dynamic error) { + _disconnect(); + if (onDisconnected != null) { + MqttLogger.log( + 'MqttConnectionBase::_onError - calling disconnected callback'); + onDisconnected!(); + } + } + + /// OnDone listener callback + @override + void onDone() { + _disconnect(); + if (onDisconnected != null) { + MqttLogger.log( + 'MqttConnectionBase::_onDone - calling disconnected callback'); + onDisconnected!(); + } + } + + void _disconnect() { + if (client != null) { + client.close(); + client = null; + } + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/browser/mqtt_client_synchronous_mqtt_browser_connection_handler.dart b/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/browser/mqtt_client_synchronous_mqtt_browser_connection_handler.dart new file mode 100755 index 0000000..f35cab2 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/browser/mqtt_client_synchronous_mqtt_browser_connection_handler.dart @@ -0,0 +1,108 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 22/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_browser_client; + +/// Connection handler that performs connections and disconnections +/// to the hostname in a synchronous manner. +class SynchronousMqttBrowserConnectionHandler + extends MqttBrowserConnectionHandler { + /// Initializes a new instance of the MqttConnectionHandler class. + SynchronousMqttBrowserConnectionHandler( + clientEventBus, { + required int maxConnectionAttempts, + }) : super(clientEventBus, maxConnectionAttempts: maxConnectionAttempts) { + connectTimer = MqttCancellableAsyncSleep(5000); + initialiseListeners(); + } + + /// Synchronously connect to the specific Mqtt Connection. + @override + Future internalConnect( + String? hostname, int? port, MqttConnectMessage? connectMessage) async { + var connectionAttempts = 0; + MqttLogger.log( + 'SynchronousMqttBrowserConnectionHandler::internalConnect entered'); + do { + // Initiate the connection + MqttLogger.log( + 'SynchronousMqttBrowserConnectionHandler::internalConnect - ' + 'initiating connection try $connectionAttempts, auto reconnect in progress $autoReconnectInProgress'); + connectionStatus.state = MqttConnectionState.connecting; + connectionStatus.returnCode = MqttConnectReturnCode.noneSpecified; + // Don't reallocate the connection if this is an auto reconnect + if (!autoReconnectInProgress!) { + connection = MqttBrowserWsConnection(clientEventBus); + if (websocketProtocols != null) { + connection.protocols = websocketProtocols; + } + connection.onDisconnected = onDisconnected; + } + // Connect + try { + if (!autoReconnectInProgress!) { + MqttLogger.log( + 'SynchronousMqttBrowserConnectionHandler::internalConnect - calling connect'); + await connection.connect(hostname, port); + } else { + MqttLogger.log( + 'SynchronousMqttBrowserConnectionHandler::internalConnect - calling connectAuto'); + await connection.connectAuto(hostname, port); + } + } on Exception { + // Ignore exceptions in an auto reconnect sequence + if (autoReconnectInProgress!) { + MqttLogger.log( + 'SynchronousMqttBrowserConnectionHandler::internalConnect' + ' exception thrown during auto reconnect - ignoring'); + } else { + rethrow; + } + } + MqttLogger.log( + 'SynchronousMqttBrowserConnectionHandler::internalConnect - ' + 'connection complete'); + // Transmit the required connection message to the broker. + MqttLogger.log('SynchronousMqttBrowserConnectionHandler::internalConnect ' + 'sending connect message'); + sendMessage(connectMessage); + MqttLogger.log( + 'SynchronousMqttBrowserConnectionHandler::internalConnect - ' + 'pre sleep, state = $connectionStatus'); + // We're the sync connection handler so we need to wait for the + // brokers acknowledgement of the connections + await connectTimer.sleep(); + MqttLogger.log( + 'SynchronousMqttBrowserConnectionHandler::internalConnect - ' + 'post sleep, state = $connectionStatus'); + } while (connectionStatus.state != MqttConnectionState.connected && + ++connectionAttempts < maxConnectionAttempts!); + // If we've failed to handshake with the broker, throw an exception. + if (connectionStatus.state != MqttConnectionState.connected) { + if (!autoReconnectInProgress!) { + MqttLogger.log( + 'SynchronousMqttBrowserConnectionHandler::internalConnect failed'); + if (connectionStatus.returnCode == + MqttConnectReturnCode.noneSpecified) { + throw NoConnectionException('The maximum allowed connection attempts ' + '({$maxConnectionAttempts}) were exceeded. ' + 'The broker is not responding to the connection request message ' + '(Missing Connection Acknowledgement?'); + } else { + throw NoConnectionException('The maximum allowed connection attempts ' + '({$maxConnectionAttempts}) were exceeded. ' + 'The broker is not responding to the connection request message correctly ' + 'The return code is ${connectionStatus.returnCode}'); + } + } + } + MqttLogger.log('SynchronousMqttBrowserConnectionHandler::internalConnect ' + 'exited with state $connectionStatus'); + initialConnectionComplete = true; + return connectionStatus; + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/mqtt_client_connection_state.dart b/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/mqtt_client_connection_state.dart new file mode 100755 index 0000000..b698e1a --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/mqtt_client_connection_state.dart @@ -0,0 +1,41 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 31/05/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Enumeration that indicates the origin of a client disconnection +enum MqttDisconnectionOrigin { + /// Unsolicited, i.e. not requested by the client, + /// for example a broker/network initiated disconnect + unsolicited, + + /// Solicited, i.e. requested by the client, + /// for example disconnect called on the client. + solicited, + + /// None set + none +} + +/// Enumeration that indicates various client connection states +enum MqttConnectionState { + /// The MQTT Connection is in the process of disconnecting from the broker. + disconnecting, + + /// MQTT Connection is not currently connected to any broker. + disconnected, + + /// The MQTT Connection is in the process of connecting to the broker. + connecting, + + /// The MQTT Connection is currently connected to the broker. + connected, + + /// The MQTT Connection is faulted and no longer communicating + /// with the broker. + faulted +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/mqtt_client_imqtt_connection_handler.dart b/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/mqtt_client_imqtt_connection_handler.dart new file mode 100755 index 0000000..c8eae83 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/mqtt_client_imqtt_connection_handler.dart @@ -0,0 +1,80 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 22/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Callback function definitions +typedef MessageCallbackFunction = bool Function(MqttMessage? message); + +/// The connection handler interface class +abstract class IMqttConnectionHandler { + /// The connection status + MqttClientConnectionStatus get connectionStatus; + + /// Successful connection callback + ConnectCallback? onConnected; + + /// Unsolicited disconnection callback + DisconnectCallback? onDisconnected; + + /// Auto reconnect callback + AutoReconnectCallback? onAutoReconnect; + + /// Auto reconnected callback + AutoReconnectCompleteCallback? onAutoReconnected; + + /// Auto reconnect in progress + bool? autoReconnectInProgress; + + // Server name, needed for auto reconnect. + String? server; + + // Port number, needed for auto reconnect. + int? port; + + // Connection message, needed for auto reconnect. + MqttConnectMessage? connectionMessage; + + /// Callback function to handle bad certificate. if true, ignore the error. + bool Function(dynamic certificate)? onBadCertificate; + + /// Runs the disconnection process to stop communicating + /// with a message broker. + MqttConnectionState disconnect(); + + /// Closes a connection. + void close(); + + /// Connects to a message broker + /// The broker server to connect to + /// The port to connect to + /// The connect message to use to initiate the connection + Future connect( + String server, int port, MqttConnectMessage message); + + /// Register the specified callback to receive messages of a specific type. + /// The type of message that the callback should be sent + /// The callback function that will accept the message type + void registerForMessage( + MqttMessageType msgType, MessageCallbackFunction msgProcessorCallback); + + /// Sends a message to a message broker. + void sendMessage(MqttMessage message); + + /// Unregisters the specified callbacks so it not longer receives + /// messages of the specified type. + /// The message type the callback currently receives + void unRegisterForMessage(MqttMessageType msgType); + + /// Registers a callback to be executed whenever a message is + /// sent by the connection handler. + void registerForAllSentMessages(MessageCallbackFunction sentMsgCallback); + + /// UnRegisters a callback that is registerd to be executed whenever a + /// message is sent by the connection handler. + void unRegisterForAllSentMessages(MessageCallbackFunction sentMsgCallback); +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/mqtt_client_mqtt_connection_base.dart b/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/mqtt_client_mqtt_connection_base.dart new file mode 100755 index 0000000..10ab2a7 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/mqtt_client_mqtt_connection_base.dart @@ -0,0 +1,93 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 29/03/2020 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// The MQTT client connection base class +class MqttConnectionBase { + /// Default constructor + MqttConnectionBase(this.clientEventBus); + + /// Initializes a new instance of the MqttConnection class. + MqttConnectionBase.fromConnect(String server, int port, this.clientEventBus) { + connect(server, port); + } + + /// The socket that maintains the connection to the MQTT broker. + @protected + dynamic client; + + /// The read wrapper + @protected + ReadWrapper? readWrapper; + + ///The read buffer + @protected + late MqttByteBuffer messageStream; + + /// Unsolicited disconnection callback + @protected + DisconnectCallback? onDisconnected; + + /// The event bus + @protected + events.EventBus? clientEventBus; + + /// Connect for auto reconnect , must be overridden in connection classes + @protected + Future connectAuto(String server, int port) { + final completer = Completer(); + return completer.future; + } + + /// Connect, must be overridden in connection classes + @protected + Future connect(String server, int port) { + final completer = Completer(); + return completer.future; + } + + /// OnError listener callback + @protected + void onError(dynamic error) { + _disconnect(); + if (onDisconnected != null) { + MqttLogger.log( + 'MqttConnectionBase::_onError - calling disconnected callback'); + onDisconnected!(); + } + } + + /// OnDone listener callback + @protected + void onDone() { + _disconnect(); + if (onDisconnected != null) { + MqttLogger.log( + 'MqttConnectionBase::_onDone - calling disconnected callback'); + onDisconnected!(); + } + } + + void _disconnect() { + if (client != null) { + client.close(); + client.destroy(); + client = null; + } + } + + /// User requested or auto disconnect disconnection + @protected + void disconnect({bool auto = false}) { + if (auto) { + _disconnect(); + } else { + onDone(); + } + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/mqtt_client_mqtt_connection_handler_base.dart b/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/mqtt_client_mqtt_connection_handler_base.dart new file mode 100755 index 0000000..f27680c --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/mqtt_client_mqtt_connection_handler_base.dart @@ -0,0 +1,301 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 27/03/2020 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// This class provides shared connection functionality +/// to serverand browser connection handler implementations. +abstract class MqttConnectionHandlerBase implements IMqttConnectionHandler { + /// Initializes a new instance of the [MqttConnectionHandlerBase] class. + MqttConnectionHandlerBase(this.clientEventBus, + {required this.maxConnectionAttempts}); + + /// Successful connection callback. + @override + ConnectCallback? onConnected; + + /// Unsolicited disconnection callback. + @override + DisconnectCallback? onDisconnected; + + /// Auto reconnect callback + @override + AutoReconnectCallback? onAutoReconnect; + + /// Auto reconnected callback + @override + AutoReconnectCompleteCallback? onAutoReconnected; + + /// Auto reconnect in progress + @override + bool? autoReconnectInProgress = false; + + // Server name, needed for auto reconnect. + @override + String? server; + + // Port number, needed for auto reconnect. + @override + int? port; + + // Connection message, needed for auto reconnect. + @override + MqttConnectMessage? connectionMessage; + + /// Callback function to handle bad certificate. if true, ignore the error. + @override + bool Function(dynamic certificate)? onBadCertificate; + + /// Max connection attempts + final int? maxConnectionAttempts; + + /// The broker connection acknowledgment timer + @protected + late MqttCancellableAsyncSleep connectTimer; + + /// The event bus + @protected + events.EventBus? clientEventBus; + + /// User supplied websocket protocols + @protected + List? websocketProtocols; + + /// The connection + @protected + late dynamic connection; + + /// Registry of message processors + @protected + Map messageProcessorRegistry = + {}; + + /// Registry of sent message callbacks + @protected + List sentMessageCallbacks = + []; + + /// We have had an initial connection + @protected + bool initialConnectionComplete = false; + + /// Connection status + @override + MqttClientConnectionStatus connectionStatus = MqttClientConnectionStatus(); + + /// Connect to the specific Mqtt Connection. + @override + Future connect( + String? server, int? port, MqttConnectMessage? message) async { + // Save the parameters for auto reconnect. + this.server = server; + this.port = port; + MqttLogger.log( + 'MqttConnectionHandlerBase::connect - server $server, port $port'); + // ignore: unnecessary_this + this.connectionMessage = message; + try { + await internalConnect(server, port, message); + return connectionStatus; + } on Exception { + connectionStatus.state = MqttConnectionState.faulted; + rethrow; + } + } + + /// Connect to the specific Mqtt Connection internally. + @protected + Future internalConnect( + String? hostname, int? port, MqttConnectMessage? message); + + /// Auto reconnect + @protected + void autoReconnect(AutoReconnect reconnectEvent) async { + MqttLogger.log('MqttConnectionHandlerBase::autoReconnect entered'); + // If already in progress exit and we were not connected return + if (autoReconnectInProgress! && !reconnectEvent.wasConnected) { + return; + } + autoReconnectInProgress = true; + // If the auto reconnect callback is set call it + if (onAutoReconnect != null) { + onAutoReconnect!(); + } + + // If we are connected disconnect from the broker. + if (reconnectEvent.wasConnected) { + MqttLogger.log( + 'MqttConnectionHandlerBase::autoReconnect - was connected, sending disconnect'); + sendMessage(MqttDisconnectMessage()); + connectionStatus.state = MqttConnectionState.disconnecting; + } + connection.disconnect(auto: true); + connection.onDisconnected = null; + MqttLogger.log( + 'MqttConnectionHandlerBase::autoReconnect - attempting reconnection'); + connectionStatus = await connect(server, port, connectionMessage); + autoReconnectInProgress = false; + if (connectionStatus.state == MqttConnectionState.connected) { + connection.onDisconnected = onDisconnected; + // Fire the re subscribe event. + clientEventBus!.fire(Resubscribe(fromAutoReconnect: true)); + MqttLogger.log( + 'MqttConnectionHandlerBase::autoReconnect - auto reconnect complete'); + // If the auto reconnect callback is set call it + if (onAutoReconnected != null) { + onAutoReconnected!(); + } + } else { + MqttLogger.log( + 'MqttConnectionHandlerBase::autoReconnect - auto reconnect failed - re trying'); + clientEventBus!.fire(AutoReconnect()); + } + } + + /// Sends a message to the broker through the current connection. + @override + void sendMessage(MqttMessage? message) { + MqttLogger.log('MqttConnectionHandlerBase::sendMessage - ', message); + if ((connectionStatus.state == MqttConnectionState.connected) || + (connectionStatus.state == MqttConnectionState.connecting)) { + final buff = typed.Uint8Buffer(); + final stream = MqttByteBuffer(buff); + message!.writeTo(stream); + stream.seek(0); + connection.send(stream); + // Let any registered people know we're doing a message. + for (final callback in sentMessageCallbacks) { + callback(message); + } + } else { + MqttLogger.log('MqttConnectionHandlerBase::sendMessage - not connected'); + } + } + + /// Closes the connection to the Mqtt message broker. + @override + void close() { + if (connectionStatus.state == MqttConnectionState.connected) { + disconnect(); + } + } + + /// Registers for the receipt of messages when they arrive. + @override + void registerForMessage( + MqttMessageType msgType, MessageCallbackFunction? callback) { + messageProcessorRegistry[msgType] = callback; + } + + /// UnRegisters for the receipt of messages when they arrive. + @override + void unRegisterForMessage(MqttMessageType msgType) { + messageProcessorRegistry.remove(msgType); + } + + /// Registers a callback to be called whenever a message is sent. + @override + void registerForAllSentMessages(MessageCallbackFunction sentMsgCallback) { + sentMessageCallbacks.add(sentMsgCallback); + } + + /// UnRegisters a callback that is called whenever a message is sent. + @override + void unRegisterForAllSentMessages(MessageCallbackFunction sentMsgCallback) { + sentMessageCallbacks.remove(sentMsgCallback); + } + + /// Handles the Message Available event of the connection control for + /// handling non connection messages. + @protected + void messageAvailable(MessageAvailable event) { + final messageType = event.message!.header!.messageType; + MqttLogger.log( + 'MqttConnectionHandlerBase::messageAvailable - message type is $messageType'); + final callback = messageProcessorRegistry[messageType!]; + if (callback != null) { + callback(event.message); + } else { + MqttLogger.log( + 'MqttConnectionHandlerBase::messageAvailable - WARN - no registered callback for this message type'); + } + } + + /// Disconnects + @override + MqttConnectionState disconnect() { + MqttLogger.log('MqttConnectionHandlerBase::disconnect - entered'); + if (connectionStatus.state == MqttConnectionState.connected) { + // Send a disconnect message to the broker + sendMessage(MqttDisconnectMessage()); + } + // Disconnect + _performConnectionDisconnect(); + return connectionStatus.state; + } + + /// Disconnects the underlying connection object. + @protected + void _performConnectionDisconnect() { + MqttLogger.log( + 'MqttConnectionHandlerBase::_performConnectionDisconnect entered'); + connectionStatus.state = MqttConnectionState.disconnected; + } + + /// Processes the connect acknowledgement message. + @protected + bool connectAckProcessor(MqttMessage msg) { + MqttLogger.log('MqttConnectionHandlerBase::_connectAckProcessor'); + try { + final ackMsg = msg as MqttConnectAckMessage; + // Drop the connection if our connect request has been rejected. + if (ackMsg.variableHeader.returnCode == + MqttConnectReturnCode.brokerUnavailable || + ackMsg.variableHeader.returnCode == + MqttConnectReturnCode.identifierRejected || + ackMsg.variableHeader.returnCode == + MqttConnectReturnCode.unacceptedProtocolVersion || + ackMsg.variableHeader.returnCode == + MqttConnectReturnCode.notAuthorized || + ackMsg.variableHeader.returnCode == + MqttConnectReturnCode.badUsernameOrPassword) { + MqttLogger.log('MqttConnectionHandlerBase::_connectAckProcessor ' + 'connection rejected'); + connectionStatus.returnCode = ackMsg.variableHeader.returnCode; + _performConnectionDisconnect(); + } else { + // Initialize the keepalive to start the ping based keepalive process. + MqttLogger.log('MqttConnectionHandlerBase:_connectAckProcessor ' + '- state = connected'); + connectionStatus.state = MqttConnectionState.connected; + connectionStatus.returnCode = MqttConnectReturnCode.connectionAccepted; + // Call the connected callback if we have one + if (onConnected != null) { + onConnected!(); + } + } + } on Exception { + _performConnectionDisconnect(); + } + // Cancel the connect timer; + MqttLogger.log('MqttConnectionHandlerBase:: cancelling connect timer'); + connectTimer.cancel(); + return true; + } + + /// Connect acknowledge recieved + void connectAckReceived(ConnectAckMessageAvailable event) { + connectAckProcessor(event.message!); + } + + /// Initialise the event listeners; + void initialiseListeners() { + clientEventBus!.on().listen(autoReconnect); + clientEventBus!.on().listen(messageAvailable); + clientEventBus!.on().listen(connectAckReceived); + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/mqtt_client_mqtt_connection_keep_alive.dart b/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/mqtt_client_mqtt_connection_keep_alive.dart new file mode 100755 index 0000000..9ec32bb --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/mqtt_client_mqtt_connection_keep_alive.dart @@ -0,0 +1,115 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 22/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Ping response received callback +typedef PongCallback = void Function(); + +/// Implements keep alive functionality on the Mqtt Connection, +/// ensuring that the connection remains active according to the +/// keep alive seconds setting. +/// This class implements the keep alive by sending an MqttPingRequest +/// to the broker if a message has not been sent or received +/// within the keep alive period. +class MqttConnectionKeepAlive { + /// Initializes a new instance of the MqttConnectionKeepAlive class. + MqttConnectionKeepAlive( + IMqttConnectionHandler connectionHandler, int keepAliveSeconds) { + _connectionHandler = connectionHandler; + keepAlivePeriod = keepAliveSeconds * 1000; + // Register for message handling of ping request and response messages. + connectionHandler.registerForMessage( + MqttMessageType.pingRequest, pingRequestReceived); + connectionHandler.registerForMessage( + MqttMessageType.pingResponse, pingResponseReceived); + connectionHandler.registerForAllSentMessages(messageSent); + // Start the timer so we do a ping whenever required. + pingTimer = Timer(Duration(milliseconds: keepAlivePeriod), pingRequired); + MqttLogger.log( + 'MqttConnectionKeepAlive:: initialised with a keep alive value of $keepAliveSeconds seconds'); + } + + /// The keep alive period in milliseconds + late int keepAlivePeriod; + + /// The timer that manages the ping callbacks. + Timer? pingTimer; + + /// The connection handler + late IMqttConnectionHandler _connectionHandler; + + /// Used to synchronise shutdown and ping operations. + bool _shutdownPadlock = false; + + /// Ping response received callback + PongCallback? pongCallback; + + /// Pings the message broker if there has been no activity for + /// the specified amount of idle time. + bool pingRequired() { + MqttLogger.log('MqttConnectionKeepAlive::pingRequired'); + if (_shutdownPadlock) { + return false; + } else { + _shutdownPadlock = true; + } + var pinged = false; + final pingMsg = MqttPingRequestMessage(); + if (_connectionHandler.connectionStatus.state == + MqttConnectionState.connected) { + MqttLogger.log( + 'MqttConnectionKeepAlive::pingRequired - sending ping request'); + _connectionHandler.sendMessage(pingMsg); + pinged = true; + } else { + MqttLogger.log( + 'MqttConnectionKeepAlive::pingRequired - NOT sending ping - not connected'); + } + MqttLogger.log( + 'MqttConnectionKeepAlive::pingRequired - restarting ping timer'); + pingTimer = Timer(Duration(milliseconds: keepAlivePeriod), pingRequired); + _shutdownPadlock = false; + return pinged; + } + + /// A ping request has been received from the message broker. + /// The effect of calling this method on the keep alive handler is the + /// transmission of a ping response message to the message broker on + /// the current connection. + bool pingRequestReceived(MqttMessage? pingMsg) { + MqttLogger.log('MqttConnectionKeepAlive::pingRequestReceived'); + if (_shutdownPadlock) { + return false; + } else { + _shutdownPadlock = true; + } + final pingMsg = MqttPingResponseMessage(); + _connectionHandler.sendMessage(pingMsg); + _shutdownPadlock = false; + return true; + } + + /// Processed ping response messages received from a message broker. + bool pingResponseReceived(MqttMessage? pingMsg) { + MqttLogger.log('MqttConnectionKeepAlive::pingResponseReceived'); + // Call the pong callback if not null + if (pongCallback != null) { + pongCallback!(); + } + return true; + } + + /// Handles the MessageSent event of the connectionHandler control. + bool messageSent(MqttMessage? msg) => true; + + /// Stop the keep alive process + void stop() { + MqttLogger.log('MqttConnectionKeepAlive::stop - stopping keep alive'); + pingTimer!.cancel(); + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/mqtt_client_read_wrapper.dart b/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/mqtt_client_read_wrapper.dart new file mode 100755 index 0000000..2df73e0 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/mqtt_client_read_wrapper.dart @@ -0,0 +1,20 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 23/01/2020 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// State and logic used to read from the underlying network stream. +class ReadWrapper { + /// Creates a new ReadWrapper that wraps the state used to read + /// a message from a stream. + ReadWrapper() { + messageBytes = []; + } + + /// The bytes associated with the message being read. + List? messageBytes; +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/server/mqtt_client_mqtt_server_connection.dart b/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/server/mqtt_client_mqtt_server_connection.dart new file mode 100755 index 0000000..a768ecd --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/server/mqtt_client_mqtt_server_connection.dart @@ -0,0 +1,97 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 22/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_server_client; + +/// The MQTT client server connection base class +class MqttServerConnection extends MqttConnectionBase { + /// Default constructor + MqttServerConnection(clientEventBus) : super(clientEventBus); + + /// Initializes a new instance of the MqttConnection class. + MqttServerConnection.fromConnect(server, port, clientEventBus) + : super(clientEventBus) { + connect(server, port); + } + + /// Connect, must be overridden in connection classes + @override + Future connect(String server, int port) { + final completer = Completer(); + return completer.future; + } + + /// Connect for auto reconnect , must be overridden in connection classes + @override + Future connectAuto(String server, int port) { + final completer = Completer(); + return completer.future; + } + + /// Create the listening stream subscription and subscribe the callbacks + void _startListening() { + MqttLogger.log('MqttServerConnection::_startListening'); + try { + client.listen(_onData, onError: onError, onDone: onDone); + } on Exception catch (e) { + print('MqttServerConnection::_startListening - exception raised $e'); + } + } + + /// OnData listener callback + void _onData(dynamic data) { + MqttLogger.log('MqttConnection::_onData'); + // Protect against 0 bytes but should never happen. + if (data.length == 0) { + MqttLogger.log('MqttServerConnection::_ondata - Error - 0 byte message'); + return; + } + + messageStream.addAll(data); + + while (messageStream.isMessageAvailable()) { + var messageIsValid = true; + MqttMessage? msg; + + try { + msg = MqttMessage.createFrom(messageStream); + } on Exception { + MqttLogger.log( + 'MqttServerConnection::_ondata - message is not yet valid, ' + 'waiting for more data ...'); + messageIsValid = false; + } + if (!messageIsValid) { + messageStream.reset(); + return; + } + if (messageIsValid) { + messageStream.shrink(); + MqttLogger.log( + 'MqttServerConnection::_onData - message received ', msg); + if (!clientEventBus!.streamController.isClosed) { + if (msg!.header!.messageType == MqttMessageType.connectAck) { + clientEventBus!.fire(ConnectAckMessageAvailable(msg)); + } else { + clientEventBus!.fire(MessageAvailable(msg)); + } + MqttLogger.log( + 'MqttServerConnection::_onData - message available event fired'); + } else { + MqttLogger.log( + 'MqttServerConnection::_onData - WARN - message available event not fired, event bus is closed'); + } + } + } + } + + /// Sends the message in the stream to the broker. + void send(MqttByteBuffer message) { + final messageBytes = message.read(message.length); + client?.add(messageBytes.toList()); + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/server/mqtt_client_mqtt_server_connection_handler.dart b/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/server/mqtt_client_mqtt_server_connection_handler.dart new file mode 100755 index 0000000..6178c0d --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/server/mqtt_client_mqtt_server_connection_handler.dart @@ -0,0 +1,38 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 22/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_server_client; + +/// This class provides specific connection functionality +/// for server based connections. +abstract class MqttServerConnectionHandler extends MqttConnectionHandlerBase { + /// Initializes a new instance of the [MqttServerConnectionHandler] class. + MqttServerConnectionHandler(var clientEventBus, + {required int? maxConnectionAttempts}) + : super(clientEventBus, maxConnectionAttempts: maxConnectionAttempts); + + /// Use a websocket rather than TCP + bool useWebSocket = false; + + /// Alternate websocket implementation. + /// + /// The Amazon Web Services (AWS) IOT MQTT interface(and maybe others) + /// has a bug that causes it not to connect if unexpected message headers are + /// present in the initial GET message during the handshake. + /// Since the httpclient classes insist on adding those headers, an alternate + /// method is used to perform the handshake. + /// After the handshake everything goes back to the normal websocket class. + /// Only use this websocket implementation if you know it is needed + /// by your broker. + bool useAlternateWebSocketImplementation = false; + + /// If set use a secure connection, note TCP only, not websocket. + bool secure = false; + + /// The security context for secure usage + dynamic securityContext; +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/server/mqtt_client_mqtt_server_normal_connection.dart b/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/server/mqtt_client_mqtt_server_normal_connection.dart new file mode 100755 index 0000000..516dab4 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/server/mqtt_client_mqtt_server_normal_connection.dart @@ -0,0 +1,85 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 22/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_server_client; + +/// The MQTT normal(insecure TCP) server connection class +class MqttServerNormalConnection extends MqttServerConnection { + /// Default constructor + MqttServerNormalConnection(events.EventBus? eventBus) : super(eventBus); + + /// Initializes a new instance of the MqttConnection class. + MqttServerNormalConnection.fromConnect( + String server, int port, events.EventBus eventBus) + : super(eventBus) { + connect(server, port); + } + + /// Connect + @override + Future connect(String server, int port) { + final completer = Completer(); + MqttLogger.log('MqttNormalConnection::connect - entered'); + try { + // Connect and save the socket. + Socket.connect(server, port).then((dynamic socket) { + client = socket; + readWrapper = ReadWrapper(); + messageStream = MqttByteBuffer(typed.Uint8Buffer()); + _startListening(); + completer.complete(); + }).catchError((dynamic e) { + onError(e); + completer.completeError(e); + }); + } on SocketException catch (e) { + final message = + 'MqttNormalConnection::connect - The connection to the message broker ' + '{$server}:{$port} could not be made. Error is ${e.toString()}'; + completer.completeError(e); + throw NoConnectionException(message); + } on Exception catch (e) { + completer.completeError(e); + final message = + 'MqttNormalConnection::Connect - The connection to the message ' + 'broker {$server}:{$port} could not be made.'; + throw NoConnectionException(message); + } + return completer.future; + } + + /// Connect Auto + @override + Future connectAuto(String server, int port) { + final completer = Completer(); + MqttLogger.log('MqttNormalConnection::connectAuto - entered'); + try { + // Connect and save the socket. + Socket.connect(server, port).then((dynamic socket) { + client = socket; + _startListening(); + completer.complete(); + }).catchError((dynamic e) { + onError(e); + completer.completeError(e); + }); + } on SocketException catch (e) { + final message = + 'MqttNormalConnection::connectAuto - The connection to the message broker ' + '{$server}:{$port} could not be made. Error is ${e.toString()}'; + completer.completeError(e); + throw NoConnectionException(message); + } on Exception catch (e) { + completer.completeError(e); + final message = + 'MqttNormalConnection::ConnectAuto - The connection to the message ' + 'broker {$server}:{$port} could not be made.'; + throw NoConnectionException(message); + } + return completer.future; + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/server/mqtt_client_mqtt_server_secure_connection.dart b/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/server/mqtt_client_mqtt_server_secure_connection.dart new file mode 100755 index 0000000..37e30bb --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/server/mqtt_client_mqtt_server_secure_connection.dart @@ -0,0 +1,108 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 02/10/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_server_client; + +/// The MQTT server secure connection class +class MqttServerSecureConnection extends MqttServerConnection { + /// Default constructor + MqttServerSecureConnection( + this.context, events.EventBus? eventBus, this.onBadCertificate) + : super(eventBus); + + /// Initializes a new instance of the MqttSecureConnection class. + MqttServerSecureConnection.fromConnect( + String server, int port, events.EventBus eventBus) + : super(eventBus) { + connect(server, port); + } + + /// The security context for secure usage + SecurityContext? context; + + /// Callback function to handle bad certificate. if true, ignore the error. + bool Function(X509Certificate certificate)? onBadCertificate; + + /// Connect + @override + Future connect(String server, int port) { + final completer = Completer(); + MqttLogger.log('MqttSecureConnection::connect - entered'); + try { + SecureSocket.connect(server, port, + onBadCertificate: onBadCertificate, context: context) + .then((SecureSocket socket) { + MqttLogger.log('MqttSecureConnection::connect - securing socket'); + client = socket; + readWrapper = ReadWrapper(); + messageStream = MqttByteBuffer(typed.Uint8Buffer()); + MqttLogger.log('MqttSecureConnection::connect - start listening'); + _startListening(); + completer.complete(); + }).catchError((dynamic e) { + onError(e); + completer.completeError(e); + }); + } on SocketException catch (e) { + final message = + 'MqttSecureConnection::connect - The connection to the message broker ' + '{$server}:{$port} could not be made. Error is ${e.toString()}'; + completer.completeError(e); + throw NoConnectionException(message); + } on HandshakeException catch (e) { + final message = + 'MqttSecureConnection::connect - Handshake exception to the message broker ' + '{$server}:{$port}. Error is ${e.toString()}'; + completer.completeError(e); + throw NoConnectionException(message); + } on TlsException catch (e) { + final message = 'MqttSecureConnection::TLS exception raised on secure ' + 'connection. Error is ${e.toString()}'; + throw NoConnectionException(message); + } + return completer.future; + } + + /// Connect Auto + @override + Future connectAuto(String server, int port) { + final completer = Completer(); + MqttLogger.log('MqttSecureConnection::connectAuto - entered'); + try { + SecureSocket.connect(server, port, + onBadCertificate: onBadCertificate, context: context) + .then((SecureSocket socket) { + MqttLogger.log('MqttSecureConnection::connectAuto - securing socket'); + client = socket; + MqttLogger.log('MqttSecureConnection::connectAuto - start listening'); + _startListening(); + completer.complete(); + }).catchError((dynamic e) { + onError(e); + completer.completeError(e); + }); + } on SocketException catch (e) { + final message = + 'MqttSecureConnection::connectAuto - The connection to the message broker ' + '{$server}:{$port} could not be made. Error is ${e.toString()}'; + completer.completeError(e); + throw NoConnectionException(message); + } on HandshakeException catch (e) { + final message = + 'MqttSecureConnection::connectAuto - Handshake exception to the message broker ' + '{$server}:{$port}. Error is ${e.toString()}'; + completer.completeError(e); + throw NoConnectionException(message); + } on TlsException catch (e) { + final message = + 'MqttSecureConnection::connectAuto - TLS exception raised on secure ' + 'connection. Error is ${e.toString()}'; + throw NoConnectionException(message); + } + return completer.future; + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/server/mqtt_client_mqtt_server_ws2_connection.dart b/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/server/mqtt_client_mqtt_server_ws2_connection.dart new file mode 100755 index 0000000..fd27c30 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/server/mqtt_client_mqtt_server_ws2_connection.dart @@ -0,0 +1,356 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 02/10/2017 + * Copyright : S.Hamblett + * 01/19/2019 : Don Edvalson - Added this alternate websocket class to work around AWS deficiencies. + */ + +part of mqtt_server_client; + +/// Detatched socket class for alternative websocket support +class _DetachedSocket extends Stream implements Socket { + _DetachedSocket(this._socket, this._subscription); + + final StreamSubscription? _subscription; + final Socket _socket; + + @override + StreamSubscription listen(void Function(Uint8List event)? onData, + {Function? onError, void Function()? onDone, bool? cancelOnError}) { + _subscription! + ..onData(onData) + ..onError(onError) + ..onDone(onDone); + return _subscription!; + } + + @override + Encoding get encoding => _socket.encoding; + + @override + set encoding(Encoding value) => _socket.encoding = value; + + @override + void write(Object? obj) => _socket.write(obj); + + @override + void writeln([Object? obj = '']) => _socket.writeln(obj); + + @override + void writeCharCode(int charCode) => _socket.writeCharCode(charCode); + + @override + void writeAll(Iterable objects, [String separator = '']) => + _socket.writeAll(objects, separator); + + @override + void add(List bytes) => _socket.add(bytes); + + @override + void addError(Object error, [StackTrace? stackTrace]) => + _socket.addError(error, stackTrace); + + @override + Future addStream(Stream> stream) => + _socket.addStream(stream); + + @override + void destroy() => _socket.destroy(); + + @override + Future flush() => _socket.flush(); + + @override + Future close() => _socket.close(); + + @override + Future get done => _socket.done; + + @override + int get port => _socket.port; + + @override + InternetAddress get address => _socket.address; + + @override + InternetAddress get remoteAddress => _socket.remoteAddress; + + @override + int get remotePort => _socket.remotePort; + + @override + bool setOption(SocketOption option, bool enabled) => + _socket.setOption(option, enabled); + + @override + Uint8List getRawOption(RawSocketOption option) => + _socket.getRawOption(option); + + @override + void setRawOption(RawSocketOption option) => _socket.setRawOption(option); +} + +/// The MQTT server alternative websocket connection class +class MqttServerWs2Connection extends MqttServerConnection { + /// Default constructor + MqttServerWs2Connection(this.context, events.EventBus? eventBus) + : super(eventBus); + + /// Initializes a new instance of the MqttWs2Connection class. + MqttServerWs2Connection.fromConnect( + String server, int port, events.EventBus eventBus) + : super(eventBus) { + connect(server, port); + } + + /// The websocket subprotocol list + List protocols = MqttClientConstants.protocolsMultipleDefault; + + /// The security context for secure usage + SecurityContext? context; + + StreamSubscription? _subscription; + + /// Connect + @override + Future connect(String server, int port) { + final completer = Completer(); + MqttLogger.log('MqttWs2Connection::connect - entered'); + Uri uri; + try { + uri = Uri.parse(server); + } on Exception { + final message = + 'MqttWsConnection::connect - The URI supplied for the WS2 connection ' + 'is not valid - $server'; + throw NoConnectionException(message); + } + if (uri.scheme != 'wss') { + final message = + 'MqttWsConnection::connect - The URI supplied for the WS2 has an ' + 'incorrect scheme - $server'; + throw NoConnectionException(message); + } + uri = uri.replace(port: port); + + final uriString = uri.toString(); + MqttLogger.log( + 'MqttWs2Connection::connect - WS URL is $uriString, protocols are $protocols'); + + try { + SecureSocket.connect(uri.host, uri.port, context: context) + .then((Socket socket) { + MqttLogger.log('MqttWs2Connection::connect - securing socket'); + _performWSHandshake(socket, uri).then((bool b) { + client = WebSocket.fromUpgradedSocket( + _DetachedSocket( + socket, _subscription as StreamSubscription?), + serverSide: false); + readWrapper = ReadWrapper(); + messageStream = MqttByteBuffer(typed.Uint8Buffer()); + MqttLogger.log('MqttWs2Connection::connect - start listening'); + _startListening(); + completer.complete(); + }).catchError((dynamic e) { + onError(e); + completer.completeError(e); + }); + }); + } on SocketException catch (e) { + final message = + 'MqttWs2Connection::connect - The connection to the message broker ' + '{$server}:{$port} could not be made. Error is ${e.toString()}'; + completer.completeError(e); + throw NoConnectionException(message); + } on HandshakeException catch (e) { + final message = + 'MqttWs2Connection::connect - Handshake exception to the message broker ' + '{$server}:{$port}. Error is ${e.toString()}'; + completer.completeError(e); + throw NoConnectionException(message); + } on TlsException catch (e) { + final message = + 'MqttWs2Connection::connect - TLS exception raised on secure connection. ' + 'Error is ${e.toString()}'; + throw NoConnectionException(message); + } + return completer.future; + } + + /// Connect Auto + @override + Future connectAuto(String server, int port) { + final completer = Completer(); + MqttLogger.log('MqttWs2Connection::connectAuto - entered'); + Uri uri; + try { + uri = Uri.parse(server); + } on Exception { + final message = + 'MqttWsConnection::connectAuto - The URI supplied for the WS2 connection ' + 'is not valid - $server'; + throw NoConnectionException(message); + } + if (uri.scheme != 'wss') { + final message = + 'MqttWsConnection::connectAuto - The URI supplied for the WS2 has an ' + 'incorrect scheme - $server'; + throw NoConnectionException(message); + } + uri = uri.replace(port: port); + + final uriString = uri.toString(); + MqttLogger.log( + 'MqttWs2Connection::connectAuto - WS URL is $uriString, protocols are $protocols'); + + try { + SecureSocket.connect(uri.host, uri.port, context: context) + .then((Socket socket) { + MqttLogger.log('MqttWs2Connection::connectAuto - securing socket'); + _performWSHandshake(socket, uri).then((bool b) { + client = WebSocket.fromUpgradedSocket( + _DetachedSocket( + socket, _subscription as StreamSubscription?), + serverSide: false); + MqttLogger.log('MqttWs2Connection::connectAuto - start listening'); + _startListening(); + completer.complete(); + }).catchError((dynamic e) { + onError(e); + completer.completeError(e); + }); + }); + } on SocketException catch (e) { + final message = + 'MqttWs2Connection::connectAuto - The connection to the message broker ' + '{$server}:{$port} could not be made. Error is ${e.toString()}'; + completer.completeError(e); + throw NoConnectionException(message); + } on HandshakeException catch (e) { + final message = + 'MqttWs2Connection::connectAuto - Handshake exception to the message broker ' + '{$server}:{$port}. Error is ${e.toString()}'; + completer.completeError(e); + throw NoConnectionException(message); + } on TlsException catch (e) { + final message = + 'MqttWs2Connection::connectAuto - TLS exception raised on secure connection. ' + 'Error is ${e.toString()}'; + throw NoConnectionException(message); + } + return completer.future; + } + + Future _performWSHandshake(Socket socket, Uri uri) async { + _response = ''; + final c = Completer(); + const endL = '\r\n'; + final path = '${uri.path}?${uri.query}'; + final host = '${uri.host}:${uri.port.toString()}'; + final now = DateTime.now().millisecondsSinceEpoch; + final key = 'mqtt-$now'; + final key64 = base64.encode(utf8.encode(key)); + + var request = 'GET $path HTTP/1.1 $endL'; + request += 'Host: $host$endL'; + request += 'Upgrade: websocket$endL'; + request += 'Connection: Upgrade$endL'; + request += 'Sec-WebSocket-Key: $key64$endL'; + request += 'Sec-WebSocket-Protocol: ${protocols.join(' ').trim()}$endL'; + request += 'Sec-WebSocket-Version: 13$endL'; + request += endL; + socket.write(request); + _subscription = socket.listen((Uint8List data) { + var s = String.fromCharCodes(data); + s = s.replaceAll('\r', ''); + if (!_parseResponse(s, key64)) { + c.complete(true); + } + }, onDone: () { + _subscription!.cancel(); + const message = 'MqttWs2Connection::TLS connection unexpectedly closed'; + throw NoConnectionException(message); + }); + return c.future; + } +} + +late String _response; +bool _parseResponse(String resp, String key) { + _response += resp; + final bodyOffset = _response.indexOf('\n\n'); + // if we don't have a double newline yet we need to go back for more. + if (bodyOffset < 0) { + return true; + } + final lines = _response.substring(0, bodyOffset).split('\n'); + if (lines.isEmpty) { + throw NoConnectionException( + 'MqttWs2Connection::server returned invalid response'); + } + // split apart the status line + final status = lines[0].split(' '); + if (status.length < 3) { + throw NoConnectionException( + 'MqttWs2Connection::server returned malformed status line'); + } + // make a map of the headers + final headers = {}; + lines.removeAt(0); + for (final l in lines) { + final space = l.indexOf(' '); + if (space < 0) { + throw NoConnectionException( + 'MqttWs2Connection::server returned malformed header line'); + } + headers[l.substring(0, space - 1).toLowerCase()] = l.substring(space + 1); + } + var body = ''; + // if we have a Content-Length key we can't stop till we read the body. + if (headers.containsKey('content-length')) { + final bodyLength = int.parse(headers['content-length']!); + if (_response.length < bodyOffset + bodyLength + 2) { + return true; + } + body = _response.substring(bodyOffset, bodyOffset + bodyLength + 2); + } + // if we make it to here we have read all we are going to read. + // now lets see if we like what we found. + if (status[1] != '101') { + throw NoConnectionException( + 'MqttWs2Connection::server refused to upgrade, response = ' + '${status[1]} - ${status[2]} - $body'); + } + + if (!headers.containsKey('connection') || + headers['connection']!.toLowerCase() != 'upgrade') { + throw NoConnectionException( + 'MqttWs2Connection::server returned improper connection header line'); + } + if (!headers.containsKey('upgrade') || + headers['upgrade']!.toLowerCase() != 'websocket') { + throw NoConnectionException( + 'MqttWs2Connection::server returned improper upgrade header line'); + } + if (!headers.containsKey('sec-websocket-protocol')) { + throw NoConnectionException( + 'MqttWs2Connection::server failed to return protocol header'); + } + if (!headers.containsKey('sec-websocket-accept')) { + throw NoConnectionException( + 'MqttWs2Connection::server failed to return accept header'); + } + // We build up the accept in the same way the server should + // then we check that the response is the same. + + // Do not change: https://tools.ietf.org/html/rfc6455#section-1.3 + const acceptSalt = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; + + final sha1Bytes = sha1.convert(utf8.encode(key + acceptSalt)); + final encodedSha1Bytes = base64.encode(sha1Bytes.bytes); + if (encodedSha1Bytes != headers['sec-websocket-accept']) { + throw NoConnectionException('MqttWs2Connection::handshake mismatch'); + } + return false; +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/server/mqtt_client_mqtt_server_ws_connection.dart b/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/server/mqtt_client_mqtt_server_ws_connection.dart new file mode 100755 index 0000000..6d27f0a --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/server/mqtt_client_mqtt_server_ws_connection.dart @@ -0,0 +1,149 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 14/08/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_server_client; + +/// The MQTT server connection class for the websocket interface +class MqttServerWsConnection extends MqttServerConnection { + /// Default constructor + MqttServerWsConnection(events.EventBus? eventBus) : super(eventBus); + + /// Initializes a new instance of the MqttConnection class. + MqttServerWsConnection.fromConnect( + String server, int port, events.EventBus eventBus) + : super(eventBus) { + connect(server, port); + } + + /// The websocket subprotocol list + List protocols = MqttClientConstants.protocolsMultipleDefault; + + /// Connect + @override + Future connect(String server, int port) { + final completer = Completer(); + MqttLogger.log('MqttWsConnection::connect - entered'); + // Add the port if present + Uri uri; + try { + uri = Uri.parse(server); + } on Exception { + final message = 'MqttWsConnection::connect - The URI supplied for the WS ' + 'connection is not valid - $server'; + throw NoConnectionException(message); + } + if (uri.scheme != 'ws' && uri.scheme != 'wss') { + final message = + 'MqttWsConnection::connect - The URI supplied for the WS has ' + 'an incorrect scheme - $server'; + throw NoConnectionException(message); + } + + uri = uri.replace(port: port); + + final uriString = uri.toString(); + MqttLogger.log( + 'MqttWsConnection::connect - WS URL is $uriString, protocols are $protocols'); + try { + // Connect and save the socket. + WebSocket.connect(uriString, + protocols: protocols.isNotEmpty ? protocols : null) + .then((dynamic socket) { + client = socket; + readWrapper = ReadWrapper(); + messageStream = MqttByteBuffer(typed.Uint8Buffer()); + _startListening(); + completer.complete(); + }).catchError((dynamic e) { + onError(e); + completer.completeError(e); + }); + } on Exception { + final message = + 'MqttWsConnection::connect - The connection to the message broker ' + '{$uriString} could not be made.'; + throw NoConnectionException(message); + } + return completer.future; + } + + /// Connect Auto + @override + Future connectAuto(String server, int port) { + final completer = Completer(); + MqttLogger.log('MqttWsConnection::connectAuto - entered'); + // Add the port if present + Uri uri; + try { + uri = Uri.parse(server); + } on Exception { + final message = + 'MqttWsConnection::connectAuto - The URI supplied for the WS ' + 'connection is not valid - $server'; + throw NoConnectionException(message); + } + if (uri.scheme != 'ws' && uri.scheme != 'wss') { + final message = + 'MqttWsConnection::connectAuto - The URI supplied for the WS has ' + 'an incorrect scheme - $server'; + throw NoConnectionException(message); + } + + uri = uri.replace(port: port); + + final uriString = uri.toString(); + MqttLogger.log( + 'MqttWsConnection::connectAuto - WS URL is $uriString, protocols are $protocols'); + try { + // Connect and save the socket. + WebSocket.connect(uriString, + protocols: protocols.isNotEmpty ? protocols : null) + .then((dynamic socket) { + client = socket; + _startListening(); + completer.complete(); + }).catchError((dynamic e) { + onError(e); + completer.completeError(e); + }); + } on Exception { + final message = + 'MqttWsConnection::connectAuto - The connection to the message broker ' + '{$uriString} could not be made.'; + throw NoConnectionException(message); + } + return completer.future; + } + + /// User requested or auto disconnect disconnection + @override + void disconnect({bool auto = false}) { + if (auto) { + client = null; + } else { + onDone(); + } + } + + /// OnDone listener callback + @override + void onDone() { + _disconnect(); + if (onDisconnected != null) { + MqttLogger.log( + 'MqttWsConnection::::onDone - calling disconnected callback'); + onDisconnected!(); + } + } + + void _disconnect() { + if (client != null) { + client.close(); + client = null; + } + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/server/mqtt_client_synchronous_mqtt_server_connection_handler.dart b/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/server/mqtt_client_synchronous_mqtt_server_connection_handler.dart new file mode 100755 index 0000000..ddbec03 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/connectionhandling/server/mqtt_client_synchronous_mqtt_server_connection_handler.dart @@ -0,0 +1,133 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 22/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_server_client; + +/// Connection handler that performs server based connections and disconnections +/// to the hostname in a synchronous manner. +class SynchronousMqttServerConnectionHandler + extends MqttServerConnectionHandler { + /// Initializes a new instance of the SynchronousMqttConnectionHandler class. + SynchronousMqttServerConnectionHandler( + clientEventBus, { + required int maxConnectionAttempts, + }) : super(clientEventBus, maxConnectionAttempts: maxConnectionAttempts) { + connectTimer = MqttCancellableAsyncSleep(5000); + initialiseListeners(); + } + + /// Synchronously connect to the specific Mqtt Connection. + @override + Future internalConnect( + String? hostname, int? port, MqttConnectMessage? connectMessage) async { + var connectionAttempts = 0; + MqttLogger.log( + 'SynchronousMqttServerConnectionHandler::internalConnect entered'); + do { + // Initiate the connection + MqttLogger.log( + 'SynchronousMqttServerConnectionHandler::internalConnect - ' + 'initiating connection try $connectionAttempts, auto reconnect in progress $autoReconnectInProgress'); + connectionStatus.state = MqttConnectionState.connecting; + connectionStatus.returnCode = MqttConnectReturnCode.noneSpecified; + // Don't reallocate the connection if this is an auto reconnect + if (!autoReconnectInProgress!) { + if (useWebSocket) { + if (useAlternateWebSocketImplementation) { + MqttLogger.log( + 'SynchronousMqttServerConnectionHandler::internalConnect - ' + 'alternate websocket implementation selected'); + connection = + MqttServerWs2Connection(securityContext, clientEventBus); + } else { + MqttLogger.log( + 'SynchronousMqttServerConnectionHandler::internalConnect - ' + 'websocket selected'); + connection = MqttServerWsConnection(clientEventBus); + } + if (websocketProtocols != null) { + connection.protocols = websocketProtocols; + } + } else if (secure) { + MqttLogger.log( + 'SynchronousMqttServerConnectionHandler::internalConnect - ' + 'secure selected'); + connection = MqttServerSecureConnection( + securityContext, clientEventBus, onBadCertificate); + } else { + MqttLogger.log( + 'SynchronousMqttServerConnectionHandler::internalConnect - ' + 'insecure TCP selected'); + connection = MqttServerNormalConnection(clientEventBus); + } + connection.onDisconnected = onDisconnected; + } + + // Connect + try { + if (!autoReconnectInProgress!) { + MqttLogger.log( + 'SynchronousMqttServerConnectionHandler::internalConnect - calling connect'); + await connection.connect(hostname, port); + } else { + MqttLogger.log( + 'SynchronousMqttServerConnectionHandler::internalConnect - calling connectAuto'); + await connection.connectAuto(hostname, port); + } + } on Exception { + // Ignore exceptions in an auto reconnect sequence + if (autoReconnectInProgress!) { + MqttLogger.log( + 'SynchronousMqttServerConnectionHandler::internalConnect' + ' exception thrown during auto reconnect - ignoring'); + } else { + rethrow; + } + } + MqttLogger.log( + 'SynchronousMqttServerConnectionHandler::internalConnect - ' + 'connection complete'); + // Transmit the required connection message to the broker. + MqttLogger.log('SynchronousMqttServerConnectionHandler::internalConnect ' + 'sending connect message'); + sendMessage(connectMessage); + MqttLogger.log( + 'SynchronousMqttServerConnectionHandler::internalConnect - ' + 'pre sleep, state = $connectionStatus'); + // We're the sync connection handler so we need to wait for the + // brokers acknowledgement of the connections + await connectTimer.sleep(); + MqttLogger.log( + 'SynchronousMqttServerConnectionHandler::internalConnect - ' + 'post sleep, state = $connectionStatus'); + } while (connectionStatus.state != MqttConnectionState.connected && + ++connectionAttempts < maxConnectionAttempts!); + // If we've failed to handshake with the broker, throw an exception. + if (connectionStatus.state != MqttConnectionState.connected) { + if (!autoReconnectInProgress!) { + MqttLogger.log( + 'SynchronousMqttServerConnectionHandler::internalConnect failed'); + if (connectionStatus.returnCode == + MqttConnectReturnCode.noneSpecified) { + throw NoConnectionException('The maximum allowed connection attempts ' + '({$maxConnectionAttempts}) were exceeded. ' + 'The broker is not responding to the connection request message ' + '(Missing Connection Acknowledgement?'); + } else { + throw NoConnectionException('The maximum allowed connection attempts ' + '({$maxConnectionAttempts}) were exceeded. ' + 'The broker is not responding to the connection request message correctly ' + 'The return code is ${connectionStatus.returnCode}'); + } + } + } + MqttLogger.log('SynchronousMqttServerConnectionHandler::internalConnect ' + 'exited with state $connectionStatus'); + initialConnectionComplete = true; + return connectionStatus; + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/dataconvertors/mqtt_client_ascii_payload_convertor.dart b/bytedesk_kefu/lib/mqtt/lib/src/dataconvertors/mqtt_client_ascii_payload_convertor.dart new file mode 100755 index 0000000..64f10d2 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/dataconvertors/mqtt_client_ascii_payload_convertor.dart @@ -0,0 +1,27 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 31/05/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Converts string data to and from the MQTT wire format +class AsciiPayloadConverter implements PayloadConverter { + /// Processes received data and returns it as a string. + @override + String convertFromBytes(typed.Uint8Buffer messageData) { + const decoder = Utf8Decoder(); + return decoder.convert(messageData.toList()); + } + + /// Converts sent data from a string to a byte array. + @override + typed.Uint8Buffer convertToBytes(String data) { + const encoder = Utf8Encoder(); + final buff = typed.Uint8Buffer(); + buff.addAll(encoder.convert(data)); + return buff; + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/dataconvertors/mqtt_client_passthru_payload_convertor.dart b/bytedesk_kefu/lib/mqtt/lib/src/dataconvertors/mqtt_client_passthru_payload_convertor.dart new file mode 100755 index 0000000..b24706d --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/dataconvertors/mqtt_client_passthru_payload_convertor.dart @@ -0,0 +1,20 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 31/05/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Acts as a pass through for the raw data without doing any conversion. +class PassthruPayloadConverter implements PayloadConverter { + /// Processes received data and returns it as a byte array. + @override + typed.Uint8Buffer convertFromBytes(typed.Uint8Buffer messageData) => + messageData; + + /// Converts sent data from an object graph to a byte array. + @override + typed.Uint8Buffer convertToBytes(typed.Uint8Buffer data) => data; +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/dataconvertors/mqtt_client_payload_convertor.dart b/bytedesk_kefu/lib/mqtt/lib/src/dataconvertors/mqtt_client_payload_convertor.dart new file mode 100755 index 0000000..fc16d9e --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/dataconvertors/mqtt_client_payload_convertor.dart @@ -0,0 +1,33 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 31/05/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Interface that defines the methods and properties that must be provided +/// by classes that interpret and convert inbound and outbound +/// published message data. +/// +/// Types that implement this interface should be aware that for the +/// purposes of converting data from published messages +/// (byte array to object model) that the MqttSubscriptionsManager +/// creates a single instance of the data converter and uses it for +/// all messages that are received. +/// +/// The same is true for the publishing of data to a broker. +/// The PublishingManager will also cache instances of the converters +/// until the MqttClient is disposed. +/// This means, in both cases you can store state in the data +/// converters if you wish, and that state will persist between messages +/// received or published, but only a default empty constructor is +/// supported. +abstract class PayloadConverter { + /// Converts received data from a raw byte array to an object graph. + T convertFromBytes(typed.Uint8Buffer messageData); + + /// Converts sent data from an object graph to a byte array. + typed.Uint8Buffer convertToBytes(T data); +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/encoding/mqtt_client_mqtt_encoding.dart b/bytedesk_kefu/lib/mqtt/lib/src/encoding/mqtt_client_mqtt_encoding.dart new file mode 100755 index 0000000..3814b21 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/encoding/mqtt_client_mqtt_encoding.dart @@ -0,0 +1,58 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 31/05/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Encoding implementation that can encode and decode strings +/// in the MQTT string format. +/// +/// The MQTT string format is simply a pascal string with ANSI character +/// encoding. The first 2 bytes define the length of the string, and they +/// are followed by the string itself. +class MqttEncoding extends Utf8Codec { + /// Encodes all the characters in the specified string + /// into a sequence of bytes. + typed.Uint8Buffer getBytes(String s) { + _validateString(s); + final stringBytes = typed.Uint8Buffer(); + stringBytes.add(s.length >> 8); + stringBytes.add(s.length & 0xFF); + stringBytes.addAll(encoder.convert(s)); + return stringBytes; + } + + /// Decodes the bytes in the specified byte array into a string. + String getString(typed.Uint8Buffer bytes) => decoder.convert(bytes.toList()); + + /// When overridden in a derived class, calculates the number of characters + /// produced by decoding all the bytes in the specified byte array. + int getCharCount(typed.Uint8Buffer bytes) { + if (bytes.length < 2) { + throw Exception( + 'mqtt_client::MQTTEncoding: Length byte array must comprise 2 bytes'); + } + return (bytes[0] << 8) + bytes[1]; + } + + /// Calculates the number of bytes produced by encoding the + /// characters in the specified. + int getByteCount(String chars) => getBytes(chars).length; + + /// Validates the string to ensure it doesn't contain any characters + /// invalid within the Mqtt string format. + static void _validateString(String s) { + for (var i = 0; i < s.length; i++) { + if (Protocol.version == MqttClientConstants.mqttV31ProtocolVersion) { + if (s.codeUnitAt(i) > 0x7F) { + throw Exception( + 'mqtt_client::MQTTEncoding: The input string has extended ' + 'UTF characters, which are not supported'); + } + } + } + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/exception/mqtt_client_client_identifier_exception.dart b/bytedesk_kefu/lib/mqtt/lib/src/exception/mqtt_client_client_identifier_exception.dart new file mode 100755 index 0000000..62cbfce --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/exception/mqtt_client_client_identifier_exception.dart @@ -0,0 +1,25 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 31/05/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Exception thrown when a client identifier included in a message is too long. +class ClientIdentifierException implements Exception { + /// Construct + ClientIdentifierException(String clientIdentifier) { + _message = + 'mqtt-client::ClientIdentifierException: Client id $clientIdentifier ' + 'is too long at ${clientIdentifier.length}, ' + 'Maximum ClientIdentifier length is ' + '${MqttClientConstants.maxClientIdentifierLength}'; + } + + late String _message; + + @override + String toString() => _message; +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/exception/mqtt_client_connection_exception.dart b/bytedesk_kefu/lib/mqtt/lib/src/exception/mqtt_client_connection_exception.dart new file mode 100755 index 0000000..de153fd --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/exception/mqtt_client_connection_exception.dart @@ -0,0 +1,25 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 31/05/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Exception thrown when the connection state is incorrect. +class ConnectionException implements Exception { + /// Construct + ConnectionException(MqttConnectionState? state) { + _message = 'mqtt-client::ConnectionException: The connection must be in ' + 'the Connected state in order to perform this operation.'; + if (null != state) { + _message = '$_message Current state is ${state.toString().split('.')[1]}'; + } + } + + late String _message; + + @override + String toString() => _message; +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/exception/mqtt_client_incorrect_instantiation_exception.dart b/bytedesk_kefu/lib/mqtt/lib/src/exception/mqtt_client_incorrect_instantiation_exception.dart new file mode 100755 index 0000000..985aba3 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/exception/mqtt_client_incorrect_instantiation_exception.dart @@ -0,0 +1,23 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 17/03/2020 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Exception thrown when a browser or server client is instantiated incorrectly. +class IncorrectInstantiationException implements Exception { + /// Construct + IncorrectInstantiationException() { + _message = + 'mqtt-client::ClientIncorrectInstantiationException: Incorrect instantiation, do not' + 'instantiate MqttClient directly, use MqttServerClient or MqttBrowserClient'; + } + + late String _message; + + @override + String toString() => _message; +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/exception/mqtt_client_invalid_header_exception.dart b/bytedesk_kefu/lib/mqtt/lib/src/exception/mqtt_client_invalid_header_exception.dart new file mode 100755 index 0000000..2b27456 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/exception/mqtt_client_invalid_header_exception.dart @@ -0,0 +1,21 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 31/05/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Exception thrown when processing a header that is invalid in some way. +class InvalidHeaderException implements Exception { + /// Construct + InvalidHeaderException(String text) { + _message = 'mqtt-client::InvalidHeaderException: $text'; + } + + late String _message; + + @override + String toString() => _message; +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/exception/mqtt_client_invalid_message_exception.dart b/bytedesk_kefu/lib/mqtt/lib/src/exception/mqtt_client_invalid_message_exception.dart new file mode 100755 index 0000000..bbd00fa --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/exception/mqtt_client_invalid_message_exception.dart @@ -0,0 +1,21 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 31/05/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Exception thrown when processing a Message that is invalid in some way. +class InvalidMessageException implements Exception { + /// Construct + InvalidMessageException(String text) { + _message = 'mqtt-client::InvalidMessageException: $text'; + } + + late String _message; + + @override + String toString() => _message; +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/exception/mqtt_client_invalid_payload_size_exception.dart b/bytedesk_kefu/lib/mqtt/lib/src/exception/mqtt_client_invalid_payload_size_exception.dart new file mode 100755 index 0000000..5b03125 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/exception/mqtt_client_invalid_payload_size_exception.dart @@ -0,0 +1,24 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 31/05/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Exception that is thrown when the payload of a message +/// is not the correct size. +class InvalidPayloadSizeException implements Exception { + /// Construct + InvalidPayloadSizeException(int size, int max) { + _message = 'mqtt-client::InvalidPayloadSizeException: The size of the ' + 'payload ($size bytes) must ' + 'be equal to or greater than 0 and less than $max bytes'; + } + + late String _message; + + @override + String toString() => _message; +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/exception/mqtt_client_invalid_topic_exception.dart b/bytedesk_kefu/lib/mqtt/lib/src/exception/mqtt_client_invalid_topic_exception.dart new file mode 100755 index 0000000..7722740 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/exception/mqtt_client_invalid_topic_exception.dart @@ -0,0 +1,21 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 31/05/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Exception thrown when the topic of a message is invalid +class InvalidTopicException implements Exception { + /// Construct + InvalidTopicException(String message, String topic) { + _message = 'mqtt-client::InvalidTopicException: Topic $topic is $message'; + } + + late String _message; + + @override + String toString() => _message; +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/exception/mqtt_client_noconnection_exception.dart b/bytedesk_kefu/lib/mqtt/lib/src/exception/mqtt_client_noconnection_exception.dart new file mode 100755 index 0000000..219c48e --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/exception/mqtt_client_noconnection_exception.dart @@ -0,0 +1,21 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 31/05/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Exception thrown when the client fails to connect +class NoConnectionException implements Exception { + /// Construct + NoConnectionException(String message) { + _message = 'mqtt-client::NoConnectionException: $message'; + } + + late String _message; + + @override + String toString() => _message; +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/management/mqtt_client_topic_filter.dart b/bytedesk_kefu/lib/mqtt/lib/src/management/mqtt_client_topic_filter.dart new file mode 100755 index 0000000..f3bfd1e --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/management/mqtt_client_topic_filter.dart @@ -0,0 +1,63 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 01/02/2019 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// This class allows specific topics to be listened for. It essentially +/// acts as a bandpass filter for the topics you are interested in if +/// you subscribe to more than one topic or use wildcard topics. +/// Simply construct it, and listen to its message stream rather than +/// that of the client. Note this class will only filter valid receive topics +/// so if you filter on wildcard topics for instance, which you should only +/// subscribe to, it will always generate a no match. +class MqttClientTopicFilter { + /// Construction + MqttClientTopicFilter(this._topic, this._clientUpdates) { + _subscriptionTopic = SubscriptionTopic(_topic); + _clientUpdates!.listen(_topicIn); + _updates = + StreamController>>.broadcast( + sync: true); + } + + final String _topic; + + late SubscriptionTopic _subscriptionTopic; + + /// The topic on which to filter + String get topic => _topic; + + final Stream>?>? _clientUpdates; + + late StreamController>> _updates; + + /// The stream on which all matching topic updates are published to + Stream>> get updates => + _updates.stream; + + void _topicIn(List>? c) { + String? lastTopic; + try { + // Pass through if we have a match + final List> tmp = + >[]; + for (final message in c!) { + lastTopic = message.topic; + if (_subscriptionTopic.matches(PublicationTopic(message.topic))) { + tmp.add(message); + } + } + if (tmp.isNotEmpty) { + _updates.add(tmp); + } + } on RangeError catch (e) { + MqttLogger.log('MqttClientTopicFilter::_topicIn - cannot process ' + 'received topic: $lastTopic'); + MqttLogger.log('MqttClientTopicFilter::_topicIn - exception is $e'); + } + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/messages/connect/mqtt_client_mqtt_connect_flags.dart b/bytedesk_kefu/lib/mqtt/lib/src/messages/connect/mqtt_client_mqtt_connect_flags.dart new file mode 100755 index 0000000..3cfeb5f --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/messages/connect/mqtt_client_mqtt_connect_flags.dart @@ -0,0 +1,79 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 31/05/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Represents the connect flags part of the MQTT Variable Header +class MqttConnectFlags { + /// Initializes a new instance of the MqttConnectFlags class. + MqttConnectFlags(); + + /// Initializes a new instance of the MqttConnectFlags class configured + /// as per the supplied stream. + MqttConnectFlags.fromByteBuffer(MqttByteBuffer connectFlagsStream) { + readFrom(connectFlagsStream); + } + + /// Reserved1 + bool reserved1 = false; + + /// Clean start + bool cleanStart = false; + + /// Will + bool willFlag = false; + + /// Will Qos + MqttQos willQos = MqttQos.atMostOnce; + + /// Will retain + bool willRetain = false; + + /// Password present + bool passwordFlag = false; + + /// Username present + bool usernameFlag = false; + + /// Return the connect flag value + int connectFlagByte() => + (reserved1 ? 1 : 0) | + (cleanStart ? 1 : 0) << 1 | + (willFlag ? 1 : 0) << 2 | + (willQos.index) << 3 | + (willRetain ? 1 : 0) << 5 | + (passwordFlag ? 1 : 0) << 6 | + (usernameFlag ? 1 : 0) << 7; + + /// Writes the connect flag byte to the supplied stream. + void writeTo(MqttByteBuffer connectFlagsStream) { + connectFlagsStream.writeByte(connectFlagByte()); + } + + /// Reads the connect flags from the underlying stream. + void readFrom(MqttByteBuffer stream) { + final connectFlagsByte = stream.readByte(); + + reserved1 = (connectFlagsByte & 1) == 1; + cleanStart = (connectFlagsByte & 2) == 2; + willFlag = (connectFlagsByte & 4) == 4; + willQos = MqttUtilities.getQosLevel((connectFlagsByte >> 3) & 3); + willRetain = (connectFlagsByte & 32) == 32; + passwordFlag = (connectFlagsByte & 64) == 64; + usernameFlag = (connectFlagsByte & 128) == 128; + } + + /// Gets the length of data written when WriteTo is called. + static int getWriteLength() => 1; + + /// Returns a String that represents the current connect flag settings + @override + String toString() => + 'Connect Flags: Reserved1=$reserved1, CleanStart=$cleanStart, ' + 'WillFlag=$willFlag, WillQos=$willQos, WillRetain=$willRetain, ' + 'PasswordFlag=$passwordFlag, UserNameFlag=$usernameFlag'; +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/messages/connect/mqtt_client_mqtt_connect_message.dart b/bytedesk_kefu/lib/mqtt/lib/src/messages/connect/mqtt_client_mqtt_connect_message.dart new file mode 100755 index 0000000..c3d0911 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/messages/connect/mqtt_client_mqtt_connect_message.dart @@ -0,0 +1,137 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 31/05/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// An Mqtt message that is used to initiate a connection to a message broker. +class MqttConnectMessage extends MqttMessage { + /// Initializes a new instance of the MqttConnectMessage class. + /// Only called via the MqttMessage.create operation during processing of + /// an Mqtt message stream. + MqttConnectMessage() { + header = MqttHeader().asType(MqttMessageType.connect); + variableHeader = MqttConnectVariableHeader(); + payload = MqttConnectPayload(variableHeader); + } + + /// Initializes a new instance of the MqttConnectMessage class. + MqttConnectMessage.fromByteBuffer( + MqttHeader header, MqttByteBuffer messageStream) { + this.header = header; + readFrom(messageStream); + } + + /// Sets the name of the protocol to use. + MqttConnectMessage withProtocolName(String protocolName) { + variableHeader!.protocolName = protocolName; + return this; + } + + /// Sets the protocol version. (Defaults to v3, the only protcol + /// version supported) + MqttConnectMessage withProtocolVersion(int protocolVersion) { + variableHeader!.protocolVersion = protocolVersion; + return this; + } + + /// Sets the startClean flag so that the broker drops any messages + /// that were previously destined for us. + MqttConnectMessage startClean() { + variableHeader!.connectFlags.cleanStart = true; + return this; + } + + /// Sets the keep alive period + @Deprecated( + 'This will be removed, you must now set this through the client keepAlivePeriod') + MqttConnectMessage keepAliveFor(int keepAliveSeconds) { + variableHeader!.keepAlive = keepAliveSeconds; + return this; + } + + /// Sets the Will flag of the variable header + MqttConnectMessage will() { + variableHeader!.connectFlags.willFlag = true; + return this; + } + + /// Sets the WillQos of the connect flag. + MqttConnectMessage withWillQos(MqttQos qos) { + variableHeader!.connectFlags.willQos = qos; + return this; + } + + /// Sets the WillRetain flag of the Connection Flags + MqttConnectMessage withWillRetain() { + variableHeader!.connectFlags.willRetain = true; + return this; + } + + /// Sets the client identifier of the message. + MqttConnectMessage withClientIdentifier(String clientIdentifier) { + payload.clientIdentifier = clientIdentifier; + return this; + } + + /// Sets the will message. + MqttConnectMessage withWillMessage(String willMessage) { + will(); + payload.willMessage = willMessage; + return this; + } + + /// Sets the Will Topic + MqttConnectMessage withWillTopic(String willTopic) { + will(); + payload.willTopic = willTopic; + return this; + } + + /// Sets the authentication + MqttConnectMessage authenticateAs(String? username, String? password) { + if (username != null) { + variableHeader!.connectFlags.usernameFlag = username.isNotEmpty; + payload.username = username; + } + if (password != null) { + variableHeader!.connectFlags.passwordFlag = password.isNotEmpty; + payload.password = password; + } + return this; + } + + /// The variable header contents. Contains extended metadata about the message + MqttConnectVariableHeader? variableHeader; + + /// The payload of the Mqtt Message. + late MqttConnectPayload payload; + + /// Writes the message to the supplied stream. + @override + void writeTo(MqttByteBuffer messageStream) { + header!.writeTo(variableHeader!.getWriteLength() + payload.getWriteLength(), + messageStream); + variableHeader!.writeTo(messageStream); + payload.writeTo(messageStream); + } + + /// Reads a message from the supplied stream. + @override + void readFrom(MqttByteBuffer messageStream) { + variableHeader = MqttConnectVariableHeader.fromByteBuffer(messageStream); + payload = MqttConnectPayload.fromByteBuffer(variableHeader, messageStream); + } + + @override + String toString() { + final sb = StringBuffer(); + sb.write(super.toString()); + sb.writeln(variableHeader.toString()); + sb.writeln(payload.toString()); + return sb.toString(); + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/messages/connect/mqtt_client_mqtt_connect_payload.dart b/bytedesk_kefu/lib/mqtt/lib/src/messages/connect/mqtt_client_mqtt_connect_payload.dart new file mode 100755 index 0000000..b133c12 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/messages/connect/mqtt_client_mqtt_connect_payload.dart @@ -0,0 +1,111 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 12/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Class that contains details related to an MQTT Connect messages payload. +class MqttConnectPayload extends MqttPayload { + /// Initializes a new instance of the MqttConnectPayload class. + MqttConnectPayload(this.variableHeader); + + /// Initializes a new instance of the MqttConnectPayload class. + MqttConnectPayload.fromByteBuffer( + this.variableHeader, MqttByteBuffer payloadStream) { + readFrom(payloadStream); + } + + String _clientIdentifier = ''; + + /// Client identifier + String get clientIdentifier => _clientIdentifier; + + set clientIdentifier(String id) { + // if (id.length > MqttClientConstants.maxClientIdentifierLength) { + // throw ClientIdentifierException(id); + // } + // if (id.length > MqttClientConstants.maxClientIdentifierLengthSpec) { + // MqttLogger.log('MqttConnectPayload::Client id exceeds spec value of ' + // '${MqttClientConstants.maxClientIdentifierLengthSpec}'); + // } + _clientIdentifier = id; + } + + /// Variable header + MqttConnectVariableHeader? variableHeader = MqttConnectVariableHeader(); + String? _username; + + /// User name + String? get username => _username; + + set username(String? name) => _username = name != null ? name.trim() : name; + String? _password; + + /// Password + String? get password => _password; + + set password(String? pwd) => _password = pwd != null ? pwd.trim() : pwd; + + /// Will topic + String? willTopic; + + /// Will message + String? willMessage; + + /// Creates a payload from the specified header stream. + @override + void readFrom(MqttByteBuffer payloadStream) { + clientIdentifier = payloadStream.readMqttStringM(); + if (variableHeader!.connectFlags.willFlag) { + willTopic = payloadStream.readMqttStringM(); + willMessage = payloadStream.readMqttStringM(); + } + if (variableHeader!.connectFlags.usernameFlag) { + username = payloadStream.readMqttStringM(); + } + if (variableHeader!.connectFlags.passwordFlag) { + password = payloadStream.readMqttStringM(); + } + } + + /// Writes the connect message payload to the supplied stream. + @override + void writeTo(MqttByteBuffer payloadStream) { + payloadStream.writeMqttStringM(clientIdentifier); + if (variableHeader!.connectFlags.willFlag) { + payloadStream.writeMqttStringM(willTopic!); + payloadStream.writeMqttStringM(willMessage!); + } + if (variableHeader!.connectFlags.usernameFlag) { + payloadStream.writeMqttStringM(username!); + } + if (variableHeader!.connectFlags.passwordFlag) { + payloadStream.writeMqttStringM(password!); + } + } + + @override + int getWriteLength() { + var length = 0; + final enc = MqttEncoding(); + length += enc.getByteCount(clientIdentifier); + if (variableHeader!.connectFlags.willFlag) { + length += enc.getByteCount(willTopic!); + length += enc.getByteCount(willMessage!); + } + if (variableHeader!.connectFlags.usernameFlag) { + length += enc.getByteCount(username!); + } + if (variableHeader!.connectFlags.passwordFlag) { + length += enc.getByteCount(password!); + } + return length; + } + + @override + String toString() => + 'MqttConnectPayload - client identifier is : $clientIdentifier'; +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/messages/connect/mqtt_client_mqtt_connect_return_code.dart b/bytedesk_kefu/lib/mqtt/lib/src/messages/connect/mqtt_client_mqtt_connect_return_code.dart new file mode 100755 index 0000000..8a6bb70 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/messages/connect/mqtt_client_mqtt_connect_return_code.dart @@ -0,0 +1,32 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 31/05/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Enumeration of allowable connection request return codes from a broker. +enum MqttConnectReturnCode { + /// Connection accepted + connectionAccepted, + + /// Invalid protocol version + unacceptedProtocolVersion, + + /// Invalid client identifier + identifierRejected, + + /// Broker unavailable + brokerUnavailable, + + /// Invalid username or password + badUsernameOrPassword, + + /// Not authorised + notAuthorized, + + /// Default + noneSpecified +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/messages/connect/mqtt_client_mqtt_connect_variable_header.dart b/bytedesk_kefu/lib/mqtt/lib/src/messages/connect/mqtt_client_mqtt_connect_variable_header.dart new file mode 100755 index 0000000..238dccc --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/messages/connect/mqtt_client_mqtt_connect_variable_header.dart @@ -0,0 +1,54 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 12/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Implementation of the variable header for an MQTT Connect message. +class MqttConnectVariableHeader extends MqttVariableHeader { + /// Initializes a new instance of the MqttConnectVariableHeader class. + MqttConnectVariableHeader(); + + /// Initializes a new instance of the MqttConnectVariableHeader class. + MqttConnectVariableHeader.fromByteBuffer(MqttByteBuffer headerStream) + : super.fromByteBuffer(headerStream); + + /// Creates a variable header from the specified header stream. + @override + void readFrom(MqttByteBuffer variableHeaderStream) { + readProtocolName(variableHeaderStream); + readProtocolVersion(variableHeaderStream); + readConnectFlags(variableHeaderStream); + readKeepAlive(variableHeaderStream); + } + + /// Writes the variable header to the supplied stream. + @override + void writeTo(MqttByteBuffer variableHeaderStream) { + writeProtocolName(variableHeaderStream); + writeProtocolVersion(variableHeaderStream); + writeConnectFlags(variableHeaderStream); + writeKeepAlive(variableHeaderStream); + } + + /// Gets the length of the write data when WriteTo will be called. + @override + int getWriteLength() { + var headerLength = 0; + final enc = MqttEncoding(); + headerLength += enc.getByteCount(protocolName); + headerLength += 1; // protocolVersion + headerLength += MqttConnectFlags.getWriteLength(); + headerLength += 2; // keepAlive + return headerLength; + } + + @override + String toString() => 'Connect Variable Header: ProtocolName=$protocolName, ' + 'ProtocolVersion=$protocolVersion, ' + 'ConnectFlags=${connectFlags.toString()}, ' + 'KeepAlive=$keepAlive'; +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/messages/connectack/mqtt_client_mqtt_connect_ack_message.dart b/bytedesk_kefu/lib/mqtt/lib/src/messages/connectack/mqtt_client_mqtt_connect_ack_message.dart new file mode 100755 index 0000000..12ea3c1 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/messages/connectack/mqtt_client_mqtt_connect_ack_message.dart @@ -0,0 +1,59 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 15/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Message that indicates a connection acknowledgement. +class MqttConnectAckMessage extends MqttMessage { + /// Initializes a new instance of the MqttConnectAckMessage class. + /// Only called via the MqttMessage.Create operation during processing + /// of an Mqtt message stream. + MqttConnectAckMessage() { + header = MqttHeader().asType(MqttMessageType.connectAck); + variableHeader = MqttConnectAckVariableHeader(); + variableHeader.returnCode = MqttConnectReturnCode.connectionAccepted; + } + + /// Initializes a new instance of the MqttConnectAckMessage class. + MqttConnectAckMessage.fromByteBuffer( + MqttHeader header, MqttByteBuffer messageStream) { + this.header = header; + readFrom(messageStream); + } + + /// Gets or sets the variable header contents. Contains extended + /// metadata about the message + late MqttConnectAckVariableHeader variableHeader; + + /// Reads a message from the supplied stream. + @override + void readFrom(MqttByteBuffer messageStream) { + super.readFrom(messageStream); + variableHeader = MqttConnectAckVariableHeader.fromByteBuffer(messageStream); + } + + /// Writes a message to the supplied stream. + @override + void writeTo(MqttByteBuffer messageStream) { + header!.writeTo(variableHeader.getWriteLength(), messageStream); + variableHeader.writeTo(messageStream); + } + + /// Sets the return code of the Variable Header. + MqttConnectAckMessage withReturnCode(MqttConnectReturnCode returnCode) { + variableHeader.returnCode = returnCode; + return this; + } + + @override + String toString() { + final sb = StringBuffer(); + sb.write(super.toString()); + sb.writeln(variableHeader.toString()); + return sb.toString(); + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/messages/connectack/mqtt_client_mqtt_connect_ack_variable_header.dart b/bytedesk_kefu/lib/mqtt/lib/src/messages/connectack/mqtt_client_mqtt_connect_ack_variable_header.dart new file mode 100755 index 0000000..09d6fee --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/messages/connectack/mqtt_client_mqtt_connect_ack_variable_header.dart @@ -0,0 +1,50 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 15/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Implementation of the variable header for an MQTT ConnectAck message. +class MqttConnectAckVariableHeader extends MqttVariableHeader { + /// Initializes a new instance of the MqttConnectVariableHeader class. + MqttConnectAckVariableHeader(); + + /// Initializes a new instance of the MqttConnectVariableHeader class. + MqttConnectAckVariableHeader.fromByteBuffer(MqttByteBuffer headerStream) + : super.fromByteBuffer(headerStream); + + /// Writes the variable header for an MQTT Connect message to + /// the supplied stream. + @override + void writeTo(MqttByteBuffer variableHeaderStream) { + // Unused additional 'compression' byte used within the variable + // header acknowledgement. + variableHeaderStream.writeByte(0); + writeReturnCode(variableHeaderStream); + } + + /// Creates a variable header from the specified header stream. + @override + void readFrom(MqttByteBuffer variableHeaderStream) { + // Unused additional 'compression' byte used within the variable + // header acknowledgement. + variableHeaderStream.readByte(); + readReturnCode(variableHeaderStream); + } + + /// Gets the length of the write data when WriteTo will be called. + /// This method is overriden by the ConnectAckVariableHeader because the + /// variable header of this message type, for some reason, contains an extra + /// byte that is not present in the variable header spec, meaning we have to + /// do some custom serialization and deserialization. + @override + int getWriteLength() => 2; + + @override + String toString() => + 'Connect Variable Header: TopicNameCompressionResponse={0}, ' + 'ReturnCode={$returnCode}'; +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/messages/disconnect/mqtt_client_mqtt_disconnect_message.dart b/bytedesk_kefu/lib/mqtt/lib/src/messages/disconnect/mqtt_client_mqtt_disconnect_message.dart new file mode 100755 index 0000000..dc16e20 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/messages/disconnect/mqtt_client_mqtt_disconnect_message.dart @@ -0,0 +1,28 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 19/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Implementation of an MQTT Disconnect Message. +class MqttDisconnectMessage extends MqttMessage { + /// Initializes a new instance of the MqttDisconnectMessage class. + MqttDisconnectMessage() { + header = MqttHeader().asType(MqttMessageType.disconnect); + } + + /// Initializes a new instance of the MqttDisconnectMessage class. + MqttDisconnectMessage.fromHeader(MqttHeader header) { + this.header = header; + } + + @override + String toString() { + final sb = StringBuffer(); + sb.write(super.toString()); + return sb.toString(); + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/messages/mqtt_client_mqtt_header.dart b/bytedesk_kefu/lib/mqtt/lib/src/messages/mqtt_client_mqtt_header.dart new file mode 100755 index 0000000..ac8d12c --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/messages/mqtt_client_mqtt_header.dart @@ -0,0 +1,191 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 31/05/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Represents the Fixed Header of an MQTT message. +class MqttHeader { + /// Initializes a new instance of the MqttHeader class. + MqttHeader(); + + /// Initializes a new instance of MqttHeader' based on data + /// contained within the supplied stream. + MqttHeader.fromByteBuffer(MqttByteBuffer headerStream) { + readFrom(headerStream); + } + + /// Backing storage for the payload size. + int _messageSize = 0; + + /// Gets or sets the type of the MQTT message. + MqttMessageType? messageType; + + /// Gets or sets a value indicating whether this MQTT Message is + /// duplicate of a previous message. + /// True if duplicate; otherwise, false. + bool duplicate = false; + + /// Gets or sets the Quality of Service indicator for the message. + MqttQos qos = MqttQos.atMostOnce; + + /// Gets or sets a value indicating whether this MQTT message should be + /// retained by the message broker for transmission to new subscribers. + /// True if message should be retained by the message broker; + /// otherwise, false. + bool retain = false; + + /// Gets or sets the size of the variable header + payload + /// section of the message. + /// The size of the variable header + payload. + int get messageSize => _messageSize; + + set messageSize(int value) { + if (value < 0 || value > MqttClientConstants.maxMessageSize) { + throw InvalidPayloadSizeException( + value, MqttClientConstants.maxMessageSize); + } + _messageSize = value; + } + + /// Writes the header to a supplied stream. + void writeTo(int messageSize, MqttByteBuffer messageStream) { + _messageSize = messageSize; + final headerBuff = headerBytes(); + messageStream.write(headerBuff); + } + + /// Creates a new MqttHeader based on a list of bytes. + void readFrom(MqttByteBuffer headerStream) { + if (headerStream.length < 2) { + headerStream.reset(); + throw InvalidHeaderException( + 'The supplied header is invalid. Header must be at ' + 'least 2 bytes long.'); + } + final firstHeaderByte = headerStream.readByte(); + // Pull out the first byte + retain = (firstHeaderByte & 1) == 1; + qos = MqttUtilities.getQosLevel((firstHeaderByte & 6) >> 1); + duplicate = ((firstHeaderByte & 8) >> 3) == 1; + messageType = MqttMessageType.values[((firstHeaderByte & 240) >> 4)]; + + // Decode the remaining bytes as the remaining/payload size, input param is the 2nd to last byte of the header byte list + try { + _messageSize = readRemainingLength(headerStream); + } on Exception { + throw InvalidHeaderException( + 'The header being processed contained an invalid size byte pattern. ' + 'Message size must take a most 4 bytes, and the last byte ' + 'must have bit 8 set to 0.'); + } on Error { + throw InvalidHeaderException( + 'The header being processed contained an invalid size byte pattern. ' + 'Message size must take a most 4 bytes, and the last byte ' + 'must have bit 8 set to 0.'); + } + } + + /// Gets the value of the Mqtt header as a byte array + typed.Uint8Buffer headerBytes() { + final headerBytes = typed.Uint8Buffer(); + + // Build the bytes that make up the header. The first byte is a + // combination of message type, dup, qos and retain, and the + // following bytes (up to 4 of them) are the size of the + // payload + variable header. + final messageTypeLength = messageType!.index << 4; + final duplicateLength = (duplicate ? 1 : 0) << 3; + final qosLength = qos.index << 1; + final retainLength = retain ? 1 : 0; + final firstByte = + messageTypeLength + duplicateLength + qosLength + retainLength; + headerBytes.add(firstByte); + headerBytes.addAll(getRemainingLengthBytes()); + return headerBytes; + } + + /// Get the remaining byte length in the buffer + static int readRemainingLength(MqttByteBuffer headerStream) { + final lengthBytes = readLengthBytes(headerStream); + return calculateLength(lengthBytes); + } + + /// Reads the length bytes of an MqttHeader from the supplied stream. + static typed.Uint8Buffer readLengthBytes(MqttByteBuffer headerStream) { + final lengthBytes = typed.Uint8Buffer(); + // Read until we've got the entire size, or the 4 byte limit is reached + int sizeByte; + var byteCount = 0; + do { + sizeByte = headerStream.readByte(); + lengthBytes.add(sizeByte); + } while (++byteCount <= 4 && (sizeByte & 0x80) == 0x80); + return lengthBytes; + } + + /// Calculates and return the bytes that represent the + /// remaining length of the message. + typed.Uint8Buffer getRemainingLengthBytes() { + final lengthBytes = typed.Uint8Buffer(); + var payloadCalc = _messageSize; + + // Generate a byte array based on the message size, splitting it up into + // 7 bit chunks, with the 8th bit being used to indicate 'one more to come' + do { + var nextByteValue = payloadCalc % 128; + payloadCalc = payloadCalc ~/ 128; + if (payloadCalc > 0) { + nextByteValue = nextByteValue | 0x80; + } + lengthBytes.add(nextByteValue); + } while (payloadCalc > 0); + + return lengthBytes; + } + + /// Calculates the remaining length of an MqttMessage + /// from the bytes that make up the length. + static int calculateLength(typed.Uint8Buffer lengthBytes) { + var remainingLength = 0; + var multiplier = 1; + + for (final currentByte in lengthBytes) { + remainingLength += (currentByte & 0x7f) * multiplier; + multiplier *= 0x80; + } + return remainingLength; + } + + /// Sets the IsDuplicate flag of the header. + MqttHeader isDuplicate() { + duplicate = true; + return this; + } + + /// Sets the Qos of the message header. + MqttHeader withQos(MqttQos qos) { + this.qos = qos; + return this; + } + + /// Sets the type of the message identified in the header. + MqttHeader asType(MqttMessageType messageType) { + this.messageType = messageType; + return this; + } + + /// Defines that the message should be retained. + MqttHeader shouldBeRetained() { + retain = true; + return this; + } + + @override + String toString() => + 'Header: MessageType = $messageType, Duplicate = $duplicate, ' + 'Retain = $retain, Qos = $qos, Size = $_messageSize'; +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/messages/mqtt_client_mqtt_message.dart b/bytedesk_kefu/lib/mqtt/lib/src/messages/mqtt_client_mqtt_message.dart new file mode 100755 index 0000000..d7833ca --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/messages/mqtt_client_mqtt_message.dart @@ -0,0 +1,82 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 31/05/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Represents an MQTT message that contains a fixed header, variable +/// header and message body. +/// +/// Messages roughly look as follows. +/// ---------------------------- +/// | Header, 2-5 Bytes Length | +/// ---------------------------- +/// | Variable Header(VH) | +/// | n Bytes Length | +/// ---------------------------- +/// | Message Payload | +/// | 256MB minus VH Size | +/// ---------------------------- + +class MqttMessage { + /// Initializes a new instance of the MqttMessage class. + MqttMessage(); + + /// Initializes a new instance of the MqttMessage class. + MqttMessage.fromHeader(MqttHeader header) { + header = header; + } + + /// The header of the MQTT Message. Contains metadata about the message + MqttHeader? header; + + /// Creates a new instance of an MQTT Message based on a raw message stream. + static MqttMessage createFrom(MqttByteBuffer messageStream) { + try { + var header = MqttHeader(); + // Pass the input stream sequentially through the component + // deserialization(create) methods to build a full MqttMessage. + header = MqttHeader.fromByteBuffer(messageStream); + //expected position after reading payload + final expectedPos = messageStream.position + header.messageSize; + + if (messageStream.availableBytes < header.messageSize) { + messageStream.reset(); + throw InvalidMessageException( + 'Available bytes is less than the message size'); + } + final message = MqttMessageFactory.getMessage(header, messageStream); + + if (messageStream.position < expectedPos) { + messageStream.skipBytes = expectedPos - messageStream.position; + } + + return message; + } on Exception catch (e) { + throw InvalidMessageException( + 'The data provided in the message stream was not a ' + 'valid MQTT Message, ' + 'exception is $e'); + } + } + + /// Writes the message to the supplied stream. + void writeTo(MqttByteBuffer messageStream) { + header!.writeTo(0, messageStream); + } + + /// Reads a message from the supplied stream. + void readFrom(MqttByteBuffer messageStream) {} + + @override + String toString() { + final sb = StringBuffer(); + sb.write('MQTTMessage of type '); + sb.writeln(header!.messageType.toString()); + sb.writeln(header.toString()); + return sb.toString(); + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/messages/mqtt_client_mqtt_message_factory.dart b/bytedesk_kefu/lib/mqtt/lib/src/messages/mqtt_client_mqtt_message_factory.dart new file mode 100755 index 0000000..0c86a5b --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/messages/mqtt_client_mqtt_message_factory.dart @@ -0,0 +1,70 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 15/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Factory for generating instances of MQTT Messages +class MqttMessageFactory { + /// Gets an instance of an MqttMessage based on the message type requested. + static MqttMessage getMessage( + MqttHeader header, MqttByteBuffer messageStream) { + MqttMessage message; + switch (header.messageType) { + case MqttMessageType.connect: + message = MqttConnectMessage.fromByteBuffer(header, messageStream); + break; + case MqttMessageType.connectAck: + message = MqttConnectAckMessage.fromByteBuffer(header, messageStream); + break; + case MqttMessageType.publish: + message = MqttPublishMessage.fromByteBuffer(header, messageStream); + break; + case MqttMessageType.publishAck: + message = MqttPublishAckMessage.fromByteBuffer(header, messageStream); + break; + case MqttMessageType.publishComplete: + message = + MqttPublishCompleteMessage.fromByteBuffer(header, messageStream); + break; + case MqttMessageType.publishReceived: + message = + MqttPublishReceivedMessage.fromByteBuffer(header, messageStream); + break; + case MqttMessageType.publishRelease: + message = + MqttPublishReleaseMessage.fromByteBuffer(header, messageStream); + break; + case MqttMessageType.subscribe: + message = MqttSubscribeMessage.fromByteBuffer(header, messageStream); + break; + case MqttMessageType.subscribeAck: + message = MqttSubscribeAckMessage.fromByteBuffer(header, messageStream); + break; + case MqttMessageType.unsubscribe: + message = MqttUnsubscribeMessage.fromByteBuffer(header, messageStream); + break; + case MqttMessageType.unsubscribeAck: + message = + MqttUnsubscribeAckMessage.fromByteBuffer(header, messageStream); + break; + case MqttMessageType.pingRequest: + message = MqttPingRequestMessage.fromHeader(header); + break; + case MqttMessageType.pingResponse: + message = MqttPingResponseMessage.fromHeader(header); + break; + case MqttMessageType.disconnect: + message = MqttDisconnectMessage.fromHeader(header); + break; + default: + throw InvalidHeaderException( + 'The Message Type specified ($header.messageType) is not a valid ' + 'MQTT Message type or currently not supported.'); + } + return message; + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/messages/mqtt_client_mqtt_message_type.dart b/bytedesk_kefu/lib/mqtt/lib/src/messages/mqtt_client_mqtt_message_type.dart new file mode 100755 index 0000000..697ad5c --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/messages/mqtt_client_mqtt_message_type.dart @@ -0,0 +1,59 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 31/05/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// An enumeration of all available MQTT Message Types +enum MqttMessageType { + /// Reserved by the MQTT spec, should not be used. + reserved1, + + /// Connect + connect, + + /// Connect acknowledge + connectAck, + + /// Publish + publish, + + /// Publish acknowledge + publishAck, + + /// Publish recieved + publishReceived, + + /// Publish release + publishRelease, + + /// Publish complete + publishComplete, + + /// Subscribe + subscribe, + + /// Subscribe acknowledge + subscribeAck, + + /// Unsubscribe + unsubscribe, + + /// Unsubscribe acknowledge + unsubscribeAck, + + /// Ping request + pingRequest, + + /// Ping response + pingResponse, + + /// Disconnect + disconnect, + + /// Reserved by the MQTT spec, should not be used. + reserved2 +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/messages/mqtt_client_mqtt_payload.dart b/bytedesk_kefu/lib/mqtt/lib/src/messages/mqtt_client_mqtt_payload.dart new file mode 100755 index 0000000..580ff40 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/messages/mqtt_client_mqtt_payload.dart @@ -0,0 +1,29 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 31/05/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Represents the payload (Body) of an MQTT Message. +abstract class MqttPayload { + /// Initializes a new instance of the MqttPayload class. + MqttPayload(); + + /// Initializes a new instance of the MqttPayload class. + MqttPayload.fromMqttByteBuffer(MqttByteBuffer payloadStream) { + readFrom(payloadStream); + } + + /// Writes the payload to the supplied stream. + /// A basic message has no Variable Header. + void writeTo(MqttByteBuffer payloadStream); + + /// Creates a payload from the specified header stream. + void readFrom(MqttByteBuffer payloadStream); + + /// Gets the length of the payload in bytes when written to a stream. + int getWriteLength(); +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/messages/mqtt_client_mqtt_variable_header.dart b/bytedesk_kefu/lib/mqtt/lib/src/messages/mqtt_client_mqtt_variable_header.dart new file mode 100755 index 0000000..128139b --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/messages/mqtt_client_mqtt_variable_header.dart @@ -0,0 +1,209 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 31/05/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Enumeration used by subclasses to tell the variable header what +/// should be read from the underlying stream. +enum MqttReadWriteFlags { + /// Nothing + none, + + /// Protocol name + protocolName, + + /// Protocol version + protocolVersion, + + /// Connect flags + connectFlags, + + /// Keep alive + keepAlive, + + /// Return code + returnCode, + + /// Topic name + topicName, + + /// Message identifier + messageIdentifier +} + +/// Represents the base class for the Variable Header portion +/// of some MQTT Messages. +class MqttVariableHeader { + /// Initializes a new instance of the MqttVariableHeader class. + MqttVariableHeader() { + protocolName = Protocol.name; + protocolVersion = Protocol.version; + connectFlags = MqttConnectFlags(); + } + + /// Initializes a new instance of the MqttVariableHeader class, + /// populating it with data from a stream. + MqttVariableHeader.fromByteBuffer(MqttByteBuffer headerStream) { + readFrom(headerStream); + } + + /// The length, in bytes, consumed by the variable header. + int length = 0; + + /// Protocol name + String protocolName = ''; + + /// Protocol version + int protocolVersion = 0; + + /// Conenct flags + late MqttConnectFlags connectFlags; + + /// Defines the maximum allowable lag, in seconds, between expected messages. + /// The spec indicates that clients won't be disconnected until KeepAlive + 1/2 KeepAlive time period + /// elapses. + int keepAlive = 0; + + /// Return code + MqttConnectReturnCode returnCode = MqttConnectReturnCode.brokerUnavailable; + + /// Topic name + String topicName = ''; + + /// Message identifier + int? messageIdentifier = 0; + + /// Encoder + final MqttEncoding _enc = MqttEncoding(); + + /// Creates a variable header from the specified header stream. + /// A subclass can override this method to do completely + /// custom read operations if required. + void readFrom(MqttByteBuffer variableHeaderStream) { + readProtocolName(variableHeaderStream); + readProtocolVersion(variableHeaderStream); + readConnectFlags(variableHeaderStream); + readKeepAlive(variableHeaderStream); + readReturnCode(variableHeaderStream); + readTopicName(variableHeaderStream); + readMessageIdentifier(variableHeaderStream); + } + + /// Writes the variable header to the supplied stream. + /// A subclass can override this method to do completely + /// custom write operations if required. + void writeTo(MqttByteBuffer variableHeaderStream) { + writeProtocolName(variableHeaderStream); + writeProtocolVersion(variableHeaderStream); + writeConnectFlags(variableHeaderStream); + writeKeepAlive(variableHeaderStream); + writeReturnCode(variableHeaderStream); + writeTopicName(variableHeaderStream); + writeMessageIdentifier(variableHeaderStream); + } + + /// Gets the length of the write data when WriteTo will be called. + /// A subclass that overrides writeTo must also overwrite this method. + int getWriteLength() { + var headerLength = 0; + final enc = MqttEncoding(); + headerLength += enc.getByteCount(protocolName); + headerLength += 1; // protocolVersion + headerLength += MqttConnectFlags.getWriteLength(); + headerLength += 2; // keepAlive + headerLength += 1; // returnCode + headerLength += enc.getByteCount(topicName.toString()); + headerLength += 2; // MessageIdentifier + return headerLength; + } + + /// Write functions + + /// Protocol name + void writeProtocolName(MqttByteBuffer stream) { + MqttByteBuffer.writeMqttString(stream, protocolName); + } + + /// Protocol version + void writeProtocolVersion(MqttByteBuffer stream) { + stream.writeByte(protocolVersion); + } + + /// Keep alive + void writeKeepAlive(MqttByteBuffer stream) { + stream.writeShort(keepAlive); + } + + /// Return code + void writeReturnCode(MqttByteBuffer stream) { + stream.writeByte(returnCode.index); + } + + /// Topic name + void writeTopicName(MqttByteBuffer stream) { + MqttByteBuffer.writeMqttString(stream, topicName.toString()); + } + + /// Message identifier + void writeMessageIdentifier(MqttByteBuffer stream) { + stream.writeShort(messageIdentifier!); + } + + /// Connect flags + void writeConnectFlags(MqttByteBuffer stream) { + connectFlags.writeTo(stream); + } + + /// Read functions + + /// Protocol name + void readProtocolName(MqttByteBuffer stream) { + protocolName = MqttByteBuffer.readMqttString(stream); + length += protocolName.length + 2; // 2 for length short at front of string + } + + /// Protocol version + void readProtocolVersion(MqttByteBuffer stream) { + protocolVersion = stream.readByte(); + length++; + } + + /// Keep alive + void readKeepAlive(MqttByteBuffer stream) { + keepAlive = stream.readShort(); + length += 2; + } + + /// Return code + void readReturnCode(MqttByteBuffer stream) { + returnCode = MqttConnectReturnCode.values[stream.readByte()]; + length++; + } + + /// Topic name + void readTopicName(MqttByteBuffer stream) { + topicName = MqttByteBuffer.readMqttString(stream); + // If the protocol si V311 allow extended UTF8 characters + if (Protocol.version == MqttClientConstants.mqttV311ProtocolVersion) { + length += _enc.getByteCount(topicName); + } else { + length = topicName.length + 2; // 2 for length short at front of string. + } + } + + /// Message identifier + void readMessageIdentifier(MqttByteBuffer stream) { + messageIdentifier = stream.readShort(); + length += 2; + } + + /// Connect flags + void readConnectFlags(MqttByteBuffer stream) { + connectFlags = MqttConnectFlags.fromByteBuffer(stream); + length += 1; + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/messages/pingrequest/mqtt_client_mqtt_ping_request_message.dart b/bytedesk_kefu/lib/mqtt/lib/src/messages/pingrequest/mqtt_client_mqtt_ping_request_message.dart new file mode 100755 index 0000000..3693627 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/messages/pingrequest/mqtt_client_mqtt_ping_request_message.dart @@ -0,0 +1,28 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 19/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Implementation of an MQTT ping Request Message. +class MqttPingRequestMessage extends MqttMessage { + /// Initializes a new instance of the MqttPingRequestMessage class. + MqttPingRequestMessage() { + header = MqttHeader().asType(MqttMessageType.pingRequest); + } + + /// Initializes a new instance of the MqttPingRequestMessage class. + MqttPingRequestMessage.fromHeader(MqttHeader header) { + this.header = header; + } + + @override + String toString() { + final sb = StringBuffer(); + sb.write(super.toString()); + return sb.toString(); + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/messages/pingresponse/mqtt_client_mqtt_ping_response_message.dart b/bytedesk_kefu/lib/mqtt/lib/src/messages/pingresponse/mqtt_client_mqtt_ping_response_message.dart new file mode 100755 index 0000000..8e55630 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/messages/pingresponse/mqtt_client_mqtt_ping_response_message.dart @@ -0,0 +1,28 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 19/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Implementation of an MQTT ping Request Message. +class MqttPingResponseMessage extends MqttMessage { + /// Initializes a new instance of the MqttPingResponseMessage class. + MqttPingResponseMessage() { + header = MqttHeader().asType(MqttMessageType.pingResponse); + } + + /// Initializes a new instance of the MqttPingResponseMessage class. + MqttPingResponseMessage.fromHeader(MqttHeader header) { + this.header = header; + } + + @override + String toString() { + final sb = StringBuffer(); + sb.write(super.toString()); + return sb.toString(); + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/messages/publish/mqtt_client_mqtt_publish_message.dart b/bytedesk_kefu/lib/mqtt/lib/src/messages/publish/mqtt_client_mqtt_publish_message.dart new file mode 100755 index 0000000..fa60ecf --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/messages/publish/mqtt_client_mqtt_publish_message.dart @@ -0,0 +1,98 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 19/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Implementation of an MQTT Publish Message, used for publishing +/// telemetry data along a live MQTT stream. +class MqttPublishMessage extends MqttMessage { + /// Initializes a new instance of the MqttPublishMessage class. + MqttPublishMessage() { + header = MqttHeader().asType(MqttMessageType.publish); + variableHeader = MqttPublishVariableHeader(header); + payload = MqttPublishPayload(); + } + + /// Initializes a new instance of the MqttPublishMessage class. + MqttPublishMessage.fromByteBuffer( + MqttHeader header, MqttByteBuffer messageStream) { + this.header = header; + readFrom(messageStream); + } + + /// The variable header contents. Contains extended metadata about the message + MqttPublishVariableHeader? variableHeader; + + /// Gets or sets the payload of the Mqtt Message. + late MqttPublishPayload payload; + + /// Reads a message from the supplied stream. + @override + void readFrom(MqttByteBuffer messageStream) { + super.readFrom(messageStream); + variableHeader = + MqttPublishVariableHeader.fromByteBuffer(header, messageStream); + payload = MqttPublishPayload.fromByteBuffer( + header, variableHeader, messageStream); + } + + /// Writes the message to the supplied stream. + @override + void writeTo(MqttByteBuffer messageStream) { + final variableHeaderLength = variableHeader!.getWriteLength(); + final payloadLength = payload.getWriteLength(); + header!.writeTo(variableHeaderLength + payloadLength, messageStream); + variableHeader!.writeTo(messageStream); + payload.writeTo(messageStream); + } + + /// Sets the topic to publish data to. + MqttPublishMessage toTopic(String topicName) { + variableHeader!.topicName = topicName; + return this; + } + + /// Appends data to publish to the end of the current message payload. + MqttPublishMessage publishData(typed.Uint8Buffer data) { + payload.message!.addAll(data); + return this; + } + + /// Sets the message identifier of the message. + MqttPublishMessage withMessageIdentifier(int messageIdentifier) { + variableHeader!.messageIdentifier = messageIdentifier; + return this; + } + + /// Sets the Qos of the published message. + MqttPublishMessage withQos(MqttQos qos) { + header!.withQos(qos); + return this; + } + + /// Removes the current published data. + MqttPublishMessage clearPublishData() { + payload.message!.clear(); + return this; + } + + /// Set the retain flag on the message + void setRetain({bool? state}) { + if ((state != null) && state) { + header!.shouldBeRetained(); + } + } + + @override + String toString() { + final sb = StringBuffer(); + sb.write(super.toString()); + sb.writeln(variableHeader.toString()); + sb.writeln(payload.toString()); + return sb.toString(); + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/messages/publish/mqtt_client_mqtt_publish_payload.dart b/bytedesk_kefu/lib/mqtt/lib/src/messages/publish/mqtt_client_mqtt_publish_payload.dart new file mode 100755 index 0000000..db22335 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/messages/publish/mqtt_client_mqtt_publish_payload.dart @@ -0,0 +1,74 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 19/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Class that contains details related to an MQTT Connect messages payload +class MqttPublishPayload extends MqttPayload { + /// Initializes a new instance of the MqttPublishPayload class. + MqttPublishPayload() { + message = typed.Uint8Buffer(); + } + + /// Initializes a new instance of the MqttPublishPayload class. + MqttPublishPayload.fromByteBuffer( + this.header, this.variableHeader, MqttByteBuffer payloadStream) { + readFrom(payloadStream); + } + + /// Message header + MqttHeader? header; + + /// Variable header + MqttPublishVariableHeader? variableHeader; + + /// The message that forms the payload of the publish message. + typed.Uint8Buffer? message; + + /// Creates a payload from the specified header stream. + @override + void readFrom(MqttByteBuffer payloadStream) { + // The payload of the publish message is not a string, just + // a binary chunk of bytes. + // The length of the bytes is the length specified in the header, + // minus any bytes spent in the variable header. + final messageBytes = header!.messageSize - variableHeader!.length; + message = payloadStream.read(messageBytes); + } + + /// Writes the payload to the supplied stream. + @override + void writeTo(MqttByteBuffer payloadStream) { + payloadStream.write(message); + } + + /// Gets the length of the payload in bytes when written to a stream. + @override + int getWriteLength() => message!.length; + + @override + String toString() => + 'Payload: {${message!.length} bytes={${bytesToString(message!)}'; + + /// Converts an array of bytes to a byte string. + static String bytesToString(typed.Uint8Buffer message) { + final sb = StringBuffer(); + for (final b in message) { + sb.write('<'); + sb.write(b); + sb.write('>'); + } + return sb.toString(); + } + + /// Converts an array of bytes to a character string. + static String bytesToStringAsString(typed.Uint8Buffer message) { + final sb = StringBuffer(); + message.forEach(sb.writeCharCode); + return sb.toString(); + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/messages/publish/mqtt_client_mqtt_publish_variable_header.dart b/bytedesk_kefu/lib/mqtt/lib/src/messages/publish/mqtt_client_mqtt_publish_variable_header.dart new file mode 100755 index 0000000..40b32fd --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/messages/publish/mqtt_client_mqtt_publish_variable_header.dart @@ -0,0 +1,60 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 19/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Implementation of the variable header for an MQTT Connect message. +class MqttPublishVariableHeader extends MqttVariableHeader { + /// Initializes a new instance of the MqttPublishVariableHeader class. + MqttPublishVariableHeader(this.header); + + /// Initializes a new instance of the MqttPublishVariableHeader class. + MqttPublishVariableHeader.fromByteBuffer( + this.header, MqttByteBuffer variableHeaderStream) { + readFrom(variableHeaderStream); + } + + /// Standard header + MqttHeader? header; + + /// Creates a variable header from the specified header stream. + @override + void readFrom(MqttByteBuffer variableHeaderStream) { + readTopicName(variableHeaderStream); + if (header!.qos == MqttQos.atLeastOnce || + header!.qos == MqttQos.exactlyOnce) { + readMessageIdentifier(variableHeaderStream); + } + } + + /// Writes the variable header to the supplied stream. + @override + void writeTo(MqttByteBuffer variableHeaderStream) { + writeTopicName(variableHeaderStream); + if (header!.qos == MqttQos.atLeastOnce || + header!.qos == MqttQos.exactlyOnce) { + writeMessageIdentifier(variableHeaderStream); + } + } + + /// Gets the length of the write data when WriteTo will be called. + @override + int getWriteLength() { + var headerLength = 0; + final enc = MqttEncoding(); + headerLength += enc.getByteCount(topicName); + if (header!.qos == MqttQos.atLeastOnce || + header!.qos == MqttQos.exactlyOnce) { + headerLength += 2; + } + return headerLength; + } + + @override + String toString() => 'Publish Variable Header: TopicName={$topicName}, ' + 'MessageIdentifier={$messageIdentifier}, VH Length={$length}'; +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/messages/publishack/mqtt_client_mqtt_publish_ack_message.dart b/bytedesk_kefu/lib/mqtt/lib/src/messages/publishack/mqtt_client_mqtt_publish_ack_message.dart new file mode 100755 index 0000000..85dea05 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/messages/publishack/mqtt_client_mqtt_publish_ack_message.dart @@ -0,0 +1,50 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 19/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Implementation of an MQTT Publish Acknowledgement Message, used to ACK a +/// publish message that has it's QOS set to AtLeast or Exactly Once. +class MqttPublishAckMessage extends MqttMessage { + /// Initializes a new instance of the MqttPublishAckMessage class. + MqttPublishAckMessage() { + header = MqttHeader().asType(MqttMessageType.publishAck); + variableHeader = MqttPublishAckVariableHeader(); + } + + /// Initializes a new instance of the MqttPublishAckMessage class. + MqttPublishAckMessage.fromByteBuffer( + MqttHeader header, MqttByteBuffer messageStream) { + this.header = header; + variableHeader = MqttPublishAckVariableHeader.fromByteBuffer(messageStream); + } + + /// Gets or sets the variable header contents. Contains extended + /// metadata about the message + late MqttPublishAckVariableHeader variableHeader; + + /// Writes the message to the supplied stream. + @override + void writeTo(MqttByteBuffer messageStream) { + header!.writeTo(variableHeader.getWriteLength(), messageStream); + variableHeader.writeTo(messageStream); + } + + /// Sets the message identifier of the MqttMessage. + MqttPublishAckMessage withMessageIdentifier(int? messageIdentifier) { + variableHeader.messageIdentifier = messageIdentifier; + return this; + } + + @override + String toString() { + final sb = StringBuffer(); + sb.write(super.toString()); + sb.writeln(variableHeader.toString()); + return sb.toString(); + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/messages/publishack/mqtt_client_mqtt_publish_ack_variable_header.dart b/bytedesk_kefu/lib/mqtt/lib/src/messages/publishack/mqtt_client_mqtt_publish_ack_variable_header.dart new file mode 100755 index 0000000..1efddab --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/messages/publishack/mqtt_client_mqtt_publish_ack_variable_header.dart @@ -0,0 +1,40 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 19/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Implementation of the variable header for an MQTT Publish +/// Acknowledgement message. +class MqttPublishAckVariableHeader extends MqttVariableHeader { + /// Initializes a new instance of the MqttPublishAckVariableHeader class. + MqttPublishAckVariableHeader(); + + /// Initializes a new instance of the class. + MqttPublishAckVariableHeader.fromByteBuffer(MqttByteBuffer headerStream) { + readFrom(headerStream); + } + + /// Creates a variable header from the specified header stream. + @override + void readFrom(MqttByteBuffer variableHeaderStream) { + readMessageIdentifier(variableHeaderStream); + } + + /// Writes the variable header to the supplied stream. + @override + void writeTo(MqttByteBuffer variableHeaderStream) { + writeMessageIdentifier(variableHeaderStream); + } + + /// Gets the length of the write data when WriteTo will be called. + @override + int getWriteLength() => 2; + + @override + String toString() => + 'PublishAck Variable Header: MessageIdentifier={$messageIdentifier}'; +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/messages/publishcomplete/mqtt_client_mqtt_publish_complete_message.dart b/bytedesk_kefu/lib/mqtt/lib/src/messages/publishcomplete/mqtt_client_mqtt_publish_complete_message.dart new file mode 100755 index 0000000..bff2e4b --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/messages/publishcomplete/mqtt_client_mqtt_publish_complete_message.dart @@ -0,0 +1,50 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 19/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Implementation of an MQTT Publish Complete Message. +class MqttPublishCompleteMessage extends MqttMessage { + /// Initializes a new instance of the MqttPublishCompleteMessage class. + MqttPublishCompleteMessage() { + header = MqttHeader().asType(MqttMessageType.publishComplete); + variableHeader = MqttPublishCompleteVariableHeader(); + } + + /// Initializes a new instance of the MqttPublishCompleteMessage class. + MqttPublishCompleteMessage.fromByteBuffer( + MqttHeader header, MqttByteBuffer messageStream) { + this.header = header; + variableHeader = + MqttPublishCompleteVariableHeader.fromByteBuffer(messageStream); + } + + /// Gets or sets the variable header contents. Contains extended + /// metadata about the message + late MqttPublishCompleteVariableHeader variableHeader; + + /// Writes the message to the supplied stream. + @override + void writeTo(MqttByteBuffer messageStream) { + header!.writeTo(variableHeader.getWriteLength(), messageStream); + variableHeader.writeTo(messageStream); + } + + /// Sets the message identifier of the MqttMessage. + MqttPublishCompleteMessage withMessageIdentifier(int? messageIdentifier) { + variableHeader.messageIdentifier = messageIdentifier; + return this; + } + + @override + String toString() { + final sb = StringBuffer(); + sb.write(super.toString()); + sb.writeln(variableHeader.toString()); + return sb.toString(); + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/messages/publishcomplete/mqtt_client_mqtt_publish_complete_variable_header.dart b/bytedesk_kefu/lib/mqtt/lib/src/messages/publishcomplete/mqtt_client_mqtt_publish_complete_variable_header.dart new file mode 100755 index 0000000..3875438 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/messages/publishcomplete/mqtt_client_mqtt_publish_complete_variable_header.dart @@ -0,0 +1,40 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 19/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Implementation of the variable header for an MQTT Publish Complete message. +class MqttPublishCompleteVariableHeader extends MqttVariableHeader { + /// Initializes a new instance of the MqttPublishCompleteVariableHeader class. + MqttPublishCompleteVariableHeader(); + + /// Initializes a new instance of the MqttPublishCompleteVariableHeader class. + MqttPublishCompleteVariableHeader.fromByteBuffer( + MqttByteBuffer headerStream) { + readFrom(headerStream); + } + + /// Creates a variable header from the specified header stream. + @override + void readFrom(MqttByteBuffer variableHeaderStream) { + readMessageIdentifier(variableHeaderStream); + } + + /// Writes the variable header to the supplied stream. + @override + void writeTo(MqttByteBuffer variableHeaderStream) { + writeMessageIdentifier(variableHeaderStream); + } + + /// Gets the length of the write data when WriteTo will be called. + @override + int getWriteLength() => 2; + + @override + String toString() => + 'PublishComplete Variable Header: MessageIdentifier={$messageIdentifier}'; +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/messages/publishreceived/mqtt_client_mqtt_publish_received_message.dart b/bytedesk_kefu/lib/mqtt/lib/src/messages/publishreceived/mqtt_client_mqtt_publish_received_message.dart new file mode 100755 index 0000000..2912447 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/messages/publishreceived/mqtt_client_mqtt_publish_received_message.dart @@ -0,0 +1,50 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 19/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Implementation of an MQTT Publish Received Message. +class MqttPublishReceivedMessage extends MqttMessage { + /// Initializes a new instance of the MqttPublishReceivedMessage class. + MqttPublishReceivedMessage() { + header = MqttHeader().asType(MqttMessageType.publishReceived); + variableHeader = MqttPublishReceivedVariableHeader(); + } + + /// Initializes a new instance of the MqttPublishReceivedMessage class. + MqttPublishReceivedMessage.fromByteBuffer( + MqttHeader header, MqttByteBuffer messageStream) { + this.header = header; + variableHeader = + MqttPublishReceivedVariableHeader.fromByteBuffer(messageStream); + } + + /// Gets or sets the variable header contents. Contains extended + /// metadata about the message. + late MqttPublishReceivedVariableHeader variableHeader; + + /// Writes the message to the supplied stream. + @override + void writeTo(MqttByteBuffer messageStream) { + header!.writeTo(variableHeader.getWriteLength(), messageStream); + variableHeader.writeTo(messageStream); + } + + /// Sets the message identifier of the MqttMessage. + MqttPublishReceivedMessage withMessageIdentifier(int? messageIdentifier) { + variableHeader.messageIdentifier = messageIdentifier; + return this; + } + + @override + String toString() { + final sb = StringBuffer(); + sb.write(super.toString()); + sb.writeln(variableHeader.toString()); + return sb.toString(); + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/messages/publishreceived/mqtt_client_mqtt_publish_received_variable_header.dart b/bytedesk_kefu/lib/mqtt/lib/src/messages/publishreceived/mqtt_client_mqtt_publish_received_variable_header.dart new file mode 100755 index 0000000..5ec2a71 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/messages/publishreceived/mqtt_client_mqtt_publish_received_variable_header.dart @@ -0,0 +1,40 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 19/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Implementation of the variable header for an MQTT Publish Received message. +class MqttPublishReceivedVariableHeader extends MqttVariableHeader { + /// Initializes a new instance of the MqttPublishCompleteVariableHeader class. + MqttPublishReceivedVariableHeader(); + + /// Initializes a new instance of the MqttPublishReceivedVariableHeader class. + MqttPublishReceivedVariableHeader.fromByteBuffer( + MqttByteBuffer headerStream) { + readFrom(headerStream); + } + + /// Creates a variable header from the specified header stream. + @override + void readFrom(MqttByteBuffer variableHeaderStream) { + readMessageIdentifier(variableHeaderStream); + } + + /// Writes the variable header to the supplied stream. + @override + void writeTo(MqttByteBuffer variableHeaderStream) { + writeMessageIdentifier(variableHeaderStream); + } + + /// Gets the length of the write data when WriteTo will be called. + @override + int getWriteLength() => 2; + + @override + String toString() => + 'PublishReceived Variable Header: MessageIdentifier={$messageIdentifier}'; +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/messages/publishrelease/mqtt_client_mqtt_publish_release_message.dart b/bytedesk_kefu/lib/mqtt/lib/src/messages/publishrelease/mqtt_client_mqtt_publish_release_message.dart new file mode 100755 index 0000000..b033091 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/messages/publishrelease/mqtt_client_mqtt_publish_release_message.dart @@ -0,0 +1,52 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 19/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Implementation of an MQTT Publish Release Message. +class MqttPublishReleaseMessage extends MqttMessage { + /// Initializes a new instance of the MqttPublishReleaseMessage class. + MqttPublishReleaseMessage() { + header = MqttHeader().asType(MqttMessageType.publishRelease); + // Qos is specified for this message + header!.qos = MqttQos.atLeastOnce; + variableHeader = MqttPublishReleaseVariableHeader(); + } + + /// Initializes a new instance of the MqttPublishReleaseMessage class. + MqttPublishReleaseMessage.fromByteBuffer( + MqttHeader header, MqttByteBuffer messageStream) { + this.header = header; + variableHeader = + MqttPublishReleaseVariableHeader.fromByteBuffer(messageStream); + } + + /// Gets or sets the variable header contents. Contains extended + /// metadata about the message. + late MqttPublishReleaseVariableHeader variableHeader; + + /// Writes the message to the supplied stream. + @override + void writeTo(MqttByteBuffer messageStream) { + header!.writeTo(variableHeader.getWriteLength(), messageStream); + variableHeader.writeTo(messageStream); + } + + /// Sets the message identifier of the MqttMessage. + MqttPublishReleaseMessage withMessageIdentifier(int? messageIdentifier) { + variableHeader.messageIdentifier = messageIdentifier; + return this; + } + + @override + String toString() { + final sb = StringBuffer(); + sb.write(super.toString()); + sb.writeln(variableHeader.toString()); + return sb.toString(); + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/messages/publishrelease/mqtt_client_mqtt_publish_release_variable_header.dart b/bytedesk_kefu/lib/mqtt/lib/src/messages/publishrelease/mqtt_client_mqtt_publish_release_variable_header.dart new file mode 100755 index 0000000..3aa8756 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/messages/publishrelease/mqtt_client_mqtt_publish_release_variable_header.dart @@ -0,0 +1,39 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 19/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Implementation of the variable header for an MQTT Publish Release message. +class MqttPublishReleaseVariableHeader extends MqttVariableHeader { + /// Initializes a new instance of the MqttPublishReleaseVariableHeader class. + MqttPublishReleaseVariableHeader(); + + /// Initializes a new instance of the MqttPublishReleaseVariableHeader class. + MqttPublishReleaseVariableHeader.fromByteBuffer(MqttByteBuffer headerStream) { + readFrom(headerStream); + } + + /// Creates a variable header from the specified header stream. + @override + void readFrom(MqttByteBuffer variableHeaderStream) { + readMessageIdentifier(variableHeaderStream); + } + + /// Writes the variable header to the supplied stream. + @override + void writeTo(MqttByteBuffer variableHeaderStream) { + writeMessageIdentifier(variableHeaderStream); + } + + /// Gets the length of the write data when WriteTo will be called. + @override + int getWriteLength() => 2; + + @override + String toString() => + 'PublishRelease Variable Header: MessageIdentifier={$messageIdentifier}'; +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/messages/subscribe/mqtt_client_mqtt_subscribe_message.dart b/bytedesk_kefu/lib/mqtt/lib/src/messages/subscribe/mqtt_client_mqtt_subscribe_message.dart new file mode 100755 index 0000000..551a9a9 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/messages/subscribe/mqtt_client_mqtt_subscribe_message.dart @@ -0,0 +1,101 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 19/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Implementation of an MQTT Subscribe Message. +class MqttSubscribeMessage extends MqttMessage { + /// Initializes a new instance of the MqttSubscribeMessage class. + MqttSubscribeMessage() { + header = MqttHeader().asType(MqttMessageType.subscribe); + header!.qos = MqttQos.atLeastOnce; + variableHeader = MqttSubscribeVariableHeader(); + payload = MqttSubscribePayload(); + } + + /// Initializes a new instance of the MqttSubscribeMessage class. + MqttSubscribeMessage.fromByteBuffer( + MqttHeader header, MqttByteBuffer messageStream) { + this.header = header; + this.header!.qos = MqttQos.atLeastOnce; + readFrom(messageStream); + } + + /// Gets or sets the variable header contents. Contains extended + /// metadata about the message. + MqttSubscribeVariableHeader? variableHeader; + + /// Gets or sets the payload of the Mqtt Message. + late MqttSubscribePayload payload; + + String? _lastTopic; + + /// Writes the message to the supplied stream. + @override + void writeTo(MqttByteBuffer messageStream) { + header!.writeTo(variableHeader!.getWriteLength() + payload.getWriteLength(), + messageStream); + variableHeader!.writeTo(messageStream); + payload.writeTo(messageStream); + } + + /// Reads a message from the supplied stream. + @override + void readFrom(MqttByteBuffer messageStream) { + variableHeader = MqttSubscribeVariableHeader.fromByteBuffer(messageStream); + payload = MqttSubscribePayload.fromByteBuffer( + header, variableHeader, messageStream); + } + + /// Adds a new subscription topic with the AtMostOnce Qos Level. + /// If you want to change the Qos level follow this call with a + /// call to AtTopic(MqttQos). + MqttSubscribeMessage toTopic(String topic) { + _lastTopic = topic; + payload.addSubscription(topic, MqttQos.atMostOnce); + return this; + } + + /// Sets the Qos level of the last topic added to the subscription + /// list via a call to ToTopic(string). + MqttSubscribeMessage atQos(MqttQos? qos) { + if (payload.subscriptions.containsKey(_lastTopic)) { + payload.subscriptions[_lastTopic] = qos; + } + return this; + } + + /// Sets the message identifier on the subscribe message. + MqttSubscribeMessage withMessageIdentifier(int? messageIdentifier) { + variableHeader!.messageIdentifier = messageIdentifier; + return this; + } + + /// Sets the message up to request acknowledgement from the + /// broker for each topic subscription. + MqttSubscribeMessage expectAcknowledgement() { + header!.withQos(MqttQos.atLeastOnce); + return this; + } + + /// Sets the duplicate flag for the message to indicate its a + /// duplicate of a previous message type + /// with the same message identifier. + MqttSubscribeMessage isDuplicate() { + header!.isDuplicate(); + return this; + } + + @override + String toString() { + final sb = StringBuffer(); + sb.write(super.toString()); + sb.writeln(variableHeader.toString()); + sb.writeln(payload.toString()); + return sb.toString(); + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/messages/subscribe/mqtt_client_mqtt_subscribe_payload.dart b/bytedesk_kefu/lib/mqtt/lib/src/messages/subscribe/mqtt_client_mqtt_subscribe_payload.dart new file mode 100755 index 0000000..571d852 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/messages/subscribe/mqtt_client_mqtt_subscribe_payload.dart @@ -0,0 +1,85 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 19/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Class that contains details related to an MQTT Subscribe messages payload +class MqttSubscribePayload extends MqttPayload { + /// Initializes a new instance of the MqttSubscribePayload class. + MqttSubscribePayload(); + + /// Initializes a new instance of the MqttSubscribePayload class. + MqttSubscribePayload.fromByteBuffer( + this.header, this.variableHeader, MqttByteBuffer payloadStream) { + readFrom(payloadStream); + } + + /// Variable header + MqttVariableHeader? variableHeader; + + /// Message header + MqttHeader? header; + + /// The collection of subscriptions, Key is the topic, Value is the qos + Map subscriptions = {}; + + /// Writes the payload to the supplied stream. + @override + void writeTo(MqttByteBuffer payloadStream) { + subscriptions.forEach((String? key, MqttQos? value) { + payloadStream.writeMqttStringM(key!); + payloadStream.writeByte(value!.index); + }); + } + + /// Creates a payload from the specified header stream. + @override + void readFrom(MqttByteBuffer payloadStream) { + var payloadBytesRead = 0; + final payloadLength = header!.messageSize - variableHeader!.length; + // Read all the topics and qos subscriptions from the message payload + while (payloadBytesRead < payloadLength) { + final topic = payloadStream.readMqttStringM(); + final qos = MqttUtilities.getQosLevel(payloadStream.readByte()); + payloadBytesRead += + topic.length + 3; // +3 = Mqtt string length bytes + qos byte + addSubscription(topic, qos); + } + } + + /// Gets the length of the payload in bytes when written to a stream. + @override + int getWriteLength() { + var length = 0; + final enc = MqttEncoding(); + subscriptions.forEach((String? key, MqttQos? value) { + length += enc.getByteCount(key!); + length += 1; + }); + return length; + } + + /// Adds a new subscription to the collection of subscriptions. + void addSubscription(String topic, MqttQos qos) { + subscriptions[topic] = qos; + } + + /// Clears the subscriptions. + void clearSubscriptions() { + subscriptions.clear(); + } + + @override + String toString() { + final sb = StringBuffer(); + sb.writeln('Payload: Subscription [{${subscriptions.length}}]'); + subscriptions.forEach((String? key, MqttQos? value) { + sb.writeln('{{ Topic={$key}, Qos={$value} }}'); + }); + return sb.toString(); + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/messages/subscribe/mqtt_client_mqtt_subscribe_variable_header.dart b/bytedesk_kefu/lib/mqtt/lib/src/messages/subscribe/mqtt_client_mqtt_subscribe_variable_header.dart new file mode 100755 index 0000000..fa28cf4 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/messages/subscribe/mqtt_client_mqtt_subscribe_variable_header.dart @@ -0,0 +1,39 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 19/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Implementation of the variable header for an MQTT Subscribe message. +class MqttSubscribeVariableHeader extends MqttVariableHeader { + /// Initializes a new instance of the MqttSubscribeVariableHeader class. + MqttSubscribeVariableHeader(); + + /// Initializes a new instance of the MqttSubscribeVariableHeader class. + MqttSubscribeVariableHeader.fromByteBuffer(MqttByteBuffer headerStream) { + readFrom(headerStream); + } + + /// Creates a variable header from the specified header stream. + @override + void readFrom(MqttByteBuffer variableHeaderStream) { + readMessageIdentifier(variableHeaderStream); + } + + /// Writes the variable header to the supplied stream. + @override + void writeTo(MqttByteBuffer variableHeaderStream) { + writeMessageIdentifier(variableHeaderStream); + } + + /// Gets the length of the write data when WriteTo will be called. + @override + int getWriteLength() => 2; + + @override + String toString() => + 'Subscribe Variable Header: MessageIdentifier={$messageIdentifier}'; +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/messages/subscribeack/mqtt_client_mqtt_subscribe_ack_message.dart b/bytedesk_kefu/lib/mqtt/lib/src/messages/subscribeack/mqtt_client_mqtt_subscribe_ack_message.dart new file mode 100755 index 0000000..16c4cef --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/messages/subscribeack/mqtt_client_mqtt_subscribe_ack_message.dart @@ -0,0 +1,71 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 19/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Implementation of an MQTT Subscribe Ack Message. +class MqttSubscribeAckMessage extends MqttMessage { + /// Initializes a new instance of the MqttSubscribeAckMessage class. + MqttSubscribeAckMessage() { + header = MqttHeader().asType(MqttMessageType.subscribeAck); + variableHeader = MqttSubscribeAckVariableHeader(); + payload = MqttSubscribeAckPayload(); + } + + /// Initializes a new instance of the MqttSubscribeAckMessage class. + MqttSubscribeAckMessage.fromByteBuffer( + MqttHeader header, MqttByteBuffer messageStream) { + this.header = header; + readFrom(messageStream); + } + + /// Gets or sets the variable header contents. Contains extended + /// metadata about the message. + MqttSubscribeAckVariableHeader? variableHeader; + + /// Gets or sets the payload of the Mqtt Message. + late MqttSubscribeAckPayload payload; + + /// Writes the message to the supplied stream. + @override + void writeTo(MqttByteBuffer messageStream) { + header!.writeTo(variableHeader!.getWriteLength() + payload.getWriteLength(), + messageStream); + variableHeader!.writeTo(messageStream); + payload.writeTo(messageStream); + } + + /// Reads a message from the supplied stream. + @override + void readFrom(MqttByteBuffer messageStream) { + variableHeader = + MqttSubscribeAckVariableHeader.fromByteBuffer(messageStream); + payload = MqttSubscribeAckPayload.fromByteBuffer( + header, variableHeader, messageStream); + } + + /// Sets the message identifier on the subscribe message. + MqttSubscribeAckMessage withMessageIdentifier(int messageIdentifier) { + variableHeader!.messageIdentifier = messageIdentifier; + return this; + } + + /// Adds a Qos grant to the message. + MqttSubscribeAckMessage addQosGrant(MqttQos qosGranted) { + payload.addGrant(qosGranted); + return this; + } + + @override + String toString() { + final sb = StringBuffer(); + sb.write(super.toString()); + sb.writeln(variableHeader.toString()); + sb.writeln(payload.toString()); + return sb.toString(); + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/messages/subscribeack/mqtt_client_mqtt_subscribe_ack_payload.dart b/bytedesk_kefu/lib/mqtt/lib/src/messages/subscribeack/mqtt_client_mqtt_subscribe_ack_payload.dart new file mode 100755 index 0000000..bf34868 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/messages/subscribeack/mqtt_client_mqtt_subscribe_ack_payload.dart @@ -0,0 +1,75 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 19/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Class that contains details related to an MQTT Subscribe Ack +/// messages payload. +class MqttSubscribeAckPayload extends MqttPayload { + /// Initializes a new instance of the MqttSubscribeAckPayload class. + MqttSubscribeAckPayload(); + + /// Initializes a new instance of the MqttSubscribeAckPayload class. + MqttSubscribeAckPayload.fromByteBuffer( + this.header, this.variableHeader, MqttByteBuffer payloadStream) { + readFrom(payloadStream); + } + + /// Variable header + MqttVariableHeader? variableHeader; + + /// Message header + MqttHeader? header; + + /// The collection of Qos grants, Key is the topic, Value is the qos + List qosGrants = []; + + /// Writes the payload to the supplied stream. + @override + void writeTo(MqttByteBuffer payloadStream) { + for (final value in qosGrants) { + payloadStream.writeByte(value.index); + } + } + + /// Creates a payload from the specified header stream. + @override + void readFrom(MqttByteBuffer payloadStream) { + var payloadBytesRead = 0; + final payloadLength = header!.messageSize - variableHeader!.length; + // Read the qos grants from the message payload + while (payloadBytesRead < payloadLength) { + final granted = MqttUtilities.getQosLevel(payloadStream.readByte()); + payloadBytesRead++; + addGrant(granted); + } + } + + /// Gets the length of the payload in bytes when written to a stream. + @override + int getWriteLength() => qosGrants.length; + + /// Adds a new QosGrant to the collection of QosGrants + void addGrant(MqttQos grantedQos) { + qosGrants.add(grantedQos); + } + + /// Clears the grants. + void clearGrants() { + qosGrants.clear(); + } + + @override + String toString() { + final sb = StringBuffer(); + sb.writeln('Payload: Qos grants [{${qosGrants.length}}]'); + for (final value in qosGrants) { + sb.writeln('{{ Grant={$value} }}'); + } + return sb.toString(); + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/messages/subscribeack/mqtt_client_mqtt_subscribe_ack_variable_header.dart b/bytedesk_kefu/lib/mqtt/lib/src/messages/subscribeack/mqtt_client_mqtt_subscribe_ack_variable_header.dart new file mode 100755 index 0000000..e6e8639 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/messages/subscribeack/mqtt_client_mqtt_subscribe_ack_variable_header.dart @@ -0,0 +1,39 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 19/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Implementation of the variable header for an MQTT Subscribe Ack message. +class MqttSubscribeAckVariableHeader extends MqttVariableHeader { + /// Initializes a new instance of the MqttSubscribeAckVariableHeader class. + MqttSubscribeAckVariableHeader(); + + /// Initializes a new instance of the MqttSubscribeAckVariableHeader class. + MqttSubscribeAckVariableHeader.fromByteBuffer(MqttByteBuffer headerStream) { + readFrom(headerStream); + } + + /// Creates a variable header from the specified header stream. + @override + void readFrom(MqttByteBuffer variableHeaderStream) { + readMessageIdentifier(variableHeaderStream); + } + + /// Writes the variable header to the supplied stream. + @override + void writeTo(MqttByteBuffer variableHeaderStream) { + writeMessageIdentifier(variableHeaderStream); + } + + /// Gets the length of the write data when WriteTo will be called. + @override + int getWriteLength() => 2; + + @override + String toString() => + 'SubscribeAck Variable Header: MessageIdentifier={$messageIdentifier}'; +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/messages/unsubscribe/mqtt_client_mqtt_unsubscribe_message.dart b/bytedesk_kefu/lib/mqtt/lib/src/messages/unsubscribe/mqtt_client_mqtt_unsubscribe_message.dart new file mode 100755 index 0000000..e40fd87 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/messages/unsubscribe/mqtt_client_mqtt_unsubscribe_message.dart @@ -0,0 +1,92 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 19/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Implementation of an MQTT Unsubscribe Message. +class MqttUnsubscribeMessage extends MqttMessage { + /// Initializes a new instance of the MqttUnsubscribeMessage class. + MqttUnsubscribeMessage() { + header = MqttHeader().asType(MqttMessageType.unsubscribe); + variableHeader = MqttUnsubscribeVariableHeader(); + payload = MqttUnsubscribePayload(); + } + + /// Initializes a new instance of the MqttUnsubscribeMessage class. + MqttUnsubscribeMessage.fromByteBuffer( + MqttHeader header, MqttByteBuffer messageStream) { + this.header = header; + readFrom(messageStream); + } + + /// Gets or sets the variable header contents. Contains extended + /// metadata about the message. + MqttUnsubscribeVariableHeader? variableHeader; + + /// Gets or sets the payload of the Mqtt Message. + late MqttUnsubscribePayload payload; + + /// Writes the message to the supplied stream. + @override + void writeTo(MqttByteBuffer messageStream) { + // If the protocol is V3.1.1 the following header fields + // must be set as below as in this protocol they are reserved. + if (Protocol.version == MqttClientConstants.mqttV311ProtocolVersion) { + header!.duplicate = false; + header!.qos = MqttQos.atLeastOnce; + header!.retain = false; + } + header!.writeTo(variableHeader!.getWriteLength() + payload.getWriteLength(), + messageStream); + variableHeader!.writeTo(messageStream); + payload.writeTo(messageStream); + } + + /// Reads a message from the supplied stream. + @override + void readFrom(MqttByteBuffer messageStream) { + variableHeader = + MqttUnsubscribeVariableHeader.fromByteBuffer(messageStream); + payload = MqttUnsubscribePayload.fromByteBuffer( + header, variableHeader, messageStream); + } + + /// Adds a topic to the list of topics to unsubscribe from. + MqttUnsubscribeMessage fromTopic(String topic) { + payload.addSubscription(topic); + return this; + } + + /// Sets the message identifier on the subscribe message. + MqttUnsubscribeMessage withMessageIdentifier(int messageIdentifier) { + variableHeader!.messageIdentifier = messageIdentifier; + return this; + } + + /// Sets the message up to request acknowledgement from the + /// broker for each topic subscription. + MqttUnsubscribeMessage expectAcknowledgement() { + header!.withQos(MqttQos.atLeastOnce); + return this; + } + + /// Sets the duplicate flag for the message to indicate its a + /// duplicate of a previous message type with the same message identifier. + MqttUnsubscribeMessage isDuplicate() { + header!.isDuplicate(); + return this; + } + + @override + String toString() { + final sb = StringBuffer(); + sb.write(super.toString()); + sb.writeln(variableHeader.toString()); + sb.writeln(payload.toString()); + return sb.toString(); + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/messages/unsubscribe/mqtt_client_mqtt_unsubscribe_payload.dart b/bytedesk_kefu/lib/mqtt/lib/src/messages/unsubscribe/mqtt_client_mqtt_unsubscribe_payload.dart new file mode 100755 index 0000000..1154776 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/messages/unsubscribe/mqtt_client_mqtt_unsubscribe_payload.dart @@ -0,0 +1,79 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 19/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Class that contains details related to an MQTT Unsubscribe messages payload +class MqttUnsubscribePayload extends MqttPayload { + /// Initializes a new instance of the MqttUnsubscribePayload class. + MqttUnsubscribePayload(); + + /// Initializes a new instance of the MqttUnsubscribePayload class. + MqttUnsubscribePayload.fromByteBuffer( + this.header, this.variableHeader, MqttByteBuffer payloadStream) { + readFrom(payloadStream); + } + + /// Variable header + MqttVariableHeader? variableHeader; + + /// Message header + MqttHeader? header; + + /// The collection of subscriptions. + List subscriptions = []; + + /// Writes the payload to the supplied stream. + @override + void writeTo(MqttByteBuffer payloadStream) { + subscriptions.forEach(payloadStream.writeMqttStringM); + } + + /// Creates a payload from the specified header stream. + @override + void readFrom(MqttByteBuffer payloadStream) { + var payloadBytesRead = 0; + final payloadLength = header!.messageSize - variableHeader!.length; + // Read all the topics and qos subscriptions from the message payload + while (payloadBytesRead < payloadLength) { + final topic = payloadStream.readMqttStringM(); + payloadBytesRead += topic.length + 2; // +2 = Mqtt string length bytes + addSubscription(topic); + } + } + + /// Gets the length of the payload in bytes when written to a stream. + @override + int getWriteLength() { + var length = 0; + final enc = MqttEncoding(); + for (final subscription in subscriptions) { + length += enc.getByteCount(subscription); + } + return length; + } + + /// Adds a new subscription to the collection of subscriptions. + void addSubscription(String topic) { + subscriptions.add(topic); + } + + /// Clears the subscriptions. + void clearSubscriptions() { + subscriptions.clear(); + } + + @override + String toString() { + final sb = StringBuffer(); + sb.writeln('Payload: Unsubscription [{${subscriptions.length}}]'); + for (final subscription in subscriptions) { + sb.writeln('{{ Topic={$subscription}}'); + } + return sb.toString(); + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/messages/unsubscribe/mqtt_client_mqtt_unsubscribe_variable_header.dart b/bytedesk_kefu/lib/mqtt/lib/src/messages/unsubscribe/mqtt_client_mqtt_unsubscribe_variable_header.dart new file mode 100755 index 0000000..967d2f1 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/messages/unsubscribe/mqtt_client_mqtt_unsubscribe_variable_header.dart @@ -0,0 +1,39 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 19/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Implementation of the variable header for an MQTT Unsubscribe message. +class MqttUnsubscribeVariableHeader extends MqttVariableHeader { + /// Initializes a new instance of the MqttUnsubscribeVariableHeader class. + MqttUnsubscribeVariableHeader(); + + /// Initializes a new instance of the MqttUnsubscribeVariableHeader class. + MqttUnsubscribeVariableHeader.fromByteBuffer(MqttByteBuffer headerStream) { + readFrom(headerStream); + } + + /// Creates a variable header from the specified header stream. + @override + void readFrom(MqttByteBuffer variableHeaderStream) { + readMessageIdentifier(variableHeaderStream); + } + + /// Writes the variable header to the supplied stream. + @override + void writeTo(MqttByteBuffer variableHeaderStream) { + writeMessageIdentifier(variableHeaderStream); + } + + /// Gets the length of the write data when WriteTo will be called. + @override + int getWriteLength() => 2; + + @override + String toString() => 'Unsubscribe VariableHeader Variable Header: ' + 'MessageIdentifier={$messageIdentifier}'; +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/messages/unsubscribeack/mqtt_client_mqtt_unsubscribe_ack_message.dart b/bytedesk_kefu/lib/mqtt/lib/src/messages/unsubscribeack/mqtt_client_mqtt_unsubscribe_ack_message.dart new file mode 100755 index 0000000..c5a49fd --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/messages/unsubscribeack/mqtt_client_mqtt_unsubscribe_ack_message.dart @@ -0,0 +1,56 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 19/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Implementation of an MQTT Unsubscribe Ack Message. +class MqttUnsubscribeAckMessage extends MqttMessage { + /// Initializes a new instance of the MqttUnsubscribeAckMessage class. + MqttUnsubscribeAckMessage() { + header = MqttHeader().asType(MqttMessageType.unsubscribeAck); + variableHeader = MqttUnsubscribeAckVariableHeader(); + } + + /// Initializes a new instance of the MqttUnsubscribeAckMessage class. + MqttUnsubscribeAckMessage.fromByteBuffer( + MqttHeader header, MqttByteBuffer messageStream) { + this.header = header; + readFrom(messageStream); + } + + /// Gets or sets the variable header contents. Contains extended + /// metadata about the message. + late MqttUnsubscribeAckVariableHeader variableHeader; + + /// Writes the message to the supplied stream. + @override + void writeTo(MqttByteBuffer messageStream) { + header!.writeTo(variableHeader.getWriteLength(), messageStream); + variableHeader.writeTo(messageStream); + } + + /// Reads a message from the supplied stream. + @override + void readFrom(MqttByteBuffer messageStream) { + variableHeader = + MqttUnsubscribeAckVariableHeader.fromByteBuffer(messageStream); + } + + /// Sets the message identifier on the subscribe message. + MqttUnsubscribeAckMessage withMessageIdentifier(int messageIdentifier) { + variableHeader.messageIdentifier = messageIdentifier; + return this; + } + + @override + String toString() { + final sb = StringBuffer(); + sb.write(super.toString()); + sb.writeln(variableHeader.toString()); + return sb.toString(); + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/messages/unsubscribeack/mqtt_client_mqtt_unsubscribe_ack_variable_header.dart b/bytedesk_kefu/lib/mqtt/lib/src/messages/unsubscribeack/mqtt_client_mqtt_unsubscribe_ack_variable_header.dart new file mode 100755 index 0000000..cb17fab --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/messages/unsubscribeack/mqtt_client_mqtt_unsubscribe_ack_variable_header.dart @@ -0,0 +1,39 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 19/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Implementation of the variable header for an MQTT Unsubscribe Ack message. +class MqttUnsubscribeAckVariableHeader extends MqttVariableHeader { + /// Initializes a new instance of the MqttUnsubscribeAckVariableHeader class. + MqttUnsubscribeAckVariableHeader(); + + /// Initializes a new instance of the MqttUnsubscribeAckVariableHeader class. + MqttUnsubscribeAckVariableHeader.fromByteBuffer(MqttByteBuffer headerStream) { + readFrom(headerStream); + } + + /// Creates a variable header from the specified header stream. + @override + void readFrom(MqttByteBuffer variableHeaderStream) { + readMessageIdentifier(variableHeaderStream); + } + + /// Writes the variable header to the supplied stream. + @override + void writeTo(MqttByteBuffer variableHeaderStream) { + writeMessageIdentifier(variableHeaderStream); + } + + /// Gets the length of the write data when WriteTo will be called. + @override + int getWriteLength() => 2; + + @override + String toString() => + 'UnsubscribeAck Variable Header: MessageIdentifier={$messageIdentifier}'; +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/mqtt_browser_client.dart b/bytedesk_kefu/lib/mqtt/lib/src/mqtt_browser_client.dart new file mode 100755 index 0000000..1797d98 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/mqtt_browser_client.dart @@ -0,0 +1,53 @@ +/* + * Package : mqtt_browser_client + * Author : S. Hamblett + * Date : 21/01/2020 + * Copyright : S.Hamblett + */ + +part of mqtt_browser_client; + +class MqttBrowserClient extends MqttClient { + /// Initializes a new instance of the MqttServerClient class using the + /// default Mqtt Port. + /// The server hostname or URL to connect to + /// The client identifier to use to connect with + MqttBrowserClient( + String server, + String clientIdentifier, { + this.maxConnectionAttempts = 3, + }) : super(server, clientIdentifier); + + /// Initializes a new instance of the MqttServerClient class using + /// the supplied Mqtt Port. + /// The server hostname to connect to + /// The client identifier to use to connect with + /// The port to use + MqttBrowserClient.withPort( + String server, + String clientIdentifier, + int port, { + this.maxConnectionAttempts = 3, + }) : super.withPort(server, clientIdentifier, port); + + /// Max connection attempts + final int maxConnectionAttempts; + + /// Performs a connect to the message broker with an optional + /// username and password for the purposes of authentication. + /// If a username and password are supplied these will override + /// any previously set in a supplied connection message so if you + /// supply your own connection message and use the authenticateAs method to + /// set these parameters do not set them again here. + @override + Future connect( + [String? username, String? password]) async { + instantiationCorrect = true; + clientEventBus = events.EventBus(); + connectionHandler = SynchronousMqttBrowserConnectionHandler( + clientEventBus, + maxConnectionAttempts: maxConnectionAttempts, + ); + return await super.connect(username, password); + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client.dart b/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client.dart new file mode 100755 index 0000000..7094aa3 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client.dart @@ -0,0 +1,488 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 10/07/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// The client disconnect callback type +typedef DisconnectCallback = void Function(); + +/// The client Connect callback type +typedef ConnectCallback = void Function(); + +/// The client auto reconnect callback type +typedef AutoReconnectCallback = void Function(); + +/// The client auto reconnect complete callback type +typedef AutoReconnectCompleteCallback = void Function(); + +/// A client class for interacting with MQTT Data Packets. +/// Do not instantiate this class directly, instead instantiate +/// either a [MqttClientServer] class or an [MqttBrowserClient] as needed. +/// This class now provides common functionality between server side +/// and web based clients. +class MqttClient { + /// Initializes a new instance of the MqttClient class using the + /// default Mqtt Port. + /// The server hostname to connect to + /// The client identifier to use to connect with + MqttClient(this.server, this.clientIdentifier) { + port = MqttClientConstants.defaultMqttPort; + } + + /// Initializes a new instance of the MqttClient class using + /// the supplied Mqtt Port. + /// The server hostname to connect to + /// The client identifier to use to connect with + /// The port to use + MqttClient.withPort(this.server, this.clientIdentifier, this.port); + + /// Server name + String server; + + /// Port number + int? port; + + /// Client identifier + String clientIdentifier; + + /// Incorrect instantiation protection + @protected + var instantiationCorrect = false; + + /// Auto reconnect, the client will auto reconnect if set true. + /// + /// The auto reconnect mechanism will not be invoked either for a client + /// that has not been connected, i.e. you must have established an initial + /// connection to the broker or for a solicited disconnect request. + /// + /// Once invoked the mechanism will try forever to reconnect to the broker with its + /// original connection parameters. This can be stopped only by calling + /// [disconnect()] on the client. + bool autoReconnect = false; + + /// Re subscribe on auto reconnect. + /// Auto reconnect will perform automatic re subscription of existing confirmed subscriptions + /// unless this is set to false. + /// In this case the caller must perform their own re subscriptions manually using [unsubscribe], + /// [subscribe] and [resubscribe] as needed from the appropriate callbacks. + bool resubscribeOnAutoReconnect = true; + + /// Indicates that received QOS 1 messages(AtLeastOnce) are not to be automatically acknowledged by + /// the client. The user must do this when the message has been taken off the update stream + /// using the [acknowledgeQos1Message] method. + bool _manuallyAcknowledgeQos1 = false; + set manuallyAcknowledgeQos1(bool state) { + publishingManager?.manuallyAcknowledgeQos1 = state; + _manuallyAcknowledgeQos1 = state; + } + + bool get manuallyAcknowledgeQos1 => _manuallyAcknowledgeQos1; + + /// Manually acknowledge a received QOS 1 message. + /// Has no effect if [manuallyAcknowledgeQos1] is not in force + /// or the message is not awaiting a QOS 1 acknowledge. + /// Returns true if an acknowledgement is sent to the broker. + bool? acknowledgeQos1Message(MqttPublishMessage message) => + publishingManager?.acknowledgeQos1Message(message); + + /// The number of QOS 1 messages awaiting manual acknowledge. + int get messagesAwaitingManualAcknowledge => publishingManager == null + ? 0 + : publishingManager!.awaitingManualAcknowledge.length; + + /// The Handler that is managing the connection to the remote server. + @protected + dynamic connectionHandler; + + @protected + List? websocketProtocolString; + + /// User definable websocket protocols. Use this for non default websocket + /// protocols only if your broker needs this. There are two defaults in + /// MqttWsConnection class, the multiple protocol is the default. Some brokers + /// will not accept a list and only expect a single protocol identifier, + /// in this case use the single protocol default. You can supply your own + /// list, or to disable this entirely set the protocols to an + /// empty list , i.e []. + set websocketProtocols(List protocols) { + websocketProtocolString = protocols; + if (connectionHandler != null) { + connectionHandler.websocketProtocols = protocols; + } + } + + /// The subscriptions manager responsible for tracking subscriptions. + @protected + SubscriptionsManager? subscriptionsManager; + + /// Handles the connection management while idle. + /// Not instantiated if keep alive is disabled. + @protected + MqttConnectionKeepAlive? keepAlive; + + /// Keep alive period, seconds. + /// Keep alive is defaulted to off, this must be set to a valid value to + /// enable keep alive. + int keepAlivePeriod = MqttClientConstants.defaultKeepAlive; + + /// Handles everything to do with publication management. + @protected + PublishingManager? publishingManager; + + /// Published message stream. A publish message is added to this + /// stream on completion of the message publishing protocol for a Qos level. + /// Attach listeners only after connect has been called. + Stream? get published => + publishingManager != null ? publishingManager!.published.stream : null; + + /// Gets the current connection state of the Mqtt Client. + /// Will be removed, use connectionStatus + @Deprecated('Use ConnectionStatus, not this') + MqttConnectionState? get connectionState => connectionHandler != null + ? connectionHandler.connectionStatus.state + : MqttConnectionState.disconnected; + + final MqttClientConnectionStatus _connectionStatus = + MqttClientConnectionStatus(); + + /// Gets the current connection status of the Mqtt Client. + /// This is the connection state as above also with the broker return code. + /// Set after every connection attempt. + MqttClientConnectionStatus? get connectionStatus => connectionHandler != null + ? connectionHandler.connectionStatus + : _connectionStatus; + + /// The connection message to use to override the default + MqttConnectMessage? connectionMessage; + + /// Client disconnect callback, called on unsolicited disconnect. + /// This will not be called even if set if [autoReconnect} is set,instead + /// [AutoReconnectCallback] will be called. + DisconnectCallback? onDisconnected; + + /// Client connect callback, called on successful connect + ConnectCallback? onConnected; + + /// Auto reconnect callback, if auto reconnect is selected this callback will + /// be called before auto reconnect processing is invoked to allow the user to + /// perform any pre auto reconnect actions. + AutoReconnectCallback? onAutoReconnect; + + /// Auto reconnected callback, if auto reconnect is selected this callback will + /// be called after auto reconnect processing is completed to allow the user to + /// perform any post auto reconnect actions. + AutoReconnectCompleteCallback? onAutoReconnected; + + /// Subscribed callback, function returns a void and takes a + /// string parameter, the topic that has been subscribed to. + SubscribeCallback? _onSubscribed; + + /// On subscribed + SubscribeCallback? get onSubscribed => _onSubscribed; + + set onSubscribed(SubscribeCallback? cb) { + _onSubscribed = cb; + subscriptionsManager?.onSubscribed = cb; + } + + /// Subscribed failed callback, function returns a void and takes a + /// string parameter, the topic that has failed subscription. + /// Invoked either by subscribe if an invalid topic is supplied or on + /// reception of a failed subscribe indication from the broker. + SubscribeFailCallback? _onSubscribeFail; + + /// On subscribed fail + SubscribeFailCallback? get onSubscribeFail => _onSubscribeFail; + + set onSubscribeFail(SubscribeFailCallback? cb) { + _onSubscribeFail = cb; + subscriptionsManager?.onSubscribeFail = cb; + } + + /// Unsubscribed callback, function returns a void and takes a + /// string parameter, the topic that has been unsubscribed. + UnsubscribeCallback? _onUnsubscribed; + + /// On unsubscribed + UnsubscribeCallback? get onUnsubscribed => _onUnsubscribed; + + set onUnsubscribed(UnsubscribeCallback? cb) { + _onUnsubscribed = cb; + subscriptionsManager?.onUnsubscribed = cb; + } + + /// Ping response received callback. + /// If set when a ping response is received from the broker + /// this will be called. + /// Can be used for health monitoring outside of the client itself. + PongCallback? _pongCallback; + + /// The ping received callback + PongCallback? get pongCallback => _pongCallback; + + set pongCallback(PongCallback? cb) { + _pongCallback = cb; + keepAlive?.pongCallback = cb; + } + + /// The event bus + @protected + events.EventBus? clientEventBus; + + /// The stream on which all subscribed topic updates are published to + Stream>>? get updates => + subscriptionsManager?.subscriptionNotifier; + + /// Common client connection method. + Future connect( + [String? username, String? password]) async { + // Protect against an incorrect instantiation + if (!instantiationCorrect) { + throw IncorrectInstantiationException(); + } + // Generate the client id for logging + MqttLogger.clientId++; + + checkCredentials(username, password); + // Set the authentication parameters in the connection + // message if we have one. + connectionMessage?.authenticateAs(username, password); + + // Do the connection + if (websocketProtocolString != null) { + connectionHandler.websocketProtocols = websocketProtocolString; + } + connectionHandler.onDisconnected = internalDisconnect; + connectionHandler.onConnected = onConnected; + connectionHandler.onAutoReconnect = onAutoReconnect; + connectionHandler.onAutoReconnected = onAutoReconnected; + + publishingManager = PublishingManager(connectionHandler, clientEventBus); + publishingManager!.manuallyAcknowledgeQos1 = _manuallyAcknowledgeQos1; + subscriptionsManager = SubscriptionsManager( + connectionHandler, publishingManager, clientEventBus); + subscriptionsManager!.onSubscribed = onSubscribed; + subscriptionsManager!.onUnsubscribed = onUnsubscribed; + subscriptionsManager!.onSubscribeFail = onSubscribeFail; + subscriptionsManager!.resubscribeOnAutoReconnect = + resubscribeOnAutoReconnect; + if (keepAlivePeriod != MqttClientConstants.defaultKeepAlive) { + MqttLogger.log( + 'MqttClient::connect - keep alive is enabled with a value of $keepAlivePeriod seconds'); + keepAlive = MqttConnectionKeepAlive(connectionHandler, keepAlivePeriod); + if (pongCallback != null) { + keepAlive!.pongCallback = pongCallback; + } + } else { + MqttLogger.log('MqttClient::connect - keep alive is disabled'); + } + final connectMessage = getConnectMessage(username, password); + // If the client id is not set in the connection message use the one + // supplied in the constructor. + if (connectMessage.payload.clientIdentifier.isEmpty) { + connectMessage.payload.clientIdentifier = clientIdentifier; + } + // Set keep alive period. + connectMessage.variableHeader?.keepAlive = keepAlivePeriod; + connectionMessage = connectMessage; + return connectionHandler.connect(server, port, connectMessage); + } + + /// Gets a pre-configured connect message if one has not been + /// supplied by the user. + /// Returns an MqttConnectMessage that can be used to connect to a + /// message broker if the user has not set one. + MqttConnectMessage getConnectMessage(String? username, String? password) => + connectionMessage ??= MqttConnectMessage() + .withClientIdentifier(clientIdentifier) + // Explicitly set the will flag + .withWillQos(MqttQos.atMostOnce) + .authenticateAs(username, password) + .startClean(); + + /// Auto reconnect method, used to invoke a manual auto reconnect sequence. + /// If [autoReconnect] is not set this method does nothing. + /// If the client is not disconnected this method will have no effect + /// unless the [force] parameter is set to true, otherwise + /// auto reconnect will try indefinitely to reconnect to the broker. + void doAutoReconnect({bool force = false}) { + if (!autoReconnect) { + MqttLogger.log( + 'MqttClient::doAutoReconnect - auto reconnect is not set, exiting'); + return; + } + + if (connectionStatus!.state != MqttConnectionState.connected || force) { + // Fire a manual auto reconnect request. + final wasConnected = + connectionStatus!.state == MqttConnectionState.connected; + clientEventBus! + .fire(AutoReconnect(userRequested: true, wasConnected: wasConnected)); + } + } + + /// Initiates a topic subscription request to the connected broker + /// with a strongly typed data processor callback. + /// The topic to subscribe to. + /// The qos level the message was published at. + /// Returns the subscription or null on failure + Subscription? subscribe(String topic, MqttQos qosLevel) { + if (connectionStatus!.state != MqttConnectionState.connected) { + throw ConnectionException(connectionHandler?.connectionStatus?.state); + } + return subscriptionsManager!.registerSubscription(topic, qosLevel); + } + + /// Re subscribe. + /// Unsubscribes all confirmed subscriptions and re subscribes them + /// without sending unsubscribe messages to the broker. + /// If an unsubscribe message to the broker is needed then use + /// [unsubscribe] followed by [subscribe] for each subscription. + /// Can be used in auto reconnect processing to force manual re subscription of all existing + /// confirmed subscriptions. + void resubscribe() => subscriptionsManager!.resubscribe(); + + /// Publishes a message to the message broker. + /// Returns The message identifer assigned to the message. + /// Raises InvalidTopicException if the topic supplied violates the + /// MQTT topic format rules. + int publishMessage( + String topic, MqttQos qualityOfService, typed.Uint8Buffer data, + {bool retain = false}) { + if (connectionHandler?.connectionStatus?.state != + MqttConnectionState.connected) { + throw ConnectionException(connectionHandler?.connectionStatus?.state); + } + try { + final pubTopic = PublicationTopic(topic); + return publishingManager! + .publish(pubTopic, qualityOfService, data, retain); + } on Exception catch (e) { + throw InvalidTopicException(e.toString(), topic); + } + } + + /// Unsubscribe from a topic + void unsubscribe(String topic) { + subscriptionsManager!.unsubscribe(topic); + } + + /// Gets the current status of a subscription. + MqttSubscriptionStatus getSubscriptionsStatus(String topic) => + subscriptionsManager!.getSubscriptionsStatus(topic); + + /// Disconnect from the broker. + /// This is a hard disconnect, a disconnect message is sent to the + /// broker and the client is then reset to its pre-connection state, + /// i.e all subscriptions are deleted, on subsequent reconnection the + /// use must re-subscribe, also the updates change notifier is re-initialised + /// and as such the user must re-listen on this stream. + /// + /// Do NOT call this in any onDisconnect callback that may be set, + /// this will result in a loop situation. + /// + /// This method will disconnect regardless of the [autoReconnect] state. + void disconnect() { + _disconnect(unsolicited: false); + } + + /// Internal disconnect + /// This is always passed to the connection handler to allow the + /// client to close itself down correctly on disconnect. + @protected + void internalDisconnect() { + // if we don't have a connection Handler we are already disconnected. + if (connectionHandler == null) { + MqttLogger.log( + 'MqttClient::internalDisconnect - not invoking disconnect, no connection handler'); + return; + } + if (autoReconnect && connectionHandler.initialConnectionComplete) { + if (!connectionHandler.autoReconnectInProgress) { + // Fire an automatic auto reconnect request + clientEventBus!.fire(AutoReconnect(userRequested: false)); + } else { + MqttLogger.log( + 'MqttClient::internalDisconnect - not invoking auto connect, already in progress'); + } + } else { + // Unsolicited disconnect only if we are connected initially + if (connectionHandler.initialConnectionComplete) { + _disconnect(unsolicited: true); + } + } + } + + /// Actual disconnect processing + void _disconnect({bool unsolicited = true}) { + // Only disconnect the connection handler if the request is + // solicited, unsolicited requests, ie broker termination don't + // need this. + var disconnectOrigin = MqttDisconnectionOrigin.unsolicited; + if (!unsolicited) { + connectionHandler?.disconnect(); + disconnectOrigin = MqttDisconnectionOrigin.solicited; + } + publishingManager?.published.close(); + publishingManager = null; + subscriptionsManager = null; + keepAlive?.stop(); + keepAlive = null; + _connectionStatus.returnCode = connectionStatus?.returnCode; + connectionHandler = null; + clientEventBus?.destroy(); + clientEventBus = null; + // Set the connection status before calling onDisconnected + _connectionStatus.state = MqttConnectionState.disconnected; + _connectionStatus.disconnectionOrigin = disconnectOrigin; + if (onDisconnected != null) { + onDisconnected!(); + } + } + + /// Check the username and password validity + @protected + void checkCredentials(String? username, String? password) { + if (username != null) { + MqttLogger.log("Authenticating with username '{$username}' " + "and password '{$password}'"); + if (username.trim().length > + MqttClientConstants.recommendedMaxUsernamePasswordLength) { + MqttLogger.log( + 'MqttClient::checkCredentials - Username length (${username.trim().length}) ' + 'exceeds the max recommended in the MQTT spec. '); + } + } + if (password != null && + password.trim().length > + MqttClientConstants.recommendedMaxUsernamePasswordLength) { + MqttLogger.log( + 'MqttClient::checkCredentials - Password length (${password.trim().length}) ' + 'exceeds the max recommended in the MQTT spec. '); + } + } + + /// Turn on logging, true to start, false to stop + void logging({required bool on}) { + MqttLogger.loggingOn = false; + if (on) { + MqttLogger.loggingOn = true; + } + } + + /// Set the protocol version to V3.1 - default + void setProtocolV31() { + Protocol.version = MqttClientConstants.mqttV31ProtocolVersion; + Protocol.name = MqttClientConstants.mqttV31ProtocolName; + } + + /// Set the protocol version to V3.1.1 + void setProtocolV311() { + Protocol.version = MqttClientConstants.mqttV311ProtocolVersion; + Protocol.name = MqttClientConstants.mqttV311ProtocolName; + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_connection_status.dart b/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_connection_status.dart new file mode 100755 index 0000000..7863a1f --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_connection_status.dart @@ -0,0 +1,28 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 31/05/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Records the status of the last connection attempt +class MqttClientConnectionStatus { + /// Connection state + MqttConnectionState state = MqttConnectionState.disconnected; + + /// Return code + MqttConnectReturnCode? returnCode = MqttConnectReturnCode.noneSpecified; + + /// Disconnection origin + MqttDisconnectionOrigin disconnectionOrigin = MqttDisconnectionOrigin.none; + + @override + String toString() { + final s = state.toString().split('.')[1]; + final r = returnCode.toString().split('.')[1]; + final t = disconnectionOrigin.toString().split('.')[1]; + return 'Connection status is $s with return code of $r and a disconnection origin of $t'; + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_constants.dart b/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_constants.dart new file mode 100755 index 0000000..2e9a20b --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_constants.dart @@ -0,0 +1,57 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 31/05/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Library wide constants +class MqttClientConstants { + /// The Maximum allowed message size as defined by the MQTT v3 Spec (256MB). + static const int maxMessageSize = 268435455; + + /// The Maximum allowed client identifier length as specified by the 3.1 + /// specification is 23 characters, however we allow more than + /// this, a warning is given in the log if 23 is exceeded. + /// NOte: this is only a warning, it changes no client behaviour. + static const int maxClientIdentifierLength = 1024; + + /// Specification length + static const int maxClientIdentifierLengthSpec = 23; + + /// The default Mqtt port to connect to. + static const int defaultMqttPort = 1883; + + /// The recommended length for usernames and passwords. + static const int recommendedMaxUsernamePasswordLength = 12; + + /// Default keep alive in seconds. + /// The default of 0 disables keep alive. + static int defaultKeepAlive = 0; + + /// Protocol variants + /// V3 + static const int mqttV31ProtocolVersion = 3; + + /// V3 name + static const String mqttV31ProtocolName = 'MQIsdp'; + + /// V4 + static const int mqttV311ProtocolVersion = 4; + + /// V4 name + static const String mqttV311ProtocolName = 'MQTT'; + + /// The default websocket subprotocol list + static const List protocolsMultipleDefault = [ + 'mqtt', + 'mqttv3.1', + 'mqttv3.11' + ]; + + /// The default websocket subprotocol list for brokers who expect + /// this field to be a single entry + static const List protocolsSingleDefault = ['mqtt']; +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_events.dart b/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_events.dart new file mode 100755 index 0000000..f673bdc --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_events.dart @@ -0,0 +1,72 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 22/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// The message available event raised by the Connection class +class MessageAvailable { + /// Constructor + MessageAvailable(this._message); + + /// The message associated with the event + final MqttMessage? _message; + + /// Message + MqttMessage? get message => _message; +} + +/// The connect acknowledge message available event raised by the Connection class +class ConnectAckMessageAvailable { + /// Constructor + ConnectAckMessageAvailable(this._message); + + /// The message associated with the event + final MqttMessage? _message; + + /// Message + MqttMessage? get message => _message; +} + +/// Message recieved class for publishing +class MessageReceived { + /// Constructor + MessageReceived(this._topic, this._message); + + /// The message associated with the event + final MqttMessage _message; + + /// Message + MqttMessage get message => _message; + + /// The topic + final PublicationTopic _topic; + + /// Topic + PublicationTopic get topic => _topic; +} + +/// Auto reconnect event +class AutoReconnect { + /// Constructor + AutoReconnect({this.userRequested = false, this.wasConnected = false}); + + /// If set auto reconnect has been invoked through the client + /// [doAutoReconnect] method, i.e. a user request. + bool userRequested = false; + + /// True if the previous state was connected + bool wasConnected = false; +} + +/// Re subscribe event +class Resubscribe { + /// Constructor + Resubscribe({this.fromAutoReconnect = false}); + + /// If set re sunscribe has been triggered from auto reconnect. + bool fromAutoReconnect = false; +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_ipublishing_manager.dart b/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_ipublishing_manager.dart new file mode 100755 index 0000000..5fc809d --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_ipublishing_manager.dart @@ -0,0 +1,24 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 30/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Interface that defines how the publishing manager publishes +/// messages to the broker and how it passed on messages that are +/// received from the broker. +abstract class IPublishingManager { + /// Publish a message to the broker on the specified topic. + /// The topic to send the message to + /// The QOS to use when publishing the message. + /// The message to send. + /// The message identifier assigned to the message. + int publish( + PublicationTopic topic, MqttQos qualityOfService, typed.Uint8Buffer data); + + /// The message received event + MessageReceived? publishEvent; +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_message_identifier_dispenser.dart b/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_message_identifier_dispenser.dart new file mode 100755 index 0000000..ac8da30 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_message_identifier_dispenser.dart @@ -0,0 +1,48 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 30/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Message identifier handling +class MessageIdentifierDispenser { + /// Factory constructor + factory MessageIdentifierDispenser() => _singleton; + + MessageIdentifierDispenser._internal(); + + static final MessageIdentifierDispenser _singleton = + MessageIdentifierDispenser._internal(); + + /// Maximum message identifier + static const int maxMessageIdentifier = 32768; + + /// Initial value + static const int initialValue = 0; + + /// Minimum message identifier + static const int startMessageIdentifier = 1; + + /// Message identifier, zero is forbidden + int _mid = initialValue; + + /// Mid + int get mid => _mid; + + /// Gets the next message identifier + int getNextMessageIdentifier() { + _mid++; + if (_mid == maxMessageIdentifier) { + _mid = startMessageIdentifier; + } + return mid; + } + + /// Resets the mid + void reset() { + _mid = initialValue; + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_mqtt_qos.dart b/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_mqtt_qos.dart new file mode 100755 index 0000000..a3b36f8 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_mqtt_qos.dart @@ -0,0 +1,33 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 31/05/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Enumeration of available QoS types. +enum MqttQos { + /// QOS Level 0 - Message is not guaranteed delivery. No retries are made + /// to ensure delivery is successful. + atMostOnce, + + /// QOS Level 1 - Message is guaranteed delivery. It will be delivered at + /// least one time, but may be delivered more than once if network + /// errors occur. + atLeastOnce, + + /// QOS Level 2 - Message will be delivered once, and only once. + /// Message will be retried until it is successfully sent. + exactlyOnce, + + /// Reserved by the MQTT Spec. Currently unused from here on until the fail + /// indicator below + reserved1, + + /// Failure indication + /// This is a QOS value of 128, used in a sub ack message to indicate failure + /// to subscribe to a topic + failure +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_mqtt_received_message.dart b/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_mqtt_received_message.dart new file mode 100755 index 0000000..52707d6 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_mqtt_received_message.dart @@ -0,0 +1,20 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 31/05/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Represents a MQTT message that has been received from a broker. +class MqttReceivedMessage extends observe.ChangeRecord { + /// Initializes a new instance of an MqttReceivedMessage class. + MqttReceivedMessage(this.topic, this.payload); + + /// The topic the message was received on. + String topic; + + /// The payload of the message received. + T payload; +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_protocol.dart b/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_protocol.dart new file mode 100755 index 0000000..8fdef4a --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_protocol.dart @@ -0,0 +1,17 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 31/05/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Protocol selection helper class, protocol defaults V3.1 +class Protocol { + /// Version + static int version = MqttClientConstants.mqttV31ProtocolVersion; + + /// Name + static String name = MqttClientConstants.mqttV31ProtocolName; +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_publication_topic.dart b/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_publication_topic.dart new file mode 100755 index 0000000..f78d912 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_publication_topic.dart @@ -0,0 +1,30 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 31/05/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Implementation of a Publication topic that performs additional validations +/// of messages that are published. +class PublicationTopic extends Topic { + /// Construction + PublicationTopic(String topic) + : super(topic, [ + Topic.validateMinLength, + Topic.validateMaxLength, + _validateWildcards + ]); + + /// Validates that the topic has no wildcards which are not allowed + /// in publication topics. + static void _validateWildcards(Topic topicInstance) { + if (topicInstance.hasWildcards) { + throw Exception( + 'mqtt_client::PublicationTopic: Cannot publish to a topic that ' + 'contains MQTT topic wildcards (# or +)'); + } + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_publishing_manager.dart b/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_publishing_manager.dart new file mode 100755 index 0000000..2df576e --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_publishing_manager.dart @@ -0,0 +1,255 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 30/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Handles the logic and workflow surrounding the message publishing and receipt process. +/// +/// It's probably worth going into a bit of the detail around publishing and Quality of Service levels +/// as they are primarily the reason why message publishing has been split out into this class. +/// +/// There are 3 different QOS levels. QOS0 AtMostOnce(0), means that the message, when sent from broker to client, or +/// client to broker, should be delivered at most one time, and it does not matter if the message is +/// "lost". QOS 1, AtLeastOnce(1), means that the message should be successfully received by the receiving +/// party at least one time, so requires some sort of acknowledgement so the sender can re-send if the +/// receiver does not acknowledge. +/// +/// QOS 2 ExactlyOnce(2) is a bit more complicated as it provides the facility for guaranteed delivery of the message +/// exactly one time, no more, no less. +/// +/// Each of these have different message flow between the sender and receiver. +/// QOS 0 - AtMostOnce +/// Sender --> Publish --> Receiver +/// QOS 1 - AtLeastOnce +/// Sender --> Publish --> Receiver --> PublishAck --> Sender +/// | +/// v +/// Message Processor +/// QOS 2 - ExactlyOnce +/// Sender --> Publish --> Receiver --> PublishReceived --> Sender --> PublishRelease --> Reciever --> PublishComplete --> Sender +/// | v +/// Message Processor +class PublishingManager implements IPublishingManager { + /// Initializes a new instance of the PublishingManager class. + PublishingManager(this.connectionHandler, this._clientEventBus) { + connectionHandler!.registerForMessage( + MqttMessageType.publishAck, handlePublishAcknowledgement); + connectionHandler! + .registerForMessage(MqttMessageType.publish, handlePublish); + connectionHandler!.registerForMessage( + MqttMessageType.publishComplete, handlePublishComplete); + connectionHandler!.registerForMessage( + MqttMessageType.publishRelease, handlePublishRelease); + connectionHandler!.registerForMessage( + MqttMessageType.publishReceived, handlePublishReceived); + } + + /// Handles dispensing of message ids for messages published to a topic. + MessageIdentifierDispenser messageIdentifierDispenser = + MessageIdentifierDispenser(); + + /// Stores messages that have been pubished but not yet acknowledged. + Map publishedMessages = {}; + + /// Stores Qos 1 messages that have been received but not yet acknowledged as + /// manual acknowledgement has been selected. + Map awaitingManualAcknowledge = + {}; + + /// Stores messages that have been received from a broker with qos level 2 (Exactly Once). + Map receivedMessages = {}; + + /// Stores a cache of data converters used when publishing data to a broker. + Map dataConverters = {}; + + /// The current connection handler. + IMqttConnectionHandler? connectionHandler; + + final StreamController _published = + StreamController.broadcast(); + + /// The stream on which all confirmed published messages are added to + StreamController get published => _published; + + /// Indicates that received QOS 1 messages(AtLeastOnce) are not to be automatically acknowledged by + /// the client. The user must do this when the message has been taken off the update stream + /// using the [acknowledgeQos1Message] method. + bool manuallyAcknowledgeQos1 = false; + + /// Raised when a message has been received by the client and the relevant QOS handshake is complete. + @override + MessageReceived? publishEvent; + + /// The event bus + final events.EventBus? _clientEventBus; + + /// Publish a message to the broker on the specified topic. + /// The topic to send the message to + /// The QOS to use when publishing the message. + /// The message to send. + /// The message identifier assigned to the message. + @override + int publish( + PublicationTopic topic, MqttQos qualityOfService, typed.Uint8Buffer data, + [bool retain = false]) { + MqttLogger.log( + 'PublishingManager::publish - entered with topic ${topic.rawTopic}'); + final msgId = messageIdentifierDispenser.getNextMessageIdentifier(); + final msg = MqttPublishMessage() + .toTopic(topic.toString()) + .withMessageIdentifier(msgId) + .withQos(qualityOfService) + .publishData(data); + // Retain + msg.setRetain(state: retain); + // QOS level 1 or 2 messages need to be saved so we can do the ack processes + if (qualityOfService == MqttQos.atLeastOnce || + qualityOfService == MqttQos.exactlyOnce) { + publishedMessages[msgId] = msg; + } + connectionHandler!.sendMessage(msg); + return msgId; + } + + /// Handles the receipt of publish acknowledgement messages. + bool handlePublishAcknowledgement(MqttMessage? msg) { + final ackMsg = msg as MqttPublishAckMessage; + // If we're expecting an ack for the message, remove it from the list of pubs awaiting ack. + final messageIdentifier = ackMsg.variableHeader.messageIdentifier; + MqttLogger.log( + 'PublishingManager::handlePublishAcknowledgement for message id $messageIdentifier'); + if (publishedMessages.keys.contains(messageIdentifier)) { + _notifyPublish(publishedMessages[messageIdentifier!]); + publishedMessages.remove(messageIdentifier); + } + return true; + } + + /// Manually acknowledge a received QOS 1 message. + /// Has no effect if [manuallyAcknowledgeQos1] is not in force + /// or the message is not awaiting a QOS 1 acknowledge. + /// Returns true if an acknowledgement is sent to the broker. + bool acknowledgeQos1Message(MqttPublishMessage message) { + final messageIdentifier = message.variableHeader!.messageIdentifier; + if (awaitingManualAcknowledge.keys.contains(messageIdentifier) && + manuallyAcknowledgeQos1) { + final ackMsg = + MqttPublishAckMessage().withMessageIdentifier(messageIdentifier); + connectionHandler!.sendMessage(ackMsg); + awaitingManualAcknowledge.remove(messageIdentifier); + return true; + } + return false; + } + + /// Handles the receipt of publish messages from a message broker. + bool handlePublish(MqttMessage? msg) { + final pubMsg = msg as MqttPublishMessage; + var publishSuccess = true; + try { + final topic = PublicationTopic(pubMsg.variableHeader!.topicName); + MqttLogger.log( + 'PublishingManager::handlePublish - publish received from broker with topic $topic'); + if (pubMsg.header!.qos == MqttQos.atMostOnce) { + // QOS AtMostOnce 0 require no response. + // Send the message for processing to whoever is waiting. + _clientEventBus!.fire(MessageReceived(topic, msg)); + _notifyPublish(msg); + } else if (pubMsg.header!.qos == MqttQos.atLeastOnce) { + // QOS AtLeastOnce 1 requires an acknowledgement + // Send the message for processing to whoever is waiting. + _clientEventBus!.fire(MessageReceived(topic, msg)); + _notifyPublish(msg); + // If configured the client will send the acknowledgement, else the user must. + final messageIdentifier = pubMsg.variableHeader!.messageIdentifier; + if (!manuallyAcknowledgeQos1) { + final ackMsg = + MqttPublishAckMessage().withMessageIdentifier(messageIdentifier); + connectionHandler!.sendMessage(ackMsg); + } else { + // Add to the awaiting manual acknowledge list + awaitingManualAcknowledge[messageIdentifier!] = pubMsg; + } + } else if (pubMsg.header!.qos == MqttQos.exactlyOnce) { + // QOS ExactlyOnce means we can't give it away yet, we need to do a handshake + // to make sure the broker knows we got it, and we know he knows we got it. + // If we've already got it thats ok, it just means its being republished because + // of a handshake breakdown, overwrite our existing one for the sake of it + if (!receivedMessages + .containsKey(pubMsg.variableHeader!.messageIdentifier)) { + receivedMessages[pubMsg.variableHeader!.messageIdentifier] = pubMsg; + } + final pubRecv = MqttPublishReceivedMessage() + .withMessageIdentifier(pubMsg.variableHeader!.messageIdentifier); + connectionHandler!.sendMessage(pubRecv); + } + } on Exception { + publishSuccess = false; + } + return publishSuccess; + } + + /// Handles the publish release, for messages that are undergoing Qos ExactlyOnce processing. + bool handlePublishRelease(MqttMessage? msg) { + final pubRelMsg = msg as MqttPublishReleaseMessage; + final messageIdentifier = pubRelMsg.variableHeader.messageIdentifier; + MqttLogger.log( + 'PublishingManager::handlePublishRelease - for message identifier $messageIdentifier'); + var publishSuccess = true; + try { + final pubMsg = receivedMessages.remove(messageIdentifier); + if (pubMsg != null) { + // Send the message for processing to whoever is waiting. + final topic = PublicationTopic(pubMsg.variableHeader!.topicName); + _clientEventBus!.fire(MessageReceived(topic, pubMsg)); + final compMsg = MqttPublishCompleteMessage() + .withMessageIdentifier(pubMsg.variableHeader!.messageIdentifier); + connectionHandler!.sendMessage(compMsg); + } + } on Exception { + publishSuccess = false; + } + return publishSuccess; + } + + /// Handles a publish complete message received from a broker. + /// Returns true if the message flow completed successfully, otherwise false. + bool handlePublishComplete(MqttMessage? msg) { + final compMsg = msg as MqttPublishCompleteMessage; + final messageIdentifier = compMsg.variableHeader.messageIdentifier; + MqttLogger.log( + 'PublishingManager::handlePublishComplete - for message identifier $messageIdentifier'); + final publishMessage = publishedMessages.remove(messageIdentifier); + _notifyPublish(publishMessage); + return true; + } + + /// Handles publish received messages during processing of QOS level 2 (Exactly once) messages. + /// Returns true or false, depending on the success of message processing. + bool handlePublishReceived(MqttMessage? msg) { + final recvMsg = msg as MqttPublishReceivedMessage; + final messageIdentifier = recvMsg.variableHeader.messageIdentifier; + MqttLogger.log( + 'PublishingManager::handlePublishReceived - for message identifier $messageIdentifier'); + // If we've got a matching message, respond with a "ok release it for processing" + if (publishedMessages.containsKey(messageIdentifier)) { + final relMsg = MqttPublishReleaseMessage() + .withMessageIdentifier(recvMsg.variableHeader.messageIdentifier); + connectionHandler!.sendMessage(relMsg); + } + return true; + } + + /// On publish complete add the message to the published stream if needed + void _notifyPublish(MqttPublishMessage? message) { + if (_published.hasListener && message != null) { + MqttLogger.log( + 'PublishingManager::_notifyPublish - adding message to published stream for topic ${message.variableHeader!.topicName}'); + _published.add(message); + } + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_subscription.dart b/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_subscription.dart new file mode 100755 index 0000000..cedd204 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_subscription.dart @@ -0,0 +1,24 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 31/05/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Entity that captures data related to an individual subscription +class Subscription extends Object + with observe.Observable { + /// The message identifier assigned to the subscription + int? messageIdentifier; + + /// The time the subscription was created. + DateTime? createdTime; + + /// The Topic that is subscribed to. + late SubscriptionTopic topic; + + /// The QOS level of the topics subscription + MqttQos? qos; +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_subscription_status.dart b/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_subscription_status.dart new file mode 100755 index 0000000..1936bc2 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_subscription_status.dart @@ -0,0 +1,20 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 31/05/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Describes the status of a subscription +enum MqttSubscriptionStatus { + /// The subscription does not exist / is not known + doesNotExist, + + /// The subscription is currently pending acknowledgement by a broker. + pending, + + /// The subscription is currently active and messages will be received. + active +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_subscription_topic.dart b/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_subscription_topic.dart new file mode 100755 index 0000000..eb5960c --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_subscription_topic.dart @@ -0,0 +1,111 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 31/05/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Implementation of a Subscription topic that performs additional validations +/// of topics that are subscribed to. +class SubscriptionTopic extends Topic { + /// Creates a new instance of a rawTopic from a topic string. + SubscriptionTopic(String rawTopic) + : super(rawTopic, [ + Topic.validateMinLength, + Topic.validateMaxLength, + _validateMultiWildcard, + _validateFragments + ]); + + /// Validates all unique fragments in the topic match the + /// MQTT spec requirements. + static void _validateFragments(Topic topicInstance) { + // If any fragment contains a wildcard or a multi wildcard + // but is greater than 1 character long, then it's an error - + // wildcards must appear by themselves. + final invalidFragment = topicInstance.topicFragments.any( + (String fragment) => + (fragment.contains(Topic.multiWildcard) || + fragment.contains(Topic.wildcard)) && + fragment.length > 1); + if (invalidFragment) { + throw Exception( + 'mqtt_client::SubscriptionTopic: rawTopic Fragment contains ' + 'a wildcard but is more than one character long'); + } + } + + /// Validates the placement of the multi-wildcard character + /// in subscription topics. + static void _validateMultiWildcard(Topic topicInstance) { + if (topicInstance.rawTopic.contains(Topic.multiWildcard) && + !topicInstance.rawTopic.endsWith(Topic.multiWildcard)) { + throw Exception('mqtt_client::SubscriptionTopic: The rawTopic wildcard # ' + 'can only be present at the end of a topic'); + } + if (topicInstance.rawTopic.length > 1 && + topicInstance.rawTopic.endsWith(Topic.multiWildcard) && + !topicInstance.rawTopic.endsWith(Topic.multiWildcardValidEnd)) { + throw Exception( + 'mqtt_client::SubscriptionTopic: Topics using the # wildcard ' + 'longer than 1 character must ' + 'be immediately preceeded by a the rawTopic separator /'); + } + } + + /// Checks if the rawTopic matches the supplied rawTopic using + /// the MQTT rawTopic matching rules. + /// Returns true if the rawTopic matches based on the MQTT rawTopic + /// matching rules, otherwise false. + bool matches(PublicationTopic matcheeTopic) { + // If the left rawTopic is just a multi wildcard then we + // have a match without + // needing to check any further. + if (rawTopic == Topic.multiWildcard) { + return true; + } + // If the topics are an exact match, bail early with a cheap comparison + if (rawTopic == matcheeTopic.rawTopic) { + return true; + } + // no match yet so we need to check each fragment + for (var i = 0; i < topicFragments.length; i++) { + final lhsFragment = topicFragments[i]; + // If we've reached a multi wildcard in the lhs rawTopic, + // we have a match. + // (this is the mqtt spec rule finance matches finance or finance/#) + if (lhsFragment == Topic.multiWildcard) { + return true; + } + final isLhsWildcard = lhsFragment == Topic.wildcard; + // If we've reached a wildcard match but the matchee does + // not have anything at this fragment level then it's not a match. + // (this is the MQTT spec rule 'finance does not match finance/+' + if (isLhsWildcard && matcheeTopic.topicFragments.length <= i) { + return false; + } + // if lhs is not a wildcard we need to check whether the + // two fragments match each other. + if (!isLhsWildcard) { + final rhsFragment = matcheeTopic.topicFragments[i]; + // If the hs fragment is not wildcard then we need an exact match + if (lhsFragment != rhsFragment) { + return false; + } + } + // If we're at the last fragment of the lhs rawTopic but there are + // more fragments in the in the matchee then the matchee rawTopic + // is too specific to be a match. + if (i + 1 == topicFragments.length && + matcheeTopic.topicFragments.length > topicFragments.length) { + return false; + } + // If we're here the current fragment matches so check the next + } + // If we exit out of the loop without a return then we have a full match rawTopic/rawTopic which would + // have been caught by the original exact match check at the top anyway. + return true; + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_subscriptions_manager.dart b/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_subscriptions_manager.dart new file mode 100755 index 0000000..451cf22 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_subscriptions_manager.dart @@ -0,0 +1,236 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 30/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Subscribed and Unsubscribed callback typedefs +typedef SubscribeCallback = void Function(String topic); +typedef SubscribeFailCallback = void Function(String topic); +typedef UnsubscribeCallback = void Function(String? topic); + +/// A class that can manage the topic subscription process. +class SubscriptionsManager { + /// Creates a new instance of a SubscriptionsManager that uses the + /// specified connection to manage subscriptions. + SubscriptionsManager( + this.connectionHandler, this.publishingManager, this._clientEventBus) { + connectionHandler! + .registerForMessage(MqttMessageType.subscribeAck, confirmSubscription); + connectionHandler! + .registerForMessage(MqttMessageType.unsubscribeAck, confirmUnsubscribe); + // Start listening for published messages and re subscribe events. + _clientEventBus!.on().listen(publishMessageReceived); + _clientEventBus!.on().listen(_resubscribe); + } + + /// Dispenser used for keeping track of subscription ids + MessageIdentifierDispenser messageIdentifierDispenser = + MessageIdentifierDispenser(); + + /// List of confirmed subscriptions, keyed on the topic name. + Map subscriptions = {}; + + /// A list of subscriptions that are pending acknowledgement, keyed + /// on the message identifier. + Map pendingSubscriptions = {}; + + /// A list of unsubscribe requests waiting for an unsubscribe ack message. + /// Index is the message identifier of the unsubscribe message + Map pendingUnsubscriptions = {}; + + /// The connection handler that we use to subscribe to subscription + /// acknowledgements. + IMqttConnectionHandler? connectionHandler; + + /// Publishing manager used for passing on published messages to subscribers. + PublishingManager? publishingManager; + + /// Subscribe and Unsubscribe callbacks + SubscribeCallback? onSubscribed; + + /// Unsubscribed + UnsubscribeCallback? onUnsubscribed; + + /// Subscription failed callback + SubscribeFailCallback? onSubscribeFail; + + /// Re subscribe on auto reconnect. + bool resubscribeOnAutoReconnect = true; + + /// The event bus + final events.EventBus? _clientEventBus; + + /// Stream for all subscribed topics + final _subscriptionNotifier = + StreamController>>.broadcast( + sync: true); + + /// Subscription notifier + Stream>> get subscriptionNotifier => + _subscriptionNotifier.stream; + + /// Registers a new subscription with the subscription manager. + Subscription? registerSubscription(String topic, MqttQos qos) { + var cn = tryGetExistingSubscription(topic); + return cn ??= createNewSubscription(topic, qos); + } + + /// Gets a view on the existing observable, if the subscription + /// already exists. + Subscription? tryGetExistingSubscription(String topic) { + final retSub = subscriptions[topic]; + if (retSub == null) { + // Search the pending subscriptions + for (final sub in pendingSubscriptions.values) { + if (sub.topic.rawTopic == topic) { + return sub; + } + } + } + return retSub; + } + + /// Creates a new subscription for the specified topic. + /// If the subscription cannot be created null is returned. + Subscription? createNewSubscription(String topic, MqttQos? qos) { + try { + final subscriptionTopic = SubscriptionTopic(topic); + // Get an ID that represents the subscription. We will use this + // same ID for unsubscribe as well. + final msgId = messageIdentifierDispenser.getNextMessageIdentifier(); + final sub = Subscription(); + sub.topic = subscriptionTopic; + sub.qos = qos; + sub.messageIdentifier = msgId; + sub.createdTime = DateTime.now(); + pendingSubscriptions[sub.messageIdentifier] = sub; + // Build a subscribe message for the caller and send it off to the broker. + final msg = MqttSubscribeMessage() + .withMessageIdentifier(sub.messageIdentifier) + .toTopic(sub.topic.rawTopic) + .atQos(sub.qos); + connectionHandler!.sendMessage(msg); + return sub; + } on Exception catch (e) { + MqttLogger.log('Subscriptionsmanager::createNewSubscription ' + 'exception raised, text is $e'); + if (onSubscribeFail != null) { + onSubscribeFail!(topic); + } + return null; + } + } + + /// Publish message received + void publishMessageReceived(MessageReceived event) { + final topic = event.topic; + final msg = MqttReceivedMessage(topic.rawTopic, event.message); + _subscriptionNotifier.add([msg]); + } + + /// Unsubscribe from a topic + void unsubscribe(String topic) { + final unsubscribeMsg = MqttUnsubscribeMessage() + .withMessageIdentifier( + messageIdentifierDispenser.getNextMessageIdentifier()) + .fromTopic(topic); + connectionHandler!.sendMessage(unsubscribeMsg); + pendingUnsubscriptions[unsubscribeMsg.variableHeader!.messageIdentifier] = + topic; + } + + /// Re subscribe. + /// Unsubscribes all confirmed subscriptions and re subscribes them + /// without sending unsubscribe messages to the broker. + void resubscribe() { + for (final subscription in subscriptions.values) { + createNewSubscription(subscription!.topic.rawTopic, subscription.qos); + } + subscriptions.clear(); + } + + /// Confirms a subscription has been made with the broker. + /// Marks the sub as confirmed in the subs storage. + /// Returns true on successful subscription, false on fail. + bool confirmSubscription(MqttMessage? msg) { + final subAck = msg as MqttSubscribeAckMessage; + String topic; + if (pendingSubscriptions + .containsKey(subAck.variableHeader!.messageIdentifier)) { + topic = pendingSubscriptions[subAck.variableHeader!.messageIdentifier]! + .topic + .rawTopic; + subscriptions[topic] = + pendingSubscriptions[subAck.variableHeader!.messageIdentifier]; + pendingSubscriptions.remove(subAck.variableHeader!.messageIdentifier); + } else { + return false; + } + + // Check the Qos, we can get a failure indication(value 0x80) here if the + // topic cannot be subscribed to. + if (subAck.payload.qosGrants.isEmpty || + subAck.payload.qosGrants[0] == MqttQos.failure) { + subscriptions.remove(topic); + if (onSubscribeFail != null) { + onSubscribeFail!(topic); + return false; + } + } + // Success, call the subscribed callback + if (onSubscribed != null) { + onSubscribed!(topic); + } + return true; + } + + /// Cleans up after an unsubscribe message is received from the broker. + /// returns true, always + bool confirmUnsubscribe(MqttMessage? msg) { + final unSubAck = msg as MqttUnsubscribeAckMessage; + final topic = + pendingUnsubscriptions[unSubAck.variableHeader.messageIdentifier]; + subscriptions.remove(topic); + pendingUnsubscriptions.remove(unSubAck.variableHeader.messageIdentifier); + if (onUnsubscribed != null) { + onUnsubscribed!(topic); + } + return true; + } + + /// Gets the current status of a subscription. + MqttSubscriptionStatus getSubscriptionsStatus(String topic) { + var status = MqttSubscriptionStatus.doesNotExist; + if (subscriptions.containsKey(topic)) { + status = MqttSubscriptionStatus.active; + } + pendingSubscriptions.forEach((int? key, Subscription value) { + if (value.topic.rawTopic == topic) { + status = MqttSubscriptionStatus.pending; + } + }); + return status; + } + + // Re subscribe. + // Takes all active completed subscriptions and re subscribes them if + // [resubscribeOnAutoReconnect] is true. + // Automatically fired after auto reconnect has completed. + void _resubscribe(Resubscribe resubscribeEvent) { + if (resubscribeOnAutoReconnect) { + MqttLogger.log( + 'Subscriptionsmanager::_resubscribe - resubscribing from auto reconnect ${resubscribeEvent.fromAutoReconnect}'); + for (final subscription in subscriptions.values) { + createNewSubscription(subscription!.topic.rawTopic, subscription.qos); + } + subscriptions.clear(); + } else { + MqttLogger.log('Subscriptionsmanager::_resubscribe - ' + 'NOT resubscribing from auto reconnect ${resubscribeEvent.fromAutoReconnect}, resubscribeOnAutoReconnect is false'); + } + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_topic.dart b/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_topic.dart new file mode 100755 index 0000000..c774448 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/mqtt_client_topic.dart @@ -0,0 +1,84 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 31/05/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Provides the base implementation of an MQTT topic. +abstract class Topic { + /// Creates a new instance of a rawTopic from a rawTopic string. + /// rawTopic - The topic to represent. + /// validations - The validations to run on the rawTopic. + Topic(this.rawTopic, List validations) { + topicFragments = rawTopic.split(topicSeparator[0]); + // run all validations + for (final dynamic validation in validations) { + validation(this); + } + } + + /// Separator + static const String topicSeparator = '/'; + + /// Multi wildcard + static const String multiWildcard = '#'; + + /// Multi wildcard end + static const String multiWildcardValidEnd = topicSeparator + multiWildcard; + + /// Wildcard + static const String wildcard = '+'; + + /// Topic length + static const int maxTopicLength = 65535; + + /// Raw topic + String rawTopic; + + /// Topic fragments + late List topicFragments; + + /// Validates that the topic does not exceed the maximum length. + /// topicInstance - The instance to check. + static void validateMaxLength(Topic topicInstance) { + if (topicInstance.rawTopic.length > maxTopicLength) { + throw Exception('mqtt_client::Topic: The length of the supplied rawTopic ' + '(${topicInstance.rawTopic.length}) is longer than the ' + 'maximum allowable ($maxTopicLength)'); + } + } + + /// Returns true if there are any wildcards in the specified + /// rawTopic, otherwise false. + bool get hasWildcards => + rawTopic.contains(multiWildcard) || rawTopic.contains(wildcard); + + /// Validates that the topic does not fall below the minimum length. + /// topicInstance - The instance to check. + static void validateMinLength(Topic topicInstance) { + if (topicInstance.rawTopic.isEmpty) { + throw Exception( + 'mqtt_client::Topic: rawTopic must contain at least one character'); + } + } + + /// Serves as a hash function for a topics. + @override + int get hashCode => rawTopic.hashCode; + + /// Checks if one topic equals another topic exactly. + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + return other is Topic && rawTopic == other.rawTopic; + } + + /// Returns a String representation of the topic. + @override + String toString() => rawTopic; +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/mqtt_server_client.dart b/bytedesk_kefu/lib/mqtt/lib/src/mqtt_server_client.dart new file mode 100755 index 0000000..385161d --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/mqtt_server_client.dart @@ -0,0 +1,85 @@ +/* + * Package : mqtt_server_client + * Author : S. Hamblett + * Date : 21/01/2020 + * Copyright : S.Hamblett + */ + +part of mqtt_server_client; + +class MqttServerClient extends MqttClient { + /// Initializes a new instance of the MqttServerClient class using the + /// default Mqtt Port. + /// The server hostname or URL to connect to + /// The client identifier to use to connect with + MqttServerClient( + String server, + String clientIdentifier, { + this.maxConnectionAttempts = 3, + }) : super(server, clientIdentifier); + + /// Initializes a new instance of the MqttServerClient class using + /// the supplied Mqtt Port. + /// The server hostname to connect to + /// The client identifier to use to connect with + /// The port to use + MqttServerClient.withPort( + String server, + String clientIdentifier, + int port, { + this.maxConnectionAttempts = 3, + }) : super.withPort(server, clientIdentifier, port); + + /// The security context for secure usage + SecurityContext securityContext = SecurityContext.defaultContext; + + /// Callback function to handle bad certificate. if true, ignore the error. + bool Function(X509Certificate certificate)? onBadCertificate; + + /// If set use a websocket connection, otherwise use the default TCP one + bool useWebSocket = false; + + /// If set use the alternate websocket implementation + bool useAlternateWebSocketImplementation = false; + + /// If set use a secure connection, note TCP only, do not use for + /// secure websockets(wss). + bool secure = false; + + /// Max connection attempts + final int maxConnectionAttempts; + + /// Performs a connect to the message broker with an optional + /// username and password for the purposes of authentication. + /// If a username and password are supplied these will override + /// any previously set in a supplied connection message so if you + /// supply your own connection message and use the authenticateAs method to + /// set these parameters do not set them again here. + @override + Future connect( + [String? username, String? password]) async { + instantiationCorrect = true; + clientEventBus = events.EventBus(); + connectionHandler = SynchronousMqttServerConnectionHandler( + clientEventBus, + maxConnectionAttempts: maxConnectionAttempts, + ); + if (useWebSocket) { + connectionHandler.secure = false; + connectionHandler.useWebSocket = true; + connectionHandler.useAlternateWebSocketImplementation = + useAlternateWebSocketImplementation; + if (websocketProtocolString != null) { + connectionHandler.websocketProtocols = websocketProtocolString; + } + } + if (secure) { + connectionHandler.secure = true; + connectionHandler.useWebSocket = false; + connectionHandler.useAlternateWebSocketImplementation = false; + connectionHandler.securityContext = securityContext; + connectionHandler.onBadCertificate = onBadCertificate; + } + return await super.connect(username, password); + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/observable/observable.dart b/bytedesk_kefu/lib/mqtt/lib/src/observable/observable.dart new file mode 100755 index 0000000..590c027 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/observable/observable.dart @@ -0,0 +1,9 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +library observable; + +export 'src/change_notifier.dart' show ChangeNotifier; +export 'src/observable.dart'; +export 'src/records.dart' show ChangeRecord; diff --git a/bytedesk_kefu/lib/mqtt/lib/src/observable/src/change_notifier.dart b/bytedesk_kefu/lib/mqtt/lib/src/observable/src/change_notifier.dart new file mode 100755 index 0000000..0a5c53c --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/observable/src/change_notifier.dart @@ -0,0 +1,84 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; + +import 'observable.dart'; +import 'records.dart'; + +/// Supplies [changes] and various hooks to implement [Observable]. +/// +/// May use [notifyChange] to queue a change record; they are asynchronously +/// delivered at the end of the VM turn. +/// +/// [ChangeNotifier] may be extended, mixed in, or used as a delegate. +class ChangeNotifier implements Observable { + late StreamController> _changes; + + bool _scheduled = false; + List? _queue; + + /// Emits a list of changes when the state of the object changes. + /// + /// Changes should produced in order, if significant. + @override + Stream> get changes => + (_changes = StreamController>.broadcast( + sync: true, + onListen: observed, + onCancel: unobserved, + )) + .stream; + + /// May override to be notified when [changes] is first observed. + @override + @mustCallSuper + void observed() {} + + /// May override to be notified when [changes] is no longer observed. + @override + @mustCallSuper + void unobserved() { + _changes.close(); + } + + /// If [hasObservers], synchronously emits [changes] that have been queued. + /// + /// Returns `true` if changes were emitted. + @override + @mustCallSuper + bool deliverChanges() { + List changes; + if (_scheduled && hasObservers) { + changes = ChangeRecord.any as List; + _scheduled = false; + _changes.add(changes); + return true; + } + return false; + } + + /// Whether [changes] has at least one active listener. + /// + /// May be used to optimize whether to produce change records. + @override + bool get hasObservers => _changes.hasListener == true; + + /// Schedules [change] to be delivered. + /// + /// If [change] is omitted then [ChangeRecord.any] will be sent. + /// + /// If there are no listeners to [changes], this method does nothing. + @override + void notifyChange([C? change]) { + if (!hasObservers) { + return; + } + if (change != null) { + (_queue ??= []).add(change); + } + if (!_scheduled) { + scheduleMicrotask(deliverChanges); + _scheduled = true; + } + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/observable/src/observable.dart b/bytedesk_kefu/lib/mqtt/lib/src/observable/src/observable.dart new file mode 100755 index 0000000..5509993 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/observable/src/observable.dart @@ -0,0 +1,61 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +library observable.src.observable; + +import 'dart:async'; + +import 'package:meta/meta.dart'; + +import 'change_notifier.dart'; +import 'records.dart'; + +/// Represents an object with observable state or properties. +/// +/// The interface does not require any specific technique to implement +/// observability. You may implement it in the following ways: +/// - Extend or mixin [ChangeNotifier] +/// - Implement the interface yourself and provide your own implementation +abstract class Observable { + // To be removed when https://github.com/dart-lang/observable/issues/10 + final ChangeNotifier _delegate = ChangeNotifier(); + + /// Emits a list of changes when the state of the object changes. + /// + /// Changes should produced in order, if significant. + Stream?> get changes => _delegate.changes; + + /// May override to be notified when [changes] is first observed. + @protected + @mustCallSuper + @Deprecated('Use ChangeNotifier instead to have this method available') + // REMOVE IGNORE when https://github.com/dart-lang/observable/issues/10 + void observed() => _delegate.observed(); + + /// May override to be notified when [changes] is no longer observed. + @protected + @mustCallSuper + @Deprecated('Use ChangeNotifier instead to have this method available') + // REMOVE IGNORE when https://github.com/dart-lang/observable/issues/10 + void unobserved() => _delegate.unobserved(); + + /// True if this object has any observers. + @Deprecated('Use ChangeNotifier instead to have this method available') + bool get hasObservers => _delegate.hasObservers; + + /// If [hasObservers], synchronously emits [changes] that have been queued. + /// + /// Returns `true` if changes were emitted. + @Deprecated('Use ChangeNotifier instead to have this method available') + // REMOVE IGNORE when https://github.com/dart-lang/observable/issues/10 + bool deliverChanges() => _delegate.deliverChanges(); + + /// Schedules [change] to be delivered. + /// + /// If [change] is omitted then [ChangeRecord.any] will be sent. + /// + /// If there are no listeners to [changes], this method does nothing. + @Deprecated('Use ChangeNotifier instead to have this method available') + void notifyChange([C? change]) => _delegate.notifyChange(change); +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/observable/src/records.dart b/bytedesk_kefu/lib/mqtt/lib/src/observable/src/records.dart new file mode 100755 index 0000000..62cff74 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/observable/src/records.dart @@ -0,0 +1,20 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +library observable.src.records; + +/// Result of a change to an observed object. +class ChangeRecord { + /// Constructor + const ChangeRecord(); + + /// Signifies a change occurred, but without details of the specific change. + /// + /// May be used to produce lower-GC-pressure records where more verbose change + /// records will not be used directly. + static const List any = [ChangeRecord()]; + + /// Signifies no changes occurred. + static const List none = []; +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/utility/mqtt_client_byte_buffer.dart b/bytedesk_kefu/lib/mqtt/lib/src/utility/mqtt_client_byte_buffer.dart new file mode 100755 index 0000000..51fbd2c --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/utility/mqtt_client_byte_buffer.dart @@ -0,0 +1,177 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 31/05/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Utility class to allow stream like access to a sized byte buffer. +/// This class is in effect a cut-down implementation of the C# NET +/// System.IO class with Mqtt client specific extensions. +class MqttByteBuffer { + /// The byte buffer + MqttByteBuffer(this.buffer); + + /// From a list + MqttByteBuffer.fromList(List data) { + buffer = typed.Uint8Buffer(); + buffer!.addAll(data); + } + + /// The current position within the buffer. + int _position = 0; + + /// The underlying byte buffer + typed.Uint8Buffer? buffer; + + /// Position + int get position => _position; + + /// Length + int get length => buffer!.length; + + /// Available bytes + int get availableBytes => length - _position; + + /// Resets the position to 0 + void reset() { + _position = 0; + } + + /// Skip bytes + set skipBytes(int bytes) => _position += bytes; + + /// Add a list + void addAll(List data) { + buffer!.addAll(data); + } + + /// Shrink the buffer + void shrink() { + buffer!.removeRange(0, _position); + _position = 0; + } + + /// Message available + bool isMessageAvailable() { + if (availableBytes > 0) { + return true; + } + + return false; + } + + /// Reads a byte from the buffer and advances the position + /// within the buffer by one byte, or returns -1 if at the end of the buffer. + int readByte() { + final tmp = buffer![_position]; + if (_position <= (length - 1)) { + _position++; + } else { + return -1; + } + return tmp; + } + + /// Read a short int(16 bits) + int readShort() { + final high = readByte(); + final low = readByte(); + return (high << 8) + low; + } + + /// Reads a sequence of bytes from the current + /// buffer and advances the position within the buffer + /// by the number of bytes read. + typed.Uint8Buffer read(int count) { + if ((length < count) || (_position + count) > length) { + throw Exception('mqtt_client::ByteBuffer: The buffer did not have ' + 'enough bytes for the read operation ' + 'length $length, count $count, position $_position, buffer $buffer'); + } + final tmp = typed.Uint8Buffer(); + tmp.addAll(buffer!.getRange(_position, _position + count)); + _position += count; + final tmp2 = typed.Uint8Buffer(); + tmp2.addAll(tmp); + return tmp2; + } + + /// Writes a byte to the current position in the buffer + /// and advances the position within the buffer by one byte. + void writeByte(int byte) { + if (buffer!.length == _position) { + buffer!.add(byte); + } else { + buffer![_position] = byte; + } + _position++; + } + + /// Write a short(16 bit) + void writeShort(int short) { + writeByte(short >> 8); + writeByte(short & 0xFF); + } + + /// Writes a sequence of bytes to the current + /// buffer and advances the position within the buffer by the number of + /// bytes written. + void write(typed.Uint8Buffer? buffer) { + if (this.buffer == null) { + this.buffer = buffer; + } else { + this.buffer!.addAll(buffer!); + } + _position = length; + } + + /// Seek. Sets the position in the buffer. If overflow occurs + /// the position is set to the end of the buffer. + void seek(int seek) { + if ((seek <= length) && (seek >= 0)) { + _position = seek; + } else { + _position = length; + } + } + + /// Writes an MQTT string member + void writeMqttStringM(String stringToWrite) { + writeMqttString(this, stringToWrite); + } + + /// Writes an MQTT string. + /// stringStream - The stream containing the string to write. + /// stringToWrite - The string to write. + static void writeMqttString( + MqttByteBuffer stringStream, String stringToWrite) { + final enc = MqttEncoding(); + final stringBytes = enc.getBytes(stringToWrite); + stringStream.write(stringBytes); + } + + /// Reads an MQTT string from the underlying stream member + String readMqttStringM() => MqttByteBuffer.readMqttString(this); + + /// Reads an MQTT string from the underlying stream. + static String readMqttString(MqttByteBuffer buffer) { + // Read and check the length + final lengthBytes = buffer.read(2); + final enc = MqttEncoding(); + final stringLength = enc.getCharCount(lengthBytes); + final stringBuff = buffer.read(stringLength); + return enc.getString(stringBuff); + } + + @override + String toString() { + if (buffer != null && buffer!.isNotEmpty) { + return buffer!.toList().toString(); + } else { + return 'null or empty'; + } + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/utility/mqtt_client_logger.dart b/bytedesk_kefu/lib/mqtt/lib/src/utility/mqtt_client_logger.dart new file mode 100755 index 0000000..61f0d2f --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/utility/mqtt_client_logger.dart @@ -0,0 +1,44 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 28/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Library wide logging class +class MqttLogger { + /// Log or not + static bool loggingOn = false; + + /// Unique per client identifier + static int clientId = 0; + + /// Test output + static String testOutput = ''; + + /// Test mode + static bool testMode = false; + + /// Log method + /// If the optimise parameter is supplied it must have a toString method, + /// this allows large objects such as lots of payload data not to be + /// converted into a string in the message parameter if logging is not enabled. + static void log(String message, [dynamic optimise = false]) { + if (loggingOn) { + final now = DateTime.now(); + var output = ''; + if (optimise is bool) { + output = '${clientId.toString()}-$now -- $message'; + print(output); + } else { + output = '${clientId.toString()}-$now -- $message$optimise'; + print(output); + } + if (testMode) { + testOutput = output; + } + } + } +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/utility/mqtt_client_payload_builder.dart b/bytedesk_kefu/lib/mqtt/lib/src/utility/mqtt_client_payload_builder.dart new file mode 100755 index 0000000..916e28e --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/utility/mqtt_client_payload_builder.dart @@ -0,0 +1,99 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 18/04/2018 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// Utility class to assist with the build in of message topic payloads. +class MqttClientPayloadBuilder { + /// Construction + MqttClientPayloadBuilder() { + _payload = typed.Uint8Buffer(); + } + + typed.Uint8Buffer? _payload; + + /// Payload + typed.Uint8Buffer? get payload => _payload; + + /// Length + int get length => _payload!.length; + + /// Add a buffer + void addBuffer(typed.Uint8Buffer buffer) { + _payload!.addAll(buffer); + } + + /// Add byte, this will overflow on values > 2**8-1 + void addByte(int val) { + _payload!.add(val); + } + + /// Add a bool, true is 1, false is 0 + void addBool({required bool val}) { + val ? addByte(1) : addByte(0); + } + + /// Add a halfword, 16 bits, this will overflow on values > 2**16-1 + void addHalf(int val) { + final tmp = Uint16List.fromList([val]); + _payload!.addAll(tmp.buffer.asInt8List()); + } + + /// Add a word, 32 bits, this will overflow on values > 2**32-1 + void addWord(int val) { + final tmp = Uint32List.fromList([val]); + _payload!.addAll(tmp.buffer.asInt8List()); + } + + /// Add a long word, 64 bits or a Dart int + void addInt(int val) { + final tmp = Uint64List.fromList([val]); + _payload!.addAll(tmp.buffer.asInt8List()); + } + + /// Add a standard Dart string + void addString(String val) { + addUTF16String(val); + } + + /// Add a UTF16 string, note Dart natively encodes strings as UTF16 + void addUTF16String(String val) { + for (final codeunit in val.codeUnits) { + if (codeunit <= 255 && codeunit >= 0) { + _payload!.add(codeunit); + } else { + addHalf(codeunit); + } + } + } + + /// Add a UTF8 string + void addUTF8String(String val) { + const encoder = Utf8Encoder(); + _payload!.addAll(encoder.convert(val)); + } + + /// Add a 32 bit double + void addHalfDouble(double val) { + final tmp = Float32List.fromList([val]); + _payload!.addAll(tmp.buffer.asInt8List()); + } + + /// Add a 64 bit double + void addDouble(double val) { + final tmp = Float64List.fromList([val]); + _payload!.addAll(tmp.buffer.asInt8List()); + } + + // added by jackning, 2019/12/03 + void addProtobuf(Uint8List val) { + _payload!.addAll(val); + } + + /// Clear the buffer + void clear() => _payload!.clear(); +} diff --git a/bytedesk_kefu/lib/mqtt/lib/src/utility/mqtt_client_utilities.dart b/bytedesk_kefu/lib/mqtt/lib/src/utility/mqtt_client_utilities.dart new file mode 100755 index 0000000..37caf7e --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/lib/src/utility/mqtt_client_utilities.dart @@ -0,0 +1,82 @@ +/* + * Package : mqtt_client + * Author : S. Hamblett + * Date : 28/06/2017 + * Copyright : S.Hamblett + */ + +part of mqtt_client; + +/// General library wide utilties +class MqttUtilities { + /// Sleep function that allows asynchronous activity to continue. + /// Time units are seconds + static Future asyncSleep(int seconds) => + Future.delayed(Duration(seconds: seconds)); + + /// Qos conversion, always use this to get a Qos + /// enumeration from a value + static MqttQos getQosLevel(int value) { + switch (value) { + case 0: + return MqttQos.atMostOnce; + case 1: + return MqttQos.atLeastOnce; + case 2: + return MqttQos.exactlyOnce; + case 0x80: + return MqttQos.failure; + default: + return MqttQos.reserved1; + } + } +} + +/// Cancellable asynchronous sleep support class +class MqttCancellableAsyncSleep { + /// Timeout value in milliseconds + MqttCancellableAsyncSleep(this._timeout); + + /// Millisecond timeout + final int _timeout; + + /// Timeout + int get timeout => _timeout; + + /// The completer + late Completer _completer; + + /// The timer + late Timer _timer; + + /// Timer running flag + bool _running = false; + + /// Running + bool get isRunning => _running; + + /// Start the timer + Future sleep() { + if (!_running) { + _completer = Completer(); + _timer = Timer(Duration(milliseconds: _timeout), _timerCallback); + _running = true; + } + return _completer.future; + } + + /// Cancel the timer + void cancel() { + if (_running) { + _timer.cancel(); + _running = false; + _completer.complete(); + } + } + + /// The timer callback + void _timerCallback() { + _running = false; + _completer.complete(); + } +} diff --git a/bytedesk_kefu/lib/mqtt/readme.md b/bytedesk_kefu/lib/mqtt/readme.md new file mode 100755 index 0000000..396a350 --- /dev/null +++ b/bytedesk_kefu/lib/mqtt/readme.md @@ -0,0 +1,18 @@ +# mqtt 说明 + +- 当前版本9.3.1, 更新日期2021/05/24 +- 在mqtt/lib/src/utility/mqtt_client_payload_builder.dart添加 + +```dart + // added by jackning, 2019/12/03 + void addProtobuf(Uint8List val) { + _payload!.addAll(val); + } +``` + +- 修改src/messages/connect/mqtt_client_mqtt_connect_payload.dart注释掉函数 set clientIdentifier限制clientId长度的代码 +- 修改lib/mqtt_browser_client.dart,替换 import 'dart:html'; 为 import 'package:universal_html/html.dart'; + +## TODO + +- 搞定继承MqttClientPayloadBuilder之后,将mqtt包改为package依赖 diff --git a/bytedesk_kefu/lib/protobuf/message.pb.dart b/bytedesk_kefu/lib/protobuf/message.pb.dart new file mode 100644 index 0000000..1192e3c --- /dev/null +++ b/bytedesk_kefu/lib/protobuf/message.pb.dart @@ -0,0 +1,2196 @@ +/// +// Generated code. Do not modify. +// source: message.proto +// +// @dart = 2.12 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +import 'user.pb.dart' as $0; +import 'thread.pb.dart' as $1; + +class Text extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'Text', + createEmptyInstance: create) + ..aOS( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'content') + ..hasRequiredFields = false; + + Text._() : super(); + factory Text({ + $core.String? content, + }) { + final _result = create(); + if (content != null) { + _result.content = content; + } + return _result; + } + factory Text.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory Text.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Text clone() => Text()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Text copyWith(void Function(Text) updates) => + super.copyWith((message) => updates(message as Text)) + as Text; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static Text create() => Text._(); + Text createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Text getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Text? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get content => $_getSZ(0); + @$pb.TagNumber(1) + set content($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasContent() => $_has(0); + @$pb.TagNumber(1) + void clearContent() => clearField(1); +} + +class Image extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'Image', + createEmptyInstance: create) + ..aOS( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'mediaId', + protoName: 'mediaId') + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'picUrl', + protoName: 'picUrl') + ..aOS( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'imageUrl', + protoName: 'imageUrl') + ..hasRequiredFields = false; + + Image._() : super(); + factory Image({ + $core.String? mediaId, + $core.String? picUrl, + $core.String? imageUrl, + }) { + final _result = create(); + if (mediaId != null) { + _result.mediaId = mediaId; + } + if (picUrl != null) { + _result.picUrl = picUrl; + } + if (imageUrl != null) { + _result.imageUrl = imageUrl; + } + return _result; + } + factory Image.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory Image.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Image clone() => Image()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Image copyWith(void Function(Image) updates) => + super.copyWith((message) => updates(message as Image)) + as Image; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static Image create() => Image._(); + Image createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Image getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Image? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get mediaId => $_getSZ(0); + @$pb.TagNumber(1) + set mediaId($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasMediaId() => $_has(0); + @$pb.TagNumber(1) + void clearMediaId() => clearField(1); + + @$pb.TagNumber(2) + $core.String get picUrl => $_getSZ(1); + @$pb.TagNumber(2) + set picUrl($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasPicUrl() => $_has(1); + @$pb.TagNumber(2) + void clearPicUrl() => clearField(2); + + @$pb.TagNumber(3) + $core.String get imageUrl => $_getSZ(2); + @$pb.TagNumber(3) + set imageUrl($core.String v) { + $_setString(2, v); + } + + @$pb.TagNumber(3) + $core.bool hasImageUrl() => $_has(2); + @$pb.TagNumber(3) + void clearImageUrl() => clearField(3); +} + +class File extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'File', + createEmptyInstance: create) + ..aOS( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'fileUrl', + protoName: 'fileUrl') + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'fileName', + protoName: 'fileName') + ..aOS( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'fileSize', + protoName: 'fileSize') + ..hasRequiredFields = false; + + File._() : super(); + factory File({ + $core.String? fileUrl, + $core.String? fileName, + $core.String? fileSize, + }) { + final _result = create(); + if (fileUrl != null) { + _result.fileUrl = fileUrl; + } + if (fileName != null) { + _result.fileName = fileName; + } + if (fileSize != null) { + _result.fileSize = fileSize; + } + return _result; + } + factory File.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory File.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + File clone() => File()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + File copyWith(void Function(File) updates) => + super.copyWith((message) => updates(message as File)) + as File; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static File create() => File._(); + File createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static File getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static File? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get fileUrl => $_getSZ(0); + @$pb.TagNumber(1) + set fileUrl($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasFileUrl() => $_has(0); + @$pb.TagNumber(1) + void clearFileUrl() => clearField(1); + + @$pb.TagNumber(2) + $core.String get fileName => $_getSZ(1); + @$pb.TagNumber(2) + set fileName($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasFileName() => $_has(1); + @$pb.TagNumber(2) + void clearFileName() => clearField(2); + + @$pb.TagNumber(3) + $core.String get fileSize => $_getSZ(2); + @$pb.TagNumber(3) + set fileSize($core.String v) { + $_setString(2, v); + } + + @$pb.TagNumber(3) + $core.bool hasFileSize() => $_has(2); + @$pb.TagNumber(3) + void clearFileSize() => clearField(3); +} + +class Voice extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'Voice', + createEmptyInstance: create) + ..aOS( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'mediaId', + protoName: 'mediaId') + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'format') + ..aOS( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'voiceUrl', + protoName: 'voiceUrl') + ..a<$core.int>( + 4, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'length', + $pb.PbFieldType.O3) + ..aOB( + 5, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'played') + ..hasRequiredFields = false; + + Voice._() : super(); + factory Voice({ + $core.String? mediaId, + $core.String? format, + $core.String? voiceUrl, + $core.int? length, + $core.bool? played, + }) { + final _result = create(); + if (mediaId != null) { + _result.mediaId = mediaId; + } + if (format != null) { + _result.format = format; + } + if (voiceUrl != null) { + _result.voiceUrl = voiceUrl; + } + if (length != null) { + _result.length = length; + } + if (played != null) { + _result.played = played; + } + return _result; + } + factory Voice.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory Voice.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Voice clone() => Voice()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Voice copyWith(void Function(Voice) updates) => + super.copyWith((message) => updates(message as Voice)) + as Voice; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static Voice create() => Voice._(); + Voice createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Voice getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Voice? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get mediaId => $_getSZ(0); + @$pb.TagNumber(1) + set mediaId($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasMediaId() => $_has(0); + @$pb.TagNumber(1) + void clearMediaId() => clearField(1); + + @$pb.TagNumber(2) + $core.String get format => $_getSZ(1); + @$pb.TagNumber(2) + set format($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasFormat() => $_has(1); + @$pb.TagNumber(2) + void clearFormat() => clearField(2); + + @$pb.TagNumber(3) + $core.String get voiceUrl => $_getSZ(2); + @$pb.TagNumber(3) + set voiceUrl($core.String v) { + $_setString(2, v); + } + + @$pb.TagNumber(3) + $core.bool hasVoiceUrl() => $_has(2); + @$pb.TagNumber(3) + void clearVoiceUrl() => clearField(3); + + @$pb.TagNumber(4) + $core.int get length => $_getIZ(3); + @$pb.TagNumber(4) + set length($core.int v) { + $_setSignedInt32(3, v); + } + + @$pb.TagNumber(4) + $core.bool hasLength() => $_has(3); + @$pb.TagNumber(4) + void clearLength() => clearField(4); + + @$pb.TagNumber(5) + $core.bool get played => $_getBF(4); + @$pb.TagNumber(5) + set played($core.bool v) { + $_setBool(4, v); + } + + @$pb.TagNumber(5) + $core.bool hasPlayed() => $_has(4); + @$pb.TagNumber(5) + void clearPlayed() => clearField(5); +} + +class Video extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'Video', + createEmptyInstance: create) + ..aOS( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'mediaId', + protoName: 'mediaId') + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'thumbMediaId', + protoName: 'thumbMediaId') + ..aOS( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'videoOrShortUrl', + protoName: 'videoOrShortUrl') + ..aOS( + 4, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'videoOrShortThumbUrl', + protoName: 'videoOrShortThumbUrl') + ..hasRequiredFields = false; + + Video._() : super(); + factory Video({ + $core.String? mediaId, + $core.String? thumbMediaId, + $core.String? videoOrShortUrl, + $core.String? videoOrShortThumbUrl, + }) { + final _result = create(); + if (mediaId != null) { + _result.mediaId = mediaId; + } + if (thumbMediaId != null) { + _result.thumbMediaId = thumbMediaId; + } + if (videoOrShortUrl != null) { + _result.videoOrShortUrl = videoOrShortUrl; + } + if (videoOrShortThumbUrl != null) { + _result.videoOrShortThumbUrl = videoOrShortThumbUrl; + } + return _result; + } + factory Video.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory Video.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Video clone() => Video()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Video copyWith(void Function(Video) updates) => + super.copyWith((message) => updates(message as Video)) + as Video; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static Video create() => Video._(); + Video createEmptyInstance() => create(); + static $pb.PbList