├── .gitignore ├── Dockerfile ├── LICENSE ├── NotifyQQ.hpi ├── README.md ├── Screenshots ├── 1.png ├── 2.png ├── 3.png ├── 4.png └── 5.png ├── pom.xml └── src └── main ├── java └── net │ └── aimeizi │ ├── NotifyQQ.java │ ├── QQNumber.java │ └── QQType.java └── resources ├── index.jelly └── net └── aimeizi └── NotifyQQ ├── config.jelly └── help-name.html /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea 3 | target 4 | work -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.6 2 | MAINTAINER ameizi 3 | 4 | WORKDIR /root 5 | 6 | RUN echo "http://mirrors.aliyun.com/alpine/v3.6/main" > /etc/apk/repositories \ 7 | && echo "http://mirrors.aliyun.com/alpine/v3.6/community" >> /etc/apk/repositories \ 8 | && apk update upgrade \ 9 | && apk add --no-cache gcc g++ make tzdata openssl openssl-dev perl perl-dev libc-dev curl wget \ 10 | && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ 11 | && echo "Asia/Shanghai" > /etc/timezone \ 12 | && curl -L https://cpanmin.us/ -o /usr/bin/cpanm \ 13 | && chmod +x /usr/bin/cpanm \ 14 | && cpanm -fn Encode::Locale IO::Socket::SSL Mojolicious \ 15 | && wget -q https://github.com/sjdy521/Mojo-Webqq/archive/master.zip -OMojo-Webqq.zip \ 16 | && unzip -qo Mojo-Webqq.zip \ 17 | && cd Mojo-Webqq-master \ 18 | && cpanm -fn . \ 19 | && cd .. \ 20 | && rm -rf Mojo-Webqq-master Mojo-Webqq.zip \ 21 | && rm -rf /var/cache/apk/* 22 | 23 | VOLUME /tmp 24 | 25 | EXPOSE 5000 26 | 27 | CMD perl -MMojo::Webqq -e 'Mojo::Webqq->new(log_encoding=>"utf8")->load(["ShowMsg","UploadQRcode"])->load("Openqq",data=>{listen=>[{port=>$ENV{MOJO_WEBQQ_PLUGIN_OPENQQ_PORT}//5000}],post_api=>$ENV{MOJO_WEBQQ_PLUGIN_OPENQQ_POST_API}})->run' -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NotifyQQ.hpi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v5tech/NotifyQQ/51ac832000cdfc4a5e50ee8a633a7f21f5ce7e3e/NotifyQQ.hpi -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NotifyQQ 2 | This is a Jenkins notify plugin, send a message to QQ when a job is finished. 3 | 4 | I think this is a demand for Chinese only, so I will write the follow readme content in Chinese. if you want it change to English, please let me know. 5 | 6 | 使用`Docker`构建`Mojo-Webqq` 7 | 8 | 使用本项目提供的`Dockerfile`构建`Mojo-Webqq`镜像 9 | 10 | ``` 11 | docker build -t mojo-webqq . 12 | ``` 13 | 14 | 运行构建好的`mojo-webqq`镜像 15 | 16 | ``` 17 | docker run -it --env MOJO_WEBQQ_LOG_ENCODING=utf8 -p 5000:5000 -v /tmp:/tmp mojo-webqq 18 | ``` 19 | 20 | 接下来扫码登录你懂得。 21 | 22 | ## 使用说明 23 | 24 | 需要做如下准备: 25 | 26 | 1. 基于WebQQ协议,[Mojo-Webqq](https://github.com/sjdy521/Mojo-Webqq),运行openqq模块。 27 | 2. (可选)Jenkins插件编写基础。 28 | 3. 安装NotifyQQ插件,在job配置界面设置需要通知的QQ号,支持群号和个人号。 29 | 4. 注意:该插件依赖[Mojo-Webqq](https://github.com/sjdy521/Mojo-Webqq)的运行,否则不能正常工作。 30 | 5. 使用Maven构建,即`mvn compile hpi:hpi`最终输出`NotifyQQ.hpi`、`NotifyQQ.jar`文件,在Jenkins插件列表中安装`NotifyQQ.hpi`即可。 31 | ## Screenshots 32 | 33 | * Jenkins配置 34 | 35 | ![](Screenshots/1.png) 36 | 37 | * NotifyQQ插件配置 38 | 39 | ![](Screenshots/2.png) 40 | 41 | * NotifyQQ插件输出 42 | 43 | ![](Screenshots/3.png) 44 | 45 | * NotifyQQ插件QQ群消息输出 46 | 47 | ![](Screenshots/4.png) 48 | 49 | * NotifyQQ插件QQ个人消息输出 50 | 51 | ![](Screenshots/5.png) 52 | 53 | 54 | ## 参考资料 55 | 56 | https://wiki.jenkins-ci.org/display/JENKINS/Plugin+tutorial 57 | 58 | https://wiki.jenkins-ci.org/display/JENKINS/Extend+Jenkins 59 | 60 | 61 | ## 感谢 62 | 63 | 感谢[go3k](https://github.com/go3k)给了我灵感,该项目基于[go3k](https://github.com/go3k)的[NotifyQQ](https://github.com/go3k/NotifyQQ)项目修改而来。最后感谢[灰灰](https://github.com/sjdy521)缔造了开源项目[Mojo-Webqq](https://github.com/sjdy521/Mojo-Webqq)并为此带来了便利。 64 | 65 | 66 | 67 | ## 与我联系 68 | 69 | * QQ:*184675420* 70 | 71 | * Email:*sxyx2008#gmail.com*(#替换为@) 72 | 73 | * HomePage:*[notes.coding.me](http://notes.coding.me)* 74 | 75 | * Weibo:*[http://weibo.com/qq184675420](http://weibo.com/qq184675420)*(荧星诉语) 76 | 77 | * Twitter:*[https://twitter.com/sxyx2008](https://twitter.com/sxyx2008)* 78 | 79 | 80 | # License 81 | 82 | MIT 83 | 84 | Copyright (c) 2016 ameizi -------------------------------------------------------------------------------- /Screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v5tech/NotifyQQ/51ac832000cdfc4a5e50ee8a633a7f21f5ce7e3e/Screenshots/1.png -------------------------------------------------------------------------------- /Screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v5tech/NotifyQQ/51ac832000cdfc4a5e50ee8a633a7f21f5ce7e3e/Screenshots/2.png -------------------------------------------------------------------------------- /Screenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v5tech/NotifyQQ/51ac832000cdfc4a5e50ee8a633a7f21f5ce7e3e/Screenshots/3.png -------------------------------------------------------------------------------- /Screenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v5tech/NotifyQQ/51ac832000cdfc4a5e50ee8a633a7f21f5ce7e3e/Screenshots/4.png -------------------------------------------------------------------------------- /Screenshots/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v5tech/NotifyQQ/51ac832000cdfc4a5e50ee8a633a7f21f5ce7e3e/Screenshots/5.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | org.jenkins-ci.plugins 8 | plugin 9 | 1.580.1 10 | 11 | 12 | net.aimeizi 13 | NotifyQQ 14 | 1.0 15 | hpi 16 | 17 | NotifyQQ 18 | This plugin can notify build result to QQ! 19 | https://wiki.jenkins-ci.org/display/JENKINS/TODO+Plugin 20 | 21 | 22 | MIT License 23 | http://opensource.org/licenses/MIT 24 | 25 | 26 | 27 | 28 | 29 | repo.jenkins-ci.org 30 | http://repo.jenkins-ci.org/public/ 31 | 32 | 33 | 34 | 35 | repo.jenkins-ci.org 36 | http://repo.jenkins-ci.org/public/ 37 | 38 | 39 | 40 | 41 | 42 | org.apache.httpcomponents 43 | httpasyncclient 44 | 4.1 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/main/java/net/aimeizi/NotifyQQ.java: -------------------------------------------------------------------------------- 1 | package net.aimeizi; 2 | 3 | import hudson.Extension; 4 | import hudson.Launcher; 5 | import hudson.model.AbstractBuild; 6 | import hudson.model.AbstractProject; 7 | import hudson.model.BuildListener; 8 | import hudson.model.Result; 9 | import hudson.tasks.*; 10 | import hudson.util.FormValidation; 11 | import jenkins.model.Jenkins; 12 | import net.sf.json.JSONObject; 13 | import org.apache.http.HttpResponse; 14 | import org.apache.http.client.config.RequestConfig; 15 | import org.apache.http.client.methods.HttpGet; 16 | import org.apache.http.concurrent.FutureCallback; 17 | import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; 18 | import org.apache.http.impl.nio.client.HttpAsyncClients; 19 | import org.kohsuke.stapler.DataBoundConstructor; 20 | import org.kohsuke.stapler.QueryParameter; 21 | import org.kohsuke.stapler.StaplerRequest; 22 | 23 | import javax.servlet.ServletException; 24 | import java.io.*; 25 | import java.net.HttpURLConnection; 26 | import java.net.URL; 27 | import java.net.URLEncoder; 28 | import java.util.ArrayList; 29 | import java.util.List; 30 | import java.util.regex.Matcher; 31 | import java.util.regex.Pattern; 32 | 33 | /** 34 | * @author Kohsuke Kawaguchi 35 | */ 36 | public class NotifyQQ extends Notifier { 37 | 38 | /** 39 | * QQ号码列表 40 | */ 41 | private final List qQNumbers; 42 | /** 43 | * QQ消息内容 44 | */ 45 | private final String qqmessage; 46 | /** 47 | * QQ服务器地址 48 | */ 49 | private final String serverurl; 50 | private PrintStream logger; 51 | 52 | @DataBoundConstructor 53 | public NotifyQQ(String serverurl, List qQNumbers, String qqmessage) { 54 | this.serverurl = serverurl; 55 | this.qQNumbers = new ArrayList(qQNumbers); 56 | this.qqmessage = qqmessage; 57 | } 58 | 59 | public String getServerurl() { 60 | return serverurl; 61 | } 62 | 63 | public List getQQNumbers() { 64 | return qQNumbers; 65 | } 66 | 67 | public String getQqmessage() { 68 | return qqmessage; 69 | } 70 | 71 | public BuildStepMonitor getRequiredMonitorService() { 72 | return BuildStepMonitor.NONE; 73 | } 74 | 75 | @Override 76 | public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws UnsupportedEncodingException { 77 | 78 | logger = listener.getLogger(); 79 | 80 | Jenkins.getInstance(); 81 | String jobURL = ""; 82 | try { 83 | jobURL = build.getEnvironment(listener).expand("${JOB_URL}"); 84 | logger.println("jobURL = " + jobURL); 85 | } catch (Exception e) { 86 | logger.println("tokenmacro expand error."); 87 | } 88 | 89 | String msg = "各位小伙伴,项目"; 90 | msg += build.getFullDisplayName(); 91 | if (build.getResult() == Result.SUCCESS) { 92 | msg += "编译成功!" + qqmessage; 93 | } else { 94 | msg += "编译失败了..."; 95 | msg += "jenkins地址:" + jobURL; 96 | } 97 | 98 | msg = URLEncoder.encode(msg, "UTF-8"); 99 | msg = msg.replaceAll("\\+", "_"); 100 | 101 | for (int i = 0; i < qQNumbers.size(); i++) { 102 | QQNumber number = qQNumbers.get(i); 103 | send(GenerateMessageURL(number.GetUrlString(), msg)); 104 | } 105 | 106 | return true; 107 | } 108 | 109 | /** 110 | * 构建发送QQ消息url 111 | * 112 | * @param qq 113 | * @param msg 114 | * @return 115 | */ 116 | private String GenerateMessageURL(String qq, String msg) { 117 | return String.format(this.getServerurl() + "/openqq/%s&content=%s", qq, msg); 118 | } 119 | 120 | /** 121 | * 发送QQ消息 122 | * 123 | * @param url 124 | */ 125 | protected void send(String url) { 126 | logger.println("Sendurl: " + url); 127 | HttpURLConnection connection = null; 128 | InputStream is = null; 129 | String resultData = ""; 130 | try { 131 | URL targetUrl = new URL(url); 132 | connection = (HttpURLConnection) targetUrl.openConnection(); 133 | connection.setConnectTimeout(10 * 1000); 134 | connection.setReadTimeout(10 * 1000); 135 | connection.connect(); 136 | is = connection.getInputStream(); 137 | InputStreamReader isr = new InputStreamReader(is); 138 | BufferedReader bufferReader = new BufferedReader(isr); 139 | String inputLine = ""; 140 | while ((inputLine = bufferReader.readLine()) != null) { 141 | resultData += inputLine + "\n"; 142 | } 143 | logger.println("response: " + resultData); 144 | } catch (Exception e) { 145 | logger.println("http error." + e); 146 | } finally { 147 | if (is != null) { 148 | try { 149 | is.close(); 150 | } catch (IOException e) { 151 | e.printStackTrace(); 152 | } 153 | } 154 | if (connection != null) { 155 | connection.disconnect(); 156 | } 157 | } 158 | logger.println("Send url finish"); 159 | } 160 | 161 | protected void sendAsync(String url) { 162 | RequestConfig requestConfig = RequestConfig.custom() 163 | .setSocketTimeout(100000) 164 | .setConnectTimeout(100000).build(); 165 | CloseableHttpAsyncClient httpclient = HttpAsyncClients.custom(). 166 | setDefaultRequestConfig(requestConfig) 167 | .build(); 168 | try { 169 | httpclient.start(); 170 | final HttpGet request = new HttpGet(url); 171 | httpclient.execute(request, new FutureCallback() { 172 | 173 | @Override 174 | public void completed(final HttpResponse response) { 175 | logger.println(request.getRequestLine() + "->" + response.getStatusLine()); 176 | } 177 | 178 | @Override 179 | public void failed(final Exception ex) { 180 | logger.println(request.getRequestLine() + "->" + ex); 181 | } 182 | 183 | @Override 184 | public void cancelled() { 185 | logger.println(request.getRequestLine() + " cancelled"); 186 | } 187 | }); 188 | } catch (Exception e) { 189 | logger.println("http error." + e); 190 | } finally { 191 | try { 192 | httpclient.close(); 193 | } catch (Exception e) { 194 | } 195 | } 196 | logger.println("send Done"); 197 | } 198 | 199 | @Override 200 | public DescriptorImpl getDescriptor() { 201 | return (DescriptorImpl) super.getDescriptor(); 202 | } 203 | 204 | @Extension 205 | public static final class DescriptorImpl extends 206 | BuildStepDescriptor { 207 | 208 | public DescriptorImpl() { 209 | load(); 210 | } 211 | 212 | private boolean isNumeric(String str) { 213 | Pattern pattern = Pattern.compile("[0-9]*"); 214 | Matcher isNum = pattern.matcher(str); 215 | if (!isNum.matches()) { 216 | return false; 217 | } 218 | return true; 219 | } 220 | 221 | /** 222 | * QQ号码合法性校验 223 | * 224 | * @param value 225 | * @return 226 | * @throws IOException 227 | * @throws ServletException 228 | */ 229 | public FormValidation doCheckNumber(@QueryParameter String value) 230 | throws IOException, ServletException { 231 | if (value.length() <= 4) 232 | return FormValidation.error("QQ号太短!"); 233 | else if (value.length() > 15) 234 | return FormValidation.error("QQ号太长!"); 235 | else if (!isNumeric(value)) 236 | return FormValidation.error("QQ号格式不对,数字数字数字!"); 237 | return FormValidation.ok(); 238 | } 239 | 240 | /** 241 | * QQ消息内容校验 242 | * 243 | * @param value 244 | * @return 245 | * @throws IOException 246 | * @throws ServletException 247 | */ 248 | public FormValidation doCheckQqmessage(@QueryParameter String value) 249 | throws IOException, ServletException { 250 | return FormValidation.ok(); 251 | } 252 | 253 | /** 254 | * QQ服务器地址校验 255 | * 256 | * @param value 257 | * @return 258 | * @throws IOException 259 | * @throws ServletException 260 | */ 261 | public FormValidation doCheckServerurl(@QueryParameter String value) 262 | throws IOException, ServletException { 263 | return FormValidation.ok(); 264 | } 265 | 266 | public boolean isApplicable(Class aClass) { 267 | return true; 268 | } 269 | 270 | /** 271 | * jenkins中显示名称 272 | * 273 | * @return 274 | */ 275 | public String getDisplayName() { 276 | return "QQ通知"; 277 | } 278 | 279 | @Override 280 | public boolean configure(StaplerRequest req, JSONObject formData) throws FormException { 281 | save(); 282 | return super.configure(req, formData); 283 | } 284 | 285 | } 286 | } 287 | 288 | -------------------------------------------------------------------------------- /src/main/java/net/aimeizi/QQNumber.java: -------------------------------------------------------------------------------- 1 | package net.aimeizi; 2 | 3 | import java.io.Serializable; 4 | 5 | import org.kohsuke.stapler.DataBoundConstructor; 6 | 7 | public class QQNumber implements Serializable { 8 | 9 | private static final long serialVersionUID = 1L; 10 | private String number; 11 | private QQType type; 12 | 13 | @DataBoundConstructor 14 | public QQNumber(String number, QQType type) { 15 | super(); 16 | this.number = number; 17 | this.type = type; 18 | } 19 | 20 | public QQNumber() { 21 | super(); 22 | } 23 | 24 | public String getNumber() { 25 | return number; 26 | } 27 | 28 | public QQType getType() { 29 | return type; 30 | } 31 | 32 | public void setNumber(String number) { 33 | this.number = number; 34 | } 35 | 36 | public void setType(QQType type) { 37 | this.type = type; 38 | } 39 | 40 | /** 41 | * 根据QQ号码类型返回QQ消息发送请求url 42 | * 43 | * @return 44 | */ 45 | public String GetUrlString() { 46 | if (type == QQType.Qun) { 47 | return "send_group_message?uid=" + number; 48 | } else { 49 | return "send_friend_message?uid=" + number; 50 | } 51 | } 52 | 53 | @Override 54 | public String toString() { 55 | StringBuilder builder = new StringBuilder(); 56 | builder.append("QQNumber [ "); 57 | builder.append("type="); 58 | builder.append(type); 59 | builder.append(" number="); 60 | builder.append(number + " ]"); 61 | return builder.toString(); 62 | } 63 | 64 | @Override 65 | public int hashCode() { 66 | final int prime = 31; 67 | int result = 1; 68 | result = prime * result + ((number == null) ? 0 : number.hashCode()); 69 | return result; 70 | } 71 | 72 | @Override 73 | public boolean equals(Object obj) { 74 | if (this == obj) 75 | return true; 76 | if (obj == null) 77 | return false; 78 | if (getClass() != obj.getClass()) 79 | return false; 80 | QQNumber other = (QQNumber) obj; 81 | if (number == null) { 82 | if (other.number != null) 83 | return false; 84 | } else if (!number.equals(other.number)) 85 | return false; 86 | return true; 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/net/aimeizi/QQType.java: -------------------------------------------------------------------------------- 1 | package net.aimeizi; 2 | 3 | public enum QQType { 4 | Qun, 5 | Normal 6 | } -------------------------------------------------------------------------------- /src/main/resources/index.jelly: -------------------------------------------------------------------------------- 1 | 2 | 5 |
6 | This plugin can notify build result to QQ. 7 |
8 | -------------------------------------------------------------------------------- /src/main/resources/net/aimeizi/NotifyQQ/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 28 | 33 | 38 | 39 |
21 | 22 | 26 | 27 | 29 | 30 | 31 | 32 | 34 | 35 |
36 |
37 |
40 |
41 |
42 |
43 | 44 | 45 | 46 |
47 | -------------------------------------------------------------------------------- /src/main/resources/net/aimeizi/NotifyQQ/help-name.html: -------------------------------------------------------------------------------- 1 |
2 | Help file for fields are discovered through a file name convention. This file is 3 | help for the "name" field. You can have arbitrary HTML here. You can write 4 | this file as a Jelly script if you need a dynamic content (but if you do so, change 5 | the extension to .jelly). 6 |
7 | --------------------------------------------------------------------------------