├── README.md ├── pom.xml └── src └── main └── java ├── DockerFile ├── Main.java ├── Qbittorrent.java ├── TelegramBot.java ├── Torrent.java └── image ├── teleqbit1.PNG ├── teleqbit2.jpg └── teleqbit3.PNG /README.md: -------------------------------------------------------------------------------- 1 | # 目的 2 | 本程序连接telegrambot和qbittorrent,通过telegram聊天窗口控制查看qbittorrent情况,目前只能运用qbittorrent的主要功能。 3 | 4 | ``` 5 | 发送包含http或magnet的链接时,将自动添加该种子 6 | status - 查询磁盘剩余及下载情况 7 | status_downloading - 查询正在下载任务进度 8 | pause_all - 暂停所有下载任务 9 | resume_all - 继续所有下载任务 10 | delete_all - 删除所有下载任务 11 | about - 关于 12 | ``` 13 | 14 | # Docker一键部署 15 | 16 | ``` 17 | docker run \ 18 | --name teleqbit \ 19 | -e QB_IP='http://替换成qbittorrent的服务器地址:端口/api/v2/' \ 20 | -e QB_NAME='替换成qbittorrent的账号' \ 21 | -e QB_PASSWORD='替换成qbittorrent的密码' \ 22 | -e BOT_TOKEN='替换成telegram bot的token' \ 23 | -e BOT_NAME='替换成telegram bot的name不包含@' \ 24 | closty/teleqbit 25 | ``` 26 | 27 | ### 实例 28 | ``` 29 | docker run \ 30 | --name teleqbit \ 31 | -e QB_IP='http://101.73.233.123:8080/api/v2/' \ 32 | -e QB_NAME='admin' \ 33 | -e QB_PASSWORD='adminadmin' \ 34 | -e BOT_TOKEN='1290117084:AAH2pwEoe7pQ_SFVcq8UR2hENyf31Pqeo' \ 35 | -e BOT_NAME='TeleqBot' \ 36 | closty/teleqbit 37 | ``` 38 | 39 | # 相关截图 40 | 41 | 42 | 43 | 44 | # TODO 45 | - 拆包 46 | - 增加翻页按钮 47 | 48 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | org.example 6 | Teleqbit 7 | 1.0-SNAPSHOT 8 | 9 | 10 | 11 | org.apache.maven.plugins 12 | maven-compiler-plugin 13 | 14 | 15 15 | 15 16 | 17 | 18 | 19 | 20 | jar 21 | 22 | Teleqbit 23 | http://maven.apache.org 24 | 25 | 26 | UTF-8 27 | 28 | 29 | 30 | 31 | junit 32 | junit 33 | 3.8.1 34 | test 35 | 36 | 37 | 38 | 39 | org.telegram 40 | telegrambots 41 | 6.1.0 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/main/java/DockerFile: -------------------------------------------------------------------------------- 1 | FROM openjdk:20 2 | RUN mkdir /teleqbit 3 | COPY . /Teleqbit 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/java/Main.java: -------------------------------------------------------------------------------- 1 | import org.telegram.telegrambots.meta.TelegramBotsApi; 2 | import org.telegram.telegrambots.meta.exceptions.TelegramApiException; 3 | import org.telegram.telegrambots.updatesreceivers.DefaultBotSession; 4 | 5 | public class Main { 6 | public static String username; 7 | public static String qbServer; 8 | public static String password; 9 | public static String botToken; 10 | 11 | public static String botName; 12 | 13 | 14 | public static void main(String[] args){ 15 | if (args.length != 5) { 16 | System.out.println("您设置的参数不是五个!请依次写入参数qb地址 qb用户名 qb密码 TelegramBotToken TelegramBotName"); 17 | return; 18 | } 19 | 20 | try { 21 | qbServer = args[0]; 22 | username = args[1]; 23 | password = args[2]; 24 | TelegramBot c = new TelegramBot(); 25 | 26 | c.botToken = args[3]; 27 | c.botName = args[4]; 28 | 29 | TelegramBotsApi botsApi = new TelegramBotsApi(DefaultBotSession.class); 30 | botsApi.registerBot(c); 31 | } catch (TelegramApiException e) { 32 | e.printStackTrace(); 33 | } 34 | 35 | 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/Qbittorrent.java: -------------------------------------------------------------------------------- 1 | import org.apache.http.NameValuePair; 2 | import org.apache.http.client.ClientProtocolException; 3 | import org.apache.http.client.entity.UrlEncodedFormEntity; 4 | import org.apache.http.client.methods.CloseableHttpResponse; 5 | import org.apache.http.client.methods.HttpPost; 6 | import org.apache.http.impl.client.CloseableHttpClient; 7 | import org.apache.http.impl.client.HttpClients; 8 | import org.apache.http.message.BasicNameValuePair; 9 | import org.apache.http.util.EntityUtils; 10 | import org.json.JSONArray; 11 | import org.json.JSONObject; 12 | import java.io.*; 13 | import java.util.*; 14 | 15 | public class Qbittorrent { 16 | 17 | 18 | 19 | public static String getCookie(){ 20 | String cookie = ""; 21 | try { 22 | File input = new File("cookie.txt"); 23 | Scanner s = new Scanner(input); 24 | while(s.hasNext()) { 25 | cookie = s.nextLine(); 26 | System.out.println("readCookie():" + cookie); 27 | } 28 | } catch (FileNotFoundException var3) { 29 | System.out.println("readCookie():File not found!"); 30 | } 31 | System.out.println("getCookie(): " + cookie); 32 | if (isCookieValid(cookie)) { 33 | return cookie; 34 | } 35 | System.out.println("getCookie():Cookie expired, refreshing cookie now"); 36 | cookie = updateCookie(); 37 | return cookie; 38 | } 39 | 40 | public static String updateCookie() { 41 | HttpPost httpPost1 = new HttpPost(Main.qbServer + "auth/login");//https://stackoverflow.com/questions/3324717/sending-http-post-request-in-java 42 | List nvps = new ArrayList<>(); 43 | System.out.println(Main.qbServer + 222 + Main.username + Main.password); 44 | nvps.add(new BasicNameValuePair("username", Main.username)); 45 | nvps.add(new BasicNameValuePair("password", Main.password)); 46 | try { 47 | httpPost1.setEntity(new UrlEncodedFormEntity(nvps)); 48 | } catch (UnsupportedEncodingException e) { 49 | return "0"; 50 | } 51 | CloseableHttpClient httpclient = HttpClients.createDefault(); 52 | try (CloseableHttpResponse response4 = httpclient.execute(httpPost1)) { 53 | response4.close(); 54 | System.out.println("updateCookie():Trying to log in to refresh cookies"); 55 | String qbResponse = response4.toString(); 56 | System.out.println("updateCookie(): " + qbResponse); 57 | if (qbResponse.contains("OK")) { 58 | String cookie = qbResponse.substring(qbResponse.indexOf("SID="), qbResponse.indexOf("; HttpOnly;"));//Intercepting cookie contents after sid 59 | System.out.println("cookie为: " + cookie); 60 | PrintWriter outFile = new PrintWriter("cookie.txt");//Save and write cookies to this directory 61 | outFile.println(cookie); 62 | outFile.close(); 63 | System.out.println("updateCookie():Cookie refreshed successfully"); 64 | return cookie; 65 | } else { 66 | System.out.println("updateCookie():Login Fail"); 67 | return "0"; 68 | } 69 | 70 | } catch (ClientProtocolException e) { 71 | return "0"; 72 | } catch (IOException e) { 73 | return "0"; 74 | } 75 | } 76 | 77 | public static boolean isCookieValid(String cookie){ 78 | System.out.println("isCookieValid(): start"); 79 | HttpPost httpPost = new HttpPost(Main.qbServer + "transfer/info"); 80 | httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded"); 81 | httpPost.addHeader("cookie", cookie); 82 | CloseableHttpClient httpclient = HttpClients.createDefault(); 83 | System.out.println("isCookieValid()"+ Main.qbServer); 84 | CloseableHttpResponse response2 = null; 85 | String body; 86 | try { 87 | response2 = httpclient.execute(httpPost); 88 | body = EntityUtils.toString(response2.getEntity()); 89 | response2.close(); 90 | } catch (IOException e) { 91 | System.out.println("wrong password."); 92 | return false; 93 | } 94 | 95 | if (body.contains("dl_info_speed")) { 96 | System.out.println("isCookieValid():cookies is valid"); 97 | return true; 98 | } 99 | System.out.println("isCookieValid():cookies is not valid!!!!!!!!!!!!!!"); 100 | return false; 101 | } 102 | 103 | 104 | public static void downloadTorrent(String link) throws IOException, InterruptedException { 105 | String cookie = getCookie(); 106 | if (cookie.contains("SID")) { 107 | HttpPost httpPost = new HttpPost(Main.qbServer + "torrents/add"); 108 | List nvps = new ArrayList<>(); 109 | nvps.add(new BasicNameValuePair("urls", link)); 110 | httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded"); 111 | httpPost.addHeader("cookie", cookie); 112 | httpPost.setEntity(new UrlEncodedFormEntity(nvps)); 113 | CloseableHttpClient httpclient = HttpClients.createDefault(); 114 | CloseableHttpResponse response2 = httpclient.execute(httpPost); 115 | response2.close(); 116 | System.out.println("downloadTorrent(): Have tried to add to the list, but it always returns FAILS, not sure if it was actually added successfully!"); 117 | 118 | } 119 | System.out.println("downloadTorrent(): " + cookie); 120 | } 121 | 122 | 123 | 124 | 125 | public static String getDetail() throws IOException { 126 | String cookie = getCookie(); 127 | HttpPost httpPost = new HttpPost(Main.qbServer + "torrents/info"); 128 | httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded"); 129 | httpPost.addHeader("cookie", cookie); 130 | CloseableHttpClient httpclient = HttpClients.createDefault(); 131 | CloseableHttpResponse response2 = httpclient.execute(httpPost); 132 | String body = EntityUtils.toString(response2.getEntity()); 133 | System.out.println(body); 134 | response2.close(); 135 | return body; 136 | } 137 | 138 | public static String getTagsByHash(String hash) throws IOException { 139 | String cookie = getCookie(); 140 | HttpPost httpPost = new HttpPost(Main.qbServer + "torrents/info"); 141 | httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded"); 142 | httpPost.addHeader("cookie", cookie); 143 | List nvps = new ArrayList<>(); 144 | nvps.add(new BasicNameValuePair("hashes", hash)); 145 | httpPost.setEntity(new UrlEncodedFormEntity(nvps)); 146 | CloseableHttpClient httpclient = HttpClients.createDefault(); 147 | CloseableHttpResponse response2 = httpclient.execute(httpPost); 148 | String body = EntityUtils.toString(response2.getEntity()); 149 | System.out.println(body); 150 | response2.close(); 151 | JSONArray array = new JSONArray(body); 152 | String tags = array.getJSONObject(0).getString("tags"); 153 | return tags; 154 | } 155 | 156 | public static boolean isAbilityManageTorrent(String hash, long userId) throws IOException { 157 | return true; 158 | } 159 | 160 | public static String getDownloadingDetail() throws IOException { 161 | String cookie = getCookie(); 162 | HttpPost httpPost = new HttpPost(Main.qbServer + "torrents/info"); 163 | httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded"); 164 | httpPost.addHeader("cookie", cookie); 165 | List nvps = new ArrayList<>(); 166 | nvps.add(new BasicNameValuePair("filter", "downloading")); 167 | httpPost.setEntity(new UrlEncodedFormEntity(nvps)); 168 | CloseableHttpClient httpclient = HttpClients.createDefault(); 169 | CloseableHttpResponse response2 = httpclient.execute(httpPost); 170 | String body = EntityUtils.toString(response2.getEntity()); 171 | System.out.println(body); 172 | response2.close(); 173 | return body; 174 | } 175 | 176 | public static String printDownloadingDetail() throws IOException, InterruptedException { 177 | System.out.println("printDownloadingDetail()start"); 178 | String body1 = getDownloadingDetail(); 179 | JSONArray array = new JSONArray(body1); 180 | if(array.isNull(0) ){ 181 | System.out.println("printDownloadingDetail():The list is empty"); 182 | return "No tasks are being downloaded!"; 183 | } 184 | StringBuilder detail = new StringBuilder("\uD83D\uDCBE Free Space:" + diskSpaceRemain() + "\n");//emoji https://dplatz.de/blog/2019/emojis-for-java-commandline.html 185 | for (int j = 0; j < array.length(); j++) {//Take the seeds from each list 186 | String name = array.getJSONObject(j).getString("name"); 187 | String size; 188 | String category = array.getJSONObject(j).getString("category"); 189 | String categoryIcon = "\uD83D\uDCC1";//icon 190 | if(category.equals("tvseries")) categoryIcon = "\uD83D\uDCFA"; 191 | else if (category.equals("movie")) categoryIcon = "\uD83C\uDFAC"; 192 | else if (category.equals("anime")) categoryIcon = "\uD83C\uDFA8"; 193 | else if (category.equals("bbc")) categoryIcon = "\uD83D\uDD0D"; 194 | 195 | if (array.getJSONObject(j).getDouble("total_size")/1024/1024<1024) { 196 | size = String.format("%.2f", array.getJSONObject(j).getDouble("total_size") / 1024 / 1024) + "M"; 197 | }else{ 198 | size = String.format("%.2f", array.getJSONObject(j).getDouble("total_size") / 1024 / 1024 / 1024) + "G"; 199 | } 200 | String status = array.getJSONObject(j).getString("state"); 201 | String hash = array.getJSONObject(j).getString("hash"); 202 | double progress = array.getJSONObject(j).getDouble("progress"); 203 | if (progress != 1 && status.contains("downloading")) {//Determine if a download is in progress 204 | double completed = array.getJSONObject(j).getDouble("completed"); 205 | double total = array.getJSONObject(j).getDouble("size"); 206 | String left; 207 | if (array.getJSONObject(j).getDouble("amount_left") /1024/1024<1024) { 208 | left = String.format("%.2f", array.getJSONObject(j).getDouble("amount_left") / 1024 / 1024) + "M"; 209 | }else{ 210 | left = String.format("%.2f", array.getJSONObject(j).getDouble("amount_left") / 1024 / 1024 / 1024) + "G"; 211 | } 212 | System.out.println((int) completed); 213 | System.out.println((int) total); 214 | double remain = completed / total; 215 | System.out.println(remain); 216 | String percent = String.format("%.2f", completed / total * 100) + "%" + "\n" + progressPercentage((int) Math.round(remain * 100), 100); 217 | String speed = String.format("%.2f", array.getJSONObject(j).getDouble("dlspeed") / 1024 / 1024) + "M/s"; 218 | int eta = array.getJSONObject(j).getInt("eta"); 219 | String strEta; 220 | if (eta > 60 && eta < 3600) { 221 | long minutes = eta / 60;//Seconds to minutes 222 | eta = eta % 60;//remaining seconds 223 | strEta = minutes + "m" + eta + "s"; 224 | } else if (eta >= 3600 && eta < 8640000) { 225 | long hours = eta / 3600;//Seconds to hours 226 | eta = eta % 3600;//remaining seconds 227 | long minutes = eta / 60;//seconds to minutes 228 | eta = eta % 60;//remaining seconds 229 | strEta = hours + "h" + minutes + "m" + eta + "s"; 230 | } else if (eta == 8640000) { 231 | strEta = "Cannot be completed"; 232 | } else { 233 | strEta = eta + "s"; 234 | } 235 | detail.append("\n" + categoryIcon + "name: ").append(name).append("\n").append("Size: ").append(size).append(" ").append("State: ").append("\n").append("Left:").append(left).append(" ").append("Speed: ").append(speed).append("\nPercentage completed: ").append(percent).append("\nTime left: ").append(strEta).append("\n\uD83D\uDE48Pause:/pause").append(hash).append("\n----------------------------------------"); 236 | } 237 | 238 | } 239 | System.out.println(detail); 240 | 241 | return detail.toString(); 242 | } 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | public static String progressPercentage(int done, int total) {//Generate progress bar, referenced in printDownloadingDetail() 251 | int size = 26; 252 | String iconLeftBoundary = ""; 253 | String iconDone = "⣿"; 254 | String iconRemain = "⣀"; 255 | String iconRightBoundary = ""; 256 | 257 | if (done > total) { 258 | throw new IllegalArgumentException(); 259 | } 260 | int donePercents = (100 * done) / total; 261 | int doneLength = size * donePercents / 100; 262 | 263 | StringBuilder bar = new StringBuilder(iconLeftBoundary); 264 | for (int i = 0; i < size; i++) { 265 | if (i < doneLength) { 266 | bar.append(iconDone); 267 | } else { 268 | bar.append(iconRemain); 269 | } 270 | } 271 | bar.append(iconRightBoundary); 272 | return "\r" + bar ; 273 | } 274 | 275 | 276 | public static String printDetail() throws IOException, InterruptedException { 277 | String body1 = getDetail(); 278 | JSONArray array = new JSONArray(body1); 279 | if(array.isNull(0)){ 280 | return "\uD83D\uDCBE Free Space:" + diskSpaceRemain() + "\n\nThere are currently no downloads available"; 281 | } 282 | System.out.println("printDetail(): " + array.getJSONObject(0)); 283 | StringBuffer detail = new StringBuffer("\uD83D\uDCBE Free Space:" + diskSpaceRemain() + "\n"); 284 | for (int j = 0; j < array.length(); j++) { 285 | String name = array.getJSONObject(j).getString("name"); 286 | String size; 287 | if (array.getJSONObject(j).getDouble("total_size")/1024/1024<1024) { 288 | size = String.format("%.2f", array.getJSONObject(j).getDouble("total_size") / 1024 / 1024) + "M"; 289 | }else{ 290 | size = String.format("%.2f", array.getJSONObject(j).getDouble("total_size") / 1024 / 1024 / 1024) + "G"; 291 | } 292 | 293 | String status = array.getJSONObject(j).getString("state"); 294 | String hash = array.getJSONObject(j).getString("hash"); 295 | String category = array.getJSONObject(j).getString("category"); 296 | 297 | String categoryIcon = "\uD83D\uDCC1";//icon 298 | if(category.equals("tvseries")) categoryIcon = "\uD83D\uDCFA"; 299 | else if (category.equals("movie")) categoryIcon = "\uD83C\uDFAC"; 300 | else if (category.equals("anime")) categoryIcon = "\uD83C\uDFA8"; 301 | else if (category.equals("bbc")) categoryIcon = "\uD83D\uDD0D"; 302 | 303 | double progress = array.getJSONObject(j).getDouble("progress"); 304 | if (progress != 1) { 305 | double completed = array.getJSONObject(j).getDouble("completed"); 306 | double total = array.getJSONObject(j).getDouble("size"); 307 | 308 | String left; 309 | if (array.getJSONObject(j).getDouble("amount_left") /1024/1024<1024) { 310 | left = String.format("%.2f", array.getJSONObject(j).getDouble("amount_left") / 1024 / 1024) + "M"; 311 | }else{ 312 | left = String.format("%.2f", array.getJSONObject(j).getDouble("amount_left") / 1024 / 1024 / 1024) + "G"; 313 | } 314 | String percent = String.format("%.2f", completed / total * 100) + "%"; 315 | String speed = String.format("%.2f", array.getJSONObject(j).getDouble("dlspeed") / 1024 / 1024) + "M/s"; 316 | int eta = array.getJSONObject(j).getInt("eta") ; 317 | String strEta; 318 | if (eta > 60 && eta <3600) { 319 | long minutes = eta / 60;//Seconds to minutes 320 | eta = eta % 60;//remaining seconds 321 | strEta = minutes + "m" + eta + "s"; 322 | } else if (eta >= 3600 && eta < 8640000) { 323 | long hours = eta / 3600;//Seconds to hours 324 | eta = eta % 3600;//remaining seconds 325 | long minutes = eta / 60;//seconds to minutes 326 | eta = eta % 60;//remaining seconds 327 | strEta = hours + "h" + minutes + "m" + eta + "s" ; 328 | } else if (eta == 8640000) { 329 | strEta = "Cannot be completed" ; 330 | } else strEta = eta + "s" ; 331 | 332 | 333 | 334 | 335 | if(status.equals("pausedDL")){ 336 | detail.append("\n" + categoryIcon + "Name: ").append(name).append("\n").append("Size: ").append(size).append(" ").append("State: Paused").append("\n").append("Left:").append(left).append(" ").append("\nPercentage completed: ").append(percent).append("\n\uD83D\uDE49Resume:/resume").append(hash).append("\n----------------------------------------"); 337 | } else if (status.equals("downloading")) { 338 | double remain = completed / total; 339 | percent = String.format("%.2f", completed / total * 100) + "%" + "\n" + progressPercentage((int) Math.round(remain * 100), 100); 340 | detail.append("\n" + categoryIcon + "Name: ").append(name).append("\n").append("Size: ").append(size).append(" ").append("State: ").append("\n").append("Left:").append(left).append(" ").append("Speed: ").append(speed).append("\nPercentage completed: ").append(percent).append("\nTime left: ").append(strEta).append("\n\uD83D\uDE48Pause:/pause").append(hash).append("\n----------------------------------------"); 341 | } else { 342 | detail.append("\n" + categoryIcon + "Name: ").append(name).append("\n").append("Size: ").append(size).append(" ").append("State: ").append("\n").append("Left:").append(left).append(" ").append("Speed: ").append(speed).append("\nPercentage completed: ").append(percent).append("\nTime left: ").append(strEta).append("\n\uD83D\uDE4ADelete:/delete").append(hash).append("\n----------------------------------------"); 343 | } 344 | } else if (status.equals("uploading")) { 345 | String upspeed = String.format("%.2f", array.getJSONObject(j).getDouble("upspeed") / 1024 / 1024) + "M/s"; 346 | detail.append("\n" + categoryIcon + "Name: ").append(name).append("\n").append("Size: ").append(size).append(" ").append("State: Completed").append(" ").append("Speed: ").append(upspeed).append("\n\n\uD83D\uDE4ADelete:/delete").append(hash).append("\n----------------------------------------"); 347 | }else{ 348 | detail.append("\n" + categoryIcon + "Name: ").append(name).append("\n").append("Size: ").append(size).append(" ").append("State: Completed").append("\n\n\uD83D\uDE4ADelete:/delete").append(hash).append("\n----------------------------------------"); 349 | } 350 | } 351 | System.out.println(detail); 352 | return detail.toString(); 353 | } 354 | 355 | public static String diskSpaceRemain() throws IOException { 356 | String cookie = getCookie(); 357 | HttpPost httpGet = new HttpPost(Main.qbServer + "sync/maindata"); 358 | List nvps = new ArrayList<>(); 359 | nvps.add(new BasicNameValuePair("deleteFiles", "true"));//delete file 360 | httpGet.addHeader("Content-Type", "application/x-www-form-urlencoded"); 361 | httpGet.addHeader("cookie", cookie); 362 | httpGet.setEntity(new UrlEncodedFormEntity(nvps)); 363 | CloseableHttpClient httpclient = HttpClients.createDefault(); 364 | CloseableHttpResponse response2 = httpclient.execute(httpGet); 365 | String body1 = EntityUtils.toString(response2.getEntity()); 366 | JSONObject b = new JSONObject(body1); 367 | Double c = b.getJSONObject("server_state").getDouble("free_space_on_disk"); 368 | String freeSpace; 369 | 370 | if (c/1024/1024<1024) { 371 | freeSpace = String.format("%.2f", c / 1024 / 1024) + "M"; 372 | }else{ 373 | freeSpace = String.format("%.2f", c / 1024 / 1024 / 1024) + "G"; 374 | } 375 | 376 | // System.out.println(freeSpace); 377 | response2.close(); 378 | return freeSpace; 379 | 380 | 381 | } 382 | 383 | public static void deleteAll() throws IOException, InterruptedException {//The hashes of the torrents you want to delete. hashes can contain multiple hashes separated by |, to delete multiple torrents, or set to all, to delete all torrents. 384 | delete("all"); 385 | System.out.println("deleteAll(): Deleting all files"); 386 | 387 | } 388 | 389 | public static void pauseAll() throws IOException, InterruptedException {//The hashes of the torrents you want to delete. hashes can contain multiple hashes separated by |, to delete multiple torrents, or set to all, to delete all torrents. 390 | pause("all"); 391 | System.out.println("pauseAll(): Pausing all files"); 392 | } 393 | 394 | public static void resumeAll() throws IOException, InterruptedException {//The hashes of the torrents you want to delete. hashes can contain multiple hashes separated by |, to delete multiple torrents, or set to all, to delete all torrents. 395 | resume("all"); 396 | System.out.println("resumeAll(): Resuming all files"); 397 | } 398 | 399 | public static void delete(String hash) throws IOException, InterruptedException { 400 | String cookie = getCookie(); 401 | if (cookie.contains("SID")) { 402 | HttpPost httpPost = new HttpPost(Main.qbServer + "torrents/delete"); 403 | List nvps = new ArrayList<>(); 404 | nvps.add(new BasicNameValuePair("hashes", hash)); 405 | nvps.add(new BasicNameValuePair("deleteFiles", "true"));//delete file 406 | httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded"); 407 | httpPost.addHeader("cookie", cookie); 408 | httpPost.setEntity(new UrlEncodedFormEntity(nvps)); 409 | CloseableHttpClient httpclient = HttpClients.createDefault(); 410 | CloseableHttpResponse response2 = httpclient.execute(httpPost); 411 | System.out.println("delete(): " + response2); 412 | response2.close(); 413 | System.out.println("delete(): Delete" + hash); 414 | } 415 | 416 | } 417 | 418 | public static void pause(String hash) throws IOException, InterruptedException { 419 | String cookie = getCookie(); 420 | if (cookie.contains("SID")) { 421 | HttpPost httpPost = new HttpPost(Main.qbServer + "torrents/pause"); 422 | List nvps = new ArrayList<>(); 423 | nvps.add(new BasicNameValuePair("hashes", hash)); 424 | httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded"); 425 | httpPost.addHeader("cookie", cookie); 426 | httpPost.setEntity(new UrlEncodedFormEntity(nvps)); 427 | CloseableHttpClient httpclient = HttpClients.createDefault(); 428 | CloseableHttpResponse response2 = httpclient.execute(httpPost); 429 | System.out.println("pause(): " + response2); 430 | response2.close(); 431 | System.out.println("pause(): Pause" + hash); 432 | } 433 | 434 | } 435 | 436 | 437 | public static void resume(String hash) throws IOException, InterruptedException { 438 | String cookie = getCookie(); 439 | if (cookie.contains("SID")) { 440 | HttpPost httpPost = new HttpPost(Main.qbServer + "torrents/resume"); 441 | List nvps = new ArrayList<>(); 442 | nvps.add(new BasicNameValuePair("hashes", hash)); 443 | httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded"); 444 | httpPost.addHeader("cookie", cookie); 445 | httpPost.setEntity(new UrlEncodedFormEntity(nvps)); 446 | CloseableHttpClient httpclient = HttpClients.createDefault(); 447 | CloseableHttpResponse response2 = httpclient.execute(httpPost); 448 | System.out.println("resume(): " + response2); 449 | response2.close(); 450 | System.out.println("resume(): Resume" + hash); 451 | } 452 | } 453 | 454 | } 455 | -------------------------------------------------------------------------------- /src/main/java/TelegramBot.java: -------------------------------------------------------------------------------- 1 | import org.telegram.telegrambots.bots.TelegramLongPollingBot; 2 | import org.telegram.telegrambots.meta.api.methods.AnswerCallbackQuery; 3 | import org.telegram.telegrambots.meta.api.methods.send.SendMessage; 4 | import org.telegram.telegrambots.meta.api.methods.updatingmessages.EditMessageText; 5 | import org.telegram.telegrambots.meta.api.objects.CallbackQuery; 6 | import org.telegram.telegrambots.meta.api.objects.Message; 7 | import org.telegram.telegrambots.meta.api.objects.Update; 8 | import org.telegram.telegrambots.meta.api.objects.replykeyboard.InlineKeyboardMarkup; 9 | import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.InlineKeyboardButton; 10 | import org.telegram.telegrambots.meta.exceptions.TelegramApiException; 11 | import java.io.IOException; 12 | import java.util.*; 13 | 14 | public class TelegramBot extends TelegramLongPollingBot { 15 | 16 | 17 | public String botToken; 18 | public String botName; 19 | 20 | 21 | @Override 22 | public void onUpdateReceived(Update update) { 23 | 24 | if(update.hasMessage() && update.getMessage().hasText()) { 25 | String userSend = update.getMessage().getText(); 26 | System.out.println(userSend); 27 | long userId = update.getMessage().getFrom().getId(); 28 | if (userSend.startsWith("/hi")) { 29 | SendMessage message = new SendMessage(); 30 | message.setChatId(update.getMessage().getChatId()); 31 | message.setText(""" 32 | Hi, am here for your dissertation. 33 | """); 34 | try { 35 | execute(message); 36 | } catch (TelegramApiException e) { 37 | throw new RuntimeException(e); 38 | } 39 | } 40 | 41 | if (userSend.contains("http") || userSend.contains("magnet")) {//馒头链接 42 | System.out.println("This task has been added"); 43 | SendMessage message = new SendMessage(); 44 | message.setChatId(update.getMessage().getChatId()); 45 | message.setText("This task has been added"); 46 | sendDownloadMessage(userSend, message); 47 | } 48 | 49 | else if (userSend.startsWith("/delete")) { 50 | if (userSend.equals("/delete_all")) { 51 | System.out.println("delete all"); 52 | SendMessage message = new SendMessage(); 53 | message.setChatId(update.getMessage().getChatId()); 54 | message.setText("Are you sure to delete all?"); 55 | InlineKeyboardMarkup markupInline = new InlineKeyboardMarkup(); 56 | List> rowsInline = new ArrayList<>(); 57 | List rowInline = new ArrayList<>(); 58 | InlineKeyboardButton s = new InlineKeyboardButton(); 59 | InlineKeyboardButton r = new InlineKeyboardButton(); 60 | s.setText("\uD83D\uDEABCancel"); 61 | r.setText("\u26A0Confirm"); 62 | s.setCallbackData("delete_all_torrent_cancel"); 63 | r.setCallbackData("delete_all_torrent_confirm"); 64 | rowInline.add(s); 65 | rowInline.add(r); 66 | rowsInline.add(rowInline); 67 | markupInline.setKeyboard(rowsInline); 68 | message.setReplyMarkup(markupInline); 69 | try { 70 | execute(message); 71 | } catch (TelegramApiException e) { 72 | System.out.println(e.getMessage()); 73 | throw new RuntimeException(e); 74 | } 75 | } else { 76 | System.out.println("Delete a file"); 77 | SendMessage message = new SendMessage(); 78 | message.setChatId(update.getMessage().getChatId()); 79 | try { 80 | String hash = userSend.substring(userSend.indexOf("/delete") + 7); 81 | System.out.println(hash); 82 | if (Qbittorrent.isAbilityManageTorrent(hash, userId)) { 83 | Qbittorrent.delete(hash); 84 | Thread.sleep(1000); 85 | String detail = Qbittorrent.printDetail(); 86 | if (!detail.contains("no downloads available")) { 87 | setButtons(message); 88 | } 89 | message.setText(detail); 90 | }else message.setText("You do not have permission!"); 91 | 92 | } catch (IOException | InterruptedException e) { 93 | System.out.println(e.getMessage()); 94 | throw new RuntimeException(e); 95 | } 96 | try { 97 | execute(message); 98 | } catch (TelegramApiException e) { 99 | System.out.println(e.getMessage()); 100 | throw new RuntimeException(e); 101 | } 102 | } 103 | } 104 | 105 | else if (userSend.startsWith("/resume")) { 106 | if (userSend.equals("/resume_all")) { 107 | System.out.println("resume all"); 108 | SendMessage message = new SendMessage(); 109 | message.setChatId(update.getMessage().getChatId()); 110 | try { 111 | Qbittorrent.resumeAll(); 112 | message.setText("All download tasks are resuming!"); 113 | } catch (IOException | InterruptedException e) { 114 | throw new RuntimeException(e); 115 | } 116 | try { 117 | execute(message); 118 | Thread.sleep(2000); 119 | String detail = Qbittorrent.printDetail(); 120 | if (!detail.contains("no downloads available")) { 121 | setButtons(message); 122 | } 123 | message.setText(detail); 124 | execute(message); 125 | } catch (TelegramApiException | InterruptedException | IOException e) { 126 | System.out.println(e.getMessage()); 127 | throw new RuntimeException(e); 128 | } 129 | 130 | } else { 131 | SendMessage message = new SendMessage(); 132 | message.setChatId(update.getMessage().getChatId()); 133 | try { 134 | String hash = userSend.substring(userSend.indexOf("/resume") + 7); 135 | System.out.println(hash); 136 | if (Qbittorrent.isAbilityManageTorrent(hash, userId)) { 137 | Qbittorrent.resume(hash); 138 | Thread.sleep(2000); 139 | String detail = Qbittorrent.printDetail(); 140 | if (!detail.contains("no downloads available")) { 141 | setButtons(message); 142 | } 143 | message.setText(detail); 144 | }else message.setText("You do not have permission!"); 145 | } catch (IOException | InterruptedException e) { 146 | System.out.println(e.getMessage()); 147 | throw new RuntimeException(e); 148 | } 149 | try { 150 | execute(message); 151 | 152 | } catch (TelegramApiException e) { 153 | System.out.println(e.getMessage()); 154 | throw new RuntimeException(e); 155 | } 156 | 157 | } 158 | } 159 | 160 | 161 | 162 | else if (userSend.startsWith("/pause")) { 163 | if (userSend.equals("/pause_all")) { 164 | System.out.println("pause all"); 165 | SendMessage message = new SendMessage(); 166 | message.setChatId(update.getMessage().getChatId()); 167 | try { 168 | Qbittorrent.pauseAll(); 169 | message.setText("All download tasks are being suspended!"); 170 | } catch (IOException | InterruptedException e) { 171 | System.out.println(e.getMessage()); 172 | throw new RuntimeException(e); 173 | } 174 | try { 175 | execute(message); 176 | Thread.sleep(2000); 177 | String detail = Qbittorrent.printDetail(); 178 | if (!detail.contains("no downloads available")) { 179 | setButtons(message); 180 | } 181 | message.setText(detail); 182 | execute(message); 183 | 184 | } catch (TelegramApiException | IOException | InterruptedException e) { 185 | System.out.println(e.getMessage()); 186 | throw new RuntimeException(e); 187 | } 188 | 189 | } else { 190 | SendMessage message = new SendMessage(); 191 | message.setChatId(update.getMessage().getChatId()); 192 | try { 193 | String hash = userSend.substring(userSend.indexOf("/pause") + 6); 194 | if (Qbittorrent.isAbilityManageTorrent(hash, userId)) { 195 | Qbittorrent.pause(hash); 196 | Thread.sleep(2000); 197 | String detail = Qbittorrent.printDetail(); 198 | if (!detail.contains("no downloads available")) { 199 | setButtons(message); 200 | } 201 | message.setText(detail); 202 | }else message.setText("You do not have permission!"); 203 | } catch (IOException | InterruptedException e) { 204 | System.out.println(e.getMessage()); 205 | throw new RuntimeException(e); 206 | } 207 | try { 208 | execute(message); 209 | 210 | } catch (TelegramApiException e) { 211 | System.out.println(e.getMessage()); 212 | throw new RuntimeException(e); 213 | } 214 | } 215 | } 216 | 217 | 218 | 219 | 220 | else if (userSend.equals("/status")) { 221 | SendMessage message = new SendMessage(); 222 | try { 223 | message.setChatId(update.getMessage().getChatId()); 224 | String text = Qbittorrent.printDetail(); 225 | message.setText(text); 226 | if (!text.contains("no downloads available")) { 227 | setButtons(message); 228 | } 229 | execute(message); 230 | } catch (IOException | TelegramApiException | InterruptedException e) { 231 | System.out.println(e.getMessage()); 232 | throw new RuntimeException(e); 233 | } 234 | } 235 | 236 | else if (userSend.equals("/status_downloading")) { 237 | String status; 238 | try { 239 | status = Qbittorrent.printDownloadingDetail(); 240 | } catch (IOException | InterruptedException e) { 241 | System.out.println(e.getMessage()); 242 | throw new RuntimeException(e); 243 | } 244 | SendMessage message = new SendMessage(); 245 | long chatId = update.getMessage().getChatId(); 246 | message.setChatId(chatId); 247 | message.setText(status); 248 | if(status.contains("Name")){ 249 | InlineKeyboardMarkup markupInline = new InlineKeyboardMarkup(); 250 | List> rowsInline = new ArrayList<>(); 251 | List rowInline = new ArrayList<>(); 252 | InlineKeyboardButton k = new InlineKeyboardButton(); 253 | k.setText("Refresh"); 254 | k.setCallbackData("refreshDownloadingStatus"); 255 | rowInline.add(k); 256 | rowsInline.add(rowInline); 257 | markupInline.setKeyboard(rowsInline); 258 | message.setReplyMarkup(markupInline); 259 | } 260 | 261 | 262 | int messageId; 263 | try { 264 | Message response = execute(message); 265 | messageId = response.getMessageId();//messageID 266 | } catch (TelegramApiException e) { 267 | System.out.println(e.getMessage()); 268 | throw new RuntimeException(e); 269 | } 270 | 271 | } 272 | 273 | 274 | 275 | 276 | } 277 | 278 | if (update.hasCallbackQuery()) { 279 | long userId = update.getCallbackQuery().getFrom().getId(); 280 | System.out.println("userId: " + userId); 281 | String call_data = update.getCallbackQuery().getData(); 282 | update.getCallbackQuery().getMessage(); 283 | int message_id = update.getCallbackQuery().getMessage().getMessageId(); 284 | long chat_id = update.getCallbackQuery().getMessage().getChatId(); 285 | 286 | 287 | if(call_data.contains("edit_torrent")){ 288 | EditMessageText new_message = new EditMessageText(); 289 | new_message.setChatId(chat_id); 290 | new_message.setMessageId(message_id); 291 | setButtons(new_message); 292 | String answer; 293 | try { 294 | answer = Qbittorrent.printDetail(); 295 | } catch (IOException | InterruptedException e) { 296 | System.out.println(e.getMessage()); 297 | throw new RuntimeException(e); 298 | } 299 | String option = ""; 300 | switch (call_data) { 301 | case "edit_torrent_pause" -> { 302 | System.out.println(512); 303 | // System.out.println("更改之前的\n" + answer); 304 | answer = answer.replace("\uD83D\uDE4ADelete:/delete", "\uD83D\uDE48Pause:/pause"); 305 | answer = answer.replace("\uD83D\uDE49Resume:/resume", "\uD83D\uDE48Pause:/pause"); 306 | new_message.setText(answer); 307 | option = "Pause"; 308 | } 309 | case "edit_torrent_resume" -> { 310 | answer = answer.replace("\uD83D\uDE4ADelete:/delete", "\uD83D\uDE49Resume:/resume"); 311 | answer = answer.replace("\uD83D\uDE48Pause:/pause", "\uD83D\uDE49Resume:/resume"); 312 | new_message.setText(answer); 313 | option = "Resume"; 314 | } 315 | case "edit_torrent_delete" -> { 316 | answer = answer.replace("\uD83D\uDE48Delete:/pause", "\uD83D\uDE4ADelete:/delete"); 317 | answer = answer.replace("\uD83D\uDE49Resume:/resume", "\uD83D\uDE4ADelete:/delete"); 318 | new_message.setText(answer); 319 | option = "Delete"; 320 | } 321 | } 322 | try { 323 | execute(new_message); 324 | sendAnswerCallbackQuery(option + " done",false, update.getCallbackQuery()); 325 | } catch (TelegramApiException e) { 326 | System.out.println(e.getMessage()); 327 | e.printStackTrace(); 328 | try { 329 | sendAnswerCallbackQuery("Failed!",false, update.getCallbackQuery()); 330 | } catch (TelegramApiException ex) { 331 | throw new RuntimeException(ex); 332 | } 333 | }} 334 | 335 | 336 | 337 | if (call_data.equals("refreshDownloadingStatus")){ 338 | System.out.println(165); 339 | String answer; 340 | try { 341 | answer = Qbittorrent.printDownloadingDetail(); 342 | } catch (IOException | InterruptedException e) { 343 | throw new RuntimeException(e); 344 | } 345 | 346 | EditMessageText new_message = new EditMessageText(); 347 | new_message.setChatId(update.getCallbackQuery().getMessage().getChatId()); 348 | new_message.setMessageId(update.getCallbackQuery().getMessage().getMessageId()); 349 | new_message.setText(answer); 350 | InlineKeyboardMarkup markupInline = new InlineKeyboardMarkup(); 351 | List> rowsInline = new ArrayList<>(); 352 | List rowInline = new ArrayList<>(); 353 | InlineKeyboardButton k = new InlineKeyboardButton(); 354 | k.setText("Refresh"); 355 | k.setCallbackData("refreshDownloadingStatus"); 356 | rowInline.add(k); 357 | rowsInline.add(rowInline); 358 | markupInline.setKeyboard(rowsInline); 359 | if(!answer.contains("No tasks are being downloaded")){//If there is a task being downloaded, set a refresh button 360 | new_message.setReplyMarkup(markupInline); 361 | } 362 | try { 363 | execute(new_message); 364 | sendAnswerCallbackQuery("Refreshed",false, update.getCallbackQuery()); 365 | } catch (TelegramApiException e) { 366 | try { 367 | sendAnswerCallbackQuery("Failed!",false, update.getCallbackQuery()); 368 | } catch (TelegramApiException ex) { 369 | throw new RuntimeException(ex); 370 | } 371 | System.out.println(166); 372 | System.out.println(e.getMessage()); 373 | e.printStackTrace(); 374 | } 375 | } 376 | 377 | if (call_data.contains("delete_all_torrent_")){ 378 | if (call_data.equals("delete_all_torrent_cancel")){ 379 | EditMessageText new_message = new EditMessageText(); 380 | new_message.setChatId(chat_id); 381 | new_message.setMessageId(message_id); 382 | new_message.setText("Canceled"); 383 | try { 384 | execute(new_message); 385 | sendAnswerCallbackQuery("Canceled", false, update.getCallbackQuery()); 386 | } catch (TelegramApiException e) { 387 | System.out.println(e.getMessage()); 388 | throw new RuntimeException(e); 389 | } 390 | 391 | } else if (call_data.equals("delete_all_torrent_confirm")) { 392 | try { 393 | Qbittorrent.deleteAll(); 394 | EditMessageText new_message = new EditMessageText(); 395 | new_message.setChatId(chat_id); 396 | new_message.setMessageId(message_id); 397 | Thread.sleep(2000); 398 | new_message.setText(Qbittorrent.printDetail()); 399 | execute(new_message); 400 | sendAnswerCallbackQuery("All file have been deleted",true, update.getCallbackQuery()); 401 | 402 | } catch (IOException | InterruptedException | TelegramApiException e) { 403 | try { 404 | sendAnswerCallbackQuery("Failed!",false, update.getCallbackQuery()); 405 | } catch (TelegramApiException ex) { 406 | throw new RuntimeException(ex); 407 | } 408 | System.out.println(e.getMessage()); 409 | throw new RuntimeException(e); 410 | } 411 | 412 | 413 | } 414 | } 415 | 416 | 417 | } 418 | } 419 | 420 | private void setButtons(SendMessage message) { 421 | InlineKeyboardMarkup markupInline = new InlineKeyboardMarkup(); 422 | List> rowsInline = new ArrayList<>(); 423 | List rowInline = new ArrayList<>(); 424 | InlineKeyboardButton s = new InlineKeyboardButton(); 425 | InlineKeyboardButton r = new InlineKeyboardButton(); 426 | InlineKeyboardButton d = new InlineKeyboardButton(); 427 | s.setText("\uD83D\uDE48Pause"); 428 | r.setText("\uD83D\uDE49Resume"); 429 | d.setText("\uD83D\uDE4ADelete"); 430 | s.setCallbackData("edit_torrent_pause"); 431 | r.setCallbackData("edit_torrent_resume"); 432 | d.setCallbackData("edit_torrent_delete"); 433 | rowInline.add(s); 434 | rowInline.add(r); 435 | rowInline.add(d); 436 | rowsInline.add(rowInline); 437 | markupInline.setKeyboard(rowsInline); 438 | message.setReplyMarkup(markupInline); 439 | } 440 | 441 | private void setButtons(EditMessageText message) { 442 | InlineKeyboardMarkup markupInline = new InlineKeyboardMarkup(); 443 | List> rowsInline = new ArrayList<>(); 444 | List rowInline = new ArrayList<>(); 445 | InlineKeyboardButton s = new InlineKeyboardButton(); 446 | InlineKeyboardButton r = new InlineKeyboardButton(); 447 | InlineKeyboardButton d = new InlineKeyboardButton(); 448 | s.setText("\uD83D\uDE48Pause"); 449 | r.setText("\uD83D\uDE49Resume"); 450 | d.setText("\uD83D\uDE4ADelete"); 451 | s.setCallbackData("edit_torrent_pause"); 452 | r.setCallbackData("edit_torrent_resume"); 453 | d.setCallbackData("edit_torrent_delete"); 454 | rowInline.add(s); 455 | rowInline.add(r); 456 | rowInline.add(d); 457 | rowsInline.add(rowInline); 458 | markupInline.setKeyboard(rowsInline); 459 | message.setReplyMarkup(markupInline); 460 | } 461 | 462 | private void sendDownloadMessage(String userSend, SendMessage message) { 463 | try { 464 | Message m = execute(message); 465 | Qbittorrent.downloadTorrent(userSend); 466 | Thread.sleep(5000); 467 | // message = setButtons(message); 468 | EditMessageText message1 = new EditMessageText(); 469 | message1.setMessageId(m.getMessageId()); 470 | message1.setChatId(m.getChatId()); 471 | String a = Qbittorrent.printDownloadingDetail(); 472 | System.out.println(a);//Send a message about the list being downloaded after five seconds. 473 | message1.setText(a);//Execute download_status after each download task 474 | InlineKeyboardMarkup markupInline = new InlineKeyboardMarkup(); 475 | List> rowsInline = new ArrayList<>(); 476 | List rowInline = new ArrayList<>(); 477 | InlineKeyboardButton k = new InlineKeyboardButton(); 478 | k.setText("Refresh"); 479 | k.setCallbackData("refreshDownloadingStatus"); 480 | rowInline.add(k); 481 | rowsInline.add(rowInline); 482 | markupInline.setKeyboard(rowsInline); 483 | message1.setReplyMarkup(markupInline); 484 | execute(message1); 485 | } catch (IOException | InterruptedException | TelegramApiException e) { 486 | System.out.println(e.getMessage()); 487 | throw new RuntimeException(e); 488 | } 489 | } 490 | 491 | 492 | 493 | 494 | 495 | 496 | private void sendAnswerCallbackQuery(String text, boolean alert, CallbackQuery callbackquery) throws TelegramApiException{ 497 | AnswerCallbackQuery answerCallbackQuery = new AnswerCallbackQuery(); 498 | answerCallbackQuery.setCallbackQueryId(callbackquery.getId()); 499 | answerCallbackQuery.setShowAlert(alert); 500 | answerCallbackQuery.setText(text); 501 | execute(answerCallbackQuery); 502 | } 503 | 504 | 505 | @Override 506 | public String getBotUsername() { 507 | return botName; 508 | } 509 | 510 | @Override 511 | public String getBotToken() { 512 | return botToken; 513 | } 514 | } 515 | 516 | -------------------------------------------------------------------------------- /src/main/java/Torrent.java: -------------------------------------------------------------------------------- 1 | import java.util.Date; 2 | 3 | public class Torrent { 4 | public String id; 5 | private String category; 6 | public String title; 7 | public String size; 8 | public int seederNumber; 9 | public String uploadDate; 10 | public Date downloadDate; 11 | public String imageUrl; 12 | public String imdbUrl; 13 | 14 | public int number; 15 | 16 | 17 | 18 | 19 | } -------------------------------------------------------------------------------- /src/main/java/image/teleqbit1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Closty/Teleqbit/ce6b2a9fa1963b9ddfafe67a0a00cf48e45e132d/src/main/java/image/teleqbit1.PNG -------------------------------------------------------------------------------- /src/main/java/image/teleqbit2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Closty/Teleqbit/ce6b2a9fa1963b9ddfafe67a0a00cf48e45e132d/src/main/java/image/teleqbit2.jpg -------------------------------------------------------------------------------- /src/main/java/image/teleqbit3.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Closty/Teleqbit/ce6b2a9fa1963b9ddfafe67a0a00cf48e45e132d/src/main/java/image/teleqbit3.PNG --------------------------------------------------------------------------------