package de.mrbesen.telegram; 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.commands.FeedbackCommand; import de.mrbesen.telegram.event.Event; import de.mrbesen.telegram.event.EventManager; import de.mrbesen.telegram.event.events.*; 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.TFile; 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; import lombok.Getter; import lombok.Setter; import org.json.JSONArray; import org.json.JSONObject; import java.io.IOException; import java.io.InputStream; import java.io.OutputStreamWriter; import java.net.ConnectException; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLEncoder; import java.util.*; import java.util.function.*; import java.util.stream.Collector; import static java.util.stream.Collector.Characteristics.IDENTITY_FINISH; import static java.util.stream.Collector.Characteristics.UNORDERED; public class TelegramAPI implements Runnable { private static final String API_URL_DEFAULT = "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.10";//Jan 16, 2021 private int msg_offset = 0; private int updateInterval = 60; private String apikey; private String botname; private Thread thread; private boolean run = true; @Getter @Setter private boolean longpolling = true; @Setter @Getter private boolean disableFeedback = false; private String apiurl; private String helpmessage = "generic helppage\nuse TelegramAPI.setHelpText(java.lang.String) to change this."; //stats protected int fetchedUpdates = 0; protected long start = 0; private Map users = new TreeMap<>(); @Setter private List admins = new LinkedList<>(); //required for feedback private CommandManager cmdmgr = new CommandManager(this); private EventManager evntmgr = new EventManager(); private FeedbackCommand feedbackCmd = null; //async private AsyncHandler async = null; 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) { this(null, apikey); } public TelegramAPI(String apiurl, String apikey) { this(apiurl, apikey, ""); } public TelegramAPI(String apiurl, String apikey, String botname) { this.apiurl = (apiurl == null ? API_URL_DEFAULT : apiurl); this.botname = botname != null ? botname : ""; if (!apikey.matches(TOKENREGEX) ) { throw new IllegalArgumentException("Invalid API key: " + apikey); } this.apiurl += this.apikey = apikey; } public void start() { start(1); } //start with a specified threadcount for async operations public void start(int threadCount) { if(thread == null) { async = new AsyncHandler(this, threadCount); if(!disableFeedback) { //init Feedback feedbackCmd = new FeedbackCommand(this, admins); cmdmgr.registerCommand("feedback", feedbackCmd); } run = true; thread = new Thread(this, "TelegramAPI"); thread.start(); } else { throw new IllegalStateException("Still Running."); } } public void addAdmin(long admin) { admins.add(admin); } public boolean isAdmin(long admin) { return admins.contains(admin); } public void request(Task task) { async.enque(task); } public void requestAsync(String request, String parameter, long userid) { this.async.enque(request, parameter, userid); } public JSONObject request(String request, String parameter, long userid) throws IOException { return request(request, parameter, userid,true); } public JSONObject request(String request, String parameter, long userid, boolean logging) throws IOException { //do https stuff boolean toomany = true; //für retry after 429 error int trycount = 0; while(toomany) { toomany = false; ++trycount; URL url = new URL(apiurl + "/" + request); HttpURLConnection con = (HttpURLConnection) url.openConnection(); con.setDoInput(true); con.setDoOutput(true); OutputStreamWriter wr = new OutputStreamWriter(con.getOutputStream()); wr.write(parameter); wr.flush(); if (logging) { String small = parameter; if (small.length() > 60) { small = small.substring(0, Math.min(60, small.length())) + "..."; } log.log("request: " + request + " content " + small + " -> " + con.getResponseCode() + " " + con.getResponseMessage()); } int response = con.getResponseCode(); if (response == 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) { } log.log("Request failed error: \"" + errdesc + "\" detailed request: " + request + "?" + parameter); //catch 429 too many error if (response == 429) { if(trycount < 10) toomany = true; //try to read timeout //too Many Requests: retry after 19 int timeout = 10; int idx = errdesc.lastIndexOf(" "); try { timeout = Integer.parseInt(errdesc.substring(idx))+1; System.out.println("timeout read: " + timeout); } catch(NumberFormatException | StringIndexOutOfBoundsException e ) {} try { System.out.println("Got 429 -> sleep for " + timeout + " seconds"); Thread.sleep(timeout * 1000); } catch(InterruptedException e) {} if(trycount < 10) continue; } else if(response == 403) { if(errdesc.equals("Forbidden: bot was blocked by the user")) { if(userid != 0) evntmgr.callEvent(new UserBlockedBotEvent(getUser(userid))); } } 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); } } //unreachable code? throw new IllegalStateException(); } 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(), msg.getUserid(), adapter); t.setExceptionhandl(asyncm.excpt == null ? IOE400supressor : asyncm.excpt); async.enque(t); } else { JSONObject o = request(msg.getCommand(), msg.getQ(), msg.getUserid(), true); return new TMessage(o.getJSONObject("result"), this); } } catch(IOException e) { log.log("", e); } return null; } /** * is always async * @param cmds cmdname -> description */ public void setBotCommand(Map cmds) { //build json obj JSONArray arr = cmds.entrySet().stream().map(e -> new JSONObject().put("command", e.getKey()).put("description", e.getKey())).collect(new Collector() { @Override public Supplier supplier() { return JSONArray::new; } @Override public BiConsumer accumulator() { return JSONArray::put; } @Override public BinaryOperator combiner() { return (a,b) -> { b.forEach(a::put); return b; }; } @Override public Function finisher() { return a -> a; } @Override public Set characteristics() { Set c = new HashSet<>(); c.add(IDENTITY_FINISH); c.add(UNORDERED); return c; } }); //commit Task t = new Task("setMyCommands", "commands=" + arr.toString(), 0, null); async.enque(t); } public void setFeedbackCallback(Function clb) { if(!disableFeedback && feedbackCmd != null) feedbackCmd.setFeedbackCallback(clb); } public TFile getFile(final String fileid) throws IOException { JSONObject jfile = request("getFile", "file_id=" + fileid, 0); return new TFile(jfile.getJSONObject("result")); } public void getFile(final String fileid, Consumer callback) { async.enque(new Task("getFile", "file_id=" + fileid, 0, new Callback() { @Override public Void call(JSONObject j) throws Throwable { TFile file = new TFile(j.getJSONObject("result")); callback.accept(file); 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, 0); } else { try { request("answerCallbackQuery", "callback_query_id=" + callbackid + "&text=" + text, 0); } 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, chatid); 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, chatid); } } 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, chatid); } else { request("editMessageReplyMarkup", q, chatid); } } catch(IOException e) { log.log("", e); } } public void stop() { run = false; if(thread == null) return; thread.interrupt(); thread = null; log.log("TelegramAPI stoped."); } @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), 0,false)); } catch (IOException e) { log.log("error getting updates.", e); try { Thread.sleep(e instanceof ConnectException ? 10000 : 500); } 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.values()) { if(us.getName().equals(name)) return us; } return null; } public TUser getUser(long id, boolean createIfNotExitsts) { TUser u = users.get(id); if(u != null || !createIfNotExitsts) return u; u = new TUser(id, this); evntmgr.callEvent(new NewUserEvent(u)); users.put(id, u); return u; } public TUser getUser(long id) { return getUser(id, true); } /** * 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) { long id = json.getLong("id"); TUser user = getUser(id, false); if(user != null) return user; user = new TUser(json, this); users.put(id, 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 between 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; 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+))?(\\s.*)?")) { //is a command text = text.substring(1);//remove '/' 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 = new UserSendMessageEvent(msg); //call feedback cmd first if(!disableFeedback) { if (feedbackCmd.onMsg((UserSendMessageEvent) e)) { e = null; } } getEventManager().callEvent(e); return; //do not process other events } } Event e = null; //process media events if(msg.has(Member.audio)) { e = new UserSendAudioEvent(msg); } else if(msg.has(Member.video)) { //TODO } else if(msg.has(Member.document)) { e = new UserSendDocumentEvent(msg); } else if(msg.has(Member.photo)) { e = new UserSendPhotoEvent(msg); } else if(msg.has(Member.invoice)) { //TODO } else if(msg.has(Member.location)) { //TODO } else if(msg.has(Member.video_note)) { //TODO } else if(msg.has(Member.game)) { //TODO } else if(msg.has(Member.contact)) { //TODO } else if(msg.has(Member.sticker)) { //TODO } 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); UserCallbackEvent event = new UserCallbackEvent(from, data, id, msg); if(!disableFeedback) { if (!feedbackCmd.onCallback(event)) getEventManager().callEvent(event); } else { getEventManager().callEvent(event); } } } } 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); } } }