├── README.md ├── demo ├── html │ ├── index01.html │ ├── index02.html │ └── js │ │ ├── jquery-1.8.3.min.js │ │ └── websocket.js ├── img │ ├── 1.jpg │ ├── 2.jpg │ ├── 3.jpg │ ├── 4.jpg │ ├── 5.jpg │ ├── 6.jpg │ └── 7.jpg ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── demo │ │ │ ├── ChatMsgListener.java │ │ │ └── DemoApplication.java │ └── resources │ │ └── application.yml │ └── test │ └── java │ └── com │ └── example │ └── demo │ └── DemoApplicationTests.java ├── mvnw ├── mvnw.cmd ├── pom.xml └── src └── main ├── java └── io │ └── github │ └── yangyouwang │ └── springbootstarterim │ ├── NettyBooter.java │ ├── bean │ ├── ChatMsg.java │ ├── DataContent.java │ └── DataContentEvent.java │ ├── config │ ├── ChatConfig.java │ ├── MsgConfig.java │ ├── NettyConfig.java │ └── NettyProperties.java │ ├── constant │ ├── MsgActionEnum.java │ ├── MsgStatusEnum.java │ └── MsgTypeEnum.java │ ├── core │ ├── ChatHandler.java │ ├── HeartBeatHandler.java │ ├── UserChanelRel.java │ └── WSServerInitialzer.java │ ├── factory │ └── MsgContextFactory.java │ └── strategy │ ├── MsgStrategy.java │ └── impl │ ├── ChatStrategy.java │ ├── ConnectStrategy.java │ ├── KeepAliveStrategy.java │ └── SignedStrategy.java └── resources ├── META-INF └── spring.factories └── application.yml /README.md: -------------------------------------------------------------------------------- 1 | ## 即时通讯插件简介 2 | 很多聊天功能都是基于第三方聊天api,比如融x,而且第三方运营成本比较高。那为何不研发一款高并发聊天插件呢? 3 | 4 | 环境 springboot2.x、jdk8、maven 5 | 框架 netty、fastjson 6 | 7 | ## 使用说明 8 | 1.通讯插件源码下载到本地。命令行输入`maven install`。项目工程中引入插件依赖。 9 | ` 10 | io.github.yangyouwang 11 | springboot-starter-im 12 | 0.0.1-SNAPSHOT 13 | ` 14 | ![引入jar](https://raw.githubusercontent.com/YangYouWang/springboot-starter-im/master/demo/img/1.jpg "1.png") 15 | 16 | 2.resource中application.yml配置插件通讯端口。 17 | `im: 18 | netty: 19 | port: 8888 # 配置聊天端口` 20 | ![配置端口](https://raw.githubusercontent.com/YangYouWang/springboot-starter-im/master/demo/img/2.jpg "2.png") 21 | 22 | 3.启动类main方法加入启动插件代码 23 | `NettyBooter nettyBooter = SpringUtil.getBean(NettyBooter.class); 24 | nettyBooter.start();` 25 | ![配置端口](https://raw.githubusercontent.com/YangYouWang/springboot-starter-im/master/demo/img/3.jpg "3.png") 26 | 27 | 4.控制台打印出:启动 Netty 成功。默认访问路径`ws://localhost:8888/im/ws` 28 | ![启动成功](https://raw.githubusercontent.com/YangYouWang/springboot-starter-im/master/demo/img/4.jpg "4.png") 29 | ![启动成功](https://raw.githubusercontent.com/YangYouWang/springboot-starter-im/master/demo/img/5.jpg "5.png") 30 | 31 | ## 如何在程序中获取消息 32 | 33 | 代码加入事件监听(参数是DataContentEvent,而不是DataContent) 34 | `@Component 35 | public class ChatMsgListener { 36 | @EventListener 37 | public void getData(DataContentEvent dataContentEvent) { 38 | System.out.println("收到消息了" + dataContentEvent.getDataContent()); 39 | } 40 | }` 41 | 42 | ![加入事件](https://raw.githubusercontent.com/YangYouWang/springboot-starter-im/master/demo/img/6.jpg "6.png") 43 | ![控制台输出](https://raw.githubusercontent.com/YangYouWang/springboot-starter-im/master/demo/img/7.jpg "7.png") 44 | 45 | 开源不易,切勿白嫖。 46 | 47 | **革命尚未成功,同志仍须努力** -------------------------------------------------------------------------------- /demo/html/index01.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 15 | 16 | 17 | 18 | 23 | 24 | 25 | 28 | 29 |
发送内容: 13 | 14 |
接收内容: 19 | 22 |
26 | 27 |
30 |
31 | 32 | 33 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /demo/html/index02.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 15 | 16 | 17 | 18 | 23 | 24 | 25 | 28 | 29 |
发送内容: 13 | 14 |
接收内容: 19 | 22 |
26 | 27 |
30 |
31 | 32 | 33 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /demo/html/js/jquery-1.8.3.min.js: -------------------------------------------------------------------------------- 1 | /*! jQuery v1.8.3 jquery.com | jquery.org/license */ 2 | (function(e,t){function _(e){var t=M[e]={};return v.each(e.split(y),function(e,n){t[n]=!0}),t}function H(e,n,r){if(r===t&&e.nodeType===1){var i="data-"+n.replace(P,"-$1").toLowerCase();r=e.getAttribute(i);if(typeof r=="string"){try{r=r==="true"?!0:r==="false"?!1:r==="null"?null:+r+""===r?+r:D.test(r)?v.parseJSON(r):r}catch(s){}v.data(e,n,r)}else r=t}return r}function B(e){var t;for(t in e){if(t==="data"&&v.isEmptyObject(e[t]))continue;if(t!=="toJSON")return!1}return!0}function et(){return!1}function tt(){return!0}function ut(e){return!e||!e.parentNode||e.parentNode.nodeType===11}function at(e,t){do e=e[t];while(e&&e.nodeType!==1);return e}function ft(e,t,n){t=t||0;if(v.isFunction(t))return v.grep(e,function(e,r){var i=!!t.call(e,r,e);return i===n});if(t.nodeType)return v.grep(e,function(e,r){return e===t===n});if(typeof t=="string"){var r=v.grep(e,function(e){return e.nodeType===1});if(it.test(t))return v.filter(t,r,!n);t=v.filter(t,r)}return v.grep(e,function(e,r){return v.inArray(e,t)>=0===n})}function lt(e){var t=ct.split("|"),n=e.createDocumentFragment();if(n.createElement)while(t.length)n.createElement(t.pop());return n}function Lt(e,t){return e.getElementsByTagName(t)[0]||e.appendChild(e.ownerDocument.createElement(t))}function At(e,t){if(t.nodeType!==1||!v.hasData(e))return;var n,r,i,s=v._data(e),o=v._data(t,s),u=s.events;if(u){delete o.handle,o.events={};for(n in u)for(r=0,i=u[n].length;r").appendTo(i.body),n=t.css("display");t.remove();if(n==="none"||n===""){Pt=i.body.appendChild(Pt||v.extend(i.createElement("iframe"),{frameBorder:0,width:0,height:0}));if(!Ht||!Pt.createElement)Ht=(Pt.contentWindow||Pt.contentDocument).document,Ht.write(""),Ht.close();t=Ht.body.appendChild(Ht.createElement(e)),n=Dt(t,"display"),i.body.removeChild(Pt)}return Wt[e]=n,n}function fn(e,t,n,r){var i;if(v.isArray(t))v.each(t,function(t,i){n||sn.test(e)?r(e,i):fn(e+"["+(typeof i=="object"?t:"")+"]",i,n,r)});else if(!n&&v.type(t)==="object")for(i in t)fn(e+"["+i+"]",t[i],n,r);else r(e,t)}function Cn(e){return function(t,n){typeof t!="string"&&(n=t,t="*");var r,i,s,o=t.toLowerCase().split(y),u=0,a=o.length;if(v.isFunction(n))for(;u)[^>]*$|#([\w\-]*)$)/,E=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,S=/^[\],:{}\s]*$/,x=/(?:^|:|,)(?:\s*\[)+/g,T=/\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,N=/"[^"\\\r\n]*"|true|false|null|-?(?:\d\d*\.|)\d+(?:[eE][\-+]?\d+|)/g,C=/^-ms-/,k=/-([\da-z])/gi,L=function(e,t){return(t+"").toUpperCase()},A=function(){i.addEventListener?(i.removeEventListener("DOMContentLoaded",A,!1),v.ready()):i.readyState==="complete"&&(i.detachEvent("onreadystatechange",A),v.ready())},O={};v.fn=v.prototype={constructor:v,init:function(e,n,r){var s,o,u,a;if(!e)return this;if(e.nodeType)return this.context=this[0]=e,this.length=1,this;if(typeof e=="string"){e.charAt(0)==="<"&&e.charAt(e.length-1)===">"&&e.length>=3?s=[null,e,null]:s=w.exec(e);if(s&&(s[1]||!n)){if(s[1])return n=n instanceof v?n[0]:n,a=n&&n.nodeType?n.ownerDocument||n:i,e=v.parseHTML(s[1],a,!0),E.test(s[1])&&v.isPlainObject(n)&&this.attr.call(e,n,!0),v.merge(this,e);o=i.getElementById(s[2]);if(o&&o.parentNode){if(o.id!==s[2])return r.find(e);this.length=1,this[0]=o}return this.context=i,this.selector=e,this}return!n||n.jquery?(n||r).find(e):this.constructor(n).find(e)}return v.isFunction(e)?r.ready(e):(e.selector!==t&&(this.selector=e.selector,this.context=e.context),v.makeArray(e,this))},selector:"",jquery:"1.8.3",length:0,size:function(){return this.length},toArray:function(){return l.call(this)},get:function(e){return e==null?this.toArray():e<0?this[this.length+e]:this[e]},pushStack:function(e,t,n){var r=v.merge(this.constructor(),e);return r.prevObject=this,r.context=this.context,t==="find"?r.selector=this.selector+(this.selector?" ":"")+n:t&&(r.selector=this.selector+"."+t+"("+n+")"),r},each:function(e,t){return v.each(this,e,t)},ready:function(e){return v.ready.promise().done(e),this},eq:function(e){return e=+e,e===-1?this.slice(e):this.slice(e,e+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(l.apply(this,arguments),"slice",l.call(arguments).join(","))},map:function(e){return this.pushStack(v.map(this,function(t,n){return e.call(t,n,t)}))},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:[].sort,splice:[].splice},v.fn.init.prototype=v.fn,v.extend=v.fn.extend=function(){var e,n,r,i,s,o,u=arguments[0]||{},a=1,f=arguments.length,l=!1;typeof u=="boolean"&&(l=u,u=arguments[1]||{},a=2),typeof u!="object"&&!v.isFunction(u)&&(u={}),f===a&&(u=this,--a);for(;a0)return;r.resolveWith(i,[v]),v.fn.trigger&&v(i).trigger("ready").off("ready")},isFunction:function(e){return v.type(e)==="function"},isArray:Array.isArray||function(e){return v.type(e)==="array"},isWindow:function(e){return e!=null&&e==e.window},isNumeric:function(e){return!isNaN(parseFloat(e))&&isFinite(e)},type:function(e){return e==null?String(e):O[h.call(e)]||"object"},isPlainObject:function(e){if(!e||v.type(e)!=="object"||e.nodeType||v.isWindow(e))return!1;try{if(e.constructor&&!p.call(e,"constructor")&&!p.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(n){return!1}var r;for(r in e);return r===t||p.call(e,r)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},error:function(e){throw new Error(e)},parseHTML:function(e,t,n){var r;return!e||typeof e!="string"?null:(typeof t=="boolean"&&(n=t,t=0),t=t||i,(r=E.exec(e))?[t.createElement(r[1])]:(r=v.buildFragment([e],t,n?null:[]),v.merge([],(r.cacheable?v.clone(r.fragment):r.fragment).childNodes)))},parseJSON:function(t){if(!t||typeof t!="string")return null;t=v.trim(t);if(e.JSON&&e.JSON.parse)return e.JSON.parse(t);if(S.test(t.replace(T,"@").replace(N,"]").replace(x,"")))return(new Function("return "+t))();v.error("Invalid JSON: "+t)},parseXML:function(n){var r,i;if(!n||typeof n!="string")return null;try{e.DOMParser?(i=new DOMParser,r=i.parseFromString(n,"text/xml")):(r=new ActiveXObject("Microsoft.XMLDOM"),r.async="false",r.loadXML(n))}catch(s){r=t}return(!r||!r.documentElement||r.getElementsByTagName("parsererror").length)&&v.error("Invalid XML: "+n),r},noop:function(){},globalEval:function(t){t&&g.test(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(C,"ms-").replace(k,L)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,n,r){var i,s=0,o=e.length,u=o===t||v.isFunction(e);if(r){if(u){for(i in e)if(n.apply(e[i],r)===!1)break}else for(;s0&&e[0]&&e[a-1]||a===0||v.isArray(e));if(f)for(;u-1)a.splice(n,1),i&&(n<=o&&o--,n<=u&&u--)}),this},has:function(e){return v.inArray(e,a)>-1},empty:function(){return a=[],this},disable:function(){return a=f=n=t,this},disabled:function(){return!a},lock:function(){return f=t,n||c.disable(),this},locked:function(){return!f},fireWith:function(e,t){return t=t||[],t=[e,t.slice?t.slice():t],a&&(!r||f)&&(i?f.push(t):l(t)),this},fire:function(){return c.fireWith(this,arguments),this},fired:function(){return!!r}};return c},v.extend({Deferred:function(e){var t=[["resolve","done",v.Callbacks("once memory"),"resolved"],["reject","fail",v.Callbacks("once memory"),"rejected"],["notify","progress",v.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return v.Deferred(function(n){v.each(t,function(t,r){var s=r[0],o=e[t];i[r[1]](v.isFunction(o)?function(){var e=o.apply(this,arguments);e&&v.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[s+"With"](this===i?n:this,[e])}:n[s])}),e=null}).promise()},promise:function(e){return e!=null?v.extend(e,r):r}},i={};return r.pipe=r.then,v.each(t,function(e,s){var o=s[2],u=s[3];r[s[1]]=o.add,u&&o.add(function(){n=u},t[e^1][2].disable,t[2][2].lock),i[s[0]]=o.fire,i[s[0]+"With"]=o.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t=0,n=l.call(arguments),r=n.length,i=r!==1||e&&v.isFunction(e.promise)?r:0,s=i===1?e:v.Deferred(),o=function(e,t,n){return function(r){t[e]=this,n[e]=arguments.length>1?l.call(arguments):r,n===u?s.notifyWith(t,n):--i||s.resolveWith(t,n)}},u,a,f;if(r>1){u=new Array(r),a=new Array(r),f=new Array(r);for(;t
a",n=p.getElementsByTagName("*"),r=p.getElementsByTagName("a")[0];if(!n||!r||!n.length)return{};s=i.createElement("select"),o=s.appendChild(i.createElement("option")),u=p.getElementsByTagName("input")[0],r.style.cssText="top:1px;float:left;opacity:.5",t={leadingWhitespace:p.firstChild.nodeType===3,tbody:!p.getElementsByTagName("tbody").length,htmlSerialize:!!p.getElementsByTagName("link").length,style:/top/.test(r.getAttribute("style")),hrefNormalized:r.getAttribute("href")==="/a",opacity:/^0.5/.test(r.style.opacity),cssFloat:!!r.style.cssFloat,checkOn:u.value==="on",optSelected:o.selected,getSetAttribute:p.className!=="t",enctype:!!i.createElement("form").enctype,html5Clone:i.createElement("nav").cloneNode(!0).outerHTML!=="<:nav>",boxModel:i.compatMode==="CSS1Compat",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0,boxSizingReliable:!0,pixelPosition:!1},u.checked=!0,t.noCloneChecked=u.cloneNode(!0).checked,s.disabled=!0,t.optDisabled=!o.disabled;try{delete p.test}catch(d){t.deleteExpando=!1}!p.addEventListener&&p.attachEvent&&p.fireEvent&&(p.attachEvent("onclick",h=function(){t.noCloneEvent=!1}),p.cloneNode(!0).fireEvent("onclick"),p.detachEvent("onclick",h)),u=i.createElement("input"),u.value="t",u.setAttribute("type","radio"),t.radioValue=u.value==="t",u.setAttribute("checked","checked"),u.setAttribute("name","t"),p.appendChild(u),a=i.createDocumentFragment(),a.appendChild(p.lastChild),t.checkClone=a.cloneNode(!0).cloneNode(!0).lastChild.checked,t.appendChecked=u.checked,a.removeChild(u),a.appendChild(p);if(p.attachEvent)for(l in{submit:!0,change:!0,focusin:!0})f="on"+l,c=f in p,c||(p.setAttribute(f,"return;"),c=typeof p[f]=="function"),t[l+"Bubbles"]=c;return v(function(){var n,r,s,o,u="padding:0;margin:0;border:0;display:block;overflow:hidden;",a=i.getElementsByTagName("body")[0];if(!a)return;n=i.createElement("div"),n.style.cssText="visibility:hidden;border:0;width:0;height:0;position:static;top:0;margin-top:1px",a.insertBefore(n,a.firstChild),r=i.createElement("div"),n.appendChild(r),r.innerHTML="
t
",s=r.getElementsByTagName("td"),s[0].style.cssText="padding:0;margin:0;border:0;display:none",c=s[0].offsetHeight===0,s[0].style.display="",s[1].style.display="none",t.reliableHiddenOffsets=c&&s[0].offsetHeight===0,r.innerHTML="",r.style.cssText="box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;",t.boxSizing=r.offsetWidth===4,t.doesNotIncludeMarginInBodyOffset=a.offsetTop!==1,e.getComputedStyle&&(t.pixelPosition=(e.getComputedStyle(r,null)||{}).top!=="1%",t.boxSizingReliable=(e.getComputedStyle(r,null)||{width:"4px"}).width==="4px",o=i.createElement("div"),o.style.cssText=r.style.cssText=u,o.style.marginRight=o.style.width="0",r.style.width="1px",r.appendChild(o),t.reliableMarginRight=!parseFloat((e.getComputedStyle(o,null)||{}).marginRight)),typeof r.style.zoom!="undefined"&&(r.innerHTML="",r.style.cssText=u+"width:1px;padding:1px;display:inline;zoom:1",t.inlineBlockNeedsLayout=r.offsetWidth===3,r.style.display="block",r.style.overflow="visible",r.innerHTML="
",r.firstChild.style.width="5px",t.shrinkWrapBlocks=r.offsetWidth!==3,n.style.zoom=1),a.removeChild(n),n=r=s=o=null}),a.removeChild(p),n=r=s=o=u=a=p=null,t}();var D=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,P=/([A-Z])/g;v.extend({cache:{},deletedIds:[],uuid:0,expando:"jQuery"+(v.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(e){return e=e.nodeType?v.cache[e[v.expando]]:e[v.expando],!!e&&!B(e)},data:function(e,n,r,i){if(!v.acceptData(e))return;var s,o,u=v.expando,a=typeof n=="string",f=e.nodeType,l=f?v.cache:e,c=f?e[u]:e[u]&&u;if((!c||!l[c]||!i&&!l[c].data)&&a&&r===t)return;c||(f?e[u]=c=v.deletedIds.pop()||v.guid++:c=u),l[c]||(l[c]={},f||(l[c].toJSON=v.noop));if(typeof n=="object"||typeof n=="function")i?l[c]=v.extend(l[c],n):l[c].data=v.extend(l[c].data,n);return s=l[c],i||(s.data||(s.data={}),s=s.data),r!==t&&(s[v.camelCase(n)]=r),a?(o=s[n],o==null&&(o=s[v.camelCase(n)])):o=s,o},removeData:function(e,t,n){if(!v.acceptData(e))return;var r,i,s,o=e.nodeType,u=o?v.cache:e,a=o?e[v.expando]:v.expando;if(!u[a])return;if(t){r=n?u[a]:u[a].data;if(r){v.isArray(t)||(t in r?t=[t]:(t=v.camelCase(t),t in r?t=[t]:t=t.split(" ")));for(i=0,s=t.length;i1,null,!1))},removeData:function(e){return this.each(function(){v.removeData(this,e)})}}),v.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=v._data(e,t),n&&(!r||v.isArray(n)?r=v._data(e,t,v.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=v.queue(e,t),r=n.length,i=n.shift(),s=v._queueHooks(e,t),o=function(){v.dequeue(e,t)};i==="inprogress"&&(i=n.shift(),r--),i&&(t==="fx"&&n.unshift("inprogress"),delete s.stop,i.call(e,o,s)),!r&&s&&s.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return v._data(e,n)||v._data(e,n,{empty:v.Callbacks("once memory").add(function(){v.removeData(e,t+"queue",!0),v.removeData(e,n,!0)})})}}),v.fn.extend({queue:function(e,n){var r=2;return typeof e!="string"&&(n=e,e="fx",r--),arguments.length1)},removeAttr:function(e){return this.each(function(){v.removeAttr(this,e)})},prop:function(e,t){return v.access(this,v.prop,e,t,arguments.length>1)},removeProp:function(e){return e=v.propFix[e]||e,this.each(function(){try{this[e]=t,delete this[e]}catch(n){}})},addClass:function(e){var t,n,r,i,s,o,u;if(v.isFunction(e))return this.each(function(t){v(this).addClass(e.call(this,t,this.className))});if(e&&typeof e=="string"){t=e.split(y);for(n=0,r=this.length;n=0)r=r.replace(" "+n[s]+" "," ");i.className=e?v.trim(r):""}}}return this},toggleClass:function(e,t){var n=typeof e,r=typeof t=="boolean";return v.isFunction(e)?this.each(function(n){v(this).toggleClass(e.call(this,n,this.className,t),t)}):this.each(function(){if(n==="string"){var i,s=0,o=v(this),u=t,a=e.split(y);while(i=a[s++])u=r?u:!o.hasClass(i),o[u?"addClass":"removeClass"](i)}else if(n==="undefined"||n==="boolean")this.className&&v._data(this,"__className__",this.className),this.className=this.className||e===!1?"":v._data(this,"__className__")||""})},hasClass:function(e){var t=" "+e+" ",n=0,r=this.length;for(;n=0)return!0;return!1},val:function(e){var n,r,i,s=this[0];if(!arguments.length){if(s)return n=v.valHooks[s.type]||v.valHooks[s.nodeName.toLowerCase()],n&&"get"in n&&(r=n.get(s,"value"))!==t?r:(r=s.value,typeof r=="string"?r.replace(R,""):r==null?"":r);return}return i=v.isFunction(e),this.each(function(r){var s,o=v(this);if(this.nodeType!==1)return;i?s=e.call(this,r,o.val()):s=e,s==null?s="":typeof s=="number"?s+="":v.isArray(s)&&(s=v.map(s,function(e){return e==null?"":e+""})),n=v.valHooks[this.type]||v.valHooks[this.nodeName.toLowerCase()];if(!n||!("set"in n)||n.set(this,s,"value")===t)this.value=s})}}),v.extend({valHooks:{option:{get:function(e){var t=e.attributes.value;return!t||t.specified?e.value:e.text}},select:{get:function(e){var t,n,r=e.options,i=e.selectedIndex,s=e.type==="select-one"||i<0,o=s?null:[],u=s?i+1:r.length,a=i<0?u:s?i:0;for(;a=0}),n.length||(e.selectedIndex=-1),n}}},attrFn:{},attr:function(e,n,r,i){var s,o,u,a=e.nodeType;if(!e||a===3||a===8||a===2)return;if(i&&v.isFunction(v.fn[n]))return v(e)[n](r);if(typeof e.getAttribute=="undefined")return v.prop(e,n,r);u=a!==1||!v.isXMLDoc(e),u&&(n=n.toLowerCase(),o=v.attrHooks[n]||(X.test(n)?F:j));if(r!==t){if(r===null){v.removeAttr(e,n);return}return o&&"set"in o&&u&&(s=o.set(e,r,n))!==t?s:(e.setAttribute(n,r+""),r)}return o&&"get"in o&&u&&(s=o.get(e,n))!==null?s:(s=e.getAttribute(n),s===null?t:s)},removeAttr:function(e,t){var n,r,i,s,o=0;if(t&&e.nodeType===1){r=t.split(y);for(;o=0}})});var $=/^(?:textarea|input|select)$/i,J=/^([^\.]*|)(?:\.(.+)|)$/,K=/(?:^|\s)hover(\.\S+|)\b/,Q=/^key/,G=/^(?:mouse|contextmenu)|click/,Y=/^(?:focusinfocus|focusoutblur)$/,Z=function(e){return v.event.special.hover?e:e.replace(K,"mouseenter$1 mouseleave$1")};v.event={add:function(e,n,r,i,s){var o,u,a,f,l,c,h,p,d,m,g;if(e.nodeType===3||e.nodeType===8||!n||!r||!(o=v._data(e)))return;r.handler&&(d=r,r=d.handler,s=d.selector),r.guid||(r.guid=v.guid++),a=o.events,a||(o.events=a={}),u=o.handle,u||(o.handle=u=function(e){return typeof v=="undefined"||!!e&&v.event.triggered===e.type?t:v.event.dispatch.apply(u.elem,arguments)},u.elem=e),n=v.trim(Z(n)).split(" ");for(f=0;f=0&&(y=y.slice(0,-1),a=!0),y.indexOf(".")>=0&&(b=y.split("."),y=b.shift(),b.sort());if((!s||v.event.customEvent[y])&&!v.event.global[y])return;n=typeof n=="object"?n[v.expando]?n:new v.Event(y,n):new v.Event(y),n.type=y,n.isTrigger=!0,n.exclusive=a,n.namespace=b.join("."),n.namespace_re=n.namespace?new RegExp("(^|\\.)"+b.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,h=y.indexOf(":")<0?"on"+y:"";if(!s){u=v.cache;for(f in u)u[f].events&&u[f].events[y]&&v.event.trigger(n,r,u[f].handle.elem,!0);return}n.result=t,n.target||(n.target=s),r=r!=null?v.makeArray(r):[],r.unshift(n),p=v.event.special[y]||{};if(p.trigger&&p.trigger.apply(s,r)===!1)return;m=[[s,p.bindType||y]];if(!o&&!p.noBubble&&!v.isWindow(s)){g=p.delegateType||y,l=Y.test(g+y)?s:s.parentNode;for(c=s;l;l=l.parentNode)m.push([l,g]),c=l;c===(s.ownerDocument||i)&&m.push([c.defaultView||c.parentWindow||e,g])}for(f=0;f=0:v.find(h,this,null,[s]).length),u[h]&&f.push(c);f.length&&w.push({elem:s,matches:f})}d.length>m&&w.push({elem:this,matches:d.slice(m)});for(r=0;r0?this.on(t,null,e,n):this.trigger(t)},Q.test(t)&&(v.event.fixHooks[t]=v.event.keyHooks),G.test(t)&&(v.event.fixHooks[t]=v.event.mouseHooks)}),function(e,t){function nt(e,t,n,r){n=n||[],t=t||g;var i,s,a,f,l=t.nodeType;if(!e||typeof e!="string")return n;if(l!==1&&l!==9)return[];a=o(t);if(!a&&!r)if(i=R.exec(e))if(f=i[1]){if(l===9){s=t.getElementById(f);if(!s||!s.parentNode)return n;if(s.id===f)return n.push(s),n}else if(t.ownerDocument&&(s=t.ownerDocument.getElementById(f))&&u(t,s)&&s.id===f)return n.push(s),n}else{if(i[2])return S.apply(n,x.call(t.getElementsByTagName(e),0)),n;if((f=i[3])&&Z&&t.getElementsByClassName)return S.apply(n,x.call(t.getElementsByClassName(f),0)),n}return vt(e.replace(j,"$1"),t,n,r,a)}function rt(e){return function(t){var n=t.nodeName.toLowerCase();return n==="input"&&t.type===e}}function it(e){return function(t){var n=t.nodeName.toLowerCase();return(n==="input"||n==="button")&&t.type===e}}function st(e){return N(function(t){return t=+t,N(function(n,r){var i,s=e([],n.length,t),o=s.length;while(o--)n[i=s[o]]&&(n[i]=!(r[i]=n[i]))})})}function ot(e,t,n){if(e===t)return n;var r=e.nextSibling;while(r){if(r===t)return-1;r=r.nextSibling}return 1}function ut(e,t){var n,r,s,o,u,a,f,l=L[d][e+" "];if(l)return t?0:l.slice(0);u=e,a=[],f=i.preFilter;while(u){if(!n||(r=F.exec(u)))r&&(u=u.slice(r[0].length)||u),a.push(s=[]);n=!1;if(r=I.exec(u))s.push(n=new m(r.shift())),u=u.slice(n.length),n.type=r[0].replace(j," ");for(o in i.filter)(r=J[o].exec(u))&&(!f[o]||(r=f[o](r)))&&(s.push(n=new m(r.shift())),u=u.slice(n.length),n.type=o,n.matches=r);if(!n)break}return t?u.length:u?nt.error(e):L(e,a).slice(0)}function at(e,t,r){var i=t.dir,s=r&&t.dir==="parentNode",o=w++;return t.first?function(t,n,r){while(t=t[i])if(s||t.nodeType===1)return e(t,n,r)}:function(t,r,u){if(!u){var a,f=b+" "+o+" ",l=f+n;while(t=t[i])if(s||t.nodeType===1){if((a=t[d])===l)return t.sizset;if(typeof a=="string"&&a.indexOf(f)===0){if(t.sizset)return t}else{t[d]=l;if(e(t,r,u))return t.sizset=!0,t;t.sizset=!1}}}else while(t=t[i])if(s||t.nodeType===1)if(e(t,r,u))return t}}function ft(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function lt(e,t,n,r,i){var s,o=[],u=0,a=e.length,f=t!=null;for(;u-1&&(s[f]=!(o[f]=c))}}else g=lt(g===o?g.splice(d,g.length):g),i?i(null,o,g,a):S.apply(o,g)})}function ht(e){var t,n,r,s=e.length,o=i.relative[e[0].type],u=o||i.relative[" "],a=o?1:0,f=at(function(e){return e===t},u,!0),l=at(function(e){return T.call(t,e)>-1},u,!0),h=[function(e,n,r){return!o&&(r||n!==c)||((t=n).nodeType?f(e,n,r):l(e,n,r))}];for(;a1&&ft(h),a>1&&e.slice(0,a-1).join("").replace(j,"$1"),n,a0,s=e.length>0,o=function(u,a,f,l,h){var p,d,v,m=[],y=0,w="0",x=u&&[],T=h!=null,N=c,C=u||s&&i.find.TAG("*",h&&a.parentNode||a),k=b+=N==null?1:Math.E;T&&(c=a!==g&&a,n=o.el);for(;(p=C[w])!=null;w++){if(s&&p){for(d=0;v=e[d];d++)if(v(p,a,f)){l.push(p);break}T&&(b=k,n=++o.el)}r&&((p=!v&&p)&&y--,u&&x.push(p))}y+=w;if(r&&w!==y){for(d=0;v=t[d];d++)v(x,m,a,f);if(u){if(y>0)while(w--)!x[w]&&!m[w]&&(m[w]=E.call(l));m=lt(m)}S.apply(l,m),T&&!u&&m.length>0&&y+t.length>1&&nt.uniqueSort(l)}return T&&(b=k,c=N),x};return o.el=0,r?N(o):o}function dt(e,t,n){var r=0,i=t.length;for(;r2&&(f=u[0]).type==="ID"&&t.nodeType===9&&!s&&i.relative[u[1].type]){t=i.find.ID(f.matches[0].replace($,""),t,s)[0];if(!t)return n;e=e.slice(u.shift().length)}for(o=J.POS.test(e)?-1:u.length-1;o>=0;o--){f=u[o];if(i.relative[l=f.type])break;if(c=i.find[l])if(r=c(f.matches[0].replace($,""),z.test(u[0].type)&&t.parentNode||t,s)){u.splice(o,1),e=r.length&&u.join("");if(!e)return S.apply(n,x.call(r,0)),n;break}}}return a(e,h)(r,t,s,n,z.test(e)),n}function mt(){}var n,r,i,s,o,u,a,f,l,c,h=!0,p="undefined",d=("sizcache"+Math.random()).replace(".",""),m=String,g=e.document,y=g.documentElement,b=0,w=0,E=[].pop,S=[].push,x=[].slice,T=[].indexOf||function(e){var t=0,n=this.length;for(;ti.cacheLength&&delete e[t.shift()],e[n+" "]=r},e)},k=C(),L=C(),A=C(),O="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[-\\w]|[^\\x00-\\xa0])+",_=M.replace("w","w#"),D="([*^$|!~]?=)",P="\\["+O+"*("+M+")"+O+"*(?:"+D+O+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+_+")|)|)"+O+"*\\]",H=":("+M+")(?:\\((?:(['\"])((?:\\\\.|[^\\\\])*?)\\2|([^()[\\]]*|(?:(?:"+P+")|[^:]|\\\\.)*|.*))\\)|)",B=":(even|odd|eq|gt|lt|nth|first|last)(?:\\("+O+"*((?:-\\d)?\\d*)"+O+"*\\)|)(?=[^-]|$)",j=new RegExp("^"+O+"+|((?:^|[^\\\\])(?:\\\\.)*)"+O+"+$","g"),F=new RegExp("^"+O+"*,"+O+"*"),I=new RegExp("^"+O+"*([\\x20\\t\\r\\n\\f>+~])"+O+"*"),q=new RegExp(H),R=/^(?:#([\w\-]+)|(\w+)|\.([\w\-]+))$/,U=/^:not/,z=/[\x20\t\r\n\f]*[+~]/,W=/:not\($/,X=/h\d/i,V=/input|select|textarea|button/i,$=/\\(?!\\)/g,J={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),NAME:new RegExp("^\\[name=['\"]?("+M+")['\"]?\\]"),TAG:new RegExp("^("+M.replace("w","w*")+")"),ATTR:new RegExp("^"+P),PSEUDO:new RegExp("^"+H),POS:new RegExp(B,"i"),CHILD:new RegExp("^:(only|nth|first|last)-child(?:\\("+O+"*(even|odd|(([+-]|)(\\d*)n|)"+O+"*(?:([+-]|)"+O+"*(\\d+)|))"+O+"*\\)|)","i"),needsContext:new RegExp("^"+O+"*[>+~]|"+B,"i")},K=function(e){var t=g.createElement("div");try{return e(t)}catch(n){return!1}finally{t=null}},Q=K(function(e){return e.appendChild(g.createComment("")),!e.getElementsByTagName("*").length}),G=K(function(e){return e.innerHTML="",e.firstChild&&typeof e.firstChild.getAttribute!==p&&e.firstChild.getAttribute("href")==="#"}),Y=K(function(e){e.innerHTML="";var t=typeof e.lastChild.getAttribute("multiple");return t!=="boolean"&&t!=="string"}),Z=K(function(e){return e.innerHTML="",!e.getElementsByClassName||!e.getElementsByClassName("e").length?!1:(e.lastChild.className="e",e.getElementsByClassName("e").length===2)}),et=K(function(e){e.id=d+0,e.innerHTML="
",y.insertBefore(e,y.firstChild);var t=g.getElementsByName&&g.getElementsByName(d).length===2+g.getElementsByName(d+0).length;return r=!g.getElementById(d),y.removeChild(e),t});try{x.call(y.childNodes,0)[0].nodeType}catch(tt){x=function(e){var t,n=[];for(;t=this[e];e++)n.push(t);return n}}nt.matches=function(e,t){return nt(e,null,null,t)},nt.matchesSelector=function(e,t){return nt(t,null,null,[e]).length>0},s=nt.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(i===1||i===9||i===11){if(typeof e.textContent=="string")return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=s(e)}else if(i===3||i===4)return e.nodeValue}else for(;t=e[r];r++)n+=s(t);return n},o=nt.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?t.nodeName!=="HTML":!1},u=nt.contains=y.contains?function(e,t){var n=e.nodeType===9?e.documentElement:e,r=t&&t.parentNode;return e===r||!!(r&&r.nodeType===1&&n.contains&&n.contains(r))}:y.compareDocumentPosition?function(e,t){return t&&!!(e.compareDocumentPosition(t)&16)}:function(e,t){while(t=t.parentNode)if(t===e)return!0;return!1},nt.attr=function(e,t){var n,r=o(e);return r||(t=t.toLowerCase()),(n=i.attrHandle[t])?n(e):r||Y?e.getAttribute(t):(n=e.getAttributeNode(t),n?typeof e[t]=="boolean"?e[t]?t:null:n.specified?n.value:null:null)},i=nt.selectors={cacheLength:50,createPseudo:N,match:J,attrHandle:G?{}:{href:function(e){return e.getAttribute("href",2)},type:function(e){return e.getAttribute("type")}},find:{ID:r?function(e,t,n){if(typeof t.getElementById!==p&&!n){var r=t.getElementById(e);return r&&r.parentNode?[r]:[]}}:function(e,n,r){if(typeof n.getElementById!==p&&!r){var i=n.getElementById(e);return i?i.id===e||typeof i.getAttributeNode!==p&&i.getAttributeNode("id").value===e?[i]:t:[]}},TAG:Q?function(e,t){if(typeof t.getElementsByTagName!==p)return t.getElementsByTagName(e)}:function(e,t){var n=t.getElementsByTagName(e);if(e==="*"){var r,i=[],s=0;for(;r=n[s];s++)r.nodeType===1&&i.push(r);return i}return n},NAME:et&&function(e,t){if(typeof t.getElementsByName!==p)return t.getElementsByName(name)},CLASS:Z&&function(e,t,n){if(typeof t.getElementsByClassName!==p&&!n)return t.getElementsByClassName(e)}},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace($,""),e[3]=(e[4]||e[5]||"").replace($,""),e[2]==="~="&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),e[1]==="nth"?(e[2]||nt.error(e[0]),e[3]=+(e[3]?e[4]+(e[5]||1):2*(e[2]==="even"||e[2]==="odd")),e[4]=+(e[6]+e[7]||e[2]==="odd")):e[2]&&nt.error(e[0]),e},PSEUDO:function(e){var t,n;if(J.CHILD.test(e[0]))return null;if(e[3])e[2]=e[3];else if(t=e[4])q.test(t)&&(n=ut(t,!0))&&(n=t.indexOf(")",t.length-n)-t.length)&&(t=t.slice(0,n),e[0]=e[0].slice(0,n)),e[2]=t;return e.slice(0,3)}},filter:{ID:r?function(e){return e=e.replace($,""),function(t){return t.getAttribute("id")===e}}:function(e){return e=e.replace($,""),function(t){var n=typeof t.getAttributeNode!==p&&t.getAttributeNode("id");return n&&n.value===e}},TAG:function(e){return e==="*"?function(){return!0}:(e=e.replace($,"").toLowerCase(),function(t){return t.nodeName&&t.nodeName.toLowerCase()===e})},CLASS:function(e){var t=k[d][e+" "];return t||(t=new RegExp("(^|"+O+")"+e+"("+O+"|$)"))&&k(e,function(e){return t.test(e.className||typeof e.getAttribute!==p&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r,i){var s=nt.attr(r,e);return s==null?t==="!=":t?(s+="",t==="="?s===n:t==="!="?s!==n:t==="^="?n&&s.indexOf(n)===0:t==="*="?n&&s.indexOf(n)>-1:t==="$="?n&&s.substr(s.length-n.length)===n:t==="~="?(" "+s+" ").indexOf(n)>-1:t==="|="?s===n||s.substr(0,n.length+1)===n+"-":!1):!0}},CHILD:function(e,t,n,r){return e==="nth"?function(e){var t,i,s=e.parentNode;if(n===1&&r===0)return!0;if(s){i=0;for(t=s.firstChild;t;t=t.nextSibling)if(t.nodeType===1){i++;if(e===t)break}}return i-=r,i===n||i%n===0&&i/n>=0}:function(t){var n=t;switch(e){case"only":case"first":while(n=n.previousSibling)if(n.nodeType===1)return!1;if(e==="first")return!0;n=t;case"last":while(n=n.nextSibling)if(n.nodeType===1)return!1;return!0}}},PSEUDO:function(e,t){var n,r=i.pseudos[e]||i.setFilters[e.toLowerCase()]||nt.error("unsupported pseudo: "+e);return r[d]?r(t):r.length>1?(n=[e,e,"",t],i.setFilters.hasOwnProperty(e.toLowerCase())?N(function(e,n){var i,s=r(e,t),o=s.length;while(o--)i=T.call(e,s[o]),e[i]=!(n[i]=s[o])}):function(e){return r(e,0,n)}):r}},pseudos:{not:N(function(e){var t=[],n=[],r=a(e.replace(j,"$1"));return r[d]?N(function(e,t,n,i){var s,o=r(e,null,i,[]),u=e.length;while(u--)if(s=o[u])e[u]=!(t[u]=s)}):function(e,i,s){return t[0]=e,r(t,null,s,n),!n.pop()}}),has:N(function(e){return function(t){return nt(e,t).length>0}}),contains:N(function(e){return function(t){return(t.textContent||t.innerText||s(t)).indexOf(e)>-1}}),enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return t==="input"&&!!e.checked||t==="option"&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},parent:function(e){return!i.pseudos.empty(e)},empty:function(e){var t;e=e.firstChild;while(e){if(e.nodeName>"@"||(t=e.nodeType)===3||t===4)return!1;e=e.nextSibling}return!0},header:function(e){return X.test(e.nodeName)},text:function(e){var t,n;return e.nodeName.toLowerCase()==="input"&&(t=e.type)==="text"&&((n=e.getAttribute("type"))==null||n.toLowerCase()===t)},radio:rt("radio"),checkbox:rt("checkbox"),file:rt("file"),password:rt("password"),image:rt("image"),submit:it("submit"),reset:it("reset"),button:function(e){var t=e.nodeName.toLowerCase();return t==="input"&&e.type==="button"||t==="button"},input:function(e){return V.test(e.nodeName)},focus:function(e){var t=e.ownerDocument;return e===t.activeElement&&(!t.hasFocus||t.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},active:function(e){return e===e.ownerDocument.activeElement},first:st(function(){return[0]}),last:st(function(e,t){return[t-1]}),eq:st(function(e,t,n){return[n<0?n+t:n]}),even:st(function(e,t){for(var n=0;n=0;)e.push(r);return e}),gt:st(function(e,t,n){for(var r=n<0?n+t:n;++r",e.querySelectorAll("[selected]").length||i.push("\\["+O+"*(?:checked|disabled|ismap|multiple|readonly|selected|value)"),e.querySelectorAll(":checked").length||i.push(":checked")}),K(function(e){e.innerHTML="

",e.querySelectorAll("[test^='']").length&&i.push("[*^$]="+O+"*(?:\"\"|'')"),e.innerHTML="",e.querySelectorAll(":enabled").length||i.push(":enabled",":disabled")}),i=new RegExp(i.join("|")),vt=function(e,r,s,o,u){if(!o&&!u&&!i.test(e)){var a,f,l=!0,c=d,h=r,p=r.nodeType===9&&e;if(r.nodeType===1&&r.nodeName.toLowerCase()!=="object"){a=ut(e),(l=r.getAttribute("id"))?c=l.replace(n,"\\$&"):r.setAttribute("id",c),c="[id='"+c+"'] ",f=a.length;while(f--)a[f]=c+a[f].join("");h=z.test(e)&&r.parentNode||r,p=a.join(",")}if(p)try{return S.apply(s,x.call(h.querySelectorAll(p),0)),s}catch(v){}finally{l||r.removeAttribute("id")}}return t(e,r,s,o,u)},u&&(K(function(t){e=u.call(t,"div");try{u.call(t,"[test!='']:sizzle"),s.push("!=",H)}catch(n){}}),s=new RegExp(s.join("|")),nt.matchesSelector=function(t,n){n=n.replace(r,"='$1']");if(!o(t)&&!s.test(n)&&!i.test(n))try{var a=u.call(t,n);if(a||e||t.document&&t.document.nodeType!==11)return a}catch(f){}return nt(n,null,null,[t]).length>0})}(),i.pseudos.nth=i.pseudos.eq,i.filters=mt.prototype=i.pseudos,i.setFilters=new mt,nt.attr=v.attr,v.find=nt,v.expr=nt.selectors,v.expr[":"]=v.expr.pseudos,v.unique=nt.uniqueSort,v.text=nt.getText,v.isXMLDoc=nt.isXML,v.contains=nt.contains}(e);var nt=/Until$/,rt=/^(?:parents|prev(?:Until|All))/,it=/^.[^:#\[\.,]*$/,st=v.expr.match.needsContext,ot={children:!0,contents:!0,next:!0,prev:!0};v.fn.extend({find:function(e){var t,n,r,i,s,o,u=this;if(typeof e!="string")return v(e).filter(function(){for(t=0,n=u.length;t0)for(i=r;i=0:v.filter(e,this).length>0:this.filter(e).length>0)},closest:function(e,t){var n,r=0,i=this.length,s=[],o=st.test(e)||typeof e!="string"?v(e,t||this.context):0;for(;r-1:v.find.matchesSelector(n,e)){s.push(n);break}n=n.parentNode}}return s=s.length>1?v.unique(s):s,this.pushStack(s,"closest",e)},index:function(e){return e?typeof e=="string"?v.inArray(this[0],v(e)):v.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.prevAll().length:-1},add:function(e,t){var n=typeof e=="string"?v(e,t):v.makeArray(e&&e.nodeType?[e]:e),r=v.merge(this.get(),n);return this.pushStack(ut(n[0])||ut(r[0])?r:v.unique(r))},addBack:function(e){return this.add(e==null?this.prevObject:this.prevObject.filter(e))}}),v.fn.andSelf=v.fn.addBack,v.each({parent:function(e){var t=e.parentNode;return t&&t.nodeType!==11?t:null},parents:function(e){return v.dir(e,"parentNode")},parentsUntil:function(e,t,n){return v.dir(e,"parentNode",n)},next:function(e){return at(e,"nextSibling")},prev:function(e){return at(e,"previousSibling")},nextAll:function(e){return v.dir(e,"nextSibling")},prevAll:function(e){return v.dir(e,"previousSibling")},nextUntil:function(e,t,n){return v.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return v.dir(e,"previousSibling",n)},siblings:function(e){return v.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return v.sibling(e.firstChild)},contents:function(e){return v.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:v.merge([],e.childNodes)}},function(e,t){v.fn[e]=function(n,r){var i=v.map(this,t,n);return nt.test(e)||(r=n),r&&typeof r=="string"&&(i=v.filter(r,i)),i=this.length>1&&!ot[e]?v.unique(i):i,this.length>1&&rt.test(e)&&(i=i.reverse()),this.pushStack(i,e,l.call(arguments).join(","))}}),v.extend({filter:function(e,t,n){return n&&(e=":not("+e+")"),t.length===1?v.find.matchesSelector(t[0],e)?[t[0]]:[]:v.find.matches(e,t)},dir:function(e,n,r){var i=[],s=e[n];while(s&&s.nodeType!==9&&(r===t||s.nodeType!==1||!v(s).is(r)))s.nodeType===1&&i.push(s),s=s[n];return i},sibling:function(e,t){var n=[];for(;e;e=e.nextSibling)e.nodeType===1&&e!==t&&n.push(e);return n}});var ct="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",ht=/ jQuery\d+="(?:null|\d+)"/g,pt=/^\s+/,dt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,vt=/<([\w:]+)/,mt=/]","i"),Et=/^(?:checkbox|radio)$/,St=/checked\s*(?:[^=]|=\s*.checked.)/i,xt=/\/(java|ecma)script/i,Tt=/^\s*\s*$/g,Nt={option:[1,""],legend:[1,"
","
"],thead:[1,"","
"],tr:[2,"","
"],td:[3,"","
"],col:[2,"","
"],area:[1,"",""],_default:[0,"",""]},Ct=lt(i),kt=Ct.appendChild(i.createElement("div"));Nt.optgroup=Nt.option,Nt.tbody=Nt.tfoot=Nt.colgroup=Nt.caption=Nt.thead,Nt.th=Nt.td,v.support.htmlSerialize||(Nt._default=[1,"X
","
"]),v.fn.extend({text:function(e){return v.access(this,function(e){return e===t?v.text(this):this.empty().append((this[0]&&this[0].ownerDocument||i).createTextNode(e))},null,e,arguments.length)},wrapAll:function(e){if(v.isFunction(e))return this.each(function(t){v(this).wrapAll(e.call(this,t))});if(this[0]){var t=v(e,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstChild&&e.firstChild.nodeType===1)e=e.firstChild;return e}).append(this)}return this},wrapInner:function(e){return v.isFunction(e)?this.each(function(t){v(this).wrapInner(e.call(this,t))}):this.each(function(){var t=v(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=v.isFunction(e);return this.each(function(n){v(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){v.nodeName(this,"body")||v(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(e){(this.nodeType===1||this.nodeType===11)&&this.appendChild(e)})},prepend:function(){return this.domManip(arguments,!0,function(e){(this.nodeType===1||this.nodeType===11)&&this.insertBefore(e,this.firstChild)})},before:function(){if(!ut(this[0]))return this.domManip(arguments,!1,function(e){this.parentNode.insertBefore(e,this)});if(arguments.length){var e=v.clean(arguments);return this.pushStack(v.merge(e,this),"before",this.selector)}},after:function(){if(!ut(this[0]))return this.domManip(arguments,!1,function(e){this.parentNode.insertBefore(e,this.nextSibling)});if(arguments.length){var e=v.clean(arguments);return this.pushStack(v.merge(this,e),"after",this.selector)}},remove:function(e,t){var n,r=0;for(;(n=this[r])!=null;r++)if(!e||v.filter(e,[n]).length)!t&&n.nodeType===1&&(v.cleanData(n.getElementsByTagName("*")),v.cleanData([n])),n.parentNode&&n.parentNode.removeChild(n);return this},empty:function(){var e,t=0;for(;(e=this[t])!=null;t++){e.nodeType===1&&v.cleanData(e.getElementsByTagName("*"));while(e.firstChild)e.removeChild(e.firstChild)}return this},clone:function(e,t){return e=e==null?!1:e,t=t==null?e:t,this.map(function(){return v.clone(this,e,t)})},html:function(e){return v.access(this,function(e){var n=this[0]||{},r=0,i=this.length;if(e===t)return n.nodeType===1?n.innerHTML.replace(ht,""):t;if(typeof e=="string"&&!yt.test(e)&&(v.support.htmlSerialize||!wt.test(e))&&(v.support.leadingWhitespace||!pt.test(e))&&!Nt[(vt.exec(e)||["",""])[1].toLowerCase()]){e=e.replace(dt,"<$1>");try{for(;r1&&typeof f=="string"&&St.test(f))return this.each(function(){v(this).domManip(e,n,r)});if(v.isFunction(f))return this.each(function(i){var s=v(this);e[0]=f.call(this,i,n?s.html():t),s.domManip(e,n,r)});if(this[0]){i=v.buildFragment(e,this,l),o=i.fragment,s=o.firstChild,o.childNodes.length===1&&(o=s);if(s){n=n&&v.nodeName(s,"tr");for(u=i.cacheable||c-1;a0?this.clone(!0):this).get(),v(o[i])[t](r),s=s.concat(r);return this.pushStack(s,e,o.selector)}}),v.extend({clone:function(e,t,n){var r,i,s,o;v.support.html5Clone||v.isXMLDoc(e)||!wt.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(kt.innerHTML=e.outerHTML,kt.removeChild(o=kt.firstChild));if((!v.support.noCloneEvent||!v.support.noCloneChecked)&&(e.nodeType===1||e.nodeType===11)&&!v.isXMLDoc(e)){Ot(e,o),r=Mt(e),i=Mt(o);for(s=0;r[s];++s)i[s]&&Ot(r[s],i[s])}if(t){At(e,o);if(n){r=Mt(e),i=Mt(o);for(s=0;r[s];++s)At(r[s],i[s])}}return r=i=null,o},clean:function(e,t,n,r){var s,o,u,a,f,l,c,h,p,d,m,g,y=t===i&&Ct,b=[];if(!t||typeof t.createDocumentFragment=="undefined")t=i;for(s=0;(u=e[s])!=null;s++){typeof u=="number"&&(u+="");if(!u)continue;if(typeof u=="string")if(!gt.test(u))u=t.createTextNode(u);else{y=y||lt(t),c=t.createElement("div"),y.appendChild(c),u=u.replace(dt,"<$1>"),a=(vt.exec(u)||["",""])[1].toLowerCase(),f=Nt[a]||Nt._default,l=f[0],c.innerHTML=f[1]+u+f[2];while(l--)c=c.lastChild;if(!v.support.tbody){h=mt.test(u),p=a==="table"&&!h?c.firstChild&&c.firstChild.childNodes:f[1]===""&&!h?c.childNodes:[];for(o=p.length-1;o>=0;--o)v.nodeName(p[o],"tbody")&&!p[o].childNodes.length&&p[o].parentNode.removeChild(p[o])}!v.support.leadingWhitespace&&pt.test(u)&&c.insertBefore(t.createTextNode(pt.exec(u)[0]),c.firstChild),u=c.childNodes,c.parentNode.removeChild(c)}u.nodeType?b.push(u):v.merge(b,u)}c&&(u=c=y=null);if(!v.support.appendChecked)for(s=0;(u=b[s])!=null;s++)v.nodeName(u,"input")?_t(u):typeof u.getElementsByTagName!="undefined"&&v.grep(u.getElementsByTagName("input"),_t);if(n){m=function(e){if(!e.type||xt.test(e.type))return r?r.push(e.parentNode?e.parentNode.removeChild(e):e):n.appendChild(e)};for(s=0;(u=b[s])!=null;s++)if(!v.nodeName(u,"script")||!m(u))n.appendChild(u),typeof u.getElementsByTagName!="undefined"&&(g=v.grep(v.merge([],u.getElementsByTagName("script")),m),b.splice.apply(b,[s+1,0].concat(g)),s+=g.length)}return b},cleanData:function(e,t){var n,r,i,s,o=0,u=v.expando,a=v.cache,f=v.support.deleteExpando,l=v.event.special;for(;(i=e[o])!=null;o++)if(t||v.acceptData(i)){r=i[u],n=r&&a[r];if(n){if(n.events)for(s in n.events)l[s]?v.event.remove(i,s):v.removeEvent(i,s,n.handle);a[r]&&(delete a[r],f?delete i[u]:i.removeAttribute?i.removeAttribute(u):i[u]=null,v.deletedIds.push(r))}}}}),function(){var e,t;v.uaMatch=function(e){e=e.toLowerCase();var t=/(chrome)[ \/]([\w.]+)/.exec(e)||/(webkit)[ \/]([\w.]+)/.exec(e)||/(opera)(?:.*version|)[ \/]([\w.]+)/.exec(e)||/(msie) ([\w.]+)/.exec(e)||e.indexOf("compatible")<0&&/(mozilla)(?:.*? rv:([\w.]+)|)/.exec(e)||[];return{browser:t[1]||"",version:t[2]||"0"}},e=v.uaMatch(o.userAgent),t={},e.browser&&(t[e.browser]=!0,t.version=e.version),t.chrome?t.webkit=!0:t.webkit&&(t.safari=!0),v.browser=t,v.sub=function(){function e(t,n){return new e.fn.init(t,n)}v.extend(!0,e,this),e.superclass=this,e.fn=e.prototype=this(),e.fn.constructor=e,e.sub=this.sub,e.fn.init=function(r,i){return i&&i instanceof v&&!(i instanceof e)&&(i=e(i)),v.fn.init.call(this,r,i,t)},e.fn.init.prototype=e.fn;var t=e(i);return e}}();var Dt,Pt,Ht,Bt=/alpha\([^)]*\)/i,jt=/opacity=([^)]*)/,Ft=/^(top|right|bottom|left)$/,It=/^(none|table(?!-c[ea]).+)/,qt=/^margin/,Rt=new RegExp("^("+m+")(.*)$","i"),Ut=new RegExp("^("+m+")(?!px)[a-z%]+$","i"),zt=new RegExp("^([-+])=("+m+")","i"),Wt={BODY:"block"},Xt={position:"absolute",visibility:"hidden",display:"block"},Vt={letterSpacing:0,fontWeight:400},$t=["Top","Right","Bottom","Left"],Jt=["Webkit","O","Moz","ms"],Kt=v.fn.toggle;v.fn.extend({css:function(e,n){return v.access(this,function(e,n,r){return r!==t?v.style(e,n,r):v.css(e,n)},e,n,arguments.length>1)},show:function(){return Yt(this,!0)},hide:function(){return Yt(this)},toggle:function(e,t){var n=typeof e=="boolean";return v.isFunction(e)&&v.isFunction(t)?Kt.apply(this,arguments):this.each(function(){(n?e:Gt(this))?v(this).show():v(this).hide()})}}),v.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Dt(e,"opacity");return n===""?"1":n}}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":v.support.cssFloat?"cssFloat":"styleFloat"},style:function(e,n,r,i){if(!e||e.nodeType===3||e.nodeType===8||!e.style)return;var s,o,u,a=v.camelCase(n),f=e.style;n=v.cssProps[a]||(v.cssProps[a]=Qt(f,a)),u=v.cssHooks[n]||v.cssHooks[a];if(r===t)return u&&"get"in u&&(s=u.get(e,!1,i))!==t?s:f[n];o=typeof r,o==="string"&&(s=zt.exec(r))&&(r=(s[1]+1)*s[2]+parseFloat(v.css(e,n)),o="number");if(r==null||o==="number"&&isNaN(r))return;o==="number"&&!v.cssNumber[a]&&(r+="px");if(!u||!("set"in u)||(r=u.set(e,r,i))!==t)try{f[n]=r}catch(l){}},css:function(e,n,r,i){var s,o,u,a=v.camelCase(n);return n=v.cssProps[a]||(v.cssProps[a]=Qt(e.style,a)),u=v.cssHooks[n]||v.cssHooks[a],u&&"get"in u&&(s=u.get(e,!0,i)),s===t&&(s=Dt(e,n)),s==="normal"&&n in Vt&&(s=Vt[n]),r||i!==t?(o=parseFloat(s),r||v.isNumeric(o)?o||0:s):s},swap:function(e,t,n){var r,i,s={};for(i in t)s[i]=e.style[i],e.style[i]=t[i];r=n.call(e);for(i in t)e.style[i]=s[i];return r}}),e.getComputedStyle?Dt=function(t,n){var r,i,s,o,u=e.getComputedStyle(t,null),a=t.style;return u&&(r=u.getPropertyValue(n)||u[n],r===""&&!v.contains(t.ownerDocument,t)&&(r=v.style(t,n)),Ut.test(r)&&qt.test(n)&&(i=a.width,s=a.minWidth,o=a.maxWidth,a.minWidth=a.maxWidth=a.width=r,r=u.width,a.width=i,a.minWidth=s,a.maxWidth=o)),r}:i.documentElement.currentStyle&&(Dt=function(e,t){var n,r,i=e.currentStyle&&e.currentStyle[t],s=e.style;return i==null&&s&&s[t]&&(i=s[t]),Ut.test(i)&&!Ft.test(t)&&(n=s.left,r=e.runtimeStyle&&e.runtimeStyle.left,r&&(e.runtimeStyle.left=e.currentStyle.left),s.left=t==="fontSize"?"1em":i,i=s.pixelLeft+"px",s.left=n,r&&(e.runtimeStyle.left=r)),i===""?"auto":i}),v.each(["height","width"],function(e,t){v.cssHooks[t]={get:function(e,n,r){if(n)return e.offsetWidth===0&&It.test(Dt(e,"display"))?v.swap(e,Xt,function(){return tn(e,t,r)}):tn(e,t,r)},set:function(e,n,r){return Zt(e,n,r?en(e,t,r,v.support.boxSizing&&v.css(e,"boxSizing")==="border-box"):0)}}}),v.support.opacity||(v.cssHooks.opacity={get:function(e,t){return jt.test((t&&e.currentStyle?e.currentStyle.filter:e.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":t?"1":""},set:function(e,t){var n=e.style,r=e.currentStyle,i=v.isNumeric(t)?"alpha(opacity="+t*100+")":"",s=r&&r.filter||n.filter||"";n.zoom=1;if(t>=1&&v.trim(s.replace(Bt,""))===""&&n.removeAttribute){n.removeAttribute("filter");if(r&&!r.filter)return}n.filter=Bt.test(s)?s.replace(Bt,i):s+" "+i}}),v(function(){v.support.reliableMarginRight||(v.cssHooks.marginRight={get:function(e,t){return v.swap(e,{display:"inline-block"},function(){if(t)return Dt(e,"marginRight")})}}),!v.support.pixelPosition&&v.fn.position&&v.each(["top","left"],function(e,t){v.cssHooks[t]={get:function(e,n){if(n){var r=Dt(e,t);return Ut.test(r)?v(e).position()[t]+"px":r}}}})}),v.expr&&v.expr.filters&&(v.expr.filters.hidden=function(e){return e.offsetWidth===0&&e.offsetHeight===0||!v.support.reliableHiddenOffsets&&(e.style&&e.style.display||Dt(e,"display"))==="none"},v.expr.filters.visible=function(e){return!v.expr.filters.hidden(e)}),v.each({margin:"",padding:"",border:"Width"},function(e,t){v.cssHooks[e+t]={expand:function(n){var r,i=typeof n=="string"?n.split(" "):[n],s={};for(r=0;r<4;r++)s[e+$t[r]+t]=i[r]||i[r-2]||i[0];return s}},qt.test(e)||(v.cssHooks[e+t].set=Zt)});var rn=/%20/g,sn=/\[\]$/,on=/\r?\n/g,un=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,an=/^(?:select|textarea)/i;v.fn.extend({serialize:function(){return v.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?v.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||an.test(this.nodeName)||un.test(this.type))}).map(function(e,t){var n=v(this).val();return n==null?null:v.isArray(n)?v.map(n,function(e,n){return{name:t.name,value:e.replace(on,"\r\n")}}):{name:t.name,value:n.replace(on,"\r\n")}}).get()}}),v.param=function(e,n){var r,i=[],s=function(e,t){t=v.isFunction(t)?t():t==null?"":t,i[i.length]=encodeURIComponent(e)+"="+encodeURIComponent(t)};n===t&&(n=v.ajaxSettings&&v.ajaxSettings.traditional);if(v.isArray(e)||e.jquery&&!v.isPlainObject(e))v.each(e,function(){s(this.name,this.value)});else for(r in e)fn(r,e[r],n,s);return i.join("&").replace(rn,"+")};var ln,cn,hn=/#.*$/,pn=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,dn=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,vn=/^(?:GET|HEAD)$/,mn=/^\/\//,gn=/\?/,yn=/)<[^<]*)*<\/script>/gi,bn=/([?&])_=[^&]*/,wn=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,En=v.fn.load,Sn={},xn={},Tn=["*/"]+["*"];try{cn=s.href}catch(Nn){cn=i.createElement("a"),cn.href="",cn=cn.href}ln=wn.exec(cn.toLowerCase())||[],v.fn.load=function(e,n,r){if(typeof e!="string"&&En)return En.apply(this,arguments);if(!this.length)return this;var i,s,o,u=this,a=e.indexOf(" ");return a>=0&&(i=e.slice(a,e.length),e=e.slice(0,a)),v.isFunction(n)?(r=n,n=t):n&&typeof n=="object"&&(s="POST"),v.ajax({url:e,type:s,dataType:"html",data:n,complete:function(e,t){r&&u.each(r,o||[e.responseText,t,e])}}).done(function(e){o=arguments,u.html(i?v("
").append(e.replace(yn,"")).find(i):e)}),this},v.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(e,t){v.fn[t]=function(e){return this.on(t,e)}}),v.each(["get","post"],function(e,n){v[n]=function(e,r,i,s){return v.isFunction(r)&&(s=s||i,i=r,r=t),v.ajax({type:n,url:e,data:r,success:i,dataType:s})}}),v.extend({getScript:function(e,n){return v.get(e,t,n,"script")},getJSON:function(e,t,n){return v.get(e,t,n,"json")},ajaxSetup:function(e,t){return t?Ln(e,v.ajaxSettings):(t=e,e=v.ajaxSettings),Ln(e,t),e},ajaxSettings:{url:cn,isLocal:dn.test(ln[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded; charset=UTF-8",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":Tn},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":e.String,"text html":!0,"text json":v.parseJSON,"text xml":v.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:Cn(Sn),ajaxTransport:Cn(xn),ajax:function(e,n){function T(e,n,s,a){var l,y,b,w,S,T=n;if(E===2)return;E=2,u&&clearTimeout(u),o=t,i=a||"",x.readyState=e>0?4:0,s&&(w=An(c,x,s));if(e>=200&&e<300||e===304)c.ifModified&&(S=x.getResponseHeader("Last-Modified"),S&&(v.lastModified[r]=S),S=x.getResponseHeader("Etag"),S&&(v.etag[r]=S)),e===304?(T="notmodified",l=!0):(l=On(c,w),T=l.state,y=l.data,b=l.error,l=!b);else{b=T;if(!T||e)T="error",e<0&&(e=0)}x.status=e,x.statusText=(n||T)+"",l?d.resolveWith(h,[y,T,x]):d.rejectWith(h,[x,T,b]),x.statusCode(g),g=t,f&&p.trigger("ajax"+(l?"Success":"Error"),[x,c,l?y:b]),m.fireWith(h,[x,T]),f&&(p.trigger("ajaxComplete",[x,c]),--v.active||v.event.trigger("ajaxStop"))}typeof e=="object"&&(n=e,e=t),n=n||{};var r,i,s,o,u,a,f,l,c=v.ajaxSetup({},n),h=c.context||c,p=h!==c&&(h.nodeType||h instanceof v)?v(h):v.event,d=v.Deferred(),m=v.Callbacks("once memory"),g=c.statusCode||{},b={},w={},E=0,S="canceled",x={readyState:0,setRequestHeader:function(e,t){if(!E){var n=e.toLowerCase();e=w[n]=w[n]||e,b[e]=t}return this},getAllResponseHeaders:function(){return E===2?i:null},getResponseHeader:function(e){var n;if(E===2){if(!s){s={};while(n=pn.exec(i))s[n[1].toLowerCase()]=n[2]}n=s[e.toLowerCase()]}return n===t?null:n},overrideMimeType:function(e){return E||(c.mimeType=e),this},abort:function(e){return e=e||S,o&&o.abort(e),T(0,e),this}};d.promise(x),x.success=x.done,x.error=x.fail,x.complete=m.add,x.statusCode=function(e){if(e){var t;if(E<2)for(t in e)g[t]=[g[t],e[t]];else t=e[x.status],x.always(t)}return this},c.url=((e||c.url)+"").replace(hn,"").replace(mn,ln[1]+"//"),c.dataTypes=v.trim(c.dataType||"*").toLowerCase().split(y),c.crossDomain==null&&(a=wn.exec(c.url.toLowerCase()),c.crossDomain=!(!a||a[1]===ln[1]&&a[2]===ln[2]&&(a[3]||(a[1]==="http:"?80:443))==(ln[3]||(ln[1]==="http:"?80:443)))),c.data&&c.processData&&typeof c.data!="string"&&(c.data=v.param(c.data,c.traditional)),kn(Sn,c,n,x);if(E===2)return x;f=c.global,c.type=c.type.toUpperCase(),c.hasContent=!vn.test(c.type),f&&v.active++===0&&v.event.trigger("ajaxStart");if(!c.hasContent){c.data&&(c.url+=(gn.test(c.url)?"&":"?")+c.data,delete c.data),r=c.url;if(c.cache===!1){var N=v.now(),C=c.url.replace(bn,"$1_="+N);c.url=C+(C===c.url?(gn.test(c.url)?"&":"?")+"_="+N:"")}}(c.data&&c.hasContent&&c.contentType!==!1||n.contentType)&&x.setRequestHeader("Content-Type",c.contentType),c.ifModified&&(r=r||c.url,v.lastModified[r]&&x.setRequestHeader("If-Modified-Since",v.lastModified[r]),v.etag[r]&&x.setRequestHeader("If-None-Match",v.etag[r])),x.setRequestHeader("Accept",c.dataTypes[0]&&c.accepts[c.dataTypes[0]]?c.accepts[c.dataTypes[0]]+(c.dataTypes[0]!=="*"?", "+Tn+"; q=0.01":""):c.accepts["*"]);for(l in c.headers)x.setRequestHeader(l,c.headers[l]);if(!c.beforeSend||c.beforeSend.call(h,x,c)!==!1&&E!==2){S="abort";for(l in{success:1,error:1,complete:1})x[l](c[l]);o=kn(xn,c,n,x);if(!o)T(-1,"No Transport");else{x.readyState=1,f&&p.trigger("ajaxSend",[x,c]),c.async&&c.timeout>0&&(u=setTimeout(function(){x.abort("timeout")},c.timeout));try{E=1,o.send(b,T)}catch(k){if(!(E<2))throw k;T(-1,k)}}return x}return x.abort()},active:0,lastModified:{},etag:{}});var Mn=[],_n=/\?/,Dn=/(=)\?(?=&|$)|\?\?/,Pn=v.now();v.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Mn.pop()||v.expando+"_"+Pn++;return this[e]=!0,e}}),v.ajaxPrefilter("json jsonp",function(n,r,i){var s,o,u,a=n.data,f=n.url,l=n.jsonp!==!1,c=l&&Dn.test(f),h=l&&!c&&typeof a=="string"&&!(n.contentType||"").indexOf("application/x-www-form-urlencoded")&&Dn.test(a);if(n.dataTypes[0]==="jsonp"||c||h)return s=n.jsonpCallback=v.isFunction(n.jsonpCallback)?n.jsonpCallback():n.jsonpCallback,o=e[s],c?n.url=f.replace(Dn,"$1"+s):h?n.data=a.replace(Dn,"$1"+s):l&&(n.url+=(_n.test(f)?"&":"?")+n.jsonp+"="+s),n.converters["script json"]=function(){return u||v.error(s+" was not called"),u[0]},n.dataTypes[0]="json",e[s]=function(){u=arguments},i.always(function(){e[s]=o,n[s]&&(n.jsonpCallback=r.jsonpCallback,Mn.push(s)),u&&v.isFunction(o)&&o(u[0]),u=o=t}),"script"}),v.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(e){return v.globalEval(e),e}}}),v.ajaxPrefilter("script",function(e){e.cache===t&&(e.cache=!1),e.crossDomain&&(e.type="GET",e.global=!1)}),v.ajaxTransport("script",function(e){if(e.crossDomain){var n,r=i.head||i.getElementsByTagName("head")[0]||i.documentElement;return{send:function(s,o){n=i.createElement("script"),n.async="async",e.scriptCharset&&(n.charset=e.scriptCharset),n.src=e.url,n.onload=n.onreadystatechange=function(e,i){if(i||!n.readyState||/loaded|complete/.test(n.readyState))n.onload=n.onreadystatechange=null,r&&n.parentNode&&r.removeChild(n),n=t,i||o(200,"success")},r.insertBefore(n,r.firstChild)},abort:function(){n&&n.onload(0,1)}}}});var Hn,Bn=e.ActiveXObject?function(){for(var e in Hn)Hn[e](0,1)}:!1,jn=0;v.ajaxSettings.xhr=e.ActiveXObject?function(){return!this.isLocal&&Fn()||In()}:Fn,function(e){v.extend(v.support,{ajax:!!e,cors:!!e&&"withCredentials"in e})}(v.ajaxSettings.xhr()),v.support.ajax&&v.ajaxTransport(function(n){if(!n.crossDomain||v.support.cors){var r;return{send:function(i,s){var o,u,a=n.xhr();n.username?a.open(n.type,n.url,n.async,n.username,n.password):a.open(n.type,n.url,n.async);if(n.xhrFields)for(u in n.xhrFields)a[u]=n.xhrFields[u];n.mimeType&&a.overrideMimeType&&a.overrideMimeType(n.mimeType),!n.crossDomain&&!i["X-Requested-With"]&&(i["X-Requested-With"]="XMLHttpRequest");try{for(u in i)a.setRequestHeader(u,i[u])}catch(f){}a.send(n.hasContent&&n.data||null),r=function(e,i){var u,f,l,c,h;try{if(r&&(i||a.readyState===4)){r=t,o&&(a.onreadystatechange=v.noop,Bn&&delete Hn[o]);if(i)a.readyState!==4&&a.abort();else{u=a.status,l=a.getAllResponseHeaders(),c={},h=a.responseXML,h&&h.documentElement&&(c.xml=h);try{c.text=a.responseText}catch(p){}try{f=a.statusText}catch(p){f=""}!u&&n.isLocal&&!n.crossDomain?u=c.text?200:404:u===1223&&(u=204)}}}catch(d){i||s(-1,d)}c&&s(u,f,c,l)},n.async?a.readyState===4?setTimeout(r,0):(o=++jn,Bn&&(Hn||(Hn={},v(e).unload(Bn)),Hn[o]=r),a.onreadystatechange=r):r()},abort:function(){r&&r(0,1)}}}});var qn,Rn,Un=/^(?:toggle|show|hide)$/,zn=new RegExp("^(?:([-+])=|)("+m+")([a-z%]*)$","i"),Wn=/queueHooks$/,Xn=[Gn],Vn={"*":[function(e,t){var n,r,i=this.createTween(e,t),s=zn.exec(t),o=i.cur(),u=+o||0,a=1,f=20;if(s){n=+s[2],r=s[3]||(v.cssNumber[e]?"":"px");if(r!=="px"&&u){u=v.css(i.elem,e,!0)||n||1;do a=a||".5",u/=a,v.style(i.elem,e,u+r);while(a!==(a=i.cur()/o)&&a!==1&&--f)}i.unit=r,i.start=u,i.end=s[1]?u+(s[1]+1)*n:n}return i}]};v.Animation=v.extend(Kn,{tweener:function(e,t){v.isFunction(e)?(t=e,e=["*"]):e=e.split(" ");var n,r=0,i=e.length;for(;r-1,f={},l={},c,h;a?(l=i.position(),c=l.top,h=l.left):(c=parseFloat(o)||0,h=parseFloat(u)||0),v.isFunction(t)&&(t=t.call(e,n,s)),t.top!=null&&(f.top=t.top-s.top+c),t.left!=null&&(f.left=t.left-s.left+h),"using"in t?t.using.call(e,f):i.css(f)}},v.fn.extend({position:function(){if(!this[0])return;var e=this[0],t=this.offsetParent(),n=this.offset(),r=er.test(t[0].nodeName)?{top:0,left:0}:t.offset();return n.top-=parseFloat(v.css(e,"marginTop"))||0,n.left-=parseFloat(v.css(e,"marginLeft"))||0,r.top+=parseFloat(v.css(t[0],"borderTopWidth"))||0,r.left+=parseFloat(v.css(t[0],"borderLeftWidth"))||0,{top:n.top-r.top,left:n.left-r.left}},offsetParent:function(){return this.map(function(){var e=this.offsetParent||i.body;while(e&&!er.test(e.nodeName)&&v.css(e,"position")==="static")e=e.offsetParent;return e||i.body})}}),v.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(e,n){var r=/Y/.test(n);v.fn[e]=function(i){return v.access(this,function(e,i,s){var o=tr(e);if(s===t)return o?n in o?o[n]:o.document.documentElement[i]:e[i];o?o.scrollTo(r?v(o).scrollLeft():s,r?s:v(o).scrollTop()):e[i]=s},e,i,arguments.length,null)}}),v.each({Height:"height",Width:"width"},function(e,n){v.each({padding:"inner"+e,content:n,"":"outer"+e},function(r,i){v.fn[i]=function(i,s){var o=arguments.length&&(r||typeof i!="boolean"),u=r||(i===!0||s===!0?"margin":"border");return v.access(this,function(n,r,i){var s;return v.isWindow(n)?n.document.documentElement["client"+e]:n.nodeType===9?(s=n.documentElement,Math.max(n.body["scroll"+e],s["scroll"+e],n.body["offset"+e],s["offset"+e],s["client"+e])):i===t?v.css(n,r,i,u):v.style(n,r,i,u)},n,o?i:t,o,null)}})}),e.jQuery=e.$=v,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return v})})(window); -------------------------------------------------------------------------------- /demo/html/js/websocket.js: -------------------------------------------------------------------------------- 1 | var _websocket ={ 2 | option: { 3 | websocket: window.WebSocket || window.MozWebSocket, 4 | websockets: '', 5 | _websocket_des: '', 6 | url: '' 7 | }, 8 | init: function(url,send,message){ 9 | if(this.option.websocket){ 10 | window.onbeforeunload = function(){ 11 | _websocket.option._websocket_des.close(); 12 | } 13 | this.option.url=url; 14 | if(send && Object.prototype.toString.call(send)=="[object Function]") this.option.send=send; 15 | if(message && Object.prototype.toString.call(message)=="[object Function]") this.option.message=message; 16 | if(this.option.message && this.option.send){ 17 | return this.main(url,send,message); 18 | }else if(this.option.message){ 19 | return this.main(url,false,message); 20 | }else if(this.option.send){ 21 | return this.main(url,send,false); 22 | }else{ 23 | return this.main(url); 24 | } 25 | 26 | }else{ 27 | return '当前设备不支持' 28 | } 29 | }, 30 | doopen:function(){ 31 | console.log('连接成功') 32 | }, 33 | doclose:function(){ 34 | console.log('连接失败'); 35 | _websocket.option._websocket_des.close(); 36 | }, 37 | doError:function(){ 38 | console.log('连接出错'); 39 | }, 40 | doMessage:function(message){ 41 | console.log('收到message='+message); 42 | }, 43 | doSend:function(message){ 44 | if(_websocket.option.websockets.readyState == 1) { 45 | _websocket.option.websockets.send(JSON.stringify(message)); 46 | }else { 47 | _websocket.Reconnect(); 48 | console.log("您已经掉线,无法与服务器通信!"); 49 | } 50 | }, 51 | Reconnect:function(){ 52 | return _websocket.main(_websocket.option.url,_websocket.option.send,_websocket.option.message); 53 | }, 54 | main:function(wcUrl,send,message){ 55 | this.option.websockets = new this.option.websocket(encodeURI(wcUrl)); 56 | this.option.websockets.onopen = this.doopen;//打开 57 | this.option.websockets.onerror = this.doError;//出错 58 | this.option.websockets.onclose = this.doclose;//关闭 59 | this.option.websockets.onmessage = (message || this.doMessage);//接收 60 | this.option.websockets.onSend = (send || this.doSend);//发送 61 | this.option.websockets.Reconnect = this.Reconnect;//重新连接 62 | this.option._websocket_des = this.option.websockets; 63 | return this.option.websockets; 64 | } 65 | } -------------------------------------------------------------------------------- /demo/img/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YangYouWang/springboot-starter-im/9b764d978fcd7e824aac168de0b1eaf12165b8f5/demo/img/1.jpg -------------------------------------------------------------------------------- /demo/img/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YangYouWang/springboot-starter-im/9b764d978fcd7e824aac168de0b1eaf12165b8f5/demo/img/2.jpg -------------------------------------------------------------------------------- /demo/img/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YangYouWang/springboot-starter-im/9b764d978fcd7e824aac168de0b1eaf12165b8f5/demo/img/3.jpg -------------------------------------------------------------------------------- /demo/img/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YangYouWang/springboot-starter-im/9b764d978fcd7e824aac168de0b1eaf12165b8f5/demo/img/4.jpg -------------------------------------------------------------------------------- /demo/img/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YangYouWang/springboot-starter-im/9b764d978fcd7e824aac168de0b1eaf12165b8f5/demo/img/5.jpg -------------------------------------------------------------------------------- /demo/img/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YangYouWang/springboot-starter-im/9b764d978fcd7e824aac168de0b1eaf12165b8f5/demo/img/6.jpg -------------------------------------------------------------------------------- /demo/img/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YangYouWang/springboot-starter-im/9b764d978fcd7e824aac168de0b1eaf12165b8f5/demo/img/7.jpg -------------------------------------------------------------------------------- /demo/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.4.3 9 | 10 | 11 | com.example 12 | demo 13 | 0.0.1-SNAPSHOT 14 | demo 15 | Demo project for Spring Boot 16 | 17 | 1.8 18 | 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-starter 23 | 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-test 28 | test 29 | 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-web 34 | 35 | 36 | 37 | 38 | io.github.yangyouwang 39 | springboot-starter-im 40 | 0.0.1-SNAPSHOT 41 | 42 | 43 | 44 | 45 | 46 | 47 | org.springframework.boot 48 | spring-boot-maven-plugin 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /demo/src/main/java/com/example/demo/ChatMsgListener.java: -------------------------------------------------------------------------------- 1 | package com.example.demo; 2 | 3 | import io.github.yangyouwang.springbootstarterim.bean.DataContentEvent; 4 | import io.github.yangyouwang.springbootstarterim.constant.MsgActionEnum; 5 | import org.springframework.context.event.EventListener; 6 | import org.springframework.stereotype.Component; 7 | 8 | /** 9 | * @author yangyouwang 10 | * @title: ChatMsgListener 11 | * @projectName oa 12 | * @description: 监听消息 13 | * @date 2021/3/1211:01 AM 14 | */ 15 | @Component 16 | public class ChatMsgListener { 17 | 18 | @EventListener 19 | public void getData(DataContentEvent dataContentEvent) { 20 | MsgActionEnum action = dataContentEvent.getAction(); 21 | switch (action) { 22 | case CHAT: 23 | System.out.println("聊天消息:" + dataContentEvent); 24 | break; 25 | case SIGNED: 26 | System.out.println("消息签收:"+dataContentEvent); 27 | break; 28 | } 29 | 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /demo/src/main/java/com/example/demo/DemoApplication.java: -------------------------------------------------------------------------------- 1 | package com.example.demo; 2 | 3 | import io.github.yangyouwang.springbootstarterim.NettyBooter; 4 | import org.springframework.boot.ApplicationArguments; 5 | import org.springframework.boot.ApplicationRunner; 6 | import org.springframework.boot.SpringApplication; 7 | import org.springframework.boot.autoconfigure.SpringBootApplication; 8 | 9 | import javax.annotation.Resource; 10 | 11 | @SpringBootApplication 12 | public class DemoApplication implements ApplicationRunner { 13 | 14 | @Resource 15 | private NettyBooter nettyBooter; 16 | 17 | public static void main(String[] args) { 18 | SpringApplication.run(DemoApplication.class, args); 19 | } 20 | 21 | @Override 22 | public void run(ApplicationArguments args) { 23 | nettyBooter.start(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /demo/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | im: 2 | netty: 3 | port: 8888 # 配置聊天端口 4 | path: /im/ws -------------------------------------------------------------------------------- /demo/src/test/java/com/example/demo/DemoApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.example.demo; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class DemoApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | fi 118 | 119 | if [ -z "$JAVA_HOME" ]; then 120 | javaExecutable="`which javac`" 121 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 122 | # readlink(1) is not available as standard on Solaris 10. 123 | readLink=`which readlink` 124 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 125 | if $darwin ; then 126 | javaHome="`dirname \"$javaExecutable\"`" 127 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 128 | else 129 | javaExecutable="`readlink -f \"$javaExecutable\"`" 130 | fi 131 | javaHome="`dirname \"$javaExecutable\"`" 132 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 133 | JAVA_HOME="$javaHome" 134 | export JAVA_HOME 135 | fi 136 | fi 137 | fi 138 | 139 | if [ -z "$JAVACMD" ] ; then 140 | if [ -n "$JAVA_HOME" ] ; then 141 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 142 | # IBM's JDK on AIX uses strange locations for the executables 143 | JAVACMD="$JAVA_HOME/jre/sh/java" 144 | else 145 | JAVACMD="$JAVA_HOME/bin/java" 146 | fi 147 | else 148 | JAVACMD="`which java`" 149 | fi 150 | fi 151 | 152 | if [ ! -x "$JAVACMD" ] ; then 153 | echo "Error: JAVA_HOME is not defined correctly." >&2 154 | echo " We cannot execute $JAVACMD" >&2 155 | exit 1 156 | fi 157 | 158 | if [ -z "$JAVA_HOME" ] ; then 159 | echo "Warning: JAVA_HOME environment variable is not set." 160 | fi 161 | 162 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 163 | 164 | # traverses directory structure from process work directory to filesystem root 165 | # first directory with .mvn subdirectory is considered project base directory 166 | find_maven_basedir() { 167 | 168 | if [ -z "$1" ] 169 | then 170 | echo "Path not specified to find_maven_basedir" 171 | return 1 172 | fi 173 | 174 | basedir="$1" 175 | wdir="$1" 176 | while [ "$wdir" != '/' ] ; do 177 | if [ -d "$wdir"/.mvn ] ; then 178 | basedir=$wdir 179 | break 180 | fi 181 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 182 | if [ -d "${wdir}" ]; then 183 | wdir=`cd "$wdir/.."; pwd` 184 | fi 185 | # end of workaround 186 | done 187 | echo "${basedir}" 188 | } 189 | 190 | # concatenates all lines of a file 191 | concat_lines() { 192 | if [ -f "$1" ]; then 193 | echo "$(tr -s '\n' ' ' < "$1")" 194 | fi 195 | } 196 | 197 | BASE_DIR=`find_maven_basedir "$(pwd)"` 198 | if [ -z "$BASE_DIR" ]; then 199 | exit 1; 200 | fi 201 | 202 | ########################################################################################## 203 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 204 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 205 | ########################################################################################## 206 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 207 | if [ "$MVNW_VERBOSE" = true ]; then 208 | echo "Found .mvn/wrapper/maven-wrapper.jar" 209 | fi 210 | else 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 213 | fi 214 | if [ -n "$MVNW_REPOURL" ]; then 215 | jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 216 | else 217 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 218 | fi 219 | while IFS="=" read key value; do 220 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 221 | esac 222 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 223 | if [ "$MVNW_VERBOSE" = true ]; then 224 | echo "Downloading from: $jarUrl" 225 | fi 226 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 227 | if $cygwin; then 228 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 229 | fi 230 | 231 | if command -v wget > /dev/null; then 232 | if [ "$MVNW_VERBOSE" = true ]; then 233 | echo "Found wget ... using wget" 234 | fi 235 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 236 | wget "$jarUrl" -O "$wrapperJarPath" 237 | else 238 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" 239 | fi 240 | elif command -v curl > /dev/null; then 241 | if [ "$MVNW_VERBOSE" = true ]; then 242 | echo "Found curl ... using curl" 243 | fi 244 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 245 | curl -o "$wrapperJarPath" "$jarUrl" -f 246 | else 247 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 248 | fi 249 | 250 | else 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo "Falling back to using Java to download" 253 | fi 254 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 255 | # For Cygwin, switch paths to Windows format before running javac 256 | if $cygwin; then 257 | javaClass=`cygpath --path --windows "$javaClass"` 258 | fi 259 | if [ -e "$javaClass" ]; then 260 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 261 | if [ "$MVNW_VERBOSE" = true ]; then 262 | echo " - Compiling MavenWrapperDownloader.java ..." 263 | fi 264 | # Compiling the Java class 265 | ("$JAVA_HOME/bin/javac" "$javaClass") 266 | fi 267 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 268 | # Running the downloader 269 | if [ "$MVNW_VERBOSE" = true ]; then 270 | echo " - Running MavenWrapperDownloader.java ..." 271 | fi 272 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 273 | fi 274 | fi 275 | fi 276 | fi 277 | ########################################################################################## 278 | # End of extension 279 | ########################################################################################## 280 | 281 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 282 | if [ "$MVNW_VERBOSE" = true ]; then 283 | echo $MAVEN_PROJECTBASEDIR 284 | fi 285 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 286 | 287 | # For Cygwin, switch paths to Windows format before running java 288 | if $cygwin; then 289 | [ -n "$M2_HOME" ] && 290 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 291 | [ -n "$JAVA_HOME" ] && 292 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 293 | [ -n "$CLASSPATH" ] && 294 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 295 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 296 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 297 | fi 298 | 299 | # Provide a "standardized" way to retrieve the CLI args that will 300 | # work with both Windows and non-Windows executions. 301 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 302 | export MAVEN_CMD_LINE_ARGS 303 | 304 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 305 | 306 | exec "$JAVACMD" \ 307 | $MAVEN_OPTS \ 308 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 309 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 310 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 311 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 124 | 125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 162 | if ERRORLEVEL 1 goto error 163 | goto end 164 | 165 | :error 166 | set ERROR_CODE=1 167 | 168 | :end 169 | @endlocal & set ERROR_CODE=%ERROR_CODE% 170 | 171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 175 | :skipRcPost 176 | 177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 179 | 180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 181 | 182 | exit /B %ERROR_CODE% 183 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | io.github.yangyouwang 8 | springboot-starter-im 9 | 0.0.1-SNAPSHOT 10 | springboot-starter-im 11 | 聊天starter 12 | 13 | 14 | 1.8 15 | 16 | 17 | 18 | 19 | org.springframework.boot 20 | spring-boot-configuration-processor 21 | 2.2.4.RELEASE 22 | 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-autoconfigure 27 | 2.2.4.RELEASE 28 | 29 | 30 | 31 | 32 | io.netty 33 | netty-all 34 | 4.1.50.Final 35 | 36 | 37 | 38 | 39 | com.alibaba 40 | fastjson 41 | 1.2.47 42 | 43 | 44 | 45 | 46 | 47 | 48 | org.apache.maven.plugins 49 | maven-compiler-plugin 50 | 51 | 8 52 | 8 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/main/java/io/github/yangyouwang/springbootstarterim/NettyBooter.java: -------------------------------------------------------------------------------- 1 | package io.github.yangyouwang.springbootstarterim; 2 | 3 | import io.github.yangyouwang.springbootstarterim.config.NettyProperties; 4 | import io.netty.bootstrap.ServerBootstrap; 5 | import io.netty.channel.ChannelFuture; 6 | import org.springframework.stereotype.Component; 7 | 8 | import javax.annotation.Resource; 9 | 10 | 11 | /** 12 | * @author yangyouwang 13 | * @title: NettyBooter 14 | * @projectName springboot-starter-im 15 | * @description: 启动类 16 | * @date 2021/3/123:02 PM 17 | */ 18 | @Component 19 | public class NettyBooter { 20 | 21 | @Resource 22 | private ServerBootstrap server; 23 | 24 | @Resource 25 | private NettyProperties properties; 26 | 27 | public void start() { 28 | try { 29 | if (null == properties.getPort()) { 30 | throw new RuntimeException("未设置启动端口~"); 31 | } 32 | if (null == properties.getPath()) { 33 | throw new RuntimeException("未设置url路径口~"); 34 | } 35 | ChannelFuture future = server.bind(properties.getPort()).sync(); 36 | if (future.isSuccess()) { 37 | System.out.println("启动 Netty 成功"); 38 | } 39 | } catch (InterruptedException e) { 40 | e.printStackTrace(); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/io/github/yangyouwang/springbootstarterim/bean/ChatMsg.java: -------------------------------------------------------------------------------- 1 | package io.github.yangyouwang.springbootstarterim.bean; 2 | 3 | 4 | import java.io.Serializable; 5 | 6 | /** 7 | * 聊天bean 8 | * @author yangyouwang 9 | */ 10 | public class ChatMsg implements Serializable { 11 | 12 | /** 发送者id **/ 13 | private Long fromUserId; 14 | 15 | /** 接收者id **/ 16 | private Long toUserId; 17 | 18 | /** 消息内容 **/ 19 | private String message; 20 | /** 21 | * 类型 0:文字 1:图片 22 | */ 23 | private Integer type = 0; 24 | /** 25 | * 状态 0:未读 1:已读 26 | */ 27 | private Integer status = 0 ; 28 | 29 | public Long getFromUserId() { 30 | return fromUserId; 31 | } 32 | 33 | public void setFromUserId(Long fromUserId) { 34 | this.fromUserId = fromUserId; 35 | } 36 | 37 | public Long getToUserId() { 38 | return toUserId; 39 | } 40 | 41 | public void setToUserId(Long toUserId) { 42 | this.toUserId = toUserId; 43 | } 44 | 45 | public String getMessage() { 46 | return message; 47 | } 48 | 49 | public void setMessage(String message) { 50 | this.message = message; 51 | } 52 | 53 | public Integer getType() { 54 | return type; 55 | } 56 | 57 | public void setType(Integer type) { 58 | this.type = type; 59 | } 60 | 61 | public Integer getStatus() { 62 | return status; 63 | } 64 | 65 | public void setStatus(Integer status) { 66 | this.status = status; 67 | } 68 | 69 | @Override 70 | public String toString() { 71 | return "ChatMsg{" + 72 | "fromUserId=" + fromUserId + 73 | ", toUserId=" + toUserId + 74 | ", message='" + message + '\'' + 75 | ", type=" + type + 76 | ", status=" + status + 77 | '}'; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/io/github/yangyouwang/springbootstarterim/bean/DataContent.java: -------------------------------------------------------------------------------- 1 | package io.github.yangyouwang.springbootstarterim.bean; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * 聊天数据 7 | * 8 | * @author yangyouwang 9 | */ 10 | public class DataContent implements Serializable { 11 | /** 12 | * 动作类型 13 | */ 14 | private Integer action; 15 | /** 16 | * 消息内容 17 | */ 18 | private ChatMsg chatMsg; 19 | 20 | public Integer getAction() { 21 | return action; 22 | } 23 | 24 | public void setAction(Integer action) { 25 | this.action = action; 26 | } 27 | 28 | public ChatMsg getChatMsg() { 29 | return chatMsg; 30 | } 31 | 32 | public void setChatMsg(ChatMsg chatMsg) { 33 | this.chatMsg = chatMsg; 34 | } 35 | 36 | @Override 37 | public String toString() { 38 | return "DataContent{" + 39 | "action=" + action + 40 | ", chatMsg=" + chatMsg + 41 | '}'; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/io/github/yangyouwang/springbootstarterim/bean/DataContentEvent.java: -------------------------------------------------------------------------------- 1 | package io.github.yangyouwang.springbootstarterim.bean; 2 | 3 | import io.github.yangyouwang.springbootstarterim.constant.MsgActionEnum; 4 | import io.github.yangyouwang.springbootstarterim.constant.MsgStatusEnum; 5 | import io.github.yangyouwang.springbootstarterim.constant.MsgTypeEnum; 6 | import org.springframework.context.ApplicationEvent; 7 | 8 | /** 9 | * @author yangyouwang 10 | * @title: DataContentEvent 11 | * @projectName springboot-starter-im 12 | * @description: 聊天数据事件 13 | * @date 2021/3/129:43 AM 14 | */ 15 | public class DataContentEvent extends ApplicationEvent { 16 | 17 | /** 发送者id **/ 18 | private Long fromUserId; 19 | 20 | /** 接收者id **/ 21 | private Long toUserId; 22 | 23 | /** 消息内容 **/ 24 | private String message; 25 | /** 26 | * 动作类型 27 | */ 28 | private MsgActionEnum action; 29 | /** 30 | * 类型 0:文字 1:图片 31 | */ 32 | private MsgTypeEnum type; 33 | /** 34 | * 状态 0:未读 1:已读 35 | */ 36 | private MsgStatusEnum status; 37 | 38 | public DataContentEvent(Object source) { 39 | super(source); 40 | } 41 | 42 | public Long getFromUserId() { 43 | return fromUserId; 44 | } 45 | 46 | public void setFromUserId(Long fromUserId) { 47 | this.fromUserId = fromUserId; 48 | } 49 | 50 | public Long getToUserId() { 51 | return toUserId; 52 | } 53 | 54 | public void setToUserId(Long toUserId) { 55 | this.toUserId = toUserId; 56 | } 57 | 58 | public String getMessage() { 59 | return message; 60 | } 61 | 62 | public void setMessage(String message) { 63 | this.message = message; 64 | } 65 | 66 | public MsgActionEnum getAction() { 67 | return action; 68 | } 69 | 70 | public void setAction(MsgActionEnum action) { 71 | this.action = action; 72 | } 73 | 74 | public MsgTypeEnum getType() { 75 | return type; 76 | } 77 | 78 | public void setType(MsgTypeEnum type) { 79 | this.type = type; 80 | } 81 | 82 | public MsgStatusEnum getStatus() { 83 | return status; 84 | } 85 | 86 | public void setStatus(MsgStatusEnum status) { 87 | this.status = status; 88 | } 89 | 90 | @Override 91 | public String toString() { 92 | return "DataContentEvent{" + 93 | "fromUserId=" + fromUserId + 94 | ", toUserId=" + toUserId + 95 | ", message='" + message + '\'' + 96 | ", action=" + action + 97 | ", type=" + type + 98 | ", status=" + status + 99 | '}'; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/io/github/yangyouwang/springbootstarterim/config/ChatConfig.java: -------------------------------------------------------------------------------- 1 | package io.github.yangyouwang.springbootstarterim.config; 2 | 3 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 4 | import org.springframework.context.annotation.ComponentScan; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.context.annotation.Import; 7 | 8 | /** 9 | * @author yangyouwang 10 | * @title: ChatConfig 11 | * @projectName springboot-starter-im 12 | * @description: 系统配置 13 | * @date 2021/3/173:38 PM 14 | */ 15 | @Configuration 16 | @Import({NettyConfig.class,MsgConfig.class}) 17 | @EnableConfigurationProperties(NettyProperties.class) 18 | @ComponentScan(basePackages = "io.github.yangyouwang.springbootstarterim") 19 | public class ChatConfig { 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/io/github/yangyouwang/springbootstarterim/config/MsgConfig.java: -------------------------------------------------------------------------------- 1 | package io.github.yangyouwang.springbootstarterim.config; 2 | 3 | import io.github.yangyouwang.springbootstarterim.constant.MsgActionEnum; 4 | import io.github.yangyouwang.springbootstarterim.strategy.MsgStrategy; 5 | import io.github.yangyouwang.springbootstarterim.strategy.impl.ChatStrategy; 6 | import io.github.yangyouwang.springbootstarterim.strategy.impl.ConnectStrategy; 7 | import io.github.yangyouwang.springbootstarterim.strategy.impl.KeepAliveStrategy; 8 | import io.github.yangyouwang.springbootstarterim.strategy.impl.SignedStrategy; 9 | import org.springframework.beans.factory.annotation.Qualifier; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | 16 | /** 17 | * @author yangyouwang 18 | * @title: MsgActionConfig 19 | * @projectName springboot-starter-im 20 | * @description: 消息bean 21 | * @date 2021/3/1511:16 AM 22 | */ 23 | @Configuration 24 | public class MsgConfig { 25 | 26 | @Bean 27 | public ChatStrategy chatStrategy() { 28 | return new ChatStrategy(); 29 | } 30 | 31 | @Bean 32 | public ConnectStrategy connectStrategy() { 33 | return new ConnectStrategy(); 34 | } 35 | 36 | @Bean 37 | public SignedStrategy signedStrategy() { 38 | return new SignedStrategy(); 39 | } 40 | 41 | @Bean 42 | public KeepAliveStrategy keepAliveStrategy() { 43 | return new KeepAliveStrategy(); 44 | } 45 | 46 | @Bean 47 | public Map actionMap(@Qualifier("connectStrategy") ConnectStrategy connectStrategy, 48 | @Qualifier("chatStrategy") ChatStrategy chatStrategy, 49 | @Qualifier("signedStrategy") SignedStrategy signedStrategy, 50 | @Qualifier("keepAliveStrategy") KeepAliveStrategy keepAliveStrategy){ 51 | Map map = new HashMap<>(); 52 | map.put(MsgActionEnum.CONNECT.TYPE, connectStrategy); 53 | map.put(MsgActionEnum.CHAT.TYPE, chatStrategy); 54 | map.put(MsgActionEnum.SIGNED.TYPE, signedStrategy); 55 | map.put(MsgActionEnum.KEEP_ALIVE.TYPE, keepAliveStrategy); 56 | return map; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/io/github/yangyouwang/springbootstarterim/config/NettyConfig.java: -------------------------------------------------------------------------------- 1 | package io.github.yangyouwang.springbootstarterim.config; 2 | 3 | import io.github.yangyouwang.springbootstarterim.core.ChatHandler; 4 | import io.github.yangyouwang.springbootstarterim.core.HeartBeatHandler; 5 | import io.github.yangyouwang.springbootstarterim.core.WSServerInitialzer; 6 | import io.netty.bootstrap.ServerBootstrap; 7 | import io.netty.channel.nio.NioEventLoopGroup; 8 | import io.netty.channel.socket.nio.NioServerSocketChannel; 9 | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 10 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.context.annotation.Configuration; 13 | 14 | 15 | /** 16 | * @author yangyouwang 17 | * @title: AutoConfigureImport 18 | * @projectName springboot-starter-im 19 | * @description: 主配置 20 | * @date 2021/3/94:05 PM 21 | */ 22 | @Configuration 23 | @ConditionalOnClass({NioEventLoopGroup.class,ServerBootstrap.class, 24 | WSServerInitialzer.class}) 25 | public class NettyConfig { 26 | 27 | @Bean("mainGroup") 28 | @ConditionalOnMissingBean 29 | public NioEventLoopGroup mainGroup() { 30 | return new NioEventLoopGroup(); 31 | } 32 | 33 | @Bean("subGroup") 34 | @ConditionalOnMissingBean 35 | public NioEventLoopGroup subGroup() { 36 | return new NioEventLoopGroup(); 37 | } 38 | 39 | @Bean("chanelInit") 40 | @ConditionalOnMissingBean 41 | public WSServerInitialzer wsServerInitialzer() { 42 | return new WSServerInitialzer(); 43 | } 44 | 45 | @Bean("HeartBeatHandler") 46 | public HeartBeatHandler HeartBeatHandler() { 47 | return new HeartBeatHandler(); 48 | } 49 | 50 | @Bean("chatHandler") 51 | public ChatHandler chatHandler() { 52 | return new ChatHandler(); 53 | } 54 | 55 | @Bean("server") 56 | @ConditionalOnMissingBean 57 | public ServerBootstrap server(NioEventLoopGroup mainGroup,NioEventLoopGroup subGroup, 58 | WSServerInitialzer chanelInit) { 59 | return new ServerBootstrap().group(mainGroup, subGroup) 60 | .channel(NioServerSocketChannel.class) 61 | .childHandler(chanelInit); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/io/github/yangyouwang/springbootstarterim/config/NettyProperties.java: -------------------------------------------------------------------------------- 1 | package io.github.yangyouwang.springbootstarterim.config; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | 5 | /** 6 | * @author yangyouwang 7 | * @title: NettyProperties 8 | * @projectName springboot-starter-im 9 | * @description: 10 | * @date 2021/3/92:39 PM 11 | */ 12 | @ConfigurationProperties(prefix = "im.netty") 13 | public class NettyProperties { 14 | 15 | /** 16 | * 端口 17 | */ 18 | private Integer port; 19 | /** 20 | * 路径 21 | */ 22 | private String path; 23 | 24 | public Integer getPort() { 25 | return port; 26 | } 27 | 28 | public void setPort(Integer port) { 29 | this.port = port; 30 | } 31 | 32 | public String getPath() { 33 | return path; 34 | } 35 | 36 | public void setPath(String path) { 37 | this.path = path; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/io/github/yangyouwang/springbootstarterim/constant/MsgActionEnum.java: -------------------------------------------------------------------------------- 1 | package io.github.yangyouwang.springbootstarterim.constant; 2 | 3 | /** 4 | * 5 | * @author yangyouwang 6 | * @Description: 发送消息的动作 枚举 7 | */ 8 | public enum MsgActionEnum { 9 | 10 | CONNECT(1, "第一次(或重连)初始化连接"), 11 | CHAT(2, "聊天消息"), 12 | SIGNED(3, "消息签收"), 13 | KEEP_ALIVE(4, "客户端保持心跳"); 14 | 15 | public final Integer TYPE; 16 | public final String CONTENT; 17 | 18 | MsgActionEnum(Integer TYPE, String CONTENT){ 19 | this.TYPE = TYPE; 20 | this.CONTENT = CONTENT; 21 | } 22 | 23 | public static MsgActionEnum getEnum(Integer type) { 24 | MsgActionEnum[] msgActionEnums = values(); 25 | for (MsgActionEnum msgActionEnum : msgActionEnums) { 26 | if (msgActionEnum.TYPE == type) { 27 | return msgActionEnum; 28 | } 29 | } 30 | return null; 31 | } 32 | 33 | public int getTYPE() { 34 | return TYPE; 35 | } 36 | 37 | public String getCONTENT() { 38 | return CONTENT; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/io/github/yangyouwang/springbootstarterim/constant/MsgStatusEnum.java: -------------------------------------------------------------------------------- 1 | package io.github.yangyouwang.springbootstarterim.constant; 2 | 3 | 4 | /** 5 | * 消息状态 6 | * @author yangyouwang 7 | */ 8 | public enum MsgStatusEnum { 9 | 10 | MSG_STATUS_UNREAD(0, "未读"), 11 | MSG_STATUS_HAVE_READ(1, "已读"); 12 | 13 | public final int CODE; 14 | public final String TYPE; 15 | 16 | MsgStatusEnum(int CODE, String TYPE) { 17 | this.CODE = CODE; 18 | this.TYPE = TYPE; 19 | } 20 | 21 | public static MsgStatusEnum getEnum(int CODE) { 22 | MsgStatusEnum[] msgTypeEnums = values(); 23 | for (MsgStatusEnum msgTypeEnum : msgTypeEnums) { 24 | if (msgTypeEnum.CODE == CODE) { 25 | return msgTypeEnum; 26 | } 27 | } 28 | return null; 29 | } 30 | 31 | public int getCODE() { 32 | return CODE; 33 | } 34 | 35 | public String getTYPE() { 36 | return TYPE; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/io/github/yangyouwang/springbootstarterim/constant/MsgTypeEnum.java: -------------------------------------------------------------------------------- 1 | package io.github.yangyouwang.springbootstarterim.constant; 2 | 3 | 4 | /** 5 | * 消息类型 6 | * @author yangyouwang 7 | */ 8 | public enum MsgTypeEnum { 9 | 10 | MSG_TYPE_TEXT(0, "文字"), 11 | MSG_TYPE_IMG(1, "图片"); 12 | 13 | private int CODE; 14 | private String TYPE; 15 | 16 | MsgTypeEnum(int CODE, String TYPE) { 17 | this.CODE = CODE; 18 | this.TYPE = TYPE; 19 | } 20 | 21 | public static MsgTypeEnum getEnum(int CODE) { 22 | MsgTypeEnum[] msgTypeEnums = values(); 23 | for (MsgTypeEnum msgTypeEnum : msgTypeEnums) { 24 | if (msgTypeEnum.CODE == CODE) { 25 | return msgTypeEnum; 26 | } 27 | } 28 | return null; 29 | } 30 | 31 | public int getCODE() { 32 | return CODE; 33 | } 34 | 35 | public void setCODE(int CODE) { 36 | this.CODE = CODE; 37 | } 38 | 39 | public String getTYPE() { 40 | return TYPE; 41 | } 42 | 43 | public void setTYPE(String TYPE) { 44 | this.TYPE = TYPE; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/io/github/yangyouwang/springbootstarterim/core/ChatHandler.java: -------------------------------------------------------------------------------- 1 | package io.github.yangyouwang.springbootstarterim.core; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import io.github.yangyouwang.springbootstarterim.constant.MsgActionEnum; 5 | import io.github.yangyouwang.springbootstarterim.bean.DataContent; 6 | import io.github.yangyouwang.springbootstarterim.factory.MsgContextFactory; 7 | import io.netty.channel.ChannelHandler; 8 | import io.netty.channel.ChannelHandlerContext; 9 | import io.netty.channel.SimpleChannelInboundHandler; 10 | import io.netty.channel.group.ChannelGroup; 11 | import io.netty.channel.group.DefaultChannelGroup; 12 | import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; 13 | import io.netty.util.concurrent.GlobalEventExecutor; 14 | 15 | import javax.annotation.Resource; 16 | 17 | /** 18 | * 用于处理消息的handler 19 | * @author yangyouwang 20 | */ 21 | @ChannelHandler.Sharable 22 | public class ChatHandler extends SimpleChannelInboundHandler { 23 | /** 24 | * 用于记录和管理所有客户端的channel 25 | */ 26 | public static ChannelGroup users = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); 27 | 28 | public static ChannelHandlerContext ctx; 29 | 30 | @Resource 31 | private MsgContextFactory msgContextFactory; 32 | 33 | @Override 34 | protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) { 35 | String content = msg.text(); 36 | this.ctx = ctx; 37 | // 获取客户端发来的消息 38 | DataContent dataContent = JSONObject.parseObject(content,DataContent.class); 39 | // 业务操作 40 | MsgActionEnum msgActionEnum = MsgActionEnum.getEnum(dataContent.getAction()); 41 | msgContextFactory.getMsgStrategy(msgActionEnum).doAction(dataContent); 42 | } 43 | 44 | @Override 45 | public void handlerAdded(ChannelHandlerContext ctx) { 46 | users.add(ctx.channel()); 47 | } 48 | 49 | @Override 50 | public void handlerRemoved(ChannelHandlerContext ctx) { 51 | users.remove(ctx.channel()); 52 | } 53 | 54 | @Override 55 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { 56 | cause.printStackTrace(); 57 | //发生了异常后关闭连接,同时从channelgroup移除 58 | ctx.channel().close(); 59 | users.remove(ctx.channel()); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/io/github/yangyouwang/springbootstarterim/core/HeartBeatHandler.java: -------------------------------------------------------------------------------- 1 | package io.github.yangyouwang.springbootstarterim.core; 2 | 3 | import io.netty.channel.Channel; 4 | import io.netty.channel.ChannelHandler; 5 | import io.netty.channel.ChannelHandlerContext; 6 | import io.netty.channel.ChannelInboundHandlerAdapter; 7 | import io.netty.handler.timeout.IdleState; 8 | import io.netty.handler.timeout.IdleStateEvent; 9 | 10 | 11 | /** 12 | * 用于检测channel 的心跳handler 13 | * 继承ChannelInboundHandlerAdapter,目的是不需要实现ChannelRead0 这个方法 14 | * @author yangyouwang 15 | */ 16 | @ChannelHandler.Sharable 17 | public class HeartBeatHandler extends ChannelInboundHandlerAdapter { 18 | 19 | @Override 20 | public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { 21 | if(evt instanceof IdleStateEvent){ 22 | IdleStateEvent event = (IdleStateEvent)evt; 23 | if(event.state()== IdleState.ALL_IDLE){ 24 | Channel channel = ctx.channel(); 25 | //资源释放 26 | channel.close(); 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/io/github/yangyouwang/springbootstarterim/core/UserChanelRel.java: -------------------------------------------------------------------------------- 1 | package io.github.yangyouwang.springbootstarterim.core; 2 | 3 | import io.netty.channel.Channel; 4 | 5 | import java.util.Map; 6 | import java.util.concurrent.ConcurrentHashMap; 7 | 8 | /** 9 | * 用户id 和channel 的关联关系处理 10 | * @author yangyouwang 11 | */ 12 | public class UserChanelRel { 13 | 14 | private static Map manage = new ConcurrentHashMap(); 15 | 16 | public static void put(Long fromUserId,Channel channel){ 17 | manage.put(fromUserId,channel); 18 | } 19 | 20 | public static Channel get(Long toUserId){ 21 | return manage.get(toUserId); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/io/github/yangyouwang/springbootstarterim/core/WSServerInitialzer.java: -------------------------------------------------------------------------------- 1 | package io.github.yangyouwang.springbootstarterim.core; 2 | 3 | import io.github.yangyouwang.springbootstarterim.config.NettyProperties; 4 | import io.netty.channel.ChannelInitializer; 5 | import io.netty.channel.ChannelPipeline; 6 | import io.netty.channel.socket.SocketChannel; 7 | import io.netty.handler.codec.http.HttpObjectAggregator; 8 | import io.netty.handler.codec.http.HttpServerCodec; 9 | import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; 10 | import io.netty.handler.stream.ChunkedWriteHandler; 11 | import io.netty.handler.timeout.IdleStateHandler; 12 | 13 | import javax.annotation.Resource; 14 | 15 | /** 16 | * 通道 17 | * @author yangyouwang 18 | */ 19 | public class WSServerInitialzer extends ChannelInitializer { 20 | 21 | @Resource 22 | private ChatHandler chatHandler; 23 | 24 | @Resource 25 | private HeartBeatHandler heartBeatHandler; 26 | 27 | @Resource 28 | private NettyProperties nettyProperties; 29 | 30 | @Override 31 | protected void initChannel(SocketChannel channel) { 32 | //获取管道(pipeline) 33 | ChannelPipeline pipeline = channel.pipeline(); 34 | //websocket 基于http协议,所需要的http 编解码器 35 | pipeline.addLast(new HttpServerCodec()); 36 | //在http上有一些数据流产生,有大有小,我们对其进行处理,既然如此,我们需要使用netty 对下数据流写 提供支持,这个类叫:ChunkedWriteHandler 37 | pipeline.addLast(new ChunkedWriteHandler()); 38 | //对httpMessage 进行聚合处理,聚合成request或 response 39 | pipeline.addLast(new HttpObjectAggregator(1024 * 64)); 40 | 41 | //===========================增加心跳支持============================== 42 | 43 | /** 44 | * 针对客户端,如果在1分钟时间内没有向服务端发送读写心跳(ALL),则主动断开连接 45 | * 如果有读空闲和写空闲,则不做任何处理 46 | */ 47 | pipeline.addLast(new IdleStateHandler(8,10,12)); 48 | //自定义的空闲状态检测的handler 49 | pipeline.addLast(heartBeatHandler); 50 | 51 | /** 52 | * 本handler 会帮你处理一些繁重复杂的事情 53 | * 会帮你处理握手动作:handshaking(close、ping、pong) ping+pong = 心跳 54 | * 对于websocket 来讲frams 进行传输的,不同的数据类型对应的frams 也不同 55 | */ 56 | String path = nettyProperties.getPath(); 57 | pipeline.addLast(new WebSocketServerProtocolHandler(path)); 58 | 59 | //自定义的handler 60 | pipeline.addLast(chatHandler); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/io/github/yangyouwang/springbootstarterim/factory/MsgContextFactory.java: -------------------------------------------------------------------------------- 1 | package io.github.yangyouwang.springbootstarterim.factory; 2 | 3 | import io.github.yangyouwang.springbootstarterim.constant.MsgActionEnum; 4 | import io.github.yangyouwang.springbootstarterim.strategy.MsgStrategy; 5 | import org.springframework.stereotype.Service; 6 | 7 | import javax.annotation.Resource; 8 | import java.util.Map; 9 | 10 | /** 11 | * @author yangyouwang 12 | * @title: MsgContextFactory 13 | * @projectName springboot-starter-im 14 | * @description: 上下文 15 | * @date 2021/3/159:45 AM 16 | */ 17 | @Service 18 | public class MsgContextFactory { 19 | 20 | @Resource 21 | private Map actionMap; 22 | 23 | public MsgStrategy getMsgStrategy(MsgActionEnum msgActionEnum) { 24 | MsgStrategy msgStrategy = actionMap.get(msgActionEnum.TYPE); 25 | if (null == msgStrategy) { 26 | throw new RuntimeException("no strategy defined"); 27 | } 28 | return msgStrategy; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/io/github/yangyouwang/springbootstarterim/strategy/MsgStrategy.java: -------------------------------------------------------------------------------- 1 | package io.github.yangyouwang.springbootstarterim.strategy; 2 | 3 | import io.github.yangyouwang.springbootstarterim.bean.DataContent; 4 | 5 | /** 6 | * @author yangyouwang 7 | * @title: MsgStrategy 8 | * @projectName springboot-starter-im 9 | * @description: 消息策略 10 | * @date 2021/3/159:04 AM 11 | */ 12 | public interface MsgStrategy { 13 | 14 | public void doAction(DataContent dataContent); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/io/github/yangyouwang/springbootstarterim/strategy/impl/ChatStrategy.java: -------------------------------------------------------------------------------- 1 | package io.github.yangyouwang.springbootstarterim.strategy.impl; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import io.github.yangyouwang.springbootstarterim.bean.ChatMsg; 5 | import io.github.yangyouwang.springbootstarterim.bean.DataContent; 6 | import io.github.yangyouwang.springbootstarterim.bean.DataContentEvent; 7 | import io.github.yangyouwang.springbootstarterim.constant.MsgActionEnum; 8 | import io.github.yangyouwang.springbootstarterim.constant.MsgStatusEnum; 9 | import io.github.yangyouwang.springbootstarterim.constant.MsgTypeEnum; 10 | import io.github.yangyouwang.springbootstarterim.core.ChatHandler; 11 | import io.github.yangyouwang.springbootstarterim.core.UserChanelRel; 12 | import io.github.yangyouwang.springbootstarterim.strategy.MsgStrategy; 13 | import io.netty.channel.Channel; 14 | import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.context.ApplicationContext; 17 | 18 | /** 19 | * @author yangyouwang 20 | * @title: ChatStrategy 21 | * @projectName springboot-starter-im 22 | * @description: 聊天消息 23 | * @date 2021/3/159:20 AM 24 | */ 25 | public class ChatStrategy implements MsgStrategy { 26 | 27 | @Autowired 28 | private ApplicationContext applicationContext; 29 | 30 | @Override 31 | public void doAction(DataContent dataContent) { 32 | // 聊天类型的消息 33 | MsgActionEnum msgActionEnum = MsgActionEnum.getEnum(dataContent.getAction()); 34 | ChatMsg chatMsg = dataContent.getChatMsg(); 35 | Long toUserId = chatMsg.getToUserId(); 36 | Long fromUserId = chatMsg.getFromUserId(); 37 | String message = chatMsg.getMessage(); 38 | MsgTypeEnum msgTypeEnum = MsgTypeEnum.getEnum(dataContent.getChatMsg().getType()); 39 | //发送消息 40 | Channel receiverChannel = UserChanelRel.get(toUserId); 41 | if(receiverChannel!= null){ 42 | Channel findChanel = ChatHandler.users.find(receiverChannel.id()); 43 | if(findChanel!=null){ 44 | //用户在线 45 | receiverChannel.writeAndFlush( 46 | new TextWebSocketFrame( 47 | JSONObject.toJSONString(dataContent) 48 | ) 49 | ); 50 | } 51 | // 用户离线 52 | DataContentEvent dataContentEvent = new DataContentEvent(applicationContext); 53 | dataContentEvent.setFromUserId(fromUserId); 54 | dataContentEvent.setToUserId(toUserId); 55 | dataContentEvent.setType(msgTypeEnum); 56 | dataContentEvent.setStatus(MsgStatusEnum.MSG_STATUS_UNREAD); 57 | dataContentEvent.setMessage(message); 58 | dataContentEvent.setAction(msgActionEnum); 59 | applicationContext.publishEvent(dataContentEvent); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/io/github/yangyouwang/springbootstarterim/strategy/impl/ConnectStrategy.java: -------------------------------------------------------------------------------- 1 | package io.github.yangyouwang.springbootstarterim.strategy.impl; 2 | 3 | import io.github.yangyouwang.springbootstarterim.bean.DataContent; 4 | import io.github.yangyouwang.springbootstarterim.bean.DataContentEvent; 5 | import io.github.yangyouwang.springbootstarterim.constant.MsgActionEnum; 6 | import io.github.yangyouwang.springbootstarterim.core.ChatHandler; 7 | import io.github.yangyouwang.springbootstarterim.core.UserChanelRel; 8 | import io.github.yangyouwang.springbootstarterim.strategy.MsgStrategy; 9 | import io.netty.channel.Channel; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.context.ApplicationContext; 12 | 13 | /** 14 | * @author yangyouwang 15 | * @title: ConnectStrategy 16 | * @projectName springboot-starter-im 17 | * @description: 第一次(或重连)初始化连接 18 | * @date 2021/3/159:06 AM 19 | */ 20 | public class ConnectStrategy implements MsgStrategy { 21 | 22 | @Autowired 23 | private ApplicationContext applicationContext; 24 | 25 | @Override 26 | public void doAction(DataContent dataContent) { 27 | Long fromUserId = dataContent.getChatMsg().getFromUserId(); 28 | MsgActionEnum msgActionEnum = MsgActionEnum.getEnum(dataContent.getAction()); 29 | 30 | Channel channel = ChatHandler.ctx.channel(); 31 | UserChanelRel.put(fromUserId,channel); 32 | 33 | DataContentEvent dataContentEvent = new DataContentEvent(applicationContext); 34 | dataContentEvent.setFromUserId(fromUserId); 35 | dataContentEvent.setAction(msgActionEnum); 36 | applicationContext.publishEvent(dataContentEvent); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/io/github/yangyouwang/springbootstarterim/strategy/impl/KeepAliveStrategy.java: -------------------------------------------------------------------------------- 1 | package io.github.yangyouwang.springbootstarterim.strategy.impl; 2 | 3 | import io.github.yangyouwang.springbootstarterim.bean.DataContent; 4 | import io.github.yangyouwang.springbootstarterim.bean.DataContentEvent; 5 | import io.github.yangyouwang.springbootstarterim.constant.MsgActionEnum; 6 | import io.github.yangyouwang.springbootstarterim.strategy.MsgStrategy; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.context.ApplicationContext; 9 | 10 | /** 11 | * @author yangyouwang 12 | * @title: KeepAliveStrategy 13 | * @projectName springboot-starter-im 14 | * @description: 检测心跳 15 | * @date 2021/3/155:14 PM 16 | */ 17 | public class KeepAliveStrategy implements MsgStrategy { 18 | 19 | @Autowired 20 | private ApplicationContext applicationContext; 21 | 22 | @Override 23 | public void doAction(DataContent dataContent) { 24 | MsgActionEnum msgActionEnum = MsgActionEnum.getEnum(dataContent.getAction()); 25 | DataContentEvent dataContentEvent = new DataContentEvent(applicationContext); 26 | dataContentEvent.setAction(msgActionEnum); 27 | applicationContext.publishEvent(dataContentEvent); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/io/github/yangyouwang/springbootstarterim/strategy/impl/SignedStrategy.java: -------------------------------------------------------------------------------- 1 | package io.github.yangyouwang.springbootstarterim.strategy.impl; 2 | 3 | import io.github.yangyouwang.springbootstarterim.bean.ChatMsg; 4 | import io.github.yangyouwang.springbootstarterim.bean.DataContent; 5 | import io.github.yangyouwang.springbootstarterim.bean.DataContentEvent; 6 | import io.github.yangyouwang.springbootstarterim.constant.MsgActionEnum; 7 | import io.github.yangyouwang.springbootstarterim.constant.MsgStatusEnum; 8 | import io.github.yangyouwang.springbootstarterim.strategy.MsgStrategy; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.context.ApplicationContext; 11 | 12 | /** 13 | * @author yangyouwang 14 | * @title: SignedStrategy 15 | * @projectName springboot-starter-im 16 | * @description: 消息签收 17 | * @date 2021/3/159:25 AM 18 | */ 19 | public class SignedStrategy implements MsgStrategy { 20 | 21 | @Autowired 22 | private ApplicationContext applicationContext; 23 | 24 | @Override 25 | public void doAction(DataContent dataContent) { 26 | MsgActionEnum msgActionEnum = MsgActionEnum.getEnum(dataContent.getAction()); 27 | ChatMsg chatMsg = dataContent.getChatMsg(); 28 | Long toUserId = chatMsg.getToUserId(); 29 | Long fromUserId = chatMsg.getFromUserId(); 30 | // 签收消息类型 31 | DataContentEvent dataContentEvent = new DataContentEvent(applicationContext); 32 | dataContentEvent.setStatus(MsgStatusEnum.MSG_STATUS_HAVE_READ); 33 | dataContentEvent.setFromUserId(fromUserId); 34 | dataContentEvent.setToUserId(toUserId); 35 | dataContentEvent.setAction(msgActionEnum); 36 | applicationContext.publishEvent(dataContentEvent); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 2 | io.github.yangyouwang.springbootstarterim.config.ChatConfig -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | im: 2 | netty: 3 | port: 8888 # 配置聊天端口 4 | path: /im/ws --------------------------------------------------------------------------------