/* * Copyright 2016 Sam Sun <me@samczsun.com> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.samczsun.skype4j.internal; import com.eclipsesource.json.JsonArray; import com.eclipsesource.json.JsonObject; import com.eclipsesource.json.JsonValue; import com.samczsun.skype4j.exceptions.ConnectionException; import com.samczsun.skype4j.internal.utils.Encoder; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; public class Endpoints { private static Map<Class<?>, Converter<?>> converters = new HashMap<>(); static { converters.put(InputStream.class, HttpURLConnection::getInputStream); converters.put(HttpURLConnection.class, in -> in); converters.put(JsonObject.class, in -> Utils.parseJsonObject(in.getInputStream())); converters.put(JsonArray.class, in -> Utils.parseJsonArray(in.getInputStream())); converters.put(String.class, in -> StreamUtils.readFully(in.getInputStream())); converters.put(BufferedImage.class, in -> ImageIO.read(in.getInputStream())); } public static <T> T convert(Class<?> type, SkypeImpl skype, HttpURLConnection in) throws IOException { return (T) converters.get(type).convert(in); } public static final Provider<String> AUTHORIZATION = skype -> "skype_token " + skype.getSkypeToken(); public static final Provider<String> COOKIE = skype -> "skypetoken_asm=" + skype.getSkypeToken(); public static final Endpoints ACCEPT_CONTACT_REQUEST = new Endpoints( "https://api.skype.com/users/self/contacts/auth-request/%s/accept").skypetoken(); public static final Endpoints GET_JOIN_URL = new Endpoints("https://api.scheduler.skype.com/threads").skypetoken(); public static final Endpoints CHAT_INFO_URL = new Endpoints( "https://%sclient-s.gateway.messenger.live.com/v1/threads/%s/?view=msnp24Equivalent").cloud().regtoken(); public static final Endpoints CONVERSATION_PROPERTY_SELF = new Endpoints( "https://%sclient-s.gateway.messenger.live.com/v1/users/ME/conversations/%s/properties?name=%s") .cloud() .regtoken(); public static final Endpoints SEND_MESSAGE_URL = new Endpoints( "https://%sclient-s.gateway.messenger.live.com/v1/users/ME/conversations/%s/messages").cloud().regtoken(); public static final Endpoints MODIFY_MEMBER_URL = new Endpoints( "https://%sclient-s.gateway.messenger.live.com/v1/threads/%s/members/%s").cloud().regtoken(); public static final Endpoints CONVERSATION_PROPERTY_GLOBAL = new Endpoints( "https://%sclient-s.gateway.messenger.live.com/v1/threads/%s/properties?name=%s").cloud().regtoken(); public static final Endpoints ADD_MEMBER_URL = new Endpoints( "https://client-s.gateway.messenger.live.com/v1/threads/%s/members/8:%s").regtoken(); public static final Endpoints LOGIN_URL = new Endpoints("https://api.skype.com/login/skypetoken"); public static final Endpoints PING_URL = new Endpoints("https://web.skype.com/api/v1/session-ping").skypetoken(); public static final Endpoints TOKEN_AUTH_URL = new Endpoints("https://api.asm.skype.com/v1/skypetokenauth"); public static final Endpoints LOGOUT_URL = new Endpoints( "https://login.skype.com/logout?client_id=578134&redirect_uri=https%3A%2F%2Fweb.skype.com&intsrc=client-_-webapp-_-production-_-go-signin"); public static final Endpoints ENDPOINTS_URL = new Endpoints( "https://client-s.gateway.messenger.live.com/v1/users/ME/endpoints"); public static final Endpoints AUTH_REQUESTS_URL = new Endpoints( "https://api.skype.com/users/self/contacts/auth-request").skypetoken(); public static final Endpoints TROUTER_URL = new Endpoints("https://go.trouter.io/v2/a"); public static final Endpoints POLICIES_URL = new Endpoints("https://prod.tpc.skype.com/v1/policies").skypetoken(); public static final Endpoints REGISTRATIONS = new Endpoints( "https://prod.registrar.skype.com/v2/registrations").skypetoken(); public static final Endpoints THREAD_URL = new Endpoints("https://%sclient-s.gateway.messenger.live.com/v1/threads") .cloud() .regtoken(); public static final Endpoints SUBSCRIPTIONS_URL = new Endpoints( "https://%sclient-s.gateway.messenger.live.com/v1/users/ME/endpoints/SELF/subscriptions") .cloud() .regtoken(); public static final Endpoints MESSAGINGSERVICE_URL = new Endpoints( "https://%sclient-s.gateway.messenger.live.com/v1/users/ME/endpoints/%s/presenceDocs/messagingService") .cloud() .regtoken(); public static final Endpoints POLL = new Endpoints( "https://%sclient-s.gateway.messenger.live.com/v1/users/ME/endpoints/SELF/subscriptions/%s/poll") .cloud() .regtoken(); public static final Endpoints NEW_GUEST = new Endpoints("https://join.skype.com/api/v1/users/guests"); public static final Endpoints LEAVE_GUEST = new Endpoints("https://join.skype.com/guests/leave?threadId=%s"); public static final Endpoints ACTIVE = new Endpoints( "https://%sclient-s.gateway.messenger.live.com/v1/users/ME/endpoints/%s/active").cloud().regtoken(); public static final Endpoints LOAD_CHATS = new Endpoints( "https://client-s.gateway.messenger.live.com/v1/users/ME/conversations?startTime=%s&pageSize=%s&view=msnp24Equivalent&targetType=Passport|Skype|Lync|Thread|PSTN|Agent") .regtoken(); public static final Endpoints LOAD_MESSAGES = new Endpoints( "https://client-s.gateway.messenger.live.com/v1/users/ME/conversations/%s/messages?startTime=0&pageSize=%s&view=msnp24Equivalent|supportsMessageProperties&targetType=Passport|Skype|Lync|Thread") .regtoken(); public static final Endpoints OBJECTS = new Endpoints("https://api.asm.skype.com/v1/objects").defaultHeader( "Authorization", AUTHORIZATION); public static final Endpoints UPLOAD_IMAGE = new Endpoints( "https://api.asm.skype.com/v1/objects/%s/content/%s").defaultHeader("Authorization", AUTHORIZATION); public static final Endpoints IMG_STATUS = new Endpoints( "https://api.asm.skype.com/v1/objects/%s/views/%s/status").defaultHeader("Cookie", COOKIE); public static final Endpoints FETCH_IMAGE = new Endpoints( "https://api.asm.skype.com/v1/objects/%s/views/%s").defaultHeader("Authorization", AUTHORIZATION); public static final Endpoints VISIBILITY = new Endpoints( "https://%sclient-s.gateway.messenger.live.com/v1/users/ME/presenceDocs/messagingService") .cloud() .regtoken(); public static final Endpoints SEARCH_SKYPE_DIRECTORY = new Endpoints( "https://api.skype.com/search/users/any?keyWord=%s&contactTypes[]=skype").skypetoken(); public static final Endpoints GET_ALL_CONTACTS = new Endpoints( "https://contacts.skype.com/contacts/v1/users/%s/contacts?delta&$filter=type%%20eq%%20%%27skype%%27%%20or%%20type%%20eq%%20%%27msn%%27%%20or%%20type%%20eq%%20%%27pstn%%27%%20or%%20type%%20eq%%20%%27agent%%27%%20or%%20type%%20eq%%20%%27lync%%27&reason=%s") .skypetoken(); public static final Endpoints GET_CONTACT_BY_ID = new Endpoints( "https://contacts.skype.com/contacts/v1/users/%s/contacts?$filter=id%%20eq%%20%%27%s%%27&reason=default").skypetoken(); public static final Endpoints BLOCK_CONTACT = new Endpoints( "https://api.skype.com/users/self/contacts/%s/block").skypetoken(); public static final Endpoints UNBLOCK_CONTACT = new Endpoints( "https://api.skype.com/users/self/contacts/%s/unblock").skypetoken(); public static final Endpoints AUTHORIZE_CONTACT = new Endpoints( "https://api.skype.com/users/self/contacts/auth-request/%s/accept").skypetoken(); public static final Endpoints UNAUTHORIZE_CONTACT = new Endpoints( "https://client-s.gateway.messenger.live.com/v1/users/ME/contacts/8:%s").regtoken(); public static final Endpoints DECLINE_CONTACT_REQUEST = new Endpoints( "https://api.skype.com/users/self/contacts/auth-request/%s/decline").skypetoken(); public static final Endpoints UNAUTHORIZE_CONTACT_SELF = new Endpoints( "https://api.skype.com/users/self/contacts/%s").skypetoken(); public static final Endpoints AUTHORIZATION_REQUEST = new Endpoints( "https://api.skype.com/users/self/contacts/auth-request/%s").skypetoken(); @Deprecated public static final Endpoints CONTACT_INFO = new Endpoints( "https://api.skype.com/users/self/contacts/profiles").skypetoken(); public static final Endpoints PROFILE_INFO = new Endpoints("https://api.skype.com/users/batch/profiles").skypetoken(); public static final Endpoints RECONNECT_WEBSOCKET = new Endpoints( "https://go.trouter.io/v2/h?ccid=%s&dom=web.skype.com"); public static final Endpoints ELIGIBILITY_CHECK = new Endpoints("https://web.skype.com/api/v2/eligibility-check").skypetoken(); public static final Endpoints AGENT_INFO = new Endpoints("https://api.aps.skype.com/v1/agents?agentId=%s").skypetoken(); // todo implement // what other scopes are there? public static final Endpoints LANGUAGES_GET = new Endpoints("https://dev.microsofttranslator.com/api/languages?scope=text").skypetoken(); public static final Endpoints NEW_KEY = new Endpoints("https://kes.skype.com/v2/swx/newkey").skypetoken(); public static final Endpoints PETOKEN = new Endpoints("https://static.asm.skype.com/pes/v1/petoken").defaultHeader("Authorization", AUTHORIZATION); public static final Endpoints PROFILE = new Endpoints("https://api.skype.com/users/self/profile").skypetoken(); private boolean requiresCloud; private boolean requiresRegToken; private boolean requiresSkypeToken; private Map<String, Provider<String>> providers = new HashMap<>(); private String url; public String url() { return this.url; } private Endpoints(String url) { this.url = url; } public static EndpointConnection<HttpURLConnection> custom(String url, SkypeImpl skype, String... args) { if (skype.isShutdownRequested()) { throw new IllegalStateException("API is shut down"); } return new EndpointConnection(new Endpoints(url), skype, args).as(HttpURLConnection.class); } public EndpointConnection<HttpURLConnection> open(SkypeImpl skype, Object... args) { if (skype.isShutdownRequested()) { throw new IllegalStateException("API is shut down"); } return new EndpointConnection(this, skype, args).as(HttpURLConnection.class); } private Endpoints cloud() { this.requiresCloud = true; return this; } private Endpoints regtoken() { this.requiresRegToken = true; return this; } private Endpoints skypetoken() { this.requiresSkypeToken = true; return this; } private Endpoints defaultHeader(String key, Provider<String> val) { this.providers.put(key, val); return this; } public static class EndpointConnection<E_TYPE> { private Class<E_TYPE> clazz = (Class<E_TYPE>) HttpURLConnection.class; private Endpoints endpoint; private SkypeImpl skype; private Object[] args; private Map<String, String> headers = new HashMap<>(); private Map<String, String> cookies = new HashMap<>(); private Map<Predicate<Integer>, UncheckedFunction<E_TYPE>> errors = new HashMap<>(); private URL url; private String cause; private boolean dontConnect; private boolean redirect = true; private EndpointConnection(Endpoints endpoint, SkypeImpl skype, Object[] args) { this.endpoint = endpoint; this.skype = skype; this.args = args; header("User-Agent", "Mozilla/5.0 (Windows NT 10; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.73 Safari/537.36 Skype4J/" + SkypeImpl.VERSION); } public EndpointConnection<E_TYPE> header(String key, String val) { this.headers.put(key, val); return this; } public EndpointConnection<E_TYPE> cookie(String key, String val) { this.cookies.put(key, val); return this; } public EndpointConnection<E_TYPE> cookies(Map<String, String> cookies) { this.cookies.putAll(cookies); return this; } public EndpointConnection<E_TYPE> on(int code, UncheckedFunction<E_TYPE> action) { return on(x -> x == code, action); } public EndpointConnection<E_TYPE> on(Predicate<Integer> check, UncheckedFunction<E_TYPE> result) { this.errors.put(check, result); return this; } public EndpointConnection<E_TYPE> expect(int code, String cause) { return expect(x -> x == code, cause); } public EndpointConnection<E_TYPE> expect(Predicate<Integer> check, String cause) { this.cause = cause; return on(check, (connection) -> convert(clazz, skype, connection)); } public EndpointConnection<E_TYPE> noRedirects() { this.redirect = false; return this; } public <NEW_E_TYPE> EndpointConnection<NEW_E_TYPE> as(Class<NEW_E_TYPE> clazz) { this.clazz = (Class<E_TYPE>) clazz; return (EndpointConnection<NEW_E_TYPE>) this; } public EndpointConnection<E_TYPE> dontConnect() { this.dontConnect = true; return this; } public E_TYPE get() throws ConnectionException { return connect("GET", new byte[0]); } public E_TYPE delete() throws ConnectionException { return connect("DELETE", new byte[0]); } public E_TYPE post() throws ConnectionException { return connect("POST", new byte[0]); } public E_TYPE post(String data) throws ConnectionException { return connect("POST", data); } public E_TYPE post(JsonValue json) throws ConnectionException { return header("Content-Type", "application/json").connect("POST", json.toString()); } public E_TYPE put() throws ConnectionException { return connect("PUT", new byte[0]); } public E_TYPE put(String data) throws ConnectionException { return connect("PUT", data); } public E_TYPE put(JsonValue json) throws ConnectionException { return header("Content-Type", "application/json").connect("PUT", json.toString()); } public E_TYPE connect(String method, String data) throws ConnectionException { return this.connect(method, data != null ? data.getBytes(StandardCharsets.UTF_8) : new byte[0]); } public E_TYPE connect(String method, byte[] rawData) throws ConnectionException { if (!cookies.isEmpty()) { header("Cookie", serializeCookies(cookies)); } if (endpoint.requiresRegToken) { header("RegistrationToken", skype.getRegistrationToken()); } if (endpoint.requiresSkypeToken) { header("X-SkypeToken", skype.getSkypeToken()); } if (this.redirect) { this.on(code -> (code >= 301 && code <= 303) || code == 307 || code == 308, connection -> { skype.updateCloud(connection.getHeaderField("Location")); this.url = new URL(connection.getHeaderField("Location")); return this.connect(method, rawData); }); } for (Map.Entry<String, Provider<String>> provider : endpoint.providers.entrySet()) { header(provider.getKey(), provider.getValue().provide(skype)); } HttpURLConnection connection = null; try { if (this.url == null) { //todo could fail if cloud is updated? String surl = endpoint.url; if (endpoint.requiresCloud) { Object[] format = new Object[args.length + 1]; format[0] = skype.getCloud(); for (int i = 1; i < format.length; i++) { format[i] = args[i - 1].toString(); } surl = String.format(surl, format); } else if (args.length > 0) { Object[] format = new Object[args.length]; for (int i = 0; i < format.length; i++) { format[i] = args[i].toString(); } surl = String.format(surl, args); } this.url = new URL(surl); } connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod(method); connection.setInstanceFollowRedirects(false); for (Map.Entry<String, String> ent : headers.entrySet()) { connection.setRequestProperty(ent.getKey(), ent.getValue()); } if (!method.equalsIgnoreCase("GET")) { connection.setDoOutput(true); if (rawData != null) { connection.getOutputStream().write(rawData); } else { connection.getOutputStream().write(new byte[0]); } } if (!this.dontConnect) { if (connection.getHeaderField("Set-RegistrationToken") != null) { skype.setRegistrationToken(connection.getHeaderField("Set-RegistrationToken")); } for (Map.Entry<Predicate<Integer>, UncheckedFunction<E_TYPE>> entry : errors.entrySet()) { if (entry.getKey().test(connection.getResponseCode())) { try { return entry.getValue().apply(connection); } catch (Throwable t) { Utils.sneakyThrow(t); } } } throw ExceptionHandler.generateException(cause == null ? this.url.toString() : cause, connection); } else if (HttpURLConnection.class.isAssignableFrom(clazz)) { return (E_TYPE) connection; } else { throw new IllegalArgumentException( "DontConnect requested but did not request cast to HttpURLConnection"); } } catch (IOException e) { throw ExceptionHandler.generateException(cause, e); } finally { if (clazz != InputStream.class && clazz != HttpURLConnection.class) { if (connection != null) { connection.disconnect(); } } } } private String serializeCookies(Map<String, String> cookies) { StringBuilder result = new StringBuilder(); for (Map.Entry<String, String> cookie : cookies.entrySet()) { result.append(Encoder.encode(cookie.getKey())).append("=").append(Encoder.encode(cookie.getValue())).append(";"); } return result.toString(); } } public interface Provider<T> { T provide(SkypeImpl skype); } public interface Converter<T> { T convert(HttpURLConnection connection) throws IOException; } public interface UncheckedFunction<R> extends Function<HttpURLConnection, R> { default R apply(HttpURLConnection httpURLConnection) { try { return apply0(httpURLConnection); } catch (Throwable t) { Utils.sneakyThrow(t); } return null; } R apply0(HttpURLConnection httpURLConnection) throws Throwable; } }