package de.mrbesen.telegram; import java.io.IOException; import java.io.InputStream; import java.io.OutputStreamWriter; import java.net.URL; import java.net.URLEncoder; import java.util.LinkedList; import java.util.Scanner; import javax.net.ssl.HttpsURLConnection; import de.mrbesen.telegram.commands.JSONCommandHandler; import org.json.JSONArray; import org.json.JSONObject; import de.mrbesen.telegram.AsyncHandler.Callback; import de.mrbesen.telegram.AsyncHandler.Task; import de.mrbesen.telegram.MessageBuilder.AsyncSendable; import de.mrbesen.telegram.MessageBuilder.SendableMessage; import de.mrbesen.telegram.commands.CommandManager; import de.mrbesen.telegram.event.Event; import de.mrbesen.telegram.event.EventManager; import de.mrbesen.telegram.event.events.UserCallbackEvent; import de.mrbesen.telegram.event.events.UserSendAudioEvent; import de.mrbesen.telegram.event.events.UserSendMessageEvent; import de.mrbesen.telegram.log.Log; import de.mrbesen.telegram.log.Log4JLog; import de.mrbesen.telegram.log.SimpleLog; import de.mrbesen.telegram.objects.JSONBased.Member; import de.mrbesen.telegram.objects.TAudio; import de.mrbesen.telegram.objects.TMessage; import de.mrbesen.telegram.objects.TReplyMarkup; import de.mrbesen.telegram.objects.TUser; import de.mrbesen.telegram.objects.TUser.Status; public class TelegramAPI implements Runnable { private static final String API_URL = "https://api.telegram.org/bot"; private static final String TOKENREGEX = "^\\d{4,10}:[\\w-]{12,64}$"; private static final int TELEGRAMFILESIZELIMIT = 20000000;//20MB filesize https://core.telegram.org/bots/api#sending-files public static final String APIVERSION = "3.8";//May 18, 2019 private int msg_offset = 0; private int updateInterval = 60; private String apikey; private String botname; private Thread thread; private boolean run = true; private boolean longpolling = true; public boolean isLongpolling() { return longpolling; } public void setLongpolling(boolean longpolling) { this.longpolling = longpolling; } private String helpmessage = "generic helppage\nuse TelegramAPI.setHelpText(java.lang.String) to change this."; //stats protected int fetchedUpdates = 0; protected long start = 0; private LinkedList users = new LinkedList<>(); private CommandManager cmdmgr = new CommandManager(this); private EventManager evntmgr = new EventManager(); //async private AsyncHandler async = new AsyncHandler(this); public static Callback IOE400supressor = new Callback() { @Override public Void call(Throwable t) throws Throwable { if(t instanceof IOException) { if(t.getMessage().startsWith("Server returned HTTP response code: 400")) return null; } throw t; } }; Log log = new Log4JLog(); /** * please only use if bot is not used in groups * @param apikey */ public TelegramAPI(String apikey) { botname = ""; if (!apikey.matches(TOKENREGEX) ) { throw new IllegalArgumentException("Invalid API key: " + apikey); } this.apikey = apikey; } public TelegramAPI(String apikey, String botname) { this.botname = botname; if (!apikey.matches(TOKENREGEX) ) { throw new IllegalArgumentException("Invalid API key: " + apikey); } this.apikey = apikey; } public void start() { if(thread == null) { run = true; thread = new Thread(this, "TelegramAPI"); thread.start(); } else { throw new IllegalStateException("Still Running."); } } public void request(Task task) { async.enque(task); } public void requestAsync(String request, String parameter) { this.async.enque(request, parameter); } public JSONObject request(String request, String parameter) throws IOException { return request(request, parameter, true); } public JSONObject request(String request, String parameter, boolean logging) throws IOException { //do https stuff URL url = new URL(API_URL + apikey + "/" + request); HttpsURLConnection con = (HttpsURLConnection)url.openConnection(); con.setDoInput(true); con.setDoOutput(true); OutputStreamWriter wr = new OutputStreamWriter(con.getOutputStream()); wr.write(parameter); wr.flush(); if(logging) log.log( "request: " + request + " content " + parameter + " -> " + con.getResponseCode() + ", " + con.getResponseMessage()); if(con.getResponseCode() == 200) { return new JSONObject(readfromIS(con.getInputStream())); } else { String errdesc = "[No description available]"; try { //try to read error message JSONObject returned = new JSONObject(readfromIS(con.getErrorStream())); errdesc = returned.getString("description"); } catch(Throwable ignore) { } throw new APIError(parameter, request, con.getResponseCode(), null, errdesc); //throw new IOException("API request returned HTTP " + con.getResponseCode() + " (" + con.getResponseMessage() + ") for action " + request + "\nurl: " + url.toString() + "\nparams: " + parameter + "\nerror description: " + errdesc); } } protected String readfromIS(InputStream is) { Scanner s = new Scanner(is); StringBuilder sb = new StringBuilder(); while(s.hasNextLine()) { sb.append(s.nextLine()).append('\n'); } s.close(); return sb.toString(); } public void sendAsync(long chatid, String msg) { sendMessage(new MessageBuilder().setAsync().setReciver(chatid).setText(msg).build()); } public TMessage sendMessage(TUser user, String text) { MessageBuilder builder = new MessageBuilder(); builder.setReciver(user.getID()); builder.setText(text); return sendMessage(builder.build()); } public TMessage sendMessage(SendableMessage msg) { try { if(msg instanceof AsyncSendable) { AsyncSendable asyncm = (AsyncSendable) msg; Callback adapter = getCallbackAdapter(this); adapter.next = asyncm.callback; Task t = new Task(msg.getCommand(), msg.getQ(), adapter); t.setExceptionhandl(asyncm.excpt == null ? IOE400supressor : asyncm.excpt); async.enque(t); } else { JSONObject o = request(msg.getCommand(), msg.getQ(), true); return new TMessage(o.getJSONObject("result"), this); } } catch(IOException e) { log.log("", e); } return null; } public void sendTypedMessage(final String msg, final TUser user, final int seconds) { new Thread(new Runnable() { @Override public void run() { user.sendStatus(Status.Typing); try { Thread.sleep(seconds*1000); } catch (InterruptedException e) { } sendMessage(new MessageBuilder().setText(msg).setReciver(user.getID()).build()); } }).start(); } public void answerCallbackQuery(String callbackid, String text, boolean async) { if(callbackid == null) return; if(async) { requestAsync("answerCallbackQuery", "callback_query_id=" + callbackid + "&text=" + text); } else { try { request("answerCallbackQuery", "callback_query_id=" + callbackid + "&text=" + text); } catch(IOException e) { e.printStackTrace(); } } } /** * creates internal APIEror, when message is not modyfied! * @param newCaption * @param chatid * @param msg_id * @param rm * @param async * @param clb */ public void updateCaption(final String newCaption, long chatid, int msg_id, TReplyMarkup rm, boolean async, Callback clb) { try { String rply = ""; if(rm != null) rply = "&reply_markup=" + URLEncoder.encode(rm.toJSONString(), "UTF-8"); String q = "chat_id=" + chatid + "&message_id=" + msg_id + "&caption=" + URLEncoder.encode(newCaption, "UTF-8") + rply; if(async) { Task t = new Task("editMessageCaption", q); t.setExceptionhandl(new Callback() { @Override public Void call(Throwable j) throws Throwable { if(j instanceof APIError) { String errmsg = ((APIError) j).getMessage(); if(errmsg.equals("Bad Request: message is not modified") || errmsg.equals("Bad Request: message to edit not found")) { //both have code 400 return null; } } throw j; } }); this.async.enque(t); } else { request("editMessageCaption", q); } } catch(IOException e) { log.log("", e); } } public void updateMarkup(long chatid, int msg_id, TReplyMarkup rm, boolean async) { try { if(rm == null) return;//nope String q = "chat_id=" + chatid + "&message_id=" + msg_id + "&reply_markup=" + URLEncoder.encode(rm.toJSONString(), "UTF-8"); if(async) { this.async.enque("editMessageReplyMarkup", q); } else { request("editMessageReplyMarkup", q); } } catch(IOException e) { log.log("", e); } } public void stop() { run = false; thread.interrupt(); Thread.yield();//try to not get into that loop while(isRunning()) { thread.interrupt(); Thread.yield(); try { Thread.sleep(10); } catch(InterruptedException e) {} } log.log("tapi stoped."); thread = null; } @Override public void run() { start = System.currentTimeMillis(); if(longpolling) { while(run) { fetchUpdates(); fetchedUpdates++; } } else { while(run) { long runstart = System.currentTimeMillis(); fetchUpdates(); fetchedUpdates++; try { int wait = (int) (updateInterval - (System.currentTimeMillis() - runstart)); if(wait > 0) Thread.sleep(wait); } catch (InterruptedException e) { break; } } } } public boolean isRunning() { return thread.isAlive(); } private void fetchUpdates() { try { processUpdates(request("getUpdates", "offset=" + msg_offset + "&timeout=" + (longpolling ? updateInterval : 1), false)); } catch (IOException e) { log.log("error getting updates.", e); try { Thread.sleep(100); } catch(InterruptedException ignored) {} } } private void processUpdates(JSONObject object) { if(object == null) return; JSONArray arr_results = object.getJSONArray("result"); for(int i = 0; !arr_results.isNull(i); i++) { JSONObject entry = arr_results.getJSONObject(i); TelegramAPIUpdate upd = new TelegramAPIUpdate(entry, this); msg_offset = (upd.update_id+1 > msg_offset ? upd.update_id+1 : msg_offset); } } public TUser getUser(String name) { for(TUser us : users) { if(us.getName().equals(name)) return us; } return null; } public TUser getUser(int id) { for(TUser us : users) { if(us.getID() == id) return us; } TUser u = new TUser(id, this); users.add(u); return u; } /** * gets a user by id from the known user lists, * if no user found, it creates a new User and adds it to the list. * @param json * @return */ public TUser getUser(JSONObject json) { int id = json.getInt("id"); TUser user = getUser(id); if(user != null) return user; user = new TUser(json, this); users.add(user); return user; } public void setHelpText(String helptext) { helpmessage = ( helptext == null ? "" : helptext); } /** * Set the logging method, use {@link Log4JLog} for usage of LOG4J, * or {@link SimpleLog} for usage of System.out.println(); * use Null t odisable logging the default is {@link SimpleLog} * @param l */ public void setLog(Log l) { if(l == null) log = new Log(); else log = l; } public CommandManager getCommandManager() { return cmdmgr; } public EventManager getEventManager() { return evntmgr; } /** * seconds to wait for longpolling or milliseconds to wait petween shortpolling * @param d */ public void setUpdateInterval(int d) { if(d < 0) throw new IllegalArgumentException("UpdateInterval is not allowed to be negative."); updateInterval = d; } public String getHelpMessage() { return helpmessage; } public int getupdateInterval() { return updateInterval; } public float getUpdatesperSecond() { if(start == 0) return -1; return fetchedUpdates / ((float) (System.currentTimeMillis() - start) / 1000); } public static boolean isSendable(long filesize) { return filesize < TELEGRAMFILESIZELIMIT; } public static Callback getCallbackAdapter(TelegramAPI api) { return new Callback() { @Override public TMessage call(JSONObject j) { return new TMessage(j.getJSONObject("result"), api); } }; } protected class TelegramAPIUpdate { protected int update_id = 0; private TMessage msg = null; private boolean isCommand = false; protected TelegramAPIUpdate(JSONObject json, TelegramAPI api) { update_id = json.getInt("update_id"); if(json.has("message")) { msg = new TMessage(json.getJSONObject("message"), api); String text = msg.getText(); if(text != null) { if(text.matches("^\\/(\\w*)(@(\\w*))?")) { text = text.substring(1);//remove '/' isCommand = true; if(text.contains("@")) {//check name int at = text.indexOf('@'); int end = text.indexOf(' ', at+1); if(end == -1) end = text.length(); String botname = text.substring(at+1, end); if(botname.equalsIgnoreCase(api.botname)) { cmdmgr.onCommand(text, msg.getFrom(), msg); } else api.log.log("other botname found: " + botname); } else { cmdmgr.onCommand(text, msg.getFrom(), msg); } } else { Event e = null; if(getMsg().has(Member.audio)) { e = new UserSendAudioEvent(getUser(), new TAudio((JSONObject) getMsg().get(Member.audio))); } else if(getMsg().has(Member.video)) { } else if(getMsg().has(Member.document)) { } else if(getMsg().has(Member.invoice)) { } else if(getMsg().has(Member.location)) { } else if(getMsg().has(Member.video_note)) { } else if(getMsg().has(Member.game)) { } else if(getMsg().has(Member.contact)) { } else if(getMsg().has(Member.sticker)) { } else { e = new UserSendMessageEvent(getMsg()); } getEventManager().callEvent(e); } } } else if(json.has("callback_query")) { JSONObject cbq = json.getJSONObject("callback_query"); TUser from = api.getUser(cbq.getJSONObject("from")); String data = cbq.getString("data"); String id = cbq.getString("id"); TMessage msg = new TMessage(cbq.getJSONObject("message"), api); getEventManager().callEvent(new UserCallbackEvent(from, data, id, msg)); } } protected int getUpdate_id() { return update_id; } protected TMessage getMsg() { return msg; } protected TUser getUser() { return msg.getFrom(); } protected boolean isCommand() { return isCommand; } } public enum JSONObjectType { User, Chat, Message, MessageEntity, Audio, Document, Game, Sticker, Video, Voice, Videonote, Location, Venue, Contact; boolean isArray = false; void setArray(boolean b) { isArray = b; } } public class APIError extends IOException { /** * */ private static final long serialVersionUID = 1L; String params; String method; int returncode; public String getParams() { return params; } public String getMethod() { return method; } public int getReturncode() { return returncode; } public void setParams(String params) { this.params = params; } public void setMethod(String method) { this.method = method; } public void setReturncode(int returncode) { this.returncode = returncode; } public APIError(String params, String method, int returncode, Throwable t, String msg) { super(msg, t); this.params = params; this.method = method; this.returncode = returncode; } public APIError(String arg0, Throwable arg1) { super(arg0, arg1); } public APIError(String arg0) { super(arg0); } public APIError(Throwable arg0) { super(arg0); } } }