├── .gitignore
├── NotifyQQ.hpi
├── Screenshots
├── 1.png
├── 2.png
├── 3.png
├── 4.png
└── 5.png
├── src
└── main
│ ├── java
│ └── net
│ │ └── aimeizi
│ │ ├── QQType.java
│ │ ├── QQNumber.java
│ │ └── NotifyQQ.java
│ └── resources
│ ├── index.jelly
│ └── net
│ └── aimeizi
│ └── NotifyQQ
│ ├── help-name.html
│ └── config.jelly
├── LICENSE
├── Dockerfile
├── pom.xml
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .idea
3 | target
4 | work
--------------------------------------------------------------------------------
/NotifyQQ.hpi:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/v5tech/NotifyQQ/HEAD/NotifyQQ.hpi
--------------------------------------------------------------------------------
/Screenshots/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/v5tech/NotifyQQ/HEAD/Screenshots/1.png
--------------------------------------------------------------------------------
/Screenshots/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/v5tech/NotifyQQ/HEAD/Screenshots/2.png
--------------------------------------------------------------------------------
/Screenshots/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/v5tech/NotifyQQ/HEAD/Screenshots/3.png
--------------------------------------------------------------------------------
/Screenshots/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/v5tech/NotifyQQ/HEAD/Screenshots/4.png
--------------------------------------------------------------------------------
/Screenshots/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/v5tech/NotifyQQ/HEAD/Screenshots/5.png
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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'
--------------------------------------------------------------------------------
/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/resources/net/aimeizi/NotifyQQ/config.jelly:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | 群号
24 | 个人号
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/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 | 
36 |
37 | * NotifyQQ插件配置
38 |
39 | 
40 |
41 | * NotifyQQ插件输出
42 |
43 | 
44 |
45 | * NotifyQQ插件QQ群消息输出
46 |
47 | 
48 |
49 | * NotifyQQ插件QQ个人消息输出
50 |
51 | 
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
--------------------------------------------------------------------------------
/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/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 extends AbstractProject> 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 |
--------------------------------------------------------------------------------